magic-editor-x 1.1.0 → 1.2.2

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 +83 -6
  2. package/dist/_chunks/{App-mtrlABtd.js → App-BHNqY71z.js} +4 -4
  3. package/dist/_chunks/{App-B1FgOsWa.mjs → App-LgFoHtyD.mjs} +4 -4
  4. package/dist/_chunks/{LicensePage-BnyWSrWs.js → LicensePage-VwKQMnUO.js} +2 -2
  5. package/dist/_chunks/{LicensePage-CWH-AFR-.mjs → LicensePage-oxUnaZmr.mjs} +2 -2
  6. package/dist/_chunks/{LiveCollaborationPanel-DbDHwr2C.js → LiveCollaborationPanel-CqtkFWJs.js} +1 -1
  7. package/dist/_chunks/{LiveCollaborationPanel-ryjcDAA7.mjs → LiveCollaborationPanel-elejZRkh.mjs} +1 -1
  8. package/dist/_chunks/{Settings-Bk9bxJTy.js → Settings-4wUHMbn0.js} +1 -1
  9. package/dist/_chunks/{Settings-D-V2MLVm.mjs → Settings-BI9zxX3k.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-ChB_HlBd.mjs} +7 -6
  19. package/dist/_chunks/{getTranslation-D35vbDap.js → getTranslation-DxG1pB5q.js} +2 -1
  20. package/dist/_chunks/{index-BiLy_f7C.js → index-C_SiBh7v.js} +9 -9
  21. package/dist/_chunks/{index-B5MzUyo0.mjs → index-CtyxDZ0S.mjs} +9 -9
  22. package/dist/_chunks/{index-CQx7-dFP.js → index-DGRg45vZ.js} +890 -65
  23. package/dist/_chunks/{index-BRVqbnOb.mjs → index-IYdGq7Rl.mjs} +891 -66
  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-CjnQJ9w2.mjs → tools-BAvbiUHr.mjs} +6 -1
  27. package/dist/_chunks/{tools-DNt2tioN.js → tools-Dn4jPdJs.js} +6 -1
  28. package/dist/admin/index.js +1 -1
  29. package/dist/admin/index.mjs +1 -1
  30. package/dist/server/index.js +37244 -1293
  31. package/dist/server/index.mjs +37230 -1286
  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-DxG1pB5q.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-Dn4jPdJs.js");
10
10
  const admin = require("@strapi/strapi/admin");
11
- const index = require("./index-BiLy_f7C.js");
11
+ const index = require("./index-C_SiBh7v.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;
@@ -1820,11 +2194,14 @@ const EditorJSGlobalStyles = styled.createGlobalStyle`
1820
2194
 
1821
2195
  /* ============================================
1822
2196
  STRAPI MEDIA LIBRARY - Higher z-index for fullscreen
2197
+ Must be higher than fullscreen z-index (9999)
1823
2198
  ============================================ */
1824
2199
  [data-react-portal],
1825
2200
  .ReactModalPortal,
1826
2201
  [role="dialog"],
1827
2202
  [data-strapi-modal="true"],
2203
+ [class*="Dialog"],
2204
+ [class*="Modal"],
1828
2205
  .upload-dialog,
1829
2206
  [class*="Modal"],
1830
2207
  [class*="modal"],
@@ -2473,15 +2850,36 @@ const EditorContent = styled__default.default.div`
2473
2850
  flex: 1;
2474
2851
  overflow: visible; /* Allow toolbars/popovers to escape */
2475
2852
  position: relative;
2476
- padding: 24px 24px 24px 16px; /* Less left padding since toolbar has its own space */
2853
+ padding: 24px;
2477
2854
  min-height: 200px;
2478
2855
 
2479
2856
  ${(props) => props.$isFullscreen && styled.css`
2480
- padding: clamp(32px, 4vw, 60px);
2857
+ padding: clamp(24px, 3vw, 48px);
2481
2858
  width: 100%;
2482
2859
  max-width: 100%;
2483
2860
  margin: 0;
2484
2861
  align-self: stretch;
2862
+
2863
+ /* Make blocks stretch full width in fullscreen */
2864
+ .codex-editor {
2865
+ width: 100%;
2866
+ }
2867
+
2868
+ .ce-block__content,
2869
+ .ce-toolbar__content {
2870
+ max-width: 100% !important;
2871
+ padding: 0 !important;
2872
+ }
2873
+
2874
+ .ce-toolbar {
2875
+ max-width: 100% !important;
2876
+ left: 0 !important;
2877
+ transform: none !important;
2878
+ }
2879
+
2880
+ .ce-toolbar__actions {
2881
+ right: 0 !important;
2882
+ }
2485
2883
  `}
2486
2884
  `;
2487
2885
  const EditorWrapper = styled__default.default.div`
@@ -2556,18 +2954,19 @@ const EditorWrapper = styled__default.default.div`
2556
2954
  TOOLBAR INSIDE EDITOR - Position Fix
2557
2955
  ============================================ */
2558
2956
 
2559
- /* Make the redactor (content area) have left padding for toolbar */
2957
+ /* Centered content area */
2560
2958
  .codex-editor__redactor {
2561
2959
  padding-bottom: 100px !important;
2562
- padding-left: 50px !important; /* Space for toolbar */
2563
- margin-left: 0 !important;
2960
+ padding-left: 0 !important;
2961
+ margin: 0 auto !important;
2962
+ max-width: 800px !important;
2564
2963
  }
2565
2964
 
2566
- /* Content blocks - full width within padded area */
2965
+ /* Content blocks - centered */
2567
2966
  .ce-block__content {
2568
2967
  max-width: 100%;
2569
- margin-left: 0;
2570
- margin-right: 0;
2968
+ margin: 0 auto;
2969
+ padding: 0 16px;
2571
2970
  }
2572
2971
 
2573
2972
  /* ============================================
@@ -2616,24 +3015,27 @@ const EditorWrapper = styled__default.default.div`
2616
3015
  border-radius: 6px;
2617
3016
  }
