@poodle64/ui 2026.8.10 → 2026.8.12

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 (57) hide show
  1. package/README.md +180 -30
  2. package/dist/components/ui/app-shell/app-nav.svelte +12 -12
  3. package/dist/components/ui/app-shell/app-shell.svelte +14 -12
  4. package/dist/components/ui/app-shell/app-shell.svelte.d.ts +14 -6
  5. package/dist/components/ui/app-shell/types.d.ts +20 -3
  6. package/dist/components/ui/app-shell/types.js +29 -5
  7. package/dist/components/ui/collection-detail/collection-detail.svelte +169 -0
  8. package/dist/components/ui/collection-detail/collection-detail.svelte.d.ts +47 -0
  9. package/dist/components/ui/collection-detail/index.d.ts +3 -0
  10. package/dist/components/ui/collection-detail/index.js +2 -0
  11. package/dist/components/ui/document-detail/document-detail.svelte +184 -0
  12. package/dist/components/ui/document-detail/document-detail.svelte.d.ts +34 -0
  13. package/dist/components/ui/document-detail/index.d.ts +3 -0
  14. package/dist/components/ui/document-detail/index.js +2 -0
  15. package/dist/components/ui/library-browse/document-table.svelte +101 -0
  16. package/dist/components/ui/library-browse/document-table.svelte.d.ts +10 -0
  17. package/dist/components/ui/library-browse/facet-rail.svelte +62 -0
  18. package/dist/components/ui/library-browse/facet-rail.svelte.d.ts +8 -0
  19. package/dist/components/ui/library-browse/index.d.ts +3 -0
  20. package/dist/components/ui/library-browse/index.js +2 -0
  21. package/dist/components/ui/library-browse/library-browse.svelte +235 -0
  22. package/dist/components/ui/library-browse/library-browse.svelte.d.ts +44 -0
  23. package/dist/components/ui/library-browse/types.d.ts +101 -0
  24. package/dist/components/ui/library-browse/types.js +1 -0
  25. package/dist/components/ui/schema-form/context.d.ts +31 -0
  26. package/dist/components/ui/schema-form/context.js +10 -0
  27. package/dist/components/ui/schema-form/data.d.ts +42 -0
  28. package/dist/components/ui/schema-form/data.js +97 -0
  29. package/dist/components/ui/schema-form/dispatch.d.ts +35 -0
  30. package/dist/components/ui/schema-form/dispatch.js +139 -0
  31. package/dist/components/ui/schema-form/index.d.ts +5 -0
  32. package/dist/components/ui/schema-form/index.js +5 -0
  33. package/dist/components/ui/schema-form/schema-form-control.svelte +146 -0
  34. package/dist/components/ui/schema-form/schema-form-control.svelte.d.ts +7 -0
  35. package/dist/components/ui/schema-form/schema-form-element.svelte +105 -0
  36. package/dist/components/ui/schema-form/schema-form-element.svelte.d.ts +7 -0
  37. package/dist/components/ui/schema-form/schema-form-widget.svelte +169 -0
  38. package/dist/components/ui/schema-form/schema-form-widget.svelte.d.ts +18 -0
  39. package/dist/components/ui/schema-form/schema-form.svelte +153 -0
  40. package/dist/components/ui/schema-form/schema-form.svelte.d.ts +4 -0
  41. package/dist/components/ui/schema-form/types.d.ts +61 -0
  42. package/dist/components/ui/schema-form/types.js +21 -0
  43. package/dist/components/ui/schema-form/widgets/radio-field.svelte +48 -0
  44. package/dist/components/ui/schema-form/widgets/radio-field.svelte.d.ts +15 -0
  45. package/dist/components/ui/schema-form/widgets/slider-field.svelte +51 -0
  46. package/dist/components/ui/schema-form/widgets/slider-field.svelte.d.ts +13 -0
  47. package/dist/components/ui/schema-form/widgets/tags-field.svelte +98 -0
  48. package/dist/components/ui/schema-form/widgets/tags-field.svelte.d.ts +12 -0
  49. package/dist/components/ui/schema-form/widgets/unknown-field.svelte +123 -0
  50. package/dist/components/ui/schema-form/widgets/unknown-field.svelte.d.ts +20 -0
  51. package/dist/components/ui/search-results/index.d.ts +3 -0
  52. package/dist/components/ui/search-results/index.js +2 -0
  53. package/dist/components/ui/search-results/search-results.svelte +183 -0
  54. package/dist/components/ui/search-results/search-results.svelte.d.ts +40 -0
  55. package/package.json +2 -1
  56. package/registry/component-map.json +52 -3
  57. package/registry/component-map.md +20 -1
