@bendyline/squisq-editor-react 2.11.17 → 2.11.18

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.
@@ -1,3 +1,7 @@
1
+ import {
2
+ isSpeechInputProviderFactory,
3
+ resolveSpeechInputProvider
4
+ } from "./chunk-DNEVUOBZ.js";
1
5
  import {
2
6
  markdownToTiptap
3
7
  } from "./chunk-DPAMJV6A.js";
@@ -87,6 +91,175 @@ function sliceIndexAtOffset(slices, offset) {
87
91
  return slices.findIndex((s) => offset >= s.range.startOffset && offset < s.range.endOffset);
88
92
  }
89
93
 
94
+ // src/sourceEdits.ts
95
+ import { parseMarkdown as parseMarkdown2 } from "@bendyline/squisq/markdown";
96
+ import { isContainerTemplate } from "@bendyline/squisq/doc";
97
+ function normalizeSourceEdits(source, edits) {
98
+ const sorted = edits.map((edit, order) => ({ edit, order })).sort((a, b) => a.edit.start - b.edit.start || a.edit.end - b.edit.end || a.order - b.order).map(({ edit }) => edit);
99
+ let previousEnd = 0;
100
+ for (const edit of sorted) {
101
+ if (!Number.isInteger(edit.start) || !Number.isInteger(edit.end) || typeof edit.text !== "string" || edit.start < 0 || edit.end < edit.start || edit.end > source.length || edit.start < previousEnd) {
102
+ return null;
103
+ }
104
+ previousEnd = edit.end;
105
+ }
106
+ return sorted;
107
+ }
108
+ function applySourceEditsToText(source, edits) {
109
+ const sorted = normalizeSourceEdits(source, edits);
110
+ if (!sorted) throw new RangeError("Source edits are out of range or overlap.");
111
+ let output = "";
112
+ let cursor = 0;
113
+ for (const edit of sorted) {
114
+ output += source.slice(cursor, edit.start) + edit.text;
115
+ cursor = edit.end;
116
+ }
117
+ return output + source.slice(cursor);
118
+ }
119
+ function minimalReplaceEdit(previous, next) {
120
+ if (previous === next) return null;
121
+ const limit = Math.min(previous.length, next.length);
122
+ let prefix = 0;
123
+ while (prefix < limit && previous.charCodeAt(prefix) === next.charCodeAt(prefix)) prefix++;
124
+ let suffix = 0;
125
+ while (suffix < limit - prefix && previous.charCodeAt(previous.length - 1 - suffix) === next.charCodeAt(next.length - 1 - suffix)) {
126
+ suffix++;
127
+ }
128
+ return {
129
+ start: prefix,
130
+ end: previous.length - suffix,
131
+ text: next.slice(prefix, next.length - suffix)
132
+ };
133
+ }
134
+ function containerOwners(blocks) {
135
+ const owners = [];
136
+ let active = null;
137
+ blocks.forEach((block, index) => {
138
+ if (block.headingDepth !== null) {
139
+ if (active && block.headingDepth <= active.depth) active = null;
140
+ if (!active && block.container) active = { index, depth: block.headingDepth };
141
+ }
142
+ owners.push(active ? active.index : -1);
143
+ });
144
+ return owners;
145
+ }
146
+ function resolveOutlineInsertion(blocks, cursorIndex, placement) {
147
+ const owners = containerOwners(blocks);
148
+ const cursor = Math.min(Math.max(cursorIndex, -1), blocks.length - 1);
149
+ const firstHeadingFrom = (from) => {
150
+ for (let i = Math.max(from, 0); i < blocks.length; i++) {
151
+ if (blocks[i]?.headingDepth !== null) return i;
152
+ }
153
+ return blocks.length;
154
+ };
155
+ let index;
156
+ const container = cursor >= 0 ? owners[cursor] ?? -1 : -1;
157
+ if (container >= 0) {
158
+ const depth = blocks[container]?.headingDepth ?? 1;
159
+ if (placement === "afterBlock") {
160
+ index = container;
161
+ } else {
162
+ index = blocks.length;
163
+ for (let i = container + 1; i < blocks.length; i++) {
164
+ const headingDepth = blocks[i]?.headingDepth ?? null;
165
+ if (headingDepth !== null && headingDepth <= depth) {
166
+ index = i;
167
+ break;
168
+ }
169
+ }
170
+ }
171
+ } else if (placement === "afterBlock") {
172
+ index = cursor + 1;
173
+ } else {
174
+ index = firstHeadingFrom(cursor + 1);
175
+ }
176
+ const nextHeading = firstHeadingFrom(index);
177
+ const nextHeadingDepth = nextHeading < blocks.length ? blocks[nextHeading]?.headingDepth ?? null : null;
178
+ let previousHeading = -1;
179
+ for (let i = index - 1; i >= 0; i--) {
180
+ if (blocks[i]?.headingDepth !== null) {
181
+ previousHeading = i;
182
+ break;
183
+ }
184
+ }
185
+ let sectionDepth = 0;
186
+ let maxHeadingDepth = 6;
187
+ if (previousHeading >= 0) {
188
+ const owner = owners[previousHeading] ?? -1;
189
+ if (owner >= 0) {
190
+ const ownerDepth = blocks[owner]?.headingDepth ?? 1;
191
+ sectionDepth = ownerDepth - 1;
192
+ maxHeadingDepth = ownerDepth;
193
+ } else {
194
+ sectionDepth = blocks[previousHeading]?.headingDepth ?? 0;
195
+ }
196
+ }
197
+ return { index, sectionDepth, nextHeadingDepth, maxHeadingDepth };
198
+ }
199
+ function headingDepthForInsertion(context, options = {}) {
200
+ const deepest = Math.min(context.maxHeadingDepth, 6 - (options.childLevels ?? 0));
201
+ const wanted = Math.max(context.sectionDepth + 1, 2, context.nextHeadingDepth ?? 0);
202
+ const depth = Math.min(wanted, deepest);
203
+ if (depth < 1 || depth < (context.nextHeadingDepth ?? 1)) return null;
204
+ return depth;
205
+ }
206
+ function sourceOutline(source) {
207
+ const blocks = [];
208
+ const ranges = [];
209
+ const bodyStart = frontmatterEndOffset(source);
210
+ let children;
211
+ try {
212
+ children = parseMarkdown2(source).children;
213
+ } catch {
214
+ return { blocks, ranges };
215
+ }
216
+ for (const node of children) {
217
+ const start = node.position?.start.offset;
218
+ const end = node.position?.end.offset;
219
+ if (typeof start !== "number" || typeof end !== "number" || start < bodyStart) continue;
220
+ if (node.type === "heading") {
221
+ blocks.push({
222
+ headingDepth: node.depth,
223
+ container: isContainerTemplate(node.templateAnnotation?.template)
224
+ });
225
+ } else {
226
+ blocks.push({ headingDepth: null, container: false });
227
+ }
228
+ ranges.push({ start, end });
229
+ }
230
+ return { blocks, ranges };
231
+ }
232
+ function blockInsertionPoint(source, cursorOffset, placement) {
233
+ const { blocks, ranges } = sourceOutline(source);
234
+ let cursorIndex = -1;
235
+ for (let i = 0; i < ranges.length; i++) {
236
+ if ((ranges[i]?.start ?? Infinity) <= cursorOffset) cursorIndex = i;
237
+ else break;
238
+ }
239
+ const { index, ...context } = resolveOutlineInsertion(blocks, cursorIndex, placement);
240
+ const offset = index > 0 ? ranges[index - 1]?.end ?? source.length : ranges[0]?.start ?? source.length;
241
+ return { offset, ...context };
242
+ }
243
+ function joinBlockAt(source, offset, block) {
244
+ const bodyStart = frontmatterEndOffset(source);
245
+ const isSpace = (char) => char === "\n" || char === "\r" || char === " " || char === " ";
246
+ let start = Math.min(Math.max(offset, bodyStart), source.length);
247
+ while (start > bodyStart && isSpace(source[start - 1])) start--;
248
+ let end = Math.max(start, Math.min(offset, source.length));
249
+ while (end < source.length) {
250
+ const newline = source.indexOf("\n", end);
251
+ const lineEnd = newline === -1 ? source.length : newline + 1;
252
+ if (source.slice(end, lineEnd).trim() !== "") break;
253
+ end = lineEnd;
254
+ }
255
+ const body = block.replace(/^\s*\n/, "").replace(/\s+$/, "");
256
+ const before = source.slice(0, start);
257
+ const trailingNewlines = before.endsWith("\n\n") ? 2 : before.endsWith("\n") ? 1 : 0;
258
+ const prefix = before.length === 0 ? "" : "\n".repeat(2 - trailingNewlines);
259
+ const suffix = end >= source.length ? "\n" : "\n\n";
260
+ return { start, end, text: prefix + body + suffix };
261
+ }
262
+
90
263
  // src/fileKind.ts
91
264
  var DATA_MODE_EXTENSIONS = /* @__PURE__ */ new Set(["csv", "tsv", "parquet"]);
