@beyondwork/docx-react-component 1.0.84 → 1.0.86

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 (53) hide show
  1. package/package.json +1 -1
  2. package/src/api/internal/build-ref-projections.ts +3 -0
  3. package/src/api/public-types.ts +38 -0
  4. package/src/api/v3/_runtime-handle.ts +11 -0
  5. package/src/api/v3/runtime/content.ts +148 -1
  6. package/src/api/v3/runtime/formatting.ts +41 -0
  7. package/src/api/v3/runtime/review.ts +98 -0
  8. package/src/core/commands/index.ts +81 -25
  9. package/src/core/state/editor-state.ts +15 -0
  10. package/src/io/ooxml/header-footer-reference.ts +38 -0
  11. package/src/io/ooxml/parse-headers-footers.ts +11 -23
  12. package/src/io/ooxml/parse-main-document.ts +7 -10
  13. package/src/model/canonical-document.ts +9 -0
  14. package/src/model/review/comment-types.ts +2 -0
  15. package/src/runtime/document-runtime.ts +677 -54
  16. package/src/runtime/formatting/field/resolver.ts +73 -8
  17. package/src/runtime/layout/layout-engine-version.ts +31 -12
  18. package/src/runtime/layout/paginated-layout-engine.ts +18 -11
  19. package/src/runtime/layout/public-facet.ts +119 -16
  20. package/src/runtime/layout/resolve-page-fields.ts +68 -6
  21. package/src/runtime/layout/resolve-page-previews.ts +1 -1
  22. package/src/runtime/suggestions-snapshot.ts +24 -0
  23. package/src/runtime/surface-projection.ts +59 -2
  24. package/src/shell/ref-commands.ts +3 -354
  25. package/src/shell/session-bootstrap.ts +8 -0
  26. package/src/ui/WordReviewEditor.tsx +192 -35
  27. package/src/ui/editor-command-bag.ts +7 -1
  28. package/src/ui/editor-shell-view.tsx +1 -0
  29. package/src/ui/headless/revision-decoration-model.ts +13 -0
  30. package/src/ui/headless/selection-tool-types.ts +2 -0
  31. package/src/ui-tailwind/chrome/editor-action-registry.ts +7 -26
  32. package/src/ui-tailwind/chrome/tw-detach-handle.tsx +6 -2
  33. package/src/ui-tailwind/chrome/tw-suggestion-card.tsx +7 -3
  34. package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +175 -25
  35. package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +1 -1
  36. package/src/ui-tailwind/editor-surface/pm-decorations.ts +12 -0
  37. package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +18 -30
  38. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +1 -1
  39. package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +1 -1
  40. package/src/ui-tailwind/page-stack/tw-page-chrome-entry.tsx +20 -11
  41. package/src/ui-tailwind/page-stack/tw-page-footer-band.tsx +9 -4
  42. package/src/ui-tailwind/page-stack/tw-page-header-band.tsx +12 -7
  43. package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +29 -10
  44. package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +1 -1
  45. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +8 -3
  46. package/src/ui-tailwind/review/tw-review-rail.tsx +2 -0
  47. package/src/ui-tailwind/review/tw-revision-sidebar.tsx +46 -31
  48. package/src/ui-tailwind/review/tw-workflow-tab.tsx +159 -8
  49. package/src/ui-tailwind/review-workspace/types.ts +7 -2
  50. package/src/ui-tailwind/review-workspace/use-page-markers.ts +11 -1
  51. package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +8 -9
  52. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +21 -16
  53. package/src/ui-tailwind/tw-review-workspace.tsx +27 -4
