Schedule List
alphav0.2.0Time-anchored agenda list — time or range, title, optional description and icons, per-row links, framed or bare.
Context
Use for conference programs, course curricula, podcast / broadcast schedules, meeting agendas, recipe-step lists with timing — anywhere a time-ordered list of activities renders. Migration origin: kasder kas-social-front-v0 events/[id]/page.tsx Program section. Composes naturally inside event-detail-page-01 (deferred sibling).
Installation
pnpm dlx shadcn@latest initpnpm dlx shadcn@latest add @ilinxa/schedule-listAdd -fixtures for dummy data:
pnpm dlx shadcn@latest add @ilinxa/schedule-list-fixturesCLI 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
Program
- 09:00
Kayıt ve Karşılama Kahvaltısı
Katılımcı kayıtları ve networking
- 10:00
Açılış Konuşması
Dernek Başkanı tarafından açılış
- 11:00
Panel: Geleceğin Şehirleri
Uzman panelimizle interaktif tartışma
- 13:00
Öğle Yemeği
Networking lunch
- 14:30
Çalıştaylar
Paralel çalıştay oturumları
- 17:00
Kapanış ve Networking
Sonuç bildirisi ve kokteyl
Demo source
"use client"; import { Mic } from "lucide-react";import { Tabs, TabsContent, TabsTrigger } from "@/components/ui/tabs";import { SwipeTabsList } from "@/components/site/swipe-tabs-list";import { ScheduleList } from "./schedule-list";import { dummySchedule, dummyScheduleTr } from "./dummy-data";import type { ScheduleListItem } from "./types"; const speakers: Record<string, { name: string; image: string }> = { "2": { name: "Prof. Ahmet Yılmaz", image: "https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=80&h=80&fit=crop&crop=faces", }, "3": { name: "Dr. Elif Kaya", image: "https://images.unsplash.com/photo-1573496359142-b8d87734a5a2?w=80&h=80&fit=crop&crop=faces", },}; function SpeakerAugmentedRow({ item }: { item: ScheduleListItem }) { const speaker = speakers[item.id]; return ( <div className="flex items-start gap-4 p-4 bg-card rounded-xl border border-border/50"> <div className="w-auto min-w-20 shrink-0"> <span className="text-lg font-bold text-primary">{item.time}</span> </div> {speaker && ( <img src={speaker.image} alt={speaker.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"> <h4 className="font-semibold text-foreground">{item.title}</h4> {speaker && ( <p className="text-xs text-primary mt-0.5 inline-flex items-center gap-1"> <Mic aria-hidden="true" className="w-3 h-3" /> {speaker.name} </p> )} {item.description && ( <p className="text-sm text-muted-foreground mt-0.5"> {item.description} </p> )} </div> </div> );} export default function ScheduleListDemo() { return ( <Tabs defaultValue="default" className="w-full"> <SwipeTabsList> <TabsTrigger value="default">Default (TR)</TabsTrigger> <TabsTrigger value="english">English</TabsTrigger> <TabsTrigger value="bare">Bare rows</TabsTrigger> <TabsTrigger value="links">Icons + links</TabsTrigger> <TabsTrigger value="custom">Custom renderItem</TabsTrigger> </SwipeTabsList> {/* 1. Default — Turkish, mirrors the kasder source verbatim */} <TabsContent value="default" className="mt-6"> <div className="max-w-3xl mx-auto"> <ScheduleList heading="Program" items={dummyScheduleTr} /> </div> </TabsContent> {/* 2. English — same shape, English content */} <TabsContent value="english" className="mt-6"> <div className="max-w-3xl mx-auto"> <ScheduleList heading="Program" items={dummySchedule} /> </div> </TabsContent> {/* 3. Bare rows — framed=false for sidebar/widget contexts */} <TabsContent value="bare" className="mt-6"> <div className="max-w-md mx-auto rounded-2xl border border-border/50 bg-card p-4"> <ScheduleList heading="Today" headingAs="h3" framed={false} items={dummySchedule.slice(0, 4)} /> </div> </TabsContent> {/* 4. Icons + links — items with icon + clickable href */} <TabsContent value="links" className="mt-6"> <div className="max-w-3xl mx-auto"> <p className="mb-3 text-sm text-muted-foreground"> Items with icons (Coffee / Users / Utensils) and links — the entire row is wrapped in a polymorphic link when <code>href</code> is provided. </p> <ScheduleList heading="Conference Day 1" items={dummySchedule.map((item) => ({ ...item, href: `#talk-${item.id}`, }))} /> </div> </TabsContent> {/* 5. Custom renderItem — speaker-augmented row */} <TabsContent value="custom" className="mt-6"> <div className="max-w-3xl mx-auto"> <p className="mb-3 text-sm text-muted-foreground"> Custom <code>renderItem</code> slot — drops a speaker avatar adjacent to the time column on items 2 + 3. </p> <ScheduleList heading="Program with Speakers" items={dummySchedule} renderItem={(item) => <SpeakerAugmentedRow item={item} />} /> </div> </TabsContent> </Tabs> );} Usage
When to use
Reach for ScheduleList when you need a vertical time-anchored list of activities — conference programs, course curricula, podcast / broadcast schedules, meeting agendas, recipe steps with timing. Each row is a time + title + optional description triplet, with optional icons and optional per-row links.
Minimal example
import { ScheduleList } from "@/components/schedule-list";
<ScheduleList
heading="Program"
items={[
{ id: "1", time: "09:00", title: "Welcome", description: "Check-in" },
{ id: "2", time: "10:00", endTime: "10:30", title: "Opening Keynote" },
{ id: "3", time: "11:00", title: "Panel: Future Cities" },
]}
/>;Item shape — what's required
id,time,title— required.endTime— optional. When present, time renders as{time}{separator}{endTime}(default separator" - ", configurable vialabels.timeRangeSeparator).description— optional. Description block omitted when missing.icon— optional Lucide-style component (ComponentType<{ className?: string }>). When present, sits left of the time column.href— optional. When present +linkComponentprovided, the entire row becomes clickable via the overlay-link pattern.
Frame toggle
Default framed: true — each row gets card chrome (bg-card rounded-xl border) and rows are spaced space-y-4. Pass framed={false} for bare rows in dense contexts (sidebars, widgets) — chrome dropped, rows tighten to space-y-2.
Custom rendering
renderItem(item)— bypass the default row layout entirely. Useful for embedding speaker avatars, status badges, per-item actions.renderTime(item)— bypass the default time string renderer (e.g. transform "09:00" → "9:00 AM"). Used inside the default row layout whenrenderItemis not supplied.
Polymorphic root
import NextLink from "next/link";
<ScheduleList
heading="Conference Day 1"
items={items.map((item) => ({ ...item, href: `/talks/${item.id}` }))}
linkComponent={NextLink}
/>;Empty state
When items is empty, the component renders emptyState (if provided) OR a <p role="status"> with labels.emptyText(default "No items scheduled.").
Notes
- Renders semantic
<ol role="list">— schedules ARE ordered. - Section
aria-labelledbyresolves to the heading id (computed viauseId) — only whenheadingis supplied. - Decorative icons get
aria-hidden="true"; link's accessible name is the item title. - All hover transitions gated via
motion-safe:— reduced-motion users see static rows.
Features
- Vertical schedule list — time + title + optional description per row
- Optional time range (start - end), separator localizable
- Optional Lucide-style icon per row
- Polymorphic per-row link via linkComponent + per-item href
- Frame toggle (framed=true card chrome / framed=false bare rows)
- Custom renderItem + renderTime slots
- Optional section heading with configurable level (h2/h3/h4)
- Soft-failure on optional fields (description / icon / endTime / href)
- i18n via labels object
- Semantic <ol role='list'> — schedules are ordered
- Empty state slot with labels fallback