@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
@@ -1,45 +1,40 @@
1
1
  import type { PageSize } from '@quillmark/wasm';
2
- /** CSS px per PDF pt at 100%: 96 CSS px to the inch over 72 pt, a PDF viewer's actual
3
- * size. */
2
+ /** CSS px per PDF pt at 100%: 96 CSS px to the inch over 72 pt. */
4
3
  export declare const ACTUAL: number;
5
4
  /** The furthest a gesture zooms in. */
6
5
  export declare const MAX_ZOOM = 2;
7
- /** What `Mod +` and `Mod −` step through: a subset of the levels a browser's own zoom
8
- * keys land on, so a step lands somewhere a reader has been before. */
6
+ /** What `Mod +` and `Mod −` step through. */
9
7
  export declare const STEPS: readonly [0.67, 0.75, 0.9, 1, 1.1, 1.25, 1.5, 1.75, 2];
10
8
  /** The pane's content box, in CSS px. */
11
9
  export interface Pane {
12
10
  width: number;
13
11
  height: number;
14
12
  }
15
- /** The default: the widest page fits the pane's width, capped at {@link FIT_CAP}. A
16
- * portrait-only document lands at the pane's width, as a narrow pane always drew it. */
13
+ /** The default: the widest page fits the pane's width, capped at {@link FIT_CAP}. */
17
14
  export declare function fitZoom(pages: readonly PageSize[], pane: Pane): number;
18
- /** The furthest a gesture zooms out: every page fits the pane whole with
19
- * {@link FLOOR_FILL} of it filled on the page's limiting axis, and never past the
20
- * default, which on a pane past the cap is already smaller than that. */
15
+ /**
16
+ * The furthest a gesture zooms out: every page fits the pane whole with {@link FLOOR_FILL} filled
17
+ * on its limiting axis, and never past the default.
18
+ */
21
19
  export declare function floorZoom(pages: readonly PageSize[], pane: Pane): number;
22
20
  export declare function clampZoom(zoom: number, floor: number): number;
23
- /** The next level in `dir` from `zoom`, `fit` among them, skipping levels under `floor`.
24
- * From between two levels the first press lands on the nearer one in that direction;
25
- * past the last level above the floor, the floor. Half a percent of slack, so a zoom
26
- * shown as a level is at it. */
21
+ /**
22
+ * The next level in `dir` from `zoom`, skipping levels under `floor`; past the last, the floor.
23
+ * Half a percent of slack, so a zoom shown as a level is at it.
24
+ */
27
25
  export declare function stepZoom(zoom: number, dir: 1 | -1, floor: number, fit: number): number;
28
26
  /**
29
- * A continuous gesture's run: the zoom its events have asked for (`raw`), what it shows,
30
- * and, while it shows a hold, how far past the hold the events have carried (`slack`, a
31
- * log of the ratio). The run moves `shown` by each event's ratio rather than snapping
32
- * `raw`, so a trackpad's small deltas carry it out of a hold as smoothly as they carried
33
- * it in.
27
+ * A continuous gesture's run: the zoom its events asked for (`raw`), what it shows, and while it
28
+ * shows a hold, how far past it the events have carried (`slack`, a log of the ratio).
34
29
  */
35
30
  export interface Glide {
36
31
  raw: number;
37
32
  shown: number;
38
33
  slack: number;
39
34
  }
40
- /** One event of a continuous gesture, carrying the run to `raw`. It stops at `fit` or
41
- * 100% when it crosses one, at the first it meets, so one wheel notch across either
42
- * stops there; it leaves a hold once carried {@link DETENT} past, moving on from the
43
- * hold rather than jumping to where the events would have put it. 100% gives way to a
44
- * `fit` within the detent of it: two holds that close would pin the run between them. */
35
+ /**
36
+ * One event of a continuous gesture, carrying the run to `raw`. It stops at the first of `fit` or
37
+ * 100% it crosses, and leaves a hold once carried {@link DETENT} past. 100% gives way to a `fit`
38
+ * within the detent of it.
39
+ */
45
40
  export declare function glide(run: Glide, raw: number, floor: number, fit: number): Glide;
@@ -1,30 +1,31 @@
1
- /** CSS px per PDF pt at 100%: 96 CSS px to the inch over 72 pt, a PDF viewer's actual
2
- * size. */
1
+ /** CSS px per PDF pt at 100%: 96 CSS px to the inch over 72 pt. */
3
2
  export const ACTUAL = 96 / 72;
