@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
|
@@ -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 {
|
|
@@ -64,6 +67,7 @@ import {
|
|
|
64
67
|
import type { EditorCommandBag } from "../ui/editor-command-bag.ts";
|
|
65
68
|
import { preserveEditorSelectionMouseDown } from "../ui/headless/preserve-editor-selection";
|
|
66
69
|
import { TwAlertBanner } from "./chrome/tw-alert-banner";
|
|
70
|
+
import { TwModeDock } from "./chrome/tw-mode-dock";
|
|
67
71
|
import { TwLayoutPanel } from "./chrome/tw-layout-panel";
|
|
68
72
|
import { TwPageRuler } from "./chrome/tw-page-ruler";
|
|
69
73
|
import {
|
|
@@ -77,8 +81,9 @@ import {
|
|
|
77
81
|
resolveChromePresetOptions,
|
|
78
82
|
resolveChromeVisibilityForPreset,
|
|
79
83
|
} from "./chrome/chrome-preset-model";
|
|
80
|
-
import { TwReviewQueueBar } from "./chrome/review-queue-bar";
|
|
81
84
|
import { TwSelectionToolHost } from "./chrome/tw-selection-tool-host";
|
|
85
|
+
import { resolveSelectionAnchor } from "./chrome/tw-selection-anchor-resolver";
|
|
86
|
+
import { resolveSelectionToolPlacement } from "./chrome/tw-selection-tool-placement";
|
|
82
87
|
import { TwReviewRail, type ReviewRailTab } from "./review/tw-review-rail";
|
|
83
88
|
import { TwStatusBar } from "./status/tw-status-bar";
|
|
84
89
|
import { type ToolbarInteractionPolicy } from "./toolbar/tw-toolbar";
|
|
@@ -125,6 +130,15 @@ export interface TwReviewWorkspaceProps {
|
|
|
125
130
|
searchLabel?: string;
|
|
126
131
|
helpLabel?: string;
|
|
127
132
|
};
|
|
133
|
+
/**
|
|
134
|
+
* Opt-in floating mode dock pinned to the bottom of the workspace.
|
|
135
|
+
* Hosts pass the derived label / icon / actions; defaults to hidden.
|
|
136
|
+
*/
|
|
137
|
+
modeDock?: {
|
|
138
|
+
label: string;
|
|
139
|
+
icon?: ReactNode;
|
|
140
|
+
actions?: readonly import("./chrome/tw-mode-dock").TwModeDockAction[];
|
|
141
|
+
};
|
|
128
142
|
document: ReactNode;
|
|
129
143
|
workspaceMode: WorkspaceMode;
|
|
130
144
|
zoomLevel?: ZoomLevel;
|
|
@@ -262,6 +276,14 @@ export interface TwReviewWorkspaceProps {
|
|
|
262
276
|
onCloseStory?: () => void;
|
|
263
277
|
onOpenHeaderStory?: () => void;
|
|
264
278
|
onOpenFooterStory?: () => void;
|
|
279
|
+
/**
|
|
280
|
+
* Open a header/footer story for a specific page. Called when the user
|
|
281
|
+
* double-clicks a per-page header/footer band in the page-stack chrome.
|
|
282
|
+
* Must resolve the correct variant for that page's section and call
|
|
283
|
+
* `runtime.openStory()`.
|
|
284
|
+
*/
|
|
285
|
+
onOpenHeaderStoryForPage?: (pageIndex: number) => void;
|
|
286
|
+
onOpenFooterStoryForPage?: (pageIndex: number) => void;
|
|
265
287
|
onSetParagraphIndentation?: (indentation: {
|
|
266
288
|
left?: number;
|
|
267
289
|
right?: number;
|
|
@@ -271,13 +293,56 @@ export interface TwReviewWorkspaceProps {
|
|
|
271
293
|
onSetParagraphTabStops?: (tabStops: Array<{ pos: number; val?: string; leader?: string }>) => void;
|
|
272
294
|
onRestartNumbering?: () => void;
|
|
273
295
|
onContinueNumbering?: () => void;
|
|
296
|
+
// P6: new table ops
|
|
297
|
+
onToggleRowHeader?: () => void;
|
|
298
|
+
onToggleRowCantSplit?: () => void;
|
|
299
|
+
onDistributeColumnsEvenly?: () => void;
|
|
300
|
+
onSetTableAlignment?: (alignment: "left" | "center" | "right") => void;
|
|
301
|
+
onSetCellVerticalAlign?: (align: "top" | "center" | "bottom") => void;
|
|
302
|
+
/** P6: active table context for chrome overlay grips. */
|
|
303
|
+
tableContext?: import("../api/public-types").TableStructureContextSnapshot | null;
|
|
304
|
+
/** P6: column resize committed from overlay grip → set-column-width op. */
|
|
305
|
+
onSetColumnWidth?: (columnIndex: number, twips: number) => void;
|
|
306
|
+
/** P6: row resize committed from overlay grip → set-row-height op. */
|
|
307
|
+
onSetRowHeight?: (rowIndex: number, twips: number, rule: "auto" | "atLeast" | "exact") => void;
|
|
308
|
+
onListIndent?: () => void;
|
|
309
|
+
onListOutdent?: () => void;
|
|
274
310
|
onUpdateFields?: () => void;
|
|
275
311
|
onUpdateTableOfContents?: () => void;
|
|
276
312
|
onGoToPreviousReviewItem?: () => void;
|
|
277
313
|
onGoToNextReviewItem?: () => void;
|
|
278
314
|
onMarkSectionForReview?: () => void;
|
|
315
|
+
/** Optional: open sidebar to tracked-changes panel. When provided, the review role shows a sidebar-TC icon. */
|
|
316
|
+
onReviewSidebarTrackedChanges?: () => void;
|
|
317
|
+
/** Optional: open sidebar to comments panel. When provided, the review role shows a sidebar-comments icon. */
|
|
318
|
+
onReviewSidebarComments?: () => void;
|
|
279
319
|
onNavigateHeading?: (headingId: string) => void;
|
|
280
320
|
chromeVisibility?: Partial<ReviewWorkspaceChromeVisibility>;
|
|
321
|
+
/**
|
|
322
|
+
* Called when the shell-header mode tab changes or any chrome surface fires
|
|
323
|
+
* a role switch. Wire to `runtime.setEditorRole(role)` so the workspace
|
|
324
|
+
* re-renders with the new per-role action set.
|
|
325
|
+
*/
|
|
326
|
+
onEditorRoleChange?: (role: import("../api/public-types.ts").EditorRole) => void;
|
|
327
|
+
/**
|
|
328
|
+
* Scope card mode selector fired a mode change. Wire to the host's
|
|
329
|
+
* existing overlay-apply path (or an equivalent CCEP workflow
|
|
330
|
+
* endpoint). The card never mutates runtime state directly.
|
|
331
|
+
*/
|
|
332
|
+
onScopeModeChangeRequested?: (payload: {
|
|
333
|
+
scopeId: string;
|
|
334
|
+
mode: import("../api/public-types.ts").WorkflowScopeMode;
|
|
335
|
+
}) => void;
|
|
336
|
+
/**
|
|
337
|
+
* Scope card issue row fired an action (resolve/waive/escalate/
|
|
338
|
+
* acknowledge). Host updates the attached `IssueMetadataValue`
|
|
339
|
+
* state and re-pushes via `setWorkflowMetadataEntries`.
|
|
340
|
+
*/
|
|
341
|
+
onScopeIssueActionRequested?: (payload: {
|
|
342
|
+
scopeId: string;
|
|
343
|
+
issueId: string;
|
|
344
|
+
action: import("../api/public-types.ts").ScopeIssueAction;
|
|
345
|
+
}) => void;
|
|
281
346
|
}
|
|
282
347
|
|
|
283
348
|
export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
@@ -292,6 +357,40 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
292
357
|
const markupDisplay = props.markupDisplay;
|
|
293
358
|
const [navOpen, setNavOpen] = useState(false);
|
|
294
359
|
const [layoutToolsOpen, setLayoutToolsOpen] = useState(false);
|
|
360
|
+
|
|
361
|
+
// Scope card state — tracks which scope's card is currently open so
|
|
362
|
+
// the ChromeOverlay's card layer renders the right one. The card
|
|
363
|
+
// closes on click-outside, Escape, or a repeat click on its stripe.
|
|
364
|
+
const [activeScopeId, setActiveScopeId] = useState<string | null>(null);
|
|
365
|
+
const handleScopeStripeClick = useCallback(
|
|
366
|
+
(segment: { scopeId: string }) => {
|
|
367
|
+
setActiveScopeId((current) =>
|
|
368
|
+
current === segment.scopeId ? null : segment.scopeId,
|
|
369
|
+
);
|
|
370
|
+
},
|
|
371
|
+
[],
|
|
372
|
+
);
|
|
373
|
+
const handleScopeCardClose = useCallback(() => {
|
|
374
|
+
setActiveScopeId(null);
|
|
375
|
+
}, []);
|
|
376
|
+
const onScopeModeChangeRequested = props.onScopeModeChangeRequested;
|
|
377
|
+
const handleScopeCardModeChange = useCallback(
|
|
378
|
+
(scopeId: string, mode: import("../api/public-types.ts").WorkflowScopeMode) => {
|
|
379
|
+
onScopeModeChangeRequested?.({ scopeId, mode });
|
|
380
|
+
},
|
|
381
|
+
[onScopeModeChangeRequested],
|
|
382
|
+
);
|
|
383
|
+
const onScopeIssueActionRequested = props.onScopeIssueActionRequested;
|
|
384
|
+
const handleScopeCardIssueAction = useCallback(
|
|
385
|
+
(
|
|
386
|
+
scopeId: string,
|
|
387
|
+
issueId: string,
|
|
388
|
+
action: import("../api/public-types.ts").ScopeIssueAction,
|
|
389
|
+
) => {
|
|
390
|
+
onScopeIssueActionRequested?.({ scopeId, issueId, action });
|
|
391
|
+
},
|
|
392
|
+
[onScopeIssueActionRequested],
|
|
393
|
+
);
|
|
295
394
|
const zoomLevel = props.zoomLevel ?? 100;
|
|
296
395
|
const zoomScale = typeof zoomLevel === "number" ? zoomLevel / 100 : 1;
|
|
297
396
|
const pageZoomBucket =
|
|
@@ -319,6 +418,9 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
319
418
|
reviewRailAvailable,
|
|
320
419
|
}),
|
|
321
420
|
);
|
|
421
|
+
// Incremented on zoom_changed / render_frame_ready so the placement
|
|
422
|
+
// useMemo below re-executes when the render kernel emits new rects.
|
|
423
|
+
const [renderFrameRevision, setRenderFrameRevision] = useState(0);
|
|
322
424
|
const responsiveChromeSignatureRef = useRef<string | null>(null);
|
|
323
425
|
const headings = props.documentNavigation?.headings ?? [];
|
|
324
426
|
const headerVariant = snapshot.pageLayout?.headerVariants[0]?.variant ?? "default";
|
|
@@ -348,11 +450,6 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
348
450
|
),
|
|
349
451
|
[props.documentNavigation, snapshot.activeStory, snapshot.pageLayout, snapshot.surface],
|
|
350
452
|
);
|
|
351
|
-
const selectionToolbarPlacement = resolveSelectionToolbarPlacement(
|
|
352
|
-
props.selectionToolAnchor,
|
|
353
|
-
selectionToolbarRootRef.current,
|
|
354
|
-
zoomScale,
|
|
355
|
-
);
|
|
356
453
|
const effectiveSelectionMode = props.interactionGuardSnapshot?.effectiveMode ?? "edit";
|
|
357
454
|
const allowLocalChromeMutations = Boolean(caps?.canEdit) && effectiveSelectionMode === "edit";
|
|
358
455
|
const gatedSelectionTool = useMemo(() => {
|
|
@@ -364,6 +461,40 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
364
461
|
}
|
|
365
462
|
return props.activeSelectionTool;
|
|
366
463
|
}, [props.activeSelectionTool, chromeVisibility.contextToolbars]);
|
|
464
|
+
const selectionToolbarPlacement = useMemo(() => {
|
|
465
|
+
// Prefer render-frame anchors when the layout facet is available — this
|
|
466
|
+
// keeps the tool glued to kernel coordinates across zoom, scroll, and
|
|
467
|
+
// predicted-text reconciliation (R4).
|
|
468
|
+
if (props.layoutFacet && gatedSelectionTool) {
|
|
469
|
+
const anchorRect = resolveSelectionAnchor({
|
|
470
|
+
facet: props.layoutFacet,
|
|
471
|
+
selection: viewState.selection,
|
|
472
|
+
tool: gatedSelectionTool,
|
|
473
|
+
});
|
|
474
|
+
if (anchorRect && selectionToolbarRootRef.current) {
|
|
475
|
+
const containerRect = selectionToolbarRootRef.current.getBoundingClientRect();
|
|
476
|
+
const result = resolveSelectionToolPlacement({
|
|
477
|
+
anchor: anchorRect,
|
|
478
|
+
container: { widthPx: containerRect.width, heightPx: containerRect.height },
|
|
479
|
+
});
|
|
480
|
+
if (result) return result;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
// Fall back to DOM rects for hosts that do not supply a layout facet.
|
|
484
|
+
return resolveSelectionToolbarPlacement(
|
|
485
|
+
props.selectionToolAnchor,
|
|
486
|
+
selectionToolbarRootRef.current,
|
|
487
|
+
zoomScale,
|
|
488
|
+
);
|
|
489
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
490
|
+
}, [
|
|
491
|
+
props.layoutFacet,
|
|
492
|
+
props.selectionToolAnchor,
|
|
493
|
+
gatedSelectionTool,
|
|
494
|
+
viewState.selection,
|
|
495
|
+
zoomScale,
|
|
496
|
+
renderFrameRevision,
|
|
497
|
+
]);
|
|
367
498
|
const activePage = props.documentNavigation?.pages[props.documentNavigation.activePageIndex] ?? null;
|
|
368
499
|
const pageShellMetrics = useMemo(
|
|
369
500
|
() => buildPageShellMetrics(snapshot.pageLayout),
|
|
@@ -388,6 +519,9 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
388
519
|
}),
|
|
389
520
|
[reviewRailAvailable, reviewRailOpen, viewportWidth],
|
|
390
521
|
);
|
|
522
|
+
const hasSidebarPanelAccess = Boolean(
|
|
523
|
+
props.onReviewSidebarTrackedChanges || props.onReviewSidebarComments,
|
|
524
|
+
);
|
|
391
525
|
const scopedChromePolicy = useMemo(
|
|
392
526
|
() =>
|
|
393
527
|
resolveScopedChromePolicy({
|
|
@@ -397,14 +531,18 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
397
531
|
interactionGuardSnapshot: props.interactionGuardSnapshot,
|
|
398
532
|
workflowScopeSnapshot: props.workflowScopeSnapshot,
|
|
399
533
|
activeListContext: props.activeListContext,
|
|
534
|
+
role: viewState.editorRole,
|
|
535
|
+
hasSidebarPanelAccess,
|
|
400
536
|
}),
|
|
401
537
|
[
|
|
402
538
|
caps,
|
|
403
539
|
chromePreset,
|
|
540
|
+
hasSidebarPanelAccess,
|
|
404
541
|
props.activeListContext,
|
|
405
542
|
props.interactionGuardSnapshot,
|
|
406
543
|
props.workflowScopeSnapshot,
|
|
407
544
|
responsiveChrome.isNarrow,
|
|
545
|
+
viewState.editorRole,
|
|
408
546
|
],
|
|
409
547
|
);
|
|
410
548
|
const toolbarInteractionPolicy: ToolbarInteractionPolicy | undefined = caps
|
|
@@ -446,6 +584,18 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
446
584
|
};
|
|
447
585
|
}, []);
|
|
448
586
|
|
|
587
|
+
// Subscribe to layout facet events so chrome re-projects on zoom changes
|
|
588
|
+
// and after incremental relayout (R4).
|
|
589
|
+
useEffect(() => {
|
|
590
|
+
if (!props.layoutFacet) return;
|
|
591
|
+
const unsub = props.layoutFacet.subscribe((event) => {
|
|
592
|
+
if (event.kind === "zoom_changed" || event.kind === "render_frame_ready") {
|
|
593
|
+
setRenderFrameRevision((n) => n + 1);
|
|
594
|
+
}
|
|
595
|
+
});
|
|
596
|
+
return unsub;
|
|
597
|
+
}, [props.layoutFacet]);
|
|
598
|
+
|
|
449
599
|
useEffect(() => {
|
|
450
600
|
const responsiveSignature = `${reviewRailAvailable ? "1" : "0"}:${isNarrowChromeViewport(viewportWidth) ? "n" : "d"}`;
|
|
451
601
|
if (responsiveChromeSignatureRef.current === responsiveSignature) {
|
|
@@ -492,6 +642,14 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
492
642
|
};
|
|
493
643
|
}, [responsiveChrome.showDrawerReviewRail]);
|
|
494
644
|
|
|
645
|
+
useEffect(() => {
|
|
646
|
+
if (!props.layoutFacet) return;
|
|
647
|
+
const facet = props.layoutFacet;
|
|
648
|
+
void document.fonts.ready.then(() => {
|
|
649
|
+
facet.swapMeasurementProvider(createCanvasBackend());
|
|
650
|
+
});
|
|
651
|
+
}, [props.layoutFacet]);
|
|
652
|
+
|
|
495
653
|
return (
|
|
496
654
|
<Tooltip.Provider delayDuration={400}>
|
|
497
655
|
<div className="flex h-full flex-col bg-canvas text-primary">
|
|
@@ -609,6 +767,12 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
609
767
|
role={viewState.editorRole}
|
|
610
768
|
reviewQueue={props.reviewQueue}
|
|
611
769
|
markupDisplay={markupDisplay}
|
|
770
|
+
onReviewSidebarTrackedChanges={props.onReviewSidebarTrackedChanges
|
|
771
|
+
? runWithSelectionToolbarDismiss(props.onReviewSidebarTrackedChanges)
|
|
772
|
+
: undefined}
|
|
773
|
+
onReviewSidebarComments={props.onReviewSidebarComments
|
|
774
|
+
? runWithSelectionToolbarDismiss(props.onReviewSidebarComments)
|
|
775
|
+
: undefined}
|
|
612
776
|
onMarkScopePosture={props.onMarkSectionForReview
|
|
613
777
|
? runWithSelectionToolbarDismiss(props.onMarkSectionForReview)
|
|
614
778
|
: undefined}
|
|
@@ -642,27 +806,14 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
642
806
|
props.onRejectRevision?.(revisionId);
|
|
643
807
|
};
|
|
644
808
|
})()}
|
|
809
|
+
chromePins={viewState.chromePins}
|
|
810
|
+
onChromePinChange={props.onChromePinChange}
|
|
645
811
|
/>
|
|
646
812
|
</div>
|
|
647
813
|
) : null}
|
|
648
814
|
|
|
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}
|
|
815
|
+
{/* Legacy TwReviewQueueBar is suppressed — review role's action region
|
|
816
|
+
now owns queue prev/next + counts inline in the top toolbar. */}
|
|
666
817
|
|
|
667
818
|
{chromeVisibility.alerts ? <TwAlertBanner
|
|
668
819
|
snapshot={snapshot}
|
|
@@ -967,6 +1118,11 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
967
1118
|
onSetImageFrame={props.onSetImageFrame}
|
|
968
1119
|
onRestartNumbering={props.onRestartNumbering}
|
|
969
1120
|
onContinueNumbering={props.onContinueNumbering}
|
|
1121
|
+
onToggleRowHeader={props.onToggleRowHeader}
|
|
1122
|
+
onToggleRowCantSplit={props.onToggleRowCantSplit}
|
|
1123
|
+
onDistributeColumnsEvenly={props.onDistributeColumnsEvenly}
|
|
1124
|
+
onSetTableAlignment={props.onSetTableAlignment}
|
|
1125
|
+
onSetCellVerticalAlign={props.onSetCellVerticalAlign}
|
|
970
1126
|
chromePins={viewState.chromePins}
|
|
971
1127
|
onChromePinChange={props.onChromePinChange}
|
|
972
1128
|
/>
|
|
@@ -1035,15 +1191,27 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
1035
1191
|
style={pageChromeModel.pageBorderStyle}
|
|
1036
1192
|
/>
|
|
1037
1193
|
) : null}
|
|
1038
|
-
<div className={isPageWorkspace ? "relative z-10" :
|
|
1194
|
+
<div className={isPageWorkspace ? "relative z-10" : "relative"}>
|
|
1195
|
+
{/* Page chrome (frame borders, header/footer bands,
|
|
1196
|
+
page-number labels, inter-page separators) is
|
|
1197
|
+
rendered as in-flow widget decorations inside
|
|
1198
|
+
the PM surface itself — see
|
|
1199
|
+
`pm-page-break-decorations.ts`. That keeps the
|
|
1200
|
+
chrome perfectly aligned with PM content without
|
|
1201
|
+
any absolute-positioned overlay that would drift
|
|
1202
|
+
relative to the browser's line layout. */}
|
|
1039
1203
|
{props.document}
|
|
1040
1204
|
{props.layoutFacet ? (
|
|
1041
1205
|
<TwChromeOverlay
|
|
1042
1206
|
facet={props.layoutFacet}
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
}
|
|
1046
|
-
|
|
1207
|
+
tableContext={props.tableContext}
|
|
1208
|
+
onSetColumnWidth={props.onSetColumnWidth}
|
|
1209
|
+
onSetRowHeight={props.onSetRowHeight}
|
|
1210
|
+
activeScopeId={activeScopeId}
|
|
1211
|
+
onScopeStripeClick={handleScopeStripeClick}
|
|
1212
|
+
onScopeCardClose={handleScopeCardClose}
|
|
1213
|
+
onScopeCardModeChange={handleScopeCardModeChange}
|
|
1214
|
+
onScopeCardIssueAction={handleScopeCardIssueAction}
|
|
1047
1215
|
/>
|
|
1048
1216
|
) : null}
|
|
1049
1217
|
</div>
|
|
@@ -1070,32 +1238,10 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
1070
1238
|
</div>
|
|
1071
1239
|
</div>
|
|
1072
1240
|
</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}
|
|
1241
|
+
{/* Pages 2..N in page mode are now rendered by TwPageStackChrome
|
|
1242
|
+
as absolute overlays above the single flowing PM surface.
|
|
1243
|
+
The PM surface holds all editable content; page frames draw
|
|
1244
|
+
borders, header/footer bands, and per-page numbers on top. */}
|
|
1099
1245
|
</div>
|
|
1100
1246
|
|
|
1101
1247
|
{chromeVisibility.statusBar ? (
|
|
@@ -1202,6 +1348,13 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
1202
1348
|
</div>
|
|
1203
1349
|
) : null}
|
|
1204
1350
|
</div>
|
|
1351
|
+
{props.modeDock ? (
|
|
1352
|
+
<TwModeDock
|
|
1353
|
+
label={props.modeDock.label}
|
|
1354
|
+
icon={props.modeDock.icon}
|
|
1355
|
+
actions={props.modeDock.actions}
|
|
1356
|
+
/>
|
|
1357
|
+
) : null}
|
|
1205
1358
|
</div>
|
|
1206
1359
|
</Tooltip.Provider>
|
|
1207
1360
|
);
|
|
@@ -1,254 +0,0 @@
|
|
|
1
|
-
import * as Y from "yjs";
|
|
2
|
-
|
|
3
|
-
import type { DocumentRuntime, DocumentRuntimeEvent, Unsubscribe } from "./document-runtime.ts";
|
|
4
|
-
|
|
5
|
-
// ---------------------------------------------------------------------------
|
|
6
|
-
// Serialised shapes stored inside the Y.Maps
|
|
7
|
-
// ---------------------------------------------------------------------------
|
|
8
|
-
|
|
9
|
-
interface YCommentThread {
|
|
10
|
-
commentId: string;
|
|
11
|
-
status: "open" | "resolved" | "detached";
|
|
12
|
-
anchor: { kind: string; [key: string]: unknown };
|
|
13
|
-
createdAt: string;
|
|
14
|
-
createdBy: string;
|
|
15
|
-
authorId: string;
|
|
16
|
-
body: string;
|
|
17
|
-
entries: Array<{
|
|
18
|
-
entryId: string;
|
|
19
|
-
authorId: string;
|
|
20
|
-
body: string;
|
|
21
|
-
createdAt: string;
|
|
22
|
-
}>;
|
|
23
|
-
resolvedAt?: string;
|
|
24
|
-
resolvedBy?: string;
|
|
25
|
-
warningIds: string[];
|
|
26
|
-
sourceClientId: number;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
interface YRevisionAction {
|
|
30
|
-
changeId: string;
|
|
31
|
-
action: "accept" | "reject";
|
|
32
|
-
sourceClientId: number;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// ---------------------------------------------------------------------------
|
|
36
|
-
// Public API
|
|
37
|
-
// ---------------------------------------------------------------------------
|
|
38
|
-
|
|
39
|
-
export interface CollabReviewSyncHandle {
|
|
40
|
-
destroy(): void;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export function createCollabReviewSync(
|
|
44
|
-
ydoc: Y.Doc,
|
|
45
|
-
runtime: DocumentRuntime,
|
|
46
|
-
): CollabReviewSyncHandle {
|
|
47
|
-
const yComments = ydoc.getMap<YCommentThread>("comments");
|
|
48
|
-
const yRevisionActions = ydoc.getMap<YRevisionAction>("revisionActions");
|
|
49
|
-
const clientId = ydoc.clientID;
|
|
50
|
-
|
|
51
|
-
let suppressLocalEvents = false;
|
|
52
|
-
|
|
53
|
-
// --- Local → Yjs ---------------------------------------------------------
|
|
54
|
-
|
|
55
|
-
const unsubEvents: Unsubscribe = runtime.subscribeToEvents((event) => {
|
|
56
|
-
if (suppressLocalEvents) return;
|
|
57
|
-
|
|
58
|
-
switch (event.type) {
|
|
59
|
-
case "comment_added":
|
|
60
|
-
pushCommentToYjs(event.commentId);
|
|
61
|
-
break;
|
|
62
|
-
case "comment_resolved":
|
|
63
|
-
syncCommentFieldToYjs(event.commentId);
|
|
64
|
-
break;
|
|
65
|
-
case "change_accepted":
|
|
66
|
-
yRevisionActions.set(revisionActionKey(event.changeId, "accept"), {
|
|
67
|
-
changeId: event.changeId,
|
|
68
|
-
action: "accept",
|
|
69
|
-
sourceClientId: clientId,
|
|
70
|
-
});
|
|
71
|
-
break;
|
|
72
|
-
case "change_rejected":
|
|
73
|
-
yRevisionActions.set(revisionActionKey(event.changeId, "reject"), {
|
|
74
|
-
changeId: event.changeId,
|
|
75
|
-
action: "reject",
|
|
76
|
-
sourceClientId: clientId,
|
|
77
|
-
});
|
|
78
|
-
break;
|
|
79
|
-
}
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
function pushCommentToYjs(commentId: string): void {
|
|
83
|
-
const thread = runtime
|
|
84
|
-
.getRenderSnapshot()
|
|
85
|
-
.comments.threads.find((t) => t.commentId === commentId);
|
|
86
|
-
if (!thread) return;
|
|
87
|
-
|
|
88
|
-
yComments.set(commentId, {
|
|
89
|
-
commentId: thread.commentId,
|
|
90
|
-
status: thread.status,
|
|
91
|
-
anchor: thread.anchor,
|
|
92
|
-
createdAt: thread.createdAt,
|
|
93
|
-
createdBy: thread.createdBy,
|
|
94
|
-
authorId: thread.createdBy,
|
|
95
|
-
body: thread.entries[0]?.body ?? "",
|
|
96
|
-
entries: thread.entries.map((e) => ({
|
|
97
|
-
entryId: e.entryId,
|
|
98
|
-
authorId: e.authorId,
|
|
99
|
-
body: e.body,
|
|
100
|
-
createdAt: e.createdAt,
|
|
101
|
-
})),
|
|
102
|
-
resolvedAt: thread.resolvedAt,
|
|
103
|
-
resolvedBy: thread.resolvedBy,
|
|
104
|
-
warningIds: [],
|
|
105
|
-
sourceClientId: clientId,
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function syncCommentFieldToYjs(commentId: string): void {
|
|
110
|
-
const existing = yComments.get(commentId);
|
|
111
|
-
const thread = runtime
|
|
112
|
-
.getRenderSnapshot()
|
|
113
|
-
.comments.threads.find((t) => t.commentId === commentId);
|
|
114
|
-
if (!existing || !thread) return;
|
|
115
|
-
|
|
116
|
-
yComments.set(commentId, {
|
|
117
|
-
...existing,
|
|
118
|
-
status: thread.status,
|
|
119
|
-
entries: thread.entries.map((e) => ({
|
|
120
|
-
entryId: e.entryId,
|
|
121
|
-
authorId: e.authorId,
|
|
122
|
-
body: e.body,
|
|
123
|
-
createdAt: e.createdAt,
|
|
124
|
-
})),
|
|
125
|
-
resolvedAt: thread.resolvedAt,
|
|
126
|
-
resolvedBy: thread.resolvedBy,
|
|
127
|
-
sourceClientId: clientId,
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// --- Yjs → Local ---------------------------------------------------------
|
|
132
|
-
|
|
133
|
-
function onCommentMapChange(event: Y.YMapEvent<YCommentThread>): void {
|
|
134
|
-
suppressLocalEvents = true;
|
|
135
|
-
try {
|
|
136
|
-
for (const [commentId, change] of event.changes.keys) {
|
|
137
|
-
const entry = yComments.get(commentId);
|
|
138
|
-
if (!entry || entry.sourceClientId === clientId) continue;
|
|
139
|
-
|
|
140
|
-
const existing = runtime
|
|
141
|
-
.getRenderSnapshot()
|
|
142
|
-
.comments.threads.find((t) => t.commentId === commentId);
|
|
143
|
-
|
|
144
|
-
if (change.action === "add" && !existing) {
|
|
145
|
-
runtime.dispatch({
|
|
146
|
-
type: "comment.add",
|
|
147
|
-
comment: {
|
|
148
|
-
commentId: entry.commentId,
|
|
149
|
-
status: entry.status,
|
|
150
|
-
anchor: entry.anchor as never,
|
|
151
|
-
createdAt: entry.createdAt,
|
|
152
|
-
createdBy: entry.createdBy,
|
|
153
|
-
authorId: entry.authorId,
|
|
154
|
-
body: entry.body,
|
|
155
|
-
entries: entry.entries,
|
|
156
|
-
warningIds: entry.warningIds,
|
|
157
|
-
isResolved: entry.status === "resolved",
|
|
158
|
-
metadata: { source: "runtime" },
|
|
159
|
-
},
|
|
160
|
-
});
|
|
161
|
-
} else if (change.action === "update" && existing) {
|
|
162
|
-
if (entry.status === "resolved" && existing.status !== "resolved") {
|
|
163
|
-
runtime.dispatch({
|
|
164
|
-
type: "comment.resolve",
|
|
165
|
-
commentId,
|
|
166
|
-
resolvedBy: entry.resolvedBy,
|
|
167
|
-
});
|
|
168
|
-
} else if (entry.status === "open" && existing.status === "resolved") {
|
|
169
|
-
runtime.dispatch({ type: "comment.reopen", commentId });
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
const localEntryCount = existing.entries.length;
|
|
173
|
-
if (entry.entries.length > localEntryCount) {
|
|
174
|
-
for (const newEntry of entry.entries.slice(localEntryCount)) {
|
|
175
|
-
runtime.dispatch({
|
|
176
|
-
type: "comment.add-reply",
|
|
177
|
-
commentId,
|
|
178
|
-
body: newEntry.body,
|
|
179
|
-
authorId: newEntry.authorId,
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
} finally {
|
|
186
|
-
suppressLocalEvents = false;
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function onRevisionActionMapChange(event: Y.YMapEvent<YRevisionAction>): void {
|
|
191
|
-
suppressLocalEvents = true;
|
|
192
|
-
try {
|
|
193
|
-
for (const [, change] of event.changes.keys) {
|
|
194
|
-
if (change.action !== "add") continue;
|
|
195
|
-
const entries = [...yRevisionActions.entries()];
|
|
196
|
-
const latest = entries[entries.length - 1];
|
|
197
|
-
if (!latest) continue;
|
|
198
|
-
const entry = latest[1];
|
|
199
|
-
if (entry.sourceClientId === clientId) continue;
|
|
200
|
-
|
|
201
|
-
runtime.dispatch({
|
|
202
|
-
type: entry.action === "accept" ? "change.accept" : "change.reject",
|
|
203
|
-
changeId: entry.changeId,
|
|
204
|
-
});
|
|
205
|
-
}
|
|
206
|
-
} finally {
|
|
207
|
-
suppressLocalEvents = false;
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
yComments.observe(onCommentMapChange);
|
|
212
|
-
yRevisionActions.observe(onRevisionActionMapChange);
|
|
213
|
-
|
|
214
|
-
// --- Initial sync: push existing comments to Yjs if first client ----------
|
|
215
|
-
|
|
216
|
-
const snapshot = runtime.getRenderSnapshot();
|
|
217
|
-
if (yComments.size === 0 && snapshot.comments.threads.length > 0) {
|
|
218
|
-
ydoc.transact(() => {
|
|
219
|
-
for (const thread of snapshot.comments.threads) {
|
|
220
|
-
yComments.set(thread.commentId, {
|
|
221
|
-
commentId: thread.commentId,
|
|
222
|
-
status: thread.status,
|
|
223
|
-
anchor: thread.anchor,
|
|
224
|
-
createdAt: thread.createdAt,
|
|
225
|
-
createdBy: thread.createdBy,
|
|
226
|
-
authorId: thread.createdBy,
|
|
227
|
-
body: thread.entries[0]?.body ?? "",
|
|
228
|
-
entries: thread.entries.map((e) => ({
|
|
229
|
-
entryId: e.entryId,
|
|
230
|
-
authorId: e.authorId,
|
|
231
|
-
body: e.body,
|
|
232
|
-
createdAt: e.createdAt,
|
|
233
|
-
})),
|
|
234
|
-
resolvedAt: thread.resolvedAt,
|
|
235
|
-
resolvedBy: thread.resolvedBy,
|
|
236
|
-
warningIds: [],
|
|
237
|
-
sourceClientId: clientId,
|
|
238
|
-
});
|
|
239
|
-
}
|
|
240
|
-
});
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
return {
|
|
244
|
-
destroy() {
|
|
245
|
-
unsubEvents();
|
|
246
|
-
yComments.unobserve(onCommentMapChange);
|
|
247
|
-
yRevisionActions.unobserve(onRevisionActionMapChange);
|
|
248
|
-
},
|
|
249
|
-
};
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
function revisionActionKey(changeId: string, action: string): string {
|
|
253
|
-
return `${changeId}:${action}`;
|
|
254
|
-
}
|