@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.
Files changed (95) hide show
  1. package/NOTICE +4 -4
  2. package/README.md +26 -8
  3. package/THEMING.md +10 -8
  4. package/dist/core/address.d.ts +35 -64
  5. package/dist/core/address.js +35 -55
  6. package/dist/core/bloom.d.ts +4 -10
  7. package/dist/core/bloom.js +14 -27
  8. package/dist/core/codec/blocks.d.ts +41 -43
  9. package/dist/core/codec/blocks.js +97 -130
  10. package/dist/core/codec/code.d.ts +7 -10
  11. package/dist/core/codec/code.js +37 -46
  12. package/dist/core/codec/decode.d.ts +19 -23
  13. package/dist/core/codec/decode.js +76 -111
  14. package/dist/core/codec/encode.d.ts +11 -15
  15. package/dist/core/codec/encode.js +60 -67
  16. package/dist/core/codec/field.d.ts +62 -95
  17. package/dist/core/codec/field.js +107 -179
  18. package/dist/core/codec/islands.d.ts +33 -41
  19. package/dist/core/codec/islands.js +76 -86
  20. package/dist/core/codec/lists.d.ts +17 -26
  21. package/dist/core/codec/lists.js +117 -157
  22. package/dist/core/codec/marks.d.ts +4 -3
  23. package/dist/core/codec/marks.js +8 -3
  24. package/dist/core/codec/prose.css +84 -46
  25. package/dist/core/codec/schema.d.ts +18 -23
  26. package/dist/core/codec/schema.js +84 -76
  27. package/dist/core/codec/slash.d.ts +42 -48
  28. package/dist/core/codec/slash.js +0 -0
  29. package/dist/core/codec/table-copy.d.ts +4 -3
  30. package/dist/core/codec/table-copy.js +25 -7
  31. package/dist/core/codec/table-view.d.ts +12 -12
  32. package/dist/core/codec/table-view.js +460 -383
  33. package/dist/core/codec/table.d.ts +94 -61
  34. package/dist/core/codec/table.js +331 -83
  35. package/dist/core/codec/tasks.d.ts +6 -0
  36. package/dist/core/codec/tasks.js +30 -3
  37. package/dist/core/errors.d.ts +15 -30
  38. package/dist/core/errors.js +2 -6
  39. package/dist/core/index.js +7 -12
  40. package/dist/core/lifecycle.d.ts +7 -16
  41. package/dist/core/lifecycle.js +10 -21
  42. package/dist/core/rebind.svelte.d.ts +10 -20
  43. package/dist/core/rebind.svelte.js +15 -27
  44. package/dist/core/rungs.d.ts +14 -11
  45. package/dist/core/rungs.js +17 -19
  46. package/dist/core/teardown.d.ts +4 -6
  47. package/dist/core/teardown.js +7 -10
  48. package/dist/core/theme.css +74 -54
  49. package/dist/index.js +3 -8
  50. package/dist/preset/app.css +3 -1
  51. package/dist/preset/index.css +6 -8
  52. package/dist/preset/recipes.css +12 -15
  53. package/dist/preset/scale.css +18 -17
  54. package/dist/preset/shell.css +10 -0
  55. package/dist/preview/bridge.d.ts +18 -22
  56. package/dist/preview/bridge.js +29 -58
  57. package/dist/preview/controller.d.ts +21 -29
  58. package/dist/preview/controller.js +14 -39
  59. package/dist/preview/geometry.d.ts +13 -34
  60. package/dist/preview/geometry.js +13 -34
  61. package/dist/preview/index.js +4 -16
  62. package/dist/preview/paint.d.ts +3 -3
  63. package/dist/preview/paint.js +28 -68
  64. package/dist/preview/strings.d.ts +1 -1
  65. package/dist/preview/strings.js +2 -14
  66. package/dist/preview/zoom.d.ts +18 -23
  67. package/dist/preview/zoom.js +28 -27
  68. package/dist/visual/ArrayField.svelte +77 -16
  69. package/dist/visual/BooleanField.svelte +27 -14
  70. package/dist/visual/Card.svelte +61 -48
  71. package/dist/visual/CardControls.svelte +5 -11
  72. package/dist/visual/DateField.svelte +20 -4
  73. package/dist/visual/EnumField.svelte +3 -0
  74. package/dist/visual/FieldHint.svelte +2 -1
  75. package/dist/visual/FormatPopover.svelte +82 -15
  76. package/dist/visual/FormatPopover.svelte.d.ts +4 -0
  77. package/dist/visual/MatrixAdd.svelte +30 -12
  78. package/dist/visual/MatrixField.svelte +24 -16
  79. package/dist/visual/NumberField.svelte +7 -0
  80. package/dist/visual/ObjectField.svelte +4 -4
  81. package/dist/visual/ProseField.svelte +0 -5
  82. package/dist/visual/TipsCard.svelte +11 -10
  83. package/dist/visual/VariantField.svelte +3 -1
  84. package/dist/visual/VisualEditorInner.svelte +19 -3
  85. package/dist/visual/controls.css +7 -2
  86. package/dist/visual/fonts.d.ts +18 -0
  87. package/dist/visual/fonts.js +59 -0
  88. package/dist/visual/icons/nodes.d.ts +0 -5
  89. package/dist/visual/icons/nodes.js +0 -4
  90. package/dist/visual/props.d.ts +14 -1
  91. package/dist/visual/strings.d.ts +7 -0
  92. package/dist/visual/strings.js +5 -2
  93. package/dist/visual/structure.d.ts +43 -87
  94. package/dist/visual/structure.js +42 -83
  95. 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
