@glw907/cairn-cms 0.58.0 → 0.60.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 (97) hide show
  1. package/CHANGELOG.md +84 -0
  2. package/dist/components/CairnAdmin.svelte +3 -0
  3. package/dist/components/CairnMediaLibrary.svelte +1101 -27
  4. package/dist/components/CairnMediaLibrary.svelte.d.ts +10 -2
  5. package/dist/components/CairnTidySettings.svelte +553 -0
  6. package/dist/components/CairnTidySettings.svelte.d.ts +32 -0
  7. package/dist/components/EditPage.svelte +371 -2
  8. package/dist/components/MarkdownEditor.svelte +168 -1
  9. package/dist/components/MarkdownEditor.svelte.d.ts +44 -0
  10. package/dist/components/TidyReview.svelte +463 -0
  11. package/dist/components/TidyReview.svelte.d.ts +47 -0
  12. package/dist/components/admin-icons.d.ts +1 -0
  13. package/dist/components/admin-icons.js +1 -0
  14. package/dist/components/cairn-admin.css +913 -2
  15. package/dist/components/editor-tidy.d.ts +31 -0
  16. package/dist/components/editor-tidy.js +199 -0
  17. package/dist/components/index.d.ts +1 -0
  18. package/dist/components/index.js +1 -0
  19. package/dist/components/markdown-directives.d.ts +16 -0
  20. package/dist/components/markdown-directives.js +34 -0
  21. package/dist/components/objective-errors.d.ts +30 -0
  22. package/dist/components/objective-errors.js +113 -0
  23. package/dist/components/spellcheck-assets/dictionary-en-us.txt +104743 -0
  24. package/dist/components/spellcheck-assets/spellchecker-wasm-LICENSE.txt +21 -0
  25. package/dist/components/spellcheck-assets/spellchecker-wasm.wasm +0 -0
  26. package/dist/components/spellcheck-worker.d.ts +80 -0
  27. package/dist/components/spellcheck-worker.js +161 -0
  28. package/dist/components/spellcheck.d.ts +146 -0
  29. package/dist/components/spellcheck.js +541 -0
  30. package/dist/components/tidy-categorize.d.ts +67 -0
  31. package/dist/components/tidy-categorize.js +392 -0
  32. package/dist/components/tidy-diff.d.ts +60 -0
  33. package/dist/components/tidy-diff.js +147 -0
  34. package/dist/components/tidy-validate.d.ts +37 -0
  35. package/dist/components/tidy-validate.js +174 -0
  36. package/dist/content/compose.d.ts +1 -1
  37. package/dist/content/compose.js +11 -0
  38. package/dist/content/site-dictionary.d.ts +31 -0
  39. package/dist/content/site-dictionary.js +82 -0
  40. package/dist/content/types.d.ts +25 -0
  41. package/dist/doctor/checks-local.d.ts +1 -0
  42. package/dist/doctor/checks-local.js +55 -6
  43. package/dist/doctor/index.js +2 -1
  44. package/dist/log/events.d.ts +1 -1
  45. package/dist/media/bulk-delete-plan.d.ts +24 -0
  46. package/dist/media/bulk-delete-plan.js +25 -0
  47. package/dist/media/orphan-scan.d.ts +37 -0
  48. package/dist/media/orphan-scan.js +42 -0
  49. package/dist/media/reconcile.d.ts +3 -0
  50. package/dist/media/reconcile.js +3 -2
  51. package/dist/nav/site-config.d.ts +98 -0
  52. package/dist/nav/site-config.js +132 -0
  53. package/dist/sveltekit/admin-dispatch.d.ts +2 -0
  54. package/dist/sveltekit/admin-dispatch.js +6 -2
  55. package/dist/sveltekit/cairn-admin.d.ts +16 -1
  56. package/dist/sveltekit/cairn-admin.js +28 -3
  57. package/dist/sveltekit/content-routes.d.ts +171 -4
  58. package/dist/sveltekit/content-routes.js +597 -3
  59. package/dist/sveltekit/index.d.ts +1 -1
  60. package/dist/sveltekit/tidy-prompt.d.ts +11 -0
  61. package/dist/sveltekit/tidy-prompt.js +118 -0
  62. package/package.json +10 -1
  63. package/src/lib/components/CairnAdmin.svelte +3 -0
  64. package/src/lib/components/CairnMediaLibrary.svelte +1101 -27
  65. package/src/lib/components/CairnTidySettings.svelte +553 -0
  66. package/src/lib/components/EditPage.svelte +371 -2
  67. package/src/lib/components/MarkdownEditor.svelte +168 -1
  68. package/src/lib/components/TidyReview.svelte +463 -0
  69. package/src/lib/components/admin-icons.ts +1 -0
  70. package/src/lib/components/cairn-admin.css +25 -0
  71. package/src/lib/components/editor-tidy.ts +241 -0
  72. package/src/lib/components/index.ts +1 -0
  73. package/src/lib/components/markdown-directives.ts +35 -0
  74. package/src/lib/components/objective-errors.ts +155 -0
  75. package/src/lib/components/spellcheck-assets/dictionary-en-us.txt +104743 -0
  76. package/src/lib/components/spellcheck-assets/spellchecker-wasm-LICENSE.txt +21 -0
  77. package/src/lib/components/spellcheck-assets/spellchecker-wasm.wasm +0 -0
  78. package/src/lib/components/spellcheck-worker.ts +279 -0
  79. package/src/lib/components/spellcheck.ts +679 -0
  80. package/src/lib/components/tidy-categorize.ts +460 -0
  81. package/src/lib/components/tidy-diff.ts +196 -0
  82. package/src/lib/components/tidy-validate.ts +202 -0
  83. package/src/lib/content/compose.ts +11 -1
  84. package/src/lib/content/site-dictionary.ts +84 -0
  85. package/src/lib/content/types.ts +25 -0
  86. package/src/lib/doctor/checks-local.ts +59 -5
  87. package/src/lib/doctor/index.ts +2 -0
  88. package/src/lib/log/events.ts +9 -1
  89. package/src/lib/media/bulk-delete-plan.ts +54 -0
  90. package/src/lib/media/orphan-scan.ts +74 -0
  91. package/src/lib/media/reconcile.ts +3 -2
  92. package/src/lib/nav/site-config.ts +197 -0
  93. package/src/lib/sveltekit/admin-dispatch.ts +7 -3
  94. package/src/lib/sveltekit/cairn-admin.ts +38 -4
  95. package/src/lib/sveltekit/content-routes.ts +795 -7
  96. package/src/lib/sveltekit/index.ts +1 -0
  97. package/src/lib/sveltekit/tidy-prompt.ts +153 -0
