@beyondwork/docx-react-component 1.0.87 → 1.0.89

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 (47) hide show
  1. package/package.json +1 -1
  2. package/src/api/v3/_runtime-handle.ts +5 -0
  3. package/src/api/v3/ai/replacement.ts +82 -0
  4. package/src/api/v3/runtime/content.ts +3 -0
  5. package/src/api/v3/runtime/formatting.ts +64 -0
  6. package/src/core/commands/formatting-commands.ts +107 -0
  7. package/src/core/state/text-transaction.ts +11 -4
  8. package/src/runtime/document-runtime.ts +293 -27
  9. package/src/runtime/scopes/_scope-dependencies.ts +1 -0
  10. package/src/runtime/scopes/action-validation.ts +12 -3
  11. package/src/runtime/scopes/audit-bundle.ts +2 -2
  12. package/src/runtime/scopes/compiler-service.ts +70 -0
  13. package/src/runtime/scopes/formatting/apply.ts +262 -0
  14. package/src/runtime/scopes/index.ts +12 -0
  15. package/src/runtime/scopes/replacement/propose.ts +2 -0
  16. package/src/runtime/scopes/scope-kinds/paragraph.ts +1 -0
  17. package/src/runtime/scopes/semantic-scope-types.ts +48 -4
  18. package/src/runtime/scopes/workflow-overlap.ts +9 -11
  19. package/src/shell/session-bootstrap.ts +1 -0
  20. package/src/ui/WordReviewEditor.tsx +277 -28
  21. package/src/ui/editor-command-bag.ts +11 -0
  22. package/src/ui/editor-shell-view.tsx +10 -0
  23. package/src/ui/headless/chrome-registry.ts +6 -6
  24. package/src/ui/headless/role-action-sets.ts +4 -10
  25. package/src/ui/headless/selection-tool-resolver.ts +11 -0
  26. package/src/ui-tailwind/chrome/editor-action-registry.ts +1 -1
  27. package/src/ui-tailwind/chrome/tw-context-band.tsx +7 -7
  28. package/src/ui-tailwind/chrome/tw-detach-handle.tsx +13 -18
  29. package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +8 -5
  30. package/src/ui-tailwind/chrome/tw-inline-find-bar.tsx +100 -0
  31. package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +0 -40
  32. package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +9 -7
  33. package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +17 -1
  34. package/src/ui-tailwind/chrome-overlay/tw-scope-card-layer.tsx +6 -1
  35. package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +17 -7
  36. package/src/ui-tailwind/editor-surface/preserve-position.ts +61 -11
  37. package/src/ui-tailwind/editor-surface/scroll-anchor.ts +20 -5
  38. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +52 -6
  39. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +5 -1
  40. package/src/ui-tailwind/review/tw-review-rail.tsx +5 -0
  41. package/src/ui-tailwind/review/tw-workflow-tab.tsx +32 -38
  42. package/src/ui-tailwind/review-workspace/types.ts +2 -0
  43. package/src/ui-tailwind/theme/editor-theme.css +25 -12
  44. package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +13 -4
  45. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +6 -15
  46. package/src/ui-tailwind/tw-review-workspace.tsx +28 -18
  47. package/src/ui-tailwind/workflow-scope-layers.ts +70 -0
