@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
@@ -10,12 +10,16 @@
10
10
  container with one cell written — and cells outside the drawn world ride through
11
11
  untouched rather than needing to be merged back. A content cell reads at the key it
12
12
  declares: the boundary's schema walk unions the worlds, so a dormant cell reads absent
13
- rather than raising.
13
+ rather than raising. A cell may be a container of its own, which the subform draws at
14
+ the next rung.
14
15
  -->
15
16
  <script lang="ts">
16
17
  import type { Content, PathStep, QuillFieldSchema } from '@quillmark/wasm';
17
18
  import EnumField from './EnumField.svelte';
18
19
  import ObjectField from './ObjectField.svelte';
20
+ import DiagnosticList from './DiagnosticList.svelte';
21
+ import type { DeepDiagnostic } from './diagnostics.js';
22
+ import type { LandingBox } from './leaves.js';
19
23
  import {
20
24
  VARIANT_DISCRIMINANT,
21
25
  commitDiscriminant,
@@ -43,6 +47,10 @@
43
47
  onCommit: (v: Record<string, unknown> | undefined) => void;
44
48
  optionAllowed?: (value: string) => boolean;
45
49
  enumDisallowed?: 'hide' | 'disable';
50
+ /** The diagnostics routed into this field, each with its steps still to walk: one
51
+ * naming a live cell draws under it; the rest — the discriminant's, a dormant
52
+ * world's — draw at the field's foot. */
53
+ diagnostics?: readonly DeepDiagnostic[];
46
54
  }
47
55
  let {
48
56
  value,
@@ -55,12 +63,52 @@
55
63
  contentAt,
56
64
  onCommit,
57
65
  optionAllowed,
58
- enumDisallowed
66
+ enumDisallowed,
67
+ diagnostics
59
68
  }: Props = $props();
60
69
 
61
70
  const member = $derived(variantMember(value, ghostMember));
62
71
  const cells = $derived(variantCells(schema, member));
63
72
  const discriminant = $derived(value?.[VARIANT_DISCRIMINANT] as string | undefined);
73
+
74
+ type Subform = {
75
+ focus: () => void;
76
+ focusPath: (steps: readonly PathStep[], pos?: number) => LandingBox;
77
+ };
78
+ let cellsEl = $state<Subform | undefined>();
79
+
80
+ /** Whether a step names a cell the live world draws. The one test the landing and
81
+ * the diagnostics both read, so the cell a caret lands in is the cell a message
82
+ * draws under. The discriminant is the field's own control and no world's cell. */
83
+ function live(step: PathStep | undefined): boolean {
84
+ return (
85
+ typeof step === 'string' &&
86
+ step !== VARIANT_DISCRIMINANT &&
87
+ !!cells &&
88
+ Object.hasOwn(cells, step)
89
+ );
90
+ }
91
+ // The cells' own, and the field's: a diagnostic naming the discriminant or a world
92
+ // that is not drawn has no cell to sit under, and the subform's foot is inside a box
93
+ // holding the live world alone. So it draws under the field, where the control it is
94
+ // about is.
95
+ const inside = $derived((diagnostics ?? []).filter((d) => live(d.steps[0])));
96
+ const foot = $derived(
97
+ (diagnostics ?? []).filter((d) => !live(d.steps[0])).map((d) => d.diagnostic)
98
+ );
99
+
100
+ /** Take the caret: the discriminant's trigger, the field's own control. */
101
+ export function focus(): void {
102
+ if (id) document.getElementById(id)?.focus();
103
+ }
104
+ /** Land at `steps`: a live cell, and the rest of the walk inside it where the cell is
105
+ * a container; the discriminant, a dormant world's cell and a key no world declares
106
+ * land on the trigger ({@link FieldControl.focusPath}). */
107
+ export function focusPath(steps: readonly PathStep[], pos?: number): LandingBox {
108
+ if (live(steps[0]) && cellsEl) return cellsEl.focusPath(steps, pos);
109
+ focus();
110
+ return undefined;
111
+ }
64
112
  </script>
65
113
 
66
114
  <div class="qm-variant">
