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.
Files changed (147) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +205 -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 +65 -1
  25. package/dist/canonicalEditorRuntime.js +63 -5
  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 +56 -1
  33. package/dist/components/CanonicalAuthorityEditor.js +1304 -116
  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 +287 -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/MediaDetailsPopover.d.ts +35 -0
  49. package/dist/components/MediaDetailsPopover.js +136 -0
  50. package/dist/components/MediaManager.d.ts +4 -20
  51. package/dist/components/MediaManager.js +40 -11
  52. package/dist/components/MediaOverlay.d.ts +27 -0
  53. package/dist/components/MediaOverlay.js +228 -0
  54. package/dist/components/SpecialCharacterPopover.d.ts +18 -0
  55. package/dist/components/SpecialCharacterPopover.js +99 -0
  56. package/dist/components/StructuralSuggestionMarkers.d.ts +20 -0
  57. package/dist/components/StructuralSuggestionMarkers.js +56 -0
  58. package/dist/components/SuggestionPanel.d.ts +26 -0
  59. package/dist/components/SuggestionPanel.js +33 -0
  60. package/dist/components/TableBorderPopover.d.ts +59 -0
  61. package/dist/components/TableBorderPopover.js +149 -0
  62. package/dist/components/TableResizeHandles.d.ts +25 -0
  63. package/dist/components/TableResizeHandles.js +323 -0
  64. package/dist/components/TableSizePickerPopover.d.ts +16 -0
  65. package/dist/components/TableSizePickerPopover.js +126 -0
  66. package/dist/components/ToolbarPrimitives.d.ts +161 -0
  67. package/dist/components/ToolbarPrimitives.js +237 -0
  68. package/dist/components/VersionHistoryPanel.d.ts +9 -0
  69. package/dist/components/VersionHistoryPanel.js +157 -0
  70. package/dist/components/fixedPositioning.d.ts +25 -0
  71. package/dist/components/fixedPositioning.js +42 -0
  72. package/dist/formulaLibrary.d.ts +31 -0
  73. package/dist/formulaLibrary.js +90 -0
  74. package/dist/index.d.ts +25 -24
  75. package/dist/index.js +15 -11
  76. package/dist/mediaManagerAdapter.d.ts +12 -0
  77. package/dist/mediaManagerAdapter.js +34 -0
  78. package/dist/mediaProvider.d.ts +21 -1
  79. package/dist/specialCharacters.d.ts +22 -0
  80. package/dist/specialCharacters.js +86 -0
  81. package/dist/standalone/classic-editor-embed.js +19 -12
  82. package/dist/suggestionProvider.d.ts +17 -0
  83. package/dist/suggestionProvider.js +1 -0
  84. package/dist/theme.d.ts +1 -1
  85. package/dist/theme.js +219 -30
  86. package/dist/toolbarTools.d.ts +95 -0
  87. package/dist/toolbarTools.js +17 -0
  88. package/dist/versionProvider.d.ts +29 -0
  89. package/dist/versionProvider.js +1 -0
  90. package/package.json +28 -14
  91. package/dist/adapters/canonicalInlineCommandBridge.d.ts +0 -15
  92. package/dist/adapters/canonicalInlineCommandBridge.js +0 -269
  93. package/dist/adapters/canonicalListCommandBridge.d.ts +0 -26
  94. package/dist/adapters/canonicalListCommandBridge.js +0 -430
  95. package/dist/adapters/coreBold.d.ts +0 -5
  96. package/dist/adapters/coreBold.js +0 -3
  97. package/dist/adapters/documentFormats.d.ts +0 -26
  98. package/dist/adapters/documentFormats.js +0 -49
  99. package/dist/adapters/docxFormat.d.ts +0 -5
  100. package/dist/adapters/docxFormat.js +0 -272
  101. package/dist/adapters/domBlockCommandBridge.d.ts +0 -19
  102. package/dist/adapters/domBlockCommandBridge.js +0 -79
  103. package/dist/adapters/inlineMarkCoreExecution.d.ts +0 -9
  104. package/dist/adapters/inlineMarkCoreExecution.js +0 -25
  105. package/dist/adapters/pdfFormat.d.ts +0 -23
  106. package/dist/adapters/pdfFormat.js +0 -204
  107. package/dist/adapters/portableDocxAtoms.d.ts +0 -4
  108. package/dist/adapters/portableDocxAtoms.js +0 -58
  109. package/dist/adapters/styledDocxFormat.d.ts +0 -8
  110. package/dist/adapters/styledDocxFormat.js +0 -190
  111. package/dist/builtInFormatDefinitions.d.ts +0 -5
  112. package/dist/builtInFormatDefinitions.js +0 -82
  113. package/dist/canonicalAuthorityFlag.d.ts +0 -32
  114. package/dist/canonicalAuthorityFlag.js +0 -49
  115. package/dist/canonicalClipboardRuntime.d.ts +0 -15
  116. package/dist/canonicalClipboardRuntime.js +0 -76
  117. package/dist/components/ClassicEditor.d.ts +0 -76
  118. package/dist/components/ClassicEditor.js +0 -6729
  119. package/dist/editorController.d.ts +0 -78
  120. package/dist/editorController.js +0 -298
  121. package/dist/formatFidelity.d.ts +0 -14
  122. package/dist/formatFidelity.js +0 -107
  123. package/dist/formatRuntime.d.ts +0 -50
  124. package/dist/formatRuntime.js +0 -20
  125. package/dist/pluginRuntime.d.ts +0 -68
  126. package/dist/pluginRuntime.js +0 -78
  127. package/dist/standalone/editor.js +0 -1068
  128. package/dist/test-harness/atomShadowComparator.d.ts +0 -14
  129. package/dist/test-harness/atomShadowComparator.js +0 -63
  130. package/dist/test-harness/blockShadowComparator.d.ts +0 -30
  131. package/dist/test-harness/blockShadowComparator.js +0 -140
  132. package/dist/test-harness/clipboardShadowComparator.d.ts +0 -12
  133. package/dist/test-harness/clipboardShadowComparator.js +0 -46
  134. package/dist/test-harness/inlineShadowComparator.d.ts +0 -32
  135. package/dist/test-harness/inlineShadowComparator.js +0 -152
  136. package/dist/test-harness/legacyAtomEngine.d.ts +0 -10
  137. package/dist/test-harness/legacyAtomEngine.js +0 -15
  138. package/dist/test-harness/legacyBlockEngine.d.ts +0 -31
  139. package/dist/test-harness/legacyBlockEngine.js +0 -31
  140. package/dist/test-harness/legacyClipboardEngine.d.ts +0 -14
  141. package/dist/test-harness/legacyClipboardEngine.js +0 -67
  142. package/dist/test-harness/legacyInlineEngine.d.ts +0 -19
  143. package/dist/test-harness/legacyInlineEngine.js +0 -49
  144. package/dist/test-harness/legacyTableEngine.d.ts +0 -12
  145. package/dist/test-harness/legacyTableEngine.js +0 -25
  146. package/dist/test-harness/tableShadowComparator.d.ts +0 -29
  147. package/dist/test-harness/tableShadowComparator.js +0 -101
