@beyondwork/docx-react-component 1.0.37 → 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.
Files changed (116) hide show
  1. package/package.json +41 -31
  2. package/src/api/public-types.ts +496 -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 +845 -56
  6. package/src/core/commands/text-commands.ts +122 -2
  7. package/src/io/docx-session.ts +1 -0
  8. package/src/io/export/serialize-main-document.ts +2 -11
  9. package/src/io/export/serialize-numbering.ts +43 -10
  10. package/src/io/export/serialize-paragraph-formatting.ts +152 -0
  11. package/src/io/export/serialize-run-formatting.ts +90 -0
  12. package/src/io/export/serialize-styles.ts +212 -0
  13. package/src/io/export/serialize-tables.ts +74 -0
  14. package/src/io/export/table-properties-xml.ts +139 -4
  15. package/src/io/normalize/normalize-text.ts +15 -0
  16. package/src/io/ooxml/parse-fields.ts +10 -3
  17. package/src/io/ooxml/parse-footnotes.ts +60 -0
  18. package/src/io/ooxml/parse-headers-footers.ts +60 -0
  19. package/src/io/ooxml/parse-main-document.ts +137 -0
  20. package/src/io/ooxml/parse-numbering.ts +41 -1
  21. package/src/io/ooxml/parse-paragraph-formatting.ts +188 -0
  22. package/src/io/ooxml/parse-run-formatting.ts +129 -0
  23. package/src/io/ooxml/parse-styles.ts +31 -0
  24. package/src/io/ooxml/parse-tables.ts +249 -0
  25. package/src/io/ooxml/xml-attr-helpers.ts +60 -0
  26. package/src/io/ooxml/xml-element.ts +19 -0
  27. package/src/model/canonical-document.ts +117 -3
  28. package/src/runtime/collab/event-types.ts +165 -0
  29. package/src/runtime/collab/index.ts +22 -0
  30. package/src/runtime/collab/remote-cursor-awareness.ts +93 -0
  31. package/src/runtime/collab/runtime-collab-sync.ts +273 -0
  32. package/src/runtime/document-layout.ts +4 -2
  33. package/src/runtime/document-navigation.ts +1 -1
  34. package/src/runtime/document-runtime.ts +248 -18
  35. package/src/runtime/layout/default-page-format.ts +96 -0
  36. package/src/runtime/layout/index.ts +47 -0
  37. package/src/runtime/layout/inert-layout-facet.ts +16 -0
  38. package/src/runtime/layout/layout-engine-instance.ts +100 -23
  39. package/src/runtime/layout/layout-invalidation.ts +14 -5
  40. package/src/runtime/layout/margin-preset-catalog.ts +178 -0
  41. package/src/runtime/layout/page-format-catalog.ts +233 -0
  42. package/src/runtime/layout/page-graph.ts +55 -0
  43. package/src/runtime/layout/paginate-paragraph-lines.ts +128 -0
  44. package/src/runtime/layout/paginated-layout-engine.ts +484 -37
  45. package/src/runtime/layout/project-block-fragments.ts +225 -0
  46. package/src/runtime/layout/public-facet.ts +748 -16
  47. package/src/runtime/layout/resolve-page-fields.ts +70 -0
  48. package/src/runtime/layout/resolve-page-previews.ts +185 -0
  49. package/src/runtime/layout/resolved-formatting-state.ts +30 -26
  50. package/src/runtime/layout/table-render-plan.ts +249 -0
  51. package/src/runtime/numbering-prefix.ts +5 -0
  52. package/src/runtime/paragraph-style-resolver.ts +194 -0
  53. package/src/runtime/render/block-fragment-projection.ts +35 -0
  54. package/src/runtime/render/decoration-resolver.ts +189 -0
  55. package/src/runtime/render/index.ts +57 -0
  56. package/src/runtime/render/pending-op-delta-reader.ts +129 -0
  57. package/src/runtime/render/render-frame-types.ts +317 -0
  58. package/src/runtime/render/render-kernel.ts +759 -0
  59. package/src/runtime/resolved-numbering-geometry.ts +9 -1
  60. package/src/runtime/surface-projection.ts +129 -9
  61. package/src/runtime/table-schema.ts +11 -0
  62. package/src/runtime/view-state.ts +67 -0
  63. package/src/runtime/workflow-markup.ts +1 -5
  64. package/src/runtime/workflow-rail-segments.ts +280 -0
  65. package/src/ui/WordReviewEditor.tsx +368 -19
  66. package/src/ui/editor-command-bag.ts +4 -0
  67. package/src/ui/editor-runtime-boundary.ts +16 -0
  68. package/src/ui/editor-shell-view.tsx +10 -0
  69. package/src/ui/editor-surface-controller.tsx +9 -1
  70. package/src/ui/headless/chrome-registry.ts +310 -15
  71. package/src/ui/headless/scoped-chrome-policy.ts +49 -1
  72. package/src/ui/headless/selection-tool-types.ts +10 -0
  73. package/src/ui-tailwind/chrome/chrome-preset-model.ts +23 -2
  74. package/src/ui-tailwind/chrome/review-queue-bar.tsx +2 -14
  75. package/src/ui-tailwind/chrome/role-action-sets.ts +80 -0
  76. package/src/ui-tailwind/chrome/tw-detach-handle.tsx +147 -0
  77. package/src/ui-tailwind/chrome/tw-selection-anchor-resolver.ts +160 -0
  78. package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +68 -92
  79. package/src/ui-tailwind/chrome/tw-selection-tool-placement.ts +149 -0
  80. package/src/ui-tailwind/chrome/tw-selection-tool-structure.tsx +11 -0
  81. package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +15 -4
  82. package/src/ui-tailwind/chrome/tw-table-border-picker.tsx +245 -0
  83. package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +356 -140
  84. package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +284 -0
  85. package/src/ui-tailwind/chrome-overlay/chrome-overlay-projector.ts +94 -0
  86. package/src/ui-tailwind/chrome-overlay/index.ts +16 -0
  87. package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +96 -0
  88. package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +178 -0
  89. package/src/ui-tailwind/editor-surface/fast-text-edit-lane.ts +4 -0
  90. package/src/ui-tailwind/editor-surface/local-edit-session-state.ts +11 -0
  91. package/src/ui-tailwind/editor-surface/page-slice-util.ts +15 -0
  92. package/src/ui-tailwind/editor-surface/perf-probe.ts +7 -1
  93. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +40 -4
  94. package/src/ui-tailwind/editor-surface/pm-decorations.ts +22 -12
  95. package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +389 -0
  96. package/src/ui-tailwind/editor-surface/pm-schema.ts +40 -2
  97. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +144 -62
  98. package/src/ui-tailwind/editor-surface/remote-cursor-plugin.ts +179 -0
  99. package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +559 -0
  100. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +224 -75
  101. package/src/ui-tailwind/editor-surface/tw-table-bands.css +61 -0
  102. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +19 -0
  103. package/src/ui-tailwind/index.ts +29 -0
  104. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +2 -2
  105. package/src/ui-tailwind/review/tw-rail-card.tsx +150 -0
  106. package/src/ui-tailwind/review/tw-review-rail-footer.tsx +52 -0
  107. package/src/ui-tailwind/review/tw-review-rail.tsx +166 -11
  108. package/src/ui-tailwind/review/tw-workflow-tab.tsx +108 -0
  109. package/src/ui-tailwind/theme/editor-theme.css +498 -163
  110. package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +680 -0
  111. package/src/ui-tailwind/toolbar/tw-scope-posture-menu.tsx +182 -0
  112. package/src/ui-tailwind/toolbar/tw-shell-header.tsx +162 -0
  113. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +104 -2
  114. package/src/ui-tailwind/tw-review-workspace.tsx +234 -21
  115. package/src/runtime/collab-review-sync.ts +0 -254
  116. package/src/ui-tailwind/editor-surface/pm-collab-plugins.ts +0 -40
