@quillmark/svelte 0.27.0 → 0.28.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/NOTICE +4 -4
- package/README.md +26 -8
- package/THEMING.md +10 -8
- package/dist/core/address.d.ts +35 -64
- package/dist/core/address.js +35 -55
- package/dist/core/bloom.d.ts +4 -10
- package/dist/core/bloom.js +14 -27
- package/dist/core/codec/blocks.d.ts +41 -43
- package/dist/core/codec/blocks.js +97 -130
- package/dist/core/codec/code.d.ts +7 -10
- package/dist/core/codec/code.js +37 -46
- package/dist/core/codec/decode.d.ts +19 -23
- package/dist/core/codec/decode.js +76 -111
- package/dist/core/codec/encode.d.ts +11 -15
- package/dist/core/codec/encode.js +60 -67
- package/dist/core/codec/field.d.ts +62 -95
- package/dist/core/codec/field.js +107 -179
- package/dist/core/codec/islands.d.ts +33 -41
- package/dist/core/codec/islands.js +76 -86
- package/dist/core/codec/lists.d.ts +17 -26
- package/dist/core/codec/lists.js +117 -157
- package/dist/core/codec/marks.d.ts +4 -3
- package/dist/core/codec/marks.js +8 -3
- package/dist/core/codec/prose.css +84 -46
- package/dist/core/codec/schema.d.ts +18 -23
- package/dist/core/codec/schema.js +84 -76
- package/dist/core/codec/slash.d.ts +42 -48
- package/dist/core/codec/slash.js +0 -0
- package/dist/core/codec/table-copy.d.ts +4 -3
- package/dist/core/codec/table-copy.js +25 -7
- package/dist/core/codec/table-view.d.ts +12 -12
- package/dist/core/codec/table-view.js +460 -383
- package/dist/core/codec/table.d.ts +94 -61
- package/dist/core/codec/table.js +331 -83
- package/dist/core/codec/tasks.d.ts +6 -0
- package/dist/core/codec/tasks.js +30 -3
- package/dist/core/errors.d.ts +15 -30
- package/dist/core/errors.js +2 -6
- package/dist/core/index.js +7 -12
- package/dist/core/lifecycle.d.ts +7 -16
- package/dist/core/lifecycle.js +10 -21
- package/dist/core/rebind.svelte.d.ts +10 -20
- package/dist/core/rebind.svelte.js +15 -27
- package/dist/core/rungs.d.ts +14 -11
- package/dist/core/rungs.js +17 -19
- package/dist/core/teardown.d.ts +4 -6
- package/dist/core/teardown.js +7 -10
- package/dist/core/theme.css +74 -54
- package/dist/index.js +3 -8
- package/dist/preset/app.css +3 -1
- package/dist/preset/index.css +6 -8
- package/dist/preset/recipes.css +12 -15
- package/dist/preset/scale.css +18 -17
- package/dist/preset/shell.css +10 -0
- package/dist/preview/bridge.d.ts +18 -22
- package/dist/preview/bridge.js +29 -58
- package/dist/preview/controller.d.ts +21 -29
- package/dist/preview/controller.js +14 -39
- package/dist/preview/geometry.d.ts +13 -34
- package/dist/preview/geometry.js +13 -34
- package/dist/preview/index.js +4 -16
- package/dist/preview/paint.d.ts +3 -3
- package/dist/preview/paint.js +28 -68
- package/dist/preview/strings.d.ts +1 -1
- package/dist/preview/strings.js +2 -14
- package/dist/preview/zoom.d.ts +18 -23
- package/dist/preview/zoom.js +28 -27
- package/dist/visual/ArrayField.svelte +77 -16
- package/dist/visual/BooleanField.svelte +27 -14
- package/dist/visual/Card.svelte +61 -48
- package/dist/visual/CardControls.svelte +5 -11
- package/dist/visual/DateField.svelte +20 -4
- package/dist/visual/EnumField.svelte +3 -0
- package/dist/visual/FieldHint.svelte +2 -1
- package/dist/visual/FormatPopover.svelte +82 -15
- package/dist/visual/FormatPopover.svelte.d.ts +4 -0
- package/dist/visual/MatrixAdd.svelte +30 -12
- package/dist/visual/MatrixField.svelte +24 -16
- package/dist/visual/NumberField.svelte +7 -0
- package/dist/visual/ObjectField.svelte +4 -4
- package/dist/visual/ProseField.svelte +0 -5
- package/dist/visual/TipsCard.svelte +11 -10
- package/dist/visual/VariantField.svelte +3 -1
- package/dist/visual/VisualEditorInner.svelte +19 -3
- package/dist/visual/controls.css +7 -2
- package/dist/visual/fonts.d.ts +18 -0
- package/dist/visual/fonts.js +59 -0
- package/dist/visual/icons/nodes.d.ts +0 -5
- package/dist/visual/icons/nodes.js +0 -4
- package/dist/visual/props.d.ts +14 -1
- package/dist/visual/strings.d.ts +7 -0
- package/dist/visual/strings.js +5 -2
- package/dist/visual/structure.d.ts +43 -87
- package/dist/visual/structure.js +42 -83
- package/package.json +2 -2
|
@@ -7,15 +7,18 @@ export interface PreviewOptions {
|
|
|
7
7
|
container: HTMLElement;
|
|
8
8
|
/** Pages kept painted beyond the visible band. Default 1. */
|
|
9
9
|
margin?: number;
|
|
10
|
-
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
/**
|
|
11
|
+
* A click resolved to an address ({@link Landing}): a caret where the compile tracks the
|
|
12
|
+
* content under the point, the field alone where it tracks only the placement. Does not fire
|
|
13
|
+
* where it tracks neither.
|
|
14
|
+
*/
|
|
13
15
|
onPick?(at: Landing): void;
|
|
14
|
-
/**
|
|
15
|
-
*
|
|
16
|
+
/**
|
|
17
|
+
* A page paint the backend refused ({@link EditorErrorHandler}); the error message state shows
|
|
18
|
+
* either way.
|
|
19
|
+
*/
|
|
16
20
|
onError?: EditorErrorHandler;
|
|
17
|
-
/** The surface's strings
|
|
18
|
-
* English. */
|
|
21
|
+
/** The surface's strings; unset keys take the package's English. */
|
|
19
22
|
strings?: PreviewStringsInput;
|
|
20
23
|
}
|
|
21
24
|
export interface PreviewController {
|
|
@@ -23,37 +26,26 @@ export interface PreviewController {
|
|
|
23
26
|
* apply-driven hop. */
|
|
24
27
|
refresh(change: ChangeSet): void;
|
|
25
28
|
/**
|
|
26
|
-
* Scroll `field`'s first box into view, and end any follow it outranks. `false`
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* that from a field the host misnamed. The editor's `focusField` is what
|
|
30
|
-
* distinguishes them.
|
|
29
|
+
* Scroll `field`'s first box into view, and end any follow it outranks. `false` when this
|
|
30
|
+
* compile places nothing at that address; the preview has no schema to tell that from a
|
|
31
|
+
* misnamed field.
|
|
31
32
|
*/
|
|
32
33
|
scrollToField(field: DocPath): boolean;
|
|
33
34
|
/**
|
|
34
|
-
* Bring the caret at `at` into view
|
|
35
|
-
* the
|
|
36
|
-
*
|
|
37
|
-
* Takes the editor's own `onCaretMove` payload, so the editor→preview half of
|
|
38
|
-
* the bridge is `onCaretMove={preview.focusPosition}` and translates nothing. A
|
|
39
|
-
* `ContentHit` carries both members and fits here too.
|
|
35
|
+
* Bring the caret at `at` into view; the pane moves only when the caret has left the fold.
|
|
36
|
+
* Takes the editor's `onCaretMove` payload, so `onCaretMove={preview.focusPosition}` wires it.
|
|
40
37
|
*/
|
|
41
38
|
focusPosition(at: Place): void;
|
|
42
39
|
/**
|
|
43
|
-
* End the follow: the pane stays where the user left it until the next
|
|
44
|
-
* `
|
|
45
|
-
*
|
|
46
|
-
* The editor's `onActiveLeafChange` is the signal, firing for a form control as for
|
|
47
|
-
* a prose leaf: `onActiveLeafChange={preview.endFollow}`. A control has no caret
|
|
48
|
-
* coordinate to report, so unwired the follow keeps naming the leaf the focus left
|
|
49
|
-
* and every recompile pulls the pane back there. A prose leaf restarts it with its
|
|
50
|
-
* own next caret.
|
|
40
|
+
* End the follow: the pane stays where the user left it until the next `focusPosition`. Wire
|
|
41
|
+
* `onActiveLeafChange={preview.endFollow}`; a form control reports no caret, so unwired the
|
|
42
|
+
* follow keeps pulling the pane back to the leaf the focus left.
|
|
51
43
|
*/
|
|
52
44
|
endFollow(): void;
|
|
53
45
|
/**
|
|
54
|
-
* Draw every page at `zoom`, a fraction of actual size (`1` is 100%), held between
|
|
55
|
-
*
|
|
56
|
-
*
|
|
46
|
+
* Draw every page at `zoom`, a fraction of actual size (`1` is 100%), held between the pane's
|
|
47
|
+
* floor and 200%; `undefined` is the default, the widest page fitting the pane's width up to
|
|
48
|
+
* 125%.
|
|
57
49
|
*/
|
|
58
50
|
setZoom(zoom: number | undefined): void;
|
|
59
51
|
destroy(): void;
|
|
@@ -4,8 +4,6 @@ import { createBridge } from './bridge.js';
|
|
|
4
4
|
import { createViewport } from './viewport.js';
|
|
5
5
|
import { mergePreviewStrings } from './strings.js';
|
|
6
6
|
const CONTAINER_CLASS = 'qm-preview';
|
|
7
|
-
// The message states share one element; each carries `MESSAGE_CLASS` plus a state
|
|
8
|
-
// class, the hooks a consumer and the tests target.
|
|
9
7
|
const MESSAGE_CLASS = 'qm-preview-message';
|
|
10
8
|
const EMPTY_CLASS = 'qm-preview-empty';
|
|
11
9
|
const ERROR_CLASS = 'qm-preview-error';
|
|
@@ -14,8 +12,7 @@ export function createPreview(session, opts) {
|
|
|
14
12
|
const margin = opts.margin ?? 1;
|
|
15
13
|
const t = mergePreviewStrings(opts.strings);
|
|
16
14
|
container.classList.add(CONTAINER_CLASS);
|
|
17
|
-
// One element, restamped: the
|
|
18
|
-
// threw) are mutually exclusive.
|
|
15
|
+
// One element, restamped: the message states are mutually exclusive.
|
|
19
16
|
let message;
|
|
20
17
|
function showMessage(text, state) {
|
|
21
18
|
if (!message) {
|
|
@@ -29,12 +26,8 @@ export function createPreview(session, opts) {
|
|
|
29
26
|
message?.remove();
|
|
30
27
|
message = undefined;
|
|
31
28
|
}
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
// there. A paint that unexpectedly throws is caught per-slot and surfaced
|
|
35
|
-
// through the shared message rather than aborting the observer callback
|
|
36
|
-
// mid-sweep (a paint is the one surface that can still refuse a counted page;
|
|
37
|
-
// brittle to leave uncaught).
|
|
29
|
+
// A paint that throws is caught per slot and surfaced through the message rather than aborting
|
|
30
|
+
// the observer callback mid-sweep.
|
|
38
31
|
const paintLoop = createPaintLoop(session, container, margin, (page, err) => {
|
|
39
32
|
reportError(opts.onError, {
|
|
40
33
|
code: 'paint-failed',
|
|
@@ -45,14 +38,10 @@ export function createPreview(session, opts) {
|
|
|
45
38
|
});
|
|
46
39
|
showMessage(t.renderFailed, ERROR_CLASS);
|
|
47
40
|
});
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
// slots array is a stable identity but its members are swapped on reconcile, and
|
|
51
|
-
// the listeners are per slot.
|
|
41
|
+
// Rebuilt whenever the page count changes: the listeners are per slot and slots are swapped on
|
|
42
|
+
// reconcile.
|
|
52
43
|
let bridge;
|
|
53
|
-
// Outlives
|
|
54
|
-
// follow is re-asserted after a recompile, and a `scrollToField` the pane could not
|
|
55
|
-
// run has nothing else that would ask again for it.
|
|
44
|
+
// Outlives the bridge rebuild, so a trip held for a pane with no box crosses it.
|
|
56
45
|
const held = {};
|
|
57
46
|
function attach() {
|
|
58
47
|
bridge = createBridge(session, container, paintLoop.slots, opts.onPick, held);
|
|
@@ -61,9 +50,6 @@ export function createPreview(session, opts) {
|
|
|
61
50
|
bridge?.destroy();
|
|
62
51
|
bridge = undefined;
|
|
63
52
|
}
|
|
64
|
-
// Called at construction and after every `apply`, so a 0-page compile that later
|
|
65
|
-
// gains pages recovers. A counted page is the whole condition: every backend
|
|
66
|
-
// paints, and `paint`/`pageSize` refuse only a page the count excludes.
|
|
67
53
|
function render(pageCount, dirtyPages) {
|
|
68
54
|
if (pageCount === 0) {
|
|
69
55
|
paintLoop.refresh([], 0);
|
|
@@ -71,46 +57,35 @@ export function createPreview(session, opts) {
|
|
|
71
57
|
showMessage(t.noPages, EMPTY_CLASS);
|
|
72
58
|
return;
|
|
73
59
|
}
|
|
74
|
-
//
|
|
75
|
-
// clear any prior message before painting so a paint that throws mid-refresh
|
|
76
|
-
// leaves its error surfaced rather than have `hideMessage` clobber it.
|
|
60
|
+
// Clear the message before painting so an error from this paint is not clobbered.
|
|
77
61
|
const countChanged = pageCount !== paintLoop.slots.length;
|
|
78
62
|
hideMessage();
|
|
79
63
|
paintLoop.refresh(dirtyPages, pageCount);
|
|
80
|
-
// The slot set moved under the bridge's feet, or we are leaving a message state.
|
|
81
64
|
if (countChanged || !bridge) {
|
|
82
65
|
detach();
|
|
83
66
|
attach();
|
|
84
67
|
}
|
|
85
68
|
}
|
|
86
|
-
// Built before the first bridge
|
|
87
|
-
//
|
|
69
|
+
// Built before the first bridge so its ResizeObserver fires first: a re-shown pane re-fits
|
|
70
|
+
// before the bridge runs a held trip.
|
|
88
71
|
const viewport = createViewport(container, paintLoop.slots, paintLoop.setScale, t.resetZoom);
|
|
89
72
|
render(session.pageCount, []);
|
|
90
73
|
viewport.update();
|
|
91
|
-
// The last
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
// layout's content is off-content for the whole burst: `focusPosition` no-ops and
|
|
95
|
-
// the pane sits still, and a caret event is otherwise the only thing that asks
|
|
96
|
-
// again. The re-locate is the preview's, not the consumer's: the staleness sits
|
|
97
|
-
// between two session queries this module owns both ends of.
|
|
74
|
+
// The last caret the editor reported, re-located after every recompile: `session.locate`
|
|
75
|
+
// answers against the last compiled layout, so a caret typed past it no-ops until the recompile
|
|
76
|
+
// lands.
|
|
98
77
|
let followed;
|
|
99
78
|
return {
|
|
100
79
|
refresh(change) {
|
|
101
80
|
render(change.pageCount, change.dirtyPages);
|
|
102
|
-
// The default fits the widest page, which a recompile can move.
|
|
103
81
|
viewport.update();
|
|
104
|
-
// Guarded like any other caret hop, so a caret already clear of the fold
|
|
105
|
-
// leaves a pane the user scrolled where they left it.
|
|
106
82
|
if (followed)
|
|
107
83
|
bridge?.focusPosition(followed.field, followed.pos);
|
|
108
84
|
},
|
|
109
85
|
scrollToField(field) {
|
|
110
86
|
const placed = bridge?.scrollToField(field) ?? false;
|
|
111
|
-
// The
|
|
112
|
-
// recompile
|
|
113
|
-
// for. A hop that placed nothing moved nothing, so the follow stands.
|
|
87
|
+
// The hop outranks the follow, which would otherwise pull the pane back at the next
|
|
88
|
+
// recompile.
|
|
114
89
|
if (placed)
|
|
115
90
|
followed = undefined;
|
|
116
91
|
return placed;
|
|
@@ -14,50 +14,29 @@ export interface PdfPoint {
|
|
|
14
14
|
y: number;
|
|
15
15
|
}
|
|
16
16
|
/**
|
|
17
|
-
* An address's boxes: `fieldBoxes`'s union, else every `regions()` rect at or under it
|
|
18
|
-
*
|
|
19
|
-
*
|
|
17
|
+
* An address's boxes: `fieldBoxes`'s union, else every `regions()` rect at or under it
|
|
18
|
+
* (PREVIEW.md §"Two responsibilities"). `fieldBoxes` answers `[]` for a scalar reference
|
|
19
|
+
* the plate places without tracking its content and for an array itself; the fallback
|
|
20
|
+
* takes the address and its descendants, matched at a path boundary (`.` or `[`):
|
|
21
|
+
* `main.keywords` matches `main.keywords[0]`, not `main.keywords_note`.
|
|
20
22
|
*
|
|
21
|
-
* `
|
|
22
|
-
* its content and for an array itself; runtime.d.ts says what such a field's box is, a
|
|
23
|
-
* single `regions()` rect. The second rung takes an address and its descendants
|
|
24
|
-
* together, since a container holds both: a plate composing an array's presentation
|
|
25
|
-
* regions the array on the ink it composed — a separator, a heading — where each
|
|
26
|
-
* element regions the ink it carries itself, and the field is placed at all of it. The
|
|
27
|
-
* boundary character (`.` for a nested field, `[` for an array element) keeps the
|
|
28
|
-
* prefix a path boundary: `main.keywords` matches `main.keywords[0]`, not
|
|
29
|
-
* `main.keywords_note`.
|
|
30
|
-
*
|
|
31
|
-
* `regions` is a thunk because the first rung answers without it, and taking it
|
|
32
|
-
* marshals every placement in the compile across the WASM boundary.
|
|
23
|
+
* `regions` is a thunk because taking it marshals every placement across the WASM boundary.
|
|
33
24
|
*/
|
|
34
25
|
export declare function boxesForField(field: string, boxes: readonly FieldRegion[], regions: () => readonly FieldRegion[]): readonly FieldRegion[];
|
|
35
26
|
/**
|
|
36
|
-
* Forward: a PDF-pt rect -> CSS % of the page box
|
|
37
|
-
*
|
|
38
|
-
* scroll's measurement needs no zoom or DPR term of its own (bridge.ts).
|
|
27
|
+
* Forward: a PDF-pt rect -> CSS % of the page box, which tracks the displayed size without a zoom
|
|
28
|
+
* or DPR term.
|
|
39
29
|
*/
|
|
40
30
|
export declare function rectToPercent(rect: PdfRect, pageSize: PageSize): PercentRect;
|
|
41
|
-
/**
|
|
42
|
-
* Position `el` absolutely at `pct` (the % rect this module produces): the one
|
|
43
|
-
* `PercentRect` → `element.style` write, so the positioning convention lives beside
|
|
44
|
-
* the transform that produces it rather than at the caller placing a marker.
|
|
45
|
-
*/
|
|
31
|
+
/** Position `el` absolutely at `pct`. */
|
|
46
32
|
export declare function applyPercentRect(el: HTMLElement, pct: PercentRect): void;
|
|
47
33
|
/**
|
|
48
|
-
* A CSS-px length on a page element of CSS width `cssW` ->
|
|
49
|
-
*
|
|
50
|
-
* by width under a locked aspect ratio, so {@link clickToPdfPt}'s two ratios are one
|
|
51
|
-
* number and a length has one to convert through. What the click bridge reads its
|
|
52
|
-
* pointer slack in points through.
|
|
34
|
+
* A CSS-px length on a page element of CSS width `cssW` -> PDF-pt. Width alone, since `paint.ts`
|
|
35
|
+
* locks the aspect ratio.
|
|
53
36
|
*/
|
|
54
37
|
export declare function pxToPt(px: number, cssW: number, pageSize: PageSize): number;
|
|
55
38
|
/**
|
|
56
|
-
* Inverse: a click at CSS px `(px, py)` within a page element of CSS size
|
|
57
|
-
* `(cssW, cssH)` -> PDF-pt.
|
|
58
|
-
* fraction `rectToPercent` would place a point's box corner at is the same
|
|
59
|
-
* fraction this reads back from `px/cssW`, `py/cssH`; DPR-independent, since
|
|
60
|
-
* the page element's CSS size (not its canvas backing store) is the only input.
|
|
61
|
-
* This is what the click bridge feeds `session.positionAt`/`fieldAt` (bridge.ts).
|
|
39
|
+
* Inverse of {@link rectToPercent}: a click at CSS px `(px, py)` within a page element of CSS size
|
|
40
|
+
* `(cssW, cssH)` -> PDF-pt.
|
|
62
41
|
*/
|
|
63
42
|
export declare function clickToPdfPt(px: number, py: number, cssW: number, cssH: number, pageSize: PageSize): PdfPoint;
|
package/dist/preview/geometry.js
CHANGED
|
@@ -1,20 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* An address's boxes: `fieldBoxes`'s union, else every `regions()` rect at or under it
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* An address's boxes: `fieldBoxes`'s union, else every `regions()` rect at or under it
|
|
3
|
+
* (PREVIEW.md §"Two responsibilities"). `fieldBoxes` answers `[]` for a scalar reference
|
|
4
|
+
* the plate places without tracking its content and for an array itself; the fallback
|
|
5
|
+
* takes the address and its descendants, matched at a path boundary (`.` or `[`):
|
|
6
|
+
* `main.keywords` matches `main.keywords[0]`, not `main.keywords_note`.
|
|
5
7
|
*
|
|
6
|
-
* `
|
|
7
|
-
* its content and for an array itself; runtime.d.ts says what such a field's box is, a
|
|
8
|
-
* single `regions()` rect. The second rung takes an address and its descendants
|
|
9
|
-
* together, since a container holds both: a plate composing an array's presentation
|
|
10
|
-
* regions the array on the ink it composed — a separator, a heading — where each
|
|
11
|
-
* element regions the ink it carries itself, and the field is placed at all of it. The
|
|
12
|
-
* boundary character (`.` for a nested field, `[` for an array element) keeps the
|
|
13
|
-
* prefix a path boundary: `main.keywords` matches `main.keywords[0]`, not
|
|
14
|
-
* `main.keywords_note`.
|
|
15
|
-
*
|
|
16
|
-
* `regions` is a thunk because the first rung answers without it, and taking it
|
|
17
|
-
* marshals every placement in the compile across the WASM boundary.
|
|
8
|
+
* `regions` is a thunk because taking it marshals every placement across the WASM boundary.
|
|
18
9
|
*/
|
|
19
10
|
export function boxesForField(field, boxes, regions) {
|
|
20
11
|
if (boxes.length)
|
|
@@ -22,9 +13,8 @@ export function boxesForField(field, boxes, regions) {
|
|
|
22
13
|
return regions().filter((r) => r.field === field || r.field.startsWith(`${field}.`) || r.field.startsWith(`${field}[`));
|
|
23
14
|
}
|
|
24
15
|
/**
|
|
25
|
-
* Forward: a PDF-pt rect -> CSS % of the page box
|
|
26
|
-
*
|
|
27
|
-
* scroll's measurement needs no zoom or DPR term of its own (bridge.ts).
|
|
16
|
+
* Forward: a PDF-pt rect -> CSS % of the page box, which tracks the displayed size without a zoom
|
|
17
|
+
* or DPR term.
|
|
28
18
|
*/
|
|
29
19
|
export function rectToPercent(rect, pageSize) {
|
|
30
20
|
const [x0, y0, x1, y1] = rect;
|
|
@@ -36,11 +26,7 @@ export function rectToPercent(rect, pageSize) {
|
|
|
36
26
|
height: ((y1 - y0) / heightPt) * 100
|
|
37
27
|
};
|
|
38
28
|
}
|
|
39
|
-
/**
|
|
40
|
-
* Position `el` absolutely at `pct` (the % rect this module produces): the one
|
|
41
|
-
* `PercentRect` → `element.style` write, so the positioning convention lives beside
|
|
42
|
-
* the transform that produces it rather than at the caller placing a marker.
|
|
43
|
-
*/
|
|
29
|
+
/** Position `el` absolutely at `pct`. */
|
|
44
30
|
export function applyPercentRect(el, pct) {
|
|
45
31
|
Object.assign(el.style, {
|
|
46
32
|
position: 'absolute',
|
|
@@ -51,22 +37,15 @@ export function applyPercentRect(el, pct) {
|
|
|
51
37
|
});
|
|
52
38
|
}
|
|
53
39
|
/**
|
|
54
|
-
* A CSS-px length on a page element of CSS width `cssW` ->
|
|
55
|
-
*
|
|
56
|
-
* by width under a locked aspect ratio, so {@link clickToPdfPt}'s two ratios are one
|
|
57
|
-
* number and a length has one to convert through. What the click bridge reads its
|
|
58
|
-
* pointer slack in points through.
|
|
40
|
+
* A CSS-px length on a page element of CSS width `cssW` -> PDF-pt. Width alone, since `paint.ts`
|
|
41
|
+
* locks the aspect ratio.
|
|
59
42
|
*/
|
|
60
43
|
export function pxToPt(px, cssW, pageSize) {
|
|
61
44
|
return (px / cssW) * pageSize.widthPt;
|
|
62
45
|
}
|
|
63
46
|
/**
|
|
64
|
-
* Inverse: a click at CSS px `(px, py)` within a page element of CSS size
|
|
65
|
-
* `(cssW, cssH)` -> PDF-pt.
|
|
66
|
-
* fraction `rectToPercent` would place a point's box corner at is the same
|
|
67
|
-
* fraction this reads back from `px/cssW`, `py/cssH`; DPR-independent, since
|
|
68
|
-
* the page element's CSS size (not its canvas backing store) is the only input.
|
|
69
|
-
* This is what the click bridge feeds `session.positionAt`/`fieldAt` (bridge.ts).
|
|
47
|
+
* Inverse of {@link rectToPercent}: a click at CSS px `(px, py)` within a page element of CSS size
|
|
48
|
+
* `(cssW, cssH)` -> PDF-pt.
|
|
70
49
|
*/
|
|
71
50
|
export function clickToPdfPt(px, py, cssW, cssH, pageSize) {
|
|
72
51
|
const { widthPt, heightPt } = pageSize;
|
package/dist/preview/index.js
CHANGED
|
@@ -1,20 +1,8 @@
|
|
|
1
|
-
// `@quillmark/svelte/preview`: the live-preview surface.
|
|
2
|
-
//
|
|
3
|
-
// `
|
|
4
|
-
//
|
|
5
|
-
// (the codec, the VisualEditor) so a `@quillmark/preview` promotion stays a
|
|
6
|
-
// re-export, not a refactor: it may reach `/core` (the shared WASM boundary) and
|
|
7
|
-
// nothing else. Enforced by `check:deps`, at the subpath's edge: a relative hop past a
|
|
8
|
-
// module directly in `core/` is what pulls the codec, and all of ProseMirror with it.
|
|
9
|
-
// The theme derivation, which the page slots read through `var()`.
|
|
10
|
-
// Imported at the barrel because a subpath is what a consumer gets: this surface
|
|
11
|
-
// reaches modules inside `core/` and never its entry, which carries no sheet.
|
|
12
|
-
// `check:deps` holds the reach.
|
|
1
|
+
// `@quillmark/svelte/preview`: the live-preview surface. Imports no editor-side code; it may reach
|
|
2
|
+
// `/core` and nothing else (`check:deps`).
|
|
3
|
+
// The theme derivation the page slots read through `var()`; a subpath import reaches `core/`
|
|
4
|
+
// modules, never its entry, which carries no sheet.
|
|
13
5
|
import '../core/theme.css';
|
|
14
6
|
export { createPreview } from './controller.js';
|
|
15
7
|
export { default as Preview } from './Preview.svelte';
|
|
16
|
-
// What the preview says: its message states and the zoom pill's name. Keyed and partial; the
|
|
17
|
-
// English is exported so a consumer composes against it rather than restating it.
|
|
18
|
-
// The merge is the surface's own: a consumer hands `strings` to a prop, never
|
|
19
|
-
// merges one.
|
|
20
8
|
export { DEFAULT_PREVIEW_STRINGS } from './strings.js';
|
package/dist/preview/paint.d.ts
CHANGED
|
@@ -11,9 +11,9 @@ export interface PaintLoop {
|
|
|
11
11
|
/** Reconcile slot count to `pageCount`, re-cache geometry, repaint mounted `dirtyPages`. */
|
|
12
12
|
refresh(dirtyPages: readonly number[], pageCount: number): void;
|
|
13
13
|
/**
|
|
14
|
-
* Draw every page at `scale` CSS px per PDF pt and repaint the mounted ones
|
|
15
|
-
*
|
|
16
|
-
*
|
|
14
|
+
* Draw every page at `scale` CSS px per PDF pt and repaint the mounted ones on the next frame,
|
|
15
|
+
* or once `settle` ms pass without another call (a gesture's request, the stretched raster
|
|
16
|
+
* standing in).
|
|
17
17
|
*/
|
|
18
18
|
setScale(scale: number, settle?: number): void;
|
|
19
19
|
destroy(): void;
|
package/dist/preview/paint.js
CHANGED
|
@@ -1,19 +1,13 @@
|
|
|
1
|
-
// Vertical breathing room between stacked pages; cosmetic only, and a rung like
|
|
2
|
-
// any other gap: the stack breathes on the same rhythm the cards do.
|
|
3
1
|
const PAGE_GAP = 'var(--_qm-space-4)';
|
|
4
|
-
// The least raster density, in device pixels per CSS pixel
|
|
5
|
-
//
|
|
6
|
-
// is resampled nearly 1:1 and small text smears; a raster at least twice the
|
|
7
|
-
// box is downsampled instead, which keeps body text legible.
|
|
2
|
+
// The least raster density, in device pixels per CSS pixel: glyphs rasterize unhinted, so a 1× or
|
|
3
|
+
// fractional raster smears small text where a 2× one downsamples cleanly.
|
|
8
4
|
const MIN_DENSITY = 2;
|
|
9
5
|
export function createPaintLoop(session, container, margin,
|
|
10
|
-
// Notified when a slot's `session.paint` throws
|
|
11
|
-
//
|
|
12
|
-
// decides how to surface it.
|
|
6
|
+
// Notified when a slot's `session.paint` throws; the throw is swallowed so one bad page never
|
|
7
|
+
// aborts the band sweep.
|
|
13
8
|
onPaintError) {
|
|
14
|
-
// The IntersectionObserver root must be a scrollable ancestor of the slots;
|
|
15
|
-
//
|
|
16
|
-
// a stylesheet rule counts as a choice, not just an inline style).
|
|
9
|
+
// The IntersectionObserver root must be a scrollable ancestor of the slots; a consumer's own
|
|
10
|
+
// choice stands.
|
|
17
11
|
const computed = getComputedStyle(container);
|
|
18
12
|
if (computed.overflowY === 'visible')
|
|
19
13
|
container.style.overflowY = 'auto';
|
|
@@ -42,27 +36,15 @@ onPaintError) {
|
|
|
42
36
|
}, { root: container, threshold: 0 });
|
|
43
37
|
function makeSlot(page) {
|
|
44
38
|
const size = session.pageSize(page);
|
|
45
|
-
// The
|
|
46
|
-
//
|
|
47
|
-
// edge is where two tones meet and no stroke is drawn around it
|
|
48
|
-
// (ARCHITECTURE §Styling). It leaves the no-ink rule untouched, since a
|
|
49
|
-
// boundary that is not painted is not a mark on the page (PREVIEW §"Two
|
|
50
|
-
// responsibilities"). `border-box` costs nothing here and keeps a slot a
|
|
51
|
-
// consumer insets from measuring wider than its paper.
|
|
39
|
+
// The paper edge is where `--_qm-surface` meets the container's `--_qm-surface-sunken`; no
|
|
40
|
+
// stroke is drawn (ARCHITECTURE §Styling).
|
|
52
41
|
const el = document.createElement('div');
|
|
53
|
-
// Not `.qm-page`: that
|
|
54
|
-
//
|
|
55
|
-
// rule's `color-scheme` inside a mounted root, which the derivation leaves to the
|
|
56
|
-
// host (`core/theme.css`).
|
|
42
|
+
// Not `.qm-page`: that is the preset's page baseline, whose `color-scheme` a slot must not
|
|
43
|
+
// take inside a mounted root (`core/theme.css`).
|
|
57
44
|
el.className = 'qm-page-slot';
|
|
58
|
-
// The page's index, in the DOM. `pageByEl` is this module's and a consumer
|
|
59
|
-
// drawing an overlay of its own needs the number: position among the slots is
|
|
60
|
-
// the only handle without it, which is right today and is not a contract.
|
|
61
45
|
el.dataset.page = String(page);
|
|
62
|
-
// `flex: none`
|
|
63
|
-
//
|
|
64
|
-
// narrower than the pane and resolve to zero on one wider, so it overflows to the
|
|
65
|
-
// scrollable side.
|
|
46
|
+
// `flex: none` keeps the flex column from shrinking a page; auto inline margins centre a
|
|
47
|
+
// narrower page and overflow a wider one to the scrollable side.
|
|
66
48
|
Object.assign(el.style, {
|
|
67
49
|
position: 'relative',
|
|
68
50
|
boxSizing: 'border-box',
|
|
@@ -74,7 +56,7 @@ onPaintError) {
|
|
|
74
56
|
sizeSlot(el, size);
|
|
75
57
|
if (page > 0)
|
|
76
58
|
el.style.marginTop = PAGE_GAP;
|
|
77
|
-
// After the last slot
|
|
59
|
+
// After the last slot: the zoom pill's dock is a child too.
|
|
78
60
|
const last = slots.at(-1)?.el;
|
|
79
61
|
if (last)
|
|
80
62
|
last.after(el);
|
|
@@ -92,8 +74,6 @@ onPaintError) {
|
|
|
92
74
|
const layoutScale = scale ?? (slot.el.clientWidth || slot.size.widthPt) / slot.size.widthPt;
|
|
93
75
|
return layoutScale * Math.max(window.devicePixelRatio || 1, MIN_DENSITY);
|
|
94
76
|
}
|
|
95
|
-
// Rasterize `slot` into its canvas (creating the canvas if this is its first
|
|
96
|
-
// paint since mounting).
|
|
97
77
|
function paintSlot(slot) {
|
|
98
78
|
let canvas = canvases.get(slot.page);
|
|
99
79
|
if (!canvas) {
|
|
@@ -107,8 +87,7 @@ onPaintError) {
|
|
|
107
87
|
height: '100%'
|
|
108
88
|
});
|
|
109
89
|
}
|
|
110
|
-
//
|
|
111
|
-
// the page as unmounted and retries instead of keeping a blank page.
|
|
90
|
+
// Bail before registering so `updateBand` retries rather than keeping a blank page.
|
|
112
91
|
const ctx = canvas.getContext('2d');
|
|
113
92
|
if (!ctx)
|
|
114
93
|
return;
|
|
@@ -122,11 +101,8 @@ onPaintError) {
|
|
|
122
101
|
rastered.set(slot.page, at);
|
|
123
102
|
}
|
|
124
103
|
catch (err) {
|
|
125
|
-
//
|
|
126
|
-
// the
|
|
127
|
-
// uncaught throw would strand the rest of the band unpainted. Unmount the
|
|
128
|
-
// half-mounted canvas (so the page is a retryable unmounted slot, not a
|
|
129
|
-
// blank registered one) and surface the failure; the sweep moves on.
|
|
104
|
+
// Unmount the half-mounted canvas so the page stays a retryable unmounted slot, and let
|
|
105
|
+
// the sweep move on.
|
|
130
106
|
unmountPage(slot.page);
|
|
131
107
|
onPaintError?.(slot.page, err);
|
|
132
108
|
}
|
|
@@ -139,10 +115,8 @@ onPaintError) {
|
|
|
139
115
|
canvases.delete(page);
|
|
140
116
|
rastered.delete(page);
|
|
141
117
|
}
|
|
142
|
-
// Mount pages newly inside [min(visible)-margin, max(visible)+margin]
|
|
143
|
-
//
|
|
144
|
-
// an already-mounted page in the band is left alone (paint contract: an idle
|
|
145
|
-
// canvas keeps its pixels for free; repainting it here would be pure waste).
|
|
118
|
+
// Mount pages newly inside [min(visible)-margin, max(visible)+margin] and unmount those outside
|
|
119
|
+
// it; a mounted page in the band keeps its pixels.
|
|
146
120
|
function updateBand() {
|
|
147
121
|
const painted = new Set();
|
|
148
122
|
if (visible.size === 0 || slots.length === 0)
|
|
@@ -162,10 +136,7 @@ onPaintError) {
|
|
|
162
136
|
}
|
|
163
137
|
return painted;
|
|
164
138
|
}
|
|
165
|
-
// Grow
|
|
166
|
-
// reports the new count, and dirtyPages already covers any reflow), then
|
|
167
|
-
// re-cache every page's geometry (cheap report-only reads; simplest way to
|
|
168
|
-
// stay correct across any recompile, not just a count change).
|
|
139
|
+
// Grow or shrink the slots to `pageCount`, then re-cache every page's geometry.
|
|
169
140
|
function reconcile(pageCount) {
|
|
170
141
|
while (slots.length > pageCount) {
|
|
171
142
|
const slot = slots.pop();
|
|
@@ -183,22 +154,15 @@ onPaintError) {
|
|
|
183
154
|
for (let i = 0; i < slots.length; i++) {
|
|
184
155
|
const size = session.pageSize(i);
|
|
185
156
|
slots[i] = { ...slots[i], size };
|
|
186
|
-
//
|
|
187
|
-
// can change a page's dimensions without changing the count, and every
|
|
188
|
-
// %-space click/scroll transform assumes box shape matches PageSize.
|
|
157
|
+
// A recompile can change a page's dimensions without changing the count.
|
|
189
158
|
slots[i].el.style.aspectRatio = `${size.widthPt} / ${size.heightPt}`;
|
|
190
159
|
sizeSlot(slots[i].el, size);
|
|
191
160
|
}
|
|
192
161
|
}
|
|
193
|
-
// Slots are built on the first `refresh`
|
|
194
|
-
//
|
|
195
|
-
//
|
|
196
|
-
//
|
|
197
|
-
// A canvas stretches with its slot, so a page that stays mounted while the scale
|
|
198
|
-
// or `devicePixelRatio` shifts keeps its place and goes soft. Both repaint mounted
|
|
199
|
-
// pages; canvases are `position:absolute`, so a repaint can't feed back into
|
|
200
|
-
// layout.
|
|
201
|
-
// Repaint mounted pages whose raster scale moved. A no-op for an unmounted page.
|
|
162
|
+
// Slots are built on the first `refresh`: `session.pageSize` throws for a page the count
|
|
163
|
+
// excludes.
|
|
164
|
+
// Repaint mounted pages whose raster scale moved; a canvas stretches with its slot and goes
|
|
165
|
+
// soft otherwise.
|
|
202
166
|
function repaint() {
|
|
203
167
|
for (const page of canvases.keys()) {
|
|
204
168
|
const slot = slots[page];
|
|
@@ -229,11 +193,9 @@ onPaintError) {
|
|
|
229
193
|
repaint();
|
|
230
194
|
});
|
|
231
195
|
}
|
|
232
|
-
// `devicePixelRatio`
|
|
233
|
-
//
|
|
234
|
-
//
|
|
235
|
-
// is therefore one-shot: re-arm it against the new ratio on each change. Guarded
|
|
236
|
-
// for environments (jsdom) without `matchMedia`.
|
|
196
|
+
// A `devicePixelRatio` change never reaches `setScale`. A `(resolution: …dppx)` query pins the
|
|
197
|
+
// current ratio, so it is re-armed on each change. Guarded for jsdom, which has no
|
|
198
|
+
// `matchMedia`.
|
|
237
199
|
let dprQuery;
|
|
238
200
|
function onDprChange() {
|
|
239
201
|
scheduleRepaint();
|
|
@@ -252,9 +214,7 @@ onPaintError) {
|
|
|
252
214
|
},
|
|
253
215
|
refresh(dirtyPages, pageCount) {
|
|
254
216
|
reconcile(pageCount);
|
|
255
|
-
//
|
|
256
|
-
// band just painted is already post-apply, so a second full
|
|
257
|
-
// rasterization from the dirty loop would add nothing.
|
|
217
|
+
// A page the band just painted is already post-apply.
|
|
258
218
|
const painted = updateBand();
|
|
259
219
|
for (const page of dirtyPages) {
|
|
260
220
|
if (painted.has(page))
|
|
@@ -6,7 +6,7 @@ export interface PreviewStrings {
|
|
|
6
6
|
/** The zoom pill's action, after the percentage it shows. */
|
|
7
7
|
resetZoom: string;
|
|
8
8
|
}
|
|
9
|
-
/** The package's English.
|
|
9
|
+
/** The package's English. */
|
|
10
10
|
export declare const DEFAULT_PREVIEW_STRINGS: PreviewStrings;
|
|
11
11
|
/** A consumer's wording: any subset, the rest the package's. */
|
|
12
12
|
export type PreviewStringsInput = Partial<PreviewStrings>;
|
package/dist/preview/strings.js
CHANGED
|
@@ -1,17 +1,5 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
3
|
-
// reader gives the zoom pill.
|
|
4
|
-
//
|
|
5
|
-
// Separate from the visual surface's set rather than pooled with it: `/preview`
|
|
6
|
-
// reaches `/core` and nothing editor-side, which is what lets it promote to its own
|
|
7
|
-
// package, and a shared strings module would be an edge back across that line. The
|
|
8
|
-
// shape is the same, so a consumer wording both writes the same kind of object
|
|
9
|
-
// twice rather than learning two idioms.
|
|
10
|
-
//
|
|
11
|
-
// A plain options field rather than a context channel, unlike the visual surface's:
|
|
12
|
-
// they are read in one module by one function, so there is no tree to make them
|
|
13
|
-
// ambient for.
|
|
14
|
-
/** The package's English. Every key, so the merge below is total. */
|
|
1
|
+
// Separate from the visual surface's strings: `/preview` reaches `/core` and nothing editor-side.
|
|
2
|
+
/** The package's English. */
|
|
15
3
|
export const DEFAULT_PREVIEW_STRINGS = {
|
|
16
4
|
renderFailed: 'Preview failed to render.',
|
|
17
5
|
noPages: 'No pages to preview.',
|