@@ -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;
@@ -0,0 +1,149 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useLayoutEffect, useRef, useState } from "react";
3
+ import { ColorPickerPopover } from "./ColorPickerPopover.js";
4
+ import { getFixedPositioningOrigin } from "./fixedPositioning.js";
5
+ // Widths start at 2px, not 1px - table_cell's border-collapse table (see
6
+ // theme.ts's th/td rule) already renders a 1px ambient default gridline on
7
+ // every cell. A custom border tied at 1px loses CSS border-collapse's
8
+ // conflict resolution on shared edges half the time (whichever side faces
9
+ // an earlier-in-table-order neighbor keeps the neighbor's default border,
10
+ // confirmed empirically: 1px, and even a fractional 1.5px, lost this way in
11
+ // every engine tested) - only a width strictly greater than the ambient
12
+ // default reliably wins on every side regardless of table position. 2px is
13
+ // the thinnest value that's actually renderable as a real, always-visible
14
+ // custom border under this constraint - see
15
+ // docs/bugs/table-cell-border-thin-preset-loses-border-collapse-tie.md.
16
+ export const BORDER_WIDTH_PRESETS = [
17
+ { id: "thin", label: "Thin", px: 2 },
18
+ { id: "medium", label: "Medium", px: 3 },
19
+ { id: "thick", label: "Thick", px: 4 },
20
+ ];
21
+ const STYLE_OPTIONS = ["solid", "dashed", "dotted"];
22
+ const buttonStyle = {
23
+ minHeight: 32, padding: "0 10px", border: "1px solid var(--srte-input-border)", borderRadius: 8,
24
+ background: "var(--srte-input-bg)", color: "var(--srte-menu-text)", cursor: "pointer", fontWeight: 500, fontSize: 13,
25
+ };
26
+ const selectStyle = {
27
+ width: "100%", height: 32, boxSizing: "border-box", padding: "0 8px", borderRadius: 8,
28
+ border: "1px solid var(--srte-input-border)", background: "var(--srte-input-bg)", color: "var(--srte-input-text)",
29
+ font: "inherit", fontSize: 13, cursor: "pointer",
30
+ };
31
+ const fieldLabelStyle = { fontSize: 11, fontWeight: 700, textTransform: "uppercase", letterSpacing: "0.04em", opacity: 0.7, marginBottom: 4 };
32
+ const SIDE_BAR_LENGTH = 40;
33
+ const SIDE_BAR_THICKNESS = 8;
34
+ const SIDE_BOX = 64;
35
+ /**
36
+ * "Border options" - one popover combining every dimension of a cell
37
+ * border, replacing what used to be a separate toolbar-dropdown width
38
+ * <select> and a standalone "Border colour" item. table_cell.attrs only
39
+ * ever stored ONE uniform border for all four sides (`borders`) - "which
40
+ * sides get a border" (per-cell top/right/bottom/left) needed new per-side
41
+ * schema attrs (borderTop/Right/Bottom/Left), added specifically to back
42
+ * this control (see docs/bugs/table-cell-border-color-width-no-ui.md's
43
+ * addendum). One style/width/colour choice applies to whichever sides are
44
+ * toggled on here, rather than letting all four vary independently - real
45
+ * border dialogs (Word, Google Docs) default to this simpler mode too, and
46
+ * a fully independent 4-way style/width/colour matrix wasn't asked for.
47
+ *
48
+ * The colour swatch opens the existing ColorPickerPopover completely
49
+ * unmodified, as a nested popover anchored to the swatch - the dismiss/
50
+ * Escape handling below explicitly accounts for it being a DOM sibling
51
+ * (both are position:fixed overlays, so `rootRef.contains()` alone would
52
+ * never recognize a click inside it as "inside" this popover).
53
+ */
54
+ export function TableBorderPopover({ x, y, initial, recentColors, onPreview, onApply, onCancel }) {
55
+ const [draft, setDraft] = useState(initial);
56
+ const [colorPickerAnchor, setColorPickerAnchor] = useState(null);
57
+ const hexBeforeNestedPickerRef = useRef(initial.hex);
58
+ const rootRef = useRef(null);
59
+ const swatchRef = useRef(null);
60
+ const [placement, setPlacement] = useState(null);
61
+ const onCancelRef = useRef(onCancel);
62
+ onCancelRef.current = onCancel;
63
+ const onPreviewRef = useRef(onPreview);
64
+ onPreviewRef.current = onPreview;
65
+ useLayoutEffect(() => {
66
+ const el = rootRef.current;
67
+ if (!el)
68
+ return;
69
+ const margin = 8;
70
+ const { width, height } = el.getBoundingClientRect();
71
+ const viewportWidth = window.innerWidth;
72
+ const viewportHeight = window.innerHeight;
73
+ const overflowsRight = x + width > viewportWidth - margin;
74
+ const left = overflowsRight ? x - width : x;
75
+ const clampedLeft = Math.min(Math.max(margin, left), Math.max(margin, viewportWidth - width - margin));
76
+ const overflowsBottom = y + height > viewportHeight - margin;
77
+ const top = overflowsBottom ? y - height : y;
78
+ const clampedTop = Math.min(Math.max(margin, top), Math.max(margin, viewportHeight - margin));
79
+ const origin = getFixedPositioningOrigin(el);
80
+ setPlacement({ left: clampedLeft - origin.left, top: clampedTop - origin.top });
81
+ }, [x, y]);
82
+ useEffect(() => {
83
+ const dismissIfOutside = (event) => {
84
+ const target = event.target;
85
+ if (rootRef.current?.contains(target))
86
+ return;
87
+ // The nested colour picker is a DOM sibling (both are position:fixed
88
+ // overlays), not a descendant of rootRef - without this, clicking
89
+ // inside it would look like an "outside" click on this popover.
90
+ if (colorPickerAnchor && document.querySelector('[data-srte-color-popover="true"]')?.contains(target))
91
+ return;
92
+ onCancelRef.current();
93
+ };
94
+ window.addEventListener("pointerdown", dismissIfOutside, true);
95
+ window.addEventListener("mousedown", dismissIfOutside, true);
96
+ return () => {
97
+ window.removeEventListener("pointerdown", dismissIfOutside, true);
98
+ window.removeEventListener("mousedown", dismissIfOutside, true);
99
+ };
100
+ }, [colorPickerAnchor]);
101
+ useEffect(() => {
102
+ const onWindowKeyDown = (event) => {
103
+ if (event.key !== "Escape")
104
+ return;
105
+ // While the nested colour picker is open, let its own Escape handler
106
+ // commit/dismiss just that picker first - a second Escape closes this
107
+ // whole dialog.
108
+ if (colorPickerAnchor)
109
+ return;
110
+ onCancelRef.current();
111
+ };
112
+ window.addEventListener("keydown", onWindowKeyDown);
113
+ return () => window.removeEventListener("keydown", onWindowKeyDown);
114
+ }, [colorPickerAnchor]);
115
+ const update = (patch) => {
116
+ const next = { ...draft, ...patch };
117
+ setDraft(next);
118
+ onPreviewRef.current(next);
119
+ };
120
+ const toggleSide = (side) => update({ sides: { ...draft.sides, [side]: !draft.sides[side] } });
121
+ const edgeButton = (side, style, label) => (_jsx("button", { type: "button", "aria-label": label, "aria-pressed": draft.sides[side], onMouseDown: (event) => event.preventDefault(), onClick: () => toggleSide(side), style: {
122
+ position: "absolute", border: "none", borderRadius: 3, cursor: "pointer", padding: 0,
123
+ background: draft.sides[side] ? "var(--srte-primary)" : "var(--srte-input-border)",
124
+ ...style,
125
+ } }));
126
+ return (_jsxs("div", { ref: rootRef, "data-srte-table-border-popover": "true", role: "dialog", "aria-label": "Border options", style: {
127
+ position: "fixed",
128
+ left: placement?.left ?? x,
129
+ top: placement?.top ?? y,
130
+ visibility: placement ? "visible" : "hidden",
131
+ zIndex: 70,
132
+ width: 240,
133
+ boxSizing: "border-box",
134
+ background: "var(--srte-menu-bg)",
135
+ color: "var(--srte-menu-text)",
136
+ border: "1px solid var(--srte-border)",
137
+ borderRadius: 12,
138
+ boxShadow: "var(--srte-menu-shadow)",
139
+ padding: 14,
140
+ display: "flex",
141
+ flexDirection: "column",
142
+ gap: 12,
143
+ }, children: [_jsx("div", { style: { fontWeight: 650 }, children: "Border options" }), _jsxs("div", { style: { display: "flex", gap: 14, alignItems: "center" }, children: [_jsxs("div", { children: [_jsx("div", { style: fieldLabelStyle, children: "Sides" }), _jsxs("div", { style: { position: "relative", width: SIDE_BOX, height: SIDE_BOX, border: "1px dashed var(--srte-border)", borderRadius: 6 }, children: [edgeButton("top", { top: -SIDE_BAR_THICKNESS / 2, left: (SIDE_BOX - SIDE_BAR_LENGTH) / 2, width: SIDE_BAR_LENGTH, height: SIDE_BAR_THICKNESS }, "Top border"), edgeButton("bottom", { bottom: -SIDE_BAR_THICKNESS / 2, left: (SIDE_BOX - SIDE_BAR_LENGTH) / 2, width: SIDE_BAR_LENGTH, height: SIDE_BAR_THICKNESS }, "Bottom border"), edgeButton("left", { left: -SIDE_BAR_THICKNESS / 2, top: (SIDE_BOX - SIDE_BAR_LENGTH) / 2, width: SIDE_BAR_THICKNESS, height: SIDE_BAR_LENGTH }, "Left border"), edgeButton("right", { right: -SIDE_BAR_THICKNESS / 2, top: (SIDE_BOX - SIDE_BAR_LENGTH) / 2, width: SIDE_BAR_THICKNESS, height: SIDE_BAR_LENGTH }, "Right border")] })] }), _jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 6 }, children: [_jsx("button", { type: "button", style: buttonStyle, onMouseDown: (event) => event.preventDefault(), onClick: () => update({ sides: { top: true, right: true, bottom: true, left: true } }), children: "All sides" }), _jsx("button", { type: "button", style: buttonStyle, onMouseDown: (event) => event.preventDefault(), onClick: () => update({ sides: { top: false, right: false, bottom: false, left: false } }), children: "No border" })] })] }), _jsxs("div", { children: [_jsx("div", { style: fieldLabelStyle, children: "Style" }), _jsx("select", { "aria-label": "Border style", value: draft.style, onChange: (event) => update({ style: event.target.value }), style: selectStyle, children: STYLE_OPTIONS.map((style) => _jsxs("option", { value: style, children: [style[0].toUpperCase(), style.slice(1)] }, style)) })] }), _jsxs("div", { children: [_jsx("div", { style: fieldLabelStyle, children: "Width" }), _jsx("select", { "aria-label": "Border width", value: BORDER_WIDTH_PRESETS.find((preset) => preset.px === draft.widthPx)?.id ?? BORDER_WIDTH_PRESETS[0].id, onChange: (event) => { const preset = BORDER_WIDTH_PRESETS.find((entry) => entry.id === event.target.value); if (preset)
144
+ update({ widthPx: preset.px }); }, style: selectStyle, children: BORDER_WIDTH_PRESETS.map((preset) => _jsx("option", { value: preset.id, children: preset.label }, preset.id)) })] }), _jsxs("div", { children: [_jsx("div", { style: fieldLabelStyle, children: "Colour" }), _jsxs("button", { ref: swatchRef, type: "button", "aria-label": "Border colour", onMouseDown: (event) => event.preventDefault(), onClick: () => {
145
+ hexBeforeNestedPickerRef.current = draft.hex;
146
+ const rect = swatchRef.current?.getBoundingClientRect();
147
+ setColorPickerAnchor({ x: rect?.left ?? x, y: (rect?.bottom ?? y) + 4 });
148
+ }, style: { ...buttonStyle, width: "100%", display: "flex", alignItems: "center", gap: 8, justifyContent: "flex-start" }, children: [_jsx("span", { style: { width: 18, height: 18, borderRadius: 4, border: "1px solid var(--srte-input-border)", background: draft.hex }, "aria-hidden": "true" }), draft.hex] })] }), _jsxs("div", { style: { display: "flex", justifyContent: "flex-end", gap: 8 }, children: [_jsx("button", { type: "button", style: buttonStyle, onClick: onCancel, children: "Cancel" }), _jsx("button", { type: "button", onClick: () => onApply(draft), style: { ...buttonStyle, borderColor: "var(--srte-primary)", background: "var(--srte-primary)", color: "var(--srte-on-primary)" }, children: "Apply" })] }), colorPickerAnchor && _jsx(ColorPickerPopover, { x: colorPickerAnchor.x, y: colorPickerAnchor.y, label: "Border colour", initialValue: draft.hex, recentColors: recentColors, onPreview: (hex) => update({ hex }), onApply: (hex) => { update({ hex }); setColorPickerAnchor(null); }, onCancel: () => { update({ hex: hexBeforeNestedPickerRef.current }); setColorPickerAnchor(null); } })] }));
149
+ }
@@ -0,0 +1,25 @@
1
+ export interface TableResizeHandlesProps {
2
+ tableElement: HTMLTableElement;
3
+ /**
4
+ * `currentWidths` is every column's real, currently-rendered width (not
5
+ * just the dragged one) - see the handleUp comment below for why the
6
+ * caller needs this, not just the one changed index.
7
+ */
8
+ onResizeColumn: (index: number, width: number, currentWidths: readonly number[]) => void;
9
+ onResizeRow: (index: number, height: number, adjacent?: {
10
+ index: number;
11
+ height: number;
12
+ }) => void;
13
+ }
14
+ /**
15
+ * Phase 11.5 §2.2: column-width/row-height drag handles calling the
16
+ * existing setTableColumnWidthCommand/setTableRowHeightCommand (row/column
17
+ * reorder already had toolbar buttons - "Move row up/down"/"Move column
18
+ * left/right" - wired since before this phase; resize had no UI at all).
19
+ * Commit-on-release, not a live preview during drag - a deliberately
20
+ * bounded scope for the first pass. Recomputes handle geometry from the
21
+ * live table's actual rendered cell rects on every table layout change via
22
+ * ResizeObserver, since table content (and therefore column/row sizes)
23
+ * changes independently of resize actions.
24
+ */
25
+ export declare function TableResizeHandles({ tableElement, onResizeColumn, onResizeRow }: TableResizeHandlesProps): import("react/jsx-runtime").JSX.Element;