@quillmark/svelte 0.2.0 → 0.3.1

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 (119) hide show
  1. package/NOTICE +6 -6
  2. package/README.md +3 -3
  3. package/THEMING.md +12 -8
  4. package/dist/core/address.d.ts +7 -7
  5. package/dist/core/address.js +6 -6
  6. package/dist/core/bloom.d.ts +3 -32
  7. package/dist/core/bloom.js +19 -51
  8. package/dist/core/codec/anchor.d.ts +1 -1
  9. package/dist/core/codec/anchor.js +1 -1
  10. package/dist/core/codec/atoms.d.ts +10 -0
  11. package/dist/core/codec/atoms.js +78 -0
  12. package/dist/core/codec/clipboard.d.ts +59 -0
  13. package/dist/core/codec/clipboard.js +202 -0
  14. package/dist/core/codec/code.d.ts +1 -1
  15. package/dist/core/codec/code.js +4 -4
  16. package/dist/core/codec/decode.d.ts +1 -1
  17. package/dist/core/codec/decode.js +8 -8
  18. package/dist/core/codec/encode.d.ts +4 -4
  19. package/dist/core/codec/encode.js +10 -10
  20. package/dist/core/codec/field.d.ts +21 -26
  21. package/dist/core/codec/field.js +33 -32
  22. package/dist/core/codec/index.js +5 -8
  23. package/dist/core/codec/inputrules.js +56 -18
  24. package/dist/core/codec/islands.d.ts +11 -7
  25. package/dist/core/codec/islands.js +16 -10
  26. package/dist/core/codec/keymap.d.ts +5 -6
  27. package/dist/core/codec/keymap.js +8 -7
  28. package/dist/core/codec/lists.d.ts +3 -3
  29. package/dist/core/codec/lists.js +32 -13
  30. package/dist/core/codec/marks.d.ts +1 -1
  31. package/dist/core/codec/marks.js +1 -1
  32. package/dist/core/codec/positions.js +1 -1
  33. package/dist/core/codec/prose.css +235 -221
  34. package/dist/core/codec/schema.js +4 -4
  35. package/dist/core/codec/slash.d.ts +39 -41
  36. package/dist/core/codec/slash.js +0 -0
  37. package/dist/core/codec/table-view.d.ts +12 -16
  38. package/dist/core/codec/table-view.js +488 -292
  39. package/dist/core/codec/table.d.ts +42 -15
  40. package/dist/core/codec/table.js +70 -26
  41. package/dist/core/errors.d.ts +5 -5
  42. package/dist/core/index.js +2 -2
  43. package/dist/core/lifecycle.js +1 -1
  44. package/dist/core/rebind.svelte.d.ts +2 -2
  45. package/dist/core/rebind.svelte.js +4 -4
  46. package/dist/core/teardown.d.ts +1 -1
  47. package/dist/core/teardown.js +1 -1
  48. package/dist/core/theme.css +170 -64
  49. package/dist/preset/index.css +1 -1
  50. package/dist/preset/recipes.css +31 -29
  51. package/dist/preset/scale.css +37 -26
  52. package/dist/preview/Preview.svelte +17 -27
  53. package/dist/preview/Preview.svelte.d.ts +4 -5
  54. package/dist/preview/bridge.js +6 -6
  55. package/dist/preview/controller.d.ts +8 -10
  56. package/dist/preview/controller.js +19 -44
  57. package/dist/preview/geometry.d.ts +8 -10
  58. package/dist/preview/geometry.js +8 -10
  59. package/dist/preview/index.js +4 -4
  60. package/dist/preview/paint.d.ts +1 -1
  61. package/dist/preview/paint.js +17 -19
  62. package/dist/visual/ArrayField.svelte +80 -66
  63. package/dist/visual/ArrayField.svelte.d.ts +1 -1
  64. package/dist/visual/BooleanField.svelte +8 -6
  65. package/dist/visual/BooleanField.svelte.d.ts +2 -2
  66. package/dist/visual/Card.svelte +160 -329
  67. package/dist/visual/Card.svelte.d.ts +2 -7
  68. package/dist/visual/CardControls.svelte +1 -1
  69. package/dist/visual/DateField.svelte +19 -19
  70. package/dist/visual/DateField.svelte.d.ts +3 -3
  71. package/dist/visual/DiagnosticList.svelte +1 -1
  72. package/dist/visual/EnumField.svelte +16 -16
  73. package/dist/visual/EnumField.svelte.d.ts +2 -2
  74. package/dist/visual/Field.svelte +128 -115
  75. package/dist/visual/Field.svelte.d.ts +1 -1
  76. package/dist/visual/FieldHint.svelte +24 -42
  77. package/dist/visual/FieldHint.svelte.d.ts +1 -1
  78. package/dist/visual/FieldLabel.svelte +8 -8
  79. package/dist/visual/FormatPopover.svelte +80 -35
  80. package/dist/visual/NumberField.svelte +5 -5
  81. package/dist/visual/NumberField.svelte.d.ts +1 -1
  82. package/dist/visual/ObjectField.svelte +9 -9
  83. package/dist/visual/ObjectField.svelte.d.ts +1 -1
  84. package/dist/visual/ProseArrayElement.svelte +11 -13
  85. package/dist/visual/ProseArrayElement.svelte.d.ts +1 -1
  86. package/dist/visual/ProseField.svelte +53 -87
  87. package/dist/visual/ProseField.svelte.d.ts +13 -19
  88. package/dist/visual/SlashMenu.svelte +42 -16
  89. package/dist/visual/TextField.svelte +5 -5
  90. package/dist/visual/TextField.svelte.d.ts +1 -1
  91. package/dist/visual/TipsCard.svelte +68 -56
  92. package/dist/visual/VisualEditor.svelte +6 -6
  93. package/dist/visual/VisualEditorInner.svelte +203 -290
  94. package/dist/visual/controls.css +74 -30
  95. package/dist/visual/diagnostics.d.ts +5 -5
  96. package/dist/visual/diagnostics.js +3 -3
  97. package/dist/visual/domid.js +3 -3
  98. package/dist/visual/ext.d.ts +1 -1
  99. package/dist/visual/ext.js +1 -1
  100. package/dist/visual/icons/nodes.d.ts +19 -0
  101. package/dist/visual/icons/nodes.js +13 -1
  102. package/dist/visual/index.js +4 -4
  103. package/dist/visual/leaves.d.ts +3 -3
  104. package/dist/visual/leaves.js +1 -1
  105. package/dist/visual/links.d.ts +24 -0
  106. package/dist/visual/links.js +67 -0
  107. package/dist/visual/props.d.ts +8 -8
  108. package/dist/visual/signals.d.ts +6 -6
  109. package/dist/visual/strings.d.ts +14 -19
  110. package/dist/visual/strings.js +5 -7
  111. package/dist/visual/structure.d.ts +24 -17
  112. package/dist/visual/structure.js +8 -8
  113. package/dist/visual/synced.svelte.d.ts +1 -1
  114. package/dist/visual/synced.svelte.js +4 -4
  115. package/dist/visual/tips.d.ts +2 -2
  116. package/dist/visual/tips.js +3 -3
  117. package/package.json +2 -2
  118. package/dist/preview/overlay.d.ts +0 -11
  119. package/dist/preview/overlay.js +0 -97
