@quillmark/svelte 0.2.0 → 0.3.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 (116) hide show
  1. package/README.md +2 -2
  2. package/THEMING.md +11 -7
  3. package/dist/core/address.d.ts +7 -7
  4. package/dist/core/address.js +6 -6
  5. package/dist/core/bloom.d.ts +5 -5
  6. package/dist/core/bloom.js +8 -8
  7. package/dist/core/codec/anchor.d.ts +1 -1
  8. package/dist/core/codec/anchor.js +1 -1
  9. package/dist/core/codec/atoms.d.ts +10 -0
  10. package/dist/core/codec/atoms.js +78 -0
  11. package/dist/core/codec/clipboard.d.ts +59 -0
  12. package/dist/core/codec/clipboard.js +202 -0
  13. package/dist/core/codec/code.d.ts +1 -1
  14. package/dist/core/codec/code.js +4 -4
  15. package/dist/core/codec/decode.d.ts +1 -1
  16. package/dist/core/codec/decode.js +8 -8
  17. package/dist/core/codec/encode.d.ts +4 -4
  18. package/dist/core/codec/encode.js +10 -10
  19. package/dist/core/codec/field.d.ts +21 -26
  20. package/dist/core/codec/field.js +33 -32
  21. package/dist/core/codec/index.js +5 -8
  22. package/dist/core/codec/inputrules.js +56 -18
  23. package/dist/core/codec/islands.d.ts +11 -7
  24. package/dist/core/codec/islands.js +16 -10
  25. package/dist/core/codec/keymap.d.ts +5 -6
  26. package/dist/core/codec/keymap.js +8 -7
  27. package/dist/core/codec/lists.d.ts +3 -3
  28. package/dist/core/codec/lists.js +32 -13
  29. package/dist/core/codec/marks.d.ts +1 -1
  30. package/dist/core/codec/marks.js +1 -1
  31. package/dist/core/codec/positions.js +1 -1
  32. package/dist/core/codec/prose.css +230 -215
  33. package/dist/core/codec/schema.js +4 -4
  34. package/dist/core/codec/slash.d.ts +39 -41
  35. package/dist/core/codec/slash.js +0 -0
  36. package/dist/core/codec/table-view.d.ts +12 -16
  37. package/dist/core/codec/table-view.js +488 -292
  38. package/dist/core/codec/table.d.ts +42 -15
  39. package/dist/core/codec/table.js +70 -26
  40. package/dist/core/errors.d.ts +5 -5
  41. package/dist/core/index.js +2 -2
  42. package/dist/core/lifecycle.js +1 -1
  43. package/dist/core/rebind.svelte.d.ts +2 -2
  44. package/dist/core/rebind.svelte.js +4 -4
  45. package/dist/core/teardown.d.ts +1 -1
  46. package/dist/core/teardown.js +1 -1
  47. package/dist/core/theme.css +144 -54
  48. package/dist/preset/index.css +1 -1
  49. package/dist/preset/recipes.css +31 -29
  50. package/dist/preset/scale.css +36 -25
  51. package/dist/preview/Preview.svelte +12 -10
  52. package/dist/preview/Preview.svelte.d.ts +3 -3
  53. package/dist/preview/bridge.js +4 -4
  54. package/dist/preview/controller.js +3 -3
  55. package/dist/preview/geometry.d.ts +2 -2
  56. package/dist/preview/geometry.js +2 -2
  57. package/dist/preview/index.js +3 -3
  58. package/dist/preview/overlay.d.ts +1 -1
  59. package/dist/preview/overlay.js +3 -3
  60. package/dist/preview/paint.js +15 -17
  61. package/dist/visual/ArrayField.svelte +80 -66
  62. package/dist/visual/ArrayField.svelte.d.ts +1 -1
  63. package/dist/visual/BooleanField.svelte +8 -6
  64. package/dist/visual/BooleanField.svelte.d.ts +2 -2
  65. package/dist/visual/Card.svelte +151 -325
  66. package/dist/visual/Card.svelte.d.ts +2 -6
  67. package/dist/visual/CardControls.svelte +1 -1
  68. package/dist/visual/DateField.svelte +19 -19
  69. package/dist/visual/DateField.svelte.d.ts +3 -3
  70. package/dist/visual/DiagnosticList.svelte +1 -1
  71. package/dist/visual/EnumField.svelte +16 -16
  72. package/dist/visual/EnumField.svelte.d.ts +2 -2
  73. package/dist/visual/Field.svelte +128 -115
  74. package/dist/visual/Field.svelte.d.ts +1 -1
  75. package/dist/visual/FieldHint.svelte +24 -42
  76. package/dist/visual/FieldHint.svelte.d.ts +1 -1
  77. package/dist/visual/FieldLabel.svelte +8 -8
  78. package/dist/visual/FormatPopover.svelte +80 -35
  79. package/dist/visual/NumberField.svelte +5 -5
  80. package/dist/visual/NumberField.svelte.d.ts +1 -1
  81. package/dist/visual/ObjectField.svelte +9 -9
  82. package/dist/visual/ObjectField.svelte.d.ts +1 -1
  83. package/dist/visual/ProseArrayElement.svelte +11 -13
  84. package/dist/visual/ProseArrayElement.svelte.d.ts +1 -1
  85. package/dist/visual/ProseField.svelte +53 -84
  86. package/dist/visual/ProseField.svelte.d.ts +12 -17
  87. package/dist/visual/SlashMenu.svelte +42 -16
  88. package/dist/visual/TextField.svelte +5 -5
  89. package/dist/visual/TextField.svelte.d.ts +1 -1
  90. package/dist/visual/TipsCard.svelte +43 -28
  91. package/dist/visual/VisualEditor.svelte +6 -6
  92. package/dist/visual/VisualEditorInner.svelte +122 -129
  93. package/dist/visual/controls.css +74 -30
  94. package/dist/visual/diagnostics.d.ts +5 -5
  95. package/dist/visual/diagnostics.js +3 -3
  96. package/dist/visual/domid.js +3 -3
  97. package/dist/visual/ext.d.ts +1 -1
  98. package/dist/visual/ext.js +1 -1
  99. package/dist/visual/icons/nodes.d.ts +3 -0
  100. package/dist/visual/icons/nodes.js +2 -1
  101. package/dist/visual/index.js +4 -4
  102. package/dist/visual/leaves.d.ts +3 -3
  103. package/dist/visual/leaves.js +1 -1
  104. package/dist/visual/links.d.ts +24 -0
  105. package/dist/visual/links.js +67 -0
  106. package/dist/visual/props.d.ts +8 -8
  107. package/dist/visual/signals.d.ts +6 -6
  108. package/dist/visual/strings.d.ts +14 -19
  109. package/dist/visual/strings.js +5 -7
  110. package/dist/visual/structure.d.ts +24 -17
  111. package/dist/visual/structure.js +8 -8
  112. package/dist/visual/synced.svelte.d.ts +1 -1
  113. package/dist/visual/synced.svelte.js +4 -4
  114. package/dist/visual/tips.d.ts +2 -2
  115. package/dist/visual/tips.js +3 -3
  116. package/package.json +1 -1
