magic-editor-x 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +890 -0
  3. package/dist/_chunks/App-B1FgOsWa.mjs +2143 -0
  4. package/dist/_chunks/App-mtrlABtd.js +2146 -0
  5. package/dist/_chunks/LicensePage-BnyWSrWs.js +375 -0
  6. package/dist/_chunks/LicensePage-CWH-AFR-.mjs +373 -0
  7. package/dist/_chunks/LiveCollaborationPanel-DbDHwr2C.js +222 -0
  8. package/dist/_chunks/LiveCollaborationPanel-ryjcDAA7.mjs +220 -0
  9. package/dist/_chunks/Settings-Bk9bxJTy.js +440 -0
  10. package/dist/_chunks/Settings-D-V2MLVm.mjs +438 -0
  11. package/dist/_chunks/de-CSrHZWEb.mjs +295 -0
  12. package/dist/_chunks/de-CzSo1oD2.js +295 -0
  13. package/dist/_chunks/en-DuQun2v4.mjs +295 -0
  14. package/dist/_chunks/en-DxIkVPUh.js +295 -0
  15. package/dist/_chunks/es-DAQ_97zx.js +273 -0
  16. package/dist/_chunks/es-DEB0CA8S.mjs +273 -0
  17. package/dist/_chunks/fr-Bqkhvdx2.mjs +273 -0
  18. package/dist/_chunks/fr-ChPabvNP.js +273 -0
  19. package/dist/_chunks/getTranslation-C4uWR0DB.mjs +50985 -0
  20. package/dist/_chunks/getTranslation-D35vbDap.js +51001 -0
  21. package/dist/_chunks/index-B5MzUyo0.mjs +2541 -0
  22. package/dist/_chunks/index-BRVqbnOb.mjs +4450 -0
  23. package/dist/_chunks/index-BiLy_f7C.js +2540 -0
  24. package/dist/_chunks/index-CQx7-dFP.js +4472 -0
  25. package/dist/_chunks/pt-BMoYltav.mjs +273 -0
  26. package/dist/_chunks/pt-Cm74LpyZ.js +273 -0
  27. package/dist/_chunks/tools-CjnQJ9w2.mjs +2155 -0
  28. package/dist/_chunks/tools-DNt2tioN.js +2186 -0
  29. package/dist/admin/index.js +3 -0
  30. package/dist/admin/index.mjs +4 -0
  31. package/dist/server/index.js +2554 -0
  32. package/dist/server/index.mjs +2544 -0
  33. package/dist/style.css +164 -0
  34. package/package.json +122 -0
  35. package/pics/collab-magiceditorX.png +0 -0
  36. package/pics/editorX.png +0 -0
  37. package/pics/liveCollabwidget1.png +0 -0