@@ -58,6 +58,17 @@ through the adapter's render. Swapping the editor stays a one-file change.
58
58
  registerImagePlaceholders?: (api: import('./editor-placeholder.js').ImagePlaceholderApi) => void;
59
59
  /** Receives a `() => string` returning the selected text; the web link dialog reads it. */
60
60
  registerGetSelection?: (get: () => string) => void;
61
+ /** Receives a `() => { from, to } | null` returning the selection's document offsets, or null when
62
+ * the selection is empty (a bare caret). The tidy host reads it so a selection tidy maps onto the
63
+ * exact selected span, never an identical-looking passage earlier in the document. */
64
+ registerGetSelectionRange?: (get: () => { from: number; to: number } | null) => void;
65
+ /** Receives the tidy apply api (spec 2.5): the review surface drives the in-buffer decorations and
66
+ * the accept/reject state machine through it. The author's original stays in the buffer until an
67
+ * accept writes; a reject or reject-all leaves it byte-identical. */
68
+ registerTidy?: (api: import('./editor-tidy.js').TidyApi) => void;
69
+ /** Receives a `() => void` that undoes the last editor transaction; the "Undo tidy" chip calls it
70
+ * to take the whole applied tidy back in one move (the apply lands as one history entry). */
71
+ registerUndo?: (undo: () => void) => void;
61
72
  /** Receives a `(kind) => void` that transforms the current selection; the host's toolbar calls it. */
62
73
  registerFormat?: (format: (kind: FormatKind) => void) => void;
