@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.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/Tessera.tsx
2
- import { useCallback as useCallback3, useEffect as useEffect12, useMemo, useRef as useRef7, useState as useState13 } from "react";
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
- return () => window.removeEventListener("keydown", close);
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__ */ jsx("div", { className: "tessera-lightbox", onClick: () => setPreview(null), children: /* @__PURE__ */ jsx("img", { src: preview, alt: alt ?? "" }) }),
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 useState2 } from "react";
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 [open, setOpen] = useState2(false);
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: () => setOpen((v) => !v),
268
+ onClick: toggle,
177
269
  children: "\u2304"
178
270
  }
179
271
  ),
180
- open ? /* @__PURE__ */ jsxs2("div", { className: "tessera-popover tessera-col-menu", contentEditable: false, onMouseDown: (e) => e.preventDefault(), children: [
181
- /* @__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)) }),
182
- /* @__PURE__ */ jsx2("div", { className: "tessera-menu-sep" }),
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
- type: "button",
195
- onClick: () => run(() => {
196
- const csv = tableToCsvAt(editor);
197
- if (csv) {
198
- void navigator.clipboard.writeText(csv);
199
- }
200
- }),
201
- children: t("tableCopyCsv")
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
- /* @__PURE__ */ jsx2("button", { type: "button", className: "tessera-danger", onClick: () => run(() => editor.commands.deleteTable()), children: t("tableDelete") })
205
- ] }) : null
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] = useState2("");
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 useEffect2, useState as useState3 } from "react";
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] = useState3(typeof node.attrs.src === "string" ? node.attrs.src : "");
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] = useState3([]);
400
- useEffect2(() => {
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 useEffect3, useImperativeHandle, useRef as useRef2, useState as useState4, forwardRef } from "react";
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] = useState4(0);
452
- const listRef = useRef2(null);
453
- const [rect, setRect] = useState4(null);
454
- useEffect3(() => {
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
- useEffect3(() => {
562
+ useEffect5(() => {
458
563
  setSelectedIndex(0);
459
564
  }, [items]);
460
- useEffect3(() => {
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 useEffect4, useImperativeHandle as useImperativeHandle2, useRef as useRef3, useState as useState5, forwardRef as forwardRef2 } from "react";
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] = useState5(0);
581
- const [rect, setRect] = useState5(null);
582
- const gridRef = useRef3(null);
583
- useEffect4(() => {
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
- useEffect4(() => {
691
+ useEffect6(() => {
587
692
  setSelectedIndex(0);
588
693
  }, [items]);
589
- useEffect4(() => {
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 useEffect6, useLayoutEffect, useRef as useRef4, useState as useState7 } from "react";
681
- import { createPortal as createPortal2 } from "react-dom";
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, hasAi) {
712
- const base = defaultSlashItems(t);
713
- if (!hasAi) {
714
- return base;
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, ai } = useContext5(TesseraContext);
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 = useRef4(false);
725
- const rootRef = useRef4(null);
726
- const panelRef = useRef4(null);
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
- useEffect6(() => {
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, !!ai);
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 createPortal2(
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/SelectionToolbar.tsx
873
- import { useCallback as useCallback2, useContext as useContext7, useEffect as useEffect8, useLayoutEffect as useLayoutEffect2, useRef as useRef5, useState as useState9 } from "react";
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 useContext6, useEffect as useEffect7, useState as useState8 } from "react";
880
- import { createPortal as createPortal3 } from "react-dom";
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 jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
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 } = useContext6(TesseraContext);
888
- const [visible, setVisible] = useState8(false);
889
- const [threads, setThreads] = useState8([]);
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
- useEffect7(() => {
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__ */ jsxs6("div", { className: "tessera-comment-panel", "data-testid": "comment-panel", children: [
944
- /* @__PURE__ */ jsxs6("div", { className: "tessera-ask-header", children: [
945
- /* @__PURE__ */ jsxs6("span", { children: [
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__ */ jsxs6("div", { className: "tessera-history-actions", children: [
951
- /* @__PURE__ */ jsx7("button", { type: "button", title: "\u2191", onClick: () => editor.commands.focusNextCommentThread(), children: "\u2191" }),
952
- /* @__PURE__ */ jsx7("button", { type: "button", title: "\u2193", onClick: () => editor.commands.focusNextCommentThread(), children: "\u2193" }),
953
- /* @__PURE__ */ jsx7("button", { type: "button", onClick: () => setVisible(false), children: "\xD7" })
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__ */ jsx7("div", { className: "tessera-ask-error", children: "CommentStore not injected" }) : null,
957
- /* @__PURE__ */ jsxs6("div", { className: "tessera-ask-body", children: [
958
- threads.length === 0 ? /* @__PURE__ */ jsx7("div", { className: "tessera-toc-empty", children: t("commentEmpty") }) : null,
959
- threads.map((thread) => /* @__PURE__ */ jsxs6("div", { className: `tessera-thread${thread.resolved ? " tessera-thread--resolved" : ""}`, children: [
960
- /* @__PURE__ */ jsxs6("div", { className: "tessera-thread-quote", children: [
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__ */ jsxs6("div", { className: "tessera-thread-entry", children: [
966
- /* @__PURE__ */ jsx7("span", { className: "tessera-thread-author", children: entry.authorName }),
967
- /* @__PURE__ */ jsx7("span", { className: "tessera-thread-text", children: entry.text })
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__ */ jsxs6("div", { className: "tessera-thread-actions", children: [
970
- /* @__PURE__ */ jsx7("button", { type: "button", onClick: () => void toggleResolve(thread), children: thread.resolved ? t("commentReopen") : t("commentResolve") }),
971
- /* @__PURE__ */ jsx7("button", { type: "button", className: "tessera-danger", onClick: () => void removeThread(thread), children: t("commentDelete") }),
972
- thread.resolved ? /* @__PURE__ */ jsx7("span", { className: "tessera-thread-badge", children: t("commentResolvedBadge") }) : null
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__ */ jsxs6("div", { className: "tessera-comment-hint", children: [
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 } = useContext6(TesseraContext);
984
- const [text, setText] = useState8("");
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 createPortal3(
1013
- /* @__PURE__ */ jsxs6("div", { className: "tessera-popover tessera-comment-composer", "data-testid": "comment-composer", children: [
1014
- /* @__PURE__ */ jsxs6("div", { className: "tessera-thread-quote", children: [
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__ */ jsx7(
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__ */ jsx7("div", { className: "tessera-comment-composer-actions", children: /* @__PURE__ */ jsx7("button", { type: "button", disabled: !text.trim(), onClick: () => void submit(), children: t("commentSend") }) })
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 jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
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 = useRef5(null);
1052
- const [placement, setPlacement] = useState9("bottom");
1053
- const [maxHeight, setMaxHeight] = useState9(void 0);
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__ */ jsx8(
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 } = useContext7(TesseraContext);
1083
- const [style, setStyle] = useState9(null);
1084
- const [popover, setPopover] = useState9(null);
1085
- const [linkValue, setLinkValue] = useState9("");
1086
- const composingRef = useRef5(false);
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
- useEffect8(() => {
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 createPortal4(
1157
- /* @__PURE__ */ jsxs7(
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__ */ jsx8(TbBtn, { title: t("tooltipImprove"), className: "tessera-tb-ai", onClick: () => setPopover((p) => p === "improve" ? null : "improve"), children: "\u2728" }) : null,
1166
- /* @__PURE__ */ jsx8(TbBtn, { title: t("tooltipBold"), active: editor.isActive("bold"), onClick: () => chain().toggleBold().run(), children: /* @__PURE__ */ jsx8("b", { children: "B" }) }),
1167
- /* @__PURE__ */ jsx8(TbBtn, { title: t("tooltipItalic"), active: editor.isActive("italic"), onClick: () => chain().toggleItalic().run(), children: /* @__PURE__ */ jsx8("i", { children: "I" }) }),
1168
- /* @__PURE__ */ jsx8(TbBtn, { title: t("tooltipUnderline"), active: editor.isActive("underline"), onClick: () => chain().toggleUnderline().run(), children: /* @__PURE__ */ jsx8("u", { children: "U" }) }),
1169
- /* @__PURE__ */ jsx8(TbBtn, { title: t("tooltipStrike"), active: editor.isActive("strike"), onClick: () => chain().toggleStrike().run(), children: /* @__PURE__ */ jsx8("s", { children: "S" }) }),
1170
- /* @__PURE__ */ jsx8(TbBtn, { title: t("tooltipCode"), active: editor.isActive("code"), onClick: () => chain().toggleCode().run(), children: "</>" }),
1171
- /* @__PURE__ */ jsx8(TbBtn, { title: t("tooltipColor"), active: !!currentColor, onClick: () => setPopover((p) => p === "color" ? null : "color"), children: /* @__PURE__ */ jsx8("span", { className: "tessera-tb-colorchip", style: { background: currentColor ?? "#262629" } }) }),
1172
- /* @__PURE__ */ jsx8(TbBtn, { title: t("tooltipHighlight"), active: editor.isActive("highlight"), onClick: () => setPopover((p) => p === "highlight" ? null : "highlight"), children: /* @__PURE__ */ jsx8("span", { className: "tessera-tb-colorchip", style: { background: "#fff2a8" } }) }),
1173
- /* @__PURE__ */ jsx8(TbBtn, { title: t("tooltipLink"), active: editor.isActive("link"), onClick: () => setPopover((p) => p === "link" ? null : "link"), children: "\u{1F517}" }),
1174
- /* @__PURE__ */ jsx8(
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__ */ jsx8(TbBtn, { title: t("tooltipTurnCollapsible"), onClick: () => chain().insertCollapsible().run(), children: "\u25B8" }),
1184
- /* @__PURE__ */ jsx8(TbBtn, { title: t("tooltipMore"), onClick: () => setPopover((p) => p === "more" ? null : "more"), children: "\u22EF" }),
1185
- popover === "color" ? /* @__PURE__ */ jsxs7(FlipPopover, { posKey: style, children: [
1186
- /* @__PURE__ */ jsx8("button", { type: "button", className: "tessera-color-swatch tessera-color-none", title: t("colorDefault"), onClick: () => chain().unsetColor().run() }),
1187
- TEXT_COLORS.map((color) => /* @__PURE__ */ jsx8("button", { type: "button", className: "tessera-color-swatch", style: { background: color }, onClick: () => chain().setColor(color).run() }, color))
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__ */ jsxs7(FlipPopover, { posKey: style, children: [
1190
- /* @__PURE__ */ jsx8("button", { type: "button", className: "tessera-color-swatch tessera-color-none", title: t("highlightNone"), onClick: () => chain().unsetHighlight().run() }),
1191
- HIGHLIGHT_COLORS.map((color) => /* @__PURE__ */ jsx8("button", { type: "button", className: "tessera-color-swatch", style: { background: color }, onClick: () => chain().toggleHighlight({ color }).run() }, color))
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__ */ jsxs7(FlipPopover, { posKey: style, className: "tessera-link-popover", children: [
1194
- /* @__PURE__ */ jsx8(
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__ */ jsx8(
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__ */ jsx8(
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__ */ jsx8(FlipPopover, { posKey: style, className: "tessera-popover-menu", children: /* @__PURE__ */ jsx8(
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__ */ jsx8(ImprovePopover, { posKey: style, onSession, onClose: () => setPopover(null) }) : null,
1250
- popover === "comment" ? /* @__PURE__ */ jsx8(CommentComposer, { onClose: () => setPopover(null) }) : null
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__ */ jsx8(
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 } = useContext7(TesseraContext);
1283
- const [instruction, setInstruction] = useState9("");
1284
- const [busy, setBusy] = useState9(false);
1285
- const [error, setError] = useState9(null);
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__ */ jsxs7(FlipPopover, { posKey, className: "tessera-improve-popover", testId: "improve-popover", children: [
1303
- IMPROVE_PRESETS.map((preset) => /* @__PURE__ */ jsx8("button", { type: "button", disabled: busy, onClick: () => run(preset.instruction), children: locale === "zh-CN" ? preset.labelZh : preset.labelEn }, preset.id)),
1304
- /* @__PURE__ */ jsxs7("div", { className: "tessera-improve-custom", children: [
1305
- /* @__PURE__ */ jsx8(
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__ */ jsx8("button", { type: "button", disabled: busy || !instruction.trim(), onClick: () => run(instruction.trim()), children: t("aiImproveRun") })
1501
+ /* @__PURE__ */ jsx9("button", { type: "button", disabled: busy || !instruction.trim(), onClick: () => run(instruction.trim()), children: t("aiImproveRun") })
1319
1502
  ] }),
1320
- error ? /* @__PURE__ */ jsx8("div", { className: "tessera-popover-error", children: error }) : null
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 useContext8, useEffect as useEffect9, useRef as useRef6, useState as useState10 } from "react";
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 jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
1510
+ import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
1328
1511
  function FindReplacePanel() {
1329
- const { editor, t } = useContext8(TesseraContext);
1330
- const [visible, setVisible] = useState10(false);
1331
- const [query, setQuery] = useState10("");
1332
- const [replacement, setReplacement] = useState10("");
1333
- const [count, setCount] = useState10(0);
1334
- const [active, setActive] = useState10(0);
1335
- const queryRef = useRef6(query);
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
- useEffect9(() => {
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__ */ jsxs8("div", { className: "tessera-find-panel", "data-testid": "find-panel", children: [
1372
- /* @__PURE__ */ jsx9(
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__ */ jsx9("span", { className: "tessera-find-count", children: count > 0 ? `${active + 1}/${count}` : "0" }),
1391
- /* @__PURE__ */ jsx9("button", { type: "button", title: t("findPrev"), onClick: () => editor.commands.findPrev(), children: "\u2191" }),
1392
- /* @__PURE__ */ jsx9("button", { type: "button", title: t("findNext"), onClick: () => editor.commands.findNext(), children: "\u2193" }),
1393
- /* @__PURE__ */ jsx9(
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__ */ jsx9("button", { type: "button", onClick: () => editor.commands.replaceCurrent(replacement), children: t("replaceOne") }),
1402
- /* @__PURE__ */ jsx9("button", { type: "button", onClick: () => editor.commands.replaceAll(replacement), children: t("replaceAll") }),
1403
- /* @__PURE__ */ jsx9("button", { type: "button", className: "tessera-find-close", title: t("findClose"), onClick: () => editor.commands.closeFindPanel(), children: "\xD7" })
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 } = useContext8(TesseraContext);
1408
- const [visible, setVisible] = useState10(false);
1409
- const [question, setQuestion] = useState10("");
1410
- const [answer, setAnswer] = useState10("");
1411
- const [thinking, setThinking] = useState10(false);
1412
- const [error, setError] = useState10(null);
1413
- useEffect9(() => {
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__ */ jsxs8("div", { className: "tessera-ask-panel", "data-testid": "ask-panel", children: [
1439
- /* @__PURE__ */ jsxs8("div", { className: "tessera-ask-header", children: [
1440
- /* @__PURE__ */ jsx9("span", { children: t("aiTitle") }),
1441
- /* @__PURE__ */ jsx9("button", { type: "button", onClick: () => setVisible(false), children: "\xD7" })
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__ */ jsx9("div", { className: "tessera-ask-error", children: t("aiRuntimeMissing") }),
1444
- /* @__PURE__ */ jsxs8("div", { className: "tessera-ask-body", children: [
1445
- answer ? /* @__PURE__ */ jsx9("div", { className: "tessera-ask-answer", children: answer }) : null,
1446
- thinking ? /* @__PURE__ */ jsx9("div", { className: "tessera-ask-thinking", children: t("aiThinking") }) : null,
1447
- error ? /* @__PURE__ */ jsx9("div", { className: "tessera-ask-error", children: error }) : null
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__ */ jsxs8("div", { className: "tessera-ask-input", children: [
1450
- /* @__PURE__ */ jsx9(
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__ */ jsx9("button", { type: "button", disabled: !ai || thinking || !question.trim(), onClick: ask, children: t("aiSend") })
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 } = useContext8(TesseraContext);
1654
+ const { t } = useContext9(TesseraContext);
1472
1655
  if (!session) {
1473
1656
  return null;
1474
1657
  }
1475
- return /* @__PURE__ */ jsxs8("div", { className: "tessera-suggestion-bar", "data-testid": "suggestion-bar", children: [
1476
- /* @__PURE__ */ jsxs8("span", { className: "tessera-suggestion-label", children: [
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__ */ jsx9(
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__ */ jsx9(
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 useEffect11, useState as useState12 } from "react";
1645
- import { createPortal as createPortal5 } from "react-dom";
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
- useEffect11(() => {
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 createPortal5(
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 = useRef7(null);
1780
- const dragNodeRef = useRef7(null);
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 = useRef7(onUpdate);
1833
+ const onUpdateRef = useRef9(onUpdate);
1785
1834
  onUpdateRef.current = onUpdate;
1786
- const onCreateRef = useRef7(onCreate);
1835
+ const onCreateRef = useRef9(onCreate);
1787
1836
  onCreateRef.current = onCreate;
1788
- const runtimeRef = useRef7(runtime);
1837
+ const runtimeRef = useRef9(runtime);
1789
1838
  runtimeRef.current = runtime;
1790
- const initialContentRef = useRef7(content);
1791
- const editorRef = useRef7(null);
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({ locale, historyIdleMs }).map((ext) => {
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, historyIdleMs]
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 storage2 = ed.storage;
1834
- const svc = storage2.tesseraServices?.upload;
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 = useRef7(uploadAndInsert);
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 }) => onCreateRef.current?.(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
- useEffect12(() => {
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, storage, comments, identity]);
1898
- useEffect12(() => {
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,