@beyondwork/docx-react-component 1.0.19 → 1.0.21

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 (71) hide show
  1. package/package.json +44 -25
  2. package/src/api/public-types.ts +336 -0
  3. package/src/api/session-state.ts +2 -0
  4. package/src/core/commands/formatting-commands.ts +1 -1
  5. package/src/core/commands/index.ts +14 -2
  6. package/src/core/search/search-text.ts +28 -0
  7. package/src/core/state/editor-state.ts +3 -0
  8. package/src/index.ts +21 -0
  9. package/src/io/docx-session.ts +363 -17
  10. package/src/io/export/serialize-comments.ts +104 -34
  11. package/src/io/export/serialize-footnotes.ts +198 -1
  12. package/src/io/export/serialize-headers-footers.ts +203 -10
  13. package/src/io/export/serialize-main-document.ts +83 -3
  14. package/src/io/export/split-review-boundaries.ts +181 -19
  15. package/src/io/normalize/normalize-text.ts +82 -8
  16. package/src/io/ooxml/highlight-colors.ts +39 -0
  17. package/src/io/ooxml/parse-comments.ts +85 -19
  18. package/src/io/ooxml/parse-fields.ts +396 -0
  19. package/src/io/ooxml/parse-footnotes.ts +240 -2
  20. package/src/io/ooxml/parse-headers-footers.ts +431 -7
  21. package/src/io/ooxml/parse-inline-media.ts +15 -1
  22. package/src/io/ooxml/parse-main-document.ts +396 -14
  23. package/src/io/ooxml/parse-revisions.ts +317 -38
  24. package/src/legal/bookmarks.ts +44 -0
  25. package/src/legal/cross-references.ts +59 -1
  26. package/src/model/canonical-document.ts +117 -1
  27. package/src/model/snapshot.ts +85 -1
  28. package/src/review/store/revision-store.ts +6 -0
  29. package/src/review/store/revision-types.ts +1 -0
  30. package/src/runtime/document-navigation.ts +52 -13
  31. package/src/runtime/document-runtime.ts +1521 -75
  32. package/src/runtime/read-only-diagnostics-runtime.ts +8 -0
  33. package/src/runtime/session-capabilities.ts +33 -3
  34. package/src/runtime/surface-projection.ts +86 -25
  35. package/src/runtime/table-schema.ts +2 -2
  36. package/src/runtime/view-state.ts +24 -6
  37. package/src/runtime/workflow-markup.ts +349 -0
  38. package/src/ui/WordReviewEditor.tsx +915 -1314
  39. package/src/ui/editor-command-bag.ts +120 -0
  40. package/src/ui/editor-runtime-boundary.ts +1448 -0
  41. package/src/ui/editor-shell-view.tsx +134 -0
  42. package/src/ui/editor-surface-controller.tsx +55 -0
  43. package/src/ui/headless/revision-decoration-model.ts +4 -4
  44. package/src/ui/runtime-snapshot-selectors.ts +197 -0
  45. package/src/ui/workflow-surface-blocked-rails.ts +94 -0
  46. package/src/ui-tailwind/chrome/tw-alert-banner.tsx +18 -2
  47. package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +129 -0
  48. package/src/ui-tailwind/chrome/tw-layout-panel.tsx +114 -0
  49. package/src/ui-tailwind/chrome/tw-object-context-toolbar.tsx +34 -0
  50. package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +27 -2
  51. package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +128 -0
  52. package/src/ui-tailwind/editor-surface/perf-probe.ts +86 -14
  53. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +2 -2
  54. package/src/ui-tailwind/editor-surface/pm-decorations.ts +237 -0
  55. package/src/ui-tailwind/editor-surface/pm-position-map.ts +1 -1
  56. package/src/ui-tailwind/editor-surface/pm-schema.ts +139 -8
  57. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +98 -48
  58. package/src/ui-tailwind/editor-surface/surface-build-keys.ts +55 -0
  59. package/src/ui-tailwind/editor-surface/tw-opaque-block.tsx +7 -1
  60. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +190 -48
  61. package/src/ui-tailwind/page-chrome-model.ts +27 -0
  62. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +7 -7
  63. package/src/ui-tailwind/review/tw-health-panel.tsx +31 -2
  64. package/src/ui-tailwind/review/tw-review-rail.tsx +3 -3
  65. package/src/ui-tailwind/review/tw-revision-sidebar.tsx +15 -15
  66. package/src/ui-tailwind/theme/editor-theme.css +130 -0
  67. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +543 -5
  68. package/src/ui-tailwind/tw-review-workspace.tsx +316 -19
  69. package/src/validation/compatibility-engine.ts +27 -4
  70. package/src/validation/compatibility-report.ts +1 -0
  71. package/src/validation/docx-comment-proof.ts +220 -0
