@quillmark/svelte 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +15 -11
  2. package/THEMING.md +42 -23
  3. package/dist/core/address.d.ts +39 -4
  4. package/dist/core/address.js +47 -19
  5. package/dist/core/codec/encode.js +4 -1
  6. package/dist/core/codec/field.d.ts +1 -1
  7. package/dist/core/codec/prose.css +41 -18
  8. package/dist/core/codec/table.js +3 -1
  9. package/dist/core/errors.d.ts +15 -5
  10. package/dist/core/index.d.ts +1 -1
  11. package/dist/core/index.js +5 -4
  12. package/dist/core/lifecycle.d.ts +21 -5
  13. package/dist/core/lifecycle.js +34 -8
  14. package/dist/core/rebind.svelte.d.ts +16 -2
  15. package/dist/core/rebind.svelte.js +26 -8
  16. package/dist/core/theme.css +50 -18
  17. package/dist/preset/recipes.css +55 -8
  18. package/dist/preset/scale.css +5 -4
  19. package/dist/preview/Preview.svelte +10 -9
  20. package/dist/preview/Preview.svelte.d.ts +5 -5
  21. package/dist/preview/bridge.d.ts +6 -4
  22. package/dist/preview/bridge.js +17 -7
  23. package/dist/preview/controller.d.ts +13 -6
  24. package/dist/preview/controller.js +5 -2
  25. package/dist/preview/geometry.d.ts +16 -1
  26. package/dist/preview/geometry.js +20 -0
  27. package/dist/preview/index.d.ts +1 -0
  28. package/dist/preview/index.js +7 -0
  29. package/dist/preview/overlay.d.ts +2 -1
  30. package/dist/preview/overlay.js +24 -9
  31. package/dist/preview/paint.js +4 -0
  32. package/dist/visual/ArrayField.svelte +26 -20
  33. package/dist/visual/ArrayField.svelte.d.ts +4 -3
  34. package/dist/visual/Card.svelte +21 -19
  35. package/dist/visual/Card.svelte.d.ts +8 -3
  36. package/dist/visual/DiagnosticList.svelte +1 -1
  37. package/dist/visual/EnumField.svelte +34 -14
  38. package/dist/visual/EnumField.svelte.d.ts +4 -3
  39. package/dist/visual/Field.svelte +87 -25
  40. package/dist/visual/Field.svelte.d.ts +11 -7
  41. package/dist/visual/FieldHint.svelte +59 -10
  42. package/dist/visual/FormatPopover.svelte +3 -1
  43. package/dist/visual/ObjectField.svelte +21 -2
  44. package/dist/visual/ObjectField.svelte.d.ts +3 -1
  45. package/dist/visual/ProseArrayElement.svelte +1 -6
  46. package/dist/visual/ProseArrayElement.svelte.d.ts +0 -1
  47. package/dist/visual/ProseField.svelte +20 -12
  48. package/dist/visual/ProseField.svelte.d.ts +5 -3
  49. package/dist/visual/TipsCard.svelte +1 -1
  50. package/dist/visual/VisualEditor.svelte +12 -13
  51. package/dist/visual/VisualEditor.svelte.d.ts +2 -3
  52. package/dist/visual/VisualEditorInner.svelte +140 -66
  53. package/dist/visual/VisualEditorInner.svelte.d.ts +2 -3
  54. package/dist/visual/controls.css +14 -11
  55. package/dist/visual/diagnostics.d.ts +12 -20
  56. package/dist/visual/diagnostics.js +6 -18
  57. package/dist/visual/index.d.ts +1 -0
  58. package/dist/visual/index.js +7 -0
  59. package/dist/visual/leaves.d.ts +47 -0
  60. package/dist/visual/leaves.js +31 -0
  61. package/dist/visual/props.d.ts +27 -6
  62. package/dist/visual/tips.d.ts +3 -1
  63. package/dist/visual/tips.js +5 -2
  64. package/package.json +3 -5
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
 
@@ -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,26 @@ 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: an app that paints its own plate and leaves them mounts one plate inside another. Two tones a few percent apart read as a fault rather than as a layer, so meet your page's tone exactly or stand far enough off it to be a layer.
45
+
46
+ 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.
47
+
42
48
  ### The dials
