@quillmark/svelte 0.22.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.
@@ -0,0 +1,21 @@
1
+ import type { Node as PMNode } from 'prosemirror-model';
2
+ import { Plugin } from 'prosemirror-state';
3
+ /** A leaf's character cap: the schema's `max:`, and whether the count drops trailing
4
+ * whitespace, which a one-line `text` does and nothing else. */
5
+ export interface TextCap {
6
+ max: number;
7
+ trimEnd?: boolean;
8
+ /** The count, at mount and after every change to the document. */
9
+ onCount?: (count: number) => void;
10
+ }
11
+ /** The characters `doc` holds as `FieldSchema::char_count` counts them: Unicode scalar
12
+ * values of the plain projection, islands counting nothing and trailing line breaks
13
+ * none. */
14
+ export declare function capCount(doc: PMNode, trimEnd?: boolean): number;
15
+ /**
16
+ * Refuses a transaction leaving the leaf past `max`, so a keystroke at the cap lands
17
+ * nothing, and cuts a paste to what fits. Past the cap, a transaction passes where it
18
+ * leaves the count no higher, so a value stored over the cap still edits down; undo and
19
+ * redo pass, each restoring a state the leaf already held.
20
+ */
21
+ export declare function capPlugin(cap: TextCap): Plugin;
@@ -0,0 +1,63 @@
1
+ import { isHistoryTransaction } from 'prosemirror-history';
2
+ import { Plugin } from 'prosemirror-state';
3
+ import { usvLength } from './decode.js';
4
+ import { scanDoc } from './encode.js';
5
+ import { ISLAND_SLOT } from './islands.js';
6
+ /** The characters `doc` holds as `FieldSchema::char_count` counts them: Unicode scalar
7
+ * values of the plain projection, islands counting nothing and trailing line breaks
8
+ * none. */
9
+ export function capCount(doc, trimEnd = false) {
10
+ const text = scanDoc(doc).text.replaceAll(ISLAND_SLOT, '');
11
+ return usvLength(trimEnd ? text.trimEnd() : text.replace(/[\n\r]+$/, ''));
12
+ }
13
+ /**
14
+ * Refuses a transaction leaving the leaf past `max`, so a keystroke at the cap lands
15
+ * nothing, and cuts a paste to what fits. Past the cap, a transaction passes where it
16
+ * leaves the count no higher, so a value stored over the cap still edits down; undo and
17
+ * redo pass, each restoring a state the leaf already held.
18
+ */
19
+ export function capPlugin(cap) {
20
+ const count = (doc) => capCount(doc, cap.trimEnd);
21
+ return new Plugin({
22
+ filterTransaction: (tr) => {
23
+ if (!tr.docChanged || isHistoryTransaction(tr))
24
+ return true;
25
+ const after = count(tr.doc);
26
+ return after <= cap.max || after <= count(tr.before);
27
+ },
28
+ props: {
29
+ handlePaste: (view, _event, slice) => {
30
+ const { state } = view;
31
+ const pasted = () => state.tr.replaceSelection(slice);
32
+ if (count(pasted().doc) <= cap.max)
33
+ return false;
34
+ const tr = pasted();
35
+ const start = tr.mapping.map(state.selection.from, -1);
36
+ const end = tr.mapping.map(state.selection.to, 1);
37
+ const fits = (cut) => count(pasted().delete(cut, end).doc) <= cap.max;
38
+ if (!fits(start))
39
+ return true;
40
+ let lo = start;
41
+ let hi = end;
42
+ while (hi - lo > 1) {
43
+ const mid = (lo + hi) >> 1;
44
+ if (fits(mid))
45
+ lo = mid;
46
+ else
47
+ hi = mid;
48
+ }
49
+ view.dispatch(tr.delete(lo, end).scrollIntoView().setMeta('uiEvent', 'paste'));
50
+ return true;
51
+ }
52
+ },
53
+ view: (v) => {
54
+ cap.onCount?.(count(v.state.doc));
55
+ return {
56
+ update: (next, prev) => {
57
+ if (next.state.doc !== prev.doc)
58
+ cap.onCount?.(count(next.state.doc));
59
+ }
60
+ };
61
+ }
62
+ });
63
+ }
@@ -3,6 +3,7 @@ import { Plugin } from 'prosemirror-state';
3
3
  import { EditorView } from 'prosemirror-view';
4
4
  import type { Document, Content, Addr, Quill } from '@quillmark/wasm';
