İçeriğe geç
BigBang

Sık açılanlar

    ↑↓Gezin↵AçEscKapat
    EN

    Ş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.

    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.

    Canlı örnek, StorybookStorybook'ta aç
    Canlı örnek, StorybookStorybook'ta aç
    Canlı örnek, StorybookStorybook'ta aç
    Terminal window
    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) } })}
    />
    </>
    );
    }
    1. AppShell şablonu (width="md") ve h1 “Kayıtlar”.
    2. Düğme satırlarından oluşan ListItem listesi; açık kayıt selected ile işaretlenir.
    3. Drawer (side="end", size="md"): başlık kayıt adı, açıklama kategori ve tarih.
    4. Panel gövdesi: StatusBadge, konum metni ve önceki/sonraki IconButton’ları, stacked DescriptionList, h3 “Etkinlik” altında son üç olay.
    5. Alt bilgi: Drawer.Close ile “Kapat” ve birincil “Düzenle”; düzenleme bir Toast ile onaylanır.

    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.

    • Panel dialog rolü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ığı h2 düzeyindedir.

    Drawer, ListItem, DescriptionList, StatusBadge, IconButton, Button, Toast. Kalıplar: Bildirimler.