@quillmark/svelte 0.10.1 → 0.12.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.
Files changed (80) hide show
  1. package/README.md +1 -1
  2. package/THEMING.md +2 -2
  3. package/dist/core/address.d.ts +17 -10
  4. package/dist/core/address.js +26 -12
  5. package/dist/core/bloom.js +4 -3
  6. package/dist/core/codec/blocks.js +21 -1
  7. package/dist/core/codec/breaks.d.ts +10 -2
  8. package/dist/core/codec/breaks.js +65 -9
  9. package/dist/core/codec/code.d.ts +6 -4
  10. package/dist/core/codec/code.js +7 -4
  11. package/dist/core/codec/decode.js +103 -68
  12. package/dist/core/codec/encode.d.ts +5 -8
  13. package/dist/core/codec/encode.js +56 -86
  14. package/dist/core/codec/field.js +43 -24
  15. package/dist/core/codec/index.d.ts +2 -1
  16. package/dist/core/codec/index.js +6 -3
  17. package/dist/core/codec/islands.d.ts +16 -11
  18. package/dist/core/codec/islands.js +70 -59
  19. package/dist/core/codec/keymap.js +2 -1
  20. package/dist/core/codec/marks.d.ts +17 -13
  21. package/dist/core/codec/marks.js +39 -42
  22. package/dist/core/codec/positions.d.ts +4 -1
  23. package/dist/core/codec/positions.js +5 -1
  24. package/dist/core/codec/reconcile.d.ts +6 -0
  25. package/dist/core/codec/reconcile.js +19 -0
  26. package/dist/core/codec/schema.d.ts +5 -8
  27. package/dist/core/codec/schema.js +30 -156
  28. package/dist/core/codec/slash.js +0 -0
  29. package/dist/core/codec/table-view.js +77 -26
  30. package/dist/core/codec/table.d.ts +6 -5
  31. package/dist/core/codec/table.js +14 -31
  32. package/dist/core/codec/urls.d.ts +20 -0
  33. package/dist/core/codec/urls.js +45 -0
  34. package/dist/core/rungs.d.ts +8 -0
  35. package/dist/core/rungs.js +24 -0
  36. package/dist/core/theme.css +10 -0
  37. package/dist/preset/recipes.css +1 -1
  38. package/dist/preset/scale.css +7 -4
  39. package/dist/preview/bridge.d.ts +8 -1
  40. package/dist/preview/bridge.js +31 -25
  41. package/dist/preview/controller.js +13 -13
  42. package/dist/preview/geometry.d.ts +12 -1
  43. package/dist/preview/geometry.js +14 -1
  44. package/dist/preview/paint.js +6 -4
  45. package/dist/preview/strings.d.ts +0 -2
  46. package/dist/preview/strings.js +5 -6
  47. package/dist/visual/ArrayField.svelte +600 -173
  48. package/dist/visual/ArrayField.svelte.d.ts +14 -1
  49. package/dist/visual/BooleanField.svelte +10 -7
  50. package/dist/visual/Card.svelte +8 -30
  51. package/dist/visual/DateField.svelte +2 -2
  52. package/dist/visual/Field.svelte +76 -32
  53. package/dist/visual/FieldHint.svelte +1 -1
  54. package/dist/visual/MatrixField.svelte +352 -0
  55. package/dist/visual/MatrixField.svelte.d.ts +36 -0
  56. package/dist/visual/ObjectField.svelte +356 -133
  57. package/dist/visual/ObjectField.svelte.d.ts +17 -2
  58. package/dist/visual/VariantField.svelte +55 -2
  59. package/dist/visual/VariantField.svelte.d.ts +10 -1
  60. package/dist/visual/VisualEditor.svelte +1 -1
  61. package/dist/visual/VisualEditorInner.svelte +118 -67
  62. package/dist/visual/controls.css +39 -9
  63. package/dist/visual/diagnostics.d.ts +26 -2
  64. package/dist/visual/diagnostics.js +52 -5
  65. package/dist/visual/ext.d.ts +12 -13
  66. package/dist/visual/ext.js +22 -19
  67. package/dist/visual/hold.js +2 -2
  68. package/dist/visual/icons/nodes.d.ts +3 -0
  69. package/dist/visual/icons/nodes.js +2 -1
  70. package/dist/visual/index.js +2 -3
  71. package/dist/visual/leaves.d.ts +25 -12
  72. package/dist/visual/links.d.ts +4 -0
  73. package/dist/visual/links.js +15 -4
  74. package/dist/visual/motion.d.ts +14 -5
  75. package/dist/visual/motion.js +13 -11
  76. package/dist/visual/strings.d.ts +26 -13
  77. package/dist/visual/strings.js +7 -2
  78. package/dist/visual/structure.d.ts +101 -8
  79. package/dist/visual/structure.js +252 -13
  80. package/package.json +2 -2
