@quillmark/svelte 0.1.1 → 0.3.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 (124) hide show
  1. package/README.md +16 -12
  2. package/THEMING.md +48 -25
  3. package/dist/core/address.d.ts +42 -7
  4. package/dist/core/address.js +49 -21
  5. package/dist/core/bloom.d.ts +5 -5
  6. package/dist/core/bloom.js +8 -8
  7. package/dist/core/codec/anchor.d.ts +1 -1
  8. package/dist/core/codec/anchor.js +1 -1
  9. package/dist/core/codec/atoms.d.ts +10 -0
  10. package/dist/core/codec/atoms.js +78 -0
  11. package/dist/core/codec/clipboard.d.ts +59 -0
  12. package/dist/core/codec/clipboard.js +202 -0
  13. package/dist/core/codec/code.d.ts +1 -1
  14. package/dist/core/codec/code.js +4 -4
  15. package/dist/core/codec/decode.d.ts +1 -1
  16. package/dist/core/codec/decode.js +8 -8
  17. package/dist/core/codec/encode.d.ts +4 -4
  18. package/dist/core/codec/encode.js +14 -11
  19. package/dist/core/codec/field.d.ts +22 -27
  20. package/dist/core/codec/field.js +33 -32
  21. package/dist/core/codec/index.js +5 -8
  22. package/dist/core/codec/inputrules.js +56 -18
  23. package/dist/core/codec/islands.d.ts +11 -7
  24. package/dist/core/codec/islands.js +16 -10
  25. package/dist/core/codec/keymap.d.ts +5 -6
  26. package/dist/core/codec/keymap.js +8 -7
  27. package/dist/core/codec/lists.d.ts +3 -3
  28. package/dist/core/codec/lists.js +32 -13
  29. package/dist/core/codec/marks.d.ts +1 -1
  30. package/dist/core/codec/marks.js +1 -1
  31. package/dist/core/codec/positions.js +1 -1
  32. package/dist/core/codec/prose.css +266 -228
  33. package/dist/core/codec/schema.js +4 -4
  34. package/dist/core/codec/slash.d.ts +39 -41
  35. package/dist/core/codec/slash.js +0 -0
  36. package/dist/core/codec/table-view.d.ts +12 -16
  37. package/dist/core/codec/table-view.js +488 -292
  38. package/dist/core/codec/table.d.ts +42 -15
  39. package/dist/core/codec/table.js +73 -27
  40. package/dist/core/errors.d.ts +17 -7
  41. package/dist/core/index.d.ts +1 -1
  42. package/dist/core/index.js +6 -5
  43. package/dist/core/lifecycle.d.ts +21 -5
  44. package/dist/core/lifecycle.js +34 -8
  45. package/dist/core/rebind.svelte.d.ts +18 -4
  46. package/dist/core/rebind.svelte.js +30 -12
  47. package/dist/core/teardown.d.ts +1 -1
  48. package/dist/core/teardown.js +1 -1
  49. package/dist/core/theme.css +173 -58
  50. package/dist/preset/index.css +1 -1
  51. package/dist/preset/recipes.css +79 -30
  52. package/dist/preset/scale.css +40 -28
  53. package/dist/preview/Preview.svelte +22 -19
  54. package/dist/preview/Preview.svelte.d.ts +8 -8
  55. package/dist/preview/bridge.d.ts +6 -4
  56. package/dist/preview/bridge.js +19 -9
  57. package/dist/preview/controller.d.ts +13 -6
  58. package/dist/preview/controller.js +8 -5
  59. package/dist/preview/geometry.d.ts +17 -2
  60. package/dist/preview/geometry.js +21 -1
  61. package/dist/preview/index.d.ts +1 -0
  62. package/dist/preview/index.js +9 -2
  63. package/dist/preview/overlay.d.ts +2 -1
  64. package/dist/preview/overlay.js +26 -11
  65. package/dist/preview/paint.js +19 -17
  66. package/dist/visual/ArrayField.svelte +103 -83
  67. package/dist/visual/ArrayField.svelte.d.ts +5 -4
  68. package/dist/visual/BooleanField.svelte +8 -6
  69. package/dist/visual/BooleanField.svelte.d.ts +2 -2
  70. package/dist/visual/Card.svelte +161 -333
  71. package/dist/visual/Card.svelte.d.ts +5 -4
  72. package/dist/visual/CardControls.svelte +1 -1
  73. package/dist/visual/DateField.svelte +19 -19
  74. package/dist/visual/DateField.svelte.d.ts +3 -3
  75. package/dist/visual/DiagnosticList.svelte +2 -2
  76. package/dist/visual/EnumField.svelte +48 -28
  77. package/dist/visual/EnumField.svelte.d.ts +5 -4
  78. package/dist/visual/Field.svelte +207 -132
  79. package/dist/visual/Field.svelte.d.ts +11 -7
  80. package/dist/visual/FieldHint.svelte +79 -48
  81. package/dist/visual/FieldHint.svelte.d.ts +1 -1
  82. package/dist/visual/FieldLabel.svelte +8 -8
  83. package/dist/visual/FormatPopover.svelte +82 -35
  84. package/dist/visual/NumberField.svelte +5 -5
  85. package/dist/visual/NumberField.svelte.d.ts +1 -1
  86. package/dist/visual/ObjectField.svelte +28 -9
  87. package/dist/visual/ObjectField.svelte.d.ts +4 -2
  88. package/dist/visual/ProseArrayElement.svelte +12 -19
  89. package/dist/visual/ProseArrayElement.svelte.d.ts +1 -2
  90. package/dist/visual/ProseField.svelte +62 -85
  91. package/dist/visual/ProseField.svelte.d.ts +14 -17
  92. package/dist/visual/SlashMenu.svelte +42 -16
  93. package/dist/visual/TextField.svelte +5 -5
  94. package/dist/visual/TextField.svelte.d.ts +1 -1
  95. package/dist/visual/TipsCard.svelte +43 -28
  96. package/dist/visual/VisualEditor.svelte +18 -19
  97. package/dist/visual/VisualEditor.svelte.d.ts +2 -3
  98. package/dist/visual/VisualEditorInner.svelte +251 -184
  99. package/dist/visual/VisualEditorInner.svelte.d.ts +2 -3
  100. package/dist/visual/controls.css +85 -38
  101. package/dist/visual/diagnostics.d.ts +13 -21
  102. package/dist/visual/diagnostics.js +7 -19
  103. package/dist/visual/domid.js +3 -3
  104. package/dist/visual/ext.d.ts +1 -1
  105. package/dist/visual/ext.js +1 -1
  106. package/dist/visual/icons/nodes.d.ts +3 -0
  107. package/dist/visual/icons/nodes.js +2 -1
  108. package/dist/visual/index.d.ts +1 -0
  109. package/dist/visual/index.js +10 -3
  110. package/dist/visual/leaves.d.ts +47 -0
  111. package/dist/visual/leaves.js +31 -0
  112. package/dist/visual/links.d.ts +24 -0
  113. package/dist/visual/links.js +67 -0
  114. package/dist/visual/props.d.ts +33 -12
  115. package/dist/visual/signals.d.ts +6 -6
  116. package/dist/visual/strings.d.ts +14 -19
  117. package/dist/visual/strings.js +5 -7
  118. package/dist/visual/structure.d.ts +24 -17
  119. package/dist/visual/structure.js +8 -8
  120. package/dist/visual/synced.svelte.d.ts +1 -1
  121. package/dist/visual/synced.svelte.js +4 -4
  122. package/dist/visual/tips.d.ts +4 -2
  123. package/dist/visual/tips.js +7 -4
  124. package/package.json +2 -2