@@ -33,14 +33,13 @@ onPaintError) {
33
33
  }, { root: container, threshold: 0 });
34
34
  function makeSlot(page) {
35
35
  const size = session.pageSize(page);
36
- // The sheet's own edge, and the only thing separating it from the desk: the
37
- // page is at `--_qm-surface`, and the tone behind it is the host's, which
38
- // THEMING permits to be plain `--qm-bg` (the same value). A hairline holds at
39
- // both poles where a shadow's offset does not, and the
40
- // boundary of the paper is not a mark on it, so the overlay's no-ink-at-rest
41
- // rule is untouched (PREVIEW §Overlay). `border-box`, so the stroke lands
42
- // inside the width the container gives and `clientWidth` below still measures
43
- // the paper.
36
+ // The sheet is a plane and the desk is the one under it: paper sits at
37
+ // `--_qm-surface` and the container paints `--_qm-surface-sunken`, so the
38
+ // edge is where two tones meet and no stroke is drawn around it
39
+ // (ARCHITECTURE §Styling). It leaves the no-ink rule untouched, since a
40
+ // boundary that is not painted is not a mark on the page (PREVIEW §"Two
41
+ // responsibilities"). `border-box` costs nothing here and keeps a slot a
42
+ // consumer insets from measuring wider than its paper.
44
43
  const el = document.createElement('div');
45
44
  el.className = 'qm-page';
46
45
  // The page's index, in the DOM. `pageByEl` is this module's and a consumer
@@ -52,8 +51,7 @@ onPaintError) {
52
51
  boxSizing: 'border-box',
53
52
  width: '100%',
54
53
  aspectRatio: `${size.widthPt} / ${size.heightPt}`,
55
- background: 'var(--_qm-surface)',
56
- border: 'var(--_qm-border-width) solid var(--_qm-border)'
54
+ background: 'var(--_qm-surface)'
57
55
  });
