@quillmark/svelte 0.21.0 → 0.23.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 (69) hide show
  1. package/NOTICE +6 -7
  2. package/README.md +1 -1
  3. package/dist/core/address.d.ts +6 -1
  4. package/dist/core/address.js +12 -3
  5. package/dist/core/bloom.d.ts +9 -4
  6. package/dist/core/bloom.js +18 -49
  7. package/dist/core/codec/atoms.d.ts +1 -1
  8. package/dist/core/codec/atoms.js +1 -1
  9. package/dist/core/codec/blocks.js +4 -2
  10. package/dist/core/codec/cap.d.ts +21 -0
  11. package/dist/core/codec/cap.js +63 -0
  12. package/dist/core/codec/code.d.ts +1 -1
  13. package/dist/core/codec/code.js +1 -1
  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 -10
  18. package/dist/core/codec/field.js +54 -18
  19. package/dist/core/codec/index.d.ts +3 -1
  20. package/dist/core/codec/index.js +3 -2
  21. package/dist/core/codec/inputrules.js +1 -1
  22. package/dist/core/codec/lists.d.ts +3 -2
  23. package/dist/core/codec/lists.js +21 -5
  24. package/dist/core/codec/nesting.d.ts +20 -0
  25. package/dist/core/codec/nesting.js +62 -0
  26. package/dist/core/codec/prose.css +74 -20
  27. package/dist/core/codec/schema.d.ts +2 -2
  28. package/dist/core/codec/schema.js +16 -7
  29. package/dist/core/codec/table-copy.d.ts +17 -0
  30. package/dist/core/codec/table-copy.js +84 -0
  31. package/dist/core/codec/table-view.d.ts +1 -7
  32. package/dist/core/codec/table-view.js +70 -119
  33. package/dist/core/codec/table.d.ts +2 -9
  34. package/dist/core/codec/table.js +10 -19
  35. package/dist/core/codec/tasks.d.ts +19 -0
  36. package/dist/core/codec/tasks.js +140 -0
  37. package/dist/core/errors.d.ts +2 -2
  38. package/dist/visual/ArrayField.svelte +103 -63
  39. package/dist/visual/ArrayField.svelte.d.ts +0 -2
  40. package/dist/visual/Card.svelte +9 -3
  41. package/dist/visual/Card.svelte.d.ts +2 -2
  42. package/dist/visual/Field.svelte +33 -17
  43. package/dist/visual/Field.svelte.d.ts +4 -2
  44. package/dist/visual/FieldLabel.svelte +19 -33
  45. package/dist/visual/FieldLabel.svelte.d.ts +4 -2
  46. package/dist/visual/FormatPopover.svelte +54 -73
  47. package/dist/visual/MatrixField.svelte +18 -12
  48. package/dist/visual/NumberField.svelte +32 -3
  49. package/dist/visual/ObjectField.svelte +50 -22
  50. package/dist/visual/ObjectField.svelte.d.ts +6 -0
  51. package/dist/visual/ProseField.svelte +20 -14
  52. package/dist/visual/ProseField.svelte.d.ts +8 -2
  53. package/dist/visual/ProseValue.svelte +30 -19
  54. package/dist/visual/ProseValue.svelte.d.ts +7 -4
  55. package/dist/visual/TextField.svelte +3 -2
  56. package/dist/visual/TipsCard.svelte +5 -2
  57. package/dist/visual/VariantField.svelte +1 -1
  58. package/dist/visual/VariantField.svelte.d.ts +1 -1
  59. package/dist/visual/VisualEditorInner.svelte +13 -13
  60. package/dist/visual/controls.css +19 -1
  61. package/dist/visual/icons/nodes.d.ts +0 -21
  62. package/dist/visual/icons/nodes.js +1 -7
  63. package/dist/visual/leaves.d.ts +12 -9
  64. package/dist/visual/leaves.js +19 -0
  65. package/dist/visual/strings.d.ts +16 -5
  66. package/dist/visual/strings.js +17 -9
  67. package/dist/visual/structure.d.ts +26 -32
  68. package/dist/visual/structure.js +71 -53
  69. package/package.json +3 -3
