smartrte-react 0.3.5 → 1.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -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 +133 -1
- 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,163 @@
|
|
|
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 buttonStyle = {
|
|
4
|
+
minHeight: 32,
|
|
5
|
+
padding: "0 10px",
|
|
6
|
+
border: "1px solid var(--srte-input-border)",
|
|
7
|
+
borderRadius: 8,
|
|
8
|
+
background: "var(--srte-input-bg)",
|
|
9
|
+
color: "var(--srte-menu-text)",
|
|
10
|
+
cursor: "pointer",
|
|
11
|
+
fontWeight: 500,
|
|
12
|
+
fontSize: 13,
|
|
13
|
+
};
|
|
14
|
+
const HANDLE_SIZE = 12;
|
|
15
|
+
const MIN_ATOM_SIZE = 24;
|
|
16
|
+
/**
|
|
17
|
+
* Context menu scope reduction: media no longer gets a generic right-click
|
|
18
|
+
* menu - it gets this dedicated overlay, anchored to the atom's own live
|
|
19
|
+
* element rect (the same ResizeObserver/scroll-tracking pattern
|
|
20
|
+
* TableResizeHandles.tsx already uses for anchoring UI to a live element,
|
|
21
|
+
* per §1's investigation) instead of click coordinates, appearing whenever
|
|
22
|
+
* an atom is selected. The caller controls visibility by conditionally
|
|
23
|
+
* rendering this component based on atomSelected - when the selection
|
|
24
|
+
* moves off the atom, the component unmounts on its own, which is the
|
|
25
|
+
* "dismiss when selection moves away" requirement with no extra logic
|
|
26
|
+
* needed here.
|
|
27
|
+
*
|
|
28
|
+
* Post-batch follow-up ("images doesn't have a resizer"): the Resize +/-
|
|
29
|
+
* buttons were the only resize affordance - no direct drag handle existed
|
|
30
|
+
* for atoms, unlike tables. Added a corner drag handle at the atom's own
|
|
31
|
+
* bottom-right corner, reusing TableResizeHandles.tsx's exact live-preview-
|
|
32
|
+
* during-drag / commit-on-release pattern (mutate the real element's style
|
|
33
|
+
* directly while dragging for immediate feedback, commit via a real command
|
|
34
|
+
* only on release) rather than inventing a different interaction model for
|
|
35
|
+
* the second resizable-thing in this codebase.
|
|
36
|
+
*/
|
|
37
|
+
export function MediaOverlay({ atomElement, alt, width, height, src, resizable = true, onEdit, onResize, onResizeTo, onDelete, onDismiss }) {
|
|
38
|
+
const rootRef = useRef(null);
|
|
39
|
+
const handleRef = useRef(null);
|
|
40
|
+
const [placement, setPlacement] = useState(null);
|
|
41
|
+
const [handlePosition, setHandlePosition] = useState(null);
|
|
42
|
+
const dragRef = useRef(null);
|
|
43
|
+
const [dragging, setDragging] = useState(false);
|
|
44
|
+
useLayoutEffect(() => {
|
|
45
|
+
const recompute = () => {
|
|
46
|
+
const rect = atomElement.getBoundingClientRect();
|
|
47
|
+
const margin = 8;
|
|
48
|
+
const viewportWidth = atomElement.ownerDocument.defaultView?.innerWidth ?? 0;
|
|
49
|
+
const viewportHeight = atomElement.ownerDocument.defaultView?.innerHeight ?? 0;
|
|
50
|
+
const panelWidth = rootRef.current?.getBoundingClientRect().width ?? 260;
|
|
51
|
+
const left = Math.min(Math.max(margin, rect.left), Math.max(margin, viewportWidth - panelWidth - margin));
|
|
52
|
+
const overflowsBottom = rect.bottom + 6 + 120 > viewportHeight - margin;
|
|
53
|
+
const top = overflowsBottom ? Math.max(margin, rect.top - 6 - 120) : rect.bottom + 6;
|
|
54
|
+
setPlacement({ left, top });
|
|
55
|
+
setHandlePosition({ left: rect.right - HANDLE_SIZE / 2, top: rect.bottom - HANDLE_SIZE / 2 });
|
|
56
|
+
};
|
|
57
|
+
recompute();
|
|
58
|
+
const observer = new ResizeObserver(recompute);
|
|
59
|
+
observer.observe(atomElement);
|
|
60
|
+
const ownerWindow = atomElement.ownerDocument.defaultView;
|
|
61
|
+
ownerWindow?.addEventListener("scroll", recompute, true);
|
|
62
|
+
return () => {
|
|
63
|
+
observer.disconnect();
|
|
64
|
+
ownerWindow?.removeEventListener("scroll", recompute, true);
|
|
65
|
+
};
|
|
66
|
+
}, [atomElement]);
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
const ownerWindow = atomElement.ownerDocument.defaultView;
|
|
69
|
+
if (!ownerWindow)
|
|
70
|
+
return;
|
|
71
|
+
const handleMove = (event) => {
|
|
72
|
+
const drag = dragRef.current;
|
|
73
|
+
if (!drag)
|
|
74
|
+
return;
|
|
75
|
+
const delta = Math.max(event.clientX - drag.startClientX, event.clientY - drag.startClientY);
|
|
76
|
+
const liveWidth = Math.max(MIN_ATOM_SIZE, Math.round(drag.startWidth + delta));
|
|
77
|
+
const liveHeight = Math.max(MIN_ATOM_SIZE, Math.round(drag.startHeight + (delta * drag.startHeight) / drag.startWidth));
|
|
78
|
+
atomElement.style.width = `${liveWidth}px`;
|
|
79
|
+
atomElement.style.height = `${liveHeight}px`;
|
|
80
|
+
if (handleRef.current) {
|
|
81
|
+
const rect = atomElement.getBoundingClientRect();
|
|
82
|
+
handleRef.current.style.left = `${rect.right - HANDLE_SIZE / 2}px`;
|
|
83
|
+
handleRef.current.style.top = `${rect.bottom - HANDLE_SIZE / 2}px`;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
const handleUp = (event) => {
|
|
87
|
+
const drag = dragRef.current;
|
|
88
|
+
if (!drag)
|
|
89
|
+
return;
|
|
90
|
+
const delta = Math.max(event.clientX - drag.startClientX, event.clientY - drag.startClientY);
|
|
91
|
+
const finalWidth = Math.max(MIN_ATOM_SIZE, Math.round(drag.startWidth + delta));
|
|
92
|
+
const finalHeight = Math.max(MIN_ATOM_SIZE, Math.round(drag.startHeight + (delta * drag.startHeight) / drag.startWidth));
|
|
93
|
+
dragRef.current = null;
|
|
94
|
+
setDragging(false);
|
|
95
|
+
// Committing triggers the renderer's own re-render from authoritative
|
|
96
|
+
// model state, which overwrites the live-preview inline style above -
|
|
97
|
+
// same reasoning as TableResizeHandles.tsx's commit-on-release.
|
|
98
|
+
onResizeTo(finalWidth, finalHeight);
|
|
99
|
+
};
|
|
100
|
+
ownerWindow.addEventListener("pointermove", handleMove);
|
|
101
|
+
ownerWindow.addEventListener("pointerup", handleUp);
|
|
102
|
+
return () => {
|
|
103
|
+
ownerWindow.removeEventListener("pointermove", handleMove);
|
|
104
|
+
ownerWindow.removeEventListener("pointerup", handleUp);
|
|
105
|
+
};
|
|
106
|
+
}, [atomElement, onResizeTo]);
|
|
107
|
+
const onDismissRef = useRef(onDismiss);
|
|
108
|
+
onDismissRef.current = onDismiss;
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
const dismissIfOutside = (event) => {
|
|
111
|
+
const target = event.target;
|
|
112
|
+
if (dragRef.current)
|
|
113
|
+
return;
|
|
114
|
+
if (rootRef.current && !rootRef.current.contains(target) && !atomElement.contains(target) && target !== handleRef.current)
|
|
115
|
+
onDismissRef.current();
|
|
116
|
+
};
|
|
117
|
+
window.addEventListener("pointerdown", dismissIfOutside, true);
|
|
118
|
+
window.addEventListener("mousedown", dismissIfOutside, true);
|
|
119
|
+
return () => {
|
|
120
|
+
window.removeEventListener("pointerdown", dismissIfOutside, true);
|
|
121
|
+
window.removeEventListener("mousedown", dismissIfOutside, true);
|
|
122
|
+
};
|
|
123
|
+
}, [atomElement]);
|
|
124
|
+
return (_jsxs(_Fragment, { children: [resizable && _jsx("div", { ref: handleRef, "data-srte-media-resize-handle": "true", role: "separator", "aria-orientation": "horizontal", "aria-label": "Resize media", onPointerDown: (event) => {
|
|
125
|
+
event.preventDefault();
|
|
126
|
+
const rect = atomElement.getBoundingClientRect();
|
|
127
|
+
dragRef.current = { startClientX: event.clientX, startClientY: event.clientY, startWidth: rect.width, startHeight: rect.height };
|
|
128
|
+
setDragging(true);
|
|
129
|
+
}, style: {
|
|
130
|
+
position: "fixed",
|
|
131
|
+
left: handlePosition?.left ?? 0,
|
|
132
|
+
top: handlePosition?.top ?? 0,
|
|
133
|
+
visibility: handlePosition ? "visible" : "hidden",
|
|
134
|
+
width: HANDLE_SIZE,
|
|
135
|
+
height: HANDLE_SIZE,
|
|
136
|
+
borderRadius: "50%",
|
|
137
|
+
background: dragging ? "var(--srte-primary)" : "var(--srte-menu-bg)",
|
|
138
|
+
border: "2px solid var(--srte-primary)",
|
|
139
|
+
cursor: "nwse-resize",
|
|
140
|
+
zIndex: 71,
|
|
141
|
+
touchAction: "none",
|
|
142
|
+
} }), _jsxs("div", { ref: rootRef, "data-srte-media-overlay": "true", role: "dialog", "aria-label": "Media actions", style: {
|
|
143
|
+
position: "fixed",
|
|
144
|
+
left: placement?.left ?? 0,
|
|
145
|
+
top: placement?.top ?? 0,
|
|
146
|
+
visibility: placement ? "visible" : "hidden",
|
|
147
|
+
zIndex: 70,
|
|
148
|
+
width: 260,
|
|
149
|
+
maxWidth: "calc(100vw - 16px)",
|
|
150
|
+
boxSizing: "border-box",
|
|
151
|
+
background: "var(--srte-menu-bg)",
|
|
152
|
+
color: "var(--srte-menu-text)",
|
|
153
|
+
border: "1px solid var(--srte-border)",
|
|
154
|
+
borderRadius: 10,
|
|
155
|
+
boxShadow: "var(--srte-menu-shadow)",
|
|
156
|
+
padding: 10,
|
|
157
|
+
}, onKeyDown: (event) => {
|
|
158
|
+
if (event.key === "Escape") {
|
|
159
|
+
event.preventDefault();
|
|
160
|
+
onDismiss();
|
|
161
|
+
}
|
|
162
|
+
}, children: [_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: [_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" })] })] })] }));
|
|
163
|
+
}
|
|
@@ -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;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
3
|
+
import { SPECIAL_CHARACTERS, SPECIAL_CHARACTER_CATEGORIES } from "../specialCharacters.js";
|
|
4
|
+
const RECENT_ROW_LIMIT = 8;
|
|
5
|
+
/**
|
|
6
|
+
* Special character picker: six standardized-Unicode-block categories
|
|
7
|
+
* (~200 characters total - see specialCharacters.ts's own doc comment),
|
|
8
|
+
* reusing the grid-with-hover-feedback shape already established by
|
|
9
|
+
* ColorPickerPopover/TableSizePickerPopover rather than a new component
|
|
10
|
+
* pattern. Insertion is immediate on click (a character, unlike a dragged
|
|
11
|
+
* color, is already a single complete choice) via a real, page-owned text
|
|
12
|
+
* insert - see `insertSpecialCharacter` in CanonicalAuthorityEditor.tsx.
|
|
13
|
+
*/
|
|
14
|
+
export function SpecialCharacterPopover({ x, y, recentCharacters, onInsert, onCancel }) {
|
|
15
|
+
const [category, setCategory] = useState("greek");
|
|
16
|
+
const [query, setQuery] = useState("");
|
|
17
|
+
const rootRef = useRef(null);
|
|
18
|
+
const [placement, setPlacement] = useState(null);
|
|
19
|
+
const onCancelRef = useRef(onCancel);
|
|
20
|
+
onCancelRef.current = onCancel;
|
|
21
|
+
useLayoutEffect(() => {
|
|
22
|
+
const el = rootRef.current;
|
|
23
|
+
if (!el)
|
|
24
|
+
return;
|
|
25
|
+
const margin = 8;
|
|
26
|
+
const { width, height } = el.getBoundingClientRect();
|
|
27
|
+
const viewportWidth = window.innerWidth;
|
|
28
|
+
const viewportHeight = window.innerHeight;
|
|
29
|
+
const overflowsRight = x + width > viewportWidth - margin;
|
|
30
|
+
const left = overflowsRight ? x - width : x;
|
|
31
|
+
const clampedLeft = Math.min(Math.max(margin, left), Math.max(margin, viewportWidth - width - margin));
|
|
32
|
+
const overflowsBottom = y + height > viewportHeight - margin;
|
|
33
|
+
const top = overflowsBottom ? y - height : y;
|
|
34
|
+
const clampedTop = Math.min(Math.max(margin, top), Math.max(margin, viewportHeight - margin));
|
|
35
|
+
setPlacement({ left: clampedLeft, top: clampedTop, maxHeight: viewportHeight - clampedTop - margin });
|
|
36
|
+
}, [x, y]);
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
const dismissIfOutside = (event) => {
|
|
39
|
+
if (rootRef.current && !rootRef.current.contains(event.target))
|
|
40
|
+
onCancelRef.current();
|
|
41
|
+
};
|
|
42
|
+
window.addEventListener("pointerdown", dismissIfOutside, true);
|
|
43
|
+
window.addEventListener("mousedown", dismissIfOutside, true);
|
|
44
|
+
return () => {
|
|
45
|
+
window.removeEventListener("pointerdown", dismissIfOutside, true);
|
|
46
|
+
window.removeEventListener("mousedown", dismissIfOutside, true);
|
|
47
|
+
};
|
|
48
|
+
}, []);
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
const onWindowKeyDown = (event) => {
|
|
51
|
+
if (event.key === "Escape")
|
|
52
|
+
onCancelRef.current();
|
|
53
|
+
};
|
|
54
|
+
window.addEventListener("keydown", onWindowKeyDown);
|
|
55
|
+
return () => window.removeEventListener("keydown", onWindowKeyDown);
|
|
56
|
+
}, []);
|
|
57
|
+
const needle = query.trim().toLowerCase();
|
|
58
|
+
const entries = useMemo(() => {
|
|
59
|
+
if (needle)
|
|
60
|
+
return SPECIAL_CHARACTERS.filter((entry) => entry.name.toLowerCase().includes(needle));
|
|
61
|
+
return SPECIAL_CHARACTERS.filter((entry) => entry.category === category);
|
|
62
|
+
}, [category, needle]);
|
|
63
|
+
return (_jsxs("div", { ref: rootRef, "data-srte-special-char-popover": "true", role: "dialog", "aria-label": "Special characters", style: {
|
|
64
|
+
position: "fixed",
|
|
65
|
+
left: placement?.left ?? x,
|
|
66
|
+
top: placement?.top ?? y,
|
|
67
|
+
visibility: placement ? "visible" : "hidden",
|
|
68
|
+
zIndex: 70,
|
|
69
|
+
width: 380,
|
|
70
|
+
maxWidth: "calc(100vw - 16px)",
|
|
71
|
+
maxHeight: placement ? placement.maxHeight : "calc(100vh - 16px)",
|
|
72
|
+
display: "flex",
|
|
73
|
+
flexDirection: "column",
|
|
74
|
+
boxSizing: "border-box",
|
|
75
|
+
background: "var(--srte-menu-bg)",
|
|
76
|
+
color: "var(--srte-menu-text)",
|
|
77
|
+
border: "1px solid var(--srte-border)",
|
|
78
|
+
borderRadius: 12,
|
|
79
|
+
boxShadow: "var(--srte-menu-shadow)",
|
|
80
|
+
overflow: "hidden",
|
|
81
|
+
}, children: [_jsx("div", { style: { padding: "12px 14px 8px", borderBottom: "1px solid var(--srte-border)" }, children: _jsx("input", { "data-srte-special-char-search": "true", value: query, onChange: (event) => setQuery(event.target.value), placeholder: "Search by name\u2026 (e.g. \u201Calpha\u201D)", "aria-label": "Search special characters", autoComplete: "off", style: {
|
|
82
|
+
width: "100%", height: 34, boxSizing: "border-box", padding: "0 10px",
|
|
83
|
+
border: "1px solid var(--srte-input-border)", borderRadius: 8, outline: "none",
|
|
84
|
+
background: "var(--srte-input-bg)", color: "var(--srte-input-text)", font: "inherit", fontSize: 13.5,
|
|
85
|
+
} }) }), _jsxs("div", { style: { display: "flex", flex: 1, minHeight: 0 }, children: [!needle && (_jsx("div", { role: "tablist", "aria-label": "Character category", style: { display: "flex", flexDirection: "column", gap: 2, padding: 8, borderRight: "1px solid var(--srte-border)", minWidth: 108 }, children: SPECIAL_CHARACTER_CATEGORIES.map((entry) => (_jsx("button", { type: "button", role: "tab", "aria-selected": category === entry.id, onMouseDown: (event) => event.preventDefault(), onClick: () => setCategory(entry.id), style: {
|
|
86
|
+
fontSize: 12.5, fontWeight: 600, textAlign: "left", padding: "7px 9px", borderRadius: 7,
|
|
87
|
+
border: "none", cursor: "pointer",
|
|
88
|
+
background: category === entry.id ? "var(--srte-accent-bg)" : "transparent",
|
|
89
|
+
color: category === entry.id ? "var(--srte-primary)" : "var(--srte-menu-text)",
|
|
90
|
+
}, children: entry.label }, entry.id))) })), _jsx("div", { style: { flex: 1, overflowY: "auto" }, children: _jsxs("div", { "data-srte-special-char-grid": "true", style: { display: "grid", gridTemplateColumns: "repeat(8, 1fr)", gap: 6, padding: 12 }, children: [entries.map((entry) => (_jsx("button", { type: "button", "data-srte-special-char": entry.char, "aria-label": entry.name, title: entry.name, onMouseDown: (event) => event.preventDefault(), onClick: () => onInsert(entry.char), style: {
|
|
91
|
+
aspectRatio: "1", display: "flex", alignItems: "center", justifyContent: "center",
|
|
92
|
+
fontSize: 16, border: "1px solid var(--srte-input-border)", borderRadius: 7,
|
|
93
|
+
background: "var(--srte-input-bg)", color: "var(--srte-input-text)", cursor: "pointer",
|
|
94
|
+
}, children: entry.char }, `${entry.category}-${entry.char}`))), entries.length === 0 && (_jsxs("div", { style: { gridColumn: "1 / -1", fontSize: 13, opacity: 0.7, padding: "8px 2px" }, children: ["No characters match \u201C", query, "\u201D."] }))] }) })] }), recentCharacters && recentCharacters.length > 0 && (_jsxs("div", { style: { display: "flex", alignItems: "center", gap: 6, padding: "8px 12px", borderTop: "1px solid var(--srte-border)" }, children: [_jsx("span", { style: { fontSize: 10.5, fontWeight: 700, textTransform: "uppercase", letterSpacing: "0.04em", color: "var(--srte-menu-text)", opacity: 0.7, marginRight: 2 }, children: "Recent" }), recentCharacters.slice(0, RECENT_ROW_LIMIT).map((char, index) => (_jsx("button", { type: "button", "data-srte-recent-special-char": char, "aria-label": `Recently used ${char}`, onMouseDown: (event) => event.preventDefault(), onClick: () => onInsert(char), style: {
|
|
95
|
+
width: 26, height: 26, display: "flex", alignItems: "center", justifyContent: "center",
|
|
96
|
+
fontSize: 14, border: "1px solid var(--srte-input-border)", borderRadius: 6,
|
|
97
|
+
background: "var(--srte-input-bg)", color: "var(--srte-input-text)", cursor: "pointer",
|
|
98
|
+
}, children: char }, `${char}-${index}`)))] }))] }));
|
|
99
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ModelDomMapping, PositionLookup, StructuralSuggestion } from "smartrte-core/foundation";
|
|
2
|
+
export interface StructuralSuggestionMarkersProps {
|
|
3
|
+
positions: PositionLookup;
|
|
4
|
+
mapping: ModelDomMapping;
|
|
5
|
+
containerElement: HTMLElement;
|
|
6
|
+
suggestions: readonly StructuralSuggestion[];
|
|
7
|
+
activeSuggestionId: string | null;
|
|
8
|
+
onSelectSuggestion: (id: string) => void;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Live overlay for structural (whole-node) suggestions, the AnnotationRange
|
|
12
|
+
* half of Phase 12a §2.3's hybrid architecture. Simpler than CommentMarkers:
|
|
13
|
+
* a structural suggestion always anchors to exactly one whole node, so this
|
|
14
|
+
* measures that single element's own rect directly via mapping.nodeToDom
|
|
15
|
+
* rather than resolving a Range across DOM text positions. Re-measures on
|
|
16
|
+
* the same ResizeObserver/MutationObserver/scroll schedule as
|
|
17
|
+
* CommentMarkers/TableResizeHandles, for the same reason: live DOM layout
|
|
18
|
+
* changes independently of React re-renders.
|
|
19
|
+
*/
|
|
20
|
+
export declare function StructuralSuggestionMarkers({ positions, mapping, containerElement, suggestions, activeSuggestionId, onSelectSuggestion }: StructuralSuggestionMarkersProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useState } from "react";
|
|
3
|
+
import { resolveAnnotationRange } from "smartrte-core/foundation";
|
|
4
|
+
/**
|
|
5
|
+
* Live overlay for structural (whole-node) suggestions, the AnnotationRange
|
|
6
|
+
* half of Phase 12a §2.3's hybrid architecture. Simpler than CommentMarkers:
|
|
7
|
+
* a structural suggestion always anchors to exactly one whole node, so this
|
|
8
|
+
* measures that single element's own rect directly via mapping.nodeToDom
|
|
9
|
+
* rather than resolving a Range across DOM text positions. Re-measures on
|
|
10
|
+
* the same ResizeObserver/MutationObserver/scroll schedule as
|
|
11
|
+
* CommentMarkers/TableResizeHandles, for the same reason: live DOM layout
|
|
12
|
+
* changes independently of React re-renders.
|
|
13
|
+
*/
|
|
14
|
+
export function StructuralSuggestionMarkers({ positions, mapping, containerElement, suggestions, activeSuggestionId, onSelectSuggestion }) {
|
|
15
|
+
const [rects, setRects] = useState([]);
|
|
16
|
+
const recompute = useCallback(() => {
|
|
17
|
+
if (!suggestions.length) {
|
|
18
|
+
setRects([]);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const next = [];
|
|
22
|
+
suggestions.forEach((suggestion) => {
|
|
23
|
+
if (!resolveAnnotationRange(suggestion.range, positions))
|
|
24
|
+
return;
|
|
25
|
+
const element = mapping.nodeToDom(suggestion.range.startId);
|
|
26
|
+
if (!element)
|
|
27
|
+
return;
|
|
28
|
+
const rect = element.getBoundingClientRect();
|
|
29
|
+
next.push({ id: suggestion.id, left: rect.left, top: rect.top, width: rect.width, height: rect.height });
|
|
30
|
+
});
|
|
31
|
+
setRects(next);
|
|
32
|
+
}, [positions, mapping, suggestions]);
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
recompute();
|
|
35
|
+
const observer = new ResizeObserver(recompute);
|
|
36
|
+
observer.observe(containerElement);
|
|
37
|
+
const mutationObserver = new MutationObserver(recompute);
|
|
38
|
+
mutationObserver.observe(containerElement, { childList: true, subtree: true, characterData: true });
|
|
39
|
+
const ownerWindow = containerElement.ownerDocument.defaultView;
|
|
40
|
+
ownerWindow?.addEventListener("scroll", recompute, true);
|
|
41
|
+
ownerWindow?.addEventListener("resize", recompute);
|
|
42
|
+
return () => {
|
|
43
|
+
observer.disconnect();
|
|
44
|
+
mutationObserver.disconnect();
|
|
45
|
+
ownerWindow?.removeEventListener("scroll", recompute, true);
|
|
46
|
+
ownerWindow?.removeEventListener("resize", recompute);
|
|
47
|
+
};
|
|
48
|
+
}, [containerElement, recompute]);
|
|
49
|
+
return (_jsx("div", { "data-srte-suggestion-markers": "true", style: { position: "fixed", inset: 0, pointerEvents: "none", zIndex: 55 }, children: rects.map((rect) => (_jsx("div", { "data-srte-suggestion-highlight": rect.id, onPointerDown: () => onSelectSuggestion(rect.id), style: {
|
|
50
|
+
position: "fixed", left: rect.left, top: rect.top, width: rect.width, height: rect.height,
|
|
51
|
+
background: activeSuggestionId === rect.id ? "var(--srte-suggestion-highlight-active, rgba(220, 60, 60, 0.28))" : "var(--srte-suggestion-highlight, rgba(220, 60, 60, 0.14))",
|
|
52
|
+
borderLeft: "3px solid var(--srte-suggestion-border, #dc3c3c)",
|
|
53
|
+
cursor: "pointer",
|
|
54
|
+
pointerEvents: "auto",
|
|
55
|
+
} }, rect.id))) }));
|
|
56
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { SmartDocument, StructuralSuggestion } from "smartrte-core/foundation";
|
|
2
|
+
export interface SuggestionPanelProps {
|
|
3
|
+
open: boolean;
|
|
4
|
+
onClose: () => void;
|
|
5
|
+
document: SmartDocument;
|
|
6
|
+
structuralSuggestions: readonly StructuralSuggestion[];
|
|
7
|
+
activeId: string | null;
|
|
8
|
+
onSelect: (id: string) => void;
|
|
9
|
+
/** Set when "Suggest insertion" was just clicked at a real caret position - renders a compose box for the proposed text. */
|
|
10
|
+
pendingInsert: boolean;
|
|
11
|
+
onSubmitInsert: (text: string) => void;
|
|
12
|
+
onAcceptInline: (id: string) => void;
|
|
13
|
+
onRejectInline: (id: string) => void;
|
|
14
|
+
onAcceptStructural: (suggestion: StructuralSuggestion) => void;
|
|
15
|
+
onRejectStructural: (suggestion: StructuralSuggestion) => void;
|
|
16
|
+
busyId: string | null;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Docked side panel listing every pending suggestion - both inline
|
|
20
|
+
* (discovered fresh each render via listInlineSuggestions, since those
|
|
21
|
+
* live as marks inside `document` itself, not a separate list the way
|
|
22
|
+
* comments/versions do) and structural (an explicit prop, since those are
|
|
23
|
+
* an out-of-band record like CommentThread). Non-modal, same reasoning as
|
|
24
|
+
* CommentThreadPanel: reviewing shouldn't block continuing to edit nearby.
|
|
25
|
+
*/
|
|
26
|
+
export declare function SuggestionPanel(props: SuggestionPanelProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { listInlineSuggestions } from "smartrte-core/foundation";
|
|
4
|
+
const formatTimestamp = (createdAt) => new Date(createdAt).toLocaleString();
|
|
5
|
+
/**
|
|
6
|
+
* Docked side panel listing every pending suggestion - both inline
|
|
7
|
+
* (discovered fresh each render via listInlineSuggestions, since those
|
|
8
|
+
* live as marks inside `document` itself, not a separate list the way
|
|
9
|
+
* comments/versions do) and structural (an explicit prop, since those are
|
|
10
|
+
* an out-of-band record like CommentThread). Non-modal, same reasoning as
|
|
11
|
+
* CommentThreadPanel: reviewing shouldn't block continuing to edit nearby.
|
|
12
|
+
*/
|
|
13
|
+
export function SuggestionPanel(props) {
|
|
14
|
+
const { open, onClose, document, structuralSuggestions, activeId, onSelect, pendingInsert, onSubmitInsert, onAcceptInline, onRejectInline, onAcceptStructural, onRejectStructural, busyId, } = props;
|
|
15
|
+
const [draft, setDraft] = useState("");
|
|
16
|
+
if (!open)
|
|
17
|
+
return null;
|
|
18
|
+
const inline = listInlineSuggestions(document);
|
|
19
|
+
const submitInsert = () => {
|
|
20
|
+
const text = draft.trim();
|
|
21
|
+
if (!text)
|
|
22
|
+
return;
|
|
23
|
+
onSubmitInsert(text);
|
|
24
|
+
setDraft("");
|
|
25
|
+
};
|
|
26
|
+
const isEmpty = inline.length === 0 && structuralSuggestions.length === 0;
|
|
27
|
+
return (_jsxs("div", { role: "complementary", "aria-label": "Suggestions", "data-srte-suggestion-panel": "true", style: {
|
|
28
|
+
position: "fixed", top: 0, right: 0, bottom: 0, width: 320, maxWidth: "90vw",
|
|
29
|
+
background: "var(--srte-modal-bg)", color: "var(--srte-modal-text)",
|
|
30
|
+
borderLeft: "1px solid var(--srte-border-light)", boxShadow: "var(--srte-menu-shadow)",
|
|
31
|
+
display: "flex", flexDirection: "column", zIndex: 70,
|
|
32
|
+
}, 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: "Suggestions" }), _jsx("button", { type: "button", "aria-label": "Close suggestions", onClick: onClose, children: "\u2715" })] }), pendingInsert && (_jsxs("div", { "data-srte-suggestion-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: "Suggest inserting\u2026", rows: 2, 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", marginTop: 6 }, children: _jsx("button", { type: "button", "data-srte-suggestion-compose-submit": "true", disabled: !draft.trim(), onClick: submitInsert, children: "Suggest" }) })] })), _jsxs("div", { style: { flex: 1, overflowY: "auto", padding: "0 14px 14px" }, children: [isEmpty && _jsx("div", { style: { color: "var(--srte-text-muted)", padding: "12px 0" }, children: "No pending suggestions." }), inline.map((entry) => (_jsxs("div", { "data-srte-suggestion-entry": entry.id, onClick: () => onSelect(entry.id), style: { padding: "10px 0", borderBottom: "1px solid var(--srte-border-light)", outline: activeId === entry.id ? "2px solid var(--srte-primary)" : "none" }, children: [_jsxs("div", { style: { fontSize: 12, fontWeight: 600 }, children: [entry.authorId, " suggests ", entry.kind === "insert" ? "inserting" : "deleting"] }), _jsxs("div", { style: { fontSize: 13, whiteSpace: "pre-wrap" }, children: ["\"", entry.text, "\""] }), _jsx("div", { style: { fontSize: 11, color: "var(--srte-text-muted)" }, children: formatTimestamp(entry.createdAt) }), _jsxs("div", { style: { display: "flex", gap: 8, marginTop: 6 }, children: [_jsx("button", { type: "button", "data-srte-suggestion-accept": entry.id, disabled: busyId === entry.id, onClick: (event) => { event.stopPropagation(); onAcceptInline(entry.id); }, children: "Accept" }), _jsx("button", { type: "button", "data-srte-suggestion-reject": entry.id, disabled: busyId === entry.id, onClick: (event) => { event.stopPropagation(); onRejectInline(entry.id); }, style: { color: "var(--srte-danger)" }, children: "Reject" })] })] }, entry.id))), structuralSuggestions.map((suggestion) => (_jsxs("div", { "data-srte-suggestion-entry": suggestion.id, onClick: () => onSelect(suggestion.id), style: { padding: "10px 0", borderBottom: "1px solid var(--srte-border-light)", outline: activeId === suggestion.id ? "2px solid var(--srte-primary)" : "none" }, children: [_jsxs("div", { style: { fontSize: 12, fontWeight: 600 }, children: [suggestion.authorId, " suggests removing this block"] }), _jsx("div", { style: { fontSize: 11, color: "var(--srte-text-muted)" }, children: formatTimestamp(suggestion.createdAt) }), _jsxs("div", { style: { display: "flex", gap: 8, marginTop: 6 }, children: [_jsx("button", { type: "button", "data-srte-suggestion-accept": suggestion.id, disabled: busyId === suggestion.id, onClick: (event) => { event.stopPropagation(); onAcceptStructural(suggestion); }, children: "Accept" }), _jsx("button", { type: "button", "data-srte-suggestion-reject": suggestion.id, disabled: busyId === suggestion.id, onClick: (event) => { event.stopPropagation(); onRejectStructural(suggestion); }, style: { color: "var(--srte-danger)" }, children: "Reject" })] })] }, suggestion.id)))] })] }));
|
|
33
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export type BorderStyle = "solid" | "dashed" | "dotted";
|
|
2
|
+
export interface BorderSides {
|
|
3
|
+
top: boolean;
|
|
4
|
+
right: boolean;
|
|
5
|
+
bottom: boolean;
|
|
6
|
+
left: boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface BorderDraft {
|
|
9
|
+
sides: BorderSides;
|
|
10
|
+
style: BorderStyle;
|
|
11
|
+
widthPx: number;
|
|
12
|
+
hex: string;
|
|
13
|
+
}
|
|
14
|
+
export interface TableBorderPopoverProps {
|
|
15
|
+
x: number;
|
|
16
|
+
y: number;
|
|
17
|
+
initial: BorderDraft;
|
|
18
|
+
/** Up to 4 previously-committed border colours, most-recent first - same pattern as the other colour buckets. */
|
|
19
|
+
recentColors?: readonly string[];
|
|
20
|
+
/** Fires on every change (a side toggled, style/width picked, or a colour drag frame) - the caller live-previews it on the real cell(s), same pattern as the plain colour picker's onPreview. */
|
|
21
|
+
onPreview: (draft: BorderDraft) => void;
|
|
22
|
+
/** Fires once, only from the explicit Apply button - unlike ColorPickerPopover, this composite dialog (four independent fields assembled together) commits on a deliberate action, not on any dismissal. */
|
|
23
|
+
onApply: (draft: BorderDraft) => void;
|
|
24
|
+
/** Fires from Cancel, Escape, or an outside click - reverts to `initial`. */
|
|
25
|
+
onCancel: () => void;
|
|
26
|
+
}
|
|
27
|
+
export declare const BORDER_WIDTH_PRESETS: readonly [{
|
|
28
|
+
readonly id: "thin";
|
|
29
|
+
readonly label: "Thin";
|
|
30
|
+
readonly px: 2;
|
|
31
|
+
}, {
|
|
32
|
+
readonly id: "medium";
|
|
33
|
+
readonly label: "Medium";
|
|
34
|
+
readonly px: 3;
|
|
35
|
+
}, {
|
|
36
|
+
readonly id: "thick";
|
|
37
|
+
readonly label: "Thick";
|
|
38
|
+
readonly px: 4;
|
|
39
|
+
}];
|
|
40
|
+
/**
|
|
41
|
+
* "Border options" - one popover combining every dimension of a cell
|
|
42
|
+
* border, replacing what used to be a separate toolbar-dropdown width
|
|
43
|
+
* <select> and a standalone "Border colour" item. table_cell.attrs only
|
|
44
|
+
* ever stored ONE uniform border for all four sides (`borders`) - "which
|
|
45
|
+
* sides get a border" (per-cell top/right/bottom/left) needed new per-side
|
|
46
|
+
* schema attrs (borderTop/Right/Bottom/Left), added specifically to back
|
|
47
|
+
* this control (see docs/bugs/table-cell-border-color-width-no-ui.md's
|
|
48
|
+
* addendum). One style/width/colour choice applies to whichever sides are
|
|
49
|
+
* toggled on here, rather than letting all four vary independently - real
|
|
50
|
+
* border dialogs (Word, Google Docs) default to this simpler mode too, and
|
|
51
|
+
* a fully independent 4-way style/width/colour matrix wasn't asked for.
|
|
52
|
+
*
|
|
53
|
+
* The colour swatch opens the existing ColorPickerPopover completely
|
|
54
|
+
* unmodified, as a nested popover anchored to the swatch - the dismiss/
|
|
55
|
+
* Escape handling below explicitly accounts for it being a DOM sibling
|
|
56
|
+
* (both are position:fixed overlays, so `rootRef.contains()` alone would
|
|
57
|
+
* never recognize a click inside it as "inside" this popover).
|
|
58
|
+
*/
|
|
59
|
+
export declare function TableBorderPopover({ x, y, initial, recentColors, onPreview, onApply, onCancel }: TableBorderPopoverProps): import("react/jsx-runtime").JSX.Element;
|