5
5
  import type { EditorErrorHandler } from '../errors.js';
6
+ import { type TextCap } from './cap.js';
6
7
  import { type TableChromeStrings } from './table-view.js';
7
8
  import { type SlashState, type SlashStrings } from './slash.js';
8
9
  /** Options for {@link createField}. */
@@ -75,6 +76,9 @@ export interface CreateFieldOpts {
75
76
  * a body's invitation. The initial value; {@link FieldController.setPlaceholder}
76
77
  * moves it after mount. Empty/absent shows no ghost. */
77
78
  placeholder?: string;
79
+ /** The schema's `max:` on the field: a character count no edit takes the leaf past
80
+ * (`cap.ts`). */
81
+ cap?: TextCap;
78
82
  /** The placeholder goes at the leaf's first edit: the edit answers the leaf, and
79
83
  * emptied it holds an empty answer, which prints empty. Absent, the placeholder
80
84
  * returns whenever the leaf is empty. */
@@ -224,5 +228,8 @@ export declare function proseLeafPlugins(schema: Schema, opts: {
224
228
  placeholder?: () => string | undefined;
225
229
  /** {@link CreateFieldOpts.placeholderUntilEdit}. */
226
230
  placeholderUntilEdit?: boolean;
231
+ /** {@link CreateFieldOpts.cap}. Ahead of every other paste handler, which a paste
232
+ * it cuts never reaches. */
233
+ cap?: TextCap;
227
234
  afterHistory?: Plugin[];
228
235
  }): Plugin[];
@@ -24,6 +24,7 @@ import { reportError, errorMessage } from '../errors.js';
24
24
  import { decode, fitsLeaf } from './decode.js';
25
25
  import { usvToPM, pmToUsv, buildLineIndex, lineIndexOf } from './positions.js';
26
26
  import { lower, pmToContent, scanContent, scanDoc, contentEdit } from './encode.js';
27
+ import { capPlugin } from './cap.js';
27
28
  import { islandPastePlugin } from './islands.js';
28
29
  import { anchorsFromContent } from './marks.js';
29
30
  import { contentEqual, createReconciler } from './reconcile.js';
@@ -282,6 +283,7 @@ export function createField(opts) {
282
283
  // given one later; the plugin draws nothing while the text is empty.
283
284
  placeholder: () => placeholderText,
284
285
  placeholderUntilEdit: opts.placeholderUntilEdit,
286
+ cap: opts.cap,
285
287
  afterHistory: [anchorPlugin(seededAnchors)]
286
288
  })
287
289
  });