63
74
  /** Reports the directive container at the caret (or null when outside any container) whenever
@@ -84,6 +95,36 @@ through the adapter's render. Swapping the editor stays a one-file change.
84
95
  /** The surface posture. Prose is the writing instrument (72ch measure, larger type, looser
85
96
  * leading); markup is the working surface (fills the card, denser). Prose by default. */
86
97
  surface?: 'prose' | 'markup';
98
+ /** Spellcheck and the objective-error layer: the markdown-aware lint underlines. On by default;
99
+ * when off the lint compartment reconfigures to empty (the underlines vanish, the Worker stays
100
+ * idle). The footer toggle drives this. */
101
+ spellcheck?: boolean;
102
+ /** The dialect-resolved dictionary filename, e.g. "dictionary-en-us.txt", from EditData. The
103
+ * source resolves it to a real asset URL and hands it to the spellcheck Worker's init. Defaults to
104
+ * US English. */
105
+ spellcheckDictionary?: string;
106
+ /** The committed personal-dictionary words (spec 1.6), from EditData.siteDictionary. The lint
107
+ * source seeds the spellcheck Worker's personal layer with these at init, so a word another editor
108
+ * committed answers correct from the first lint. Empty by default (dialect-only). */
109
+ siteDictionary?: ReadonlyArray<string>;
110
+ /** The caller-owned pending personal-dictionary additions. When an author chooses "Add to
111
+ * dictionary" the lint source adds the lowercased word here (the underline clears at once); the
112
+ * host (EditPage) commits this set through the addDictionaryWord action at save time and reconciles
113
+ * it against the merged response. A fresh set by default. */
114
+ pendingAdditions?: Set<string>;
115
+ /** Test-only seam for the spellcheck Worker. The real wasm and dictionary assets are resolved with
116
+ * `import.meta.url` and do not load under the vitest browser dev server, so the component test
117
+ * injects a deterministic fake Worker factory and asks the lint source to skip the `ready` wait.
118
+ * When this is absent the production path is untouched: the real `new Worker(...)` and the real
119
+ * asset resolution. Never set this outside a test. */
120
+ spellcheckTest?: {
121
+ createWorker?: () => import('./spellcheck.js').SpellWorker;
122
+ assumeReady?: boolean;
123
+ };
124
+ /** Tidy mode: while a tidy review is open the surface is read-only the way Preview disables the
125
+ * toolbar, so the author cannot edit underneath a pending review. The host sets this when it opens
126
+ * the review and clears it on apply or cancel. Off by default. */
127
+ tidyMode?: boolean;
87
128
  }
88
129
 
89
130
  let {
@@ -98,6 +139,9 @@ through the adapter's render. Swapping the editor stays a one-file change.
98
139
  registerFocusEditor,
99
140
  registerImagePlaceholders,
100
141
  registerGetSelection,
142
+ registerGetSelectionRange,
143
+ registerTidy,
144
+ registerUndo,
101
145
  registerFormat,
102
146
  onComponentAtCaret,
103
147
  onMediaImageAtCaret,
@@ -107,6 +151,12 @@ through the adapter's render. Swapping the editor stays a one-file change.
107
151
  focusMode = false,
108
152
  typewriter = false,
109
153
  surface = 'prose',
154
+ spellcheck = true,
155
+ spellcheckDictionary = 'dictionary-en-us.txt',
156
+ siteDictionary = [],
157
+ pendingAdditions = new Set<string>(),
158
+ spellcheckTest,
159
+ tidyMode = false,
110
160
  }: Props = $props();
111
161
 
112
162
  let host = $state<HTMLDivElement | null>(null);
@@ -127,6 +177,21 @@ through the adapter's render. Swapping the editor stays a one-file change.
127
177
  // module loads with the other dynamic editor modules in onMount.
128
178
  let mediaCompartment: import('@codemirror/state').Compartment | null = null;
129
179
  let mediaMod: typeof import('./editor-media.js') | null = null;