@@ -66,6 +66,7 @@ import type {
66
66
  ViewMode as EditorViewMode,
67
67
  WorkflowBlockedCommandReason,
68
68
  WorkflowMarkupSnapshot,
69
+ WorkflowMarkupMode,
69
70
  WorkflowScopeSnapshot,
70
71
  WordReviewEditorChromeOptions,
71
72
  WordReviewEditorChromePreset,
@@ -250,6 +251,21 @@ function normalizeHostMarkupDisplay(
250
251
  return "markup";
251
252
  }
252
253
 
254
+ function toWorkflowMarkupMode(mode: MarkupDisplay): WorkflowMarkupMode {
255
+ switch (mode) {
256
+ case "all":
257
+ case "all-markup":
258
+ return "all";
259
+ case "simple":
260
+ case "simple-markup":
261
+ return "simple";
262
+ case "clean":
263
+ case "no-markup":
264
+ case "original":
265
+ return "clean";
266
+ }
267
+ }
268
+
253
269
  const VISUALLY_HIDDEN_STYLES: React.CSSProperties = {
254
270
  position: "absolute",
255
271
  width: "1px",
@@ -383,6 +399,11 @@ export function __createWordReviewEditorRefBridge(
383
399
  resolveComment: (commentId) => runtime.resolveComment(commentId),
384
400
  reopenComment: (commentId) => runtime.reopenComment(commentId),
385
401
  addCommentReply: (commentId, body) => runtime.addCommentReply(commentId, body),
402
+ getCommentThreadForChange: (changeId) =>
403
+ clonePublicValue(runtime.getCommentThreadForChange(changeId)),
404
+ ensureCommentThreadForChange: (changeId) =>
405
+ runtime.ensureCommentThreadForChange(changeId),
406
+ addReplyToChange: (changeId, body) => runtime.addReplyToChange(changeId, body),
386
407
  editCommentBody: (commentId, body) => runtime.editCommentBody(commentId, body),
387
408
  deleteComment: (commentId) => {
388
409
  applyRuntimeDeleteComment(runtime, commentId);
@@ -1041,7 +1062,8 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
1041
1062
  } = props;
1042
1063
 
1043
1064
  const [activeRailTab, setActiveRailTab] = useState<ReviewRailTab>("comments");
1044
- const [showTrackedChanges, setShowTrackedChanges] = useState(false);
1065
+ const [localMarkupDisplay, setLocalMarkupDisplay] =
1066
+ useState<WorkflowMarkupMode | null>(() => suggestionsEnabled ? "all" : null);
1045
1067
  const [activeRevisionId, setActiveRevisionId] = useState<string | undefined>();
1046
1068
  const [suppressedSuggestionRevisionId, setSuppressedSuggestionRevisionId] = useState<string | null>(null);
1047
1069
  const [selectionToolbarAnchor, setSelectionToolbarAnchor] = useState<SelectionToolbarAnchor | null>(null);
@@ -1343,7 +1365,10 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
1343
1365
  loadingViewState,
1344
1366
  );
1345
1367
  const isPageWorkspace = viewState.workspaceMode === "page";
1346
- const liveMarkupDisplay = __resolveLiveMarkupDisplay(markupDisplay, isPageWorkspace);
1368
+ const liveMarkupDisplay = localMarkupDisplay ??
1369
+ __resolveLiveMarkupDisplay(markupDisplay, isPageWorkspace);
1370
+ const trackedChangesAuthoringEnabled = viewState.documentMode === "suggesting";
1371
+ const showTrackedChanges = toWorkflowMarkupMode(liveMarkupDisplay) !== "clean";
1347
1372
  const documentNavigation = useRuntimeValue(
1348
1373
  runtime
1349
1374
  ? {
@@ -1483,9 +1508,27 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
1483
1508
  activeRuntime.setViewMode(effectiveViewMode);
1484
1509
  }, [activeRuntime, effectiveViewMode]);
1485
1510
 
1511
+ const setReviewMarkupMode = useCallback((mode: MarkupDisplay) => {
1512
+ const workflowMode = toWorkflowMarkupMode(mode);
1513
+ setLocalMarkupDisplay(workflowMode);
1514
+ api.ui?.viewport.setLocalMarkupMode(workflowMode);
1515
+ }, [api]);
1516
+
1517
+ const setTrackedChangesAuthoring = useCallback((enabled: boolean) => {
1518
+ api.runtime.document.setMode(enabled ? "suggesting" : "editing");
1519
+ if (enabled) {
1520
+ setLocalMarkupDisplay("all");
1521
+ api.ui?.viewport.setLocalMarkupMode("all");
1522
+ }
1523
+ }, [api]);
1524
+
1486
1525
  useEffect(() => {
1487
- activeRuntime.setDocumentMode(suggestionsEnabled ? "suggesting" : "editing");
1488
- }, [activeRuntime, suggestionsEnabled]);
1526
+ if (suggestionsEnabled) {
1527
+ setTrackedChangesAuthoring(true);
1528
+ return;
1529
+ }
1530
+ api.runtime.document.setMode("editing");
1531
+ }, [api, setTrackedChangesAuthoring, suggestionsEnabled]);
1489
1532
 
