@quillmark/svelte 0.20.1 → 0.22.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 (85) hide show
  1. package/NOTICE +6 -7
  2. package/README.md +1 -1
  3. package/THEMING.md +1 -1
  4. package/dist/core/address.d.ts +6 -1
  5. package/dist/core/address.js +12 -3
  6. package/dist/core/bloom.d.ts +9 -4
  7. package/dist/core/bloom.js +18 -49
  8. package/dist/core/codec/atoms.d.ts +1 -1
  9. package/dist/core/codec/atoms.js +26 -11
  10. package/dist/core/codec/blocks.d.ts +5 -5
  11. package/dist/core/codec/blocks.js +27 -8
  12. package/dist/core/codec/code.d.ts +4 -4
  13. package/dist/core/codec/code.js +52 -22
  14. package/dist/core/codec/decode.d.ts +5 -5
  15. package/dist/core/codec/decode.js +18 -12
  16. package/dist/core/codec/encode.js +6 -1
  17. package/dist/core/codec/field.d.ts +22 -15
  18. package/dist/core/codec/field.js +66 -26
  19. package/dist/core/codec/index.d.ts +1 -1
  20. package/dist/core/codec/index.js +2 -2
  21. package/dist/core/codec/inputrules.d.ts +4 -0
  22. package/dist/core/codec/inputrules.js +44 -6
  23. package/dist/core/codec/link-paste.d.ts +11 -0
  24. package/dist/core/codec/link-paste.js +32 -0
  25. package/dist/core/codec/lists.d.ts +5 -4
  26. package/dist/core/codec/lists.js +87 -31
  27. package/dist/core/codec/nesting.d.ts +20 -0
  28. package/dist/core/codec/nesting.js +62 -0
  29. package/dist/core/codec/prose.css +145 -63
  30. package/dist/core/codec/schema.d.ts +2 -2
  31. package/dist/core/codec/schema.js +71 -18
  32. package/dist/core/codec/slash.js +0 -0
  33. package/dist/core/codec/table-copy.d.ts +17 -0
  34. package/dist/core/codec/table-copy.js +84 -0
  35. package/dist/core/codec/table-view.d.ts +6 -7
  36. package/dist/core/codec/table-view.js +281 -201
  37. package/dist/core/codec/table.d.ts +2 -9
  38. package/dist/core/codec/table.js +10 -19
  39. package/dist/core/codec/tasks.d.ts +19 -0
  40. package/dist/core/codec/tasks.js +140 -0
  41. package/dist/core/errors.d.ts +2 -2
  42. package/dist/core/rungs.d.ts +6 -0
  43. package/dist/core/rungs.js +11 -0
  44. package/dist/core/theme.css +24 -12
  45. package/dist/visual/ArrayField.svelte +236 -78
  46. package/dist/visual/ArrayField.svelte.d.ts +0 -2
  47. package/dist/visual/BooleanField.svelte +12 -1
  48. package/dist/visual/Card.svelte +41 -23
  49. package/dist/visual/Card.svelte.d.ts +3 -2
  50. package/dist/visual/CardControls.svelte +15 -15
  51. package/dist/visual/DateField.svelte +93 -64
  52. package/dist/visual/EnumField.svelte +9 -4
  53. package/dist/visual/Field.svelte +20 -16
  54. package/dist/visual/Field.svelte.d.ts +4 -2
  55. package/dist/visual/FieldHint.svelte +22 -5
  56. package/dist/visual/FieldLabel.svelte +5 -33
  57. package/dist/visual/FieldLabel.svelte.d.ts +0 -2
  58. package/dist/visual/FormatPopover.svelte +78 -81
  59. package/dist/visual/MatrixField.svelte +28 -19
  60. package/dist/visual/NumberField.svelte +32 -3
  61. package/dist/visual/ObjectField.svelte +40 -24
  62. package/dist/visual/ObjectField.svelte.d.ts +6 -0
  63. package/dist/visual/ProseField.svelte +10 -13
  64. package/dist/visual/ProseField.svelte.d.ts +5 -2
  65. package/dist/visual/ProseValue.svelte +25 -19
  66. package/dist/visual/ProseValue.svelte.d.ts +4 -4
  67. package/dist/visual/TextField.svelte +3 -2
  68. package/dist/visual/TipsCard.svelte +7 -7
  69. package/dist/visual/VariantField.svelte +1 -1
  70. package/dist/visual/VariantField.svelte.d.ts +1 -1
  71. package/dist/visual/VisualEditorInner.svelte +121 -39
  72. package/dist/visual/controls.css +21 -2
  73. package/dist/visual/icons/Icon.svelte +3 -2
  74. package/dist/visual/icons/Icon.svelte.d.ts +3 -2
  75. package/dist/visual/icons/nodes.d.ts +25 -21
  76. package/dist/visual/icons/nodes.js +9 -7
  77. package/dist/visual/leaves.d.ts +12 -9
  78. package/dist/visual/leaves.js +19 -0
  79. package/dist/visual/motion.d.ts +25 -3
  80. package/dist/visual/motion.js +58 -9
  81. package/dist/visual/strings.d.ts +15 -9
  82. package/dist/visual/strings.js +19 -12
  83. package/dist/visual/structure.d.ts +21 -31
  84. package/dist/visual/structure.js +54 -51
  85. package/package.json +3 -3