180
+ // The spellcheck lint source (and the objective-error layer it bundles) live in their own
181
+ // compartment, reconfigured to empty when the footer toggle turns spellcheck off. Both surfaces ride
182
+ // the one extension cairnSpellcheck returns, so one compartment gates both. The extension is built
183
+ // asynchronously (it lazy-imports CodeMirror and the lint modules), so it is held here once resolved
184
+ // and the on/off effect reconfigures against it.
185
+ let spellcheckCompartment: import('@codemirror/state').Compartment | null = null;
186
+ let spellcheckExt: import('@codemirror/state').Extension | null = null;
187
+ // The tidy decoration field lives in its own compartment (entering and leaving tidy is a reconfigure,
188
+ // not a rebuild) beside the media and fold decorations. A second compartment carries the read-only +
189
+ // edit-disable extension while a review is open, so the author cannot edit underneath a pending
190
+ // review (the same posture Preview takes on the toolbar). Both load with the other editor modules.
191
+ let tidyMod: typeof import('./editor-tidy.js') | null = null;
192
+ let tidyCompartment: import('@codemirror/state').Compartment | null = null;
193
+ let tidyReadonlyCompartment: import('@codemirror/state').Compartment | null = null;
194
+ let tidyReadonlyExt: import('@codemirror/state').Extension | null = null;
130
195
  // The posture themes, swapped through the surface compartment. Each owns its type step and
131
196
  // leading (the base theme deliberately sets neither on the content node, so the postures never
132
197
  // contest it on adoption order). Built in onMount beside the base theme.
@@ -139,12 +204,15 @@ through the adapter's render. Swapping the editor stays a one-file change.
139
204
  const markdownMod = await import('@codemirror/lang-markdown');
140
205
  const commandsMod = await import('@codemirror/commands');
141
206
  const languageMod = await import('@codemirror/language');
207
+ const lintMod = await import('@codemirror/lint');
142
208
  const autocompleteMod = await import('@codemirror/autocomplete');
143
209
  const highlightMod = await import('./editor-highlight.js');
144
210
  const modesMod = await import('./editor-modes.js');
145
211
  const foldingMod = await import('./editor-folding.js');
146
212
  const placeholderMod = await import('./editor-placeholder.js');
147
213
  mediaMod = await import('./editor-media.js');
214
+ tidyMod = await import('./editor-tidy.js');
215
+ const spellcheckMod = await import('./spellcheck.js');
148
216
 
149
217
  if (!host) return;
150
218
 
@@ -494,6 +562,37 @@ through the adapter's render. Swapping the editor stays a one-file change.
494
562
  '--cairn-directive-rail-3': 'var(--cairn-focus-dim-rail-3, 32%)',
495
563
  '--cairn-directive-rail-active': 'var(--cairn-focus-dim-rail-active, 36%)',
496
564
  },
565
+ // Tidy review decorations (spec 2.5). The author's original stays in the buffer; a deletion run
566
+ // strikes through in --cairn-error-ink (reserved for tidy deletions) and the proposed insertion
567
+ // shows as decoration content in --color-positive-ink (the locked addition token). The two are
568
+ // a locked pair: deletion red and insertion green never speak the same color, so the author sees
569
+ // exactly what tidy removes and what it adds. Both carry a non-color cue (the strike-through and
570
+ // the leading marker) so the change reads without hue alone.
571
+ '.cm-cairn-tidy-del': {
572
+ color: 'var(--cairn-error-ink, oklch(50% 0.19 25))',
573
+ textDecoration: 'line-through',
574
+ textDecorationThickness: '1px',
575
+ backgroundColor: 'color-mix(in oklab, var(--cairn-error-ink, oklch(50% 0.19 25)) 12%, transparent)',
576
+ borderRadius: '2px',
577
+ },
578
+ '.cm-cairn-tidy-del-marker': {
579
+ // A small leading wedge in the deletion ink, the non-color marker that pairs with the red.
580
+ display: 'inline-block',
581
+ width: '0',
582
+ borderLeft: '2px solid var(--cairn-error-ink, oklch(50% 0.19 25))',
583
+ height: '1em',
584
+ verticalAlign: '-0.15em',
585
+ marginRight: '1px',
586
+ },
587
+ '.cm-cairn-tidy-ins': {
588
+ color: 'var(--color-positive-ink, oklch(48% 0.12 150))',
589
+ backgroundColor: 'color-mix(in oklab, var(--color-positive-ink, oklch(48% 0.12 150)) 16%, transparent)',
590
+ borderRadius: '2px',
591
+ padding: '0 1px',
592
+ marginLeft: '2px',
593
+ // The non-color cue for an insertion: a leading caret glyph in the addition ink.
594
+ '&::before': { content: '"+"', fontSize: '0.8em', opacity: '0.7', marginRight: '1px' },
595
+ },
497
596
  },
