magic-editor-x 1.1.1 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/README.md +82 -5
  2. package/dist/_chunks/{App-mtrlABtd.js → App-CWXreQMz.js} +4 -4
  3. package/dist/_chunks/{App-B1FgOsWa.mjs → App-Czke2os9.mjs} +4 -4
  4. package/dist/_chunks/{LicensePage-BnyWSrWs.js → LicensePage-BIaCAB4C.js} +2 -2
  5. package/dist/_chunks/{LicensePage-CWH-AFR-.mjs → LicensePage-Cj6-z6rO.mjs} +2 -2
  6. package/dist/_chunks/{LiveCollaborationPanel-DbDHwr2C.js → LiveCollaborationPanel-BmAFvNll.js} +1 -1
  7. package/dist/_chunks/{LiveCollaborationPanel-ryjcDAA7.mjs → LiveCollaborationPanel-D11eAcKk.mjs} +1 -1
  8. package/dist/_chunks/{Settings-Bk9bxJTy.js → Settings-BsoK7S5l.js} +1 -1
  9. package/dist/_chunks/{Settings-D-V2MLVm.mjs → Settings-CsaF0hO7.mjs} +1 -1
  10. package/dist/_chunks/{de-CSrHZWEb.mjs → de-C_0Mj-Zo.mjs} +1 -0
  11. package/dist/_chunks/{de-CzSo1oD2.js → de-DVNVpAt9.js} +1 -0
  12. package/dist/_chunks/{en-DuQun2v4.mjs → en-B7AAf1ie.mjs} +1 -0
  13. package/dist/_chunks/{en-DxIkVPUh.js → en-C2hv5GsA.js} +1 -0
  14. package/dist/_chunks/{es-DAQ_97zx.js → es-BzJqcIST.js} +1 -0
  15. package/dist/_chunks/{es-DEB0CA8S.mjs → es-zdP8sd-f.mjs} +1 -0
  16. package/dist/_chunks/{fr-Bqkhvdx2.mjs → fr-CtAgOgH1.mjs} +1 -0
  17. package/dist/_chunks/{fr-ChPabvNP.js → fr-D3GwqAAJ.js} +1 -0
  18. package/dist/_chunks/{getTranslation-C4uWR0DB.mjs → getTranslation-CZ77ytJY.mjs} +7 -6
  19. package/dist/_chunks/{getTranslation-D35vbDap.js → getTranslation-DB9tlKh9.js} +2 -1
  20. package/dist/_chunks/{index-B5MzUyo0.mjs → index-BNxjfrVK.mjs} +9 -9
  21. package/dist/_chunks/{index-CQx7-dFP.js → index-BmWx_nJX.js} +1099 -87
  22. package/dist/_chunks/{index-BRVqbnOb.mjs → index-Byp_IaHi.mjs} +1100 -88
  23. package/dist/_chunks/{index-BiLy_f7C.js → index-DO-QpiC9.js} +9 -9
  24. package/dist/_chunks/{pt-BMoYltav.mjs → pt-BmUw6YMP.mjs} +1 -0
  25. package/dist/_chunks/{pt-Cm74LpyZ.js → pt-CFo0nTbj.js} +1 -0
  26. package/dist/_chunks/{tools-DNt2tioN.js → tools-3bMqs3Or.js} +103 -4
  27. package/dist/_chunks/{tools-CjnQJ9w2.mjs → tools-CNqrBm-q.mjs} +103 -4
  28. package/dist/admin/index.js +1 -1
  29. package/dist/admin/index.mjs +1 -1
  30. package/dist/server/index.js +37242 -1292
  31. package/dist/server/index.mjs +37228 -1285
  32. package/package.json +1 -1
@@ -2,13 +2,13 @@
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const jsxRuntime = require("react/jsx-runtime");
4
4
  const React = require("react");
5
- const getTranslation = require("./getTranslation-D35vbDap.js");
5
+ const getTranslation = require("./getTranslation-DB9tlKh9.js");
6
6
  const styled = require("styled-components");
7
7
  const outline = require("@heroicons/react/24/outline");
8
8
  const EditorJS = require("@editorjs/editorjs");
9
- const tools = require("./tools-DNt2tioN.js");
9
+ const tools = require("./tools-3bMqs3Or.js");
10
10
  const admin = require("@strapi/strapi/admin");
11
- const index = require("./index-BiLy_f7C.js");
11
+ const index = require("./index-DO-QpiC9.js");
12
12
  const socket_ioClient = require("socket.io-client");
13
13
  const Y = require("yjs");
14
14
  const yIndexeddb = require("y-indexeddb");
@@ -448,6 +448,89 @@ const getUserColor = (userId) => {
448
448
  const hash = String(userId).split("").reduce((acc, char) => acc + char.charCodeAt(0), 0);
449
449
  return CURSOR_COLORS[hash % CURSOR_COLORS.length];
450
450
  };