@@ -41,18 +41,21 @@
41
41
  baseType,
42
42
  controlKind,
43
43
  declaredContent,
44
+ fieldModels,
44
45
  flatObject,
45
46
  humanize,
46
47
  isContainer,
48
+ literalText,
47
49
  mountKey,
48
- obliged,
49
50
  optionalCell,
51
+ placeFields,
50
52
  printedText,
51
53
  renderedBoolean,
52
54
  shortCell
53
55
  } from './structure.js';
54
56
  import { splitDeep, unrouted, type DeepDiagnostic } from './diagnostics.js';
55
57
  import type { LandingBox } from './leaves.js';
58
+ import { isEmptyContent } from '../core/codec/index.js';
56
59
  import { wording } from './strings.js';
57
60
  import { propertyDomIds } from './domid.js';
58
61
  import FieldLabel from './FieldLabel.svelte';
@@ -102,6 +105,12 @@
102
105
  /** `ui.layout: flat`: no vertical and no rung, the properties at the depth of the
103
106
  * list the subform stands in. */
104
107
  flat?: boolean;
108
+ /** A card-level object, or one nested in it: its properties are placed as a
109
+ * card's fields are (`placeFields`), so moving a field into a group leaves its row
110
+ * as it was. Unset,
111
+ * every short cell packs: an array's record, a variant's world and a matrix
112
+ * member's columns are one record's cells, not a card's fields. */
113
+ placed?: boolean;
105
114
  /** Raw keydown off a text or prose cell, for a row whose own keys run through its
106
115
  * cells: the table's Enter and Backspace ({@link ArrayField}). */
107
116
  onCellKey?: (e: KeyboardEvent, key: string) => void;
@@ -121,6 +130,7 @@
121
130
  onCommit,
122
131
  bare = false,
123
132
  flat = false,
133
+ placed = false,
124
134
  onCellKey,
125
135
  diagnostics
126
136
  }: Props = $props();
@@ -129,15 +139,16 @@
129
139
  const entries = $derived(Object.entries(properties ?? {}));
130
140
  const obj = $derived((value ?? {}) as Record<string, unknown>);
131
141
  const has = (key: string): boolean => Object.hasOwn(properties ?? {}, key);
142
+ const spans = $derived(
143
+ placed
144
+ ? new Map(
145
+ placeFields(fieldModels({ fields: properties ?? {} })).map((p) => [p.field.name, p.span])
146
+ )
147
+ : undefined
148
+ );
132
149
  const routed = $derived(splitDeep(diagnostics));