@@ -1,12 +1,15 @@
1
- /* THE HOST'S DERIVATION: the same public dials in, a closed `--qmh-*` scale for the
2
- page AROUND the surfaces out. `core/theme.css` is the same shape one level down
3
- (dials in, `--_qm-*` out), and the two are calibrated against each other: the mixes
4
- here are the percentages there (4pp between surface steps, 17% border, 45% and 60%
5
- ink), so a host plate and a card on it agree about which way "raised" goes and land
6
- on the same tone at both poles.
1
+ /* The host'S derivation: the same public dials in, a closed `--qmh-*` scale for the
2
+ page around the surfaces out. `core/theme.css` is the same shape one level down
3
+ (dials in, `--_qm-*` out), and the two are calibrated against each other: the
4
+ figures here are the figures there (the ground step's two arms, 8% hover, 17%
5
+ border, 45% and 60% ink) and the dial means the same plane in both, so a host plate
6
+ and a card on it land on the same tone at both poles and a host page meets an
7
+ editor's own column exactly. The well under the ground has no counterpart here: a
8
+ host's own controls are boxed and carry a hairline, so nothing on a page needs the
9
+ step an edgeless field does.
7
10
 
8
- TWO SCALES rather than one because they answer to different roots. `--_qm-*` is
9
- declared ON each mounted root, out of reach from an ancestor, so a surface holds its
11
+ Two scales rather than one because they answer to different roots. `--_qm-*` is
12
+ declared on each mounted root, out of reach from an ancestor, so a surface holds its
10
13
  scale wherever it is dropped. A host scale has the opposite job: it is the document,
11
14
  so it is declared at `:root` and everything on the page reads it. A single namespace
12
15
  could not be both.
@@ -14,26 +17,34 @@
14
17
  Literals live here and nowhere else in the preset: `check:style` runs its axes over
15
18
  `recipes.css` beside it, with this file exempt as the place the defaults are minted.
16
19
 
17
- The poles fall back to the SYSTEM colours, so a host that turns no dial still lands
18
- where its own unstyled text does. The dials sit in front, so `--qm-bg`/`--qm-fg` on
19
- an ancestor retunes page and surface together. */
20
+ The poles fall back to the system colours, so a host that turns no dial still lands
21
+ where its own unstyled text does — `Canvas` being the plane the UA would have
22
+ painted a page with, which is what a plate should be. The dials sit in front, so
23
+ `--qm-bg`/`--qm-fg` on an ancestor retunes page and surface together. */
20
24
 
