@hachej/boring-ui-kit 0.4.11 → 0.4.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/dist/shared-chat/socket.js +3 -1
  2. package/dist/shared-chat/source.d.ts +7 -1
  3. package/package.json +2 -2
  4. package/r/agent-target.json +4 -4
  5. package/r/app-shell.json +10 -8
  6. package/r/board-viewer.json +12 -6
  7. package/r/button.json +1 -1
  8. package/r/diagram-viewer.json +8 -2
  9. package/r/dock.json +1 -1
  10. package/r/documents.json +14 -12
  11. package/r/feedback-preview.json +1 -1
  12. package/r/feedback.json +7 -6
  13. package/r/file-tree.json +5 -5
  14. package/r/highlights.json +3 -3
  15. package/r/html-viewer.json +3 -3
  16. package/r/looks-viewer.json +9 -3
  17. package/r/maquette-viewer.json +9 -3
  18. package/r/markdown-editor.json +3 -3
  19. package/r/mockup-viewer.json +14 -2
  20. package/r/pi-ambient.json +11 -8
  21. package/r/pi-app.json +31 -18
  22. package/r/pi-artifacts.json +32 -0
  23. package/r/pi-chat-approval.json +26 -0
  24. package/r/pi-chat-attachments.json +25 -0
  25. package/r/pi-chat-markdown.json +26 -0
  26. package/r/pi-chat-mentions.json +23 -0
  27. package/r/pi-chat-pickers.json +24 -0
  28. package/r/pi-chat-plugins.json +31 -0
  29. package/r/pi-chat-question.json +29 -0
  30. package/r/pi-chat-slash.json +26 -0
  31. package/r/pi-chat-subagent.json +34 -0
  32. package/r/pi-chat-voice.json +39 -0
  33. package/r/pi-chat.json +15 -97
  34. package/r/pi-workspace.json +4 -4
  35. package/r/placed-agent.json +4 -4
  36. package/r/provider-setup.json +2 -2
  37. package/r/record-viewer.json +8 -2
  38. package/r/registry.json +667 -242
  39. package/r/requests-board.json +1 -1
  40. package/r/shared-chat.json +13 -9
  41. package/r/task-list-viewer.json +2 -2
  42. package/r/version-history.json +3 -3
  43. package/r/viewer-code.json +36 -0
  44. package/r/viewer-frame.json +447 -0
  45. package/r/viewer-html.json +32 -0
  46. package/r/viewer-image.json +36 -0
  47. package/r/viewer-markdown.json +47 -0
  48. package/r/viewer-pdf.json +45 -0
  49. package/r/viewers.json +8 -491
  50. package/r/windows.json +1 -1
@@ -8,10 +8,10 @@
8
8
  "react-dom@^19.0.0"
9
9
  ],
10
10
  "registryDependencies": [
11
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.11/r/theme.json",
12
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.11/r/pi-chat.json",
13
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.11/r/utils.json",
14
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.11/r/dock.json",
11
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.13/r/theme.json",
12
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.13/r/pi-chat.json",
13
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.13/r/utils.json",
14
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.13/r/dock.json",
15
15
  "badge",
16
16
  "card"
17
17
  ],
@@ -9,10 +9,10 @@
9
9
  "lucide-react@^1.51.0"
10
10
  ],
11
11
  "registryDependencies": [
12
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.11/r/pi-app.json",
13
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.11/r/pi-ambient.json",
14
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.11/r/app-shell.json",
15
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.11/r/utils.json",
12
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.13/r/pi-app.json",
13
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.13/r/pi-ambient.json",
14
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.13/r/app-shell.json",
15
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.13/r/utils.json",
16
16
  "button"
17
17
  ],
18
18
  "files": [
@@ -9,8 +9,8 @@
9
9
  "lucide-react@^1.51.0"
10
10
  ],
