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
@@ -1,12 +1,12 @@
1
1
  import { jsx, jsxs, Fragment } from "react/jsx-runtime";
2
2
  import React__default, { useState, useEffect, useCallback, useRef, useMemo, forwardRef } from "react";
3
- import { u as useIntl, F as Field, L as Loader, g as getTranslation } from "./getTranslation-C4uWR0DB.mjs";
3
+ import { u as useIntl, F as Flex, T as Typography, B as Button$2, D as Divider, a as Box, g as getTranslation, b as Field, L as Loader } from "./getTranslation-ChB_HlBd.mjs";
4
4
  import styled, { createGlobalStyle, css } from "styled-components";
5
- import { SparklesIcon as SparklesIcon$1, Bars3BottomLeftIcon, ListBulletIcon, CheckCircleIcon, PhotoIcon, LinkIcon, CodeBracketIcon, TableCellsIcon, ChatBubbleBottomCenterTextIcon, ExclamationTriangleIcon, MinusIcon, DocumentDuplicateIcon, TrashIcon, ArrowsPointingInIcon, ArrowsPointingOutIcon, EyeIcon, PencilSquareIcon } from "@heroicons/react/24/outline";
5
+ import { ClockIcon, ExclamationTriangleIcon, SparklesIcon as SparklesIcon$1, Bars3BottomLeftIcon, ListBulletIcon, CheckCircleIcon, PhotoIcon, LinkIcon, CodeBracketIcon, TableCellsIcon, ChatBubbleBottomCenterTextIcon, MinusIcon, DocumentDuplicateIcon, TrashIcon, ArrowsPointingInIcon, ArrowsPointingOutIcon, EyeIcon, PencilSquareIcon } from "@heroicons/react/24/outline";
6
6
  import EditorJS from "@editorjs/editorjs";
7
- import { M as MagicEditorAPI, t as toastManager, g as getTools, i as initUndoRedo, a as initDragDrop, A as AIToast, b as AIInlineToolbar } from "./tools-CjnQJ9w2.mjs";
7
+ import { M as MagicEditorAPI, t as toastManager, g as getTools, i as initUndoRedo, a as initDragDrop, A as AIToast, b as AIInlineToolbar } from "./tools-BAvbiUHr.mjs";
8
8
  import { useStrapiApp, useFetchClient, useAuth } from "@strapi/strapi/admin";
9
- import { P as PLUGIN_ID } from "./index-B5MzUyo0.mjs";
9
+ import { P as PLUGIN_ID } from "./index-CtyxDZ0S.mjs";
10
10
  import { io } from "socket.io-client";
11
11
  import * as Y from "yjs";
12
12
  import { IndexeddbPersistence } from "y-indexeddb";
@@ -424,6 +424,89 @@ const getUserColor = (userId) => {
424
424
  const hash = String(userId).split("").reduce((acc, char) => acc + char.charCodeAt(0), 0);
425
425
  return CURSOR_COLORS[hash % CURSOR_COLORS.length];
426
426
  };
