@editability/editor-lexical 0.1.0 → 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 +559 -72
- package/dist/overlay.js.map +1 -1
- package/package.json +4 -2
package/dist/overlay.js
CHANGED
|
@@ -1,17 +1,31 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
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);
|
|
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);
|
|
15
29
|
const observerRef = useRef(null);
|
|
16
30
|
const applyingRef = useRef(false);
|
|
17
31
|
const applyScheduledRef = useRef(null);
|
|
@@ -21,6 +35,9 @@ export function OverlayApp({ apiBase, googleClientId }) {
|
|
|
21
35
|
const activeSnapshotRef = useRef(null);
|
|
22
36
|
const activeChangeRef = useRef(null);
|
|
23
37
|
const draftRef = useRef(draft);
|
|
38
|
+
const linkInputRef = useRef(null);
|
|
39
|
+
const linkPanelRef = useRef(null);
|
|
40
|
+
const blockMenuRef = useRef(null);
|
|
24
41
|
const changes = useMemo(() => Object.values(draft.changes), [draft]);
|
|
25
42
|
useEffect(() => {
|
|
26
43
|
ensureStyles();
|
|
@@ -41,6 +58,46 @@ export function OverlayApp({ apiBase, googleClientId }) {
|
|
|
41
58
|
useEffect(() => {
|
|
42
59
|
activeIdRef.current = activeId;
|
|
43
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]);
|
|
44
101
|
useEffect(() => {
|
|
45
102
|
document.body.classList.toggle("editability-authenticated", Boolean(user));
|
|
46
103
|
return () => {
|
|
@@ -89,6 +146,11 @@ export function OverlayApp({ apiBase, googleClientId }) {
|
|
|
89
146
|
if (!id) {
|
|
90
147
|
return;
|
|
91
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
|
+
}
|
|
92
154
|
if (activeId === id) {
|
|
93
155
|
return;
|
|
94
156
|
}
|
|
@@ -97,10 +159,18 @@ export function OverlayApp({ apiBase, googleClientId }) {
|
|
|
97
159
|
}
|
|
98
160
|
const existing = draftRef.current.changes[id];
|
|
99
161
|
activeSnapshotRef.current = { id, html: existing ? existing.originalHtml : block.innerHTML };
|
|
100
|
-
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);
|
|
101
163
|
activeEditorRef.current = editor;
|
|
102
164
|
activeElementRef.current = block;
|
|
103
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
|
+
});
|
|
104
174
|
};
|
|
105
175
|
const linkGuard = (event) => {
|
|
106
176
|
const target = event.target;
|
|
@@ -136,7 +206,71 @@ export function OverlayApp({ apiBase, googleClientId }) {
|
|
|
136
206
|
destroyActiveEditor("done", true, draftRef, activeEditorRef, activeElementRef, activeIdRef, activeSnapshotRef, activeChangeRef, setDraft, setActiveEditor, setActiveId, observerRef, applyingRef);
|
|
137
207
|
}
|
|
138
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
|
+
};
|
|
139
269
|
const handlePublish = async () => {
|
|
270
|
+
if (publishing) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
setPublishing(true);
|
|
140
274
|
setError(null);
|
|
141
275
|
try {
|
|
142
276
|
const attemptPublish = async (force) => {
|
|
@@ -189,6 +323,9 @@ export function OverlayApp({ apiBase, googleClientId }) {
|
|
|
189
323
|
catch (err) {
|
|
190
324
|
setError(err instanceof Error ? err.message : "Publish failed.");
|
|
191
325
|
}
|
|
326
|
+
finally {
|
|
327
|
+
setPublishing(false);
|
|
328
|
+
}
|
|
192
329
|
};
|
|
193
330
|
const handleDiscard = () => {
|
|
194
331
|
if (!confirm("Discard all local edits?")) {
|
|
@@ -213,25 +350,30 @@ export function OverlayApp({ apiBase, googleClientId }) {
|
|
|
213
350
|
if (!user) {
|
|
214
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" })] }) }));
|
|
215
352
|
}
|
|
216
|
-
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,
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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] }));
|
|
235
377
|
}
|
|
236
378
|
function LoginPanel({ apiBase, googleClientId, onLogin, loading }) {
|
|
237
379
|
const containerRef = useRef(null);
|
|
@@ -278,8 +420,8 @@ function LoginPanel({ apiBase, googleClientId, onLogin, loading }) {
|
|
|
278
420
|
}
|
|
279
421
|
return _jsx("div", { ref: containerRef });
|
|
280
422
|
}
|
|
281
|
-
function PublishModal({ changes, message, setMessage, onClose, onPublish, error, }) {
|
|
282
|
-
return (_jsx("div", { className: "editability-modal", children: _jsxs("div", { className: "editability-modal-content", children: [_jsx("h2", { children: "Review changes" }), _jsx("p", { children: "Make sure everything looks right before publishing." }), _jsx("div", { className: "editability-summary", children: changes.map((change) => (_jsx(ChangePreview, { change: change }, change.id))) }), _jsxs("label", { className: "editability-label", children: ["Summary (optional)", _jsx("input", { value: message, onChange: (event) => setMessage(event.target.value), placeholder: "e.g. Updated hero message" })] }), error ? _jsx("p", { className: "editability-error", children: error }) : null, _jsxs("div", { className: "editability-actions", children: [_jsx("button", { className: "editability-btn", onClick: onClose, children: "Cancel" }), _jsx("button", { className: "editability-btn editability-primary", onClick: onPublish, children: "Publish" })] })] }) }));
|
|
423
|
+
function PublishModal({ changes, message, setMessage, onClose, onPublish, error, publishing, }) {
|
|
424
|
+
return (_jsx("div", { className: "editability-modal", children: _jsxs("div", { className: "editability-modal-content", children: [_jsx("h2", { children: "Review changes" }), _jsx("p", { children: "Make sure everything looks right before publishing." }), _jsx("div", { className: "editability-summary", children: changes.map((change) => (_jsx(ChangePreview, { change: change }, change.id))) }), _jsxs("label", { className: "editability-label", children: ["Summary (optional)", _jsx("input", { value: message, onChange: (event) => setMessage(event.target.value), placeholder: "e.g. Updated hero message" })] }), error ? _jsx("p", { className: "editability-error", children: error }) : null, _jsxs("div", { className: "editability-actions", children: [_jsx("button", { className: "editability-btn", onClick: onClose, disabled: publishing, children: "Cancel" }), _jsx("button", { className: "editability-btn editability-primary", onClick: onPublish, disabled: publishing, children: publishing ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "editability-spinner", "aria-hidden": true }), "Publishing\u2026"] })) : ("Publish") })] })] }) }));
|
|
283
425
|
}
|
|
284
426
|
function ChangePreview({ change }) {
|
|
285
427
|
const afterHtml = renderAstToHtml(change.ast);
|
|
@@ -295,19 +437,20 @@ function buildLabel(change) {
|
|
|
295
437
|
const tag = change.tag.toLowerCase();
|
|
296
438
|
return `${tag.toUpperCase()}: ${snippet}${change.originalText.length > 40 ? "…" : ""}`;
|
|
297
439
|
}
|
|
298
|
-
function activateBlock(element, id, draft, setDraft, setActiveId, setActiveEditor) {
|
|
440
|
+
function activateBlock(element, id, draft, setDraft, setActiveId, setActiveEditor, setFormatState, setHistoryState) {
|
|
299
441
|
const mode = determineMode(element);
|
|
300
442
|
const existing = draft.changes[id];
|
|
301
443
|
const originalHtml = existing?.originalHtml ?? element.innerHTML;
|
|
302
444
|
const originalText = existing?.originalText ?? normalizeTextContent(element.textContent ?? "");
|
|
303
445
|
const datasetFingerprint = element.dataset.editabilityFp;
|
|
304
446
|
const fingerprint = datasetFingerprint ?? existing?.fingerprint ?? fnv1a64(originalText);
|
|
447
|
+
const baselineAst = htmlToAst(originalHtml, mode);
|
|
305
448
|
const current = existing
|
|
306
449
|
? { ...existing, fingerprint }
|
|
307
450
|
: {
|
|
308
451
|
id,
|
|
309
452
|
mode,
|
|
310
|
-
ast:
|
|
453
|
+
ast: baselineAst,
|
|
311
454
|
fingerprint,
|
|
312
455
|
originalHtml,
|
|
313
456
|
originalText,
|
|
@@ -320,11 +463,8 @@ function activateBlock(element, id, draft, setDraft, setActiveId, setActiveEdito
|
|
|
320
463
|
initialHtml: existing ? renderAstToHtml(existing.ast) : originalHtml,
|
|
321
464
|
onUpdate: (ast) => {
|
|
322
465
|
const next = { ...current, ast, lastPath: window.location.pathname };
|
|
323
|
-
const afterHtml = renderAstToHtml(ast);
|
|
324
|
-
const normalizedOriginal = normalizeHtml(current.originalHtml);
|
|
325
|
-
const normalizedAfter = normalizeHtml(afterHtml);
|
|
326
466
|
const updated = { ...draft, updatedAt: Date.now(), changes: { ...draft.changes } };
|
|
327
|
-
if (
|
|
467
|
+
if (astEquals(ast, baselineAst)) {
|
|
328
468
|
delete updated.changes[id];
|
|
329
469
|
}
|
|
330
470
|
else {
|
|
@@ -333,6 +473,8 @@ function activateBlock(element, id, draft, setDraft, setActiveId, setActiveEdito
|
|
|
333
473
|
saveDraft(updated);
|
|
334
474
|
setDraft(updated);
|
|
335
475
|
},
|
|
476
|
+
onFormat: setFormatState,
|
|
477
|
+
onHistory: setHistoryState,
|
|
336
478
|
});
|
|
337
479
|
setActiveId(id);
|
|
338
480
|
setActiveEditor(active);
|
|
@@ -340,7 +482,16 @@ function activateBlock(element, id, draft, setDraft, setActiveId, setActiveEdito
|
|
|
340
482
|
}
|
|
341
483
|
function determineMode(element) {
|
|
342
484
|
const tag = element.tagName.toLowerCase();
|
|
343
|
-
if ([
|
|
485
|
+
if ([
|
|
486
|
+
"div",
|
|
487
|
+
"section",
|
|
488
|
+
"article",
|
|
489
|
+
"main",
|
|
490
|
+
"aside",
|
|
491
|
+
"nav",
|
|
492
|
+
"header",
|
|
493
|
+
"footer",
|
|
494
|
+
].includes(tag)) {
|
|
344
495
|
return "block";
|
|
345
496
|
}
|
|
346
497
|
return "inline";
|
|
@@ -427,7 +578,121 @@ function renderAstToHtml(ast) {
|
|
|
427
578
|
if (ast.mode === "inline") {
|
|
428
579
|
return ast.nodes.map(renderInlineNode).join("");
|
|
429
580
|
}
|
|
430
|
-
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;
|
|
431
696
|
}
|
|
432
697
|
function renderInlineNode(node) {
|
|
433
698
|
if (node.type === "text") {
|
|
@@ -457,6 +722,24 @@ function renderInlineNode(node) {
|
|
|
457
722
|
}
|
|
458
723
|
return "";
|
|
459
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
|
+
}
|
|
460
743
|
function escapeHtml(value) {
|
|
461
744
|
return value
|
|
462
745
|
.replace(/&/g, "&")
|
|
@@ -464,14 +747,23 @@ function escapeHtml(value) {
|
|
|
464
747
|
.replace(/>/g, ">")
|
|
465
748
|
.replace(/\"/g, """);
|
|
466
749
|
}
|
|
467
|
-
function normalizeHtml(html) {
|
|
468
|
-
return html.replace(/\s+/g, " ").trim();
|
|
469
|
-
}
|
|
470
750
|
function astToText(ast) {
|
|
471
751
|
if (ast.mode === "inline") {
|
|
472
752
|
return ast.nodes.map(nodeToText).join("");
|
|
473
753
|
}
|
|
474
|
-
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
|
+
}
|
|
475
767
|
}
|
|
476
768
|
function nodeToText(node) {
|
|
477
769
|
if (node.type === "text") {
|
|
@@ -491,26 +783,96 @@ function htmlToAst(html, mode) {
|
|
|
491
783
|
if (mode === "inline") {
|
|
492
784
|
return { mode: "inline", nodes: collectInline(wrapper) };
|
|
493
785
|
}
|
|
494
|
-
const blocks =
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
}
|
|
499
|
-
else {
|
|
500
|
-
paragraphs.forEach((p) => {
|
|
501
|
-
blocks.push({ type: "paragraph", children: collectInline(p) });
|
|
502
|
-
});
|
|
786
|
+
const blocks = collectBlocks(wrapper);
|
|
787
|
+
if (blocks.length === 0) {
|
|
788
|
+
const inline = collectInline(wrapper);
|
|
789
|
+
return { mode: "block", nodes: [{ type: "paragraph", children: inline }] };
|
|
503
790
|
}
|
|
504
791
|
return { mode: "block", nodes: blocks };
|
|
505
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
|
+
}
|
|
506
871
|
function collectInline(root) {
|
|
507
872
|
const nodes = [];
|
|
508
873
|
root.childNodes.forEach((node) => {
|
|
509
874
|
if (node.nodeType === Node.TEXT_NODE) {
|
|
510
|
-
|
|
511
|
-
if (text) {
|
|
512
|
-
nodes.push({ type: "text", text });
|
|
513
|
-
}
|
|
875
|
+
nodes.push(...collectInlineFromText(node.textContent ?? ""));
|
|
514
876
|
return;
|
|
515
877
|
}
|
|
516
878
|
if (node.nodeType !== Node.ELEMENT_NODE) {
|
|
@@ -581,15 +943,13 @@ function persistActiveDraft(draftRef, activeChangeRef, editor, setDraft) {
|
|
|
581
943
|
return;
|
|
582
944
|
}
|
|
583
945
|
const ast = editor.getAst();
|
|
584
|
-
const
|
|
585
|
-
const normalizedOriginal = normalizeHtml(change.originalHtml);
|
|
586
|
-
const normalizedAfter = normalizeHtml(afterHtml);
|
|
946
|
+
const baselineAst = htmlToAst(change.originalHtml, change.mode);
|
|
587
947
|
const updated = {
|
|
588
948
|
...draftRef.current,
|
|
589
949
|
updatedAt: Date.now(),
|
|
590
950
|
changes: { ...draftRef.current.changes },
|
|
591
951
|
};
|
|
592
|
-
if (
|
|
952
|
+
if (astEquals(ast, baselineAst)) {
|
|
593
953
|
delete updated.changes[change.id];
|
|
594
954
|
}
|
|
595
955
|
else {
|
|
@@ -607,6 +967,47 @@ async function fetchMe(apiBase) {
|
|
|
607
967
|
const data = (await response.json());
|
|
608
968
|
return data.user;
|
|
609
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
|
+
}
|
|
610
1011
|
function ensureStyles() {
|
|
611
1012
|
if (document.getElementById("editability-style")) {
|
|
612
1013
|
return;
|
|
@@ -614,35 +1015,85 @@ function ensureStyles() {
|
|
|
614
1015
|
const style = document.createElement("style");
|
|
615
1016
|
style.id = "editability-style";
|
|
616
1017
|
style.textContent = `
|
|
617
|
-
.
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
.editability-
|
|
629
|
-
.editability-
|
|
630
|
-
.editability-
|
|
631
|
-
.editability-
|
|
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); }
|
|
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; }
|
|
1061
|
+
@keyframes editability-spin { to { transform: rotate(360deg); } }
|
|
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; }
|
|
632
1076
|
.editability-change { border-bottom: 1px solid #e2e8f0; padding: 12px 0; }
|
|
633
1077
|
.editability-change-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 16px; }
|
|
634
|
-
.editability-preview { background: #f8fafc; border-radius:
|
|
1078
|
+
.editability-preview { background: #f8fafc; border-radius: 10px; padding: 12px; min-height: 40px; }
|
|
635
1079
|
.editability-diff { margin-top: 8px; font-size: 12px; }
|
|
636
1080
|
.editability-diff span.added { background: #bbf7d0; }
|
|
637
1081
|
.editability-diff span.removed { background: #fecaca; text-decoration: line-through; }
|
|
638
1082
|
.editability-muted { opacity: 0.7; }
|
|
639
1083
|
.editability-label { display: flex; flex-direction: column; gap: 6px; margin-top: 12px; }
|
|
640
|
-
.editability-label input { padding: 8px; border-radius:
|
|
1084
|
+
.editability-label input { padding: 8px; border-radius: 8px; border: 1px solid #cbd5f5; }
|
|
641
1085
|
.editability-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 16px; }
|
|
642
1086
|
.editability-error { color: #dc2626; }
|
|
643
|
-
.editability-
|
|
644
|
-
.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; }
|
|
645
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
|
+
}
|
|
646
1097
|
`;
|
|
647
1098
|
document.head.appendChild(style);
|
|
648
1099
|
}
|
|
@@ -661,4 +1112,40 @@ function loadGoogleScript() {
|
|
|
661
1112
|
document.head.appendChild(script);
|
|
662
1113
|
});
|
|
663
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
|
+
}
|
|
664
1151
|
//# sourceMappingURL=overlay.js.map
|