@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
package/README.md CHANGED
@@ -26,10 +26,12 @@ Each subpath is its own module root; a bundler pulls only what the entry you imp
26
26
  The **consumer** owns the session and the handles, and drives every edit; the surfaces are views over it. The handles come from the `@quillmark/wasm` peer, never re-exported here.
27
27
 
28
28
  ```ts
29
- import { Document, Engine, Quill } from '@quillmark/wasm';
29
+ import { Engine } from '@quillmark/wasm';
30
30
  import { init } from '@quillmark/svelte/core';
31
31
 
32
- init(); // one-time WASM panic-hook install
32
+ // One-time, and the only door to Quill and Document: the artifact exports
33
+ // neither statically.
34
+ const { Quill, Document } = await init();
33
35
 
34
36
  const quill = Quill.fromTree(tree); // tree: Map<string, Uint8Array> of the quill dir
35
37
 
@@ -49,7 +51,7 @@ const session = await new Engine().open(quill, doc);
49
51
  A stored document carries none of its quill's bytes, only a reference: `doc.quillRef` is `name@version`, persisted in the markdown itself. Resolution is **host code**: read the ref, map it to a `Quill`, open. No surface resolves, so one resolution per document holds by construction.
50
52
 
51
53
  ```ts
52
- const doc = Document.fromMarkdown(stored);
54
+ const doc = Document.fromMarkdown(stored); // Document, from the gate above
53
55
  const quill = await registry.getQuill(doc.quillRef); // your ref → Quill mapping
54
56
  const session = await new Engine().open(quill, doc);
55
57
  ```
@@ -67,8 +69,8 @@ import { createPreview } from '@quillmark/svelte/preview';
67
69
 
68
70
  const preview = createPreview(session, {
69
71
  container: document.querySelector('#preview')!,
70
- onCaretPick: (hit) => {
71
- /* a click resolved to a content position; see the bridge below */
72
+ onPick: (at) => {
73
+ /* a click resolved to an address, with a caret where the compile has one */
72
74
  },
73
75
  onError: (err) => {
74
76
  /* a page paint the backend refused; the preview shows its error state either way */
@@ -79,9 +81,9 @@ const preview = createPreview(session, {
79
81
  preview.refresh(session.update(doc)); // repaint dirtyPages ∩ visible
80
82
  ```
81
83
 
82
- In Svelte, `<Preview {session} onCaretPick={…} />` exposes the same verbs (`refresh`, `scrollToField`, `focusPosition`, `setZoom`) via `bind:this`.
84
+ In Svelte, `<Preview {session} onPick={…} />` exposes the same verbs (`refresh`, `scrollToField`, `focusPosition`, `setZoom`) via `bind:this`.
83
85
 
84
- `<Preview>` binds **once**, at mount: swapping `session`, `margin`, `overlays`, `onCaretPick`, `onError` or `strings` in place changes nothing on screen and reports `rebind-ignored` through `onError` at `dev` severity, naming the prop. Swap by remounting (`{#key session}`); drive in-place edits through `refresh(change)`. `class` and `style` are the exceptions, landing on the root element and staying live.
86
+ `<Preview>` binds **once**, at mount: swapping `session`, `margin`, `overlays`, `onPick`, `onError` or `strings` in place changes nothing on screen and reports `rebind-ignored` through `onError` at `dev` severity, naming the prop. Swap by remounting (`{#key session}`); drive in-place edits through `refresh(change)`. `class` and `style` are the exceptions, landing on the root element and staying live.
85
87
 
86
88
  ## Visual editor
87
89
 
@@ -135,7 +137,7 @@ Hand it a different `doc` and the editor **re-keys itself**: every leaf remounts
135
137
  <button onclick={() => editor?.focusField('main.subject')}>Jump to subject</button>
136
138
  ```
137
139
 
138
- `insertCard` hands back the new card's `cardId`; `removeCard`, `moveCard` and `setKind` take one. A card key or a path the surface does not hold is a no-op that reports `target-unknown` through `onError` at `dev` severity.
140
+ `insertCard` hands back the new card's `cardId`; `removeCard`, `moveCard` and `setKind` take one. `focusField` reaches any mounted field: a prose leaf takes its view's focus, a form control the same handoff a click on its label takes, and either way the group holding it is revealed first. A card key or a path the surface does not hold is a no-op that reports `target-unknown` through `onError` at `dev` severity.
139
141
 
140
142
  ### Wording
141
143
 
@@ -147,7 +149,7 @@ The package ships English and a seam to replace it. `strings` is keyed and **par
147
149
  {quill}
148
150
  strings={{
149
151
  cardDelete: 'Supprimer la carte',
150
- addCardOfKind: (kind) => `Ajouter : ${kind}`
152
+ addCard: '+ Ajouter une carte'
151
153
  }}
152
154
  formatDiagnostic={(d) =>
153
155
  d.code === 'validation::must_fill' ? `${d.path} est obligatoire` : undefined}
@@ -191,13 +193,15 @@ This shell layer is deliberately yours: the debounce value, what applies at once
191
193
  The bridge lives at the **consumer** layer and is opt-in; the editor is unaware of the preview, the preview unaware of the editor. Both hops are pass-throughs: the two surfaces already speak one address grammar, the canonical `DocPath`.
192
194
 
193
195
  ```ts