@@ -547,7 +549,12 @@ export function createField(opts) {
547
549
  * its author typed without a second rule saying so.
548
550
  */
549
551
  export function proseLeafPlugins(schema, opts) {
550
- const list = [history(), ...(opts.afterHistory ?? []), linebreakPlugin(schema)];
552
+ const list = [
553
+ history(),
554
+ ...(opts.cap ? [capPlugin(opts.cap)] : []),
555
+ ...(opts.afterHistory ?? []),
556
+ linebreakPlugin(schema)
557
+ ];
551
558
  if (opts.slash)
552
559
  list.push(slashPlugin(opts.slash, opts.slashHint));
553
560
  if (!opts.noInputRules)
@@ -2,6 +2,8 @@ export { rangeAnchor } from './anchor.js';
2
2
  export type { RangeAnchor } from './anchor.js';
3
3
  export { createField, emptyContent, heldAttributes, isEmptyContent, proseAttributes, proseLeafPlugins } from './field.js';
4
4
  export type { CreateFieldOpts, FieldController, LeafViews } from './field.js';
5
+ export { capCount } from './cap.js';
6
+ export type { TextCap } from './cap.js';
5
7
  export { DEFAULT_TABLE_STRINGS } from './table-view.js';
6
8
  export type { TableChromeStrings } from './table-view.js';
7
9
  export { DEFAULT_SLASH_STRINGS } from './slash.js';
@@ -14,6 +14,7 @@ import './prose.css';
14
14
  export { rangeAnchor } from './anchor.js';
15
15
  // The prose leaf.
16
16
  export { createField, emptyContent, heldAttributes, isEmptyContent, proseAttributes, proseLeafPlugins } from './field.js';
17
+ export { capCount } from './cap.js';
17
18
  // The table island's chrome vocabulary: the visual tier's `strings` set extends it,
18
19
  // so the island's wording is overridden beside every other key.
19
20
  export { DEFAULT_TABLE_STRINGS } from './table-view.js';
@@ -74,6 +74,7 @@
74
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';
@@ -800,6 +801,7 @@
800
801
  content={() => readAt([k])}
801
802
  value={arr[k]}
802
803
  plaintext={items != null && literalText(items)}
804
+ cap={textCap(items)}
803
805
  label={rowName(k)}
804
806
  onChange={(rt) => commitElement(k, rt)}
805
807
  onKey={(e) => onElementKey(e, k)}
@@ -40,6 +40,7 @@
40
40
  printedText,
41
41
  renderedBoolean,
42
42
  stringifyGhost,
43
+ textCap,
43
44
  titleText
44
45
  } from './structure.js';
45
46
  import { wording } from './strings.js';
@@ -163,6 +164,14 @@
163
164
  // reference must vanish with it: `aria-describedby` pointing at nothing describes
164
165
  // nothing, and silently.
165
166
  const describedBy = $derived(field.description ? domIds.description : undefined);
167
+ // A capped prose leaf reports its count, and the count describes it too: a keystroke
168
+ // the cap refuses changes nothing else a reader can perceive.
169
+ let chars = $state<number>();
170
+ const cap = $derived(field.control === 'prose' ? textCap(field.schema) : undefined);
171
+ const countId = $derived(`${domIds.control}-count`);
172
+ const proseDescribedBy = $derived(
173
+ cap ? [describedBy, countId].filter(Boolean).join(' ') : describedBy
174
+ );
166
175
  // Two controls own their label row, the add chip and the held count sharing it.
167
176
  const ownsLabel = $derived(field.control === 'array' || field.control === 'matrix');
168
177
  const flat = $derived(flatObject(field.schema));
@@ -285,6 +294,8 @@
285
294
  descriptionId={domIds.description}
286
295
  {onActivate}
287
296
  description={field.description}
297
+ count={cap && chars != null ? t.strings.textCount(chars, cap.max) : undefined}
298
+ {countId}
288
299
  />
289
300
  {/if}
290
301
  <!-- The control and what hangs under it, in one grid cell: the diagnostics stack
@@ -305,7 +316,8 @@
305
316
  placeholder={none}
306
317
  emptyUnsets={optional}
307
318
  labelledBy={domIds.label}
308
- {describedBy}
319
+ describedBy={proseDescribedBy}
320
+ cap={cap && { ...cap, onCount: (n) => (chars = n) }}
309
321
  {leafKey}
310
322
  {onFocus}
311
323
  {onCaretMove}
@@ -40,8 +40,13 @@
40
40
  onActivate?: () => void;
41
41
  /** Schema `description`, or undefined: the affordance renders only when set. */
42
42
  description?: string;
43
+ /** A capped field's character count, drawn at the row's end. */
44
+ count?: string;
45
+ /** The count's own id, for the control's `aria-describedby`. */
46
+ countId?: string;
43
47
  }
44
- let { label, controlId, id, descriptionId, onActivate, description }: Props = $props();
48
+ let { label, controlId, id, descriptionId, onActivate, description, count, countId }: Props =
49
+ $props();
45
50
  </script>
46
51
 
47
52
  <div class="qm-field-label-row">
@@ -68,6 +73,9 @@
68
73
  <FieldHint {description} {label} describedBy={descriptionId} />
69
74
  <span class="qm-visually-hidden" id={descriptionId}>{description}</span>
70
75
  {/if}
76
+ {#if count != null}
77
+ <span class="qm-field-count" id={countId}>{count}</span>
78
+ {/if}
71
79
  </div>
72
80
 
73
81
  <style>
@@ -91,6 +99,12 @@
91
99
  color: var(--_qm-ink-label);
92
100
  min-width: 0;
93
101
  }
102
+ .qm-field-count {
103
+ margin-inline-start: auto;
104
+ font-size: var(--_qm-text-label);
105
+ color: var(--_qm-ink-label);
106
+ font-variant-numeric: tabular-nums;
107
+ }
94
108
  /* A label that lands focus says so. Only when it has somewhere to land: an
95
109
  `ObjectField` property's label names a subform, not one control. */
