@beyondwork/docx-react-component 1.0.84 → 1.0.86
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/internal/build-ref-projections.ts +3 -0
- package/src/api/public-types.ts +38 -0
- package/src/api/v3/_runtime-handle.ts +11 -0
- package/src/api/v3/runtime/content.ts +148 -1
- package/src/api/v3/runtime/formatting.ts +41 -0
- package/src/api/v3/runtime/review.ts +98 -0
- package/src/core/commands/index.ts +81 -25
- package/src/core/state/editor-state.ts +15 -0
- package/src/io/ooxml/header-footer-reference.ts +38 -0
- package/src/io/ooxml/parse-headers-footers.ts +11 -23
- package/src/io/ooxml/parse-main-document.ts +7 -10
- package/src/model/canonical-document.ts +9 -0
- package/src/model/review/comment-types.ts +2 -0
- package/src/runtime/document-runtime.ts +677 -54
- package/src/runtime/formatting/field/resolver.ts +73 -8
- package/src/runtime/layout/layout-engine-version.ts +31 -12
- package/src/runtime/layout/paginated-layout-engine.ts +18 -11
- package/src/runtime/layout/public-facet.ts +119 -16
- package/src/runtime/layout/resolve-page-fields.ts +68 -6
- package/src/runtime/layout/resolve-page-previews.ts +1 -1
- package/src/runtime/suggestions-snapshot.ts +24 -0
- package/src/runtime/surface-projection.ts +59 -2
- package/src/shell/ref-commands.ts +3 -354
- package/src/shell/session-bootstrap.ts +8 -0
- package/src/ui/WordReviewEditor.tsx +192 -35
- package/src/ui/editor-command-bag.ts +7 -1
- package/src/ui/editor-shell-view.tsx +1 -0
- package/src/ui/headless/revision-decoration-model.ts +13 -0
- package/src/ui/headless/selection-tool-types.ts +2 -0
- package/src/ui-tailwind/chrome/editor-action-registry.ts +7 -26
- package/src/ui-tailwind/chrome/tw-detach-handle.tsx +6 -2
- package/src/ui-tailwind/chrome/tw-suggestion-card.tsx +7 -3
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +175 -25
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +1 -1
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +12 -0
- package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +18 -30
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +1 -1
- package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +1 -1
- package/src/ui-tailwind/page-stack/tw-page-chrome-entry.tsx +20 -11
- package/src/ui-tailwind/page-stack/tw-page-footer-band.tsx +9 -4
- package/src/ui-tailwind/page-stack/tw-page-header-band.tsx +12 -7
- package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +29 -10
- package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +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 +46 -31
- package/src/ui-tailwind/review/tw-workflow-tab.tsx +159 -8
- package/src/ui-tailwind/review-workspace/types.ts +7 -2
- package/src/ui-tailwind/review-workspace/use-page-markers.ts +11 -1
- package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +8 -9
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +21 -16
- package/src/ui-tailwind/tw-review-workspace.tsx +27 -4
|
@@ -153,6 +153,7 @@ function CommentThreadCard(props: {
|
|
|
153
153
|
}, [presentation]);
|
|
154
154
|
const leadEntry = thread.entries[0];
|
|
155
155
|
const isDraftThread = thread.status === "open" && thread.entryCount === 1 && isEmptyCommentBody(leadEntry?.body);
|
|
156
|
+
const isLinkedRevisionThread = thread.linkedRevisionId != null;
|
|
156
157
|
const isOwnComment = props.currentUserId != null && leadEntry?.authorId === props.currentUserId;
|
|
157
158
|
const canEdit = isOwnComment && thread.status === "open" && props.onEditBody != null;
|
|
158
159
|
const hasNoBody = isEmptyCommentBody(leadEntry?.body);
|
|
@@ -205,6 +206,7 @@ function CommentThreadCard(props: {
|
|
|
205
206
|
{formatCommentDate(thread.createdAt)}
|
|
206
207
|
</span>
|
|
207
208
|
<span className="flex-1" />
|
|
209
|
+
{isLinkedRevisionThread ? <StatusBadge label="tracked change" tone="revision" /> : null}
|
|
208
210
|
{isDraftThread ? <StatusBadge label="draft" tone="draft" /> : null}
|
|
209
211
|
{thread.status === "resolved" ? <StatusBadge label="resolved" tone="resolved" /> : null}
|
|
210
212
|
</div>
|
|
@@ -222,7 +224,9 @@ function CommentThreadCard(props: {
|
|
|
222
224
|
body={leadEntry?.body ?? ""}
|
|
223
225
|
autoFocus={isActive && hasNoBody}
|
|
224
226
|
onSave={(newBody) => props.onEditBody?.(thread.commentId, newBody)}
|
|
225
|
-
label={isDraftThread
|
|
227
|
+
label={isDraftThread
|
|
228
|
+
? (isLinkedRevisionThread ? "Tracked change discussion" : "New comment")
|
|
229
|
+
: undefined}
|
|
226
230
|
/>
|
|
227
231
|
) : presentation ? (
|
|
228
232
|
<CommentMarkdownRenderer
|
|
@@ -247,7 +251,7 @@ function CommentThreadCard(props: {
|
|
|
247
251
|
props.onOpenComment?.(thread);
|
|
248
252
|
}}
|
|
249
253
|
>
|
|
250
|
-
New comment
|
|
254
|
+
{isLinkedRevisionThread ? "Tracked change discussion" : "New comment"}
|
|
251
255
|
</p>
|
|
252
256
|
) : null}
|
|
253
257
|
|
|
@@ -494,11 +498,12 @@ function formatCommentDate(raw: string): string {
|
|
|
494
498
|
}
|
|
495
499
|
}
|
|
496
500
|
|
|
497
|
-
function StatusBadge(props: { label: string; tone: "resolved" | "detached" | "draft" }) {
|
|
501
|
+
function StatusBadge(props: { label: string; tone: "resolved" | "detached" | "draft" | "revision" }) {
|
|
498
502
|
const styles: Record<string, string> = {
|
|
499
503
|
resolved: "text-insert bg-insert-soft",
|
|
500
504
|
detached: "text-comment bg-warning-soft",
|
|
501
505
|
draft: "text-secondary bg-subtle",
|
|
506
|
+
revision: "text-accent bg-accent-soft",
|
|
502
507
|
};
|
|
503
508
|
return (
|
|
504
509
|
<span
|
|
@@ -111,6 +111,7 @@ export interface TwReviewRailProps {
|
|
|
111
111
|
onAddReply?: (commentId: string, body: string) => void;
|
|
112
112
|
onEditBody?: (commentId: string, body: string) => void;
|
|
113
113
|
onOpenRevision?: (revision: TrackedChangeEntrySnapshot) => void;
|
|
114
|
+
onReplyToRevision?: (revision: TrackedChangeEntrySnapshot) => void;
|
|
114
115
|
onAcceptRevision?: (revisionId: string) => void;
|
|
115
116
|
onRejectRevision?: (revisionId: string) => void;
|
|
116
117
|
onAcceptAllChanges?: () => void;
|
|
@@ -285,6 +286,7 @@ export function TwReviewRail(props: TwReviewRailProps) {
|
|
|
285
286
|
markupDisplay={props.markupDisplay}
|
|
286
287
|
activeRevisionId={props.activeRevisionId}
|
|
287
288
|
onOpenRevision={props.onOpenRevision}
|
|
289
|
+
onReplyToRevision={props.onReplyToRevision}
|
|
288
290
|
onAcceptRevision={props.onAcceptRevision}
|
|
289
291
|
onRejectRevision={props.onRejectRevision}
|
|
290
292
|
onAcceptAllChanges={props.onAcceptAllChanges}
|
|
@@ -1,5 +1,5 @@
|
|
|
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";
|
|
@@ -10,6 +10,7 @@ export interface TwRevisionSidebarProps {
|
|
|
10
10
|
markupDisplay: MarkupDisplay;
|
|
11
11
|
activeRevisionId?: string;
|
|
12
12
|
onOpenRevision?: (revision: TrackedChangeEntrySnapshot) => void;
|
|
13
|
+
onReplyToRevision?: (revision: TrackedChangeEntrySnapshot) => void;
|
|
13
14
|
onAcceptRevision?: (revisionId: string) => void;
|
|
14
15
|
onRejectRevision?: (revisionId: string) => void;
|
|
15
16
|
onAcceptAllChanges?: () => void;
|
|
@@ -120,46 +121,61 @@ export function TwRevisionSidebar(props: TwRevisionSidebarProps) {
|
|
|
120
121
|
const isActive = activeRevisionId === rev.revisionId;
|
|
121
122
|
|
|
122
123
|
return (
|
|
123
|
-
<
|
|
124
|
+
<div
|
|
124
125
|
key={rev.revisionId}
|
|
125
|
-
|
|
126
|
-
className={`w-full text-left flex cursor-pointer rounded-md bg-surface/90 transition-colors ring-1 ring-border ${focusRingClass} ${isActive ? "bg-accent-soft/40 ring-accent/25" : "hover:bg-surface"}`}
|
|
127
|
-
onClick={() => props.onOpenRevision?.(rev)}
|
|
126
|
+
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" : "hover:bg-surface"}`}
|
|
128
127
|
>
|
|
129
128
|
<div className={`w-0.5 shrink-0 rounded-l-md ${
|
|
130
129
|
rev.kind === "insertion" ? "bg-insert"
|
|
131
130
|
: rev.kind === "deletion" ? "bg-danger"
|
|
132
131
|
: "bg-tertiary"
|
|
133
132
|
}`} />
|
|
134
|
-
<div className="
|
|
135
|
-
<
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
rev.
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
{
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
133
|
+
<div className="flex-1 min-w-0">
|
|
134
|
+
<button
|
|
135
|
+
type="button"
|
|
136
|
+
className={`w-full p-2 text-left ${focusRingClass}`}
|
|
137
|
+
onClick={() => props.onOpenRevision?.(rev)}
|
|
138
|
+
>
|
|
139
|
+
<div className="mb-0.5 flex items-start justify-between gap-2">
|
|
140
|
+
<span className="text-[11px] font-medium text-primary">{rev.anchorLabel}</span>
|
|
141
|
+
<RevisionBadge status={rev.status} actionability={rev.actionability} />
|
|
142
|
+
</div>
|
|
143
|
+
<p className="mb-1 text-[10px] text-tertiary">{rev.authorId} · {rev.createdAt}</p>
|
|
144
|
+
{rev.excerpt ? (
|
|
145
|
+
<p className={`text-[11px] ${
|
|
146
|
+
rev.kind === "insertion" ? "text-insert"
|
|
147
|
+
: rev.kind === "deletion" ? "text-danger line-through"
|
|
148
|
+
: "text-secondary"
|
|
149
|
+
}`}>
|
|
150
|
+
{rev.excerpt}
|
|
151
|
+
</p>
|
|
152
|
+
) : (
|
|
153
|
+
<p className="text-[11px] text-secondary">{rev.label}</p>
|
|
154
|
+
)}
|
|
155
|
+
{rev.detail ? (
|
|
156
|
+
<p className="mt-1 text-[10px] text-secondary">{rev.detail}</p>
|
|
157
|
+
) : null}
|
|
158
|
+
</button>
|
|
159
|
+
<div className="flex flex-wrap gap-1.5 px-2 pb-2">
|
|
160
|
+
{props.onReplyToRevision ? (
|
|
161
|
+
<button
|
|
162
|
+
type="button"
|
|
163
|
+
className="inline-flex items-center gap-1 rounded-md px-1.5 py-0.5 text-[10px] text-secondary hover:bg-canvas transition-colors"
|
|
164
|
+
onClick={() => props.onReplyToRevision?.(rev)}
|
|
165
|
+
>
|
|
166
|
+
<MessageSquare className="h-3 w-3" />
|
|
167
|
+
{rev.replyCount && rev.replyCount > 0
|
|
168
|
+
? `Reply ${rev.replyCount}`
|
|
169
|
+
: "Reply"}
|
|
170
|
+
</button>
|
|
171
|
+
) : null}
|
|
155
172
|
{rev.actionability === "actionable" ? (
|
|
156
173
|
<>
|
|
157
174
|
<button
|
|
158
175
|
type="button"
|
|
159
176
|
disabled={!rev.canAccept || rev.status === "accepted"}
|
|
160
177
|
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();
|
|
178
|
+
onClick={() => {
|
|
163
179
|
props.onAcceptRevision?.(rev.revisionId);
|
|
164
180
|
}}
|
|
165
181
|
>
|
|
@@ -169,8 +185,7 @@ export function TwRevisionSidebar(props: TwRevisionSidebarProps) {
|
|
|
169
185
|
type="button"
|
|
170
186
|
disabled={!rev.canReject || rev.status === "rejected"}
|
|
171
187
|
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();
|
|
188
|
+
onClick={() => {
|
|
174
189
|
props.onRejectRevision?.(rev.revisionId);
|
|
175
190
|
}}
|
|
176
191
|
>
|
|
@@ -182,7 +197,7 @@ export function TwRevisionSidebar(props: TwRevisionSidebarProps) {
|
|
|
182
197
|
)}
|
|
183
198
|
</div>
|
|
184
199
|
</div>
|
|
185
|
-
</
|
|
200
|
+
</div>
|
|
186
201
|
);
|
|
187
202
|
})}
|
|
188
203
|
</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;
|
|
@@ -192,6 +195,7 @@ export interface TwReviewWorkspaceProps {
|
|
|
192
195
|
onZoomChange?: (level: ZoomLevel) => void;
|
|
193
196
|
onActiveRailTabChange?: (value: ReviewRailTab) => void;
|
|
194
197
|
onShowTrackedChangesChange?: (show: boolean) => void;
|
|
198
|
+
onReviewMarkupModeChange?: (mode: MarkupDisplay) => void;
|
|
195
199
|
onUndo?: () => void;
|
|
196
200
|
onRedo?: () => void;
|
|
197
201
|
onSetParagraphStyle?: (styleId: string) => void;
|
|
@@ -285,6 +289,7 @@ export interface TwReviewWorkspaceProps {
|
|
|
285
289
|
onAddReply?: (commentId: string, body: string) => void;
|
|
286
290
|
onEditBody?: (commentId: string, body: string) => void;
|
|
287
291
|
onOpenRevision?: (revision: TrackedChangeEntrySnapshot) => void;
|
|
292
|
+
onReplyToRevision?: (revision: TrackedChangeEntrySnapshot) => void;
|
|
288
293
|
onAcceptRevision?: (revisionId: string) => void;
|
|
289
294
|
onRejectRevision?: (revisionId: string) => void;
|
|
290
295
|
onAcceptAllChanges?: () => void;
|
|
@@ -293,7 +298,7 @@ export interface TwReviewWorkspaceProps {
|
|
|
293
298
|
/**
|
|
294
299
|
* @deprecated P8.11 — the workspace no longer renders a workspace-level
|
|
295
300
|
* header band with an "Edit header" button; per-page header bands route
|
|
296
|
-
* clicks via `onOpenStory` / `runtime.openStory` directly.
|
|
301
|
+
* double-clicks via `onOpenStory` / `runtime.openStory` directly. The prop
|
|
297
302
|
* remains optional for one release so existing hosts continue to
|
|
298
303
|
* compile; supplying it emits a `console.warn` on mount.
|
|
299
304
|
*/
|
|
@@ -313,7 +318,7 @@ export interface TwReviewWorkspaceProps {
|
|
|
313
318
|
onOpenFooterStoryForPage?: (pageIndex: number) => void;
|
|
314
319
|
/**
|
|
315
320
|
* P8.11 — fired when a per-page chrome band (header / footer) is
|
|
316
|
-
* clicked to promote it into the active editing surface.
|
|
321
|
+
* double-clicked to promote it into the active editing surface. Wire to
|
|
317
322
|
* `runtime.openStory(target)`; the chrome layer's portal mechanism
|
|
318
323
|
* then reparents the PM surface into the matching band's active slot.
|
|
319
324
|
*/
|
|
@@ -212,6 +212,16 @@ export function usePageMarkers(options: UsePageMarkersOptions): PageMarkersResul
|
|
|
212
212
|
const [pageMarkers, setPageMarkers] = useState<readonly HTMLElement[]>([]);
|
|
213
213
|
|
|
214
214
|
useEffect(() => {
|
|
215
|
+
if (geometryFacet && layoutFacet) {
|
|
216
|
+
// Warm path: render-kernel geometry gives us page frames and the layout
|
|
217
|
+
// facet gives us page offsets, so DOM page-break markers are only legacy
|
|
218
|
+
// fallback. Skipping the marker scan avoids querySelectorAll +
|
|
219
|
+
// IntersectionObserver churn when PM swaps virtualized blocks at page
|
|
220
|
+
// dividers during scroll.
|
|
221
|
+
setPageMarkers((prev) => (prev.length === 0 ? prev : []));
|
|
222
|
+
return undefined;
|
|
223
|
+
}
|
|
224
|
+
|
|
215
225
|
const root = pageStackScrollRoot;
|
|
216
226
|
if (!root) {
|
|
217
227
|
setPageMarkers((prev) => (prev.length === 0 ? prev : []));
|
|
@@ -283,7 +293,7 @@ export function usePageMarkers(options: UsePageMarkersOptions): PageMarkersResul
|
|
|
283
293
|
// every requestViewportRefresh(), which would add two extra render passes per
|
|
284
294
|
// scroll event. The page-0 fallback uses -1 when page1First is unknown,
|
|
285
295
|
// which is correct (no page-0 blocks → synthetic marker contributes nothing).
|
|
286
|
-
}, [pageStackScrollRoot, snapshot.revisionToken]);
|
|
296
|
+
}, [geometryFacet, layoutFacet, pageStackScrollRoot, snapshot.revisionToken]);
|
|
287
297
|
|
|
288
298
|
const selectionBlockIndex = useMemo(() => {
|
|
289
299
|
const sel = snapshot.selection;
|
|
@@ -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,32 +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)}
|
|
237
|
+
aria-label={trackChangesLabel}
|
|
236
238
|
disabled={props.capabilities ? !props.capabilities.trackChangesSupported : false}
|
|
237
239
|
onMouseDown={preserveEditorSelectionMouseDown}
|
|
238
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}`}
|
|
239
241
|
data-testid="role-tracked-changes-toggle"
|
|
240
242
|
>
|
|
241
|
-
|
|
242
|
-
<Eye className="h-3.5 w-3.5" />
|
|
243
|
-
) : (
|
|
244
|
-
<EyeOff className="h-3.5 w-3.5" />
|
|
245
|
-
)}
|
|
243
|
+
<FileDiff className="h-3.5 w-3.5" />
|
|
246
244
|
</Toggle.Root>
|
|
247
245
|
</Tooltip.Trigger>
|
|
248
246
|
<Tooltip.Portal>
|
|
249
247
|
<Tooltip.Content className="rounded-md bg-primary px-2 py-1 text-xs text-white shadow-md z-50" sideOffset={6}>
|
|
250
|
-
{
|
|
248
|
+
{trackChangesLabel}
|
|
251
249
|
</Tooltip.Content>
|
|
252
250
|
</Tooltip.Portal>
|
|
253
251
|
</Tooltip.Root>
|
|
254
252
|
);
|
|
253
|
+
}
|
|
255
254
|
case "review-sidebar-tracked-changes":
|
|
256
255
|
return (
|
|
257
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,11 +673,12 @@ export function TwToolbar(props: TwToolbarProps) {
|
|
|
669
673
|
<Toggle.Root
|
|
670
674
|
pressed={props.showTrackedChanges}
|
|
671
675
|
onPressedChange={props.onShowTrackedChangesChange}
|
|
676
|
+
aria-label={props.showTrackedChanges ? "Stop tracking changes" : "Start tracking changes"}
|
|
672
677
|
disabled={caps ? !caps.trackChangesSupported : false}
|
|
673
678
|
onMouseDown={preserveEditorSelectionMouseDown}
|
|
674
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}`}
|
|
675
680
|
>
|
|
676
|
-
|
|
681
|
+
<FileDiff className="h-3.5 w-3.5" />
|
|
677
682
|
</Toggle.Root>
|
|
678
683
|
</Tooltip.Trigger>
|
|
679
684
|
<Tooltip.Portal>
|
|
@@ -681,7 +686,7 @@ export function TwToolbar(props: TwToolbarProps) {
|
|
|
681
686
|
className="rounded-md bg-primary px-2 py-1 text-xs text-white shadow-md z-50"
|
|
682
687
|
sideOffset={6}
|
|
683
688
|
>
|
|
684
|
-
{props.showTrackedChanges ? "
|
|
689
|
+
{props.showTrackedChanges ? "Stop tracking changes" : "Start tracking changes"}
|
|
685
690
|
</Tooltip.Content>
|
|
686
691
|
</Tooltip.Portal>
|
|
687
692
|
</Tooltip.Root>
|
|
@@ -1419,7 +1424,7 @@ function ToolbarFormattingOverflow(props: {
|
|
|
1419
1424
|
<ToolbarPopoverActionButton
|
|
1420
1425
|
active={props.formattingState?.strikethrough ?? false}
|
|
1421
1426
|
ariaLabel="Strikethrough"
|
|
1422
|
-
disabled={props.disabled}
|
|
1427
|
+
disabled={props.disabled || !props.onToggleStrikethrough}
|
|
1423
1428
|
icon={<Strikethrough className="h-3.5 w-3.5" />}
|
|
1424
1429
|
onClick={() => {
|
|
1425
1430
|
props.onToggleStrikethrough?.();
|
|
@@ -1429,7 +1434,7 @@ function ToolbarFormattingOverflow(props: {
|
|
|
1429
1434
|
<ToolbarPopoverActionButton
|
|
1430
1435
|
active={props.formattingState?.superscript ?? false}
|
|
1431
1436
|
ariaLabel="Superscript"
|
|
1432
|
-
disabled={props.disabled}
|
|
1437
|
+
disabled={props.disabled || !props.onToggleSuperscript}
|
|
1433
1438
|
icon={<Superscript className="h-3.5 w-3.5" />}
|
|
1434
1439
|
onClick={() => {
|
|
1435
1440
|
props.onToggleSuperscript?.();
|
|
@@ -1439,7 +1444,7 @@ function ToolbarFormattingOverflow(props: {
|
|
|
1439
1444
|
<ToolbarPopoverActionButton
|
|
1440
1445
|
active={props.formattingState?.subscript ?? false}
|
|
1441
1446
|
ariaLabel="Subscript"
|
|
1442
|
-
disabled={props.disabled}
|
|
1447
|
+
disabled={props.disabled || !props.onToggleSubscript}
|
|
1443
1448
|
icon={<Subscript className="h-3.5 w-3.5" />}
|
|
1444
1449
|
onClick={() => {
|
|
1445
1450
|
props.onToggleSubscript?.();
|