@quillmark/svelte 0.11.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/address.d.ts +17 -10
- package/dist/core/address.js +26 -12
- package/dist/core/bloom.js +4 -3
- package/dist/core/codec/decode.d.ts +17 -0
- package/dist/core/codec/decode.js +78 -19
- package/dist/core/codec/encode.js +1 -1
- package/dist/core/codec/field.d.ts +64 -15
- package/dist/core/codec/field.js +201 -65
- package/dist/core/codec/index.d.ts +3 -3
- package/dist/core/codec/index.js +3 -3
- package/dist/core/codec/islands.d.ts +5 -4
- package/dist/core/codec/islands.js +23 -38
- package/dist/core/codec/keymap.d.ts +5 -4
- package/dist/core/codec/keymap.js +5 -4
- package/dist/core/codec/prose.css +42 -0
- package/dist/core/codec/schema.d.ts +24 -5
- package/dist/core/codec/schema.js +40 -9
- package/dist/core/codec/slash.js +0 -0
- package/dist/core/errors.d.ts +3 -2
- package/dist/core/rungs.d.ts +8 -0
- package/dist/core/rungs.js +24 -0
- package/dist/core/theme.css +19 -6
- package/dist/preview/paint.js +21 -17
- package/dist/visual/ArrayField.svelte +649 -176
- package/dist/visual/ArrayField.svelte.d.ts +16 -1
- package/dist/visual/BooleanField.svelte +36 -11
- package/dist/visual/BooleanField.svelte.d.ts +2 -0
- package/dist/visual/Card.svelte +6 -25
- package/dist/visual/DateField.svelte +66 -31
- package/dist/visual/EnumField.svelte +27 -9
- package/dist/visual/EnumField.svelte.d.ts +5 -0
- package/dist/visual/Field.svelte +113 -38
- package/dist/visual/FormatPopover.svelte +4 -3
- package/dist/visual/MatrixField.svelte +353 -0
- package/dist/visual/MatrixField.svelte.d.ts +36 -0
- package/dist/visual/NumberField.svelte +31 -14
- package/dist/visual/NumberField.svelte.d.ts +5 -1
- package/dist/visual/ObjectField.svelte +404 -139
- package/dist/visual/ObjectField.svelte.d.ts +17 -2
- package/dist/visual/ProseField.svelte +53 -23
- package/dist/visual/ProseField.svelte.d.ts +15 -3
- package/dist/visual/ProseValue.svelte +102 -46
- package/dist/visual/ProseValue.svelte.d.ts +17 -5
- package/dist/visual/TextField.svelte +35 -18
- package/dist/visual/TextField.svelte.d.ts +7 -0
- package/dist/visual/VariantField.svelte +56 -2
- package/dist/visual/VariantField.svelte.d.ts +10 -1
- package/dist/visual/VisualEditorInner.svelte +77 -73
- package/dist/visual/controls.css +47 -5
- package/dist/visual/diagnostics.d.ts +26 -2
- package/dist/visual/diagnostics.js +52 -5
- package/dist/visual/hold.js +2 -2
- package/dist/visual/icons/nodes.d.ts +3 -0
- package/dist/visual/icons/nodes.js +2 -1
- package/dist/visual/leaves.d.ts +25 -12
- package/dist/visual/motion.d.ts +14 -5
- package/dist/visual/motion.js +13 -11
- package/dist/visual/strings.d.ts +27 -10
- package/dist/visual/strings.js +8 -2
- package/dist/visual/structure.d.ts +159 -40
- package/dist/visual/structure.js +337 -45
- package/package.json +2 -2
|
@@ -1,32 +1,55 @@
|
|
|
1
1
|
<!--
|
|
2
2
|
An `object` field → a nested subform over `properties` (declaration order),
|
|
3
|
-
committing the whole object by value on any nested change.
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
committing the whole object by value on any nested change. A scalar property draws
|
|
4
|
+
its own control, a content property mounts the by-value prose leaf ({@link
|
|
5
|
+
ProseValue}) over the boundary's nested read, and a container property — an `array`,
|
|
6
|
+
an `object`, a `matrix`, a variant — mounts its own control here, at the next rung of
|
|
7
|
+
the same figure. Depth is bounded by what is open, not by the schema: a repeater inside
|
|
8
|
+
a subform is collapsed rows again, one open at a time, so an open row under an open
|
|
9
|
+
row is one figure two rungs in.
|
|
7
10
|
|
|
8
11
|
The nesting is a vertical at `--_qm-border`, the ladder's last stroke: the card's edge,
|
|
9
12
|
an open section's vertical one `--_qm-nest` in where the field is in one, this one a rung
|
|
10
13
|
further, the properties a rung inside it (ARCHITECTURE §"A plane is a tone").
|
|
11
14
|
|
|
12
|
-
One figure wherever it mounts
|
|
13
|
-
|
|
14
|
-
at
|
|
15
|
+
One figure wherever it mounts, and where its stroke lands is the stacker's. A
|
|
16
|
+
variant's cells and a matrix member's columns sit at the depth a field-level subform
|
|
17
|
+
sits at: each stands beside the cell it is stored with — the discriminant, the tick —
|
|
18
|
+
rather than inside it, so the box says which world or which member and not how deep.
|
|
19
|
+
An array element's properties hang a rung further in ({@link ArrayField}), the summary
|
|
20
|
+
above them standing for the whole row.
|
|
15
21
|
|
|
16
|
-
A property's
|
|
17
|
-
|
|
18
|
-
carries no per-property row (an object field resolves
|
|
19
|
-
the whole object).
|
|
22
|
+
A property's `default:`, which an unset cell holds until an edit takes it, is the
|
|
23
|
+
static schema `sub.default`, not the resolved provenance a field reads
|
|
24
|
+
(FIELD_PROVENANCE): `resolve` carries no per-property row (an object field resolves
|
|
25
|
+
as one row whose value is the whole object).
|
|
20
26
|
|
|
21
27
|
Every caller hands down `contentAt`, which is `reader.getContentAt` with the field
|
|
22
28
|
already bound: a field-level subform passes the property key through untouched, a
|
|
23
|
-
variant passes its cell's,
|
|
24
|
-
reads at whatever depth it sits, which nothing here knows.
|
|
29
|
+
variant passes its cell's, an array's open row prefixes its index and a matrix its
|
|
30
|
+
member's id. So a cell reads at whatever depth it sits, which nothing here knows.
|
|
31
|
+
|
|
32
|
+
`bare` is the table's row: the same cells with no labels and no figure of their own,
|
|
33
|
+
laid onto the tracks the table's header names ({@link ArrayField}).
|
|
25
34
|
-->
|
|
26
35
|
<script lang="ts">
|
|
27
36
|
import type { Content, PathStep, QuillFieldSchema } from '@quillmark/wasm';
|
|
28
|
-
import {
|
|
29
|
-
|
|
37
|
+
import {
|
|
38
|
+
arrayLayout,
|
|
39
|
+
baseType,
|
|
40
|
+
controlKind,
|
|
41
|
+
declaredContent,
|
|
42
|
+
exampleGhost,
|
|
43
|
+
humanize,
|
|
44
|
+
isContainer,
|
|
45
|
+
mountKey,
|
|
46
|
+
obliged,
|
|
47
|
+
optionalCell,
|
|
48
|
+
printedText,
|
|
49
|
+
shortCell
|
|
50
|
+
} from './structure.js';
|
|
51
|
+
import { splitDeep, unrouted, type DeepDiagnostic } from './diagnostics.js';
|
|
52
|
+
import type { LandingBox } from './leaves.js';
|
|
30
53
|
import { wording } from './strings.js';
|
|
31
54
|
import { propertyDomIds } from './domid.js';
|
|
32
55
|
import FieldLabel from './FieldLabel.svelte';
|
|
@@ -36,6 +59,12 @@
|
|
|
36
59
|
import BooleanField from './BooleanField.svelte';
|
|
37
60
|
import DateField from './DateField.svelte';
|
|
38
61
|
import ProseValue from './ProseValue.svelte';
|
|
62
|
+
import ArrayField from './ArrayField.svelte';
|
|
63
|
+
import MatrixField from './MatrixField.svelte';
|
|
64
|
+
import VariantField from './VariantField.svelte';
|
|
65
|
+
import DiagnosticList from './DiagnosticList.svelte';
|
|
66
|
+
import ObjectField from './ObjectField.svelte';
|
|
67
|
+
import './controls.css';
|
|
39
68
|
|
|
40
69
|
interface Props {
|
|
41
70
|
value: Record<string, unknown> | undefined;
|
|
@@ -49,8 +78,8 @@
|
|
|
49
78
|
/**
|
|
50
79
|
* The parent control's DOM id: the base each property's own three names derive
|
|
51
80
|
* from ({@link propertyDomIds}). Absent — a subform mounted with no field around
|
|
52
|
-
* it — the properties fall back to `aria-label`, which names
|
|
53
|
-
* `for` target to click.
|
|
81
|
+
* it, or a table's row — the properties fall back to `aria-label`, which names
|
|
82
|
+
* them without a `for` target to click.
|
|
54
83
|
*/
|
|
55
84
|
idBase?: string;
|
|
56
85
|
/** Accessible-name prefix used only on the `aria-label` fallback above. */
|
|
@@ -62,13 +91,37 @@
|
|
|
62
91
|
* `undefined` for a property the stored value does not reach. */
|
|
63
92
|
contentAt: (path: PathStep[]) => Content | undefined;
|
|
64
93
|
onCommit: (obj: Record<string, unknown>) => void;
|
|
94
|
+
/** A table's row: the cells alone, on the tracks the table's header names, with
|
|
95
|
+
* no labels and no vertical of their own. */
|
|
96
|
+
bare?: boolean;
|
|
97
|
+
/** Raw keydown off a text or prose cell, for a row whose own keys run through its
|
|
98
|
+
* cells: the table's Enter and Backspace ({@link ArrayField}). */
|
|
99
|
+
onCellKey?: (e: KeyboardEvent, key: string) => void;
|
|
100
|
+
/** The diagnostics routed into this subform, each with its steps still to walk:
|
|
101
|
+
* one naming a property draws under that property's control, and one naming
|
|
102
|
+
* nothing this subform holds draws at its foot (`diagnostics.ts`). */
|
|
103
|
+
diagnostics?: readonly DeepDiagnostic[];
|
|
65
104
|
}
|
|
66
|
-
let {
|
|
67
|
-
|
|
105
|
+
let {
|
|
106
|
+
value,
|
|
107
|
+
properties,
|
|
108
|
+
labelledBy,
|
|
109
|
+
describedBy,
|
|
110
|
+
idBase,
|
|
111
|
+
label,
|
|
112
|
+
contentAt,
|
|
113
|
+
onCommit,
|
|
114
|
+
bare = false,
|
|
115
|
+
onCellKey,
|
|
116
|
+
diagnostics
|
|
117
|
+
}: Props = $props();
|
|
68
118
|
|
|
69
119
|
const t = wording();
|
|
70
120
|
const entries = $derived(Object.entries(properties ?? {}));
|
|
71
121
|
const obj = $derived((value ?? {}) as Record<string, unknown>);
|
|
122
|
+
const has = (key: string): boolean => Object.hasOwn(properties ?? {}, key);
|
|
123
|
+
const routed = $derived(splitDeep(diagnostics));
|
|
124
|
+
const foot = $derived(unrouted(routed, (step) => typeof step === 'string' && has(step)));
|
|
72
125
|
|
|
73
126
|
/** Obligation, read off the cell exactly as `fieldModels` reads it off a field
|
|
74
127
|
* ({@link obliged}). `validate` anchors it per leaf, so a property with no
|
|
@@ -76,50 +129,102 @@
|
|
|
76
129
|
* (VISUAL_EDITOR §"Enum variants"). */
|
|
77
130
|
const required = obliged;
|
|
78
131
|
|
|
79
|
-
const title = (key: string, sub: QuillFieldSchema): string => sub.
|
|
132
|
+
const title = (key: string, sub: QuillFieldSchema): string => sub.title ?? humanize(key);
|
|
133
|
+
/** What an unset optional cell ghosts, which is what it prints: `none`. An answered
|
|
134
|
+
* cell ghosts nothing, an empty answer printing empty. */
|
|
135
|
+
const noneOf = (key: string, sub: QuillFieldSchema): string | undefined =>
|
|
136
|
+
obj[key] == null && optionalCell(sub) ? t.strings.optionalGhost : undefined;
|
|
137
|
+
/** What an unset free-text cell ghosts where nothing prints ({@link exampleGhost}). */
|
|
138
|
+
const exampleOf = (key: string, sub: QuillFieldSchema): string | undefined =>
|
|
139
|
+
obj[key] == null ? exampleGhost(sub) : undefined;
|
|
140
|
+
/** A scalar cell's `default:` as the text an unset cell holds, where it prints. */
|
|
141
|
+
const textDefault = (sub: QuillFieldSchema): string | undefined => printedText(sub.default);
|
|
142
|
+
/** A content cell's `default:` as the content an unset leaf holds, where it prints. */
|
|
143
|
+
const contentDefault = (sub: QuillFieldSchema): Content | undefined =>
|
|
144
|
+
declaredContent(sub.default, baseType(sub) === 'richtext');
|
|
80
145
|
/** The `aria-label` fallback, for a subform mounted without a field's id space:
|
|
81
146
|
* the field's name and the property's, since nothing else names the control. */
|
|
82
147
|
const fallbackName = (key: string, sub: QuillFieldSchema): string =>
|
|
83
148
|
`${label != null ? `${label} ` : ''}${title(key, sub)}` +
|
|
84
149
|
(required(sub) ? ` ${t.strings.fieldRequired}` : '');
|
|
85
150
|
|
|
86
|
-
/** The
|
|
87
|
-
* drawing its properties by declaration and never by
|
|
88
|
-
*
|
|
89
|
-
* focusing the node is not focusing the view ({@link ProseValue.focus})
|
|
90
|
-
*
|
|
91
|
-
* restating document
|
|
151
|
+
/** The handles of the cells the DOM cannot answer for, keyed by property name —
|
|
152
|
+
* which is stable, a subform drawing its properties by declaration and never by
|
|
153
|
+
* position. A prose cell, because a `contenteditable` matches no focusable
|
|
154
|
+
* selector and focusing the node is not focusing the view ({@link ProseValue.focus});
|
|
155
|
+
* a container cell, because its focus and its landing are its own. A scalar control
|
|
156
|
+
* is resolved off the markup, so a handle each would be five refs restating document
|
|
157
|
+
* order.
|
|
92
158
|
*
|
|
93
159
|
* `$state` for the binding's sake: `bind:this` into a property of a plain object
|
|
94
160
|
* is a write Svelte cannot track, and it says so once per cell per render
|
|
95
161
|
* (`ArrayField` keeps its element refs the same way). */
|
|
96
|
-
const proseEls: Record<
|
|
162
|
+
const proseEls: Record<
|
|
163
|
+
string,
|
|
164
|
+
{ focus: () => void; setCaret: (pos: number) => void } | undefined
|
|
165
|
+
> = $state({});
|
|
166
|
+
const nestedEls: Record<
|
|
167
|
+
string,
|
|
168
|
+
| { focus: () => void; focusPath: (steps: readonly PathStep[], pos?: number) => LandingBox }
|
|
169
|
+
| undefined
|
|
170
|
+
> = $state({});
|
|
97
171
|
let rootEl = $state<HTMLElement | undefined>();
|
|
172
|
+
/** A handle by property name, own keys only: a property can be named `constructor`. */
|
|
173
|
+
const ref = <T,>(map: Record<string, T | undefined>, key: string): T | undefined =>
|
|
174
|
+
Object.hasOwn(map, key) ? map[key] : undefined;
|
|
98
175
|
const FOCUSABLE = 'input, select, button, [tabindex]:not([tabindex="-1"])';
|
|
99
176
|
/** Take the caret: the first property that has somewhere to put it, in document
|
|
100
|
-
* order — a subform has no single control of its own to land on
|
|
101
|
-
*
|
|
102
|
-
* empty or wholly unsupported subform lands nothing. */
|
|
177
|
+
* order — a subform has no single control of its own to land on. An empty subform
|
|
178
|
+
* lands nothing. */
|
|
103
179
|
export function focus(): void {
|
|
104
180
|
for (const cell of propCells()) if (landOn(cell)) return;
|
|
105
181
|
}
|
|
182
|
+
/**
|
|
183
|
+
* Land at `steps` inside this subform: the property the first step names, and the
|
|
184
|
+
* rest of the walk inside it where that property is a container ({@link
|
|
185
|
+
* FieldControl.focusPath}). A prose cell takes the offset; a scalar takes the bare
|
|
186
|
+
* focus. A step naming no property here falls back to {@link focus}. What comes back
|
|
187
|
+
* is the innermost row the walk opened, which a property of this subform never is:
|
|
188
|
+
* the caller washes its own box for a landing that ends here.
|
|
189
|
+
*/
|
|
190
|
+
export function focusPath(steps: readonly PathStep[], pos?: number): LandingBox {
|
|
191
|
+
const [key, ...rest] = steps;
|
|
192
|
+
if (typeof key !== 'string' || !has(key)) {
|
|
193
|
+
focus();
|
|
194
|
+
return undefined;
|
|
195
|
+
}
|
|
196
|
+
const nested = ref(nestedEls, key);
|
|
197
|
+
if (nested) return nested.focusPath(rest, pos);
|
|
198
|
+
const prose = ref(proseEls, key);
|
|
199
|
+
if (prose) {
|
|
200
|
+
if (pos != null) prose.setCaret(pos);
|
|
201
|
+
else prose.focus();
|
|
202
|
+
return undefined;
|
|
203
|
+
}
|
|
204
|
+
focusProp(key);
|
|
205
|
+
return undefined;
|
|
206
|
+
}
|
|
106
207
|
/** A label click for the properties `for` cannot reach: the date control, whose
|
|
107
|
-
* focus lives on a segment,
|
|
108
|
-
* to that property's own cell, so the click lands where the
|
|
109
|
-
* the subform's first control. */
|
|
208
|
+
* focus lives on a segment, a prose cell, which is a `contenteditable`, and a
|
|
209
|
+
* nested subform. Scoped to that property's own cell, so the click lands where the
|
|
210
|
+
* label is rather than on the subform's first control. */
|
|
110
211
|
function focusProp(key: string): void {
|
|
111
212
|
for (const cell of propCells()) if (cell.dataset.qmProp === key) return void landOn(cell);
|
|
112
213
|
}
|
|
113
214
|
// Matched on the dataset: a key is the schema's own string, and spelling one into a
|
|
114
|
-
// selector needs `CSS.escape`, which the test DOM does not carry.
|
|
215
|
+
// selector needs `CSS.escape`, which the test DOM does not carry. `:scope >` keeps a
|
|
216
|
+
// nested subform's cells out of this one's walk: they are its own to land in.
|
|
115
217
|
function propCells(): HTMLElement[] {
|
|
116
|
-
return [
|
|
218
|
+
return [
|
|
219
|
+
...(rootEl?.querySelectorAll<HTMLElement>(':scope > .qm-object-grid > [data-qm-prop]') ?? [])
|
|
220
|
+
];
|
|
117
221
|
}
|
|
118
222
|
/** Land in one property's cell, answering whether it took the caret. */
|
|
119
223
|
function landOn(cell: HTMLElement): boolean {
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
224
|
+
const key = cell.dataset.qmProp ?? '';
|
|
225
|
+
const owned = ref(nestedEls, key) ?? ref(proseEls, key);
|
|
226
|
+
if (owned) {
|
|
227
|
+
owned.focus();
|
|
123
228
|
return true;
|
|
124
229
|
}
|
|
125
230
|
// Past the label row: a property carrying a `description` opens with the hint
|
|
@@ -148,99 +253,200 @@
|
|
|
148
253
|
<div
|
|
149
254
|
bind:this={rootEl}
|
|
150
255
|
class="qm-object"
|
|
256
|
+
class:qm-object-bare={bare}
|
|
151
257
|
role="group"
|
|
152
258
|
aria-labelledby={labelledBy}
|
|
153
259
|
aria-describedby={describedBy}
|
|
154
260
|
>
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
{
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
{
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
261
|
+
<!-- The grid is one box in, so the wrapper can be the query container it reads its
|
|
262
|
+
capacity from: a container cannot query itself. -->
|
|
263
|
+
<div class="qm-object-grid qm-tracks">
|
|
264
|
+
{#each entries as [key, sub] (mountKey(key, sub))}
|
|
265
|
+
{@const kind = controlKind(sub)}
|
|
266
|
+
{@const ids = idBase && !bare ? propertyDomIds(idBase, key) : undefined}
|
|
267
|
+
{@const named = ids ? undefined : fallbackName(key, sub)}
|
|
268
|
+
{@const describes = sub.description && ids ? ids.description : undefined}
|
|
269
|
+
{@const deep = routed.below.get(key)}
|
|
270
|
+
{@const own = isContainer(kind) ? undefined : deep?.map((d) => d.diagnostic)}
|
|
271
|
+
<!-- `for` reaches the labelable controls, a variant through its discriminant; the
|
|
272
|
+
date field's focus lives on a segment and a prose cell is a `contenteditable`,
|
|
273
|
+
so those take the click handoff instead, exactly as `Field` does one level up. -->
|
|
274
|
+
{@const labelable =
|
|
275
|
+
kind === 'text' ||
|
|
276
|
+
kind === 'enum' ||
|
|
277
|
+
kind === 'variant' ||
|
|
278
|
+
kind === 'number' ||
|
|
279
|
+
kind === 'boolean'}
|
|
280
|
+
<!-- A container takes the full span at every depth: its rows are the document's
|
|
281
|
+
to count, and a track beside it would stand in a column of whitespace
|
|
282
|
+
(`packable`). A block prose cell spans it too, and keeps its label row. -->
|
|
283
|
+
{@const block = kind === 'prose' && !shortCell(sub)}
|
|
284
|
+
<div
|
|
285
|
+
class="qm-object-prop"
|
|
286
|
+
class:qm-prop-full={isContainer(kind)}
|
|
287
|
+
class:qm-prop-wide={block}
|
|
288
|
+
data-qm-prop={key}
|
|
289
|
+
>
|
|
290
|
+
{#if ids && kind !== 'array' && kind !== 'matrix'}
|
|
291
|
+
<FieldLabel
|
|
292
|
+
label={title(key, sub)}
|
|
293
|
+
controlId={labelable ? ids.control : undefined}
|
|
294
|
+
id={ids.label}
|
|
295
|
+
descriptionId={describes}
|
|
296
|
+
onActivate={labelable ? undefined : () => focusProp(key)}
|
|
297
|
+
required={required(sub)}
|
|
298
|
+
description={sub.description}
|
|
299
|
+
/>
|
|
300
|
+
{/if}
|
|
301
|
+
{#if kind === 'enum'}
|
|
302
|
+
<EnumField
|
|
303
|
+
label={named}
|
|
304
|
+
id={ids?.control}
|
|
305
|
+
describedBy={describes}
|
|
306
|
+
value={obj[key] as string | undefined}
|
|
307
|
+
values={sub.values ?? []}
|
|
308
|
+
fallback={sub.default as string | undefined}
|
|
309
|
+
blankTitle={sub.ui?.blank_title}
|
|
310
|
+
optional={optionalCell(sub)}
|
|
311
|
+
onCommit={(v) => commitProp(key, v)}
|
|
312
|
+
/>
|
|
313
|
+
{:else if kind === 'number'}
|
|
314
|
+
<NumberField
|
|
315
|
+
label={named}
|
|
316
|
+
id={ids?.control}
|
|
317
|
+
describedBy={describes}
|
|
318
|
+
value={obj[key] as number | undefined}
|
|
319
|
+
integer={baseType(sub) === 'integer'}
|
|
320
|
+
fallback={textDefault(sub)}
|
|
321
|
+
placeholder={noneOf(key, sub)}
|
|
322
|
+
onCommit={(v) => commitProp(key, v)}
|
|
323
|
+
/>
|
|
324
|
+
{:else if kind === 'boolean'}
|
|
325
|
+
<BooleanField
|
|
326
|
+
label={named}
|
|
327
|
+
id={ids?.control}
|
|
328
|
+
describedBy={describes}
|
|
329
|
+
value={obj[key] as boolean | undefined}
|
|
330
|
+
fallback={sub.default as boolean | undefined}
|
|
331
|
+
optional={optionalCell(sub)}
|
|
332
|
+
onCommit={(v) => commitProp(key, v)}
|
|
333
|
+
/>
|
|
334
|
+
{:else if kind === 'date'}
|
|
335
|
+
<DateField
|
|
336
|
+
label={named}
|
|
337
|
+
labelledBy={ids?.label}
|
|
338
|
+
describedBy={describes}
|
|
339
|
+
value={obj[key] as string | undefined}
|
|
340
|
+
fallback={sub.default != null ? String(sub.default) : undefined}
|
|
341
|
+
onCommit={(v) => commitProp(key, v)}
|
|
342
|
+
/>
|
|
343
|
+
{:else if kind === 'text'}
|
|
344
|
+
<TextField
|
|
345
|
+
label={named}
|
|
346
|
+
id={ids?.control}
|
|
347
|
+
describedBy={describes}
|
|
348
|
+
value={obj[key] as string | undefined}
|
|
349
|
+
fallback={textDefault(sub)}
|
|
350
|
+
placeholder={noneOf(key, sub)}
|
|
351
|
+
example={exampleOf(key, sub)}
|
|
352
|
+
onCommit={(v) => commitProp(key, v)}
|
|
353
|
+
onKey={onCellKey ? (e) => onCellKey(e, key) : undefined}
|
|
354
|
+
/>
|
|
355
|
+
{:else if kind === 'prose'}
|
|
356
|
+
<!-- Keyed by name, so a re-derive of the container this cell commits whole
|
|
357
|
+
leaves the leaf mounted and the caret in it. -->
|
|
358
|
+
<ProseValue
|
|
359
|
+
bind:this={proseEls[key]}
|
|
360
|
+
content={() => contentAt([key])}
|
|
361
|
+
fallback={contentDefault(sub)}
|
|
362
|
+
plaintext={baseType(sub) === 'plaintext'}
|
|
363
|
+
placeholder={noneOf(key, sub)}
|
|
364
|
+
example={exampleOf(key, sub)}
|
|
365
|
+
{block}
|
|
366
|
+
label={named}
|
|
367
|
+
labelledBy={ids?.label}
|
|
368
|
+
describedBy={describes}
|
|
369
|
+
onChange={(rt) => commitProp(key, rt)}
|
|
370
|
+
onKey={onCellKey ? (e) => onCellKey(e, key) : undefined}
|
|
371
|
+
/>
|
|
372
|
+
{:else if kind === 'array'}
|
|
373
|
+
<!-- The repeater owns its label row, the add chip sharing it (`Field` skips
|
|
374
|
+
its own label for one too), so the names are handed down rather than
|
|
375
|
+
drawn here. Its rows are collapsed rows again, one open at a time: the
|
|
376
|
+
depth an open row under an open row costs is two rungs of the ladder
|
|
377
|
+
and one figure. -->
|
|
378
|
+
<ArrayField
|
|
379
|
+
bind:this={nestedEls[key]}
|
|
380
|
+
value={obj[key] as unknown[] | undefined}
|
|
381
|
+
fallback={Array.isArray(sub.default) ? sub.default : undefined}
|
|
382
|
+
items={sub.items}
|
|
383
|
+
layout={arrayLayout(sub)}
|
|
384
|
+
max={sub.max}
|
|
385
|
+
label={title(key, sub)}
|
|
386
|
+
required={required(sub)}
|
|
387
|
+
description={sub.description}
|
|
388
|
+
labelId={ids?.label}
|
|
389
|
+
descriptionId={describes}
|
|
390
|
+
idBase={ids?.control}
|
|
391
|
+
contentAt={(path) => contentAt([key, ...path])}
|
|
392
|
+
onCommit={(v) => commitProp(key, v)}
|
|
393
|
+
diagnostics={deep}
|
|
394
|
+
/>
|
|
395
|
+
{:else if kind === 'object'}
|
|
396
|
+
<ObjectField
|
|
397
|
+
bind:this={nestedEls[key]}
|
|
398
|
+
value={obj[key] as Record<string, unknown> | undefined}
|
|
399
|
+
properties={sub.properties}
|
|
400
|
+
label={ids ? undefined : fallbackName(key, sub)}
|
|
401
|
+
idBase={ids?.control}
|
|
402
|
+
labelledBy={ids?.label}
|
|
403
|
+
describedBy={describes}
|
|
404
|
+
contentAt={(path) => contentAt([key, ...path])}
|
|
405
|
+
onCommit={(v) => commitProp(key, v)}
|
|
406
|
+
diagnostics={deep}
|
|
407
|
+
/>
|
|
408
|
+
{:else if kind === 'matrix'}
|
|
409
|
+
<MatrixField
|
|
410
|
+
bind:this={nestedEls[key]}
|
|
411
|
+
value={obj[key] as Record<string, unknown> | undefined}
|
|
412
|
+
schema={sub}
|
|
413
|
+
label={title(key, sub)}
|
|
414
|
+
description={sub.description}
|
|
415
|
+
labelId={ids?.label}
|
|
416
|
+
descriptionId={describes}
|
|
417
|
+
idBase={ids?.control}
|
|
418
|
+
contentAt={(path) => contentAt([key, ...path])}
|
|
419
|
+
onCommit={(v) => commitProp(key, v)}
|
|
420
|
+
diagnostics={deep}
|
|
421
|
+
/>
|
|
422
|
+
{:else if kind === 'variant'}
|
|
423
|
+
<VariantField
|
|
424
|
+
bind:this={nestedEls[key]}
|
|
425
|
+
value={obj[key] as Record<string, unknown> | undefined}
|
|
426
|
+
schema={sub}
|
|
427
|
+
ghostMember={sub.default as string | undefined}
|
|
428
|
+
label={title(key, sub)}
|
|
429
|
+
id={ids?.control}
|
|
430
|
+
labelledBy={ids?.label}
|
|
431
|
+
describedBy={describes}
|
|
432
|
+
contentAt={(path) => contentAt([key, ...path])}
|
|
433
|
+
onCommit={(v) => commitProp(key, v)}
|
|
434
|
+
diagnostics={deep}
|
|
435
|
+
/>
|
|
436
|
+
{:else}
|
|
437
|
+
<TextField
|
|
438
|
+
label={named}
|
|
439
|
+
id={ids?.control}
|
|
440
|
+
describedBy={describes}
|
|
441
|
+
value={obj[key] as string | undefined}
|
|
442
|
+
onCommit={(v) => commitProp(key, v)}
|
|
443
|
+
/>
|
|
444
|
+
{/if}
|
|
445
|
+
<DiagnosticList diagnostics={own} />
|
|
446
|
+
</div>
|
|
447
|
+
{/each}
|
|
448
|
+
</div>
|
|
449
|
+
<DiagnosticList diagnostics={foot} />
|
|
244
450
|
</div>
|
|
245
451
|
|
|
246
452
|
<style>
|
|
@@ -256,19 +462,32 @@
|
|
|
256
462
|
section's (`Card.svelte`). A cap and not a leading gap: what stands the subform off
|
|
257
463
|
the label or the box above it is still that stacker's own.
|
|
258
464
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
465
|
+
A query container over its own width: the capacity its grid steps is the width the
|
|
466
|
+
properties get, which three rungs in is three `--_qm-nest` and three verticals
|
|
467
|
+
narrower than the section's, and a subform reading the section's count over that
|
|
468
|
+
box draws four tracks where two fit. The ladder itself is the one shared recipe
|
|
469
|
+
(`.qm-tracks`, controls.css), so this container and the section's step at the same
|
|
470
|
+
widths. Not on a table's row, which is a subgrid of the table and sizes nothing of
|
|
471
|
+
its own. */
|
|
264
472
|
.qm-object {
|
|
265
|
-
display:
|
|
266
|
-
|
|
267
|
-
gap: var(--_qm-space
|
|
473
|
+
display: flex;
|
|
474
|
+
flex-direction: column;
|
|
475
|
+
gap: var(--_qm-space);
|
|
476
|
+
}
|
|
477
|
+
.qm-object:not(.qm-object-bare) {
|
|
478
|
+
container-type: inline-size;
|
|
268
479
|
border-inline-start: var(--_qm-vertical-width) solid var(--_qm-border);
|
|
269
480
|
padding-inline-start: var(--_qm-nest);
|
|
270
481
|
padding-block: var(--_qm-space);
|
|
271
482
|
}
|
|
483
|
+
/* The tracks are this subform's own count over its own width (`.qm-tracks`); the
|
|
484
|
+
edges are not the section's: the inset is the nesting, and a cell lining up with the
|
|
485
|
+
field above it would claim a depth it is not at. */
|
|
486
|
+
.qm-object-grid {
|
|
487
|
+
display: grid;
|
|
488
|
+
grid-template-columns: repeat(var(--cols), 1fr);
|
|
489
|
+
gap: var(--_qm-space-2);
|
|
490
|
+
}
|
|
272
491
|
/* A property measures like a field: two tracks over the subform's own rows, so a
|
|
273
492
|
label wrapping in one column leaves its control on the row's baseline rather than a
|
|
274
493
|
line below it — `Field.svelte`'s rule for a row-sharing field, one level in. The
|
|
@@ -291,9 +510,55 @@
|
|
|
291
510
|
.qm-object-prop:only-child {
|
|
292
511
|
grid-column: span var(--cols-half);
|
|
293
512
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
513
|
+
/* A block prose property holds paragraphs, so a track beside it stands in whitespace
|
|
514
|
+
once it grows (`packable`); its label still rides the row above it. */
|
|
515
|
+
.qm-object-prop.qm-prop-wide,
|
|
516
|
+
.qm-object-prop.qm-prop-wide:only-child {
|
|
517
|
+
grid-column: 1 / -1;
|
|
518
|
+
}
|
|
519
|
+
/* A container property takes the whole row and its own rows: its label rides inside
|
|
520
|
+
the control it draws (a repeater's header, a matrix's) or above a subform of its
|
|
521
|
+
own, and neither is a track a scalar's label shares. */
|
|
522
|
+
.qm-object-prop.qm-prop-full,
|
|
523
|
+
.qm-object-prop.qm-prop-full:only-child {
|
|
524
|
+
grid-column: 1 / -1;
|
|
525
|
+
grid-row: auto;
|
|
526
|
+
grid-template-rows: none;
|
|
527
|
+
row-gap: var(--_qm-space);
|
|
528
|
+
}
|
|
529
|
+
/* ── A table's row ──────────────────────────────────────────────────────────
|
|
530
|
+
The cells alone, each on the track the header names: this box and its grid both
|
|
531
|
+
subgrid onto the row that holds them, so a cell's edge is the column's. No
|
|
532
|
+
vertical, no inset, no capacity of its own, and one row per cell, the label having
|
|
533
|
+
moved up to the header. `.qm-array-row` is the row (`ArrayField`), which spends
|
|
534
|
+
the last track on the row's remove; this box takes the rest. */
|
|
535
|
+
.qm-object-bare {
|
|
536
|
+
display: grid;
|
|
537
|
+
grid-template-columns: subgrid;
|
|
538
|
+
grid-column: 1 / -2;
|
|
539
|
+
}
|
|
540
|
+
.qm-object-bare > .qm-object-grid {
|
|
541
|
+
grid-template-columns: subgrid;
|
|
542
|
+
grid-column: 1 / -1;
|
|
543
|
+
/* The table's own gutter, not the subform's: a subgrid that states a wider gap
|
|
544
|
+
than its parent's shifts its items half the difference off the tracks the
|
|
545
|
+
header cells sit on. */
|
|
546
|
+
column-gap: var(--_qm-space);
|
|
547
|
+
}
|
|
548
|
+
.qm-object-bare .qm-object-prop,
|
|
549
|
+
.qm-object-bare .qm-object-prop:only-child {
|
|
550
|
+
grid-column: auto;
|
|
551
|
+
grid-row: auto;
|
|
552
|
+
grid-template-rows: none;
|
|
553
|
+
row-gap: var(--_qm-space-half);
|
|
554
|
+
/* A row is one line of controls, so a cell shorter than the box beside it stands
|
|
555
|
+
on that line: a switch held at the top of a row a text box set the height of
|
|
556
|
+
reads as a cell that slipped its track. */
|
|
557
|
+
align-items: center;
|
|
558
|
+
}
|
|
559
|
+
/* What the row could not hand down stands across it: a diagnostic in the first
|
|
560
|
+
column alone would read as that column's. */
|
|
561
|
+
.qm-object-bare > :global(.qm-diag-list) {
|
|
562
|
+
grid-column: 1 / -1;
|
|
298
563
|
}
|
|
299
564
|
</style>
|