96
110
  .qm-field-label.targeted {
@@ -11,6 +11,10 @@ interface Props {
11
11
  onActivate?: () => void;
12
12
  /** Schema `description`, or undefined: the affordance renders only when set. */
13
13
  description?: string;
14
+ /** A capped field's character count, drawn at the row's end. */
15
+ count?: string;
16
+ /** The count's own id, for the control's `aria-describedby`. */
17
+ countId?: string;
14
18
  }
15
19
  declare const FieldLabel: import("svelte").Component<Props, {}, "">;
16
20
  type FieldLabel = ReturnType<typeof FieldLabel>;
@@ -51,7 +51,8 @@
51
51
  placeFields,
52
52
  printedText,
53
53
  renderedBoolean,
54
- shortCell
54
+ shortCell,
55
+ textCap
55
56
  } from './structure.js';
56
57
  import { splitDeep, unrouted, type DeepDiagnostic } from './diagnostics.js';
57
58
  import type { LandingBox } from './leaves.js';
@@ -136,6 +137,8 @@
136
137
  }: Props = $props();
137
138
 
138
139
  const t = wording();
140
+ /** A capped prose cell's character count, by property. */
141
+ const counts: Record<string, number> = $state({});
139
142
  const entries = $derived(Object.entries(properties ?? {}));
140
143
  const obj = $derived((value ?? {}) as Record<string, unknown>);
141
144
  const has = (key: string): boolean => Object.hasOwn(properties ?? {}, key);
@@ -298,6 +301,8 @@
298
301
  (`packable`). A block prose cell spans it too, and keeps its label row, as
299
302
  does a placed cell `placeFields` gives the whole row. -->
300
303
  {@const block = kind === 'prose' && !shortCell(sub)}
304
+ {@const cap = kind === 'prose' ? textCap(sub) : undefined}
305
+ {@const countId = cap && ids ? `${ids.control}-count` : undefined}
301
306
  <div
