@quillmark/svelte 0.10.1 → 0.12.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 (80) hide show
  1. package/README.md +1 -1
  2. package/THEMING.md +2 -2
  3. package/dist/core/address.d.ts +17 -10
  4. package/dist/core/address.js +26 -12
  5. package/dist/core/bloom.js +4 -3
  6. package/dist/core/codec/blocks.js +21 -1
  7. package/dist/core/codec/breaks.d.ts +10 -2
  8. package/dist/core/codec/breaks.js +65 -9
  9. package/dist/core/codec/code.d.ts +6 -4
  10. package/dist/core/codec/code.js +7 -4
  11. package/dist/core/codec/decode.js +103 -68
  12. package/dist/core/codec/encode.d.ts +5 -8
  13. package/dist/core/codec/encode.js +56 -86
  14. package/dist/core/codec/field.js +43 -24
  15. package/dist/core/codec/index.d.ts +2 -1
  16. package/dist/core/codec/index.js +6 -3
  17. package/dist/core/codec/islands.d.ts +16 -11
  18. package/dist/core/codec/islands.js +70 -59
  19. package/dist/core/codec/keymap.js +2 -1
  20. package/dist/core/codec/marks.d.ts +17 -13
  21. package/dist/core/codec/marks.js +39 -42
  22. package/dist/core/codec/positions.d.ts +4 -1
  23. package/dist/core/codec/positions.js +5 -1
  24. package/dist/core/codec/reconcile.d.ts +6 -0
  25. package/dist/core/codec/reconcile.js +19 -0
  26. package/dist/core/codec/schema.d.ts +5 -8
  27. package/dist/core/codec/schema.js +30 -156
  28. package/dist/core/codec/slash.js +0 -0
  29. package/dist/core/codec/table-view.js +77 -26
  30. package/dist/core/codec/table.d.ts +6 -5
  31. package/dist/core/codec/table.js +14 -31
  32. package/dist/core/codec/urls.d.ts +20 -0
  33. package/dist/core/codec/urls.js +45 -0
  34. package/dist/core/rungs.d.ts +8 -0
  35. package/dist/core/rungs.js +24 -0
  36. package/dist/core/theme.css +10 -0
  37. package/dist/preset/recipes.css +1 -1
  38. package/dist/preset/scale.css +7 -4
  39. package/dist/preview/bridge.d.ts +8 -1
  40. package/dist/preview/bridge.js +31 -25
  41. package/dist/preview/controller.js +13 -13
  42. package/dist/preview/geometry.d.ts +12 -1
  43. package/dist/preview/geometry.js +14 -1
  44. package/dist/preview/paint.js +6 -4
  45. package/dist/preview/strings.d.ts +0 -2
  46. package/dist/preview/strings.js +5 -6
  47. package/dist/visual/ArrayField.svelte +600 -173
  48. package/dist/visual/ArrayField.svelte.d.ts +14 -1
  49. package/dist/visual/BooleanField.svelte +10 -7
  50. package/dist/visual/Card.svelte +8 -30
  51. package/dist/visual/DateField.svelte +2 -2
  52. package/dist/visual/Field.svelte +76 -32
  53. package/dist/visual/FieldHint.svelte +1 -1
  54. package/dist/visual/MatrixField.svelte +352 -0
  55. package/dist/visual/MatrixField.svelte.d.ts +36 -0
  56. package/dist/visual/ObjectField.svelte +356 -133
  57. package/dist/visual/ObjectField.svelte.d.ts +17 -2
  58. package/dist/visual/VariantField.svelte +55 -2
  59. package/dist/visual/VariantField.svelte.d.ts +10 -1
  60. package/dist/visual/VisualEditor.svelte +1 -1
  61. package/dist/visual/VisualEditorInner.svelte +118 -67
  62. package/dist/visual/controls.css +39 -9
  63. package/dist/visual/diagnostics.d.ts +26 -2
  64. package/dist/visual/diagnostics.js +52 -5
  65. package/dist/visual/ext.d.ts +12 -13
  66. package/dist/visual/ext.js +22 -19
  67. package/dist/visual/hold.js +2 -2
  68. package/dist/visual/icons/nodes.d.ts +3 -0
  69. package/dist/visual/icons/nodes.js +2 -1
  70. package/dist/visual/index.js +2 -3
  71. package/dist/visual/leaves.d.ts +25 -12
  72. package/dist/visual/links.d.ts +4 -0
  73. package/dist/visual/links.js +15 -4
  74. package/dist/visual/motion.d.ts +14 -5
  75. package/dist/visual/motion.js +13 -11
  76. package/dist/visual/strings.d.ts +26 -13
  77. package/dist/visual/strings.js +7 -2
  78. package/dist/visual/structure.d.ts +101 -8
  79. package/dist/visual/structure.js +252 -13
  80. package/package.json +2 -2
