@quillmark/svelte 0.1.1 → 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 +43 -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 +2 -2
package/dist/core/lifecycle.js
CHANGED
|
@@ -1,21 +1,47 @@
|
|
|
1
1
|
// `@quillmark/wasm` ships wasm-bindgen's web target: the core build is
|
|
2
|
-
// instantiated by an explicit `await init()`,
|
|
3
|
-
//
|
|
4
|
-
//
|
|
2
|
+
// instantiated by an explicit `await init()`, which RESOLVES TO the classes and
|
|
3
|
+
// free functions — the artifact exports none of them statically, so awaiting is
|
|
4
|
+
// the only way to hold one. Instantiation carries the panic hook on its start
|
|
5
|
+
// section, so a Rust panic reaches JS as a stack trace.
|
|
5
6
|
import { init as initWasm } from '@quillmark/wasm';
|
|
6
7
|
let started;
|
|
8
|
+
/** The resolved surface, for the sync verbs below that cannot await one. */
|
|
9
|
+
let latched;
|
|
7
10
|
/**
|
|
8
|
-
* Instantiate the core WASM build. Await once before
|
|
9
|
-
* from any surface's setup path; the promise is memoized,
|
|
10
|
-
* share one instantiation and repeat calls are free. A failed
|
|
11
|
-
* memo, leaving a retry possible.
|
|
11
|
+
* Instantiate the core WASM build and resolve to its surface. Await once before
|
|
12
|
+
* any other boundary verb, from any surface's setup path; the promise is memoized,
|
|
13
|
+
* so concurrent callers share one instantiation and repeat calls are free. A failed
|
|
14
|
+
* init clears the memo, leaving a retry possible.
|
|
15
|
+
*
|
|
16
|
+
* The surface is the artifact's own, handed back unchanged: `const { Quill, Document }
|
|
17
|
+
* = await init()` is the whole of a host's access to the classes, and this package
|
|
18
|
+
* re-declares none of it.
|
|
12
19
|
*/
|
|
13
20
|
export function init() {
|
|
14
21
|
if (started)
|
|
15
22
|
return started;
|
|
16
|
-
started = initWasm().
|
|
23
|
+
started = initWasm().then((surface) => {
|
|
24
|
+
latched = surface;
|
|
25
|
+
return surface;
|
|
26
|
+
}, (err) => {
|
|
17
27
|
started = undefined;
|
|
18
28
|
throw err;
|
|
19
29
|
});
|
|
20
30
|
return started;
|
|
21
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* The surface synchronously, for this package's pure verbs: the `DocPath` formatter
|
|
34
|
+
* on the public API and the codec inside a ProseMirror transaction, neither of which
|
|
35
|
+
* can await. Package-internal, and read at the call rather than destructured at
|
|
36
|
+
* module scope, so no module's import graph carries an ordering constraint.
|
|
37
|
+
*
|
|
38
|
+
* Reaching one before {@link init} resolves throws, naming the fix. This is the seam
|
|
39
|
+
* that turns an awaited surface back into sync verbs, so it is the one place the
|
|
40
|
+
* precondition the gate made structural has to be stated again.
|
|
41
|
+
*/
|
|
42
|
+
export function core() {
|
|
43
|
+
if (!latched) {
|
|
44
|
+
throw new Error('@quillmark/svelte: the WASM core is not initialized. Await `init()` from `@quillmark/svelte/core` before mounting a surface or calling a boundary verb.');
|
|
45
|
+
}
|
|
46
|
+
return latched;
|
|
47
|
+
}
|
|
@@ -3,10 +3,24 @@ import { type EditorErrorHandler } from './errors.js';
|
|
|
3
3
|
export type Bound = Record<string, unknown> & {
|
|
4
4
|
onError?: EditorErrorHandler;
|
|
5
5
|
};
|
|
6
|
+
/**
|
|
7
|
+
* Report the violation, then throw it in a dev build. Both surfaces raise it here; the
|
|
8
|
+
* guards themselves stay separate, since the preview snapshots its props once and the
|
|
9
|
+
* editor re-reads its pair on every re-key.
|
|
10
|
+
*
|
|
11
|
+
* The report alone reaches whoever bound `onError` at mount, and a consumer swapping
|
|
12
|
+
* that handler beside the handle it belongs to is the case this exists for: they are
|
|
13
|
+
* told through the sink they just abandoned. A throw needs no handler to arrive.
|
|
14
|
+
*
|
|
15
|
+
* It costs the surface nothing: an effect that throws neither unmounts its tree nor
|
|
16
|
+
* stops the ones beside it, so the surface goes on serving what it bound, which is the
|
|
17
|
+
* very state being reported.
|
|
18
|
+
*/
|
|
19
|
+
export declare function reportRebindIgnored(onError: EditorErrorHandler | undefined, message: string): void;
|
|
6
20
|
/**
|
|
7
21
|
* Report `rebind-ignored` once, at `dev`, when any prop `snapshot` names is swapped
|
|
8
|
-
* in place. `remount` closes the message with the
|
|
9
|
-
* (`Remount the preview ({#key session}) to rebind.`).
|
|
22
|
+
* in place, and in a dev build throw it after. `remount` closes the message with the
|
|
23
|
+
* surface's own remount spelling (`Remount the preview ({#key session}) to rebind.`).
|
|
10
24
|
*
|
|
11
25
|
* Mounted identities are recorded on the effect's FIRST run, the flush the surface
|
|
12
26
|
* binds in; every later comparison reads them through the same `snapshot`.
|
|
@@ -1,16 +1,38 @@
|
|
|
1
1
|
// The remount contract, enforced. `Preview` binds its props ONCE, in `onMount`, and
|
|
2
2
|
// observes nothing after: the vanilla core below it owns scroll position, mounted
|
|
3
3
|
// slots and observer sets that a remount would discard on every apply. So a prop
|
|
4
|
-
// swapped in place is reported, not dropped in silence.
|
|
4
|
+
// swapped in place is reported, not dropped in silence, and thrown in a dev build.
|
|
5
5
|
//
|
|
6
6
|
// A guard over ONE prop of a set is worse than none: a consumer who learns the
|
|
7
7
|
// surface complains when it ignores a prop reads silence on the rest as reactivity.
|
|
8
8
|
// The snapshot is every once-bound prop, and one report names whichever went stale.
|
|
9
9
|
import { reportError } from './errors.js';
|
|
10
|
+
// True in a dev build whose bundler injects `import.meta.env`. Cast, since the package
|
|
11
|
+
// declares no bundler's ambient types; false where nothing injects it, so a toolchain
|
|
12
|
+
// that does not answer gets the report alone.
|
|
13
|
+
const DEV = import.meta.env?.DEV === true;
|
|
14
|
+
/**
|
|
15
|
+
* Report the violation, then throw it in a dev build. Both surfaces raise it here; the
|
|
16
|
+
* guards themselves stay separate, since the preview snapshots its props once and the
|
|
17
|
+
* editor re-reads its pair on every re-key.
|
|
18
|
+
*
|
|
19
|
+
* The report alone reaches whoever bound `onError` at mount, and a consumer swapping
|
|
20
|
+
* that handler beside the handle it belongs to is the case this exists for: they are
|
|
21
|
+
* told through the sink they just abandoned. A throw needs no handler to arrive.
|
|
22
|
+
*
|
|
23
|
+
* It costs the surface nothing: an effect that throws neither unmounts its tree nor
|
|
24
|
+
* stops the ones beside it, so the surface goes on serving what it bound, which is the
|
|
25
|
+
* very state being reported.
|
|
26
|
+
*/
|
|
27
|
+
export function reportRebindIgnored(onError, message) {
|
|
28
|
+
reportError(onError, { code: 'rebind-ignored', severity: 'dev', message });
|
|
29
|
+
if (DEV)
|
|
30
|
+
throw new Error(`[quillmark/ui] rebind-ignored: ${message}`);
|
|
31
|
+
}
|
|
10
32
|
/**
|
|
11
33
|
* Report `rebind-ignored` once, at `dev`, when any prop `snapshot` names is swapped
|
|
12
|
-
* in place. `remount` closes the message with the
|
|
13
|
-
* (`Remount the preview ({#key session}) to rebind.`).
|
|
34
|
+
* in place, and in a dev build throw it after. `remount` closes the message with the
|
|
35
|
+
* surface's own remount spelling (`Remount the preview ({#key session}) to rebind.`).
|
|
14
36
|
*
|
|
15
37
|
* Mounted identities are recorded on the effect's FIRST run, the flush the surface
|
|
16
38
|
* binds in; every later comparison reads them through the same `snapshot`.
|
|
@@ -35,10 +57,6 @@ export function guardRebind(snapshot, remount) {
|
|
|
35
57
|
if (stale.length === 0)
|
|
36
58
|
return;
|
|
37
59
|
reported = true;
|
|
38
|
-
|
|
39
|
-
code: 'rebind-ignored',
|
|
40
|
-
severity: 'dev',
|
|
41
|
-
message: `${stale.join(', ')} swapped in place; the mounted surface still holds what it bound at mount. ${remount}`
|
|
42
|
-
});
|
|
60
|
+
reportRebindIgnored(bound.onError, `${stale.join(', ')} swapped in place; the mounted surface still holds what it bound at mount. ${remount}`);
|
|
43
61
|
});
|
|
44
62
|
}
|
package/dist/core/theme.css
CHANGED
|
@@ -105,12 +105,13 @@
|
|
|
105
105
|
--_qm-border: color-mix(in oklab, var(--_qm-bg), var(--_qm-fg) 17%);
|
|
106
106
|
/* A rule that separates without structuring: the lines BETWEEN a table's cells,
|
|
107
107
|
where the frame around them is the structure. Half the border's mix rather
|
|
108
|
-
than a
|
|
108
|
+
than a surface rung of its own, because it is a stroke and reads against the
|
|
109
109
|
surface behind it, not a fill stacked on one. */
|
|
110
110
|
--_qm-border-faint: color-mix(in oklab, var(--_qm-bg), var(--_qm-fg) 9%);
|
|
111
|
+
/* A fourth step between `label` and `ghost` is a number with no fourth kind of
|
|
112
|
+
text under it. */
|
|
111
113
|
--_qm-ink: var(--_qm-fg);
|
|
112
114
|
--_qm-ink-label: color-mix(in oklab, var(--_qm-fg), var(--_qm-bg) 35%);
|
|
113
|
-
--_qm-ink-meta: color-mix(in oklab, var(--_qm-fg), var(--_qm-bg) 45%);
|
|
114
115
|
--_qm-ink-ghost: color-mix(in oklab, var(--_qm-fg), var(--_qm-bg) 60%);
|
|
115
116
|
/* The correlation wash (`core/bloom.ts`). Its alpha is a PEAK, not a tint: the
|
|
116
117
|
wash animates from and back to zero, never held, so it reads at pane scale
|
|
@@ -141,7 +142,12 @@
|
|
|
141
142
|
only up to the point where it is assembled. ONE width: focus is the only
|
|
142
143
|
state that draws a ring anywhere, so there is no held-back tier under it. */
|
|
143
144
|
--_qm-ring-width: 2px;
|
|
144
|
-
|
|
145
|
+
/* Every ring the surface draws, whatever the state under it: focus and selection
|
|
146
|
+
alike, which is why the name carries neither. Assembled here because the parts
|
|
147
|
+
spelled out at a use site pass every axis — each reads a rung — while drawing a
|
|
148
|
+
ring nothing else agrees with. The width stays separately reachable: a drop
|
|
149
|
+
indicator is the ring's thickness without being a ring. */
|
|
150
|
+
--_qm-ring: var(--_qm-ring-width) solid var(--_qm-accent);
|
|
145
151
|
--_qm-ring-offset: 1px;
|
|
146
152
|
|
|
147
153
|
/* ── Motion ──────────────────────────────────────────────────────────────
|
|
@@ -154,12 +160,11 @@
|
|
|
154
160
|
animates over WAAPI and reads it from here. */
|
|
155
161
|
--_qm-bloom-dwell: 1100ms;
|
|
156
162
|
|
|
157
|
-
/*
|
|
158
|
-
|
|
159
|
-
|
|
163
|
+
/* ONE curve, symmetric because an interruption turns these trips around: a hover
|
|
164
|
+
leaves before it arrives, a card collapses mid-expand. The one-way pair a scale
|
|
165
|
+
like this often carries is minted when a trip long enough to read one arrives;
|
|
166
|
+
the popover's pop is the nearest and at `fast` it is under that threshold. */
|
|
160
167
|
--_qm-ease-reverse: cubic-bezier(0.4, 0, 0.6, 1);
|
|
161
|
-
--_qm-ease-arrive: cubic-bezier(0, 0, 0.2, 1);
|
|
162
|
-
--_qm-ease-depart: cubic-bezier(0.4, 0, 1, 1);
|
|
163
168
|
|
|
164
169
|
/* ── Geometry ──────────────────────────────────────────────────────────*/
|
|
165
170
|
/* ONE stroke width for every edge the chrome draws: a card's, a control's,
|
|
@@ -171,6 +176,9 @@
|
|
|
171
176
|
against a token. */
|
|
172
177
|
--_qm-border-width: 1px;
|
|
173
178
|
--_qm-radius: var(--qm-radius, 8px);
|
|
179
|
+
/* The INTERIOR corner, and HALF is the relationship rather than the number: a
|
|
180
|
+
corner inside a corner reads as nested only while it is visibly tighter, and
|
|
181
|
+
halving holds that at every setting of the dial, zero included. */
|
|
174
182
|
--_qm-radius-inner: calc(var(--_qm-radius) / 2);
|
|
175
183
|
/* A fully-rounded end cap: a shape tier, not a step on the radius ramp, so
|
|
176
184
|
it does not derive from the dial: a pill stays a pill at any radius. */
|
|
@@ -180,6 +188,13 @@
|
|
|
180
188
|
--_qm-space-2: calc(var(--_qm-space) * 2);
|
|
181
189
|
--_qm-space-3: calc(var(--_qm-space) * 3);
|
|
182
190
|
--_qm-space-4: calc(var(--_qm-space) * 4);
|
|
191
|
+
/* A rung because the two axes are two decisions and the ladder alone cannot hold
|
|
192
|
+
them together: `check:style`'s rhythm axis sees each box read a rung, never that
|
|
193
|
+
they read the SAME one, which is the drift `visual/controls.css` already names
|
|
194
|
+
for the type half. Asymmetric because the vertical is what leading leaves once
|
|
195
|
+
it has taken the line box, and the horizontal is how far in the text starts,
|
|
196
|
+
which nothing else fixes. */
|
|
197
|
+
--_qm-inset-control: var(--_qm-space) var(--_qm-space-3);
|
|
183
198
|
/* The scroll TAIL: dead space under the last card, so it can be read at the
|
|
184
199
|
middle of a pane rather than at its bottom edge. A fraction of the VIEWPORT
|
|
185
200
|
rather than a step on the ramp, because what it answers is how far the eye
|
|
@@ -203,26 +218,36 @@
|
|
|
203
218
|
--_qm-glyph-control: 14px;
|
|
204
219
|
|
|
205
220
|
/* ── Type ────────────────────────────────────────────────────────────────
|
|
206
|
-
`--qm-font-size` anchors the
|
|
207
|
-
rungs: a step up (title) and
|
|
208
|
-
|
|
209
|
-
ratio, and a dial nobody turns is a name every reader still holds.
|
|
221
|
+
`--qm-font-size` anchors the CHROME rung; 1.125 is the ratio between adjacent
|
|
222
|
+
rungs: a step up (title) and one down (label). The ratio and the
|
|
223
|
+
weight are fixed conventions, not dials: no consumer tunes a typographic
|
|
224
|
+
ratio, and a dial nobody turns is a name every reader still holds. THREE chrome
|
|
225
|
+
rungs: a second step below `label` lands near 11px, under a size anything here
|
|
226
|
+
wants to be read at, and the supporting text that would take it is what `label`
|
|
227
|
+
already is. */
|
|
210
228
|
--_qm-text-body: var(--qm-font-size, 0.875rem);
|
|
211
229
|
--_qm-text-title: calc(var(--_qm-text-body) * 1.125);
|
|
212
230
|
--_qm-text-label: calc(var(--_qm-text-body) / 1.125);
|
|
213
|
-
|
|
231
|
+
/* PAPER: the document body answers to the preview's paper where every other
|
|
232
|
+
surface here answers to the control beside it, so a rung shared with the chrome
|
|
233
|
+
makes a consumer asking for larger prose take larger inputs with it. 16/15 where
|
|
234
|
+
the ramp steps at 9/8: small enough to read as a change of REGISTER rather than
|
|
235
|
+
a rung of hierarchy, and clear of the title rung, which one 9/8 step lands on
|
|
236
|
+
exactly. */
|
|
237
|
+
--_qm-text-paper: calc(var(--_qm-text-body) * 16 / 15);
|
|
238
|
+
/* ONE weight over the regular: every heading here is a name over the value under
|
|
239
|
+
it, so a second is a rung with no register beneath it. Hierarchy is the size
|
|
240
|
+
ramp's. */
|
|
214
241
|
--_qm-weight-label: 600;
|
|
215
|
-
--_qm-weight-soft: 500;
|
|
216
242
|
/* Leading: the ramp's third axis, and it does not track the sizes one for one:
|
|
217
243
|
two rungs, because a wrapped label and a wrapped paragraph want different
|
|
218
244
|
rhythms and nothing wants four. `body` is reading rhythm (both prose leaves,
|
|
219
245
|
every control beside them, the tips card's guidance); `tight` is a line that is
|
|
220
246
|
a label rather than a passage. Unitless, so a rung inherits MULTIPLICATIVELY
|
|
221
247
|
and holds against whichever size rung the surface reads: a `rem` leading
|
|
222
|
-
would freeze at one size and disagree with the rest of the ramp.
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
size rung over it. `line-height: 1` is outside the axis: it collapses the
|
|
248
|
+
would freeze at one size and disagree with the rest of the ramp. It is the
|
|
249
|
+
axis paper and chrome SHARE: both read `body`, and the size rung is what
|
|
250
|
+
separates them. `line-height: 1` is outside the axis: it collapses the
|
|
226
251
|
line box onto its content, a glyph or a button's one-line label. */
|
|
227
252
|
--_qm-leading-body: 1.5;
|
|
228
253
|
--_qm-leading-tight: 1.3;
|
package/dist/preset/recipes.css
CHANGED
|
@@ -86,11 +86,7 @@ html:has(.qm-workspace) {
|
|
|
86
86
|
The GAP is the endorsed default and not a fact about splitting: two surfaces standing
|
|
87
87
|
apart on a page read as two things, which is what a host showing its instruments wants.
|
|
88
88
|
A host that spends the whole screen on the mounts closes it to nothing and carries a
|
|
89
|
-
hairline on one pane instead: one property, and the tracks are untouched.
|
|
90
|
-
|
|
91
|
-
Below the width that fits two mounts abreast the split stops being one: the tracks
|
|
92
|
-
stack and each takes a pane's height. WHICH element scrolls then is the host's, since
|
|
93
|
-
it is the host that knows where the room comes from. */
|
|
89
|
+
hairline on one pane instead: one property, and the tracks are untouched. */
|
|
94
90
|
.qm-split {
|
|
95
91
|
display: grid;
|
|
96
92
|
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
|
@@ -99,11 +95,62 @@ html:has(.qm-workspace) {
|
|
|
99
95
|
min-height: 0;
|
|
100
96
|
}
|
|
101
97
|
|
|
102
|
-
|
|
98
|
+
/* THE BAND THAT SWITCHES THEM, and nothing at the widths where both tracks are showing
|
|
99
|
+
and there is nothing to switch. Where it stands is the host's, like any other band; what
|
|
100
|
+
goes in it is a pair of `.qm-control`s, which already carry the on-state a pressed
|
|
101
|
+
control takes. Its visibility is the PRESET's because the threshold is: a host hiding
|
|
102
|
+
its own tab row writes this file's number a second time, which is the one thing the
|
|
103
|
+
single source is for. */
|
|
104
|
+
.qm-switch {
|
|
105
|
+
display: none;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/* ONE MOUNT AT A TIME under the threshold, rather than two of them abreast. Half a narrow
|
|
109
|
+
viewport is under the width either surface reads at, so the split stops being one and
|
|
110
|
+
the tracks become views of the same room: the host says which with `data-qm-show`, and
|
|
111
|
+
the attribute names the TRACK rather than what is in it, so nothing here knows an editor
|
|
112
|
+
from a preview. No attribute is the first track.
|
|
113
|
+
|
|
114
|
+
The other track is `display: none` and not a clip, which is the whole of the hiding: it
|
|
115
|
+
leaves the tab order, hit-testing and the accessibility tree in one property, so the
|
|
116
|
+
pane needs no `inert` and the host needs no breakpoint in JS. Both stay MOUNTED (a
|
|
117
|
+
preview binds its handle once, an editor holds a caret, a history and a scroll), so
|
|
118
|
+
the switch is presentational, never conditional. A pane hidden this way keeps the
|
|
119
|
+
pixels it had (the paint loop's band sweep no-ops on an empty visible set) and repaints
|
|
120
|
+
from its live box when it comes back, which its resize observer sees as a width moving
|
|
121
|
+
off zero.
|
|
122
|
+
|
|
123
|
+
HEIGHT is the second clause because a landscape phone passes the first one: it clears
|
|
124
|
+
the width with room for two tracks and has no body left to put them in. A desktop window
|
|
125
|
+
that short wants one mount for the same reason. */
|
|
126
|
+
@media (width < 50rem), (height < 30rem) {
|
|
127
|
+
.qm-switch {
|
|
128
|
+
display: flex;
|
|
129
|
+
gap: var(--qmh-space);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/* Even halves of the band, at the height a THUMB needs. The floor is the recipe's and
|
|
133
|
+
not the host's, because the band exists only at the sizes where the pointer is a
|
|
134
|
+
finger: a host is free to stand the band deeper, and has nothing to gain from
|
|
135
|
+
standing it shallower than the thing it is tapping. */
|
|
136
|
+
.qm-switch > * {
|
|
137
|
+
flex: 1 1 0;
|
|
138
|
+
min-height: var(--qmh-tap);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/* Centred, which is what a control filling half a band reads as; `.qm-control` itself
|
|
142
|
+
reads left, for the menus it also dresses. */
|
|
143
|
+
.qm-switch > .qm-control {
|
|
144
|
+
text-align: center;
|
|
145
|
+
}
|
|
146
|
+
|
|
103
147
|
.qm-split {
|
|
104
148
|
grid-template-columns: minmax(0, 1fr);
|
|
105
|
-
|
|
106
|
-
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.qm-split:not([data-qm-show='2']) > :nth-child(2),
|
|
152
|
+
.qm-split[data-qm-show='2'] > :nth-child(1) {
|
|
153
|
+
display: none;
|
|
107
154
|
}
|
|
108
155
|
}
|
|
109
156
|
|
package/dist/preset/scale.css
CHANGED
|
@@ -58,10 +58,11 @@
|
|
|
58
58
|
so it does not derive from the spacing dial. */
|
|
59
59
|
--qmh-measure: 34rem;
|
|
60
60
|
|
|
61
|
-
/*
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
61
|
+
/* The floor under a control a THUMB lands on. Hand geometry rather than rhythm, which
|
|
62
|
+
is why it does not derive from the spacing dial either: 44px is where both platform
|
|
63
|
+
guidelines and WCAG put a touch target, and a control padded to a pointer's size is
|
|
64
|
+
under it whatever rung the padding reads. */
|
|
65
|
+
--qmh-tap: 2.75rem;
|
|
65
66
|
|
|
66
67
|
/* ── Type ────────────────────────────────────────────────────────────────── */
|
|
67
68
|
/* Two faces with a job each: prose is sans, chrome is mono. Both are system stacks,
|
|
@@ -11,13 +11,13 @@
|
|
|
11
11
|
import { createPreview, type PreviewController } from './controller.js';
|
|
12
12
|
import { guardRebind } from '../core/rebind.svelte.js';
|
|
13
13
|
import type { PreviewStringsInput } from './strings.js';
|
|
14
|
-
import type { LiveSession,
|
|
15
|
-
import type { DocPath, Place } from '../core/address.js';
|
|
14
|
+
import type { LiveSession, ChangeSet } from '@quillmark/wasm';
|
|
15
|
+
import type { DocPath, Landing, Place } from '../core/address.js';
|
|
16
16
|
import type { EditorErrorHandler } from '../core/errors.js';
|
|
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`, `overlays`, `
|
|
20
|
+
* prop it closed over (`session`, `margin`, `overlays`, `onPick`, `onError`,
|
|
21
21
|
* `strings`) is NOT 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.
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
style?: string;
|
|
37
37
|
margin?: number;
|
|
38
38
|
overlays?: boolean;
|
|
39
|
-
|
|
39
|
+
onPick?: (at: Landing) => void;
|
|
40
40
|
/** A page paint the backend refused; the error message state shows either way. */
|
|
41
41
|
onError?: EditorErrorHandler;
|
|
42
42
|
/** The message-state wording, keyed and partial (`PreviewStrings`). */
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
session,
|
|
48
48
|
margin,
|
|
49
49
|
overlays,
|
|
50
|
-
|
|
50
|
+
onPick,
|
|
51
51
|
onError,
|
|
52
52
|
strings,
|
|
53
53
|
class: className,
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
let controller: PreviewController | undefined;
|
|
59
59
|
|
|
60
60
|
guardRebind(
|
|
61
|
-
() => ({ session, margin, overlays,
|
|
61
|
+
() => ({ session, margin, overlays, onPick, onError, strings }),
|
|
62
62
|
'Remount the preview ({#key session}) to rebind.'
|
|
63
63
|
);
|
|
64
64
|
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
container: containerEl,
|
|
69
69
|
margin,
|
|
70
70
|
overlays,
|
|
71
|
-
|
|
71
|
+
onPick,
|
|
72
72
|
onError,
|
|
73
73
|
strings
|
|
74
74
|
});
|
|
@@ -81,8 +81,9 @@
|
|
|
81
81
|
export function refresh(change: ChangeSet): void {
|
|
82
82
|
controller?.refresh(change);
|
|
83
83
|
}
|
|
84
|
-
|
|
85
|
-
|
|
84
|
+
/** `false` when this compile places nothing at `field` ({@link PreviewController}). */
|
|
85
|
+
export function scrollToField(field: DocPath): boolean {
|
|
86
|
+
return controller?.scrollToField(field) ?? false;
|
|
86
87
|
}
|
|
87
88
|
/** Takes the editor's `onCaretMove` payload: `onCaretMove={preview.focusPosition}`. */
|
|
88
89
|
export function focusPosition(at: Place): void {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { PreviewStringsInput } from './strings.js';
|
|
2
|
-
import type { LiveSession,
|
|
3
|
-
import type { DocPath, Place } from '../core/address.js';
|
|
2
|
+
import type { LiveSession, ChangeSet } from '@quillmark/wasm';
|
|
3
|
+
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`, `overlays`, `
|
|
7
|
+
* prop it closed over (`session`, `margin`, `overlays`, `onPick`, `onError`,
|
|
8
8
|
* `strings`) is NOT 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.
|
|
@@ -23,7 +23,7 @@ interface Props {
|
|
|
23
23
|
style?: string;
|
|
24
24
|
margin?: number;
|
|
25
25
|
overlays?: boolean;
|
|
26
|
-
|
|
26
|
+
onPick?: (at: Landing) => void;
|
|
27
27
|
/** A page paint the backend refused; the error message state shows either way. */
|
|
28
28
|
onError?: EditorErrorHandler;
|
|
29
29
|
/** The message-state wording, keyed and partial (`PreviewStrings`). */
|
|
@@ -31,7 +31,7 @@ interface Props {
|
|
|
31
31
|
}
|
|
32
32
|
declare const Preview: import("svelte").Component<Props, {
|
|
33
33
|
refresh: (change: ChangeSet) => void;
|
|
34
|
-
scrollToField: (field: DocPath) =>
|
|
34
|
+
scrollToField: (field: DocPath) => boolean;
|
|
35
35
|
focusPosition: (at: Place) => void;
|
|
36
36
|
setZoom: (scale: number) => void;
|
|
37
37
|
}, "">;
|
package/dist/preview/bridge.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import type { LiveSession
|
|
1
|
+
import type { LiveSession } from '@quillmark/wasm';
|
|
2
|
+
import type { Landing } from '../core/address.js';
|
|
2
3
|
import type { PageSlot } from './paint.js';
|
|
3
4
|
export interface BridgeController {
|
|
4
|
-
/** Scroll `field`'s first
|
|
5
|
-
|
|
5
|
+
/** Scroll `field`'s first box to centre; `false` when this compile places none
|
|
6
|
+
* for that address, which is the whole of what the preview can say about it. */
|
|
7
|
+
scrollToField(field: string): boolean;
|
|
6
8
|
/**
|
|
7
9
|
* Bring the caret rect at `field`/`pos` into view (a no-op off-content, and a
|
|
8
10
|
* no-op when it is already clear of the fold).
|
|
@@ -10,4 +12,4 @@ export interface BridgeController {
|
|
|
10
12
|
focusPosition(field: string, pos: number): void;
|
|
11
13
|
destroy(): void;
|
|
12
14
|
}
|
|
13
|
-
export declare function createBridge(session: LiveSession, container: HTMLElement, slots: readonly PageSlot[],
|
|
15
|
+
export declare function createBridge(session: LiveSession, container: HTMLElement, slots: readonly PageSlot[], onPick: ((at: Landing) => void) | undefined): BridgeController;
|
package/dist/preview/bridge.js
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
|
-
import { rectToPercent, clickToPdfPt, applyPercentRect } from './geometry.js';
|
|
2
|
-
export function createBridge(session, container, slots,
|
|
1
|
+
import { boxesForField, rectToPercent, clickToPdfPt, applyPercentRect } from './geometry.js';
|
|
2
|
+
export function createBridge(session, container, slots, onPick) {
|
|
3
3
|
const unlisten = [];
|
|
4
|
-
|
|
4
|
+
// TWO RUNGS: `positionAt` over span-tracked content, `fieldAt` over every placement
|
|
5
|
+
// the compile tracks, a strict superset of it (PREVIEW.md §"Click bridge" carries
|
|
6
|
+
// the measurement). The second fires where a field is placed without its content
|
|
7
|
+
// being tracked, and the pick carries NO `pos` there: a fabricated `0` would be an
|
|
8
|
+
// invented offset wearing a real one's type. No third rung hit-tests `regions()`,
|
|
9
|
+
// whose rects bound ink the field does not fill.
|
|
10
|
+
if (onPick) {
|
|
5
11
|
for (const { page, el } of slots) {
|
|
6
12
|
const handleClick = (ev) => {
|
|
7
13
|
// Re-read through the live array: a same-count `refresh` swaps the
|
|
@@ -16,7 +22,10 @@ export function createBridge(session, container, slots, onCaretPick) {
|
|
|
16
22
|
const pt = clickToPdfPt(px, py, box.width, box.height, slot.size);
|
|
17
23
|
const hit = session.positionAt(slot.page, pt.x, pt.y);
|
|
18
24
|
if (hit)
|
|
19
|
-
|
|
25
|
+
return void onPick(hit);
|
|
26
|
+
const field = session.fieldAt(slot.page, pt.x, pt.y);
|
|
27
|
+
if (field)
|
|
28
|
+
onPick({ field });
|
|
20
29
|
};
|
|
21
30
|
el.addEventListener('click', handleClick);
|
|
22
31
|
unlisten.push(() => el.removeEventListener('click', handleClick));
|
|
@@ -54,15 +63,16 @@ export function createBridge(session, container, slots, onCaretPick) {
|
|
|
54
63
|
}
|
|
55
64
|
return {
|
|
56
65
|
scrollToField(field) {
|
|
57
|
-
const box = session.fieldBoxes(field)[0];
|
|
66
|
+
const box = boxesForField(field, session.fieldBoxes(field), session.regions())[0];
|
|
58
67
|
if (!box)
|
|
59
|
-
return;
|
|
68
|
+
return false;
|
|
60
69
|
const slot = slots[box.page];
|
|
61
70
|
if (!slot)
|
|
62
|
-
return;
|
|
71
|
+
return false;
|
|
63
72
|
// A discrete act ("show me this field"), so it centres every time: the
|
|
64
73
|
// asymmetry the bloom draws between a continuous signal and a one-shot one.
|
|
65
74
|
centre(measure(slot, rectToPercent(box.rect, slot.size)));
|
|
75
|
+
return true;
|
|
66
76
|
},
|
|
67
77
|
focusPosition(field, pos) {
|
|
68
78
|
const region = session.locate(field, pos);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { LiveSession, ChangeSet
|
|
2
|
-
import type { DocPath, Place } from '../core/address.js';
|
|
1
|
+
import type { LiveSession, ChangeSet } from '@quillmark/wasm';
|
|
2
|
+
import type { DocPath, Landing, Place } from '../core/address.js';
|
|
3
3
|
import { type EditorErrorHandler } from '../core/errors.js';
|
|
4
4
|
import { type PreviewStringsInput } from './strings.js';
|
|
5
5
|
export interface PreviewOptions {
|
|
@@ -9,8 +9,10 @@ export interface PreviewOptions {
|
|
|
9
9
|
margin?: number;
|
|
10
10
|
/** Draw field-box overlays. Default true. */
|
|
11
11
|
overlays?: boolean;
|
|
12
|
-
/** A click resolved to
|
|
13
|
-
|
|
12
|
+
/** A click resolved to an address ({@link Landing}): a caret where the compile
|
|
13
|
+
* tracks the content under the point, the field alone where it tracks only the
|
|
14
|
+
* placement. The hook does not fire where the compile tracks neither. */
|
|
15
|
+
onPick?(at: Landing): void;
|
|
14
16
|
/** A page paint the backend refused ({@link EditorErrorHandler}). The preview
|
|
15
17
|
* shows its error message state either way; this routes it to an app's sink. */
|
|
16
18
|
onError?: EditorErrorHandler;
|
|
@@ -21,8 +23,13 @@ export interface PreviewOptions {
|
|
|
21
23
|
export interface PreviewController {
|
|
22
24
|
/** Repaint `dirtyPages ∩ visible` and re-read geometry; the only apply-driven hop. */
|
|
23
25
|
refresh(change: ChangeSet): void;
|
|
24
|
-
/**
|
|
25
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Scroll `field`'s first box into view and bloom it. `false` when this compile
|
|
28
|
+
* places nothing at that address — an answer, not a failure: the plate places
|
|
29
|
+
* plenty it does not track, and the preview carries no schema to tell that from a
|
|
30
|
+
* field the host misnamed. The editor's `focusField` is what distinguishes them.
|
|
31
|
+
*/
|
|
32
|
+
scrollToField(field: DocPath): boolean;
|
|
26
33
|
/**
|
|
27
34
|
* Bring the caret at `at` into view and bloom its field on arrival. Both halves
|
|
28
35
|
* are change-guarded: the pane moves only when the caret has left the fold, the
|
|
@@ -59,7 +59,7 @@ export function createPreview(session, opts) {
|
|
|
59
59
|
// swapped on reconcile, and both attach DOM/listeners per slot.
|
|
60
60
|
function attach() {
|
|
61
61
|
overlay = overlaysEnabled ? createOverlay(session, paintLoop.slots) : undefined;
|
|
62
|
-
bridge = createBridge(session, container, paintLoop.slots, opts.
|
|
62
|
+
bridge = createBridge(session, container, paintLoop.slots, opts.onPick);
|
|
63
63
|
}
|
|
64
64
|
function detach() {
|
|
65
65
|
overlay?.destroy();
|
|
@@ -120,8 +120,11 @@ export function createPreview(session, opts) {
|
|
|
120
120
|
bridge?.focusPosition(followed.field, followed.pos);
|
|
121
121
|
},
|
|
122
122
|
scrollToField(field) {
|
|
123
|
-
bridge?.scrollToField(field);
|
|
123
|
+
const found = bridge?.scrollToField(field) ?? false;
|
|
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`).
|
|
124
126
|
overlay?.flashField(field);
|
|
127
|
+
return found;
|
|
125
128
|
},
|
|
126
129
|
focusPosition(at) {
|
|
127
130
|
followed = at;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { PageSize } from '@quillmark/wasm';
|
|
1
|
+
import type { FieldRegion, PageSize } from '@quillmark/wasm';
|
|
2
2
|
/** A page rect in PDF points, `[x0, y0, x1, y1]`, bottom-left origin (`FieldRegion.rect`'s shape). */
|
|
3
3
|
export type PdfRect = readonly [number, number, number, number];
|
|
4
4
|
/** A rect as CSS percent of the page box (top-left origin; Y is flipped from PDF-pt). */
|
|
@@ -13,6 +13,21 @@ export interface PdfPoint {
|
|
|
13
13
|
x: number;
|
|
14
14
|
y: number;
|
|
15
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* An address's boxes: `fieldBoxes`'s union, else its own `regions()` rects, else the
|
|
18
|
+
* rects of everything UNDER it. The one rule the overlay and the scroll both read, so
|
|
19
|
+
* an address `regions()` names never draws nothing (PREVIEW.md §"Three
|
|
20
|
+
* responsibilities").
|
|
21
|
+
*
|
|
22
|
+
* `fieldBoxes` answers `[]` for a scalar reference the plate places without tracking
|
|
23
|
+
* its content and for a `richtext[]` element; runtime.d.ts says what such a field's
|
|
24
|
+
* box is, a single `regions()` rect. The third rung is that sentence one granularity
|
|
25
|
+
* down — an array's ink is its elements' — and is a fallback, not a union, so an
|
|
26
|
+
* address with rects of its own never also draws its children's. The `.` keeps the
|
|
27
|
+
* prefix a path boundary: `main.references` matches `main.references.0`, not
|
|
28
|
+
* `main.references_note`.
|
|
29
|
+
*/
|
|
30
|
+
export declare function boxesForField(field: string, boxes: readonly FieldRegion[], regions: readonly FieldRegion[]): readonly FieldRegion[];
|
|
16
31
|
/**
|
|
17
32
|
* Forward: a PDF-pt rect -> CSS % of the page box. Percent positioning tracks
|
|
18
33
|
* the page's displayed size across DPR and container resize for free; this is
|
package/dist/preview/geometry.js
CHANGED
|
@@ -1,3 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An address's boxes: `fieldBoxes`'s union, else its own `regions()` rects, else the
|
|
3
|
+
* rects of everything UNDER it. The one rule the overlay and the scroll both read, so
|
|
4
|
+
* an address `regions()` names never draws nothing (PREVIEW.md §"Three
|
|
5
|
+
* responsibilities").
|
|
6
|
+
*
|
|
7
|
+
* `fieldBoxes` answers `[]` for a scalar reference the plate places without tracking
|
|
8
|
+
* its content and for a `richtext[]` element; runtime.d.ts says what such a field's
|
|
9
|
+
* box is, a single `regions()` rect. The third rung is that sentence one granularity
|
|
10
|
+
* down — an array's ink is its elements' — and is a fallback, not a union, so an
|
|
11
|
+
* address with rects of its own never also draws its children's. The `.` keeps the
|
|
12
|
+
* prefix a path boundary: `main.references` matches `main.references.0`, not
|
|
13
|
+
* `main.references_note`.
|
|
14
|
+
*/
|
|
15
|
+
export function boxesForField(field, boxes, regions) {
|
|
16
|
+
if (boxes.length)
|
|
17
|
+
return boxes;
|
|
18
|
+
const own = regions.filter((r) => r.field === field);
|
|
19
|
+
return own.length ? own : regions.filter((r) => r.field.startsWith(`${field}.`));
|
|
20
|
+
}
|
|
1
21
|
/**
|
|
2
22
|
* Forward: a PDF-pt rect -> CSS % of the page box. Percent positioning tracks
|
|
3
23
|
* the page's displayed size across DPR and container resize for free; this is
|
package/dist/preview/index.d.ts
CHANGED