133
150
  const foot = $derived(unrouted(routed, (step) => typeof step === 'string' && has(step)));
134
151
 
135
- /** Obligation, read off the cell exactly as `fieldModels` reads it off a field
136
- * ({@link obliged}). `validate` anchors it per leaf, so a property with no
137
- * `default:` is obliged in its own right and the container around it holds none
138
- * (VISUAL_EDITOR §"Enum variants"). */
139
- const required = obliged;
140
-
141
152
  const title = (key: string, sub: QuillFieldSchema): string => sub.title ?? humanize(key);
142
153
  /** What an unset optional cell ghosts, which is what it prints: `none`. An answered
143
154
  * cell ghosts nothing, an empty answer printing empty. */
@@ -151,8 +162,7 @@
151
162
  /** The `aria-label` fallback, for a subform mounted without a field's id space:
152
163
  * the field's name and the property's, since nothing else names the control. */
153
164
  const fallbackName = (key: string, sub: QuillFieldSchema): string =>
154
- `${label != null ? `${label} ` : ''}${title(key, sub)}` +
155
- (required(sub) ? ` ${t.strings.fieldRequired}` : '');
165
+ `${label != null ? `${label} ` : ''}${title(key, sub)}`;
156
166
 
157
167
  /** The handles of the cells the DOM cannot answer for, keyed by property name —
158
168
  * which is stable, a subform drawing its properties by declaration and never by
@@ -285,10 +295,17 @@
285
295
  kind === 'boolean'}
286
296
  <!-- A container takes the full span at every depth: its rows are the document's
287
297
  to count, and a track beside it would stand in a column of whitespace
288
- (`packable`). A block prose cell spans it too, and keeps its label row. -->
298
+ (`packable`). A block prose cell spans it too, and keeps its label row, as
299
+ does a placed cell `placeFields` gives the whole row. -->
289
300
  {@const block = kind === 'prose' && !shortCell(sub)}
290
301
  <div
291
- class={['qm-object-prop', { 'qm-prop-full': isContainer(kind), 'qm-prop-wide': block }]}
302
+ class={[
303
+ 'qm-object-prop',
304
+ {
305
+ 'qm-prop-full': isContainer(kind),
306
+ 'qm-prop-wide': block || (!isContainer(kind) && spans?.get(key) === 'full')
307
+ }
308
+ ]}
292
309
  data-qm-prop={key}
293
310
  >
294
311
  {#if ids && kind !== 'array' && kind !== 'matrix'}
@@ -298,7 +315,6 @@
298
315
  id={ids.label}
299
316
  descriptionId={describes}
300
317
  onActivate={labelable ? undefined : () => focusProp(key)}
301
- required={required(sub)}
302
318
  description={sub.description}
303
319
  />
304
320
  {/if}
@@ -364,13 +380,14 @@
364
380
  content={() => contentAt([key])}
365
381
  value={obj[key]}
366
382
  fallback={contentDefault(sub)}
367
- plaintext={baseType(sub) === 'plaintext'}
383
+ plaintext={literalText(sub)}
368
384
  placeholder={noneOf(key, sub)}
369
385
  {block}
370
386
  label={named}
371
387
  labelledBy={ids?.label}
372
388
  describedBy={describes}
373
- onChange={(rt) => commitProp(key, rt)}
389
+ onChange={(rt) =>
390
+ commitProp(key, optionalCell(sub) && isEmptyContent(rt) ? undefined : rt)}
374
391
  onKey={onCellKey ? (e) => onCellKey(e, key) : undefined}
375
392
  />
376
393
  {:else if kind === 'array'}
@@ -386,7 +403,6 @@
386
403
  layout={arrayLayout(sub)}
387
404
  max={sub.max}
388
405
  label={title(key, sub)}
389
- required={required(sub)}
390
406
  description={sub.description}
391
407
  labelId={ids?.label}
392
408
  descriptionId={describes}
@@ -401,6 +417,7 @@
401
417
  value={obj[key] as Record<string, unknown> | undefined}
402
418
  properties={sub.properties}
403
419
  flat={flatObject(sub)}
420
+ {placed}
404
421
  label={ids ? undefined : fallbackName(key, sub)}
405
422
  idBase={ids?.control}
406
423
  labelledBy={ids?.label}
@@ -457,10 +474,8 @@
457
474
  /* The vertical and the rung between it and the properties, `--_qm-nest` being the step
458
475
  the card and the section hold too.
459
476
 
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.
477
+ `--_qm-border`: this is the stroke that structures (ARCHITECTURE §"A plane is a
478
+ tone"), the one the card's edge and the section's vertical read.
464
479
 
465
480
  `padding-block` is the stroke's end caps, the same rung and the same reason as a
466
481
  section's (`Card.svelte`). A cap and not a leading gap: what stands the subform off
@@ -516,15 +531,16 @@
516
531
  align-items: start;
517
532
  min-width: 0;
518
533
  }
519
- /* A subform of one takes half the capacity: at full capacity a single track reads as
520
- truncated, and there are no sibling properties for it to line up with. A stranded
521
- field takes the whole row instead (`placeFields`): the width it would leave empty is
522
- the card's, where this one sits inside a figure the nesting has already narrowed. */
534
+ /* A record of one cell takes half the capacity: at full capacity a single track reads
535
+ as truncated, and there are no sibling properties for it to line up with. A placed
536
+ property's span is `placeFields`', which strands a lone one to the whole row as it
537
+ does a card's field. */
523
538
  .qm-object-prop:only-child {
524
539
  grid-column: span var(--cols-half);
525
540
  }