21
25
  :root {
22
26
  /* ── Colour ──────────────────────────────────────────────────────────────── */
23
- --qmh-page: var(--qm-bg, Canvas);
27
+ /* The dial is the plate, exactly as it is the card one level down: a plate beside
28
+ a mounted editor is the same plane as a card in it, so both read the pole and
29
+ the page sinks from it by the same two arms. That is what puts a host page and
30
+ an editor's own column on one tone rather than a plane apart. Planes step by
31
+ light and everything else by ink, which is the package's own split
32
+ (ARCHITECTURE §Styling); the figures are its figures. */
33
+ --qmh-surface: var(--qm-bg, Canvas);
34
+ --qmh-page: light-dark(
35
+ oklab(from var(--qmh-surface) calc(l - 0.0214) calc(a - 0.00065) calc(b - 0.00158)),
36
+ oklab(from var(--qmh-surface) calc(l - 0.0553) calc(a - 0.00048) calc(b + 0.00165))
37
+ );
24
38
  --qmh-ink: var(--qm-fg, CanvasText);
25
- --qmh-ink-meta: color-mix(in oklab, var(--qmh-ink), var(--qmh-page) 45%);
26
- --qmh-ghost: color-mix(in oklab, var(--qmh-ink), var(--qmh-page) 60%);
27
- /* Two planes behind content, 4pp apart exactly as the surfaces step, so a host
28
- plate and a card on it agree about which way "raised" goes in both schemes. */
29
- --qmh-surface: color-mix(in oklab, var(--qmh-page), var(--qmh-ink) 4%);
30
- --qmh-surface-hover: color-mix(in oklab, var(--qmh-page), var(--qmh-ink) 8%);
31
- --qmh-border: color-mix(in oklab, var(--qmh-page), var(--qmh-ink) 17%);
39
+ --qmh-ink-meta: color-mix(in oklab, var(--qmh-ink), var(--qmh-surface) 45%);
40
+ --qmh-ghost: color-mix(in oklab, var(--qmh-ink), var(--qmh-surface) 60%);
41
+ --qmh-surface-hover: color-mix(in oklab, var(--qmh-surface), var(--qmh-ink) 8%);
42
+ --qmh-border: color-mix(in oklab, var(--qmh-surface), var(--qmh-ink) 17%);
32
43
  /* The edge that says "engaged" where the hairline says "edge": a pressed switch,
33
44
  the underline under a link. */
34
- --qmh-border-strong: color-mix(in oklab, var(--qmh-page), var(--qmh-ink) 40%);
45
+ --qmh-border-strong: color-mix(in oklab, var(--qmh-surface), var(--qmh-ink) 40%);
35
46
 
36
- /* The two hues a host spends, and it spends them on a message. Both read the DIAL,
47
+ /* The two hues a host spends, and it spends them on a message. Both read the dial,
37
48
  so a page carries one colour meaning error and one meaning warning rather than a
38
49
  second pair beside the surfaces'. The dark half is a lift the dials do not carry:
39
50
  a hue calibrated against white reads as a stain on a dark page. */
@@ -42,7 +53,7 @@
42
53
 
43
54
  /* ── Rhythm ──────────────────────────────────────────────────────────────── */
44
55
  /* One base and its multiples, the same four steps the surfaces derive. A host
45
- separating SECTIONS down a page wants steps above these; it derives them from
56
+ separating sections down a page wants steps above these; it derives them from
46
57
  this base rather than minting them, which is what keeps its ramp one ramp. */
47
58
  --qmh-space: var(--qm-space, 0.25rem);
48
59
  --qmh-space-half: calc(var(--qmh-space) / 2);
@@ -58,10 +69,11 @@
58
69
  so it does not derive from the spacing dial. */
59
70
  --qmh-measure: 34rem;
60
71
 
61
- /* What one mount takes once a split has stacked. Reading geometry too, and about
62
- the surface rather than the text: how much of a document is worth showing when it
63
- cannot stand beside the thing it answers to. */
64
- --qmh-pane: 34rem;
72
+ /* The floor under a control a thumb lands on. Hand geometry rather than rhythm, which
73
+ is why it does not derive from the spacing dial either: 44px is where both platform
74
+ guidelines and WCAG put a touch target, and a control padded to a pointer's size is
75
+ under it whatever rung the padding reads. */
76
+ --qmh-tap: 2.75rem;
65
77
 
66
78
  /* ── Type ────────────────────────────────────────────────────────────────── */
67
79
  /* Two faces with a job each: prose is sans, chrome is mono. Both are system stacks,
@@ -72,7 +84,7 @@
72
84
  --qmh-font: var(--qm-font, ui-sans-serif, system-ui, sans-serif);
73
85
  --qmh-font-mono: var(--qm-font-mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
74
86
 
75
- /* The page's own ramp, and it does NOT read `--qm-font-size`: that dial anchors the
87
+ /* The page's own ramp, and it does not read `--qm-font-size`: that dial anchors the
76
88
  surfaces' ramp, which is chrome inside a card, and a page's body text is a
77
89
  different job at a different size. A host that wants them locked sets both. */
78
90
  --qmh-text: 1rem;
@@ -93,7 +105,7 @@
93
105
  --qmh-ease-reverse: cubic-bezier(0.4, 0, 0.6, 1);
94
106
  }
95
107
 
96
- /* Collapse the SCALE, not each transition: every animated recipe reads the rung, so one
108
+ /* Collapse the scale, not each transition: every animated recipe reads the rung, so one
97
109
  override reaches all of them with no `*` rule touching mounted package elements. */