92
265
  var EXT_TO_LANGUAGE = {
@@ -258,20 +431,679 @@ function useBlockNavigator(source, setSource, opts) {
258
431
  }
259
432
 
260
433
  // src/EditorContext.tsx
434
+ import {
435
+ createContext as createContext2,
436
+ useContext as useContext2,
437
+ useState as useState3,
438
+ useCallback as useCallback3,
439
+ useMemo as useMemo3,
440
+ useRef as useRef2,
441
+ useEffect as useEffect2
442
+ } from "react";
443
+ import { parseMarkdown as parseMarkdown3, stringifyMarkdown } from "@bendyline/squisq/markdown";
444
+ import { markdownToDoc } from "@bendyline/squisq/doc";
445
+
446
+ // src/speech/useDictationController.ts
261
447
  import {
262
448
  createContext,
263
- useContext,
264
- useState as useState2,
265
449
  useCallback as useCallback2,
450
+ useContext,
451
+ useEffect,
266
452
  useMemo as useMemo2,
267
453
  useRef,
268
- useEffect
454
+ useState as useState2
269
455
  } from "react";
270
- import { parseMarkdown as parseMarkdown2, stringifyMarkdown } from "@bendyline/squisq/markdown";
271
- import { markdownToDoc } from "@bendyline/squisq/doc";
456
+
457
+ // src/speech/dictationInsertion.ts
458
+ import { Selection, TextSelection } from "@tiptap/pm/state";
459
+ import { closeHistory } from "@tiptap/pm/history";
460
+ var INLINE_OBJECT = "\uFFFC";
461
+ function normalizeDictatedText(transcript) {
462
+ return transcript.replace(/\s+/g, " ").trim();
463
+ }
464
+ function needsLeadingSpace(charBefore, text) {
465
+ if (charBefore === null || charBefore === "") return false;
466
+ if (/\s/.test(charBefore)) return false;
467
+ if (/^[.,;:!?…)\]}]/.test(text)) return false;
468
+ return true;
469
+ }
470
+ function needsTrailingSpace(charAfter) {
471
+ if (charAfter === null || charAfter === "") return false;
472
+ if (/\s/.test(charAfter)) return false;
473
+ return !/^[.,;:!?…)\]}]/.test(charAfter);
474
+ }
475
+ function composeDictatedInsertion(charBefore, transcript, charAfter = null) {
476
+ const words = normalizeDictatedText(transcript);
477
+ if (!words) return null;
478
+ const lead = needsLeadingSpace(charBefore, words) ? " " : "";
479
+ const trail = needsTrailingSpace(charAfter) ? " " : "";
480
+ return { text: `${lead}${words}${trail}`, caret: lead.length + words.length };
481
+ }
482
+ function focusIsInOtherEditable() {
483
+ if (typeof document === "undefined") return false;
484
+ const active = document.activeElement;
485
+ if (!active || active === document.body) return false;
486
+ if (active instanceof HTMLInputElement || active instanceof HTMLTextAreaElement || active instanceof HTMLSelectElement) {
487
+ return true;
488
+ }
489
+ return active instanceof HTMLElement && active.isContentEditable;
490
+ }
491
+ function charBeforeInTiptap($pos) {
492
+ if ($pos.parentOffset === 0) return null;
493
+ const before = $pos.nodeBefore;
494
+ if (!before) return null;
495
+ if (before.isText) return before.text ? before.text.slice(-1) : null;
496
+ if (before.type.name === "hardBreak") return null;
497
+ return INLINE_OBJECT;
498
+ }
499
+ function charAfterInTiptap($pos) {
500
+ if ($pos.parentOffset === $pos.parent.content.size) return null;
501
+ const after = $pos.nodeAfter;
502
+ if (!after) return null;
503
+ if (after.isText) return after.text ? after.text.charAt(0) : null;
504
+ if (after.type.name === "hardBreak") return null;
505
+ return INLINE_OBJECT;
506
+ }
507
+ function insertDictationIntoTiptap(editor, transcript) {
508
+ if (editor.isDestroyed || !editor.isEditable) return false;
509
+ const { state } = editor;
510
+ let pos = state.selection.to;
511
+ let $pos = state.doc.resolve(pos);
512
+ if (!$pos.parent.inlineContent) {
513
+ const near = Selection.near($pos, 1);
514
+ if (!(near instanceof TextSelection)) return false;
515
+ pos = near.head;
516
+ $pos = near.$head;
517
+ }
518
+ const insertion = composeDictatedInsertion(
519
+ charBeforeInTiptap($pos),
520
+ transcript,
521
+ charAfterInTiptap($pos)
522
+ );
523
+ if (insertion === null) return false;
524
+ const alreadyFocused = editor.view.hasFocus();
525
+ const chain = alreadyFocused || focusIsInOtherEditable() ? editor.chain() : editor.chain().focus();
526
+ const inserted = chain.command(({ tr }) => {
527
+ closeHistory(tr);
528
+ tr.insertText(insertion.text, pos, pos);
529
+ tr.setSelection(TextSelection.create(tr.doc, pos + insertion.caret));
530
+ tr.scrollIntoView();
531
+ return true;
532
+ }).run();
533
+ if (inserted && !editor.isDestroyed) {
534
+ editor.view.dispatch(closeHistory(editor.state.tr).setMeta("addToHistory", false));
535
+ }
536
+ return inserted;
537
+ }
538
+ function insertDictationIntoMonaco(editor, transcript) {
539
+ const model = editor.getModel();
540
+ if (!model) return false;
541
+ const selection = editor.getSelection();
542
+ const caret = selection ? { lineNumber: selection.endLineNumber, column: selection.endColumn } : editor.getPosition();
543
+ if (!caret) return false;
544
+ const line = model.getLineContent(caret.lineNumber);
545
+ const charBefore = caret.column > 1 ? line.charAt(caret.column - 2) : null;
546
+ const charAfter = caret.column <= line.length ? line.charAt(caret.column - 1) : null;
547
+ const insertion = composeDictatedInsertion(charBefore, transcript, charAfter);
548
+ if (insertion === null) return false;
549
+ const range = {
550
+ startLineNumber: caret.lineNumber,
551
+ startColumn: caret.column,
552
+ endLineNumber: caret.lineNumber,
553
+ endColumn: caret.column
554
+ };
555
+ editor.pushUndoStop();
556
+ const applied = editor.executeEdits("squisq-dictation", [
557
+ { range, text: insertion.text, forceMoveMarkers: true }
558
+ ]);
559
+ editor.pushUndoStop();
560
+ if (applied === false) return false;
561
+ const end = { lineNumber: caret.lineNumber, column: caret.column + insertion.caret };
562
+ editor.setPosition(end);
563
+ editor.revealPositionInCenterIfOutsideViewport(end);
564
+ if (!editor.hasTextFocus() && !focusIsInOtherEditable()) editor.focus();
565
+ return true;
566
+ }
567
+
568
+ // src/speech/DictationExtension.ts
569
+ import { Extension } from "@tiptap/core";
570
+ import { Plugin, PluginKey } from "@tiptap/pm/state";
571
+ import { Decoration, DecorationSet } from "@tiptap/pm/view";
572
+ var DICTATION_KEY = new PluginKey("squisq-dictation-interim");
573
+ var DICTATION_INTERIM_CLASS = "squisq-dictation-interim";
574
+ function interimElement(doc, label) {
575
+ const span = doc.createElement("span");
576
+ span.className = DICTATION_INTERIM_CLASS;
577
+ span.setAttribute("contenteditable", "false");
578
+ span.setAttribute("aria-hidden", "true");
579
+ span.dataset.squisqDictation = "interim";
580
+ span.textContent = label;
581
+ return span;
582
+ }
583
+ var DictationExtension = Extension.create({
584
+ name: "squisqDictation",
585
+ addProseMirrorPlugins() {
586
+ return [
587
+ new Plugin({
588
+ key: DICTATION_KEY,
589
+ state: {
590
+ init: () => ({ label: null }),
591
+ apply: (transaction, previous) => transaction.getMeta(DICTATION_KEY) ?? previous
592
+ },
593
+ props: {
594
+ decorations: (state) => {
595
+ const label = DICTATION_KEY.getState(state)?.label;
596
+ if (!label) return null;
597
+ return DecorationSet.create(state.doc, [
598
+ Decoration.widget(
599
+ state.selection.to,
600
+ (view) => interimElement(view.dom.ownerDocument, label),
601
+ {
602
+ // Same key ⇒ ProseMirror reuses the DOM node as the caret moves.
603
+ key: `squisq-dictation:${label}`,
604
+ // Drawn after the caret, so typed or dictated text lands before it.
605
+ side: 1,
606
+ ignoreSelection: true
607
+ }
608
+ )
609
+ ]);
610
+ }
611
+ }
612
+ })
613
+ ];
614
+ }
615
+ });
616
+ function setDictationInterim(editor, label) {
617
+ if (editor.isDestroyed || !editor.state) return;
618
+ const current = DICTATION_KEY.getState(editor.state);
619
+ if (!current || current.label === label) return;
620
+ editor.view.dispatch(
621
+ editor.state.tr.setMeta(DICTATION_KEY, { label }).setMeta("addToHistory", false)
622
+ );
623
+ }
624
+
625
+ // src/speech/monacoDictationInterim.ts
626
+ function createMonacoDictationInterim(editor) {
627
+ const collection = editor.createDecorationsCollection();
628
+ let label = null;
629
+ let cursorListener = null;
630
+ const render = () => {
631
+ const selection = editor.getSelection();
632
+ if (!label || !selection) {
633
+ collection.clear();
634
+ return;
635
+ }
636
+ const lineNumber = selection.endLineNumber;
637
+ const column = selection.endColumn;
638
+ collection.set([
639
+ {
640
+ range: {
641
+ startLineNumber: lineNumber,
642
+ startColumn: column,
643
+ endLineNumber: lineNumber,
644
+ endColumn: column
645
+ },
646
+ options: {
647
+ showIfCollapsed: true,
648
+ after: {
649
+ content: label,
650
+ inlineClassName: `${DICTATION_INTERIM_CLASS} ${DICTATION_INTERIM_CLASS}--source`
651
+ }
652
+ }
653
+ }
654
+ ]);
655
+ };
656
+ const safely = (action) => {
657
+ try {
658
+ action();
659
+ } catch {
660
+ }
661
+ };
662
+ return {
663
+ update(next) {
664
+ label = next;
665
+ safely(() => {
666
+ if (label && !cursorListener) cursorListener = editor.onDidChangeCursorSelection(render);
667
+ if (!label && cursorListener) {
668
+ cursorListener.dispose();
669
+ cursorListener = null;
670
+ }
671
+ render();
672
+ });
673
+ },
674
+ dispose() {
675
+ label = null;
676
+ safely(() => {
677
+ cursorListener?.dispose();
678
+ cursorListener = null;
679
+ collection.clear();
680
+ });
681
+ }
682
+ };
683
+ }
684
+
685
+ // src/speech/dictationErrors.ts
686
+ function humanizeDictationError(caught) {
687
+ const error = caught instanceof Error ? caught : new Error(String(caught));
688
+ const name = error.name;
689
+ const message = error.message || String(caught);
690
+ if (name === "NotAllowedError" || name === "SecurityError" || /permission|not allowed|denied/i.test(message)) {
691
+ return "Microphone access was not granted. Allow it in your system settings, then try again.";
692
+ }
693
+ if (name === "OverconstrainedError" || /constraint|selected microphone/i.test(message)) {
694
+ return "The selected microphone is unavailable. Choose another microphone, then try again.";
695
+ }
696
+ if (name === "NotFoundError" || /requested device not found|no microphone/i.test(message)) {
697
+ return "No microphone is available.";
698
+ }
699
+ if (name === "NotReadableError" || /could not start audio source/i.test(message)) {
700
+ return "The microphone is in use by another application.";
701
+ }
702
+ if (/getUserMedia is not available|MediaRecorder is not defined/i.test(message)) {
703
+ return "Voice input is unavailable in this browser.";
704
+ }
705
+ return `Dictation stopped: ${message}`;
706
+ }
707
+ var NO_SPEECH_DETECTED_MESSAGE = "No speech was detected, so dictation stopped. Check that the right microphone is selected.";
708
+ function setupRequiredMessage(reason) {
709
+ return reason ? `Dictation needs a one-time setup first: ${reason}` : "Dictation needs a one-time setup before it can start.";
710
+ }
711
+
712
+ // src/speech/useDictationController.ts
713
+ var DICTATION_METER_BARS = 12;
714
+ var EMPTY_LEVELS = Object.freeze(
715
+ Array.from({ length: DICTATION_METER_BARS }, () => 0)
716
+ );
717
+ function normalizeMeterLevel(level) {
718
+ return Math.min(1, Math.max(0, level * 14));
719
+ }
720
+ function createLevelStore() {
721
+ let levels = EMPTY_LEVELS;
722
+ const listeners = /* @__PURE__ */ new Set();
723
+ const notify = () => {
724
+ for (const listener of listeners) listener();
725
+ };
726
+ return {
727
+ subscribe(listener) {
728
+ listeners.add(listener);
729
+ return () => {
730
+ listeners.delete(listener);
731
+ };
732
+ },
733
+ getSnapshot: () => levels,
734
+ push(level) {
735
+ levels = [...levels.slice(1), normalizeMeterLevel(level)];
736
+ notify();
737
+ },
738
+ reset() {
739
+ if (levels === EMPTY_LEVELS) return;
740
+ levels = EMPTY_LEVELS;
741
+ notify();
742
+ }
743
+ };
744
+ }
745
+ var DictationStateContext = createContext(null);
746
+ function useDictationState() {
747
+ return useContext(DictationStateContext);
748
+ }
749
+ function dictationInterimText(status, pendingTakes) {
750
+ switch (status) {
751
+ case "preparing":
752
+ return "Starting\u2026";
753
+ case "listening":
754
+ return pendingTakes > 0 ? "Transcribing\u2026" : "Listening\u2026";
755
+ case "transcribing":
756
+ return "Transcribing\u2026";
757
+ default:
758
+ return null;
759
+ }
760
+ }
761
+ function isTextView(view) {
762
+ return view === "wysiwyg" || view === "raw";
763
+ }
764
+ function useDictationController({
765
+ capability,
766
+ activeView,
767
+ editorMode,
768
+ tiptapEditor,
769
+ monacoEditor
770
+ }) {
771
+ const [provider, setProvider] = useState2(null);
772
+ const [readiness, setReadiness] = useState2(null);
773
+ const [status, setStatus] = useState2("idle");
774
+ const [pendingTakes, setPendingTakes] = useState2(0);
775
+ const [error, setError] = useState2(null);
776
+ const levelsRef = useRef(null);
777
+ if (!levelsRef.current) levelsRef.current = createLevelStore();
778
+ const levels = levelsRef.current;
779
+ const providerRef = useRef(provider);
780
+ providerRef.current = provider;
781
+ const readinessRef = useRef(readiness);
782
+ readinessRef.current = readiness;
783
+ const activeViewRef = useRef(activeView);
784
+ activeViewRef.current = activeView;
785
+ const tiptapRef = useRef(tiptapEditor);
786
+ tiptapRef.current = tiptapEditor;
787
+ const monacoRef = useRef(monacoEditor);
788
+ monacoRef.current = monacoEditor;
789
+ const sessionRef = useRef(null);
790
+ const heldPhrasesRef = useRef([]);
791
+ const textViewActive = editorMode === "markdown" && isTextView(activeView);
792
+ const available = provider !== null && readiness !== null && readiness.state !== "unavailable" && textViewActive;
793
+ const availableRef = useRef(available);
794
+ availableRef.current = available;
795
+ const restoreEditorFocus = useCallback2(() => {
796
+ if (typeof document === "undefined") return;
797
+ const active = document.activeElement;
798
+ const reclaimable = !active || active === document.body || active instanceof Element && active.closest(".squisq-dictation") !== null;
799
+ if (!reclaimable) return;
800
+ const view = activeViewRef.current;
801
+ const tiptap = tiptapRef.current;
802
+ if (view === "wysiwyg" && tiptap && !tiptap.isDestroyed) {
803
+ tiptap.commands.focus();
804
+ return;
805
+ }
806
+ const monaco = monacoRef.current;
807
+ if (view === "raw" && monaco) monaco.focus();
808
+ }, []);
809
+ const endSession = useCallback2(
810
+ (session, options = {}) => {
811
+ if (sessionRef.current !== session) return;
812
+ sessionRef.current = null;
813
+ session.abort.abort();
814
+ levels.reset();
815
+ setPendingTakes(0);
816
+ setStatus("idle");
817
+ if (options.restoreFocus) restoreEditorFocus();
818
+ },
819
+ [levels, restoreEditorFocus]
820
+ );
821
+ const cancel = useCallback2(() => {
822
+ const session = sessionRef.current;
823
+ if (!session) return;
824
+ session.handle?.cancel();
825
+ endSession(session, { restoreFocus: true });
826
+ }, [endSession]);
827
+ const stop = useCallback2(async () => {
828
+ const session = sessionRef.current;
829
+ if (!session) return;
830
+ if (!session.handle) {
831
+ cancel();
832
+ return;
833
+ }
834
+ if (session.stopping) return;
835
+ session.stopping = true;
836
+ setStatus("transcribing");
837
+ try {
838
+ await session.handle.stop();
839
+ } finally {
840
+ endSession(session, { restoreFocus: true });
841
+ }
842
+ }, [cancel, endSession]);
843
+ const insertPhrase = useCallback2((text) => {
844
+ const view = activeViewRef.current;
845
+ const tiptap = tiptapRef.current;
846
+ if (view === "wysiwyg" && tiptap && !tiptap.isDestroyed) {
847
+ insertDictationIntoTiptap(tiptap, text);
848
+ return true;
849
+ }
850
+ const monaco = monacoRef.current;
851
+ if (view === "raw" && monaco) {
852
+ insertDictationIntoMonaco(monaco, text);
853
+ return true;
854
+ }
855
+ return false;
856
+ }, []);
857
+ const start = useCallback2(async () => {
858
+ const active = providerRef.current;
859
+ if (!active || sessionRef.current) return;
860
+ const session = { abort: new AbortController(), handle: null, stopping: false };
861
+ sessionRef.current = session;
862
+ const isCurrent = () => sessionRef.current === session;
863
+ setError(null);
864
+ setPendingTakes(0);
865
+ levels.reset();
866
+ setStatus("preparing");
867
+ try {
868
+ const fresh = await active.status();
869
+ if (!isCurrent()) return;
870
+ setReadiness(fresh);
871
+ if (fresh.state === "download-required" || fresh.state === "unavailable") {
872
+ endSession(session);
873
+ if (fresh.state === "download-required") {
874
+ if (active.requestSetup) active.requestSetup();
875
+ else setError(setupRequiredMessage(fresh.reason));
876
+ }
877
+ return;
878
+ }
879
+ const { startDictationSession } = await import("./dictationSession-PN3OT5LJ.js");
880
+ if (!isCurrent()) return;
881
+ const handle = await startDictationSession({
882
+ provider: active,
883
+ signal: session.abort.signal,
884
+ onTranscript: (text) => {
885
+ if (!isCurrent()) return;
886
+ if (!insertPhrase(text)) heldPhrasesRef.current.push(text);
887
+ },
888
+ onAudioLevel: (level) => {
889
+ if (isCurrent()) levels.push(level);
890
+ },
891
+ onPendingChange: (pending) => {
892
+ if (isCurrent()) setPendingTakes(pending);
893
+ },
894
+ onLongPause: (hadTranscript) => {
895
+ if (!isCurrent()) return;
896
+ if (!hadTranscript) setError(NO_SPEECH_DETECTED_MESSAGE);
897
+ void stop();
898
+ },
899
+ onError: (caught) => {
900
+ if (!isCurrent()) return;
901
+ session.handle?.cancel();
902
+ endSession(session, { restoreFocus: true });
903
+ setError(humanizeDictationError(caught));
904
+ }
905
+ });
906
+ if (!isCurrent()) {
907
+ handle.cancel();
908
+ return;
909
+ }
910
+ session.handle = handle;
911
+ setStatus("listening");
912
+ } catch (caught) {
913
+ if (!isCurrent()) return;
914
+ endSession(session, { restoreFocus: true });
915
+ setError(humanizeDictationError(caught));
916
+ }
917
+ }, [endSession, insertPhrase, levels, stop]);
918
+ const toggle = useCallback2(() => {
919
+ const session = sessionRef.current;
920
+ if (session) {
921
+ void stop();
922
+ return;
923
+ }
924
+ const active = providerRef.current;
925
+ if (!active || !availableRef.current) return;
926
+ const known = readinessRef.current;
927
+ if (known?.state === "download-required") {
928
+ setError(null);
929
+ if (active.requestSetup) active.requestSetup();
930
+ else setError(setupRequiredMessage(known.reason));
931
+ active.status().then(
932
+ (next) => {
933
+ if (providerRef.current === active) setReadiness(next);
934
+ },
935
+ () => void 0
936
+ );
937
+ return;
938
+ }
939
+ void start();
940
+ }, [start, stop]);
941
+ const dismissError = useCallback2(() => setError(null), []);
942
+ useEffect(() => {
943
+ if (!capability) {
944
+ setProvider(null);
945
+ setReadiness(null);
946
+ return;
947
+ }
948
+ const owned = isSpeechInputProviderFactory(capability);
949
+ const instance = resolveSpeechInputProvider(capability);
950
+ let alive = true;
951
+ setProvider(instance);
952
+ setReadiness(null);
953
+ instance.status().then(
954
+ (next) => {
955
+ if (alive) setReadiness(next);
956
+ },
957
+ (caught) => {
958
+ if (!alive) return;
959
+ const reason = caught instanceof Error ? caught.message : String(caught);
960
+ setReadiness({ state: "unavailable", reason });
961
+ }
962
+ );
963
+ const unsubscribe = instance.onStatus?.((next) => {
964
+ if (alive) setReadiness(next);
965
+ });
966
+ return () => {
967
+ alive = false;
968
+ unsubscribe?.();
969
+ const session = sessionRef.current;
970
+ if (session) {
971
+ session.handle?.cancel();
972
+ sessionRef.current = null;
973
+ session.abort.abort();
974
+ }
975
+ setStatus("idle");
976
+ setPendingTakes(0);
977
+ if (owned) instance.dispose?.();
978
+ };
979
+ }, [capability]);
980
+ useEffect(
981
+ () => () => {
982
+ const session = sessionRef.current;
983
+ if (!session) return;
984
+ session.handle?.cancel();
985
+ sessionRef.current = null;
986
+ session.abort.abort();
987
+ },
988
+ []
989
+ );
990
+ useEffect(() => {
991
+ if (!textViewActive && sessionRef.current) void stop();
992
+ }, [textViewActive, stop]);
993
+ const sessionActive = status !== "idle";
994
+ useEffect(() => {
995
+ if (!sessionActive || typeof window === "undefined") return;
996
+ const onKeyDown = (event) => {
997
+ if (event.key !== "Escape" || event.defaultPrevented) return;
998
+ void stop();
999
+ };
1000
+ window.addEventListener("keydown", onKeyDown);
1001
+ return () => window.removeEventListener("keydown", onKeyDown);
1002
+ }, [sessionActive, stop]);
1003
+ useEffect(() => {
1004
+ if (heldPhrasesRef.current.length === 0) return;
1005
+ const held = heldPhrasesRef.current;
1006
+ heldPhrasesRef.current = [];
1007
+ for (let index = 0; index < held.length; index += 1) {
1008
+ if (!insertPhrase(held[index])) {
1009
+ heldPhrasesRef.current = held.slice(index);
1010
+ return;
1011
+ }
1012
+ }
1013
+ }, [activeView, tiptapEditor, monacoEditor, insertPhrase]);
1014
+ const hasCapability = capability !== null;
1015
+ const interimLabel = hasCapability ? dictationInterimText(status, pendingTakes) : null;
1016
+ useEffect(() => {
1017
+ if (!tiptapEditor || !hasCapability) return;
1018
+ setDictationInterim(tiptapEditor, activeView === "wysiwyg" ? interimLabel : null);
1019
+ }, [tiptapEditor, hasCapability, activeView, interimLabel]);
1020
+ useEffect(() => {
1021
+ if (!tiptapEditor || !hasCapability) return;
1022
+ return () => setDictationInterim(tiptapEditor, null);
1023
+ }, [tiptapEditor, hasCapability]);
1024
+ const monacoInterimRef = useRef(null);
1025
+ useEffect(() => {
1026
+ if (!monacoEditor) return;
1027
+ return () => {
1028
+ monacoInterimRef.current?.dispose();
1029
+ monacoInterimRef.current = null;
1030
+ };
1031
+ }, [monacoEditor]);
1032
+ useEffect(() => {
1033
+ if (!monacoEditor) return;
1034
+ const label = activeView === "raw" ? interimLabel : null;
1035
+ if (!label && !monacoInterimRef.current) return;
1036
+ if (!monacoInterimRef.current)
1037
+ monacoInterimRef.current = createMonacoDictationInterim(monacoEditor);
1038
+ monacoInterimRef.current.update(label);
1039
+ }, [monacoEditor, activeView, interimLabel]);
1040
+ const control = useMemo2(
1041
+ () => hasCapability ? { active: sessionActive, available, toggle } : null,
1042
+ [hasCapability, sessionActive, available, toggle]
1043
+ );
1044
+ const providerLabel = provider?.label ?? "";
1045
+ const state = useMemo2(
1046
+ () => hasCapability ? {
1047
+ status,
1048
+ readiness,
1049
+ available,
1050
+ providerLabel,
1051
+ pendingTakes,
1052
+ error,
1053
+ levels,
1054
+ toggle,
1055
+ dismissError
1056
+ } : null,
1057
+ [
1058
+ hasCapability,
1059
+ status,
1060
+ readiness,
1061
+ available,
1062
+ providerLabel,
1063
+ pendingTakes,
1064
+ error,
1065
+ levels,
1066
+ toggle,
1067
+ dismissError
1068
+ ]
1069
+ );
1070
+ return { control, state };
1071
+ }
1072
+
1073
+ // src/EditorContext.tsx
272
1074
  import {
273
1075
  DocumentVersionManager
274
1076
  } from "@bendyline/squisq/versions";
