@quillmark/svelte 0.11.0 → 0.13.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 (62) hide show
  1. package/dist/core/address.d.ts +17 -10
  2. package/dist/core/address.js +26 -12
  3. package/dist/core/bloom.js +4 -3
  4. package/dist/core/codec/decode.d.ts +17 -0
  5. package/dist/core/codec/decode.js +78 -19
  6. package/dist/core/codec/encode.js +1 -1
  7. package/dist/core/codec/field.d.ts +64 -15
  8. package/dist/core/codec/field.js +201 -65
  9. package/dist/core/codec/index.d.ts +3 -3
  10. package/dist/core/codec/index.js +3 -3
  11. package/dist/core/codec/islands.d.ts +5 -4
  12. package/dist/core/codec/islands.js +23 -38
  13. package/dist/core/codec/keymap.d.ts +5 -4
  14. package/dist/core/codec/keymap.js +5 -4
  15. package/dist/core/codec/prose.css +42 -0
  16. package/dist/core/codec/schema.d.ts +24 -5
  17. package/dist/core/codec/schema.js +40 -9
  18. package/dist/core/codec/slash.js +0 -0
  19. package/dist/core/errors.d.ts +3 -2
  20. package/dist/core/rungs.d.ts +8 -0
  21. package/dist/core/rungs.js +24 -0
  22. package/dist/core/theme.css +19 -6
  23. package/dist/preview/paint.js +21 -17
  24. package/dist/visual/ArrayField.svelte +649 -176
  25. package/dist/visual/ArrayField.svelte.d.ts +16 -1
  26. package/dist/visual/BooleanField.svelte +36 -11
  27. package/dist/visual/BooleanField.svelte.d.ts +2 -0
  28. package/dist/visual/Card.svelte +6 -25
  29. package/dist/visual/DateField.svelte +66 -31
  30. package/dist/visual/EnumField.svelte +27 -9
  31. package/dist/visual/EnumField.svelte.d.ts +5 -0
  32. package/dist/visual/Field.svelte +113 -38
  33. package/dist/visual/FormatPopover.svelte +4 -3
  34. package/dist/visual/MatrixField.svelte +353 -0
  35. package/dist/visual/MatrixField.svelte.d.ts +36 -0
  36. package/dist/visual/NumberField.svelte +31 -14
  37. package/dist/visual/NumberField.svelte.d.ts +5 -1
  38. package/dist/visual/ObjectField.svelte +404 -139
  39. package/dist/visual/ObjectField.svelte.d.ts +17 -2
  40. package/dist/visual/ProseField.svelte +53 -23
  41. package/dist/visual/ProseField.svelte.d.ts +15 -3
  42. package/dist/visual/ProseValue.svelte +102 -46
  43. package/dist/visual/ProseValue.svelte.d.ts +17 -5
  44. package/dist/visual/TextField.svelte +35 -18
  45. package/dist/visual/TextField.svelte.d.ts +7 -0
  46. package/dist/visual/VariantField.svelte +56 -2
  47. package/dist/visual/VariantField.svelte.d.ts +10 -1
  48. package/dist/visual/VisualEditorInner.svelte +77 -73
  49. package/dist/visual/controls.css +47 -5
  50. package/dist/visual/diagnostics.d.ts +26 -2
  51. package/dist/visual/diagnostics.js +52 -5
  52. package/dist/visual/hold.js +2 -2
  53. package/dist/visual/icons/nodes.d.ts +3 -0
  54. package/dist/visual/icons/nodes.js +2 -1
  55. package/dist/visual/leaves.d.ts +25 -12
  56. package/dist/visual/motion.d.ts +14 -5
  57. package/dist/visual/motion.js +13 -11
  58. package/dist/visual/strings.d.ts +27 -10
  59. package/dist/visual/strings.js +8 -2
  60. package/dist/visual/structure.d.ts +159 -40
  61. package/dist/visual/structure.js +337 -45
  62. package/package.json +2 -2
