@quillmark/svelte 0.20.0 → 0.21.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 (46) hide show
  1. package/THEMING.md +1 -1
  2. package/dist/core/codec/atoms.js +25 -10
  3. package/dist/core/codec/blocks.d.ts +5 -5
  4. package/dist/core/codec/blocks.js +23 -6
  5. package/dist/core/codec/code.d.ts +3 -3
  6. package/dist/core/codec/code.js +51 -21
  7. package/dist/core/codec/field.d.ts +9 -7
  8. package/dist/core/codec/field.js +22 -11
  9. package/dist/core/codec/inputrules.d.ts +4 -0
  10. package/dist/core/codec/inputrules.js +43 -5
  11. package/dist/core/codec/link-paste.d.ts +11 -0
  12. package/dist/core/codec/link-paste.js +32 -0
  13. package/dist/core/codec/lists.d.ts +2 -2
  14. package/dist/core/codec/lists.js +66 -26
  15. package/dist/core/codec/prose.css +71 -43
  16. package/dist/core/codec/schema.js +55 -11
  17. package/dist/core/codec/slash.js +0 -0
  18. package/dist/core/codec/table-view.d.ts +5 -0
  19. package/dist/core/codec/table-view.js +232 -103
  20. package/dist/core/rungs.d.ts +6 -0
  21. package/dist/core/rungs.js +11 -0
  22. package/dist/core/theme.css +24 -12
  23. package/dist/visual/ArrayField.svelte +168 -48
  24. package/dist/visual/BooleanField.svelte +12 -1
  25. package/dist/visual/Card.svelte +32 -20
  26. package/dist/visual/Card.svelte.d.ts +1 -0
  27. package/dist/visual/CardControls.svelte +15 -15
  28. package/dist/visual/DateField.svelte +93 -64
  29. package/dist/visual/EnumField.svelte +9 -4
  30. package/dist/visual/FieldHint.svelte +22 -5
  31. package/dist/visual/FieldLabel.svelte +3 -3
  32. package/dist/visual/FormatPopover.svelte +25 -9
  33. package/dist/visual/MatrixField.svelte +10 -7
  34. package/dist/visual/ObjectField.svelte +2 -4
  35. package/dist/visual/TipsCard.svelte +2 -5
  36. package/dist/visual/VisualEditorInner.svelte +108 -26
  37. package/dist/visual/controls.css +2 -1
  38. package/dist/visual/icons/Icon.svelte +3 -2
  39. package/dist/visual/icons/Icon.svelte.d.ts +3 -2
  40. package/dist/visual/icons/nodes.d.ts +25 -0
  41. package/dist/visual/icons/nodes.js +8 -0
  42. package/dist/visual/motion.d.ts +25 -3
  43. package/dist/visual/motion.js +58 -9
  44. package/dist/visual/strings.d.ts +6 -5
  45. package/dist/visual/strings.js +3 -3
  46. package/package.json +8 -8
@@ -1,8 +1,8 @@
1
1
  <!--
2
2
  A `date` (or `datetime`) field → a styled segmented date field on bits-ui. The
3
- stored value is a string, `YYYY-MM-DD` or `today`; a cleared control commits
4
- `undefined` (the unset rung): the parent removes the field, and the quill's
5
- `default:` applies. The value-object a date field lowers to is a render-time
3
+ stored value is a string, `YYYY-MM-DD` or `today`; a cleared control commits as the
4
+ focus leaves it, the blank `""` over a `default:`, which stays blank, and `undefined`
5
+ (the unset rung) where nothing is declared, the parent removing the field. The value-object a date field lowers to is a render-time
6
6
  concern: the editor only sees the stored string.
7
7
 
8
8
  `today` is the render date rather than a written one (canon `SCHEMAS.md`), so its
@@ -43,8 +43,15 @@
43
43
 
44
44
  Entering an unset field seats the default as the primitive's value, still at the
45
45
  default rung, so an edit to one segment commits a whole date with the others the
