@quillmark/svelte 0.1.1 → 0.2.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 (64) hide show
  1. package/README.md +15 -11
  2. package/THEMING.md +42 -23
  3. package/dist/core/address.d.ts +39 -4
  4. package/dist/core/address.js +47 -19
  5. package/dist/core/codec/encode.js +4 -1
  6. package/dist/core/codec/field.d.ts +1 -1
  7. package/dist/core/codec/prose.css +41 -18
  8. package/dist/core/codec/table.js +3 -1
  9. package/dist/core/errors.d.ts +15 -5
  10. package/dist/core/index.d.ts +1 -1
  11. package/dist/core/index.js +5 -4
  12. package/dist/core/lifecycle.d.ts +21 -5
  13. package/dist/core/lifecycle.js +34 -8
  14. package/dist/core/rebind.svelte.d.ts +16 -2
  15. package/dist/core/rebind.svelte.js +26 -8
  16. package/dist/core/theme.css +43 -18
  17. package/dist/preset/recipes.css +55 -8
  18. package/dist/preset/scale.css +5 -4
  19. package/dist/preview/Preview.svelte +10 -9
  20. package/dist/preview/Preview.svelte.d.ts +5 -5
  21. package/dist/preview/bridge.d.ts +6 -4
  22. package/dist/preview/bridge.js +17 -7
  23. package/dist/preview/controller.d.ts +13 -6
  24. package/dist/preview/controller.js +5 -2
  25. package/dist/preview/geometry.d.ts +16 -1
  26. package/dist/preview/geometry.js +20 -0
  27. package/dist/preview/index.d.ts +1 -0
  28. package/dist/preview/index.js +7 -0
  29. package/dist/preview/overlay.d.ts +2 -1
  30. package/dist/preview/overlay.js +24 -9
  31. package/dist/preview/paint.js +4 -0
  32. package/dist/visual/ArrayField.svelte +26 -20
  33. package/dist/visual/ArrayField.svelte.d.ts +4 -3
  34. package/dist/visual/Card.svelte +21 -19
  35. package/dist/visual/Card.svelte.d.ts +8 -3
  36. package/dist/visual/DiagnosticList.svelte +1 -1
  37. package/dist/visual/EnumField.svelte +34 -14
  38. package/dist/visual/EnumField.svelte.d.ts +4 -3
  39. package/dist/visual/Field.svelte +87 -25
  40. package/dist/visual/Field.svelte.d.ts +11 -7
  41. package/dist/visual/FieldHint.svelte +59 -10
  42. package/dist/visual/FormatPopover.svelte +3 -1
  43. package/dist/visual/ObjectField.svelte +21 -2
  44. package/dist/visual/ObjectField.svelte.d.ts +3 -1
  45. package/dist/visual/ProseArrayElement.svelte +1 -6
  46. package/dist/visual/ProseArrayElement.svelte.d.ts +0 -1
  47. package/dist/visual/ProseField.svelte +20 -12
  48. package/dist/visual/ProseField.svelte.d.ts +5 -3
  49. package/dist/visual/TipsCard.svelte +1 -1
  50. package/dist/visual/VisualEditor.svelte +12 -13
  51. package/dist/visual/VisualEditor.svelte.d.ts +2 -3
  52. package/dist/visual/VisualEditorInner.svelte +140 -66
  53. package/dist/visual/VisualEditorInner.svelte.d.ts +2 -3
  54. package/dist/visual/controls.css +14 -11
  55. package/dist/visual/diagnostics.d.ts +12 -20
  56. package/dist/visual/diagnostics.js +6 -18
  57. package/dist/visual/index.d.ts +1 -0
  58. package/dist/visual/index.js +7 -0
  59. package/dist/visual/leaves.d.ts +47 -0
  60. package/dist/visual/leaves.js +31 -0
  61. package/dist/visual/props.d.ts +27 -6
  62. package/dist/visual/tips.d.ts +3 -1
  63. package/dist/visual/tips.js +5 -2
  64. package/package.json +2 -2
@@ -16,7 +16,7 @@
16
16
  • commit routing: prose leaves lower to `applyChange` (in the codec); scalars/
17
17
  arrays/objects go through the typed `writer`; structure through the mutators;
18
18
  • focus + the bridge outputs (`onActiveLeafChange`, `onCaretMove`) and the