58
56
  if (page > 0)
59
57
  el.style.marginTop = PAGE_GAP;
@@ -63,7 +61,7 @@ onPaintError) {
63
61
  return { page, size, el };
64
62
  }
65
63
  // Rasterize `slot` into its canvas (creating the canvas if this is its first
66
- // paint since mounting). `layoutScale` is derived from the slot's OWN current
64
+ // paint since mounting). `layoutScale` is derived from the slot's own current
67
65
  // CSS width, so layout width tracks the container per the zoom settled
68
66
  // decision; `canvas.style.*` is then set from the authoritative PaintResult,
69
67
  // never guessed.
@@ -74,7 +72,7 @@ onPaintError) {
74
72
  canvas.className = 'qm-page-canvas';
75
73
  Object.assign(canvas.style, { position: 'absolute', inset: '0', display: 'block' });
76
74
  }
77
- // Contextless canvas: bail BEFORE registering, so `updateBand` still sees
75
+ // Contextless canvas: bail before registering, so `updateBand` still sees
78
76
  // the page as unmounted and retries instead of keeping a blank page.
79
77
  const ctx = canvas.getContext('2d');
80
78
  if (!ctx)
@@ -108,7 +106,7 @@ onPaintError) {
108
106
  canvases.delete(page);
109
107
  }
110
108
  // Mount pages newly inside [min(visible)-margin, max(visible)+margin], unmount
111
- // pages that fell outside it. Only touches pages CHANGING band membership;
109
+ // pages that fell outside it. Only touches pages changing band membership;
112
110
  // an already-mounted page in the band is left alone (paint contract: an idle
113
111
  // canvas keeps its pixels for free; repainting it here would be pure waste).
114
112
  function updateBand() {
@@ -153,20 +151,20 @@ onPaintError) {
153
151
  slots[i] = { ...slots[i], size };
154
152
  // Keep the page box's shape in step with the re-read size: a recompile
155
153
  // can change a page's dimensions without changing the count, and every
156
- // %-space overlay/click transform assumes box shape matches PageSize.
154
+ // %-space click/scroll transform assumes box shape matches PageSize.
157
155
  slots[i].el.style.aspectRatio = `${size.widthPt} / ${size.heightPt}`;
158
156
  }
159
157
  }
160
158
  // Slots are built on the first `refresh` (the controller calls it immediately),
161
- // NOT here: reconcile → `session.pageSize` must run AFTER the controller's
159
+ // not here: reconcile → `session.pageSize` must run after the controller's
162
160
  // `supportsCanvas`/`pageCount` gate, since a `supportsCanvas: false` compile with
163
161
  // pages would throw on `pageSize` (runtime.d.ts: it succeeds iff `supportsCanvas`)
164
162
  // before the controller can surface its "unsupported" message.
165
163
  // ── Keep mounted rasters in step with the display ───────────────────────────
166
- // Every paint freezes `canvas.style.width/height` to the box width AT THAT
167
- // PAINT (paintSlot). A page that stays mounted while the container's CSS width
164
+ // Every paint freezes `canvas.style.width/height` to the box width at that
165
+ // paint (paintSlot). A page that stays mounted while the container's CSS width
168
166
  // or `devicePixelRatio` shifts would otherwise keep a stale raster the %-space
