@beyondwork/docx-react-component 1.0.37 → 1.0.38

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.
Files changed (74) hide show
  1. package/package.json +1 -1
  2. package/src/api/public-types.ts +319 -1
  3. package/src/core/commands/section-layout-commands.ts +58 -0
  4. package/src/core/commands/table-grid.ts +431 -0
  5. package/src/core/commands/table-structure-commands.ts +815 -55
  6. package/src/io/export/serialize-main-document.ts +2 -11
  7. package/src/io/export/serialize-numbering.ts +1 -2
  8. package/src/io/export/serialize-tables.ts +74 -0
  9. package/src/io/export/table-properties-xml.ts +139 -4
  10. package/src/io/normalize/normalize-text.ts +15 -0
  11. package/src/io/ooxml/parse-footnotes.ts +60 -0
  12. package/src/io/ooxml/parse-headers-footers.ts +60 -0
  13. package/src/io/ooxml/parse-main-document.ts +137 -0
  14. package/src/io/ooxml/parse-tables.ts +249 -0
  15. package/src/model/canonical-document.ts +34 -0
  16. package/src/runtime/document-layout.ts +4 -2
  17. package/src/runtime/document-navigation.ts +1 -1
  18. package/src/runtime/document-runtime.ts +114 -0
  19. package/src/runtime/layout/default-page-format.ts +96 -0
  20. package/src/runtime/layout/index.ts +45 -0
  21. package/src/runtime/layout/inert-layout-facet.ts +14 -0
  22. package/src/runtime/layout/layout-engine-instance.ts +33 -23
  23. package/src/runtime/layout/margin-preset-catalog.ts +178 -0
  24. package/src/runtime/layout/page-format-catalog.ts +233 -0
  25. package/src/runtime/layout/page-graph.ts +19 -0
  26. package/src/runtime/layout/paginated-layout-engine.ts +142 -9
  27. package/src/runtime/layout/project-block-fragments.ts +91 -0
  28. package/src/runtime/layout/public-facet.ts +709 -16
  29. package/src/runtime/layout/table-render-plan.ts +229 -0
  30. package/src/runtime/render/block-fragment-projection.ts +35 -0
  31. package/src/runtime/render/decoration-resolver.ts +189 -0
  32. package/src/runtime/render/index.ts +57 -0
  33. package/src/runtime/render/pending-op-delta-reader.ts +129 -0
  34. package/src/runtime/render/render-frame-types.ts +317 -0
  35. package/src/runtime/render/render-kernel.ts +755 -0
  36. package/src/runtime/view-state.ts +67 -0
  37. package/src/runtime/workflow-markup.ts +1 -5
  38. package/src/runtime/workflow-rail-segments.ts +280 -0
  39. package/src/ui/WordReviewEditor.tsx +84 -15
  40. package/src/ui/editor-shell-view.tsx +6 -0
  41. package/src/ui/headless/chrome-registry.ts +280 -14
  42. package/src/ui/headless/scoped-chrome-policy.ts +20 -1
  43. package/src/ui/headless/selection-tool-types.ts +10 -0
  44. package/src/ui-tailwind/chrome/chrome-preset-model.ts +23 -2
  45. package/src/ui-tailwind/chrome/role-action-sets.ts +74 -0
  46. package/src/ui-tailwind/chrome/tw-detach-handle.tsx +147 -0
  47. package/src/ui-tailwind/chrome/tw-selection-anchor-resolver.ts +163 -0
  48. package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +57 -92
  49. package/src/ui-tailwind/chrome/tw-selection-tool-placement.ts +149 -0
  50. package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +15 -4
  51. package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +274 -138
  52. package/src/ui-tailwind/chrome-overlay/chrome-overlay-projector.ts +90 -0
  53. package/src/ui-tailwind/chrome-overlay/index.ts +22 -0
  54. package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +86 -0
  55. package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +178 -0
  56. package/src/ui-tailwind/chrome-overlay/tw-workspace-view-switcher.tsx +95 -0
  57. package/src/ui-tailwind/editor-surface/fast-text-edit-lane.ts +4 -0
  58. package/src/ui-tailwind/editor-surface/local-edit-session-state.ts +11 -0
  59. package/src/ui-tailwind/editor-surface/perf-probe.ts +7 -1
  60. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +12 -1
  61. package/src/ui-tailwind/editor-surface/pm-decorations.ts +22 -12
  62. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +3 -0
  63. package/src/ui-tailwind/index.ts +33 -0
  64. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +2 -2
  65. package/src/ui-tailwind/review/tw-rail-card.tsx +150 -0
  66. package/src/ui-tailwind/review/tw-review-rail-footer.tsx +52 -0
  67. package/src/ui-tailwind/review/tw-review-rail.tsx +166 -11
  68. package/src/ui-tailwind/review/tw-workflow-tab.tsx +108 -0
  69. package/src/ui-tailwind/theme/editor-theme.css +498 -163
  70. package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +559 -0
  71. package/src/ui-tailwind/toolbar/tw-scope-posture-menu.tsx +182 -0
  72. package/src/ui-tailwind/toolbar/tw-shell-header.tsx +162 -0
  73. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +69 -0
  74. package/src/ui-tailwind/tw-review-workspace.tsx +136 -1
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Scope posture menu — replaces the old "Mark section" button with a
3
+ * topnav dropdown listing the seven `ScopeRailPosture` values so
4
+ * editors can mark regions with an explicit workflow mode instead of a
5
+ * single "marked" flag.
6
+ *
7
+ * Per runtime-rendering-and-chrome-phase.md §6.4, the menu lives inline
8
+ * in the editor role's primary action region (not in the review queue
9
+ * strip). Postures align 1:1 with the rail vocabulary so the rail
10
+ * updates visually as soon as the user picks one.
11
+ */
12
+
13
+ import React, { useState } from "react";
14
+ import * as Popover from "@radix-ui/react-popover";
15
+ import {
16
+ BookmarkPlus,
17
+ ChevronDown,
18
+ Eye,
19
+ Flag,
20
+ Lock,
21
+ MessageCircle,
22
+ Pencil,
23
+ Sparkles,
24
+ } from "lucide-react";
25
+
26
+ import type { ScopeRailPosture } from "../../api/public-types";
27
+ import { preserveEditorSelectionMouseDown } from "../../ui/headless/preserve-editor-selection";
28
+
29
+ export interface TwScopePostureMenuProps {
30
+ disabled?: boolean;
31
+ /** Called when the user picks a posture. Host decides the scope to mark. */
32
+ onSelect: (posture: ScopeRailPosture) => void;
33
+ /** Optional explicit label override (defaults to "Mark…"). */
34
+ label?: string;
35
+ "data-testid"?: string;
36
+ }
37
+
38
+ interface PostureEntry {
39
+ posture: ScopeRailPosture;
40
+ label: string;
41
+ hint: string;
42
+ icon: React.ComponentType<{ className?: string }>;
43
+ tone: "accent" | "warning" | "comment" | "secondary" | "danger";
44
+ }
45
+
46
+ /**
47
+ * Posture catalog. Mirrors `POSTURE_STYLES` in `tw-scope-rail-layer.tsx`
48
+ * but with lucide icon components (the rail uses CSS pseudo-element
49
+ * glyphs via the `data-icon` attribute). Extract both into a single
50
+ * source of truth in a follow-up.
51
+ */
52
+ const POSTURE_ENTRIES: readonly PostureEntry[] = [
53
+ {
54
+ posture: "edit",
55
+ label: "Edit scope",
56
+ hint: "Full authoring inside this region",
57
+ icon: Pencil,
58
+ tone: "accent",
59
+ },
60
+ {
61
+ posture: "suggest",
62
+ label: "Suggest scope",
63
+ hint: "Tracked-change suggestions only",
64
+ icon: Sparkles,
65
+ tone: "warning",
66
+ },
67
+ {
68
+ posture: "comment",
69
+ label: "Comment scope",
70
+ hint: "Comments only; body is read-only",
71
+ icon: MessageCircle,
72
+ tone: "comment",
73
+ },
74
+ {
75
+ posture: "view",
76
+ label: "In scope",
77
+ hint: "Read-only, in scope of review",
78
+ icon: Eye,
79
+ tone: "secondary",
80
+ },
81
+ {
82
+ posture: "candidate",
83
+ label: "Propose scope",
84
+ hint: "Candidate — not yet committed",
85
+ icon: Flag,
86
+ tone: "warning",
87
+ },
88
+ {
89
+ posture: "preserve-only",
90
+ label: "Preserve only",
91
+ hint: "Blocked — export-preserving only",
92
+ icon: Lock,
93
+ tone: "danger",
94
+ },
95
+ {
96
+ posture: "blocked-import",
97
+ label: "Blocked import",
98
+ hint: "Blocked — imported region is locked",
99
+ icon: Lock,
100
+ tone: "danger",
101
+ },
102
+ ];
103
+
104
+ export function TwScopePostureMenu(props: TwScopePostureMenuProps): React.JSX.Element {
105
+ const [open, setOpen] = useState(false);
106
+
107
+ return (
108
+ <Popover.Root open={open} onOpenChange={setOpen}>
109
+ <Popover.Trigger asChild>
110
+ <button
111
+ type="button"
112
+ aria-label={`${props.label ?? "Mark"} section`}
113
+ aria-expanded={open}
114
+ disabled={props.disabled}
115
+ onMouseDown={preserveEditorSelectionMouseDown}
116
+ className="inline-flex h-6 items-center gap-1 rounded-md border border-border bg-canvas px-2 text-[11px] font-medium text-primary transition-colors hover:bg-surface outline-none disabled:cursor-not-allowed disabled:opacity-40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-canvas"
117
+ data-testid={props["data-testid"] ?? "scope-posture-menu-trigger"}
118
+ >
119
+ <BookmarkPlus className="h-3.5 w-3.5" />
120
+ <span>{props.label ?? "Mark…"}</span>
121
+ <ChevronDown className="h-3.5 w-3.5 text-tertiary" />
122
+ </button>
123
+ </Popover.Trigger>
124
+ <Popover.Portal>
125
+ <Popover.Content
126
+ className="z-50 w-[260px] rounded-lg bg-canvas p-1 shadow-lg ring-1 ring-border"
127
+ sideOffset={8}
128
+ align="start"
129
+ data-testid="scope-posture-menu-content"
130
+ >
131
+ <div className="mb-1 px-2 py-1 text-[10px] font-semibold uppercase tracking-[0.12em] text-tertiary">
132
+ Mark section with posture
133
+ </div>
134
+ {POSTURE_ENTRIES.map((entry) => (
135
+ <Popover.Close key={entry.posture} asChild>
136
+ <button
137
+ type="button"
138
+ aria-label={`Mark section as ${entry.label}`}
139
+ onMouseDown={preserveEditorSelectionMouseDown}
140
+ onClick={() => {
141
+ props.onSelect(entry.posture);
142
+ }}
143
+ className="flex w-full items-start gap-2 rounded-md px-2 py-1.5 text-left text-[11px] transition-colors hover:bg-surface focus-visible:outline-none focus-visible:bg-surface"
144
+ data-testid={`scope-posture-option-${entry.posture}`}
145
+ data-posture={entry.posture}
146
+ >
147
+ <entry.icon
148
+ className={[
149
+ "mt-0.5 h-3.5 w-3.5 shrink-0",
150
+ toneClass(entry.tone),
151
+ ].join(" ")}
152
+ />
153
+ <span className="flex flex-col">
154
+ <span className="font-medium text-primary">{entry.label}</span>
155
+ <span className="text-[10px] text-secondary">{entry.hint}</span>
156
+ </span>
157
+ </button>
158
+ </Popover.Close>
159
+ ))}
160
+ </Popover.Content>
161
+ </Popover.Portal>
162
+ </Popover.Root>
163
+ );
164
+ }
165
+
166
+ function toneClass(tone: PostureEntry["tone"]): string {
167
+ switch (tone) {
168
+ case "accent":
169
+ return "text-accent";
170
+ case "warning":
171
+ return "text-warning";
172
+ case "comment":
173
+ return "text-comment";
174
+ case "danger":
175
+ return "text-danger";
176
+ case "secondary":
177
+ default:
178
+ return "text-secondary";
179
+ }
180
+ }
181
+
182
+ export default TwScopePostureMenu;
@@ -0,0 +1,162 @@
1
+ import React, { type ReactNode } from "react";
2
+ import * as Tabs from "@radix-ui/react-tabs";
3
+
4
+ /**
5
+ * TwShellHeader — the top "app chrome" bar above the document canvas.
6
+ *
7
+ * Anatomy matches the editorial reference mock:
8
+ * ┌─────────────────────────────────────────────────────────────────────┐
9
+ * │ [brand] Edit | Review | Workflow | More [⋯] [CTA] │
10
+ * └─────────────────────────────────────────────────────────────────────┘
11
+ *
12
+ * All three regions are optional slots — hosts opt in by supplying the
13
+ * corresponding prop. When nothing is supplied the header renders empty but
14
+ * preserves layout height so the document canvas does not jump when a CTA
15
+ * appears.
16
+ */
17
+
18
+ export type ShellHeaderMode = "edit" | "review" | "workflow" | "more";
19
+
20
+ export interface ShellHeaderModeOption {
21
+ id: ShellHeaderMode;
22
+ label: string;
23
+ disabled?: boolean;
24
+ }
25
+
26
+ export interface ShellHeaderPrimaryAction {
27
+ label: string;
28
+ onClick: () => void;
29
+ tone?: "accent" | "neutral";
30
+ disabled?: boolean;
31
+ }
32
+
33
+ export interface ShellHeaderIconAction {
34
+ id: string;
35
+ label: string;
36
+ icon: ReactNode;
37
+ onClick?: () => void;
38
+ href?: string;
39
+ }
40
+
41
+ export interface TwShellHeaderProps {
42
+ brand?: ReactNode;
43
+ modes?: readonly ShellHeaderModeOption[];
44
+ activeMode?: ShellHeaderMode;
45
+ onModeChange?: (mode: ShellHeaderMode) => void;
46
+ iconActions?: readonly ShellHeaderIconAction[];
47
+ primaryAction?: ShellHeaderPrimaryAction;
48
+ /** Thin bottom border appears only when the document has scrolled. */
49
+ isScrolled?: boolean;
50
+ className?: string;
51
+ }
52
+
53
+ const focusRingClass =
54
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1 focus-visible:ring-offset-canvas";
55
+
56
+ export function TwShellHeader(props: TwShellHeaderProps) {
57
+ const hasContent =
58
+ props.brand ||
59
+ (props.modes && props.modes.length > 0) ||
60
+ (props.iconActions && props.iconActions.length > 0) ||
61
+ props.primaryAction;
62
+
63
+ if (!hasContent) {
64
+ return null;
65
+ }
66
+
67
+ const className = [
68
+ "flex h-12 shrink-0 items-center justify-between gap-4 px-4 bg-canvas/92 backdrop-blur-sm",
69
+ props.isScrolled ? "border-b border-border" : "border-b border-transparent",
70
+ "transition-colors duration-[var(--motion-fast)]",
71
+ props.className,
72
+ ]
73
+ .filter(Boolean)
74
+ .join(" ");
75
+
76
+ return (
77
+ <header className={className} data-testid="tw-shell-header">
78
+ <div className="flex min-w-0 items-center gap-3">
79
+ {props.brand ? (
80
+ <div
81
+ className="font-[family-name:var(--font-legal-serif)] text-[15px] font-semibold text-primary truncate"
82
+ data-testid="tw-shell-header__brand"
83
+ >
84
+ {props.brand}
85
+ </div>
86
+ ) : null}
87
+ </div>
88
+
89
+ {props.modes && props.modes.length > 0 && props.activeMode ? (
90
+ <Tabs.Root
91
+ value={props.activeMode}
92
+ onValueChange={(v: string) =>
93
+ props.onModeChange?.(v as ShellHeaderMode)
94
+ }
95
+ >
96
+ <Tabs.List
97
+ aria-label="Workspace modes"
98
+ className="flex items-center gap-1"
99
+ >
100
+ {props.modes.map((mode) => (
101
+ <Tabs.Trigger
102
+ key={mode.id}
103
+ value={mode.id}
104
+ disabled={mode.disabled}
105
+ className={`wre-rail-tab ${focusRingClass}`}
106
+ data-testid={`tw-shell-header__mode-${mode.id}`}
107
+ >
108
+ {mode.label}
109
+ </Tabs.Trigger>
110
+ ))}
111
+ </Tabs.List>
112
+ </Tabs.Root>
113
+ ) : (
114
+ <div aria-hidden="true" />
115
+ )}
116
+
117
+ <div className="flex items-center gap-1">
118
+ {props.iconActions?.map((action) => {
119
+ const commonProps = {
120
+ key: action.id,
121
+ "aria-label": action.label,
122
+ title: action.label,
123
+ className: `inline-flex h-8 w-8 items-center justify-center rounded-sm text-secondary transition-colors hover:bg-surface-hover hover:text-primary ${focusRingClass}`,
124
+ } as const;
125
+
126
+ if (action.href) {
127
+ return (
128
+ <a {...commonProps} href={action.href}>
129
+ <span aria-hidden="true">{action.icon}</span>
130
+ </a>
131
+ );
132
+ }
133
+
134
+ return (
135
+ <button {...commonProps} type="button" onClick={action.onClick}>
136
+ <span aria-hidden="true">{action.icon}</span>
137
+ </button>
138
+ );
139
+ })}
140
+
141
+ {props.primaryAction ? (
142
+ <button
143
+ type="button"
144
+ disabled={props.primaryAction.disabled}
145
+ onClick={props.primaryAction.onClick}
146
+ data-tone={props.primaryAction.tone ?? "accent"}
147
+ className={[
148
+ "ml-2 inline-flex h-8 items-center rounded-sm px-3 text-xs font-semibold transition-colors disabled:opacity-40",
149
+ props.primaryAction.tone === "neutral"
150
+ ? "bg-surface text-primary hover:bg-surface-hover"
151
+ : "bg-accent text-white hover:bg-[color-mix(in_srgb,var(--color-accent)_85%,#000)]",
152
+ focusRingClass,
153
+ ].join(" ")}
154
+ data-testid="tw-shell-header__primary-action"
155
+ >
156
+ {props.primaryAction.label}
157
+ </button>
158
+ ) : null}
159
+ </div>
160
+ </header>
161
+ );
162
+ }
@@ -43,11 +43,14 @@ import {
43
43
  import type {
44
44
  ActiveListContext,
45
45
  CompatibilityPanelSnapshot,
46
+ EditorRole,
46
47
  EditorStoryTarget,
47
48
  EditorWarning,
48
49
  FormattingStateSnapshot,
49
50
  FormattingAlignment,
50
51
  InsertImageOptions,
52
+ ReviewQueueSnapshot,
53
+ ScopeRailPosture,
51
54
  SectionBreakType,
52
55
  StyleCatalogSnapshot,
53
56
  WorkflowBlockedCommandReason,
@@ -64,6 +67,11 @@ import {
64
67
  } from "../../ui/headless/scoped-chrome-policy";
65
68
  import { preserveEditorSelectionMouseDown } from "../../ui/headless/preserve-editor-selection";
66
69
  import { TwHealthPanel } from "../review/tw-health-panel";
70
+ import {
71
+ TwRoleActionRegion,
72
+ type MarkupDisplayMode,
73
+ type WorkflowWorkItemSnapshot,
74
+ } from "./tw-role-action-region";
67
75
  import { TwToolbarIconButton } from "./tw-toolbar-icon-button";
68
76
 
69
77
  export interface TwToolbarProps {
@@ -121,6 +129,39 @@ export interface TwToolbarProps {
121
129
  onContinueNumbering?: () => void;
122
130
  onUpdateFields?: () => void;
123
131
  onUpdateTableOfContents?: () => void;
132
+
133
+ // ───── R1: role-scoped inline action region (spec §6.4) ──────────────
134
+ /**
135
+ * Active editor role. When supplied, the toolbar renders the inline
136
+ * `TwRoleActionRegion` between the left formatting cluster and the
137
+ * right view cluster. Omit to keep the pre-R1 layout.
138
+ */
139
+ role?: EditorRole;
140
+ /** Review-queue snapshot for the review role's inline prev/next/counts. */
141
+ reviewQueue?: ReviewQueueSnapshot;
142
+ /** Active work item for the workflow role's inline queue. */
143
+ workflowItem?: WorkflowWorkItemSnapshot | null;
144
+ /** Markup display mode for the review role. */
145
+ markupDisplay?: MarkupDisplayMode;
146
+
147
+ // Editor role
148
+ onMarkScopePosture?: (posture: ScopeRailPosture) => void;
149
+ // Review role
150
+ onReviewPrev?: () => void;
151
+ onReviewNext?: () => void;
152
+ onReviewAccept?: () => void;
153
+ onReviewReject?: () => void;
154
+ onReviewAcceptAll?: () => void;
155
+ onReviewRejectAll?: () => void;
156
+ onReviewMarkupMode?: (mode: MarkupDisplayMode) => void;
157
+ // Workflow role
158
+ onWorkflowPrev?: () => void;
159
+ onWorkflowNext?: () => void;
160
+ onWorkflowMarkComplete?: () => void;
161
+ onWorkflowClaim?: () => void;
162
+ onWorkflowSkip?: () => void;
163
+ onWorkflowMarkBlocked?: () => void;
164
+ onWorkflowJumpToScope?: () => void;
124
165
  }
125
166
 
126
167
  export interface ToolbarInteractionPolicy {
@@ -170,6 +211,7 @@ export function TwToolbar(props: TwToolbarProps) {
170
211
  } as any)
171
212
  : undefined,
172
213
  activeListContext: props.activeListContext,
214
+ ...(props.role ? { role: props.role } : {}),
173
215
  });
174
216
  const showStyleSelectors = isToolbarChromeItemVisible(scopedChromePolicy, "text-style-selectors");
175
217
  const showInlineFormatting = isToolbarChromeItemVisible(scopedChromePolicy, "inline-formatting");
@@ -476,6 +518,33 @@ export function TwToolbar(props: TwToolbarProps) {
476
518
  ) : null}