498
597
  { dark: isDark },
499
598
  );
@@ -517,6 +616,30 @@ through the adapter's render. Swapping the editor stays a one-file change.
517
616
  typewriterCompartment = new stateMod.Compartment();
518
617
  surfaceCompartment = new stateMod.Compartment();
519
618
  mediaCompartment = new stateMod.Compartment();
619
+ spellcheckCompartment = new stateMod.Compartment();
620
+ tidyCompartment = new stateMod.Compartment();
621
+ tidyReadonlyCompartment = new stateMod.Compartment();
622
+ // The read-only posture while a tidy review is open: EditorState.readOnly bars edits, and
623
+ // editable: false drops the contenteditable so the surface is inert under the review (the same
624
+ // posture Preview takes). The compartment starts empty and the tidyMode effect swaps this in.
625
+ tidyReadonlyExt = [stateMod.EditorState.readOnly.of(true), viewMod.EditorView.editable.of(false)];
626
+ // Build the spellcheck extension once: the lint source resolves the dictionary asset URL from the
627
+ // dialect-resolved filename and posts it to the Worker's init. The compartment starts with the
628
+ // extension only when spellcheck is on, so a site that opens with it off never spins up the Worker.
629
+ spellcheckExt = await spellcheckMod.cairnSpellcheck({
630
+ dictionaryFile: spellcheckDictionary,
631
+ // Seed the Worker's personal layer from the committed site dictionary, and share the host's
632
+ // pending-additions set so an add-to-dictionary choice records here for the host to commit.
633
+ siteWords: siteDictionary,
634
+ pendingAdditions,
635
+ // Hand the lint source the editor's own CodeMirror module instances so its extension lands on the
636
+ // same copies; a separate dynamic import can resolve to a different instance and break instanceof.
637
+ modules: { lint: lintMod, language: languageMod, view: viewMod, state: stateMod },
638
+ // The test seam: a deterministic fake Worker and the skip-ready flag, both straight through to the
639
+ // lint source. Absent in production, where the real Worker and real asset resolution run.
640
+ createWorker: spellcheckTest?.createWorker,
641
+ assumeReady: spellcheckTest?.assumeReady,
642
+ });
520
643
 
521
644
  view = new EditorView({
522
645
  parent: host,
@@ -552,6 +675,14 @@ through the adapter's render. Swapping the editor stays a one-file change.
552
675
  // reconfigures it without rebuilding the editor. The chip and the atomic ranges read the
553
676
  // library; an empty library decorates nothing.
554
677
  mediaCompartment.of(mediaMod.cairnMediaDecorations(mediaLibrary)),
678
+ // The spellcheck and objective-error lint sources plus the locked amber underline theme, in
679
+ // their own compartment so the footer toggle gates both surfaces at once. Empty when off.
680
+ spellcheckCompartment.of(spellcheck ? spellcheckExt : []),
681
+ // The tidy decoration field, in its own compartment so entering and leaving a review is a
682
+ // reconfigure beside the media and fold decorations. The api the host drives is built below.
683
+ tidyCompartment.of(tidyMod.cairnTidy()),
684
+ // The read-only posture while a review is open, empty until the host sets tidyMode.
685
+ tidyReadonlyCompartment.of(tidyMode ? tidyReadonlyExt : []),
555
686
  // Paste and drop ingest: an image carried by either gesture is preventDefault'd and handed
556
687
  // to onImageIngest (the host opens the capture card with the bytes); a gesture carrying no
557
688
  // image falls through to CodeMirror's default. 2b is single-file per gesture (open risk 3),
@@ -581,7 +712,11 @@ through the adapter's render. Swapping the editor stays a one-file change.
581
712
  return true;
582
713
  },
583
714
  }),