169
- // overlay/click math silently drifts off of: the box tracks the container, the
167
+ // click/scroll math silently drifts off of: the box tracks the container, the
170
168
  // ink does not. Two observers close the gap by repainting mounted pages from
171
169
  // their current box; canvases are `position:absolute`, so a repaint can't feed
172
170
  // back into layout (and thus can't re-trigger the resize observer).
@@ -212,7 +210,7 @@ onPaintError) {
212
210
  resizeObserver?.observe(container);
213
211
  // `devicePixelRatio` changes (window dragged to a different-DPI monitor, browser
214
212
  // zoom) leave CSS-px width untouched, so the ResizeObserver never sees them;
215
- // watch DPR directly. A `(resolution: …dppx)` query pins the CURRENT ratio and
213
+ // watch DPR directly. A `(resolution: …dppx)` query pins the current ratio and
216
214
  // is therefore one-shot: re-arm it against the new ratio on each change. Guarded
217
215
  // for environments (jsdom) without `matchMedia`.
218
216
  let dprQuery;
@@ -1,5 +1,5 @@
1
1
  <!--
2
- An `array` field → an add/remove repeater. Elements commit by VALUE: every
2
+ An `array` field → an add/remove repeater. Elements commit by value: every
3
3
  edit / add / remove rebuilds the whole array and hands it to the parent's typed
4
4
  `writer.set(field, wholeArray)` (arrays are not op-addressed). Element control
5
5
  by `items.type`: `string` → text input, `richtext` → a prose element
@@ -8,19 +8,19 @@
8
8
  label header row (space-between with the field label); {@link Field} skips its
9
9
  own label for array controls and hands this component the label track with it.
10
10
 
11
- A row ends in the reserved action column (`--action-col`), which the field insets
12
- and this reaches back across: the element controls stop where a scalar's does and
13
- the remove sits beyond them, on the right edge the field's track keeps.
11
+ The remove is inside the element: a slab over the end of the element's own box, taking
12
+ its two end-side corners. So a row's box is the element's box, and an array's rows end
13
+ where every other field's control does.
14
14
 
15
15
  Keys carry the list without the mouse: Enter inserts a
16
- sibling below and takes the caret there, Backspace on an EMPTY element removes it
16
+ sibling below and takes the caret there, Backspace on an empty element removes it
17
17
  and hands focus back up the list.
18
18
 
19
19
  No reorder: an array's order is fixed at declaration/entry order. Elements
20
20
  carry a parallel session-id list, spliced with the values as one operation at
21
21
  whatever index a mutation names; so an element holds its id for life and the
22
22
  surviving order never permutes, which is what lets a keyed prose element survive an
23
- insert or a remove ABOVE it rather than remounting.
23
+ insert or a remove above it rather than remounting.
24
24
  -->
25
25
  <script lang="ts">
26
26
  import { wording } from './strings.js';
@@ -48,7 +48,7 @@
48
48
  required?: boolean;
49
49
  /** Schema `description`: the label's help affordance. */
50
50
  description?: string;
51
- /** The label's own DOM id. An array is a GROUP (N inputs, no single `for`
51
+ /** The label's own DOM id. An array is a group (N inputs, no single `for`
52
52
  * target) so the label names the set and each element keeps its indexed
53
53
  * `aria-label`. */
54
54
  labelId?: string;
@@ -59,14 +59,14 @@
59
59
  let { value, items, label, required, description, labelId, descriptionId, onCommit }: Props =
60
60
  $props();
61
61
 
62
- // The ELEMENT control is the item schema's own; an array declaring no `items`
62
+ // The element control is the item schema's own; an array declaring no `items`
63
63
  // has text elements.
64
64
  const control = $derived(items ? controlKind(items) : 'text');
65
65
  const plaintext = $derived(items?.type === 'plaintext');
66
66
  const arr = $derived((value ?? []) as unknown[]);
67
67
 
68
68
  // Parallel stable ids, one per element, kept in lockstep with the data below.
69
- // Seeded eagerly so a non-empty array renders its rows on the FIRST pass:
69
+ // Seeded eagerly so a non-empty array renders its rows on the first pass:
70
70
  // an effect-only seed mounts every element editor in a second render.
71
71
  const seq = new IdSeq();
72
72
  // svelte-ignore state_referenced_locally
@@ -89,10 +89,10 @@
89
89
  // is stated on `ProseArrayElement.focus`, which owns it.
90
90
  //
91
91
  // Every path that drops an id deletes its entry: `bind:this` teardown nulls the
92
- // VALUE on unmount and leaves the key, so a card that outlives its elements
92
+ // value on unmount and leaves the key, so a card that outlives its elements
93
93
  // accumulates one dead key per element ever created.
94
94
  //
95
- // `$state` for the BINDING's sake, not this component's: nothing here reads `els`
95
+ // `$state` for the binding's sake, not this component's: nothing here reads `els`
96
96
  // reactively (every read is inside an event handler or a post-flush focus hop), but
97
97
  // `bind:this` into a property of a plain object is a write Svelte cannot track, and
98
98
  // it says so once per element per render. Thirteen lines on one memo's first paint,
@@ -147,7 +147,7 @@
147
147
  // key does: the button under the pointer is part of what it destroys.
148
148
  focusAfterFlush(next[Math.max(k - 1, 0)]);
149
149
  }
