@quillmark/svelte 0.26.2 → 0.28.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 (97) hide show
  1. package/NOTICE +4 -4
  2. package/README.md +26 -8
  3. package/THEMING.md +40 -4
  4. package/dist/core/address.d.ts +35 -64
  5. package/dist/core/address.js +35 -55
  6. package/dist/core/bloom.d.ts +4 -10
  7. package/dist/core/bloom.js +14 -27
  8. package/dist/core/codec/blocks.d.ts +41 -43
  9. package/dist/core/codec/blocks.js +97 -130
  10. package/dist/core/codec/code.d.ts +7 -10
  11. package/dist/core/codec/code.js +37 -46
  12. package/dist/core/codec/decode.d.ts +19 -23
  13. package/dist/core/codec/decode.js +76 -111
  14. package/dist/core/codec/encode.d.ts +11 -15
  15. package/dist/core/codec/encode.js +60 -67
  16. package/dist/core/codec/field.d.ts +62 -95
  17. package/dist/core/codec/field.js +107 -179
  18. package/dist/core/codec/islands.d.ts +33 -41
  19. package/dist/core/codec/islands.js +76 -86
  20. package/dist/core/codec/lists.d.ts +17 -26
  21. package/dist/core/codec/lists.js +117 -157
  22. package/dist/core/codec/marks.d.ts +4 -3
  23. package/dist/core/codec/marks.js +8 -3
  24. package/dist/core/codec/prose.css +84 -46
  25. package/dist/core/codec/schema.d.ts +18 -23
  26. package/dist/core/codec/schema.js +84 -76
  27. package/dist/core/codec/slash.d.ts +42 -48
  28. package/dist/core/codec/slash.js +0 -0
  29. package/dist/core/codec/table-copy.d.ts +4 -3
  30. package/dist/core/codec/table-copy.js +25 -7
  31. package/dist/core/codec/table-view.d.ts +12 -12
  32. package/dist/core/codec/table-view.js +460 -383
  33. package/dist/core/codec/table.d.ts +94 -61
  34. package/dist/core/codec/table.js +331 -83
  35. package/dist/core/codec/tasks.d.ts +6 -0
  36. package/dist/core/codec/tasks.js +30 -3
  37. package/dist/core/errors.d.ts +15 -30
  38. package/dist/core/errors.js +2 -6
  39. package/dist/core/index.js +7 -12
  40. package/dist/core/lifecycle.d.ts +7 -16
  41. package/dist/core/lifecycle.js +10 -21
  42. package/dist/core/rebind.svelte.d.ts +10 -20
  43. package/dist/core/rebind.svelte.js +15 -27
  44. package/dist/core/rungs.d.ts +14 -11
  45. package/dist/core/rungs.js +17 -19
  46. package/dist/core/teardown.d.ts +4 -6
  47. package/dist/core/teardown.js +7 -10
  48. package/dist/core/theme.css +74 -54
  49. package/dist/index.js +3 -8
  50. package/dist/preset/app.css +71 -0
  51. package/dist/preset/index.css +8 -7
  52. package/dist/preset/recipes.css +19 -164
  53. package/dist/preset/scale.css +28 -18
  54. package/dist/preset/shell.css +157 -0
  55. package/dist/preview/Preview.svelte +8 -0
  56. package/dist/preview/bridge.d.ts +18 -22
  57. package/dist/preview/bridge.js +29 -58
  58. package/dist/preview/controller.d.ts +21 -29
  59. package/dist/preview/controller.js +14 -39
  60. package/dist/preview/geometry.d.ts +13 -34
  61. package/dist/preview/geometry.js +13 -34
  62. package/dist/preview/index.js +4 -16
  63. package/dist/preview/paint.d.ts +3 -3
  64. package/dist/preview/paint.js +28 -68
  65. package/dist/preview/strings.d.ts +1 -1
  66. package/dist/preview/strings.js +2 -14
  67. package/dist/preview/viewport.js +20 -15
  68. package/dist/preview/zoom.d.ts +21 -22
  69. package/dist/preview/zoom.js +53 -30
  70. package/dist/visual/ArrayField.svelte +77 -16
  71. package/dist/visual/BooleanField.svelte +28 -15
  72. package/dist/visual/Card.svelte +66 -51
  73. package/dist/visual/CardControls.svelte +5 -11
  74. package/dist/visual/DateField.svelte +20 -4
  75. package/dist/visual/EnumField.svelte +3 -0
  76. package/dist/visual/FieldHint.svelte +2 -1
  77. package/dist/visual/FormatPopover.svelte +82 -15
  78. package/dist/visual/FormatPopover.svelte.d.ts +4 -0
  79. package/dist/visual/MatrixAdd.svelte +30 -12
  80. package/dist/visual/MatrixField.svelte +24 -16
  81. package/dist/visual/NumberField.svelte +7 -0
  82. package/dist/visual/ObjectField.svelte +4 -4
  83. package/dist/visual/ProseField.svelte +0 -5
  84. package/dist/visual/TipsCard.svelte +11 -10
  85. package/dist/visual/VariantField.svelte +3 -1
  86. package/dist/visual/VisualEditorInner.svelte +19 -3
  87. package/dist/visual/controls.css +7 -2
  88. package/dist/visual/fonts.d.ts +18 -0
  89. package/dist/visual/fonts.js +59 -0
  90. package/dist/visual/icons/nodes.d.ts +0 -5
  91. package/dist/visual/icons/nodes.js +0 -4
  92. package/dist/visual/props.d.ts +14 -1
  93. package/dist/visual/strings.d.ts +7 -0
  94. package/dist/visual/strings.js +5 -2
  95. package/dist/visual/structure.d.ts +43 -87
  96. package/dist/visual/structure.js +42 -83
  97. package/package.json +2 -2
