@quillmark/svelte 0.10.1 → 0.12.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 (80) hide show
  1. package/README.md +1 -1
  2. package/THEMING.md +2 -2
  3. package/dist/core/address.d.ts +17 -10
  4. package/dist/core/address.js +26 -12
  5. package/dist/core/bloom.js +4 -3
  6. package/dist/core/codec/blocks.js +21 -1
  7. package/dist/core/codec/breaks.d.ts +10 -2
  8. package/dist/core/codec/breaks.js +65 -9
  9. package/dist/core/codec/code.d.ts +6 -4
  10. package/dist/core/codec/code.js +7 -4
  11. package/dist/core/codec/decode.js +103 -68
  12. package/dist/core/codec/encode.d.ts +5 -8
  13. package/dist/core/codec/encode.js +56 -86
  14. package/dist/core/codec/field.js +43 -24
  15. package/dist/core/codec/index.d.ts +2 -1
  16. package/dist/core/codec/index.js +6 -3
  17. package/dist/core/codec/islands.d.ts +16 -11
  18. package/dist/core/codec/islands.js +70 -59
  19. package/dist/core/codec/keymap.js +2 -1
  20. package/dist/core/codec/marks.d.ts +17 -13
  21. package/dist/core/codec/marks.js +39 -42
  22. package/dist/core/codec/positions.d.ts +4 -1
  23. package/dist/core/codec/positions.js +5 -1
  24. package/dist/core/codec/reconcile.d.ts +6 -0
  25. package/dist/core/codec/reconcile.js +19 -0
  26. package/dist/core/codec/schema.d.ts +5 -8
  27. package/dist/core/codec/schema.js +30 -156
  28. package/dist/core/codec/slash.js +0 -0
  29. package/dist/core/codec/table-view.js +77 -26
  30. package/dist/core/codec/table.d.ts +6 -5
  31. package/dist/core/codec/table.js +14 -31
  32. package/dist/core/codec/urls.d.ts +20 -0
  33. package/dist/core/codec/urls.js +45 -0
  34. package/dist/core/rungs.d.ts +8 -0
  35. package/dist/core/rungs.js +24 -0
  36. package/dist/core/theme.css +10 -0
  37. package/dist/preset/recipes.css +1 -1
  38. package/dist/preset/scale.css +7 -4
  39. package/dist/preview/bridge.d.ts +8 -1
  40. package/dist/preview/bridge.js +31 -25
  41. package/dist/preview/controller.js +13 -13
  42. package/dist/preview/geometry.d.ts +12 -1
  43. package/dist/preview/geometry.js +14 -1
  44. package/dist/preview/paint.js +6 -4
  45. package/dist/preview/strings.d.ts +0 -2
  46. package/dist/preview/strings.js +5 -6
  47. package/dist/visual/ArrayField.svelte +600 -173
  48. package/dist/visual/ArrayField.svelte.d.ts +14 -1
  49. package/dist/visual/BooleanField.svelte +10 -7
  50. package/dist/visual/Card.svelte +8 -30
  51. package/dist/visual/DateField.svelte +2 -2
  52. package/dist/visual/Field.svelte +76 -32
  53. package/dist/visual/FieldHint.svelte +1 -1
  54. package/dist/visual/MatrixField.svelte +352 -0
  55. package/dist/visual/MatrixField.svelte.d.ts +36 -0
  56. package/dist/visual/ObjectField.svelte +356 -133
  57. package/dist/visual/ObjectField.svelte.d.ts +17 -2
  58. package/dist/visual/VariantField.svelte +55 -2
  59. package/dist/visual/VariantField.svelte.d.ts +10 -1
  60. package/dist/visual/VisualEditor.svelte +1 -1
  61. package/dist/visual/VisualEditorInner.svelte +118 -67
  62. package/dist/visual/controls.css +39 -9
  63. package/dist/visual/diagnostics.d.ts +26 -2
  64. package/dist/visual/diagnostics.js +52 -5
  65. package/dist/visual/ext.d.ts +12 -13
  66. package/dist/visual/ext.js +22 -19
  67. package/dist/visual/hold.js +2 -2
  68. package/dist/visual/icons/nodes.d.ts +3 -0
  69. package/dist/visual/icons/nodes.js +2 -1
  70. package/dist/visual/index.js +2 -3
  71. package/dist/visual/leaves.d.ts +25 -12
  72. package/dist/visual/links.d.ts +4 -0
  73. package/dist/visual/links.js +15 -4
  74. package/dist/visual/motion.d.ts +14 -5
  75. package/dist/visual/motion.js +13 -11
  76. package/dist/visual/strings.d.ts +26 -13
  77. package/dist/visual/strings.js +7 -2
  78. package/dist/visual/structure.d.ts +101 -8
  79. package/dist/visual/structure.js +252 -13
  80. package/package.json +2 -2