@@ -0,0 +1,24 @@
1
+ // Where a script reads the scale. The derivation declares every `--_qm-*` rung on
2
+ // `[data-qm-root]` and nowhere else (`core/theme.css`), so a descendant's value is that
3
+ // one inherited and the root is the element the question is about: asking a leaf three
4
+ // containers deep is asking an arbitrary element what the surface is set to.
5
+ //
6
+ // The read is also the cost. `getComputedStyle` compounds with nesting depth under jsdom,
7
+ // so a suite that resolves rungs off landed elements pays the depth of whatever it landed
8
+ // on; resolving them off the root pays the root's.
9
+ /** The element the scale is declared on, for `node`: its root, or `node` where it stands
10
+ * outside one (a detached surface, a consumer's own mount). */
11
+ export function rungStyle(node) {
12
+ return getComputedStyle(node.closest('[data-qm-root]') ?? node);
13
+ }
14
+ /** A duration rung off `style`, in ms; `undefined` where the derivation is out of reach
15
+ * (an unstyled root, or jsdom, where `getComputedStyle` reports custom properties as
16
+ * empty). No rung, no motion: a fallback here would be the scale restated in the one
17
+ * place `check:style` cannot read. */
18
+ export function rungMs(style, rung) {
19
+ const raw = style.getPropertyValue(rung).trim();
20
+ const n = Number.parseFloat(raw);
21
+ if (!Number.isFinite(n))
22
+ return undefined;
23
+ return raw.endsWith('ms') ? n : n * 1000;
24
+ }
@@ -336,6 +336,16 @@
336
336
  through the icon's `size`, so a stylesheet beats the SVG presentation attribute
337
337
  and one rung is the whole of what a control draws. */
338
338
  --_qm-glyph-control: 14px;
339
+ /* The narrowest track a control that fills its width stays usable in: a table's
340
+ column floors at it, rises no further than its content, and a row of them scrolls
341
+ sideways rather than crushing a cell. A column drawing a mark takes no floor — a
342
+ switch is one width at every width, so there is no width for it to be usable in
343
+ (`ArrayField`). What the field has spare stands after the last column, an inner
344
+ grid carrying its own rhythm rather than the card's. Under the field minimum the
345
+ capacity ladder steps at (`.qm-tracks`), since a cell in a row has the header to
346
+ name it and no label of its own to fit. A threshold rather than a rhythm choice,
347
+ so it does not move with `--qm-space`. */
348
+ --_qm-track-min: 7rem;
339
349
 