package/NOTICE CHANGED
@@ -6,9 +6,9 @@ Copyright 2026 Nibs
6
6
  This package contains SVG glyph geometry derived from Lucide, carried in source
7
7
  rather than installed.
8
8
 
9
- `visual/icons/nodes.ts` holds eighteen verbatim from @lucide/svelte 1.28.0:
9
+ `visual/icons/nodes.ts` holds seventeen verbatim from @lucide/svelte 1.28.0:
10
10
  chevron-right, arrow-right, chevron-down, chevron-up, bold, italic, underline,
11
- strikethrough, code, link, info, plus, x, trash-2, minus, check, calendar and
11
+ strikethrough, code, link, info, plus, trash-2, minus, check, calendar and
12
12
  calendar-check.
13
13
 
14
14
  `core/codec/table-view.ts` holds grip-horizontal, grip-vertical, align-left,
@@ -33,8 +33,8 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
33
33
 
34
34
  --------------------------------------------------------------------------------
35
35
 
36
- Twelve of those glyphs — arrow-right, chevron-down, chevron-right, chevron-up,
37
- code, info, italic, link, minus, plus, trash-2 and x — are drawn exactly as
36
+ Eleven of those glyphs — arrow-right, chevron-down, chevron-right, chevron-up,
37
+ code, info, italic, link, minus, plus and trash-2 — are drawn exactly as
38
38
  the Feather project draws them and carry its license as well.
39
39
 
40
40
  The MIT License (MIT)
package/README.md CHANGED
@@ -29,12 +29,12 @@ The **consumer** owns the session and the handles, and drives every edit; the su
29
29
  import { Engine } from '@quillmark/wasm';
30
30
  import { init } from '@quillmark/svelte/core';
31
31
 
32
- // Quill and Document are exported only through init(), which loads the wasm once.
32
+ // Quill and Document are exported only through init().
33
33
  const { Quill, Document } = await init();
34
34
 
35
35
  const quill = Quill.fromTree(tree); // tree: Map<string, Uint8Array> of the quill dir
36
36
 
37
- // A NEW document: seeded from the quill's blueprint.
37
+ // A NEW document: seeded from the quill's schema.
38
38
  const doc = quill.seedDocument();
39
39
 
40
40
  // An EXISTING document: parsed back from what you stored.