@@ -1,4 +1,4 @@
1
- import { nearestAddrForFieldPath } from '../core/address.js';
1
+ import { nearestAddrForFieldPath, nestedAddrForFieldPath } from '../core/address.js';
2
2
  /** Canonical string key for a `FieldKey`: the routing `Map`'s key space. */
3
3
  export function fieldKeyToString(k) {
4
4
  return `${k.card ?? 'main'}:${k.field ?? '$body'}`;
@@ -42,11 +42,53 @@ export function routeAndResolve(diagnostics, cardIds) {
42
42
  }
43
43
  return out;
44
44
  }
45
+ /** A field's routed diagnostics, each with its steps past the field: none for a
46
+ * diagnostic anchored at the field itself, or one whose path is not a nested one
47
+ * (a local commit error carries the field's own path). */
48
+ export function deepen(diagnostics) {
49
+ return (diagnostics ?? []).map((d) => ({
50
+ steps: (d.path && nestedAddrForFieldPath(d.path)?.steps) || [],
51
+ diagnostic: d
52
+ }));
53
+ }
54
+ export function splitDeep(list) {
55
+ const here = [];
56
+ const below = new Map();
57
+ for (const d of list ?? []) {
58
+ if (d.steps.length === 0) {
59
+ here.push(d.diagnostic);
60
+ continue;
61
+ }
62
+ const [step, ...rest] = d.steps;
63
+ const bucket = below.get(step);
64
+ const next = { steps: rest, diagnostic: d.diagnostic };
65
+ if (bucket)
66
+ bucket.push(next);
67
+ else
68
+ below.set(step, [next]);
69
+ }
70
+ return { here, below };
71
+ }
72
+ /** The diagnostics a rung cannot hand down: the ones anchored here, plus every one
73
+ * whose next step names nothing in `held`. Drawn at the rung's own foot, so a leaf the
74
+ * tree does not draw still has its message read somewhere the user can see. */
75
+ export function unrouted(split, held) {
76
+ const out = [...split.here];
77
+ for (const [step, list] of split.below)
78
+ if (!held(step))
79
+ for (const d of list)
80
+ out.push(d.diagnostic);
81
+ return out;
82
+ }
45
83
  /**
46
84
  * Merge N routed groups into `Map<fieldKeyToString(key), Diagnostic[]>`.
47
- * Dedupes an identical `(severity, message)` pair landing on the same key from
85
+ * Dedupes an identical `(path, severity, message)` quad landing on the same key from
48
86
  * more than one group (e.g. the same error present in both `validate()` and
49
87
  * an external feed). Producer order within a key.
88
+ *
89
+ * The key is the field, but the walk past it is the `path`'s ({@link deepen}), so two
90
+ * cells of one container share a key and differ only there: without `path` in the
91
+ * quad, one message failing two rows draws on the first row alone.
50
92
  */