477
519
  </div>
478
520
 
521
+ {/* R1: role-scoped inline action region (spec §6.4) */}
522
+ {props.role ? (
523
+ <TwRoleActionRegion
524
+ role={props.role}
525
+ policy={scopedChromePolicy}
526
+ compactMode={isCompact}
527
+ reviewQueue={props.reviewQueue}
528
+ workflowItem={props.workflowItem}
529
+ markupDisplay={props.markupDisplay}
530
+ onMarkScopePosture={props.onMarkScopePosture}
531
+ onReviewPrev={props.onReviewPrev}
532
+ onReviewNext={props.onReviewNext}
533
+ onReviewAccept={props.onReviewAccept}
534
+ onReviewReject={props.onReviewReject}
535
+ onReviewAcceptAll={props.onReviewAcceptAll}
536
+ onReviewRejectAll={props.onReviewRejectAll}
537
+ onReviewMarkupMode={props.onReviewMarkupMode}
538
+ onWorkflowPrev={props.onWorkflowPrev}
539
+ onWorkflowNext={props.onWorkflowNext}
540
+ onWorkflowMarkComplete={props.onWorkflowMarkComplete}
541
+ onWorkflowClaim={props.onWorkflowClaim}
542
+ onWorkflowSkip={props.onWorkflowSkip}
543
+ onWorkflowMarkBlocked={props.onWorkflowMarkBlocked}
544
+ onWorkflowJumpToScope={props.onWorkflowJumpToScope}
545
+ />
546
+ ) : null}
547
+
479
548
  {/* Right cluster: comment, track changes, markup, view, export */}