584
- EditorView.contentAttributes.of({ spellcheck: 'true', autocorrect: 'on', autocapitalize: 'sentences' }),
715
+ // No native text-correction override here (Task 7). The old `spellcheck: 'true'` is gone, so
716
+ // the content node falls back to CodeMirror's own defaults: spellcheck "false", autocorrect
717
+ // "off", autocapitalize "off". The cairn lint source replaces the browser's spellcheck
718
+ // (running both would double-underline), and autocorrect/autocapitalize stay off so a browser
719
+ // never silently rewrites a `media:` token, a directive name, or frontmatter.
585
720
  theme,
586
721
  surfaceCompartment.of(surface === 'prose' ? proseTheme : markupTheme),
587
722
  EditorView.updateListener.of((update) => {
@@ -606,6 +741,11 @@ through the adapter's render. Swapping the editor stays a one-file change.
606
741
  registerFocusEditor?.(focusEditor);
607
742
  registerImagePlaceholders?.(placeholderMod.imagePlaceholderApi(view));
608
743
  registerGetSelection?.(selectedText);
744
+ registerGetSelectionRange?.(selectedRange);
745
+ registerTidy?.(tidyMod.tidyApi(view));
746
+ registerUndo?.(() => {
747
+ if (view) commandsMod.undo(view);
748
+ });
609
749
  registerFormat?.(applyFormat);
610
750
  registerReplaceRange?.(replaceRange);
611
751
  registerSelectRange?.(selectRange);
@@ -654,6 +794,25 @@ through the adapter's render. Swapping the editor stays a one-file change.
654
794
  view.dispatch({ effects: mediaCompartment.reconfigure(mediaMod.cairnMediaDecorations(library)) });
655
795
  });
656
796
 
797
+ // Reconfigure the spellcheck compartment when the footer toggle flips. On restores the bundled
798
+ // extension (both lint sources and the theme); off swaps in an empty extension, so the underlines
799
+ // vanish and the Worker goes idle. Reading the prop tracks it; the guard waits for the mounted
800
+ // editor and the resolved extension.
801
+ $effect(() => {
802
+ const on = spellcheck;
803
+ if (!mounted || !view || !spellcheckCompartment || !spellcheckExt) return;
804
+ view.dispatch({ effects: spellcheckCompartment.reconfigure(on ? spellcheckExt : []) });
805
+ });
806
+
807
+ // Reconfigure the read-only posture when tidyMode flips. On makes the surface inert under the open
808
+ // review (no edits beneath a pending review); off restores editing on apply or cancel. Reading the
809
+ // prop tracks it; the guard waits for the mounted editor and the resolved extension.
810
+ $effect(() => {
811
+ const on = tidyMode;
812
+ if (!mounted || !view || !tidyReadonlyCompartment || !tidyReadonlyExt) return;
813
+ view.dispatch({ effects: tidyReadonlyCompartment.reconfigure(on ? tidyReadonlyExt : []) });
814
+ });
815
+
657
816
  // The last value handed to onComponentAtCaret, so the reporter fires only on a change. The
658
817
  // identity compared is name + markdown + from + to. A pure caret move within one block leaves all
659
818
  // four unchanged, so it does not refire; an edit inside the block changes the markdown even when
@@ -792,6 +951,14 @@ through the adapter's render. Swapping the editor stays a one-file change.
792
951
  return view.state.sliceDoc(from, to);
793
952
  }
794
953
 
954
+ // The selection's document offsets, for the tidy host to scope a selection tidy to the exact span.
955
+ // Null when the selection is empty (a bare caret), which the host reads as document scope.
956
+ function selectedRange(): { from: number; to: number } | null {
957
+ if (!view) return null;
958
+ const { from, to } = view.state.selection.main;
959
+ return from === to ? null : { from, to };
960
+ }
961
+
795
962
  // The caret's viewport coordinates, for the insert popover to anchor itself to the cursor. Null
796
963
  // before the editor mounts or when the caret has no measurable position (an unrendered line).