@@ -18,10 +18,17 @@ import type {
18
18
  DocumentNavigationSnapshot,
19
19
  EditorViewStateSnapshot,
20
20
  FormattingStateSnapshot,
21
+ FormattingAlignment,
22
+ HeaderFooterLinkPatch,
23
+ InteractionGuardSnapshot,
24
+ InsertImageOptions,
21
25
  RuntimeRenderSnapshot,
26
+ SectionPageNumberingPatch,
27
+ SectionBreakType,
22
28
  StyleCatalogSnapshot,
23
29
  SurfaceBlockSnapshot,
24
30
  TrackedChangeEntrySnapshot,
31
+ WorkflowScopeSnapshot,
25
32
  WorkspaceMode,
26
33
  ZoomLevel,
27
34
  } from "../api/public-types";
@@ -33,16 +40,26 @@ import {
33
40
  estimateParagraphLineHeight,
34
41
  getUsableColumnWidth,
35
42
  } from "../runtime/page-layout-estimation.ts";
43
+ import {
44
+ incrementInvalidationCounter,
45
+ recordPerfSample,
46
+ } from "./editor-surface/perf-probe.ts";
47
+ import { computeLineMarkersIfEnabled } from "./page-chrome-model.ts";
36
48
  import type { SessionCapabilities } from "../runtime/session-capabilities";
37
49
  import type {
38
50
  SelectionToolbarAnchor,
39
51
  SelectionToolbarModel,
40
52
  } from "../ui/headless/selection-toolbar-model";
41
53
  import type { MarkupDisplay } from "../ui/headless/comment-decoration-model";
54
+ import type { EditorCommandBag } from "../ui/editor-command-bag.ts";
42
55
  import { preserveEditorSelectionMouseDown } from "../ui/headless/preserve-editor-selection";
43
56
  import { TwAlertBanner } from "./chrome/tw-alert-banner";
57
+ import { TwImageContextToolbar, type ActiveImageContext } from "./chrome/tw-image-context-toolbar";
58
+ import { TwLayoutPanel } from "./chrome/tw-layout-panel";
59
+ import { TwObjectContextToolbar, type ActiveObjectContext } from "./chrome/tw-object-context-toolbar";
44
60
  import { TwPageRuler } from "./chrome/tw-page-ruler";
45
61
  import { TwSelectionToolbar } from "./chrome/tw-selection-toolbar";
62
+ import { TwTableContextToolbar } from "./chrome/tw-table-context-toolbar";
46
63
  import { TwReviewRail, type ReviewRailTab } from "./review/tw-review-rail";
47
64
  import { TwStatusBar } from "./status/tw-status-bar";
48
65
  import { TwToolbar } from "./toolbar/tw-toolbar";
