smartrte-react 0.3.5 → 1.0.0-beta.10
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 +55 -0
- package/README.md +205 -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 +65 -1
- package/dist/canonicalEditorRuntime.js +63 -5
- 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 +56 -1
- package/dist/components/CanonicalAuthorityEditor.js +1304 -116
- 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 +287 -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/MediaDetailsPopover.d.ts +35 -0
- package/dist/components/MediaDetailsPopover.js +136 -0
- package/dist/components/MediaManager.d.ts +4 -20
- package/dist/components/MediaManager.js +40 -11
- package/dist/components/MediaOverlay.d.ts +27 -0
- package/dist/components/MediaOverlay.js +228 -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 +149 -0
- package/dist/components/TableResizeHandles.d.ts +25 -0
- package/dist/components/TableResizeHandles.js +323 -0
- package/dist/components/TableSizePickerPopover.d.ts +16 -0
- package/dist/components/TableSizePickerPopover.js +126 -0
- package/dist/components/ToolbarPrimitives.d.ts +161 -0
- package/dist/components/ToolbarPrimitives.js +237 -0
- package/dist/components/VersionHistoryPanel.d.ts +9 -0
- package/dist/components/VersionHistoryPanel.js +157 -0
- package/dist/components/fixedPositioning.d.ts +25 -0
- package/dist/components/fixedPositioning.js +42 -0
- package/dist/formulaLibrary.d.ts +31 -0
- package/dist/formulaLibrary.js +90 -0
- package/dist/index.d.ts +25 -24
- package/dist/index.js +15 -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 +219 -30
- package/dist/toolbarTools.d.ts +95 -0
- package/dist/toolbarTools.js +17 -0
- 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,136 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
3
|
+
import { normalizeLinkInput } from "smartrte-core/legacy";
|
|
4
|
+
const LICENSE_TYPE_PRESETS = ["CC BY", "CC BY-SA", "CC BY-ND", "CC BY-NC", "CC BY-NC-SA", "CC BY-NC-ND", "CC0 (Public Domain)", "All rights reserved"];
|
|
5
|
+
const inputStyle = {
|
|
6
|
+
width: "100%", height: 34, boxSizing: "border-box", marginTop: 4, padding: "0 9px",
|
|
7
|
+
border: "1px solid var(--srte-input-border)", borderRadius: 8, outline: "none",
|
|
8
|
+
background: "var(--srte-input-bg)", color: "var(--srte-input-text)", font: "inherit", fontSize: 13,
|
|
9
|
+
};
|
|
10
|
+
const selectStyle = { ...inputStyle, cursor: "pointer" };
|
|
11
|
+
const labelStyle = { display: "block", fontSize: 12, fontWeight: 600 };
|
|
12
|
+
const buttonStyle = {
|
|
13
|
+
minHeight: 34, padding: "0 12px", border: "1px solid var(--srte-input-border)", borderRadius: 8,
|
|
14
|
+
background: "var(--srte-input-bg)", color: "var(--srte-menu-text)", cursor: "pointer", fontWeight: 500, fontSize: 13,
|
|
15
|
+
};
|
|
16
|
+
const sectionHeaderStyle = {
|
|
17
|
+
fontSize: 11, fontWeight: 700, textTransform: "uppercase", letterSpacing: "0.04em", opacity: 0.7,
|
|
18
|
+
marginTop: 4, paddingTop: 10, borderTop: "1px solid var(--srte-border)",
|
|
19
|
+
};
|
|
20
|
+
const alignButton = (current, value, label, onClick) => (_jsx("button", { type: "button", "aria-label": label, "aria-pressed": current === value, onClick: onClick, style: {
|
|
21
|
+
...buttonStyle,
|
|
22
|
+
flex: 1,
|
|
23
|
+
minHeight: 32,
|
|
24
|
+
padding: 0,
|
|
25
|
+
background: current === value ? "var(--srte-accent-bg)" : "var(--srte-input-bg)",
|
|
26
|
+
borderColor: current === value ? "var(--srte-primary)" : "var(--srte-input-border)",
|
|
27
|
+
color: current === value ? "var(--srte-primary-pressed)" : "var(--srte-menu-text)",
|
|
28
|
+
}, children: label }, label));
|
|
29
|
+
/**
|
|
30
|
+
* "Media details" - replaces the crude window.prompt("Alt text") edit path
|
|
31
|
+
* (docs/bugs/media-details-old-editor-field-parity.md) with a real settings
|
|
32
|
+
* panel matching the old editor's own field set, following this project's
|
|
33
|
+
* established compact-popover pattern (position:fixed + JS-measured
|
|
34
|
+
* placement, Apply/Cancel commit - same shape as TableBorderPopover for a
|
|
35
|
+
* multi-field dialog like this one, rather than LinkEditorPopover's simpler
|
|
36
|
+
* live-Enter-to-apply form).
|
|
37
|
+
*
|
|
38
|
+
* Opened directly by an image's right-click interaction; the details panel
|
|
39
|
+
* is the image editing surface, while resize handles remain available
|
|
40
|
+
* independently around the selected image.
|
|
41
|
+
*/
|
|
42
|
+
export function MediaDetailsPopover({ x, y, initial, onApply, onCancel }) {
|
|
43
|
+
const [draft, setDraft] = useState(initial);
|
|
44
|
+
const [licenseTypeCustom, setLicenseTypeCustom] = useState(!!initial.licenseType && !LICENSE_TYPE_PRESETS.includes(initial.licenseType));
|
|
45
|
+
const [error, setError] = useState("");
|
|
46
|
+
const rootRef = useRef(null);
|
|
47
|
+
const [placement, setPlacement] = useState(null);
|
|
48
|
+
const onCancelRef = useRef(onCancel);
|
|
49
|
+
onCancelRef.current = onCancel;
|
|
50
|
+
useLayoutEffect(() => {
|
|
51
|
+
const el = rootRef.current;
|
|
52
|
+
if (!el)
|
|
53
|
+
return;
|
|
54
|
+
const margin = 8;
|
|
55
|
+
const { width, height } = el.getBoundingClientRect();
|
|
56
|
+
const viewportWidth = window.innerWidth;
|
|
57
|
+
const viewportHeight = window.innerHeight;
|
|
58
|
+
const overflowsRight = x + width > viewportWidth - margin;
|
|
59
|
+
const left = overflowsRight ? x - width : x;
|
|
60
|
+
const clampedLeft = Math.min(Math.max(margin, left), Math.max(margin, viewportWidth - width - margin));
|
|
61
|
+
const overflowsBottom = y + height > viewportHeight - margin;
|
|
62
|
+
const top = overflowsBottom ? Math.max(margin, y - height) : y;
|
|
63
|
+
const clampedTop = Math.min(Math.max(margin, top), Math.max(margin, viewportHeight - height - margin));
|
|
64
|
+
setPlacement({ left: clampedLeft, top: clampedTop });
|
|
65
|
+
}, [x, y]);
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
const dismissIfOutside = (event) => {
|
|
68
|
+
const target = event.target;
|
|
69
|
+
const elementTarget = target instanceof Element ? target : target.parentElement;
|
|
70
|
+
// Resize handles live outside this dialog and must receive their own
|
|
71
|
+
// pointer gesture without the editor closing on the first pointerdown.
|
|
72
|
+
if (elementTarget?.closest("[data-srte-media-resize-handle-direction]"))
|
|
73
|
+
return;
|
|
74
|
+
if (!rootRef.current?.contains(target))
|
|
75
|
+
onCancelRef.current();
|
|
76
|
+
};
|
|
77
|
+
window.addEventListener("pointerdown", dismissIfOutside, true);
|
|
78
|
+
window.addEventListener("mousedown", dismissIfOutside, true);
|
|
79
|
+
return () => {
|
|
80
|
+
window.removeEventListener("pointerdown", dismissIfOutside, true);
|
|
81
|
+
window.removeEventListener("mousedown", dismissIfOutside, true);
|
|
82
|
+
};
|
|
83
|
+
}, []);
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
const onWindowKeyDown = (event) => { if (event.key === "Escape")
|
|
86
|
+
onCancelRef.current(); };
|
|
87
|
+
window.addEventListener("keydown", onWindowKeyDown);
|
|
88
|
+
return () => window.removeEventListener("keydown", onWindowKeyDown);
|
|
89
|
+
}, []);
|
|
90
|
+
const update = (patch) => setDraft((current) => ({ ...current, ...patch }));
|
|
91
|
+
const apply = () => {
|
|
92
|
+
if (!draft.alt.trim()) {
|
|
93
|
+
setError("Enter alt text.");
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const trimmedHref = draft.href.trim();
|
|
97
|
+
if (!trimmedHref) {
|
|
98
|
+
onApply({ ...draft, href: "" });
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const normalized = normalizeLinkInput(trimmedHref);
|
|
102
|
+
if (!normalized.href) {
|
|
103
|
+
setError("Enter a safe, valid web URL, email address, phone number, or page anchor for the link, or clear it.");
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
onApply({ ...draft, href: normalized.href });
|
|
107
|
+
};
|
|
108
|
+
return (_jsxs("div", { ref: rootRef, "data-srte-media-details-popover": "true", role: "dialog", "aria-label": "Media details", style: {
|
|
109
|
+
position: "fixed",
|
|
110
|
+
left: placement?.left ?? x,
|
|
111
|
+
top: placement?.top ?? y,
|
|
112
|
+
visibility: placement ? "visible" : "hidden",
|
|
113
|
+
zIndex: 72,
|
|
114
|
+
width: 320,
|
|
115
|
+
maxWidth: "calc(100vw - 16px)",
|
|
116
|
+
maxHeight: "calc(100vh - 16px)",
|
|
117
|
+
overflowY: "auto",
|
|
118
|
+
boxSizing: "border-box",
|
|
119
|
+
background: "var(--srte-menu-bg)",
|
|
120
|
+
color: "var(--srte-menu-text)",
|
|
121
|
+
border: "1px solid var(--srte-border)",
|
|
122
|
+
borderRadius: 12,
|
|
123
|
+
boxShadow: "var(--srte-menu-shadow)",
|
|
124
|
+
padding: 14,
|
|
125
|
+
display: "flex",
|
|
126
|
+
flexDirection: "column",
|
|
127
|
+
gap: 10,
|
|
128
|
+
}, children: [_jsxs("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between" }, children: [_jsx("div", { style: { fontWeight: 650 }, children: "Media details" }), _jsx("button", { type: "button", "aria-label": "Close media details", onClick: onCancel, style: { ...buttonStyle, minWidth: 26, minHeight: 26, padding: 0, border: 0, background: "transparent", fontSize: 18 }, children: "\u00D7" })] }), _jsxs("label", { style: labelStyle, children: ["Alt text", _jsx("input", { "data-srte-media-alt-input": "true", value: draft.alt, onChange: (event) => { update({ alt: event.target.value }); setError(""); }, style: { ...inputStyle, borderColor: error ? "var(--srte-danger)" : "var(--srte-input-border)" } })] }), error && _jsx("div", { role: "alert", style: { color: "var(--srte-danger)", fontSize: 12 }, children: error }), _jsxs("label", { style: labelStyle, children: ["Link", _jsx("input", { "data-srte-media-href-input": "true", value: draft.href, placeholder: "Paste a URL", inputMode: "url", autoCapitalize: "none", autoCorrect: "off", spellCheck: false, onChange: (event) => { update({ href: event.target.value }); setError(""); }, style: { ...inputStyle, borderColor: error ? "var(--srte-danger)" : "var(--srte-input-border)" } })] }), _jsxs("label", { style: { display: "flex", alignItems: "center", gap: 8, fontSize: 13, cursor: "pointer" }, children: [_jsx("input", { "data-srte-media-new-tab-input": "true", type: "checkbox", checked: draft.openInNewTab, disabled: !draft.href.trim(), onChange: (event) => update({ openInNewTab: event.target.checked }) }), "Open in a new tab"] }), _jsxs("div", { style: { display: "flex", gap: 8 }, children: [_jsxs("label", { style: { ...labelStyle, flex: 1 }, children: ["Width (px)", _jsx("input", { "data-srte-media-width-input": "true", type: "number", min: 16, value: draft.width ?? "", onChange: (event) => update({ width: event.target.value === "" ? undefined : Math.max(16, Number(event.target.value)) }), style: inputStyle })] }), _jsxs("label", { style: { ...labelStyle, flex: 1 }, children: ["Corner radius (px)", _jsx("input", { "data-srte-media-radius-input": "true", type: "number", min: 0, value: draft.borderRadius ?? "", onChange: (event) => update({ borderRadius: event.target.value === "" ? undefined : Math.max(0, Number(event.target.value)) }), style: inputStyle })] })] }), _jsxs("div", { style: labelStyle, children: ["Align", _jsxs("div", { style: { display: "flex", gap: 6, marginTop: 4 }, children: [alignButton(draft.align, undefined, "None", () => update({ align: undefined })), alignButton(draft.align, "left", "Left", () => update({ align: "left" })), alignButton(draft.align, "center", "Center", () => update({ align: "center" })), alignButton(draft.align, "right", "Right", () => update({ align: "right" }))] })] }), _jsx("div", { style: sectionHeaderStyle, children: "License" }), _jsxs("label", { style: labelStyle, children: ["Description", _jsx("input", { "data-srte-media-license-description-input": "true", value: draft.licenseDescription, onChange: (event) => update({ licenseDescription: event.target.value }), style: inputStyle })] }), _jsxs("label", { style: labelStyle, children: ["Source URL", _jsx("input", { "data-srte-media-license-source-input": "true", value: draft.licenseSourceUrl, onChange: (event) => update({ licenseSourceUrl: event.target.value }), style: inputStyle })] }), _jsxs("label", { style: labelStyle, children: ["Type", _jsxs("select", { "data-srte-media-license-type-select": "true", value: licenseTypeCustom ? "custom" : draft.licenseType, onChange: (event) => {
|
|
129
|
+
if (event.target.value === "custom") {
|
|
130
|
+
setLicenseTypeCustom(true);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
setLicenseTypeCustom(false);
|
|
134
|
+
update({ licenseType: event.target.value });
|
|
135
|
+
}, style: selectStyle, children: [_jsx("option", { value: "", children: "(none)" }), LICENSE_TYPE_PRESETS.map((preset) => _jsx("option", { value: preset, children: preset }, preset)), _jsx("option", { value: "custom", children: "Custom\u2026" })] }), licenseTypeCustom && (_jsx("input", { "data-srte-media-license-type-custom-input": "true", value: draft.licenseType, placeholder: "License type", onChange: (event) => update({ licenseType: event.target.value }), style: inputStyle }))] }), _jsxs("label", { style: labelStyle, children: ["Version", _jsx("input", { "data-srte-media-license-version-input": "true", value: draft.licenseVersion, onChange: (event) => update({ licenseVersion: event.target.value }), style: inputStyle })] }), _jsxs("label", { style: labelStyle, children: ["Attribution", _jsx("input", { "data-srte-media-license-attribution-input": "true", value: draft.licenseAttribution, onChange: (event) => update({ licenseAttribution: event.target.value }), style: inputStyle })] }), _jsxs("div", { style: { display: "flex", justifyContent: "flex-end", gap: 7, marginTop: 4 }, children: [_jsx("button", { type: "button", onClick: onCancel, style: buttonStyle, children: "Cancel" }), _jsx("button", { type: "button", onClick: apply, style: { ...buttonStyle, borderColor: "var(--srte-primary)", background: "var(--srte-primary)", color: "var(--srte-on-primary)" }, children: "Apply" })] })] }));
|
|
136
|
+
}
|
|
@@ -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
|
-
background: activeTab === "upload" ? "var(--srte-
|
|
141
|
+
background: activeTab === "upload" ? "var(--srte-muted)" : "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
|
-
background: activeTab === "library" ? "var(--srte-
|
|
147
|
+
background: activeTab === "library" ? "var(--srte-muted)" : "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();
|
|
@@ -138,7 +157,7 @@ export function MediaManager(props) {
|
|
|
138
157
|
padding: 24,
|
|
139
158
|
textAlign: "center",
|
|
140
159
|
color: "var(--srte-text-muted)",
|
|
141
|
-
background: "var(--srte-
|
|
160
|
+
background: "var(--srte-muted)",
|
|
142
161
|
cursor: uploading ? "default" : "pointer",
|
|
143
162
|
opacity: uploading ? 0.7 : 1,
|
|
144
163
|
}, children: uploading ? "Uploading…" : "Click or drag images to upload" })] })) : (_jsxs("div", { style: {
|
|
@@ -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,
|
|
@@ -188,7 +207,7 @@ export function MediaManager(props) {
|
|
|
188
207
|
height: 24,
|
|
189
208
|
border: "1px solid var(--srte-border)",
|
|
190
209
|
borderRadius: 999,
|
|
191
|
-
background: "var(--srte-
|
|
210
|
+
background: "var(--srte-muted)",
|
|
192
211
|
color: "var(--srte-input-text)",
|
|
193
212
|
cursor: "pointer",
|
|
194
213
|
flex: "0 0 auto",
|
|
@@ -196,7 +215,7 @@ export function MediaManager(props) {
|
|
|
196
215
|
fontSize: 10,
|
|
197
216
|
padding: "1px 5px",
|
|
198
217
|
borderRadius: 999,
|
|
199
|
-
background: "var(--srte-
|
|
218
|
+
background: "var(--srte-muted)",
|
|
200
219
|
color: "var(--srte-text-muted)",
|
|
201
220
|
}, children: tag }, tag))) }))] }, it.id || it.url))) })] })), infoItem && (_jsx("div", { style: {
|
|
202
221
|
position: "fixed",
|
|
@@ -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,27 @@
|
|
|
1
|
+
export interface MediaOverlayProps {
|
|
2
|
+
atomElement: HTMLElement;
|
|
3
|
+
alt: string;
|
|
4
|
+
width?: number;
|
|
5
|
+
height?: number;
|
|
6
|
+
src?: string;
|
|
7
|
+
x?: number;
|
|
8
|
+
y?: number;
|
|
9
|
+
/** Render only the handles when the media details editor owns the interaction surface. */
|
|
10
|
+
showMenu?: boolean;
|
|
11
|
+
/** False for atoms with no width/height concept (e.g. formula, sized by KaTeX from source/font-size) - hides resize controls and handles, keeping Edit/Delete. */
|
|
12
|
+
resizable?: boolean;
|
|
13
|
+
/** False for atoms with no editable fields at all (divider, page break) - hides the Alt/Size/Source info block and the Edit button, keeping only Delete. */
|
|
14
|
+
editable?: boolean;
|
|
15
|
+
onEdit: () => void;
|
|
16
|
+
onResize: (by: number) => void;
|
|
17
|
+
onResizeTo: (width: number, height: number) => void;
|
|
18
|
+
onDelete: () => void;
|
|
19
|
+
onDismiss: () => void;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Media resize handles stay anchored to the selected media element and
|
|
23
|
+
* preview size changes live, committing one canonical resize operation when
|
|
24
|
+
* the pointer is released. The quick-action menu is optional because image
|
|
25
|
+
* right-clicks use MediaDetailsPopover directly.
|
|
26
|
+
*/
|
|
27
|
+
export declare function MediaOverlay({ atomElement, alt, width, height, src, x, y, showMenu, resizable, editable, onEdit, onResize, onResizeTo, onDelete, onDismiss }: MediaOverlayProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
3
|
+
const RESIZE_DIRECTIONS = ["n", "ne", "e", "se", "s", "sw", "w", "nw"];
|
|
4
|
+
const HANDLE_SIZE = 10;
|
|
5
|
+
const MIN_ATOM_SIZE = 24;
|
|
6
|
+
const buttonStyle = {
|
|
7
|
+
minHeight: 32,
|
|
8
|
+
padding: "0 10px",
|
|
9
|
+
border: "1px solid var(--srte-input-border)",
|
|
10
|
+
borderRadius: 8,
|
|
11
|
+
background: "var(--srte-input-bg)",
|
|
12
|
+
color: "var(--srte-menu-text)",
|
|
13
|
+
cursor: "pointer",
|
|
14
|
+
fontWeight: 500,
|
|
15
|
+
fontSize: 13,
|
|
16
|
+
};
|
|
17
|
+
const cursorFor = (direction) => {
|
|
18
|
+
if (direction === "n" || direction === "s")
|
|
19
|
+
return "ns-resize";
|
|
20
|
+
if (direction === "e" || direction === "w")
|
|
21
|
+
return "ew-resize";
|
|
22
|
+
return direction === "ne" || direction === "sw" ? "nesw-resize" : "nwse-resize";
|
|
23
|
+
};
|
|
24
|
+
const isCorner = (direction) => direction.length === 2;
|
|
25
|
+
const visibleBoundsFor = (atomElement, modelWidth, modelHeight) => {
|
|
26
|
+
const rect = atomElement.getBoundingClientRect();
|
|
27
|
+
const editorElement = atomElement.closest(".srte-editor");
|
|
28
|
+
const editorRect = editorElement?.getBoundingClientRect();
|
|
29
|
+
const inset = HANDLE_SIZE / 2;
|
|
30
|
+
// Broken or still-loading images can expose only a tiny fallback box.
|
|
31
|
+
// Use the canonical dimensions for that case, but use the real rendered
|
|
32
|
+
// dimensions whenever the image has a meaningful layout box.
|
|
33
|
+
const imageWidth = rect.width >= MIN_ATOM_SIZE ? rect.width : modelWidth && modelWidth >= MIN_ATOM_SIZE ? modelWidth : rect.width;
|
|
34
|
+
const imageHeight = rect.height >= MIN_ATOM_SIZE ? rect.height : modelHeight && modelHeight >= MIN_ATOM_SIZE ? modelHeight : rect.height;
|
|
35
|
+
const left = Math.max(rect.left, editorRect ? editorRect.left + inset : rect.left);
|
|
36
|
+
const top = Math.max(rect.top, editorRect ? editorRect.top + inset : rect.top);
|
|
37
|
+
const right = Math.min(rect.left + imageWidth, editorRect ? editorRect.right - inset : rect.left + imageWidth);
|
|
38
|
+
const bottom = Math.min(rect.top + imageHeight, editorRect ? editorRect.bottom - inset : rect.top + imageHeight);
|
|
39
|
+
if (right <= left || bottom <= top)
|
|
40
|
+
return null;
|
|
41
|
+
return { left, top, width: right - left, height: bottom - top };
|
|
42
|
+
};
|
|
43
|
+
const dimensionsForDrag = (drag, clientX, clientY) => {
|
|
44
|
+
const deltaX = clientX - drag.startClientX;
|
|
45
|
+
const deltaY = clientY - drag.startClientY;
|
|
46
|
+
const widthDelta = drag.direction.includes("e") ? deltaX : drag.direction.includes("w") ? -deltaX : 0;
|
|
47
|
+
const heightDelta = drag.direction.includes("s") ? deltaY : drag.direction.includes("n") ? -deltaY : 0;
|
|
48
|
+
if (!isCorner(drag.direction)) {
|
|
49
|
+
return {
|
|
50
|
+
width: Math.max(MIN_ATOM_SIZE, Math.round(drag.startWidth + widthDelta)),
|
|
51
|
+
height: Math.max(MIN_ATOM_SIZE, Math.round(drag.startHeight + heightDelta)),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
// Corner handles keep the same aspect ratio as the current image. Use the
|
|
55
|
+
// axis with the larger proportional movement so dragging either diagonal
|
|
56
|
+
// direction feels natural, including when one pointer axis barely moves.
|
|
57
|
+
const widthScale = (drag.startWidth + widthDelta) / drag.startWidth;
|
|
58
|
+
const heightScale = (drag.startHeight + heightDelta) / drag.startHeight;
|
|
59
|
+
const scale = Math.max(MIN_ATOM_SIZE / drag.startWidth, MIN_ATOM_SIZE / drag.startHeight, Math.abs(widthDelta / drag.startWidth) >= Math.abs(heightDelta / drag.startHeight) ? widthScale : heightScale);
|
|
60
|
+
return {
|
|
61
|
+
width: Math.max(MIN_ATOM_SIZE, Math.round(drag.startWidth * scale)),
|
|
62
|
+
height: Math.max(MIN_ATOM_SIZE, Math.round(drag.startHeight * scale)),
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
const boundsForDirection = (bounds, direction) => {
|
|
66
|
+
const half = HANDLE_SIZE / 2;
|
|
67
|
+
const centerX = bounds.left + bounds.width / 2 - half;
|
|
68
|
+
const centerY = bounds.top + bounds.height / 2 - half;
|
|
69
|
+
const right = bounds.left + bounds.width - half;
|
|
70
|
+
const bottom = bounds.top + bounds.height - half;
|
|
71
|
+
const left = direction.includes("w") ? bounds.left - half : direction.includes("e") ? right : centerX;
|
|
72
|
+
const top = direction.includes("n") ? bounds.top - half : direction.includes("s") ? bottom : centerY;
|
|
73
|
+
return { left, top };
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* Media resize handles stay anchored to the selected media element and
|
|
77
|
+
* preview size changes live, committing one canonical resize operation when
|
|
78
|
+
* the pointer is released. The quick-action menu is optional because image
|
|
79
|
+
* right-clicks use MediaDetailsPopover directly.
|
|
80
|
+
*/
|
|
81
|
+
export function MediaOverlay({ atomElement, alt, width, height, src, x, y, showMenu = true, resizable = true, editable = true, onEdit, onResize, onResizeTo, onDelete, onDismiss }) {
|
|
82
|
+
const rootRef = useRef(null);
|
|
83
|
+
const dragRef = useRef(null);
|
|
84
|
+
const [placement, setPlacement] = useState(null);
|
|
85
|
+
const [bounds, setBounds] = useState(null);
|
|
86
|
+
const [dragging, setDragging] = useState(false);
|
|
87
|
+
useLayoutEffect(() => {
|
|
88
|
+
const recompute = () => {
|
|
89
|
+
const rect = atomElement.getBoundingClientRect();
|
|
90
|
+
const margin = 8;
|
|
91
|
+
const viewportWidth = atomElement.ownerDocument.defaultView?.innerWidth ?? 0;
|
|
92
|
+
const viewportHeight = atomElement.ownerDocument.defaultView?.innerHeight ?? 0;
|
|
93
|
+
const panelWidth = rootRef.current?.getBoundingClientRect().width ?? 260;
|
|
94
|
+
const panelHeight = rootRef.current?.getBoundingClientRect().height ?? 160;
|
|
95
|
+
const preferredLeft = x ?? rect.left;
|
|
96
|
+
const preferredTop = y ?? rect.bottom + 6;
|
|
97
|
+
const left = preferredLeft + panelWidth > viewportWidth - margin ? preferredLeft - panelWidth : preferredLeft;
|
|
98
|
+
const top = preferredTop + panelHeight > viewportHeight - margin ? preferredTop - panelHeight : preferredTop;
|
|
99
|
+
setPlacement({
|
|
100
|
+
left: Math.min(Math.max(margin, left), Math.max(margin, viewportWidth - panelWidth - margin)),
|
|
101
|
+
top: Math.min(Math.max(margin, top), Math.max(margin, viewportHeight - panelHeight - margin)),
|
|
102
|
+
});
|
|
103
|
+
// Handles are fixed siblings of the contenteditable surface, so they
|
|
104
|
+
// do not inherit the editor's overflow clipping. Use the visible
|
|
105
|
+
// intersection with the editor viewport to keep every handle inside
|
|
106
|
+
// that boundary when a tall image extends below the scroll area.
|
|
107
|
+
setBounds(visibleBoundsFor(atomElement, width, height));
|
|
108
|
+
};
|
|
109
|
+
recompute();
|
|
110
|
+
const observer = new ResizeObserver(recompute);
|
|
111
|
+
observer.observe(atomElement);
|
|
112
|
+
const ownerWindow = atomElement.ownerDocument.defaultView;
|
|
113
|
+
ownerWindow?.addEventListener("scroll", recompute, true);
|
|
114
|
+
ownerWindow?.addEventListener("resize", recompute);
|
|
115
|
+
return () => {
|
|
116
|
+
observer.disconnect();
|
|
117
|
+
ownerWindow?.removeEventListener("scroll", recompute, true);
|
|
118
|
+
ownerWindow?.removeEventListener("resize", recompute);
|
|
119
|
+
};
|
|
120
|
+
}, [atomElement, height, width, x, y]);
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
if (showMenu && placement)
|
|
123
|
+
rootRef.current?.focus();
|
|
124
|
+
}, [placement, showMenu]);
|
|
125
|
+
useEffect(() => {
|
|
126
|
+
const ownerWindow = atomElement.ownerDocument.defaultView;
|
|
127
|
+
if (!ownerWindow)
|
|
128
|
+
return;
|
|
129
|
+
const handleMove = (event) => {
|
|
130
|
+
const drag = dragRef.current;
|
|
131
|
+
if (!drag)
|
|
132
|
+
return;
|
|
133
|
+
const dimensions = dimensionsForDrag(drag, event.clientX, event.clientY);
|
|
134
|
+
atomElement.style.width = `${dimensions.width}px`;
|
|
135
|
+
atomElement.style.height = `${dimensions.height}px`;
|
|
136
|
+
setBounds(visibleBoundsFor(atomElement));
|
|
137
|
+
};
|
|
138
|
+
const handleUp = (event) => {
|
|
139
|
+
const drag = dragRef.current;
|
|
140
|
+
if (!drag)
|
|
141
|
+
return;
|
|
142
|
+
const dimensions = dimensionsForDrag(drag, event.clientX, event.clientY);
|
|
143
|
+
dragRef.current = null;
|
|
144
|
+
setDragging(false);
|
|
145
|
+
// The model transaction below becomes authoritative. Clear the live
|
|
146
|
+
// preview first so a stale inline style cannot override later updates.
|
|
147
|
+
atomElement.style.removeProperty("width");
|
|
148
|
+
atomElement.style.removeProperty("height");
|
|
149
|
+
onResizeTo(dimensions.width, dimensions.height);
|
|
150
|
+
};
|
|
151
|
+
ownerWindow.addEventListener("pointermove", handleMove);
|
|
152
|
+
ownerWindow.addEventListener("pointerup", handleUp);
|
|
153
|
+
ownerWindow.addEventListener("pointercancel", handleUp);
|
|
154
|
+
return () => {
|
|
155
|
+
ownerWindow.removeEventListener("pointermove", handleMove);
|
|
156
|
+
ownerWindow.removeEventListener("pointerup", handleUp);
|
|
157
|
+
ownerWindow.removeEventListener("pointercancel", handleUp);
|
|
158
|
+
};
|
|
159
|
+
}, [atomElement, onResizeTo]);
|
|
160
|
+
const onDismissRef = useRef(onDismiss);
|
|
161
|
+
onDismissRef.current = onDismiss;
|
|
162
|
+
useEffect(() => {
|
|
163
|
+
if (!showMenu)
|
|
164
|
+
return;
|
|
165
|
+
const dismissIfOutside = (event) => {
|
|
166
|
+
const target = event.target;
|
|
167
|
+
const elementTarget = target instanceof Element ? target : target.parentElement;
|
|
168
|
+
const isResizeHandle = Boolean(elementTarget?.closest("[data-srte-media-resize-handle-direction]"));
|
|
169
|
+
if (dragRef.current)
|
|
170
|
+
return;
|
|
171
|
+
if (rootRef.current && !rootRef.current.contains(target) && !atomElement.contains(target) && !isResizeHandle)
|
|
172
|
+
onDismissRef.current();
|
|
173
|
+
};
|
|
174
|
+
window.addEventListener("pointerdown", dismissIfOutside, true);
|
|
175
|
+
window.addEventListener("mousedown", dismissIfOutside, true);
|
|
176
|
+
return () => {
|
|
177
|
+
window.removeEventListener("pointerdown", dismissIfOutside, true);
|
|
178
|
+
window.removeEventListener("mousedown", dismissIfOutside, true);
|
|
179
|
+
};
|
|
180
|
+
}, [atomElement, showMenu]);
|
|
181
|
+
return (_jsxs(_Fragment, { children: [resizable && bounds && RESIZE_DIRECTIONS.map((direction) => (_jsx("div", { "data-srte-media-resize-handle": "true", "data-srte-media-resize-handle-direction": direction, role: "separator", "aria-label": `Resize media ${direction}`, onPointerDown: (event) => {
|
|
182
|
+
event.preventDefault();
|
|
183
|
+
event.stopPropagation();
|
|
184
|
+
const startWidth = width && width >= MIN_ATOM_SIZE ? width : bounds.width;
|
|
185
|
+
const startHeight = height && height >= MIN_ATOM_SIZE ? height : bounds.height;
|
|
186
|
+
dragRef.current = {
|
|
187
|
+
direction,
|
|
188
|
+
startClientX: event.clientX,
|
|
189
|
+
startClientY: event.clientY,
|
|
190
|
+
startWidth,
|
|
191
|
+
startHeight,
|
|
192
|
+
};
|
|
193
|
+
setDragging(true);
|
|
194
|
+
}, style: {
|
|
195
|
+
...boundsForDirection(bounds, direction),
|
|
196
|
+
position: "fixed",
|
|
197
|
+
visibility: "visible",
|
|
198
|
+
width: HANDLE_SIZE,
|
|
199
|
+
height: HANDLE_SIZE,
|
|
200
|
+
boxSizing: "border-box",
|
|
201
|
+
borderRadius: 3,
|
|
202
|
+
background: dragging ? "var(--srte-primary)" : "var(--srte-menu-bg)",
|
|
203
|
+
border: "2px solid var(--srte-primary)",
|
|
204
|
+
cursor: cursorFor(direction),
|
|
205
|
+
zIndex: 81,
|
|
206
|
+
touchAction: "none",
|
|
207
|
+
} }, direction))), showMenu && _jsxs("div", { ref: rootRef, "data-srte-media-overlay": "true", role: "dialog", "aria-label": "Media actions", tabIndex: -1, style: {
|
|
208
|
+
position: "fixed",
|
|
209
|
+
left: placement?.left ?? x ?? 0,
|
|
210
|
+
top: placement?.top ?? y ?? 0,
|
|
211
|
+
visibility: placement ? "visible" : "hidden",
|
|
212
|
+
zIndex: 80,
|
|
213
|
+
width: 260,
|
|
214
|
+
maxWidth: "calc(100vw - 16px)",
|
|
215
|
+
boxSizing: "border-box",
|
|
216
|
+
background: "var(--srte-menu-bg)",
|
|
217
|
+
color: "var(--srte-menu-text)",
|
|
218
|
+
border: "1px solid var(--srte-border)",
|
|
219
|
+
borderRadius: 10,
|
|
220
|
+
boxShadow: "var(--srte-menu-shadow)",
|
|
221
|
+
padding: 10,
|
|
222
|
+
}, onKeyDown: (event) => {
|
|
223
|
+
if (event.key === "Escape") {
|
|
224
|
+
event.preventDefault();
|
|
225
|
+
onDismiss();
|
|
226
|
+
}
|
|
227
|
+
}, children: [editable && _jsxs("div", { style: { fontSize: 12, marginBottom: 8, display: "grid", gap: 2, opacity: 0.85 }, children: [_jsxs("div", { children: [_jsx("strong", { children: "Alt text:" }), " ", alt || "(none)"] }), width && height && _jsxs("div", { children: [_jsx("strong", { children: "Size:" }), " ", Math.round(width), "\u00D7", Math.round(height)] }), src && _jsxs("div", { style: { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }, children: [_jsx("strong", { children: "Source:" }), " ", src] })] }), _jsxs("div", { style: { display: "flex", gap: 6, flexWrap: "wrap" }, children: [editable && _jsx("button", { type: "button", style: buttonStyle, onClick: onEdit, children: "Edit" }), resizable && _jsx("button", { type: "button", style: buttonStyle, onClick: () => onResize(20), children: "Resize +" }), resizable && _jsx("button", { type: "button", style: buttonStyle, onClick: () => onResize(-20), children: "Resize \u03B2\u02C6\u2019" }), _jsx("button", { type: "button", style: { ...buttonStyle, color: "var(--srte-danger)", marginLeft: "auto" }, onClick: onDelete, children: "Delete" })] })] })] }));
|
|
228
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface SpecialCharacterPopoverProps {
|
|
2
|
+
x: number;
|
|
3
|
+
y: number;
|
|
4
|
+
/** Up to 8 previously-inserted characters, most-recent first - same recency pattern as ColorPickerPopover's recent colors. */
|
|
5
|
+
recentCharacters?: readonly string[];
|
|
6
|
+
onInsert: (char: string) => void;
|
|
7
|
+
onCancel: () => void;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Special character picker: six standardized-Unicode-block categories
|
|
11
|
+
* (~200 characters total - see specialCharacters.ts's own doc comment),
|
|
12
|
+
* reusing the grid-with-hover-feedback shape already established by
|
|
13
|
+
* ColorPickerPopover/TableSizePickerPopover rather than a new component
|
|
14
|
+
* pattern. Insertion is immediate on click (a character, unlike a dragged
|
|
15
|
+
* color, is already a single complete choice) via a real, page-owned text
|
|
16
|
+
* insert - see `insertSpecialCharacter` in CanonicalAuthorityEditor.tsx.
|
|
17
|
+
*/
|
|
18
|
+
export declare function SpecialCharacterPopover({ x, y, recentCharacters, onInsert, onCancel }: SpecialCharacterPopoverProps): import("react/jsx-runtime").JSX.Element;
|