@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.
Files changed (33) hide show
  1. package/README.md +81 -38
  2. package/package.json +1 -1
  3. package/src/api/public-types.ts +67 -1
  4. package/src/core/commands/index.ts +625 -5
  5. package/src/index.ts +5 -0
  6. package/src/io/docx-session.ts +181 -2
  7. package/src/io/export/serialize-main-document.ts +21 -1
  8. package/src/io/normalize/normalize-text.ts +4 -0
  9. package/src/io/ooxml/parse-main-document.ts +88 -7
  10. package/src/model/canonical-document.ts +22 -0
  11. package/src/review/store/revision-store.ts +1 -0
  12. package/src/review/store/revision-types.ts +2 -0
  13. package/src/runtime/document-runtime.ts +503 -51
  14. package/src/runtime/session-capabilities.ts +6 -5
  15. package/src/runtime/surface-projection.ts +2 -0
  16. package/src/runtime/table-schema.ts +2 -0
  17. package/src/runtime/workflow-markup.ts +5 -1
  18. package/src/ui/WordReviewEditor.tsx +667 -132
  19. package/src/ui/editor-runtime-boundary.ts +10 -1
  20. package/src/ui/editor-shell-view.tsx +8 -0
  21. package/src/ui/editor-surface-controller.tsx +6 -0
  22. package/src/ui/headless/selection-toolbar-model.ts +12 -0
  23. package/src/ui-tailwind/chrome/tw-suggestion-card.tsx +139 -0
  24. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +6 -0
  25. package/src/ui-tailwind/editor-surface/pm-decorations.ts +96 -28
  26. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +2 -0
  27. package/src/ui-tailwind/editor-surface/surface-build-keys.ts +6 -0
  28. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +132 -10
  29. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +82 -1
  30. package/src/ui-tailwind/status/tw-status-bar.tsx +4 -1
  31. package/src/ui-tailwind/theme/editor-theme.css +10 -0
  32. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +21 -5
  33. package/src/ui-tailwind/tw-review-workspace.tsx +110 -32
