@ai-matrx/rich-editor 0.1.2 → 0.2.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.
- package/CHANGELOG.md +13 -0
- package/dist/core/markdown-format.js +3 -3
- package/dist/core/text-insertion.d.ts +14 -0
- package/dist/core/text-insertion.js +55 -0
- package/dist/editor/RichEditor.d.ts +4 -0
- package/dist/editor/RichEditor.js +14 -0
- package/dist/editor/RichEditorContext.d.ts +16 -0
- package/dist/editor/RichEditorContext.js +17 -0
- package/dist/editor/RichEditorImpl.d.ts +137 -0
- package/dist/editor/RichEditorImpl.js +714 -0
- package/dist/editor/caretInsert.d.ts +11 -0
- package/dist/editor/caretInsert.js +9 -0
- package/dist/editor/rich-editor.css +181 -0
- package/dist/format/format-actions.d.ts +10 -0
- package/dist/format/format-actions.js +78 -0
- package/dist/format/useTextareaFormatting.d.ts +4 -0
- package/dist/format/useTextareaFormatting.js +37 -0
- package/dist/in-place/EditInPlace.d.ts +38 -0
- package/dist/in-place/EditInPlace.js +72 -0
- package/dist/in-place/EditInPlaceText.d.ts +18 -0
- package/dist/in-place/EditInPlaceText.js +159 -0
- package/dist/in-place/InPlaceEditor.d.ts +41 -0
- package/dist/in-place/InPlaceEditor.js +169 -0
- package/dist/islands/IslandCodeEditor.d.ts +12 -0
- package/dist/islands/IslandCodeEditor.js +82 -0
- package/dist/islands/IslandPreview.d.ts +6 -0
- package/dist/islands/IslandPreview.js +31 -0
- package/dist/islands/island-meta.d.ts +28 -0
- package/dist/islands/island-meta.js +176 -0
- package/dist/panels/FindReplacePanel.d.ts +10 -0
- package/dist/panels/FindReplacePanel.js +118 -0
- package/dist/panels/KindPicker.d.ts +4 -0
- package/dist/panels/KindPicker.js +95 -0
- package/dist/panels/OutlinePanel.d.ts +6 -0
- package/dist/panels/OutlinePanel.js +60 -0
- package/dist/panels/ShortcutsDialog.d.ts +4 -0
- package/dist/panels/ShortcutsDialog.js +24 -0
- package/dist/source/SourceEditor.d.ts +28 -0
- package/dist/source/SourceEditor.js +429 -0
- package/dist/source/cm-theme.d.ts +4 -0
- package/dist/source/cm-theme.js +71 -0
- package/dist/source/live-preview.d.ts +30 -0
- package/dist/source/live-preview.js +304 -0
- package/dist/source/markdown-language.d.ts +2 -0
- package/dist/source/markdown-language.js +7 -0
- package/dist/visual/BlockHandle.d.ts +6 -0
- package/dist/visual/BlockHandle.js +173 -0
- package/dist/visual/VisualEditor.d.ts +56 -0
- package/dist/visual/VisualEditor.js +312 -0
- package/dist/visual/auto-edit.d.ts +3 -0
- package/dist/visual/auto-edit.js +17 -0
- package/dist/visual/context-menu-caret.d.ts +10 -0
- package/dist/visual/context-menu-caret.js +42 -0
- package/dist/visual/decorations.d.ts +9 -0
- package/dist/visual/decorations.js +131 -0
- package/dist/visual/format-actions.d.ts +20 -0
- package/dist/visual/format-actions.js +128 -0
- package/dist/visual/menus/SuggestionMenu.d.ts +37 -0
- package/dist/visual/menus/SuggestionMenu.js +121 -0
- package/dist/visual/nodes/CalloutView.d.ts +2 -0
- package/dist/visual/nodes/CalloutView.js +45 -0
- package/dist/visual/nodes/InlineIslandView.d.ts +2 -0
- package/dist/visual/nodes/InlineIslandView.js +174 -0
- package/dist/visual/nodes/IslandBlockView.d.ts +2 -0
- package/dist/visual/nodes/IslandBlockView.js +206 -0
- package/dist/visual/nodes/SourceLockedView.d.ts +2 -0
- package/dist/visual/nodes/SourceLockedView.js +62 -0
- package/dist/visual/shortcut-handlers.d.ts +27 -0
- package/dist/visual/shortcut-handlers.js +43 -0
- package/dist/visual/slash-items.d.ts +18 -0
- package/dist/visual/slash-items.js +152 -0
- package/dist/visual/visual-extensions.d.ts +10 -0
- package/dist/visual/visual-extensions.js +246 -0
- package/dist/visual/visual-format-target.d.ts +9 -0
- package/dist/visual/visual-format-target.js +43 -0
- package/package.json +42 -4
|
@@ -0,0 +1,714 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import "./rich-editor.css";
|
|
4
|
+
import { useDeferredValue, useEffect, useImperativeHandle, useRef, useState } from "react";
|
|
5
|
+
import {
|
|
6
|
+
AlignLeft,
|
|
7
|
+
Check,
|
|
8
|
+
ChevronDown,
|
|
9
|
+
Code2,
|
|
10
|
+
Eye,
|
|
11
|
+
Focus,
|
|
12
|
+
Hash,
|
|
13
|
+
Keyboard,
|
|
14
|
+
ListTree,
|
|
15
|
+
Loader2,
|
|
16
|
+
Mic,
|
|
17
|
+
MicOff,
|
|
18
|
+
Minimize2,
|
|
19
|
+
MoreHorizontal,
|
|
20
|
+
Save,
|
|
21
|
+
Search,
|
|
22
|
+
Type
|
|
23
|
+
} from "lucide-react";
|
|
24
|
+
import { ConfirmDialog } from "@ai-matrx/rich-content/host/app-bindings";
|
|
25
|
+
import { TextInputDialog } from "@ai-matrx/rich-content/host/app-bindings";
|
|
26
|
+
import {
|
|
27
|
+
DropdownMenu,
|
|
28
|
+
DropdownMenuContent,
|
|
29
|
+
DropdownMenuItem,
|
|
30
|
+
DropdownMenuTrigger,
|
|
31
|
+
Sheet,
|
|
32
|
+
SheetContent,
|
|
33
|
+
SheetHeader,
|
|
34
|
+
SheetTitle,
|
|
35
|
+
useIsMobile
|
|
36
|
+
} from "@ai-matrx/design-system";
|
|
37
|
+
import { toast } from "@ai-matrx/rich-content/host/toast";
|
|
38
|
+
import { cn } from "@ai-matrx/design-system";
|
|
39
|
+
import { EditableContextMenu } from "@ai-matrx/rich-content/host/app-bindings";
|
|
40
|
+
import { buildApplicationScopeFromMenuContext } from "@ai-matrx/rich-content/host/app-bindings";
|
|
41
|
+
import { useMicField } from "@ai-matrx/rich-content/host/app-bindings";
|
|
42
|
+
import { useFileUpload } from "@ai-matrx/rich-content/host/app-bindings";
|
|
43
|
+
import { fileUrls } from "@ai-matrx/rich-content/host/app-bindings";
|
|
44
|
+
import { RichDocument } from "@ai-matrx/rich-content/rich-document/RichDocument";
|
|
45
|
+
import { planSave } from "../core/save-plan.js";
|
|
46
|
+
import { reconcileHostValue } from "../core/host-value.js";
|
|
47
|
+
import { measureText } from "../core/text-metrics.js";
|
|
48
|
+
import { outlineOf } from "../core/outline.js";
|
|
49
|
+
import { RichEditorContext } from "./RichEditorContext.js";
|
|
50
|
+
import { VisualEditor } from "../visual/VisualEditor.js";
|
|
51
|
+
import { SourceEditor } from "../source/SourceEditor.js";
|
|
52
|
+
import { FindReplacePanel } from "../panels/FindReplacePanel.js";
|
|
53
|
+
import { OutlinePanel } from "../panels/OutlinePanel.js";
|
|
54
|
+
import { ShortcutsDialog } from "../panels/ShortcutsDialog.js";
|
|
55
|
+
import { KindPicker } from "../panels/KindPicker.js";
|
|
56
|
+
import { islandMeta, inlineIslandLabel } from "../islands/island-meta.js";
|
|
57
|
+
import { withImagePolicy } from "@ai-matrx/rich-content/levels/prose/remote-image-policy";
|
|
58
|
+
import { RecordAnnotations } from "@ai-matrx/rich-content/host/app-bindings";
|
|
59
|
+
import { annotationRecordOf } from "@ai-matrx/rich-content/host/app-bindings";
|
|
60
|
+
import { formatCount } from "@ai-matrx/kit/format";
|
|
61
|
+
const VIEWS = ["visual", "source", "preview"];
|
|
62
|
+
function describeDelta(delta) {
|
|
63
|
+
const raw = delta.before ?? delta.after ?? "";
|
|
64
|
+
const block = islandMeta(delta.islandType, raw);
|
|
65
|
+
const excerpt = raw.replace(/\s+/g, " ").trim();
|
|
66
|
+
const quoted = `\u201C${excerpt.length > 40 ? `${excerpt.slice(0, 40)}\u2026` : excerpt}\u201D`;
|
|
67
|
+
const name = raw.startsWith("{{") ? raw : !block.generic ? block.label : `${inlineIslandLabel(delta.islandType)} ${quoted}`;
|
|
68
|
+
switch (delta.kind) {
|
|
69
|
+
case "removed":
|
|
70
|
+
return `${name} would be removed`;
|
|
71
|
+
case "changed":
|
|
72
|
+
return `${name} would be changed`;
|
|
73
|
+
case "swallowed":
|
|
74
|
+
return `${name} would stop being protected \u2014 something above it (an unclosed code block or tag) now swallows it`;
|
|
75
|
+
default:
|
|
76
|
+
return `${name} would be added`;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const VIEW_META = {
|
|
80
|
+
visual: { label: "Visual", icon: Type },
|
|
81
|
+
source: { label: "Source", icon: Code2 },
|
|
82
|
+
preview: { label: "Preview", icon: Eye }
|
|
83
|
+
};
|
|
84
|
+
function RichEditorImpl({
|
|
85
|
+
value,
|
|
86
|
+
onSave,
|
|
87
|
+
onChange,
|
|
88
|
+
variables = null,
|
|
89
|
+
onDeclareVariable,
|
|
90
|
+
defaultView = "visual",
|
|
91
|
+
view: controlledView,
|
|
92
|
+
onViewChange,
|
|
93
|
+
chrome = "full",
|
|
94
|
+
hostContextMenu = false,
|
|
95
|
+
controllerRef,
|
|
96
|
+
readOnly = false,
|
|
97
|
+
placeholder,
|
|
98
|
+
surfaceName = "matrx-user/markdown-studio",
|
|
99
|
+
sourceFeature = "documents",
|
|
100
|
+
contentSource = { type: "raw" },
|
|
101
|
+
annotationRecord,
|
|
102
|
+
saveLabel = "Save",
|
|
103
|
+
className,
|
|
104
|
+
toolbarExtras,
|
|
105
|
+
onCancel,
|
|
106
|
+
cancelLabel = "Cancel",
|
|
107
|
+
defaultOutlineOpen,
|
|
108
|
+
onNothingToSave,
|
|
109
|
+
imagePolicy
|
|
110
|
+
}) {
|
|
111
|
+
const isMobile = useIsMobile();
|
|
112
|
+
const [stored, setStored] = useState(value);
|
|
113
|
+
const [current, setCurrent] = useState(value);
|
|
114
|
+
const [view, setView] = useState(controlledView ?? defaultView);
|
|
115
|
+
const [mountKey, setMountKey] = useState(0);
|
|
116
|
+
const [findMode, setFindMode] = useState(null);
|
|
117
|
+
const [outlineChoice, setOutlineChoice] = useState(
|
|
118
|
+
defaultOutlineOpen ?? (chrome === "bare" ? false : null)
|
|
119
|
+
);
|
|
120
|
+
const outlineOpen = outlineChoice ?? !isMobile;
|
|
121
|
+
const setOutlineOpen = (next) => setOutlineChoice(typeof next === "function" ? next(outlineOpen) : next);
|
|
122
|
+
const [focusMode, setFocusMode] = useState(false);
|
|
123
|
+
const [helpOpen, setHelpOpen] = useState(false);
|
|
124
|
+
const [renderIslands, setRenderIslands] = useState(true);
|
|
125
|
+
const [loadStats, setLoadStats] = useState(null);
|
|
126
|
+
const [pendingPlan, setPendingPlan] = useState(null);
|
|
127
|
+
const [saving, setSaving] = useState(false);
|
|
128
|
+
const [saveState, setSaveState] = useState(null);
|
|
129
|
+
const [linkDialog, setLinkDialog] = useState({ open: false, href: "" });
|
|
130
|
+
const [kindRequest, setKindRequest] = useState(null);
|
|
131
|
+
const approved = useRef(/* @__PURE__ */ new Set());
|
|
132
|
+
const handle = useRef(null);
|
|
133
|
+
const imageInput = useRef(null);
|
|
134
|
+
const { upload } = useFileUpload();
|
|
135
|
+
const lastValue = useRef(value);
|
|
136
|
+
const ownSaves = useRef(/* @__PURE__ */ new Set());
|
|
137
|
+
useEffect(() => {
|
|
138
|
+
if (value === lastValue.current) return;
|
|
139
|
+
let draft = current;
|
|
140
|
+
try {
|
|
141
|
+
draft = handle.current?.flush() ?? current;
|
|
142
|
+
} catch {
|
|
143
|
+
}
|
|
144
|
+
const action = reconcileHostValue({ value, lastValue: lastValue.current, stored, draft, ownSaves: ownSaves.current });
|
|
145
|
+
lastValue.current = value;
|
|
146
|
+
if (action === "ignore") return;
|
|
147
|
+
setStored(value);
|
|
148
|
+
if (action === "adopt-stored") return;
|
|
149
|
+
if (action === "keep-draft") {
|
|
150
|
+
toast.warning("This document changed while you were editing. Your edits are kept \u2014 Save writes them over the new version.");
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
ownSaves.current.clear();
|
|
154
|
+
setCurrent(value);
|
|
155
|
+
approved.current.clear();
|
|
156
|
+
setMountKey((key) => key + 1);
|
|
157
|
+
}, [value]);
|
|
158
|
+
const deferred = useDeferredValue(current);
|
|
159
|
+
const metrics = measureText(deferred);
|
|
160
|
+
const outline = outlineOf(deferred);
|
|
161
|
+
const dirty = current !== stored;
|
|
162
|
+
const updateCurrent = (text) => {
|
|
163
|
+
setCurrent(text);
|
|
164
|
+
onChange?.(text);
|
|
165
|
+
};
|
|
166
|
+
const flush = () => {
|
|
167
|
+
const text = handle.current?.flush() ?? current;
|
|
168
|
+
if (text !== current) updateCurrent(text);
|
|
169
|
+
return text;
|
|
170
|
+
};
|
|
171
|
+
const setText = (text) => {
|
|
172
|
+
updateCurrent(text);
|
|
173
|
+
setMountKey((key) => key + 1);
|
|
174
|
+
};
|
|
175
|
+
const switchView = (next) => {
|
|
176
|
+
if (next === view) return;
|
|
177
|
+
try {
|
|
178
|
+
flush();
|
|
179
|
+
} catch (error) {
|
|
180
|
+
toast.error(error instanceof Error ? error.message : String(error));
|
|
181
|
+
if (controlledView !== void 0) onViewChange?.(view);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
handle.current?.clearFind();
|
|
185
|
+
setView(next);
|
|
186
|
+
setMountKey((key) => key + 1);
|
|
187
|
+
if (next !== controlledView) onViewChange?.(next);
|
|
188
|
+
};
|
|
189
|
+
useEffect(() => {
|
|
190
|
+
if (controlledView !== void 0 && controlledView !== view) switchView(controlledView);
|
|
191
|
+
}, [controlledView]);
|
|
192
|
+
const doSave = async (plan) => {
|
|
193
|
+
if (!onSave) return;
|
|
194
|
+
setSaving(true);
|
|
195
|
+
ownSaves.current.add(plan.text);
|
|
196
|
+
try {
|
|
197
|
+
const readBack = await onSave(plan.text, plan);
|
|
198
|
+
const storedNow = typeof readBack === "string" ? readBack : plan.text;
|
|
199
|
+
const verified = typeof readBack === "string" ? readBack === plan.text : null;
|
|
200
|
+
setStored(storedNow);
|
|
201
|
+
lastValue.current = storedNow;
|
|
202
|
+
approved.current.clear();
|
|
203
|
+
setSaveState({ at: /* @__PURE__ */ new Date(), verified });
|
|
204
|
+
if (verified === false) {
|
|
205
|
+
toast.error("Saved, but what the database holds differs from the editor. Reload to see exactly what was stored.");
|
|
206
|
+
}
|
|
207
|
+
} catch (error) {
|
|
208
|
+
toast.error(`Not saved: ${error instanceof Error ? error.message : String(error)}. Your text is still here \u2014 try again.`);
|
|
209
|
+
} finally {
|
|
210
|
+
setSaving(false);
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
const save = () => {
|
|
214
|
+
if (!onSave || readOnly || saving) return;
|
|
215
|
+
let text;
|
|
216
|
+
try {
|
|
217
|
+
text = flush();
|
|
218
|
+
} catch (error) {
|
|
219
|
+
toast.error(error instanceof Error ? error.message : String(error));
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
const plan = planSave(stored, text, { approvedIslands: approved.current });
|
|
223
|
+
if (!plan.changed) {
|
|
224
|
+
if (onNothingToSave) onNothingToSave();
|
|
225
|
+
else toast.info("Nothing changed \u2014 the stored text is already exactly this.");
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
if (plan.needsConsent.length || plan.error) {
|
|
229
|
+
setPendingPlan(plan);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
void doSave(plan);
|
|
233
|
+
};
|
|
234
|
+
const pickImage = () => imageInput.current?.click();
|
|
235
|
+
const uploadImage = async (file) => {
|
|
236
|
+
try {
|
|
237
|
+
const uploaded = await upload({ kind: "file", file }, { folderPath: "Editor images" });
|
|
238
|
+
const alt = file.name.replace(/\.[^.]+$/, "").replace(/[[\]]/g, "");
|
|
239
|
+
return `.inline})`;
|
|
240
|
+
} catch (error) {
|
|
241
|
+
toast.error(`The image did not upload: ${error instanceof Error ? error.message : String(error)}`);
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
const shellRef = useRef(null);
|
|
246
|
+
const liveShell = {
|
|
247
|
+
save,
|
|
248
|
+
find: () => setFindMode("find"),
|
|
249
|
+
replace: () => setFindMode("replace"),
|
|
250
|
+
toggleOutline: () => setOutlineOpen((open) => !open),
|
|
251
|
+
toggleFocus: () => setFocusMode((on) => !on),
|
|
252
|
+
exitFocus: () => {
|
|
253
|
+
if (!focusMode) return false;
|
|
254
|
+
setFocusMode(false);
|
|
255
|
+
return true;
|
|
256
|
+
},
|
|
257
|
+
cycleView: () => switchView(VIEWS[(VIEWS.indexOf(view) + 1) % VIEWS.length] ?? "visual"),
|
|
258
|
+
showHelp: () => setHelpOpen(true),
|
|
259
|
+
showWordCount: () => toast.info(`${formatCount(metrics.words)} words \xB7 ${formatCount(metrics.characters)} characters \xB7 ${metrics.readingMinutes} min read`),
|
|
260
|
+
editLink: () => setLinkDialog({ open: true, href: handle.current?.currentLink() ?? "https://" }),
|
|
261
|
+
pickKind: () => new Promise((resolve) => setKindRequest(() => resolve)),
|
|
262
|
+
pickImage,
|
|
263
|
+
uploadImage,
|
|
264
|
+
variables: () => variables,
|
|
265
|
+
declareVariable: (name) => onDeclareVariable?.(name),
|
|
266
|
+
approveIsland: (raw) => approved.current.add(raw)
|
|
267
|
+
};
|
|
268
|
+
useEffect(() => {
|
|
269
|
+
shellRef.current = liveShell;
|
|
270
|
+
});
|
|
271
|
+
const [shell] = useState(() => ({
|
|
272
|
+
save: () => shellRef.current?.save(),
|
|
273
|
+
find: () => shellRef.current?.find(),
|
|
274
|
+
replace: () => shellRef.current?.replace(),
|
|
275
|
+
toggleOutline: () => shellRef.current?.toggleOutline(),
|
|
276
|
+
toggleFocus: () => shellRef.current?.toggleFocus(),
|
|
277
|
+
exitFocus: () => shellRef.current?.exitFocus() ?? false,
|
|
278
|
+
cycleView: () => shellRef.current?.cycleView(),
|
|
279
|
+
showHelp: () => shellRef.current?.showHelp(),
|
|
280
|
+
showWordCount: () => shellRef.current?.showWordCount(),
|
|
281
|
+
editLink: () => shellRef.current?.editLink(),
|
|
282
|
+
pickKind: () => shellRef.current?.pickKind() ?? Promise.resolve(null),
|
|
283
|
+
pickImage: () => shellRef.current?.pickImage(),
|
|
284
|
+
uploadImage: (file) => shellRef.current?.uploadImage(file) ?? Promise.resolve(null),
|
|
285
|
+
variables: () => shellRef.current?.variables() ?? null,
|
|
286
|
+
declareVariable: (name) => shellRef.current?.declareVariable(name),
|
|
287
|
+
approveIsland: (raw) => shellRef.current?.approveIsland(raw)
|
|
288
|
+
}));
|
|
289
|
+
const contextValue = {
|
|
290
|
+
variables,
|
|
291
|
+
approveIsland: (raw) => approved.current.add(raw),
|
|
292
|
+
uploadImage,
|
|
293
|
+
pickKind: shell.pickKind,
|
|
294
|
+
onDeclareVariable,
|
|
295
|
+
readOnly
|
|
296
|
+
};
|
|
297
|
+
const mic = useMicField({
|
|
298
|
+
label: "Editor",
|
|
299
|
+
getValue: () => "",
|
|
300
|
+
writeValue: () => void 0,
|
|
301
|
+
onTranscriptionComplete: (text) => {
|
|
302
|
+
if (text.trim()) handle.current?.replaceSelection(text.trim());
|
|
303
|
+
}
|
|
304
|
+
// No toast here: useMicField shows the one honest error (with its troubleshooting action).
|
|
305
|
+
});
|
|
306
|
+
const getApplicationScope = () => {
|
|
307
|
+
const selected = handle.current?.selectedText() ?? "";
|
|
308
|
+
return buildApplicationScopeFromMenuContext({
|
|
309
|
+
selectedText: selected,
|
|
310
|
+
selectionRange: null,
|
|
311
|
+
contextData: { content: current, active_text: selected || current }
|
|
312
|
+
});
|
|
313
|
+
};
|
|
314
|
+
const jump = (entry) => {
|
|
315
|
+
if (view === "preview") switchView("visual");
|
|
316
|
+
window.setTimeout(() => handle.current?.scrollToHeading(entry.slug, entry.offset), view === "preview" ? 200 : 0);
|
|
317
|
+
if (isMobile) setOutlineOpen(false);
|
|
318
|
+
};
|
|
319
|
+
useImperativeHandle(
|
|
320
|
+
controllerRef,
|
|
321
|
+
() => ({
|
|
322
|
+
focus: () => handle.current?.focus(),
|
|
323
|
+
selectedText: () => handle.current?.selectedText() ?? "",
|
|
324
|
+
replaceSelection: (text) => handle.current?.replaceSelection(text),
|
|
325
|
+
insertText: (text, where) => handle.current?.insertText(text, where) ?? false,
|
|
326
|
+
setText,
|
|
327
|
+
flush,
|
|
328
|
+
historyDepth: () => handle.current?.historyDepth() ?? { undo: 0, redo: 0 },
|
|
329
|
+
openFind: (withReplace = false) => setFindMode(withReplace ? "replace" : "find"),
|
|
330
|
+
placeCaret: (context) => handle.current?.placeCaret(context) ?? false,
|
|
331
|
+
jumpToOffset: (offset) => {
|
|
332
|
+
const entries = outlineOf(flush());
|
|
333
|
+
let entry;
|
|
334
|
+
for (const candidate of entries) {
|
|
335
|
+
if (candidate.offset > offset) break;
|
|
336
|
+
entry = candidate;
|
|
337
|
+
}
|
|
338
|
+
entry ??= entries[0];
|
|
339
|
+
if (entry) jump(entry);
|
|
340
|
+
}
|
|
341
|
+
})
|
|
342
|
+
);
|
|
343
|
+
const lockedCount = (loadStats?.lockedBlocks ?? 0) + (loadStats?.lockedChildren ?? 0);
|
|
344
|
+
const documentFacts = [
|
|
345
|
+
`${formatCount(metrics.words)} words \xB7 ${formatCount(metrics.characters)} characters \xB7 ${metrics.readingMinutes} min read`,
|
|
346
|
+
metrics.islands > 0 ? `${metrics.islands} protected (kinds, XML sections, code, math and variables are never rewritten by the editor)` : null,
|
|
347
|
+
view === "visual" && lockedCount > 0 ? `${lockedCount} kept as written (edit with its pencil or in Source)` : null
|
|
348
|
+
].filter(Boolean).join("\n");
|
|
349
|
+
const saveStatus = dirty ? "Unsaved changes" : saveState ? `Saved ${saveState.at.toLocaleTimeString()}${saveState.verified ? " \xB7 verified byte-for-byte" : ""}` : (
|
|
350
|
+
// read-gate-exempt: save status of the document in this editor (nothing edited), not an empty read
|
|
351
|
+
"No changes"
|
|
352
|
+
);
|
|
353
|
+
const derivedRecord = contentSource.readOnly ? null : annotationRecordOf(contentSource);
|
|
354
|
+
const recordHere = annotationRecord !== void 0 ? annotationRecord : derivedRecord ? { ...derivedRecord, body: stored } : null;
|
|
355
|
+
const visualRecord = recordHere && !readOnly && current === recordHere.body && current.trim() ? {
|
|
356
|
+
token: recordHere.token,
|
|
357
|
+
id: recordHere.id,
|
|
358
|
+
title: recordHere.title ?? "",
|
|
359
|
+
body: recordHere.body,
|
|
360
|
+
contentVersion: Math.max(1, recordHere.contentVersion ?? 1),
|
|
361
|
+
...recordHere.href ? { href: recordHere.href } : {}
|
|
362
|
+
} : null;
|
|
363
|
+
const viewEditor = view === "visual" ? /* @__PURE__ */ jsx(RecordAnnotations, { record: visualRecord, className: "h-full min-h-0", children: /* @__PURE__ */ jsx(
|
|
364
|
+
VisualEditor,
|
|
365
|
+
{
|
|
366
|
+
initialText: current,
|
|
367
|
+
onChange: updateCurrent,
|
|
368
|
+
onLoadStats: setLoadStats,
|
|
369
|
+
shell,
|
|
370
|
+
placeholder,
|
|
371
|
+
focusMode,
|
|
372
|
+
handleRef: handle
|
|
373
|
+
},
|
|
374
|
+
`visual-${mountKey}`
|
|
375
|
+
) }) : /* @__PURE__ */ jsx(
|
|
376
|
+
SourceEditor,
|
|
377
|
+
{
|
|
378
|
+
initialText: current,
|
|
379
|
+
onChange: updateCurrent,
|
|
380
|
+
shell,
|
|
381
|
+
placeholder,
|
|
382
|
+
focusMode,
|
|
383
|
+
renderIslands,
|
|
384
|
+
handleRef: handle,
|
|
385
|
+
raw: true
|
|
386
|
+
},
|
|
387
|
+
`source-${mountKey}`
|
|
388
|
+
);
|
|
389
|
+
const body = view === "preview" ? /* @__PURE__ */ jsx("div", { className: "h-full overflow-y-auto", children: /* @__PURE__ */ jsx("div", { className: "mx-auto max-w-3xl px-6 py-6 sm:px-10", children: /* @__PURE__ */ jsx(
|
|
390
|
+
RichDocument,
|
|
391
|
+
{
|
|
392
|
+
imagePolicy: "inherit",
|
|
393
|
+
content: current,
|
|
394
|
+
source: contentSource,
|
|
395
|
+
enableContextMenu: true,
|
|
396
|
+
hideCopyButton: true,
|
|
397
|
+
allowFullScreenEditor: false,
|
|
398
|
+
actions: { exclude: ["edit", "delete-message"] }
|
|
399
|
+
}
|
|
400
|
+
) }) }) : hostContextMenu ? /* @__PURE__ */ jsx("div", { className: "h-full min-h-0", children: viewEditor }) : /* @__PURE__ */ jsx(
|
|
401
|
+
EditableContextMenu,
|
|
402
|
+
{
|
|
403
|
+
sourceFeature,
|
|
404
|
+
surfaceName,
|
|
405
|
+
getApplicationScope,
|
|
406
|
+
insertAtCaret: (text) => {
|
|
407
|
+
if (!handle.current) return false;
|
|
408
|
+
handle.current.replaceSelection(text);
|
|
409
|
+
return true;
|
|
410
|
+
},
|
|
411
|
+
onTextReplace: setText,
|
|
412
|
+
onTextInsertBefore: (text) => handle.current?.insertText(text, "before"),
|
|
413
|
+
onTextInsertAfter: (text) => handle.current?.insertText(text, "after"),
|
|
414
|
+
onSave: onSave ? save : void 0,
|
|
415
|
+
children: /* @__PURE__ */ jsx("div", { className: "h-full min-h-0", children: viewEditor })
|
|
416
|
+
}
|
|
417
|
+
);
|
|
418
|
+
const CurrentViewIcon = VIEW_META[view].icon;
|
|
419
|
+
const tools = [
|
|
420
|
+
{ id: "find", label: "Find & replace (\u2318F)", icon: Search, active: findMode !== null, disabled: view === "preview", run: () => setFindMode((mode) => mode ? null : "find") },
|
|
421
|
+
{ id: "outline", label: "Outline (\u2318\u2325H)", icon: ListTree, active: outlineOpen, run: () => setOutlineOpen((open) => !open) },
|
|
422
|
+
{ id: "focus", label: "Focus mode (\u2318\u21E7F)", icon: Focus, active: focusMode, disabled: view === "preview", run: () => setFocusMode((on) => !on) },
|
|
423
|
+
...view === "source" ? [{ id: "islands", label: renderIslands ? "Show protected blocks as source" : "Render protected blocks", icon: AlignLeft, active: renderIslands, run: () => setRenderIslands((on) => !on) }] : [],
|
|
424
|
+
{ id: "shortcuts", label: "Keyboard shortcuts (\u2318/)", icon: Keyboard, run: () => setHelpOpen(true) },
|
|
425
|
+
// Desktop shows the count in the toolbar; a phone reads it from the tools menu.
|
|
426
|
+
...isMobile ? [{ id: "count", label: `${formatCount(metrics.words)} words \xB7 ${saveStatus}`, icon: Hash, run: () => toast.info(documentFacts) }] : []
|
|
427
|
+
];
|
|
428
|
+
const micButton = mic.available && !readOnly && view !== "preview" ? /* @__PURE__ */ jsx(
|
|
429
|
+
ToolbarButton,
|
|
430
|
+
{
|
|
431
|
+
label: mic.isRecording ? "Stop dictation" : mic.isTranscribing ? "Finishing the transcript\u2026" : "Dictate at the cursor",
|
|
432
|
+
active: mic.isRecording,
|
|
433
|
+
onClick: () => void mic.handleVoiceClick(),
|
|
434
|
+
children: mic.isTranscribing ? /* @__PURE__ */ jsx(Loader2, { className: "h-4 w-4 animate-spin" }) : mic.isRecording ? /* @__PURE__ */ jsx(MicOff, { className: "h-4 w-4 text-destructive" }) : /* @__PURE__ */ jsx(Mic, { className: "h-4 w-4" })
|
|
435
|
+
}
|
|
436
|
+
) : null;
|
|
437
|
+
const cancelButton = onCancel ? /* @__PURE__ */ jsx(
|
|
438
|
+
"button",
|
|
439
|
+
{
|
|
440
|
+
type: "button",
|
|
441
|
+
onClick: onCancel,
|
|
442
|
+
disabled: saving,
|
|
443
|
+
className: "flex h-8 items-center rounded-md px-3 text-xs font-medium text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
444
|
+
title: dirty ? "Leave without saving \u2014 your unsaved changes are dropped" : "Close the editor",
|
|
445
|
+
children: cancelLabel
|
|
446
|
+
}
|
|
447
|
+
) : null;
|
|
448
|
+
const saveButton = onSave && !readOnly ? /* @__PURE__ */ jsxs(
|
|
449
|
+
"button",
|
|
450
|
+
{
|
|
451
|
+
type: "button",
|
|
452
|
+
onClick: save,
|
|
453
|
+
disabled: saving,
|
|
454
|
+
className: cn(
|
|
455
|
+
"flex h-8 items-center gap-1.5 rounded-md px-3 text-xs font-medium",
|
|
456
|
+
dirty ? "bg-primary text-primary-foreground hover:bg-primary/90" : "border border-border text-muted-foreground"
|
|
457
|
+
),
|
|
458
|
+
title: `${saveStatus}. Save (\u2318S) \u2014 only what you changed is written`,
|
|
459
|
+
children: [
|
|
460
|
+
saving ? /* @__PURE__ */ jsx(Loader2, { className: "h-3.5 w-3.5 animate-spin" }) : /* @__PURE__ */ jsx(Save, { className: "h-3.5 w-3.5" }),
|
|
461
|
+
dirty ? saveLabel : "Saved"
|
|
462
|
+
]
|
|
463
|
+
}
|
|
464
|
+
) : null;
|
|
465
|
+
const editor = /* @__PURE__ */ jsxs(RichEditorContext.Provider, { value: contextValue, children: [
|
|
466
|
+
/* @__PURE__ */ jsxs(
|
|
467
|
+
"div",
|
|
468
|
+
{
|
|
469
|
+
className: cn("relative flex h-full min-h-0 flex-col bg-textured", className),
|
|
470
|
+
"data-testid": "rich-editor",
|
|
471
|
+
"data-rich-editor": "",
|
|
472
|
+
onKeyDown: (event) => {
|
|
473
|
+
if (event.key === "Escape" && focusMode && !event.defaultPrevented) {
|
|
474
|
+
event.preventDefault();
|
|
475
|
+
setFocusMode(false);
|
|
476
|
+
}
|
|
477
|
+
},
|
|
478
|
+
children: [
|
|
479
|
+
focusMode && /* @__PURE__ */ jsxs("div", { className: "matrx-touch-targets absolute right-2 top-2 z-20 flex items-center gap-1 rounded-lg border border-border bg-card/90 p-1 shadow-sm backdrop-blur", children: [
|
|
480
|
+
/* @__PURE__ */ jsxs(
|
|
481
|
+
"button",
|
|
482
|
+
{
|
|
483
|
+
type: "button",
|
|
484
|
+
onClick: () => setFocusMode(false),
|
|
485
|
+
className: "flex h-8 items-center gap-1.5 rounded-md px-2 text-xs font-medium text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
486
|
+
title: "Exit focus mode (Esc or \u2318\u21E7F)",
|
|
487
|
+
children: [
|
|
488
|
+
/* @__PURE__ */ jsx(Minimize2, { className: "h-3.5 w-3.5" }),
|
|
489
|
+
"Exit focus"
|
|
490
|
+
]
|
|
491
|
+
}
|
|
492
|
+
),
|
|
493
|
+
saveButton
|
|
494
|
+
] }),
|
|
495
|
+
/* @__PURE__ */ jsxs("div", { className: "relative z-20 shrink-0", children: [
|
|
496
|
+
!focusMode && chrome === "full" && /* @__PURE__ */ jsxs("div", { className: "matrx-touch-targets flex min-h-10 items-center gap-1 border-b border-border bg-card/80 px-2 py-1 backdrop-blur", children: [
|
|
497
|
+
/* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 items-center gap-1 overflow-x-auto", children: [
|
|
498
|
+
isMobile ? (
|
|
499
|
+
// A phone gets the view as one compact menu, so every control fits.
|
|
500
|
+
/* @__PURE__ */ jsxs(DropdownMenu, { children: [
|
|
501
|
+
/* @__PURE__ */ jsx(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
|
|
502
|
+
"button",
|
|
503
|
+
{
|
|
504
|
+
type: "button",
|
|
505
|
+
"aria-label": `View: ${VIEW_META[view].label}`,
|
|
506
|
+
title: "Switch view",
|
|
507
|
+
className: "flex h-8 shrink-0 items-center gap-1 rounded-md border border-border bg-background/60 px-2 text-xs font-medium text-primary",
|
|
508
|
+
children: [
|
|
509
|
+
/* @__PURE__ */ jsx(CurrentViewIcon, { className: "h-3.5 w-3.5" }),
|
|
510
|
+
/* @__PURE__ */ jsx(ChevronDown, { className: "h-3 w-3 text-muted-foreground" })
|
|
511
|
+
]
|
|
512
|
+
}
|
|
513
|
+
) }),
|
|
514
|
+
/* @__PURE__ */ jsx(DropdownMenuContent, { align: "start", className: "w-44", children: VIEWS.map((option) => {
|
|
515
|
+
const Icon = VIEW_META[option].icon;
|
|
516
|
+
return /* @__PURE__ */ jsxs(DropdownMenuItem, { onSelect: () => switchView(option), children: [
|
|
517
|
+
/* @__PURE__ */ jsx(Icon, { className: "mr-2 h-4 w-4" }),
|
|
518
|
+
/* @__PURE__ */ jsx("span", { className: "flex-1", children: VIEW_META[option].label }),
|
|
519
|
+
view === option && /* @__PURE__ */ jsx(Check, { className: "ml-2 h-4 w-4 text-primary" })
|
|
520
|
+
] }, option);
|
|
521
|
+
}) })
|
|
522
|
+
] })
|
|
523
|
+
) : /* @__PURE__ */ jsx("div", { role: "tablist", "aria-label": "View", className: "flex shrink-0 items-center rounded-md border border-border bg-background/60 p-0.5", children: VIEWS.map((option) => {
|
|
524
|
+
const Icon = VIEW_META[option].icon;
|
|
525
|
+
return /* @__PURE__ */ jsxs(
|
|
526
|
+
"button",
|
|
527
|
+
{
|
|
528
|
+
type: "button",
|
|
529
|
+
role: "tab",
|
|
530
|
+
title: VIEW_META[option].label,
|
|
531
|
+
"aria-selected": view === option,
|
|
532
|
+
onClick: () => switchView(option),
|
|
533
|
+
className: cn(
|
|
534
|
+
"flex h-7 items-center gap-1 rounded px-2 text-xs font-medium text-muted-foreground",
|
|
535
|
+
view === option && "bg-primary/10 text-primary-ink"
|
|
536
|
+
),
|
|
537
|
+
children: [
|
|
538
|
+
/* @__PURE__ */ jsx(Icon, { className: "h-3.5 w-3.5" }),
|
|
539
|
+
VIEW_META[option].label
|
|
540
|
+
]
|
|
541
|
+
},
|
|
542
|
+
option
|
|
543
|
+
);
|
|
544
|
+
}) }),
|
|
545
|
+
!isMobile && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
546
|
+
/* @__PURE__ */ jsx("span", { className: "mx-1 h-5 w-px shrink-0 bg-border" }),
|
|
547
|
+
tools.map((tool) => /* @__PURE__ */ jsx(ToolbarButton, { label: tool.label, active: tool.active, onClick: tool.run, disabled: tool.disabled, children: /* @__PURE__ */ jsx(tool.icon, { className: "h-4 w-4" }) }, tool.id)),
|
|
548
|
+
micButton
|
|
549
|
+
] })
|
|
550
|
+
] }),
|
|
551
|
+
/* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center gap-1", children: [
|
|
552
|
+
isMobile && micButton,
|
|
553
|
+
isMobile && /* @__PURE__ */ jsxs(DropdownMenu, { children: [
|
|
554
|
+
/* @__PURE__ */ jsx(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
|
|
555
|
+
"button",
|
|
556
|
+
{
|
|
557
|
+
type: "button",
|
|
558
|
+
"aria-label": "More tools",
|
|
559
|
+
title: "More tools",
|
|
560
|
+
className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-md text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
561
|
+
children: /* @__PURE__ */ jsx(MoreHorizontal, { className: "h-4 w-4" })
|
|
562
|
+
}
|
|
563
|
+
) }),
|
|
564
|
+
/* @__PURE__ */ jsx(DropdownMenuContent, { align: "end", className: "w-60", children: tools.map((tool) => /* @__PURE__ */ jsxs(DropdownMenuItem, { disabled: tool.disabled, onSelect: tool.run, children: [
|
|
565
|
+
/* @__PURE__ */ jsx(tool.icon, { className: "mr-2 h-4 w-4" }),
|
|
566
|
+
/* @__PURE__ */ jsx("span", { className: "flex-1", children: tool.label }),
|
|
567
|
+
tool.active && /* @__PURE__ */ jsx(Check, { className: "ml-2 h-4 w-4 text-primary" })
|
|
568
|
+
] }, tool.id)) })
|
|
569
|
+
] }),
|
|
570
|
+
!isMobile && /* @__PURE__ */ jsxs("span", { className: "shrink-0 whitespace-nowrap px-1 text-[11px] tabular-nums text-muted-foreground", title: documentFacts, children: [
|
|
571
|
+
formatCount(metrics.words),
|
|
572
|
+
" words"
|
|
573
|
+
] }),
|
|
574
|
+
toolbarExtras,
|
|
575
|
+
cancelButton,
|
|
576
|
+
saveButton
|
|
577
|
+
] })
|
|
578
|
+
] }),
|
|
579
|
+
findMode && view !== "preview" && /* @__PURE__ */ jsx(
|
|
580
|
+
FindReplacePanel,
|
|
581
|
+
{
|
|
582
|
+
showReplace: findMode === "replace",
|
|
583
|
+
onFind: (query, options, step) => handle.current?.find(query, options, step) ?? { count: 0, current: -1, skippedProtected: 0, error: null },
|
|
584
|
+
onReplace: (query, replacement, options) => {
|
|
585
|
+
for (const raw of handle.current?.replaceCurrent(query, replacement, options) ?? []) approved.current.add(raw);
|
|
586
|
+
},
|
|
587
|
+
onReplaceAll: (query, replacement, options) => {
|
|
588
|
+
const before = handle.current?.find(query, options).count ?? 0;
|
|
589
|
+
for (const raw of handle.current?.replaceAll(query, replacement, options) ?? []) approved.current.add(raw);
|
|
590
|
+
return before;
|
|
591
|
+
},
|
|
592
|
+
onClose: () => {
|
|
593
|
+
handle.current?.clearFind();
|
|
594
|
+
setFindMode(null);
|
|
595
|
+
handle.current?.focus();
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
)
|
|
599
|
+
] }),
|
|
600
|
+
/* @__PURE__ */ jsxs("div", { className: "flex min-h-0 flex-1", children: [
|
|
601
|
+
outlineOpen && !focusMode && !isMobile && /* @__PURE__ */ jsx("aside", { className: "hidden w-56 shrink-0 border-r border-border bg-card/40 md:block", children: /* @__PURE__ */ jsx(OutlinePanel, { entries: outline, onJump: jump }) }),
|
|
602
|
+
/* @__PURE__ */ jsxs("div", { className: "relative min-h-0 min-w-0 flex-1", children: [
|
|
603
|
+
body,
|
|
604
|
+
mic.isRecording && mic.liveTranscript && /* @__PURE__ */ jsxs(
|
|
605
|
+
"div",
|
|
606
|
+
{
|
|
607
|
+
className: "pointer-events-none absolute inset-x-3 bottom-3 z-20 mx-auto max-w-xl truncate rounded-lg border border-border bg-popover/95 px-3 py-1.5 text-xs text-muted-foreground shadow-lg backdrop-blur",
|
|
608
|
+
"aria-live": "polite",
|
|
609
|
+
children: [
|
|
610
|
+
/* @__PURE__ */ jsx(Mic, { className: "mr-1.5 inline h-3 w-3 text-destructive", "aria-hidden": true }),
|
|
611
|
+
mic.liveTranscript
|
|
612
|
+
]
|
|
613
|
+
}
|
|
614
|
+
)
|
|
615
|
+
] })
|
|
616
|
+
] })
|
|
617
|
+
]
|
|
618
|
+
}
|
|
619
|
+
),
|
|
620
|
+
isMobile && /* @__PURE__ */ jsx(Sheet, { open: outlineOpen, onOpenChange: setOutlineOpen, children: /* @__PURE__ */ jsxs(SheetContent, { side: "bottom", className: "h-[70dvh] p-0 pb-safe", children: [
|
|
621
|
+
/* @__PURE__ */ jsx(SheetHeader, { className: "sr-only", children: /* @__PURE__ */ jsx(SheetTitle, { children: "Outline" }) }),
|
|
622
|
+
/* @__PURE__ */ jsx(OutlinePanel, { entries: outline, onJump: jump })
|
|
623
|
+
] }) }),
|
|
624
|
+
/* @__PURE__ */ jsx(ShortcutsDialog, { open: helpOpen, onOpenChange: setHelpOpen }),
|
|
625
|
+
/* @__PURE__ */ jsx(
|
|
626
|
+
KindPicker,
|
|
627
|
+
{
|
|
628
|
+
open: kindRequest !== null,
|
|
629
|
+
onResolve: (markdown) => {
|
|
630
|
+
kindRequest?.(markdown);
|
|
631
|
+
setKindRequest(null);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
),
|
|
635
|
+
/* @__PURE__ */ jsx(
|
|
636
|
+
TextInputDialog,
|
|
637
|
+
{
|
|
638
|
+
open: linkDialog.open,
|
|
639
|
+
onOpenChange: (open) => setLinkDialog((state) => ({ ...state, open })),
|
|
640
|
+
title: "Link",
|
|
641
|
+
description: "Paste the address. Leave it empty to remove the link.",
|
|
642
|
+
placeholder: "https://",
|
|
643
|
+
defaultValue: linkDialog.href,
|
|
644
|
+
confirmLabel: "Apply",
|
|
645
|
+
onConfirm: (href) => {
|
|
646
|
+
handle.current?.editLink(href.trim() && href.trim() !== "https://" ? href.trim() : null);
|
|
647
|
+
setLinkDialog({ open: false, href: "" });
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
),
|
|
651
|
+
/* @__PURE__ */ jsx(
|
|
652
|
+
"input",
|
|
653
|
+
{
|
|
654
|
+
ref: imageInput,
|
|
655
|
+
type: "file",
|
|
656
|
+
accept: "image/*",
|
|
657
|
+
className: "hidden",
|
|
658
|
+
onChange: (event) => {
|
|
659
|
+
const file = event.target.files?.[0];
|
|
660
|
+
event.target.value = "";
|
|
661
|
+
if (!file) return;
|
|
662
|
+
void uploadImage(file).then((markdown) => {
|
|
663
|
+
if (markdown) handle.current?.replaceSelection(markdown);
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
),
|
|
668
|
+
/* @__PURE__ */ jsx(
|
|
669
|
+
ConfirmDialog,
|
|
670
|
+
{
|
|
671
|
+
open: pendingPlan !== null,
|
|
672
|
+
onOpenChange: (open) => !open && setPendingPlan(null),
|
|
673
|
+
title: "This save changes protected content",
|
|
674
|
+
description: pendingPlan?.error ?? "Kinds, XML sections, code, math and {{variables}} are protected. You did not change these through their own editors, so here is exactly what saving would do:",
|
|
675
|
+
content: pendingPlan && /* @__PURE__ */ jsx("ul", { className: "max-h-60 list-disc space-y-1 overflow-y-auto pl-5 text-sm", children: pendingPlan.needsConsent.map((delta, index) => /* @__PURE__ */ jsx("li", { children: describeDelta(delta) }, `${delta.at}-${index}`)) }),
|
|
676
|
+
confirmLabel: "Save anyway",
|
|
677
|
+
cancelLabel: "Go back and fix it",
|
|
678
|
+
onConfirm: async () => {
|
|
679
|
+
const plan = pendingPlan;
|
|
680
|
+
setPendingPlan(null);
|
|
681
|
+
if (plan) await doSave(plan);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
)
|
|
685
|
+
] });
|
|
686
|
+
return /* @__PURE__ */ jsx(Fragment, { children: withImagePolicy(imagePolicy, editor) });
|
|
687
|
+
}
|
|
688
|
+
function ToolbarButton({
|
|
689
|
+
label,
|
|
690
|
+
active,
|
|
691
|
+
disabled,
|
|
692
|
+
onClick,
|
|
693
|
+
children
|
|
694
|
+
}) {
|
|
695
|
+
return /* @__PURE__ */ jsx(
|
|
696
|
+
"button",
|
|
697
|
+
{
|
|
698
|
+
type: "button",
|
|
699
|
+
title: label,
|
|
700
|
+
"aria-label": label,
|
|
701
|
+
"aria-pressed": active,
|
|
702
|
+
disabled,
|
|
703
|
+
onClick,
|
|
704
|
+
className: cn(
|
|
705
|
+
"flex h-8 w-8 shrink-0 items-center justify-center rounded-md text-muted-foreground hover:bg-muted hover:text-foreground disabled:opacity-40",
|
|
706
|
+
active && "bg-primary/10 text-primary-ink"
|
|
707
|
+
),
|
|
708
|
+
children
|
|
709
|
+
}
|
|
710
|
+
);
|
|
711
|
+
}
|
|
712
|
+
export {
|
|
713
|
+
RichEditorImpl as default
|
|
714
|
+
};
|