11
11
  "registryDependencies": [
12
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.11/r/theme.json",
13
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.11/r/utils.json",
12
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.13/r/theme.json",
13
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.13/r/utils.json",
14
14
  "button",
15
15
  "input",
16
16
  "label",
@@ -8,7 +8,7 @@
8
8
  "lucide-react@^1.51.0"
9
9
  ],
10
10
  "registryDependencies": [
11
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.11/r/theme.json",
11
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.13/r/theme.json",
12
12
  "table",
13
13
  "badge",
14
14
  "item",
@@ -31,9 +31,15 @@
31
31
  },
32
32
  {
33
33
  "path": "registry/record-viewer/record-viewer.tsx",
34
- "content": "'use client';\n\nimport { useMemo } from 'react';\nimport type { ComponentType, ReactNode } from 'react';\nimport { Rows3Icon } from 'lucide-react';\nimport { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';\nimport { Badge } from '@/components/ui/badge';\nimport { Button } from '@/components/ui/button';\nimport { Empty, EmptyHeader, EmptyTitle } from '@/components/ui/empty';\nimport { Item, ItemActions, ItemContent, ItemGroup } from '@/components/ui/item';\nimport { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';\nimport { formatRecordValue, isRecordsPath, parseRecords, recordValue } from './records';\nimport type { RecordAction, RecordColumn, RecordRow, RecordStatus, RecordTone } from './records';\nimport { recordsLabelsFor } from './records-labels';\nimport type { RecordsLabels, RecordsLocale } from './records-labels';\n\nexport { formatRecordValue, isRecordsPath, parseRecords } from './records';\nexport type { RecordAction, RecordColumn, RecordColumnType, RecordRow, RecordStatus, RecordTone, RecordValue, RecordsFile, RecordsParse } from './records';\nexport { defaultRecordsLabels, frenchRecordsLabels, recordsLabelsFor } from './records-labels';\nexport type { RecordsLabels, RecordsLocale } from './records-labels';\n\nconst variants = { ok: 'default', warn: 'secondary', error: 'destructive', info: 'outline', neutral: 'secondary' } as const satisfies Record<RecordTone, string>;\nfunction Status({ status, label }: { readonly status: RecordStatus; readonly label?: string | undefined }) {\n return <Badge data-record-status={status.tone} variant={variants[status.tone]} role={label ? 'img' : undefined} aria-label={label ? `${label}: ${status.label}` : undefined}>{status.label}</Badge>;\n}\nfunction NoRecords({ label }: { readonly label: string }) {\n return <Empty><EmptyHeader><EmptyTitle>{label}</EmptyTitle></EmptyHeader></Empty>;\n}\ninterface Words {\n readonly locale?: RecordsLocale | undefined;\n readonly labels?: Partial<RecordsLabels> | undefined;\n}\nexport interface RecordTableProps extends Words {\n readonly columns: readonly RecordColumn[];\n readonly rows: readonly RecordRow[];\n readonly groupBy?: string | undefined;\n readonly onAction?: ((action: RecordAction, row: RecordRow) => void) | undefined;\n}\n\n/** Groups retain their first-seen order; the key is the typed raw value, never its rounded display label. */\nfunction groupsOf(rows: readonly RecordRow[], columns: readonly RecordColumn[], groupBy: string | undefined, locale: RecordsLocale | undefined, labels: RecordsLabels) {\n const column = columns.find(column => column.key === groupBy);\n if (!column) return [{ key: '', label: undefined, rows }];\n const groups = new Map<string, { key: string; label: string; rows: RecordRow[] }>();\n for (const row of rows) {\n const value = recordValue(row, column.key), key = JSON.stringify(value !== null && typeof value === 'object' ? [value.tone, value.label] : value);\n let group = groups.get(key);\n if (!group) { group = { key, label: `${column.label}: ${formatRecordValue(value, column.type, locale, labels.missing)}`, rows: [] }; groups.set(key, group); }\n group.rows.push(row);\n }\n return [...groups.values()];\n}\n\n/** A typed table of app data. Actions call the host only; their file metadata grants no permission. */\nexport function RecordTable({ columns, rows, groupBy, onAction, locale, labels: given }: RecordTableProps) {\n const labels = recordsLabelsFor(locale, given);\n const groups = groupsOf(rows, columns, groupBy, locale, labels);\n const fields = columns.filter(column => column.key !== groupBy);\n const actions = rows.some(row => row.actions?.length);\n if (!rows.length) return <NoRecords label={labels.empty} />;\n return <div data-boring=\"record-table\" className=\"min-w-0 overflow-auto\"><Table aria-label={labels.kind}>\n <TableHeader><TableRow>{fields.map(column => <TableHead key={column.key} scope=\"col\" className={column.type === 'number' ? 'text-right' : undefined}>{column.label}</TableHead>)}{actions && <TableHead scope=\"col\">{labels.actions}</TableHead>}</TableRow></TableHeader>\n {groups.map(group => <TableBody key={group.key}>\n {group.label !== undefined && <TableRow><TableHead data-record-group={group.key} scope=\"rowgroup\" colSpan={Math.max(1, fields.length + Number(actions))}>{group.label}</TableHead></TableRow>}\n {group.rows.map(row => <TableRow key={row.id} data-record-row={row.id}>{fields.map(column => {\n const value = recordValue(row, column.key);\n return <TableCell key={column.key} className={column.type === 'number' ? 'text-right' : undefined}>{column.type === 'status' && value !== null && typeof value === 'object' ? <Status status={value} /> : formatRecordValue(value, column.type, locale, labels.missing)}</TableCell>;\n })}{actions && <TableCell><div className=\"flex flex-wrap gap-2\">{row.actions?.map(action => <Button key={action.id} variant=\"outline\" className=\"min-h-11\" disabled={!onAction} onClick={() => onAction?.(action, row)}>{action.label}</Button>)}</div></TableCell>}</TableRow>)}\n </TableBody>)}\n </Table></div>;\n}\n\nexport interface RecordListItem {\n readonly id: string;\n readonly title: string;\n readonly detail?: string | undefined;\n readonly status?: RecordStatus | undefined;\n /** Labelled status columns when projecting typed rows. */\n readonly statuses?: readonly { readonly label: string; readonly value: RecordStatus }[] | undefined;\n readonly action?: RecordAction | undefined;\n /** Extra actions when projecting a row with more than one action into a list. */\n readonly actions?: readonly RecordAction[] | undefined;\n}\nexport interface RecordListProps extends Words {\n readonly items: readonly RecordListItem[];\n readonly onAction?: ((action: RecordAction, item: RecordListItem) => void) | undefined;\n}\n\n/** A compact status list. Detail remains wrapping text rather than a line-clamped shadcn description. */\nexport function RecordList({ items, onAction, locale, labels: given }: RecordListProps) {\n const labels = recordsLabelsFor(locale, given);\n if (!items.length) return <NoRecords label={labels.empty} />;\n return <ItemGroup data-boring=\"record-list\" aria-label={labels.kind}>{items.map(item => {\n const actions = [...(item.action ? [item.action] : []), ...(item.actions ?? [])];\n return <Item key={item.id} role=\"listitem\" data-record-item={item.id} variant=\"outline\" className=\"min-w-0\">\n <ItemContent className=\"min-w-0\"><div className=\"whitespace-normal wrap-anywhere\">{item.title}</div>\n {item.detail && <div className=\"whitespace-pre-wrap wrap-anywhere\">{item.detail}</div>}\n {item.status && <div className=\"max-w-full overflow-auto\"><Status status={item.status} /></div>}\n {item.statuses?.map((status, index) => <div key={index} className=\"max-w-full overflow-auto\"><Status status={status.value} label={status.label} /></div>)}\n </ItemContent>\n {!!actions.length && <ItemActions className=\"max-w-full flex-wrap overflow-auto\">{actions.map(action => <Button key={action.id} variant=\"outline\" className=\"min-h-11\" disabled={!onAction} onClick={() => onAction?.(action, item)}>{action.label}</Button>)}</ItemActions>}\n </Item>;\n })}</ItemGroup>;\n}\n\nexport interface RecordsViewerProps extends Words {\n readonly text: string;\n readonly onAction?: RecordTableProps['onAction'];\n}\n\n/** The phone projection preserves every column and action; domain status meaning stays with the app. */\nexport function RecordsViewer({ text, onAction, locale, labels: given }: RecordsViewerProps) {\n const parsed = useMemo(() => parseRecords(text), [text]);\n const labels = recordsLabelsFor(locale, given);\n if (parsed.kind !== 'ok') return <section data-boring=\"records-viewer\" data-state=\"invalid\" className=\"flex min-w-0 flex-col gap-3 p-3\">\n <Alert data-testid=\"records-invalid\"><AlertTitle>{labels.invalidTitle}</AlertTitle><AlertDescription>{labels.invalidNote}</AlertDescription></Alert>\n <pre data-testid=\"records-raw\" className=\"overflow-auto whitespace-pre-wrap wrap-anywhere\">{text}</pre>\n </section>;\n const { columns, rows, groupBy } = parsed.records;\n const groups = groupsOf(rows, columns, groupBy, locale, labels);\n const fields = columns.filter(column => column.key !== groupBy);\n const first = fields.find(column => column.type !== 'status');\n return <section data-boring=\"records-viewer\" data-state=\"ok\" aria-label={labels.kind} className=\"min-w-0 p-3\">\n <div className=\"hidden sm:block\"><RecordTable columns={columns} rows={rows} groupBy={groupBy} onAction={onAction} locale={locale} labels={given} /></div>\n <div data-testid=\"records-phone\" className=\"flex flex-col gap-4 sm:hidden\">{!rows.length ? <NoRecords label={labels.empty} /> : groups.map(group => {\n const items = group.rows.map(row => ({ id: row.id, title: first ? formatRecordValue(recordValue(row, first.key), first.type, locale, labels.missing) : row.id,\n detail: fields.filter(column => column.key !== first?.key && column.type !== 'status').map(column => `${column.label}: ${formatRecordValue(recordValue(row, column.key), column.type, locale, labels.missing)}`).join('\\n'),\n statuses: fields.filter(column => column.type === 'status').map(column => {\n const value = recordValue(row, column.key);\n return { label: column.label, value: value !== null && typeof value === 'object' ? value : { label: labels.missing, tone: 'neutral' as const } };\n }), actions: row.actions }));\n return <div key={group.key} className=\"flex min-w-0 flex-col gap-2\">{group.label !== undefined && <h3 data-record-list-group={group.key}>{group.label}</h3>}\n <RecordList items={items} locale={locale} labels={given} onAction={onAction ? (action, item) => { const row = group.rows.find(row => row.id === item.id); if (row) onAction(action, row); } : undefined} />\n </div>;\n })}</div>\n </section>;\n}\n\nexport interface RecordsKind {\n readonly match: (target: { readonly resource: { readonly path: string } }) => boolean;\n readonly label: string;\n readonly icon: ComponentType<{ readonly className?: string; readonly 'aria-hidden'?: boolean | 'true' }>;\n readonly renderer: (props: { readonly text: string }) => ReactNode;\n}\nexport function createRecordsKind(options: Words & { readonly onAction?: RecordTableProps['onAction'] } = {}): RecordsKind {\n return { match: target => isRecordsPath(target.resource.path), label: recordsLabelsFor(options.locale, options.labels).kind, icon: Rows3Icon,\n renderer: ({ text }) => <RecordsViewer text={text} {...options} /> };\n}\nexport const recordsKind: RecordsKind = createRecordsKind();\n",
34
+ "content": "'use client';\n\nimport { useMemo } from 'react';\nimport { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';\nimport { Badge } from '@/components/ui/badge';\nimport { Button } from '@/components/ui/button';\nimport { Empty, EmptyHeader, EmptyTitle } from '@/components/ui/empty';\nimport { Item, ItemActions, ItemContent, ItemGroup } from '@/components/ui/item';\nimport { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';\nimport { formatRecordValue, parseRecords, recordValue } from './records';\nimport type { RecordAction, RecordColumn, RecordRow, RecordStatus, RecordTone } from './records';\nimport { recordsLabelsFor } from './records-labels';\nimport type { RecordsLabels, RecordsLocale } from './records-labels';\n\nexport { formatRecordValue, isRecordsPath, parseRecords } from './records';\nexport type { RecordAction, RecordColumn, RecordColumnType, RecordRow, RecordStatus, RecordTone, RecordValue, RecordsFile, RecordsParse } from './records';\nexport { defaultRecordsLabels, frenchRecordsLabels, recordsLabelsFor } from './records-labels';\nexport type { RecordsLabels, RecordsLocale } from './records-labels';\n\nconst variants = { ok: 'default', warn: 'secondary', error: 'destructive', info: 'outline', neutral: 'secondary' } as const satisfies Record<RecordTone, string>;\nfunction Status({ status, label }: { readonly status: RecordStatus; readonly label?: string | undefined }) {\n return <Badge data-record-status={status.tone} variant={variants[status.tone]} role={label ? 'img' : undefined} aria-label={label ? `${label}: ${status.label}` : undefined}>{status.label}</Badge>;\n}\nfunction NoRecords({ label }: { readonly label: string }) {\n return <Empty><EmptyHeader><EmptyTitle>{label}</EmptyTitle></EmptyHeader></Empty>;\n}\nexport interface Words {\n readonly locale?: RecordsLocale | undefined;\n readonly labels?: Partial<RecordsLabels> | undefined;\n}\nexport interface RecordTableProps extends Words {\n readonly columns: readonly RecordColumn[];\n readonly rows: readonly RecordRow[];\n readonly groupBy?: string | undefined;\n readonly onAction?: ((action: RecordAction, row: RecordRow) => void) | undefined;\n}\n\n/** Groups retain their first-seen order; the key is the typed raw value, never its rounded display label. */\nfunction groupsOf(rows: readonly RecordRow[], columns: readonly RecordColumn[], groupBy: string | undefined, locale: RecordsLocale | undefined, labels: RecordsLabels) {\n const column = columns.find(column => column.key === groupBy);\n if (!column) return [{ key: '', label: undefined, rows }];\n const groups = new Map<string, { key: string; label: string; rows: RecordRow[] }>();\n for (const row of rows) {\n const value = recordValue(row, column.key), key = JSON.stringify(value !== null && typeof value === 'object' ? [value.tone, value.label] : value);\n let group = groups.get(key);\n if (!group) { group = { key, label: `${column.label}: ${formatRecordValue(value, column.type, locale, labels.missing)}`, rows: [] }; groups.set(key, group); }\n group.rows.push(row);\n }\n return [...groups.values()];\n}\n\n/** A typed table of app data. Actions call the host only; their file metadata grants no permission. */\nexport function RecordTable({ columns, rows, groupBy, onAction, locale, labels: given }: RecordTableProps) {\n const labels = recordsLabelsFor(locale, given);\n const groups = groupsOf(rows, columns, groupBy, locale, labels);\n const fields = columns.filter(column => column.key !== groupBy);\n const actions = rows.some(row => row.actions?.length);\n if (!rows.length) return <NoRecords label={labels.empty} />;\n return <div data-boring=\"record-table\" className=\"min-w-0 overflow-auto\"><Table aria-label={labels.kind}>\n <TableHeader><TableRow>{fields.map(column => <TableHead key={column.key} scope=\"col\" className={column.type === 'number' ? 'text-right' : undefined}>{column.label}</TableHead>)}{actions && <TableHead scope=\"col\">{labels.actions}</TableHead>}</TableRow></TableHeader>\n {groups.map(group => <TableBody key={group.key}>\n {group.label !== undefined && <TableRow><TableHead data-record-group={group.key} scope=\"rowgroup\" colSpan={Math.max(1, fields.length + Number(actions))}>{group.label}</TableHead></TableRow>}\n {group.rows.map(row => <TableRow key={row.id} data-record-row={row.id}>{fields.map(column => {\n const value = recordValue(row, column.key);\n return <TableCell key={column.key} className={column.type === 'number' ? 'text-right' : undefined}>{column.type === 'status' && value !== null && typeof value === 'object' ? <Status status={value} /> : formatRecordValue(value, column.type, locale, labels.missing)}</TableCell>;\n })}{actions && <TableCell><div className=\"flex flex-wrap gap-2\">{row.actions?.map(action => <Button key={action.id} variant=\"outline\" className=\"min-h-11\" disabled={!onAction} onClick={() => onAction?.(action, row)}>{action.label}</Button>)}</div></TableCell>}</TableRow>)}\n </TableBody>)}\n </Table></div>;\n}\n\nexport interface RecordListItem {\n readonly id: string;\n readonly title: string;\n readonly detail?: string | undefined;\n readonly status?: RecordStatus | undefined;\n /** Labelled status columns when projecting typed rows. */\n readonly statuses?: readonly { readonly label: string; readonly value: RecordStatus }[] | undefined;\n readonly action?: RecordAction | undefined;\n /** Extra actions when projecting a row with more than one action into a list. */\n readonly actions?: readonly RecordAction[] | undefined;\n}\nexport interface RecordListProps extends Words {\n readonly items: readonly RecordListItem[];\n readonly onAction?: ((action: RecordAction, item: RecordListItem) => void) | undefined;\n}\n\n/** A compact status list. Detail remains wrapping text rather than a line-clamped shadcn description. */\nexport function RecordList({ items, onAction, locale, labels: given }: RecordListProps) {\n const labels = recordsLabelsFor(locale, given);\n if (!items.length) return <NoRecords label={labels.empty} />;\n return <ItemGroup data-boring=\"record-list\" aria-label={labels.kind}>{items.map(item => {\n const actions = [...(item.action ? [item.action] : []), ...(item.actions ?? [])];\n return <Item key={item.id} role=\"listitem\" data-record-item={item.id} variant=\"outline\" className=\"min-w-0\">\n <ItemContent className=\"min-w-0\"><div className=\"whitespace-normal wrap-anywhere\">{item.title}</div>\n {item.detail && <div className=\"whitespace-pre-wrap wrap-anywhere\">{item.detail}</div>}\n {item.status && <div className=\"max-w-full overflow-auto\"><Status status={item.status} /></div>}\n {item.statuses?.map((status, index) => <div key={index} className=\"max-w-full overflow-auto\"><Status status={status.value} label={status.label} /></div>)}\n </ItemContent>\n {!!actions.length && <ItemActions className=\"max-w-full flex-wrap overflow-auto\">{actions.map(action => <Button key={action.id} variant=\"outline\" className=\"min-h-11\" disabled={!onAction} onClick={() => onAction?.(action, item)}>{action.label}</Button>)}</ItemActions>}\n </Item>;\n })}</ItemGroup>;\n}\n\nexport interface RecordsViewerProps extends Words {\n readonly text: string;\n readonly onAction?: RecordTableProps['onAction'];\n}\n\n/** The phone projection preserves every column and action; domain status meaning stays with the app. */\nexport function RecordsViewer({ text, onAction, locale, labels: given }: RecordsViewerProps) {\n const parsed = useMemo(() => parseRecords(text), [text]);\n const labels = recordsLabelsFor(locale, given);\n if (parsed.kind !== 'ok') return <section data-boring=\"records-viewer\" data-state=\"invalid\" className=\"flex min-w-0 flex-col gap-3 p-3\">\n <Alert data-testid=\"records-invalid\"><AlertTitle>{labels.invalidTitle}</AlertTitle><AlertDescription>{labels.invalidNote}</AlertDescription></Alert>\n <pre data-testid=\"records-raw\" className=\"overflow-auto whitespace-pre-wrap wrap-anywhere\">{text}</pre>\n </section>;\n const { columns, rows, groupBy } = parsed.records;\n const groups = groupsOf(rows, columns, groupBy, locale, labels);\n const fields = columns.filter(column => column.key !== groupBy);\n const first = fields.find(column => column.type !== 'status');\n return <section data-boring=\"records-viewer\" data-state=\"ok\" aria-label={labels.kind} className=\"min-w-0 p-3\">\n <div className=\"hidden sm:block\"><RecordTable columns={columns} rows={rows} groupBy={groupBy} onAction={onAction} locale={locale} labels={given} /></div>\n <div data-testid=\"records-phone\" className=\"flex flex-col gap-4 sm:hidden\">{!rows.length ? <NoRecords label={labels.empty} /> : groups.map(group => {\n const items = group.rows.map(row => ({ id: row.id, title: first ? formatRecordValue(recordValue(row, first.key), first.type, locale, labels.missing) : row.id,\n detail: fields.filter(column => column.key !== first?.key && column.type !== 'status').map(column => `${column.label}: ${formatRecordValue(recordValue(row, column.key), column.type, locale, labels.missing)}`).join('\\n'),\n statuses: fields.filter(column => column.type === 'status').map(column => {\n const value = recordValue(row, column.key);\n return { label: column.label, value: value !== null && typeof value === 'object' ? value : { label: labels.missing, tone: 'neutral' as const } };\n }), actions: row.actions }));\n return <div key={group.key} className=\"flex min-w-0 flex-col gap-2\">{group.label !== undefined && <h3 data-record-list-group={group.key}>{group.label}</h3>}\n <RecordList items={items} locale={locale} labels={given} onAction={onAction ? (action, item) => { const row = group.rows.find(row => row.id === item.id); if (row) onAction(action, row); } : undefined} />\n </div>;\n })}</div>\n </section>;\n}\n",
35
35
  "type": "registry:component",
36
36
  "target": "components/record-viewer/record-viewer.tsx"
37
+ },
38
+ {
39
+ "path": "registry/record-viewer/records-kind.tsx",
40
+ "content": "'use client';\n\nimport { lazy } from 'react';\nimport type { ComponentType, ReactNode } from 'react';\nimport { Rows3Icon } from 'lucide-react';\nimport { isRecordsPath } from './records';\nimport { recordsLabelsFor } from './records-labels';\nimport type { RecordTableProps, Words } from './record-viewer';\n\nconst RecordsViewer = lazy(() => import('./record-viewer').then(module => ({ default: module.RecordsViewer })));\n\nexport interface RecordsKind {\n readonly match: (target: { readonly resource: { readonly path: string } }) => boolean;\n readonly label: string;\n readonly icon: ComponentType<{ readonly className?: string; readonly 'aria-hidden'?: boolean | 'true' }>;\n readonly renderer: (props: { readonly text: string }) => ReactNode;\n}\n/** The `*.records.json` kind; the viewer loads on first open. */\nexport function createRecordsKind(options: Words & { readonly onAction?: RecordTableProps['onAction'] } = {}): RecordsKind {\n return { match: target => isRecordsPath(target.resource.path), label: recordsLabelsFor(options.locale, options.labels).kind, icon: Rows3Icon,\n renderer: ({ text }) => <RecordsViewer text={text} {...options} /> };\n}\nexport const recordsKind: RecordsKind = createRecordsKind();\n",
41
+ "type": "registry:component",
42
+ "target": "components/record-viewer/records-kind.tsx"
37
43
  }
38
44
  ],
39
45
  "type": "registry:component"