150
- /** Take the caret: the FIRST element, or the add affordance when the list is empty;
150
+ /** Take the caret: the first element, or the add affordance when the list is empty;
151
151
  * which is then the only thing there is to land on, and the next thing the user
152
152
  * wants anyway. Reached by a label click and by the editor's landing verbs, which
153
153
  * ask one function so they cannot disagree (`Field`, `leaves.ts`). */
@@ -159,7 +159,7 @@
159
159
  // nothing about focusing it that the DOM does not already know.
160
160
  rootEl?.querySelector<HTMLTextAreaElement>('.qm-array-row textarea')?.focus();
161
161
  }
162
- /** Take the caret to element `k`: what a landing on an element ADDRESS resolves to
162
+ /** Take the caret to element `k`: what a landing on an element address resolves to
163
163
  * (`leaves.ts`). The index resolves to the element's session id here, at the call,
164
164
  * never carried as one — an index is stale the moment anything above it splices.
165
165
  * Past the live list it falls back to {@link focus}: the field is right and the row
@@ -170,10 +170,10 @@
170
170
  else focus();
171
171
  }
172
172
  /** Focus element `id` after the flush, never in the same tick: a mutation commits
173
- * the array BY VALUE, so the parent re-derives and the row does not exist until
173
+ * the array by value, so the parent re-derives and the row does not exist until
174
174
  * then. `undefined` is the empty list: the add affordance.
175
175
  *
176
- * The commit that schedules this can also remove the CARD holding the field, which
176
+ * The commit that schedules this can also remove the card holding the field, which
177
177
  * unmounts this component inside the window (core/teardown.ts). */
178
178
  async function focusAfterFlush(id: string | undefined): Promise<void> {
179
179
  if (!(await span.resumes(tick()))) return;
@@ -181,7 +181,7 @@
181
181
  else els[id]?.focus();
182
182
  }
183
183
  /** Whether element `k` reads empty to the user. A text element's committed value
184
- * LAGS the input: a cleared field commits at `change`, not per keystroke
184
+ * lags the input: a cleared field commits at `change`, not per keystroke
185
185
  * ({@link TextField}); so the input's own value is the truth; a prose element
186
186
  * commits every edit, so the committed `Content` is. */