@@ -1,4 +1,4 @@
1
- import type { Addr, HitGranularity } from '@quillmark/wasm';
1
+ import type { Addr, HitGranularity, PathStep } from '@quillmark/wasm';
2
2
  /**
3
3
  * A canonical field address: `main.<field>` / `main.body` /
4
4
  * `cards.<kind>[<i>].<field>`, cards keyed by absolute document index.
@@ -63,7 +63,7 @@ export declare function cardPath(index: number, kinds: readonly string[]): DocPa
63
63
  /**
64
64
  * The inverse: a canonical `DocPath` back to the `Addr` the document verbs take, or
65
65
  * `undefined` for a path that names no single commit address — a nested or
66
- * array-element path (`main.keywords[0]`, which {@link elementAddrForFieldPath} and
66
+ * array-element path (`main.keywords[0]`, which {@link nestedAddrForFieldPath} and
67
67
  * {@link nearestAddrForFieldPath} take instead), a field-rooted one, or a malformed
68
68
  * one. A bare card and a `.body` terminal both land on the field-less `{card: i}`
69
69
  * the body leaf answers to.
@@ -86,14 +86,21 @@ export declare function addrForFieldPath(path: DocPath): Addr | undefined;
86
86
  */
87
87
  export declare function nearestAddrForFieldPath(path: DocPath): Addr | undefined;
88
88
  /**
89
- * An array element's address, split: `main.keywords[0]` lands here, and anything
90
- * {@link addrForFieldPath} can name — or whose trailing segment is not an index —
91
- * does not. `regions()`, `positionAt` and `formatDocPath` all spell the index
92
- * segment bracketed, so there is one spelling to read and none to bridge.
89
+ * A nested address, split: the field's `Addr`, and the steps from it to the leaf.
90
+ * `main.keywords[0]` is `{field: keywords, steps: [0]}`,
91
+ * `main.vectors[0].tours[2].title` is `[0, 'tours', 2, 'title']`, and
92
+ * `main.contact.email` is `['email']`: an index segment reads as a number and a key as
93
+ * a string, the `PathStep` grammar `reader.getContentAt` walks. Anything
94
+ * {@link addrForFieldPath} can name — a field, a body, a card — has no steps and lands
95
+ * here as `undefined`; so does a body with something after it, a body holding no
96
+ * cells. Whether the schema declares each step is the caller's to check: this module
97
+ * holds the grammar and no schema. `regions()`, `positionAt` and `formatDocPath` all
98
+ * spell the index segment bracketed, so there is one spelling to read and none to
99
+ * bridge.
93
100
  */
