@editability/editor-lexical 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lexical/editor.d.ts +22 -0
- package/dist/lexical/editor.d.ts.map +1 -1
- package/dist/lexical/editor.js +185 -5
- package/dist/lexical/editor.js.map +1 -1
- package/dist/overlay.d.ts.map +1 -1
- package/dist/overlay.js +546 -69
- package/dist/overlay.js.map +1 -1
- package/package.json +4 -2
package/dist/overlay.js
CHANGED
|
@@ -1,18 +1,31 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
3
3
|
import { diffWords, fnv1a64, normalizeTextContent, sanitizeHref } from "@editability/shared";
|
|
4
|
-
import { createActiveEditor } from "./lexical/editor.js";
|
|
4
|
+
import { createActiveEditor, } from "./lexical/editor.js";
|
|
5
5
|
const DRAFT_KEY = "editability:draft:v1";
|
|
6
|
+
const EMPTY_FORMAT_STATE = {
|
|
7
|
+
bold: false,
|
|
8
|
+
italic: false,
|
|
9
|
+
link: false,
|
|
10
|
+
blockType: "paragraph",
|
|
11
|
+
listType: null,
|
|
12
|
+
};
|
|
6
13
|
export function OverlayApp({ apiBase, googleClientId }) {
|
|
7
14
|
const [user, setUser] = useState(null);
|
|
8
15
|
const [loading, setLoading] = useState(true);
|
|
9
16
|
const [draft, setDraft] = useState(() => loadDraft());
|
|
10
17
|
const [activeId, setActiveId] = useState(null);
|
|
11
18
|
const [activeEditor, setActiveEditor] = useState(null);
|
|
19
|
+
const [formatState, setFormatState] = useState(EMPTY_FORMAT_STATE);
|
|
20
|
+
const [historyState, setHistoryState] = useState({ canUndo: false, canRedo: false });
|
|
12
21
|
const [publishOpen, setPublishOpen] = useState(false);
|
|
13
22
|
const [message, setMessage] = useState("");
|
|
14
23
|
const [error, setError] = useState(null);
|
|
15
24
|
const [publishing, setPublishing] = useState(false);
|
|
25
|
+
const [linkOpen, setLinkOpen] = useState(false);
|
|
26
|
+
const [linkUrl, setLinkUrl] = useState("");
|
|
27
|
+
const [linkError, setLinkError] = useState(null);
|
|
28
|
+
const [blockMenuOpen, setBlockMenuOpen] = useState(false);
|
|
16
29
|
const observerRef = useRef(null);
|
|
17
30
|
const applyingRef = useRef(false);
|
|
18
31
|
const applyScheduledRef = useRef(null);
|
|
@@ -22,6 +35,9 @@ export function OverlayApp({ apiBase, googleClientId }) {
|
|
|
22
35
|
const activeSnapshotRef = useRef(null);
|
|
23
36
|
const activeChangeRef = useRef(null);
|
|
24
37
|
const draftRef = useRef(draft);
|
|
38
|
+
const linkInputRef = useRef(null);
|
|
39
|
+
const linkPanelRef = useRef(null);
|
|
40
|
+
const blockMenuRef = useRef(null);
|
|
25
41
|
const changes = useMemo(() => Object.values(draft.changes), [draft]);
|
|
26
42
|
useEffect(() => {
|
|
27
43
|
ensureStyles();
|
|
@@ -42,6 +58,46 @@ export function OverlayApp({ apiBase, googleClientId }) {
|
|
|
42
58
|
useEffect(() => {
|
|
43
59
|
activeIdRef.current = activeId;
|
|
44
60
|
}, [activeId]);
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
if (!activeEditor) {
|
|
63
|
+
setFormatState(EMPTY_FORMAT_STATE);
|
|
64
|
+
setHistoryState({ canUndo: false, canRedo: false });
|
|
65
|
+
}
|
|
66
|
+
}, [activeEditor]);
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (!linkOpen) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const id = window.requestAnimationFrame(() => linkInputRef.current?.focus());
|
|
72
|
+
return () => window.cancelAnimationFrame(id);
|
|
73
|
+
}, [linkOpen]);
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
if (!linkOpen && !blockMenuOpen) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const handler = (event) => {
|
|
79
|
+
const target = event.target;
|
|
80
|
+
if (!target) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (linkOpen && linkPanelRef.current?.contains(target)) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (blockMenuOpen && blockMenuRef.current?.contains(target)) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
setLinkOpen(false);
|
|
90
|
+
setBlockMenuOpen(false);
|
|
91
|
+
};
|
|
92
|
+
document.addEventListener("mousedown", handler, true);
|
|
93
|
+
return () => document.removeEventListener("mousedown", handler, true);
|
|
94
|
+
}, [linkOpen, blockMenuOpen]);
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
setLinkOpen(false);
|
|
97
|
+
setBlockMenuOpen(false);
|
|
98
|
+
setLinkUrl("");
|
|
99
|
+
setLinkError(null);
|
|
100
|
+
}, [activeId]);
|
|
45
101
|
useEffect(() => {
|
|
46
102
|
document.body.classList.toggle("editability-authenticated", Boolean(user));
|
|
47
103
|
return () => {
|
|
@@ -90,6 +146,11 @@ export function OverlayApp({ apiBase, googleClientId }) {
|
|
|
90
146
|
if (!id) {
|
|
91
147
|
return;
|
|
92
148
|
}
|
|
149
|
+
const nestedInteractive = findNestedInteractive(block);
|
|
150
|
+
if (nestedInteractive) {
|
|
151
|
+
console.warn("Editability: blocked editing for element containing interactive children. Wrap only text nodes or add nested <Editability> for interactive labels.");
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
93
154
|
if (activeId === id) {
|
|
94
155
|
return;
|
|
95
156
|
}
|
|
@@ -98,10 +159,18 @@ export function OverlayApp({ apiBase, googleClientId }) {
|
|
|
98
159
|
}
|
|
99
160
|
const existing = draftRef.current.changes[id];
|
|
100
161
|
activeSnapshotRef.current = { id, html: existing ? existing.originalHtml : block.innerHTML };
|
|
101
|
-
const { editor, change } = activateBlock(block, id, draftRef.current, setDraft, setActiveId, setActiveEditor);
|
|
162
|
+
const { editor, change } = activateBlock(block, id, draftRef.current, setDraft, setActiveId, setActiveEditor, setFormatState, setHistoryState);
|
|
102
163
|
activeEditorRef.current = editor;
|
|
103
164
|
activeElementRef.current = block;
|
|
104
165
|
activeChangeRef.current = change;
|
|
166
|
+
const clickPoint = { x: event.clientX, y: event.clientY };
|
|
167
|
+
window.requestAnimationFrame(() => {
|
|
168
|
+
if (activeElementRef.current !== block) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
placeCaretAtPoint(block, clickPoint);
|
|
172
|
+
editor.editor.focus();
|
|
173
|
+
});
|
|
105
174
|
};
|
|
106
175
|
const linkGuard = (event) => {
|
|
107
176
|
const target = event.target;
|
|
@@ -137,6 +206,66 @@ export function OverlayApp({ apiBase, googleClientId }) {
|
|
|
137
206
|
destroyActiveEditor("done", true, draftRef, activeEditorRef, activeElementRef, activeIdRef, activeSnapshotRef, activeChangeRef, setDraft, setActiveEditor, setActiveId, observerRef, applyingRef);
|
|
138
207
|
}
|
|
139
208
|
};
|
|
209
|
+
const isBlockMode = activeEditor?.mode === "block";
|
|
210
|
+
const blockLabel = formatState.blockType === "paragraph"
|
|
211
|
+
? "Paragraph"
|
|
212
|
+
: formatState.blockType === "blockquote"
|
|
213
|
+
? "Quote"
|
|
214
|
+
: `Heading ${formatState.blockType.replace("h", "")}`;
|
|
215
|
+
const handleBlockSelect = (type) => {
|
|
216
|
+
if (!activeEditor || !isBlockMode) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
activeEditor.setBlockType(type);
|
|
220
|
+
setBlockMenuOpen(false);
|
|
221
|
+
};
|
|
222
|
+
const handleListToggle = (type) => {
|
|
223
|
+
if (!activeEditor || !isBlockMode) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
activeEditor.toggleList(type);
|
|
227
|
+
};
|
|
228
|
+
const handleLinkOpen = () => {
|
|
229
|
+
if (!activeEditor) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
activeEditor.captureSelection();
|
|
233
|
+
const existingUrl = activeEditor.getSelectedLinkUrl();
|
|
234
|
+
setLinkUrl(existingUrl ?? "");
|
|
235
|
+
setLinkError(null);
|
|
236
|
+
setLinkOpen(true);
|
|
237
|
+
};
|
|
238
|
+
const handleLinkApply = () => {
|
|
239
|
+
if (!activeEditor) {
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
const trimmed = linkUrl.trim();
|
|
243
|
+
if (!trimmed) {
|
|
244
|
+
activeEditor.setLink(null);
|
|
245
|
+
setLinkOpen(false);
|
|
246
|
+
setLinkUrl("");
|
|
247
|
+
setLinkError(null);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
const sanitized = sanitizeHref(trimmed);
|
|
251
|
+
if (!sanitized) {
|
|
252
|
+
setLinkError("Please enter a valid URL.");
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
activeEditor.setLink(sanitized.href);
|
|
256
|
+
setLinkOpen(false);
|
|
257
|
+
setLinkUrl("");
|
|
258
|
+
setLinkError(null);
|
|
259
|
+
};
|
|
260
|
+
const handleLinkRemove = () => {
|
|
261
|
+
if (!activeEditor) {
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
activeEditor.setLink(null);
|
|
265
|
+
setLinkOpen(false);
|
|
266
|
+
setLinkUrl("");
|
|
267
|
+
setLinkError(null);
|
|
268
|
+
};
|
|
140
269
|
const handlePublish = async () => {
|
|
141
270
|
if (publishing) {
|
|
142
271
|
return;
|
|
@@ -221,25 +350,30 @@ export function OverlayApp({ apiBase, googleClientId }) {
|
|
|
221
350
|
if (!user) {
|
|
222
351
|
return (_jsx("div", { className: "editability-overlay", children: _jsxs("div", { className: "editability-login", children: [_jsx("h2", { children: "Sign in to edit" }), _jsx("p", { children: "Only allowlisted editors can enter edit mode." }), _jsx(LoginPanel, { apiBase: apiBase, googleClientId: googleClientId, onLogin: setUser, loading: false }), _jsx("button", { className: "editability-btn", onClick: handleExit, children: "Exit" })] }) }));
|
|
223
352
|
}
|
|
224
|
-
return (_jsxs("div", { className: "editability-overlay", children: [_jsxs("div", { className: "editability-bar", children: [_jsxs("div", { className: "editability-left", children: [_jsx("span", { className: "editability-pill", children: "Editability" }), activeId ? _jsx("span", { className: "editability-status", children: "Editing" }) : _jsx("span", { className: "editability-status", children: "Select text" })] }), _jsxs("div", { className: "editability-center", children: [_jsx("button", { className: "editability-btn", onClick: () => activeEditor?.undo(), disabled: !activeEditor,
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
353
|
+
return (_jsxs("div", { className: "editability-overlay", children: [_jsxs("div", { className: "editability-bar", children: [_jsxs("div", { className: "editability-left", children: [_jsxs("div", { className: "editability-brand", children: [_jsx("span", { className: "editability-pill", children: "Editability" }), activeId ? _jsx("span", { className: "editability-status", children: "Editing" }) : _jsx("span", { className: "editability-status", children: "Select text" })] }), _jsxs("span", { className: `editability-badge ${changeCount > 0 ? "editability-badge-active" : ""}`, children: [changeCount, " ", changeCount === 1 ? "change" : "changes"] })] }), _jsxs("div", { className: "editability-center", children: [_jsxs("div", { className: "editability-group", children: [_jsx("button", { className: "editability-btn editability-icon-btn", onClick: () => activeEditor?.undo(), disabled: !activeEditor || !historyState.canUndo, "aria-label": "Undo", children: _jsx(IconUndo, {}) }), _jsx("button", { className: "editability-btn editability-icon-btn", onClick: () => activeEditor?.redo(), disabled: !activeEditor || !historyState.canRedo, "aria-label": "Redo", children: _jsx(IconRedo, {}) })] }), _jsx("div", { className: "editability-divider" }), _jsx("div", { className: "editability-group", children: _jsxs("div", { className: "editability-popover-anchor", children: [_jsxs("button", { className: "editability-btn editability-block-btn", onClick: () => setBlockMenuOpen((open) => !open), disabled: !activeEditor || !isBlockMode, "aria-haspopup": "menu", "aria-expanded": blockMenuOpen, children: [_jsx("span", { children: blockLabel }), _jsx(IconChevron, {})] }), blockMenuOpen ? (_jsxs("div", { className: "editability-menu", ref: blockMenuRef, role: "menu", children: [_jsxs("button", { className: `editability-menu-item ${formatState.blockType === "paragraph" ? "active" : ""}`, onClick: () => handleBlockSelect("paragraph"), role: "menuitem", children: [_jsx(IconParagraph, {}), _jsx("span", { children: "Paragraph" })] }), _jsxs("button", { className: `editability-menu-item ${formatState.blockType === "h1" ? "active" : ""}`, onClick: () => handleBlockSelect("h1"), role: "menuitem", children: [_jsx(IconHeading, { label: "H1" }), _jsx("span", { children: "Heading 1" })] }), _jsxs("button", { className: `editability-menu-item ${formatState.blockType === "h2" ? "active" : ""}`, onClick: () => handleBlockSelect("h2"), role: "menuitem", children: [_jsx(IconHeading, { label: "H2" }), _jsx("span", { children: "Heading 2" })] }), _jsxs("button", { className: `editability-menu-item ${formatState.blockType === "h3" ? "active" : ""}`, onClick: () => handleBlockSelect("h3"), role: "menuitem", children: [_jsx(IconHeading, { label: "H3" }), _jsx("span", { children: "Heading 3" })] }), _jsxs("button", { className: `editability-menu-item ${formatState.blockType === "h4" ? "active" : ""}`, onClick: () => handleBlockSelect("h4"), role: "menuitem", children: [_jsx(IconHeading, { label: "H4" }), _jsx("span", { children: "Heading 4" })] }), _jsxs("button", { className: `editability-menu-item ${formatState.blockType === "h5" ? "active" : ""}`, onClick: () => handleBlockSelect("h5"), role: "menuitem", children: [_jsx(IconHeading, { label: "H5" }), _jsx("span", { children: "Heading 5" })] }), _jsxs("button", { className: `editability-menu-item ${formatState.blockType === "h6" ? "active" : ""}`, onClick: () => handleBlockSelect("h6"), role: "menuitem", children: [_jsx(IconHeading, { label: "H6" }), _jsx("span", { children: "Heading 6" })] }), _jsxs("button", { className: `editability-menu-item ${formatState.blockType === "blockquote" ? "active" : ""}`, onClick: () => handleBlockSelect("blockquote"), role: "menuitem", children: [_jsx(IconQuote, {}), _jsx("span", { children: "Quote" })] })] })) : null] }) }), _jsxs("div", { className: "editability-group", children: [_jsx("button", { className: `editability-btn editability-icon-btn ${formatState.listType === "bullet" ? "active" : ""}`, onClick: () => handleListToggle("bullet"), disabled: !activeEditor || !isBlockMode, "aria-pressed": formatState.listType === "bullet", "aria-label": "Bulleted list", children: _jsx(IconListBullet, {}) }), _jsx("button", { className: `editability-btn editability-icon-btn ${formatState.listType === "number" ? "active" : ""}`, onClick: () => handleListToggle("number"), disabled: !activeEditor || !isBlockMode, "aria-pressed": formatState.listType === "number", "aria-label": "Numbered list", children: _jsx(IconListNumber, {}) })] }), _jsxs("div", { className: "editability-group", children: [_jsx("button", { className: `editability-btn editability-icon-btn ${formatState.bold ? "active" : ""}`, onClick: () => activeEditor?.setBold(), disabled: !activeEditor, "aria-pressed": formatState.bold, "aria-label": "Bold", children: _jsx(IconBold, {}) }), _jsx("button", { className: `editability-btn editability-icon-btn ${formatState.italic ? "active" : ""}`, onClick: () => activeEditor?.setItalic(), disabled: !activeEditor, "aria-pressed": formatState.italic, "aria-label": "Italic", children: _jsx(IconItalic, {}) }), _jsxs("div", { className: "editability-popover-anchor", children: [_jsx("button", { className: `editability-btn editability-icon-btn ${formatState.link ? "active" : ""}`, onMouseDown: (event) => {
|
|
354
|
+
if (!activeEditor) {
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
event.preventDefault();
|
|
358
|
+
activeEditor.captureSelection();
|
|
359
|
+
}, onClick: () => {
|
|
360
|
+
if (!activeEditor)
|
|
361
|
+
return;
|
|
362
|
+
if (linkOpen) {
|
|
363
|
+
setLinkOpen(false);
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
handleLinkOpen();
|
|
367
|
+
}, disabled: !activeEditor, "aria-pressed": formatState.link, "aria-label": "Link", children: _jsx(IconLink, {}) }), linkOpen ? (_jsxs("div", { className: "editability-popover", ref: linkPanelRef, children: [_jsx("div", { className: "editability-popover-title", children: "Add link" }), _jsx("input", { ref: linkInputRef, className: "editability-input", placeholder: "https:// or /path", value: linkUrl, onChange: (event) => setLinkUrl(event.target.value), onKeyDown: (event) => {
|
|
368
|
+
if (event.key === "Enter") {
|
|
369
|
+
event.preventDefault();
|
|
370
|
+
handleLinkApply();
|
|
371
|
+
}
|
|
372
|
+
if (event.key === "Escape") {
|
|
373
|
+
setLinkOpen(false);
|
|
374
|
+
setLinkError(null);
|
|
375
|
+
}
|
|
376
|
+
} }), linkError ? _jsx("div", { className: "editability-error editability-error-inline", children: linkError }) : null, _jsxs("div", { className: "editability-popover-actions", children: [_jsx("button", { className: "editability-btn editability-primary", onClick: handleLinkApply, children: "Apply" }), _jsx("button", { className: "editability-btn editability-ghost", onClick: handleLinkRemove, children: "Remove" })] })] })) : null] })] }), _jsx("button", { className: "editability-btn editability-cta", onClick: handleDone, disabled: !activeEditor, children: "Done" })] }), _jsxs("div", { className: "editability-right", children: [_jsx("button", { className: "editability-btn editability-primary", onClick: () => setPublishOpen(true), disabled: changeCount === 0 || publishing, children: publishing ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "editability-spinner", "aria-hidden": true }), "Publishing\u2026"] })) : (`Publish (${changeCount})`) }), _jsx("button", { className: "editability-btn", onClick: handleDiscard, disabled: changeCount === 0, children: "Discard" }), user ? (_jsxs("div", { className: "editability-user", children: [user.picture ? _jsx("img", { src: user.picture, alt: "" }) : null, _jsx("span", { children: user.name ?? user.email }), _jsx("button", { className: "editability-btn", onClick: handleLogout, children: "Log out" })] })) : (_jsx(LoginPanel, { apiBase: apiBase, googleClientId: googleClientId, onLogin: setUser, loading: loading })), _jsx("button", { className: "editability-btn editability-ghost", onClick: handleExit, children: "Exit" })] })] }), publishOpen ? (_jsx(PublishModal, { changes: changes, message: message, setMessage: setMessage, onClose: () => setPublishOpen(false), onPublish: handlePublish, error: error, publishing: publishing })) : null] }));
|
|
243
377
|
}
|
|
244
378
|
function LoginPanel({ apiBase, googleClientId, onLogin, loading }) {
|
|
245
379
|
const containerRef = useRef(null);
|
|
@@ -303,19 +437,20 @@ function buildLabel(change) {
|
|
|
303
437
|
const tag = change.tag.toLowerCase();
|
|
304
438
|
return `${tag.toUpperCase()}: ${snippet}${change.originalText.length > 40 ? "…" : ""}`;
|
|
305
439
|
}
|
|
306
|
-
function activateBlock(element, id, draft, setDraft, setActiveId, setActiveEditor) {
|
|
440
|
+
function activateBlock(element, id, draft, setDraft, setActiveId, setActiveEditor, setFormatState, setHistoryState) {
|
|
307
441
|
const mode = determineMode(element);
|
|
308
442
|
const existing = draft.changes[id];
|
|
309
443
|
const originalHtml = existing?.originalHtml ?? element.innerHTML;
|
|
310
444
|
const originalText = existing?.originalText ?? normalizeTextContent(element.textContent ?? "");
|
|
311
445
|
const datasetFingerprint = element.dataset.editabilityFp;
|
|
312
446
|
const fingerprint = datasetFingerprint ?? existing?.fingerprint ?? fnv1a64(originalText);
|
|
447
|
+
const baselineAst = htmlToAst(originalHtml, mode);
|
|
313
448
|
const current = existing
|
|
314
449
|
? { ...existing, fingerprint }
|
|
315
450
|
: {
|
|
316
451
|
id,
|
|
317
452
|
mode,
|
|
318
|
-
ast:
|
|
453
|
+
ast: baselineAst,
|
|
319
454
|
fingerprint,
|
|
320
455
|
originalHtml,
|
|
321
456
|
originalText,
|
|
@@ -328,11 +463,8 @@ function activateBlock(element, id, draft, setDraft, setActiveId, setActiveEdito
|
|
|
328
463
|
initialHtml: existing ? renderAstToHtml(existing.ast) : originalHtml,
|
|
329
464
|
onUpdate: (ast) => {
|
|
330
465
|
const next = { ...current, ast, lastPath: window.location.pathname };
|
|
331
|
-
const afterHtml = renderAstToHtml(ast);
|
|
332
|
-
const normalizedOriginal = normalizeHtml(current.originalHtml);
|
|
333
|
-
const normalizedAfter = normalizeHtml(afterHtml);
|
|
334
466
|
const updated = { ...draft, updatedAt: Date.now(), changes: { ...draft.changes } };
|
|
335
|
-
if (
|
|
467
|
+
if (astEquals(ast, baselineAst)) {
|
|
336
468
|
delete updated.changes[id];
|
|
337
469
|
}
|
|
338
470
|
else {
|
|
@@ -341,6 +473,8 @@ function activateBlock(element, id, draft, setDraft, setActiveId, setActiveEdito
|
|
|
341
473
|
saveDraft(updated);
|
|
342
474
|
setDraft(updated);
|
|
343
475
|
},
|
|
476
|
+
onFormat: setFormatState,
|
|
477
|
+
onHistory: setHistoryState,
|
|
344
478
|
});
|
|
345
479
|
setActiveId(id);
|
|
346
480
|
setActiveEditor(active);
|
|
@@ -348,7 +482,16 @@ function activateBlock(element, id, draft, setDraft, setActiveId, setActiveEdito
|
|
|
348
482
|
}
|
|
349
483
|
function determineMode(element) {
|
|
350
484
|
const tag = element.tagName.toLowerCase();
|
|
351
|
-
if ([
|
|
485
|
+
if ([
|
|
486
|
+
"div",
|
|
487
|
+
"section",
|
|
488
|
+
"article",
|
|
489
|
+
"main",
|
|
490
|
+
"aside",
|
|
491
|
+
"nav",
|
|
492
|
+
"header",
|
|
493
|
+
"footer",
|
|
494
|
+
].includes(tag)) {
|
|
352
495
|
return "block";
|
|
353
496
|
}
|
|
354
497
|
return "inline";
|
|
@@ -435,7 +578,121 @@ function renderAstToHtml(ast) {
|
|
|
435
578
|
if (ast.mode === "inline") {
|
|
436
579
|
return ast.nodes.map(renderInlineNode).join("");
|
|
437
580
|
}
|
|
438
|
-
return ast.nodes.map(
|
|
581
|
+
return ast.nodes.map(renderBlockNode).join("");
|
|
582
|
+
}
|
|
583
|
+
function renderBlockNode(block) {
|
|
584
|
+
switch (block.type) {
|
|
585
|
+
case "paragraph":
|
|
586
|
+
return `<p>${block.children.map(renderInlineNode).join("")}</p>`;
|
|
587
|
+
case "heading": {
|
|
588
|
+
const level = Math.min(6, Math.max(1, block.level));
|
|
589
|
+
return `<h${level}>${block.children.map(renderInlineNode).join("")}</h${level}>`;
|
|
590
|
+
}
|
|
591
|
+
case "blockquote":
|
|
592
|
+
return `<blockquote>${block.children.map(renderInlineNode).join("")}</blockquote>`;
|
|
593
|
+
case "list": {
|
|
594
|
+
const tag = block.ordered ? "ol" : "ul";
|
|
595
|
+
const items = block.items
|
|
596
|
+
.map((item) => `<li>${item.map(renderInlineNode).join("")}</li>`)
|
|
597
|
+
.join("");
|
|
598
|
+
return `<${tag}>${items}</${tag}>`;
|
|
599
|
+
}
|
|
600
|
+
default:
|
|
601
|
+
return "";
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
function isInlineAst(ast) {
|
|
605
|
+
return ast.mode === "inline";
|
|
606
|
+
}
|
|
607
|
+
function isBlockAst(ast) {
|
|
608
|
+
return ast.mode === "block";
|
|
609
|
+
}
|
|
610
|
+
function astEquals(a, b) {
|
|
611
|
+
if (isInlineAst(a) && isInlineAst(b)) {
|
|
612
|
+
return inlineNodesEqual(a.nodes, b.nodes);
|
|
613
|
+
}
|
|
614
|
+
if (isBlockAst(a) && isBlockAst(b)) {
|
|
615
|
+
return blockNodesEqual(a.nodes, b.nodes);
|
|
616
|
+
}
|
|
617
|
+
return false;
|
|
618
|
+
}
|
|
619
|
+
function blockNodesEqual(a, b) {
|
|
620
|
+
if (a.length !== b.length) {
|
|
621
|
+
return false;
|
|
622
|
+
}
|
|
623
|
+
return a.every((node, idx) => {
|
|
624
|
+
const right = b[idx];
|
|
625
|
+
if (node.type !== right.type) {
|
|
626
|
+
return false;
|
|
627
|
+
}
|
|
628
|
+
switch (node.type) {
|
|
629
|
+
case "paragraph":
|
|
630
|
+
case "blockquote":
|
|
631
|
+
return inlineNodesEqual(node.children, right.children);
|
|
632
|
+
case "heading":
|
|
633
|
+
return (node.level === right.level &&
|
|
634
|
+
inlineNodesEqual(node.children, right.children));
|
|
635
|
+
case "list":
|
|
636
|
+
return (node.ordered === right.ordered &&
|
|
637
|
+
listItemsEqual(node.items, right.items));
|
|
638
|
+
default:
|
|
639
|
+
return false;
|
|
640
|
+
}
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
function listItemsEqual(a, b) {
|
|
644
|
+
if (a.length !== b.length) {
|
|
645
|
+
return false;
|
|
646
|
+
}
|
|
647
|
+
for (let i = 0; i < a.length; i += 1) {
|
|
648
|
+
if (!inlineNodesEqual(a[i], b[i])) {
|
|
649
|
+
return false;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
return true;
|
|
653
|
+
}
|
|
654
|
+
function inlineNodesEqual(a, b) {
|
|
655
|
+
if (a.length !== b.length) {
|
|
656
|
+
return false;
|
|
657
|
+
}
|
|
658
|
+
for (let i = 0; i < a.length; i += 1) {
|
|
659
|
+
const left = a[i];
|
|
660
|
+
const right = b[i];
|
|
661
|
+
if (left.type !== right.type) {
|
|
662
|
+
return false;
|
|
663
|
+
}
|
|
664
|
+
switch (left.type) {
|
|
665
|
+
case "text": {
|
|
666
|
+
if (right.type !== "text") {
|
|
667
|
+
return false;
|
|
668
|
+
}
|
|
669
|
+
if (left.text !== right.text ||
|
|
670
|
+
Boolean(left.bold) !== Boolean(right.bold) ||
|
|
671
|
+
Boolean(left.italic) !== Boolean(right.italic)) {
|
|
672
|
+
return false;
|
|
673
|
+
}
|
|
674
|
+
break;
|
|
675
|
+
}
|
|
676
|
+
case "linebreak": {
|
|
677
|
+
if (right.type !== "linebreak") {
|
|
678
|
+
return false;
|
|
679
|
+
}
|
|
680
|
+
break;
|
|
681
|
+
}
|
|
682
|
+
case "link": {
|
|
683
|
+
if (right.type !== "link") {
|
|
684
|
+
return false;
|
|
685
|
+
}
|
|
686
|
+
if (left.href !== right.href || !inlineNodesEqual(left.children, right.children)) {
|
|
687
|
+
return false;
|
|
688
|
+
}
|
|
689
|
+
break;
|
|
690
|
+
}
|
|
691
|
+
default:
|
|
692
|
+
return false;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
return true;
|
|
439
696
|
}
|
|
440
697
|
function renderInlineNode(node) {
|
|
441
698
|
if (node.type === "text") {
|
|
@@ -465,6 +722,24 @@ function renderInlineNode(node) {
|
|
|
465
722
|
}
|
|
466
723
|
return "";
|
|
467
724
|
}
|
|
725
|
+
const BLOCKED_INTERACTIVE_SELECTOR = "button, input, textarea, select, option, label, details, summary";
|
|
726
|
+
function findNestedInteractive(root) {
|
|
727
|
+
if (root.matches(BLOCKED_INTERACTIVE_SELECTOR)) {
|
|
728
|
+
return root;
|
|
729
|
+
}
|
|
730
|
+
const matches = root.querySelectorAll(BLOCKED_INTERACTIVE_SELECTOR);
|
|
731
|
+
for (const el of Array.from(matches)) {
|
|
732
|
+
if (el === root) {
|
|
733
|
+
continue;
|
|
734
|
+
}
|
|
735
|
+
const editableAncestor = el.closest("[data-editability-id]");
|
|
736
|
+
if (editableAncestor && editableAncestor !== root) {
|
|
737
|
+
continue;
|
|
738
|
+
}
|
|
739
|
+
return el;
|
|
740
|
+
}
|
|
741
|
+
return null;
|
|
742
|
+
}
|
|
468
743
|
function escapeHtml(value) {
|
|
469
744
|
return value
|
|
470
745
|
.replace(/&/g, "&")
|
|
@@ -472,14 +747,23 @@ function escapeHtml(value) {
|
|
|
472
747
|
.replace(/>/g, ">")
|
|
473
748
|
.replace(/\"/g, """);
|
|
474
749
|
}
|
|
475
|
-
function normalizeHtml(html) {
|
|
476
|
-
return html.replace(/\s+/g, " ").trim();
|
|
477
|
-
}
|
|
478
750
|
function astToText(ast) {
|
|
479
751
|
if (ast.mode === "inline") {
|
|
480
752
|
return ast.nodes.map(nodeToText).join("");
|
|
481
753
|
}
|
|
482
|
-
return ast.nodes.map(
|
|
754
|
+
return ast.nodes.map(blockToText).join(" ");
|
|
755
|
+
}
|
|
756
|
+
function blockToText(block) {
|
|
757
|
+
switch (block.type) {
|
|
758
|
+
case "paragraph":
|
|
759
|
+
case "heading":
|
|
760
|
+
case "blockquote":
|
|
761
|
+
return block.children.map(nodeToText).join(" ");
|
|
762
|
+
case "list":
|
|
763
|
+
return block.items.map((item) => item.map(nodeToText).join(" ")).join(" ");
|
|
764
|
+
default:
|
|
765
|
+
return "";
|
|
766
|
+
}
|
|
483
767
|
}
|
|
484
768
|
function nodeToText(node) {
|
|
485
769
|
if (node.type === "text") {
|
|
@@ -499,26 +783,96 @@ function htmlToAst(html, mode) {
|
|
|
499
783
|
if (mode === "inline") {
|
|
500
784
|
return { mode: "inline", nodes: collectInline(wrapper) };
|
|
501
785
|
}
|
|
502
|
-
const blocks =
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
}
|
|
507
|
-
else {
|
|
508
|
-
paragraphs.forEach((p) => {
|
|
509
|
-
blocks.push({ type: "paragraph", children: collectInline(p) });
|
|
510
|
-
});
|
|
786
|
+
const blocks = collectBlocks(wrapper);
|
|
787
|
+
if (blocks.length === 0) {
|
|
788
|
+
const inline = collectInline(wrapper);
|
|
789
|
+
return { mode: "block", nodes: [{ type: "paragraph", children: inline }] };
|
|
511
790
|
}
|
|
512
791
|
return { mode: "block", nodes: blocks };
|
|
513
792
|
}
|
|
793
|
+
function collectBlocks(wrapper) {
|
|
794
|
+
const blocks = [];
|
|
795
|
+
let pendingInline = [];
|
|
796
|
+
const flushInline = () => {
|
|
797
|
+
if (!hasInlineContent(pendingInline)) {
|
|
798
|
+
pendingInline = [];
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
blocks.push({ type: "paragraph", children: pendingInline });
|
|
802
|
+
pendingInline = [];
|
|
803
|
+
};
|
|
804
|
+
wrapper.childNodes.forEach((node) => {
|
|
805
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
806
|
+
pendingInline.push(...collectInlineFromText(node.textContent ?? ""));
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
if (node.nodeType !== Node.ELEMENT_NODE) {
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
const element = node;
|
|
813
|
+
const tag = element.tagName.toLowerCase();
|
|
814
|
+
if (tag === "p") {
|
|
815
|
+
flushInline();
|
|
816
|
+
blocks.push({ type: "paragraph", children: collectInline(element) });
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
if (tag === "blockquote") {
|
|
820
|
+
flushInline();
|
|
821
|
+
blocks.push({ type: "blockquote", children: collectInline(element) });
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
if (tag === "ul" || tag === "ol") {
|
|
825
|
+
flushInline();
|
|
826
|
+
blocks.push({ type: "list", ordered: tag === "ol", items: collectListItems(element) });
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
if (isHeadingTag(tag)) {
|
|
830
|
+
flushInline();
|
|
831
|
+
blocks.push({
|
|
832
|
+
type: "heading",
|
|
833
|
+
level: Math.min(6, Math.max(1, Number(tag.slice(1)))),
|
|
834
|
+
children: collectInline(element),
|
|
835
|
+
});
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
pendingInline.push(...collectInline(element));
|
|
839
|
+
});
|
|
840
|
+
flushInline();
|
|
841
|
+
return blocks;
|
|
842
|
+
}
|
|
843
|
+
function collectListItems(list) {
|
|
844
|
+
const items = [];
|
|
845
|
+
list.childNodes.forEach((child) => {
|
|
846
|
+
if (child.nodeType !== Node.ELEMENT_NODE) {
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
const element = child;
|
|
850
|
+
if (element.tagName.toLowerCase() !== "li") {
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
const onlyParagraph = element.children.length === 1 && element.children[0].tagName.toLowerCase() === "p";
|
|
854
|
+
const target = onlyParagraph ? element.children[0] : element;
|
|
855
|
+
items.push(collectInline(target));
|
|
856
|
+
});
|
|
857
|
+
return items;
|
|
858
|
+
}
|
|
859
|
+
function isHeadingTag(tag) {
|
|
860
|
+
return /^h[1-6]$/.test(tag);
|
|
861
|
+
}
|
|
862
|
+
function hasInlineContent(nodes) {
|
|
863
|
+
return normalizeTextContent(nodes.map(nodeToText).join("")).length > 0;
|
|
864
|
+
}
|
|
865
|
+
function collectInlineFromText(text) {
|
|
866
|
+
if (!text) {
|
|
867
|
+
return [];
|
|
868
|
+
}
|
|
869
|
+
return [{ type: "text", text }];
|
|
870
|
+
}
|
|
514
871
|
function collectInline(root) {
|
|
515
872
|
const nodes = [];
|
|
516
873
|
root.childNodes.forEach((node) => {
|
|
517
874
|
if (node.nodeType === Node.TEXT_NODE) {
|
|
518
|
-
|
|
519
|
-
if (text) {
|
|
520
|
-
nodes.push({ type: "text", text });
|
|
521
|
-
}
|
|
875
|
+
nodes.push(...collectInlineFromText(node.textContent ?? ""));
|
|
522
876
|
return;
|
|
523
877
|
}
|
|
524
878
|
if (node.nodeType !== Node.ELEMENT_NODE) {
|
|
@@ -589,15 +943,13 @@ function persistActiveDraft(draftRef, activeChangeRef, editor, setDraft) {
|
|
|
589
943
|
return;
|
|
590
944
|
}
|
|
591
945
|
const ast = editor.getAst();
|
|
592
|
-
const
|
|
593
|
-
const normalizedOriginal = normalizeHtml(change.originalHtml);
|
|
594
|
-
const normalizedAfter = normalizeHtml(afterHtml);
|
|
946
|
+
const baselineAst = htmlToAst(change.originalHtml, change.mode);
|
|
595
947
|
const updated = {
|
|
596
948
|
...draftRef.current,
|
|
597
949
|
updatedAt: Date.now(),
|
|
598
950
|
changes: { ...draftRef.current.changes },
|
|
599
951
|
};
|
|
600
|
-
if (
|
|
952
|
+
if (astEquals(ast, baselineAst)) {
|
|
601
953
|
delete updated.changes[change.id];
|
|
602
954
|
}
|
|
603
955
|
else {
|
|
@@ -615,6 +967,47 @@ async function fetchMe(apiBase) {
|
|
|
615
967
|
const data = (await response.json());
|
|
616
968
|
return data.user;
|
|
617
969
|
}
|
|
970
|
+
function getRangeFromPoint(point) {
|
|
971
|
+
const doc = document;
|
|
972
|
+
if (doc.caretRangeFromPoint) {
|
|
973
|
+
return doc.caretRangeFromPoint(point.x, point.y);
|
|
974
|
+
}
|
|
975
|
+
if (doc.caretPositionFromPoint) {
|
|
976
|
+
const position = doc.caretPositionFromPoint(point.x, point.y);
|
|
977
|
+
if (!position) {
|
|
978
|
+
return null;
|
|
979
|
+
}
|
|
980
|
+
const range = document.createRange();
|
|
981
|
+
range.setStart(position.offsetNode, position.offset);
|
|
982
|
+
range.collapse(true);
|
|
983
|
+
return range;
|
|
984
|
+
}
|
|
985
|
+
return null;
|
|
986
|
+
}
|
|
987
|
+
function placeCaretAtPoint(container, point) {
|
|
988
|
+
const range = getRangeFromPoint(point);
|
|
989
|
+
if (!range || !container.contains(range.startContainer)) {
|
|
990
|
+
placeCaretAtEnd(container);
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
const selection = window.getSelection();
|
|
994
|
+
if (!selection) {
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
selection.removeAllRanges();
|
|
998
|
+
selection.addRange(range);
|
|
999
|
+
}
|
|
1000
|
+
function placeCaretAtEnd(container) {
|
|
1001
|
+
const selection = window.getSelection();
|
|
1002
|
+
if (!selection) {
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
const range = document.createRange();
|
|
1006
|
+
range.selectNodeContents(container);
|
|
1007
|
+
range.collapse(false);
|
|
1008
|
+
selection.removeAllRanges();
|
|
1009
|
+
selection.addRange(range);
|
|
1010
|
+
}
|
|
618
1011
|
function ensureStyles() {
|
|
619
1012
|
if (document.getElementById("editability-style")) {
|
|
620
1013
|
return;
|
|
@@ -622,37 +1015,85 @@ function ensureStyles() {
|
|
|
622
1015
|
const style = document.createElement("style");
|
|
623
1016
|
style.id = "editability-style";
|
|
624
1017
|
style.textContent = `
|
|
625
|
-
.
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
1018
|
+
@import url("https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&display=swap");
|
|
1019
|
+
:root {
|
|
1020
|
+
--editability-bg: #0b1020;
|
|
1021
|
+
--editability-surface: rgba(11, 18, 38, 0.92);
|
|
1022
|
+
--editability-surface-soft: rgba(14, 24, 45, 0.9);
|
|
1023
|
+
--editability-border: rgba(148, 163, 184, 0.28);
|
|
1024
|
+
--editability-accent: #5eead4;
|
|
1025
|
+
--editability-accent-strong: #38bdf8;
|
|
1026
|
+
--editability-text: #f8fafc;
|
|
1027
|
+
--editability-muted: #94a3b8;
|
|
1028
|
+
}
|
|
1029
|
+
.editability-authenticated [data-editability-id]:hover { outline: 2px dashed rgba(94, 234, 212, 0.9); outline-offset: 2px; cursor: text; }
|
|
1030
|
+
.editability-authenticated [data-editability-id].editability-dirty { outline: 2px solid rgba(56, 189, 248, 0.9); outline-offset: 2px; }
|
|
1031
|
+
.editability-active.editability-inline p { margin: 0; padding: 0; display: inline; font: inherit; line-height: inherit; }
|
|
1032
|
+
.editability-active.editability-inline p > span,
|
|
1033
|
+
.editability-active.editability-inline span[data-lexical-text] { font: inherit; line-height: inherit; }
|
|
1034
|
+
.editability-active.editability-block.editability-no-paragraphs p { margin: 0; padding: 0; font: inherit; line-height: inherit; }
|
|
1035
|
+
.editability-active.editability-block.editability-no-paragraphs p > span,
|
|
1036
|
+
.editability-active.editability-block.editability-no-paragraphs span[data-lexical-text] { font: inherit; line-height: inherit; }
|
|
1037
|
+
.editability-overlay { position: fixed; inset: auto 0 0 0; z-index: 9999; font-family: "Space Grotesk", ui-sans-serif, system-ui; color: var(--editability-text); }
|
|
1038
|
+
.editability-bar { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 14px 18px; background: linear-gradient(135deg, rgba(11, 16, 32, 0.98), rgba(15, 23, 42, 0.92)); border-top: 1px solid rgba(148, 163, 184, 0.2); box-shadow: 0 -14px 30px rgba(2, 6, 23, 0.5); backdrop-filter: blur(16px); }
|
|
1039
|
+
.editability-left, .editability-center, .editability-right { display: flex; align-items: center; gap: 12px; }
|
|
1040
|
+
.editability-left { min-width: 220px; }
|
|
1041
|
+
.editability-center { flex: 1; flex-wrap: wrap; justify-content: center; }
|
|
1042
|
+
.editability-right { justify-content: flex-end; min-width: 260px; }
|
|
1043
|
+
.editability-brand { display: flex; align-items: center; gap: 8px; }
|
|
1044
|
+
.editability-pill { padding: 4px 12px; background: var(--editability-accent); color: #05121d; border-radius: 999px; font-weight: 700; letter-spacing: 0.2px; }
|
|
1045
|
+
.editability-status { font-size: 12px; opacity: 0.8; }
|
|
1046
|
+
.editability-badge { padding: 4px 10px; border-radius: 999px; border: 1px solid var(--editability-border); font-size: 12px; color: var(--editability-muted); }
|
|
1047
|
+
.editability-badge-active { color: #bae6fd; border-color: rgba(56, 189, 248, 0.6); background: rgba(56, 189, 248, 0.15); }
|
|
1048
|
+
.editability-group { display: flex; align-items: center; gap: 6px; }
|
|
1049
|
+
.editability-divider { width: 1px; height: 26px; background: rgba(148, 163, 184, 0.2); }
|
|
1050
|
+
.editability-btn { display: inline-flex; align-items: center; gap: 8px; padding: 6px 12px; border-radius: 10px; border: 1px solid var(--editability-border); background: var(--editability-surface-soft); color: var(--editability-text); font-size: 12px; font-weight: 600; cursor: pointer; transition: transform 0.12s ease, border-color 0.12s ease, background 0.12s ease; }
|
|
1051
|
+
.editability-btn:disabled { opacity: 0.45; cursor: not-allowed; }
|
|
1052
|
+
.editability-btn:hover:not(:disabled) { background: rgba(30, 41, 59, 0.85); border-color: rgba(148, 163, 184, 0.45); transform: translateY(-1px); }
|
|
1053
|
+
.editability-btn.active { background: rgba(94, 234, 212, 0.18); border-color: rgba(94, 234, 212, 0.6); color: var(--editability-accent); }
|
|
1054
|
+
.editability-icon-btn { width: 36px; height: 34px; padding: 0; justify-content: center; }
|
|
1055
|
+
.editability-icon { width: 18px; height: 18px; }
|
|
1056
|
+
.editability-block-btn { min-width: 140px; justify-content: space-between; }
|
|
1057
|
+
.editability-primary { background: var(--editability-accent); color: #05121d; border-color: transparent; }
|
|
1058
|
+
.editability-cta { background: rgba(56, 189, 248, 0.2); border-color: rgba(56, 189, 248, 0.5); color: #bae6fd; }
|
|
1059
|
+
.editability-ghost { background: transparent; border-color: rgba(148, 163, 184, 0.2); color: var(--editability-muted); }
|
|
636
1060
|
.editability-spinner { display: inline-block; width: 12px; height: 12px; border-radius: 999px; border: 2px solid rgba(248, 250, 252, 0.4); border-top-color: #f8fafc; animation: editability-spin 0.8s linear infinite; margin-right: 6px; vertical-align: -2px; }
|
|
637
1061
|
@keyframes editability-spin { to { transform: rotate(360deg); } }
|
|
638
|
-
.editability-
|
|
639
|
-
.editability-
|
|
640
|
-
.editability-
|
|
641
|
-
.editability-
|
|
1062
|
+
.editability-popover-anchor { position: relative; }
|
|
1063
|
+
.editability-menu { position: absolute; bottom: calc(100% + 10px); left: 0; top: auto; background: var(--editability-surface); border: 1px solid var(--editability-border); border-radius: 14px; padding: 6px; min-width: 180px; display: flex; flex-direction: column; gap: 4px; box-shadow: 0 18px 30px rgba(2, 6, 23, 0.45); z-index: 10001; }
|
|
1064
|
+
.editability-menu-item { display: flex; align-items: center; gap: 8px; width: 100%; padding: 8px 10px; border-radius: 10px; border: 1px solid transparent; background: transparent; color: var(--editability-text); font-size: 12px; font-weight: 600; text-align: left; cursor: pointer; }
|
|
1065
|
+
.editability-menu-item:hover { background: rgba(148, 163, 184, 0.12); }
|
|
1066
|
+
.editability-menu-item.active { background: rgba(94, 234, 212, 0.2); border-color: rgba(94, 234, 212, 0.5); color: var(--editability-accent); }
|
|
1067
|
+
.editability-popover { position: absolute; bottom: calc(100% + 10px); right: 0; top: auto; min-width: 240px; background: var(--editability-surface); border: 1px solid var(--editability-border); border-radius: 14px; padding: 12px; box-shadow: 0 18px 30px rgba(2, 6, 23, 0.45); display: flex; flex-direction: column; gap: 8px; z-index: 10001; }
|
|
1068
|
+
.editability-popover-title { font-size: 12px; font-weight: 600; color: var(--editability-muted); }
|
|
1069
|
+
.editability-input { padding: 8px 10px; border-radius: 10px; border: 1px solid rgba(148, 163, 184, 0.3); background: rgba(15, 23, 42, 0.6); color: var(--editability-text); font-size: 13px; }
|
|
1070
|
+
.editability-input:focus { outline: none; border-color: rgba(94, 234, 212, 0.7); box-shadow: 0 0 0 3px rgba(94, 234, 212, 0.15); }
|
|
1071
|
+
.editability-popover-actions { display: flex; gap: 8px; justify-content: flex-end; }
|
|
1072
|
+
.editability-user { display: flex; align-items: center; gap: 8px; padding: 4px 8px; border-radius: 999px; background: rgba(148, 163, 184, 0.12); }
|
|
1073
|
+
.editability-user img { width: 26px; height: 26px; border-radius: 999px; }
|
|
1074
|
+
.editability-modal { position: fixed; inset: 0; background: rgba(11, 16, 32, 0.7); display: flex; align-items: center; justify-content: center; z-index: 10000; }
|
|
1075
|
+
.editability-modal-content { background: #ffffff; color: #0f172a; padding: 24px; border-radius: 18px; width: min(900px, 92vw); max-height: 80vh; overflow: auto; }
|
|
642
1076
|
.editability-change { border-bottom: 1px solid #e2e8f0; padding: 12px 0; }
|
|
643
1077
|
.editability-change-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 16px; }
|
|
644
|
-
.editability-preview { background: #f8fafc; border-radius:
|
|
1078
|
+
.editability-preview { background: #f8fafc; border-radius: 10px; padding: 12px; min-height: 40px; }
|
|
645
1079
|
.editability-diff { margin-top: 8px; font-size: 12px; }
|
|
646
1080
|
.editability-diff span.added { background: #bbf7d0; }
|
|
647
1081
|
.editability-diff span.removed { background: #fecaca; text-decoration: line-through; }
|
|
648
1082
|
.editability-muted { opacity: 0.7; }
|
|
649
1083
|
.editability-label { display: flex; flex-direction: column; gap: 6px; margin-top: 12px; }
|
|
650
|
-
.editability-label input { padding: 8px; border-radius:
|
|
1084
|
+
.editability-label input { padding: 8px; border-radius: 8px; border: 1px solid #cbd5f5; }
|
|
651
1085
|
.editability-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 16px; }
|
|
652
1086
|
.editability-error { color: #dc2626; }
|
|
653
|
-
.editability-
|
|
654
|
-
.editability-login
|
|
1087
|
+
.editability-error-inline { font-size: 12px; color: #fecaca; }
|
|
1088
|
+
.editability-login { position: fixed; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px; background: radial-gradient(circle at top, rgba(56, 189, 248, 0.2), rgba(11, 16, 32, 0.95)); color: #f8fafc; padding: 24px; text-align: center; }
|
|
1089
|
+
.editability-login h2 { margin: 0; font-size: 22px; }
|
|
655
1090
|
.editability-login p { margin: 0; max-width: 360px; opacity: 0.9; }
|
|
1091
|
+
@media (max-width: 900px) {
|
|
1092
|
+
.editability-bar { flex-direction: column; align-items: stretch; }
|
|
1093
|
+
.editability-left, .editability-right { justify-content: space-between; }
|
|
1094
|
+
.editability-center { justify-content: flex-start; }
|
|
1095
|
+
.editability-right { flex-wrap: wrap; }
|
|
1096
|
+
}
|
|
656
1097
|
`;
|
|
657
1098
|
document.head.appendChild(style);
|
|
658
1099
|
}
|
|
@@ -671,4 +1112,40 @@ function loadGoogleScript() {
|
|
|
671
1112
|
document.head.appendChild(script);
|
|
672
1113
|
});
|
|
673
1114
|
}
|
|
1115
|
+
function Icon({ children, viewBox = "0 0 24 24", }) {
|
|
1116
|
+
return (_jsx("svg", { className: "editability-icon", viewBox: viewBox, "aria-hidden": "true", fill: "none", stroke: "currentColor", strokeWidth: "1.7", strokeLinecap: "round", strokeLinejoin: "round", children: children }));
|
|
1117
|
+
}
|
|
1118
|
+
function IconUndo() {
|
|
1119
|
+
return (_jsxs(Icon, { children: [_jsx("path", { d: "M7 7l-4 5 4 5" }), _jsx("path", { d: "M3 12h9a6 6 0 0 1 0 12h-2" })] }));
|
|
1120
|
+
}
|
|
1121
|
+
function IconRedo() {
|
|
1122
|
+
return (_jsxs(Icon, { children: [_jsx("path", { d: "M17 7l4 5-4 5" }), _jsx("path", { d: "M21 12h-9a6 6 0 0 0 0 12h2" })] }));
|
|
1123
|
+
}
|
|
1124
|
+
function IconChevron() {
|
|
1125
|
+
return (_jsx(Icon, { children: _jsx("path", { d: "M6 9l6 6 6-6" }) }));
|
|
1126
|
+
}
|
|
1127
|
+
function IconBold() {
|
|
1128
|
+
return (_jsxs(Icon, { children: [_jsx("path", { d: "M8 5h6a3 3 0 0 1 0 6H8z" }), _jsx("path", { d: "M8 11h7a3 3 0 0 1 0 6H8z" })] }));
|
|
1129
|
+
}
|
|
1130
|
+
function IconItalic() {
|
|
1131
|
+
return (_jsxs(Icon, { children: [_jsx("path", { d: "M10 5h8" }), _jsx("path", { d: "M6 19h8" }), _jsx("path", { d: "M14 5l-4 14" })] }));
|
|
1132
|
+
}
|
|
1133
|
+
function IconLink() {
|
|
1134
|
+
return (_jsxs(Icon, { children: [_jsx("path", { d: "M10 13a5 5 0 0 1 0-7l1.5-1.5a5 5 0 1 1 7 7L17 12" }), _jsx("path", { d: "M14 11a5 5 0 0 1 0 7L12.5 19.5a5 5 0 0 1-7-7L7 11" })] }));
|
|
1135
|
+
}
|
|
1136
|
+
function IconListBullet() {
|
|
1137
|
+
return (_jsxs(Icon, { children: [_jsx("circle", { cx: "5", cy: "7", r: "1.5", fill: "currentColor", stroke: "none" }), _jsx("circle", { cx: "5", cy: "12", r: "1.5", fill: "currentColor", stroke: "none" }), _jsx("circle", { cx: "5", cy: "17", r: "1.5", fill: "currentColor", stroke: "none" }), _jsx("path", { d: "M9 7h10" }), _jsx("path", { d: "M9 12h10" }), _jsx("path", { d: "M9 17h10" })] }));
|
|
1138
|
+
}
|
|
1139
|
+
function IconListNumber() {
|
|
1140
|
+
return (_jsxs(Icon, { children: [_jsx("text", { x: "3", y: "9", fontSize: "7", fontWeight: "600", fontFamily: "inherit", fill: "currentColor", stroke: "none", children: "1" }), _jsx("text", { x: "3", y: "17", fontSize: "7", fontWeight: "600", fontFamily: "inherit", fill: "currentColor", stroke: "none", children: "2" }), _jsx("path", { d: "M9 7h10" }), _jsx("path", { d: "M9 12h10" }), _jsx("path", { d: "M9 17h10" })] }));
|
|
1141
|
+
}
|
|
1142
|
+
function IconParagraph() {
|
|
1143
|
+
return (_jsxs(Icon, { children: [_jsx("text", { x: "4", y: "16", fontSize: "9", fontWeight: "600", fontFamily: "inherit", fill: "currentColor", stroke: "none", children: "P" }), _jsx("path", { d: "M12 8h8" }), _jsx("path", { d: "M12 12h8" }), _jsx("path", { d: "M12 16h8" })] }));
|
|
1144
|
+
}
|
|
1145
|
+
function IconHeading({ label }) {
|
|
1146
|
+
return (_jsxs(Icon, { children: [_jsx("text", { x: "3", y: "15", fontSize: "8", fontWeight: "700", fontFamily: "inherit", fill: "currentColor", stroke: "none", children: label }), _jsx("path", { d: "M12 8h8" }), _jsx("path", { d: "M12 12h8" }), _jsx("path", { d: "M12 16h8" })] }));
|
|
1147
|
+
}
|
|
1148
|
+
function IconQuote() {
|
|
1149
|
+
return (_jsxs(Icon, { children: [_jsx("path", { d: "M6 8h5v5H6z" }), _jsx("path", { d: "M13 8h5v5h-5z" }), _jsx("path", { d: "M6 13a4 4 0 0 0 4 4" }), _jsx("path", { d: "M13 13a4 4 0 0 0 4 4" })] }));
|
|
1150
|
+
}
|
|
674
1151
|
//# sourceMappingURL=overlay.js.map
|