43
49
 
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. |
50
+ | Token | Default | What it sets |
51
+ | ---------------- | -------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
52
+ | `--qm-bg` | `#fff` / `#14171c` | Base surface. Cards, fields, the painted page, the popover, and the column behind them step off it. |
53
+ | `--qm-fg` | `#1a1a1a` / `#e8eaed` | Base ink. Body text, labels, and borders step off it. |
54
+ | `--qm-accent` | `#2563eb` | Focus rings, active marks, the preview's active field box. |
55
+ | `--qm-danger` | `#c5221f` | Error diagnostics, the required marker, the delete glyph. |
56
+ | `--qm-warning` | `#b25000` | Warning diagnostics. |
57
+ | `--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. |
58
+ | `--qm-font-mono` | `ui-monospace, monospace` | The monospace face: the JSON array control, the tips card. |
59
+ | `--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. |
60
+ | `--qm-radius` | `8px` | Card & popover corner. Interior controls derive a tighter tier (half), so a square brand is square throughout. |
61
+ | `--qm-space` | `0.25rem` | Spacing base. Gaps and insets are `half`/`1×`/`2×`/`3×`/`4×` multiples of it. |
56
62
 
57
63
  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
64
 
@@ -131,26 +137,39 @@ It carries a `--qmh-*` scale for the page, derived from the same ten dials and c
131
137
 
132
138
  ### The shell, if you are building a tool
133
139
 
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:
140
+ 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
141
 
136
142
  ```html
137
143
  <div class="qm-workspace">
138
144
  <header class="qm-bar site-head">
139
145
  <span class="site-mark">acme<span class="site-slash">/</span>studio</span>
140
146
  </header>
141
- <div class="qm-split">
142
- <section class="qm-frame"><!-- <VisualEditor class="qm-pane" /> --></section>
143
- <section class="qm-frame"><!-- <Preview /> --></section>
147
+ <div class="site-body">
148
+ <div class="qm-switch" role="group" aria-label="Visible pane">
149
+ <button class="qm-control" aria-pressed="{shown" ="" ="" ="1}" onclick="{()" ="">
150
+ (shown = 1)}>Editor
151
+ </button>
152
+ <button class="qm-control" aria-pressed="{shown" ="" ="" ="2}" onclick="{()" ="">
153
+ (shown = 2)}>Preview
154
+ </button>
155
+ </div>
156
+ <div class="qm-split" data-qm-show="{shown}">
157
+ <section class="qm-frame"><!-- <VisualEditor class="qm-pane" /> --></section>
158
+ <section class="qm-frame"><!-- <Preview /> --></section>
159
+ </div>
144
160
  </div>
145
161
  </div>
146
162
  ```