340
350
  /* ── Type ────────────────────────────────────────────────────────────────
341
351
  `--qm-font-size` anchors the chrome rung; 1.125 is the ratio between adjacent
@@ -112,7 +112,7 @@ html:has(.qm-workspace) {
112
112
  display: none;
113
113
  }
114
114
 
115
- /* One mount at A time under the threshold, rather than two of them abreast. Half a narrow
115
+ /* One mount at a time under the threshold, rather than two of them abreast. Half a narrow
116
116
  viewport is under the width either surface reads at, so the split stops being one and
117
117
  the tracks become views of the same room: the host says which with `data-qm-show`, and
118
118
  the attribute names the track rather than what is in it, so nothing here knows an editor
@@ -1,10 +1,13 @@
1
- /* The host'S derivation: the same public dials in, a closed `--qmh-*` scale for the
1
+ /* The host's derivation: the same public dials in, a closed `--qmh-*` scale for the
2
2
  page around the surfaces out. `core/theme.css` is the same shape one level down
3
3
  (dials in, `--_qm-*` out), and the two are calibrated against each other: the
4
- figures here are the figures there (the ground step's ramp, 8% hover, 17%
5
- border, 45% ink, the ring's width, the status hue at both poles) and the
4
+ figures here are the figures there (the ground step's ramp, 8% hover, the
5
+ ring's width, the status hue at both poles) and the
6
6
  dial means the same plane in both, so a host plate and a card on it land on the same
7
- tone at both poles and a host page meets an editor's own column exactly. The well
7
+ tone at both poles and a host page meets an editor's own column exactly. The border
8
+ and ink rungs are each calibrated for their own plane rather than shared: a page's
9
+ chrome is read at one size against one ground, a card's interior at several against
10
+ the rungs under it, so the mixes are not the same figure. The well
8
11
  under the ground has no counterpart here: a host's own controls are boxed and carry a
9
12
  hairline, so nothing on a page needs the step an edgeless field does.
10
13
 
@@ -35,4 +35,11 @@ export interface VerticalSpan {
35
35
  * the port: no scroll shows more of it.
36
36
  */
37
37
  export declare function clearOfTheFold(target: VerticalSpan, port: VerticalSpan): boolean;
38
- export declare function createBridge(session: LiveSession, container: HTMLElement, slots: readonly PageSlot[], onPick: ((at: Landing) => void) | undefined): BridgeController;
38
+ /** The trip a pane with no box could not run, held for the box coming back. One, and the
39
+ * last one asked for: a caret hop supersedes the hop before it on a showing pane too.
40
+ * A cell rather than closure state, so a bridge rebuilt under a changed page count
41
+ * inherits what the one it replaced was holding (`controller.ts`). */
42
+ export interface HeldTrip {
43
+ run?: () => void;
44
+ }
45
+ export declare function createBridge(session: LiveSession, container: HTMLElement, slots: readonly PageSlot[], onPick: ((at: Landing) => void) | undefined, held?: HeldTrip): BridgeController;
@@ -1,4 +1,4 @@
1
- import { boxesForField, rectToPercent, clickToPdfPt, applyPercentRect } from './geometry.js';
1
+ import { boxesForField, rectToPercent, clickToPdfPt, pxToPt, applyPercentRect } from './geometry.js';
2
2
  /** A pane hidden by the shell's narrow switch (`preset/recipes.css`) measures 0×0, and so
3
3
  * does every box on it: the clearance arithmetic reads that as a caret clear of both
4
4
  * edges, and a centring computes its trip out of zeros. Neither is an answer about where
@@ -9,6 +9,11 @@ function hasBox(port) {
9
9
  /** What a rect reporting no height clears an edge by, in CSS px: a caret's own at the
10
10
  * smallest zoom a page still reads at. */
11
11
  const MIN_CLEARANCE = 8;