98
110
  @media (prefers-reduced-motion: reduce) {
99
111
  :root {
@@ -4,22 +4,22 @@
4
4
  paint.ts/overlay.ts/bridge.ts/controller.ts own the behavior. Exposes the
5
5
  `PreviewController` verbs as instance methods (`bind:this`) for a consumer
6
6
  that drives `refresh`/`scrollToField`/`focusPosition`/`setZoom` imperatively
7
- (e.g. after `session.apply` elsewhere); pure passthrough, no added logic.
7
+ (e.g. after `session.apply` elsewhere).
8
8
  -->
9
9
  <script lang="ts">
10
10
  import { onMount } from 'svelte';
11
11
  import { createPreview, type PreviewController } from './controller.js';
12
12
  import { guardRebind } from '../core/rebind.svelte.js';
13
13
  import type { PreviewStringsInput } from './strings.js';
14
- import type { LiveSession, ContentHit, ChangeSet } from '@quillmark/wasm';
15
- import type { DocPath, Place } from '../core/address.js';
14
+ import type { LiveSession, ChangeSet } from '@quillmark/wasm';
15
+ import type { DocPath, Landing, Place } from '../core/address.js';
16
16
  import type { EditorErrorHandler } from '../core/errors.js';
17
17
 
18
18
  /**
19
- * REMOUNT CONTRACT. `createPreview` binds once in `onMount`; a later change to any
20
- * prop it closed over (`session`, `margin`, `overlays`, `onCaretPick`, `onError`,
21
- * `strings`) is NOT observed, and each reports `rebind-ignored` when swapped. Swap
22
- * the session by REMOUNTING (`{#key session}`, as the playground does); drive
19
+ * Remount contract. `createPreview` binds once in `onMount`; a later change to any
20
+ * prop it closed over (`session`, `margin`, `overlays`, `onPick`, `onError`,
21
+ * `strings`) is not observed, and each reports `rebind-ignored` when swapped. Swap
22
+ * the session by remounting (`{#key session}`, as the playground does); drive
23
23
  * in-place edits through the `refresh(change)` method, not a prop change.
24
24
  *
25
25
  * `onError` is itself once-bound, so a swapped handler means the report of its own
@@ -36,7 +36,7 @@
36
36
  style?: string;
37
37
  margin?: number;
38
38
  overlays?: boolean;
39
- onCaretPick?: (hit: ContentHit) => void;
39
+ onPick?: (at: Landing) => void;
40
40
  /** A page paint the backend refused; the error message state shows either way. */
41
41
  onError?: EditorErrorHandler;
42
42
  /** The message-state wording, keyed and partial (`PreviewStrings`). */
@@ -47,7 +47,7 @@
47
47
  session,
48
48
  margin,
49
49
  overlays,
50
- onCaretPick,
50
+ onPick,
51
51
  onError,
52
52
  strings,
53
53
  class: className,
@@ -58,7 +58,7 @@
58
58
  let controller: PreviewController | undefined;
59
59
 
60
60
  guardRebind(
61
- () => ({ session, margin, overlays, onCaretPick, onError, strings }),
61
+ () => ({ session, margin, overlays, onPick, onError, strings }),
62
62
  'Remount the preview ({#key session}) to rebind.'
63
63
  );
64
64
 
@@ -68,7 +68,7 @@
68
68
  container: containerEl,
69
69
  margin,
70
70
  overlays,
71
- onCaretPick,
71
+ onPick,
72
72
  onError,
73
73
  strings
74
74
  });
@@ -81,8 +81,9 @@
81
81
  export function refresh(change: ChangeSet): void {
82
82
  controller?.refresh(change);
83
83
  }
