@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
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
|
|
16
16
|
import React, { useState } from "react";
|
|
17
17
|
import * as Popover from "@radix-ui/react-popover";
|
|
18
|
+
import * as Toggle from "@radix-ui/react-toggle";
|
|
19
|
+
import * as Tooltip from "@radix-ui/react-tooltip";
|
|
18
20
|
import {
|
|
19
21
|
BookmarkCheck,
|
|
20
22
|
Check,
|
|
@@ -23,8 +25,13 @@ import {
|
|
|
23
25
|
ChevronLeft,
|
|
24
26
|
ChevronRight,
|
|
25
27
|
CircleOff,
|
|
28
|
+
Eye,
|
|
29
|
+
EyeOff,
|
|
30
|
+
FileDiff,
|
|
26
31
|
Flag,
|
|
27
32
|
Hand,
|
|
33
|
+
MessageSquare,
|
|
34
|
+
MessageSquareDot,
|
|
28
35
|
MessageSquareText,
|
|
29
36
|
Rows3,
|
|
30
37
|
SkipForward,
|
|
@@ -38,6 +45,7 @@ import type {
|
|
|
38
45
|
ReviewQueueSnapshot,
|
|
39
46
|
ScopeRailPosture,
|
|
40
47
|
} from "../../api/public-types";
|
|
48
|
+
import type { SessionCapabilities } from "../../runtime/session-capabilities";
|
|
41
49
|
import type { ScopedChromePolicy } from "../../ui/headless/scoped-chrome-policy";
|
|
42
50
|
import type { ToolbarChromeItemId } from "../../ui/headless/chrome-registry";
|
|
43
51
|
import { preserveEditorSelectionMouseDown } from "../../ui/headless/preserve-editor-selection";
|
|
@@ -68,7 +76,23 @@ export interface TwRoleActionRegionProps {
|
|
|
68
76
|
workflowItem?: WorkflowWorkItemSnapshot | null;
|
|
69
77
|
markupDisplay?: MarkupDisplayMode;
|
|
70
78
|
|
|
71
|
-
//
|
|
79
|
+
// Shared: editor + review role
|
|
80
|
+
canAddComment?: boolean;
|
|
81
|
+
showTrackedChanges?: boolean;
|
|
82
|
+
onAddComment?: () => void;
|
|
83
|
+
onShowTrackedChangesChange?: (show: boolean) => void;
|
|
84
|
+
/**
|
|
85
|
+
* Session capabilities used to gate the role-region tracked-changes
|
|
86
|
+
* toggle (mirrors the right-cluster gate at tw-toolbar.tsx). When
|
|
87
|
+
* `capabilities.trackChangesSupported` is false, the toggle is disabled.
|
|
88
|
+
*/
|
|
89
|
+
capabilities?: SessionCapabilities;
|
|
90
|
+
|
|
91
|
+
// Review sidebar panel (optional — hidden when not provided)
|
|
92
|
+
onReviewSidebarTrackedChanges?: () => void;
|
|
93
|
+
onReviewSidebarComments?: () => void;
|
|
94
|
+
|
|
95
|
+
// Workflow + review role: scope posture menu
|
|
72
96
|
onMarkScopePosture?: (posture: ScopeRailPosture) => void;
|
|
73
97
|
|
|
74
98
|
// Review role
|
|
@@ -142,7 +166,104 @@ interface RoleActionButtonProps {
|
|
|
142
166
|
|
|
143
167
|
function RoleActionButton(arg: RoleActionButtonProps): React.JSX.Element | null {
|
|
144
168
|
const { id, props } = arg;
|
|
169
|
+
const focusRingClass =
|
|
170
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-canvas";
|
|
145
171
|
switch (id) {
|
|
172
|
+
case "comment":
|
|
173
|
+
return (
|
|
174
|
+
<Tooltip.Root>
|
|
175
|
+
<Tooltip.Trigger asChild>
|
|
176
|
+
<button
|
|
177
|
+
type="button"
|
|
178
|
+
aria-label="Add comment"
|
|
179
|
+
disabled={!props.canAddComment || !props.onAddComment}
|
|
180
|
+
onMouseDown={preserveEditorSelectionMouseDown}
|
|
181
|
+
onClick={props.onAddComment}
|
|
182
|
+
className={`inline-flex h-6 w-6 items-center justify-center rounded-md text-secondary transition-colors hover:bg-surface disabled:cursor-not-allowed disabled:opacity-40 outline-none ${focusRingClass}`}
|
|
183
|
+
data-testid="role-add-comment"
|
|
184
|
+
>
|
|
185
|
+
<MessageSquare className="h-3.5 w-3.5" />
|
|
186
|
+
</button>
|
|
187
|
+
</Tooltip.Trigger>
|
|
188
|
+
<Tooltip.Portal>
|
|
189
|
+
<Tooltip.Content className="rounded-md bg-primary px-2 py-1 text-xs text-white shadow-md z-50" sideOffset={6}>
|
|
190
|
+
Add comment
|
|
191
|
+
</Tooltip.Content>
|
|
192
|
+
</Tooltip.Portal>
|
|
193
|
+
</Tooltip.Root>
|
|
194
|
+
);
|
|
195
|
+
case "tracked-changes-toggle":
|
|
196
|
+
return (
|
|
197
|
+
<Tooltip.Root>
|
|
198
|
+
<Tooltip.Trigger asChild>
|
|
199
|
+
<Toggle.Root
|
|
200
|
+
pressed={props.showTrackedChanges ?? false}
|
|
201
|
+
onPressedChange={(v) => props.onShowTrackedChangesChange?.(v)}
|
|
202
|
+
disabled={props.capabilities ? !props.capabilities.trackChangesSupported : false}
|
|
203
|
+
onMouseDown={preserveEditorSelectionMouseDown}
|
|
204
|
+
className={`inline-flex h-6 w-6 items-center justify-center rounded-md text-secondary transition-colors hover:bg-surface data-[state=on]:bg-canvas data-[state=on]:text-accent data-[state=on]:ring-1 data-[state=on]:ring-accent/30 data-[state=on]:shadow-sm outline-none disabled:opacity-40 ${focusRingClass}`}
|
|
205
|
+
data-testid="role-tracked-changes-toggle"
|
|
206
|
+
>
|
|
207
|
+
{(props.showTrackedChanges ?? false) ? (
|
|
208
|
+
<Eye className="h-3.5 w-3.5" />
|
|
209
|
+
) : (
|
|
210
|
+
<EyeOff className="h-3.5 w-3.5" />
|
|
211
|
+
)}
|
|
212
|
+
</Toggle.Root>
|
|
213
|
+
</Tooltip.Trigger>
|
|
214
|
+
<Tooltip.Portal>
|
|
215
|
+
<Tooltip.Content className="rounded-md bg-primary px-2 py-1 text-xs text-white shadow-md z-50" sideOffset={6}>
|
|
216
|
+
{(props.showTrackedChanges ?? false) ? "Hide tracked changes" : "Show tracked changes"}
|
|
217
|
+
</Tooltip.Content>
|
|
218
|
+
</Tooltip.Portal>
|
|
219
|
+
</Tooltip.Root>
|
|
220
|
+
);
|
|
221
|
+
case "review-sidebar-tracked-changes":
|
|
222
|
+
return (
|
|
223
|
+
<Tooltip.Root>
|
|
224
|
+
<Tooltip.Trigger asChild>
|
|
225
|
+
<button
|
|
226
|
+
type="button"
|
|
227
|
+
aria-label="Show tracked changes in sidebar"
|
|
228
|
+
disabled={!props.onReviewSidebarTrackedChanges}
|
|
229
|
+
onMouseDown={preserveEditorSelectionMouseDown}
|
|
230
|
+
onClick={props.onReviewSidebarTrackedChanges}
|
|
231
|
+
className={`inline-flex h-6 w-6 items-center justify-center rounded-md text-secondary transition-colors hover:bg-surface disabled:cursor-not-allowed disabled:opacity-40 outline-none ${focusRingClass}`}
|
|
232
|
+
data-testid="role-sidebar-tracked-changes"
|
|
233
|
+
>
|
|
234
|
+
<FileDiff className="h-3.5 w-3.5" />
|
|
235
|
+
</button>
|
|
236
|
+
</Tooltip.Trigger>
|
|
237
|
+
<Tooltip.Portal>
|
|
238
|
+
<Tooltip.Content className="rounded-md bg-primary px-2 py-1 text-xs text-white shadow-md z-50" sideOffset={6}>
|
|
239
|
+
Tracked changes panel
|
|
240
|
+
</Tooltip.Content>
|
|
241
|
+
</Tooltip.Portal>
|
|
242
|
+
</Tooltip.Root>
|
|
243
|
+
);
|
|
244
|
+
case "review-sidebar-comments":
|
|
245
|
+
return (
|
|
246
|
+
<Tooltip.Root>
|
|
247
|
+
<Tooltip.Trigger asChild>
|
|
248
|
+
<button
|
|
249
|
+
type="button"
|
|
250
|
+
aria-label="Show comments in sidebar"
|
|
251
|
+
disabled={!props.onReviewSidebarComments}
|
|
252
|
+
onMouseDown={preserveEditorSelectionMouseDown}
|
|
253
|
+
onClick={props.onReviewSidebarComments}
|
|
254
|
+
className={`inline-flex h-6 w-6 items-center justify-center rounded-md text-secondary transition-colors hover:bg-surface disabled:cursor-not-allowed disabled:opacity-40 outline-none ${focusRingClass}`}
|
|
255
|
+
data-testid="role-sidebar-comments"
|
|
256
|
+
>
|
|
257
|
+
<MessageSquareDot className="h-3.5 w-3.5" />
|
|
258
|
+
</button>
|
|
259
|
+
</Tooltip.Trigger>
|
|
260
|
+
<Tooltip.Portal>
|
|
261
|
+
<Tooltip.Content className="rounded-md bg-primary px-2 py-1 text-xs text-white shadow-md z-50" sideOffset={6}>
|
|
262
|
+
Comments panel
|
|
263
|
+
</Tooltip.Content>
|
|
264
|
+
</Tooltip.Portal>
|
|
265
|
+
</Tooltip.Root>
|
|
266
|
+
);
|
|
146
267
|
case "editor-scope-posture-menu":
|
|
147
268
|
return (
|
|
148
269
|
<TwScopePostureMenu
|
|
@@ -42,6 +42,8 @@ import {
|
|
|
42
42
|
|
|
43
43
|
import type {
|
|
44
44
|
ActiveListContext,
|
|
45
|
+
ChromePinSurface,
|
|
46
|
+
ChromePinsState,
|
|
45
47
|
CompatibilityPanelSnapshot,
|
|
46
48
|
EditorRole,
|
|
47
49
|
EditorStoryTarget,
|
|
@@ -49,6 +51,7 @@ import type {
|
|
|
49
51
|
FormattingStateSnapshot,
|
|
50
52
|
FormattingAlignment,
|
|
51
53
|
InsertImageOptions,
|
|
54
|
+
PinState,
|
|
52
55
|
ReviewQueueSnapshot,
|
|
53
56
|
ScopeRailPosture,
|
|
54
57
|
SectionBreakType,
|
|
@@ -61,6 +64,7 @@ import type {
|
|
|
61
64
|
import type { SessionCapabilities } from "../../runtime/session-capabilities";
|
|
62
65
|
import {
|
|
63
66
|
getToolbarChromePlacement,
|
|
67
|
+
isChromeItemOwnedByRoleRegion,
|
|
64
68
|
isToolbarChromeItemVisible,
|
|
65
69
|
resolveScopedChromePolicy,
|
|
66
70
|
type ScopedChromePolicy,
|
|
@@ -72,6 +76,7 @@ import {
|
|
|
72
76
|
type MarkupDisplayMode,
|
|
73
77
|
type WorkflowWorkItemSnapshot,
|
|
74
78
|
} from "./tw-role-action-region";
|
|
79
|
+
import { TwDetachHandle } from "../chrome/tw-detach-handle";
|
|
75
80
|
import { TwToolbarIconButton } from "./tw-toolbar-icon-button";
|
|
76
81
|
|
|
77
82
|
export interface TwToolbarProps {
|
|
@@ -144,7 +149,10 @@ export interface TwToolbarProps {
|
|
|
144
149
|
/** Markup display mode for the review role. */
|
|
145
150
|
markupDisplay?: MarkupDisplayMode;
|
|
146
151
|
|
|
147
|
-
//
|
|
152
|
+
// Shared: editor + review role (comment + TC in role region)
|
|
153
|
+
onReviewSidebarTrackedChanges?: () => void;
|
|
154
|
+
onReviewSidebarComments?: () => void;
|
|
155
|
+
// Workflow + review role: scope posture
|
|
148
156
|
onMarkScopePosture?: (posture: ScopeRailPosture) => void;
|
|
149
157
|
// Review role
|
|
150
158
|
onReviewPrev?: () => void;
|
|
@@ -162,6 +170,10 @@ export interface TwToolbarProps {
|
|
|
162
170
|
onWorkflowSkip?: () => void;
|
|
163
171
|
onWorkflowMarkBlocked?: () => void;
|
|
164
172
|
onWorkflowJumpToScope?: () => void;
|
|
173
|
+
/** Current chrome pin state; when supplied enables the topnav detach handle. */
|
|
174
|
+
chromePins?: ChromePinsState;
|
|
175
|
+
/** Called when the user detaches or re-attaches the topnav. */
|
|
176
|
+
onChromePinChange?: (surface: ChromePinSurface, pin: PinState | null) => void;
|
|
165
177
|
}
|
|
166
178
|
|
|
167
179
|
export interface ToolbarInteractionPolicy {
|
|
@@ -219,7 +231,12 @@ export function TwToolbar(props: TwToolbarProps) {
|
|
|
219
231
|
const showTextColors = isToolbarChromeItemVisible(scopedChromePolicy, "text-colors");
|
|
220
232
|
const showParagraphAlignment = isToolbarChromeItemVisible(scopedChromePolicy, "paragraph-alignment");
|
|
221
233
|
const showInsertMenu = isToolbarChromeItemVisible(scopedChromePolicy, "insert-actions");
|
|
222
|
-
const showTrackedChangesToggle =
|
|
234
|
+
const showTrackedChangesToggle =
|
|
235
|
+
isToolbarChromeItemVisible(scopedChromePolicy, "tracked-changes-toggle") &&
|
|
236
|
+
!isChromeItemOwnedByRoleRegion("tracked-changes-toggle", props.role);
|
|
237
|
+
const showRightClusterComment =
|
|
238
|
+
isToolbarChromeItemVisible(scopedChromePolicy, "comment") &&
|
|
239
|
+
!isChromeItemOwnedByRoleRegion("comment", props.role);
|
|
223
240
|
const showHealth =
|
|
224
241
|
showDiagnosticsChrome &&
|
|
225
242
|
isToolbarChromeItemVisible(scopedChromePolicy, "health") &&
|
|
@@ -527,6 +544,13 @@ export function TwToolbar(props: TwToolbarProps) {
|
|
|
527
544
|
reviewQueue={props.reviewQueue}
|
|
528
545
|
workflowItem={props.workflowItem}
|
|
529
546
|
markupDisplay={props.markupDisplay}
|
|
547
|
+
canAddComment={canAddComment}
|
|
548
|
+
showTrackedChanges={props.showTrackedChanges}
|
|
549
|
+
capabilities={caps}
|
|
550
|
+
onAddComment={props.onAddComment}
|
|
551
|
+
onShowTrackedChangesChange={props.onShowTrackedChangesChange}
|
|
552
|
+
onReviewSidebarTrackedChanges={props.onReviewSidebarTrackedChanges}
|
|
553
|
+
onReviewSidebarComments={props.onReviewSidebarComments}
|
|
530
554
|
onMarkScopePosture={props.onMarkScopePosture}
|
|
531
555
|
onReviewPrev={props.onReviewPrev}
|
|
532
556
|
onReviewNext={props.onReviewNext}
|
|
@@ -567,7 +591,7 @@ export function TwToolbar(props: TwToolbarProps) {
|
|
|
567
591
|
</>
|
|
568
592
|
) : null}
|
|
569
593
|
|
|
570
|
-
{
|
|
594
|
+
{showRightClusterComment ? (
|
|
571
595
|
<TwToolbarIconButton
|
|
572
596
|
icon={MessageSquare}
|
|
573
597
|
label="Add comment"
|
|
@@ -809,6 +833,15 @@ export function TwToolbar(props: TwToolbarProps) {
|
|
|
809
833
|
onClick={props.onExport}
|
|
810
834
|
/>
|
|
811
835
|
) : null}
|
|
836
|
+
|
|
837
|
+
{props.onChromePinChange ? (
|
|
838
|
+
<TwDetachHandle
|
|
839
|
+
surface="topnav"
|
|
840
|
+
pin={props.chromePins?.topnav}
|
|
841
|
+
onChange={props.onChromePinChange}
|
|
842
|
+
label="Detach toolbar"
|
|
843
|
+
/>
|
|
844
|
+
) : null}
|
|
812
845
|
</div>
|
|
813
846
|
</header>
|
|
814
847
|
);
|
|
@@ -39,6 +39,7 @@ import type {
|
|
|
39
39
|
ZoomLevel,
|
|
40
40
|
} from "../api/public-types";
|
|
41
41
|
import { findPageForOffset } from "../runtime/document-navigation.ts";
|
|
42
|
+
import { createCanvasBackend } from "../runtime/layout/index.ts";
|
|
42
43
|
import {
|
|
43
44
|
DEFAULT_PAGE_ESTIMATE_PX_PER_TWIP,
|
|
44
45
|
estimateBlockHeight,
|
|
@@ -50,6 +51,8 @@ import {
|
|
|
50
51
|
incrementInvalidationCounter,
|
|
51
52
|
recordPerfSample,
|
|
52
53
|
} from "./editor-surface/perf-probe.ts";
|
|
54
|
+
import { sliceBlocksForPage } from "./editor-surface/page-slice-util.ts";
|
|
55
|
+
import { TwPageBlockView } from "./editor-surface/tw-page-block-view.tsx";
|
|
53
56
|
import { computeLineMarkersIfEnabled } from "./page-chrome-model.ts";
|
|
54
57
|
import type { SessionCapabilities } from "../runtime/session-capabilities";
|
|
55
58
|
import type {
|
|
@@ -77,8 +80,9 @@ import {
|
|
|
77
80
|
resolveChromePresetOptions,
|
|
78
81
|
resolveChromeVisibilityForPreset,
|
|
79
82
|
} from "./chrome/chrome-preset-model";
|
|
80
|
-
import { TwReviewQueueBar } from "./chrome/review-queue-bar";
|
|
81
83
|
import { TwSelectionToolHost } from "./chrome/tw-selection-tool-host";
|
|
84
|
+
import { resolveSelectionAnchor } from "./chrome/tw-selection-anchor-resolver";
|
|
85
|
+
import { resolveSelectionToolPlacement } from "./chrome/tw-selection-tool-placement";
|
|
82
86
|
import { TwReviewRail, type ReviewRailTab } from "./review/tw-review-rail";
|
|
83
87
|
import { TwStatusBar } from "./status/tw-status-bar";
|
|
84
88
|
import { type ToolbarInteractionPolicy } from "./toolbar/tw-toolbar";
|
|
@@ -262,6 +266,14 @@ export interface TwReviewWorkspaceProps {
|
|
|
262
266
|
onCloseStory?: () => void;
|
|
263
267
|
onOpenHeaderStory?: () => void;
|
|
264
268
|
onOpenFooterStory?: () => void;
|
|
269
|
+
/**
|
|
270
|
+
* Open a header/footer story for a specific page. Called when the user
|
|
271
|
+
* double-clicks a per-page header/footer band in the page-stack chrome.
|
|
272
|
+
* Must resolve the correct variant for that page's section and call
|
|
273
|
+
* `runtime.openStory()`.
|
|
274
|
+
*/
|
|
275
|
+
onOpenHeaderStoryForPage?: (pageIndex: number) => void;
|
|
276
|
+
onOpenFooterStoryForPage?: (pageIndex: number) => void;
|
|
265
277
|
onSetParagraphIndentation?: (indentation: {
|
|
266
278
|
left?: number;
|
|
267
279
|
right?: number;
|
|
@@ -271,13 +283,37 @@ export interface TwReviewWorkspaceProps {
|
|
|
271
283
|
onSetParagraphTabStops?: (tabStops: Array<{ pos: number; val?: string; leader?: string }>) => void;
|
|
272
284
|
onRestartNumbering?: () => void;
|
|
273
285
|
onContinueNumbering?: () => void;
|
|
286
|
+
// P6: new table ops
|
|
287
|
+
onToggleRowHeader?: () => void;
|
|
288
|
+
onToggleRowCantSplit?: () => void;
|
|
289
|
+
onDistributeColumnsEvenly?: () => void;
|
|
290
|
+
onSetTableAlignment?: (alignment: "left" | "center" | "right") => void;
|
|
291
|
+
onSetCellVerticalAlign?: (align: "top" | "center" | "bottom") => void;
|
|
292
|
+
/** P6: active table context for chrome overlay grips. */
|
|
293
|
+
tableContext?: import("../api/public-types").TableStructureContextSnapshot | null;
|
|
294
|
+
/** P6: column resize committed from overlay grip → set-column-width op. */
|
|
295
|
+
onSetColumnWidth?: (columnIndex: number, twips: number) => void;
|
|
296
|
+
/** P6: row resize committed from overlay grip → set-row-height op. */
|
|
297
|
+
onSetRowHeight?: (rowIndex: number, twips: number, rule: "auto" | "atLeast" | "exact") => void;
|
|
298
|
+
onListIndent?: () => void;
|
|
299
|
+
onListOutdent?: () => void;
|
|
274
300
|
onUpdateFields?: () => void;
|
|
275
301
|
onUpdateTableOfContents?: () => void;
|
|
276
302
|
onGoToPreviousReviewItem?: () => void;
|
|
277
303
|
onGoToNextReviewItem?: () => void;
|
|
278
304
|
onMarkSectionForReview?: () => void;
|
|
305
|
+
/** Optional: open sidebar to tracked-changes panel. When provided, the review role shows a sidebar-TC icon. */
|
|
306
|
+
onReviewSidebarTrackedChanges?: () => void;
|
|
307
|
+
/** Optional: open sidebar to comments panel. When provided, the review role shows a sidebar-comments icon. */
|
|
308
|
+
onReviewSidebarComments?: () => void;
|
|
279
309
|
onNavigateHeading?: (headingId: string) => void;
|
|
280
310
|
chromeVisibility?: Partial<ReviewWorkspaceChromeVisibility>;
|
|
311
|
+
/**
|
|
312
|
+
* Called when the shell-header mode tab changes or any chrome surface fires
|
|
313
|
+
* a role switch. Wire to `runtime.setEditorRole(role)` so the workspace
|
|
314
|
+
* re-renders with the new per-role action set.
|
|
315
|
+
*/
|
|
316
|
+
onEditorRoleChange?: (role: import("../api/public-types.ts").EditorRole) => void;
|
|
281
317
|
}
|
|
282
318
|
|
|
283
319
|
export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
@@ -319,6 +355,9 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
319
355
|
reviewRailAvailable,
|
|
320
356
|
}),
|
|
321
357
|
);
|
|
358
|
+
// Incremented on zoom_changed / render_frame_ready so the placement
|
|
359
|
+
// useMemo below re-executes when the render kernel emits new rects.
|
|
360
|
+
const [renderFrameRevision, setRenderFrameRevision] = useState(0);
|
|
322
361
|
const responsiveChromeSignatureRef = useRef<string | null>(null);
|
|
323
362
|
const headings = props.documentNavigation?.headings ?? [];
|
|
324
363
|
const headerVariant = snapshot.pageLayout?.headerVariants[0]?.variant ?? "default";
|
|
@@ -348,11 +387,6 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
348
387
|
),
|
|
349
388
|
[props.documentNavigation, snapshot.activeStory, snapshot.pageLayout, snapshot.surface],
|
|
350
389
|
);
|
|
351
|
-
const selectionToolbarPlacement = resolveSelectionToolbarPlacement(
|
|
352
|
-
props.selectionToolAnchor,
|
|
353
|
-
selectionToolbarRootRef.current,
|
|
354
|
-
zoomScale,
|
|
355
|
-
);
|
|
356
390
|
const effectiveSelectionMode = props.interactionGuardSnapshot?.effectiveMode ?? "edit";
|
|
357
391
|
const allowLocalChromeMutations = Boolean(caps?.canEdit) && effectiveSelectionMode === "edit";
|
|
358
392
|
const gatedSelectionTool = useMemo(() => {
|
|
@@ -364,6 +398,40 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
364
398
|
}
|
|
365
399
|
return props.activeSelectionTool;
|
|
366
400
|
}, [props.activeSelectionTool, chromeVisibility.contextToolbars]);
|
|
401
|
+
const selectionToolbarPlacement = useMemo(() => {
|
|
402
|
+
// Prefer render-frame anchors when the layout facet is available — this
|
|
403
|
+
// keeps the tool glued to kernel coordinates across zoom, scroll, and
|
|
404
|
+
// predicted-text reconciliation (R4).
|
|
405
|
+
if (props.layoutFacet && gatedSelectionTool) {
|
|
406
|
+
const anchorRect = resolveSelectionAnchor({
|
|
407
|
+
facet: props.layoutFacet,
|
|
408
|
+
selection: viewState.selection,
|
|
409
|
+
tool: gatedSelectionTool,
|
|
410
|
+
});
|
|
411
|
+
if (anchorRect && selectionToolbarRootRef.current) {
|
|
412
|
+
const containerRect = selectionToolbarRootRef.current.getBoundingClientRect();
|
|
413
|
+
const result = resolveSelectionToolPlacement({
|
|
414
|
+
anchor: anchorRect,
|
|
415
|
+
container: { widthPx: containerRect.width, heightPx: containerRect.height },
|
|
416
|
+
});
|
|
417
|
+
if (result) return result;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
// Fall back to DOM rects for hosts that do not supply a layout facet.
|
|
421
|
+
return resolveSelectionToolbarPlacement(
|
|
422
|
+
props.selectionToolAnchor,
|
|
423
|
+
selectionToolbarRootRef.current,
|
|
424
|
+
zoomScale,
|
|
425
|
+
);
|
|
426
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
427
|
+
}, [
|
|
428
|
+
props.layoutFacet,
|
|
429
|
+
props.selectionToolAnchor,
|
|
430
|
+
gatedSelectionTool,
|
|
431
|
+
viewState.selection,
|
|
432
|
+
zoomScale,
|
|
433
|
+
renderFrameRevision,
|
|
434
|
+
]);
|
|
367
435
|
const activePage = props.documentNavigation?.pages[props.documentNavigation.activePageIndex] ?? null;
|
|
368
436
|
const pageShellMetrics = useMemo(
|
|
369
437
|
() => buildPageShellMetrics(snapshot.pageLayout),
|
|
@@ -388,6 +456,9 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
388
456
|
}),
|
|
389
457
|
[reviewRailAvailable, reviewRailOpen, viewportWidth],
|
|
390
458
|
);
|
|
459
|
+
const hasSidebarPanelAccess = Boolean(
|
|
460
|
+
props.onReviewSidebarTrackedChanges || props.onReviewSidebarComments,
|
|
461
|
+
);
|
|
391
462
|
const scopedChromePolicy = useMemo(
|
|
392
463
|
() =>
|
|
393
464
|
resolveScopedChromePolicy({
|
|
@@ -397,14 +468,18 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
397
468
|
interactionGuardSnapshot: props.interactionGuardSnapshot,
|
|
398
469
|
workflowScopeSnapshot: props.workflowScopeSnapshot,
|
|
399
470
|
activeListContext: props.activeListContext,
|
|
471
|
+
role: viewState.editorRole,
|
|
472
|
+
hasSidebarPanelAccess,
|
|
400
473
|
}),
|
|
401
474
|
[
|
|
402
475
|
caps,
|
|
403
476
|
chromePreset,
|
|
477
|
+
hasSidebarPanelAccess,
|
|
404
478
|
props.activeListContext,
|
|
405
479
|
props.interactionGuardSnapshot,
|
|
406
480
|
props.workflowScopeSnapshot,
|
|
407
481
|
responsiveChrome.isNarrow,
|
|
482
|
+
viewState.editorRole,
|
|
408
483
|
],
|
|
409
484
|
);
|
|
410
485
|
const toolbarInteractionPolicy: ToolbarInteractionPolicy | undefined = caps
|
|
@@ -446,6 +521,18 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
446
521
|
};
|
|
447
522
|
}, []);
|
|
448
523
|
|
|
524
|
+
// Subscribe to layout facet events so chrome re-projects on zoom changes
|
|
525
|
+
// and after incremental relayout (R4).
|
|
526
|
+
useEffect(() => {
|
|
527
|
+
if (!props.layoutFacet) return;
|
|
528
|
+
const unsub = props.layoutFacet.subscribe((event) => {
|
|
529
|
+
if (event.kind === "zoom_changed" || event.kind === "render_frame_ready") {
|
|
530
|
+
setRenderFrameRevision((n) => n + 1);
|
|
531
|
+
}
|
|
532
|
+
});
|
|
533
|
+
return unsub;
|
|
534
|
+
}, [props.layoutFacet]);
|
|
535
|
+
|
|
449
536
|
useEffect(() => {
|
|
450
537
|
const responsiveSignature = `${reviewRailAvailable ? "1" : "0"}:${isNarrowChromeViewport(viewportWidth) ? "n" : "d"}`;
|
|
451
538
|
if (responsiveChromeSignatureRef.current === responsiveSignature) {
|
|
@@ -492,6 +579,14 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
492
579
|
};
|
|
493
580
|
}, [responsiveChrome.showDrawerReviewRail]);
|
|
494
581
|
|
|
582
|
+
useEffect(() => {
|
|
583
|
+
if (!props.layoutFacet) return;
|
|
584
|
+
const facet = props.layoutFacet;
|
|
585
|
+
void document.fonts.ready.then(() => {
|
|
586
|
+
facet.swapMeasurementProvider(createCanvasBackend());
|
|
587
|
+
});
|
|
588
|
+
}, [props.layoutFacet]);
|
|
589
|
+
|
|
495
590
|
return (
|
|
496
591
|
<Tooltip.Provider delayDuration={400}>
|
|
497
592
|
<div className="flex h-full flex-col bg-canvas text-primary">
|
|
@@ -609,6 +704,12 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
609
704
|
role={viewState.editorRole}
|
|
610
705
|
reviewQueue={props.reviewQueue}
|
|
611
706
|
markupDisplay={markupDisplay}
|
|
707
|
+
onReviewSidebarTrackedChanges={props.onReviewSidebarTrackedChanges
|
|
708
|
+
? runWithSelectionToolbarDismiss(props.onReviewSidebarTrackedChanges)
|
|
709
|
+
: undefined}
|
|
710
|
+
onReviewSidebarComments={props.onReviewSidebarComments
|
|
711
|
+
? runWithSelectionToolbarDismiss(props.onReviewSidebarComments)
|
|
712
|
+
: undefined}
|
|
612
713
|
onMarkScopePosture={props.onMarkSectionForReview
|
|
613
714
|
? runWithSelectionToolbarDismiss(props.onMarkSectionForReview)
|
|
614
715
|
: undefined}
|
|
@@ -642,27 +743,14 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
642
743
|
props.onRejectRevision?.(revisionId);
|
|
643
744
|
};
|
|
644
745
|
})()}
|
|
746
|
+
chromePins={viewState.chromePins}
|
|
747
|
+
onChromePinChange={props.onChromePinChange}
|
|
645
748
|
/>
|
|
646
749
|
</div>
|
|
647
750
|
) : null}
|
|
648
751
|
|
|
649
|
-
{
|
|
650
|
-
|
|
651
|
-
chromeOptions.showReviewQueueBar &&
|
|
652
|
-
props.reviewQueue ? (
|
|
653
|
-
<TwReviewQueueBar
|
|
654
|
-
queue={props.reviewQueue}
|
|
655
|
-
onPrevious={props.onGoToPreviousReviewItem
|
|
656
|
-
? runWithSelectionToolbarDismiss(props.onGoToPreviousReviewItem)
|
|
657
|
-
: undefined}
|
|
658
|
-
onNext={props.onGoToNextReviewItem
|
|
659
|
-
? runWithSelectionToolbarDismiss(props.onGoToNextReviewItem)
|
|
660
|
-
: undefined}
|
|
661
|
-
onMarkSection={chromeOptions.showSectionTagAction && props.onMarkSectionForReview
|
|
662
|
-
? runWithSelectionToolbarDismiss(props.onMarkSectionForReview)
|
|
663
|
-
: undefined}
|
|
664
|
-
/>
|
|
665
|
-
) : null}
|
|
752
|
+
{/* Legacy TwReviewQueueBar is suppressed — review role's action region
|
|
753
|
+
now owns queue prev/next + counts inline in the top toolbar. */}
|
|
666
754
|
|
|
667
755
|
{chromeVisibility.alerts ? <TwAlertBanner
|
|
668
756
|
snapshot={snapshot}
|
|
@@ -967,6 +1055,11 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
967
1055
|
onSetImageFrame={props.onSetImageFrame}
|
|
968
1056
|
onRestartNumbering={props.onRestartNumbering}
|
|
969
1057
|
onContinueNumbering={props.onContinueNumbering}
|
|
1058
|
+
onToggleRowHeader={props.onToggleRowHeader}
|
|
1059
|
+
onToggleRowCantSplit={props.onToggleRowCantSplit}
|
|
1060
|
+
onDistributeColumnsEvenly={props.onDistributeColumnsEvenly}
|
|
1061
|
+
onSetTableAlignment={props.onSetTableAlignment}
|
|
1062
|
+
onSetCellVerticalAlign={props.onSetCellVerticalAlign}
|
|
970
1063
|
chromePins={viewState.chromePins}
|
|
971
1064
|
onChromePinChange={props.onChromePinChange}
|
|
972
1065
|
/>
|
|
@@ -1035,15 +1128,22 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
1035
1128
|
style={pageChromeModel.pageBorderStyle}
|
|
1036
1129
|
/>
|
|
1037
1130
|
) : null}
|
|
1038
|
-
<div className={isPageWorkspace ? "relative z-10" :
|
|
1131
|
+
<div className={isPageWorkspace ? "relative z-10" : "relative"}>
|
|
1132
|
+
{/* Page chrome (frame borders, header/footer bands,
|
|
1133
|
+
page-number labels, inter-page separators) is
|
|
1134
|
+
rendered as in-flow widget decorations inside
|
|
1135
|
+
the PM surface itself — see
|
|
1136
|
+
`pm-page-break-decorations.ts`. That keeps the
|
|
1137
|
+
chrome perfectly aligned with PM content without
|
|
1138
|
+
any absolute-positioned overlay that would drift
|
|
1139
|
+
relative to the browser's line layout. */}
|
|
1039
1140
|
{props.document}
|
|
1040
1141
|
{props.layoutFacet ? (
|
|
1041
1142
|
<TwChromeOverlay
|
|
1042
1143
|
facet={props.layoutFacet}
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
}
|
|
1046
|
-
showWorkspaceDock={chromeVisibility.pageChrome}
|
|
1144
|
+
tableContext={props.tableContext}
|
|
1145
|
+
onSetColumnWidth={props.onSetColumnWidth}
|
|
1146
|
+
onSetRowHeight={props.onSetRowHeight}
|
|
1047
1147
|
/>
|
|
1048
1148
|
) : null}
|
|
1049
1149
|
</div>
|
|
@@ -1070,32 +1170,10 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
1070
1170
|
</div>
|
|
1071
1171
|
</div>
|
|
1072
1172
|
</div>
|
|
1073
|
-
{
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
key={`page-${page.pageIndex}`}
|
|
1078
|
-
data-wre-page-frame="true"
|
|
1079
|
-
data-page-index={page.pageIndex}
|
|
1080
|
-
className="wre-page-chrome wre-page-surface relative mx-auto w-full max-w-[840px] overflow-hidden rounded-[2px] bg-canvas"
|
|
1081
|
-
style={{
|
|
1082
|
-
minHeight: "600px",
|
|
1083
|
-
boxShadow: "0 1px 2px rgba(0,0,0,0.08), 0 2px 8px rgba(0,0,0,0.04)",
|
|
1084
|
-
border: "1px solid var(--color-border, rgba(0,0,0,0.08))",
|
|
1085
|
-
}}
|
|
1086
|
-
>
|
|
1087
|
-
<div className="absolute left-4 top-3 text-[11px] uppercase tracking-[0.12em] text-tertiary">
|
|
1088
|
-
Page {page.pageIndex + 1} of {props.documentNavigation!.pageCount}
|
|
1089
|
-
</div>
|
|
1090
|
-
<div className="absolute inset-0 flex items-center justify-center text-sm text-secondary">
|
|
1091
|
-
Continuation of document flow.
|
|
1092
|
-
<br />
|
|
1093
|
-
(Editing occurs in the page above.)
|
|
1094
|
-
</div>
|
|
1095
|
-
</div>
|
|
1096
|
-
))}
|
|
1097
|
-
</div>
|
|
1098
|
-
) : null}
|
|
1173
|
+
{/* Pages 2..N in page mode are now rendered by TwPageStackChrome
|
|
1174
|
+
as absolute overlays above the single flowing PM surface.
|
|
1175
|
+
The PM surface holds all editable content; page frames draw
|
|
1176
|
+
borders, header/footer bands, and per-page numbers on top. */}
|
|
1099
1177
|
</div>
|
|
1100
1178
|
|
|
1101
1179
|
{chromeVisibility.statusBar ? (
|