import * as React from 'react'; import { Avatar, AvatarFallback } from '@/components/avatar'; import { Badge } from '@/components/badge'; import { DataTable } from '@/components/data-table'; import type { DataTableColumn } from '@/components/data-table'; import { DatePicker, DateRangePicker } from '@/components/date-picker'; import { Input } from '@/components/input'; import { Label } from '@/components/label'; import { Progress } from '@/components/progress'; import { RangeSlider } from '@/components/range-slider'; import { Tree } from '@/components/tree'; import type { TreeNode } from '@/components/tree'; import { TreeSelect } from '@/components/tree-select'; import type { TreeSelectNode } from '@/components/tree-select'; import { Upload, UploadDropzone } from '@/components/upload'; import { VirtualList } from '@/components/virtual-list'; import { FileIcon, ImageIcon } from '@/icons'; import type { Entry } from './types'; /* The data widgets take structured input rather than a closed set of values, so their pages are compositions with real fixtures instead of variant grids. */ /* `email`, `location` and `seats` exist for the wide table below — the columns that have to scroll past a pinned one. The narrow compositions ignore them. */ interface Row { id: number; name: string; role: string; status: 'active' | 'invited'; email: string; location: string; seats: number; } const ROWS: Row[] = [ { id: 1, name: 'Aysel Mammadova', role: 'Admin', status: 'active', email: 'aysel@cbar.az', location: 'Baku', seats: 12 }, { id: 2, name: 'Rashad Aliyev', role: 'Engineer', status: 'active', email: 'rashad@cbar.az', location: 'Baku', seats: 8 }, { id: 3, name: 'Nigar Huseynova', role: 'Designer', status: 'invited', email: 'nigar@cbar.az', location: 'Ganja', seats: 3 }, { id: 4, name: 'Elvin Guliyev', role: 'Engineer', status: 'invited', email: 'elvin@cbar.az', location: 'Sumqayit', seats: 1 }, ]; const COLUMNS: DataTableColumn[] = [ { key: 'name', title: 'Name', dataIndex: 'name', sortable: true }, { key: 'role', title: 'Role', dataIndex: 'role', sortable: true }, { key: 'status', title: 'Status', dataIndex: 'status', align: 'right', render: (value) => ( {String(value)} ), }, ]; /** * Controlled sorting. * * A named component, not an inline closure — and it carries an explicit * `displayName`, which every stateful demo in the registry does. * * `chrome/lib/codegen.ts` prints a tag by looking the component up in `exportIndex`, * which only knows the kit's own exports; a local demo falls through to * `displayName || fn.name`, and a production `showcase:build` mangles the * latter. Without the assignment the copy button on this composition reads * `` in the built site. It still is not a snippet anyone can paste — no * consumer has a `ControlledSortDemo` — but it names what is on screen instead * of a minifier's letter. * * With `sort` and `onSortChange` both passed, the table stops reordering rows * and only reports the intent — swap the `useMemo` for a query and the same * markup is server-side sorting. */ function ControlledSortDemo() { const [sort, setSort] = React.useState<{ key: React.Key; direction: 'asc' | 'desc' } | null>({ key: 'name', direction: 'asc', }); const [query, setQuery] = React.useState(''); const rows = React.useMemo(() => { const needle = query.trim().toLowerCase(); const filtered = needle ? ROWS.filter((row) => `${row.name} ${row.role}`.toLowerCase().includes(needle)) : ROWS; if (!sort) return filtered; const key = sort.key as keyof Row; const sorted = [...filtered].sort((a, b) => String(a[key]).localeCompare(String(b[key]), undefined, { numeric: true }) ); return sort.direction === 'desc' ? sorted.reverse() : sorted; }, [query, sort]); return (
setQuery(event.target.value)} placeholder="Filter members…" aria-label="Filter members" className="max-w-xs" />

Sort: {sort ? `${String(sort.key)} ${sort.direction}` : 'none'}

); } ControlledSortDemo.displayName = 'ControlledSortDemo'; export const dataTable: Entry = { slug: 'data-table', name: 'DataTable', group: 'Data', description: 'A column-driven table with sorting, expandable rows and sticky headers. Written locally rather than wrapping a table library, so its whole API is the props below.', imports: { subpath: 'data-table', names: ['DataTable'] }, compositions: [ { name: 'Sortable columns', description: 'Click a header to sort; `sorter` overrides the default comparison.', render: () => ( ), }, { name: 'Expandable rows', render: () => ( (

{record.name} joined as {record.role.toLowerCase()} and is currently {record.status}.

), }} /> ), }, { name: 'Empty', render: () => ( ), }, { name: 'Sticky header and pinned columns', description: '`scroll.y` makes the header sticky; `scroll.x` plus `fixed` on a column pins it while the rest slides underneath. Give every column an explicit `width` here — a pinned column is positioned, so the browser cannot fall back to measuring it.', render: () => ( ({ ...row, id: row.id + 10 }))]} scroll={{ x: 900, y: 240 }} columns={[ { ...COLUMNS[0], fixed: 'left', width: 200 }, { key: 'role', title: 'Role', dataIndex: 'role', width: 160 }, { key: 'email', title: 'E-mail', dataIndex: 'email', width: 240 }, { key: 'location', title: 'Location', dataIndex: 'location', width: 180 }, { key: 'seats', title: 'Seats', dataIndex: 'seats', width: 100, align: 'right' }, { ...COLUMNS[2], key: 'status-pinned', title: 'Status', fixed: 'right', width: 140 }, ]} /> ), }, { name: 'Controlled sorting', description: 'Pass `sort` and `onSortChange` and the table stops reordering its own rows — the header becomes a report of intent, which is what a server-sorted table needs.', render: () => , }, ], }; export const virtualList: Entry = { slug: 'virtual-list', name: 'VirtualList', group: 'Data', description: 'Renders only the rows inside the viewport plus an overscan margin, so a list of ten thousand costs the same as a list of twenty.', imports: { subpath: 'virtual-list', names: ['VirtualList'] }, compositions: [ { name: '10,000 rows', render: () => ( ({ id: i }))} height={240} itemHeight={36} itemKey="id" className="w-72 rounded-lg border border-border" aria-label="Ten thousand rows" > {(item) => (
Row {item.id + 1}
)}
), }, { name: 'Rich rows', description: 'A row can hold anything, as long as every row is exactly `itemHeight` tall — the window is computed from that number, not measured. Note the DOM still holds only what fits.', render: () => ( ({ id: i, name: `Person ${i + 1}`, email: `person${i + 1}@cbar.az`, }))} height={320} itemHeight={56} itemKey="id" className="w-96 rounded-lg border border-border" aria-label="Two thousand people" > {(item) => (
{String(item.id + 1).slice(-2)}
{item.name}
{item.email}
)}
), }, ], }; const TREE_DATA: TreeNode[] = [ { key: 'src', title: 'src', children: [ { key: 'components', title: 'components', children: [ { key: 'button', title: 'button', isLeaf: true }, { key: 'badge', title: 'badge', isLeaf: true }, ], }, { key: 'lib', title: 'lib', children: [{ key: 'cx', title: 'cx.ts', isLeaf: true }] }, ], }, { key: 'scripts', title: 'scripts', children: [{ key: 'gen', title: 'gen-exports.mjs', isLeaf: true }] }, ]; export const tree: Entry = { slug: 'tree', name: 'Tree', group: 'Data', description: 'A hierarchy with expand, select and tri-state checkboxes — a parent goes half-checked when only some of its children are.', imports: { subpath: 'tree', names: ['Tree'] }, compositions: [ { name: 'Checkable', render: () => ( ), }, { name: 'Selectable only', render: () => ( ), }, { name: 'Collapsed', description: '`defaultExpandedKeys` opens named branches instead of the whole hierarchy — the right default once a tree is deeper than a screen.', render: () => ( ), }, { name: 'Virtualised', description: '`virtual` plus a `height` windows the rows, the same technique VirtualList uses. Two thousand children here; the DOM holds a dozen.', render: () => ( ({ key: `node-${i}`, title: `node-${i}`, isLeaf: true, })), }, ]} /> ), }, ], }; const TREE_SELECT_DATA: TreeSelectNode[] = [ { value: 'absheron', title: 'Absheron', children: [ { value: 'baku', title: 'Baku' }, { value: 'sumqayit', title: 'Sumqayit' }, ], }, { value: 'ganja-qazakh', title: 'Ganja-Qazakh', children: [ { value: 'ganja', title: 'Ganja' }, { value: 'qazakh', title: 'Qazakh' }, ], }, ]; export const treeSelect: Entry = { slug: 'tree-select', name: 'TreeSelect', group: 'Data', description: 'A Select whose options are a hierarchy. Optionally multiple, checkable and searchable.', imports: { subpath: 'tree-select', names: ['TreeSelect'] }, compositions: [ { name: 'Single', render: () => ( ), }, { name: 'Multiple and checkable', render: () => ( ), }, { name: 'Searchable', description: '`showSearch` filters the tree as you type, keeping the ancestors of anything that matches so a hit is never orphaned from its branch.', render: () => ( ), }, { name: 'With a label', description: 'The trigger takes an `id`, so a plain Label binds to it the way it would to any field.', render: () => (
), }, { name: 'Disabled', render: () => ( ), }, ], }; /** Named and `displayName`-ed for `chrome/lib/codegen.ts` — see `ControlledSortDemo`. */ function ControlledDateDemo() { const [value, setValue] = React.useState(new Date(2026, 0, 15)); return (

{value ? value.toDateString() : 'Nothing selected'}

); } ControlledDateDemo.displayName = 'ControlledDateDemo'; export const datePicker: Entry = { slug: 'date-picker', name: 'DatePicker', group: 'Data', description: 'A date field with a calendar popover. DateRangePicker and the date maths ship from this same subpath, so a consumer formats a value without installing a date library.', imports: { subpath: 'date-picker', names: ['DatePicker', 'DateRangePicker', 'formatDate'] }, compositions: [ { name: 'Granularity', description: '`picker` switches the calendar between day, month and year.', render: () => (
), }, { name: 'Range', render: () => , }, { name: 'Constrained', description: 'disabledDate rejects a day without removing it from the grid.', render: () => ( date.getDay() === 0 || date.getDay() === 6} /> ), }, { name: 'No past dates', description: 'The scheduling case: the same `disabledDate` hook, comparing against midnight today rather than against `now`, or the current day would disable itself by lunchtime.', render: () => ( date < new Date(new Date().setHours(0, 0, 0, 0))} /> ), }, { name: 'Localised', description: '`locale` drives the month and weekday names and where the week starts — through `Intl`, with one exception: Chrome ships no `az` data and silently falls back to English, so the Azerbaijani names are carried in the kit. Give it a matching `format` for the text field.', render: () => (
), }, { name: 'Controlled', render: () => , }, { name: 'Disabled', render: () => , }, ], }; /** Named and `displayName`-ed for `chrome/lib/codegen.ts` — see `ControlledSortDemo`. */ function PriceFilterDemo() { const [value, setValue] = React.useState([120, 640]); return (
{value[0]} – {value[1]} ₼
); } PriceFilterDemo.displayName = 'PriceFilterDemo'; export const rangeSlider: Entry = { slug: 'range-slider', name: 'RangeSlider', group: 'Data', description: 'One or two thumbs over a numeric range, on Radix Slider. Each thumb carries role="slider", which takes no name from surrounding content — pass `thumbLabels` or a screen reader announces a bare number.', imports: { subpath: 'range-slider', names: ['RangeSlider'] }, figma: { set: 'slider' }, compositions: [ { name: 'Single and dual', render: () => (
), }, { name: 'With marks', description: 'Ticks are decorative — they label the scale, they are not snap targets.', render: () => (
), }, { name: 'Price filter', description: 'Controlled, with the span echoed beside it. `onValueChange` fires while dragging, so the readout tracks the thumb rather than waiting for the release.', render: () => , }, { name: 'Vertical', description: 'The track needs a height from its container — `orientation` changes the axis, not the box.', render: () => (
), }, { name: 'Disabled', render: () => (
), }, ], }; /** * Named and `displayName`-ed for `chrome/lib/codegen.ts` — see `ControlledSortDemo`. * * `beforeUpload` returning `false` cancels the request, so the demo stays * offline and the ticker below stands in for one. */ function UploadListDemo() { const [files, setFiles] = React.useState<{ name: string; percent: number }[]>([]); React.useEffect(() => { if (!files.some((file) => file.percent < 100)) return; const id = setInterval(() => { setFiles((current) => current.map((file) => (file.percent >= 100 ? file : { ...file, percent: file.percent + 20 })) ); }, 400); return () => clearInterval(id); }, [files]); return (
{ setFiles((current) => [...current, { name: file.name, percent: 0 }]); return false; }} > {files.length > 0 ? (
    {files.map((file, index) => (
  • {file.name} {file.percent}%
  • ))}
) : null}
); } UploadListDemo.displayName = 'UploadListDemo'; export const upload: Entry = { slug: 'upload', name: 'Upload', group: 'Data', description: 'A file picker with drag-and-drop, per-file progress and a request layer of its own — no rc-upload underneath.', imports: { subpath: 'upload', names: ['Upload', 'UploadDropzone'] }, figma: { set: 'FileUpload' }, compositions: [ { name: 'Dropzone', description: 'No `action` is set here, so nothing leaves the page — the files stay in the list.', render: () => ( ), }, { name: 'Restricted', render: () => ( ), }, { name: 'Button trigger', description: '`variant="button"` is the second surface CBAR draws — a plain trigger for a toolbar or a form row, where a 224px drop target is too much. It is still the same `