451
+ const htmlToDelta = (html) => {
452
+ if (!html || html === "<br>" || html === "<br/>") return [];
453
+ const parser = new DOMParser();
454
+ const doc = parser.parseFromString(`<div>${html}</div>`, "text/html");
455
+ const delta = [];
456
+ function processNode(node, attributes = {}) {
457
+ if (node.nodeType === Node.TEXT_NODE) {
458
+ const text = node.textContent;
459
+ if (text) {
460
+ const attrs = Object.keys(attributes).length > 0 ? { ...attributes } : void 0;
461
+ delta.push({ insert: text, attributes: attrs });
462
+ }
463
+ return;
464
+ }
465
+ if (node.nodeType === Node.ELEMENT_NODE) {
466
+ const newAttrs = { ...attributes };
467
+ const tagName = node.tagName.toLowerCase();
468
+ switch (tagName) {
469
+ case "b":
470
+ case "strong":
471
+ newAttrs.bold = true;
472
+ break;
473
+ case "i":
474
+ case "em":
475
+ newAttrs.italic = true;
476
+ break;
477
+ case "u":
478
+ newAttrs.underline = true;
479
+ break;
480
+ case "code":
481
+ newAttrs.code = true;
482
+ break;
483
+ case "a":
484
+ newAttrs.a = {
485
+ href: node.getAttribute("href") || "",
486
+ target: node.getAttribute("target") || "_blank",
487
+ rel: node.getAttribute("rel") || "noopener noreferrer"
488
+ };
489
+ break;
490
+ case "mark":
491
+ newAttrs.mark = true;
492
+ break;
493
+ case "br":
494
+ delta.push({ insert: "\n" });
495
+ return;
496
+ }
497
+ for (const child of node.childNodes) {
498
+ processNode(child, newAttrs);
499
+ }
500
+ }
501
+ }
502
+ const wrapper = doc.body.firstChild;
503
+ if (wrapper) {
504
+ for (const child of wrapper.childNodes) {
505
+ processNode(child);
506
+ }
507
+ }
508
+ return delta;
509
+ };
510
+ const deltaToHtml = (delta) => {
511
+ if (!delta || delta.length === 0) return "";
512
+ let html = "";
513
+ for (const op of delta) {
514
+ if (typeof op.insert !== "string") continue;
515
+ let text = op.insert;
516
+ text = text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/\n/g, "<br>");
517
+ const attrs = op.attributes || {};
518
+ let result = text;
519
+ if (attrs.code) result = `<code>${result}</code>`;
520
+ if (attrs.italic) result = `<i>${result}</i>`;
521
+ if (attrs.bold) result = `<b>${result}</b>`;
522
+ if (attrs.underline) result = `<u>${result}</u>`;
523
+ if (attrs.mark) result = `<mark>${result}</mark>`;
524
+ if (attrs.a) {
525
+ const href = attrs.a.href || "";
526
+ const target = attrs.a.target || "_blank";
527
+ const rel = attrs.a.rel || "noopener noreferrer";
528
+ result = `<a href="${href}" target="${target}" rel="${rel}">${result}</a>`;
529
+ }
530
+ html += result;
531
+ }
532
+ return html;
533
+ };
451
534
  const useMagicCollaboration = ({
452
535
  enabled,
453
536
  roomId,
@@ -472,16 +555,47 @@ const useMagicCollaboration = ({
472
555
  React.useEffect(() => {
473
556
  onRemoteUpdateRef.current = onRemoteUpdate;
474
557
  }, [onRemoteUpdate]);
475
- const { doc, blocksMap, metaMap } = React.useMemo(() => {
558
+ const { doc, blocksMap, textMap, metaMap } = React.useMemo(() => {
476
559
  const yDoc = new Y__namespace.Doc();
477
560
  return {
478
561
  doc: yDoc,
479
562
  blocksMap: yDoc.getMap("blocks"),
480
- // Each block stored by ID
563
+ // Block metadata (type, tunes)
564
+ textMap: yDoc.getMap("text"),
565
+ // Y.Text per block (character-level sync!)
481
566
  metaMap: yDoc.getMap("meta")
482
- // Metadata (time, blockOrder, etc.)
567
+ // Document metadata (time, blockOrder)
483
568
  };
484
569
  }, [roomId]);
570
+ const getBlockText = React.useCallback((blockId) => {
571
+ if (!blockId) return null;
572
+ let ytext = textMap.get(blockId);
573
+ if (!ytext) {
574
+ ytext = new Y__namespace.Text();
575
+ textMap.set(blockId, ytext);
576
+ }
577
+ return ytext;
578
+ }, [textMap]);
579
+ const setBlockText = React.useCallback((blockId, html) => {
580
+ if (!blockId) return;
581
+ const ytext = getBlockText(blockId);
582
+ if (!ytext) return;
583
+ doc.transact(() => {
584
+ if (ytext.length > 0) {
585
+ ytext.delete(0, ytext.length);
586
+ }
587
+ const delta = htmlToDelta(html);
588
+ if (delta.length > 0) {
589
+ ytext.applyDelta(delta);
590
+ }
591
+ }, "local");
592
+ }, [doc, getBlockText]);
593
+ const getBlockTextHtml = React.useCallback((blockId) => {
594
+ if (!blockId) return "";
595
+ const ytext = textMap.get(blockId);
596
+ if (!ytext) return "";
597
+ return deltaToHtml(ytext.toDelta());
598
+ }, [textMap]);
485
599
  React.useEffect(() => {
486
600
  return () => {
487
601
  doc.destroy();
@@ -546,9 +660,6 @@ const useMagicCollaboration = ({
546
660
  persistenceRef.current = persistence;
547
661
  persistence.on("synced", () => {
548
662
  console.log("[Magic Collab] [CACHE] IndexedDB synced for room:", roomId);
549
- const blockOrder = metaMap.get("blockOrder");
550
- fetch("http://127.0.0.1:7242/ingest/12c1170b-f275-4a73-9ee7-3006bf7f0881", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ location: "useMagicCollaboration:indexeddb:synced", message: "IndexedDB synced - loaded local state", data: { blocksMapSize: blocksMap.size, blocksMapKeys: Array.from(blocksMap.keys()), blockOrder, roomId, persistenceKey }, timestamp: Date.now(), sessionId: "debug-session", hypothesisId: "H" }) }).catch(() => {
551
- });
552
663
  });
553
664
  console.log("[Magic Collab] [CACHE] IndexedDB persistence initialized:", persistenceKey);
554
665
  } catch (e) {
@@ -635,17 +746,11 @@ const useMagicCollaboration = ({
635
746
  if (update) {
636
747
  console.log("[Magic Collab] [SYNC] Syncing initial state, update size:", update.length, "bytes");
637
748
  try {
638
- const blockOrderBefore = metaMap.get("blockOrder");
639
- fetch("http://127.0.0.1:7242/ingest/12c1170b-f275-4a73-9ee7-3006bf7f0881", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ location: "useMagicCollaboration:collab:sync:before", message: "BEFORE applying server sync", data: { blocksMapSize: blocksMap.size, blocksMapKeys: Array.from(blocksMap.keys()), blockOrder: blockOrderBefore, roomId }, timestamp: Date.now(), sessionId: "debug-session", hypothesisId: "F" }) }).catch(() => {
640
- });
641
749
  const beforeBlockCount = blocksMap.size;
642
750
  console.log("[Magic Collab] [DATA] Y.Map BEFORE sync - block count:", beforeBlockCount);
643
751
  Y__namespace.applyUpdate(doc, new Uint8Array(update), "remote");
644
752
  const afterBlockCount = blocksMap.size;
645
753
  console.log("[Magic Collab] [DATA] Y.Map AFTER sync - block count:", afterBlockCount);
646
- const blockOrderAfter = metaMap.get("blockOrder");
647
- fetch("http://127.0.0.1:7242/ingest/12c1170b-f275-4a73-9ee7-3006bf7f0881", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ location: "useMagicCollaboration:collab:sync:after", message: "AFTER applying server sync", data: { blocksMapSize: blocksMap.size, blocksMapKeys: Array.from(blocksMap.keys()), blockOrder: blockOrderAfter, roomId }, timestamp: Date.now(), sessionId: "debug-session", hypothesisId: "F" }) }).catch(() => {
648
- });
649
754
  if (onRemoteUpdateRef.current) {
650
755
  console.log("[Magic Collab] [CALLBACK] Calling onRemoteUpdate callback after sync");
651
756
  setTimeout(() => {
@@ -661,15 +766,11 @@ const useMagicCollaboration = ({
661
766
  if (update) {
662
767
  console.log("[Magic Collab] [UPDATE] Received remote update:", update.length, "bytes");
663
768
  try {
664
- fetch("http://127.0.0.1:7242/ingest/12c1170b-f275-4a73-9ee7-3006bf7f0881", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ location: "useMagicCollaboration:collab:update:before", message: "BEFORE applying remote update", data: { blocksMapSize: blocksMap.size, blocksMapKeys: Array.from(blocksMap.keys()), blockOrder: metaMap.get("blockOrder"), updateSize: update.length, roomId }, timestamp: Date.now(), sessionId: "debug-session", hypothesisId: "G" }) }).catch(() => {
665
- });
666
769
  const beforeBlockCount = blocksMap.size;
667
770
  console.log("[Magic Collab] [DATA] Y.Map BEFORE update - blocks:", beforeBlockCount);
668
771
  Y__namespace.applyUpdate(doc, new Uint8Array(update), "remote");
669
772
  const afterBlockCount = blocksMap.size;
670
773
  console.log("[Magic Collab] [DATA] Y.Map AFTER update - blocks:", afterBlockCount);
671
- fetch("http://127.0.0.1:7242/ingest/12c1170b-f275-4a73-9ee7-3006bf7f0881", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ location: "useMagicCollaboration:collab:update:after", message: "AFTER applying remote update", data: { blocksMapSize: blocksMap.size, blocksMapKeys: Array.from(blocksMap.keys()), blockOrder: metaMap.get("blockOrder"), roomId }, timestamp: Date.now(), sessionId: "debug-session", hypothesisId: "G" }) }).catch(() => {
672
- });
673
774
  if (onRemoteUpdateRef.current) {
674
775
  console.log("[Magic Collab] [CALLBACK] Calling onRemoteUpdate callback");
675
776
  setTimeout(() => {
@@ -793,13 +894,30 @@ const useMagicCollaboration = ({
793
894
  return user ? getUserColor(user.id) : CURSOR_COLORS[0];
794
895
  }, [user]);
795
896
  return {
897
+ // Y.js Document & Maps
796
898
  doc,
797
899
  blocksMap,
798
- // Y.Map for block-level sync (replaces Y.Text)
900
+ // Y.Map for block metadata (type, tunes)
901
+ textMap,
902
+ // Y.Map<blockId, Y.Text> for character-level text sync
799
903
  metaMap,
800
- // Y.Map for metadata (includes blockOrder as JSON string)
904
+ // Y.Map for document metadata (time, blockOrder)
905
+ // Character-level text helpers
906
+ getBlockText,
907
+ // Get Y.Text for a block (creates if not exists)
908
+ setBlockText,
909
+ // Set block text from HTML
910
+ getBlockTextHtml,
911
+ // Get block text as HTML
912
+ // Utility functions
913
+ htmlToDelta,
914
+ // Convert HTML to delta
915
+ deltaToHtml,
916
+ // Convert delta to HTML
917
+ // Connection status
801
918
  status,
802
919
  error,
920
+ // Collaboration
803
921
  peers,
804
922
  awareness,
805
923
  emitAwareness,
@@ -1097,6 +1215,136 @@ const useAIActions = ({ licenseKey, editorInstanceRef, isReady, onNoCredits }) =
1097
1215
  }, [replaceText, appendText, onNoCredits]);
1098
1216
  return { handleAIAction };
1099
1217
  };
1218
+ const useWebtoolsLinks = () => {
1219
+ const getPlugin = admin.useStrapiApp("WebtoolsLinks", (state) => state.getPlugin);
1220
+ const linksPlugin = React.useMemo(() => {
1221
+ try {
1222
+ return getPlugin?.("webtools-addon-links");
1223
+ } catch (e) {
1224
+ return null;
1225
+ }
1226
+ }, [getPlugin]);
1227
+ const isAvailable = React.useMemo(() => {
1228
+ const available = !!linksPlugin?.apis?.openLinkPicker;
1229
+ if (typeof window !== "undefined" && !window.__WEBTOOLS_LINKS_CHECKED__) {
1230
+ window.__WEBTOOLS_LINKS_CHECKED__ = true;
1231
+ if (available) {
1232
+ console.log("[Magic Editor X] [SUCCESS] Webtools Links addon detected - Link Picker enabled");
1233
+ } else {
1234
+ console.log("[Magic Editor X] [INFO] Webtools Links addon not installed - Link Picker disabled");
1235
+ }
1236
+ }
1237
+ return available;
1238
+ }, [linksPlugin]);
1239
+ const openLinkPicker = React.useCallback(async ({ initialHref = "", initialText = "" } = {}) => {
1240
+ if (!linksPlugin?.apis?.openLinkPicker) {
1241
+ console.warn("[Magic Editor X] Webtools Link Picker not available");
1242
+ return null;
1243
+ }
1244
+ console.log("[Magic Editor X] Opening Webtools Link Picker with:", {
1245
+ linkType: "both",
1246
+ initialHref: initialHref || "(empty)",
1247
+ initialText: initialText || "(empty)"
1248
+ });
1249
+ try {
1250
+ const result = await linksPlugin.apis.openLinkPicker({
1251
+ linkType: "both",
1252
+ // Allow both internal and external links
1253
+ initialHref: initialHref || "",
1254
+ initialText: initialText || ""
1255
+ });
1256
+ console.log("[Magic Editor X] Webtools picker result:", result);
1257
+ if (result && result.href) {
1258
+ console.log("[Magic Editor X] Webtools link selected:", result);
1259
+ return {
1260
+ href: result.href,
1261
+ label: result.label || initialText || ""
1262
+ };
1263
+ }
1264
+ return null;
1265
+ } catch (error) {
1266
+ console.error("[Magic Editor X] Error opening Webtools Link Picker:", error);
1267
+ return null;
1268
+ }
1269
+ }, [linksPlugin]);
1270
+ return {
1271
+ isAvailable,
1272
+ openLinkPicker
1273
+ };
1274
+ };
1275
+ const apiBase = "/magic-editor-x";
1276
+ const useVersionHistory = () => {
1277
+ const { get, post } = admin.useFetchClient();
1278
+ const { tier } = useLicense();
1279
+ const [snapshots, setSnapshots] = React.useState([]);
1280
+ const [loading, setLoading] = React.useState(false);
1281
+ const [error, setError] = React.useState(null);
1282
+ const fetchSnapshots = React.useCallback(
1283
+ async (roomId) => {
1284
+ if (!roomId) return;
1285
+ setLoading(true);
1286
+ setError(null);
1287
+ try {
1288
+ const { data } = await get(`${apiBase}/snapshots/${roomId}`);
1289
+ setSnapshots(data?.data || []);
1290
+ } catch (err) {
1291
+ setError(err?.message || "Failed to load snapshots");
1292
+ } finally {
1293
+ setLoading(false);
1294
+ }
1295
+ },
1296
+ [get]
1297
+ );
1298
+ const restoreSnapshot = React.useCallback(
1299
+ async (documentId, roomId) => {
1300
+ if (!documentId) return;
1301
+ setLoading(true);
1302
+ setError(null);
1303
+ try {
1304
+ const { data } = await post(`${apiBase}/snapshots/restore/${documentId}`, { roomId });
1305
+ return data?.data;
1306
+ } catch (err) {
1307
+ setError(err?.message || "Failed to restore snapshot");
1308
+ throw err;
1309
+ } finally {
1310
+ setLoading(false);
1311
+ }
1312
+ },
1313
+ [get]
1314
+ );
1315
+ const createSnapshot = React.useCallback(
1316
+ async ({ roomId, contentType, entryId, fieldName, content }) => {
1317
+ if (!roomId || !contentType || !entryId || !fieldName) return;
1318
+ setLoading(true);
1319
+ setError(null);
1320
+ try {
1321
+ const { data } = await post(`${apiBase}/snapshots/${roomId}`, {
1322
+ contentType,
1323
+ entryId,
1324
+ fieldName,
1325
+ content
1326
+ // Include editor content as fallback
1327
+ });
1328
+ return data?.data;
1329
+ } catch (err) {
1330
+ setError(err?.message || "Failed to create snapshot");
1331
+ throw err;
1332
+ } finally {
1333
+ setLoading(false);
1334
+ }
1335
+ },
1336
+ [post]
1337
+ );
1338
+ return {
1339
+ snapshots,
1340
+ loading,
1341
+ error,
1342
+ tier,
1343
+ fetchSnapshots,
1344
+ restoreSnapshot,
1345
+ createSnapshot
1346
+ };
1347
+ };
1100
1348
  const Overlay$1 = styled__default.default.div`
1101
1349
  position: fixed;
1102
1350
  top: 0;
@@ -1120,7 +1368,7 @@ const PopupContainer = styled__default.default.div`
1120
1368
  display: flex;
1121
1369
  flex-direction: column;
1122
1370
  `;
1123
- const Header$1 = styled__default.default.div`
1371
+ const Header$2 = styled__default.default.div`
1124
1372
  background: linear-gradient(135deg, #7C3AED 0%, #a855f7 100%);
1125
1373
  padding: 20px 24px;
1126
1374
  color: white;
@@ -1142,7 +1390,7 @@ const CreditsBadge = styled__default.default.div`
1142
1390
  font-size: 13px;
1143
1391
  font-weight: 500;
1144
1392
  `;
1145
- const Content$1 = styled__default.default.div`
1393
+ const Content$2 = styled__default.default.div`
1146
1394
  padding: 24px;
1147
1395
  overflow-y: auto;
1148
1396
  flex: 1;
@@ -1407,7 +1655,7 @@ const AIAssistantPopup = ({ selectedText, licenseKey, onClose, onApply }) => {
1407
1655
  return () => window.removeEventListener("keydown", handleKeyDown);
1408
1656
  }, [onClose]);
1409
1657
  return /* @__PURE__ */ jsxRuntime.jsx(Overlay$1, { onClick: handleOverlayClick, children: /* @__PURE__ */ jsxRuntime.jsxs(PopupContainer, { onClick: (e) => e.stopPropagation(), children: [
1410
- /* @__PURE__ */ jsxRuntime.jsxs(Header$1, { children: [
1658
+ /* @__PURE__ */ jsxRuntime.jsxs(Header$2, { children: [
1411
1659
  /* @__PURE__ */ jsxRuntime.jsxs(HeaderTitle, { children: [
1412
1660
  /* @__PURE__ */ jsxRuntime.jsx(SparklesIcon, {}),
1413
1661
  /* @__PURE__ */ jsxRuntime.jsx("span", { children: "KI-Assistent" })
@@ -1419,7 +1667,7 @@ const AIAssistantPopup = ({ selectedText, licenseKey, onClose, onApply }) => {
1419
1667
  !usage?.tier && "Wird geladen..."
1420
1668
  ] })
1421
1669
  ] }),
1422
- /* @__PURE__ */ jsxRuntime.jsxs(Content$1, { children: [
1670
+ /* @__PURE__ */ jsxRuntime.jsxs(Content$2, { children: [
1423
1671
  /* @__PURE__ */ jsxRuntime.jsx(TextPreview, { children: selectedText.length > 300 ? selectedText.substring(0, 300) + "..." : selectedText }),
1424
1672
  /* @__PURE__ */ jsxRuntime.jsxs(TypeButtons, { children: [
1425
1673
  /* @__PURE__ */ jsxRuntime.jsxs(
@@ -1506,6 +1754,132 @@ const AIAssistantPopup = ({ selectedText, licenseKey, onClose, onApply }) => {
1506
1754
  ] })
1507
1755
  ] }) });
1508
1756
  };
1757
+ const PanelWrapper = styled__default.default(getTranslation.Box)`
1758
+ width: 320px;
1759
+ background: ${({ theme }) => theme.colors.neutral0};
1760
+ border: 1px solid ${({ theme }) => theme.colors.neutral150};
1761
+ border-radius: 8px;
1762
+ box-shadow: ${({ theme }) => theme.shadows.filterShadow};
1763
+ display: flex;
1764
+ flex-direction: column;
1765
+ max-height: 70vh;
1766
+ `;
1767
+ const Header$1 = styled__default.default(getTranslation.Flex)`
1768
+ padding: 12px 16px;
1769
+ border-bottom: 1px solid ${({ theme }) => theme.colors.neutral150};
1770
+ `;
1771
+ const Content$1 = styled__default.default(getTranslation.Box)`
1772
+ padding: 12px 16px;
1773
+ overflow-y: auto;
1774
+ `;
1775
+ const Item = styled__default.default(getTranslation.Box)`
1776
+ padding: 10px 12px;
1777
+ border: 1px solid ${({ theme }) => theme.colors.neutral150};
1778
+ border-radius: 6px;
1779
+ margin-bottom: 10px;
1780
+ `;
1781
+ const Meta = styled__default.default(getTranslation.Typography)`
1782
+ color: ${({ theme }) => theme.colors.neutral500};
1783
+ font-size: 12px;
1784
+ `;
1785
+ const PremiumBadge = styled__default.default(getTranslation.Box)`
1786
+ background: ${({ theme }) => theme.colors.primary100};
1787
+ color: ${({ theme }) => theme.colors.primary600};
1788
+ border-radius: 6px;
1789
+ padding: 8px 10px;
1790
+ display: inline-flex;
1791
+ align-items: center;
1792
+ gap: 8px;
1793
+ font-weight: 600;
1794
+ margin-top: 8px;
1795
+ `;
1796
+ const safeDateFrom = (value) => {
1797
+ if (!value) return null;
1798
+ if (value instanceof Date) {
1799
+ return isNaN(value.getTime()) ? null : value;
1800
+ }
1801
+ try {
1802
+ const parsed = new Date(value);
1803
+ return isNaN(parsed.getTime()) ? null : parsed;
1804
+ } catch {
1805
+ return null;
1806
+ }
1807
+ };
1808
+ const formatDate = (dateValue) => {
1809
+ const date = safeDateFrom(dateValue);
1810
+ if (!date) return "—";
1811
+ try {
1812
+ return date.toLocaleString();
1813
+ } catch {
1814
+ return "—";
1815
+ }
1816
+ };
1817
+ const VersionHistoryPanel = ({
1818
+ snapshots,
1819
+ loading,
1820
+ error,
1821
+ onRestore,
1822
+ onCreate,
1823
+ tier,
1824
+ onClose
1825
+ }) => {
1826
+ const { formatMessage } = getTranslation.useIntl();
1827
+ const canRestore = tier !== "free";
1828
+ const t = (id, defaultMessage) => formatMessage(
1829
+ { id: getTranslation.getTranslation(id), defaultMessage },
1830
+ {}
1831
+ );
1832
+ return /* @__PURE__ */ jsxRuntime.jsxs(PanelWrapper, { "data-testid": "version-history-panel", children: [
1833
+ /* @__PURE__ */ jsxRuntime.jsxs(Header$1, { justifyContent: "space-between", alignItems: "center", children: [
1834
+ /* @__PURE__ */ jsxRuntime.jsxs(getTranslation.Flex, { gap: 8, alignItems: "center", children: [
1835
+ /* @__PURE__ */ jsxRuntime.jsx(outline.ClockIcon, { width: 18 }),
1836
+ /* @__PURE__ */ jsxRuntime.jsx(getTranslation.Typography, { fontWeight: "bold", children: t("versionHistory.title", "Version History") })
1837
+ ] }),
1838
+ /* @__PURE__ */ jsxRuntime.jsx(getTranslation.Button, { size: "S", variant: "tertiary", onClick: onClose, children: t("versionHistory.close", "Close") })
1839
+ ] }),
1840
+ /* @__PURE__ */ jsxRuntime.jsxs(Content$1, { children: [
1841
+ loading && /* @__PURE__ */ jsxRuntime.jsx(getTranslation.Typography, { children: t("versionHistory.loading", "Loading versions...") }),
1842
+ error && /* @__PURE__ */ jsxRuntime.jsx(getTranslation.Typography, { textColor: "danger600", children: error }),
1843
+ !loading && !error && snapshots.length === 0 && /* @__PURE__ */ jsxRuntime.jsx(getTranslation.Typography, { children: t("versionHistory.noSnapshots", "No versions saved yet") }),
1844
+ !loading && !error && snapshots.map((snap) => /* @__PURE__ */ jsxRuntime.jsxs(Item, { children: [
1845
+ /* @__PURE__ */ jsxRuntime.jsxs(getTranslation.Flex, { justifyContent: "space-between", alignItems: "center", children: [
1846
+ /* @__PURE__ */ jsxRuntime.jsxs(getTranslation.Typography, { fontWeight: "bold", children: [
1847
+ t("versionHistory.version", "Version"),
1848
+ " ",
1849
+ snap.version
1850
+ ] }),
1851
+ /* @__PURE__ */ jsxRuntime.jsx(getTranslation.Typography, { variant: "pi", children: formatDate(snap.createdAt) })
1852
+ ] }),
1853
+ /* @__PURE__ */ jsxRuntime.jsxs(Meta, { children: [
1854
+ t("versionHistory.createdBy", "By"),
1855
+ " ",
1856
+ snap.createdBy?.firstname ? `${snap.createdBy.firstname} ${snap.createdBy.lastname || ""}`.trim() : "—"
1857
+ ] }),
1858
+ /* @__PURE__ */ jsxRuntime.jsx(getTranslation.Divider, { marginTop: 2, marginBottom: 2 }),
1859
+ canRestore ? /* @__PURE__ */ jsxRuntime.jsx(getTranslation.Flex, { gap: 8, children: /* @__PURE__ */ jsxRuntime.jsx(
1860
+ getTranslation.Button,
1861
+ {
1862
+ size: "S",
1863
+ variant: "secondary",
1864
+ onClick: () => onRestore?.(snap),
1865
+ children: t("versionHistory.restore", "Restore")
1866
+ }
1867
+ ) }) : /* @__PURE__ */ jsxRuntime.jsxs(PremiumBadge, { children: [
1868
+ /* @__PURE__ */ jsxRuntime.jsx(outline.ExclamationTriangleIcon, { width: 16 }),
1869
+ t("versionHistory.premiumOnly", "Premium feature")
1870
+ ] })
1871
+ ] }, snap.documentId || snap.id)),
1872
+ /* @__PURE__ */ jsxRuntime.jsx(getTranslation.Divider, { marginTop: 4, marginBottom: 4 }),
1873
+ canRestore ? /* @__PURE__ */ jsxRuntime.jsx(getTranslation.Button, { size: "S", fullWidth: true, variant: "default", onClick: onCreate, disabled: loading, children: t("versionHistory.create", "Create Snapshot") }) : /* @__PURE__ */ jsxRuntime.jsxs(getTranslation.Box, { children: [
1874
+ /* @__PURE__ */ jsxRuntime.jsx(getTranslation.Button, { size: "S", fullWidth: true, variant: "default", disabled: true, children: t("versionHistory.create", "Create Snapshot") }),
1875
+ /* @__PURE__ */ jsxRuntime.jsxs(PremiumBadge, { style: { marginTop: "8px", width: "100%", justifyContent: "center" }, children: [
1876
+ /* @__PURE__ */ jsxRuntime.jsx(outline.ExclamationTriangleIcon, { width: 16 }),
1877
+ t("versionHistory.premiumOnly", "Premium feature")
1878
+ ] })
1879
+ ] })
1880
+ ] })
1881
+ ] });
1882
+ };
1509
1883
  const Overlay = styled__default.default.div`
1510
1884
  position: fixed;
1511
1885
  top: 0;
@@ -1762,19 +2136,118 @@ const FullscreenGlobalStyle = styled.createGlobalStyle`
1762
2136
  }
1763
2137
  `;
1764
2138
  const EditorJSGlobalStyles = styled.createGlobalStyle`
1765
- /* Popover rendered at document body */
1766
- body > .ce-popover,
1767
- body > .ce-popover--opened,
1768
- body > .ce-popover__container,
1769
- body > .ce-settings,
1770
- body > .ce-conversion-toolbar,
1771
- body > .ce-inline-toolbar {
2139
+ /* ============================================
2140
+ INLINE TOOLBAR - EditorJS 2.31
2141
+ Structure: .ce-inline-toolbar > .ce-popover--inline > .ce-popover__items > .ce-popover-item-html > .ce-inline-tool
2142
+ ============================================ */
2143
+
2144
+ /* Hide "Nothing found" message when inline tools ARE present */
2145
+ .ce-popover--inline .ce-popover__nothing-found-message {
2146
+ display: none !important;
2147
+ }
2148
+
2149
+ /* Inline Toolbar Popover - horizontal layout for tool buttons */
2150
+ .ce-popover--inline.ce-popover--opened {
2151
+ display: block !important;
2152
+ opacity: 1 !important;
2153
+ visibility: visible !important;
1772
2154
  z-index: 99999 !important;
2155
+ background: white !important;
2156
+ border: 1px solid #e2e8f0 !important;
2157
+ border-radius: 8px !important;
2158
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.12) !important;
2159
+ padding: 4px !important;
1773
2160
  }
1774
2161
 
1775
- /* Ensure popovers are visible above Strapi modals */
1776
- .ce-popover,
1777
- .ce-popover--opened {
2162
+ .ce-popover--inline .ce-popover__container {
2163
+ display: block !important;
2164
+ }
2165
+
2166
+ /* Items container - HORIZONTAL layout for inline tools */
2167
+ .ce-popover--inline .ce-popover__items {
2168
+ display: flex !important;
2169
+ flex-direction: row !important;
2170
+ flex-wrap: wrap !important;
2171
+ align-items: center !important;
2172
+ gap: 2px !important;
2173
+ opacity: 1 !important;
2174
+ visibility: visible !important;
2175
+ }
2176
+
2177
+ /* Custom HTML wrapper for inline tools */
2178
+ .ce-popover--inline .ce-popover-item-html {
2179
+ display: flex !important;
2180
+ opacity: 1 !important;
2181
+ visibility: visible !important;
2182
+ }
2183
+
2184
+ /* The actual inline tool buttons (Bold, Italic, etc.) */
2185
+ .ce-popover--inline .ce-inline-tool {
2186
+ display: flex !important;
2187
+ align-items: center !important;
2188
+ justify-content: center !important;
2189
+ width: 32px !important;
2190
+ height: 32px !important;
2191
+ opacity: 1 !important;
2192
+ visibility: visible !important;
2193
+ background: transparent !important;
2194
+ border: none !important;
2195
+ border-radius: 6px !important;
2196
+ cursor: pointer !important;
2197
+ color: #64748b !important;
2198
+ transition: background 0.15s ease, color 0.15s ease !important;
2199
+ }
2200
+
2201
+ .ce-popover--inline .ce-inline-tool:hover {
2202
+ background: #f1f5f9 !important;
2203
+ color: #334155 !important;
2204
+ }
2205
+
2206
+ .ce-popover--inline .ce-inline-tool--active {
2207
+ background: #ede9fe !important;
2208
+ color: #7C3AED !important;
2209
+ }
2210
+
2211
+ .ce-popover--inline .ce-inline-tool svg {
2212
+ width: 18px !important;
2213
+ height: 18px !important;
2214
+ }
2215
+
2216
+ /* Convert-to button (block type changer) */
2217
+ .ce-popover--inline .ce-popover-item[data-item-name="convert-to"] {
2218
+ display: flex !important;
2219
+ align-items: center !important;
2220
+ padding: 4px 8px !important;
2221
+ border-radius: 6px !important;
2222
+ cursor: pointer !important;
2223
+ }
2224
+
2225
+ .ce-popover--inline .ce-popover-item[data-item-name="convert-to"]:hover {
2226
+ background: #f1f5f9 !important;
2227
+ }
2228
+
2229
+ /* Separator line between convert-to and inline tools */
2230
+ .ce-popover--inline .ce-popover-item-separator {
2231
+ width: 1px !important;
2232
+ height: 24px !important;
2233
+ background: #e2e8f0 !important;
2234
+ margin: 0 4px !important;
2235
+ }
2236
+
2237
+ .ce-popover--inline .ce-popover-item-separator__line {
2238
+ display: none !important;
2239
+ }
2240
+
2241
+ /* ============================================
2242
+ GLOBAL Z-INDEX FOR ALL EDITOR POPOVERS
2243
+ ============================================ */
2244
+
2245
+ body > .ce-popover,
2246
+ body > .ce-inline-toolbar,
2247
+ .ce-popover--opened,
2248
+ .ce-inline-toolbar,
2249
+ .ce-settings,
2250
+ .ce-conversion-toolbar {
1778
2251
  z-index: 99999 !important;
1779
2252
  }
1780
2253
 
@@ -1820,11 +2293,14 @@ const EditorJSGlobalStyles = styled.createGlobalStyle`
1820
2293
 
1821
2294
  /* ============================================
1822
2295
  STRAPI MEDIA LIBRARY - Higher z-index for fullscreen
2296
+ Must be higher than fullscreen z-index (9999)
1823
2297
  ============================================ */
1824
2298
  [data-react-portal],
1825
2299
  .ReactModalPortal,
1826
2300
  [role="dialog"],
1827
2301
  [data-strapi-modal="true"],
2302
+ [class*="Dialog"],
2303
+ [class*="Modal"],
1828
2304
  .upload-dialog,
1829
2305
  [class*="Modal"],
1830
2306
  [class*="modal"],
@@ -2473,15 +2949,36 @@ const EditorContent = styled__default.default.div`
2473
2949
  flex: 1;
2474
2950
  overflow: visible; /* Allow toolbars/popovers to escape */
2475
2951
  position: relative;
2476
- padding: 24px 24px 24px 16px; /* Less left padding since toolbar has its own space */
2952
+ padding: 24px;
2477
2953
  min-height: 200px;
2478
2954
 
2479
2955
  ${(props) => props.$isFullscreen && styled.css`
2480
- padding: clamp(32px, 4vw, 60px);
2956
+ padding: clamp(24px, 3vw, 48px);
2481
2957
  width: 100%;
2482
2958
  max-width: 100%;
2483
2959
  margin: 0;
2484
2960
  align-self: stretch;
2961
+
2962
+ /* Make blocks stretch full width in fullscreen */
2963
+ .codex-editor {
2964
+ width: 100%;
2965
+ }
2966
+
2967
+ .ce-block__content,
2968
+ .ce-toolbar__content {
2969
+ max-width: 100% !important;
2970
+ padding: 0 !important;
2971
+ }
2972
+
2973
+ .ce-toolbar {
2974
+ max-width: 100% !important;
2975
+ left: 0 !important;
2976
+ transform: none !important;
2977
+ }
2978
+
2979
+ .ce-toolbar__actions {
2980
+ right: 0 !important;
2981
+ }
2485
2982
  `}
2486
2983
  `;
2487
2984
  const EditorWrapper = styled__default.default.div`
@@ -2556,18 +3053,29 @@ const EditorWrapper = styled__default.default.div`
2556
3053
  TOOLBAR INSIDE EDITOR - Position Fix
2557
3054
  ============================================ */
2558
3055
 
2559
- /* Make the redactor (content area) have left padding for toolbar */
3056
+ /* Content area - full container width */
2560
3057
  .codex-editor__redactor {
2561
3058
  padding-bottom: 100px !important;
2562
- padding-left: 50px !important; /* Space for toolbar */
2563
- margin-left: 0 !important;
3059
+ padding-left: 0 !important;
3060
+ padding-right: 0 !important;
3061
+ margin: 0 !important;
3062
+ max-width: 100% !important;
3063
+ width: 100% !important;
2564
3064
  }
2565
3065
 
2566
- /* Content blocks - full width within padded area */
3066
+ /* Content blocks - full width, no centering */
2567
3067
  .ce-block__content {
2568
- max-width: 100%;
2569
- margin-left: 0;
2570
- margin-right: 0;
3068
+ max-width: 100% !important;
3069
+ margin: 0 !important;
3070
+ padding: 0 16px !important;
3071
+ }
3072
+
3073
+ /* Paragraph and other editable elements - full width */
3074
+ .ce-paragraph,
3075
+ .ce-header,
3076
+ .cdx-block {
3077
+ max-width: 100% !important;
3078
+ width: 100% !important;
2571
3079
  }
2572
3080
 
2573
3081
  /* ============================================
@@ -2616,24 +3124,29 @@ const EditorWrapper = styled__default.default.div`
2616
3124
  border-radius: 6px;
2617
3125
  }
2618
3126
 
2619
- /* Toolbar positioning - inside the editor */
3127
+ /* Toolbar positioning - full width */
2620
3128
  .ce-toolbar__content {
2621
- max-width: 100%;
2622
- margin-left: 0;
3129
+ max-width: 100% !important;
3130
+ margin: 0 !important;
3131
+ padding: 0 16px !important;
2623
3132
  }
2624
3133
 
2625
3134
  .ce-toolbar {
2626
3135
  left: 0 !important;
3136
+ right: 0 !important;
3137
+ transform: none !important;
3138
+ width: 100% !important;
3139
+ max-width: 100% !important;
3140
+ padding-left: 8px !important;
2627
3141
  }
2628
3142
 
2629
3143
  .ce-toolbar__plus {
2630
- left: 0 !important;
2631
3144
  position: relative !important;
2632
3145
  }
2633
3146
 
2634
3147
  .ce-toolbar__actions {
2635
- right: 0 !important;
2636
3148
  position: absolute !important;
3149
+ right: 16px !important;
2637
3150
  }
2638
3151
 
2639
3152
  /* Settings button (⋮⋮) */
@@ -3119,6 +3632,35 @@ const FooterButton = styled__default.default.button`
3119
3632
  }
3120
3633
  }
3121
3634
  `;
3635
+ const WebtoolsPromoLink = styled__default.default.a`
3636
+ display: inline-flex;
3637
+ align-items: center;
3638
+ gap: 6px;
3639
+ padding: 4px 10px;
3640
+ background: linear-gradient(135deg, rgba(99, 102, 241, 0.08) 0%, rgba(139, 92, 246, 0.08) 100%);
3641
+ border: 1px solid rgba(99, 102, 241, 0.2);
3642
+ border-radius: 6px;
3643
+ font-size: 11px;
3644
+ color: #6366f1;
3645
+ text-decoration: none;
3646
+ transition: all 0.2s ease;
3647
+ white-space: nowrap;
3648
+
3649
+ svg {
3650
+ width: 12px;
3651
+ height: 12px;
3652
+ }
3653
+
3654
+ &:hover {
3655
+ background: linear-gradient(135deg, rgba(99, 102, 241, 0.15) 0%, rgba(139, 92, 246, 0.15) 100%);
3656
+ border-color: rgba(99, 102, 241, 0.4);
3657
+ transform: translateY(-1px);
3658
+ }
3659
+
3660
+ @media (max-width: 768px) {
3661
+ display: none;
3662
+ }
3663
+ `;
3122
3664
  const LoadingOverlay = styled__default.default.div`
3123
3665
  position: absolute;
3124
3666
  top: 0;
@@ -3133,6 +3675,18 @@ const LoadingOverlay = styled__default.default.div`
3133
3675
  gap: 12px;
3134
3676
  z-index: 10;
3135
3677
  `;
3678
+ const VersionHistoryOverlay = styled__default.default.div`
3679
+ position: fixed;
3680
+ top: 0;
3681
+ left: 0;
3682
+ right: 0;
3683
+ bottom: 0;
3684
+ background: rgba(0, 0, 0, 0.5);
3685
+ display: flex;
3686
+ align-items: center;
3687
+ justify-content: center;
3688
+ z-index: 99999;
3689
+ `;
3136
3690
  const LoadingText = styled__default.default.span`
3137
3691
  font-size: 13px;
3138
3692
  color: #64748b;
@@ -3348,10 +3902,12 @@ const Editor = React.forwardRef(({
3348
3902
  }, ref) => {
3349
3903
  const { formatMessage } = getTranslation.useIntl();
3350
3904
  const t = (id, defaultMessage) => formatMessage({ id: getTranslation.getTranslation(id), defaultMessage });
3351
- const { licenseData } = useLicense();
3905
+ const { licenseData, tier: licenseTier } = useLicense();
3906
+ const { isAvailable: isWebtoolsAvailable, openLinkPicker: webtoolsOpenLinkPicker } = useWebtoolsLinks();
3352
3907
  const editorRef = React.useRef(null);
3353
3908
  const editorInstanceRef = React.useRef(null);
3354
3909
  const containerRef = React.useRef(null);
3910
+ const webtoolsSelectionRef = React.useRef({ text: "", range: null, blockIndex: -1, existingAnchor: null, existingHref: "" });
3355
3911
  const isReadyRef = React.useRef(false);
3356
3912
  const [isReady, setIsReady] = React.useState(false);
3357
3913
  const [showCreditsModal, setShowCreditsModal] = React.useState(false);
@@ -3387,6 +3943,51 @@ const Editor = React.forwardRef(({
3387
3943
  const [aiSelectedText, setAISelectedText] = React.useState("");
3388
3944
  const aiSelectionRangeRef = React.useRef(null);
3389
3945
  const [aiLoading, setAILoading] = React.useState(false);
3946
+ const [showVersionHistory, setShowVersionHistory] = React.useState(false);
3947
+ React.useEffect(() => {
3948
+ if (!isWebtoolsAvailable || !editorRef.current) return;
3949
+ const updateWebtoolsSelection = () => {
3950
+ const selection = window.getSelection();
3951
+ if (!selection || selection.rangeCount === 0) return;
3952
+ const range = selection.getRangeAt(0);
3953
+ if (!editorRef.current.contains(range.commonAncestorContainer)) return;
3954
+ const selectedText = selection.toString().trim();
3955
+ let existingAnchor = null;
3956
+ let existingHref = "";
3957
+ let node = range.commonAncestorContainer;
3958
+ while (node && node !== editorRef.current) {
3959
+ if (node.nodeName === "A") {
3960
+ existingAnchor = node;
3961
+ existingHref = node.href || "";
3962
+ break;
3963
+ }
3964
+ node = node.parentNode;
3965
+ }
3966
+ if (!existingAnchor) {
3967
+ node = range.startContainer;
3968
+ while (node && node !== editorRef.current) {
3969
+ if (node.nodeName === "A") {
3970
+ existingAnchor = node;
3971
+ existingHref = node.href || "";
3972
+ break;
3973
+ }
3974
+ node = node.parentNode;
3975
+ }
3976
+ }
3977
+ const blockIndex = editorInstanceRef.current?.blocks?.getCurrentBlockIndex?.() ?? -1;
3978
+ webtoolsSelectionRef.current = {
3979
+ text: existingAnchor ? existingAnchor.textContent : selectedText,
3980
+ range: range.cloneRange(),
3981
+ blockIndex,
3982
+ existingAnchor,
3983
+ existingHref
3984
+ };
3985
+ };
3986
+ document.addEventListener("selectionchange", updateWebtoolsSelection);
3987
+ return () => {
3988
+ document.removeEventListener("selectionchange", updateWebtoolsSelection);
3989
+ };
3990
+ }, [isWebtoolsAvailable, isReady]);
3390
3991
  const serializedInitialValue = React.useMemo(() => {
3391
3992
  if (!value) {
3392
3993
  return "";
@@ -3414,12 +4015,25 @@ const Editor = React.forwardRef(({
3414
4015
  const {
3415
4016
  doc: yDoc,
3416
4017
  blocksMap: yBlocksMap,
4018
+ textMap: yTextMap,
4019
+ // NEW: Y.Map<blockId, Y.Text> for character-level sync
3417
4020
  metaMap: yMetaMap,
4021
+ // Character-level text helpers
4022
+ getBlockText,
4023
+ // NEW: Get Y.Text for a block
4024
+ setBlockText,
4025
+ // NEW: Get block text as HTML
4026
+ // Utility functions
4027
+ htmlToDelta: collabHtmlToDelta,
4028
+ deltaToHtml: collabDeltaToHtml,
4029
+ // Connection status
3418
4030
  status: collabStatus,
3419
4031
  error: collabError,
4032
+ // Collaboration
3420
4033
  peers: collabPeers,
3421
4034
  awareness: collabAwareness,
3422
4035
  emitAwareness,
4036
+ // Role-based access control
3423
4037
  collabRole,
3424
4038
  canEdit: collabCanEdit
3425
4039
  } = useMagicCollaboration({
@@ -3434,6 +4048,172 @@ const Editor = React.forwardRef(({
3434
4048
  }
3435
4049
  }
3436
4050
  });
4051
+ const yTextBindingsRef = React.useRef(/* @__PURE__ */ new Map());
4052
+ const bindBlockToYText = React.useCallback((blockId, element) => {
4053
+ if (!collabEnabled || !blockId || !element || !yTextMap) return;
4054
+ if (yTextBindingsRef.current.has(blockId)) {
4055
+ const existing = yTextBindingsRef.current.get(blockId);
4056
+ if (existing.element === element) return;
4057
+ unbindBlockFromYText(blockId);
4058
+ }
4059
+ const ytext = getBlockText(blockId);
4060
+ if (!ytext) return;
4061
+ let isUpdating = false;
4062
+ const ytextObserver = (event) => {
4063
+ if (isUpdating) return;
4064
+ if (event.transaction.local) return;
4065
+ isUpdating = true;
4066
+ try {
4067
+ const selection = window.getSelection();
4068
+ let cursorOffset = 0;
4069
+ if (selection && selection.rangeCount > 0 && element.contains(selection.anchorNode)) {
4070
+ const range = selection.getRangeAt(0);
4071
+ const preCaretRange = document.createRange();
4072
+ preCaretRange.selectNodeContents(element);
4073
+ preCaretRange.setEnd(range.startContainer, range.startOffset);
4074
+ cursorOffset = preCaretRange.toString().length;
4075
+ }
4076
+ const html = collabDeltaToHtml(ytext.toDelta());
4077
+ if (element.innerHTML !== html) {
4078
+ element.innerHTML = html || "";
4079
+ }
4080
+ if (document.activeElement === element && cursorOffset > 0) {
4081
+ try {
4082
+ const textNode = element.firstChild;
4083
+ if (textNode && textNode.nodeType === Node.TEXT_NODE) {
4084
+ const newRange = document.createRange();
4085
+ const pos = Math.min(cursorOffset, textNode.length);
4086
+ newRange.setStart(textNode, pos);
4087
+ newRange.setEnd(textNode, pos);
4088
+ selection.removeAllRanges();
4089
+ selection.addRange(newRange);
4090
+ }
4091
+ } catch (e) {
4092
+ }
4093
+ }
4094
+ } finally {
4095
+ isUpdating = false;
4096
+ }
4097
+ };
4098
+ const inputHandler = () => {
4099
+ if (isUpdating) return;
4100
+ isUpdating = true;
4101
+ try {
4102
+ const html = element.innerHTML;
4103
+ const newDelta = collabHtmlToDelta(html);
4104
+ const currentDelta = ytext.toDelta();
4105
+ yDoc.transact(() => {
4106
+ if (ytext.length > 0) {
4107
+ ytext.delete(0, ytext.length);
4108
+ }
4109
+ if (newDelta.length > 0) {
4110
+ ytext.applyDelta(newDelta);
4111
+ }
4112
+ }, "local");
4113
+ } finally {
4114
+ isUpdating = false;
4115
+ }
4116
+ };
4117
+ ytext.observe(ytextObserver);
4118
+ element.addEventListener("input", inputHandler);
4119
+ yTextBindingsRef.current.set(blockId, {
4120
+ ytext,
4121
+ element,
4122
+ ytextObserver,
4123
+ inputHandler
4124
+ });
4125
+ console.log("[Magic Editor X] [CHAR-SYNC] Bound block to Y.Text:", blockId);
4126
+ }, [collabEnabled, yTextMap, yDoc, getBlockText, collabHtmlToDelta, collabDeltaToHtml]);
4127
+ const unbindBlockFromYText = React.useCallback((blockId) => {
4128
+ const binding = yTextBindingsRef.current.get(blockId);
4129
+ if (!binding) return;
4130
+ binding.ytext.unobserve(binding.ytextObserver);
4131
+ binding.element.removeEventListener("input", binding.inputHandler);
4132
+ yTextBindingsRef.current.delete(blockId);
4133
+ console.log("[Magic Editor X] [CHAR-SYNC] Unbound block from Y.Text:", blockId);
4134
+ }, []);
4135
+ const bindAllBlocksToYText = React.useCallback(() => {
4136
+ if (!collabEnabled || !editorInstanceRef.current || !yTextMap) return;
4137
+ const editor = editorInstanceRef.current;
4138
+ if (!editor.blocks || typeof editor.blocks.getBlocksCount !== "function") return;
4139
+ const blockCount = editor.blocks.getBlocksCount();
4140
+ console.log("[Magic Editor X] [CHAR-SYNC] Binding", blockCount, "blocks to Y.Text");
4141
+ for (let i = 0; i < blockCount; i++) {
4142
+ try {
4143
+ const block = editor.blocks.getBlockByIndex(i);
4144
+ if (!block || !block.id) continue;
4145
+ const blockHolder = block.holder;
4146
+ if (!blockHolder) continue;
4147
+ const contentEditable = blockHolder.querySelector('[contenteditable="true"]');
4148
+ if (contentEditable) {
4149
+ const ytext = getBlockText(block.id);
4150
+ if (ytext && ytext.length === 0) {
4151
+ const currentHtml = contentEditable.innerHTML;
4152
+ if (currentHtml && currentHtml !== "<br>") {
4153
+ setBlockText(block.id, currentHtml);
4154
+ }
4155
+ }
4156
+ bindBlockToYText(block.id, contentEditable);
4157
+ }
4158
+ } catch (e) {
4159
+ console.warn("[Magic Editor X] [CHAR-SYNC] Error binding block:", e);
4160
+ }
4161
+ }
4162
+ }, [collabEnabled, yTextMap, getBlockText, setBlockText, bindBlockToYText]);
4163
+ const blockObserverRef = React.useRef(null);
4164
+ React.useEffect(() => {
4165
+ if (!collabEnabled || !editorRef.current || !yTextMap) return;
4166
+ const observer = new MutationObserver((mutations) => {
4167
+ let hasNewBlocks = false;
4168
+ for (const mutation of mutations) {
4169
+ if (mutation.type === "childList" && mutation.addedNodes.length > 0) {
4170
+ for (const node of mutation.addedNodes) {
4171
+ if (node.nodeType === Node.ELEMENT_NODE && (node.classList?.contains("ce-block") || node.querySelector?.(".ce-block"))) {
4172
+ hasNewBlocks = true;
4173
+ break;
4174
+ }
4175
+ }
4176
+ }
4177
+ if (hasNewBlocks) break;
4178
+ }
4179
+ if (hasNewBlocks) {
4180
+ setTimeout(() => {
4181
+ bindAllBlocksToYText();
4182
+ }, 50);
4183
+ }
4184
+ });
4185
+ observer.observe(editorRef.current, {
4186
+ childList: true,
4187
+ subtree: true
4188
+ });
4189
+ blockObserverRef.current = observer;
4190
+ return () => {
4191
+ observer.disconnect();
4192
+ blockObserverRef.current = null;
4193
+ };
4194
+ }, [collabEnabled, yTextMap, bindAllBlocksToYText]);
4195
+ React.useEffect(() => {
4196
+ return () => {
4197
+ yTextBindingsRef.current.forEach((binding, blockId) => {
4198
+ binding.ytext.unobserve(binding.ytextObserver);
4199
+ binding.element.removeEventListener("input", binding.inputHandler);
4200
+ });
4201
+ yTextBindingsRef.current.clear();
4202
+ };
4203
+ }, []);
4204
+ const {
4205
+ snapshots,
4206
+ loading: versionHistoryLoading,
4207
+ error: versionHistoryError,
4208
+ fetchSnapshots,
4209
+ restoreSnapshot,
4210
+ createSnapshot
4211
+ } = useVersionHistory();
4212
+ React.useEffect(() => {
4213
+ if (showVersionHistory && collabRoomId) {
4214
+ fetchSnapshots(collabRoomId);
4215
+ }
4216
+ }, [showVersionHistory, collabRoomId, fetchSnapshots]);
3437
4217
  React.useMemo(() => {
3438
4218
  switch (collabRole) {
3439
4219
  case "viewer":
@@ -3767,23 +4547,20 @@ const Editor = React.forwardRef(({
3767
4547
  setWordCount(plainText.split(/\s+/).filter((w) => w.length > 0).length);
3768
4548
  }, []);
3769
4549
  const renderFromYDoc = React.useCallback(async () => {
3770
- fetch("http://127.0.0.1:7242/ingest/12c1170b-f275-4a73-9ee7-3006bf7f0881", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ location: "EditorJS/index.jsx:renderFromYDoc:entry", message: "renderFromYDoc called", data: { collabEnabled, hasYBlocksMap: !!yBlocksMap, hasYDoc: !!yDoc, hasYMetaMap: !!yMetaMap }, timestamp: Date.now(), sessionId: "debug-session", hypothesisId: "A" }) }).catch(() => {
3771
- });
3772
4550
  if (!collabEnabled || !yBlocksMap || !yDoc) {
3773
- fetch("http://127.0.0.1:7242/ingest/12c1170b-f275-4a73-9ee7-3006bf7f0881", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ location: "EditorJS/index.jsx:renderFromYDoc:guard1", message: "EARLY EXIT: missing collab deps", data: { collabEnabled, hasYBlocksMap: !!yBlocksMap, hasYDoc: !!yDoc }, timestamp: Date.now(), sessionId: "debug-session", hypothesisId: "A" }) }).catch(() => {
3774
- });
3775
4551
  return;
3776
4552
  }
3777
4553
  const editor = editorInstanceRef.current;
3778
4554
  if (!editor || !isReadyRef.current) {
3779
- fetch("http://127.0.0.1:7242/ingest/12c1170b-f275-4a73-9ee7-3006bf7f0881", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ location: "EditorJS/index.jsx:renderFromYDoc:guard2", message: "EARLY EXIT: editor not ready", data: { hasEditor: !!editor, isReady: isReadyRef.current }, timestamp: Date.now(), sessionId: "debug-session", hypothesisId: "D" }) }).catch(() => {
3780
- });
4555
+ pendingRenderRef.current = pendingRenderRef.current || true;
4556
+ return;
4557
+ }
4558
+ if (!editor.blocks || typeof editor.blocks.getBlocksCount !== "function") {
4559
+ console.warn("[Magic Editor X] Editor blocks API not ready for renderFromYDoc");
3781
4560
  pendingRenderRef.current = pendingRenderRef.current || true;
3782
4561
  return;
3783
4562
  }
3784
4563
  if (isApplyingRemoteRef.current) {
3785
- fetch("http://127.0.0.1:7242/ingest/12c1170b-f275-4a73-9ee7-3006bf7f0881", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ location: "EditorJS/index.jsx:renderFromYDoc:guard3", message: "EARLY EXIT: isApplyingRemote is true (race condition)", data: { isApplyingRemote: true }, timestamp: Date.now(), sessionId: "debug-session", hypothesisId: "B" }) }).catch(() => {
3786
- });
3787
4564
  return;
3788
4565
  }
3789
4566
  isApplyingRemoteRef.current = true;
@@ -3801,29 +4578,35 @@ const Editor = React.forwardRef(({
3801
4578
  yOrder = Array.from(yBlocksMap.keys());
3802
4579
  }
3803
4580
  const yBlocks = [];
3804
- fetch("http://127.0.0.1:7242/ingest/12c1170b-f275-4a73-9ee7-3006bf7f0881", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ location: "EditorJS/index.jsx:renderFromYDoc:yState", message: "Y.js state before parsing", data: { blockOrderFromMeta: !!blockOrderJson, yBlocksMapSize: yBlocksMap?.size, yOrder, yBlocksMapKeys: Array.from(yBlocksMap?.keys() || []) }, timestamp: Date.now(), sessionId: "debug-session", hypothesisId: "C" }) }).catch(() => {
3805
- });
3806
4581
  yOrder.forEach((id) => {
3807
4582
  const json = yBlocksMap.get(id);
3808
4583
  if (json) {
3809
4584
  try {
3810
- const block = JSON.parse(json);
3811
- yBlocks.push(block);
4585
+ const blockData = JSON.parse(json);
4586
+ if (blockData.type && !blockData.data) {
4587
+ const ytext = yTextMap?.get(id);
4588
+ const textContent = ytext ? collabDeltaToHtml(ytext.toDelta()) : "";
4589
+ yBlocks.push({
4590
+ id,
4591
+ type: blockData.type,
4592
+ data: { text: textContent },
4593
+ tunes: blockData.tunes || {}
4594
+ });
4595
+ } else if (blockData.type && blockData.data) {
4596
+ yBlocks.push({
4597
+ id,
4598
+ ...blockData
4599
+ });
4600
+ }
3812
4601
  } catch (e) {
3813
4602
  console.warn("[Magic Editor X] Invalid block JSON:", id);
3814
4603
  }
3815
4604
  }
3816
4605
  });
3817
- fetch("http://127.0.0.1:7242/ingest/12c1170b-f275-4a73-9ee7-3006bf7f0881", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ location: "EditorJS/index.jsx:renderFromYDoc:parsed", message: "Parsed yBlocks from Y.Map", data: { yBlocksCount: yBlocks.length, yBlockIds: yBlocks.map((b) => b.id), yBlockTypes: yBlocks.map((b) => b.type) }, timestamp: Date.now(), sessionId: "debug-session", hypothesisId: "A" }) }).catch(() => {
3818
- });
3819
4606
  const parsed = { blocks: yBlocks };
3820
4607
  const normalizedParsed = serializeForCompare(parsed);
3821
4608
  const renderFull = async () => {
3822
- fetch("http://127.0.0.1:7242/ingest/12c1170b-f275-4a73-9ee7-3006bf7f0881", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ location: "EditorJS/index.jsx:renderFull:start", message: "FULL RENDER triggered", data: { blocksToRender: yBlocks.length, blockIds: yBlocks.map((b) => b.id) }, timestamp: Date.now(), sessionId: "debug-session", hypothesisId: "E" }) }).catch(() => {
3823
- });
3824
4609
  await editor.render(parsed);
3825
- fetch("http://127.0.0.1:7242/ingest/12c1170b-f275-4a73-9ee7-3006bf7f0881", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ location: "EditorJS/index.jsx:renderFull:done", message: "FULL RENDER completed", data: { newBlockCount: editor.blocks.getBlocksCount() }, timestamp: Date.now(), sessionId: "debug-session", hypothesisId: "E" }) }).catch(() => {
3826
- });
3827
4610
  lastSerializedValueRef.current = normalizedParsed;
3828
4611
  setBlocksCount(yBlocks.length);
3829
4612
  calculateStats(parsed);
@@ -3836,8 +4619,6 @@ const Editor = React.forwardRef(({
3836
4619
  currentBlocks.push({ id: block.id, index: i });
3837
4620
  }
3838
4621
  }
3839
- fetch("http://127.0.0.1:7242/ingest/12c1170b-f275-4a73-9ee7-3006bf7f0881", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ location: "EditorJS/index.jsx:renderFromYDoc:comparison", message: "Comparing editor vs Y.js state", data: { editorBlockCount: blockCount, yBlocksCount: yBlocks.length, editorBlockIds: currentBlocks.map((b) => b.id), yBlockIds: yBlocks.map((b) => b.id) }, timestamp: Date.now(), sessionId: "debug-session", hypothesisId: "E" }) }).catch(() => {
3840
- });
3841
4622
  if (blockCount !== yBlocks.length) {
3842
4623
  console.log("[Magic Editor X] [SYNC] Structural change detected (count mismatch). Falling back to full render.");
3843
4624
  await renderFull();
@@ -4031,6 +4812,7 @@ const Editor = React.forwardRef(({
4031
4812
  return;
4032
4813
  }
4033
4814
  const editor = editorInstanceRef.current;
4815
+ if (!editor.blocks || typeof editor.blocks.getBlocksCount !== "function") return;
4034
4816
  const lastIndex = editor.blocks.getBlocksCount();
4035
4817
  editor.blocks.insert(blockType, {}, {}, lastIndex, true);
4036
4818
  editor.caret.setToBlock(lastIndex);
@@ -4042,7 +4824,7 @@ const Editor = React.forwardRef(({
4042
4824
  const emptyPayload = JSON.stringify({ blocks: [] });
4043
4825
  lastSerializedValueRef.current = emptyPayload;
4044
4826
  pushLocalToCollab(emptyPayload);
4045
- onChange({ target: { name, value: null, type: "text" } });
4827
+ onChange({ target: { name, value: null, type: "json" } });
4046
4828
  setBlocksCount(0);
4047
4829
  setWordCount(0);
4048
4830
  setCharCount(0);
@@ -4055,7 +4837,11 @@ const Editor = React.forwardRef(({
4055
4837
  }, [isReady]);
4056
4838
  React.useEffect(() => {
4057
4839
  if (editorRef.current && !editorInstanceRef.current) {
4058
- const tools$1 = tools.getTools({ mediaLibToggleFunc, pluginId: index.PLUGIN_ID });
4840
+ const tools$1 = tools.getTools({
4841
+ mediaLibToggleFunc,
4842
+ pluginId: index.PLUGIN_ID,
4843
+ openLinkPicker: isWebtoolsAvailable ? webtoolsOpenLinkPicker : null
4844
+ });
4059
4845
  let initialData = void 0;
4060
4846
  if (value) {
4061
4847
  try {
@@ -4074,6 +4860,18 @@ const Editor = React.forwardRef(({
4074
4860
  editorRef.current.classList.add("editor-readonly");
4075
4861
  }
4076
4862
  }
4863
+ console.log("[Magic Editor X] Registered tools:", Object.keys(tools$1));
4864
+ const inlineTools = Object.entries(tools$1).filter(([name2, config]) => {
4865
+ const toolClass = config.class || config;
4866
+ const isInline = toolClass?.isInline === true;
4867
+ if (isInline) {
4868
+ console.log(`[Magic Editor X] Found inline tool: ${name2}`, toolClass);
4869
+ }
4870
+ return isInline;
4871
+ }).map(([name2]) => name2);
4872
+ console.log("[Magic Editor X] Inline tools found:", inlineTools);
4873
+ console.log("[Magic Editor X] Marker isInline:", tools$1.marker?.class?.isInline);
4874
+ console.log("[Magic Editor X] Bold isInline:", tools$1.bold?.class?.isInline);
4077
4875
  const editor = new EditorJS__default.default({
4078
4876
  holder: editorRef.current,
4079
4877
  tools: tools$1,
@@ -4082,23 +4880,35 @@ const Editor = React.forwardRef(({
4082
4880
  placeholder: customPlaceholder,
4083
4881
  minHeight: 200,
4084
4882
  autofocus: false,
4883
+ // Note: Do NOT set inlineToolbar here - each block tool controls its own inline toolbar
4884
+ // The inline tools (bold, italic, marker, etc.) are automatically available when block tools have inlineToolbar: true
4085
4885
  onReady: async () => {
4086
4886
  isReadyRef.current = true;
4087
4887
  setIsReady(true);
4088
4888
  console.log("[Magic Editor X] [READY] Editor onReady fired");
4089
4889
  console.log("[Magic Editor X] [READY] Editor holder:", editorRef.current?.id);
4090
- try {
4091
- tools.initUndoRedo(editor);
4092
- console.log("[Magic Editor X] [SUCCESS] Undo/Redo initialized");
4093
- } catch (e) {
4094
- console.warn("[Magic Editor X] Could not initialize Undo/Redo:", e);
4095
- }
4096
- try {
4097
- tools.initDragDrop(editor);
4098
- console.log("[Magic Editor X] [SUCCESS] Drag & Drop initialized");
4099
- } catch (e) {
4100
- console.warn("[Magic Editor X] Could not initialize Drag & Drop:", e);
4101
- }
4890
+ setTimeout(() => {
4891
+ try {
4892
+ if (editor && editor.blocks && typeof editor.blocks.getBlocksCount === "function") {
4893
+ tools.initUndoRedo(editor);
4894
+ console.log("[Magic Editor X] [SUCCESS] Undo/Redo initialized");
4895
+ } else {
4896
+ console.warn("[Magic Editor X] Editor blocks API not ready for Undo/Redo");
4897
+ }
4898
+ } catch (e) {
4899
+ console.warn("[Magic Editor X] Could not initialize Undo/Redo:", e);
4900
+ }
4901
+ try {
4902
+ if (editor && editor.blocks && typeof editor.blocks.getBlocksCount === "function") {
4903
+ tools.initDragDrop(editor);
4904
+ console.log("[Magic Editor X] [SUCCESS] Drag & Drop initialized");
4905
+ } else {
4906
+ console.warn("[Magic Editor X] Editor blocks API not ready for Drag & Drop");
4907
+ }
4908
+ } catch (e) {
4909
+ console.warn("[Magic Editor X] Could not initialize Drag & Drop:", e);
4910
+ }
4911
+ }, 500);
4102
4912
  if (pendingRenderRef.current) {
4103
4913
  try {
4104
4914
  if (typeof pendingRenderRef.current === "object" && pendingRenderRef.current.blocks) {
@@ -4121,6 +4931,52 @@ const Editor = React.forwardRef(({
4121
4931
  }
4122
4932
  pendingRenderRef.current = null;
4123
4933
  }
4934
+ if (collabEnabled && yTextMap) {
4935
+ setTimeout(() => {
4936
+ bindAllBlocksToYText();
4937
+ }, 100);
4938
+ }
4939
+ if (isWebtoolsAvailable && webtoolsOpenLinkPicker && editorRef.current) {
4940
+ const handleLinkClick = async (e) => {
4941
+ const anchor = e.target.closest("a");
4942
+ if (anchor && editorRef.current?.contains(anchor)) {
4943
+ e.preventDefault();
4944
+ e.stopPropagation();
4945
+ const existingHref = anchor.href || "";
4946
+ const existingText = anchor.textContent || "";
4947
+ console.log("[Magic Editor X] Link clicked, opening editor:", existingHref);
4948
+ try {
4949
+ const selection = window.getSelection();
4950
+ const range = document.createRange();
4951
+ range.selectNodeContents(anchor);
4952
+ selection.removeAllRanges();
4953
+ selection.addRange(range);
4954
+ const result = await webtoolsOpenLinkPicker({
4955
+ initialText: existingText,
4956
+ initialHref: existingHref
4957
+ });
4958
+ if (result && result.href) {
4959
+ anchor.href = result.href;
4960
+ if (result.label && result.label !== existingText) {
4961
+ anchor.textContent = result.label;
4962
+ }
4963
+ console.log("[Magic Editor X] Link updated:", result.href);
4964
+ } else if (result === null) {
4965
+ console.log("[Magic Editor X] Link edit cancelled");
4966
+ }
4967
+ } catch (err) {
4968
+ console.error("[Magic Editor X] Error editing link:", err);
4969
+ }
4970
+ }
4971
+ };
4972
+ editorRef.current.addEventListener("click", handleLinkClick);
4973
+ const cleanup = () => {
4974
+ editorRef.current?.removeEventListener("click", handleLinkClick);
4975
+ };
4976
+ if (!editorRef.current._linkClickCleanup) {
4977
+ editorRef.current._linkClickCleanup = cleanup;
4978
+ }
4979
+ }
4124
4980
  },
4125
4981
  onChange: async (api) => {
4126
4982
  try {
@@ -4140,9 +4996,9 @@ const Editor = React.forwardRef(({
4140
4996
  pushLocalToCollabRef.current?.(docPayload);
4141
4997
  lastSerializedValueRef.current = normalized;
4142
4998
  if (count === 0) {
4143
- onChange({ target: { name, value: null, type: "text" } });
4999
+ onChange({ target: { name, value: null, type: "json" } });
4144
5000
  } else {
4145
- onChange({ target: { name, value: serialized, type: "text" } });
5001
+ onChange({ target: { name, value: outputData, type: "json" } });
4146
5002
  }
4147
5003
  } catch (error2) {
4148
5004
  console.error("[Magic Editor X] Error in onChange:", error2);
@@ -4155,6 +5011,10 @@ const Editor = React.forwardRef(({
4155
5011
  console.log("[Magic Editor X] [CLEANUP] Editor component unmounting, destroying editor");
4156
5012
  isReadyRef.current = false;
4157
5013
  setIsReady(false);
5014
+ if (editorRef.current?._linkClickCleanup) {
5015
+ editorRef.current._linkClickCleanup();
5016
+ delete editorRef.current._linkClickCleanup;
5017
+ }
4158
5018
  if (editorInstanceRef.current && editorInstanceRef.current.destroy) {
4159
5019
  try {
4160
5020
  editorInstanceRef.current.destroy();
@@ -4265,6 +5125,96 @@ const Editor = React.forwardRef(({
4265
5125
  children: /* @__PURE__ */ jsxRuntime.jsx(outline.SparklesIcon, {})
4266
5126
  }
4267
5127
  ),
5128
+ isWebtoolsAvailable && /* @__PURE__ */ jsxRuntime.jsx(
5129
+ ToolButton,
5130
+ {
5131
+ type: "button",
5132
+ "data-tooltip": "Webtools Link Picker",
5133
+ onClick: async () => {
5134
+ if (!editorInstanceRef.current || !isReady) {
5135
+ console.warn("[Magic Editor X] Editor not ready");
5136
+ return;
5137
+ }
5138
+ const editor = editorInstanceRef.current;
5139
+ const {
5140
+ text: selectedText,
5141
+ range: savedRange,
5142
+ blockIndex,
5143
+ existingAnchor,
5144
+ existingHref
5145
+ } = webtoolsSelectionRef.current;
5146
+ console.log("[Magic Editor X] Webtools button clicked with stored selection:", {
5147
+ text: selectedText || "(none)",
5148
+ existingHref: existingHref || "(new link)",
5149
+ hasRange: !!savedRange,
5150
+ blockIndex
5151
+ });
5152
+ const currentBlockIndex = blockIndex >= 0 ? blockIndex : editor.blocks.getCurrentBlockIndex();
5153
+ const result = await webtoolsOpenLinkPicker({
5154
+ initialText: selectedText || "",
5155
+ initialHref: existingHref || ""
5156
+ });
5157
+ if (result && result.href) {
5158
+ const linkText = result.label || selectedText || result.href;
5159
+ const linkHtml = `<a href="${result.href}" target="_blank" rel="noopener noreferrer">${linkText}</a>`;
5160
+ if (existingAnchor && existingAnchor.parentNode) {
5161
+ try {
5162
+ existingAnchor.href = result.href;
5163
+ existingAnchor.textContent = linkText;
5164
+ const contentEditable = existingAnchor.closest('[contenteditable="true"]');
5165
+ if (contentEditable) {
5166
+ contentEditable.dispatchEvent(new Event("input", { bubbles: true }));
5167
+ }
5168
+ console.log("[Magic Editor X] Webtools link UPDATED:", {
5169
+ oldHref: existingHref,
5170
+ newHref: result.href,
5171
+ text: linkText
5172
+ });
5173
+ } catch (e) {
5174
+ console.error("[Magic Editor X] Failed to update link:", e);
5175
+ }
5176
+ } else if (savedRange && selectedText && currentBlockIndex >= 0) {
5177
+ try {
5178
+ const blockHolder = editor.blocks.getBlockByIndex(currentBlockIndex)?.holder;
5179
+ const contentEditable = blockHolder?.querySelector('[contenteditable="true"]');
5180
+ if (contentEditable) {
5181
+ const selection = window.getSelection();
5182
+ selection.removeAllRanges();
5183
+ selection.addRange(savedRange);
5184
+ document.execCommand("insertHTML", false, linkHtml);
5185
+ contentEditable.dispatchEvent(new Event("input", { bubbles: true }));
5186
+ console.log("[Magic Editor X] Webtools link CREATED:", {
5187
+ text: linkText,
5188
+ href: result.href
5189
+ });
5190
+ } else {
5191
+ editor.blocks.insert("paragraph", { text: linkHtml }, {}, currentBlockIndex + 1, true);
5192
+ }
5193
+ } catch (e) {
5194
+ console.error("[Magic Editor X] Failed to insert link:", e);
5195
+ editor.blocks.insert("paragraph", { text: linkHtml }, {}, currentBlockIndex + 1, true);
5196
+ }
5197
+ } else if (currentBlockIndex >= 0) {
5198
+ editor.blocks.insert("paragraph", {
5199
+ text: linkHtml
5200
+ }, {}, currentBlockIndex + 1, true);
5201
+ editor.caret.setToBlock(currentBlockIndex + 1);
5202
+ console.log("[Magic Editor X] Webtools link inserted (no selection):", result);
5203
+ } else {
5204
+ editor.blocks.insert("paragraph", { text: linkHtml });
5205
+ }
5206
+ }
5207
+ webtoolsSelectionRef.current = { text: "", range: null, blockIndex: -1, existingAnchor: null, existingHref: "" };
5208
+ },
5209
+ disabled: collabEnabled && collabCanEdit === false,
5210
+ style: {
5211
+ background: "linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)",
5212
+ color: "white",
5213
+ ...collabEnabled && collabCanEdit === false ? { opacity: 0.4, cursor: "not-allowed" } : {}
5214
+ },
5215
+ children: /* @__PURE__ */ jsxRuntime.jsx(outline.LinkIcon, {})
5216
+ }
5217
+ ),
4268
5218
  /* @__PURE__ */ jsxRuntime.jsx(ToolbarDivider, {}),
4269
5219
  /* @__PURE__ */ jsxRuntime.jsx(
4270
5220
  ToolButton,
@@ -4380,9 +5330,26 @@ const Editor = React.forwardRef(({
4380
5330
  /* @__PURE__ */ jsxRuntime.jsx("strong", { children: charCount }),
4381
5331
  " ",
4382
5332
  t("editor.characters", "Zeichen")
4383
- ] })
5333
+ ] }),
5334
+ !isWebtoolsAvailable && /* @__PURE__ */ jsxRuntime.jsxs(
5335
+ WebtoolsPromoLink,
5336
+ {
5337
+ href: "https://www.pluginpal.io/plugin/webtools",
5338
+ target: "_blank",
5339
+ rel: "noopener noreferrer",
5340
+ title: "Get Webtools Links addon for internal link management",
5341
+ children: [
5342
+ /* @__PURE__ */ jsxRuntime.jsx(outline.LinkIcon, {}),
5343
+ "Internal Links? Get Webtools"
5344
+ ]
5345
+ }
5346
+ )
4384
5347
  ] }),
4385
5348
  /* @__PURE__ */ jsxRuntime.jsxs(FooterRight, { children: [
5349
+ /* @__PURE__ */ jsxRuntime.jsxs(FooterButton, { type: "button", onClick: () => setShowVersionHistory(true), children: [
5350
+ /* @__PURE__ */ jsxRuntime.jsx(outline.ClockIcon, {}),
5351
+ t("editor.versionHistory", "History")
5352
+ ] }),
4386
5353
  !(collabEnabled && collabCanEdit === false) && /* @__PURE__ */ jsxRuntime.jsxs(FooterButton, { type: "button", onClick: () => handleInsertBlock("mediaLib"), children: [
4387
5354
  /* @__PURE__ */ jsxRuntime.jsx(outline.PhotoIcon, {}),
4388
5355
  t("editor.mediaLibrary", "Media Library")
@@ -4455,6 +5422,51 @@ const Editor = React.forwardRef(({
4455
5422
  }
4456
5423
  }
4457
5424
  ),
5425
+ showVersionHistory && /* @__PURE__ */ jsxRuntime.jsx(VersionHistoryOverlay, { onClick: () => setShowVersionHistory(false), children: /* @__PURE__ */ jsxRuntime.jsx("div", { onClick: (e) => e.stopPropagation(), children: /* @__PURE__ */ jsxRuntime.jsx(
5426
+ VersionHistoryPanel,
5427
+ {
5428
+ snapshots,
5429
+ loading: versionHistoryLoading,
5430
+ error: versionHistoryError,
5431
+ tier: licenseTier,
5432
+ onClose: () => setShowVersionHistory(false),
5433
+ onRestore: async (snapshot) => {
5434
+ if (snapshot.documentId && editorInstanceRef.current && isReady) {
5435
+ try {
5436
+ const result = await restoreSnapshot(snapshot.documentId, collabRoomId);
5437
+ const contentToRestore = result?.jsonContent || snapshot.jsonContent;
5438
+ if (contentToRestore && editorInstanceRef.current) {
5439
+ await editorInstanceRef.current.render(contentToRestore);
5440
+ setShowVersionHistory(false);
5441
+ onChange({ target: { name, value: contentToRestore, type: "json" } });
5442
+ }
5443
+ } catch (err) {
5444
+ console.error("[Magic Editor X] Failed to restore snapshot:", err?.message);
5445
+ }
5446
+ }
5447
+ },
5448
+ onCreate: async () => {
5449
+ if (collabRoomId && editorInstanceRef.current && isReady) {
5450
+ const [contentType, entryId, fieldName] = collabRoomId.split("|");
5451
+ if (contentType && entryId && fieldName) {
5452
+ try {
5453
+ const editorContent = await editorInstanceRef.current.save();
5454
+ await createSnapshot({
5455
+ roomId: collabRoomId,
5456
+ contentType,
5457
+ entryId,
5458
+ fieldName,
5459
+ content: editorContent
5460
+ });
5461
+ fetchSnapshots(collabRoomId);
5462
+ } catch (err) {
5463
+ console.error("[Magic Editor X] Failed to create snapshot:", err?.message);
5464
+ }
5465
+ }
5466
+ }
5467
+ }
5468
+ }
5469
+ ) }) }),
4458
5470
  /* @__PURE__ */ jsxRuntime.jsx(
4459
5471
  CreditsModal,
4460
5472
  {