1490
1533
  // design-close-chrome Phase 2 — density contract (designsystem §4.2).
1491
1534
  // When the `density` prop is supplied, drive the root `data-density`
@@ -1645,6 +1688,12 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
1645
1688
  reopenComment: (commentId) => activeRuntime.reopenComment(commentId),
1646
1689
  addCommentReply: (commentId, body) =>
1647
1690
  activeRuntime.addCommentReply(commentId, body, currentUser.userId),
1691
+ getCommentThreadForChange: (changeId) =>
1692
+ clonePublicValue(activeRuntime.getCommentThreadForChange(changeId)),
1693
+ ensureCommentThreadForChange: (changeId) =>
1694
+ activeRuntime.ensureCommentThreadForChange(changeId, currentUser.userId),
1695
+ addReplyToChange: (changeId, body) =>
1696
+ activeRuntime.addReplyToChange(changeId, body, currentUser.userId),
1648
1697
  editCommentBody: (commentId, body) =>
1649
1698
  activeRuntime.editCommentBody(commentId, body),
1650
1699
  deleteComment: (commentId) => {
@@ -2417,13 +2466,15 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
2417
2466
 
2418
2467
  function addReviewComment(): string | null {
2419
2468
  try {
2469
+ const currentSnapshot = activeRuntime.getRenderSnapshot();
2420
2470
  const { commentId } = activeRuntime.addComment({
2421
- anchor: resolveCommentCommandAnchor(snapshot),
2471
+ anchor: resolveCommentCommandAnchor(currentSnapshot),
2422
2472
  body: "",
2423
2473
  authorId: currentUser.userId,
2424
2474
  snapToSafeBoundary: true,
2425
2475
  });
2426
2476
  activeRuntime.openComment(commentId);
2477
+ setActiveReviewQueueItemId(`comment:${commentId}`);
2427
2478
  setActiveRailTab("comments");
2428
2479
  return commentId;
2429
2480
  } catch {
@@ -2740,7 +2791,20 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
2740
2791
  );
2741
2792
 
2742
2793
  const addSelectionToolbarComment = useCallback(() => {
2743
- const commentId = addReviewComment();
2794
+ let commentId: string | null = null;
2795
+ if (activeSelectionTool?.kind === "suggestion-review") {
2796
+ const primaryChangeId = activeSelectionTool.changeIds[0];
2797
+ const linkedThread = primaryChangeId
2798
+ ? activeRuntime.ensureCommentThreadForChange(primaryChangeId, currentUser.userId)
2799
+ : null;
2800
+ commentId = linkedThread?.commentId ?? null;
2801
+ if (commentId) {
2802
+ activeRuntime.openComment(commentId);
2803
+ setActiveRailTab("comments");
2804
+ }
2805
+ } else {
2806
+ commentId = addReviewComment();
2807
+ }
2744
2808
  if (!commentId) {
2745
2809
  return;
2746
2810
  }
@@ -2748,7 +2812,51 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
2748
2812
  queueMicrotask(() => {
2749
2813
  focusDocumentSurface();
2750
2814
  });
2751
- }, [dismissSelectionToolbar, focusDocumentSurface]);
2815
+ }, [
2816
+ activeRuntime,
2817
+ activeSelectionTool,
2818
+ addReviewComment,
2819
+ currentUser.userId,
2820
+ dismissSelectionToolbar,
2821
+ focusDocumentSurface,
2822
+ ]);
2823
+
2824
+ const acceptActiveSuggestion = useCallback(() => {
2825
+ if (activeSelectionTool?.kind !== "suggestion-review") {
2826
+ return;
2827
+ }
2828
+ for (const changeId of activeSelectionTool.changeIds) {
2829
+ activeRuntime.acceptChange(changeId);
2830
+ }
2831
+ dismissSelectionToolbar("chrome-action");
2832
+ }, [activeRuntime, activeSelectionTool, dismissSelectionToolbar]);
2833
+
2834
+ const rejectActiveSuggestion = useCallback(() => {
2835
+ if (activeSelectionTool?.kind !== "suggestion-review") {
2836
+ return;
2837
+ }
2838
+ for (const changeId of activeSelectionTool.changeIds) {
2839
+ activeRuntime.rejectChange(changeId);
2840
+ }
2841
+ dismissSelectionToolbar("chrome-action");
2842
+ }, [activeRuntime, activeSelectionTool, dismissSelectionToolbar]);
2843
+
2844
+ const editActiveSuggestion = useCallback(() => {
2845
+ if (activeSelectionTool?.kind !== "suggestion-review") {
2846
+ return;
2847
+ }
2848
+ setSuppressedSuggestionRevisionId(activeSelectionTool.suggestionId);
2849
+ const activeSuggestion = suggestionsSnapshot.suggestions.find(
2850
+ (suggestion) => suggestion.suggestionId === activeSelectionTool.suggestionId,
2851
+ );
2852
+ if (activeSuggestion) {
2853
+ applyRuntimeSelection(
2854
+ activeRuntime,
2855
+ createSelectionFromAnchor(activeSuggestion.anchor, activeSuggestion.storyTarget),
2856
+ );
2857
+ }
2858
+ setSelectionToolbarFocusWithin(true);
2859
+ }, [activeRuntime, activeSelectionTool, suggestionsSnapshot.suggestions]);
2752
2860
 