797
964
  function caretCoords(): { left: number; right: number; top: number; bottom: number } | null {
@@ -46,6 +46,20 @@ interface Props {
46
46
  registerImagePlaceholders?: (api: import('./editor-placeholder.js').ImagePlaceholderApi) => void;
47
47
  /** Receives a `() => string` returning the selected text; the web link dialog reads it. */
48
48
  registerGetSelection?: (get: () => string) => void;
49
+ /** Receives a `() => { from, to } | null` returning the selection's document offsets, or null when
50
+ * the selection is empty (a bare caret). The tidy host reads it so a selection tidy maps onto the
51
+ * exact selected span, never an identical-looking passage earlier in the document. */
52
+ registerGetSelectionRange?: (get: () => {
53
+ from: number;
54
+ to: number;
55
+ } | null) => void;
56
+ /** Receives the tidy apply api (spec 2.5): the review surface drives the in-buffer decorations and
57
+ * the accept/reject state machine through it. The author's original stays in the buffer until an
58
+ * accept writes; a reject or reject-all leaves it byte-identical. */
59
+ registerTidy?: (api: import('./editor-tidy.js').TidyApi) => void;
60
+ /** Receives a `() => void` that undoes the last editor transaction; the "Undo tidy" chip calls it
61
+ * to take the whole applied tidy back in one move (the apply lands as one history entry). */
62
+ registerUndo?: (undo: () => void) => void;
49
63
  /** Receives a `(kind) => void` that transforms the current selection; the host's toolbar calls it. */
50
64
  registerFormat?: (format: (kind: FormatKind) => void) => void;
51
65
  /** Reports the directive container at the caret (or null when outside any container) whenever
@@ -72,6 +86,36 @@ interface Props {
72
86
  /** The surface posture. Prose is the writing instrument (72ch measure, larger type, looser
73
87
  * leading); markup is the working surface (fills the card, denser). Prose by default. */
74
88
  surface?: 'prose' | 'markup';
89
+ /** Spellcheck and the objective-error layer: the markdown-aware lint underlines. On by default;
90
+ * when off the lint compartment reconfigures to empty (the underlines vanish, the Worker stays
91
+ * idle). The footer toggle drives this. */
92
+ spellcheck?: boolean;
93
+ /** The dialect-resolved dictionary filename, e.g. "dictionary-en-us.txt", from EditData. The
94
+ * source resolves it to a real asset URL and hands it to the spellcheck Worker's init. Defaults to
95
+ * US English. */
96
+ spellcheckDictionary?: string;
97
+ /** The committed personal-dictionary words (spec 1.6), from EditData.siteDictionary. The lint
98
+ * source seeds the spellcheck Worker's personal layer with these at init, so a word another editor
99
+ * committed answers correct from the first lint. Empty by default (dialect-only). */
100
+ siteDictionary?: ReadonlyArray<string>;
101
+ /** The caller-owned pending personal-dictionary additions. When an author chooses "Add to
102
+ * dictionary" the lint source adds the lowercased word here (the underline clears at once); the
103
+ * host (EditPage) commits this set through the addDictionaryWord action at save time and reconciles
104
+ * it against the merged response. A fresh set by default. */
105
+ pendingAdditions?: Set<string>;
106
+ /** Test-only seam for the spellcheck Worker. The real wasm and dictionary assets are resolved with
107
+ * `import.meta.url` and do not load under the vitest browser dev server, so the component test
108
+ * injects a deterministic fake Worker factory and asks the lint source to skip the `ready` wait.
109
+ * When this is absent the production path is untouched: the real `new Worker(...)` and the real
110
+ * asset resolution. Never set this outside a test. */
111
+ spellcheckTest?: {
112
+ createWorker?: () => import('./spellcheck.js').SpellWorker;
113
+ assumeReady?: boolean;
114
+ };
115
+ /** Tidy mode: while a tidy review is open the surface is read-only the way Preview disables the
116
+ * toolbar, so the author cannot edit underneath a pending review. The host sets this when it opens
117
+ * the review and clears it on apply or cancel. Off by default. */
118
+ tidyMode?: boolean;
75
119
  }
76
120
  /**
77
121
  * The `MarkdownEditor` seam (spec §6, seam 5): a thin wrapper over CodeMirror 6 exposing a bindable