526
541
  /* A block prose property holds paragraphs, so a track beside it stands in whitespace
527
- once it grows (`packable`); its label still rides the row above it. */
542
+ once it grows (`packable`); its label still rides the row above it. A placed
543
+ property `placeFields` gives the whole row spans it the same way. */
528
544
  .qm-object-prop.qm-prop-wide,
529
545
  .qm-object-prop.qm-prop-wide:only-child {
530
546
  grid-column: 1 / -1;
@@ -35,6 +35,12 @@ interface Props {
35
35
  /** `ui.layout: flat`: no vertical and no rung, the properties at the depth of the
36
36
  * list the subform stands in. */
37
37
  flat?: boolean;
38
+ /** A card-level object, or one nested in it: its properties are placed as a
39
+ * card's fields are (`placeFields`), so moving a field into a group leaves its row
40
+ * as it was. Unset,
41
+ * every short cell packs: an array's record, a variant's world and a matrix
42
+ * member's columns are one record's cells, not a card's fields. */
43
+ placed?: boolean;
38
44
  /** Raw keydown off a text or prose cell, for a row whose own keys run through its
39
45
  * cells: the table's Enter and Backspace ({@link ArrayField}). */
40
46
  onCellKey?: (e: KeyboardEvent, key: string) => void;
@@ -24,8 +24,9 @@
24
24
  plaintext?: boolean;
25
25
  /**
26
26
  * Draw no box: the card body's leaf, and nothing else. Not `block`, which is a
27
- * different predicate: a `richtext` field without `inline` is block prose and
28
- * still a control in a row of controls, so only the caller knows what is paper.
27
+ * different predicate: a `richtext` field declaring `multiline: true` is block
28
+ * prose and still a control in a row of controls, so only the caller knows what
29
+ * is paper.
29
30
  */
30
31
  unframed?: boolean;
31
32
  /** Accessible name for a leaf nothing else names: an array element, or the
@@ -45,6 +46,8 @@
45
46
  placeholder?: string;
46
47
  /** The placeholder goes at the leaf's first edit (`createField`). */
47
48
  placeholderUntilEdit?: boolean;
49
+ /** An emptied leaf removes the field (`createField`). */
50
+ emptyUnsets?: boolean;
48
51
  /** Registry identity, stamped on the DOM node so a remount is visible as one. */
49
52
  leafKey: string;
50
53
  onFocus?: (addr: Addr) => void;
@@ -73,6 +76,7 @@
73
76
  fallback,
74
77
  placeholder,
75
78
  placeholderUntilEdit,
79
+ emptyUnsets,
76
80
  leafKey,
77
81
  onFocus,
78
82
  onCaretMove,
@@ -122,6 +126,7 @@
122
126
  fallback,
123
127
  placeholder,
124
128
  placeholderUntilEdit,
129
+ emptyUnsets,
125
130
  tableStrings: () => t.strings,
126
131
  slashStrings: () => t.strings,
127
132
  onSlash: (next) => {
@@ -166,7 +171,7 @@
166
171
  <div
167
172
  {@attach mountField}
168
173
  class={[
169
- 'qm-prose',
174
+ 'qm-prose qm-wash',
170
175
  { 'qm-control-box': !unframed, 'qm-focus-ring-within': !unframed, 'qm-prose-block': block }
171
176
  ]}
172
177
  data-leaf-key={leafKey}
@@ -178,14 +183,6 @@
178
183
  <SlashMenu menu={slash} leaf={() => controller} label={t.strings.slashLabel} />
179
184
 
180
185
  <style>
181
- /* The box is `.qm-control-box` (controls.css), the same rule the input beside it
182
- draws, so the two agree on height by construction. No `min-height` with it:
183
- `core/codec/prose.css` resets the paragraph box, so one line of prose in this box
184
- measures one line of text in that one. */
185
- .qm-prose {
186
- /* Containing block for the arrival wash's inset child (`core/bloom.ts`). */
187
- position: relative;
188
- }
189
186
  /* A leaf the inline schema does not constrain grows, and opens at one line box: the
190
187
  empty leaf is the height its first line will take, so the first keystroke displaces
191
188
  nothing below it. `1em`, not a size rung, which is the rule below's to name.
@@ -196,8 +193,8 @@
196
193
  .qm-prose-block :global(.ProseMirror) {
197
194
  min-height: calc(1em * var(--_qm-leading-body));
198
195
  }
199
- /* Paper, and the withheld box is what says so. Not `block` alone: `inline` is the
200
- quill's to declare, so a schema omitting it on a richtext field puts a block leaf in
196
+ /* Paper, and the withheld box is what says so. Not `block` alone: `multiline` is the
197
+ quill's to declare, so a schema spelling it on a richtext field puts a block leaf in
201
198
  a field row, where this rung would break the height agreement above. */
202
199
  .qm-prose-block:not(.qm-control-box) {
203
200
  font-size: var(--_qm-text-paper);
@@ -11,8 +11,9 @@ interface Props {
11
11
  plaintext?: boolean;
12
12
  /**
13
13
  * Draw no box: the card body's leaf, and nothing else. Not `block`, which is a
14
- * different predicate: a `richtext` field without `inline` is block prose and
15
- * still a control in a row of controls, so only the caller knows what is paper.
14
+ * different predicate: a `richtext` field declaring `multiline: true` is block
15
+ * prose and still a control in a row of controls, so only the caller knows what
16
+ * is paper.
16
17
  */
17
18
  unframed?: boolean;
18
19
  /** Accessible name for a leaf nothing else names: an array element, or the
@@ -32,6 +33,8 @@ interface Props {
32
33
  placeholder?: string;
33
34
  /** The placeholder goes at the leaf's first edit (`createField`). */
34
35
  placeholderUntilEdit?: boolean;
36
+ /** An emptied leaf removes the field (`createField`). */
37
+ emptyUnsets?: boolean;
35
38
  /** Registry identity, stamped on the DOM node so a remount is visible as one. */
36
39
  leafKey: string;
37
40
  onFocus?: (addr: Addr) => void;
@@ -1,6 +1,6 @@
1
1
  <!--
2
2
  A prose leaf the parent commits by value: an element of a `richtext` (fixture
3
- `keywords`) or `plaintext` (`errata`) array, and a content cell of a subform — an
3
+ `keywords`) or `text` (`authors`, `errata`) array, and a content cell of a subform — an
4
4
  `object`'s property, a variant's cell ({@link ObjectField}). None of them is
5
5
  `applyChange`-addressable: `Addr.field` is a flat name, so `keywords[0]` and
6
6
  `contact.note` have no op address. So this is not a `createField` leaf: it mounts a
@@ -19,7 +19,7 @@
19
19
 
20
20
  The content is read, not passed: `reader.getContentAt(addr, path)` decodes through
21
21
  the codec the leaf's own declared type names, so what it rests as — the content
22
- object, a `plaintext` literal, the authored string a transport door left — stops
22
+ object, a `text` literal, the authored string a transport door left — stops
23
23
  being the row's or the cell's business. It is read at mount and again wherever the
24
24
  parent's re-derive hands down a `value` that moved, and a read differing from the
25
25
  content the leaf last committed or took replaces the view, the gate `createField`
@@ -28,8 +28,8 @@
28
28
  it, so the echo replaces nothing and the caret and the history stay.
29
29
 
30
30
  The schema is the declared type's where the parent has room for it: a subform cell
31
- declaring no `inline` takes the full row ({@link ObjectField}), a `richtext` one on the
32
- block schema and a `plaintext` one on the plain schema, paragraphs and hard breaks.
31
+ declaring `multiline: true` takes the full row ({@link ObjectField}), a `richtext` one
32
+ on the block schema and a `text` one on the plain schema, paragraphs and hard breaks.
33
33
  An array element is one textblock whatever it declares, since Enter there is the
34
34
  repeater's. There is no slash menu at either width, and no island view.
35
35
 
@@ -80,8 +80,8 @@
80
80
  /** The default an unset leaf holds, where it prints: drawn at the default rung,
81
81
  * and handed up whole with the first edit in it, as `createField` takes one. */
82
82
  fallback?: Content;
83
- /** The mark-free schema (a `plaintext` leaf): literal text, no formatting,
84
- * exactly as the scalar field of that type mounts. */
83
+ /** The mark-free schema (a `text` leaf): literal text, no formatting, exactly
84
+ * as the field of that type mounts. */
85
85
  plaintext?: boolean;
86
86
  /** A multi-block schema, for a cell the parent draws at full width: the block
87
87
  * schema, or the plain one with `plaintext`. Absent, the leaf is one textblock. */
@@ -96,8 +96,8 @@
96
96
  labelledBy?: string;
97
97
  /** The parked `description` (FieldLabel) → `aria-describedby`. */
98
98
  describedBy?: string;
99
- /** The empty leaf's ghost, a subform cell's, until the leaf's first edit: the
100
- * `none` an optional cell prints unset. Read per decoration pass. */
99
+ /** The empty leaf's ghost, a subform cell's: the `none` an optional cell prints
100
+ * unset. Read per decoration pass. */
101
101
  placeholder?: string;
102
102
  onChange: (rt: Content) => void;
103
103
  /** Raw keydown, for a container whose own keys run through this leaf: the
@@ -130,9 +130,8 @@
130
130
  /** Take the caret: what a parent placing focus on this leaf calls. The view's
131
131
  * focus, not the element's: a PM view restores its selection, where a bare DOM
132
132
  * focus on a contenteditable leaves the caret unplaced. And the reveal beside it,
133
- * since PM's focus prevents the scroll a `string` element's input takes by
134
- * default — an array whose two item types revealed differently is one row of it
135
- * landing off screen. */
133
+ * since PM's focus prevents the scroll a form control takes by default — an array
134
+ * whose two item types revealed differently is one row of it landing off screen. */
136
135
  export function focus(): void {
137
136
  if (!view) return;
138
137
  view.focus();
@@ -157,11 +156,18 @@
157
156
  view.dispatch(view.state.tr.setSelection(sel).scrollIntoView());
158
157
  }
159
158
 
159
+ $effect(() => {
160
+ void [label, labelledBy, describedBy, placeholder];
161
+ view?.setProps({});
162
+ });
163
+
160
164
  function mountView(mount: HTMLDivElement): () => void {
161
165
  return untrack(() => {
162
166
  const inline = !block;
163
167
  const leaf = { inline, plaintext, islands: false };
164
- const names = { label, labelledBy, describedBy };
168
+ // Read per attribute pass, not captured: an element's name carries its index,
169
+ // which a remove above it moves.
170
+ const names = () => ({ label, labelledBy, describedBy });
165
171
  const shown = (stored: Content | undefined): Content => stored ?? fallback ?? emptyContent();
166
172
  const stored = content();
167
173
  let seen = value;
@@ -171,8 +177,8 @@
171
177
  // Which of `aria-label` / `aria-labelledby` wins is the codec's one answer
172
178
  // (`proseAttributes`), so a cell carrying a label element and a row carrying
173
179
  // none cannot name their regions by different rules.
174
- const plainNamed = proseAttributes(names) ?? {};
175
- const heldNamed = heldAttributes(names, heldId);
180
+ const plainNamed = () => proseAttributes(names()) ?? {};
181
+ const heldNamed = () => heldAttributes(names(), heldId);
176
182
  const build = (rt: Content): EditorState => {
177
183
  // The same keymap and plugin stack a `createField` leaf mounts (shared
178
184
  // `proseLeafPlugins`), minus the anchor-position plugin: anchors are dropped on
@@ -184,8 +190,7 @@
184
190
  ? []
185
191
  : proseLeafPlugins(schema, {
186
192
  inline,
187
- placeholder: () => placeholder,
188
- placeholderUntilEdit: true
193
+ placeholder: () => placeholder
189
194
  })
190
195
  });
191
196
  };
@@ -195,11 +200,12 @@
195
200
  state: build(reconciler.last),
196
201
  editable: () => !held,
197
202
  attributes: () => {
198
- const named = held ? heldNamed : plainNamed;
203
+ const named = held ? heldNamed() : plainNamed();
199
204
  return defaulted ? { ...named, 'data-default': '' } : named;
200
205
  },
201
206
  dispatchTransaction(tr) {
202
207
  const next = mounted.state.apply(tr);
208
+ if (next === mounted.state) return;
203
209
  const edit = tr.docChanged && !held;
204
210
  if (edit) defaulted = false;
205
211
  mounted.updateState(next);
@@ -265,8 +271,8 @@
265
271
  </script>
266
272
 
267
273
  <!-- `.qm-control-box` (controls.css) is the whole box, so an array of `richtext` and
268
- an array of `string` render rows of equal height, and a subform's prose cell measures
269
- like the text cell beside it. No floor: the reset in `core/codec/prose.css` makes one
274
+ an array of `text` render rows of equal height, and a subform's prose cell measures
275
+ like the form control beside it. No floor: the reset in `core/codec/prose.css` makes one
270
276
  line of prose measure one line. Width is the row's or the cell's to give: the leaf
271
277
  fills the track it is placed in. -->
272
278
  <div class="qm-prose-value qm-control-box qm-focus-ring-within">
@@ -12,8 +12,8 @@ interface Props {
12
12
  /** The default an unset leaf holds, where it prints: drawn at the default rung,
13
13
  * and handed up whole with the first edit in it, as `createField` takes one. */
14
14
  fallback?: Content;
15
- /** The mark-free schema (a `plaintext` leaf): literal text, no formatting,
16
- * exactly as the scalar field of that type mounts. */
15
+ /** The mark-free schema (a `text` leaf): literal text, no formatting, exactly
16
+ * as the field of that type mounts. */
17
17
  plaintext?: boolean;
18
18
  /** A multi-block schema, for a cell the parent draws at full width: the block
19
19
  * schema, or the plain one with `plaintext`. Absent, the leaf is one textblock. */
@@ -28,8 +28,8 @@ interface Props {
28
28
  labelledBy?: string;
29
29
  /** The parked `description` (FieldLabel) → `aria-describedby`. */
30
30
  describedBy?: string;
31
- /** The empty leaf's ghost, a subform cell's, until the leaf's first edit: the
32
- * `none` an optional cell prints unset. Read per decoration pass. */
31
+ /** The empty leaf's ghost, a subform cell's: the `none` an optional cell prints
32
+ * unset. Read per decoration pass. */
33
33
  placeholder?: string;
34
34
  onChange: (rt: Content) => void;
35
35
  /** Raw keydown, for a container whose own keys run through this leaf: the
@@ -1,6 +1,7 @@
1
1
  <!--
2
- A `string` field → text input. Commits every edit live (on input) via the parent's
3
- typed `writer.set`, so the preview tracks typing.
2
+ A text input: the control a field of a type this build does not know falls to.
3
+ Commits every edit live (on input) via the parent's typed `writer.set`, so the
4
+ preview tracks typing.
4
5
 
5
6
  An unset field whose `default:` prints holds that default as its text, at the
6
7
  default rung (theme.css): the value it prints, drawn as one and a step off a written
@@ -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.
@@ -72,7 +69,7 @@
72
69
  {#each tips as _, i (i)}
73
70
  <button
74
71
  type="button"
75
- class="qm-tips-dot"
72
+ class="qm-tips-dot qm-focus-ring"
76
73
  aria-label={t.strings.tipsDot(i + 1, tips.length)}
77
74
  aria-current={i === index}
78
75
  onclick={() => (cursor = i)}
@@ -80,11 +77,11 @@
80
77
  {/each}
81
78
  </div>
82
79
  {/if}
83
- <button type="button" class="qm-icon-btn qm-tips-action" onclick={advance}>
80
+ <button type="button" class="qm-icon-btn qm-tips-action qm-focus-ring" 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>
@@ -196,6 +193,9 @@
196
193
  .qm-tips-dot:hover {
197
194
  opacity: var(--_qm-opacity-muted);
198
195
  }
196
+ .qm-tips-dot:focus-visible {
197
+ opacity: 1;
198
+ }
199
199
  .qm-tips-dot[aria-current='true'] {
200
200
  opacity: 1;
201
201
  }
@@ -42,7 +42,7 @@
42
42
  describedBy?: string;
43
43
  /** The boundary's nested content read for this field, which the cells take
44
44
  * unprefixed: a variant's cell is one key in from the container it rests as, so
45
- * a `plaintext` cell's path is its own name ({@link ObjectField}). */
45
+ * a `text` cell's path is its own name ({@link ObjectField}). */
46
46
  contentAt: (path: PathStep[]) => Content | undefined;
47
47
  onCommit: (v: Record<string, unknown> | undefined) => void;
48
48
  optionAllowed?: (value: string) => boolean;
@@ -16,7 +16,7 @@ interface Props {
16
16
  describedBy?: string;
17
17
  /** The boundary's nested content read for this field, which the cells take
18
18
  * unprefixed: a variant's cell is one key in from the container it rests as, so
19
- * a `plaintext` cell's path is its own name ({@link ObjectField}). */
19
+ * a `text` cell's path is its own name ({@link ObjectField}). */
20
20
  contentAt: (path: PathStep[]) => Content | undefined;
21
21
  onCommit: (v: Record<string, unknown> | undefined) => void;
22
22
  optionAllowed?: (value: string) => boolean;