187
187
  function elementEmpty(k: number, target: EventTarget | null): boolean {
@@ -202,7 +202,7 @@
202
202
  } else if (e.key === 'Backspace' && !e.repeat && elementEmpty(k, e.target)) {
203
203
  // Destructive with nothing to undo it, so it takes a deliberate press:
204
204
  // `repeat` is a held key running on past the character it just cleared, and
205
- // the emptiness test reads the state BEFORE this keystroke applies; so the
205
+ // the emptiness test reads the state before this keystroke applies; so the
206
206
  // press that empties an element never also removes it.
207
207
  e.preventDefault();
208
208
  remove(k);
@@ -230,8 +230,11 @@
230
230
  {:else}
231
231
  <span></span>
232
232
  {/if}
233
- <button type="button" class="qm-add-el qm-add-affordance" bind:this={addEl} onclick={add}
234
- >{t.strings.arrayAdd}</button
233
+ <button
234
+ type="button"
235
+ class="qm-add-el qm-add-affordance qm-tap-floor"
236
+ bind:this={addEl}
237
+ onclick={add}>{t.strings.arrayAdd}</button
235
238
  >
236
239
  </div>
237
240
  {#each ids as id, k (id)}
@@ -269,9 +272,9 @@
269
272
  {/if}
270
273
  <button
271
274
  type="button"
272
- class="qm-icon-btn qm-remove"
275
+ class="qm-icon-btn qm-remove qm-focus-ring"
273
276
  title={t.strings.arrayRemove}
274
- onclick={() => remove(k)}><Icon name="x" size={14} /></button
277
+ onclick={() => remove(k)}><Icon name="minus" /></button
275
278
  >
276
279
  </div>
277
280
  {/each}
@@ -282,14 +285,8 @@
282
285
  display: flex;
283
286
  flex-direction: column;
284
287
  gap: var(--_qm-space);
285
- /* The one field that reaches across the reserved action column: its rows END in
286
- that column, so an element control stops exactly where the scalar beside it
287
- does and the remove never sits over a long value. The reservation is the
288
- FIELD's inset (Field.svelte), so this cancels it wherever the field landed:
289
- its own row, or one track of a shared one. */
290
- margin-right: calc(-1 * var(--action-col));
291
288
  }
292
- /* The array's first line IS the row's label line: this component owns the label
289
+ /* The array's first line is the row's label line: this component owns the label
293
290
  track (Field.svelte), so the header has to measure what a `.qm-field-label-row`
294
291
  measures or an array shares a row with a scalar and their labels sit apart. The
295
292
  label is the same component; what had to give was the add affordance's height,
@@ -300,19 +297,56 @@
300
297
  justify-content: space-between;
301
298
  gap: var(--_qm-space-2);
302
299
  }
303
- /* The element takes the section's tracks, the remove takes the action column, and
304
- the gutter between them is the section's own: the row is that column's only
305
- occupant, so it reads as one more column of the grid rather than a nested layout.
306
- `minmax(0, …)` because an element control must be allowed to be narrower than its
307
- content: a long unbroken value grows the track otherwise, and the edge with it. */
300
+ /* A grid rather than a block: an `<input>` and a `<textarea>` are inline-level and
301
+ would sit on a baseline, standing the row a descender taller than the box the slab
302
+ measures itself against. `minmax(0, …)` because a long unbroken value grows an `auto`
303
+ track, and the edge with it. */
308
304
  .qm-array-row {
305
+ position: relative;
309
306
  display: grid;
310
- grid-template-columns: minmax(0, 1fr) var(--_qm-tap-min);
311
- column-gap: var(--_qm-space-2);
312
- align-items: start;
307
+ grid-template-columns: minmax(0, 1fr);
308
+ }
309
+ /* The end inset the slab stands in, taken off whichever box the element drew:
310
+ `.qm-input` is the text element and the JSON textarea, `.qm-control-box` the prose
311
+ one. `:global`, because the box belongs to the child component's markup and the scope
312
+ class stops at this component's. The longhand beats the family's `padding` shorthand
313
+ without a specificity fight: this block is unlayered and `controls.css` is not. */
314
+ .qm-array-row :global(.qm-input),
315
+ .qm-array-row :global(.qm-control-box) {
316
+ padding-inline-end: var(--_qm-tap-min);
313
317
  }
318
+ /* The box's end wall, floor to ceiling: a height of its own would leave a sliver of
319
+ well above or below, and the corners it takes are the box's — the end-side pair keeps
320
+ `.qm-icon-btn`'s radius, which is the same rung the box draws.
321
+
322
+ It comes up on its own row rather than on the field: a destructive control is offered
323
+ by the row the pointer is on, not by every row at once. Hover is where it says
324
+ destructive, ink with fill, a tint alone being a wash under a label-toned glyph. */
314
325
  .qm-remove {
315
- align-self: center;
326
+ position: absolute;
327
+ inset-block: 0;
328
+ inset-inline-end: 0;
329
+ width: var(--_qm-tap-min);
330
+ color: var(--_qm-ink-label);
331
+ opacity: var(--_qm-opacity-idle);
332
+ border-start-start-radius: 0;
333
+ border-end-start-radius: 0;
334
+ transition:
335
+ opacity var(--_qm-duration-fast) var(--_qm-ease-reverse),
336
+ background-color var(--_qm-duration-fast) var(--_qm-ease-reverse),
337
+ color var(--_qm-duration-fast) var(--_qm-ease-reverse);
338
+ }
339
+ .qm-remove :global(svg) {
340
+ width: var(--_qm-glyph-control);
341
+ height: var(--_qm-glyph-control);
342
+ }
343
+ .qm-array-row:hover .qm-remove,
344
+ .qm-array-row:focus-within .qm-remove {
345
+ opacity: 1;
346
+ }
347
+ .qm-remove:hover {
348
+ background: var(--_qm-danger-tint);
349
+ color: var(--_qm-danger);
316
350
  }
317
351
  /* The JSON element is a `.qm-input` (controls.css): box, focus ring, and all;
318
352
  what a textarea adds over an input is the face and a floor on its height. */
@@ -320,48 +354,28 @@
320
354
  font-family: var(--_qm-font-mono);
321
355
  min-height: 2.5rem;
322
356
  }