480
549
  <div className={`flex items-center gap-0.5 ${isCompact ? "ml-auto flex-wrap justify-end" : ""}`}>
481
550
  {scopedChromePolicy.scopeStatusLabel ? (
@@ -82,6 +82,7 @@ import { TwSelectionToolHost } from "./chrome/tw-selection-tool-host";
82
82
  import { TwReviewRail, type ReviewRailTab } from "./review/tw-review-rail";
83
83
  import { TwStatusBar } from "./status/tw-status-bar";
84
84
  import { type ToolbarInteractionPolicy } from "./toolbar/tw-toolbar";
85
+ import { TwChromeOverlay } from "./chrome-overlay";
85
86
 
86
87
  export type ReviewWorkspaceChromeVisibility = WordReviewEditorChromeVisibility;
87
88
 
@@ -92,6 +93,38 @@ export interface TwReviewWorkspaceProps {
92
93
  currentUserId?: string;
93
94
  capabilities?: SessionCapabilities;
94
95
  reviewMode?: "editing" | "review";
96
+ /**
97
+ * Runtime-owned layout facet. Optional so existing tests + host apps
98
+ * continue to mount the workspace without installing a facet. When
99
+ * supplied, the ChromeOverlay plane (scope rail, workflow dock, etc.)
100
+ * renders over the document column.
101
+ */
102
+ layoutFacet?: import("../runtime/layout/index.ts").WordReviewEditorLayoutFacet;
103
+ /**
104
+ * Optional shell header mounted above the formatting toolbar. Pass a
105
+ * pre-assembled `<TwShellHeader />` with brand / mode switcher /
106
+ * primaryAction, or any other ReactNode. Hosts that do not supply this
107
+ * get the legacy layout.
108
+ */
109
+ shellHeader?: ReactNode;
110
+ /**
111
+ * Optional host-provided Workflow-tab override for the review rail.
112
+ * When unset the rail renders the built-in `TwWorkflowTab` sourced from
113
+ * `layoutFacet.getAllScopeRailSegments()`.
114
+ */
115
+ reviewRailWorkflowTab?: ReactNode;
116
+ reviewRailWorkflowCount?: number;
117
+ reviewRailWorkflowScopesTitle?: string;
118
+ reviewRailIntelligenceEyebrow?: string;
119
+ /** Opt in to the editorial DOCUMENT INTELLIGENCE header + underline tab chip. */
120
+ reviewRailIntelligenceHeader?: boolean;
121
+ /** Optional SEARCH / HELP utility footer at the bottom of the rail. */
122
+ reviewRailFooter?: {
123
+ onSearch?: () => void;
124
+ helpHref?: string;
125
+ searchLabel?: string;
126
+ helpLabel?: string;
127
+ };
95
128
  document: ReactNode;
96
129
  workspaceMode: WorkspaceMode;
97
130
  zoomLevel?: ZoomLevel;
@@ -114,6 +147,17 @@ export interface TwReviewWorkspaceProps {
114
147
  activeSelectionTool?: ActiveSelectionToolModel | null;
115
148
  selectionToolAnchor?: SelectionToolAnchor | null;
116
149
  documentNavigation?: DocumentNavigationSnapshot;
150
+ /**
151
+ * R2.3: chrome-pin change handler. When supplied, selection tools
152
+ * expose their detach affordance and persist pin state through to
153
+ * runtime ViewState (via the host's `setChromePin` action). When
154
+ * omitted, the detach handle is suppressed — the tool behaves as
155
+ * a non-pinnable anchored panel (pre-R2 behavior for most kinds).
156
+ */
157
+ onChromePinChange?: (
158
+ surface: import("../api/public-types").ChromePinSurface,
159
+ pin: import("../api/public-types").PinState | null,
160
+ ) => void;
117
161
  onWorkspaceModeChange?: (value: WorkspaceMode) => void;
118
162
  onZoomChange?: (level: ZoomLevel) => void;
119
163
  onActiveRailTabChange?: (value: ReviewRailTab) => void;
@@ -451,6 +495,7 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
451
495
  return (
452
496
  <Tooltip.Provider delayDuration={400}>
453
497
  <div className="flex h-full flex-col bg-canvas text-primary">
498
+ {props.shellHeader}
454
499
  {chromeVisibility.toolbar ? (
455
500
  <div className="px-3 pt-3">
456
501
  <ChromePresetToolbar
@@ -561,11 +606,50 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
561
606
  dismissSelectionToolbar();
562
607
  props.onShowTrackedChangesChange(show);
563
608
  }}
609
+ role={viewState.editorRole}
610
+ reviewQueue={props.reviewQueue}
611
+ markupDisplay={markupDisplay}
612
+ onMarkScopePosture={props.onMarkSectionForReview
613
+ ? runWithSelectionToolbarDismiss(props.onMarkSectionForReview)
614
+ : undefined}
615
+ onReviewPrev={props.onGoToPreviousReviewItem
616
+ ? runWithSelectionToolbarDismiss(props.onGoToPreviousReviewItem)
617
+ : undefined}
618
+ onReviewNext={props.onGoToNextReviewItem
619
+ ? runWithSelectionToolbarDismiss(props.onGoToNextReviewItem)
620
+ : undefined}
621
+ onReviewAccept={(() => {
622
+ const active = props.reviewQueue?.items[props.reviewQueue.activeIndex];
623
+ if (active?.kind !== "change" || !props.onAcceptRevision) {
624
+ return undefined;
625
+ }
626
+ // ReviewQueueItem.itemId for a "change" entry is the
627
+ // revision id (set by the runtime review-queue projection).
628
+ const revisionId = active.itemId;
629
+ return () => {
630
+ dismissSelectionToolbar();
631
+ props.onAcceptRevision?.(revisionId);
632
+ };
633
+ })()}
634
+ onReviewReject={(() => {
635
+ const active = props.reviewQueue?.items[props.reviewQueue.activeIndex];
636
+ if (active?.kind !== "change" || !props.onRejectRevision) {
637
+ return undefined;
638
+ }
639
+ const revisionId = active.itemId;
640
+ return () => {
641
+ dismissSelectionToolbar();
642
+ props.onRejectRevision?.(revisionId);
643
+ };
644
+ })()}
564
645
  />
565
646
  </div>
566
647
  ) : null}
567
648
 
568
- {chromePreset === "review" && chromeOptions.showReviewQueueBar && props.reviewQueue ? (
649
+ {viewState.editorRole !== "review" &&
650
+ chromePreset === "review" &&
651
+ chromeOptions.showReviewQueueBar &&
652
+ props.reviewQueue ? (
569
653
  <TwReviewQueueBar
570
654
  queue={props.reviewQueue}
571
655
  onPrevious={props.onGoToPreviousReviewItem
@@ -883,6 +967,8 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
883
967
  onSetImageFrame={props.onSetImageFrame}
884
968
  onRestartNumbering={props.onRestartNumbering}
885
969
  onContinueNumbering={props.onContinueNumbering}
970
+ chromePins={viewState.chromePins}
971
+ onChromePinChange={props.onChromePinChange}
886
972
  />
887
973
  ) : null}
888
974
  <div
@@ -951,6 +1037,15 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
951
1037
  ) : null}
952
1038
  <div className={isPageWorkspace ? "relative z-10" : undefined}>
953
1039
  {props.document}
1040
+ {props.layoutFacet ? (
1041
+ <TwChromeOverlay
1042
+ facet={props.layoutFacet}
1043
+ activeWorkspaceView={
1044
+ props.reviewMode === "review" ? "review" : "workflow"
1045
+ }
1046
+ showWorkspaceDock={chromeVisibility.pageChrome}
1047
+ />
1048
+ ) : null}
954
1049
  </div>
955
1050
  {isPageWorkspace && chromeVisibility.pageChrome ? (
956
1051
  <div
@@ -975,6 +1070,32 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
975
1070
  </div>
976
1071
  </div>
977
1072
  </div>
1073
+ {isPageWorkspace && (props.documentNavigation?.pages.length ?? 0) > 1 ? (
1074
+ <div className="flex flex-col items-center gap-8 pb-8" data-testid="page-stack-continuation">
1075
+ {props.documentNavigation!.pages.slice(1).map((page) => (
1076
+ <div
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}
978
1099
  </div>
979
1100
 
980
1101
  {chromeVisibility.statusBar ? (
@@ -1021,6 +1142,13 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
1021
1142
  onRejectRevision={props.onRejectRevision}
1022
1143
  onAcceptAllChanges={props.onAcceptAllChanges}
1023
1144
  onRejectAllChanges={props.onRejectAllChanges}
1145
+ scopeRailSegments={props.layoutFacet?.getAllScopeRailSegments?.() ?? []}
1146
+ workflowTab={props.reviewRailWorkflowTab}
1147
+ workflowCount={props.reviewRailWorkflowCount}
1148
+ workflowScopesTitle={props.reviewRailWorkflowScopesTitle}
1149
+ intelligenceEyebrow={props.reviewRailIntelligenceEyebrow}
1150
+ intelligenceHeader={props.reviewRailIntelligenceHeader}
1151
+ railFooter={props.reviewRailFooter}
1024
1152
  /> : null}
1025
1153
 
1026
1154
  {responsiveChrome.showDrawerReviewRail ? (
@@ -1062,6 +1190,13 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
1062
1190
  onRejectRevision={props.onRejectRevision}
1063
1191
  onAcceptAllChanges={props.onAcceptAllChanges}
1064
1192
  onRejectAllChanges={props.onRejectAllChanges}
1193
+ scopeRailSegments={props.layoutFacet?.getAllScopeRailSegments?.() ?? []}
1194
+ workflowTab={props.reviewRailWorkflowTab}
1195
+ workflowCount={props.reviewRailWorkflowCount}
1196
+ workflowScopesTitle={props.reviewRailWorkflowScopesTitle}
1197
+ intelligenceEyebrow={props.reviewRailIntelligenceEyebrow}
1198
+ intelligenceHeader={props.reviewRailIntelligenceHeader}
1199
+ railFooter={props.reviewRailFooter}
1065
1200
  />
1066
1201
  </div>
1067
1202
  </div>