12
+ /** What a click may miss a field's ink by and still land on it, in CSS px of the page as
13
+ * drawn, so it converts per click at the scale the slot is showing. Sized as the
14
+ * pointer's imprecision and not as the leading it partly closes, which is a trade
15
+ * PREVIEW.md §"Click bridge" carries. */
16
+ const CLICK_SLACK_PX = 2;
12
17
  /**
13
18
  * Whether `target` sits clear of `port`'s edges by the fold's clearance, which is the
14
19
  * target's own height: a caret rect flush against an edge is visible and unusable, and
@@ -29,14 +34,15 @@ export function clearOfTheFold(target, port) {
29
34
  const clearance = Math.min(Math.max(target.height, MIN_CLEARANCE), (port.height - target.height) / 3);
30
35
  return target.top - clearance >= port.top && target.bottom + clearance <= port.bottom;
31
36
  }
32
- export function createBridge(session, container, slots, onPick) {
37
+ export function createBridge(session, container, slots, onPick, held = {}) {
33
38
  const unlisten = [];
34
39
  // Two rungs: `positionAt` over span-tracked content, `fieldAt` over every placement
35
40
  // the compile tracks, a strict superset of it (PREVIEW.md §"Click bridge" carries
36
41
  // the measurement). The second fires where a field is placed without its content
37
42
  // being tracked, and the pick carries no `pos` there: a fabricated `0` would be an
38
43
  // invented offset wearing a real one's type. No third rung hit-tests `regions()`,
39
- // whose rects bound ink the field does not fill.
44
+ // whose rects bound ink the field does not fill; the slack both rungs carry is what
45
+ // that rung would have been reached for.
40
46
  if (onPick) {
41
47
  for (const { page, el } of slots) {
42
48
  const handleClick = (ev) => {
@@ -50,10 +56,11 @@ export function createBridge(session, container, slots, onPick) {
50
56
  const px = ev.clientX - box.left;
51
57
  const py = ev.clientY - box.top;
52
58
  const pt = clickToPdfPt(px, py, box.width, box.height, slot.size);
53
- const hit = session.positionAt(slot.page, pt.x, pt.y);
59
+ const tol = pxToPt(CLICK_SLACK_PX, box.width, slot.size);
60
+ const hit = session.positionAt(slot.page, pt.x, pt.y, tol);
54
61
  if (hit)
55
62
  return void onPick(hit);
56
- const field = session.fieldAt(slot.page, pt.x, pt.y);
63
+ const field = session.fieldAt(slot.page, pt.x, pt.y, tol);
57
64
  if (field)
58
65
  onPick({ field });
59
66
  };
@@ -72,52 +79,50 @@ export function createBridge(session, container, slots, onPick) {
72
79
  marker.remove();
73
80
  return rect;
74
81
  }
75
- // Centre `target` vertically in the scrollport; the cross axis takes the minimum
76
- // trip instead, since the container is a vertical scroller and only a page wider
77
- // than the port has anything to move there.
78
- function centre(target) {
79
- const port = container.getBoundingClientRect();
82
+ // Centre `target` vertically in `port`, the scrollport's box; the cross axis takes the
83
+ // minimum trip instead, since the container is a vertical scroller and only a page
84
+ // wider than the port has anything to move there. `port` is the caller's own read: the
85
+ // marker `measure` places is absolute inside the slot, so nothing between that read
86
+ // and this one moves the container's box, and one forced layout serves the whole hop.
87
+ function centre(target, port) {
80
88
  container.scrollTop += target.top + target.height / 2 - (port.top + port.height / 2);
81
89
  if (target.left < port.left)
82
90
  container.scrollLeft += target.left - port.left;
83
91
  else if (target.right > port.right)
84
92
  container.scrollLeft += target.right - port.right;
85
93
  }
86
- // The trip a pane with no box could not run, held for the box coming back. One, and
87
- // the last one asked for: a caret hop supersedes the hop before it on a showing pane
88
- // too.
89
- let held;
90
94
  // The re-assert. Nothing else asks on the way back: `refresh` re-asks only on a
91
95
  // recompile, and the reader who has just tapped Preview is not typing, so the pane
92
96
  // would open where the caret was several edits ago. Guarded for jsdom, which ships no
93
97
  // ResizeObserver and lays out nothing for one to report.
94
98
  if (typeof ResizeObserver !== 'undefined') {
95
99
  const observer = new ResizeObserver(() => {
96
- if (!held || !hasBox(container.getBoundingClientRect()))
100
+ if (!held.run || !hasBox(container.getBoundingClientRect()))
97
101
  return;
98
- const trip = held;
99
- held = undefined;
102
+ const trip = held.run;
103
+ held.run = undefined;
100
104
  trip();
101
105
  });
102
106
  observer.observe(container);
103
107
  unlisten.push(() => observer.disconnect());
104
108
  }
105
109
  function scrollToField(field) {
106
- const box = boxesForField(field, session.fieldBoxes(field), session.regions())[0];
110
+ const box = boxesForField(field, session.fieldBoxes(field), () => session.regions())[0];
107
111
  if (!box)
108
112
  return false;
109
113
  const slot = slots[box.page];
110
114
  if (!slot)
111
115
  return false;
116
+ const port = container.getBoundingClientRect();
112
117
  // The address is placed, which is the whole of what the `boolean` answers; a pane
113
118
  // with no box yet is not a second no.
114
- if (!hasBox(container.getBoundingClientRect())) {
115
- held = () => void scrollToField(field);
119
+ if (!hasBox(port)) {
120
+ held.run = () => void scrollToField(field);
116
121
  return true;
117
122
  }
118
123
  // A discrete act ("show me this field"), so it centres every time, unlike
119
124
  // the continuous hop below.
120
- centre(measure(slot, rectToPercent(box.rect, slot.size)));
125
+ centre(measure(slot, rectToPercent(box.rect, slot.size)), port);
121
126
  return true;
122
127
  }
123
128
  function focusPosition(field, pos) {
@@ -131,8 +136,9 @@ export function createBridge(session, container, slots, onPick) {
131
136
  // for every keystroke of (`hasBox`): the follow would die silently for as long as
132
137
  // a track is hidden. What is held is the request, not the trip, so it re-locates
133
138
  // against whatever compile is current when the pane comes back.
134
- if (!hasBox(container.getBoundingClientRect())) {
135
- held = () => focusPosition(field, pos);
139
+ const port = container.getBoundingClientRect();
140
+ if (!hasBox(port)) {
141
+ held.run = () => focusPosition(field, pos);
136
142
  return;
137
143
  }
138
144
  // The continuous hop: one call per keystroke and per arrow key, so it moves
@@ -140,9 +146,9 @@ export function createBridge(session, container, slots, onPick) {
140
146
  // takes the scrollport back from the user on every one of them, including
141
147
  // the ones that changed nothing about where the caret already was.
142
148
  const target = measure(slot, rectToPercent(region.rect, slot.size));
143
- if (clearOfTheFold(target, container.getBoundingClientRect()))
149
+ if (clearOfTheFold(target, port))
144
150
  return;
145
- centre(target);
151
+ centre(target, port);
146
152
  }
147
153
  return {
148
154
  scrollToField,
@@ -7,14 +7,13 @@ const CONTAINER_CLASS = 'qm-preview';
7
7
  // class, the hooks a consumer and the tests target.
8
8
  const MESSAGE_CLASS = 'qm-preview-message';
9
9
  const EMPTY_CLASS = 'qm-preview-empty';
10
- const UNSUPPORTED_CLASS = 'qm-preview-unsupported';
11
10
  const ERROR_CLASS = 'qm-preview-error';
12
11
  export function createPreview(session, opts) {
13
12
  const container = opts.container;
14
13
  const margin = opts.margin ?? 1;
15
14
  const t = mergePreviewStrings(opts.strings);
16
15
  container.classList.add(CONTAINER_CLASS);
17
- // One element, restamped: the non-paint states (empty, unsupported, a paint that
16
+ // One element, restamped: the non-paint states (empty, a paint that
18
17
  // threw) are mutually exclusive.
19
18
  let message;
20
19
  function showMessage(text, state) {
@@ -33,8 +32,8 @@ export function createPreview(session, opts) {
33
32
  // slots and never calls the `paint`/`pageSize` verbs the boundary refuses
34
33
  // there. A paint that unexpectedly throws is caught per-slot and surfaced
35
34
  // through the shared message rather than aborting the observer callback
36
- // mid-sweep (runtime.d.ts: even a `supportsCanvas` compile can hit a paint
37
- // the boundary refuses; brittle to leave uncaught).
35
+ // mid-sweep (a paint is the one surface that can still refuse a counted page;
36
+ // brittle to leave uncaught).
38
37
  const paintLoop = createPaintLoop(session, container, margin, (page, err) => {
39
38
  reportError(opts.onError, {
40
39
  code: 'paint-failed',
@@ -50,24 +49,25 @@ export function createPreview(session, opts) {
50
49
  // slots array is a stable identity but its members are swapped on reconcile, and
51
50
  // the listeners are per slot.
52
51
  let bridge;
52
+ // Outlives that rebuild, so a trip held for a pane with no box crosses it. Only the
53
+ // follow is re-asserted after a recompile, and a `scrollToField` the pane could not
54
+ // run has nothing else that would ask again for it.
55
+ const held = {};
53
56
  function attach() {
54
- bridge = createBridge(session, container, paintLoop.slots, opts.onPick);
57
+ bridge = createBridge(session, container, paintLoop.slots, opts.onPick, held);
55
58
  }
56
59
  function detach() {
57
60
  bridge?.destroy();
58
61
  bridge = undefined;
59
62
  }
60
- // Called at construction and after every `apply`, so `supportsCanvas` is re-read
61
- // per compile (runtime.d.ts: re-check after `open`) and a 0-page or non-canvas
62
- // compile that later gains paintable pages recovers; the check spans the paint
63
- // capability generally, not just the page count.
63
+ // Called at construction and after every `apply`, so a 0-page compile that later
64
+ // gains pages recovers. A counted page is the whole condition: every backend
65
+ // paints, and `paint`/`pageSize` refuse only a page the count excludes.
64
66
  function render(pageCount, dirtyPages) {
65
- if (!session.supportsCanvas || pageCount === 0) {
66
- // A 0-page compile is a recoverable empty; pages the boundary cannot
67
- // raster are a genuine unsupported.
67
+ if (pageCount === 0) {
68
68
  paintLoop.refresh([], 0);
69
69
  detach();
70
- showMessage(pageCount === 0 ? t.noPages : t.unsupported, pageCount === 0 ? EMPTY_CLASS : UNSUPPORTED_CLASS);
70
+ showMessage(t.noPages, EMPTY_CLASS);
71
71
  return;
72
72
  }
73
73
  // Read the count against the current slots before reconcile moves it, and
@@ -27,8 +27,11 @@ export interface PdfPoint {
27
27
  * boundary character (`.` for a nested field, `[` for an array element) keeps the
28
28
  * prefix a path boundary: `main.keywords` matches `main.keywords[0]`, not
29
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.
30
33
  */