@@ -0,0 +1,139 @@
1
+ import { WIDGET_KINDS } from './types.js';
2
+ const KNOWN = new Set(WIDGET_KINDS);
3
+ /** The primitive JSON Schema types a `tags` widget can carry as array items. */
4
+ const TAGGABLE = new Set(['string', 'number', 'integer']);
5
+ /** JSON Schema `format` → the input type that renders it. */
6
+ const FORMAT_WIDGETS = {
7
+ password: 'password',
8
+ date: 'date',
9
+ time: 'time',
10
+ 'date-time': 'datetime'
11
+ };
12
+ /** The first `type` a subschema declares (JSON Schema allows an array of them). */
13
+ export function schemaType(schema) {
14
+ const t = schema?.type;
15
+ if (Array.isArray(t))
16
+ return t.find((entry) => entry !== 'null') ?? t[0];
17
+ return t;
18
+ }
19
+ /** Does this subschema carry a closed set of values a select can list? */
20
+ export function enumOptions(schema) {
21
+ if (!schema)
22
+ return null;
23
+ const withEnum = schema;
24
+ if (Array.isArray(withEnum.enum)) {
25
+ return withEnum.enum.map((value) => ({ value, label: String(value) }));
26
+ }
27
+ // The `oneOf: [{ const, title }]` spelling — how a server labels its enum.
28
+ if (Array.isArray(withEnum.oneOf) && withEnum.oneOf.every((entry) => 'const' in entry)) {
29
+ return withEnum.oneOf.map((entry) => ({
30
+ value: entry.const,
31
+ label: entry.title ?? String(entry.const)
32
+ }));
33
+ }
34
+ return null;
35
+ }
36
+ /**
37
+ * Resolve a Control to a widget.
38
+ *
39
+ * @param schema the subschema the Control's scope resolved to, or `undefined`
40
+ * if the scope resolved to nothing at all.
41
+ * @param options the Control's `options` object from the UI schema.
42
+ */
43
+ export function pickWidget(schema, options = {}) {
44
+ // 1. A scope that resolves to nothing is the most fundamental defect there is
45
+ // — flag it before anything else, hint or no hint.
46
+ if (!schema) {
47
+ return {
48
+ widget: 'unknown',
49
+ reason: 'unresolved-scope',
50
+ detail: 'no such property in the JSON Schema'
51
+ };
52
+ }
53
+ // 2. An explicit hint wins — and an unrecognised one is LOUD, never a default.
54
+ // `format` is the JSON Forms spelling; `widget` is accepted because the
55
+ // estate's outgoing x-ui vocabulary used it, so a migrating server does
56
+ // not have to change both documents at once.
57
+ const hint = options.format ?? options.widget;
58
+ if (typeof hint === 'string') {
59
+ if (KNOWN.has(hint)) {
60
+ // A select or a radio group over nothing renders as an empty box — which
61
+ // is the vanishing act this whole component exists to stop, one level
62
+ // down from an unrecognised hint.
63
+ if ((hint === 'select' || hint === 'radio') && !enumOptions(schema)?.length) {
64
+ return {
65
+ widget: 'unknown',
66
+ reason: 'no-options',
67
+ detail: `the "${hint}" widget needs a closed value set, and this subschema declares no enum or oneOf`
68
+ };
69
+ }
70
+ return { widget: hint };
71
+ }
72
+ return {
73
+ widget: 'unknown',
74
+ reason: 'unknown-widget',
75
+ detail: `no widget is registered for "${hint}"`
76
+ };
77
+ }
78
+ if (hint !== undefined) {
79
+ return {
80
+ widget: 'unknown',
81
+ reason: 'unknown-widget',
82
+ detail: `widget hint must be a string, got ${typeof hint}`
83
+ };
84
+ }
85
+ // 3. The two boolean options JSON Forms itself defines.
86
+ const type = schemaType(schema);
87
+ if (options.multi === true && type === 'string')
88
+ return { widget: 'textarea' };
89
+ if (options.slider === true && (type === 'number' || type === 'integer'))
90
+ return { widget: 'slider' };
91
+ // 4. Derive from the schema.
92
+ const closedSet = enumOptions(schema);
93
+ if (closedSet) {
94
+ if (!closedSet.length) {
95
+ return {
96
+ widget: 'unknown',
97
+ reason: 'no-options',
98
+ detail: 'the subschema declares an empty enum, so there is nothing to choose from'
99
+ };
100
+ }
101
+ return { widget: 'select' };
102
+ }
103
+ switch (type) {
104
+ case 'boolean':
105
+ return { widget: 'switch' };
106
+ case 'integer':
107
+ case 'number':
108
+ return { widget: 'number' };
109
+ case 'string': {
110
+ const format = schema.format;
111
+ if (format && FORMAT_WIDGETS[format])
112
+ return { widget: FORMAT_WIDGETS[format] };
113
+ return { widget: 'text' };
114
+ }
115
+ case 'array': {
116
+ const items = schema.items;
117
+ const itemType = Array.isArray(items) ? undefined : schemaType(items);
118
+ if (itemType && TAGGABLE.has(itemType))
119
+ return { widget: 'tags' };
120
+ return {
121
+ widget: 'unknown',
122
+ reason: 'unsupported-array',
123
+ detail: `array items are ${itemType ? `"${itemType}"` : 'unspecified'}; only string, number and integer items render as tags`
124
+ };
125
+ }
126
+ case 'object':
127
+ return {
128
+ widget: 'unknown',
129
+ reason: 'object-control',
130
+ detail: 'an object needs a layout with Controls for its properties, not one Control'
131
+ };
132
+ default:
133
+ return {
134
+ widget: 'unknown',
135
+ reason: 'unsupported-type',
136
+ detail: type ? `no widget renders type "${type}"` : 'the subschema declares no type'
137
+ };
138
+ }
139
+ }
@@ -0,0 +1,5 @@
1
+ export { default as SchemaForm } from './schema-form.svelte';
2
+ export { default } from './schema-form.svelte';
3
+ export { pickWidget, enumOptions, schemaType, type WidgetChoice } from './dispatch.js';
4
+ export { addressedPaths, unmappedFields, scopeToPath, getAt, setAt } from './data.js';
5
+ export { WIDGET_KINDS, type WidgetKind, type UnknownReason, type SchemaFormChange, type SchemaFormProps, type JsonSchema, type UISchemaElement } from './types.js';
@@ -0,0 +1,5 @@
1
+ export { default as SchemaForm } from './schema-form.svelte';
2
+ export { default } from './schema-form.svelte';
3
+ export { pickWidget, enumOptions, schemaType } from './dispatch.js';
4
+ export { addressedPaths, unmappedFields, scopeToPath, getAt, setAt } from './data.js';
5
+ export { WIDGET_KINDS } from './types.js';
@@ -0,0 +1,146 @@
1
+ <script lang="ts">
2
+ import { isEnabled, resolveSchema, type ControlElement } from '@jsonforms/core';
3
+ import Label from '../label/label.svelte';
4
+ import SchemaFormWidget from './schema-form-widget.svelte';
5
+ import UnknownField from './widgets/unknown-field.svelte';
6
+ import { getSchemaFormContext } from './context.js';
7
+ import { pickWidget } from './dispatch.js';
8
+ import { getAt, scopeToPath } from './data.js';
9
+ import type { JsonSchema } from './types.js';
10
+
11
+ /**
12
+ * One Control: resolve its scope against the JSON Schema, choose a widget,
13
+ * and frame it with its label, description and validation message.
14
+ *
15
+ * Every way this can fail — no scope, a scope that resolves to nothing, a
16
+ * widget hint nobody registered — leaves through `<UnknownField>`, never
17
+ * through a branch that renders nothing.
18
+ */
19
+ let { element }: { element: ControlElement } = $props();
20
+
21
+ const form = getSchemaFormContext();
22
+
23
+ const scope = $derived(typeof element.scope === 'string' ? element.scope : '');
24
+ const path = $derived(scope ? scopeToPath(scope) : '');
25
+ const subschema = $derived(
26
+ scope ? (resolveSchema(form.schema, scope, form.schema) as JsonSchema | undefined) : undefined
27
+ );
28
+ const options = $derived((element.options ?? {}) as Record<string, unknown>);
29
+ const choice = $derived(pickWidget(subschema, options));
30
+
31
+ const value = $derived(path ? getAt(form.data, path) : undefined);
32
+ const id = $derived(`${form.idPrefix}-${(path || 'unscoped').replace(/[^a-zA-Z0-9_-]/g, '-')}`);
33
+
34
+ /** `maxDepth` / `max_depth` → `Max depth`, when nothing supplied a title. */
35
+ const humanise = (key: string) =>
36
+ key
37
+ .replace(/[_-]+/g, ' ')
38
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
39
+ .replace(/^./, (first) => first.toUpperCase());
40
+
41
+ const label = $derived(
42
+ (typeof element.label === 'string' && element.label) ||
43
+ (subschema as { title?: string } | undefined)?.title ||
44
+ humanise(path.split('.').pop() || scope || 'Field')
45
+ );
46
+
47
+ const description = $derived((subschema as { description?: string } | undefined)?.description);
48
+
49
+ // `required` lives on the PARENT object schema, so read it from there rather
50
+ // than guessing from the field.
51
+ const required = $derived.by(() => {
52
+ const parentScope = scope.replace(/\/properties\/[^/]+$/, '');
53
+ const key = scope.split('/').pop() ?? '';
54
+ const parent =
55
+ parentScope === '#' || parentScope === ''
56
+ ? form.schema
57
+ : (resolveSchema(form.schema, parentScope, form.schema) as JsonSchema | undefined);
58
+ return ((parent as { required?: string[] } | undefined)?.required ?? []).includes(key);
59
+ });
60
+
61
+ const errors = $derived(form.errors[path] ?? []);
62
+ const disabled = $derived(
63
+ form.disabled ||
64
+ options.readonly === true ||
65
+ !isEnabled(element, form.data, '', form.ajv, undefined)
66
+ );
67
+
68
+ const describedBy = $derived(
69
+ [description ? `${id}-description` : null, errors.length ? `${id}-error` : null]
70
+ .filter(Boolean)
71
+ .join(' ') || undefined
72
+ );
73
+
74
+ // A toggle reads as "control, then what it toggles"; everything else reads as
75
+ // "label, then the field". Both are still one labelled field.
76
+ const inline = $derived(choice.widget === 'switch' || choice.widget === 'checkbox');
77
+
78
+ const commit = (next: unknown) => form.change(path, next);
79
+ </script>
80
+
81
+ {#if !scope}
82
+ <UnknownField
83
+ {id}
84
+ label={typeof element.label === 'string' ? element.label : 'Control'}
85
+ scope="(none)"
86
+ path=""
87
+ value={undefined}
88
+ reason="missing-scope"
89
+ detail="this Control carries no scope, so there is no property to edit"
90
+ />
91
+ {:else if choice.widget === 'unknown'}
92
+ <UnknownField
93
+ {id}
94
+ {label}
95
+ {scope}
96
+ {path}
97
+ {value}
98
+ {disabled}
99
+ schema={subschema}
100
+ reason={choice.reason ?? 'unsupported-type'}
101
+ detail={choice.detail ?? 'this control could not be rendered'}
102
+ onchange={commit}
103
+ />
104
+ {:else}
105
+ <div class="grid gap-1.5" data-schema-form-field={path}>
106
+ {#snippet widget()}
107
+ <SchemaFormWidget
108
+ {choice}
109
+ schema={subschema as JsonSchema}
110
+ {value}
111
+ {id}
112
+ {label}
113
+ {scope}
114
+ {path}
115
+ {disabled}
116
+ {describedBy}
117
+ name={path}
118
+ onchange={commit}
119
+ />
120
+ {/snippet}
121
+ {#snippet fieldLabel()}
122
+ <Label for={id} id="{id}-label" class={disabled ? 'opacity-50' : undefined}>
123
+ {label}{#if required}<span class="text-status-error" aria-hidden="true">&nbsp;*</span
124
+ ><span class="sr-only"> (required)</span>{/if}
125
+ </Label>
126
+ {/snippet}
127
+
128
+ {#if inline}
129
+ <!-- A toggle reads as "control, then what it toggles". -->
130
+ <div class="flex items-center gap-2.5">
131
+ {@render widget()}
132
+ {@render fieldLabel()}
133
+ </div>
134
+ {:else}
135
+ {@render fieldLabel()}
136
+ {@render widget()}
137
+ {/if}
138
+
139
+ {#if description}
140
+ <p id="{id}-description" class="text-muted-foreground text-xs">{description}</p>
141
+ {/if}
142
+ {#if errors.length}
143
+ <p id="{id}-error" class="text-status-error text-xs">{errors.join('. ')}</p>
144
+ {/if}
145
+ </div>
146
+ {/if}
@@ -0,0 +1,7 @@
1
+ import { type ControlElement } from '@jsonforms/core';
2
+ type $$ComponentProps = {
3
+ element: ControlElement;
4
+ };
5
+ declare const SchemaFormControl: import("svelte").Component<$$ComponentProps, {}, "">;
6
+ type SchemaFormControl = ReturnType<typeof SchemaFormControl>;
7
+ export default SchemaFormControl;
@@ -0,0 +1,105 @@
1
+ <script lang="ts">
2
+ import { isVisible, type ControlElement, type UISchemaElement } from '@jsonforms/core';
3
+ import Panel from '../panel/panel.svelte';
4
+ import * as Tabs from '../tabs/index.js';
5
+ import SchemaFormControl from './schema-form-control.svelte';
6
+ import UnknownField from './widgets/unknown-field.svelte';
7
+ import Self from './schema-form-element.svelte';
8
+ import { getSchemaFormContext } from './context.js';
9
+
10
+ /**
11
+ * One node of the UI Schema tree, recursively.
12
+ *
13
+ * The vocabulary walked here is JSON Forms' own: VerticalLayout,
14
+ * HorizontalLayout, Group, Categorization/Category, Control and Label. An
15
+ * element type outside it renders as a flagged fallback — the previous
16
+ * renderers treated an unknown node as nothing to do, which is how a nested
17
+ * hint became inert without any signal.
18
+ *
19
+ * SHOW/HIDE is evaluated here rather than inside the control, so a rule on a
20
+ * Group hides the whole group, which is what an author writing one means.
21
+ */
22
+ let { element }: { element: UISchemaElement } = $props();
23
+
24
+ const form = getSchemaFormContext();
25
+
26
+ const visible = $derived(isVisible(element, form.data, '', form.ajv, undefined));
27
+ const children = $derived(((element as { elements?: UISchemaElement[] }).elements ?? []));
28
+ const label = $derived(
29
+ typeof (element as { label?: unknown }).label === 'string'
30
+ ? ((element as { label: string }).label)
31
+ : undefined
32
+ );
33
+ </script>
34
+
35
+ {#if visible}
36
+ {#if element.type === 'VerticalLayout'}
37
+ <div class="grid gap-4" data-schema-form-layout="vertical">
38
+ {#each children as child, index (index)}
39
+ <Self element={child} />
40
+ {/each}
41
+ </div>
42
+ {:else if element.type === 'HorizontalLayout'}
43
+ <!-- Columns of equal width on a wide viewport, one stack below it. The
44
+ column count follows the element count, so a layout never needs a
45
+ breakpoint written per form. -->
46
+ <div class="grid gap-4 sm:auto-cols-fr sm:grid-flow-col" data-schema-form-layout="horizontal">
47
+ {#each children as child, index (index)}
48
+ <Self element={child} />
49
+ {/each}
50
+ </div>
51
+ {:else if element.type === 'Group'}
52
+ <Panel title={label} data-schema-form-layout="group">
53
+ <div class="grid gap-4">
54
+ {#each children as child, index (index)}
55
+ <Self element={child} />
56
+ {/each}
57
+ </div>
58
+ </Panel>
59
+ {:else if element.type === 'Categorization'}
60
+ {@const categories = children.filter((child) => isVisible(child, form.data, '', form.ajv, undefined))}
61
+ {#if categories.length}
62
+ <Tabs.Root value="category-0" data-schema-form-layout="categorization">
63
+ <Tabs.List>
64
+ {#each categories as category, index (index)}
65
+ <Tabs.Trigger value="category-{index}">
66
+ {(category as { label?: string }).label ?? `Section ${index + 1}`}
67
+ </Tabs.Trigger>
68
+ {/each}
69
+ </Tabs.List>
70
+ {#each categories as category, index (index)}
71
+ <Tabs.Content value="category-{index}" class="pt-4">
72
+ <div class="grid gap-4">
73
+ {#each (category as { elements?: UISchemaElement[] }).elements ?? [] as child, position (position)}
74
+ <Self element={child} />
75
+ {/each}
76
+ </div>
77
+ </Tabs.Content>
78
+ {/each}
79
+ </Tabs.Root>
80
+ {/if}
81
+ {:else if element.type === 'Category'}
82
+ <!-- A Category outside a Categorization is still a section of fields. -->
83
+ <div class="grid gap-4" data-schema-form-layout="category">
84
+ {#each children as child, index (index)}
85
+ <Self element={child} />
86
+ {/each}
87
+ </div>
88
+ {:else if element.type === 'Control'}
89
+ <SchemaFormControl element={element as ControlElement} />
90
+ {:else if element.type === 'Label'}
91
+ <p class="text-2xs text-muted-foreground font-semibold tracking-wide uppercase">
92
+ {(element as { text?: string }).text ?? label ?? ''}
93
+ </p>
94
+ {:else}
95
+ <UnknownField
96
+ id="unknown-element"
97
+ label={label ?? element.type}
98
+ scope="(ui schema element)"
99
+ path=""
100
+ reason="unknown-element"
101
+ detail={`"${element.type}" is not a UI schema element this renderer knows`}
102
+ value={element}
103
+ />
104
+ {/if}
105
+ {/if}
@@ -0,0 +1,7 @@
1
+ import { type UISchemaElement } from '@jsonforms/core';
2
+ type $$ComponentProps = {
3
+ element: UISchemaElement;
4
+ };
5
+ declare const SchemaFormElement: import("svelte").Component<$$ComponentProps, {}, "">;
6
+ type SchemaFormElement = ReturnType<typeof SchemaFormElement>;
7
+ export default SchemaFormElement;
@@ -0,0 +1,169 @@
1
+ <script lang="ts">
2
+ import Input from '../input/input.svelte';
3
+ import Textarea from '../textarea/textarea.svelte';
4
+ import Switch from '../switch/switch.svelte';
5
+ import Checkbox from '../checkbox/checkbox.svelte';
6
+ import * as Select from '../select/index.js';
7
+ import SliderField from './widgets/slider-field.svelte';
8
+ import TagsField from './widgets/tags-field.svelte';
9
+ import RadioField from './widgets/radio-field.svelte';
10
+ import UnknownField from './widgets/unknown-field.svelte';
11
+ import { enumOptions, schemaType, type WidgetChoice } from './dispatch.js';
12
+ import type { JsonSchema } from './types.js';
13
+
14
+ /**
15
+ * The widget dispatch table: one branch per `WidgetKind`, and a final `:else`
16
+ * that is itself the loud fallback rather than nothing. There is deliberately
17
+ * no silent arm in this file — that is the whole contract.
18
+ */
19
+ let {
20
+ choice,
21
+ schema,
22
+ value,
23
+ id,
24
+ name,
25
+ label,
26
+ scope,
27
+ path,
28
+ disabled = false,
29
+ describedBy,
30
+ onchange
31
+ }: {
32
+ choice: WidgetChoice;
33
+ schema: JsonSchema;
34
+ value: unknown;
35
+ id: string;
36
+ name: string;
37
+ label: string;
38
+ scope: string;
39
+ path: string;
40
+ disabled?: boolean;
41
+ describedBy?: string;
42
+ onchange: (next: unknown) => void;
43
+ } = $props();
44
+
45
+ const type = $derived(schemaType(schema));
46
+ const options = $derived(enumOptions(schema) ?? []);
47
+ const bounds = $derived(schema as { minimum?: number; maximum?: number; multipleOf?: number });
48
+ const items = $derived((schema as { items?: JsonSchema }).items);
49
+
50
+ const text = $derived(value === undefined || value === null ? '' : String(value));
51
+ /** The string key bits-ui addresses an option by; enum values may be numbers. */
52
+ const selected = $derived(value === undefined || value === null ? '' : String(value));
53
+
54
+ const commitNumber = (raw: string) => {
55
+ if (raw.trim() === '') return onchange(undefined);
56
+ const parsed = Number(raw);
57
+ onchange(Number.isFinite(parsed) ? parsed : raw);
58
+ };
59
+
60
+ const commitEnum = (key: string) => {
61
+ const match = options.find((option) => String(option.value) === key);
62
+ onchange(match ? match.value : key);
63
+ };
64
+ </script>
65
+
66
+ {#if choice.widget === 'text' || choice.widget === 'password' || choice.widget === 'date' || choice.widget === 'time' || choice.widget === 'datetime'}
67
+ <Input
68
+ {id}
69
+ {name}
70
+ {disabled}
71
+ type={choice.widget === 'datetime'
72
+ ? 'datetime-local'
73
+ : choice.widget === 'text'
74
+ ? 'text'
75
+ : choice.widget}
76
+ value={text}
77
+ aria-describedby={describedBy}
78
+ oninput={(event) => onchange((event.currentTarget as HTMLInputElement).value)}
79
+ />
80
+ {:else if choice.widget === 'textarea'}
81
+ <Textarea
82
+ {id}
83
+ {name}
84
+ {disabled}
85
+ value={text}
86
+ aria-describedby={describedBy}
87
+ oninput={(event) => onchange((event.currentTarget as HTMLTextAreaElement).value)}
88
+ />
89
+ {:else if choice.widget === 'number'}
90
+ <Input
91
+ {id}
92
+ {name}
93
+ {disabled}
94
+ type="number"
95
+ min={bounds.minimum}
96
+ max={bounds.maximum}
97
+ step={bounds.multipleOf ?? (type === 'integer' ? 1 : undefined)}
98
+ value={text}
99
+ aria-describedby={describedBy}
100
+ oninput={(event) => commitNumber((event.currentTarget as HTMLInputElement).value)}
101
+ />
102
+ {:else if choice.widget === 'slider'}
103
+ <SliderField
104
+ {id}
105
+ {disabled}
106
+ {describedBy}
107
+ value={typeof value === 'number' ? value : undefined}
108
+ min={bounds.minimum ?? 0}
109
+ max={bounds.maximum ?? 100}
110
+ step={bounds.multipleOf ?? (type === 'integer' ? 1 : 0.01)}
111
+ onchange={(next) => onchange(next)}
112
+ />
113
+ {:else if choice.widget === 'select'}
114
+ <Select.Root type="single" value={selected} onValueChange={commitEnum} {disabled} {name}>
115
+ <Select.Trigger {id} class="w-full" aria-describedby={describedBy}>
116
+ {options.find((option) => String(option.value) === selected)?.label ?? 'Select…'}
117
+ </Select.Trigger>
118
+ <Select.Content>
119
+ {#each options as option (String(option.value))}
120
+ <Select.Item value={String(option.value)} label={option.label}>{option.label}</Select.Item>
121
+ {/each}
122
+ </Select.Content>
123
+ </Select.Root>
124
+ {:else if choice.widget === 'radio'}
125
+ <RadioField {id} {name} {value} {options} {disabled} {describedBy} onchange={(next) => onchange(next)} />
126
+ {:else if choice.widget === 'switch'}
127
+ <Switch
128
+ {id}
129
+ {name}
130
+ {disabled}
131
+ checked={value === true}
132
+ aria-describedby={describedBy}
133
+ onCheckedChange={(next: boolean) => onchange(next)}
134
+ />
135
+ {:else if choice.widget === 'checkbox'}
136
+ <Checkbox
137
+ {id}
138
+ {name}
139
+ {disabled}
140
+ checked={value === true}
141
+ aria-describedby={describedBy}
142
+ onCheckedChange={(next: boolean) => onchange(next === true)}
143
+ />
144
+ {:else if choice.widget === 'tags'}
145
+ <TagsField
146
+ {id}
147
+ {disabled}
148
+ {describedBy}
149
+ value={Array.isArray(value) ? value : undefined}
150
+ itemType={schemaType(items) ?? 'string'}
151
+ onchange={(next) => onchange(next)}
152
+ />
153
+ {:else}
154
+ <!-- `unknown`, and the unreachable case of a WidgetKind with no branch above.
155
+ Both land loudly: a widget kind this file forgot is exactly the class of
156
+ defect that made a field vanish in the renderers this one replaces. -->
157
+ <UnknownField
158
+ {id}
159
+ {label}
160
+ {scope}
161
+ {path}
162
+ {value}
163
+ {schema}
164
+ {disabled}
165
+ reason={choice.reason ?? 'unknown-widget'}
166
+ detail={choice.detail ?? `no branch renders the "${choice.widget}" widget`}
167
+ onchange={(next) => onchange(next)}
168
+ />
169
+ {/if}
@@ -0,0 +1,18 @@
1
+ import { type WidgetChoice } from './dispatch.js';
2
+ import type { JsonSchema } from './types.js';
3
+ type $$ComponentProps = {
4
+ choice: WidgetChoice;
5
+ schema: JsonSchema;
6
+ value: unknown;
7
+ id: string;
8
+ name: string;
9
+ label: string;
10
+ scope: string;
11
+ path: string;
12
+ disabled?: boolean;
13
+ describedBy?: string;
14
+ onchange: (next: unknown) => void;
15
+ };
16
+ declare const SchemaFormWidget: import("svelte").Component<$$ComponentProps, {}, "">;
17
+ type SchemaFormWidget = ReturnType<typeof SchemaFormWidget>;
18
+ export default SchemaFormWidget;