323
- /* Chrome, hover fill and target come from `.qm-add-affordance` (controls.css);
324
- what is here is this trigger's own type, box and recede ladder. It rests dim like
325
- the card stack's gap triggers and comes up on hover of the FIELD or on focus: one
326
- trigger at the head of a row of controls is found by looking at the array, not by
327
- grazing its first line.
357
+ /* It rests dim and comes up on hover of the field rather than of itself: one trigger
358
+ at the head of a row of controls is found by looking at the array, not by grazing its
359
+ first line.
328
360
 
329
- THE BOX IS THE LABEL'S LINE BOX, and the target is the `::after`: the same split
330
- the field label's guidance marker takes, for the same reason and one row over.
331
- This is the other affordance that sits IN a line of text rather than in a row of
332
- its own, and the line is a label's, so a
361
+ The box is the label's line box and the target is `.qm-tap-floor`'s (controls.css): a
333
362
  target-sized box would stand the header 8px taller than the `.qm-field-label-row`
334
- beside it in a shared row and put the two labels on different lines. So the family's
335
- floor comes off the box and goes out of flow, unpainted and centred on it: the row
336
- keeps the label's line box, the press keeps the floor. The horizontal edges are the
337
- button's own, the word being already wider than the threshold.
363
+ beside it in a shared row and put the two labels on different lines.
338
364
 
339
- The type is the LABEL rung in place of the family's body rung, which the family
340
- invites (controls.css: a caller whose button carries a label restates the rung it
341
- wants). It reads as one more thing on the label line rather than as a control that
342
- wandered up from the row below. */
365
+ The label rung in place of the family's body rung, which the family invites: it reads
366
+ as one more thing on the label line rather than as a control from the row below. */
343
367
  .qm-add-el {
344
- position: relative;
345
- min-height: 0;
346
368
  padding: 0 var(--_qm-space);
347
369
  font-size: var(--_qm-text-label);
348
370
  opacity: var(--_qm-opacity-idle);
349
371
  }
350
- .qm-add-el::after {
351
- content: '';
352
- position: absolute;
353
- inset-inline: 0;
354
- top: 50%;
355
- height: var(--_qm-tap-min);
356
- transform: translateY(-50%);
357
- }
358
372
  .qm-array:hover .qm-add-el,
359
373
  .qm-add-el:focus-visible {
360
374
  opacity: 1;
361
375
  }