194
- // preview → editor: a click resolved to a content position places the caret.
195
- onCaretPick: (hit) => visualEditor.setCaret(hit);
196
+ // preview → editor: a click resolved to an address lands in the field it names.
197
+ onPick: (at) => visualEditor.setCaret(at);
196
198
 
197
199
  // editor → preview: a caret move scrolls the preview to follow it.
198
200
  onCaretMove: preview.focusPosition;
199
201
  ```
200
202
 
203
+ A pick carries a caret where the compile tracks the content under the point, and the field alone where it tracks only the placement — a scalar the plate prints without tracking. `setCaret` takes both: an absent `pos` reveals and focuses the field, which is the whole of what a click on plate-placed ink can mean. The address may name an array element (`main.references.0`), which lands on that row.
204
+
201
205
  A consumer holding an `Addr` of its own maps it with `fieldPathForAddr` (from `@quillmark/svelte/core`). The playground app's split-pane route is the full reference shell: one session, both bridge directions, the preview following edits, and diagnostics routed inline.
202
206
 
203
207
  ## Errors
@@ -223,7 +227,7 @@ This is not the diagnostics channel. A `Diagnostic` is about the **document** an
223
227
  el.textContent = doc.toMarkdown();
224
228
  ```
225
229
 
226
- The playground's debug drawer is the worked version, holding scroll across the swap and showing a throw in place: `packages/playground/src/routes/playground/SourceMirror.svelte`.
230
+ 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.
227
231
 
228
232
  ## Theming
229
233
 
package/THEMING.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  `@quillmark/svelte` ships complex UX over a thin skin: the surfaces carry the behavior (direct manipulation, the caret bridge, per-field state) against a neutral visual baseline a consumer restyles to its brand without fighting baked-in design.
4
4
 
5
- There are three depths to that, and most apps stop at the first:
5
+ There are three depths to that. An app mounting into an unstyled page stops at the first; an app with a plate of its own starts at the second:
6
6
 
7
7
  | | You write | You get |
8
8
  | ----------------- | --------------------- | ------------------------------------------------------- |
@@ -39,20 +39,28 @@ The whole contract is **ten CSS custom properties**. They are dials, not a palet
39
39
  }
