@quillmark/svelte 0.17.0 → 0.18.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 +35 -43
  9. package/dist/visual/ArrayField.svelte.d.ts +0 -1
  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 +7 -8
  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 +8 -12
  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
@@ -33,30 +33,20 @@
33
33
  }
34
34
  let { menu, leaf, label }: Props = $props();
35
35
 
36
- /** Raised whenever the codec reports offers. Mirrored into local state rather than
37
- * derived, because the primitive owns this prop: its own dismissal layer (an
38
- * outside press) writes it, and a derived it cannot write to is an error rather
39
- * than a dismissal. The next report re-syncs, so the codec stays the authority on
40
- * whether a run is live. */
41
- let open = $state(false);
42
- $effect(() => {
43
- open = !!menu && menu.items.length > 0;
44
- });
36
+ let open = $derived(!!menu && menu.items.length > 0);
45
37
 
46
38
  /** The root to portal into, resolved from the leaf's own DOM so the menu lands
47
39
  * inside whichever `[data-qm-root]` raised it and inherits the consumer's dials;
48
40
  * `undefined` falls back to bits-ui's `document.body`. */
49
41
  const portalTarget = $derived(leaf()?.el.closest<HTMLElement>('[data-qm-root]') ?? undefined);
50
42
 
51
- let surface = $state<HTMLElement | undefined>();
52
-
53
43
  /** Keep the keyboard's cursor in the port: the arrows move an index rather than a
54
44
  * focus, so a row the list scrolled past has nothing bringing it back. `nearest`
55
45
  * scrolls the least that works, and no ancestor the row is already visible in. */
56
- $effect(() => {
46
+ function followHighlight(surface: HTMLElement): void {
57
47
  void menu?.index;
58
- surface?.querySelector('[data-highlighted]')?.scrollIntoView({ block: 'nearest' });
59
- });
48
+ surface.querySelector('[data-highlighted]')?.scrollIntoView({ block: 'nearest' });
49
+ }
60
50
 
61
51
  /** Swallow the item's own mousedown: without it the browser focuses the item and
62
52
  * blurs the leaf, taking the trigger run's caret with it. */
@@ -98,7 +88,7 @@
98
88
  a thing to click, tab into, or read. -->
99
89
  <div
100
90
  {...props}
101
- bind:this={surface}
91
+ {@attach followHighlight}
102
92
  class="qm-slash-menu qm-menu-surface"
103
93
  data-qm-root
104
94
  role="group"
@@ -1,5 +1,4 @@
1
1
  import type { FieldController, SlashState } from '../core/codec/index.js';
