@quillmark/svelte 0.26.2 → 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 (97) hide show
  1. package/NOTICE +4 -4
  2. package/README.md +26 -8
  3. package/THEMING.md +40 -4
  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 +71 -0
  51. package/dist/preset/index.css +8 -7
  52. package/dist/preset/recipes.css +19 -164
  53. package/dist/preset/scale.css +28 -18
  54. package/dist/preset/shell.css +157 -0
  55. package/dist/preview/Preview.svelte +8 -0
  56. package/dist/preview/bridge.d.ts +18 -22
  57. package/dist/preview/bridge.js +29 -58
  58. package/dist/preview/controller.d.ts +21 -29
  59. package/dist/preview/controller.js +14 -39
  60. package/dist/preview/geometry.d.ts +13 -34
  61. package/dist/preview/geometry.js +13 -34
  62. package/dist/preview/index.js +4 -16
  63. package/dist/preview/paint.d.ts +3 -3
  64. package/dist/preview/paint.js +28 -68
  65. package/dist/preview/strings.d.ts +1 -1
  66. package/dist/preview/strings.js +2 -14
  67. package/dist/preview/viewport.js +20 -15
  68. package/dist/preview/zoom.d.ts +21 -22
  69. package/dist/preview/zoom.js +53 -30
  70. package/dist/visual/ArrayField.svelte +77 -16
  71. package/dist/visual/BooleanField.svelte +28 -15
  72. package/dist/visual/Card.svelte +66 -51
  73. package/dist/visual/CardControls.svelte +5 -11
  74. package/dist/visual/DateField.svelte +20 -4
  75. package/dist/visual/EnumField.svelte +3 -0
  76. package/dist/visual/FieldHint.svelte +2 -1
  77. package/dist/visual/FormatPopover.svelte +82 -15
  78. package/dist/visual/FormatPopover.svelte.d.ts +4 -0
  79. package/dist/visual/MatrixAdd.svelte +30 -12
  80. package/dist/visual/MatrixField.svelte +24 -16
  81. package/dist/visual/NumberField.svelte +7 -0
  82. package/dist/visual/ObjectField.svelte +4 -4
  83. package/dist/visual/ProseField.svelte +0 -5
  84. package/dist/visual/TipsCard.svelte +11 -10
  85. package/dist/visual/VariantField.svelte +3 -1
  86. package/dist/visual/VisualEditorInner.svelte +19 -3
  87. package/dist/visual/controls.css +7 -2
  88. package/dist/visual/fonts.d.ts +18 -0
  89. package/dist/visual/fonts.js +59 -0
  90. package/dist/visual/icons/nodes.d.ts +0 -5
  91. package/dist/visual/icons/nodes.js +0 -4
  92. package/dist/visual/props.d.ts +14 -1
  93. package/dist/visual/strings.d.ts +7 -0
  94. package/dist/visual/strings.js +5 -2
  95. package/dist/visual/structure.d.ts +43 -87
  96. package/dist/visual/structure.js +42 -83
  97. package/package.json +2 -2
@@ -1,28 +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 past 100% a continuous gesture carries before it leaves it. */
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
+ */
16
18
  const DETENT = 0.04;
17
- /** The default: the widest page fits the pane's width, capped at {@link FIT_CAP}. A
18
- * portrait-only document lands at the pane's width, as a narrow pane always drew it. */
19
+ const near = (zoom, hold) => Math.abs(Math.log(zoom / hold)) <= DETENT;
20
+ /** The default: the widest page fits the pane's width, capped at {@link FIT_CAP}. */
19
21
  export function fitZoom(pages, pane) {
20
22
  const widest = Math.max(...pages.map((p) => p.widthPt));
21
23
  return Math.min(pane.width / (widest * ACTUAL), FIT_CAP);
22
24
  }