31
- export declare function boxesForField(field: string, boxes: readonly FieldRegion[], regions: readonly FieldRegion[]): readonly FieldRegion[];
34
+ export declare function boxesForField(field: string, boxes: readonly FieldRegion[], regions: () => readonly FieldRegion[]): readonly FieldRegion[];
32
35
  /**
33
36
  * Forward: a PDF-pt rect -> CSS % of the page box. Percent positioning tracks
34
37
  * the page's displayed size across DPR and container resize for free, so the
@@ -41,6 +44,14 @@ export declare function rectToPercent(rect: PdfRect, pageSize: PageSize): Percen
41
44
  * the transform that produces it rather than at the caller placing a marker.
42
45
  */
43
46
  export declare function applyPercentRect(el: HTMLElement, pct: PercentRect): void;
47
+ /**
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.
53
+ */
54
+ export declare function pxToPt(px: number, cssW: number, pageSize: PageSize): number;
44
55
  /**
45
56
  * Inverse: a click at CSS px `(px, py)` within a page element of CSS size
46
57
  * `(cssW, cssH)` -> PDF-pt. The exact inverse of {@link rectToPercent}: the
@@ -12,11 +12,14 @@
12
12
  * boundary character (`.` for a nested field, `[` for an array element) keeps the
13
13
  * prefix a path boundary: `main.keywords` matches `main.keywords[0]`, not
14
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.
15
18
  */
