@quillmark/svelte 0.17.0 → 0.19.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 (48) hide show
  1. package/README.md +1 -1
  2. package/dist/core/codec/index.d.ts +0 -3
  3. package/dist/core/rebind.svelte.js +5 -8
  4. package/dist/preset/index.d.ts +1 -0
  5. package/dist/preview/Preview.svelte +14 -19
  6. package/dist/preview/Preview.svelte.d.ts +3 -3
  7. package/dist/preview/index.d.ts +0 -1
  8. package/dist/visual/ArrayField.svelte +48 -49
  9. package/dist/visual/ArrayField.svelte.d.ts +1 -2
  10. package/dist/visual/BooleanField.svelte +9 -14
  11. package/dist/visual/BooleanField.svelte.d.ts +0 -1
  12. package/dist/visual/Card.svelte +5 -18
  13. package/dist/visual/CardControls.svelte.d.ts +0 -1
  14. package/dist/visual/DateField.svelte +14 -17
  15. package/dist/visual/DateField.svelte.d.ts +0 -1
  16. package/dist/visual/EnumField.svelte +9 -12
  17. package/dist/visual/EnumField.svelte.d.ts +0 -1
  18. package/dist/visual/Field.svelte +8 -9
  19. package/dist/visual/FieldHint.svelte.d.ts +0 -1
  20. package/dist/visual/FieldLabel.svelte +1 -2
  21. package/dist/visual/FormatPopover.svelte +22 -27
  22. package/dist/visual/FormatPopover.svelte.d.ts +0 -1
  23. package/dist/visual/MatrixAdd.svelte +1 -2
  24. package/dist/visual/MatrixAdd.svelte.d.ts +0 -1
  25. package/dist/visual/MatrixField.svelte +3 -5
  26. package/dist/visual/MatrixField.svelte.d.ts +0 -1
  27. package/dist/visual/NumberField.svelte +6 -10
  28. package/dist/visual/NumberField.svelte.d.ts +0 -1
  29. package/dist/visual/ObjectField.svelte +9 -13
  30. package/dist/visual/ObjectField.svelte.d.ts +0 -1
  31. package/dist/visual/ProseField.svelte +42 -41
  32. package/dist/visual/ProseField.svelte.d.ts +0 -1
  33. package/dist/visual/ProseValue.svelte +56 -56
  34. package/dist/visual/ProseValue.svelte.d.ts +0 -1
  35. package/dist/visual/SlashMenu.svelte +5 -15
  36. package/dist/visual/SlashMenu.svelte.d.ts +0 -1
  37. package/dist/visual/TextField.svelte +5 -9
  38. package/dist/visual/TextField.svelte.d.ts +0 -1
  39. package/dist/visual/TipsCard.svelte +10 -11
  40. package/dist/visual/TipsCard.svelte.d.ts +0 -1
  41. package/dist/visual/VisualEditorInner.svelte +6 -14
  42. package/dist/visual/VisualEditorInner.svelte.d.ts +0 -1
  43. package/dist/visual/index.d.ts +0 -1
  44. package/dist/visual/structure.d.ts +4 -0
  45. package/dist/visual/structure.js +12 -0
  46. package/package.json +20 -16
  47. package/dist/visual/synced.svelte.d.ts +0 -15
  48. package/dist/visual/synced.svelte.js +0 -34
@@ -15,7 +15,6 @@
15
15
  <script lang="ts">
16
16
  import { Select } from 'bits-ui';
17
17
  import Icon from './icons/Icon.svelte';
18
- import { syncedLocal } from './synced.svelte.js';
19
18
  import { wording } from './strings.js';
20
19
  import './controls.css';
21
20
 
@@ -67,23 +66,21 @@
67
66
  // the like), so it never collides with a real option.
68
67
  const UNSET = '__qm_unset__';
69
68
 
