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,323 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useRef, useState } from "react";
3
+ const HANDLE_THICKNESS = 6;
4
+ const MIN_SIZE = 20;
5
+ /**
6
+ * Phase 11.5 §2.2: column-width/row-height drag handles calling the
7
+ * existing setTableColumnWidthCommand/setTableRowHeightCommand (row/column
8
+ * reorder already had toolbar buttons - "Move row up/down"/"Move column
9
+ * left/right" - wired since before this phase; resize had no UI at all).
10
+ * Commit-on-release, not a live preview during drag - a deliberately
11
+ * bounded scope for the first pass. Recomputes handle geometry from the
12
+ * live table's actual rendered cell rects on every table layout change via
13
+ * ResizeObserver, since table content (and therefore column/row sizes)
14
+ * changes independently of resize actions.
15
+ */
16
+ export function TableResizeHandles({ tableElement, onResizeColumn, onResizeRow }) {
17
+ const [columns, setColumns] = useState([]);
18
+ const [rows, setRows] = useState([]);
19
+ // `nextStartSize`/`nextIndex` are null for the last row/column's own
20
+ // outer-edge handle - that one still grows/shrinks the table's overall
21
+ // size (no neighbor to redistribute with); every other handle has a
22
+ // real neighbor and redistributes between the two instead.
23
+ const dragRef = useRef(null);
24
+ const [dragging, setDragging] = useState(null);
25
+ // handleUp (below) is defined inside an effect that doesn't depend on
26
+ // `columns`/`rows` (to avoid tearing down the pointer listeners on every
27
+ // ResizeObserver tick) - these refs keep it reading the latest measured
28
+ // sizes anyway, without that staleness risk.
29
+ const columnsRef = useRef([]);
30
+ columnsRef.current = columns;
31
+ const rowsRef = useRef([]);
32
+ rowsRef.current = rows;
33
+ // Shared by the passive observers below (structural/layout changes that
34
+ // happen outside a drag) and by handleMove during an active drag (see
35
+ // that effect for why an active drag can't just wait for these
36
+ // observers to notice).
37
+ const recompute = useCallback(() => {
38
+ const tableRect = tableElement.getBoundingClientRect();
39
+ const firstRowCells = [...tableElement.querySelectorAll("tr")[0]?.children || []];
40
+ const colBoundaries = [];
41
+ firstRowCells.forEach((cell, index) => {
42
+ const rect = cell.getBoundingClientRect();
43
+ colBoundaries.push({ index, position: rect.right, length: tableRect.height, start: tableRect.top, size: rect.width });
44
+ });
45
+ const rowElements = [...tableElement.querySelectorAll(":scope > tr, :scope > tbody > tr")];
46
+ const rowBoundaries = [];
47
+ rowElements.forEach((row, index) => {
48
+ const rect = row.getBoundingClientRect();
49
+ rowBoundaries.push({ index, position: rect.bottom, length: tableRect.width, start: tableRect.left, size: rect.height });
50
+ });
51
+ setColumns(colBoundaries);
52
+ setRows(rowBoundaries);
53
+ }, [tableElement]);
54
+ useEffect(() => {
55
+ recompute();
56
+ const observer = new ResizeObserver(recompute);
57
+ observer.observe(tableElement);
58
+ // ResizeObserver only fires when tableElement's own box size changes -
59
+ // inserting/removing a row or column into a table that stays the same
60
+ // overall width (the common case: width:100%, or a pinned literal
61
+ // width unaffected by redistributing space across one more/fewer
62
+ // column) never changes that box size, so it never fires. Handles
63
+ // then stay stale: wrong count, wrong position, wrong `boundary.size`
64
+ // for whatever column/row the model now has at that index - dragging
65
+ // one computed a wildly wrong new width from a stale start size. A
66
+ // MutationObserver on the table's own structure (row/cell add/remove/
67
+ // reorder) catches exactly the class of change ResizeObserver misses.
68
+ const mutationObserver = new MutationObserver(recompute);
69
+ mutationObserver.observe(tableElement, { childList: true, subtree: true });
70
+ const ownerWindow = tableElement.ownerDocument.defaultView;
71
+ ownerWindow?.addEventListener("scroll", recompute, true);
72
+ return () => {
73
+ observer.disconnect();
74
+ mutationObserver.disconnect();
75
+ ownerWindow?.removeEventListener("scroll", recompute, true);
76
+ };
77
+ }, [tableElement, recompute]);
78
+ useEffect(() => {
79
+ const ownerWindow = tableElement.ownerDocument.defaultView;
80
+ if (!ownerWindow)
81
+ return;
82
+ // For an internal boundary (a real neighbor exists), clamp the raw
83
+ // pointer delta so neither side ever goes below MIN_SIZE, then apply
84
+ // the *same* clamped delta with opposite signs to both sides - their
85
+ // sum is therefore always exactly startSize + nextStartSize, whatever
86
+ // the clamp did. That invariant is what makes "the border just moves"
87
+ // true: the table's own total size never has to change for these.
88
+ //
89
+ // Rows can't use this arithmetic-only approach: a <tr>'s rendered
90
+ // height can never go below what its own cell content needs (an
91
+ // explicit row height behaves as a minimum, not a hard cap, in table
92
+ // layout) - unlike a column's width under table-layout:fixed, which
93
+ // genuinely can be squeezed below its content's natural width. Naively
94
+ // clamping to a fixed MIN_SIZE the way columns do would, the instant a
95
+ // row's *real* content floor was higher than that, still hand the
96
+ // *other* row the full requested delta anyway - the total table
97
+ // height would silently grow instead of staying constant, and the row
98
+ // actually being dragged would appear frozen while its neighbor
99
+ // visibly ballooned (this is what "resize rows... move cursor up
100
+ // then it is effecting below row height" was - see docs/bugs/
101
+ // row-resize-drag-up-only-grows-the-row-below.md). Measuring the real
102
+ // DOM after attempting a shrink, and handing the other row only what
103
+ // was actually ceded, keeps that same invariant true regardless of
104
+ // where a row's real floor turns out to be.
105
+ const resolveSizes = (drag, event) => {
106
+ const rawDelta = drag.kind === "column" ? event.clientX - drag.startClient : event.clientY - drag.startClient;
107
+ if (drag.kind === "column") {
108
+ if (drag.nextStartSize === null) {
109
+ return { size: Math.max(MIN_SIZE, Math.round(drag.startSize + rawDelta)), nextSize: null };
110
+ }
111
+ const minDelta = -(drag.startSize - MIN_SIZE);
112
+ const maxDelta = drag.nextStartSize - MIN_SIZE;
113
+ const delta = Math.min(Math.max(rawDelta, minDelta), maxDelta);
114
+ return { size: Math.round(drag.startSize + delta), nextSize: Math.round(drag.nextStartSize - delta) };
115
+ }
116
+ const rowElements = [...tableElement.querySelectorAll(":scope > tr, :scope > tbody > tr")];
117
+ if (drag.nextStartSize === null || drag.nextIndex === null) {
118
+ const target = Math.max(MIN_SIZE, Math.round(drag.startSize + rawDelta));
119
+ const rowElement = rowElements[drag.index];
120
+ if (rowElement)
121
+ rowElement.style.height = `${target}px`;
122
+ const actual = rowElement ? Math.round(rowElement.getBoundingClientRect().height) : target;
123
+ return { size: actual, nextSize: null };
124
+ }
125
+ const rowElement = rowElements[drag.index];
126
+ const nextRowElement = rowElements[drag.nextIndex];
127
+ if (rawDelta <= 0) {
128
+ // The dragged (above) row is the one shrinking; the row below
129
+ // only grows by however much the above row actually gave up.
130
+ // There's no equivalent fallback for the dragged row itself here -
131
+ // a row that can't shrink just can't, full stop, no matter how
132
+ // flexible the table's total height is willing to be.
133
+ const target = Math.max(MIN_SIZE, Math.round(drag.startSize + rawDelta));
134
+ if (rowElement)
135
+ rowElement.style.height = `${target}px`;
136
+ const actualSize = rowElement ? Math.round(rowElement.getBoundingClientRect().height) : target;
137
+ // If the row below is already sitting at its own natural content
138
+ // floor right now, it never actually had this space to begin with
139
+ // - a *previous* down-drag on this same boundary that hit this
140
+ // row's floor grew the table's total instead of taking space from
141
+ // it (see docs/bugs/row-resize-blocked-when-every-row-is-at-its-
142
+ // floor.md), and that growth is real table height, not this row's
143
+ // slack. Handing it back to the row here would inflate a row that
144
+ // never gave anything up, and a user dragging the border back to
145
+ // where it started would find the table permanently bloated with
146
+ // no way back except Undo (docs/bugs/row-resize-round-trip-
147
+ // inflates-neighbor.md). Measure the row's own floor by
148
+ // temporarily clearing its inline height override; only
149
+ // redistribute into it when it's currently holding real slack
150
+ // above that floor.
151
+ if (nextRowElement) {
152
+ const previousNextHeight = nextRowElement.style.height;
153
+ nextRowElement.style.height = "";
154
+ const nextFloor = Math.round(nextRowElement.getBoundingClientRect().height);
155
+ nextRowElement.style.height = previousNextHeight;
156
+ if (drag.nextStartSize <= nextFloor + 1)
157
+ return { size: actualSize, nextSize: drag.nextStartSize };
158
+ }
159
+ return { size: actualSize, nextSize: Math.round(drag.nextStartSize - (actualSize - drag.startSize)) };
160
+ }
161
+ // Dragging down: the row below is asked to shrink to pay for the
162
+ // dragged row's growth, exactly like the mirror case above - but
163
+ // unlike a row shrinking, a row *growing* is never floor-limited (a
164
+ // <tr>'s content only ever imposes a minimum, never a maximum), so
165
+ // there's no symmetric "the dragged row just can't grow" outcome to
166
+ // fall back to here. Making the dragged row's growth conditional on
167
+ // the neighbor actually having room to give (the way the shrink
168
+ // case correctly is) meant that on a table where every row already
169
+ // sits at its own content floor - true of almost any freshly
170
+ // created or pasted table with short cell text - internal
171
+ // row-boundary dragging did *nothing at all* in either direction:
172
+ // shrinking had nowhere to go, and growing was blocked because the
173
+ // neighbor had nowhere to give either. See docs/bugs/
174
+ // row-resize-blocked-when-every-row-is-at-its-floor.md. Grow the
175
+ // dragged row by the full requested amount unconditionally, and let
176
+ // the neighbor give up only what it actually can; if that's less
177
+ // than what was asked, the shortfall becomes real table growth -
178
+ // the only place that space could possibly come from - rather than
179
+ // silently refusing the drag altogether.
180
+ const size = Math.max(MIN_SIZE, Math.round(drag.startSize + rawDelta));
181
+ if (rowElement)
182
+ rowElement.style.height = `${size}px`;
183
+ const target = Math.max(MIN_SIZE, Math.round(drag.nextStartSize - rawDelta));
184
+ if (nextRowElement)
185
+ nextRowElement.style.height = `${target}px`;
186
+ const nextSize = nextRowElement ? Math.round(nextRowElement.getBoundingClientRect().height) : target;
187
+ return { size, nextSize };
188
+ };
189
+ const handleMove = (event) => {
190
+ const drag = dragRef.current;
191
+ if (!drag)
192
+ return;
193
+ setDragging({ kind: drag.kind, index: drag.index, position: drag.kind === "column" ? event.clientX : event.clientY });
194
+ // Live preview: mutate the real <col>/<tr> directly during the drag
195
+ // (commit-on-release still only touches the model, via onResizeColumn/
196
+ // onResizeRow below) - previously nothing rendered until pointerup, so
197
+ // the drag looked frozen until it suddenly jumped to the final size.
198
+ // Undoing this on the next real render is automatic: committing
199
+ // triggers the renderer's own re-render from authoritative model
200
+ // state, which overwrites whatever this loop wrote.
201
+ const { size: liveSize, nextSize: liveNextSize } = resolveSizes(drag, event);
202
+ if (drag.kind === "column") {
203
+ const cols = [...(tableElement.querySelector("colgroup")?.children || [])];
204
+ const col = cols[drag.index];
205
+ if (col)
206
+ col.style.width = `${liveSize}px`;
207
+ if (liveNextSize !== null && drag.nextIndex !== null) {
208
+ const nextCol = cols[drag.nextIndex];
209
+ if (nextCol)
210
+ nextCol.style.width = `${liveNextSize}px`;
211
+ }
212
+ // The stylesheet's `table-layout: fixed` + `width: 100%` treats
213
+ // <col> widths as proportions of the table's own rendered width,
214
+ // not literal pixels, unless the table's own width matches their
215
+ // sum (see surface/renderer.ts's table-width fix) - keep that sum
216
+ // live too, or every other column visibly compresses for the
217
+ // duration of the drag even though the fix restores them on
218
+ // release. Redistributing between two columns (liveNextSize !==
219
+ // null) never changes this sum by construction, so only the
220
+ // last column's own resize (liveNextSize === null) needs this.
221
+ if (liveNextSize === null) {
222
+ const total = cols.reduce((sum, candidate, index) => sum + (index === drag.index ? liveSize : (parseFloat(candidate.style.width) || 0)), 0);
223
+ if (total > 0)
224
+ tableElement.style.width = `${total}px`;
225
+ }
226
+ }
227
+ else {
228
+ const rowElements = [...tableElement.querySelectorAll(":scope > tr, :scope > tbody > tr")];
229
+ const rowElement = rowElements[drag.index];
230
+ if (rowElement)
231
+ rowElement.style.height = `${liveSize}px`;
232
+ if (liveNextSize !== null && drag.nextIndex !== null) {
233
+ const nextRow = rowElements[drag.nextIndex];
234
+ if (nextRow)
235
+ nextRow.style.height = `${liveNextSize}px`;
236
+ }
237
+ }
238
+ // The handles' own on-screen position/length come from `columns`/
239
+ // `rows` state, which the effect above only refreshes via
240
+ // ResizeObserver/MutationObserver/scroll - none of which reliably
241
+ // fire here. Column drags happen to nudge the table's own rendered
242
+ // width by sub-pixel rounding (table-layout: fixed redistributing a
243
+ // stray pixel), which incidentally retriggers ResizeObserver; an
244
+ // internal row-boundary drag keeps the table's total height exactly
245
+ // constant by construction (see resolveSizes above), so it never
246
+ // does. Left alone, the row handle (and the blue line it renders)
247
+ // stays frozen at its pre-drag position while the real row border
248
+ // moves live underneath it - reported as a misplaced, "very slow"
249
+ // blue line that overflows past the table/editor as the gap between
250
+ // the frozen handle and the actual, now-taller table grows. Forcing
251
+ // a synchronous re-measure after every live style mutation (a
252
+ // getBoundingClientRect read right after a style write forces the
253
+ // browser to flush layout first, so this always reflects the
254
+ // mutation just above, not a stale value) keeps both column and row
255
+ // handles tracking the real border on every pointermove instead of
256
+ // waiting on an observer that may not fire at all.
257
+ recompute();
258
+ };
259
+ const handleUp = (event) => {
260
+ const drag = dragRef.current;
261
+ if (!drag)
262
+ return;
263
+ const { size, nextSize } = resolveSizes(drag, event);
264
+ if (drag.kind === "column") {
265
+ // A table pasted/created with no columnWidths yet (natural,
266
+ // stretched rendering - see docs/bugs/table-shrinks-after-paste.md)
267
+ // has no real per-column data for the command layer to fall back
268
+ // on; without this, setTableColumnWidthCommand's own fallback
269
+ // (Array(columns).fill(120)) would silently reset every OTHER
270
+ // column to a fabricated 120px default the instant any one column
271
+ // was resized, shrinking the whole table - passing the real,
272
+ // currently-measured width of every column lets the caller seed a
273
+ // complete, accurate columnWidths array instead of guessing.
274
+ const currentWidths = columnsRef.current.map((boundary) => boundary.size);
275
+ if (nextSize !== null && drag.nextIndex !== null)
276
+ currentWidths[drag.nextIndex] = nextSize;
277
+ onResizeColumn(drag.index, size, currentWidths);
278
+ }
279
+ else if (nextSize !== null && drag.nextIndex !== null) {
280
+ onResizeRow(drag.index, size, { index: drag.nextIndex, height: nextSize });
281
+ }
282
+ else
283
+ onResizeRow(drag.index, size);
284
+ dragRef.current = null;
285
+ setDragging(null);
286
+ };
287
+ ownerWindow.addEventListener("pointermove", handleMove);
288
+ ownerWindow.addEventListener("pointerup", handleUp);
289
+ return () => {
290
+ ownerWindow.removeEventListener("pointermove", handleMove);
291
+ ownerWindow.removeEventListener("pointerup", handleUp);
292
+ };
293
+ }, [tableElement, onResizeColumn, onResizeRow, recompute]);
294
+ const startDrag = (kind, boundary) => (event) => {
295
+ event.preventDefault();
296
+ const siblings = kind === "column" ? columnsRef.current : rowsRef.current;
297
+ const next = siblings[boundary.index + 1] ?? null;
298
+ dragRef.current = {
299
+ kind, index: boundary.index, startClient: kind === "column" ? event.clientX : event.clientY, startSize: boundary.size,
300
+ nextIndex: next?.index ?? null, nextStartSize: next?.size ?? null,
301
+ };
302
+ setDragging({ kind, index: boundary.index, position: kind === "column" ? event.clientX : event.clientY });
303
+ };
304
+ return (_jsxs("div", { "data-srte-table-resize-handles": "true", style: { position: "fixed", inset: 0, pointerEvents: "none", zIndex: 60 }, children: [rows.map((boundary) => (_jsx("div", { role: "separator", "aria-orientation": "horizontal", "aria-label": `Resize row ${boundary.index + 1}`, "data-srte-row-resize-handle": boundary.index, onPointerDown: startDrag("row", boundary), style: {
305
+ position: "fixed",
306
+ left: boundary.start,
307
+ top: boundary.position - HANDLE_THICKNESS / 2,
308
+ width: boundary.length,
309
+ height: HANDLE_THICKNESS,
310
+ cursor: "row-resize",
311
+ pointerEvents: "auto",
312
+ background: dragging?.kind === "row" && dragging.index === boundary.index ? "var(--srte-primary)" : "transparent",
313
+ } }, `row-${boundary.index}`))), columns.map((boundary) => (_jsx("div", { role: "separator", "aria-orientation": "vertical", "aria-label": `Resize column ${boundary.index + 1}`, "data-srte-column-resize-handle": boundary.index, onPointerDown: startDrag("column", boundary), style: {
314
+ position: "fixed",
315
+ left: boundary.position - HANDLE_THICKNESS / 2,
316
+ top: boundary.start,
317
+ width: HANDLE_THICKNESS,
318
+ height: boundary.length,
319
+ cursor: "col-resize",
320
+ pointerEvents: "auto",
321
+ background: dragging?.kind === "column" && dragging.index === boundary.index ? "var(--srte-primary)" : "transparent",
322
+ } }, `col-${boundary.index}`)))] }));
323
+ }
@@ -0,0 +1,16 @@
1
+ export interface TableSizePickerPopoverProps {
2
+ x: number;
3
+ y: number;
4
+ onInsert: (rows: number, columns: number) => void;
5
+ onCancel: () => void;
6
+ }
7
+ /**
8
+ * "Insert table" size picker - CKEditor's own pattern: hover an N×M grid to
9
+ * preview the size, click a cell to insert immediately (one decisive click,
10
+ * no separate Apply/Confirm step, since a grid cell click is already a
11
+ * single deliberate choice - the same reasoning ColorPickerPopover's
12
+ * recent-swatch buttons and the removed preset grid used). A numeric
13
+ * fallback below covers sizes past the grid's cap without capping what the
14
+ * tool can actually do.
15
+ */
16
+ export declare function TableSizePickerPopover({ x, y, onInsert, onCancel }: TableSizePickerPopoverProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,126 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useLayoutEffect, useRef, useState } from "react";
3
+ const MAX_GRID = 8;
4
+ const buttonStyle = {
5
+ minHeight: 32,
6
+ padding: "0 10px",
7
+ border: "1px solid var(--srte-input-border)",
8
+ borderRadius: 8,
9
+ background: "var(--srte-input-bg)",
10
+ color: "var(--srte-menu-text)",
11
+ cursor: "pointer",
12
+ fontWeight: 500,
13
+ fontSize: 13,
14
+ };
15
+ const inputStyle = {
16
+ width: 48,
17
+ height: 32,
18
+ boxSizing: "border-box",
19
+ padding: "0 8px",
20
+ border: "1px solid var(--srte-input-border)",
21
+ borderRadius: 8,
22
+ outline: "none",
23
+ background: "var(--srte-input-bg)",
24
+ color: "var(--srte-input-text)",
25
+ font: "inherit",
26
+ textAlign: "center",
27
+ };
28
+ /**
29
+ * "Insert table" size picker - CKEditor's own pattern: hover an N×M grid to
30
+ * preview the size, click a cell to insert immediately (one decisive click,
31
+ * no separate Apply/Confirm step, since a grid cell click is already a
32
+ * single deliberate choice - the same reasoning ColorPickerPopover's
33
+ * recent-swatch buttons and the removed preset grid used). A numeric
34
+ * fallback below covers sizes past the grid's cap without capping what the
35
+ * tool can actually do.
36
+ */
37
+ export function TableSizePickerPopover({ x, y, onInsert, onCancel }) {
38
+ const rootRef = useRef(null);
39
+ const rowsRef = useRef(null);
40
+ const [placement, setPlacement] = useState(null);
41
+ const [hover, setHover] = useState(null);
42
+ const [customRows, setCustomRows] = useState("2");
43
+ const [customColumns, setCustomColumns] = useState("2");
44
+ const onCancelRef = useRef(onCancel);
45
+ onCancelRef.current = onCancel;
46
+ useLayoutEffect(() => {
47
+ const el = rootRef.current;
48
+ if (!el)
49
+ return;
50
+ const margin = 8;
51
+ const { width, height } = el.getBoundingClientRect();
52
+ const viewportWidth = window.innerWidth;
53
+ const viewportHeight = window.innerHeight;
54
+ const overflowsRight = x + width > viewportWidth - margin;
55
+ const left = overflowsRight ? x - width : x;
56
+ const clampedLeft = Math.min(Math.max(margin, left), Math.max(margin, viewportWidth - width - margin));
57
+ const overflowsBottom = y + height > viewportHeight - margin;
58
+ const top = overflowsBottom ? y - height : y;
59
+ const clampedTop = Math.min(Math.max(margin, top), Math.max(margin, viewportHeight - margin));
60
+ setPlacement({ left: clampedLeft, top: clampedTop });
61
+ }, [x, y]);
62
+ useEffect(() => {
63
+ const dismissIfOutside = (event) => {
64
+ if (rootRef.current && !rootRef.current.contains(event.target))
65
+ onCancelRef.current();
66
+ };
67
+ window.addEventListener("pointerdown", dismissIfOutside, true);
68
+ window.addEventListener("mousedown", dismissIfOutside, true);
69
+ return () => {
70
+ window.removeEventListener("pointerdown", dismissIfOutside, true);
71
+ window.removeEventListener("mousedown", dismissIfOutside, true);
72
+ };
73
+ }, []);
74
+ // Escape must dismiss regardless of focus - unlike LinkEditorPopover/
75
+ // ColorPickerPopover, nothing here is autofocused on open (the grid is
76
+ // meant to be hovered, not tabbed into first), so focus stays wherever it
77
+ // was (typically the "Insert table" toolbar button) and the div's own
78
+ // onKeyDown below would never see the keypress.
79
+ useEffect(() => {
80
+ const onWindowKeyDown = (event) => {
81
+ if (event.key === "Escape")
82
+ onCancelRef.current();
83
+ };
84
+ window.addEventListener("keydown", onWindowKeyDown);
85
+ return () => window.removeEventListener("keydown", onWindowKeyDown);
86
+ }, []);
87
+ const insertCustom = () => {
88
+ const rows = Math.min(50, Math.max(1, Math.round(Number(customRows)) || 0));
89
+ const columns = Math.min(50, Math.max(1, Math.round(Number(customColumns)) || 0));
90
+ if (!rows || !columns)
91
+ return;
92
+ onInsert(rows, columns);
93
+ };
94
+ return (_jsxs("div", { ref: rootRef, "data-srte-table-size-popover": "true", role: "dialog", "aria-label": "Insert table", style: {
95
+ position: "fixed",
96
+ left: placement?.left ?? x,
97
+ top: placement?.top ?? y,
98
+ visibility: placement ? "visible" : "hidden",
99
+ zIndex: 70,
100
+ width: 232,
101
+ boxSizing: "border-box",
102
+ background: "var(--srte-menu-bg)",
103
+ color: "var(--srte-menu-text)",
104
+ border: "1px solid var(--srte-border)",
105
+ borderRadius: 12,
106
+ boxShadow: "var(--srte-menu-shadow)",
107
+ padding: 14,
108
+ }, children: [_jsx("div", { style: { fontWeight: 650, marginBottom: 10 }, children: "Insert table" }), _jsx("div", { "data-srte-table-size-grid": "true", style: { display: "grid", gridTemplateColumns: `repeat(${MAX_GRID}, 1fr)`, gap: 3, marginBottom: 8 }, onMouseLeave: () => setHover(null), children: Array.from({ length: MAX_GRID * MAX_GRID }, (_, index) => {
109
+ const row = Math.floor(index / MAX_GRID);
110
+ const column = index % MAX_GRID;
111
+ const active = hover ? row <= hover.row && column <= hover.column : false;
112
+ return (_jsx("button", { type: "button", "data-srte-table-size-cell": `${row + 1}x${column + 1}`, "aria-label": `Insert a ${row + 1} by ${column + 1} table`, onMouseEnter: () => setHover({ row, column }), onFocus: () => setHover({ row, column }), onClick: () => onInsert(row + 1, column + 1), style: {
113
+ width: 20, height: 20, padding: 0, boxSizing: "border-box",
114
+ border: `1px solid ${active ? "var(--srte-primary)" : "var(--srte-input-border)"}`,
115
+ borderRadius: 3,
116
+ background: active ? "var(--srte-primary)" : "var(--srte-input-bg)",
117
+ cursor: "pointer",
118
+ } }, index));
119
+ }) }), _jsx("div", { "data-srte-table-size-label": "true", style: { fontSize: 12, marginBottom: 12, minHeight: 16 }, children: hover ? `${hover.row + 1} × ${hover.column + 1} table` : "Hover to choose a size" }), _jsx("div", { style: { fontSize: 12, fontWeight: 600, marginBottom: 6 }, children: "Custom size" }), _jsxs("div", { style: { display: "flex", alignItems: "center", gap: 6, marginBottom: 12 }, children: [_jsx("input", { ref: rowsRef, type: "number", min: 1, max: 50, "aria-label": "Rows", value: customRows, onChange: (event) => setCustomRows(event.target.value), onKeyDown: (event) => { if (event.key === "Enter") {
120
+ event.preventDefault();
121
+ insertCustom();
122
+ } }, style: inputStyle }), _jsx("span", { children: "\u00D7" }), _jsx("input", { type: "number", min: 1, max: 50, "aria-label": "Columns", value: customColumns, onChange: (event) => setCustomColumns(event.target.value), onKeyDown: (event) => { if (event.key === "Enter") {
123
+ event.preventDefault();
124
+ insertCustom();
125
+ } }, style: inputStyle }), _jsx("button", { type: "button", onClick: insertCustom, style: { ...buttonStyle, marginLeft: "auto", borderColor: "var(--srte-primary)", background: "var(--srte-primary)", color: "var(--srte-on-primary)" }, children: "Insert" })] }), _jsx("div", { style: { display: "flex", justifyContent: "flex-end" }, children: _jsx("button", { type: "button", onClick: onCancel, style: buttonStyle, children: "Cancel" }) })] }));
126
+ }
@@ -0,0 +1,161 @@
1
+ import React from "react";
2
+ export declare const toolbarIcons: {
3
+ readonly bold: import("react/jsx-runtime").JSX.Element;
4
+ readonly italic: import("react/jsx-runtime").JSX.Element;
5
+ readonly underline: import("react/jsx-runtime").JSX.Element;
6
+ readonly strikethrough: import("react/jsx-runtime").JSX.Element;
7
+ readonly code: import("react/jsx-runtime").JSX.Element;
8
+ readonly superscript: import("react/jsx-runtime").JSX.Element;
9
+ readonly subscript: import("react/jsx-runtime").JSX.Element;
10
+ readonly textColor: import("react/jsx-runtime").JSX.Element;
11
+ readonly backgroundColor: import("react/jsx-runtime").JSX.Element;
12
+ readonly fontSize: import("react/jsx-runtime").JSX.Element;
13
+ readonly fontFamily: import("react/jsx-runtime").JSX.Element;
14
+ readonly paragraphStyle: import("react/jsx-runtime").JSX.Element;
15
+ readonly alignLeft: import("react/jsx-runtime").JSX.Element;
16
+ readonly alignCenter: import("react/jsx-runtime").JSX.Element;
17
+ readonly alignRight: import("react/jsx-runtime").JSX.Element;
18
+ readonly alignJustify: import("react/jsx-runtime").JSX.Element;
19
+ readonly quote: import("react/jsx-runtime").JSX.Element;
20
+ readonly moveUp: import("react/jsx-runtime").JSX.Element;
21
+ readonly moveDown: import("react/jsx-runtime").JSX.Element;
22
+ readonly moveLeft: import("react/jsx-runtime").JSX.Element;
23
+ readonly moveRight: import("react/jsx-runtime").JSX.Element;
24
+ readonly indent: import("react/jsx-runtime").JSX.Element;
25
+ readonly outdent: import("react/jsx-runtime").JSX.Element;
26
+ readonly link: import("react/jsx-runtime").JSX.Element;
27
+ readonly unlink: import("react/jsx-runtime").JSX.Element;
28
+ readonly bulletedList: import("react/jsx-runtime").JSX.Element;
29
+ readonly numberedList: import("react/jsx-runtime").JSX.Element;
30
+ readonly checklist: import("react/jsx-runtime").JSX.Element;
31
+ readonly checkSquare: import("react/jsx-runtime").JSX.Element;
32
+ readonly restart: import("react/jsx-runtime").JSX.Element;
33
+ readonly continueNumbering: import("react/jsx-runtime").JSX.Element;
34
+ readonly table: import("react/jsx-runtime").JSX.Element;
35
+ readonly mergeCells: import("react/jsx-runtime").JSX.Element;
36
+ readonly splitCell: import("react/jsx-runtime").JSX.Element;
37
+ readonly addRow: import("react/jsx-runtime").JSX.Element;
38
+ readonly addColumn: import("react/jsx-runtime").JSX.Element;
39
+ readonly headerRow: import("react/jsx-runtime").JSX.Element;
40
+ readonly deleteTable: import("react/jsx-runtime").JSX.Element;
41
+ readonly image: import("react/jsx-runtime").JSX.Element;
42
+ readonly video: import("react/jsx-runtime").JSX.Element;
43
+ readonly audio: import("react/jsx-runtime").JSX.Element;
44
+ readonly formula: import("react/jsx-runtime").JSX.Element;
45
+ readonly specialChar: import("react/jsx-runtime").JSX.Element;
46
+ readonly cellBorder: import("react/jsx-runtime").JSX.Element;
47
+ readonly edit: import("react/jsx-runtime").JSX.Element;
48
+ readonly zoomIn: import("react/jsx-runtime").JSX.Element;
49
+ readonly zoomOut: import("react/jsx-runtime").JSX.Element;
50
+ readonly delete: import("react/jsx-runtime").JSX.Element;
51
+ readonly import: import("react/jsx-runtime").JSX.Element;
52
+ readonly saveCopy: import("react/jsx-runtime").JSX.Element;
53
+ readonly json: import("react/jsx-runtime").JSX.Element;
54
+ readonly history: import("react/jsx-runtime").JSX.Element;
55
+ readonly addComment: import("react/jsx-runtime").JSX.Element;
56
+ readonly comments: import("react/jsx-runtime").JSX.Element;
57
+ readonly suggest: import("react/jsx-runtime").JSX.Element;
58
+ readonly suggestions: import("react/jsx-runtime").JSX.Element;
59
+ readonly showEdits: import("react/jsx-runtime").JSX.Element;
60
+ readonly undo: import("react/jsx-runtime").JSX.Element;
61
+ readonly redo: import("react/jsx-runtime").JSX.Element;
62
+ readonly more: import("react/jsx-runtime").JSX.Element;
63
+ readonly divider: import("react/jsx-runtime").JSX.Element;
64
+ readonly pageBreak: import("react/jsx-runtime").JSX.Element;
65
+ readonly lineHeight: import("react/jsx-runtime").JSX.Element;
66
+ };
67
+ export type ToolbarIconKey = keyof typeof toolbarIcons;
68
+ /** A single always-visible toolbar control: icon + plain-language label together, per the redesign's core principle. */
69
+ export declare function ToolbarButton({ icon, label, ariaLabel, pressed, disabled, title, onClick, iconOnly, widePromote }: {
70
+ icon: ToolbarIconKey;
71
+ label: string;
72
+ ariaLabel?: string;
73
+ /** "mixed" renders aria-pressed="mixed" - the selection is partially covered by this mark (see markCoverage in CanonicalAuthorityEditor.tsx). */
74
+ pressed?: boolean | "mixed";
75
+ disabled?: boolean;
76
+ title?: string;
77
+ onClick: (event: React.MouseEvent<HTMLButtonElement>) => void;
78
+ /** Only for genuinely space-constrained spots (e.g. a 4-way align cluster) - the accessible name is preserved via ariaLabel/label regardless. */
79
+ iconOnly?: boolean;
80
+ /**
81
+ * This is the wide-viewport standalone copy of a tool that also lives in a
82
+ * dropdown (via the matching `widePromote` ToolbarMenuItem below) - hidden
83
+ * by default, shown only past theme.ts's wide-promotion breakpoint. See
84
+ * docs/bugs/toolbar-priority-collapse-fixed-threshold-no-wide-promotion.md.
85
+ */
86
+ widePromote?: boolean;
87
+ }): import("react/jsx-runtime").JSX.Element;
88
+ /** One row inside an open dropdown menu - closes the dropdown after acting, matching ContextMenu.tsx's own dismiss-on-action convention. */
89
+ export declare function ToolbarMenuItem({ icon, label, ariaLabel, pressed, disabled, title, onClick, widePromote }: {
90
+ icon?: ToolbarIconKey;
91
+ label: string;
92
+ ariaLabel?: string;
93
+ pressed?: boolean | "mixed";
94
+ disabled?: boolean;
95
+ title?: string;
96
+ onClick: (event: React.MouseEvent<HTMLButtonElement>) => void;
97
+ /** This tool also has a standalone wide-viewport ToolbarButton copy - hide this dropdown/mobile-menu row once that copy is showing (see ToolbarButton's own doc comment). */
98
+ widePromote?: boolean;
99
+ }): import("react/jsx-runtime").JSX.Element;
100
+ /**
101
+ * A labeled dropdown for a group's long-tail tools - wires up the
102
+ * `.srte-toolbar-menu`/`.srte-menu` CSS `theme.ts` already defines but the
103
+ * pre-redesign toolbar never used. `<details>/<summary>` gives free
104
+ * keyboard support (Enter/Space to open, native focus handling) without a
105
+ * new open/close state to manage; outside-click dismiss is handled by
106
+ * useDismissDetailsOnOutsideClick above.
107
+ *
108
+ * The panel itself renders `position: fixed` with JS-measured coordinates
109
+ * (docs/bugs/toolbar-dropdown-clipped-by-host-overflow-hidden.md), the same
110
+ * pattern ColorPickerPopover/TableBorderPopover already use - a real host
111
+ * can (and, reported live, does) wrap the editor in an `overflow: hidden`
112
+ * container for its own layout reasons; a `position: absolute` panel
113
+ * anchored inside that container gets visually clipped the moment it needs
114
+ * to extend past the container's own bounds, which is exactly when a
115
+ * narrow host is most likely to need the dropdown in the first place.
116
+ * `position: fixed`'s containing block is normally the viewport, so this
117
+ * escapes ANY ancestor's overflow clipping regardless of DOM depth - unless
118
+ * an ancestor has its own transform/perspective/filter, which becomes the
119
+ * containing block instead (see fixedPositioning.ts, and
120
+ * docs/bugs/toolbar-menu-misplaced-inside-transformed-ancestor.md); the
121
+ * placement math below accounts for that case too.
122
+ */
123
+ export declare function ToolbarDropdown({ icon, label, priority, children }: {
124
+ icon?: ToolbarIconKey;
125
+ label: string;
126
+ /** Mirrors theme.ts's `[data-srte-priority]` responsive rules - 2 collapses into the mobile "More" menu before 1 (never collapses), leaving priority 3 whole *groups* to collapse first. */
127
+ priority?: 2;
128
+ children: React.ReactNode;
129
+ }): import("react/jsx-runtime").JSX.Element;
130
+ /** A visually-separated cluster of controls, optionally collapsing as a whole unit on narrow viewports (theme.ts's `.srte-toolbar-group[data-srte-priority="3"]`). */
131
+ export declare function ToolbarGroup({ priority, children }: {
132
+ priority?: 3;
133
+ children: React.ReactNode;
134
+ }): import("react/jsx-runtime").JSX.Element;
135
+ /**
136
+ * The narrow-viewport overflow trigger - `.srte-mobile-more` is CSS-hidden
137
+ * except under theme.ts's `max-width: 639px` rules, which also hide every
138
+ * `[data-srte-priority="2"]`/`[data-srte-priority="3"]` element. Its own
139
+ * menu statically lists every tool that lives inside one of those
140
+ * collapsing dropdowns/groups, so nothing becomes unreachable once the
141
+ * primary row starts shedding groups - this is plain CSS visibility
142
+ * toggling of pre-rendered content, not JS media-query logic, matching how
143
+ * the rest of this responsive system already works.
144
+ *
145
+ * The panel itself renders `position: fixed` with JS-measured coordinates,
146
+ * the same fix and reasoning as `ToolbarDropdown` (docs/bugs/
147
+ * toolbar-dropdown-clipped-by-host-overflow-hidden.md) - this component was
148
+ * NOT covered by that fix at the time (incorrectly assumed to be
149
+ * unaffected since it already had its own separate mobile CSS), but it has
150
+ * the exact same `position: absolute` vulnerability to a host's own
151
+ * `overflow: hidden` container, and since this is the ONLY toolbar
152
+ * overflow affordance on narrow viewports (every ToolbarDropdown hides
153
+ * entirely below the mobile breakpoint), a clipped host is more likely to
154
+ * matter here, not less. Confirmed live: a real narrow host wrapping the
155
+ * editor in `overflow: hidden` computed this panel at `left: -214px` -
156
+ * mostly off-screen, functionally invisible despite being technically
157
+ * present in the DOM.
158
+ */
159
+ export declare function MobileMoreMenu({ children }: {
160
+ children: React.ReactNode;
161
+ }): import("react/jsx-runtime").JSX.Element;