@beyondwork/docx-react-component 1.0.38 → 1.0.40
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 +305 -6
- package/src/core/commands/table-structure-commands.ts +31 -2
- package/src/core/commands/text-commands.ts +122 -2
- package/src/index.ts +9 -0
- 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 +141 -18
- package/src/runtime/layout/docx-font-loader.ts +30 -11
- package/src/runtime/layout/index.ts +2 -0
- package/src/runtime/layout/inert-layout-facet.ts +3 -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 +81 -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/runtime/workflow-rail-segments.ts +149 -1
- package/src/ui/WordReviewEditor.tsx +302 -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 +22 -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-mode-dock.tsx +80 -0
- 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 +353 -0
- package/src/ui-tailwind/chrome-overlay/chrome-overlay-projector.ts +5 -1
- package/src/ui-tailwind/chrome-overlay/index.ts +2 -6
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +82 -18
- package/src/ui-tailwind/chrome-overlay/tw-scope-card-layer.tsx +133 -0
- package/src/ui-tailwind/chrome-overlay/tw-scope-card.tsx +386 -0
- package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +140 -69
- 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-decorations.ts +7 -2
- 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 +170 -63
- 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 +6 -5
- package/src/ui-tailwind/theme/editor-theme.css +108 -15
- 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 +207 -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
|
@@ -21,6 +21,12 @@ export interface TwTableContextToolbarProps {
|
|
|
21
21
|
onSplitCell?: () => void;
|
|
22
22
|
onSetCellBackground?: (color: string) => void;
|
|
23
23
|
onDeleteTable?: () => void;
|
|
24
|
+
// P6: new ops surfaced from P2 capability flags
|
|
25
|
+
onToggleRowHeader?: () => void;
|
|
26
|
+
onToggleRowCantSplit?: () => void;
|
|
27
|
+
onDistributeColumnsEvenly?: () => void;
|
|
28
|
+
onSetTableAlignment?: (alignment: "left" | "center" | "right") => void;
|
|
29
|
+
onSetCellVerticalAlign?: (align: "top" | "center" | "bottom") => void;
|
|
24
30
|
}
|
|
25
31
|
|
|
26
32
|
const CELL_COLORS = [
|
|
@@ -113,6 +119,24 @@ export function TwTableContextToolbar(props: TwTableContextToolbarProps) {
|
|
|
113
119
|
<ToolbarBadge tone="accent">Header row</ToolbarBadge>
|
|
114
120
|
) : null}
|
|
115
121
|
|
|
122
|
+
{/* T5 whole-table: table alignment */}
|
|
123
|
+
{tier === "whole-table" ? (
|
|
124
|
+
<ToolbarSection label="Align">
|
|
125
|
+
{(["left", "center", "right"] as const).map((align) => (
|
|
126
|
+
<ToolbarButton
|
|
127
|
+
key={align}
|
|
128
|
+
ariaLabel={`Align table ${align}`}
|
|
129
|
+
capability={tableContext?.operations.setTableAlignment}
|
|
130
|
+
disabled={props.disabled}
|
|
131
|
+
onClick={() => props.onSetTableAlignment?.(align)}
|
|
132
|
+
active={tableContext?.currentCell != null && align === "left"}
|
|
133
|
+
>
|
|
134
|
+
{align[0]!.toUpperCase()}
|
|
135
|
+
</ToolbarButton>
|
|
136
|
+
))}
|
|
137
|
+
</ToolbarSection>
|
|
138
|
+
) : null}
|
|
139
|
+
|
|
116
140
|
{/* T5 whole-table: style selector (primary), delete table */}
|
|
117
141
|
{tier === "whole-table" ? (
|
|
118
142
|
<ToolbarSection label="Style">
|
|
@@ -162,15 +186,34 @@ export function TwTableContextToolbar(props: TwTableContextToolbarProps) {
|
|
|
162
186
|
Below
|
|
163
187
|
</ToolbarButton>
|
|
164
188
|
{tier === "row-selected" ? (
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
189
|
+
<>
|
|
190
|
+
<ToolbarButton
|
|
191
|
+
ariaLabel="Delete row"
|
|
192
|
+
capability={tableContext?.operations.deleteRow}
|
|
193
|
+
disabled={props.disabled}
|
|
194
|
+
onClick={props.onDeleteRow}
|
|
195
|
+
danger
|
|
196
|
+
>
|
|
197
|
+
Delete row
|
|
198
|
+
</ToolbarButton>
|
|
199
|
+
<ToolbarButton
|
|
200
|
+
ariaLabel="Toggle header row"
|
|
201
|
+
capability={tableContext?.operations.setRowIsHeader}
|
|
202
|
+
disabled={props.disabled}
|
|
203
|
+
onClick={props.onToggleRowHeader}
|
|
204
|
+
active={tableContext?.currentCell.isHeader}
|
|
205
|
+
>
|
|
206
|
+
Header
|
|
207
|
+
</ToolbarButton>
|
|
208
|
+
<ToolbarButton
|
|
209
|
+
ariaLabel="Toggle row can't split"
|
|
210
|
+
capability={tableContext?.operations.setRowCantSplit}
|
|
211
|
+
disabled={props.disabled}
|
|
212
|
+
onClick={props.onToggleRowCantSplit}
|
|
213
|
+
>
|
|
214
|
+
No break
|
|
215
|
+
</ToolbarButton>
|
|
216
|
+
</>
|
|
174
217
|
) : null}
|
|
175
218
|
</ToolbarSection>
|
|
176
219
|
) : null}
|
|
@@ -195,15 +238,25 @@ export function TwTableContextToolbar(props: TwTableContextToolbarProps) {
|
|
|
195
238
|
Right
|
|
196
239
|
</ToolbarButton>
|
|
197
240
|
{tier === "column-selected" ? (
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
241
|
+
<>
|
|
242
|
+
<ToolbarButton
|
|
243
|
+
ariaLabel="Delete column"
|
|
244
|
+
capability={tableContext?.operations.deleteColumn}
|
|
245
|
+
disabled={props.disabled}
|
|
246
|
+
onClick={props.onDeleteColumn}
|
|
247
|
+
danger
|
|
248
|
+
>
|
|
249
|
+
Delete column
|
|
250
|
+
</ToolbarButton>
|
|
251
|
+
<ToolbarButton
|
|
252
|
+
ariaLabel="Distribute columns evenly"
|
|
253
|
+
capability={tableContext?.operations.distributeColumnsEvenly}
|
|
254
|
+
disabled={props.disabled}
|
|
255
|
+
onClick={props.onDistributeColumnsEvenly}
|
|
256
|
+
>
|
|
257
|
+
Distribute
|
|
258
|
+
</ToolbarButton>
|
|
259
|
+
</>
|
|
207
260
|
) : null}
|
|
208
261
|
</ToolbarSection>
|
|
209
262
|
) : null}
|
|
@@ -257,6 +310,29 @@ export function TwTableContextToolbar(props: TwTableContextToolbarProps) {
|
|
|
257
310
|
</ToolbarSection>
|
|
258
311
|
) : null}
|
|
259
312
|
|
|
313
|
+
{/* Cell vertical alignment (caret-in-cell + multi-cell) */}
|
|
314
|
+
{(tier === "caret-in-cell" || tier === "multi-cell") ? (
|
|
315
|
+
<ToolbarSection label="V-Align">
|
|
316
|
+
{(
|
|
317
|
+
[
|
|
318
|
+
["top", "Top"],
|
|
319
|
+
["center", "Mid"],
|
|
320
|
+
["bottom", "Bot"],
|
|
321
|
+
] as const
|
|
322
|
+
).map(([align, label]) => (
|
|
323
|
+
<ToolbarButton
|
|
324
|
+
key={align}
|
|
325
|
+
ariaLabel={`Cell vertical align ${align}`}
|
|
326
|
+
capability={tableContext?.operations.setCellVerticalAlign}
|
|
327
|
+
disabled={props.disabled}
|
|
328
|
+
onClick={() => props.onSetCellVerticalAlign?.(align)}
|
|
329
|
+
>
|
|
330
|
+
{label}
|
|
331
|
+
</ToolbarButton>
|
|
332
|
+
))}
|
|
333
|
+
</ToolbarSection>
|
|
334
|
+
) : null}
|
|
335
|
+
|
|
260
336
|
{/* T5 only: delete table (danger) */}
|
|
261
337
|
{tier === "whole-table" ? (
|
|
262
338
|
<ToolbarSection label="Table">
|
|
@@ -362,6 +438,7 @@ function ToolbarButton(props: {
|
|
|
362
438
|
danger?: boolean;
|
|
363
439
|
disabled: boolean;
|
|
364
440
|
onClick?: () => void;
|
|
441
|
+
active?: boolean;
|
|
365
442
|
}) {
|
|
366
443
|
const capabilityEnabled = props.capability?.enabled ?? true;
|
|
367
444
|
const title = !capabilityEnabled ? props.capability?.reason : undefined;
|
|
@@ -369,14 +446,17 @@ function ToolbarButton(props: {
|
|
|
369
446
|
<button
|
|
370
447
|
type="button"
|
|
371
448
|
aria-label={props.ariaLabel}
|
|
449
|
+
aria-pressed={props.active}
|
|
372
450
|
disabled={props.disabled || !props.onClick || !capabilityEnabled}
|
|
373
451
|
onMouseDown={preserveEditorSelectionMouseDown}
|
|
374
452
|
onClick={props.onClick}
|
|
375
453
|
title={title}
|
|
376
454
|
className={`inline-flex h-7 items-center rounded-md px-2 text-[11px] font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-40 ${
|
|
377
|
-
props.
|
|
378
|
-
? "
|
|
379
|
-
:
|
|
455
|
+
props.active
|
|
456
|
+
? "bg-accent/15 text-accent"
|
|
457
|
+
: props.danger
|
|
458
|
+
? "text-danger hover:bg-danger/10"
|
|
459
|
+
: "text-primary hover:bg-surface"
|
|
380
460
|
}`}
|
|
381
461
|
>
|
|
382
462
|
{props.children}
|
|
@@ -0,0 +1,353 @@
|
|
|
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
|
+
* Click-trap fix: the grip is an invisible 6px-wide (column) / 6px-tall (row)
|
|
16
|
+
* strip with `pointer-events-auto`, so a click landing near a cell edge hits
|
|
17
|
+
* the grip instead of cell text. To keep non-drag clicks from being swallowed,
|
|
18
|
+
* mousedown no longer calls `preventDefault` — that's deferred to the first
|
|
19
|
+
* mousemove that crosses `DRAG_THRESHOLD_PX`. A mouseup without any crossing
|
|
20
|
+
* is forwarded via `document.elementFromPoint` to the element beneath the
|
|
21
|
+
* grip so PM still receives the click and places the caret.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import React, { useCallback, useEffect, useRef } from "react";
|
|
25
|
+
|
|
26
|
+
import type {
|
|
27
|
+
TableStructureContextSnapshot,
|
|
28
|
+
WordReviewEditorLayoutFacet,
|
|
29
|
+
} from "../../api/public-types";
|
|
30
|
+
import type { OverlayCoordinateSpace } from "../chrome-overlay/chrome-overlay-projector";
|
|
31
|
+
import { projectRectToOverlay } from "../chrome-overlay/chrome-overlay-projector";
|
|
32
|
+
import { DEFAULT_PX_PER_TWIP } from "../../runtime/render/render-frame-types";
|
|
33
|
+
|
|
34
|
+
const GRIP_PX = 6;
|
|
35
|
+
const DRAG_THRESHOLD_PX = 3;
|
|
36
|
+
const MIN_COLUMN_TWIPS = 720;
|
|
37
|
+
const MIN_ROW_TWIPS = 120;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Re-dispatch a click that landed on an invisible resize grip to the element
|
|
41
|
+
* beneath it. Called when a mouseup fires without any drag movement, so the
|
|
42
|
+
* user's intended target (typically PM-rendered cell text) still receives
|
|
43
|
+
* mousedown/mouseup/click and can place the caret.
|
|
44
|
+
*/
|
|
45
|
+
function forwardNonDragClick(gripEl: HTMLElement, event: MouseEvent): void {
|
|
46
|
+
const previous = gripEl.style.pointerEvents;
|
|
47
|
+
gripEl.style.pointerEvents = "none";
|
|
48
|
+
try {
|
|
49
|
+
const beneath = gripEl.ownerDocument?.elementFromPoint(
|
|
50
|
+
event.clientX,
|
|
51
|
+
event.clientY,
|
|
52
|
+
);
|
|
53
|
+
if (!beneath || beneath === gripEl) return;
|
|
54
|
+
const init: MouseEventInit = {
|
|
55
|
+
bubbles: true,
|
|
56
|
+
cancelable: true,
|
|
57
|
+
view: gripEl.ownerDocument?.defaultView ?? window,
|
|
58
|
+
clientX: event.clientX,
|
|
59
|
+
clientY: event.clientY,
|
|
60
|
+
screenX: event.screenX,
|
|
61
|
+
screenY: event.screenY,
|
|
62
|
+
button: event.button,
|
|
63
|
+
buttons: event.buttons,
|
|
64
|
+
ctrlKey: event.ctrlKey,
|
|
65
|
+
metaKey: event.metaKey,
|
|
66
|
+
shiftKey: event.shiftKey,
|
|
67
|
+
altKey: event.altKey,
|
|
68
|
+
};
|
|
69
|
+
beneath.dispatchEvent(new MouseEvent("mousedown", init));
|
|
70
|
+
beneath.dispatchEvent(new MouseEvent("mouseup", init));
|
|
71
|
+
beneath.dispatchEvent(new MouseEvent("click", init));
|
|
72
|
+
} finally {
|
|
73
|
+
gripEl.style.pointerEvents = previous;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface TwTableGripLayerProps {
|
|
78
|
+
facet: WordReviewEditorLayoutFacet;
|
|
79
|
+
tableContext: TableStructureContextSnapshot | null;
|
|
80
|
+
space?: OverlayCoordinateSpace;
|
|
81
|
+
disabled?: boolean;
|
|
82
|
+
onSetColumnWidth?: (columnIndex: number, twips: number) => void;
|
|
83
|
+
onSetRowHeight?: (
|
|
84
|
+
rowIndex: number,
|
|
85
|
+
twips: number,
|
|
86
|
+
rule: "auto" | "atLeast" | "exact",
|
|
87
|
+
) => void;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function TwTableGripLayer({
|
|
91
|
+
facet,
|
|
92
|
+
tableContext,
|
|
93
|
+
space,
|
|
94
|
+
disabled,
|
|
95
|
+
onSetColumnWidth,
|
|
96
|
+
onSetRowHeight,
|
|
97
|
+
}: TwTableGripLayerProps) {
|
|
98
|
+
if (!tableContext) return null;
|
|
99
|
+
|
|
100
|
+
const frame =
|
|
101
|
+
typeof facet.getRenderFrame === "function"
|
|
102
|
+
? (facet.getRenderFrame() ?? null)
|
|
103
|
+
: null;
|
|
104
|
+
if (!frame) return null;
|
|
105
|
+
|
|
106
|
+
const blockId = `table-${tableContext.tableBlockIndex}`;
|
|
107
|
+
const pageIndex = facet.getFirstPageIndexForBlock(blockId) ?? 0;
|
|
108
|
+
const plan = facet.getTableRenderPlan(blockId, pageIndex);
|
|
109
|
+
if (!plan) return null;
|
|
110
|
+
|
|
111
|
+
const pxPerTwip =
|
|
112
|
+
typeof facet.getRenderZoom === "function"
|
|
113
|
+
? (facet.getRenderZoom()?.pxPerTwip ?? DEFAULT_PX_PER_TWIP)
|
|
114
|
+
: DEFAULT_PX_PER_TWIP;
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
<>
|
|
118
|
+
{plan.columnResizeHandles.map((handle) => {
|
|
119
|
+
const rect = frame.anchorIndex.byTableColumnEdge(
|
|
120
|
+
blockId,
|
|
121
|
+
handle.columnIndex,
|
|
122
|
+
);
|
|
123
|
+
if (!rect) return null;
|
|
124
|
+
const pos = projectRectToOverlay(rect, space);
|
|
125
|
+
return (
|
|
126
|
+
<ColResizeGrip
|
|
127
|
+
key={`col-${blockId}-${handle.columnIndex}`}
|
|
128
|
+
pos={pos}
|
|
129
|
+
colIndex={handle.columnIndex}
|
|
130
|
+
originalTwips={plan.columnsTwips[handle.columnIndex] ?? 720}
|
|
131
|
+
pxPerTwip={pxPerTwip}
|
|
132
|
+
disabled={!!disabled || !onSetColumnWidth}
|
|
133
|
+
onCommit={onSetColumnWidth}
|
|
134
|
+
/>
|
|
135
|
+
);
|
|
136
|
+
})}
|
|
137
|
+
{Array.from({ length: tableContext.rowCount }, (_, rowIndex) => {
|
|
138
|
+
const rect = frame.anchorIndex.byTableRowEdge(blockId, rowIndex);
|
|
139
|
+
if (!rect) return null;
|
|
140
|
+
const pos = projectRectToOverlay(rect, space);
|
|
141
|
+
return (
|
|
142
|
+
<RowResizeGrip
|
|
143
|
+
key={`row-${blockId}-${rowIndex}`}
|
|
144
|
+
pos={pos}
|
|
145
|
+
rowIndex={rowIndex}
|
|
146
|
+
pxPerTwip={pxPerTwip}
|
|
147
|
+
disabled={!!disabled || !onSetRowHeight}
|
|
148
|
+
onCommit={onSetRowHeight}
|
|
149
|
+
/>
|
|
150
|
+
);
|
|
151
|
+
})}
|
|
152
|
+
</>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
// Column resize grip
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
|
|
160
|
+
interface ColResizeGripProps {
|
|
161
|
+
pos: { left: string; top: string; width: string; height: string };
|
|
162
|
+
colIndex: number;
|
|
163
|
+
originalTwips: number;
|
|
164
|
+
pxPerTwip: number;
|
|
165
|
+
disabled: boolean;
|
|
166
|
+
onCommit?: (columnIndex: number, twips: number) => void;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function ColResizeGrip({
|
|
170
|
+
pos,
|
|
171
|
+
colIndex,
|
|
172
|
+
originalTwips,
|
|
173
|
+
pxPerTwip,
|
|
174
|
+
disabled,
|
|
175
|
+
onCommit,
|
|
176
|
+
}: ColResizeGripProps) {
|
|
177
|
+
const dragRef = useRef<{
|
|
178
|
+
startX: number;
|
|
179
|
+
originalTwips: number;
|
|
180
|
+
dragStarted: boolean;
|
|
181
|
+
gripEl: HTMLElement;
|
|
182
|
+
} | null>(null);
|
|
183
|
+
|
|
184
|
+
const handleMouseDown = useCallback(
|
|
185
|
+
(e: React.MouseEvent<HTMLElement>) => {
|
|
186
|
+
if (disabled) return;
|
|
187
|
+
dragRef.current = {
|
|
188
|
+
startX: e.clientX,
|
|
189
|
+
originalTwips,
|
|
190
|
+
dragStarted: false,
|
|
191
|
+
gripEl: e.currentTarget,
|
|
192
|
+
};
|
|
193
|
+
},
|
|
194
|
+
[disabled, originalTwips],
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
useEffect(() => {
|
|
198
|
+
const handleMove = (e: MouseEvent) => {
|
|
199
|
+
const drag = dragRef.current;
|
|
200
|
+
if (!drag) return;
|
|
201
|
+
if (!drag.dragStarted) {
|
|
202
|
+
if (Math.abs(e.clientX - drag.startX) < DRAG_THRESHOLD_PX) return;
|
|
203
|
+
drag.dragStarted = true;
|
|
204
|
+
}
|
|
205
|
+
e.preventDefault();
|
|
206
|
+
};
|
|
207
|
+
const handleUp = (e: MouseEvent) => {
|
|
208
|
+
const drag = dragRef.current;
|
|
209
|
+
if (!drag) return;
|
|
210
|
+
dragRef.current = null;
|
|
211
|
+
if (!drag.dragStarted) {
|
|
212
|
+
forwardNonDragClick(drag.gripEl, e);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
const deltaTwips = (e.clientX - drag.startX) / pxPerTwip;
|
|
216
|
+
const newTwips = Math.max(
|
|
217
|
+
MIN_COLUMN_TWIPS,
|
|
218
|
+
Math.round(drag.originalTwips + deltaTwips),
|
|
219
|
+
);
|
|
220
|
+
onCommit?.(colIndex, newTwips);
|
|
221
|
+
};
|
|
222
|
+
window.addEventListener("mousemove", handleMove);
|
|
223
|
+
window.addEventListener("mouseup", handleUp);
|
|
224
|
+
return () => {
|
|
225
|
+
window.removeEventListener("mousemove", handleMove);
|
|
226
|
+
window.removeEventListener("mouseup", handleUp);
|
|
227
|
+
};
|
|
228
|
+
}, [colIndex, pxPerTwip, onCommit]);
|
|
229
|
+
|
|
230
|
+
return (
|
|
231
|
+
<div
|
|
232
|
+
role="separator"
|
|
233
|
+
aria-orientation="vertical"
|
|
234
|
+
aria-label={`Resize column ${colIndex + 1}`}
|
|
235
|
+
data-testid={`col-resize-grip-${colIndex}`}
|
|
236
|
+
className={[
|
|
237
|
+
"pointer-events-auto absolute",
|
|
238
|
+
disabled
|
|
239
|
+
? "cursor-default opacity-0"
|
|
240
|
+
: "cursor-col-resize opacity-0 hover:opacity-100",
|
|
241
|
+
"transition-opacity",
|
|
242
|
+
"bg-accent",
|
|
243
|
+
].join(" ")}
|
|
244
|
+
style={{
|
|
245
|
+
left: `calc(${pos.left} - ${GRIP_PX / 2}px)`,
|
|
246
|
+
top: pos.top,
|
|
247
|
+
width: `${GRIP_PX}px`,
|
|
248
|
+
height: pos.height,
|
|
249
|
+
}}
|
|
250
|
+
onMouseDown={handleMouseDown}
|
|
251
|
+
/>
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ---------------------------------------------------------------------------
|
|
256
|
+
// Row resize grip
|
|
257
|
+
// ---------------------------------------------------------------------------
|
|
258
|
+
|
|
259
|
+
interface RowResizeGripProps {
|
|
260
|
+
pos: { left: string; top: string; width: string; height: string };
|
|
261
|
+
rowIndex: number;
|
|
262
|
+
pxPerTwip: number;
|
|
263
|
+
disabled: boolean;
|
|
264
|
+
onCommit?: (
|
|
265
|
+
rowIndex: number,
|
|
266
|
+
twips: number,
|
|
267
|
+
rule: "auto" | "atLeast" | "exact",
|
|
268
|
+
) => void;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function RowResizeGrip({
|
|
272
|
+
pos,
|
|
273
|
+
rowIndex,
|
|
274
|
+
pxPerTwip,
|
|
275
|
+
disabled,
|
|
276
|
+
onCommit,
|
|
277
|
+
}: RowResizeGripProps) {
|
|
278
|
+
const dragRef = useRef<{
|
|
279
|
+
startY: number;
|
|
280
|
+
dragStarted: boolean;
|
|
281
|
+
gripEl: HTMLElement;
|
|
282
|
+
} | null>(null);
|
|
283
|
+
|
|
284
|
+
const handleMouseDown = useCallback(
|
|
285
|
+
(e: React.MouseEvent<HTMLElement>) => {
|
|
286
|
+
if (disabled) return;
|
|
287
|
+
dragRef.current = {
|
|
288
|
+
startY: e.clientY,
|
|
289
|
+
dragStarted: false,
|
|
290
|
+
gripEl: e.currentTarget,
|
|
291
|
+
};
|
|
292
|
+
},
|
|
293
|
+
[disabled],
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
useEffect(() => {
|
|
297
|
+
const handleMove = (e: MouseEvent) => {
|
|
298
|
+
const drag = dragRef.current;
|
|
299
|
+
if (!drag) return;
|
|
300
|
+
if (!drag.dragStarted) {
|
|
301
|
+
if (Math.abs(e.clientY - drag.startY) < DRAG_THRESHOLD_PX) return;
|
|
302
|
+
drag.dragStarted = true;
|
|
303
|
+
}
|
|
304
|
+
e.preventDefault();
|
|
305
|
+
};
|
|
306
|
+
const handleUp = (e: MouseEvent) => {
|
|
307
|
+
const drag = dragRef.current;
|
|
308
|
+
if (!drag) return;
|
|
309
|
+
dragRef.current = null;
|
|
310
|
+
if (!drag.dragStarted) {
|
|
311
|
+
forwardNonDragClick(drag.gripEl, e);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
const deltaY = e.clientY - drag.startY;
|
|
315
|
+
const deltaTwips = deltaY / pxPerTwip;
|
|
316
|
+
// We don't know the current row height from the grip alone; use atLeast
|
|
317
|
+
// so that a positive drag expands and a negative drag collapses to auto.
|
|
318
|
+
const newTwips = Math.max(MIN_ROW_TWIPS, Math.round(deltaTwips));
|
|
319
|
+
const rule = newTwips > 0 ? "atLeast" : "auto";
|
|
320
|
+
onCommit?.(rowIndex, newTwips, rule as "atLeast");
|
|
321
|
+
};
|
|
322
|
+
window.addEventListener("mousemove", handleMove);
|
|
323
|
+
window.addEventListener("mouseup", handleUp);
|
|
324
|
+
return () => {
|
|
325
|
+
window.removeEventListener("mousemove", handleMove);
|
|
326
|
+
window.removeEventListener("mouseup", handleUp);
|
|
327
|
+
};
|
|
328
|
+
}, [rowIndex, pxPerTwip, onCommit]);
|
|
329
|
+
|
|
330
|
+
return (
|
|
331
|
+
<div
|
|
332
|
+
role="separator"
|
|
333
|
+
aria-orientation="horizontal"
|
|
334
|
+
aria-label={`Resize row ${rowIndex + 1}`}
|
|
335
|
+
data-testid={`row-resize-grip-${rowIndex}`}
|
|
336
|
+
className={[
|
|
337
|
+
"pointer-events-auto absolute",
|
|
338
|
+
disabled
|
|
339
|
+
? "cursor-default opacity-0"
|
|
340
|
+
: "cursor-row-resize opacity-0 hover:opacity-100",
|
|
341
|
+
"transition-opacity",
|
|
342
|
+
"bg-accent",
|
|
343
|
+
].join(" ")}
|
|
344
|
+
style={{
|
|
345
|
+
left: pos.left,
|
|
346
|
+
top: `calc(${pos.top} - ${GRIP_PX / 2}px)`,
|
|
347
|
+
width: pos.width,
|
|
348
|
+
height: `${GRIP_PX}px`,
|
|
349
|
+
}}
|
|
350
|
+
onMouseDown={handleMouseDown}
|
|
351
|
+
/>
|
|
352
|
+
);
|
|
353
|
+
}
|
|
@@ -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,8 @@
|
|
|
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
|
-
|
|
12
|
-
type TwWorkspaceViewSwitcherProps,
|
|
13
|
-
type WorkspaceView,
|
|
14
|
-
type WorkspaceViewAction,
|
|
15
|
-
} from "./tw-workspace-view-switcher";
|
|
10
|
+
export { TwScopeCard, type TwScopeCardProps } from "./tw-scope-card";
|
|
11
|
+
export { TwScopeCardLayer, type TwScopeCardLayerProps } from "./tw-scope-card-layer";
|
|
16
12
|
export {
|
|
17
13
|
inflateRect,
|
|
18
14
|
projectRectToOverlay,
|