@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.
- package/README.md +15 -11
- package/THEMING.md +42 -23
- package/dist/core/address.d.ts +39 -4
- package/dist/core/address.js +47 -19
- package/dist/core/codec/encode.js +4 -1
- package/dist/core/codec/field.d.ts +1 -1
- package/dist/core/codec/prose.css +41 -18
- package/dist/core/codec/table.js +3 -1
- package/dist/core/errors.d.ts +15 -5
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.js +5 -4
- package/dist/core/lifecycle.d.ts +21 -5
- package/dist/core/lifecycle.js +34 -8
- package/dist/core/rebind.svelte.d.ts +16 -2
- package/dist/core/rebind.svelte.js +26 -8
- package/dist/core/theme.css +50 -18
- package/dist/preset/recipes.css +55 -8
- package/dist/preset/scale.css +5 -4
- package/dist/preview/Preview.svelte +10 -9
- package/dist/preview/Preview.svelte.d.ts +5 -5
- package/dist/preview/bridge.d.ts +6 -4
- package/dist/preview/bridge.js +17 -7
- package/dist/preview/controller.d.ts +13 -6
- package/dist/preview/controller.js +5 -2
- package/dist/preview/geometry.d.ts +16 -1
- package/dist/preview/geometry.js +20 -0
- package/dist/preview/index.d.ts +1 -0
- package/dist/preview/index.js +7 -0
- package/dist/preview/overlay.d.ts +2 -1
- package/dist/preview/overlay.js +24 -9
- package/dist/preview/paint.js +4 -0
- package/dist/visual/ArrayField.svelte +26 -20
- package/dist/visual/ArrayField.svelte.d.ts +4 -3
- package/dist/visual/Card.svelte +21 -19
- package/dist/visual/Card.svelte.d.ts +8 -3
- package/dist/visual/DiagnosticList.svelte +1 -1
- package/dist/visual/EnumField.svelte +34 -14
- package/dist/visual/EnumField.svelte.d.ts +4 -3
- package/dist/visual/Field.svelte +87 -25
- package/dist/visual/Field.svelte.d.ts +11 -7
- package/dist/visual/FieldHint.svelte +59 -10
- package/dist/visual/FormatPopover.svelte +3 -1
- package/dist/visual/ObjectField.svelte +21 -2
- package/dist/visual/ObjectField.svelte.d.ts +3 -1
- package/dist/visual/ProseArrayElement.svelte +1 -6
- package/dist/visual/ProseArrayElement.svelte.d.ts +0 -1
- package/dist/visual/ProseField.svelte +20 -12
- package/dist/visual/ProseField.svelte.d.ts +5 -3
- package/dist/visual/TipsCard.svelte +1 -1
- package/dist/visual/VisualEditor.svelte +12 -13
- package/dist/visual/VisualEditor.svelte.d.ts +2 -3
- package/dist/visual/VisualEditorInner.svelte +140 -66
- package/dist/visual/VisualEditorInner.svelte.d.ts +2 -3
- package/dist/visual/controls.css +14 -11
- package/dist/visual/diagnostics.d.ts +12 -20
- package/dist/visual/diagnostics.js +6 -18
- package/dist/visual/index.d.ts +1 -0
- package/dist/visual/index.js +7 -0
- package/dist/visual/leaves.d.ts +47 -0
- package/dist/visual/leaves.js +31 -0
- package/dist/visual/props.d.ts +27 -6
- package/dist/visual/tips.d.ts +3 -1
- package/dist/visual/tips.js +5 -2
- 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 {
|
|
29
|
+
import { Engine } from '@quillmark/wasm';
|
|
30
30
|
import { init } from '@quillmark/svelte/core';
|
|
31
31
|
|
|
32
|
-
|
|
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
|
-
|
|
71
|
-
/* a click resolved to a
|
|
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}
|
|
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`, `
|
|
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
|
|
195
|
-
|
|
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
|
-
|
|
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
|
|
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` |
|
|
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.
|
|
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="
|
|
142
|
-
<
|
|
143
|
-
|
|
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.
|
|
153
|
-
| `.qm-
|
|
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
|
|
package/dist/core/address.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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
|
|
50
|
-
*
|
|
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
|
+
}
|
package/dist/core/address.js
CHANGED
|
@@ -1,15 +1,4 @@
|
|
|
1
|
-
|
|
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
|
|
52
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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.
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
|
457
|
+
outline: var(--_qm-ring);
|
|
435
458
|
outline-offset: var(--_qm-ring-offset);
|
|
436
459
|
}
|
|
437
460
|
|
package/dist/core/codec/table.js
CHANGED
|
@@ -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
|
|
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 };
|
package/dist/core/errors.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
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. */
|
package/dist/core/index.d.ts
CHANGED
|
@@ -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';
|
package/dist/core/index.js
CHANGED
|
@@ -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
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
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
|
//
|
package/dist/core/lifecycle.d.ts
CHANGED
|
@@ -1,7 +1,23 @@
|
|
|
1
|
+
import { type CoreSurface } from '@quillmark/wasm';
|
|
1
2
|
/**
|
|
2
|
-
* Instantiate the core WASM build. Await once before
|
|
3
|
-
* from any surface's setup path; the promise is memoized,
|
|
4
|
-
* share one instantiation and repeat calls are free. A failed
|
|
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<
|
|
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;
|