@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.
- package/README.md +180 -30
- package/dist/components/ui/app-shell/app-nav.svelte +12 -12
- package/dist/components/ui/app-shell/app-shell.svelte +14 -12
- package/dist/components/ui/app-shell/app-shell.svelte.d.ts +14 -6
- package/dist/components/ui/app-shell/types.d.ts +20 -3
- package/dist/components/ui/app-shell/types.js +29 -5
- package/dist/components/ui/collection-detail/collection-detail.svelte +169 -0
- package/dist/components/ui/collection-detail/collection-detail.svelte.d.ts +47 -0
- package/dist/components/ui/collection-detail/index.d.ts +3 -0
- package/dist/components/ui/collection-detail/index.js +2 -0
- package/dist/components/ui/document-detail/document-detail.svelte +184 -0
- package/dist/components/ui/document-detail/document-detail.svelte.d.ts +34 -0
- package/dist/components/ui/document-detail/index.d.ts +3 -0
- package/dist/components/ui/document-detail/index.js +2 -0
- package/dist/components/ui/library-browse/document-table.svelte +101 -0
- package/dist/components/ui/library-browse/document-table.svelte.d.ts +10 -0
- package/dist/components/ui/library-browse/facet-rail.svelte +62 -0
- package/dist/components/ui/library-browse/facet-rail.svelte.d.ts +8 -0
- package/dist/components/ui/library-browse/index.d.ts +3 -0
- package/dist/components/ui/library-browse/index.js +2 -0
- package/dist/components/ui/library-browse/library-browse.svelte +235 -0
- package/dist/components/ui/library-browse/library-browse.svelte.d.ts +44 -0
- package/dist/components/ui/library-browse/types.d.ts +101 -0
- package/dist/components/ui/library-browse/types.js +1 -0
- package/dist/components/ui/schema-form/context.d.ts +31 -0
- package/dist/components/ui/schema-form/context.js +10 -0
- package/dist/components/ui/schema-form/data.d.ts +42 -0
- package/dist/components/ui/schema-form/data.js +97 -0
- package/dist/components/ui/schema-form/dispatch.d.ts +35 -0
- package/dist/components/ui/schema-form/dispatch.js +139 -0
- package/dist/components/ui/schema-form/index.d.ts +5 -0
- package/dist/components/ui/schema-form/index.js +5 -0
- package/dist/components/ui/schema-form/schema-form-control.svelte +146 -0
- package/dist/components/ui/schema-form/schema-form-control.svelte.d.ts +7 -0
- package/dist/components/ui/schema-form/schema-form-element.svelte +105 -0
- package/dist/components/ui/schema-form/schema-form-element.svelte.d.ts +7 -0
- package/dist/components/ui/schema-form/schema-form-widget.svelte +169 -0
- package/dist/components/ui/schema-form/schema-form-widget.svelte.d.ts +18 -0
- package/dist/components/ui/schema-form/schema-form.svelte +153 -0
- package/dist/components/ui/schema-form/schema-form.svelte.d.ts +4 -0
- package/dist/components/ui/schema-form/types.d.ts +61 -0
- package/dist/components/ui/schema-form/types.js +21 -0
- package/dist/components/ui/schema-form/widgets/radio-field.svelte +48 -0
- package/dist/components/ui/schema-form/widgets/radio-field.svelte.d.ts +15 -0
- package/dist/components/ui/schema-form/widgets/slider-field.svelte +51 -0
- package/dist/components/ui/schema-form/widgets/slider-field.svelte.d.ts +13 -0
- package/dist/components/ui/schema-form/widgets/tags-field.svelte +98 -0
- package/dist/components/ui/schema-form/widgets/tags-field.svelte.d.ts +12 -0
- package/dist/components/ui/schema-form/widgets/unknown-field.svelte +123 -0
- package/dist/components/ui/schema-form/widgets/unknown-field.svelte.d.ts +20 -0
- package/dist/components/ui/search-results/index.d.ts +3 -0
- package/dist/components/ui/search-results/index.js +2 -0
- package/dist/components/ui/search-results/search-results.svelte +183 -0
- package/dist/components/ui/search-results/search-results.svelte.d.ts +40 -0
- package/package.json +2 -1
- package/registry/component-map.json +52 -3
- 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,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;
|