@beyondwork/docx-react-component 1.0.85 → 1.0.87
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 +49 -0
- package/src/api/v3/ui/chrome-composition.ts +2 -11
- package/src/api/v3/ui/chrome.ts +6 -8
- package/src/index.ts +5 -0
- package/src/io/export/serialize-main-document.ts +215 -6
- package/src/io/ooxml/parse-drawing.ts +15 -1
- package/src/io/ooxml/parse-fields.ts +410 -12
- package/src/model/canonical-document.ts +177 -2
- package/src/model/layout/page-layout-snapshot.ts +2 -0
- package/src/model/layout/runtime-page-graph-types.ts +6 -0
- package/src/preservation/store.ts +4 -5
- package/src/runtime/document-outline.ts +80 -0
- package/src/runtime/document-runtime.ts +338 -13
- package/src/runtime/formatting/field/page-number-format.ts +49 -0
- package/src/runtime/formatting/field/resolver.ts +61 -40
- package/src/runtime/layout/layout-engine-instance.ts +18 -1
- package/src/runtime/layout/layout-engine-version.ts +19 -1
- package/src/runtime/layout/measurement-backend-canvas.ts +21 -9
- package/src/runtime/layout/measurement-backend-empirical.ts +18 -4
- package/src/runtime/layout/page-graph.ts +13 -2
- package/src/runtime/layout/paginated-layout-engine.ts +440 -117
- package/src/runtime/layout/project-block-fragments.ts +87 -4
- package/src/runtime/layout/resolve-page-fields.ts +8 -5
- package/src/runtime/layout/table-row-split.ts +97 -23
- package/src/runtime/surface-projection.ts +227 -27
- package/src/shell/session-bootstrap.ts +6 -1
- package/src/ui/WordReviewEditor.tsx +112 -33
- package/src/ui/editor-command-bag.ts +4 -0
- package/src/ui/editor-shell-view.tsx +1 -0
- package/src/ui/editor-surface-controller.tsx +1 -0
- package/src/ui/headless/revision-decoration-model.ts +11 -13
- package/src/ui-tailwind/chrome/editor-action-registry.ts +7 -26
- package/src/ui-tailwind/chrome/responsive-chrome.ts +2 -2
- package/src/ui-tailwind/chrome/tw-alert-banner.tsx +27 -0
- package/src/ui-tailwind/chrome/tw-detach-handle.tsx +6 -2
- package/src/ui-tailwind/editor-surface/fast-text-edit-lane.ts +57 -6
- package/src/ui-tailwind/editor-surface/page-slice-util.ts +17 -0
- package/src/ui-tailwind/editor-surface/perf-probe.ts +5 -0
- package/src/ui-tailwind/editor-surface/pm-contextual-ui.ts +34 -0
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +146 -20
- package/src/ui-tailwind/editor-surface/pm-position-map.ts +8 -2
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +41 -44
- package/src/ui-tailwind/page-stack/use-visible-block-range.ts +1 -1
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +8 -3
- package/src/ui-tailwind/review/tw-review-rail.tsx +2 -0
- package/src/ui-tailwind/review/tw-revision-sidebar.tsx +75 -31
- package/src/ui-tailwind/review/tw-workflow-tab.tsx +159 -8
- package/src/ui-tailwind/review-workspace/types.ts +4 -0
- package/src/ui-tailwind/review-workspace/use-review-rail-state.ts +1 -1
- package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +8 -10
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +21 -17
- package/src/ui-tailwind/tw-review-workspace.tsx +27 -2
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import { Check, X } from "lucide-react";
|
|
2
|
+
import { Check, MessageSquare, X } from "lucide-react";
|
|
3
3
|
|
|
4
4
|
import type { TrackedChangesSnapshot, TrackedChangeEntrySnapshot } from "../../api/public-types";
|
|
5
5
|
import { selectVisibleRevisions } from "../../ui/shared/revision-filters";
|
|
6
6
|
import type { MarkupDisplay } from "../../ui/headless/comment-decoration-model";
|
|
7
|
+
import { getAuthorColor } from "../../ui/headless/revision-decoration-model";
|
|
7
8
|
|
|
8
9
|
export interface TwRevisionSidebarProps {
|
|
9
10
|
trackedChanges: TrackedChangesSnapshot;
|
|
10
11
|
markupDisplay: MarkupDisplay;
|
|
11
12
|
activeRevisionId?: string;
|
|
12
13
|
onOpenRevision?: (revision: TrackedChangeEntrySnapshot) => void;
|
|
14
|
+
onReplyToRevision?: (revision: TrackedChangeEntrySnapshot) => void;
|
|
13
15
|
onAcceptRevision?: (revisionId: string) => void;
|
|
14
16
|
onRejectRevision?: (revisionId: string) => void;
|
|
15
17
|
onAcceptAllChanges?: () => void;
|
|
@@ -36,6 +38,7 @@ export function TwRevisionSidebar(props: TwRevisionSidebarProps) {
|
|
|
36
38
|
|
|
37
39
|
const [typeFilter, setTypeFilter] = React.useState<TypeFilter>("all");
|
|
38
40
|
const [authorFilter, setAuthorFilter] = React.useState<string | null>(null);
|
|
41
|
+
const activeCardRef = React.useRef<HTMLDivElement | null>(null);
|
|
39
42
|
|
|
40
43
|
// Derive distinct authors from all visible revisions
|
|
41
44
|
const authors = React.useMemo(() => {
|
|
@@ -78,6 +81,11 @@ export function TwRevisionSidebar(props: TwRevisionSidebarProps) {
|
|
|
78
81
|
}
|
|
79
82
|
}, [filteredRevisions, typeFilter, authorFilter, props.onRejectAllChanges, props.onRejectRevision]);
|
|
80
83
|
|
|
84
|
+
React.useEffect(() => {
|
|
85
|
+
if (!activeRevisionId) return;
|
|
86
|
+
activeCardRef.current?.scrollIntoView({ block: "nearest" });
|
|
87
|
+
}, [activeRevisionId, filteredRevisions]);
|
|
88
|
+
|
|
81
89
|
return (
|
|
82
90
|
<div className="flex flex-col outline-none">
|
|
83
91
|
{/* Stats header */}
|
|
@@ -118,48 +126,85 @@ export function TwRevisionSidebar(props: TwRevisionSidebarProps) {
|
|
|
118
126
|
<div className="space-y-2">
|
|
119
127
|
{filteredRevisions.map((rev) => {
|
|
120
128
|
const isActive = activeRevisionId === rev.revisionId;
|
|
129
|
+
const authorColor = getAuthorColor(rev.authorId);
|
|
121
130
|
|
|
122
131
|
return (
|
|
123
|
-
<
|
|
132
|
+
<div
|
|
124
133
|
key={rev.revisionId}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
134
|
+
ref={(node) => {
|
|
135
|
+
if (isActive) {
|
|
136
|
+
activeCardRef.current = node;
|
|
137
|
+
}
|
|
138
|
+
}}
|
|
139
|
+
className={`w-full text-left flex rounded-md bg-surface/90 transition-colors ring-1 ring-border ${isActive ? "bg-accent-soft/40 ring-accent/25 shadow-[var(--shadow-soft)]" : "hover:bg-surface"}`}
|
|
140
|
+
style={
|
|
141
|
+
authorColor && isActive
|
|
142
|
+
? { boxShadow: `0 0 0 1px ${authorColor}, var(--shadow-soft)` }
|
|
143
|
+
: undefined
|
|
144
|
+
}
|
|
128
145
|
>
|
|
129
146
|
<div className={`w-0.5 shrink-0 rounded-l-md ${
|
|
130
147
|
rev.kind === "insertion" ? "bg-insert"
|
|
131
148
|
: rev.kind === "deletion" ? "bg-danger"
|
|
132
149
|
: "bg-tertiary"
|
|
133
|
-
}`} />
|
|
134
|
-
<div className="
|
|
135
|
-
<
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
rev.
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
150
|
+
}`} style={authorColor ? { backgroundColor: authorColor } : undefined} />
|
|
151
|
+
<div className="flex-1 min-w-0">
|
|
152
|
+
<button
|
|
153
|
+
type="button"
|
|
154
|
+
className={`w-full p-2 text-left ${focusRingClass}`}
|
|
155
|
+
onClick={() => props.onOpenRevision?.(rev)}
|
|
156
|
+
>
|
|
157
|
+
<div className="mb-0.5 flex items-start justify-between gap-2">
|
|
158
|
+
<span className="text-[11px] font-medium text-primary">{rev.anchorLabel}</span>
|
|
159
|
+
<RevisionBadge status={rev.status} actionability={rev.actionability} />
|
|
160
|
+
</div>
|
|
161
|
+
<p className="mb-1 flex items-center gap-1.5 text-[10px] text-tertiary">
|
|
162
|
+
{authorColor ? (
|
|
163
|
+
<span
|
|
164
|
+
aria-hidden="true"
|
|
165
|
+
className="h-2 w-2 rounded-full"
|
|
166
|
+
style={{ backgroundColor: authorColor }}
|
|
167
|
+
/>
|
|
168
|
+
) : null}
|
|
169
|
+
<span className="truncate">{rev.authorId}</span>
|
|
170
|
+
<span aria-hidden="true">·</span>
|
|
171
|
+
<span>{rev.createdAt}</span>
|
|
147
172
|
</p>
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
173
|
+
{rev.excerpt ? (
|
|
174
|
+
<p className={`text-[11px] ${
|
|
175
|
+
rev.kind === "insertion" ? "text-insert"
|
|
176
|
+
: rev.kind === "deletion" ? "text-danger line-through"
|
|
177
|
+
: "text-secondary"
|
|
178
|
+
}`}>
|
|
179
|
+
{rev.excerpt}
|
|
180
|
+
</p>
|
|
181
|
+
) : (
|
|
182
|
+
<p className="text-[11px] text-secondary">{rev.label}</p>
|
|
183
|
+
)}
|
|
184
|
+
{rev.detail ? (
|
|
185
|
+
<p className="mt-1 text-[10px] text-secondary">{rev.detail}</p>
|
|
186
|
+
) : null}
|
|
187
|
+
</button>
|
|
188
|
+
<div className="flex flex-wrap gap-1.5 px-2 pb-2">
|
|
189
|
+
{props.onReplyToRevision ? (
|
|
190
|
+
<button
|
|
191
|
+
type="button"
|
|
192
|
+
className="inline-flex items-center gap-1 rounded-md px-1.5 py-0.5 text-[10px] text-secondary hover:bg-canvas transition-colors"
|
|
193
|
+
onClick={() => props.onReplyToRevision?.(rev)}
|
|
194
|
+
>
|
|
195
|
+
<MessageSquare className="h-3 w-3" />
|
|
196
|
+
{rev.replyCount && rev.replyCount > 0
|
|
197
|
+
? `Reply ${rev.replyCount}`
|
|
198
|
+
: "Reply"}
|
|
199
|
+
</button>
|
|
200
|
+
) : null}
|
|
155
201
|
{rev.actionability === "actionable" ? (
|
|
156
202
|
<>
|
|
157
203
|
<button
|
|
158
204
|
type="button"
|
|
159
205
|
disabled={!rev.canAccept || rev.status === "accepted"}
|
|
160
206
|
className="inline-flex items-center gap-1 rounded-md px-1.5 py-0.5 text-[10px] text-accent hover:bg-accent-soft transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
|
|
161
|
-
onClick={(
|
|
162
|
-
e.stopPropagation();
|
|
207
|
+
onClick={() => {
|
|
163
208
|
props.onAcceptRevision?.(rev.revisionId);
|
|
164
209
|
}}
|
|
165
210
|
>
|
|
@@ -169,8 +214,7 @@ export function TwRevisionSidebar(props: TwRevisionSidebarProps) {
|
|
|
169
214
|
type="button"
|
|
170
215
|
disabled={!rev.canReject || rev.status === "rejected"}
|
|
171
216
|
className="inline-flex items-center gap-1 rounded-md px-1.5 py-0.5 text-[10px] text-danger hover:bg-danger-soft transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
|
|
172
|
-
onClick={(
|
|
173
|
-
e.stopPropagation();
|
|
217
|
+
onClick={() => {
|
|
174
218
|
props.onRejectRevision?.(rev.revisionId);
|
|
175
219
|
}}
|
|
176
220
|
>
|
|
@@ -182,7 +226,7 @@ export function TwRevisionSidebar(props: TwRevisionSidebarProps) {
|
|
|
182
226
|
)}
|
|
183
227
|
</div>
|
|
184
228
|
</div>
|
|
185
|
-
</
|
|
229
|
+
</div>
|
|
186
230
|
);
|
|
187
231
|
})}
|
|
188
232
|
</div>
|
|
@@ -36,6 +36,24 @@ const POSTURE_META: Record<
|
|
|
36
36
|
"blocked-import": { chip: "BLOCKED REGION", kind: "danger" },
|
|
37
37
|
};
|
|
38
38
|
|
|
39
|
+
const focusRingClass =
|
|
40
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-canvas";
|
|
41
|
+
|
|
42
|
+
type ScopeFilterKey = "edit" | "suggest" | "comment" | "view" | "candidate" | "blocked";
|
|
43
|
+
|
|
44
|
+
const SCOPE_FILTERS: ReadonlyArray<{
|
|
45
|
+
key: ScopeFilterKey;
|
|
46
|
+
label: string;
|
|
47
|
+
postures: readonly ScopeRailPosture[];
|
|
48
|
+
}> = [
|
|
49
|
+
{ key: "edit", label: "Edit", postures: ["edit"] },
|
|
50
|
+
{ key: "suggest", label: "Suggest", postures: ["suggest"] },
|
|
51
|
+
{ key: "comment", label: "Comment", postures: ["comment"] },
|
|
52
|
+
{ key: "view", label: "Review", postures: ["view"] },
|
|
53
|
+
{ key: "candidate", label: "Scheduled", postures: ["candidate"] },
|
|
54
|
+
{ key: "blocked", label: "Blocked", postures: ["preserve-only", "blocked-import"] },
|
|
55
|
+
];
|
|
56
|
+
|
|
39
57
|
export const TwWorkflowTab: React.FC<TwWorkflowTabProps> = ({
|
|
40
58
|
segments,
|
|
41
59
|
activeScopeId,
|
|
@@ -43,13 +61,38 @@ export const TwWorkflowTab: React.FC<TwWorkflowTabProps> = ({
|
|
|
43
61
|
onActiveScopeChange,
|
|
44
62
|
}) => {
|
|
45
63
|
// Dedupe by scopeId so a scope spanning multiple pages shows once.
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
byScopeId.
|
|
64
|
+
const uniqueSegments = React.useMemo(() => {
|
|
65
|
+
const byScopeId = new Map<string, ScopeRailSegment>();
|
|
66
|
+
for (const segment of segments) {
|
|
67
|
+
if (!byScopeId.has(segment.scopeId)) {
|
|
68
|
+
byScopeId.set(segment.scopeId, segment);
|
|
69
|
+
}
|
|
50
70
|
}
|
|
51
|
-
|
|
52
|
-
|
|
71
|
+
return Array.from(byScopeId.values()).sort(compareWorkflowSegments(activeScopeId ?? null));
|
|
72
|
+
}, [activeScopeId, segments]);
|
|
73
|
+
const [query, setQuery] = React.useState("");
|
|
74
|
+
const [enabledFilters, setEnabledFilters] = React.useState<ReadonlySet<ScopeFilterKey>>(
|
|
75
|
+
() => new Set(SCOPE_FILTERS.map((filter) => filter.key)),
|
|
76
|
+
);
|
|
77
|
+
const availableFilters = React.useMemo(() => {
|
|
78
|
+
const presentPostures = new Set(uniqueSegments.map((segment) => segment.posture));
|
|
79
|
+
return SCOPE_FILTERS.filter((filter) =>
|
|
80
|
+
filter.postures.some((posture) => presentPostures.has(posture)),
|
|
81
|
+
);
|
|
82
|
+
}, [uniqueSegments]);
|
|
83
|
+
const visibleSegments = React.useMemo(() => {
|
|
84
|
+
const normalizedQuery = normalizeScopeQuery(query);
|
|
85
|
+
return uniqueSegments.filter((segment) => {
|
|
86
|
+
const filterKey = filterKeyForPosture(segment.posture);
|
|
87
|
+
if (!enabledFilters.has(filterKey)) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
if (!normalizedQuery) {
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
return scopeSearchText(segment).includes(normalizedQuery);
|
|
94
|
+
});
|
|
95
|
+
}, [enabledFilters, query, uniqueSegments]);
|
|
53
96
|
|
|
54
97
|
if (uniqueSegments.length === 0) {
|
|
55
98
|
return (
|
|
@@ -73,9 +116,79 @@ export const TwWorkflowTab: React.FC<TwWorkflowTabProps> = ({
|
|
|
73
116
|
<div className="text-[10px] font-semibold uppercase tracking-[0.14em] text-accent">
|
|
74
117
|
Document Intelligence
|
|
75
118
|
</div>
|
|
76
|
-
<div className="
|
|
119
|
+
<div className="flex items-baseline justify-between gap-3">
|
|
120
|
+
<div className="text-[15px] font-semibold text-primary">Workflow Scopes</div>
|
|
121
|
+
<div
|
|
122
|
+
className="text-[10px] font-semibold uppercase tracking-[0.12em] text-tertiary"
|
|
123
|
+
data-testid="workflow-scope-count"
|
|
124
|
+
>
|
|
125
|
+
{visibleSegments.length}/{uniqueSegments.length} shown
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
77
128
|
</div>
|
|
78
|
-
|
|
129
|
+
|
|
130
|
+
<div
|
|
131
|
+
className="wre-workflow-tab-controls rounded-lg border border-border bg-surface/55 p-2"
|
|
132
|
+
data-testid="workflow-scope-controls"
|
|
133
|
+
>
|
|
134
|
+
<input
|
|
135
|
+
aria-label="Search workflow scopes"
|
|
136
|
+
className={`h-8 w-full rounded-md border border-border bg-canvas px-2 text-[12px] text-primary placeholder:text-tertiary ${focusRingClass}`}
|
|
137
|
+
placeholder="Search scope, page, section..."
|
|
138
|
+
type="search"
|
|
139
|
+
value={query}
|
|
140
|
+
onChange={(event) => setQuery(event.currentTarget.value)}
|
|
141
|
+
/>
|
|
142
|
+
{availableFilters.length > 1 ? (
|
|
143
|
+
<div
|
|
144
|
+
aria-label="Workflow scope layers"
|
|
145
|
+
className="mt-2 flex flex-wrap gap-1"
|
|
146
|
+
role="group"
|
|
147
|
+
>
|
|
148
|
+
{availableFilters.map((filter) => {
|
|
149
|
+
const isEnabled = enabledFilters.has(filter.key);
|
|
150
|
+
return (
|
|
151
|
+
<button
|
|
152
|
+
key={filter.key}
|
|
153
|
+
type="button"
|
|
154
|
+
aria-pressed={isEnabled}
|
|
155
|
+
className={[
|
|
156
|
+
"rounded-full border px-2 py-1 text-[10px] font-semibold uppercase tracking-[0.1em] transition-colors",
|
|
157
|
+
isEnabled
|
|
158
|
+
? "border-accent/50 bg-accent/10 text-accent"
|
|
159
|
+
: "border-border bg-canvas text-tertiary hover:text-secondary",
|
|
160
|
+
].join(" ")}
|
|
161
|
+
data-testid={`workflow-scope-filter-${filter.key}`}
|
|
162
|
+
onClick={() => {
|
|
163
|
+
setEnabledFilters((current) => {
|
|
164
|
+
const next = new Set(current);
|
|
165
|
+
if (next.has(filter.key)) {
|
|
166
|
+
next.delete(filter.key);
|
|
167
|
+
} else {
|
|
168
|
+
next.add(filter.key);
|
|
169
|
+
}
|
|
170
|
+
return next;
|
|
171
|
+
});
|
|
172
|
+
}}
|
|
173
|
+
>
|
|
174
|
+
{filter.label}
|
|
175
|
+
</button>
|
|
176
|
+
);
|
|
177
|
+
})}
|
|
178
|
+
</div>
|
|
179
|
+
) : null}
|
|
180
|
+
</div>
|
|
181
|
+
|
|
182
|
+
{visibleSegments.length === 0 ? (
|
|
183
|
+
<div
|
|
184
|
+
className="rounded-md border border-dashed border-border bg-canvas/50 p-3 text-[11px] text-tertiary"
|
|
185
|
+
data-testid="workflow-scope-filter-empty"
|
|
186
|
+
>
|
|
187
|
+
No workflow scopes match the current search or layer filters.
|
|
188
|
+
</div>
|
|
189
|
+
) : null}
|
|
190
|
+
|
|
191
|
+
{visibleSegments.map((segment) => {
|
|
79
192
|
const meta = POSTURE_META[segment.posture];
|
|
80
193
|
const isActive = activeScopeId === segment.scopeId || segment.isActiveWorkItem;
|
|
81
194
|
return (
|
|
@@ -115,4 +228,42 @@ export const TwWorkflowTab: React.FC<TwWorkflowTabProps> = ({
|
|
|
115
228
|
);
|
|
116
229
|
};
|
|
117
230
|
|
|
231
|
+
function compareWorkflowSegments(activeScopeId: string | null) {
|
|
232
|
+
return (left: ScopeRailSegment, right: ScopeRailSegment): number => {
|
|
233
|
+
const leftActive = left.scopeId === activeScopeId || left.isActiveWorkItem;
|
|
234
|
+
const rightActive = right.scopeId === activeScopeId || right.isActiveWorkItem;
|
|
235
|
+
if (leftActive !== rightActive) {
|
|
236
|
+
return leftActive ? -1 : 1;
|
|
237
|
+
}
|
|
238
|
+
if (left.pageIndex !== right.pageIndex) {
|
|
239
|
+
return left.pageIndex - right.pageIndex;
|
|
240
|
+
}
|
|
241
|
+
if (left.sectionIndex !== right.sectionIndex) {
|
|
242
|
+
return left.sectionIndex - right.sectionIndex;
|
|
243
|
+
}
|
|
244
|
+
return (left.label || left.scopeId).localeCompare(right.label || right.scopeId);
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function filterKeyForPosture(posture: ScopeRailPosture): ScopeFilterKey {
|
|
249
|
+
if (posture === "preserve-only" || posture === "blocked-import") {
|
|
250
|
+
return "blocked";
|
|
251
|
+
}
|
|
252
|
+
return posture;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function normalizeScopeQuery(value: string): string {
|
|
256
|
+
return value.trim().toLocaleLowerCase();
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function scopeSearchText(segment: ScopeRailSegment): string {
|
|
260
|
+
return normalizeScopeQuery([
|
|
261
|
+
segment.label,
|
|
262
|
+
segment.scopeId,
|
|
263
|
+
segment.posture,
|
|
264
|
+
`page ${segment.pageIndex + 1}`,
|
|
265
|
+
`section ${segment.sectionIndex + 1}`,
|
|
266
|
+
].filter(Boolean).join(" "));
|
|
267
|
+
}
|
|
268
|
+
|
|
118
269
|
export default TwWorkflowTab;
|
|
@@ -145,6 +145,9 @@ export interface TwReviewWorkspaceProps {
|
|
|
145
145
|
activeRailTab: ReviewRailTab;
|
|
146
146
|
activeCommentId?: string;
|
|
147
147
|
activeRevisionId?: string;
|
|
148
|
+
/** Authoring mode toggle state: whether new edits are recorded as tracked changes. */
|
|
149
|
+
trackedChangesAuthoringEnabled?: boolean;
|
|
150
|
+
/** Visual markup state: whether tracked-change decorations are currently shown. */
|
|
148
151
|
showTrackedChanges: boolean;
|
|
149
152
|
workflowScopeSnapshot?: WorkflowScopeSnapshot | null;
|
|
150
153
|
interactionGuardSnapshot?: InteractionGuardSnapshot;
|
|
@@ -286,6 +289,7 @@ export interface TwReviewWorkspaceProps {
|
|
|
286
289
|
onAddReply?: (commentId: string, body: string) => void;
|
|
287
290
|
onEditBody?: (commentId: string, body: string) => void;
|
|
288
291
|
onOpenRevision?: (revision: TrackedChangeEntrySnapshot) => void;
|
|
292
|
+
onReplyToRevision?: (revision: TrackedChangeEntrySnapshot) => void;
|
|
289
293
|
onAcceptRevision?: (revisionId: string) => void;
|
|
290
294
|
onRejectRevision?: (revisionId: string) => void;
|
|
291
295
|
onAcceptAllChanges?: () => void;
|
|
@@ -21,7 +21,7 @@ export interface ReviewRailState {
|
|
|
21
21
|
* Review-rail open/close state + the responsive transition effect.
|
|
22
22
|
*
|
|
23
23
|
* When the responsive signature flips (narrow↔wide, or
|
|
24
|
-
* `reviewRailAvailable` changes), the rail resets to its default
|
|
24
|
+
* `reviewRailAvailable` changes), the rail resets to its default closed
|
|
25
25
|
* state per `getInitialReviewRailOpen`. A ref guards the effect so it
|
|
26
26
|
* only fires on actual transitions, not every viewport resize.
|
|
27
27
|
*
|
|
@@ -25,8 +25,6 @@ import {
|
|
|
25
25
|
ChevronLeft,
|
|
26
26
|
ChevronRight,
|
|
27
27
|
CircleOff,
|
|
28
|
-
Eye,
|
|
29
|
-
EyeOff,
|
|
30
28
|
FileDiff,
|
|
31
29
|
Flag,
|
|
32
30
|
Hand,
|
|
@@ -226,33 +224,33 @@ function RoleActionButton(arg: RoleActionButtonProps): React.JSX.Element | null
|
|
|
226
224
|
</Tooltip.Portal>
|
|
227
225
|
</Tooltip.Root>
|
|
228
226
|
);
|
|
229
|
-
case "tracked-changes-toggle":
|
|
227
|
+
case "tracked-changes-toggle": {
|
|
228
|
+
const trackChangesLabel = (props.showTrackedChanges ?? false)
|
|
229
|
+
? "Stop tracking changes"
|
|
230
|
+
: "Start tracking changes";
|
|
230
231
|
return (
|
|
231
232
|
<Tooltip.Root>
|
|
232
233
|
<Tooltip.Trigger asChild>
|
|
233
234
|
<Toggle.Root
|
|
234
235
|
pressed={props.showTrackedChanges ?? false}
|
|
235
236
|
onPressedChange={(v) => props.onShowTrackedChangesChange?.(v)}
|
|
236
|
-
aria-label={
|
|
237
|
+
aria-label={trackChangesLabel}
|
|
237
238
|
disabled={props.capabilities ? !props.capabilities.trackChangesSupported : false}
|
|
238
239
|
onMouseDown={preserveEditorSelectionMouseDown}
|
|
239
240
|
className={`inline-flex h-6 w-6 items-center justify-center rounded-md text-secondary transition-colors hover:bg-surface data-[state=on]:bg-canvas data-[state=on]:text-accent data-[state=on]:ring-1 data-[state=on]:ring-accent/30 data-[state=on]:shadow-sm outline-none disabled:opacity-40 ${focusRingClass}`}
|
|
240
241
|
data-testid="role-tracked-changes-toggle"
|
|
241
242
|
>
|
|
242
|
-
|
|
243
|
-
<Eye className="h-3.5 w-3.5" />
|
|
244
|
-
) : (
|
|
245
|
-
<EyeOff className="h-3.5 w-3.5" />
|
|
246
|
-
)}
|
|
243
|
+
<FileDiff className="h-3.5 w-3.5" />
|
|
247
244
|
</Toggle.Root>
|
|
248
245
|
</Tooltip.Trigger>
|
|
249
246
|
<Tooltip.Portal>
|
|
250
247
|
<Tooltip.Content className="rounded-md bg-primary px-2 py-1 text-xs text-white shadow-md z-50" sideOffset={6}>
|
|
251
|
-
{
|
|
248
|
+
{trackChangesLabel}
|
|
252
249
|
</Tooltip.Content>
|
|
253
250
|
</Tooltip.Portal>
|
|
254
251
|
</Tooltip.Root>
|
|
255
252
|
);
|
|
253
|
+
}
|
|
256
254
|
case "review-sidebar-tracked-changes":
|
|
257
255
|
return (
|
|
258
256
|
<Tooltip.Root>
|
|
@@ -15,8 +15,7 @@ import {
|
|
|
15
15
|
Bold,
|
|
16
16
|
ChevronDown,
|
|
17
17
|
Download,
|
|
18
|
-
|
|
19
|
-
EyeOff,
|
|
18
|
+
FileDiff,
|
|
20
19
|
FileText,
|
|
21
20
|
Highlighter,
|
|
22
21
|
ImagePlus,
|
|
@@ -89,7 +88,7 @@ export interface TwToolbarProps {
|
|
|
89
88
|
formattingState?: FormattingStateSnapshot;
|
|
90
89
|
activeListContext?: ActiveListContext | null;
|
|
91
90
|
styleCatalog?: StyleCatalogSnapshot;
|
|
92
|
-
/**
|
|
91
|
+
/** Authoring toggle for recording new edits as tracked changes. */
|
|
93
92
|
showTrackedChanges: boolean;
|
|
94
93
|
/** Active story target — shows a breadcrumb when editing a secondary story. */
|
|
95
94
|
activeStory?: EditorStoryTarget;
|
|
@@ -378,7 +377,7 @@ export function TwToolbar(props: TwToolbarProps) {
|
|
|
378
377
|
label="Bold"
|
|
379
378
|
shortcut="⌘B"
|
|
380
379
|
active={props.formattingState?.bold ?? false}
|
|
381
|
-
disabled={!canEdit}
|
|
380
|
+
disabled={!canEdit || !props.onToggleBold}
|
|
382
381
|
onClick={props.onToggleBold}
|
|
383
382
|
/>
|
|
384
383
|
<TwToolbarIconButton
|
|
@@ -386,7 +385,7 @@ export function TwToolbar(props: TwToolbarProps) {
|
|
|
386
385
|
label="Italic"
|
|
387
386
|
shortcut="⌘I"
|
|
388
387
|
active={props.formattingState?.italic ?? false}
|
|
389
|
-
disabled={!canEdit}
|
|
388
|
+
disabled={!canEdit || !props.onToggleItalic}
|
|
390
389
|
onClick={props.onToggleItalic}
|
|
391
390
|
/>
|
|
392
391
|
<TwToolbarIconButton
|
|
@@ -394,12 +393,17 @@ export function TwToolbar(props: TwToolbarProps) {
|
|
|
394
393
|
label="Underline"
|
|
395
394
|
shortcut="⌘U"
|
|
396
395
|
active={props.formattingState?.underline ?? false}
|
|
397
|
-
disabled={!canEdit}
|
|
396
|
+
disabled={!canEdit || !props.onToggleUnderline}
|
|
398
397
|
onClick={props.onToggleUnderline}
|
|
399
398
|
/>
|
|
400
399
|
{showAdvancedFormatting ? (
|
|
401
400
|
<ToolbarFormattingOverflow
|
|
402
|
-
disabled={
|
|
401
|
+
disabled={
|
|
402
|
+
!canEdit ||
|
|
403
|
+
(!props.onToggleStrikethrough &&
|
|
404
|
+
!props.onToggleSuperscript &&
|
|
405
|
+
!props.onToggleSubscript)
|
|
406
|
+
}
|
|
403
407
|
formattingState={props.formattingState}
|
|
404
408
|
onToggleStrikethrough={props.onToggleStrikethrough}
|
|
405
409
|
onToggleSuperscript={props.onToggleSuperscript}
|
|
@@ -452,14 +456,14 @@ export function TwToolbar(props: TwToolbarProps) {
|
|
|
452
456
|
icon={List}
|
|
453
457
|
label="Bulleted list"
|
|
454
458
|
active={Boolean(props.activeListContext && !props.activeListContext.isOrdered)}
|
|
455
|
-
disabled={!canEdit}
|
|
459
|
+
disabled={!canEdit || !props.onToggleBulletedList}
|
|
456
460
|
onClick={props.onToggleBulletedList}
|
|
457
461
|
/>
|
|
458
462
|
<TwToolbarIconButton
|
|
459
463
|
icon={Rows3}
|
|
460
464
|
label="Numbered list"
|
|
461
465
|
active={Boolean(props.activeListContext?.isOrdered)}
|
|
462
|
-
disabled={!canEdit}
|
|
466
|
+
disabled={!canEdit || !props.onToggleNumberedList}
|
|
463
467
|
onClick={props.onToggleNumberedList}
|
|
464
468
|
/>
|
|
465
469
|
</>
|
|
@@ -469,13 +473,13 @@ export function TwToolbar(props: TwToolbarProps) {
|
|
|
469
473
|
<TwToolbarIconButton
|
|
470
474
|
icon={Outdent}
|
|
471
475
|
label="Outdent"
|
|
472
|
-
disabled={!canEdit}
|
|
476
|
+
disabled={!canEdit || !props.onOutdent}
|
|
473
477
|
onClick={props.onOutdent}
|
|
474
478
|
/>
|
|
475
479
|
<TwToolbarIconButton
|
|
476
480
|
icon={Indent}
|
|
477
481
|
label="Indent"
|
|
478
|
-
disabled={!canEdit}
|
|
482
|
+
disabled={!canEdit || !props.onIndent}
|
|
479
483
|
onClick={props.onIndent}
|
|
480
484
|
/>
|
|
481
485
|
</>
|
|
@@ -669,12 +673,12 @@ export function TwToolbar(props: TwToolbarProps) {
|
|
|
669
673
|
<Toggle.Root
|
|
670
674
|
pressed={props.showTrackedChanges}
|
|
671
675
|
onPressedChange={props.onShowTrackedChangesChange}
|
|
672
|
-
aria-label={props.showTrackedChanges ? "
|
|
676
|
+
aria-label={props.showTrackedChanges ? "Stop tracking changes" : "Start tracking changes"}
|
|
673
677
|
disabled={caps ? !caps.trackChangesSupported : false}
|
|
674
678
|
onMouseDown={preserveEditorSelectionMouseDown}
|
|
675
679
|
className={`inline-flex h-6 w-6 items-center justify-center rounded-md text-secondary transition-colors hover:bg-surface data-[state=on]:bg-canvas data-[state=on]:text-accent data-[state=on]:ring-1 data-[state=on]:ring-accent/30 data-[state=on]:shadow-sm outline-none disabled:opacity-40 ${focusRingClass}`}
|
|
676
680
|
>
|
|
677
|
-
|
|
681
|
+
<FileDiff className="h-3.5 w-3.5" />
|
|
678
682
|
</Toggle.Root>
|
|
679
683
|
</Tooltip.Trigger>
|
|
680
684
|
<Tooltip.Portal>
|
|
@@ -682,7 +686,7 @@ export function TwToolbar(props: TwToolbarProps) {
|
|
|
682
686
|
className="rounded-md bg-primary px-2 py-1 text-xs text-white shadow-md z-50"
|
|
683
687
|
sideOffset={6}
|
|
684
688
|
>
|
|
685
|
-
{props.showTrackedChanges ? "
|
|
689
|
+
{props.showTrackedChanges ? "Stop tracking changes" : "Start tracking changes"}
|
|
686
690
|
</Tooltip.Content>
|
|
687
691
|
</Tooltip.Portal>
|
|
688
692
|
</Tooltip.Root>
|
|
@@ -1420,7 +1424,7 @@ function ToolbarFormattingOverflow(props: {
|
|
|
1420
1424
|
<ToolbarPopoverActionButton
|
|
1421
1425
|
active={props.formattingState?.strikethrough ?? false}
|
|
1422
1426
|
ariaLabel="Strikethrough"
|
|
1423
|
-
disabled={props.disabled}
|
|
1427
|
+
disabled={props.disabled || !props.onToggleStrikethrough}
|
|
1424
1428
|
icon={<Strikethrough className="h-3.5 w-3.5" />}
|
|
1425
1429
|
onClick={() => {
|
|
1426
1430
|
props.onToggleStrikethrough?.();
|
|
@@ -1430,7 +1434,7 @@ function ToolbarFormattingOverflow(props: {
|
|
|
1430
1434
|
<ToolbarPopoverActionButton
|
|
1431
1435
|
active={props.formattingState?.superscript ?? false}
|
|
1432
1436
|
ariaLabel="Superscript"
|
|
1433
|
-
disabled={props.disabled}
|
|
1437
|
+
disabled={props.disabled || !props.onToggleSuperscript}
|
|
1434
1438
|
icon={<Superscript className="h-3.5 w-3.5" />}
|
|
1435
1439
|
onClick={() => {
|
|
1436
1440
|
props.onToggleSuperscript?.();
|
|
@@ -1440,7 +1444,7 @@ function ToolbarFormattingOverflow(props: {
|
|
|
1440
1444
|
<ToolbarPopoverActionButton
|
|
1441
1445
|
active={props.formattingState?.subscript ?? false}
|
|
1442
1446
|
ariaLabel="Subscript"
|
|
1443
|
-
disabled={props.disabled}
|
|
1447
|
+
disabled={props.disabled || !props.onToggleSubscript}
|
|
1444
1448
|
icon={<Subscript className="h-3.5 w-3.5" />}
|
|
1445
1449
|
onClick={() => {
|
|
1446
1450
|
props.onToggleSubscript?.();
|
|
@@ -181,9 +181,12 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
181
181
|
// in the render tree below.
|
|
182
182
|
const { bodySlotRef, pmSurfaceElement } = usePmSurfaceCapture();
|
|
183
183
|
const { scrollRootRef, pageStackScrollRoot } = useScrollRootCapture();
|
|
184
|
+
const lastHoveredRevisionIdRef = useRef<string | null>(null);
|
|
184
185
|
const caps = props.capabilities;
|
|
185
186
|
const isPageWorkspace = props.workspaceMode === "page";
|
|
186
187
|
const markupDisplay = props.markupDisplay;
|
|
188
|
+
const trackedChangesAuthoringEnabled =
|
|
189
|
+
props.trackedChangesAuthoringEnabled ?? props.showTrackedChanges;
|
|
187
190
|
const [navOpen, setNavOpen] = useState(false);
|
|
188
191
|
const handleOpenPageModeStory = useCallback(
|
|
189
192
|
(target: EditorStoryTarget) => {
|
|
@@ -246,6 +249,22 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
246
249
|
reviewRailAvailable,
|
|
247
250
|
viewportWidth,
|
|
248
251
|
});
|
|
252
|
+
const handleDocumentMouseOver = useCallback(
|
|
253
|
+
(event: React.MouseEvent<HTMLDivElement>) => {
|
|
254
|
+
const element = event.target as HTMLElement | null;
|
|
255
|
+
const revisionId =
|
|
256
|
+
element?.closest?.("[data-revision-id]")?.getAttribute("data-revision-id") ?? null;
|
|
257
|
+
if (!revisionId || (revisionId === lastHoveredRevisionIdRef.current && reviewRailOpen)) {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
lastHoveredRevisionIdRef.current = revisionId;
|
|
261
|
+
if (reviewRailAvailable) {
|
|
262
|
+
setReviewRailOpen(true);
|
|
263
|
+
props.onActiveRailTabChange?.("changes");
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
[props.onActiveRailTabChange, reviewRailAvailable, reviewRailOpen, setReviewRailOpen],
|
|
267
|
+
);
|
|
249
268
|
// Incremented on zoom_changed / render_frame_ready so the placement
|
|
250
269
|
// useMemo below re-executes when the render kernel emits new rects.
|
|
251
270
|
const renderFrameRevision = useLayoutFacetRenderSignal(props.layoutFacet);
|
|
@@ -609,7 +628,7 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
609
628
|
toolbarInteractionPolicy?.canAddComment ??
|
|
610
629
|
(caps ? caps.canAddComment : false)
|
|
611
630
|
}
|
|
612
|
-
showTrackedChanges={
|
|
631
|
+
showTrackedChanges={trackedChangesAuthoringEnabled}
|
|
613
632
|
capabilities={caps}
|
|
614
633
|
onAddComment={
|
|
615
634
|
props.onAddComment
|
|
@@ -744,7 +763,7 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
744
763
|
formattingState={props.formattingState}
|
|
745
764
|
activeListContext={props.activeListContext}
|
|
746
765
|
styleCatalog={props.styleCatalog}
|
|
747
|
-
showTrackedChanges={
|
|
766
|
+
showTrackedChanges={trackedChangesAuthoringEnabled}
|
|
748
767
|
showSidebarToggle={responsiveChrome.showSidebarToggle}
|
|
749
768
|
isSidebarOpen={reviewRailOpen}
|
|
750
769
|
onUndo={runWithSelectionToolbarDismiss(props.onUndo)}
|
|
@@ -926,6 +945,7 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
926
945
|
<div className="flex flex-1 flex-col min-w-0">
|
|
927
946
|
<div
|
|
928
947
|
ref={scrollRootRef}
|
|
948
|
+
onMouseOver={handleDocumentMouseOver}
|
|
929
949
|
className="flex-1 overflow-y-auto bg-surface"
|
|
930
950
|
data-wre-scroll-root="true"
|
|
931
951
|
>
|
|
@@ -1243,6 +1263,7 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
1243
1263
|
onAddReply: props.onAddReply,
|
|
1244
1264
|
onEditBody: props.onEditBody,
|
|
1245
1265
|
onOpenRevision: props.onOpenRevision,
|
|
1266
|
+
onReplyToRevision: props.onReplyToRevision,
|
|
1246
1267
|
onAcceptRevision: props.onAcceptRevision,
|
|
1247
1268
|
onRejectRevision: props.onRejectRevision,
|
|
1248
1269
|
onAcceptAllChanges: props.onAcceptAllChanges,
|
|
@@ -1251,6 +1272,10 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
1251
1272
|
// Layer-06 workflow facet. Layout facet no longer exposes
|
|
1252
1273
|
// `getAllScopeRailSegments` (methods removed in v40 / Slice 4C).
|
|
1253
1274
|
scopeRailSegments: props.workflowFacet?.getAllRailSegments() ?? [],
|
|
1275
|
+
activeScopeId,
|
|
1276
|
+
onOpenScope: (segment) => {
|
|
1277
|
+
handleScopeStripeClick({ scopeId: segment.scopeId });
|
|
1278
|
+
},
|
|
1254
1279
|
workflowTab: props.reviewRailWorkflowTab,
|
|
1255
1280
|
workflowCount: props.reviewRailWorkflowCount,
|
|
1256
1281
|
workflowScopesTitle: props.reviewRailWorkflowScopesTitle,
|