@@ -81,6 +129,7 @@
81
129
  {#if cells}
82
130
  {#key member}
83
131
  <ObjectField
132
+ bind:this={cellsEl}
84
133
  value={value ?? {}}
85
134
  properties={cells}
86
135
  {label}
@@ -89,9 +138,13 @@
89
138
  {describedBy}
90
139
  {contentAt}
91
140
  onCommit={(obj) => onCommit(obj)}
141
+ diagnostics={inside}
92
142
  />
93
143
  {/key}
94
144
  {/if}
145
+ <!-- The field's own foot, drawn whether or not a world is: what the live world
146
+ cannot hold belongs under the field rather than inside the box its cells sit in. -->
147
+ <DiagnosticList diagnostics={foot} />
95
148
  </div>
96
149
 
97
150
  <style>
@@ -1,4 +1,6 @@
1
1
  import type { Content, PathStep, QuillFieldSchema } from '@quillmark/wasm';
2
+ import type { DeepDiagnostic } from './diagnostics.js';
3
+ import type { LandingBox } from './leaves.js';
2
4
  interface Props {
3
5
  /** The stored container, or undefined while the field is unset. */
4
6
  value: Record<string, unknown> | undefined;
@@ -19,7 +21,14 @@ interface Props {
19
21
  onCommit: (v: Record<string, unknown> | undefined) => void;
20
22
  optionAllowed?: (value: string) => boolean;
21
23
  enumDisallowed?: 'hide' | 'disable';
24
+ /** The diagnostics routed into this field, each with its steps still to walk: one
25
+ * naming a live cell draws under it; the rest — the discriminant's, a dormant
26
+ * world's — draw at the field's foot. */
27
+ diagnostics?: readonly DeepDiagnostic[];
22
28
  }
23
- declare const VariantField: import("svelte").Component<Props, {}, "">;
29
+ declare const VariantField: import("svelte").Component<Props, {
30
+ focus: () => void;
31
+ focusPath: (steps: readonly PathStep[], pos?: number) => LandingBox;
32
+ }, "">;
24
33
  type VariantField = ReturnType<typeof VariantField>;
25
34
  export default VariantField;
@@ -3,7 +3,7 @@
3
3
  `doc` prop remounts the editor under it, so a consumer holding one mount across
4
4
  two documents gets a surface that names the live handle.
5
5
 
6
- It is A `{#key}`, not A reseed. The state a swap invalidates is not one field:
6
+ It is a `{#key}`, not a reseed. The state a swap invalidates is not one field:
7
7
  composable cards key on session id, the main card is keyed on nothing, and each
8
8
  prose leaf mounts once per stable leaf key with `createField` closing over the
9
9
  `doc` it mounted against — so after an in-place swap the main card's leaves
@@ -24,7 +24,7 @@
24
24
  import {
25
25
  addrForFieldPath,
26
26
  cardPath,
27
- elementAddrForFieldPath,
27
+ nestedAddrForFieldPath,
28
28
  fieldPathForAddr,
29
29
  type DocPath,
30
30
  type Landing
@@ -32,7 +32,16 @@
32
32
  import { errorMessage, reportError } from '../core/errors.js';
33
33
  import { bloomInside } from '../core/bloom.js';
34
34
  import { createLifespan } from '../core/teardown.js';
35
- import type { Addr, CardAddr, Diagnostic, Resolved, ResolvedField } from '@quillmark/wasm';
35
+ import type {
36
+ Addr,
37
+ CardAddr,
38
+ Diagnostic,
39
+ PathStep,
40
+ PayloadItem,
41
+ QuillFieldSchema,
42
+ Resolved,
43
+ ResolvedField
44
+ } from '@quillmark/wasm';
36
45
  import type { VisualEditorProps } from './props.js';
37
46
  import { mergeStrings, setWording } from './strings.js';
38
47
  import type { CardId, ChangeSource } from './signals.js';
@@ -40,11 +49,15 @@
40
49
  import {
41
50
  IdSeq,
42
51
  controlKind,
52
+ isContainer,
53
+ schemaAt,
43
54
  fieldModels,
44
55
  groupOrder,
45
56
  groupSections,
46
57
  groupLabel,
47
58
  cardTitle,
59
+ titleFields,
60
+ fieldValues,
48
61
  bodyEnabled,
49
62
  humanize,
50
63
  provenanceMap,
@@ -66,7 +79,7 @@
66
79
  } from './diagnostics.js';
67
80
  import { fieldDomIds, groupPanelId } from './domid.js';
68
81
  import { createLeafRegistry, type FieldControl } from './leaves.js';
69
- import { reorder, reorderTrips } from './motion.js';
82
+ import { reorder, reorderArm, reorderTrips } from './motion.js';
70
83
  import { tipsChannel } from './tips.js';
71
84
  import { patchEditorExt } from './ext.js';
72
85
  import Card from './Card.svelte';
@@ -144,6 +157,7 @@
144
157
  */
145
158
  function bump(source: ChangeSource, cardId?: CardId, at?: Addr | number): void {
146
159
  revision++;
160
+ liveTitles = {};
147
161
  const path =
148
162
  at == null ? undefined : typeof at === 'number' ? cardPath(at, liveKinds()) : pathFor(at);
149
163
  onChange?.({ source, cardId, path });
@@ -154,8 +168,11 @@
154
168
  return cardIds.indexOf(id);
155
169
  }
156
170
  /** The inverse, for the two signals that arrive as an address and nothing else (a
157
- * focus, a prose commit), off the same array the mutation boundary reads. */
158
- function cardIdOf(addr: Addr): CardId {
171
+ * focus, a prose commit), off the same array the mutation boundary reads.
172
+ * `undefined` for an address outside the live card array, as {@link pathFor}: the
173
+ * key is a host's only handle on a card, so a stale one is dropped rather than
174
+ * reported. */
175
+ function cardIdOf(addr: Addr): CardId | undefined {
159
176
  return addr.card == null ? 'main' : cardIds[addr.card];
160
177
  }
161
178
 
@@ -214,7 +231,9 @@
214
231
  lastCaretField = undefined;
215
232
  lastCaretPos = undefined;
216
233
  const field = pathFor(plain);
217
- if (field != null) onActiveLeafChange?.({ field, cardId: activeCardId });
234
+ // Both halves or neither: a stale addr names no path and no live card.
235
+ if (field != null && activeCardId != null)
236
+ onActiveLeafChange?.({ field, cardId: activeCardId });
218
237
  }
219
238
  /** No card kinds are read for a main address; the shared empty stands in so the
220
239
  * common case allocates nothing. */
@@ -261,7 +280,35 @@
261
280
  * bump `revision` (see {@link bump}). */
262
281
  function proseChanged(addr: Addr): void {
263
282
  const plain = normalize(addr);
264
- onChange?.({ source: 'prose', cardId: cardIdOf(plain), path: pathFor(plain) });
283
+ const cardId = cardIdOf(plain);
284
+ if (cardId == null) return;
285
+ refreshTitle(plain, cardId);
286
+ onChange?.({ source: 'prose', cardId, path: pathFor(plain) });
287
+ }
288
+
289
+ /** A card's header after a prose commit to a field its `{field}` title names: the
290
+ * one piece of the model that lane changes, held beside the model and dropped on
291
+ * the next bump, which rebuilds it from the document. The kind is the derived
292
+ * tree's ({@link liveKinds}), so the document is read only for a title that
293
+ * names the field. */
294
+ let liveTitles = $state<Record<CardId, string>>({});
295
+ function refreshTitle(addr: Addr, cardId: CardId): void {
296
+ const { card: at, field } = addr;
297
+ if (field == null) return;
298
+ const shown = at == null ? model.main : model.cards[at];
299
+ if (!shown) return;
300
+ const cardSchema = at == null ? quill.schema.main : quill.schema.card_kinds?.[shown.kind];
301
+ if (!titleFields(cardSchema?.ui?.title).includes(field)) return;
302
+ const live = at == null ? doc.main : doc.cards[at];
303
+ if (!live) return;
304
+ liveTitles = {
305
+ ...liveTitles,
306
+ [cardId]: cardTitle(cardSchema, shown.kind, fieldValues(live.payloadItems), undefined)
307
+ };
308
+ }
309
+ function titled(c: CardModel): CardModel {
310
+ const t = liveTitles[c.id];
311
+ return t == null ? c : { ...c, titlePlaceholder: t };
265
312
  }
266
313
 
267
314
  // ── Commit routing ──────────────────────────────────────────────────────────
@@ -381,26 +428,17 @@
381
428
  return undefined;
382
429
  }
383
430
  }
384
- // The reorder gesture's arming window: the reconcile that moves a
385
- // slot is the trip, and every other reconcile that happens to move one is not. Read
386
- // through a getter rather than passed as a value, so the flag stays out of the
387
- // template and needs no reactivity for it: `animate:` asks at apply time, which is a
388
- // microtask after the mutation and well inside the frame that disarms it.
389
- let reordering = false;
390
- let reorderFrame = 0;
391
- function armReorder(): void {
392
- reordering = true;
393
- reorderFrame = requestAnimationFrame(() => (reordering = false));
394
- }
395
- span.onEnd(() => cancelAnimationFrame(reorderFrame));
396
- const isReordering = (): boolean => reordering;
431
+ // The reorder gesture's arming window (`motion.ts`): the reconcile that moves a slot
432
+ // is the trip, and every other reconcile that happens to move one is not.
433
+ const arm = reorderArm();
434
+ span.onEnd(arm.cancel);
397
435
 
398
436
  function moveCardById(id: string, dir: -1 | 1): void {
399
437
  const from = cardIndexOf(id);
400
438
  if (from < 0) return;
401
439
  const to = from + dir;
402
440
  if (to < 0 || to >= doc.cardCount) return;
403
- armReorder();
441
+ arm.arm();
404
442
  doc.moveCard(from, to);
405
443
  const w = cardIds.slice();
406
444
  const [x] = w.splice(from, 1);
@@ -423,10 +461,11 @@
423
461
  // Ids are never reused, so an entry keyed to the gone card is unreachable rather
424
462
  // than mis-attributed to whichever card takes this position: it needs dropping
425
463
  // because nothing else will ever clear it.
426
- if ([...commitErrors.keys()].some((k) => k.startsWith(`${id}:`)))
427
- editCommitErrors((m) => {
428
- for (const k of [...m.keys()]) if (k.startsWith(`${id}:`)) m.delete(k);
429
- });
464
+ const prefix = `${id}:`;
465
+ const stale = [...commitErrors.keys()].filter((k) => k.startsWith(prefix));
466
+ // Guarded: `editCommitErrors` clones the map, and a removal matching nothing has
467
+ // nothing to re-derive.
468
+ if (stale.length) editCommitErrors((m) => stale.forEach((k) => m.delete(k)));
430
469
  // No addr: the removed card has none left and every survivor's shifted. The
431
470
  // stack changed, not a leaf, and the id is the only handle the removal leaves.
432
471
  bump('structure', id);
@@ -435,7 +474,12 @@
435
474
  const i = cardIndexOf(id);
436
475
  if (i < 0) return;
437
476
  try {
438
- doc.setCardKind(i, kind);
477
+ // Insert the retyped card ahead of the original and drop the original after.
478
+ // `insertCard` validates the whole card before it mutates and `removeCard`
479
+ // cannot throw, so a refusal leaves the document as it was; the reverse order
480
+ // would leave it a card short.
481
+ doc.insertCard({ ...doc.card(i), kind }, i);
482
+ doc.removeCard(i + 1);
439
483
  bump('structure', id, i);
440
484
  } catch (e) {
441
485
  reportError(onError, {
@@ -536,15 +580,13 @@
536
580
  isMain: boolean,
537
581
  kind: string,
538
582
  card: {
539
- payloadItems: { type: string; key?: string; value?: unknown }[];
583
+ payloadItems: readonly PayloadItem[];
540
584
  ext?: Record<string, unknown>;
541
585
  },
542
586
  cardSchema: Parameters<typeof fieldModels>[0] | undefined,
543
587
  rows: ResolvedCardRows
544
588
  ): CardModel {
545
- const values: Record<string, unknown> = {};
546
- for (const p of card.payloadItems)
547
- if (p.type === 'field' && p.key != null) values[p.key] = p.value;
589
+ const values = fieldValues(card.payloadItems);
548
590
  const fields = cardSchema ? fieldModels(cardSchema) : [];
549
591
  const sections = cardSchema
550
592
  ? groupSections(fields, groupOrder(cardSchema), (g) => groupLabel(cardSchema, g))
@@ -588,12 +630,12 @@
588
630
  // chrome, so a resolve failure degrades to no ghosts, never a blank form.
589
631
  let resolved: Resolved | undefined;
590
632
  try {
591
- resolved = quill.resolve(doc);
633
+ resolved = quill.reader(doc).resolve();
592
634
  } catch (e) {
593
635
  reportError(onError, {
594
636
  code: 'resolve-failed',
595
637
  severity: 'error',
596
- message: `quill.resolve threw; ghosted defaults fall back to none: ${errorMessage(e)}`,
638
+ message: `reader.resolve threw; ghosted defaults fall back to none: ${errorMessage(e)}`,
597
639
  cause: e
598
640
  });
599
641
  }
@@ -644,7 +686,8 @@
644
686
  // click is one discrete act, and its commonest target is the leaf already focused
645
687
  // (where landing a caret changes nothing on screen) or one off-screen, where the
646
688
  // browser's focus-scroll moves the page and leaves the caret to be hunted for.
647
- bloomInside(land(found, at.granularity === 'segment' ? undefined : at.pos));
689
+ const box = await land(found, at.granularity === 'segment' ? undefined : at.pos);
690
+ if (span.alive) bloomInside(box);
648
691
  }
649
692
  /** The active leaf's controller: the formatting popover's observation seam.
650
693
  * `undefined` for a focused form control, which holds no marks to toggle. */
@@ -683,20 +726,22 @@
683
726
 
684
727
  /**
685
728
  * Put the caret in a revealed target, at the finest grain it can take: a USV offset
686
- * in a prose leaf or an array element, a bare focus everywhere else. A form control
687
- * has no coordinate to spend an offset in (an `<input type="number">` refuses a
688
- * selection outright), which is also the whole of what a click on plate-placed ink
689
- * can mean. An element is no `createField` leaf, so the offset goes down the element
690
- * lane instead, where what it means is the row control's (`leaves.ts`).
729
+ * in a prose leaf or a nested content cell, a bare focus everywhere else. A form
730
+ * control has no coordinate to spend an offset in (an `<input type="number">`
731
+ * refuses a selection outright), which is also the whole of what a click on
732
+ * plate-placed ink can mean. A cell is no `createField` leaf, so the offset goes
733
+ * down the container's walk instead, where what it means is the leaf's (`leaves.ts`).
691
734
  *
692
735
  * Hands back the box the arrival wash blooms in, which is the landing's own
693
- * granularity rather than the registry's: an element lands in one row and says so
694
- * over that row, and everything else — the field, and a row the document has since
695
- * dropped — over the field's box.
736
+ * granularity rather than the registry's: a walk lands in one row and says so over
737
+ * the innermost row it opened, and everything else — the field, a property under a
738
+ * field-level subform, and a row the document has since dropped — over the field's
739
+ * box. Async because a row two closed boxes down exists only once the row above it
740
+ * has opened and flushed.
696
741
  */
697
- function land(found: Landed, pos: number | undefined): HTMLElement {
698
- if (found.element != null && found.control.focusElement) {
699
- return found.control.focusElement(found.element, pos) ?? found.control.el;
742
+ async function land(found: Landed, pos: number | undefined): Promise<HTMLElement> {
743
+ if (found.steps && found.control.focusPath) {
744
+ return (await found.control.focusPath(found.steps, pos)) ?? found.control.el;
700
745
  }
701
746
  if (pos != null) {
702
747
  const prose = leaves.prose(found.key);
@@ -725,7 +770,8 @@
725
770
  export async function focusField(field: DocPath): Promise<void> {
726
771
  const found = await revealLeaf(field);
727
772
  if (!found) return missed(`no mounted field at ${field}`, field);
728
- bloomInside(land(found, undefined));
773
+ const box = await land(found, undefined);
774
+ if (span.alive) bloomInside(box);
729
775
  }
730
776
  /** Seed a card of `kind` and insert it at `at` (default: the end). Returns the new
731
777
  * card's session key, or `undefined` when the quill seeds no card of that kind. */
@@ -755,11 +801,11 @@
755
801
  reportError(onError, { code: 'target-unknown', severity: 'dev', message, path });
756
802
  }
757
803
 
758
- /** What a `DocPath` resolves to in the mounted tree: a leaf key, and the array
759
- * element within it when the address names one. */
804
+ /** What a `DocPath` resolves to in the mounted tree: a leaf key, and the steps from
805
+ * that field to the leaf when the address names one inside it. */
760
806
  interface LeafTarget {
761
807
  key: string;
762
- element?: number;
808
+ steps?: PathStep[];
763
809
  }
764
810
  /** A resolved target with the handle the registry holds for it. */
765
811
  type Landed = LeafTarget & { control: FieldControl };
@@ -769,11 +815,13 @@
769
815
  * diagnostics take.
770
816
  *
771
817
  * Two rungs, because the boundary mints addresses at a finer granularity than
772
- * `Addr` can name: a `richtext[]` element surfaces as `main.keywords[0]`. The
773
- * second rung reads that trailing index segment under a field the schema declares
774
- * an array — which is why the ladder is the editor's (VISUAL_EDITOR.md §Surface):
775
- * the preview carries no schema, and truncating the address there is worse than
776
- * guessing, since `pos` is an offset into the element's own content.
818
+ * `Addr` can name: a `richtext[]` element surfaces as `main.keywords[0]`, a nested
819
+ * row's cell as `main.vectors[0].tours[2].title`. The second rung reads the steps
820
+ * past the field against the schema, rung by rung — an index under a declared
821
+ * array, a key under a declared object, variant or matrix — which is why the ladder
822
+ * is the editor's (VISUAL_EDITOR.md §Surface): the preview carries no schema, and
823
+ * truncating the address there is worse than guessing, since `pos` is an offset
824
+ * into the leaf's own content.
777
825
  */
778
826
  function leafTargetFor(field: DocPath): LeafTarget | undefined {
779
827
  const direct = addrForFieldPath(field);
@@ -781,22 +829,25 @@
781
829
  const resolved = resolveCardKey(direct, cardIds);
782
830
  return resolved ? { key: fieldKeyToString(resolved) } : undefined;
783
831
  }
784
- const element = elementAddrForFieldPath(field);
785
- if (!element || !isArrayField(element.field)) return undefined;
786
- const resolved = resolveCardKey(element.field, cardIds);
787
- return resolved ? { key: fieldKeyToString(resolved), element: element.index } : undefined;
832
+ const nested = nestedAddrForFieldPath(field);
833
+ if (!nested) return undefined;
834
+ const declared = declaredField(nested.field);
835
+ if (!declared || !isContainer(controlKind(declared))) return undefined;
836
+ if (!schemaAt(declared, nested.steps)) return undefined;
837
+ const resolved = resolveCardKey(nested.field, cardIds);
838
+ return resolved ? { key: fieldKeyToString(resolved), steps: nested.steps } : undefined;
788
839
  }
789
840
 
790
- /** The guard the element rung stands on. Asked of `controlKind`, so what the ladder
791
- * tests is the control the tree mounted — the one holding a `focusElement` — and
792
- * not a second reading of the schema beside it. */
793
- function isArrayField(addr: Addr): boolean {
794
- if (addr.field == null) return false;
841
+ /** The schema the field at `addr` declares, off the live card's kind: the guard the
842
+ * nested rung stands on. Asked of `controlKind` and `schemaAt`, so what the ladder
843
+ * tests is the control the tree mounted — the one holding a `focusPath` — and not a
844
+ * second reading of the schema beside it. */
845
+ function declaredField(addr: Addr): QuillFieldSchema | undefined {
846
+ if (addr.field == null) return undefined;
795
847
  const kind = addr.card == null ? undefined : model.cards[addr.card]?.kind;
796
848
  const schema = quill.schema;
797
849
  const card = addr.card == null ? schema.main : kind ? schema.card_kinds?.[kind] : undefined;
798
- const declared = card?.fields?.[addr.field];
799
- return !!declared && controlKind(declared) === 'array';
850
+ return card?.fields?.[addr.field];
800
851
  }
801
852
  </script>
802
853
 
@@ -807,7 +858,7 @@
807
858
  <div class="qm-primary">
808
859
  <Card
809
860
  bind:this={mainCard}
810
- card={model.main}
861
+ card={titled(model.main)}
811
862
  {doc}
812
863
  {quill}
813
864
  isFirst={true}
@@ -836,10 +887,10 @@
836
887
  rather than being slid across. It is also the shape `animate:` asks for, being the
837
888
  keyed block's only child. -->
838
889
  {#each model.cards as c, i (c.id)}
839
- <div class="qm-card-slot" bind:this={slotEls[i]} animate:reorder={isReordering}>
890
+ <div class="qm-card-slot" bind:this={slotEls[i]} animate:reorder={arm.armed}>
840
891
  <Card
841
892
  bind:this={cardRefs[i]}
842
- card={c}
893
+ card={titled(c)}
843
894
  {doc}
844
895
  {quill}
845
896
  isFirst={i === 0}
@@ -12,9 +12,9 @@
12
12
  Every selector here is `:where()`-wrapped, so a recipe computes at specificity zero.
13
13
  That is the point twice over: a consumer's stylesheet beats the chrome without
14
14
  `!important`, and a component's own scoped block overrides its shared base —
15
- `.qm-card-delete` recolours a `.qm-chip` under the pointer, `.qm-json` adds the monospace face over
16
- `.qm-input` — with no specificity fight. Unlayered with it, so a host's own layered
17
- reset never reaches these rules whatever order the sheets load in (`core/theme.css`). */
15
+ `.qm-card-delete` recolours a `.qm-chip` under the pointer — with no specificity
16
+ fight. Unlayered with it, so a host's own layered reset never reaches these rules
17
+ whatever order the sheets load in (`core/theme.css`). */
18
18
 
19
19
  /* The themed focus ring in place of the raw UA outline, as a
20
20
  rule rather than a pair of rungs each control assembles. `--_qm-ring`
@@ -63,11 +63,10 @@
63
63
  }
64
64
 
65
65
  /* A typed value's box (every scalar control and every prose leaf): the text and
66
- numeric inputs, the array's JSON textarea, the enum trigger, the date field, and
67
- the inline richtext leaves. One rule, because a
68
- leaf that agrees with the input beside it "by construction" has to draw the same
69
- declarations, not a copy of them: five hand-kept copies agree until one is
70
- edited, and nothing raises it (no literal is minted, so `check:style` stays
66
+ numeric inputs, the enum trigger, the date field, and the inline richtext leaves.
67
+ One rule, because a leaf that agrees with the input beside it "by construction" has
68
+ to draw the same declarations, not a copy of them: five hand-kept copies agree
69
+ until one is edited, and nothing raises it (no literal is minted, so `check:style` stays
71
70
  green while a control renders subtly the wrong height).
72
71
 
73
72
  Leading sits on the wide list rather than on the box's own, because a box's
@@ -91,7 +90,7 @@
91
90
  color: var(--_qm-ink);
92
91
  caret-color: currentColor;
93
92
  }
94
- /* The box is A fill and not an edge (ARCHITECTURE §Styling), and the fill is the
93
+ /* The box is a fill and not an edge (ARCHITECTURE §Styling), and the fill is the
95
94
  well rung: a field is a hole cut into the card rather than a plane of its own,
96
95
  and with no edge to carry it the step is the whole of what says so. */
97
96
  :where(.qm-control-box),
@@ -256,6 +255,37 @@
256
255
  transform: translate(-50%, -50%);
257
256
  }
258
257
 
258
+ /* The capacity ladder: how many tracks a grid of fields steps to, off the width of
259
+ the query container it stands in. One recipe, because three grids read it — a section's
260
+ fields (`Card.svelte`), a subform's properties (`ObjectField`), each over its own
261
+ container, and a compact matrix's members (`MatrixField`) — and a ladder kept in
262
+ several files agrees until one is retuned. Capacity is
263
+ the container's, not JavaScript's: nothing measures, so there is no observer to loop
264
+ and no re-packing to restructure the DOM under a prose leaf.
265
+
266
+ It steps 1 → 2 → 4, skipping 3: each rung is the width at which a track still clears
267
+ the comfortable field minimum, and an even capacity is what lets the half a subform
268
+ of one takes (`--cols-half`) land on a track boundary. A field ends on its track's
269
+ own edge, so a rung is `n * 220px + gaps`. A subform three rungs in stands three
270
+ `--_qm-nest` and three verticals narrower than its section, and steps down where the
271
+ section does not: the count is the width the tracks actually get. */
272
+ :where(.qm-tracks) {
273
+ --cols: 1;
274
+ --cols-half: 1;
275
+ }
276
+ @container (min-width: 28rem) {
277
+ :where(.qm-tracks) {
278
+ --cols: 2;
279
+ --cols-half: 1;
280
+ }
281
+ }
282
+ @container (min-width: 57rem) {
283
+ :where(.qm-tracks) {
284
+ --cols: 4;
285
+ --cols-half: 2;
286
+ }
287
+ }
288
+
259
289
  /* A floating list of choices: the enum's listbox and the card stack's kind menu.
260
290
  Both portal into the nearest `[data-qm-root]`, and both are picked from, so the
261
291
  surface (its edge, its inset, its ink) and the item (its pad, its radius, the
@@ -1,4 +1,4 @@
1
- import type { Diagnostic } from '@quillmark/wasm';
1
+ import type { Diagnostic, PathStep } from '@quillmark/wasm';
2
2
  /** A field's routing address, and `/core`'s `Addr` structurally: an `Addr` is a
3
3
  * positional `FieldKey`, so `nearestAddrForFieldPath` is the path→key walk and
4
4
  * routing carries no second copy of the grammar. `card` is `undefined` for the main
@@ -40,10 +40,34 @@ export interface RoutedDiagnostic {
40
40
  * Completeness stays a read the host makes on `quill.validate(doc)`.
41
41
  */
42
42
  export declare function routeAndResolve(diagnostics: Diagnostic[] | undefined, cardIds: readonly string[]): RoutedDiagnostic[];
43
+ /** A diagnostic with the steps still to walk from the control holding it. */
44
+ export interface DeepDiagnostic {
45
+ steps: readonly PathStep[];
46
+ diagnostic: Diagnostic;
47
+ }
48
+ /** A field's routed diagnostics, each with its steps past the field: none for a
49
+ * diagnostic anchored at the field itself, or one whose path is not a nested one
50
+ * (a local commit error carries the field's own path). */
51
+ export declare function deepen(diagnostics: Diagnostic[] | undefined): DeepDiagnostic[];
52
+ /** One rung of the walk: the diagnostics anchored here, and the rest bucketed by the
53
+ * step they take next, each with that step consumed. */
54
+ export interface DeepSplit {
55
+ here: Diagnostic[];
56
+ below: Map<PathStep, DeepDiagnostic[]>;
57
+ }
58
+ export declare function splitDeep(list: readonly DeepDiagnostic[] | undefined): DeepSplit;
59
+ /** The diagnostics a rung cannot hand down: the ones anchored here, plus every one
60
+ * whose next step names nothing in `held`. Drawn at the rung's own foot, so a leaf the
61
+ * tree does not draw still has its message read somewhere the user can see. */
62
+ export declare function unrouted(split: DeepSplit, held: (step: PathStep) => boolean): Diagnostic[];
43
63
  /**
44
64
  * Merge N routed groups into `Map<fieldKeyToString(key), Diagnostic[]>`.
45
- * Dedupes an identical `(severity, message)` pair landing on the same key from
65
+ * Dedupes an identical `(path, severity, message)` quad landing on the same key from
46
66
  * more than one group (e.g. the same error present in both `validate()` and
47
67
  * an external feed). Producer order within a key.
68
+ *
69
+ * The key is the field, but the walk past it is the `path`'s ({@link deepen}), so two
70
+ * cells of one container share a key and differ only there: without `path` in the
71
+ * quad, one message failing two rows draws on the first row alone.
48
72
  */
49
73
  export declare function mergeDiagnostics(...groups: RoutedDiagnostic[][]): Map<string, Diagnostic[]>;