40
40
  ```
41
41
 
42
+ ### Two of them are not branding
43
+
44
+ `--qm-bg` and `--qm-fg` are the two a themed host sets before any brand decision. Their defaults are a self-consistent neutral for a bare mounting site, not a guess at your page.
45
+
46
+ `--qm-bg` is the **card**, not the column behind it: the plane a document is written on, which is also the sheet the preview paints. Everything else is a step off it, and the column the cards sit in is a step down, so a mounted editor draws its own ground and the cards read as islands on it. Give it the tone you want a card to be — white, in most light themes — and let the column fall out. If you want the column to meet your page instead, set a `background` of your own on `.qm-editor`; your CSS is unlayered and wins.
47
+
48
+ The poles take a colour, not `transparent` or `inherit`: the raised surfaces, the borders and the muted inks are each a `color-mix` against `--qm-bg`, so a transparent pole makes every one of them translucent instead of letting your page through. Hand over the value your page is painted with and the derivation steps off it.
49
+
42
50
  ### The dials
43
51
 
44
- | Token | Default | What it sets |
45
- | ---------------- | -------------------------------------- | ------------------------------------------------------------------------------------------------------- |
46
- | `--qm-bg` | `#fff` / `#14171c` | Base surface. Cards, fields, the painted page, the popover, and the column behind them step off it. |
47
- | `--qm-fg` | `#1a1a1a` / `#e8eaed` | Base ink. Body text, labels, and borders step off it. |
48
- | `--qm-accent` | `#2563eb` | Focus rings, active marks, the preview's active field box. |
49
- | `--qm-danger` | `#c5221f` | Error diagnostics, the required marker, the delete glyph. |
50
- | `--qm-warning` | `#b25000` | Warning diagnostics. |
51
- | `--qm-font` | `ui-sans-serif, system-ui, sans-serif` | The editor surface's font family. Controls and buttons take it too, in place of the UA face. |
52
- | `--qm-font-mono` | `ui-monospace, monospace` | The monospace face: the JSON array control, the tips card. |
53
- | `--qm-font-size` | `0.875rem` | Body text: every control's size, and the anchor the ramp derives up (title) and down (label/meta) from. |
54
- | `--qm-radius` | `8px` | Card & popover corner. Interior controls derive a tighter tier (half). |
55
- | `--qm-space` | `0.25rem` | Spacing base. Gaps and insets are `half`/`1×`/`2×`/`3×`/`4×` multiples of it. |
52
+ | Token | Default | What it sets |
53
+ | ---------------- | -------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
54
+ | `--qm-bg` | `#fff` / `#272729` | Base surface: a card, and the preview's sheet. Fields, the popover and the column behind the cards each step off it. |
55
+ | `--qm-fg` | `#1c1c1c` / `#d7dadc` | Base ink. Body text, labels, and borders step off it. |
56
+ | `--qm-accent` | `#2563eb` | Focus rings, active marks, the preview's active field box. |
57
+ | `--qm-danger` | `#c5221f` | Error diagnostics, the required marker, the delete glyph. |
58
+ | `--qm-warning` | `#b25000` | Warning diagnostics. |
59
+ | `--qm-font` | `ui-sans-serif, system-ui, sans-serif` | The editor surface's font family. Controls and buttons take it too, in place of the UA face. |
60
+ | `--qm-font-mono` | `ui-monospace, monospace` | The monospace face: the JSON array control, the tips card. |
61
+ | `--qm-font-size` | `0.875rem` | Chrome text: every control's size, the anchor the ramp derives up (title) and down (label/meta) from, and the anchor a document body sits one small step over. |
62
+ | `--qm-radius` | `8px` | Card & popover corner. Interior controls derive a tighter tier (half), so a square brand is square throughout. |
63
+ | `--qm-space` | `0.25rem` | Spacing base. Gaps and insets are `half`/`1×`/`2×`/`3×`/`4×` multiples of it. |
56
64
 
57
65
  Give a length dial a length: `--qm-space: 4` is a valid custom property and an invalid length, so it substitutes through every `calc()` that reads it. Where `@property` is supported the surface absorbs that, landing on the package default rather than collapsing the padding to zero.
58
66
 
@@ -70,7 +78,9 @@ Declared before our sheet it fixes the order outright; declared after, `qm` is a
70
78
 
71
79
  ### The surface follows your colour scheme
72
80
 
73
- Surfaces step `bg → fg` and ink steps `fg → bg`, mixed in **oklab**, so inverting the two poles inverts the whole scale, including borders and the popover's translucent fill.
81
+ Ink steps `fg → bg` and the borders and hover fills step `bg → fg`, mixed in **oklab**, so inverting the two poles inverts all of them together.
82
+
83
+ **Planes are the exception**, and deliberately: the raised plane is the brighter one in both schemes, which is what makes a card read as a card rather than as a differently-tinted page. So the column under the cards sinks from `--qm-bg` toward shadow and a floating surface rises from it toward light, and neither reads the ink pole. Each direction takes a step sized to the scheme it is in, which is why pinning a dark palette while your `color-scheme` still says light leaves the column barely off its cards — the one case §"Make it yours" tells you to pin the scheme alongside the poles.
74
84
 
75
85
  Which way the poles default is **your `color-scheme`**, not the operating system's. Declare the scheme your app is in and the surface lands on it, along with native chrome, which reads the same property:
76
86
 
@@ -131,28 +141,41 @@ It carries a `--qmh-*` scale for the page, derived from the same ten dials and c
131
141
 
132
142
  ### The shell, if you are building a tool
133
143
 
134
- An app that mounts an editor beside a preview over one session needs a screen to put them in, and the SHAPE of that screen is the same every time. Four more classes are that shape, so what you write is the wiring rather than the grid:
144
+ An app that mounts an editor beside a preview over one session needs a screen to put them in, and the shape of that screen is the same every time. Five more classes are that shape, so what you write is the wiring rather than the grid:
135
145
 
