Skip to content
ilinxa/pro-ui

Info List

alphav0.2.0

Card-framed list of icon-prefixed rows — primary and secondary text, optional actions, per-row links, comfortable and compact variants.

Category: Data DisplayUpdated: 2026-08-11Created: 2026-05-02Author: ilinxa

Context

Use for sidebar info cards on detail pages — Event Details, Contact, Address, Account Settings, Product Specs, Restaurant Info, Listing Attributes. Comfortable variant: stacked primary/secondary/action rows with separators (kasder Etkinlik Bilgileri pattern). Compact variant: inline single-line rows, no separators (kasder İletişim pattern). Migration origin: kasder kas-social-front-v0 events/[id]/page.tsx Etkinlik Bilgileri + İletişim sidebar cards.

Installation

Initialize shadcn (once per project)Seeds lib/utils.ts and components.json. Skip if you've already used any shadcn component.
pnpm dlx shadcn@latest init
Install the component
pnpm dlx shadcn@latest add @ilinxa/info-list

Add -fixtures for dummy data:

pnpm dlx shadcn@latest add @ilinxa/info-list-fixtures

CLI can't resolve @ilinxa? The namespace is listed in the official shadcn registry directory, so current CLIs need no configuration. If yours can't resolve it (older or pinned versions, self-hosted mirrors), register it manually in components.json:

"registries": {
  "@ilinxa": "https://ui.ilinxa.com/r/{name}.json"
}

Preview

Etkinlik Bilgileri

  • 31 Mayıs 2026 Pazar

    09:00 - 18:00

  • İstanbul Kongre Merkezi

    Darülbedai Caddesi No:3, 34367 Harbiye/Şişli/İstanbul

    Haritada Gör
  • 500 Kişilik Kapasite

    423 kişi kayıtlı

Demo source

demo.tsxtsx
"use client"; import { CircleUser } from "lucide-react";import { Tabs, TabsContent, TabsTrigger } from "@/components/ui/tabs";import { SwipeTabsList } from "@/components/site/swipe-tabs-list";import { InfoList } from "./info-list";import {  dummyContactItems,  dummyContactItemsTr,  dummyEventDetails,  dummyEventDetailsTr,} from "./dummy-data";import type { InfoListItem } from "./types"; const peopleItems = [  {    id: "p1",    name: "Prof. Dr. Ahmet Yılmaz",    role: "Şehir Plancısı",    image:      "https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=80&h=80&fit=crop&crop=faces",  },  {    id: "p2",    name: "Dr. Elif Kaya",    role: "Çevre Mühendisi",    image:      "https://images.unsplash.com/photo-1573496359142-b8d87734a5a2?w=80&h=80&fit=crop&crop=faces",  },  {    id: "p3",    name: "Mehmet Demir",    role: "Akıllı Şehir Uzmanı",    image:      "https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=80&h=80&fit=crop&crop=faces",  },]; const peopleItemsAsInfoListItems: InfoListItem[] = peopleItems.map((p) => ({  id: p.id,  icon: CircleUser,  primary: p.name,  secondary: p.role,})); export default function InfoListDemo() {  return (    <Tabs defaultValue="default" className="w-full">      <SwipeTabsList>        <TabsTrigger value="default">Default (TR)</TabsTrigger>        <TabsTrigger value="compact">Compact (TR)</TabsTrigger>        <TabsTrigger value="stacked">Both stacked</TabsTrigger>        <TabsTrigger value="bare">Bare</TabsTrigger>        <TabsTrigger value="custom">Custom renderItem</TabsTrigger>      </SwipeTabsList>       {/* 1. Default — kasder Etkinlik Bilgileri verbatim */}      <TabsContent value="default" className="mt-6">        <div className="max-w-md mx-auto">          <InfoList            heading="Etkinlik Bilgileri"            variant="comfortable"            items={dummyEventDetailsTr}          />        </div>      </TabsContent>       {/* 2. Compact — kasder İletişim verbatim */}      <TabsContent value="compact" className="mt-6">        <div className="max-w-md mx-auto">          <InfoList            heading="İletişim"            variant="compact"            items={dummyContactItemsTr}          />        </div>      </TabsContent>       {/* 3. Both stacked — full sidebar shape */}      <TabsContent value="stacked" className="mt-6">        <div className="max-w-md mx-auto space-y-6">          <InfoList            heading="Etkinlik Bilgileri"            variant="comfortable"            items={dummyEventDetailsTr}          />          <InfoList            heading="İletişim"            variant="compact"            items={dummyContactItemsTr}          />        </div>      </TabsContent>       {/* 4. Bare — framed=false for embedded use */}      <TabsContent value="bare" className="mt-6">        <div className="max-w-md mx-auto rounded-2xl border border-border/50 bg-card p-6 space-y-6">          <div>            <h2 className="text-xl font-bold mb-4">Listing Details</h2>            <InfoList              framed={false}              variant="comfortable"              items={dummyEventDetails}            />          </div>          <div>            <h2 className="text-xl font-bold mb-4">Contact</h2>            <InfoList              framed={false}              variant="compact"              items={dummyContactItems}            />          </div>        </div>      </TabsContent>       {/* 5. Custom renderItem — speakers with avatars */}      <TabsContent value="custom" className="mt-6">        <div className="max-w-md mx-auto space-y-2">          <p className="text-sm text-muted-foreground">            <code>renderItem(item)</code> — full row takeover. Here: avatar            replaces the icon column for a speaker list.          </p>          <InfoList            heading="Konuşmacılar"            variant="comfortable"            items={peopleItemsAsInfoListItems}            renderItem={(item) => {              const person = peopleItems.find((p) => p.id === item.id);              return (                <div className="flex items-center gap-3">                  <img                    src={person?.image}                    alt={person?.name ?? ""}                    loading="lazy"                    className="w-12 h-12 rounded-full object-cover shrink-0 border-2 border-primary/20"                  />                  <div className="min-w-0 flex-1">                    <p className="font-medium text-foreground">                      {item.primary}                    </p>                    <p className="text-sm text-muted-foreground">                      {item.secondary}                    </p>                  </div>                </div>              );            }}          />        </div>      </TabsContent>    </Tabs>  );} 