@@ -0,0 +1,140 @@
1
+ // A task item is a `list_item` whose `checked` is a boolean (CODEC §"Task items"). Its
2
+ // `toDOM` draws a checkbox ahead of the item's blocks, disabled, which is the drawing a
3
+ // held leaf, a copy and `renderContent` take; a leaf that edits mounts the item through
4
+ // `taskItemView`, whose checkbox toggles. A task item is minted by source mode and by a
5
+ // paste alone: no shorthand, command or key makes one.
6
+ import { DOMSerializer } from 'prosemirror-model';
7
+ import { Plugin } from 'prosemirror-state';
8
+ import { splitListItem } from 'prosemirror-schema-list';
9
+ const SVG = 'http://www.w3.org/2000/svg';
10
+ /** The id the checkbox names its item's blocks by, fresh per drawing. */
11
+ let drawn = 0;
12
+ /** The `checked` an `<li>` states: `data-qm-checked`, which `toDOM` writes, else none. */
13
+ export function taskChecked(el) {
14
+ const stated = el.getAttribute('data-qm-checked');
15
+ return stated === null ? null : stated === 'true';
16
+ }
17
+ /** Where an `<li>`'s blocks are: a task item's body, past its checkbox; the item itself
18
+ * otherwise. */
19
+ export function taskBody(el) {
20
+ const li = el;
21
+ const body = li.hasAttribute('data-qm-checked')
22
+ ? li.querySelector(':scope > [data-qm-task-body]')
23
+ : null;
24
+ return body ?? li;
25
+ }
26
+ /** A task item's `<li>`: the checkbox, unreachable by the caret, then the body holding
27
+ * the item's blocks, which names it. The mark is the check glyph a matrix tick draws. */
28
+ export function taskDOM(checked) {
29
+ const body = `qm-task-${++drawn}`;
30
+ return [
31
+ 'li',
32
+ { 'data-qm-checked': String(checked) },
33
+ [
34
+ 'span',
35
+ { class: 'qm-task', contenteditable: 'false' },
36
+ [
37
+ 'input',
38
+ {
39
+ type: 'checkbox',
40
+ class: 'qm-task-box',
41
+ 'aria-labelledby': body,
42
+ checked: checked ? '' : null,
43
+ disabled: ''
44
+ }
45
+ ],
46
+ [
47
+ `${SVG} svg`,
48
+ {
49
+ class: 'qm-task-mark',
50
+ viewBox: '0 0 24 24',
51
+ fill: 'none',
52
+ stroke: 'currentColor',
53
+ 'stroke-width': '2',
54
+ 'stroke-linecap': 'round',
55
+ 'stroke-linejoin': 'round',
56
+ 'aria-hidden': 'true'
57
+ },
58
+ [`${SVG} path`, { d: 'M20 6 9 17l-5-5' }]
59
+ ]
60
+ ],
61
+ ['div', { id: body, 'data-qm-task-body': '' }, 0]
62
+ ];
63
+ }
64
+ const isTask = (node) => node.attrs.checked !== null;
65
+ /**
66
+ * Every `list_item` in a leaf that edits, drawn by its own `toDOM`. A task item's checkbox
67
+ * is enabled, and a change to it is one `checked` transaction, so one `Mod-z` takes it
68
+ * back; where the leaf declines the edit the box returns to the item's value. The box
69
+ * keeps the press from the caret: a mousedown leaves the selection and the focus where
70
+ * they were, and ProseMirror reads no event aimed at it.
71
+ */
72
+ function taskItemView(node, view, getPos) {
73
+ const { dom, contentDOM } = DOMSerializer.renderSpec(document, node.type.spec.toDOM(node));
74
+ const box = isTask(node)
75
+ ? dom.querySelector(':scope > .qm-task > input')
76
+ : null;
77
+ if (box) {
78
+ box.disabled = false;
79
+ box.addEventListener('mousedown', (e) => e.preventDefault());
80
+ box.addEventListener('change', () => {
81
+ const pos = getPos();
82
+ if (pos === undefined || !view.editable) {
83
+ box.checked = node.attrs.checked;
84
+ return;
85
+ }
86
+ view.dispatch(view.state.tr.setNodeAttribute(pos, 'checked', box.checked));
87
+ });
88
+ }
89
+ return {
90
+ dom,
91
+ contentDOM,
92
+ update(next) {
93
+ if (next.type !== node.type || isTask(next) !== isTask(node))
94
+ return false;
95
+ node = next;
96
+ if (box) {
97
+ box.checked = next.attrs.checked;
98
+ dom.setAttribute('data-qm-checked', String(next.attrs.checked));
99
+ }
100
+ return true;
101
+ },
102
+ stopEvent: (e) => box !== null && e.target === box,
103
+ ignoreMutation: (m) => m.type !== 'selection' && !contentDOM.contains(m.target)
104
+ };
105
+ }
106
+ /** The node view a block leaf mounts its list items through. */
107
+ export function taskPlugin() {
108
+ return new Plugin({ props: { nodeViews: { list_item: taskItemView } } });
109
+ }
110
+ /**
111
+ * `splitListItem`, leaving the item that takes none of a task item's text an open task:
112
+ * the one below, or the one above where Enter is pressed at the head of the item's text.
113
+ * An empty item the split lifts out of a nested list keeps its own `checked`, as a
114
+ * Shift-Tab lift keeps it.
115
+ */
116
+ export function splitTaskItem(itemType) {
117
+ const split = splitListItem(itemType);
118
+ return (state, dispatch, view) => {
119
+ const { $from, empty } = state.selection;
120
+ const item = $from.depth >= 2 ? $from.node(-1) : null;
121
+ if (!dispatch || item?.type !== itemType || !isTask(item))
122
+ return split(state, dispatch, view);
123
+ const lifted = $from.parent.content.size === 0 && item.childCount === 1;
124
+ const atHead = empty && $from.parentOffset === 0 && $from.index(-1) === 0;
125
+ return split(state, (tr) => {
126
+ const $at = tr.selection.$from;
127
+ const below = $at.before($at.depth - 1);
128
+ if (lifted)
129
+ tr.setNodeAttribute(below, 'checked', item.attrs.checked);
130
+ else if (atHead) {
131
+ const above = tr.doc.resolve(below).nodeBefore;
132
+ if (above)
133
+ tr.setNodeAttribute(below - above.nodeSize, 'checked', false);
134
+ }
135
+ else
136
+ tr.setNodeAttribute(below, 'checked', false);
137
+ dispatch(tr);
138
+ });
139
+ };
140
+ }
@@ -11,8 +11,8 @@ export type EditorErrorCode =
11
11
  * the whole projection instead, so the store is correct and this field's