@@ -54,6 +54,7 @@ import type {
54
54
  SurfaceInlineSegment,
55
55
  TableOp,
56
56
  CanonicalDocumentFragment,
57
+ CanonicalDocumentEnvelope,
57
58
  TableOpResult,
58
59
  PublicTableEvent,
59
60
  PublicTableRenderPlan,
@@ -136,6 +137,7 @@ import { findTextMatchesForRuntime, searchRuntimeDocument } from "../shell/searc
136
137
  import {
137
138
  type TwProseMirrorSurfaceRef,
138
139
  } from "../ui-tailwind/editor-surface/tw-prosemirror-surface";
140
+ import { TwInlineFindBar } from "../ui-tailwind/chrome/tw-inline-find-bar";
139
141
  import type { MediaPreviewDescriptor } from "../ui-tailwind/editor-surface/pm-state-from-snapshot";
140
142
  import {
141
143
  incrementInvalidationCounter,
@@ -1062,13 +1064,17 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
1062
1064
  } = props;
1063
1065
 
1064
1066
  const [activeRailTab, setActiveRailTab] = useState<ReviewRailTab>("comments");
1067
+ const [inlineFindOpen, setInlineFindOpen] = useState(false);
1068
+ const [inlineFindQuery, setInlineFindQuery] = useState("");
1069
+ const [inlineFindResults, setInlineFindResults] = useState<readonly SearchResultSnapshot[]>([]);
1070
+ const [inlineFindActiveIndex, setInlineFindActiveIndex] = useState(0);
1065
1071
  const [localMarkupDisplay, setLocalMarkupDisplay] =
1066
1072
  useState<WorkflowMarkupMode | null>(() => suggestionsEnabled ? "all" : null);
1067
1073
  const [activeRevisionId, setActiveRevisionId] = useState<string | undefined>();
1068
1074
  const [suppressedSuggestionRevisionId, setSuppressedSuggestionRevisionId] = useState<string | null>(null);
1069
1075
  const [selectionToolbarAnchor, setSelectionToolbarAnchor] = useState<SelectionToolbarAnchor | null>(null);
1070
1076
  const [selectionToolbarDismissedKey, setSelectionToolbarDismissedKey] = useState<string | null>(null);
1071
- const [selectionToolbarFocusWithin, setSelectionToolbarFocusWithin] = useState(false);
1077
+ const [, setSelectionToolbarFocusWithin] = useState(false);
1072
1078
  const [hostAnnotationOverlayState, setHostAnnotationOverlayState] =
1073
1079
  useState<HostAnnotationOverlay | null>(null);
1074
1080
  const [activeReviewQueueItemId, setActiveReviewQueueItemId] = useState<string | null>(null);
@@ -2536,6 +2542,17 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
2536
2542
  !capabilities.canAddComment && !snapshot.selection.isCollapsed
2537
2543
  ? "Select text within one paragraph to add a DOCX comment."
2538
2544
  : undefined;
2545
+ const selectionWorkflowPosture = resolveSelectionWorkflowPosture(
2546
+ snapshot,
2547
+ viewState,
2548
+ workflowScopeSnapshot,
2549
+ interactionGuardSnapshot,
2550
+ );
2551
+ const effectiveInteractionGuardSnapshot = createSelectionScopedInteractionGuardSnapshot(
2552
+ interactionGuardSnapshot,
2553
+ selectionWorkflowPosture,
2554
+ workflowScopeSnapshot,
2555
+ );
2539
2556
  const activeImageContext = useMemo(
2540
2557
  () =>
2541
2558
  buildActiveImageContext({
@@ -2584,16 +2601,117 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
2584
2601
  ),
2585
2602
  [canonicalDocument, snapshot],
2586
2603
  );
2604
+ const navigateInlineFindResult = useCallback(
2605
+ (results: readonly SearchResultSnapshot[], index: number) => {
2606
+ const result = results[index];
2607
+ if (!result) return;
2608
+ applyRuntimeSelection(
2609
+ activeRuntime,
2610
+ createSelectionFromAnchor(result.anchor, result.storyTarget),
2611
+ );
2612
+ },
2613
+ [activeRuntime],
2614
+ );
2615
+ const refreshInlineFindResults = useCallback(
2616
+ (query: string, requestedIndex = 0) => {
2617
+ const normalizedQuery = query.trim();
2618
+ if (!normalizedQuery) {
2619
+ surfaceRef.current?.clearSearch();
2620
+ setInlineFindResults([]);
2621
+ setInlineFindActiveIndex(0);
2622
+ return;
2623
+ }
2624
+ const results = searchRuntimeDocument(
2625
+ activeRuntime,
2626
+ surfaceRef.current,
2627
+ normalizedQuery,
2628
+ { limit: 500 },
2629
+ );
2630
+ const nextIndex =
2631
+ results.length > 0 ? wrapSearchResultIndex(requestedIndex, results.length) : 0;
2632
+ setInlineFindResults(results);
2633
+ setInlineFindActiveIndex(nextIndex);
2634
+ navigateInlineFindResult(results, nextIndex);
2635
+ },
2636
+ [activeRuntime, navigateInlineFindResult],
2637
+ );
2638
+ const handleOpenInlineFind = useCallback(() => {
2639
+ setInlineFindOpen(true);
2640
+ if (inlineFindQuery.trim()) {
2641
+ refreshInlineFindResults(inlineFindQuery, inlineFindActiveIndex);
2642
+ }
2643
+ }, [inlineFindActiveIndex, inlineFindQuery, refreshInlineFindResults]);
2644
+ const handleCloseInlineFind = useCallback(() => {
2645
+ setInlineFindOpen(false);
2646
+ surfaceRef.current?.clearSearch();
2647
+ }, []);
2648
+ const handleInlineFindQueryChange = useCallback(
2649
+ (query: string) => {
2650
+ setInlineFindQuery(query);
2651
+ refreshInlineFindResults(query, 0);
2652
+ },
2653
+ [refreshInlineFindResults],
2654
+ );
2655
+ const handleInlineFindNext = useCallback(() => {
2656
+ if (inlineFindResults.length === 0) {
2657
+ refreshInlineFindResults(inlineFindQuery, 0);
2658
+ return;
2659
+ }
2660
+ const nextIndex = wrapSearchResultIndex(
2661
+ inlineFindActiveIndex + 1,
2662
+ inlineFindResults.length,
2663
+ );
2664
+ setInlineFindActiveIndex(nextIndex);
2665
+ navigateInlineFindResult(inlineFindResults, nextIndex);
2666
+ }, [
2667
+ inlineFindActiveIndex,
2668
+ inlineFindQuery,
2669
+ inlineFindResults,
2670
+ navigateInlineFindResult,
2671
+ refreshInlineFindResults,
2672
+ ]);
2673
+ const handleInlineFindPrevious = useCallback(() => {
2674
+ if (inlineFindResults.length === 0) {
2675
+ refreshInlineFindResults(inlineFindQuery, 0);
2676
+ return;
2677
+ }
2678
+ const nextIndex = wrapSearchResultIndex(
2679
+ inlineFindActiveIndex - 1,
2680
+ inlineFindResults.length,
2681
+ );
2682
+ setInlineFindActiveIndex(nextIndex);
2683
+ navigateInlineFindResult(inlineFindResults, nextIndex);
2684
+ }, [
2685
+ inlineFindActiveIndex,
2686
+ inlineFindQuery,
2687
+ inlineFindResults,
2688
+ navigateInlineFindResult,
2689
+ refreshInlineFindResults,
2690
+ ]);
2587
2691
  const suggestionsSnapshot = activeRuntime.getSuggestionsSnapshot();
2692
+ const caretCommentPoint =
2693
+ snapshot.selection.isCollapsed && snapshot.selection.activeRange.kind === "range"
2694
+ ? snapshot.selection.activeRange.from
2695
+ : null;
2696
+ const caretCommentThread =
2697
+ caretCommentPoint !== null
2698
+ ? snapshot.comments.threads.find((thread) => {
2699
+ if (thread.status !== "open" || thread.anchor.kind !== "range") {
2700
+ return false;
2701
+ }
2702
+ return caretCommentPoint >= thread.anchor.from && caretCommentPoint <= thread.anchor.to;
2703
+ })
2704
+ : undefined;
2588
2705
  const activeCommentThread =
2589
- snapshot.comments.activeCommentId
2706
+ (snapshot.comments.activeCommentId
2590
2707
  ? snapshot.comments.threads.find((thread) => thread.commentId === snapshot.comments.activeCommentId)
2591
- : undefined;
2708
+ : undefined) ?? caretCommentThread;
2709
+ const effectiveActiveCommentId = snapshot.comments.activeCommentId ?? activeCommentThread?.commentId;
2592
2710
  const scopedChromePolicy = resolveScopedChromePolicy({
2593
2711
  preset: effectiveChromePreset,
2594
2712
  compactMode: false,
2595
2713
  capabilities,
2596
- interactionGuardSnapshot,
2714
+ interactionGuardSnapshot: effectiveInteractionGuardSnapshot,
2597
2715
  workflowScopeSnapshot,
2598
2716
  activeListContext: viewState.activeListContext,
2599
2717
  });
@@ -2605,7 +2723,7 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
2605
2723
  styleCatalog,
2606
2724
  formattingState,
2607
2725
  workflowScopeSnapshot,
2608
- interactionGuardSnapshot,
2726
+ interactionGuardSnapshot: effectiveInteractionGuardSnapshot,
2609
2727
  addCommentDisabledReason,
2610
2728
  });