@@ -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
+ }
@@ -42,12 +42,18 @@ import {
42
42
 
43
43
  import type {
44
44
  ActiveListContext,
45
+ ChromePinSurface,
46
+ ChromePinsState,
45
47
  CompatibilityPanelSnapshot,
48
+ EditorRole,
46
49
  EditorStoryTarget,
47
50
  EditorWarning,
48
51
  FormattingStateSnapshot,
49
52
  FormattingAlignment,
50
53
  InsertImageOptions,
54
+ PinState,
55
+ ReviewQueueSnapshot,
56
+ ScopeRailPosture,
51
57
  SectionBreakType,
52
58
  StyleCatalogSnapshot,
53
59
  WorkflowBlockedCommandReason,
@@ -58,12 +64,19 @@ import type {
58
64
  import type { SessionCapabilities } from "../../runtime/session-capabilities";
59
65
  import {
60
66
  getToolbarChromePlacement,
67
+ isChromeItemOwnedByRoleRegion,
61
68
  isToolbarChromeItemVisible,
62
69
  resolveScopedChromePolicy,
63
70
  type ScopedChromePolicy,
64
71
  } from "../../ui/headless/scoped-chrome-policy";
65
72
  import { preserveEditorSelectionMouseDown } from "../../ui/headless/preserve-editor-selection";
66
73
  import { TwHealthPanel } from "../review/tw-health-panel";
74
+ import {
75
+ TwRoleActionRegion,
76
+ type MarkupDisplayMode,
77
+ type WorkflowWorkItemSnapshot,
78
+ } from "./tw-role-action-region";
79
+ import { TwDetachHandle } from "../chrome/tw-detach-handle";
67
80
  import { TwToolbarIconButton } from "./tw-toolbar-icon-button";
68
81
 
69
82
  export interface TwToolbarProps {
@@ -121,6 +134,46 @@ export interface TwToolbarProps {
121
134
  onContinueNumbering?: () => void;
122
135
  onUpdateFields?: () => void;
123
136
  onUpdateTableOfContents?: () => void;
137
+
138
+ // ───── R1: role-scoped inline action region (spec §6.4) ──────────────
139
+ /**
140
+ * Active editor role. When supplied, the toolbar renders the inline
141
+ * `TwRoleActionRegion` between the left formatting cluster and the
142
+ * right view cluster. Omit to keep the pre-R1 layout.
143
+ */
144
+ role?: EditorRole;
145
+ /** Review-queue snapshot for the review role's inline prev/next/counts. */
146
+ reviewQueue?: ReviewQueueSnapshot;
147
+ /** Active work item for the workflow role's inline queue. */
148
+ workflowItem?: WorkflowWorkItemSnapshot | null;
149
+ /** Markup display mode for the review role. */
150
+ markupDisplay?: MarkupDisplayMode;
151
+
152
+ // Shared: editor + review role (comment + TC in role region)
153
+ onReviewSidebarTrackedChanges?: () => void;
154
+ onReviewSidebarComments?: () => void;
155
+ // Workflow + review role: scope posture
156
+ onMarkScopePosture?: (posture: ScopeRailPosture) => void;
157
+ // Review role
158
+ onReviewPrev?: () => void;
159
+ onReviewNext?: () => void;
160
+ onReviewAccept?: () => void;
161
+ onReviewReject?: () => void;
162
+ onReviewAcceptAll?: () => void;
163
+ onReviewRejectAll?: () => void;
164
+ onReviewMarkupMode?: (mode: MarkupDisplayMode) => void;
165
+ // Workflow role
166
+ onWorkflowPrev?: () => void;
167
+ onWorkflowNext?: () => void;
168
+ onWorkflowMarkComplete?: () => void;
169
+ onWorkflowClaim?: () => void;
170
+ onWorkflowSkip?: () => void;
171
+ onWorkflowMarkBlocked?: () => void;
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;
124
177
  }
125
178
 
126
179
  export interface ToolbarInteractionPolicy {
@@ -170,6 +223,7 @@ export function TwToolbar(props: TwToolbarProps) {
170
223
  } as any)
171
224
  : undefined,
172
225
  activeListContext: props.activeListContext,
226
+ ...(props.role ? { role: props.role } : {}),
173
227
  });
174
228
  const showStyleSelectors = isToolbarChromeItemVisible(scopedChromePolicy, "text-style-selectors");
175
229
  const showInlineFormatting = isToolbarChromeItemVisible(scopedChromePolicy, "inline-formatting");
@@ -177,7 +231,12 @@ export function TwToolbar(props: TwToolbarProps) {
177
231
  const showTextColors = isToolbarChromeItemVisible(scopedChromePolicy, "text-colors");
178
232
  const showParagraphAlignment = isToolbarChromeItemVisible(scopedChromePolicy, "paragraph-alignment");
179
233
  const showInsertMenu = isToolbarChromeItemVisible(scopedChromePolicy, "insert-actions");
180
- const showTrackedChangesToggle = isToolbarChromeItemVisible(scopedChromePolicy, "tracked-changes-toggle");
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);
181
240
  const showHealth =
182
241
  showDiagnosticsChrome &&
183
242
  isToolbarChromeItemVisible(scopedChromePolicy, "health") &&
@@ -476,6 +535,40 @@ export function TwToolbar(props: TwToolbarProps) {
476
535
  ) : null}
477
536
  </div>
478
537
 
538
+ {/* R1: role-scoped inline action region (spec §6.4) */}
539
+ {props.role ? (
540
+ <TwRoleActionRegion
541
+ role={props.role}
542
+ policy={scopedChromePolicy}
543
+ compactMode={isCompact}
544
+ reviewQueue={props.reviewQueue}
545
+ workflowItem={props.workflowItem}
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}
554
+ onMarkScopePosture={props.onMarkScopePosture}
555
+ onReviewPrev={props.onReviewPrev}
556
+ onReviewNext={props.onReviewNext}
557
+ onReviewAccept={props.onReviewAccept}
558
+ onReviewReject={props.onReviewReject}
559
+ onReviewAcceptAll={props.onReviewAcceptAll}
560
+ onReviewRejectAll={props.onReviewRejectAll}
561
+ onReviewMarkupMode={props.onReviewMarkupMode}
562
+ onWorkflowPrev={props.onWorkflowPrev}
563
+ onWorkflowNext={props.onWorkflowNext}
564
+ onWorkflowMarkComplete={props.onWorkflowMarkComplete}
565
+ onWorkflowClaim={props.onWorkflowClaim}
566
+ onWorkflowSkip={props.onWorkflowSkip}
567
+ onWorkflowMarkBlocked={props.onWorkflowMarkBlocked}
568
+ onWorkflowJumpToScope={props.onWorkflowJumpToScope}
569
+ />
570
+ ) : null}
571
+
479
572
  {/* Right cluster: comment, track changes, markup, view, export */}
480
573
  <div className={`flex items-center gap-0.5 ${isCompact ? "ml-auto flex-wrap justify-end" : ""}`}>
481
574
  {scopedChromePolicy.scopeStatusLabel ? (
@@ -498,7 +591,7 @@ export function TwToolbar(props: TwToolbarProps) {
498
591
  </>
499
592
  ) : null}
500
593
 
501
- {isToolbarChromeItemVisible(scopedChromePolicy, "comment") ? (
594
+ {showRightClusterComment ? (
502
595
  <TwToolbarIconButton
503
596
  icon={MessageSquare}
504
597
  label="Add comment"
@@ -740,6 +833,15 @@ export function TwToolbar(props: TwToolbarProps) {
740
833
  onClick={props.onExport}
741
834
  />
742
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}
743
845
  </div>
744
846
  </header>
745
847
  );