70
- // Local selection synced to `value` (sentinel when unauthored); own-picks stay
71
- // local, only an external change reconciles back in (see `syncedLocal`). Driven
72
- // controlled (`value` + `onValueChange`, never `bind:`) so reconciliation stays
73
- // the package's rather than the primitive's.
74
- const local = syncedLocal(() => value ?? UNSET);
69
+ // Driven controlled (`value` + `onValueChange`, never `bind:`) so reconciliation
70
+ // stays the package's rather than the primitive's.
71
+ let local = $derived(value ?? UNSET);
75
72
 
76
73
  /** The blank has no glyph of its own: the quill's `ui.blank_title` stands in for
77
74
  * it, else an em dash. */
78
75
  const dash = (v: string | undefined) => v || blankTitle || '—';
79
76
 
80
- const unset = $derived(local.value === UNSET);
77
+ const unset = $derived(local === UNSET);
81
78
  /** The ghost is a member the render prints, rather than a word for the blank or for
82
79
  * `none`: it draws at the default rung (theme.css). */
83
80
  const ghostsMember = $derived(!optional && !!fallback);
84
81
  const ghostText = $derived(optional ? t.strings.optionalGhost : dash(fallback));
85
82
  /** What the closed trigger shows: the pick, or the ghosted default while unset. */
86
- const shown = $derived(unset ? ghostText : dash(local.value));
83
+ const shown = $derived(unset ? ghostText : dash(local));
87
84
 
88
85
  /** The root to portal into: `document.body` would escape the consumer's dials
89
86
  * along with the editor's subtree. `undefined` falls back to bits-ui's default. */
@@ -99,10 +96,10 @@
99
96
  <Select.Root
100
97
  type="single"
101
98
  allowDeselect={false}