2753
2861
  const handleSelectionToolbarAnchorChange = useCallback(
2754
2862
  (nextAnchor: SelectionToolbarAnchor | null) => {
@@ -3140,6 +3248,19 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
3140
3248
  focusAnchor(revision.anchor, revision.storyTarget);
3141
3249
  setActiveRailTab("changes");
3142
3250
  },
3251
+ onReplyToRevision: (revision: typeof snapshot.trackedChanges.revisions[number]) => {
3252
+ const linkedThread = activeRuntime.ensureCommentThreadForChange(
3253
+ revision.revisionId,
3254
+ currentUser.userId,
3255
+ );
3256
+ if (!linkedThread?.commentId) {
3257
+ return;
3258
+ }
3259
+ setActiveRevisionId(revision.revisionId);
3260
+ focusAnchor(revision.anchor, revision.storyTarget);
3261
+ activeRuntime.openComment(linkedThread.commentId);
3262
+ setActiveRailTab("comments");
3263
+ },
3143
3264
  onAcceptRevision: (revisionId: string) => {
3144
3265
  activeRuntime.acceptChange(revisionId);
3145
3266
  setActiveRailTab("changes");
@@ -3166,7 +3287,9 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
3166
3287
  onWorkspaceModeChange: (mode) => activeRuntime.setWorkspaceMode(mode),
3167
3288
  onZoomChange: (level) => activeRuntime.setZoom(level),
3168
3289
  onActiveRailTabChange: setActiveRailTab,
3169
- onShowTrackedChangesChange: setShowTrackedChanges,
3290
+ onShowTrackedChangesChange: setTrackedChangesAuthoring,
3291
+ onReviewMarkupModeChange: setReviewMarkupMode,
3292
+ onChromePinChange: (surface, pin) => activeRuntime.setChromePin(surface, pin),
3170
3293
  onToggleBold: () =>
3171
3294
  applyRuntimeFormattingOperation(activeRuntime, { type: "toggle", mark: "bold" }),
3172
3295
  onToggleItalic: () =>
@@ -3263,10 +3386,20 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
3263
3386
  // with a mount-time `console.warn`; hosts that still pass them can
3264
3387
  // migrate to `onOpenStory` at their leisure.
3265
3388
  onOpenHeaderStoryForPage: (pageIndex: number) =>
3266
- openStoryForPage(activeRuntime, pageIndex, "header"),
3389
+ isPageWorkspace
3390
+ ? openStoryForPage(activeRuntime, pageIndex, "header")
3391
+ : undefined,
3267
3392
  onOpenFooterStoryForPage: (pageIndex: number) =>
3268
- openStoryForPage(activeRuntime, pageIndex, "footer"),
3393
+ isPageWorkspace
3394
+ ? openStoryForPage(activeRuntime, pageIndex, "footer")
3395
+ : undefined,
3269
3396
  onOpenStory: (target) => {
3397
+ if (
3398
+ !isPageWorkspace &&
3399
+ (target.kind === "header" || target.kind === "footer")
3400
+ ) {
3401
+ return;
3402
+ }
3270
3403
  activeRuntime.openStory(target);
3271
3404
  },
3272
3405
  onDeleteSectionBreak: (sectionIndex) =>
@@ -3369,6 +3502,9 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
3369
3502
  onInsertPageBreak: commands.onInsertPageBreak,
3370
3503
  onInsertSectionBreak: (type) => commands.onInsertSectionBreak?.(type),
3371
3504
  onInsertTable: commands.onInsertTable,
3505
+ onInsertImage: commands.onInsertImage
3506
+ ? () => requestImageInsertFromPicker(commands.onInsertImage)
3507
+ : undefined,
3372
3508
  onAddComment: commands.onAddComment,
3373
3509
  onFindRequested: onFindRequested
3374
3510
  ? () => onFindRequested({ selectionText: "", selectionRange: snapshot.selection })
@@ -3512,6 +3648,7 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
3512
3648
  activeRailTab={activeRailTab}
3513
3649
  activeCommentId={snapshot.comments.activeCommentId}
3514
3650
  activeRevisionId={activeRevisionId}
3651
+ trackedChangesAuthoringEnabled={trackedChangesAuthoringEnabled}
3515
3652
  showTrackedChanges={showTrackedChanges}
3516
3653
  workflowScopeSnapshot={workflowScopeSnapshot}
3517
3654
  layoutFacet={activeRuntime.layout}
@@ -3546,35 +3683,13 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
3546
3683
  onAddCommentFromSelection={addSelectionToolbarComment}
3547
3684
  onAddCommentFromSuggestion={addSelectionToolbarComment}
3548
3685
  onAcceptSuggestion={activeSelectionTool?.kind === "suggestion-review"
3549
- ? () => {
3550
- for (const changeId of activeSelectionTool.changeIds) {
3551
- activeRuntime.acceptChange(changeId);
3552
- }
3553
- dismissSelectionToolbar("chrome-action");
3554
- }
3686
+ ? acceptActiveSuggestion
3555
3687
  : undefined}
3556
3688
  onRejectSuggestion={activeSelectionTool?.kind === "suggestion-review"
3557
- ? () => {
3558
- for (const changeId of activeSelectionTool.changeIds) {
3559
- activeRuntime.rejectChange(changeId);
3560
- }
3561
- dismissSelectionToolbar("chrome-action");
3562
- }
3689
+ ? rejectActiveSuggestion
3563
3690
  : undefined}
3564
3691
  onEditSuggestion={activeSelectionTool?.kind === "suggestion-review"
3565
- ? () => {
3566
- setSuppressedSuggestionRevisionId(activeSelectionTool.suggestionId);
3567
- const activeSuggestion = suggestionsSnapshot.suggestions.find(
3568
- (suggestion) => suggestion.suggestionId === activeSelectionTool.suggestionId,
3569
- );
3570
- if (activeSuggestion) {
3571
- applyRuntimeSelection(
3572
- activeRuntime,
3573
- createSelectionFromAnchor(activeSuggestion.anchor, activeSuggestion.storyTarget),
3574
- );
3575
- }
3576
- setSelectionToolbarFocusWithin(true);
3577
- }
3692
+ ? editActiveSuggestion
3578
3693
  : undefined}
3579
3694
  onDismissSelectionToolbar={() => dismissSelectionToolbar("chrome-action")}
3580
3695
  onSelectionToolbarFocusCapture={handleSelectionToolbarFocusCapture}
@@ -4216,6 +4331,42 @@ function resolveCommentCommandAnchor(
4216
4331
  : selection.activeRange;
4217
4332
  }
4218
4333
 
4334
+ function requestImageInsertFromPicker(
4335
+ onInsertImage: ((options: InsertImageOptions) => void) | undefined,
4336
+ ): void {
4337
+ const document = globalThis.document;
4338
+ if (!onInsertImage || typeof document?.createElement !== "function" || !document.body) {
4339
+ return;
4340
+ }
4341
+
4342
+ const input = document.createElement("input");
4343
+ input.type = "file";
4344
+ input.accept = "image/png,image/jpeg,image/gif";
4345
+ input.style.position = "fixed";
4346
+ input.style.left = "-9999px";
4347
+ input.style.top = "-9999px";
4348
+ input.addEventListener("change", () => {
4349
+ const file = input.files?.[0];
4350
+ if (!file) {
4351
+ input.remove();
4352
+ return;
4353
+ }
4354
+ void file.arrayBuffer()
4355
+ .then((buffer) => {
4356
+ onInsertImage({
4357
+ data: new Uint8Array(buffer),
4358
+ mimeType: file.type || "image/png",
4359
+ altText: file.name,
4360
+ });
4361
+ })
4362
+ .finally(() => {
4363
+ input.remove();
4364
+ });
4365
+ }, { once: true });
4366
+ document.body.appendChild(input);
4367
+ input.click();
4368
+ }
4369
+
4219
4370
  function resolveCollapsedCommentRange(
4220
4371
  surface: RuntimeRenderSnapshot["surface"],
4221
4372
  selection: RuntimeRenderSnapshot["selection"],
@@ -4740,6 +4891,12 @@ function buildSuggestionCardModel(args: {
4740
4891
  canReject: canReviewSuggestion && capabilities.canRejectChange && focusedSuggestion.canReject,
4741
4892
  canEditSuggestion: canReviewSuggestion && focusedSuggestion.editable,
4742
4893
  canAddComment,
4894
+ ...(focusedSuggestion.commentThreadIds
4895
+ ? { commentThreadIds: focusedSuggestion.commentThreadIds }
4896
+ : {}),
4897
+ ...(focusedSuggestion.replyCount !== undefined
4898
+ ? { replyCount: focusedSuggestion.replyCount }
4899
+ : {}),
4743
4900
  ...(disabledReason ? { disabledReason } : {}),
4744
4901
  };
4745
4902
  }
@@ -1,6 +1,7 @@
1
1
  import { useMemo, useRef } from "react";
2
2
 
3
3
  import type {
4
+ ChromePinSurface,
4
5
  CommentSidebarThreadSnapshot,
5
6
  EditorStoryTarget,
6
7
  FormattingAlignment,
@@ -9,11 +10,13 @@ import type {
9
10
  SectionBreakType,
10
11
  SectionLayoutPatch,
11
12
  SectionPageNumberingPatch,
13
+ PinState,
12
14
  TrackedChangeEntrySnapshot,
13
15
  ZoomLevel,
14
16
  WorkspaceMode,
15
17
  } from "../api/public-types.ts";
16
18
  import type { ReviewRailTab } from "../ui-tailwind/review/tw-review-rail.tsx";
19
+ import type { MarkupDisplay } from "./headless/comment-decoration-model.ts";
17
20
 
18
21
  type CommandHandler = (...args: any[]) => unknown;
19
22
 
@@ -22,6 +25,8 @@ export interface EditorCommandBag {
22
25
  onZoomChange?(level: ZoomLevel): void;
23
26
  onActiveRailTabChange(value: ReviewRailTab): void;
24
27
  onShowTrackedChangesChange(show: boolean): void;
28
+ onReviewMarkupModeChange?(mode: MarkupDisplay): void;
29
+ onChromePinChange?(surface: ChromePinSurface, pin: PinState | null): void;
25
30
  onUndo(): void;
26
31
  onRedo(): void;
27
32
  onSetParagraphStyle?(styleId: string): void;
@@ -83,6 +88,7 @@ export interface EditorCommandBag {
83
88
  onAddReply?(commentId: string, body: string): void;
84
89
  onEditBody?(commentId: string, body: string): void;
85
90
  onOpenRevision(revision: TrackedChangeEntrySnapshot): void;
91
+ onReplyToRevision?(revision: TrackedChangeEntrySnapshot): void;
86
92
  onAcceptRevision(revisionId: string): void;
87
93
  onRejectRevision(revisionId: string): void;
88
94
  onAcceptAllChanges(): void;
@@ -102,7 +108,7 @@ export interface EditorCommandBag {
102
108
  /** Open the footer story for a specific page (double-click on its band). */
103
109
  onOpenFooterStoryForPage?(pageIndex: number): void;
104
110
  /**
105
- * P8.11 — per-page header/footer band click handler. Receives the
111
+ * P8.11 — per-page header/footer band double-click handler. Receives the
106
112
  * exact `EditorStoryTarget` the band represents; the command bag wires
107
113
  * this to `runtime.openStory(target)`.
108
114
  */
@@ -55,6 +55,7 @@ export interface EditorShellViewProps {
55
55
  activeRailTab: ReviewRailTab;
56
56
  activeCommentId?: string;
57
57
  activeRevisionId?: string;
58
+ trackedChangesAuthoringEnabled?: boolean;
58
59
  showTrackedChanges: boolean;
59
60
  workflowScopeSnapshot?: WorkflowScopeSnapshot | null;
60
61
  /**
@@ -225,6 +225,19 @@ export function buildClassFromRevisionDisplay(
225
225
  parts.push("text-secondary");
226
226
  }
227
227
 
228
+ // Formatting/property-change revisions carry their semantics through
229
+ // `kind` even when markup posture has no underline/strike flag. Give
230
+ // mounted suggestion authoring a visible, non-destructive cue instead
231
+ // of silently relying on the sidebar/card path.
232
+ if (
233
+ parts.length === 0 &&
234
+ (display.kind === "formatting" || display.kind === "property-change")
235
+ ) {
236
+ parts.push(
237
+ "underline decoration-accent/70 decoration-dotted decoration-1 underline-offset-2",
238
+ );
239
+ }
240
+
228
241
  // Surface the author palette color as a CSS variable the renderer
229
242
  // can pick up via `var(--wre-revision-author)`. Consumer stylesheet
230
243
  // composes this into ring/underline color when the palette slot is
@@ -81,6 +81,8 @@ export interface SuggestionReviewSelectionToolModel extends BaseSelectionToolMod
81
81
  canReject: boolean;
82
82
  canEditSuggestion: boolean;
83
83
  canAddComment: boolean;
84
+ commentThreadIds?: string[];
85
+ replyCount?: number;
84
86
  }
85
87
 
86
88
  export type StructureContextKind = "table" | "image" | "object" | "list";
@@ -3,16 +3,17 @@
3
3
  * contextual actions exist, keyed by `TargetKind`.
4
4
  *
5
5
  * Consumed by three access routes so DESIGN-EDITOR.md §6.4 holds by
6
- * construction ("right-click cannot be richer than the floating surface;
7
- * cannot duplicate command trees"):
6
+ * construction for general editor actions:
8
7
  *
9
8
  * 1. Right-click context menu (via `build-context-menu-entries.ts`)
10
9
  * 2. Inline "More…" affordance on reduced floating toolbars (Phase D)
11
10
  * 3. Command palette groups (Phase C.δ)
12
11
  *
13
- * All three routes produce the same actions from this registry — they
14
- * cannot diverge. This is the load-bearing decision that makes the
15
- * floating-menu demotion legal.
12
+ * All three routes produce the same general actions from this registry.
13
+ * Suggestion-specific review actions are intentionally absent for now:
14
+ * they require an exact revision/suggestion id payload, so they live on
15
+ * the floating suggestion card and Changes rail until the context-menu
16
+ * target resolver can supply that payload.
16
17
  *
17
18
  * Perf discipline: pure data + pure filter helpers. No DOM reads, no
18
19
  * runtime calls, no observers. Actions are dispatched through host-
@@ -106,10 +107,6 @@ export interface EditorActionHostCallbacks {
106
107
  readonly onPrintRequested?: () => void;
107
108
  readonly onGoToRequested?: () => void;
108
109
 
109
- // Tracked-change operations (suggestion target)
110
- readonly onAcceptSuggestion?: () => void;
111
- readonly onRejectSuggestion?: () => void;
112
-
113
110
  // Comment operations (comment-anchor target)
114
111
  readonly onResolveComment?: () => void;
115
112
  readonly onReplyToComment?: () => void;
@@ -576,7 +573,7 @@ export const EDITOR_ACTION_REGISTRY: readonly EditorAction[] = [
576
573
  mkImportant({
577
574
  id: "insert-image",
578
575
  label: "Insert image…",
579
- description: "Needs a host file picker before it can run.",
576
+ description: "Choose an image file and insert it at the caret.",
580
577
  group: "misc",
581
578
  targetKinds: [],
582
579
  callback: "onInsertImage",
@@ -636,22 +633,6 @@ export const EDITOR_ACTION_REGISTRY: readonly EditorAction[] = [
636
633
  callback: "onPrintRequested",
637
634
  }),
638
635
 
639
- // -------- Suggestion / tracked change --------
640
- mk({
641
- id: "accept-suggestion",
642
- label: "Accept suggestion",
643
- group: "suggestion",
644
- targetKinds: ["suggestion"],
645
- callback: "onAcceptSuggestion",
646
- }),
647
- mk({
648
- id: "reject-suggestion",
649
- label: "Reject suggestion",
650
- group: "suggestion",
651
- targetKinds: ["suggestion"],
652
- callback: "onRejectSuggestion",
653
- }),
654
-
655
636
  // -------- Comment --------
656
637
  mk({
657
638
  id: "resolve-comment",
@@ -43,6 +43,10 @@ export function TwDetachHandle(props: TwDetachHandleProps): React.JSX.Element {
43
43
  const { surface, pin, onChange, label } = props;
44
44
  const isDetached = pin?.detached ?? false;
45
45
  const offset = pin?.offset ?? { x: 0, y: 0 };
46
+ const dragHandleTestId =
47
+ surface === "selectionTier" ? "selection-tool-drag-handle" : `${surface}-detach-drag-handle`;
48
+ const toggleTestId =
49
+ surface === "selectionTier" ? "selection-tool-attach-toggle" : `${surface}-detach-toggle`;
46
50
  const dragState = useRef<
47
51
  | {
48
52
  startX: number;
@@ -117,7 +121,7 @@ export function TwDetachHandle(props: TwDetachHandleProps): React.JSX.Element {
117
121
  <button
118
122
  type="button"
119
123
  aria-label={isDetached ? "Drag floating menu" : "Drag to float menu"}
120
- data-testid={`${surface}-detach-drag-handle`}
124
+ data-testid={dragHandleTestId}
121
125
  className="inline-flex h-6 items-center justify-center rounded-md border border-transparent px-1.5 text-tertiary transition-colors hover:border-border/60 hover:bg-surface hover:text-primary"
122
126
  onMouseDown={beginDrag}
123
127
  >
@@ -133,7 +137,7 @@ export function TwDetachHandle(props: TwDetachHandleProps): React.JSX.Element {
133
137
  type="button"
134
138
  aria-label={isDetached ? "Dock menu" : "Float menu"}
135
139
  aria-pressed={isDetached}
136
- data-testid={`${surface}-detach-toggle`}
140
+ data-testid={toggleTestId}
137
141
  className="inline-flex h-6 items-center rounded-md border border-border/60 px-2 text-[10px] font-medium text-secondary transition-colors hover:bg-surface hover:text-primary"
138
142
  onMouseDown={preserveEditorSelectionMouseDown}
139
143
  onClick={toggle}
@@ -32,9 +32,12 @@ const focusRingClass =
32
32
  export function TwSuggestionCard(props: TwSuggestionCardProps) {
33
33
  const contextLabel = summarizeSuggestionContext(props.model);
34
34
  const commentDisabled = !props.model.canAddComment;
35
+ const replyCount = props.model.replyCount ?? 0;
35
36
  const tooltipLabel = commentDisabled
36
37
  ? props.model.disabledReason ?? "Commenting is unavailable for this selection"
37
- : "Comment on suggestion";
38
+ : props.model.commentThreadIds?.length
39
+ ? "Reply to tracked change"
40
+ : "Start tracked-change discussion";
38
41
 
39
42
  return (
40
43
  <div
@@ -87,14 +90,15 @@ export function TwSuggestionCard(props: TwSuggestionCardProps) {
87
90
  <Tooltip.Trigger asChild>
88
91
  <button
89
92
  type="button"
90
- aria-label="Comment on suggestion"
93
+ aria-label="Reply to tracked change"
94
+ data-testid="suggestion-card-reply"
91
95
  disabled={commentDisabled}
92
96
  onMouseDown={preserveEditorSelectionMouseDown}
93
97
  onClick={props.onAddComment}
94
98
  className={`inline-flex h-7 items-center gap-1 rounded-md border border-[var(--color-border-default)] px-2 text-[11px] font-medium text-[var(--color-text-secondary)] transition-colors hover:bg-[var(--color-bg-hover)] disabled:cursor-not-allowed disabled:opacity-40 ${focusRingClass}`}
95
99
  >
96
100
  <MessageSquare className="h-3 w-3" />
97
- Comment
101
+ Reply{replyCount > 0 ? ` ${replyCount}` : ""}
98
102
  </button>
99
103
  </Tooltip.Trigger>
100
104
  <Tooltip.Portal>