1077
+ import { createDocument } from "@tiptap/core";
1078
+ import { closeHistory as closeHistory2 } from "@tiptap/pm/history";
1079
+
1080
+ // src/blockInsertion.ts
1081
+ import { isContainerTemplate as isContainerTemplate2 } from "@bendyline/squisq/doc";
1082
+ function tiptapOutline(doc) {
1083
+ const blocks = [];
1084
+ const positions = [];
1085
+ doc.forEach((node, offset) => {
1086
+ positions.push(offset);
1087
+ if (node.type.name === "heading") {
1088
+ const attrs = node.attrs;
1089
+ blocks.push({
1090
+ headingDepth: attrs.level ?? 1,
1091
+ container: isContainerTemplate2(attrs.dataTemplate ?? void 0)
1092
+ });
1093
+ } else {
1094
+ blocks.push({ headingDepth: null, container: false });
1095
+ }
1096
+ });
1097
+ return { blocks, positions };
1098
+ }
1099
+ function tiptapBlockInsertion(state, placement) {
1100
+ const { blocks, positions } = tiptapOutline(state.doc);
1101
+ const { $to } = state.selection;
1102
+ const cursorIndex = $to.depth > 0 ? $to.index(0) : $to.index(0) - 1;
1103
+ const { index, ...context } = resolveOutlineInsertion(blocks, cursorIndex, placement);
1104
+ const pos = index < positions.length ? positions[index] ?? 0 : state.doc.content.size;
1105
+ return { pos, ...context };
1106
+ }
275
1107
 
