@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.
- package/package.json +1 -1
- package/src/api/public-types.ts +319 -1
- package/src/core/commands/section-layout-commands.ts +58 -0
- package/src/core/commands/table-grid.ts +431 -0
- package/src/core/commands/table-structure-commands.ts +815 -55
- package/src/io/export/serialize-main-document.ts +2 -11
- package/src/io/export/serialize-numbering.ts +1 -2
- package/src/io/export/serialize-tables.ts +74 -0
- package/src/io/export/table-properties-xml.ts +139 -4
- package/src/io/normalize/normalize-text.ts +15 -0
- package/src/io/ooxml/parse-footnotes.ts +60 -0
- package/src/io/ooxml/parse-headers-footers.ts +60 -0
- package/src/io/ooxml/parse-main-document.ts +137 -0
- package/src/io/ooxml/parse-tables.ts +249 -0
- package/src/model/canonical-document.ts +34 -0
- package/src/runtime/document-layout.ts +4 -2
- package/src/runtime/document-navigation.ts +1 -1
- package/src/runtime/document-runtime.ts +114 -0
- package/src/runtime/layout/default-page-format.ts +96 -0
- package/src/runtime/layout/index.ts +45 -0
- package/src/runtime/layout/inert-layout-facet.ts +14 -0
- package/src/runtime/layout/layout-engine-instance.ts +33 -23
- package/src/runtime/layout/margin-preset-catalog.ts +178 -0
- package/src/runtime/layout/page-format-catalog.ts +233 -0
- package/src/runtime/layout/page-graph.ts +19 -0
- package/src/runtime/layout/paginated-layout-engine.ts +142 -9
- package/src/runtime/layout/project-block-fragments.ts +91 -0
- package/src/runtime/layout/public-facet.ts +709 -16
- package/src/runtime/layout/table-render-plan.ts +229 -0
- package/src/runtime/render/block-fragment-projection.ts +35 -0
- package/src/runtime/render/decoration-resolver.ts +189 -0
- package/src/runtime/render/index.ts +57 -0
- package/src/runtime/render/pending-op-delta-reader.ts +129 -0
- package/src/runtime/render/render-frame-types.ts +317 -0
- package/src/runtime/render/render-kernel.ts +755 -0
- package/src/runtime/view-state.ts +67 -0
- package/src/runtime/workflow-markup.ts +1 -5
- package/src/runtime/workflow-rail-segments.ts +280 -0
- package/src/ui/WordReviewEditor.tsx +84 -15
- package/src/ui/editor-shell-view.tsx +6 -0
- package/src/ui/headless/chrome-registry.ts +280 -14
- package/src/ui/headless/scoped-chrome-policy.ts +20 -1
- package/src/ui/headless/selection-tool-types.ts +10 -0
- package/src/ui-tailwind/chrome/chrome-preset-model.ts +23 -2
- package/src/ui-tailwind/chrome/role-action-sets.ts +74 -0
- package/src/ui-tailwind/chrome/tw-detach-handle.tsx +147 -0
- package/src/ui-tailwind/chrome/tw-selection-anchor-resolver.ts +163 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +57 -92
- package/src/ui-tailwind/chrome/tw-selection-tool-placement.ts +149 -0
- package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +15 -4
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +274 -138
- package/src/ui-tailwind/chrome-overlay/chrome-overlay-projector.ts +90 -0
- package/src/ui-tailwind/chrome-overlay/index.ts +22 -0
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +86 -0
- package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +178 -0
- package/src/ui-tailwind/chrome-overlay/tw-workspace-view-switcher.tsx +95 -0
- package/src/ui-tailwind/editor-surface/fast-text-edit-lane.ts +4 -0
- package/src/ui-tailwind/editor-surface/local-edit-session-state.ts +11 -0
- package/src/ui-tailwind/editor-surface/perf-probe.ts +7 -1
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +12 -1
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +22 -12
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +3 -0
- package/src/ui-tailwind/index.ts +33 -0
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +2 -2
- package/src/ui-tailwind/review/tw-rail-card.tsx +150 -0
- package/src/ui-tailwind/review/tw-review-rail-footer.tsx +52 -0
- package/src/ui-tailwind/review/tw-review-rail.tsx +166 -11
- package/src/ui-tailwind/review/tw-workflow-tab.tsx +108 -0
- package/src/ui-tailwind/theme/editor-theme.css +498 -163
- package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +559 -0
- package/src/ui-tailwind/toolbar/tw-scope-posture-menu.tsx +182 -0
- package/src/ui-tailwind/toolbar/tw-shell-header.tsx +162 -0
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +69 -0
- 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
|
-
{
|
|
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>
|