84
- export function scrollToField(field: DocPath): void {
85
- controller?.scrollToField(field);
84
+ /** `false` when this compile places nothing at `field` ({@link PreviewController}). */
85
+ export function scrollToField(field: DocPath): boolean {
86
+ return controller?.scrollToField(field) ?? false;
86
87
  }
87
88
  /** Takes the editor's `onCaretMove` payload: `onCaretMove={preview.focusPosition}`. */
88
89
  export function focusPosition(at: Place): void {
@@ -96,15 +97,17 @@
96
97
  <div bind:this={containerEl} class="qm-preview {className ?? ''}" {style} data-qm-root></div>
97
98
 
98
99
  <style>
99
- /* A DETACHED root: the preview is not a descendant of the editor, so it carries
100
+ /* A detached root: the preview is not a descendant of the editor, so it carries
100
101
  `data-qm-root` for the page/overlay rungs paint.ts and overlay.ts read
101
102
  (core/theme.css).
102
103
 
103
- The DESK as well as the paper: a sheet is at `--_qm-surface` (paint.ts), so the
104
- tone behind it is one rung raised and the gutter is the margin the sheet floats
105
- in. Both are the surface's own, so a bare `<div>` is a mounting site. The
104
+ The desk as well as the paper: a sheet is at `--_qm-surface` (paint.ts), so the
105
+ tone behind it is the sunken rung and the gutter is the margin the sheet floats
106
+ in. Paper is the brighter plane at both poles, exactly as a card is over the
107
+ editor's own column, so the two panes step the same way. Both are
108
+ the surface's own, so a bare `<div>` is a mounting site. The
106
109
  padding is safe against the paint loop: a slot is a `width: 100%` child and
107
- measures the CONTENT box, and the container's own `clientWidth` is read only as
110
+ measures the content box, and the container's own `clientWidth` is read only as
108
111
  a change detector, which a constant inset does not disturb. */
109
112
  .qm-preview {
110
113
  box-sizing: border-box;
@@ -113,6 +116,6 @@
113
116
  height: 100%;
114
117
  overflow-y: auto;
115
118
  padding: var(--_qm-space-4);
116
- background: var(--_qm-surface-raised);
119
+ background: var(--_qm-surface-sunken);
117
120
  }
118
121
  </style>
@@ -1,12 +1,12 @@
1
1
  import type { PreviewStringsInput } from './strings.js';
2
- import type { LiveSession, ContentHit, ChangeSet } from '@quillmark/wasm';
3
- import type { DocPath, Place } from '../core/address.js';
2
+ import type { LiveSession, ChangeSet } from '@quillmark/wasm';
3
+ import type { DocPath, Landing, Place } from '../core/address.js';
4
4
  import type { EditorErrorHandler } from '../core/errors.js';
5
5
  /**
6
- * REMOUNT CONTRACT. `createPreview` binds once in `onMount`; a later change to any
7
- * prop it closed over (`session`, `margin`, `overlays`, `onCaretPick`, `onError`,
8
- * `strings`) is NOT observed, and each reports `rebind-ignored` when swapped. Swap
9
- * the session by REMOUNTING (`{#key session}`, as the playground does); drive
6
+ * Remount contract. `createPreview` binds once in `onMount`; a later change to any
7
+ * prop it closed over (`session`, `margin`, `overlays`, `onPick`, `onError`,
8
+ * `strings`) is not observed, and each reports `rebind-ignored` when swapped. Swap
9
+ * the session by remounting (`{#key session}`, as the playground does); drive
10
10
  * in-place edits through the `refresh(change)` method, not a prop change.
11
11
  *
12
12
  * `onError` is itself once-bound, so a swapped handler means the report of its own
@@ -23,7 +23,7 @@ interface Props {
23
23
  style?: string;
24
24
  margin?: number;
25
25
  overlays?: boolean;
26
- onCaretPick?: (hit: ContentHit) => void;
26
+ onPick?: (at: Landing) => void;
27
27
  /** A page paint the backend refused; the error message state shows either way. */
28
28
  onError?: EditorErrorHandler;
29
29
  /** The message-state wording, keyed and partial (`PreviewStrings`). */
@@ -31,7 +31,7 @@ interface Props {
31
31
  }
32
32
  declare const Preview: import("svelte").Component<Props, {
33
33
  refresh: (change: ChangeSet) => void;
34
- scrollToField: (field: DocPath) => void;
34
+ scrollToField: (field: DocPath) => boolean;
35
35
  focusPosition: (at: Place) => void;
36
36
  setZoom: (scale: number) => void;
37
37
  }, "">;
@@ -1,8 +1,10 @@
1
- import type { LiveSession, ContentHit } from '@quillmark/wasm';
1
+ import type { LiveSession } from '@quillmark/wasm';
2
+ import type { Landing } from '../core/address.js';
2
3
  import type { PageSlot } from './paint.js';
3
4
  export interface BridgeController {
4
- /** Scroll `field`'s first content box to centre (a no-op if it has none). */
5
- scrollToField(field: string): void;
5
+ /** Scroll `field`'s first box to centre; `false` when this compile places none
6
+ * for that address, which is the whole of what the preview can say about it. */
7
+ scrollToField(field: string): boolean;
6
8
  /**
7
9
  * Bring the caret rect at `field`/`pos` into view (a no-op off-content, and a
8
10
  * no-op when it is already clear of the fold).
@@ -10,4 +12,4 @@ export interface BridgeController {
10
12
  focusPosition(field: string, pos: number): void;
11
13
  destroy(): void;
12
14
  }
13
- export declare function createBridge(session: LiveSession, container: HTMLElement, slots: readonly PageSlot[], onCaretPick: ((hit: ContentHit) => void) | undefined): BridgeController;
15
+ export declare function createBridge(session: LiveSession, container: HTMLElement, slots: readonly PageSlot[], onPick: ((at: Landing) => void) | undefined): BridgeController;
@@ -1,7 +1,13 @@
1
- import { rectToPercent, clickToPdfPt, applyPercentRect } from './geometry.js';
2
- export function createBridge(session, container, slots, onCaretPick) {
1
+ import { boxesForField, rectToPercent, clickToPdfPt, applyPercentRect } from './geometry.js';
2
+ export function createBridge(session, container, slots, onPick) {
3
3
  const unlisten = [];
4
- if (onCaretPick) {
4
+ // Two rungs: `positionAt` over span-tracked content, `fieldAt` over every placement
5
+ // the compile tracks, a strict superset of it (PREVIEW.md §"Click bridge" carries
6
+ // the measurement). The second fires where a field is placed without its content
7
+ // being tracked, and the pick carries no `pos` there: a fabricated `0` would be an
8
+ // invented offset wearing a real one's type. No third rung hit-tests `regions()`,
9
+ // whose rects bound ink the field does not fill.
10
+ if (onPick) {
5
11
  for (const { page, el } of slots) {
6
12
  const handleClick = (ev) => {
7
13
  // Re-read through the live array: a same-count `refresh` swaps the
@@ -16,7 +22,10 @@ export function createBridge(session, container, slots, onCaretPick) {
16
22
  const pt = clickToPdfPt(px, py, box.width, box.height, slot.size);
17
23
  const hit = session.positionAt(slot.page, pt.x, pt.y);
18
24
  if (hit)
19
- onCaretPick(hit);
25
+ return void onPick(hit);
26
+ const field = session.fieldAt(slot.page, pt.x, pt.y);
27
+ if (field)
28
+ onPick({ field });
20
29
  };
21
30
  el.addEventListener('click', handleClick);
22
31
  unlisten.push(() => el.removeEventListener('click', handleClick));
@@ -44,7 +53,7 @@ export function createBridge(session, container, slots, onCaretPick) {
44
53
  else if (target.right > port.right)
45
54
  container.scrollLeft += target.right - port.right;
46
55
  }
47
- // Clear of the fold is the target's OWN height of clearance at each edge, not bare
56
+ // Clear of the fold is the target's own height of clearance at each edge, not bare
48
57
  // intersection: a caret rect flush against one is visible and unusable, and the
49
58
  // next line typed lands past it. Derived from the target rather than a margin
50
59
  // dial, so it scales with zoom exactly as the caret does.
@@ -54,15 +63,16 @@ export function createBridge(session, container, slots, onCaretPick) {
54
63
  }
55
64
  return {
56
65
  scrollToField(field) {
57
- const box = session.fieldBoxes(field)[0];
66
+ const box = boxesForField(field, session.fieldBoxes(field), session.regions())[0];
58
67
  if (!box)
59
- return;
68
+ return false;
60
69
  const slot = slots[box.page];
61
70
  if (!slot)
62
- return;
71
+ return false;
63
72
  // A discrete act ("show me this field"), so it centres every time: the
64
73
  // asymmetry the bloom draws between a continuous signal and a one-shot one.
65
74
  centre(measure(slot, rectToPercent(box.rect, slot.size)));
75
+ return true;
66
76
  },
67
77
  focusPosition(field, pos) {
68
78
  const region = session.locate(field, pos);
@@ -71,7 +81,7 @@ export function createBridge(session, container, slots, onCaretPick) {
71
81
  const slot = slots[region.page];
72
82
  if (!slot)
73
83
  return;
74
- // The CONTINUOUS hop: one call per keystroke and per arrow key, so it moves
84
+ // The continuous hop: one call per keystroke and per arrow key, so it moves
75
85
  // the pane only when the caret has left the fold. Centring unconditionally
76
86
  // takes the scrollport back from the user on every one of them, including
77
87
  // the ones that changed nothing about where the caret already was.
@@ -1,5 +1,5 @@
1
- import type { LiveSession, ChangeSet, ContentHit } from '@quillmark/wasm';
2
- import type { DocPath, Place } from '../core/address.js';
1
+ import type { LiveSession, ChangeSet } from '@quillmark/wasm';
2
+ import type { DocPath, Landing, Place } from '../core/address.js';
3
3
  import { type EditorErrorHandler } from '../core/errors.js';
4
4
  import { type PreviewStringsInput } from './strings.js';
5
5
  export interface PreviewOptions {
@@ -9,8 +9,10 @@ export interface PreviewOptions {
9
9
  margin?: number;
10
10
  /** Draw field-box overlays. Default true. */
11
11
  overlays?: boolean;
12
- /** A click resolved to a content position; the hook does not fire off-ink. */
13
- onCaretPick?(hit: ContentHit): void;
12
+ /** A click resolved to an address ({@link Landing}): a caret where the compile
13
+ * tracks the content under the point, the field alone where it tracks only the
14
+ * placement. The hook does not fire where the compile tracks neither. */
15
+ onPick?(at: Landing): void;
14
16
  /** A page paint the backend refused ({@link EditorErrorHandler}). The preview
15
17
  * shows its error message state either way; this routes it to an app's sink. */
16
18
  onError?: EditorErrorHandler;
@@ -21,8 +23,13 @@ export interface PreviewOptions {
21
23
  export interface PreviewController {
22
24
  /** Repaint `dirtyPages ∩ visible` and re-read geometry; the only apply-driven hop. */
23
25
  refresh(change: ChangeSet): void;
24
- /** Scroll `field`'s first box into view and bloom it. */
25
- scrollToField(field: DocPath): void;
26
+ /**
27
+ * Scroll `field`'s first box into view and bloom it. `false` when this compile
28
+ * places nothing at that address — an answer, not a failure: the plate places
29
+ * plenty it does not track, and the preview carries no schema to tell that from a
30
+ * field the host misnamed. The editor's `focusField` is what distinguishes them.
31
+ */
32
+ scrollToField(field: DocPath): boolean;
26
33
  /**
27
34
  * Bring the caret at `at` into view and bloom its field on arrival. Both halves
28
35
  * are change-guarded: the pane moves only when the caret has left the fold, the
@@ -59,7 +59,7 @@ export function createPreview(session, opts) {
59
59
  // swapped on reconcile, and both attach DOM/listeners per slot.
60
60
  function attach() {
61
61
  overlay = overlaysEnabled ? createOverlay(session, paintLoop.slots) : undefined;
62
- bridge = createBridge(session, container, paintLoop.slots, opts.onCaretPick);
62
+ bridge = createBridge(session, container, paintLoop.slots, opts.onPick);
63
63
  }
64
64
  function detach() {
65
65
  overlay?.destroy();
@@ -83,8 +83,8 @@ export function createPreview(session, opts) {
83
83
  showMessage(pageCount === 0 ? t.noPages : t.unsupported, pageCount === 0 ? EMPTY_CLASS : UNSUPPORTED_CLASS);
84
84
  return;
85
85
  }
86
- // Read the count against the current slots BEFORE reconcile moves it, and
87
- // clear any prior message BEFORE painting so a paint that throws mid-refresh
86
+ // Read the count against the current slots before reconcile moves it, and
87
+ // clear any prior message before painting so a paint that throws mid-refresh
88
88
  // leaves its error surfaced rather than have `hideMessage` clobber it.
89
89
  const countChanged = pageCount !== paintLoop.slots.length;
90
90
  hideMessage();
@@ -104,7 +104,7 @@ export function createPreview(session, opts) {
104
104
  }
105
105
  render(session.pageCount, []);
106
106
  // The last place the editor put the caret, re-located after every recompile.
107
- // `session.locate` answers against the LAST COMPILED layout and a consumer
107
+ // `session.locate` answers against the last compiled layout and a consumer
108
108
  // debounces `update`, so a caret typed past that layout's content is off-content
109
109
  // for the whole burst: `focusPosition` no-ops and the pane sits still, and a
110
110
  // caret event is otherwise the only thing that asks again. The re-locate is the
@@ -120,8 +120,11 @@ export function createPreview(session, opts) {
120
120
  bridge?.focusPosition(followed.field, followed.pos);
121
121
  },
122
122
  scrollToField(field) {
123
- bridge?.scrollToField(field);
123
+ const found = bridge?.scrollToField(field) ?? false;
124
+ // Marked either way: a compile that gains the address later blooms it on the
125
+ // rebuild, since the flash carries its own start time (`core/bloom.ts`).
124
126
  overlay?.flashField(field);
127
+ return found;
125
128
  },
126
129
  focusPosition(at) {
127
130
  followed = at;
@@ -1,4 +1,4 @@
1
- import type { PageSize } from '@quillmark/wasm';
1
+ import type { FieldRegion, PageSize } from '@quillmark/wasm';
2
2
  /** A page rect in PDF points, `[x0, y0, x1, y1]`, bottom-left origin (`FieldRegion.rect`'s shape). */
3
3
  export type PdfRect = readonly [number, number, number, number];
4
4
  /** A rect as CSS percent of the page box (top-left origin; Y is flipped from PDF-pt). */
@@ -13,6 +13,21 @@ export interface PdfPoint {
13
13
  x: number;
14
14
  y: number;
15
15
  }
16
+ /**
17
+ * An address's boxes: `fieldBoxes`'s union, else its own `regions()` rects, else the
18
+ * rects of everything under it. The one rule the overlay and the scroll both read, so
19
+ * an address `regions()` names never draws nothing (PREVIEW.md §"Three
20
+ * responsibilities").
21
+ *
22
+ * `fieldBoxes` answers `[]` for a scalar reference the plate places without tracking
23
+ * its content and for a `richtext[]` element; runtime.d.ts says what such a field's
24
+ * box is, a single `regions()` rect. The third rung is that sentence one granularity
25
+ * down — an array's ink is its elements' — and is a fallback, not a union, so an
26
+ * address with rects of its own never also draws its children's. The `.` keeps the
27
+ * prefix a path boundary: `main.references` matches `main.references.0`, not
28
+ * `main.references_note`.
29
+ */
30
+ export declare function boxesForField(field: string, boxes: readonly FieldRegion[], regions: readonly FieldRegion[]): readonly FieldRegion[];
16
31
  /**
17
32
  * Forward: a PDF-pt rect -> CSS % of the page box. Percent positioning tracks
18
33
  * the page's displayed size across DPR and container resize for free; this is
@@ -28,7 +43,7 @@ export declare function rectToPercent(rect: PdfRect, pageSize: PageSize): Percen
28
43
  export declare function applyPercentRect(el: HTMLElement, pct: PercentRect): void;
29
44
  /**
30
45
  * Inverse: a click at CSS px `(px, py)` within a page element of CSS size
31
- * `(cssW, cssH)` -> PDF-pt. The EXACT inverse of {@link rectToPercent}: the
46
+ * `(cssW, cssH)` -> PDF-pt. The exact inverse of {@link rectToPercent}: the
32
47
  * fraction `rectToPercent` would place a point's box corner at is the same
33
48
  * fraction this reads back from `px/cssW`, `py/cssH`; DPR-independent, since
34
49
  * the page element's CSS size (not its canvas backing store) is the only input.
@@ -1,3 +1,23 @@
1
+ /**
2
+ * An address's boxes: `fieldBoxes`'s union, else its own `regions()` rects, else the
3
+ * rects of everything under it. The one rule the overlay and the scroll both read, so
4
+ * an address `regions()` names never draws nothing (PREVIEW.md §"Three
5
+ * responsibilities").
6
+ *
7
+ * `fieldBoxes` answers `[]` for a scalar reference the plate places without tracking
8
+ * its content and for a `richtext[]` element; runtime.d.ts says what such a field's
9
+ * box is, a single `regions()` rect. The third rung is that sentence one granularity
10
+ * down — an array's ink is its elements' — and is a fallback, not a union, so an
11
+ * address with rects of its own never also draws its children's. The `.` keeps the
12
+ * prefix a path boundary: `main.references` matches `main.references.0`, not
13
+ * `main.references_note`.
14
+ */
15
+ export function boxesForField(field, boxes, regions) {
16
+ if (boxes.length)
17
+ return boxes;
18
+ const own = regions.filter((r) => r.field === field);
19
+ return own.length ? own : regions.filter((r) => r.field.startsWith(`${field}.`));
20
+ }
1
21
  /**
2
22
  * Forward: a PDF-pt rect -> CSS % of the page box. Percent positioning tracks
3
23
  * the page's displayed size across DPR and container resize for free; this is
@@ -30,7 +50,7 @@ export function applyPercentRect(el, pct) {
30
50
  }
31
51
  /**
32
52
  * Inverse: a click at CSS px `(px, py)` within a page element of CSS size
33
- * `(cssW, cssH)` -> PDF-pt. The EXACT inverse of {@link rectToPercent}: the
53
+ * `(cssW, cssH)` -> PDF-pt. The exact inverse of {@link rectToPercent}: the
34
54
  * fraction `rectToPercent` would place a point's box corner at is the same
35
55
  * fraction this reads back from `px/cssW`, `py/cssH`; DPR-independent, since
36
56
  * the page element's CSS size (not its canvas backing store) is the only input.
@@ -1,3 +1,4 @@
1
+ import '../core/theme.css';
1
2
  export { createPreview } from './controller.js';
2
3
  export type { PreviewOptions, PreviewController } from './controller.js';
3
4
  export { default as Preview } from './Preview.svelte';
@@ -1,13 +1,20 @@
1
1
  // `@quillmark/svelte/preview`: the live-preview surface.
2
2
  //
3
3
  // `createPreview(session)` (vanilla-TS core) + a thin `<Preview>` Svelte wrapper
4
- // land here. RESERVED-PACKAGE INVARIANT: this subpath imports no editor-side code
4
+ // land here. Reserved-package invariant: this subpath imports no editor-side code
5
5
  // (the codec, the VisualEditor) so a `@quillmark/preview` promotion stays a
6
6
  // re-export, not a refactor: it may reach `/core` (the shared WASM boundary) and
7
7
  // nothing else. Enforced by `check:deps`, which walks the subpath's import graph.
8
+ // The theme derivation, which the page slots and the overlay read through `var()`.
9
+ // Imported at the barrel because a subpath is what a consumer gets: this surface
10
+ // reaches modules inside `core/` and never its entry, so a sheet hanging off that
11
+ // entry arrives only for a consumer importing `/core` for some other reason. The
12
+ // sheet rather than the entry, because the barrel needs the derivation and not
13
+ // `init`. `check:deps` (rule 4) walks this file's own graph and holds the reach.
14
+ import '../core/theme.css';
8
15
  export { createPreview } from './controller.js';
9
16
  export { default as Preview } from './Preview.svelte';
10
- // What the preview SAYS when there is nothing to paint. Keyed and partial; the
17
+ // What the preview says when there is nothing to paint. Keyed and partial; the
11
18
  // English is exported so a consumer composes against it rather than restating it.
12
19
  // The merge is the surface's own: a consumer hands `strings` to a prop, never
13
20
  // merges one.
@@ -3,7 +3,8 @@ import type { PageSlot } from './paint.js';
3
3
  export interface OverlayController {
4
4
  /** Re-read geometry and rebuild every box; call after a layout-affecting refresh. */
5
5
  refresh(): void;
6
- /** Bloom `field`'s boxes: transient, and a no-op when `field` is already the marked one. */
6
+ /** Bloom the boxes at `field` and under it: transient, and a no-op when `field`
7
+ * is already the marked one. */
7
8
  flashField(field: string): void;
8
9
  destroy(): void;
9
10
  }
@@ -1,5 +1,5 @@
1
1
  import { bloom, bloomTiming, primeWash } from '../core/bloom.js';
2
- import { rectToPercent, applyPercentRect } from './geometry.js';
2
+ import { boxesForField, rectToPercent, applyPercentRect } from './geometry.js';
3
3
  const FIELD_ATTR = 'data-qm-field';
4
4
  export function createOverlay(session, slots) {
5
5
  const layers = slots.map((slot) => {
@@ -9,7 +9,7 @@ export function createOverlay(session, slots) {
9
9
  slot.el.appendChild(layer);
10
10
  return layer;
11
11
  });
12
- // The marked address, and with it the bloom's start. ONE variable does both jobs:
12
+ // The marked address, and with it the bloom's start. One variable does both jobs:
13
13
  // the change guard has to outlive the decay (a caret move after the wash ended
14
14
  // must not re-mark the same field), and `bloom` refuses an `elapsed` past the
15
15
  // decay, so a spent flash is inert on every later rebuild and never needs
@@ -20,32 +20,47 @@ export function createOverlay(session, slots) {
20
20
  let flash;
21
21
  /** field → its boxes, rebuilt with them. The bloom's only lookup. */
22
22
  let byField = new Map();
23
- // Start or RESUME the bloom on whatever boxes exist for it now: one path, because
23
+ // The boxes a flash covers: the address's own, plus every one under it, since the
24
+ // boxes are keyed as `regions()` names them (`main.references.0`) and an
25
+ // editor-side signal names the declared field (`main.references`). Resolved per
26
+ // apply rather than at `flashField`, so a rebuild picks up boxes the recompile
27
+ // moved or added.
28
+ function flashed(field) {
29
+ const els = [];
30
+ for (const [key, group] of byField) {
31
+ if (key === field || key.startsWith(`${field}.`))
32
+ els.push(...group);
33
+ }
34
+ return els;
35
+ }
36
+ // Start or resume the bloom on whatever boxes exist for it now: one path, because
24
37
  // starting is resuming at ~0. A field surfaces several boxes and they share
25
38
  // `startedAt`, so they bloom in step rather than shimmering unevenly.
26
39
  function applyFlash() {
27
- const els = flash && byField.get(flash.field);
28
- if (!flash || !els?.length)
40
+ if (!flash)
41
+ return;
42
+ const els = flashed(flash.field);
43
+ if (!els.length)
29
44
  return;
30
45
  const elapsed = performance.now() - flash.startedAt;
31
46
  const timing = bloomTiming(els[0]);
32
47
  for (const el of els)
33
48
  bloom(el, elapsed, timing);
34
49
  }
35
- // Field names come from `regions()` (the only session query that enumerates
36
- // them; Preview carries no schema); the boxes themselves come from
37
- // `fieldBoxes(field)`, which is content-only and `[]` for a scalar-reference
38
- // or widget field, so a nameless-of-content field contributes no boxes.
50
+ // Field names come from `regions()` (the only session query that enumerates them;
51
+ // Preview carries no schema); the rects from `boxesForField`, whose fallback is
52
+ // what makes every address `regions()` names draw.
39
53
  function build() {
40
54
  for (const layer of layers)
41
55
  layer.replaceChildren();
42
56
  byField = new Map();
57
+ const regions = session.regions();
43
58
  const fields = new Set();
44
- for (const region of session.regions())
59
+ for (const region of regions)
45
60
  fields.add(region.field);
46
61
  for (const field of fields) {
47
62
  const els = [];
48
- for (const box of session.fieldBoxes(field)) {
63
+ for (const box of boxesForField(field, session.fieldBoxes(field), regions)) {
49
64
  const layer = layers[box.page];
50
65
  const slot = slots[box.page];
51
66
  if (!layer || !slot)