@@ -1,8 +1,17 @@
1
1
  import type { Content, PathStep, QuillFieldSchema } from '@quillmark/wasm';
2
+ import { type ArrayLayout } from './structure.js';
3
+ import { type DeepDiagnostic } from './diagnostics.js';
4
+ import type { LandingBox } from './leaves.js';
2
5
  import './controls.css';
3
6
  interface Props {
4
7
  value: unknown[] | undefined;
5
8
  items: QuillFieldSchema | undefined;
9
+ /** How the elements draw: the record list, or the table an `array<object>` of
10
+ * short cells asks for (`arrayLayout`). */
11
+ layout?: ArrayLayout;
12
+ /** The schema's `max:`, past which the add chip disables and Enter stops
13
+ * inserting; `undefined` is no cap. */
14
+ max?: number;
6
15
  /** Accessible-name prefix for the element controls (`label` + 1-based index). */
7
16
  label?: string;
8
17
  /** No-default field → a persistent required `*` on the label. */
@@ -28,11 +37,15 @@ interface Props {
28
37
  * control has spliced in and whose commit has yet to land, or was refused. */
29
38
  contentAt: (path: PathStep[]) => Content | undefined;
30
39
  onCommit: (arr: unknown[]) => void;
40
+ /** The diagnostics routed into this array, each with its steps still to walk: one
41
+ * naming a row draws under that row — under its head while it is collapsed, under
42
+ * the cell once it is open — and one naming no row draws at the list's foot. */
43
+ diagnostics?: readonly DeepDiagnostic[];
31
44
  }
32
45
  declare const ArrayField: import("svelte").Component<Props, {
33
46
  focus: () => void;
34
47
  washBox: () => HTMLElement | undefined;
35
- focusElement: (k: number, pos?: number) => HTMLElement | undefined;
48
+ focusPath: (steps: readonly PathStep[], pos?: number) => LandingBox;
36
49
  }, "">;
37
50
  type ArrayField = ReturnType<typeof ArrayField>;
38
51
  export default ArrayField;
@@ -36,9 +36,12 @@
36
36
  const local = syncedLocal(() => value ?? fallback ?? false);
37
37
  </script>
38
38
 
39
- <span class="qm-switch-wrap">
39
+ <!-- Not `.qm-switch`: that name is the preset's pane band, whose narrow-viewport rule
40
+ stands every child of it at `--qmh-tap`, so a track wearing it grows a thumb three times
41
+ its own height over the labels around it (`preset/recipes.css`, held by `check:style`). -->
42
+ <span class="qm-toggle-wrap">
40
43
  <Switch.Root
41
- class="qm-switch qm-focus-ring qm-tap-floor"
44
+ class="qm-toggle qm-focus-ring qm-tap-floor"
42
45
  checked={local.value}
43
46
  {id}
44
47
  aria-label={id ? undefined : label}
@@ -48,7 +51,7 @@
48
51
  onCommit(v);
49
52
  }}