@@ -1,16 +1,14 @@
1
1
  <!--
2
- One card block: the header (composable cards only: an inline-editable title over
3
- a full-width rename region, and the reorder/
4
- delete controls), the grouped field list packed by `ui.group`/`ui.compact`, and
5
- the body prose leaf. `main` is headerless with no controls. Every prose leaf
6
- takes a parent-built LIVE address so a card reorder re-targets its commits
7
- without a remount.
2
+ One card block: the header (composable cards only), the field list grouped by
3
+ `ui.group`/`ui.compact`, and the body prose leaf. `main` is headerless with no
4
+ controls. Every prose leaf takes a parent-built live address, so a card reorder
5
+ re-targets its commits without a remount.
8
6
  -->
9
7
  <script lang="ts">
10
8
  import { wording } from './strings.js';
11
9
 
12
- // The surface's words, ambient from the editor root; the package's English
13
- // off-tree, so this component renders standalone too.
10
+ // Ambient from the editor root, with the package's English off-tree, so this
11
+ // component renders standalone too.
14
12
  const t = wording();
15
13
  import { untrack } from 'svelte';
16
14
  import Icon from './icons/Icon.svelte';
@@ -44,23 +42,19 @@
44
42
  interface Props {
45
43
  card: CardModel;
46
44
  doc: Document;
47
- /** The schema a prose leaf reads its content through (`ProseField`). */
48
45
  quill: Quill;
49
46
  index: number;
50
47
  isFirst: boolean;
51
48
  isLast: boolean;
52
49
  kinds: string[];
53
50
  ops: CardOps;
54
- /** How an option `ops.enumAllowed` refuses draws. Nothing about it is per-card,
55
- * so it sits beside the bundle rather than in it. */
56
51
  enumDisallowed?: 'hide' | 'disable';
57
52
  onFocus?: (addr: Addr) => void;
58
53
  onCaretMove?: (addr: Addr, pos: number) => void;
59
54
  onChange?: (addr: Addr) => void;
60
55
  onError?: EditorErrorHandler;
61
- /** The editor's leaf registry (`leaves.ts`): every field in this card registers
62
- * its landing handle, which is what the editor's `focusField`/`setCaret`
63
- * resolve a `DocPath` to. */
56
+ /** Every field in this card registers its landing handle here, which is what the
57
+ * editor's `focusField`/`setCaret` resolve a `DocPath` to. */
64
58
  leaves?: LeafRegistry;
65
59
  }
66
60
  let {
@@ -90,18 +84,15 @@
90
84
  });
91
85
  });
92
86
 
93
- // Inline-editable title: select-all on entry so a title reads as
94
- // text you replace, and Enter/Escape as commit/revert. Rename stays LIVE on
95
- // input: `titleAtFocus` is the pre-edit value Escape rolls back to.
87
+ // The pre-edit value Escape rolls back to; the rename itself stays live on input.
96
88
  let titleAtFocus = '';
97
89
  function onTitleFocus(e: FocusEvent): void {
98
90
  titleAtFocus = localTitle;
99
91
  (e.currentTarget as HTMLInputElement).select();
100
92
  }
101
- // A mouse press on an UNFOCUSED title would place a caret on mouseup, collapsing
102
- // the focus-time select-all. Take focus manually and suppress that caret so a
103
- // click-to-enter selects all too; a press while already focused stays normal, so
104
- // clicking mid-title to place the caret still works.
93
+ // A press on an unfocused title would place a caret on mouseup, collapsing the
94
+ // focus-time select-all. Take focus manually and suppress that caret; a press while
95
+ // already focused stays normal, so clicking mid-title to place the caret still works.
105
96
  function onTitleMousedown(e: MouseEvent): void {
106
97
  const el = e.currentTarget as HTMLInputElement;
107
98
  if (document.activeElement !== el) {
@@ -109,11 +100,9 @@
109
100
  el.focus();
110
101
  }
111
102
  }
112
- // The rename region extends the title's hit area to the header's free width.
113
- // A press on that empty space focuses the input (which selects all, the
114
- // same click-to-enter the title itself gets) and preventDefault keeps the press
115
- // from starting a text selection on the wrapper. A press ON the input is left to
116
- // `onTitleMousedown`, which owns the already-focused caret-placement case.
103
+ // A press on the region's empty width enters the title edit; preventDefault keeps it
104
+ // from starting a text selection on the wrapper. A press on the input itself is left
105
+ // to `onTitleMousedown`, which owns the already-focused caret-placement case.
117
106
  function onRenameMousedown(e: MouseEvent): void {
118
107
  const input = (e.currentTarget as HTMLElement).querySelector('input');
119
108
  if (!input || e.target === input) return;
@@ -135,36 +124,32 @@
135
124
  }
136
125
  }
137
126
 
138
- // Group accordion. Ungrouped fields (`group == null`) render above,
139
- // always visible; grouped sections collapse into a one-open-at-a-time accordion.
127
+ /** Not `--_qm-glyph-control`: that rung centres a glyph in a tap target, where this
128
+ * one stands in a run of text at the body rung and is sized to sit level with it. */
129
+ const CHEVRON = 16;
130
+
140
131
  const ungrouped = $derived(card.sections.filter((s) => s.group == null));
141
132
  const grouped = $derived(card.sections.filter((s) => s.group != null));
142
133
 