427
+ const htmlToDelta = (html) => {
428
+ if (!html || html === "<br>" || html === "<br/>") return [];
429
+ const parser = new DOMParser();
430
+ const doc = parser.parseFromString(`<div>${html}</div>`, "text/html");
431
+ const delta = [];
432
+ function processNode(node, attributes = {}) {
433
+ if (node.nodeType === Node.TEXT_NODE) {
434
+ const text = node.textContent;
435
+ if (text) {
436
+ const attrs = Object.keys(attributes).length > 0 ? { ...attributes } : void 0;
437
+ delta.push({ insert: text, attributes: attrs });
438
+ }
439
+ return;
440
+ }
441
+ if (node.nodeType === Node.ELEMENT_NODE) {
442
+ const newAttrs = { ...attributes };
443
+ const tagName = node.tagName.toLowerCase();
444
+ switch (tagName) {
445
+ case "b":
446
+ case "strong":
447
+ newAttrs.bold = true;
448
+ break;
449
+ case "i":
450
+ case "em":
451
+ newAttrs.italic = true;
452
+ break;
453
+ case "u":
454
+ newAttrs.underline = true;
455
+ break;
456
+ case "code":
457
+ newAttrs.code = true;
458
+ break;
459
+ case "a":
460
+ newAttrs.a = {
461
+ href: node.getAttribute("href") || "",
462
+ target: node.getAttribute("target") || "_blank",
463
+ rel: node.getAttribute("rel") || "noopener noreferrer"
464
+ };
465
+ break;
466
+ case "mark":
467
+ newAttrs.mark = true;
468
+ break;
469
+ case "br":
470
+ delta.push({ insert: "\n" });
471
+ return;
472
+ }
473
+ for (const child of node.childNodes) {
474
+ processNode(child, newAttrs);
475
+ }
476
+ }
477
+ }
478
+ const wrapper = doc.body.firstChild;
479
+ if (wrapper) {
480
+ for (const child of wrapper.childNodes) {
481
+ processNode(child);
482
+ }
483
+ }
484
+ return delta;
485
+ };
486
+ const deltaToHtml = (delta) => {
487
+ if (!delta || delta.length === 0) return "";
488
+ let html = "";
489
+ for (const op of delta) {
490
+ if (typeof op.insert !== "string") continue;
491
+ let text = op.insert;
492
+ text = text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/\n/g, "<br>");
493
+ const attrs = op.attributes || {};
494
+ let result = text;
495
+ if (attrs.code) result = `<code>${result}</code>`;
496
+ if (attrs.italic) result = `<i>${result}</i>`;
497
+ if (attrs.bold) result = `<b>${result}</b>`;
498
+ if (attrs.underline) result = `<u>${result}</u>`;
499
+ if (attrs.mark) result = `<mark>${result}</mark>`;
500
+ if (attrs.a) {
501
+ const href = attrs.a.href || "";
502
+ const target = attrs.a.target || "_blank";
503
+ const rel = attrs.a.rel || "noopener noreferrer";
504
+ result = `<a href="${href}" target="${target}" rel="${rel}">${result}</a>`;
505
+ }
506
+ html += result;
507
+ }
508
+ return html;
509
+ };
427
510
  const useMagicCollaboration = ({
428
511
  enabled,
429
512
  roomId,
@@ -448,16 +531,47 @@ const useMagicCollaboration = ({
448
531
  useEffect(() => {
449
532
  onRemoteUpdateRef.current = onRemoteUpdate;
450
533
  }, [onRemoteUpdate]);
451
- const { doc, blocksMap, metaMap } = useMemo(() => {
534
+ const { doc, blocksMap, textMap, metaMap } = useMemo(() => {
452
535
  const yDoc = new Y.Doc();
453
536
  return {
454
537
  doc: yDoc,
455
538
  blocksMap: yDoc.getMap("blocks"),
456
- // Each block stored by ID
539
+ // Block metadata (type, tunes)
540
+ textMap: yDoc.getMap("text"),
541
+ // Y.Text per block (character-level sync!)
457
542
  metaMap: yDoc.getMap("meta")
458
- // Metadata (time, blockOrder, etc.)
543
+ // Document metadata (time, blockOrder)
459
544
  };
460
545
  }, [roomId]);
546
+ const getBlockText = useCallback((blockId) => {
547
+ if (!blockId) return null;
548
+ let ytext = textMap.get(blockId);
549
+ if (!ytext) {
550
+ ytext = new Y.Text();
551
+ textMap.set(blockId, ytext);
552
+ }
553
+ return ytext;
554
+ }, [textMap]);
555
+ const setBlockText = useCallback((blockId, html) => {
556
+ if (!blockId) return;
557
+ const ytext = getBlockText(blockId);
558
+ if (!ytext) return;
559
+ doc.transact(() => {
560
+ if (ytext.length > 0) {
561
+ ytext.delete(0, ytext.length);
562
+ }
563
+ const delta = htmlToDelta(html);
564
+ if (delta.length > 0) {
565
+ ytext.applyDelta(delta);
566
+ }
567
+ }, "local");
568
+ }, [doc, getBlockText]);
569
+ const getBlockTextHtml = useCallback((blockId) => {
570
+ if (!blockId) return "";
571
+ const ytext = textMap.get(blockId);
572
+ if (!ytext) return "";
573
+ return deltaToHtml(ytext.toDelta());
574
+ }, [textMap]);
461
575
  useEffect(() => {
462
576
  return () => {
463
577
  doc.destroy();
@@ -522,9 +636,6 @@ const useMagicCollaboration = ({
522
636
  persistenceRef.current = persistence;
523
637
  persistence.on("synced", () => {
524
638
  console.log("[Magic Collab] [CACHE] IndexedDB synced for room:", roomId);
525
- const blockOrder = metaMap.get("blockOrder");
526
- 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(() => {
527
- });
528
639
  });
529
640
  console.log("[Magic Collab] [CACHE] IndexedDB persistence initialized:", persistenceKey);
530
641
  } catch (e) {
@@ -611,17 +722,11 @@ const useMagicCollaboration = ({
611
722
  if (update) {
612
723
  console.log("[Magic Collab] [SYNC] Syncing initial state, update size:", update.length, "bytes");
613
724
  try {
614
- const blockOrderBefore = metaMap.get("blockOrder");
615
- 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(() => {
616
- });
617
725
  const beforeBlockCount = blocksMap.size;
618
726
  console.log("[Magic Collab] [DATA] Y.Map BEFORE sync - block count:", beforeBlockCount);
619
727
  Y.applyUpdate(doc, new Uint8Array(update), "remote");
620
728
  const afterBlockCount = blocksMap.size;
621
729
  console.log("[Magic Collab] [DATA] Y.Map AFTER sync - block count:", afterBlockCount);
622
- const blockOrderAfter = metaMap.get("blockOrder");
623
- 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(() => {
624
- });
625
730
  if (onRemoteUpdateRef.current) {
626
731
  console.log("[Magic Collab] [CALLBACK] Calling onRemoteUpdate callback after sync");
627
732
  setTimeout(() => {
@@ -637,15 +742,11 @@ const useMagicCollaboration = ({
637
742
  if (update) {
638
743
  console.log("[Magic Collab] [UPDATE] Received remote update:", update.length, "bytes");
639
744
  try {
640
- 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(() => {
641
- });
642
745
  const beforeBlockCount = blocksMap.size;
643
746
  console.log("[Magic Collab] [DATA] Y.Map BEFORE update - blocks:", beforeBlockCount);
644
747
  Y.applyUpdate(doc, new Uint8Array(update), "remote");
645
748
  const afterBlockCount = blocksMap.size;
646
749
  console.log("[Magic Collab] [DATA] Y.Map AFTER update - blocks:", afterBlockCount);
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: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(() => {
648
- });
649
750
  if (onRemoteUpdateRef.current) {
650
751
  console.log("[Magic Collab] [CALLBACK] Calling onRemoteUpdate callback");
651
752
  setTimeout(() => {
@@ -769,13 +870,30 @@ const useMagicCollaboration = ({
769
870
  return user ? getUserColor(user.id) : CURSOR_COLORS[0];
770
871
  }, [user]);
771
872
  return {
873
+ // Y.js Document & Maps
772
874
  doc,
773
875
  blocksMap,
774
- // Y.Map for block-level sync (replaces Y.Text)
876
+ // Y.Map for block metadata (type, tunes)
877
+ textMap,
878
+ // Y.Map<blockId, Y.Text> for character-level text sync
775
879
  metaMap,
776
- // Y.Map for metadata (includes blockOrder as JSON string)
880
+ // Y.Map for document metadata (time, blockOrder)
881
+ // Character-level text helpers
882
+ getBlockText,
883
+ // Get Y.Text for a block (creates if not exists)
884
+ setBlockText,
885
+ // Set block text from HTML
886
+ getBlockTextHtml,
887
+ // Get block text as HTML
888
+ // Utility functions
889
+ htmlToDelta,
890
+ // Convert HTML to delta
891
+ deltaToHtml,
892
+ // Convert delta to HTML
893
+ // Connection status
777
894
  status,
778
895
  error,
896
+ // Collaboration
779
897
  peers,
780
898
  awareness,
781
899
  emitAwareness,
@@ -1073,6 +1191,136 @@ const useAIActions = ({ licenseKey, editorInstanceRef, isReady, onNoCredits }) =
1073
1191
  }, [replaceText, appendText, onNoCredits]);
1074
1192
  return { handleAIAction };
1075
1193
  };
1194
+ const useWebtoolsLinks = () => {
1195
+ const getPlugin = useStrapiApp("WebtoolsLinks", (state) => state.getPlugin);
1196
+ const linksPlugin = useMemo(() => {
1197
+ try {
1198
+ return getPlugin?.("webtools-addon-links");
1199
+ } catch (e) {
1200
+ return null;
1201
+ }
1202
+ }, [getPlugin]);
1203
+ const isAvailable = useMemo(() => {
1204
+ const available = !!linksPlugin?.apis?.openLinkPicker;
1205
+ if (typeof window !== "undefined" && !window.__WEBTOOLS_LINKS_CHECKED__) {
1206
+ window.__WEBTOOLS_LINKS_CHECKED__ = true;
1207
+ if (available) {
1208
+ console.log("[Magic Editor X] [SUCCESS] Webtools Links addon detected - Link Picker enabled");
1209
+ } else {
1210
+ console.log("[Magic Editor X] [INFO] Webtools Links addon not installed - Link Picker disabled");
1211
+ }
1212
+ }
1213
+ return available;
1214
+ }, [linksPlugin]);
1215
+ const openLinkPicker = useCallback(async ({ initialHref = "", initialText = "" } = {}) => {
1216
+ if (!linksPlugin?.apis?.openLinkPicker) {
1217
+ console.warn("[Magic Editor X] Webtools Link Picker not available");
1218
+ return null;
1219
+ }
1220
+ console.log("[Magic Editor X] Opening Webtools Link Picker with:", {
1221
+ linkType: "both",
1222
+ initialHref: initialHref || "(empty)",
1223
+ initialText: initialText || "(empty)"
1224
+ });
1225
+ try {
1226
+ const result = await linksPlugin.apis.openLinkPicker({
1227
+ linkType: "both",
1228
+ // Allow both internal and external links
1229
+ initialHref: initialHref || "",
1230
+ initialText: initialText || ""
1231
+ });
1232
+ console.log("[Magic Editor X] Webtools picker result:", result);
1233
+ if (result && result.href) {
1234
+ console.log("[Magic Editor X] Webtools link selected:", result);
1235
+ return {
1236
+ href: result.href,
1237
+ label: result.label || initialText || ""
1238
+ };
1239
+ }
1240
+ return null;
1241
+ } catch (error) {
1242
+ console.error("[Magic Editor X] Error opening Webtools Link Picker:", error);
1243
+ return null;
1244
+ }
1245
+ }, [linksPlugin]);
1246
+ return {
1247
+ isAvailable,
1248
+ openLinkPicker
1249
+ };
1250
+ };
1251
+ const apiBase = "/magic-editor-x";
1252
+ const useVersionHistory = () => {
1253
+ const { get, post } = useFetchClient();
1254
+ const { tier } = useLicense();
1255
+ const [snapshots, setSnapshots] = useState([]);
1256
+ const [loading, setLoading] = useState(false);
1257
+ const [error, setError] = useState(null);
1258
+ const fetchSnapshots = useCallback(
1259
+ async (roomId) => {
1260
+ if (!roomId) return;
1261
+ setLoading(true);
1262
+ setError(null);
1263
+ try {
1264
+ const { data } = await get(`${apiBase}/snapshots/${roomId}`);
1265
+ setSnapshots(data?.data || []);
1266
+ } catch (err) {
1267
+ setError(err?.message || "Failed to load snapshots");
1268
+ } finally {
1269
+ setLoading(false);
1270
+ }
1271
+ },
1272
+ [get]
1273
+ );
1274
+ const restoreSnapshot = useCallback(
1275
+ async (documentId, roomId) => {
1276
+ if (!documentId) return;
1277
+ setLoading(true);
1278
+ setError(null);
1279
+ try {
1280
+ const { data } = await post(`${apiBase}/snapshots/restore/${documentId}`, { roomId });
1281
+ return data?.data;
1282
+ } catch (err) {
1283
+ setError(err?.message || "Failed to restore snapshot");
1284
+ throw err;
1285
+ } finally {
1286
+ setLoading(false);
1287
+ }
1288
+ },
1289
+ [get]
1290
+ );
1291
+ const createSnapshot = useCallback(
1292
+ async ({ roomId, contentType, entryId, fieldName, content }) => {
1293
+ if (!roomId || !contentType || !entryId || !fieldName) return;
1294
+ setLoading(true);
1295
+ setError(null);
1296
+ try {
1297
+ const { data } = await post(`${apiBase}/snapshots/${roomId}`, {
1298
+ contentType,
1299
+ entryId,
1300
+ fieldName,
1301
+ content
1302
+ // Include editor content as fallback
1303
+ });
1304
+ return data?.data;
1305
+ } catch (err) {
1306
+ setError(err?.message || "Failed to create snapshot");
1307
+ throw err;
1308
+ } finally {
1309
+ setLoading(false);
1310
+ }
1311
+ },
1312
+ [post]
1313
+ );
1314
+ return {
1315
+ snapshots,
1316
+ loading,
1317
+ error,
1318
+ tier,
1319
+ fetchSnapshots,
1320
+ restoreSnapshot,
1321
+ createSnapshot
1322
+ };
1323
+ };
1076
1324
  const Overlay$1 = styled.div`
1077
1325
  position: fixed;
1078
1326
  top: 0;
@@ -1096,7 +1344,7 @@ const PopupContainer = styled.div`
1096
1344
  display: flex;
1097
1345
  flex-direction: column;
1098
1346
  `;
1099
- const Header$1 = styled.div`
1347
+ const Header$2 = styled.div`
1100
1348
  background: linear-gradient(135deg, #7C3AED 0%, #a855f7 100%);
1101
1349
  padding: 20px 24px;
1102
1350
  color: white;
@@ -1118,7 +1366,7 @@ const CreditsBadge = styled.div`
1118
1366
  font-size: 13px;
1119
1367
  font-weight: 500;
1120
1368
  `;
1121
- const Content$1 = styled.div`
1369
+ const Content$2 = styled.div`
1122
1370
  padding: 24px;
1123
1371
  overflow-y: auto;
1124
1372
  flex: 1;
@@ -1383,7 +1631,7 @@ const AIAssistantPopup = ({ selectedText, licenseKey, onClose, onApply }) => {
1383
1631
  return () => window.removeEventListener("keydown", handleKeyDown);
1384
1632
  }, [onClose]);
1385
1633
  return /* @__PURE__ */ jsx(Overlay$1, { onClick: handleOverlayClick, children: /* @__PURE__ */ jsxs(PopupContainer, { onClick: (e) => e.stopPropagation(), children: [
1386
- /* @__PURE__ */ jsxs(Header$1, { children: [
1634
+ /* @__PURE__ */ jsxs(Header$2, { children: [
1387
1635
  /* @__PURE__ */ jsxs(HeaderTitle, { children: [
1388
1636
  /* @__PURE__ */ jsx(SparklesIcon, {}),
1389
1637
  /* @__PURE__ */ jsx("span", { children: "KI-Assistent" })
@@ -1395,7 +1643,7 @@ const AIAssistantPopup = ({ selectedText, licenseKey, onClose, onApply }) => {
1395
1643
  !usage?.tier && "Wird geladen..."
1396
1644
  ] })
1397
1645
  ] }),
1398
- /* @__PURE__ */ jsxs(Content$1, { children: [
1646
+ /* @__PURE__ */ jsxs(Content$2, { children: [
1399
1647
  /* @__PURE__ */ jsx(TextPreview, { children: selectedText.length > 300 ? selectedText.substring(0, 300) + "..." : selectedText }),
1400
1648
  /* @__PURE__ */ jsxs(TypeButtons, { children: [
1401
1649
  /* @__PURE__ */ jsxs(
@@ -1482,6 +1730,132 @@ const AIAssistantPopup = ({ selectedText, licenseKey, onClose, onApply }) => {
1482
1730
  ] })
1483
1731
  ] }) });
1484
1732
  };
1733
+ const PanelWrapper = styled(Box)`
1734
+ width: 320px;
1735
+ background: ${({ theme }) => theme.colors.neutral0};
1736
+ border: 1px solid ${({ theme }) => theme.colors.neutral150};
1737
+ border-radius: 8px;
1738
+ box-shadow: ${({ theme }) => theme.shadows.filterShadow};
1739
+ display: flex;
1740
+ flex-direction: column;
1741
+ max-height: 70vh;
1742
+ `;
1743
+ const Header$1 = styled(Flex)`
1744
+ padding: 12px 16px;
1745
+ border-bottom: 1px solid ${({ theme }) => theme.colors.neutral150};
1746
+ `;
1747
+ const Content$1 = styled(Box)`
1748
+ padding: 12px 16px;
1749
+ overflow-y: auto;
1750
+ `;
1751
+ const Item = styled(Box)`
1752
+ padding: 10px 12px;
1753
+ border: 1px solid ${({ theme }) => theme.colors.neutral150};
1754
+ border-radius: 6px;
1755
+ margin-bottom: 10px;
1756
+ `;
1757
+ const Meta = styled(Typography)`
1758
+ color: ${({ theme }) => theme.colors.neutral500};
1759
+ font-size: 12px;
1760
+ `;
1761
+ const PremiumBadge = styled(Box)`
1762
+ background: ${({ theme }) => theme.colors.primary100};
1763
+ color: ${({ theme }) => theme.colors.primary600};
1764
+ border-radius: 6px;
1765
+ padding: 8px 10px;
1766
+ display: inline-flex;
1767
+ align-items: center;
1768
+ gap: 8px;
1769
+ font-weight: 600;
1770
+ margin-top: 8px;
1771
+ `;
1772
+ const safeDateFrom = (value) => {
1773
+ if (!value) return null;
1774
+ if (value instanceof Date) {
1775
+ return isNaN(value.getTime()) ? null : value;
1776
+ }
1777
+ try {
1778
+ const parsed = new Date(value);
1779
+ return isNaN(parsed.getTime()) ? null : parsed;
1780
+ } catch {
1781
+ return null;
1782
+ }
1783
+ };
1784
+ const formatDate = (dateValue) => {
1785
+ const date = safeDateFrom(dateValue);
1786
+ if (!date) return "—";
1787
+ try {
1788
+ return date.toLocaleString();
1789
+ } catch {
1790
+ return "—";
1791
+ }
1792
+ };
1793
+ const VersionHistoryPanel = ({
1794
+ snapshots,
1795
+ loading,
1796
+ error,
1797
+ onRestore,
1798
+ onCreate,
1799
+ tier,
1800
+ onClose
1801
+ }) => {
1802
+ const { formatMessage } = useIntl();
1803
+ const canRestore = tier !== "free";
1804
+ const t = (id, defaultMessage) => formatMessage(
1805
+ { id: getTranslation(id), defaultMessage },
1806
+ {}
1807
+ );
1808
+ return /* @__PURE__ */ jsxs(PanelWrapper, { "data-testid": "version-history-panel", children: [
1809
+ /* @__PURE__ */ jsxs(Header$1, { justifyContent: "space-between", alignItems: "center", children: [
1810
+ /* @__PURE__ */ jsxs(Flex, { gap: 8, alignItems: "center", children: [
1811
+ /* @__PURE__ */ jsx(ClockIcon, { width: 18 }),
1812
+ /* @__PURE__ */ jsx(Typography, { fontWeight: "bold", children: t("versionHistory.title", "Version History") })
1813
+ ] }),
1814
+ /* @__PURE__ */ jsx(Button$2, { size: "S", variant: "tertiary", onClick: onClose, children: t("versionHistory.close", "Close") })
1815
+ ] }),
1816
+ /* @__PURE__ */ jsxs(Content$1, { children: [
1817
+ loading && /* @__PURE__ */ jsx(Typography, { children: t("versionHistory.loading", "Loading versions...") }),
1818
+ error && /* @__PURE__ */ jsx(Typography, { textColor: "danger600", children: error }),
1819
+ !loading && !error && snapshots.length === 0 && /* @__PURE__ */ jsx(Typography, { children: t("versionHistory.noSnapshots", "No versions saved yet") }),
1820
+ !loading && !error && snapshots.map((snap) => /* @__PURE__ */ jsxs(Item, { children: [
1821
+ /* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "center", children: [
1822
+ /* @__PURE__ */ jsxs(Typography, { fontWeight: "bold", children: [
1823
+ t("versionHistory.version", "Version"),
1824
+ " ",
1825
+ snap.version
1826
+ ] }),
1827
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", children: formatDate(snap.createdAt) })
1828
+ ] }),
1829
+ /* @__PURE__ */ jsxs(Meta, { children: [
1830
+ t("versionHistory.createdBy", "By"),
1831
+ " ",
1832
+ snap.createdBy?.firstname ? `${snap.createdBy.firstname} ${snap.createdBy.lastname || ""}`.trim() : "—"
1833
+ ] }),
1834
+ /* @__PURE__ */ jsx(Divider, { marginTop: 2, marginBottom: 2 }),
1835
+ canRestore ? /* @__PURE__ */ jsx(Flex, { gap: 8, children: /* @__PURE__ */ jsx(
1836
+ Button$2,
1837
+ {
1838
+ size: "S",
1839
+ variant: "secondary",
1840
+ onClick: () => onRestore?.(snap),
1841
+ children: t("versionHistory.restore", "Restore")
1842
+ }
1843
+ ) }) : /* @__PURE__ */ jsxs(PremiumBadge, { children: [
1844
+ /* @__PURE__ */ jsx(ExclamationTriangleIcon, { width: 16 }),
1845
+ t("versionHistory.premiumOnly", "Premium feature")
1846
+ ] })
1847
+ ] }, snap.documentId || snap.id)),
1848
+ /* @__PURE__ */ jsx(Divider, { marginTop: 4, marginBottom: 4 }),
1849
+ canRestore ? /* @__PURE__ */ jsx(Button$2, { size: "S", fullWidth: true, variant: "default", onClick: onCreate, disabled: loading, children: t("versionHistory.create", "Create Snapshot") }) : /* @__PURE__ */ jsxs(Box, { children: [
1850
+ /* @__PURE__ */ jsx(Button$2, { size: "S", fullWidth: true, variant: "default", disabled: true, children: t("versionHistory.create", "Create Snapshot") }),
1851
+ /* @__PURE__ */ jsxs(PremiumBadge, { style: { marginTop: "8px", width: "100%", justifyContent: "center" }, children: [
1852
+ /* @__PURE__ */ jsx(ExclamationTriangleIcon, { width: 16 }),
1853
+ t("versionHistory.premiumOnly", "Premium feature")
1854
+ ] })
1855
+ ] })
1856
+ ] })
1857
+ ] });
1858
+ };
1485
1859
  const Overlay = styled.div`
1486
1860
  position: fixed;
1487
1861
  top: 0;
@@ -1796,11 +2170,14 @@ const EditorJSGlobalStyles = createGlobalStyle`
1796
2170
 
1797
2171
  /* ============================================
1798
2172
  STRAPI MEDIA LIBRARY - Higher z-index for fullscreen
2173
+ Must be higher than fullscreen z-index (9999)
1799
2174
  ============================================ */
1800
2175
  [data-react-portal],
1801
2176
  .ReactModalPortal,
1802
2177
  [role="dialog"],
1803
2178
  [data-strapi-modal="true"],
2179
+ [class*="Dialog"],
2180
+ [class*="Modal"],
1804
2181
  .upload-dialog,
1805
2182
  [class*="Modal"],
1806
2183
  [class*="modal"],
@@ -2449,15 +2826,36 @@ const EditorContent = styled.div`
2449
2826
  flex: 1;
2450
2827
  overflow: visible; /* Allow toolbars/popovers to escape */
2451
2828
  position: relative;
2452
- padding: 24px 24px 24px 16px; /* Less left padding since toolbar has its own space */
2829
+ padding: 24px;
2453
2830
  min-height: 200px;
2454
2831
 
2455
2832
  ${(props) => props.$isFullscreen && css`
2456
- padding: clamp(32px, 4vw, 60px);
2833
+ padding: clamp(24px, 3vw, 48px);
2457
2834
  width: 100%;
2458
2835
  max-width: 100%;
2459
2836
  margin: 0;
2460
2837
  align-self: stretch;
2838
+
2839
+ /* Make blocks stretch full width in fullscreen */
2840
+ .codex-editor {
2841
+ width: 100%;
2842
+ }
2843
+
2844
+ .ce-block__content,
2845
+ .ce-toolbar__content {
2846
+ max-width: 100% !important;
2847
+ padding: 0 !important;
2848
+ }
2849
+
2850
+ .ce-toolbar {
2851
+ max-width: 100% !important;
2852
+ left: 0 !important;
2853
+ transform: none !important;
2854
+ }
2855
+
2856
+ .ce-toolbar__actions {
2857
+ right: 0 !important;
2858
+ }
2461
2859
  `}
2462
2860
  `;
2463
2861
  const EditorWrapper = styled.div`
@@ -2532,18 +2930,19 @@ const EditorWrapper = styled.div`
2532
2930
  TOOLBAR INSIDE EDITOR - Position Fix
2533
2931
  ============================================ */
2534
2932
 
2535
- /* Make the redactor (content area) have left padding for toolbar */
2933
+ /* Centered content area */
2536
2934
  .codex-editor__redactor {
2537
2935
  padding-bottom: 100px !important;
2538
- padding-left: 50px !important; /* Space for toolbar */
2539
- margin-left: 0 !important;
2936
+ padding-left: 0 !important;
2937
+ margin: 0 auto !important;
2938
+ max-width: 800px !important;
2540
2939
  }
2541
2940
 
2542
- /* Content blocks - full width within padded area */
2941
+ /* Content blocks - centered */
2543
2942
  .ce-block__content {
2544
2943
  max-width: 100%;
2545
- margin-left: 0;
2546
- margin-right: 0;
2944
+ margin: 0 auto;
2945
+ padding: 0 16px;
2547
2946
  }
2548
2947
 
2549
2948
  /* ============================================
@@ -2592,24 +2991,27 @@ const EditorWrapper = styled.div`
2592
2991
  border-radius: 6px;
2593
2992
  }
2594
2993
 
2595
- /* Toolbar positioning - inside the editor */
2994
+ /* Toolbar positioning - centered with content */
2596
2995
  .ce-toolbar__content {
2597
- max-width: 100%;
2598
- margin-left: 0;
2996
+ max-width: 800px;
2997
+ margin: 0 auto;
2998
+ padding: 0 16px;
2599
2999
  }
2600
3000
 
2601
3001
  .ce-toolbar {
2602
- left: 0 !important;
3002
+ left: 50% !important;
3003
+ transform: translateX(-50%) !important;
3004
+ width: 100% !important;
3005
+ max-width: 832px !important;
2603
3006
  }
2604
3007
 
2605
3008
  .ce-toolbar__plus {
2606
- left: 0 !important;
2607
3009
  position: relative !important;
2608
3010
  }
2609
3011
 
2610
3012
  .ce-toolbar__actions {
2611
- right: 0 !important;
2612
3013
  position: absolute !important;
3014
+ right: 16px !important;
2613
3015
  }
2614
3016
 
2615
3017
  /* Settings button (⋮⋮) */
@@ -3095,6 +3497,35 @@ const FooterButton = styled.button`
3095
3497
  }
3096
3498
  }
3097
3499
  `;
3500
+ const WebtoolsPromoLink = styled.a`
3501
+ display: inline-flex;
3502
+ align-items: center;
3503
+ gap: 6px;
3504
+ padding: 4px 10px;
3505
+ background: linear-gradient(135deg, rgba(99, 102, 241, 0.08) 0%, rgba(139, 92, 246, 0.08) 100%);
3506
+ border: 1px solid rgba(99, 102, 241, 0.2);
3507
+ border-radius: 6px;
3508
+ font-size: 11px;
3509
+ color: #6366f1;
3510
+ text-decoration: none;
3511
+ transition: all 0.2s ease;
3512
+ white-space: nowrap;
3513
+
3514
+ svg {
3515
+ width: 12px;
3516
+ height: 12px;
3517
+ }
3518
+
3519
+ &:hover {
3520
+ background: linear-gradient(135deg, rgba(99, 102, 241, 0.15) 0%, rgba(139, 92, 246, 0.15) 100%);
3521
+ border-color: rgba(99, 102, 241, 0.4);
3522
+ transform: translateY(-1px);
3523
+ }
3524
+
3525
+ @media (max-width: 768px) {
3526
+ display: none;
3527
+ }
3528
+ `;
3098
3529
  const LoadingOverlay = styled.div`
3099
3530
  position: absolute;
3100
3531
  top: 0;
@@ -3109,6 +3540,18 @@ const LoadingOverlay = styled.div`
3109
3540
  gap: 12px;
3110
3541
  z-index: 10;
3111
3542
  `;
3543
+ const VersionHistoryOverlay = styled.div`
3544
+ position: fixed;
3545
+ top: 0;
3546
+ left: 0;
3547
+ right: 0;
3548
+ bottom: 0;
3549
+ background: rgba(0, 0, 0, 0.5);
3550
+ display: flex;
3551
+ align-items: center;
3552
+ justify-content: center;
3553
+ z-index: 99999;
3554
+ `;
3112
3555
  const LoadingText = styled.span`
3113
3556
  font-size: 13px;
3114
3557
  color: #64748b;
@@ -3324,10 +3767,12 @@ const Editor = forwardRef(({
3324
3767
  }, ref) => {
3325
3768
  const { formatMessage } = useIntl();
3326
3769
  const t = (id, defaultMessage) => formatMessage({ id: getTranslation(id), defaultMessage });
3327
- const { licenseData } = useLicense();
3770
+ const { licenseData, tier: licenseTier } = useLicense();
3771
+ const { isAvailable: isWebtoolsAvailable, openLinkPicker: webtoolsOpenLinkPicker } = useWebtoolsLinks();
3328
3772
  const editorRef = useRef(null);
3329
3773
  const editorInstanceRef = useRef(null);
3330
3774
  const containerRef = useRef(null);
3775
+ const webtoolsSelectionRef = useRef({ text: "", range: null, blockIndex: -1, existingAnchor: null, existingHref: "" });
3331
3776
  const isReadyRef = useRef(false);
3332
3777
  const [isReady, setIsReady] = useState(false);
3333
3778
  const [showCreditsModal, setShowCreditsModal] = useState(false);
@@ -3363,6 +3808,51 @@ const Editor = forwardRef(({
3363
3808
  const [aiSelectedText, setAISelectedText] = useState("");
3364
3809
  const aiSelectionRangeRef = useRef(null);
3365
3810
  const [aiLoading, setAILoading] = useState(false);
3811
+ const [showVersionHistory, setShowVersionHistory] = useState(false);
3812
+ useEffect(() => {
3813
+ if (!isWebtoolsAvailable || !editorRef.current) return;
3814
+ const updateWebtoolsSelection = () => {
3815
+ const selection = window.getSelection();
3816
+ if (!selection || selection.rangeCount === 0) return;
3817
+ const range = selection.getRangeAt(0);
3818
+ if (!editorRef.current.contains(range.commonAncestorContainer)) return;
3819
+ const selectedText = selection.toString().trim();
3820
+ let existingAnchor = null;
3821
+ let existingHref = "";
3822
+ let node = range.commonAncestorContainer;
3823
+ while (node && node !== editorRef.current) {
3824
+ if (node.nodeName === "A") {
3825
+ existingAnchor = node;
3826
+ existingHref = node.href || "";
3827
+ break;
3828
+ }
3829
+ node = node.parentNode;
3830
+ }
3831
+ if (!existingAnchor) {
3832
+ node = range.startContainer;
3833
+ while (node && node !== editorRef.current) {
3834
+ if (node.nodeName === "A") {
3835
+ existingAnchor = node;
3836
+ existingHref = node.href || "";
3837
+ break;
3838
+ }
3839
+ node = node.parentNode;
3840
+ }
3841
+ }
3842
+ const blockIndex = editorInstanceRef.current?.blocks?.getCurrentBlockIndex?.() ?? -1;
3843
+ webtoolsSelectionRef.current = {
3844
+ text: existingAnchor ? existingAnchor.textContent : selectedText,
3845
+ range: range.cloneRange(),
3846
+ blockIndex,
3847
+ existingAnchor,
3848
+ existingHref
3849
+ };
3850
+ };
3851
+ document.addEventListener("selectionchange", updateWebtoolsSelection);
3852
+ return () => {
3853
+ document.removeEventListener("selectionchange", updateWebtoolsSelection);
3854
+ };
3855
+ }, [isWebtoolsAvailable, isReady]);
3366
3856
  const serializedInitialValue = useMemo(() => {
3367
3857
  if (!value) {
3368
3858
  return "";
@@ -3390,12 +3880,25 @@ const Editor = forwardRef(({
3390
3880
  const {
3391
3881
  doc: yDoc,
3392
3882
  blocksMap: yBlocksMap,
3883
+ textMap: yTextMap,
3884
+ // NEW: Y.Map<blockId, Y.Text> for character-level sync
3393
3885
  metaMap: yMetaMap,
3886
+ // Character-level text helpers
3887
+ getBlockText,
3888
+ // NEW: Get Y.Text for a block
3889
+ setBlockText,
3890
+ // NEW: Get block text as HTML
3891
+ // Utility functions
3892
+ htmlToDelta: collabHtmlToDelta,
3893
+ deltaToHtml: collabDeltaToHtml,
3894
+ // Connection status
3394
3895
  status: collabStatus,
3395
3896
  error: collabError,
3897
+ // Collaboration
3396
3898
  peers: collabPeers,
3397
3899
  awareness: collabAwareness,
3398
3900
  emitAwareness,
3901
+ // Role-based access control
3399
3902
  collabRole,
3400
3903
  canEdit: collabCanEdit
3401
3904
  } = useMagicCollaboration({
@@ -3410,6 +3913,171 @@ const Editor = forwardRef(({
3410
3913
  }
3411
3914
  }
3412
3915
  });
3916
+ const yTextBindingsRef = useRef(/* @__PURE__ */ new Map());
3917
+ const bindBlockToYText = useCallback((blockId, element) => {
3918
+ if (!collabEnabled || !blockId || !element || !yTextMap) return;
3919
+ if (yTextBindingsRef.current.has(blockId)) {
3920
+ const existing = yTextBindingsRef.current.get(blockId);
3921
+ if (existing.element === element) return;
3922
+ unbindBlockFromYText(blockId);
3923
+ }
3924
+ const ytext = getBlockText(blockId);
3925
+ if (!ytext) return;
3926
+ let isUpdating = false;
3927
+ const ytextObserver = (event) => {
3928
+ if (isUpdating) return;
3929
+ if (event.transaction.local) return;
3930
+ isUpdating = true;
3931
+ try {
3932
+ const selection = window.getSelection();
3933
+ let cursorOffset = 0;
3934
+ if (selection && selection.rangeCount > 0 && element.contains(selection.anchorNode)) {
3935
+ const range = selection.getRangeAt(0);
3936
+ const preCaretRange = document.createRange();
3937
+ preCaretRange.selectNodeContents(element);
3938
+ preCaretRange.setEnd(range.startContainer, range.startOffset);
3939
+ cursorOffset = preCaretRange.toString().length;
3940
+ }
3941
+ const html = collabDeltaToHtml(ytext.toDelta());
3942
+ if (element.innerHTML !== html) {
3943
+ element.innerHTML = html || "";
3944
+ }
3945
+ if (document.activeElement === element && cursorOffset > 0) {
3946
+ try {
3947
+ const textNode = element.firstChild;
3948
+ if (textNode && textNode.nodeType === Node.TEXT_NODE) {
3949
+ const newRange = document.createRange();
3950
+ const pos = Math.min(cursorOffset, textNode.length);
3951
+ newRange.setStart(textNode, pos);
3952
+ newRange.setEnd(textNode, pos);
3953
+ selection.removeAllRanges();
3954
+ selection.addRange(newRange);
3955
+ }
3956
+ } catch (e) {
3957
+ }
3958
+ }
3959
+ } finally {
3960
+ isUpdating = false;
3961
+ }
3962
+ };
3963
+ const inputHandler = () => {
3964
+ if (isUpdating) return;
3965
+ isUpdating = true;
3966
+ try {
3967
+ const html = element.innerHTML;
3968
+ const newDelta = collabHtmlToDelta(html);
3969
+ const currentDelta = ytext.toDelta();
3970
+ yDoc.transact(() => {
3971
+ if (ytext.length > 0) {
3972
+ ytext.delete(0, ytext.length);
3973
+ }
3974
+ if (newDelta.length > 0) {
3975
+ ytext.applyDelta(newDelta);
3976
+ }
3977
+ }, "local");
3978
+ } finally {
3979
+ isUpdating = false;
3980
+ }
3981
+ };
3982
+ ytext.observe(ytextObserver);
3983
+ element.addEventListener("input", inputHandler);
3984
+ yTextBindingsRef.current.set(blockId, {
3985
+ ytext,
3986
+ element,
3987
+ ytextObserver,
3988
+ inputHandler
3989
+ });
3990
+ console.log("[Magic Editor X] [CHAR-SYNC] Bound block to Y.Text:", blockId);
3991
+ }, [collabEnabled, yTextMap, yDoc, getBlockText, collabHtmlToDelta, collabDeltaToHtml]);
3992
+ const unbindBlockFromYText = useCallback((blockId) => {
3993
+ const binding = yTextBindingsRef.current.get(blockId);
3994
+ if (!binding) return;
3995
+ binding.ytext.unobserve(binding.ytextObserver);
3996
+ binding.element.removeEventListener("input", binding.inputHandler);
3997
+ yTextBindingsRef.current.delete(blockId);
3998
+ console.log("[Magic Editor X] [CHAR-SYNC] Unbound block from Y.Text:", blockId);
3999
+ }, []);
4000
+ const bindAllBlocksToYText = useCallback(() => {
4001
+ if (!collabEnabled || !editorInstanceRef.current || !yTextMap) return;
4002
+ const editor = editorInstanceRef.current;
4003
+ const blockCount = editor.blocks.getBlocksCount();
4004
+ console.log("[Magic Editor X] [CHAR-SYNC] Binding", blockCount, "blocks to Y.Text");
4005
+ for (let i = 0; i < blockCount; i++) {
4006
+ try {
4007
+ const block = editor.blocks.getBlockByIndex(i);
4008
+ if (!block || !block.id) continue;
4009
+ const blockHolder = block.holder;
4010
+ if (!blockHolder) continue;
4011
+ const contentEditable = blockHolder.querySelector('[contenteditable="true"]');
4012
+ if (contentEditable) {
4013
+ const ytext = getBlockText(block.id);
4014
+ if (ytext && ytext.length === 0) {
4015
+ const currentHtml = contentEditable.innerHTML;
4016
+ if (currentHtml && currentHtml !== "<br>") {
4017
+ setBlockText(block.id, currentHtml);
4018
+ }
4019
+ }
4020
+ bindBlockToYText(block.id, contentEditable);
4021
+ }
4022
+ } catch (e) {
4023
+ console.warn("[Magic Editor X] [CHAR-SYNC] Error binding block:", e);
4024
+ }
4025
+ }
4026
+ }, [collabEnabled, yTextMap, getBlockText, setBlockText, bindBlockToYText]);
4027
+ const blockObserverRef = useRef(null);
4028
+ useEffect(() => {
4029
+ if (!collabEnabled || !editorRef.current || !yTextMap) return;
4030
+ const observer = new MutationObserver((mutations) => {
4031
+ let hasNewBlocks = false;
4032
+ for (const mutation of mutations) {
4033
+ if (mutation.type === "childList" && mutation.addedNodes.length > 0) {
4034
+ for (const node of mutation.addedNodes) {
4035
+ if (node.nodeType === Node.ELEMENT_NODE && (node.classList?.contains("ce-block") || node.querySelector?.(".ce-block"))) {
4036
+ hasNewBlocks = true;
4037
+ break;
4038
+ }
4039
+ }
4040
+ }
4041
+ if (hasNewBlocks) break;
4042
+ }
4043
+ if (hasNewBlocks) {
4044
+ setTimeout(() => {
4045
+ bindAllBlocksToYText();
4046
+ }, 50);
4047
+ }
4048
+ });
4049
+ observer.observe(editorRef.current, {
4050
+ childList: true,
4051
+ subtree: true
4052
+ });
4053
+ blockObserverRef.current = observer;
4054
+ return () => {
4055
+ observer.disconnect();
4056
+ blockObserverRef.current = null;
4057
+ };
4058
+ }, [collabEnabled, yTextMap, bindAllBlocksToYText]);
4059
+ useEffect(() => {
4060
+ return () => {
4061
+ yTextBindingsRef.current.forEach((binding, blockId) => {
4062
+ binding.ytext.unobserve(binding.ytextObserver);
4063
+ binding.element.removeEventListener("input", binding.inputHandler);
4064
+ });
4065
+ yTextBindingsRef.current.clear();
4066
+ };
4067
+ }, []);
4068
+ const {
4069
+ snapshots,
4070
+ loading: versionHistoryLoading,
4071
+ error: versionHistoryError,
4072
+ fetchSnapshots,
4073
+ restoreSnapshot,
4074
+ createSnapshot
4075
+ } = useVersionHistory();
4076
+ useEffect(() => {
4077
+ if (showVersionHistory && collabRoomId) {
4078
+ fetchSnapshots(collabRoomId);
4079
+ }
4080
+ }, [showVersionHistory, collabRoomId, fetchSnapshots]);
3413
4081
  useMemo(() => {
3414
4082
  switch (collabRole) {
3415
4083
  case "viewer":
@@ -3743,23 +4411,15 @@ const Editor = forwardRef(({
3743
4411
  setWordCount(plainText.split(/\s+/).filter((w) => w.length > 0).length);
3744
4412
  }, []);
3745
4413
  const renderFromYDoc = useCallback(async () => {
3746
- 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(() => {
3747
- });
3748
4414
  if (!collabEnabled || !yBlocksMap || !yDoc) {
3749
- 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(() => {
3750
- });
3751
4415
  return;
3752
4416
  }
3753
4417
  const editor = editorInstanceRef.current;
3754
4418
  if (!editor || !isReadyRef.current) {
3755
- 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(() => {
3756
- });
3757
4419
  pendingRenderRef.current = pendingRenderRef.current || true;
3758
4420
  return;
3759
4421
  }
3760
4422
  if (isApplyingRemoteRef.current) {
3761
- 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(() => {
3762
- });
3763
4423
  return;
3764
4424
  }
3765
4425
  isApplyingRemoteRef.current = true;
@@ -3777,29 +4437,35 @@ const Editor = forwardRef(({
3777
4437
  yOrder = Array.from(yBlocksMap.keys());
3778
4438
  }
3779
4439
  const yBlocks = [];
3780
- 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(() => {
3781
- });
3782
4440
  yOrder.forEach((id) => {
3783
4441
  const json = yBlocksMap.get(id);
3784
4442
  if (json) {
3785
4443
  try {
3786
- const block = JSON.parse(json);
3787
- yBlocks.push(block);
4444
+ const blockData = JSON.parse(json);
4445
+ if (blockData.type && !blockData.data) {
4446
+ const ytext = yTextMap?.get(id);
4447
+ const textContent = ytext ? collabDeltaToHtml(ytext.toDelta()) : "";
4448
+ yBlocks.push({
4449
+ id,
4450
+ type: blockData.type,
4451
+ data: { text: textContent },
4452
+ tunes: blockData.tunes || {}
4453
+ });
4454
+ } else if (blockData.type && blockData.data) {
4455
+ yBlocks.push({
4456
+ id,
4457
+ ...blockData
4458
+ });
4459
+ }
3788
4460
  } catch (e) {
3789
4461
  console.warn("[Magic Editor X] Invalid block JSON:", id);
3790
4462
  }
3791
4463
  }
3792
4464
  });
3793
- 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(() => {
3794
- });
3795
4465
  const parsed = { blocks: yBlocks };
3796
4466
  const normalizedParsed = serializeForCompare(parsed);
3797
4467
  const renderFull = async () => {
3798
- 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(() => {
3799
- });
3800
4468
  await editor.render(parsed);
3801
- 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(() => {
3802
- });
3803
4469
  lastSerializedValueRef.current = normalizedParsed;
3804
4470
  setBlocksCount(yBlocks.length);
3805
4471
  calculateStats(parsed);
@@ -3812,8 +4478,6 @@ const Editor = forwardRef(({
3812
4478
  currentBlocks.push({ id: block.id, index: i });
3813
4479
  }
3814
4480
  }
3815
- 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(() => {
3816
- });
3817
4481
  if (blockCount !== yBlocks.length) {
3818
4482
  console.log("[Magic Editor X] [SYNC] Structural change detected (count mismatch). Falling back to full render.");
3819
4483
  await renderFull();
@@ -4018,7 +4682,7 @@ const Editor = forwardRef(({
4018
4682
  const emptyPayload = JSON.stringify({ blocks: [] });
4019
4683
  lastSerializedValueRef.current = emptyPayload;
4020
4684
  pushLocalToCollab(emptyPayload);
4021
- onChange({ target: { name, value: null, type: "text" } });
4685
+ onChange({ target: { name, value: null, type: "json" } });
4022
4686
  setBlocksCount(0);
4023
4687
  setWordCount(0);
4024
4688
  setCharCount(0);
@@ -4031,7 +4695,11 @@ const Editor = forwardRef(({
4031
4695
  }, [isReady]);
4032
4696
  useEffect(() => {
4033
4697
  if (editorRef.current && !editorInstanceRef.current) {
4034
- const tools = getTools({ mediaLibToggleFunc, pluginId: PLUGIN_ID });
4698
+ const tools = getTools({
4699
+ mediaLibToggleFunc,
4700
+ pluginId: PLUGIN_ID,
4701
+ openLinkPicker: isWebtoolsAvailable ? webtoolsOpenLinkPicker : null
4702
+ });
4035
4703
  let initialData = void 0;
4036
4704
  if (value) {
4037
4705
  try {
@@ -4097,6 +4765,11 @@ const Editor = forwardRef(({
4097
4765
  }
4098
4766
  pendingRenderRef.current = null;
4099
4767
  }
4768
+ if (collabEnabled && yTextMap) {
4769
+ setTimeout(() => {
4770
+ bindAllBlocksToYText();
4771
+ }, 100);
4772
+ }
4100
4773
  },
4101
4774
  onChange: async (api) => {
4102
4775
  try {
@@ -4116,9 +4789,9 @@ const Editor = forwardRef(({
4116
4789
  pushLocalToCollabRef.current?.(docPayload);
4117
4790
  lastSerializedValueRef.current = normalized;
4118
4791
  if (count === 0) {
4119
- onChange({ target: { name, value: null, type: "text" } });
4792
+ onChange({ target: { name, value: null, type: "json" } });
4120
4793
  } else {
4121
- onChange({ target: { name, value: serialized, type: "text" } });
4794
+ onChange({ target: { name, value: outputData, type: "json" } });
4122
4795
  }
4123
4796
  } catch (error2) {
4124
4797
  console.error("[Magic Editor X] Error in onChange:", error2);
@@ -4241,6 +4914,96 @@ const Editor = forwardRef(({
4241
4914
  children: /* @__PURE__ */ jsx(SparklesIcon$1, {})
4242
4915
  }
4243
4916
  ),
4917
+ isWebtoolsAvailable && /* @__PURE__ */ jsx(
4918
+ ToolButton,
4919
+ {
4920
+ type: "button",
4921
+ "data-tooltip": "Webtools Link Picker",
4922
+ onClick: async () => {
4923
+ if (!editorInstanceRef.current || !isReady) {
4924
+ console.warn("[Magic Editor X] Editor not ready");
4925
+ return;
4926
+ }
4927
+ const editor = editorInstanceRef.current;
4928
+ const {
4929
+ text: selectedText,
4930
+ range: savedRange,
4931
+ blockIndex,
4932
+ existingAnchor,
4933
+ existingHref
4934
+ } = webtoolsSelectionRef.current;
4935
+ console.log("[Magic Editor X] Webtools button clicked with stored selection:", {
4936
+ text: selectedText || "(none)",
4937
+ existingHref: existingHref || "(new link)",
4938
+ hasRange: !!savedRange,
4939
+ blockIndex
4940
+ });
4941
+ const currentBlockIndex = blockIndex >= 0 ? blockIndex : editor.blocks.getCurrentBlockIndex();
4942
+ const result = await webtoolsOpenLinkPicker({
4943
+ initialText: selectedText || "",
4944
+ initialHref: existingHref || ""
4945
+ });
4946
+ if (result && result.href) {
4947
+ const linkText = result.label || selectedText || result.href;
4948
+ const linkHtml = `<a href="${result.href}" target="_blank" rel="noopener noreferrer">${linkText}</a>`;
4949
+ if (existingAnchor && existingAnchor.parentNode) {
4950
+ try {
4951
+ existingAnchor.href = result.href;
4952
+ existingAnchor.textContent = linkText;
4953
+ const contentEditable = existingAnchor.closest('[contenteditable="true"]');
4954
+ if (contentEditable) {
4955
+ contentEditable.dispatchEvent(new Event("input", { bubbles: true }));
4956
+ }
4957
+ console.log("[Magic Editor X] Webtools link UPDATED:", {
4958
+ oldHref: existingHref,
4959
+ newHref: result.href,
4960
+ text: linkText
4961
+ });
4962
+ } catch (e) {
4963
+ console.error("[Magic Editor X] Failed to update link:", e);
4964
+ }
4965
+ } else if (savedRange && selectedText && currentBlockIndex >= 0) {
4966
+ try {
4967
+ const blockHolder = editor.blocks.getBlockByIndex(currentBlockIndex)?.holder;
4968
+ const contentEditable = blockHolder?.querySelector('[contenteditable="true"]');
4969
+ if (contentEditable) {
4970
+ const selection = window.getSelection();
4971
+ selection.removeAllRanges();
4972
+ selection.addRange(savedRange);
4973
+ document.execCommand("insertHTML", false, linkHtml);
4974
+ contentEditable.dispatchEvent(new Event("input", { bubbles: true }));
4975
+ console.log("[Magic Editor X] Webtools link CREATED:", {
4976
+ text: linkText,
4977
+ href: result.href
4978
+ });
4979
+ } else {
4980
+ editor.blocks.insert("paragraph", { text: linkHtml }, {}, currentBlockIndex + 1, true);
4981
+ }
4982
+ } catch (e) {
4983
+ console.error("[Magic Editor X] Failed to insert link:", e);
4984
+ editor.blocks.insert("paragraph", { text: linkHtml }, {}, currentBlockIndex + 1, true);
4985
+ }
4986
+ } else if (currentBlockIndex >= 0) {
4987
+ editor.blocks.insert("paragraph", {
4988
+ text: linkHtml
4989
+ }, {}, currentBlockIndex + 1, true);
4990
+ editor.caret.setToBlock(currentBlockIndex + 1);
4991
+ console.log("[Magic Editor X] Webtools link inserted (no selection):", result);
4992
+ } else {
4993
+ editor.blocks.insert("paragraph", { text: linkHtml });
4994
+ }
4995
+ }
4996
+ webtoolsSelectionRef.current = { text: "", range: null, blockIndex: -1, existingAnchor: null, existingHref: "" };
4997
+ },
4998
+ disabled: collabEnabled && collabCanEdit === false,
4999
+ style: {
5000
+ background: "linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)",
5001
+ color: "white",
5002
+ ...collabEnabled && collabCanEdit === false ? { opacity: 0.4, cursor: "not-allowed" } : {}
5003
+ },
5004
+ children: /* @__PURE__ */ jsx(LinkIcon, {})
5005
+ }
5006
+ ),
4244
5007
  /* @__PURE__ */ jsx(ToolbarDivider, {}),
4245
5008
  /* @__PURE__ */ jsx(
4246
5009
  ToolButton,
@@ -4356,9 +5119,26 @@ const Editor = forwardRef(({
4356
5119
  /* @__PURE__ */ jsx("strong", { children: charCount }),
4357
5120
  " ",
4358
5121
  t("editor.characters", "Zeichen")
4359
- ] })
5122
+ ] }),
5123
+ !isWebtoolsAvailable && /* @__PURE__ */ jsxs(
5124
+ WebtoolsPromoLink,
5125
+ {
5126
+ href: "https://www.pluginpal.io/plugin/webtools",
5127
+ target: "_blank",
5128
+ rel: "noopener noreferrer",
5129
+ title: "Get Webtools Links addon for internal link management",
5130
+ children: [
5131
+ /* @__PURE__ */ jsx(LinkIcon, {}),
5132
+ "Internal Links? Get Webtools"
5133
+ ]
5134
+ }
5135
+ )
4360
5136
  ] }),
4361
5137
  /* @__PURE__ */ jsxs(FooterRight, { children: [
5138
+ /* @__PURE__ */ jsxs(FooterButton, { type: "button", onClick: () => setShowVersionHistory(true), children: [
5139
+ /* @__PURE__ */ jsx(ClockIcon, {}),
5140
+ t("editor.versionHistory", "History")
5141
+ ] }),
4362
5142
  !(collabEnabled && collabCanEdit === false) && /* @__PURE__ */ jsxs(FooterButton, { type: "button", onClick: () => handleInsertBlock("mediaLib"), children: [
4363
5143
  /* @__PURE__ */ jsx(PhotoIcon, {}),
4364
5144
  t("editor.mediaLibrary", "Media Library")
@@ -4431,6 +5211,51 @@ const Editor = forwardRef(({
4431
5211
  }
4432
5212
  }
4433
5213
  ),
5214
+ showVersionHistory && /* @__PURE__ */ jsx(VersionHistoryOverlay, { onClick: () => setShowVersionHistory(false), children: /* @__PURE__ */ jsx("div", { onClick: (e) => e.stopPropagation(), children: /* @__PURE__ */ jsx(
5215
+ VersionHistoryPanel,
5216
+ {
5217
+ snapshots,
5218
+ loading: versionHistoryLoading,
5219
+ error: versionHistoryError,
5220
+ tier: licenseTier,
5221
+ onClose: () => setShowVersionHistory(false),
5222
+ onRestore: async (snapshot) => {
5223
+ if (snapshot.documentId && editorInstanceRef.current && isReady) {
5224
+ try {
5225
+ const result = await restoreSnapshot(snapshot.documentId, collabRoomId);
5226
+ const contentToRestore = result?.jsonContent || snapshot.jsonContent;
5227
+ if (contentToRestore && editorInstanceRef.current) {
5228
+ await editorInstanceRef.current.render(contentToRestore);
5229
+ setShowVersionHistory(false);
5230
+ onChange({ target: { name, value: contentToRestore, type: "json" } });
5231
+ }
5232
+ } catch (err) {
5233
+ console.error("[Magic Editor X] Failed to restore snapshot:", err?.message);
5234
+ }
5235
+ }
5236
+ },
5237
+ onCreate: async () => {
5238
+ if (collabRoomId && editorInstanceRef.current && isReady) {
5239
+ const [contentType, entryId, fieldName] = collabRoomId.split("|");
5240
+ if (contentType && entryId && fieldName) {
5241
+ try {
5242
+ const editorContent = await editorInstanceRef.current.save();
5243
+ await createSnapshot({
5244
+ roomId: collabRoomId,
5245
+ contentType,
5246
+ entryId,
5247
+ fieldName,
5248
+ content: editorContent
5249
+ });
5250
+ fetchSnapshots(collabRoomId);
5251
+ } catch (err) {
5252
+ console.error("[Magic Editor X] Failed to create snapshot:", err?.message);
5253
+ }
5254
+ }
5255
+ }
5256
+ }
5257
+ }
5258
+ ) }) }),
4434
5259
  /* @__PURE__ */ jsx(
4435
5260
  CreditsModal,
4436
5261
  {