94
- export declare function elementAddrForFieldPath(path: DocPath): ElementAddr | undefined;
95
- /** An array element's address: the array's `Addr`, and which element of it. */
96
- export interface ElementAddr {
101
+ export declare function nestedAddrForFieldPath(path: DocPath): NestedAddr | undefined;
102
+ /** A nested address: the field's `Addr`, and the steps from it to the leaf. */
103
+ export interface NestedAddr {
97
104
  field: Addr;
98
- index: number;
105
+ steps: PathStep[];
99
106
  }
@@ -37,7 +37,7 @@ export function cardPath(index, kinds) {
37
37
  /**
38
38
  * The inverse: a canonical `DocPath` back to the `Addr` the document verbs take, or
39
39
  * `undefined` for a path that names no single commit address — a nested or
40
- * array-element path (`main.keywords[0]`, which {@link elementAddrForFieldPath} and
40
+ * array-element path (`main.keywords[0]`, which {@link nestedAddrForFieldPath} and
41
41
  * {@link nearestAddrForFieldPath} take instead), a field-rooted one, or a malformed
42
42
  * one. A bare card and a `.body` terminal both land on the field-less `{card: i}`
43
43
  * the body leaf answers to.
@@ -66,21 +66,35 @@ export function nearestAddrForFieldPath(path) {
66
66
  return segs && addrForSegs(segs.slice(0, 2));
67
67
  }
68
68
  /**
69
- * An array element's address, split: `main.keywords[0]` lands here, and anything
70
- * {@link addrForFieldPath} can name — or whose trailing segment is not an index —
71
- * does not. `regions()`, `positionAt` and `formatDocPath` all spell the index
72
- * segment bracketed, so there is one spelling to read and none to bridge.
69
+ * A nested address, split: the field's `Addr`, and the steps from it to the leaf.
70
+ * `main.keywords[0]` is `{field: keywords, steps: [0]}`,
71
+ * `main.vectors[0].tours[2].title` is `[0, 'tours', 2, 'title']`, and
72
+ * `main.contact.email` is `['email']`: an index segment reads as a number and a key as
73
+ * a string, the `PathStep` grammar `reader.getContentAt` walks. Anything
74
+ * {@link addrForFieldPath} can name — a field, a body, a card — has no steps and lands
75
+ * here as `undefined`; so does a body with something after it, a body holding no
76
+ * cells. Whether the schema declares each step is the caller's to check: this module
77
+ * holds the grammar and no schema. `regions()`, `positionAt` and `formatDocPath` all
78
+ * spell the index segment bracketed, so there is one spelling to read and none to
79
+ * bridge.
73
80
  */
74
- export function elementAddrForFieldPath(path) {
81
+ export function nestedAddrForFieldPath(path) {
75
82
  const segs = segsOf(path);
76
- if (!segs)
83
+ if (!segs || segs.length < 3)
77
84
  return undefined;
78
- const last = segs[segs.length - 1];
79
- if (last?.seg !== 'index')
85
+ const field = addrForSegs(segs.slice(0, 2));
86
+ if (field?.field == null)
80
87
  return undefined;
81
- const field = addrForSegs(segs.slice(0, -1));
82
- // A body has no elements: the parent must name a field.
83
- return field?.field != null ? { field, index: last.index } : undefined;
88
+ const steps = [];
89
+ for (const seg of segs.slice(2)) {
90
+ if (seg.seg === 'index')
91
+ steps.push(seg.index);
92
+ else if (seg.seg === 'field')
93
+ steps.push(seg.name);
94
+ else
95
+ return undefined;
96
+ }
97
+ return { field, steps };
84
98
  }
85
99
  /** `parseDocPath`, with a malformed path as `undefined` rather than a throw. The gate
86
100
  * is read outside the try: an uninitialized core is not a malformed path, and would
@@ -6,6 +6,7 @@
6
6
  // fixed by `--_qm-accent-wash`, so interpolating 0→1→0 keeps the scale in CSS where
7
7
  // `color-mix` resolves, instead of in keyframe values a script would have to
8
8
  // pre-resolve and re-resolve on a theme change.
9
+ import { rungStyle } from './rungs.js';
9
10
  const BLOOM_CLASS = 'qm-bloom';
10
11
  const WASH = 'var(--_qm-accent-wash)';
11
12
  /** The dwell used when the derivation is out of reach (an unstyled root, or jsdom,
@@ -24,8 +25,8 @@ const FRAMES = [
24
25
  ];
25
26
  const FRAMES_REDUCED = [{ opacity: 1 }, { opacity: 1 }];
26
27
  /**
27
- * Run one wash on `el`, resolving the dwell off it so the number is single-sourced
28
- * in CSS.
28
+ * Run one wash on `el`, resolving the dwell off its root so the number is
29
+ * single-sourced in CSS (`core/rungs.ts`).
29
30
  *
30
31
  * One property, both motion preferences: `--_qm-bloom-dwell` already shortens under
31
32
  * `prefers-reduced-motion` in the derivation, so what is left to `matchMedia` here is
@@ -36,7 +37,7 @@ function bloom(el) {
36
37
  if (typeof el.animate !== 'function')
37
38
  return undefined;
38
39
  const reduced = typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches;
39
- const raw = getComputedStyle(el).getPropertyValue('--_qm-bloom-dwell').trim();
40
+ const raw = rungStyle(el).getPropertyValue('--_qm-bloom-dwell').trim();
40
41
  const n = Number.parseFloat(raw);
41
42
  const duration = !Number.isFinite(n) ? FALLBACK_MS : raw.endsWith('ms') ? n : n * 1000;
42
43
  return el.animate(reduced ? FRAMES_REDUCED : FRAMES, { duration });
@@ -17,3 +17,20 @@ export declare function usvLength(s: string): number;
17
17
  export declare function renderContent(rt: Content, schema: Schema): Node;
18
18
  /** Decode a `Content` to a PM document under `schema`. */
19
19
  export declare function decode(rt: Content, schema: Schema): PMNode;
20
+ /** Whether `rt` decodes under an inline schema with nothing but whitespace dropped:
21
+ * upstream's `inline` rule, or one plain line and the empty one a trailing `\n`
22
+ * opens. That newline is the one a YAML `|` scalar keeps in a `plaintext` value,
23
+ * which upstream refuses (`trailingNewline`) and the inline decode drops. Marks are
24
+ * the schema's own business (`plaintextSchema` declares none). */
25
+ export declare function fitsInline(rt: Content): boolean;
26
+ /** Whether `rt` decodes under `plainSchema` losing nothing but its marks, which the
27
+ * next commit heals (CODEC §Inline mode): upstream's `isPlain`, asked of the content
28
+ * with them set aside. */
29
+ export declare function fitsPlain(rt: Content): boolean;
30
+ /** Whether `rt` decodes under the schema a leaf so declared mounts, losing nothing its
31
+ * first commit would store: {@link fitsInline} narrowed, {@link fitsPlain} plain, and
32
+ * anything on the block schema. What a leaf held over the rest reads. */
33
+ export declare function fitsLeaf(rt: Content, leaf: {
34
+ inline: boolean;
35
+ plaintext: boolean;
36
+ }): boolean;
@@ -3,14 +3,15 @@
3
3
  // nodes; a code fence's lines → one `code_block`), nest by shared `containers`
4
4
  // prefix (`list_item`/`quote` → lists/blockquote), select the block node by
5
5
  // `kind`, apply marks over their `[start,end)` USV ranges (PM splits inline nodes
6
- // at mark boundaries), and lower island slots to leaf nodes (inline in a `para`
7
- // line, block on an `island` line). Anchors are not applied here; they are
8
- // decorations (field.ts). Positions throughout are USV; `Array.from` iterates by
6
+ // at mark boundaries), and lower island slots to leaf nodes (block where a block
7
+ // island's slot stands alone on its line, inline otherwise). Anchors are not
8
+ // applied here; they are decorations (field.ts). Positions throughout are USV; `Array.from` iterates by
9
9
  // code point so an astral char is one unit, never a surrogate half.
10
10
  import { DOMSerializer } from 'prosemirror-model';
11
- import { ISLAND_SLOT } from './islands.js';
11
+ import { ISLAND_SLOT, isBlockIsland } from './islands.js';
12
12
  import { descriptorOf, markKey, pmMarkFromContent } from './marks.js';
13
- import { hasMarks, isInlineSchema, takesLineBreak } from './schema.js';
13
+ import { core } from '../lifecycle.js';
14
+ import { hasMarks, isBlockSchema, isInlineSchema, takesLineBreak } from './schema.js';
14
15
  /** Code points of `s` (USV units): the iteration granularity the content speaks. */
15
16
  export function codePoints(s) {
16
17
  return Array.from(s);
@@ -54,6 +55,9 @@ export function decode(rt, schema) {
54
55
  if (isInlineSchema(schema)) {
55
56
  return decodeInline(rt, schema, lineTexts, starts, marks, cursor);
56
57
  }
58
+ if (!isBlockSchema(schema)) {
59
+ return decodePlain(rt, schema, lineTexts, starts, marks, cursor);
60
+ }
57
61
  // Build leaves (fold `continues` runs), then nest by container prefix.
58
62
  const leaves = [];
59
63
  for (let i = 0; i < rt.lines.length; i++) {
@@ -69,13 +73,46 @@ export function decode(rt, schema) {
69
73
  const blocks = groupBlocks(schema, leaves, 0, marks, cursor);
70
74
  return schema.nodes.doc.create(null, blocks.length ? blocks : schema.nodes.paragraph.create());
71
75
  }
72
- /** Inline / plaintext decode: one paragraph, containers and islands stripped. */
76
+ /** Whether `rt` decodes under an inline schema with nothing but whitespace dropped:
77
+ * upstream's `inline` rule, or one plain line and the empty one a trailing `\n`
78
+ * opens. That newline is the one a YAML `|` scalar keeps in a `plaintext` value,
79
+ * which upstream refuses (`trailingNewline`) and the inline decode drops. Marks are
80
+ * the schema's own business (`plaintextSchema` declares none). */
81
+ export function fitsInline(rt) {
82
+ if (core().isInline(rt))
83
+ return true;
84
+ const plain = (line) => line.kind === 'para' && line.containers.length === 0;
85
+ return (rt.islands.length === 0 &&
86
+ rt.lines.length === 2 &&
87
+ rt.lines.every(plain) &&
88
+ !rt.lines[1].continues &&
89
+ rt.text.endsWith('\n'));
90
+ }
91
+ /** Whether `rt` decodes under `plainSchema` losing nothing but its marks, which the
92
+ * next commit heals (CODEC §Inline mode): upstream's `isPlain`, asked of the content
93
+ * with them set aside. */
94
+ export function fitsPlain(rt) {
95
+ return core().isPlain({ ...rt, marks: [] });
96
+ }
97
+ /** Whether `rt` decodes under the schema a leaf so declared mounts, losing nothing its
98
+ * first commit would store: {@link fitsInline} narrowed, {@link fitsPlain} plain, and
99
+ * anything on the block schema. What a leaf held over the rest reads. */
100
+ export function fitsLeaf(rt, leaf) {
101
+ if (leaf.inline)
102
+ return fitsInline(rt);
103
+ return !leaf.plaintext || fitsPlain(rt);
104
+ }
105
+ /** Inline decode: one paragraph, containers and islands stripped. */
73
106
  function decodeInline(rt, schema, lineTexts, starts, marks, cursor) {
74
107
  // An inline field is single-line; join any stray lines with a space (no
75
- // hard_break node exists in this schema). Islands are not representable inline:
76
- // the slot char is dropped and its entry skipped.
108
+ // hard_break node exists in this schema). The empty line a trailing `\n` opens is
109
+ // dropped rather than joined, so a first edit stores no space nobody typed. Islands
110
+ // are not representable inline: the slot char is dropped and its entry skipped.
77
111
  const inline = [];
112
+ const last = lineTexts.length - 1;
78
113
  for (let i = 0; i < lineTexts.length; i++) {
114
+ if (i > 0 && i === last && lineTexts[i] === '')
115
+ break;
79
116
  if (i > 0)
80
117
  inline.push(schema.text(' '));
81
118
  inline.push(...buildInline(schema, lineTexts[i], starts[i], marks, cursor, true));
@@ -83,6 +120,22 @@ function decodeInline(rt, schema, lineTexts, starts, marks, cursor) {
83
120
  const para = schema.nodes.paragraph.create(null, inline);
84
121
  return schema.nodes.doc.create(null, para);
85
122
  }
123
+ /** Plain decode: a `continues` line is a `hard_break` in the paragraph above and every
124
+ * other line opens one. A line's kind and containers and every island have no node
125
+ * here and are dropped, so a content {@link fitsPlain} refuses opens flattened. */
126
+ function decodePlain(rt, schema, lineTexts, starts, marks, cursor) {
127
+ const paras = [];
128
+ for (let i = 0; i < rt.lines.length; i++) {
129
+ const inline = buildInline(schema, lineTexts[i] ?? '', starts[i], marks, cursor, true);
130
+ const open = paras[paras.length - 1];
131
+ if (rt.lines[i].continues && open)
132
+ open.push(schema.nodes.hard_break.create(), ...inline);
133
+ else
134
+ paras.push(inline);
135
+ }
136
+ const blocks = paras.map((inline) => schema.nodes.paragraph.create(null, inline));
137
+ return schema.nodes.doc.create(null, blocks.length ? blocks : schema.nodes.paragraph.create());
138
+ }
86
139
  /** Nest a run of leaves that share a `depth`-length container prefix into blocks. */
87
140
  function groupBlocks(schema, leaves, depth, marks, cursor) {
88
141
  const out = [];
@@ -177,10 +230,6 @@ function makeLeaf(schema, leaf, marks, cursor) {
177
230
  switch (line.kind) {
178
231
  case 'rule':
179
232
  return schema.nodes.horizontal_rule.create();
180
- case 'island': {
181
- const attrs = islandAttrs(cursor);
182
- return attrs ? schema.nodes.island_block.create(attrs) : schema.nodes.paragraph.create();
183
- }
184
233
  case 'code': {
185
234
  // One code_block: the segments' texts joined by literal `\n`, no marks.
186
235
  const text = leaf.segments.map((s) => s.text).join('\n');
@@ -189,6 +238,8 @@ function makeLeaf(schema, leaf, marks, cursor) {
189
238
  }
190
239
  case 'para':
191
240
  case 'heading': {
241
+ if (line.kind === 'para' && blockIslandLine(leaf, cursor))
242
+ return schema.nodes.island_block.create(islandAttrs(cursor));
192
243
  // Inline content, the segment boundary a `hard_break` in a paragraph and a space
193
244
  // in a heading, which takes none (`schema.ts` §`takesLineBreak`). A stored heading
194
245
  // carries no continuation — the store clears one after a block of a single line —
@@ -210,15 +261,23 @@ function makeLeaf(schema, leaf, marks, cursor) {
210
261
  }
211
262
  }
212
263
  }
213
- /** Consume the next island entry as PM node attrs (text-order matched to slots):
214
- * the whole entry, `loss` included, which an island edit has to write back
215
- * (`islands.ts`). `null` where a slot has no entry behind it, which is a malformed
216
- * content: the slot draws an empty paragraph or nothing at all, so the decode stays
217
- * total without minting a node whose `type` the vocabulary would refuse on the way
218
- * back. */
264
+ /** Whether a `para` leaf is one block island: a single line holding its slot alone,
265
+ * backed by an island whose markup is a block. The island is the one fact; the line
266
+ * carries no kind of its own for it. */
267
+ function blockIslandLine(leaf, cursor) {
268
+ if (leaf.segments.length !== 1 || leaf.segments[0].text !== ISLAND_SLOT)
269
+ return false;
270
+ const isl = cursor.rt.islands[cursor.i];
271
+ return isl != null && isBlockIsland(isl.type);
272
+ }
273
+ /** Consume the next island entry as PM node attrs (text-order matched to slots): the
274
+ * whole entry, which an island edit has to write back (`islands.ts`). `null` where a
275
+ * slot has no entry behind it, which is a malformed content: the slot draws nothing
276
+ * and its line an empty paragraph, so the decode stays total without minting a node
277
+ * whose `type` the vocabulary would refuse on the way back. */
219
278
  function islandAttrs(cursor) {
220
279
  const isl = cursor.rt.islands[cursor.i++];
221
- return isl ? { id: isl.id, islandType: isl.type, props: isl.props, loss: isl.loss } : null;
280
+ return isl ? { id: isl.id, islandType: isl.type, props: isl.props } : null;
222
281
  }
223
282
  /**
224
283
  * Inline nodes for one segment: split text into maximal runs of a constant mark
@@ -104,7 +104,7 @@ function scanBlock(acc, node, nodePos, containers, instance) {
104
104
  acc.lastContentEndPm = nodePos + 1;
105
105
  break;
106
106
  case 'island_block':
107
- beginLine(acc, nodePos, containers, { kind: 'island' });
107
+ beginLine(acc, nodePos, containers, { kind: 'para' });
108
108
  acc.runs.push({ kind: 'atom', pmStart: nodePos, usvStart: acc.usvEnd });
109
109
  appendText(acc, ISLAND_SLOT);
110
110
  acc.islands.push(islandEntryFromNode(node.attrs));
@@ -1,4 +1,4 @@
1
- import type { Schema } from 'prosemirror-model';
1
+ import { type Schema } from 'prosemirror-model';
2
2
  import { Plugin } from 'prosemirror-state';
3
3
  import { EditorView } from 'prosemirror-view';
4
4
  import type { Document, Content, Addr, Quill } from '@quillmark/wasm';
@@ -14,11 +14,24 @@ export interface CreateFieldOpts {
14
14
  quill: Quill;
15
15
  addr: Addr;
16
16
  container: HTMLElement;
17
- /** Constrained single-textblock schema (a `richtext(inline)` field). */
17
+ /** Constrained single-textblock schema (a field declaring `inline`). */
18
18
  inline?: boolean;
19
- /** The mark-free inline schema (a `plaintext` field): literal text, no formatting
20
- * and no anchors. Implies `inline`. */
19
+ /** A mark-free schema (a `plaintext` field): literal text, no formatting and no
20
+ * anchors, in paragraphs and hard breaks or, with `inline`, one textblock. */
21
21
  plaintext?: boolean;
22
+ /** The id of the note the caller draws inside `container` for a held leaf →
23
+ * `aria-describedby` while it holds. */
24
+ heldNoteId?: string;
25
+ /**
26
+ * Fired when the leaf holds or releases, and at mount when it mounts held. A leaf
27
+ * declaring `inline`, or a `plaintext` one, is held over a value its schema cannot
28
+ * hold (`fitsLeaf`, upstream's `isInline` and `isPlain`): the decode would join lines
29
+ * and drop containers and islands for the first commit to store. A held leaf draws
30
+ * its content on the block schema, read-only, and commits nothing. Judged of the value
31
+ * each mount and re-hydrate reads, so {@link FieldController.applyExternal} is what
32
+ * re-evaluates it.
33
+ */
34
+ onHold?(held: boolean): void;
22
35
  /** Suppress the markdown-shorthand input rules. */
23
36
  noInputRules?: boolean;
24
37
  /** The island chrome's wording, read live (per render) so a consumer swapping
@@ -46,11 +59,28 @@ export interface CreateFieldOpts {
46
59
  labelledBy?: string;
47
60
  /** The parked `description` → `aria-describedby`; announced after the name. */
48
61
  describedBy?: string;
49
- /** Ghost text shown on the empty leaf: the resolved `default:` a field ghosts,
50
- * or a body's invitation. The initial value; {@link
51
- * FieldController.setPlaceholder} moves it after mount. Empty/absent
52
- * shows no ghost. */
62
+ /**
63
+ * The resolved `default:` where it prints: the content an unset field's leaf holds,
64
+ * marked `data-default` on the editable for the default rung. Nothing is written
65
+ * until an edit, which commits the whole leaf as authored: the default taken, with
66
+ * the edit in it. The initial value; {@link FieldController.setFallback} moves it
67
+ * after mount.
68
+ */
69
+ fallback?: Content;
70
+ /** Ghost text shown on the empty leaf: the `none` an unset optional field prints, or
71
+ * a body's invitation. The initial value; {@link FieldController.setPlaceholder}
72
+ * moves it after mount. Empty/absent shows no ghost. */
53
73
  placeholder?: string;
74
+ /** The placeholder is what the unset field prints, so it goes at the leaf's first
75
+ * edit: the edit answers the field, and emptied the leaf holds an empty answer, which
76
+ * prints empty. Absent, the placeholder is an invitation (a body's) and returns
77
+ * whenever the leaf is empty. */
78
+ placeholderUntilEdit?: boolean;
79
+ /** Ghost text shown on the empty leaf until its first edit: an unset field's
80
+ * `example:`, at rest where there is no placeholder and in the placeholder's stead
81
+ * while the leaf holds the focus. The initial value; {@link FieldController.setExample}
82
+ * moves it after mount. */
83
+ example?: string;
54
84
  onFocus?(addr: Addr): void;
55
85
  /** Fired with the new USV caret after an edit or a selection move. */
56
86
  onCaretMove?(addr: Addr, pos: number): void;
@@ -60,7 +90,8 @@ export interface CreateFieldOpts {
60
90
  * move, so a host driving a recompile off it recompiles on every arrow key.
61
91
  *
62
92
  * Fires for every commit regardless of which branch it took (`applyChange`, the
63
- * first-edit `overwrite`, the fallback `overwrite`) and for an anchor mutation,
93
+ * first-edit `overwrite`, the fallback `overwrite`, a `plaintext` leaf's typed
94
+ * write) and for an anchor mutation,
64
95
  * which changes no text. A commit that failed outright (`commit-lost`) does not
65
96
  * fire it: nothing landed.
66
97
  */
@@ -75,7 +106,8 @@ export interface FieldController {
75
106
  readonly el: HTMLElement;
76
107
  /** Place the caret at USV `pos` (preview onPick → usvToPM → here). */
77
108
  setCaret(pos: number): void;
78
- /** External content change → re-hydrate this leaf (gated by reconcile). */
109
+ /** External content change → re-hydrate this leaf (gated by reconcile), and
110
+ * re-evaluate its hold against the value now stored. */
79
111
  applyExternal(): void;
80
112
  /**
81
113
  * Move the empty-leaf ghost after mount: a card retyped to another kind takes
@@ -86,6 +118,12 @@ export interface FieldController {
86
118
  * a transaction would fire `onCaretMove` at a moment the caret did not move.
87
119
  */
88
120
  setPlaceholder(text: string | undefined): void;
121
+ /** Move the example ghost after mount, the way {@link setPlaceholder} moves the
122
+ * other one. */
123
+ setExample(text: string | undefined): void;
124
+ /** Move the default an unset leaf holds after mount: a retype or a quill swap
125
+ * re-seats an unset leaf on the new kind's default, and leaves a written one be. */
126
+ setFallback(content: Content | undefined): void;
89
127
  focus(): void;
90
128
  /** The current stored content for this addr (for tests / reconcile). */
91
129
  getContent(): Content;
@@ -146,10 +184,17 @@ export declare function proseAttributes(opts: {
146
184
  labelledBy?: string;
147
185
  describedBy?: string;
148
186
  }): Record<string, string> | undefined;
187
+ /** A held leaf's attributes, described by its note as well. A non-editable view is no
188
+ * textbox to assistive tech or to Tab, so a held leaf states both itself. */
189
+ export declare function heldAttributes(opts: {
190
+ label?: string;
191
+ labelledBy?: string;
192
+ describedBy?: string;
193
+ }, noteId: string | undefined): Record<string, string>;
149
194
  export declare function createField(opts: CreateFieldOpts): FieldController;
150
195
  /**
151
196
  * The prose-leaf plugin stack (VISUAL_EDITOR §Surface): shared by
152
- * {@link createField} and the by-value inline editor (`ProseValue`), so the two
197
+ * {@link createField} and the by-value editor (`ProseValue`), so the two
153
198
  * never fork the keymap/plugin ordering. History first, then any leaf-specific
154
199
  * plugins (`afterHistory`: the addressed leaf passes its anchor-position plugin;
155
200
  * a by-value leaf passes none), then {@link linebreakPlugin}, which normalizes what
@@ -158,10 +203,10 @@ export declare function createField(opts: CreateFieldOpts): FieldController;
158
203
  * shapes: the gap cursor, {@link pastAtomPlugin}, and the island paste pass
159
204
  * (`islands.ts`).
160
205
  *
161
- * Every mark-shaped plugin reads the schema rather than a flag: over
162
- * `plaintextSchema` the shorthand rules build nothing (each is guarded on its mark
163
- * type) and the toggles bind no key, so a `**bold**` keeps the delimiters its author
164
- * typed without a second rule saying so.
206
+ * Every mark-shaped plugin reads the schema rather than a flag: over either plaintext
207
+ * schema the shorthand rules build nothing (each is guarded on its mark type) and the
208
+ * toggles bind no key, so a `**bold**` keeps the delimiters its author typed without
209
+ * a second rule saying so.
165
210
  */
166
211
  export declare function proseLeafPlugins(schema: Schema, opts: {
167
212
  inline: boolean;
@@ -173,5 +218,9 @@ export declare function proseLeafPlugins(schema: Schema, opts: {
173
218
  * ({@link FieldController.setPlaceholder}), and a re-hydration rebuilds this
174
219
  * stack, so the plugin must ask rather than hold. */
175
220
  placeholder?: () => string | undefined;
221
+ /** {@link CreateFieldOpts.placeholderUntilEdit}. */
222
+ placeholderUntilEdit?: boolean;
223
+ /** The focused leaf's ghost, read live for the same reason. */
224
+ example?: () => string | undefined;
176
225
  afterHistory?: Plugin[];
177
226
  }): Plugin[];