2611
2729
  const suggestionCard = buildSuggestionCardModel({
@@ -2614,7 +2732,7 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
2614
2732
  viewState,
2615
2733
  capabilities,
2616
2734
  workflowScopeSnapshot,
2617
- interactionGuardSnapshot,
2735
+ interactionGuardSnapshot: effectiveInteractionGuardSnapshot,
2618
2736
  activeRevisionId,
2619
2737
  suppressedSuggestionRevisionId,
2620
2738
  addCommentDisabledReason,
@@ -2659,11 +2777,11 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
2659
2777
  styleCatalog,
2660
2778
  formattingState,
2661
2779
  workflowScopeSnapshot,
2662
- interactionGuardSnapshot,
2780
+ interactionGuardSnapshot: effectiveInteractionGuardSnapshot,
2663
2781
  workflowMarkupSnapshot: workflowMarkupSnapshot ?? undefined,
2664
2782
  suggestionsSnapshot,
2665
2783
  activeRevisionId,
2666
- activeCommentId: snapshot.comments.activeCommentId,
2784
+ activeCommentId: effectiveActiveCommentId,
2667
2785
  activeCommentThread,
2668
2786
  activeTableContext,
2669
2787
  activeImageContext,
@@ -2682,19 +2800,14 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
2682
2800
  snapshot.selection,
2683
2801
  viewState.activeStory,
2684
2802
  activeRevisionId,
2685
- snapshot.comments.activeCommentId,
2803
+ effectiveActiveCommentId,
2686
2804
  ),
2687
- [activeRevisionId, snapshot.comments.activeCommentId, snapshot.selection, viewState.activeStory],
2805
+ [activeRevisionId, effectiveActiveCommentId, snapshot.selection, viewState.activeStory],
2688
2806
  );