Usage

When to use

Reach for InfoList for sidebar info cards on detail pages — Event Details, Contact Info, Address, Account Settings, Product Specs, Restaurant Info, Listing Attributes. Two visual flavors: comfortable (stacked primary / secondary / action with separators between rows) and compact(inline single-line rows, no separators).

Minimal example

import { InfoList } from "@/components/info-list";
import { Calendar, MapPin, Users } from "lucide-react";

<InfoList
  heading="Event Details"
  variant="comfortable"
  items={[
    { id: "date", icon: Calendar, primary: "May 31, 2026", secondary: "09:00 - 18:00" },
    { id: "loc",  icon: MapPin,   primary: "Istanbul Conference Center", secondary: "Şişli/Istanbul" },
    { id: "cap",  icon: Users,    primary: "500-person capacity", secondary: "423 registered" },
  ]}
/>;

Two variants

  • comfortable — stacked content (primary bold + secondary muted + optional action). Icon w-5 h-5 text-primary. Separators between rows by default. Use for sidebar info cards with rich per-row content.
  • compact — inline single-line rows. Icon w-4 h-4 text-muted-foreground. No separators by default. Use for contact lists, footer columns, dense info summaries. Compact ignores secondary and action — they imply a richer row than the variant supports.

Linked rows

Pass href per item to wrap primary in the polymorphic linkComponent. Consumer passes full URLs — no auto-prefixing for tel: / mailto:.

<InfoList
  variant="compact"
  items={[
    { id: "phone", icon: Phone, primary: "+90 (212) 555 0123", href: "tel:+902125550123" },
    { id: "email", icon: Mail,  primary: "info@example.com",   href: "mailto:info@example.com" },
  ]}
/>

Action slot

Comfortable rows can render an actionReactNode below the secondary line — typically a link button (e.g. "View on map"):

{
  id: "location",
  icon: MapPin,
  primary: event.location,
  secondary: event.address,
  action: (
    <Button variant="link" className="px-0 h-auto">
      View on map <ExternalLink className="w-3 h-3" />
    </Button>
  ),
}

Polymorphic linkComponent

import NextLink from "next/link";

<InfoList
  variant="compact"
  linkComponent={NextLink}
  items={[
    { id: "profile", icon: User, primary: "View profile", href: "/profile" },
  ]}
/>

Custom renderItem

Bypass the default row layout entirely with renderItem(item) — useful for swapping icon for an avatar, embedding a status badge, or any non-icon row anatomy:

<InfoList
  items={items}
  renderItem={(item) => (
    <div className="flex items-center gap-3">
      <Avatar src={people[item.id].image} />
      <div>
        <p className="font-medium">{item.primary}</p>
        <p className="text-sm text-muted-foreground">{item.secondary}</p>
      </div>
    </div>
  )}
/>

Frame + separators

  • framed: true (default) — card chrome (bg-card rounded-2xl p-6 border). Pass framed={false} for embedded use inside an existing card.
  • separated — auto-defaults per variant: comfortable=true, compact=false. CSS top-border on each row except the first; override by passing the prop.

Empty state

Empty items array → renders emptyState if provided, else <p role="status"> with labels.emptyText(default "No information.").

Notes

  • Renders <ul role="list">; section aria-labelledby resolves to the heading id (via useId) when heading is supplied.
  • Decorative icons are aria-hidden="true"; link's accessible name is the visible primary text.
  • Heading defaults to h3 (sidebar info cards typically nest under page h2). Bump via headingAs="h2" when standalone.

Features

  • Two variants — comfortable (stacked, separators, w-5 primary icon) and compact (inline, no separators, w-4 muted icon)
  • Required icon + primary; optional secondary + action + href per item
  • Polymorphic per-row link via linkComponent (default 'a') — works with tel: / mailto: / etc.
  • Frame toggle (framed=true card chrome / framed=false bare)
  • Separator toggle (auto-defaults per variant; overridable)
  • Custom renderItem slot for full row takeover
  • Optional section heading with configurable level (h2/h3/h4)
  • Soft-failure on optional fields
  • Empty state slot + labels.emptyText fallback
  • i18n via labels object
  • <ul role='list'> semantics + section aria-labelledby
  • Compact variant intentionally ignores secondary + action (documented limitation)

Tags

info-listdetailsinfolistsidebar

Dependencies

npm peer deps: lucide-react@^1.11.0