@beyondwork/docx-react-component 1.0.38 → 1.0.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +41 -31
- package/src/api/public-types.ts +183 -6
- package/src/core/commands/table-structure-commands.ts +31 -2
- package/src/core/commands/text-commands.ts +122 -2
- package/src/io/docx-session.ts +1 -0
- package/src/io/export/serialize-numbering.ts +42 -8
- package/src/io/export/serialize-paragraph-formatting.ts +152 -0
- package/src/io/export/serialize-run-formatting.ts +90 -0
- package/src/io/export/serialize-styles.ts +212 -0
- package/src/io/ooxml/parse-fields.ts +10 -3
- package/src/io/ooxml/parse-numbering.ts +41 -1
- package/src/io/ooxml/parse-paragraph-formatting.ts +188 -0
- package/src/io/ooxml/parse-run-formatting.ts +129 -0
- package/src/io/ooxml/parse-styles.ts +31 -0
- package/src/io/ooxml/xml-attr-helpers.ts +60 -0
- package/src/io/ooxml/xml-element.ts +19 -0
- package/src/model/canonical-document.ts +83 -3
- package/src/runtime/collab/event-types.ts +165 -0
- package/src/runtime/collab/index.ts +22 -0
- package/src/runtime/collab/remote-cursor-awareness.ts +93 -0
- package/src/runtime/collab/runtime-collab-sync.ts +273 -0
- package/src/runtime/document-runtime.ts +134 -18
- package/src/runtime/layout/index.ts +2 -0
- package/src/runtime/layout/inert-layout-facet.ts +2 -0
- package/src/runtime/layout/layout-engine-instance.ts +69 -2
- package/src/runtime/layout/layout-invalidation.ts +14 -5
- package/src/runtime/layout/page-graph.ts +36 -0
- package/src/runtime/layout/paginate-paragraph-lines.ts +128 -0
- package/src/runtime/layout/paginated-layout-engine.ts +342 -28
- package/src/runtime/layout/project-block-fragments.ts +154 -20
- package/src/runtime/layout/public-facet.ts +40 -1
- package/src/runtime/layout/resolve-page-fields.ts +70 -0
- package/src/runtime/layout/resolve-page-previews.ts +185 -0
- package/src/runtime/layout/resolved-formatting-state.ts +30 -26
- package/src/runtime/layout/table-render-plan.ts +21 -1
- package/src/runtime/numbering-prefix.ts +5 -0
- package/src/runtime/paragraph-style-resolver.ts +194 -0
- package/src/runtime/render/render-kernel.ts +5 -1
- package/src/runtime/resolved-numbering-geometry.ts +9 -1
- package/src/runtime/surface-projection.ts +129 -9
- package/src/runtime/table-schema.ts +11 -0
- package/src/ui/WordReviewEditor.tsx +285 -5
- package/src/ui/editor-command-bag.ts +4 -0
- package/src/ui/editor-runtime-boundary.ts +16 -0
- package/src/ui/editor-shell-view.tsx +4 -0
- package/src/ui/editor-surface-controller.tsx +9 -1
- package/src/ui/headless/chrome-registry.ts +34 -5
- package/src/ui/headless/scoped-chrome-policy.ts +29 -0
- package/src/ui-tailwind/chrome/review-queue-bar.tsx +2 -14
- package/src/ui-tailwind/chrome/role-action-sets.ts +14 -8
- package/src/ui-tailwind/chrome/tw-selection-anchor-resolver.ts +7 -10
- package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +11 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-structure.tsx +11 -0
- package/src/ui-tailwind/chrome/tw-table-border-picker.tsx +245 -0
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +101 -21
- package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +284 -0
- package/src/ui-tailwind/chrome-overlay/chrome-overlay-projector.ts +5 -1
- package/src/ui-tailwind/chrome-overlay/index.ts +0 -6
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +27 -17
- package/src/ui-tailwind/editor-surface/page-slice-util.ts +15 -0
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +28 -3
- package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +389 -0
- package/src/ui-tailwind/editor-surface/pm-schema.ts +40 -2
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +144 -62
- package/src/ui-tailwind/editor-surface/remote-cursor-plugin.ts +179 -0
- package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +559 -0
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +224 -78
- package/src/ui-tailwind/editor-surface/tw-table-bands.css +61 -0
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +19 -0
- package/src/ui-tailwind/index.ts +1 -5
- package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +122 -1
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +36 -3
- package/src/ui-tailwind/tw-review-workspace.tsx +132 -54
- package/src/runtime/collab-review-sync.ts +0 -254
- package/src/ui-tailwind/chrome-overlay/tw-workspace-view-switcher.tsx +0 -95
- package/src/ui-tailwind/editor-surface/pm-collab-plugins.ts +0 -40
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TwTableGripLayer — column-resize and row-resize grips for the active table.
|
|
3
|
+
*
|
|
4
|
+
* Grips are positioned via the render-kernel anchor index (P4):
|
|
5
|
+
* - Column grips: `anchorIndex.byTableColumnEdge(blockId, colIndex)`
|
|
6
|
+
* - Row grips: `anchorIndex.byTableRowEdge(blockId, rowIndex)`
|
|
7
|
+
*
|
|
8
|
+
* This is a pure overlay consumer: no DOM measurement, no selection mutation,
|
|
9
|
+
* no PM transactions. Drag deltas are converted from px → twips using the
|
|
10
|
+
* kernel's `pxPerTwip` ratio and dispatched through `onSetColumnWidth` /
|
|
11
|
+
* `onSetRowHeight` callbacks (which route to `ref.tables.apply(op)`).
|
|
12
|
+
*
|
|
13
|
+
* Drag state lives in `useRef` so moves don't trigger re-renders mid-drag.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import React, { useCallback, useEffect, useRef } from "react";
|
|
17
|
+
|
|
18
|
+
import type {
|
|
19
|
+
TableStructureContextSnapshot,
|
|
20
|
+
WordReviewEditorLayoutFacet,
|
|
21
|
+
} from "../../api/public-types";
|
|
22
|
+
import { preserveEditorSelectionMouseDown } from "../../ui/headless/preserve-editor-selection";
|
|
23
|
+
import type { OverlayCoordinateSpace } from "../chrome-overlay/chrome-overlay-projector";
|
|
24
|
+
import { projectRectToOverlay } from "../chrome-overlay/chrome-overlay-projector";
|
|
25
|
+
import { DEFAULT_PX_PER_TWIP } from "../../runtime/render/render-frame-types";
|
|
26
|
+
|
|
27
|
+
const GRIP_PX = 6;
|
|
28
|
+
const MIN_COLUMN_TWIPS = 720;
|
|
29
|
+
const MIN_ROW_TWIPS = 120;
|
|
30
|
+
|
|
31
|
+
export interface TwTableGripLayerProps {
|
|
32
|
+
facet: WordReviewEditorLayoutFacet;
|
|
33
|
+
tableContext: TableStructureContextSnapshot | null;
|
|
34
|
+
space?: OverlayCoordinateSpace;
|
|
35
|
+
disabled?: boolean;
|
|
36
|
+
onSetColumnWidth?: (columnIndex: number, twips: number) => void;
|
|
37
|
+
onSetRowHeight?: (
|
|
38
|
+
rowIndex: number,
|
|
39
|
+
twips: number,
|
|
40
|
+
rule: "auto" | "atLeast" | "exact",
|
|
41
|
+
) => void;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function TwTableGripLayer({
|
|
45
|
+
facet,
|
|
46
|
+
tableContext,
|
|
47
|
+
space,
|
|
48
|
+
disabled,
|
|
49
|
+
onSetColumnWidth,
|
|
50
|
+
onSetRowHeight,
|
|
51
|
+
}: TwTableGripLayerProps) {
|
|
52
|
+
if (!tableContext) return null;
|
|
53
|
+
|
|
54
|
+
const frame =
|
|
55
|
+
typeof facet.getRenderFrame === "function"
|
|
56
|
+
? (facet.getRenderFrame() ?? null)
|
|
57
|
+
: null;
|
|
58
|
+
if (!frame) return null;
|
|
59
|
+
|
|
60
|
+
const blockId = `table-${tableContext.tableBlockIndex}`;
|
|
61
|
+
const pageIndex = facet.getFirstPageIndexForBlock(blockId) ?? 0;
|
|
62
|
+
const plan = facet.getTableRenderPlan(blockId, pageIndex);
|
|
63
|
+
if (!plan) return null;
|
|
64
|
+
|
|
65
|
+
const pxPerTwip =
|
|
66
|
+
typeof facet.getRenderZoom === "function"
|
|
67
|
+
? (facet.getRenderZoom()?.pxPerTwip ?? DEFAULT_PX_PER_TWIP)
|
|
68
|
+
: DEFAULT_PX_PER_TWIP;
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<>
|
|
72
|
+
{plan.columnResizeHandles.map((handle) => {
|
|
73
|
+
const rect = frame.anchorIndex.byTableColumnEdge(
|
|
74
|
+
blockId,
|
|
75
|
+
handle.columnIndex,
|
|
76
|
+
);
|
|
77
|
+
if (!rect) return null;
|
|
78
|
+
const pos = projectRectToOverlay(rect, space);
|
|
79
|
+
return (
|
|
80
|
+
<ColResizeGrip
|
|
81
|
+
key={`col-${blockId}-${handle.columnIndex}`}
|
|
82
|
+
pos={pos}
|
|
83
|
+
colIndex={handle.columnIndex}
|
|
84
|
+
originalTwips={plan.columnsTwips[handle.columnIndex] ?? 720}
|
|
85
|
+
pxPerTwip={pxPerTwip}
|
|
86
|
+
disabled={!!disabled || !onSetColumnWidth}
|
|
87
|
+
onCommit={onSetColumnWidth}
|
|
88
|
+
/>
|
|
89
|
+
);
|
|
90
|
+
})}
|
|
91
|
+
{Array.from({ length: tableContext.rowCount }, (_, rowIndex) => {
|
|
92
|
+
const rect = frame.anchorIndex.byTableRowEdge(blockId, rowIndex);
|
|
93
|
+
if (!rect) return null;
|
|
94
|
+
const pos = projectRectToOverlay(rect, space);
|
|
95
|
+
return (
|
|
96
|
+
<RowResizeGrip
|
|
97
|
+
key={`row-${blockId}-${rowIndex}`}
|
|
98
|
+
pos={pos}
|
|
99
|
+
rowIndex={rowIndex}
|
|
100
|
+
pxPerTwip={pxPerTwip}
|
|
101
|
+
disabled={!!disabled || !onSetRowHeight}
|
|
102
|
+
onCommit={onSetRowHeight}
|
|
103
|
+
/>
|
|
104
|
+
);
|
|
105
|
+
})}
|
|
106
|
+
</>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
// Column resize grip
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
|
|
114
|
+
interface ColResizeGripProps {
|
|
115
|
+
pos: { left: string; top: string; width: string; height: string };
|
|
116
|
+
colIndex: number;
|
|
117
|
+
originalTwips: number;
|
|
118
|
+
pxPerTwip: number;
|
|
119
|
+
disabled: boolean;
|
|
120
|
+
onCommit?: (columnIndex: number, twips: number) => void;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function ColResizeGrip({
|
|
124
|
+
pos,
|
|
125
|
+
colIndex,
|
|
126
|
+
originalTwips,
|
|
127
|
+
pxPerTwip,
|
|
128
|
+
disabled,
|
|
129
|
+
onCommit,
|
|
130
|
+
}: ColResizeGripProps) {
|
|
131
|
+
const dragRef = useRef<{
|
|
132
|
+
startX: number;
|
|
133
|
+
originalTwips: number;
|
|
134
|
+
} | null>(null);
|
|
135
|
+
|
|
136
|
+
const handleMouseDown = useCallback(
|
|
137
|
+
(e: React.MouseEvent<HTMLElement>) => {
|
|
138
|
+
if (disabled) return;
|
|
139
|
+
preserveEditorSelectionMouseDown(e);
|
|
140
|
+
dragRef.current = { startX: e.clientX, originalTwips };
|
|
141
|
+
},
|
|
142
|
+
[disabled, originalTwips],
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
useEffect(() => {
|
|
146
|
+
const handleMove = (e: MouseEvent) => {
|
|
147
|
+
if (!dragRef.current) return;
|
|
148
|
+
e.preventDefault();
|
|
149
|
+
};
|
|
150
|
+
const handleUp = (e: MouseEvent) => {
|
|
151
|
+
if (!dragRef.current) return;
|
|
152
|
+
const deltaX = e.clientX - dragRef.current.startX;
|
|
153
|
+
const deltaTwips = deltaX / pxPerTwip;
|
|
154
|
+
const newTwips = Math.max(
|
|
155
|
+
MIN_COLUMN_TWIPS,
|
|
156
|
+
Math.round(dragRef.current.originalTwips + deltaTwips),
|
|
157
|
+
);
|
|
158
|
+
dragRef.current = null;
|
|
159
|
+
onCommit?.(colIndex, newTwips);
|
|
160
|
+
};
|
|
161
|
+
window.addEventListener("mousemove", handleMove);
|
|
162
|
+
window.addEventListener("mouseup", handleUp);
|
|
163
|
+
return () => {
|
|
164
|
+
window.removeEventListener("mousemove", handleMove);
|
|
165
|
+
window.removeEventListener("mouseup", handleUp);
|
|
166
|
+
};
|
|
167
|
+
}, [colIndex, pxPerTwip, onCommit]);
|
|
168
|
+
|
|
169
|
+
return (
|
|
170
|
+
<div
|
|
171
|
+
role="separator"
|
|
172
|
+
aria-orientation="vertical"
|
|
173
|
+
aria-label={`Resize column ${colIndex + 1}`}
|
|
174
|
+
data-testid={`col-resize-grip-${colIndex}`}
|
|
175
|
+
className={[
|
|
176
|
+
"pointer-events-auto absolute",
|
|
177
|
+
disabled
|
|
178
|
+
? "cursor-default opacity-0"
|
|
179
|
+
: "cursor-col-resize opacity-0 hover:opacity-100",
|
|
180
|
+
"transition-opacity",
|
|
181
|
+
"bg-accent",
|
|
182
|
+
].join(" ")}
|
|
183
|
+
style={{
|
|
184
|
+
left: `calc(${pos.left} - ${GRIP_PX / 2}px)`,
|
|
185
|
+
top: pos.top,
|
|
186
|
+
width: `${GRIP_PX}px`,
|
|
187
|
+
height: pos.height,
|
|
188
|
+
}}
|
|
189
|
+
onMouseDown={handleMouseDown}
|
|
190
|
+
/>
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ---------------------------------------------------------------------------
|
|
195
|
+
// Row resize grip
|
|
196
|
+
// ---------------------------------------------------------------------------
|
|
197
|
+
|
|
198
|
+
interface RowResizeGripProps {
|
|
199
|
+
pos: { left: string; top: string; width: string; height: string };
|
|
200
|
+
rowIndex: number;
|
|
201
|
+
pxPerTwip: number;
|
|
202
|
+
disabled: boolean;
|
|
203
|
+
onCommit?: (
|
|
204
|
+
rowIndex: number,
|
|
205
|
+
twips: number,
|
|
206
|
+
rule: "auto" | "atLeast" | "exact",
|
|
207
|
+
) => void;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function RowResizeGrip({
|
|
211
|
+
pos,
|
|
212
|
+
rowIndex,
|
|
213
|
+
pxPerTwip,
|
|
214
|
+
disabled,
|
|
215
|
+
onCommit,
|
|
216
|
+
}: RowResizeGripProps) {
|
|
217
|
+
const dragRef = useRef<{
|
|
218
|
+
startY: number;
|
|
219
|
+
startHeightPx: number;
|
|
220
|
+
} | null>(null);
|
|
221
|
+
|
|
222
|
+
const handleMouseDown = useCallback(
|
|
223
|
+
(e: React.MouseEvent<HTMLElement>) => {
|
|
224
|
+
if (disabled) return;
|
|
225
|
+
preserveEditorSelectionMouseDown(e);
|
|
226
|
+
// Start height is the current visible row height from the grip rect.
|
|
227
|
+
// We use the grip's own top position vs. previous row edge as a proxy.
|
|
228
|
+
dragRef.current = { startY: e.clientY, startHeightPx: 0 };
|
|
229
|
+
},
|
|
230
|
+
[disabled],
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
useEffect(() => {
|
|
234
|
+
const handleMove = (e: MouseEvent) => {
|
|
235
|
+
if (!dragRef.current) return;
|
|
236
|
+
e.preventDefault();
|
|
237
|
+
};
|
|
238
|
+
const handleUp = (e: MouseEvent) => {
|
|
239
|
+
if (!dragRef.current) return;
|
|
240
|
+
const deltaY = e.clientY - dragRef.current.startY;
|
|
241
|
+
if (Math.abs(deltaY) < 2) {
|
|
242
|
+
dragRef.current = null;
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
const deltaTwips = deltaY / pxPerTwip;
|
|
246
|
+
// We don't know the current row height from the grip alone; use atLeast
|
|
247
|
+
// so that a positive drag expands and a negative drag collapses to auto.
|
|
248
|
+
const newTwips = Math.max(MIN_ROW_TWIPS, Math.round(deltaTwips));
|
|
249
|
+
const rule = newTwips > 0 ? "atLeast" : "auto";
|
|
250
|
+
dragRef.current = null;
|
|
251
|
+
onCommit?.(rowIndex, newTwips, rule as "atLeast");
|
|
252
|
+
};
|
|
253
|
+
window.addEventListener("mousemove", handleMove);
|
|
254
|
+
window.addEventListener("mouseup", handleUp);
|
|
255
|
+
return () => {
|
|
256
|
+
window.removeEventListener("mousemove", handleMove);
|
|
257
|
+
window.removeEventListener("mouseup", handleUp);
|
|
258
|
+
};
|
|
259
|
+
}, [rowIndex, pxPerTwip, onCommit]);
|
|
260
|
+
|
|
261
|
+
return (
|
|
262
|
+
<div
|
|
263
|
+
role="separator"
|
|
264
|
+
aria-orientation="horizontal"
|
|
265
|
+
aria-label={`Resize row ${rowIndex + 1}`}
|
|
266
|
+
data-testid={`row-resize-grip-${rowIndex}`}
|
|
267
|
+
className={[
|
|
268
|
+
"pointer-events-auto absolute",
|
|
269
|
+
disabled
|
|
270
|
+
? "cursor-default opacity-0"
|
|
271
|
+
: "cursor-row-resize opacity-0 hover:opacity-100",
|
|
272
|
+
"transition-opacity",
|
|
273
|
+
"bg-accent",
|
|
274
|
+
].join(" ")}
|
|
275
|
+
style={{
|
|
276
|
+
left: pos.left,
|
|
277
|
+
top: `calc(${pos.top} - ${GRIP_PX / 2}px)`,
|
|
278
|
+
width: pos.width,
|
|
279
|
+
height: `${GRIP_PX}px`,
|
|
280
|
+
}}
|
|
281
|
+
onMouseDown={handleMouseDown}
|
|
282
|
+
/>
|
|
283
|
+
);
|
|
284
|
+
}
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import type { RenderFrameRect } from "../../runtime/render/index.ts";
|
|
12
|
+
import { recordPerfSample } from "../editor-surface/perf-probe.ts";
|
|
12
13
|
|
|
13
14
|
export interface OverlayCoordinateSpace {
|
|
14
15
|
/** Top-left of the overlay in the document column's coordinate space. */
|
|
@@ -33,12 +34,15 @@ export function projectRectToOverlay(
|
|
|
33
34
|
rect: RenderFrameRect,
|
|
34
35
|
space: OverlayCoordinateSpace = { originLeftPx: 0, originTopPx: 0 },
|
|
35
36
|
): CSSRect {
|
|
36
|
-
|
|
37
|
+
const t0 = typeof performance !== "undefined" ? performance.now() : 0;
|
|
38
|
+
const result: CSSRect = {
|
|
37
39
|
left: `${rect.leftPx - space.originLeftPx}px`,
|
|
38
40
|
top: `${rect.topPx - space.originTopPx}px`,
|
|
39
41
|
width: `${rect.widthPx}px`,
|
|
40
42
|
height: `${rect.heightPx}px`,
|
|
41
43
|
};
|
|
44
|
+
if (t0 > 0) recordPerfSample("chrome.overlay_reposition", performance.now() - t0);
|
|
45
|
+
return result;
|
|
42
46
|
}
|
|
43
47
|
|
|
44
48
|
/**
|
|
@@ -7,12 +7,6 @@
|
|
|
7
7
|
|
|
8
8
|
export { TwChromeOverlay, type TwChromeOverlayProps } from "./tw-chrome-overlay";
|
|
9
9
|
export { TwScopeRailLayer, type TwScopeRailLayerProps } from "./tw-scope-rail-layer";
|
|
10
|
-
export {
|
|
11
|
-
TwWorkspaceViewSwitcher,
|
|
12
|
-
type TwWorkspaceViewSwitcherProps,
|
|
13
|
-
type WorkspaceView,
|
|
14
|
-
type WorkspaceViewAction,
|
|
15
|
-
} from "./tw-workspace-view-switcher";
|
|
16
10
|
export {
|
|
17
11
|
inflateRect,
|
|
18
12
|
projectRectToOverlay,
|
|
@@ -15,9 +15,12 @@
|
|
|
15
15
|
import * as React from "react";
|
|
16
16
|
import type { OverlayCoordinateSpace } from "./chrome-overlay-projector";
|
|
17
17
|
import type { ScopeRailSegment } from "../../runtime/layout";
|
|
18
|
-
import type {
|
|
18
|
+
import type {
|
|
19
|
+
TableStructureContextSnapshot,
|
|
20
|
+
WordReviewEditorLayoutFacet,
|
|
21
|
+
} from "../../api/public-types";
|
|
19
22
|
import { TwScopeRailLayer } from "./tw-scope-rail-layer";
|
|
20
|
-
import {
|
|
23
|
+
import { TwTableGripLayer } from "../chrome/tw-table-grip-layer";
|
|
21
24
|
|
|
22
25
|
export interface TwChromeOverlayProps {
|
|
23
26
|
/** Layout facet the overlay layers read from. */
|
|
@@ -28,16 +31,22 @@ export interface TwChromeOverlayProps {
|
|
|
28
31
|
activeScopeId?: string | null;
|
|
29
32
|
/** Click handler the rail layer forwards to consumers. */
|
|
30
33
|
onScopeSegmentClick?: (segment: ScopeRailSegment) => void;
|
|
31
|
-
/** Currently active workspace view preset (draft / layout / review / workflow). */
|
|
32
|
-
activeWorkspaceView?: WorkspaceView;
|
|
33
|
-
/** Handler that fires when the user picks a workspace view. */
|
|
34
|
-
onWorkspaceViewChange?: (view: WorkspaceView) => void;
|
|
35
|
-
/** Show the bottom workspace view-switcher dock. Default: true. */
|
|
36
|
-
showWorkspaceDock?: boolean;
|
|
37
34
|
/** Test id applied to the overlay root. */
|
|
38
35
|
"data-testid"?: string;
|
|
39
36
|
/** Optional extra children (e.g., future comment balloon layer). */
|
|
40
37
|
children?: React.ReactNode;
|
|
38
|
+
|
|
39
|
+
// Table grip props (P6) -----------------------------------------------
|
|
40
|
+
/** Active table context — when present, column/row resize grips are shown. */
|
|
41
|
+
tableContext?: TableStructureContextSnapshot | null;
|
|
42
|
+
/** Fires when a column grip drag completes. */
|
|
43
|
+
onSetColumnWidth?: (columnIndex: number, twips: number) => void;
|
|
44
|
+
/** Fires when a row grip drag completes. */
|
|
45
|
+
onSetRowHeight?: (
|
|
46
|
+
rowIndex: number,
|
|
47
|
+
twips: number,
|
|
48
|
+
rule: "auto" | "atLeast" | "exact",
|
|
49
|
+
) => void;
|
|
41
50
|
}
|
|
42
51
|
|
|
43
52
|
/**
|
|
@@ -54,11 +63,11 @@ export const TwChromeOverlay: React.FC<TwChromeOverlayProps> = ({
|
|
|
54
63
|
space,
|
|
55
64
|
activeScopeId,
|
|
56
65
|
onScopeSegmentClick,
|
|
57
|
-
activeWorkspaceView,
|
|
58
|
-
onWorkspaceViewChange,
|
|
59
|
-
showWorkspaceDock = true,
|
|
60
66
|
"data-testid": testId,
|
|
61
67
|
children,
|
|
68
|
+
tableContext,
|
|
69
|
+
onSetColumnWidth,
|
|
70
|
+
onSetRowHeight,
|
|
62
71
|
}) => {
|
|
63
72
|
return (
|
|
64
73
|
<div
|
|
@@ -72,13 +81,14 @@ export const TwChromeOverlay: React.FC<TwChromeOverlayProps> = ({
|
|
|
72
81
|
activeScopeId={activeScopeId}
|
|
73
82
|
onSegmentClick={onScopeSegmentClick}
|
|
74
83
|
/>
|
|
84
|
+
<TwTableGripLayer
|
|
85
|
+
facet={facet}
|
|
86
|
+
tableContext={tableContext ?? null}
|
|
87
|
+
space={space}
|
|
88
|
+
onSetColumnWidth={onSetColumnWidth}
|
|
89
|
+
onSetRowHeight={onSetRowHeight}
|
|
90
|
+
/>
|
|
75
91
|
{children}
|
|
76
|
-
{showWorkspaceDock ? (
|
|
77
|
-
<TwWorkspaceViewSwitcher
|
|
78
|
-
activeView={activeWorkspaceView ?? "review"}
|
|
79
|
-
onViewChange={onWorkspaceViewChange}
|
|
80
|
-
/>
|
|
81
|
-
) : null}
|
|
82
92
|
</div>
|
|
83
93
|
);
|
|
84
94
|
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { DocumentPageSnapshot, SurfaceBlockSnapshot } from "../../api/public-types.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Filter surface blocks to those that overlap a page's character-offset range.
|
|
5
|
+
* A block overlaps [startOffset, endOffset) when block.from < endOffset AND block.to > startOffset.
|
|
6
|
+
* Blocks that straddle a page boundary appear on both pages.
|
|
7
|
+
*/
|
|
8
|
+
export function sliceBlocksForPage(
|
|
9
|
+
blocks: SurfaceBlockSnapshot[],
|
|
10
|
+
page: Pick<DocumentPageSnapshot, "startOffset" | "endOffset">,
|
|
11
|
+
): SurfaceBlockSnapshot[] {
|
|
12
|
+
return blocks.filter(
|
|
13
|
+
(b) => b.from < page.endOffset && b.to > page.startOffset,
|
|
14
|
+
);
|
|
15
|
+
}
|
|
@@ -23,6 +23,8 @@ export interface CommandBridgeCallbacks extends SelectionSyncCallbacks {
|
|
|
23
23
|
onInsertHardBreak: () => void;
|
|
24
24
|
onInsertTab: () => void;
|
|
25
25
|
onOutdentTab?: () => void;
|
|
26
|
+
onListIndent?: () => void;
|
|
27
|
+
onListOutdent?: () => void;
|
|
26
28
|
onUndo: () => void;
|
|
27
29
|
onRedo: () => void;
|
|
28
30
|
onBlockedInput?: (command: "paste" | "drop", message: string) => void;
|
|
@@ -77,8 +79,14 @@ export function createSelectionSyncPlugin(
|
|
|
77
79
|
});
|
|
78
80
|
}
|
|
79
81
|
|
|
82
|
+
export interface CommandBridgePluginOptions {
|
|
83
|
+
/** P6: when true, omit prosemirror-tables columnResizing() — chrome overlay grips take over. */
|
|
84
|
+
useChromeColumnResizing?: boolean;
|
|
85
|
+
}
|
|
86
|
+
|
|
80
87
|
export function createCommandBridgePlugins(
|
|
81
88
|
callbacks: CommandBridgeCallbacks,
|
|
89
|
+
options?: CommandBridgePluginOptions,
|
|
82
90
|
): Plugin[] {
|
|
83
91
|
let isComposing = false;
|
|
84
92
|
|
|
@@ -145,6 +153,18 @@ export function createCommandBridgePlugins(
|
|
|
145
153
|
return false;
|
|
146
154
|
}
|
|
147
155
|
|
|
156
|
+
// Alt+Shift+Right → list indent; Alt+Shift+Left → list outdent (Word keyboard behavior)
|
|
157
|
+
if (event.altKey && event.shiftKey) {
|
|
158
|
+
if (event.key === "ArrowRight") {
|
|
159
|
+
callbacks.onListIndent?.();
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
if (event.key === "ArrowLeft") {
|
|
163
|
+
callbacks.onListOutdent?.();
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
148
168
|
const resolution = resolveSurfaceShortcut(
|
|
149
169
|
{
|
|
150
170
|
key: event.key,
|
|
@@ -195,7 +215,12 @@ export function createCommandBridgePlugins(
|
|
|
195
215
|
// selection handles. Doc-changing table transactions (new rows, etc.) are
|
|
196
216
|
// filtered by the runtime filter above; navigation-only steps pass through.
|
|
197
217
|
const tablePlugin = tableEditing();
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
218
|
+
// P6: chrome overlay grips replace prosemirror-tables column resizing.
|
|
219
|
+
// useChromeColuumnResizing=true removes the PM plugin; set to false to fall
|
|
220
|
+
// back to the legacy prosemirror-tables drag handle for one release cycle.
|
|
221
|
+
const plugins: Plugin[] = [filterPlugin, selectionPlugin, inputPlugin, keydownPlugin, tablePlugin];
|
|
222
|
+
if (!options?.useChromeColumnResizing) {
|
|
223
|
+
plugins.push(columnResizing());
|
|
224
|
+
}
|
|
225
|
+
return plugins;
|
|
201
226
|
}
|