2689
2807
  const shouldRenderSelectionChrome = Boolean(
2690
2808
  activeSelectionTool &&
2691
2809
  selectionToolbarSelectionKey &&
2692
- selectionToolbarDismissedKey !== selectionToolbarSelectionKey &&
2693
- (
2694
- viewState.isFocused ||
2695
- selectionToolbarFocusWithin ||
2696
- activeSelectionTool.kind === "structure-context"
2697
- ),
2810
+ selectionToolbarDismissedKey !== selectionToolbarSelectionKey,
2698
2811
  );
2699
2812
  const accessibilityInstructionsId = `${documentId}-accessibility-instructions`;
2700
2813
  const accessibilityStatusId = `${documentId}-accessibility-status`;
@@ -2978,16 +3091,20 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
2978
3091
  // not, the event falls through to the browser (Ctrl+F opens
2979
3092
  // Find, Ctrl+Plus zooms, etc.) — matching the legacy behavior.
2980
3093
  let handled = false;
2981
- if (shortcut.shortcut === "find" && onFindRequested) {
2982
- // selectionText is intentionally empty — hosts that need the
2983
- // selected text already receive it via the selection_changed
2984
- // event + canonicalDocument they have via onEvent listeners.
2985
- // The range is the load-bearing field so host Find panels can
2986
- // scope their search to the selection or pre-populate from it.
2987
- onFindRequested({
2988
- selectionText: "",
2989
- selectionRange: snapshot.selection,
2990
- });
3094
+ if (shortcut.shortcut === "find") {
3095
+ if (onFindRequested) {
3096
+ // selectionText is intentionally empty hosts that need the
3097
+ // selected text already receive it via the selection_changed
3098
+ // event + canonicalDocument they have via onEvent listeners.
3099
+ // The range is the load-bearing field so host Find panels can
3100
+ // scope their search to the selection or pre-populate from it.
3101
+ onFindRequested({
3102
+ selectionText: "",
3103
+ selectionRange: snapshot.selection,
3104
+ });
3105
+ } else {
3106
+ handleOpenInlineFind();
3107
+ }
2991
3108
  handled = true;
2992
3109
  } else if (shortcut.shortcut === "print" && onPrintRequested) {
2993
3110
  onPrintRequested();
@@ -3373,6 +3490,54 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
3373
3490
  type: "set-cell-background",
3374
3491
  color,
3375
3492
  }),
3493
+ onToggleRowHeader: () => {
3494
+ if (!activeTableContext?.operations.setRowIsHeader.enabled) return;
3495
+ applyRuntimeTableStructureOperation(activeRuntime, surfaceRef.current ?? null, {
3496
+ type: "set-row-is-header",
3497
+ rowIndex: activeTableContext.currentCell.rowIndex,
3498
+ value: !activeTableContext.currentCell.isHeader,
3499
+ });
3500
+ },
3501
+ onToggleRowCantSplit: () => {
3502
+ if (!activeTableContext?.operations.setRowCantSplit.enabled) return;
3503
+ applyRuntimeTableStructureOperation(activeRuntime, surfaceRef.current ?? null, {
3504
+ type: "set-row-cant-split",
3505
+ rowIndex: activeTableContext.currentCell.rowIndex,
3506
+ value: !readTableRowCantSplit(
3507
+ canonicalDocument,
3508
+ activeTableContext.tableBlockIndex,
3509
+ activeTableContext.currentCell.rowIndex,
3510
+ ),
3511
+ });
3512
+ },
3513
+ onDistributeColumnsEvenly: () =>
3514
+ applyRuntimeTableStructureOperation(activeRuntime, surfaceRef.current ?? null, {
3515
+ type: "distribute-columns-evenly",
3516
+ }),
3517
+ onSetTableAlignment: (alignment) =>
3518
+ applyRuntimeTableStructureOperation(activeRuntime, surfaceRef.current ?? null, {
3519
+ type: "set-table-alignment",
3520
+ alignment,
3521
+ }),
3522
+ onSetCellVerticalAlign: (align) =>
3523
+ applyRuntimeTableStructureOperation(activeRuntime, surfaceRef.current ?? null, {
3524
+ type: "set-cell-vertical-align",
3525
+ locator: { kind: "anchor" },
3526
+ align,
3527
+ }),
3528
+ onSetColumnWidth: (columnIndex, twips) =>
3529
+ applyRuntimeTableStructureOperation(activeRuntime, surfaceRef.current ?? null, {
3530
+ type: "set-column-width",
3531
+ columnIndex,
3532
+ twips,
3533
+ }),
3534
+ onSetRowHeight: (rowIndex, twips, rule) =>
3535
+ applyRuntimeTableStructureOperation(activeRuntime, surfaceRef.current ?? null, {
3536
+ type: "set-row-height",
3537
+ rowIndex,
3538
+ twips,
3539
+ rule,
3540
+ }),
3376
3541
  onSetImageLayout: (mediaId, dimensions) =>