- /** A click resolved to an address ({@link Landing}): a caret where the compile
11
- * tracks the content under the point, the field alone where it tracks only the
12
- * placement. The hook does not fire where the compile tracks neither. */
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
- /** A page paint the backend refused ({@link EditorErrorHandler}). The preview
15
- * shows its error message state either way; this routes it to an app's sink. */
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, keyed and partial: unset keys take the package's
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
- * when this compile places nothing at that address — an answer, not a failure: the
28
- * plate places plenty it does not track, and the preview carries no schema to tell
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: the pane moves only when the caret has left
35
- * the fold.
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
- * `focusPosition`. A `scrollToField` that placed its target ends one too.
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
- * the pane's floor and 200%; `undefined` is the default, the widest page fitting the
56
- * pane's width up to 125%. The reader's own gestures move the same zoom.
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 non-paint states (empty, a paint that
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
- // The paint loop is safe at any page count: zero pages reconciles to zero
33
- // slots and never calls the `paint`/`pageSize` verbs the boundary refuses
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
- // 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.
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 that rebuild, so a trip held for a pane with no box crosses it. Only the
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
- // Read the count against the current slots before reconcile moves it, and
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, so its ResizeObserver is the earlier one and a pane
87
- // shown again re-fits before the bridge runs the trip it held at the old scale.
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 place the editor put the caret, re-located after every recompile and
92
- // dropped on a focus change or a discrete hop. `session.locate` answers against the
93
- // last compiled layout and a consumer debounces `update`, so a caret typed past that
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 discrete hop outranks the follow: a caret re-asserted at the next
112
- // recompile would pull the pane straight off the field a host just asked
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
- * What the scroll reads, so an address `regions()` names is never one the preview
19
- * cannot place (PREVIEW.md §"Two responsibilities").
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
- * `fieldBoxes` answers `[]` for a scalar reference the plate places without tracking
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. Percent positioning tracks
37
- * the page's displayed size across DPR and container resize for free, so the
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` -> the PDF-pt length it spans.
49
- * Width alone, because that is the scale the slot is drawn at: `paint.ts` sizes a slot
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. The exact inverse of {@link rectToPercent}: the
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;
@@ -1,20 +1,11 @@
1
1
  /**
2
- * An address's boxes: `fieldBoxes`'s union, else every `regions()` rect at or under it.
3
- * What the scroll reads, so an address `regions()` names is never one the preview
4
- * cannot place (PREVIEW.md §"Two responsibilities").
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
- * `fieldBoxes` answers `[]` for a scalar reference the plate places without tracking
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. Percent positioning tracks
26
- * the page's displayed size across DPR and container resize for free, so the
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` -> the PDF-pt length it spans.
55
- * Width alone, because that is the scale the slot is drawn at: `paint.ts` sizes a slot
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. The exact inverse of {@link rectToPercent}: the
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;
@@ -1,20 +1,8 @@
1
- // `@quillmark/svelte/preview`: the live-preview surface.
2
- //
3
- // `createPreview(session)` (vanilla-TS core) + a thin `<Preview>` Svelte wrapper
4
- // land here. Reserved-package invariant: this subpath imports no editor-side code
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';
@@ -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: on the
15
- * next frame, or once `settle` has passed without another call, which is what a
16
- * gesture asks for, the stretched raster standing in until it rests.
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;
@@ -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. Glyphs rasterize
5
- // unhinted, so a raster at a 1× or fractional ratio (1.25, 1.5; browser zoom)
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: the loop swallows the throw
11
- // (see `paintSlot`) so one bad page never aborts the band sweep; the caller
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
- // respect a consumer's own choice if they already set one (computed style, so
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 sheet is a plane and the desk is the one under it: paper sits at
46
- // `--_qm-surface` and the container paints `--_qm-surface-sunken`, so the
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 name is the preset's page baseline, an unlayered rule at
54
- // ordinary specificity a host puts on `<body>`, and a slot wearing it takes that
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` under the flex column `Preview.svelte` lays the desk out as, which
63
- // would otherwise shrink a page to fit the pane; auto inline margins centre a page
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 rather than at the end: the zoom pill's dock is a child too.
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
- // Contextless canvas: bail before registering, so `updateBand` still sees
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
- // A paint that throws must not abort the band loop; `updateBand` runs in
126
- // the IntersectionObserver callback and sweeps every entry, so an
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], unmount
143
- // pages that fell outside it. Only touches pages changing band membership;
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/shrink the slot array to `pageCount` (trailing add/remove; a recompile
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
- // Keep the page box's shape in step with the re-read size: a recompile
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` (the controller calls it immediately),
194
- // not here: reconcile → `session.pageSize` must run after the controller's
195
- // `pageCount` gate, since `pageSize` throws for a page the count excludes.
196
- // ── Keep mounted rasters sharp ──────────────────────────────────────────────
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` changes (window dragged to a different-DPI monitor, browser
233
- // zoom) leave the scale untouched, so `setScale` never hears of them; watch DPR
234
- // directly. A `(resolution: …dppx)` query pins the current ratio and
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
- // Slot count may have moved the band (pages added/removed); a page the
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. Every key, so the merge below is total. */
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>;
@@ -1,17 +1,5 @@
1
- // Every word the preview says: the surface is a canvas, and its only prose is the
2
- // message states that stand in when there is nothing to paint, and the name a screen
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.',