@svgrid/enterprise 2.3.0 → 2.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/dist/cdn/svgrid-enterprise.svelte-external.js +6885 -3504
  2. package/dist/designer/assets/GridMenus-BtWVk9Ab.js +7 -0
  3. package/dist/designer/assets/SvGridChartPanel-DWlBO2SD.js +10 -0
  4. package/dist/designer/assets/SvGridChartView-8c8Mb4j8.js +1 -0
  5. package/dist/designer/assets/SvGridChartView-DX9HfkBR.css +1 -0
  6. package/dist/designer/assets/index-DsDgp9Xq.js +78758 -0
  7. package/dist/designer/assets/index-tTY_Dx4P.css +1 -0
  8. package/dist/designer/assets/jszip.min-fkJdmAmj.js +2 -0
  9. package/dist/designer/assets/pdfmake-DeCsnyl9.js +242 -0
  10. package/dist/designer/assets/smart.export-BZlSCE8T.js +35 -0
  11. package/dist/designer/assets/vfs_fonts-eX2NpmfX.js +1 -0
  12. package/dist/designer/index.html +13 -0
  13. package/dist/node/studio.js +5660 -1747
  14. package/package.json +8 -5
  15. package/src/SvGridBoard.svelte +4 -1
  16. package/src/SvGridScheduler.svelte +114 -90
  17. package/src/ai-export-pdf.dom.test.ts +77 -77
  18. package/src/ai-export-xlsx.dom.test.ts +90 -90
  19. package/src/ai-export.dom.test.ts +114 -114
  20. package/src/export-ooxml.ts +4 -0
  21. package/src/export-xls.ts +3 -0
  22. package/src/expressions/evaluate.ts +9 -0
  23. package/src/import.test.ts +1 -1
  24. package/src/import.ts +1 -1
  25. package/src/index.ts +12 -0
  26. package/src/pivot.test.ts +0 -1
  27. package/src/schema-designer.ts +1 -1
  28. package/src/studio/emit-project.test.ts +298 -7
  29. package/src/studio/emit-project.ts +483 -29
  30. package/src/studio/emit-schema.ts +20 -10
  31. package/src/studio/index.ts +34 -0
  32. package/src/studio/init-flow.test.ts +239 -0
  33. package/src/studio/init-flow.ts +358 -0
  34. package/src/studio/project.test.ts +0 -1
  35. package/src/studio/project.ts +109 -2
  36. package/src/studio/samples/datasets.test.ts +84 -0
  37. package/src/studio/samples/datasets.ts +340 -0
  38. package/src/studio/samples/live-data.test.ts +1 -1
  39. package/src/studio/samples/samples.test.ts +268 -268
  40. package/src/studio/samples/shared.ts +7 -150
  41. package/src/studio/screen-suites.test.ts +226 -0
  42. package/src/studio/screen-suites.ts +445 -0
  43. package/src/studio/ui-components-surface.test.ts +2 -2
  44. package/src/studio/ui-components.generated.ts +1643 -106
  45. package/src/studio/ui-components.ts +742 -641
  46. package/src/sveltekit/index.ts +1 -0
  47. package/src/sveltekit/sql-source.ts +1 -1
  48. package/src/sveltekit/transport-scope.test.ts +125 -0
  49. package/src/sveltekit/transport.ts +76 -2