143
- // Whether there is a metadata block to bracket at all. A schema that
144
- // declares no fields still gets a body (`bodyEnabled` defaults true), and a bracket
145
- // around nothing is worse than none: with no content between them the two
146
- // horizontals land on one y and paint as a stroke of twice the width, which is the
147
- // one thing a figure claiming one width cannot do.
134
+ // A schema that declares no fields still gets a body, and a bracket around nothing is
135
+ // worse than none: with no content between them the two horizontals land on one y and
136
+ // paint as a single stroke of twice the width.
148
137
  const hasMeta = $derived(ungrouped.length > 0 || grouped.length > 0);
149
138
 
150
- // Ephemeral session state: the open group's id (`null` = all collapsed). Seeded
151
- // ONCE from the card's shape (structure.initialExpandedGroup); Card is keyed by
152
- // stable id, so this survives the VisualEditor re-derive that reassigns `card`
153
- // and resets only on a remount (reload). Not reconciled to later section changes:
154
- // a retype is the one reshape, and the open group stays where it still exists.
139
+ // Ephemeral session state, seeded once from the card's shape. Card is keyed by stable
140
+ // id, so this survives the VisualEditor re-derive that reassigns `card` and resets
141
+ // only on a remount. Not reconciled to later section changes: a retype is the one
142
+ // reshape, and the open group stays where it still exists.
155
143
  // svelte-ignore state_referenced_locally
156
144
  let expanded = $state<string | null>(initialExpandedGroup(card.sections));
157
- // The accordion's two elements per group, by group id. A closing panel goes
158
- // `inert` with whatever it held still focused, and `inert` does not say where
159
- // focus should go: the browser drops it on the body, losing the user's place in
160
- // the card. So the close hands focus to the header, which is what reopens the
161
- // section.
162
145
  let headers = $state<Record<string, HTMLButtonElement | undefined>>({});
163
146
  let panels = $state<Record<string, HTMLElement | undefined>>({});
164
147
  /**
165
- * Move to `next` (`null` = all closed), evacuating focus from the group this
166
- * closes. Every write to `expanded` goes through here: a group also closes when
167
- * ANOTHER opens, which is not a gesture the user aimed at the closing section.
148
+ * Move to `next` (`null` = all closed). A closing panel goes `inert` with whatever it
149
+ * held still focused, and `inert` does not say where focus should go — the browser
150
+ * drops it on the body — so the close hands focus to the header, which is what
151
+ * reopens the section. Every write to `expanded` goes through here: a group also
152
+ * closes when another opens.
168
153
  */
169
154
  function setExpanded(next: string | null): void {
170
155
  const closing = expanded;
@@ -177,13 +162,10 @@
177
162
  }
178
163
 
179
164
  /**
180
- * Open the group holding leaf `key`, if this card has one. A collapsed panel is
181
- * clipped to zero height and `inert` rather than unmounted, so a caret placed
182
- * inside it does not land at all and the arrival wash goes unseen.
183
- * `VisualEditor.setCaret` calls this on every card and then waits a flush before
184
- * landing: `inert` clears when the panel renders open, not when `expanded` is
185
- * assigned. The card keeps owning `expanded`, and a card that does not hold the
186
- * key does nothing.
165
+ * Open the group holding leaf `key`, if this card has one: a caret placed in a
166
+ * collapsed panel does not land at all. `VisualEditor.setCaret` calls this on every
167
+ * card and then waits a flush before landing, since `inert` clears when the panel
168
+ * renders open, not when `expanded` is assigned.
187
169
  *
188
170
  * A call, not a prop: a reveal is an event, and modelling it as state needs a
189
171
  * fresh-identity wrapper to re-fire and an `untrack` to keep the per-keystroke
@@ -196,13 +178,10 @@
196
178
 
197
179
  let el = $state<HTMLElement | undefined>(undefined);
198
180
  /**
199
- * Bring this card into view after the structure mutation that placed it:
200
- * `center` for an insert, `nearest` for a reorder that only needs to stay
201
- * on screen. `scrollIntoView` walks to its own scroll ancestor, so this is
202
- * indifferent to whether the package or the consumer owns the scroll container.
203
- *
204
- * The reduced-motion guard is JS rather than the CSS the accordion uses: a
205
- * scroll's behaviour is an argument, not a transition a media query can cancel.
181
+ * Bring this card into view after the structure mutation that placed it: `center` for
182
+ * an insert, `nearest` for a reorder. The reduced-motion guard is JS rather than the
183
+ * CSS the accordion uses: a scroll's behaviour is an argument, not a transition a
184
+ * media query can cancel.
206
185
  */
207
186
  export function scrollIntoViewCard(block: ScrollLogicalPosition): void {
208
187
  const reduce = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
@@ -210,9 +189,6 @@
210
189
  }
211
190
  </script>
212
191
 
213
- <!-- `qm-main` is a structural marker and carries no fill: a tone here would bet on
214
- what the consumer put behind the column, and structure already says which card
215
- this is. -->
216
192
  <section
217
193
  bind:this={el}
218
194
  class="qm-card"
@@ -224,19 +200,9 @@
224
200
  {:else}
