@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/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, children: "Undo" }), _jsx("button", { className: "editability-btn", onClick: () => activeEditor?.redo(), disabled: !activeEditor, children: "Redo" }), _jsx("button", { className: "editability-btn", onClick: () => activeEditor?.setBold(), disabled: !activeEditor, children: "Bold" }), _jsx("button", { className: "editability-btn", onClick: () => activeEditor?.setItalic(), disabled: !activeEditor, children: "Italic" }), _jsx("button", { className: "editability-btn", onClick: () => {
217
- if (!activeEditor)
218
- return;
219
- activeEditor.captureSelection();
220
- const url = prompt("Link URL (leave blank to remove):");
221
- if (url === null)
222
- return;
223
- const normalized = url.trim();
224
- if (!normalized) {
225
- activeEditor.setLink(null);
226
- return;
227
- }
228
- const sanitized = sanitizeHref(normalized);
229
- if (!sanitized) {
230
- alert("Invalid link.");
231
- return;
232
- }
233
- activeEditor.setLink(sanitized.href);
234
- }, disabled: !activeEditor, children: "Link" }), _jsx("button", { className: "editability-btn", onClick: handleDone, disabled: !activeEditor, children: "Done" })] }), _jsxs("div", { className: "editability-right", children: [_jsxs("button", { className: "editability-btn", onClick: () => setPublishOpen(true), disabled: changeCount === 0, children: ["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", onClick: handleExit, children: "Exit" })] })] }), publishOpen ? (_jsx(PublishModal, { changes: changes, message: message, setMessage: setMessage, onClose: () => setPublishOpen(false), onPublish: handlePublish, error: error })) : null] }));
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: htmlToAst(originalHtml, mode),
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 (normalizedOriginal === normalizedAfter) {
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 (["div", "section", "article", "main", "aside", "nav", "header", "footer"].includes(tag)) {
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((block) => `<p>${block.children.map(renderInlineNode).join("")}</p>`).join("");
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, "&amp;")
@@ -464,14 +747,23 @@ function escapeHtml(value) {
464
747
  .replace(/>/g, "&gt;")
465
748
  .replace(/\"/g, "&quot;");
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((block) => block.children.map(nodeToText).join(" ")).join(" ");
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
- const paragraphs = wrapper.querySelectorAll("p");
496
- if (paragraphs.length === 0) {
497
- blocks.push({ type: "paragraph", children: collectInline(wrapper) });
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
- const text = node.textContent ?? "";
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 afterHtml = renderAstToHtml(ast);
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 (normalizedOriginal === normalizedAfter) {
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
- .editability-authenticated [data-editability-id]:hover { outline: 2px dashed #2f855a; outline-offset: 2px; cursor: text; }
618
- .editability-authenticated [data-editability-id].editability-dirty { outline: 2px solid #3182ce; outline-offset: 2px; }
619
- .editability-overlay { position: fixed; inset: auto 0 0 0; z-index: 9999; font-family: ui-sans-serif, system-ui; }
620
- .editability-bar { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 12px 16px; background: #0f172a; color: #f8fafc; box-shadow: 0 -6px 20px rgba(15, 23, 42, 0.25); }
621
- .editability-left, .editability-center, .editability-right { display: flex; align-items: center; gap: 8px; }
622
- .editability-pill { padding: 4px 10px; background: #38bdf8; color: #0f172a; border-radius: 999px; font-weight: 600; }
623
- .editability-status { opacity: 0.8; }
624
- .editability-btn { padding: 6px 10px; border-radius: 6px; border: 1px solid rgba(148, 163, 184, 0.3); background: #1e293b; color: #f8fafc; font-size: 13px; }
625
- .editability-btn:disabled { opacity: 0.5; cursor: not-allowed; }
626
- .editability-btn:hover:not(:disabled) { background: #334155; }
627
- .editability-primary { background: #38bdf8; color: #0f172a; border-color: transparent; }
628
- .editability-user { display: flex; align-items: center; gap: 8px; }
629
- .editability-user img { width: 24px; height: 24px; border-radius: 999px; }
630
- .editability-modal { position: fixed; inset: 0; background: rgba(15, 23, 42, 0.6); display: flex; align-items: center; justify-content: center; z-index: 10000; }
631
- .editability-modal-content { background: #ffffff; color: #0f172a; padding: 24px; border-radius: 16px; width: min(900px, 90vw); max-height: 80vh; overflow: auto; }
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: 8px; padding: 12px; min-height: 40px; }
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: 6px; border: 1px solid #cbd5f5; }
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-login { position: fixed; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px; background: rgba(15, 23, 42, 0.8); color: #f8fafc; padding: 24px; text-align: center; }
644
- .editability-login h2 { margin: 0; font-size: 20px; }
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