102
- value={local.value}
99
+ value={local}
103
100
  onValueChange={(v) => {
104
101
  if (v == null) return;
105
- local.value = v;
102
+ local = v;
106
103
  // Sentinel → unset (parent `removeField`, default renders); any real pick
107
104
  // (incl. the default value) → a genuine write.
108
105
  onCommit(v === UNSET ? undefined : v);
@@ -152,12 +149,12 @@
152
149
  policy when it is the one selected: a listbox whose selected value
153
150
  has no row shows nothing for what the document says, and offers none
154
151
  for the primitive to mark, type-ahead to, or key onto
155
- (VISUAL_EDITOR §"Enum policy"). Off `local.value`, not `value`: the
152
+ (VISUAL_EDITOR §"Enum policy"). Off `local`, not `value`: the
156
153
  row that exists is the row the control has selected, through the
157
154
  window where an own pick has not reconciled back. -->
158
155
  {#each values as v (v)}
159
156
  {@const allowed = optionAllowed?.(v) !== false}
160
- {#if allowed || enumDisallowed === 'disable' || v === local.value}
157
+ {#if allowed || enumDisallowed === 'disable' || v === local}
161
158
  <Select.Item
162
159
  class="qm-menu-item qm-select-item"
163
160
  value={v}
@@ -1,4 +1,3 @@
1
- import './controls.css';
2
1
  interface Props {
3
2
  value: string | undefined;
4
3
  values: string[];
@@ -38,6 +38,7 @@
38
38
  isContainer,
39
39
  optionalCell,
40
40
  printedText,
41
+ renderedBoolean,
41
42
  stringifyGhost,
42
43
  titleText
43
44
  } from './structure.js';
@@ -235,12 +236,10 @@
235
236
  * an address to name. `el` is the field-wide box, which a landing at depth does not
236
237
  * wash: that lane hands back its own row (`leaves.ts`).
237
238
  */
238
- let controlEl = $state<HTMLElement | undefined>();
239
- $effect(() => {
240
- if (field.control === 'prose' || !controlEl || !leaves) return;
239
+ function registerControl(wrapper: HTMLElement): (() => void) | undefined {
240
+ if (field.control === 'prose' || !leaves) return;
241
241
  const key = leafKey;
242
242
  const registry = leaves;
243
- const wrapper = controlEl;
244
243
  registry.registerControl(key, {
245
244
  focus: focusControl,
246
245
  focusPath: isContainer(field.control) ? focusPath : undefined,
@@ -249,7 +248,7 @@
249
248
  }
250
249
  });
251
250
  return () => registry.unregisterControl(key);
252
- });
251
+ }
253
252
 
254
253
  /**
255
254
  * A form control has no controller to report its focus through, so the wrapper
@@ -262,7 +261,7 @@
262
261
  const reportFocus = $derived(field.control === 'prose' ? undefined : () => onFocus?.(addr));
263
262
  </script>
264
263
 
265
- <div class="qm-field" class:cell={span === 'cell'}>
264
+ <div class={['qm-field', { cell: span === 'cell' }]}>
266
265
  {#if !ownsLabel && !headed}
267
266
  <FieldLabel
268
267
  label={field.label}
@@ -279,7 +278,7 @@
279
278
  rule below holds why). One markup for both spans; a field owning its row nests a
280
279
  level and measures the same. -->
281
280
  <div class="qm-field-stack">
282
- <div class="qm-field-control" bind:this={controlEl} onfocusin={reportFocus}>
281
+ <div class="qm-field-control" {@attach registerControl} onfocusin={reportFocus}>
283
282
  {#if field.control === 'prose'}
284
283
  <ProseField
285
284
  {quill}
@@ -342,7 +341,7 @@
342
341
  />
343
342
  {:else if field.control === 'boolean'}
344
343
  <BooleanField
345
- value={value as boolean | undefined}
344
+ value={renderedBoolean(value)}
346
345
  fallback={ghost as boolean | undefined}
347
346
  {optional}
348
347
  id={domIds.control}
@@ -362,7 +361,7 @@
362
361
  {:else if field.control === 'array'}
363
362
  <ArrayField
364
363
  bind:this={arrayEl}
365
- value={value as unknown[] | undefined}
364
+ {value}
366
365
  fallback={Array.isArray(field.schema.default) ? field.schema.default : undefined}
367
366
  items={field.schema.items}
368
367
  layout={arrayLayout(field.schema)}
@@ -1,4 +1,3 @@
1
- import './controls.css';
2
1
  interface Props {
3
2
  description: string;
4
3
  /** The field's label: the trigger's own name, so 30 info buttons on a card
@@ -65,8 +65,7 @@
65
65
  label is not a tab stop, and the control it hands focus to is already the
66
66
  keyboard's own way in. -->
67
67
  <label
68
- class="qm-field-label"
69
- class:targeted={controlId != null || onActivate != null}
68
+ class={['qm-field-label', { targeted: controlId != null || onActivate != null }]}
70
69
  {id}
71
70
  for={controlId}
72
71
  onclick={onActivate}
@@ -52,6 +52,7 @@
52
52
  selection the command is supposed to act on.
53
53
  -->
54
54
  <script lang="ts">
55
+ import { onDestroy } from 'svelte';
55
56
  import { wording } from './strings.js';
56
57
 
57
58
  // The surface's words, ambient from the editor root; the package's English
@@ -105,7 +106,6 @@
105
106
  /** Whether the selection carried a link when the prompt was raised. */
106
107
  let linkPresent = $state(false);
107
108
  let linkValue = $state('');
108
- let linkInputEl = $state<HTMLInputElement | undefined>(undefined);
109
109
  let contentEl = $state<HTMLElement | undefined>(undefined);
110
110
  /** The root to portal into: `document.body` escapes the editor's subtree and
111
111
  * the consumer's dials with it, so a pane-scoped palette misses this surface.
@@ -212,25 +212,11 @@
212
212
  deferredSync();
213
213
  }
214
214
 
215
- $effect(() => {
216
- document.addEventListener('selectionchange', deferredSync);
217
- document.addEventListener('focusout', deferredSync);
218
- // Capture, so the gesture is seen whatever a view in the middle of it does with
219
- // the event. `pointercancel` is the release a gesture the browser takes over gets.
220
- document.addEventListener('pointerdown', onPointerDown, true);
221
- document.addEventListener('pointerup', onPointerUp, true);
222
- document.addEventListener('pointercancel', onPointerUp, true);
223
- return () => {
224
- document.removeEventListener('selectionchange', deferredSync);
225
- document.removeEventListener('focusout', deferredSync);
226
- document.removeEventListener('pointerdown', onPointerDown, true);
227
- document.removeEventListener('pointerup', onPointerUp, true);
228
- document.removeEventListener('pointercancel', onPointerUp, true);
229
- // A burst-coalescing rAF may still be in flight at teardown; cancel it so
230
- // `sync()` never runs against the destroyed component.
231
- if (pending) cancelAnimationFrame(rafHandle);
232
- pending = false;
233
- };
215
+ // A burst-coalescing rAF may still be in flight at teardown; cancel it so
216
+ // `sync()` never runs against the destroyed component.
217
+ onDestroy(() => {
218
+ if (pending) cancelAnimationFrame(rafHandle);
219
+ pending = false;
234
220
  });
235
221
 
236
222
  // Any close path that bypasses `sync` (e.g. Escape while the link input holds
@@ -245,12 +231,10 @@
245
231
  // surface already open, so without this the URL is typed into the document.
246
232
  // Selected rather than merely focused, the input arriving seeded and replacing
247
233
  // that value being what the prompt is usually raised for.
248
- $effect(() => {
249
- if (linkPromptOpen && linkInputEl) {
250
- linkInputEl.focus();
251
- linkInputEl.select();
252
- }
253
- });
234
+ function focusPrompt(input: HTMLInputElement): void {
235
+ input.focus();
236
+ input.select();
237
+ }
254
238
 
255
239
  /** Swallow the button's mousedown so focus/selection never leave the leaf. */
256
240
  function keepFocus(e: MouseEvent): void {
@@ -338,6 +322,17 @@
338
322
  }
339
323
  </script>
340
324
 
325
+ <!-- The pointer handlers capture, so the gesture is seen whatever a view in the middle of
326
+ it does with the event. `pointercancel` is the release a gesture the browser takes
327
+ over gets. -->
328
+ <svelte:document
329
+ onselectionchange={deferredSync}
330
+ onfocusout={deferredSync}
331
+ onpointerdowncapture={onPointerDown}
332
+ onpointerupcapture={onPointerUp}
333
+ onpointercancelcapture={onPointerUp}
334
+ />
335
+
341
336
  <Popover.Root bind:open>
342
337
  <Popover.Portal to={portalTarget}>
343
338
  <!-- A flip is measured against the root the surface portals into, which is the box
@@ -383,7 +378,7 @@
383
378
  }}
384
379
  >
385
380
  <input
386
- bind:this={linkInputEl}
381
+ {@attach focusPrompt}
387
382
  class="qm-link-input"
388
383
  type="text"
389
384
  placeholder={t.strings.linkPlaceholder}
@@ -1,5 +1,4 @@
1
1
  import { type FieldController } from '../core/codec/index.js';
2
- import './controls.css';
3
2
  interface Props {
4
3
  /** The observation seam VisualEditor exposes over its `leaves` registry. */
5
4
  getActiveLeaf: () => FieldController | undefined;
@@ -114,8 +114,7 @@
114
114
  <!-- The input keeps focus and owns the keys; a press picks without taking it. -->
115
115
  <!-- svelte-ignore a11y_click_events_have_key_events -->
116
116
  <div
117
- class="qm-menu-item qm-matrix-add-option"
118
- class:add={option.add}
117
+ class={['qm-menu-item qm-matrix-add-option', { add: option.add }]}
119
118
  role="option"
120
119
  id={optionId(k)}
121
120
  tabindex="-1"
@@ -1,5 +1,4 @@
1
1
  import type { AddOption } from './checklists.js';
2
- import './controls.css';
3
2
  interface Props {
4
3
  /** The input's id, and the base its listbox and options derive theirs from. */
5
4
  id: string;
@@ -191,7 +191,7 @@
191
191
  /** A native checkbox carries its own state, so a commit the document declines leaves
192
192
  * the face ticked over a map that says otherwise, `checked={held(id)}` having nothing
193
193
  * new to write. The reassert after the flush is what the styled controls get from
194
- * their synced local (`synced.svelte.ts`): the document is what the face reads. */
194
+ * their local, a `$derived` of the value: the document is what the face reads. */
195
195
  async function tick(id: string, on: boolean, el: HTMLInputElement): Promise<void> {
196
196
  commit(on ? commitMember(map, id, ticked(id)) : unticked(id));
197
197
  await flush();
@@ -333,15 +333,13 @@
333
333
  </span>
334
334
  </div>
335
335
  <div
336
- class="qm-matrix-roster"
337
- class:qm-tracks={compact}
338
- class:compact
336
+ class={['qm-matrix-roster', { 'qm-tracks': compact, compact }]}
339
337
  data-default={defaulted ? '' : undefined}
340
338
  bind:this={rosterEl}
341
339
  >
342
340
  {#each members as m (m.id)}
343
341
  {@const on = held(m.id)}
344
- <div class="qm-member" class:held={on} bind:this={memberEls[m.id]}>
342
+ <div class={['qm-member', { held: on }]} bind:this={memberEls[m.id]}>
345
343
  <div class="qm-member-head">
346
344
  <!-- A real checkbox with its face drawn here: the UA's face is shadow DOM no
347
345
  dial reaches, so the input is `appearance: none` and the box and the
@@ -1,7 +1,6 @@
1
1
  import type { Content, PathStep, QuillFieldSchema } from '@quillmark/wasm';
2
2
  import { type DeepDiagnostic } from './diagnostics.js';
3
3
  import type { LandingBox } from './leaves.js';
4
- import './controls.css';
5
4
  interface Props {
6
5
  /** The stored sparse map, or undefined while the field is unset. */
7
6
  value: Record<string, unknown> | undefined;
@@ -29,7 +29,6 @@
29
29
  numbers, and `1.000` as a whole one.
30
30
  -->
31
31
  <script lang="ts">
32
- import { syncedLocal } from './synced.svelte.js';
33
32
  import './controls.css';
34
33
 
35
34
  interface Props {
@@ -53,11 +52,8 @@
53
52
  }
54
53
  let { value, integer, fallback, placeholder, label, id, describedBy, onCommit }: Props = $props();
55
54
 
56
- // Local input state synced to `value` (as a string projection), or to the default
57
- // an unset field holds; own-typing stays local, only an external change reconciles
58
- // back in (see `syncedLocal`).
59
- const local = syncedLocal(() => (value != null ? String(value) : (fallback ?? '')));
60
- const defaulted = $derived(value == null && !!fallback && local.value === fallback);
55
+ let local = $derived(value != null ? String(value) : (fallback ?? ''));
56
+ const defaulted = $derived(value == null && !!fallback && local === fallback);
61
57
 
62
58
  const partial = $derived(
63
59
  integer ? /^\s*[+-]?\d*\s*$/ : /^\s*[+-]?\d*\.?\d*(?:[eE][+-]?\d*)?\s*$/
@@ -77,7 +73,7 @@
77
73
  // `insertText` lands on the undo stack and fires `input`; `setRangeText` does neither.
78
74
  if (document.execCommand?.('insertText', false, data)) return;
79
75
  el.setRangeText(data, from, to, 'end');
80
- local.value = el.value;
76
+ local = el.value;
81
77
  }
82
78
 
83
79
  // Parse a settled entry and emit it; `local` is owned by `oninput`. Blank →
@@ -89,7 +85,7 @@
89
85
  const entry = raw.trim();
90
86
  if (entry === '') {
91
87
  onCommit(undefined);
92
- local.value = el.value = fallback ?? '';
88
+ local = el.value = fallback ?? '';
93
89
  return;
94
90
  }
95
91
  // Finite as well: a digit run past `Number.MAX_VALUE` reads as `Infinity`.
@@ -102,7 +98,7 @@
102
98
  class="qm-input qm-focus-ring"
103
99
  type="text"
104
100
  inputmode={integer ? 'numeric' : 'decimal'}
105
- value={local.value}
101
+ value={local}
106
102
  {id}
107
103
  {placeholder}
108
104
  data-default={defaulted ? '' : undefined}
@@ -110,7 +106,7 @@
110
106
  aria-describedby={describedBy}
111
107
  onbeforeinput={filter}
112
108
  oninput={(e) => {
113
- local.value = (e.currentTarget as HTMLInputElement).value;
109
+ local = (e.currentTarget as HTMLInputElement).value;
114
110
  }}
115
111
  onchange={(e) => commit(e.currentTarget as HTMLInputElement)}
116
112
  />
@@ -1,4 +1,3 @@
1
- import './controls.css';
2
1
  interface Props {
3
2
  value: number | undefined;
4
3
  integer?: boolean;
@@ -5,8 +5,8 @@
5
5
  ProseValue}) over the boundary's nested read, and a container property — an `array`,
6
6
  an `object`, a `matrix`, a variant — mounts its own control here, at the next rung of
7
7
  the same figure. Depth is bounded by what is open, not by the schema: a repeater inside
8
- a subform is collapsed rows again, one open at a time, so an open row under an open
9
- row is one figure two rungs in.
8
+ a subform is collapsed rows again, so an open row under an open row is one figure two
9
+ rungs in.
10
10
 
11
11
  The nesting is a vertical at `--_qm-border`, the ladder's last stroke: the card's edge,
12
12
  an open section's vertical one `--_qm-nest` in where the field is in one, this one a rung
@@ -48,6 +48,7 @@
48
48
  obliged,
49
49
  optionalCell,
50
50
  printedText,
51
+ renderedBoolean,
51
52
  shortCell
52
53
  } from './structure.js';
53
54
  import { splitDeep, unrouted, type DeepDiagnostic } from './diagnostics.js';
@@ -255,9 +256,7 @@
255
256
 
256
257
  <div
257
258
  bind:this={rootEl}
258
- class="qm-object"
259
- class:qm-object-bare={bare}
260
- class:qm-object-flat={flat}
259
+ class={['qm-object', { 'qm-object-bare': bare, 'qm-object-flat': flat }]}
261
260
  role="group"
262
261
  aria-labelledby={labelledBy}
263
262
  aria-describedby={describedBy}
@@ -286,9 +285,7 @@
286
285
  (`packable`). A block prose cell spans it too, and keeps its label row. -->
287
286
  {@const block = kind === 'prose' && !shortCell(sub)}
288
287
  <div
289
- class="qm-object-prop"
290
- class:qm-prop-full={isContainer(kind)}
291
- class:qm-prop-wide={block}
288
+ class={['qm-object-prop', { 'qm-prop-full': isContainer(kind), 'qm-prop-wide': block }]}
292
289
  data-qm-prop={key}
293
290
  >
294
291
  {#if ids && kind !== 'array' && kind !== 'matrix'}
@@ -330,7 +327,7 @@
330
327
  label={named}
331
328
  id={ids?.control}
332
329
  describedBy={describes}
333
- value={obj[key] as boolean | undefined}
330
+ value={renderedBoolean(obj[key])}
334
331
  fallback={sub.default as boolean | undefined}
335
332
  optional={optionalCell(sub)}
336
333
  onCommit={(v) => commitProp(key, v)}
@@ -375,12 +372,11 @@
375
372
  {:else if kind === 'array'}
376
373
  <!-- The repeater owns its label row, the add chip sharing it (`Field` skips
377
374
  its own label for one too), so the names are handed down rather than
378
- drawn here. Its rows are collapsed rows again, one open at a time: the
379
- depth an open row under an open row costs is two rungs of the ladder
380
- and one figure. -->
375
+ drawn here. Its rows are collapsed rows again: the depth an open row
376
+ under an open row costs is two rungs of the ladder and one figure. -->
381
377
  <ArrayField
382
378
  bind:this={nestedEls[key]}
383
- value={obj[key] as unknown[] | undefined}
379
+ value={obj[key]}
384
380
  fallback={Array.isArray(sub.default) ? sub.default : undefined}
385
381
  items={sub.items}
386
382
  layout={arrayLayout(sub)}
@@ -2,7 +2,6 @@ import type { Content, PathStep, QuillFieldSchema } from '@quillmark/wasm';
2
2
  import { type DeepDiagnostic } from './diagnostics.js';
3
3
  import type { LandingBox } from './leaves.js';
4
4
  import ObjectField from './ObjectField.svelte';
5
- import './controls.css';
6
5
  interface Props {
7
6
  value: Record<string, unknown> | undefined;
8
7
  properties: Record<string, QuillFieldSchema> | undefined;
@@ -5,7 +5,7 @@
5
5
  this leaf's commits with the caret riding the untouched view.
6
6
  -->
7
7
  <script lang="ts">
8
- import { onMount } from 'svelte';
8
+ import { untrack } from 'svelte';
9
9
  import { createField, type FieldController, type SlashState } from '../core/codec/index.js';
10
10
  import type { LeafRegistry } from './leaves.js';
11
11
  import SlashMenu from './SlashMenu.svelte';
@@ -82,7 +82,6 @@
82
82
  diagnostics
83
83
  }: Props = $props();
84
84
 
85
- let containerEl: HTMLDivElement | undefined = $state();
86
85
  let held = $state(false);
87
86
  const uid = $props.id();
88
87
  const heldId = `${uid}-held`;
@@ -108,41 +107,43 @@
108
107
  controller?.focus();
109
108
  }
110
109
 
111
- onMount(() => {
112
- if (!containerEl) return;
113
- controller = createField({
114
- doc,
115
- quill,
116
- addr,
117
- container: containerEl,
118
- inline,
119
- plaintext,
120
- label,
121
- labelledBy,
122
- describedBy,
123
- fallback,
124
- placeholder,
125
- placeholderUntilEdit,
126
- tableStrings: () => t.strings,
127
- onSlash: (next) => {
128
- slash = next;
129
- },
130
- onFocus,
131
- onCaretMove,
132
- onChange,
133
- onError,
134
- heldNoteId: heldId,
135
- onHold: (next) => {
136
- held = next;
137
- }
110
+ function mountField(container: HTMLDivElement): () => void {
111
+ return untrack(() => {
112
+ const mounted = createField({
113
+ doc,
114
+ quill,
115
+ addr,
116
+ container,
117
+ inline,
118
+ plaintext,
119
+ label,
120
+ labelledBy,
121
+ describedBy,
122
+ fallback,
123
+ placeholder,
124
+ placeholderUntilEdit,
125
+ tableStrings: () => t.strings,
126
+ onSlash: (next) => {
127
+ slash = next;
128
+ },
129
+ onFocus,
130
+ onCaretMove,
131
+ onChange,
132
+ onError,
133
+ heldNoteId: heldId,
134
+ onHold: (next) => {
135
+ held = next;
136
+ }
137
+ });
138
+ controller = mounted;
139
+ leaves?.registerProse(leafKey, mounted);
140
+ return () => {
141
+ leaves?.unregisterProse(leafKey);
142
+ mounted.destroy();
143
+ controller = undefined;
144
+ };
138
145
  });
139
- leaves?.registerProse(leafKey, controller);
140
- return () => {
141
- leaves?.unregisterProse(leafKey);
142
- controller?.destroy();
143
- controller = undefined;
144
- };
145
- });
146
+ }
146
147
 
147
148
  // A retype does not remount the leaf (its key is the card's session id), so the new
148
149
  // kind's ghost and default are pushed into the live view rather than paid for with
@@ -162,11 +163,11 @@
162
163
  </script>
163
164
 
164
165
  <div
165
- bind:this={containerEl}
166
- class="qm-prose"
167
- class:qm-control-box={!unframed}
168
- class:qm-focus-ring-within={!unframed}
169
- class:qm-prose-block={block}
166
+ {@attach mountField}
167
+ class={[
168
+ 'qm-prose',
169
+ { 'qm-control-box': !unframed, 'qm-focus-ring-within': !unframed, 'qm-prose-block': block }
170
+ ]}
170
171
  data-leaf-key={leafKey}
171
172
  >
172
173
  {#if held}
@@ -1,5 +1,4 @@
1
1
  import type { LeafRegistry } from './leaves.js';
2
- import './controls.css';
3
2
  import type { Document, Quill, Addr, Content, Diagnostic } from '@quillmark/wasm';
4
3
  import type { EditorErrorHandler } from '../core/errors.js';
5
4
  interface Props {