147
163
 
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. |
164
+ | Class | What it is |
165
+ | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
166
+ | `.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. |
167
+ | `.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. |
168
+ | `.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. |
169
+ | `.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. |
170
+ | `.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. |
171
+
172
+ 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
173
 
155
174
  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
175
 
@@ -1,4 +1,4 @@
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
4
  * `cards.<kind>[<i>].<field>`, cards keyed by ABSOLUTE document index.
@@ -23,6 +23,23 @@ 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
@@ -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
@@ -34,7 +23,7 @@ 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
29
  * A CARD's own path (`cards.<kind>[i]`), not a leaf's: what a structure op names,
@@ -43,26 +32,65 @@ export function fieldPathForAddr(addr, 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;
@@ -1,4 +1,5 @@
1
- import { mapPos, isAnchorMark } from '@quillmark/wasm';
1
+ import { isAnchorMark } from '@quillmark/wasm';
2
+ import { core } from '../lifecycle.js';
2
3
  import { codePoints, usvLength } from './decode.js';
3
4
  import { ISLAND_SLOT, islandEntryFromNode } from './islands.js';
4
5
  import { contentDescriptorFromPM, descriptorOf, markKey } from './marks.js';
@@ -386,6 +387,8 @@ function shiftPastIslands(pos, insertedAt, assoc) {
386
387
  /** Formatting/unknown marks → add/remove ops; anchors → add/removeAnchor by id. */
387
388
  function diffMarks(oldRt, newRt, delta, insertedAt, opts) {
388
389
  const ops = [];
390
+ // Read the gate once per diff rather than once per mark.
391
+ const { mapPos } = core();
389
392
  // Rebase old marks through BOTH text-moving channels to final coords, exactly
390
393
  // as `applyChange` does internally (start assoc `after`, end assoc `before`).
391
394
  const rebase = (pos, assoc) => shiftPastIslands(delta ? mapPos(delta, pos, assoc) : pos, insertedAt, assoc);
@@ -74,7 +74,7 @@ export interface FieldController {
74
74
  /** The mounted `container`: the leaf's element, for a caller that has to reach the
75
75
  * DOM the view sits in (the arrival wash, `core/bloom.ts`) rather than re-find it. */
76
76
  readonly el: HTMLElement;
77
- /** Place the caret at USV `pos` (preview onCaretPick → usvToPM → here). */
77
+ /** Place the caret at USV `pos` (preview onPick → usvToPM → here). */
78
78
  setCaret(pos: number): void;
79
79
  /** External content change → re-hydrate this leaf (gated by reconcile). */
80
80
  applyExternal(): void;
@@ -20,13 +20,13 @@
20
20
  re-measured here rather than in `theme.css` because a layered rule loses to that
21
21
  unlayered stylesheet: equal specificity and a later import is what wins. */
22
22
  .ProseMirror-selectednode {
23
- outline: var(--_qm-ring-width) solid var(--_qm-accent);
23
+ outline: var(--_qm-ring);
24
24
  }
25
25
  /* Upstream's `-32px` is measured against the UA's 40px list indent, which the list
26
26
  rule below replaces with a rung. The offset moves with it, or a selected item's
27
27
  outline hangs into the margin by the difference. */
28
28
  li.ProseMirror-selectednode::after {
29
- border: var(--_qm-ring-width) solid var(--_qm-accent);
29
+ border: var(--_qm-ring);
30
30
  left: calc(var(--_qm-space-4) * -1);
31
31
  }
32
32
 
@@ -96,14 +96,25 @@ li.ProseMirror-selectednode::after {
96
96
  `--_qm-surface-raised` and the only rung over it is `--_qm-surface-hover`, the
97
97
  hover lane, so a chip at rest would read as a hovered button.
98
98
 
99
- ONE selector for the mark and the block: a `code_block` renders `pre > code`
100
- (`schema.ts`), so every character of it is inside a `code` element and the `pre`
101
- needs no face of its own. `pre` takes no `overflow-x` either, because there is no
102
- overflow: ProseMirror's own stylesheet sets `white-space: pre-wrap` there, so a
103
- long line wraps. A scroller means overriding that to `pre` inside an editable
104
- `code` textblock, which is an editing change, not a display rule. */
105
- .ProseMirror code {
99
+ The withheld properties are DECLARED at their zero, which is what makes the
100
+ paragraph above true of a MOUNTED leaf: a chip on these tags is an ordinary page
101
+ rule, so left off, these are the two tags the host draws rather than the surface.
102
+ The other marks are ink, weight and decoration, which the leaf sets or safely
103
+ inherits, so the box is this mark's alone and the zeros are not a reset.
104
+
105
+ BOTH tags, where the face alone needs only one: a `code_block` renders `pre > code`
106
+ (`schema.ts`), so every character is inside a `code` element and the face reaches it
107
+ there — but the block's BOX is the `pre`, outside that element and styled by the
108
+ host on its own. `pre` still takes no `overflow-x`, because there is no overflow:
109
+ ProseMirror's own stylesheet sets `white-space: pre-wrap` there, so a long line
110
+ wraps. A scroller means overriding that to `pre` inside an editable `code`
111
+ textblock, which is an editing change, not a display rule. */
112
+ .ProseMirror :is(code, pre) {
106
113
  font-family: var(--_qm-font-mono);
114
+ padding: 0;
115
+ border-radius: 0;
116
+ background: none;
117
+ font-size: inherit;
107
118
  }
108
119
 
109
120
  /* A link takes the surface's ink (the UA's blue is the `#8cf` defect again) and a
@@ -128,21 +139,33 @@ li.ProseMirror-selectednode::after {
128
139
  }
129
140
 