@@ -1,641 +1,742 @@
1
- /**
2
- * A curated catalogue of SvGrid UI-kit components a Studio screen can host as a
3
- * `'component'` block (see `ComponentConfig` in `project.ts`) - entity-agnostic,
4
- * usable on any screen including a freestanding one. Pure metadata: no Svelte
5
- * import here (this subtree stays Svelte-free), so the designer holds the live
6
- * component references and this module only describes what to configure and how
7
- * to generate.
8
- *
9
- * Only "chrome" props (scalar values a form can drive) are listed - every
10
- * component's real content is typically a Svelte `Snippet`, which this registry
11
- * can't represent. A component with `hasContent` gets one extra literal-text
12
- * field in the property panel (stored under the reserved `_content` props key)
13
- * that becomes its plain-text children in both the live preview and codegen.
14
- */
15
-
16
- import { GENERATED_UI_SURFACE } from './ui-components.generated.js'
17
-
18
- export type UiPropType = 'string' | 'number' | 'boolean' | 'select' | 'color' | 'json' | 'date'
19
-
20
- export type UiPropGroup = 'common' | 'appearance' | 'behavior' | 'advanced'
21
-
22
- export type UiComponentProp = {
23
- key: string
24
- label: string
25
- type: UiPropType
26
- /** Choices for `type: 'select'`. */
27
- options?: string[]
28
- default?: unknown
29
- /** Guidance shown as the property panel tooltip - sourced from the component's
30
- * own JSDoc via the extractor, so it stays true to the source. */
31
- description?: string
32
- /** Property-panel grouping (defaults to 'common'). */
33
- group?: UiPropGroup
34
- /** A function / code-only prop (e.g. loadChildren) - listed read-only, set in code. */
35
- code?: boolean
36
- }
37
-
38
- /** A wireable component event: `key` is the handle event name (`ctx.x.on<key>`),
39
- * `prop` the component callback prop that fires it (e.g. `onChange`). */
40
- export type UiComponentEvent = { key: string; label: string; prop?: string; description?: string }
41
-
42
- /** Standard DOM / interaction events EVERY component block can wire (VS-style full
43
- * event surface) - forwarded by the generated wrapper to the block's handle. `dom`
44
- * is the DOM event the wrapper listens for; `key` is the handle event name. */
45
- export const STANDARD_UI_EVENTS: ReadonlyArray<{ key: string; label: string; dom: string; description: string }> = [
46
- { key: 'click', label: 'Click', dom: 'click', description: 'The component (or a child) was clicked.' },
47
- { key: 'dblclick', label: 'Double click', dom: 'dblclick', description: 'The component was double-clicked.' },
48
- { key: 'change', label: 'Change', dom: 'change', description: 'A committed value change (fires on blur for inputs).' },
49
- { key: 'input', label: 'Input', dom: 'input', description: 'Fires on every keystroke / immediate value edit.' },
50
- { key: 'focus', label: 'Focus', dom: 'focusin', description: 'The component (or a child) gained focus.' },
51
- { key: 'blur', label: 'Blur', dom: 'focusout', description: 'The component (or a child) lost focus.' },
52
- { key: 'keydown', label: 'Key down', dom: 'keydown', description: 'A key was pressed while focused.' },
53
- { key: 'keyup', label: 'Key up', dom: 'keyup', description: 'A key was released while focused.' },
54
- { key: 'contextmenu', label: 'Context menu', dom: 'contextmenu', description: 'Right-click / context-menu request.' },
55
- { key: 'mouseenter', label: 'Mouse enter', dom: 'mouseenter', description: 'The pointer entered the component.' },
56
- { key: 'mouseleave', label: 'Mouse leave', dom: 'mouseleave', description: 'The pointer left the component.' },
57
- ]
58
-
59
- export type UiComponentSpec = {
60
- /** Registry key - stored as `ComponentConfig.component`. */
61
- key: string
62
- label: string
63
- category: string
64
- /** Named export from `@svgrid/grid`. */
65
- importName: string
66
- /** Hand-curated prop entries. In the EXPORTED registry these are merged with
67
- * the extracted full surface (ui-components.generated.ts): hand entries win
68
- * by key (label/options/default), extraction supplies the rest + tooltips. */
69
- props: UiComponentProp[]
70
- /** Wireable events (merged with extracted function-prop events the same way). */
71
- events?: UiComponentEvent[]
72
- /** Extracted props to keep OUT of the property panel (edge cases). */
73
- hiddenProps?: string[]
74
- /** True if the component's real content is a `children` snippet - adds one
75
- * literal-text field to the property panel, stored under `props._content`. */
76
- hasContent?: boolean
77
- contentLabel?: string
78
- contentDefault?: string
79
- /** Array / object props a scalar form can't express (a Timeline's `items`, a
80
- * Sparkline's `data`). Each is emitted verbatim in codegen as `key={<expr>}`
81
- * with a baked demo dataset the user then edits in Code view, and its `preview`
82
- * value drives the live designer canvas. Not shown in the property panel. */
83
- fixed?: Array<{ key: string; expr: string; preview: unknown }>
84
- }
85
-
86
- const BASE_REGISTRY: ReadonlyArray<UiComponentSpec> = [
87
- {
88
- key: 'button',
89
- label: 'Button',
90
- category: 'Actions',
91
- importName: 'SvButton',
92
- props: [
93
- { key: 'variant', label: 'Variant', type: 'select', options: ['primary', 'secondary', 'outline', 'ghost', 'danger'], default: 'primary' },
94
- { key: 'size', label: 'Size', type: 'select', options: ['sm', 'md', 'lg'], default: 'md' },
95
- { key: 'block', label: 'Full width', type: 'boolean', default: false },
96
- { key: 'disabled', label: 'Disabled', type: 'boolean', default: false },
97
- ],
98
- hasContent: true,
99
- contentLabel: 'Label',
100
- contentDefault: 'Click me',
101
- },
102
- {
103
- key: 'badge',
104
- label: 'Badge',
105
- category: 'Feedback',
106
- importName: 'SvBadge',
107
- props: [
108
- { key: 'variant', label: 'Variant', type: 'select', options: ['neutral', 'accent', 'success', 'warning', 'danger', 'info'], default: 'neutral' },
109
- { key: 'size', label: 'Size', type: 'select', options: ['sm', 'md'], default: 'md' },
110
- { key: 'pill', label: 'Pill shape', type: 'boolean', default: true },
111
- { key: 'dot', label: 'Show dot', type: 'boolean', default: false },
112
- ],
113
- hasContent: true,
114
- contentLabel: 'Text',
115
- contentDefault: 'Badge',
116
- },
117
- {
118
- key: 'alert',
119
- label: 'Alert',
120
- category: 'Feedback',
121
- importName: 'SvAlert',
122
- props: [
123
- { key: 'variant', label: 'Variant', type: 'select', options: ['info', 'success', 'warning', 'danger', 'neutral'], default: 'info' },
124
- { key: 'title', label: 'Title', type: 'string' },
125
- { key: 'dismissible', label: 'Dismissible', type: 'boolean', default: false },
126
- { key: 'soft', label: 'Soft (tinted) style', type: 'boolean', default: false },
127
- ],
128
- hasContent: true,
129
- contentLabel: 'Message',
130
- contentDefault: 'This is an alert message.',
131
- },
132
- {
133
- key: 'progress',
134
- label: 'Progress bar',
135
- category: 'Feedback',
136
- importName: 'SvProgress',
137
- props: [
138
- { key: 'value', label: 'Value', type: 'number', default: 60 },
139
- { key: 'max', label: 'Max', type: 'number', default: 100 },
140
- { key: 'color', label: 'Color', type: 'select', options: ['accent', 'success', 'warning', 'danger'], default: 'accent' },
141
- { key: 'size', label: 'Size', type: 'select', options: ['sm', 'md', 'lg'], default: 'md' },
142
- { key: 'showLabel', label: 'Show percentage', type: 'boolean', default: false },
143
- { key: 'striped', label: 'Striped', type: 'boolean', default: false },
144
- ],
145
- },
146
- {
147
- key: 'card',
148
- label: 'Card',
149
- category: 'Layout',
150
- importName: 'SvCard',
151
- props: [
152
- { key: 'title', label: 'Title', type: 'string', default: 'Card title' },
153
- { key: 'subtitle', label: 'Subtitle', type: 'string' },
154
- { key: 'hoverable', label: 'Hover elevation', type: 'boolean', default: false },
155
- { key: 'flush', label: 'Flush padding', type: 'boolean', default: false },
156
- ],
157
- },
158
- {
159
- key: 'divider',
160
- label: 'Divider',
161
- category: 'Layout',
162
- importName: 'SvDivider',
163
- props: [
164
- { key: 'orientation', label: 'Orientation', type: 'select', options: ['horizontal', 'vertical'], default: 'horizontal' },
165
- { key: 'label', label: 'Label', type: 'string' },
166
- { key: 'align', label: 'Label align', type: 'select', options: ['start', 'center', 'end'], default: 'center' },
167
- { key: 'dashed', label: 'Dashed', type: 'boolean', default: false },
168
- ],
169
- },
170
- {
171
- key: 'avatar',
172
- label: 'Avatar',
173
- category: 'Display',
174
- importName: 'SvAvatar',
175
- props: [
176
- { key: 'name', label: 'Name', type: 'string', default: 'Jordan Lee' },
177
- { key: 'src', label: 'Image URL', type: 'string' },
178
- { key: 'size', label: 'Size', type: 'select', options: ['sm', 'md', 'lg'], default: 'md' },
179
- { key: 'shape', label: 'Shape', type: 'select', options: ['circle', 'square'], default: 'circle' },
180
- { key: 'status', label: 'Status', type: 'select', options: ['', 'online', 'offline', 'busy', 'away'], default: '' },
181
- { key: 'color', label: 'Color override', type: 'color' },
182
- ],
183
- },
184
- {
185
- key: 'stat',
186
- label: 'Stat',
187
- category: 'Display',
188
- importName: 'SvStat',
189
- props: [
190
- { key: 'label', label: 'Label', type: 'string', default: 'Revenue' },
191
- { key: 'value', label: 'Value', type: 'string', default: '$48,200' },
192
- { key: 'delta', label: 'Delta', type: 'string' },
193
- { key: 'trend', label: 'Trend', type: 'select', options: ['', 'up', 'down', 'flat'], default: '' },
194
- { key: 'hint', label: 'Hint', type: 'string' },
195
- { key: 'invert', label: 'Invert trend colors', type: 'boolean', default: false },
196
- ],
197
- },
198
-
199
- // --- Actions -------------------------------------------------------------
200
- {
201
- key: 'toggle-button',
202
- label: 'Toggle button',
203
- category: 'Actions',
204
- importName: 'SvToggleButton',
205
- props: [
206
- { key: 'pressed', label: 'Pressed', type: 'boolean', default: false },
207
- { key: 'size', label: 'Size', type: 'select', options: ['sm', 'md', 'lg'], default: 'md' },
208
- { key: 'disabled', label: 'Disabled', type: 'boolean', default: false },
209
- ],
210
- hasContent: true,
211
- contentLabel: 'Label',
212
- contentDefault: 'Toggle',
213
- },
214
- {
215
- key: 'switch',
216
- label: 'Switch',
217
- category: 'Actions',
218
- importName: 'SvSwitchButton',
219
- props: [
220
- { key: 'checked', label: 'On', type: 'boolean', default: true },
221
- { key: 'label', label: 'Field label', type: 'string' },
222
- { key: 'onLabel', label: 'On label', type: 'string', default: 'On' },
223
- { key: 'offLabel', label: 'Off label', type: 'string', default: 'Off' },
224
- { key: 'disabled', label: 'Disabled', type: 'boolean', default: false },
225
- ],
226
- },
227
-
228
- // --- Inputs --------------------------------------------------------------
229
- {
230
- key: 'text-input',
231
- label: 'Text input',
232
- category: 'Inputs',
233
- importName: 'SvTextInput',
234
- props: [
235
- { key: 'label', label: 'Label', type: 'string', default: 'Name' },
236
- { key: 'placeholder', label: 'Placeholder', type: 'string', default: 'Type here…' },
237
- { key: 'type', label: 'Type', type: 'select', options: ['text', 'email', 'url', 'tel', 'search'], default: 'text' },
238
- { key: 'size', label: 'Size', type: 'select', options: ['sm', 'md', 'lg'], default: 'md' },
239
- { key: 'clearable', label: 'Clearable', type: 'boolean', default: false },
240
- { key: 'disabled', label: 'Disabled', type: 'boolean', default: false },
241
- { key: 'readonly', label: 'Read only', type: 'boolean', default: false },
242
- ],
243
- },
244
- {
245
- key: 'textarea',
246
- label: 'Text area',
247
- category: 'Inputs',
248
- importName: 'SvTextArea',
249
- props: [
250
- { key: 'label', label: 'Label', type: 'string', default: 'Notes' },
251
- { key: 'placeholder', label: 'Placeholder', type: 'string', default: 'Write something…' },
252
- { key: 'rows', label: 'Rows', type: 'number', default: 3 },
253
- { key: 'disabled', label: 'Disabled', type: 'boolean', default: false },
254
- ],
255
- },
256
- {
257
- key: 'number-input',
258
- label: 'Number input',
259
- category: 'Inputs',
260
- importName: 'SvNumberInput',
261
- props: [
262
- { key: 'label', label: 'Label', type: 'string', default: 'Amount' },
263
- { key: 'placeholder', label: 'Placeholder', type: 'string' },
264
- { key: 'min', label: 'Min', type: 'number' },
265
- { key: 'max', label: 'Max', type: 'number' },
266
- { key: 'step', label: 'Step', type: 'number', default: 1 },
267
- { key: 'prefix', label: 'Prefix', type: 'string' },
268
- { key: 'suffix', label: 'Suffix', type: 'string' },
269
- { key: 'disabled', label: 'Disabled', type: 'boolean', default: false },
270
- ],
271
- },
272
- {
273
- key: 'password-input',
274
- label: 'Password input',
275
- category: 'Inputs',
276
- importName: 'SvPasswordInput',
277
- props: [
278
- { key: 'label', label: 'Label', type: 'string', default: 'Password' },
279
- { key: 'placeholder', label: 'Placeholder', type: 'string', default: '••••••••' },
280
- { key: 'revealable', label: 'Reveal toggle', type: 'boolean', default: true },
281
- { key: 'showStrength', label: 'Strength meter', type: 'boolean', default: false },
282
- { key: 'disabled', label: 'Disabled', type: 'boolean', default: false },
283
- ],
284
- },
285
- {
286
- key: 'color-input',
287
- label: 'Color input',
288
- category: 'Inputs',
289
- importName: 'SvColorInput',
290
- props: [
291
- { key: 'label', label: 'Label', type: 'string', default: 'Brand color' },
292
- { key: 'value', label: 'Value', type: 'color', default: '#6366f1' },
293
- { key: 'disabled', label: 'Disabled', type: 'boolean', default: false },
294
- ],
295
- },
296
- {
297
- key: 'checkbox',
298
- label: 'Checkbox',
299
- category: 'Inputs',
300
- importName: 'SvCheckBox',
301
- props: [
302
- { key: 'checked', label: 'Checked', type: 'boolean', default: true },
303
- { key: 'disabled', label: 'Disabled', type: 'boolean', default: false },
304
- ],
305
- hasContent: true,
306
- contentLabel: 'Label',
307
- contentDefault: 'I agree to the terms',
308
- },
309
- {
310
- key: 'rating',
311
- label: 'Rating',
312
- category: 'Inputs',
313
- importName: 'SvRating',
314
- props: [
315
- { key: 'value', label: 'Value', type: 'number', default: 3 },
316
- { key: 'max', label: 'Stars', type: 'number', default: 5 },
317
- { key: 'allowHalf', label: 'Allow half', type: 'boolean', default: false },
318
- { key: 'readOnly', label: 'Read only', type: 'boolean', default: false },
319
- { key: 'size', label: 'Size', type: 'select', options: ['sm', 'md', 'lg'], default: 'md' },
320
- ],
321
- },
322
- {
323
- key: 'slider',
324
- label: 'Slider',
325
- category: 'Inputs',
326
- importName: 'SvSlider',
327
- props: [
328
- { key: 'value', label: 'Value', type: 'number', default: 50 },
329
- { key: 'min', label: 'Min', type: 'number', default: 0 },
330
- { key: 'max', label: 'Max', type: 'number', default: 100 },
331
- { key: 'step', label: 'Step', type: 'number', default: 1 },
332
- { key: 'showValue', label: 'Show value', type: 'boolean', default: false },
333
- { key: 'labels', label: 'Tick labels', type: 'select', options: ['none', 'endpoints', 'all'], default: 'none' },
334
- ],
335
- },
336
- {
337
- key: 'otp-input',
338
- label: 'OTP input',
339
- category: 'Inputs',
340
- importName: 'SvOtpInput',
341
- props: [
342
- { key: 'label', label: 'Label', type: 'string', default: 'Verification code' },
343
- { key: 'length', label: 'Digits', type: 'number', default: 6 },
344
- { key: 'mask', label: 'Mask input', type: 'boolean', default: false },
345
- { key: 'disabled', label: 'Disabled', type: 'boolean', default: false },
346
- ],
347
- },
348
-
349
- // --- Feedback ------------------------------------------------------------
350
- {
351
- key: 'circular-progress',
352
- label: 'Circular progress',
353
- category: 'Feedback',
354
- importName: 'SvCircularProgress',
355
- props: [
356
- { key: 'value', label: 'Value', type: 'number', default: 66 },
357
- { key: 'max', label: 'Max', type: 'number', default: 100 },
358
- { key: 'size', label: 'Diameter (px)', type: 'number', default: 52 },
359
- { key: 'thickness', label: 'Thickness', type: 'number', default: 4 },
360
- { key: 'color', label: 'Color', type: 'select', options: ['accent', 'success', 'warning', 'danger'], default: 'accent' },
361
- { key: 'showLabel', label: 'Show percentage', type: 'boolean', default: true },
362
- { key: 'indeterminate', label: 'Indeterminate', type: 'boolean', default: false },
363
- ],
364
- },
365
- {
366
- key: 'skeleton',
367
- label: 'Skeleton',
368
- category: 'Feedback',
369
- importName: 'SvSkeleton',
370
- props: [
371
- { key: 'variant', label: 'Variant', type: 'select', options: ['text', 'rect', 'circle'], default: 'text' },
372
- { key: 'lines', label: 'Lines (text)', type: 'number', default: 3 },
373
- { key: 'width', label: 'Width (CSS)', type: 'string' },
374
- { key: 'height', label: 'Height (CSS)', type: 'string' },
375
- { key: 'animated', label: 'Animated', type: 'boolean', default: true },
376
- ],
377
- },
378
-
379
- // --- Display -------------------------------------------------------------
380
- {
381
- key: 'chip',
382
- label: 'Chip',
383
- category: 'Display',
384
- importName: 'SvChip',
385
- props: [
386
- { key: 'variant', label: 'Variant', type: 'select', options: ['neutral', 'accent', 'success', 'warning', 'danger', 'info'], default: 'neutral' },
387
- { key: 'size', label: 'Size', type: 'select', options: ['sm', 'md'], default: 'md' },
388
- { key: 'solid', label: 'Solid', type: 'boolean', default: false },
389
- { key: 'removable', label: 'Removable', type: 'boolean', default: false },
390
- { key: 'disabled', label: 'Disabled', type: 'boolean', default: false },
391
- ],
392
- hasContent: true,
393
- contentLabel: 'Text',
394
- contentDefault: 'Chip',
395
- },
396
- {
397
- key: 'empty-state',
398
- label: 'Empty state',
399
- category: 'Display',
400
- importName: 'SvEmptyState',
401
- props: [
402
- { key: 'title', label: 'Title', type: 'string', default: 'No results yet' },
403
- { key: 'description', label: 'Description', type: 'string', default: 'Try adjusting your filters or add a new record.' },
404
- { key: 'compact', label: 'Compact', type: 'boolean', default: false },
405
- ],
406
- },
407
- {
408
- key: 'timeline',
409
- label: 'Timeline',
410
- category: 'Display',
411
- importName: 'SvTimeline',
412
- props: [],
413
- fixed: [
414
- {
415
- key: 'items',
416
- expr: `[{ title: 'Order placed', time: '09:12', color: '#16a34a' }, { title: 'Packed', time: '11:30', description: 'Ready to ship' }, { title: 'Shipped', time: '14:40', color: '#6366f1' }, { title: 'Delivered', time: '17:05' }]`,
417
- preview: [
418
- { title: 'Order placed', time: '09:12', color: '#16a34a' },
419
- { title: 'Packed', time: '11:30', description: 'Ready to ship' },
420
- { title: 'Shipped', time: '14:40', color: '#6366f1' },
421
- { title: 'Delivered', time: '17:05' },
422
- ],
423
- },
424
- ],
425
- },
426
- {
427
- key: 'sparkline',
428
- label: 'Sparkline',
429
- category: 'Display',
430
- importName: 'SvSparkline',
431
- props: [
432
- { key: 'type', label: 'Type', type: 'select', options: ['line', 'area', 'bar', 'winloss'], default: 'area' },
433
- { key: 'color', label: 'Color', type: 'color', default: '#6366f1' },
434
- { key: 'width', label: 'Width (px)', type: 'number', default: 120 },
435
- { key: 'height', label: 'Height (px)', type: 'number', default: 32 },
436
- ],
437
- fixed: [
438
- { key: 'data', expr: `[4, 8, 5, 9, 7, 11, 8, 13, 10, 15]`, preview: [4, 8, 5, 9, 7, 11, 8, 13, 10, 15] },
439
- ],
440
- },
441
- {
442
- key: 'avatar-group',
443
- label: 'Avatar group',
444
- category: 'Display',
445
- importName: 'SvAvatarGroup',
446
- props: [
447
- { key: 'max', label: 'Max shown', type: 'number', default: 4 },
448
- { key: 'size', label: 'Size', type: 'select', options: ['sm', 'md', 'lg'], default: 'md' },
449
- ],
450
- fixed: [
451
- {
452
- key: 'avatars',
453
- expr: `[{ name: 'Jordan Lee' }, { name: 'Sam Rivera' }, { name: 'Alex Kim' }, { name: 'Priya Patel' }, { name: 'Chris Diaz' }]`,
454
- preview: [{ name: 'Jordan Lee' }, { name: 'Sam Rivera' }, { name: 'Alex Kim' }, { name: 'Priya Patel' }, { name: 'Chris Diaz' }],
455
- },
456
- ],
457
- },
458
-
459
- // --- Navigation ----------------------------------------------------------
460
- {
461
- key: 'pagination',
462
- label: 'Pagination',
463
- category: 'Navigation',
464
- importName: 'SvPagination',
465
- props: [
466
- { key: 'page', label: 'Current page', type: 'number', default: 1 },
467
- { key: 'pageCount', label: 'Page count', type: 'number', default: 5 },
468
- { key: 'size', label: 'Size', type: 'select', options: ['sm', 'md', 'lg'], default: 'md' },
469
- { key: 'showFirstLast', label: 'First / last', type: 'boolean', default: true },
470
- { key: 'showPrevNext', label: 'Prev / next', type: 'boolean', default: true },
471
- { key: 'siblingCount', label: 'Sibling count', type: 'number', default: 1 },
472
- ],
473
- },
474
- {
475
- key: 'breadcrumb',
476
- label: 'Breadcrumb',
477
- category: 'Navigation',
478
- importName: 'SvBreadcrumb',
479
- props: [],
480
- fixed: [
481
- {
482
- key: 'items',
483
- expr: `[{ label: 'Home', href: '/' }, { label: 'Orders', href: '/orders' }, { label: '#1024' }]`,
484
- preview: [{ label: 'Home', href: '/' }, { label: 'Orders', href: '/orders' }, { label: '#1024' }],
485
- },
486
- ],
487
- },
488
- {
489
- key: 'stepper',
490
- label: 'Stepper',
491
- category: 'Navigation',
492
- importName: 'SvStepper',
493
- props: [
494
- { key: 'current', label: 'Current step', type: 'number', default: 1 },
495
- { key: 'orientation', label: 'Orientation', type: 'select', options: ['horizontal', 'vertical'], default: 'horizontal' },
496
- { key: 'linear', label: 'Linear', type: 'boolean', default: true },
497
- ],
498
- fixed: [
499
- {
500
- key: 'steps',
501
- expr: `[{ label: 'Cart' }, { label: 'Shipping' }, { label: 'Payment' }, { label: 'Review' }]`,
502
- preview: [{ label: 'Cart' }, { label: 'Shipping' }, { label: 'Payment' }, { label: 'Review' }],
503
- },
504
- ],
505
- },
506
- // --- wave 1: full-kit registration (props/events come from the extractor) ---
507
- { key: 'masked-input', label: 'Masked input', category: 'Inputs', importName: 'SvMaskedInput', props: [] },
508
- { key: 'phone-input', label: 'Phone input', category: 'Inputs', importName: 'SvPhoneInput', props: [] },
509
- {
510
- key: 'tags-input', label: 'Tags input', category: 'Inputs', importName: 'SvTagsInput',
511
- props: [{ key: 'value', label: 'Tags', type: 'json', default: ['svelte', 'grid'] }],
512
- },
513
- { key: 'duration-input', label: 'Duration input', category: 'Inputs', importName: 'SvDurationInput', props: [] },
514
- {
515
- key: 'radio-group', label: 'Radio group', category: 'Inputs', importName: 'SvRadioGroup',
516
- props: [{ key: 'options', label: 'Options', type: 'json', default: [{ value: 'a', label: 'Option A' }, { value: 'b', label: 'Option B' }, { value: 'c', label: 'Option C' }] }],
517
- },
518
- {
519
- key: 'button-group', label: 'Button group', category: 'Actions', importName: 'SvButtonGroup',
520
- props: [{ key: 'items', label: 'Items', type: 'json', default: [{ value: 'day', label: 'Day' }, { value: 'week', label: 'Week' }, { value: 'month', label: 'Month' }] }],
521
- },
522
- {
523
- key: 'list-box', label: 'List box', category: 'Inputs', importName: 'SvListBox',
524
- props: [{ key: 'options', label: 'Options', type: 'json', default: [{ value: '1', label: 'First' }, { value: '2', label: 'Second' }, { value: '3', label: 'Third' }] }],
525
- },
526
- {
527
- key: 'dropdown-list', label: 'Dropdown list', category: 'Inputs', importName: 'SvDropDownList',
528
- props: [{ key: 'options', label: 'Options', type: 'json', default: [{ value: '1', label: 'First' }, { value: '2', label: 'Second' }, { value: '3', label: 'Third' }] }],
529
- },
530
- {
531
- key: 'combo-box', label: 'Combo box', category: 'Inputs', importName: 'SvComboBox',
532
- props: [{ key: 'options', label: 'Options', type: 'json', default: [{ value: '1', label: 'First' }, { value: '2', label: 'Second' }, { value: '3', label: 'Third' }] }],
533
- },
534
- {
535
- key: 'auto-complete', label: 'Auto complete', category: 'Inputs', importName: 'SvAutoComplete',
536
- props: [{ key: 'options', label: 'Options', type: 'json', default: [{ value: 'red', label: 'Red' }, { value: 'green', label: 'Green' }, { value: 'blue', label: 'Blue' }] }],
537
- },
538
- {
539
- key: 'multi-select', label: 'Multi select', category: 'Inputs', importName: 'SvMultiSelect',
540
- props: [{ key: 'options', label: 'Options', type: 'json', default: [{ value: '1', label: 'First' }, { value: '2', label: 'Second' }, { value: '3', label: 'Third' }] }],
541
- },
542
- { key: 'repeat-button', label: 'Repeat button', category: 'Actions', importName: 'SvRepeatButton', props: [], hasContent: true, contentLabel: 'Label', contentDefault: 'Hold me' },
543
- { key: 'rich-text', label: 'Rich text editor', category: 'Inputs', importName: 'SvRichText', props: [] },
544
- {
545
- key: 'tree', label: 'Tree', category: 'Display', importName: 'SvTree',
546
- props: [{ key: 'nodes', label: 'Nodes', type: 'json', default: [{ id: '1', label: 'Documents', children: [{ id: '1-1', label: 'Reports' }, { id: '1-2', label: 'Invoices' }] }, { id: '2', label: 'Pictures' }] }],
547
- },
548
- { key: 'country-input', label: 'Country input', category: 'Inputs', importName: 'SvCountryInput', props: [] },
549
- ]
550
-
551
- // ---------------------------------------------------------------------------
552
- // Merge: the exported registry = hand-curated spec + the component's FULL
553
- // extracted surface (ui-components.generated.ts). Hand entries win by key;
554
- // extraction contributes every remaining prop with its JSDoc tooltip, plus
555
- // the event list. Consumers see one complete `props`/`events` per component.
556
- // ---------------------------------------------------------------------------
557
-
558
- const GROUP_ORDER: Record<UiPropGroup, number> = { common: 0, appearance: 1, behavior: 2, advanced: 3 }
559
-
560
- function mergeSpec(spec: UiComponentSpec): UiComponentSpec {
561
- const generated = GENERATED_UI_SURFACE[spec.importName]
562
- if (!generated) return spec
563
- const skip = new Set<string>([...(spec.hiddenProps ?? []), ...(spec.fixed ?? []).map((f) => f.key), '_content'])
564
- const genByKey = new Map(generated.props.map((p) => [p.key, p]))
565
- // Hand-curated entries first (their order is intentional), enriched with the
566
- // extracted description/group when the hand entry doesn't set them.
567
- const curated = spec.props
568
- .filter((p) => !skip.has(p.key))
569
- .map((p) => {
570
- const g = genByKey.get(p.key)
571
- return g ? { ...g, ...p, description: p.description ?? g.description, group: p.group ?? g.group } : p
572
- })
573
- const curatedKeys = new Set(curated.map((p) => p.key))
574
- const rest = generated.props
575
- .filter((p) => !skip.has(p.key) && !curatedKeys.has(p.key))
576
- .map((p) => ({ ...p }) as UiComponentProp)
577
- .sort((a, b) => GROUP_ORDER[a.group ?? 'common'] - GROUP_ORDER[b.group ?? 'common'])
578
- // Events: extracted list as the base, hand entries override by key.
579
- const handEvents = new Map((spec.events ?? []).map((e) => [e.key, e]))
580
- const events: UiComponentEvent[] = [
581
- ...generated.events.map((e) => ({ ...e, ...(handEvents.get(e.key) ?? {}) })),
582
- ...(spec.events ?? []).filter((e) => !generated.events.some((g) => g.key === e.key)),
583
- ]
584
- return { ...spec, props: [...curated, ...rest], events }
585
- }
586
-
587
- export const UI_COMPONENT_REGISTRY: ReadonlyArray<UiComponentSpec> = BASE_REGISTRY.map(mergeSpec)
588
-
589
- export function uiComponentSpec(key: string): UiComponentSpec | undefined {
590
- return UI_COMPONENT_REGISTRY.find((s) => s.key === key)
591
- }
592
-
593
- // --- Grid: the "All properties" surface ------------------------------------
594
- // The grid block has rich CURATED controls (column/form builders, grouping, paging,
595
- // ...). These are the SvGrid props those controls already drive - excluded from the
596
- // grid's "All properties" panel so nothing shows twice. Everything else in the
597
- // extracted SvGrid surface is offered as a raw, editable override (config.props).
598
- export const GRID_CURATED_PROPS: ReadonlySet<string> = new Set<string>([
599
- 'data', 'columns', 'board', 'scheduler', 'loading', 'loadingOverlay', 'emptyMessage',
600
- 'fitColumns', 'sortable', 'externalSort', 'filterable', 'externalFilter', 'filterMode',
601
- 'showGlobalFilter', 'showColumnFilters', 'showFilterRow', 'showFilterMenu', 'showGroupingControls',
602
- 'groupable', 'showRowSelection', 'selectionMode', 'enableCellSelection', 'enableRowSummaries',
603
- 'zebraRows', 'showPagination', 'pageable', 'externalPagination', 'rowCount', 'pageIndex',
604
- 'pageSize', 'pageSizeOptions', 'paginationPosition', 'rowHeight', 'containerHeight',
605
- 'columnVirtualization', 'initialColumnPinning', 'conditionalFormats', 'features', 'editable',
606
- 'enableInlineEditing',
607
- ])
608
-
609
- /** The Grid's raw prop surface for the "All properties" panel: every extracted SvGrid
610
- * prop the curated controls do NOT already manage. Sorted by group then label. */
611
- export function gridPropSurface(): UiComponentProp[] {
612
- const g = GENERATED_UI_SURFACE['SvGrid']
613
- if (!g) return []
614
- const order: Record<string, number> = { common: 0, appearance: 1, behavior: 2, advanced: 3 }
615
- return g.props
616
- .filter((p) => !GRID_CURATED_PROPS.has(p.key))
617
- .map((p) => ({ ...p }) as UiComponentProp)
618
- .sort((a, b) => (order[a.group ?? 'common']! - order[b.group ?? 'common']!) || a.label.localeCompare(b.label))
619
- }
620
-
621
- // Props NOT offered on `ctx.grid` as runtime-settable options: structural data,
622
- // controller-owned plumbing, and seed-once `initial*` props (the grid reads those at
623
- // mount, so `setOption` on them is a no-op - excluding them keeps intellisense honest).
624
- const GRID_API_UNSETTABLE: ReadonlySet<string> = new Set<string>([
625
- 'data', 'columns', 'features', 'loading', 'onApiReady',
626
- 'externalSort', 'externalFilter', 'externalPagination', 'rowCount', 'pageIndex',
627
- 'initialSorting', 'initialColumnPinning', 'initialHiddenColumns', 'columnOrder',
628
- ])
629
-
630
- /** The Grid props settable at runtime through `ctx.grid.<prop> = value` (backed by
631
- * `api.setOption`): the full extracted SvGrid surface minus controller-owned /
632
- * seed-once props and function/snippet props. Drives the `ctx.grid` type + the code
633
- * view autocomplete. Sorted alphabetically. */
634
- export function gridApiSettableProps(): UiComponentProp[] {
635
- const g = GENERATED_UI_SURFACE['SvGrid']
636
- if (!g) return []
637
- return g.props
638
- .filter((p) => !GRID_API_UNSETTABLE.has(p.key) && !p.code && !/^on[A-Z]/.test(p.key))
639
- .map((p) => ({ ...p }) as UiComponentProp)
640
- .sort((a, b) => a.key.localeCompare(b.key))
641
- }
1
+ /**
2
+ * A curated catalogue of SvGrid UI-kit components a Studio screen can host as a
3
+ * `'component'` block (see `ComponentConfig` in `project.ts`) - entity-agnostic,
4
+ * usable on any screen including a freestanding one. Pure metadata: no Svelte
5
+ * import here (this subtree stays Svelte-free), so the designer holds the live
6
+ * component references and this module only describes what to configure and how
7
+ * to generate.
8
+ *
9
+ * Only "chrome" props (scalar values a form can drive) are listed - every
10
+ * component's real content is typically a Svelte `Snippet`, which this registry
11
+ * can't represent. A component with `hasContent` gets one extra literal-text
12
+ * field in the property panel (stored under the reserved `_content` props key)
13
+ * that becomes its plain-text children in both the live preview and codegen.
14
+ */
15
+
16
+ import { GENERATED_UI_SURFACE } from './ui-components.generated.js'
17
+
18
+ export type UiPropType = 'string' | 'number' | 'boolean' | 'select' | 'color' | 'json' | 'date'
19
+
20
+ export type UiPropGroup = 'common' | 'appearance' | 'behavior' | 'advanced'
21
+
22
+ export type UiComponentProp = {
23
+ key: string
24
+ label: string
25
+ type: UiPropType
26
+ /** Choices for `type: 'select'`. */
27
+ options?: string[]
28
+ default?: unknown
29
+ /** Guidance shown as the property panel tooltip - sourced from the component's
30
+ * own JSDoc via the extractor, so it stays true to the source. */
31
+ description?: string
32
+ /** Property-panel grouping (defaults to 'common'). */
33
+ group?: UiPropGroup
34
+ /** A function / code-only prop (e.g. loadChildren) - listed read-only, set in code. */
35
+ code?: boolean
36
+ }
37
+
38
+ /** A wireable component event: `key` is the handle event name (`ctx.x.on<key>`),
39
+ * `prop` the component callback prop that fires it (e.g. `onChange`). */
40
+ export type UiComponentEvent = { key: string; label: string; prop?: string; description?: string }
41
+
42
+ /** Standard DOM / interaction events EVERY component block can wire (VS-style full
43
+ * event surface) - forwarded by the generated wrapper to the block's handle. `dom`
44
+ * is the DOM event the wrapper listens for; `key` is the handle event name. */
45
+ export const STANDARD_UI_EVENTS: ReadonlyArray<{ key: string; label: string; dom: string; description: string }> = [
46
+ { key: 'click', label: 'Click', dom: 'click', description: 'The component (or a child) was clicked.' },
47
+ { key: 'dblclick', label: 'Double click', dom: 'dblclick', description: 'The component was double-clicked.' },
48
+ { key: 'change', label: 'Change', dom: 'change', description: 'A committed value change (fires on blur for inputs).' },
49
+ { key: 'input', label: 'Input', dom: 'input', description: 'Fires on every keystroke / immediate value edit.' },
50
+ { key: 'focus', label: 'Focus', dom: 'focusin', description: 'The component (or a child) gained focus.' },
51
+ { key: 'blur', label: 'Blur', dom: 'focusout', description: 'The component (or a child) lost focus.' },
52
+ { key: 'keydown', label: 'Key down', dom: 'keydown', description: 'A key was pressed while focused.' },
53
+ { key: 'keyup', label: 'Key up', dom: 'keyup', description: 'A key was released while focused.' },
54
+ { key: 'contextmenu', label: 'Context menu', dom: 'contextmenu', description: 'Right-click / context-menu request.' },
55
+ { key: 'mouseenter', label: 'Mouse enter', dom: 'mouseenter', description: 'The pointer entered the component.' },
56
+ { key: 'mouseleave', label: 'Mouse leave', dom: 'mouseleave', description: 'The pointer left the component.' },
57
+ ]
58
+
59
+ export type UiComponentSpec = {
60
+ /** Registry key - stored as `ComponentConfig.component`. */
61
+ key: string
62
+ label: string
63
+ category: string
64
+ /** Named export from `@svgrid/grid`. */
65
+ importName: string
66
+ /** Hand-curated prop entries. In the EXPORTED registry these are merged with
67
+ * the extracted full surface (ui-components.generated.ts): hand entries win
68
+ * by key (label/options/default), extraction supplies the rest + tooltips. */
69
+ props: UiComponentProp[]
70
+ /** Wireable events (merged with extracted function-prop events the same way). */
71
+ events?: UiComponentEvent[]
72
+ /** Extracted props to keep OUT of the property panel (edge cases). */
73
+ hiddenProps?: string[]
74
+ /** True if the component's real content is a `children` snippet - adds one
75
+ * literal-text field to the property panel, stored under `props._content`. */
76
+ hasContent?: boolean
77
+ contentLabel?: string
78
+ contentDefault?: string
79
+ /** Array / object props a scalar form can't express (a Timeline's `items`, a
80
+ * Sparkline's `data`). Each is emitted verbatim in codegen as `key={<expr>}`
81
+ * with a baked demo dataset the user then edits in Code view, and its `preview`
82
+ * value drives the live designer canvas. Not shown in the property panel. */
83
+ fixed?: Array<{ key: string; expr: string; preview: unknown }>
84
+ }
85
+
86
+ const BASE_REGISTRY: ReadonlyArray<UiComponentSpec> = [
87
+ {
88
+ key: 'button',
89
+ label: 'Button',
90
+ category: 'Actions',
91
+ importName: 'SvButton',
92
+ props: [
93
+ { key: 'variant', label: 'Variant', type: 'select', options: ['primary', 'secondary', 'outline', 'ghost', 'danger'], default: 'primary' },
94
+ { key: 'size', label: 'Size', type: 'select', options: ['sm', 'md', 'lg'], default: 'md' },
95
+ { key: 'block', label: 'Full width', type: 'boolean', default: false },
96
+ { key: 'disabled', label: 'Disabled', type: 'boolean', default: false },
97
+ ],
98
+ hasContent: true,
99
+ contentLabel: 'Label',
100
+ contentDefault: 'Click me',
101
+ },
102
+ {
103
+ key: 'badge',
104
+ label: 'Badge',
105
+ category: 'Feedback',
106
+ importName: 'SvBadge',
107
+ props: [
108
+ { key: 'variant', label: 'Variant', type: 'select', options: ['neutral', 'accent', 'success', 'warning', 'danger', 'info'], default: 'neutral' },
109
+ { key: 'size', label: 'Size', type: 'select', options: ['sm', 'md'], default: 'md' },
110
+ { key: 'pill', label: 'Pill shape', type: 'boolean', default: true },
111
+ { key: 'dot', label: 'Show dot', type: 'boolean', default: false },
112
+ ],
113
+ hasContent: true,
114
+ contentLabel: 'Text',
115
+ contentDefault: 'Badge',
116
+ },
117
+ {
118
+ key: 'alert',
119
+ label: 'Alert',
120
+ category: 'Feedback',
121
+ importName: 'SvAlert',
122
+ props: [
123
+ { key: 'variant', label: 'Variant', type: 'select', options: ['info', 'success', 'warning', 'danger', 'neutral'], default: 'info' },
124
+ { key: 'title', label: 'Title', type: 'string' },
125
+ { key: 'dismissible', label: 'Dismissible', type: 'boolean', default: false },
126
+ { key: 'soft', label: 'Soft (tinted) style', type: 'boolean', default: false },
127
+ ],
128
+ hasContent: true,
129
+ contentLabel: 'Message',
130
+ contentDefault: 'This is an alert message.',
131
+ },
132
+ {
133
+ key: 'progress',
134
+ label: 'Progress bar',
135
+ category: 'Feedback',
136
+ importName: 'SvProgress',
137
+ props: [
138
+ { key: 'value', label: 'Value', type: 'number', default: 60 },
139
+ { key: 'max', label: 'Max', type: 'number', default: 100 },
140
+ { key: 'color', label: 'Color', type: 'select', options: ['accent', 'success', 'warning', 'danger'], default: 'accent' },
141
+ { key: 'size', label: 'Size', type: 'select', options: ['sm', 'md', 'lg'], default: 'md' },
142
+ { key: 'showLabel', label: 'Show percentage', type: 'boolean', default: false },
143
+ { key: 'striped', label: 'Striped', type: 'boolean', default: false },
144
+ ],
145
+ },
146
+ {
147
+ key: 'card',
148
+ label: 'Card',
149
+ category: 'Layout',
150
+ importName: 'SvCard',
151
+ props: [
152
+ { key: 'title', label: 'Title', type: 'string', default: 'Card title' },
153
+ { key: 'subtitle', label: 'Subtitle', type: 'string' },
154
+ { key: 'hoverable', label: 'Hover elevation', type: 'boolean', default: false },
155
+ { key: 'flush', label: 'Flush padding', type: 'boolean', default: false },
156
+ ],
157
+ },
158
+ {
159
+ key: 'divider',
160
+ label: 'Divider',
161
+ category: 'Layout',
162
+ importName: 'SvDivider',
163
+ props: [
164
+ { key: 'orientation', label: 'Orientation', type: 'select', options: ['horizontal', 'vertical'], default: 'horizontal' },
165
+ { key: 'label', label: 'Label', type: 'string' },
166
+ { key: 'align', label: 'Label align', type: 'select', options: ['start', 'center', 'end'], default: 'center' },
167
+ { key: 'dashed', label: 'Dashed', type: 'boolean', default: false },
168
+ ],
169
+ },
170
+ {
171
+ key: 'avatar',
172
+ label: 'Avatar',
173
+ category: 'Display',
174
+ importName: 'SvAvatar',
175
+ props: [
176
+ { key: 'name', label: 'Name', type: 'string', default: 'Jordan Lee' },
177
+ { key: 'src', label: 'Image URL', type: 'string' },
178
+ { key: 'size', label: 'Size', type: 'select', options: ['sm', 'md', 'lg'], default: 'md' },
179
+ { key: 'shape', label: 'Shape', type: 'select', options: ['circle', 'square'], default: 'circle' },
180
+ { key: 'status', label: 'Status', type: 'select', options: ['', 'online', 'offline', 'busy', 'away'], default: '' },
181
+ { key: 'color', label: 'Color override', type: 'color' },
182
+ ],
183
+ },
184
+ {
185
+ key: 'stat',
186
+ label: 'Stat',
187
+ category: 'Display',
188
+ importName: 'SvStat',
189
+ props: [
190
+ { key: 'label', label: 'Label', type: 'string', default: 'Revenue' },
191
+ { key: 'value', label: 'Value', type: 'string', default: '$48,200' },
192
+ { key: 'delta', label: 'Delta', type: 'string' },
193
+ { key: 'trend', label: 'Trend', type: 'select', options: ['', 'up', 'down', 'flat'], default: '' },
194
+ { key: 'hint', label: 'Hint', type: 'string' },
195
+ { key: 'invert', label: 'Invert trend colors', type: 'boolean', default: false },
196
+ ],
197
+ },
198
+
199
+ // --- Actions -------------------------------------------------------------
200
+ {
201
+ key: 'toggle-button',
202
+ label: 'Toggle button',
203
+ category: 'Actions',
204
+ importName: 'SvToggleButton',
205
+ props: [
206
+ { key: 'pressed', label: 'Pressed', type: 'boolean', default: false },
207
+ { key: 'size', label: 'Size', type: 'select', options: ['sm', 'md', 'lg'], default: 'md' },
208
+ { key: 'disabled', label: 'Disabled', type: 'boolean', default: false },
209
+ ],
210
+ hasContent: true,
211
+ contentLabel: 'Label',
212
+ contentDefault: 'Toggle',
213
+ },
214
+ {
215
+ key: 'switch',
216
+ label: 'Switch',
217
+ category: 'Actions',
218
+ importName: 'SvSwitchButton',
219
+ props: [
220
+ { key: 'checked', label: 'On', type: 'boolean', default: true },
221
+ { key: 'label', label: 'Field label', type: 'string' },
222
+ { key: 'onLabel', label: 'On label', type: 'string', default: 'On' },
223
+ { key: 'offLabel', label: 'Off label', type: 'string', default: 'Off' },
224
+ { key: 'disabled', label: 'Disabled', type: 'boolean', default: false },
225
+ ],
226
+ },
227
+
228
+ // --- Inputs --------------------------------------------------------------
229
+ {
230
+ key: 'text-input',
231
+ label: 'Text input',
232
+ category: 'Inputs',
233
+ importName: 'SvTextInput',
234
+ props: [
235
+ { key: 'label', label: 'Label', type: 'string', default: 'Name' },
236
+ { key: 'placeholder', label: 'Placeholder', type: 'string', default: 'Type here…' },
237
+ { key: 'type', label: 'Type', type: 'select', options: ['text', 'email', 'url', 'tel', 'search'], default: 'text' },
238
+ { key: 'size', label: 'Size', type: 'select', options: ['sm', 'md', 'lg'], default: 'md' },
239
+ { key: 'clearable', label: 'Clearable', type: 'boolean', default: false },
240
+ { key: 'disabled', label: 'Disabled', type: 'boolean', default: false },
241
+ { key: 'readonly', label: 'Read only', type: 'boolean', default: false },
242
+ ],
243
+ },
244
+ {
245
+ key: 'textarea',
246
+ label: 'Text area',
247
+ category: 'Inputs',
248
+ importName: 'SvTextArea',
249
+ props: [
250
+ { key: 'label', label: 'Label', type: 'string', default: 'Notes' },
251
+ { key: 'placeholder', label: 'Placeholder', type: 'string', default: 'Write something…' },
252
+ { key: 'rows', label: 'Rows', type: 'number', default: 3 },
253
+ { key: 'disabled', label: 'Disabled', type: 'boolean', default: false },
254
+ ],
255
+ },
256
+ {
257
+ key: 'number-input',
258
+ label: 'Number input',
259
+ category: 'Inputs',
260
+ importName: 'SvNumberInput',
261
+ props: [
262
+ { key: 'label', label: 'Label', type: 'string', default: 'Amount' },
263
+ { key: 'placeholder', label: 'Placeholder', type: 'string' },
264
+ { key: 'min', label: 'Min', type: 'number' },
265
+ { key: 'max', label: 'Max', type: 'number' },
266
+ { key: 'step', label: 'Step', type: 'number', default: 1 },
267
+ { key: 'prefix', label: 'Prefix', type: 'string' },
268
+ { key: 'suffix', label: 'Suffix', type: 'string' },
269
+ { key: 'disabled', label: 'Disabled', type: 'boolean', default: false },
270
+ ],
271
+ },
272
+ {
273
+ key: 'password-input',
274
+ label: 'Password input',
275
+ category: 'Inputs',
276
+ importName: 'SvPasswordInput',
277
+ props: [
278
+ { key: 'label', label: 'Label', type: 'string', default: 'Password' },
279
+ { key: 'placeholder', label: 'Placeholder', type: 'string', default: '••••••••' },
280
+ { key: 'revealable', label: 'Reveal toggle', type: 'boolean', default: true },
281
+ { key: 'showStrength', label: 'Strength meter', type: 'boolean', default: false },
282
+ { key: 'disabled', label: 'Disabled', type: 'boolean', default: false },
283
+ ],
284
+ },
285
+ {
286
+ key: 'color-input',
287
+ label: 'Color input',
288
+ category: 'Inputs',
289
+ importName: 'SvColorInput',
290
+ props: [
291
+ { key: 'label', label: 'Label', type: 'string', default: 'Brand color' },
292
+ { key: 'value', label: 'Value', type: 'color', default: '#6366f1' },
293
+ { key: 'disabled', label: 'Disabled', type: 'boolean', default: false },
294
+ ],
295
+ },
296
+ {
297
+ key: 'checkbox',
298
+ label: 'Checkbox',
299
+ category: 'Inputs',
300
+ importName: 'SvCheckBox',
301
+ props: [
302
+ { key: 'checked', label: 'Checked', type: 'boolean', default: true },
303
+ { key: 'disabled', label: 'Disabled', type: 'boolean', default: false },
304
+ ],
305
+ hasContent: true,
306
+ contentLabel: 'Label',
307
+ contentDefault: 'I agree to the terms',
308
+ },
309
+ {
310
+ key: 'rating',
311
+ label: 'Rating',
312
+ category: 'Inputs',
313
+ importName: 'SvRating',
314
+ props: [
315
+ { key: 'value', label: 'Value', type: 'number', default: 3 },
316
+ { key: 'max', label: 'Stars', type: 'number', default: 5 },
317
+ { key: 'allowHalf', label: 'Allow half', type: 'boolean', default: false },
318
+ { key: 'readOnly', label: 'Read only', type: 'boolean', default: false },
319
+ { key: 'size', label: 'Size', type: 'select', options: ['sm', 'md', 'lg'], default: 'md' },
320
+ ],
321
+ },
322
+ {
323
+ key: 'slider',
324
+ label: 'Slider',
325
+ category: 'Inputs',
326
+ importName: 'SvSlider',
327
+ props: [
328
+ { key: 'value', label: 'Value', type: 'number', default: 50 },
329
+ { key: 'min', label: 'Min', type: 'number', default: 0 },
330
+ { key: 'max', label: 'Max', type: 'number', default: 100 },
331
+ { key: 'step', label: 'Step', type: 'number', default: 1 },
332
+ { key: 'showValue', label: 'Show value', type: 'boolean', default: false },
333
+ { key: 'labels', label: 'Tick labels', type: 'select', options: ['none', 'endpoints', 'all'], default: 'none' },
334
+ ],
335
+ },
336
+ {
337
+ key: 'otp-input',
338
+ label: 'OTP input',
339
+ category: 'Inputs',
340
+ importName: 'SvOtpInput',
341
+ props: [
342
+ { key: 'label', label: 'Label', type: 'string', default: 'Verification code' },
343
+ { key: 'length', label: 'Digits', type: 'number', default: 6 },
344
+ { key: 'mask', label: 'Mask input', type: 'boolean', default: false },
345
+ { key: 'disabled', label: 'Disabled', type: 'boolean', default: false },
346
+ ],
347
+ },
348
+
349
+ // --- Feedback ------------------------------------------------------------
350
+ {
351
+ key: 'circular-progress',
352
+ label: 'Circular progress',
353
+ category: 'Feedback',
354
+ importName: 'SvCircularProgress',
355
+ props: [
356
+ { key: 'value', label: 'Value', type: 'number', default: 66 },
357
+ { key: 'max', label: 'Max', type: 'number', default: 100 },
358
+ { key: 'size', label: 'Diameter (px)', type: 'number', default: 52 },
359
+ { key: 'thickness', label: 'Thickness', type: 'number', default: 4 },
360
+ { key: 'color', label: 'Color', type: 'select', options: ['accent', 'success', 'warning', 'danger'], default: 'accent' },
361
+ { key: 'showLabel', label: 'Show percentage', type: 'boolean', default: true },
362
+ { key: 'indeterminate', label: 'Indeterminate', type: 'boolean', default: false },
363
+ ],
364
+ },
365
+ {
366
+ key: 'skeleton',
367
+ label: 'Skeleton',
368
+ category: 'Feedback',
369
+ importName: 'SvSkeleton',
370
+ props: [
371
+ { key: 'variant', label: 'Variant', type: 'select', options: ['text', 'rect', 'circle'], default: 'text' },
372
+ { key: 'lines', label: 'Lines (text)', type: 'number', default: 3 },
373
+ { key: 'width', label: 'Width (CSS)', type: 'string' },
374
+ { key: 'height', label: 'Height (CSS)', type: 'string' },
375
+ { key: 'animated', label: 'Animated', type: 'boolean', default: true },
376
+ ],
377
+ },
378
+
379
+ // --- Display -------------------------------------------------------------
380
+ {
381
+ key: 'chip',
382
+ label: 'Chip',
383
+ category: 'Display',
384
+ importName: 'SvChip',
385
+ props: [
386
+ { key: 'variant', label: 'Variant', type: 'select', options: ['neutral', 'accent', 'success', 'warning', 'danger', 'info'], default: 'neutral' },
387
+ { key: 'size', label: 'Size', type: 'select', options: ['sm', 'md'], default: 'md' },
388
+ { key: 'solid', label: 'Solid', type: 'boolean', default: false },
389
+ { key: 'removable', label: 'Removable', type: 'boolean', default: false },
390
+ { key: 'disabled', label: 'Disabled', type: 'boolean', default: false },
391
+ ],
392
+ hasContent: true,
393
+ contentLabel: 'Text',
394
+ contentDefault: 'Chip',
395
+ },
396
+ {
397
+ key: 'empty-state',
398
+ label: 'Empty state',
399
+ category: 'Display',
400
+ importName: 'SvEmptyState',
401
+ props: [
402
+ { key: 'title', label: 'Title', type: 'string', default: 'No results yet' },
403
+ { key: 'description', label: 'Description', type: 'string', default: 'Try adjusting your filters or add a new record.' },
404
+ { key: 'compact', label: 'Compact', type: 'boolean', default: false },
405
+ ],
406
+ },
407
+ {
408
+ key: 'timeline',
409
+ label: 'Timeline',
410
+ category: 'Display',
411
+ importName: 'SvTimeline',
412
+ props: [],
413
+ fixed: [
414
+ {
415
+ key: 'items',
416
+ expr: `[{ title: 'Order placed', time: '09:12', color: '#16a34a' }, { title: 'Packed', time: '11:30', description: 'Ready to ship' }, { title: 'Shipped', time: '14:40', color: '#6366f1' }, { title: 'Delivered', time: '17:05' }]`,
417
+ preview: [
418
+ { title: 'Order placed', time: '09:12', color: '#16a34a' },
419
+ { title: 'Packed', time: '11:30', description: 'Ready to ship' },
420
+ { title: 'Shipped', time: '14:40', color: '#6366f1' },
421
+ { title: 'Delivered', time: '17:05' },
422
+ ],
423
+ },
424
+ ],
425
+ },
426
+ {
427
+ key: 'sparkline',
428
+ label: 'Sparkline',
429
+ category: 'Display',
430
+ importName: 'SvSparkline',
431
+ props: [
432
+ { key: 'type', label: 'Type', type: 'select', options: ['line', 'area', 'bar', 'winloss'], default: 'area' },
433
+ { key: 'color', label: 'Color', type: 'color', default: '#6366f1' },
434
+ { key: 'width', label: 'Width (px)', type: 'number', default: 120 },
435
+ { key: 'height', label: 'Height (px)', type: 'number', default: 32 },
436
+ ],
437
+ fixed: [
438
+ { key: 'data', expr: `[4, 8, 5, 9, 7, 11, 8, 13, 10, 15]`, preview: [4, 8, 5, 9, 7, 11, 8, 13, 10, 15] },
439
+ ],
440
+ },
441
+ {
442
+ key: 'avatar-group',
443
+ label: 'Avatar group',
444
+ category: 'Display',
445
+ importName: 'SvAvatarGroup',
446
+ props: [
447
+ { key: 'max', label: 'Max shown', type: 'number', default: 4 },
448
+ { key: 'size', label: 'Size', type: 'select', options: ['sm', 'md', 'lg'], default: 'md' },
449
+ ],
450
+ fixed: [
451
+ {
452
+ key: 'avatars',
453
+ expr: `[{ name: 'Jordan Lee' }, { name: 'Sam Rivera' }, { name: 'Alex Kim' }, { name: 'Priya Patel' }, { name: 'Chris Diaz' }]`,
454
+ preview: [{ name: 'Jordan Lee' }, { name: 'Sam Rivera' }, { name: 'Alex Kim' }, { name: 'Priya Patel' }, { name: 'Chris Diaz' }],
455
+ },
456
+ ],
457
+ },
458
+
459
+ // --- Navigation ----------------------------------------------------------
460
+ {
461
+ key: 'pagination',
462
+ label: 'Pagination',
463
+ category: 'Navigation',
464
+ importName: 'SvPagination',
465
+ props: [
466
+ { key: 'page', label: 'Current page', type: 'number', default: 1 },
467
+ { key: 'pageCount', label: 'Page count', type: 'number', default: 5 },
468
+ { key: 'size', label: 'Size', type: 'select', options: ['sm', 'md', 'lg'], default: 'md' },
469
+ { key: 'showFirstLast', label: 'First / last', type: 'boolean', default: true },
470
+ { key: 'showPrevNext', label: 'Prev / next', type: 'boolean', default: true },
471
+ { key: 'siblingCount', label: 'Sibling count', type: 'number', default: 1 },
472
+ ],
473
+ },
474
+ {
475
+ key: 'breadcrumb',
476
+ label: 'Breadcrumb',
477
+ category: 'Navigation',
478
+ importName: 'SvBreadcrumb',
479
+ props: [],
480
+ fixed: [
481
+ {
482
+ key: 'items',
483
+ expr: `[{ label: 'Home', href: '/' }, { label: 'Orders', href: '/orders' }, { label: '#1024' }]`,
484
+ preview: [{ label: 'Home', href: '/' }, { label: 'Orders', href: '/orders' }, { label: '#1024' }],
485
+ },
486
+ ],
487
+ },
488
+ {
489
+ key: 'stepper',
490
+ label: 'Stepper',
491
+ category: 'Navigation',
492
+ importName: 'SvStepper',
493
+ props: [
494
+ { key: 'current', label: 'Current step', type: 'number', default: 1 },
495
+ { key: 'orientation', label: 'Orientation', type: 'select', options: ['horizontal', 'vertical'], default: 'horizontal' },
496
+ { key: 'linear', label: 'Linear', type: 'boolean', default: true },
497
+ ],
498
+ fixed: [
499
+ {
500
+ key: 'steps',
501
+ expr: `[{ label: 'Cart' }, { label: 'Shipping' }, { label: 'Payment' }, { label: 'Review' }]`,
502
+ preview: [{ label: 'Cart' }, { label: 'Shipping' }, { label: 'Payment' }, { label: 'Review' }],
503
+ },
504
+ ],
505
+ },
506
+ // --- wave 1: full-kit registration (props/events come from the extractor) ---
507
+ { key: 'masked-input', label: 'Masked input', category: 'Inputs', importName: 'SvMaskedInput', props: [] },
508
+ { key: 'phone-input', label: 'Phone input', category: 'Inputs', importName: 'SvPhoneInput', props: [] },
509
+ {
510
+ key: 'tags-input', label: 'Tags input', category: 'Inputs', importName: 'SvTagsInput',
511
+ props: [{ key: 'value', label: 'Tags', type: 'json', default: ['svelte', 'grid'] }],
512
+ },
513
+ { key: 'duration-input', label: 'Duration input', category: 'Inputs', importName: 'SvDurationInput', props: [] },
514
+ {
515
+ key: 'radio-group', label: 'Radio group', category: 'Inputs', importName: 'SvRadioGroup',
516
+ props: [{ key: 'options', label: 'Options', type: 'json', default: [{ value: 'a', label: 'Option A' }, { value: 'b', label: 'Option B' }, { value: 'c', label: 'Option C' }] }],
517
+ },
518
+ {
519
+ key: 'button-group', label: 'Button group', category: 'Actions', importName: 'SvButtonGroup',
520
+ props: [{ key: 'items', label: 'Items', type: 'json', default: [{ value: 'day', label: 'Day' }, { value: 'week', label: 'Week' }, { value: 'month', label: 'Month' }] }],
521
+ },
522
+ {
523
+ key: 'list-box', label: 'List box', category: 'Inputs', importName: 'SvListBox',
524
+ props: [{ key: 'options', label: 'Options', type: 'json', default: [{ value: '1', label: 'First' }, { value: '2', label: 'Second' }, { value: '3', label: 'Third' }] }],
525
+ },
526
+ {
527
+ key: 'dropdown-list', label: 'Dropdown list', category: 'Inputs', importName: 'SvDropDownList',
528
+ props: [{ key: 'options', label: 'Options', type: 'json', default: [{ value: '1', label: 'First' }, { value: '2', label: 'Second' }, { value: '3', label: 'Third' }] }],
529
+ },
530
+ {
531
+ key: 'combo-box', label: 'Combo box', category: 'Inputs', importName: 'SvComboBox',
532
+ props: [{ key: 'options', label: 'Options', type: 'json', default: [{ value: '1', label: 'First' }, { value: '2', label: 'Second' }, { value: '3', label: 'Third' }] }],
533
+ },
534
+ {
535
+ key: 'auto-complete', label: 'Auto complete', category: 'Inputs', importName: 'SvAutoComplete',
536
+ props: [{ key: 'options', label: 'Options', type: 'json', default: [{ value: 'red', label: 'Red' }, { value: 'green', label: 'Green' }, { value: 'blue', label: 'Blue' }] }],
537
+ },
538
+ {
539
+ key: 'multi-select', label: 'Multi select', category: 'Inputs', importName: 'SvMultiSelect',
540
+ props: [{ key: 'options', label: 'Options', type: 'json', default: [{ value: '1', label: 'First' }, { value: '2', label: 'Second' }, { value: '3', label: 'Third' }] }],
541
+ },
542
+ { key: 'repeat-button', label: 'Repeat button', category: 'Actions', importName: 'SvRepeatButton', props: [], hasContent: true, contentLabel: 'Label', contentDefault: 'Hold me' },
543
+ { key: 'rich-text', label: 'Rich text editor', category: 'Inputs', importName: 'SvRichText', props: [] },
544
+ {
545
+ key: 'tree', label: 'Tree', category: 'Display', importName: 'SvTree',
546
+ props: [{ key: 'nodes', label: 'Nodes', type: 'json', default: [{ id: '1', label: 'Documents', children: [{ id: '1-1', label: 'Reports' }, { id: '1-2', label: 'Invoices' }] }, { id: '2', label: 'Pictures' }] }],
547
+ },
548
+ { key: 'country-input', label: 'Country input', category: 'Inputs', importName: 'SvCountryInput', props: [] },
549
+
550
+ // --- Typography / prose -------------------------------------------------
551
+ // The kit's text primitives. Cheap to expose and they are what a freestanding
552
+ // page (an "About", a help panel) is mostly made of - without them a designer
553
+ // user has no way to place a heading or a paragraph.
554
+ {
555
+ key: 'title', label: 'Heading', category: 'Display', importName: 'SvTitle',
556
+ props: [{ key: 'size', label: 'Level', type: 'select', options: ['1', '2', '3', '4', '5', '6'], default: '2' }],
557
+ hasContent: true, contentLabel: 'Text', contentDefault: 'Section heading',
558
+ },
559
+ {
560
+ key: 'text', label: 'Text', category: 'Display', importName: 'SvText',
561
+ props: [
562
+ { key: 'size', label: 'Size', type: 'select', options: ['xs', 'sm', 'md', 'lg', 'xl'], default: 'md' },
563
+ { key: 'weight', label: 'Weight', type: 'select', options: ['normal', 'medium', 'semibold', 'bold'], default: 'normal' },
564
+ { key: 'tone', label: 'Tone', type: 'select', options: ['default', 'muted', 'accent', 'success', 'warning', 'error'], default: 'default' },
565
+ ],
566
+ hasContent: true, contentLabel: 'Text', contentDefault: 'Body text.',
567
+ },
568
+ {
569
+ key: 'anchor', label: 'Link', category: 'Display', importName: 'SvAnchor',
570
+ props: [
571
+ { key: 'href', label: 'URL', type: 'string', default: '#' },
572
+ { key: 'tone', label: 'Tone', type: 'select', options: ['accent', 'default', 'muted'], default: 'accent' },
573
+ { key: 'size', label: 'Size', type: 'select', options: ['sm', 'md', 'lg'], default: 'md' },
574
+ ],
575
+ hasContent: true, contentLabel: 'Label', contentDefault: 'Read more',
576
+ },
577
+ {
578
+ key: 'blockquote', label: 'Quote', category: 'Display', importName: 'SvBlockquote',
579
+ props: [], hasContent: true, contentLabel: 'Quote', contentDefault: 'A quoted passage.',
580
+ },
581
+ {
582
+ key: 'code', label: 'Inline code', category: 'Display', importName: 'SvCode',
583
+ props: [], hasContent: true, contentLabel: 'Code', contentDefault: 'npm install',
584
+ },
585
+ {
586
+ key: 'kbd', label: 'Keyboard key', category: 'Display', importName: 'SvKbd',
587
+ props: [{ key: 'size', label: 'Size', type: 'select', options: ['sm', 'md'], default: 'md' }],
588
+ hasContent: true, contentLabel: 'Key', contentDefault: 'Ctrl',
589
+ },
590
+ {
591
+ key: 'mark', label: 'Highlight', category: 'Display', importName: 'SvMark',
592
+ props: [{ key: 'tone', label: 'Tone', type: 'select', options: ['yellow', 'accent', 'success', 'warning', 'error'], default: 'yellow' }],
593
+ hasContent: true, contentLabel: 'Text', contentDefault: 'highlighted',
594
+ },
595
+ {
596
+ key: 'list', label: 'List', category: 'Display', importName: 'SvList',
597
+ props: [],
598
+ fixed: [{ key: 'items', expr: "['First item', 'Second item', 'Third item']", preview: ['First item', 'Second item', 'Third item'] }],
599
+ },
600
+
601
+ // --- Feedback -----------------------------------------------------------
602
+ {
603
+ key: 'spinner', label: 'Spinner', category: 'Feedback', importName: 'SvSpinner',
604
+ props: [{ key: 'size', label: 'Size', type: 'select', options: ['sm', 'md', 'lg'], default: 'md' }],
605
+ },
606
+ {
607
+ key: 'loading-overlay', label: 'Loading overlay', category: 'Feedback', importName: 'SvLoadingOverlay',
608
+ props: [
609
+ { key: 'visible', label: 'Visible', type: 'boolean', default: true },
610
+ { key: 'label', label: 'Label', type: 'string', default: 'Loading...' },
611
+ ],
612
+ },
613
+ {
614
+ key: 'result', label: 'Result', category: 'Feedback', importName: 'SvResult',
615
+ props: [
616
+ { key: 'status', label: 'Status', type: 'select', options: ['success', 'error', 'warning', 'info'], default: 'success' },
617
+ { key: 'title', label: 'Title', type: 'string', default: 'All done' },
618
+ ],
619
+ },
620
+
621
+ // --- Structure ----------------------------------------------------------
622
+ {
623
+ key: 'collapsible', label: 'Collapsible', category: 'Layout', importName: 'SvCollapsible',
624
+ props: [
625
+ { key: 'title', label: 'Title', type: 'string', default: 'Details' },
626
+ { key: 'open', label: 'Open', type: 'boolean', default: false },
627
+ ],
628
+ hasContent: true, contentLabel: 'Content', contentDefault: 'Hidden until expanded.',
629
+ },
630
+ {
631
+ key: 'carousel', label: 'Carousel', category: 'Layout', importName: 'SvCarousel',
632
+ props: [],
633
+ },
634
+
635
+ // --- Date / time + richer pickers ---------------------------------------
636
+ // These are grid cell editors too; as standalone blocks they are what a
637
+ // filter bar or a booking form is built from.
638
+ { key: 'date-time-picker', label: 'Date & time picker', category: 'Inputs', importName: 'SvDateTimePicker', props: [] },
639
+ { key: 'time-picker', label: 'Time picker', category: 'Inputs', importName: 'SvTimePicker', props: [] },
640
+ { key: 'date-range-input', label: 'Date range', category: 'Inputs', importName: 'SvDateRangeInput', props: [] },
641
+ {
642
+ key: 'segmented', label: 'Segmented control', category: 'Inputs', importName: 'SvSegmented',
643
+ props: [{ key: 'options', label: 'Options', type: 'json', default: [{ value: 'day', label: 'Day' }, { value: 'week', label: 'Week' }, { value: 'month', label: 'Month' }] }],
644
+ },
645
+ {
646
+ key: 'tree-select', label: 'Tree select', category: 'Inputs', importName: 'SvTreeSelect',
647
+ props: [{ key: 'nodes', label: 'Nodes', type: 'json', default: [{ id: '1', label: 'Europe', children: [{ id: '1-1', label: 'France' }, { id: '1-2', label: 'Spain' }] }, { id: '2', label: 'Asia' }] }],
648
+ },
649
+ { key: 'file-upload', label: 'File upload', category: 'Inputs', importName: 'SvFileUpload', props: [] },
650
+ ]
651
+
652
+ // ---------------------------------------------------------------------------
653
+ // Merge: the exported registry = hand-curated spec + the component's FULL
654
+ // extracted surface (ui-components.generated.ts). Hand entries win by key;
655
+ // extraction contributes every remaining prop with its JSDoc tooltip, plus
656
+ // the event list. Consumers see one complete `props`/`events` per component.
657
+ // ---------------------------------------------------------------------------
658
+
659
+ const GROUP_ORDER: Record<UiPropGroup, number> = { common: 0, appearance: 1, behavior: 2, advanced: 3 }
660
+
661
+ function mergeSpec(spec: UiComponentSpec): UiComponentSpec {
662
+ const generated = GENERATED_UI_SURFACE[spec.importName]
663
+ if (!generated) return spec
664
+ const skip = new Set<string>([...(spec.hiddenProps ?? []), ...(spec.fixed ?? []).map((f) => f.key), '_content'])
665
+ const genByKey = new Map(generated.props.map((p) => [p.key, p]))
666
+ // Hand-curated entries first (their order is intentional), enriched with the
667
+ // extracted description/group when the hand entry doesn't set them.
668
+ const curated = spec.props
669
+ .filter((p) => !skip.has(p.key))
670
+ .map((p) => {
671
+ const g = genByKey.get(p.key)
672
+ return g ? { ...g, ...p, description: p.description ?? g.description, group: p.group ?? g.group } : p
673
+ })
674
+ const curatedKeys = new Set(curated.map((p) => p.key))
675
+ const rest = generated.props
676
+ .filter((p) => !skip.has(p.key) && !curatedKeys.has(p.key))
677
+ .map((p) => ({ ...p }) as UiComponentProp)
678
+ .sort((a, b) => GROUP_ORDER[a.group ?? 'common'] - GROUP_ORDER[b.group ?? 'common'])
679
+ // Events: extracted list as the base, hand entries override by key.
680
+ const handEvents = new Map((spec.events ?? []).map((e) => [e.key, e]))
681
+ const events: UiComponentEvent[] = [
682
+ ...generated.events.map((e) => ({ ...e, ...(handEvents.get(e.key) ?? {}) })),
683
+ ...(spec.events ?? []).filter((e) => !generated.events.some((g) => g.key === e.key)),
684
+ ]
685
+ return { ...spec, props: [...curated, ...rest], events }
686
+ }
687
+
688
+ export const UI_COMPONENT_REGISTRY: ReadonlyArray<UiComponentSpec> = BASE_REGISTRY.map(mergeSpec)
689
+
690
+ export function uiComponentSpec(key: string): UiComponentSpec | undefined {
691
+ return UI_COMPONENT_REGISTRY.find((s) => s.key === key)
692
+ }
693
+
694
+ // --- Grid: the "All properties" surface ------------------------------------
695
+ // The grid block has rich CURATED controls (column/form builders, grouping, paging,
696
+ // ...). These are the SvGrid props those controls already drive - excluded from the
697
+ // grid's "All properties" panel so nothing shows twice. Everything else in the
698
+ // extracted SvGrid surface is offered as a raw, editable override (config.props).
699
+ export const GRID_CURATED_PROPS: ReadonlySet<string> = new Set<string>([
700
+ 'data', 'columns', 'board', 'scheduler', 'loading', 'loadingOverlay', 'emptyMessage',
701
+ 'fitColumns', 'sortable', 'externalSort', 'filterable', 'externalFilter', 'filterMode',
702
+ 'showGlobalFilter', 'showColumnFilters', 'showFilterRow', 'showFilterMenu', 'showGroupingControls',
703
+ 'groupable', 'showRowSelection', 'selectionMode', 'enableCellSelection', 'enableRowSummaries',
704
+ 'zebraRows', 'showPagination', 'pageable', 'externalPagination', 'rowCount', 'pageIndex',
705
+ 'pageSize', 'pageSizeOptions', 'paginationPosition', 'rowHeight', 'containerHeight',
706
+ 'columnVirtualization', 'initialColumnPinning', 'conditionalFormats', 'features', 'editable',
707
+ 'enableInlineEditing',
708
+ ])
709
+
710
+ /** The Grid's raw prop surface for the "All properties" panel: every extracted SvGrid
711
+ * prop the curated controls do NOT already manage. Sorted by group then label. */
712
+ export function gridPropSurface(): UiComponentProp[] {
713
+ const g = GENERATED_UI_SURFACE['SvGrid']
714
+ if (!g) return []
715
+ const order: Record<string, number> = { common: 0, appearance: 1, behavior: 2, advanced: 3 }
716
+ return g.props
717
+ .filter((p) => !GRID_CURATED_PROPS.has(p.key))
718
+ .map((p) => ({ ...p }) as UiComponentProp)
719
+ .sort((a, b) => (order[a.group ?? 'common']! - order[b.group ?? 'common']!) || a.label.localeCompare(b.label))
720
+ }
721
+
722
+ // Props NOT offered on `ctx.grid` as runtime-settable options: structural data,
723
+ // controller-owned plumbing, and seed-once `initial*` props (the grid reads those at
724
+ // mount, so `setOption` on them is a no-op - excluding them keeps intellisense honest).
725
+ const GRID_API_UNSETTABLE: ReadonlySet<string> = new Set<string>([
726
+ 'data', 'columns', 'features', 'loading', 'onApiReady',
727
+ 'externalSort', 'externalFilter', 'externalPagination', 'rowCount', 'pageIndex',
728
+ 'initialSorting', 'initialColumnPinning', 'initialHiddenColumns', 'columnOrder',
729
+ ])
730
+
731
+ /** The Grid props settable at runtime through `ctx.grid.<prop> = value` (backed by
732
+ * `api.setOption`): the full extracted SvGrid surface minus controller-owned /
733
+ * seed-once props and function/snippet props. Drives the `ctx.grid` type + the code
734
+ * view autocomplete. Sorted alphabetically. */
735
+ export function gridApiSettableProps(): UiComponentProp[] {
736
+ const g = GENERATED_UI_SURFACE['SvGrid']
737
+ if (!g) return []
738
+ return g.props
739
+ .filter((p) => !GRID_API_UNSETTABLE.has(p.key) && !p.code && !/^on[A-Z]/.test(p.key))
740
+ .map((p) => ({ ...p }) as UiComponentProp)
741
+ .sort((a, b) => a.key.localeCompare(b.key))
742
+ }