@quillmark/svelte 0.3.0 → 0.3.1
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/NOTICE +6 -6
- package/README.md +1 -1
- package/THEMING.md +1 -1
- package/dist/core/bloom.d.ts +1 -30
- package/dist/core/bloom.js +13 -45
- package/dist/core/codec/prose.css +5 -6
- package/dist/core/theme.css +32 -16
- package/dist/preset/scale.css +4 -4
- package/dist/preview/Preview.svelte +6 -18
- package/dist/preview/Preview.svelte.d.ts +2 -3
- package/dist/preview/bridge.js +2 -2
- package/dist/preview/controller.d.ts +8 -10
- package/dist/preview/controller.js +16 -41
- package/dist/preview/geometry.d.ts +7 -9
- package/dist/preview/geometry.js +7 -9
- package/dist/preview/index.js +1 -1
- package/dist/preview/paint.d.ts +1 -1
- package/dist/preview/paint.js +5 -5
- package/dist/visual/Card.svelte +33 -28
- package/dist/visual/Card.svelte.d.ts +0 -1
- package/dist/visual/ProseField.svelte +17 -20
- package/dist/visual/ProseField.svelte.d.ts +1 -2
- package/dist/visual/TipsCard.svelte +49 -52
- package/dist/visual/VisualEditorInner.svelte +126 -206
- package/dist/visual/icons/nodes.d.ts +16 -0
- package/dist/visual/icons/nodes.js +11 -0
- package/package.json +2 -2
- package/dist/preview/overlay.d.ts +0 -11
- package/dist/preview/overlay.js +0 -97
package/NOTICE
CHANGED
|
@@ -6,9 +6,9 @@ Copyright 2026 Nibs
|
|
|
6
6
|
This package contains SVG glyph geometry derived from Lucide, carried in source
|
|
7
7
|
rather than installed.
|
|
8
8
|
|
|
9
|
-
`visual/icons/nodes.ts` holds
|
|
10
|
-
chevron-right, chevron-down, chevron-up, bold, italic, underline,
|
|
11
|
-
code, link, hash, info, plus and
|
|
9
|
+
`visual/icons/nodes.ts` holds sixteen verbatim from @lucide/svelte 1.28.0:
|
|
10
|
+
chevron-right, arrow-right, chevron-down, chevron-up, bold, italic, underline,
|
|
11
|
+
strikethrough, code, link, hash, info, plus, x, trash-2 and minus.
|
|
12
12
|
|
|
13
13
|
`core/codec/table-view.ts` holds a plus and four text-alignment glyphs derived
|
|
14
14
|
from an earlier release of the same project.
|
|
@@ -31,9 +31,9 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
|
31
31
|
|
|
32
32
|
--------------------------------------------------------------------------------
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
italic, link, plus and x — are
|
|
36
|
-
license as well.
|
|
34
|
+
Thirteen of those glyphs — arrow-right, chevron-down, chevron-right, chevron-up,
|
|
35
|
+
code, hash, info, italic, link, minus, plus, trash-2 and x — are drawn exactly as
|
|
36
|
+
the Feather project draws them and carry its license as well.
|
|
37
37
|
|
|
38
38
|
The MIT License (MIT)
|
|
39
39
|
|
package/README.md
CHANGED
|
@@ -83,7 +83,7 @@ preview.refresh(session.update(doc)); // repaint dirtyPages ∩ visible
|
|
|
83
83
|
|
|
84
84
|
In Svelte, `<Preview {session} onPick={…} />` exposes the same verbs (`refresh`, `scrollToField`, `focusPosition`, `setZoom`) via `bind:this`.
|
|
85
85
|
|
|
86
|
-
`<Preview>` binds **once**, at mount: swapping `session`, `margin`, `
|
|
86
|
+
`<Preview>` binds **once**, at mount: swapping `session`, `margin`, `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.
|
|
87
87
|
|
|
88
88
|
## Visual editor
|
|
89
89
|
|
package/THEMING.md
CHANGED
|
@@ -53,7 +53,7 @@ The poles take a colour, not `transparent` or `inherit`: the raised surfaces, th
|
|
|
53
53
|
| ---------------- | -------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
54
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
55
|
| `--qm-fg` | `#1c1c1c` / `#d7dadc` | Base ink. Body text, labels, and borders step off it. |
|
|
56
|
-
| `--qm-accent` | `#
|
|
56
|
+
| `--qm-accent` | `#71717a` / `#a1a1aa` | Focus rings, active marks, the preview's active field box. The surface spends no hue of its own, so this is the one it wears. |
|
|
57
57
|
| `--qm-danger` | `#c5221f` | Error diagnostics, the required marker, the delete glyph. |
|
|
58
58
|
| `--qm-warning` | `#b25000` | Warning diagnostics. |
|
|
59
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. |
|
package/dist/core/bloom.d.ts
CHANGED
|
@@ -1,37 +1,8 @@
|
|
|
1
|
-
/** The resting state a bloom animates from and back to: the wash, held at zero. */
|
|
2
|
-
export declare function primeWash(el: HTMLElement): void;
|
|
3
|
-
/** What one bloom runs: resolved from the scale, and from the viewer's motion
|
|
4
|
-
* preference. */
|
|
5
|
-
export interface BloomTiming {
|
|
6
|
-
duration: number;
|
|
7
|
-
frames: Keyframe[];
|
|
8
|
-
}
|
|
9
|
-
/**
|
|
10
|
-
* Resolve the timing off `el`, so the dwell is single-sourced in CSS.
|
|
11
|
-
*
|
|
12
|
-
* One property, both motion preferences: `--_qm-bloom-dwell` already shortens under
|
|
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
|
|
15
|
-
* number and the one half CSS cannot hand over.
|
|
16
|
-
*
|
|
17
|
-
* Read once per batch, not per element: `getComputedStyle` forces a style recalc and
|
|
18
|
-
* `animate` re-dirties style, so a read inside a loop flushes once per element (~0.15ms
|
|
19
|
-
* each). The rungs inherit, so every element in a batch resolves the same numbers.
|
|
20
|
-
*/
|
|
21
|
-
export declare function bloomTiming(el: HTMLElement): BloomTiming;
|
|
22
|
-
/**
|
|
23
|
-
* Bloom the wash on `el` itself: the caller owns the element and has primed it
|
|
24
|
-
* ({@link primeWash}). Returns the running animation, or `undefined` when there is
|
|
25
|
-
* nothing left to run: `elapsed` past the decay, or an environment without WAAPI.
|
|
26
|
-
* Pass `timing` to share one resolve across a batch.
|
|
27
|
-
*/
|
|
28
|
-
export declare function bloom(el: HTMLElement, elapsed?: number, timing?: BloomTiming): Animation | undefined;
|
|
29
1
|
/**
|
|
30
2
|
* Bloom over `host`'s content: an inset child, so the wash neither tints the host's
|
|
31
3
|
* own background nor fades the text under it. `host` must be positioned.
|
|
32
4
|
*
|
|
33
5
|
* One wash node per host, reused while it lives: two quick landings on the same leaf
|
|
34
|
-
* must not stack two alphas over it.
|
|
35
|
-
* (editor leaves) are not rebuilt under the animation the way the preview's boxes are.
|
|
6
|
+
* must not stack two alphas over it.
|
|
36
7
|
*/
|
|
37
8
|
export declare function bloomInside(host: HTMLElement): void;
|
package/dist/core/bloom.js
CHANGED
|
@@ -1,21 +1,11 @@
|
|
|
1
|
-
// The
|
|
2
|
-
// nothing.
|
|
3
|
-
//
|
|
4
|
-
// outlives the hop that caused it is ink the document did not ask for. Both
|
|
5
|
-
// directions land here: the preview's field boxes bloom themselves (they are already
|
|
6
|
-
// empty rects over the page), an editor leaf blooms a transient child over its
|
|
7
|
-
// surface.
|
|
1
|
+
// The arrival wash: a soft accent bloom that rises over a leaf the editor just
|
|
2
|
+
// landed on and decays to nothing. A landing is an event, not a state, so a
|
|
3
|
+
// highlight that outlives the hop that caused it is ink nothing asked for.
|
|
8
4
|
//
|
|
9
5
|
// Opacity is the animated property, never the colour. The wash's alpha is a peak
|
|
10
6
|
// fixed by `--_qm-accent-wash`, so interpolating 0→1→0 keeps the scale in CSS where
|
|
11
7
|
// `color-mix` resolves, instead of in keyframe values a script would have to
|
|
12
8
|
// pre-resolve and re-resolve on a theme change.
|
|
13
|
-
//
|
|
14
|
-
// `elapsed` is what lets a bloom survive the element that carries it. `overlay.ts`
|
|
15
|
-
// rebuilds every box on refresh and the playground recompiles 120ms after each
|
|
16
|
-
// keystroke burst, so a bloom re-STARTED per rebuild would re-bloom continuously
|
|
17
|
-
// while the user writes; a rebuilt node resumes at the offset the old one reached.
|
|
18
|
-
/** Marks a transient wash node: the wash is decoration, never a hit target. */
|
|
19
9
|
const BLOOM_CLASS = 'qm-bloom';
|
|
20
10
|
const WASH = 'var(--_qm-accent-wash)';
|
|
21
11
|
/** The dwell used when the derivation is out of reach (an unstyled root, or jsdom,
|
|
@@ -33,53 +23,30 @@ const FRAMES = [
|
|
|
33
23
|
{ opacity: 0, offset: 1 }
|
|
34
24
|
];
|
|
35
25
|
const FRAMES_REDUCED = [{ opacity: 1 }, { opacity: 1 }];
|
|
36
|
-
/** The resting state a bloom animates from and back to: the wash, held at zero. */
|
|
37
|
-
export function primeWash(el) {
|
|
38
|
-
Object.assign(el.style, { background: WASH, opacity: '0' });
|
|
39
|
-
}
|
|
40
26
|
/**
|
|
41
|
-
*
|
|
27
|
+
* Run one wash on `el`, resolving the dwell off it so the number is single-sourced
|
|
28
|
+
* in CSS.
|
|
42
29
|
*
|
|
43
30
|
* One property, both motion preferences: `--_qm-bloom-dwell` already shortens under
|
|
44
31
|
* `prefers-reduced-motion` in the derivation, so what is left to `matchMedia` here is
|
|
45
32
|
* the shape: a hold and a cut carry no ramps, which is a frame list rather than a
|
|
46
33
|
* number and the one half CSS cannot hand over.
|
|
47
|
-
*
|
|
48
|
-
* Read once per batch, not per element: `getComputedStyle` forces a style recalc and
|
|
49
|
-
* `animate` re-dirties style, so a read inside a loop flushes once per element (~0.15ms
|
|
50
|
-
* each). The rungs inherit, so every element in a batch resolves the same numbers.
|
|
51
34
|
*/
|
|
52
|
-
|
|
35
|
+
function bloom(el) {
|
|
36
|
+
if (typeof el.animate !== 'function')
|
|
37
|
+
return undefined;
|
|
53
38
|
const reduced = typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
54
39
|
const raw = getComputedStyle(el).getPropertyValue('--_qm-bloom-dwell').trim();
|
|
55
40
|
const n = Number.parseFloat(raw);
|
|
56
41
|
const duration = !Number.isFinite(n) ? FALLBACK_MS : raw.endsWith('ms') ? n : n * 1000;
|
|
57
|
-
return
|
|
58
|
-
}
|
|
59
|
-
/**
|
|
60
|
-
* Bloom the wash on `el` itself: the caller owns the element and has primed it
|
|
61
|
-
* ({@link primeWash}). Returns the running animation, or `undefined` when there is
|
|
62
|
-
* nothing left to run: `elapsed` past the decay, or an environment without WAAPI.
|
|
63
|
-
* Pass `timing` to share one resolve across a batch.
|
|
64
|
-
*/
|
|
65
|
-
export function bloom(el, elapsed = 0, timing) {
|
|
66
|
-
if (typeof el.animate !== 'function')
|
|
67
|
-
return undefined;
|
|
68
|
-
const { duration, frames } = timing ?? bloomTiming(el);
|
|
69
|
-
if (elapsed >= duration)
|
|
70
|
-
return undefined;
|
|
71
|
-
const anim = el.animate(frames, { duration });
|
|
72
|
-
if (elapsed > 0)
|
|
73
|
-
anim.currentTime = elapsed;
|
|
74
|
-
return anim;
|
|
42
|
+
return el.animate(reduced ? FRAMES_REDUCED : FRAMES, { duration });
|
|
75
43
|
}
|
|
76
44
|
/**
|
|
77
45
|
* Bloom over `host`'s content: an inset child, so the wash neither tints the host's
|
|
78
46
|
* own background nor fades the text under it. `host` must be positioned.
|
|
79
47
|
*
|
|
80
48
|
* One wash node per host, reused while it lives: two quick landings on the same leaf
|
|
81
|
-
* must not stack two alphas over it.
|
|
82
|
-
* (editor leaves) are not rebuilt under the animation the way the preview's boxes are.
|
|
49
|
+
* must not stack two alphas over it.
|
|
83
50
|
*/
|
|
84
51
|
export function bloomInside(host) {
|
|
85
52
|
let el = host.querySelector(`:scope > .${BLOOM_CLASS}`);
|
|
@@ -99,9 +66,10 @@ export function bloomInside(host) {
|
|
|
99
66
|
position: 'absolute',
|
|
100
67
|
inset: '0',
|
|
101
68
|
pointerEvents: 'none',
|
|
102
|
-
borderRadius: 'inherit'
|
|
69
|
+
borderRadius: 'inherit',
|
|
70
|
+
background: WASH,
|
|
71
|
+
opacity: '0'
|
|
103
72
|
});
|
|
104
|
-
primeWash(el);
|
|
105
73
|
host.appendChild(el);
|
|
106
74
|
}
|
|
107
75
|
const node = el;
|
|
@@ -136,7 +136,7 @@ li.ProseMirror-selectednode::after {
|
|
|
136
136
|
same schema, and a solid rule renders an authored `<u>` and a link identically in a
|
|
137
137
|
paragraph that can hold both. An underline at all, where the tips card's link takes
|
|
138
138
|
none, because this leaf is editable: the `href` is otherwise imperceptible, so
|
|
139
|
-
removing the rule loses an answer rather than ornament. No
|
|
139
|
+
removing the rule loses an answer rather than ornament. No tone of its own:
|
|
140
140
|
`--_qm-accent` already means focus, and a body of accent-coloured links collides
|
|
141
141
|
with the ring and with the correlation wash. */
|
|
142
142
|
.ProseMirror a {
|
|
@@ -289,9 +289,9 @@ li.ProseMirror-selectednode::after {
|
|
|
289
289
|
focus indicator here exactly as it is in the leaf: no ring.
|
|
290
290
|
|
|
291
291
|
The height floor is one line box, named as size times leading so both factors move
|
|
292
|
-
with the type ramp (`.qm-prose-block` states the same
|
|
293
|
-
empty row is then the height it will be once typed,
|
|
294
|
-
no geometry. */
|
|
292
|
+
with the type ramp (`.qm-prose-block` states the same floor against `1em`, whichever
|
|
293
|
+
rung that leaf sets). An empty row is then the height it will be once typed,
|
|
294
|
+
so the first keystroke changes no geometry. */
|
|
295
295
|
.qm-table-cell-host {
|
|
296
296
|
min-width: 4rem;
|
|
297
297
|
min-height: calc(var(--_qm-text-body) * var(--_qm-leading-body));
|
|
@@ -475,8 +475,7 @@ li.ProseMirror-selectednode::after {
|
|
|
475
475
|
|
|
476
476
|
/* A drag in flight. The lifted line reads as picked up by tone, never a shadow:
|
|
477
477
|
elevation is a surface rung here, which says the same thing at both poles. The drop
|
|
478
|
-
rule is the accent, the surface
|
|
479
|
-
to land. */
|
|
478
|
+
rule is the accent, the ink the surface keeps for a state rather than for text. */
|
|
480
479
|
.qm-table-dragging .qm-table-cell.qm-table-lifted {
|
|
481
480
|
background: var(--_qm-surface-raised);
|
|
482
481
|
opacity: var(--_qm-opacity-muted);
|
package/dist/core/theme.css
CHANGED
|
@@ -86,7 +86,9 @@
|
|
|
86
86
|
off it, and the column is a step down. */
|
|
87
87
|
--_qm-bg: var(--qm-bg, light-dark(#fff, #272729));
|
|
88
88
|
--_qm-fg: var(--qm-fg, light-dark(#1c1c1c, #d7dadc));
|
|
89
|
-
|
|
89
|
+
/* Neutral: the surface spends no hue on a state it can draw in grey. The pair
|
|
90
|
+
clears WCAG 2.4.11's 3:1 against the card at both poles, at 4.8:1 and 5.8:1. */
|
|
91
|
+
--_qm-accent: var(--qm-accent, light-dark(#71717a, #a1a1aa));
|
|
90
92
|
--_qm-danger: var(--qm-danger, #c5221f);
|
|
91
93
|
--_qm-warning: var(--qm-warning, #b25000);
|
|
92
94
|
--_qm-font: var(--qm-font, ui-sans-serif, system-ui, sans-serif);
|
|
@@ -156,9 +158,9 @@
|
|
|
156
158
|
--_qm-ink: var(--_qm-fg);
|
|
157
159
|
--_qm-ink-label: color-mix(in oklab, var(--_qm-fg), var(--_qm-bg) 35%);
|
|
158
160
|
--_qm-ink-ghost: color-mix(in oklab, var(--_qm-fg), var(--_qm-bg) 60%);
|
|
159
|
-
/* The
|
|
160
|
-
wash animates from and back to zero, never held, so it reads
|
|
161
|
-
|
|
161
|
+
/* The arrival wash (`core/bloom.ts`). Its alpha is a peak, not a tint: the
|
|
162
|
+
wash animates from and back to zero, never held, so it reads over the leaf it
|
|
163
|
+
lands on while the leaf rests clean. */
|
|
162
164
|
--_qm-accent-wash: color-mix(in oklab, var(--_qm-accent) 18%, transparent);
|
|
163
165
|
/* The accent held: what a selected line of a table is tinted with, and what its
|
|
164
166
|
grip is filled with, so the two ends of one gesture are one colour. A second
|
|
@@ -180,16 +182,26 @@
|
|
|
180
182
|
/* The tips card'S hue: the surface's one exit from the greyscale that is not
|
|
181
183
|
a status, and what the amber and the typewriter face say together is
|
|
182
184
|
draft (VISUAL_EDITOR §"Card operations"). A literal rather than a dial
|
|
183
|
-
because there is nothing left to turn: the
|
|
185
|
+
because there is nothing left to turn: the rungs under it mix over
|
|
184
186
|
the surface and the ink a consumer already tuned, so the card follows a
|
|
185
187
|
brand untold. The mixes are faint on purpose: a warmth, not a badge. */
|
|
186
188
|
--_qm-tips: #e88c0a;
|
|
187
189
|
--_qm-tips-surface: color-mix(in oklab, var(--_qm-surface), var(--_qm-tips) 4%);
|
|
188
190
|
--_qm-tips-ink: color-mix(in oklab, var(--_qm-ink), var(--_qm-tips) 24%);
|
|
189
|
-
/* The
|
|
190
|
-
|
|
191
|
-
than
|
|
192
|
-
|
|
191
|
+
/* The ink and the amber stepped up, and the pair is spent on the two runs that
|
|
192
|
+
are not the sentence: a token in a tip, and the foot's control under the
|
|
193
|
+
pointer. One rung each rather than one per run, a chip and a hover four
|
|
194
|
+
percent apart being a number with no second decision under it. */
|
|
195
|
+
--_qm-tips-ink-warm: color-mix(in oklab, var(--_qm-ink), var(--_qm-tips) 45%);
|
|
196
|
+
--_qm-tips-fill: color-mix(in oklab, var(--_qm-tips) 12%, transparent);
|
|
197
|
+
/* The typewriter, the half of the slip the amber cannot carry: a modern mono
|
|
198
|
+
reads as code where a Courier reads as typed. Its own rung rather than
|
|
199
|
+
`--_qm-font-mono`, the chrome's readout face, which a consumer turns for
|
|
200
|
+
tabular figures and not to dress this card. Courier Prime leads and is nobody's
|
|
201
|
+
system font, so a host that loads it lands on it and every other host falls to
|
|
202
|
+
the Courier the platform already carries; the package fetches nothing, a preset
|
|
203
|
+
costing a network request being one a consumer cannot decline. */
|
|
204
|
+
--_qm-tips-font: 'Courier Prime', 'Courier New', Courier, ui-monospace, monospace;
|
|
193
205
|
|
|
194
206
|
/* ── Ratios and effects ──────────────────────────────────────────────────
|
|
195
207
|
No palette fixes these, so they are their own rungs. There is no shadow
|
|
@@ -213,15 +225,18 @@
|
|
|
213
225
|
ring, and on what state, is the other half: a rule, `.qm-focus-ring` in
|
|
214
226
|
`visual/controls.css`, since a rung each control reassembles is a scale
|
|
215
227
|
only up to the point where it is assembled. One width: focus is the only
|
|
216
|
-
state that draws a ring anywhere, so there is no held-back tier under it.
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
228
|
+
state that draws a ring anywhere, so there is no held-back tier under it.
|
|
229
|
+
Sub-pixel, because at two it reads as a second border on a card whose every
|
|
230
|
+
other edge is a hairline. */
|
|
231
|
+
--_qm-ring-width: 1.5px;
|
|
232
|
+
/* Every ring the surface draws, focus and selection alike, which is why the name
|
|
233
|
+
carries neither. Assembled here because the parts spelled out at a use site pass
|
|
234
|
+
every axis — each reads a rung — while drawing a ring nothing else agrees with. */
|
|
223
235
|
--_qm-ring: var(--_qm-ring-width) solid var(--_qm-accent);
|
|
224
236
|
--_qm-ring-offset: 1px;
|
|
237
|
+
/* What a clip box carries past its content for a focused control inside it to
|
|
238
|
+
keep the whole mark. */
|
|
239
|
+
--_qm-ring-reach: calc(var(--_qm-ring-width) + var(--_qm-ring-offset));
|
|
225
240
|
|
|
226
241
|
/* ── Motion ──────────────────────────────────────────────────────────────
|
|
227
242
|
Two rungs, by what the trip disturbs: `fast` stays inside the element's own
|
|
@@ -265,6 +280,7 @@
|
|
|
265
280
|
--_qm-space-2: calc(var(--_qm-space) * 2);
|
|
266
281
|
--_qm-space-3: calc(var(--_qm-space) * 3);
|
|
267
282
|
--_qm-space-4: calc(var(--_qm-space) * 4);
|
|
283
|
+
--_qm-space-5: calc(var(--_qm-space) * 5);
|
|
268
284
|
/* A rung because the two axes are two decisions and the ladder alone cannot hold
|
|
269
285
|
them together: `check:style`'s rhythm axis sees each box read a rung, never that
|
|
270
286
|
they read the same one, which is the drift `visual/controls.css` already names
|
package/dist/preset/scale.css
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
page around the surfaces out. `core/theme.css` is the same shape one level down
|
|
3
3
|
(dials in, `--_qm-*` out), and the two are calibrated against each other: the
|
|
4
4
|
figures here are the figures there (the ground step's two arms, 8% hover, 17%
|
|
5
|
-
border, 45% and 60% ink) and the dial means the same plane in
|
|
6
|
-
and a card on it land on the same tone at both poles and a
|
|
7
|
-
editor's own column exactly. The well under the ground has no counterpart here: a
|
|
5
|
+
border, 45% and 60% ink, the ring's width) and the dial means the same plane in
|
|
6
|
+
both, so a host plate and a card on it land on the same tone at both poles and a
|
|
7
|
+
host page meets an editor's own column exactly. The well under the ground has no counterpart here: a
|
|
8
8
|
host's own controls are boxed and carry a hairline, so nothing on a page needs the
|
|
9
9
|
step an edgeless field does.
|
|
10
10
|
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
--qmh-space-3: calc(var(--qmh-space) * 3);
|
|
62
62
|
--qmh-space-4: calc(var(--qmh-space) * 4);
|
|
63
63
|
--qmh-border-width: 1px;
|
|
64
|
-
--qmh-ring-width:
|
|
64
|
+
--qmh-ring-width: 1.5px;
|
|
65
65
|
--qmh-radius: var(--qm-radius, 8px);
|
|
66
66
|
--qmh-radius-inner: calc(var(--qmh-radius) / 2);
|
|
67
67
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<!--
|
|
2
2
|
`@quillmark/svelte/preview`'s Svelte wrapper: mounts `createPreview` over a
|
|
3
3
|
container div on mount and tears it down on unmount. No logic beyond wiring;
|
|
4
|
-
paint.ts/
|
|
4
|
+
paint.ts/bridge.ts/controller.ts own the behavior. Exposes the
|
|
5
5
|
`PreviewController` verbs as instance methods (`bind:this`) for a consumer
|
|
6
6
|
that drives `refresh`/`scrollToField`/`focusPosition`/`setZoom` imperatively
|
|
7
7
|
(e.g. after `session.apply` elsewhere).
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* Remount contract. `createPreview` binds once in `onMount`; a later change to any
|
|
20
|
-
* prop it closed over (`session`, `margin`, `
|
|
21
|
-
*
|
|
20
|
+
* prop it closed over (`session`, `margin`, `onPick`, `onError`, `strings`) is not
|
|
21
|
+
* observed, and each reports `rebind-ignored` when swapped. Swap
|
|
22
22
|
* the session by remounting (`{#key session}`, as the playground does); drive
|
|
23
23
|
* in-place edits through the `refresh(change)` method, not a prop change.
|
|
24
24
|
*
|
|
@@ -35,7 +35,6 @@
|
|
|
35
35
|
* not this attribute. */
|
|
36
36
|
style?: string;
|
|
37
37
|
margin?: number;
|
|
38
|
-
overlays?: boolean;
|
|
39
38
|
onPick?: (at: Landing) => void;
|
|
40
39
|
/** A page paint the backend refused; the error message state shows either way. */
|
|
41
40
|
onError?: EditorErrorHandler;
|
|
@@ -43,22 +42,13 @@
|
|
|
43
42
|
strings?: PreviewStringsInput;
|
|
44
43
|
}
|
|
45
44
|
|
|
46
|
-
let {
|
|
47
|
-
session,
|
|
48
|
-
margin,
|
|
49
|
-
overlays,
|
|
50
|
-
onPick,
|
|
51
|
-
onError,
|
|
52
|
-
strings,
|
|
53
|
-
class: className,
|
|
54
|
-
style
|
|
55
|
-
}: Props = $props();
|
|
45
|
+
let { session, margin, onPick, onError, strings, class: className, style }: Props = $props();
|
|
56
46
|
|
|
57
47
|
let containerEl: HTMLDivElement | undefined = $state();
|
|
58
48
|
let controller: PreviewController | undefined;
|
|
59
49
|
|
|
60
50
|
guardRebind(
|
|
61
|
-
() => ({ session, margin,
|
|
51
|
+
() => ({ session, margin, onPick, onError, strings }),
|
|
62
52
|
'Remount the preview ({#key session}) to rebind.'
|
|
63
53
|
);
|
|
64
54
|
|
|
@@ -67,7 +57,6 @@
|
|
|
67
57
|
controller = createPreview(session, {
|
|
68
58
|
container: containerEl,
|
|
69
59
|
margin,
|
|
70
|
-
overlays,
|
|
71
60
|
onPick,
|
|
72
61
|
onError,
|
|
73
62
|
strings
|
|
@@ -98,8 +87,7 @@
|
|
|
98
87
|
|
|
99
88
|
<style>
|
|
100
89
|
/* A detached root: the preview is not a descendant of the editor, so it carries
|
|
101
|
-
`data-qm-root` for the page
|
|
102
|
-
(core/theme.css).
|
|
90
|
+
`data-qm-root` for the page rungs paint.ts reads (core/theme.css).
|
|
103
91
|
|
|
104
92
|
The desk as well as the paper: a sheet is at `--_qm-surface` (paint.ts), so the
|
|
105
93
|
tone behind it is the sunken rung and the gutter is the margin the sheet floats
|
|
@@ -4,8 +4,8 @@ import type { DocPath, Landing, Place } from '../core/address.js';
|
|
|
4
4
|
import type { EditorErrorHandler } from '../core/errors.js';
|
|
5
5
|
/**
|
|
6
6
|
* Remount contract. `createPreview` binds once in `onMount`; a later change to any
|
|
7
|
-
* prop it closed over (`session`, `margin`, `
|
|
8
|
-
*
|
|
7
|
+
* prop it closed over (`session`, `margin`, `onPick`, `onError`, `strings`) is not
|
|
8
|
+
* observed, and each reports `rebind-ignored` when swapped. Swap
|
|
9
9
|
* the session by remounting (`{#key session}`, as the playground does); drive
|
|
10
10
|
* in-place edits through the `refresh(change)` method, not a prop change.
|
|
11
11
|
*
|
|
@@ -22,7 +22,6 @@ interface Props {
|
|
|
22
22
|
* not this attribute. */
|
|
23
23
|
style?: string;
|
|
24
24
|
margin?: number;
|
|
25
|
-
overlays?: boolean;
|
|
26
25
|
onPick?: (at: Landing) => void;
|
|
27
26
|
/** A page paint the backend refused; the error message state shows either way. */
|
|
28
27
|
onError?: EditorErrorHandler;
|
package/dist/preview/bridge.js
CHANGED
|
@@ -69,8 +69,8 @@ export function createBridge(session, container, slots, onPick) {
|
|
|
69
69
|
const slot = slots[box.page];
|
|
70
70
|
if (!slot)
|
|
71
71
|
return false;
|
|
72
|
-
// A discrete act ("show me this field"), so it centres every time
|
|
73
|
-
//
|
|
72
|
+
// A discrete act ("show me this field"), so it centres every time, unlike
|
|
73
|
+
// the continuous hop below.
|
|
74
74
|
centre(measure(slot, rectToPercent(box.rect, slot.size)));
|
|
75
75
|
return true;
|
|
76
76
|
},
|
|
@@ -7,8 +7,6 @@ export interface PreviewOptions {
|
|
|
7
7
|
container: HTMLElement;
|
|
8
8
|
/** Pages kept painted beyond the visible band. Default 1. */
|
|
9
9
|
margin?: number;
|
|
10
|
-
/** Draw field-box overlays. Default true. */
|
|
11
|
-
overlays?: boolean;
|
|
12
10
|
/** A click resolved to an address ({@link Landing}): a caret where the compile
|
|
13
11
|
* tracks the content under the point, the field alone where it tracks only the
|
|
14
12
|
* placement. The hook does not fire where the compile tracks neither. */
|
|
@@ -21,19 +19,19 @@ export interface PreviewOptions {
|
|
|
21
19
|
strings?: PreviewStringsInput;
|
|
22
20
|
}
|
|
23
21
|
export interface PreviewController {
|
|
24
|
-
/** Repaint `dirtyPages ∩ visible` and re-
|
|
22
|
+
/** Repaint `dirtyPages ∩ visible` and re-locate the followed caret; the only
|
|
23
|
+
* apply-driven hop. */
|
|
25
24
|
refresh(change: ChangeSet): void;
|
|
26
25
|
/**
|
|
27
|
-
* Scroll `field`'s first box into view
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
26
|
+
* Scroll `field`'s first box into view. `false` when this compile places nothing
|
|
27
|
+
* at that address — an answer, not a failure: the plate places plenty it does not
|
|
28
|
+
* track, and the preview carries no schema to tell that from a field the host
|
|
29
|
+
* misnamed. The editor's `focusField` is what distinguishes them.
|
|
31
30
|
*/
|
|
32
31
|
scrollToField(field: DocPath): boolean;
|
|
33
32
|
/**
|
|
34
|
-
* Bring the caret at `at` into view
|
|
35
|
-
*
|
|
36
|
-
* bloom fires only on a change of address.
|
|
33
|
+
* Bring the caret at `at` into view: the pane moves only when the caret has left
|
|
34
|
+
* the fold.
|
|
37
35
|
*
|
|
38
36
|
* Takes the editor's own `onCaretMove` payload, so the editor→preview half of
|
|
39
37
|
* the bridge is `onCaretMove={preview.focusPosition}` and translates nothing. A
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import { reportError, errorMessage } from '../core/errors.js';
|
|
2
2
|
import { createPaintLoop } from './paint.js';
|
|
3
|
-
import { createOverlay } from './overlay.js';
|
|
4
3
|
import { createBridge } from './bridge.js';
|
|
5
4
|
import { mergePreviewStrings } from './strings.js';
|
|
6
5
|
const CONTAINER_CLASS = 'qm-preview';
|
|
7
|
-
// The message states share one element; each carries `MESSAGE_CLASS` plus a
|
|
8
|
-
//
|
|
9
|
-
// the zero-page hook consumers and tests target.
|
|
6
|
+
// The message states share one element; each carries `MESSAGE_CLASS` plus a state
|
|
7
|
+
// class, the hooks a consumer and the tests target.
|
|
10
8
|
const MESSAGE_CLASS = 'qm-preview-message';
|
|
11
9
|
const EMPTY_CLASS = 'qm-preview-empty';
|
|
12
10
|
const UNSUPPORTED_CLASS = 'qm-preview-unsupported';
|
|
@@ -14,13 +12,10 @@ const ERROR_CLASS = 'qm-preview-error';
|
|
|
14
12
|
export function createPreview(session, opts) {
|
|
15
13
|
const container = opts.container;
|
|
16
14
|
const margin = opts.margin ?? 1;
|
|
17
|
-
const overlaysEnabled = opts.overlays ?? true;
|
|
18
15
|
const t = mergePreviewStrings(opts.strings);
|
|
19
16
|
container.classList.add(CONTAINER_CLASS);
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
// and a paint that threw. One element, restamped; these states are mutually
|
|
23
|
-
// exclusive, and the zero-page case keeps its `qm-preview-empty` hook.
|
|
17
|
+
// One element, restamped: the non-paint states (empty, unsupported, a paint that
|
|
18
|
+
// threw) are mutually exclusive.
|
|
24
19
|
let message;
|
|
25
20
|
function showMessage(text, state) {
|
|
26
21
|
if (!message) {
|
|
@@ -50,34 +45,26 @@ export function createPreview(session, opts) {
|
|
|
50
45
|
});
|
|
51
46
|
showMessage(t.renderFailed, ERROR_CLASS);
|
|
52
47
|
});
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
|
|
48
|
+
// The bridge lives and dies with the slot set, so it is held until `render` finds
|
|
49
|
+
// a compile paintable. Rebuilt rather than patched whenever the count changes: the
|
|
50
|
+
// slots array is a stable identity but its members are swapped on reconcile, and
|
|
51
|
+
// the listeners are per slot.
|
|
56
52
|
let bridge;
|
|
57
|
-
// overlay/bridge live and die with the slot set. Rebuilt (not patched) whenever
|
|
58
|
-
// the count changes: the slots array is a stable identity but its members are
|
|
59
|
-
// swapped on reconcile, and both attach DOM/listeners per slot.
|
|
60
53
|
function attach() {
|
|
61
|
-
overlay = overlaysEnabled ? createOverlay(session, paintLoop.slots) : undefined;
|
|
62
54
|
bridge = createBridge(session, container, paintLoop.slots, opts.onPick);
|
|
63
55
|
}
|
|
64
56
|
function detach() {
|
|
65
|
-
overlay?.destroy();
|
|
66
57
|
bridge?.destroy();
|
|
67
|
-
overlay = undefined;
|
|
68
58
|
bridge = undefined;
|
|
69
59
|
}
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
// compile that later gains paintable pages recovers; the check spans the
|
|
75
|
-
// paint capability generally, not just the page count.
|
|
60
|
+
// Called at construction and after every `apply`, so `supportsCanvas` is re-read
|
|
61
|
+
// per compile (runtime.d.ts: re-check after `open`) and a 0-page or non-canvas
|
|
62
|
+
// compile that later gains paintable pages recovers; the check spans the paint
|
|
63
|
+
// capability generally, not just the page count.
|
|
76
64
|
function render(pageCount, dirtyPages) {
|
|
77
65
|
if (!session.supportsCanvas || pageCount === 0) {
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
// boundary cannot raster is a genuine unsupported.
|
|
66
|
+
// A 0-page compile is a recoverable empty; pages the boundary cannot
|
|
67
|
+
// raster are a genuine unsupported.
|
|
81
68
|
paintLoop.refresh([], 0);
|
|
82
69
|
detach();
|
|
83
70
|
showMessage(pageCount === 0 ? t.noPages : t.unsupported, pageCount === 0 ? EMPTY_CLASS : UNSUPPORTED_CLASS);
|
|
@@ -89,18 +76,11 @@ export function createPreview(session, opts) {
|
|
|
89
76
|
const countChanged = pageCount !== paintLoop.slots.length;
|
|
90
77
|
hideMessage();
|
|
91
78
|
paintLoop.refresh(dirtyPages, pageCount);
|
|
79
|
+
// The slot set moved under the bridge's feet, or we are leaving a message state.
|
|
92
80
|
if (countChanged || !bridge) {
|
|
93
|
-
// The slot set moved under overlay/bridge's feet (pages added/removed,
|
|
94
|
-
// or we are leaving a message state): rebuild both against the
|
|
95
|
-
// reconciled slots rather than patch a stale snapshot in place.
|
|
96
81
|
detach();
|
|
97
82
|
attach();
|
|
98
83
|
}
|
|
99
|
-
else {
|
|
100
|
-
// Box positions can still shift within the same page set (text reflow),
|
|
101
|
-
// so geometry is always re-read, not just dirty pages.
|
|
102
|
-
overlay?.refresh();
|
|
103
|
-
}
|
|
104
84
|
}
|
|
105
85
|
render(session.pageCount, []);
|
|
106
86
|
// The last place the editor put the caret, re-located after every recompile.
|
|
@@ -120,16 +100,11 @@ export function createPreview(session, opts) {
|
|
|
120
100
|
bridge?.focusPosition(followed.field, followed.pos);
|
|
121
101
|
},
|
|
122
102
|
scrollToField(field) {
|
|
123
|
-
|
|
124
|
-
// Marked either way: a compile that gains the address later blooms it on the
|
|
125
|
-
// rebuild, since the flash carries its own start time (`core/bloom.ts`).
|
|
126
|
-
overlay?.flashField(field);
|
|
127
|
-
return found;
|
|
103
|
+
return bridge?.scrollToField(field) ?? false;
|
|
128
104
|
},
|
|
129
105
|
focusPosition(at) {
|
|
130
106
|
followed = at;
|
|
131
107
|
bridge?.focusPosition(at.field, at.pos);
|
|
132
|
-
overlay?.flashField(at.field);
|
|
133
108
|
},
|
|
134
109
|
setZoom(scale) {
|
|
135
110
|
paintLoop.setDensityZoom(scale);
|
|
@@ -15,9 +15,8 @@ export interface PdfPoint {
|
|
|
15
15
|
}
|
|
16
16
|
/**
|
|
17
17
|
* An address's boxes: `fieldBoxes`'s union, else its own `regions()` rects, else the
|
|
18
|
-
* rects of everything under it.
|
|
19
|
-
*
|
|
20
|
-
* responsibilities").
|
|
18
|
+
* rects of everything under it. What the scroll reads, so an address `regions()`
|
|
19
|
+
* names is never one the preview cannot place (PREVIEW.md §"Two responsibilities").
|
|
21
20
|
*
|
|
22
21
|
* `fieldBoxes` answers `[]` for a scalar reference the plate places without tracking
|
|
23
22
|
* its content and for a `richtext[]` element; runtime.d.ts says what such a field's
|
|
@@ -30,15 +29,14 @@ export interface PdfPoint {
|
|
|
30
29
|
export declare function boxesForField(field: string, boxes: readonly FieldRegion[], regions: readonly FieldRegion[]): readonly FieldRegion[];
|
|
31
30
|
/**
|
|
32
31
|
* Forward: a PDF-pt rect -> CSS % of the page box. Percent positioning tracks
|
|
33
|
-
* the page's displayed size across DPR and container resize for free
|
|
34
|
-
*
|
|
32
|
+
* the page's displayed size across DPR and container resize for free, so the
|
|
33
|
+
* scroll's measurement needs no zoom or DPR term of its own (bridge.ts).
|
|
35
34
|
*/
|
|
36
35
|
export declare function rectToPercent(rect: PdfRect, pageSize: PageSize): PercentRect;
|
|
37
36
|
/**
|
|
38
|
-
* Position `el` absolutely at `pct` (the % rect this module produces)
|
|
39
|
-
* `PercentRect` → `element.style` write,
|
|
40
|
-
* the
|
|
41
|
-
* The caller sets any box chrome (border, background) on top.
|
|
37
|
+
* Position `el` absolutely at `pct` (the % rect this module produces): the one
|
|
38
|
+
* `PercentRect` → `element.style` write, so the positioning convention lives beside
|
|
39
|
+
* the transform that produces it rather than at the caller placing a marker.
|
|
42
40
|
*/
|
|
43
41
|
export declare function applyPercentRect(el: HTMLElement, pct: PercentRect): void;
|
|
44
42
|
/**
|