23
- /** The furthest a gesture zooms out: every page fits the pane whole with
24
- * {@link FLOOR_FILL} of it filled on the page's limiting axis, and never past the
25
- * 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
+ */
26
29
  export function floorZoom(pages, pane) {
27
30
  const whole = Math.min(...pages.map((p) => Math.min(pane.width / p.widthPt, pane.height / p.heightPt)));
28
31
  return Math.min((FLOOR_FILL * whole) / ACTUAL, fitZoom(pages, pane));
@@ -30,23 +33,43 @@ export function floorZoom(pages, pane) {
30
33
  export function clampZoom(zoom, floor) {
31
34
  return Math.min(MAX_ZOOM, Math.max(floor, zoom));
32
35
  }
33
- /** The next level in `dir` from `zoom`, skipping levels under `floor`. From between two
34
- * levels the first press lands on the nearer one in that direction; past the last level
35
- * above the floor, the floor. Half a percent of slack, so a zoom shown as a level is at
36
- * it. */
37
- export function stepZoom(zoom, dir, floor) {
36
+ /**
37
+ * The levels a step lands on: `STEPS` with `fit` replacing any level within {@link DETENT} of it.
38
+ */
39
+ function stops(fit) {
40
+ return [...STEPS.filter((s) => !near(s, fit)), fit].sort((a, b) => a - b);
41
+ }
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
+ */
46
+ export function stepZoom(zoom, dir, floor, fit) {
47
+ const levels = stops(fit);
38
48
  const next = dir > 0
39
- ? STEPS.find((s) => s > zoom + 0.005)
40
- : [...STEPS].reverse().find((s) => s < zoom - 0.005 && s >= floor);
49
+ ? levels.find((s) => s > zoom + 0.005)
50
+ : levels.reverse().find((s) => s < zoom - 0.005 && s >= floor);
41
51
  return clampZoom(next ?? (dir > 0 ? MAX_ZOOM : floor), floor);
42
52
  }
43
- /** One event of a continuous gesture, carrying the run to `raw`: held at 100% when it
44
- * crosses or lands within {@link DETENT} of it, so one wheel notch across actual size
45
- * stops there. */
46
- export function glide(run, raw, floor) {
47
- let next = clampZoom(raw, floor);
48
- if ((run.shown - 1) * (next - 1) < 0)
49
- next = 1;
50
- const shown = Math.abs(next - 1) <= DETENT ? 1 : next;
51
- return { raw: next, shown: clampZoom(shown, floor) };
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
+ */
58
+ export function glide(run, raw, floor, fit) {
59
+ const holds = near(fit, 1) ? [fit] : [fit, 1];
60
+ const step = Math.log(raw / run.raw);
61
+ let next;
62
+ if (holds.includes(run.shown)) {
63
+ const slack = run.slack + step;
64
+ if (Math.abs(slack) <= DETENT)
65
+ return { raw, shown: run.shown, slack };
66
+ next = run.shown * Math.exp(slack - Math.sign(slack) * DETENT);
67
+ }
68
+ else
69
+ next = run.shown * Math.exp(step);
70
+ next = clampZoom(next, floor);
71
+ const crossed = holds
72
+ .filter((t) => (run.shown - t) * (next - t) < 0)
73
+ .sort((a, b) => Math.abs(a - run.shown) - Math.abs(b - run.shown));
74
+ return { raw, shown: crossed[0] ?? next, slack: 0 };
52
75
  }
@@ -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 {
@@ -52,7 +52,7 @@
52
52
 
53
53
  <!-- Not `.qm-switch`: that name is the preset's pane band, whose narrow-viewport rule
54
54
  stands every child of it at `--qmh-tap`, so a track wearing it grows a thumb three times
55
- its own height over the labels around it (`preset/recipes.css`, held by `check:style`). -->
55
+ its own height over the labels around it (`preset/shell.css`, held by `check:style`). -->
56
56
  <span
57
57
  class="qm-toggle-wrap"
58
58
  data-unset={unset ? '' : undefined}
@@ -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));
@@ -211,12 +212,14 @@
211
212
  </script>
212
213
 
213
214
  <!-- `data-qm-card` names the island for a floating surface that has to stay inside one
214
- ({@link FieldHint}). A surface portals to `[data-qm-root]`, so the card is not its
215
- ancestor and nothing else marks the box its guidance belongs in. -->
215
+ ({@link FieldHint}): a surface portals to `[data-qm-root]`, so the card is not its
216
+ ancestor and nothing else marks the box its guidance belongs in. Its value is the card's
217
+ kind, `main` for the main card, which is the one schema-named hook a host's stylesheet
218
+ is promised (THEMING §"What a stylesheet may aim at"). -->
216
219
  <section
217
220
  bind:this={el}
218
221
  class={['qm-card', { 'qm-main': card.isMain, 'qm-unschemable': card.unschemable }]}
219
- data-qm-card
222
+ data-qm-card={card.kind}
220
223
  >
221
224
  {#if card.unschemable}
222
225
  {@render recoveryShell()}
@@ -426,15 +429,18 @@
426
429
  {/snippet}
427
430
 
428
431
  <style>
429
- /* An island: the card is the base plane, the column behind it the sunken one, and the
430
- hairline closes it (ARCHITECTURE §Styling). The block half is the same rung as the
431
- gap: the payload stands that far off the card's edge and off the body below it. The
432
- header insets itself to the section-heading column (`.qm-card-header`). The inline
433
- half stands a step tighter than the nesting rung the verticals inside keep: this
434
- edge closes the block rather than nesting one, and the rules it holds run that much
435
- 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. */
436
443
  .qm-card {
437
- border: var(--_qm-border-width) solid var(--_qm-border);
438
444
  border-radius: var(--_qm-radius);
439
445
  padding: var(--_qm-space-3) var(--_qm-space-2);
440
446
  background: var(--_qm-surface);
@@ -461,10 +467,9 @@
461
467
  align-items: center;
462
468
  justify-content: space-between;
463
469
  gap: var(--_qm-space-2);
464
- /* The group's vertical plus the rung the chevron stands off it: the title
465
- shares that column. Inline only. The block pull is half the card's block
466
- inset, so the header sits that far off the hairline and the rule. */
467
- 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);
468
473
  margin-block: calc(-1 * var(--_qm-space-3) / 2);
469
474
  }
470
475
  .qm-card-header-right {
@@ -498,9 +503,9 @@
498
503
  overlaid input tracks its content width. Both carry the same nothing for a box model
499
504
  and inherit the same type tokens, which is what makes the two measure alike.
500
505
 
501
- The type is the group header's, down to the ink and its hover step below: a card
502
- title and a section header both name a block of fields, so they read as one register
503
- 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. */
504
509
  .qm-card-title-sizer {
505
510
  display: inline-grid;
506
511
  align-items: center;
@@ -519,9 +524,9 @@
519
524
  min-width: 2ch;
520
525
  }
521
526
  /* The title draws no box in any state: a card title is already the one line that
522
- reads as the card's name, and the region's `cursor: text` says the rest. The ink step
523
- replaces it, the same cue the group header takes — a wide borderless target has no
524
- 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. */
525
530
  .qm-card-title {
526
531
  grid-area: 1 / 1;
527
532
  width: 100%;
@@ -529,18 +534,12 @@
529
534
  font: inherit;
530
535
  /* `font: inherit` does not carry colour: a form control defaults to the UA's own
531
536
  text colour rather than the header's ink without this line. */
532
- color: var(--_qm-ink-label);
537
+ color: var(--_qm-ink);
533
538
  border: none;
534
539
  padding: 0;
535
540
  background: transparent;
536
- transition: color var(--_qm-duration-fast) var(--_qm-ease-reverse);
537
541
  }
538
- .qm-card-title:hover,
539
- .qm-card-title:focus {
540
- color: var(--_qm-ink);
541
- }
542
- /* The automatic title, at the label ink every placeholder takes rather than the UA's
543
- 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. */
544
543
  .qm-card-title::placeholder {
545
544
  color: var(--_qm-ink-label);
546
545
  opacity: 1;
@@ -589,11 +588,10 @@
589
588
  vertical those two edges hold.
590
589
 
591
590
  No stroke: a vertical would mark a section a reader has no name for. The start
592
- inset is the heading's — the group's vertical plus the rung the chevron stands
593
- 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. */
594
592
  .qm-section {
595
593
  padding-block: var(--_qm-space);
596
- padding-inline-start: calc(var(--_qm-space) + var(--_qm-vertical-width));
594
+ padding-inline-start: var(--_qm-column);
597
595
  }
598
596
  /* Where it faces a rule, the rung a heading already stands off one. The fields
599
597
  sit on the heading column, not the card's edge. */
@@ -620,8 +618,8 @@
620
618
  }
621
619
  /* The nesting vertical, one `--_qm-nest` in from the card's edge with the panel's
622
620
  fields the same rung off it (ARCHITECTURE §"A plane is a tone"). `--_qm-border` and
623
- not `--_qm-accent`: a card's edge and the verticals inside it are one stroke at three
624
- 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
625
623
  than one figure. A named section is what earns it, which is why the ungrouped block
626
624
  above draws none.
627
625
 
@@ -682,17 +680,10 @@
682
680
  the accordion stops looking like structure. Size, weight and leading sit after
683
681
  `font`, a shorthand that carries `line-height`. */
684
682
  .qm-group-header {
685
- /* Positioned, and paint is not what it buys: a group's box grows by
686
- `--stroke-reach` at both ends and hands the growth back as margin, so
687
- consecutive boxes overlap in a band at every seam, and a closed group's header
688
- ends inside the next group's box. In flow, the later sibling takes the presses
689
- that land in the band and the bottom of the header is a dead strip; positioned,
690
- the control holds its own box, and the band paints the same either way. */
691
- position: relative;
692
683
  display: flex;
693
684
  align-items: center;
694
685
  gap: var(--_qm-space);
695
- width: 100%;
686
+ min-width: 0;
696
687
  border: none;
697
688
  background: transparent;
698
689
  padding: var(--_qm-space) 0 var(--_qm-space) var(--_qm-space);
@@ -707,17 +698,26 @@
707
698
  }
708
699
  /* The header row: the press target, and beside it the guidance marker a headed
709
700
  section carries (`FieldHint`), which is its own control and so not inside the
710
- button. Positioned for the reason the header is, the row being the box that
711
- 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. */
712
711
  .qm-group-head {
713
712
  position: relative;
714
713
  display: flex;
715
714
  align-items: center;
716
715
  gap: var(--_qm-space);
717
716
  }
718
- .qm-group-head > .qm-group-header {
719
- flex: 1;
720
- min-width: 0;
717
+ .qm-group-header::after {
718
+ content: '';
719
+ position: absolute;
720
+ inset: 0;
721
721
  }
722
722
  /* The parked description: `FieldLabel`'s recipe, for the one description a
723
723
  header rather than a label parks. */
@@ -732,10 +732,14 @@
732
732
  .qm-group-header :global(.qm-group-chevron) {
733
733
  flex-shrink: 0;
734
734
  display: block;
735
+ stroke-width: var(--_qm-glyph-stroke);
735
736
  transform: rotate(0deg);
736
737
  transform-origin: center;
737
738
  transition: transform var(--_qm-duration-slow) var(--_qm-ease-reverse);
738
739
  }
740
+ .qm-group-header :global(.qm-group-chevron > *) {
741
+ vector-effect: non-scaling-stroke;
742
+ }
739
743
  /* Open shares hover's ink step: a big borderless target draws no box, so the ink is
740
744
  the only cue an open section keeps once the pointer leaves it. The chevron's
741
745
  rotation is the other cue, and stays open-only below. */
@@ -790,9 +794,17 @@
790
794
  flex-direction: column;
791
795
  gap: var(--_qm-space);
792
796
  }
793
- /* 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. */
794
806
  .qm-card.qm-unschemable {
795
- border-style: dashed;
807
+ border: var(--_qm-border-width) dashed var(--_qm-border);
796
808
  }
797
809
  /* One typographic role with the editable title above. */
798
810
  .qm-card-title-static {
@@ -805,6 +817,7 @@
805
817
  display: flex;
806
818
  flex-direction: column;
807
819
  gap: var(--_qm-space-2);
820
+ padding-inline: var(--_qm-column);
808
821
  }
809
822
  .qm-recovery-note {
810
823
  margin: 0;
@@ -818,9 +831,11 @@
818
831
  font-size: var(--_qm-text-label);
819
832
  color: var(--_qm-ink-label);
820
833
  }
821
- /* 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. */
822
836
  .qm-recovery-retype select {
823
837
  font: inherit;
838
+ color: var(--_qm-ink);
824
839
  border: none;
825
840
  border-radius: var(--_qm-radius-inner);
826
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>