@@ -718,7 +718,10 @@ function createLoadingRuntimeBridge(input: {
718
718
  subscribeToEvents: () => () => undefined,
719
719
  emitBlockedCommand: () => undefined,
720
720
  getRenderSnapshot: () => input.snapshot,
721
+ getCanonicalDocument: () => input.sessionState.canonicalDocument,
722
+ getSourcePackage: () => input.sessionState.sourcePackage,
721
723
  replaceText: () => undefined,
724
+ applyActiveStoryTextCommand: () => undefined,
722
725
  dispatch: () => undefined,
723
726
  undo: () => undefined,
724
727
  redo: () => undefined,
@@ -770,7 +773,7 @@ function createLoadingRuntimeBridge(input: {
770
773
  setWorkflowOverlay: () => undefined,
771
774
  clearWorkflowOverlay: () => undefined,
772
775
  getWorkflowScopeSnapshot: () => null,
773
- getInteractionGuardSnapshot: () => ({ blockedReasons: [] }),
776
+ getInteractionGuardSnapshot: () => ({ effectiveMode: "edit", blockedReasons: [] }),
774
777
  getWorkflowMarkupSnapshot: () => ({
775
778
  totalCount: 0,
776
779
  items: [],
@@ -781,6 +784,12 @@ function createLoadingRuntimeBridge(input: {
781
784
  protectedRanges: [],
782
785
  opaqueFragments: [],
783
786
  }),
787
+ setHostAnnotationOverlay: () => undefined,
788
+ clearHostAnnotationOverlay: () => undefined,
789
+ getHostAnnotationSnapshot: () => ({
790
+ totalCount: 0,
791
+ annotations: [],
792
+ }),
784
793
  getWorkflowCandidateRanges: () => [],
785
794
  replaceWorkflowMarkupText: () => undefined,
786
795
  };
@@ -7,6 +7,7 @@ import type {
7
7
  InteractionGuardSnapshot,
8
8
  RuntimeRenderSnapshot,
9
9
  StyleCatalogSnapshot,
10
+ WordReviewEditorChromeVisibility,
10
11
  WorkflowScopeSnapshot,
11
12
  WorkspaceMode,
12
13
  ZoomLevel,
@@ -16,6 +17,7 @@ import type { MarkupDisplay } from "./headless/comment-decoration-model.ts";
16
17
  import type {
17
18
  SelectionToolbarAnchor,
18
19
  SelectionToolbarModel,
20
+ SuggestionCardModel,
19
21
  } from "./headless/selection-toolbar-model.ts";
20
22
  import type { EditorCommandBag } from "./editor-command-bag.ts";
21
23
  import type { ReviewRailTab } from "../ui-tailwind/review/tw-review-rail.tsx";
@@ -56,17 +58,23 @@ export interface EditorShellViewProps {
56
58
  workflowScopeSnapshot?: WorkflowScopeSnapshot | null;
57
59
  interactionGuardSnapshot?: InteractionGuardSnapshot;
58
60
  selectionToolbar?: SelectionToolbarModel | null;
61
+ suggestionCard?: SuggestionCardModel | null;
59
62
  selectionToolbarAnchor?: SelectionToolbarAnchor | null;
60
63
  documentNavigation?: DocumentNavigationSnapshot;
61
64
  commands: EditorCommandBag;
62
65
  document: ReactNode;
63
66
  onAddCommentFromSelection?: () => void;
67
+ onAddCommentFromSuggestion?: () => void;
68
+ onAcceptSuggestion?: () => void;
69
+ onRejectSuggestion?: () => void;
70
+ onEditSuggestion?: () => void;
64
71
  onDismissSelectionToolbar?: () => void;
65
72
  onSelectionToolbarFocusCapture?: React.FocusEventHandler<HTMLDivElement>;
66
73
  onSelectionToolbarBlurCapture?: React.FocusEventHandler<HTMLDivElement>;
67
74
  selectionToolbarRef?: React.Ref<HTMLDivElement>;
68
75
  activeImageContext?: ActiveImageContext | null;
69
76
  activeObjectContext?: ActiveObjectContext | null;
77
+ chromeVisibility?: Partial<WordReviewEditorChromeVisibility>;
70
78
  }
71
79
 
72
80
  export function EditorShellView(props: EditorShellViewProps) {
@@ -27,6 +27,7 @@ export interface EditorSurfaceControllerProps {
27
27
  markupDisplay: MarkupDisplay;
28
28
  activeRevisionId?: string;
29
29
  showTrackedChanges?: boolean;
30
+ suggestionsEnabled?: boolean;
30
31
  mediaPreviews?: Record<string, MediaPreviewDescriptor>;
31
32
  isPageWorkspace?: boolean;
32
33
  onSelectionToolbarAnchorChange?: (anchor: SelectionToolbarAnchor | null) => void;
@@ -40,11 +41,16 @@ export interface EditorSurfaceControllerProps {
40
41
  onOutdentTab?: () => void;
41
42
  onInsertHardBreak?: () => void;
42
43
  onSplitParagraph?: () => void;
44
+ onUndo?: () => void;
45
+ onRedo?: () => void;
46
+ onBlockedInput?: (command: "paste" | "drop", message: string) => void;
43
47
  onCommentActivated?: (commentId: string) => void;
44
48
  onRevisionActivated?: (revisionId: string) => void;
45
49
  workflowScopes?: readonly WorkflowScope[];
46
50
  workflowCandidates?: readonly WorkflowCandidateRange[];
47
51
  workflowBlockedReasons?: readonly WorkflowBlockedCommandReason[];
52
+ activeWorkflowWorkItemId?: string | null;
53
+ activeWorkflowScopeIds?: readonly string[];
48
54
  }
49
55
 
50
56
  export const EditorSurfaceController = forwardRef<
@@ -14,6 +14,18 @@ export interface SelectionToolbarModel {
14
14
  disabledReason?: string;
15
15
  }
16
16
 
17
+ export interface SuggestionCardModel {
18
+ revisionId: string;
19
+ kindLabel: string;
20
+ previewText: string;
21
+ badges: SelectionToolbarBadge[];
22
+ canAccept: boolean;
23
+ canReject: boolean;
24
+ canEditSuggestion: boolean;
25
+ canAddComment: boolean;
26
+ disabledReason?: string;
27
+ }
28
+
17
29
  export interface SelectionToolbarAnchor {
18
30
  left: number;
19
31
  right: number;
@@ -0,0 +1,139 @@
1
+ import React from "react";
2
+ import type { FocusEventHandler } from "react";
3
+ import * as Tooltip from "@radix-ui/react-tooltip";
4
+ import { Check, MessageSquare, Pencil, X } from "lucide-react";
5
+
6
+ import type { SuggestionCardModel } from "../../ui/headless/selection-toolbar-model";
7
+ import { preserveEditorSelectionMouseDown } from "../../ui/headless/preserve-editor-selection";
8
+
9
+ export interface TwSuggestionCardProps {
10
+ model: SuggestionCardModel;
11
+ onFocusCapture?: FocusEventHandler<HTMLDivElement>;
12
+ onBlurCapture?: FocusEventHandler<HTMLDivElement>;
13
+ onAccept?: () => void;
14
+ onReject?: () => void;
15
+ onEditSuggestion?: () => void;
16
+ onAddComment?: () => void;
17
+ }
18
+
19
+ const focusRingClass =
20
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-canvas";
21
+
22
+ export function TwSuggestionCard(props: TwSuggestionCardProps) {
23
+ const contextLabel = summarizeSuggestionContext(props.model);
24
+ const commentDisabled = !props.model.canAddComment;
25
+ const tooltipLabel = commentDisabled
26
+ ? props.model.disabledReason ?? "Commenting is unavailable for this selection"
27
+ : "Comment on suggestion";
28
+
29
+ return (
30
+ <div
31
+ data-testid="suggestion-card"
32
+ className="inline-flex max-w-[min(28rem,calc(100vw-2rem))] flex-col gap-2 rounded-2xl border border-border/80 bg-canvas px-3 py-2 shadow-xl ring-1 ring-border/80"
33
+ onFocusCapture={props.onFocusCapture}
34
+ onBlurCapture={props.onBlurCapture}
35
+ role="group"
36
+ aria-label="Suggestion actions"
37
+ >
38
+ <div className="flex items-start justify-between gap-3">
39
+ <div className="min-w-0">
40
+ <div className="text-[11px] font-semibold uppercase tracking-[0.12em] text-warning">
41
+ {props.model.kindLabel}
42
+ </div>
43
+ <div className="mt-1 max-w-[16rem] truncate text-sm text-primary">
44
+ {props.model.previewText}
45
+ </div>
46
+ </div>
47
+ {contextLabel ? (
48
+ <div className="shrink-0 rounded-full bg-accent-soft px-2 py-0.5 text-[10px] font-medium uppercase tracking-[0.08em] text-accent">
49
+ {contextLabel}
50
+ </div>
51
+ ) : null}
52
+ </div>
53
+
54
+ <div className="flex flex-wrap items-center gap-1.5">
55
+ <SuggestionActionButton
56
+ icon={<Check className="h-3.5 w-3.5" />}
57
+ label="Accept suggestion"
58
+ disabled={!props.model.canAccept}
59
+ tone="accept"
60
+ onClick={props.onAccept}
61
+ />
62
+ <SuggestionActionButton
63
+ icon={<X className="h-3.5 w-3.5" />}
64
+ label="Reject suggestion"
65
+ disabled={!props.model.canReject}
66
+ tone="reject"
67
+ onClick={props.onReject}
68
+ />
69
+ <SuggestionActionButton
70
+ icon={<Pencil className="h-3.5 w-3.5" />}
71
+ label="Edit suggestion"
72
+ disabled={!props.model.canEditSuggestion}
73
+ tone="neutral"
74
+ onClick={props.onEditSuggestion}
75
+ />
76
+ <Tooltip.Root>
77
+ <Tooltip.Trigger asChild>
78
+ <button
79
+ type="button"
80
+ aria-label="Comment on suggestion"
81
+ disabled={commentDisabled}
82
+ onMouseDown={preserveEditorSelectionMouseDown}
83
+ onClick={props.onAddComment}
84
+ className={`inline-flex h-8 items-center gap-1 rounded-lg border border-border px-2.5 text-xs font-medium text-secondary transition-colors hover:bg-surface disabled:cursor-not-allowed disabled:opacity-40 ${focusRingClass}`}
85
+ >
86
+ <MessageSquare className="h-3.5 w-3.5" />
87
+ Comment
88
+ </button>
89
+ </Tooltip.Trigger>
90
+ <Tooltip.Portal>
91
+ <Tooltip.Content
92
+ className="rounded-md bg-primary px-2 py-1 text-xs text-white shadow-md z-50"
93
+ sideOffset={6}
94
+ >
95
+ {tooltipLabel}
96
+ </Tooltip.Content>
97
+ </Tooltip.Portal>
98
+ </Tooltip.Root>
99
+ </div>
100
+ </div>
101
+ );
102
+ }
103
+
104
+ function summarizeSuggestionContext(model: SuggestionCardModel): string | null {
105
+ const labels = model.badges.map((badge) => badge.label.trim()).filter(Boolean);
106
+ if (labels.length === 0) {
107
+ return null;
108
+ }
109
+ const summary = labels.slice(0, 2).join(" · ");
110
+ return summary.length > 36 ? `${summary.slice(0, 33)}...` : summary;
111
+ }
112
+
113
+ function SuggestionActionButton(props: {
114
+ icon: React.ReactNode;
115
+ label: string;
116
+ disabled: boolean;
117
+ tone: "accept" | "reject" | "neutral";
118
+ onClick?: () => void;
119
+ }) {
120
+ const toneClass = props.tone === "accept"
121
+ ? "border-emerald-500/30 bg-emerald-500/10 text-emerald-700 hover:bg-emerald-500/15 dark:text-emerald-300"
122
+ : props.tone === "reject"
123
+ ? "border-rose-500/30 bg-rose-500/10 text-rose-700 hover:bg-rose-500/15 dark:text-rose-300"
124
+ : "border-border text-secondary hover:bg-surface";
125
+
126
+ return (
127
+ <button
128
+ type="button"
129
+ aria-label={props.label}
130
+ disabled={props.disabled}
131
+ onMouseDown={preserveEditorSelectionMouseDown}
132
+ onClick={props.onClick}
133
+ className={`inline-flex h-8 items-center gap-1 rounded-lg border px-2.5 text-xs font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-40 ${toneClass} ${focusRingClass}`}
134
+ >
135
+ {props.icon}
136
+ {props.label.replace(" suggestion", "").replace(" on suggestion", "")}
137
+ </button>
138
+ );
139
+ }
@@ -19,6 +19,7 @@ export interface CommandBridgeCallbacks {
19
19
  onOutdentTab?: () => void;
20
20
  onUndo: () => void;
21
21
  onRedo: () => void;
22
+ onBlockedInput?: (command: "paste" | "drop", message: string) => void;
22
23
  onSelectionChange: (selection: SelectionSnapshot) => void;
23
24
  getPositionMap: () => PositionMap | null;
24
25
  isSelectionSyncSuppressed?: () => boolean;
@@ -100,17 +101,22 @@ export function createCommandBridgePlugins(
100
101
  },
101
102
  },
102
103
  handleTextInput(_view, _from, _to, text) {
104
+ if (isComposing) {
105
+ return true;
106
+ }
103
107
  callbacks.onInsertText(text);
104
108
  return true; // Block PM from processing
105
109
  },
106
110
 
107
111
  // Block paste (rich paste is not safe, plain paste via text.insert is TODO)
108
112
  handlePaste() {
113
+ callbacks.onBlockedInput?.("paste", "Paste is not supported in the mounted editor yet.");
109
114
  return true; // Block
110
115
  },
111
116
 
112
117
  // Block drop
113
118
  handleDrop() {
119
+ callbacks.onBlockedInput?.("drop", "Drag and drop is not supported in the mounted editor.");
114
120
  return true; // Block
115
121
  },
116
122
  },
@@ -21,18 +21,28 @@ type RailDecorationSpec = {
21
21
  attrs: Record<string, string>;
22
22
  };
23
23
 
24
- function getWorkflowInlineClass(scope: WorkflowScope): string {
25
- if (scope.mode === "edit") return "wre-workflow-inline wre-workflow-inline-edit";
26
- if (scope.mode === "suggest") return "wre-workflow-inline wre-workflow-inline-suggest";
27
- if (scope.mode === "comment") return "wre-workflow-inline wre-workflow-inline-comment";
28
- return "wre-workflow-inline wre-workflow-inline-view";
24
+ function getWorkflowInlineClass(scope: WorkflowScope, isActiveWorkItem: boolean): string {
25
+ const base =
26
+ scope.mode === "edit"
27
+ ? "wre-workflow-inline wre-workflow-inline-edit"
28
+ : scope.mode === "suggest"
29
+ ? "wre-workflow-inline wre-workflow-inline-suggest"
30
+ : scope.mode === "comment"
31
+ ? "wre-workflow-inline wre-workflow-inline-comment"
32
+ : "wre-workflow-inline wre-workflow-inline-view";
33
+ return isActiveWorkItem ? `${base} wre-workflow-inline-active` : base;
29
34
  }
30
35
 
31
- function getWorkflowRailClass(scope: WorkflowScope): string {
32
- if (scope.mode === "edit") return "wre-workflow-rail wre-workflow-rail-edit";
33
- if (scope.mode === "suggest") return "wre-workflow-rail wre-workflow-rail-suggest";
34
- if (scope.mode === "comment") return "wre-workflow-rail wre-workflow-rail-comment";
35
- return "wre-workflow-rail wre-workflow-rail-view";
36
+ function getWorkflowRailClass(scope: WorkflowScope, isActiveWorkItem: boolean): string {
37
+ const base =
38
+ scope.mode === "edit"
39
+ ? "wre-workflow-rail wre-workflow-rail-edit"
40
+ : scope.mode === "suggest"
41
+ ? "wre-workflow-rail wre-workflow-rail-suggest"
42
+ : scope.mode === "comment"
43
+ ? "wre-workflow-rail wre-workflow-rail-comment"
44
+ : "wre-workflow-rail wre-workflow-rail-view";
45
+ return isActiveWorkItem ? `${base} wre-workflow-rail-active` : base;
36
46
  }
37
47
 
38
48
  function getWorkflowCandidateInlineClass(): string {
@@ -142,8 +152,14 @@ function pushRailDecorations(
142
152
  from: number,
143
153
  to: number,
144
154
  spec: RailDecorationSpec,
155
+ rangeCache: Map<string, Array<{ from: number; to: number }>>,
145
156
  ): void {
146
- for (const range of collectRailRanges(doc, from, to)) {
157
+ const cacheKey = `${from}:${to}`;
158
+ const ranges = rangeCache.get(cacheKey) ?? collectRailRanges(doc, from, to);
159
+ if (!rangeCache.has(cacheKey)) {
160
+ rangeCache.set(cacheKey, ranges);
161
+ }
162
+ for (const range of ranges) {
147
163
  decorations.push(
148
164
  Decoration.node(range.from, range.to, {
149
165
  class: spec.className,
@@ -172,8 +188,15 @@ export function buildDecorations(
172
188
  activeStory: EditorStoryTarget = MAIN_STORY_TARGET,
173
189
  workflowCandidates?: readonly WorkflowCandidateRange[],
174
190
  workflowBlockedReasons?: readonly WorkflowBlockedCommandReason[],
191
+ activeWorkflowWorkItemId?: string | null,
192
+ activeWorkflowScopeIds?: readonly string[],
193
+ suggestionsEnabled = false,
175
194
  ): DecorationSet {
176
195
  const decorations: Decoration[] = [];
196
+ const railRangeCache = new Map<string, Array<{ from: number; to: number }>>();
197
+ const activeScopeIds = new Set(activeWorkflowScopeIds ?? []);
198
+ // In suggestions mode, tracked changes are always shown regardless of the toggle.
199
+ const effectiveShowTracked = suggestionsEnabled ? true : showTrackedChanges;
177
200
 
178
201
  // Walk comment threads and create inline decorations
179
202
  if (commentModel) {
@@ -207,7 +230,7 @@ export function buildDecorations(
207
230
  // Always hide deletions in clean mode (final-text semantics).
208
231
  // This is the critical behavior: "hide tracked changes" must show
209
232
  // the document as if accepted, not show deleted text as kept text.
210
- if (markupDisplay === "clean" && rev.kind === "deletion") {
233
+ if (markupDisplay === "clean" && rev.kind === "deletion" && !suggestionsEnabled) {
211
234
  const pmFrom = positionMap.runtimeToPm(rev.from);
212
235
  const pmTo = positionMap.runtimeToPm(rev.to);
213
236
  if (pmFrom < pmTo) {
@@ -222,7 +245,48 @@ export function buildDecorations(
222
245
  }
223
246
 
224
247
  // Skip visual styling when tracked changes display is off
225
- if (!showTrackedChanges) continue;
248
+ if (!effectiveShowTracked) continue;
249
+
250
+ const pmFrom = positionMap.runtimeToPm(rev.from);
251
+ const pmTo = positionMap.runtimeToPm(rev.to);
252
+ if (pmFrom >= pmTo) continue;
253
+
254
+ if (suggestionsEnabled) {
255
+ if (rev.kind === "insertion") {
256
+ decorations.push(
257
+ Decoration.inline(pmFrom, pmTo, {
258
+ class: "text-insert",
259
+ "data-revision-id": rev.revisionId,
260
+ }),
261
+ );
262
+ decorations.push(
263
+ Decoration.widget(pmFrom, () => {
264
+ const el = document.createElement("span");
265
+ el.textContent = "[";
266
+ el.className = "text-insert";
267
+ el.setAttribute("contenteditable", "false");
268
+ return el;
269
+ }, { side: -1, key: `${rev.revisionId}-open` }),
270
+ );
271
+ decorations.push(
272
+ Decoration.widget(pmTo, () => {
273
+ const el = document.createElement("span");
274
+ el.textContent = "]";
275
+ el.className = "text-insert";
276
+ el.setAttribute("contenteditable", "false");
277
+ return el;
278
+ }, { side: 1, key: `${rev.revisionId}-close` }),
279
+ );
280
+ } else if (rev.kind === "deletion") {
281
+ decorations.push(
282
+ Decoration.inline(pmFrom, pmTo, {
283
+ class: "text-danger line-through decoration-danger/80 decoration-1",
284
+ "data-revision-id": rev.revisionId,
285
+ }),
286
+ );
287
+ }
288
+ continue;
289
+ }
226
290
 
227
291
  const cls = getRevisionHighlightClass(
228
292
  revisionModel,
@@ -232,16 +296,12 @@ export function buildDecorations(
232
296
  );
233
297
  if (!cls) continue;
234
298
 
235
- const pmFrom = positionMap.runtimeToPm(rev.from);
236
- const pmTo = positionMap.runtimeToPm(rev.to);
237
- if (pmFrom < pmTo) {
238
- decorations.push(
239
- Decoration.inline(pmFrom, pmTo, {
240
- class: cls,
241
- "data-revision-id": rev.revisionId,
242
- }),
243
- );
244
- }
299
+ decorations.push(
300
+ Decoration.inline(pmFrom, pmTo, {
301
+ class: cls,
302
+ "data-revision-id": rev.revisionId,
303
+ }),
304
+ );
245
305
  }
246
306
  }
247
307
 
@@ -251,25 +311,33 @@ export function buildDecorations(
251
311
  if (!storyTargetsEqual(scopeStoryTarget, activeStory)) continue;
252
312
  const pmRange = buildAnchorPmRange(scope.anchor, positionMap);
253
313
  if (!pmRange) continue;
314
+ const isActiveWorkItem =
315
+ Boolean(activeWorkflowWorkItemId) &&
316
+ (
317
+ scope.workItemId === activeWorkflowWorkItemId ||
318
+ activeScopeIds.has(scope.scopeId)
319
+ );
254
320
 
255
321
  if (pmRange.allowInline && pmRange.from < pmRange.to) {
256
322
  decorations.push(
257
323
  Decoration.inline(pmRange.from, pmRange.to, {
258
- class: getWorkflowInlineClass(scope),
324
+ class: getWorkflowInlineClass(scope, isActiveWorkItem),
259
325
  "data-workflow-scope-id": scope.scopeId,
260
326
  "data-workflow-scope-mode": scope.mode,
327
+ "data-workflow-active": isActiveWorkItem ? "true" : "false",
261
328
  }),
262
329
  );
263
330
  }
264
331
 
265
332
  pushRailDecorations(decorations, doc, pmRange.from, pmRange.to, {
266
333
  railKind: "scope",
267
- className: getWorkflowRailClass(scope),
334
+ className: getWorkflowRailClass(scope, isActiveWorkItem),
268
335
  attrs: {
269
336
  "data-workflow-scope-id": scope.scopeId,
270
337
  "data-workflow-scope-mode": scope.mode,
338
+ "data-workflow-active": isActiveWorkItem ? "true" : "false",
271
339
  },
272
- });
340
+ }, railRangeCache);
273
341
  }
274
342
  }
275
343
 
@@ -295,7 +363,7 @@ export function buildDecorations(
295
363
  attrs: {
296
364
  "data-workflow-candidate-id": candidate.candidateId,
297
365
  },
298
- });
366
+ }, railRangeCache);
299
367
  }
300
368
  }
301
369
 
@@ -327,7 +395,7 @@ export function buildDecorations(
327
395
  attrs: {
328
396
  "data-workflow-blocked-code": reason.code,
329
397
  },
330
- });
398
+ }, railRangeCache);
331
399
  }
332
400
  }
333
401
 
@@ -383,6 +383,8 @@ function buildTable(
383
383
  }
384
384
  rows.push(editorSchema.nodes.table_row.create(
385
385
  {
386
+ gridBefore: row.gridBefore ?? 0,
387
+ gridAfter: row.gridAfter ?? 0,
386
388
  height: row.height ?? null,
387
389
  heightRule: row.heightRule ?? null,
388
390
  isHeader: row.isHeader ?? false,
@@ -41,6 +41,9 @@ export function createSurfaceDecorationKey(input: {
41
41
  workflowScopeSignature?: string;
42
42
  workflowCandidateSignature?: string;
43
43
  workflowBlockedSignature?: string;
44
+ activeWorkflowWorkItemId?: string | null;
45
+ activeWorkflowScopeIds?: readonly string[];
46
+ suggestionsEnabled?: boolean;
44
47
  }): string {
45
48
  return JSON.stringify({
46
49
  markupDisplay: input.markupDisplay,
@@ -51,5 +54,8 @@ export function createSurfaceDecorationKey(input: {
51
54
  workflowScopeSignature: input.workflowScopeSignature ?? null,
52
55
  workflowCandidateSignature: input.workflowCandidateSignature ?? null,
53
56
  workflowBlockedSignature: input.workflowBlockedSignature ?? null,
57
+ activeWorkflowWorkItemId: input.activeWorkflowWorkItemId ?? null,
58
+ activeWorkflowScopeIds: input.activeWorkflowScopeIds ?? [],
59
+ suggestionsEnabled: input.suggestionsEnabled ?? false,
54
60
  });
55
61
  }