Şablonlar
Ayrıntı paneli
Listeden seçilen kaydın ayrıntıları sayfadan ayrılmadan yan panelde; önceki ve sonraki kayda geçiş, etkinlik ve düzenleme eylemi.
Ayrıntı paneli, bir listedeki kaydı sayfayı değiştirmeden açar. Kullanıcı listedeki yerini kaybetmez; panel kapanınca odak seçtiği satıra döner. Aynı panel Liste ve filtreler bloğundaki satır menüsünden de açılır.
Önizleme
Bölüm başlığı “Önizleme”Bir satıra basın: panel satır sonundan kayarak gelir. Önceki ve sonraki düğmeleriyle panel kapanmadan kayıtlar arasında geçin. Sayfa başlığındaki marka ve mod seçimi önizlemeyi de değiştirir.
pnpm add @bigbang/ui| Veri | Tür | Not |
|---|---|---|
record |
DetailRecord | null |
null paneli kapatır; kayan çıkış sırasında son kayıt çizilmeye devam eder. |
position |
{ index, total, onMove } |
Verilirse önceki ve sonraki düğmeleri ile “6 kayıttan 2.” konumu görünür. |
openRecord |
number |
Blok örneğinde açılışta açık kayıt (1’den başlar). |
RecordDetailDrawer ayrı dışa aktarılır; başka bir liste aynı paneli kullanabilir.
import { Button, DescriptionList, Drawer, Heading, Icon, IconButton, Inline, ListItem, Stack, StatusBadge, Text, useToast } from '@bigbang/ui';import type { IconName, StatusKey } from '@bigbang/ui';import { useState } from 'react';import type { Lang } from '../../patterns/lang';import { AppShellTemplate } from '../../templates/app-shell/AppShellTemplate';
const copy = { tr: { title: 'Kayıtlar', description: 'Bir kaydı seçin; ayrıntılar sayfadan ayrılmadan yan panelde açılır.', listLabel: 'Kayıt listesi', record: (index: number) => `Kayıt ${index}`, categories: { a: 'Kategori A', b: 'Kategori B', c: 'Kategori C' }, status: 'Durum', category: 'Kategori', created: 'Oluşturulma', count: 'Adet', owner: 'Sahibi', ownerValue: 'Kullanıcı A', activity: 'Etkinlik', events: [ { icon: 'pencil', title: 'Ad güncellendi', meta: '2 saat önce' }, { icon: 'user', title: 'Kullanıcı B eklendi', meta: 'Dün' }, { icon: 'plus', title: 'Kayıt oluşturuldu', meta: '3 gün önce' }, ], previous: 'Önceki kayıt', next: 'Sonraki kayıt', position: (index: number, total: number) => `${total} kayıttan ${index}.`, close: 'Kapat', edit: 'Düzenle', editing: (name: string) => `${name} düzenleme için açıldı.`, }, en: { title: 'Records', description: 'Pick a record; its details open in a side panel without leaving the page.', listLabel: 'Record list', record: (index: number) => `Record ${index}`, categories: { a: 'Category A', b: 'Category B', c: 'Category C' }, status: 'Status', category: 'Category', created: 'Created', count: 'Count', owner: 'Owner', ownerValue: 'User A', activity: 'Activity', events: [ { icon: 'pencil', title: 'Name updated', meta: '2 hours ago' }, { icon: 'user', title: 'User B added', meta: 'Yesterday' }, { icon: 'plus', title: 'Record created', meta: '3 days ago' }, ], previous: 'Previous record', next: 'Next record', position: (index: number, total: number) => `${index} of ${total}`, close: 'Close', edit: 'Edit', editing: (name: string) => `${name} opened for editing.`, },};
export type DetailRecord = { id: string; name: string; status: StatusKey; category: 'a' | 'b' | 'c'; day: number; count: number };
const statusCycle: StatusKey[] = ['active', 'pending', 'completed', 'archived'];const categoryCycle = ['a', 'b', 'c'] as const;
function makeRecords(lang: Lang): DetailRecord[] { return Array.from({ length: 6 }, (_, index) => ({ id: `record-${index + 1}`, name: copy[lang].record(index + 1), status: statusCycle[index % statusCycle.length] ?? 'active', category: categoryCycle[index % categoryCycle.length] ?? 'a', day: index + 20, count: (index + 1) * 50, }));}
const dateFormat = (lang: Lang) => new Intl.DateTimeFormat(lang === 'tr' ? 'tr-TR' : 'en-GB', { day: 'numeric', month: 'long', year: 'numeric' });
export type RecordDetailDrawerProps = { lang: Lang; /** The record to show; `null` closes the drawer. */ record: DetailRecord | null; onOpenChange: (open: boolean) => void; /** Position in the list, for the previous and next buttons. Leave it out to hide them. */ position?: { index: number; total: number; onMove: (step: -1 | 1) => void };};
/** * The details of one record in a Drawer: status, a description list, recent activity, previous and * next buttons and an edit action. The list and list-filters blocks share it. */export function RecordDetailDrawer({ lang, record, onOpenChange, position }: RecordDetailDrawerProps) { const t = copy[lang]; const toast = useToast(); const date = dateFormat(lang); // The last record stays drawn while the drawer slides out. const [shown, setShown] = useState(record); if (record !== null && record !== shown) setShown(record); const current = record ?? shown; return ( <Drawer open={record !== null} onOpenChange={onOpenChange} size="md" title={current?.name ?? ''} {...(current ? { description: `${t.categories[current.category]} · ${date.format(new Date(2026, 9, current.day))}` } : {})} closeLabel={t.close} footer={ <> <Drawer.Close> <Button variant="secondary">{t.close}</Button> </Drawer.Close> <Button startIcon={<Icon name="pencil" />} onClick={() => { if (current) toast.show({ title: t.editing(current.name) }); }} > {t.edit} </Button> </> } > {current ? ( <Stack gap="xl"> {position ? ( <Inline justify="between" align="center"> <StatusBadge status={current.status} /> <Inline gap="xs" align="center"> <Text size="sm" tone="subtle" numeric> {t.position(position.index + 1, position.total)} </Text> <IconButton icon="chevron-left" label={t.previous} variant="tertiary" size="sm" disabled={position.index === 0} onClick={() => position.onMove(-1)} /> <IconButton icon="chevron-right" label={t.next} variant="tertiary" size="sm" disabled={position.index === position.total - 1} onClick={() => position.onMove(1)} /> </Inline> </Inline> ) : ( <StatusBadge status={current.status} /> )} <DescriptionList layout="stacked" dividers items={[ { term: t.category, details: t.categories[current.category] }, { term: t.created, details: date.format(new Date(2026, 9, current.day)) }, { term: t.count, details: current.count }, { term: t.owner, details: t.ownerValue }, ]} /> <section aria-labelledby={`activity-${current.id}-${lang}`}> <Stack gap="sm"> <Heading level={3} size="sm" id={`activity-${current.id}-${lang}`}> {t.activity} </Heading> <ListItem.List divided> {t.events.map((event) => ( <ListItem key={event.title} size="sm" leading={<Icon name={event.icon as IconName} tone="subtle" />} title={event.title} meta={event.meta} /> ))} </ListItem.List> </Stack> </section> </Stack> ) : null} </Drawer> );}
export type DetailDrawerBlockProps = { lang?: Lang; /** Opens the drawer on this record (1-based), as after a click in the list. */ openRecord?: number;};
/** A record list whose rows open their details in a side drawer, with previous and next inside the drawer. */export function DetailDrawerBlock({ lang = 'tr', openRecord }: DetailDrawerBlockProps) { const t = copy[lang]; return ( <AppShellTemplate lang={lang} current="records" title={t.title} description={t.description} width="md"> <DetailDrawerContent lang={lang} openRecord={openRecord} /> </AppShellTemplate> );}
function DetailDrawerContent({ lang, openRecord }: { lang: Lang; openRecord: number | undefined }) { const t = copy[lang]; const [records] = useState(() => makeRecords(lang)); const [openIndex, setOpenIndex] = useState<number | null>(openRecord === undefined ? null : openRecord - 1); const record = openIndex === null ? null : (records[openIndex] ?? null);
return ( <> <ListItem.List divided aria-label={t.listLabel} className="rounded-md border border-line-subtle"> {records.map((item, index) => ( <ListItem key={item.id} render={<button type="button" />} selected={index === openIndex} title={item.name} subtitle={t.categories[item.category]} trailing={<StatusBadge status={item.status} size="sm" />} onClick={() => setOpenIndex(index)} /> ))} </ListItem.List> <RecordDetailDrawer lang={lang} record={record} onOpenChange={(open) => { if (!open) setOpenIndex(null); }} {...(openIndex === null ? {} : { position: { index: openIndex, total: records.length, onMove: (step: -1 | 1) => setOpenIndex(openIndex + step) } })} /> </> );}- AppShell şablonu (
width="md") veh1“Kayıtlar”. - Düğme satırlarından oluşan ListItem listesi; açık kayıt
selectedile işaretlenir. - Drawer (
side="end",size="md"): başlık kayıt adı, açıklama kategori ve tarih. - Panel gövdesi: StatusBadge, konum metni ve önceki/sonraki IconButton’ları,
stackedDescriptionList,h3“Etkinlik” altında son üç olay. - Alt bilgi:
Drawer.Closeile “Kapat” ve birincil “Düzenle”; düzenleme bir Toast ile onaylanır.
Hareket
Bölüm başlığı “Hareket”Panel motion hazır ayarlarıyla satır sonundan kayar, arka plan solar; 640 px altında alttan çıkan sayfaya döner. Azaltılmış harekette kayma kalkar, yalnızca kısa solma kalır. Kayıtlar arasında geçişte panel yerinde kalır, yalnızca içerik değişir.
Erişilebilirlik
Bölüm başlığı “Erişilebilirlik”- Panel
dialogrolündedir ve kayıt adıyla adlandırılır; açıklama kategori ve tarihi okur. - Açılınca odak panele girer ve içinde tutulur; Escape, dışarı tıklama ve Kapat paneli kapatır, odak basılan satıra döner.
- Önceki ve sonraki düğmelerinin adları “Önceki kayıt” ve “Sonraki kayıt”tır; uçlarda devre dışıdır. Başlık değişince panelin adı da değişir.
- Satırlar tek bir düğmedir; durum rozeti satırın adına eklenir, rengi tek başına anlam taşımaz.
- “Etkinlik”
h3’tür; panel başlığıh2düzeyindedir.
Kullanılan bileşenler
Bölüm başlığı “Kullanılan bileşenler”Drawer, ListItem, DescriptionList, StatusBadge, IconButton, Button, Toast. Kalıplar: Bildirimler.