@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
@@ -28,6 +28,7 @@ import type {
28
28
  StyleCatalogSnapshot,
29
29
  SurfaceBlockSnapshot,
30
30
  TrackedChangeEntrySnapshot,
31
+ WordReviewEditorChromeVisibility,
31
32
  WorkflowScopeSnapshot,
32
33
  WorkspaceMode,
33
34
  ZoomLevel,
@@ -49,6 +50,7 @@ import type { SessionCapabilities } from "../runtime/session-capabilities";
49
50
  import type {
50
51
  SelectionToolbarAnchor,
51
52
  SelectionToolbarModel,
53
+ SuggestionCardModel,
52
54
  } from "../ui/headless/selection-toolbar-model";
53
55
  import type { MarkupDisplay } from "../ui/headless/comment-decoration-model";
54
56
  import type { EditorCommandBag } from "../ui/editor-command-bag.ts";
@@ -59,10 +61,13 @@ import { TwLayoutPanel } from "./chrome/tw-layout-panel";
59
61
  import { TwObjectContextToolbar, type ActiveObjectContext } from "./chrome/tw-object-context-toolbar";
60
62
  import { TwPageRuler } from "./chrome/tw-page-ruler";
61
63
  import { TwSelectionToolbar } from "./chrome/tw-selection-toolbar";
64
+ import { TwSuggestionCard } from "./chrome/tw-suggestion-card";
62
65
  import { TwTableContextToolbar } from "./chrome/tw-table-context-toolbar";
63
66
  import { TwReviewRail, type ReviewRailTab } from "./review/tw-review-rail";
64
67
  import { TwStatusBar } from "./status/tw-status-bar";
65
- import { TwToolbar } from "./toolbar/tw-toolbar";
68
+ import { TwToolbar, type ToolbarInteractionPolicy } from "./toolbar/tw-toolbar";
69
+
70
+ export type ReviewWorkspaceChromeVisibility = WordReviewEditorChromeVisibility;
66
71
 
67
72
  export interface TwReviewWorkspaceProps {
68
73
  snapshot: RuntimeRenderSnapshot;
@@ -84,6 +89,7 @@ export interface TwReviewWorkspaceProps {
84
89
  interactionGuardSnapshot?: InteractionGuardSnapshot;
85
90
  commands: EditorCommandBag;
86
91
  selectionToolbar?: SelectionToolbarModel | null;
92
+ suggestionCard?: SuggestionCardModel | null;
87
93
  selectionToolbarAnchor?: SelectionToolbarAnchor | null;
88
94
  documentNavigation?: DocumentNavigationSnapshot;
89
95
  onWorkspaceModeChange?: (value: WorkspaceMode) => void;
@@ -170,6 +176,10 @@ export interface TwReviewWorkspaceProps {
170
176
  onAddCommentFromSelection?: () => void;
171
177
  onExport?: () => void;
172
178
  onDismissSelectionToolbar?: () => void;
179
+ onAcceptSuggestion?: () => void;
180
+ onRejectSuggestion?: () => void;
181
+ onEditSuggestion?: () => void;
182
+ onAddCommentFromSuggestion?: () => void;
173
183
  onSelectionToolbarFocusCapture?: FocusEventHandler<HTMLDivElement>;
174
184
  onSelectionToolbarBlurCapture?: FocusEventHandler<HTMLDivElement>;
175
185
  selectionToolbarRef?: Ref<HTMLDivElement>;
@@ -196,6 +206,7 @@ export interface TwReviewWorkspaceProps {
196
206
  onRestartNumbering?: () => void;
197
207
  onContinueNumbering?: () => void;
198
208
  onNavigateHeading?: (headingId: string) => void;
209
+ chromeVisibility?: Partial<ReviewWorkspaceChromeVisibility>;
199
210
  }
200
211
 
201
212
  export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
@@ -220,7 +231,17 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
220
231
  props.interactionGuardSnapshot?.blockedReasons ??
221
232
  props.workflowScopeSnapshot?.blockedReasons ??
222
233
  [];
223
- const showReviewRail = caps?.reviewRailVisible ?? true;
234
+ const chromeVisibility: ReviewWorkspaceChromeVisibility = {
235
+ toolbar: true,
236
+ alerts: true,
237
+ selectionOverlay: true,
238
+ contextToolbars: true,
239
+ pageChrome: true,
240
+ statusBar: true,
241
+ reviewRail: true,
242
+ ...props.chromeVisibility,
243
+ };
244
+ const showReviewRail = chromeVisibility.reviewRail && (caps?.reviewRailVisible ?? true);
224
245
  const headings = props.documentNavigation?.headings ?? [];
225
246
  const headerVariant = snapshot.pageLayout?.headerVariants[0]?.variant ?? "default";
226
247
  const footerVariant = snapshot.pageLayout?.footerVariants[0]?.variant ?? "default";
@@ -267,6 +288,23 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
267
288
  isPageWorkspace &&
268
289
  snapshot.activeStory.kind === "main" &&
269
290
  shouldHidePageBorderForSelection(viewState.selection);
291
+ const effectiveSelectionMode = props.interactionGuardSnapshot?.effectiveMode ?? "edit";
292
+ const allowLocalChromeMutations = Boolean(caps?.canEdit) && effectiveSelectionMode === "edit";
293
+ const pageChromeReadOnly =
294
+ snapshot.readOnly ||
295
+ snapshot.activeStory.kind !== "main" ||
296
+ effectiveSelectionMode !== "edit";
297
+ const toolbarInteractionPolicy: ToolbarInteractionPolicy | undefined = caps
298
+ ? {
299
+ mode: effectiveSelectionMode,
300
+ canFormatText: caps.canEdit && effectiveSelectionMode === "edit",
301
+ canInsertStructural: caps.canEdit && effectiveSelectionMode === "edit",
302
+ canAddComment:
303
+ caps.canAddComment &&
304
+ effectiveSelectionMode !== "view" &&
305
+ effectiveSelectionMode !== "blocked",
306
+ }
307
+ : undefined;
270
308
 
271
309
  useEffect(() => {
272
310
  recordPerfSample("workspace.chrome");
@@ -294,11 +332,12 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
294
332
  return (
295
333
  <Tooltip.Provider delayDuration={400}>
296
334
  <div className="flex h-full flex-col bg-canvas text-primary">
297
- <TwToolbar
335
+ {chromeVisibility.toolbar ? <TwToolbar
298
336
  sourceLabel={snapshot.sourceLabel}
299
337
  capabilities={caps}
300
338
  compatibility={snapshot.compatibility}
301
339
  warnings={snapshot.warnings}
340
+ interactionPolicy={toolbarInteractionPolicy}
302
341
  workspaceMode={props.workspaceMode}
303
342
  zoomLevel={props.zoomLevel}
304
343
  formattingState={props.formattingState}
@@ -385,17 +424,17 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
385
424
  props.onShowTrackedChangesChange(show);
386
425
  }}
387
426
  blockedReasons={blockedReasons}
388
- />
427
+ /> : null}
389
428
 
390
- <TwAlertBanner
429
+ {chromeVisibility.alerts ? <TwAlertBanner
391
430
  snapshot={snapshot}
392
431
  preserveOnlyCount={preserveOnlyCount}
393
432
  workflowBlockedReasons={blockedReasons}
394
- />
433
+ /> : null}
395
434
 
396
435
  <div className="flex flex-1 min-h-0">
397
436
  {/* Collapsible document navigator — page mode only */}
398
- {isPageWorkspace ? (
437
+ {isPageWorkspace && chromeVisibility.pageChrome ? (
399
438
  <aside
400
439
  aria-label="Document navigator"
401
440
  className={`shrink-0 border-r border-border bg-surface transition-[width] duration-200 ${
@@ -459,7 +498,7 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
459
498
  ) : null}
460
499
 
461
500
  {/* Navigator expand toggle — page mode only when collapsed */}
462
- {isPageWorkspace && !navOpen ? (
501
+ {isPageWorkspace && chromeVisibility.pageChrome && !navOpen ? (
463
502
  <div className="shrink-0 flex items-start pt-2 pl-1">
464
503
  <Tooltip.Root>
465
504
  <Tooltip.Trigger asChild>
@@ -500,7 +539,7 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
500
539
  }`}
501
540
  style={isPageWorkspace && zoomScale !== 1 ? { transform: `scale(${zoomScale})`, transformOrigin: "top center" } : undefined}
502
541
  >
503
- {isPageWorkspace && snapshot.pageLayout ? (
542
+ {isPageWorkspace && chromeVisibility.pageChrome && snapshot.pageLayout ? (
504
543
  <div className="border-b border-border/70 bg-surface/65 px-5 py-3" data-testid="page-context-summary">
505
544
  <div className="flex flex-wrap items-center justify-between gap-2">
506
545
  <div className="flex flex-wrap items-center gap-2 text-xs text-secondary">
@@ -531,7 +570,7 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
531
570
  <button
532
571
  type="button"
533
572
  aria-label="Link header to previous"
534
- disabled={!props.onSetHeaderFooterLink}
573
+ disabled={!props.onSetHeaderFooterLink || !allowLocalChromeMutations}
535
574
  onMouseDown={preserveEditorSelectionMouseDown}
536
575
  onClick={() => {
537
576
  dismissSelectionToolbar();
@@ -548,7 +587,7 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
548
587
  <button
549
588
  type="button"
550
589
  aria-label="Link footer to previous"
551
- disabled={!props.onSetHeaderFooterLink}
590
+ disabled={!props.onSetHeaderFooterLink || !allowLocalChromeMutations}
552
591
  onMouseDown={preserveEditorSelectionMouseDown}
553
592
  onClick={() => {
554
593
  dismissSelectionToolbar();
@@ -582,13 +621,13 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
582
621
  </div>
583
622
  </div>
584
623
  ) : null}
585
- {isPageWorkspace && snapshot.pageLayout && layoutToolsOpen ? (
624
+ {isPageWorkspace && chromeVisibility.pageChrome && snapshot.pageLayout && layoutToolsOpen ? (
586
625
  <div className="px-5 pt-3">
587
626
  <TwPageRuler
588
627
  pageLayout={snapshot.pageLayout}
589
628
  viewState={viewState}
590
629
  paragraphLayout={activeParagraphLayout}
591
- readOnly={snapshot.readOnly}
630
+ readOnly={pageChromeReadOnly}
592
631
  onReturnToBody={props.onCloseStory
593
632
  ? runWithSelectionToolbarDismiss(props.onCloseStory)
594
633
  : () => undefined}
@@ -619,7 +658,7 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
619
658
  />
620
659
  <TwLayoutPanel
621
660
  pageLayout={snapshot.pageLayout}
622
- readOnly={snapshot.readOnly || snapshot.activeStory.kind !== "main"}
661
+ readOnly={pageChromeReadOnly}
623
662
  onInsertSectionBreak={props.onInsertSectionBreak
624
663
  ? (type) => {
625
664
  dismissSelectionToolbar();
@@ -647,11 +686,11 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
647
686
  />
648
687
  </div>
649
688
  ) : null}
650
- {contextualSurface ? (
689
+ {chromeVisibility.contextToolbars && contextualSurface ? (
651
690
  <div className="px-5 pt-3 space-y-3">
652
691
  {contextualSurface === "table" ? (
653
692
  <TwTableContextToolbar
654
- disabled={!caps?.canEdit}
693
+ disabled={!allowLocalChromeMutations}
655
694
  tableStyles={props.styleCatalog?.tables ?? []}
656
695
  onSetTableStyle={props.onSetTableStyle
657
696
  ? (styleId) => {
@@ -679,7 +718,7 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
679
718
  {contextualSurface === "image" && props.activeImageContext ? (
680
719
  <TwImageContextToolbar
681
720
  activeImage={props.activeImageContext}
682
- disabled={!caps?.canEdit}
721
+ disabled={!allowLocalChromeMutations}
683
722
  onSetImageLayout={props.onSetImageLayout
684
723
  ? (mediaId, dimensions) => {
685
724
  dismissSelectionToolbar();
@@ -699,7 +738,44 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
699
738
  ) : null}
700
739
  </div>
701
740
  ) : null}
702
- {props.selectionToolbar && selectionToolbarPlacement ? (
741
+ {chromeVisibility.selectionOverlay && props.suggestionCard && selectionToolbarPlacement ? (
742
+ <div className="pointer-events-none absolute inset-0 z-20" data-testid="suggestion-card-overlay">
743
+ <div
744
+ className="pointer-events-auto absolute"
745
+ data-placement={selectionToolbarPlacement.placement}
746
+ style={selectionToolbarPlacement.style}
747
+ >
748
+ <TwSuggestionCard
749
+ model={props.suggestionCard}
750
+ onFocusCapture={props.onSelectionToolbarFocusCapture}
751
+ onBlurCapture={props.onSelectionToolbarBlurCapture}
752
+ onAccept={props.onAcceptSuggestion}
753
+ onReject={props.onRejectSuggestion}
754
+ onEditSuggestion={props.onEditSuggestion}
755
+ onAddComment={props.onAddCommentFromSuggestion ?? props.onAddComment}
756
+ />
757
+ </div>
758
+ </div>
759
+ ) : null}
760
+ {chromeVisibility.selectionOverlay && props.suggestionCard && !selectionToolbarPlacement ? (
761
+ <div
762
+ className="pointer-events-none absolute inset-x-0 top-0 z-20 flex justify-center px-4 pt-3"
763
+ data-testid="suggestion-card-fallback"
764
+ >
765
+ <div className="pointer-events-auto" data-placement="fallback">
766
+ <TwSuggestionCard
767
+ model={props.suggestionCard}
768
+ onFocusCapture={props.onSelectionToolbarFocusCapture}
769
+ onBlurCapture={props.onSelectionToolbarBlurCapture}
770
+ onAccept={props.onAcceptSuggestion}
771
+ onReject={props.onRejectSuggestion}
772
+ onEditSuggestion={props.onEditSuggestion}
773
+ onAddComment={props.onAddCommentFromSuggestion ?? props.onAddComment}
774
+ />
775
+ </div>
776
+ </div>
777
+ ) : null}
778
+ {chromeVisibility.selectionOverlay && props.selectionToolbar && !props.suggestionCard && selectionToolbarPlacement ? (
703
779
  <div className="pointer-events-none absolute inset-0 z-20" data-testid="selection-toolbar-overlay">
704
780
  <div
705
781
  className="pointer-events-auto absolute"
@@ -722,7 +798,7 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
722
798
  </div>
723
799
  </div>
724
800
  ) : null}
725
- {props.selectionToolbar && !selectionToolbarPlacement ? (
801
+ {chromeVisibility.selectionOverlay && props.selectionToolbar && !props.suggestionCard && !selectionToolbarPlacement ? (
726
802
  <div
727
803
  className="pointer-events-none absolute inset-x-0 top-0 z-20 flex justify-center px-4 pt-3"
728
804
  data-testid="selection-toolbar-fallback"
@@ -748,7 +824,7 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
748
824
  className={isPageWorkspace ? "relative" : undefined}
749
825
  data-line-numbering={pageChromeModel.lineNumberingEnabled ? "enabled" : "disabled"}
750
826
  >
751
- {isPageWorkspace && pageChromeModel.lineNumberingEnabled ? (
827
+ {isPageWorkspace && chromeVisibility.pageChrome && pageChromeModel.lineNumberingEnabled ? (
752
828
  <div
753
829
  aria-hidden="true"
754
830
  className="pointer-events-none absolute inset-y-0 left-0 z-10"
@@ -767,7 +843,7 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
767
843
  </div>
768
844
  ) : null}
769
845
  <div
770
- className={isPageWorkspace && pageChromeModel.lineNumberingEnabled ? "pl-12" : undefined}
846
+ className={isPageWorkspace && chromeVisibility.pageChrome && pageChromeModel.lineNumberingEnabled ? "pl-12" : undefined}
771
847
  style={isPageWorkspace ? pageShellMetrics.contentInsetStyle : undefined}
772
848
  >
773
849
  <div
@@ -781,7 +857,7 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
781
857
  }
782
858
  : pageChromeModel.documentGridStyle}
783
859
  >
784
- {isPageWorkspace ? (
860
+ {isPageWorkspace && chromeVisibility.pageChrome ? (
785
861
  <div
786
862
  data-testid="page-header-band"
787
863
  className="relative z-10 flex items-center justify-between border-b border-dashed border-border/60 px-4 text-[11px] text-secondary"
@@ -800,7 +876,7 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
800
876
  ) : null}
801
877
  </div>
802
878
  ) : null}
803
- {isPageWorkspace && pageChromeModel.showPageBorder && !hidePageBorderForActiveEditing ? (
879
+ {isPageWorkspace && chromeVisibility.pageChrome && pageChromeModel.showPageBorder && !hidePageBorderForActiveEditing ? (
804
880
  <div
805
881
  aria-hidden="true"
806
882
  className="pointer-events-none absolute inset-0 z-0 rounded-[2px]"
@@ -811,7 +887,7 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
811
887
  <div className={isPageWorkspace ? "relative z-10" : undefined}>
812
888
  {props.document}
813
889
  </div>
814
- {isPageWorkspace ? (
890
+ {isPageWorkspace && chromeVisibility.pageChrome ? (
815
891
  <div
816
892
  data-testid="page-footer-band"
817
893
  className="relative z-10 flex items-center justify-between border-t border-dashed border-border/60 px-4 text-[11px] text-secondary"
@@ -836,14 +912,16 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
836
912
  </div>
837
913
  </div>
838
914
 
839
- <TwStatusBar
840
- isDirty={snapshot.isDirty}
841
- isExportBlocked={snapshot.compatibility.blockExport}
842
- preserveOnlyCount={preserveOnlyCount}
843
- commentCount={snapshot.comments.totalCount}
844
- changeCount={snapshot.trackedChanges.totalCount}
845
- sessionId={snapshot.sessionId}
846
- />
915
+ {chromeVisibility.statusBar ? (
916
+ <TwStatusBar
917
+ isDirty={snapshot.isDirty}
918
+ isExportBlocked={snapshot.compatibility.blockExport}
919
+ preserveOnlyCount={preserveOnlyCount}
920
+ commentCount={snapshot.comments.totalCount}
921
+ changeCount={snapshot.trackedChanges.totalCount}
922
+ sessionId={snapshot.sessionId}
923
+ />
924
+ ) : null}
847
925
  </div>
848
926
 
849
927
  {/* Review rail — hidden in editing mode unless toggled */}