smartrte-react 0.3.5 → 1.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +106 -14
  3. package/dist/adapters/domChecklistCommandBridge.js +1 -1
  4. package/dist/adapters/domCommandBridge.d.ts +2 -2
  5. package/dist/adapters/domCommandBridge.js +2 -2
  6. package/dist/adapters/domInlineAtomCommandBridge.d.ts +10 -9
  7. package/dist/adapters/domInlineAtomCommandBridge.js +154 -50
  8. package/dist/adapters/domInlineImageCommandBridge.d.ts +7 -7
  9. package/dist/adapters/domInlineImageCommandBridge.js +6 -72
  10. package/dist/adapters/domMoveCommandBridge.js +1 -1
  11. package/dist/adapters/domSelectionBridge.d.ts +3 -3
  12. package/dist/adapters/domSmartDocument.d.ts +4 -4
  13. package/dist/adapters/domSmartDocument.js +2 -2
  14. package/dist/adapters/domTableCommandBridge.d.ts +42 -16
  15. package/dist/adapters/domTableCommandBridge.js +203 -173
  16. package/dist/adapters/internalFlags.d.ts +0 -8
  17. package/dist/adapters/internalFlags.js +0 -24
  18. package/dist/adapters/legacyListShadowComparator.d.ts +1 -1
  19. package/dist/adapters/legacyListShadowComparator.js +1 -1
  20. package/dist/adapters/pdfPrint.d.ts +8 -0
  21. package/dist/adapters/pdfPrint.js +22 -0
  22. package/dist/adapters/shadowMode.d.ts +3 -3
  23. package/dist/adapters/shadowMode.js +1 -1
  24. package/dist/canonicalEditorRuntime.d.ts +53 -1
  25. package/dist/canonicalEditorRuntime.js +61 -4
  26. package/dist/capabilityPresets.d.ts +27 -0
  27. package/dist/capabilityPresets.js +21 -0
  28. package/dist/collabTransport.d.ts +66 -0
  29. package/dist/collabTransport.js +1 -0
  30. package/dist/commentProvider.d.ts +24 -0
  31. package/dist/commentProvider.js +1 -0
  32. package/dist/components/CanonicalAuthorityEditor.d.ts +35 -1
  33. package/dist/components/CanonicalAuthorityEditor.js +1086 -112
  34. package/dist/components/ClassicEditorAuthority.d.ts +93 -22
  35. package/dist/components/ClassicEditorAuthority.js +11 -43
  36. package/dist/components/ColorPickerPopover.d.ts +39 -0
  37. package/dist/components/ColorPickerPopover.js +285 -0
  38. package/dist/components/CommentMarkers.d.ts +25 -0
  39. package/dist/components/CommentMarkers.js +105 -0
  40. package/dist/components/CommentThreadPanel.d.ts +26 -0
  41. package/dist/components/CommentThreadPanel.js +47 -0
  42. package/dist/components/ContextMenu.d.ts +23 -0
  43. package/dist/components/ContextMenu.js +141 -0
  44. package/dist/components/FormulaLibraryPopover.d.ts +16 -0
  45. package/dist/components/FormulaLibraryPopover.js +107 -0
  46. package/dist/components/LinkEditorPopover.d.ts +3 -1
  47. package/dist/components/LinkEditorPopover.js +22 -6
  48. package/dist/components/MediaManager.d.ts +4 -20
  49. package/dist/components/MediaManager.js +35 -6
  50. package/dist/components/MediaOverlay.d.ts +36 -0
  51. package/dist/components/MediaOverlay.js +163 -0
  52. package/dist/components/SpecialCharacterPopover.d.ts +18 -0
  53. package/dist/components/SpecialCharacterPopover.js +99 -0
  54. package/dist/components/StructuralSuggestionMarkers.d.ts +20 -0
  55. package/dist/components/StructuralSuggestionMarkers.js +56 -0
  56. package/dist/components/SuggestionPanel.d.ts +26 -0
  57. package/dist/components/SuggestionPanel.js +33 -0
  58. package/dist/components/TableBorderPopover.d.ts +59 -0
  59. package/dist/components/TableBorderPopover.js +147 -0
  60. package/dist/components/TableResizeHandles.d.ts +25 -0
  61. package/dist/components/TableResizeHandles.js +301 -0
  62. package/dist/components/TableSizePickerPopover.d.ts +16 -0
  63. package/dist/components/TableSizePickerPopover.js +126 -0
  64. package/dist/components/ToolbarPrimitives.d.ts +129 -0
  65. package/dist/components/ToolbarPrimitives.js +146 -0
  66. package/dist/components/VersionHistoryPanel.d.ts +9 -0
  67. package/dist/components/VersionHistoryPanel.js +157 -0
  68. package/dist/formulaLibrary.d.ts +31 -0
  69. package/dist/formulaLibrary.js +90 -0
  70. package/dist/index.d.ts +22 -24
  71. package/dist/index.js +14 -11
  72. package/dist/mediaManagerAdapter.d.ts +12 -0
  73. package/dist/mediaManagerAdapter.js +34 -0
  74. package/dist/mediaProvider.d.ts +21 -1
  75. package/dist/specialCharacters.d.ts +22 -0
  76. package/dist/specialCharacters.js +86 -0
  77. package/dist/standalone/classic-editor-embed.js +19 -12
  78. package/dist/suggestionProvider.d.ts +17 -0
  79. package/dist/suggestionProvider.js +1 -0
  80. package/dist/theme.d.ts +1 -1
  81. package/dist/theme.js +133 -1
  82. package/dist/versionProvider.d.ts +29 -0
  83. package/dist/versionProvider.js +1 -0
  84. package/package.json +28 -14
  85. package/dist/adapters/canonicalInlineCommandBridge.d.ts +0 -15
  86. package/dist/adapters/canonicalInlineCommandBridge.js +0 -269
  87. package/dist/adapters/canonicalListCommandBridge.d.ts +0 -26
  88. package/dist/adapters/canonicalListCommandBridge.js +0 -430
  89. package/dist/adapters/coreBold.d.ts +0 -5
  90. package/dist/adapters/coreBold.js +0 -3
  91. package/dist/adapters/documentFormats.d.ts +0 -26
  92. package/dist/adapters/documentFormats.js +0 -49
  93. package/dist/adapters/docxFormat.d.ts +0 -5
  94. package/dist/adapters/docxFormat.js +0 -272
  95. package/dist/adapters/domBlockCommandBridge.d.ts +0 -19
  96. package/dist/adapters/domBlockCommandBridge.js +0 -79
  97. package/dist/adapters/inlineMarkCoreExecution.d.ts +0 -9
  98. package/dist/adapters/inlineMarkCoreExecution.js +0 -25
  99. package/dist/adapters/pdfFormat.d.ts +0 -23
  100. package/dist/adapters/pdfFormat.js +0 -204
  101. package/dist/adapters/portableDocxAtoms.d.ts +0 -4
  102. package/dist/adapters/portableDocxAtoms.js +0 -58
  103. package/dist/adapters/styledDocxFormat.d.ts +0 -8
  104. package/dist/adapters/styledDocxFormat.js +0 -190
  105. package/dist/builtInFormatDefinitions.d.ts +0 -5
  106. package/dist/builtInFormatDefinitions.js +0 -82
  107. package/dist/canonicalAuthorityFlag.d.ts +0 -32
  108. package/dist/canonicalAuthorityFlag.js +0 -49
  109. package/dist/canonicalClipboardRuntime.d.ts +0 -15
  110. package/dist/canonicalClipboardRuntime.js +0 -76
  111. package/dist/components/ClassicEditor.d.ts +0 -76
  112. package/dist/components/ClassicEditor.js +0 -6729
  113. package/dist/editorController.d.ts +0 -78
  114. package/dist/editorController.js +0 -298
  115. package/dist/formatFidelity.d.ts +0 -14
  116. package/dist/formatFidelity.js +0 -107
  117. package/dist/formatRuntime.d.ts +0 -50
  118. package/dist/formatRuntime.js +0 -20
  119. package/dist/pluginRuntime.d.ts +0 -68
  120. package/dist/pluginRuntime.js +0 -78
  121. package/dist/standalone/editor.js +0 -1068
  122. package/dist/test-harness/atomShadowComparator.d.ts +0 -14
  123. package/dist/test-harness/atomShadowComparator.js +0 -63
  124. package/dist/test-harness/blockShadowComparator.d.ts +0 -30
  125. package/dist/test-harness/blockShadowComparator.js +0 -140
  126. package/dist/test-harness/clipboardShadowComparator.d.ts +0 -12
  127. package/dist/test-harness/clipboardShadowComparator.js +0 -46
  128. package/dist/test-harness/inlineShadowComparator.d.ts +0 -32
  129. package/dist/test-harness/inlineShadowComparator.js +0 -152
  130. package/dist/test-harness/legacyAtomEngine.d.ts +0 -10
  131. package/dist/test-harness/legacyAtomEngine.js +0 -15
  132. package/dist/test-harness/legacyBlockEngine.d.ts +0 -31
  133. package/dist/test-harness/legacyBlockEngine.js +0 -31
  134. package/dist/test-harness/legacyClipboardEngine.d.ts +0 -14
  135. package/dist/test-harness/legacyClipboardEngine.js +0 -67
  136. package/dist/test-harness/legacyInlineEngine.d.ts +0 -19
  137. package/dist/test-harness/legacyInlineEngine.js +0 -49
  138. package/dist/test-harness/legacyTableEngine.d.ts +0 -12
  139. package/dist/test-harness/legacyTableEngine.js +0 -25
  140. package/dist/test-harness/tableShadowComparator.d.ts +0 -29
  141. package/dist/test-harness/tableShadowComparator.js +0 -101
