@beyondwork/docx-react-component 1.0.28 → 1.0.30
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 +26 -37
- package/src/api/public-types.ts +531 -0
- package/src/api/session-state.ts +2 -0
- package/src/core/commands/index.ts +201 -79
- package/src/core/commands/table-structure-commands.ts +138 -5
- package/src/core/state/text-transaction.ts +370 -3
- package/src/index.ts +41 -0
- package/src/io/docx-session.ts +318 -25
- package/src/io/export/serialize-footnotes.ts +41 -46
- package/src/io/export/serialize-headers-footers.ts +36 -40
- package/src/io/export/serialize-main-document.ts +55 -89
- package/src/io/export/serialize-numbering.ts +104 -4
- package/src/io/export/serialize-runtime-revisions.ts +196 -2
- package/src/io/export/split-story-blocks-for-runtime-revisions.ts +252 -0
- package/src/io/export/table-properties-xml.ts +318 -0
- package/src/io/normalize/normalize-text.ts +34 -3
- package/src/io/ooxml/parse-comments.ts +6 -0
- package/src/io/ooxml/parse-footnotes.ts +69 -13
- package/src/io/ooxml/parse-headers-footers.ts +54 -11
- package/src/io/ooxml/parse-main-document.ts +112 -42
- package/src/io/ooxml/parse-numbering.ts +341 -26
- package/src/io/ooxml/parse-revisions.ts +118 -4
- package/src/io/ooxml/parse-styles.ts +176 -0
- package/src/io/ooxml/parse-tables.ts +34 -25
- package/src/io/ooxml/revision-boundaries.ts +127 -3
- package/src/io/ooxml/workflow-payload.ts +544 -0
- package/src/model/canonical-document.ts +91 -1
- package/src/model/snapshot.ts +112 -1
- package/src/preservation/store.ts +73 -3
- package/src/review/store/comment-store.ts +19 -1
- package/src/review/store/revision-actions.ts +29 -0
- package/src/review/store/revision-store.ts +12 -1
- package/src/review/store/revision-types.ts +11 -0
- package/src/runtime/context-analytics.ts +824 -0
- package/src/runtime/document-locations.ts +521 -0
- package/src/runtime/document-navigation.ts +14 -1
- package/src/runtime/document-outline.ts +440 -0
- package/src/runtime/document-runtime.ts +941 -45
- package/src/runtime/event-refresh-hints.ts +137 -0
- package/src/runtime/numbering-prefix.ts +67 -39
- package/src/runtime/page-layout-estimation.ts +100 -7
- package/src/runtime/resolved-numbering-geometry.ts +293 -0
- package/src/runtime/session-capabilities.ts +2 -2
- package/src/runtime/suggestions-snapshot.ts +137 -0
- package/src/runtime/surface-projection.ts +223 -27
- package/src/runtime/table-style-resolver.ts +409 -0
- package/src/runtime/view-state.ts +17 -1
- package/src/runtime/workflow-markup.ts +54 -14
- package/src/ui/WordReviewEditor.tsx +1269 -87
- package/src/ui/editor-command-bag.ts +7 -0
- package/src/ui/editor-runtime-boundary.ts +111 -10
- package/src/ui/editor-shell-view.tsx +17 -15
- package/src/ui/editor-surface-controller.tsx +5 -0
- package/src/ui/headless/selection-tool-context.ts +19 -0
- package/src/ui/headless/selection-tool-resolver.ts +752 -0
- package/src/ui/headless/selection-tool-types.ts +129 -0
- package/src/ui/headless/selection-toolbar-model.ts +10 -33
- package/src/ui/runtime-shortcut-dispatch.ts +365 -0
- package/src/ui-tailwind/chrome/chrome-preset-model.ts +107 -0
- package/src/ui-tailwind/chrome/chrome-preset-toolbar.tsx +15 -0
- package/src/ui-tailwind/chrome/review-queue-bar.tsx +97 -0
- package/src/ui-tailwind/chrome/tw-context-analytics-summary.tsx +122 -0
- package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +1 -9
- package/src/ui-tailwind/chrome/tw-object-context-toolbar.tsx +1 -5
- package/src/ui-tailwind/chrome/tw-page-ruler.tsx +8 -29
- package/src/ui-tailwind/chrome/tw-selection-tool-blocked.tsx +23 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-comment.tsx +35 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-formatting.tsx +37 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +298 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-structure.tsx +116 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-suggestion.tsx +29 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-workflow.tsx +27 -0
- package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +3 -3
- package/src/ui-tailwind/chrome/tw-suggestion-card.tsx +3 -3
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +86 -14
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +57 -52
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +36 -52
- package/src/ui-tailwind/editor-surface/pm-schema.ts +56 -5
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +87 -24
- package/src/ui-tailwind/editor-surface/surface-build-keys.ts +4 -0
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +135 -32
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +74 -7
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +17 -17
- package/src/ui-tailwind/review/tw-review-rail.tsx +19 -17
- package/src/ui-tailwind/review/tw-revision-sidebar.tsx +10 -10
- package/src/ui-tailwind/status/tw-status-bar.tsx +10 -6
- package/src/ui-tailwind/theme/editor-theme.css +58 -40
- package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +4 -4
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +250 -181
- package/src/ui-tailwind/tw-review-workspace.tsx +323 -280
- package/src/validation/compatibility-engine.ts +246 -2
- package/src/validation/docx-comment-proof.ts +24 -11
|
@@ -0,0 +1,752 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
EditorStoryTarget,
|
|
3
|
+
FormattingStateSnapshot,
|
|
4
|
+
SuggestionEntrySnapshot,
|
|
5
|
+
TrackedChangeEntrySnapshot,
|
|
6
|
+
WorkflowScopeSnapshot,
|
|
7
|
+
} from "../../api/public-types";
|
|
8
|
+
import { storyTargetsEqual } from "../../core/selection/mapping";
|
|
9
|
+
import type { SelectionToolResolverInput } from "./selection-tool-context";
|
|
10
|
+
import type {
|
|
11
|
+
ActiveSelectionToolModel,
|
|
12
|
+
BlockedExplainerSelectionToolModel,
|
|
13
|
+
CommentThreadSelectionToolModel,
|
|
14
|
+
FormattingInlineSelectionToolModel,
|
|
15
|
+
SelectionToolBadge,
|
|
16
|
+
StructureContextSelectionToolModel,
|
|
17
|
+
SuggestionReviewSelectionToolModel,
|
|
18
|
+
WorkflowTaskSelectionToolModel,
|
|
19
|
+
} from "./selection-tool-types";
|
|
20
|
+
|
|
21
|
+
export function resolveActiveSelectionTool(
|
|
22
|
+
input: SelectionToolResolverInput,
|
|
23
|
+
): ActiveSelectionToolModel | null {
|
|
24
|
+
const suggestionTool = buildSuggestionReviewSelectionToolModel(input);
|
|
25
|
+
const commentTool = buildCommentThreadSelectionToolModel(input);
|
|
26
|
+
const workflowTool = buildWorkflowTaskSelectionToolModel(input);
|
|
27
|
+
const structureTool = buildStructureContextSelectionToolModel(input);
|
|
28
|
+
// Semantic targets beat generic range formatting so structure tools do not
|
|
29
|
+
// leak preview text when the user is acting on a table, image, or object.
|
|
30
|
+
const formattingTool = buildFormattingInlineSelectionToolModel(input);
|
|
31
|
+
const blockedTool = buildBlockedExplainerSelectionToolModel(input);
|
|
32
|
+
const resolvedTool =
|
|
33
|
+
suggestionTool ??
|
|
34
|
+
commentTool ??
|
|
35
|
+
workflowTool ??
|
|
36
|
+
structureTool ??
|
|
37
|
+
formattingTool ??
|
|
38
|
+
blockedTool;
|
|
39
|
+
|
|
40
|
+
return resolvedTool;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function buildFormattingInlineSelectionToolModel(
|
|
44
|
+
input: SelectionToolResolverInput,
|
|
45
|
+
): FormattingInlineSelectionToolModel | null {
|
|
46
|
+
const {
|
|
47
|
+
snapshot,
|
|
48
|
+
viewState,
|
|
49
|
+
capabilities,
|
|
50
|
+
documentNavigation,
|
|
51
|
+
styleCatalog,
|
|
52
|
+
formattingState,
|
|
53
|
+
workflowScopeSnapshot,
|
|
54
|
+
interactionGuardSnapshot,
|
|
55
|
+
addCommentDisabledReason,
|
|
56
|
+
} = input;
|
|
57
|
+
|
|
58
|
+
if (
|
|
59
|
+
!snapshot.surface ||
|
|
60
|
+
snapshot.selection.isCollapsed ||
|
|
61
|
+
snapshot.selection.activeRange.kind !== "range" ||
|
|
62
|
+
viewState.viewMode === "view"
|
|
63
|
+
) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const previewText = summarizeSelectionPreview(snapshot);
|
|
68
|
+
if (!previewText) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const workflowPosture = resolveSelectionWorkflowPosture(
|
|
73
|
+
snapshot,
|
|
74
|
+
viewState.activeStory,
|
|
75
|
+
workflowScopeSnapshot,
|
|
76
|
+
interactionGuardSnapshot,
|
|
77
|
+
);
|
|
78
|
+
const targetAccess = getSelectionTargetAccess(input, workflowPosture);
|
|
79
|
+
const formattingCapability = getCommandCapability(input, "formatting", workflowPosture);
|
|
80
|
+
|
|
81
|
+
if (!capabilities.canEdit && !formattingCapability.supported) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
if (targetAccess === "comment-only" || targetAccess === "view-only" || targetAccess === "blocked") {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
if (Boolean(input.preferListStructureContext) && Boolean(viewState.activeListContext?.isOrdered)) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
const badges = [
|
|
91
|
+
createSelectionToolStoryBadge(viewState.activeStory),
|
|
92
|
+
createSelectionToolWorkflowBadge(workflowPosture),
|
|
93
|
+
viewState.workspaceMode === "page" && documentNavigation.pageCount > 0
|
|
94
|
+
? { label: `Page ${documentNavigation.activePageIndex + 1}` as const }
|
|
95
|
+
: null,
|
|
96
|
+
formattingState ? createSelectionToolStyleBadge(styleCatalog.paragraphs, formattingState) : null,
|
|
97
|
+
createSelectionToolListBadge(viewState.activeListContext),
|
|
98
|
+
].filter((badge): badge is SelectionToolBadge => Boolean(badge));
|
|
99
|
+
|
|
100
|
+
const canToggleFormatting = formattingCapability.supported;
|
|
101
|
+
const canAddComment = capabilities.canAddComment;
|
|
102
|
+
const disabledReason = resolveToolDisabledReason(
|
|
103
|
+
canToggleFormatting ? undefined : formattingCapability.blockedReason,
|
|
104
|
+
workflowPosture.disabledReason,
|
|
105
|
+
canAddComment ? undefined : addCommentDisabledReason,
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
kind: "formatting-inline",
|
|
110
|
+
previewText,
|
|
111
|
+
badges,
|
|
112
|
+
canToggleFormatting,
|
|
113
|
+
boldActive: formattingState?.bold ?? false,
|
|
114
|
+
italicActive: formattingState?.italic ?? false,
|
|
115
|
+
underlineActive: formattingState?.underline ?? false,
|
|
116
|
+
canAddComment,
|
|
117
|
+
...(disabledReason ? { disabledReason } : {}),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function buildSuggestionReviewSelectionToolModel(
|
|
122
|
+
input: SelectionToolResolverInput,
|
|
123
|
+
): SuggestionReviewSelectionToolModel | null {
|
|
124
|
+
const {
|
|
125
|
+
snapshot,
|
|
126
|
+
viewState,
|
|
127
|
+
capabilities,
|
|
128
|
+
workflowScopeSnapshot,
|
|
129
|
+
interactionGuardSnapshot,
|
|
130
|
+
suggestionsSnapshot,
|
|
131
|
+
activeRevisionId,
|
|
132
|
+
suppressedSuggestionRevisionId,
|
|
133
|
+
addCommentDisabledReason,
|
|
134
|
+
activeTableContext,
|
|
135
|
+
activeImageContext,
|
|
136
|
+
activeObjectContext,
|
|
137
|
+
activeListContext,
|
|
138
|
+
preferListStructureContext,
|
|
139
|
+
} = input;
|
|
140
|
+
|
|
141
|
+
if (
|
|
142
|
+
!snapshot.surface ||
|
|
143
|
+
!capabilities.canEdit ||
|
|
144
|
+
viewState.viewMode === "view"
|
|
145
|
+
) {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
if (
|
|
149
|
+
activeTableContext ||
|
|
150
|
+
activeImageContext ||
|
|
151
|
+
activeObjectContext ||
|
|
152
|
+
(preferListStructureContext && activeListContext)
|
|
153
|
+
) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const activeRange =
|
|
158
|
+
!snapshot.selection.isCollapsed && snapshot.selection.activeRange.kind === "range"
|
|
159
|
+
? snapshot.selection.activeRange
|
|
160
|
+
: null;
|
|
161
|
+
const selectionFrom = activeRange
|
|
162
|
+
? Math.min(activeRange.from, activeRange.to)
|
|
163
|
+
: null;
|
|
164
|
+
const selectionTo = activeRange
|
|
165
|
+
? Math.max(activeRange.from, activeRange.to)
|
|
166
|
+
: null;
|
|
167
|
+
const matchingScope = findSelectionMatchingScope(
|
|
168
|
+
activeRange,
|
|
169
|
+
viewState.activeStory,
|
|
170
|
+
workflowScopeSnapshot,
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
const candidateSuggestions = suggestionsSnapshot?.suggestions.filter((suggestion) =>
|
|
174
|
+
storyTargetsEqual(suggestion.storyTarget, viewState.activeStory) &&
|
|
175
|
+
suggestion.status === "active" &&
|
|
176
|
+
suggestion.actionability === "actionable" &&
|
|
177
|
+
suggestion.anchor.kind === "range" &&
|
|
178
|
+
(
|
|
179
|
+
(
|
|
180
|
+
activeRevisionId !== undefined &&
|
|
181
|
+
suggestion.changeIds.includes(activeRevisionId) &&
|
|
182
|
+
selectionMatchesSuggestion(snapshot.selection, suggestion)
|
|
183
|
+
) ||
|
|
184
|
+
(
|
|
185
|
+
selectionFrom !== null &&
|
|
186
|
+
selectionTo !== null &&
|
|
187
|
+
rangesOverlap(
|
|
188
|
+
selectionFrom,
|
|
189
|
+
selectionTo,
|
|
190
|
+
suggestion.anchor.from,
|
|
191
|
+
suggestion.anchor.to,
|
|
192
|
+
)
|
|
193
|
+
)
|
|
194
|
+
)
|
|
195
|
+
) ?? [];
|
|
196
|
+
|
|
197
|
+
const focusedSuggestion = (
|
|
198
|
+
activeRevisionId
|
|
199
|
+
? candidateSuggestions.find((suggestion) => suggestion.changeIds.includes(activeRevisionId))
|
|
200
|
+
: null
|
|
201
|
+
) ?? candidateSuggestions[0];
|
|
202
|
+
|
|
203
|
+
if (!focusedSuggestion || focusedSuggestion.suggestionId === suppressedSuggestionRevisionId) {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const badges = [
|
|
208
|
+
createSelectionToolStoryBadge(viewState.activeStory),
|
|
209
|
+
matchingScope?.label
|
|
210
|
+
? {
|
|
211
|
+
label: matchingScope.label,
|
|
212
|
+
tone: "accent" as const,
|
|
213
|
+
}
|
|
214
|
+
: workflowScopeSnapshot?.activeWorkItem?.title
|
|
215
|
+
? {
|
|
216
|
+
label: workflowScopeSnapshot.activeWorkItem.title,
|
|
217
|
+
tone: "accent" as const,
|
|
218
|
+
}
|
|
219
|
+
: null,
|
|
220
|
+
].filter((badge): badge is SelectionToolBadge => Boolean(badge));
|
|
221
|
+
const workflowPosture = resolveSelectionWorkflowPosture(
|
|
222
|
+
snapshot,
|
|
223
|
+
viewState.activeStory,
|
|
224
|
+
workflowScopeSnapshot,
|
|
225
|
+
interactionGuardSnapshot,
|
|
226
|
+
);
|
|
227
|
+
const targetAccess = getSelectionTargetAccess(input, workflowPosture);
|
|
228
|
+
const canReviewSuggestion = workflowPosture.mode === "edit" || workflowPosture.mode === "suggest";
|
|
229
|
+
const canAddComment = targetAccess === "view-only" || targetAccess === "blocked"
|
|
230
|
+
? false
|
|
231
|
+
: capabilities.canAddComment;
|
|
232
|
+
const disabledReason = resolveToolDisabledReason(
|
|
233
|
+
workflowPosture.disabledReason,
|
|
234
|
+
canAddComment ? undefined : addCommentDisabledReason,
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
kind: "suggestion-review",
|
|
239
|
+
suggestionId: focusedSuggestion.suggestionId,
|
|
240
|
+
changeIds: focusedSuggestion.changeIds,
|
|
241
|
+
kindLabel: getSuggestionKindLabel(focusedSuggestion.kind),
|
|
242
|
+
previewText:
|
|
243
|
+
focusedSuggestion.excerpt ??
|
|
244
|
+
focusedSuggestion.detail ??
|
|
245
|
+
"Suggested change",
|
|
246
|
+
badges,
|
|
247
|
+
canAccept: canReviewSuggestion && capabilities.canAcceptChange && focusedSuggestion.canAccept,
|
|
248
|
+
canReject: canReviewSuggestion && capabilities.canRejectChange && focusedSuggestion.canReject,
|
|
249
|
+
canEditSuggestion: canReviewSuggestion && focusedSuggestion.editable,
|
|
250
|
+
canAddComment,
|
|
251
|
+
...(disabledReason ? { disabledReason } : {}),
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function selectionMatchesSuggestion(
|
|
256
|
+
selection: SelectionToolResolverInput["snapshot"]["selection"],
|
|
257
|
+
suggestion: SuggestionEntrySnapshot,
|
|
258
|
+
): boolean {
|
|
259
|
+
if (suggestion.anchor.kind !== "range" || selection.activeRange.kind !== "range") {
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
if (selection.isCollapsed) {
|
|
263
|
+
const point = selection.activeRange.from;
|
|
264
|
+
return point >= suggestion.anchor.from && point <= suggestion.anchor.to;
|
|
265
|
+
}
|
|
266
|
+
const selectionFrom = Math.min(selection.activeRange.from, selection.activeRange.to);
|
|
267
|
+
const selectionTo = Math.max(selection.activeRange.from, selection.activeRange.to);
|
|
268
|
+
return rangesOverlap(
|
|
269
|
+
selectionFrom,
|
|
270
|
+
selectionTo,
|
|
271
|
+
suggestion.anchor.from,
|
|
272
|
+
suggestion.anchor.to,
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function findSelectionMatchingScope(
|
|
277
|
+
activeRange: SelectionToolResolverInput["snapshot"]["selection"]["activeRange"] | null,
|
|
278
|
+
activeStory: EditorStoryTarget,
|
|
279
|
+
workflowScopeSnapshot?: WorkflowScopeSnapshot | null,
|
|
280
|
+
) {
|
|
281
|
+
if (!activeRange || activeRange.kind !== "range" || !workflowScopeSnapshot) {
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
return workflowScopeSnapshot.scopes.find((scope) => {
|
|
285
|
+
const scopeStoryTarget = scope.storyTarget ?? { kind: "main" as const };
|
|
286
|
+
if (!storyTargetsEqual(scopeStoryTarget, activeStory)) {
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
if (scope.anchor.kind === "detached") {
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
292
|
+
const scopeFrom = scope.anchor.kind === "range" ? scope.anchor.from : scope.anchor.at;
|
|
293
|
+
const scopeTo = scope.anchor.kind === "range" ? scope.anchor.to : scope.anchor.at;
|
|
294
|
+
return activeRange.from >= scopeFrom && activeRange.to <= scopeTo;
|
|
295
|
+
}) ?? null;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export function buildStructureContextSelectionToolModel(
|
|
299
|
+
input: SelectionToolResolverInput,
|
|
300
|
+
): StructureContextSelectionToolModel | null {
|
|
301
|
+
const workflowPosture = resolveSelectionWorkflowPosture(
|
|
302
|
+
input.snapshot,
|
|
303
|
+
input.viewState.activeStory,
|
|
304
|
+
input.workflowScopeSnapshot,
|
|
305
|
+
input.interactionGuardSnapshot,
|
|
306
|
+
);
|
|
307
|
+
const structureCapability = getCommandCapability(input, "structure", workflowPosture);
|
|
308
|
+
const canMutate = structureCapability.supported;
|
|
309
|
+
const badges = [
|
|
310
|
+
createSelectionToolStoryBadge(input.viewState.activeStory),
|
|
311
|
+
createSelectionToolWorkflowBadge(workflowPosture),
|
|
312
|
+
].filter((badge): badge is SelectionToolBadge => Boolean(badge));
|
|
313
|
+
const disabledReason = resolveToolDisabledReason(
|
|
314
|
+
canMutate ? undefined : structureCapability.blockedReason,
|
|
315
|
+
canMutate ? undefined : workflowPosture.disabledReason,
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
if (!structureCapability.supported) {
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (input.activeImageContext) {
|
|
323
|
+
return {
|
|
324
|
+
kind: "structure-context",
|
|
325
|
+
structureKind: "image",
|
|
326
|
+
badges,
|
|
327
|
+
activeImage: input.activeImageContext,
|
|
328
|
+
canMutate,
|
|
329
|
+
...(disabledReason ? { disabledReason } : {}),
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (input.activeObjectContext) {
|
|
334
|
+
return {
|
|
335
|
+
kind: "structure-context",
|
|
336
|
+
structureKind: "object",
|
|
337
|
+
badges,
|
|
338
|
+
activeObject: input.activeObjectContext,
|
|
339
|
+
canMutate,
|
|
340
|
+
...(disabledReason ? { disabledReason } : {}),
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const isTableContext = Boolean(
|
|
345
|
+
input.formattingState?.breadcrumb.some(
|
|
346
|
+
(item) => item.kind === "table" || item.kind === "table_cell" || item.kind === "table_row",
|
|
347
|
+
),
|
|
348
|
+
);
|
|
349
|
+
const isTableTextRangeContext =
|
|
350
|
+
input.activeTableContext?.selectionKind === "text" &&
|
|
351
|
+
!input.snapshot.selection.isCollapsed;
|
|
352
|
+
if (isTableTextRangeContext) {
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
if (isTableContext) {
|
|
356
|
+
return {
|
|
357
|
+
kind: "structure-context",
|
|
358
|
+
structureKind: "table",
|
|
359
|
+
badges,
|
|
360
|
+
tableStyles: input.styleCatalog.tables,
|
|
361
|
+
activeTable: input.activeTableContext ?? null,
|
|
362
|
+
canMutate,
|
|
363
|
+
...(disabledReason ? { disabledReason } : {}),
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (Boolean(input.preferListStructureContext) && Boolean(input.viewState.activeListContext?.isOrdered)) {
|
|
368
|
+
return {
|
|
369
|
+
kind: "structure-context",
|
|
370
|
+
structureKind: "list",
|
|
371
|
+
badges,
|
|
372
|
+
activeListContext: input.viewState.activeListContext,
|
|
373
|
+
canMutate,
|
|
374
|
+
...(disabledReason ? { disabledReason } : {}),
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
export function buildWorkflowTaskSelectionToolModel(
|
|
382
|
+
input: SelectionToolResolverInput,
|
|
383
|
+
): WorkflowTaskSelectionToolModel | null {
|
|
384
|
+
const workflowPosture = resolveSelectionWorkflowPosture(
|
|
385
|
+
input.snapshot,
|
|
386
|
+
input.viewState.activeStory,
|
|
387
|
+
input.workflowScopeSnapshot,
|
|
388
|
+
input.interactionGuardSnapshot,
|
|
389
|
+
);
|
|
390
|
+
const targetAccess = getSelectionTargetAccess(input, workflowPosture);
|
|
391
|
+
const activeWorkItem = input.workflowScopeSnapshot?.activeWorkItem;
|
|
392
|
+
if (!activeWorkItem || targetAccess === "direct-edit" || targetAccess === "blocked") {
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return {
|
|
397
|
+
kind: "workflow-task",
|
|
398
|
+
badges: [
|
|
399
|
+
{ label: activeWorkItem.title, tone: "accent" },
|
|
400
|
+
...(
|
|
401
|
+
workflowPosture.mode === "suggest"
|
|
402
|
+
? [{ label: "Suggest", tone: "accent" as const }]
|
|
403
|
+
: workflowPosture.mode === "comment"
|
|
404
|
+
? [{ label: "Comment only", tone: "accent" as const }]
|
|
405
|
+
: workflowPosture.mode === "view"
|
|
406
|
+
? [{ label: "View only" }]
|
|
407
|
+
: [{ label: "Blocked" }]
|
|
408
|
+
),
|
|
409
|
+
],
|
|
410
|
+
workflowTitle: activeWorkItem.title,
|
|
411
|
+
workflowDetail: activeWorkItem.description,
|
|
412
|
+
...(workflowPosture.disabledReason ? { disabledReason: workflowPosture.disabledReason } : {}),
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
export function buildCommentThreadSelectionToolModel(
|
|
417
|
+
input: SelectionToolResolverInput,
|
|
418
|
+
): CommentThreadSelectionToolModel | null {
|
|
419
|
+
const workflowPosture = resolveSelectionWorkflowPosture(
|
|
420
|
+
input.snapshot,
|
|
421
|
+
input.viewState.activeStory,
|
|
422
|
+
input.workflowScopeSnapshot,
|
|
423
|
+
input.interactionGuardSnapshot,
|
|
424
|
+
);
|
|
425
|
+
const targetAccess = getSelectionTargetAccess(input, workflowPosture);
|
|
426
|
+
if (!input.activeCommentThread || !input.snapshot.selection.isCollapsed) {
|
|
427
|
+
return null;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const canAddComment = targetAccess !== "view-only" &&
|
|
431
|
+
targetAccess !== "blocked" &&
|
|
432
|
+
input.capabilities.canAddComment;
|
|
433
|
+
return {
|
|
434
|
+
kind: "comment-thread",
|
|
435
|
+
badges: [
|
|
436
|
+
{ label: input.activeCommentThread.status === "open" ? "Comment" : "Resolved comment", tone: "accent" },
|
|
437
|
+
],
|
|
438
|
+
previewText: input.activeCommentThread.excerpt,
|
|
439
|
+
thread: input.activeCommentThread,
|
|
440
|
+
canAddComment,
|
|
441
|
+
...(workflowPosture.disabledReason ? { disabledReason: workflowPosture.disabledReason } : {}),
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
export function buildBlockedExplainerSelectionToolModel(
|
|
446
|
+
input: SelectionToolResolverInput,
|
|
447
|
+
): BlockedExplainerSelectionToolModel | null {
|
|
448
|
+
const workflowPosture = resolveSelectionWorkflowPosture(
|
|
449
|
+
input.snapshot,
|
|
450
|
+
input.viewState.activeStory,
|
|
451
|
+
input.workflowScopeSnapshot,
|
|
452
|
+
input.interactionGuardSnapshot,
|
|
453
|
+
);
|
|
454
|
+
|
|
455
|
+
if (!workflowPosture.disabledReason) {
|
|
456
|
+
return null;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (workflowPosture.mode === "edit") {
|
|
460
|
+
return null;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
return {
|
|
464
|
+
kind: "blocked-explainer",
|
|
465
|
+
badges: [
|
|
466
|
+
createSelectionToolStoryBadge(input.viewState.activeStory),
|
|
467
|
+
createSelectionToolWorkflowBadge(workflowPosture),
|
|
468
|
+
].filter((badge): badge is SelectionToolBadge => Boolean(badge)),
|
|
469
|
+
disabledReason: workflowPosture.disabledReason,
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function createSelectionToolStoryBadge(target: EditorStoryTarget): SelectionToolBadge | null {
|
|
474
|
+
if (target.kind === "main") {
|
|
475
|
+
return null;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
return {
|
|
479
|
+
label:
|
|
480
|
+
target.kind === "header"
|
|
481
|
+
? target.variant === "default"
|
|
482
|
+
? "Header"
|
|
483
|
+
: `Header ${target.variant}`
|
|
484
|
+
: target.kind === "footer"
|
|
485
|
+
? target.variant === "default"
|
|
486
|
+
? "Footer"
|
|
487
|
+
: `Footer ${target.variant}`
|
|
488
|
+
: target.kind === "footnote"
|
|
489
|
+
? "Footnote"
|
|
490
|
+
: "Endnote",
|
|
491
|
+
tone: "accent",
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function createSelectionToolStyleBadge(
|
|
496
|
+
paragraphStyles: Array<{ styleId: string; displayName: string; isDefault: boolean }>,
|
|
497
|
+
formattingState: FormattingStateSnapshot,
|
|
498
|
+
): SelectionToolBadge | null {
|
|
499
|
+
if (!formattingState.paragraphStyleId) {
|
|
500
|
+
return null;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const styleEntry = paragraphStyles.find(
|
|
504
|
+
(entry) => entry.styleId === formattingState.paragraphStyleId,
|
|
505
|
+
);
|
|
506
|
+
if (!styleEntry || styleEntry.isDefault) {
|
|
507
|
+
return null;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
return { label: styleEntry.displayName };
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function createSelectionToolListBadge(
|
|
514
|
+
activeListContext: SelectionToolResolverInput["viewState"]["activeListContext"],
|
|
515
|
+
): SelectionToolBadge | null {
|
|
516
|
+
if (!activeListContext) {
|
|
517
|
+
return null;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
return {
|
|
521
|
+
label: activeListContext.isOrdered ? "Numbered list" : "Bulleted list",
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function createSelectionToolWorkflowBadge(
|
|
526
|
+
posture: ReturnType<typeof resolveSelectionWorkflowPosture>,
|
|
527
|
+
): SelectionToolBadge | null {
|
|
528
|
+
switch (posture.mode) {
|
|
529
|
+
case "suggest":
|
|
530
|
+
return { label: "Suggest", tone: "accent" };
|
|
531
|
+
case "comment":
|
|
532
|
+
return { label: "Comment only", tone: "accent" };
|
|
533
|
+
case "view":
|
|
534
|
+
return { label: "View only" };
|
|
535
|
+
case "blocked":
|
|
536
|
+
return { label: "Blocked" };
|
|
537
|
+
default:
|
|
538
|
+
return null;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function getSelectionTargetAccess(
|
|
543
|
+
input: SelectionToolResolverInput,
|
|
544
|
+
workflowPosture: ReturnType<typeof resolveSelectionWorkflowPosture>,
|
|
545
|
+
): NonNullable<NonNullable<SelectionToolResolverInput["interactionGuardSnapshot"]>["targetAccess"]> {
|
|
546
|
+
if (input.interactionGuardSnapshot?.targetAccess) {
|
|
547
|
+
return input.interactionGuardSnapshot.targetAccess;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
switch (workflowPosture.mode) {
|
|
551
|
+
case "suggest":
|
|
552
|
+
return "suggest";
|
|
553
|
+
case "comment":
|
|
554
|
+
return "comment-only";
|
|
555
|
+
case "view":
|
|
556
|
+
return "view-only";
|
|
557
|
+
case "blocked":
|
|
558
|
+
return "blocked";
|
|
559
|
+
default:
|
|
560
|
+
return "direct-edit";
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function getCommandCapability(
|
|
565
|
+
input: SelectionToolResolverInput,
|
|
566
|
+
family: "text" | "formatting" | "structure",
|
|
567
|
+
workflowPosture: ReturnType<typeof resolveSelectionWorkflowPosture>,
|
|
568
|
+
): {
|
|
569
|
+
supported: boolean;
|
|
570
|
+
blockedReason?: string;
|
|
571
|
+
} {
|
|
572
|
+
const explicitCapability = input.interactionGuardSnapshot?.commandCapabilities?.find(
|
|
573
|
+
(entry) => entry.family === family,
|
|
574
|
+
);
|
|
575
|
+
if (explicitCapability) {
|
|
576
|
+
return {
|
|
577
|
+
supported: explicitCapability.supported,
|
|
578
|
+
blockedReason: explicitCapability.blockedReasons[0]?.message,
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const targetAccess = getSelectionTargetAccess(input, workflowPosture);
|
|
583
|
+
const supported = Boolean(input.capabilities.canEdit) && targetAccess === "direct-edit";
|
|
584
|
+
return {
|
|
585
|
+
supported,
|
|
586
|
+
blockedReason: supported ? undefined : input.interactionGuardSnapshot?.disabledReason,
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function resolveToolDisabledReason(
|
|
591
|
+
...reasons: Array<string | null | undefined>
|
|
592
|
+
): string | undefined {
|
|
593
|
+
return reasons.find((reason): reason is string => typeof reason === "string" && reason.length > 0);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
export function resolveSelectionWorkflowPosture(
|
|
597
|
+
snapshot: SelectionToolResolverInput["snapshot"],
|
|
598
|
+
activeStory: EditorStoryTarget,
|
|
599
|
+
workflowScopeSnapshot?: WorkflowScopeSnapshot | null,
|
|
600
|
+
interactionGuardSnapshot?: SelectionToolResolverInput["interactionGuardSnapshot"],
|
|
601
|
+
): {
|
|
602
|
+
mode: "edit" | "suggest" | "comment" | "view" | "blocked";
|
|
603
|
+
disabledReason?: string;
|
|
604
|
+
} {
|
|
605
|
+
const blockedReasons =
|
|
606
|
+
interactionGuardSnapshot?.blockedReasons ??
|
|
607
|
+
workflowScopeSnapshot?.blockedReasons ??
|
|
608
|
+
[];
|
|
609
|
+
const blockingReason = blockedReasons[0];
|
|
610
|
+
if (blockingReason) {
|
|
611
|
+
if (blockingReason.code === "workflow_comment_only") {
|
|
612
|
+
return {
|
|
613
|
+
mode: "comment",
|
|
614
|
+
disabledReason: blockingReason.message,
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
if (blockingReason.code === "workflow_view_only") {
|
|
618
|
+
return {
|
|
619
|
+
mode: "view",
|
|
620
|
+
disabledReason: blockingReason.message,
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
return {
|
|
624
|
+
mode: "blocked",
|
|
625
|
+
disabledReason: blockingReason.message,
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
if (interactionGuardSnapshot) {
|
|
630
|
+
if (interactionGuardSnapshot.effectiveMode === "suggest") {
|
|
631
|
+
return {
|
|
632
|
+
mode: "suggest",
|
|
633
|
+
disabledReason:
|
|
634
|
+
interactionGuardSnapshot.disabledReason ??
|
|
635
|
+
"Suggestion authoring is active here; direct formatting changes are blocked.",
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
if (interactionGuardSnapshot.effectiveMode === "comment") {
|
|
639
|
+
return {
|
|
640
|
+
mode: "comment",
|
|
641
|
+
disabledReason: interactionGuardSnapshot.disabledReason,
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
if (interactionGuardSnapshot.effectiveMode === "view") {
|
|
645
|
+
return {
|
|
646
|
+
mode: "view",
|
|
647
|
+
disabledReason: interactionGuardSnapshot.disabledReason,
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
if (interactionGuardSnapshot.effectiveMode === "blocked") {
|
|
651
|
+
return {
|
|
652
|
+
mode: "blocked",
|
|
653
|
+
disabledReason: interactionGuardSnapshot.disabledReason,
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
const activeRange =
|
|
659
|
+
!snapshot.selection.isCollapsed && snapshot.selection.activeRange.kind === "range"
|
|
660
|
+
? snapshot.selection.activeRange
|
|
661
|
+
: null;
|
|
662
|
+
const matchingScope = activeRange && workflowScopeSnapshot
|
|
663
|
+
? workflowScopeSnapshot.scopes.find((scope) => {
|
|
664
|
+
const scopeStoryTarget = scope.storyTarget ?? { kind: "main" as const };
|
|
665
|
+
if (!storyTargetsEqual(scopeStoryTarget, activeStory)) {
|
|
666
|
+
return false;
|
|
667
|
+
}
|
|
668
|
+
if (scope.anchor.kind === "detached") {
|
|
669
|
+
return false;
|
|
670
|
+
}
|
|
671
|
+
const scopeFrom = scope.anchor.kind === "range" ? scope.anchor.from : scope.anchor.at;
|
|
672
|
+
const scopeTo = scope.anchor.kind === "range" ? scope.anchor.to : scope.anchor.at;
|
|
673
|
+
return activeRange.from >= scopeFrom && activeRange.to <= scopeTo;
|
|
674
|
+
})
|
|
675
|
+
: null;
|
|
676
|
+
|
|
677
|
+
if (matchingScope?.mode === "suggest") {
|
|
678
|
+
return {
|
|
679
|
+
mode: "suggest",
|
|
680
|
+
disabledReason: "Suggestion authoring is active here; direct formatting changes are blocked.",
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
if (matchingScope?.mode === "comment") {
|
|
684
|
+
return {
|
|
685
|
+
mode: "comment",
|
|
686
|
+
disabledReason: `Scope "${matchingScope.label ?? matchingScope.scopeId}" allows comments only.`,
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
if (matchingScope?.mode === "view") {
|
|
690
|
+
return {
|
|
691
|
+
mode: "view",
|
|
692
|
+
disabledReason: `Scope "${matchingScope.label ?? matchingScope.scopeId}" is view-only.`,
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
return { mode: "edit" };
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function summarizeSelectionPreview(snapshot: SelectionToolResolverInput["snapshot"]): string | null {
|
|
699
|
+
if (!snapshot.surface || snapshot.selection.isCollapsed) {
|
|
700
|
+
return null;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
const range = snapshot.selection.activeRange;
|
|
704
|
+
if (range.kind !== "range") {
|
|
705
|
+
return "Selected range";
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
const preview = snapshot.surface.plainText
|
|
709
|
+
.slice(range.from, range.to)
|
|
710
|
+
.replace(/\s+/g, " ")
|
|
711
|
+
.trim();
|
|
712
|
+
|
|
713
|
+
if (!preview) {
|
|
714
|
+
return "Selected range";
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
return preview.length > 48 ? `${preview.slice(0, 45)}...` : preview;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
function rangesOverlap(
|
|
721
|
+
leftFrom: number,
|
|
722
|
+
leftTo: number,
|
|
723
|
+
rightFrom: number,
|
|
724
|
+
rightTo: number,
|
|
725
|
+
): boolean {
|
|
726
|
+
return leftFrom < rightTo && rightFrom < leftTo;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
function getSuggestionKindLabel(
|
|
730
|
+
kind: TrackedChangeEntrySnapshot["kind"] | SuggestionEntrySnapshot["kind"],
|
|
731
|
+
): string {
|
|
732
|
+
switch (kind) {
|
|
733
|
+
case "insertion":
|
|
734
|
+
return "Suggested insertion";
|
|
735
|
+
case "deletion":
|
|
736
|
+
return "Suggested deletion";
|
|
737
|
+
case "replacement":
|
|
738
|
+
return "Suggested replacement";
|
|
739
|
+
case "formatting":
|
|
740
|
+
case "formatting-change":
|
|
741
|
+
return "Suggested formatting change";
|
|
742
|
+
case "property-change":
|
|
743
|
+
case "paragraph-property-change":
|
|
744
|
+
return "Suggested property change";
|
|
745
|
+
case "move":
|
|
746
|
+
return "Suggested move";
|
|
747
|
+
case "structural-change":
|
|
748
|
+
return "Suggested structural change";
|
|
749
|
+
case "object-change":
|
|
750
|
+
return "Suggested object change";
|
|
751
|
+
}
|
|
752
|
+
}
|