smartrte-react 0.3.5 → 1.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +106 -14
  3. package/dist/adapters/domChecklistCommandBridge.js +1 -1
  4. package/dist/adapters/domCommandBridge.d.ts +2 -2
  5. package/dist/adapters/domCommandBridge.js +2 -2
  6. package/dist/adapters/domInlineAtomCommandBridge.d.ts +10 -9
  7. package/dist/adapters/domInlineAtomCommandBridge.js +154 -50
  8. package/dist/adapters/domInlineImageCommandBridge.d.ts +7 -7
  9. package/dist/adapters/domInlineImageCommandBridge.js +6 -72
  10. package/dist/adapters/domMoveCommandBridge.js +1 -1
  11. package/dist/adapters/domSelectionBridge.d.ts +3 -3
  12. package/dist/adapters/domSmartDocument.d.ts +4 -4
  13. package/dist/adapters/domSmartDocument.js +2 -2
  14. package/dist/adapters/domTableCommandBridge.d.ts +42 -16
  15. package/dist/adapters/domTableCommandBridge.js +203 -173
  16. package/dist/adapters/internalFlags.d.ts +0 -8
  17. package/dist/adapters/internalFlags.js +0 -24
  18. package/dist/adapters/legacyListShadowComparator.d.ts +1 -1
  19. package/dist/adapters/legacyListShadowComparator.js +1 -1
  20. package/dist/adapters/pdfPrint.d.ts +8 -0
  21. package/dist/adapters/pdfPrint.js +22 -0
  22. package/dist/adapters/shadowMode.d.ts +3 -3
  23. package/dist/adapters/shadowMode.js +1 -1
  24. package/dist/canonicalEditorRuntime.d.ts +53 -1
  25. package/dist/canonicalEditorRuntime.js +61 -4
  26. package/dist/capabilityPresets.d.ts +27 -0
  27. package/dist/capabilityPresets.js +21 -0
  28. package/dist/collabTransport.d.ts +66 -0
  29. package/dist/collabTransport.js +1 -0
  30. package/dist/commentProvider.d.ts +24 -0
  31. package/dist/commentProvider.js +1 -0
  32. package/dist/components/CanonicalAuthorityEditor.d.ts +35 -1
  33. package/dist/components/CanonicalAuthorityEditor.js +1086 -112
  34. package/dist/components/ClassicEditorAuthority.d.ts +93 -22
  35. package/dist/components/ClassicEditorAuthority.js +11 -43
  36. package/dist/components/ColorPickerPopover.d.ts +39 -0
  37. package/dist/components/ColorPickerPopover.js +285 -0
  38. package/dist/components/CommentMarkers.d.ts +25 -0
  39. package/dist/components/CommentMarkers.js +105 -0
  40. package/dist/components/CommentThreadPanel.d.ts +26 -0
  41. package/dist/components/CommentThreadPanel.js +47 -0
  42. package/dist/components/ContextMenu.d.ts +23 -0
  43. package/dist/components/ContextMenu.js +141 -0
  44. package/dist/components/FormulaLibraryPopover.d.ts +16 -0
  45. package/dist/components/FormulaLibraryPopover.js +107 -0
  46. package/dist/components/LinkEditorPopover.d.ts +3 -1
  47. package/dist/components/LinkEditorPopover.js +22 -6
  48. package/dist/components/MediaManager.d.ts +4 -20
  49. package/dist/components/MediaManager.js +35 -6
  50. package/dist/components/MediaOverlay.d.ts +36 -0
  51. package/dist/components/MediaOverlay.js +163 -0
  52. package/dist/components/SpecialCharacterPopover.d.ts +18 -0
  53. package/dist/components/SpecialCharacterPopover.js +99 -0
  54. package/dist/components/StructuralSuggestionMarkers.d.ts +20 -0
  55. package/dist/components/StructuralSuggestionMarkers.js +56 -0
  56. package/dist/components/SuggestionPanel.d.ts +26 -0
  57. package/dist/components/SuggestionPanel.js +33 -0
  58. package/dist/components/TableBorderPopover.d.ts +59 -0
  59. package/dist/components/TableBorderPopover.js +147 -0
  60. package/dist/components/TableResizeHandles.d.ts +25 -0
  61. package/dist/components/TableResizeHandles.js +301 -0
  62. package/dist/components/TableSizePickerPopover.d.ts +16 -0
  63. package/dist/components/TableSizePickerPopover.js +126 -0
  64. package/dist/components/ToolbarPrimitives.d.ts +129 -0
  65. package/dist/components/ToolbarPrimitives.js +146 -0
  66. package/dist/components/VersionHistoryPanel.d.ts +9 -0
  67. package/dist/components/VersionHistoryPanel.js +157 -0
  68. package/dist/formulaLibrary.d.ts +31 -0
  69. package/dist/formulaLibrary.js +90 -0
  70. package/dist/index.d.ts +22 -24
  71. package/dist/index.js +14 -11
  72. package/dist/mediaManagerAdapter.d.ts +12 -0
  73. package/dist/mediaManagerAdapter.js +34 -0
  74. package/dist/mediaProvider.d.ts +21 -1
  75. package/dist/specialCharacters.d.ts +22 -0
  76. package/dist/specialCharacters.js +86 -0
  77. package/dist/standalone/classic-editor-embed.js +19 -12
  78. package/dist/suggestionProvider.d.ts +17 -0
  79. package/dist/suggestionProvider.js +1 -0
  80. package/dist/theme.d.ts +1 -1
  81. package/dist/theme.js +133 -1
  82. package/dist/versionProvider.d.ts +29 -0
  83. package/dist/versionProvider.js +1 -0
  84. package/package.json +28 -14
  85. package/dist/adapters/canonicalInlineCommandBridge.d.ts +0 -15
  86. package/dist/adapters/canonicalInlineCommandBridge.js +0 -269
  87. package/dist/adapters/canonicalListCommandBridge.d.ts +0 -26
  88. package/dist/adapters/canonicalListCommandBridge.js +0 -430
  89. package/dist/adapters/coreBold.d.ts +0 -5
  90. package/dist/adapters/coreBold.js +0 -3
  91. package/dist/adapters/documentFormats.d.ts +0 -26
  92. package/dist/adapters/documentFormats.js +0 -49
  93. package/dist/adapters/docxFormat.d.ts +0 -5
  94. package/dist/adapters/docxFormat.js +0 -272
  95. package/dist/adapters/domBlockCommandBridge.d.ts +0 -19
  96. package/dist/adapters/domBlockCommandBridge.js +0 -79
  97. package/dist/adapters/inlineMarkCoreExecution.d.ts +0 -9
  98. package/dist/adapters/inlineMarkCoreExecution.js +0 -25
  99. package/dist/adapters/pdfFormat.d.ts +0 -23
  100. package/dist/adapters/pdfFormat.js +0 -204
  101. package/dist/adapters/portableDocxAtoms.d.ts +0 -4
  102. package/dist/adapters/portableDocxAtoms.js +0 -58
  103. package/dist/adapters/styledDocxFormat.d.ts +0 -8
  104. package/dist/adapters/styledDocxFormat.js +0 -190
  105. package/dist/builtInFormatDefinitions.d.ts +0 -5
  106. package/dist/builtInFormatDefinitions.js +0 -82
  107. package/dist/canonicalAuthorityFlag.d.ts +0 -32
  108. package/dist/canonicalAuthorityFlag.js +0 -49
  109. package/dist/canonicalClipboardRuntime.d.ts +0 -15
  110. package/dist/canonicalClipboardRuntime.js +0 -76
  111. package/dist/components/ClassicEditor.d.ts +0 -76
  112. package/dist/components/ClassicEditor.js +0 -6729
  113. package/dist/editorController.d.ts +0 -78
  114. package/dist/editorController.js +0 -298
  115. package/dist/formatFidelity.d.ts +0 -14
  116. package/dist/formatFidelity.js +0 -107
  117. package/dist/formatRuntime.d.ts +0 -50
  118. package/dist/formatRuntime.js +0 -20
  119. package/dist/pluginRuntime.d.ts +0 -68
  120. package/dist/pluginRuntime.js +0 -78
  121. package/dist/standalone/editor.js +0 -1068
  122. package/dist/test-harness/atomShadowComparator.d.ts +0 -14
  123. package/dist/test-harness/atomShadowComparator.js +0 -63
  124. package/dist/test-harness/blockShadowComparator.d.ts +0 -30
  125. package/dist/test-harness/blockShadowComparator.js +0 -140
  126. package/dist/test-harness/clipboardShadowComparator.d.ts +0 -12
  127. package/dist/test-harness/clipboardShadowComparator.js +0 -46
  128. package/dist/test-harness/inlineShadowComparator.d.ts +0 -32
  129. package/dist/test-harness/inlineShadowComparator.js +0 -152
  130. package/dist/test-harness/legacyAtomEngine.d.ts +0 -10
  131. package/dist/test-harness/legacyAtomEngine.js +0 -15
  132. package/dist/test-harness/legacyBlockEngine.d.ts +0 -31
  133. package/dist/test-harness/legacyBlockEngine.js +0 -31
  134. package/dist/test-harness/legacyClipboardEngine.d.ts +0 -14
  135. package/dist/test-harness/legacyClipboardEngine.js +0 -67
  136. package/dist/test-harness/legacyInlineEngine.d.ts +0 -19
  137. package/dist/test-harness/legacyInlineEngine.js +0 -49
  138. package/dist/test-harness/legacyTableEngine.d.ts +0 -12
  139. package/dist/test-harness/legacyTableEngine.js +0 -25
  140. package/dist/test-harness/tableShadowComparator.d.ts +0 -29
  141. package/dist/test-harness/tableShadowComparator.js +0 -101
@@ -0,0 +1,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;