@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
@@ -101,11 +101,10 @@
101
101
  --_qm-font-mono: var(--qm-font-mono, ui-monospace, monospace);
102
102
 
103
103
  /* ── Colour ──────────────────────────────────────────────────────────────
104
- Ink steps fg → bg, the border and hover rungs step bg → fg, all mixed in
105
- oklab; sRGB muddies the mid-tones and the label/meta/border rungs are
106
- exactly mid-tone. So inverting the poles inverts that half of the scale.
107
- The percentages are calibrated to the light-mode literals they stand in
108
- for (17% ≈ #d4d4d4, 35% ≈ #555).
104
+ Ink steps fg → bg and the hover rung steps bg → fg, both mixed in oklab;
105
+ sRGB muddies the mid-tones and the label rung is exactly mid-tone. So
106
+ inverting the poles inverts that half of the scale. The percentages are
107
+ calibrated to the light-mode literals they stand in for (35% ≈ #555).
109
108
 
110
109
  Planes are the half that does not read the ink pole, because what makes a
111
110
  card read as a card is that it is the brighter plane, in both schemes
@@ -115,57 +114,68 @@
115
114
  light, and the base plane — a card, the preview's paper — is the pole
116
115
  itself.
117
116
 
118
- Two planes sink. `sunken` is the ground — the column behind the cards, the
119
- desk behind the sheets — and lifting the plane above it takes very little
120
- at the light pole. `well` is the hole cut into a card: a field, the one
121
- native select, neither of which draws an edge, so the step is the whole of
122
- what says a value is typed there. The order at the
123
- light pole is card, ground, well; at the dark pole the two sink to within
124
- a channel of each other, which is where a card with little light left to
125
- give up leaves them.
117
+ One plane sinks, in two roles. `well` is the hole cut into a card: a field,
118
+ the one native select, neither of which draws an edge, so the step is the
119
+ whole of what says a value is typed there. `sunken` is the ground — the
120
+ column behind the cards, the desk behind the sheets — and it is the same
121
+ tone: a card draws no edge either, so the step off the ground is the whole
122
+ of where one card stops and the next starts, and the well's step is one
123
+ that carries that at the light pole. The order is card, then ground
124
+ and well together, at both poles.
126
125
 
127
- How far each sinks is a function of the card's own L, not of the scheme the
126
+ How far it sinks is a function of the card's own L, not of the scheme the
128
127
  host declared: a dark surface has to spend more of what little it has
129
128
  before the card above it reads as lifted, so the two calibrated distances
130
- — 0.0214 off white, 0.0553 off the dark default — interpolate linearly
129
+ — 0.04 off white, 0.0547 off the dark default — interpolate linearly
131
130
  between. A card pinned at neither pole takes a proportionate step whatever
132
131
  `color-scheme` says, which is the half of the derivation that reads a
133
132
  colour rather than a declaration.
134
133
 
135
- Both sink by subtraction where every other rung mixes: a mix toward black
134
+ It sinks by subtraction where every other rung mixes: a mix toward black
136
135
  is a ratio, so it scales what it takes and pulls the result toward
137
136
  neutral, and a ground is a fixed distance. `a b` pass through, so the
138
137
  plane keeps the pole's hue and a turned `--qm-bg` tints it. The defaults
139
- land within a channel of `#f7f8f9` and `#1a1a1b`. Below L ≈ 0.065 the
140
- ground clamps to black, which is where a near-black card leaves no room
138
+ land within a channel of `#f2f2f2` and `#1a1a1c`. Below L ≈ 0.06 the
139
+ plane clamps to black, which is where a near-black card leaves no room
141
140
  for a ground of its own. */
142
141
  --_qm-surface: var(--_qm-bg);
143
- --_qm-surface-sunken: oklab(from var(--_qm-bg) calc(l - (0.0214 + 0.0467 * (1 - l))) a b);
144
142
  --_qm-surface-well: oklab(from var(--_qm-bg) calc(l - (0.04 + 0.0202 * (1 - l))) a b);
143
+ --_qm-surface-sunken: var(--_qm-surface-well);
145
144
  /* `raised` is the same ramp spelled as a mix, its step 0.08 × (1 − l): the light
146
145
  arm is zero, a white card having nowhere further to go, so a floating surface
147
146
  separates on its hairline and `--_qm-lift` there and the step joins them in
148
147
  dark. */