46
- default's. Leaving without an edit unseats it, and nothing is written; so does
47
- clearing a segment.
46
+ default's. Leaving without an edit unseats it, and nothing is written.
47
+
48
+ A segment edit that leaves a segment empty empties the primitive's value, which is
49
+ held rather than committed while the focus stays inside: the first Backspace through
50
+ a date writes nothing unset, and the empty segment shows its hint, not the default's
51
+ digits beside the ones still standing. Leaving settles it: blank where every segment
52
+ is empty, otherwise the stored date back. A field storing no date puts its segments
53
+ back by remounting the primitive, which re-reads an unchanged empty value as nothing
54
+ to sync.
48
55
  -->
49
56
  <script lang="ts">
50
57
  import { DateField as BitsDateField } from 'bits-ui';
@@ -108,13 +115,18 @@
108
115
 
109
116
  const fallbackDate = $derived(toDateValue(fallback?.slice(0, 10) ?? ''));
110
117
  /** The default is the primitive's value: from entering an unset field until an
111
- * edit, a cleared segment or leaving. Part of the projection, so a re-derivation
112
- * neither drops a seat nor restores one over the segments a clear left. */
118
+ * edit or leaving. Part of the projection, so a re-derivation keeps it. */
113
119
  let seat = $state(false);
120
+ /** An emptied value held while the focus stays inside (see the header). Part of the
121
+ * projection too, so a re-derivation restores neither the stored date nor the seat
122
+ * over the segments it emptied. */
123
+ let held = $state(false);
124
+ /** Remounts the primitive when bumped (see the header). */
125
+ let generation = $state(0);
114
126
  // Driven controlled (`value` + `onValueChange`, never `bind:`) so reconciliation
115
127
  // stays the package's.
116
128
  let local = $derived(
117
- value?.slice(0, 10) ?? (seat && fallbackDate ? fallbackDate.toString() : '')
129
+ held ? '' : (value?.slice(0, 10) ?? (seat && fallbackDate ? fallbackDate.toString() : ''))
118
130
  );
119
131
  const parsed = $derived(toDateValue(local));
120
132
  // The date the empty segments ghost, or undefined when there is nothing to ghost:
@@ -122,7 +134,7 @@
122
134
  // fails to parse is AUTHORED-but-malformed, which the empty field states
123
135
  // honestly: the ghost would claim it unset. Held as the parsed value rather than
124
136
  // a boolean so the substitution below narrows on the one fact it needs.
125
- const ghost = $derived(local === '' ? fallbackDate : undefined);
137
+ const ghost = $derived(value == null && local === '' && !held ? fallbackDate : undefined);
126
138
  const seated = $derived(value == null && seat && local !== '');
127
139
  /** Who put `today` here, while the field follows the render date. */
128
140
  const follows = $derived(
@@ -130,6 +142,7 @@
130
142
  );
131
143
 
132
144
  function write(next: string): void {
145
+ held = false;
133
146
  local = next;
134
147
  seat = false;
135
148
  onCommit(next);
@@ -152,7 +165,17 @@
152
165
  if (!within(e) && value == null && fallbackDate) seat = true;
153
166
  }
154
167
  function leave(e: FocusEvent): void {
155
- if (!within(e)) seat = false;
168
+ if (within(e)) return;
169
+ seat = false;
170
+ if (!held) return;
171
+ held = false;
172
+ const filled = [...(wrapEl?.querySelectorAll('[data-segment]') ?? [])].some(
173
+ (s) => s.getAttribute('data-segment') !== 'literal' && !s.hasAttribute('data-ghosted')
174
+ );
175
+ const blank = fallbackDate ? '' : undefined;
176
+ if (!filled) {
177
+ if (value !== blank) onCommit(blank);
178
+ } else if (!value) generation++;
156
179
  }
157
180
 
158
181
  // What one segment prints, and which rung it takes.
@@ -194,61 +217,67 @@
194
217
  onfocusout={leave}
195
218
  onkeydowncapture={shortcut}
196
219
  >