2
- import './controls.css';
3
2
  interface Props {
4
3
  /** The live menu, or `undefined` when closed: the codec's report channel. Named
5
4
  * `menu` rather than `state`, which is the rune's word in this file. */
@@ -14,7 +14,6 @@
14
14
  unanswered, and an optional cell returns to `none`.
15
15
  -->
16
16
  <script lang="ts">
17
- import { syncedLocal } from './synced.svelte.js';
18
17
  import './controls.css';
19
18
 
20
19
  interface Props {
@@ -40,11 +39,8 @@
40
39
  }
41
40
  let { value, fallback, placeholder, label, id, describedBy, onCommit, onKey }: Props = $props();
42
41
 
43
- // Local input state synced to `value`, or to the default an unset field holds:
44
- // own-typing stays local, only an external change reconciles back in (see
45
- // `syncedLocal`).
46
- const local = syncedLocal(() => value ?? fallback ?? '');
47
- const defaulted = $derived(value == null && !!fallback && local.value === fallback);
42
+ let local = $derived(value ?? fallback ?? '');
43
+ const defaulted = $derived(value == null && !!fallback && local === fallback);
48
44
 
49
45
  let inputEl: HTMLInputElement | undefined = $state();
50
46
  /** Take the caret: what a parent placing focus on this control calls. */
@@ -57,7 +53,7 @@
57
53
  bind:this={inputEl}
58
54
  class="qm-input qm-focus-ring"
59
55
  type="text"
60
- value={local.value}
56
+ value={local}
61
57
  {id}
62
58
  {placeholder}
63
59
  data-default={defaulted ? '' : undefined}
@@ -65,7 +61,7 @@
65
61
  aria-describedby={describedBy}
66
62
  onkeydown={onKey}
67
63
  oninput={(e) => {
68
- local.value = (e.currentTarget as HTMLInputElement).value;
69
- onCommit(local.value !== '' ? local.value : fallback ? '' : undefined);
64
+ local = (e.currentTarget as HTMLInputElement).value;
65
+ onCommit(local !== '' ? local : fallback ? '' : undefined);
70
66
  }}
71
67
  />
@@ -1,4 +1,3 @@
1
- import './controls.css';
2
1
  interface Props {
3
2
  value: string | undefined;
4
3
  /** The resolved `default:` where it prints: the text an unset field holds. */
@@ -40,20 +40,19 @@
40
40
  const index = $derived(Math.min(cursor, tips.length - 1));
41
41
  const isLast = $derived(index >= tips.length - 1);
42
42
 
43
- // The tip string is derived before the effect reads it, and that is load-bearing.
44
- // Reading `tips[index]` inside the effect makes the parent's `model` derive the
45
- // dependency (a fresh object every `revision` bump) so the effect would re-run
46
- // on every unrelated commit, re-crossing the WASM boundary and rebuilding this
47
- // `aria-live` region per keystroke. A derived string short-circuits on `===`.
43
+ // The tip string is derived before the attachment reads it, and that is
44
+ // load-bearing. Reading `tips[index]` inside the attachment makes the parent's
45
+ // `model` derive the dependency (a fresh object every `revision` bump) so the
46
+ // attachment would re-run on every unrelated commit, re-crossing the WASM boundary
47
+ // and rebuilding this `aria-live` region per keystroke. A derived string
48
+ // short-circuits on `===`.
48
49
  const tip = $derived(tips[index] ?? '');
49
50
 
50
51
  // The tip is DOM, not text: `renderTip` returns a fragment the codec's own
51
52
  // `toDOM` built, so it is written in rather than interpolated.
52
- let bodyEl: HTMLDivElement | undefined = $state();
53
- $effect(() => {
54
- const el = bodyEl;
55
- if (el) el.replaceChildren(renderTip(tip, onError));
56
- });
53
+ function paintTip(body: HTMLDivElement): void {
54
+ body.replaceChildren(renderTip(tip, onError));
55
+ }
57
56
 
58
57
  function advance(): void {
59
58
  if (isLast) onDismiss();
@@ -64,7 +63,7 @@
64
63
  <aside class="qm-tips" aria-label={t.strings.tipsLabel}>
65
64
  <!-- Advancing swaps the text under a button that keeps focus, so the region
66
65
  announces rather than the change passing silently. -->
67
- <div class="qm-tips-body" aria-live="polite" bind:this={bodyEl}></div>
66
+ <div class="qm-tips-body" aria-live="polite" {@attach paintTip}></div>
68
67
  <div class="qm-tips-foot">
69
68
  <!-- One tip is its own whole set, so a lone dot reports nothing the card does
70
69
  not already show. -->
@@ -1,5 +1,4 @@
1
1
  import type { EditorErrorHandler } from '../core/errors.js';
2
- import './controls.css';
3
2
  interface Props {
4
3
  /** The narrowed channel (`tipsChannel`); never empty; the parent gates on length. */
5
4
  tips: string[];
@@ -19,6 +19,7 @@
19
19
  -->
20
20
  <script lang="ts">
21
21
  import { onDestroy, tick } from 'svelte';
22
+ import { SvelteMap } from 'svelte/reactivity';
22
23
  import { DropdownMenu } from 'bits-ui';
23
24
  import { isQuillmarkError, MAIN_CARD_ADDR } from '@quillmark/wasm';
24
25
  import {
@@ -135,14 +136,7 @@
135
136
  // Local commit-error diagnostics (VISUAL_EDITOR §Diagnostics, producer #2),
136
137
  // id-keyed rather than positional so an error stays pinned to its field across a
137
138
  // card reorder.
138
- let commitErrors = $state(new Map<string, RoutedDiagnostic>());
139
- /** Edit the commit-error map copy-on-write: a `$state` Map is not deeply
140
- * reactive, so a mutation in place re-derives nothing. */
141
- function editCommitErrors(edit: (m: Map<string, RoutedDiagnostic>) => void): void {
142
- const next = new Map(commitErrors);
143
- edit(next);
144
- commitErrors = next;
145
- }
139
+ const commitErrors = new SvelteMap<string, RoutedDiagnostic>();
146
140
 
147
141
  const kinds = $derived(Object.keys(quill.schema.card_kinds ?? {}));
148
142
 
@@ -350,14 +344,14 @@
350
344
  w.card(i).set(name, value);
351
345
  }
352
346
  }
353
- if (commitErrors.has(keyStr)) editCommitErrors((m) => m.delete(keyStr));
347
+ commitErrors.delete(keyStr);
354
348
  bump('field', id, makeAddr(id, isMain, name));
355
349
  } catch (e) {
356
350
  const diagnostic: Diagnostic = (isQuillmarkError(e) ? e.diagnostics[0] : undefined) ?? {
357
351
  severity: 'error',
358
352
  message: errorMessage(e)
359
353
  };
360
- editCommitErrors((m) => m.set(keyStr, { key, diagnostic }));
354
+ commitErrors.set(keyStr, { key, diagnostic });
361
355
  // Both channels, deliberately: the diagnostic pins to the field on
362
356
  // screen, the error reaches the app's sink (core/errors.ts).
363
357
  reportError(onError, {
@@ -462,9 +456,7 @@
462
456
  // because nothing else will ever clear it.
463
457
  const prefix = `${id}:`;
464
458
  const stale = [...commitErrors.keys()].filter((k) => k.startsWith(prefix));
465
- // Guarded: `editCommitErrors` clones the map, and a removal matching nothing has
466
- // nothing to re-derive.
467
- if (stale.length) editCommitErrors((m) => stale.forEach((k) => m.delete(k)));
459
+ for (const k of stale) commitErrors.delete(k);
468
460
  // No addr: the removed card has none left and every survivor's shifted. The
469
461
  // stack changed, not a leaf, and the id is the only handle the removal leaves.
470
462
  bump('structure', id);
@@ -937,7 +929,7 @@
937
929
  gaps between cards stay bare: the strip is the gap, and the pill each fills on
938
930
  hover draws what a label would state, the space the new card takes. -->
939
931
  {@const marked = atIndex === model.cards.length}
940
- <div class="qm-add-card" class:qm-add-card-marked={marked}>
932
+ <div class={['qm-add-card', { 'qm-add-card-marked': marked }]}>
941
933
  <!-- Marked, the words are the accessible name and no `aria-label` doubles
942
934
  them; bare, the same words land as the label, the strip having no
943
935
  geometry to carry them. -->
@@ -2,7 +2,6 @@ import { type DocPath, type Landing } from '../core/address.js';
2
2
  import type { VisualEditorProps } from './props.js';
3
3
  import type { CardId } from './signals.js';
4
4
  import type { FieldController } from '../core/codec/index.js';
5
- import './controls.css';
6
5
  declare const VisualEditorInner: import("svelte").Component<VisualEditorProps, {
7
6
  setCaret: (at: Landing) => Promise<void>;
8
7
  getActiveLeaf: () => FieldController | undefined;
@@ -1,4 +1,3 @@
1
- import '../core/theme.css';
2
1
  export { default as VisualEditor } from './VisualEditor.svelte';
3
2
  export type { VisualEditorProps } from './props.js';
4
3
  export { createField } from '../core/codec/index.js';
@@ -114,6 +114,10 @@ export declare function resolvedByCardIndex(resolved: Resolved | undefined): Map
114
114
  * use (`source === 'default'`), else undefined: an `authored` field shows its value,
115
115
  * a `blank` field has no default to show. */
116
116
  export declare function ghostDefault(row: ResolvedField | undefined): unknown;
117
+ /** A stored `boolean` cell as the render reads it: a boolean, `true` or `false` in any
118
+ * case, or a number by whether it is non-zero; undefined where the render refuses the
119
+ * value. The engine's coercion, restated, so the suite holds it to `reader.resolve()`. */
120
+ export declare function renderedBoolean(value: unknown): boolean | undefined;
117
121
  /** A ghost value's string form, or undefined for null/object (only text ghosts
118
122
  * render a placeholder). A scalar's resolved default reads through it; a content
119
123
  * field's resolves as `Content`, which a prose leaf reads by its text instead
@@ -45,6 +45,18 @@ export function resolvedByCardIndex(resolved) {
45
45
  export function ghostDefault(row) {
46
46
  return row?.source === 'default' ? row.value : undefined;
47
47
  }
48
+ /** A stored `boolean` cell as the render reads it: a boolean, `true` or `false` in any
49
+ * case, or a number by whether it is non-zero; undefined where the render refuses the
50
+ * value. The engine's coercion, restated, so the suite holds it to `reader.resolve()`. */
51
+ export function renderedBoolean(value) {
52
+ if (typeof value === 'boolean')
53
+ return value;
54
+ if (typeof value === 'number')
55
+ return value !== 0;
56
+ if (typeof value === 'string' && /^(true|false)$/i.test(value))
57
+ return value.toLowerCase() === 'true';
58
+ return undefined;
59
+ }
48
60
  /** A ghost value's string form, or undefined for null/object (only text ghosts
49
61
  * render a placeholder). A scalar's resolved default reads through it; a content
50
62
  * field's resolves as `Content`, which a prose leaf reads by its text instead
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quillmark/svelte",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "description": "Editor, live-preview and source surfaces over a Quillmark WASM session.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -48,7 +48,10 @@
48
48
  "svelte": "./dist/visual/index.js",
49
49
  "default": "./dist/visual/index.js"
50
50
  },
51
- "./preset": "./dist/preset/index.css"
51
+ "./preset": {
52
+ "types": "./dist/preset/index.d.ts",
53
+ "default": "./dist/preset/index.css"
54
+ }
52
55
  },
53
56
  "files": [
54
57
  "dist",
@@ -57,21 +60,22 @@
57
60
  ],
58
61
  "sideEffects": true,
59
62
  "scripts": {
60
- "build": "svelte-package",
63
+ "build": "svelte-package && node scripts/strip-dts-css.mjs",
61
64
  "prepare": "npm run build",
62
65
  "prepack": "rm -rf dist && npm run build",
63
66
  "check:pack": "publint",
64
67
  "test": "vitest run",
65
68
  "test:watch": "vitest",
66
- "check": "svelte-check --tsconfig ./tsconfig.json"
69
+ "check": "svelte-check --tsconfig ./tsconfig.json --fail-on-warnings"
67
70
  },
68
71
  "peerDependencies": {
69
- "@quillmark/wasm": ">=0.121.0-0",
70
- "svelte": "^5.0.0"
72
+ "@quillmark/wasm": ">=0.122.0-0",
73
+ "svelte": "^5.33.0"
71
74
  },
72
75
  "dependencies": {
73
76
  "@internationalized/date": "^3.12.2",
74
- "bits-ui": "^2.0.0",
77
+ "bits-ui": "^2.19.0",
78
+ "esm-env": "^1.2.2",
75
79
  "prosemirror-commands": "1.7.1",
76
80
  "prosemirror-gapcursor": "1.3.2",
77
81
  "prosemirror-history": "1.5.0",
@@ -84,15 +88,15 @@
84
88
  "prosemirror-view": "1.42.1"
85
89
  },
86
90
  "devDependencies": {
87
- "@sveltejs/package": "^2.5.0",
88
- "@sveltejs/vite-plugin-svelte": "^6.0.0",
89
- "@types/node": "^26.1.1",
90
- "jsdom": "^29.1.1",
91
- "publint": "^0.3.0",
92
- "svelte": "^5.0.0",
93
- "svelte-check": "^4.1.0",
94
- "typescript": "^5.7.0",
95
- "vitest": "^4.1.10"
91
+ "@sveltejs/package": "^3.0.0",
92
+ "@sveltejs/vite-plugin-svelte": "^7.3.0",
93
+ "@types/node": "^24.19.1",
94
+ "jsdom": "^30.1.0",
95
+ "publint": "^0.3.25",
96
+ "svelte": "^5.54.0",
97
+ "svelte-check": "^4.7.6",
98
+ "typescript": "^6.0.0",
99
+ "vitest": "^5.0.0"
96
100
  },
97
101
  "publishConfig": {
98
102
  "access": "public"
@@ -1,15 +0,0 @@
1
- /** A settable local synced to an external projection. Bind `.value` in the
2
- * template; assign `.value` on input. */
3
- export interface SyncedLocal<T> {
4
- value: T;
5
- }
6
- /**
7
- * Seed a local from `project` and reconcile only an external change into it.
8
- * `project` reads the reactive source (a prop) and maps it to the local's type:
9
- * ` => value ?? ''`, ` => (value != null ? String(value) : '')`, etc. The
10
- * seed is `untrack`ed (the ongoing sync is the `$effect`, not the initializer),
11
- * and each reconcile is `untrack`ed so an own-edit write never re-triggers it.
12
- * Reassignment through the `value` setter is a plain local write; only a changed
13
- * `project` result flows the other way.
14
- */
15
- export declare function syncedLocal<T>(project: () => T): SyncedLocal<T>;
@@ -1,34 +0,0 @@
1
- // The controlled-local reconcile shared by the scalar controls (TextField /
2
- // NumberField / DateField / BooleanField / EnumField). Each seeds a local from a
3
- // projection of its `value` prop and reconciles only an external change back into
4
- // the local: own edits stay `untrack`ed, so typing never re-runs the sync and
5
- // resets the caret. One copy for all five, rather than a character-identical block
6
- // in each (VISUAL_EDITOR §Surface).
7
- import { untrack } from 'svelte';
8
- /**
9
- * Seed a local from `project` and reconcile only an external change into it.
10
- * `project` reads the reactive source (a prop) and maps it to the local's type:
11
- * ` => value ?? ''`, ` => (value != null ? String(value) : '')`, etc. The
12
- * seed is `untrack`ed (the ongoing sync is the `$effect`, not the initializer),
13
- * and each reconcile is `untrack`ed so an own-edit write never re-triggers it.
14
- * Reassignment through the `value` setter is a plain local write; only a changed
15
- * `project` result flows the other way.
16
- */
17
- export function syncedLocal(project) {
18
- let local = $state(untrack(project));
19
- $effect(() => {
20
- const incoming = project();
21
- untrack(() => {
22
- if (incoming !== local)
23
- local = incoming;
24
- });
25
- });
26
- return {
27
- get value() {
28
- return local;
29
- },
30
- set value(v) {
31
- local = v;
32
- }
33
- };
34
- }