136
146
  ```html
137
147
  <div class="qm-workspace">
138
148
  <header class="qm-bar site-head">
139
149
  <span class="site-mark">acme<span class="site-slash">/</span>studio</span>
140
150
  </header>
141
- <div class="qm-split">
142
- <section class="qm-frame"><!-- <VisualEditor class="qm-pane" /> --></section>
143
- <section class="qm-frame"><!-- <Preview /> --></section>
151
+ <div class="site-body">
152
+ <div class="qm-switch" role="group" aria-label="Visible pane">
153
+ <button class="qm-control" aria-pressed="{shown" ="" ="" ="1}" onclick="{()" ="">
154
+ (shown = 1)}>Editor
155
+ </button>
156
+ <button class="qm-control" aria-pressed="{shown" ="" ="" ="2}" onclick="{()" ="">
157
+ (shown = 2)}>Preview
158
+ </button>
159
+ </div>
160
+ <div class="qm-split" data-qm-show="{shown}">
161
+ <section class="qm-frame"><!-- <VisualEditor class="qm-pane" /> --></section>
162
+ <section class="qm-frame"><!-- <Preview /> --></section>
163
+ </div>
144
164
  </div>
145
165
  </div>
146
166
  ```
147
167
 
148
- | Class | What it is |
149
- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
150
- | `.qm-workspace` | The screen: pinned to the viewport, three bands (a head, the body, an optional foot), the body taking what is left and the document scrolling nowhere. |
151
- | `.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. |
152
- | `.qm-split` | Two even tracks for two surfaces, the page gap between them. Below `60rem` they stack, each at `--qmh-pane`; which element scrolls then is yours. |
153
- | `.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. |
168
+ | Class | What it is |
169
+ | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
170
+ | `.qm-workspace` | The screen: pinned to the viewport, three bands (a head, the body, an optional foot), the body taking what is left and the document scrolling nowhere. |
171
+ | `.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. |
172
+ | `.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. |
173
+ | `.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. |
174
+ | `.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. |
175
+
176
+ 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 repaints itself when it comes back. 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.
154
177
 
155
- 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.
178
+ 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.
156
179
 
157
180
  It is **unlayered**, so your own rules beat it by ordinary precedence, exactly as they beat the surfaces. It is also opt-in and side-effect free until imported: nothing that styles your document arrives with a component.
158
181
 
@@ -1,7 +1,7 @@
1
- import { type Addr } from '@quillmark/wasm';
1
+ import type { Addr, HitGranularity } from '@quillmark/wasm';
2
2
  /**
3
3
  * A canonical field address: `main.<field>` / `main.body` /
4
- * `cards.<kind>[<i>].<field>`, cards keyed by ABSOLUTE document index.
4
+ * `cards.<kind>[<i>].<field>`, cards keyed by absolute document index.
5
5
  *
6
6
  * The grammar `parseDocPath` / `formatDocPath` speak, and the one
7
7
  * `Diagnostic.path`, `ContentHit.field`, `FieldRegion.field` and
@@ -23,10 +23,27 @@ export interface Place {
23
23
  /** The caret in USV. */
24
24
  pos: number;
25
25
  }
26
+ /**
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.
34
+ */
35
+ export interface Landing {
36
+ field: DocPath;
37
+ /** The caret in USV, absent on the placement rung. */
38
+ pos?: number;
39
+ /** Whether `pos` is cluster-exact or floored to a segment start; the boundary's
40
+ * own marker, carried through untouched. */
41
+ granularity?: HitGranularity;
42
+ }
26
43
  /**
27
44
  * A field's canonical `DocPath`, or `undefined` when `addr.card` is outside the live
28
45
  * `kinds` array (a stale address: drop it rather than mis-target). A field-less card
29
- * addresses its BODY, which is the leaf `{card: i}` names.
46
+ * addresses its body, which is the leaf `{card: i}` names.
30
47
  *
31
48
  * - `{}` → `"main.body"`
32
49
  * - `{field}` → `"main.<field>"`
@@ -38,7 +55,7 @@ export interface Place {
38
55
  */
39
56
  export declare function fieldPathForAddr(addr: Addr, kinds: readonly string[]): DocPath | undefined;
40
57
  /**
41
- * A CARD's own path (`cards.<kind>[i]`), not a leaf's: what a structure op names,
58
+ * A card's own path (`cards.<kind>[i]`), not a leaf's: what a structure op names,
42
59
  * where the change is the card rather than anything inside it. `undefined` for an
43
60
  * index outside `kinds`.
44
61
  */
@@ -46,11 +63,29 @@ export declare function cardPath(index: number, kinds: readonly string[]): DocPa
46
63
  /**
47
64
  * The inverse: a canonical `DocPath` back to the `Addr` the document verbs take, or
48
65
  * `undefined` for a path that names no single commit address — a nested or
49
- * array-element path (`main.references.0`), a field-rooted one, or a malformed one
50
- * (which `parseDocPath` throws on). A bare card and a `.body` terminal both land on
51
- * the field-less `{card: i}` the body leaf answers to.
66
+ * array-element path (`main.references.0`, which {@link elementAddrForFieldPath}
67
+ * takes instead), a field-rooted one, or a malformed one. A bare card and a `.body`
68
+ * terminal both land on the field-less `{card: i}` the body leaf answers to.
52
69
  *
53
70
  * Needs no `kinds`: the path carries the absolute index, and the kind in it is
54
71
  * decoration the `Addr` has no room for.
55
72
  */