16
19
  export function boxesForField(field, boxes, regions) {
17
20
  if (boxes.length)
18
21
  return boxes;
19
- return regions.filter((r) => r.field === field || r.field.startsWith(`${field}.`) || r.field.startsWith(`${field}[`));
22
+ return regions().filter((r) => r.field === field || r.field.startsWith(`${field}.`) || r.field.startsWith(`${field}[`));
20
23
  }
21
24
  /**
22
25
  * Forward: a PDF-pt rect -> CSS % of the page box. Percent positioning tracks
@@ -47,6 +50,16 @@ export function applyPercentRect(el, pct) {
47
50
  height: `${pct.height}%`
48
51
  });
49
52
  }
53
+ /**
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.
59
+ */
60
+ export function pxToPt(px, cssW, pageSize) {
61
+ return (px / cssW) * pageSize.widthPt;
62
+ }
50
63
  /**
51
64
  * Inverse: a click at CSS px `(px, py)` within a page element of CSS size
52
65
  * `(cssW, cssH)` -> PDF-pt. The exact inverse of {@link rectToPercent}: the
@@ -41,7 +41,11 @@ onPaintError) {
41
41
  // responsibilities"). `border-box` costs nothing here and keeps a slot a
42
42
  // consumer insets from measuring wider than its paper.
43
43
  const el = document.createElement('div');
44
- el.className = 'qm-page';
44
+ // Not `.qm-page`: that name is the preset's page baseline, an unlayered rule at
45
+ // ordinary specificity a host puts on `<body>`, and a slot wearing it takes that
46
+ // rule's `color-scheme` inside a mounted root, which the derivation leaves to the
47
+ // host (`core/theme.css`).
48
+ el.className = 'qm-page-slot';
45
49
  // The page's index, in the DOM. `pageByEl` is this module's and a consumer
46
50
  // drawing an overlay of its own needs the number: position among siblings is
47
51
  // the only handle without it, which is right today and is not a contract.
@@ -157,9 +161,7 @@ onPaintError) {
157
161
  }
158
162
  // Slots are built on the first `refresh` (the controller calls it immediately),
159
163
  // not here: reconcile → `session.pageSize` must run after the controller's
160
- // `supportsCanvas`/`pageCount` gate, since a `supportsCanvas: false` compile with
161
- // pages would throw on `pageSize` (runtime.d.ts: it succeeds iff `supportsCanvas`)
162
- // before the controller can surface its "unsupported" message.
164
+ // `pageCount` gate, since `pageSize` throws for a page the count excludes.
163
165
  // ── Keep mounted rasters in step with the display ───────────────────────────
164
166
  // Every paint freezes `canvas.style.width/height` to the box width at that
165
167
  // paint (paintSlot). A page that stays mounted while the container's CSS width
@@ -3,8 +3,6 @@ export interface PreviewStrings {
3
3
  renderFailed: string;
4
4
  /** A compile with zero pages: recoverable, and the commonest state of a fresh seed. */