51
93
  export function mergeDiagnostics(...groups) {
52
94
  const byKey = new Map();
@@ -54,9 +96,14 @@ export function mergeDiagnostics(...groups) {
54
96
  for (const group of groups) {
55
97
  for (const { key, diagnostic } of group) {
56
98
  const k = fieldKeyToString(key);
57
- // Collision-proof key: JSON-encode the triple so arbitrary `message` text
58
- // can never merge two distinct triples into one (a real diagnostic dropped).
59
- const dedupeKey = JSON.stringify([k, diagnostic.severity, diagnostic.message]);
99
+ // Collision-proof key: JSON-encode the quad so arbitrary `message` text
100
+ // can never merge two distinct quads into one (a real diagnostic dropped).
101
+ const dedupeKey = JSON.stringify([
102
+ k,
103
+ diagnostic.path ?? null,
104
+ diagnostic.severity,
105
+ diagnostic.message
106
+ ]);
60
107
  if (seen.has(dedupeKey))
61
108
  continue;
62
109
  seen.add(dedupeKey);
@@ -4,23 +4,22 @@ import type { Document, CardAddr } from '@quillmark/wasm';
4
4
  * main). A key whose patch value is `undefined` is dropped; every other key in the
5
5
  * namespace is carried through untouched.
6
6
  *
7
- * **The namespace is the write unit.** `storeExtNamespace` replaces the namespace
8
- * it targets (preserving sibling namespaces, but not sibling keys) so a writer
9
- * that stores only its own key silently destroys the others, and
10
- * `removeExtNamespace` destroys all of them. `tips` and `title` are siblings here,
11
- * so a namespace-replacing dismissal would wipe every renamed card's title. Routing
12
- * every writer through this function is what makes that unexpressible rather than
13
- * merely documented: key N+1 inherits the merge instead of re-deriving it.
7
+ * **The whole map is the write unit.** `storeExt` replaces `$ext` entire, so a writer
8
+ * that stores only its own key silently destroys every sibling namespace, and a
9
+ * namespace-replacing write destroys the sibling keys inside it. `tips` and `title`
10
+ * are siblings here, so either would wipe every renamed card's title. Routing every
11
+ * writer through this function is what makes that unexpressible rather than merely
12
+ * documented: key N+1 inherits the merge instead of re-deriving it.
14
13
  *
15
14
  * The drop is an explicit `delete`, not a stored `undefined`: whether a JS
16
15
  * `undefined` survives the wasm-bindgen crossing is not a property worth depending
17
16
  * on.
18
17
  *
19
- * **A namespace emptied of keys is removed, not stored empty.** That is
20
- * `removeExtNamespace`'s one correct use: it takes the whole namespace, and there is
21
- * nothing left in this one to lose. `$ext` goes with it when `editor` was the last
22
- * namespace, so a document nothing has renamed or hinted carries no editor slot
23
- * rather than an empty one. A patch dropping keys the namespace does not have writes
24
- * nothing, so dismissing tips on a document that carries none is not a mutation.
18
+ * **A namespace emptied of keys is removed, not stored empty**, and `$ext` goes with
19
+ * it when `editor` was the last namespace — `removeExt` being the one call that drops
20
+ * the map, where `storeExt({})` records an explicit empty one. So a document nothing
21
+ * has renamed or hinted carries no editor slot rather than an empty one. A patch
22
+ * dropping keys the namespace does not have writes nothing, so dismissing tips on a
23
+ * document that carries none is not a mutation.
25
24
  */
26
25
  export declare function patchEditorExt(doc: Document, addr: CardAddr, patch: Record<string, unknown>): void;
@@ -3,29 +3,27 @@
3
3
  * main). A key whose patch value is `undefined` is dropped; every other key in the
4
4
  * namespace is carried through untouched.
5
5
  *
6
- * **The namespace is the write unit.** `storeExtNamespace` replaces the namespace
7
- * it targets (preserving sibling namespaces, but not sibling keys) so a writer
8
- * that stores only its own key silently destroys the others, and
9
- * `removeExtNamespace` destroys all of them. `tips` and `title` are siblings here,
10
- * so a namespace-replacing dismissal would wipe every renamed card's title. Routing
11
- * every writer through this function is what makes that unexpressible rather than
12
- * merely documented: key N+1 inherits the merge instead of re-deriving it.
6
+ * **The whole map is the write unit.** `storeExt` replaces `$ext` entire, so a writer
7
+ * that stores only its own key silently destroys every sibling namespace, and a
8
+ * namespace-replacing write destroys the sibling keys inside it. `tips` and `title`
9
+ * are siblings here, so either would wipe every renamed card's title. Routing every
10
+ * writer through this function is what makes that unexpressible rather than merely
11
+ * documented: key N+1 inherits the merge instead of re-deriving it.
13
12
  *
14
13
  * The drop is an explicit `delete`, not a stored `undefined`: whether a JS
15
14
  * `undefined` survives the wasm-bindgen crossing is not a property worth depending
16
15
  * on.
17
16
  *
18
- * **A namespace emptied of keys is removed, not stored empty.** That is
19
- * `removeExtNamespace`'s one correct use: it takes the whole namespace, and there is
20
- * nothing left in this one to lose. `$ext` goes with it when `editor` was the last
21
- * namespace, so a document nothing has renamed or hinted carries no editor slot
22
- * rather than an empty one. A patch dropping keys the namespace does not have writes
23
- * nothing, so dismissing tips on a document that carries none is not a mutation.
17
+ * **A namespace emptied of keys is removed, not stored empty**, and `$ext` goes with
18
+ * it when `editor` was the last namespace — `removeExt` being the one call that drops
19
+ * the map, where `storeExt({})` records an explicit empty one. So a document nothing
20
+ * has renamed or hinted carries no editor slot rather than an empty one. A patch
21
+ * dropping keys the namespace does not have writes nothing, so dismissing tips on a
22
+ * document that carries none is not a mutation.
24
23
  */
25
24
  export function patchEditorExt(doc, addr, patch) {
26
- // `getExtNamespace` reads just this namespace rather than serializing the whole
27
- // card to fish out one `$ext` slot.
28
- const current = doc.getExtNamespace(addr, 'editor');
25
+ const ext = doc.getExt(addr) ?? {};
26
+ const current = ext.editor;
29
27
  const next = { ...(current ?? {}) };
30
28
  for (const [key, value] of Object.entries(patch)) {
31
29
  if (value === undefined)
@@ -34,7 +32,12 @@ export function patchEditorExt(doc, addr, patch) {
34
32
  next[key] = value;
35
33
  }
36
34
  if (Object.keys(next).length > 0)
37
- doc.storeExtNamespace(addr, 'editor', next);
38
- else if (current)
39
- doc.removeExtNamespace(addr, 'editor');
35
+ doc.storeExt(addr, { ...ext, editor: next });
36
+ else if (current) {
37
+ const { editor: _editor, ...rest } = ext;
38
+ if (Object.keys(rest).length > 0)
39
+ doc.storeExt(addr, rest);
40
+ else
41
+ doc.removeExt(addr);
42
+ }
40
43
  }
@@ -2,7 +2,7 @@
2
2
  // the layout, and what fills the viewport as a section collapses is that section's own
3
3
  // content, whose top never moves.
4
4
  import { flushSync } from 'svelte';
5
- import { rungMs } from './motion.js';
5
+ import { rungMs, rungStyle } from '../core/rungs.js';
6
6
  /**
7
7
  * Run `change` and keep `anchor` inside the fold for the length of the move.
8
8
  *
@@ -26,7 +26,7 @@ export function holdInView(anchor, change) {
26
26
  flushSync();
27
27
  const reveal = () => anchor.scrollIntoView({ block: 'nearest', behavior: 'instant' });
28
28
  reveal();
29
- const rung = rungMs(getComputedStyle(anchor), '--_qm-duration-slow');
29
+ const rung = rungMs(rungStyle(anchor), '--_qm-duration-slow');
30
30
  if (rung === undefined)
31
31
  return;
32
32
  const until = performance.now() + rung;
@@ -116,6 +116,9 @@ export declare const ICONS: {
116
116
  readonly minus: readonly [readonly ["path", {
117
117
  readonly d: "M5 12h14";
118
118
  }]];
119
+ readonly check: readonly [readonly ["path", {
120
+ readonly d: "M20 6 9 17l-5-5";
121
+ }]];
119
122
  };
120
123
  /** The closed set. A name that is not here is a compile error at the call site,
121
124
  * which is what a string key buys over thirteen imported components. */
@@ -63,5 +63,6 @@ export const ICONS = {
63
63
  ['path', { d: 'M3 6h18' }],
64
64
  ['path', { d: 'M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2' }]
65
65
  ],
66
- minus: [['path', { d: 'M5 12h14' }]]
66
+ minus: [['path', { d: 'M5 12h14' }]],
67
+ check: [['path', { d: 'M20 6 9 17l-5-5' }]]
67
68
  };
@@ -25,7 +25,6 @@ export { createField } from '../core/codec/index.js';
25
25
  // it. The empty body's per-card hook is the `bodyPlaceholder` key inside it.
26
26
  export { DEFAULT_VISUAL_STRINGS } from './strings.js';
27
27
  // The `$ext.editor` write unit. A consumer seeding editor-side chrome
28
- // state (a card title, the tips channel) goes through this rather than
29
- // `storeExtNamespace`, which replaces the namespace and takes the sibling keys with
30
- // it.
28
+ // state (a card title, the tips channel) goes through this rather than `storeExt`,
29
+ // which replaces the whole map and takes every sibling with it.
31
30
  export { patchEditorExt } from './ext.js';
@@ -1,4 +1,12 @@
1
+ import type { PathStep } from '@quillmark/wasm';
1
2
  import type { FieldController } from '../core/codec/index.js';
3
+ /**
4
+ * What a landing at depth hands back: the box the arrival wash blooms in, now or once
5
+ * the row it had to open has rendered. A row two closed boxes down does not exist
6
+ * until the row above it opens and flushes, so the innermost box is a promise wherever
7
+ * the walk had to open something on the way.
8
+ */
9
+ export type LandingBox = HTMLElement | undefined | Promise<HTMLElement | undefined>;
2
10
  /**
3
11
  * A mounted field's landing handle: focus it, and the element the arrival wash blooms
4
12
  * inside ({@link import('../core/bloom.js').bloomInside}).
@@ -10,22 +18,27 @@ import type { FieldController } from '../core/codec/index.js';
10
18
  export interface FieldControl {
11
19
  focus(): void;
12
20
  /**
13
- * Land in element `index` of a control that has elements, at USV `pos` where the
14
- * row's control can take one; resolving the index to the element's session id at
15
- * the call, with {@link focus}'s own answer for an index past the live list. Absent
16
- * on every control without elements.
21
+ * Land at `steps` inside a control that has an inside — an array's element, an
22
+ * object's property, a variant's cell, a matrix's member, and any of them under any
23
+ * other — at USV `pos` where the leaf the walk ends in can take one. Each rung
24
+ * resolves its own step at the call (an index to a session id, a key to a mounted
25
+ * cell) and hands the rest to the control it opened, so nothing positional is
26
+ * carried. A step the tree does not hold falls back to that rung's own
27
+ * {@link focus}: the field is right and the row is gone. Absent on every control
28
+ * with no inside.
17
29
  *
18
30
  * A landing, not a focus: an absent `pos` is the placement rung, exactly as on
19
- * `Landing`, and what a `pos` means is the row control's, the same way focusing is.
20
- * The place rides the call because the registry stays parent-keyed: a per-element
21
- * key is positional, in a registry whose doctrine is dodging positional churn.
31
+ * `Landing`, and what a `pos` means is the leaf's, the same way focusing is. The
32
+ * place rides the call because the registry stays parent-keyed: a per-element key is
33
+ * positional, in a registry whose doctrine is dodging positional churn.
22
34
  *
23
- * Returns the element's own box, which is what the wash blooms in: the address
24
- * named one row, and a wash over the whole repeater says the field where the click
25
- * said the row. `undefined` is the fallback the index past the live list took, and
26
- * reads as {@link el}.
35
+ * Returns the innermost row the address named, which is what the wash blooms in: a
36
+ * box wider than the one the click resolved says the field where the pick said the
37
+ * row. `undefined` is a landing that named no row — a property under a field-level
38
+ * subform, or the fallback an index past the live list took — and reads as
39
+ * {@link el}.
27
40
  */
28
- focusElement?(index: number, pos?: number): HTMLElement | undefined;
41
+ focusPath?(steps: readonly PathStep[], pos?: number): LandingBox;
29
42
  readonly el: HTMLElement;
30
43
  }
31
44
  /**
@@ -9,6 +9,10 @@ import type { Command, EditorState } from 'prosemirror-state';
9
9
  * A rooted value is left alone, being the spelling that asks for the embedding
10
10
  * page, and so is anything already carrying a scheme the link mark renders.
11
11
  *
12
+ * `localhost:5173` names a host too, and `SCHEME` reads its host as the scheme: a
13
+ * colon followed by a port is a host with a port, so it takes the host prefix rather
14
+ * than the refusal a scheme nothing renders gets.
15
+ *
12
16
  * `''` for a blank value and for a scheme the mark renders inert, both being nothing
13
17
  * to apply: `setLink` declines an empty href, so the refusal lands at the prompt's
14
18
  * own exit rather than storing a link that draws as plain text.
@@ -1,4 +1,4 @@
1
- import { rendersHref } from '../core/codec/index.js';
1
+ import { rendersHref, storableUrl } from '../core/codec/index.js';
2
2
  /** A scheme: a letter, then letters, digits, `+`, `-` or `.`, then `:` (RFC 3986). */
3
3
  const SCHEME = /^[a-z][a-z0-9+.-]*:/i;
4
4
  /** Rooted at the page rather than at a host: an absolute path, a query, a fragment,
@@ -6,6 +6,9 @@ const SCHEME = /^[a-z][a-z0-9+.-]*:/i;
6
6
  const ROOTED = /^[/#?]/;
7
7
  /** A bare mail address: one `@`, no scheme and no path. */
8
8
  const ADDRESS = /^[^\s/@]+@[^\s/@]+$/;
9
+ /** A host carrying a port, which spells a scheme to `SCHEME` and is not one: what
10
+ * follows the colon is digits, and no scheme the mark renders takes a port. */
11
+ const HOST_PORT = /^[a-z0-9.-]+:\d+([/?#]|$)/i;
9
12
  /**
10
13
  * The href a typed value stands for. A value with no scheme names a host, and
11
14
  * stored verbatim it is a relative href resolving against whatever page the editor
@@ -16,15 +19,23 @@ const ADDRESS = /^[^\s/@]+@[^\s/@]+$/;
16
19
  * A rooted value is left alone, being the spelling that asks for the embedding
17
20
  * page, and so is anything already carrying a scheme the link mark renders.
18
21
  *
22
+ * `localhost:5173` names a host too, and `SCHEME` reads its host as the scheme: a
23
+ * colon followed by a port is a host with a port, so it takes the host prefix rather
24
+ * than the refusal a scheme nothing renders gets.
25
+ *
19
26
  * `''` for a blank value and for a scheme the mark renders inert, both being nothing
20
27
  * to apply: `setLink` declines an empty href, so the refusal lands at the prompt's
21
28
  * own exit rather than storing a link that draws as plain text.
22
29
  */
23
30
  export function normalizeHref(raw) {
24
- const value = raw.trim();
25
- if (!value || !rendersHref(value))
31
+ const value = storableUrl(raw.trim());
32
+ if (!value)
33
+ return '';
34
+ if (!rendersHref(value) && !HOST_PORT.test(value))
26
35
  return '';
27
- if (SCHEME.test(value) || ROOTED.test(value))
36
+ if (SCHEME.test(value) && rendersHref(value))
37
+ return value;
38
+ if (ROOTED.test(value))
28
39
  return value;
29
40
  return ADDRESS.test(value) ? `mailto:${value}` : `https://${value}`;
30
41
  }
@@ -3,11 +3,20 @@ import type { AnimationConfig } from 'svelte/animate';
3
3
  * position for its whole length, so anything that measures the node's box waits on
4
4
  * these rather than reading the one a transform is holding. */
5
5
  export declare function reorderTrips(node: HTMLElement): Animation[];
6
- /** A duration rung off `style`, in ms; `undefined` where the derivation is out of reach
7
- * (an unstyled root, or jsdom, where `getComputedStyle` reports custom properties as
8
- * empty). No rung, no motion: a fallback here would be the scale restated in the one
9
- * place `check:style` cannot read. */
10
- export declare function rungMs(style: CSSStyleDeclaration, rung: string): number | undefined;
6
+ /**
7
+ * The reorder gesture's arming window, for a keyed list that takes the trip. The
8
+ * reconcile that moves a slot is the trip, and every other reconcile that happens to
9
+ * move one is not; so the command arms the gesture and the frame it lands in disarms
10
+ * it, `animate:` asking at apply time, a microtask after the mutation and well inside
11
+ * that frame. `armed` is a getter rather than a value, so the flag stays out of the
12
+ * template and needs no reactivity for it. `cancel` is for the surface's teardown.
13
+ */
14
+ export interface ReorderArm {
15
+ arm(): void;
16
+ armed(): boolean;
17
+ cancel(): void;
18
+ }
19
+ export declare function reorderArm(): ReorderArm;
11
20
  /**
12
21
  * Slide a keyed slot from where it was to where it now is.
13
22
  *
@@ -4,6 +4,7 @@
4
4
  // cannot see (the rects either side of the reconciliation) and WAAPI paints the trip,
5
5
  // the same seam `core/bloom.ts` takes to keep a duration in the derivation while a
6
6
  // script runs the animation.
7
+ import { rungMs, rungStyle } from '../core/rungs.js';
7
8
  /** Marks this module's runs, so a second reorder cancels the first rather than layering
8
9
  * a second transform over it. */
9
10
  const RUN_ID = 'qm-reorder';
@@ -15,16 +16,17 @@ export function reorderTrips(node) {
15
16
  return [];
16
17
  return node.getAnimations().filter((run) => run.id === RUN_ID);
17
18
  }
18
- /** A duration rung off `style`, in ms; `undefined` where the derivation is out of reach
19
- * (an unstyled root, or jsdom, where `getComputedStyle` reports custom properties as
20
- * empty). No rung, no motion: a fallback here would be the scale restated in the one
21
- * place `check:style` cannot read. */
22
- export function rungMs(style, rung) {
23
- const raw = style.getPropertyValue(rung).trim();
24
- const n = Number.parseFloat(raw);
25
- if (!Number.isFinite(n))
26
- return undefined;
27
- return raw.endsWith('ms') ? n : n * 1000;
19
+ export function reorderArm() {
20
+ let reordering = false;
21
+ let frame = 0;
22
+ return {
23
+ arm() {
24
+ reordering = true;
25
+ frame = requestAnimationFrame(() => (reordering = false));
26
+ },
27
+ armed: () => reordering,
28
+ cancel: () => cancelAnimationFrame(frame)
29
+ };
28
30
  }
29
31
  /**
30
32
  * Slide a keyed slot from where it was to where it now is.
@@ -45,7 +47,7 @@ export function reorder(node, { from, to }, armed) {
45
47
  // rung the derivation mints.
46
48
  if (!armed() || typeof node.animate !== 'function')
47
49
  return {};
48
- const style = getComputedStyle(node);
50
+ const style = rungStyle(node);
49
51
  const duration = rungMs(style, '--_qm-duration-slow');
50
52
  const easing = style.getPropertyValue('--_qm-ease-reverse').trim();
51
53
  if (duration === undefined || !easing)
@@ -16,27 +16,37 @@ export interface VisualStrings extends TableChromeStrings, SlashStrings {
16
16
  cardTitle: string;
17
17
  /** The trailing strip's visible words, which are also every strip's accessible
18
18
  * name; the plus rides the wording. One string over
19
- * one kind and many: the trigger seeds A card, and which kind is the menu's word
19
+ * one kind and many: the trigger seeds a card, and which kind is the menu's word
20
20
  * to say or the schema's to have already settled. */
21
21
  addCard: string;
22
22
  arrayAdd: string;
23
23
  arrayRemove: string;
24
- /** The tag on the unset sentinel's row, which is what tells it from the member it
25
- * ghosts ({@link EnumField}). */
26
- enumUnsetTag: string;
24
+ /** The reorder pair on an `object` row, accessible names: the buttons are glyphs. */
25
+ arrayMoveUp: string;
26
+ arrayMoveDown: string;
27
+ /**
28
+ * A row control's accessible name, the action and the row it acts on: `Remove
29
+ * Authors 2`. The action alone stays the tooltip. Parametric, so word order stays
30
+ * the translator's.
31
+ */
32
+ arrayRowAction: (action: string, row: string) => string;
27
33
  /**
28
- * What stands where a subform declines to recurse — a nested `array` or `object`
29
- * property, whose own figure would nest a field past the depth this one draws —
30
- * and where to edit that property instead. The whole sentence, since a translator
31
- * orders the kind within it.
34
+ * The count beside the add chip on an array declaring `max:`, the carrier a
35
+ * disabled chip has no other way to state: `2 / 3`. Parametric, so a translator
36
+ * spells the separator.
32
37
  */
33
- nestedUnsupported: (kind: string) => string;
38
+ arrayCount: (count: number, max: number) => string;
34
39
  /**
35
- * A collapsed array element whose first text property is empty: the label and the
40
+ * A collapsed array element with nothing to summarize itself by: the label and the
36
41
  * 1-based index, which is what its accessible name already says. Parametric, so
37
42
  * word order stays the translator's.
38
43
  */
39
44
  elementUntitled: (label: string, index: number) => string;
45
+ /** The tag on the unset sentinel's row, which is what tells it from the member it
46
+ * ghosts ({@link EnumField}). */
47
+ enumUnsetTag: string;
48
+ /** The count in the matrix's label row: how many of the roster are held. */
49
+ matrixHeld: (held: number, total: number) => string;
40
50
  /** The required marker's accessible name; the glyph itself is a `*`. */
41
51
  fieldRequired: string;
42
52
  formatGroup: string;
@@ -64,6 +74,9 @@ export interface VisualStrings extends TableChromeStrings, SlashStrings {
64
74
  retypeLabel: string;
65
75
  retypePlaceholder: string;
66
76
  noCardKinds: string;
77
+ /** The body leaf's accessible name: no label names it, the body being the one
78
+ * surface in a card that is paper ({@link Card}). */
79
+ bodyLabel: string;
67
80
  /**
68
81
  * The flat built-in invitation: the package's own words, kept to the one thing
69
82
  * true of every body so it claims nothing about the card it sits in.
@@ -91,9 +104,9 @@ export interface VisualStrings extends TableChromeStrings, SlashStrings {
91
104
  * - **`edit::field_coercion_failed`**: re-words from the app's own control state, the
92
105
  * refused value being in neither document (unchanged on throw) nor schema.
93
106
  * - **`parse::yaml_error_with_location`, `invalid_structure`**: does not re-word. No
94
- * `path`, and `args` carries the location (`blockIndex`, `line`) and nothing else,
95
- * engine prose riding under no key: the parser's own text, its column and its caret
96
- * snippet exist only inside `message`.
107
+ * `path`; `args` names the block (`blockIndex`) and `location` places the failure in
108
+ * the document's own coordinates, engine prose riding under no key: the parser's own
109
+ * text, its column and its caret snippet exist only inside `message`.
97
110
  * - **`LiveSession.warnings`**: does not re-word. Backend text, an external feed.
98
111
  *
99
112
  * The last two are the boundary's shape, not a gap in it, so the fallback arm is
@@ -30,9 +30,13 @@ export const DEFAULT_VISUAL_STRINGS = {
30
30
  addCard: '+ Add Card',
31
31
  arrayAdd: '+ Add',
32
32
  arrayRemove: 'Remove',
33
- enumUnsetTag: 'default',
34
- nestedUnsupported: (kind) => `A nested ${kind} — edit this field in the source view.`,
33
+ arrayMoveUp: 'Move up',
34
+ arrayMoveDown: 'Move down',
35
+ arrayRowAction: (action, row) => `${action} ${row}`,
36
+ arrayCount: (count, max) => `${count} / ${max}`,
35
37
  elementUntitled: (label, index) => `${label} ${index}`,
38
+ enumUnsetTag: 'default',
39
+ matrixHeld: (held, total) => `${held} of ${total} held`,
36
40
  fieldRequired: 'required',
37
41
  formatGroup: 'Formatting',
38
42
  formatBold: 'Bold (Mod-B)',
@@ -55,6 +59,7 @@ export const DEFAULT_VISUAL_STRINGS = {
55
59
  retypeLabel: 'Change to',
56
60
  retypePlaceholder: 'Choose a type…',
57
61
  noCardKinds: 'This document declares no card types — delete this card to remove it.',
62
+ bodyLabel: 'Body',
58
63
  bodyGhost: 'Write…',
59
64
  bodyPlaceholder: () => undefined
60
65
  };