130
141
  /* The UA's 40px list indent is the one box the reset above does not take (it is
131
- padding, not margin), and it is a length nothing chose. A rung replaces it. The
132
- MARKERS stay the UA's: `disc` and `decimal` are already right, and restating them
133
- buys nothing. Per-item spacing is untouched, which is the point of putting this on
134
- the list rather than the item: the sibling-gap rule deliberately omits `ul`/`ol`,
135
- and padding on the container cannot reintroduce what it withholds. */
142
+ padding, not margin), and it is a length nothing chose. A rung replaces it.
143
+ Per-item spacing is untouched, which is the point of putting this on the list
144
+ rather than the item: the sibling-gap rule deliberately omits `ul`/`ol`, and
145
+ padding on the container cannot reintroduce what it withholds.
146
+
147
+ The MARKERS are the UA's, RECLAIMED rather than inherited. `disc` and `decimal`
148
+ are right and this restates neither: a host reset (Tailwind's preflight sets
149
+ `list-style: none` on `ol, ul, menu`) is the common case, and against one an
150
+ unstated default is no default at all — bullets and numbers vanish from the leaf
151
+ while the preview beside it still draws them, which is an editor/paper
152
+ divergence on the plainest formatting there is. `revert` over a named type
153
+ because the UA's markers step by DEPTH (disc, circle, square) and the schema
154
+ nests (`lists.ts`: Tab sinks an item); naming `disc` would flatten that ramp to
155
+ hold the first level. Same reclaim for `strong`/`em`/`u`/`s` above if a reset
156
+ that takes those ever turns up: the positive half states what it leaves to the
157
+ UA, and what a reset takes is what has to be asked back. */
136
158
  .ProseMirror :is(ul, ol) {
137
159
  padding-left: var(--_qm-space-4);
160
+ list-style: revert;
138
161
  }
139
162
 
140
163
  /* Six levels, one weight, no size step. The quill reads no level at all, so a size
141
164
  ramp here would be the editor predicting a hierarchy the paper does not set; the
142
165
  level stays in the model, which is where the codec round-trips it. It is also all
143
- the ramp could carry: `--_qm-text-title` is one 1.125 step over the body, the
144
- body sits on `--_qm-text-body` by decision, and leading alone is what separates
145
- paper from chrome. Six levels do not fit in one step, and
166
+ the ramp could carry: this leaf renders at `--_qm-text-paper` and the next rung up
167
+ is `--_qm-text-title`, a chrome rung a heading inside a document has no business
168
+ reading. Six levels do not fit in the gap, and
146
169
  minting five more is the drift the scale exists to prevent. */