197
- <BitsDateField.Root
198
- value={parsed}
199
- placeholder={fallbackDate}
200
- onValueChange={(d) => {
201
- // `CalendarDate.toString()` is exactly `YYYY-MM-DD`. A cleared or
202
- // half-typed field yields undefined: the unset rung. The primitive reports
203
- // its own value again as a segment loses focus, which is no edit, and over a
204
- // seated default or `today` would write it.
205
- const next = d?.toString() ?? '';
206
- if (next === (parsed?.toString() ?? '')) return;
207
- local = next;
208
- if (!d) seat = false;
209
- onCommit(d?.toString());
210
- }}
211
- >
212
- <!-- `data-ghosted` states the rung the way the enum trigger does. The date
213
- primitive emits no per-segment placeholder marker (only bits' `select`
214
- does), so the tone rides the same attribute, set per segment from the
215
- substitution itself; which is also what keeps a half-typed date's own
216
- digits at full ink while the segments around them ghost. -->
217
- <BitsDateField.Input
218
- class="qm-date qm-control-box qm-focus-ring-within"
219
- aria-label={labelledBy ? undefined : label}
220
- aria-labelledby={labelledBy}
221
- aria-describedby={describedBy}
222
- data-ghosted={ghost ? '' : undefined}
223
- data-default={seated || follows ? '' : undefined}
220
+ {#key generation}
221
+ <BitsDateField.Root
222
+ value={parsed}
223
+ placeholder={fallbackDate}
224
+ onValueChange={(d) => {
225
+ // `CalendarDate.toString()` is exactly `YYYY-MM-DD`. A cleared or
226
+ // half-typed field yields undefined, held until the focus leaves. The
227
+ // primitive reports its own value again as a segment loses focus, which is
228
+ // no edit, and over a seated default or `today` would write it.
229
+ const next = d?.toString() ?? '';
230
+ if (next === (parsed?.toString() ?? '')) return;
231
+ if (!d) {
232
+ held = true;
233
+ return;
234
+ }
235
+ held = false;
236
+ local = next;
237
+ onCommit(next);
238
+ }}
224
239
  >
225
- {#snippet children({ segments })}
226
- <!-- Keyed by index: `part` repeats (the `literal` separators between
227
- segments all carry it) so a part-keyed block collides. -->
228
- {#each segments as seg, i (i)}
229
- {@const shown = segmentText(seg.part, seg.value)}
230
- <BitsDateField.Segment
231
- class="qm-date-segment"
232
- part={seg.part}
233
- data-ghosted={shown.ghosted}
234
- >
235
- {shown.text}
236
- </BitsDateField.Segment>
237
- {/each}
238
- {#if !datetime}
239
- <button
240
- type="button"
241
- class="qm-chip qm-tap-floor qm-focus-ring qm-date-today"
242
- aria-pressed={!!follows}
243
- data-default={follows === 'default' ? '' : undefined}
244
- onclick={toggle}
245
- >
246
- <Icon name={follows ? 'calendar-check' : 'calendar'} />{t.strings.dateToday}
247
- </button>
248
- {/if}
249
- {/snippet}
250
- </BitsDateField.Input>
251
- </BitsDateField.Root>
240
+ <!-- `data-ghosted` states the rung the way the enum trigger does. The date
241
+ primitive emits no per-segment placeholder marker (only bits' `select`
242
+ does), so the tone rides the same attribute, set per segment from the
243
+ substitution itself; which is also what keeps a half-typed date's own
244
+ digits at full ink while the segments around them ghost. -->
245
+ <BitsDateField.Input
246
+ class="qm-date qm-control-box qm-focus-ring-within"
247
+ aria-label={labelledBy ? undefined : label}
248
+ aria-labelledby={labelledBy}
249
+ aria-describedby={describedBy}
250
+ data-ghosted={ghost ? '' : undefined}
251
+ data-default={seated || follows ? '' : undefined}
252
+ >
253
+ {#snippet children({ segments })}
254
+ <!-- Keyed by index: `part` repeats (the `literal` separators between
255
+ segments all carry it) so a part-keyed block collides. -->
256
+ {#each segments as seg, i (i)}
257
+ {@const shown = segmentText(seg.part, seg.value)}
258
+ <BitsDateField.Segment
259
+ class="qm-date-segment"
260
+ part={seg.part}
261
+ data-ghosted={shown.ghosted}
262
+ >
263
+ {shown.text}
264
+ </BitsDateField.Segment>
265
+ {/each}
266
+ {#if !datetime}
267
+ <button
268
+ type="button"
269
+ class="qm-chip qm-tap-floor qm-focus-ring qm-date-today"
270
+ aria-pressed={!!follows}
271
+ data-default={follows === 'default' ? '' : undefined}
272
+ onclick={toggle}
273
+ >
274
+ <Icon name={follows ? 'calendar-check' : 'calendar'} />{t.strings.dateToday}
275
+ </button>
276
+ {/if}
277
+ {/snippet}
278
+ </BitsDateField.Input>
279
+ </BitsDateField.Root>
280
+ {/key}
252
281
  </span>
253
282
 
254
283
  <style>
@@ -16,6 +16,7 @@
16
16
  import { Select } from 'bits-ui';
17
17
  import Icon from './icons/Icon.svelte';
18
18
  import { wording } from './strings.js';
19
+ import { spacePx } from '../core/rungs.js';
19
20
  import './controls.css';
20
21
 
21
22
  const t = wording();
@@ -79,6 +80,8 @@
79
80
  * `none`: it draws at the default rung (theme.css). */
80
81
  const ghostsMember = $derived(!optional && !!fallback);
81
82
  const ghostText = $derived(optional ? t.strings.optionalGhost : dash(fallback));
83
+ /** The sentinel's tag says what picking it does: return to the default, or unset. */
84
+ const tag = $derived(ghostsMember ? t.strings.enumUnsetTag : t.strings.enumUnsetTagNone);
82
85
  /** What the closed trigger shows: the pick, or the ghosted default while unset. */
83
86
  const shown = $derived(unset ? ghostText : dash(local));
84
87
 
@@ -113,10 +116,10 @@
113
116
  data-ghosted={unset ? (ghostsMember ? 'default' : '') : undefined}
114
117
  >
115
118
  <span class="qm-select-shown">{shown}</span>
116
- <Icon name="chevron-down" class="qm-select-chevron" size={14} />
119
+ <Icon name="chevron-down" class="qm-select-chevron" />
117
120
  </Select.Trigger>
118
121
  <Select.Portal to={portalTarget}>
119
- <Select.Content collisionBoundary={portalTarget ?? []} sideOffset={4}>
122
+ <Select.Content collisionBoundary={portalTarget ?? []} sideOffset={spacePx(wrapEl)}>
120
123
  <!-- The list portals out of the trigger's DOM but into the nearest
121
124
  `[data-qm-root]`, and carries the marker itself, like FormatPopover.
122
125
  That root is the boundary a flip is measured against too, being the box
@@ -138,11 +141,11 @@
138
141
  <Select.Item
139
142
  class="qm-menu-item qm-select-item qm-select-unset"
140
143
  value={UNSET}
141
- label={optional ? ghostText : `${ghostText} ${t.strings.enumUnsetTag}`}
144
+ label={optional ? ghostText : `${ghostText} ${tag}`}
142
145
  >
143
146
  <span class="qm-select-ghost">{ghostText}</span>
144
147
  {#if !optional}
145
- <span class="qm-select-tag">{t.strings.enumUnsetTag}</span>
148
+ <span class="qm-select-tag">{tag}</span>
146
149
  {/if}
147
150
  </Select.Item>
148
151
  <!-- A refused option still draws under `'disable'`, and under either
@@ -202,6 +205,8 @@
202
205
  }
203
206
  .qm-select-wrap :global(.qm-select-chevron) {
204
207
  flex-shrink: 0;
208
+ width: var(--_qm-glyph-control);
209
+ height: var(--_qm-glyph-control);
205
210
  }
206
211
  /* The focus ring rides `.qm-focus-ring` on the trigger (controls.css). */
207
212
  /* Shown-never-written: the closed control reads muted while unset, at the rung of
@@ -180,7 +180,7 @@
180
180
  e.stopPropagation();
181
181
  open = false;
182
182
  }
183
- }}><Icon name="info" size={13} /></button
183
+ }}><Icon name="info" /></button
184
184
  >
185
185
 
186
186
  <!-- Placement is the description's height. One line rides the label rung beside its own
@@ -249,8 +249,15 @@
249
249
  So the box is the glyph and the target is `.qm-tap-floor`'s (controls.css), which
250
250
  carries that split and the giving-up of the family floor together.
251
251
 
252
- It takes no fill either: the ink step below says hover, and the surface itself
253
- opens on it. */
252
+ It takes no fill either: the surface itself opens on hover.
253
+
254
+ At rest it recedes to the idle rung, a card describing every field otherwise
255
+ standing a mark beside each label before anything is touched. It lifts with the
256
+ scope that owns its row (the field, the property, the array or matrix, the table
257
+ its column heads, the section a lone field is named by), each reached by child
258
+ steps so a nested field's marker stays at rest under its container's hover, and
259
+ while its surface is open. The keyboard's `Mod-/` and the parked description do not
260
+ depend on the lift. */
254
261
  .qm-field-hint {
255
262
  /* The marker recedes to the label rung; the surface carries the text. */
256
263
  color: var(--_qm-ink-label);
@@ -258,10 +265,20 @@
258
265
  cursor: help;
259
266
  padding: 0;
260
267
  background: none;
268
+ opacity: var(--_qm-opacity-idle);
269
+ transition: opacity var(--_qm-duration-fast) var(--_qm-ease-reverse);
261
270
  }
271
+ :global(:is(.qm-field, .qm-object-prop):is(:hover, :focus-within) > .qm-field-label-row)
272
+ > .qm-field-hint,
273
+ :global(:is(.qm-array, .qm-matrix):is(:hover, :focus-within) > * > .qm-field-label-row)
274
+ > .qm-field-hint,
275
+ :global(.qm-array-table:is(:hover, :focus-within) .qm-array-table-col > .qm-field-label-row)
276
+ > .qm-field-hint,
277
+ :global(.qm-group:is(:hover, :focus-within) > .qm-group-head) > .qm-field-hint,
262
278
  .qm-field-hint:hover,
263
- .qm-field-hint:focus-visible {
264
- color: var(--_qm-ink-label);
279
+ .qm-field-hint:focus-visible,
280
+ .qm-field-hint[aria-expanded='true'] {
281
+ opacity: 1;
265
282
  }
266
283
  /* The lift, the translucency, the blur and the scale-in come from
267
284
  `.qm-popover-surface` (controls.css). This surface's own is a measure: guidance
@@ -116,12 +116,12 @@
116
116
  .qm-field-label.targeted {
117
117
  cursor: pointer;
118
118
  }
119
- /* Required marker: a quiet accent glyph, not an alarm; required-ness is guidance,
120
- the document still edits and renders unmet. */
119
+ /* Required marker: the label's own ink, not an alarm; required-ness is guidance,
120
+ the document still edits and renders unmet, and the danger hue is a diagnostic's. */
121
121
  .qm-field-required {
122
122
  display: inline-flex;
123
123
  align-items: center;
124
- color: var(--_qm-danger);
124
+ color: var(--_qm-ink-label);
125
125
  line-height: 1;
126
126
  }
127
127
  /* The parked description: in the accessibility tree, out of the layout, and never
@@ -24,7 +24,10 @@
24
24
  A press holds it down, which is what makes the surface answer a selection rather
25
25
  than a pointer: a drag fires `selectionchange` on every frame, so a surface driven
26
26
  by that alone rises on the first character and rides the cursor across the text it
27
- is covering. Keyboard selection holds nothing, so a Shift-arrow syncs as it fires.
27
+ is covering. Keyboard selection holds nothing, so it waits instead: a rise with no
28
+ release behind it comes once the selection has stood still for `SETTLE_MS`, so a
29
+ Shift-arrow run, or a select-all on its way to a cut, raises nothing until it stops.
30
+ A lowering never waits, and an open surface follows each step.
28
31
 
29
32
  Nothing here watches scroll. `sync` decides whether the surface is up, which a
30
33
  scroll does not change; where it sits is the anchor's own question, and the anchor
@@ -70,6 +73,7 @@
70
73
  import { hasMarks, rangeAnchor, type RangeAnchor } from '../core/codec/index.js';
71
74
  import type { ActiveProse } from './leaves.js';
72
75
  import { clearLink, hrefInSelection, normalizeHref, setLink } from './links.js';
76
+ import { spacePx } from '../core/rungs.js';
73
77
  import './controls.css';
74
78
 
75
79
  interface Props {
@@ -78,7 +82,7 @@
78
82
  }
79
83
  let { getActiveLeaf }: Props = $props();
80
84
 
81
- const GLYPH = 15;
85
+ const SETTLE_MS = 450;
82
86
  /** The six content formatting marks. `anchor` is a seventh, drawn separately below:
83
87
  * a decoration toggle rather than a PM `toggleMark`. One string per mark carries both
84
88
  * the tooltip and the accessible name; the anchor button spells the two separately. */
@@ -120,7 +124,8 @@
120
124
  return getActiveLeaf()?.views.focusedView();
121
125
  }
122
126
 
123
- function sync(): void {
127
+ function sync(wait = false): void {
128
+ clearTimeout(settling);
124
129
  const insidePopover =
125
130
  !!contentEl && !!document.activeElement && contentEl.contains(document.activeElement);
126
131
  // Interacting with the popover itself (e.g. the link input): leave state as-is.
@@ -151,6 +156,10 @@
151
156
  linkPromptOpen = false;
152
157
  return;
153
158
  }
159
+ if (wait && !open) {
160
+ settling = setTimeout(() => sync(), SETTLE_MS);
161
+ return;
162
+ }
154
163
  portalTarget = view.dom.closest<HTMLElement>('[data-qm-root]') ?? undefined;
155
164
  const { from, to } = view.state.selection;
156
165
  anchor = rangeAnchor(view, from, to);
@@ -186,12 +195,17 @@
186
195
  // selection. `pending` collapses a burst to a single scheduled callback.
187
196
  let pending = false;
188
197
  let rafHandle = 0;
198
+ /** The check in flight answers a pointer's release, which raises at once. */
199
+ let released = false;
200
+ let settling: ReturnType<typeof setTimeout> | undefined;
189
201
  function deferredSync(): void {
190
202
  if (pending) return;
191
203
  pending = true;
192
204
  rafHandle = requestAnimationFrame(() => {
193
205
  pending = false;
194
- sync();
206
+ const wait = !released;
207
+ released = false;
208
+ sync(wait);
195
209
  });
196
210
  }
197
211
 
@@ -205,14 +219,16 @@
205
219
  function onPointerUp(): void {
206
220
  if (!pressed) return;
207
221
  pressed = false;
222
+ released = true;
208
223
  deferredSync();
209
224
  }
210
225
 
211
- // A burst-coalescing rAF may still be in flight at teardown; cancel it so
212
- // `sync()` never runs against the destroyed component.
226
+ // A burst-coalescing rAF or a settling rise may still be in flight at teardown;
227
+ // cancel both so `sync()` never runs against the destroyed component.
213
228
  onDestroy(() => {
214
229
  if (pending) cancelAnimationFrame(rafHandle);
215
230
  pending = false;
231
+ clearTimeout(settling);
216
232
  });
217
233
 
218
234
  // Any close path that bypasses `sync` (e.g. Escape while the link input holds
@@ -342,7 +358,7 @@
342
358
  side="top"
343
359
  align="center"
344
360
  trapFocus={false}
345
- sideOffset={8}
361
+ sideOffset={spacePx(portalTarget, 2)}
346
362
  onOpenAutoFocus={(e: Event) => e.preventDefault()}
347
363
  onCloseAutoFocus={(e: Event) => e.preventDefault()}
348
364
  >
@@ -411,7 +427,7 @@
411
427
  title={m.title}
412
428
  aria-label={m.title}
413
429
  onmousedown={keepFocus}
414
- onclick={() => toggle(m.name)}><Icon name={m.icon} size={GLYPH} /></button
430
+ onclick={() => toggle(m.name)}><Icon name={m.icon} /></button
415
431
  >
416
432
  {/each}
417
433
  {#if anchorAvailable}
@@ -421,7 +437,7 @@
421
437
  title={t.strings.formatAnchorTitle}
422
438
  aria-label={t.strings.formatAnchor}
423
439
  onmousedown={keepFocus}
424
- onclick={toggleAnchor}><Icon name="hash" size={GLYPH} /></button
440
+ onclick={toggleAnchor}><Icon name="hash" /></button
425
441
  >
426
442
  {/if}
427
443
  </div>
@@ -525,16 +525,19 @@
525
525
  background: var(--_qm-danger-tint);
526
526
  color: var(--_qm-danger);
527
527
  }
528
- .qm-member-remove :global(svg) {
529
- width: var(--_qm-glyph-control);
530
- height: var(--_qm-glyph-control);
531
- }
532
- /* The default's set is what prints and nothing written: its titles and every value
533
- its columns draw take the default rung (theme.css), and the first gesture, writing
534
- it, takes it off. */
528
+ /* The default's set is what prints and nothing written: its titles, its ticks and
529
+ every value its columns draw take the default rung (theme.css), and the first
530
+ gesture, writing it, takes it off. */
535
531
  .qm-matrix-roster[data-default] :global(:is(.qm-input, .ProseMirror, .qm-date)),
536
532
  .qm-matrix-roster[data-default] :global(.qm-select:not([data-ghosted])),
537
533
  .qm-matrix-roster[data-default] .qm-member.held .qm-member-title {
538
534
  color: var(--_qm-ink-default);
539
535
  }
536
+ .qm-matrix-roster[data-default] :is(.qm-tick-input:checked, .qm-tick-fixed) {
537
+ background: var(--_qm-accent-default);
538
+ border-color: var(--_qm-accent-default);
539
+ }
540
+ .qm-matrix-roster[data-default] :global(.qm-toggle[data-state='checked']) {
541
+ background: var(--_qm-accent-default);
542
+ }
540
543
  </style>
@@ -457,10 +457,8 @@
457
457
  /* The vertical and the rung between it and the properties, `--_qm-nest` being the step
458
458
  the card and the section hold too.
459
459
 
460
- `--_qm-border` and not `--_qm-border-faint`: this is the stroke that structures
461
- (ARCHITECTURE §"A plane is a tone"), the one the card's edge and the section's
462
- vertical read. `faint` separates without structuring — a table's interior lines
463
- under a frame that is doing the structuring — which is the other job.
460
+ `--_qm-border`: this is the stroke that structures (ARCHITECTURE §"A plane is a
461
+ tone"), the one the card's edge and the section's vertical read.
464
462
 
465
463
  `padding-block` is the stroke's end caps, the same rung and the same reason as a
466
464
  section's (`Card.svelte`). A cap and not a leading gap: what stands the subform off
@@ -31,9 +31,6 @@
31
31
  }
32
32
  let { tips, onDismiss, onError }: Props = $props();
33
33
 
34
- /** Control-glyph size: the shared rule, as CardControls. */
35
- const GLYPH = 14;
36
-
37
34
  let cursor = $state(0);
38
35
  // Clamped: the channel is consumer-authored and can be re-seeded shorter while
39
36
  // the card is open, which would otherwise index past the end.
@@ -82,9 +79,9 @@
82
79
  {/if}
83
80
  <button type="button" class="qm-icon-btn qm-tips-action" onclick={advance}>
84
81
  {#if isLast}
85
- {t.strings.tipsDismiss} <Icon name="trash-2" size={GLYPH} />
82
+ {t.strings.tipsDismiss} <Icon name="trash-2" />
86
83
  {:else}
87
- {t.strings.tipsNext} <Icon name="arrow-right" size={GLYPH} />
84
+ {t.strings.tipsNext} <Icon name="arrow-right" />
88
85
  {/if}
89
86
  </button>
90
87
  </div>