12
12
  * identity anchors were paid. */
13
13
  | 'commit-fallback'
14
- /** The overwrite fallback also failed, or the typed writer refused a `plaintext`
15
- * leaf's text: the optimistic PM state stands and the store is stale for this
14
+ /** The overwrite fallback also failed, or the typed writer refused a `text` leaf's
15
+ * text: the optimistic PM state stands and the store is stale for this
16
16
  * field. The one code here that leaves damage. */
17
17
  | 'commit-lost'
18
18
  /** A card operation (add, move, remove, retype) that threw. The document is
@@ -2,7 +2,7 @@
2
2
  An `array` field → an add/remove repeater. Elements commit by value: every
3
3
  edit / add / remove / move 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
- by `items.type`: `richtext` / `plaintext` → a prose element
5
+ by `items.type`: `richtext` / `text` → a prose element
6
6
  ({@link ProseValue}), `object` → a summary row that opens onto a subform, or a
7
7
  table's row where the array asks for one and the cells are short, everything else →
8
8
  a text input. The add chip sits in the label header row (space-between with the field
@@ -71,9 +71,10 @@
71
71
  controlKind,
72
72
  declaredContent,
73
73
  humanize,
74
- obliged,
74
+ literalText,
75
75
  rowSummary,
76
76
  schemaAt,
77
+ textCap,
77
78
  type ArrayLayout
78
79
  } from './structure.js';
79
80
  import { splitDeep, unrouted, type DeepDiagnostic } from './diagnostics.js';
@@ -105,8 +106,6 @@
105
106
  max?: number;
106
107
  /** Accessible-name prefix for the element controls (`label` + 1-based index). */
