@quillmark/svelte 0.12.0 → 0.14.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 (46) hide show
  1. package/NOTICE +3 -2
  2. package/dist/core/codec/decode.d.ts +17 -0
  3. package/dist/core/codec/decode.js +57 -4
  4. package/dist/core/codec/field.d.ts +63 -15
  5. package/dist/core/codec/field.js +201 -65
  6. package/dist/core/codec/index.d.ts +3 -3
  7. package/dist/core/codec/index.js +3 -3
  8. package/dist/core/codec/keymap.d.ts +5 -4
  9. package/dist/core/codec/keymap.js +5 -4
  10. package/dist/core/codec/prose.css +42 -0
  11. package/dist/core/codec/schema.d.ts +24 -5
  12. package/dist/core/codec/schema.js +40 -9
  13. package/dist/core/errors.d.ts +3 -2
  14. package/dist/core/theme.css +9 -6
  15. package/dist/preview/paint.js +21 -17
  16. package/dist/visual/ArrayField.svelte +66 -22
  17. package/dist/visual/ArrayField.svelte.d.ts +2 -0
  18. package/dist/visual/BooleanField.svelte +28 -6
  19. package/dist/visual/BooleanField.svelte.d.ts +2 -0
  20. package/dist/visual/Card.svelte +4 -3
  21. package/dist/visual/DateField.svelte +158 -40
  22. package/dist/visual/DateField.svelte.d.ts +5 -2
  23. package/dist/visual/EnumField.svelte +27 -9
  24. package/dist/visual/EnumField.svelte.d.ts +5 -0
  25. package/dist/visual/Field.svelte +48 -11
  26. package/dist/visual/FormatPopover.svelte +4 -3
  27. package/dist/visual/MatrixField.svelte +3 -2
  28. package/dist/visual/NumberField.svelte +73 -29
  29. package/dist/visual/NumberField.svelte.d.ts +5 -1
  30. package/dist/visual/ObjectField.svelte +63 -15
  31. package/dist/visual/ProseField.svelte +53 -23
  32. package/dist/visual/ProseField.svelte.d.ts +15 -3
  33. package/dist/visual/ProseValue.svelte +102 -46
  34. package/dist/visual/ProseValue.svelte.d.ts +17 -5
  35. package/dist/visual/TextField.svelte +35 -18
  36. package/dist/visual/TextField.svelte.d.ts +7 -0
  37. package/dist/visual/VariantField.svelte +1 -0
  38. package/dist/visual/VisualEditorInner.svelte +24 -24
  39. package/dist/visual/controls.css +16 -5
  40. package/dist/visual/icons/nodes.d.ts +28 -0
  41. package/dist/visual/icons/nodes.js +14 -1
  42. package/dist/visual/strings.d.ts +11 -1
  43. package/dist/visual/strings.js +3 -0
  44. package/dist/visual/structure.d.ts +77 -43
  45. package/dist/visual/structure.js +138 -64
  46. package/package.json +2 -2
package/NOTICE CHANGED
@@ -6,9 +6,10 @@ Copyright 2026 Nibs
6
6
  This package contains SVG glyph geometry derived from Lucide, carried in source
7
7
  rather than installed.
8
8
 
9
- `visual/icons/nodes.ts` holds sixteen verbatim from @lucide/svelte 1.28.0:
9
+ `visual/icons/nodes.ts` holds nineteen verbatim from @lucide/svelte 1.28.0:
10
10
  chevron-right, arrow-right, chevron-down, chevron-up, bold, italic, underline,
11
- strikethrough, code, link, hash, info, plus, x, trash-2 and minus.
11
+ strikethrough, code, link, hash, info, plus, x, trash-2, minus, check, calendar
12
+ and calendar-check.
12
13
 
13
14
  `core/codec/table-view.ts` holds a plus and four text-alignment glyphs derived
14
15
  from an earlier release of the same project.
@@ -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;
@@ -10,7 +10,8 @@
10
10
  import { DOMSerializer } from 'prosemirror-model';
11
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 = [];
@@ -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,27 @@ 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 goes at the leaf's first edit: the edit answers the leaf, and
75
+ * emptied it holds an empty answer, which prints empty. Absent, the placeholder
76
+ * returns whenever the leaf is empty. */
77
+ placeholderUntilEdit?: boolean;
78
+ /** Ghost text shown on the empty leaf until its first edit: an unset field's
79
+ * `example:`, at rest where there is no placeholder and in the placeholder's stead
80
+ * while the leaf holds the focus. The initial value; {@link FieldController.setExample}
81
+ * moves it after mount. */
82
+ example?: string;
54
83
  onFocus?(addr: Addr): void;
