smartrte-react 0.3.5 → 1.0.0-beta.2
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/CHANGELOG.md +14 -0
- package/README.md +106 -14
- package/dist/adapters/domChecklistCommandBridge.js +1 -1
- package/dist/adapters/domCommandBridge.d.ts +2 -2
- package/dist/adapters/domCommandBridge.js +2 -2
- package/dist/adapters/domInlineAtomCommandBridge.d.ts +10 -9
- package/dist/adapters/domInlineAtomCommandBridge.js +154 -50
- package/dist/adapters/domInlineImageCommandBridge.d.ts +7 -7
- package/dist/adapters/domInlineImageCommandBridge.js +6 -72
- package/dist/adapters/domMoveCommandBridge.js +1 -1
- package/dist/adapters/domSelectionBridge.d.ts +3 -3
- package/dist/adapters/domSmartDocument.d.ts +4 -4
- package/dist/adapters/domSmartDocument.js +2 -2
- package/dist/adapters/domTableCommandBridge.d.ts +42 -16
- package/dist/adapters/domTableCommandBridge.js +203 -173
- package/dist/adapters/internalFlags.d.ts +0 -8
- package/dist/adapters/internalFlags.js +0 -24
- package/dist/adapters/legacyListShadowComparator.d.ts +1 -1
- package/dist/adapters/legacyListShadowComparator.js +1 -1
- package/dist/adapters/pdfPrint.d.ts +8 -0
- package/dist/adapters/pdfPrint.js +22 -0
- package/dist/adapters/shadowMode.d.ts +3 -3
- package/dist/adapters/shadowMode.js +1 -1
- package/dist/canonicalEditorRuntime.d.ts +53 -1
- package/dist/canonicalEditorRuntime.js +61 -4
- package/dist/capabilityPresets.d.ts +27 -0
- package/dist/capabilityPresets.js +21 -0
- package/dist/collabTransport.d.ts +66 -0
- package/dist/collabTransport.js +1 -0
- package/dist/commentProvider.d.ts +24 -0
- package/dist/commentProvider.js +1 -0
- package/dist/components/CanonicalAuthorityEditor.d.ts +35 -1
- package/dist/components/CanonicalAuthorityEditor.js +1086 -112
- package/dist/components/ClassicEditorAuthority.d.ts +93 -22
- package/dist/components/ClassicEditorAuthority.js +11 -43
- package/dist/components/ColorPickerPopover.d.ts +39 -0
- package/dist/components/ColorPickerPopover.js +285 -0
- package/dist/components/CommentMarkers.d.ts +25 -0
- package/dist/components/CommentMarkers.js +105 -0
- package/dist/components/CommentThreadPanel.d.ts +26 -0
- package/dist/components/CommentThreadPanel.js +47 -0
- package/dist/components/ContextMenu.d.ts +23 -0
- package/dist/components/ContextMenu.js +141 -0
- package/dist/components/FormulaLibraryPopover.d.ts +16 -0
- package/dist/components/FormulaLibraryPopover.js +107 -0
- package/dist/components/LinkEditorPopover.d.ts +3 -1
- package/dist/components/LinkEditorPopover.js +22 -6
- package/dist/components/MediaManager.d.ts +4 -20
- package/dist/components/MediaManager.js +35 -6
- package/dist/components/MediaOverlay.d.ts +36 -0
- package/dist/components/MediaOverlay.js +163 -0
- package/dist/components/SpecialCharacterPopover.d.ts +18 -0
- package/dist/components/SpecialCharacterPopover.js +99 -0
- package/dist/components/StructuralSuggestionMarkers.d.ts +20 -0
- package/dist/components/StructuralSuggestionMarkers.js +56 -0
- package/dist/components/SuggestionPanel.d.ts +26 -0
- package/dist/components/SuggestionPanel.js +33 -0
- package/dist/components/TableBorderPopover.d.ts +59 -0
- package/dist/components/TableBorderPopover.js +147 -0
- package/dist/components/TableResizeHandles.d.ts +25 -0
- package/dist/components/TableResizeHandles.js +301 -0
- package/dist/components/TableSizePickerPopover.d.ts +16 -0
- package/dist/components/TableSizePickerPopover.js +126 -0
- package/dist/components/ToolbarPrimitives.d.ts +129 -0
- package/dist/components/ToolbarPrimitives.js +146 -0
- package/dist/components/VersionHistoryPanel.d.ts +9 -0
- package/dist/components/VersionHistoryPanel.js +157 -0
- package/dist/formulaLibrary.d.ts +31 -0
- package/dist/formulaLibrary.js +90 -0
- package/dist/index.d.ts +22 -24
- package/dist/index.js +14 -11
- package/dist/mediaManagerAdapter.d.ts +12 -0
- package/dist/mediaManagerAdapter.js +34 -0
- package/dist/mediaProvider.d.ts +21 -1
- package/dist/specialCharacters.d.ts +22 -0
- package/dist/specialCharacters.js +86 -0
- package/dist/standalone/classic-editor-embed.js +19 -12
- package/dist/suggestionProvider.d.ts +17 -0
- package/dist/suggestionProvider.js +1 -0
- package/dist/theme.d.ts +1 -1
- package/dist/theme.js +150 -20
- package/dist/versionProvider.d.ts +29 -0
- package/dist/versionProvider.js +1 -0
- package/package.json +28 -14
- package/dist/adapters/canonicalInlineCommandBridge.d.ts +0 -15
- package/dist/adapters/canonicalInlineCommandBridge.js +0 -269
- package/dist/adapters/canonicalListCommandBridge.d.ts +0 -26
- package/dist/adapters/canonicalListCommandBridge.js +0 -430
- package/dist/adapters/coreBold.d.ts +0 -5
- package/dist/adapters/coreBold.js +0 -3
- package/dist/adapters/documentFormats.d.ts +0 -26
- package/dist/adapters/documentFormats.js +0 -49
- package/dist/adapters/docxFormat.d.ts +0 -5
- package/dist/adapters/docxFormat.js +0 -272
- package/dist/adapters/domBlockCommandBridge.d.ts +0 -19
- package/dist/adapters/domBlockCommandBridge.js +0 -79
- package/dist/adapters/inlineMarkCoreExecution.d.ts +0 -9
- package/dist/adapters/inlineMarkCoreExecution.js +0 -25
- package/dist/adapters/pdfFormat.d.ts +0 -23
- package/dist/adapters/pdfFormat.js +0 -204
- package/dist/adapters/portableDocxAtoms.d.ts +0 -4
- package/dist/adapters/portableDocxAtoms.js +0 -58
- package/dist/adapters/styledDocxFormat.d.ts +0 -8
- package/dist/adapters/styledDocxFormat.js +0 -190
- package/dist/builtInFormatDefinitions.d.ts +0 -5
- package/dist/builtInFormatDefinitions.js +0 -82
- package/dist/canonicalAuthorityFlag.d.ts +0 -32
- package/dist/canonicalAuthorityFlag.js +0 -49
- package/dist/canonicalClipboardRuntime.d.ts +0 -15
- package/dist/canonicalClipboardRuntime.js +0 -76
- package/dist/components/ClassicEditor.d.ts +0 -76
- package/dist/components/ClassicEditor.js +0 -6729
- package/dist/editorController.d.ts +0 -78
- package/dist/editorController.js +0 -298
- package/dist/formatFidelity.d.ts +0 -14
- package/dist/formatFidelity.js +0 -107
- package/dist/formatRuntime.d.ts +0 -50
- package/dist/formatRuntime.js +0 -20
- package/dist/pluginRuntime.d.ts +0 -68
- package/dist/pluginRuntime.js +0 -78
- package/dist/standalone/editor.js +0 -1068
- package/dist/test-harness/atomShadowComparator.d.ts +0 -14
- package/dist/test-harness/atomShadowComparator.js +0 -63
- package/dist/test-harness/blockShadowComparator.d.ts +0 -30
- package/dist/test-harness/blockShadowComparator.js +0 -140
- package/dist/test-harness/clipboardShadowComparator.d.ts +0 -12
- package/dist/test-harness/clipboardShadowComparator.js +0 -46
- package/dist/test-harness/inlineShadowComparator.d.ts +0 -32
- package/dist/test-harness/inlineShadowComparator.js +0 -152
- package/dist/test-harness/legacyAtomEngine.d.ts +0 -10
- package/dist/test-harness/legacyAtomEngine.js +0 -15
- package/dist/test-harness/legacyBlockEngine.d.ts +0 -31
- package/dist/test-harness/legacyBlockEngine.js +0 -31
- package/dist/test-harness/legacyClipboardEngine.d.ts +0 -14
- package/dist/test-harness/legacyClipboardEngine.js +0 -67
- package/dist/test-harness/legacyInlineEngine.d.ts +0 -19
- package/dist/test-harness/legacyInlineEngine.js +0 -49
- package/dist/test-harness/legacyTableEngine.d.ts +0 -12
- package/dist/test-harness/legacyTableEngine.js +0 -25
- package/dist/test-harness/tableShadowComparator.d.ts +0 -29
- 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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
2
|
-
|
|
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;
|