107
108
  label?: string;
108
- /** No-default field → a persistent required `*` on the label. */
109
- required?: boolean;
110
109
  /** Schema `description`: the label's help affordance. */
111
110
  description?: string;
112
111
  /** The label's own DOM id. An array is a group (N inputs, no single `for`
@@ -140,7 +139,6 @@
140
139
  layout = 'list',
141
140
  max,
142
141
  label,
143
- required,
144
142
  description,
145
143
  labelId,
146
144
  descriptionId,
@@ -519,9 +517,10 @@
519
517
  * any cell an open row holds: Alt+↑/↓, the table island's own binding. The nearest
520
518
  * row answers, or none does: a press inside a nested row is that row's whether it
521
519
  * holds a reorder or not, so a key in a table's cell or a list of strings' input
522
- * moves nothing, and never the record around it. */
520
+ * moves nothing, and never the record around it. A held drag owns the row until its
521
+ * release. */
523
522
  function onRowKey(e: KeyboardEvent, k: number): void {
524
- if (!e.altKey || e.shiftKey || e.ctrlKey || e.metaKey || e.isComposing) return;
523
+ if (!e.altKey || e.shiftKey || e.ctrlKey || e.metaKey || e.isComposing || drag) return;
525
524
  // A cell that took the key — a select opening, a date segment stepping, a prose
526
525
  // leaf joining — owns it.
527
526
  if (e.defaultPrevented) return;
@@ -536,42 +535,53 @@
536
535
 
537
536
  // ── The drag handle ──────────────────────────────────────────────────────────
538
537
  // Press-and-drag the handle moves its row; a press that does not travel past the dead
539
- // zone does nothing. The rows are measured once the drag engages, nothing reflowing
540
- // until the release (the lift is a tone and the drop rule is out of flow), and the
541
- // release is the one splice `move` makes.
538
+ // zone does nothing. The drop is the row under the pointer, read off the rows where
539
+ // they stand, so a scroll by any hand re-picks it. Held in an edge band of its
540
+ // scrollport, the drag scrolls it toward the rows out of view, once the pointer has
541
+ // been outside the band: a press on a row already in the band does not scroll. Nothing
542
+ // reflows until the release (the lift is a tone and the drop rule is out of flow), and
543
+ // the release is the one splice `move` makes. A handle that loses the pointer, or
544
+ // leaves the DOM, ends the drag with no move.
542
545
  const DEAD_ZONE = 3;
543
- type RowDrag = {
544
- k: number;
545
- origin: number;
546
- engaged: boolean;
547
- drop: number;
548
- heads: { start: number; end: number }[];
549
- };
546
+ /** The band at a scrollport's top and bottom edges that scrolls it under a held drag,
547
+ * and the most one frame scrolls, reached at the edge itself. */
548
+ const EDGE = 48;
549
+ const STEP = 16;
550
+ type RowDrag = { k: number; origin: number; y: number; engaged: boolean; drop: number };
550
551
  let drag = $state<RowDrag | undefined>();
552
+ let follow: { grip: Element; port: Element; armed: boolean; frame: number } | undefined;
551
553
  function onHandleDown(e: PointerEvent, k: number): void {
552
554
  if (e.button !== 0) return;
553
555
  e.preventDefault();
554
- (e.currentTarget as HTMLElement).setPointerCapture?.(e.pointerId);
555
- drag = { k, origin: e.clientY, engaged: false, drop: k, heads: [] };
556
+ release();
557
+ const grip = e.currentTarget as HTMLElement;
558
+ grip.setPointerCapture?.(e.pointerId);
559
+ drag = { k, origin: e.clientY, y: e.clientY, engaged: false, drop: k };
560
+ follow = { grip, port: scrollport(grip), armed: false, frame: 0 };
556
561
  }
557
562
  function onHandleMove(e: PointerEvent): void {
558
563
  if (!drag) return;
564
+ drag.y = e.clientY;
559
565
  if (!drag.engaged) {
560
566
  if (Math.abs(e.clientY - drag.origin) < DEAD_ZONE) return;
561
567
  drag.engaged = true;
562
- drag.heads = ids.map((id) => {
563
- const box = rowEls[id]?.getBoundingClientRect();
564
- return { start: box?.top ?? 0, end: box?.bottom ?? 0 };
565
- });
568
+ addEventListener('scroll', repick, { capture: true, passive: true });
569
+ if (follow) follow.frame = requestAnimationFrame(edgeScroll);
566
570
  }
567
- drag.drop = dropIndex(drag, e.clientY);
571
+ repick();
572
+ }
573
+ function repick(): void {
574
+ if (drag?.engaged) drag.drop = dropIndex(drag.k, drag.y);
568
575
  }
576
+ const rowBox = (i: number): DOMRect | undefined => rowEls[ids[i]]?.getBoundingClientRect();
569
577
  /** The row the pointer is over, or the nearest one past either end. */
570
- function dropIndex(d: RowDrag, y: number): number {
571
- let best = d.k;
578
+ function dropIndex(k: number, y: number): number {
579
+ let best = k;
572
580
  let nearest = Infinity;
573
- d.heads.forEach((head, i) => {
574
- const gap = y < head.start ? head.start - y : y > head.end ? y - head.end : 0;
581
+ ids.forEach((_, i) => {
582
+ const box = rowBox(i);
583
+ if (!box) return;
584
+ const gap = y < box.top ? box.top - y : y > box.bottom ? y - box.bottom : 0;
575
585
  if (gap < nearest) {
576
586
  nearest = gap;
577
587
  best = i;
@@ -579,9 +589,45 @@
579
589
  });
580
590
  return best;
581
591
  }
592
+ const page = (): Element => document.scrollingElement ?? document.documentElement;
593
+ /** The nearest ancestor that scrolls vertically, else the page. */
594
+ function scrollport(from: Element): Element {
595
+ for (let el = from.parentElement; el; el = el.parentElement) {
596
+ const { overflowY } = getComputedStyle(el);
597
+ if (/auto|scroll/.test(overflowY) && el.scrollHeight > el.clientHeight) return el;
598
+ }
599
+ return page();
600
+ }
601
+ function edgeScroll(): void {
602
+ if (!follow || !drag?.engaged) return;
603
+ if (!follow.grip.isConnected) {
604
+ drag = undefined;
605
+ release();
606
+ return;
607
+ }
608
+ const { port } = follow;
609
+ const box = port === page() ? undefined : port.getBoundingClientRect();
610
+ const top = Math.max(box?.top ?? 0, 0);
611
+ const bottom = Math.min(box?.bottom ?? innerHeight, innerHeight);
612
+ const into = Math.min(drag.y - top - EDGE, 0) + Math.max(drag.y - bottom + EDGE, 0);
613
+ if (!into) follow.armed = true;
614
+ // Only toward rows still out of view: past the list's end there is nothing to reach.
615
+ const beyond =
616
+ into < 0 ? (rowBox(0)?.top ?? top) < top : (rowBox(ids.length - 1)?.bottom ?? 0) > bottom;
617
+ if (into && follow.armed && beyond)
618
+ port.scrollBy({ top: STEP * Math.max(-1, Math.min(1, into / EDGE)), behavior: 'instant' });
619
+ follow.frame = requestAnimationFrame(edgeScroll);
620
+ }
621
+ function release(): void {
622
+ if (follow) cancelAnimationFrame(follow.frame);
623
+ follow = undefined;
624
+ removeEventListener('scroll', repick, { capture: true });
625
+ }
626
+ onDestroy(release);
582
627
  function onHandleUp(e: PointerEvent): void {
583
628
  const done = drag;
584
629
  drag = undefined;
630
+ release();
585
631
  if (!done?.engaged || e.type !== 'pointerup') return;
586
632
  move(done.k, done.drop);
587
633
  }
@@ -600,14 +646,7 @@
600
646
  >
601
647
  <div class="qm-array-header">
602
648
  {#if label != null}
603
- <FieldLabel
604
- {label}
605
- id={labelId}
606
- {descriptionId}
607
- onActivate={focus}
608
- {required}
609
- {description}
610
- />
649
+ <FieldLabel {label} id={labelId} {descriptionId} onActivate={focus} {description} />
611
650
  {:else}
612
651
  <span></span>
613
652
  {/if}
@@ -632,15 +671,15 @@
632
671
  each block's one child: a branch inside the block would stand between them. -->
633
672
  {#if table}
634
673
  <div
635
- class={['qm-array-rows qm-array-table', { empty: ids.length === 0 }]}
674
+ class={['qm-array-rows qm-array-table qm-wash', { empty: ids.length === 0 }]}
636
675
  data-default={defaulted ? '' : undefined}
637
676
  bind:this={rowsEl}
638
677
  >
639
678
  <div class="qm-array-table-scroller" style:--table-cols={columns.length}>
640
679
  <!-- The header names the columns, so a cell carries no label of its own: its
641
680
  accessible name is the row's and the column's composed (`ObjectField`). The
642
- obligation mark and the guidance affordance ride the header, being the
643
- column's rather than any one cell's. -->
681
+ guidance affordance rides the header, being the column's rather than any
682
+ one cell's. -->
644
683
  <div class="qm-array-table-head">
645
684
  {#each columns as [key, sub] (key)}
646
685
  <div
@@ -652,7 +691,6 @@
652
691
  <FieldLabel
653
692
  label={columnTitle(key, sub)}
654
693
  descriptionId={idBase != null ? propertyDomIds(idBase, key).description : undefined}
655
- required={obliged(sub)}
656
694
  description={sub.description}
657
695
  />
658
696
  </div>
@@ -660,7 +698,11 @@
660
698
  <span class="qm-array-table-head-end"></span>
661
699
  </div>
662
700
  {#each ids as id, k (id)}
663
- <div class="qm-array-row qm-array-table-row" bind:this={rowEls[id]}>
701
+ <div
702
+ class="qm-array-row qm-array-table-row qm-wash"
703
+ data-qm-index={k}
704
+ bind:this={rowEls[id]}
705
+ >
664
706
  <ObjectField
665
707
  bare
666
708
  bind:this={cellEls[id]}
@@ -680,7 +722,7 @@
680
722
  </div>
681
723
  {:else if control === 'object'}
682
724
  <div
683
- class={['qm-array-rows', { empty: ids.length === 0 }]}
725
+ class={['qm-array-rows qm-wash', { empty: ids.length === 0 }]}
684
726
  data-default={defaulted ? '' : undefined}
685
727
  bind:this={rowsEl}
686
728
  >
@@ -690,10 +732,11 @@
690
732
  interaction of the row's own: the row is no tab stop. -->
691
733
  <div
692
734
  class={[
693
- 'qm-array-row qm-element',
735
+ 'qm-array-row qm-element qm-wash',
694
736
  { open: open.has(id), lifted: drag?.engaged && drag.k === k }
695
737
  ]}
696
738
  data-drop={dropSide(k)}
739
+ data-qm-index={k}
697
740
  bind:this={rowEls[id]}
698
741
  animate:reorder={arm.armed}
699
742
  onkeydown={(e) => onRowKey(e, k)}
@@ -708,6 +751,7 @@
708
751
  type="button"
709
752
  class="qm-control-box qm-focus-ring qm-element-summary"
710
753
  aria-expanded={open.has(id)}
754
+ aria-keyshortcuts="Alt+ArrowUp Alt+ArrowDown"
711
755
  onclick={() => toggleRow(id)}
712
756
  >
713
757
  <!-- Leading, and it rotates: trailing is the figure for pushing a new
@@ -745,18 +789,19 @@
745
789
  </div>
746
790
  {:else}
747
791
  <div
748
- class={['qm-array-rows', { empty: ids.length === 0 }]}
792
+ class={['qm-array-rows qm-wash', { empty: ids.length === 0 }]}
749
793
  data-default={defaulted ? '' : undefined}
750
794
  bind:this={rowsEl}
751
795
  >
752
796
  {#each ids as id, k (id)}
753
- <div class="qm-array-row" bind:this={rowEls[id]}>
797
+ <div class="qm-array-row qm-wash" data-qm-index={k} bind:this={rowEls[id]}>
754
798
  {#if control === 'prose'}
755
799
  <ProseValue
756
800
  bind:this={els[id]}
757
801
  content={() => readAt([k])}
758
802
  value={arr[k]}
759
- plaintext={items != null && baseType(items) === 'plaintext'}
803
+ plaintext={items != null && literalText(items)}
804
+ cap={textCap(items)}
760
805
  label={rowName(k)}
761
806
  onChange={(rt) => commitElement(k, rt)}
762
807
  onKey={(e) => onElementKey(e, k)}
@@ -794,20 +839,20 @@
794
839
  {/snippet}
795
840
 
796
841
  <!-- An `object` row's drag handle, a table grip's twin hung off the row's start edge.
797
- It holds no focus, as a grip does not: its keyboard twin is Alt+↑/↓ from anywhere in
798
- the row. -->
842
+ Pointer-only and out of the accessibility tree: a press on it does nothing, so a
843
+ button there would be one a screen reader activates to no effect. Its twin is
844
+ Alt+↑/↓ from anywhere in the row, which the summary announces. -->
799
845
  {#snippet handle(k: number)}
800
- <button
801
- type="button"
846
+ <span
802
847
  class="qm-row-handle"
803
- tabindex="-1"
848
+ aria-hidden="true"
804
849
  title={t.strings.arrayDrag}
805
- aria-label={rowAction(t.strings.arrayDrag, k)}
806
850
  onpointerdown={(e) => onHandleDown(e, k)}
807
851
  onpointermove={onHandleMove}
808
852
  onpointerup={onHandleUp}
809
853
  onpointercancel={onHandleUp}
810
- ><span class="qm-row-handle-bar"><Icon name="grip-vertical" /></span></button
854
+ onlostpointercapture={onHandleUp}
855
+ ><span class="qm-row-handle-bar"><Icon name="grip-vertical" /></span></span
811
856
  >
812
857
  {/snippet}
813
858
 
@@ -841,14 +886,13 @@
841
886
  }
842
887
  /* The elements, in a box of their own: the arrival wash blooms here rather than over
843
888
  the wrapper `Field` hands this component (`washBox`), which is the label's box too.
844
- Positioned for the wash's inset child, and rounded to the rung a row's own box
845
- draws, the way `.qm-field-control` is for every other control.
889
+ Rounded to the rung a row's own box draws, the way `.qm-field-control` is for every
890
+ other control.
846
891
 
847
892
  `display: none` when there are no elements, so the header does not stand a gap above
848
893
  an empty box; a wash over it then paints nothing, which is what an array with
849
894
  nothing in it has to show. */
850
895
  .qm-array-rows {
851
- position: relative;
852
896
  display: flex;
853
897
  flex-direction: column;
854
898
  gap: var(--_qm-space);
@@ -873,7 +917,6 @@
873
917
  against. `minmax(0, …)` because a long unbroken value grows an `auto` track, and the
874
918
  edge with it. */
875
919
  .qm-array-row {
876
- position: relative;
877
920
  display: grid;
878
921
  grid-template-columns: minmax(0, 1fr);
879
922
  row-gap: var(--_qm-space);
@@ -953,9 +996,6 @@
953
996
  display: flex;
954
997
  align-items: center;
955
998
  justify-content: center;
956
- padding: 0;
957
- border: none;
958
- background: none;
959
999
  color: var(--_qm-ink-label);
960
1000
  cursor: grab;
961
1001
  touch-action: none;
@@ -988,7 +1028,7 @@
988
1028
  .qm-element.lifted > .qm-element-head > .qm-element-summary {
989
1029
  opacity: var(--_qm-opacity-muted);
990
1030
  }
991
- .qm-element[data-drop]::after {
1031
+ .qm-element[data-drop]::before {
992
1032
  content: '';
993
1033
  position: absolute;
994
1034
  inset-inline: 0;
@@ -997,10 +1037,10 @@
997
1037
  border-radius: var(--_qm-radius-pill);
998
1038
  pointer-events: none;
999
1039
  }
1000
- .qm-element[data-drop='before']::after {
1040
+ .qm-element[data-drop='before']::before {
1001
1041
  top: calc(-0.5 * (var(--_qm-space) + var(--_qm-ring-width)));
1002
1042
  }
1003
- .qm-element[data-drop='after']::after {
1043
+ .qm-element[data-drop='after']::before {
1004
1044
  bottom: calc(-0.5 * (var(--_qm-space) + var(--_qm-ring-width)));
1005
1045
  }
1006
1046
  /* ── An object element ──────────────────────────────────────────────────────
@@ -15,8 +15,6 @@ interface Props {
15
15
  max?: number;
16
16
  /** Accessible-name prefix for the element controls (`label` + 1-based index). */
17
17
  label?: string;
18
- /** No-default field → a persistent required `*` on the label. */
19
- required?: boolean;
20
18
  /** Schema `description`: the label's help affordance. */
21
19
  description?: string;
22
20
  /** The label's own DOM id. An array is a group (N inputs, no single `for`
@@ -10,7 +10,7 @@
10
10
  // component renders standalone too.
11
11
  const t = wording();
12
12
  import Icon from './icons/Icon.svelte';
13
- import type { Document, Quill, Addr, Diagnostic } from '@quillmark/wasm';
13
+ import type { Document, Quill, Addr, Diagnostic, PathStep } from '@quillmark/wasm';
14
14
  import type { EditorErrorHandler } from '../core/errors.js';
15
15
  import type { LeafRegistry } from './leaves.js';
16
16
  import type { CardModel, FieldModel } from './structure.js';
@@ -48,7 +48,7 @@
48
48
  kinds: string[];
49
49
  ops: CardOps;
50
50
  enumDisallowed?: 'hide' | 'disable';
51
- onFocus?: (addr: Addr) => void;
51
+ onFocus?: (addr: Addr, steps?: readonly PathStep[]) => void;
52
52
  onCaretMove?: (addr: Addr, pos: number) => void;
53
53
  onChange?: (addr: Addr) => void;
54
54
  onError?: EditorErrorHandler;
@@ -279,7 +279,7 @@
279
279
  <div class="qm-group-head">
280
280
  <button
281
281
  type="button"
282
- class="qm-group-header"
282
+ class="qm-group-header qm-focus-ring"
283
283
  id={headIds?.label}
284
284
  bind:this={headers[group]}
285
285
  aria-expanded={isOpen}
@@ -538,6 +538,12 @@
538
538
  .qm-card-title:focus {
539
539
  color: var(--_qm-ink);
540
540
  }
541
+ /* The automatic title, at the label ink every placeholder takes rather than the UA's
542
+ grey, and steady under focus, where a typed title steps to the ink. */
543
+ .qm-card-title::placeholder {
544
+ color: var(--_qm-ink-label);
545
+ opacity: 1;
546
+ }
541
547
  .qm-card-body {
542
548
  display: flex;
543
549
  flex-direction: column;
@@ -1,4 +1,4 @@
1
- import type { Document, Quill, Addr, Diagnostic } from '@quillmark/wasm';
1
+ import type { Document, Quill, Addr, Diagnostic, PathStep } from '@quillmark/wasm';
2
2
  import type { EditorErrorHandler } from '../core/errors.js';
3
3
  import type { LeafRegistry } from './leaves.js';
4
4
  import type { CardModel } from './structure.js';
@@ -27,7 +27,7 @@ interface Props {
27
27
  kinds: string[];
28
28
  ops: CardOps;
29
29
  enumDisallowed?: 'hide' | 'disable';
30
- onFocus?: (addr: Addr) => void;
30
+ onFocus?: (addr: Addr, steps?: readonly PathStep[]) => void;
31
31
  onCaretMove?: (addr: Addr, pos: number) => void;
32
32
  onChange?: (addr: Addr) => void;
33
33
  onError?: EditorErrorHandler;