@poodle64/ui 2026.8.11 → 2026.8.13

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 (52) hide show
  1. package/README.md +164 -20
  2. package/dist/components/ui/collection-detail/collection-detail.svelte +169 -0
  3. package/dist/components/ui/collection-detail/collection-detail.svelte.d.ts +47 -0
  4. package/dist/components/ui/collection-detail/index.d.ts +3 -0
  5. package/dist/components/ui/collection-detail/index.js +2 -0
  6. package/dist/components/ui/document-detail/document-detail.svelte +184 -0
  7. package/dist/components/ui/document-detail/document-detail.svelte.d.ts +34 -0
  8. package/dist/components/ui/document-detail/index.d.ts +3 -0
  9. package/dist/components/ui/document-detail/index.js +2 -0
  10. package/dist/components/ui/library-browse/document-table.svelte +101 -0
  11. package/dist/components/ui/library-browse/document-table.svelte.d.ts +10 -0
  12. package/dist/components/ui/library-browse/facet-rail.svelte +62 -0
  13. package/dist/components/ui/library-browse/facet-rail.svelte.d.ts +8 -0
  14. package/dist/components/ui/library-browse/index.d.ts +3 -0
  15. package/dist/components/ui/library-browse/index.js +2 -0
  16. package/dist/components/ui/library-browse/library-browse.svelte +235 -0
  17. package/dist/components/ui/library-browse/library-browse.svelte.d.ts +44 -0
  18. package/dist/components/ui/library-browse/types.d.ts +101 -0
  19. package/dist/components/ui/library-browse/types.js +1 -0
  20. package/dist/components/ui/schema-form/context.d.ts +31 -0
  21. package/dist/components/ui/schema-form/context.js +10 -0
  22. package/dist/components/ui/schema-form/data.d.ts +42 -0
  23. package/dist/components/ui/schema-form/data.js +97 -0
  24. package/dist/components/ui/schema-form/dispatch.d.ts +35 -0
  25. package/dist/components/ui/schema-form/dispatch.js +139 -0
  26. package/dist/components/ui/schema-form/index.d.ts +5 -0
  27. package/dist/components/ui/schema-form/index.js +5 -0
  28. package/dist/components/ui/schema-form/schema-form-control.svelte +146 -0
  29. package/dist/components/ui/schema-form/schema-form-control.svelte.d.ts +7 -0
  30. package/dist/components/ui/schema-form/schema-form-element.svelte +105 -0
  31. package/dist/components/ui/schema-form/schema-form-element.svelte.d.ts +7 -0
  32. package/dist/components/ui/schema-form/schema-form-widget.svelte +169 -0
  33. package/dist/components/ui/schema-form/schema-form-widget.svelte.d.ts +18 -0
  34. package/dist/components/ui/schema-form/schema-form.svelte +153 -0
  35. package/dist/components/ui/schema-form/schema-form.svelte.d.ts +4 -0
  36. package/dist/components/ui/schema-form/types.d.ts +61 -0
  37. package/dist/components/ui/schema-form/types.js +21 -0
  38. package/dist/components/ui/schema-form/widgets/radio-field.svelte +48 -0
  39. package/dist/components/ui/schema-form/widgets/radio-field.svelte.d.ts +15 -0
  40. package/dist/components/ui/schema-form/widgets/slider-field.svelte +51 -0
  41. package/dist/components/ui/schema-form/widgets/slider-field.svelte.d.ts +13 -0
  42. package/dist/components/ui/schema-form/widgets/tags-field.svelte +98 -0
  43. package/dist/components/ui/schema-form/widgets/tags-field.svelte.d.ts +12 -0
  44. package/dist/components/ui/schema-form/widgets/unknown-field.svelte +123 -0
  45. package/dist/components/ui/schema-form/widgets/unknown-field.svelte.d.ts +20 -0
  46. package/dist/components/ui/search-results/index.d.ts +3 -0
  47. package/dist/components/ui/search-results/index.js +2 -0
  48. package/dist/components/ui/search-results/search-results.svelte +187 -0
  49. package/dist/components/ui/search-results/search-results.svelte.d.ts +44 -0
  50. package/package.json +2 -1
  51. package/registry/component-map.json +52 -3
  52. package/registry/component-map.md +20 -1