3377
3542
  applyRuntimeImageResize(activeRuntime, mediaId, dimensions),
3378
3543
  onSetImageFrame: (mediaId, offsets) =>
@@ -3508,7 +3673,7 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
3508
3673
  onAddComment: commands.onAddComment,
3509
3674
  onFindRequested: onFindRequested
3510
3675
  ? () => onFindRequested({ selectionText: "", selectionRange: snapshot.selection })
3511
- : undefined,
3676
+ : handleOpenInlineFind,
3512
3677
  onReplaceRequested: onReplaceRequested
3513
3678
  ? () => onReplaceRequested({ selectionText: "", selectionRange: snapshot.selection })
3514
3679
  : undefined,
@@ -3534,6 +3699,7 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
3534
3699
  activeRuntime,
3535
3700
  commands,
3536
3701
  editorActionHost,
3702
+ handleOpenInlineFind,
3537
3703
  onFindRequested,
3538
3704
  onGoToRequested,
3539
3705
  onPrintRequested,
@@ -3662,7 +3828,7 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
3662
3828
  layoutFacet={activeRuntime.layout}
3663
3829
  geometryFacet={activeRuntime.geometry}
3664
3830
  workflowFacet={activeRuntime.workflow}
3665
- interactionGuardSnapshot={interactionGuardSnapshot}
3831
+ interactionGuardSnapshot={effectiveInteractionGuardSnapshot}
3666
3832
  chromePreset={effectiveChromePreset}
3667
3833
  chromeOptions={chromeOptions}
3668
3834
  density={density}
@@ -3688,6 +3854,7 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
3688
3854
  currentScopeContextAnalytics={currentScopeContextAnalytics}
3689
3855
  activeSelectionTool={shouldRenderSelectionChrome ? activeSelectionTool : null}
3690
3856
  selectionToolAnchor={shouldRenderSelectionChrome ? selectionToolbarAnchor : null}
3857
+ tableContext={activeTableContext}
3691
3858
  onAddCommentFromSelection={addSelectionToolbarComment}
3692
3859
  onAddCommentFromSuggestion={addSelectionToolbarComment}