@@ -50,6 +67,7 @@ import { TwToolbar } from "./toolbar/tw-toolbar";
50
67
  export interface TwReviewWorkspaceProps {
51
68
  snapshot: RuntimeRenderSnapshot;
52
69
  viewState: EditorViewStateSnapshot;
70
+ markupDisplay: MarkupDisplay;
53
71
  currentUserId?: string;
54
72
  capabilities?: SessionCapabilities;
55
73
  reviewMode?: "editing" | "review";
@@ -62,38 +80,109 @@ export interface TwReviewWorkspaceProps {
62
80
  activeCommentId?: string;
63
81
  activeRevisionId?: string;
64
82
  showTrackedChanges: boolean;
83
+ workflowScopeSnapshot?: WorkflowScopeSnapshot | null;
84
+ interactionGuardSnapshot?: InteractionGuardSnapshot;
85
+ commands: EditorCommandBag;
65
86
  selectionToolbar?: SelectionToolbarModel | null;
66
87
  selectionToolbarAnchor?: SelectionToolbarAnchor | null;
67
88
  documentNavigation?: DocumentNavigationSnapshot;
68
- onWorkspaceModeChange: (value: WorkspaceMode) => void;
89
+ onWorkspaceModeChange?: (value: WorkspaceMode) => void;
69
90
  onZoomChange?: (level: ZoomLevel) => void;
70
- onActiveRailTabChange: (value: ReviewRailTab) => void;
71
- onShowTrackedChangesChange: (show: boolean) => void;
72
- onUndo: () => void;
73
- onRedo: () => void;
91
+ onActiveRailTabChange?: (value: ReviewRailTab) => void;
92
+ onShowTrackedChangesChange?: (show: boolean) => void;
93
+ onUndo?: () => void;
94
+ onRedo?: () => void;
74
95
  onSetParagraphStyle?: (styleId: string) => void;
75
96
  onToggleBold?: () => void;
76
97
  onToggleItalic?: () => void;
77
98
  onToggleUnderline?: () => void;
99
+ onSetSelectionTextColor?: (color: string) => void;
100
+ onSetSelectionHighlightColor?: (color: string | null) => void;
101
+ onToggleStrikethrough?: () => void;
102
+ onToggleSuperscript?: () => void;
103
+ onToggleSubscript?: () => void;
104
+ onSetFontFamily?: (fontFamily: string) => void;
105
+ onSetFontSize?: (fontSize: number) => void;
106
+ onSetTextColor?: (color: string) => void;
107
+ onSetHighlightColor?: (color: string | null) => void;
108
+ onSetAlignment?: (alignment: FormattingAlignment) => void;
78
109
  onOutdent?: () => void;
79
110
  onIndent?: () => void;
80
- onAddComment: () => void;
111
+ onAddComment?: () => void;
112
+ onInsertPageBreak?: () => void;
113
+ onInsertTable?: () => void;
114
+ onInsertSectionBreak?: (type: SectionBreakType) => void;
115
+ onInsertImage?: (options: InsertImageOptions) => void;
116
+ onSetTableStyle?: (styleId: string) => void;
117
+ onAddRowBefore?: () => void;
118
+ onAddRowAfter?: () => void;
119
+ onAddColumnBefore?: () => void;
120
+ onAddColumnAfter?: () => void;
121
+ onDeleteRow?: () => void;
122
+ onDeleteColumn?: () => void;
123
+ onDeleteTable?: () => void;
124
+ onMergeCells?: () => void;
125
+ onSplitCell?: () => void;
126
+ onSetCellBackground?: (color: string) => void;
127
+ activeImageContext?: ActiveImageContext | null;
128
+ activeObjectContext?: ActiveObjectContext | null;
129
+ onSetImageLayout?: (
130
+ mediaId: string,
131
+ dimensions: { widthEmu: number; heightEmu: number },
132
+ ) => void;
133
+ onSetImageFrame?: (
134
+ mediaId: string,
135
+ offsets: { horizontalOffsetEmu?: number; verticalOffsetEmu?: number },
136
+ ) => void;
137
+ onDeleteSectionBreak?: (sectionIndex: number) => void;
138
+ onUpdateSectionLayout?: (
139
+ sectionIndex: number,
140
+ patch: {
141
+ pageSize?: { width?: number; height?: number; orientation?: "portrait" | "landscape" };
142
+ pageMargins?: {
143
+ top?: number;
144
+ right?: number;
145
+ bottom?: number;
146
+ left?: number;
147
+ header?: number;
148
+ footer?: number;
149
+ gutter?: number;
150
+ };
151
+ columns?: {
152
+ count?: number;
153
+ space?: number;
154
+ equalWidth?: boolean;
155
+ columns?: Array<{ width: number; space?: number }>;
156
+ separator?: boolean;
157
+ };
158
+ titlePage?: boolean;
159
+ sectionType?: SectionBreakType;
160
+ },
161
+ ) => void;
162
+ onSetSectionPageNumbering?: (
163
+ sectionIndex: number,
164
+ patch: SectionPageNumberingPatch | null,
165
+ ) => void;
166
+ onSetHeaderFooterLink?: (
167
+ sectionIndex: number,
168
+ patch: HeaderFooterLinkPatch,
169
+ ) => void;
81
170
  onAddCommentFromSelection?: () => void;
82
- onExport: () => void;
171
+ onExport?: () => void;
83
172
  onDismissSelectionToolbar?: () => void;
84
173
  onSelectionToolbarFocusCapture?: FocusEventHandler<HTMLDivElement>;
85
174
  onSelectionToolbarBlurCapture?: FocusEventHandler<HTMLDivElement>;
86
175
  selectionToolbarRef?: Ref<HTMLDivElement>;
87
- onOpenComment: (thread: CommentSidebarThreadSnapshot) => void;
88
- onResolveComment: (commentId: string) => void;
176
+ onOpenComment?: (thread: CommentSidebarThreadSnapshot) => void;
177
+ onResolveComment?: (commentId: string) => void;
89
178
  onReopenComment?: (commentId: string) => void;
90
179
  onAddReply?: (commentId: string, body: string) => void;
91
180
  onEditBody?: (commentId: string, body: string) => void;
92
- onOpenRevision: (revision: TrackedChangeEntrySnapshot) => void;
93
- onAcceptRevision: (revisionId: string) => void;
94
- onRejectRevision: (revisionId: string) => void;
95
- onAcceptAllChanges: () => void;
96
- onRejectAllChanges: () => void;
181
+ onOpenRevision?: (revision: TrackedChangeEntrySnapshot) => void;
182
+ onAcceptRevision?: (revisionId: string) => void;
183
+ onRejectRevision?: (revisionId: string) => void;
184
+ onAcceptAllChanges?: () => void;
185
+ onRejectAllChanges?: () => void;
97
186
  onCloseStory?: () => void;
98
187
  onOpenHeaderStory?: () => void;
99
188
  onOpenFooterStory?: () => void;
@@ -109,12 +198,16 @@ export interface TwReviewWorkspaceProps {
109
198
  onNavigateHeading?: (headingId: string) => void;
110
199
  }
111
200
 
112
- export function TwReviewWorkspace(props: TwReviewWorkspaceProps) {
201
+ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
202
+ const props = {
203
+ ...inputProps,
204
+ ...inputProps.commands,
205
+ } as TwReviewWorkspaceProps & EditorCommandBag;
113
206
  const { snapshot, viewState } = props;
114
207
  const selectionToolbarRootRef = useRef<HTMLDivElement>(null);
115
208
  const caps = props.capabilities;
116
209
  const isPageWorkspace = props.workspaceMode === "page";
117
- const markupDisplay: MarkupDisplay = isPageWorkspace ? "all" : "clean";
210
+ const markupDisplay = props.markupDisplay;
118
211
  const [navOpen, setNavOpen] = useState(false);
119
212
  const [layoutToolsOpen, setLayoutToolsOpen] = useState(false);
120
213
  const zoomLevel = props.zoomLevel ?? 100;
@@ -123,13 +216,33 @@ export function TwReviewWorkspace(props: TwReviewWorkspaceProps) {
123
216
  snapshot.compatibility.featureEntries.filter(
124
217
  (entry) => entry.featureClass === "preserve-only",
125
218
  ).length;
219
+ const blockedReasons =
220
+ props.interactionGuardSnapshot?.blockedReasons ??
221
+ props.workflowScopeSnapshot?.blockedReasons ??
222
+ [];
126
223
  const showReviewRail = caps?.reviewRailVisible ?? true;
127
224
  const headings = props.documentNavigation?.headings ?? [];
225
+ const headerVariant = snapshot.pageLayout?.headerVariants[0]?.variant ?? "default";
226
+ const footerVariant = snapshot.pageLayout?.footerVariants[0]?.variant ?? "default";
128
227
  const selectionPosition =
129
228
  viewState.selection.activeRange.kind === "node"
130
229
  ? viewState.selection.activeRange.at
131
230
  : viewState.selection.head;
132
- const activeParagraphLayout = resolveActiveParagraphLayout(snapshot.surface, selectionPosition);
231
+ const activeParagraphLayout = useMemo(
232
+ () => resolveActiveParagraphLayout(snapshot.surface, selectionPosition),
233
+ [selectionPosition, snapshot.surface],
234
+ );
235
+ const isTableContext = Boolean(
236
+ props.formattingState?.breadcrumb.some((item) => item.kind === "table" || item.kind === "table_cell" || item.kind === "table_row"),
237
+ );
238
+ const contextualSurface =
239
+ props.activeImageContext
240
+ ? "image"
241
+ : props.activeObjectContext
242
+ ? "object"
243
+ : isTableContext
244
+ ? "table"
245
+ : null;
133
246
  const pageChromeModel = useMemo(
134
247
  () =>
135
248
  buildPageChromeModel(
@@ -155,6 +268,11 @@ export function TwReviewWorkspace(props: TwReviewWorkspaceProps) {
155
268
  snapshot.activeStory.kind === "main" &&
156
269
  shouldHidePageBorderForSelection(viewState.selection);
157
270
 
271
+ useEffect(() => {
272
+ recordPerfSample("workspace.chrome");
273
+ incrementInvalidationCounter("workspace.chrome.recomputes");
274
+ }, [activeParagraphLayout, pageChromeModel, pageShellMetrics]);
275
+
158
276
  useEffect(() => {
159
277
  if (isPageWorkspace && snapshot.activeStory.kind !== "main") {
160
278
  setLayoutToolsOpen(true);
@@ -197,9 +315,56 @@ export function TwReviewWorkspace(props: TwReviewWorkspaceProps) {
197
315
  onToggleBold={runWithSelectionToolbarDismiss(props.onToggleBold)}
198
316
  onToggleItalic={runWithSelectionToolbarDismiss(props.onToggleItalic)}
199
317
  onToggleUnderline={runWithSelectionToolbarDismiss(props.onToggleUnderline)}
318
+ onToggleStrikethrough={runWithSelectionToolbarDismiss(props.onToggleStrikethrough)}
319
+ onToggleSuperscript={runWithSelectionToolbarDismiss(props.onToggleSuperscript)}
320
+ onToggleSubscript={runWithSelectionToolbarDismiss(props.onToggleSubscript)}
321
+ onSetFontFamily={props.onSetFontFamily
322
+ ? (fontFamily) => {
323
+ dismissSelectionToolbar();
324
+ props.onSetFontFamily?.(fontFamily);
325
+ }
326
+ : undefined}
327
+ onSetFontSize={props.onSetFontSize
328
+ ? (fontSize) => {
329
+ dismissSelectionToolbar();
330
+ props.onSetFontSize?.(fontSize);
331
+ }
332
+ : undefined}
333
+ onSetTextColor={props.onSetTextColor
334
+ ? (color) => {
335
+ dismissSelectionToolbar();
336
+ props.onSetTextColor?.(color);
337
+ }
338
+ : undefined}
339
+ onSetHighlightColor={props.onSetHighlightColor
340
+ ? (color) => {
341
+ dismissSelectionToolbar();
342
+ props.onSetHighlightColor?.(color);
343
+ }
344
+ : undefined}
345
+ onSetAlignment={props.onSetAlignment
346
+ ? (alignment) => {
347
+ dismissSelectionToolbar();
348
+ props.onSetAlignment?.(alignment);
349
+ }
350
+ : undefined}
200
351
  onOutdent={runWithSelectionToolbarDismiss(props.onOutdent)}
201
352
  onIndent={runWithSelectionToolbarDismiss(props.onIndent)}
202
353
  onAddComment={runWithSelectionToolbarDismiss(props.onAddComment)}
354
+ onInsertPageBreak={runWithSelectionToolbarDismiss(props.onInsertPageBreak)}
355
+ onInsertTable={runWithSelectionToolbarDismiss(props.onInsertTable)}
356
+ onInsertSectionBreak={props.onInsertSectionBreak
357
+ ? (type) => {
358
+ dismissSelectionToolbar();
359
+ props.onInsertSectionBreak?.(type);
360
+ }
361
+ : undefined}
362
+ onInsertImage={props.onInsertImage
363
+ ? (options) => {
364
+ dismissSelectionToolbar();
365
+ props.onInsertImage?.(options);
366
+ }
367
+ : undefined}
203
368
  onExport={runWithSelectionToolbarDismiss(props.onExport)}
204
369
  activeStory={snapshot.activeStory}
205
370
  onCloseStory={props.onCloseStory
@@ -219,9 +384,14 @@ export function TwReviewWorkspace(props: TwReviewWorkspaceProps) {
219
384
  dismissSelectionToolbar();
220
385
  props.onShowTrackedChangesChange(show);
221
386
  }}
387
+ blockedReasons={blockedReasons}
222
388
  />
223
389
 
224
- <TwAlertBanner snapshot={snapshot} preserveOnlyCount={preserveOnlyCount} />
390
+ <TwAlertBanner
391
+ snapshot={snapshot}
392
+ preserveOnlyCount={preserveOnlyCount}
393
+ workflowBlockedReasons={blockedReasons}
394
+ />
225
395
 
226
396
  <div className="flex flex-1 min-h-0">
227
397
  {/* Collapsible document navigator — page mode only */}
@@ -356,6 +526,44 @@ export function TwReviewWorkspace(props: TwReviewWorkspaceProps) {
356
526
  Body
357
527
  </button>
358
528
  ) : null}
529
+ {snapshot.activeStory.kind === "main" && snapshot.pageLayout.sectionIndex > 0 ? (
530
+ <>
531
+ <button
532
+ type="button"
533
+ aria-label="Link header to previous"
534
+ disabled={!props.onSetHeaderFooterLink}
535
+ onMouseDown={preserveEditorSelectionMouseDown}
536
+ onClick={() => {
537
+ dismissSelectionToolbar();
538
+ props.onSetHeaderFooterLink?.(snapshot.pageLayout!.sectionIndex, {
539
+ kind: "header",
540
+ variant: headerVariant,
541
+ linkToPrevious: true,
542
+ });
543
+ }}
544
+ className="inline-flex items-center gap-1 rounded-md border border-border bg-canvas px-2 py-1 text-xs font-medium text-primary transition-colors hover:bg-surface disabled:cursor-not-allowed disabled:opacity-40"
545
+ >
546
+ Link header
547
+ </button>
548
+ <button
549
+ type="button"
550
+ aria-label="Link footer to previous"
551
+ disabled={!props.onSetHeaderFooterLink}
552
+ onMouseDown={preserveEditorSelectionMouseDown}
553
+ onClick={() => {
554
+ dismissSelectionToolbar();
555
+ props.onSetHeaderFooterLink?.(snapshot.pageLayout!.sectionIndex, {
556
+ kind: "footer",
557
+ variant: footerVariant,
558
+ linkToPrevious: true,
559
+ });
560
+ }}
561
+ className="inline-flex items-center gap-1 rounded-md border border-border bg-canvas px-2 py-1 text-xs font-medium text-primary transition-colors hover:bg-surface disabled:cursor-not-allowed disabled:opacity-40"
562
+ >
563
+ Link footer
564
+ </button>
565
+ </>
566
+ ) : null}
359
567
  <button
360
568
  type="button"
361
569
  aria-label="Toggle layout tools"
@@ -409,6 +617,86 @@ export function TwReviewWorkspace(props: TwReviewWorkspaceProps) {
409
617
  ? runWithSelectionToolbarDismiss(props.onContinueNumbering)
410
618
  : undefined}
411
619
  />
620
+ <TwLayoutPanel
621
+ pageLayout={snapshot.pageLayout}
622
+ readOnly={snapshot.readOnly || snapshot.activeStory.kind !== "main"}
623
+ onInsertSectionBreak={props.onInsertSectionBreak
624
+ ? (type) => {
625
+ dismissSelectionToolbar();
626
+ props.onInsertSectionBreak?.(type);
627
+ }
628
+ : undefined}
629
+ onDeleteSectionBreak={props.onDeleteSectionBreak
630
+ ? (sectionIndex) => {
631
+ dismissSelectionToolbar();
632
+ props.onDeleteSectionBreak?.(sectionIndex);
633
+ }
634
+ : undefined}
635
+ onUpdateSectionLayout={props.onUpdateSectionLayout
636
+ ? (sectionIndex, patch) => {
637
+ dismissSelectionToolbar();
638
+ props.onUpdateSectionLayout?.(sectionIndex, patch);
639
+ }
640
+ : undefined}
641
+ onSetSectionPageNumbering={props.onSetSectionPageNumbering
642
+ ? (sectionIndex, patch) => {
643
+ dismissSelectionToolbar();
644
+ props.onSetSectionPageNumbering?.(sectionIndex, patch);
645
+ }
646
+ : undefined}
647
+ />
648
+ </div>
649
+ ) : null}
650
+ {contextualSurface ? (
651
+ <div className="px-5 pt-3 space-y-3">
652
+ {contextualSurface === "table" ? (
653
+ <TwTableContextToolbar
654
+ disabled={!caps?.canEdit}
655
+ tableStyles={props.styleCatalog?.tables ?? []}
656
+ onSetTableStyle={props.onSetTableStyle
657
+ ? (styleId) => {
658
+ dismissSelectionToolbar();
659
+ props.onSetTableStyle?.(styleId);
660
+ }
661
+ : undefined}
662
+ onAddRowBefore={runWithSelectionToolbarDismiss(props.onAddRowBefore)}
663
+ onAddRowAfter={runWithSelectionToolbarDismiss(props.onAddRowAfter)}
664
+ onAddColumnBefore={runWithSelectionToolbarDismiss(props.onAddColumnBefore)}
665
+ onAddColumnAfter={runWithSelectionToolbarDismiss(props.onAddColumnAfter)}
666
+ onDeleteRow={runWithSelectionToolbarDismiss(props.onDeleteRow)}
667
+ onDeleteColumn={runWithSelectionToolbarDismiss(props.onDeleteColumn)}
668
+ onDeleteTable={runWithSelectionToolbarDismiss(props.onDeleteTable)}
669
+ onMergeCells={runWithSelectionToolbarDismiss(props.onMergeCells)}
670
+ onSplitCell={runWithSelectionToolbarDismiss(props.onSplitCell)}
671
+ onSetCellBackground={props.onSetCellBackground
672
+ ? (color) => {
673
+ dismissSelectionToolbar();
674
+ props.onSetCellBackground?.(color);
675
+ }
676
+ : undefined}
677
+ />
678
+ ) : null}
679
+ {contextualSurface === "image" && props.activeImageContext ? (
680
+ <TwImageContextToolbar
681
+ activeImage={props.activeImageContext}
682
+ disabled={!caps?.canEdit}
683
+ onSetImageLayout={props.onSetImageLayout
684
+ ? (mediaId, dimensions) => {
685
+ dismissSelectionToolbar();
686
+ props.onSetImageLayout?.(mediaId, dimensions);
687
+ }
688
+ : undefined}
689
+ onSetImageFrame={props.onSetImageFrame
690
+ ? (mediaId, offsets) => {
691
+ dismissSelectionToolbar();
692
+ props.onSetImageFrame?.(mediaId, offsets);
693
+ }
694
+ : undefined}
695
+ />
696
+ ) : null}
697
+ {contextualSurface === "object" && props.activeObjectContext ? (
698
+ <TwObjectContextToolbar activeObject={props.activeObjectContext} />
699
+ ) : null}
412
700
  </div>
413
701
  ) : null}
414
702
  {props.selectionToolbar && selectionToolbarPlacement ? (
@@ -427,6 +715,8 @@ export function TwReviewWorkspace(props: TwReviewWorkspaceProps) {
427
715
  onToggleBold={props.onToggleBold}
428
716
  onToggleItalic={props.onToggleItalic}
429
717
  onToggleUnderline={props.onToggleUnderline}
718
+ onSetTextColor={props.onSetSelectionTextColor}
719
+ onSetHighlightColor={props.onSetSelectionHighlightColor}
430
720
  onAddComment={props.onAddCommentFromSelection ?? props.onAddComment}
431
721
  />
432
722
  </div>
@@ -447,6 +737,8 @@ export function TwReviewWorkspace(props: TwReviewWorkspaceProps) {
447
737
  onToggleBold={props.onToggleBold}
448
738
  onToggleItalic={props.onToggleItalic}
449
739
  onToggleUnderline={props.onToggleUnderline}
740
+ onSetTextColor={props.onSetSelectionTextColor}
741
+ onSetHighlightColor={props.onSetSelectionHighlightColor}
450
742
  onAddComment={props.onAddCommentFromSelection ?? props.onAddComment}
451
743
  />
452
744
  </div>
@@ -686,7 +978,12 @@ function buildPageChromeModel(
686
978
  return EMPTY_PAGE_CHROME_MODEL;
687
979
  }
688
980
 
689
- const lineMarkers = buildLineNumberMarkers(surface.blocks, navigation.pages);
981
+ const lineMarkers = computeLineMarkersIfEnabled({
982
+ pageLayout,
983
+ surfaceBlocks: surface.blocks,
984
+ pages: navigation.pages,
985
+ buildLineNumberMarkers,
986
+ });
690
987
  const lineNumberingEnabled =
691
988
  Boolean(pageLayout.lineNumbering) && lineMarkers.length > 0;
692
989
  const distance = pageLayout.lineNumbering?.distance ?? 0;
@@ -479,6 +479,15 @@ function collectLossyBlocks(
479
479
  const issues: string[] = [];
480
480
 
481
481
  for (const block of blocks) {
482
+ if (block.type === "table") {
483
+ for (const row of block.rows) {
484
+ for (const cell of row.cells) {
485
+ issues.push(...collectLossyBlocks(cell.children, `${surface}:table-cell`));
486
+ }
487
+ }
488
+ continue;
489
+ }
490
+
482
491
  if (block.type !== "paragraph") {
483
492
  issues.push(`${surface}:${block.type}`);
484
493
  continue;
@@ -486,9 +495,6 @@ function collectLossyBlocks(
486
495
 
487
496
  if (
488
497
  block.numbering !== undefined ||
489
- block.spacing !== undefined ||
490
- block.indentation !== undefined ||
491
- block.tabStops !== undefined ||
492
498
  block.keepNext !== undefined ||
493
499
  block.keepLines !== undefined ||
494
500
  block.outlineLevel !== undefined ||
@@ -517,6 +523,8 @@ function collectLossyInlineContent(
517
523
  ): string[] {
518
524
  switch (node.type) {
519
525
  case "text": {
526
+ const allowSecondaryStoryColorMarks =
527
+ surface.startsWith("header:") || surface.startsWith("footer:");
520
528
  const unsupportedMarks = (node.marks ?? [])
521
529
  .filter(
522
530
  (mark) =>
@@ -524,7 +532,14 @@ function collectLossyInlineContent(
524
532
  mark.type !== "italic" &&
525
533
  mark.type !== "underline" &&
526
534
  mark.type !== "strikethrough" &&
527
- mark.type !== "doubleStrikethrough",
535
+ mark.type !== "doubleStrikethrough" &&
536
+ mark.type !== "fontFamily" &&
537
+ mark.type !== "fontSize" &&
538
+ mark.type !== "textColor" &&
539
+ (!allowSecondaryStoryColorMarks || mark.type !== "backgroundColor") &&
540
+ (!allowSecondaryStoryColorMarks || mark.type !== "highlight") &&
541
+ mark.type !== "smallCaps" &&
542
+ mark.type !== "allCaps",
528
543
  )
529
544
  .map((mark) => `${surface}:mark:${mark.type}`);
530
545
  return unsupportedMarks;
@@ -532,6 +547,14 @@ function collectLossyInlineContent(
532
547
  case "tab":
533
548
  case "hard_break":
534
549
  case "footnote_ref":
550
+ case "field":
551
+ case "bookmark_start":
552
+ case "bookmark_end":
553
+ case "shape":
554
+ case "wordart":
555
+ case "vml_shape":
556
+ case "chart_preview":
557
+ case "smartart_preview":
535
558
  return [];
536
559
  default:
537
560
  return [`${surface}:${node.type}`];
@@ -44,6 +44,7 @@ export const COMPATIBILITY_FEATURE_KEYS = [
44
44
  "unknown-package-parts",
45
45
  "package-integrity",
46
46
  "relationship-integrity",
47
+ "workflow-scopes",
47
48
  ] as const;
48
49
 
49
50
  export type CompatibilityFeatureKey =