@svgrid/enterprise 2.0.4 → 2.2.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 +18 -6
- package/dist/cdn/svgrid-enterprise.svelte-external.js +14024 -6835
- package/dist/node/studio.js +7888 -2459
- package/package.json +9 -4
- package/src/SvGridMasterDetail.svelte +24 -3
- package/src/SvGridScheduler.svelte +4410 -0
- package/src/SvPivotDesigner.svelte +1990 -1045
- package/src/SvSchemaChart.svelte +10 -9
- package/src/ai.test.ts +522 -522
- package/src/ai.ts +202 -2
- package/src/index.ts +409 -384
- package/src/install.ts +10 -0
- package/src/pivot-chart.test.ts +86 -0
- package/src/pivot-chart.ts +112 -0
- package/src/scheduler.ts +37 -0
- package/src/scheduling.test.ts +194 -0
- package/src/scheduling.ts +293 -0
- package/src/sources/filters.ts +6 -0
- package/src/studio/HANDLERS-DESIGN.md +142 -0
- package/src/studio/cli.ts +7 -2
- package/src/studio/emit-project.test.ts +1447 -13
- package/src/studio/emit-project.ts +3995 -1273
- package/src/studio/emit-schema.ts +146 -29
- package/src/studio/index.ts +320 -195
- package/src/studio/project.test.ts +370 -0
- package/src/studio/project.ts +1146 -26
- package/src/studio/sample-data.ts +4 -1
- package/src/studio/samples/ats.ts +2 -2
- package/src/studio/samples/clinic.ts +4 -2
- package/src/studio/samples/crm.ts +16 -8
- package/src/studio/samples/events.ts +4 -2
- package/src/studio/samples/fleet.ts +4 -2
- package/src/studio/samples/gym.ts +4 -2
- package/src/studio/samples/hr.ts +3 -1
- package/src/studio/samples/live-data.ts +308 -308
- package/src/studio/samples/projects.ts +2 -2
- package/src/studio/samples/restaurant.ts +4 -2
- package/src/studio/samples/samples.test.ts +13 -5
- package/src/studio/samples/shared.ts +346 -305
- package/src/studio/samples/support.ts +3 -1
- package/src/studio/scaffold.test.ts +15 -1
- package/src/studio/scaffold.ts +16 -0
- package/src/studio/themes.ts +7 -0
- package/src/studio/ui-components.ts +472 -0
- package/src/sveltekit/transport.test.ts +26 -0
- package/src/sveltekit/transport.ts +50 -5
- package/dist/designer/assets/index-Dp44bTid.js +0 -939
- package/dist/designer/assets/index-RJp6x8tw.css +0 -1
- package/dist/designer/assets/jszip.min-CjMo-QGg.js +0 -2
- package/dist/designer/index.html +0 -13
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { EntitySchema } from '../../schema.js'
|
|
2
|
-
import { screen, formScreen, boardScreen, detailScreen, project, dashScreen, statusPills, pad, ids, type SampleApp } from './shared.js'
|
|
2
|
+
import { screen, formScreen, boardScreen, workspaceScreen, detailScreen, project, dashScreen, statusPills, pad, ids, type SampleApp } from './shared.js'
|
|
3
3
|
|
|
4
4
|
const customers: EntitySchema = {
|
|
5
5
|
name: 'customers',
|
|
@@ -148,6 +148,8 @@ export const support: SampleApp = {
|
|
|
148
148
|
// priority (rows) x status (columns).
|
|
149
149
|
// Ticket board: tickets as cards in status columns (drag to change status).
|
|
150
150
|
boardScreen(tickets, { id: 'board', title: 'Board', order: 1 }, { groupBy: 'status', titleField: 'subject', subtitleField: 'priority', filter: ['priority', 'channel', 'agentId'] }),
|
|
151
|
+
// A docking console: filter | ticket queue | selected-ticket panel (drag / float / pin).
|
|
152
|
+
workspaceScreen(tickets, { id: 'console', title: 'Console', order: 2 }, { filter: ['priority', 'channel', 'agentId'], record: true, grid: { format: [...statusPills(tickets, 'status'), ...statusPills(tickets, 'priority')] } }),
|
|
151
153
|
// Customer 360: account header (plan pill + health tile) with a timeline of
|
|
152
154
|
// the customer's tickets (newest first, colored by status).
|
|
153
155
|
detailScreen(customers, { id: 'customer-360', title: 'Customer 360', order: 2 }, {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
2
|
import { compile } from 'svelte/compiler'
|
|
3
3
|
import type { EntitySchema } from '../schema'
|
|
4
|
-
import { MANAGED_END, MANAGED_START, mergeManaged, scaffold } from './scaffold'
|
|
4
|
+
import { MANAGED_END, MANAGED_START, mergeManaged, scaffold, skipUserOwned } from './scaffold'
|
|
5
5
|
|
|
6
6
|
const schema: EntitySchema = {
|
|
7
7
|
name: 'customers',
|
|
@@ -176,3 +176,17 @@ describe('mergeManaged', () => {
|
|
|
176
176
|
expect(mergeManaged('no markers here', 'GEN')).toBe('GEN')
|
|
177
177
|
})
|
|
178
178
|
})
|
|
179
|
+
|
|
180
|
+
describe('skipUserOwned (the handlers.ts write-once contract)', () => {
|
|
181
|
+
it('skips a user-owned file that already exists on disk (never clobber user code)', () => {
|
|
182
|
+
expect(skipUserOwned({ userOwned: true }, true)).toBe(true)
|
|
183
|
+
})
|
|
184
|
+
it('writes a user-owned file when it does not exist yet (scaffold the stub once)', () => {
|
|
185
|
+
expect(skipUserOwned({ userOwned: true }, false)).toBe(false)
|
|
186
|
+
})
|
|
187
|
+
it('always writes a generated (non-user-owned) file, whether or not it exists', () => {
|
|
188
|
+
expect(skipUserOwned({ userOwned: false }, true)).toBe(false)
|
|
189
|
+
expect(skipUserOwned({}, true)).toBe(false)
|
|
190
|
+
expect(skipUserOwned({ userOwned: undefined }, true)).toBe(false)
|
|
191
|
+
})
|
|
192
|
+
})
|
package/src/studio/scaffold.ts
CHANGED
|
@@ -20,6 +20,10 @@ export type GeneratedFile = {
|
|
|
20
20
|
path: string
|
|
21
21
|
contents: string
|
|
22
22
|
description: string
|
|
23
|
+
/** A user-owned companion (e.g. a screen's `handlers.ts`): scaffolded once as a
|
|
24
|
+
* starting stub, then never overwritten. In-place writers must skip it if it
|
|
25
|
+
* already exists. See HANDLERS-DESIGN.md. */
|
|
26
|
+
userOwned?: boolean
|
|
23
27
|
}
|
|
24
28
|
|
|
25
29
|
export type ScaffoldOptions = {
|
|
@@ -68,6 +72,18 @@ const DRIVERS: Record<'postgres' | 'mysql' | 'mssql' | 'sqlite', DriverWiring> =
|
|
|
68
72
|
},
|
|
69
73
|
}
|
|
70
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Whether an in-place writer must SKIP this file to preserve the developer's
|
|
77
|
+
* code: a user-owned companion (a screen's `handlers.ts`) that already exists on
|
|
78
|
+
* disk. Such files carry no `svgrid:managed` markers, so `mergeManaged` would
|
|
79
|
+
* overwrite them wholesale - the write-once contract lives here instead, shared
|
|
80
|
+
* by every writer (CLI regenerate + the designer's bundle write). See
|
|
81
|
+
* HANDLERS-DESIGN.md.
|
|
82
|
+
*/
|
|
83
|
+
export function skipUserOwned(file: Pick<GeneratedFile, 'userOwned'>, existsOnDisk: boolean): boolean {
|
|
84
|
+
return file.userOwned === true && existsOnDisk
|
|
85
|
+
}
|
|
86
|
+
|
|
71
87
|
export const MANAGED_START = '// svgrid:managed:start'
|
|
72
88
|
export const MANAGED_END = '// svgrid:managed:end'
|
|
73
89
|
const MANAGED_NOTE = '// Regenerated by SvGrid Studio. Edits inside these markers are overwritten.'
|
package/src/studio/themes.ts
CHANGED
|
@@ -47,6 +47,13 @@ export function resolveThemeTokens(theme?: ProjectTheme): Record<string, string>
|
|
|
47
47
|
return resolveTokens(preset, mode, theme?.accent)
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
/** Resolve the `--sg-*` tokens for a project theme in an EXPLICIT mode (for the
|
|
51
|
+
* generated app's light/dark switcher, which ships both token sets). */
|
|
52
|
+
export function resolveThemeTokensFor(theme: ProjectTheme | undefined, mode: 'light' | 'dark'): Record<string, string> {
|
|
53
|
+
const preset = getStudioTheme(theme?.preset) ?? defaultStudioTheme
|
|
54
|
+
return resolveTokens(preset, mode, theme?.accent)
|
|
55
|
+
}
|
|
56
|
+
|
|
50
57
|
/** An inline `style` string (`--sg-x: y; ...; color-scheme; font-family`) for previews. */
|
|
51
58
|
export function themeStyleString(theme?: ProjectTheme): string {
|
|
52
59
|
const tokens = resolveThemeTokens(theme)
|
|
@@ -0,0 +1,472 @@
|
|
|
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
|
+
export type UiPropType = 'string' | 'number' | 'boolean' | 'select' | 'color'
|
|
17
|
+
|
|
18
|
+
export type UiComponentProp = {
|
|
19
|
+
key: string
|
|
20
|
+
label: string
|
|
21
|
+
type: UiPropType
|
|
22
|
+
/** Choices for `type: 'select'`. */
|
|
23
|
+
options?: string[]
|
|
24
|
+
default?: unknown
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type UiComponentSpec = {
|
|
28
|
+
/** Registry key - stored as `ComponentConfig.component`. */
|
|
29
|
+
key: string
|
|
30
|
+
label: string
|
|
31
|
+
category: string
|
|
32
|
+
/** Named export from `@svgrid/grid`. */
|
|
33
|
+
importName: string
|
|
34
|
+
/** Scalar "chrome" props only - see module doc. */
|
|
35
|
+
props: UiComponentProp[]
|
|
36
|
+
/** True if the component's real content is a `children` snippet - adds one
|
|
37
|
+
* literal-text field to the property panel, stored under `props._content`. */
|
|
38
|
+
hasContent?: boolean
|
|
39
|
+
contentLabel?: string
|
|
40
|
+
contentDefault?: string
|
|
41
|
+
/** Array / object props a scalar form can't express (a Timeline's `items`, a
|
|
42
|
+
* Sparkline's `data`). Each is emitted verbatim in codegen as `key={<expr>}`
|
|
43
|
+
* with a baked demo dataset the user then edits in Code view, and its `preview`
|
|
44
|
+
* value drives the live designer canvas. Not shown in the property panel. */
|
|
45
|
+
fixed?: Array<{ key: string; expr: string; preview: unknown }>
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const UI_COMPONENT_REGISTRY: ReadonlyArray<UiComponentSpec> = [
|
|
49
|
+
{
|
|
50
|
+
key: 'button',
|
|
51
|
+
label: 'Button',
|
|
52
|
+
category: 'Actions',
|
|
53
|
+
importName: 'SvButton',
|
|
54
|
+
props: [
|
|
55
|
+
{ key: 'variant', label: 'Variant', type: 'select', options: ['primary', 'secondary', 'outline', 'ghost', 'danger'], default: 'primary' },
|
|
56
|
+
{ key: 'size', label: 'Size', type: 'select', options: ['sm', 'md', 'lg'], default: 'md' },
|
|
57
|
+
{ key: 'block', label: 'Full width', type: 'boolean', default: false },
|
|
58
|
+
{ key: 'disabled', label: 'Disabled', type: 'boolean', default: false },
|
|
59
|
+
],
|
|
60
|
+
hasContent: true,
|
|
61
|
+
contentLabel: 'Label',
|
|
62
|
+
contentDefault: 'Click me',
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
key: 'badge',
|
|
66
|
+
label: 'Badge',
|
|
67
|
+
category: 'Feedback',
|
|
68
|
+
importName: 'SvBadge',
|
|
69
|
+
props: [
|
|
70
|
+
{ key: 'variant', label: 'Variant', type: 'select', options: ['neutral', 'accent', 'success', 'warning', 'danger', 'info'], default: 'neutral' },
|
|
71
|
+
{ key: 'size', label: 'Size', type: 'select', options: ['sm', 'md'], default: 'md' },
|
|
72
|
+
{ key: 'pill', label: 'Pill shape', type: 'boolean', default: true },
|
|
73
|
+
{ key: 'dot', label: 'Show dot', type: 'boolean', default: false },
|
|
74
|
+
],
|
|
75
|
+
hasContent: true,
|
|
76
|
+
contentLabel: 'Text',
|
|
77
|
+
contentDefault: 'Badge',
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
key: 'alert',
|
|
81
|
+
label: 'Alert',
|
|
82
|
+
category: 'Feedback',
|
|
83
|
+
importName: 'SvAlert',
|
|
84
|
+
props: [
|
|
85
|
+
{ key: 'variant', label: 'Variant', type: 'select', options: ['info', 'success', 'warning', 'danger', 'neutral'], default: 'info' },
|
|
86
|
+
{ key: 'title', label: 'Title', type: 'string' },
|
|
87
|
+
{ key: 'dismissible', label: 'Dismissible', type: 'boolean', default: false },
|
|
88
|
+
{ key: 'soft', label: 'Soft (tinted) style', type: 'boolean', default: false },
|
|
89
|
+
],
|
|
90
|
+
hasContent: true,
|
|
91
|
+
contentLabel: 'Message',
|
|
92
|
+
contentDefault: 'This is an alert message.',
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
key: 'progress',
|
|
96
|
+
label: 'Progress bar',
|
|
97
|
+
category: 'Feedback',
|
|
98
|
+
importName: 'SvProgress',
|
|
99
|
+
props: [
|
|
100
|
+
{ key: 'value', label: 'Value', type: 'number', default: 60 },
|
|
101
|
+
{ key: 'max', label: 'Max', type: 'number', default: 100 },
|
|
102
|
+
{ key: 'color', label: 'Color', type: 'select', options: ['accent', 'success', 'warning', 'danger'], default: 'accent' },
|
|
103
|
+
{ key: 'size', label: 'Size', type: 'select', options: ['sm', 'md', 'lg'], default: 'md' },
|
|
104
|
+
{ key: 'showLabel', label: 'Show percentage', type: 'boolean', default: false },
|
|
105
|
+
{ key: 'striped', label: 'Striped', type: 'boolean', default: false },
|
|
106
|
+
],
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
key: 'card',
|
|
110
|
+
label: 'Card',
|
|
111
|
+
category: 'Layout',
|
|
112
|
+
importName: 'SvCard',
|
|
113
|
+
props: [
|
|
114
|
+
{ key: 'title', label: 'Title', type: 'string', default: 'Card title' },
|
|
115
|
+
{ key: 'subtitle', label: 'Subtitle', type: 'string' },
|
|
116
|
+
{ key: 'hoverable', label: 'Hover elevation', type: 'boolean', default: false },
|
|
117
|
+
{ key: 'flush', label: 'Flush padding', type: 'boolean', default: false },
|
|
118
|
+
],
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
key: 'divider',
|
|
122
|
+
label: 'Divider',
|
|
123
|
+
category: 'Layout',
|
|
124
|
+
importName: 'SvDivider',
|
|
125
|
+
props: [
|
|
126
|
+
{ key: 'orientation', label: 'Orientation', type: 'select', options: ['horizontal', 'vertical'], default: 'horizontal' },
|
|
127
|
+
{ key: 'label', label: 'Label', type: 'string' },
|
|
128
|
+
{ key: 'align', label: 'Label align', type: 'select', options: ['start', 'center', 'end'], default: 'center' },
|
|
129
|
+
{ key: 'dashed', label: 'Dashed', type: 'boolean', default: false },
|
|
130
|
+
],
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
key: 'avatar',
|
|
134
|
+
label: 'Avatar',
|
|
135
|
+
category: 'Display',
|
|
136
|
+
importName: 'SvAvatar',
|
|
137
|
+
props: [
|
|
138
|
+
{ key: 'name', label: 'Name', type: 'string', default: 'Jordan Lee' },
|
|
139
|
+
{ key: 'src', label: 'Image URL', type: 'string' },
|
|
140
|
+
{ key: 'size', label: 'Size', type: 'select', options: ['sm', 'md', 'lg'], default: 'md' },
|
|
141
|
+
{ key: 'shape', label: 'Shape', type: 'select', options: ['circle', 'square'], default: 'circle' },
|
|
142
|
+
{ key: 'status', label: 'Status', type: 'select', options: ['', 'online', 'offline', 'busy', 'away'], default: '' },
|
|
143
|
+
{ key: 'color', label: 'Color override', type: 'color' },
|
|
144
|
+
],
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
key: 'stat',
|
|
148
|
+
label: 'Stat',
|
|
149
|
+
category: 'Display',
|
|
150
|
+
importName: 'SvStat',
|
|
151
|
+
props: [
|
|
152
|
+
{ key: 'label', label: 'Label', type: 'string', default: 'Revenue' },
|
|
153
|
+
{ key: 'value', label: 'Value', type: 'string', default: '$48,200' },
|
|
154
|
+
{ key: 'delta', label: 'Delta', type: 'string' },
|
|
155
|
+
{ key: 'trend', label: 'Trend', type: 'select', options: ['', 'up', 'down', 'flat'], default: '' },
|
|
156
|
+
{ key: 'hint', label: 'Hint', type: 'string' },
|
|
157
|
+
{ key: 'invert', label: 'Invert trend colors', type: 'boolean', default: false },
|
|
158
|
+
],
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
// --- Actions -------------------------------------------------------------
|
|
162
|
+
{
|
|
163
|
+
key: 'toggle-button',
|
|
164
|
+
label: 'Toggle button',
|
|
165
|
+
category: 'Actions',
|
|
166
|
+
importName: 'SvToggleButton',
|
|
167
|
+
props: [
|
|
168
|
+
{ key: 'pressed', label: 'Pressed', type: 'boolean', default: false },
|
|
169
|
+
{ key: 'size', label: 'Size', type: 'select', options: ['sm', 'md', 'lg'], default: 'md' },
|
|
170
|
+
{ key: 'disabled', label: 'Disabled', type: 'boolean', default: false },
|
|
171
|
+
],
|
|
172
|
+
hasContent: true,
|
|
173
|
+
contentLabel: 'Label',
|
|
174
|
+
contentDefault: 'Toggle',
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
key: 'switch',
|
|
178
|
+
label: 'Switch',
|
|
179
|
+
category: 'Actions',
|
|
180
|
+
importName: 'SvSwitchButton',
|
|
181
|
+
props: [
|
|
182
|
+
{ key: 'checked', label: 'On', type: 'boolean', default: true },
|
|
183
|
+
{ key: 'label', label: 'Field label', type: 'string' },
|
|
184
|
+
{ key: 'onLabel', label: 'On label', type: 'string', default: 'On' },
|
|
185
|
+
{ key: 'offLabel', label: 'Off label', type: 'string', default: 'Off' },
|
|
186
|
+
{ key: 'disabled', label: 'Disabled', type: 'boolean', default: false },
|
|
187
|
+
],
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
// --- Inputs --------------------------------------------------------------
|
|
191
|
+
{
|
|
192
|
+
key: 'text-input',
|
|
193
|
+
label: 'Text input',
|
|
194
|
+
category: 'Inputs',
|
|
195
|
+
importName: 'SvTextInput',
|
|
196
|
+
props: [
|
|
197
|
+
{ key: 'label', label: 'Label', type: 'string', default: 'Name' },
|
|
198
|
+
{ key: 'placeholder', label: 'Placeholder', type: 'string', default: 'Type here…' },
|
|
199
|
+
{ key: 'type', label: 'Type', type: 'select', options: ['text', 'email', 'url', 'tel', 'search'], default: 'text' },
|
|
200
|
+
{ key: 'size', label: 'Size', type: 'select', options: ['sm', 'md', 'lg'], default: 'md' },
|
|
201
|
+
{ key: 'clearable', label: 'Clearable', type: 'boolean', default: false },
|
|
202
|
+
{ key: 'disabled', label: 'Disabled', type: 'boolean', default: false },
|
|
203
|
+
{ key: 'readonly', label: 'Read only', type: 'boolean', default: false },
|
|
204
|
+
],
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
key: 'textarea',
|
|
208
|
+
label: 'Text area',
|
|
209
|
+
category: 'Inputs',
|
|
210
|
+
importName: 'SvTextArea',
|
|
211
|
+
props: [
|
|
212
|
+
{ key: 'label', label: 'Label', type: 'string', default: 'Notes' },
|
|
213
|
+
{ key: 'placeholder', label: 'Placeholder', type: 'string', default: 'Write something…' },
|
|
214
|
+
{ key: 'rows', label: 'Rows', type: 'number', default: 3 },
|
|
215
|
+
{ key: 'disabled', label: 'Disabled', type: 'boolean', default: false },
|
|
216
|
+
],
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
key: 'number-input',
|
|
220
|
+
label: 'Number input',
|
|
221
|
+
category: 'Inputs',
|
|
222
|
+
importName: 'SvNumberInput',
|
|
223
|
+
props: [
|
|
224
|
+
{ key: 'label', label: 'Label', type: 'string', default: 'Amount' },
|
|
225
|
+
{ key: 'placeholder', label: 'Placeholder', type: 'string' },
|
|
226
|
+
{ key: 'min', label: 'Min', type: 'number' },
|
|
227
|
+
{ key: 'max', label: 'Max', type: 'number' },
|
|
228
|
+
{ key: 'step', label: 'Step', type: 'number', default: 1 },
|
|
229
|
+
{ key: 'prefix', label: 'Prefix', type: 'string' },
|
|
230
|
+
{ key: 'suffix', label: 'Suffix', type: 'string' },
|
|
231
|
+
{ key: 'disabled', label: 'Disabled', type: 'boolean', default: false },
|
|
232
|
+
],
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
key: 'password-input',
|
|
236
|
+
label: 'Password input',
|
|
237
|
+
category: 'Inputs',
|
|
238
|
+
importName: 'SvPasswordInput',
|
|
239
|
+
props: [
|
|
240
|
+
{ key: 'label', label: 'Label', type: 'string', default: 'Password' },
|
|
241
|
+
{ key: 'placeholder', label: 'Placeholder', type: 'string', default: '••••••••' },
|
|
242
|
+
{ key: 'revealable', label: 'Reveal toggle', type: 'boolean', default: true },
|
|
243
|
+
{ key: 'showStrength', label: 'Strength meter', type: 'boolean', default: false },
|
|
244
|
+
{ key: 'disabled', label: 'Disabled', type: 'boolean', default: false },
|
|
245
|
+
],
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
key: 'color-input',
|
|
249
|
+
label: 'Color input',
|
|
250
|
+
category: 'Inputs',
|
|
251
|
+
importName: 'SvColorInput',
|
|
252
|
+
props: [
|
|
253
|
+
{ key: 'label', label: 'Label', type: 'string', default: 'Brand color' },
|
|
254
|
+
{ key: 'value', label: 'Value', type: 'color', default: '#6366f1' },
|
|
255
|
+
{ key: 'disabled', label: 'Disabled', type: 'boolean', default: false },
|
|
256
|
+
],
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
key: 'checkbox',
|
|
260
|
+
label: 'Checkbox',
|
|
261
|
+
category: 'Inputs',
|
|
262
|
+
importName: 'SvCheckBox',
|
|
263
|
+
props: [
|
|
264
|
+
{ key: 'checked', label: 'Checked', type: 'boolean', default: true },
|
|
265
|
+
{ key: 'disabled', label: 'Disabled', type: 'boolean', default: false },
|
|
266
|
+
],
|
|
267
|
+
hasContent: true,
|
|
268
|
+
contentLabel: 'Label',
|
|
269
|
+
contentDefault: 'I agree to the terms',
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
key: 'rating',
|
|
273
|
+
label: 'Rating',
|
|
274
|
+
category: 'Inputs',
|
|
275
|
+
importName: 'SvRating',
|
|
276
|
+
props: [
|
|
277
|
+
{ key: 'value', label: 'Value', type: 'number', default: 3 },
|
|
278
|
+
{ key: 'max', label: 'Stars', type: 'number', default: 5 },
|
|
279
|
+
{ key: 'allowHalf', label: 'Allow half', type: 'boolean', default: false },
|
|
280
|
+
{ key: 'readOnly', label: 'Read only', type: 'boolean', default: false },
|
|
281
|
+
{ key: 'size', label: 'Size', type: 'select', options: ['sm', 'md', 'lg'], default: 'md' },
|
|
282
|
+
],
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
key: 'slider',
|
|
286
|
+
label: 'Slider',
|
|
287
|
+
category: 'Inputs',
|
|
288
|
+
importName: 'SvSlider',
|
|
289
|
+
props: [
|
|
290
|
+
{ key: 'value', label: 'Value', type: 'number', default: 50 },
|
|
291
|
+
{ key: 'min', label: 'Min', type: 'number', default: 0 },
|
|
292
|
+
{ key: 'max', label: 'Max', type: 'number', default: 100 },
|
|
293
|
+
{ key: 'step', label: 'Step', type: 'number', default: 1 },
|
|
294
|
+
{ key: 'showValue', label: 'Show value', type: 'boolean', default: false },
|
|
295
|
+
{ key: 'labels', label: 'Tick labels', type: 'select', options: ['none', 'endpoints', 'all'], default: 'none' },
|
|
296
|
+
],
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
key: 'otp-input',
|
|
300
|
+
label: 'OTP input',
|
|
301
|
+
category: 'Inputs',
|
|
302
|
+
importName: 'SvOtpInput',
|
|
303
|
+
props: [
|
|
304
|
+
{ key: 'label', label: 'Label', type: 'string', default: 'Verification code' },
|
|
305
|
+
{ key: 'length', label: 'Digits', type: 'number', default: 6 },
|
|
306
|
+
{ key: 'mask', label: 'Mask input', type: 'boolean', default: false },
|
|
307
|
+
{ key: 'disabled', label: 'Disabled', type: 'boolean', default: false },
|
|
308
|
+
],
|
|
309
|
+
},
|
|
310
|
+
|
|
311
|
+
// --- Feedback ------------------------------------------------------------
|
|
312
|
+
{
|
|
313
|
+
key: 'circular-progress',
|
|
314
|
+
label: 'Circular progress',
|
|
315
|
+
category: 'Feedback',
|
|
316
|
+
importName: 'SvCircularProgress',
|
|
317
|
+
props: [
|
|
318
|
+
{ key: 'value', label: 'Value', type: 'number', default: 66 },
|
|
319
|
+
{ key: 'max', label: 'Max', type: 'number', default: 100 },
|
|
320
|
+
{ key: 'size', label: 'Diameter (px)', type: 'number', default: 52 },
|
|
321
|
+
{ key: 'thickness', label: 'Thickness', type: 'number', default: 4 },
|
|
322
|
+
{ key: 'color', label: 'Color', type: 'select', options: ['accent', 'success', 'warning', 'danger'], default: 'accent' },
|
|
323
|
+
{ key: 'showLabel', label: 'Show percentage', type: 'boolean', default: true },
|
|
324
|
+
{ key: 'indeterminate', label: 'Indeterminate', type: 'boolean', default: false },
|
|
325
|
+
],
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
key: 'skeleton',
|
|
329
|
+
label: 'Skeleton',
|
|
330
|
+
category: 'Feedback',
|
|
331
|
+
importName: 'SvSkeleton',
|
|
332
|
+
props: [
|
|
333
|
+
{ key: 'variant', label: 'Variant', type: 'select', options: ['text', 'rect', 'circle'], default: 'text' },
|
|
334
|
+
{ key: 'lines', label: 'Lines (text)', type: 'number', default: 3 },
|
|
335
|
+
{ key: 'width', label: 'Width (CSS)', type: 'string' },
|
|
336
|
+
{ key: 'height', label: 'Height (CSS)', type: 'string' },
|
|
337
|
+
{ key: 'animated', label: 'Animated', type: 'boolean', default: true },
|
|
338
|
+
],
|
|
339
|
+
},
|
|
340
|
+
|
|
341
|
+
// --- Display -------------------------------------------------------------
|
|
342
|
+
{
|
|
343
|
+
key: 'chip',
|
|
344
|
+
label: 'Chip',
|
|
345
|
+
category: 'Display',
|
|
346
|
+
importName: 'SvChip',
|
|
347
|
+
props: [
|
|
348
|
+
{ key: 'variant', label: 'Variant', type: 'select', options: ['neutral', 'accent', 'success', 'warning', 'danger', 'info'], default: 'neutral' },
|
|
349
|
+
{ key: 'size', label: 'Size', type: 'select', options: ['sm', 'md'], default: 'md' },
|
|
350
|
+
{ key: 'solid', label: 'Solid', type: 'boolean', default: false },
|
|
351
|
+
{ key: 'removable', label: 'Removable', type: 'boolean', default: false },
|
|
352
|
+
{ key: 'disabled', label: 'Disabled', type: 'boolean', default: false },
|
|
353
|
+
],
|
|
354
|
+
hasContent: true,
|
|
355
|
+
contentLabel: 'Text',
|
|
356
|
+
contentDefault: 'Chip',
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
key: 'empty-state',
|
|
360
|
+
label: 'Empty state',
|
|
361
|
+
category: 'Display',
|
|
362
|
+
importName: 'SvEmptyState',
|
|
363
|
+
props: [
|
|
364
|
+
{ key: 'title', label: 'Title', type: 'string', default: 'No results yet' },
|
|
365
|
+
{ key: 'description', label: 'Description', type: 'string', default: 'Try adjusting your filters or add a new record.' },
|
|
366
|
+
{ key: 'compact', label: 'Compact', type: 'boolean', default: false },
|
|
367
|
+
],
|
|
368
|
+
},
|
|
369
|
+
{
|
|
370
|
+
key: 'timeline',
|
|
371
|
+
label: 'Timeline',
|
|
372
|
+
category: 'Display',
|
|
373
|
+
importName: 'SvTimeline',
|
|
374
|
+
props: [],
|
|
375
|
+
fixed: [
|
|
376
|
+
{
|
|
377
|
+
key: 'items',
|
|
378
|
+
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' }]`,
|
|
379
|
+
preview: [
|
|
380
|
+
{ title: 'Order placed', time: '09:12', color: '#16a34a' },
|
|
381
|
+
{ title: 'Packed', time: '11:30', description: 'Ready to ship' },
|
|
382
|
+
{ title: 'Shipped', time: '14:40', color: '#6366f1' },
|
|
383
|
+
{ title: 'Delivered', time: '17:05' },
|
|
384
|
+
],
|
|
385
|
+
},
|
|
386
|
+
],
|
|
387
|
+
},
|
|
388
|
+
{
|
|
389
|
+
key: 'sparkline',
|
|
390
|
+
label: 'Sparkline',
|
|
391
|
+
category: 'Display',
|
|
392
|
+
importName: 'SvSparkline',
|
|
393
|
+
props: [
|
|
394
|
+
{ key: 'type', label: 'Type', type: 'select', options: ['line', 'area', 'bar', 'winloss'], default: 'area' },
|
|
395
|
+
{ key: 'color', label: 'Color', type: 'color', default: '#6366f1' },
|
|
396
|
+
{ key: 'width', label: 'Width (px)', type: 'number', default: 120 },
|
|
397
|
+
{ key: 'height', label: 'Height (px)', type: 'number', default: 32 },
|
|
398
|
+
],
|
|
399
|
+
fixed: [
|
|
400
|
+
{ key: 'data', expr: `[4, 8, 5, 9, 7, 11, 8, 13, 10, 15]`, preview: [4, 8, 5, 9, 7, 11, 8, 13, 10, 15] },
|
|
401
|
+
],
|
|
402
|
+
},
|
|
403
|
+
{
|
|
404
|
+
key: 'avatar-group',
|
|
405
|
+
label: 'Avatar group',
|
|
406
|
+
category: 'Display',
|
|
407
|
+
importName: 'SvAvatarGroup',
|
|
408
|
+
props: [
|
|
409
|
+
{ key: 'max', label: 'Max shown', type: 'number', default: 4 },
|
|
410
|
+
{ key: 'size', label: 'Size', type: 'select', options: ['sm', 'md', 'lg'], default: 'md' },
|
|
411
|
+
],
|
|
412
|
+
fixed: [
|
|
413
|
+
{
|
|
414
|
+
key: 'avatars',
|
|
415
|
+
expr: `[{ name: 'Jordan Lee' }, { name: 'Sam Rivera' }, { name: 'Alex Kim' }, { name: 'Priya Patel' }, { name: 'Chris Diaz' }]`,
|
|
416
|
+
preview: [{ name: 'Jordan Lee' }, { name: 'Sam Rivera' }, { name: 'Alex Kim' }, { name: 'Priya Patel' }, { name: 'Chris Diaz' }],
|
|
417
|
+
},
|
|
418
|
+
],
|
|
419
|
+
},
|
|
420
|
+
|
|
421
|
+
// --- Navigation ----------------------------------------------------------
|
|
422
|
+
{
|
|
423
|
+
key: 'pagination',
|
|
424
|
+
label: 'Pagination',
|
|
425
|
+
category: 'Navigation',
|
|
426
|
+
importName: 'SvPagination',
|
|
427
|
+
props: [
|
|
428
|
+
{ key: 'page', label: 'Current page', type: 'number', default: 1 },
|
|
429
|
+
{ key: 'pageCount', label: 'Page count', type: 'number', default: 5 },
|
|
430
|
+
{ key: 'size', label: 'Size', type: 'select', options: ['sm', 'md', 'lg'], default: 'md' },
|
|
431
|
+
{ key: 'showFirstLast', label: 'First / last', type: 'boolean', default: true },
|
|
432
|
+
{ key: 'showPrevNext', label: 'Prev / next', type: 'boolean', default: true },
|
|
433
|
+
{ key: 'siblingCount', label: 'Sibling count', type: 'number', default: 1 },
|
|
434
|
+
],
|
|
435
|
+
},
|
|
436
|
+
{
|
|
437
|
+
key: 'breadcrumb',
|
|
438
|
+
label: 'Breadcrumb',
|
|
439
|
+
category: 'Navigation',
|
|
440
|
+
importName: 'SvBreadcrumb',
|
|
441
|
+
props: [],
|
|
442
|
+
fixed: [
|
|
443
|
+
{
|
|
444
|
+
key: 'items',
|
|
445
|
+
expr: `[{ label: 'Home', href: '/' }, { label: 'Orders', href: '/orders' }, { label: '#1024' }]`,
|
|
446
|
+
preview: [{ label: 'Home', href: '/' }, { label: 'Orders', href: '/orders' }, { label: '#1024' }],
|
|
447
|
+
},
|
|
448
|
+
],
|
|
449
|
+
},
|
|
450
|
+
{
|
|
451
|
+
key: 'stepper',
|
|
452
|
+
label: 'Stepper',
|
|
453
|
+
category: 'Navigation',
|
|
454
|
+
importName: 'SvStepper',
|
|
455
|
+
props: [
|
|
456
|
+
{ key: 'current', label: 'Current step', type: 'number', default: 1 },
|
|
457
|
+
{ key: 'orientation', label: 'Orientation', type: 'select', options: ['horizontal', 'vertical'], default: 'horizontal' },
|
|
458
|
+
{ key: 'linear', label: 'Linear', type: 'boolean', default: true },
|
|
459
|
+
],
|
|
460
|
+
fixed: [
|
|
461
|
+
{
|
|
462
|
+
key: 'steps',
|
|
463
|
+
expr: `[{ label: 'Cart' }, { label: 'Shipping' }, { label: 'Payment' }, { label: 'Review' }]`,
|
|
464
|
+
preview: [{ label: 'Cart' }, { label: 'Shipping' }, { label: 'Payment' }, { label: 'Review' }],
|
|
465
|
+
},
|
|
466
|
+
],
|
|
467
|
+
},
|
|
468
|
+
]
|
|
469
|
+
|
|
470
|
+
export function uiComponentSpec(key: string): UiComponentSpec | undefined {
|
|
471
|
+
return UI_COMPONENT_REGISTRY.find((s) => s.key === key)
|
|
472
|
+
}
|
|
@@ -126,6 +126,32 @@ describe('createKitHandlers server-enforced validation', () => {
|
|
|
126
126
|
const res = await post(handlers, { kind: 'mutate', op: 'create', input: { id: '9', age: 999 } })
|
|
127
127
|
expect(res.status).toBe(200)
|
|
128
128
|
})
|
|
129
|
+
|
|
130
|
+
it('business-rule hooks transform the payload, reject writes, and fire after-effects', async () => {
|
|
131
|
+
const audited: string[] = []
|
|
132
|
+
const handlers = createKitHandlers({
|
|
133
|
+
schema: vSchema,
|
|
134
|
+
source: createInMemoryDataSource<Customer>([], vSchema),
|
|
135
|
+
hooks: {
|
|
136
|
+
beforeCreate: ({ values }) => {
|
|
137
|
+
const v = values as Record<string, unknown>
|
|
138
|
+
if (v['name'] == null || v['name'] === '') throw new Error('name required')
|
|
139
|
+
if (v['tier'] == null) v['tier'] = 'free' // default a field server-side
|
|
140
|
+
},
|
|
141
|
+
afterCreate: ({ row }) => { audited.push(String((row as Record<string, unknown>).id)) },
|
|
142
|
+
},
|
|
143
|
+
})
|
|
144
|
+
// transform: tier defaulted to 'free', afterCreate fired
|
|
145
|
+
const ok = await post(handlers, { kind: 'mutate', op: 'create', input: { id: '1', name: 'Ada' } })
|
|
146
|
+
expect(ok.status).toBe(200)
|
|
147
|
+
expect((await ok.json()).tier).toBe('free')
|
|
148
|
+
expect(audited).toEqual(['1'])
|
|
149
|
+
// reject: a thrown before-hook -> 422 with the message, no after-effect
|
|
150
|
+
const bad = await post(handlers, { kind: 'mutate', op: 'create', input: { id: '2' } })
|
|
151
|
+
expect(bad.status).toBe(422)
|
|
152
|
+
expect((await bad.json()).error).toBe('name required')
|
|
153
|
+
expect(audited).toEqual(['1']) // unchanged - the write never happened
|
|
154
|
+
})
|
|
129
155
|
})
|
|
130
156
|
|
|
131
157
|
describe('createKitHandlers audit hook', () => {
|