@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,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;
@@ -0,0 +1,123 @@
1
+ <script lang="ts">
2
+ import TriangleAlert from '@lucide/svelte/icons/triangle-alert';
3
+ import StatusBadge from '../../status-badge/status-badge.svelte';
4
+ import Input from '../../input/input.svelte';
5
+ import Label from '../../label/label.svelte';
6
+ import type { JsonSchema, UnknownReason } from '../types.js';
7
+ import { schemaType } from '../dispatch.js';
8
+
9
+ /**
10
+ * THE LOUD FALLBACK — the reason this package exists rather than a fourth
11
+ * hand-rolled renderer.
12
+ *
13
+ * The estate's outgoing renderers failed silently in three ways: an
14
+ * unrecognised `widget` hint rendered no input, a config group missing from a
15
+ * hardcoded order rendered nothing at all, and a nested hint was simply
16
+ * inert. Every one of those made the field VANISH, and a form with a missing
17
+ * field looks exactly like a form. Nobody noticed for months.
18
+ *
19
+ * So there is no silent branch anywhere in `<SchemaForm>`: everything the
20
+ * renderer cannot dispatch lands here, and here always renders. It says what
21
+ * it could not do, names the pointer so the fix is a copy-paste, shows the
22
+ * value that would otherwise have been lost, and keeps it editable whenever
23
+ * editing a raw value is safe — which is when the value is a primitive. An
24
+ * object or an array is shown, not edited: a text box over structured data
25
+ * is a data-loss affordance, not a fallback.
26
+ */
27
+ let {
28
+ label,
29
+ scope,
30
+ path,
31
+ reason,
32
+ detail,
33
+ value,
34
+ schema,
35
+ disabled = false,
36
+ id,
37
+ onchange
38
+ }: {
39
+ label: string;
40
+ /** JSON Pointer this control addressed, or the property's own pointer. */
41
+ scope: string;
42
+ /** Dot path into the value. */
43
+ path: string;
44
+ reason: UnknownReason;
45
+ /** What specifically could not be done, in one sentence. */
46
+ detail: string;
47
+ value: unknown;
48
+ schema?: JsonSchema;
49
+ disabled?: boolean;
50
+ id: string;
51
+ /** Emitted when the raw editor commits a primitive. */
52
+ onchange?: (next: unknown) => void;
53
+ } = $props();
54
+
55
+ // Editing is offered only where a text box can round-trip the value without
56
+ // destroying structure: a primitive, or an absent value under a primitive
57
+ // schema. Anything structured is shown read-only.
58
+ const declared = $derived(schemaType(schema));
59
+ const structural = $derived(
60
+ (value !== null && typeof value === 'object') || declared === 'object' || declared === 'array'
61
+ );
62
+ const editable = $derived(!structural);
63
+
64
+ const raw = $derived(
65
+ value === undefined || value === null
66
+ ? ''
67
+ : typeof value === 'object'
68
+ ? JSON.stringify(value, null, 2)
69
+ : String(value)
70
+ );
71
+
72
+ /** Coerce the typed text back to the schema's declared type where it can. */
73
+ function commit(text: string) {
74
+ if (declared === 'number' || declared === 'integer') {
75
+ const parsed = text.trim() === '' ? undefined : Number(text);
76
+ onchange?.(parsed !== undefined && Number.isFinite(parsed) ? parsed : text);
77
+ return;
78
+ }
79
+ if (declared === 'boolean') {
80
+ if (text === 'true' || text === 'false') return onchange?.(text === 'true');
81
+ }
82
+ onchange?.(text);
83
+ }
84
+ </script>
85
+
86
+ <div
87
+ role="group"
88
+ aria-label="Unrecognised field: {label}"
89
+ data-schema-form-unknown=""
90
+ data-unknown-reason={reason}
91
+ data-path={path}
92
+ class="border-status-warning/50 bg-status-warning/5 grid gap-2 rounded-lg border border-dashed p-3"
93
+ >
94
+ <div class="flex flex-wrap items-center gap-2">
95
+ <TriangleAlert class="text-status-warning size-4 shrink-0" aria-hidden="true" />
96
+ <span class="text-body font-semibold">{label}</span>
97
+ <StatusBadge status="warning" label="Unrecognised control" />
98
+ </div>
99
+
100
+ <p class="text-muted-foreground text-xs">
101
+ {detail}. Rendered raw and unstyled so the value is not lost, at
102
+ <code class="font-mono">{scope}</code>
103
+ </p>
104
+
105
+ {#if editable}
106
+ <Label for={id} class="text-2xs text-muted-foreground uppercase">Raw value</Label>
107
+ <Input
108
+ {id}
109
+ {disabled}
110
+ value={raw}
111
+ class="font-mono"
112
+ oninput={(event) => commit((event.currentTarget as HTMLInputElement).value)}
113
+ />
114
+ {:else}
115
+ <pre
116
+ data-schema-form-unknown-readonly=""
117
+ class="bg-surface-1 border-border text-muted-foreground max-h-48 overflow-auto rounded-md border p-2 font-mono text-xs">{raw ||
118
+ '(no value)'}</pre>
119
+ <p class="text-muted-foreground text-xs">
120
+ Structured value: shown, not edited, so nothing is silently rewritten.
121
+ </p>
122
+ {/if}
123
+ </div>
@@ -0,0 +1,20 @@
1
+ import type { JsonSchema, UnknownReason } from '../types.js';
2
+ type $$ComponentProps = {
3
+ label: string;
4
+ /** JSON Pointer this control addressed, or the property's own pointer. */
5
+ scope: string;
6
+ /** Dot path into the value. */
7
+ path: string;
8
+ reason: UnknownReason;
9
+ /** What specifically could not be done, in one sentence. */
10
+ detail: string;
11
+ value: unknown;
12
+ schema?: JsonSchema;
13
+ disabled?: boolean;
14
+ id: string;
15
+ /** Emitted when the raw editor commits a primitive. */
16
+ onchange?: (next: unknown) => void;
17
+ };
18
+ declare const UnknownField: import("svelte").Component<$$ComponentProps, {}, "">;
19
+ type UnknownField = ReturnType<typeof UnknownField>;
20
+ export default UnknownField;
@@ -0,0 +1,3 @@
1
+ export { default as SearchResults } from './search-results.svelte';
2
+ export { default } from './search-results.svelte';
3
+ export type { LibraryBadge, LibrarySearchResult, SearchSnippetSegment } from '../library-browse/types.js';
@@ -0,0 +1,2 @@
1
+ export { default as SearchResults } from './search-results.svelte';
2
+ export { default } from './search-results.svelte';