225
201
  {#if !card.isMain}
226
202
  <header class="qm-card-header">
227
- <!-- The rename target is the header's whole free width, not the title's text
228
- box. The autosize keeps that box exactly as wide as its
229
- text; this wrapper makes the rest of the row enter the edit too. -->
230
203
  <!-- svelte-ignore a11y_no_static_element_interactions -->
231
204
  <div class="qm-card-rename" onmousedown={onRenameMousedown}>
232
- <!-- Autosize: the sizer span's ::after mirrors the text and dictates the grid
233
- cell width, so the overlaid input grows with content and reads as text,
234
- not a persistent box. `data-value` falls back to the
235
- placeholder so an empty title still reserves its resolved-title width. -->
236
205
  <span class="qm-card-title-sizer" data-value={localTitle || card.titlePlaceholder}>
237
- <!-- `qm-focus-ring` is this control's ONLY focus indicator: the ink step
238
- cannot be one, since hover already takes that step. `:focus-visible`, so
239
- a press enters the edit unringed while a tab into it draws one. -->
240
206
  <input
241
207
  class="qm-card-title qm-focus-ring"
242
208
  value={localTitle}
@@ -266,45 +232,26 @@
266
232
  {/if}
267
233
 
268
234
  <div class="qm-card-body">
269
- <!-- The metadata block, and the thing the bracket brackets: every
270
- field the card has, ungrouped and grouped alike, inside one element whose
271
- top and bottom EDGES are the bracket's two horizontals. Anchoring them here
272
- rather than on the header and on the accordion is what lets an open
273
- section's vertical actually reach them: a rule drawn in the card body's
274
- gap instead would stand clear of where that vertical stops, and the corner
275
- it exists to close would miss. It also survives the shapes the accordion
276
- does not have: a card with no groups still terminates above its body,
277
- because the rule belongs to the fields, not to the sections.
278
-
279
- Rendered only when there ARE fields: the bracket is a statement about a
280
- block, so with no block there is nothing to say, and an empty wrapper
281
- between two rules is what collapses them onto each other. -->
235
+ <!-- Every field the card has, ungrouped and grouped alike, inside one element
236
+ whose top and bottom edges are the bracket's two horizontals. Anchored here
237
+ rather than on the header and on the accordion so an open section's vertical
238
+ reaches them, and so a card with no groups still terminates above its body. An
239
+ empty wrapper between the two rules would collapse them onto each other. -->
282
240
  {#if hasMeta}
283
241
  <div
284
242
  class="qm-card-meta"
285
243
  class:qm-meta-top={!card.isMain}
286
244
  class:qm-meta-bottom={card.hasBody}
287
245
  >
288
- <!-- Ungrouped fields render above the accordion, always visible:
289
- a label-less section has no header to toggle, and these read as the card's
290
- primary fields. -->
291
246
  {#each ungrouped as section (section.group ?? '_ungrouped')}
292
247
  <div class="qm-section">
293
248
  {@render sectionFields(section.fields)}
294
249
  </div>
295
250
  {/each}
296
251
 
297
- <!-- Group accordion: each `ui.group` is a collapsible section,
298
- one open at a time. The header toggles; the panel slides via a
299
- 0fr↔1fr grid row (the `slow` duration rung). The sections share ONE wrapper
300
- with no gap of its own: each header's symmetric padding is the whole
301
- inter-group rhythm. The card body's gap still separates this
302
- block from the ungrouped fields and the body leaf.
303
-
304
- Rendered only when a card HAS groups, so an empty accordion is not the
305
- bracket's last child: the bottom rule's inset belongs to whichever block
306
- actually abuts it, and a zero-height wrapper standing there would take it
307
- from the ungrouped fields that do. -->
252
+ <!-- One open at a time. Rendered only when a card has groups, so an empty
253
+ accordion is not the bracket's last child: the bottom rule's inset belongs
254
+ to whichever block abuts it. -->
308
255
  {#if grouped.length}
309
256
  <div class="qm-groups">
310
257
  {#each grouped as section (section.group)}
@@ -320,13 +267,15 @@
320
267
  aria-controls={panelId}
321
268
  onclick={() => toggleGroup(group)}
322
269
  >
323
- <Icon name="chevron-right" class="qm-group-chevron" size={14} />{section.label}
270
+ <Icon
271
+ name="chevron-right"
272
+ class="qm-group-chevron"
273
+ size={CHEVRON}
274
+ />{section.label}
324
275
  </button>
325
- <!-- `inert` is what makes closed mean CLOSED: the panel is clipped, not
326
- unmounted, so without it every field
276
+ <!-- The panel is clipped, not unmounted, so without `inert` every field
327
277
  in a hidden section keeps its place in the tab order and in the a11y
328
- tree, under a header announcing `aria-expanded="false"`. Paint is
329
- untouched, so the 0fr↔1fr slide is the same slide. -->
278
+ tree, under a header announcing `aria-expanded="false"`. -->
330
279
  <div
331
280
  class="qm-group-panel"
332
281
  id={panelId}
@@ -344,13 +293,9 @@
344
293
  </div>
345
294
  {/if}
346
295
 
347
- <!-- The body: no label and no box. "Body" names the surface the
348
- card is printed on, so a label restates it; and the accessible name
349
- survives on the leaf itself, so the region
350
- is still announced. Unframed is the point rather than a saving: the body is
351
- the ONLY surface in the card without an edge, which is what makes it read
352
- as paper instead of as one more field, and the bracket's bottom rule
353
- already does the separating a box was doing. -->
296
+ <!-- Unframed: the body is the only surface at the card's own tone, where every
297
+ other typed value sits in a well cut into it, and that is what makes it read as
298
+ paper instead of as one more field. -->
354
299
  {#if card.hasBody}
355
300
  <div class="qm-body-leaf">
356
301
  <ProseField
@@ -374,10 +319,6 @@
374
319
  {/if}
375
320
  </section>
376
321
 
377
- <!-- A section's fields as ONE grid: shared by the ungrouped block and the accordion
378
- panels so both render a group's fields identically. Rows are the
379
- grid's business: fields carry a span and auto-place, so nothing here re-derives
380
- structure on resize and a prose leaf's key never moves. -->
381
322
  {#snippet sectionFields(fields: FieldModel[])}
382
323
  <div class="qm-fields">
383
324
  {#each placeFields(fields) as { field: f, span } (f.name)}
@@ -405,12 +346,10 @@
405
346
  </div>
406
347
  {/snippet}
407
348
 
408
- <!-- Recovery shell for an un-schemable card: a card whose `kind` has no
409
- schema (foreign kind, or a schema declaring no `card_kinds`). It stays VISIBLE and
410
- REMOVABLE (its fields/body/`$ext` remain in the Document) so it is never a data
411
- trap. Retyping to a declared kind projects it against that kind on the next
412
- derive: `setCardKind` swaps the kind and leaves payload and body untouched, so
413
- no field is dropped; with no kinds to offer, delete is the only exit. -->
349
+ <!-- A card whose `kind` has no schema stays visible and removable, its fields, body and
350
+ `$ext` intact in the Document, so it is never a data trap. `setCardKind` swaps the kind
351
+ and leaves payload and body untouched, so retyping drops no field; with no kinds to
352
+ offer, delete is the only exit. -->
414
353
  {#snippet recoveryShell()}
415
354
  <header class="qm-card-header">
416
355
  <span class="qm-card-title-static">{humanize(card.kind)}</span>
@@ -449,8 +388,6 @@
449
388
  </div>
450
389
  {/snippet}
451
390
 
452
- <!-- The declared card kinds as `<option>`s for the recovery shell's retype,
453
- the one place a card's kind changes after insert. -->
454
391
  {#snippet kindOptions()}
455
392
  {#each kinds as k (k)}
456
393
  <option value={k}>{humanize(k)}</option>
@@ -458,16 +395,14 @@
458
395
  {/snippet}
459
396
 
460
397
  <style>
398
+ /* An island, and the tone is the whole of what makes it one: the card is the base
399
+ plane and the column behind it is the sunken one (ARCHITECTURE §Styling). The inset
400
+ is the same rung as the gap, so what separates the header from the fields is what
401
+ separates the fields from the card's edge. */
461
402
  .qm-card {
462
- border: var(--_qm-border-width) solid var(--_qm-border);
463
403
  border-radius: var(--_qm-radius);
464
- /* Uniform inset on every side: a body-shown and a
465
- body-hidden card stay symmetric, every left edge on one gutter. The SAME rung
466
- as the gap between the card's stacked regions below: the card's inset and its
467
- internal rhythm are one number, so what separates the header from the fields is
468
- what separates the fields from the card's edge. */
469
404
  padding: var(--_qm-space-3);
470
- background: var(--_qm-surface-raised);
405
+ background: var(--_qm-surface);
471
406
  display: flex;
472
407
  flex-direction: column;
473
408
  gap: var(--_qm-space-3);
@@ -478,46 +413,25 @@
478
413
  justify-content: space-between;
479
414
  gap: var(--_qm-space-2);
480
415
  }
481
- /* The header's other end, hung so the row is balanced: the title's ink sits on the
482
- card's gutter, and this puts the control glyphs on the opposite one. What stands
483
- between a glyph and the region's edge is the tap target's own centring — half the
484
- difference between `--_qm-tap-min` and the glyph — which is a target, not a
485
- rhythm, and so is exactly what a gutter should not be measured from. Both ends
486
- therefore hang by the chrome they carry and by nothing else; the title's is now
487
- zero, and this one is the arithmetic of two rungs rather than a rung of its own,
488
- because the amount is off the space scale (5px at the shipped dials) and a scale
489
- that could express it would be a scale with a step nothing else uses.
490
-
491
- It is the fill, not the box, that overhangs the gutter here: an icon button rests
492
- unboxed and fills only under the pointer, so the
493
- thing crossing the line is a transient target wash, not an edge the card claims. */
416
+ /* The overhang is the tap target's own centring given back, so the glyphs sit on the
417
+ card's gutter rather than the targets around them. It is the fill that crosses the
418
+ line, not a box: an icon button rests unboxed and fills only under the pointer. */
494
419
  .qm-card-header-right {
495
420
  display: flex;
496
421
  align-items: center;
497
422
  gap: var(--_qm-space-2);
498
423
  margin-right: calc(-1 * (var(--_qm-tap-min) - var(--_qm-glyph-control)) / 2);
499
424
  }
500
- /* Reveal the reorder chevrons while the pointer or the caret is in the card
501
- (CardControls owns the default hidden state). Focus is read off the CARD, so a
502
- caret in any leaf, the title, or a chevron itself holds the reveal: hover
503
- alone would strand the pair on keyboard and touch. This is the card's whole
504
- active treatment; nothing marks the section itself. */
425
+ /* Focus as well as hover, so a caret in any leaf, the title, or a chevron holds the
426
+ reveal: hover alone would strand the pair on keyboard and touch. */
505
427
  .qm-card:hover :global(.qm-card-reorder),
506
428
  .qm-card:focus-within :global(.qm-card-reorder) {
507
429
  opacity: 1;
508
430
  }
509
- /* The rename hit region: the header's free width, full height, so a
510
- press anywhere left of the controls enters the title edit. `min-width: 0` lets
511
- it shrink past the title's intrinsic width; `align-self: stretch` beats the
512
- header's `align-items: center` so the region is the row's whole height rather
513
- than the input's.
514
-
515
- It hangs by NOTHING, which is the whole of what the title carrying no box buys:
516
- the first character lands on the card's gutter with the field list, the
517
- bracket's rules and the body's first character, because nothing stands between
518
- the region's edge and the glyph. The stretch is also what
519
- keeps the target legal with the padding gone: WCAG 2.5.8's floor is the ROW's,
520
- which the controls hold at `--_qm-tap-min`, so the input never carried it. */
431
+ /* The rename hit region is the header's free width at full height, so a press
432
+ anywhere left of the controls enters the title edit. `align-self: stretch` beats the
433
+ header's centring, which is also what keeps the target legal with no padding: WCAG
434
+ 2.5.8's floor is the row's, which the controls hold at `--_qm-tap-min`. */
521
435
  .qm-card-rename {
522
436
  flex: 1;
523
437
  min-width: 0;
@@ -526,25 +440,19 @@
526
440
  align-items: center;
527
441
  cursor: text;
528
442
  }
529
- /* Autosize sizer: an inline-grid whose ::after mirrors the text
530
- into the single cell, so the overlaid input tracks its content width. Bounded
531
- to the rename region's width (`min-width: 0` + `max-width: 100%`), which the
532
- header's space-between keeps clear of the controls. The input inherits the type
533
- tokens (`font: inherit`, leading included) so the mirror and the input measure
534
- alike; which is why the leading rung is declared here and not on the input. */
443
+ /* Autosize: the ::after mirrors the text into the same grid cell as the input, so the
444
+ overlaid input tracks its content width. Both carry the same nothing for a box model
445
+ and inherit the same type tokens, which is what makes the two measure alike. */
535
446
  .qm-card-title-sizer {
536
447
  display: inline-grid;
537
448
  align-items: center;
538
449
  min-width: 0;
539
450
  max-width: 100%;
540
451
  font-size: var(--_qm-text-title);
541
- font-weight: var(--_qm-weight-label);
452
+ font-weight: var(--_qm-weight-mid);
542
453
  line-height: var(--_qm-leading-tight);
543
454
  font-family: inherit;
544
455
  }
545
- /* The mirror has no box model to match, because the input has none: with the
546
- padding and the hairline gone from both, the two measure alike by carrying the
547
- same nothing. */
548
456
  .qm-card-title-sizer::after {
549
457
  content: attr(data-value) ' ';
550
458
  grid-area: 1 / 1;
@@ -552,28 +460,17 @@
552
460
  white-space: pre;
553
461
  min-width: 2ch;
554
462
  }
555
- /* The title draws NO box, in any state. A box is
556
- what says "type here", and a card title is already the one line of a card that
557
- reads as its name; the region's `cursor: text` and the caret that lands on a
558
- press say the rest. A hover-summoned box was also the inverse of the recipe every
559
- other control follows, where the box rests drawn and hover changes nothing, and
560
- it filled to `--_qm-surface` — the page rung,
561
- BELOW the card it sits on, so the one hover on this surface that receded instead
562
- of lifting.
563
-
564
- What replaces it is an ink step, the same cue the group header takes for the same
565
- reason: a wide borderless target drawing no box has no other way to answer the
566
- pointer. Rest is `--_qm-ink-label`, which is where the recovery shell's static
567
- title already sits, so the editable title and its unschemable twin finally agree
568
- on tone as well as on the three type rungs they share. */
463
+ /* The title draws no box in any state: a card title is already the one line that
464
+ reads as the card's name, and the region's `cursor: text` says the rest. The ink step
465
+ replaces it, the same cue the group header takes — a wide borderless target has no
466
+ other way to answer the pointer. */
569
467
  .qm-card-title {
570
468
  grid-area: 1 / 1;
571
469
  width: 100%;
572
470
  min-width: 0;
573
471
  font: inherit;
574
- /* `font: inherit` does not carry colour: an actual form control, unlike this
575
- rule's neighbours, defaults to the UA's own text colour rather than the
576
- header's ink without this line. */
472
+ /* `font: inherit` does not carry colour: a form control defaults to the UA's own
473
+ text colour rather than the header's ink without this line. */
577
474
  color: var(--_qm-ink-label);
578
475
  border: none;
579
476
  padding: 0;
@@ -584,121 +481,79 @@
584
481
  .qm-card-title:focus {
585
482
  color: var(--_qm-ink);
586
483
  }
587
- /* One rhythm for the card's stacked regions and for the metadata block inside it:
588
- the gap between a field list and the body is the gap between two field lists. */
589
484
  .qm-card-body,
590
485
  .qm-card-meta {
591
486
  display: flex;
592
487
  flex-direction: column;
593
488
  gap: var(--_qm-space-3);
594
489
  }
595
- /* The metadata bracket: the card's chrome brackets its metadata, and
596
- the body is what falls outside it. Three strokes at ONE rung: the two
597
- horizontals here and the open section's vertical below; because a bracket whose
598
- sides disagree on width or tone reads as three unrelated lines rather than as
599
- one figure. That sameness is the whole effect; it is why the vertical takes
600
- `--_qm-border` and not `--_qm-accent`, which would be ornament with the chevron
601
- and the header's ink step already saying open.
490
+ /* The metadata bracket, in three strokes at one rung: these two horizontals and the
491
+ open section's vertical below. A bracket whose sides disagree on width or tone reads
492
+ as three unrelated lines rather than one figure, which is why the vertical takes
493
+ `--_qm-border` and not `--_qm-accent`.
602
494
 
603
- Both horizontals are conditional, and each condition is what the stroke means: a
604
- top rule is the line under a card title, so the headerless `main` has none and
605
- its bracket starts at the card's own top inset; a bottom rule divides fields from
606
- body, so a card with no body has nothing to divide. The bracket is therefore
607
- sometimes two strokes and sometimes one: an open figure by construction, which
608
- is what distinguishes it from a second box inside the card. */
495
+ Each condition is what its stroke means: a top rule is the line under a card title,
496
+ so the headerless `main` has none; a bottom rule divides fields from body, so a card
497
+ with no body has nothing to divide. Sometimes two strokes and sometimes one — an open
498
+ figure, which is what distinguishes it from a second box inside the card. */
609
499
  .qm-meta-top {
610
500
  border-top: var(--_qm-border-width) solid var(--_qm-border);
611
501
  }
612
502
  .qm-meta-bottom {
613
503
  border-bottom: var(--_qm-border-width) solid var(--_qm-border);
614
504
  }
615
- /* No inset between a horizontal and the accordion, deliberately: an open section's
616
- vertical has to REACH the rule to close a corner against it, and any padding here
617
- is the distance by which it would miss. The accordion needs none anyway: a group
618
- header's symmetric padding is the whole inter-group rhythm.
619
-
620
- A block of FIELDS has no padding of its own, so it takes the inset wherever it
621
- abuts a rule; three places, since three blocks can: an ungrouped block under the
622
- top rule, an ungrouped block over the bottom rule (only reachable because the
623
- accordion's `{#if}` keeps an empty wrapper out of last position), and the last
624
- section's panel when it is open, whose inset is otherwise zero at the bottom and
625
- would stand its final row of controls directly on the stroke. The panel's padding
626
- is inside `.qm-group`, so the vertical still runs the full height to the corner.
627
- Ungrouped fields always render first, so the top selector needs no qualifier. */
505
+ /* The inset sits on the fields rather than on the block that holds them: an open
506
+ section's vertical has to reach the rule to close a corner against it, and padding
507
+ between the two is the distance by which it would miss. Unqualified by open — a shut
508
+ panel clips it. */
628
509
  .qm-meta-top > .qm-section {
629
510
  padding-top: var(--_qm-space-3);
630
511
  }
631
512
  .qm-meta-bottom > .qm-section:last-child,
632
- .qm-meta-bottom .qm-group:last-child.qm-open .qm-group-panel-inner {
513
+ .qm-meta-bottom .qm-group:last-child .qm-group-panel-inner > .qm-fields {
633
514
  padding-bottom: var(--_qm-space-3);
634
515
  }
635
- /* The two query containers: an ungrouped section, and a group panel's inner. Each
636
- is its own, so capacity follows the width the fields actually get: a panel's
637
- inset makes that narrower than the card.
638
-
639
- The ACTION COLUMN is minted here and spent one level down, on the field
640
- (`Field.svelte` holds why it is the field's inset and not this container's). It
641
- is declared on the query container because both of its consumers hang off one:
642
- the fields inside, and the capacity ramp below, which carries the column in its
643
- own arithmetic because the query reads a box the reservation has not narrowed. */
516
+ /* Two query containers, one per kind of section, so capacity follows the width the
517
+ fields actually get: a panel's inset makes that narrower than the card. */
644
518
  .qm-section,
645
519
  .qm-group-panel-inner {
646
- --action-col: calc(var(--_qm-tap-min) + var(--_qm-space-2));
647
520
  container-type: inline-size;
648
521
  }
649
- /* One grid per section. Capacity is the container's, not JavaScript's:
650
- nothing measures, so there is no observer to loop, no pre-measure pass at the
651
- wrong capacity, and no re-packing to restructure the DOM under a prose leaf.
652
- Fields auto-place, which is what keeps a trailing orphan at its column width
653
- instead of growing to fill the line.
654
-
655
- Capacity steps 1 → 2 → 4, skipping 3: each rung is the width at which a track
656
- still clears the comfortable field minimum (2 needs 32rem, 4 needs 65rem), and
657
- an even capacity is what lets `lone`'s half land on a track boundary. Each rung
658
- pays for a track's own action column as well as the gutters between tracks, since
659
- the reservation is per field now: a rung is `n * (220px + action) + gaps`.
522
+ /* Capacity is the container's, not JavaScript's: nothing measures, so there is no
523
+ observer to loop and no re-packing to restructure the DOM under a prose leaf.
660
524
 
661
- A row-sharing field spans three implicit row tracks (Field.svelte), so `row-gap`
662
- here is the gutter BETWEEN field rows; the tighter one inside a field is the
663
- subgrid's own. The grid takes the container's full width; each field insets its
664
- own action column inside the track it lands in. */
525
+ It steps 1 → 2 → 4, skipping 3: each rung is the width at which a track still clears
526
+ the comfortable field minimum, and an even capacity is what lets `lone`'s half land
527
+ on a track boundary. A field ends on its track's own edge, so a rung is
528
+ `n * 220px + gaps`. */
665
529
  .qm-fields {
666
530
  --cols: 1;
667
531
  --cols-half: 1;
668
532
  display: grid;
669
533
  grid-template-columns: repeat(var(--cols), 1fr);
670
534
  column-gap: var(--_qm-space-2);
671
- row-gap: var(--_qm-space-2);
535
+ row-gap: var(--_qm-space-3);
672
536
  }
673
- @container (min-width: 32rem) {
537
+ @container (min-width: 28rem) {
674
538
  .qm-fields {
675
539
  --cols: 2;
676
540
  --cols-half: 1;
677
541
  }
678
542
  }
679
- @container (min-width: 65rem) {
543
+ @container (min-width: 57rem) {
680
544
  .qm-fields {
681
545
  --cols: 4;
682
546
  --cols-half: 2;
683
547
  }
684
548
  }
685
- /* Group accordion. The header is a toggle
686
- at the field-label rung; the panel slides via a 0fr↔1fr grid row so the height
687
- animates without a magic max-height. */
688
- /* The wrapper carries NO gap: the headers' own padding is the rhythm (see the
689
- markup note). */
690
549
  .qm-groups,
691
550
  .qm-group {
692
551
  display: flex;
693
552
  flex-direction: column;
694
553
  }
695
- /* The bracket's third stroke, on the SECTION rather than on its panel:
696
- spanning the header and the content is what gives it a corner to close into at
697
- the block's edge: a rule that starts below the header has nothing to meet there.
698
- One rung with the two horizontals, held there by `check:style`'s border-width axis.
699
-
700
- Drawn transparent when closed rather than absent, so the 1px it occupies is the
701
- same open or shut and a toggle moves no text. */
554
+ /* The bracket's third stroke, on the section rather than on its panel: spanning the
555
+ header is what gives it a corner to close into at the block's edge. Drawn transparent
556
+ when closed rather than absent, so a toggle moves no text. */
702
557
  .qm-group {
703
558
  border-left: var(--_qm-border-width) solid transparent;
704
559
  transition: border-color var(--_qm-duration-slow) var(--_qm-ease-reverse);
@@ -706,33 +561,17 @@
706
561
  .qm-group.qm-open {
707
562
  border-left-color: var(--_qm-border);
708
563
  }
709
- /* Vertical padding is symmetric and is the TIGHTEST rung that still clears WCAG
710
- 2.5.8's 24×24 floor over the label rung's line box: the header is the whole row,
711
- so adjacent labels share one rhythm with no dead strip outside the button, and a
712
- rung above the floor is a rung spent per section on a card whose scarce axis is
713
- vertical. Horizontal is one rung left and zero right. Left, because the section's
714
- vertical runs down this row: at zero the chevron stands on the stroke with only
715
- the icon box's own bearing between them, which is neither a rung nor a constant:
716
- the glyph's rotation swaps which bearing faces the stroke, so the clearance moves
717
- as the section opens. Right stays at zero because the row is the target and an
718
- inset there is target given back; the left one is inside the button and costs
719
- none.
720
-
721
- A button by tag and NEITHER button family by recipe: it reads no
722
- `--_qm-tap-min` (the padding above already clears the floor) and takes no hover
723
- fill (an ink step, below); so the type a family would have carried is declared
724
- here. `font: inherit` because a UA button inherits no face and `--qm-font` would
725
- stop at this row; then the field-label rung, at the tight leading a single line
726
- takes. Sentence case at that rung, reused rather than a fifth size minted: a
727
- section name is structurally a heading, and uppercase costs it twice: the word
728
- shape a column of them is scanned by, and apparent width, so a long label crowds
729
- sooner. Size, weight and leading all sit after `font`, which is a shorthand that
730
- carries `line-height`.
731
-
732
- On the BUTTON rather than on a span inside it: the row's type is one decision,
733
- and a wrapper that carries nothing else is indirection between the header and
734
- its own label. The label is a text run beside the chevron, and flex gives it an
735
- anonymous item either way. */
564
+ /* Symmetric vertical padding at the tightest rung that still clears WCAG 2.5.8's
565
+ 24×24 floor: the header is the whole row, so adjacent labels share one rhythm with no
566
+ dead strip outside the button. Horizontal is one rung left and zero right — left,
567
+ because at zero the chevron stands on the section's vertical with only the icon box's
568
+ own bearing between them, and the glyph's rotation swaps which bearing faces the
569
+ stroke; right, because the row is the target and an inset there is target given back.
570
+
571
+ `font: inherit` because a UA button inherits no face, then the body rung, one step
572
+ over the field labels beneath it: at the label rung the two read as one register and
573
+ the accordion stops looking like structure. Size, weight and leading sit after
574
+ `font`, a shorthand that carries `line-height`. */
736
575
  .qm-group-header {
737
576
  display: flex;
738
577
  align-items: center;
@@ -742,8 +581,8 @@
742
581
  background: transparent;
743
582
  padding: var(--_qm-space) 0 var(--_qm-space) var(--_qm-space);
744
583
  font: inherit;
745
- font-size: var(--_qm-text-label);
746
- font-weight: var(--_qm-weight-label);
584
+ font-size: var(--_qm-text-body);
585
+ font-weight: var(--_qm-weight-mid);
747
586
  line-height: var(--_qm-leading-tight);
748
587
  cursor: pointer;
749
588
  color: var(--_qm-ink-label);
@@ -752,28 +591,21 @@
752
591
  }
753
592
  .qm-group-header :global(.qm-group-chevron) {
754
593
  flex-shrink: 0;
594
+ display: block;
595
+ transform: rotate(0deg);
596
+ transform-origin: center;
755
597
  transition: transform var(--_qm-duration-slow) var(--_qm-ease-reverse);
756
598
  }
757
- /* An ink step, not a hue: the three status hues are the only exits from the
758
- greyscale, and a section standing open is not a status anyway.
759
-
760
- HOVER'S ALONE, though the step suits open as well. `color` is one transition
761
- declaration, and open is the accordion's gesture where hover is its own, so the two
762
- share a rung and one of them is wrong: an ink step at the slow rung lags the
763
- pointer, and at the fast one it settles 80ms before the panel it belongs to.
764
- Hover keeps the property because open has another cue in the
765
- chevron's rotation and hover has none, a big borderless target drawing no box. */
766
- .qm-group-header:hover {
599
+ /* Open shares hover's ink step: a big borderless target draws no box, so the ink is
600
+ the only cue an open section keeps once the pointer leaves it. The chevron's
601
+ rotation is the other cue, and stays open-only below. */
602
+ .qm-group-header:hover,
603
+ .qm-group.qm-open .qm-group-header {
767
604
  color: var(--_qm-ink);
768
605
  }
769
606
  .qm-group.qm-open .qm-group-header :global(.qm-group-chevron) {
770
607
  transform: rotate(90deg);
771
608
  }
772
- /* Sliding panel: the grid track goes 0fr→1fr; the inner clips at min-height 0.
773
- Clipping is the whole of what CSS does here: what a closed panel is OUT of is
774
- `inert`'s to say, on the element. The panel
775
- draws no rule of its own: the vertical is the section's (`.qm-group`), which is
776
- what spans the header too. */
777
609
  .qm-group-panel {
778
610
  display: grid;
779
611
  grid-template-rows: 0fr;
@@ -782,42 +614,35 @@
782
614
  .qm-group.qm-open .qm-group-panel {
783
615
  grid-template-rows: 1fr;
784
616
  }
785
- /* The grouped sections' query container (declared above): its left inset is
786
- exactly what makes a panel's usable width differ from the card's. */
617
+ /* Two boxes because the axes clip differently, and the track is the only thing that
618
+ animates: a field sits at its open position from the first frame of the reveal.
619
+
620
+ The clip box takes the inline inset, which costs a shut panel no height and holds the
621
+ queried width still. The block inset goes on the content, where a `0fr` track clips
622
+ it: declared one box out it collapses the content but not itself, and stands under a
623
+ shut header as dead space. Neither is animated, and neither is qualified by open. */
787
624
  .qm-group-panel-inner {
788
- display: flex;
789
- flex-direction: column;
790
- gap: var(--_qm-space-2);
791
625
  min-height: 0;
792
626
  overflow: hidden;
793
- /* Zero when closed, so the insets arrive with the panel: a `0fr` track collapses
794
- the CONTENT, not the padding, and a top inset declared here stands under a
795
- closed header as dead space the header's symmetric padding cannot absorb. The
796
- panel animates its width along with them, and the capacity ramp reads that
797
- width: a closed panel is `inert` and unmeasured either way, so what a rung
798
- crosses mid-slide is a hidden grid. */
799
- padding: 0;
800
- transition: padding var(--_qm-duration-slow) var(--_qm-ease-reverse);
627
+ padding-inline-start: var(--_qm-space-4);
801
628
  }
802
- .qm-group.qm-open .qm-group-panel-inner {
803
- padding: var(--_qm-space-2) 0 0 var(--_qm-space-3);
629
+ .qm-group-panel-inner > .qm-fields {
630
+ padding-block: var(--_qm-space-2);
804
631
  }
805
632
  .qm-body-leaf {
806
633
  display: flex;
807
634
  flex-direction: column;
808
635
  gap: var(--_qm-space);
809
636
  }
810
- /* Recovery shell: dashed edge marks an un-schemable card. The edge
811
- alone carries that: fill stays the card rung, content behind it intact. */
637
+ /* The one card that draws an edge at all: an ordinary card is a plane, so a stroke
638
+ around this one is a state rather than heavier chrome. */
812
639
  .qm-card.qm-unschemable {
813
- border-style: dashed;
640
+ border: var(--_qm-border-width) dashed var(--_qm-border);
814
641
  }
815
- /* One typographic role with the editable title above, so it reads the same three
816
- rungs: including the tight leading a card title takes over the root's reading
817
- rhythm. */
642
+ /* One typographic role with the editable title above. */
818
643
  .qm-card-title-static {
819
644
  font-size: var(--_qm-text-title);
820
- font-weight: var(--_qm-weight-label);
645
+ font-weight: var(--_qm-weight-mid);
821
646
  line-height: var(--_qm-leading-tight);
822
647
  color: var(--_qm-ink-label);
823
648
  }
@@ -841,11 +666,12 @@
841
666
  font-size: var(--_qm-text-label);
842
667
  color: var(--_qm-ink-label);
843
668
  }
669
+ /* The one native control the surface draws, on the box's own recipe (controls.css). */
844
670
  .qm-recovery-retype select {
845
671
  font: inherit;
846
- border: var(--_qm-border-width) solid var(--_qm-border);
672
+ border: none;
847
673
  border-radius: var(--_qm-radius-inner);
848
- background: var(--_qm-surface);
674
+ background: var(--_qm-surface-well);
849
675
  padding: var(--_qm-space-half) var(--_qm-space-2);
850
676
  }
851
677
  </style>