3693
3860
  onAcceptSuggestion={activeSelectionTool?.kind === "suggestion-review"
@@ -3703,6 +3870,20 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
3703
3870
  onSelectionToolbarFocusCapture={handleSelectionToolbarFocusCapture}
3704
3871
  onSelectionToolbarBlurCapture={handleSelectionToolbarBlurCapture}
3705
3872
  selectionToolbarRef={selectionToolbarElementRef}
3873
+ onOpenInlineFind={handleOpenInlineFind}
3874
+ inlineFindBar={
3875
+ inlineFindOpen ? (
3876
+ <TwInlineFindBar
3877
+ query={inlineFindQuery}
3878
+ activeIndex={inlineFindActiveIndex}
3879
+ resultCount={inlineFindResults.length}
3880
+ onQueryChange={handleInlineFindQueryChange}
3881
+ onPrevious={handleInlineFindPrevious}
3882
+ onNext={handleInlineFindNext}
3883
+ onClose={handleCloseInlineFind}
3884
+ />
3885
+ ) : null
3886
+ }
3706
3887
  commands={commands}
3707
3888
  document={documentElement}
3708
3889
  onReviewSidebarTrackedChanges={onReviewSidebarTrackedChanges}
@@ -4070,6 +4251,23 @@ function openStoryForPage(
4070
4251
  runtime.openStory(target);
4071
4252
  }
4072
4253
 
4254
+ function wrapSearchResultIndex(index: number, resultCount: number): number {
4255
+ if (resultCount <= 0) return 0;
4256
+ return ((index % resultCount) + resultCount) % resultCount;
4257
+ }
4258
+
4259
+ function readTableRowCantSplit(
4260
+ document: CanonicalDocumentEnvelope,
4261
+ tableBlockIndex: number,
4262
+ rowIndex: number,
4263
+ ): boolean {
4264
+ const root = document.content;
4265
+ if (!root || root.type !== "doc") return false;
4266
+ const block = root.children[tableBlockIndex];
4267
+ if (!block || block.type !== "table") return false;
4268
+ return block.rows[rowIndex]?.cantSplit === true;
4269
+ }
4270
+
4073
4271
  function applyRegionAttributes(shell: HTMLElement): void {
4074
4272
  const toolbar = shell.querySelector<HTMLElement>("header");
4075
4273
  if (toolbar) {
@@ -5013,6 +5211,46 @@ function createSelectionToolbarWorkflowBadge(
5013
5211
  }
5014
5212
  }
5015
5213
 
5214
+ function createSelectionScopedInteractionGuardSnapshot(
5215
+ base: InteractionGuardSnapshot,
5216
+ posture: {
5217
+ mode: "edit" | "suggest" | "comment" | "view" | "blocked";
5218
+ disabledReason?: string;
5219
+ },
5220
+ workflowScopeSnapshot?: WorkflowScopeSnapshot | null,
5221
+ ): InteractionGuardSnapshot {
5222
+ if (posture.mode === "edit") {
5223
+ return base;
5224
+ }
5225
+
5226
+ const targetAccess =
5227
+ posture.mode === "suggest"
5228
+ ? "suggest"
5229
+ : posture.mode === "comment"
5230
+ ? "comment-only"
5231
+ : posture.mode === "view"
5232
+ ? "view-only"
5233
+ : "blocked";
5234
+ const blockedReasons =
5235
+ posture.mode === "blocked" && posture.disabledReason
5236
+ ? [{
5237
+ code: "outside_workflow_scope" as const,
5238
+ message: posture.disabledReason,
5239
+ ...(workflowScopeSnapshot?.activeWorkItemId
5240
+ ? { workItemId: workflowScopeSnapshot.activeWorkItemId }
5241
+ : {}),
5242
+ }]
5243
+ : base.blockedReasons;
5244
+
5245
+ return {
5246
+ ...base,
5247
+ effectiveMode: posture.mode,
5248
+ targetAccess,
5249
+ ...(posture.disabledReason ? { disabledReason: posture.disabledReason } : {}),
5250
+ blockedReasons,
5251
+ };
5252
+ }
5253
+
5016
5254
  function resolveSelectionWorkflowPosture(
5017
5255
  snapshot: RuntimeRenderSnapshot,
5018
5256
  viewState: ReturnType<WordReviewEditorRuntime["getViewState"]>,
@@ -5093,6 +5331,17 @@ function resolveSelectionWorkflowPosture(
5093
5331
  return activeRange.from >= scopeFrom && activeRange.to <= scopeTo;
5094
5332
  })
5095
5333
  : null;
5334
+ const activeWorkItem = workflowScopeSnapshot?.activeWorkItem;
5335
+ const activeWorkItemHasScopes = Boolean(
5336
+ activeWorkItem &&
5337
+ workflowScopeSnapshot?.scopes.some((scope) => scope.workItemId === activeWorkItem.workItemId),
5338
+ );
5339
+ if (activeRange && activeWorkItem && activeWorkItemHasScopes && !matchingScope) {
5340
+ return {
5341
+ mode: "blocked",
5342
+ disabledReason: `Unavailable here. "${activeWorkItem.title}" only applies inside its assigned scope.`,
5343
+ };
5344
+ }
5096
5345
 
5097
5346
  if (matchingScope?.mode === "suggest") {
5098
5347
  return {
@@ -63,6 +63,17 @@ export interface EditorCommandBag {
63
63
  onMergeCells?(): void;
64
64
  onSplitCell?(): void;
65
65
  onSetCellBackground?(color: string): void;
66
+ onToggleRowHeader?(): void;
67
+ onToggleRowCantSplit?(): void;
68
+ onDistributeColumnsEvenly?(): void;
69
+ onSetTableAlignment?(alignment: "left" | "center" | "right"): void;
70
+ onSetCellVerticalAlign?(align: "top" | "center" | "bottom"): void;
71
+ onSetColumnWidth?(columnIndex: number, twips: number): void;
72
+ onSetRowHeight?(
73
+ rowIndex: number,
74
+ twips: number,
75
+ rule: "auto" | "atLeast" | "exact",
76
+ ): void;
66
77
  onSetImageLayout?(
67
78
  mediaId: string,
68
79
  dimensions: { widthEmu: number; heightEmu: number },
@@ -11,6 +11,7 @@ import type {
11
11
  RuntimeContextAnalyticsSnapshot,
12
12
  RuntimeRenderSnapshot,
13
13
  StyleCatalogSnapshot,
14
+ TableStructureContextSnapshot,
14
15
  WordReviewEditorChromeOptions,
15
16
  WordReviewEditorChromePreset,
16
17
  WordReviewEditorChromeVisibility,
@@ -90,6 +91,8 @@ export interface EditorShellViewProps {
90
91
  import("../ui-tailwind/chrome/tw-workspace-chrome-host.tsx").TwWorkspaceChromeHostController
91
92
  >;
92
93
  commandPaletteDisabled?: boolean;
94
+ inlineFindBar?: ReactNode;
95
+ onOpenInlineFind?: () => void;
93
96
  /** P9g — live collab session for the `"collab"` chrome preset's top nav. */
94
97
  collabSession?: import("../runtime/collab-session.ts").CollabSession;
95
98
  collabTransportStatus?: import("../api/awareness-identity-types.ts").TransportStatus;
@@ -106,6 +109,7 @@ export interface EditorShellViewProps {
106
109
  currentScopeContextAnalytics?: RuntimeContextAnalyticsSnapshot | null;
107
110
  activeSelectionTool?: ActiveSelectionToolModel | null;
108
111
  selectionToolAnchor?: SelectionToolAnchor | null;
112
+ tableContext?: TableStructureContextSnapshot | null;
109
113
  documentNavigation?: DocumentNavigationSnapshot;
110
114
  commands: EditorCommandBag;
111
115
  shellHeader?: ReactNode;
@@ -185,6 +189,7 @@ export function EditorShellView(props: EditorShellViewProps) {
185
189
  onShellKeyDownCapture,
186
190
  document,
187
191
  commands,
192
+ inlineFindBar,
188
193
  ...workspaceProps
189
194
  } = props;
190
195
 
@@ -230,6 +235,11 @@ export function EditorShellView(props: EditorShellViewProps) {
230
235
  document={document}
231
236
  {...workspaceProps}
232
237
  />
238
+ {inlineFindBar ? (
239
+ <div className="pointer-events-none absolute right-6 top-16 z-50">
240
+ {inlineFindBar}
241
+ </div>
242
+ ) : null}
233
243
  </div>
234
244
  );
235
245
  }
@@ -170,7 +170,7 @@ export const TOOLBAR_CHROME_REGISTRY: ReadonlyArray<ToolbarChromeRegistryEntry>
170
170
  id: "text-style-selectors",
171
171
  surfaces: ["top-toolbar"],
172
172
  group: "text",
173
- presets: ["advanced", "workflow"],
173
+ presets: ["advanced", "review", "workflow"],
174
174
  roles: ALL_ROLES,
175
175
  fullPlacement: "inline",
176
176
  compactPlacement: "overflow",
@@ -200,7 +200,7 @@ export const TOOLBAR_CHROME_REGISTRY: ReadonlyArray<ToolbarChromeRegistryEntry>
200
200
  id: "text-colors",
201
201
  surfaces: ["top-toolbar"],
202
202
  group: "text",
203
- presets: ["simple", "advanced"],
203
+ presets: ["simple", "advanced", "review"],
204
204
  roles: EDITOR_AND_REVIEW,
205
205
  fullPlacement: "inline",
206
206
  compactPlacement: "inline",
@@ -214,7 +214,7 @@ export const TOOLBAR_CHROME_REGISTRY: ReadonlyArray<ToolbarChromeRegistryEntry>
214
214
  id: "paragraph-alignment",
215
215
  surfaces: ["top-toolbar"],
216
216
  group: "paragraph",
217
- presets: ["simple", "advanced"],
217
+ presets: ["simple", "advanced", "review"],
218
218
  roles: EDITOR_AND_REVIEW,
219
219
  fullPlacement: "inline",
220
220
  compactPlacement: "inline",
@@ -266,7 +266,7 @@ export const TOOLBAR_CHROME_REGISTRY: ReadonlyArray<ToolbarChromeRegistryEntry>
266
266
  id: "insert-actions",
267
267
  surfaces: ["top-toolbar"],
268
268
  group: "document",
269
- presets: ["simple", "advanced"],
269
+ presets: ["simple", "advanced", "review"],
270
270
  roles: EDITOR_ONLY,
271
271
  fullPlacement: "inline",
272
272
  compactPlacement: "overflow",
@@ -333,8 +333,8 @@ export const TOOLBAR_CHROME_REGISTRY: ReadonlyArray<ToolbarChromeRegistryEntry>
333
333
  surfaces: ["top-toolbar"],
334
334
  group: "review",
335
335
  presets: ["simple", "advanced", "review", "workflow"],
336
- // Same ownership rule as `comment` — editor/review role regions own it.
337
- roles: ALL_ROLES,
336
+ // Recording changes is review-authoring, not workflow/edit chrome.
337
+ roles: REVIEW_ONLY,
338
338
  fullPlacement: "inline",
339
339
  compactPlacement: "inline",
340
340
  runtimeBehavior: "always",
@@ -18,26 +18,20 @@ import type { ToolbarChromeItemId } from "./chrome-registry";
18
18
  /**
19
19
  * Ordered role-action ids. Each array covers the *role-primary* actions
20
20
  * only — the general left cluster (history, formatting, style selectors)
21
- * and right cluster (tracked-changes, workspace mode, zoom, health,
22
- * export) stay in the base `TwToolbar` layout regardless of role.
21
+ * and right cluster (workspace mode, zoom, health, export) stay in the
22
+ * base `TwToolbar` layout regardless of role.
23
23
  */
24
24
  export const ROLE_ACTION_SETS: Record<
25
25
  EditorRole,
26
26
  ReadonlyArray<ToolbarChromeItemId>
27
27
  > = {
28
- editor: [
29
- // Comment + inline tracked-changes toggle are the two review-layer
30
- // actions relevant to authoring. They live in the role region rather
31
- // than the right cluster so the right cluster stays view-focused.
32
- "comment",
33
- "tracked-changes-toggle",
34
- ],
28
+ editor: [],
35
29
  review: [
36
30
  // Optional sidebar panel shortcuts — visible only when the host provides
37
31
  // hasSidebarPanelAccess (e.g. the harness). Hidden in base runtime.
38
32
  "review-sidebar-tracked-changes",
39
33
  "review-sidebar-comments",
40
- // Inline review actions shared with editor role.
34
+ // Inline review actions.
41
35
  "comment",
42
36
  "tracked-changes-toggle",
43
37
  // Queue navigation + counts, collapsed from the old TwReviewQueueBar.
@@ -705,6 +705,17 @@ export function resolveSelectionWorkflowPosture(
705
705
  return activeRange.from >= scopeFrom && activeRange.to <= scopeTo;
706
706
  })
707
707
  : null;
708
+ const activeWorkItem = workflowScopeSnapshot?.activeWorkItem;
709
+ const activeWorkItemHasScopes = Boolean(
710
+ activeWorkItem &&
711
+ workflowScopeSnapshot?.scopes.some((scope) => scope.workItemId === activeWorkItem.workItemId),
712
+ );
713
+ if (activeRange && activeWorkItem && activeWorkItemHasScopes && !matchingScope) {
714
+ return {
715
+ mode: "blocked",
716
+ disabledReason: `Unavailable here. "${activeWorkItem.title}" only applies inside its assigned scope.`,
717
+ };
718
+ }
708
719
 
709
720
  if (matchingScope?.mode === "suggest") {
710
721
  return {
@@ -599,7 +599,7 @@ export const EDITOR_ACTION_REGISTRY: readonly EditorAction[] = [
599
599
  mkImportant({
600
600
  id: "find",
601
601
  label: "Find…",
602
- description: "Host search panel is not wired.",
602
+ description: "Find text in this document.",
603
603
  shortcut: ["Mod", "F"],
604
604
  group: "misc",
605
605
  targetKinds: [],
@@ -14,12 +14,12 @@
14
14
  * children are whatever the mode owner decides to render inside:
15
15
  * - review mode → review prev/next + accept/reject + markup selector
16
16
  * - workflow mode → scope prev/next + claim/skip/complete
17
- * - edit mode → (filled in by Phase D paragraph style + compact
18
- * formatting cluster)
17
+ * - edit mode → no band unless a future pass adds real contextual
18
+ * authoring content; toolbar owns direct formatting
19
19
  * - more mode → diagnostics links + command search affordance
20
20
  *
21
- * Phase B.3 moves the band OUT of the toolbar so the workspace row is:
22
- * [ toolbar left cluster ] · [ context band ] · [ toolbar right cluster ]
21
+ * Phase B.3 moves the band OUT of the toolbar so it sits below the shell
22
+ * header and above the toolbar when it has meaningful content.
23
23
  *
24
24
  * Perf discipline (CLAUDE.md §Performance Invariants):
25
25
  * - Pure presentational component; no DOM reads, no runtime calls.
@@ -66,9 +66,9 @@ const DEFAULT_EYEBROW: Record<EditorChromeMode, string> = {
66
66
  /**
67
67
  * Accent tint classes per mode. Review and workflow get a quiet
68
68
  * `color.accent.soft` tint so the band visually differs from the
69
- * neutral toolbar clusters flanking it; edit mode stays neutral per
70
- * DESIGN-EDITOR.md §3 ("Edit mode receives chrome to normal density,
71
- * no review emphasis").
69
+ * neutral toolbar clusters flanking it; edit mode stays neutral for
70
+ * compatibility with the composition model even though the workspace does
71
+ * not mount an empty editor band.
72
72
  *
73
73
  * "more" mode stays neutral (`bg-chrome`) because the More posture
74
74
  * opens diagnostics / compatibility drawers — the emphasis belongs on