@@ -0,0 +1,26 @@
1
+ import type { AnnotationRange, CommentThread } from "smartrte-core/foundation";
2
+ export interface CommentThreadPanelProps {
3
+ open: boolean;
4
+ onClose: () => void;
5
+ threads: readonly CommentThread[];
6
+ activeThreadId: string | null;
7
+ onSelectThread: (id: string) => void;
8
+ /** Set when "Add comment" was just clicked against a valid, non-collapsed selection - renders a compose box for a brand-new thread. */
9
+ pendingRange: AnnotationRange | null;
10
+ onCreateThread: (text: string) => void;
11
+ onReply: (threadId: string, text: string) => void;
12
+ onResolve: (threadId: string, resolved: boolean) => void;
13
+ onDelete: (threadId: string) => void;
14
+ busyThreadId: string | null;
15
+ }
16
+ /**
17
+ * Docked side panel, not a blocking modal like `VersionHistoryPanel`/
18
+ * `MediaManager` - a comment thread is meant to be read and replied to
19
+ * while continuing to edit nearby, not a full-attention dialog. Threads
20
+ * whose range no longer resolves aren't distinguished here with a special
21
+ * flag (per `CommentThread`'s own doc comment, "orphaned" is derived, not
22
+ * stored) - a host wanting to visually flag them can cross-reference
23
+ * `CommentMarkers`' own resolution, which already only renders markers for
24
+ * threads that still resolve.
25
+ */
26
+ export declare function CommentThreadPanel(props: CommentThreadPanelProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,47 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from "react";
3
+ const formatTimestamp = (createdAt) => new Date(createdAt).toLocaleString();
4
+ /**
5
+ * Docked side panel, not a blocking modal like `VersionHistoryPanel`/
6
+ * `MediaManager` - a comment thread is meant to be read and replied to
7
+ * while continuing to edit nearby, not a full-attention dialog. Threads
8
+ * whose range no longer resolves aren't distinguished here with a special
9
+ * flag (per `CommentThread`'s own doc comment, "orphaned" is derived, not
10
+ * stored) - a host wanting to visually flag them can cross-reference
11
+ * `CommentMarkers`' own resolution, which already only renders markers for
12
+ * threads that still resolve.
13
+ */
14
+ export function CommentThreadPanel(props) {
15
+ const { open, onClose, threads, activeThreadId, onSelectThread, pendingRange, onCreateThread, onReply, onResolve, onDelete, busyThreadId } = props;
16
+ const [showResolved, setShowResolved] = useState(false);
17
+ const [draft, setDraft] = useState("");
18
+ const [replyDrafts, setReplyDrafts] = useState({});
19
+ if (!open)
20
+ return null;
21
+ const visible = threads.filter((thread) => showResolved || !thread.resolved);
22
+ const submitCompose = () => {
23
+ const text = draft.trim();
24
+ if (!text)
25
+ return;
26
+ onCreateThread(text);
27
+ setDraft("");
28
+ };
29
+ const submitReply = (threadId) => {
30
+ const text = (replyDrafts[threadId] || "").trim();
31
+ if (!text)
32
+ return;
33
+ onReply(threadId, text);
34
+ setReplyDrafts((current) => ({ ...current, [threadId]: "" }));
35
+ };
36
+ return (_jsxs("div", { role: "complementary", "aria-label": "Comments", "data-srte-comment-panel": "true", style: {
37
+ position: "fixed", top: 0, right: 0, bottom: 0, width: 320, maxWidth: "90vw",
38
+ background: "var(--srte-modal-bg)", color: "var(--srte-modal-text)",
39
+ borderLeft: "1px solid var(--srte-border-light)", boxShadow: "var(--srte-menu-shadow)",
40
+ display: "flex", flexDirection: "column", zIndex: 70,
41
+ }, children: [_jsxs("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between", padding: "10px 14px", borderBottom: "1px solid var(--srte-border-light)" }, children: [_jsx("div", { style: { fontWeight: 600 }, children: "Comments" }), _jsx("button", { type: "button", "aria-label": "Close comments", onClick: onClose, children: "\u2715" })] }), _jsxs("label", { style: { display: "flex", alignItems: "center", gap: 6, padding: "8px 14px", fontSize: 13, color: "var(--srte-text-muted)", borderBottom: "1px solid var(--srte-border-light)" }, children: [_jsx("input", { type: "checkbox", checked: showResolved, onChange: (event) => setShowResolved(event.target.checked) }), "Show resolved"] }), pendingRange && (_jsxs("div", { "data-srte-comment-compose": "true", style: { padding: "10px 14px", borderBottom: "1px solid var(--srte-border-light)" }, children: [_jsx("textarea", { value: draft, onChange: (event) => setDraft(event.target.value), placeholder: "Add a comment\u2026", rows: 3, style: { width: "100%", padding: "6px 8px", border: "1px solid var(--srte-border)", borderRadius: 6, background: "var(--srte-input-bg)", color: "var(--srte-input-text)", resize: "vertical" } }), _jsx("div", { style: { display: "flex", justifyContent: "flex-end", gap: 8, marginTop: 6 }, children: _jsx("button", { type: "button", "data-srte-comment-compose-submit": "true", disabled: !draft.trim(), onClick: submitCompose, children: "Comment" }) })] })), _jsxs("div", { style: { flex: 1, overflowY: "auto", padding: "0 14px 14px" }, children: [visible.length === 0 && _jsx("div", { style: { color: "var(--srte-text-muted)", padding: "12px 0" }, children: "No comments yet." }), visible.map((thread) => (_jsxs("div", { "data-srte-comment-thread": thread.id, onClick: () => onSelectThread(thread.id), style: {
42
+ padding: "10px 0", borderBottom: "1px solid var(--srte-border-light)",
43
+ opacity: thread.resolved ? 0.6 : 1,
44
+ outline: activeThreadId === thread.id ? "2px solid var(--srte-primary)" : "none",
45
+ }, children: [thread.replies.map((reply) => (_jsxs("div", { style: { marginBottom: 6 }, children: [_jsx("div", { style: { fontSize: 12, fontWeight: 600 }, children: reply.authorId }), _jsx("div", { style: { fontSize: 13, whiteSpace: "pre-wrap" }, children: reply.text }), _jsx("div", { style: { fontSize: 11, color: "var(--srte-text-muted)" }, children: formatTimestamp(reply.createdAt) })] }, reply.id))), _jsxs("div", { style: { display: "flex", gap: 8, marginTop: 6 }, children: [_jsx("button", { type: "button", "data-srte-comment-resolve": thread.id, disabled: busyThreadId === thread.id, onClick: (event) => { event.stopPropagation(); onResolve(thread.id, !thread.resolved); }, children: thread.resolved ? "Reopen" : "Resolve" }), _jsx("button", { type: "button", "data-srte-comment-delete": thread.id, disabled: busyThreadId === thread.id, onClick: (event) => { event.stopPropagation(); onDelete(thread.id); }, style: { color: "var(--srte-danger)" }, children: "Delete" })] }), !thread.resolved && (_jsxs("div", { style: { display: "flex", gap: 6, marginTop: 8 }, onClick: (event) => event.stopPropagation(), children: [_jsx("input", { value: replyDrafts[thread.id] || "", onChange: (event) => setReplyDrafts((current) => ({ ...current, [thread.id]: event.target.value })), onKeyDown: (event) => { if (event.key === "Enter")
46
+ submitReply(thread.id); }, placeholder: "Reply\u2026", style: { flex: 1, padding: "5px 7px", border: "1px solid var(--srte-border)", borderRadius: 6, background: "var(--srte-input-bg)", color: "var(--srte-input-text)" } }), _jsx("button", { type: "button", "data-srte-comment-reply-submit": thread.id, disabled: !(replyDrafts[thread.id] || "").trim(), onClick: () => submitReply(thread.id), children: "Reply" })] }))] }, thread.id)))] })] }));
47
+ }
@@ -0,0 +1,23 @@
1
+ export interface ContextMenuItem {
2
+ id: string;
3
+ label: string;
4
+ onSelect: () => void;
5
+ danger?: boolean;
6
+ }
7
+ export interface ContextMenuProps {
8
+ x: number;
9
+ y: number;
10
+ items: readonly ContextMenuItem[];
11
+ onDismiss: () => void;
12
+ }
13
+ /**
14
+ * Phase 11.5 §2.3: a generic right-click menu, position-at-cursor and
15
+ * keyboard-dismissible following LinkEditorPopover/ColorPickerPopover's
16
+ * pattern - plus click-outside-to-close, which those two don't have but a
17
+ * native-context-menu-replacement genuinely needs (an accidental click
18
+ * elsewhere shouldn't leave it stuck open). Items are supplied by the
19
+ * caller (CanonicalAuthorityEditor's resolveContextMenuItems), which
20
+ * resolves the applicable plugin registry `contextMenu` contributions for
21
+ * the current scope - this component has no knowledge of commands/scopes.
22
+ */
23
+ export declare function ContextMenu({ x, y, items, onDismiss }: ContextMenuProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,141 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useLayoutEffect, useRef, useState } from "react";
3
+ // A fixed cap, not just "whatever fits below the click point" - the menu's
4
+ // item count has grown considerably (marks, table, media, cell colour,
5
+ // link actions can all stack for one click), and a height that changes
6
+ // with click position made the scroll behavior feel inconsistent. Still
7
+ // clamped against the real available viewport space too (see placement
8
+ // below), so it never overflows a genuinely short viewport.
9
+ const MAX_MENU_HEIGHT = 400;
10
+ const itemStyle = (danger) => ({
11
+ display: "block",
12
+ width: "100%",
13
+ textAlign: "left",
14
+ padding: "8px 10px",
15
+ border: 0,
16
+ borderRadius: 6,
17
+ background: "transparent",
18
+ color: danger ? "var(--srte-danger)" : "var(--srte-menu-text)",
19
+ cursor: "pointer",
20
+ font: "inherit",
21
+ fontSize: 13,
22
+ fontWeight: 500,
23
+ });
24
+ /**
25
+ * Phase 11.5 §2.3: a generic right-click menu, position-at-cursor and
26
+ * keyboard-dismissible following LinkEditorPopover/ColorPickerPopover's
27
+ * pattern - plus click-outside-to-close, which those two don't have but a
28
+ * native-context-menu-replacement genuinely needs (an accidental click
29
+ * elsewhere shouldn't leave it stuck open). Items are supplied by the
30
+ * caller (CanonicalAuthorityEditor's resolveContextMenuItems), which
31
+ * resolves the applicable plugin registry `contextMenu` contributions for
32
+ * the current scope - this component has no knowledge of commands/scopes.
33
+ */
34
+ export function ContextMenu({ x, y, items, onDismiss }) {
35
+ const rootRef = useRef(null);
36
+ // Null until measured: the menu's real width/height depends on its
37
+ // content (item label lengths), not a fixed guess, so the final position
38
+ // - including whether to flip left/up to stay in the viewport - can only
39
+ // be computed after a first, invisible render exposes the real
40
+ // getBoundingClientRect(). Fixes both the viewport-overflow bug (the
41
+ // previous clamp math assumed a hardcoded 220px width that real labels
42
+ // like "Insert column right" can exceed) and gives scroll a real
43
+ // available-height number to clamp against.
44
+ const [placement, setPlacement] = useState(null);
45
+ // CanonicalAuthorityEditor passes a fresh onDismiss closure on every
46
+ // render (it re-renders on every editor subscribe tick, i.e. on every
47
+ // document/selection change - possible even while this menu is open, if
48
+ // the triggering right-click itself moved the caret). Reading through a
49
+ // ref - rather than depending on `onDismiss` in the effect below - keeps
50
+ // the outside-click listener mounted exactly once for this menu's
51
+ // lifetime, so a re-render can't create a window where the listener has
52
+ // been torn down and not yet reattached.
53
+ const onDismissRef = useRef(onDismiss);
54
+ onDismissRef.current = onDismiss;
55
+ useLayoutEffect(() => {
56
+ const el = rootRef.current;
57
+ if (!el)
58
+ return;
59
+ const margin = 8;
60
+ const { width, height } = el.getBoundingClientRect();
61
+ const viewportWidth = window.innerWidth;
62
+ const viewportHeight = window.innerHeight;
63
+ // Open leftward (menu's right edge at the click point) instead of the
64
+ // default rightward open when the default would overflow the right
65
+ // edge - never silently overflow.
66
+ const overflowsRight = x + width > viewportWidth - margin;
67
+ const left = overflowsRight ? x - width : x;
68
+ const clampedLeft = Math.min(Math.max(margin, left), Math.max(margin, viewportWidth - width - margin));
69
+ // Same flip for the bottom edge.
70
+ const overflowsBottom = y + height > viewportHeight - margin;
71
+ const top = overflowsBottom ? y - height : y;
72
+ const clampedTop = Math.min(Math.max(margin, top), Math.max(margin, viewportHeight - margin));
73
+ // The smaller of the fixed cap and whatever space actually remains
74
+ // below wherever the menu opens - a menu taller than that scrolls
75
+ // internally instead of extending past the viewport OR growing
76
+ // without bound on a tall screen.
77
+ const maxHeight = Math.min(MAX_MENU_HEIGHT, viewportHeight - clampedTop - margin);
78
+ setPlacement({ left: clampedLeft, top: clampedTop, maxHeight });
79
+ }, [x, y, items.length]);
80
+ useEffect(() => {
81
+ if (!placement)
82
+ return;
83
+ const buttons = rootRef.current?.querySelectorAll("[role=menuitem]");
84
+ buttons?.[0]?.focus();
85
+ }, [placement]);
86
+ useEffect(() => {
87
+ const dismissIfOutside = (event) => {
88
+ if (rootRef.current && !rootRef.current.contains(event.target))
89
+ onDismissRef.current();
90
+ };
91
+ // Both pointerdown and mousedown: pointerdown is the primary signal,
92
+ // but not every input path is guaranteed to dispatch PointerEvents in
93
+ // every embedding context - mousedown is the same "was this outside"
94
+ // check as a defense-in-depth fallback, not a second independent path.
95
+ window.addEventListener("pointerdown", dismissIfOutside, true);
96
+ window.addEventListener("mousedown", dismissIfOutside, true);
97
+ return () => {
98
+ window.removeEventListener("pointerdown", dismissIfOutside, true);
99
+ window.removeEventListener("mousedown", dismissIfOutside, true);
100
+ };
101
+ }, []);
102
+ return (_jsxs("div", { ref: rootRef, "data-srte-context-menu": "true", role: "menu", "aria-label": "Editor actions", style: {
103
+ position: "fixed",
104
+ left: placement?.left ?? x,
105
+ top: placement?.top ?? y,
106
+ // Hidden (not unrendered) until the real size is measured and a
107
+ // final, in-viewport position is computed - unrendered would mean
108
+ // getBoundingClientRect() has nothing to measure; visible-but-
109
+ // unpositioned would flash at the wrong spot for a frame first.
110
+ visibility: placement ? "visible" : "hidden",
111
+ zIndex: 80,
112
+ minWidth: 200,
113
+ maxWidth: "calc(100vw - 16px)",
114
+ maxHeight: placement ? placement.maxHeight : "calc(100vh - 16px)",
115
+ overflowY: "auto",
116
+ boxSizing: "border-box",
117
+ background: "var(--srte-menu-bg)",
118
+ color: "var(--srte-menu-text)",
119
+ border: "1px solid var(--srte-border)",
120
+ borderRadius: 10,
121
+ boxShadow: "var(--srte-menu-shadow)",
122
+ padding: 6,
123
+ }, onKeyDown: (event) => {
124
+ if (event.key === "Escape") {
125
+ event.preventDefault();
126
+ onDismiss();
127
+ return;
128
+ }
129
+ if (event.key !== "ArrowDown" && event.key !== "ArrowUp")
130
+ return;
131
+ event.preventDefault();
132
+ const buttons = Array.from(rootRef.current?.querySelectorAll("[role=menuitem]") || []);
133
+ if (!buttons.length)
134
+ return;
135
+ const currentIndex = buttons.indexOf(document.activeElement);
136
+ const nextIndex = event.key === "ArrowDown"
137
+ ? (currentIndex + 1) % buttons.length
138
+ : (currentIndex - 1 + buttons.length) % buttons.length;
139
+ buttons[nextIndex]?.focus();
140
+ }, children: [items.length === 0 && (_jsx("div", { style: { padding: "8px 10px", fontSize: 13, color: "var(--srte-menu-text)", opacity: 0.6 }, children: "No actions here" })), items.map((item) => (_jsx("button", { type: "button", role: "menuitem", "data-srte-context-menu-item": item.id, onClick: () => { item.onSelect(); onDismiss(); }, style: itemStyle(item.danger), children: item.label }, item.id)))] }));
141
+ }
@@ -0,0 +1,16 @@
1
+ import "katex/contrib/mhchem";
2
+ import { type FormulaLibraryEntry } from "../formulaLibrary.js";
3
+ export interface FormulaLibraryPopoverProps {
4
+ x: number;
5
+ y: number;
6
+ onInsert: (entry: FormulaLibraryEntry) => void;
7
+ onCancel: () => void;
8
+ }
9
+ /**
10
+ * Formula library popover ("Small" scope, 55 entries across 6 domains -
11
+ * see formulaLibrary.ts's own doc comment). Browsable by domain tab and
12
+ * searchable by name; each entry renders a real KaTeX preview (the same
13
+ * renderer the document itself uses) so the user sees exactly what they're
14
+ * about to insert, not a name they have to guess the shape of.
15
+ */
16
+ export declare function FormulaLibraryPopover({ x, y, onInsert, onCancel }: FormulaLibraryPopoverProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,107 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
3
+ import katex from "katex";
4
+ // Registers \ce{...} for this popover's own chemistry-entry previews - see
5
+ // the matching import's comment in core's surface/renderer.ts for why this
6
+ // is needed once per katex module instance.
7
+ import "katex/contrib/mhchem";
8
+ import { FORMULA_DOMAINS, FORMULA_LIBRARY } from "../formulaLibrary.js";
9
+ const renderPreview = (latex) => {
10
+ try {
11
+ return katex.renderToString(latex, { trust: false, strict: "error", throwOnError: true });
12
+ }
13
+ catch {
14
+ return latex;
15
+ }
16
+ };
17
+ /**
18
+ * Formula library popover ("Small" scope, 55 entries across 6 domains -
19
+ * see formulaLibrary.ts's own doc comment). Browsable by domain tab and
20
+ * searchable by name; each entry renders a real KaTeX preview (the same
21
+ * renderer the document itself uses) so the user sees exactly what they're
22
+ * about to insert, not a name they have to guess the shape of.
23
+ */
24
+ export function FormulaLibraryPopover({ x, y, onInsert, onCancel }) {
25
+ const [domain, setDomain] = useState("all");
26
+ const [query, setQuery] = useState("");
27
+ const rootRef = useRef(null);
28
+ const [placement, setPlacement] = useState(null);
29
+ const onCancelRef = useRef(onCancel);
30
+ onCancelRef.current = onCancel;
31
+ useLayoutEffect(() => {
32
+ const el = rootRef.current;
33
+ if (!el)
34
+ return;
35
+ const margin = 8;
36
+ const { width, height } = el.getBoundingClientRect();
37
+ const viewportWidth = window.innerWidth;
38
+ const viewportHeight = window.innerHeight;
39
+ const overflowsRight = x + width > viewportWidth - margin;
40
+ const left = overflowsRight ? x - width : x;
41
+ const clampedLeft = Math.min(Math.max(margin, left), Math.max(margin, viewportWidth - width - margin));
42
+ const overflowsBottom = y + height > viewportHeight - margin;
43
+ const top = overflowsBottom ? y - height : y;
44
+ const clampedTop = Math.min(Math.max(margin, top), Math.max(margin, viewportHeight - margin));
45
+ setPlacement({ left: clampedLeft, top: clampedTop, maxHeight: viewportHeight - clampedTop - margin });
46
+ }, [x, y]);
47
+ useEffect(() => {
48
+ const dismissIfOutside = (event) => {
49
+ if (rootRef.current && !rootRef.current.contains(event.target))
50
+ onCancelRef.current();
51
+ };
52
+ window.addEventListener("pointerdown", dismissIfOutside, true);
53
+ window.addEventListener("mousedown", dismissIfOutside, true);
54
+ return () => {
55
+ window.removeEventListener("pointerdown", dismissIfOutside, true);
56
+ window.removeEventListener("mousedown", dismissIfOutside, true);
57
+ };
58
+ }, []);
59
+ useEffect(() => {
60
+ const onWindowKeyDown = (event) => {
61
+ if (event.key === "Escape")
62
+ onCancelRef.current();
63
+ };
64
+ window.addEventListener("keydown", onWindowKeyDown);
65
+ return () => window.removeEventListener("keydown", onWindowKeyDown);
66
+ }, []);
67
+ const filtered = useMemo(() => {
68
+ const needle = query.trim().toLowerCase();
69
+ return FORMULA_LIBRARY.filter((entry) => (domain === "all" || entry.domain === domain) && (!needle || entry.name.toLowerCase().includes(needle)));
70
+ }, [domain, query]);
71
+ return (_jsxs("div", { ref: rootRef, "data-srte-formula-library": "true", role: "dialog", "aria-label": "Formula library", style: {
72
+ position: "fixed",
73
+ left: placement?.left ?? x,
74
+ top: placement?.top ?? y,
75
+ visibility: placement ? "visible" : "hidden",
76
+ zIndex: 70,
77
+ width: 420,
78
+ maxWidth: "calc(100vw - 16px)",
79
+ maxHeight: placement ? placement.maxHeight : "calc(100vh - 16px)",
80
+ display: "flex",
81
+ flexDirection: "column",
82
+ boxSizing: "border-box",
83
+ background: "var(--srte-menu-bg)",
84
+ color: "var(--srte-menu-text)",
85
+ border: "1px solid var(--srte-border)",
86
+ borderRadius: 12,
87
+ boxShadow: "var(--srte-menu-shadow)",
88
+ overflow: "hidden",
89
+ }, children: [_jsxs("div", { style: { padding: "12px 14px 8px", borderBottom: "1px solid var(--srte-border)" }, children: [_jsx("input", { "data-srte-formula-search": "true", value: query, onChange: (event) => setQuery(event.target.value), placeholder: "Search formulas\u2026 (e.g. \u201Cquadratic\u201D)", "aria-label": "Search formulas", autoComplete: "off", style: {
90
+ width: "100%", height: 34, boxSizing: "border-box", padding: "0 10px",
91
+ border: "1px solid var(--srte-input-border)", borderRadius: 8, outline: "none",
92
+ background: "var(--srte-input-bg)", color: "var(--srte-input-text)", font: "inherit", fontSize: 13.5,
93
+ } }), _jsxs("div", { role: "tablist", "aria-label": "Formula domain", style: { display: "flex", gap: 4, flexWrap: "wrap", marginTop: 10 }, children: [_jsx("button", { type: "button", role: "tab", "aria-selected": domain === "all", onMouseDown: (event) => event.preventDefault(), onClick: () => setDomain("all"), style: tabStyle(domain === "all"), children: "All" }), FORMULA_DOMAINS.map((entry) => (_jsx("button", { type: "button", role: "tab", "aria-selected": domain === entry.id, onMouseDown: (event) => event.preventDefault(), onClick: () => setDomain(entry.id), style: tabStyle(domain === entry.id), children: entry.label }, entry.id)))] })] }), _jsxs("div", { "data-srte-formula-grid": "true", style: {
94
+ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(120px, 1fr))", gap: 8,
95
+ padding: 12, overflowY: "auto",
96
+ }, children: [filtered.length === 0 && (_jsxs("div", { style: { gridColumn: "1 / -1", fontSize: 13, color: "var(--srte-menu-text)", opacity: 0.7, padding: "12px 4px" }, children: ["No formulas match \u201C", query, "\u201D."] })), filtered.map((entry) => (_jsxs("button", { type: "button", "data-srte-formula-entry": entry.id, "aria-label": `Insert ${entry.name}`, title: entry.name, onMouseDown: (event) => event.preventDefault(), onClick: () => onInsert(entry), style: {
97
+ display: "flex", flexDirection: "column", alignItems: "center", gap: 8,
98
+ border: "1px solid var(--srte-input-border)", borderRadius: 10, padding: "10px 8px",
99
+ background: "var(--srte-input-bg)", cursor: "pointer", minHeight: 76,
100
+ }, children: [_jsx("span", { "aria-hidden": "true", style: { fontSize: 14, lineHeight: 1.2, minHeight: 22, display: "flex", alignItems: "center", justifyContent: "center", overflow: "hidden", maxWidth: "100%" }, dangerouslySetInnerHTML: { __html: renderPreview(entry.latex) } }), _jsx("span", { style: { fontSize: 11, color: "var(--srte-menu-text)", opacity: 0.75, textAlign: "center" }, children: entry.name })] }, entry.id)))] })] }));
101
+ }
102
+ const tabStyle = (active) => ({
103
+ fontSize: 12, fontWeight: 600, padding: "5px 10px", borderRadius: 7, cursor: "pointer",
104
+ border: "1px solid " + (active ? "var(--srte-primary)" : "transparent"),
105
+ background: active ? "var(--srte-accent-bg)" : "transparent",
106
+ color: active ? "var(--srte-primary)" : "var(--srte-menu-text)",
107
+ });
@@ -12,9 +12,11 @@ export interface LinkEditorPopoverProps {
12
12
  showTextInput?: boolean;
13
13
  showOpen?: boolean;
14
14
  showRemove?: boolean;
15
+ /** Defaults to true (the original toolbar-Link-button behavior: the user asked to edit, so jump straight to the href field). Pass false when the popover opened itself just because the caret entered a link - autofocusing there would steal focus from the editor on every ordinary click/caret-move, trapping the cursor the overlay exists specifically to avoid. */
16
+ autoFocus?: boolean;
15
17
  onApply: (value: LinkEditorApplyValue) => void;
16
18
  onOpen?: () => void;
17
19
  onRemove?: () => void;
18
20
  onCancel: () => void;
19
21
  }