@@ -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;
@@ -0,0 +1,153 @@
1
+ <script lang="ts">
2
+ import { createAjv, Generate, type UISchemaElement } from '@jsonforms/core';
3
+ import Panel from '../panel/panel.svelte';
4
+ import SchemaFormElement from './schema-form-element.svelte';
5
+ import UnknownField from './widgets/unknown-field.svelte';
6
+ import { setSchemaFormContext } from './context.js';
7
+ import { addressedPaths, getAt, setAt, unmappedFields } from './data.js';
8
+ import { cn } from '../../../utils.js';
9
+ import type { SchemaFormProps } from './types.js';
10
+
11
+ /**
12
+ * Render a form the server described: a JSON Schema for what the value IS,
13
+ * and a JSON Forms UI Schema for how it is laid out.
14
+ *
15
+ * This component knows nothing about any application. It takes the two
16
+ * documents, the current value and a change handler; it performs no fetch,
17
+ * holds no endpoint, and names no service. That is what lets the engine
18
+ * behind the schema be swapped without touching a consumer — and what lets
19
+ * three apps that each hand-rolled this converge on one renderer.
20
+ *
21
+ * `@jsonforms/core` is used HEADLESS — for JSON-Pointer scope resolution,
22
+ * rule evaluation and Ajv only. The renderers are this package's own and
23
+ * dispatch to this package's own widgets; there is no official Svelte
24
+ * renderer for JSON Forms and the community one carries no external
25
+ * validation.
26
+ *
27
+ * THE ONE NON-NEGOTIABLE RULE: nothing renders silently as nothing. An
28
+ * unknown widget hint, an unknown element type, a scope that resolves to
29
+ * nothing, or a schema property the layout never mentions each renders as a
30
+ * flagged fallback carrying `data-schema-form-unknown`. The renderers this
31
+ * replaces failed all four ways in silence, and shipped that way for months.
32
+ */
33
+ let {
34
+ schema,
35
+ uischema,
36
+ value,
37
+ onChange,
38
+ disabled = false,
39
+ idPrefix = 'sf',
40
+ class: className
41
+ }: SchemaFormProps = $props();
42
+
43
+ // One Ajv for the life of the form: rule conditions and validation both use
44
+ // it, and compiling per keystroke would be the whole cost of the component.
45
+ const ajv = createAjv();
46
+
47
+ // No UI schema is a legitimate mode, not an error: generating one from the
48
+ // schema is the only arrangement in which a field CANNOT be missing from the
49
+ // layout, so it is the safe default rather than a blank form.
50
+ const layout = $derived(uischema ?? (Generate.uiSchema(schema) as UISchemaElement));
51
+
52
+ const addressed = $derived(addressedPaths(layout));
53
+ const unmapped = $derived(unmappedFields(schema, addressed));
54
+
55
+ /**
56
+ * Validation messages by dot path. Ajv is asked about a CLONE, because
57
+ * `createAjv` enables `useDefaults` and would otherwise write defaults into
58
+ * the caller's value behind its back.
59
+ */
60
+ const errors = $derived.by(() => {
61
+ const byPath: Record<string, string[]> = {};
62
+ try {
63
+ const validate = ajv.compile(schema as object);
64
+ const probe = structuredClone($state.snapshot(value));
65
+ if (validate(probe)) return byPath;
66
+ for (const error of validate.errors ?? []) {
67
+ const missing = (error.params as { missingProperty?: string })?.missingProperty;
68
+ const instance = error.instancePath.replace(/^\//, '').split('/').filter(Boolean).join('.');
69
+ const path = missing ? [instance, missing].filter(Boolean).join('.') : instance;
70
+ if (!path) continue;
71
+ (byPath[path] ??= []).push(missing ? 'Required' : (error.message ?? 'is invalid'));
72
+ }
73
+ } catch {
74
+ // A schema Ajv will not compile, or a value it will not clone, is still
75
+ // a form this renderer can lay out — it simply cannot be validated.
76
+ // Never let a validation problem blank the form: that is the failure
77
+ // mode this whole component exists to remove.
78
+ return byPath;
79
+ }
80
+ return byPath;
81
+ });
82
+
83
+ setSchemaFormContext({
84
+ get schema() {
85
+ return schema;
86
+ },
87
+ get data() {
88
+ return value;
89
+ },
90
+ get disabled() {
91
+ return disabled;
92
+ },
93
+ get idPrefix() {
94
+ return idPrefix;
95
+ },
96
+ get errors() {
97
+ return errors;
98
+ },
99
+ ajv,
100
+ change(path, next) {
101
+ onChange(setAt($state.snapshot(value) as Record<string, unknown>, path, next), {
102
+ path,
103
+ value: next
104
+ });
105
+ }
106
+ });
107
+ </script>
108
+
109
+ <div class={cn('grid gap-4', className)} data-schema-form="">
110
+ <SchemaFormElement element={layout} />
111
+
112
+ {#if unmapped.length}
113
+ <!--
114
+ The defect this section exists for: in the renderer being retired, three
115
+ whole top-level config groups never rendered because their names were
116
+ absent from a hardcoded order. Nothing failed, nothing warned — the
117
+ groups were simply not there. A field the layout does not mention is now
118
+ the loudest thing on the page instead of the quietest.
119
+ -->
120
+ <Panel
121
+ title="Not in the layout"
122
+ subtitle="{unmapped.length} field{unmapped.length === 1 ? '' : 's'} no Control addresses"
123
+ data-schema-form-unmapped=""
124
+ >
125
+ <p class="text-muted-foreground mb-3 text-xs">
126
+ The JSON Schema describes {unmapped.length === 1 ? 'this field' : 'these fields'} but the UI
127
+ schema never {unmapped.length === 1 ? 'mentions it' : 'mentions them'}. Add a Control for
128
+ each; until then {unmapped.length === 1 ? 'it is' : 'they are'} edited raw here rather than
129
+ disappearing.
130
+ </p>
131
+ <div class="grid gap-3">
132
+ {#each unmapped as field (field.path)}
133
+ <UnknownField
134
+ id="{idPrefix}-unmapped-{field.path.replace(/[^a-zA-Z0-9_-]/g, '-')}"
135
+ label={(field.schema as { title?: string }).title ?? field.path}
136
+ scope={field.scope}
137
+ path={field.path}
138
+ schema={field.schema}
139
+ reason={field.reason}
140
+ {disabled}
141
+ detail="no Control in the UI schema addresses this property"
142
+ value={getAt(value, field.path)}
143
+ onchange={(next) =>
144
+ onChange(
145
+ setAt($state.snapshot(value) as Record<string, unknown>, field.path, next),
146
+ { path: field.path, value: next }
147
+ )}
148
+ />
149
+ {/each}
150
+ </div>
151
+ </Panel>
152
+ {/if}
153
+ </div>
@@ -0,0 +1,4 @@
1
+ import type { SchemaFormProps } from './types.js';
2
+ declare const SchemaForm: import("svelte").Component<SchemaFormProps, {}, "">;
3
+ type SchemaForm = ReturnType<typeof SchemaForm>;
4
+ export default SchemaForm;
@@ -0,0 +1,61 @@
1
+ import type { JsonSchema, UISchemaElement } from '@jsonforms/core';
2
+ export type { JsonSchema, UISchemaElement };
3
+ /**
4
+ * The widgets `<SchemaForm>` can dispatch a Control to. This list IS the
5
+ * contract: a hint naming anything outside it resolves to `'unknown'` and
6
+ * renders loudly, it does not fall back to a default control and it never
7
+ * renders nothing.
8
+ */
9
+ export declare const WIDGET_KINDS: readonly ["text", "textarea", "password", "number", "slider", "date", "time", "datetime", "select", "radio", "switch", "checkbox", "tags"];
10
+ export type WidgetKind = (typeof WIDGET_KINDS)[number] | 'unknown';
11
+ /**
12
+ * Why something could not be rendered as a real control. Every value is
13
+ * surfaced in the DOM as `data-unknown-reason`, so a consuming app's own gate
14
+ * can assert on it, and so a screenshot of a broken form says WHY it is broken.
15
+ */
16
+ export type UnknownReason =
17
+ /** `options.format`/`options.widget` named a widget this package does not ship. */
18
+ 'unknown-widget'
19
+ /** The UI schema element's `type` is not in the JSON Forms vocabulary this renderer walks. */
20
+ | 'unknown-element'
21
+ /** The Control's `scope` pointer resolves to nothing in the JSON Schema. */
22
+ | 'unresolved-scope'
23
+ /** The Control has no `scope` at all. */
24
+ | 'missing-scope'
25
+ /** The resolved subschema has a `type` no widget covers. */
26
+ | 'unsupported-type'
27
+ /** A Control addressing an object — an object needs a layout, not a control. */
28
+ | 'object-control'
29
+ /** A hint asked for a closed value set, but the subschema declares none. */
30
+ | 'no-options'
31
+ /** An array whose items are not primitives; `tags` cannot represent it. */
32
+ | 'unsupported-array'
33
+ /** The JSON Schema describes this field but the UI schema never mentions it. */
34
+ | 'not-in-layout';
35
+ /** The change `<SchemaForm>` emits alongside the next whole value. */
36
+ export interface SchemaFormChange {
37
+ /** Dot path of the field that changed, e.g. `tuning.depth`. */
38
+ path: string;
39
+ /** The field's new value. */
40
+ value: unknown;
41
+ }
42
+ export interface SchemaFormProps {
43
+ /** The JSON Schema describing the object being edited. */
44
+ schema: JsonSchema;
45
+ /**
46
+ * The JSON Forms UI Schema describing the layout. Omit it and one is
47
+ * generated from the schema, which is the only mode in which no field can
48
+ * be missing from the layout.
49
+ */
50
+ uischema?: UISchemaElement;
51
+ /** The current value. `<SchemaForm>` never mutates it. */
52
+ value: Record<string, unknown>;
53
+ /** Called with the next whole value and the single field that changed. */
54
+ onChange: (next: Record<string, unknown>, change: SchemaFormChange) => void;
55
+ /** Disable every control in the form. */
56
+ disabled?: boolean;
57
+ /** Prefix for generated element ids, when two forms share a page. */
58
+ idPrefix?: string;
59
+ /** Extra classes on the form's root element. */
60
+ class?: string;
61
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * The widgets `<SchemaForm>` can dispatch a Control to. This list IS the
3
+ * contract: a hint naming anything outside it resolves to `'unknown'` and
4
+ * renders loudly, it does not fall back to a default control and it never
5
+ * renders nothing.
6
+ */
7
+ export const WIDGET_KINDS = [
8
+ 'text',
9
+ 'textarea',
10
+ 'password',
11
+ 'number',
12
+ 'slider',
13
+ 'date',
14
+ 'time',
15
+ 'datetime',
16
+ 'select',
17
+ 'radio',
18
+ 'switch',
19
+ 'checkbox',
20
+ 'tags'
21
+ ];
@@ -0,0 +1,48 @@
1
+ <script lang="ts">
2
+ import Label from '../../label/label.svelte';
3
+
4
+ /**
5
+ * A closed value set as a visible row of choices, for the case where a select
6
+ * hides the options a user needs to compare. Composed here for the same
7
+ * reason as the slider: the package ships no radio group, and its only caller
8
+ * is an enum Control that asked for `options.format: "radio"`.
9
+ *
10
+ * Native inputs, one `name` per field, so arrow-key roving and the radiogroup
11
+ * role come from the platform rather than from markup that has to be right.
12
+ */
13
+ let {
14
+ id,
15
+ name,
16
+ value,
17
+ options,
18
+ disabled = false,
19
+ describedBy,
20
+ onchange
21
+ }: {
22
+ id: string;
23
+ name: string;
24
+ value: unknown;
25
+ options: { value: unknown; label: string }[];
26
+ disabled?: boolean;
27
+ describedBy?: string;
28
+ onchange: (next: unknown) => void;
29
+ } = $props();
30
+ </script>
31
+
32
+ <div {id} role="radiogroup" aria-labelledby="{id}-label" aria-describedby={describedBy} class="flex flex-wrap gap-x-4 gap-y-2">
33
+ {#each options as option, index (String(option.value))}
34
+ <div class="flex items-center gap-1.5">
35
+ <input
36
+ id="{id}-{index}"
37
+ {name}
38
+ {disabled}
39
+ type="radio"
40
+ value={String(option.value)}
41
+ checked={option.value === value}
42
+ class="accent-primary focus-visible:ring-ring/50 size-4 outline-none focus-visible:ring-3 disabled:cursor-not-allowed disabled:opacity-50"
43
+ onchange={() => onchange(option.value)}
44
+ />
45
+ <Label for="{id}-{index}" class="font-normal">{option.label}</Label>
46
+ </div>
47
+ {/each}
48
+ </div>
@@ -0,0 +1,15 @@
1
+ type $$ComponentProps = {
2
+ id: string;
3
+ name: string;
4
+ value: unknown;
5
+ options: {
6
+ value: unknown;
7
+ label: string;
8
+ }[];
9
+ disabled?: boolean;
10
+ describedBy?: string;
11
+ onchange: (next: unknown) => void;
12
+ };
13
+ declare const RadioField: import("svelte").Component<$$ComponentProps, {}, "">;
14
+ type RadioField = ReturnType<typeof RadioField>;
15
+ export default RadioField;
@@ -0,0 +1,51 @@
1
+ <script lang="ts">
2
+ /**
3
+ * A bounded numeric field as a track plus a live readout.
4
+ *
5
+ * Composed here rather than imported: the package ships no slider, and the
6
+ * only caller is a Control whose subschema carries `minimum`/`maximum`. It is
7
+ * a native `<input type="range">` so the keyboard, ARIA and touch behaviour
8
+ * are the platform's; the only styling is the accent utility, which resolves
9
+ * through the palette, so an app that changes `--ds-color-primary` changes
10
+ * the track with it.
11
+ */
12
+ let {
13
+ id,
14
+ value,
15
+ min,
16
+ max,
17
+ step = 1,
18
+ disabled = false,
19
+ describedBy,
20
+ onchange
21
+ }: {
22
+ id: string;
23
+ value: number | undefined;
24
+ min: number;
25
+ max: number;
26
+ step?: number;
27
+ disabled?: boolean;
28
+ describedBy?: string;
29
+ onchange: (next: number) => void;
30
+ } = $props();
31
+
32
+ const current = $derived(typeof value === 'number' && Number.isFinite(value) ? value : min);
33
+ </script>
34
+
35
+ <div class="flex items-center gap-3">
36
+ <input
37
+ {id}
38
+ {min}
39
+ {max}
40
+ {step}
41
+ {disabled}
42
+ type="range"
43
+ value={current}
44
+ aria-describedby={describedBy}
45
+ class="accent-primary focus-visible:ring-ring/50 h-8 w-full cursor-pointer rounded-lg outline-none focus-visible:ring-3 disabled:cursor-not-allowed disabled:opacity-50"
46
+ oninput={(event) => onchange(Number((event.currentTarget as HTMLInputElement).value))}
47
+ />
48
+ <output for={id} class="ds-tabular text-muted-foreground w-10 shrink-0 text-right text-xs"
49
+ >{current}</output
50
+ >
51
+ </div>
@@ -0,0 +1,13 @@
1
+ type $$ComponentProps = {
2
+ id: string;
3
+ value: number | undefined;
4
+ min: number;
5
+ max: number;
6
+ step?: number;
7
+ disabled?: boolean;
8
+ describedBy?: string;
9
+ onchange: (next: number) => void;
10
+ };
11
+ declare const SliderField: import("svelte").Component<$$ComponentProps, {}, "">;
12
+ type SliderField = ReturnType<typeof SliderField>;
13
+ export default SliderField;
@@ -0,0 +1,98 @@
1
+ <script lang="ts">
2
+ import X from '@lucide/svelte/icons/x';
3
+ import Badge from '../../badge/badge.svelte';
4
+ import Input from '../../input/input.svelte';
5
+
6
+ /**
7
+ * An array of primitives as removable chips plus one entry field.
8
+ *
9
+ * Composed here because the package ships no tags input and an array of
10
+ * strings is the commonest non-scalar in a server-described config (allowed
11
+ * hosts, enabled features, ignore globs). Enter or comma commits; Backspace
12
+ * on an empty field removes the last chip, which is the behaviour every tags
13
+ * input has and the one users try first.
14
+ *
15
+ * Item coercion follows the subschema: a `number`/`integer` item schema means
16
+ * the array stays numeric rather than quietly becoming strings.
17
+ */
18
+ let {
19
+ id,
20
+ value,
21
+ itemType = 'string',
22
+ disabled = false,
23
+ placeholder = 'Add and press Enter',
24
+ describedBy,
25
+ onchange
26
+ }: {
27
+ id: string;
28
+ value: unknown[] | undefined;
29
+ itemType?: string;
30
+ disabled?: boolean;
31
+ placeholder?: string;
32
+ describedBy?: string;
33
+ onchange: (next: unknown[]) => void;
34
+ } = $props();
35
+
36
+ let draft = $state('');
37
+
38
+ const items = $derived(Array.isArray(value) ? value : []);
39
+
40
+ const coerce = (text: string): unknown => {
41
+ if (itemType !== 'number' && itemType !== 'integer') return text;
42
+ const parsed = Number(text);
43
+ return Number.isFinite(parsed) ? parsed : text;
44
+ };
45
+
46
+ function add() {
47
+ const text = draft.trim();
48
+ if (!text) return;
49
+ const next = coerce(text);
50
+ if (!items.some((item) => item === next)) onchange([...items, next]);
51
+ draft = '';
52
+ }
53
+
54
+ function removeAt(index: number) {
55
+ onchange(items.filter((_, position) => position !== index));
56
+ }
57
+
58
+ function onkeydown(event: KeyboardEvent) {
59
+ if (event.key === 'Enter' || event.key === ',') {
60
+ event.preventDefault();
61
+ add();
62
+ return;
63
+ }
64
+ if (event.key === 'Backspace' && draft === '' && items.length) removeAt(items.length - 1);
65
+ }
66
+ </script>
67
+
68
+ <div class="grid gap-1.5">
69
+ {#if items.length}
70
+ <ul class="flex flex-wrap gap-1.5" aria-label="Current values">
71
+ {#each items as item, index (`${String(item)}-${index}`)}
72
+ <li>
73
+ <Badge variant="secondary" class="h-6 gap-1 pr-1">
74
+ <span class="font-mono">{String(item)}</span>
75
+ <button
76
+ type="button"
77
+ {disabled}
78
+ aria-label="Remove {String(item)}"
79
+ class="hover:bg-muted hover:text-foreground text-muted-foreground focus-visible:ring-ring/50 flex size-5 items-center justify-center rounded-full outline-none focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50"
80
+ onclick={() => removeAt(index)}
81
+ >
82
+ <X class="size-3" aria-hidden="true" />
83
+ </button>
84
+ </Badge>
85
+ </li>
86
+ {/each}
87
+ </ul>
88
+ {/if}
89
+ <Input
90
+ {id}
91
+ {disabled}
92
+ {placeholder}
93
+ bind:value={draft}
94
+ aria-describedby={describedBy}
95
+ {onkeydown}
96
+ onblur={add}
97
+ />
98
+ </div>
@@ -0,0 +1,12 @@
1
+ type $$ComponentProps = {
2
+ id: string;
3
+ value: unknown[] | undefined;
4
+ itemType?: string;
5
+ disabled?: boolean;
6
+ placeholder?: string;
7
+ describedBy?: string;
8
+ onchange: (next: unknown[]) => void;
9
+ };
10
+ declare const TagsField: import("svelte").Component<$$ComponentProps, {}, "">;
11
+ type TagsField = ReturnType<typeof TagsField>;
12
+ export default TagsField;