@@ -0,0 +1,4450 @@
1
+ import { jsx, jsxs, Fragment } from "react/jsx-runtime";
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";
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";
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";
8
+ import { useStrapiApp, useFetchClient, useAuth } from "@strapi/strapi/admin";
9
+ import { P as PLUGIN_ID } from "./index-B5MzUyo0.mjs";
10
+ import { io } from "socket.io-client";
11
+ import * as Y from "yjs";
12
+ import { IndexeddbPersistence } from "y-indexeddb";
13
+ import { useNavigate } from "react-router-dom";
14
+ const getBackendUrl = () => {
15
+ return window.location.origin;
16
+ };
17
+ const prefixFileUrl = (url) => {
18
+ if (!url) return url;
19
+ if (url.startsWith("http://") || url.startsWith("https://")) {
20
+ return url;
21
+ }
22
+ return `${getBackendUrl()}${url}`;
23
+ };
24
+ const MediaLibComponent = ({
25
+ isOpen = false,
26
+ onChange = () => {
27
+ },
28
+ onToggle = () => {
29
+ },
30
+ allowedTypes = ["images", "files", "videos", "audios"],
31
+ multiple = true
32
+ }) => {
33
+ const [isLoading, setIsLoading] = useState(true);
34
+ const [retryCount, setRetryCount] = useState(0);
35
+ const components = useStrapiApp("MediaLibComponent", (state) => state.components);
36
+ const MediaLibraryDialog = components?.["media-library"];
37
+ useEffect(() => {
38
+ if (isOpen && !MediaLibraryDialog && retryCount < 5) {
39
+ const timer = setTimeout(() => {
40
+ setRetryCount((prev) => prev + 1);
41
+ }, 200);
42
+ return () => clearTimeout(timer);
43
+ }
44
+ setIsLoading(false);
45
+ }, [isOpen, MediaLibraryDialog, retryCount]);
46
+ useEffect(() => {
47
+ if (!isOpen) {
48
+ setRetryCount(0);
49
+ setIsLoading(true);
50
+ }
51
+ }, [isOpen]);
52
+ useEffect(() => {
53
+ if (isOpen) {
54
+ console.log("[Magic Editor X] MediaLib opened");
55
+ console.log("[Magic Editor X] Available components:", Object.keys(components || {}));
56
+ console.log("[Magic Editor X] MediaLibraryDialog:", MediaLibraryDialog ? "✅ Found" : "❌ Not found");
57
+ console.log("[Magic Editor X] Retry count:", retryCount);
58
+ }
59
+ }, [isOpen, components, MediaLibraryDialog, retryCount]);
60
+ const handleSelectAssets = useCallback((files) => {
61
+ console.log("[Magic Editor X] Selected assets:", files);
62
+ if (!files || files.length === 0) {
63
+ onToggle();
64
+ return;
65
+ }
66
+ const formattedFiles = files.map((file) => ({
67
+ alt: file.alternativeText || file.name || "",
68
+ url: prefixFileUrl(file.url),
69
+ width: file.width,
70
+ height: file.height,
71
+ size: file.size,
72
+ mime: file.mime,
73
+ formats: file.formats,
74
+ name: file.name,
75
+ caption: file.caption || "",
76
+ id: file.id,
77
+ documentId: file.documentId
78
+ }));
79
+ console.log("[Magic Editor X] Formatted files:", formattedFiles);
80
+ onChange(formattedFiles);
81
+ }, [onChange, onToggle]);
82
+ const handleClose = useCallback(() => {
83
+ console.log("[Magic Editor X] MediaLib closing");
84
+ onToggle();
85
+ }, [onToggle]);
86
+ if (!isOpen) {
87
+ return null;
88
+ }
89
+ if (isLoading && !MediaLibraryDialog && retryCount < 5) {
90
+ return /* @__PURE__ */ jsx(
91
+ "div",
92
+ {
93
+ style: {
94
+ position: "fixed",
95
+ top: 0,
96
+ left: 0,
97
+ right: 0,
98
+ bottom: 0,
99
+ backgroundColor: "rgba(0, 0, 0, 0.7)",
100
+ display: "flex",
101
+ alignItems: "center",
102
+ justifyContent: "center",
103
+ zIndex: 1e5
104
+ // Higher than fullscreen (9999)
105
+ },
106
+ children: /* @__PURE__ */ jsxs(
107
+ "div",
108
+ {
109
+ style: {
110
+ backgroundColor: "white",
111
+ padding: "40px",
112
+ borderRadius: "16px",
113
+ textAlign: "center",
114
+ maxWidth: "400px",
115
+ boxShadow: "0 20px 40px rgba(0, 0, 0, 0.2)"
116
+ },
117
+ children: [
118
+ /* @__PURE__ */ jsx("div", { style: {
119
+ width: "48px",
120
+ height: "48px",
121
+ margin: "0 auto 20px",
122
+ border: "4px solid #e2e8f0",
123
+ borderTop: "4px solid #7C3AED",
124
+ borderRadius: "50%",
125
+ animation: "spin 1s linear infinite"
126
+ } }),
127
+ /* @__PURE__ */ jsx("style", { children: `
128
+ @keyframes spin {
129
+ 0% { transform: rotate(0deg); }
130
+ 100% { transform: rotate(360deg); }
131
+ }
132
+ ` }),
133
+ /* @__PURE__ */ jsx("p", { style: { fontSize: "14px", color: "#64748b" }, children: "Media Library wird geladen..." })
134
+ ]
135
+ }
136
+ )
137
+ }
138
+ );
139
+ }
140
+ if (!MediaLibraryDialog) {
141
+ console.warn("[Magic Editor X] Media Library component not available after retries");
142
+ console.log("[Magic Editor X] Registered components:", Object.keys(components || {}));
143
+ return /* @__PURE__ */ jsx(
144
+ "div",
145
+ {
146
+ style: {
147
+ position: "fixed",
148
+ top: 0,
149
+ left: 0,
150
+ right: 0,
151
+ bottom: 0,
152
+ backgroundColor: "rgba(0, 0, 0, 0.7)",
153
+ display: "flex",
154
+ alignItems: "center",
155
+ justifyContent: "center",
156
+ zIndex: 1e5
157
+ // Higher than fullscreen (9999)
158
+ },
159
+ onClick: handleClose,
160
+ children: /* @__PURE__ */ jsxs(
161
+ "div",
162
+ {
163
+ style: {
164
+ backgroundColor: "white",
165
+ padding: "40px",
166
+ borderRadius: "16px",
167
+ textAlign: "center",
168
+ maxWidth: "400px",
169
+ boxShadow: "0 20px 40px rgba(0, 0, 0, 0.2)"
170
+ },
171
+ onClick: (e) => e.stopPropagation(),
172
+ children: [
173
+ /* @__PURE__ */ jsx("div", { style: {
174
+ width: "64px",
175
+ height: "64px",
176
+ margin: "0 auto 20px",
177
+ background: "linear-gradient(135deg, #fee2e2 0%, #fecaca 100%)",
178
+ borderRadius: "50%",
179
+ display: "flex",
180
+ alignItems: "center",
181
+ justifyContent: "center"
182
+ }, children: /* @__PURE__ */ jsxs("svg", { width: "32", height: "32", viewBox: "0 0 24 24", fill: "none", stroke: "#ef4444", strokeWidth: "2", children: [
183
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "10" }),
184
+ /* @__PURE__ */ jsx("line", { x1: "12", y1: "8", x2: "12", y2: "12" }),
185
+ /* @__PURE__ */ jsx("line", { x1: "12", y1: "16", x2: "12.01", y2: "16" })
186
+ ] }) }),
187
+ /* @__PURE__ */ jsx("h3", { style: {
188
+ fontSize: "18px",
189
+ fontWeight: "600",
190
+ color: "#1e293b",
191
+ marginBottom: "12px"
192
+ }, children: "Media Library nicht verfügbar" }),
193
+ /* @__PURE__ */ jsx("p", { style: {
194
+ fontSize: "14px",
195
+ color: "#64748b",
196
+ marginBottom: "16px",
197
+ lineHeight: "1.5"
198
+ }, children: "Die Strapi Media Library konnte nicht geladen werden." }),
199
+ /* @__PURE__ */ jsxs("p", { style: {
200
+ fontSize: "12px",
201
+ color: "#94a3b8",
202
+ marginBottom: "24px",
203
+ background: "#f1f5f9",
204
+ padding: "8px 12px",
205
+ borderRadius: "6px",
206
+ fontFamily: "monospace"
207
+ }, children: [
208
+ "Verfügbare Komponenten: ",
209
+ Object.keys(components || {}).join(", ") || "Keine"
210
+ ] }),
211
+ /* @__PURE__ */ jsx(
212
+ "button",
213
+ {
214
+ onClick: handleClose,
215
+ style: {
216
+ padding: "12px 24px",
217
+ background: "linear-gradient(135deg, #7C3AED 0%, #6d28d9 100%)",
218
+ color: "white",
219
+ border: "none",
220
+ borderRadius: "8px",
221
+ fontSize: "14px",
222
+ fontWeight: "600",
223
+ cursor: "pointer",
224
+ transition: "all 0.2s ease"
225
+ },
226
+ onMouseOver: (e) => e.target.style.transform = "translateY(-1px)",
227
+ onMouseOut: (e) => e.target.style.transform = "translateY(0)",
228
+ children: "Schließen"
229
+ }
230
+ )
231
+ ]
232
+ }
233
+ )
234
+ }
235
+ );
236
+ }
237
+ return /* @__PURE__ */ jsx(MediaLibErrorBoundary, { onClose: handleClose, children: /* @__PURE__ */ jsx(
238
+ MediaLibraryDialog,
239
+ {
240
+ allowedTypes,
241
+ onClose: handleClose,
242
+ onSelectAssets: handleSelectAssets,
243
+ multiple
244
+ }
245
+ ) });
246
+ };
247
+ class MediaLibErrorBoundary extends React__default.Component {
248
+ constructor(props) {
249
+ super(props);
250
+ this.state = { hasError: false, error: null };
251
+ }
252
+ static getDerivedStateFromError(error) {
253
+ return { hasError: true, error };
254
+ }
255
+ componentDidCatch(error, errorInfo) {
256
+ console.error("[Magic Editor X] Media Library error:", error, errorInfo);
257
+ }
258
+ render() {
259
+ if (this.state.hasError) {
260
+ return /* @__PURE__ */ jsx(
261
+ "div",
262
+ {
263
+ style: {
264
+ position: "fixed",
265
+ top: 0,
266
+ left: 0,
267
+ right: 0,
268
+ bottom: 0,
269
+ backgroundColor: "rgba(0, 0, 0, 0.7)",
270
+ display: "flex",
271
+ alignItems: "center",
272
+ justifyContent: "center",
273
+ zIndex: 1e5
274
+ },
275
+ onClick: this.props.onClose,
276
+ children: /* @__PURE__ */ jsxs(
277
+ "div",
278
+ {
279
+ style: {
280
+ backgroundColor: "white",
281
+ padding: "40px",
282
+ borderRadius: "16px",
283
+ textAlign: "center",
284
+ maxWidth: "450px",
285
+ boxShadow: "0 20px 40px rgba(0, 0, 0, 0.2)"
286
+ },
287
+ onClick: (e) => e.stopPropagation(),
288
+ children: [
289
+ /* @__PURE__ */ jsx("div", { style: {
290
+ width: "64px",
291
+ height: "64px",
292
+ margin: "0 auto 20px",
293
+ background: "linear-gradient(135deg, #fef3c7 0%, #fde68a 100%)",
294
+ borderRadius: "50%",
295
+ display: "flex",
296
+ alignItems: "center",
297
+ justifyContent: "center"
298
+ }, children: /* @__PURE__ */ jsx("svg", { width: "32", height: "32", viewBox: "0 0 24 24", fill: "none", stroke: "#d97706", strokeWidth: "2", children: /* @__PURE__ */ jsx("path", { d: "M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" }) }) }),
299
+ /* @__PURE__ */ jsx("h3", { style: {
300
+ fontSize: "18px",
301
+ fontWeight: "600",
302
+ color: "#1e293b",
303
+ marginBottom: "12px"
304
+ }, children: "Keine Upload-Berechtigung" }),
305
+ /* @__PURE__ */ jsxs("p", { style: {
306
+ fontSize: "14px",
307
+ color: "#64748b",
308
+ marginBottom: "20px",
309
+ lineHeight: "1.6"
310
+ }, children: [
311
+ "Du benötigst Upload-Berechtigungen um die Media Library zu nutzen.",
312
+ /* @__PURE__ */ jsx("br", {}),
313
+ /* @__PURE__ */ jsx("br", {}),
314
+ /* @__PURE__ */ jsx("strong", { children: "Lösung:" }),
315
+ " Bitte einen Super Admin, dir die Upload-Rechte unter ",
316
+ /* @__PURE__ */ jsx("em", { children: "Settings → Administration panel → Roles" }),
317
+ " zu geben."
318
+ ] }),
319
+ /* @__PURE__ */ jsx(
320
+ "button",
321
+ {
322
+ onClick: this.props.onClose,
323
+ style: {
324
+ padding: "12px 24px",
325
+ background: "linear-gradient(135deg, #7C3AED 0%, #6d28d9 100%)",
326
+ color: "white",
327
+ border: "none",
328
+ borderRadius: "8px",
329
+ fontSize: "14px",
330
+ fontWeight: "600",
331
+ cursor: "pointer"
332
+ },
333
+ children: "Schließen"
334
+ }
335
+ )
336
+ ]
337
+ }
338
+ )
339
+ }
340
+ );
341
+ }
342
+ return this.props.children;
343
+ }
344
+ }
345
+ const getToggleFunc = ({ openStateSetter, indexStateSetter }) => {
346
+ return (idx) => {
347
+ if (idx !== void 0 && idx !== null) {
348
+ indexStateSetter(idx);
349
+ }
350
+ openStateSetter((prev) => !prev);
351
+ };
352
+ };
353
+ const changeFunc = ({ indexStateSetter, editor, data, index }) => {
354
+ if (!editor || !data || data.length === 0) {
355
+ indexStateSetter(-1);
356
+ return;
357
+ }
358
+ let insertedBlocksCount = 0;
359
+ data.forEach((entry) => {
360
+ if (!entry.mime || !entry.mime.includes("image")) {
361
+ console.warn("[Magic Editor X] Skipping non-image file:", entry.name);
362
+ return;
363
+ }
364
+ const newBlockType = "image";
365
+ const newBlockData = {
366
+ file: {
367
+ url: entry.url.replace(window.location.origin, ""),
368
+ mime: entry.mime,
369
+ height: entry.height,
370
+ width: entry.width,
371
+ size: entry.size,
372
+ alt: entry.alt || entry.name || "",
373
+ formats: entry.formats
374
+ },
375
+ caption: entry.caption || "",
376
+ withBorder: false,
377
+ withBackground: false,
378
+ stretched: false
379
+ };
380
+ try {
381
+ editor.blocks.insert(
382
+ newBlockType,
383
+ newBlockData,
384
+ {},
385
+ index + insertedBlocksCount,
386
+ true
387
+ );
388
+ insertedBlocksCount++;
389
+ } catch (error) {
390
+ console.error("[Magic Editor X] Error inserting image block:", error);
391
+ }
392
+ });
393
+ if (insertedBlocksCount > 0) {
394
+ try {
395
+ editor.blocks.delete(index + insertedBlocksCount);
396
+ } catch (error) {
397
+ console.warn("[Magic Editor X] Could not delete placeholder block:", error);
398
+ }
399
+ }
400
+ indexStateSetter(-1);
401
+ };
402
+ const pluginId = "magic-editor-x";
403
+ const SOCKET_FALLBACK_PATH = "/magic-editor-x/realtime";
404
+ const CURSOR_COLORS = [
405
+ { bg: "#EF4444", text: "#FFFFFF", name: "Red" },
406
+ // Red
407
+ { bg: "#3B82F6", text: "#FFFFFF", name: "Blue" },
408
+ // Blue
409
+ { bg: "#10B981", text: "#FFFFFF", name: "Green" },
410
+ // Green
411
+ { bg: "#F59E0B", text: "#000000", name: "Amber" },
412
+ // Amber
413
+ { bg: "#8B5CF6", text: "#FFFFFF", name: "Purple" },
414
+ // Purple
415
+ { bg: "#EC4899", text: "#FFFFFF", name: "Pink" },
416
+ // Pink
417
+ { bg: "#06B6D4", text: "#FFFFFF", name: "Cyan" },
418
+ // Cyan
419
+ { bg: "#F97316", text: "#FFFFFF", name: "Orange" }
420
+ // Orange
421
+ ];
422
+ const getUserColor = (userId) => {
423
+ if (!userId) return CURSOR_COLORS[0];
424
+ const hash = String(userId).split("").reduce((acc, char) => acc + char.charCodeAt(0), 0);
425
+ return CURSOR_COLORS[hash % CURSOR_COLORS.length];
426
+ };
427
+ const useMagicCollaboration = ({
428
+ enabled,
429
+ roomId,
430
+ fieldName,
431
+ initialValue,
432
+ onRemoteUpdate
433
+ // Callback when remote update is received
434
+ }) => {
435
+ const { get, post } = useFetchClient();
436
+ const authContext = useAuth("useMagicCollaboration", (state) => state);
437
+ const user = authContext?.user || null;
438
+ const [status, setStatus] = useState(enabled ? "idle" : "disabled");
439
+ const [error, setError] = useState(null);
440
+ const [peers, setPeers] = useState([]);
441
+ const [awareness, setAwareness] = useState({});
442
+ const [collabRole, setCollabRole] = useState(null);
443
+ const [canEdit, setCanEdit] = useState(null);
444
+ const socketRef = useRef(null);
445
+ const persistenceRef = useRef(null);
446
+ const bootstrappedRef = useRef(false);
447
+ const onRemoteUpdateRef = useRef(onRemoteUpdate);
448
+ useEffect(() => {
449
+ onRemoteUpdateRef.current = onRemoteUpdate;
450
+ }, [onRemoteUpdate]);
451
+ const { doc, blocksMap, metaMap } = useMemo(() => {
452
+ const yDoc = new Y.Doc();
453
+ return {
454
+ doc: yDoc,
455
+ blocksMap: yDoc.getMap("blocks"),
456
+ // Each block stored by ID
457
+ metaMap: yDoc.getMap("meta")
458
+ // Metadata (time, blockOrder, etc.)
459
+ };
460
+ }, [roomId]);
461
+ useEffect(() => {
462
+ return () => {
463
+ doc.destroy();
464
+ };
465
+ }, [doc]);
466
+ useEffect(() => {
467
+ bootstrappedRef.current = false;
468
+ setPeers([]);
469
+ setAwareness({});
470
+ if (socketRef.current) {
471
+ socketRef.current.disconnect();
472
+ socketRef.current = null;
473
+ }
474
+ return () => {
475
+ if (socketRef.current) {
476
+ socketRef.current.disconnect();
477
+ socketRef.current = null;
478
+ }
479
+ };
480
+ }, [roomId]);
481
+ useEffect(() => {
482
+ if (!enabled) return void 0;
483
+ const cleanupInterval = setInterval(() => {
484
+ const now = Date.now();
485
+ const staleThreshold = 3e4;
486
+ setAwareness((prev) => {
487
+ const next = { ...prev };
488
+ let changed = false;
489
+ Object.keys(next).forEach((key) => {
490
+ if (now - next[key].lastUpdate > staleThreshold) {
491
+ delete next[key];
492
+ changed = true;
493
+ }
494
+ });
495
+ return changed ? next : prev;
496
+ });
497
+ }, 1e4);
498
+ return () => clearInterval(cleanupInterval);
499
+ }, [enabled]);
500
+ useEffect(() => {
501
+ if (!enabled) {
502
+ setStatus("disabled");
503
+ } else if (status === "disabled") {
504
+ setStatus("idle");
505
+ }
506
+ }, [enabled, status]);
507
+ useEffect(() => {
508
+ if (!enabled || !roomId) {
509
+ return void 0;
510
+ }
511
+ bootstrappedRef.current = true;
512
+ console.log("[Magic Collab] [READY] Client ready, waiting for server sync...");
513
+ return void 0;
514
+ }, [enabled, roomId]);
515
+ useEffect(() => {
516
+ if (!enabled || !roomId || !doc) {
517
+ return void 0;
518
+ }
519
+ const persistenceKey = `magic-editor-x::${roomId}`;
520
+ try {
521
+ const persistence = new IndexeddbPersistence(persistenceKey, doc);
522
+ persistenceRef.current = persistence;
523
+ persistence.on("synced", () => {
524
+ 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
+ });
529
+ console.log("[Magic Collab] [CACHE] IndexedDB persistence initialized:", persistenceKey);
530
+ } catch (e) {
531
+ console.warn("[Magic Collab] IndexedDB persistence failed:", e.message);
532
+ }
533
+ return () => {
534
+ if (persistenceRef.current) {
535
+ try {
536
+ persistenceRef.current.destroy();
537
+ persistenceRef.current = null;
538
+ console.log("[Magic Collab] [CACHE] IndexedDB persistence destroyed");
539
+ } catch (e) {
540
+ }
541
+ }
542
+ };
543
+ }, [enabled, roomId, doc]);
544
+ const initialValueRef = useRef(initialValue);
545
+ useEffect(() => {
546
+ initialValueRef.current = initialValue;
547
+ }, [initialValue]);
548
+ useEffect(() => {
549
+ if (!enabled || !roomId || !user) {
550
+ return void 0;
551
+ }
552
+ if (socketRef.current?.connected) {
553
+ return void 0;
554
+ }
555
+ let cancelled = false;
556
+ setError(null);
557
+ setStatus("requesting");
558
+ const startSession = async () => {
559
+ try {
560
+ console.log("[Magic Collab] [SESSION] Requesting session for room:", roomId);
561
+ const { data } = await post(`/${pluginId}/collab/session`, {
562
+ roomId,
563
+ fieldName,
564
+ initialValue: initialValueRef.current || "",
565
+ meta: { roomId, fieldName }
566
+ });
567
+ console.log("[Magic Collab] [SUCCESS] Session response:", data);
568
+ if (cancelled) {
569
+ return;
570
+ }
571
+ if (!data || !data.token) {
572
+ throw new Error("Invalid session response: missing token");
573
+ }
574
+ const userRole = data.role || "viewer";
575
+ const userCanEdit = data.canEdit !== void 0 ? data.canEdit : ["editor", "owner"].includes(userRole);
576
+ setCollabRole(userRole);
577
+ setCanEdit(userCanEdit);
578
+ console.log("[Magic Collab] [ROLE] User role:", userRole, "| Can edit:", userCanEdit);
579
+ setStatus("connecting");
580
+ const wsUrl = data.wsUrl || window.location.origin;
581
+ const wsPath = data.wsPath || SOCKET_FALLBACK_PATH;
582
+ console.log("[Magic Collab] [SOCKET] Connecting to Socket.io:", { wsUrl, wsPath });
583
+ const socket = io(wsUrl, {
584
+ path: wsPath,
585
+ auth: { token: data.token },
586
+ transports: ["websocket", "polling"],
587
+ // Add polling as fallback
588
+ reconnection: true,
589
+ reconnectionDelay: 1e3,
590
+ reconnectionAttempts: 5
591
+ });
592
+ socketRef.current = socket;
593
+ socket.on("connect", () => {
594
+ setStatus("connected");
595
+ console.log("[Magic Collab] [SUCCESS] Connected to room:", roomId);
596
+ });
597
+ socket.on("disconnect", (reason) => {
598
+ setStatus("disconnected");
599
+ console.log("[Magic Collab] [WARNING] Disconnected:", reason);
600
+ });
601
+ socket.on("connect_error", (error2) => {
602
+ console.error("[Magic Collab] [ERROR] Connection error:", error2);
603
+ setStatus("error");
604
+ setError(`Connection failed: ${error2.message}`);
605
+ });
606
+ socket.on("collab:error", (payload) => {
607
+ console.error("[Magic Collab] [ERROR] Collaboration error:", payload);
608
+ setError(payload?.message || "Realtime collaboration error");
609
+ });
610
+ socket.on("collab:sync", (update) => {
611
+ if (update) {
612
+ console.log("[Magic Collab] [SYNC] Syncing initial state, update size:", update.length, "bytes");
613
+ 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
+ const beforeBlockCount = blocksMap.size;
618
+ console.log("[Magic Collab] [DATA] Y.Map BEFORE sync - block count:", beforeBlockCount);
619
+ Y.applyUpdate(doc, new Uint8Array(update), "remote");
620
+ const afterBlockCount = blocksMap.size;
621
+ 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
+ if (onRemoteUpdateRef.current) {
626
+ console.log("[Magic Collab] [CALLBACK] Calling onRemoteUpdate callback after sync");
627
+ setTimeout(() => {
628
+ onRemoteUpdateRef.current();
629
+ }, 0);
630
+ }
631
+ } catch (err) {
632
+ console.error("[Magic Collab] Failed to apply initial sync:", err);
633
+ }
634
+ }
635
+ });
636
+ socket.on("collab:update", (update) => {
637
+ if (update) {
638
+ console.log("[Magic Collab] [UPDATE] Received remote update:", update.length, "bytes");
639
+ 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
+ const beforeBlockCount = blocksMap.size;
643
+ console.log("[Magic Collab] [DATA] Y.Map BEFORE update - blocks:", beforeBlockCount);
644
+ Y.applyUpdate(doc, new Uint8Array(update), "remote");
645
+ const afterBlockCount = blocksMap.size;
646
+ 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
+ if (onRemoteUpdateRef.current) {
650
+ console.log("[Magic Collab] [CALLBACK] Calling onRemoteUpdate callback");
651
+ setTimeout(() => {
652
+ onRemoteUpdateRef.current();
653
+ }, 0);
654
+ }
655
+ } catch (err) {
656
+ console.error("[Magic Collab] Failed to apply update:", err);
657
+ }
658
+ }
659
+ });
660
+ socket.on("collab:presence", ({ type, user: peerUser }) => {
661
+ if (!peerUser?.id) {
662
+ return;
663
+ }
664
+ console.log("[Magic Collab] [PEERS] Peer presence:", type, peerUser.email);
665
+ setPeers((current) => {
666
+ if (type === "leave") {
667
+ setAwareness((prev) => {
668
+ const next = { ...prev };
669
+ delete next[peerUser.id];
670
+ return next;
671
+ });
672
+ return current.filter((peer) => peer.id !== peerUser.id);
673
+ }
674
+ const exists = current.some((peer) => peer.id === peerUser.id);
675
+ if (exists) {
676
+ return current;
677
+ }
678
+ return [...current, peerUser];
679
+ });
680
+ });
681
+ socket.on("collab:awareness", ({ user: peerUser, payload }) => {
682
+ if (!peerUser?.id) {
683
+ return;
684
+ }
685
+ if (user && peerUser.id === user.id) {
686
+ return;
687
+ }
688
+ if (payload?.blockIndex === -1 || payload?.blockIndex === null) {
689
+ return;
690
+ }
691
+ setAwareness((prev) => ({
692
+ ...prev,
693
+ [peerUser.id]: {
694
+ user: peerUser,
695
+ cursor: payload?.cursor || null,
696
+ blockId: payload?.blockId || null,
697
+ blockIndex: payload?.blockIndex ?? null,
698
+ selection: payload?.selection || null,
699
+ color: getUserColor(peerUser.id),
700
+ lastUpdate: Date.now()
701
+ }
702
+ }));
703
+ });
704
+ } catch (err) {
705
+ if (!cancelled) {
706
+ console.error("[Magic Collab] [ERROR] Session error:", err);
707
+ const errorMessage = err?.response?.data?.error?.message || err?.message;
708
+ const errorStatus = err?.response?.status;
709
+ setStatus("denied");
710
+ if (errorStatus === 403 || errorMessage?.includes("Freigabe") || errorMessage?.includes("forbidden")) {
711
+ setError("[DENIED] Keine Freigabe: Kontaktiere einen Super Admin fuer Zugriff");
712
+ } else if (errorStatus === 401 || errorMessage?.includes("unauthorized")) {
713
+ setError("[AUTH] Authentifizierung fehlgeschlagen");
714
+ } else if (errorMessage?.includes("disabled")) {
715
+ setError("[DISABLED] Echtzeit-Bearbeitung ist deaktiviert");
716
+ } else if (errorStatus === 400) {
717
+ setError(`[ERROR] Ungueltige Anfrage: ${errorMessage}`);
718
+ } else {
719
+ setError(errorMessage || "[ERROR] Keine Berechtigung fuer Live Editing");
720
+ }
721
+ }
722
+ }
723
+ };
724
+ startSession();
725
+ return () => {
726
+ cancelled = true;
727
+ if (socketRef.current) {
728
+ console.log("[Magic Collab] [SOCKET] Disconnecting socket");
729
+ socketRef.current.disconnect();
730
+ socketRef.current = null;
731
+ }
732
+ setPeers([]);
733
+ };
734
+ }, [enabled, post, fieldName, roomId, user]);
735
+ useEffect(() => {
736
+ if (!enabled) {
737
+ console.log("[Magic Collab] [SKIP] Update handler not registered (disabled)");
738
+ return void 0;
739
+ }
740
+ console.log("[Magic Collab] [HANDLER] Registering update handler on doc");
741
+ const handler = (update, origin) => {
742
+ const updateSize = update?.length || 0;
743
+ if (updateSize > 10) {
744
+ console.log("[Magic Collab] [DOC] Doc update - origin:", origin, "size:", updateSize, "bytes");
745
+ }
746
+ if (origin === "remote" || origin === "bootstrap") {
747
+ return;
748
+ }
749
+ if (!socketRef.current?.connected) {
750
+ console.log("[Magic Collab] [WARNING] Socket not connected, update queued");
751
+ return;
752
+ }
753
+ const updateArray = Array.from(update);
754
+ socketRef.current.emit("collab:update", updateArray);
755
+ console.log("[Magic Collab] [SENT] Sent update:", updateArray.length, "bytes to server");
756
+ };
757
+ doc.on("update", handler);
758
+ return () => {
759
+ console.log("[Magic Collab] [CLEANUP] Removing update handler");
760
+ doc.off("update", handler);
761
+ };
762
+ }, [doc, enabled]);
763
+ const emitAwareness = useCallback((payload) => {
764
+ if (socketRef.current?.connected) {
765
+ socketRef.current.emit("collab:awareness", payload);
766
+ }
767
+ }, []);
768
+ const localUserColor = useMemo(() => {
769
+ return user ? getUserColor(user.id) : CURSOR_COLORS[0];
770
+ }, [user]);
771
+ return {
772
+ doc,
773
+ blocksMap,
774
+ // Y.Map for block-level sync (replaces Y.Text)
775
+ metaMap,
776
+ // Y.Map for metadata (includes blockOrder as JSON string)
777
+ status,
778
+ error,
779
+ peers,
780
+ awareness,
781
+ emitAwareness,
782
+ localUserColor,
783
+ getUserColor,
784
+ // Role-based access control
785
+ collabRole,
786
+ // 'viewer' | 'editor' | 'owner' | null
787
+ canEdit
788
+ // true if user can edit (editor/owner), false for viewer
789
+ };
790
+ };
791
+ const useLicense = () => {
792
+ const { get } = useFetchClient();
793
+ const [isPremium, setIsPremium] = useState(false);
794
+ const [isAdvanced, setIsAdvanced] = useState(false);
795
+ const [isEnterprise, setIsEnterprise] = useState(false);
796
+ const [tier, setTier] = useState("free");
797
+ const [loading, setLoading] = useState(true);
798
+ const [error, setError] = useState(null);
799
+ const [licenseData, setLicenseData] = useState(null);
800
+ const [limits, setLimits] = useState(null);
801
+ useEffect(() => {
802
+ let mounted = true;
803
+ const fetchLicense = async () => {
804
+ if (mounted) {
805
+ await checkLicense();
806
+ }
807
+ };
808
+ fetchLicense();
809
+ const interval = setInterval(() => {
810
+ if (mounted) {
811
+ checkLicense(true);
812
+ }
813
+ }, 60 * 60 * 1e3);
814
+ return () => {
815
+ mounted = false;
816
+ clearInterval(interval);
817
+ };
818
+ }, []);
819
+ const checkLicense = async (silent = false) => {
820
+ if (!silent) {
821
+ setLoading(true);
822
+ }
823
+ try {
824
+ const response = await get("/magic-editor-x/license/status");
825
+ const isValid = response.data?.valid || false;
826
+ const tierName = response.data?.tier || "free";
827
+ const hasPremiumFeature = response.data?.data?.features?.premium || tierName === "premium";
828
+ const hasAdvancedFeature = response.data?.data?.features?.advanced || tierName === "advanced";
829
+ const hasEnterpriseFeature = response.data?.data?.features?.enterprise || tierName === "enterprise";
830
+ setTier(tierName);
831
+ setIsPremium(isValid && (hasPremiumFeature || hasAdvancedFeature || hasEnterpriseFeature));
832
+ setIsAdvanced(isValid && (hasAdvancedFeature || hasEnterpriseFeature));
833
+ setIsEnterprise(isValid && hasEnterpriseFeature);
834
+ setLicenseData(response.data?.data || null);
835
+ setError(null);
836
+ await fetchLimits();
837
+ } catch (err) {
838
+ if (err.name === "AbortError") {
839
+ return;
840
+ }
841
+ if (!silent) {
842
+ console.error("[Magic Editor X] License check error:", err);
843
+ }
844
+ setTier("free");
845
+ setIsPremium(false);
846
+ setIsAdvanced(false);
847
+ setIsEnterprise(false);
848
+ setLicenseData(null);
849
+ setError(err);
850
+ } finally {
851
+ if (!silent) {
852
+ setLoading(false);
853
+ }
854
+ }
855
+ };
856
+ const fetchLimits = async () => {
857
+ try {
858
+ const response = await get("/magic-editor-x/license/limits");
859
+ setLimits(response.data?.limits || null);
860
+ } catch (err) {
861
+ console.error("[Magic Editor X] Error fetching limits:", err);
862
+ }
863
+ };
864
+ const hasFeature = (featureName) => {
865
+ if (!featureName) return false;
866
+ const freeFeatures = [
867
+ "editor",
868
+ "allTools",
869
+ "collaboration-basic"
870
+ // 2 collaborators
871
+ ];
872
+ if (freeFeatures.includes(featureName)) return true;
873
+ const premiumFeatures = [
874
+ "collaboration-extended",
875
+ // 10 collaborators
876
+ "ai-basic"
877
+ ];
878
+ if (premiumFeatures.includes(featureName) && isPremium) return true;
879
+ const advancedFeatures = [
880
+ "collaboration-unlimited",
881
+ "ai-full"
882
+ ];
883
+ if (advancedFeatures.includes(featureName) && isAdvanced) return true;
884
+ const enterpriseFeatures = [
885
+ "priority-support",
886
+ "custom-branding"
887
+ ];
888
+ if (enterpriseFeatures.includes(featureName) && isEnterprise) return true;
889
+ return false;
890
+ };
891
+ const canAddCollaborator = () => {
892
+ if (!limits?.collaborators) return true;
893
+ return limits.collaborators.canAdd;
894
+ };
895
+ const getCollaboratorInfo = () => {
896
+ if (!limits?.collaborators) {
897
+ return { current: 0, max: 2, unlimited: false, canAdd: true };
898
+ }
899
+ return limits.collaborators;
900
+ };
901
+ return {
902
+ tier,
903
+ isPremium,
904
+ isAdvanced,
905
+ isEnterprise,
906
+ loading,
907
+ error,
908
+ licenseData,
909
+ limits,
910
+ features: {
911
+ premium: isPremium,
912
+ advanced: isAdvanced,
913
+ enterprise: isEnterprise
914
+ },
915
+ hasFeature,
916
+ canAddCollaborator,
917
+ getCollaboratorInfo,
918
+ refetch: checkLicense
919
+ };
920
+ };
921
+ const useAIActions = ({ licenseKey, editorInstanceRef, isReady, onNoCredits }) => {
922
+ const apiClientRef = React__default.useRef(null);
923
+ React__default.useEffect(() => {
924
+ if (licenseKey && !apiClientRef.current) {
925
+ apiClientRef.current = new MagicEditorAPI(licenseKey);
926
+ }
927
+ }, [licenseKey]);
928
+ const replaceText = useCallback((range, newText) => {
929
+ if (!range) return false;
930
+ try {
931
+ const selection = window.getSelection();
932
+ selection.removeAllRanges();
933
+ selection.addRange(range);
934
+ const success = document.execCommand("insertText", false, newText);
935
+ if (editorInstanceRef.current && isReady) {
936
+ const currentBlock = editorInstanceRef.current.blocks.getBlockByIndex(
937
+ editorInstanceRef.current.blocks.getCurrentBlockIndex()
938
+ );
939
+ currentBlock?.dispatchChange?.();
940
+ }
941
+ return success;
942
+ } catch (err) {
943
+ console.error("[AI Actions] Failed to replace text:", err);
944
+ return false;
945
+ }
946
+ }, [editorInstanceRef, isReady]);
947
+ const appendText = useCallback((range, additionalText) => {
948
+ if (!range) return false;
949
+ try {
950
+ const selection = window.getSelection();
951
+ const newRange = range.cloneRange();
952
+ newRange.collapse(false);
953
+ selection.removeAllRanges();
954
+ selection.addRange(newRange);
955
+ const success = document.execCommand("insertText", false, additionalText);
956
+ if (editorInstanceRef.current && isReady) {
957
+ const currentBlock = editorInstanceRef.current.blocks.getBlockByIndex(
958
+ editorInstanceRef.current.blocks.getCurrentBlockIndex()
959
+ );
960
+ currentBlock?.dispatchChange?.();
961
+ }
962
+ return success;
963
+ } catch (err) {
964
+ console.error("[AI Actions] Failed to append text:", err);
965
+ return false;
966
+ }
967
+ }, [editorInstanceRef, isReady]);
968
+ const handleAIAction = useCallback(async (action, options, { text, range }) => {
969
+ if (!apiClientRef.current || !text) return;
970
+ const originalText = text;
971
+ const langNames = { en: "Englisch", de: "Deutsch", fr: "Französisch", es: "Spanisch" };
972
+ try {
973
+ let result;
974
+ let type;
975
+ let apiOptions = {};
976
+ switch (action) {
977
+ case "fix":
978
+ type = "grammar";
979
+ break;
980
+ case "rewrite":
981
+ type = "rewrite";
982
+ apiOptions = { tone: options.tone };
983
+ break;
984
+ case "expand":
985
+ type = "expand";
986
+ break;
987
+ case "summarize":
988
+ type = "summarize";
989
+ break;
990
+ case "continue":
991
+ type = "continue";
992
+ break;
993
+ case "translate":
994
+ type = "translate";
995
+ apiOptions = { language: options.language };
996
+ break;
997
+ default:
998
+ console.warn("Unknown AI action:", action);
999
+ return false;
1000
+ }
1001
+ result = await apiClientRef.current.correct(text, type, apiOptions);
1002
+ if (result.data.hasChanges && result.data.corrected) {
1003
+ if (action === "continue") {
1004
+ appendText(range, " " + result.data.corrected);
1005
+ } else {
1006
+ replaceText(range, result.data.corrected);
1007
+ }
1008
+ const messages = {
1009
+ fix: `✓ ${result.data.changes?.length || 1} Korrekturen angewendet`,
1010
+ rewrite: `✨ Text umgeschrieben (${options.tone || "standard"})`,
1011
+ expand: "📈 Text erweitert",
1012
+ summarize: "📉 Text zusammengefasst",
1013
+ continue: "✍️ Text fortgesetzt",
1014
+ translate: `🌍 Übersetzt zu ${langNames[options.language] || options.language}`
1015
+ };
1016
+ toastManager.success(messages[action], {
1017
+ description: `${result.credits?.remaining ?? 0} Credits übrig`,
1018
+ onUndo: () => replaceText(range, originalText),
1019
+ duration: 5e3
1020
+ });
1021
+ } else {
1022
+ toastManager.info("Text ist bereits korrekt");
1023
+ }
1024
+ return true;
1025
+ } catch (err) {
1026
+ console.error("[AI Actions] Action failed:", err);
1027
+ switch (err.code) {
1028
+ case "NO_CREDITS":
1029
+ if (onNoCredits) {
1030
+ onNoCredits(err.upgrade);
1031
+ } else {
1032
+ toastManager.warning("Keine Credits", {
1033
+ description: err.message || "Bitte Credits kaufen",
1034
+ duration: 5e3
1035
+ });
1036
+ }
1037
+ break;
1038
+ case "TYPE_NOT_ALLOWED":
1039
+ toastManager.warning("Funktion nicht verfügbar", {
1040
+ description: "Diese Funktion ist in deinem Tier nicht enthalten",
1041
+ duration: 5e3
1042
+ });
1043
+ break;
1044
+ case "LICENSE_NOT_FOUND":
1045
+ case "LICENSE_INACTIVE":
1046
+ case "WRONG_PLUGIN":
1047
+ toastManager.error("Lizenz ungültig", {
1048
+ description: err.message || "Bitte prüfe deine Lizenz",
1049
+ duration: 5e3
1050
+ });
1051
+ break;
1052
+ case "DAILY_LIMIT_EXCEEDED":
1053
+ case "MONTHLY_LIMIT_EXCEEDED":
1054
+ toastManager.warning("Limit erreicht", {
1055
+ description: err.message,
1056
+ duration: 5e3
1057
+ });
1058
+ break;
1059
+ case "CORRECTION_FAILED":
1060
+ toastManager.error("AI Fehler", {
1061
+ description: "Die KI konnte die Anfrage nicht verarbeiten",
1062
+ duration: 4e3
1063
+ });
1064
+ break;
1065
+ default:
1066
+ toastManager.error("Fehler", {
1067
+ description: err.message || "Aktion fehlgeschlagen",
1068
+ duration: 4e3
1069
+ });
1070
+ }
1071
+ return false;
1072
+ }
1073
+ }, [replaceText, appendText, onNoCredits]);
1074
+ return { handleAIAction };
1075
+ };
1076
+ const Overlay$1 = styled.div`
1077
+ position: fixed;
1078
+ top: 0;
1079
+ left: 0;
1080
+ right: 0;
1081
+ bottom: 0;
1082
+ background: rgba(0, 0, 0, 0.5);
1083
+ display: flex;
1084
+ align-items: center;
1085
+ justify-content: center;
1086
+ z-index: 999999;
1087
+ `;
1088
+ const PopupContainer = styled.div`
1089
+ background: ${(props) => props.theme.colors.neutral0};
1090
+ border-radius: 16px;
1091
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.2);
1092
+ width: 100%;
1093
+ max-width: 500px;
1094
+ max-height: 80vh;
1095
+ overflow: hidden;
1096
+ display: flex;
1097
+ flex-direction: column;
1098
+ `;
1099
+ const Header$1 = styled.div`
1100
+ background: linear-gradient(135deg, #7C3AED 0%, #a855f7 100%);
1101
+ padding: 20px 24px;
1102
+ color: white;
1103
+ display: flex;
1104
+ justify-content: space-between;
1105
+ align-items: center;
1106
+ `;
1107
+ const HeaderTitle = styled.div`
1108
+ display: flex;
1109
+ align-items: center;
1110
+ gap: 12px;
1111
+ font-size: 16px;
1112
+ font-weight: 600;
1113
+ `;
1114
+ const CreditsBadge = styled.div`
1115
+ background: rgba(255, 255, 255, 0.2);
1116
+ padding: 6px 14px;
1117
+ border-radius: 20px;
1118
+ font-size: 13px;
1119
+ font-weight: 500;
1120
+ `;
1121
+ const Content$1 = styled.div`
1122
+ padding: 24px;
1123
+ overflow-y: auto;
1124
+ flex: 1;
1125
+ `;
1126
+ const TextPreview = styled.div`
1127
+ background: ${(props) => props.theme.colors.neutral100};
1128
+ border: 1px solid ${(props) => props.theme.colors.neutral200};
1129
+ border-radius: 10px;
1130
+ padding: 14px;
1131
+ margin-bottom: 20px;
1132
+ max-height: 100px;
1133
+ overflow-y: auto;
1134
+ font-size: 14px;
1135
+ color: ${(props) => props.theme.colors.neutral600};
1136
+ line-height: 1.6;
1137
+ `;
1138
+ const TypeButtons = styled.div`
1139
+ display: flex;
1140
+ gap: 10px;
1141
+ margin-bottom: 20px;
1142
+ `;
1143
+ const TypeButton = styled.button`
1144
+ flex: 1;
1145
+ padding: 14px 16px;
1146
+ border: 2px solid ${(props) => props.$active ? "#7C3AED" : props.theme.colors.neutral200};
1147
+ border-radius: 10px;
1148
+ background: ${(props) => props.$active ? "#f5f3ff" : props.theme.colors.neutral0};
1149
+ color: ${(props) => props.$active ? "#7C3AED" : props.theme.colors.neutral700};
1150
+ font-size: 13px;
1151
+ font-weight: 600;
1152
+ cursor: pointer;
1153
+ transition: all 0.15s ease;
1154
+ display: flex;
1155
+ align-items: center;
1156
+ justify-content: center;
1157
+ gap: 8px;
1158
+
1159
+ &:hover:not(:disabled) {
1160
+ border-color: #7C3AED;
1161
+ background: #faf8ff;
1162
+ }
1163
+
1164
+ &:disabled {
1165
+ opacity: 0.5;
1166
+ cursor: not-allowed;
1167
+ }
1168
+ `;
1169
+ const ResultArea = styled.div`
1170
+ min-height: 80px;
1171
+ `;
1172
+ const LoadingSpinner = styled.div`
1173
+ text-align: center;
1174
+ padding: 40px;
1175
+
1176
+ .spinner {
1177
+ width: 40px;
1178
+ height: 40px;
1179
+ border: 4px solid ${(props) => props.theme.colors.neutral200};
1180
+ border-top-color: #7C3AED;
1181
+ border-radius: 50%;
1182
+ margin: 0 auto 16px;
1183
+ animation: spin 0.8s linear infinite;
1184
+ }
1185
+
1186
+ @keyframes spin {
1187
+ to { transform: rotate(360deg); }
1188
+ }
1189
+ `;
1190
+ const ResultBox = styled.div`
1191
+ margin-bottom: 16px;
1192
+ `;
1193
+ const ResultLabel = styled.div`
1194
+ font-size: 11px;
1195
+ font-weight: 700;
1196
+ color: ${(props) => props.theme.colors.neutral500};
1197
+ text-transform: uppercase;
1198
+ margin-bottom: 8px;
1199
+ letter-spacing: 0.5px;
1200
+ `;
1201
+ const OriginalText = styled.div`
1202
+ background: #fef2f2;
1203
+ border: 1px solid #fecaca;
1204
+ border-radius: 8px;
1205
+ padding: 14px;
1206
+ font-size: 14px;
1207
+ color: #991b1b;
1208
+ text-decoration: line-through;
1209
+ line-height: 1.6;
1210
+ `;
1211
+ const CorrectedText = styled.div`
1212
+ background: #f0fdf4;
1213
+ border: 1px solid #bbf7d0;
1214
+ border-radius: 8px;
1215
+ padding: 14px;
1216
+ font-size: 14px;
1217
+ color: #166534;
1218
+ line-height: 1.6;
1219
+ `;
1220
+ const SuccessMessage = styled.div`
1221
+ text-align: center;
1222
+ padding: 30px;
1223
+ color: #10b981;
1224
+
1225
+ .icon {
1226
+ font-size: 48px;
1227
+ margin-bottom: 12px;
1228
+ }
1229
+
1230
+ .title {
1231
+ font-size: 16px;
1232
+ font-weight: 600;
1233
+ margin-bottom: 4px;
1234
+ }
1235
+
1236
+ .subtitle {
1237
+ font-size: 13px;
1238
+ color: ${(props) => props.theme.colors.neutral500};
1239
+ }
1240
+ `;
1241
+ const ErrorMessage = styled.div`
1242
+ text-align: center;
1243
+ padding: 30px;
1244
+ color: #dc2626;
1245
+
1246
+ .icon {
1247
+ font-size: 48px;
1248
+ margin-bottom: 12px;
1249
+ }
1250
+
1251
+ .title {
1252
+ font-size: 16px;
1253
+ font-weight: 600;
1254
+ margin-bottom: 4px;
1255
+ }
1256
+
1257
+ .subtitle {
1258
+ font-size: 13px;
1259
+ color: ${(props) => props.theme.colors.neutral500};
1260
+ }
1261
+ `;
1262
+ const Footer$1 = styled.div`
1263
+ padding: 16px 24px;
1264
+ border-top: 1px solid ${(props) => props.theme.colors.neutral200};
1265
+ display: flex;
1266
+ justify-content: flex-end;
1267
+ gap: 12px;
1268
+ background: ${(props) => props.theme.colors.neutral100};
1269
+ `;
1270
+ const Button$1 = styled.button`
1271
+ padding: 10px 20px;
1272
+ border-radius: 8px;
1273
+ font-size: 14px;
1274
+ font-weight: 500;
1275
+ cursor: pointer;
1276
+ transition: all 0.15s ease;
1277
+ `;
1278
+ const CancelButton = styled(Button$1)`
1279
+ background: ${(props) => props.theme.colors.neutral0};
1280
+ border: 1px solid ${(props) => props.theme.colors.neutral300};
1281
+ color: ${(props) => props.theme.colors.neutral700};
1282
+
1283
+ &:hover {
1284
+ background: ${(props) => props.theme.colors.neutral100};
1285
+ }
1286
+ `;
1287
+ const ApplyButton = styled(Button$1)`
1288
+ background: linear-gradient(135deg, #7C3AED 0%, #6d28d9 100%);
1289
+ border: none;
1290
+ color: white;
1291
+
1292
+ &:hover {
1293
+ transform: translateY(-1px);
1294
+ box-shadow: 0 4px 12px rgba(124, 58, 237, 0.3);
1295
+ }
1296
+
1297
+ &:disabled {
1298
+ opacity: 0.5;
1299
+ cursor: not-allowed;
1300
+ transform: none;
1301
+ }
1302
+ `;
1303
+ const PlaceholderText = styled.div`
1304
+ text-align: center;
1305
+ padding: 40px;
1306
+ color: ${(props) => props.theme.colors.neutral400};
1307
+ font-size: 14px;
1308
+ `;
1309
+ const SparklesIcon = () => /* @__PURE__ */ jsxs("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
1310
+ /* @__PURE__ */ jsx("path", { d: "M12 3l1.912 5.813a2 2 0 001.275 1.275L21 12l-5.813 1.912a2 2 0 00-1.275 1.275L12 21l-1.912-5.813a2 2 0 00-1.275-1.275L3 12l5.813-1.912a2 2 0 001.275-1.275L12 3z" }),
1311
+ /* @__PURE__ */ jsx("path", { d: "M5 3v4" }),
1312
+ /* @__PURE__ */ jsx("path", { d: "M3 5h4" }),
1313
+ /* @__PURE__ */ jsx("path", { d: "M19 17v4" }),
1314
+ /* @__PURE__ */ jsx("path", { d: "M17 19h4" })
1315
+ ] });
1316
+ const AIAssistantPopup = ({ selectedText, licenseKey, onClose, onApply }) => {
1317
+ const [activeType, setActiveType] = useState(null);
1318
+ const [isLoading, setIsLoading] = useState(false);
1319
+ const [result, setResult] = useState(null);
1320
+ const [error, setError] = useState(null);
1321
+ const [credits, setCredits] = useState(0);
1322
+ const [usage, setUsage] = useState(null);
1323
+ const [apiClient, setApiClient] = useState(null);
1324
+ useEffect(() => {
1325
+ if (licenseKey) {
1326
+ const client = new MagicEditorAPI(licenseKey);
1327
+ setApiClient(client);
1328
+ client.getUsage().then((res) => {
1329
+ setUsage(res.data);
1330
+ setCredits(res.data?.credits?.balance || 0);
1331
+ }).catch((err) => {
1332
+ console.error("[AI Popup] Failed to fetch usage:", err);
1333
+ });
1334
+ }
1335
+ }, [licenseKey]);
1336
+ const handleCorrection = useCallback(async (type) => {
1337
+ if (!apiClient || !selectedText) return;
1338
+ setActiveType(type);
1339
+ setIsLoading(true);
1340
+ setResult(null);
1341
+ setError(null);
1342
+ try {
1343
+ const response = await apiClient.correct(selectedText, type);
1344
+ setResult(response.data);
1345
+ if (response.credits?.remaining !== void 0) {
1346
+ setCredits(response.credits.remaining);
1347
+ }
1348
+ if (response.usage) {
1349
+ setUsage((prev) => ({
1350
+ ...prev,
1351
+ daily: {
1352
+ ...prev?.daily,
1353
+ used: response.usage.used,
1354
+ remaining: response.usage.remaining,
1355
+ limit: response.usage.limit
1356
+ }
1357
+ }));
1358
+ }
1359
+ } catch (err) {
1360
+ console.error("[AI Popup] Correction failed:", err);
1361
+ setError(err.message || "Korrektur fehlgeschlagen");
1362
+ } finally {
1363
+ setIsLoading(false);
1364
+ }
1365
+ }, [apiClient, selectedText]);
1366
+ const handleApply = useCallback(() => {
1367
+ if (result?.corrected) {
1368
+ onApply(result.corrected);
1369
+ }
1370
+ }, [result, onApply]);
1371
+ const handleOverlayClick = (e) => {
1372
+ if (e.target === e.currentTarget) {
1373
+ onClose();
1374
+ }
1375
+ };
1376
+ useEffect(() => {
1377
+ const handleKeyDown = (e) => {
1378
+ if (e.key === "Escape") {
1379
+ onClose();
1380
+ }
1381
+ };
1382
+ window.addEventListener("keydown", handleKeyDown);
1383
+ return () => window.removeEventListener("keydown", handleKeyDown);
1384
+ }, [onClose]);
1385
+ return /* @__PURE__ */ jsx(Overlay$1, { onClick: handleOverlayClick, children: /* @__PURE__ */ jsxs(PopupContainer, { onClick: (e) => e.stopPropagation(), children: [
1386
+ /* @__PURE__ */ jsxs(Header$1, { children: [
1387
+ /* @__PURE__ */ jsxs(HeaderTitle, { children: [
1388
+ /* @__PURE__ */ jsx(SparklesIcon, {}),
1389
+ /* @__PURE__ */ jsx("span", { children: "KI-Assistent" })
1390
+ ] }),
1391
+ /* @__PURE__ */ jsxs(CreditsBadge, { children: [
1392
+ usage?.tier === "free" && `Free • ${usage?.daily?.remaining ?? 0}/${usage?.daily?.limit ?? 3} heute`,
1393
+ usage?.tier === "basic" && `Basic • ${usage?.daily?.remaining ?? 0}/${usage?.daily?.limit ?? 10} heute`,
1394
+ usage?.tier === "pro" && `Pro • ${usage?.daily?.remaining ?? 0}/${usage?.daily?.limit ?? 50} heute`,
1395
+ !usage?.tier && "Wird geladen..."
1396
+ ] })
1397
+ ] }),
1398
+ /* @__PURE__ */ jsxs(Content$1, { children: [
1399
+ /* @__PURE__ */ jsx(TextPreview, { children: selectedText.length > 300 ? selectedText.substring(0, 300) + "..." : selectedText }),
1400
+ /* @__PURE__ */ jsxs(TypeButtons, { children: [
1401
+ /* @__PURE__ */ jsxs(
1402
+ TypeButton,
1403
+ {
1404
+ type: "button",
1405
+ $active: activeType === "grammar",
1406
+ onClick: () => handleCorrection("grammar"),
1407
+ disabled: isLoading || (usage?.daily?.remaining ?? 0) <= 0 && credits <= 0,
1408
+ children: [
1409
+ /* @__PURE__ */ jsx("span", { children: "✓" }),
1410
+ /* @__PURE__ */ jsx("span", { children: "Grammatik" })
1411
+ ]
1412
+ }
1413
+ ),
1414
+ /* @__PURE__ */ jsxs(
1415
+ TypeButton,
1416
+ {
1417
+ type: "button",
1418
+ $active: activeType === "style",
1419
+ onClick: () => handleCorrection("style"),
1420
+ disabled: isLoading || !usage?.allowedTypes?.includes("style") || (usage?.daily?.remaining ?? 0) <= 0 && credits <= 0,
1421
+ title: !usage?.allowedTypes?.includes("style") ? "Basic oder höher erforderlich" : void 0,
1422
+ children: [
1423
+ /* @__PURE__ */ jsx("span", { children: usage?.allowedTypes?.includes("style") ? "✨" : "🔒" }),
1424
+ /* @__PURE__ */ jsx("span", { children: "Stil" })
1425
+ ]
1426
+ }
1427
+ ),
1428
+ /* @__PURE__ */ jsxs(
1429
+ TypeButton,
1430
+ {
1431
+ type: "button",
1432
+ $active: activeType === "rewrite",
1433
+ onClick: () => handleCorrection("rewrite"),
1434
+ disabled: isLoading || !usage?.allowedTypes?.includes("rewrite") || (usage?.daily?.remaining ?? 0) <= 0 && credits <= 0,
1435
+ title: !usage?.allowedTypes?.includes("rewrite") ? "Pro erforderlich" : void 0,
1436
+ children: [
1437
+ /* @__PURE__ */ jsx("span", { children: usage?.allowedTypes?.includes("rewrite") ? "↻" : "🔒" }),
1438
+ /* @__PURE__ */ jsx("span", { children: "Umschreiben" })
1439
+ ]
1440
+ }
1441
+ )
1442
+ ] }),
1443
+ /* @__PURE__ */ jsxs(ResultArea, { children: [
1444
+ isLoading && /* @__PURE__ */ jsxs(LoadingSpinner, { children: [
1445
+ /* @__PURE__ */ jsx("div", { className: "spinner" }),
1446
+ /* @__PURE__ */ jsx("div", { children: "Korrigiere..." })
1447
+ ] }),
1448
+ !isLoading && !result && !error && /* @__PURE__ */ jsx(PlaceholderText, { children: "Wähle einen Korrektur-Typ, um den Text zu verbessern" }),
1449
+ !isLoading && result && !result.hasChanges && /* @__PURE__ */ jsxs(SuccessMessage, { children: [
1450
+ /* @__PURE__ */ jsx("div", { className: "icon", children: "✓" }),
1451
+ /* @__PURE__ */ jsx("div", { className: "title", children: "Keine Änderungen nötig" }),
1452
+ /* @__PURE__ */ jsx("div", { className: "subtitle", children: "Der Text ist bereits korrekt" })
1453
+ ] }),
1454
+ !isLoading && result && result.hasChanges && /* @__PURE__ */ jsxs(Fragment, { children: [
1455
+ /* @__PURE__ */ jsxs(ResultBox, { children: [
1456
+ /* @__PURE__ */ jsx(ResultLabel, { children: "Original" }),
1457
+ /* @__PURE__ */ jsx(OriginalText, { children: result.original })
1458
+ ] }),
1459
+ /* @__PURE__ */ jsxs(ResultBox, { children: [
1460
+ /* @__PURE__ */ jsx(ResultLabel, { children: "Korrigiert" }),
1461
+ /* @__PURE__ */ jsx(CorrectedText, { children: result.corrected })
1462
+ ] })
1463
+ ] }),
1464
+ !isLoading && error && /* @__PURE__ */ jsxs(ErrorMessage, { children: [
1465
+ /* @__PURE__ */ jsx("div", { className: "icon", children: "✕" }),
1466
+ /* @__PURE__ */ jsx("div", { className: "title", children: "Fehler" }),
1467
+ /* @__PURE__ */ jsx("div", { className: "subtitle", children: error })
1468
+ ] })
1469
+ ] })
1470
+ ] }),
1471
+ /* @__PURE__ */ jsxs(Footer$1, { children: [
1472
+ /* @__PURE__ */ jsx(CancelButton, { type: "button", onClick: onClose, children: "Abbrechen" }),
1473
+ /* @__PURE__ */ jsx(
1474
+ ApplyButton,
1475
+ {
1476
+ type: "button",
1477
+ onClick: handleApply,
1478
+ disabled: !result?.hasChanges,
1479
+ children: "Übernehmen"
1480
+ }
1481
+ )
1482
+ ] })
1483
+ ] }) });
1484
+ };
1485
+ const Overlay = styled.div`
1486
+ position: fixed;
1487
+ top: 0;
1488
+ left: 0;
1489
+ right: 0;
1490
+ bottom: 0;
1491
+ background: rgba(0, 0, 0, 0.5);
1492
+ backdrop-filter: blur(4px);
1493
+ display: flex;
1494
+ align-items: center;
1495
+ justify-content: center;
1496
+ z-index: 100000;
1497
+ animation: fadeIn 0.2s ease-out;
1498
+
1499
+ @keyframes fadeIn {
1500
+ from { opacity: 0; }
1501
+ to { opacity: 1; }
1502
+ }
1503
+ `;
1504
+ const Modal = styled.div`
1505
+ background: white;
1506
+ border-radius: 16px;
1507
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.2);
1508
+ max-width: 600px;
1509
+ width: 90%;
1510
+ overflow: hidden;
1511
+ animation: slideUp 0.3s ease-out;
1512
+
1513
+ @keyframes slideUp {
1514
+ from {
1515
+ opacity: 0;
1516
+ transform: translateY(20px);
1517
+ }
1518
+ to {
1519
+ opacity: 1;
1520
+ transform: translateY(0);
1521
+ }
1522
+ }
1523
+ `;
1524
+ const Header = styled.div`
1525
+ background: linear-gradient(135deg, #7C3AED 0%, #6d28d9 100%);
1526
+ padding: 24px;
1527
+ text-align: center;
1528
+ color: white;
1529
+ `;
1530
+ const Icon = styled.div`
1531
+ width: 64px;
1532
+ height: 64px;
1533
+ background: rgba(255, 255, 255, 0.2);
1534
+ border-radius: 50%;
1535
+ display: flex;
1536
+ align-items: center;
1537
+ justify-content: center;
1538
+ margin: 0 auto 16px;
1539
+ font-size: 28px;
1540
+ `;
1541
+ const Title = styled.h2`
1542
+ margin: 0 0 8px;
1543
+ font-size: 22px;
1544
+ font-weight: 700;
1545
+ `;
1546
+ const Subtitle = styled.p`
1547
+ margin: 0;
1548
+ font-size: 14px;
1549
+ opacity: 0.9;
1550
+ `;
1551
+ const Content = styled.div`
1552
+ padding: 24px;
1553
+ `;
1554
+ const PackagesGrid = styled.div`
1555
+ display: grid;
1556
+ grid-template-columns: repeat(3, 1fr);
1557
+ gap: 16px;
1558
+ margin-bottom: 24px;
1559
+
1560
+ @media (max-width: 540px) {
1561
+ grid-template-columns: 1fr;
1562
+ }
1563
+ `;
1564
+ const Package = styled.div`
1565
+ background: ${(props) => props.$best ? "linear-gradient(135deg, #f5f3ff 0%, #ede9fe 100%)" : "#f8fafc"};
1566
+ border: 2px solid ${(props) => props.$best ? "#7C3AED" : "#e2e8f0"};
1567
+ border-radius: 12px;
1568
+ padding: 20px;
1569
+ text-align: center;
1570
+ position: relative;
1571
+ transition: all 0.2s ease;
1572
+
1573
+ &:hover {
1574
+ transform: translateY(-4px);
1575
+ box-shadow: 0 8px 24px rgba(124, 58, 237, 0.15);
1576
+ }
1577
+
1578
+ ${(props) => props.$best && `
1579
+ transform: scale(1.05);
1580
+
1581
+ &:hover {
1582
+ transform: scale(1.05) translateY(-4px);
1583
+ }
1584
+ `}
1585
+ `;
1586
+ const BestBadge = styled.div`
1587
+ position: absolute;
1588
+ top: -10px;
1589
+ left: 50%;
1590
+ transform: translateX(-50%);
1591
+ background: linear-gradient(135deg, #7C3AED 0%, #6d28d9 100%);
1592
+ color: white;
1593
+ font-size: 10px;
1594
+ font-weight: 700;
1595
+ padding: 4px 12px;
1596
+ border-radius: 20px;
1597
+ text-transform: uppercase;
1598
+ letter-spacing: 0.5px;
1599
+ `;
1600
+ const Credits = styled.div`
1601
+ font-size: 32px;
1602
+ font-weight: 800;
1603
+ color: #1e293b;
1604
+ margin-bottom: 4px;
1605
+ `;
1606
+ const CreditsLabel = styled.div`
1607
+ font-size: 12px;
1608
+ color: #64748b;
1609
+ margin-bottom: 12px;
1610
+ `;
1611
+ const Price = styled.div`
1612
+ font-size: 24px;
1613
+ font-weight: 700;
1614
+ color: ${(props) => props.$best ? "#7C3AED" : "#334155"};
1615
+
1616
+ span {
1617
+ font-size: 14px;
1618
+ font-weight: 400;
1619
+ color: #64748b;
1620
+ }
1621
+ `;
1622
+ const PerCredit = styled.div`
1623
+ font-size: 11px;
1624
+ color: #94a3b8;
1625
+ margin-top: 4px;
1626
+ `;
1627
+ const Actions = styled.div`
1628
+ display: flex;
1629
+ gap: 12px;
1630
+ justify-content: center;
1631
+ `;
1632
+ const Button = styled.button`
1633
+ padding: 12px 32px;
1634
+ border-radius: 8px;
1635
+ font-size: 14px;
1636
+ font-weight: 600;
1637
+ cursor: pointer;
1638
+ transition: all 0.15s ease;
1639
+
1640
+ ${(props) => props.$primary ? `
1641
+ background: linear-gradient(135deg, #7C3AED 0%, #6d28d9 100%);
1642
+ border: none;
1643
+ color: white;
1644
+
1645
+ &:hover {
1646
+ transform: translateY(-2px);
1647
+ box-shadow: 0 4px 12px rgba(124, 58, 237, 0.4);
1648
+ }
1649
+ ` : `
1650
+ background: white;
1651
+ border: 1px solid #e2e8f0;
1652
+ color: #64748b;
1653
+
1654
+ &:hover {
1655
+ background: #f8fafc;
1656
+ border-color: #cbd5e1;
1657
+ }
1658
+ `}
1659
+
1660
+ &:active {
1661
+ transform: scale(0.98);
1662
+ }
1663
+ `;
1664
+ const Footer = styled.div`
1665
+ padding: 16px 24px;
1666
+ background: #f8fafc;
1667
+ border-top: 1px solid #e2e8f0;
1668
+ text-align: center;
1669
+ font-size: 12px;
1670
+ color: #64748b;
1671
+
1672
+ a {
1673
+ color: #7C3AED;
1674
+ text-decoration: none;
1675
+ font-weight: 500;
1676
+
1677
+ &:hover {
1678
+ text-decoration: underline;
1679
+ }
1680
+ }
1681
+ `;
1682
+ const CreditsModal = ({
1683
+ isOpen,
1684
+ onClose,
1685
+ upgradeInfo
1686
+ }) => {
1687
+ const navigate = useNavigate();
1688
+ if (!isOpen) return null;
1689
+ const packages = upgradeInfo?.packages || [
1690
+ { credits: 50, price: 4.99, currency: "EUR" },
1691
+ { credits: 200, price: 14.99, currency: "EUR", bestValue: true },
1692
+ { credits: 500, price: 29.99, currency: "EUR" }
1693
+ ];
1694
+ const handleUpgrade = () => {
1695
+ onClose();
1696
+ navigate("/settings/magic-editor-x/upgrade");
1697
+ };
1698
+ const formatPrice = (price, currency) => {
1699
+ return new Intl.NumberFormat("de-DE", {
1700
+ style: "currency",
1701
+ currency: currency || "EUR"
1702
+ }).format(price);
1703
+ };
1704
+ const getPerCredit = (price, credits) => {
1705
+ return (price / credits).toFixed(3).replace(".", ",");
1706
+ };
1707
+ return /* @__PURE__ */ jsx(Overlay, { onClick: onClose, children: /* @__PURE__ */ jsxs(Modal, { onClick: (e) => e.stopPropagation(), children: [
1708
+ /* @__PURE__ */ jsxs(Header, { children: [
1709
+ /* @__PURE__ */ jsx(Icon, { children: "✨" }),
1710
+ /* @__PURE__ */ jsx(Title, { children: "Keine Credits verfügbar" }),
1711
+ /* @__PURE__ */ jsx(Subtitle, { children: upgradeInfo?.message || "Kaufe Credits um Magic Editor AI zu nutzen" })
1712
+ ] }),
1713
+ /* @__PURE__ */ jsxs(Content, { children: [
1714
+ /* @__PURE__ */ jsx(PackagesGrid, { children: packages.map((pkg, index) => /* @__PURE__ */ jsxs(Package, { $best: pkg.bestValue, children: [
1715
+ pkg.bestValue && /* @__PURE__ */ jsx(BestBadge, { children: "Best Value" }),
1716
+ /* @__PURE__ */ jsx(Credits, { children: pkg.credits }),
1717
+ /* @__PURE__ */ jsx(CreditsLabel, { children: "Credits" }),
1718
+ /* @__PURE__ */ jsx(Price, { $best: pkg.bestValue, children: formatPrice(pkg.price, pkg.currency) }),
1719
+ /* @__PURE__ */ jsxs(PerCredit, { children: [
1720
+ getPerCredit(pkg.price, pkg.credits),
1721
+ " € / Credit"
1722
+ ] })
1723
+ ] }, index)) }),
1724
+ /* @__PURE__ */ jsxs(Actions, { children: [
1725
+ /* @__PURE__ */ jsx(Button, { onClick: onClose, children: "Später" }),
1726
+ /* @__PURE__ */ jsx(Button, { $primary: true, onClick: handleUpgrade, children: "Credits kaufen" })
1727
+ ] })
1728
+ ] }),
1729
+ /* @__PURE__ */ jsxs(Footer, { children: [
1730
+ "Credits werden nie ablaufen • ",
1731
+ /* @__PURE__ */ jsx("a", { href: "#", onClick: handleUpgrade, children: "Alle Pakete ansehen" })
1732
+ ] })
1733
+ ] }) });
1734
+ };
1735
+ const FullscreenGlobalStyle = createGlobalStyle`
1736
+ body.editor-fullscreen {
1737
+ overflow: hidden !important;
1738
+ }
1739
+ `;
1740
+ const EditorJSGlobalStyles = createGlobalStyle`
1741
+ /* Popover rendered at document body */
1742
+ body > .ce-popover,
1743
+ body > .ce-popover--opened,
1744
+ body > .ce-popover__container,
1745
+ body > .ce-settings,
1746
+ body > .ce-conversion-toolbar,
1747
+ body > .ce-inline-toolbar {
1748
+ z-index: 99999 !important;
1749
+ }
1750
+
1751
+ /* Ensure popovers are visible above Strapi modals */
1752
+ .ce-popover,
1753
+ .ce-popover--opened {
1754
+ z-index: 99999 !important;
1755
+ }
1756
+
1757
+ /* ============================================
1758
+ READONLY MODE - Block all editing interactions
1759
+ ============================================ */
1760
+ .editor-readonly {
1761
+ /* Block all pointer events on editing elements */
1762
+ .ce-block__content,
1763
+ .ce-paragraph,
1764
+ .cdx-block,
1765
+ .ce-header,
1766
+ [contenteditable] {
1767
+ pointer-events: none !important;
1768
+ cursor: not-allowed !important;
1769
+ user-select: text !important; /* Allow text selection for copying */
1770
+ }
1771
+
1772
+ /* Hide editing UI elements */
1773
+ .ce-toolbar,
1774
+ .ce-toolbar__plus,
1775
+ .ce-toolbar__actions,
1776
+ .ce-settings,
1777
+ .ce-block--selected::after,
1778
+ .ce-inline-toolbar,
1779
+ .ce-conversion-toolbar {
1780
+ display: none !important;
1781
+ visibility: hidden !important;
1782
+ pointer-events: none !important;
1783
+ }
1784
+
1785
+ /* Visual indicator that editor is readonly */
1786
+ .codex-editor {
1787
+ opacity: 0.9;
1788
+ }
1789
+
1790
+ /* Disable contenteditable */
1791
+ [contenteditable="true"] {
1792
+ pointer-events: none !important;
1793
+ -webkit-user-modify: read-only !important;
1794
+ }
1795
+ }
1796
+
1797
+ /* ============================================
1798
+ STRAPI MEDIA LIBRARY - Higher z-index for fullscreen
1799
+ ============================================ */
1800
+ [data-react-portal],
1801
+ .ReactModalPortal,
1802
+ [role="dialog"],
1803
+ [data-strapi-modal="true"],
1804
+ .upload-dialog,
1805
+ [class*="Modal"],
1806
+ [class*="modal"],
1807
+ [class*="Dialog"],
1808
+ [class*="dialog"] {
1809
+ z-index: 100001 !important;
1810
+ }
1811
+
1812
+ /* Strapi overlay */
1813
+ [data-strapi-modal-overlay="true"],
1814
+ [class*="Overlay"] {
1815
+ z-index: 100000 !important;
1816
+ }
1817
+
1818
+ .ce-popover__container {
1819
+ z-index: 99999 !important;
1820
+ background: white;
1821
+ border-radius: 8px;
1822
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15) !important;
1823
+ }
1824
+
1825
+ /* Settings popover */
1826
+ .ce-settings {
1827
+ z-index: 99999 !important;
1828
+ }
1829
+
1830
+ /* Inline toolbar */
1831
+ .ce-inline-toolbar {
1832
+ z-index: 99998 !important;
1833
+ }
1834
+
1835
+ /* Conversion toolbar */
1836
+ .ce-conversion-toolbar {
1837
+ z-index: 99999 !important;
1838
+ }
1839
+
1840
+ /* Block tune popovers */
1841
+ .ce-block-tunes,
1842
+ .ce-block-tunes__buttons {
1843
+ z-index: 99999 !important;
1844
+ }
1845
+ `;
1846
+ const EditorContainer = styled.div`
1847
+ position: ${(props) => props.$isFullscreen ? "fixed" : "relative"};
1848
+ top: ${(props) => props.$isFullscreen ? "0" : "auto"};
1849
+ left: ${(props) => props.$isFullscreen ? "0" : "auto"};
1850
+ right: ${(props) => props.$isFullscreen ? "0" : "auto"};
1851
+ bottom: ${(props) => props.$isFullscreen ? "0" : "auto"};
1852
+ z-index: ${(props) => props.$isFullscreen ? "9999" : "1"};
1853
+ background: ${(props) => props.$isFullscreen ? props.theme.colors.neutral100 : "transparent"};
1854
+ display: flex;
1855
+ flex-direction: column;
1856
+ min-height: ${(props) => props.$isFullscreen ? "100vh" : `${props.$minHeight}px`};
1857
+ height: ${(props) => props.$isFullscreen ? "100vh" : "auto"};
1858
+ margin-top: ${(props) => props.$isFullscreen ? "0" : "4px"};
1859
+
1860
+ ${(props) => props.$isFullscreen && css`
1861
+ padding: 20px;
1862
+
1863
+ @media (min-width: 1200px) {
1864
+ padding: 40px 80px;
1865
+ }
1866
+ `}
1867
+ `;
1868
+ const EditorCard = styled.div`
1869
+ background: ${(props) => props.theme.colors.neutral0};
1870
+ border: 1px solid ${(props) => props.$hasError ? "#dc2626" : props.$isFocused ? "#7C3AED" : props.theme.colors.neutral200};
1871
+ border-radius: 16px;
1872
+ display: flex;
1873
+ flex-direction: column;
1874
+ flex: 1;
1875
+ box-shadow: ${(props) => props.$isFocused ? "0 0 0 3px rgba(124, 58, 237, 0.1), 0 4px 16px rgba(0, 0, 0, 0.08)" : "0 1px 3px rgba(0, 0, 0, 0.04)"};
1876
+ transition: all 0.2s ease;
1877
+ resize: ${(props) => props.$isFullscreen ? "none" : "vertical"};
1878
+ min-height: ${(props) => props.$minHeight}px;
1879
+ overflow: visible; /* Allow dropdowns to escape container bounds */
1880
+
1881
+ &:hover {
1882
+ border-color: ${(props) => props.$hasError ? "#dc2626" : props.theme.colors.primary200};
1883
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
1884
+ }
1885
+
1886
+ ${(props) => props.$disabled && css`
1887
+ opacity: 0.6;
1888
+ pointer-events: none;
1889
+ background: ${(props2) => props2.theme.colors.neutral100};
1890
+ `}
1891
+ `;
1892
+ const EditorHeader = styled.div`
1893
+ display: flex;
1894
+ align-items: center;
1895
+ justify-content: space-between;
1896
+ padding: 12px 16px;
1897
+ background: ${(props) => props.theme.colors.neutral100};
1898
+ border-bottom: 1px solid ${(props) => props.theme.colors.neutral200};
1899
+ flex-shrink: 0;
1900
+ border-radius: 16px 16px 0 0;
1901
+ gap: 8px;
1902
+ flex-wrap: wrap;
1903
+
1904
+ @media (max-width: 640px) {
1905
+ padding: 10px 12px;
1906
+ gap: 6px;
1907
+ }
1908
+ `;
1909
+ const HeaderLeft = styled.div`
1910
+ display: flex;
1911
+ align-items: center;
1912
+ gap: 12px;
1913
+ flex-shrink: 0;
1914
+
1915
+ @media (max-width: 640px) {
1916
+ gap: 8px;
1917
+ }
1918
+ `;
1919
+ const Logo = styled.div`
1920
+ display: flex;
1921
+ align-items: center;
1922
+ gap: 8px;
1923
+
1924
+ svg {
1925
+ width: 20px;
1926
+ height: 20px;
1927
+ color: #7C3AED;
1928
+ }
1929
+
1930
+ @media (max-width: 480px) {
1931
+ gap: 4px;
1932
+
1933
+ svg {
1934
+ width: 18px;
1935
+ height: 18px;
1936
+ }
1937
+ }
1938
+ `;
1939
+ const LogoText = styled.span`
1940
+ font-size: 13px;
1941
+ font-weight: 600;
1942
+ color: #6366f1;
1943
+ text-transform: uppercase;
1944
+ letter-spacing: 0.5px;
1945
+
1946
+ @media (max-width: 768px) {
1947
+ font-size: 11px;
1948
+ }
1949
+
1950
+ @media (max-width: 480px) {
1951
+ display: none;
1952
+ }
1953
+ `;
1954
+ const STATUS_TOKENS = {
1955
+ idle: { bg: "#e2e8f0", color: "#475569", dot: "#94a3b8" },
1956
+ requesting: { bg: "#fef3c7", color: "#92400e", dot: "#f59e0b" },
1957
+ connecting: { bg: "#ede9fe", color: "#5b21b6", dot: "#7C3AED" },
1958
+ connected: { bg: "#dcfce7", color: "#166534", dot: "#22c55e" },
1959
+ disconnected: { bg: "#fee2e2", color: "#991b1b", dot: "#ef4444" },
1960
+ denied: { bg: "#fee2e2", color: "#b91c1c", dot: "#dc2626" },
1961
+ disabled: { bg: "#e2e8f0", color: "#475569", dot: "#94a3b8" }
1962
+ };
1963
+ const getStatusToken = (status) => STATUS_TOKENS[status] || STATUS_TOKENS.idle;
1964
+ const BlockCount = styled.span`
1965
+ font-size: 11px;
1966
+ font-weight: 600;
1967
+ color: #7C3AED;
1968
+ background: #ede9fe;
1969
+ padding: 4px 10px;
1970
+ border-radius: 20px;
1971
+ white-space: nowrap;
1972
+
1973
+ @media (max-width: 480px) {
1974
+ font-size: 10px;
1975
+ padding: 3px 8px;
1976
+ }
1977
+ `;
1978
+ styled.span`
1979
+ display: inline-flex;
1980
+ align-items: center;
1981
+ gap: 6px;
1982
+ font-size: 11px;
1983
+ font-weight: 600;
1984
+ padding: 4px 10px;
1985
+ border-radius: 999px;
1986
+ background: ${({ $status }) => getStatusToken($status).bg};
1987
+ color: ${({ $status }) => getStatusToken($status).color};
1988
+ border: 1px solid rgba(148, 163, 184, 0.25);
1989
+ `;
1990
+ styled.span`
1991
+ width: 8px;
1992
+ height: 8px;
1993
+ border-radius: 50%;
1994
+ background: ${({ $status }) => getStatusToken($status).dot};
1995
+ box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.6);
1996
+ `;
1997
+ styled.div`
1998
+ display: inline-flex;
1999
+ align-items: center;
2000
+ margin-left: 8px;
2001
+ `;
2002
+ styled.span`
2003
+ width: 22px;
2004
+ height: 22px;
2005
+ border-radius: 50%;
2006
+ background: rgba(124, 58, 237, 0.08);
2007
+ border: 1px solid rgba(124, 58, 237, 0.25);
2008
+ color: #5b21b6;
2009
+ font-size: 10px;
2010
+ font-weight: 700;
2011
+ display: inline-flex;
2012
+ align-items: center;
2013
+ justify-content: center;
2014
+ margin-left: -6px;
2015
+
2016
+ &:first-child {
2017
+ margin-left: 0;
2018
+ }
2019
+ `;
2020
+ const CollabNotice = styled.div`
2021
+ padding: 10px 16px;
2022
+ font-size: 12px;
2023
+ color: #b45309;
2024
+ background: #fffbeb;
2025
+ border-top: 1px solid #fde68a;
2026
+ `;
2027
+ const ViewerBanner = styled.div`
2028
+ display: flex;
2029
+ align-items: center;
2030
+ justify-content: center;
2031
+ gap: 10px;
2032
+ padding: 12px 20px;
2033
+ background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
2034
+ color: white;
2035
+ font-size: 14px;
2036
+ font-weight: 600;
2037
+ text-align: center;
2038
+
2039
+ svg {
2040
+ width: 20px;
2041
+ height: 20px;
2042
+ }
2043
+
2044
+ span {
2045
+ opacity: 0.9;
2046
+ font-weight: 400;
2047
+ }
2048
+
2049
+ @media (max-width: 640px) {
2050
+ padding: 10px 16px;
2051
+ font-size: 13px;
2052
+ gap: 8px;
2053
+ }
2054
+ `;
2055
+ styled.div`
2056
+ display: inline-flex;
2057
+ align-items: center;
2058
+ gap: 6px;
2059
+ padding: 4px 12px;
2060
+ border-radius: 20px;
2061
+ font-size: 12px;
2062
+ font-weight: 600;
2063
+ background: ${(props) => props.$role === "viewer" ? "rgba(59, 130, 246, 0.15)" : props.$role === "owner" ? "rgba(245, 158, 11, 0.15)" : "rgba(16, 185, 129, 0.15)"};
2064
+ color: ${(props) => props.$role === "viewer" ? "#1d4ed8" : props.$role === "owner" ? "#b45309" : "#047857"};
2065
+ border: 1px solid ${(props) => props.$role === "viewer" ? "rgba(59, 130, 246, 0.3)" : props.$role === "owner" ? "rgba(245, 158, 11, 0.3)" : "rgba(16, 185, 129, 0.3)"};
2066
+ `;
2067
+ styled.div`
2068
+ position: absolute;
2069
+ top: 60px;
2070
+ right: 16px;
2071
+ z-index: 50;
2072
+ display: flex;
2073
+ flex-direction: column;
2074
+ align-items: flex-end;
2075
+ gap: 8px;
2076
+
2077
+ @media (max-width: 768px) {
2078
+ top: 50px;
2079
+ right: 12px;
2080
+ gap: 6px;
2081
+ }
2082
+
2083
+ @media (max-width: 480px) {
2084
+ top: auto;
2085
+ bottom: 60px;
2086
+ right: 8px;
2087
+ gap: 4px;
2088
+ }
2089
+ `;
2090
+ styled.div`
2091
+ background: white;
2092
+ border: 1px solid ${({ $status }) => $status === "connected" ? "rgba(34, 197, 94, 0.3)" : $status === "denied" ? "rgba(239, 68, 68, 0.3)" : "rgba(124, 58, 237, 0.2)"};
2093
+ border-radius: 12px;
2094
+ padding: 10px 14px;
2095
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08), 0 2px 8px rgba(0, 0, 0, 0.04);
2096
+ display: flex;
2097
+ align-items: center;
2098
+ gap: 10px;
2099
+ backdrop-filter: blur(8px);
2100
+ transition: all 0.3s ease;
2101
+
2102
+ &:hover {
2103
+ transform: translateX(-4px);
2104
+ box-shadow: 0 6px 24px rgba(0, 0, 0, 0.12);
2105
+ }
2106
+
2107
+ @media (max-width: 640px) {
2108
+ padding: 8px 12px;
2109
+ gap: 8px;
2110
+ border-radius: 10px;
2111
+ }
2112
+
2113
+ @media (max-width: 480px) {
2114
+ padding: 6px 10px;
2115
+ gap: 6px;
2116
+ border-radius: 8px;
2117
+
2118
+ &:hover {
2119
+ transform: none;
2120
+ }
2121
+ }
2122
+ `;
2123
+ styled.div`
2124
+ width: 10px;
2125
+ height: 10px;
2126
+ border-radius: 50%;
2127
+ flex-shrink: 0;
2128
+ background: ${({ $status }) => getStatusToken($status).dot};
2129
+ box-shadow: ${({ $status }) => $status === "connected" ? "0 0 0 3px rgba(34, 197, 94, 0.2)" : $status === "denied" ? "0 0 0 3px rgba(239, 68, 68, 0.2)" : "0 0 0 3px rgba(124, 58, 237, 0.15)"};
2130
+
2131
+ ${({ $status }) => $status === "connected" && css`
2132
+ animation: pulse 2s ease-in-out infinite;
2133
+
2134
+ @keyframes pulse {
2135
+ 0%, 100% {
2136
+ box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.2);
2137
+ transform: scale(1);
2138
+ }
2139
+ 50% {
2140
+ box-shadow: 0 0 0 6px rgba(34, 197, 94, 0.1);
2141
+ transform: scale(1.1);
2142
+ }
2143
+ }
2144
+ `}
2145
+
2146
+ @media (max-width: 480px) {
2147
+ width: 8px;
2148
+ height: 8px;
2149
+ }
2150
+ `;
2151
+ styled.div`
2152
+ display: flex;
2153
+ flex-direction: column;
2154
+ gap: 2px;
2155
+ `;
2156
+ styled.span`
2157
+ font-size: 12px;
2158
+ font-weight: 600;
2159
+ color: ${({ $status }) => getStatusToken($status).color};
2160
+
2161
+ @media (max-width: 640px) {
2162
+ font-size: 11px;
2163
+ }
2164
+
2165
+ @media (max-width: 480px) {
2166
+ font-size: 10px;
2167
+ }
2168
+ `;
2169
+ styled.span`
2170
+ font-size: 10px;
2171
+ color: #94a3b8;
2172
+
2173
+ @media (max-width: 480px) {
2174
+ display: none;
2175
+ }
2176
+ `;
2177
+ styled.div`
2178
+ background: white;
2179
+ border: 1px solid rgba(124, 58, 237, 0.15);
2180
+ border-radius: 12px;
2181
+ padding: 8px 12px;
2182
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.06);
2183
+ display: flex;
2184
+ align-items: center;
2185
+ gap: 8px;
2186
+ transition: all 0.3s ease;
2187
+
2188
+ &:hover {
2189
+ transform: translateX(-4px);
2190
+ border-color: rgba(124, 58, 237, 0.3);
2191
+ }
2192
+
2193
+ @media (max-width: 640px) {
2194
+ padding: 6px 10px;
2195
+ gap: 6px;
2196
+ border-radius: 10px;
2197
+ }
2198
+
2199
+ @media (max-width: 480px) {
2200
+ padding: 4px 8px;
2201
+ border-radius: 8px;
2202
+
2203
+ &:hover {
2204
+ transform: none;
2205
+ }
2206
+ }
2207
+ `;
2208
+ styled.div`
2209
+ display: flex;
2210
+ align-items: center;
2211
+ `;
2212
+ styled.div`
2213
+ width: 28px;
2214
+ height: 28px;
2215
+ border-radius: 50%;
2216
+ background: ${({ $color }) => $color || "linear-gradient(135deg, #7C3AED 0%, #a855f7 100%)"};
2217
+ border: 2px solid white;
2218
+ color: white;
2219
+ font-size: 10px;
2220
+ font-weight: 700;
2221
+ display: flex;
2222
+ align-items: center;
2223
+ justify-content: center;
2224
+ margin-left: -8px;
2225
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
2226
+ transition: all 0.2s ease;
2227
+ cursor: default;
2228
+ position: relative;
2229
+
2230
+ &:first-child {
2231
+ margin-left: 0;
2232
+ }
2233
+
2234
+ &:hover {
2235
+ transform: translateY(-2px) scale(1.1);
2236
+ z-index: 10;
2237
+ }
2238
+
2239
+ @media (max-width: 640px) {
2240
+ width: 24px;
2241
+ height: 24px;
2242
+ font-size: 9px;
2243
+ margin-left: -6px;
2244
+
2245
+ &:first-child {
2246
+ margin-left: 0;
2247
+ }
2248
+ }
2249
+
2250
+ @media (max-width: 480px) {
2251
+ width: 22px;
2252
+ height: 22px;
2253
+ font-size: 8px;
2254
+ border-width: 1.5px;
2255
+ margin-left: -5px;
2256
+
2257
+ &:first-child {
2258
+ margin-left: 0;
2259
+ }
2260
+
2261
+ &:hover {
2262
+ transform: none;
2263
+ }
2264
+
2265
+ /* Hide tooltip on mobile */
2266
+ &::after {
2267
+ display: none;
2268
+ }
2269
+ }
2270
+
2271
+ &::after {
2272
+ content: attr(data-name);
2273
+ position: absolute;
2274
+ bottom: calc(100% + 6px);
2275
+ left: 50%;
2276
+ transform: translateX(-50%);
2277
+ background: #1e293b;
2278
+ color: white;
2279
+ font-size: 10px;
2280
+ font-weight: 500;
2281
+ padding: 4px 8px;
2282
+ border-radius: 6px;
2283
+ white-space: nowrap;
2284
+ opacity: 0;
2285
+ visibility: hidden;
2286
+ transition: all 0.15s ease;
2287
+ pointer-events: none;
2288
+ z-index: 100;
2289
+ }
2290
+
2291
+ &:hover::after {
2292
+ opacity: 1;
2293
+ visibility: visible;
2294
+ }
2295
+ `;
2296
+ styled.span`
2297
+ font-size: 11px;
2298
+ color: #64748b;
2299
+ font-weight: 500;
2300
+ white-space: nowrap;
2301
+
2302
+ @media (max-width: 640px) {
2303
+ font-size: 10px;
2304
+ }
2305
+
2306
+ @media (max-width: 480px) {
2307
+ display: none;
2308
+ }
2309
+ `;
2310
+ const Toolbar = styled.div`
2311
+ display: flex;
2312
+ align-items: center;
2313
+ gap: 4px;
2314
+ flex-wrap: wrap;
2315
+
2316
+ @media (max-width: 640px) {
2317
+ gap: 2px;
2318
+ }
2319
+ `;
2320
+ const ToolbarDivider = styled.div`
2321
+ width: 1px;
2322
+ height: 24px;
2323
+ background: ${(props) => props.theme.colors.neutral200};
2324
+ margin: 0 8px;
2325
+
2326
+ @media (max-width: 768px) {
2327
+ margin: 0 4px;
2328
+ height: 20px;
2329
+ }
2330
+
2331
+ @media (max-width: 480px) {
2332
+ display: none;
2333
+ }
2334
+ `;
2335
+ const ToolButton = styled.button`
2336
+ display: flex;
2337
+ align-items: center;
2338
+ justify-content: center;
2339
+ width: 34px;
2340
+ height: 34px;
2341
+ border: none;
2342
+ background: ${(props) => props.$active ? "#7C3AED" : "transparent"};
2343
+ color: ${(props) => props.$active ? "white" : props.theme.colors.neutral600};
2344
+ border-radius: 8px;
2345
+ cursor: pointer;
2346
+ transition: all 0.15s ease;
2347
+ position: relative;
2348
+ flex-shrink: 0;
2349
+
2350
+ svg {
2351
+ width: 18px;
2352
+ height: 18px;
2353
+ }
2354
+
2355
+ &:hover {
2356
+ background: ${(props) => props.$active ? "#6d28d9" : props.theme.colors.neutral150};
2357
+ color: ${(props) => props.$active ? "white" : "#7C3AED"};
2358
+ transform: translateY(-1px);
2359
+ }
2360
+
2361
+ &:active {
2362
+ transform: translateY(0);
2363
+ }
2364
+
2365
+ &::after {
2366
+ content: attr(data-tooltip);
2367
+ position: absolute;
2368
+ bottom: calc(100% + 8px);
2369
+ left: 50%;
2370
+ transform: translateX(-50%);
2371
+ background: #1e293b;
2372
+ color: white;
2373
+ font-size: 11px;
2374
+ font-weight: 500;
2375
+ padding: 6px 10px;
2376
+ border-radius: 6px;
2377
+ white-space: nowrap;
2378
+ opacity: 0;
2379
+ visibility: hidden;
2380
+ transition: all 0.15s ease;
2381
+ pointer-events: none;
2382
+ z-index: 100;
2383
+ }
2384
+
2385
+ &:hover::after {
2386
+ opacity: 1;
2387
+ visibility: visible;
2388
+ }
2389
+
2390
+ @media (max-width: 768px) {
2391
+ width: 32px;
2392
+ height: 32px;
2393
+
2394
+ svg {
2395
+ width: 16px;
2396
+ height: 16px;
2397
+ }
2398
+ }
2399
+
2400
+ @media (max-width: 480px) {
2401
+ width: 28px;
2402
+ height: 28px;
2403
+ border-radius: 6px;
2404
+
2405
+ svg {
2406
+ width: 14px;
2407
+ height: 14px;
2408
+ }
2409
+
2410
+ /* Hide tooltips on mobile - use touch */
2411
+ &::after {
2412
+ display: none;
2413
+ }
2414
+ }
2415
+ `;
2416
+ const QuickActions = styled.div`
2417
+ display: flex;
2418
+ align-items: center;
2419
+ gap: 2px;
2420
+ padding: 4px;
2421
+ background: ${(props) => props.theme.colors.neutral0};
2422
+ border-radius: 10px;
2423
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
2424
+ flex-wrap: wrap;
2425
+
2426
+ @media (max-width: 768px) {
2427
+ padding: 3px;
2428
+ gap: 1px;
2429
+ }
2430
+
2431
+ @media (max-width: 480px) {
2432
+ padding: 2px;
2433
+ border-radius: 8px;
2434
+
2435
+ /* Hide less essential buttons on mobile */
2436
+ & > button:nth-child(n+6) {
2437
+ display: none;
2438
+ }
2439
+ }
2440
+
2441
+ @media (max-width: 360px) {
2442
+ /* Show only first 4 buttons on very small screens */
2443
+ & > button:nth-child(n+5) {
2444
+ display: none;
2445
+ }
2446
+ }
2447
+ `;
2448
+ const EditorContent = styled.div`
2449
+ flex: 1;
2450
+ overflow: visible; /* Allow toolbars/popovers to escape */
2451
+ position: relative;
2452
+ padding: 24px 24px 24px 16px; /* Less left padding since toolbar has its own space */
2453
+ min-height: 200px;
2454
+
2455
+ ${(props) => props.$isFullscreen && css`
2456
+ padding: clamp(32px, 4vw, 60px);
2457
+ width: 100%;
2458
+ max-width: 100%;
2459
+ margin: 0;
2460
+ align-self: stretch;
2461
+ `}
2462
+ `;
2463
+ const EditorWrapper = styled.div`
2464
+ min-height: ${(props) => props.$minHeight - 120}px;
2465
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
2466
+ font-size: 15px;
2467
+ line-height: 1.7;
2468
+ color: ${(props) => props.theme.colors.neutral800};
2469
+ position: relative;
2470
+
2471
+ /* ============================================
2472
+ Z-INDEX FIXES FOR DROPDOWNS/POPOVERS
2473
+ ============================================ */
2474
+
2475
+ /* Main Editor.js container - ensure proper stacking context */
2476
+ .codex-editor {
2477
+ position: relative;
2478
+ z-index: 1;
2479
+ }
2480
+
2481
+ /* Toolbar - needs to be above content */
2482
+ .ce-toolbar {
2483
+ z-index: 100 !important;
2484
+ position: absolute;
2485
+ }
2486
+
2487
+ /* Toolbox (+ button dropdown) - keep position relative to toolbar */
2488
+ .ce-toolbox {
2489
+ z-index: 1000 !important;
2490
+ }
2491
+
2492
+ /* Popover/Dropdown menus - high z-index but keep natural positioning */
2493
+ .ce-popover {
2494
+ z-index: 10000 !important;
2495
+ }
2496
+
2497
+ .ce-popover--opened {
2498
+ z-index: 10000 !important;
2499
+ }
2500
+
2501
+ .ce-popover__container {
2502
+ z-index: 10000 !important;
2503
+ }
2504
+
2505
+ /* Block settings popover */
2506
+ .ce-settings {
2507
+ z-index: 10000 !important;
2508
+ }
2509
+
2510
+ /* Inline toolbar (bold, italic, etc.) */
2511
+ .ce-inline-toolbar {
2512
+ z-index: 9000 !important;
2513
+ }
2514
+
2515
+ /* Inline toolbar actions dropdown */
2516
+ .ce-inline-toolbar__dropdown {
2517
+ z-index: 9500 !important;
2518
+ }
2519
+
2520
+ /* Conversion toolbar */
2521
+ .ce-conversion-toolbar {
2522
+ z-index: 10000 !important;
2523
+ }
2524
+
2525
+ /* Search field in popovers */
2526
+ .cdx-search-field,
2527
+ .cdx-search-field__input {
2528
+ z-index: 10001 !important;
2529
+ }
2530
+
2531
+ /* ============================================
2532
+ TOOLBAR INSIDE EDITOR - Position Fix
2533
+ ============================================ */
2534
+
2535
+ /* Make the redactor (content area) have left padding for toolbar */
2536
+ .codex-editor__redactor {
2537
+ padding-bottom: 100px !important;
2538
+ padding-left: 50px !important; /* Space for toolbar */
2539
+ margin-left: 0 !important;
2540
+ }
2541
+
2542
+ /* Content blocks - full width within padded area */
2543
+ .ce-block__content {
2544
+ max-width: 100%;
2545
+ margin-left: 0;
2546
+ margin-right: 0;
2547
+ }
2548
+
2549
+ /* ============================================
2550
+ BLOCK HOVER EFFECT - Visual feedback
2551
+ ============================================ */
2552
+ .ce-block {
2553
+ position: relative;
2554
+ border-radius: 8px;
2555
+ transition: all 0.15s ease;
2556
+ margin: 2px 0;
2557
+
2558
+ &::before {
2559
+ content: '';
2560
+ position: absolute;
2561
+ top: -2px;
2562
+ left: -8px;
2563
+ right: -8px;
2564
+ bottom: -2px;
2565
+ border: 2px solid transparent;
2566
+ border-radius: 8px;
2567
+ pointer-events: none;
2568
+ transition: all 0.15s ease;
2569
+ }
2570
+
2571
+ &:hover::before {
2572
+ border-color: #e2e8f0;
2573
+ background: rgba(241, 245, 249, 0.3);
2574
+ }
2575
+
2576
+ /* Selected block - stronger highlight */
2577
+ &--selected::before {
2578
+ border-color: #cbd5e1 !important;
2579
+ background: rgba(203, 213, 225, 0.15) !important;
2580
+ }
2581
+
2582
+ /* Focused block */
2583
+ &--focused::before {
2584
+ border-color: #7C3AED !important;
2585
+ background: rgba(124, 58, 237, 0.05) !important;
2586
+ }
2587
+ }
2588
+
2589
+ /* Block content hover - subtle background */
2590
+ .ce-block__content:hover {
2591
+ background: rgba(248, 250, 252, 0.5);
2592
+ border-radius: 6px;
2593
+ }
2594
+
2595
+ /* Toolbar positioning - inside the editor */
2596
+ .ce-toolbar__content {
2597
+ max-width: 100%;
2598
+ margin-left: 0;
2599
+ }
2600
+
2601
+ .ce-toolbar {
2602
+ left: 0 !important;
2603
+ }
2604
+
2605
+ .ce-toolbar__plus {
2606
+ left: 0 !important;
2607
+ position: relative !important;
2608
+ }
2609
+
2610
+ .ce-toolbar__actions {
2611
+ right: 0 !important;
2612
+ position: absolute !important;
2613
+ }
2614
+
2615
+ /* Settings button (⋮⋮) */
2616
+ .ce-toolbar__settings-btn {
2617
+ margin-left: 4px;
2618
+ }
2619
+
2620
+ /* ============================================
2621
+ POPOVER/DROPDOWN STYLING
2622
+ ============================================ */
2623
+
2624
+ .ce-popover {
2625
+ border-radius: 12px !important;
2626
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12), 0 2px 8px rgba(0, 0, 0, 0.08) !important;
2627
+ border: 1px solid #e2e8f0 !important;
2628
+ overflow: hidden;
2629
+ }
2630
+
2631
+ .ce-popover__container {
2632
+ background: white !important;
2633
+ border-radius: 12px !important;
2634
+ }
2635
+
2636
+ .ce-popover__items {
2637
+ padding: 8px !important;
2638
+ }
2639
+
2640
+ .ce-popover-item {
2641
+ border-radius: 8px !important;
2642
+ padding: 8px 12px !important;
2643
+ margin: 2px 0 !important;
2644
+ transition: all 0.15s ease !important;
2645
+
2646
+ &:hover {
2647
+ background: #f1f5f9 !important;
2648
+ }
2649
+ }
2650
+
2651
+ .ce-popover-item__icon {
2652
+ color: #64748b !important;
2653
+ width: 20px !important;
2654
+ height: 20px !important;
2655
+ margin-right: 10px !important;
2656
+ }
2657
+
2658
+ .ce-popover-item__title {
2659
+ font-weight: 500 !important;
2660
+ color: #334155 !important;
2661
+ }
2662
+
2663
+ .ce-popover-item--focused {
2664
+ background: #ede9fe !important;
2665
+
2666
+ .ce-popover-item__icon {
2667
+ color: #7C3AED !important;
2668
+ }
2669
+
2670
+ .ce-popover-item__title {
2671
+ color: #7C3AED !important;
2672
+ }
2673
+ }
2674
+
2675
+ /* Search field in popover */
2676
+ .cdx-search-field {
2677
+ margin: 8px !important;
2678
+ border-radius: 8px !important;
2679
+ border: 1px solid #e2e8f0 !important;
2680
+ background: #f8fafc !important;
2681
+
2682
+ &:focus-within {
2683
+ border-color: #7C3AED !important;
2684
+ box-shadow: 0 0 0 3px rgba(124, 58, 237, 0.1) !important;
2685
+ }
2686
+ }
2687
+
2688
+ .cdx-search-field__input {
2689
+ font-size: 14px !important;
2690
+ color: #334155 !important;
2691
+
2692
+ &::placeholder {
2693
+ color: #94a3b8 !important;
2694
+ }
2695
+ }
2696
+
2697
+ /* Separator in popover */
2698
+ .ce-popover__item-separator {
2699
+ margin: 8px !important;
2700
+ background: #e2e8f0 !important;
2701
+ }
2702
+
2703
+ /* ============================================
2704
+ INDENT TUNE FIX
2705
+ ============================================ */
2706
+
2707
+ /* Indent tune container */
2708
+ .ce-popover-item[data-item-name="indentTune"],
2709
+ .ce-popover-item[data-item-name="indent"] {
2710
+ display: flex !important;
2711
+ align-items: center !important;
2712
+ gap: 8px !important;
2713
+ padding: 4px 8px !important;
2714
+ }
2715
+
2716
+ /* Indent tune wrapper - force horizontal layout */
2717
+ .indent-tune,
2718
+ [class*="indent-tune"],
2719
+ .ce-popover-item__custom-html {
2720
+ display: flex !important;
2721
+ align-items: center !important;
2722
+ gap: 4px !important;
2723
+ }
2724
+
2725
+ /* Indent buttons */
2726
+ .indent-tune__button,
2727
+ .indent-tune button,
2728
+ [class*="indent"] button {
2729
+ width: 32px !important;
2730
+ height: 32px !important;
2731
+ min-width: 32px !important;
2732
+ border: 1px solid #e2e8f0 !important;
2733
+ border-radius: 6px !important;
2734
+ background: white !important;
2735
+ cursor: pointer !important;
2736
+ display: flex !important;
2737
+ align-items: center !important;
2738
+ justify-content: center !important;
2739
+ transition: all 0.15s ease !important;
2740
+ pointer-events: auto !important;
2741
+ position: relative !important;
2742
+ z-index: 10 !important;
2743
+
2744
+ &:hover {
2745
+ background: #f1f5f9 !important;
2746
+ border-color: #7C3AED !important;
2747
+ }
2748
+
2749
+ &:active {
2750
+ background: #ede9fe !important;
2751
+ transform: scale(0.95) !important;
2752
+ }
2753
+ }
2754
+
2755
+ /* Indent button icons */
2756
+ .indent-tune__button svg,
2757
+ .indent-tune button svg,
2758
+ [class*="indent"] button svg {
2759
+ width: 16px !important;
2760
+ height: 16px !important;
2761
+ color: #64748b !important;
2762
+ }
2763
+
2764
+ /* Indent label */
2765
+ .indent-tune__label,
2766
+ .indent-tune span:not(:empty) {
2767
+ font-size: 13px !important;
2768
+ font-weight: 500 !important;
2769
+ color: #334155 !important;
2770
+ margin: 0 4px !important;
2771
+ }
2772
+
2773
+ /* ============================================
2774
+ ALIGNMENT TUNE FIX
2775
+ ============================================ */
2776
+
2777
+ .ce-popover-item[data-item-name="alignmentTune"],
2778
+ .ce-popover-item[data-item-name="alignment"] {
2779
+ display: flex !important;
2780
+ align-items: center !important;
2781
+ gap: 4px !important;
2782
+ padding: 4px 8px !important;
2783
+ }
2784
+
2785
+ .alignment-tune,
2786
+ [class*="alignment-tune"],
2787
+ [class*="text-alignment"] {
2788
+ display: flex !important;
2789
+ align-items: center !important;
2790
+ gap: 2px !important;
2791
+ }
2792
+
2793
+ .alignment-tune button,
2794
+ [class*="alignment"] button,
2795
+ [class*="text-alignment"] button {
2796
+ width: 28px !important;
2797
+ height: 28px !important;
2798
+ min-width: 28px !important;
2799
+ border: 1px solid transparent !important;
2800
+ border-radius: 4px !important;
2801
+ background: transparent !important;
2802
+ cursor: pointer !important;
2803
+ display: flex !important;
2804
+ align-items: center !important;
2805
+ justify-content: center !important;
2806
+ transition: all 0.15s ease !important;
2807
+ pointer-events: auto !important;
2808
+
2809
+ &:hover {
2810
+ background: #f1f5f9 !important;
2811
+ }
2812
+
2813
+ &.active,
2814
+ &[class*="active"] {
2815
+ background: #ede9fe !important;
2816
+ border-color: #7C3AED !important;
2817
+ }
2818
+ }
2819
+
2820
+ .alignment-tune button svg,
2821
+ [class*="alignment"] button svg {
2822
+ width: 16px !important;
2823
+ height: 16px !important;
2824
+ color: #64748b !important;
2825
+ }
2826
+
2827
+ /* Remote user editing highlight - subtle background only, cursor shown separately */
2828
+ .ce-block--remote-editing {
2829
+ position: relative;
2830
+ background: rgba(59, 130, 246, 0.04);
2831
+ border-radius: 4px;
2832
+ transition: background 0.2s ease;
2833
+ }
2834
+
2835
+ /* Typing indicator animation */
2836
+ .typing-indicator .dot {
2837
+ display: inline-block;
2838
+ width: 4px;
2839
+ height: 4px;
2840
+ border-radius: 50%;
2841
+ background: currentColor;
2842
+ opacity: 0.7;
2843
+ animation: typingBounce 1.4s ease-in-out infinite;
2844
+ }
2845
+
2846
+ .typing-indicator .dot:nth-child(1) {
2847
+ animation-delay: 0s;
2848
+ }
2849
+
2850
+ .typing-indicator .dot:nth-child(2) {
2851
+ animation-delay: 0.2s;
2852
+ }
2853
+
2854
+ .typing-indicator .dot:nth-child(3) {
2855
+ animation-delay: 0.4s;
2856
+ }
2857
+
2858
+ @keyframes typingBounce {
2859
+ 0%, 60%, 100% {
2860
+ transform: translateY(0);
2861
+ opacity: 0.4;
2862
+ }
2863
+ 30% {
2864
+ transform: translateY(-3px);
2865
+ opacity: 1;
2866
+ }
2867
+ }
2868
+
2869
+ /* Remote cursor pulse animation */
2870
+ .remote-cursor-indicator {
2871
+ animation: cursorPulse 1.5s ease-in-out infinite;
2872
+ }
2873
+
2874
+ @keyframes cursorPulse {
2875
+ 0%, 100% {
2876
+ opacity: 1;
2877
+ box-shadow: 0 0 8px currentColor;
2878
+ }
2879
+ 50% {
2880
+ opacity: 0.7;
2881
+ box-shadow: 0 0 12px currentColor;
2882
+ }
2883
+ }
2884
+ `;
2885
+ const EmptyState = styled.div`
2886
+ position: absolute;
2887
+ top: 50%;
2888
+ left: 50%;
2889
+ transform: translate(-50%, -50%);
2890
+ text-align: center;
2891
+ pointer-events: none;
2892
+ `;
2893
+ const EmptyIcon = styled.div`
2894
+ width: 100px;
2895
+ height: 100px;
2896
+ margin: 0 auto 20px;
2897
+ background: linear-gradient(135deg, #f0f4ff 0%, #e8ecff 100%);
2898
+ border-radius: 50%;
2899
+ display: flex;
2900
+ align-items: center;
2901
+ justify-content: center;
2902
+ animation: float 4s ease-in-out infinite;
2903
+
2904
+ svg {
2905
+ width: 48px;
2906
+ height: 48px;
2907
+ color: #a5b4fc;
2908
+ }
2909
+
2910
+ @keyframes float {
2911
+ 0%, 100% { transform: translateY(0); }
2912
+ 50% { transform: translateY(-10px); }
2913
+ }
2914
+
2915
+ @media (max-width: 480px) {
2916
+ width: 72px;
2917
+ height: 72px;
2918
+ margin: 0 auto 16px;
2919
+
2920
+ svg {
2921
+ width: 32px;
2922
+ height: 32px;
2923
+ }
2924
+ }
2925
+ `;
2926
+ const EmptyTitle = styled.h3`
2927
+ font-size: 18px;
2928
+ font-weight: 600;
2929
+ color: #64748b;
2930
+ margin: 0 0 8px 0;
2931
+
2932
+ @media (max-width: 480px) {
2933
+ font-size: 16px;
2934
+ }
2935
+ `;
2936
+ const EmptySubtitle = styled.p`
2937
+ font-size: 14px;
2938
+ color: #94a3b8;
2939
+ margin: 0 0 20px 0;
2940
+
2941
+ @media (max-width: 480px) {
2942
+ font-size: 12px;
2943
+ margin: 0 0 16px 0;
2944
+ padding: 0 16px;
2945
+ }
2946
+ `;
2947
+ const KeyboardHints = styled.div`
2948
+ display: flex;
2949
+ gap: 16px;
2950
+ justify-content: center;
2951
+ flex-wrap: wrap;
2952
+
2953
+ @media (max-width: 640px) {
2954
+ gap: 10px;
2955
+ }
2956
+
2957
+ @media (max-width: 480px) {
2958
+ display: none; /* Hide keyboard hints on mobile - not useful for touch */
2959
+ }
2960
+ `;
2961
+ const KeyHint = styled.span`
2962
+ font-size: 12px;
2963
+ color: #94a3b8;
2964
+
2965
+ kbd {
2966
+ display: inline-block;
2967
+ padding: 2px 8px;
2968
+ background: #f1f5f9;
2969
+ border: 1px solid #e2e8f0;
2970
+ border-radius: 6px;
2971
+ font-family: 'SF Mono', Monaco, monospace;
2972
+ font-size: 11px;
2973
+ color: #475569;
2974
+ margin-right: 4px;
2975
+ box-shadow: 0 1px 0 rgba(0,0,0,0.05);
2976
+ }
2977
+
2978
+ @media (max-width: 640px) {
2979
+ font-size: 10px;
2980
+
2981
+ kbd {
2982
+ padding: 2px 6px;
2983
+ font-size: 9px;
2984
+ }
2985
+ }
2986
+ `;
2987
+ const EditorFooter = styled.div`
2988
+ display: flex;
2989
+ align-items: center;
2990
+ justify-content: space-between;
2991
+ padding: 10px 16px;
2992
+ background: ${(props) => props.theme.colors.neutral100};
2993
+ border-top: 1px solid ${(props) => props.theme.colors.neutral150};
2994
+ flex-shrink: 0;
2995
+ border-radius: 0 0 16px 16px;
2996
+ gap: 8px;
2997
+ flex-wrap: wrap;
2998
+
2999
+ @media (max-width: 640px) {
3000
+ padding: 8px 12px;
3001
+ }
3002
+
3003
+ @media (max-width: 480px) {
3004
+ padding: 8px 10px;
3005
+ gap: 6px;
3006
+ }
3007
+ `;
3008
+ const FooterLeft = styled.div`
3009
+ display: flex;
3010
+ align-items: center;
3011
+ gap: 16px;
3012
+
3013
+ @media (max-width: 640px) {
3014
+ gap: 12px;
3015
+ }
3016
+
3017
+ @media (max-width: 480px) {
3018
+ gap: 8px;
3019
+ }
3020
+ `;
3021
+ const FooterStat = styled.span`
3022
+ font-size: 12px;
3023
+ color: ${(props) => props.theme.colors.neutral500};
3024
+ white-space: nowrap;
3025
+
3026
+ strong {
3027
+ color: ${(props) => props.theme.colors.neutral700};
3028
+ font-weight: 600;
3029
+ }
3030
+
3031
+ @media (max-width: 640px) {
3032
+ font-size: 11px;
3033
+ }
3034
+
3035
+ @media (max-width: 480px) {
3036
+ font-size: 10px;
3037
+ }
3038
+ `;
3039
+ const FooterRight = styled.div`
3040
+ display: flex;
3041
+ align-items: center;
3042
+ gap: 8px;
3043
+
3044
+ @media (max-width: 640px) {
3045
+ gap: 6px;
3046
+ }
3047
+ `;
3048
+ const FooterButton = styled.button`
3049
+ display: inline-flex;
3050
+ align-items: center;
3051
+ gap: 6px;
3052
+ padding: 6px 14px;
3053
+ background: ${(props) => props.$primary ? "linear-gradient(135deg, #7C3AED 0%, #6d28d9 100%)" : props.theme.colors.neutral0};
3054
+ border: 1px solid ${(props) => props.$primary ? "transparent" : props.theme.colors.neutral200};
3055
+ border-radius: 8px;
3056
+ font-size: 12px;
3057
+ font-weight: 500;
3058
+ color: ${(props) => props.$primary ? "white" : props.theme.colors.neutral600};
3059
+ cursor: pointer;
3060
+ transition: all 0.15s ease;
3061
+ white-space: nowrap;
3062
+
3063
+ svg {
3064
+ width: 14px;
3065
+ height: 14px;
3066
+ }
3067
+
3068
+ &:hover {
3069
+ background: ${(props) => props.$primary ? "linear-gradient(135deg, #6d28d9 0%, #5b21b6 100%)" : "#f8fafc"};
3070
+ border-color: ${(props) => props.$primary ? "transparent" : "#7C3AED"};
3071
+ color: ${(props) => props.$primary ? "white" : "#7C3AED"};
3072
+ transform: translateY(-1px);
3073
+ box-shadow: ${(props) => props.$primary ? "0 4px 12px rgba(124, 58, 237, 0.3)" : "none"};
3074
+ }
3075
+
3076
+ @media (max-width: 640px) {
3077
+ padding: 5px 10px;
3078
+ font-size: 11px;
3079
+ gap: 4px;
3080
+
3081
+ svg {
3082
+ width: 12px;
3083
+ height: 12px;
3084
+ }
3085
+ }
3086
+
3087
+ @media (max-width: 480px) {
3088
+ padding: 4px 8px;
3089
+ font-size: 10px;
3090
+ border-radius: 6px;
3091
+
3092
+ /* Hide text on very small screens, show only icon */
3093
+ & > span {
3094
+ display: none;
3095
+ }
3096
+ }
3097
+ `;
3098
+ const LoadingOverlay = styled.div`
3099
+ position: absolute;
3100
+ top: 0;
3101
+ left: 0;
3102
+ right: 0;
3103
+ bottom: 0;
3104
+ background: rgba(255, 255, 255, 0.9);
3105
+ display: flex;
3106
+ flex-direction: column;
3107
+ align-items: center;
3108
+ justify-content: center;
3109
+ gap: 12px;
3110
+ z-index: 10;
3111
+ `;
3112
+ const LoadingText = styled.span`
3113
+ font-size: 13px;
3114
+ color: #64748b;
3115
+ `;
3116
+ const ResizeHandle = styled.div`
3117
+ position: absolute;
3118
+ bottom: 0;
3119
+ left: 50%;
3120
+ transform: translateX(-50%);
3121
+ width: 60px;
3122
+ height: 16px;
3123
+ cursor: ns-resize;
3124
+ display: flex;
3125
+ align-items: center;
3126
+ justify-content: center;
3127
+
3128
+ &::before {
3129
+ content: '';
3130
+ width: 40px;
3131
+ height: 4px;
3132
+ background: #e2e8f0;
3133
+ border-radius: 2px;
3134
+ transition: background 0.15s ease;
3135
+ }
3136
+
3137
+ &:hover::before {
3138
+ background: #7C3AED;
3139
+ }
3140
+ `;
3141
+ styled.div`
3142
+ position: absolute;
3143
+ top: 0;
3144
+ left: 0;
3145
+ right: 0;
3146
+ bottom: 0;
3147
+ pointer-events: none;
3148
+ z-index: 100;
3149
+ overflow: hidden;
3150
+ `;
3151
+ styled.div`
3152
+ position: absolute;
3153
+ pointer-events: none;
3154
+ transition: all 0.15s ease-out;
3155
+ z-index: 100;
3156
+ `;
3157
+ styled.div`
3158
+ width: 2px;
3159
+ height: 20px;
3160
+ background: ${(props) => props.$color || "#3B82F6"};
3161
+ border-radius: 1px;
3162
+ box-shadow: 0 0 4px ${(props) => props.$color || "#3B82F6"}40;
3163
+ animation: cursorBlink 1s ease-in-out infinite;
3164
+
3165
+ @keyframes cursorBlink {
3166
+ 0%, 100% { opacity: 1; }
3167
+ 50% { opacity: 0.6; }
3168
+ }
3169
+ `;
3170
+ styled.div`
3171
+ position: absolute;
3172
+ top: -22px;
3173
+ left: 0;
3174
+ background: ${(props) => props.$bgColor || "#3B82F6"};
3175
+ color: ${(props) => props.$textColor || "#FFFFFF"};
3176
+ font-size: 10px;
3177
+ font-weight: 600;
3178
+ padding: 2px 6px;
3179
+ border-radius: 4px 4px 4px 0;
3180
+ white-space: nowrap;
3181
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
3182
+ transform-origin: bottom left;
3183
+ animation: cursorLabelFadeIn 0.2s ease-out;
3184
+
3185
+ @keyframes cursorLabelFadeIn {
3186
+ from {
3187
+ opacity: 0;
3188
+ transform: scale(0.8);
3189
+ }
3190
+ to {
3191
+ opacity: 1;
3192
+ transform: scale(1);
3193
+ }
3194
+ }
3195
+ `;
3196
+ styled.div`
3197
+ position: absolute;
3198
+ background: ${(props) => props.$color || "#3B82F6"}20;
3199
+ border: 1px solid ${(props) => props.$color || "#3B82F6"}40;
3200
+ border-radius: 2px;
3201
+ pointer-events: none;
3202
+ `;
3203
+ const ActiveEditorsBar = styled.div`
3204
+ display: flex;
3205
+ align-items: center;
3206
+ gap: 8px;
3207
+ padding: 8px 16px;
3208
+ background: linear-gradient(90deg, #f8fafc 0%, #f1f5f9 100%);
3209
+ border-bottom: 1px solid #e2e8f0;
3210
+ flex-shrink: 0;
3211
+ flex-wrap: wrap;
3212
+
3213
+ @media (max-width: 640px) {
3214
+ padding: 6px 12px;
3215
+ gap: 6px;
3216
+ }
3217
+
3218
+ @media (max-width: 480px) {
3219
+ padding: 6px 10px;
3220
+ gap: 4px;
3221
+ }
3222
+ `;
3223
+ const ActiveEditorBadge = styled.div`
3224
+ display: inline-flex;
3225
+ align-items: center;
3226
+ gap: 6px;
3227
+ padding: 4px 10px;
3228
+ background: white;
3229
+ border: 1px solid ${(props) => props.$color || "#e2e8f0"};
3230
+ border-radius: 20px;
3231
+ font-size: 11px;
3232
+ font-weight: 500;
3233
+ color: #374151;
3234
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
3235
+ transition: all 0.2s ease;
3236
+ white-space: nowrap;
3237
+
3238
+ &:hover {
3239
+ transform: translateY(-1px);
3240
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
3241
+ }
3242
+
3243
+ @media (max-width: 640px) {
3244
+ padding: 3px 8px;
3245
+ font-size: 10px;
3246
+ gap: 4px;
3247
+ }
3248
+
3249
+ @media (max-width: 480px) {
3250
+ /* Hide "Block X" info on mobile */
3251
+ & > span:last-child {
3252
+ display: none;
3253
+ }
3254
+ }
3255
+ `;
3256
+ const ActiveEditorDot = styled.span`
3257
+ width: 8px;
3258
+ height: 8px;
3259
+ border-radius: 50%;
3260
+ background: ${(props) => props.$color || "#3B82F6"};
3261
+ box-shadow: 0 0 0 2px ${(props) => props.$color || "#3B82F6"}30;
3262
+ animation: pulse 2s ease-in-out infinite;
3263
+ flex-shrink: 0;
3264
+
3265
+ @keyframes pulse {
3266
+ 0%, 100% { box-shadow: 0 0 0 2px ${(props) => props.$color || "#3B82F6"}30; }
3267
+ 50% { box-shadow: 0 0 0 4px ${(props) => props.$color || "#3B82F6"}20; }
3268
+ }
3269
+
3270
+ @media (max-width: 480px) {
3271
+ width: 6px;
3272
+ height: 6px;
3273
+ }
3274
+ `;
3275
+ const ActiveEditorsLabel = styled.span`
3276
+ font-size: 11px;
3277
+ color: #64748b;
3278
+ font-weight: 500;
3279
+ white-space: nowrap;
3280
+
3281
+ @media (max-width: 640px) {
3282
+ font-size: 10px;
3283
+ }
3284
+
3285
+ @media (max-width: 480px) {
3286
+ display: none;
3287
+ }
3288
+ `;
3289
+ const buildRoomId = (fieldName, documentId = null) => {
3290
+ if (typeof window === "undefined") {
3291
+ return `ssr|${fieldName}`;
3292
+ }
3293
+ const path = window.location.pathname;
3294
+ const matches = path.match(/content-manager\/(collection-types|single-types)\/([^/]+)(?:\/([^/?]+))?/);
3295
+ const contentType = matches?.[2] ? decodeURIComponent(matches[2]) : "unknown";
3296
+ const urlDocumentId = matches?.[3] || null;
3297
+ let docId = documentId || urlDocumentId;
3298
+ if (!docId) {
3299
+ docId = matches?.[1] === "single-types" ? "single" : "new";
3300
+ }
3301
+ const roomId = `${contentType}|${docId}|${fieldName}`;
3302
+ console.log("[Magic Editor X] buildRoomId:", { contentType, documentId: docId, fieldName, roomId });
3303
+ return roomId;
3304
+ };
3305
+ const getPeerName = (user = {}) => {
3306
+ if (user.firstname || user.lastname) {
3307
+ return `${user.firstname || ""} ${user.lastname || ""}`.trim();
3308
+ }
3309
+ return user.email?.split("@")[0] || "Anonymous";
3310
+ };
3311
+ const Editor = forwardRef(({
3312
+ name,
3313
+ value,
3314
+ onChange,
3315
+ attribute,
3316
+ disabled,
3317
+ error,
3318
+ required,
3319
+ hint,
3320
+ label,
3321
+ labelAction,
3322
+ placeholder,
3323
+ ...props
3324
+ }, ref) => {
3325
+ const { formatMessage } = useIntl();
3326
+ const t = (id, defaultMessage) => formatMessage({ id: getTranslation(id), defaultMessage });
3327
+ const { licenseData } = useLicense();
3328
+ const editorRef = useRef(null);
3329
+ const editorInstanceRef = useRef(null);
3330
+ const containerRef = useRef(null);
3331
+ const isReadyRef = useRef(false);
3332
+ const [isReady, setIsReady] = useState(false);
3333
+ const [showCreditsModal, setShowCreditsModal] = useState(false);
3334
+ const [creditsUpgradeInfo, setCreditsUpgradeInfo] = useState(null);
3335
+ const { handleAIAction } = useAIActions({
3336
+ licenseKey: licenseData?.licenseKey,
3337
+ editorInstanceRef,
3338
+ isReady,
3339
+ onNoCredits: (upgradeInfo) => {
3340
+ setCreditsUpgradeInfo(upgradeInfo);
3341
+ setShowCreditsModal(true);
3342
+ }
3343
+ });
3344
+ useEffect(() => {
3345
+ if (licenseData?.licenseKey) {
3346
+ window.__MAGIC_EDITOR_LICENSE_KEY__ = licenseData.licenseKey;
3347
+ }
3348
+ return () => {
3349
+ delete window.__MAGIC_EDITOR_LICENSE_KEY__;
3350
+ };
3351
+ }, [licenseData?.licenseKey]);
3352
+ const [blocksCount, setBlocksCount] = useState(0);
3353
+ const [wordCount, setWordCount] = useState(0);
3354
+ const [charCount, setCharCount] = useState(0);
3355
+ const [isFocused, setIsFocused] = useState(false);
3356
+ const [isFullscreen, setIsFullscreen] = useState(false);
3357
+ const [editorHeight, setEditorHeight] = useState(400);
3358
+ const [mediaLibBlockIndex, setMediaLibBlockIndex] = useState(-1);
3359
+ const [isMediaLibOpen, setIsMediaLibOpen] = useState(false);
3360
+ const [showAIPopup, setShowAIPopup] = useState(false);
3361
+ const [showAIToolbar, setShowAIToolbar] = useState(false);
3362
+ const [aiToolbarPosition, setAIToolbarPosition] = useState({ top: 0, left: 0 });
3363
+ const [aiSelectedText, setAISelectedText] = useState("");
3364
+ const aiSelectionRangeRef = useRef(null);
3365
+ const [aiLoading, setAILoading] = useState(false);
3366
+ const serializedInitialValue = useMemo(() => {
3367
+ if (!value) {
3368
+ return "";
3369
+ }
3370
+ return typeof value === "string" ? value : JSON.stringify(value);
3371
+ }, [value]);
3372
+ const serializeForCompare = useCallback((payload) => {
3373
+ if (!payload) return "";
3374
+ try {
3375
+ const dataObj = typeof payload === "string" ? JSON.parse(payload) : payload;
3376
+ if (!dataObj || typeof dataObj !== "object") return "";
3377
+ const clone = { ...dataObj };
3378
+ if ("time" in clone) {
3379
+ clone.time = 0;
3380
+ }
3381
+ return JSON.stringify(clone);
3382
+ } catch (err) {
3383
+ console.warn("[Magic Editor X] [COMPARE] Failed to normalize payload", err?.message);
3384
+ return "";
3385
+ }
3386
+ }, []);
3387
+ const collabRoomId = useMemo(() => buildRoomId(name), [name]);
3388
+ const collabEnabled = (attribute?.options?.collaboration?.enabled ?? true) && !disabled;
3389
+ const renderFromYDocRef = useRef(null);
3390
+ const {
3391
+ doc: yDoc,
3392
+ blocksMap: yBlocksMap,
3393
+ metaMap: yMetaMap,
3394
+ status: collabStatus,
3395
+ error: collabError,
3396
+ peers: collabPeers,
3397
+ awareness: collabAwareness,
3398
+ emitAwareness,
3399
+ collabRole,
3400
+ canEdit: collabCanEdit
3401
+ } = useMagicCollaboration({
3402
+ enabled: collabEnabled,
3403
+ roomId: collabRoomId,
3404
+ fieldName: name,
3405
+ initialValue: serializedInitialValue || "",
3406
+ onRemoteUpdate: () => {
3407
+ console.log("[Magic Editor X] [CALLBACK] onRemoteUpdate callback received");
3408
+ if (renderFromYDocRef.current) {
3409
+ renderFromYDocRef.current();
3410
+ }
3411
+ }
3412
+ });
3413
+ useMemo(() => {
3414
+ switch (collabRole) {
3415
+ case "viewer":
3416
+ return { icon: "V", text: "Viewer", color: "#3b82f6" };
3417
+ case "editor":
3418
+ return { icon: "E", text: "Editor", color: "#10b981" };
3419
+ case "owner":
3420
+ return { icon: "O", text: "Owner", color: "#f59e0b" };
3421
+ default:
3422
+ return null;
3423
+ }
3424
+ }, [collabRole]);
3425
+ useMemo(() => {
3426
+ switch (collabStatus) {
3427
+ case "connected":
3428
+ if (collabRole === "viewer") {
3429
+ return "Nur Lesen";
3430
+ }
3431
+ if (collabRole === "owner") {
3432
+ return "Live Sync";
3433
+ }
3434
+ return "Live Sync";
3435
+ case "connecting":
3436
+ return "Verbinde...";
3437
+ case "requesting":
3438
+ return "Freigabe pruefen";
3439
+ case "denied":
3440
+ return "Freigabe gesperrt";
3441
+ case "disconnected":
3442
+ return "Neu verbinden";
3443
+ case "disabled":
3444
+ return "Sync aus";
3445
+ default:
3446
+ return "Bereit";
3447
+ }
3448
+ }, [collabStatus, collabRole]);
3449
+ useMemo(() => collabPeers.slice(0, 3), [collabPeers]);
3450
+ const activeEditors = useMemo(() => {
3451
+ return Object.values(collabAwareness || {}).filter(
3452
+ (entry) => entry?.user && Date.now() - entry.lastUpdate < 3e4
3453
+ );
3454
+ }, [collabAwareness]);
3455
+ const lastCursorUpdateRef = useRef(0);
3456
+ const lastEmittedPositionRef = useRef(null);
3457
+ const getAbsoluteOffset = useCallback((container, node, offset) => {
3458
+ if (!container || !node) return 0;
3459
+ let absoluteOffset = 0;
3460
+ const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT, null, false);
3461
+ let currentNode = walker.nextNode();
3462
+ while (currentNode) {
3463
+ if (currentNode === node) {
3464
+ return absoluteOffset + offset;
3465
+ }
3466
+ absoluteOffset += currentNode.textContent?.length || 0;
3467
+ currentNode = walker.nextNode();
3468
+ }
3469
+ return absoluteOffset + offset;
3470
+ }, []);
3471
+ const emitCursorPosition = useCallback(() => {
3472
+ if (!collabEnabled || !emitAwareness || collabStatus !== "connected") {
3473
+ return;
3474
+ }
3475
+ const now = Date.now();
3476
+ if (now - lastCursorUpdateRef.current < 200) {
3477
+ return;
3478
+ }
3479
+ if (!editorInstanceRef.current) {
3480
+ return;
3481
+ }
3482
+ try {
3483
+ const currentBlockIndex = editorInstanceRef.current.blocks.getCurrentBlockIndex();
3484
+ if (currentBlockIndex < 0) {
3485
+ return;
3486
+ }
3487
+ const currentBlock = editorInstanceRef.current.blocks.getBlockByIndex(currentBlockIndex);
3488
+ const blockId = currentBlock?.id || null;
3489
+ const selection = window.getSelection();
3490
+ let selectionInfo = null;
3491
+ if (selection && selection.rangeCount > 0) {
3492
+ const range = selection.getRangeAt(0);
3493
+ const blocks = editorRef.current?.querySelectorAll(".ce-block");
3494
+ const targetBlock = blocks?.[currentBlockIndex];
3495
+ const contentContainer = targetBlock?.querySelector(
3496
+ '.ce-paragraph, .ce-header, [contenteditable="true"], .cdx-block'
3497
+ );
3498
+ const absoluteStart = contentContainer ? getAbsoluteOffset(contentContainer, range.startContainer, range.startOffset) : range.startOffset;
3499
+ const absoluteEnd = contentContainer ? getAbsoluteOffset(contentContainer, range.endContainer, range.endOffset) : range.endOffset;
3500
+ selectionInfo = {
3501
+ collapsed: selection.isCollapsed,
3502
+ startOffset: absoluteStart,
3503
+ endOffset: absoluteEnd
3504
+ };
3505
+ }
3506
+ const newPosition = JSON.stringify({ blockIndex: currentBlockIndex, blockId, selection: selectionInfo });
3507
+ if (newPosition === lastEmittedPositionRef.current) {
3508
+ return;
3509
+ }
3510
+ lastCursorUpdateRef.current = now;
3511
+ lastEmittedPositionRef.current = newPosition;
3512
+ emitAwareness({
3513
+ blockIndex: currentBlockIndex,
3514
+ blockId,
3515
+ selection: selectionInfo,
3516
+ timestamp: now
3517
+ });
3518
+ } catch (err) {
3519
+ }
3520
+ }, [collabEnabled, emitAwareness, collabStatus, getAbsoluteOffset]);
3521
+ useEffect(() => {
3522
+ if (!collabEnabled || !editorRef.current) {
3523
+ return void 0;
3524
+ }
3525
+ const editorElement = editorRef.current;
3526
+ const handleInteraction = () => {
3527
+ emitCursorPosition();
3528
+ };
3529
+ editorElement.addEventListener("click", handleInteraction);
3530
+ editorElement.addEventListener("keyup", handleInteraction);
3531
+ editorElement.addEventListener("keydown", handleInteraction);
3532
+ editorElement.addEventListener("input", handleInteraction);
3533
+ editorElement.addEventListener("mouseup", handleInteraction);
3534
+ editorElement.addEventListener("selectionchange", handleInteraction);
3535
+ let intervalId = null;
3536
+ const handleFocus = () => {
3537
+ intervalId = setInterval(emitCursorPosition, 1e3);
3538
+ };
3539
+ const handleBlur = () => {
3540
+ if (intervalId) {
3541
+ clearInterval(intervalId);
3542
+ intervalId = null;
3543
+ }
3544
+ };
3545
+ editorElement.addEventListener("focusin", handleFocus);
3546
+ editorElement.addEventListener("focusout", handleBlur);
3547
+ return () => {
3548
+ editorElement.removeEventListener("click", handleInteraction);
3549
+ editorElement.removeEventListener("keyup", handleInteraction);
3550
+ editorElement.removeEventListener("keydown", handleInteraction);
3551
+ editorElement.removeEventListener("input", handleInteraction);
3552
+ editorElement.removeEventListener("mouseup", handleInteraction);
3553
+ editorElement.removeEventListener("selectionchange", handleInteraction);
3554
+ editorElement.removeEventListener("focusin", handleFocus);
3555
+ editorElement.removeEventListener("focusout", handleBlur);
3556
+ if (intervalId) {
3557
+ clearInterval(intervalId);
3558
+ }
3559
+ };
3560
+ }, [collabEnabled, emitCursorPosition]);
3561
+ const findTextPosition = useCallback((container, targetOffset) => {
3562
+ if (!container || targetOffset === null || targetOffset === void 0) {
3563
+ return null;
3564
+ }
3565
+ let currentOffset = 0;
3566
+ const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT, null, false);
3567
+ let node = walker.nextNode();
3568
+ while (node) {
3569
+ const nodeLength = node.textContent?.length || 0;
3570
+ if (currentOffset + nodeLength >= targetOffset) {
3571
+ return {
3572
+ node,
3573
+ offset: targetOffset - currentOffset,
3574
+ rect: (() => {
3575
+ try {
3576
+ const range = document.createRange();
3577
+ range.setStart(node, Math.min(targetOffset - currentOffset, nodeLength));
3578
+ range.setEnd(node, Math.min(targetOffset - currentOffset, nodeLength));
3579
+ return range.getBoundingClientRect();
3580
+ } catch {
3581
+ return null;
3582
+ }
3583
+ })()
3584
+ };
3585
+ }
3586
+ currentOffset += nodeLength;
3587
+ node = walker.nextNode();
3588
+ }
3589
+ return null;
3590
+ }, []);
3591
+ useRef(null);
3592
+ useEffect(() => {
3593
+ if (!collabEnabled || !editorRef.current || !isReady) {
3594
+ return void 0;
3595
+ }
3596
+ const editorElement = editorRef.current;
3597
+ const previousHighlights = editorElement.querySelectorAll(".ce-block--remote-editing");
3598
+ previousHighlights.forEach((el) => {
3599
+ el.classList.remove("ce-block--remote-editing");
3600
+ el.removeAttribute("data-remote-user");
3601
+ el.style.removeProperty("--remote-color");
3602
+ });
3603
+ const previousCursors = editorElement.querySelectorAll(".remote-cursor-indicator");
3604
+ previousCursors.forEach((el) => el.remove());
3605
+ activeEditors.forEach((editor) => {
3606
+ if (editor.blockIndex === null || editor.blockIndex === void 0) {
3607
+ return;
3608
+ }
3609
+ const blocks = editorElement.querySelectorAll(".ce-block");
3610
+ const targetBlock = blocks[editor.blockIndex];
3611
+ if (!targetBlock) {
3612
+ return;
3613
+ }
3614
+ targetBlock.classList.add("ce-block--remote-editing");
3615
+ targetBlock.setAttribute("data-remote-user", getPeerName(editor.user));
3616
+ targetBlock.style.setProperty("--remote-color", editor.color?.bg || "#3B82F6");
3617
+ const contentElement = targetBlock.querySelector(
3618
+ '.ce-paragraph, .ce-header, [contenteditable="true"], .cdx-block'
3619
+ );
3620
+ if (!contentElement) {
3621
+ return;
3622
+ }
3623
+ let position = null;
3624
+ if (editor.selection?.startOffset !== void 0 && editor.selection.startOffset >= 0) {
3625
+ position = findTextPosition(contentElement, editor.selection.startOffset);
3626
+ }
3627
+ const editorRect = editorElement.getBoundingClientRect();
3628
+ const contentRect = contentElement.getBoundingClientRect();
3629
+ let cursorLeft, cursorTop;
3630
+ if (position?.rect && position.rect.width !== void 0) {
3631
+ cursorLeft = position.rect.left - editorRect.left;
3632
+ cursorTop = position.rect.top - editorRect.top;
3633
+ } else {
3634
+ cursorLeft = contentRect.left - editorRect.left;
3635
+ cursorTop = contentRect.top - editorRect.top;
3636
+ }
3637
+ if (isNaN(cursorLeft) || isNaN(cursorTop) || cursorLeft < 0) {
3638
+ cursorLeft = contentRect.left - editorRect.left;
3639
+ cursorTop = contentRect.top - editorRect.top;
3640
+ }
3641
+ const cursorIndicator = document.createElement("div");
3642
+ cursorIndicator.className = "remote-cursor-indicator";
3643
+ cursorIndicator.dataset.userId = editor.user.id;
3644
+ cursorIndicator.style.cssText = `
3645
+ position: absolute;
3646
+ left: ${cursorLeft}px;
3647
+ top: ${cursorTop}px;
3648
+ width: 2px;
3649
+ height: 18px;
3650
+ background: ${editor.color?.bg || "#3B82F6"};
3651
+ border-radius: 1px;
3652
+ pointer-events: none;
3653
+ z-index: 1000;
3654
+ transition: left 0.05s linear, top 0.08s ease-out;
3655
+ box-shadow: 0 0 8px ${editor.color?.bg || "#3B82F6"}60;
3656
+ `;
3657
+ const cursorLabel = document.createElement("div");
3658
+ cursorLabel.className = "remote-cursor-label";
3659
+ cursorLabel.style.cssText = `
3660
+ position: absolute;
3661
+ top: -20px;
3662
+ left: 0;
3663
+ background: ${editor.color?.bg || "#3B82F6"};
3664
+ color: ${editor.color?.text || "#FFFFFF"};
3665
+ font-size: 9px;
3666
+ font-weight: 600;
3667
+ padding: 3px 8px;
3668
+ border-radius: 4px 4px 4px 0;
3669
+ white-space: nowrap;
3670
+ box-shadow: 0 2px 6px rgba(0,0,0,0.15);
3671
+ display: flex;
3672
+ align-items: center;
3673
+ gap: 4px;
3674
+ `;
3675
+ const nameSpan = document.createElement("span");
3676
+ nameSpan.textContent = getPeerName(editor.user);
3677
+ cursorLabel.appendChild(nameSpan);
3678
+ const typingDots = document.createElement("span");
3679
+ typingDots.className = "typing-indicator";
3680
+ typingDots.innerHTML = `
3681
+ <span class="dot"></span>
3682
+ <span class="dot"></span>
3683
+ <span class="dot"></span>
3684
+ `;
3685
+ typingDots.style.cssText = `
3686
+ display: inline-flex;
3687
+ align-items: center;
3688
+ gap: 2px;
3689
+ margin-left: 2px;
3690
+ `;
3691
+ cursorLabel.appendChild(typingDots);
3692
+ cursorIndicator.appendChild(cursorLabel);
3693
+ requestAnimationFrame(() => {
3694
+ editorElement.appendChild(cursorIndicator);
3695
+ });
3696
+ });
3697
+ return () => {
3698
+ const cursors = editorElement?.querySelectorAll(".remote-cursor-indicator");
3699
+ cursors?.forEach((el) => el.remove());
3700
+ };
3701
+ }, [collabEnabled, activeEditors, isReady, findTextPosition]);
3702
+ useEffect(() => {
3703
+ const state = {
3704
+ status: collabStatus,
3705
+ peers: collabPeers,
3706
+ error: collabError
3707
+ };
3708
+ window.__MAGIC_EDITOR_COLLAB_STATE__ = state;
3709
+ window.dispatchEvent(new CustomEvent("magic-editor-collab-update", {
3710
+ detail: state
3711
+ }));
3712
+ return () => {
3713
+ window.__MAGIC_EDITOR_COLLAB_STATE__ = { status: "disabled", peers: [], error: null };
3714
+ window.dispatchEvent(new CustomEvent("magic-editor-collab-update", {
3715
+ detail: { status: "disabled", peers: [], error: null }
3716
+ }));
3717
+ };
3718
+ }, [collabStatus, collabPeers, collabError]);
3719
+ const isApplyingRemoteRef = useRef(false);
3720
+ const pendingRenderRef = useRef(null);
3721
+ const lastSerializedValueRef = useRef(serializeForCompare(serializedInitialValue || null));
3722
+ useEffect(() => {
3723
+ lastSerializedValueRef.current = serializeForCompare(serializedInitialValue || null);
3724
+ }, [serializedInitialValue, serializeForCompare]);
3725
+ const calculateStats = useCallback((data) => {
3726
+ if (!data?.blocks) {
3727
+ setWordCount(0);
3728
+ setCharCount(0);
3729
+ return;
3730
+ }
3731
+ let text = "";
3732
+ data.blocks.forEach((block) => {
3733
+ if (block.data?.text) text += block.data.text + " ";
3734
+ if (block.data?.items) {
3735
+ block.data.items.forEach((item) => {
3736
+ if (typeof item === "string") text += item + " ";
3737
+ else if (item.content) text += item.content + " ";
3738
+ });
3739
+ }
3740
+ });
3741
+ const plainText = text.replace(/<[^>]*>/g, "").trim();
3742
+ setCharCount(plainText.length);
3743
+ setWordCount(plainText.split(/\s+/).filter((w) => w.length > 0).length);
3744
+ }, []);
3745
+ 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
+ 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
+ return;
3752
+ }
3753
+ const editor = editorInstanceRef.current;
3754
+ 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
+ pendingRenderRef.current = pendingRenderRef.current || true;
3758
+ return;
3759
+ }
3760
+ 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
+ return;
3764
+ }
3765
+ isApplyingRemoteRef.current = true;
3766
+ try {
3767
+ let yOrder = [];
3768
+ const blockOrderJson = yMetaMap?.get("blockOrder");
3769
+ if (blockOrderJson) {
3770
+ try {
3771
+ yOrder = JSON.parse(blockOrderJson);
3772
+ } catch (e) {
3773
+ console.warn("[Magic Editor X] Invalid blockOrder JSON, falling back to Map keys");
3774
+ yOrder = Array.from(yBlocksMap.keys());
3775
+ }
3776
+ } else {
3777
+ yOrder = Array.from(yBlocksMap.keys());
3778
+ }
3779
+ 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
+ yOrder.forEach((id) => {
3783
+ const json = yBlocksMap.get(id);
3784
+ if (json) {
3785
+ try {
3786
+ const block = JSON.parse(json);
3787
+ yBlocks.push(block);
3788
+ } catch (e) {
3789
+ console.warn("[Magic Editor X] Invalid block JSON:", id);
3790
+ }
3791
+ }
3792
+ });
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
+ const parsed = { blocks: yBlocks };
3796
+ const normalizedParsed = serializeForCompare(parsed);
3797
+ 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
+ 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
+ lastSerializedValueRef.current = normalizedParsed;
3804
+ setBlocksCount(yBlocks.length);
3805
+ calculateStats(parsed);
3806
+ };
3807
+ const blockCount = editor.blocks.getBlocksCount();
3808
+ const currentBlocks = [];
3809
+ for (let i = 0; i < blockCount; i++) {
3810
+ const block = editor.blocks.getBlockByIndex(i);
3811
+ if (block) {
3812
+ currentBlocks.push({ id: block.id, index: i });
3813
+ }
3814
+ }
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
+ if (blockCount !== yBlocks.length) {
3818
+ console.log("[Magic Editor X] [SYNC] Structural change detected (count mismatch). Falling back to full render.");
3819
+ await renderFull();
3820
+ return;
3821
+ }
3822
+ const targetIds = yBlocks.map((b) => b.id);
3823
+ const orderMatches = targetIds.every((id, idx) => currentBlocks[idx]?.id === id);
3824
+ const allIdsPresent = targetIds.every((id) => yBlocksMap.has(id));
3825
+ if (!orderMatches || !allIdsPresent) {
3826
+ console.log("[Magic Editor X] [SYNC] Order mismatch detected. Falling back to full render.");
3827
+ await renderFull();
3828
+ return;
3829
+ }
3830
+ const activeBlockIndex = editor.blocks.getCurrentBlockIndex();
3831
+ for (let i = 0; i < yBlocks.length; i++) {
3832
+ const targetBlock = yBlocks[i];
3833
+ const currentBlock = editor.blocks.getBlockByIndex(i);
3834
+ if (!currentBlock || currentBlock.id !== targetBlock.id) {
3835
+ console.log("[Magic Editor X] [SYNC] Unexpected block mismatch during update, rerendering.");
3836
+ await renderFull();
3837
+ return;
3838
+ }
3839
+ if (activeBlockIndex === i) {
3840
+ continue;
3841
+ }
3842
+ try {
3843
+ editor.blocks.update(targetBlock.id, targetBlock.data);
3844
+ } catch (e) {
3845
+ console.warn("[Magic Editor X] [SYNC] Block update failed, rerendering:", targetBlock.id, e);
3846
+ await renderFull();
3847
+ return;
3848
+ }
3849
+ }
3850
+ lastSerializedValueRef.current = normalizedParsed;
3851
+ setBlocksCount(yBlocks.length);
3852
+ calculateStats(parsed);
3853
+ } catch (error2) {
3854
+ console.error("[Magic Editor X] [SYNC] Error in smart sync:", error2);
3855
+ } finally {
3856
+ isApplyingRemoteRef.current = false;
3857
+ }
3858
+ }, [collabEnabled, yBlocksMap, yDoc, yMetaMap]);
3859
+ useEffect(() => {
3860
+ renderFromYDocRef.current = renderFromYDoc;
3861
+ }, [renderFromYDoc]);
3862
+ const pushLocalToCollab = useCallback((payload) => {
3863
+ console.log("[Magic Editor X] [PUSH] pushLocalToCollab called, enabled:", collabEnabled, "yDoc:", !!yDoc, "yBlocksMap:", !!yBlocksMap);
3864
+ if (!collabEnabled || !yDoc || !yBlocksMap) {
3865
+ console.log("[Magic Editor X] [SKIP] Skipping push - not enabled or missing yDoc/yBlocksMap");
3866
+ return;
3867
+ }
3868
+ try {
3869
+ const data = JSON.parse(payload);
3870
+ const blocks = data?.blocks || [];
3871
+ const time = data?.time || Date.now();
3872
+ console.log("[Magic Editor X] [DOC] Pushing", blocks.length, "blocks to Y.Map");
3873
+ yDoc.transact(() => {
3874
+ const uniqueBlocks = [];
3875
+ const currentBlockIds = /* @__PURE__ */ new Set();
3876
+ for (const block of blocks) {
3877
+ if (!block?.id) {
3878
+ console.warn("[Magic Editor X] [WARNING] Block without ID, skipping");
3879
+ continue;
3880
+ }
3881
+ if (currentBlockIds.has(block.id)) {
3882
+ console.warn("[Magic Editor X] [WARNING] Duplicate block ID detected, keeping first occurrence:", block.id);
3883
+ continue;
3884
+ }
3885
+ currentBlockIds.add(block.id);
3886
+ uniqueBlocks.push(block);
3887
+ }
3888
+ for (const block of uniqueBlocks) {
3889
+ const blockJson = JSON.stringify(block);
3890
+ const existing = yBlocksMap.get(block.id);
3891
+ if (existing !== blockJson) {
3892
+ yBlocksMap.set(block.id, blockJson);
3893
+ }
3894
+ }
3895
+ if (yMetaMap) {
3896
+ yMetaMap.set("time", time);
3897
+ const newOrder = uniqueBlocks.map((b) => b.id);
3898
+ yMetaMap.set("blockOrder", JSON.stringify(newOrder));
3899
+ console.log("[Magic Editor X] [ORDER] Updated block order in Y.Map:", newOrder.length, "blocks");
3900
+ }
3901
+ }, "local");
3902
+ console.log("[Magic Editor X] [SUCCESS] Successfully pushed", blocks.length, "blocks to Y.Map");
3903
+ } catch (error2) {
3904
+ console.error("[Magic Editor X] Failed to push local update", error2);
3905
+ }
3906
+ }, [collabEnabled, yDoc, yBlocksMap, yMetaMap]);
3907
+ const pushLocalToCollabRef = useRef(pushLocalToCollab);
3908
+ useEffect(() => {
3909
+ pushLocalToCollabRef.current = pushLocalToCollab;
3910
+ }, [pushLocalToCollab]);
3911
+ const customPlaceholder = attribute?.options?.placeholder || placeholder || "Start writing your amazing content...";
3912
+ attribute?.options?.minHeight || 400;
3913
+ const mediaLibToggleFunc = useCallback(
3914
+ getToggleFunc({
3915
+ openStateSetter: setIsMediaLibOpen,
3916
+ indexStateSetter: setMediaLibBlockIndex
3917
+ }),
3918
+ []
3919
+ );
3920
+ const handleMediaLibChange = useCallback(
3921
+ (data) => {
3922
+ changeFunc({
3923
+ indexStateSetter: setMediaLibBlockIndex,
3924
+ data,
3925
+ index: mediaLibBlockIndex,
3926
+ editor: editorInstanceRef.current
3927
+ });
3928
+ mediaLibToggleFunc();
3929
+ },
3930
+ [mediaLibBlockIndex, mediaLibToggleFunc]
3931
+ );
3932
+ const toggleFullscreen = useCallback(() => {
3933
+ setIsFullscreen((prev) => {
3934
+ if (!prev) {
3935
+ document.body.classList.add("editor-fullscreen");
3936
+ } else {
3937
+ document.body.classList.remove("editor-fullscreen");
3938
+ }
3939
+ return !prev;
3940
+ });
3941
+ }, []);
3942
+ useEffect(() => {
3943
+ const handleKeyDown = (e) => {
3944
+ if (e.key === "Escape" && isFullscreen) {
3945
+ toggleFullscreen();
3946
+ }
3947
+ if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
3948
+ toggleFullscreen();
3949
+ }
3950
+ };
3951
+ window.addEventListener("keydown", handleKeyDown);
3952
+ return () => window.removeEventListener("keydown", handleKeyDown);
3953
+ }, [isFullscreen, toggleFullscreen]);
3954
+ const handleAIAssistant = useCallback(() => {
3955
+ const selection = window.getSelection();
3956
+ let text = selection?.toString().trim();
3957
+ if (!text && editorInstanceRef.current && isReady) {
3958
+ const currentBlockIndex = editorInstanceRef.current.blocks.getCurrentBlockIndex();
3959
+ const currentBlock = editorInstanceRef.current.blocks.getBlockByIndex(currentBlockIndex);
3960
+ if (currentBlock) {
3961
+ const blockElement = currentBlock.holder?.querySelector("[contenteditable]") || currentBlock.holder?.querySelector(".ce-paragraph") || currentBlock.holder?.querySelector(".ce-header");
3962
+ if (blockElement) {
3963
+ text = blockElement.textContent?.trim() || "";
3964
+ if (text) {
3965
+ const range = document.createRange();
3966
+ range.selectNodeContents(blockElement);
3967
+ selection.removeAllRanges();
3968
+ selection.addRange(range);
3969
+ }
3970
+ }
3971
+ }
3972
+ }
3973
+ if (!text) {
3974
+ toastManager.warning("Bitte Text markieren");
3975
+ return;
3976
+ }
3977
+ if (selection.rangeCount > 0) {
3978
+ aiSelectionRangeRef.current = selection.getRangeAt(0).cloneRange();
3979
+ const rect = selection.getRangeAt(0).getBoundingClientRect();
3980
+ const toolbarWidth = 450;
3981
+ let left = rect.left + rect.width / 2 - toolbarWidth / 2;
3982
+ let top = rect.bottom + 10;
3983
+ if (left < 10) left = 10;
3984
+ if (left + toolbarWidth > window.innerWidth - 10) {
3985
+ left = window.innerWidth - toolbarWidth - 10;
3986
+ }
3987
+ if (top + 60 > window.innerHeight) {
3988
+ top = rect.top - 60;
3989
+ }
3990
+ setAIToolbarPosition({ left, top });
3991
+ }
3992
+ setAISelectedText(text);
3993
+ setShowAIToolbar(true);
3994
+ }, [isReady]);
3995
+ const handleInsertBlock = useCallback((blockType) => {
3996
+ if (!editorInstanceRef.current || !isReady) return;
3997
+ if (blockType === "mediaLib") {
3998
+ if (collabEnabled && collabCanEdit === false) {
3999
+ console.log("[Magic Editor X] Viewer cannot open Media Library");
4000
+ return;
4001
+ }
4002
+ setIsMediaLibOpen(true);
4003
+ return;
4004
+ }
4005
+ if (collabEnabled && collabCanEdit === false) {
4006
+ console.log("[Magic Editor X] Viewer cannot insert blocks");
4007
+ return;
4008
+ }
4009
+ const editor = editorInstanceRef.current;
4010
+ const lastIndex = editor.blocks.getBlocksCount();
4011
+ editor.blocks.insert(blockType, {}, {}, lastIndex, true);
4012
+ editor.caret.setToBlock(lastIndex);
4013
+ }, [isReady, collabEnabled, collabCanEdit]);
4014
+ const handleClear = useCallback(async () => {
4015
+ if (!editorInstanceRef.current || !isReady) return;
4016
+ if (window.confirm("Clear all content? This cannot be undone.")) {
4017
+ await editorInstanceRef.current.clear();
4018
+ const emptyPayload = JSON.stringify({ blocks: [] });
4019
+ lastSerializedValueRef.current = emptyPayload;
4020
+ pushLocalToCollab(emptyPayload);
4021
+ onChange({ target: { name, value: null, type: "text" } });
4022
+ setBlocksCount(0);
4023
+ setWordCount(0);
4024
+ setCharCount(0);
4025
+ }
4026
+ }, [isReady, name, onChange, pushLocalToCollab]);
4027
+ const handleCopy = useCallback(async () => {
4028
+ if (!editorInstanceRef.current || !isReady) return;
4029
+ const data = await editorInstanceRef.current.save();
4030
+ await navigator.clipboard.writeText(JSON.stringify(data, null, 2));
4031
+ }, [isReady]);
4032
+ useEffect(() => {
4033
+ if (editorRef.current && !editorInstanceRef.current) {
4034
+ const tools = getTools({ mediaLibToggleFunc, pluginId: PLUGIN_ID });
4035
+ let initialData = void 0;
4036
+ if (value) {
4037
+ try {
4038
+ initialData = typeof value === "string" ? JSON.parse(value) : value;
4039
+ setBlocksCount(initialData?.blocks?.length || 0);
4040
+ calculateStats(initialData);
4041
+ } catch (e) {
4042
+ console.warn("[Magic Editor X] Could not parse initial value:", e);
4043
+ }
4044
+ }
4045
+ const isReadOnly = disabled || collabEnabled && collabCanEdit === false;
4046
+ console.log("[Magic Editor X] [INIT] Initial readOnly:", isReadOnly, "| collabCanEdit:", collabCanEdit);
4047
+ if (isReadOnly && collabEnabled && !disabled) {
4048
+ console.log("[Magic Editor X] [VIEWER] Viewer mode - editor is read-only");
4049
+ if (editorRef.current) {
4050
+ editorRef.current.classList.add("editor-readonly");
4051
+ }
4052
+ }
4053
+ const editor = new EditorJS({
4054
+ holder: editorRef.current,
4055
+ tools,
4056
+ data: initialData,
4057
+ readOnly: isReadOnly,
4058
+ placeholder: customPlaceholder,
4059
+ minHeight: 200,
4060
+ autofocus: false,
4061
+ onReady: async () => {
4062
+ isReadyRef.current = true;
4063
+ setIsReady(true);
4064
+ console.log("[Magic Editor X] [READY] Editor onReady fired");
4065
+ console.log("[Magic Editor X] [READY] Editor holder:", editorRef.current?.id);
4066
+ try {
4067
+ initUndoRedo(editor);
4068
+ console.log("[Magic Editor X] [SUCCESS] Undo/Redo initialized");
4069
+ } catch (e) {
4070
+ console.warn("[Magic Editor X] Could not initialize Undo/Redo:", e);
4071
+ }
4072
+ try {
4073
+ initDragDrop(editor);
4074
+ console.log("[Magic Editor X] [SUCCESS] Drag & Drop initialized");
4075
+ } catch (e) {
4076
+ console.warn("[Magic Editor X] Could not initialize Drag & Drop:", e);
4077
+ }
4078
+ if (pendingRenderRef.current) {
4079
+ try {
4080
+ if (typeof pendingRenderRef.current === "object" && pendingRenderRef.current.blocks) {
4081
+ console.log("[Magic Editor X] [PENDING] Rendering pending data with", pendingRenderRef.current?.blocks?.length, "blocks");
4082
+ isApplyingRemoteRef.current = true;
4083
+ try {
4084
+ await editor.render(pendingRenderRef.current);
4085
+ lastSerializedValueRef.current = serializeForCompare(pendingRenderRef.current);
4086
+ console.log("[Magic Editor X] [SUCCESS] Rendered pending blocks on ready");
4087
+ } finally {
4088
+ isApplyingRemoteRef.current = false;
4089
+ }
4090
+ } else {
4091
+ console.log("[Magic Editor X] [PENDING] Triggering queued sync after onReady");
4092
+ await renderFromYDocRef.current?.();
4093
+ }
4094
+ } catch (err) {
4095
+ console.error("[Magic Editor X] Error applying pending sync:", err);
4096
+ isApplyingRemoteRef.current = false;
4097
+ }
4098
+ pendingRenderRef.current = null;
4099
+ }
4100
+ },
4101
+ onChange: async (api) => {
4102
+ try {
4103
+ if (isApplyingRemoteRef.current) {
4104
+ return;
4105
+ }
4106
+ const outputData = await api.saver.save();
4107
+ const count = outputData.blocks?.length || 0;
4108
+ const serialized = JSON.stringify(outputData);
4109
+ const normalized = serializeForCompare(outputData);
4110
+ if (normalized === lastSerializedValueRef.current) {
4111
+ return;
4112
+ }
4113
+ setBlocksCount(count);
4114
+ calculateStats(outputData);
4115
+ const docPayload = count === 0 ? JSON.stringify({ blocks: [] }) : serialized;
4116
+ pushLocalToCollabRef.current?.(docPayload);
4117
+ lastSerializedValueRef.current = normalized;
4118
+ if (count === 0) {
4119
+ onChange({ target: { name, value: null, type: "text" } });
4120
+ } else {
4121
+ onChange({ target: { name, value: serialized, type: "text" } });
4122
+ }
4123
+ } catch (error2) {
4124
+ console.error("[Magic Editor X] Error in onChange:", error2);
4125
+ }
4126
+ }
4127
+ });
4128
+ editorInstanceRef.current = editor;
4129
+ }
4130
+ return () => {
4131
+ console.log("[Magic Editor X] [CLEANUP] Editor component unmounting, destroying editor");
4132
+ isReadyRef.current = false;
4133
+ setIsReady(false);
4134
+ if (editorInstanceRef.current && editorInstanceRef.current.destroy) {
4135
+ try {
4136
+ editorInstanceRef.current.destroy();
4137
+ } catch (e) {
4138
+ console.warn("[Magic Editor X] Error destroying editor:", e);
4139
+ }
4140
+ editorInstanceRef.current = null;
4141
+ }
4142
+ document.body.classList.remove("editor-fullscreen");
4143
+ };
4144
+ }, []);
4145
+ useEffect(() => {
4146
+ const editor = editorInstanceRef.current;
4147
+ if (!editor || !isReady) return;
4148
+ const shouldBeReadOnly = disabled || collabEnabled && collabCanEdit === false;
4149
+ console.log("[Magic Editor X] [READONLY] ReadOnly check:", {
4150
+ disabled,
4151
+ collabEnabled,
4152
+ collabCanEdit,
4153
+ collabRole,
4154
+ shouldBeReadOnly
4155
+ });
4156
+ if (typeof editor.readOnly?.toggle === "function") {
4157
+ const currentReadOnly = editor.readOnly?.isEnabled;
4158
+ if (currentReadOnly !== shouldBeReadOnly) {
4159
+ console.log("[Magic Editor X] [TOGGLE] Toggling readOnly:", shouldBeReadOnly, "| Role:", collabRole);
4160
+ editor.readOnly.toggle(shouldBeReadOnly);
4161
+ if (editorRef.current) {
4162
+ if (shouldBeReadOnly) {
4163
+ editorRef.current.classList.add("editor-readonly");
4164
+ } else {
4165
+ editorRef.current.classList.remove("editor-readonly");
4166
+ }
4167
+ }
4168
+ }
4169
+ }
4170
+ }, [disabled, collabEnabled, collabCanEdit, collabRole, isReady]);
4171
+ const quickActions = [
4172
+ { icon: Bars3BottomLeftIcon, label: "Heading", block: "header" },
4173
+ { icon: ListBulletIcon, label: "List", block: "list" },
4174
+ { icon: CheckCircleIcon, label: "Checklist", block: "checklist" },
4175
+ { icon: PhotoIcon, label: "Image", block: "image" },
4176
+ { icon: LinkIcon, label: "Link", block: "linkTool" },
4177
+ { icon: CodeBracketIcon, label: "Code", block: "code" },
4178
+ { icon: TableCellsIcon, label: "Table", block: "table" },
4179
+ { icon: ChatBubbleBottomCenterTextIcon, label: "Quote", block: "quote" },
4180
+ { icon: ExclamationTriangleIcon, label: "Warning", block: "warning" },
4181
+ { icon: MinusIcon, label: "Divider", block: "delimiter" }
4182
+ ];
4183
+ return /* @__PURE__ */ jsxs(Field.Root, { name, id: name, error, required, hint, children: [
4184
+ /* @__PURE__ */ jsx(FullscreenGlobalStyle, {}),
4185
+ /* @__PURE__ */ jsx(EditorJSGlobalStyles, {}),
4186
+ /* @__PURE__ */ jsx(AIToast, {}),
4187
+ label && !isFullscreen && /* @__PURE__ */ jsx(Field.Label, { action: labelAction, children: label }),
4188
+ /* @__PURE__ */ jsx(
4189
+ EditorContainer,
4190
+ {
4191
+ ref: containerRef,
4192
+ $isFullscreen: isFullscreen,
4193
+ $minHeight: editorHeight,
4194
+ children: /* @__PURE__ */ jsxs(
4195
+ EditorCard,
4196
+ {
4197
+ $isFocused: isFocused,
4198
+ $hasError: !!error,
4199
+ $disabled: disabled,
4200
+ $isFullscreen: isFullscreen,
4201
+ $minHeight: editorHeight,
4202
+ children: [
4203
+ /* @__PURE__ */ jsxs(EditorHeader, { children: [
4204
+ /* @__PURE__ */ jsxs(HeaderLeft, { children: [
4205
+ /* @__PURE__ */ jsxs(Logo, { children: [
4206
+ /* @__PURE__ */ jsx(SparklesIcon$1, {}),
4207
+ /* @__PURE__ */ jsx(LogoText, { children: "Magic Editor" })
4208
+ ] }),
4209
+ isReady && blocksCount > 0 && /* @__PURE__ */ jsxs(BlockCount, { children: [
4210
+ blocksCount,
4211
+ " ",
4212
+ blocksCount === 1 ? "Block" : "Blocks"
4213
+ ] })
4214
+ ] }),
4215
+ /* @__PURE__ */ jsxs(Toolbar, { children: [
4216
+ /* @__PURE__ */ jsx(QuickActions, { children: quickActions.map(({ icon: Icon2, label: label2, block }) => /* @__PURE__ */ jsx(
4217
+ ToolButton,
4218
+ {
4219
+ type: "button",
4220
+ "data-tooltip": label2,
4221
+ onClick: () => handleInsertBlock(block),
4222
+ disabled: collabEnabled && collabCanEdit === false,
4223
+ style: collabEnabled && collabCanEdit === false ? { opacity: 0.4, cursor: "not-allowed" } : {},
4224
+ children: /* @__PURE__ */ jsx(Icon2, {})
4225
+ },
4226
+ block
4227
+ )) }),
4228
+ /* @__PURE__ */ jsx(ToolbarDivider, {}),
4229
+ /* @__PURE__ */ jsx(
4230
+ ToolButton,
4231
+ {
4232
+ type: "button",
4233
+ "data-tooltip": "KI-Assistent (Text markieren)",
4234
+ onClick: handleAIAssistant,
4235
+ disabled: collabEnabled && collabCanEdit === false,
4236
+ style: {
4237
+ background: "linear-gradient(135deg, #7C3AED 0%, #6d28d9 100%)",
4238
+ color: "white",
4239
+ ...collabEnabled && collabCanEdit === false ? { opacity: 0.4, cursor: "not-allowed" } : {}
4240
+ },
4241
+ children: /* @__PURE__ */ jsx(SparklesIcon$1, {})
4242
+ }
4243
+ ),
4244
+ /* @__PURE__ */ jsx(ToolbarDivider, {}),
4245
+ /* @__PURE__ */ jsx(
4246
+ ToolButton,
4247
+ {
4248
+ type: "button",
4249
+ "data-tooltip": "Copy JSON",
4250
+ onClick: handleCopy,
4251
+ children: /* @__PURE__ */ jsx(DocumentDuplicateIcon, {})
4252
+ }
4253
+ ),
4254
+ /* @__PURE__ */ jsx(
4255
+ ToolButton,
4256
+ {
4257
+ type: "button",
4258
+ "data-tooltip": "Clear All",
4259
+ onClick: handleClear,
4260
+ disabled: collabEnabled && collabCanEdit === false,
4261
+ style: collabEnabled && collabCanEdit === false ? { opacity: 0.4, cursor: "not-allowed" } : {},
4262
+ children: /* @__PURE__ */ jsx(TrashIcon, {})
4263
+ }
4264
+ ),
4265
+ /* @__PURE__ */ jsx(ToolbarDivider, {}),
4266
+ /* @__PURE__ */ jsx(
4267
+ ToolButton,
4268
+ {
4269
+ type: "button",
4270
+ $active: isFullscreen,
4271
+ "data-tooltip": isFullscreen ? "Exit Fullscreen (Esc)" : "Fullscreen (⌘+Enter)",
4272
+ onClick: toggleFullscreen,
4273
+ children: isFullscreen ? /* @__PURE__ */ jsx(ArrowsPointingInIcon, {}) : /* @__PURE__ */ jsx(ArrowsPointingOutIcon, {})
4274
+ }
4275
+ )
4276
+ ] })
4277
+ ] }),
4278
+ collabError && collabEnabled && /* @__PURE__ */ jsx(CollabNotice, { children: collabError }),
4279
+ collabEnabled && collabStatus === "connected" && collabRole === "viewer" && /* @__PURE__ */ jsxs(ViewerBanner, { children: [
4280
+ /* @__PURE__ */ jsx(EyeIcon, {}),
4281
+ /* @__PURE__ */ jsx("strong", { children: "Nur-Lesen Modus" }),
4282
+ /* @__PURE__ */ jsx("span", { children: "• Du kannst den Inhalt sehen, aber nicht bearbeiten" })
4283
+ ] }),
4284
+ collabEnabled && collabStatus === "connected" && activeEditors.length > 0 && /* @__PURE__ */ jsxs(ActiveEditorsBar, { children: [
4285
+ /* @__PURE__ */ jsx(ActiveEditorsLabel, { children: "Aktive Bearbeiter:" }),
4286
+ activeEditors.map((editor) => /* @__PURE__ */ jsxs(
4287
+ ActiveEditorBadge,
4288
+ {
4289
+ $color: editor.color?.bg,
4290
+ title: `${getPeerName(editor.user)} bearbeitet Block ${editor.blockIndex !== null ? editor.blockIndex + 1 : "?"}`,
4291
+ children: [
4292
+ /* @__PURE__ */ jsx(ActiveEditorDot, { $color: editor.color?.bg }),
4293
+ /* @__PURE__ */ jsx("span", { children: getPeerName(editor.user) }),
4294
+ editor.blockIndex !== null && /* @__PURE__ */ jsxs("span", { style: { opacity: 0.6, fontSize: "10px" }, children: [
4295
+ "Block ",
4296
+ editor.blockIndex + 1
4297
+ ] })
4298
+ ]
4299
+ },
4300
+ editor.user.id
4301
+ ))
4302
+ ] }),
4303
+ /* @__PURE__ */ jsxs(
4304
+ EditorContent,
4305
+ {
4306
+ $isFullscreen: isFullscreen,
4307
+ onFocus: () => setIsFocused(true),
4308
+ onBlur: () => setIsFocused(false),
4309
+ children: [
4310
+ !isReady && /* @__PURE__ */ jsxs(LoadingOverlay, { children: [
4311
+ /* @__PURE__ */ jsx(Loader, { small: true }),
4312
+ /* @__PURE__ */ jsx(LoadingText, { children: "Initializing editor..." })
4313
+ ] }),
4314
+ /* @__PURE__ */ jsx(
4315
+ EditorWrapper,
4316
+ {
4317
+ ref: editorRef,
4318
+ id: `editor-${name}`,
4319
+ $minHeight: editorHeight
4320
+ }
4321
+ ),
4322
+ isReady && blocksCount === 0 && !collabEnabled && /* @__PURE__ */ jsxs(EmptyState, { children: [
4323
+ /* @__PURE__ */ jsx(EmptyIcon, { children: /* @__PURE__ */ jsx(PencilSquareIcon, {}) }),
4324
+ /* @__PURE__ */ jsx(EmptyTitle, { children: "Start Writing" }),
4325
+ /* @__PURE__ */ jsx(EmptySubtitle, { children: "Click anywhere or use the toolbar to add content" }),
4326
+ /* @__PURE__ */ jsxs(KeyboardHints, { children: [
4327
+ /* @__PURE__ */ jsxs(KeyHint, { children: [
4328
+ /* @__PURE__ */ jsx("kbd", { children: "/" }),
4329
+ " Commands"
4330
+ ] }),
4331
+ /* @__PURE__ */ jsxs(KeyHint, { children: [
4332
+ /* @__PURE__ */ jsx("kbd", { children: "⌘B" }),
4333
+ " Bold"
4334
+ ] }),
4335
+ /* @__PURE__ */ jsxs(KeyHint, { children: [
4336
+ /* @__PURE__ */ jsx("kbd", { children: "⌘I" }),
4337
+ " Italic"
4338
+ ] }),
4339
+ /* @__PURE__ */ jsxs(KeyHint, { children: [
4340
+ /* @__PURE__ */ jsx("kbd", { children: "Tab" }),
4341
+ " Add Block"
4342
+ ] })
4343
+ ] })
4344
+ ] })
4345
+ ]
4346
+ }
4347
+ ),
4348
+ /* @__PURE__ */ jsxs(EditorFooter, { children: [
4349
+ /* @__PURE__ */ jsxs(FooterLeft, { children: [
4350
+ /* @__PURE__ */ jsxs(FooterStat, { children: [
4351
+ /* @__PURE__ */ jsx("strong", { children: wordCount }),
4352
+ " ",
4353
+ t("editor.words", "Wörter")
4354
+ ] }),
4355
+ /* @__PURE__ */ jsxs(FooterStat, { children: [
4356
+ /* @__PURE__ */ jsx("strong", { children: charCount }),
4357
+ " ",
4358
+ t("editor.characters", "Zeichen")
4359
+ ] })
4360
+ ] }),
4361
+ /* @__PURE__ */ jsxs(FooterRight, { children: [
4362
+ !(collabEnabled && collabCanEdit === false) && /* @__PURE__ */ jsxs(FooterButton, { type: "button", onClick: () => handleInsertBlock("mediaLib"), children: [
4363
+ /* @__PURE__ */ jsx(PhotoIcon, {}),
4364
+ t("editor.mediaLibrary", "Media Library")
4365
+ ] }),
4366
+ isFullscreen && /* @__PURE__ */ jsxs(FooterButton, { type: "button", $primary: true, onClick: toggleFullscreen, children: [
4367
+ /* @__PURE__ */ jsx(ArrowsPointingInIcon, {}),
4368
+ "Exit Fullscreen"
4369
+ ] })
4370
+ ] })
4371
+ ] }),
4372
+ !isFullscreen && /* @__PURE__ */ jsx(ResizeHandle, {})
4373
+ ]
4374
+ }
4375
+ )
4376
+ }
4377
+ ),
4378
+ !isFullscreen && /* @__PURE__ */ jsxs(Fragment, { children: [
4379
+ /* @__PURE__ */ jsx(Field.Hint, {}),
4380
+ /* @__PURE__ */ jsx(Field.Error, {})
4381
+ ] }),
4382
+ /* @__PURE__ */ jsx(
4383
+ MediaLibComponent,
4384
+ {
4385
+ isOpen: isMediaLibOpen,
4386
+ onChange: handleMediaLibChange,
4387
+ onToggle: mediaLibToggleFunc
4388
+ }
4389
+ ),
4390
+ showAIToolbar && /* @__PURE__ */ jsx(
4391
+ AIInlineToolbar,
4392
+ {
4393
+ position: aiToolbarPosition,
4394
+ onAction: async (action, options) => {
4395
+ setAILoading(true);
4396
+ const success = await handleAIAction(action, options, {
4397
+ text: aiSelectedText,
4398
+ range: aiSelectionRangeRef.current
4399
+ });
4400
+ setAILoading(false);
4401
+ if (success) {
4402
+ setShowAIToolbar(false);
4403
+ aiSelectionRangeRef.current = null;
4404
+ }
4405
+ },
4406
+ loading: aiLoading,
4407
+ onClose: () => {
4408
+ setShowAIToolbar(false);
4409
+ aiSelectionRangeRef.current = null;
4410
+ }
4411
+ }
4412
+ ),
4413
+ showAIPopup && /* @__PURE__ */ jsx(
4414
+ AIAssistantPopup,
4415
+ {
4416
+ selectedText: aiSelectedText,
4417
+ licenseKey: licenseData?.licenseKey,
4418
+ onClose: () => {
4419
+ setShowAIPopup(false);
4420
+ aiSelectionRangeRef.current = null;
4421
+ },
4422
+ onApply: (correctedText) => {
4423
+ if (aiSelectionRangeRef.current) {
4424
+ const selection = window.getSelection();
4425
+ selection.removeAllRanges();
4426
+ selection.addRange(aiSelectionRangeRef.current);
4427
+ document.execCommand("insertText", false, correctedText);
4428
+ aiSelectionRangeRef.current = null;
4429
+ }
4430
+ setShowAIPopup(false);
4431
+ }
4432
+ }
4433
+ ),
4434
+ /* @__PURE__ */ jsx(
4435
+ CreditsModal,
4436
+ {
4437
+ isOpen: showCreditsModal,
4438
+ onClose: () => {
4439
+ setShowCreditsModal(false);
4440
+ setCreditsUpgradeInfo(null);
4441
+ },
4442
+ upgradeInfo: creditsUpgradeInfo
4443
+ }
4444
+ )
4445
+ ] });
4446
+ });
4447
+ Editor.displayName = "MagicEditorXInput";
4448
+ export {
4449
+ Editor as default
4450
+ };