20
- export declare function LinkEditorPopover({ x, y, initialHref, initialText, initialOpenInNewTab, showTextInput, showOpen, showRemove, onApply, onOpen, onRemove, onCancel, }: LinkEditorPopoverProps): import("react/jsx-runtime").JSX.Element;
22
+ export declare function LinkEditorPopover({ x, y, initialHref, initialText, initialOpenInNewTab, showTextInput, showOpen, showRemove, autoFocus, onApply, onOpen, onRemove, onCancel, }: LinkEditorPopoverProps): import("react/jsx-runtime").JSX.Element;
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useId, useRef, useState } from "react";
3
- import { normalizeLinkInput } from "smartrte-core";
3
+ import { normalizeLinkInput } from "smartrte-core/legacy";
4
4
  const inputStyle = {
5
5
  width: "100%",
6
6
  height: 36,
@@ -24,7 +24,7 @@ const buttonStyle = {
24
24
  cursor: "pointer",
25
25
  fontWeight: 500,
26
26
  };
27
- export function LinkEditorPopover({ x, y, initialHref = "", initialText = "", initialOpenInNewTab = false, showTextInput = false, showOpen = false, showRemove = false, onApply, onOpen, onRemove, onCancel, }) {
27
+ export function LinkEditorPopover({ x, y, initialHref = "", initialText = "", initialOpenInNewTab = false, showTextInput = false, showOpen = false, showRemove = false, autoFocus = true, onApply, onOpen, onRemove, onCancel, }) {
28
28
  const [href, setHref] = useState(initialHref);
29
29
  const [text, setText] = useState(initialText);
30
30
  const [openInNewTab, setOpenInNewTab] = useState(initialOpenInNewTab);
@@ -33,8 +33,24 @@ export function LinkEditorPopover({ x, y, initialHref = "", initialText = "", in
33
33
  const titleId = useId();
34
34
  const errorId = useId();
35
35
  useEffect(() => {
36
+ if (!autoFocus)
37
+ return;
36
38
  hrefRef.current?.focus();
37
39
  hrefRef.current?.select();
40
+ // eslint-disable-next-line react-hooks/exhaustive-deps
41
+ }, []);
42
+ // Escape must dismiss regardless of where focus actually is - when
43
+ // autoFocus is false (the auto-triggered case), focus deliberately stays
44
+ // on the editor, so the div's own onKeyDown below (which only fires while
45
+ // focus is inside the popover) would never see the keypress.
46
+ useEffect(() => {
47
+ const onWindowKeyDown = (event) => {
48
+ if (event.key === "Escape")
49
+ onCancel();
50
+ };
51
+ window.addEventListener("keydown", onWindowKeyDown);
52
+ return () => window.removeEventListener("keydown", onWindowKeyDown);
53
+ // eslint-disable-next-line react-hooks/exhaustive-deps
38
54
  }, []);
39
55
  const apply = () => {
40
56
  const normalized = normalizeLinkInput(href);
@@ -69,10 +85,10 @@ export function LinkEditorPopover({ x, y, initialHref = "", initialText = "", in
69
85
  boxShadow: "var(--srte-menu-shadow)",
70
86
  padding: 14,
71
87
  }, onKeyDown: (event) => {
72
- if (event.key === "Escape") {
73
- event.preventDefault();
74
- onCancel();
75
- }
88
+ // Escape is handled by the window-level listener above (needs to
89
+ // work even when autoFocus=false and focus never entered the
90
+ // popover) - not duplicated here to avoid a double-dismiss when
91
+ // focus *is* inside.
76
92
  if (event.key === "Enter" && !event.shiftKey) {
77
93
  event.preventDefault();
78
94
  apply();
@@ -1,23 +1,5 @@
1
- export type MediaItem = {
2
- id: string;
3
- url: string;
4
- width?: number;
5
- height?: number;
6
- sizeBytes?: number;
7
- mimeType?: string;
8
- hashHex?: string;
9
- createdAt?: string;
10
- title?: string;
11
- alt?: string;
12
- tags?: string[];
13
- license?: {
14
- author?: string;
15
- licenseType?: string;
16
- licenseText?: string;
17
- sourceUrl?: string;
18
- workName?: string;
19
- };
20
- };
1
+ import type { MediaItem } from "../mediaProvider.js";
2
+ export type { MediaItem } from "../mediaProvider.js";
21
3
  export type MediaSearchQuery = {
22
4
  q?: string;
23
5
  tags?: string[];
@@ -29,6 +11,8 @@ export type MediaSearchQuery = {
29
11
  export type MediaManagerAdapter = {
30
12
  upload: (files: File[]) => Promise<MediaItem[]>;
31
13
  search: (query: MediaSearchQuery) => Promise<MediaItem[]>;
14
+ /** Optional - MediaProvider.remove exists but nothing here called it before Phase 11.5 §2.1. */
15
+ remove?: (id: string) => Promise<void>;
32
16
  };
33
17
  export declare function MediaManager(props: {
34
18
  open: boolean;
@@ -8,6 +8,7 @@ export function MediaManager(props) {
8
8
  const [query, setQuery] = useState("");
9
9
  const [results, setResults] = useState([]);
10
10
  const [infoItem, setInfoItem] = useState(null);
11
+ const [removing, setRemoving] = useState(false);
11
12
  const fileInputRef = useRef(null);
12
13
  useEffect(() => {
13
14
  if (!open)
@@ -31,6 +32,24 @@ export function MediaManager(props) {
31
32
  setError("Failed to search media.");
32
33
  }
33
34
  };
35
+ /** Phase 11.5 §2.1: MediaProvider.remove already existed; nothing here ever called it. */
36
+ const handleRemove = async (item) => {
37
+ if (!adapter.remove)
38
+ return;
39
+ setRemoving(true);
40
+ setError(null);
41
+ try {
42
+ await adapter.remove(item.id);
43
+ setResults((current) => current.filter((entry) => entry.id !== item.id));
44
+ setInfoItem(null);
45
+ }
46
+ catch (e) {
47
+ setError("Failed to delete media.");
48
+ }
49
+ finally {
50
+ setRemoving(false);
51
+ }
52
+ };
34
53
  const computeSha256Hex = async (file) => {
35
54
  const buf = await file.arrayBuffer();
36
55
  const digest = await crypto.subtle.digest("SHA-256", buf);
@@ -99,7 +118,7 @@ export function MediaManager(props) {
99
118
  alignItems: "center",
100
119
  justifyContent: "center",
101
120
  zIndex: 80,
102
- }, onClick: onClose, children: _jsxs("div", { style: {
121
+ }, onClick: onClose, children: _jsxs("div", { role: "dialog", "aria-label": "Media library", "data-srte-media-manager": "true", style: {
103
122
  background: "var(--srte-modal-bg)",
104
123
  color: "var(--srte-modal-text)",
105
124
  width: 820,
@@ -115,19 +134,19 @@ export function MediaManager(props) {
115
134
  justifyContent: "space-between",
116
135
  padding: "10px 14px",
117
136
  borderBottom: "1px solid var(--srte-border-light)",
118
- }, children: [_jsxs("div", { style: { display: "flex", gap: 8 }, children: [_jsx("button", { onClick: () => setActiveTab("upload"), style: {
137
+ }, children: [_jsxs("div", { style: { display: "flex", gap: 8 }, children: [_jsx("button", { type: "button", onClick: () => setActiveTab("upload"), style: {
119
138
  padding: "6px 10px",
120
139
  borderRadius: 6,
121
140
  border: "1px solid var(--srte-border)",
122
141
  background: activeTab === "upload" ? "var(--srte-surface-subtle)" : "var(--srte-input-bg)",
123
142
  color: "var(--srte-input-text)",
124
- }, children: "Upload" }), _jsx("button", { onClick: () => setActiveTab("library"), style: {
143
+ }, children: "Upload" }), _jsx("button", { type: "button", onClick: () => setActiveTab("library"), style: {
125
144
  padding: "6px 10px",
126
145
  borderRadius: 6,
127
146
  border: "1px solid var(--srte-border)",
128
147
  background: activeTab === "library" ? "var(--srte-surface-subtle)" : "var(--srte-input-bg)",
129
148
  color: "var(--srte-input-text)",
130
- }, children: "Library" })] }), _jsx("button", { onClick: onClose, children: "\u2715" })] }), error && (_jsx("div", { style: { color: "var(--srte-danger)", padding: "8px 14px" }, children: error })), activeTab === "upload" ? (_jsxs("div", { style: { padding: 16 }, children: [_jsx("input", { ref: fileInputRef, type: "file", accept: "image/*", multiple: true, style: { display: "none" }, onChange: (e) => handleUploadFiles(e.currentTarget.files) }), _jsx("div", { onClick: () => fileInputRef.current?.click(), onDragOver: (e) => {
149
+ }, children: "Library" })] }), _jsx("button", { type: "button", "aria-label": "Close media library", onClick: onClose, children: "\u2715" })] }), error && (_jsx("div", { style: { color: "var(--srte-danger)", padding: "8px 14px" }, children: error })), activeTab === "upload" ? (_jsxs("div", { style: { padding: 16 }, children: [_jsx("input", { ref: fileInputRef, type: "file", accept: "image/*", multiple: true, style: { display: "none" }, onChange: (e) => handleUploadFiles(e.currentTarget.files) }), _jsx("div", { onClick: () => fileInputRef.current?.click(), onDragOver: (e) => {
131
150
  e.preventDefault();
132
151
  }, onDrop: (e) => {
133
152
  e.preventDefault();
@@ -153,7 +172,7 @@ export function MediaManager(props) {
153
172
  borderRadius: 6,
154
173
  background: "var(--srte-input-bg)",
155
174
  color: "var(--srte-input-text)",
156
- } }), _jsx("button", { onClick: performSearch, children: "Search" })] }), _jsx("div", { style: {
175
+ } }), _jsx("button", { type: "button", onClick: performSearch, children: "Search" })] }), _jsx("div", { style: {
157
176
  display: "grid",
158
177
  gridTemplateColumns: "repeat(auto-fill, minmax(120px, 1fr))",
159
178
  gap: 12,
@@ -223,10 +242,20 @@ export function MediaManager(props) {
223
242
  ["MIME type", infoItem.mimeType],
224
243
  ["Size", infoItem.sizeBytes ? `${Math.round(infoItem.sizeBytes / 1024)} KB` : undefined],
225
244
  ["Created", infoItem.createdAt],
245
+ ["Used", infoItem.usageCount !== undefined ? `${infoItem.usageCount} time${infoItem.usageCount === 1 ? "" : "s"}` : undefined],
226
246
  ["Tags", infoItem.tags?.join(", ")],
227
247
  ["Work", infoItem.license?.workName],
228
248
  ["Author", infoItem.license?.author],
229
249
  ["License", [infoItem.license?.licenseType, infoItem.license?.licenseText].filter(Boolean).join(" - ")],
230
250
  ["Source", infoItem.license?.sourceUrl],
231
- ].filter(([, value]) => value).map(([label, value]) => (_jsxs("div", { style: { display: "grid", gridTemplateColumns: "92px 1fr", gap: 8, fontSize: 12, marginBottom: 6 }, children: [_jsx("div", { style: { color: "var(--srte-text-muted)" }, children: label }), _jsx("div", { style: { overflowWrap: "anywhere" }, children: value })] }, label)))] }) }))] }) }));
251
+ ].filter(([, value]) => value).map(([label, value]) => (_jsxs("div", { style: { display: "grid", gridTemplateColumns: "92px 1fr", gap: 8, fontSize: 12, marginBottom: 6 }, children: [_jsx("div", { style: { color: "var(--srte-text-muted)" }, children: label }), _jsx("div", { style: { overflowWrap: "anywhere" }, children: value })] }, label))), adapter.remove && (_jsx("button", { type: "button", "data-srte-media-delete": "true", disabled: removing, onClick: () => void handleRemove(infoItem), style: {
252
+ marginTop: 8,
253
+ padding: "6px 12px",
254
+ borderRadius: 6,
255
+ border: "1px solid var(--srte-danger)",
256
+ background: "transparent",
257
+ color: "var(--srte-danger)",
258
+ cursor: removing ? "default" : "pointer",
259
+ opacity: removing ? 0.6 : 1,
260
+ }, children: removing ? "Deleting…" : "Delete" }))] }) }))] }) }));
232
261
  }