362
- /* Touch has no hover: keep a faint always-on affordance so add stays reachable. */
363
376
  @media (hover: none) {
364
- .qm-add-el {
377
+ .qm-add-el,
378
+ .qm-remove {
365
379
  opacity: var(--_qm-opacity-muted);
366
380
  }
367
381
  }
@@ -9,7 +9,7 @@ interface Props {
9
9
  required?: boolean;
10
10
  /** Schema `description`: the label's help affordance. */
11
11
  description?: string;
12
- /** The label's own DOM id. An array is a GROUP (N inputs, no single `for`
12
+ /** The label's own DOM id. An array is a group (N inputs, no single `for`
13
13
  * target) so the label names the set and each element keeps its indexed
14
14
  * `aria-label`. */
15
15
  labelId?: string;
@@ -13,11 +13,11 @@
13
13
  interface Props {
14
14
  value: boolean | undefined;
15
15
  fallback?: boolean;
16
- /** Accessible name for a switch NOTHING else names: an object property, whose
16
+ /** Accessible name for a switch nothing else names: an object property, whose
17
17
  * name is the field label plus the property's. A field's own switch takes `id`
18
18
  * instead and is named by the `<label for>` beside it. */
19
19
  label?: string;
20
- /** `<label for>` target. `Switch.Root` renders a `<button>`, which IS labelable,
20
+ /** `<label for>` target. `Switch.Root` renders a `<button>`, which is labelable,
21
21
  * so the click a label forwards toggles the switch: correct for this control,
22
22
  * and single-fire: the label is a sibling, not a wrapper, so there is no second
23
23
  * click bubbling back up to be re-dispatched. */
@@ -30,7 +30,7 @@
30
30
 
31
31
  // Local toggle state synced to `value`; own-toggles stay local, only an external
32
32
  // change reconciles back in (see `syncedLocal`). The primitive is driven
33
- // CONTROLLED (`checked` + `onCheckedChange`, never `bind:`) so reconciliation
33
+ // controlled (`checked` + `onCheckedChange`, never `bind:`) so reconciliation
34
34
  // stays the package's: a two-way bind hands the primitive a lane around it,
35
35
  // which repeats the reconciliation hazard in miniature.
36
36
  const local = syncedLocal(() => value ?? fallback ?? false);
@@ -53,7 +53,7 @@
53
53
  </span>
54
54
 
55
55
  <style>
56
- /* A primitive renders its OWN element, which a scoped selector cannot reach:
56
+ /* A primitive renders its own element, which a scoped selector cannot reach:
57
57
  styled through the wrapper with `:global`. */
58
58
  .qm-switch-wrap :global(.qm-switch) {
59
59
  display: inline-flex;
@@ -61,15 +61,17 @@
61
61
  width: 1.75rem;
62
62
  height: 1rem;
63
63
  padding: var(--_qm-space-half);
64
- border: var(--_qm-border-width) solid var(--_qm-border);
64
+ border: none;
65
65
  border-radius: var(--_qm-radius-pill);
66
+ /* The track is a fill and nothing else: an edge around it would be a second
67
+ statement of a shape the tone already draws, and the checked state swaps that
68
+ one fill for the accent (ARCHITECTURE §Styling). */
66
69
  background: var(--_qm-surface-hover);
67
70
  cursor: pointer;
68
71
  transition: background var(--_qm-duration-fast) var(--_qm-ease-reverse);
69
72
  }
70
73
  .qm-switch-wrap :global(.qm-switch[data-state='checked']) {
71
74
  background: var(--_qm-accent);
72
- border-color: var(--_qm-accent);
73
75
  }
74
76
  /* The focus ring rides `.qm-focus-ring` on the switch (controls.css). */
75
77
  /* The thumb reads on tone alone: the base surface over the track's `hover` rung
@@ -2,11 +2,11 @@ import './controls.css';
2
2
  interface Props {
3
3
  value: boolean | undefined;
4
4
  fallback?: boolean;
5
- /** Accessible name for a switch NOTHING else names: an object property, whose
5
+ /** Accessible name for a switch nothing else names: an object property, whose
6
6
  * name is the field label plus the property's. A field's own switch takes `id`
7
7
  * instead and is named by the `<label for>` beside it. */
8
8
  label?: string;
9
- /** `<label for>` target. `Switch.Root` renders a `<button>`, which IS labelable,
9
+ /** `<label for>` target. `Switch.Root` renders a `<button>`, which is labelable,
10
10
  * so the click a label forwards toggles the switch: correct for this control,
11
11
  * and single-fire: the label is a sibling, not a wrapper, so there is no second
12
12
  * click bubbling back up to be re-dispatched. */