2618
3017
 
2619
- /* Toolbar positioning - inside the editor */
3018
+ /* Toolbar positioning - centered with content */
2620
3019
  .ce-toolbar__content {
2621
- max-width: 100%;
2622
- margin-left: 0;
3020
+ max-width: 800px;
3021
+ margin: 0 auto;
3022
+ padding: 0 16px;
2623
3023
  }
2624
3024
 
2625
3025
  .ce-toolbar {
2626
- left: 0 !important;
3026
+ left: 50% !important;
3027
+ transform: translateX(-50%) !important;
3028
+ width: 100% !important;
3029
+ max-width: 832px !important;
2627
3030
  }
2628
3031
 
2629
3032
  .ce-toolbar__plus {
2630
- left: 0 !important;
2631
3033
  position: relative !important;
2632
3034
  }
2633
3035
 
2634
3036
  .ce-toolbar__actions {
2635
- right: 0 !important;
2636
3037
  position: absolute !important;
3038
+ right: 16px !important;
2637
3039
  }
2638
3040
 
2639
3041
  /* Settings button (⋮⋮) */
@@ -3119,6 +3521,35 @@ const FooterButton = styled__default.default.button`
3119
3521
  }
3120
3522
  }
3121
3523
  `;
3524
+ const WebtoolsPromoLink = styled__default.default.a`
3525
+ display: inline-flex;
3526
+ align-items: center;
3527
+ gap: 6px;
3528
+ padding: 4px 10px;
3529
+ background: linear-gradient(135deg, rgba(99, 102, 241, 0.08) 0%, rgba(139, 92, 246, 0.08) 100%);
3530
+ border: 1px solid rgba(99, 102, 241, 0.2);
3531
+ border-radius: 6px;
3532
+ font-size: 11px;
3533
+ color: #6366f1;
3534
+ text-decoration: none;
3535
+ transition: all 0.2s ease;
3536
+ white-space: nowrap;
3537
+
3538
+ svg {
3539
+ width: 12px;
3540
+ height: 12px;
3541
+ }
3542
+
3543
+ &:hover {
3544
+ background: linear-gradient(135deg, rgba(99, 102, 241, 0.15) 0%, rgba(139, 92, 246, 0.15) 100%);
3545
+ border-color: rgba(99, 102, 241, 0.4);
3546
+ transform: translateY(-1px);
3547
+ }
3548
+
3549
+ @media (max-width: 768px) {
3550
+ display: none;
3551
+ }
3552
+ `;
3122
3553
  const LoadingOverlay = styled__default.default.div`
3123
3554
  position: absolute;
3124
3555
  top: 0;
@@ -3133,6 +3564,18 @@ const LoadingOverlay = styled__default.default.div`
3133
3564
  gap: 12px;
3134
3565
  z-index: 10;
3135
3566
  `;
3567
+ const VersionHistoryOverlay = styled__default.default.div`
3568
+ position: fixed;
3569
+ top: 0;
3570
+ left: 0;
3571
+ right: 0;
3572
+ bottom: 0;
3573
+ background: rgba(0, 0, 0, 0.5);
3574
+ display: flex;
3575
+ align-items: center;
3576
+ justify-content: center;
3577
+ z-index: 99999;
3578
+ `;
3136
3579
  const LoadingText = styled__default.default.span`