56
73
  export declare function addrForFieldPath(path: DocPath): Addr | undefined;
74
+ /**
75
+ * An array element's address, split. `main.references.0` and `main.references[0]`
76
+ * both land here; anything {@link addrForFieldPath} can name, and anything whose
77
+ * trailing segment is not an index, does not.
78
+ *
79
+ * Both spellings, because the boundary mints one and formats the other: `regions()`
80
+ * and `positionAt` emit `main.references.0`, which `parseDocPath` reads as a field
81
+ * literally named `"0"`, while `formatDocPath` spells the index segment
82
+ * `main.references[0]`. A trailing all-digits field name is therefore an index that
83
+ * lost its brackets — but only under a field the schema declares an array, the
84
+ * caller's guard to apply: this module holds the grammar and no schema.
85
+ */
86
+ export declare function elementAddrForFieldPath(path: DocPath): ElementAddr | undefined;
87
+ /** An array element's address: the array's `Addr`, and which element of it. */
88
+ export interface ElementAddr {
89
+ field: Addr;
90
+ index: number;
91
+ }
@@ -1,15 +1,4 @@
1
- // The one address vocabulary the surfaces speak in public: canonical `DocPath`
2
- // strings for places, `Addr` for the document verbs. Declared HERE because both
3
- // `/preview` and `/visual` name a place in their hooks, and a type declared twice
4
- // structurally is drift with nothing to catch it: `/core` is the module both
5
- // already import.
6
- //
7
- // Slogan: paths for places, indexes for structure ops. `Addr` (`{card?, field?}`,
8
- // from `@quillmark/wasm`) is the mutator currency; the conversions at the foot of
9
- // this module are the hop between the two. `fieldPathForAddr` and `addrForFieldPath`
10
- // are public, because a host given a path by a hook and holding a verb that takes an
11
- // `Addr` needs them; `cardPath` is the editor's own.
12
- import { formatDocPath, parseDocPath } from '@quillmark/wasm';
1
+ import { core } from './lifecycle.js';
13
2
  // ── Addr ↔ DocPath ──────────────────────────────────────────────────────────
14
3
  // The one hop between the two vocabularies, both directions, pure and
15
4
  // document-free: `kinds` is the kind of each composable card by content index
@@ -19,7 +8,7 @@ import { formatDocPath, parseDocPath } from '@quillmark/wasm';
19
8
  /**
20
9
  * A field's canonical `DocPath`, or `undefined` when `addr.card` is outside the live
21
10
  * `kinds` array (a stale address: drop it rather than mis-target). A field-less card
22
- * addresses its BODY, which is the leaf `{card: i}` names.
11
+ * addresses its body, which is the leaf `{card: i}` names.
23
12
  *
24
13
  * - `{}` → `"main.body"`
25
14
  * - `{field}` → `"main.<field>"`
@@ -34,35 +23,74 @@ export function fieldPathForAddr(addr, kinds) {
34
23
  if (!head)
35
24
  return undefined;
36
25
  const tail = addr.field != null ? { seg: 'field', name: addr.field } : { seg: 'body' };
37
- return formatDocPath([head, tail]);
26
+ return core().formatDocPath([head, tail]);
38
27
  }
39
28
  /**
40
- * A CARD's own path (`cards.<kind>[i]`), not a leaf's: what a structure op names,
29
+ * A card's own path (`cards.<kind>[i]`), not a leaf's: what a structure op names,
41
30
  * where the change is the card rather than anything inside it. `undefined` for an
42
31
  * index outside `kinds`.
43
32
  */
44
33
  export function cardPath(index, kinds) {
45
34
  const head = cardHead(index, kinds);
46
- return head ? formatDocPath([head]) : undefined;
35
+ return head ? core().formatDocPath([head]) : undefined;
47
36
  }
48
37
  /**
49
38
  * The inverse: a canonical `DocPath` back to the `Addr` the document verbs take, or
50
39
  * `undefined` for a path that names no single commit address — a nested or
51
- * array-element path (`main.references.0`), a field-rooted one, or a malformed one
52
- * (which `parseDocPath` throws on). A bare card and a `.body` terminal both land on
53
- * the field-less `{card: i}` the body leaf answers to.
40
+ * array-element path (`main.references.0`, which {@link elementAddrForFieldPath}
41
+ * takes instead), a field-rooted one, or a malformed one. A bare card and a `.body`
42
+ * terminal both land on the field-less `{card: i}` the body leaf answers to.
54
43
  *
55
44
  * Needs no `kinds`: the path carries the absolute index, and the kind in it is
56
45
  * decoration the `Addr` has no room for.
57
46
  */
