@beyondwork/docx-react-component 1.0.22 → 1.0.24-rc
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/README.md +81 -38
- package/package.json +1 -1
- package/src/api/public-types.ts +67 -1
- package/src/core/commands/index.ts +625 -5
- package/src/index.ts +5 -0
- package/src/io/docx-session.ts +181 -2
- package/src/io/export/serialize-main-document.ts +21 -1
- package/src/io/normalize/normalize-text.ts +4 -0
- package/src/io/ooxml/parse-main-document.ts +88 -7
- package/src/model/canonical-document.ts +22 -0
- package/src/review/store/revision-store.ts +1 -0
- package/src/review/store/revision-types.ts +2 -0
- package/src/runtime/document-runtime.ts +503 -51
- package/src/runtime/session-capabilities.ts +6 -5
- package/src/runtime/surface-projection.ts +2 -0
- package/src/runtime/table-schema.ts +2 -0
- package/src/runtime/workflow-markup.ts +5 -1
- package/src/ui/WordReviewEditor.tsx +667 -132
- package/src/ui/editor-runtime-boundary.ts +10 -1
- package/src/ui/editor-shell-view.tsx +8 -0
- package/src/ui/editor-surface-controller.tsx +6 -0
- package/src/ui/headless/selection-toolbar-model.ts +12 -0
- package/src/ui-tailwind/chrome/tw-suggestion-card.tsx +139 -0
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +6 -0
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +96 -28
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +2 -0
- package/src/ui-tailwind/editor-surface/surface-build-keys.ts +6 -0
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +132 -10
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +82 -1
- package/src/ui-tailwind/status/tw-status-bar.tsx +4 -1
- package/src/ui-tailwind/theme/editor-theme.css +10 -0
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +21 -5
- package/src/ui-tailwind/tw-review-workspace.tsx +110 -32
|
@@ -75,6 +75,7 @@ export interface TwProseMirrorSurfaceProps {
|
|
|
75
75
|
markupDisplay: MarkupDisplay;
|
|
76
76
|
activeRevisionId?: string;
|
|
77
77
|
showTrackedChanges?: boolean;
|
|
78
|
+
suggestionsEnabled?: boolean;
|
|
78
79
|
/** When true, the surface renders inside the page workspace (vs canvas). */
|
|
79
80
|
isPageWorkspace?: boolean;
|
|
80
81
|
onFocus: FocusEventHandler<HTMLDivElement>;
|
|
@@ -87,6 +88,9 @@ export interface TwProseMirrorSurfaceProps {
|
|
|
87
88
|
onOutdentTab?: () => void;
|
|
88
89
|
onInsertHardBreak?: () => void;
|
|
89
90
|
onSplitParagraph?: () => void;
|
|
91
|
+
onUndo?: () => void;
|
|
92
|
+
onRedo?: () => void;
|
|
93
|
+
onBlockedInput?: (command: "paste" | "drop", message: string) => void;
|
|
90
94
|
onCommentActivated?: (commentId: string) => void;
|
|
91
95
|
onRevisionActivated?: (revisionId: string) => void;
|
|
92
96
|
onSelectionToolbarAnchorChange?: (anchor: SelectionToolbarAnchor | null) => void;
|
|
@@ -94,6 +98,8 @@ export interface TwProseMirrorSurfaceProps {
|
|
|
94
98
|
workflowScopes?: readonly WorkflowScope[];
|
|
95
99
|
workflowCandidates?: readonly WorkflowCandidateRange[];
|
|
96
100
|
workflowBlockedReasons?: readonly WorkflowBlockedCommandReason[];
|
|
101
|
+
activeWorkflowWorkItemId?: string | null;
|
|
102
|
+
activeWorkflowScopeIds?: readonly string[];
|
|
97
103
|
}
|
|
98
104
|
|
|
99
105
|
export interface TwProseMirrorSurfaceRef {
|
|
@@ -161,8 +167,11 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
161
167
|
onInsertHardBreak: () => props.onInsertHardBreak?.(),
|
|
162
168
|
onInsertTab: () => props.onInsertTab?.(),
|
|
163
169
|
onOutdentTab: () => props.onOutdentTab?.(),
|
|
164
|
-
onUndo: () =>
|
|
165
|
-
onRedo: () =>
|
|
170
|
+
onUndo: () => props.onUndo?.(),
|
|
171
|
+
onRedo: () => props.onRedo?.(),
|
|
172
|
+
onBlockedInput: (command, message) => {
|
|
173
|
+
props.onBlockedInput?.(command, message);
|
|
174
|
+
},
|
|
166
175
|
onSelectionChange: (sel) => {
|
|
167
176
|
pendingSelectionProbeRef.current = startPerfProbe("selection");
|
|
168
177
|
props.onSelectionChange?.(
|
|
@@ -196,6 +205,7 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
196
205
|
}),
|
|
197
206
|
[mediaPreviewKey, snapshot.activeStory, surface],
|
|
198
207
|
);
|
|
208
|
+
const suggestionsEnabled = props.suggestionsEnabled === true;
|
|
199
209
|
const decorationBuildKey = useMemo(
|
|
200
210
|
() =>
|
|
201
211
|
createSurfaceDecorationKey({
|
|
@@ -204,9 +214,12 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
204
214
|
canEdit,
|
|
205
215
|
activeCommentId: snapshot.comments.activeCommentId,
|
|
206
216
|
activeRevisionId: props.activeRevisionId,
|
|
207
|
-
workflowScopeSignature:
|
|
208
|
-
workflowCandidateSignature:
|
|
209
|
-
workflowBlockedSignature:
|
|
217
|
+
workflowScopeSignature: createWorkflowScopeSignature(props.workflowScopes),
|
|
218
|
+
workflowCandidateSignature: createWorkflowCandidateSignature(props.workflowCandidates),
|
|
219
|
+
workflowBlockedSignature: createWorkflowBlockedSignature(props.workflowBlockedReasons),
|
|
220
|
+
activeWorkflowWorkItemId: props.activeWorkflowWorkItemId ?? null,
|
|
221
|
+
activeWorkflowScopeIds: props.activeWorkflowScopeIds ?? [],
|
|
222
|
+
suggestionsEnabled,
|
|
210
223
|
}),
|
|
211
224
|
[
|
|
212
225
|
canEdit,
|
|
@@ -214,9 +227,12 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
214
227
|
props.activeRevisionId,
|
|
215
228
|
props.workflowCandidates,
|
|
216
229
|
props.workflowBlockedReasons,
|
|
230
|
+
props.activeWorkflowWorkItemId,
|
|
231
|
+
props.activeWorkflowScopeIds,
|
|
217
232
|
props.workflowScopes,
|
|
218
233
|
showTrackedChanges,
|
|
219
234
|
snapshot.comments.activeCommentId,
|
|
235
|
+
suggestionsEnabled,
|
|
220
236
|
],
|
|
221
237
|
);
|
|
222
238
|
|
|
@@ -233,6 +249,7 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
233
249
|
onOutdentTab: () => callbacksRef.current?.onOutdentTab?.(),
|
|
234
250
|
onUndo: () => callbacksRef.current?.onUndo(),
|
|
235
251
|
onRedo: () => callbacksRef.current?.onRedo(),
|
|
252
|
+
onBlockedInput: (command, message) => callbacksRef.current?.onBlockedInput?.(command, message),
|
|
236
253
|
onSelectionChange: (sel) => callbacksRef.current?.onSelectionChange(sel),
|
|
237
254
|
getPositionMap: () => callbacksRef.current?.getPositionMap() ?? null,
|
|
238
255
|
isSelectionSyncSuppressed: () =>
|
|
@@ -259,6 +276,9 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
259
276
|
snapshot.activeStory,
|
|
260
277
|
props.workflowCandidates,
|
|
261
278
|
props.workflowBlockedReasons,
|
|
279
|
+
props.activeWorkflowWorkItemId,
|
|
280
|
+
props.activeWorkflowScopeIds,
|
|
281
|
+
suggestionsEnabled,
|
|
262
282
|
);
|
|
263
283
|
view.setProps({
|
|
264
284
|
editable: () => canEdit,
|
|
@@ -273,13 +293,13 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
273
293
|
commentModel,
|
|
274
294
|
decorationBuildKey,
|
|
275
295
|
markupDisplay,
|
|
276
|
-
|
|
277
|
-
|
|
296
|
+
props.activeWorkflowScopeIds,
|
|
297
|
+
props.activeWorkflowWorkItemId,
|
|
278
298
|
props.workflowBlockedReasons,
|
|
279
299
|
props.workflowCandidates,
|
|
280
300
|
props.workflowScopes,
|
|
281
|
-
|
|
282
|
-
|
|
301
|
+
revisionModel,
|
|
302
|
+
showTrackedChanges,
|
|
283
303
|
],
|
|
284
304
|
);
|
|
285
305
|
|
|
@@ -309,6 +329,8 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
309
329
|
snapshot.activeStory,
|
|
310
330
|
props.workflowCandidates,
|
|
311
331
|
props.workflowBlockedReasons,
|
|
332
|
+
props.activeWorkflowWorkItemId,
|
|
333
|
+
props.activeWorkflowScopeIds,
|
|
312
334
|
);
|
|
313
335
|
recordPerfSample("pm.rebuild");
|
|
314
336
|
incrementInvalidationCounter("pm.laneA.rebuilds");
|
|
@@ -371,6 +393,20 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
371
393
|
applyDecorationProps(view, positionMap);
|
|
372
394
|
}, [applyDecorationProps, decorationBuildKey, surface]);
|
|
373
395
|
|
|
396
|
+
useEffect(() => {
|
|
397
|
+
if (!activeSearchRef.current || !surface) {
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
applySearch(activeSearchRef.current.query, activeSearchRef.current.options);
|
|
401
|
+
}, [
|
|
402
|
+
markupDisplay,
|
|
403
|
+
props.canonicalDocument,
|
|
404
|
+
props.documentNavigation,
|
|
405
|
+
snapshot.activeStory,
|
|
406
|
+
snapshot.trackedChanges,
|
|
407
|
+
surface,
|
|
408
|
+
]);
|
|
409
|
+
|
|
374
410
|
useEffect(() => {
|
|
375
411
|
const view = viewRef.current;
|
|
376
412
|
const positionMap = positionMapRef.current;
|
|
@@ -442,7 +478,15 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
442
478
|
return getTableSelectionDescriptor(view.state);
|
|
443
479
|
},
|
|
444
480
|
}),
|
|
445
|
-
[
|
|
481
|
+
[
|
|
482
|
+
markupDisplay,
|
|
483
|
+
props.canonicalDocument,
|
|
484
|
+
props.documentNavigation,
|
|
485
|
+
snapshot.activeStory,
|
|
486
|
+
snapshot.selection,
|
|
487
|
+
snapshot.surface,
|
|
488
|
+
snapshot.trackedChanges,
|
|
489
|
+
],
|
|
446
490
|
);
|
|
447
491
|
|
|
448
492
|
function applySearch(query: string, options: SearchOptions): SearchResultSnapshot[] {
|
|
@@ -788,6 +832,84 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
788
832
|
}
|
|
789
833
|
});
|
|
790
834
|
|
|
835
|
+
function createWorkflowScopeSignature(scopes: readonly WorkflowScope[] | undefined): string {
|
|
836
|
+
if (!scopes || scopes.length === 0) {
|
|
837
|
+
return "";
|
|
838
|
+
}
|
|
839
|
+
return scopes.map((scope) =>
|
|
840
|
+
[
|
|
841
|
+
scope.scopeId,
|
|
842
|
+
scope.mode,
|
|
843
|
+
scope.workItemId ?? "",
|
|
844
|
+
serializeAnchorSignature(scope.anchor),
|
|
845
|
+
serializeStoryTargetSignature(scope.storyTarget),
|
|
846
|
+
].join(":")
|
|
847
|
+
).join("|");
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
function createWorkflowCandidateSignature(
|
|
851
|
+
candidates: readonly WorkflowCandidateRange[] | undefined,
|
|
852
|
+
): string {
|
|
853
|
+
if (!candidates || candidates.length === 0) {
|
|
854
|
+
return "";
|
|
855
|
+
}
|
|
856
|
+
return candidates.map((candidate) =>
|
|
857
|
+
[
|
|
858
|
+
candidate.candidateId,
|
|
859
|
+
serializeAnchorSignature(candidate.anchor),
|
|
860
|
+
serializeStoryTargetSignature(candidate.storyTarget),
|
|
861
|
+
candidate.source ?? "",
|
|
862
|
+
].join(":")
|
|
863
|
+
).join("|");
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
function createWorkflowBlockedSignature(
|
|
867
|
+
blockedReasons: readonly WorkflowBlockedCommandReason[] | undefined,
|
|
868
|
+
): string {
|
|
869
|
+
if (!blockedReasons || blockedReasons.length === 0) {
|
|
870
|
+
return "";
|
|
871
|
+
}
|
|
872
|
+
return blockedReasons.map((reason) =>
|
|
873
|
+
[
|
|
874
|
+
reason.code,
|
|
875
|
+
reason.scopeId ?? "",
|
|
876
|
+
reason.workItemId ?? "",
|
|
877
|
+
serializeAnchorSignature(reason.anchor),
|
|
878
|
+
serializeStoryTargetSignature(reason.storyTarget),
|
|
879
|
+
].join(":")
|
|
880
|
+
).join("|");
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
function serializeAnchorSignature(anchor: WorkflowScope["anchor"] | WorkflowCandidateRange["anchor"] | WorkflowBlockedCommandReason["anchor"] | undefined): string {
|
|
884
|
+
if (!anchor) {
|
|
885
|
+
return "";
|
|
886
|
+
}
|
|
887
|
+
switch (anchor.kind) {
|
|
888
|
+
case "range":
|
|
889
|
+
return `range:${anchor.from}:${anchor.to}:${anchor.assoc.start}:${anchor.assoc.end}`;
|
|
890
|
+
case "node":
|
|
891
|
+
return `node:${anchor.at}:${anchor.assoc}`;
|
|
892
|
+
case "detached":
|
|
893
|
+
return `detached:${anchor.lastKnownRange.from}:${anchor.lastKnownRange.to}:${anchor.reason}`;
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
function serializeStoryTargetSignature(storyTarget: WorkflowScope["storyTarget"] | WorkflowCandidateRange["storyTarget"] | WorkflowBlockedCommandReason["storyTarget"]): string {
|
|
898
|
+
if (!storyTarget) {
|
|
899
|
+
return "";
|
|
900
|
+
}
|
|
901
|
+
switch (storyTarget.kind) {
|
|
902
|
+
case "main":
|
|
903
|
+
return "main";
|
|
904
|
+
case "header":
|
|
905
|
+
case "footer":
|
|
906
|
+
return `${storyTarget.kind}:${storyTarget.relationshipId}:${storyTarget.variant}:${storyTarget.sectionIndex ?? ""}`;
|
|
907
|
+
case "footnote":
|
|
908
|
+
case "endnote":
|
|
909
|
+
return `${storyTarget.kind}:${storyTarget.noteId}`;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
|
|
791
913
|
function buildSelectionToolbarMeasurementKey(
|
|
792
914
|
selection: SelectionSnapshot,
|
|
793
915
|
activeStory: RuntimeRenderSnapshot["activeStory"],
|
|
@@ -33,6 +33,58 @@ interface OpenExplicitRowSpan {
|
|
|
33
33
|
remainingRows: number;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
function readRowPadding(node: PMNode, side: "gridBefore" | "gridAfter"): number {
|
|
37
|
+
const value = node.attrs[side];
|
|
38
|
+
return typeof value === "number" && value > 0 ? value : 0;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function sumGridColumns(
|
|
42
|
+
gridColumns: readonly number[],
|
|
43
|
+
start: number,
|
|
44
|
+
count: number,
|
|
45
|
+
): number {
|
|
46
|
+
let total = 0;
|
|
47
|
+
for (let index = start; index < start + count; index += 1) {
|
|
48
|
+
total += gridColumns[index] ?? 0;
|
|
49
|
+
}
|
|
50
|
+
return total;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function removePaddingCells(rowElement: HTMLTableRowElement): void {
|
|
54
|
+
for (const cell of Array.from(rowElement.cells)) {
|
|
55
|
+
if (cell.hasAttribute("data-row-padding")) {
|
|
56
|
+
cell.remove();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function createPaddingCell(colSpan: number, widthTwips: number): HTMLTableCellElement {
|
|
62
|
+
const cell = document.createElement("td");
|
|
63
|
+
cell.setAttribute("data-row-padding", "true");
|
|
64
|
+
cell.setAttribute("aria-hidden", "true");
|
|
65
|
+
cell.colSpan = Math.max(1, colSpan);
|
|
66
|
+
cell.style.border = "none";
|
|
67
|
+
cell.style.padding = "0";
|
|
68
|
+
cell.style.background = "transparent";
|
|
69
|
+
if (widthTwips > 0) {
|
|
70
|
+
cell.style.width = `${widthTwips / 20}pt`;
|
|
71
|
+
}
|
|
72
|
+
return cell;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function nodesAreOnlyRowPadding(nodes: ArrayLike<Node> & { item?(index: number): Node | null }): boolean {
|
|
76
|
+
for (let index = 0; index < nodes.length; index += 1) {
|
|
77
|
+
const node = nodes.item ? nodes.item(index) : nodes[index];
|
|
78
|
+
if (!node) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (!(node instanceof HTMLElement) || !node.hasAttribute("data-row-padding")) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
|
|
36
88
|
function resolveRenderedColspan(node: PMNode): number {
|
|
37
89
|
const colspan = node.attrs.colspan as number | undefined;
|
|
38
90
|
if (typeof colspan === "number" && colspan > 1) {
|
|
@@ -172,6 +224,7 @@ function computeTableLayout(tableNode: PMNode): TableCellLayout[][] {
|
|
|
172
224
|
function syncRenderedTableLayout(tableBody: HTMLTableSectionElement, tableNode: PMNode): void {
|
|
173
225
|
const rowLayouts = computeTableLayout(tableNode);
|
|
174
226
|
const rowElements = Array.from(tableBody.rows);
|
|
227
|
+
const gridColumns = Array.isArray(tableNode.attrs.gridColumns) ? tableNode.attrs.gridColumns as number[] : [];
|
|
175
228
|
|
|
176
229
|
for (let rowIndex = 0; rowIndex < rowLayouts.length; rowIndex += 1) {
|
|
177
230
|
const rowLayout = rowLayouts[rowIndex];
|
|
@@ -180,6 +233,7 @@ function syncRenderedTableLayout(tableBody: HTMLTableSectionElement, tableNode:
|
|
|
180
233
|
continue;
|
|
181
234
|
}
|
|
182
235
|
|
|
236
|
+
removePaddingCells(rowElement);
|
|
183
237
|
const cellElements = Array.from(rowElement.cells);
|
|
184
238
|
for (const cellLayout of rowLayout) {
|
|
185
239
|
const element = cellElements[cellLayout.cellIndex];
|
|
@@ -199,6 +253,22 @@ function syncRenderedTableLayout(tableBody: HTMLTableSectionElement, tableNode:
|
|
|
199
253
|
element.removeAttribute("data-vertical-merge-hidden");
|
|
200
254
|
}
|
|
201
255
|
}
|
|
256
|
+
|
|
257
|
+
const rowNode = tableNode.child(rowIndex);
|
|
258
|
+
const gridBefore = readRowPadding(rowNode, "gridBefore");
|
|
259
|
+
const gridAfter = readRowPadding(rowNode, "gridAfter");
|
|
260
|
+
if (gridBefore > 0) {
|
|
261
|
+
rowElement.insertBefore(
|
|
262
|
+
createPaddingCell(gridBefore, sumGridColumns(gridColumns, 0, gridBefore)),
|
|
263
|
+
rowElement.firstChild,
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
if (gridAfter > 0) {
|
|
267
|
+
const start = Math.max(0, gridColumns.length - gridAfter);
|
|
268
|
+
rowElement.appendChild(
|
|
269
|
+
createPaddingCell(gridAfter, sumGridColumns(gridColumns, start, gridAfter)),
|
|
270
|
+
);
|
|
271
|
+
}
|
|
202
272
|
}
|
|
203
273
|
}
|
|
204
274
|
|
|
@@ -253,7 +323,18 @@ export class TableNodeView {
|
|
|
253
323
|
}
|
|
254
324
|
|
|
255
325
|
ignoreMutation(record: ViewMutationRecord): boolean {
|
|
256
|
-
|
|
326
|
+
if (record.type === "attributes") {
|
|
327
|
+
return record.target === this.dom
|
|
328
|
+
|| (record.target instanceof HTMLElement && record.target.hasAttribute("data-row-padding"));
|
|
329
|
+
}
|
|
330
|
+
if (record.type === "childList" && this.dom.contains(record.target)) {
|
|
331
|
+
const addedNodes = record.addedNodes as ArrayLike<Node> & { item?(index: number): Node | null };
|
|
332
|
+
const removedNodes = record.removedNodes as ArrayLike<Node> & { item?(index: number): Node | null };
|
|
333
|
+
const addedOkay = (addedNodes?.length ?? 0) === 0 || nodesAreOnlyRowPadding(addedNodes);
|
|
334
|
+
const removedOkay = (removedNodes?.length ?? 0) === 0 || nodesAreOnlyRowPadding(removedNodes);
|
|
335
|
+
return addedOkay && removedOkay;
|
|
336
|
+
}
|
|
337
|
+
return false;
|
|
257
338
|
}
|
|
258
339
|
|
|
259
340
|
private scheduleLayoutSync(): void {
|
|
@@ -22,7 +22,10 @@ export function TwStatusBar(props: TwStatusBarProps) {
|
|
|
22
22
|
: "Ready";
|
|
23
23
|
|
|
24
24
|
return (
|
|
25
|
-
<footer
|
|
25
|
+
<footer
|
|
26
|
+
data-testid="status-bar"
|
|
27
|
+
className="flex h-7 shrink-0 items-center gap-4 border-t border-border px-3 text-xs text-tertiary"
|
|
28
|
+
>
|
|
26
29
|
<span className="flex items-center gap-1.5">
|
|
27
30
|
<span
|
|
28
31
|
className={`inline-block h-1.5 w-1.5 rounded-full ${
|
|
@@ -323,6 +323,16 @@
|
|
|
323
323
|
background: var(--wre-workflow-rail-color, var(--color-border-strong));
|
|
324
324
|
}
|
|
325
325
|
|
|
326
|
+
.prosemirror-surface .ProseMirror .wre-workflow-rail-active::before {
|
|
327
|
+
width: 0.3125rem;
|
|
328
|
+
opacity: 1;
|
|
329
|
+
box-shadow: 0 0 0 1px color-mix(in oklab, var(--wre-workflow-rail-color, var(--color-border-strong)) 30%, transparent);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
.prosemirror-surface .ProseMirror .wre-workflow-inline-active {
|
|
333
|
+
box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--wre-workflow-rail-color, var(--color-border-strong)) 28%, transparent);
|
|
334
|
+
}
|
|
335
|
+
|
|
326
336
|
.prosemirror-surface .ProseMirror .wre-workflow-rail-edit {
|
|
327
337
|
--wre-workflow-rail-color: var(--color-accent);
|
|
328
338
|
background: color-mix(in srgb, var(--color-accent) 7%, transparent);
|
|
@@ -62,6 +62,7 @@ export interface TwToolbarProps {
|
|
|
62
62
|
compatibility?: CompatibilityPanelSnapshot;
|
|
63
63
|
warnings?: EditorWarning[];
|
|
64
64
|
blockedReasons?: WorkflowBlockedCommandReason[];
|
|
65
|
+
interactionPolicy?: ToolbarInteractionPolicy;
|
|
65
66
|
workspaceMode: WorkspaceMode;
|
|
66
67
|
zoomLevel?: ZoomLevel;
|
|
67
68
|
formattingState?: FormattingStateSnapshot;
|
|
@@ -99,6 +100,13 @@ export interface TwToolbarProps {
|
|
|
99
100
|
onShowTrackedChangesChange: (show: boolean) => void;
|
|
100
101
|
}
|
|
101
102
|
|
|
103
|
+
export interface ToolbarInteractionPolicy {
|
|
104
|
+
mode: "edit" | "suggest" | "comment" | "view" | "blocked";
|
|
105
|
+
canFormatText: boolean;
|
|
106
|
+
canInsertStructural: boolean;
|
|
107
|
+
canAddComment: boolean;
|
|
108
|
+
}
|
|
109
|
+
|
|
102
110
|
export function getSupportedZoomPresets(): ReadonlyArray<number> {
|
|
103
111
|
return [75, 100, 125, 150];
|
|
104
112
|
}
|
|
@@ -123,7 +131,9 @@ export function TwToolbar(props: TwToolbarProps) {
|
|
|
123
131
|
const isPageMode = workspaceMode === "page";
|
|
124
132
|
const paragraphStyles = props.styleCatalog?.paragraphs ?? [];
|
|
125
133
|
const zoomLevel = props.zoomLevel ?? 100;
|
|
126
|
-
const canEdit = caps ? caps.canEdit : false;
|
|
134
|
+
const canEdit = props.interactionPolicy?.canFormatText ?? (caps ? caps.canEdit : false);
|
|
135
|
+
const canInsertStructural = props.interactionPolicy?.canInsertStructural ?? canEdit;
|
|
136
|
+
const canAddComment = props.interactionPolicy?.canAddComment ?? (caps ? caps.canAddComment : false);
|
|
127
137
|
const zoomLabel =
|
|
128
138
|
typeof zoomLevel === "number"
|
|
129
139
|
? `${zoomLevel}%`
|
|
@@ -173,14 +183,14 @@ export function TwToolbar(props: TwToolbarProps) {
|
|
|
173
183
|
icon={Bold}
|
|
174
184
|
label="Bold"
|
|
175
185
|
active={props.formattingState?.bold ?? false}
|
|
176
|
-
disabled={
|
|
186
|
+
disabled={!canEdit}
|
|
177
187
|
onClick={props.onToggleBold}
|
|
178
188
|
/>
|
|
179
189
|
<TwToolbarIconButton
|
|
180
190
|
icon={Italic}
|
|
181
191
|
label="Italic"
|
|
182
192
|
active={props.formattingState?.italic ?? false}
|
|
183
|
-
disabled={
|
|
193
|
+
disabled={!canEdit}
|
|
184
194
|
onClick={props.onToggleItalic}
|
|
185
195
|
/>
|
|
186
196
|
<TwToolbarIconButton
|
|
@@ -238,7 +248,7 @@ export function TwToolbar(props: TwToolbarProps) {
|
|
|
238
248
|
onClick={props.onIndent}
|
|
239
249
|
/>
|
|
240
250
|
<ToolbarInsertMenu
|
|
241
|
-
disabled={!
|
|
251
|
+
disabled={!canInsertStructural}
|
|
242
252
|
onInsertImage={props.onInsertImage}
|
|
243
253
|
onInsertPageBreak={props.onInsertPageBreak}
|
|
244
254
|
onInsertSectionBreak={props.onInsertSectionBreak}
|
|
@@ -275,7 +285,7 @@ export function TwToolbar(props: TwToolbarProps) {
|
|
|
275
285
|
<TwToolbarIconButton
|
|
276
286
|
icon={MessageSquare}
|
|
277
287
|
label="Add comment"
|
|
278
|
-
disabled={
|
|
288
|
+
disabled={!canAddComment}
|
|
279
289
|
emphasis
|
|
280
290
|
onClick={props.onAddComment}
|
|
281
291
|
/>
|
|
@@ -554,6 +564,8 @@ function ToolbarParagraphStyleSelect(props: {
|
|
|
554
564
|
>
|
|
555
565
|
<Select.Trigger
|
|
556
566
|
aria-label="Paragraph style"
|
|
567
|
+
aria-disabled={props.disabled || undefined}
|
|
568
|
+
data-disabled={props.disabled ? "" : undefined}
|
|
557
569
|
onMouseDown={preserveEditorSelectionMouseDown}
|
|
558
570
|
className={`inline-flex h-7 min-w-[8.5rem] items-center justify-between gap-2 rounded-md border border-border bg-canvas px-2.5 text-xs font-medium text-primary transition-colors hover:bg-surface outline-none disabled:cursor-not-allowed disabled:opacity-40 ${focusRingClass}`}
|
|
559
571
|
>
|
|
@@ -601,6 +613,8 @@ function ToolbarFontFamilySelect(props: {
|
|
|
601
613
|
>
|
|
602
614
|
<Select.Trigger
|
|
603
615
|
aria-label="Font family"
|
|
616
|
+
aria-disabled={props.disabled || undefined}
|
|
617
|
+
data-disabled={props.disabled ? "" : undefined}
|
|
604
618
|
onMouseDown={preserveEditorSelectionMouseDown}
|
|
605
619
|
className={`inline-flex h-7 min-w-[7rem] items-center justify-between gap-2 rounded-md border border-border bg-canvas px-2 text-xs font-medium text-primary transition-colors hover:bg-surface outline-none disabled:cursor-not-allowed disabled:opacity-40 ${focusRingClass}`}
|
|
606
620
|
>
|
|
@@ -649,6 +663,8 @@ function ToolbarFontSizeSelect(props: {
|
|
|
649
663
|
>
|
|
650
664
|
<Select.Trigger
|
|
651
665
|
aria-label="Font size"
|
|
666
|
+
aria-disabled={props.disabled || undefined}
|
|
667
|
+
data-disabled={props.disabled ? "" : undefined}
|
|
652
668
|
onMouseDown={preserveEditorSelectionMouseDown}
|
|
653
669
|
className={`inline-flex h-7 min-w-[4rem] items-center justify-between gap-2 rounded-md border border-border bg-canvas px-2 text-xs font-medium text-primary transition-colors hover:bg-surface outline-none disabled:cursor-not-allowed disabled:opacity-40 ${focusRingClass}`}
|
|
654
670
|
>
|