19
- `setCaret(hit)` entry wired to the preview;
19
+ `setCaret(at)` entry wired to the preview;
20
20
  • the ONE formatting popover (`FormatPopover`, mounted once, observing the
21
21
  active leaf via `getActiveLeaf`) and diagnostics routing (`diagnostics.ts`:
22
22
  quill.validate + local commit errors + the external `diagnostics` prop,
@@ -34,24 +34,25 @@
34
34
  import { onDestroy, tick } from 'svelte';
35
35
  import { DropdownMenu } from 'bits-ui';
36
36
  import { isQuillmarkError, MAIN_CARD_ADDR } from '@quillmark/wasm';
37
- import { cardPath, fieldPathForAddr, type DocPath } from '../core/address.js';
37
+ import {
38
+ addrForFieldPath,
39
+ cardPath,
40
+ elementAddrForFieldPath,
41
+ fieldPathForAddr,
42
+ type DocPath,
43
+ type Landing
44
+ } from '../core/address.js';
38
45
  import { errorMessage, reportError } from '../core/errors.js';
39
46
  import { bloomInside } from '../core/bloom.js';
40
47
  import { createLifespan } from '../core/teardown.js';
41
- import type {
42
- Addr,
43
- CardAddr,
44
- ContentHit,
45
- Diagnostic,
46
- Resolved,
47
- ResolvedField
48
- } from '@quillmark/wasm';
48
+ import type { Addr, CardAddr, Diagnostic, Resolved, ResolvedField } from '@quillmark/wasm';
49
49
  import type { VisualEditorProps } from './props.js';
50
50
  import { mergeStrings, setWording } from './strings.js';
51
51
  import type { CardId, ChangeSource } from './signals.js';
52
52
  import type { FieldController } from '../core/codec/index.js';
53
53
  import {
54
54
  IdSeq,
55
+ controlKind,
55
56
  fieldModels,
56
57
  groupOrder,
57
58
  groupSections,
@@ -71,7 +72,6 @@
71
72
  } from './structure.js';
72
73
  import {
73
74
  fieldKeyToString,
74
- parsePath,
75
75
  resolveCardKey,
76
76
  routeAndResolve,
77
77
  mergeDiagnostics,
@@ -79,6 +79,7 @@
79
79
  type RoutedDiagnostic
80
80
  } from './diagnostics.js';
81
81
  import { fieldDomIds, groupPanelId } from './domid.js';
82
+ import { createLeafRegistry, type FieldControl } from './leaves.js';
82
83
  import { reorder } from './motion.js';
83
84
  import { tipsChannel } from './tips.js';
84
85
  import { patchEditorExt } from './ext.js';
@@ -98,6 +99,7 @@
98
99
  onError,
99
100
  diagnostics,
100
101
  enumOptionAllowed,
102
+ enumDisallowed = 'disable',
101
103
  strings,
102
104
  formatDiagnostic,
103
105
  class: className,
@@ -187,8 +189,11 @@
187
189
  const span = createLifespan();
188
190
  onDestroy(() => span.end());
189
191
 
190
- // ── Leaf registry (setCaret target lookup + the active-leaf seam) ───────────
191
- const leaves = new Map<string, FieldController>();
192
+ // ── Leaf registry (landing target lookup + the active-leaf seam) ────────────
193
+ // Every mounted field is in it, prose leaf and form control alike (`leaves.ts`):
194
+ // a landing needs a focus and a box to bloom, which every control has, and the
195
+ // codec lane is asked for only where there is an offset to place.
196
+ const leaves = createLeafRegistry();
192
197
  // Each leaf unregisters itself on its own teardown; this covers the surface
193
198
  // going away as a whole, where a leaf's cleanup order relative to the parent's
194
199
  // is Svelte's business and not something to depend on.
@@ -204,12 +209,6 @@
204
209
  /** The stack's own element: it carries `data-qm-root`, so it is what the kind
205
210
  * menu portals INTO, the way each leaf's surface resolves its nearest root. */
206
211
  let rootEl = $state<HTMLElement | undefined>(undefined);
207
- function register(key: string, controller: FieldController): void {
208
- leaves.set(key, controller);
209
- }
210
- function unregister(key: string): void {
211
- leaves.delete(key);
212
- }
213
212
 
214
213
  // ── Focus + bridge outputs ──────────────────────────────────────────────────
215
214
  // `activeCardId` is the id-keyed half of `activeAddr` (whose `card` is positional),
@@ -670,64 +669,93 @@
670
669
 
671
670
  // ── Public entry points ─────────────────────────────────────────────────────
672
671
  /**
673
- * Resolve a preview `ContentHit` to a mounted leaf and place its caret.
672
+ * Resolve a preview {@link Landing} to a mounted leaf and land in it.
674
673
  *
675
674
  * Async because the reveal has to RENDER before the landing: a collapsed group
676
675
  * is `inert`, which swallows a focus silently, so a caret placed in the same
677
676
  * tick as the reveal would go nowhere and report nothing. The consumer's
678
- * `onCaretPick` ignores the promise: awaiting it is for a caller that wants to
677
+ * `onPick` ignores the promise: awaiting it is for a caller that wants to
679
678
  * observe where the caret went.
680
679
  *
681
680
  * A destroy lands INSIDE this one: `leaf` is looked up before the flush and
682
681
  * dispatched into after it, and a PM view destroyed in that window throws on the
683
- * dispatch. A consumer's `onCaretPick` outlives the surface it points at too, so
684
- * the span is asked on the way in as well as after the await.
682
+ * dispatch. A consumer's `onPick` outlives the surface it points at too, so the
683
+ * span is asked on the way in as well as after the await.
685
684
  */
686
- export async function setCaret(hit: ContentHit): Promise<void> {
687
- const leaf = await revealLeaf(hit.field);
688
- if (!leaf) return;
685
+ export async function setCaret(at: Landing): Promise<void> {
686
+ const found = await revealLeaf(at.field);
687
+ if (!found) return missed(`no mounted field at ${at.field}`, at.field);
689
688
  // A `'segment'` hit landed on origin-less ink (list markers, a code fence's
690
689
  // interior): `pos` is the segment START, not a cluster-exact caret
691
- // (HitGranularity), so just focus the leaf rather than snap the caret to a
692
- // spot the click did not resolve. `'cluster'` (and an absent granularity:
693
- // the backend did not report it, treat as exact) places the caret.
694
- if (hit.granularity === 'segment') leaf.focus();
695
- else leaf.setCaret(hit.pos);
690
+ // (HitGranularity), so it is dropped rather than snapped to a spot the click
691
+ // did not resolve. `'cluster'` (and an absent granularity: the backend did not
692
+ // report it, treat as exact) places the caret. An absent `pos` is the PLACEMENT
693
+ // rung and reaches the same floor.
694
+ land(found, at.granularity === 'segment' ? undefined : at.pos);
696
695
  // The arrival cue. Unconditional, unlike the preview side's change-guarded
697
696
  // bloom: a preview click is one discrete act, and its commonest target is the
698
697
  // leaf ALREADY focused (where landing a caret changes nothing on screen) or
699
698
  // one off-screen, where the browser's focus-scroll moves the page and leaves
700
699
  // the caret to be hunted for in a long form.
701
- bloomInside(leaf.el);
700
+ bloomInside(found.control.el);
702
701
  }
703
- /** The active leaf's controller: the formatting popover's observation seam. */
702
+ /** The active leaf's controller: the formatting popover's observation seam.
703
+ * `undefined` for a focused form control, which holds no marks to toggle. */
704
704
  export function getActiveLeaf(): FieldController | undefined {
705
705
  if (!activeAddr) return undefined;
706
706
  const card = activeAddr.card != null ? activeCardId : undefined;
707
- return leaves.get(fieldKeyToString({ card, field: activeAddr.field }));
707
+ return leaves.prose(fieldKeyToString({ card, field: activeAddr.field }));
708
708
  }
709
709
 
710
710
  /**
711
- * Reveal the leaf at `field` and hand back its controller, once the reveal has
712
- * RENDERED. The shared half of the two landing verbs: a collapsed group is clipped
713
- * to zero height and sits inside an `inert` panel, which swallows a focus silently,
714
- * so a caret placed in the same tick as the reveal goes nowhere and reports
715
- * nothing. Exactly one card holds the key; the rest do nothing.
711
+ * Reveal the field at `field` and hand back its landing handle plus the target it
712
+ * resolved to, once the reveal has RENDERED. The shared half of the two landing
713
+ * verbs: a collapsed group is clipped to zero height and sits inside an `inert`
714
+ * panel, which swallows a focus silently, so a caret placed in the same tick as the
715
+ * reveal goes nowhere and reports nothing. Exactly one card holds the key; the rest
716
+ * do nothing.
716
717
  *
717
- * A destroy lands INSIDE this: the leaf is looked up before the flush and
718
+ * The target comes back with the handle because the caller needs its key to ask for
719
+ * the codec lane (`leaves.prose`), and minting it twice would be two parses of one
720
+ * path. The reveal itself is the KEY's: a group holds the field, and an element
721
+ * address opens the same group its array sits in.
722
+ *
723
+ * A destroy lands INSIDE this: the field is looked up before the flush and
718
724
  * dispatched into after it, and a PM view destroyed in that window throws on the
719
725
  * dispatch. A consumer's call outlives the surface it points at too, so the span is
720
726
  * asked on the way in as well as after the await.
721
727
  */
722
- async function revealLeaf(field: DocPath): Promise<FieldController | undefined> {
728
+ async function revealLeaf(field: DocPath): Promise<Landed | undefined> {
723
729
  if (!span.alive) return undefined;
724
- const key = leafKeyForHit(field);
725
- const leaf = key == null ? undefined : leaves.get(key);
726
- if (!leaf) return undefined;
727
- mainCard?.revealLeaf(key!);
728
- for (const card of cardRefs) card?.revealLeaf(key!);
730
+ const target = leafTargetFor(field);
731
+ const control = target && leaves.control(target.key);
732
+ if (!target || !control) return undefined;
733
+ mainCard?.revealLeaf(target.key);
734
+ for (const card of cardRefs) card?.revealLeaf(target.key);
729
735
  if (!(await span.resumes(tick()))) return undefined;
730
- return leaf;
736
+ return { ...target, control };
737
+ }
738
+
739
+ /**
740
+ * Put the caret in a revealed target, at the finest grain it can take.
741
+ *
742
+ * - An ELEMENT address focuses that row, with no caret inside it: an array element
743
+ * is no `createField` leaf and its handle is a bare focus.
744
+ * - A prose leaf with a `pos` takes the caret at that USV offset.
745
+ * - Everything else takes the focus alone: the offset is a position in RENDERED
746
+ * content and a form control has no coordinate to spend it in (an
747
+ * `<input type="number">` refuses a selection outright), which is also the whole
748
+ * of what a click on plate-placed ink can mean.
749
+ */
750
+ function land(found: Landed, pos: number | undefined): void {
751
+ if (found.element != null && found.control.focusElement) {
752
+ return found.control.focusElement(found.element);
753
+ }
754
+ if (pos != null) {
755
+ const prose = leaves.prose(found.key);
756
+ if (prose) return prose.setCaret(pos);
757
+ }
758
+ found.control.focus();
731
759
  }
732
760
 
733
761
  // ── The verbs, as instance exports ──────────────────────────────────────────
@@ -738,15 +766,24 @@
738
766
  // card — so a host drives them with what the hooks handed it.
739
767
  //
740
768
  // A target the surface does not hold is a NO-OP that reports `target-unknown` at
741
- // `dev`: the chrome cannot mint a bad one, so it only ever fires on a host holding
742
- // a key from a previous session or a card already removed.
769
+ // `dev`: a key from a previous session or a card already removed, or a path naming
770
+ // no declared field — an ELEMENT path (`main.references.0`) whose array is
771
+ // undeclared or unmounted included, that being the only way one misses. `setCaret`
772
+ // reports it too, being the verb a preview click drives: a landing that resolved
773
+ // nothing is indistinguishable from one that landed, and a consumer wiring the
774
+ // bridge reads the difference off nothing else.
743
775
 
744
- /** Reveal and focus the leaf at `field`, without placing a caret inside it. */
776
+ /**
777
+ * Reveal and focus the field at `field`, without placing a caret inside it. Any
778
+ * mounted field: a prose leaf takes its view's focus, a form control takes the same
779
+ * handoff a click on its label does (`Field`, `leaves.ts`), and an element address
780
+ * takes its own row.
781
+ */
745
782
  export async function focusField(field: DocPath): Promise<void> {
746
- const leaf = await revealLeaf(field);
747
- if (!leaf) return missed(`no mounted leaf at ${field}`, field);
748
- leaf.focus();
749
- bloomInside(leaf.el);
783
+ const found = await revealLeaf(field);
784
+ if (!found) return missed(`no mounted field at ${field}`, field);
785
+ land(found, undefined);
786
+ bloomInside(found.control.el);
750
787
  }
751
788
  /** Seed a card of `kind` and insert it at `at` (default: the end). Returns the new
752
789
  * card's session key, or `undefined` when the quill seeds no card of that kind. */
@@ -777,14 +814,51 @@
777
814
  reportError(onError, { code: 'target-unknown', severity: 'dev', message, path });
778
815
  }
779
816
 
780
- /** Map a `ContentHit.field` (a canonical `DocPath`) to a mounted leaf key: the
781
- * same `addrForFieldPath` route the diagnostics take, the absolute card index
782
- * resolved to its live stable id, then the shared `fieldKeyToString` form. */
783
- function leafKeyForHit(field: string): string | undefined {
784
- const key = parsePath(field);
785
- if (!key) return undefined;
786
- const resolved = resolveCardKey(key, cardIds);
787
- return resolved ? fieldKeyToString(resolved) : undefined;
817
+ /** What a `DocPath` resolves to in the mounted tree: a leaf key, and the array
818
+ * ELEMENT within it when the address names one. */
819
+ interface LeafTarget {
820
+ key: string;
821
+ element?: number;
822
+ }
823
+ /** A resolved target with the handle the registry holds for it. */
824
+ type Landed = LeafTarget & { control: FieldControl };
825
+
826
+ /**
827
+ * Map a landing's `field` (a canonical `DocPath`) to a mounted target: the
828
+ * `addrForFieldPath` route the diagnostics take, the absolute card index resolved
829
+ * to its live stable id, then the shared `fieldKeyToString` form.
830
+ *
831
+ * TWO RUNGS, because the boundary mints addresses at a finer granularity than
832
+ * `Addr` can name: a `richtext[]` element surfaces as `main.references.0`, which
833
+ * the grammar reads as a field literally named `"0"`. The second rung reads that
834
+ * trailing segment as an INDEX, under a field the schema declares an array — which
835
+ * is why the ladder is the editor's (VISUAL_EDITOR.md §Surface): the preview
836
+ * carries no schema, and truncating the address there is worse than guessing, since
837
+ * `pos` is an offset into the ELEMENT's own content.
838
+ */
839
+ function leafTargetFor(field: DocPath): LeafTarget | undefined {
840
+ const direct = addrForFieldPath(field);
841
+ if (direct) {
842
+ const resolved = resolveCardKey(direct, cardIds);
843
+ return resolved ? { key: fieldKeyToString(resolved) } : undefined;
844
+ }
845
+ const element = elementAddrForFieldPath(field);
846
+ if (!element || !isArrayField(element.field)) return undefined;
847
+ const resolved = resolveCardKey(element.field, cardIds);
848
+ return resolved ? { key: fieldKeyToString(resolved), element: element.index } : undefined;
849
+ }
850
+
851
+ /** Whether `addr` names a field this document renders as an array repeater: the
852
+ * guard the element rung stands on. Asked of `controlKind`, so what the ladder
853
+ * tests is the control the tree mounted — the one holding a `focusElement` — and
854
+ * not a second reading of the schema beside it. */
855
+ function isArrayField(addr: Addr): boolean {
856
+ if (addr.field == null) return false;
857
+ const kind = addr.card == null ? undefined : model.cards[addr.card]?.kind;
858
+ const schema = quill.schema;
859
+ const card = addr.card == null ? schema.main : kind ? schema.card_kinds?.[kind] : undefined;
860
+ const declared = card?.fields?.[addr.field];
861
+ return !!declared && controlKind(declared) === 'array';
788
862
  }
789
863
  </script>
790
864
 
@@ -803,12 +877,12 @@
803
877
  isLast={true}
804
878
  {kinds}
805
879
  ops={opsFor('main', true)}
880
+ {enumDisallowed}
806
881
  onFocus={handleFocus}
807
882
  onCaretMove={handleCaret}
808
883
  onChange={proseChanged}
809
884
  {onError}
810
- {register}
811
- {unregister}
885
+ {leaves}
812
886
  />
813
887
 
814
888
  <!-- The tips card: a fixed slot after `main`, ahead of the cards, so
@@ -837,12 +911,12 @@
837
911
  isLast={i === model.cards.length - 1}
838
912
  {kinds}
839
913
  ops={opsFor(c.id, false)}
914
+ {enumDisallowed}
840
915
  onFocus={handleFocus}
841
916
  onCaretMove={handleCaret}
842
917
  onChange={proseChanged}
843
918
  {onError}
844
- {register}
845
- {unregister}
919
+ {leaves}
846
920
  />
847
921
  {@render addAffordance(i + 1)}
848
922
  </div>
@@ -1,11 +1,10 @@
1
- import { type DocPath } from '../core/address.js';
2
- import type { ContentHit } from '@quillmark/wasm';
1
+ import { type DocPath, type Landing } from '../core/address.js';
3
2
  import type { VisualEditorProps } from './props.js';
4
3
  import type { CardId } from './signals.js';
5
4
  import type { FieldController } from '../core/codec/index.js';
6
5
  import './controls.css';
7
6
  declare const VisualEditorInner: import("svelte").Component<VisualEditorProps, {
8
- setCaret: (hit: ContentHit) => Promise<void>;
7
+ setCaret: (at: Landing) => Promise<void>;
9
8
  getActiveLeaf: () => FieldController | undefined;
10
9
  focusField: (field: DocPath) => Promise<void>;
11
10
  insertCard: (kind: string, at?: number) => CardId | undefined;
@@ -17,7 +17,7 @@
17
17
 
18
18
  @layer qm.chrome {
19
19
  /* The themed focus ring in place of the raw UA outline, as a
20
- RULE rather than a pair of rungs each control assembles. `--_qm-ring-focus`
20
+ RULE rather than a pair of rungs each control assembles. `--_qm-ring`
21
21
  fixes the VALUE; which properties draw a ring, and on what state, is the other
22
22
  half of the decision, and `check:style` sees a minted literal but never a
23
23
  divergent rule. A control opts in by carrying the marker class.
@@ -28,7 +28,7 @@
28
28
  it. That is the only distinction, and this pair is the whole of it. */
29
29
  .qm-focus-ring:focus-visible,
30
30
  .qm-focus-ring-within:focus-within {
31
- outline: var(--_qm-ring-focus);
31
+ outline: var(--_qm-ring);
32
32
  outline-offset: var(--_qm-ring-offset);
33
33
  }
34
34
 
@@ -46,9 +46,10 @@
46
46
  DIFFERENT rungs, which no gate can see: `check:style`'s type axis holds that
47
47
  each side reads a rung, never that both read the same one.
48
48
 
49
- The list is wider than either recipe's chrome: the body leaf withholds the box
49
+ The list is wider than either recipe's chrome: a prose leaf withholds the box
50
50
  and still reads the ramp, so `.qm-prose` is
51
- here and not below.
51
+ here and not below. The BODY leaf steps off this rung onto `--_qm-text-paper`
52
+ from its own block (`ProseField`).
52
53
 
53
54
  `font` is a shorthand carrying `line-height`, so each family's leading is
54
55
  declared in the rules BELOW and would be dropped in silence above them. */
@@ -70,9 +71,11 @@
70
71
  green while a control renders subtly the wrong height).
71
72
 
72
73
  LEADING sits on the wide list rather than on the box's own, because a box's
73
- height is a padding rung plus a line box plus two hairlines: a leaf and the
74
- input beside it agree on height exactly as long as they agree on leading, which
75
- is the whole of what "by construction" buys.
74
+ height is an inset rung plus a line box plus two hairlines: a leaf and the
75
+ input beside it agree on height exactly as long as they agree on leading and on
76
+ size, which is the whole of what "by construction" buys. The body leaf keeps the
77
+ leading and steps off the size, which costs it nothing: it draws no box and
78
+ stands in no row.
76
79
 
77
80
  COLOUR is the trap one property over: `font` does not carry it either, so an
78
81
  input or trigger left off this line falls to the UA's own text colour: a second
@@ -90,7 +93,7 @@
90
93
  }
91
94
  .qm-control-box,
92
95
  .qm-input {
93
- padding: var(--_qm-space) var(--_qm-space-2);
96
+ padding: var(--_qm-inset-control);
94
97
  border: var(--_qm-border-width) solid var(--_qm-border);
95
98
  border-radius: var(--_qm-radius-inner);
96
99
  background: var(--_qm-surface);
@@ -137,7 +140,7 @@
137
140
  which is why it is here and not on one family, and it is what keeps a button off
138
141
  the host page's leading the way the size rung keeps it off the host page's body
139
142
  size. A caller whose button carries a LABEL restates the rung it wants from its
140
- own block: the tips foot takes the meta rung, which is that surface's decision,
143
+ own block: the tips foot takes the label rung, which is that surface's decision,
141
144
  not the family's. */
142
145
  .qm-icon-btn,
143
146
  .qm-add-affordance {
@@ -244,10 +247,10 @@
244
247
  -webkit-backdrop-filter: blur(var(--_qm-blur));
245
248
  border: var(--_qm-border-width) solid var(--_qm-border);
246
249
  border-radius: var(--_qm-radius);
247
- animation: qm-pop-in var(--_qm-duration-fast) var(--_qm-ease-arrive);
250
+ animation: qm-pop-in var(--_qm-duration-fast) var(--_qm-ease-reverse);
248
251
  }
249
252
  .qm-popover-surface[data-ending-style] {
250
- animation: qm-pop-out var(--_qm-duration-fast) var(--_qm-ease-depart);
253
+ animation: qm-pop-out var(--_qm-duration-fast) var(--_qm-ease-reverse);
251
254
  }
252
255
  @keyframes qm-pop-in {
253
256
  from {
@@ -1,28 +1,20 @@
1
1
  import type { Diagnostic } from '@quillmark/wasm';
2
- /** A field's routing address. `card` is `undefined` for the main card; a
3
- * composable card slot is a stable session id (the editor's own bookkeeping)
4
- * once resolved, or an ABSOLUTE document-array index straight off a parsed
5
- * `DocPath` (resolve via {@link resolveCardKey} before merging with id-keyed
6
- * sources). `field` `undefined` addresses the body (the field-less leaf). */
2
+ /** A field's routing address, and `/core`'s `Addr` structurally: an `Addr` IS a
3
+ * positional `FieldKey`, so `addrForFieldPath` is the path→key walk and routing
4
+ * carries no second copy of the grammar. `card` is `undefined` for the main card; a
5
+ * composable card slot is a stable session id (the editor's own bookkeeping) once
6
+ * resolved, or an ABSOLUTE document-array index straight off a parsed `DocPath`
7
+ * (resolve via {@link resolveCardKey} before merging with id-keyed sources). `field`
8
+ * `undefined` addresses the body (the field-less leaf). */
7
9
  export interface FieldKey {
8
10
  card?: string | number;
9
11
  field?: string;
10
12
  }
11
13
  /** Canonical string key for a `FieldKey`: the routing `Map`'s key space. */
12
14
  export declare function fieldKeyToString(k: FieldKey): string;
13
- /**
14
- * Route a canonical `DocPath` (a `Diagnostic.path`, or a `ContentHit` /
15
- * `FieldRegion` field address; one grammar) to a `FieldKey`, `card` the ABSOLUTE
16
- * document-array index. `/core`'s `addrForFieldPath` IS this walk — an `Addr` is a
17
- * positional `FieldKey` — so routing reads the one public inverse rather than a
18
- * second copy of the grammar that drifts from it. `undefined` for a path naming no
19
- * single commit address (a nested / array-element path, a field-rooted one, a
20
- * malformed one).
21
- */
22
- export declare function parsePath(path: string): FieldKey | undefined;
23
15
  /**
24
16
  * Resolve a positional `FieldKey` (an ABSOLUTE document-array index, straight off
25
- * {@link parsePath}) to the editor's stable-id keying using the LIVE `cardIds`
17
+ * `addrForFieldPath`) to the editor's stable-id keying using the LIVE `cardIds`
26
18
  * array: re-resolved fresh on every call (never cached), the same "resolve only
27
19
  * at the point of use" discipline `cardIndexOf` applies to writes (VISUAL_EDITOR
28
20
  * §"The address is the spine"). A key already id-keyed (`card` a string) or main
@@ -37,10 +29,10 @@ export interface RoutedDiagnostic {
37
29
  }
38
30
  /**
39
31
  * Route a path-keyed producer's raw `Diagnostic[]` (validate / external) to the
40
- * editor's stable-id keying: the ONE door producers #1/#3 take, `parsePath` and
41
- * `resolveCardKey` in a single pass. An entry drops rather than mis-routes when it
42
- * carries no `path`, when `parsePath` cannot place it (a nested / array-element
43
- * address), or when its absolute card index is out of the live `cardIds`.
32
+ * editor's stable-id keying: the ONE door producers #1/#3 take, `addrForFieldPath`
33
+ * and `resolveCardKey` in a single pass. An entry drops rather than mis-routes when
34
+ * it carries no `path`, when the path names no single commit address (a nested /
35
+ * array-element one), or when its absolute card index is out of the live `cardIds`.
44
36
  */
45
37
  export declare function routeAndResolve(diagnostics: Diagnostic[] | undefined, cardIds: readonly string[]): RoutedDiagnostic[];
46
38
  /**
@@ -3,21 +3,9 @@ import { addrForFieldPath } from '../core/address.js';
3
3
  export function fieldKeyToString(k) {
4
4
  return `${k.card ?? 'main'}:${k.field ?? '$body'}`;
5
5
  }
6
- /**
7
- * Route a canonical `DocPath` (a `Diagnostic.path`, or a `ContentHit` /
8
- * `FieldRegion` field address; one grammar) to a `FieldKey`, `card` the ABSOLUTE
9
- * document-array index. `/core`'s `addrForFieldPath` IS this walk — an `Addr` is a
10
- * positional `FieldKey` — so routing reads the one public inverse rather than a
11
- * second copy of the grammar that drifts from it. `undefined` for a path naming no
12
- * single commit address (a nested / array-element path, a field-rooted one, a
13
- * malformed one).
14
- */
15
- export function parsePath(path) {
16
- return addrForFieldPath(path);
17
- }
18
6
  /**
19
7
  * Resolve a positional `FieldKey` (an ABSOLUTE document-array index, straight off
20
- * {@link parsePath}) to the editor's stable-id keying using the LIVE `cardIds`
8
+ * `addrForFieldPath`) to the editor's stable-id keying using the LIVE `cardIds`
21
9
  * array: re-resolved fresh on every call (never cached), the same "resolve only
22
10
  * at the point of use" discipline `cardIndexOf` applies to writes (VISUAL_EDITOR
23
11
  * §"The address is the spine"). A key already id-keyed (`card` a string) or main
@@ -32,17 +20,17 @@ export function resolveCardKey(key, cardIds) {
32
20
  }
33
21
  /**
34
22
  * Route a path-keyed producer's raw `Diagnostic[]` (validate / external) to the
35
- * editor's stable-id keying: the ONE door producers #1/#3 take, `parsePath` and
36
- * `resolveCardKey` in a single pass. An entry drops rather than mis-routes when it
37
- * carries no `path`, when `parsePath` cannot place it (a nested / array-element
38
- * address), or when its absolute card index is out of the live `cardIds`.
23
+ * editor's stable-id keying: the ONE door producers #1/#3 take, `addrForFieldPath`
24
+ * and `resolveCardKey` in a single pass. An entry drops rather than mis-routes when
25
+ * it carries no `path`, when the path names no single commit address (a nested /
26
+ * array-element one), or when its absolute card index is out of the live `cardIds`.
39
27
  */
40
28
  export function routeAndResolve(diagnostics, cardIds) {
41
29
  const out = [];
42
30
  for (const d of diagnostics ?? []) {
43
31
  if (!d.path)
44
32
  continue;
45
- const key = parsePath(d.path);
33
+ const key = addrForFieldPath(d.path);
46
34
  const resolved = key && resolveCardKey(key, cardIds);
47
35
  if (resolved)
48
36
  out.push({ key: resolved, diagnostic: d });
@@ -1,3 +1,4 @@
1
+ import '../core/theme.css';
1
2
  export { default as VisualEditor } from './VisualEditor.svelte';
2
3
  export type { VisualEditorProps } from './props.js';
3
4
  export { createField } from '../core/codec/index.js';
@@ -8,6 +8,13 @@
8
8
  // The barrel carries what a consumer story wants, and nothing else: a symbol
9
9
  // earns its line when a caller outside the package needs it, and grows one when
10
10
  // that caller arrives, not before.
11
+ // The theme derivation, which every control on this surface reads through `var()`.
12
+ // Imported at the BARREL because a subpath is what a consumer gets: this surface
13
+ // reaches modules inside `core/` and never its entry, so a sheet hanging off that
14
+ // entry arrives only for a consumer importing `/core` for some other reason. The
15
+ // sheet rather than the entry, because the barrel needs the derivation and not
16
+ // `init`. `check:deps` (rule 4) walks this file's own graph and holds the reach.
17
+ import '../core/theme.css';
11
18
  export { default as VisualEditor } from './VisualEditor.svelte';
12
19
  // The prose leaf: the vanilla mount API a non-Svelte consumer (or a custom
13
20
  // surface) builds on.
@@ -0,0 +1,47 @@
1
+ import type { FieldController } from '../core/codec/index.js';
2
+ /**
3
+ * A mounted field's landing handle: focus it, and the element the arrival wash blooms
4
+ * inside ({@link import('../core/bloom.js').bloomInside}).
5
+ *
6
+ * A `FieldController` is one, which is what lets the two lanes share a lookup; `Field`
7
+ * builds the same shape over a form control off the handoff a label click already
8
+ * takes, so `focusField` and a label cannot land in different places.
9
+ */
10
+ export interface FieldControl {
11
+ focus(): void;
12
+ /**
13
+ * Focus element `index` of a control that HAS elements, resolving the index to the
14
+ * element's session id at the call; {@link focus}'s own answer for an index past
15
+ * the live list. Absent on every control without elements.
16
+ *
17
+ * The granularity rides the CALL because the registry stays parent-keyed: a
18
+ * per-element key is positional, in a registry whose doctrine is dodging
19
+ * positional churn.
20
+ */
21
+ focusElement?(index: number): void;
22
+ readonly el: HTMLElement;
23
+ }
24
+ /**
25
+ * The registry the editor holds and the mounted tree writes into.
26
+ *
27
+ * A LANE IS DROPPED BY ITS OWN OWNER, which is what the symmetric pairs are for: a
28
+ * card retyped to a kind that declares the same field name at another type swaps the
29
+ * control under one leaf key without remounting `Field`, so the leaving control and
30
+ * the arriving prose leaf both touch that key with no order between them to rely on.
31
+ */
32
+ export interface LeafRegistry {
33
+ /** A prose leaf, by its controller: the landing handle and the codec seam at once. */
34
+ registerProse(key: string, controller: FieldController): void;
35
+ unregisterProse(key: string): void;
36
+ /** A form control: the landing lane only, having no codec half to offer. */
37
+ registerControl(key: string, control: FieldControl): void;
38
+ unregisterControl(key: string): void;
39
+ /** The landing handle at `key`, whichever lane holds it. */
40
+ control(key: string): FieldControl | undefined;
41
+ /** The codec seam at `key`, `undefined` for a form control. */
42
+ prose(key: string): FieldController | undefined;
43
+ /** Drop everything: the surface going away as a whole, where a leaf's cleanup
44
+ * order relative to the parent's is Svelte's business. */
45
+ clear(): void;
46
+ }
47
+ export declare function createLeafRegistry(): LeafRegistry;
@@ -0,0 +1,31 @@
1
+ export function createLeafRegistry() {
2
+ const controls = new Map();
3
+ const proses = new Map();
4
+ return {
5
+ registerProse(key, controller) {
6
+ proses.set(key, controller);
7
+ },
8
+ unregisterProse(key) {
9
+ proses.delete(key);
10
+ },
11
+ registerControl(key, control) {
12
+ controls.set(key, control);
13
+ },
14
+ unregisterControl(key) {
15
+ controls.delete(key);
16
+ },
17
+ // The prose map is read SECOND: a controller is a control, so a leaf answers a
18
+ // landing without a second entry, and a key the two lanes overlap on during a
19
+ // retype resolves to the form control the tree is currently rendering.
20
+ control(key) {
21
+ return controls.get(key) ?? proses.get(key);
22
+ },
23
+ prose(key) {
24
+ return proses.get(key);
25
+ },
26
+ clear() {
27
+ controls.clear();
28
+ proses.clear();
29
+ }
30
+ };
31
+ }