58
47
  export function addrForFieldPath(path) {
59
- let segs;
48
+ const segs = segsOf(path);
49
+ return segs && addrForSegs(segs);
50
+ }
51
+ /**
52
+ * An array element's address, split. `main.references.0` and `main.references[0]`
53
+ * both land here; anything {@link addrForFieldPath} can name, and anything whose
54
+ * trailing segment is not an index, does not.
55
+ *
56
+ * Both spellings, because the boundary mints one and formats the other: `regions()`
57
+ * and `positionAt` emit `main.references.0`, which `parseDocPath` reads as a field
58
+ * literally named `"0"`, while `formatDocPath` spells the index segment
59
+ * `main.references[0]`. A trailing all-digits field name is therefore an index that
60
+ * lost its brackets — but only under a field the schema declares an array, the
61
+ * caller's guard to apply: this module holds the grammar and no schema.
62
+ */
63
+ export function elementAddrForFieldPath(path) {
64
+ const segs = segsOf(path);
65
+ if (!segs)
66
+ return undefined;
67
+ const last = segs[segs.length - 1];
68
+ const index = last?.seg === 'index'
69
+ ? last.index
70
+ : last?.seg === 'field' && /^\d+$/.test(last.name)
71
+ ? Number(last.name)
72
+ : undefined;
73
+ if (index == null)
74
+ return undefined;
75
+ const field = addrForSegs(segs.slice(0, -1));
76
+ // A body has no elements: the parent must name a field.
77
+ return field?.field != null ? { field, index } : undefined;
78
+ }
79
+ /** `parseDocPath`, with a malformed path as `undefined` rather than a throw. The gate
80
+ * is read outside the try: an uninitialized core is not a malformed path, and would
81
+ * otherwise leave here as one. */
82
+ function segsOf(path) {
83
+ const { parseDocPath } = core();
60
84
  try {
61
- segs = parseDocPath(path);
85
+ return parseDocPath(path);
62
86
  }
63
87
  catch {
64
88
  return undefined;
65
89
  }
90
+ }
91
+ /** {@link addrForFieldPath} over already-parsed segments, so the element walk reuses
92
+ * it for the head of a path rather than re-serializing one. */
93
+ function addrForSegs(segs) {
66
94
  const [head, ...rest] = segs;
67
95
  if (!head)
68
96
  return undefined;
@@ -9,12 +9,12 @@ export interface BloomTiming {
9
9
  /**
10
10
  * Resolve the timing off `el`, so the dwell is single-sourced in CSS.
11
11
  *
12
- * ONE property, both motion preferences: `--_qm-bloom-dwell` already shortens under
12
+ * One property, both motion preferences: `--_qm-bloom-dwell` already shortens under
13
13
  * `prefers-reduced-motion` in the derivation, so what is left to `matchMedia` here is
14
- * the SHAPE: a hold and a cut carry no ramps, which is a frame list rather than a
14
+ * the shape: a hold and a cut carry no ramps, which is a frame list rather than a
15
15
  * number and the one half CSS cannot hand over.
16
16
  *
17
- * Read ONCE PER BATCH, not per element: `getComputedStyle` forces a style recalc and
17
+ * Read once per batch, not per element: `getComputedStyle` forces a style recalc and
18
18
  * `animate` re-dirties style, so a read inside a loop flushes once per element (~0.15ms
19
19
  * each). The rungs inherit, so every element in a batch resolves the same numbers.
20
20
  */
@@ -27,10 +27,10 @@ export declare function bloomTiming(el: HTMLElement): BloomTiming;
27
27
  */
28
28
  export declare function bloom(el: HTMLElement, elapsed?: number, timing?: BloomTiming): Animation | undefined;
29
29
  /**
30
- * Bloom over `host`'s CONTENT: an inset child, so the wash neither tints the host's
30
+ * Bloom over `host`'s content: an inset child, so the wash neither tints the host's
31
31
  * own background nor fades the text under it. `host` must be positioned.
32
32
  *
33
- * ONE wash node per host, reused while it lives: two quick landings on the same leaf
33
+ * One wash node per host, reused while it lives: two quick landings on the same leaf
34
34
  * must not stack two alphas over it. No `elapsed` seam: the surfaces that use this
35
35
  * (editor leaves) are not rebuilt under the animation the way the preview's boxes are.
36
36
  */
@@ -1,12 +1,12 @@
1
1
  // The correlation bloom: a soft accent wash that rises over a target and decays to
2
- // nothing. The editor↔preview address is an EVENT, not a state: the preview claims
2
+ // nothing. The editor↔preview address is an event, not a state: the preview claims
3
3
  // to be the rendered output, so it carries no resting ink, and a highlight that
4
4
  // outlives the hop that caused it is ink the document did not ask for. Both
5
5
  // directions land here: the preview's field boxes bloom themselves (they are already
6
6
  // empty rects over the page), an editor leaf blooms a transient child over its
7
7
  // surface.
8
8
  //
9
- // OPACITY is the animated property, never the colour. The wash's alpha is a peak
9
+ // Opacity is the animated property, never the colour. The wash's alpha is a peak
10
10
  // fixed by `--_qm-accent-wash`, so interpolating 0→1→0 keeps the scale in CSS where
11
11
  // `color-mix` resolves, instead of in keyframe values a script would have to
12
12
  // pre-resolve and re-resolve on a theme change.
@@ -23,7 +23,7 @@ const WASH = 'var(--_qm-accent-wash)';
23
23
  * absence of the scale, not a copy of it. */
24
24
  const FALLBACK_MS = 1100;
25
25
  // Rise fast, hold, then leave slowly: a highlighter's decay, not a linear fade. A
26
- // keyframe's easing governs the segment that STARTS at it. Under reduced motion the
26
+ // keyframe's easing governs the segment that starts at it. Under reduced motion the
27
27
  // ramps are dropped entirely: the wash holds at full for a beat and cuts, which
28
28
  // still answers "here" with nothing to track.
29
29
  const FRAMES = [
@@ -40,12 +40,12 @@ export function primeWash(el) {
40
40
  /**
41
41
  * Resolve the timing off `el`, so the dwell is single-sourced in CSS.
42
42
  *
43
- * ONE property, both motion preferences: `--_qm-bloom-dwell` already shortens under
43
+ * One property, both motion preferences: `--_qm-bloom-dwell` already shortens under
44
44
  * `prefers-reduced-motion` in the derivation, so what is left to `matchMedia` here is
45
- * the SHAPE: a hold and a cut carry no ramps, which is a frame list rather than a
45
+ * the shape: a hold and a cut carry no ramps, which is a frame list rather than a
46
46
  * number and the one half CSS cannot hand over.
47
47
  *
48
- * Read ONCE PER BATCH, not per element: `getComputedStyle` forces a style recalc and
48
+ * Read once per batch, not per element: `getComputedStyle` forces a style recalc and
49
49
  * `animate` re-dirties style, so a read inside a loop flushes once per element (~0.15ms
50
50
  * each). The rungs inherit, so every element in a batch resolves the same numbers.
51
51
  */
@@ -74,10 +74,10 @@ export function bloom(el, elapsed = 0, timing) {
74
74
  return anim;
75
75
  }
76
76
  /**
77
- * Bloom over `host`'s CONTENT: an inset child, so the wash neither tints the host's
77
+ * Bloom over `host`'s content: an inset child, so the wash neither tints the host's
78
78
  * own background nor fades the text under it. `host` must be positioned.
79
79
  *
80
- * ONE wash node per host, reused while it lives: two quick landings on the same leaf
80
+ * One wash node per host, reused while it lives: two quick landings on the same leaf
81
81
  * must not stack two alphas over it. No `elapsed` seam: the surfaces that use this
82
82
  * (editor leaves) are not rebuilt under the animation the way the preview's boxes are.
83
83
  */
@@ -8,7 +8,7 @@ export interface RangeAnchor {
8
8
  /**
9
9
  * A virtual anchor over one PM range, measured when asked rather than when made.
10
10
  *
11
- * The measure is TOTAL. `coordsAtPos` reads layout, and layout can be gone: a
11
+ * The measure is total. `coordsAtPos` reads layout, and layout can be gone: a
12
12
  * position the document has since invalidated, a leaf inside a collapsed section, a
13
13
  * view torn down between a scroll firing and the measure running. `autoUpdate` calls
14
14
  * this at moments none of its callers choose, so an unmeasurable anchor returns the
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * A virtual anchor over one PM range, measured when asked rather than when made.
3
3
  *
4
- * The measure is TOTAL. `coordsAtPos` reads layout, and layout can be gone: a
4
+ * The measure is total. `coordsAtPos` reads layout, and layout can be gone: a
5
5
  * position the document has since invalidated, a leaf inside a collapsed section, a
6
6
  * view torn down between a scroll firing and the measure running. `autoUpdate` calls
7
7
  * this at moments none of its callers choose, so an unmeasurable anchor returns the
@@ -0,0 +1,10 @@
1
+ import { type Command } from 'prosemirror-state';
2
+ import type { Schema } from 'prosemirror-model';
3
+ /**
4
+ * The block-atom bindings for a block-schema leaf: `{}` for the inline/plaintext
5
+ * schemas, which hold no block to be against.
6
+ *
7
+ * Both keys are the same command on opposite sides, because what a delete against an
8
+ * atom means does not depend on which side the caret approached from.
9
+ */
10
+ export declare function atomKeymap(schema: Schema): Record<string, Command>;
@@ -0,0 +1,78 @@
1
+ // The block-atom link of the body's Backspace/Delete chains: a delete against an
2
+ // island or a divider selects it, and the press after that deletes it.
3
+ //
4
+ // This is the neighbour half of the rule the leaf already holds for a key typed over a
5
+ // selection (CODEC §"The table island"): a selection is the subject of the next
6
+ // command, never a thing armed for replacement. Without it the base keymap's
7
+ // `joinBackward` / `joinForward` reach their "the neighbour is an atom, delete it" arm
8
+ // and a table goes in one keystroke, from the paragraph a table insert parks the caret
9
+ // in — with nothing drawn first to say what is about to go.
10
+ //
11
+ // It runs last of the links, after the list ones: a Backspace at the start of a list's
12
+ // first item lifts the item, and that stays the item's answer whatever sits above the
13
+ // list.
14
+ import { NodeSelection, TextSelection } from 'prosemirror-state';
15
+ /** The caret, when it sits at a textblock edge with an empty selection: the only place
16
+ * a delete is about a neighbour rather than about a character. `null` everywhere else,
17
+ * which is where the key keeps its ordinary meaning. */
18
+ function atEdge(state, side) {
19
+ const { selection } = state;
20
+ if (!(selection instanceof TextSelection) || !selection.empty)
21
+ return null;
22
+ const $head = selection.$head;
23
+ const at = side < 0 ? 0 : $head.parent.content.size;
24
+ return $head.parentOffset === at ? $head : null;
25
+ }
26
+ /**
27
+ * The cut the caret's block sits against on `side`: the shallowest boundary with a
28
+ * sibling beyond it. `prosemirror-commands` walks the same one to find the node a join
29
+ * would reach; this reads it to find the node a delete would take.
30
+ */
31
+ function cutAt($pos, side) {
32
+ if ($pos.parent.type.spec.isolating)
33
+ return null;
34
+ for (let d = $pos.depth - 1; d >= 0; d--) {
35
+ if (side < 0) {
36
+ if ($pos.index(d) > 0)
37
+ return $pos.doc.resolve($pos.before(d + 1));
38
+ }
39
+ else if ($pos.index(d) + 1 < $pos.node(d).childCount) {
40
+ return $pos.doc.resolve($pos.after(d + 1));
41
+ }
42
+ if ($pos.node(d).type.spec.isolating)
43
+ break;
44
+ }
45
+ return null;
46
+ }
47
+ /** Select the block atom the caret is against, rather than deleting it. Declines when
48
+ * the neighbour is anything a caret can enter, so a paragraph, a heading, a quote and
49
+ * a list all keep the join the base keymap gives them. */
50
+ function selectAtom(side) {
51
+ return (state, dispatch) => {
52
+ const $head = atEdge(state, side);
53
+ const $cut = $head && cutAt($head, side);
54
+ if (!$cut)
55
+ return false;
56
+ const node = side < 0 ? $cut.nodeBefore : $cut.nodeAfter;
57
+ if (!node || !node.isAtom || !node.isBlock || !NodeSelection.isSelectable(node))
58
+ return false;
59
+ const at = side < 0 ? $cut.pos - node.nodeSize : $cut.pos;
60
+ dispatch?.(state.tr.setSelection(NodeSelection.create(state.doc, at)).scrollIntoView());
61
+ return true;
62
+ };
63
+ }
64
+ /**
65
+ * The block-atom bindings for a block-schema leaf: `{}` for the inline/plaintext
66
+ * schemas, which hold no block to be against.
67
+ *
68
+ * Both keys are the same command on opposite sides, because what a delete against an
69
+ * atom means does not depend on which side the caret approached from.
70
+ */
71
+ export function atomKeymap(schema) {
72
+ if (!schema.nodes.island_block)
73
+ return {};
74
+ return {
75
+ Backspace: selectAtom(-1),
76
+ Delete: selectAtom(1)
77
+ };
78
+ }