50
53
  >
51
- <Switch.Thumb class="qm-switch-thumb" />
54
+ <Switch.Thumb class="qm-toggle-thumb" />
52
55
  </Switch.Root>
53
56
  </span>
54
57
 
@@ -60,7 +63,7 @@
60
63
  2.5.8's floor is half again that. The `<label for>` beside it forwards a press but
61
64
  stands at the label rung, so it is no taller: without the floor no region on the
62
65
  surface reaches this control at the size every other control on it holds. */
63
- .qm-switch-wrap :global(.qm-switch) {
66
+ .qm-toggle-wrap :global(.qm-toggle) {
64
67
  display: inline-flex;
65
68
  align-items: center;
66
69
  width: 1.75rem;
@@ -75,21 +78,21 @@
75
78
  cursor: pointer;
76
79
  transition: background var(--_qm-duration-fast) var(--_qm-ease-reverse);
77
80
  }
78
- .qm-switch-wrap :global(.qm-switch[data-state='checked']) {
81
+ .qm-toggle-wrap :global(.qm-toggle[data-state='checked']) {
79
82
  background: var(--_qm-accent);
80
83
  }
81
84
  /* The focus ring rides `.qm-focus-ring` on the switch (controls.css). */
82
85
  /* The thumb reads on tone alone: the base surface over the track's `hover` rung
83
86
  unchecked, and over `--_qm-accent` checked. No edge of its own: a hairline around
84
87
  a 12px pill is a second box inside the track's. */
85
- .qm-switch-wrap :global(.qm-switch-thumb) {
88
+ .qm-toggle-wrap :global(.qm-toggle-thumb) {
86
89
  width: 0.75rem;
87
90
  height: 0.75rem;
88
91
  border-radius: var(--_qm-radius-pill);
89
92
  background: var(--_qm-surface);
90
93
  transition: translate var(--_qm-duration-fast) var(--_qm-ease-reverse);
91
94
  }
92
- .qm-switch-wrap :global(.qm-switch-thumb[data-state='checked']) {
95
+ .qm-toggle-wrap :global(.qm-toggle-thumb[data-state='checked']) {
93
96
  translate: 0.75rem;
94
97
  }
95
98
  </style>
@@ -315,7 +315,7 @@
315
315
  {quill}
316
316
  addr={ops.makeAddr(undefined)}
317
317
  unframed
318
- label="Body"
318
+ label={t.strings.bodyLabel}
319
319
  placeholder={card.bodyGhost}
320
320
  leafKey={ops.leafKey(undefined)}
321
321
  {onFocus}
@@ -332,7 +332,7 @@
332
332
  </section>
333
333
 
334
334
  {#snippet sectionFields(fields: FieldModel[])}
335
- <div class="qm-fields">
335
+ <div class="qm-fields qm-tracks">
336
336
  {#each placeFields(fields) as { field: f, span } (f.name)}
337
337
  <Field
338
338
  field={f}
@@ -359,9 +359,9 @@
359
359
  {/snippet}
360
360
 
361
361
  <!-- A card whose `kind` has no schema stays visible and removable, its fields, body and
362
- `$ext` intact in the Document, so it is never a data trap. `setCardKind` swaps the kind
363
- and leaves payload and body untouched, so retyping drops no field; with no kinds to
364
- offer, delete is the only exit. -->
362
+ `$ext` intact in the Document, so it is never a data trap. A retype swaps the kind and
363
+ leaves payload and body untouched, so it drops no field; with no kinds to offer, delete
364
+ is the only exit. -->
365
365
  {#snippet recoveryShell()}
366
366
  <header class="qm-card-header">
367
367
  <span class="qm-card-title-static">{humanize(card.kind)}</span>
@@ -393,7 +393,7 @@
393
393
  </select>
394
394
  </label>
395
395
  {:else}
396
- <p class="qm-recovery-note qm-recovery-muted">
396
+ <p class="qm-recovery-note">
397
397
  {t.strings.noCardKinds}
398
398
  </p>
399
399
  {/if}
@@ -584,33 +584,14 @@
584
584
  .qm-group-panel-inner {
585
585
  container-type: inline-size;
586
586
  }
587
- /* Capacity is the container's, not JavaScript's: nothing measures, so there is no
588
- observer to loop and no re-packing to restructure the DOM under a prose leaf.
589
-
590
- It steps 1 → 2 → 4, skipping 3: each rung is the width at which a track still clears
591
- the comfortable field minimum, and an even capacity is what lets the half a subform
592
- of one takes (`--cols-half`, `ObjectField`) land on a track boundary. A field ends on
593
- its track's own edge, so a rung is `n * 220px + gaps`. */
587
+ /* The count is the ladder's (`.qm-tracks`, controls.css), stepped by the container
588
+ above; what this block owns is the grid the count fills. */
594
589
  .qm-fields {
595
- --cols: 1;
596
- --cols-half: 1;
597
590
  display: grid;
598
591
  grid-template-columns: repeat(var(--cols), 1fr);
599
592
  column-gap: var(--_qm-space-2);
600
593
  row-gap: var(--_qm-space-3);
601
594
  }
602
- @container (min-width: 28rem) {
603
- .qm-fields {
604
- --cols: 2;
605
- --cols-half: 1;
606
- }
607
- }
608
- @container (min-width: 57rem) {
609
- .qm-fields {
610
- --cols: 4;
611
- --cols-half: 2;
612
- }
613
- }
614
595
  /* The nesting vertical, one `--_qm-nest` in from the card's edge with the panel's
615
596
  fields the same rung off it (ARCHITECTURE §"A plane is a tone"). `--_qm-border` and
616
597
  not `--_qm-accent`: a card's edge and the verticals inside it are one stroke at three
@@ -780,9 +761,6 @@
780
761
  font-size: var(--_qm-text-label);
781
762
  color: var(--_qm-ink-label);
782
763
  }
783
- .qm-recovery-muted {
784
- color: var(--_qm-ink-label);
785
- }
786
764
  .qm-recovery-retype {
787
765
  display: inline-flex;
788
766
  align-items: center;
@@ -11,7 +11,7 @@
11
11
  calendar) rather than `DatePicker`: the segments are the entry affordance, and a
12
12
  calendar is a second surface this field does not need.
13
13
 
14
- The boundary is A string, and the local is too. The primitive speaks
14
+ The boundary is a string, and the local is too. The primitive speaks
15
15
  `CalendarDate`; the document speaks `YYYY-MM-DD`. `CalendarDate` carries no time
16
16
  and no zone, so the round-trip is lossless and no local-midnight shift can occur:
17
17
  the hazard that makes `new Date('2026-07-25')` the wrong tool here. Authored
@@ -22,7 +22,7 @@
22
22
  parsed value: a fresh `CalendarDate` is never `===` the last one, which would
23
23
  make every reconcile fire and re-render all seven segments on each commit.
24
24
 
25
- The ghost is the default'S digits, not A format hint. An unset field
25
+ The ghost is the default's digits, not a format hint. An unset field
26
26
  carrying a `default:` prints the default's digits in the segments, ghost-toned,
27
27
  instead of the primitive's `mm`/`dd`/`yyyy` hints, which say "empty" where the
28
28
  rung says "will render 2026-01-01". The ghost is painted in the segment snippet,
@@ -1,8 +1,10 @@
1
1
  <!--
2
2
  Type dispatch (VISUAL_EDITOR §"Structure mirrors the schema"). Given one projected
3
3
  {@link FieldModel} and its live value, render the label + the control the type
4
- maps to. Array controls own their label (paired with the add affordance in
5
- {@link ArrayField}); other types render the label here. Prose leaves take a
4
+ maps to. Array and matrix controls own their label (paired with the add affordance in
5
+ {@link ArrayField}, with the held count in {@link MatrixField}); other types render
6
+ the label here. A container control also takes the walk a nested address makes into
7
+ it ({@link FieldControl.focusPath}) and the diagnostics anchored inside it. Prose leaves take a
6
8
  parent-built live `addr` (its `card` a getter over the stable-id→index map) so
7
9
  a reorder re-targets without a remount; scalars, arrays, and objects commit
8
10
  their value up through `onCommitScalar`, which the parent lowers to the typed
@@ -24,9 +26,16 @@
24
26
  ResolvedField
25
27
  } from '@quillmark/wasm';
26
28
  import type { EditorErrorHandler } from '../core/errors.js';
27
- import type { LeafRegistry } from './leaves.js';
29
+ import type { LandingBox, LeafRegistry } from './leaves.js';
28
30
  import type { FieldModel, FieldSpan } from './structure.js';
29
- import { VARIANT_DISCRIMINANT, ghostDefault, stringifyGhost } from './structure.js';
31
+ import {
32
+ VARIANT_DISCRIMINANT,
33
+ arrayLayout,
34
+ ghostDefault,
35
+ isContainer,
36
+ stringifyGhost
37
+ } from './structure.js';
38
+ import { deepen } from './diagnostics.js';
30
39
  import type { FieldDomIds } from './domid.js';
31
40
  import ProseField from './ProseField.svelte';
32
41
  import TextField from './TextField.svelte';
@@ -37,6 +46,7 @@
37
46
  import ArrayField from './ArrayField.svelte';
38
47
  import ObjectField from './ObjectField.svelte';
39
48
  import VariantField from './VariantField.svelte';
49
+ import MatrixField from './MatrixField.svelte';
40
50
  import DiagnosticList from './DiagnosticList.svelte';
41
51
  import FieldLabel from './FieldLabel.svelte';
42
52
 
@@ -123,15 +133,25 @@
123
133
  // reference must vanish with it: `aria-describedby` pointing at nothing describes
124
134
  // nothing, and silently.
125
135
  const describedBy = $derived(field.description ? domIds.description : undefined);
136
+ // Two controls own their label row, the add chip and the held count sharing it.
137
+ const ownsLabel = $derived(field.control === 'array' || field.control === 'matrix');
138
+
139
+ // A container walks its diagnostics down to the cell each names and draws the rest
140
+ // at its own foot (`diagnostics.ts`); every other control draws them here, under
141
+ // itself, there being nothing inside it for a path to name.
142
+ const deep = $derived(deepen(diagnostics));
126
143
 
127
144
  /** The boundary's nested content read with this field's address already bound: an
128
145
  * array's elements, an object's properties, a variant's cells, and a subform's cells
129
146
  * under an open element. One door rather than one per depth, since `getContentAt`
130
147
  * takes the whole path and the walk down is each container prefixing its own step.
131
- * Read per call and never held: a leaf takes its state at mount, and a control asks
132
- * once. */
148
+ *
149
+ * The reader is held, as the codec's is (`field.ts`): a `{quill, doc}` pair reads
150
+ * live, so every call through it sees the commit before it, and an array of prose
151
+ * elements is one handle across the boundary rather than one per element. */
152
+ const reader = $derived(quill.reader(doc));
133
153
  function contentAt(path: PathStep[]): Content | undefined {
134
- return quill.reader(doc).getContentAt(addr, path);
154
+ return reader.getContentAt(addr, path);
135
155
  }
136
156
 
137
157
  // ── Focus: one answer, two callers (`leaves.ts`) ─────────────────────────────
@@ -142,28 +162,29 @@
142
162
  // restores a selection, a date field lands on its first segment, an array lands on
143
163
  // its first element or on the add affordance that is all an empty one has, an
144
164
  // object on its first property.
165
+ /** What a container control exports: its focus, and the walk into it. */
166
+ type Inside = {
167
+ focus: () => void;
168
+ focusPath: (steps: readonly PathStep[], pos?: number) => LandingBox;
169
+ washBox?: () => HTMLElement | undefined;
170
+ };
145
171
  let proseEl = $state<{ focus: () => void } | undefined>();
146
172
  let dateEl = $state<{ focus: () => void } | undefined>();
147
- let arrayEl = $state<
148
- | {
149
- focus: () => void;
150
- focusElement: (k: number, pos?: number) => HTMLElement | undefined;
151
- washBox: () => HTMLElement | undefined;
152
- }
153
- | undefined
154
- >();
155
- let objectEl = $state<{ focus: () => void } | undefined>();
173
+ let arrayEl = $state<Inside | undefined>();
174
+ let objectEl = $state<Inside | undefined>();
175
+ let variantEl = $state<Inside | undefined>();
176
+ let matrixEl = $state<Inside | undefined>();
177
+ const inside = (): Inside | undefined => arrayEl ?? objectEl ?? variantEl ?? matrixEl;
156
178
  function focusControl(): void {
157
- const owner = proseEl ?? dateEl ?? arrayEl ?? objectEl;
179
+ const owner = proseEl ?? dateEl ?? inside();
158
180
  if (owner) return owner.focus();
159
181
  document.getElementById(domIds.control)?.focus();
160
182
  }
161
- /** The array's per-ELEMENT landing, for the addresses the preview mints under an
162
- * array field (`leaves.ts`); read at the call, so it tracks the mounted repeater.
163
- * The row it settled in comes back with it, the wash being the landing's own
164
- * granularity. */
165
- function focusElement(k: number, pos?: number): HTMLElement | undefined {
166
- return arrayEl?.focusElement(k, pos);
183
+ /** The landing at depth, for the addresses the boundary mints under a container
184
+ * (`leaves.ts`); read at the call, so it tracks the mounted control. The innermost
185
+ * row it settled in comes back with it, the wash being the landing's own granularity. */
186
+ function focusPath(steps: readonly PathStep[], pos?: number): LandingBox {
187
+ return inside()?.focusPath(steps, pos);
167
188
  }
168
189
  // Only where `for` cannot reach; the labelable four are the browser's own, and a
169
190
  // second handler over them would be a focus the label already placed.
@@ -173,17 +194,17 @@
173
194
  * This field's landing handle. The wrapper is the bloom host rather than the
174
195
  * control: `bloomInside` appends an inset child and an `<input>` holds none. The
175
196
  * label is outside it, and stays out of the wash: an arrival marks where the caret
176
- * landed, which is the control. The one control that owns its label is the array, so
177
- * that one names its own box (`ArrayField.washBox`), read at the bloom the way
178
- * `focusElement` is read at the call.
197
+ * landed, which is the control. A control that owns its label (`ownsLabel`: the array
198
+ * and the matrix) holds that row inside itself, so it names its own box
199
+ * (`washBox`) and the bloom reads that in the wrapper's place.
179
200
  *
180
201
  * A prose leaf is absent here — it registers its own controller from inside
181
202
  * `ProseField`, carrying the codec seam this handle has no half of — and reactive
182
203
  * rather than mount-once, because a retype can swap the control under a leaf key
183
204
  * that does not remount.
184
205
  *
185
- * An array carries the per-element lane as well; no other control has elements for
186
- * an address to name. `el` is the field-wide box, which an element landing does not
206
+ * A container carries the walk into it as well; no other control has an inside for
207
+ * an address to name. `el` is the field-wide box, which a landing at depth does not
187
208
  * wash: that lane hands back its own row (`leaves.ts`).
188
209
  */
189
210
  let controlEl = $state<HTMLElement | undefined>();
@@ -194,9 +215,9 @@
194
215
  const wrapper = controlEl;
195
216
  registry.registerControl(key, {
196
217
  focus: focusControl,
197
- focusElement: field.control === 'array' ? focusElement : undefined,
218
+ focusPath: isContainer(field.control) ? focusPath : undefined,
198
219
  get el() {
199
- return arrayEl?.washBox() ?? wrapper;
220
+ return inside()?.washBox?.() ?? wrapper;
200
221
  }
201
222
  });
202
223
  return () => registry.unregisterControl(key);
@@ -214,7 +235,7 @@
214
235
  </script>
215
236
 
216
237
  <div class="qm-field" class:cell={span === 'cell'}>
217
- {#if field.control !== 'array'}
238
+ {#if !ownsLabel}
218
239
  <FieldLabel
219
240
  label={field.label}
220
241
  controlId={labelable ? domIds.control : undefined}
@@ -239,6 +260,7 @@
239
260
  {addr}
240
261
  inline={field.inline}
241
262
  plaintext={field.plaintext}
263
+ placeholder={defaultStr}
242
264
  labelledBy={domIds.label}
243
265
  {describedBy}
244
266
  {leafKey}
@@ -261,6 +283,7 @@
261
283
  />
262
284
  {:else if field.control === 'variant'}
263
285
  <VariantField
286
+ bind:this={variantEl}
264
287
  value={value as Record<string, unknown> | undefined}
265
288
  schema={field.schema}
266
289
  {ghostMember}
@@ -272,6 +295,7 @@
272
295
  onCommit={onCommitScalar}
273
296
  {optionAllowed}
274
297
  {enumDisallowed}
298
+ diagnostics={deep}
275
299
  />
276
300
  {:else if field.control === 'number'}
277
301
  <NumberField
@@ -304,6 +328,8 @@
304
328
  bind:this={arrayEl}
305
329
  value={value as unknown[] | undefined}
306
330
  items={field.schema.items}
331
+ layout={arrayLayout(field.schema)}
332
+ max={field.schema.max}
307
333
  label={field.label}
308
334
  required={field.required}
309
335
  description={field.description}
@@ -312,6 +338,7 @@
312
338
  idBase={domIds.control}
313
339
  {contentAt}
314
340
  onCommit={onCommitScalar}
341
+ diagnostics={deep}
315
342
  />
316
343
  {:else if field.control === 'object'}
317
344
  <ObjectField
@@ -324,6 +351,21 @@
324
351
  {describedBy}
325
352
  {contentAt}
326
353
  onCommit={onCommitScalar}
354
+ diagnostics={deep}
355
+ />
356
+ {:else if field.control === 'matrix'}
357
+ <MatrixField
358
+ bind:this={matrixEl}
359
+ value={value as Record<string, unknown> | undefined}
360
+ schema={field.schema}
361
+ label={field.label}
362
+ description={field.description}
363
+ labelId={domIds.label}
364
+ descriptionId={domIds.description}
365
+ idBase={domIds.control}
366
+ {contentAt}
367
+ onCommit={onCommitScalar}
368
+ diagnostics={deep}
327
369
  />
328
370
  {:else}
329
371
  <TextField
@@ -336,7 +378,9 @@
336
378
  {/if}
337
379
  </div>
338
380
 
339
- <DiagnosticList {diagnostics} />
381
+ {#if !isContainer(field.control)}
382
+ <DiagnosticList {diagnostics} />
383
+ {/if}
340
384
  </div>
341
385
  </div>
342
386
 
@@ -131,7 +131,7 @@
131
131
  }}><Icon name="info" size={13} /></button
132
132
  >
133
133
 
134
- <!-- Placement is the description'S height. One line rides the label rung beside its own
134
+ <!-- Placement is the description's height. One line rides the label rung beside its own
135
135
  glyph, where the overhang past the rung's line box falls in the label-to-control gaps.
136
136
  Anything taller stands over the field: beside the glyph it would hang onto the control
137
137
  it describes, the one overlap a reader cannot clear by reaching for what is hidden,