@hachej/boring-ui-kit 0.4.2 → 0.4.4
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.
- package/README.md +51 -1
- package/bin/boring-ui-blocks.mjs +29 -0
- package/dist/canvas-editor-native.d.ts +8 -1
- package/dist/canvas-editor-native.js +13 -7
- package/dist/canvas-editor.d.ts +4 -2
- package/dist/canvas-editor.js +5 -0
- package/dist/chat-outbox.d.ts +18 -0
- package/dist/chat-outbox.js +12 -0
- package/dist/file-tree-view.d.ts +29 -1
- package/dist/file-tree-view.js +74 -7
- package/dist/mark-store.d.ts +38 -0
- package/dist/mark-store.js +84 -0
- package/dist/markdown-editor.d.ts +6 -0
- package/dist/markdown-editor.js +17 -2
- package/dist/markdown-highlight-layer.d.ts +26 -0
- package/dist/markdown-highlight-layer.js +107 -0
- package/dist/markdown-quote.d.ts +23 -0
- package/dist/markdown-quote.js +39 -0
- package/dist/native-chat.d.ts +21 -0
- package/dist/native-chat.js +111 -7
- package/dist/remote-chat.js +16 -14
- package/dist/shared-chat/error.d.ts +10 -0
- package/dist/shared-chat/error.js +13 -0
- package/dist/shared-chat/fake.d.ts +39 -0
- package/dist/shared-chat/fake.js +90 -0
- package/dist/shared-chat/index.d.ts +51 -0
- package/dist/shared-chat/index.js +255 -0
- package/dist/shared-chat/socket.d.ts +20 -0
- package/dist/shared-chat/socket.js +114 -0
- package/dist/shared-chat/source.d.ts +117 -0
- package/dist/shared-chat/source.js +6 -0
- package/package.json +15 -3
- package/r/agent-target.json +36 -0
- package/r/app-shell.json +26 -11
- package/r/board-viewer.json +49 -0
- package/r/button.json +1 -1
- package/r/diagram-viewer.json +42 -0
- package/r/dock.json +1 -1
- package/r/documents.json +11 -11
- package/r/feedback-preview.json +1 -1
- package/r/feedback.json +6 -6
- package/r/file-tree.json +7 -6
- package/r/highlights.json +29 -0
- package/r/html-viewer.json +3 -3
- package/r/maquette-viewer.json +10 -13
- package/r/markdown-editor.json +3 -3
- package/r/mockup-viewer.json +45 -0
- package/r/pi-ambient.json +7 -7
- package/r/pi-app.json +21 -18
- package/r/pi-chat.json +28 -20
- package/r/pi-workspace.json +4 -4
- package/r/provider-setup.json +2 -2
- package/r/record-viewer.json +40 -0
- package/r/registry.json +449 -83
- package/r/requests-board.json +41 -0
- package/r/shared-chat.json +79 -0
- package/r/task-list-viewer.json +2 -2
- package/r/theme.json +15 -0
- package/r/version-history.json +3 -3
- package/r/viewers.json +16 -10
package/r/pi-workspace.json
CHANGED
|
@@ -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.
|
|
12
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
13
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
14
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
11
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.4/r/theme.json",
|
|
12
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.4/r/pi-chat.json",
|
|
13
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.4/r/utils.json",
|
|
14
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.4/r/dock.json",
|
|
15
15
|
"badge",
|
|
16
16
|
"card"
|
|
17
17
|
],
|
package/r/provider-setup.json
CHANGED
|
@@ -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.
|
|
13
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
12
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.4/r/theme.json",
|
|
13
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.4/r/utils.json",
|
|
14
14
|
"button",
|
|
15
15
|
"input",
|
|
16
16
|
"label",
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "record-viewer",
|
|
4
|
+
"title": "Record viewer",
|
|
5
|
+
"description": "Typed RecordTable and RecordList, with grouping, app-defined status badges and host row actions. createRecordsKind renders *.records.json (records@1), with a phone list and exact raw-text fallback for invalid files. English and French labels and locale formatting.",
|
|
6
|
+
"dependencies": [
|
|
7
|
+
"react@^19.0.0",
|
|
8
|
+
"lucide-react@^1.51.0"
|
|
9
|
+
],
|
|
10
|
+
"registryDependencies": [
|
|
11
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.4/r/theme.json",
|
|
12
|
+
"table",
|
|
13
|
+
"badge",
|
|
14
|
+
"item",
|
|
15
|
+
"button",
|
|
16
|
+
"alert",
|
|
17
|
+
"empty"
|
|
18
|
+
],
|
|
19
|
+
"files": [
|
|
20
|
+
{
|
|
21
|
+
"path": "registry/record-viewer/records.ts",
|
|
22
|
+
"content": "export type RecordColumnType = 'text' | 'number' | 'date' | 'status';\nexport type RecordTone = 'ok' | 'warn' | 'error' | 'info' | 'neutral';\nexport interface RecordStatus { readonly label: string; readonly tone: RecordTone }\nexport interface RecordColumn { readonly key: string; readonly label: string; readonly type: RecordColumnType }\nexport interface RecordAction { readonly id: string; readonly label: string }\nexport type RecordValue = string | number | RecordStatus | null;\nexport interface RecordRow {\n readonly id: string;\n readonly values: Readonly<Record<string, RecordValue>>;\n readonly actions?: readonly RecordAction[] | undefined;\n}\nexport interface RecordsFile {\n readonly kind: 'records@1';\n readonly columns: readonly RecordColumn[];\n readonly rows: readonly RecordRow[];\n readonly groupBy?: string | undefined;\n}\nexport type RecordsParse = { readonly kind: 'ok'; readonly records: RecordsFile } | { readonly kind: 'invalid' };\n\nconst object = (value: unknown): value is Record<string, unknown> => value !== null && typeof value === 'object' && !Array.isArray(value);\nconst nonempty = (value: unknown): value is string => typeof value === 'string' && value.trim().length > 0;\nconst tones = new Set(['ok', 'warn', 'error', 'info', 'neutral']);\nconst types = new Set(['text', 'number', 'date', 'status']);\nconst unique = (values: readonly string[]) => new Set(values).size === values.length;\n\n/** ISO calendar dates or timestamps with an explicit timezone; calendar days must exist, not roll into the next month. */\nexport function isRecordDate(value: string): boolean {\n if (!/^\\d{4}-\\d{2}-\\d{2}(?:T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{1,3})?(?:Z|[+-]\\d{2}:\\d{2}))?$/.test(value)) return false;\n const day = value.slice(0, 10);\n const date = new Date(`${day}T00:00:00Z`);\n return Number.isFinite(Date.parse(value)) && Number.isFinite(date.getTime()) && date.toISOString().slice(0, 10) === day;\n}\n\n/** Untrusted files are all-or-nothing: no row or cell is silently dropped. File actions are data, never executable code. */\nexport function parseRecords(text: string): RecordsParse {\n let value: unknown;\n try { value = JSON.parse(text); } catch { return { kind: 'invalid' }; }\n if (!object(value) || value.kind !== 'records@1' || !Array.isArray(value.columns) || !value.columns.length || !Array.isArray(value.rows)) return { kind: 'invalid' };\n if (!value.columns.every(column => object(column) && nonempty(column.key) && nonempty(column.label) && typeof column.type === 'string' && types.has(column.type))) return { kind: 'invalid' };\n const columns = value.columns as unknown as readonly RecordColumn[];\n const keys = new Set(columns.map(column => column.key));\n if (keys.size !== columns.length || value.groupBy !== undefined && (typeof value.groupBy !== 'string' || !keys.has(value.groupBy))) return { kind: 'invalid' };\n for (const row of value.rows) {\n if (!object(row) || !nonempty(row.id) || !object(row.values) || Object.keys(row.values).some(key => !keys.has(key))) return { kind: 'invalid' };\n for (const column of columns) {\n const cell = Object.hasOwn(row.values, column.key) ? row.values[column.key] : null;\n if (cell === null) continue;\n if (column.type === 'text' && typeof cell === 'string' || column.type === 'number' && typeof cell === 'number' && Number.isFinite(cell) || column.type === 'date' && typeof cell === 'string' && isRecordDate(cell) || column.type === 'status' && object(cell) && nonempty(cell.label) && typeof cell.tone === 'string' && tones.has(cell.tone)) continue;\n return { kind: 'invalid' };\n }\n if (row.actions !== undefined && (!Array.isArray(row.actions) || !row.actions.every(action => object(action) && nonempty(action.id) && nonempty(action.label)) || !unique(row.actions.map(action => action.id)))) return { kind: 'invalid' };\n }\n if (!unique(value.rows.map(row => row.id))) return { kind: 'invalid' };\n return { kind: 'ok', records: value as unknown as RecordsFile };\n}\n\nexport const isRecordsPath = (path: string): boolean => /\\.records\\.json$/i.test(path);\nexport const recordValue = (row: RecordRow, key: string): RecordValue => Object.hasOwn(row.values, key) ? row.values[key] ?? null : null;\n\n/** Dates display in UTC so a calendar date does not move to the previous day in a viewer's timezone. */\nexport function formatRecordValue(value: RecordValue, type: RecordColumnType, locale = 'en', missing = '—'): string {\n if (value === null) return missing;\n if (typeof value === 'object') return value.label;\n if (type === 'number' && typeof value === 'number') return new Intl.NumberFormat(locale).format(value);\n if (type === 'date' && typeof value === 'string' && isRecordDate(value)) return new Intl.DateTimeFormat(locale, { dateStyle: 'medium', timeZone: 'UTC' }).format(new Date(value));\n return String(value);\n}\n",
|
|
23
|
+
"type": "registry:lib",
|
|
24
|
+
"target": "components/record-viewer/records.ts"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"path": "registry/record-viewer/records-labels.ts",
|
|
28
|
+
"content": "export type RecordsLocale = 'en' | 'fr';\n\nexport const defaultRecordsLabels = {\n kind: 'Records',\n actions: 'Actions',\n empty: 'No records',\n missing: '—',\n invalidTitle: 'This file cannot be shown as records',\n invalidNote: 'Invalid records@1 data. The original raw text is shown below.',\n};\nexport type RecordsLabels = typeof defaultRecordsLabels;\nexport const frenchRecordsLabels: RecordsLabels = {\n kind: 'Données',\n actions: 'Actions',\n empty: 'Aucune donnée',\n missing: '—',\n invalidTitle: 'Ce fichier ne peut pas être affiché en tableau',\n invalidNote: 'Données records@1 invalides. Le texte brut original est affiché ci-dessous.',\n};\nexport const recordsLabelsFor = (locale: RecordsLocale | undefined, labels?: Partial<RecordsLabels>): RecordsLabels =>\n ({ ...(locale === 'fr' ? frenchRecordsLabels : defaultRecordsLabels), ...labels });\n",
|
|
29
|
+
"type": "registry:lib",
|
|
30
|
+
"target": "components/record-viewer/records-labels.ts"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
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",
|
|
35
|
+
"type": "registry:component",
|
|
36
|
+
"target": "components/record-viewer/record-viewer.tsx"
|
|
37
|
+
}
|
|
38
|
+
],
|
|
39
|
+
"type": "registry:component"
|
|
40
|
+
}
|