149
148
  --_qm-surface-raised: color-mix(in oklab, var(--_qm-bg), #fff 8%);
150
- --_qm-surface-hover: color-mix(in oklab, var(--_qm-bg), var(--_qm-fg) 8%);
151
- /* A band laid on the card rather than cut into it: a table's header, and the pill a
152
- grip draws. It steps toward the ink at both poles, a shade under white in light
153
- and a shade over the card in dark, where `well` sinks to a black stripe. */
154
- --_qm-surface-band: color-mix(in oklab, var(--_qm-bg), var(--_qm-fg) 6%);
155
- /* The one rung whose arms are not a single ratio: a stroke is read against what it
156
- separates, and the mix that reads as a hairline on the dark default draws the
157
- light card in a line heavier than the type it frames. The light arm is
158
- calibrated rather than derived, which makes this the one rung under the poles
159
- that reads the declared scheme rather than only the colours it resolves to. */
149
+ /* A step toward the ink laid over whatever plane is under it, rather than a tone off
150
+ the card: a hover lands on the card, a well, a band and a floating surface, and a
151
+ fixed tone stepped off the first sits level with `raised` in dark. Translucent, as
152
+ the accent's tint is, so the step is the same over each. */
153
+ --_qm-surface-hover: color-mix(in oklab, var(--_qm-fg) 8%, transparent);
154
+ /* A band laid on the card rather than cut into it: a table's header, the pill a
155
+ grip draws, the off switch's track and the unticked box. A resting fill, where
156
+ `hover` above is a state, so the two have to be told apart on a card that shows
157
+ both at once: each arm stands one ground step past the hover step at its pole,
158
+ which lands the band two steps off the card in dark and a little over in light.
159
+ It steps toward the ink at both poles, a shade under white in light and a shade
160
+ over the card in dark, where `well` sinks to a black stripe. */
161
+ --_qm-surface-band: light-dark(
162
+ color-mix(in oklab, var(--_qm-bg), var(--_qm-fg) 12%),
163
+ color-mix(in oklab, var(--_qm-bg), var(--_qm-fg) 18%)
164
+ );
165
+ /* The one stroke: a card's two horizontals, a section's and a subform's verticals,
166
+ a table's frame and the lines between its cells, a floating surface's edge, the
167
+ popover's divider. One and a half ground steps off the card at both poles, the
168
+ same derivation the well takes, so a hairline stands off the card by the same
169
+ measure a card stands off the ground whatever the scheme. Toward the ink, which
170
+ is the half of the step `light-dark()` carries: the well always sinks, a stroke
171
+ darkens a light card and lightens a dark one. On the defaults the arms land near
172
+ #ececec and #3b3c3e. The multiple is the whole calibration: at it a stroke reads
173
+ in light without outweighing the type beside it, and reads in dark without being
174
+ the loudest line on the card. */
160
175
  --_qm-border: light-dark(
161
- color-mix(in oklab, var(--_qm-bg), var(--_qm-fg) 6%),
162
- color-mix(in oklab, var(--_qm-bg), var(--_qm-fg) 17%)
176
+ oklab(from var(--_qm-bg) calc(l - 1.5 * (0.04 + 0.0202 * (1 - l))) a b),
177
+ oklab(from var(--_qm-bg) calc(l + 1.5 * (0.04 + 0.0202 * (1 - l))) a b)
163
178
  );
164
- /* A stroke ruled through content rather than round it: a table's frame and every
165
- line between its cells. The light arm of `border` is a card's edge against the
166
- ground, and drawn between lines of type it vanishes, so a grid takes one ratio at
167
- both poles, which lands within a few points of `border` in dark. */
168
- --_qm-rule: color-mix(in oklab, var(--_qm-bg), var(--_qm-fg) 14%);
169
179
  /* Three rungs, and nothing under the last: a step below `label` lands under 3:1 on
170
180
  the well a field draws it against, and what would take it — an unset value's word,
171
181
  an invitation — is content a reader has to read (WCAG 1.4.3), not decoration.
@@ -259,7 +269,7 @@
259
269
  only up to the point where it is assembled. One width: focus is the only
260
270
  state that draws a ring anywhere, so there is no held-back tier under it.
261
271
  Sub-pixel, because at two it reads as a second border on a card whose every
262
- other edge is a hairline. */
272
+ other stroke is a hairline. */
263
273
  --_qm-ring-width: 1.5px;
264
274
  /* Every ring the surface draws, focus and selection alike, which is why the name
265
275
  carries neither. Assembled here because the parts spelled out at a use site pass
@@ -296,17 +306,17 @@
296
306
 
297
307
  /* ── Geometry ──────────────────────────────────────────────────────────*/
298
308
  /* One stroke width for every edge the chrome draws and every rule it ranks
299
- with: a card's, a control's, the two closing a card's payload. A threshold
300
- rather than a rhythm choice, like the tap floor: a hairline is a hairline at
301
- any `--qm-space`, so neither derives from a dial. Both exist as rungs because
309
+ with: a control's, a floating surface's, the two closing a card's payload. A
310
+ threshold rather than a rhythm choice, like the tap floor: a hairline is a
311
+ hairline at any `--qm-space`, so neither derives from a dial. Both exist as rungs because
302
312
  `check:style`'s border-width axis can only hold a stroke against a token. */
303
313
  --_qm-border-width: 1px;
304
314
  /* Half a step over it, for the verticals alone — a section's, a subform's, a
305
315
  blockquote's. What a vertical states is depth, and depth is the one thing a
306
- reader has to find in a dense card; the strokes that close a block or rank two
307
- stay at the hairline, so the ladder is what carries the extra weight. Half,
308
- because a whole step reads as a different kind of line rather than the same
309
- one doing more work. */
316
+ reader has to find in a dense card; the strokes that rank two or edge a
317
+ floating surface stay at the hairline, so the ladder is what carries the extra
318
+ weight. Half, because a whole step reads as a different kind of line rather
319
+ than the same one doing more work. */
310
320
  --_qm-vertical-width: 1.5px;
311
321
  --_qm-radius: var(--qm-radius, 8px);
312
322
  /* The interior corner, and half is the relationship rather than the number: a
@@ -342,6 +352,15 @@
342
352
  `check:style`'s rhythm axis sees each of them read a rung, never that they read
343
353
  the same one — the case `--_qm-inset-control` above is cut for. */
344
354
  --_qm-nest: var(--_qm-space-3);
355
+ /* The numerals a list marker is given room for: two digits, so a tenth item's number
356
+ stands inside the leaf as the first's does. In `ch`, so it tracks the face the
357
+ numbers are set in. */
358
+ --_qm-marker-digits: 2ch;
359
+ /* The text column, measured in from a block's own inline inset: a section's vertical
360
+ plus the rung its heading stands off it, so a heading riding that vertical and a
361
+ line beside it start on one edge. A rung for the reason the nesting step is: the
362
+ card and the tip tucked under `main` are two components holding one edge. */
363
+ --_qm-column: calc(var(--_qm-space) + var(--_qm-vertical-width));
345
364
  /* The scroll tail: dead space under the last card, so it can be read at the
346
365
  middle of a pane rather than at its bottom edge. A fraction of the viewport
347
366
  rather than a step on the ramp, because what it answers is how far the eye
@@ -358,6 +377,10 @@
358
377
  rather than through the icon's `size`, so a stylesheet beats the SVG presentation
359
378
  attribute and one rung is the whole of what a control draws. */
360
379
  --_qm-glyph-control: 14px;
380
+ /* The stroke a control glyph draws, in screen length: the frame's 2 units of 24 at
381
+ the control rung. A glyph drawn at another edge reads it under
382
+ `vector-effect: non-scaling-stroke`, so every glyph strokes at one weight. */
383
+ --_qm-glyph-stroke: calc(var(--_qm-glyph-control) / 12);
361
384
  /* The narrowest track a control that fills its width stays usable in: a table's
362
385
  column floors at it, rises no further than its content, and a row of them scrolls
363
386
  sideways rather than crushing a cell. A column drawing a mark takes no floor — a
@@ -374,22 +397,19 @@
374
397
  --_qm-track-prose: calc(var(--_qm-track-min) * 2);
375
398
 
376
399
  /* ── Type ────────────────────────────────────────────────────────────────
377
- `--qm-font-size` anchors the chrome rung; 1.125 is the ratio between adjacent
378
- rungs: a step up (title) and one down (label). The ratio and the
379
- weight are fixed conventions, not dials: no consumer tunes a typographic
380
- ratio, and a dial nobody turns is a name every reader still holds. Three chrome
381
- rungs: a second step below `label` lands near 11px, under a size anything here
382
- wants to be read at, and the supporting text that would take it is what `label`
383
- already is. */
400
+ `--qm-font-size` anchors the chrome rung and `label` stands one 1.125 step under
401
+ it. The ratio and the weight are fixed conventions, not dials: no consumer tunes
402
+ a typographic ratio, and a dial nobody turns is a name every reader still holds.
403
+ Two chrome rungs: nothing the chrome names ranks above the value it names, a card
404
+ title being body at the mid weight, and a second step below `label` lands near
405
+ 11px, under a size anything here wants to be read at. */
384
406
  --_qm-text-body: var(--qm-font-size, 0.875rem);
385
- --_qm-text-title: calc(var(--_qm-text-body) * 1.125);
386
407
  --_qm-text-label: calc(var(--_qm-text-body) / 1.125);
387
408
  /* Paper: the document body answers to the preview's paper where every other
388
409
  surface here answers to the control beside it, so a rung shared with the chrome
389
410
  makes a consumer asking for larger prose take larger inputs with it. 16/15 where
390
411
  the ramp steps at 9/8: small enough to read as a change of register rather than
391
- a rung of hierarchy, and clear of the title rung, which one 9/8 step lands on
392
- exactly. */
412
+ a rung of hierarchy. */
393
413
  --_qm-text-paper: calc(var(--_qm-text-body) * 16 / 15);
394
414
  /* Two weights over the regular, and only one of them is chrome. `mid` names
395
415
  everything the surface names — a card title, a section header, a field
package/dist/index.js CHANGED
@@ -1,9 +1,4 @@
1
- // `@quillmark/svelte` root: the substrate, free of any UI framework.
2
- //
3
- // The headline surfaces mount from their own subpaths (`@quillmark/svelte/preview`,
4
- // `/visual`) so a bundler pulls only what an entry reaches; the root
5
- // re-exports `/core` (the address vocabulary, the error channel, `init`) for a
6
- // consumer that wants one import for the substrate. It carries no framework code.
7
- // The `@quillmark/wasm` API is imported from the peer dependency directly, never
8
- // from here.
1
+ // `@quillmark/svelte` root: the substrate, free of any UI framework. The headline surfaces mount
2
+ // from their own subpaths (`/preview`, `/visual`); the root re-exports `/core` for a consumer that
3
+ // wants one import for the substrate.
9
4
  export * from './core/index.js';
@@ -27,9 +27,11 @@
27
27
  overflow-wrap: anywhere;
28
28
  }
29
29
 
30
- /* The foot: a line of meta type under a hairline, its links in the line's own ink. */
30
+ /* The foot: a line of meta type on the head's plate under a hairline, its links in the
31
+ line's own ink. The two bands framing the panes stand on one plate. */
31
32
  :where(.qm-app-footer) {
32
33
  padding: var(--qmh-space-2) var(--qmh-space-4);
34
+ background: var(--qmh-surface);
33
35
  border-block-start: var(--qmh-border-width) solid var(--qmh-border);
34
36
  font-size: var(--qmh-text-meta);
35
37
  line-height: var(--qmh-leading-tight);
@@ -1,11 +1,9 @@
1
- /* `@quillmark/svelte/preset`, the package's one CSS entry point: the endorsed look for
2
- the page a surface is mounted on. Opt-in and side-effect free until imported:
3
- the surface barrels pull `core/theme.css` and never this, because this
4
- styles the document and nothing may put that in a consumer's page uninvited.
5
-
6
- `scale.css` is where the literals are minted and is exempt; the rest are held to
7
- reading them. `recipes.css` ranks at ordinary specificity, `shell.css` and `app.css`
8
- at zero. */
1
+ /*
2
+ * `@quillmark/svelte/preset`, the package's one CSS entry point: the endorsed look for the page a
3
+ * surface is mounted on. Opt-in: the surface barrels pull `core/theme.css` and never this, because
4
+ * this styles the document. `scale.css` is where the literals are minted; the rest read them.
5
+ * `recipes.css` ranks at ordinary specificity, `shell.css` and `app.css` at zero.
6
+ */
9
7
 
10
8
  @import './scale.css';
11
9
  @import './recipes.css';
@@ -1,18 +1,15 @@
1
- /* The endorsed look: the rules a host draws around a mounted surface, as classes it puts
2
- on its own elements; the shell it mounts one in is `shell.css` beside this, and an
3
- app's own chrome is `app.css`. The rungs beside this in
4
- `scale.css` are the invisible half (a scale alone still leaves a consumer writing
5
- every declaration before anything on the page looks like anything), so the recipes
6
- are what the preset is for. This file mints nothing; `check:style` runs its axes here.
7
-
8
- These are host classes. They land on the consumer's DOM, never on a mounted root, so
9
- they freeze nothing about the surfaces' interior: the reason the package withholds
10
- its own internal classes does not reach them.
11
-
12
- Unlayered, deliberately: a layer of the preset's own would put a name in the contract,
13
- and unlayered a consumer's rules win over these by ordinary precedence. Ordinary
14
- specificity too, not the surfaces' `:where()` zero: a consumer takes these classes on
15
- purpose, so a rule of their own beside one is aimed rather than incidental. */
1
+ /*
2
+ * The endorsed look: the rules a host draws around a mounted surface, as classes it puts on its
3
+ * own elements. The shell is `shell.css` and an app's chrome `app.css`; the rungs in `scale.css`
4
+ * are the invisible half. This file mints nothing; `check:style` runs its axes here.
5
+ *
6
+ * These are host classes: they land on the consumer's DOM, never on a mounted root, so they freeze
7
+ * nothing about the surfaces' interior.
8
+ *
9
+ * Unlayered, at ordinary specificity (not the surfaces' `:where()` zero): a consumer takes these
10
+ * classes on purpose, so a rule of their own beside one is aimed rather than incidental, and a
11
+ * layer of the preset's own would put a name in the contract.
12
+ */
16
13
 
17
14
  /* ── The page ──────────────────────────────────────────────────────────────── */
18
15
 
@@ -1,13 +1,13 @@
1
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, the
5
- ring's width, the status hue at both poles) and the
4
+ figures here are the figures there (the poles, the ground step's ramp, 8% hover, the
5
+ label ink's 35%, the 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
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
+ is calibrated for its own plane rather than shared: a page's controls are boxed and
9
+ read against one ground, a card's strokes at several weights against the rungs
10
+ under it, so the mixes are not the same figure. The well
11
11
  under the ground has no counterpart here: a host's own controls are boxed and carry a
12
12
  hairline, so nothing on a page needs the step an edgeless field does.
13
13
 
@@ -20,9 +20,8 @@
20
20
  Literals live here and nowhere else in the preset: `check:style` runs its axes over
21
21
  the sheets beside it, with this file exempt as the place the defaults are minted.
22
22
 
23
- The poles fall back to the system colours, so a host that turns no dial still lands
24
- where its own unstyled text does — `Canvas` being the plane the UA would have
25
- painted a page with, which is what a plate should be. The dials sit in front, so
23
+ The poles fall back to the surfaces' own pair, so a host that turns no dial lands on
24
+ the plane an editor mounted in it paints at both poles. The dials sit in front, so
26
25
  `--qm-bg`/`--qm-fg` on an ancestor retunes page and surface together. */
27
26
 
28
27
  :root {
@@ -32,21 +31,20 @@
32
31
  the page sinks from it by the same ramp. That is what puts a host page and
33
32
  an editor's own column on one tone rather than a plane apart. Planes step by
34
33
  light and everything else by ink, which is the package's own split
35
- (ARCHITECTURE §Styling); the figures are its figures. `Canvas` flips with the
36
- declared scheme on its own, so the ramp reads whichever plane it resolved to
37
- rather than trusting an arm to match. */
38
- --qmh-surface: var(--qm-bg, Canvas);
39
- --qmh-page: oklab(from var(--qmh-surface) calc(l - (0.0214 + 0.0467 * (1 - l))) a b);
40
- --qmh-ink: var(--qm-fg, CanvasText);
34
+ (ARCHITECTURE §Styling); the figures are its figures. */
35
+ --qmh-surface: var(--qm-bg, light-dark(#fff, #272729));
36
+ --qmh-page: oklab(from var(--qmh-surface) calc(l - (0.04 + 0.0202 * (1 - l))) a b);
37
+ --qmh-ink: var(--qm-fg, light-dark(#1c1c1c, #d7dadc));
41
38
  /* One step off the ink and no second: a page's chrome type is small, tracked and
42
39
  uppercase, so a rung below this reads under 4.5:1 against the plate and the
43
40
  label and status lines that would take it are text (WCAG 1.4.3). */
44
- --qmh-ink-meta: color-mix(in oklab, var(--qmh-ink), var(--qmh-surface) 45%);
41
+ --qmh-ink-meta: color-mix(in oklab, var(--qmh-ink), var(--qmh-surface) 35%);
45
42
  --qmh-surface-hover: color-mix(in oklab, var(--qmh-surface), var(--qmh-ink) 8%);
46
43
  --qmh-border: color-mix(in oklab, var(--qmh-surface), var(--qmh-ink) 17%);
47
44
  /* The edge that says "engaged" where the hairline says "edge": a pressed switch,
48
- the underline under a link. */
49
- --qmh-border-strong: color-mix(in oklab, var(--qmh-surface), var(--qmh-ink) 40%);
45
+ the underline under a link. Half way to the ink, which clears WCAG 1.4.11's 3:1
46
+ against the page and the plate at both poles. */
47
+ --qmh-border-strong: color-mix(in oklab, var(--qmh-surface), var(--qmh-ink) 50%);
50
48
 
51
49
  /* The one hue a host's page spends on purpose: the primary action wears it. The
52
50
  figures are the surfaces' own default for the same dial, so a page that turns no
@@ -62,6 +60,9 @@
62
60
  on a dark page. The dial sits in front of the pair, as every pole's does, so
63
61
  a host that turns one owns both schemes. */
64
62
  --qmh-alert: var(--qm-danger, light-dark(#9e1518, #f9786c));
63
+ /* What a modal's backdrop lays over the page: the ground held translucent, so the
64
+ page recedes under the dialog without a second tone to read. */
65
+ --qmh-scrim: color-mix(in srgb, var(--qmh-page) 80%, transparent);
65
66
 
66
67
  /* ── Rhythm ──────────────────────────────────────────────────────────────── */
67
68
  /* One base and its multiples, the same four steps the surfaces derive. A host
@@ -145,3 +145,13 @@
145
145
  border-radius: var(--qmh-radius);
146
146
  overflow: hidden;
147
147
  }
148
+
149
+ /* One track is the whole width under the threshold, so a frame's sides and corners would
150
+ frame the screen rather than a pane. It keeps the rules above and below it, which part
151
+ it from the bands. After the frame, since both rank zero and the later one wins. */
152
+ @media (width < 50rem), (height < 30rem) {
153
+ :where(.qm-frame) {
154
+ border-inline: none;
155
+ border-radius: 0;
156
+ }
157
+ }
@@ -1,15 +1,18 @@
1
1
  import type { LiveSession } from '@quillmark/wasm';
2
2
  import type { Landing } from '../core/address.js';
3
3
  import type { PageSlot } from './paint.js';
4
- /** Both trips are held while the pane has no box to measure against, and the last one
5
- * held runs when it has one again. */
4
+ /**
5
+ * Both trips are held while the pane has no box to measure against; the last one held runs when it
6
+ * has one again.
7
+ */
6
8
  export interface BridgeController {
7
- /** Scroll `field`'s first box to centre; `false` when this compile places none
8
- * for that address, which is the whole of what the preview can say about it. */
9
+ /**
10
+ * Scroll `field`'s first box to centre; `false` when this compile places none for that address.
11
+ */
9
12
  scrollToField(field: string): boolean;
10
13
  /**
11
- * Bring the caret rect at `field`/`pos` into view (a no-op off-content, and a
12
- * no-op when it is already clear of the fold).
14
+ * Bring the caret rect at `field`/`pos` into view; a no-op off-content or when already clear of
15
+ * the fold.
13
16
  */
14
17
  focusPosition(field: string, pos: number): void;
15
18
  destroy(): void;
@@ -21,24 +24,17 @@ export interface VerticalSpan {
21
24
  height: number;
22
25
  }
23
26
  /**
24
- * Whether `target` sits clear of `port`'s edges by the fold's clearance, which is the
25
- * target's own height: a caret rect flush against an edge is visible and unusable, and
26
- * the next line typed lands past it. Derived from the target rather than a margin dial,
27
- * so it scales with zoom exactly as the caret does, and bounded at both ends so the
28
- * answer is reachable across the whole range of targets a compile locates.
29
- *
30
- * The floor is what a zero-height rect gets, which bare intersection would score as
31
- * clear at either edge. The cap is a third of the room the port has to spare, so a
32
- * target too tall for its own height of clearance — a located line at zoom, a short
33
- * split track — passes once centred and holds a band around it, rather than being
34
- * re-centred by every keystroke. A target taller than the port is clear while it covers
35
- * the port: no scroll shows more of it.
27
+ * Whether `target` sits clear of `port`'s edges by the fold's clearance, which is the target's own
28
+ * height, so it scales with zoom as the caret does. The floor is {@link MIN_CLEARANCE}, for a
29
+ * zero-height rect. The cap is a third of the room the port has to spare, so a target too tall for
30
+ * its own clearance passes once centred rather than being re-centred by every keystroke. A target
31
+ * taller than the port is clear while it covers the port.
36
32
  */
37
33
  export declare function clearOfTheFold(target: VerticalSpan, port: VerticalSpan): boolean;
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`). */
34
+ /**
35
+ * The last trip a pane with no box could not run. A cell so a rebuilt bridge inherits it
36
+ * (`controller.ts`).
37
+ */
42
38
  export interface HeldTrip {
43
39
  run?: () => void;
44
40
  }
@@ -1,32 +1,24 @@
1
1
  import { boxesForField, rectToPercent, clickToPdfPt, pxToPt, applyPercentRect } from './geometry.js';
2
- /** A pane hidden by the shell's narrow switch (`preset/shell.css`) measures 0×0, and so
3
- * does every box on it: the clearance arithmetic reads that as a caret clear of both
4
- * edges, and a centring computes its trip out of zeros. Neither is an answer about where
5
- * the caret is. */
2
+ /**
3
+ * A pane hidden by the shell's narrow switch (`preset/shell.css`) measures 0×0, as does every box
4
+ * on it, which the clearance arithmetic would read as a caret clear of both edges.
5
+ */
6
6
  function hasBox(port) {
7
7
  return port.width > 0 && port.height > 0;
8
8
  }
9
- /** What a rect reporting no height clears an edge by, in CSS px: a caret's own at the
10
- * smallest zoom a page still reads at. */
9
+ /** What a rect reporting no height clears an edge by, in CSS px. */
11
10
  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. */
11
+ /**
12
+ * What a click may miss a field's ink by and still land on it, in CSS px of the page as drawn
13
+ * (PREVIEW.md §"Click bridge").
14
+ */
16
15
  const CLICK_SLACK_PX = 2;
17
16
  /**
18
- * Whether `target` sits clear of `port`'s edges by the fold's clearance, which is the
19
- * target's own height: a caret rect flush against an edge is visible and unusable, and
20
- * the next line typed lands past it. Derived from the target rather than a margin dial,
21
- * so it scales with zoom exactly as the caret does, and bounded at both ends so the
22
- * answer is reachable across the whole range of targets a compile locates.
23
- *
24
- * The floor is what a zero-height rect gets, which bare intersection would score as
25
- * clear at either edge. The cap is a third of the room the port has to spare, so a
26
- * target too tall for its own height of clearance — a located line at zoom, a short
27
- * split track — passes once centred and holds a band around it, rather than being
28
- * re-centred by every keystroke. A target taller than the port is clear while it covers
29
- * the port: no scroll shows more of it.
17
+ * Whether `target` sits clear of `port`'s edges by the fold's clearance, which is the target's own
18
+ * height, so it scales with zoom as the caret does. The floor is {@link MIN_CLEARANCE}, for a
19
+ * zero-height rect. The cap is a third of the room the port has to spare, so a target too tall for
20
+ * its own clearance passes once centred rather than being re-centred by every keystroke. A target
21
+ * taller than the port is clear while it covers the port.
30
22
  */
31
23
  export function clearOfTheFold(target, port) {
32
24
  if (target.height >= port.height)
@@ -36,19 +28,13 @@ export function clearOfTheFold(target, port) {
36
28
  }
37
29
  export function createBridge(session, container, slots, onPick, held = {}) {
38
30
  const unlisten = [];
39
- // Two rungs: `positionAt` over span-tracked content, `fieldAt` over every placement
40
- // the compile tracks, a strict superset of it (PREVIEW.md §"Click bridge" carries
41
- // the measurement). The second fires where a field is placed without its content
42
- // being tracked, and the pick carries no `pos` there: a fabricated `0` would be an
43
- // invented offset wearing a real one's type. No third rung hit-tests `regions()`,
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.
31
+ // Two rungs: `positionAt` over span-tracked content, then `fieldAt` over every tracked
32
+ // placement (PREVIEW.md §"Click bridge"). The second carries no `pos`: a fabricated `0` would
33
+ // be an invented offset.
46
34
  if (onPick) {
47
35
  for (const { page, el } of slots) {
48
36
  const handleClick = (ev) => {
49
- // Re-read through the live array: a same-count `refresh` swaps the
50
- // slot objects to re-cache `size`, and the click math must use the
51
- // current compile's PageSize, not the one captured at bridge build.
37
+ // Re-read through the live array: a refresh swaps slot objects to re-cache `size`.
52
38
  const slot = slots[page];
53
39
  if (!slot)
54
40
  return;
@@ -68,9 +54,8 @@ export function createBridge(session, container, slots, onPick, held = {}) {
68
54
  unlisten.push(() => el.removeEventListener('click', handleClick));
69
55
  }
70
56
  }
71
- // Where the % rect `pct` on `slot` currently sits, in the scrollport's own
72
- // coordinates: a throwaway marker, measured and removed. Absolutely positioned,
73
- // so it displaces nothing and no layout survives the read.
57
+ // Where the % rect `pct` on `slot` currently sits: a throwaway absolute marker, measured and
58
+ // removed.
74
59
  function measure(slot, pct) {
75
60
  const marker = document.createElement('div');
76
61
  applyPercentRect(marker, pct);
@@ -79,11 +64,7 @@ export function createBridge(session, container, slots, onPick, held = {}) {
79
64
  marker.remove();
80
65
  return rect;
81
66
  }
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.
67
+ // Centre `target` vertically in `port`; the cross axis takes the minimum trip.
87
68
  function centre(target, port) {
88
69
  container.scrollTop += target.top + target.height / 2 - (port.top + port.height / 2);
89
70
  reveal(target, port);
@@ -94,10 +75,8 @@ export function createBridge(session, container, slots, onPick, held = {}) {
94
75
  else if (target.right > port.right)
95
76
  container.scrollLeft += target.right - port.right;
96
77
  }
97
- // The re-assert. Nothing else asks on the way back: `refresh` re-asks only on a
98
- // recompile, and the reader who has just tapped Preview is not typing, so the pane
99
- // would open where the caret was several edits ago. Guarded for jsdom, which ships no
100
- // ResizeObserver and lays out nothing for one to report.
78
+ // Runs the held trip once the pane has a box again. Guarded for jsdom, which has no
79
+ // ResizeObserver.
101
80
  if (typeof ResizeObserver !== 'undefined') {
102
81
  const observer = new ResizeObserver(() => {
103
82
  if (!held.run || !hasBox(container.getBoundingClientRect()))
@@ -117,14 +96,12 @@ export function createBridge(session, container, slots, onPick, held = {}) {
117
96
  if (!slot)
118
97
  return false;
119
98
  const port = container.getBoundingClientRect();
120
- // The address is placed, which is the whole of what the `boolean` answers; a pane
121
- // with no box yet is not a second no.
99
+ // Placed, so `true`; a pane with no box yet is not a no.
122
100
  if (!hasBox(port)) {
123
101
  held.run = () => void scrollToField(field);
124
102
  return true;
125
103
  }
126
- // A discrete act ("show me this field"), so it centres every time, unlike
127
- // the continuous hop below.
104
+ // A discrete act, so it centres every time.
128
105
  centre(measure(slot, rectToPercent(box.rect, slot.size)), port);
129
106
  return true;
130
107
  }
@@ -135,21 +112,15 @@ export function createBridge(session, container, slots, onPick, held = {}) {
135
112
  const slot = slots[region.page];
136
113
  if (!slot)
137
114
  return;
138
- // Held before the guard below, which a pane measuring zero answers "already clear"
139
- // for every keystroke of (`hasBox`): the follow would die silently for as long as
140
- // a track is hidden. What is held is the request, not the trip, so it re-locates
141
- // against whatever compile is current when the pane comes back.
115
+ // Held before the fold test, which would call a zero-sized pane clear. The request is held,
116
+ // so it re-locates against the compile current on return.
142
117
  const port = container.getBoundingClientRect();
143
118
  if (!hasBox(port)) {
144
119
  held.run = () => focusPosition(field, pos);
145
120
  return;
146
121
  }
147
- // The continuous hop: one call per keystroke and per arrow key, so it moves
148
- // the pane only when the caret has left the fold. Centring unconditionally
149
- // takes the scrollport back from the user on every one of them, including
150
- // the ones that changed nothing about where the caret already was.
151
- // Across, a page zoomed wider than the pane has a caret to keep in view too, and
152
- // the least trip does it without moving the line the reader is on.
122
+ // Called per keystroke, so it moves the pane only when the caret has left the fold; across,
123
+ // the least trip.
153
124
  const target = measure(slot, rectToPercent(region.rect, slot.size));
154
125
  if (clearOfTheFold(target, port))
155
126
  reveal(target, port);