276
1108
  // src/mediaEdit/mediaEditTargets.ts
277
1109
  import { collectDocMediaClips } from "@bendyline/squisq/mediaEdit";
@@ -569,16 +1401,16 @@ function restoreWysiwygScrollRatio(editor, ratio) {
569
1401
 
570
1402
  // src/EditorContext.tsx
571
1403
  import { jsx } from "react/jsx-runtime";
572
- var EditorContext = createContext(null);
1404
+ var EditorContext = createContext2(null);
573
1405
  function useEditorContext() {
574
- const ctx = useContext(EditorContext);
1406
+ const ctx = useContext2(EditorContext);
575
1407
  if (!ctx) {
576
1408
  throw new Error("useEditorContext must be used within an <EditorProvider>");
577
1409
  }
578
1410
  return ctx;
579
1411
  }
580
1412
  function useEditorContextOptional() {
581
- return useContext(EditorContext);
1413
+ return useContext2(EditorContext);
582
1414
  }
583
1415
  var MEDIA_EDIT_GC_DELAY_MS = 5e3;
584
1416
  var DEFAULT_PRUNE_POLICY = { type: "keep-last-n", n: 50 };
@@ -608,6 +1440,7 @@ function EditorProvider({
608
1440
  proofingSpellingEnabled = true,
609
1441
  proofingGrammarEnabled = true,
610
1442
  proofingIgnoreStore = null,
1443
+ speechInput = null,
611
1444
  documentLinkProvider = null,
612
1445
  fenceRenderers = null,
613
1446
  onCopyCode,
@@ -640,20 +1473,22 @@ function EditorProvider({
640
1473
  );
641
1474
  const effectiveThemeInheritance = viewPreferences?.themeInheritance ?? themeInheritance;
642
1475
  const effectiveLayoutMode = viewPreferences?.layoutMode ?? layoutMode;
643
- const { mode: fileMode, language: resolvedLanguage } = useMemo2(
1476
+ const { mode: fileMode, language: resolvedLanguage } = useMemo3(
644
1477
  () => resolveFileKind(fileName, language),
645
1478
  [fileName, language]
646
1479
  );
647
1480
  const editorMode = fileMode === "data" ? "code" : fileMode;
648
- const [markdownSource, setMarkdownSourceRaw] = useState2(initialMarkdown);
649
- const [markdownDoc, setMarkdownDocState] = useState2(null);
650
- const [doc, setDoc] = useState2(null);
651
- const [activeView, setActiveViewRaw] = useState2(
1481
+ const [markdownSource, setMarkdownSourceRaw] = useState3(initialMarkdown);
1482
+ const markdownSourceRef = useRef2(markdownSource);
1483
+ markdownSourceRef.current = markdownSource;
1484
+ const [markdownDoc, setMarkdownDocState] = useState3(null);
1485
+ const [doc, setDoc] = useState3(null);
1486
+ const [activeView, setActiveViewRaw] = useState3(
652
1487
  editorMode === "markdown" ? initialView : "raw"
653
1488
  );
654
- const [uncontrolledFindMode, setUncontrolledFindMode] = useState2(false);
1489
+ const [uncontrolledFindMode, setUncontrolledFindMode] = useState3(false);
655
1490
  const findMode = editorMode !== "image" && (controlledFindMode ?? uncontrolledFindMode);
656
- const setFindMode = useCallback2(
1491
+ const setFindMode = useCallback3(
657
1492
  (active) => {
658
1493
  if (editorMode === "image") return;
659
1494
  if (controlledFindMode === void 0) setUncontrolledFindMode(active);
@@ -661,12 +1496,12 @@ function EditorProvider({
661
1496
  },
662
1497
  [controlledFindMode, editorMode, onFindModeChange]
663
1498
  );
664
- const activeViewRef = useRef(activeView);
1499
+ const activeViewRef = useRef2(activeView);
665
1500
  activeViewRef.current = activeView;
666
- const tiptapEditorRef = useRef(null);
667
- const monacoEditorRef = useRef(null);
668
- const pendingEditorScrollRef = useRef(null);
669
- const setActiveView = useCallback2(
1501
+ const tiptapEditorRef = useRef2(null);
1502
+ const monacoEditorRef = useRef2(null);
1503
+ const pendingEditorScrollRef = useRef2(null);
1504
+ const setActiveView = useCallback3(
670
1505
  (view) => {
671
1506
  if (editorMode === "code" && view !== "raw") return;
672
1507
  if (editorMode === "image") return;
@@ -686,59 +1521,59 @@ function EditorProvider({
686
1521
  },
687
1522
  [editorMode]
688
1523
  );
689
- const [parseError, setParseError] = useState2(null);
690
- const [isParsing, setIsParsing] = useState2(false);
691
- const [colorScheme, setColorScheme] = useState2(initialColorScheme);
692
- const [inlinePreviewVisible, setInlinePreviewVisibleRaw] = useState2(effectiveInlinePreview);
693
- useEffect(() => {
1524
+ const [parseError, setParseError] = useState3(null);
1525
+ const [isParsing, setIsParsing] = useState3(false);
1526
+ const [colorScheme, setColorScheme] = useState3(initialColorScheme);
1527
+ const [inlinePreviewVisible, setInlinePreviewVisibleRaw] = useState3(effectiveInlinePreview);
1528
+ useEffect2(() => {
694
1529
  if (viewPreferences?.inlinePreview === void 0) setInlinePreviewVisibleRaw(inlinePreview);
695
1530
  }, [inlinePreview, viewPreferences?.inlinePreview]);
696
- const [statusBarVisible, setStatusBarVisibleRaw] = useState2(effectiveShowStatusBar);
697
- useEffect(() => {
1531
+ const [statusBarVisible, setStatusBarVisibleRaw] = useState3(effectiveShowStatusBar);
1532
+ useEffect2(() => {
698
1533
  if (viewPreferences?.showStatusBar === void 0) setStatusBarVisibleRaw(showStatusBar);
699
1534
  }, [showStatusBar, viewPreferences?.showStatusBar]);
700
- const [outlineVisible, setOutlineVisibleRaw] = useState2(effectiveOutline);
701
- useEffect(() => {
1535
+ const [outlineVisible, setOutlineVisibleRaw] = useState3(effectiveOutline);
1536
+ useEffect2(() => {
702
1537
  if (viewPreferences?.outline === void 0) setOutlineVisibleRaw(outline);
703
1538
  }, [outline, viewPreferences?.outline]);
704
- const [blockTagVisibilityState, setBlockTagVisibilityRaw] = useState2(
1539
+ const [blockTagVisibilityState, setBlockTagVisibilityRaw] = useState3(
705
1540
  effectiveBlockTagVisibility
706
1541
  );
707
1542
  const blockTagsVisible = blockTagVisibilityState !== "none";
708
- useEffect(() => {
1543
+ useEffect2(() => {
709
1544
  if (viewPreferences?.blockTagVisibility === void 0 && viewPreferences?.blockTags === void 0) {
710
1545
  setBlockTagVisibilityRaw(normalizeBlockTagVisibility(initialBlockTagVisibility ?? blockTags));
711
1546
  }
712
1547
  }, [blockTags, initialBlockTagVisibility, viewPreferences]);
713
- const [themeInheritanceState, setThemeInheritanceRaw] = useState2(effectiveThemeInheritance);
714
- useEffect(() => {
1548
+ const [themeInheritanceState, setThemeInheritanceRaw] = useState3(effectiveThemeInheritance);
1549
+ useEffect2(() => {
715
1550
  if (viewPreferences?.themeInheritance === void 0) {
716
1551
  setThemeInheritanceRaw(themeInheritance);
717
1552
  }
718
1553
  }, [themeInheritance, viewPreferences?.themeInheritance]);
719
- const [layoutModeState, setLayoutModeRaw] = useState2(effectiveLayoutMode);
720
- useEffect(() => {
1554
+ const [layoutModeState, setLayoutModeRaw] = useState3(effectiveLayoutMode);
1555
+ useEffect2(() => {
721
1556
  if (viewPreferences?.layoutMode === void 0) setLayoutModeRaw(layoutMode);
722
1557
  }, [layoutMode, viewPreferences?.layoutMode]);
723
- const [imageEditTarget, setImageEditTarget] = useState2(null);
724
- const [mediaRevision, setMediaRevision] = useState2(0);
725
- const openImageEdit = useCallback2((relativePath) => {
1558
+ const [imageEditTarget, setImageEditTarget] = useState3(null);
1559
+ const [mediaRevision, setMediaRevision] = useState3(0);
1560
+ const openImageEdit = useCallback3((relativePath) => {
726
1561
  setImageEditTarget(relativePath);
727
1562
  }, []);
728
- const closeImageEdit = useCallback2(() => {
1563
+ const closeImageEdit = useCallback3(() => {
729
1564
  setImageEditTarget(null);
730
1565
  }, []);
731
- const [mediaEditTarget, setMediaEditTarget] = useState2(null);
732
- const openMediaEdit = useCallback2((target) => {
1566
+ const [mediaEditTarget, setMediaEditTarget] = useState3(null);
1567
+ const openMediaEdit = useCallback3((target) => {
733
1568
  setMediaEditTarget(target);
734
1569
  }, []);
735
- const closeMediaEdit = useCallback2(() => {
1570
+ const closeMediaEdit = useCallback3(() => {
736
1571
  setMediaEditTarget(null);
737
1572
  }, []);
738
- const bumpMediaRevision = useCallback2(() => {
1573
+ const bumpMediaRevision = useCallback3(() => {
739
1574
  setMediaRevision((n) => n + 1);
740
1575
  }, []);
741
- useEffect(() => {
1576
+ useEffect2(() => {
742
1577
  if (!viewPreferences) return;
743
1578
  if (viewPreferences.inlinePreview !== void 0) {
744
1579
  setInlinePreviewVisibleRaw(viewPreferences.inlinePreview);
@@ -761,21 +1596,21 @@ function EditorProvider({
761
1596
  setLayoutModeRaw(viewPreferences.layoutMode);
762
1597
  }
763
1598
  }, [viewPreferences]);
764
- const onViewPreferencesChangeRef = useRef(onViewPreferencesChange);
1599
+ const onViewPreferencesChangeRef = useRef2(onViewPreferencesChange);
765
1600
  onViewPreferencesChangeRef.current = onViewPreferencesChange;
766
- const inlinePreviewRef = useRef(inlinePreviewVisible);
1601
+ const inlinePreviewRef = useRef2(inlinePreviewVisible);
767
1602
  inlinePreviewRef.current = inlinePreviewVisible;
768
- const statusBarRef = useRef(statusBarVisible);
1603
+ const statusBarRef = useRef2(statusBarVisible);
769
1604
  statusBarRef.current = statusBarVisible;
770
- const outlineRef = useRef(outlineVisible);
1605
+ const outlineRef = useRef2(outlineVisible);
771
1606
  outlineRef.current = outlineVisible;
772
- const blockTagVisibilityRef = useRef(blockTagVisibilityState);
1607
+ const blockTagVisibilityRef = useRef2(blockTagVisibilityState);
773
1608
  blockTagVisibilityRef.current = blockTagVisibilityState;
774
- const themeInheritanceRef = useRef(themeInheritanceState);
1609
+ const themeInheritanceRef = useRef2(themeInheritanceState);
775
1610
  themeInheritanceRef.current = themeInheritanceState;
776
- const layoutModeRef = useRef(layoutModeState);
1611
+ const layoutModeRef = useRef2(layoutModeState);
777
1612
  layoutModeRef.current = layoutModeState;
778
- const setInlinePreviewVisible = useCallback2((visible) => {
1613
+ const setInlinePreviewVisible = useCallback3((visible) => {
779
1614
  setInlinePreviewVisibleRaw(visible);
780
1615
  onViewPreferencesChangeRef.current?.({
781
1616
  inlinePreview: visible,
@@ -787,7 +1622,7 @@ function EditorProvider({
787
1622
  layoutMode: layoutModeRef.current
788
1623
  });
789
1624
  }, []);
790
- const setStatusBarVisible = useCallback2((visible) => {
1625
+ const setStatusBarVisible = useCallback3((visible) => {
791
1626
  setStatusBarVisibleRaw(visible);
792
1627
  onViewPreferencesChangeRef.current?.({
793
1628
  inlinePreview: inlinePreviewRef.current,
@@ -799,7 +1634,7 @@ function EditorProvider({
799
1634
  layoutMode: layoutModeRef.current
800
1635
  });
801
1636
  }, []);
802
- const setOutlineVisible = useCallback2((visible) => {
1637
+ const setOutlineVisible = useCallback3((visible) => {
803
1638
  setOutlineVisibleRaw(visible);
804
1639
  onViewPreferencesChangeRef.current?.({
805
1640
  inlinePreview: inlinePreviewRef.current,
@@ -811,7 +1646,7 @@ function EditorProvider({
811
1646
  layoutMode: layoutModeRef.current
812
1647
  });
813
1648
  }, []);
814
- const setBlockTagVisibility = useCallback2((visibility) => {
1649
+ const setBlockTagVisibility = useCallback3((visibility) => {
815
1650
  setBlockTagVisibilityRaw(visibility);
816
1651
  onViewPreferencesChangeRef.current?.({
817
1652
  inlinePreview: inlinePreviewRef.current,
@@ -823,11 +1658,11 @@ function EditorProvider({
823
1658
  layoutMode: layoutModeRef.current
824
1659
  });
825
1660
  }, []);
826
- const setBlockTagsVisible = useCallback2(
1661
+ const setBlockTagsVisible = useCallback3(
827
1662
  (visible) => setBlockTagVisibility(visible ? "always" : "none"),
828
1663
  [setBlockTagVisibility]
829
1664
  );
830
- const setThemeInheritance = useCallback2((mode) => {
1665
+ const setThemeInheritance = useCallback3((mode) => {
831
1666
  setThemeInheritanceRaw(mode);
832
1667
  onViewPreferencesChangeRef.current?.({
833
1668
  inlinePreview: inlinePreviewRef.current,
@@ -839,7 +1674,7 @@ function EditorProvider({
839
1674
  layoutMode: layoutModeRef.current
840
1675
  });
841
1676
  }, []);
842
- const setLayoutMode = useCallback2((mode) => {
1677
+ const setLayoutMode = useCallback3((mode) => {
843
1678
  setLayoutModeRaw(mode);
844
1679
  onViewPreferencesChangeRef.current?.({
845
1680
  inlinePreview: inlinePreviewRef.current,
@@ -851,17 +1686,17 @@ function EditorProvider({
851
1686
  layoutMode: mode
852
1687
  });
853
1688
  }, []);
854
- const [tiptapEditor, setTiptapEditorRaw] = useState2(null);
855
- const [monacoEditor, setMonacoEditorRaw] = useState2(null);
856
- const setTiptapEditor = useCallback2((editor) => {
1689
+ const [tiptapEditor, setTiptapEditorRaw] = useState3(null);
1690
+ const [monacoEditor, setMonacoEditorRaw] = useState3(null);
1691
+ const setTiptapEditor = useCallback3((editor) => {
857
1692
  tiptapEditorRef.current = editor;
858
1693
  setTiptapEditorRaw(editor);
859
1694
  }, []);
860
- const setMonacoEditor = useCallback2((editor) => {
1695
+ const setMonacoEditor = useCallback3((editor) => {
861
1696
  monacoEditorRef.current = editor;
862
1697
  setMonacoEditorRaw(editor);
863
1698
  }, []);
864
- useEffect(() => {
1699
+ useEffect2(() => {
865
1700
  const pending = pendingEditorScrollRef.current;
866
1701
  if (!pending || pending.view !== activeView) return;
867
1702
  const restore = activeView === "raw" && monacoEditor ? () => restoreMonacoScrollRatio(monacoEditor, pending.ratio) : activeView === "wysiwyg" && tiptapEditor ? () => restoreWysiwygScrollRatio(tiptapEditor, pending.ratio) : null;
@@ -885,22 +1720,22 @@ function EditorProvider({
885
1720
  clearTimeout(timeout);
886
1721
  };
887
1722
  }, [activeView, monacoEditor, tiptapEditor]);
888
- const [activeSceneText, setActiveSceneText] = useState2(null);
889
- const sceneTextChannel = useMemo2(createSceneTextChannel, []);
890
- useEffect(() => sceneTextChannel.subscribe(setActiveSceneText), [sceneTextChannel]);
891
- const articleIdRef = useRef(articleId);
1723
+ const [activeSceneText, setActiveSceneText] = useState3(null);
1724
+ const sceneTextChannel = useMemo3(createSceneTextChannel, []);
1725
+ useEffect2(() => sceneTextChannel.subscribe(setActiveSceneText), [sceneTextChannel]);
1726
+ const articleIdRef = useRef2(articleId);
892
1727
  articleIdRef.current = articleId;
893
- const fileNameRef = useRef(fileName);
1728
+ const fileNameRef = useRef2(fileName);
894
1729
  fileNameRef.current = fileName;
895
- useEffect(() => {
1730
+ useEffect2(() => {
896
1731
  setColorScheme(initialColorScheme);
897
1732
  }, [initialColorScheme]);
898
- const parseTimeoutRef = useRef(null);
899
- const skipInitialDebouncedParseRef = useRef(Boolean(initialMarkdown));
900
- const doParse = useCallback2((source) => {
1733
+ const parseTimeoutRef = useRef2(null);
1734
+ const skipInitialDebouncedParseRef = useRef2(Boolean(initialMarkdown));
1735
+ const doParse = useCallback3((source) => {
901
1736
  setIsParsing(true);
902
1737
  try {
903
- const parsed = parseMarkdown2(source);
1738
+ const parsed = parseMarkdown3(source);
904
1739
  setMarkdownDocState(parsed);
905
1740
  setParseError(null);
906
1741
  try {
@@ -921,7 +1756,7 @@ function EditorProvider({
921
1756
  setIsParsing(false);
922
1757
  }
923
1758
  }, []);
924
- useEffect(() => {
1759
+ useEffect2(() => {
925
1760
  if (editorMode !== "markdown") return;
926
1761
  if (skipInitialDebouncedParseRef.current) {
927
1762
  skipInitialDebouncedParseRef.current = false;
@@ -940,13 +1775,13 @@ function EditorProvider({
940
1775
  }
941
1776
  };
942
1777
  }, [markdownSource, doParse, editorMode]);
943
- useEffect(() => {
1778
+ useEffect2(() => {
944
1779
  if (editorMode !== "markdown") return;
945
1780
  if (initialMarkdown) {
946
1781
  doParse(initialMarkdown);
947
1782
  }
948
1783
  }, []);
949
- const setMarkdownSource = useCallback2((source) => {
1784
+ const setMarkdownSource = useCallback3((source) => {
950
1785
  setMarkdownSourceRaw(source);
951
1786
  }, []);
952
1787
  const blockNav = useBlockNavigator(markdownSource, setMarkdownSource, {
@@ -964,8 +1799,8 @@ function EditorProvider({
964
1799
  nextBlock,
965
1800
  addBlock
966
1801
  } = blockNav;
967
- const [selectionVersion, setSelectionVersion] = useState2(0);
968
- useEffect(() => {
1802
+ const [selectionVersion, setSelectionVersion] = useState3(0);
1803
+ useEffect2(() => {
969
1804
  const bump = () => setSelectionVersion((version) => version + 1);
970
1805
  const disposers = [];
971
1806
  if (typeof tiptapEditor?.on === "function" && typeof tiptapEditor.off === "function") {
@@ -982,7 +1817,7 @@ function EditorProvider({
982
1817
  for (const dispose of disposers) dispose();
983
1818
  };
984
1819
  }, [tiptapEditor, monacoEditor]);
985
- const getSelection = useCallback2(() => {
1820
+ const getSelection = useCallback3(() => {
986
1821
  if (activeView === "wysiwyg" && tiptapEditor) {
987
1822
  const { from, to, empty } = tiptapEditor.state.selection;
988
1823
  return {
@@ -1000,7 +1835,7 @@ function EditorProvider({
1000
1835
  }
1001
1836
  return null;
1002
1837
  }, [activeView, tiptapEditor, monacoEditor]);
1003
- const replaceSelection = useCallback2(
1838
+ const replaceSelection = useCallback3(
1004
1839
  (markdown) => {
1005
1840
  if (activeView === "wysiwyg" && tiptapEditor) {
1006
1841
  tiptapEditor.chain().focus().insertContent(markdownToTiptap(markdown)).run();
@@ -1019,7 +1854,7 @@ function EditorProvider({
1019
1854
  },
1020
1855
  [activeView, tiptapEditor, monacoEditor]
1021
1856
  );
1022
- const getBlockAtCursor = useCallback2(() => {
1857
+ const getBlockAtCursor = useCallback3(() => {
1023
1858
  if (activeView !== "raw" || !monacoEditor) return null;
1024
1859
  const model = monacoEditor.getModel();
1025
1860
  const position = monacoEditor.getPosition();
@@ -1028,7 +1863,7 @@ function EditorProvider({
1028
1863
  if (slices.length === 0) return null;
1029
1864
  return slices[sliceIndexAtOffset(slices, model.getOffsetAt(position))] ?? null;
1030
1865
  }, [activeView, monacoEditor]);
1031
- const insertAtCursor = useCallback2(
1866
+ const insertAtCursor = useCallback3(
1032
1867
  (text) => {
1033
1868
  if (activeView === "wysiwyg" && tiptapEditor) {
1034
1869
  const html = markdownToTiptap(text);
@@ -1055,11 +1890,126 @@ function EditorProvider({
1055
1890
  },
1056
1891
  [activeView, tiptapEditor, monacoEditor]
1057
1892
  );
1058
- const replaceAll = useCallback2((text) => {
1059
- setMarkdownSourceRaw(text);
1060
- }, []);
1061
- const versioningWarnedRef = useRef(false);
1062
- useEffect(() => {
1893
+ const applySourceEdits = useCallback3(
1894
+ (edits, options) => {
1895
+ if (editorMode !== "markdown" || layoutModeState !== "document") return false;
1896
+ if (activeView === "raw" && monacoEditor) {
1897
+ const model = monacoEditor.getModel();
1898
+ if (!model) return false;
1899
+ const base = model.getValue();
1900
+ if (options?.baseSource !== void 0 && options.baseSource !== base) return false;
1901
+ const sorted = normalizeSourceEdits(base, edits);
1902
+ if (!sorted) return false;
1903
+ if (sorted.length === 0) return true;
1904
+ const ops = sorted.map((edit) => {
1905
+ const start = model.getPositionAt(edit.start);
1906
+ const end = model.getPositionAt(edit.end);
1907
+ return {
1908
+ range: {
1909
+ startLineNumber: start.lineNumber,
1910
+ startColumn: start.column,
1911
+ endLineNumber: end.lineNumber,
1912
+ endColumn: end.column
1913
+ },
1914
+ text: edit.text
1915
+ };
1916
+ });
1917
+ monacoEditor.pushUndoStop();
1918
+ monacoEditor.executeEdits("squisq-source-edits", ops);
1919
+ monacoEditor.pushUndoStop();
1920
+ return true;
1921
+ }
1922
+ if (activeView === "wysiwyg" && tiptapEditor) {
1923
+ const base = markdownSourceRef.current;
1924
+ if (options?.baseSource !== void 0 && options.baseSource !== base) return false;
1925
+ const sorted = normalizeSourceEdits(base, edits);
1926
+ if (!sorted) return false;
1927
+ if (sorted.length === 0) return true;
1928
+ setMarkdownSourceRaw(applySourceEditsToText(base, sorted));
1929
+ return true;
1930
+ }
1931
+ return false;
1932
+ },
1933
+ [activeView, editorMode, layoutModeState, monacoEditor, tiptapEditor]
1934
+ );
1935
+ const replaceAll = useCallback3(
1936
+ (text) => {
1937
+ const base = activeView === "raw" && monacoEditor ? monacoEditor.getModel()?.getValue() ?? null : markdownSourceRef.current;
1938
+ const edit = base === null ? null : minimalReplaceEdit(base, text);
1939
+ if (edit && applySourceEdits([edit])) return;
1940
+ setMarkdownSourceRaw(text);
1941
+ },
1942
+ [activeView, monacoEditor, applySourceEdits]
1943
+ );
1944
+ const insertBlockAfterCursor = useCallback3(
1945
+ (block, options) => {
1946
+ if (editorMode !== "markdown") return false;
1947
+ const placement = options?.placement ?? "afterBlock";
1948
+ const build = (context) => {
1949
+ const markdown = typeof block === "function" ? block(context) : block;
1950
+ return markdown && markdown.trim() ? markdown : null;
1951
+ };
1952
+ if (activeView === "wysiwyg" && tiptapEditor) {
1953
+ const { pos, ...context } = tiptapBlockInsertion(tiptapEditor.state, placement);
1954
+ const markdown = build(context);
1955
+ if (markdown === null) return false;
1956
+ let content;
1957
+ try {
1958
+ content = createDocument(markdownToTiptap(markdown), tiptapEditor.schema).content;
1959
+ } catch {
1960
+ return false;
1961
+ }
1962
+ if (content.size === 0) return false;
1963
+ const inserted = tiptapEditor.chain().command(({ tr, dispatch }) => {
1964
+ if (dispatch) {
1965
+ closeHistory2(tr);
1966
+ tr.insert(pos, content);
1967
+ }
1968
+ return true;
1969
+ }).run();
1970
+ if (inserted) {
1971
+ const dom = tiptapEditor.view.nodeDOM(pos);
1972
+ if (dom instanceof HTMLElement && typeof dom.scrollIntoView === "function") {
1973
+ dom.scrollIntoView({ block: "nearest" });
1974
+ }
1975
+ }
1976
+ return inserted;
1977
+ }
1978
+ if (activeView === "raw" && monacoEditor) {
1979
+ const model = monacoEditor.getModel();
1980
+ const selection = monacoEditor.getSelection();
1981
+ if (!model || !selection) return false;
1982
+ const source = model.getValue();
1983
+ const cursorOffset = model.getOffsetAt(selection.getEndPosition());
1984
+ const { offset, ...context } = blockInsertionPoint(source, cursorOffset, placement);
1985
+ const markdown = build(context);
1986
+ if (markdown === null) return false;
1987
+ const edit = joinBlockAt(source, offset, markdown);
1988
+ const start = model.getPositionAt(edit.start);
1989
+ const end = model.getPositionAt(edit.end);
1990
+ monacoEditor.pushUndoStop();
1991
+ monacoEditor.executeEdits("squisq-insert-block", [
1992
+ {
1993
+ range: {
1994
+ startLineNumber: start.lineNumber,
1995
+ startColumn: start.column,
1996
+ endLineNumber: end.lineNumber,
1997
+ endColumn: end.column
1998
+ },
1999
+ text: edit.text
2000
+ }
2001
+ ]);
2002
+ monacoEditor.pushUndoStop();
2003
+ const firstLine = model.getPositionAt(Math.min(edit.start + 1, model.getValueLength()));
2004
+ monacoEditor.revealLineInCenterIfOutsideViewport?.(firstLine.lineNumber);
2005
+ return true;
2006
+ }
2007
+ return false;
2008
+ },
2009
+ [activeView, editorMode, monacoEditor, tiptapEditor]
2010
+ );
2011
+ const versioningWarnedRef = useRef2(false);
2012
+ useEffect2(() => {
1063
2013
  if (allowVersioning && !workspaceContainer && !versioningWarnedRef.current) {
1064
2014
  console.warn(
1065
2015
  "[squisq-editor] allowVersioning requires a `workspaceContainer` prop; versioning is disabled."
@@ -1067,15 +2017,15 @@ function EditorProvider({
1067
2017
  versioningWarnedRef.current = true;
1068
2018
  }
1069
2019
  }, [allowVersioning, workspaceContainer]);
1070
- const versioning = useMemo2(() => {
2020
+ const versioning = useMemo3(() => {
1071
2021
  if (!allowVersioning || !workspaceContainer) return null;
1072
2022
  return new DocumentVersionManager(workspaceContainer, { basename: versionBasename });
1073
2023
  }, [allowVersioning, workspaceContainer, versionBasename]);
1074
- const onSaveVersionRef = useRef(onSaveVersion);
2024
+ const onSaveVersionRef = useRef2(onSaveVersion);
1075
2025
  onSaveVersionRef.current = onSaveVersion;
1076
- const prunePolicyRef = useRef(versioningPrunePolicy);
2026
+ const prunePolicyRef = useRef2(versioningPrunePolicy);
1077
2027
  prunePolicyRef.current = versioningPrunePolicy;
1078
- const saveVersion = useCallback2(
2028
+ const saveVersion = useCallback3(
1079
2029
  async (options) => {
1080
2030
  if (!versioning) {
1081
2031
  const skipped = { saved: false, version: null, reason: "no-document" };
@@ -1096,8 +2046,8 @@ function EditorProvider({
1096
2046
  },
1097
2047
  [versioning]
1098
2048
  );
1099
- const autoSaveSourceRef = useRef(null);
1100
- useEffect(() => {
2049
+ const autoSaveSourceRef = useRef2(null);
2050
+ useEffect2(() => {
1101
2051
  if (!versioning) return;
1102
2052
  if (versioningAutoSaveIdleMs <= 0) return;
1103
2053
  const previous = autoSaveSourceRef.current;
@@ -1113,7 +2063,7 @@ function EditorProvider({
1113
2063
  }, versioningAutoSaveIdleMs);
1114
2064
  return () => clearTimeout(timer);
1115
2065
  }, [markdownSource, versioning, versioningAutoSaveIdleMs, saveVersion]);
1116
- const setMarkdownDoc = useCallback2((newDoc) => {
2066
+ const setMarkdownDoc = useCallback3((newDoc) => {
1117
2067
  setMarkdownDocState(newDoc);
1118
2068
  try {
1119
2069
  const newSource = stringifyMarkdown(newDoc);
@@ -1133,12 +2083,12 @@ function EditorProvider({
1133
2083
  setParseError(err instanceof Error ? err.message : "Stringify error");
1134
2084
  }
1135
2085
  }, []);
1136
- useEffect(() => {
2086
+ useEffect2(() => {
1137
2087
  if (mediaEditRenders && doc) mediaEditRenders.sync(doc, embeddedMediaClips(doc));
1138
2088
  }, [mediaEditRenders, doc]);
1139
- const latestDocRef = useRef(doc);
2089
+ const latestDocRef = useRef2(doc);
1140
2090
  latestDocRef.current = doc;
1141
- useEffect(() => {
2091
+ useEffect2(() => {
1142
2092
  if (!mediaEditRenders) return;
1143
2093
  const timer = setTimeout(() => {
1144
2094
  const current = latestDocRef.current;
@@ -1147,7 +2097,15 @@ function EditorProvider({
1147
2097
  }, MEDIA_EDIT_GC_DELAY_MS);
1148
2098
  return () => clearTimeout(timer);
1149
2099
  }, [mediaEditRenders]);
1150
- const value = useMemo2(
2100
+ const dictationController = useDictationController({
2101
+ capability: speechInput,
2102
+ activeView,
2103
+ editorMode,
2104
+ tiptapEditor,
2105
+ monacoEditor
2106
+ });
2107
+ const dictation = dictationController.control;
2108
+ const value = useMemo3(
1151
2109
  () => ({
1152
2110
  markdownSource,
1153
2111
  markdownDoc,
@@ -1193,6 +2151,7 @@ function EditorProvider({
1193
2151
  proofingSpellingEnabled,
1194
2152
  proofingGrammarEnabled,
1195
2153
  proofingIgnoreStore,
2154
+ dictation,
1196
2155
  articleId,
1197
2156
  documentLinkProvider,
1198
2157
  fenceRenderers,
@@ -1226,6 +2185,8 @@ function EditorProvider({
1226
2185
  getBlockAtCursor,
1227
2186
  insertAtCursor,
1228
2187
  replaceAll,
2188
+ applySourceEdits,
2189
+ insertBlockAfterCursor,
1229
2190
  openImageEdit,
1230
2191
  closeImageEdit,
1231
2192
  openMediaEdit,
@@ -1271,6 +2232,7 @@ function EditorProvider({
1271
2232
  proofingSpellingEnabled,
1272
2233
  proofingGrammarEnabled,
1273
2234
  proofingIgnoreStore,
2235
+ dictation,
1274
2236
  articleId,
1275
2237
  documentLinkProvider,
1276
2238
  fenceRenderers,
@@ -1304,6 +2266,8 @@ function EditorProvider({
1304
2266
  getBlockAtCursor,
1305
2267
  insertAtCursor,
1306
2268
  replaceAll,
2269
+ applySourceEdits,
2270
+ insertBlockAfterCursor,
1307
2271
  imageEditTarget,
1308
2272
  mediaEditTarget,
1309
2273
  mediaRevision,
@@ -1317,7 +2281,7 @@ function EditorProvider({
1317
2281
  bumpMediaRevision
1318
2282
  ]
1319
2283
  );
1320
- return /* @__PURE__ */ jsx(EditorContext.Provider, { value, children });
2284
+ return /* @__PURE__ */ jsx(EditorContext.Provider, { value, children: /* @__PURE__ */ jsx(DictationStateContext.Provider, { value: dictationController.state, children }) });
1321
2285
  }
1322
2286
 
1323
2287
  // src/review/decorations.ts
@@ -1395,17 +2359,17 @@ function suggestionLabel(kind) {
1395
2359
  }
1396
2360
 
1397
2361
  // src/review/ReviewExtension.ts
1398
- import { Extension } from "@tiptap/core";
1399
- import { Plugin, PluginKey } from "@tiptap/pm/state";
1400
- import { Decoration, DecorationSet } from "@tiptap/pm/view";
1401
- var REVIEW_KEY = new PluginKey("squisq-review");
2362
+ import { Extension as Extension2 } from "@tiptap/core";
2363
+ import { Plugin as Plugin2, PluginKey as PluginKey2 } from "@tiptap/pm/state";
2364
+ import { Decoration as Decoration2, DecorationSet as DecorationSet2 } from "@tiptap/pm/view";
2365
+ var REVIEW_KEY = new PluginKey2("squisq-review");
1402
2366
  function buildSet(doc, specs) {
1403
2367
  const max = doc.content.size;
1404
2368
  const decorations = [];
1405
2369
  for (const spec of specs) {
1406
2370
  if (spec.from >= spec.to || spec.to > max) continue;
1407
2371
  decorations.push(
1408
- Decoration.inline(
2372
+ Decoration2.inline(
1409
2373
  spec.from,
1410
2374
  spec.to,
1411
2375
  {
@@ -1417,16 +2381,16 @@ function buildSet(doc, specs) {
1417
2381
  )
1418
2382
  );
1419
2383
  }
1420
- return DecorationSet.create(doc, decorations);
2384
+ return DecorationSet2.create(doc, decorations);
1421
2385
  }
1422
- var ReviewExtension = Extension.create({
2386
+ var ReviewExtension = Extension2.create({
1423
2387
  name: "squisqReview",
1424
2388
  addProseMirrorPlugins() {
1425
2389
  return [
1426
- new Plugin({
2390
+ new Plugin2({
1427
2391
  key: REVIEW_KEY,
1428
2392
  state: {
1429
- init: () => ({ decorations: DecorationSet.empty }),
2393
+ init: () => ({ decorations: DecorationSet2.empty }),
1430
2394
  apply: (transaction, previous) => {
1431
2395
  const meta = transaction.getMeta(REVIEW_KEY);
1432
2396
  if (meta) return { decorations: buildSet(transaction.doc, meta.specs) };
@@ -1685,7 +2649,7 @@ function messageOf(error) {
1685
2649
  }
1686
2650
 
1687
2651
  // src/review/useReview.ts
1688
- import { useCallback as useCallback3, useEffect as useEffect2, useMemo as useMemo3, useRef as useRef2, useState as useState3 } from "react";
2652
+ import { useCallback as useCallback4, useEffect as useEffect3, useMemo as useMemo4, useRef as useRef3, useState as useState4 } from "react";
1689
2653
  var EMPTY = [];
1690
2654
  var EMPTY_FINDINGS = [];
1691
2655
  function useReview(capabilities = EMPTY) {
@@ -1698,23 +2662,23 @@ function useReview(capabilities = EMPTY) {
1698
2662
  articleId,
1699
2663
  goToBlockByLine
1700
2664
  } = useEditorContext();
1701
- const providers = useMemo3(
2665
+ const providers = useMemo4(
1702
2666
  () => capabilities.map(resolveReviewProvider),
1703
2667
  [capabilities]
1704
2668
  );
1705
- const [snapshot, setSnapshot] = useState3(null);
1706
- const sourceRef = useRef2(markdownSource);
1707
- const revisionRef = useRef2(0);
1708
- const blocksRef = useRef2([]);
1709
- const runnerRef = useRef2(null);
2669
+ const [snapshot, setSnapshot] = useState4(null);
2670
+ const sourceRef = useRef3(markdownSource);
2671
+ const revisionRef = useRef3(0);
2672
+ const blocksRef = useRef3([]);
2673
+ const runnerRef = useRef3(null);
1710
2674
  sourceRef.current = markdownSource;
1711
- const hueFor = useMemo3(() => {
2675
+ const hueFor = useMemo4(() => {
1712
2676
  const hues = new Map(
1713
2677
  providers.map((provider) => [provider.id, provider.hue ?? DEFAULT_REVIEW_HUE])
1714
2678
  );
1715
2679
  return (source) => hues.get(source) ?? DEFAULT_REVIEW_HUE;
1716
2680
  }, [providers]);
1717
- useEffect2(() => {
2681
+ useEffect3(() => {
1718
2682
  if (providers.length === 0) {
1719
2683
  setSnapshot(null);
1720
2684
  return;
@@ -1741,12 +2705,12 @@ function useReview(capabilities = EMPTY) {
1741
2705
  runnerRef.current = null;
1742
2706
  };
1743
2707
  }, [providers, articleId, fileName]);
1744
- useEffect2(() => {
2708
+ useEffect3(() => {
1745
2709
  revisionRef.current += 1;
1746
2710
  runnerRef.current?.schedule();
1747
2711
  }, [markdownSource]);
1748
- const findings = useMemo3(() => snapshot?.findings ?? EMPTY_FINDINGS, [snapshot]);
1749
- useEffect2(() => {
2712
+ const findings = useMemo4(() => snapshot?.findings ?? EMPTY_FINDINGS, [snapshot]);
2713
+ useEffect3(() => {
1750
2714
  if (!tiptapEditor || activeView !== "wysiwyg") return;
1751
2715
  if (findings.length === 0) {
1752
2716
  clearReviewDecorations(tiptapEditor);
@@ -1757,8 +2721,8 @@ function useReview(capabilities = EMPTY) {
1757
2721
  buildWriteViewDecorations(tiptapEditor.state.doc, findings, hueFor)
1758
2722
  );
1759
2723
  }, [tiptapEditor, activeView, findings, hueFor]);
1760
- const collectionRef = useRef2(null);
1761
- useEffect2(() => {
2724
+ const collectionRef = useRef3(null);
2725
+ useEffect3(() => {
1762
2726
  if (!monacoEditor || activeView !== "raw") return;
1763
2727
  const model = monacoEditor.getModel();
1764
2728
  if (!model) return;
@@ -1768,7 +2732,7 @@ function useReview(capabilities = EMPTY) {
1768
2732
  collectionRef.current?.clear();
1769
2733
  };
1770
2734
  }, [monacoEditor, activeView, findings, hueFor]);
1771
- const goToFinding = useCallback3(
2735
+ const goToFinding = useCallback4(
1772
2736
  (findingId) => {
1773
2737
  const finding = findings.find((candidate) => candidate.id === findingId);
1774
2738
  if (!finding) return;
@@ -1779,22 +2743,22 @@ function useReview(capabilities = EMPTY) {
1779
2743
  },
1780
2744
  [findings, goToBlockByLine]
1781
2745
  );
1782
- const reviewNow = useCallback3(() => runnerRef.current?.runNow(), []);
1783
- const dismissFinding = useCallback3((findingId) => {
2746
+ const reviewNow = useCallback4(() => runnerRef.current?.runNow(), []);
2747
+ const dismissFinding = useCallback4((findingId) => {
1784
2748
  runnerRef.current?.removeFinding(findingId);
1785
2749
  }, []);
1786
- return useMemo3(
2750
+ return useMemo4(
1787
2751
  () => snapshot ? { ...snapshot, reviewNow, dismissFinding, goToFinding } : null,
1788
2752
  [snapshot, reviewNow, dismissFinding, goToFinding]
1789
2753
  );
1790
2754
  }
1791
2755
 
1792
2756
  // src/review/ReviewContext.tsx
1793
- import { createContext as createContext2, useContext as useContext2 } from "react";
2757
+ import { createContext as createContext3, useContext as useContext3 } from "react";
1794
2758
  import { jsx as jsx2 } from "react/jsx-runtime";
1795
- var ReviewStateContext = createContext2(null);
2759
+ var ReviewStateContext = createContext3(null);
1796
2760
  function useReviewState() {
1797
- return useContext2(ReviewStateContext);
2761
+ return useContext3(ReviewStateContext);
1798
2762
  }
1799
2763
  function ReviewRoot({
1800
2764
  providers,
@@ -1888,12 +2852,23 @@ function Row({
1888
2852
  }
1889
2853
 
1890
2854
  export {
2855
+ DictationExtension,
2856
+ useDictationState,
1891
2857
  stripFrontmatter,
1892
2858
  getBlockSlices,
1893
2859
  spliceBlock,
1894
2860
  lineToOffset,
1895
2861
  offsetToLine,
1896
2862
  sliceIndexAtOffset,
2863
+ normalizeSourceEdits,
2864
+ applySourceEditsToText,
2865
+ minimalReplaceEdit,
2866
+ resolveOutlineInsertion,
2867
+ headingDepthForInsertion,
2868
+ sourceOutline,
2869
+ blockInsertionPoint,
2870
+ joinBlockAt,
2871
+ tiptapBlockInsertion,
1897
2872
  detectLanguageFromFileName,
1898
2873
  resolveFileKind,
1899
2874
  useBlockNavigator,