@@ -0,0 +1,36 @@
1
+ export interface MediaOverlayProps {
2
+ atomElement: HTMLElement;
3
+ alt: string;
4
+ width?: number;
5
+ height?: number;
6
+ src?: string;
7
+ /** False for atoms with no width/height concept (e.g. formula, sized by KaTeX from source/font-size) - hides the Resize +/- buttons and drag handle, keeping Edit/Delete. */
8
+ resizable?: boolean;
9
+ onEdit: () => void;
10
+ onResize: (by: number) => void;
11
+ onResizeTo: (width: number, height: number) => void;
12
+ onDelete: () => void;
13
+ onDismiss: () => void;
14
+ }
15
+ /**
16
+ * Context menu scope reduction: media no longer gets a generic right-click
17
+ * menu - it gets this dedicated overlay, anchored to the atom's own live
18
+ * element rect (the same ResizeObserver/scroll-tracking pattern
19
+ * TableResizeHandles.tsx already uses for anchoring UI to a live element,
20
+ * per §1's investigation) instead of click coordinates, appearing whenever
21
+ * an atom is selected. The caller controls visibility by conditionally
22
+ * rendering this component based on atomSelected - when the selection
23
+ * moves off the atom, the component unmounts on its own, which is the
24
+ * "dismiss when selection moves away" requirement with no extra logic
25
+ * needed here.
26
+ *
27
+ * Post-batch follow-up ("images doesn't have a resizer"): the Resize +/-
28
+ * buttons were the only resize affordance - no direct drag handle existed
29
+ * for atoms, unlike tables. Added a corner drag handle at the atom's own
30
+ * bottom-right corner, reusing TableResizeHandles.tsx's exact live-preview-
31
+ * during-drag / commit-on-release pattern (mutate the real element's style
32
+ * directly while dragging for immediate feedback, commit via a real command
33
+ * only on release) rather than inventing a different interaction model for
34
+ * the second resizable-thing in this codebase.
35
+ */
36
+ export declare function MediaOverlay({ atomElement, alt, width, height, src, resizable, onEdit, onResize, onResizeTo, onDelete, onDismiss }: MediaOverlayProps): import("react/jsx-runtime").JSX.Element;