4
- /** The default never draws a page larger than this, so a wide pane shows a sheet at a
5
- * size print would recognise rather than one stretched to the pane. */
3
+ /** The default never draws a page larger than this. */
6
4
  const FIT_CAP = 1.25;
7
5
  /** The furthest a gesture zooms in. */
8
6
  export const MAX_ZOOM = 2;
9
- /** How much of the pane the page still fills, on its limiting axis, at the furthest a
10
- * gesture zooms out: the whole page in view with a thin margin, and never a stamp. */
7
+ /**
8
+ * How much of the pane the page still fills, on its limiting axis, at the furthest a gesture zooms
9
+ * out.
10
+ */
11
11
  const FLOOR_FILL = 0.9;
12
- /** What `Mod +` and `Mod −` step through: a subset of the levels a browser's own zoom
13
- * keys land on, so a step lands somewhere a reader has been before. */
12
+ /** What `Mod +` and `Mod −` step through. */
14
13
  export const STEPS = [0.67, 0.75, 0.9, 1, 1.1, 1.25, 1.5, 1.75, 2];
15
- /** How far, as a log of the zoom's ratio to a hold, a continuous gesture carries past it
16
- * before it leaves it: a ratio, so a hold is as wide at a phone's fit as at 100%. */
14
+ /**
15
+ * How far, as a log of the zoom's ratio to a hold, a continuous gesture carries past it before it
16
+ * leaves it.
17
+ */
17
18
  const DETENT = 0.04;
18
19
  const near = (zoom, hold) => Math.abs(Math.log(zoom / hold)) <= DETENT;
19
- /** The default: the widest page fits the pane's width, capped at {@link FIT_CAP}. A
20
- * portrait-only document lands at the pane's width, as a narrow pane always drew it. */
20
+ /** The default: the widest page fits the pane's width, capped at {@link FIT_CAP}. */
21
21
  export function fitZoom(pages, pane) {
22
22
  const widest = Math.max(...pages.map((p) => p.widthPt));
23
23
  return Math.min(pane.width / (widest * ACTUAL), FIT_CAP);
24
24
  }