147
170
  .ProseMirror :is(h1, h2, h3, h4, h5, h6) {
148
171
  font-size: inherit;
@@ -431,7 +454,7 @@ li.ProseMirror-selectednode::after {
431
454
  .qm-table-handle:focus-visible,
432
455
  .qm-table-seam:focus-visible,
433
456
  .qm-table-align-item:focus-visible {
434
- outline: var(--_qm-ring-focus);
457
+ outline: var(--_qm-ring);
435
458
  outline-offset: var(--_qm-ring-offset);
436
459
  }
437
460
 
@@ -15,7 +15,8 @@
15
15
  // the codec already runs (one paragraph, no containers, no islands) is exactly its
16
16
  // shape, so a cell decodes and projects through the same machinery a
17
17
  // `richtext(inline)` field does.
18
- import { isAnchorMark, mapPos } from '@quillmark/wasm';
18
+ import { isAnchorMark } from '@quillmark/wasm';
19
+ import { core } from '../lifecycle.js';
19
20
  import { contentEdit, pmToContent } from './encode.js';
20
21
  /** The alignments a column can take: the four the content declares, `none` first
21
22
  * because it is what a column arrives at and what a set is cleared back to. */
@@ -201,6 +202,7 @@ export function cellFromDoc(doc, prior) {
201
202
  if (!anchors.length)
202
203
  return { text, marks: projected.marks };
203
204
  const delta = contentEdit(cellContent(prior), cellContent({ text, marks: [] })).delta;
205
+ const { mapPos } = core();
204
206
  const rebased = anchors.map((a) => {
205
207
  const pos = delta ? mapPos(delta, a.start, 'after') : a.start;
206
208
  return { ...a, start: pos, end: pos };
@@ -30,14 +30,24 @@ export type EditorErrorCode =
30
30
  * re-keyed on it nor observed it. The mounted surface still names the previous
31
31
  * handle. `dev` throughout: the fix is a remount at the call site, so this is
32
32
  * aimed at whoever wired it and never at a running app's telemetry.
33
+ *
34
+ * The one code that is also THROWN, in a dev build, once reported: a swapped
35
+ * `onError` is itself a rebind, so the handler this arrives at may be the one the
36
+ * consumer just replaced (`rebind.svelte.ts`).
33
37
  */
34
38
  | 'rebind-ignored'
35
39
  /**
36
- * An instance verb named a card or a field the surface does not hold: a `cardId`
37
- * from a previous session or an already-removed card, a `DocPath` naming no
38
- * mounted leaf. The call is a no-op and the document is untouched. `dev`: the
39
- * editor's own chrome cannot mint a bad target, so this only ever reports a host
40
- * driving the verbs from outside.
40
+ * A landing named a card or a field the surface does not hold: a `cardId` from a
41
+ * previous session or an already-removed card, or a `DocPath` naming no declared
42
+ * field — an array ELEMENT path included, where the array itself is undeclared or
43
+ * unmounted. The call is a no-op and the document is untouched.
44
+ *
45
+ * `dev`, because the fix is at the call site: a host driving the instance verbs
46
+ * from outside, or a preview pick the editor has no field to route to. Every
47
+ * MOUNTED field is a target, prose leaf and form control alike, and an array's
48
+ * elements are targets through it, so this is not how a scalar or an element
49
+ * reports. The preview never reports it at all: a field it places nothing for is
50
+ * `scrollToField` returning `false`.
41
51
  */
42
52
  | 'target-unknown';
43
53
  /** A failure a surface recovered from. */
@@ -1,5 +1,5 @@
1
1
  import './theme.css';
2
2
  export { init } from './lifecycle.js';
3
- export type { DocPath, Place } from './address.js';
3
+ export type { DocPath, Landing, Place } from './address.js';
4
4
  export { fieldPathForAddr, addrForFieldPath } from './address.js';
5
5
  export type { EditorError, EditorErrorCode, EditorErrorHandler } from './errors.js';
@@ -1,8 +1,9 @@
1
1
  // The theme derivation: the public dials → the private `--_qm-*` scale, applied
2
- // to every `data-qm-root` element. Imported HERE because `core/` is the one module
3
- // both `preview/` and `visual/` already pull, so a consumer cannot forget it and
4
- // the derivation is minted once rather than re-declared per detached root
5
- // (THEMING.md).
2
+ // to every `data-qm-root` element (THEMING.md). Imported at every barrel a
3
+ // consumer can enter through — here, `/visual`, `/preview` — because a subpath is
4
+ // what a consumer gets, and a surface reaching a module INSIDE `core/` reaches
5
+ // nothing this file imports. One sheet however many barrels name it: the
6
+ // derivation is minted once rather than re-declared per detached root.
6
7
  import './theme.css';
7
8
  // `@quillmark/svelte/core`: what this package OWNS at the substrate seam.
8
9
  //
@@ -1,7 +1,23 @@
1
+ import { type CoreSurface } from '@quillmark/wasm';
1
2
  /**
2
- * Instantiate the core WASM build. Await once before any other boundary verb,
3
- * from any surface's setup path; the promise is memoized, so concurrent callers
4
- * share one instantiation and repeat calls are free. A failed init clears the
5
- * memo, leaving a retry possible.
3
+ * Instantiate the core WASM build and resolve to its surface. Await once before
4
+ * any other boundary verb, from any surface's setup path; the promise is memoized,
5
+ * so concurrent callers share one instantiation and repeat calls are free. A failed
6
+ * init clears the memo, leaving a retry possible.
7
+ *
8
+ * The surface is the artifact's own, handed back unchanged: `const { Quill, Document }
9
+ * = await init()` is the whole of a host's access to the classes, and this package
10
+ * re-declares none of it.
6
11
  */
7
- export declare function init(): Promise<void>;
12
+ export declare function init(): Promise<CoreSurface>;
13
+ /**
14
+ * The surface synchronously, for this package's pure verbs: the `DocPath` formatter
15
+ * on the public API and the codec inside a ProseMirror transaction, neither of which
16
+ * can await. Package-internal, and read at the call rather than destructured at
17
+ * module scope, so no module's import graph carries an ordering constraint.
18
+ *
19
+ * Reaching one before {@link init} resolves throws, naming the fix. This is the seam
20
+ * that turns an awaited surface back into sync verbs, so it is the one place the
21
+ * precondition the gate made structural has to be stated again.
22
+ */
23
+ export declare function core(): CoreSurface;