55
84
  /** Fired with the new USV caret after an edit or a selection move. */
56
85
  onCaretMove?(addr: Addr, pos: number): void;
@@ -60,7 +89,8 @@ export interface CreateFieldOpts {
60
89
  * move, so a host driving a recompile off it recompiles on every arrow key.
61
90
  *
62
91
  * Fires for every commit regardless of which branch it took (`applyChange`, the
63
- * first-edit `overwrite`, the fallback `overwrite`) and for an anchor mutation,
92
+ * first-edit `overwrite`, the fallback `overwrite`, a `plaintext` leaf's typed
93
+ * write) and for an anchor mutation,
64
94
  * which changes no text. A commit that failed outright (`commit-lost`) does not
65
95
  * fire it: nothing landed.
66
96
  */
@@ -75,7 +105,8 @@ export interface FieldController {
75
105
  readonly el: HTMLElement;
76
106
  /** Place the caret at USV `pos` (preview onPick → usvToPM → here). */
77
107
  setCaret(pos: number): void;
78
- /** External content change → re-hydrate this leaf (gated by reconcile). */
108
+ /** External content change → re-hydrate this leaf (gated by reconcile), and
109
+ * re-evaluate its hold against the value now stored. */
79
110
  applyExternal(): void;
80
111
  /**
81
112
  * Move the empty-leaf ghost after mount: a card retyped to another kind takes
@@ -86,6 +117,12 @@ export interface FieldController {
86
117
  * a transaction would fire `onCaretMove` at a moment the caret did not move.
87
118
  */
88
119
  setPlaceholder(text: string | undefined): void;
120
+ /** Move the example ghost after mount, the way {@link setPlaceholder} moves the
121
+ * other one. */
122
+ setExample(text: string | undefined): void;
123
+ /** Move the default an unset leaf holds after mount: a retype or a quill swap
124
+ * re-seats an unset leaf on the new kind's default, and leaves a written one be. */
125
+ setFallback(content: Content | undefined): void;
89
126
  focus(): void;
90
127
  /** The current stored content for this addr (for tests / reconcile). */
91
128
  getContent(): Content;
@@ -146,10 +183,17 @@ export declare function proseAttributes(opts: {
146
183
  labelledBy?: string;
147
184
  describedBy?: string;
148
185
  }): Record<string, string> | undefined;
186
+ /** A held leaf's attributes, described by its note as well. A non-editable view is no
187
+ * textbox to assistive tech or to Tab, so a held leaf states both itself. */
188
+ export declare function heldAttributes(opts: {
189
+ label?: string;
190
+ labelledBy?: string;
191
+ describedBy?: string;
192
+ }, noteId: string | undefined): Record<string, string>;
149
193
  export declare function createField(opts: CreateFieldOpts): FieldController;
150
194
  /**
151
195
  * The prose-leaf plugin stack (VISUAL_EDITOR §Surface): shared by
152
- * {@link createField} and the by-value inline editor (`ProseValue`), so the two
196
+ * {@link createField} and the by-value editor (`ProseValue`), so the two
153
197
  * never fork the keymap/plugin ordering. History first, then any leaf-specific
154
198
  * plugins (`afterHistory`: the addressed leaf passes its anchor-position plugin;
155
199
  * a by-value leaf passes none), then {@link linebreakPlugin}, which normalizes what
@@ -158,10 +202,10 @@ export declare function createField(opts: CreateFieldOpts): FieldController;
158
202
  * shapes: the gap cursor, {@link pastAtomPlugin}, and the island paste pass
159
203
  * (`islands.ts`).
160
204
  *
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.
205
+ * Every mark-shaped plugin reads the schema rather than a flag: over either plaintext
206
+ * schema the shorthand rules build nothing (each is guarded on its mark type) and the
207
+ * toggles bind no key, so a `**bold**` keeps the delimiters its author typed without
208
+ * a second rule saying so.
165
209
  */
166
210
  export declare function proseLeafPlugins(schema: Schema, opts: {
167
211
  inline: boolean;
@@ -173,5 +217,9 @@ export declare function proseLeafPlugins(schema: Schema, opts: {
173
217
  * ({@link FieldController.setPlaceholder}), and a re-hydration rebuilds this
174
218
  * stack, so the plugin must ask rather than hold. */
175
219
  placeholder?: () => string | undefined;
220
+ /** {@link CreateFieldOpts.placeholderUntilEdit}. */
221
+ placeholderUntilEdit?: boolean;
222
+ /** The focused leaf's ghost, read live for the same reason. */
223
+ example?: () => string | undefined;
176
224
  afterHistory?: Plugin[];
177
225
  }): Plugin[];