25
- /** The furthest a gesture zooms out: every page fits the pane whole with
26
- * {@link FLOOR_FILL} of it filled on the page's limiting axis, and never past the
27
- * default, which on a pane past the cap is already smaller than that. */
25
+ /**
26
+ * The furthest a gesture zooms out: every page fits the pane whole with {@link FLOOR_FILL} filled
27
+ * on its limiting axis, and never past the default.
28
+ */
28
29
  export function floorZoom(pages, pane) {
29
30
  const whole = Math.min(...pages.map((p) => Math.min(pane.width / p.widthPt, pane.height / p.heightPt)));
30
31
  return Math.min((FLOOR_FILL * whole) / ACTUAL, fitZoom(pages, pane));
@@ -32,16 +33,16 @@ export function floorZoom(pages, pane) {
32
33
  export function clampZoom(zoom, floor) {
33
34
  return Math.min(MAX_ZOOM, Math.max(floor, zoom));
34
35
  }
35
- /** The levels a step lands on: `STEPS` with `fit` among them, which takes the
36
- * place of any level within {@link DETENT} of it, since fit is the one that follows the
37
- * pane. */
36
+ /**
37
+ * The levels a step lands on: `STEPS` with `fit` replacing any level within {@link DETENT} of it.
38
+ */
38
39
  function stops(fit) {
39
40
  return [...STEPS.filter((s) => !near(s, fit)), fit].sort((a, b) => a - b);
40
41
  }
41
- /** The next level in `dir` from `zoom`, `fit` among them, skipping levels under `floor`.
42
- * From between two levels the first press lands on the nearer one in that direction;
43
- * past the last level above the floor, the floor. Half a percent of slack, so a zoom
44
- * shown as a level is at it. */
42
+ /**
43
+ * The next level in `dir` from `zoom`, skipping levels under `floor`; past the last, the floor.
44
+ * Half a percent of slack, so a zoom shown as a level is at it.
45
+ */
45
46
  export function stepZoom(zoom, dir, floor, fit) {
46
47
  const levels = stops(fit);
47
48
  const next = dir > 0
@@ -49,11 +50,11 @@ export function stepZoom(zoom, dir, floor, fit) {
49
50
  : levels.reverse().find((s) => s < zoom - 0.005 && s >= floor);
50
51
  return clampZoom(next ?? (dir > 0 ? MAX_ZOOM : floor), floor);
51
52
  }
52
- /** One event of a continuous gesture, carrying the run to `raw`. It stops at `fit` or
53
- * 100% when it crosses one, at the first it meets, so one wheel notch across either
54
- * stops there; it leaves a hold once carried {@link DETENT} past, moving on from the
55
- * hold rather than jumping to where the events would have put it. 100% gives way to a
56
- * `fit` within the detent of it: two holds that close would pin the run between them. */
53
+ /**
54
+ * One event of a continuous gesture, carrying the run to `raw`. It stops at the first of `fit` or
55
+ * 100% it crosses, and leaves a hold once carried {@link DETENT} past. 100% gives way to a `fit`
56
+ * within the detent of it.
57
+ */
57
58
  export function glide(run, raw, floor, fit) {
58
59
  const holds = near(fit, 1) ? [fit] : [fit, 1];
59
60
  const step = Math.log(raw / run.raw);
@@ -652,6 +652,27 @@
652
652
  if (!drag?.engaged || drag.drop !== k || drag.drop === drag.k) return undefined;
653
653
  return drag.drop > drag.k ? 'after' : 'before';
654
654
  }
655
+
656
+ /** Whether cells stand past the table scroller's end edge, under the pinned remove,
657
+ * which is what the fade before the remove draws on. */
658
+ let tableOverflow = $state(false);
659
+ /** The head row spans every track, so its box is the content width a new row or a
660
+ * longer cell changes. */
661
+ function trackOverflow(el: HTMLElement): () => void {
662
+ const measure = () => {
663
+ tableOverflow = el.scrollWidth - el.clientWidth - Math.abs(el.scrollLeft) > 1;
664
+ };
665
+ measure();
666
+ el.addEventListener('scroll', measure, { passive: true });
667
+ const observer =
668
+ typeof ResizeObserver === 'undefined' ? undefined : new ResizeObserver(measure);
669
+ observer?.observe(el);
670
+ if (el.firstElementChild) observer?.observe(el.firstElementChild);
671
+ return () => {
672
+ el.removeEventListener('scroll', measure);
673
+ observer?.disconnect();
674
+ };
675
+ }
655
676
  </script>
656
677
 
657
678
  <div
@@ -691,7 +712,12 @@
691
712
  data-default={defaulted ? '' : undefined}
692
713
  bind:this={rowsEl}
693
714
  >
694
- <div class="qm-array-table-scroller" style:--table-cols={columns.length}>
715
+ <div
716
+ class="qm-array-table-scroller"
717
+ data-overflow={tableOverflow ? '' : undefined}
718
+ style:--table-cols={columns.length}
719
+ {@attach trackOverflow}
720
+ >
695
721
  <!-- The header names the columns, so a cell carries no label of its own: its
696
722
  accessible name is the row's and the column's composed (`ObjectField`). The
697
723
  guidance affordance rides the header, being the column's rather than any
@@ -1072,27 +1098,21 @@
1072
1098
  The distance inside an open row is the list's own, and the row then takes a rung of
1073
1099
  air from the rows either side of it, so what it unfolded is nearer the summary it
1074
1100
  hangs off than the sibling below. The wider distance on the inside is the inversion:
1075
- a subform reading as the next row's preamble. A variant spends the wider rung on the
1076
- same join and needs no such guard, a field carrying one control and no siblings for
1077
- its cells to drift toward. */
1101
+ a subform reading as the next row's preamble. A variant takes the same distance and
1102
+ needs no such guard, a field carrying one control and no siblings for its cells to
1103
+ drift toward. */
1078
1104
  .qm-element {
1079
1105
  row-gap: var(--_qm-space);
1080
1106
  }
1081
1107
  .qm-element.open {
1082
1108
  margin-block: var(--_qm-space);
1083
1109
  }
1084
- /* The inline distance is the stacker's too, and here it is a rung: the summary stands
1085
- for the row rather than being a cell of it, so its subform's vertical takes the
1086
- disclosure's own column — `--_qm-nest` in, the distance every stroke on the ladder
1087
- keeps from the one outside it (ARCHITECTURE §"A plane is a tone"). Flush with the
1088
- row's edge that stroke is collinear with the box above it and with the sibling row
1089
- below, so a row's cells read as the list's own rung and the content of an open row
1090
- stands outboard of the chevron that opened it. A variant's cells and a matrix's
1091
- columns keep the flush edge: those sit beside the discriminant and the tick they are
1092
- stored with, not under them. */
1093
- .qm-element > :global(.qm-object) {
1094
- margin-inline-start: var(--_qm-nest);
1095
- }
1110
+ /* No inline distance: the subform's vertical hangs at the row's edge, under the
1111
+ summary that unfolded it, as a variant's cells hang at the discriminant's and a
1112
+ matrix's columns at the tick's. Every vertical hangs at the edge of the control it
1113
+ unfolds from, so a step in is one nesting distance wherever it is read, the
1114
+ `--_qm-nest` the subform keeps from its own stroke (ARCHITECTURE §"A plane is a
1115
+ tone"); a second rung here read as a deeper level than the object the row holds. */
1096
1116
  /* The slabs measure the head, not the row: an open element is the head plus
1097
1117
  everything it unfolded, and the controls belong to the line they sit on rather than
1098
1118
  to all of that. Positioned, so `.qm-row-actions` anchors here. */
@@ -1123,6 +1143,7 @@
1123
1143
  .qm-element-summary :global(.qm-el-chevron) {
1124
1144
  flex-shrink: 0;
1125
1145
  display: block;
1146
+ stroke-width: var(--_qm-glyph-stroke);
1126
1147
  color: var(--_qm-ink-label);
1127
1148
  transform: rotate(0deg);
1128
1149
  transform-origin: center;
@@ -1130,6 +1151,9 @@
1130
1151
  transform var(--_qm-duration-slow) var(--_qm-ease-reverse),
1131
1152
  color var(--_qm-duration-fast) var(--_qm-ease-reverse);
1132
1153
  }
1154
+ .qm-element-summary :global(.qm-el-chevron > *) {
1155
+ vector-effect: non-scaling-stroke;
1156
+ }
1133
1157
  .qm-element-summary:hover :global(.qm-el-chevron),
1134
1158
  .qm-element.open > .qm-element-head :global(.qm-el-chevron) {
1135
1159
  color: var(--_qm-ink);
@@ -1251,6 +1275,28 @@
1251
1275
  background: var(--_qm-surface);
1252
1276
  box-shadow: 0 0 0 var(--_qm-ring-reach) var(--_qm-surface);
1253
1277
  }
1278
+ /* While cells stand past the end edge, the slab fades them in over a step before it
1279
+ rather than cutting them at its edge: the column the cut hides is still there. */
1280
+ .qm-array-table-row > .qm-remove::before,
1281
+ .qm-array-table-head-end::before {
1282
+ content: '';
1283
+ position: absolute;
1284
+ inset-block: calc(-1 * var(--_qm-ring-reach));
1285
+ inset-inline-end: calc(100% + var(--_qm-ring-reach));
1286
+ width: var(--_qm-space-3);
1287
+ background: linear-gradient(to right, transparent, var(--_qm-surface));
1288
+ pointer-events: none;
1289
+ opacity: 0;
1290
+ transition: opacity var(--_qm-duration-fast) var(--_qm-ease-reverse);
1291
+ }
1292
+ .qm-array-table-row > .qm-remove:dir(rtl)::before,
1293
+ .qm-array-table-head-end:dir(rtl)::before {
1294
+ background: linear-gradient(to left, transparent, var(--_qm-surface));
1295
+ }
1296
+ .qm-array-table-scroller[data-overflow] > .qm-array-table-row > .qm-remove::before,
1297
+ .qm-array-table-scroller[data-overflow] .qm-array-table-head-end::before {
1298
+ opacity: 1;
1299
+ }
1254
1300
  /* The glyph stands on the row's first line with the cells, however far the slab
1255
1301
  stretches under it. */
1256
1302
  .qm-array-table-row > .qm-remove {
@@ -1261,6 +1307,17 @@
1261
1307
  border-radius: var(--_qm-radius-inner);
1262
1308
  color: var(--_qm-ink-label);
1263
1309
  }
1310
+ /* The pad past the slab, flat: the plane's spread above rounds with the slab's
1311
+ corners, and a cell scrolled under them shows through at the clip. */
1312
+ .qm-array-table-row > .qm-remove::after,
1313
+ .qm-array-table-head-end::after {
1314
+ content: '';
1315
+ position: absolute;
1316
+ inset-block: calc(-1 * var(--_qm-ring-reach));
1317
+ inset-inline-start: 100%;
1318
+ width: var(--_qm-ring-reach);
1319
+ background: var(--_qm-surface);
1320
+ }
1264
1321
  /* The label line's own type: size, weight and leading are `.qm-field-label`'s, so
1265
1322
  the two read as one register. Inner radius: the chip family is the card's, and this
1266
1323
  box is a line of type. The chip's tap floor is given back (`.qm-tap-floor`),
@@ -1273,6 +1330,10 @@
1273
1330
  line-height: var(--_qm-leading-tight);
1274
1331
  border-radius: var(--_qm-radius-inner);
1275
1332
  opacity: var(--_qm-opacity-idle);
1333
+ transition:
1334
+ opacity var(--_qm-duration-fast) var(--_qm-ease-reverse),
1335
+ background-color var(--_qm-duration-fast) var(--_qm-ease-reverse),
1336
+ color var(--_qm-duration-fast) var(--_qm-ease-reverse);
1276
1337
  }
1277
1338
  .qm-add-el:hover:not(:disabled),
1278
1339
  .qm-add-el:focus-visible {
@@ -78,8 +78,13 @@
78
78
 
79
79
  <style>
80
80
  /* The switch stands on the first line of the boxes beside it: a row of controls tops
81
- out together, and a mark carries no line of its own to sit on. */
81
+ out together, and a mark carries no line of its own to sit on.
82
+
83
+ One length, the thumb's, and the track is two of them around the inset: the travel
84
+ is a thumb's width at every setting of `--qm-space`, where a track sized apart from
85
+ its inset ran the checked thumb into the end cap as the inset grew. */
82
86
  .qm-toggle-wrap {
87
+ --thumb: 0.75rem;
83
88
  display: flex;
84
89
  align-items: center;
85
90
  min-height: var(--_qm-box-line);
@@ -94,15 +99,17 @@
94
99
  .qm-toggle-wrap :global(.qm-toggle) {
95
100
  display: inline-flex;
96
101
  align-items: center;
97
- width: 1.75rem;
98
- height: 1rem;
102
+ width: calc(2 * var(--thumb) + 2 * var(--_qm-space-half));
103
+ height: calc(var(--thumb) + 2 * var(--_qm-space-half));
99
104
  padding: var(--_qm-space-half);
100
105
  border: none;
101
106
  border-radius: var(--_qm-radius-pill);
102
107
  /* The track is a fill and nothing else: an edge around it would be a second
103
108
  statement of a shape the tone already draws, and the checked state swaps that
104
- one fill for the accent (ARCHITECTURE §Styling). */
105
- background: var(--_qm-surface-hover);
109
+ one fill for the accent (ARCHITECTURE §Styling). The fill is the band, a
110
+ resting tone: on the hover rung an off switch read as a button under the
111
+ pointer. */
112
+ background: var(--_qm-surface-band);
106
113
  cursor: pointer;
107
114
  transition: background var(--_qm-duration-fast) var(--_qm-ease-reverse);
108
115
  }
@@ -113,21 +120,27 @@
113
120
  background: var(--_qm-accent-default);
114
121
  }
115
122
  /* The focus ring rides `.qm-focus-ring` on the switch (controls.css). */
116
- /* The thumb reads on tone alone: the base surface over the track's `hover` rung
117
- unchecked, and over `--_qm-accent` checked. No edge of its own: a hairline around
118
- a 12px pill is a second box inside the track's. */
123
+ /* The thumb reads on tone alone, and its tone is what says where it stands: the
124
+ label rung over the track's band unchecked, and the base surface over
125
+ `--_qm-accent` checked. The card's own tone over the band is a step of about
126
+ 1.3:1 in light and 1.5:1 in dark, a state indicator under WCAG 1.4.11's 3:1; the
127
+ label rung clears it at both, at 4.6:1 and 3.3:1. No edge of its own: a hairline
128
+ around a 12px pill is a second box inside the track's. */
119
129
  .qm-toggle-wrap :global(.qm-toggle-thumb) {
120
- width: 0.75rem;
121
- height: 0.75rem;
130
+ width: var(--thumb);
131
+ height: var(--thumb);
122
132
  border-radius: var(--_qm-radius-pill);
123
- background: var(--_qm-surface);
124
- transition: translate var(--_qm-duration-fast) var(--_qm-ease-reverse);
133
+ background: var(--_qm-ink-label);
134
+ transition:
135
+ translate var(--_qm-duration-fast) var(--_qm-ease-reverse),
136
+ background var(--_qm-duration-fast) var(--_qm-ease-reverse);
125
137
  }
126
138
  .qm-toggle-wrap :global(.qm-toggle-thumb[data-state='checked']) {
127
- translate: 0.75rem;
139
+ translate: var(--thumb);
140
+ background: var(--_qm-surface);
128
141
  }
129
142
  /* Unset, the thumb stands at neither end: halfway along its travel. */
130
143
  .qm-toggle-wrap[data-unset] :global(.qm-toggle-thumb) {
131
- translate: 0.375rem;
144
+ translate: calc(var(--thumb) / 2);
132
145
  }
133
146
  </style>
@@ -122,7 +122,8 @@
122
122
  }
123
123
 
124
124
  /** Not `--_qm-glyph-control`: that rung centres a glyph in a tap target, where this
125
- * one stands in a run of text at the body rung and is sized to sit level with it. */
125
+ * one stands in a run of text at the body rung and is sized to sit level with it. It
126
+ * strokes at the control glyph's weight (`--_qm-glyph-stroke`). */
126
127
  const CHEVRON = 16;
127
128
 
128
129
  const ungrouped = $derived(card.sections.filter((s) => s.group == null));
@@ -428,15 +429,18 @@
428
429
  {/snippet}
429
430
 
430
431
  <style>
431
- /* An island: the card is the base plane, the column behind it the sunken one, and the
432
- hairline closes it (ARCHITECTURE §Styling). The block half is the same rung as the
433
- gap: the payload stands that far off the card's edge and off the body below it. The
434
- header insets itself to the section-heading column (`.qm-card-header`). The inline
435
- half stands a step tighter than the nesting rung the verticals inside keep: this
436
- edge closes the block rather than nesting one, and the rules it holds run that much
437
- further for it. */
432
+ /* An island: the card is the base plane and the column behind it the sunken one, and
433
+ the step between the two tones is the whole of its edge (ARCHITECTURE §"A plane is a
434
+ tone"). The block half is the same rung as the gap: the payload stands that far off
435
+ the card's edge and off the body below it. The header insets itself to the
436
+ section-heading column (`.qm-card-header`). The inline half stands a step tighter
437
+ than the nesting rung the verticals inside keep: this edge closes the block rather
438
+ than nesting one, and the rules it holds run that much further for it.
439
+
440
+ Inside that inset is the card's one text column (`--_qm-column`): the title, a
441
+ top-level field, the body's lines, the recovery note and the delete glyph's far edge
442
+ all stand on it, so the card reads as one measure at both ends. */
438
443
  .qm-card {
439
- border: var(--_qm-border-width) solid var(--_qm-border);
440
444
  border-radius: var(--_qm-radius);
441
445
  padding: var(--_qm-space-3) var(--_qm-space-2);
442
446
  background: var(--_qm-surface);
@@ -463,10 +467,9 @@
463
467
  align-items: center;
464
468
  justify-content: space-between;
465
469
  gap: var(--_qm-space-2);
466
- /* The group's vertical plus the rung the chevron stands off it: the title
467
- shares that column. Inline only. The block pull is half the card's block
468
- inset, so the header sits that far off the hairline and the rule. */
469
- padding-inline: calc(var(--_qm-space) + var(--_qm-vertical-width));
470
+ /* On the column, inline only. The block pull is half the card's block inset,
471
+ so the header sits that far off the card's edge and the rule. */
472
+ padding-inline: var(--_qm-column);
470
473
  margin-block: calc(-1 * var(--_qm-space-3) / 2);
471
474
  }
472
475
  .qm-card-header-right {
@@ -500,9 +503,9 @@
500
503
  overlaid input tracks its content width. Both carry the same nothing for a box model
501
504
  and inherit the same type tokens, which is what makes the two measure alike.
502
505
 
503
- The type is the group header's, down to the ink and its hover step below: a card
504
- title and a section header both name a block of fields, so they read as one register
505
- and what ranks them is position rather than a size step. */
506
+ The type is the group header's: a card title and a section header both name a block
507
+ of fields, so they read as one register and what ranks them is position rather than
508
+ a size step. */
506
509
  .qm-card-title-sizer {
507
510
  display: inline-grid;
508
511
  align-items: center;
@@ -521,9 +524,9 @@
521
524
  min-width: 2ch;
522
525
  }
523
526
  /* The title draws no box in any state: a card title is already the one line that
524
- reads as the card's name, and the region's `cursor: text` says the rest. The ink step
525
- replaces it, the same cue the group header takes — a wide borderless target has no
526
- other way to answer the pointer. */
527
+ reads as the card's name, and the region's `cursor: text` answers the pointer. A
528
+ typed title is a value, at the ink, and the automatic one a placeholder, at label
529
+ ink, as in every field. */
527
530
  .qm-card-title {
528
531
  grid-area: 1 / 1;
529
532
  width: 100%;
@@ -531,18 +534,12 @@
531
534
  font: inherit;
532
535
  /* `font: inherit` does not carry colour: a form control defaults to the UA's own
533
536
  text colour rather than the header's ink without this line. */
534
- color: var(--_qm-ink-label);
537
+ color: var(--_qm-ink);
535
538
  border: none;
536
539
  padding: 0;
537
540
  background: transparent;
538
- transition: color var(--_qm-duration-fast) var(--_qm-ease-reverse);
539
541
  }
540
- .qm-card-title:hover,
541
- .qm-card-title:focus {
542
- color: var(--_qm-ink);
543
- }
544
- /* The automatic title, at the label ink every placeholder takes rather than the UA's
545
- grey, and steady under focus, where a typed title steps to the ink. */
542
+ /* The label ink every placeholder takes rather than the UA's grey. */
546
543
  .qm-card-title::placeholder {
547
544
  color: var(--_qm-ink-label);
548
545
  opacity: 1;
@@ -591,11 +588,10 @@
591
588
  vertical those two edges hold.
592
589
 
593
590
  No stroke: a vertical would mark a section a reader has no name for. The start
594
- inset is the heading's — the group's vertical plus the rung the chevron stands
595
- off it — so a top-level field and a section heading share a column. */
591
+ inset is the column, so a top-level field and a section heading share it. */
596
592
  .qm-section {
597
593
  padding-block: var(--_qm-space);
598
- padding-inline-start: calc(var(--_qm-space) + var(--_qm-vertical-width));
594
+ padding-inline-start: var(--_qm-column);
599
595
  }
600
596
  /* Where it faces a rule, the rung a heading already stands off one. The fields
601
597
  sit on the heading column, not the card's edge. */
@@ -622,8 +618,8 @@
622
618
  }
623
619
  /* The nesting vertical, one `--_qm-nest` in from the card's edge with the panel's
624
620
  fields the same rung off it (ARCHITECTURE §"A plane is a tone"). `--_qm-border` and
625
- not `--_qm-accent`: a card's edge and the verticals inside it are one stroke at three
626
- depths, and a tone that changed as it went in would read as unrelated lines rather
621
+ not `--_qm-accent`: the verticals and the rules beside them are one stroke at every
622
+ depth, and a tone that changed as it went in would read as unrelated lines rather
627
623
  than one figure. A named section is what earns it, which is why the ungrouped block
628
624
  above draws none.
629
625
 
@@ -684,17 +680,10 @@
684
680
  the accordion stops looking like structure. Size, weight and leading sit after
685
681
  `font`, a shorthand that carries `line-height`. */
686
682
  .qm-group-header {
687
- /* Positioned, and paint is not what it buys: a group's box grows by
688
- `--stroke-reach` at both ends and hands the growth back as margin, so
689
- consecutive boxes overlap in a band at every seam, and a closed group's header
690
- ends inside the next group's box. In flow, the later sibling takes the presses
691
- that land in the band and the bottom of the header is a dead strip; positioned,
692
- the control holds its own box, and the band paints the same either way. */
693
- position: relative;
694
683
  display: flex;
695
684
  align-items: center;
696
685
  gap: var(--_qm-space);
697
- width: 100%;
686
+ min-width: 0;
698
687
  border: none;
699
688
  background: transparent;
700
689
  padding: var(--_qm-space) 0 var(--_qm-space) var(--_qm-space);
@@ -709,17 +698,26 @@
709
698
  }
710
699
  /* The header row: the press target, and beside it the guidance marker a headed
711
700
  section carries (`FieldHint`), which is its own control and so not inside the
712
- button. Positioned for the reason the header is, the row being the box that
713
- stands in the band. */
701
+ button. The button sizes to its text so the marker follows the name, and its
702
+ `::after` spans the row so the whole row still presses it; the marker, positioned
703
+ by its tap floor and later in the row, stands over that plane.
704
+
705
+ Positioned, and paint is not what it buys: a group's box grows by `--stroke-reach`
706
+ at both ends and hands the growth back as margin, so consecutive boxes overlap in a
707
+ band at every seam, and a closed group's header ends inside the next group's box.
708
+ In flow, the later sibling takes the presses that land in the band and the bottom
709
+ of the header is a dead strip; positioned, the row holds its own box, and the band
710
+ paints the same either way. */
714
711
  .qm-group-head {
715
712
  position: relative;
716
713
  display: flex;
717
714
  align-items: center;
718
715
  gap: var(--_qm-space);
719
716
  }
720
- .qm-group-head > .qm-group-header {
721
- flex: 1;
722
- min-width: 0;
717
+ .qm-group-header::after {
718
+ content: '';
719
+ position: absolute;
720
+ inset: 0;
723
721
  }
724
722
  /* The parked description: `FieldLabel`'s recipe, for the one description a
725
723
  header rather than a label parks. */
@@ -734,10 +732,14 @@
734
732
  .qm-group-header :global(.qm-group-chevron) {
735
733
  flex-shrink: 0;
736
734
  display: block;
735
+ stroke-width: var(--_qm-glyph-stroke);
737
736
  transform: rotate(0deg);
738
737
  transform-origin: center;
739
738
  transition: transform var(--_qm-duration-slow) var(--_qm-ease-reverse);
740
739
  }
740
+ .qm-group-header :global(.qm-group-chevron > *) {
741
+ vector-effect: non-scaling-stroke;
742
+ }
741
743
  /* Open shares hover's ink step: a big borderless target draws no box, so the ink is
742
744
  the only cue an open section keeps once the pointer leaves it. The chevron's
743
745
  rotation is the other cue, and stays open-only below. */
@@ -792,9 +794,17 @@
792
794
  flex-direction: column;
793
795
  gap: var(--_qm-space);
794
796
  }
795
- /* The state is in the stroke's pattern, every card being closed by one already. */
797
+ /* Paper's margin is the column. On the editable, so a press in the strip lands a
798
+ caret; carried by the leaf's inset it would be dead width. The diagnostics under the
799
+ body start where its lines do, as a field's start under its well. */
800
+ .qm-body-leaf :global(.ProseMirror),
801
+ .qm-body-leaf > :global(.qm-diag-list) {
802
+ padding-inline: var(--_qm-column);
803
+ }
804
+ /* The one card that draws an edge, and the edge is the state: a dashed stroke is the
805
+ placeholder idiom, which a card the schema cannot project is. */
796
806
  .qm-card.qm-unschemable {
797
- border-style: dashed;
807
+ border: var(--_qm-border-width) dashed var(--_qm-border);
798
808
  }
799
809
  /* One typographic role with the editable title above. */
800
810
  .qm-card-title-static {
@@ -807,6 +817,7 @@
807
817
  display: flex;
808
818
  flex-direction: column;
809
819
  gap: var(--_qm-space-2);
820
+ padding-inline: var(--_qm-column);
810
821
  }
811
822
  .qm-recovery-note {
812
823
  margin: 0;
@@ -820,9 +831,11 @@
820
831
  font-size: var(--_qm-text-label);
821
832
  color: var(--_qm-ink-label);
822
833
  }
823
- /* The one native control the surface draws, on the box's own recipe (controls.css). */
834
+ /* The one native control the surface draws, on the box's own recipe (controls.css).
835
+ `font` carries no colour, so the value's ink is restated over the UA's. */
824
836
  .qm-recovery-retype select {
825
837
  font: inherit;
838
+ color: var(--_qm-ink);
826
839
  border: none;
827
840
  border-radius: var(--_qm-radius-inner);
828
841
  background: var(--_qm-surface-well);
@@ -3,7 +3,7 @@
3
3
  Right-aligned in the header: a move-up/move-down chevron pair (each disabled at its
4
4
  edge), then delete, the row revealed while the pointer or the caret is in the card.
5
5
  Reorder is buttons, not drag (V1). All three are chips (controls.css): fill arrives
6
- on hover, and delete draws faded danger, full under the pointer. The card scopes
6
+ on hover, and delete rests at the label ink beside them. The card scopes
7
7
  that reveal, so its condition lives in the parent's CSS; the row keeps the narrower
8
8
  guarantee here, that a focused control is a visible one.
9
9
  -->
@@ -80,18 +80,12 @@
80
80
  height: var(--_qm-tap-min);
81
81
  padding: 0;
82
82
  }
83
- /* Hover fill comes from `.qm-chip` (controls.css). Idle recede is on the ink,
84
- not the box: a layer on the chip is two rasters of the same stroke, and the
85
- fill has to stay opaque. Danger mixed to the idle rung; full under the pointer. */
86
- .qm-card-delete {
87
- color: color-mix(
88
- in oklab,
89
- var(--_qm-danger) calc(100% * var(--_qm-opacity-idle)),
90
- var(--_qm-surface)
91
- );
92
- }
83
+ /* The delete rests at the chip's label ink, as an array row's and a matrix member's
84
+ remove do, and under the pointer or the focus takes their danger tint and ink in
85
+ place of the chip's hover fill. */
93
86
  .qm-card-delete:hover,
94
87
  .qm-card-delete:focus-visible {
88
+ background: var(--_qm-danger-tint);
95
89
  color: var(--_qm-danger);
96
90
  }
97
91
  </style>