@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.
- package/package.json +41 -31
- package/src/api/public-types.ts +496 -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 +845 -56
- package/src/core/commands/text-commands.ts +122 -2
- package/src/io/docx-session.ts +1 -0
- package/src/io/export/serialize-main-document.ts +2 -11
- package/src/io/export/serialize-numbering.ts +43 -10
- 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/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-fields.ts +10 -3
- 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-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/parse-tables.ts +249 -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 +117 -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-layout.ts +4 -2
- package/src/runtime/document-navigation.ts +1 -1
- package/src/runtime/document-runtime.ts +248 -18
- package/src/runtime/layout/default-page-format.ts +96 -0
- package/src/runtime/layout/index.ts +47 -0
- package/src/runtime/layout/inert-layout-facet.ts +16 -0
- package/src/runtime/layout/layout-engine-instance.ts +100 -23
- package/src/runtime/layout/layout-invalidation.ts +14 -5
- 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 +55 -0
- package/src/runtime/layout/paginate-paragraph-lines.ts +128 -0
- package/src/runtime/layout/paginated-layout-engine.ts +484 -37
- package/src/runtime/layout/project-block-fragments.ts +225 -0
- package/src/runtime/layout/public-facet.ts +748 -16
- 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 +249 -0
- package/src/runtime/numbering-prefix.ts +5 -0
- package/src/runtime/paragraph-style-resolver.ts +194 -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 +759 -0
- 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/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 +368 -19
- 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 +10 -0
- package/src/ui/editor-surface-controller.tsx +9 -1
- package/src/ui/headless/chrome-registry.ts +310 -15
- package/src/ui/headless/scoped-chrome-policy.ts +49 -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/review-queue-bar.tsx +2 -14
- package/src/ui-tailwind/chrome/role-action-sets.ts +80 -0
- package/src/ui-tailwind/chrome/tw-detach-handle.tsx +147 -0
- package/src/ui-tailwind/chrome/tw-selection-anchor-resolver.ts +160 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +68 -92
- package/src/ui-tailwind/chrome/tw-selection-tool-placement.ts +149 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-structure.tsx +11 -0
- package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +15 -4
- package/src/ui-tailwind/chrome/tw-table-border-picker.tsx +245 -0
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +356 -140
- package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +284 -0
- package/src/ui-tailwind/chrome-overlay/chrome-overlay-projector.ts +94 -0
- package/src/ui-tailwind/chrome-overlay/index.ts +16 -0
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +96 -0
- package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +178 -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/page-slice-util.ts +15 -0
- package/src/ui-tailwind/editor-surface/perf-probe.ts +7 -1
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +40 -4
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +22 -12
- package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +389 -0
- package/src/ui-tailwind/editor-surface/pm-schema.ts +40 -2
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +144 -62
- package/src/ui-tailwind/editor-surface/remote-cursor-plugin.ts +179 -0
- package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +559 -0
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +224 -75
- 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 +29 -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 +680 -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 +104 -2
- package/src/ui-tailwind/tw-review-workspace.tsx +234 -21
- package/src/runtime/collab-review-sync.ts +0 -254
- 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 =
|
|
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
|
-
{
|
|
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
|
);
|