5
5
  noPages: string;
6
- /** A compile whose pages this backend cannot raster at all. */
7
- unsupported: string;
8
6
  }
9
7
  /** The package's English. Every key, so the merge below is total. */
10
8
  export declare const DEFAULT_PREVIEW_STRINGS: PreviewStrings;
@@ -1,5 +1,5 @@
1
- // Every word the preview says. Three, because the surface is a canvas and its
2
- // only prose is the message states that stand in when there is nothing to paint.
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.
3
3
  //
4
4
  // Separate from the visual surface's set rather than pooled with it: `/preview`
5
5
  // reaches `/core` and nothing editor-side, which is what lets it promote to its own
@@ -8,13 +8,12 @@
8
8
  // twice rather than learning two idioms.
9
9
  //
10
10
  // A plain options field rather than a context channel, unlike the visual surface's:
11
- // these three are read in one module by one function, so there is no tree to make
12
- // them ambient for.
11
+ // they are read in one module by one function, so there is no tree to make them
12
+ // ambient for.
13
13
  /** The package's English. Every key, so the merge below is total. */
14
14
  export const DEFAULT_PREVIEW_STRINGS = {
15
15
  renderFailed: 'Preview failed to render.',
16
- noPages: 'No pages to preview.',
17
- unsupported: 'Preview is not available for this document.'
16
+ noPages: 'No pages to preview.'
18
17
  };
19
18
  /** Fill a partial override to the whole key set. */
20
19
  export function mergePreviewStrings(custom) {