@krizaka/ui 3.21.0 → 3.22.0
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 +1 -0
- package/dist/app-shell.js +1 -1
- package/dist/argument-card.js +3 -3
- package/dist/{chunk-5MH64NIL.js → chunk-2L67YTNV.js} +2 -2
- package/dist/chunk-AQSZJJAJ.js +126 -0
- package/dist/chunk-AQSZJJAJ.js.map +1 -0
- package/dist/{chunk-3GODX3D6.js → chunk-QDXV544C.js} +2 -2
- package/dist/chunk-QDXV544C.js.map +1 -0
- package/dist/chunk-R6NK6ZNY.js +1080 -0
- package/dist/chunk-R6NK6ZNY.js.map +1 -0
- package/dist/{chunk-LIPK4UCS.js → chunk-YCIBJ6OQ.js} +140 -209
- package/dist/chunk-YCIBJ6OQ.js.map +1 -0
- package/dist/comparison-table.d.ts +44 -0
- package/dist/comparison-table.js +84 -0
- package/dist/comparison-table.js.map +1 -0
- package/dist/data-table.js +19 -1066
- package/dist/data-table.js.map +1 -1
- package/dist/date-picker.js +3 -3
- package/dist/genui.d.ts +219 -0
- package/dist/genui.js +506 -0
- package/dist/genui.js.map +1 -0
- package/dist/index.d.ts +17 -24
- package/dist/index.js +1 -1
- package/dist/marketing-layout.js +1 -1
- package/dist/marketing-layout.js.map +1 -1
- package/dist/media-grid.js +43 -151
- package/dist/media-grid.js.map +1 -1
- package/dist/meta-row.d.ts +54 -0
- package/dist/meta-row.js +35 -0
- package/dist/meta-row.js.map +1 -0
- package/dist/native.d.ts +25 -16
- package/dist/native.js +493 -520
- package/dist/native.js.map +1 -1
- package/dist/nav-bar.js +2 -2
- package/dist/page-hero.js +3 -3
- package/dist/site-footer.js +2 -2
- package/dist/skip-link.js +1 -1
- package/dist/step-showcase.d.ts +62 -0
- package/dist/step-showcase.js +227 -0
- package/dist/step-showcase.js.map +1 -0
- package/package.json +61 -4
- package/registry/achievement.json +1 -1
- package/registry/action-bar.json +1 -1
- package/registry/activity-feed.json +1 -1
- package/registry/alert.json +1 -1
- package/registry/app-shell.json +1 -1
- package/registry/argument-card.json +1 -1
- package/registry/audio-player.json +1 -1
- package/registry/auth-layout.json +1 -1
- package/registry/avatar.json +1 -1
- package/registry/badge.json +1 -1
- package/registry/button.json +1 -1
- package/registry/card-grid.json +1 -1
- package/registry/card.json +1 -1
- package/registry/catalogue-page.json +1 -1
- package/registry/changelog-page.json +1 -1
- package/registry/chat.json +1 -1
- package/registry/checkbox.json +1 -1
- package/registry/chip-link.json +1 -1
- package/registry/chip.json +1 -1
- package/registry/choice-grid.json +1 -1
- package/registry/code-block.json +1 -1
- package/registry/combobox.json +1 -1
- package/registry/command.json +1 -1
- package/registry/comparison-table.json +207 -0
- package/registry/component-page.json +1 -1
- package/registry/confirm-button.json +1 -1
- package/registry/contact-page.json +1 -1
- package/registry/continue-prompt.json +1 -1
- package/registry/countdown.json +1 -1
- package/registry/cta-panel.json +1 -1
- package/registry/dashboard-layout.json +1 -1
- package/registry/data-table.json +1 -1
- package/registry/date-picker.json +1 -1
- package/registry/decision-list.json +1 -1
- package/registry/description-list.json +1 -1
- package/registry/detail-layout.json +1 -1
- package/registry/dialog.json +1 -1
- package/registry/docs-article-page.json +1 -1
- package/registry/docs-hub-page.json +1 -1
- package/registry/docs-layout.json +1 -1
- package/registry/document-viewer.json +1 -1
- package/registry/dropdown-menu.json +1 -1
- package/registry/empty-state.json +1 -1
- package/registry/error-page.json +1 -1
- package/registry/examples/comparison-table/versus.d.ts +4 -0
- package/registry/examples/comparison-table/versus.js +28 -0
- package/registry/examples/comparison-table/versus.tsx +20 -0
- package/registry/examples/dialog/native/sheet.dark.png +0 -0
- package/registry/examples/dialog/native/sheet.light.png +0 -0
- package/registry/examples/genui/streaming.d.ts +4 -0
- package/registry/examples/genui/streaming.js +18 -0
- package/registry/examples/genui/streaming.tsx +14 -0
- package/registry/examples/genui/table.d.ts +4 -0
- package/registry/examples/genui/table.js +19 -0
- package/registry/examples/genui/table.tsx +14 -0
- package/registry/examples/meta-row/native/video.d.ts +4 -0
- package/registry/examples/meta-row/native/video.dark.png +0 -0
- package/registry/examples/meta-row/native/video.js +11 -0
- package/registry/examples/meta-row/native/video.light.png +0 -0
- package/registry/examples/meta-row/native/video.tsx +5 -0
- package/registry/examples/meta-row/status.d.ts +4 -0
- package/registry/examples/meta-row/status.js +12 -0
- package/registry/examples/meta-row/status.tsx +7 -0
- package/registry/examples/meta-row/video.d.ts +4 -0
- package/registry/examples/meta-row/video.js +12 -0
- package/registry/examples/meta-row/video.tsx +7 -0
- package/registry/examples/step-showcase/desktop.d.ts +4 -0
- package/registry/examples/step-showcase/desktop.js +24 -0
- package/registry/examples/step-showcase/desktop.tsx +19 -0
- package/registry/examples/step-showcase/phone.d.ts +4 -0
- package/registry/examples/step-showcase/phone.js +24 -0
- package/registry/examples/step-showcase/phone.tsx +19 -0
- package/registry/field.json +1 -1
- package/registry/file-upload.json +1 -1
- package/registry/filter-bar.json +1 -1
- package/registry/force-update-screen.json +1 -1
- package/registry/form.json +1 -1
- package/registry/gallery-page.json +1 -1
- package/registry/genui.json +409 -0
- package/registry/glow-card.json +1 -1
- package/registry/goal-gauge.json +1 -1
- package/registry/how-it-works-page.json +1 -1
- package/registry/image-cropper.json +1 -1
- package/registry/index.json +130 -4
- package/registry/iso.json +1 -1
- package/registry/kanban.json +1 -1
- package/registry/kbd.json +1 -1
- package/registry/landing-page.json +1 -1
- package/registry/landscape.json +1 -1
- package/registry/legal-page.json +1 -1
- package/registry/link-card.json +1 -1
- package/registry/list.json +1 -1
- package/registry/loader.json +1 -1
- package/registry/longform-page.json +1 -1
- package/registry/maintenance-screen.json +1 -1
- package/registry/map.json +2 -2
- package/registry/markdown.json +1 -1
- package/registry/marketing-layout.json +1 -1
- package/registry/marquee.json +1 -1
- package/registry/mascot-quote.json +1 -1
- package/registry/mascot-state.json +1 -1
- package/registry/mascots.json +1 -1
- package/registry/match-dialog.json +1 -1
- package/registry/media-capture.json +1 -1
- package/registry/media-grid.json +1 -1
- package/registry/meta-row.json +215 -0
- package/registry/meter.json +1 -1
- package/registry/nav-bar.json +13 -13
- package/registry/number-ticker.json +1 -1
- package/registry/page-header.json +1 -1
- package/registry/page-hero.json +1 -1
- package/registry/page.json +1 -1
- package/registry/pagination.json +1 -1
- package/registry/photo-grid.json +1 -1
- package/registry/pointer-light.json +1 -1
- package/registry/popover.json +1 -1
- package/registry/pricing-page.json +1 -1
- package/registry/product-page.json +1 -1
- package/registry/profile-card.json +1 -1
- package/registry/progress.json +1 -1
- package/registry/promise-card.json +1 -1
- package/registry/radio-group.json +1 -1
- package/registry/recipes.json +1 -1
- package/registry/scroll-stage.json +1 -1
- package/registry/search-field.json +1 -1
- package/registry/section-backdrop.json +1 -1
- package/registry/separator.json +1 -1
- package/registry/settings-layout.json +1 -1
- package/registry/showcase.json +1 -1
- package/registry/site-footer.json +13 -13
- package/registry/sites.json +1 -1
- package/registry/skeleton.json +1 -1
- package/registry/skip-link.json +2 -2
- package/registry/slider.json +1 -1
- package/registry/slot.json +1 -1
- package/registry/spinner.json +1 -1
- package/registry/split-view.json +1 -1
- package/registry/stage-banner.json +1 -1
- package/registry/stat.json +1 -1
- package/registry/status-page.json +1 -1
- package/registry/step-progress.json +1 -1
- package/registry/step-showcase.json +296 -0
- package/registry/stepper.json +1 -1
- package/registry/swipe-deck.json +1 -1
- package/registry/switch.json +1 -1
- package/registry/tabs.json +1 -1
- package/registry/tag-input.json +1 -1
- package/registry/tag-selector.json +1 -1
- package/registry/terminal-replay.json +1 -1
- package/registry/theme.json +1 -1
- package/registry/toast.json +1 -1
- package/registry/toc.json +1 -1
- package/registry/tooltip.json +1 -1
- package/registry/tree-view.json +1 -1
- package/registry/update-prompt.json +1 -1
- package/registry/verified-badge.json +1 -1
- package/registry/vertical-video.json +1 -1
- package/registry/video-player.json +1 -1
- package/registry/video-trimmer.json +1 -1
- package/registry/virtual.json +1 -1
- package/dist/chunk-3GODX3D6.js.map +0 -1
- package/dist/chunk-LIPK4UCS.js.map +0 -1
- /package/dist/{chunk-5MH64NIL.js.map → chunk-2L67YTNV.js.map} +0 -0
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "genui",
|
|
3
|
+
"type": "primitive",
|
|
4
|
+
"title": "Generated view",
|
|
5
|
+
"summary": "Draws the interface a model describes in A2UI v0.9 — tables, figures, charts, cards, callouts — from a catalog the app declares, never from code.",
|
|
6
|
+
"description": "Draws the interface a model describes in A2UI v0.9 — tables, figures, charts, cards, callouts — from a catalog the app declares, never from code.",
|
|
7
|
+
"status": "beta",
|
|
8
|
+
"deprecated": null,
|
|
9
|
+
"category": "data-display",
|
|
10
|
+
"platforms": "web",
|
|
11
|
+
"parity": {
|
|
12
|
+
"web": {
|
|
13
|
+
"state": "yes"
|
|
14
|
+
},
|
|
15
|
+
"native": {
|
|
16
|
+
"state": "gap",
|
|
17
|
+
"reason": "Slice 3 of the data-driven UI epic: the same messages drawn with the native primitives.",
|
|
18
|
+
"issue": "krizaka/orazaka#207"
|
|
19
|
+
},
|
|
20
|
+
"desktop": {
|
|
21
|
+
"state": "yes"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"whenToUse": [
|
|
25
|
+
"When a model answers a question about data and a table, a figure or a chart says it better than a paragraph.",
|
|
26
|
+
"In a chat that streams: the view grows message by message while the answer arrives.",
|
|
27
|
+
"When the same answer must be drawn by several apps: the messages are data, each app draws them with its catalog."
|
|
28
|
+
],
|
|
29
|
+
"whenNotToUse": [
|
|
30
|
+
{
|
|
31
|
+
"when": "For an interface the app knows in advance: write it with the components themselves.",
|
|
32
|
+
"use": "data-table"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"when": "For prose: render the model's text as markdown.",
|
|
36
|
+
"use": "markdown"
|
|
37
|
+
}
|
|
38
|
+
],
|
|
39
|
+
"bestPractices": [
|
|
40
|
+
"Merge `baseGenUiCatalog` with `chartsGenUiCatalog` of @krizaka/charts; a component outside the catalog is refused, never guessed.",
|
|
41
|
+
"Pass `onError` and log what was refused: a refused message leaves the rest of the view as it was.",
|
|
42
|
+
"Give the model the catalog and the rule that every number comes from the data it was given — the renderer checks shapes, not truth."
|
|
43
|
+
],
|
|
44
|
+
"accessibility": {
|
|
45
|
+
"keyboard": [],
|
|
46
|
+
"notes": [
|
|
47
|
+
"The view is a `section` named by `labels.surface`, `aria-busy` while the answer streams.",
|
|
48
|
+
"Text is rendered as text (escaped): no HTML, no script, no link in the catalog v1.",
|
|
49
|
+
"Charts carry their own written summary and data table (@krizaka/charts); tables are `DataTable`s with a caption."
|
|
50
|
+
]
|
|
51
|
+
},
|
|
52
|
+
"related": [
|
|
53
|
+
"data-table",
|
|
54
|
+
"stat",
|
|
55
|
+
"alert",
|
|
56
|
+
"card",
|
|
57
|
+
"markdown",
|
|
58
|
+
"chat"
|
|
59
|
+
],
|
|
60
|
+
"web": {
|
|
61
|
+
"entry": "@krizaka/ui/genui",
|
|
62
|
+
"import": "import { GenUiStream, GenUiSurface, baseGenUiCatalog, validateMessage, readStream } from \"@krizaka/ui/genui\";",
|
|
63
|
+
"props": [
|
|
64
|
+
{
|
|
65
|
+
"component": "GENUI_CATALOG_ID",
|
|
66
|
+
"description": "The catalog a surface must name: the Krizaka components, version 1.",
|
|
67
|
+
"props": []
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"component": "GENUI_FORMATS",
|
|
71
|
+
"description": "The value formats a model may name.",
|
|
72
|
+
"props": []
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"component": "GENUI_VERSION",
|
|
76
|
+
"description": "The protocol version every message carries.",
|
|
77
|
+
"props": []
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"component": "GenUiStream",
|
|
81
|
+
"description": "A model's answer with its generated view: the prose, then every surface of its `a2ui` fence, growing as the answer\nstreams. An answer without a valid surface falls back to its text.",
|
|
82
|
+
"props": [
|
|
83
|
+
{
|
|
84
|
+
"name": "catalog",
|
|
85
|
+
"type": "Readonly<Record<string, GenUiEntry>>",
|
|
86
|
+
"default": null,
|
|
87
|
+
"description": "The components it may use.",
|
|
88
|
+
"required": true
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"name": "text",
|
|
92
|
+
"type": "string",
|
|
93
|
+
"default": null,
|
|
94
|
+
"description": "A model's answer, complete or still arriving: its prose and its `a2ui` fence.",
|
|
95
|
+
"required": true
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"name": "className",
|
|
99
|
+
"type": "string",
|
|
100
|
+
"default": null,
|
|
101
|
+
"description": "Extra classes on the frame.",
|
|
102
|
+
"required": false
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
"name": "fallback",
|
|
106
|
+
"type": "((text: string) => ReactNode)",
|
|
107
|
+
"default": null,
|
|
108
|
+
"description": "What to show when the answer holds no valid surface: the answer itself, as text (the fallback).",
|
|
109
|
+
"required": false
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
"name": "labels",
|
|
113
|
+
"type": "Partial<GenUiLabels>",
|
|
114
|
+
"default": null,
|
|
115
|
+
"description": "The surface's words.",
|
|
116
|
+
"required": false
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
"name": "locale",
|
|
120
|
+
"type": "string",
|
|
121
|
+
"default": null,
|
|
122
|
+
"description": "The locale numbers are written in. Default `en`.",
|
|
123
|
+
"required": false
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
"name": "onError",
|
|
127
|
+
"type": "((error: string) => void)",
|
|
128
|
+
"default": null,
|
|
129
|
+
"description": "Called with what could not be drawn (a cycle, a missing child once the answer is complete).",
|
|
130
|
+
"required": false
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
"name": "renderProse",
|
|
134
|
+
"type": "((prose: string) => ReactNode)",
|
|
135
|
+
"default": null,
|
|
136
|
+
"description": "Draws the prose (markdown in a chat); default: paragraphs of text.",
|
|
137
|
+
"required": false
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
"name": "streaming",
|
|
141
|
+
"type": "boolean",
|
|
142
|
+
"default": null,
|
|
143
|
+
"description": "The answer is still arriving: missing children are skeletons and the surface is busy.",
|
|
144
|
+
"required": false
|
|
145
|
+
}
|
|
146
|
+
]
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
"component": "GenUiSurface",
|
|
150
|
+
"description": "One surface, drawn from its root.",
|
|
151
|
+
"props": [
|
|
152
|
+
{
|
|
153
|
+
"name": "catalog",
|
|
154
|
+
"type": "Readonly<Record<string, GenUiEntry>>",
|
|
155
|
+
"default": null,
|
|
156
|
+
"description": "The components it may use.",
|
|
157
|
+
"required": true
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
"name": "surface",
|
|
161
|
+
"type": "GenUiSurfaceState",
|
|
162
|
+
"default": null,
|
|
163
|
+
"description": "The surface to draw (from `readStream` or `applyMessage`).",
|
|
164
|
+
"required": true
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
"name": "className",
|
|
168
|
+
"type": "string",
|
|
169
|
+
"default": null,
|
|
170
|
+
"description": "Extra classes on the frame.",
|
|
171
|
+
"required": false
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
"name": "labels",
|
|
175
|
+
"type": "Partial<GenUiLabels>",
|
|
176
|
+
"default": null,
|
|
177
|
+
"description": "The surface's words.",
|
|
178
|
+
"required": false
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
"name": "locale",
|
|
182
|
+
"type": "string",
|
|
183
|
+
"default": "en",
|
|
184
|
+
"description": "The locale numbers are written in. Default `en`.",
|
|
185
|
+
"required": false
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
"name": "onError",
|
|
189
|
+
"type": "((error: string) => void)",
|
|
190
|
+
"default": null,
|
|
191
|
+
"description": "Called with what could not be drawn (a cycle, a missing child once the answer is complete).",
|
|
192
|
+
"required": false
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
"name": "streaming",
|
|
196
|
+
"type": "boolean",
|
|
197
|
+
"default": "false",
|
|
198
|
+
"description": "The answer is still arriving: missing children are skeletons and the surface is busy.",
|
|
199
|
+
"required": false
|
|
200
|
+
}
|
|
201
|
+
]
|
|
202
|
+
}
|
|
203
|
+
],
|
|
204
|
+
"examples": [
|
|
205
|
+
{
|
|
206
|
+
"name": "table",
|
|
207
|
+
"title": "A table from a model's answer",
|
|
208
|
+
"description": "Prose, then a callout and a sortable table bound to the data model.",
|
|
209
|
+
"path": "examples/genui/table.tsx",
|
|
210
|
+
"module": "@krizaka/ui/registry/examples/genui/table",
|
|
211
|
+
"code": "import { baseGenUiCatalog, GenUiStream } from \"@krizaka/ui/genui\";\n\nconst ANSWER = [\n \"Three cities sold more than 100 passes this month.\",\n \"```a2ui\",\n '{\"version\":\"v0.9\",\"createSurface\":{\"surfaceId\":\"s1\",\"catalogId\":\"krizaka:genui/v1\"}}',\n '{\"version\":\"v0.9\",\"updateComponents\":{\"surfaceId\":\"s1\",\"components\":[{\"id\":\"root\",\"component\":\"Column\",\"children\":[\"note\",\"grid\"]},{\"id\":\"note\",\"component\":\"Callout\",\"tone\":\"success\",\"title\":\"Montréal leads\",\"text\":\"412 passes, +18 % on last month.\"},{\"id\":\"grid\",\"component\":\"Table\",\"caption\":\"Passes by city\",\"columns\":[{\"key\":\"city\",\"label\":\"City\"},{\"key\":\"passes\",\"label\":\"Passes\",\"format\":\"integer\"}],\"rows\":{\"path\":\"/cities\"}}]}}',\n '{\"version\":\"v0.9\",\"updateDataModel\":{\"surfaceId\":\"s1\",\"path\":\"/cities\",\"value\":[{\"city\":\"Montréal\",\"passes\":412},{\"city\":\"Québec\",\"passes\":187},{\"city\":\"Laval\",\"passes\":121}]}}',\n \"```\",\n].join(\"\\n\");\n\nexport default function GenUiTable() {\n return <GenUiStream text={ANSWER} catalog={baseGenUiCatalog} />;\n}\n"
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
"name": "streaming",
|
|
215
|
+
"title": "While the answer arrives",
|
|
216
|
+
"description": "A half-written answer: what has arrived is drawn, the rest is a skeleton.",
|
|
217
|
+
"path": "examples/genui/streaming.tsx",
|
|
218
|
+
"module": "@krizaka/ui/registry/examples/genui/streaming",
|
|
219
|
+
"code": "import { baseGenUiCatalog, GenUiStream } from \"@krizaka/ui/genui\";\n\n// The answer stops mid-way: the card has arrived, its table not yet.\nconst PARTIAL = [\n \"Here is the summary so far.\",\n \"```a2ui\",\n '{\"version\":\"v0.9\",\"createSurface\":{\"surfaceId\":\"s1\",\"catalogId\":\"krizaka:genui/v1\"}}',\n '{\"version\":\"v0.9\",\"updateComponents\":{\"surfaceId\":\"s1\",\"components\":[{\"id\":\"root\",\"component\":\"Card\",\"title\":\"Weekly signups\",\"child\":\"body\"}]}}',\n '{\"version\":\"v0.9\",\"updateComponents\":{\"surfaceId\":\"s1\",\"comp',\n].join(\"\\n\");\n\nexport default function GenUiStreaming() {\n return <GenUiStream text={PARTIAL} catalog={baseGenUiCatalog} streaming />;\n}\n"
|
|
220
|
+
}
|
|
221
|
+
]
|
|
222
|
+
},
|
|
223
|
+
"native": null,
|
|
224
|
+
"files": [
|
|
225
|
+
{
|
|
226
|
+
"path": "genui/catalog.tsx",
|
|
227
|
+
"content": "// The base catalog of `krizaka:genui/v1` (ADR-082): layout, text, card, callout and table — what a model may draw with\n// @krizaka/ui alone. `KpiStat` and `ChartCard` come from @krizaka/charts (`chartsGenUiCatalog`), which depends on this\n// package, never the reverse.\nimport { formatCompact, formatMoney, formatNumber, formatPercent } from \"@krizaka/intl\";\nimport type * as React from \"react\";\n\nimport { Alert } from \"../alert\";\nimport { CardBody, CardRoot, CardTitle } from \"../card\";\nimport { DataTable, type DataTableColumn } from \"../data-table\";\nimport {\n allowKeys,\n checkBinding,\n checkChildren,\n checkCurrency,\n checkEnum,\n checkId,\n checkText,\n GENUI_FORMATS,\n GENUI_LIMITS,\n type GenUiComponent,\n type GenUiFormat,\n} from \"./spec\";\nimport type { GenUiCatalog, GenUiEntry, GenUiRenderContext } from \"./surface\";\n\nconst first = (...problems: (string | null)[]) => problems.find((p) => p !== null) ?? null;\n\n/** A resolved value as text — strings and numbers only; anything else is not drawn. */\nfunction asText(value: unknown): string {\n if (typeof value === \"string\") return value.slice(0, GENUI_LIMITS.string);\n if (typeof value === \"number\" && Number.isFinite(value)) return String(value);\n return \"\";\n}\n\n/** A number written in one of the closed formats. */\nexport function formatGenUiValue(value: unknown, format: GenUiFormat | undefined, locale: string, currency?: string): string {\n if (typeof value !== \"number\" || !Number.isFinite(value)) return asText(value);\n switch (format) {\n case \"integer\":\n return formatNumber(Math.round(value), { locale, maximumFractionDigits: 0 });\n case \"compact\":\n return formatCompact(value, { locale });\n case \"percent\":\n return formatPercent(value, { locale, maximumFractionDigits: 1 });\n case \"money\":\n return currency ? formatMoney(Math.round(value), { currency, locale }) : formatNumber(value, { locale });\n default:\n return formatNumber(value, { locale, maximumFractionDigits: 2 });\n }\n}\n\nconst TONES = [\"info\", \"success\", \"warning\", \"danger\"] as const;\nconst VARIANTS = [\"body\", \"heading\", \"caption\"] as const;\n\nconst column: GenUiEntry = {\n check: (c) => first(allowKeys(c, [\"children\"]), checkChildren(c.children)),\n render: (c, ctx) => <div className=\"flex min-w-0 flex-col gap-4\">{ctx.children(c.children)}</div>,\n};\n\nconst row: GenUiEntry = {\n check: (c) => first(allowKeys(c, [\"children\"]), checkChildren(c.children)),\n render: (c, ctx) => <div className=\"grid min-w-0 gap-4 [grid-template-columns:repeat(auto-fit,minmax(12rem,1fr))]\">{ctx.children(c.children)}</div>,\n};\n\nconst text: GenUiEntry = {\n check: (c) => first(allowKeys(c, [\"text\", \"variant\"]), checkText(c.text, \"text\", true), checkEnum(c.variant, \"variant\", VARIANTS)),\n render: (c, ctx) => {\n const value = asText(ctx.resolve(c.text));\n if (c.variant === \"heading\") return <h3 className=\"text-base font-semibold text-fg\">{value}</h3>;\n if (c.variant === \"caption\") return <p className=\"text-xs text-fg-muted\">{value}</p>;\n return <p className=\"text-sm text-fg\">{value}</p>;\n },\n};\n\nconst card: GenUiEntry = {\n check: (c) => first(allowKeys(c, [\"title\", \"child\"]), checkText(c.title, \"title\", false), c.child === undefined ? null : checkId(c.child, \"child\")),\n render: (c, ctx) => (\n <CardRoot>\n <CardBody>\n {c.title !== undefined && <CardTitle>{asText(ctx.resolve(c.title))}</CardTitle>}\n {c.child !== undefined && <div className=\"mt-3\">{ctx.child(c.child)}</div>}\n </CardBody>\n </CardRoot>\n ),\n};\n\nconst callout: GenUiEntry = {\n check: (c) => first(allowKeys(c, [\"tone\", \"title\", \"text\"]), checkEnum(c.tone, \"tone\", TONES), checkText(c.title, \"title\", false), checkText(c.text, \"text\", false)),\n render: (c, ctx) => (\n <Alert tone={(c.tone as (typeof TONES)[number] | undefined) ?? \"info\"} title={c.title === undefined ? undefined : asText(ctx.resolve(c.title))}>\n {c.text === undefined ? undefined : asText(ctx.resolve(c.text))}\n </Alert>\n ),\n};\n\ntype Column = { key: string; label: string; format?: GenUiFormat; currency?: string };\n\nfunction checkColumns(value: unknown): string | null {\n if (!Array.isArray(value) || value.length === 0 || value.length > 12) return \"columns is an array of 1 to 12\";\n for (const raw of value) {\n if (raw === null || typeof raw !== \"object\" || Array.isArray(raw)) return \"a column is an object\";\n const col = raw as Record<string, unknown>;\n const keys = Object.keys(col).find((k) => ![\"key\", \"label\", \"format\", \"currency\"].includes(k));\n if (keys) return `a column has no key \"${keys}\"`;\n if (typeof col.key !== \"string\" || !/^[A-Za-z0-9_]{1,64}$/.test(col.key)) return \"a column key is 1-64 characters of [A-Za-z0-9_]\";\n if (typeof col.label !== \"string\" || col.label.length > 120) return \"a column label is a string\";\n const problem = first(checkEnum(col.format, \"format\", GENUI_FORMATS), checkCurrency(col.currency, col.format === \"money\"));\n if (problem) return problem;\n }\n return null;\n}\n\ntype Row = Record<string, unknown> & { __genuiRow: string };\n\nfunction TableView({ c, ctx }: { c: GenUiComponent; ctx: GenUiRenderContext }) {\n const columns = c.columns as readonly Column[];\n const raw = ctx.resolve(c.rows);\n const rows: Row[] = Array.isArray(raw)\n ? raw.slice(0, GENUI_LIMITS.array).flatMap((r, i) => (r !== null && typeof r === \"object\" && !Array.isArray(r) ? [{ ...(r as Record<string, unknown>), __genuiRow: String(i) }] : []))\n : [];\n const cols: DataTableColumn<Row>[] = columns.map((col) => ({\n id: col.key,\n header: col.label,\n sortable: true,\n type: col.format ? \"number\" : \"text\",\n value: (r) => {\n const v = r[col.key];\n return typeof v === \"number\" || typeof v === \"string\" ? v : null;\n },\n cell: (r) => formatGenUiValue(r[col.key], col.format, ctx.locale, col.currency),\n }));\n if (rows.length === 0) return <p className=\"text-sm text-fg-muted\">{ctx.labels.noData}</p>;\n return <DataTable rows={rows} columns={cols} getRowId={(r) => r.__genuiRow} caption={c.caption === undefined ? ctx.labels.surface : asText(ctx.resolve(c.caption))} showCaption={c.caption !== undefined} />;\n}\n\nconst table: GenUiEntry = {\n check: (c) => first(allowKeys(c, [\"caption\", \"columns\", \"rows\"]), checkText(c.caption, \"caption\", false), checkColumns(c.columns), checkBinding(c.rows, \"rows\")),\n render: (c, ctx): React.ReactNode => <TableView c={c} ctx={ctx} />,\n};\n\n/** `Column`, `Row`, `Text`, `Card`, `Callout`, `Table` — the catalog @krizaka/ui draws on its own. */\nexport const baseGenUiCatalog: GenUiCatalog = { Column: column, Row: row, Text: text, Card: card, Callout: callout, Table: table };\n"
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
"path": "genui/index.ts",
|
|
231
|
+
"content": "/** @krizaka/ui/genui — data-driven UI on A2UI v0.9 (ADR-082): `GenUiStream`, `GenUiSurface`, `baseGenUiCatalog`, the validator (`validateMessage`), the store (`readStream`, `applyMessage`, `applyLine`, `extractGenUi`), the shared checks for catalog entries. Client (\"use client\"). */\nexport { baseGenUiCatalog, formatGenUiValue } from \"./catalog\";\nexport {\n allowKeys,\n checkBinding,\n checkChildren,\n checkCurrency,\n checkEnum,\n checkId,\n checkNumber,\n checkText,\n GENUI_CATALOG_ID,\n GENUI_FORMATS,\n GENUI_LIMITS,\n GENUI_VERSION,\n type GenUiBinding,\n type GenUiComponent,\n type GenUiFormat,\n type GenUiMessage,\n type GenUiPropsCheck,\n type GenUiResult,\n isBinding,\n parsePath,\n sanitizeData,\n validateMessage,\n} from \"./spec\";\nexport { applyLine, applyMessage, emptyGenUiState, extractGenUi, type GenUiExtract, type GenUiState, type GenUiSurfaceState, readPath, readStream } from \"./store\";\nexport {\n type GenUiCatalog,\n type GenUiEntry,\n type GenUiLabels,\n genUiLabels,\n type GenUiRenderContext,\n GenUiStream,\n type GenUiStreamProps,\n GenUiSurface,\n type GenUiSurfaceProps,\n} from \"./surface\";\n"
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
"path": "genui/meta.ts",
|
|
235
|
+
"content": "import type { ComponentMeta } from \"../meta\";\n\nexport const meta = {\n title: \"Generated view\",\n summary: \"Draws the interface a model describes in A2UI v0.9 — tables, figures, charts, cards, callouts — from a catalog the app declares, never from code.\",\n status: \"beta\",\n category: \"data-display\",\n platforms: \"web\",\n parity: {\n native: { status: \"gap\", issue: \"krizaka/orazaka#207\", reason: \"Slice 3 of the data-driven UI epic: the same messages drawn with the native primitives.\" },\n desktop: \"web\",\n },\n whenToUse: [\n \"When a model answers a question about data and a table, a figure or a chart says it better than a paragraph.\",\n \"In a chat that streams: the view grows message by message while the answer arrives.\",\n \"When the same answer must be drawn by several apps: the messages are data, each app draws them with its catalog.\",\n ],\n whenNotToUse: [\n { when: \"For an interface the app knows in advance: write it with the components themselves.\", use: \"data-table\" },\n { when: \"For prose: render the model's text as markdown.\", use: \"markdown\" },\n ],\n bestPractices: [\n \"Merge `baseGenUiCatalog` with `chartsGenUiCatalog` of @krizaka/charts; a component outside the catalog is refused, never guessed.\",\n \"Pass `onError` and log what was refused: a refused message leaves the rest of the view as it was.\",\n \"Give the model the catalog and the rule that every number comes from the data it was given — the renderer checks shapes, not truth.\",\n ],\n accessibility: {\n keyboard: [],\n notes: [\n \"The view is a `section` named by `labels.surface`, `aria-busy` while the answer streams.\",\n \"Text is rendered as text (escaped): no HTML, no script, no link in the catalog v1.\",\n \"Charts carry their own written summary and data table (@krizaka/charts); tables are `DataTable`s with a caption.\",\n ],\n },\n related: [\"data-table\", \"stat\", \"alert\", \"card\", \"markdown\", \"chat\"],\n web: {\n imports: [\"GenUiStream\", \"GenUiSurface\", \"baseGenUiCatalog\", \"validateMessage\", \"readStream\"],\n examples: [\n { name: \"table\", title: \"A table from a model's answer\", description: \"Prose, then a callout and a sortable table bound to the data model.\" },\n { name: \"streaming\", title: \"While the answer arrives\", description: \"A half-written answer: what has arrived is drawn, the rest is a skeleton.\" },\n ],\n },\n} satisfies ComponentMeta;\n"
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
"path": "genui/spec.ts",
|
|
239
|
+
"content": "// The data-driven UI spec: a strict subset of A2UI v0.9 (ADR-082, krizaka/orazaka-packs). A model describes an interface\n// as data — never code — and only with the components of a catalog the client declares. Everything here is a pure\n// function over JSON: parse, validate, apply. Nothing is evaluated, nothing is trusted.\n\n/** The protocol version every message carries. */\nexport const GENUI_VERSION = \"v0.9\";\n\n/** The catalog a surface must name: the Krizaka components, version 1. */\nexport const GENUI_CATALOG_ID = \"krizaka:genui/v1\";\n\n/** The bounds of a surface — what a model may ask for, at most. */\nexport const GENUI_LIMITS = {\n /** Components per surface. */\n components: 300,\n /** Nesting depth from the root. */\n depth: 12,\n /** Characters in one string. */\n string: 4000,\n /** Items in one array of the data model. */\n array: 1000,\n /** Nesting depth of a data model value. */\n dataDepth: 12,\n /** Values (objects, arrays, scalars) in one data model update. */\n dataNodes: 50_000,\n /** Segments of a data path. */\n pathSegments: 16,\n} as const;\n\n/** One component as the model writes it: an id, the catalog name, its properties. */\nexport type GenUiComponent = { readonly id: string; readonly component: string } & Readonly<Record<string, unknown>>;\n\n/** A value bound to the data model: `{ \"path\": \"/rows\" }`. */\nexport type GenUiBinding = { readonly path: string };\n\n/** The four messages of A2UI v0.9. */\nexport type GenUiMessage =\n | { readonly version: typeof GENUI_VERSION; readonly createSurface: { readonly surfaceId: string; readonly catalogId: string } }\n | { readonly version: typeof GENUI_VERSION; readonly updateComponents: { readonly surfaceId: string; readonly components: readonly GenUiComponent[] } }\n | { readonly version: typeof GENUI_VERSION; readonly updateDataModel: { readonly surfaceId: string; readonly path: string; readonly value?: unknown } }\n | { readonly version: typeof GENUI_VERSION; readonly deleteSurface: { readonly surfaceId: string } };\n\n/** What validating a message says. */\nexport type GenUiResult<T> = { readonly ok: true; readonly value: T } | { readonly ok: false; readonly error: string };\n\n/** How a catalog entry checks a component's own properties: `null` when they are fine, else why not. */\nexport type GenUiPropsCheck = (component: GenUiComponent) => string | null;\n\nconst ID = /^[A-Za-z0-9_-]{1,64}$/;\nconst MESSAGE_KEYS = [\"createSurface\", \"updateComponents\", \"updateDataModel\", \"deleteSurface\"] as const;\nconst FORBIDDEN_SEGMENTS = new Set([\"__proto__\", \"prototype\", \"constructor\"]);\n\nconst fail = (error: string): { ok: false; error: string } => ({ ok: false, error });\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n if (value === null || typeof value !== \"object\" || Array.isArray(value)) return false;\n const proto = Object.getPrototypeOf(value);\n return proto === Object.prototype || proto === null;\n}\n\nfunction onlyKeys(value: Record<string, unknown>, allowed: readonly string[], where: string): string | null {\n for (const key of Object.keys(value)) if (!allowed.includes(key)) return `${where}: unknown key \"${key}\"`;\n return null;\n}\n\n/** An id of a surface or a component, or why not. */\nexport function checkId(value: unknown, where: string): string | null {\n return typeof value === \"string\" && ID.test(value) ? null : `${where}: an id is 1-64 characters of [A-Za-z0-9_-]`;\n}\n\n/** The segments of a JSON pointer (`/a/b/0`), or why it is refused — `__proto__` and friends never pass. */\nexport function parsePath(path: unknown): GenUiResult<readonly string[]> {\n if (typeof path !== \"string\" || (path !== \"/\" && !path.startsWith(\"/\")) || path.length > 512) return fail(\"a path is a JSON pointer: /a/b\");\n if (path === \"/\") return { ok: true, value: [] };\n const segments = path\n .slice(1)\n .split(\"/\")\n .map((s) => s.replace(/~1/g, \"/\").replace(/~0/g, \"~\"));\n if (segments.length > GENUI_LIMITS.pathSegments) return fail(`a path has at most ${GENUI_LIMITS.pathSegments} segments`);\n for (const s of segments) {\n if (s === \"\" || FORBIDDEN_SEGMENTS.has(s)) return fail(`a path may not hold the segment \"${s}\"`);\n }\n return { ok: true, value: segments };\n}\n\n/** `true` when the value is a binding `{ path }` (and only that). */\nexport function isBinding(value: unknown): value is GenUiBinding {\n return isPlainObject(value) && Object.keys(value).length === 1 && typeof value.path === \"string\";\n}\n\n/**\n * A copy of a data model value that is plain JSON within the limits — strings, finite numbers, booleans, null, arrays\n * and plain objects — or why it is refused. The copy has a null prototype for every object: nothing it holds can\n * reach `Object.prototype`.\n */\nexport function sanitizeData(value: unknown): GenUiResult<unknown> {\n let nodes = 0;\n const walk = (v: unknown, depth: number): GenUiResult<unknown> => {\n if (++nodes > GENUI_LIMITS.dataNodes) return fail(`the data holds more than ${GENUI_LIMITS.dataNodes} values`);\n if (depth > GENUI_LIMITS.dataDepth) return fail(`the data nests deeper than ${GENUI_LIMITS.dataDepth}`);\n if (v === null || typeof v === \"boolean\") return { ok: true, value: v };\n if (typeof v === \"number\") return Number.isFinite(v) ? { ok: true, value: v } : fail(\"a number must be finite\");\n if (typeof v === \"string\") return v.length <= GENUI_LIMITS.string ? { ok: true, value: v } : fail(`a string has at most ${GENUI_LIMITS.string} characters`);\n if (Array.isArray(v)) {\n if (v.length > GENUI_LIMITS.array) return fail(`an array has at most ${GENUI_LIMITS.array} items`);\n const out: unknown[] = [];\n for (const item of v) {\n const r = walk(item, depth + 1);\n if (!r.ok) return r;\n out.push(r.value);\n }\n return { ok: true, value: out };\n }\n if (isPlainObject(v)) {\n const out: Record<string, unknown> = Object.create(null);\n for (const [key, item] of Object.entries(v)) {\n if (FORBIDDEN_SEGMENTS.has(key)) return fail(`a key may not be \"${key}\"`);\n const r = walk(item, depth + 1);\n if (!r.ok) return r;\n out[key] = r.value;\n }\n return { ok: true, value: out };\n }\n return fail(\"the data is plain JSON only\");\n };\n return walk(value, 0);\n}\n\n/**\n * A message as the model sent it — parsed and checked against the spec and the catalog, or why it is refused. A\n * component's own properties are checked by its catalog entry; a component the catalog does not hold is refused.\n */\nexport function validateMessage(input: unknown, catalog: Readonly<Record<string, { readonly check: GenUiPropsCheck }>>): GenUiResult<GenUiMessage> {\n if (!isPlainObject(input)) return fail(\"a message is a JSON object\");\n if (input.version !== GENUI_VERSION) return fail(`version must be \"${GENUI_VERSION}\"`);\n const present = MESSAGE_KEYS.filter((k) => k in input);\n if (present.length !== 1) return fail(\"a message holds exactly one of createSurface, updateComponents, updateDataModel, deleteSurface\");\n const kind = present[0];\n const extra = onlyKeys(input, [\"version\", kind], \"message\");\n if (extra) return fail(extra);\n const body = input[kind];\n if (!isPlainObject(body)) return fail(`${kind} is an object`);\n const surface = checkId(body.surfaceId, `${kind}.surfaceId`);\n if (surface) return fail(surface);\n\n switch (kind) {\n case \"createSurface\": {\n const keys = onlyKeys(body, [\"surfaceId\", \"catalogId\", \"theme\", \"sendDataModel\"], kind);\n if (keys) return fail(keys);\n if (body.catalogId !== GENUI_CATALOG_ID) return fail(`catalogId must be \"${GENUI_CATALOG_ID}\"`);\n return { ok: true, value: { version: GENUI_VERSION, createSurface: { surfaceId: body.surfaceId as string, catalogId: GENUI_CATALOG_ID } } };\n }\n case \"updateComponents\": {\n const keys = onlyKeys(body, [\"surfaceId\", \"components\"], kind);\n if (keys) return fail(keys);\n if (!Array.isArray(body.components) || body.components.length > GENUI_LIMITS.components) {\n return fail(`components is an array of at most ${GENUI_LIMITS.components}`);\n }\n const components: GenUiComponent[] = [];\n for (const raw of body.components) {\n if (!isPlainObject(raw)) return fail(\"a component is an object\");\n const id = checkId(raw.id, \"component.id\");\n if (id) return fail(id);\n if (typeof raw.component !== \"string\" || !Object.hasOwn(catalog, raw.component)) {\n return fail(`component \"${String(raw.id)}\": \"${String(raw.component)}\" is not in the catalog`);\n }\n const clean = sanitizeData(raw);\n if (!clean.ok) return fail(`component \"${raw.id}\": ${clean.error}`);\n const component = { ...(clean.value as Record<string, unknown>) } as GenUiComponent;\n const problem = catalog[raw.component].check(component);\n if (problem) return fail(`component \"${raw.id}\" (${raw.component}): ${problem}`);\n components.push(component);\n }\n return { ok: true, value: { version: GENUI_VERSION, updateComponents: { surfaceId: body.surfaceId as string, components } } };\n }\n case \"updateDataModel\": {\n const keys = onlyKeys(body, [\"surfaceId\", \"path\", \"value\"], kind);\n if (keys) return fail(keys);\n const path = body.path === undefined ? \"/\" : body.path;\n const parsed = parsePath(path);\n if (!parsed.ok) return parsed;\n if (!(\"value\" in body)) return { ok: true, value: { version: GENUI_VERSION, updateDataModel: { surfaceId: body.surfaceId as string, path: path as string } } };\n const clean = sanitizeData(body.value);\n if (!clean.ok) return clean;\n return { ok: true, value: { version: GENUI_VERSION, updateDataModel: { surfaceId: body.surfaceId as string, path: path as string, value: clean.value } } };\n }\n default: {\n const keys = onlyKeys(body, [\"surfaceId\"], kind);\n if (keys) return fail(keys);\n return { ok: true, value: { version: GENUI_VERSION, deleteSurface: { surfaceId: body.surfaceId as string } } };\n }\n }\n}\n\n// ── checks the catalog entries share ─────────────────────────────────────────────────────────────────────────────\n\n/** Why the component's keys are refused, or null: only `id`, `component` and the entry's own keys. */\nexport function allowKeys(component: GenUiComponent, keys: readonly string[]): string | null {\n return onlyKeys(component, [\"id\", \"component\", ...keys], \"properties\");\n}\n\n/** A short text (or a binding to one), required or not. */\nexport function checkText(value: unknown, name: string, required: boolean): string | null {\n if (value === undefined) return required ? `${name} is required` : null;\n if (isBinding(value)) return parsePath(value.path).ok ? null : `${name}: a bad path`;\n return typeof value === \"string\" ? null : `${name} is a string`;\n}\n\n/** A value among a closed list. */\nexport function checkEnum(value: unknown, name: string, allowed: readonly string[], required = false): string | null {\n if (value === undefined) return required ? `${name} is required` : null;\n return typeof value === \"string\" && allowed.includes(value) ? null : `${name} is one of ${allowed.join(\", \")}`;\n}\n\n/** A list of child ids. */\nexport function checkChildren(value: unknown, name = \"children\"): string | null {\n if (!Array.isArray(value) || value.length > GENUI_LIMITS.components) return `${name} is an array of component ids`;\n for (const id of value) if (checkId(id, name)) return `${name} holds ids only`;\n return null;\n}\n\n/** A binding to the data model (required). */\nexport function checkBinding(value: unknown, name: string): string | null {\n if (!isBinding(value)) return `${name} is a binding { \"path\": \"/…\" }`;\n return parsePath(value.path).ok ? null : `${name}: a bad path`;\n}\n\n/** A number, or a binding to one. */\nexport function checkNumber(value: unknown, name: string, required: boolean): string | null {\n if (value === undefined) return required ? `${name} is required` : null;\n if (isBinding(value)) return parsePath(value.path).ok ? null : `${name}: a bad path`;\n return typeof value === \"number\" && Number.isFinite(value) ? null : `${name} is a number`;\n}\n\n/** The value formats a model may name. */\nexport const GENUI_FORMATS = [\"number\", \"integer\", \"compact\", \"percent\", \"money\"] as const;\nexport type GenUiFormat = (typeof GENUI_FORMATS)[number];\n\n/** A three-letter currency code. */\nexport function checkCurrency(value: unknown, required: boolean): string | null {\n if (value === undefined) return required ? \"currency is required with the money format\" : null;\n return typeof value === \"string\" && /^[A-Z]{3}$/.test(value) ? null : \"currency is a three-letter code (CAD)\";\n}\n"
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
"path": "genui/store.ts",
|
|
243
|
+
"content": "// The surfaces a stream of A2UI messages builds — pure: `applyMessage` returns a new state, `readStream` turns a growing\n// text (a model's answer as it arrives) into the messages it holds so far. Nothing here renders.\nimport { GENUI_LIMITS, type GenUiComponent, type GenUiMessage, type GenUiPropsCheck, parsePath, validateMessage } from \"./spec\";\n\n/** One surface: its components by id, and its data model. */\nexport type GenUiSurfaceState = {\n readonly id: string;\n readonly components: ReadonlyMap<string, GenUiComponent>;\n readonly data: unknown;\n};\n\n/** Every surface of a stream, and what was refused on the way. */\nexport type GenUiState = {\n readonly surfaces: ReadonlyMap<string, GenUiSurfaceState>;\n /** The order surfaces were created in. */\n readonly order: readonly string[];\n /** Why messages were dropped, in order (a dropped message leaves the rest of the surface as it was). */\n readonly errors: readonly string[];\n};\n\nexport const emptyGenUiState: GenUiState = { surfaces: new Map(), order: [], errors: [] };\n\nconst withError = (state: GenUiState, error: string): GenUiState => ({ ...state, errors: [...state.errors, error] });\n\nfunction setAt(root: unknown, segments: readonly string[], value: unknown, remove: boolean): unknown {\n if (segments.length === 0) return remove ? Object.create(null) : value;\n const [head, ...rest] = segments;\n if (Array.isArray(root)) {\n const index = Number(head);\n if (!Number.isInteger(index) || index < 0 || index > root.length || index >= GENUI_LIMITS.array) return root;\n const copy = [...root];\n if (remove && rest.length === 0) copy.splice(index, 1);\n else copy[index] = setAt(root[index], rest, value, remove);\n return copy;\n }\n const base: Record<string, unknown> = Object.assign(Object.create(null), root !== null && typeof root === \"object\" ? root : {});\n if (remove && rest.length === 0) delete base[head];\n else base[head] = setAt(base[head], rest, value, remove);\n return base;\n}\n\n/** The value at a JSON pointer of the data model, `undefined` when there is none. */\nexport function readPath(data: unknown, path: string): unknown {\n const parsed = parsePath(path);\n if (!parsed.ok) return undefined;\n let cursor: unknown = data;\n for (const segment of parsed.value) {\n if (cursor === null || typeof cursor !== \"object\") return undefined;\n if (!Object.hasOwn(cursor, segment)) return undefined;\n cursor = (cursor as Record<string, unknown>)[segment];\n }\n return cursor;\n}\n\n/** The state after one validated message. */\nexport function applyMessage(state: GenUiState, message: GenUiMessage): GenUiState {\n if (\"createSurface\" in message) {\n const { surfaceId } = message.createSurface;\n if (state.surfaces.has(surfaceId)) return withError(state, `surface \"${surfaceId}\" already exists`);\n const surfaces = new Map(state.surfaces);\n surfaces.set(surfaceId, { id: surfaceId, components: new Map(), data: Object.create(null) });\n return { ...state, surfaces, order: [...state.order, surfaceId] };\n }\n if (\"deleteSurface\" in message) {\n const surfaces = new Map(state.surfaces);\n surfaces.delete(message.deleteSurface.surfaceId);\n return { ...state, surfaces, order: state.order.filter((id) => id !== message.deleteSurface.surfaceId) };\n }\n const surfaceId = \"updateComponents\" in message ? message.updateComponents.surfaceId : message.updateDataModel.surfaceId;\n const surface = state.surfaces.get(surfaceId);\n if (!surface) return withError(state, `surface \"${surfaceId}\" was not created`);\n const surfaces = new Map(state.surfaces);\n if (\"updateComponents\" in message) {\n const components = new Map(surface.components);\n for (const component of message.updateComponents.components) components.set(component.id, component);\n if (components.size > GENUI_LIMITS.components) return withError(state, `surface \"${surfaceId}\" holds more than ${GENUI_LIMITS.components} components`);\n surfaces.set(surfaceId, { ...surface, components });\n } else {\n const { path, value } = message.updateDataModel;\n const parsed = parsePath(path);\n if (!parsed.ok) return withError(state, parsed.error);\n surfaces.set(surfaceId, { ...surface, data: setAt(surface.data, parsed.value, value, !(\"value\" in message.updateDataModel)) });\n }\n return { ...state, surfaces };\n}\n\n/** The state after a line of JSON (one message), the line refused with its reason when it is not valid. */\nexport function applyLine(state: GenUiState, line: string, catalog: Readonly<Record<string, { readonly check: GenUiPropsCheck }>>): GenUiState {\n const text = line.trim();\n if (!text) return state;\n let json: unknown;\n try {\n json = JSON.parse(text);\n } catch {\n return withError(state, \"a line is not JSON\");\n }\n const result = validateMessage(json, catalog);\n return result.ok ? applyMessage(state, result.value) : withError(state, result.error);\n}\n\n/** Where a fenced `a2ui` block sits in a text: its JSON Lines so far, whether it is closed, and the prose around it. */\nexport type GenUiExtract = { readonly lines: readonly string[]; readonly closed: boolean; readonly prose: string; readonly found: boolean };\n\nconst FENCE = /```a2ui[^\\n]*\\n/;\n\n/**\n * Splits a model's answer — complete or still arriving — into its prose and the complete lines of its `a2ui` fence.\n * A line still being written (no newline yet, the fence open) is left for the next call: the surface grows message by\n * message as the answer streams.\n */\nexport function extractGenUi(text: string): GenUiExtract {\n const open = FENCE.exec(text);\n if (!open) return { lines: [], closed: false, prose: text, found: false };\n const start = open.index + open[0].length;\n const end = text.indexOf(\"\\n```\", start - 1);\n const closed = end >= 0;\n const body = closed ? text.slice(start, end) : text.slice(start);\n const parts = body.split(\"\\n\");\n const lines = closed ? parts : parts.slice(0, -1);\n const after = closed ? text.slice(end + 4).replace(/^[^\\n]*\\n?/, \"\") : \"\";\n const prose = [text.slice(0, open.index).trim(), after.trim()].filter(Boolean).join(\"\\n\\n\");\n return { lines: lines.filter((l) => l.trim()), closed, prose, found: true };\n}\n\n/** The state of every surface a model's answer describes so far. */\nexport function readStream(text: string, catalog: Readonly<Record<string, { readonly check: GenUiPropsCheck }>>): GenUiState & { readonly prose: string; readonly found: boolean } {\n const { lines, prose, found } = extractGenUi(text);\n const state = lines.reduce((s, line) => applyLine(s, line, catalog), emptyGenUiState);\n return { ...state, prose, found };\n}\n"
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
"path": "genui/surface.tsx",
|
|
247
|
+
"content": "// Renders a surface from its root: each component through its catalog entry, children by id, bound values from the data\n// model. A tree only — a component met twice (a cycle, two parents) or beyond the depth is not drawn — and a child still\n// to come (the answer is streaming) is a skeleton. Text is text: React escapes it, nothing is ever parsed as HTML.\nimport * as React from \"react\";\n\nimport { cn } from \"../cn\";\nimport { Skeleton } from \"../skeleton\";\nimport { GENUI_LIMITS, type GenUiComponent, type GenUiPropsCheck, isBinding } from \"./spec\";\nimport { type GenUiState, type GenUiSurfaceState, readPath, readStream } from \"./store\";\n\n/** What an entry gets to draw a component. */\nexport type GenUiRenderContext = {\n /** A child by id: drawn, a skeleton while it has not arrived, nothing when refused. */\n child: (id: unknown) => React.ReactNode;\n /** Children by id, in order. */\n children: (ids: unknown) => React.ReactNode[];\n /** A literal, or the value a binding `{ path }` points at in the data model. */\n resolve: (value: unknown) => unknown;\n /** The words of the surface. */\n labels: GenUiLabels;\n /** The locale numbers and dates are written in. */\n locale: string;\n};\n\n/** One component of a catalog: how its properties are checked, how it is drawn. */\nexport type GenUiEntry = {\n /** `null` when the component's own properties are fine, else why not (the message is refused). */\n readonly check: GenUiPropsCheck;\n /** Draws the component. */\n readonly render: (component: GenUiComponent, context: GenUiRenderContext) => React.ReactNode;\n};\n\n/** The components a model may use, by name: `baseGenUiCatalog`, merged with `chartsGenUiCatalog` of @krizaka/charts. */\nexport type GenUiCatalog = Readonly<Record<string, GenUiEntry>>;\n\n/** The surface's own words (passed translated). */\nexport type GenUiLabels = {\n /** Announced while the surface is still arriving. */\n loading: string;\n /** The accessible name of the surface. */\n surface: string;\n /** Under a table whose data has not arrived or is not a list. */\n noData: string;\n};\n\nexport const genUiLabels: GenUiLabels = { loading: \"Building the view…\", surface: \"Generated view\", noData: \"No data\" };\n\nexport type GenUiSurfaceProps = {\n /** The surface to draw (from `readStream` or `applyMessage`). */\n surface: GenUiSurfaceState;\n /** The components it may use. */\n catalog: GenUiCatalog;\n /** The answer is still arriving: missing children are skeletons and the surface is busy. */\n streaming?: boolean;\n /** The surface's words. */\n labels?: Partial<GenUiLabels>;\n /** The locale numbers are written in. Default `en`. */\n locale?: string;\n /** Called with what could not be drawn (a cycle, a missing child once the answer is complete). */\n onError?: (error: string) => void;\n /** Extra classes on the frame. */\n className?: string;\n};\n\n/** One surface, drawn from its root. */\nexport function GenUiSurface({ surface, catalog, streaming = false, labels, locale = \"en\", onError, className }: GenUiSurfaceProps) {\n const words = { ...genUiLabels, ...labels };\n const problems: string[] = [];\n const seen = new Set<string>();\n\n const draw = (id: unknown, depth: number): React.ReactNode => {\n if (typeof id !== \"string\") return null;\n if (depth > GENUI_LIMITS.depth) {\n problems.push(`\"${id}\" is deeper than ${GENUI_LIMITS.depth}`);\n return null;\n }\n if (seen.has(id)) {\n problems.push(`\"${id}\" is drawn twice (a cycle or two parents)`);\n return null;\n }\n const component = surface.components.get(id);\n if (!component) {\n if (!streaming) problems.push(`\"${id}\" never arrived`);\n return streaming ? <Skeleton key={id} className=\"h-10 w-full rounded-lg\" /> : null;\n }\n const entry = Object.hasOwn(catalog, component.component) ? catalog[component.component] : undefined;\n if (!entry) return null;\n seen.add(id);\n const context: GenUiRenderContext = {\n child: (c) => draw(c, depth + 1),\n children: (ids) => (Array.isArray(ids) ? ids.map((c) => <React.Fragment key={String(c)}>{draw(c, depth + 1)}</React.Fragment>) : []),\n resolve: (value) => (isBinding(value) ? readPath(surface.data, value.path) : value),\n labels: words,\n locale,\n };\n return <React.Fragment key={id}>{entry.render(component, context)}</React.Fragment>;\n };\n\n const root = draw(\"root\", 0);\n React.useEffect(() => {\n if (onError) for (const problem of problems) onError(problem);\n // Reported once per drawn state.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [surface, streaming]);\n\n return (\n <section aria-label={words.surface} aria-busy={streaming || undefined} data-genui-surface={surface.id} className={cn(\"flex flex-col gap-4\", className)}>\n {root ?? (streaming ? <Skeleton className=\"h-24 w-full rounded-xl\" /> : null)}\n </section>\n );\n}\n\nexport type GenUiStreamProps = Omit<GenUiSurfaceProps, \"surface\"> & {\n /** A model's answer, complete or still arriving: its prose and its `a2ui` fence. */\n text: string;\n /** Draws the prose (markdown in a chat); default: paragraphs of text. */\n renderProse?: (prose: string) => React.ReactNode;\n /** What to show when the answer holds no valid surface: the answer itself, as text (the fallback). */\n fallback?: (text: string) => React.ReactNode;\n};\n\n/**\n * A model's answer with its generated view: the prose, then every surface of its `a2ui` fence, growing as the answer\n * streams. An answer without a valid surface falls back to its text.\n */\nexport function GenUiStream({ text, catalog, streaming, renderProse, fallback, onError, ...rest }: GenUiStreamProps) {\n const state = React.useMemo(() => readStream(text, catalog), [text, catalog]);\n const reported = React.useRef(0);\n React.useEffect(() => {\n if (!onError) return;\n for (const error of state.errors.slice(reported.current)) onError(error);\n reported.current = state.errors.length;\n }, [state, onError]);\n const prose = (value: string) => (renderProse ? renderProse(value) : value.split(/\\n{2,}/).map((p, i) => <p key={i}>{p}</p>));\n const surfaces = state.order.map((id) => state.surfaces.get(id)).filter((s): s is GenUiSurfaceState => !!s && s.components.has(\"root\"));\n if (!state.found || (!streaming && surfaces.length === 0)) return <>{fallback ? fallback(text) : prose(text)}</>;\n return (\n <div className=\"flex flex-col gap-4\" data-genui-stream=\"\">\n {state.prose && prose(state.prose)}\n {surfaces.map((surface) => (\n <GenUiSurface key={surface.id} surface={surface} catalog={catalog} streaming={streaming} onError={onError} {...rest} />\n ))}\n {streaming && surfaces.length === 0 && <Skeleton className=\"h-24 w-full rounded-xl\" />}\n </div>\n );\n}\n\n/** The state a list of messages builds — for a surface the app holds itself (not from a model's text). */\nexport type { GenUiState };\n"
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
"path": "meta.ts",
|
|
251
|
+
"content": "// The documentation of a component, carried by its code: each primitive has a `meta.ts` beside its source\n// (`src/<name>/meta.ts`; a component that exists only in React Native: `src/native/meta/<name>.ts`). Never built into\n// the package: `scripts/build-registry.mjs` reads them into `registry/<name>.json`, which ui.krizaka.com (apps/site) renders as\n// ui.krizaka.com/components/<name>, and `scripts/registry.test.mjs` fails when one is missing or incomplete.\n//\n// Why a typed module and not JSDoc tags: the fields are lists and links (`related`, `whenNotToUse[].use`, examples),\n// which a type checks and a comment does not; the file is plain data (erasable TypeScript), so Node reads it without a\n// compiler, and an editor completes it.\n\n/** Every documented component: the web primitives (one entry of the package each) and the native-only ones. */\nexport type ComponentName =\n | \"achievement\"\n | \"action-bar\"\n | \"action-sheet\"\n | \"activity-feed\"\n | \"alert\"\n | \"app-shell\"\n | \"argument-card\"\n | \"audio-player\"\n | \"auth-layout\"\n | \"avatar\"\n | \"badge\"\n | \"button\"\n | \"card\"\n | \"card-grid\"\n | \"catalogue-page\"\n | \"changelog-page\"\n | \"chat\"\n | \"checkbox\"\n | \"chip\"\n | \"chip-link\"\n | \"choice-grid\"\n | \"cn\"\n | \"code-block\"\n | \"combobox\"\n | \"comparison-table\"\n | \"command\"\n | \"component-page\"\n | \"confirm-button\"\n | \"contact-page\"\n | \"continue-prompt\"\n | \"countdown\"\n | \"cta-panel\"\n | \"dashboard-layout\"\n | \"data-table\"\n | \"date-picker\"\n | \"decision-list\"\n | \"description-list\"\n | \"detail-layout\"\n | \"dialog\"\n | \"docs-article-page\"\n | \"docs-hub-page\"\n | \"docs-layout\"\n | \"document-viewer\"\n | \"dropdown-menu\"\n | \"empty-state\"\n | \"error-page\"\n | \"fab\"\n | \"field\"\n | \"file-upload\"\n | \"filter-bar\"\n | \"force-update-screen\"\n | \"form\"\n | \"gallery-page\"\n | \"genui\"\n | \"glow-card\"\n | \"goal-gauge\"\n | \"header\"\n | \"how-it-works-page\"\n | \"image-cropper\"\n | \"image-viewer\"\n | \"iso\"\n | \"kanban\"\n | \"kbd\"\n | \"landing-page\"\n | \"landscape\"\n | \"legal-page\"\n | \"link-card\"\n | \"list\"\n | \"loader\"\n | \"longform-page\"\n | \"maintenance-screen\"\n | \"map\"\n | \"markdown\"\n | \"meta-row\"\n | \"marketing-layout\"\n | \"marquee\"\n | \"mascot-quote\"\n | \"mascot-state\"\n | \"mascots\"\n | \"match-dialog\"\n | \"media-capture\"\n | \"media-grid\"\n | \"media-picker\"\n | \"meter\"\n | \"nav-bar\"\n | \"number-ticker\"\n | \"onboarding\"\n | \"otp-input\"\n | \"page\"\n | \"page-header\"\n | \"page-hero\"\n | \"pagination\"\n | \"permission-prompt\"\n | \"photo-grid\"\n | \"pointer-light\"\n | \"popover\"\n | \"pricing-page\"\n | \"product-page\"\n | \"profile-card\"\n | \"progress\"\n | \"promise-card\"\n | \"pull-to-refresh\"\n | \"radio-group\"\n | \"recipes\"\n | \"screen\"\n | \"scroll-stage\"\n | \"search-field\"\n | \"section-backdrop\"\n | \"segmented\"\n | \"separator\"\n | \"settings-layout\"\n | \"showcase\"\n | \"step-showcase\"\n | \"site-footer\"\n | \"sites\"\n | \"skeleton\"\n | \"skip-link\"\n | \"slider\"\n | \"slot\"\n | \"spinner\"\n | \"splash-handoff\"\n | \"split-view\"\n | \"stage-banner\"\n | \"stat\"\n | \"status-page\"\n | \"step-progress\"\n | \"stepper\"\n | \"swipe-deck\"\n | \"swipeable-row\"\n | \"switch\"\n | \"tab-bar\"\n | \"tabs\"\n | \"tag-input\"\n | \"tag-selector\"\n | \"terminal-replay\"\n | \"theme\"\n | \"toast\"\n | \"toc\"\n | \"tooltip\"\n | \"tree-view\"\n | \"txt\"\n | \"update-prompt\"\n | \"verified-badge\"\n | \"vertical-video\"\n | \"video-player\"\n | \"video-trimmer\"\n | \"virtual\";\n\n/** Where a component exists: `@krizaka/ui/<name>` (web), `@krizaka/ui/native` (React Native), or both. */\nexport type Platform = \"web\" | \"native\" | \"both\";\n\n/**\n * Why a component is not on a platform — a decision, never a silence (the parity matrix of ui.krizaka.com/platforms):\n * - `gap` (✗): it belongs there and is not built yet; `issue` is the issue that tracks it (`krizaka/krizaka-ui#275`);\n * - `n/a`: it is not meant for that platform (a site's page block on a phone, a swipe on a desktop); `use` names the\n * component that answers there, when one does.\n */\nexport type Absence =\n | { readonly status: \"gap\"; readonly issue: string; readonly reason: string; readonly use?: undefined }\n | { readonly status: \"n/a\"; readonly reason: string; readonly use?: ComponentName; readonly issue?: undefined };\n\n/**\n * Desktop is the Orazaka Mac app (Tauri 2: the system WebView renders the web components — ADR-002). `web`: the web\n * component runs there as it is; `desktop`: it also has a desktop form (a window's title bar, a denser layout…), said in\n * its documentation; otherwise the absence and why.\n */\nexport type DesktopSupport = \"web\" | \"desktop\" | Absence;\n\n/**\n * The three platforms of Krizaka UI — web · native (React Native, Expo) · desktop — for one component. The side a\n * component has is `platforms`; the side it lacks says why here: `native` is required for (and only for) a web-only\n * component, `web` for a native-only one; `desktop` always. `scripts/registry.test.mjs` fails on a missing decision.\n */\nexport interface Parity {\n readonly web?: Absence;\n readonly native?: Absence;\n readonly desktop: DesktopSupport;\n}\n\n/**\n * `stable`: its API only changes in a major. `beta`: it may still change in a minor. `deprecated`: no product uses it and\n * no work plans to — it still works, says what replaces it, and goes in the major named by `deprecated.removedIn`.\n */\nexport type Status = \"stable\" | \"beta\" | \"deprecated\";\n\n/** What a deprecated component says (required when `status` is `deprecated`, absent otherwise). */\nexport interface Deprecation {\n /** The version that deprecated it (`3.6.0`). */\n readonly since: string;\n /** The major that removes it (`4.0.0`). */\n readonly removedIn: string;\n /** What replaces it, when a component does. */\n readonly use?: ComponentName;\n /** Why it goes: no product uses it, no planned work needs it. */\n readonly reason: string;\n}\n\n/**\n * The group of the catalogue. `media`: video, sound, camera, files, cropping. `blocks`: the page blocks — the frame,\n * the hero, sections, cards that lead or argue, decisions, code, calls to act — that every page of a site is made of.\n * `layouts`: the shells of whole screens — an application, a dashboard, settings, sign-in, an object, a list and its\n * item, a site's page, a documentation page — each with its screens. `templates`: the page templates — whole pages of a\n * site (a landing, a product, a docs hub, a long read, a changelog, pricing, status, an error…), each a distinct\n * composition of the blocks, words and content as props.\n */\nexport type Category = \"actions\" | \"forms\" | \"navigation\" | \"overlays\" | \"feedback\" | \"data-display\" | \"media\" | \"blocks\" | \"layouts\" | \"templates\" | \"layout\" | \"foundations\";\n\n/** Why a reader needs a template: the seven intents the templates are grouped by (sidebar sub-headings, catalogue filter). */\nexport type TemplatePurpose = \"convince\" | \"explain\" | \"show\" | \"act\" | \"operate\" | \"converse\" | \"recover\";\n\n/** The groups of templates, in reading order: the title, and the one line that says when to come to that group. */\nexport const templatePurposes = {\n convince: { title: \"Convince\", useWhen: \"You come here when a visitor must decide: a landing, a product, a price, a comparison, a proof.\" },\n explain: { title: \"Explain\", useWhen: \"You come here when a reader must understand: how it works, a documentation hub or article, a changelog, a FAQ, a policy.\" },\n show: { title: \"Show\", useWhen: \"You come here when the work itself is the argument: a gallery, a story, a case study, an object and its details.\" },\n act: { title: \"Act\", useWhen: \"You come here when a person must do something once: reach the team, sign in or up, set up, pay.\" },\n operate: { title: \"Operate\", useWhen: \"You come here when a person works every day: a dashboard, a console with a table, settings, an app shell.\" },\n converse: { title: \"Converse\", useWhen: \"You come here when people talk: an inbox, a chat, comments, a notifications centre.\" },\n recover: { title: \"Recover\", useWhen: \"You come here when the way is blocked: an empty list, an error, a missing page, maintenance, a product still being built.\" },\n} as const satisfies Record<TemplatePurpose, { readonly title: string; readonly useWhen: string }>;\n\n/**\n * A named example: the file `registry/examples/<component>/<name>.tsx` (web) or\n * `registry/examples/<component>/native/<name>.tsx` (React Native) — at most 40 lines, imported by the stories and\n * shown (live, with its code) by the documentation.\n */\ninterface ExampleBase {\n /** The file name, kebab-case (`primary`, `icon-button`). */\n readonly name: string;\n /** A short title (`Primary`, `Icon button`). */\n readonly title: string;\n /** One or two sentences: when this variant is the right one. */\n readonly description: string;\n}\n\n/** A plain example, or a template: a whole screen with its intent (`purpose`) and the user goal its layout serves (`why`). */\nexport type Example =\n | (ExampleBase & { readonly template?: false; readonly purpose?: undefined; readonly why?: undefined })\n | (ExampleBase & {\n /**\n * A template: a whole screen of a product composed from the platform (a layout and its blocks, real words, its\n * phone version thought out), copied as a starting point — at most 160 lines, listed on ui.krizaka.com/templates.\n */\n readonly template: true;\n /** The intent that groups it: why you need this block. */\n readonly purpose: TemplatePurpose;\n /** Why this layout: the user goal it serves, in one sentence. */\n readonly why: string;\n });\n\n/** One platform's side of a component. */\nexport interface PlatformDocs {\n /** The names to import (`[\"Button\", \"IconButton\"]`): from `@krizaka/ui/<name>` on the web, `@krizaka/ui/native` in React Native. */\n readonly imports: readonly string[];\n /** The examples, in reading order; the first one is the component's preview. */\n readonly examples: readonly Example[];\n}\n\n/** The React Native side: the same concept, and what differs from the web API. */\nexport interface NativeDocs extends PlatformDocs {\n /**\n * The entry to import from when it is not `@krizaka/ui/native`: a component that needs an Expo module (an optional\n * peer) has its own, `@krizaka/ui/native/<name>`, so an app that does not use it never installs the module.\n */\n readonly entry?: string;\n /** What differs from the web API (`label` instead of children…); empty for a native-only component. */\n readonly differences: readonly string[];\n}\n\nexport interface KeyboardInteraction {\n /** The keys, as written on a keyboard: `Space`, `Enter`, `Arrow keys`, `Esc`, `Tab`. */\n readonly keys: string;\n /** What they do. */\n readonly action: string;\n}\n\nexport interface ComponentMeta {\n /** The display name: `Button`, `Dropdown menu`. */\n readonly title: string;\n /** One sentence: what it is. */\n readonly summary: string;\n readonly status: Status;\n /** Required for, and only for, a `deprecated` component. */\n readonly deprecated?: Deprecation;\n readonly category: Category;\n /** Must match the sides below: `both` ⇔ `web` and `native`. */\n readonly platforms: Platform;\n /** The platforms it is not on, and why; and its desktop form (see `Parity`). */\n readonly parity: Parity;\n /** When to reach for it: concrete situations, one per item. */\n readonly whenToUse: readonly string[];\n /** When not to — and what to use instead (`use`), when another component is the answer. */\n readonly whenNotToUse: readonly { readonly when: string; readonly use?: ComponentName }[];\n /** How to use it well: wording, placement, composition. */\n readonly bestPractices: readonly string[];\n readonly accessibility: {\n /** The keyboard, when it does more than Tab and Enter. */\n readonly keyboard: readonly KeyboardInteraction[];\n /** Roles, names and announcements: what the component does, what the app must provide. */\n readonly notes: readonly string[];\n };\n /** The components a reader of this page should know about. */\n readonly related: readonly ComponentName[];\n readonly web?: PlatformDocs;\n readonly native?: NativeDocs;\n}\n"
|
|
252
|
+
}
|
|
253
|
+
],
|
|
254
|
+
"dependencies": [
|
|
255
|
+
"@krizaka/intl@^0.2.1"
|
|
256
|
+
],
|
|
257
|
+
"registryDependencies": [
|
|
258
|
+
"alert",
|
|
259
|
+
"card",
|
|
260
|
+
"cn",
|
|
261
|
+
"data-table",
|
|
262
|
+
"skeleton"
|
|
263
|
+
],
|
|
264
|
+
"demo": {
|
|
265
|
+
"path": "examples/genui/table.tsx",
|
|
266
|
+
"content": "import { baseGenUiCatalog, GenUiStream } from \"@krizaka/ui/genui\";\n\nconst ANSWER = [\n \"Three cities sold more than 100 passes this month.\",\n \"```a2ui\",\n '{\"version\":\"v0.9\",\"createSurface\":{\"surfaceId\":\"s1\",\"catalogId\":\"krizaka:genui/v1\"}}',\n '{\"version\":\"v0.9\",\"updateComponents\":{\"surfaceId\":\"s1\",\"components\":[{\"id\":\"root\",\"component\":\"Column\",\"children\":[\"note\",\"grid\"]},{\"id\":\"note\",\"component\":\"Callout\",\"tone\":\"success\",\"title\":\"Montréal leads\",\"text\":\"412 passes, +18 % on last month.\"},{\"id\":\"grid\",\"component\":\"Table\",\"caption\":\"Passes by city\",\"columns\":[{\"key\":\"city\",\"label\":\"City\"},{\"key\":\"passes\",\"label\":\"Passes\",\"format\":\"integer\"}],\"rows\":{\"path\":\"/cities\"}}]}}',\n '{\"version\":\"v0.9\",\"updateDataModel\":{\"surfaceId\":\"s1\",\"path\":\"/cities\",\"value\":[{\"city\":\"Montréal\",\"passes\":412},{\"city\":\"Québec\",\"passes\":187},{\"city\":\"Laval\",\"passes\":121}]}}',\n \"```\",\n].join(\"\\n\");\n\nexport default function GenUiTable() {\n return <GenUiStream text={ANSWER} catalog={baseGenUiCatalog} />;\n}\n"
|
|
267
|
+
},
|
|
268
|
+
"props": [
|
|
269
|
+
{
|
|
270
|
+
"component": "GENUI_CATALOG_ID",
|
|
271
|
+
"description": "The catalog a surface must name: the Krizaka components, version 1.",
|
|
272
|
+
"props": []
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
"component": "GENUI_FORMATS",
|
|
276
|
+
"description": "The value formats a model may name.",
|
|
277
|
+
"props": []
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
"component": "GENUI_VERSION",
|
|
281
|
+
"description": "The protocol version every message carries.",
|
|
282
|
+
"props": []
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
"component": "GenUiStream",
|
|
286
|
+
"description": "A model's answer with its generated view: the prose, then every surface of its `a2ui` fence, growing as the answer\nstreams. An answer without a valid surface falls back to its text.",
|
|
287
|
+
"props": [
|
|
288
|
+
{
|
|
289
|
+
"name": "catalog",
|
|
290
|
+
"type": "Readonly<Record<string, GenUiEntry>>",
|
|
291
|
+
"default": null,
|
|
292
|
+
"description": "The components it may use.",
|
|
293
|
+
"required": true
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
"name": "text",
|
|
297
|
+
"type": "string",
|
|
298
|
+
"default": null,
|
|
299
|
+
"description": "A model's answer, complete or still arriving: its prose and its `a2ui` fence.",
|
|
300
|
+
"required": true
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
"name": "className",
|
|
304
|
+
"type": "string",
|
|
305
|
+
"default": null,
|
|
306
|
+
"description": "Extra classes on the frame.",
|
|
307
|
+
"required": false
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
"name": "fallback",
|
|
311
|
+
"type": "((text: string) => ReactNode)",
|
|
312
|
+
"default": null,
|
|
313
|
+
"description": "What to show when the answer holds no valid surface: the answer itself, as text (the fallback).",
|
|
314
|
+
"required": false
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
"name": "labels",
|
|
318
|
+
"type": "Partial<GenUiLabels>",
|
|
319
|
+
"default": null,
|
|
320
|
+
"description": "The surface's words.",
|
|
321
|
+
"required": false
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
"name": "locale",
|
|
325
|
+
"type": "string",
|
|
326
|
+
"default": null,
|
|
327
|
+
"description": "The locale numbers are written in. Default `en`.",
|
|
328
|
+
"required": false
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
"name": "onError",
|
|
332
|
+
"type": "((error: string) => void)",
|
|
333
|
+
"default": null,
|
|
334
|
+
"description": "Called with what could not be drawn (a cycle, a missing child once the answer is complete).",
|
|
335
|
+
"required": false
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
"name": "renderProse",
|
|
339
|
+
"type": "((prose: string) => ReactNode)",
|
|
340
|
+
"default": null,
|
|
341
|
+
"description": "Draws the prose (markdown in a chat); default: paragraphs of text.",
|
|
342
|
+
"required": false
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
"name": "streaming",
|
|
346
|
+
"type": "boolean",
|
|
347
|
+
"default": null,
|
|
348
|
+
"description": "The answer is still arriving: missing children are skeletons and the surface is busy.",
|
|
349
|
+
"required": false
|
|
350
|
+
}
|
|
351
|
+
]
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
"component": "GenUiSurface",
|
|
355
|
+
"description": "One surface, drawn from its root.",
|
|
356
|
+
"props": [
|
|
357
|
+
{
|
|
358
|
+
"name": "catalog",
|
|
359
|
+
"type": "Readonly<Record<string, GenUiEntry>>",
|
|
360
|
+
"default": null,
|
|
361
|
+
"description": "The components it may use.",
|
|
362
|
+
"required": true
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
"name": "surface",
|
|
366
|
+
"type": "GenUiSurfaceState",
|
|
367
|
+
"default": null,
|
|
368
|
+
"description": "The surface to draw (from `readStream` or `applyMessage`).",
|
|
369
|
+
"required": true
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
"name": "className",
|
|
373
|
+
"type": "string",
|
|
374
|
+
"default": null,
|
|
375
|
+
"description": "Extra classes on the frame.",
|
|
376
|
+
"required": false
|
|
377
|
+
},
|
|
378
|
+
{
|
|
379
|
+
"name": "labels",
|
|
380
|
+
"type": "Partial<GenUiLabels>",
|
|
381
|
+
"default": null,
|
|
382
|
+
"description": "The surface's words.",
|
|
383
|
+
"required": false
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
"name": "locale",
|
|
387
|
+
"type": "string",
|
|
388
|
+
"default": "en",
|
|
389
|
+
"description": "The locale numbers are written in. Default `en`.",
|
|
390
|
+
"required": false
|
|
391
|
+
},
|
|
392
|
+
{
|
|
393
|
+
"name": "onError",
|
|
394
|
+
"type": "((error: string) => void)",
|
|
395
|
+
"default": null,
|
|
396
|
+
"description": "Called with what could not be drawn (a cycle, a missing child once the answer is complete).",
|
|
397
|
+
"required": false
|
|
398
|
+
},
|
|
399
|
+
{
|
|
400
|
+
"name": "streaming",
|
|
401
|
+
"type": "boolean",
|
|
402
|
+
"default": "false",
|
|
403
|
+
"description": "The answer is still arriving: missing children are skeletons and the surface is busy.",
|
|
404
|
+
"required": false
|
|
405
|
+
}
|
|
406
|
+
]
|
|
407
|
+
}
|
|
408
|
+
]
|
|
409
|
+
}
|
package/registry/glow-card.json
CHANGED
|
@@ -132,7 +132,7 @@
|
|
|
132
132
|
},
|
|
133
133
|
{
|
|
134
134
|
"path": "meta.ts",
|
|
135
|
-
"content": "// The documentation of a component, carried by its code: each primitive has a `meta.ts` beside its source\n// (`src/<name>/meta.ts`; a component that exists only in React Native: `src/native/meta/<name>.ts`). Never built into\n// the package: `scripts/build-registry.mjs` reads them into `registry/<name>.json`, which ui.krizaka.com (apps/site) renders as\n// ui.krizaka.com/components/<name>, and `scripts/registry.test.mjs` fails when one is missing or incomplete.\n//\n// Why a typed module and not JSDoc tags: the fields are lists and links (`related`, `whenNotToUse[].use`, examples),\n// which a type checks and a comment does not; the file is plain data (erasable TypeScript), so Node reads it without a\n// compiler, and an editor completes it.\n\n/** Every documented component: the web primitives (one entry of the package each) and the native-only ones. */\nexport type ComponentName =\n | \"achievement\"\n | \"action-bar\"\n | \"action-sheet\"\n | \"activity-feed\"\n | \"alert\"\n | \"app-shell\"\n | \"argument-card\"\n | \"audio-player\"\n | \"auth-layout\"\n | \"avatar\"\n | \"badge\"\n | \"button\"\n | \"card\"\n | \"card-grid\"\n | \"catalogue-page\"\n | \"changelog-page\"\n | \"chat\"\n | \"checkbox\"\n | \"chip\"\n | \"chip-link\"\n | \"choice-grid\"\n | \"cn\"\n | \"code-block\"\n | \"combobox\"\n | \"command\"\n | \"component-page\"\n | \"confirm-button\"\n | \"contact-page\"\n | \"continue-prompt\"\n | \"countdown\"\n | \"cta-panel\"\n | \"dashboard-layout\"\n | \"data-table\"\n | \"date-picker\"\n | \"decision-list\"\n | \"description-list\"\n | \"detail-layout\"\n | \"dialog\"\n | \"docs-article-page\"\n | \"docs-hub-page\"\n | \"docs-layout\"\n | \"document-viewer\"\n | \"dropdown-menu\"\n | \"empty-state\"\n | \"error-page\"\n | \"fab\"\n | \"field\"\n | \"file-upload\"\n | \"filter-bar\"\n | \"force-update-screen\"\n | \"form\"\n | \"gallery-page\"\n | \"glow-card\"\n | \"goal-gauge\"\n | \"header\"\n | \"how-it-works-page\"\n | \"image-cropper\"\n | \"image-viewer\"\n | \"iso\"\n | \"kanban\"\n | \"kbd\"\n | \"landing-page\"\n | \"landscape\"\n | \"legal-page\"\n | \"link-card\"\n | \"list\"\n | \"loader\"\n | \"longform-page\"\n | \"maintenance-screen\"\n | \"map\"\n | \"markdown\"\n | \"marketing-layout\"\n | \"marquee\"\n | \"mascot-quote\"\n | \"mascot-state\"\n | \"mascots\"\n | \"match-dialog\"\n | \"media-capture\"\n | \"media-grid\"\n | \"media-picker\"\n | \"meter\"\n | \"nav-bar\"\n | \"number-ticker\"\n | \"onboarding\"\n | \"otp-input\"\n | \"page\"\n | \"page-header\"\n | \"page-hero\"\n | \"pagination\"\n | \"permission-prompt\"\n | \"photo-grid\"\n | \"pointer-light\"\n | \"popover\"\n | \"pricing-page\"\n | \"product-page\"\n | \"profile-card\"\n | \"progress\"\n | \"promise-card\"\n | \"pull-to-refresh\"\n | \"radio-group\"\n | \"recipes\"\n | \"screen\"\n | \"scroll-stage\"\n | \"search-field\"\n | \"section-backdrop\"\n | \"segmented\"\n | \"separator\"\n | \"settings-layout\"\n | \"showcase\"\n | \"site-footer\"\n | \"sites\"\n | \"skeleton\"\n | \"skip-link\"\n | \"slider\"\n | \"slot\"\n | \"spinner\"\n | \"splash-handoff\"\n | \"split-view\"\n | \"stage-banner\"\n | \"stat\"\n | \"status-page\"\n | \"step-progress\"\n | \"stepper\"\n | \"swipe-deck\"\n | \"swipeable-row\"\n | \"switch\"\n | \"tab-bar\"\n | \"tabs\"\n | \"tag-input\"\n | \"tag-selector\"\n | \"terminal-replay\"\n | \"theme\"\n | \"toast\"\n | \"toc\"\n | \"tooltip\"\n | \"tree-view\"\n | \"txt\"\n | \"update-prompt\"\n | \"verified-badge\"\n | \"vertical-video\"\n | \"video-player\"\n | \"video-trimmer\"\n | \"virtual\";\n\n/** Where a component exists: `@krizaka/ui/<name>` (web), `@krizaka/ui/native` (React Native), or both. */\nexport type Platform = \"web\" | \"native\" | \"both\";\n\n/**\n * Why a component is not on a platform — a decision, never a silence (the parity matrix of ui.krizaka.com/platforms):\n * - `gap` (✗): it belongs there and is not built yet; `issue` is the issue that tracks it (`krizaka/krizaka-ui#275`);\n * - `n/a`: it is not meant for that platform (a site's page block on a phone, a swipe on a desktop); `use` names the\n * component that answers there, when one does.\n */\nexport type Absence =\n | { readonly status: \"gap\"; readonly issue: string; readonly reason: string; readonly use?: undefined }\n | { readonly status: \"n/a\"; readonly reason: string; readonly use?: ComponentName; readonly issue?: undefined };\n\n/**\n * Desktop is the Orazaka Mac app (Tauri 2: the system WebView renders the web components — ADR-002). `web`: the web\n * component runs there as it is; `desktop`: it also has a desktop form (a window's title bar, a denser layout…), said in\n * its documentation; otherwise the absence and why.\n */\nexport type DesktopSupport = \"web\" | \"desktop\" | Absence;\n\n/**\n * The three platforms of Krizaka UI — web · native (React Native, Expo) · desktop — for one component. The side a\n * component has is `platforms`; the side it lacks says why here: `native` is required for (and only for) a web-only\n * component, `web` for a native-only one; `desktop` always. `scripts/registry.test.mjs` fails on a missing decision.\n */\nexport interface Parity {\n readonly web?: Absence;\n readonly native?: Absence;\n readonly desktop: DesktopSupport;\n}\n\n/**\n * `stable`: its API only changes in a major. `beta`: it may still change in a minor. `deprecated`: no product uses it and\n * no work plans to — it still works, says what replaces it, and goes in the major named by `deprecated.removedIn`.\n */\nexport type Status = \"stable\" | \"beta\" | \"deprecated\";\n\n/** What a deprecated component says (required when `status` is `deprecated`, absent otherwise). */\nexport interface Deprecation {\n /** The version that deprecated it (`3.6.0`). */\n readonly since: string;\n /** The major that removes it (`4.0.0`). */\n readonly removedIn: string;\n /** What replaces it, when a component does. */\n readonly use?: ComponentName;\n /** Why it goes: no product uses it, no planned work needs it. */\n readonly reason: string;\n}\n\n/**\n * The group of the catalogue. `media`: video, sound, camera, files, cropping. `blocks`: the page blocks — the frame,\n * the hero, sections, cards that lead or argue, decisions, code, calls to act — that every page of a site is made of.\n * `layouts`: the shells of whole screens — an application, a dashboard, settings, sign-in, an object, a list and its\n * item, a site's page, a documentation page — each with its screens. `templates`: the page templates — whole pages of a\n * site (a landing, a product, a docs hub, a long read, a changelog, pricing, status, an error…), each a distinct\n * composition of the blocks, words and content as props.\n */\nexport type Category = \"actions\" | \"forms\" | \"navigation\" | \"overlays\" | \"feedback\" | \"data-display\" | \"media\" | \"blocks\" | \"layouts\" | \"templates\" | \"layout\" | \"foundations\";\n\n/** Why a reader needs a template: the seven intents the templates are grouped by (sidebar sub-headings, catalogue filter). */\nexport type TemplatePurpose = \"convince\" | \"explain\" | \"show\" | \"act\" | \"operate\" | \"converse\" | \"recover\";\n\n/** The groups of templates, in reading order: the title, and the one line that says when to come to that group. */\nexport const templatePurposes = {\n convince: { title: \"Convince\", useWhen: \"You come here when a visitor must decide: a landing, a product, a price, a comparison, a proof.\" },\n explain: { title: \"Explain\", useWhen: \"You come here when a reader must understand: how it works, a documentation hub or article, a changelog, a FAQ, a policy.\" },\n show: { title: \"Show\", useWhen: \"You come here when the work itself is the argument: a gallery, a story, a case study, an object and its details.\" },\n act: { title: \"Act\", useWhen: \"You come here when a person must do something once: reach the team, sign in or up, set up, pay.\" },\n operate: { title: \"Operate\", useWhen: \"You come here when a person works every day: a dashboard, a console with a table, settings, an app shell.\" },\n converse: { title: \"Converse\", useWhen: \"You come here when people talk: an inbox, a chat, comments, a notifications centre.\" },\n recover: { title: \"Recover\", useWhen: \"You come here when the way is blocked: an empty list, an error, a missing page, maintenance, a product still being built.\" },\n} as const satisfies Record<TemplatePurpose, { readonly title: string; readonly useWhen: string }>;\n\n/**\n * A named example: the file `registry/examples/<component>/<name>.tsx` (web) or\n * `registry/examples/<component>/native/<name>.tsx` (React Native) — at most 40 lines, imported by the stories and\n * shown (live, with its code) by the documentation.\n */\ninterface ExampleBase {\n /** The file name, kebab-case (`primary`, `icon-button`). */\n readonly name: string;\n /** A short title (`Primary`, `Icon button`). */\n readonly title: string;\n /** One or two sentences: when this variant is the right one. */\n readonly description: string;\n}\n\n/** A plain example, or a template: a whole screen with its intent (`purpose`) and the user goal its layout serves (`why`). */\nexport type Example =\n | (ExampleBase & { readonly template?: false; readonly purpose?: undefined; readonly why?: undefined })\n | (ExampleBase & {\n /**\n * A template: a whole screen of a product composed from the platform (a layout and its blocks, real words, its\n * phone version thought out), copied as a starting point — at most 160 lines, listed on ui.krizaka.com/templates.\n */\n readonly template: true;\n /** The intent that groups it: why you need this block. */\n readonly purpose: TemplatePurpose;\n /** Why this layout: the user goal it serves, in one sentence. */\n readonly why: string;\n });\n\n/** One platform's side of a component. */\nexport interface PlatformDocs {\n /** The names to import (`[\"Button\", \"IconButton\"]`): from `@krizaka/ui/<name>` on the web, `@krizaka/ui/native` in React Native. */\n readonly imports: readonly string[];\n /** The examples, in reading order; the first one is the component's preview. */\n readonly examples: readonly Example[];\n}\n\n/** The React Native side: the same concept, and what differs from the web API. */\nexport interface NativeDocs extends PlatformDocs {\n /**\n * The entry to import from when it is not `@krizaka/ui/native`: a component that needs an Expo module (an optional\n * peer) has its own, `@krizaka/ui/native/<name>`, so an app that does not use it never installs the module.\n */\n readonly entry?: string;\n /** What differs from the web API (`label` instead of children…); empty for a native-only component. */\n readonly differences: readonly string[];\n}\n\nexport interface KeyboardInteraction {\n /** The keys, as written on a keyboard: `Space`, `Enter`, `Arrow keys`, `Esc`, `Tab`. */\n readonly keys: string;\n /** What they do. */\n readonly action: string;\n}\n\nexport interface ComponentMeta {\n /** The display name: `Button`, `Dropdown menu`. */\n readonly title: string;\n /** One sentence: what it is. */\n readonly summary: string;\n readonly status: Status;\n /** Required for, and only for, a `deprecated` component. */\n readonly deprecated?: Deprecation;\n readonly category: Category;\n /** Must match the sides below: `both` ⇔ `web` and `native`. */\n readonly platforms: Platform;\n /** The platforms it is not on, and why; and its desktop form (see `Parity`). */\n readonly parity: Parity;\n /** When to reach for it: concrete situations, one per item. */\n readonly whenToUse: readonly string[];\n /** When not to — and what to use instead (`use`), when another component is the answer. */\n readonly whenNotToUse: readonly { readonly when: string; readonly use?: ComponentName }[];\n /** How to use it well: wording, placement, composition. */\n readonly bestPractices: readonly string[];\n readonly accessibility: {\n /** The keyboard, when it does more than Tab and Enter. */\n readonly keyboard: readonly KeyboardInteraction[];\n /** Roles, names and announcements: what the component does, what the app must provide. */\n readonly notes: readonly string[];\n };\n /** The components a reader of this page should know about. */\n readonly related: readonly ComponentName[];\n readonly web?: PlatformDocs;\n readonly native?: NativeDocs;\n}\n"
|
|
135
|
+
"content": "// The documentation of a component, carried by its code: each primitive has a `meta.ts` beside its source\n// (`src/<name>/meta.ts`; a component that exists only in React Native: `src/native/meta/<name>.ts`). Never built into\n// the package: `scripts/build-registry.mjs` reads them into `registry/<name>.json`, which ui.krizaka.com (apps/site) renders as\n// ui.krizaka.com/components/<name>, and `scripts/registry.test.mjs` fails when one is missing or incomplete.\n//\n// Why a typed module and not JSDoc tags: the fields are lists and links (`related`, `whenNotToUse[].use`, examples),\n// which a type checks and a comment does not; the file is plain data (erasable TypeScript), so Node reads it without a\n// compiler, and an editor completes it.\n\n/** Every documented component: the web primitives (one entry of the package each) and the native-only ones. */\nexport type ComponentName =\n | \"achievement\"\n | \"action-bar\"\n | \"action-sheet\"\n | \"activity-feed\"\n | \"alert\"\n | \"app-shell\"\n | \"argument-card\"\n | \"audio-player\"\n | \"auth-layout\"\n | \"avatar\"\n | \"badge\"\n | \"button\"\n | \"card\"\n | \"card-grid\"\n | \"catalogue-page\"\n | \"changelog-page\"\n | \"chat\"\n | \"checkbox\"\n | \"chip\"\n | \"chip-link\"\n | \"choice-grid\"\n | \"cn\"\n | \"code-block\"\n | \"combobox\"\n | \"comparison-table\"\n | \"command\"\n | \"component-page\"\n | \"confirm-button\"\n | \"contact-page\"\n | \"continue-prompt\"\n | \"countdown\"\n | \"cta-panel\"\n | \"dashboard-layout\"\n | \"data-table\"\n | \"date-picker\"\n | \"decision-list\"\n | \"description-list\"\n | \"detail-layout\"\n | \"dialog\"\n | \"docs-article-page\"\n | \"docs-hub-page\"\n | \"docs-layout\"\n | \"document-viewer\"\n | \"dropdown-menu\"\n | \"empty-state\"\n | \"error-page\"\n | \"fab\"\n | \"field\"\n | \"file-upload\"\n | \"filter-bar\"\n | \"force-update-screen\"\n | \"form\"\n | \"gallery-page\"\n | \"genui\"\n | \"glow-card\"\n | \"goal-gauge\"\n | \"header\"\n | \"how-it-works-page\"\n | \"image-cropper\"\n | \"image-viewer\"\n | \"iso\"\n | \"kanban\"\n | \"kbd\"\n | \"landing-page\"\n | \"landscape\"\n | \"legal-page\"\n | \"link-card\"\n | \"list\"\n | \"loader\"\n | \"longform-page\"\n | \"maintenance-screen\"\n | \"map\"\n | \"markdown\"\n | \"meta-row\"\n | \"marketing-layout\"\n | \"marquee\"\n | \"mascot-quote\"\n | \"mascot-state\"\n | \"mascots\"\n | \"match-dialog\"\n | \"media-capture\"\n | \"media-grid\"\n | \"media-picker\"\n | \"meter\"\n | \"nav-bar\"\n | \"number-ticker\"\n | \"onboarding\"\n | \"otp-input\"\n | \"page\"\n | \"page-header\"\n | \"page-hero\"\n | \"pagination\"\n | \"permission-prompt\"\n | \"photo-grid\"\n | \"pointer-light\"\n | \"popover\"\n | \"pricing-page\"\n | \"product-page\"\n | \"profile-card\"\n | \"progress\"\n | \"promise-card\"\n | \"pull-to-refresh\"\n | \"radio-group\"\n | \"recipes\"\n | \"screen\"\n | \"scroll-stage\"\n | \"search-field\"\n | \"section-backdrop\"\n | \"segmented\"\n | \"separator\"\n | \"settings-layout\"\n | \"showcase\"\n | \"step-showcase\"\n | \"site-footer\"\n | \"sites\"\n | \"skeleton\"\n | \"skip-link\"\n | \"slider\"\n | \"slot\"\n | \"spinner\"\n | \"splash-handoff\"\n | \"split-view\"\n | \"stage-banner\"\n | \"stat\"\n | \"status-page\"\n | \"step-progress\"\n | \"stepper\"\n | \"swipe-deck\"\n | \"swipeable-row\"\n | \"switch\"\n | \"tab-bar\"\n | \"tabs\"\n | \"tag-input\"\n | \"tag-selector\"\n | \"terminal-replay\"\n | \"theme\"\n | \"toast\"\n | \"toc\"\n | \"tooltip\"\n | \"tree-view\"\n | \"txt\"\n | \"update-prompt\"\n | \"verified-badge\"\n | \"vertical-video\"\n | \"video-player\"\n | \"video-trimmer\"\n | \"virtual\";\n\n/** Where a component exists: `@krizaka/ui/<name>` (web), `@krizaka/ui/native` (React Native), or both. */\nexport type Platform = \"web\" | \"native\" | \"both\";\n\n/**\n * Why a component is not on a platform — a decision, never a silence (the parity matrix of ui.krizaka.com/platforms):\n * - `gap` (✗): it belongs there and is not built yet; `issue` is the issue that tracks it (`krizaka/krizaka-ui#275`);\n * - `n/a`: it is not meant for that platform (a site's page block on a phone, a swipe on a desktop); `use` names the\n * component that answers there, when one does.\n */\nexport type Absence =\n | { readonly status: \"gap\"; readonly issue: string; readonly reason: string; readonly use?: undefined }\n | { readonly status: \"n/a\"; readonly reason: string; readonly use?: ComponentName; readonly issue?: undefined };\n\n/**\n * Desktop is the Orazaka Mac app (Tauri 2: the system WebView renders the web components — ADR-002). `web`: the web\n * component runs there as it is; `desktop`: it also has a desktop form (a window's title bar, a denser layout…), said in\n * its documentation; otherwise the absence and why.\n */\nexport type DesktopSupport = \"web\" | \"desktop\" | Absence;\n\n/**\n * The three platforms of Krizaka UI — web · native (React Native, Expo) · desktop — for one component. The side a\n * component has is `platforms`; the side it lacks says why here: `native` is required for (and only for) a web-only\n * component, `web` for a native-only one; `desktop` always. `scripts/registry.test.mjs` fails on a missing decision.\n */\nexport interface Parity {\n readonly web?: Absence;\n readonly native?: Absence;\n readonly desktop: DesktopSupport;\n}\n\n/**\n * `stable`: its API only changes in a major. `beta`: it may still change in a minor. `deprecated`: no product uses it and\n * no work plans to — it still works, says what replaces it, and goes in the major named by `deprecated.removedIn`.\n */\nexport type Status = \"stable\" | \"beta\" | \"deprecated\";\n\n/** What a deprecated component says (required when `status` is `deprecated`, absent otherwise). */\nexport interface Deprecation {\n /** The version that deprecated it (`3.6.0`). */\n readonly since: string;\n /** The major that removes it (`4.0.0`). */\n readonly removedIn: string;\n /** What replaces it, when a component does. */\n readonly use?: ComponentName;\n /** Why it goes: no product uses it, no planned work needs it. */\n readonly reason: string;\n}\n\n/**\n * The group of the catalogue. `media`: video, sound, camera, files, cropping. `blocks`: the page blocks — the frame,\n * the hero, sections, cards that lead or argue, decisions, code, calls to act — that every page of a site is made of.\n * `layouts`: the shells of whole screens — an application, a dashboard, settings, sign-in, an object, a list and its\n * item, a site's page, a documentation page — each with its screens. `templates`: the page templates — whole pages of a\n * site (a landing, a product, a docs hub, a long read, a changelog, pricing, status, an error…), each a distinct\n * composition of the blocks, words and content as props.\n */\nexport type Category = \"actions\" | \"forms\" | \"navigation\" | \"overlays\" | \"feedback\" | \"data-display\" | \"media\" | \"blocks\" | \"layouts\" | \"templates\" | \"layout\" | \"foundations\";\n\n/** Why a reader needs a template: the seven intents the templates are grouped by (sidebar sub-headings, catalogue filter). */\nexport type TemplatePurpose = \"convince\" | \"explain\" | \"show\" | \"act\" | \"operate\" | \"converse\" | \"recover\";\n\n/** The groups of templates, in reading order: the title, and the one line that says when to come to that group. */\nexport const templatePurposes = {\n convince: { title: \"Convince\", useWhen: \"You come here when a visitor must decide: a landing, a product, a price, a comparison, a proof.\" },\n explain: { title: \"Explain\", useWhen: \"You come here when a reader must understand: how it works, a documentation hub or article, a changelog, a FAQ, a policy.\" },\n show: { title: \"Show\", useWhen: \"You come here when the work itself is the argument: a gallery, a story, a case study, an object and its details.\" },\n act: { title: \"Act\", useWhen: \"You come here when a person must do something once: reach the team, sign in or up, set up, pay.\" },\n operate: { title: \"Operate\", useWhen: \"You come here when a person works every day: a dashboard, a console with a table, settings, an app shell.\" },\n converse: { title: \"Converse\", useWhen: \"You come here when people talk: an inbox, a chat, comments, a notifications centre.\" },\n recover: { title: \"Recover\", useWhen: \"You come here when the way is blocked: an empty list, an error, a missing page, maintenance, a product still being built.\" },\n} as const satisfies Record<TemplatePurpose, { readonly title: string; readonly useWhen: string }>;\n\n/**\n * A named example: the file `registry/examples/<component>/<name>.tsx` (web) or\n * `registry/examples/<component>/native/<name>.tsx` (React Native) — at most 40 lines, imported by the stories and\n * shown (live, with its code) by the documentation.\n */\ninterface ExampleBase {\n /** The file name, kebab-case (`primary`, `icon-button`). */\n readonly name: string;\n /** A short title (`Primary`, `Icon button`). */\n readonly title: string;\n /** One or two sentences: when this variant is the right one. */\n readonly description: string;\n}\n\n/** A plain example, or a template: a whole screen with its intent (`purpose`) and the user goal its layout serves (`why`). */\nexport type Example =\n | (ExampleBase & { readonly template?: false; readonly purpose?: undefined; readonly why?: undefined })\n | (ExampleBase & {\n /**\n * A template: a whole screen of a product composed from the platform (a layout and its blocks, real words, its\n * phone version thought out), copied as a starting point — at most 160 lines, listed on ui.krizaka.com/templates.\n */\n readonly template: true;\n /** The intent that groups it: why you need this block. */\n readonly purpose: TemplatePurpose;\n /** Why this layout: the user goal it serves, in one sentence. */\n readonly why: string;\n });\n\n/** One platform's side of a component. */\nexport interface PlatformDocs {\n /** The names to import (`[\"Button\", \"IconButton\"]`): from `@krizaka/ui/<name>` on the web, `@krizaka/ui/native` in React Native. */\n readonly imports: readonly string[];\n /** The examples, in reading order; the first one is the component's preview. */\n readonly examples: readonly Example[];\n}\n\n/** The React Native side: the same concept, and what differs from the web API. */\nexport interface NativeDocs extends PlatformDocs {\n /**\n * The entry to import from when it is not `@krizaka/ui/native`: a component that needs an Expo module (an optional\n * peer) has its own, `@krizaka/ui/native/<name>`, so an app that does not use it never installs the module.\n */\n readonly entry?: string;\n /** What differs from the web API (`label` instead of children…); empty for a native-only component. */\n readonly differences: readonly string[];\n}\n\nexport interface KeyboardInteraction {\n /** The keys, as written on a keyboard: `Space`, `Enter`, `Arrow keys`, `Esc`, `Tab`. */\n readonly keys: string;\n /** What they do. */\n readonly action: string;\n}\n\nexport interface ComponentMeta {\n /** The display name: `Button`, `Dropdown menu`. */\n readonly title: string;\n /** One sentence: what it is. */\n readonly summary: string;\n readonly status: Status;\n /** Required for, and only for, a `deprecated` component. */\n readonly deprecated?: Deprecation;\n readonly category: Category;\n /** Must match the sides below: `both` ⇔ `web` and `native`. */\n readonly platforms: Platform;\n /** The platforms it is not on, and why; and its desktop form (see `Parity`). */\n readonly parity: Parity;\n /** When to reach for it: concrete situations, one per item. */\n readonly whenToUse: readonly string[];\n /** When not to — and what to use instead (`use`), when another component is the answer. */\n readonly whenNotToUse: readonly { readonly when: string; readonly use?: ComponentName }[];\n /** How to use it well: wording, placement, composition. */\n readonly bestPractices: readonly string[];\n readonly accessibility: {\n /** The keyboard, when it does more than Tab and Enter. */\n readonly keyboard: readonly KeyboardInteraction[];\n /** Roles, names and announcements: what the component does, what the app must provide. */\n readonly notes: readonly string[];\n };\n /** The components a reader of this page should know about. */\n readonly related: readonly ComponentName[];\n readonly web?: PlatformDocs;\n readonly native?: NativeDocs;\n}\n"
|
|
136
136
|
}
|
|
137
137
|
],
|
|
138
138
|
"dependencies": [
|