3137
3580
  font-size: 13px;
3138
3581
  color: #64748b;
@@ -3348,10 +3791,12 @@ const Editor = React.forwardRef(({
3348
3791
  }, ref) => {
3349
3792
  const { formatMessage } = getTranslation.useIntl();
3350
3793
  const t = (id, defaultMessage) => formatMessage({ id: getTranslation.getTranslation(id), defaultMessage });
3351
- const { licenseData } = useLicense();
3794
+ const { licenseData, tier: licenseTier } = useLicense();
3795
+ const { isAvailable: isWebtoolsAvailable, openLinkPicker: webtoolsOpenLinkPicker } = useWebtoolsLinks();
3352
3796
  const editorRef = React.useRef(null);
3353
3797
  const editorInstanceRef = React.useRef(null);
3354
3798
  const containerRef = React.useRef(null);
3799
+ const webtoolsSelectionRef = React.useRef({ text: "", range: null, blockIndex: -1, existingAnchor: null, existingHref: "" });
3355
3800
  const isReadyRef = React.useRef(false);
3356
3801
  const [isReady, setIsReady] = React.useState(false);
3357
3802
  const [showCreditsModal, setShowCreditsModal] = React.useState(false);
@@ -3387,6 +3832,51 @@ const Editor = React.forwardRef(({
3387
3832
  const [aiSelectedText, setAISelectedText] = React.useState("");
3388
3833
  const aiSelectionRangeRef = React.useRef(null);
3389
3834
  const [aiLoading, setAILoading] = React.useState(false);
3835
+ const [showVersionHistory, setShowVersionHistory] = React.useState(false);
3836
+ React.useEffect(() => {
3837
+ if (!isWebtoolsAvailable || !editorRef.current) return;
3838
+ const updateWebtoolsSelection = () => {
3839
+ const selection = window.getSelection();
3840
+ if (!selection || selection.rangeCount === 0) return;
3841
+ const range = selection.getRangeAt(0);
3842
+ if (!editorRef.current.contains(range.commonAncestorContainer)) return;
3843
+ const selectedText = selection.toString().trim();
3844
+ let existingAnchor = null;
3845
+ let existingHref = "";
3846
+ let node = range.commonAncestorContainer;
3847
+ while (node && node !== editorRef.current) {
3848
+ if (node.nodeName === "A") {
3849
+ existingAnchor = node;
3850
+ existingHref = node.href || "";
3851
+ break;
3852
+ }
3853
+ node = node.parentNode;
3854
+ }
3855
+ if (!existingAnchor) {
3856
+ node = range.startContainer;
3857
+ while (node && node !== editorRef.current) {
3858
+ if (node.nodeName === "A") {
3859
+ existingAnchor = node;
3860
+ existingHref = node.href || "";
3861
+ break;
3862
+ }
3863
+ node = node.parentNode;
3864
+ }
3865
+ }
3866
+ const blockIndex = editorInstanceRef.current?.blocks?.getCurrentBlockIndex?.() ?? -1;
3867
+ webtoolsSelectionRef.current = {
3868
+ text: existingAnchor ? existingAnchor.textContent : selectedText,
3869
+ range: range.cloneRange(),
3870
+ blockIndex,
3871
+ existingAnchor,
3872
+ existingHref
3873
+ };
3874
+ };
3875
+ document.addEventListener("selectionchange", updateWebtoolsSelection);
3876
+ return () => {
3877
+ document.removeEventListener("selectionchange", updateWebtoolsSelection);
3878
+ };
3879
+ }, [isWebtoolsAvailable, isReady]);
3390
3880
  const serializedInitialValue = React.useMemo(() => {
3391
3881
  if (!value) {
3392
3882
  return "";
@@ -3414,12 +3904,25 @@ const Editor = React.forwardRef(({
3414
3904
  const {
3415
3905
  doc: yDoc,
3416
3906
  blocksMap: yBlocksMap,
3907
+ textMap: yTextMap,
3908
+ // NEW: Y.Map<blockId, Y.Text> for character-level sync
3417
3909
  metaMap: yMetaMap,
3910
+ // Character-level text helpers
3911
+ getBlockText,
3912
+ // NEW: Get Y.Text for a block
3913
+ setBlockText,
3914
+ // NEW: Get block text as HTML
3915
+ // Utility functions
3916
+ htmlToDelta: collabHtmlToDelta,
3917
+ deltaToHtml: collabDeltaToHtml,
3918
+ // Connection status
3418
3919
  status: collabStatus,
3419
3920
  error: collabError,
3921
+ // Collaboration
3420
3922
  peers: collabPeers,
3421
3923
  awareness: collabAwareness,
3422
3924
  emitAwareness,
3925
+ // Role-based access control
3423
3926
  collabRole,
3424
3927
  canEdit: collabCanEdit
3425
3928
  } = useMagicCollaboration({
@@ -3434,6 +3937,171 @@ const Editor = React.forwardRef(({
3434
3937
  }
3435
3938
  }
3436
3939
  });
3940
+ const yTextBindingsRef = React.useRef(/* @__PURE__ */ new Map());
3941
+ const bindBlockToYText = React.useCallback((blockId, element) => {
3942
+ if (!collabEnabled || !blockId || !element || !yTextMap) return;
3943
+ if (yTextBindingsRef.current.has(blockId)) {
3944
+ const existing = yTextBindingsRef.current.get(blockId);
3945
+ if (existing.element === element) return;
3946
+ unbindBlockFromYText(blockId);
3947
+ }
3948
+ const ytext = getBlockText(blockId);
3949
+ if (!ytext) return;
3950
+ let isUpdating = false;
3951
+ const ytextObserver = (event) => {
3952
+ if (isUpdating) return;
3953
+ if (event.transaction.local) return;
3954
+ isUpdating = true;
3955
+ try {
3956
+ const selection = window.getSelection();
3957
+ let cursorOffset = 0;
3958
+ if (selection && selection.rangeCount > 0 && element.contains(selection.anchorNode)) {
3959
+ const range = selection.getRangeAt(0);
3960
+ const preCaretRange = document.createRange();
3961
+ preCaretRange.selectNodeContents(element);
3962
+ preCaretRange.setEnd(range.startContainer, range.startOffset);
3963
+ cursorOffset = preCaretRange.toString().length;
3964
+ }
3965
+ const html = collabDeltaToHtml(ytext.toDelta());
3966
+ if (element.innerHTML !== html) {
3967
+ element.innerHTML = html || "";
3968
+ }
3969
+ if (document.activeElement === element && cursorOffset > 0) {
3970
+ try {
3971
+ const textNode = element.firstChild;
3972
+ if (textNode && textNode.nodeType === Node.TEXT_NODE) {
3973
+ const newRange = document.createRange();
3974
+ const pos = Math.min(cursorOffset, textNode.length);
3975
+ newRange.setStart(textNode, pos);
3976
+ newRange.setEnd(textNode, pos);
3977
+ selection.removeAllRanges();
3978
+ selection.addRange(newRange);
3979
+ }
3980
+ } catch (e) {
3981
+ }
3982
+ }
3983
+ } finally {
3984
+ isUpdating = false;
3985
+ }
3986
+ };
3987
+ const inputHandler = () => {
3988
+ if (isUpdating) return;
3989
+ isUpdating = true;
3990
+ try {
3991
+ const html = element.innerHTML;
3992
+ const newDelta = collabHtmlToDelta(html);
3993
+ const currentDelta = ytext.toDelta();
3994
+ yDoc.transact(() => {
3995
+ if (ytext.length > 0) {
3996
+ ytext.delete(0, ytext.length);
3997
+ }
3998
+ if (newDelta.length > 0) {
3999
+ ytext.applyDelta(newDelta);
4000
+ }
4001
+ }, "local");
4002
+ } finally {
4003
+ isUpdating = false;
4004
+ }
4005
+ };
4006
+ ytext.observe(ytextObserver);
4007
+ element.addEventListener("input", inputHandler);
4008
+ yTextBindingsRef.current.set(blockId, {
4009
+ ytext,
4010
+ element,
4011
+ ytextObserver,
4012
+ inputHandler
4013
+ });
4014
+ console.log("[Magic Editor X] [CHAR-SYNC] Bound block to Y.Text:", blockId);
4015
+ }, [collabEnabled, yTextMap, yDoc, getBlockText, collabHtmlToDelta, collabDeltaToHtml]);
4016
+ const unbindBlockFromYText = React.useCallback((blockId) => {
4017
+ const binding = yTextBindingsRef.current.get(blockId);
4018
+ if (!binding) return;
4019
+ binding.ytext.unobserve(binding.ytextObserver);
4020
+ binding.element.removeEventListener("input", binding.inputHandler);
4021
+ yTextBindingsRef.current.delete(blockId);
4022
+ console.log("[Magic Editor X] [CHAR-SYNC] Unbound block from Y.Text:", blockId);
4023
+ }, []);
4024
+ const bindAllBlocksToYText = React.useCallback(() => {
4025
+ if (!collabEnabled || !editorInstanceRef.current || !yTextMap) return;
4026
+ const editor = editorInstanceRef.current;
4027
+ const blockCount = editor.blocks.getBlocksCount();
4028
+ console.log("[Magic Editor X] [CHAR-SYNC] Binding", blockCount, "blocks to Y.Text");
4029
+ for (let i = 0; i < blockCount; i++) {
4030
+ try {
4031
+ const block = editor.blocks.getBlockByIndex(i);
4032
+ if (!block || !block.id) continue;
4033
+ const blockHolder = block.holder;
4034
+ if (!blockHolder) continue;
4035
+ const contentEditable = blockHolder.querySelector('[contenteditable="true"]');
4036
+ if (contentEditable) {
4037
+ const ytext = getBlockText(block.id);
4038
+ if (ytext && ytext.length === 0) {
4039
+ const currentHtml = contentEditable.innerHTML;
4040
+ if (currentHtml && currentHtml !== "<br>") {
4041
+ setBlockText(block.id, currentHtml);
4042
+ }
4043
+ }
4044
+ bindBlockToYText(block.id, contentEditable);
4045
+ }
4046
+ } catch (e) {
4047
+ console.warn("[Magic Editor X] [CHAR-SYNC] Error binding block:", e);
4048
+ }
4049
+ }
4050
+ }, [collabEnabled, yTextMap, getBlockText, setBlockText, bindBlockToYText]);
4051
+ const blockObserverRef = React.useRef(null);
4052
+ React.useEffect(() => {
4053
+ if (!collabEnabled || !editorRef.current || !yTextMap) return;
4054
+ const observer = new MutationObserver((mutations) => {
4055
+ let hasNewBlocks = false;
4056
+ for (const mutation of mutations) {
4057
+ if (mutation.type === "childList" && mutation.addedNodes.length > 0) {
4058
+ for (const node of mutation.addedNodes) {
4059
+ if (node.nodeType === Node.ELEMENT_NODE && (node.classList?.contains("ce-block") || node.querySelector?.(".ce-block"))) {
4060
+ hasNewBlocks = true;
4061
+ break;
4062
+ }
4063
+ }
4064
+ }
4065
+ if (hasNewBlocks) break;
4066
+ }
4067
+ if (hasNewBlocks) {
4068
+ setTimeout(() => {
4069
+ bindAllBlocksToYText();
4070
+ }, 50);
4071
+ }
4072
+ });
4073
+ observer.observe(editorRef.current, {
4074
+ childList: true,
4075
+ subtree: true
4076
+ });
4077
+ blockObserverRef.current = observer;
4078
+ return () => {
4079
+ observer.disconnect();
4080
+ blockObserverRef.current = null;
4081
+ };
4082
+ }, [collabEnabled, yTextMap, bindAllBlocksToYText]);
4083
+ React.useEffect(() => {
4084
+ return () => {
4085
+ yTextBindingsRef.current.forEach((binding, blockId) => {
4086
+ binding.ytext.unobserve(binding.ytextObserver);
4087
+ binding.element.removeEventListener("input", binding.inputHandler);
4088
+ });
4089
+ yTextBindingsRef.current.clear();
4090
+ };
4091
+ }, []);
4092
+ const {
4093
+ snapshots,
4094
+ loading: versionHistoryLoading,
4095
+ error: versionHistoryError,
4096
+ fetchSnapshots,
4097
+ restoreSnapshot,
4098
+ createSnapshot
4099
+ } = useVersionHistory();
4100
+ React.useEffect(() => {
4101
+ if (showVersionHistory && collabRoomId) {
4102
+ fetchSnapshots(collabRoomId);
4103
+ }
4104
+ }, [showVersionHistory, collabRoomId, fetchSnapshots]);
3437
4105
  React.useMemo(() => {
3438
4106
  switch (collabRole) {
3439
4107
  case "viewer":
@@ -3767,23 +4435,15 @@ const Editor = React.forwardRef(({
3767
4435
  setWordCount(plainText.split(/\s+/).filter((w) => w.length > 0).length);
3768
4436
  }, []);
3769
4437
  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
4438
  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
4439
  return;
3776
4440
  }
3777
4441
  const editor = editorInstanceRef.current;
3778
4442
  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
- });
3781
4443
  pendingRenderRef.current = pendingRenderRef.current || true;
3782
4444
  return;
3783
4445
  }
3784
4446
  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
4447
  return;
3788
4448
  }
3789
4449
  isApplyingRemoteRef.current = true;
@@ -3801,29 +4461,35 @@ const Editor = React.forwardRef(({
3801
4461
  yOrder = Array.from(yBlocksMap.keys());
3802
4462
  }
3803
4463
  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
4464
  yOrder.forEach((id) => {
3807
4465
  const json = yBlocksMap.get(id);
3808
4466
  if (json) {
3809
4467
  try {
3810
- const block = JSON.parse(json);
3811
- yBlocks.push(block);
4468
+ const blockData = JSON.parse(json);
4469
+ if (blockData.type && !blockData.data) {
4470
+ const ytext = yTextMap?.get(id);
4471
+ const textContent = ytext ? collabDeltaToHtml(ytext.toDelta()) : "";
4472
+ yBlocks.push({
4473
+ id,
4474
+ type: blockData.type,
4475
+ data: { text: textContent },
4476
+ tunes: blockData.tunes || {}
4477
+ });
4478
+ } else if (blockData.type && blockData.data) {
4479
+ yBlocks.push({
4480
+ id,
4481
+ ...blockData
4482
+ });
4483
+ }
3812
4484
  } catch (e) {
3813
4485
  console.warn("[Magic Editor X] Invalid block JSON:", id);
3814
4486
  }
3815
4487
  }
3816
4488
  });
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
4489
  const parsed = { blocks: yBlocks };
3820
4490
  const normalizedParsed = serializeForCompare(parsed);
3821
4491
  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
4492
  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
4493
  lastSerializedValueRef.current = normalizedParsed;
3828
4494
  setBlocksCount(yBlocks.length);
3829
4495
  calculateStats(parsed);
@@ -3836,8 +4502,6 @@ const Editor = React.forwardRef(({
3836
4502
  currentBlocks.push({ id: block.id, index: i });
3837
4503
  }
3838
4504
  }
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
4505
  if (blockCount !== yBlocks.length) {
3842
4506
  console.log("[Magic Editor X] [SYNC] Structural change detected (count mismatch). Falling back to full render.");
3843
4507
  await renderFull();
@@ -4042,7 +4706,7 @@ const Editor = React.forwardRef(({
4042
4706
  const emptyPayload = JSON.stringify({ blocks: [] });
4043
4707
  lastSerializedValueRef.current = emptyPayload;
4044
4708
  pushLocalToCollab(emptyPayload);
4045
- onChange({ target: { name, value: null, type: "text" } });
4709
+ onChange({ target: { name, value: null, type: "json" } });
4046
4710
  setBlocksCount(0);
4047
4711
  setWordCount(0);
4048
4712
  setCharCount(0);
@@ -4055,7 +4719,11 @@ const Editor = React.forwardRef(({
4055
4719
  }, [isReady]);
4056
4720
  React.useEffect(() => {
4057
4721
  if (editorRef.current && !editorInstanceRef.current) {
4058
- const tools$1 = tools.getTools({ mediaLibToggleFunc, pluginId: index.PLUGIN_ID });
4722
+ const tools$1 = tools.getTools({
4723
+ mediaLibToggleFunc,
4724
+ pluginId: index.PLUGIN_ID,
4725
+ openLinkPicker: isWebtoolsAvailable ? webtoolsOpenLinkPicker : null
4726
+ });
4059
4727
  let initialData = void 0;
4060
4728
  if (value) {
4061
4729
  try {
@@ -4121,6 +4789,11 @@ const Editor = React.forwardRef(({
4121
4789
  }
4122
4790
  pendingRenderRef.current = null;
4123
4791
  }
4792
+ if (collabEnabled && yTextMap) {
4793
+ setTimeout(() => {
4794
+ bindAllBlocksToYText();
4795
+ }, 100);
4796
+ }
4124
4797
  },
4125
4798
  onChange: async (api) => {
4126
4799
  try {
@@ -4140,9 +4813,9 @@ const Editor = React.forwardRef(({
4140
4813
  pushLocalToCollabRef.current?.(docPayload);
4141
4814
  lastSerializedValueRef.current = normalized;
4142
4815
  if (count === 0) {
4143
- onChange({ target: { name, value: null, type: "text" } });
4816
+ onChange({ target: { name, value: null, type: "json" } });
4144
4817
  } else {
4145
- onChange({ target: { name, value: serialized, type: "text" } });
4818
+ onChange({ target: { name, value: outputData, type: "json" } });
4146
4819
  }
4147
4820
  } catch (error2) {
4148
4821
  console.error("[Magic Editor X] Error in onChange:", error2);
@@ -4265,6 +4938,96 @@ const Editor = React.forwardRef(({
4265
4938
  children: /* @__PURE__ */ jsxRuntime.jsx(outline.SparklesIcon, {})
4266
4939
  }
4267
4940
  ),
4941
+ isWebtoolsAvailable && /* @__PURE__ */ jsxRuntime.jsx(
4942
+ ToolButton,
4943
+ {
4944
+ type: "button",
4945
+ "data-tooltip": "Webtools Link Picker",
4946
+ onClick: async () => {
4947
+ if (!editorInstanceRef.current || !isReady) {
4948
+ console.warn("[Magic Editor X] Editor not ready");
4949
+ return;
4950
+ }
4951
+ const editor = editorInstanceRef.current;
4952
+ const {
4953
+ text: selectedText,
4954
+ range: savedRange,
4955
+ blockIndex,
4956
+ existingAnchor,
4957
+ existingHref
4958
+ } = webtoolsSelectionRef.current;
4959
+ console.log("[Magic Editor X] Webtools button clicked with stored selection:", {
4960
+ text: selectedText || "(none)",
4961
+ existingHref: existingHref || "(new link)",
4962
+ hasRange: !!savedRange,
4963
+ blockIndex
4964
+ });
4965
+ const currentBlockIndex = blockIndex >= 0 ? blockIndex : editor.blocks.getCurrentBlockIndex();
4966
+ const result = await webtoolsOpenLinkPicker({
4967
+ initialText: selectedText || "",
4968
+ initialHref: existingHref || ""
4969
+ });
4970
+ if (result && result.href) {
4971
+ const linkText = result.label || selectedText || result.href;
4972
+ const linkHtml = `<a href="${result.href}" target="_blank" rel="noopener noreferrer">${linkText}</a>`;
4973
+ if (existingAnchor && existingAnchor.parentNode) {
4974
+ try {
4975
+ existingAnchor.href = result.href;
4976
+ existingAnchor.textContent = linkText;
4977
+ const contentEditable = existingAnchor.closest('[contenteditable="true"]');
4978
+ if (contentEditable) {
4979
+ contentEditable.dispatchEvent(new Event("input", { bubbles: true }));
4980
+ }
4981
+ console.log("[Magic Editor X] Webtools link UPDATED:", {
4982
+ oldHref: existingHref,
4983
+ newHref: result.href,
4984
+ text: linkText
4985
+ });
4986
+ } catch (e) {
4987
+ console.error("[Magic Editor X] Failed to update link:", e);
4988
+ }
4989
+ } else if (savedRange && selectedText && currentBlockIndex >= 0) {
4990
+ try {
4991
+ const blockHolder = editor.blocks.getBlockByIndex(currentBlockIndex)?.holder;
4992
+ const contentEditable = blockHolder?.querySelector('[contenteditable="true"]');
4993
+ if (contentEditable) {
4994
+ const selection = window.getSelection();
4995
+ selection.removeAllRanges();
4996
+ selection.addRange(savedRange);
4997
+ document.execCommand("insertHTML", false, linkHtml);
4998
+ contentEditable.dispatchEvent(new Event("input", { bubbles: true }));
4999
+ console.log("[Magic Editor X] Webtools link CREATED:", {
5000
+ text: linkText,
5001
+ href: result.href
5002
+ });
5003
+ } else {
5004
+ editor.blocks.insert("paragraph", { text: linkHtml }, {}, currentBlockIndex + 1, true);
5005
+ }
5006
+ } catch (e) {
5007
+ console.error("[Magic Editor X] Failed to insert link:", e);
5008
+ editor.blocks.insert("paragraph", { text: linkHtml }, {}, currentBlockIndex + 1, true);
5009
+ }
5010
+ } else if (currentBlockIndex >= 0) {
5011
+ editor.blocks.insert("paragraph", {
5012
+ text: linkHtml
5013
+ }, {}, currentBlockIndex + 1, true);
5014
+ editor.caret.setToBlock(currentBlockIndex + 1);
5015
+ console.log("[Magic Editor X] Webtools link inserted (no selection):", result);
5016
+ } else {
5017
+ editor.blocks.insert("paragraph", { text: linkHtml });
5018
+ }
5019
+ }
5020
+ webtoolsSelectionRef.current = { text: "", range: null, blockIndex: -1, existingAnchor: null, existingHref: "" };
5021
+ },
5022
+ disabled: collabEnabled && collabCanEdit === false,
5023
+ style: {
5024
+ background: "linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)",
5025
+ color: "white",
5026
+ ...collabEnabled && collabCanEdit === false ? { opacity: 0.4, cursor: "not-allowed" } : {}
5027
+ },
5028
+ children: /* @__PURE__ */ jsxRuntime.jsx(outline.LinkIcon, {})
5029
+ }
5030
+ ),
4268
5031
  /* @__PURE__ */ jsxRuntime.jsx(ToolbarDivider, {}),
4269
5032
  /* @__PURE__ */ jsxRuntime.jsx(
4270
5033
  ToolButton,
@@ -4380,9 +5143,26 @@ const Editor = React.forwardRef(({
4380
5143
  /* @__PURE__ */ jsxRuntime.jsx("strong", { children: charCount }),
4381
5144
  " ",
4382
5145
  t("editor.characters", "Zeichen")
4383
- ] })
5146
+ ] }),
5147
+ !isWebtoolsAvailable && /* @__PURE__ */ jsxRuntime.jsxs(
5148
+ WebtoolsPromoLink,
5149
+ {
5150
+ href: "https://www.pluginpal.io/plugin/webtools",
5151
+ target: "_blank",
5152
+ rel: "noopener noreferrer",
5153
+ title: "Get Webtools Links addon for internal link management",
5154
+ children: [
5155
+ /* @__PURE__ */ jsxRuntime.jsx(outline.LinkIcon, {}),
5156
+ "Internal Links? Get Webtools"
5157
+ ]
5158
+ }
5159
+ )
4384
5160
  ] }),
4385
5161
  /* @__PURE__ */ jsxRuntime.jsxs(FooterRight, { children: [
5162
+ /* @__PURE__ */ jsxRuntime.jsxs(FooterButton, { type: "button", onClick: () => setShowVersionHistory(true), children: [
5163
+ /* @__PURE__ */ jsxRuntime.jsx(outline.ClockIcon, {}),
5164
+ t("editor.versionHistory", "History")
5165
+ ] }),
4386
5166
  !(collabEnabled && collabCanEdit === false) && /* @__PURE__ */ jsxRuntime.jsxs(FooterButton, { type: "button", onClick: () => handleInsertBlock("mediaLib"), children: [
4387
5167
  /* @__PURE__ */ jsxRuntime.jsx(outline.PhotoIcon, {}),
4388
5168
  t("editor.mediaLibrary", "Media Library")
@@ -4455,6 +5235,51 @@ const Editor = React.forwardRef(({
4455
5235
  }
4456
5236
  }
4457
5237
  ),
5238
+ showVersionHistory && /* @__PURE__ */ jsxRuntime.jsx(VersionHistoryOverlay, { onClick: () => setShowVersionHistory(false), children: /* @__PURE__ */ jsxRuntime.jsx("div", { onClick: (e) => e.stopPropagation(), children: /* @__PURE__ */ jsxRuntime.jsx(
5239
+ VersionHistoryPanel,
5240
+ {
5241
+ snapshots,
5242
+ loading: versionHistoryLoading,
5243
+ error: versionHistoryError,
5244
+ tier: licenseTier,
5245
+ onClose: () => setShowVersionHistory(false),
5246
+ onRestore: async (snapshot) => {
5247
+ if (snapshot.documentId && editorInstanceRef.current && isReady) {
5248
+ try {
5249
+ const result = await restoreSnapshot(snapshot.documentId, collabRoomId);
5250
+ const contentToRestore = result?.jsonContent || snapshot.jsonContent;
5251
+ if (contentToRestore && editorInstanceRef.current) {
5252
+ await editorInstanceRef.current.render(contentToRestore);
5253
+ setShowVersionHistory(false);
5254
+ onChange({ target: { name, value: contentToRestore, type: "json" } });
5255
+ }
5256
+ } catch (err) {
5257
+ console.error("[Magic Editor X] Failed to restore snapshot:", err?.message);
5258
+ }
5259
+ }
5260
+ },
5261
+ onCreate: async () => {
5262
+ if (collabRoomId && editorInstanceRef.current && isReady) {
5263
+ const [contentType, entryId, fieldName] = collabRoomId.split("|");
5264
+ if (contentType && entryId && fieldName) {
5265
+ try {
5266
+ const editorContent = await editorInstanceRef.current.save();
5267
+ await createSnapshot({
5268
+ roomId: collabRoomId,
5269
+ contentType,
5270
+ entryId,
5271
+ fieldName,
5272
+ content: editorContent
5273
+ });
5274
+ fetchSnapshots(collabRoomId);
5275
+ } catch (err) {
5276
+ console.error("[Magic Editor X] Failed to create snapshot:", err?.message);
5277
+ }
5278
+ }
5279
+ }
5280
+ }
5281
+ }
5282
+ ) }) }),
4458
5283
  /* @__PURE__ */ jsxRuntime.jsx(
4459
5284
  CreditsModal,
4460
5285
  {