302
307
  class={[
303
308
  'qm-object-prop',
@@ -316,6 +321,10 @@
316
321
  descriptionId={describes}
317
322
  onActivate={labelable ? undefined : () => focusProp(key)}
318
323
  description={sub.description}
324
+ count={cap && counts[key] != null
325
+ ? t.strings.textCount(counts[key], cap.max)
326
+ : undefined}
327
+ {countId}
319
328
  />
320
329
  {/if}
321
330
  {#if kind === 'enum'}
@@ -385,7 +394,8 @@
385
394
  {block}
386
395
  label={named}
387
396
  labelledBy={ids?.label}
388
- describedBy={describes}
397
+ describedBy={countId ? [describes, countId].filter(Boolean).join(' ') : describes}
398
+ cap={cap && { ...cap, onCount: (n) => (counts[key] = n) }}
389
399
  onChange={(rt) =>
390
400
  commitProp(key, optionalCell(sub) && isEmptyContent(rt) ? undefined : rt)}
391
401
  onKey={onCellKey ? (e) => onCellKey(e, key) : undefined}
@@ -6,7 +6,12 @@
6
6
  -->
7
7
  <script lang="ts">
8
8
  import { untrack } from 'svelte';
9
- import { createField, type FieldController, type SlashState } from '../core/codec/index.js';
9
+ import {
10
+ createField,
11
+ type FieldController,
12
+ type SlashState,
13
+ type TextCap
14
+ } from '../core/codec/index.js';
10
15
  import type { LeafRegistry } from './leaves.js';
11
16
  import SlashMenu from './SlashMenu.svelte';
12
17
  import { wording } from './strings.js';
@@ -48,6 +53,8 @@
48
53
  placeholderUntilEdit?: boolean;
49
54
  /** An emptied leaf removes the field (`createField`). */
50
55
  emptyUnsets?: boolean;
56
+ /** The declared `max:`, a character count no edit takes the leaf past. */
57
+ cap?: TextCap;
51
58
  /** Registry identity, stamped on the DOM node so a remount is visible as one. */
52
59
  leafKey: string;
53
60
  onFocus?: (addr: Addr) => void;
@@ -77,6 +84,7 @@
77
84
  placeholder,
78
85
  placeholderUntilEdit,
79
86
  emptyUnsets,
87
+ cap,
80
88
  leafKey,
81
89
  onFocus,
82
90
  onCaretMove,
@@ -127,6 +135,7 @@
127
135
  placeholder,
128
136
  placeholderUntilEdit,
129
137
  emptyUnsets,
138
+ cap,
130
139
  tableStrings: () => t.strings,
131
140
  slashStrings: () => t.strings,
132
141
  onSlash: (next) => {
@@ -1,3 +1,4 @@
1
+ import { type TextCap } from '../core/codec/index.js';
1
2
  import type { LeafRegistry } from './leaves.js';
2
3
  import type { Document, Quill, Addr, Content, Diagnostic } from '@quillmark/wasm';
3
4
  import type { EditorErrorHandler } from '../core/errors.js';
@@ -35,6 +36,8 @@ interface Props {
35
36
  placeholderUntilEdit?: boolean;
36
37
  /** An emptied leaf removes the field (`createField`). */
37
38
  emptyUnsets?: boolean;
39
+ /** The declared `max:`, a character count no edit takes the leaf past. */
40
+ cap?: TextCap;
38
41
  /** Registry identity, stamped on the DOM node so a remount is visible as one. */
39
42
  leafKey: string;
40
43
  onFocus?: (addr: Addr) => void;
@@ -60,7 +60,8 @@
60
60
  pmToUsv,
61
61
  usvToPM,
62
62
  valueEqual,
63
- type LeafViews
63
+ type LeafViews,
64
+ type TextCap
64
65
  } from '../core/codec/index.js';
65
66
  import { valueLeafSeat } from './leaves.js';
66
67
  import { wording } from './strings.js';
@@ -99,6 +100,8 @@
99
100
  /** The empty leaf's ghost, a subform cell's: the `none` an optional cell prints
100
101
  * unset. Read per decoration pass. */
101
102
  placeholder?: string;
103
+ /** The declared `max:`, a character count no edit takes the leaf past. */
104
+ cap?: TextCap;
102
105
  onChange: (rt: Content) => void;
103
106
  /** Raw keydown, for a container whose own keys run through this leaf: the
104
107
  * array repeater's Enter/Backspace (`ArrayField`). Fires before the view's own
@@ -115,6 +118,7 @@
115
118
  labelledBy,
116
119
  describedBy,
117
120
  placeholder,
121
+ cap,
118
122
  onChange,
119
123
  onKey
120
124
  }: Props = $props();
@@ -190,7 +194,8 @@
190
194
  ? []
191
195
  : proseLeafPlugins(schema, {
192
196
  inline,
193
- placeholder: () => placeholder
197
+ placeholder: () => placeholder,
198
+ cap
194
199
  })
195
200
  });
196
201
  };
@@ -1,3 +1,4 @@
1
+ import { type TextCap } from '../core/codec/index.js';
1
2
  import type { Content } from '@quillmark/wasm';
2
3
  interface Props {
3
4
  /** This leaf's content (the parent's boundary read), `undefined` where nothing is
@@ -31,6 +32,8 @@ interface Props {
31
32
  /** The empty leaf's ghost, a subform cell's: the `none` an optional cell prints
32
33
  * unset. Read per decoration pass. */
33
34
  placeholder?: string;
35
+ /** The declared `max:`, a character count no edit takes the leaf past. */
36
+ cap?: TextCap;
34
37
  onChange: (rt: Content) => void;
35
38
  /** Raw keydown, for a container whose own keys run through this leaf: the
36
39
  * array repeater's Enter/Backspace (`ArrayField`). Fires before the view's own
@@ -35,6 +35,12 @@ export interface VisualStrings extends TableChromeStrings, SlashStrings {
35
35
  * spells the separator.
36
36
  */
37
37
  arrayCount: (count: number, max: number) => string;
38
+ /**
39
+ * The count beside the label of a `text` or `richtext` field declaring `max:`, the
40
+ * carrier of a keystroke the cap refuses: `12 / 40`. Parametric, so a translator
41
+ * spells the separator.
42
+ */
43
+ textCount: (count: number, max: number) => string;
38
44
  /**
39
45
  * A collapsed array element with nothing to summarize itself by: the label and the
40
46
  * 1-based index, which is what its accessible name already says. Parametric, so
@@ -41,6 +41,7 @@ export const DEFAULT_VISUAL_STRINGS = {
41
41
  arrayDrag: 'Drag to move',
42
42
  arrayRowAction: (action, row) => `${action} ${row}`,
43
43
  arrayCount: (count, max) => `${count} / ${max}`,
44
+ textCount: (count, max) => `${count} / ${max}`,
44
45
  elementUntitled: (label, index) => `${label} ${index}`,
45
46
  enumUnsetTag: 'Default',
46
47
  enumUnsetTagNone: 'Unset',
@@ -1,4 +1,5 @@
1
1
  import { type Content, type PathStep, type PayloadItem, type QuillCardSchema, type QuillFieldSchema, type QuillFieldType, type ResolvedField, type Resolved } from '@quillmark/wasm';
2
+ import type { TextCap } from '../core/codec/index.js';
2
3
  /** The control a field type maps to (VISUAL_EDITOR §"Structure mirrors the schema"). */
3
4
  export type ControlKind = 'prose' | 'text' | 'enum' | 'variant' | 'number' | 'boolean' | 'date' | 'array' | 'object' | 'matrix';
4
5
  /** The container controls: the ones a nested address walks into and a deep
@@ -161,6 +162,8 @@ export declare function baseType(f: QuillFieldSchema): QuillFieldType;
161
162
  export declare function oneLine(f: QuillFieldSchema): boolean;
162
163
  /** Whether a content field's codec is the literal one, its value text with no marks: `text`. */
163
164
  export declare function literalText(f: QuillFieldSchema): boolean;
165
+ /** The character cap a content field's `max:` declares, `undefined` on any other. */
166
+ export declare function textCap(f: QuillFieldSchema | undefined): TextCap | undefined;
164
167
  /**
165
168
  * A scalar `default:` as the text an unset control holds, where it prints: as
166
169
  * declared, `undefined` for none, a blank or a non-scalar. A default of spaces prints
@@ -175,9 +178,10 @@ export declare function printedText(v: unknown): string | undefined;
175
178
  * a blank or a non-scalar.
176
179
  */
177
180
  export declare function declaredContent(v: unknown, markdown: boolean): Content | undefined;
178
- /** A field's `{#each}` key: its name and what a mounted leaf reads once, its type and
179
- * {@link oneLine} and its elements'. A retype or a quill swap that changes any of them
180
- * under a name remounts the field, where a kept leaf would commit on the old schema. */
181
+ /** A field's `{#each}` key: its name and what a mounted leaf reads once, its type,
182
+ * {@link oneLine} and `max:`, and its elements'. A retype or a quill swap that changes
183
+ * any of them under a name remounts the field, where a kept leaf would commit on the old
184
+ * schema. */
181
185
  export declare function mountKey(name: string, f: QuillFieldSchema): string;
182
186
  /** Map a field schema to its control. An array's element control is this over `items`;
183
187
  * a type this build does not know takes the text input. */
@@ -93,6 +93,12 @@ export function oneLine(f) {
93
93
  export function literalText(f) {
94
94
  return baseType(f) === 'text';
95
95
  }
96
+ /** The character cap a content field's `max:` declares, `undefined` on any other. */
97
+ export function textCap(f) {
98
+ if (f?.max == null || controlKind(f) !== 'prose')
99
+ return undefined;
100
+ return { max: f.max, trimEnd: literalText(f) && oneLine(f) };
101
+ }
96
102
  /**
97
103
  * A scalar `default:` as the text an unset control holds, where it prints: as
98
104
  * declared, `undefined` for none, a blank or a non-scalar. A default of spaces prints
@@ -126,12 +132,21 @@ export function declaredContent(v, markdown) {
126
132
  islands: []
127
133
  };
128
134
  }
129
- /** A field's `{#each}` key: its name and what a mounted leaf reads once, its type and
130
- * {@link oneLine} and its elements'. A retype or a quill swap that changes any of them
131
- * under a name remounts the field, where a kept leaf would commit on the old schema. */
135
+ /** A field's `{#each}` key: its name and what a mounted leaf reads once, its type,
136
+ * {@link oneLine} and `max:`, and its elements'. A retype or a quill swap that changes
137
+ * any of them under a name remounts the field, where a kept leaf would commit on the old
138
+ * schema. */
132
139
  export function mountKey(name, f) {
133
140
  const items = f.items;
134
- return `${name}\u0000${f.type}\u0000${oneLine(f)}\u0000${items?.type ?? ''}\u0000${items ? oneLine(items) : ''}`;
141
+ return [
142
+ name,
143
+ f.type,
144
+ oneLine(f),
145
+ f.max ?? '',
146
+ items?.type ?? '',
147
+ items ? oneLine(items) : '',
148
+ items?.max ?? ''
149
+ ].join('\u0000');
135
150
  }
136
151
  /** Map a field schema to its control. An array's element control is this over `items`;
137
152
  * a type this build does not know takes the text input. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quillmark/svelte",
3
- "version": "0.22.0",
3
+ "version": "0.23.0",
4
4
  "description": "Editor, live-preview and source surfaces over a Quillmark WASM session.",
5
5
  "repository": {
6
6
  "type": "git",