@@ -120,7 +120,7 @@ In Svelte, `<Preview {session} onPick={…} />` exposes the same verbs (`refresh
120
120
  />
121
121
  ```
122
122
 
123
- Hand it a different `doc` and the editor **re-keys itself**: every leaf remounts against the new handle, and the id state, the commit-error map and the active address seed fresh. Nothing to key at the call site. `quill` is not part of that key — the schema is re-read on every derive, so a quill swap re-projects on its own; swapping it _without_ the doc reports `rebind-ignored`.
123
+ Hand it a different `doc` and the editor **re-keys itself**: every leaf remounts against the new handle, and the id state, the commit-error map and the active address seed fresh. `quill` is not part of that key — the schema is re-read on every derive, so a quill swap re-projects on its own; swapping it _without_ the doc reports `rebind-ignored`.
124
124
 
125
125
  ### Driving it from outside
126
126
 
@@ -165,6 +165,26 @@ The package ships English and a seam to replace it. `strings` is keyed and **par
165
165
 
166
166
  `formatDiagnostic` gets the whole `Diagnostic` and returns `undefined` to take its own message. Validation and edit diagnostics re-word from `code`, `path` and `args`; parse errors and render warnings carry their detail inside English text and stand as they are ([VISUAL_EDITOR.md](prose/canon/VISUAL_EDITOR.md)).
167
167
 
168
+ ### Fonts
169
+
170
+ A run of text can be set in a font family the **host** supplies, spelled `<qm-span family="…">` in markdown. Build the engine with the font files (`.ttf`, `.otf`, `.ttc`, `.otc`) and hand the editor the faces it lists: the format popover then offers each family, and the editor registers the faces with the page so a run draws in the face the preview sets it in.
171
+
172
+ ```ts
173
+ const carlito = new Uint8Array(await (await fetch('/fonts/Carlito-Regular.ttf')).arrayBuffer());
174
+ const engine = new Engine({ fonts: [carlito] });
175
+ ```
176
+
177
+ ```svelte
178
+ <script lang="ts">
179
+ let fonts = $state<FontFace[]>([]);
180
+ engine.fontFaces(quill).then((faces) => (fonts = faces));
181
+ </script>
182
+
183
+ <VisualEditor {doc} {quill} {fonts} />
184
+ ```
185
+
186
+ [`@quillmark/fonts`](../fonts) is the library every Quillmark client builds its engine over, so a family a document names sets the same way in each; a host's own files go after it. `fontFaces` loads the render build, as `open` does, so mount the editor first and let the faces arrive behind it. A family the document already sets edits with no library at all, and the picker offers it so it can be cleared.
187
+
168
188
  ### Recompiling
169
189
 
170
190
  `onChange` is the signal to recompile, and it covers **all three lanes**: a prose keystroke, a scalar write, a card operation. `onCaretMove` is a _selection_ signal, not a change signal: it fires on a bare arrow key, so a recompile hung off it recompiles on every one.
@@ -193,7 +213,7 @@ function recompileNow() {
193
213
 
194
214
  `session.warnings` is a **getter on a handle Svelte does not track**: pull it per apply, or the editor shows the previous compile's errors.
195
215
 
196
- This shell layer is deliberately yours: the debounce value, what applies at once, and which surfaces refresh are host policy, so the package ships no scheduler over them. Three obligations ride with that: an edit of your own (an import, an undo, a direct `doc` write) recompiles by the same calls, since nothing polls the document; a compile owes the diagnostics re-read above, since nothing polls the session; and teardown clears the timer **before** freeing the handles, so a pending recompile never touches a freed session.
216
+ The schedule is host policy, so the package ships no scheduler. Obligations: an edit of your own (an import, an undo, a direct `doc` write) recompiles by the same calls, since nothing polls the document; a compile re-reads the diagnostics; and teardown clears the timer **before** freeing the handles.
197
217
 
198
218
  ## The caret bridge
199
219
 
@@ -217,7 +237,7 @@ A consumer holding an `Addr` of its own maps it with `fieldPathForAddr` (from `@
217
237
 
218
238
  ## Errors
219
239
 
220
- Every surface takes `onError`. It reports failures the surface **recovered from**: a commit the boundary refused, a card operation that threw, a page paint that failed, a serialize that threw. None of them stop editing. Wire nothing and each lands in the console, which an app cannot route, filter or count.
240
+ Every surface takes `onError`. It reports failures the surface **recovered from**: a commit the boundary refused, a card operation that threw, a page paint that failed, a serialize that threw. None of them stop editing. Wire nothing and each lands in the console.
221
241
 
222
242
  ```ts
223
243
  onError: (err) => {
@@ -238,8 +258,6 @@ This is not the diagnostics channel. A `Diagnostic` is about the **document** an
238
258
  el.textContent = doc.toMarkdown();
239
259
  ```
240
260
 
241
- A worked one holds the scroll across the swap and shows a throw in place; the call under it is still the one line above.
242
-
243
261
  ## Theming
244
262
 
245
263
  The surfaces carry the behavior against a neutral, overridable visual baseline: a set of `--qm-*` CSS custom properties you override on any ancestor. See [`THEMING.md`](THEMING.md).
@@ -250,7 +268,7 @@ This package is one workspace of [`quillmark-js`](../..); the gates are the root
250
268
 
251
269
  ```sh
252
270
  npm run build -w packages/svelte # svelte-package → dist/
253
- npm run test -w packages/svelte # Vitest (codec, diagnostics, geometry, chain)
271
+ npm run test -w packages/svelte # Vitest
254
272
  npm run check -w packages/svelte # svelte-check
255
273
  npm run dev # the playground, from the root
256
274
  ```
package/THEMING.md CHANGED
@@ -18,7 +18,7 @@ There are three depths to that. An app mounting into an unstyled page stops at t
18
18
 
19
19
  Nothing to import and nothing to set. The package pulls its own stylesheet, and each surface owns its whole **column**, not only the cards in it: the gutter the stack sits in, the tone behind it, and (for the preview) the desk the painted sheet floats on. A bare `<div>` is a mounting site.
20
20
 
21
- **Mounting into a fixed-height pane**: pass `class="qm-pane"`. The surface becomes its own scroll container and takes a scroll tail, so the last card can be read at the middle of your pane rather than against its bottom edge. Without it the editor grows to its content and your page scrolls, which is what you want when the editor _is_ the page. The preview always scrolls: a viewport onto a document has nothing else to be.
21
+ **Mounting into a fixed-height pane**: pass `class="qm-pane"`. The surface becomes its own scroll container and takes a scroll tail, so the last card can be read at the middle of your pane rather than against its bottom edge. The gutter around the cards narrows, since your frame already stands the stack off your page. Without it the editor grows to its content and your page scrolls, which is what you want when the editor _is_ the page. The preview always scrolls: a viewport onto a document has nothing else to be.
22
22
 
23
23
  `.qm-pane` is opt-in on the editor rather than the default because the editor is what its own popovers portal into, and a scroll container clips them. In a pane you would have had that clipping from your own scrolling frame anyway; in a page you should not have it at all.
24
24
 
@@ -127,7 +127,7 @@ import '@quillmark/svelte/preset';
127
127
  </body>
128
128
  ```
129
129
 
130
- This is the stylesheet our own apps import: the playground and studio draw with these rules and nothing else, so what you get is what we ship, not a reduction of it.
130
+ This is the stylesheet our own apps import: the playground, studio and quillkit's app client draw with these rules and nothing else, so what you get is what we ship, not a reduction of it.
131
131
 
132
132
  It carries a `--qmh-*` scale for the page, derived from the same nine dials and calibrated against the surfaces' own steps, so a plate of yours and a card of ours agree about which way "raised" goes. Turning a dial moves both.
133
133
 
@@ -173,13 +173,15 @@ An app that mounts an editor beside a preview over one session needs a screen to
173
173
  | `.qm-bar` | The row a band puts its parts on, wrapping and centred. The rule under it, the gutter beside it and its depth stay yours: that is your page shell. |
174
174
  | `.qm-split` | Two even tracks for two surfaces, the page gap between them. Under `50rem` wide or `30rem` tall, one track at a time; `data-qm-show="1"\|"2"` says which. |
175
175
  | `.qm-switch` | The band that says which track is showing, and nothing at the widths where both are. Its controls read as even halves and stand at the touch floor (`--qmh-tap`, 44px); depth past that is yours. |
176
- | `.qm-frame` | The mounting site: an edge, a corner, and the clip that holds a surface to them. What is inside it is the surface's own. |
176
+ | `.qm-frame` | The mounting site: an edge, a corner, and the clip that holds a surface to them. Under the split's threshold it keeps only its top and bottom rules. What is inside it is the surface's own. |
177
177
 
178
178
  The narrow shape is one mount at a time, not two of them stacked: half a narrow viewport is under the width either surface reads at. The track you are not showing is `display: none`, so it leaves the tab order, hit-testing and the accessibility tree together: you need no `inert` on it, and no breakpoint in your JS, since the only state you hold is which of the two you last asked for. Keep both mounted: the hidden pane keeps its pixels, its caret and its scroll, and comes back to them — it rasters nothing at a box it has none of, and repaints only where it returns to a box that draws its pages at another scale. A preview scroll asked for while it was hidden is held and run on the way back, so it opens at the caret rather than several edits behind it. A click that lands in the hidden pane is the one thing the shape adds to your wiring, so send the caret and reveal the track together, or the hit goes somewhere the reader cannot see.
179
179
 
180
+ Every rule behind the five is wrapped in `:where()`, so it computes at specificity zero: a rule of yours on the same element wins at any specificity and in any sheet order, which is what lets a stylesheet written by someone other than the app's author (an `app.css` over quillkit's app client) restyle a band without knowing which shell rules set it.
181
+
180
182
  The shape is shared; the look of a band is not. Your wordmark, your nav and the depth of your head are the most app-specific things on the screen, and a preset that drew them would hand every tool built on this the same face, so they read `--qmh-*` like everything else and you write the rules. `.qm-split`'s gap is a default rather than a fact about splitting: close it to `0` and carry a hairline on one pane if you want the two mounts to meet instead of stand apart.
181
183
 
182
- It is **unlayered** and at ordinary specificity, so your own rules beat it by ordinary precedence. It is also opt-in and side-effect free until imported: nothing that styles your document arrives with a component.
184
+ It is **unlayered**, and nothing in it outranks a rule of yours aimed at the same element: the page, type, status and control recipes compute at ordinary specificity, so your own rules beat them by ordinary precedence, and the shell and the app chrome compute at specificity zero, as the surfaces do. It is also opt-in and side-effect free until imported: nothing that styles your document arrives with a component.
183
185
 
184
186
  One rule in it earns its place regardless of whether you take the rest, and you want it if you draw a focus ring of your own — cut the mounted surfaces out of the blanket:
185
187
 
@@ -191,6 +193,38 @@ One rule in it earns its place regardless of whether you take the rest, and you
191
193
 
192
194
  A blanket `:focus-visible` of yours outranks ours, including on the controls that already draw one, so a ring written for the controls that draw none repaints the ones that do. The cut-out leaves yours everywhere else, and `:not(:where(…))` contributes no specificity, so your rule ranks exactly as it would without it.
193
195
 
196
+ ### The app chrome
197
+
198
+ A tool's bands are yours, since a tool's head is the most app-specific thing on its screen. An app whose whole page is one form to fill in has less to say there: a name, the action the page leads to, and a line signing it. Four classes draw that much, quillkit's app client wears them, and a host of its own takes them the same way:
199
+
200
+ | Class | What it is |
201
+ | ----------------- | --------------------------------------------------------------------------------------------------------------- |
202
+ | `.qm-app-header` | The band naming the app: a plate, a gutter and a hairline under it. Put it beside `.qm-bar`. |
203
+ | `.qm-app-title` | What the app is called, at the title rung: the one line on the screen read as a name. |
204
+ | `.qm-app-footer` | The line signing the app, in meta type on the head's plate under a hairline; its links take the line's own ink. |
205
+ | `.qm-app-primary` | The one action the page leads to: the control wearing `--qm-accent`, standing at the touch floor. |
206
+
207
+ They compute at specificity zero, as the shell does, so `.qm-app-header { background: #00308f; color: #fff; }` is the whole of rebranding the head.
208
+
209
+ ## What a stylesheet may aim at
210
+
211
+ A stylesheet you write against a mounted surface, or one an author writes over an app someone else built, may aim at these and nothing else:
212
+
213
+ - the nine dials (§"The dials");
214
+ - the surface roots, `.qm-editor` and `.qm-preview`, and `.qm-pane`;
215
+ - `data-qm-card="<kind>"`, on the box of every card the editor draws, its value the card's kind and `main` for the main card;
216
+ - the preset's classes, on your own DOM, if you import it (§"Match ours").
217
+
218
+ `data-qm-card` is the one hook named by the schema rather than by the package: a rule aimed at a kind lands on that kind's cards wherever the editor puts them, which is the one thing a quill-specific restyle needs and a dial cannot say.
219
+
220
+ ```css
221
+ [data-qm-card='indorsement'] {
222
+ border-left: 3px solid var(--qm-accent);
223
+ }
224
+ ```
225
+
226
+ Nothing names a field or a property. Every other `data-qm-*` attribute the surfaces carry, `data-qm-prop` among them, is something the surface queries itself, and moves when it does.
227
+
194
228
  ## What is deliberately not public
195
229
 
196
230
  The two root classes and `.qm-pane` are the whole class contract on the surfaces. Every other `qm-*` class inside them is internal and renames without notice: a class contract over the interior would freeze the DOM shape the surfaces are free to re-cut, and the dials already reach the values a restyle wants. The preset's classes are a contract of their own: they land on _your_ DOM, so they freeze nothing of ours.
@@ -200,3 +234,5 @@ The derived surface scale (surface / border / ink rungs, the blur radius, the po
200
234
  ## Requirements
201
235
 
202
236
  `:where()`, `color-mix()`, `light-dark()` and relative colour syntax (`oklab(from …)`): Baseline since 2021, 2023, 2024 and 2024 respectively — the last is what lets a plane step by the card's own lightness. `@property` is the one feature the surface does not require: where it is missing, an invalid length dial collapses the surface rather than falling back.
237
+
238
+ Build your stylesheets for browsers that carry `light-dark()` natively. A minifier targeting older ones lowers each pole to a pair of variables only an element declaring `color-scheme` defines, and the preset states its poles at `:root`, where that pair is two colours at once and no colour: the primary press and the error hue draw nothing. Under Vite, `build.cssTarget: ['chrome123', 'edge123', 'firefox128', 'safari18']`.
@@ -1,22 +1,16 @@
1
1
  import type { Addr, HitGranularity, PathStep } from '@quillmark/wasm';
2
2
  /**
3
- * A canonical field address: `main.<field>` / `main.body` /
4
- * `cards.<kind>[<i>].<field>`, cards keyed by absolute document index.
5
- *
6
- * The grammar `parseDocPath` / `formatDocPath` speak, and the one
7
- * `Diagnostic.path`, `ContentHit.field`, `FieldRegion.field` and
8
- * `session.regions()` keys already use. An alias over `string`: the boundary
9
- * exports the parser and the segment type, not a nominal type, so this names the
10
- * grammar at a signature rather than inventing a checked one.
3
+ * A canonical field address: `main.<field>` / `main.body` / `cards.<kind>[<i>].<field>`, cards
4
+ * keyed by absolute document index. The grammar `parseDocPath` / `formatDocPath` speak, and the one
5
+ * `Diagnostic.path`, `ContentHit.field`, `FieldRegion.field` and `session.regions()` keys use. An
6
+ * alias over `string`: the boundary exports no nominal type.
11
7
  */
12
8
  export type DocPath = string;
13
9
  /**
14
- * A place in the document: a field and a caret within it. What the editor reports
15
- * when its caret moves and what the preview scrolls to, so the editor→preview hop
16
- * is `onCaretMove={preview.focusPosition}` and translates nothing.
17
- *
18
- * `pos` is USV, the shared content coordinate on both sides of the boundary: a
19
- * `ContentHit` is a `Place` with its own extras, and fits wherever one is taken.
10
+ * A place in the document: a field and a caret within it. What the editor reports when its caret
11
+ * moves and what the preview scrolls to, so `onCaretMove={preview.focusPosition}` translates
12
+ * nothing. `pos` is USV, the coordinate shared on both sides of the boundary; a `ContentHit` is a
13
+ * `Place` with extras.
20
14
  */
21
15
  export interface Place {
22
16
  field: DocPath;
@@ -24,13 +18,10 @@ export interface Place {
24
18
  pos: number;
25
19
  }
26
20
  /**
27
- * Where a preview click landed. What the preview surfaces and what the editor's
28
- * `setCaret` takes.
29
- *
30
- * **An absent `pos` is the placement rung**, not a caret at zero: the click resolved
31
- * a field the plate places without tracking its content (`fieldAt` answers,
32
- * `positionAt` does not), so there is no offset and the landing is a focus. A `Place`
33
- * is a landing carrying its caret, and a `ContentHit` is one too.
21
+ * Where a preview click landed: what the preview surfaces and the editor's `setCaret` takes. **An
22
+ * absent `pos` is the placement rung**, not a caret at zero: the click resolved a field the plate
23
+ * places without tracking its content (`fieldAt` answers, `positionAt` does not), so the landing is
24
+ * a focus.
34
25
  */
35
26
  export interface Landing {
36
27
  field: DocPath;
@@ -41,67 +32,47 @@ export interface Landing {
41
32
  granularity?: HitGranularity;
42
33
  }
43
34
  /**
44
- * A field's canonical `DocPath`, or `undefined` when `addr.card` is outside the live
45
- * `kinds` array (a stale address: drop it rather than mis-target). A field-less card
46
- * addresses its body, which is the leaf `{card: i}` names.
35
+ * A field's canonical `DocPath`, or `undefined` when `addr.card` is outside the live `kinds` array
36
+ * (a stale address: drop it rather than mis-target). A field-less card addresses its body.
47
37
  *
48
38
  * - `{}` → `"main.body"`
49
39
  * - `{field}` → `"main.<field>"`
50
40
  * - `{card: i}` → `"cards.<kind>[i].body"`
51
41
  * - `{card: i, field}` → `"cards.<kind>[i].<field>"`
52
42
  *
53
- * `steps` descend from the field to a leaf inside it, the inverse of
54
- * {@link nestedAddrForFieldPath}: `{field: 'contact'}` with `['email']` is
55
- * `"main.contact.email"`, `{field: 'keywords'}` with `[0]` is `"main.keywords[0]"`. A body
56
- * holds no cells, so a field-less address with steps is `undefined`.
57
- *
58
- * `cards[i]` stands in for an unknown or blank kind. The index is the addr's own: no
59
- * per-kind counting, since `DocPath` addresses cards by document-array index.
43
+ * `steps` descend from the field to a leaf inside it, the inverse of {@link
44
+ * nestedAddrForFieldPath}: `{field: 'keywords'}` with `[0]` is `"main.keywords[0]"`. A body holds
45
+ * no cells, so a field-less address with steps is `undefined`. `cards[i]` stands in for an unknown
46
+ * or blank kind; the index is the addr's own, `DocPath` addressing cards by document-array index.
60
47
  */
61
48
  export declare function fieldPathForAddr(addr: Addr, kinds: readonly string[], steps?: readonly PathStep[]): DocPath | undefined;
62
49
  /**
63
- * A card's own path (`cards.<kind>[i]`), not a leaf's: what a structure op names,
64
- * where the change is the card rather than anything inside it. `undefined` for an
65
- * index outside `kinds`.
50
+ * A card's own path (`cards.<kind>[i]`), what a structure op names. `undefined` for an index
51
+ * outside `kinds`.
66
52
  */
67
53
  export declare function cardPath(index: number, kinds: readonly string[]): DocPath | undefined;
68
54
  /**
69
- * The inverse: a canonical `DocPath` back to the `Addr` the document verbs take, or
70
- * `undefined` for a path that names no single commit address — a nested or
71
- * array-element path (`main.keywords[0]`, which {@link nestedAddrForFieldPath} and
72
- * {@link nearestAddrForFieldPath} take instead), a field-rooted one, or a malformed
73
- * one. A bare card and a `.body` terminal both land on the field-less `{card: i}`
74
- * the body leaf answers to.
75
- *
76
- * Needs no `kinds`: the path carries the absolute index, and the kind in it is
77
- * decoration the `Addr` has no room for.
55
+ * The inverse: a canonical `DocPath` back to the `Addr` the document verbs take, or `undefined` for
56
+ * a path that names no single commit address: a nested or array-element path (see {@link
57
+ * nestedAddrForFieldPath} and {@link nearestAddrForFieldPath}), a field-rooted one, or a malformed
58
+ * one. A bare card and a `.body` terminal both land on the field-less `{card: i}`. Needs no
59
+ * `kinds`: the path carries the absolute index.
78
60
  */
79
61
  export declare function addrForFieldPath(path: DocPath): Addr | undefined;
80
62
  /**
81
- * The nearest ancestor a commit address can name: {@link addrForFieldPath} where the
82
- * path has one, else the root and its first field. `main.contact.email` and
83
- * `main.keywords[0]` land on their field, any deeper nesting on the top-level field
84
- * it hangs off. `undefined` where those two segments name nothing — a field-rooted
85
- * (config-space) or malformed path.
86
- *
87
- * An `Addr` reaches a root and one field, so two segments is the whole search: a
88
- * deeper prefix is unaddressable by arity alone. Addressability being `Addr`'s reach
89
- * rather than the grammar's is what makes the truncation a consumer's to make
90
- * (VISUAL_EDITOR §Diagnostics).
63
+ * The nearest ancestor a commit address can name: {@link addrForFieldPath} where the path has one,
64
+ * else the root and its first field (`main.contact.email` and `main.keywords[0]` land on their
65
+ * field). `undefined` for a field-rooted (config-space) or malformed path. An `Addr` reaches a root
66
+ * and one field, so the truncation is a consumer's to make (VISUAL_EDITOR §Diagnostics).
91
67
  */
92
68
  export declare function nearestAddrForFieldPath(path: DocPath): Addr | undefined;
93
69
  /**
94
- * A nested address, split: the field's `Addr`, and the steps from it to the leaf.
95
- * `main.keywords[0]` is `{field: keywords, steps: [0]}`,
96
- * `main.vectors[0].tours[2].title` is `[0, 'tours', 2, 'title']`, and
97
- * `main.contact.email` is `['email']`: an index segment reads as a number and a key as
98
- * a string, the `PathStep` grammar `reader.getContentAt` walks. Anything
99
- * {@link addrForFieldPath} can name — a field, a body, a card — has no steps and lands
100
- * here as `undefined`; so does a body with something after it, a body holding no
101
- * cells. Whether the schema declares each step is the caller's to check: this module
102
- * holds the grammar and no schema. `regions()`, `positionAt` and `formatDocPath` all
103
- * spell the index segment bracketed, so there is one spelling to read and none to
104
- * bridge.
70
+ * A nested address, split: the field's `Addr`, and the steps from it to the leaf (the `PathStep`
71
+ * grammar `reader.getContentAt` walks). `main.keywords[0]` is `{field: keywords, steps: [0]}`,
72
+ * `main.contact.email` is `['email']`, `main.vectors[0].tours[2].title` is `[0, 'tours', 2,
73
+ * 'title']`. Anything {@link addrForFieldPath} can name has no steps and lands here as `undefined`,
74
+ * as does a body with something after it. Whether the schema declares each step is the caller's to
75
+ * check.
105
76
  */
106
77
  export declare function nestedAddrForFieldPath(path: DocPath): NestedAddr | undefined;
107
78
  /** A nested address: the field's `Addr`, and the steps from it to the leaf. */
@@ -1,27 +1,21 @@
1
1
  import { core } from './lifecycle.js';
2
2
  // ── Addr ↔ DocPath ──────────────────────────────────────────────────────────
3
- // The one hop between the two vocabularies, both directions, pure and
4
- // document-free: `kinds` is the kind of each composable card by content index
5
- // (`doc.cards.map(c => c.kind)`), which is all the card segment needs. The editor
6
- // mints it off its own derived card tree rather than re-reading `doc.cards`, which
7
- // serializes every card per read and is not a thing to do per keystroke.
3
+ // The one hop between the two vocabularies, pure and document-free: `kinds` is the kind of each
4
+ // card by content index (`doc.cards.map(c => c.kind)`). The editor mints it off its own derived
5
+ // card tree, since `doc.cards` serializes every card per read.
8
6
  /**
9
- * A field's canonical `DocPath`, or `undefined` when `addr.card` is outside the live
10
- * `kinds` array (a stale address: drop it rather than mis-target). A field-less card
11
- * addresses its body, which is the leaf `{card: i}` names.
7
+ * A field's canonical `DocPath`, or `undefined` when `addr.card` is outside the live `kinds` array
8
+ * (a stale address: drop it rather than mis-target). A field-less card addresses its body.
12
9
  *
13
10
  * - `{}` → `"main.body"`
14
11
  * - `{field}` → `"main.<field>"`
15
12
  * - `{card: i}` → `"cards.<kind>[i].body"`
16
13
  * - `{card: i, field}` → `"cards.<kind>[i].<field>"`
17
14
  *
18
- * `steps` descend from the field to a leaf inside it, the inverse of
19
- * {@link nestedAddrForFieldPath}: `{field: 'contact'}` with `['email']` is
20
- * `"main.contact.email"`, `{field: 'keywords'}` with `[0]` is `"main.keywords[0]"`. A body
21
- * holds no cells, so a field-less address with steps is `undefined`.
22
- *
23
- * `cards[i]` stands in for an unknown or blank kind. The index is the addr's own: no
24
- * per-kind counting, since `DocPath` addresses cards by document-array index.
15
+ * `steps` descend from the field to a leaf inside it, the inverse of {@link
16
+ * nestedAddrForFieldPath}: `{field: 'keywords'}` with `[0]` is `"main.keywords[0]"`. A body holds
17
+ * no cells, so a field-less address with steps is `undefined`. `cards[i]` stands in for an unknown
18
+ * or blank kind; the index is the addr's own, `DocPath` addressing cards by document-array index.
25
19
  */
26
20
  export function fieldPathForAddr(addr, kinds, steps = []) {
27
21
  const head = cardHead(addr.card, kinds);
@@ -35,57 +29,41 @@ export function fieldPathForAddr(addr, kinds, steps = []) {
35
29
  return core().formatDocPath(segs);
36
30
  }
37
31
  /**
38
- * A card's own path (`cards.<kind>[i]`), not a leaf's: what a structure op names,
39
- * where the change is the card rather than anything inside it. `undefined` for an
40
- * index outside `kinds`.
32
+ * A card's own path (`cards.<kind>[i]`), what a structure op names. `undefined` for an index
33
+ * outside `kinds`.
41
34
  */
42
35
  export function cardPath(index, kinds) {
43
36
  const head = cardHead(index, kinds);
44
37
  return head ? core().formatDocPath([head]) : undefined;
45
38
  }
46
39
  /**
47
- * The inverse: a canonical `DocPath` back to the `Addr` the document verbs take, or
48
- * `undefined` for a path that names no single commit address — a nested or
49
- * array-element path (`main.keywords[0]`, which {@link nestedAddrForFieldPath} and
50
- * {@link nearestAddrForFieldPath} take instead), a field-rooted one, or a malformed
51
- * one. A bare card and a `.body` terminal both land on the field-less `{card: i}`
52
- * the body leaf answers to.
53
- *
54
- * Needs no `kinds`: the path carries the absolute index, and the kind in it is
55
- * decoration the `Addr` has no room for.
40
+ * The inverse: a canonical `DocPath` back to the `Addr` the document verbs take, or `undefined` for
41
+ * a path that names no single commit address: a nested or array-element path (see {@link
42
+ * nestedAddrForFieldPath} and {@link nearestAddrForFieldPath}), a field-rooted one, or a malformed
43
+ * one. A bare card and a `.body` terminal both land on the field-less `{card: i}`. Needs no
44
+ * `kinds`: the path carries the absolute index.
56
45
  */
57
46
  export function addrForFieldPath(path) {
58
47
  const segs = segsOf(path);
59
48
  return segs && addrForSegs(segs);
60
49
  }
61
50
  /**
62
- * The nearest ancestor a commit address can name: {@link addrForFieldPath} where the
63
- * path has one, else the root and its first field. `main.contact.email` and
64
- * `main.keywords[0]` land on their field, any deeper nesting on the top-level field
65
- * it hangs off. `undefined` where those two segments name nothing — a field-rooted
66
- * (config-space) or malformed path.
67
- *
68
- * An `Addr` reaches a root and one field, so two segments is the whole search: a
69
- * deeper prefix is unaddressable by arity alone. Addressability being `Addr`'s reach
70
- * rather than the grammar's is what makes the truncation a consumer's to make
71
- * (VISUAL_EDITOR §Diagnostics).
51
+ * The nearest ancestor a commit address can name: {@link addrForFieldPath} where the path has one,
52
+ * else the root and its first field (`main.contact.email` and `main.keywords[0]` land on their
53
+ * field). `undefined` for a field-rooted (config-space) or malformed path. An `Addr` reaches a root
54
+ * and one field, so the truncation is a consumer's to make (VISUAL_EDITOR §Diagnostics).
72
55
  */
73
56
  export function nearestAddrForFieldPath(path) {
74
57
  const segs = segsOf(path);
75
58
  return segs && addrForSegs(segs.slice(0, 2));
76
59
  }
77
60
  /**
78
- * A nested address, split: the field's `Addr`, and the steps from it to the leaf.
79
- * `main.keywords[0]` is `{field: keywords, steps: [0]}`,
80
- * `main.vectors[0].tours[2].title` is `[0, 'tours', 2, 'title']`, and
81
- * `main.contact.email` is `['email']`: an index segment reads as a number and a key as
82
- * a string, the `PathStep` grammar `reader.getContentAt` walks. Anything
83
- * {@link addrForFieldPath} can name — a field, a body, a card — has no steps and lands
84
- * here as `undefined`; so does a body with something after it, a body holding no
85
- * cells. Whether the schema declares each step is the caller's to check: this module
86
- * holds the grammar and no schema. `regions()`, `positionAt` and `formatDocPath` all
87
- * spell the index segment bracketed, so there is one spelling to read and none to
88
- * bridge.
61
+ * A nested address, split: the field's `Addr`, and the steps from it to the leaf (the `PathStep`
62
+ * grammar `reader.getContentAt` walks). `main.keywords[0]` is `{field: keywords, steps: [0]}`,
63
+ * `main.contact.email` is `['email']`, `main.vectors[0].tours[2].title` is `[0, 'tours', 2,
64
+ * 'title']`. Anything {@link addrForFieldPath} can name has no steps and lands here as `undefined`,
65
+ * as does a body with something after it. Whether the schema declares each step is the caller's to
66
+ * check.
89
67
  */
90
68
  export function nestedAddrForFieldPath(path) {
91
69
  const segs = segsOf(path);
@@ -105,9 +83,10 @@ export function nestedAddrForFieldPath(path) {
105
83
  }
106
84
  return { field, steps };
107
85
  }
108
- /** `parseDocPath`, with a malformed path as `undefined` rather than a throw. The gate
109
- * is read outside the try: an uninitialized core is not a malformed path, and would
110
- * otherwise leave here as one. */
86
+ /**
87
+ * `parseDocPath`, with a malformed path as `undefined` rather than a throw. The gate is read
88
+ * outside the try: an uninitialized core is not a malformed path.
89
+ */
111
90
  function segsOf(path) {
112
91
  const { parseDocPath } = core();
113
92
  try {
@@ -117,8 +96,7 @@ function segsOf(path) {
117
96
  return undefined;
118
97
  }
119
98
  }
120
- /** {@link addrForFieldPath} over already-parsed segments, so the element walk reuses
121
- * it for the head of a path rather than re-serializing one. */
99
+ /** {@link addrForFieldPath} over already-parsed segments. */
122
100
  function addrForSegs(segs) {
123
101
  const [head, ...rest] = segs;
124
102
  if (!head)
@@ -141,8 +119,10 @@ function addrForSegs(segs) {
141
119
  return { ...addr, field: tail.name };
142
120
  return undefined;
143
121
  }
144
- /** The head segment a card index resolves to, or `undefined` when it is out of the
145
- * live array. `null` kind is the unknown-kind form the grammar spells `cards[i]`. */
122
+ /**
123
+ * The head segment a card index resolves to, or `undefined` when out of the live array. A falsy
124
+ * kind is the unknown-kind form `cards[i]`.
125
+ */
146
126
  function cardHead(index, kinds) {
147
127
  if (index == null)
148
128
  return { seg: 'main' };
@@ -1,13 +1,7 @@
1
1
  /**
2
- * Run one wash over `host`, a `.qm-wash` box, resolving the dwell off its root so the
3
- * number is single-sourced in CSS (`core/rungs.ts`).
4
- *
5
- * One property, both motion preferences: `--_qm-bloom-dwell` already shortens under
6
- * `prefers-reduced-motion` in the derivation, so what is left to `matchMedia` here is
7
- * the shape: a hold and a cut carry no ramps, which is a frame list rather than a
8
- * number and the one half CSS cannot hand over.
9
- *
10
- * A run in flight on the same host is cancelled first: two quick landings on one leaf
11
- * must not stack two alphas over it.
2
+ * Run one wash over `host`, a `.qm-wash` box, resolving the dwell off its root so the number is
3
+ * single-sourced in CSS (`core/rungs.ts`). `--_qm-bloom-dwell` already shortens under
4
+ * `prefers-reduced-motion`, so `matchMedia` here only picks the frame list. A run in flight on the
5
+ * same host is cancelled first, so two quick landings do not stack alphas.
12
6
  */
13
7
  export declare function bloom(host: HTMLElement): void;
@@ -1,24 +1,17 @@
1
- // The arrival wash: a soft accent bloom that rises over a leaf the editor just
2
- // landed on and decays to nothing. A landing is an event, not a state, so a
3
- // highlight that outlives the hop that caused it is ink nothing asked for.
1
+ // The arrival wash: a soft accent bloom that rises over a leaf the editor just landed on and decays
2
+ // to nothing. A landing is an event, not a state, so a highlight that outlives the hop is ink
3
+ // nothing asked for.
4
4
  //
5
- // Opacity is the animated property, never the colour. The wash's alpha is a peak
6
- // fixed by `--_qm-accent-wash`, so interpolating 0→1→0 keeps the scale in CSS where
7
- // `color-mix` resolves, instead of in keyframe values a script would have to
8
- // pre-resolve and re-resolve on a theme change.
9
- //
10
- // The wash is the host's `::after` (`.qm-wash`, controls.css), drawn from mount: this
11
- // module plays it and adds nothing to the tree.
5
+ // Opacity is the animated property, never the colour: the wash's alpha is a peak fixed by
6
+ // `--_qm-accent-wash`, so the scale stays in CSS where `color-mix` resolves. The wash is the host's
7
+ // `::after` (`.qm-wash`, controls.css); this module plays it and adds nothing to the tree.
12
8
  import { rungStyle } from './rungs.js';
13
9
  const WASH = '::after';
14
- /** The dwell used when the derivation is out of reach (an unstyled root, or jsdom,
15
- * where `getComputedStyle` reports custom properties as empty). A fallback is the
16
- * absence of the scale, not a copy of it. */
10
+ /** The dwell used when the derivation is out of reach (an unstyled root, or jsdom). */
17
11
  const FALLBACK_MS = 1100;
18
- // Rise fast, hold, then leave slowly: a highlighter's decay, not a linear fade. A
19
- // keyframe's easing governs the segment that starts at it. Under reduced motion the
20
- // ramps are dropped entirely: the wash holds at full for a beat and cuts, which
21
- // still answers "here" with nothing to track.
12
+ // Rise fast, hold, then leave slowly: a highlighter's decay. A keyframe's easing governs the
13
+ // segment that starts at it. Under reduced motion the ramps are dropped: the wash holds at full for
14
+ // a beat and cuts.
22
15
  const FRAMES = [
23
16
  { opacity: 0, offset: 0, easing: 'ease-out' },
24
17
  { opacity: 1, offset: 0.08 },
@@ -27,16 +20,10 @@ const FRAMES = [
27
20
  ];
28
21
  const FRAMES_REDUCED = [{ opacity: 1 }, { opacity: 1 }];
29
22
  /**
30
- * Run one wash over `host`, a `.qm-wash` box, resolving the dwell off its root so the
31
- * number is single-sourced in CSS (`core/rungs.ts`).
32
- *
33
- * One property, both motion preferences: `--_qm-bloom-dwell` already shortens under
34
- * `prefers-reduced-motion` in the derivation, so what is left to `matchMedia` here is
35
- * the shape: a hold and a cut carry no ramps, which is a frame list rather than a
36
- * number and the one half CSS cannot hand over.
37
- *
38
- * A run in flight on the same host is cancelled first: two quick landings on one leaf
39
- * must not stack two alphas over it.
23
+ * Run one wash over `host`, a `.qm-wash` box, resolving the dwell off its root so the number is
24
+ * single-sourced in CSS (`core/rungs.ts`). `--_qm-bloom-dwell` already shortens under
25
+ * `prefers-reduced-motion`, so `matchMedia` here only picks the frame list. A run in flight on the
26
+ * same host is cancelled first, so two quick landings do not stack alphas.
40
27
  */
41
28
  export function bloom(host) {
42
29
  if (typeof host.animate !== 'function')