@beyondwork/docx-react-component 1.0.21 → 1.0.23
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/README.md +763 -38
- package/package.json +25 -36
- package/src/api/public-types.ts +66 -1
- package/src/core/commands/index.ts +574 -5
- package/src/index.ts +5 -0
- package/src/io/docx-session.ts +181 -2
- package/src/io/export/serialize-main-document.ts +21 -1
- package/src/io/normalize/normalize-text.ts +4 -0
- package/src/io/ooxml/parse-main-document.ts +88 -7
- package/src/model/canonical-document.ts +22 -0
- package/src/review/store/revision-store.ts +1 -0
- package/src/review/store/revision-types.ts +2 -0
- package/src/runtime/document-runtime.ts +503 -51
- package/src/runtime/session-capabilities.ts +6 -5
- package/src/runtime/surface-projection.ts +2 -0
- package/src/runtime/table-schema.ts +2 -0
- package/src/runtime/workflow-markup.ts +5 -1
- package/src/ui/WordReviewEditor.tsx +661 -132
- package/src/ui/editor-runtime-boundary.ts +10 -1
- package/src/ui/editor-shell-view.tsx +8 -0
- package/src/ui/editor-surface-controller.tsx +5 -0
- package/src/ui/headless/selection-toolbar-model.ts +12 -0
- package/src/ui-tailwind/chrome/tw-suggestion-card.tsx +139 -0
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +6 -0
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +44 -16
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +2 -0
- package/src/ui-tailwind/editor-surface/surface-build-keys.ts +4 -0
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +127 -10
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +82 -1
- package/src/ui-tailwind/status/tw-status-bar.tsx +4 -1
- package/src/ui-tailwind/theme/editor-theme.css +10 -0
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +21 -5
- package/src/ui-tailwind/tw-review-workspace.tsx +110 -32
|
@@ -33,6 +33,58 @@ interface OpenExplicitRowSpan {
|
|
|
33
33
|
remainingRows: number;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
function readRowPadding(node: PMNode, side: "gridBefore" | "gridAfter"): number {
|
|
37
|
+
const value = node.attrs[side];
|
|
38
|
+
return typeof value === "number" && value > 0 ? value : 0;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function sumGridColumns(
|
|
42
|
+
gridColumns: readonly number[],
|
|
43
|
+
start: number,
|
|
44
|
+
count: number,
|
|
45
|
+
): number {
|
|
46
|
+
let total = 0;
|
|
47
|
+
for (let index = start; index < start + count; index += 1) {
|
|
48
|
+
total += gridColumns[index] ?? 0;
|
|
49
|
+
}
|
|
50
|
+
return total;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function removePaddingCells(rowElement: HTMLTableRowElement): void {
|
|
54
|
+
for (const cell of Array.from(rowElement.cells)) {
|
|
55
|
+
if (cell.hasAttribute("data-row-padding")) {
|
|
56
|
+
cell.remove();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function createPaddingCell(colSpan: number, widthTwips: number): HTMLTableCellElement {
|
|
62
|
+
const cell = document.createElement("td");
|
|
63
|
+
cell.setAttribute("data-row-padding", "true");
|
|
64
|
+
cell.setAttribute("aria-hidden", "true");
|
|
65
|
+
cell.colSpan = Math.max(1, colSpan);
|
|
66
|
+
cell.style.border = "none";
|
|
67
|
+
cell.style.padding = "0";
|
|
68
|
+
cell.style.background = "transparent";
|
|
69
|
+
if (widthTwips > 0) {
|
|
70
|
+
cell.style.width = `${widthTwips / 20}pt`;
|
|
71
|
+
}
|
|
72
|
+
return cell;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function nodesAreOnlyRowPadding(nodes: ArrayLike<Node> & { item?(index: number): Node | null }): boolean {
|
|
76
|
+
for (let index = 0; index < nodes.length; index += 1) {
|
|
77
|
+
const node = nodes.item ? nodes.item(index) : nodes[index];
|
|
78
|
+
if (!node) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (!(node instanceof HTMLElement) || !node.hasAttribute("data-row-padding")) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
|
|
36
88
|
function resolveRenderedColspan(node: PMNode): number {
|
|
37
89
|
const colspan = node.attrs.colspan as number | undefined;
|
|
38
90
|
if (typeof colspan === "number" && colspan > 1) {
|
|
@@ -172,6 +224,7 @@ function computeTableLayout(tableNode: PMNode): TableCellLayout[][] {
|
|
|
172
224
|
function syncRenderedTableLayout(tableBody: HTMLTableSectionElement, tableNode: PMNode): void {
|
|
173
225
|
const rowLayouts = computeTableLayout(tableNode);
|
|
174
226
|
const rowElements = Array.from(tableBody.rows);
|
|
227
|
+
const gridColumns = Array.isArray(tableNode.attrs.gridColumns) ? tableNode.attrs.gridColumns as number[] : [];
|
|
175
228
|
|
|
176
229
|
for (let rowIndex = 0; rowIndex < rowLayouts.length; rowIndex += 1) {
|
|
177
230
|
const rowLayout = rowLayouts[rowIndex];
|
|
@@ -180,6 +233,7 @@ function syncRenderedTableLayout(tableBody: HTMLTableSectionElement, tableNode:
|
|
|
180
233
|
continue;
|
|
181
234
|
}
|
|
182
235
|
|
|
236
|
+
removePaddingCells(rowElement);
|
|
183
237
|
const cellElements = Array.from(rowElement.cells);
|
|
184
238
|
for (const cellLayout of rowLayout) {
|
|
185
239
|
const element = cellElements[cellLayout.cellIndex];
|
|
@@ -199,6 +253,22 @@ function syncRenderedTableLayout(tableBody: HTMLTableSectionElement, tableNode:
|
|
|
199
253
|
element.removeAttribute("data-vertical-merge-hidden");
|
|
200
254
|
}
|
|
201
255
|
}
|
|
256
|
+
|
|
257
|
+
const rowNode = tableNode.child(rowIndex);
|
|
258
|
+
const gridBefore = readRowPadding(rowNode, "gridBefore");
|
|
259
|
+
const gridAfter = readRowPadding(rowNode, "gridAfter");
|
|
260
|
+
if (gridBefore > 0) {
|
|
261
|
+
rowElement.insertBefore(
|
|
262
|
+
createPaddingCell(gridBefore, sumGridColumns(gridColumns, 0, gridBefore)),
|
|
263
|
+
rowElement.firstChild,
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
if (gridAfter > 0) {
|
|
267
|
+
const start = Math.max(0, gridColumns.length - gridAfter);
|
|
268
|
+
rowElement.appendChild(
|
|
269
|
+
createPaddingCell(gridAfter, sumGridColumns(gridColumns, start, gridAfter)),
|
|
270
|
+
);
|
|
271
|
+
}
|
|
202
272
|
}
|
|
203
273
|
}
|
|
204
274
|
|
|
@@ -253,7 +323,18 @@ export class TableNodeView {
|
|
|
253
323
|
}
|
|
254
324
|
|
|
255
325
|
ignoreMutation(record: ViewMutationRecord): boolean {
|
|
256
|
-
|
|
326
|
+
if (record.type === "attributes") {
|
|
327
|
+
return record.target === this.dom
|
|
328
|
+
|| (record.target instanceof HTMLElement && record.target.hasAttribute("data-row-padding"));
|
|
329
|
+
}
|
|
330
|
+
if (record.type === "childList" && this.dom.contains(record.target)) {
|
|
331
|
+
const addedNodes = record.addedNodes as ArrayLike<Node> & { item?(index: number): Node | null };
|
|
332
|
+
const removedNodes = record.removedNodes as ArrayLike<Node> & { item?(index: number): Node | null };
|
|
333
|
+
const addedOkay = (addedNodes?.length ?? 0) === 0 || nodesAreOnlyRowPadding(addedNodes);
|
|
334
|
+
const removedOkay = (removedNodes?.length ?? 0) === 0 || nodesAreOnlyRowPadding(removedNodes);
|
|
335
|
+
return addedOkay && removedOkay;
|
|
336
|
+
}
|
|
337
|
+
return false;
|
|
257
338
|
}
|
|
258
339
|
|
|
259
340
|
private scheduleLayoutSync(): void {
|
|
@@ -22,7 +22,10 @@ export function TwStatusBar(props: TwStatusBarProps) {
|
|
|
22
22
|
: "Ready";
|
|
23
23
|
|
|
24
24
|
return (
|
|
25
|
-
<footer
|
|
25
|
+
<footer
|
|
26
|
+
data-testid="status-bar"
|
|
27
|
+
className="flex h-7 shrink-0 items-center gap-4 border-t border-border px-3 text-xs text-tertiary"
|
|
28
|
+
>
|
|
26
29
|
<span className="flex items-center gap-1.5">
|
|
27
30
|
<span
|
|
28
31
|
className={`inline-block h-1.5 w-1.5 rounded-full ${
|
|
@@ -323,6 +323,16 @@
|
|
|
323
323
|
background: var(--wre-workflow-rail-color, var(--color-border-strong));
|
|
324
324
|
}
|
|
325
325
|
|
|
326
|
+
.prosemirror-surface .ProseMirror .wre-workflow-rail-active::before {
|
|
327
|
+
width: 0.3125rem;
|
|
328
|
+
opacity: 1;
|
|
329
|
+
box-shadow: 0 0 0 1px color-mix(in oklab, var(--wre-workflow-rail-color, var(--color-border-strong)) 30%, transparent);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
.prosemirror-surface .ProseMirror .wre-workflow-inline-active {
|
|
333
|
+
box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--wre-workflow-rail-color, var(--color-border-strong)) 28%, transparent);
|
|
334
|
+
}
|
|
335
|
+
|
|
326
336
|
.prosemirror-surface .ProseMirror .wre-workflow-rail-edit {
|
|
327
337
|
--wre-workflow-rail-color: var(--color-accent);
|
|
328
338
|
background: color-mix(in srgb, var(--color-accent) 7%, transparent);
|
|
@@ -62,6 +62,7 @@ export interface TwToolbarProps {
|
|
|
62
62
|
compatibility?: CompatibilityPanelSnapshot;
|
|
63
63
|
warnings?: EditorWarning[];
|
|
64
64
|
blockedReasons?: WorkflowBlockedCommandReason[];
|
|
65
|
+
interactionPolicy?: ToolbarInteractionPolicy;
|
|
65
66
|
workspaceMode: WorkspaceMode;
|
|
66
67
|
zoomLevel?: ZoomLevel;
|
|
67
68
|
formattingState?: FormattingStateSnapshot;
|
|
@@ -99,6 +100,13 @@ export interface TwToolbarProps {
|
|
|
99
100
|
onShowTrackedChangesChange: (show: boolean) => void;
|
|
100
101
|
}
|
|
101
102
|
|
|
103
|
+
export interface ToolbarInteractionPolicy {
|
|
104
|
+
mode: "edit" | "suggest" | "comment" | "view" | "blocked";
|
|
105
|
+
canFormatText: boolean;
|
|
106
|
+
canInsertStructural: boolean;
|
|
107
|
+
canAddComment: boolean;
|
|
108
|
+
}
|
|
109
|
+
|
|
102
110
|
export function getSupportedZoomPresets(): ReadonlyArray<number> {
|
|
103
111
|
return [75, 100, 125, 150];
|
|
104
112
|
}
|
|
@@ -123,7 +131,9 @@ export function TwToolbar(props: TwToolbarProps) {
|
|
|
123
131
|
const isPageMode = workspaceMode === "page";
|
|
124
132
|
const paragraphStyles = props.styleCatalog?.paragraphs ?? [];
|
|
125
133
|
const zoomLevel = props.zoomLevel ?? 100;
|
|
126
|
-
const canEdit = caps ? caps.canEdit : false;
|
|
134
|
+
const canEdit = props.interactionPolicy?.canFormatText ?? (caps ? caps.canEdit : false);
|
|
135
|
+
const canInsertStructural = props.interactionPolicy?.canInsertStructural ?? canEdit;
|
|
136
|
+
const canAddComment = props.interactionPolicy?.canAddComment ?? (caps ? caps.canAddComment : false);
|
|
127
137
|
const zoomLabel =
|
|
128
138
|
typeof zoomLevel === "number"
|
|
129
139
|
? `${zoomLevel}%`
|
|
@@ -173,14 +183,14 @@ export function TwToolbar(props: TwToolbarProps) {
|
|
|
173
183
|
icon={Bold}
|
|
174
184
|
label="Bold"
|
|
175
185
|
active={props.formattingState?.bold ?? false}
|
|
176
|
-
disabled={
|
|
186
|
+
disabled={!canEdit}
|
|
177
187
|
onClick={props.onToggleBold}
|
|
178
188
|
/>
|
|
179
189
|
<TwToolbarIconButton
|
|
180
190
|
icon={Italic}
|
|
181
191
|
label="Italic"
|
|
182
192
|
active={props.formattingState?.italic ?? false}
|
|
183
|
-
disabled={
|
|
193
|
+
disabled={!canEdit}
|
|
184
194
|
onClick={props.onToggleItalic}
|
|
185
195
|
/>
|
|
186
196
|
<TwToolbarIconButton
|
|
@@ -238,7 +248,7 @@ export function TwToolbar(props: TwToolbarProps) {
|
|
|
238
248
|
onClick={props.onIndent}
|
|
239
249
|
/>
|
|
240
250
|
<ToolbarInsertMenu
|
|
241
|
-
disabled={!
|
|
251
|
+
disabled={!canInsertStructural}
|
|
242
252
|
onInsertImage={props.onInsertImage}
|
|
243
253
|
onInsertPageBreak={props.onInsertPageBreak}
|
|
244
254
|
onInsertSectionBreak={props.onInsertSectionBreak}
|
|
@@ -275,7 +285,7 @@ export function TwToolbar(props: TwToolbarProps) {
|
|
|
275
285
|
<TwToolbarIconButton
|
|
276
286
|
icon={MessageSquare}
|
|
277
287
|
label="Add comment"
|
|
278
|
-
disabled={
|
|
288
|
+
disabled={!canAddComment}
|
|
279
289
|
emphasis
|
|
280
290
|
onClick={props.onAddComment}
|
|
281
291
|
/>
|
|
@@ -554,6 +564,8 @@ function ToolbarParagraphStyleSelect(props: {
|
|
|
554
564
|
>
|
|
555
565
|
<Select.Trigger
|
|
556
566
|
aria-label="Paragraph style"
|
|
567
|
+
aria-disabled={props.disabled || undefined}
|
|
568
|
+
data-disabled={props.disabled ? "" : undefined}
|
|
557
569
|
onMouseDown={preserveEditorSelectionMouseDown}
|
|
558
570
|
className={`inline-flex h-7 min-w-[8.5rem] items-center justify-between gap-2 rounded-md border border-border bg-canvas px-2.5 text-xs font-medium text-primary transition-colors hover:bg-surface outline-none disabled:cursor-not-allowed disabled:opacity-40 ${focusRingClass}`}
|
|
559
571
|
>
|
|
@@ -601,6 +613,8 @@ function ToolbarFontFamilySelect(props: {
|
|
|
601
613
|
>
|
|
602
614
|
<Select.Trigger
|
|
603
615
|
aria-label="Font family"
|
|
616
|
+
aria-disabled={props.disabled || undefined}
|
|
617
|
+
data-disabled={props.disabled ? "" : undefined}
|
|
604
618
|
onMouseDown={preserveEditorSelectionMouseDown}
|
|
605
619
|
className={`inline-flex h-7 min-w-[7rem] items-center justify-between gap-2 rounded-md border border-border bg-canvas px-2 text-xs font-medium text-primary transition-colors hover:bg-surface outline-none disabled:cursor-not-allowed disabled:opacity-40 ${focusRingClass}`}
|
|
606
620
|
>
|
|
@@ -649,6 +663,8 @@ function ToolbarFontSizeSelect(props: {
|
|
|
649
663
|
>
|
|
650
664
|
<Select.Trigger
|
|
651
665
|
aria-label="Font size"
|
|
666
|
+
aria-disabled={props.disabled || undefined}
|
|
667
|
+
data-disabled={props.disabled ? "" : undefined}
|
|
652
668
|
onMouseDown={preserveEditorSelectionMouseDown}
|
|
653
669
|
className={`inline-flex h-7 min-w-[4rem] items-center justify-between gap-2 rounded-md border border-border bg-canvas px-2 text-xs font-medium text-primary transition-colors hover:bg-surface outline-none disabled:cursor-not-allowed disabled:opacity-40 ${focusRingClass}`}
|
|
654
670
|
>
|
|
@@ -28,6 +28,7 @@ import type {
|
|
|
28
28
|
StyleCatalogSnapshot,
|
|
29
29
|
SurfaceBlockSnapshot,
|
|
30
30
|
TrackedChangeEntrySnapshot,
|
|
31
|
+
WordReviewEditorChromeVisibility,
|
|
31
32
|
WorkflowScopeSnapshot,
|
|
32
33
|
WorkspaceMode,
|
|
33
34
|
ZoomLevel,
|
|
@@ -49,6 +50,7 @@ import type { SessionCapabilities } from "../runtime/session-capabilities";
|
|
|
49
50
|
import type {
|
|
50
51
|
SelectionToolbarAnchor,
|
|
51
52
|
SelectionToolbarModel,
|
|
53
|
+
SuggestionCardModel,
|
|
52
54
|
} from "../ui/headless/selection-toolbar-model";
|
|
53
55
|
import type { MarkupDisplay } from "../ui/headless/comment-decoration-model";
|
|
54
56
|
import type { EditorCommandBag } from "../ui/editor-command-bag.ts";
|
|
@@ -59,10 +61,13 @@ import { TwLayoutPanel } from "./chrome/tw-layout-panel";
|
|
|
59
61
|
import { TwObjectContextToolbar, type ActiveObjectContext } from "./chrome/tw-object-context-toolbar";
|
|
60
62
|
import { TwPageRuler } from "./chrome/tw-page-ruler";
|
|
61
63
|
import { TwSelectionToolbar } from "./chrome/tw-selection-toolbar";
|
|
64
|
+
import { TwSuggestionCard } from "./chrome/tw-suggestion-card";
|
|
62
65
|
import { TwTableContextToolbar } from "./chrome/tw-table-context-toolbar";
|
|
63
66
|
import { TwReviewRail, type ReviewRailTab } from "./review/tw-review-rail";
|
|
64
67
|
import { TwStatusBar } from "./status/tw-status-bar";
|
|
65
|
-
import { TwToolbar } from "./toolbar/tw-toolbar";
|
|
68
|
+
import { TwToolbar, type ToolbarInteractionPolicy } from "./toolbar/tw-toolbar";
|
|
69
|
+
|
|
70
|
+
export type ReviewWorkspaceChromeVisibility = WordReviewEditorChromeVisibility;
|
|
66
71
|
|
|
67
72
|
export interface TwReviewWorkspaceProps {
|
|
68
73
|
snapshot: RuntimeRenderSnapshot;
|
|
@@ -84,6 +89,7 @@ export interface TwReviewWorkspaceProps {
|
|
|
84
89
|
interactionGuardSnapshot?: InteractionGuardSnapshot;
|
|
85
90
|
commands: EditorCommandBag;
|
|
86
91
|
selectionToolbar?: SelectionToolbarModel | null;
|
|
92
|
+
suggestionCard?: SuggestionCardModel | null;
|
|
87
93
|
selectionToolbarAnchor?: SelectionToolbarAnchor | null;
|
|
88
94
|
documentNavigation?: DocumentNavigationSnapshot;
|
|
89
95
|
onWorkspaceModeChange?: (value: WorkspaceMode) => void;
|
|
@@ -170,6 +176,10 @@ export interface TwReviewWorkspaceProps {
|
|
|
170
176
|
onAddCommentFromSelection?: () => void;
|
|
171
177
|
onExport?: () => void;
|
|
172
178
|
onDismissSelectionToolbar?: () => void;
|
|
179
|
+
onAcceptSuggestion?: () => void;
|
|
180
|
+
onRejectSuggestion?: () => void;
|
|
181
|
+
onEditSuggestion?: () => void;
|
|
182
|
+
onAddCommentFromSuggestion?: () => void;
|
|
173
183
|
onSelectionToolbarFocusCapture?: FocusEventHandler<HTMLDivElement>;
|
|
174
184
|
onSelectionToolbarBlurCapture?: FocusEventHandler<HTMLDivElement>;
|
|
175
185
|
selectionToolbarRef?: Ref<HTMLDivElement>;
|
|
@@ -196,6 +206,7 @@ export interface TwReviewWorkspaceProps {
|
|
|
196
206
|
onRestartNumbering?: () => void;
|
|
197
207
|
onContinueNumbering?: () => void;
|
|
198
208
|
onNavigateHeading?: (headingId: string) => void;
|
|
209
|
+
chromeVisibility?: Partial<ReviewWorkspaceChromeVisibility>;
|
|
199
210
|
}
|
|
200
211
|
|
|
201
212
|
export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
@@ -220,7 +231,17 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
220
231
|
props.interactionGuardSnapshot?.blockedReasons ??
|
|
221
232
|
props.workflowScopeSnapshot?.blockedReasons ??
|
|
222
233
|
[];
|
|
223
|
-
const
|
|
234
|
+
const chromeVisibility: ReviewWorkspaceChromeVisibility = {
|
|
235
|
+
toolbar: true,
|
|
236
|
+
alerts: true,
|
|
237
|
+
selectionOverlay: true,
|
|
238
|
+
contextToolbars: true,
|
|
239
|
+
pageChrome: true,
|
|
240
|
+
statusBar: true,
|
|
241
|
+
reviewRail: true,
|
|
242
|
+
...props.chromeVisibility,
|
|
243
|
+
};
|
|
244
|
+
const showReviewRail = chromeVisibility.reviewRail && (caps?.reviewRailVisible ?? true);
|
|
224
245
|
const headings = props.documentNavigation?.headings ?? [];
|
|
225
246
|
const headerVariant = snapshot.pageLayout?.headerVariants[0]?.variant ?? "default";
|
|
226
247
|
const footerVariant = snapshot.pageLayout?.footerVariants[0]?.variant ?? "default";
|
|
@@ -267,6 +288,23 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
267
288
|
isPageWorkspace &&
|
|
268
289
|
snapshot.activeStory.kind === "main" &&
|
|
269
290
|
shouldHidePageBorderForSelection(viewState.selection);
|
|
291
|
+
const effectiveSelectionMode = props.interactionGuardSnapshot?.effectiveMode ?? "edit";
|
|
292
|
+
const allowLocalChromeMutations = Boolean(caps?.canEdit) && effectiveSelectionMode === "edit";
|
|
293
|
+
const pageChromeReadOnly =
|
|
294
|
+
snapshot.readOnly ||
|
|
295
|
+
snapshot.activeStory.kind !== "main" ||
|
|
296
|
+
effectiveSelectionMode !== "edit";
|
|
297
|
+
const toolbarInteractionPolicy: ToolbarInteractionPolicy | undefined = caps
|
|
298
|
+
? {
|
|
299
|
+
mode: effectiveSelectionMode,
|
|
300
|
+
canFormatText: caps.canEdit && effectiveSelectionMode === "edit",
|
|
301
|
+
canInsertStructural: caps.canEdit && effectiveSelectionMode === "edit",
|
|
302
|
+
canAddComment:
|
|
303
|
+
caps.canAddComment &&
|
|
304
|
+
effectiveSelectionMode !== "view" &&
|
|
305
|
+
effectiveSelectionMode !== "blocked",
|
|
306
|
+
}
|
|
307
|
+
: undefined;
|
|
270
308
|
|
|
271
309
|
useEffect(() => {
|
|
272
310
|
recordPerfSample("workspace.chrome");
|
|
@@ -294,11 +332,12 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
294
332
|
return (
|
|
295
333
|
<Tooltip.Provider delayDuration={400}>
|
|
296
334
|
<div className="flex h-full flex-col bg-canvas text-primary">
|
|
297
|
-
<TwToolbar
|
|
335
|
+
{chromeVisibility.toolbar ? <TwToolbar
|
|
298
336
|
sourceLabel={snapshot.sourceLabel}
|
|
299
337
|
capabilities={caps}
|
|
300
338
|
compatibility={snapshot.compatibility}
|
|
301
339
|
warnings={snapshot.warnings}
|
|
340
|
+
interactionPolicy={toolbarInteractionPolicy}
|
|
302
341
|
workspaceMode={props.workspaceMode}
|
|
303
342
|
zoomLevel={props.zoomLevel}
|
|
304
343
|
formattingState={props.formattingState}
|
|
@@ -385,17 +424,17 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
385
424
|
props.onShowTrackedChangesChange(show);
|
|
386
425
|
}}
|
|
387
426
|
blockedReasons={blockedReasons}
|
|
388
|
-
/>
|
|
427
|
+
/> : null}
|
|
389
428
|
|
|
390
|
-
<TwAlertBanner
|
|
429
|
+
{chromeVisibility.alerts ? <TwAlertBanner
|
|
391
430
|
snapshot={snapshot}
|
|
392
431
|
preserveOnlyCount={preserveOnlyCount}
|
|
393
432
|
workflowBlockedReasons={blockedReasons}
|
|
394
|
-
/>
|
|
433
|
+
/> : null}
|
|
395
434
|
|
|
396
435
|
<div className="flex flex-1 min-h-0">
|
|
397
436
|
{/* Collapsible document navigator — page mode only */}
|
|
398
|
-
{isPageWorkspace ? (
|
|
437
|
+
{isPageWorkspace && chromeVisibility.pageChrome ? (
|
|
399
438
|
<aside
|
|
400
439
|
aria-label="Document navigator"
|
|
401
440
|
className={`shrink-0 border-r border-border bg-surface transition-[width] duration-200 ${
|
|
@@ -459,7 +498,7 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
459
498
|
) : null}
|
|
460
499
|
|
|
461
500
|
{/* Navigator expand toggle — page mode only when collapsed */}
|
|
462
|
-
{isPageWorkspace && !navOpen ? (
|
|
501
|
+
{isPageWorkspace && chromeVisibility.pageChrome && !navOpen ? (
|
|
463
502
|
<div className="shrink-0 flex items-start pt-2 pl-1">
|
|
464
503
|
<Tooltip.Root>
|
|
465
504
|
<Tooltip.Trigger asChild>
|
|
@@ -500,7 +539,7 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
500
539
|
}`}
|
|
501
540
|
style={isPageWorkspace && zoomScale !== 1 ? { transform: `scale(${zoomScale})`, transformOrigin: "top center" } : undefined}
|
|
502
541
|
>
|
|
503
|
-
{isPageWorkspace && snapshot.pageLayout ? (
|
|
542
|
+
{isPageWorkspace && chromeVisibility.pageChrome && snapshot.pageLayout ? (
|
|
504
543
|
<div className="border-b border-border/70 bg-surface/65 px-5 py-3" data-testid="page-context-summary">
|
|
505
544
|
<div className="flex flex-wrap items-center justify-between gap-2">
|
|
506
545
|
<div className="flex flex-wrap items-center gap-2 text-xs text-secondary">
|
|
@@ -531,7 +570,7 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
531
570
|
<button
|
|
532
571
|
type="button"
|
|
533
572
|
aria-label="Link header to previous"
|
|
534
|
-
disabled={!props.onSetHeaderFooterLink}
|
|
573
|
+
disabled={!props.onSetHeaderFooterLink || !allowLocalChromeMutations}
|
|
535
574
|
onMouseDown={preserveEditorSelectionMouseDown}
|
|
536
575
|
onClick={() => {
|
|
537
576
|
dismissSelectionToolbar();
|
|
@@ -548,7 +587,7 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
548
587
|
<button
|
|
549
588
|
type="button"
|
|
550
589
|
aria-label="Link footer to previous"
|
|
551
|
-
disabled={!props.onSetHeaderFooterLink}
|
|
590
|
+
disabled={!props.onSetHeaderFooterLink || !allowLocalChromeMutations}
|
|
552
591
|
onMouseDown={preserveEditorSelectionMouseDown}
|
|
553
592
|
onClick={() => {
|
|
554
593
|
dismissSelectionToolbar();
|
|
@@ -582,13 +621,13 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
582
621
|
</div>
|
|
583
622
|
</div>
|
|
584
623
|
) : null}
|
|
585
|
-
{isPageWorkspace && snapshot.pageLayout && layoutToolsOpen ? (
|
|
624
|
+
{isPageWorkspace && chromeVisibility.pageChrome && snapshot.pageLayout && layoutToolsOpen ? (
|
|
586
625
|
<div className="px-5 pt-3">
|
|
587
626
|
<TwPageRuler
|
|
588
627
|
pageLayout={snapshot.pageLayout}
|
|
589
628
|
viewState={viewState}
|
|
590
629
|
paragraphLayout={activeParagraphLayout}
|
|
591
|
-
readOnly={
|
|
630
|
+
readOnly={pageChromeReadOnly}
|
|
592
631
|
onReturnToBody={props.onCloseStory
|
|
593
632
|
? runWithSelectionToolbarDismiss(props.onCloseStory)
|
|
594
633
|
: () => undefined}
|
|
@@ -619,7 +658,7 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
619
658
|
/>
|
|
620
659
|
<TwLayoutPanel
|
|
621
660
|
pageLayout={snapshot.pageLayout}
|
|
622
|
-
readOnly={
|
|
661
|
+
readOnly={pageChromeReadOnly}
|
|
623
662
|
onInsertSectionBreak={props.onInsertSectionBreak
|
|
624
663
|
? (type) => {
|
|
625
664
|
dismissSelectionToolbar();
|
|
@@ -647,11 +686,11 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
647
686
|
/>
|
|
648
687
|
</div>
|
|
649
688
|
) : null}
|
|
650
|
-
{contextualSurface ? (
|
|
689
|
+
{chromeVisibility.contextToolbars && contextualSurface ? (
|
|
651
690
|
<div className="px-5 pt-3 space-y-3">
|
|
652
691
|
{contextualSurface === "table" ? (
|
|
653
692
|
<TwTableContextToolbar
|
|
654
|
-
disabled={!
|
|
693
|
+
disabled={!allowLocalChromeMutations}
|
|
655
694
|
tableStyles={props.styleCatalog?.tables ?? []}
|
|
656
695
|
onSetTableStyle={props.onSetTableStyle
|
|
657
696
|
? (styleId) => {
|
|
@@ -679,7 +718,7 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
679
718
|
{contextualSurface === "image" && props.activeImageContext ? (
|
|
680
719
|
<TwImageContextToolbar
|
|
681
720
|
activeImage={props.activeImageContext}
|
|
682
|
-
disabled={!
|
|
721
|
+
disabled={!allowLocalChromeMutations}
|
|
683
722
|
onSetImageLayout={props.onSetImageLayout
|
|
684
723
|
? (mediaId, dimensions) => {
|
|
685
724
|
dismissSelectionToolbar();
|
|
@@ -699,7 +738,44 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
699
738
|
) : null}
|
|
700
739
|
</div>
|
|
701
740
|
) : null}
|
|
702
|
-
{props.
|
|
741
|
+
{chromeVisibility.selectionOverlay && props.suggestionCard && selectionToolbarPlacement ? (
|
|
742
|
+
<div className="pointer-events-none absolute inset-0 z-20" data-testid="suggestion-card-overlay">
|
|
743
|
+
<div
|
|
744
|
+
className="pointer-events-auto absolute"
|
|
745
|
+
data-placement={selectionToolbarPlacement.placement}
|
|
746
|
+
style={selectionToolbarPlacement.style}
|
|
747
|
+
>
|
|
748
|
+
<TwSuggestionCard
|
|
749
|
+
model={props.suggestionCard}
|
|
750
|
+
onFocusCapture={props.onSelectionToolbarFocusCapture}
|
|
751
|
+
onBlurCapture={props.onSelectionToolbarBlurCapture}
|
|
752
|
+
onAccept={props.onAcceptSuggestion}
|
|
753
|
+
onReject={props.onRejectSuggestion}
|
|
754
|
+
onEditSuggestion={props.onEditSuggestion}
|
|
755
|
+
onAddComment={props.onAddCommentFromSuggestion ?? props.onAddComment}
|
|
756
|
+
/>
|
|
757
|
+
</div>
|
|
758
|
+
</div>
|
|
759
|
+
) : null}
|
|
760
|
+
{chromeVisibility.selectionOverlay && props.suggestionCard && !selectionToolbarPlacement ? (
|
|
761
|
+
<div
|
|
762
|
+
className="pointer-events-none absolute inset-x-0 top-0 z-20 flex justify-center px-4 pt-3"
|
|
763
|
+
data-testid="suggestion-card-fallback"
|
|
764
|
+
>
|
|
765
|
+
<div className="pointer-events-auto" data-placement="fallback">
|
|
766
|
+
<TwSuggestionCard
|
|
767
|
+
model={props.suggestionCard}
|
|
768
|
+
onFocusCapture={props.onSelectionToolbarFocusCapture}
|
|
769
|
+
onBlurCapture={props.onSelectionToolbarBlurCapture}
|
|
770
|
+
onAccept={props.onAcceptSuggestion}
|
|
771
|
+
onReject={props.onRejectSuggestion}
|
|
772
|
+
onEditSuggestion={props.onEditSuggestion}
|
|
773
|
+
onAddComment={props.onAddCommentFromSuggestion ?? props.onAddComment}
|
|
774
|
+
/>
|
|
775
|
+
</div>
|
|
776
|
+
</div>
|
|
777
|
+
) : null}
|
|
778
|
+
{chromeVisibility.selectionOverlay && props.selectionToolbar && !props.suggestionCard && selectionToolbarPlacement ? (
|
|
703
779
|
<div className="pointer-events-none absolute inset-0 z-20" data-testid="selection-toolbar-overlay">
|
|
704
780
|
<div
|
|
705
781
|
className="pointer-events-auto absolute"
|
|
@@ -722,7 +798,7 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
722
798
|
</div>
|
|
723
799
|
</div>
|
|
724
800
|
) : null}
|
|
725
|
-
{props.selectionToolbar && !selectionToolbarPlacement ? (
|
|
801
|
+
{chromeVisibility.selectionOverlay && props.selectionToolbar && !props.suggestionCard && !selectionToolbarPlacement ? (
|
|
726
802
|
<div
|
|
727
803
|
className="pointer-events-none absolute inset-x-0 top-0 z-20 flex justify-center px-4 pt-3"
|
|
728
804
|
data-testid="selection-toolbar-fallback"
|
|
@@ -748,7 +824,7 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
748
824
|
className={isPageWorkspace ? "relative" : undefined}
|
|
749
825
|
data-line-numbering={pageChromeModel.lineNumberingEnabled ? "enabled" : "disabled"}
|
|
750
826
|
>
|
|
751
|
-
{isPageWorkspace && pageChromeModel.lineNumberingEnabled ? (
|
|
827
|
+
{isPageWorkspace && chromeVisibility.pageChrome && pageChromeModel.lineNumberingEnabled ? (
|
|
752
828
|
<div
|
|
753
829
|
aria-hidden="true"
|
|
754
830
|
className="pointer-events-none absolute inset-y-0 left-0 z-10"
|
|
@@ -767,7 +843,7 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
767
843
|
</div>
|
|
768
844
|
) : null}
|
|
769
845
|
<div
|
|
770
|
-
className={isPageWorkspace && pageChromeModel.lineNumberingEnabled ? "pl-12" : undefined}
|
|
846
|
+
className={isPageWorkspace && chromeVisibility.pageChrome && pageChromeModel.lineNumberingEnabled ? "pl-12" : undefined}
|
|
771
847
|
style={isPageWorkspace ? pageShellMetrics.contentInsetStyle : undefined}
|
|
772
848
|
>
|
|
773
849
|
<div
|
|
@@ -781,7 +857,7 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
781
857
|
}
|
|
782
858
|
: pageChromeModel.documentGridStyle}
|
|
783
859
|
>
|
|
784
|
-
{isPageWorkspace ? (
|
|
860
|
+
{isPageWorkspace && chromeVisibility.pageChrome ? (
|
|
785
861
|
<div
|
|
786
862
|
data-testid="page-header-band"
|
|
787
863
|
className="relative z-10 flex items-center justify-between border-b border-dashed border-border/60 px-4 text-[11px] text-secondary"
|
|
@@ -800,7 +876,7 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
800
876
|
) : null}
|
|
801
877
|
</div>
|
|
802
878
|
) : null}
|
|
803
|
-
{isPageWorkspace && pageChromeModel.showPageBorder && !hidePageBorderForActiveEditing ? (
|
|
879
|
+
{isPageWorkspace && chromeVisibility.pageChrome && pageChromeModel.showPageBorder && !hidePageBorderForActiveEditing ? (
|
|
804
880
|
<div
|
|
805
881
|
aria-hidden="true"
|
|
806
882
|
className="pointer-events-none absolute inset-0 z-0 rounded-[2px]"
|
|
@@ -811,7 +887,7 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
811
887
|
<div className={isPageWorkspace ? "relative z-10" : undefined}>
|
|
812
888
|
{props.document}
|
|
813
889
|
</div>
|
|
814
|
-
{isPageWorkspace ? (
|
|
890
|
+
{isPageWorkspace && chromeVisibility.pageChrome ? (
|
|
815
891
|
<div
|
|
816
892
|
data-testid="page-footer-band"
|
|
817
893
|
className="relative z-10 flex items-center justify-between border-t border-dashed border-border/60 px-4 text-[11px] text-secondary"
|
|
@@ -836,14 +912,16 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
836
912
|
</div>
|
|
837
913
|
</div>
|
|
838
914
|
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
915
|
+
{chromeVisibility.statusBar ? (
|
|
916
|
+
<TwStatusBar
|
|
917
|
+
isDirty={snapshot.isDirty}
|
|
918
|
+
isExportBlocked={snapshot.compatibility.blockExport}
|
|
919
|
+
preserveOnlyCount={preserveOnlyCount}
|
|
920
|
+
commentCount={snapshot.comments.totalCount}
|
|
921
|
+
changeCount={snapshot.trackedChanges.totalCount}
|
|
922
|
+
sessionId={snapshot.sessionId}
|
|
923
|
+
/>
|
|
924
|
+
) : null}
|
|
847
925
|
</div>
|
|
848
926
|
|
|
849
927
|
{/* Review rail — hidden in editing mode unless toggled */}
|