@tessera-editor/react 0.1.0 → 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/dist/index.d.ts +35 -13
- package/dist/index.js +456 -387
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/EmptyLineToolbar.tsx +8 -11
- package/src/ImageNodeView.tsx +38 -3
- package/src/LinkEditor.tsx +121 -0
- package/src/SelectionToolbar.tsx +21 -11
- package/src/SlashMenu.tsx +1 -1
- package/src/TableNodeViews.tsx +59 -9
- package/src/Tessera.tsx +62 -18
- package/src/__tests__/host-config.test.tsx +117 -0
- package/src/__tests__/setup.ts +4 -0
- package/src/__tests__/slash-menu-scroll.test.tsx +53 -0
- package/src/context.ts +7 -0
- package/src/index.ts +2 -1
- package/src/HistoryPanel.tsx +0 -166
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/Tessera.tsx
|
|
2
|
-
import { useCallback as useCallback3, useEffect as
|
|
2
|
+
import { useCallback as useCallback3, useEffect as useEffect13, useMemo, useRef as useRef9, useState as useState13 } from "react";
|
|
3
3
|
import { EditorContent, useEditor } from "@tiptap/react";
|
|
4
4
|
import { DragHandle } from "@tiptap/extension-drag-handle-react";
|
|
5
5
|
import {
|
|
@@ -28,7 +28,6 @@ function useTesseraContext() {
|
|
|
28
28
|
// src/ImageNodeView.tsx
|
|
29
29
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
30
30
|
function AiImageNodeView({ node, updateAttributes, selected, decorations }) {
|
|
31
|
-
console.log("[imgview] decorations:", Array.isArray(decorations), decorations?.length);
|
|
32
31
|
const galleryAttrs = (() => {
|
|
33
32
|
for (const deco of decorations ?? []) {
|
|
34
33
|
const attrs = deco.type?.attrs;
|
|
@@ -41,13 +40,28 @@ function AiImageNodeView({ node, updateAttributes, selected, decorations }) {
|
|
|
41
40
|
const { t } = useContext2(TesseraContext);
|
|
42
41
|
const imgRef = useRef(null);
|
|
43
42
|
const [preview, setPreview] = useState(null);
|
|
43
|
+
const [zoom, setZoom] = useState(1);
|
|
44
|
+
const clampZoom = (z) => Math.min(8, Math.max(0.2, z));
|
|
44
45
|
useEffect(() => {
|
|
45
46
|
if (!preview) return;
|
|
47
|
+
setZoom(1);
|
|
46
48
|
const close = (e) => {
|
|
47
49
|
if (e.key === "Escape") setPreview(null);
|
|
50
|
+
else if (e.key === "+" || e.key === "=") setZoom((z) => clampZoom(z * 1.25));
|
|
51
|
+
else if (e.key === "-") setZoom((z) => clampZoom(z / 1.25));
|
|
52
|
+
else if (e.key === "0") setZoom(1);
|
|
53
|
+
};
|
|
54
|
+
const onWheel = (e) => {
|
|
55
|
+
if (!e.ctrlKey) return;
|
|
56
|
+
e.preventDefault();
|
|
57
|
+
setZoom((z) => clampZoom(z * (e.deltaY < 0 ? 1.15 : 1 / 1.15)));
|
|
48
58
|
};
|
|
49
59
|
window.addEventListener("keydown", close);
|
|
50
|
-
|
|
60
|
+
window.addEventListener("wheel", onWheel, { passive: false });
|
|
61
|
+
return () => {
|
|
62
|
+
window.removeEventListener("keydown", close);
|
|
63
|
+
window.removeEventListener("wheel", onWheel);
|
|
64
|
+
};
|
|
51
65
|
}, [preview]);
|
|
52
66
|
const { src, alt, width, align } = node.attrs;
|
|
53
67
|
const startResize = (event) => {
|
|
@@ -97,7 +111,28 @@ function AiImageNodeView({ node, updateAttributes, selected, decorations }) {
|
|
|
97
111
|
/* @__PURE__ */ jsx("div", { className: "tessera-image-resize", onPointerDown: startResize, title: "\u2194" })
|
|
98
112
|
] }),
|
|
99
113
|
preview ? createPortal(
|
|
100
|
-
/* @__PURE__ */
|
|
114
|
+
/* @__PURE__ */ jsxs("div", { className: "tessera-lightbox", onClick: () => setPreview(null), children: [
|
|
115
|
+
/* @__PURE__ */ jsx(
|
|
116
|
+
"img",
|
|
117
|
+
{
|
|
118
|
+
src: preview,
|
|
119
|
+
alt: alt ?? "",
|
|
120
|
+
style: { transform: `scale(${zoom})` },
|
|
121
|
+
onClick: (e) => e.stopPropagation(),
|
|
122
|
+
onDoubleClick: () => setZoom(1)
|
|
123
|
+
}
|
|
124
|
+
),
|
|
125
|
+
/* @__PURE__ */ jsxs("div", { className: "tessera-lightbox-bar", onClick: (e) => e.stopPropagation(), children: [
|
|
126
|
+
/* @__PURE__ */ jsx("button", { type: "button", title: t("imageZoomOut"), onClick: () => setZoom((z) => clampZoom(z / 1.25)), children: "\u2212" }),
|
|
127
|
+
/* @__PURE__ */ jsxs("span", { className: "tessera-lightbox-scale", children: [
|
|
128
|
+
Math.round(zoom * 100),
|
|
129
|
+
"%"
|
|
130
|
+
] }),
|
|
131
|
+
/* @__PURE__ */ jsx("button", { type: "button", title: t("imageZoomIn"), onClick: () => setZoom((z) => clampZoom(z * 1.25)), children: "+" }),
|
|
132
|
+
/* @__PURE__ */ jsx("button", { type: "button", title: t("imageZoomReset"), onClick: () => setZoom(1), children: "1:1" }),
|
|
133
|
+
/* @__PURE__ */ jsx("span", { className: "tessera-lightbox-hint", children: t("imageZoomHint") })
|
|
134
|
+
] })
|
|
135
|
+
] }),
|
|
101
136
|
document.body
|
|
102
137
|
) : null,
|
|
103
138
|
/* @__PURE__ */ jsxs("div", { className: "tessera-image-align", children: [
|
|
@@ -116,7 +151,8 @@ var ImageBlockView = ImageBlock.extend({
|
|
|
116
151
|
});
|
|
117
152
|
|
|
118
153
|
// src/TableNodeViews.tsx
|
|
119
|
-
import { useContext as useContext3, useState as
|
|
154
|
+
import { useContext as useContext3, useEffect as useEffect3, useRef as useRef2, useState as useState3 } from "react";
|
|
155
|
+
import { createPortal as createPortal2 } from "react-dom";
|
|
120
156
|
import { NodeViewWrapper as NodeViewWrapper2, NodeViewContent, ReactNodeViewRenderer as ReactNodeViewRenderer2 } from "@tiptap/react";
|
|
121
157
|
import {
|
|
122
158
|
AiTableCell,
|
|
@@ -124,6 +160,32 @@ import {
|
|
|
124
160
|
TABLE_COLUMN_KINDS,
|
|
125
161
|
tableToCsvAt
|
|
126
162
|
} from "@tessera-editor/core";
|
|
163
|
+
|
|
164
|
+
// src/portal.ts
|
|
165
|
+
import { useEffect as useEffect2, useState as useState2 } from "react";
|
|
166
|
+
function useTesseraPortalRoot(editor) {
|
|
167
|
+
const [target, setTarget] = useState2(null);
|
|
168
|
+
useEffect2(() => {
|
|
169
|
+
const resolve = () => {
|
|
170
|
+
setTarget((prev) => {
|
|
171
|
+
if (prev && prev !== document.body) {
|
|
172
|
+
return prev;
|
|
173
|
+
}
|
|
174
|
+
return editor.view.dom.closest(".tessera-root") ?? document.body;
|
|
175
|
+
});
|
|
176
|
+
};
|
|
177
|
+
resolve();
|
|
178
|
+
editor.on("focus", resolve);
|
|
179
|
+
editor.on("selectionUpdate", resolve);
|
|
180
|
+
return () => {
|
|
181
|
+
editor.off("focus", resolve);
|
|
182
|
+
editor.off("selectionUpdate", resolve);
|
|
183
|
+
};
|
|
184
|
+
}, [editor]);
|
|
185
|
+
return target;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// src/TableNodeViews.tsx
|
|
127
189
|
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
128
190
|
function kindLabel(t, kind) {
|
|
129
191
|
switch (kind) {
|
|
@@ -153,9 +215,39 @@ function columnIndex(props) {
|
|
|
153
215
|
}
|
|
154
216
|
function AiTableHeaderView(props) {
|
|
155
217
|
const { editor, t } = useContext3(TesseraContext);
|
|
156
|
-
const
|
|
218
|
+
const portalRoot = useTesseraPortalRoot(editor);
|
|
219
|
+
const [open, setOpen] = useState3(false);
|
|
220
|
+
const [style, setStyle] = useState3({});
|
|
221
|
+
const thRef = useRef2(null);
|
|
222
|
+
const menuRef = useRef2(null);
|
|
157
223
|
const index = columnIndex(props);
|
|
224
|
+
const toggle = () => {
|
|
225
|
+
const th = thRef.current;
|
|
226
|
+
if (th) {
|
|
227
|
+
const r = th.getBoundingClientRect();
|
|
228
|
+
const flip = window.innerHeight - r.bottom < 340 && r.top > 340;
|
|
229
|
+
const left = Math.min(Math.max(8, r.left), window.innerWidth - 198);
|
|
230
|
+
setStyle(
|
|
231
|
+
flip ? { left: `${left}px`, top: "auto", bottom: `${window.innerHeight - r.top + 6}px` } : { left: `${left}px`, top: `${r.bottom + 6}px`, bottom: "auto" }
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
setOpen((v) => !v);
|
|
235
|
+
};
|
|
236
|
+
useEffect3(() => {
|
|
237
|
+
if (!open) return;
|
|
238
|
+
const onDown = (e) => {
|
|
239
|
+
const target = e.target;
|
|
240
|
+
if (menuRef.current?.contains(target) || target.closest(".tessera-col-menu-btn")) return;
|
|
241
|
+
setOpen(false);
|
|
242
|
+
};
|
|
243
|
+
window.addEventListener("mousedown", onDown);
|
|
244
|
+
return () => window.removeEventListener("mousedown", onDown);
|
|
245
|
+
}, [open]);
|
|
158
246
|
const run = (fn) => {
|
|
247
|
+
if (!editor.isEditable) {
|
|
248
|
+
setOpen(false);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
159
251
|
setOpen(false);
|
|
160
252
|
const pos = props.getPos();
|
|
161
253
|
if (typeof pos === "number") {
|
|
@@ -163,7 +255,7 @@ function AiTableHeaderView(props) {
|
|
|
163
255
|
}
|
|
164
256
|
void fn();
|
|
165
257
|
};
|
|
166
|
-
return /* @__PURE__ */ jsxs2(NodeViewWrapper2, { as: "th", className: "tessera-th", "data-index": index, children: [
|
|
258
|
+
return /* @__PURE__ */ jsxs2(NodeViewWrapper2, { ref: thRef, as: "th", className: "tessera-th", "data-index": index, children: [
|
|
167
259
|
/* @__PURE__ */ jsx2(NodeViewContent, { className: "tessera-th-content" }),
|
|
168
260
|
/* @__PURE__ */ jsx2(
|
|
169
261
|
"button",
|
|
@@ -173,36 +265,49 @@ function AiTableHeaderView(props) {
|
|
|
173
265
|
contentEditable: false,
|
|
174
266
|
title: t("colMenuTitle"),
|
|
175
267
|
onMouseDown: (e) => e.preventDefault(),
|
|
176
|
-
onClick:
|
|
268
|
+
onClick: toggle,
|
|
177
269
|
children: "\u2304"
|
|
178
270
|
}
|
|
179
271
|
),
|
|
180
|
-
open
|
|
181
|
-
/* @__PURE__ */
|
|
182
|
-
|
|
183
|
-
/* @__PURE__ */ jsx2("button", { type: "button", onClick: () => run(() => editor.commands.sortTableByColumn(index, "asc")), children: t("colMenuSortAsc") }),
|
|
184
|
-
/* @__PURE__ */ jsx2("button", { type: "button", onClick: () => run(() => editor.commands.sortTableByColumn(index, "desc")), children: t("colMenuSortDesc") }),
|
|
185
|
-
/* @__PURE__ */ jsx2("div", { className: "tessera-menu-sep" }),
|
|
186
|
-
/* @__PURE__ */ jsx2("button", { type: "button", onClick: () => run(() => editor.commands.addColumnBefore()), children: t("colMenuInsertLeft") }),
|
|
187
|
-
/* @__PURE__ */ jsx2("button", { type: "button", onClick: () => run(() => editor.commands.addColumnAfter()), children: t("colMenuInsertRight") }),
|
|
188
|
-
/* @__PURE__ */ jsx2("button", { type: "button", className: "tessera-danger", onClick: () => run(() => editor.commands.deleteColumn()), children: t("colMenuDelete") }),
|
|
189
|
-
/* @__PURE__ */ jsx2("div", { className: "tessera-menu-sep" }),
|
|
190
|
-
/* @__PURE__ */ jsx2("button", { type: "button", onClick: () => run(() => editor.commands.toggleHeaderRow()), children: t("tableToggleHeader") }),
|
|
191
|
-
/* @__PURE__ */ jsx2(
|
|
192
|
-
"button",
|
|
272
|
+
open && portalRoot ? createPortal2(
|
|
273
|
+
/* @__PURE__ */ jsxs2(
|
|
274
|
+
"div",
|
|
193
275
|
{
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
276
|
+
ref: menuRef,
|
|
277
|
+
className: "tessera-popover tessera-col-menu",
|
|
278
|
+
style: { position: "fixed", ...style },
|
|
279
|
+
contentEditable: false,
|
|
280
|
+
onMouseDown: (e) => e.preventDefault(),
|
|
281
|
+
children: [
|
|
282
|
+
/* @__PURE__ */ jsx2("div", { className: "tessera-menu-section", children: TABLE_COLUMN_KINDS.map((kind) => /* @__PURE__ */ jsx2("button", { type: "button", onClick: () => run(() => editor.commands.setColumnType(index, kind)), children: kindLabel(t, kind) }, kind)) }),
|
|
283
|
+
/* @__PURE__ */ jsx2("div", { className: "tessera-menu-sep" }),
|
|
284
|
+
/* @__PURE__ */ jsx2("button", { type: "button", onClick: () => run(() => editor.commands.sortTableByColumn(index, "asc")), children: t("colMenuSortAsc") }),
|
|
285
|
+
/* @__PURE__ */ jsx2("button", { type: "button", onClick: () => run(() => editor.commands.sortTableByColumn(index, "desc")), children: t("colMenuSortDesc") }),
|
|
286
|
+
/* @__PURE__ */ jsx2("div", { className: "tessera-menu-sep" }),
|
|
287
|
+
/* @__PURE__ */ jsx2("button", { type: "button", onClick: () => run(() => editor.commands.addColumnBefore()), children: t("colMenuInsertLeft") }),
|
|
288
|
+
/* @__PURE__ */ jsx2("button", { type: "button", onClick: () => run(() => editor.commands.addColumnAfter()), children: t("colMenuInsertRight") }),
|
|
289
|
+
/* @__PURE__ */ jsx2("button", { type: "button", className: "tessera-danger", onClick: () => run(() => editor.commands.deleteColumn()), children: t("colMenuDelete") }),
|
|
290
|
+
/* @__PURE__ */ jsx2("div", { className: "tessera-menu-sep" }),
|
|
291
|
+
/* @__PURE__ */ jsx2("button", { type: "button", onClick: () => run(() => editor.commands.toggleHeaderRow()), children: t("tableToggleHeader") }),
|
|
292
|
+
/* @__PURE__ */ jsx2(
|
|
293
|
+
"button",
|
|
294
|
+
{
|
|
295
|
+
type: "button",
|
|
296
|
+
onClick: () => run(() => {
|
|
297
|
+
const csv = tableToCsvAt(editor);
|
|
298
|
+
if (csv) {
|
|
299
|
+
void navigator.clipboard.writeText(csv);
|
|
300
|
+
}
|
|
301
|
+
}),
|
|
302
|
+
children: t("tableCopyCsv")
|
|
303
|
+
}
|
|
304
|
+
),
|
|
305
|
+
/* @__PURE__ */ jsx2("button", { type: "button", className: "tessera-danger", onClick: () => run(() => editor.commands.deleteTable()), children: t("tableDelete") })
|
|
306
|
+
]
|
|
202
307
|
}
|
|
203
308
|
),
|
|
204
|
-
|
|
205
|
-
|
|
309
|
+
portalRoot
|
|
310
|
+
) : null
|
|
206
311
|
] });
|
|
207
312
|
}
|
|
208
313
|
function AiTableCellView(props) {
|
|
@@ -231,10 +336,10 @@ function AiTableCellView(props) {
|
|
|
231
336
|
return types[cellIndex] ?? "text";
|
|
232
337
|
})();
|
|
233
338
|
if (kind === "text") {
|
|
234
|
-
return /* @__PURE__ */ jsx2(NodeViewWrapper2, { as: "td", className: "tessera-td", children: /* @__PURE__ */ jsx2(NodeViewContent, {}) });
|
|
339
|
+
return /* @__PURE__ */ jsx2(NodeViewWrapper2, { as: "td", className: "tessera-td", "data-index": columnIndex(props), children: /* @__PURE__ */ jsx2(NodeViewContent, {}) });
|
|
235
340
|
}
|
|
236
341
|
const setValue = (next) => props.updateAttributes({ value: next });
|
|
237
|
-
return /* @__PURE__ */ jsxs2(NodeViewWrapper2, { as: "td", className: `tessera-td tessera-td--${kind}`, children: [
|
|
342
|
+
return /* @__PURE__ */ jsxs2(NodeViewWrapper2, { as: "td", className: `tessera-td tessera-td--${kind}`, "data-index": columnIndex(props), children: [
|
|
238
343
|
/* @__PURE__ */ jsx2(NodeViewContent, { className: "tessera-td-hidden" }),
|
|
239
344
|
kind === "checkbox" ? /* @__PURE__ */ jsx2(
|
|
240
345
|
"input",
|
|
@@ -289,7 +394,7 @@ function AiTableCellView(props) {
|
|
|
289
394
|
] });
|
|
290
395
|
}
|
|
291
396
|
function TagEditor({ multi, value, onChange }) {
|
|
292
|
-
const [input, setInput] =
|
|
397
|
+
const [input, setInput] = useState3("");
|
|
293
398
|
const commit = () => {
|
|
294
399
|
const tag = input.trim();
|
|
295
400
|
if (!tag) {
|
|
@@ -342,13 +447,13 @@ var AiTableHeaderViewExtension = AiTableHeader.extend({
|
|
|
342
447
|
});
|
|
343
448
|
|
|
344
449
|
// src/EmbedTocViews.tsx
|
|
345
|
-
import { useContext as useContext4, useEffect as
|
|
450
|
+
import { useContext as useContext4, useEffect as useEffect4, useState as useState4 } from "react";
|
|
346
451
|
import { NodeViewWrapper as NodeViewWrapper3, ReactNodeViewRenderer as ReactNodeViewRenderer3 } from "@tiptap/react";
|
|
347
452
|
import { EmbedBlock, TocBlock } from "@tessera-editor/core";
|
|
348
453
|
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
349
454
|
function EmbedView({ node, updateAttributes, selected }) {
|
|
350
455
|
const { t } = useContext4(TesseraContext);
|
|
351
|
-
const [url, setUrl] =
|
|
456
|
+
const [url, setUrl] = useState4(typeof node.attrs.src === "string" ? node.attrs.src : "");
|
|
352
457
|
const src = typeof node.attrs.src === "string" ? node.attrs.src : "";
|
|
353
458
|
const height = typeof node.attrs.height === "number" ? node.attrs.height : 360;
|
|
354
459
|
if (!src) {
|
|
@@ -396,8 +501,8 @@ function EmbedView({ node, updateAttributes, selected }) {
|
|
|
396
501
|
}
|
|
397
502
|
function TocView({ editor }) {
|
|
398
503
|
const { t } = useContext4(TesseraContext);
|
|
399
|
-
const [items, setItems] =
|
|
400
|
-
|
|
504
|
+
const [items, setItems] = useState4([]);
|
|
505
|
+
useEffect4(() => {
|
|
401
506
|
const scan = () => {
|
|
402
507
|
const found = [];
|
|
403
508
|
editor.state.doc.forEach((node, _offset, index) => {
|
|
@@ -443,21 +548,21 @@ var TocBlockView = TocBlock.extend({
|
|
|
443
548
|
});
|
|
444
549
|
|
|
445
550
|
// src/SlashMenu.tsx
|
|
446
|
-
import { useEffect as
|
|
551
|
+
import { useEffect as useEffect5, useImperativeHandle, useRef as useRef3, useState as useState5, forwardRef } from "react";
|
|
447
552
|
import { ReactRenderer } from "@tiptap/react";
|
|
448
553
|
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
449
554
|
var SlashMenuView = forwardRef(
|
|
450
555
|
function SlashMenuView2({ items, command, clientRect, t }, ref) {
|
|
451
|
-
const [selectedIndex, setSelectedIndex] =
|
|
452
|
-
const listRef =
|
|
453
|
-
const [rect, setRect] =
|
|
454
|
-
|
|
556
|
+
const [selectedIndex, setSelectedIndex] = useState5(0);
|
|
557
|
+
const listRef = useRef3(null);
|
|
558
|
+
const [rect, setRect] = useState5(null);
|
|
559
|
+
useEffect5(() => {
|
|
455
560
|
setRect(clientRect?.() ?? null);
|
|
456
561
|
}, [items, clientRect]);
|
|
457
|
-
|
|
562
|
+
useEffect5(() => {
|
|
458
563
|
setSelectedIndex(0);
|
|
459
564
|
}, [items]);
|
|
460
|
-
|
|
565
|
+
useEffect5(() => {
|
|
461
566
|
listRef.current?.querySelector('[data-selected="true"]')?.scrollIntoView({ block: "nearest" });
|
|
462
567
|
}, [selectedIndex]);
|
|
463
568
|
useImperativeHandle(ref, () => ({
|
|
@@ -498,7 +603,7 @@ var SlashMenuView = forwardRef(
|
|
|
498
603
|
width: 320
|
|
499
604
|
};
|
|
500
605
|
})() : { position: "fixed", left: -9999, top: -9999 };
|
|
501
|
-
return /* @__PURE__ */ jsx4("div", { className: "tessera-slash-menu", style, "data-testid": "slash-menu", children: Array.from(groups.entries()).map(([group, groupItems]) => /* @__PURE__ */ jsxs4("div", { className: "tessera-slash-group", children: [
|
|
606
|
+
return /* @__PURE__ */ jsx4("div", { ref: listRef, className: "tessera-slash-menu", style, "data-testid": "slash-menu", children: Array.from(groups.entries()).map(([group, groupItems]) => /* @__PURE__ */ jsxs4("div", { className: "tessera-slash-group", children: [
|
|
502
607
|
/* @__PURE__ */ jsx4("div", { className: "tessera-slash-group-title", children: group === "ai" ? t("groupAi") : group === "advanced" ? t("groupAdvanced") : t("groupBasic") }),
|
|
503
608
|
groupItems.map((item) => {
|
|
504
609
|
flatIndex += 1;
|
|
@@ -573,20 +678,20 @@ function createSlashRenderer(t) {
|
|
|
573
678
|
}
|
|
574
679
|
|
|
575
680
|
// src/EmojiMenu.tsx
|
|
576
|
-
import { useEffect as
|
|
681
|
+
import { useEffect as useEffect6, useImperativeHandle as useImperativeHandle2, useRef as useRef4, useState as useState6, forwardRef as forwardRef2 } from "react";
|
|
577
682
|
import { ReactRenderer as ReactRenderer2 } from "@tiptap/react";
|
|
578
683
|
import { jsx as jsx5 } from "react/jsx-runtime";
|
|
579
684
|
var EmojiMenuView = forwardRef2(function EmojiMenuView2({ items, command, clientRect }, ref) {
|
|
580
|
-
const [selectedIndex, setSelectedIndex] =
|
|
581
|
-
const [rect, setRect] =
|
|
582
|
-
const gridRef =
|
|
583
|
-
|
|
685
|
+
const [selectedIndex, setSelectedIndex] = useState6(0);
|
|
686
|
+
const [rect, setRect] = useState6(null);
|
|
687
|
+
const gridRef = useRef4(null);
|
|
688
|
+
useEffect6(() => {
|
|
584
689
|
setRect(clientRect?.() ?? null);
|
|
585
690
|
}, [items, clientRect]);
|
|
586
|
-
|
|
691
|
+
useEffect6(() => {
|
|
587
692
|
setSelectedIndex(0);
|
|
588
693
|
}, [items]);
|
|
589
|
-
|
|
694
|
+
useEffect6(() => {
|
|
590
695
|
gridRef.current?.querySelectorAll(".tessera-emoji-item")[selectedIndex]?.scrollIntoView({ block: "nearest" });
|
|
591
696
|
}, [selectedIndex]);
|
|
592
697
|
useImperativeHandle2(ref, () => ({
|
|
@@ -677,53 +782,25 @@ function createEmojiRenderer() {
|
|
|
677
782
|
}
|
|
678
783
|
|
|
679
784
|
// src/EmptyLineToolbar.tsx
|
|
680
|
-
import { useCallback, useContext as useContext5, useEffect as
|
|
681
|
-
import { createPortal as
|
|
682
|
-
import { defaultSlashItems } from "@tessera-editor/core";
|
|
785
|
+
import { useCallback, useContext as useContext5, useEffect as useEffect7, useLayoutEffect, useRef as useRef5, useState as useState7 } from "react";
|
|
786
|
+
import { createPortal as createPortal3 } from "react-dom";
|
|
787
|
+
import { defaultSlashItems, filterSlashItems } from "@tessera-editor/core";
|
|
683
788
|
import { aiSlashItems } from "@tessera-editor/ai";
|
|
684
|
-
|
|
685
|
-
// src/portal.ts
|
|
686
|
-
import { useEffect as useEffect5, useState as useState6 } from "react";
|
|
687
|
-
function useTesseraPortalRoot(editor) {
|
|
688
|
-
const [target, setTarget] = useState6(null);
|
|
689
|
-
useEffect5(() => {
|
|
690
|
-
const resolve = () => {
|
|
691
|
-
setTarget((prev) => {
|
|
692
|
-
if (prev && prev !== document.body) {
|
|
693
|
-
return prev;
|
|
694
|
-
}
|
|
695
|
-
return editor.view.dom.closest(".tessera-root") ?? document.body;
|
|
696
|
-
});
|
|
697
|
-
};
|
|
698
|
-
resolve();
|
|
699
|
-
editor.on("focus", resolve);
|
|
700
|
-
editor.on("selectionUpdate", resolve);
|
|
701
|
-
return () => {
|
|
702
|
-
editor.off("focus", resolve);
|
|
703
|
-
editor.off("selectionUpdate", resolve);
|
|
704
|
-
};
|
|
705
|
-
}, [editor]);
|
|
706
|
-
return target;
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
// src/EmptyLineToolbar.tsx
|
|
710
789
|
import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
711
|
-
function buildItems(editor, t
|
|
712
|
-
const
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
}
|
|
716
|
-
return base;
|
|
790
|
+
function buildItems(editor, t) {
|
|
791
|
+
const slash = editor.extensionManager.extensions.find((ext) => ext.name === "tesseraSlashMenu");
|
|
792
|
+
const excluded = slash?.options?.excludeItems;
|
|
793
|
+
return filterSlashItems(defaultSlashItems(t), excluded);
|
|
717
794
|
}
|
|
718
795
|
function EmptyLineToolbar() {
|
|
719
|
-
const { editor, t
|
|
796
|
+
const { editor, t } = useContext5(TesseraContext);
|
|
720
797
|
const [style, setStyle] = useState7(null);
|
|
721
798
|
const [expanded, setExpanded] = useState7(false);
|
|
722
799
|
const [panelPlacement, setPanelPlacement] = useState7("bottom");
|
|
723
800
|
const [panelMaxHeight, setPanelMaxHeight] = useState7(void 0);
|
|
724
|
-
const composingRef =
|
|
725
|
-
const rootRef =
|
|
726
|
-
const panelRef =
|
|
801
|
+
const composingRef = useRef5(false);
|
|
802
|
+
const rootRef = useRef5(null);
|
|
803
|
+
const panelRef = useRef5(null);
|
|
727
804
|
const portalRoot = useTesseraPortalRoot(editor);
|
|
728
805
|
const hideAll = useCallback(() => {
|
|
729
806
|
setStyle(null);
|
|
@@ -760,7 +837,7 @@ function EmptyLineToolbar() {
|
|
|
760
837
|
});
|
|
761
838
|
dom.setAttribute("data-toolbar-line", "true");
|
|
762
839
|
}, [editor, hideAll]);
|
|
763
|
-
|
|
840
|
+
useEffect7(() => {
|
|
764
841
|
const hide = () => {
|
|
765
842
|
setStyle(null);
|
|
766
843
|
setExpanded(false);
|
|
@@ -815,13 +892,13 @@ function EmptyLineToolbar() {
|
|
|
815
892
|
if (!style || !portalRoot) {
|
|
816
893
|
return null;
|
|
817
894
|
}
|
|
818
|
-
const items = buildItems(editor, t
|
|
895
|
+
const items = buildItems(editor, t);
|
|
819
896
|
const runItem = (item) => {
|
|
820
897
|
const { $from } = editor.state.selection;
|
|
821
898
|
item.command({ editor, range: { from: $from.pos, to: $from.pos } });
|
|
822
899
|
setExpanded(false);
|
|
823
900
|
};
|
|
824
|
-
return
|
|
901
|
+
return createPortal3(
|
|
825
902
|
/* @__PURE__ */ jsxs5(
|
|
826
903
|
"div",
|
|
827
904
|
{
|
|
@@ -869,24 +946,126 @@ function EmptyLineToolbar() {
|
|
|
869
946
|
);
|
|
870
947
|
}
|
|
871
948
|
|
|
872
|
-
// src/
|
|
873
|
-
import {
|
|
949
|
+
// src/LinkEditor.tsx
|
|
950
|
+
import { useContext as useContext6, useEffect as useEffect8, useRef as useRef6, useState as useState8 } from "react";
|
|
874
951
|
import { createPortal as createPortal4 } from "react-dom";
|
|
952
|
+
import { findLinkRange, removeLinkRange, saveLinkRange } from "@tessera-editor/core";
|
|
953
|
+
import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
954
|
+
function LinkEditor() {
|
|
955
|
+
const { editor, t } = useContext6(TesseraContext);
|
|
956
|
+
const portalRoot = useTesseraPortalRoot(editor);
|
|
957
|
+
const [range, setRange] = useState8(null);
|
|
958
|
+
const [text, setText] = useState8("");
|
|
959
|
+
const [href, setHref] = useState8("");
|
|
960
|
+
const [style, setStyle] = useState8({});
|
|
961
|
+
const panelRef = useRef6(null);
|
|
962
|
+
useEffect8(() => {
|
|
963
|
+
const onClick = (e) => {
|
|
964
|
+
const target = e.target;
|
|
965
|
+
const anchor = target.closest("a[href]");
|
|
966
|
+
if (!anchor || !editor.view.dom.contains(target)) {
|
|
967
|
+
setRange(null);
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
if (!editor.isEditable) {
|
|
971
|
+
const href2 = anchor.getAttribute("href");
|
|
972
|
+
if (href2) {
|
|
973
|
+
window.open(href2, "_blank", "noopener,noreferrer");
|
|
974
|
+
}
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
const found = findLinkRange(editor, editor.view.posAtDOM(anchor, 0));
|
|
978
|
+
if (found) openPanel(found);
|
|
979
|
+
};
|
|
980
|
+
const dom = editor.view.dom;
|
|
981
|
+
dom.addEventListener("click", onClick);
|
|
982
|
+
return () => dom.removeEventListener("click", onClick);
|
|
983
|
+
}, [editor]);
|
|
984
|
+
useEffect8(() => {
|
|
985
|
+
if (!range) return;
|
|
986
|
+
const onDown = (e) => {
|
|
987
|
+
if (!panelRef.current?.contains(e.target)) setRange(null);
|
|
988
|
+
};
|
|
989
|
+
window.addEventListener("mousedown", onDown);
|
|
990
|
+
return () => window.removeEventListener("mousedown", onDown);
|
|
991
|
+
}, [range]);
|
|
992
|
+
function openPanel(found) {
|
|
993
|
+
setRange(found);
|
|
994
|
+
setText(found.text);
|
|
995
|
+
setHref(found.href);
|
|
996
|
+
const a = editor.view.coordsAtPos(found.from);
|
|
997
|
+
const b = editor.view.coordsAtPos(found.to);
|
|
998
|
+
const left = Math.min(Math.max(8, (a.left + b.left) / 2 - 160), window.innerWidth - 328);
|
|
999
|
+
const bottom = Math.max(a.bottom, b.bottom);
|
|
1000
|
+
const panelMax = 170;
|
|
1001
|
+
const flip = bottom + 8 + panelMax > window.innerHeight && a.top - 8 - panelMax >= 0;
|
|
1002
|
+
setStyle(
|
|
1003
|
+
flip ? { left: `${left}px`, bottom: `${window.innerHeight - Math.min(a.top, b.top) + 8}px` } : { left: `${left}px`, top: `${bottom + 8}px` }
|
|
1004
|
+
);
|
|
1005
|
+
}
|
|
1006
|
+
function save() {
|
|
1007
|
+
if (!range || !text.trim()) return;
|
|
1008
|
+
saveLinkRange(editor, range, { text, href });
|
|
1009
|
+
setRange(null);
|
|
1010
|
+
}
|
|
1011
|
+
function remove() {
|
|
1012
|
+
if (!range) return;
|
|
1013
|
+
removeLinkRange(editor, range);
|
|
1014
|
+
setRange(null);
|
|
1015
|
+
}
|
|
1016
|
+
function onInputKeyDown(e) {
|
|
1017
|
+
if (e.key === "Enter") save();
|
|
1018
|
+
if (e.key === "Escape") setRange(null);
|
|
1019
|
+
}
|
|
1020
|
+
if (!range || !portalRoot) return null;
|
|
1021
|
+
return createPortal4(
|
|
1022
|
+
/* @__PURE__ */ jsxs6("div", { ref: panelRef, className: "tessera-link-editor", style, onMouseDown: (e) => e.stopPropagation(), children: [
|
|
1023
|
+
/* @__PURE__ */ jsx7(
|
|
1024
|
+
"input",
|
|
1025
|
+
{
|
|
1026
|
+
autoFocus: true,
|
|
1027
|
+
value: text,
|
|
1028
|
+
placeholder: t("linkTextPlaceholder"),
|
|
1029
|
+
onChange: (e) => setText(e.target.value),
|
|
1030
|
+
onKeyDown: onInputKeyDown
|
|
1031
|
+
}
|
|
1032
|
+
),
|
|
1033
|
+
/* @__PURE__ */ jsx7(
|
|
1034
|
+
"input",
|
|
1035
|
+
{
|
|
1036
|
+
value: href,
|
|
1037
|
+
placeholder: t("linkPlaceholder"),
|
|
1038
|
+
onChange: (e) => setHref(e.target.value),
|
|
1039
|
+
onKeyDown: onInputKeyDown
|
|
1040
|
+
}
|
|
1041
|
+
),
|
|
1042
|
+
/* @__PURE__ */ jsxs6("div", { className: "tessera-link-editor-row", children: [
|
|
1043
|
+
/* @__PURE__ */ jsx7("button", { type: "button", disabled: !text.trim(), onClick: save, children: t("linkSave") }),
|
|
1044
|
+
/* @__PURE__ */ jsx7("button", { type: "button", className: "tessera-danger", onClick: remove, children: t("linkRemove") })
|
|
1045
|
+
] })
|
|
1046
|
+
] }),
|
|
1047
|
+
portalRoot
|
|
1048
|
+
);
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
// src/SelectionToolbar.tsx
|
|
1052
|
+
import { useCallback as useCallback2, useContext as useContext8, useEffect as useEffect10, useLayoutEffect as useLayoutEffect2, useRef as useRef7, useState as useState10 } from "react";
|
|
1053
|
+
import { createPortal as createPortal6 } from "react-dom";
|
|
875
1054
|
import { docToMarkdown } from "@tessera-editor/core";
|
|
876
1055
|
import { IMPROVE_PRESETS, improveSelection } from "@tessera-editor/ai";
|
|
877
1056
|
|
|
878
1057
|
// src/CommentPanel.tsx
|
|
879
|
-
import { useContext as
|
|
880
|
-
import { createPortal as
|
|
1058
|
+
import { useContext as useContext7, useEffect as useEffect9, useState as useState9 } from "react";
|
|
1059
|
+
import { createPortal as createPortal5 } from "react-dom";
|
|
881
1060
|
import { getCommentStore, getIdentityService, listCommentRanges } from "@tessera-editor/core";
|
|
882
|
-
import { jsx as
|
|
1061
|
+
import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
883
1062
|
function uid(prefix) {
|
|
884
1063
|
return `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
885
1064
|
}
|
|
886
1065
|
function CommentPanel() {
|
|
887
|
-
const { editor, t } =
|
|
888
|
-
const [visible, setVisible] =
|
|
889
|
-
const [threads, setThreads] =
|
|
1066
|
+
const { editor, t } = useContext7(TesseraContext);
|
|
1067
|
+
const [visible, setVisible] = useState9(false);
|
|
1068
|
+
const [threads, setThreads] = useState9([]);
|
|
890
1069
|
const refresh = async () => {
|
|
891
1070
|
const store2 = getCommentStore(editor);
|
|
892
1071
|
if (!store2) {
|
|
@@ -904,7 +1083,7 @@ function CommentPanel() {
|
|
|
904
1083
|
views.sort((a, b) => a.from - b.from);
|
|
905
1084
|
setThreads(views);
|
|
906
1085
|
};
|
|
907
|
-
|
|
1086
|
+
useEffect9(() => {
|
|
908
1087
|
const open = () => {
|
|
909
1088
|
setVisible(true);
|
|
910
1089
|
void refresh();
|
|
@@ -940,48 +1119,48 @@ function CommentPanel() {
|
|
|
940
1119
|
editor.commands.removeCommentThread(thread.id);
|
|
941
1120
|
await refresh();
|
|
942
1121
|
};
|
|
943
|
-
return /* @__PURE__ */
|
|
944
|
-
/* @__PURE__ */
|
|
945
|
-
/* @__PURE__ */
|
|
1122
|
+
return /* @__PURE__ */ jsxs7("div", { className: "tessera-comment-panel", "data-testid": "comment-panel", children: [
|
|
1123
|
+
/* @__PURE__ */ jsxs7("div", { className: "tessera-ask-header", children: [
|
|
1124
|
+
/* @__PURE__ */ jsxs7("span", { children: [
|
|
946
1125
|
t("commentTitle"),
|
|
947
1126
|
" \xB7 ",
|
|
948
1127
|
threads.filter((x) => !x.resolved).length
|
|
949
1128
|
] }),
|
|
950
|
-
/* @__PURE__ */
|
|
951
|
-
/* @__PURE__ */
|
|
952
|
-
/* @__PURE__ */
|
|
953
|
-
/* @__PURE__ */
|
|
1129
|
+
/* @__PURE__ */ jsxs7("div", { className: "tessera-history-actions", children: [
|
|
1130
|
+
/* @__PURE__ */ jsx8("button", { type: "button", title: "\u2191", onClick: () => editor.commands.focusNextCommentThread(), children: "\u2191" }),
|
|
1131
|
+
/* @__PURE__ */ jsx8("button", { type: "button", title: "\u2193", onClick: () => editor.commands.focusNextCommentThread(), children: "\u2193" }),
|
|
1132
|
+
/* @__PURE__ */ jsx8("button", { type: "button", onClick: () => setVisible(false), children: "\xD7" })
|
|
954
1133
|
] })
|
|
955
1134
|
] }),
|
|
956
|
-
!store ? /* @__PURE__ */
|
|
957
|
-
/* @__PURE__ */
|
|
958
|
-
threads.length === 0 ? /* @__PURE__ */
|
|
959
|
-
threads.map((thread) => /* @__PURE__ */
|
|
960
|
-
/* @__PURE__ */
|
|
1135
|
+
!store ? /* @__PURE__ */ jsx8("div", { className: "tessera-ask-error", children: "CommentStore not injected" }) : null,
|
|
1136
|
+
/* @__PURE__ */ jsxs7("div", { className: "tessera-ask-body", children: [
|
|
1137
|
+
threads.length === 0 ? /* @__PURE__ */ jsx8("div", { className: "tessera-toc-empty", children: t("commentEmpty") }) : null,
|
|
1138
|
+
threads.map((thread) => /* @__PURE__ */ jsxs7("div", { className: `tessera-thread${thread.resolved ? " tessera-thread--resolved" : ""}`, children: [
|
|
1139
|
+
/* @__PURE__ */ jsxs7("div", { className: "tessera-thread-quote", children: [
|
|
961
1140
|
"\u201C",
|
|
962
1141
|
thread.quote,
|
|
963
1142
|
"\u201D"
|
|
964
1143
|
] }),
|
|
965
|
-
thread.entries.map((entry) => /* @__PURE__ */
|
|
966
|
-
/* @__PURE__ */
|
|
967
|
-
/* @__PURE__ */
|
|
1144
|
+
thread.entries.map((entry) => /* @__PURE__ */ jsxs7("div", { className: "tessera-thread-entry", children: [
|
|
1145
|
+
/* @__PURE__ */ jsx8("span", { className: "tessera-thread-author", children: entry.authorName }),
|
|
1146
|
+
/* @__PURE__ */ jsx8("span", { className: "tessera-thread-text", children: entry.text })
|
|
968
1147
|
] }, entry.id)),
|
|
969
|
-
/* @__PURE__ */
|
|
970
|
-
/* @__PURE__ */
|
|
971
|
-
/* @__PURE__ */
|
|
972
|
-
thread.resolved ? /* @__PURE__ */
|
|
1148
|
+
/* @__PURE__ */ jsxs7("div", { className: "tessera-thread-actions", children: [
|
|
1149
|
+
/* @__PURE__ */ jsx8("button", { type: "button", onClick: () => void toggleResolve(thread), children: thread.resolved ? t("commentReopen") : t("commentResolve") }),
|
|
1150
|
+
/* @__PURE__ */ jsx8("button", { type: "button", className: "tessera-danger", onClick: () => void removeThread(thread), children: t("commentDelete") }),
|
|
1151
|
+
thread.resolved ? /* @__PURE__ */ jsx8("span", { className: "tessera-thread-badge", children: t("commentResolvedBadge") }) : null
|
|
973
1152
|
] })
|
|
974
1153
|
] }, thread.id))
|
|
975
1154
|
] }),
|
|
976
|
-
/* @__PURE__ */
|
|
1155
|
+
/* @__PURE__ */ jsxs7("div", { className: "tessera-comment-hint", children: [
|
|
977
1156
|
"\u2318\u2325M / \u{1F4AC} \xB7 ",
|
|
978
1157
|
me.name
|
|
979
1158
|
] })
|
|
980
1159
|
] });
|
|
981
1160
|
}
|
|
982
1161
|
function CommentComposer({ onClose }) {
|
|
983
|
-
const { editor, t } =
|
|
984
|
-
const [text, setText] =
|
|
1162
|
+
const { editor, t } = useContext7(TesseraContext);
|
|
1163
|
+
const [text, setText] = useState9("");
|
|
985
1164
|
const portalRoot = useTesseraPortalRoot(editor);
|
|
986
1165
|
const quote = editor.state.doc.textBetween(editor.state.selection.from, editor.state.selection.to, " ");
|
|
987
1166
|
const submit = async () => {
|
|
@@ -1009,15 +1188,15 @@ function CommentComposer({ onClose }) {
|
|
|
1009
1188
|
if (!portalRoot) {
|
|
1010
1189
|
return null;
|
|
1011
1190
|
}
|
|
1012
|
-
return
|
|
1013
|
-
/* @__PURE__ */
|
|
1014
|
-
/* @__PURE__ */
|
|
1191
|
+
return createPortal5(
|
|
1192
|
+
/* @__PURE__ */ jsxs7("div", { className: "tessera-popover tessera-comment-composer", "data-testid": "comment-composer", children: [
|
|
1193
|
+
/* @__PURE__ */ jsxs7("div", { className: "tessera-thread-quote", children: [
|
|
1015
1194
|
"\u201C",
|
|
1016
1195
|
quote.slice(0, 60),
|
|
1017
1196
|
quote.length > 60 ? "\u2026" : "",
|
|
1018
1197
|
"\u201D"
|
|
1019
1198
|
] }),
|
|
1020
|
-
/* @__PURE__ */
|
|
1199
|
+
/* @__PURE__ */ jsx8(
|
|
1021
1200
|
"textarea",
|
|
1022
1201
|
{
|
|
1023
1202
|
autoFocus: true,
|
|
@@ -1032,14 +1211,14 @@ function CommentComposer({ onClose }) {
|
|
|
1032
1211
|
}
|
|
1033
1212
|
}
|
|
1034
1213
|
),
|
|
1035
|
-
/* @__PURE__ */
|
|
1214
|
+
/* @__PURE__ */ jsx8("div", { className: "tessera-comment-composer-actions", children: /* @__PURE__ */ jsx8("button", { type: "button", disabled: !text.trim(), onClick: () => void submit(), children: t("commentSend") }) })
|
|
1036
1215
|
] }),
|
|
1037
1216
|
portalRoot
|
|
1038
1217
|
);
|
|
1039
1218
|
}
|
|
1040
1219
|
|
|
1041
1220
|
// src/SelectionToolbar.tsx
|
|
1042
|
-
import { jsx as
|
|
1221
|
+
import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
1043
1222
|
var TEXT_COLORS = ["#262629", "#d9414f", "#e8912d", "#31a56f", "#2f9ea8", "#4f6df5", "#7a4fd8", "#98a0ab"];
|
|
1044
1223
|
var HIGHLIGHT_COLORS = ["#fff2a8", "#d6eaff", "#d3f5df", "#fdd9e7", "#e6dcff"];
|
|
1045
1224
|
function FlipPopover({
|
|
@@ -1048,9 +1227,9 @@ function FlipPopover({
|
|
|
1048
1227
|
testId,
|
|
1049
1228
|
children
|
|
1050
1229
|
}) {
|
|
1051
|
-
const ref =
|
|
1052
|
-
const [placement, setPlacement] =
|
|
1053
|
-
const [maxHeight, setMaxHeight] =
|
|
1230
|
+
const ref = useRef7(null);
|
|
1231
|
+
const [placement, setPlacement] = useState10("bottom");
|
|
1232
|
+
const [maxHeight, setMaxHeight] = useState10(void 0);
|
|
1054
1233
|
useLayoutEffect2(() => {
|
|
1055
1234
|
const pop = ref.current;
|
|
1056
1235
|
const toolbar = pop?.parentElement;
|
|
@@ -1066,7 +1245,7 @@ function FlipPopover({
|
|
|
1066
1245
|
const avail = (flip ? spaceAbove : spaceBelow) - 12;
|
|
1067
1246
|
setMaxHeight(avail > 100 && avail < natural ? avail : void 0);
|
|
1068
1247
|
}, [posKey]);
|
|
1069
|
-
return /* @__PURE__ */
|
|
1248
|
+
return /* @__PURE__ */ jsx9(
|
|
1070
1249
|
"div",
|
|
1071
1250
|
{
|
|
1072
1251
|
ref,
|
|
@@ -1079,12 +1258,15 @@ function FlipPopover({
|
|
|
1079
1258
|
);
|
|
1080
1259
|
}
|
|
1081
1260
|
function SelectionToolbar({ onSession }) {
|
|
1082
|
-
const { editor, t, ai } =
|
|
1083
|
-
const [style, setStyle] =
|
|
1084
|
-
const [popover, setPopover] =
|
|
1085
|
-
const [linkValue, setLinkValue] =
|
|
1086
|
-
const composingRef =
|
|
1261
|
+
const { editor, t, ai, extraSelectionItems } = useContext8(TesseraContext);
|
|
1262
|
+
const [style, setStyle] = useState10(null);
|
|
1263
|
+
const [popover, setPopover] = useState10(null);
|
|
1264
|
+
const [linkValue, setLinkValue] = useState10("");
|
|
1265
|
+
const composingRef = useRef7(false);
|
|
1087
1266
|
const portalRoot = useTesseraPortalRoot(editor);
|
|
1267
|
+
const services = editor.storage.tesseraServices;
|
|
1268
|
+
const hasComments = !!services?.comments;
|
|
1269
|
+
const hasCollapsible = editor.extensionManager.extensions.some((ext) => ext.name === "collapsible");
|
|
1088
1270
|
const reposition = useCallback2(() => {
|
|
1089
1271
|
if (composingRef.current || !editor.isFocused) {
|
|
1090
1272
|
setStyle(null);
|
|
@@ -1109,7 +1291,7 @@ function SelectionToolbar({ onSession }) {
|
|
|
1109
1291
|
left: `${Math.max(8, Math.min(left, window.innerWidth - 380))}px`
|
|
1110
1292
|
});
|
|
1111
1293
|
}, [editor]);
|
|
1112
|
-
|
|
1294
|
+
useEffect10(() => {
|
|
1113
1295
|
const hide = () => {
|
|
1114
1296
|
setStyle(null);
|
|
1115
1297
|
setPopover(null);
|
|
@@ -1153,8 +1335,8 @@ function SelectionToolbar({ onSession }) {
|
|
|
1153
1335
|
return null;
|
|
1154
1336
|
}
|
|
1155
1337
|
const currentColor = editor.getAttributes("textStyle").color ?? null;
|
|
1156
|
-
return
|
|
1157
|
-
/* @__PURE__ */
|
|
1338
|
+
return createPortal6(
|
|
1339
|
+
/* @__PURE__ */ jsxs8(
|
|
1158
1340
|
"div",
|
|
1159
1341
|
{
|
|
1160
1342
|
className: "tessera-selection-toolbar",
|
|
@@ -1162,16 +1344,16 @@ function SelectionToolbar({ onSession }) {
|
|
|
1162
1344
|
"data-testid": "selection-toolbar",
|
|
1163
1345
|
onMouseDown: (e) => e.preventDefault(),
|
|
1164
1346
|
children: [
|
|
1165
|
-
ai ? /* @__PURE__ */
|
|
1166
|
-
/* @__PURE__ */
|
|
1167
|
-
/* @__PURE__ */
|
|
1168
|
-
/* @__PURE__ */
|
|
1169
|
-
/* @__PURE__ */
|
|
1170
|
-
/* @__PURE__ */
|
|
1171
|
-
/* @__PURE__ */
|
|
1172
|
-
/* @__PURE__ */
|
|
1173
|
-
/* @__PURE__ */
|
|
1174
|
-
/* @__PURE__ */
|
|
1347
|
+
ai ? /* @__PURE__ */ jsx9(TbBtn, { title: t("tooltipImprove"), className: "tessera-tb-ai", onClick: () => setPopover((p) => p === "improve" ? null : "improve"), children: "\u2728" }) : null,
|
|
1348
|
+
/* @__PURE__ */ jsx9(TbBtn, { title: t("tooltipBold"), active: editor.isActive("bold"), onClick: () => chain().toggleBold().run(), children: /* @__PURE__ */ jsx9("b", { children: "B" }) }),
|
|
1349
|
+
/* @__PURE__ */ jsx9(TbBtn, { title: t("tooltipItalic"), active: editor.isActive("italic"), onClick: () => chain().toggleItalic().run(), children: /* @__PURE__ */ jsx9("i", { children: "I" }) }),
|
|
1350
|
+
/* @__PURE__ */ jsx9(TbBtn, { title: t("tooltipUnderline"), active: editor.isActive("underline"), onClick: () => chain().toggleUnderline().run(), children: /* @__PURE__ */ jsx9("u", { children: "U" }) }),
|
|
1351
|
+
/* @__PURE__ */ jsx9(TbBtn, { title: t("tooltipStrike"), active: editor.isActive("strike"), onClick: () => chain().toggleStrike().run(), children: /* @__PURE__ */ jsx9("s", { children: "S" }) }),
|
|
1352
|
+
/* @__PURE__ */ jsx9(TbBtn, { title: t("tooltipCode"), active: editor.isActive("code"), onClick: () => chain().toggleCode().run(), children: "</>" }),
|
|
1353
|
+
/* @__PURE__ */ jsx9(TbBtn, { title: t("tooltipColor"), active: !!currentColor, onClick: () => setPopover((p) => p === "color" ? null : "color"), children: /* @__PURE__ */ jsx9("span", { className: "tessera-tb-colorchip", style: { background: currentColor ?? "#262629" } }) }),
|
|
1354
|
+
/* @__PURE__ */ jsx9(TbBtn, { title: t("tooltipHighlight"), active: editor.isActive("highlight"), onClick: () => setPopover((p) => p === "highlight" ? null : "highlight"), children: /* @__PURE__ */ jsx9("span", { className: "tessera-tb-colorchip", style: { background: "#fff2a8" } }) }),
|
|
1355
|
+
/* @__PURE__ */ jsx9(TbBtn, { title: t("tooltipLink"), active: editor.isActive("link"), onClick: () => setPopover((p) => p === "link" ? null : "link"), children: "\u{1F517}" }),
|
|
1356
|
+
hasComments ? /* @__PURE__ */ jsx9(
|
|
1175
1357
|
TbBtn,
|
|
1176
1358
|
{
|
|
1177
1359
|
title: t("tooltipCommentV11"),
|
|
@@ -1179,19 +1361,20 @@ function SelectionToolbar({ onSession }) {
|
|
|
1179
1361
|
onClick: () => setPopover((p) => p === "comment" ? null : "comment"),
|
|
1180
1362
|
children: "\u{1F4AC}"
|
|
1181
1363
|
}
|
|
1182
|
-
),
|
|
1183
|
-
/* @__PURE__ */
|
|
1184
|
-
/* @__PURE__ */
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1364
|
+
) : null,
|
|
1365
|
+
hasCollapsible ? /* @__PURE__ */ jsx9(TbBtn, { title: t("tooltipTurnCollapsible"), onClick: () => chain().insertCollapsible().run(), children: "\u25B8" }) : null,
|
|
1366
|
+
/* @__PURE__ */ jsx9(TbBtn, { title: t("tooltipMore"), onClick: () => setPopover((p) => p === "more" ? null : "more"), children: "\u22EF" }),
|
|
1367
|
+
typeof extraSelectionItems === "function" ? extraSelectionItems({ editor, t }) : extraSelectionItems,
|
|
1368
|
+
popover === "color" ? /* @__PURE__ */ jsxs8(FlipPopover, { posKey: style, children: [
|
|
1369
|
+
/* @__PURE__ */ jsx9("button", { type: "button", className: "tessera-color-swatch tessera-color-none", title: t("colorDefault"), onClick: () => chain().unsetColor().run() }),
|
|
1370
|
+
TEXT_COLORS.map((color) => /* @__PURE__ */ jsx9("button", { type: "button", className: "tessera-color-swatch", style: { background: color }, onClick: () => chain().setColor(color).run() }, color))
|
|
1188
1371
|
] }) : null,
|
|
1189
|
-
popover === "highlight" ? /* @__PURE__ */
|
|
1190
|
-
/* @__PURE__ */
|
|
1191
|
-
HIGHLIGHT_COLORS.map((color) => /* @__PURE__ */
|
|
1372
|
+
popover === "highlight" ? /* @__PURE__ */ jsxs8(FlipPopover, { posKey: style, children: [
|
|
1373
|
+
/* @__PURE__ */ jsx9("button", { type: "button", className: "tessera-color-swatch tessera-color-none", title: t("highlightNone"), onClick: () => chain().unsetHighlight().run() }),
|
|
1374
|
+
HIGHLIGHT_COLORS.map((color) => /* @__PURE__ */ jsx9("button", { type: "button", className: "tessera-color-swatch", style: { background: color }, onClick: () => chain().toggleHighlight({ color }).run() }, color))
|
|
1192
1375
|
] }) : null,
|
|
1193
|
-
popover === "link" ? /* @__PURE__ */
|
|
1194
|
-
/* @__PURE__ */
|
|
1376
|
+
popover === "link" ? /* @__PURE__ */ jsxs8(FlipPopover, { posKey: style, className: "tessera-link-popover", children: [
|
|
1377
|
+
/* @__PURE__ */ jsx9(
|
|
1195
1378
|
"input",
|
|
1196
1379
|
{
|
|
1197
1380
|
autoFocus: true,
|
|
@@ -1206,7 +1389,7 @@ function SelectionToolbar({ onSession }) {
|
|
|
1206
1389
|
}
|
|
1207
1390
|
}
|
|
1208
1391
|
),
|
|
1209
|
-
/* @__PURE__ */
|
|
1392
|
+
/* @__PURE__ */ jsx9(
|
|
1210
1393
|
"button",
|
|
1211
1394
|
{
|
|
1212
1395
|
type: "button",
|
|
@@ -1219,7 +1402,7 @@ function SelectionToolbar({ onSession }) {
|
|
|
1219
1402
|
children: t("linkApply")
|
|
1220
1403
|
}
|
|
1221
1404
|
),
|
|
1222
|
-
editor.isActive("link") ? /* @__PURE__ */
|
|
1405
|
+
editor.isActive("link") ? /* @__PURE__ */ jsx9(
|
|
1223
1406
|
"button",
|
|
1224
1407
|
{
|
|
1225
1408
|
type: "button",
|
|
@@ -1232,7 +1415,7 @@ function SelectionToolbar({ onSession }) {
|
|
|
1232
1415
|
}
|
|
1233
1416
|
) : null
|
|
1234
1417
|
] }) : null,
|
|
1235
|
-
popover === "more" ? /* @__PURE__ */
|
|
1418
|
+
popover === "more" ? /* @__PURE__ */ jsx9(FlipPopover, { posKey: style, className: "tessera-popover-menu", children: /* @__PURE__ */ jsx9(
|
|
1236
1419
|
"button",
|
|
1237
1420
|
{
|
|
1238
1421
|
type: "button",
|
|
@@ -1246,8 +1429,8 @@ function SelectionToolbar({ onSession }) {
|
|
|
1246
1429
|
children: t("tooltipCopyMarkdown")
|
|
1247
1430
|
}
|
|
1248
1431
|
) }) : null,
|
|
1249
|
-
popover === "improve" ? /* @__PURE__ */
|
|
1250
|
-
popover === "comment" ? /* @__PURE__ */
|
|
1432
|
+
popover === "improve" ? /* @__PURE__ */ jsx9(ImprovePopover, { posKey: style, onSession, onClose: () => setPopover(null) }) : null,
|
|
1433
|
+
popover === "comment" ? /* @__PURE__ */ jsx9(CommentComposer, { onClose: () => setPopover(null) }) : null
|
|
1251
1434
|
]
|
|
1252
1435
|
}
|
|
1253
1436
|
),
|
|
@@ -1261,7 +1444,7 @@ function TbBtn({
|
|
|
1261
1444
|
onClick,
|
|
1262
1445
|
children
|
|
1263
1446
|
}) {
|
|
1264
|
-
return /* @__PURE__ */
|
|
1447
|
+
return /* @__PURE__ */ jsx9(
|
|
1265
1448
|
"button",
|
|
1266
1449
|
{
|
|
1267
1450
|
type: "button",
|
|
@@ -1279,10 +1462,10 @@ function ImprovePopover({
|
|
|
1279
1462
|
onSession,
|
|
1280
1463
|
onClose
|
|
1281
1464
|
}) {
|
|
1282
|
-
const { editor, t, locale, ai } =
|
|
1283
|
-
const [instruction, setInstruction] =
|
|
1284
|
-
const [busy, setBusy] =
|
|
1285
|
-
const [error, setError] =
|
|
1465
|
+
const { editor, t, locale, ai } = useContext8(TesseraContext);
|
|
1466
|
+
const [instruction, setInstruction] = useState10("");
|
|
1467
|
+
const [busy, setBusy] = useState10(false);
|
|
1468
|
+
const [error, setError] = useState10(null);
|
|
1286
1469
|
const run = async (presetInstruction) => {
|
|
1287
1470
|
if (!ai) {
|
|
1288
1471
|
setError(t("aiRuntimeMissing"));
|
|
@@ -1299,10 +1482,10 @@ function ImprovePopover({
|
|
|
1299
1482
|
setBusy(false);
|
|
1300
1483
|
}
|
|
1301
1484
|
};
|
|
1302
|
-
return /* @__PURE__ */
|
|
1303
|
-
IMPROVE_PRESETS.map((preset) => /* @__PURE__ */
|
|
1304
|
-
/* @__PURE__ */
|
|
1305
|
-
/* @__PURE__ */
|
|
1485
|
+
return /* @__PURE__ */ jsxs8(FlipPopover, { posKey, className: "tessera-improve-popover", testId: "improve-popover", children: [
|
|
1486
|
+
IMPROVE_PRESETS.map((preset) => /* @__PURE__ */ jsx9("button", { type: "button", disabled: busy, onClick: () => run(preset.instruction), children: locale === "zh-CN" ? preset.labelZh : preset.labelEn }, preset.id)),
|
|
1487
|
+
/* @__PURE__ */ jsxs8("div", { className: "tessera-improve-custom", children: [
|
|
1488
|
+
/* @__PURE__ */ jsx9(
|
|
1306
1489
|
"input",
|
|
1307
1490
|
{
|
|
1308
1491
|
value: instruction,
|
|
@@ -1315,26 +1498,26 @@ function ImprovePopover({
|
|
|
1315
1498
|
}
|
|
1316
1499
|
}
|
|
1317
1500
|
),
|
|
1318
|
-
/* @__PURE__ */
|
|
1501
|
+
/* @__PURE__ */ jsx9("button", { type: "button", disabled: busy || !instruction.trim(), onClick: () => run(instruction.trim()), children: t("aiImproveRun") })
|
|
1319
1502
|
] }),
|
|
1320
|
-
error ? /* @__PURE__ */
|
|
1503
|
+
error ? /* @__PURE__ */ jsx9("div", { className: "tessera-popover-error", children: error }) : null
|
|
1321
1504
|
] });
|
|
1322
1505
|
}
|
|
1323
1506
|
|
|
1324
1507
|
// src/Panels.tsx
|
|
1325
|
-
import { useContext as
|
|
1508
|
+
import { useContext as useContext9, useEffect as useEffect11, useRef as useRef8, useState as useState11 } from "react";
|
|
1326
1509
|
import { findReplaceKey } from "@tessera-editor/core";
|
|
1327
|
-
import { jsx as
|
|
1510
|
+
import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
1328
1511
|
function FindReplacePanel() {
|
|
1329
|
-
const { editor, t } =
|
|
1330
|
-
const [visible, setVisible] =
|
|
1331
|
-
const [query, setQuery] =
|
|
1332
|
-
const [replacement, setReplacement] =
|
|
1333
|
-
const [count, setCount] =
|
|
1334
|
-
const [active, setActive] =
|
|
1335
|
-
const queryRef =
|
|
1512
|
+
const { editor, t } = useContext9(TesseraContext);
|
|
1513
|
+
const [visible, setVisible] = useState11(false);
|
|
1514
|
+
const [query, setQuery] = useState11("");
|
|
1515
|
+
const [replacement, setReplacement] = useState11("");
|
|
1516
|
+
const [count, setCount] = useState11(0);
|
|
1517
|
+
const [active, setActive] = useState11(0);
|
|
1518
|
+
const queryRef = useRef8(query);
|
|
1336
1519
|
queryRef.current = query;
|
|
1337
|
-
|
|
1520
|
+
useEffect11(() => {
|
|
1338
1521
|
const open = () => {
|
|
1339
1522
|
setVisible(true);
|
|
1340
1523
|
requestAnimationFrame(() => {
|
|
@@ -1368,8 +1551,8 @@ function FindReplacePanel() {
|
|
|
1368
1551
|
setQuery(value);
|
|
1369
1552
|
editor.commands.setFindQuery(value);
|
|
1370
1553
|
};
|
|
1371
|
-
return /* @__PURE__ */
|
|
1372
|
-
/* @__PURE__ */
|
|
1554
|
+
return /* @__PURE__ */ jsxs9("div", { className: "tessera-find-panel", "data-testid": "find-panel", children: [
|
|
1555
|
+
/* @__PURE__ */ jsx10(
|
|
1373
1556
|
"input",
|
|
1374
1557
|
{
|
|
1375
1558
|
className: "tessera-find-input",
|
|
@@ -1387,10 +1570,10 @@ function FindReplacePanel() {
|
|
|
1387
1570
|
}
|
|
1388
1571
|
}
|
|
1389
1572
|
),
|
|
1390
|
-
/* @__PURE__ */
|
|
1391
|
-
/* @__PURE__ */
|
|
1392
|
-
/* @__PURE__ */
|
|
1393
|
-
/* @__PURE__ */
|
|
1573
|
+
/* @__PURE__ */ jsx10("span", { className: "tessera-find-count", children: count > 0 ? `${active + 1}/${count}` : "0" }),
|
|
1574
|
+
/* @__PURE__ */ jsx10("button", { type: "button", title: t("findPrev"), onClick: () => editor.commands.findPrev(), children: "\u2191" }),
|
|
1575
|
+
/* @__PURE__ */ jsx10("button", { type: "button", title: t("findNext"), onClick: () => editor.commands.findNext(), children: "\u2193" }),
|
|
1576
|
+
/* @__PURE__ */ jsx10(
|
|
1394
1577
|
"input",
|
|
1395
1578
|
{
|
|
1396
1579
|
value: replacement,
|
|
@@ -1398,19 +1581,19 @@ function FindReplacePanel() {
|
|
|
1398
1581
|
onChange: (e) => setReplacement(e.target.value)
|
|
1399
1582
|
}
|
|
1400
1583
|
),
|
|
1401
|
-
/* @__PURE__ */
|
|
1402
|
-
/* @__PURE__ */
|
|
1403
|
-
/* @__PURE__ */
|
|
1584
|
+
/* @__PURE__ */ jsx10("button", { type: "button", onClick: () => editor.commands.replaceCurrent(replacement), children: t("replaceOne") }),
|
|
1585
|
+
/* @__PURE__ */ jsx10("button", { type: "button", onClick: () => editor.commands.replaceAll(replacement), children: t("replaceAll") }),
|
|
1586
|
+
/* @__PURE__ */ jsx10("button", { type: "button", className: "tessera-find-close", title: t("findClose"), onClick: () => editor.commands.closeFindPanel(), children: "\xD7" })
|
|
1404
1587
|
] });
|
|
1405
1588
|
}
|
|
1406
1589
|
function AskPanel() {
|
|
1407
|
-
const { editor, t, ai } =
|
|
1408
|
-
const [visible, setVisible] =
|
|
1409
|
-
const [question, setQuestion] =
|
|
1410
|
-
const [answer, setAnswer] =
|
|
1411
|
-
const [thinking, setThinking] =
|
|
1412
|
-
const [error, setError] =
|
|
1413
|
-
|
|
1590
|
+
const { editor, t, ai } = useContext9(TesseraContext);
|
|
1591
|
+
const [visible, setVisible] = useState11(false);
|
|
1592
|
+
const [question, setQuestion] = useState11("");
|
|
1593
|
+
const [answer, setAnswer] = useState11("");
|
|
1594
|
+
const [thinking, setThinking] = useState11(false);
|
|
1595
|
+
const [error, setError] = useState11(null);
|
|
1596
|
+
useEffect11(() => {
|
|
1414
1597
|
const open = () => setVisible(true);
|
|
1415
1598
|
editor.on("tessera:askPanel", open);
|
|
1416
1599
|
return () => {
|
|
@@ -1435,19 +1618,19 @@ function AskPanel() {
|
|
|
1435
1618
|
setThinking(false);
|
|
1436
1619
|
}
|
|
1437
1620
|
};
|
|
1438
|
-
return /* @__PURE__ */
|
|
1439
|
-
/* @__PURE__ */
|
|
1440
|
-
/* @__PURE__ */
|
|
1441
|
-
/* @__PURE__ */
|
|
1621
|
+
return /* @__PURE__ */ jsxs9("div", { className: "tessera-ask-panel", "data-testid": "ask-panel", children: [
|
|
1622
|
+
/* @__PURE__ */ jsxs9("div", { className: "tessera-ask-header", children: [
|
|
1623
|
+
/* @__PURE__ */ jsx10("span", { children: t("aiTitle") }),
|
|
1624
|
+
/* @__PURE__ */ jsx10("button", { type: "button", onClick: () => setVisible(false), children: "\xD7" })
|
|
1442
1625
|
] }),
|
|
1443
|
-
ai ? null : /* @__PURE__ */
|
|
1444
|
-
/* @__PURE__ */
|
|
1445
|
-
answer ? /* @__PURE__ */
|
|
1446
|
-
thinking ? /* @__PURE__ */
|
|
1447
|
-
error ? /* @__PURE__ */
|
|
1626
|
+
ai ? null : /* @__PURE__ */ jsx10("div", { className: "tessera-ask-error", children: t("aiRuntimeMissing") }),
|
|
1627
|
+
/* @__PURE__ */ jsxs9("div", { className: "tessera-ask-body", children: [
|
|
1628
|
+
answer ? /* @__PURE__ */ jsx10("div", { className: "tessera-ask-answer", children: answer }) : null,
|
|
1629
|
+
thinking ? /* @__PURE__ */ jsx10("div", { className: "tessera-ask-thinking", children: t("aiThinking") }) : null,
|
|
1630
|
+
error ? /* @__PURE__ */ jsx10("div", { className: "tessera-ask-error", children: error }) : null
|
|
1448
1631
|
] }),
|
|
1449
|
-
/* @__PURE__ */
|
|
1450
|
-
/* @__PURE__ */
|
|
1632
|
+
/* @__PURE__ */ jsxs9("div", { className: "tessera-ask-input", children: [
|
|
1633
|
+
/* @__PURE__ */ jsx10(
|
|
1451
1634
|
"input",
|
|
1452
1635
|
{
|
|
1453
1636
|
value: question,
|
|
@@ -1460,7 +1643,7 @@ function AskPanel() {
|
|
|
1460
1643
|
}
|
|
1461
1644
|
}
|
|
1462
1645
|
),
|
|
1463
|
-
/* @__PURE__ */
|
|
1646
|
+
/* @__PURE__ */ jsx10("button", { type: "button", disabled: !ai || thinking || !question.trim(), onClick: ask, children: t("aiSend") })
|
|
1464
1647
|
] })
|
|
1465
1648
|
] });
|
|
1466
1649
|
}
|
|
@@ -1468,16 +1651,16 @@ function SuggestionBar({
|
|
|
1468
1651
|
session,
|
|
1469
1652
|
onClear
|
|
1470
1653
|
}) {
|
|
1471
|
-
const { t } =
|
|
1654
|
+
const { t } = useContext9(TesseraContext);
|
|
1472
1655
|
if (!session) {
|
|
1473
1656
|
return null;
|
|
1474
1657
|
}
|
|
1475
|
-
return /* @__PURE__ */
|
|
1476
|
-
/* @__PURE__ */
|
|
1658
|
+
return /* @__PURE__ */ jsxs9("div", { className: "tessera-suggestion-bar", "data-testid": "suggestion-bar", children: [
|
|
1659
|
+
/* @__PURE__ */ jsxs9("span", { className: "tessera-suggestion-label", children: [
|
|
1477
1660
|
"\u2728 ",
|
|
1478
1661
|
t("aiStreaming")
|
|
1479
1662
|
] }),
|
|
1480
|
-
/* @__PURE__ */
|
|
1663
|
+
/* @__PURE__ */ jsx10(
|
|
1481
1664
|
"button",
|
|
1482
1665
|
{
|
|
1483
1666
|
type: "button",
|
|
@@ -1489,7 +1672,7 @@ function SuggestionBar({
|
|
|
1489
1672
|
children: t("aiAccept")
|
|
1490
1673
|
}
|
|
1491
1674
|
),
|
|
1492
|
-
/* @__PURE__ */
|
|
1675
|
+
/* @__PURE__ */ jsx10(
|
|
1493
1676
|
"button",
|
|
1494
1677
|
{
|
|
1495
1678
|
type: "button",
|
|
@@ -1504,152 +1687,16 @@ function SuggestionBar({
|
|
|
1504
1687
|
] });
|
|
1505
1688
|
}
|
|
1506
1689
|
|
|
1507
|
-
// src/HistoryPanel.tsx
|
|
1508
|
-
import { useContext as useContext9, useEffect as useEffect10, useState as useState11 } from "react";
|
|
1509
|
-
import {
|
|
1510
|
-
diffDocs,
|
|
1511
|
-
diffSummary,
|
|
1512
|
-
getStorageService
|
|
1513
|
-
} from "@tessera-editor/core";
|
|
1514
|
-
import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
1515
|
-
function HistoryPanel() {
|
|
1516
|
-
const { editor, t } = useContext9(TesseraContext);
|
|
1517
|
-
const [visible, setVisible] = useState11(false);
|
|
1518
|
-
const [snapshots, setSnapshots] = useState11([]);
|
|
1519
|
-
const [selected, setSelected] = useState11(null);
|
|
1520
|
-
const [entries, setEntries] = useState11(null);
|
|
1521
|
-
const [summary, setSummary] = useState11({ added: 0, removed: 0, changed: 0 });
|
|
1522
|
-
const refresh = async () => {
|
|
1523
|
-
const storage = getStorageService(editor);
|
|
1524
|
-
if (!storage) {
|
|
1525
|
-
return;
|
|
1526
|
-
}
|
|
1527
|
-
const list = await storage.listSnapshots();
|
|
1528
|
-
setSnapshots([...list].sort((a, b) => b.ts - a.ts));
|
|
1529
|
-
};
|
|
1530
|
-
useEffect10(() => {
|
|
1531
|
-
const open = () => {
|
|
1532
|
-
setVisible(true);
|
|
1533
|
-
void refresh();
|
|
1534
|
-
};
|
|
1535
|
-
editor.on("tessera:historyPanel", open);
|
|
1536
|
-
const saved = () => void refresh();
|
|
1537
|
-
editor.on("tessera:snapshotSaved", saved);
|
|
1538
|
-
return () => {
|
|
1539
|
-
editor.off("tessera:historyPanel", open);
|
|
1540
|
-
editor.off("tessera:snapshotSaved", saved);
|
|
1541
|
-
};
|
|
1542
|
-
}, [editor]);
|
|
1543
|
-
if (!visible) {
|
|
1544
|
-
return null;
|
|
1545
|
-
}
|
|
1546
|
-
const hasStorage = !!getStorageService(editor);
|
|
1547
|
-
const select = (snap) => {
|
|
1548
|
-
setSelected(snap);
|
|
1549
|
-
const result = diffDocs(snap.doc, editor.getJSON());
|
|
1550
|
-
setEntries(result);
|
|
1551
|
-
setSummary(diffSummary(result.filter((e) => e.kind !== "unchanged")));
|
|
1552
|
-
};
|
|
1553
|
-
const restore = (snap) => {
|
|
1554
|
-
if (!window.confirm(t("historyConfirmRestore"))) {
|
|
1555
|
-
return;
|
|
1556
|
-
}
|
|
1557
|
-
editor.commands.setContent(snap.doc);
|
|
1558
|
-
setVisible(false);
|
|
1559
|
-
};
|
|
1560
|
-
return /* @__PURE__ */ jsxs9("div", { className: "tessera-history-panel", "data-testid": "history-panel", children: [
|
|
1561
|
-
/* @__PURE__ */ jsxs9("div", { className: "tessera-ask-header", children: [
|
|
1562
|
-
/* @__PURE__ */ jsx10("span", { children: t("historyTitle") }),
|
|
1563
|
-
/* @__PURE__ */ jsxs9("div", { className: "tessera-history-actions", children: [
|
|
1564
|
-
hasStorage ? /* @__PURE__ */ jsx10("button", { type: "button", onClick: () => editor.commands.captureSnapshot(), children: t("historyCapture") }) : null,
|
|
1565
|
-
/* @__PURE__ */ jsx10("button", { type: "button", onClick: () => setVisible(false), children: "\xD7" })
|
|
1566
|
-
] })
|
|
1567
|
-
] }),
|
|
1568
|
-
!hasStorage ? /* @__PURE__ */ jsx10("div", { className: "tessera-ask-error", children: t("aiRuntimeMissing").replace("AI Runtime", "StorageService") }) : null,
|
|
1569
|
-
/* @__PURE__ */ jsxs9("div", { className: "tessera-history-body", children: [
|
|
1570
|
-
/* @__PURE__ */ jsxs9("div", { className: "tessera-history-list", children: [
|
|
1571
|
-
snapshots.length === 0 ? /* @__PURE__ */ jsx10("div", { className: "tessera-toc-empty", children: t("historyEmpty") }) : null,
|
|
1572
|
-
snapshots.map((snap) => /* @__PURE__ */ jsxs9(
|
|
1573
|
-
"button",
|
|
1574
|
-
{
|
|
1575
|
-
type: "button",
|
|
1576
|
-
className: "tessera-history-item",
|
|
1577
|
-
"data-selected": selected?.id === snap.id,
|
|
1578
|
-
onClick: () => select(snap),
|
|
1579
|
-
children: [
|
|
1580
|
-
/* @__PURE__ */ jsx10("span", { className: "tessera-history-time", children: formatTime(snap.ts) }),
|
|
1581
|
-
snap.label ? /* @__PURE__ */ jsx10("span", { className: "tessera-history-label", children: snap.label }) : null,
|
|
1582
|
-
/* @__PURE__ */ jsx10(
|
|
1583
|
-
"span",
|
|
1584
|
-
{
|
|
1585
|
-
className: "tessera-history-action",
|
|
1586
|
-
role: "button",
|
|
1587
|
-
tabIndex: 0,
|
|
1588
|
-
onClick: (e) => {
|
|
1589
|
-
e.stopPropagation();
|
|
1590
|
-
restore(snap);
|
|
1591
|
-
},
|
|
1592
|
-
onKeyDown: (e) => {
|
|
1593
|
-
if (e.key === "Enter") restore(snap);
|
|
1594
|
-
},
|
|
1595
|
-
children: t("historyRestore")
|
|
1596
|
-
}
|
|
1597
|
-
)
|
|
1598
|
-
]
|
|
1599
|
-
},
|
|
1600
|
-
snap.id
|
|
1601
|
-
))
|
|
1602
|
-
] }),
|
|
1603
|
-
selected ? /* @__PURE__ */ jsxs9("div", { className: "tessera-history-diff", children: [
|
|
1604
|
-
/* @__PURE__ */ jsxs9("div", { className: "tessera-diff-summary", children: [
|
|
1605
|
-
"+",
|
|
1606
|
-
summary.added,
|
|
1607
|
-
" ",
|
|
1608
|
-
t("historyDiffAdded"),
|
|
1609
|
-
" \xB7 \u2212",
|
|
1610
|
-
summary.removed,
|
|
1611
|
-
" ",
|
|
1612
|
-
t("historyDiffRemoved"),
|
|
1613
|
-
" \xB7 ~",
|
|
1614
|
-
summary.changed,
|
|
1615
|
-
" ",
|
|
1616
|
-
t("historyDiffChanged")
|
|
1617
|
-
] }),
|
|
1618
|
-
(entries ?? []).filter((e) => e.kind !== "unchanged").slice(0, 80).map((entry, i) => /* @__PURE__ */ jsxs9("div", { className: `tessera-diff-block tessera-diff-block--${entry.kind}`, children: [
|
|
1619
|
-
/* @__PURE__ */ jsx10("span", { className: "tessera-diff-badge", children: entry.kind === "added" ? t("historyDiffAdded") : entry.kind === "removed" ? t("historyDiffRemoved") : t("historyDiffChanged") }),
|
|
1620
|
-
entry.kind === "changed" && entry.wordDiff ? /* @__PURE__ */ jsx10("span", { className: "tessera-diff-text", children: entry.wordDiff.map((part, j) => /* @__PURE__ */ jsx10("span", { className: `tessera-diff-part tessera-diff-part--${part.type}`, children: part.text }, j)) }) : /* @__PURE__ */ jsx10("span", { className: "tessera-diff-text", children: blockText(entry) })
|
|
1621
|
-
] }, entry.id ?? i))
|
|
1622
|
-
] }) : null
|
|
1623
|
-
] })
|
|
1624
|
-
] });
|
|
1625
|
-
}
|
|
1626
|
-
function blockText(entry) {
|
|
1627
|
-
const json = entry.after ?? entry.before;
|
|
1628
|
-
const walk = (node) => {
|
|
1629
|
-
if (!node || typeof node !== "object") {
|
|
1630
|
-
return "";
|
|
1631
|
-
}
|
|
1632
|
-
const n = node;
|
|
1633
|
-
return (n.text ?? "") + (n.content ?? []).map(walk).join("");
|
|
1634
|
-
};
|
|
1635
|
-
return walk(json).trim().slice(0, 120);
|
|
1636
|
-
}
|
|
1637
|
-
function formatTime(ts) {
|
|
1638
|
-
const d = new Date(ts);
|
|
1639
|
-
const pad = (n) => String(n).padStart(2, "0");
|
|
1640
|
-
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
1641
|
-
}
|
|
1642
|
-
|
|
1643
1690
|
// src/BlockMenu.tsx
|
|
1644
|
-
import { useContext as useContext10, useEffect as
|
|
1645
|
-
import { createPortal as
|
|
1691
|
+
import { useContext as useContext10, useEffect as useEffect12, useState as useState12 } from "react";
|
|
1692
|
+
import { createPortal as createPortal7 } from "react-dom";
|
|
1646
1693
|
import { tableToCsvAt as tableToCsvAt2 } from "@tessera-editor/core";
|
|
1647
1694
|
import { Fragment, jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
1648
1695
|
function BlockContextMenuUI() {
|
|
1649
1696
|
const { editor, t } = useContext10(TesseraContext);
|
|
1650
1697
|
const [menu, setMenu] = useState12(null);
|
|
1651
1698
|
const portalRoot = useTesseraPortalRoot(editor);
|
|
1652
|
-
|
|
1699
|
+
useEffect12(() => {
|
|
1653
1700
|
const onMenu = (payload) => {
|
|
1654
1701
|
const found = findBlockPosById(editor, payload.blockId);
|
|
1655
1702
|
if (found !== null) {
|
|
@@ -1680,7 +1727,7 @@ function BlockContextMenuUI() {
|
|
|
1680
1727
|
void navigator.clipboard.writeText(text);
|
|
1681
1728
|
setMenu(null);
|
|
1682
1729
|
};
|
|
1683
|
-
return
|
|
1730
|
+
return createPortal7(
|
|
1684
1731
|
/* @__PURE__ */ jsxs10("div", { className: "tessera-block-menu", style, onMouseDown: (e) => e.stopPropagation(), children: [
|
|
1685
1732
|
inTable ? /* @__PURE__ */ jsxs10(Fragment, { children: [
|
|
1686
1733
|
/* @__PURE__ */ jsx11(MenuLabel, { children: t("rowMenuTitle") }),
|
|
@@ -1764,33 +1811,47 @@ var DRAG_HANDLE_POSITION_CONFIG = { placement: "left", strategy: "absolute" };
|
|
|
1764
1811
|
function Tessera({
|
|
1765
1812
|
content,
|
|
1766
1813
|
locale = "zh-CN",
|
|
1814
|
+
placeholder,
|
|
1815
|
+
messages,
|
|
1816
|
+
editable = true,
|
|
1817
|
+
excludeBlocks,
|
|
1767
1818
|
upload,
|
|
1768
|
-
storage,
|
|
1769
1819
|
comments,
|
|
1770
1820
|
identity,
|
|
1771
1821
|
ai: runtime,
|
|
1772
|
-
historyIdleMs,
|
|
1773
1822
|
docWidth,
|
|
1823
|
+
extraSelectionItems,
|
|
1774
1824
|
onUpdate,
|
|
1775
1825
|
onCreate
|
|
1776
1826
|
}) {
|
|
1777
|
-
const t = useMemo(() => createTesseraT(locale), [locale]);
|
|
1778
1827
|
const [session, setSession] = useState13(null);
|
|
1779
|
-
const fileInputRef =
|
|
1780
|
-
const dragNodeRef =
|
|
1828
|
+
const fileInputRef = useRef9(null);
|
|
1829
|
+
const dragNodeRef = useRef9(null);
|
|
1781
1830
|
const handleDragNodeChange = useCallback3((data) => {
|
|
1782
1831
|
dragNodeRef.current = data.node ? data : null;
|
|
1783
1832
|
}, []);
|
|
1784
|
-
const onUpdateRef =
|
|
1833
|
+
const onUpdateRef = useRef9(onUpdate);
|
|
1785
1834
|
onUpdateRef.current = onUpdate;
|
|
1786
|
-
const onCreateRef =
|
|
1835
|
+
const onCreateRef = useRef9(onCreate);
|
|
1787
1836
|
onCreateRef.current = onCreate;
|
|
1788
|
-
const runtimeRef =
|
|
1837
|
+
const runtimeRef = useRef9(runtime);
|
|
1789
1838
|
runtimeRef.current = runtime;
|
|
1790
|
-
const
|
|
1791
|
-
|
|
1839
|
+
const placeholderRef = useRef9(placeholder);
|
|
1840
|
+
placeholderRef.current = placeholder;
|
|
1841
|
+
const messagesRef = useRef9(messages);
|
|
1842
|
+
messagesRef.current = messages;
|
|
1843
|
+
const excludeBlocksRef = useRef9(excludeBlocks);
|
|
1844
|
+
excludeBlocksRef.current = excludeBlocks;
|
|
1845
|
+
const initialContentRef = useRef9(content);
|
|
1846
|
+
const editorRef = useRef9(null);
|
|
1847
|
+
const t = useMemo(() => createTesseraT(locale, messagesRef.current), [locale]);
|
|
1792
1848
|
const extensions = useMemo(
|
|
1793
|
-
() => createTesseraExtensions({
|
|
1849
|
+
() => createTesseraExtensions({
|
|
1850
|
+
locale,
|
|
1851
|
+
placeholder: placeholderRef.current,
|
|
1852
|
+
messages: messagesRef.current,
|
|
1853
|
+
excludeBlocks: excludeBlocksRef.current
|
|
1854
|
+
}).map((ext) => {
|
|
1794
1855
|
if (ext.name === "tesseraSlashMenu") {
|
|
1795
1856
|
return ext.configure({
|
|
1796
1857
|
render: createSlashRenderer(t),
|
|
@@ -1823,15 +1884,15 @@ function Tessera({
|
|
|
1823
1884
|
// runtime is read through runtimeRef on purpose: rebuilding the extension
|
|
1824
1885
|
// list after mount cannot apply anyway (extensions are only read when the
|
|
1825
1886
|
// editor is created) and would only trigger the setOptions churn above
|
|
1826
|
-
[locale, t
|
|
1887
|
+
[locale, t]
|
|
1827
1888
|
);
|
|
1828
1889
|
async function uploadAndInsert(file) {
|
|
1829
1890
|
const ed = editorRef.current;
|
|
1830
1891
|
if (!ed) {
|
|
1831
1892
|
return;
|
|
1832
1893
|
}
|
|
1833
|
-
const
|
|
1834
|
-
const svc =
|
|
1894
|
+
const storage = ed.storage;
|
|
1895
|
+
const svc = storage.tesseraServices?.upload;
|
|
1835
1896
|
if (!svc) {
|
|
1836
1897
|
return;
|
|
1837
1898
|
}
|
|
@@ -1842,7 +1903,7 @@ function Tessera({
|
|
|
1842
1903
|
console.error("[Tessera] image upload failed:", err);
|
|
1843
1904
|
}
|
|
1844
1905
|
}
|
|
1845
|
-
const uploadAndInsertRef =
|
|
1906
|
+
const uploadAndInsertRef = useRef9(uploadAndInsert);
|
|
1846
1907
|
uploadAndInsertRef.current = uploadAndInsert;
|
|
1847
1908
|
const editorProps = useMemo(
|
|
1848
1909
|
() => ({
|
|
@@ -1874,28 +1935,36 @@ function Tessera({
|
|
|
1874
1935
|
[]
|
|
1875
1936
|
);
|
|
1876
1937
|
const handleUpdate = useCallback3(({ editor: e }) => onUpdateRef.current?.(e), []);
|
|
1877
|
-
const handleCreate = useCallback3(({ editor: e }) =>
|
|
1938
|
+
const handleCreate = useCallback3(({ editor: e }) => {
|
|
1939
|
+
e.commands.normalizeTypedCells();
|
|
1940
|
+
onCreateRef.current?.(e);
|
|
1941
|
+
}, []);
|
|
1878
1942
|
const editor = useEditor({
|
|
1879
1943
|
extensions,
|
|
1880
1944
|
content: initialContentRef.current,
|
|
1945
|
+
editable,
|
|
1881
1946
|
onUpdate: handleUpdate,
|
|
1882
1947
|
onCreate: handleCreate,
|
|
1883
1948
|
editorProps
|
|
1884
1949
|
});
|
|
1885
1950
|
editorRef.current = editor ?? null;
|
|
1886
|
-
|
|
1951
|
+
useEffect13(() => {
|
|
1887
1952
|
if (editor) {
|
|
1888
1953
|
const bag = editor.storage.tesseraServices ?? {};
|
|
1889
1954
|
editor.storage.tesseraServices = {
|
|
1890
1955
|
...bag,
|
|
1891
1956
|
upload,
|
|
1892
|
-
storage,
|
|
1893
1957
|
comments,
|
|
1894
1958
|
identity
|
|
1895
1959
|
};
|
|
1896
1960
|
}
|
|
1897
|
-
}, [editor, upload,
|
|
1898
|
-
|
|
1961
|
+
}, [editor, upload, comments, identity]);
|
|
1962
|
+
useEffect13(() => {
|
|
1963
|
+
if (editor) {
|
|
1964
|
+
editor.setEditable(editable);
|
|
1965
|
+
}
|
|
1966
|
+
}, [editor, editable]);
|
|
1967
|
+
useEffect13(() => {
|
|
1899
1968
|
if (!editor) {
|
|
1900
1969
|
return;
|
|
1901
1970
|
}
|
|
@@ -1915,10 +1984,11 @@ function Tessera({
|
|
|
1915
1984
|
if (!editor) {
|
|
1916
1985
|
return null;
|
|
1917
1986
|
}
|
|
1918
|
-
return /* @__PURE__ */ jsx12(TesseraContext.Provider, { value: { editor, locale, t, ai }, children: /* @__PURE__ */ jsxs11(
|
|
1987
|
+
return /* @__PURE__ */ jsx12(TesseraContext.Provider, { value: { editor, locale, t, ai, extraSelectionItems }, children: /* @__PURE__ */ jsxs11(
|
|
1919
1988
|
"div",
|
|
1920
1989
|
{
|
|
1921
1990
|
className: "tessera-root",
|
|
1991
|
+
"data-readonly": editable ? void 0 : "true",
|
|
1922
1992
|
style: { "--te-doc-max-width": docWidth ? `${docWidth}px` : void 0 },
|
|
1923
1993
|
children: [
|
|
1924
1994
|
/* @__PURE__ */ jsx12(EditorContent, { editor }),
|
|
@@ -1956,9 +2026,9 @@ function Tessera({
|
|
|
1956
2026
|
),
|
|
1957
2027
|
/* @__PURE__ */ jsx12(EmptyLineToolbar, {}),
|
|
1958
2028
|
/* @__PURE__ */ jsx12(SelectionToolbar, { onSession: setSession }),
|
|
2029
|
+
/* @__PURE__ */ jsx12(LinkEditor, {}),
|
|
1959
2030
|
/* @__PURE__ */ jsx12(FindReplacePanel, {}),
|
|
1960
2031
|
/* @__PURE__ */ jsx12(AskPanel, {}),
|
|
1961
|
-
/* @__PURE__ */ jsx12(HistoryPanel, {}),
|
|
1962
2032
|
/* @__PURE__ */ jsx12(CommentPanel, {}),
|
|
1963
2033
|
/* @__PURE__ */ jsx12(BlockContextMenuUI, {}),
|
|
1964
2034
|
/* @__PURE__ */ jsx12(SuggestionBar, { session, onClear: () => setSession(null) }),
|
|
@@ -1992,7 +2062,6 @@ export {
|
|
|
1992
2062
|
EmbedBlockView,
|
|
1993
2063
|
EmptyLineToolbar,
|
|
1994
2064
|
FindReplacePanel,
|
|
1995
|
-
HistoryPanel,
|
|
1996
2065
|
ImageBlockView,
|
|
1997
2066
|
SelectionToolbar,
|
|
1998
2067
|
SlashMenuView,
|