@beyondwork/docx-react-component 1.0.42 → 1.0.45

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 (82) hide show
  1. package/README.md +17 -0
  2. package/package.json +5 -4
  3. package/src/api/editor-state-types.ts +110 -0
  4. package/src/api/public-types.ts +333 -4
  5. package/src/core/commands/formatting-commands.ts +7 -1
  6. package/src/core/commands/index.ts +60 -10
  7. package/src/core/commands/text-commands.ts +59 -0
  8. package/src/core/search/search-text.ts +15 -2
  9. package/src/core/selection/review-anchors.ts +131 -21
  10. package/src/index.ts +29 -1
  11. package/src/io/chart-preview-resolver.ts +281 -0
  12. package/src/io/docx-session.ts +692 -2
  13. package/src/io/export/build-app-properties-xml.ts +1 -1
  14. package/src/io/export/serialize-comments.ts +38 -9
  15. package/src/io/export/twip.ts +1 -1
  16. package/src/io/load-scheduler.ts +230 -0
  17. package/src/io/normalize/normalize-text.ts +116 -0
  18. package/src/io/ooxml/parse-comments.ts +0 -33
  19. package/src/io/ooxml/parse-complex-content.ts +14 -0
  20. package/src/io/ooxml/parse-main-document.ts +4 -0
  21. package/src/io/ooxml/workflow-payload-validator.ts +97 -1
  22. package/src/io/ooxml/workflow-payload.ts +172 -1
  23. package/src/preservation/opaque-region.ts +5 -0
  24. package/src/review/store/comment-remapping.ts +2 -2
  25. package/src/runtime/collab-session.ts +1 -1
  26. package/src/runtime/document-runtime.ts +661 -42
  27. package/src/runtime/edit-dispatch/dispatch-text-command.ts +98 -0
  28. package/src/runtime/edit-dispatch/index.ts +2 -0
  29. package/src/runtime/edit-dispatch/list-aware-dispatch.ts +125 -0
  30. package/src/runtime/editor-state-channel.ts +544 -0
  31. package/src/runtime/editor-state-integration.ts +217 -0
  32. package/src/runtime/editor-surface/capabilities.ts +411 -0
  33. package/src/runtime/layout/index.ts +2 -0
  34. package/src/runtime/layout/inert-layout-facet.ts +4 -0
  35. package/src/runtime/layout/layout-engine-instance.ts +63 -2
  36. package/src/runtime/layout/layout-engine-version.ts +41 -0
  37. package/src/runtime/layout/paginated-layout-engine.ts +211 -14
  38. package/src/runtime/layout/public-facet.ts +430 -1
  39. package/src/runtime/perf-counters.ts +28 -0
  40. package/src/runtime/prerender/cache-envelope.ts +29 -0
  41. package/src/runtime/prerender/cache-key.ts +66 -0
  42. package/src/runtime/prerender/font-fingerprint.ts +17 -0
  43. package/src/runtime/prerender/graph-canonicalize.ts +121 -0
  44. package/src/runtime/prerender/indexeddb-cache.ts +184 -0
  45. package/src/runtime/prerender/prerender-document.ts +145 -0
  46. package/src/runtime/render/block-fragment-projection.ts +2 -0
  47. package/src/runtime/render/render-frame-types.ts +17 -0
  48. package/src/runtime/render/render-kernel.ts +172 -29
  49. package/src/runtime/selection/post-edit-validator.ts +77 -0
  50. package/src/runtime/surface-projection.ts +45 -7
  51. package/src/runtime/workflow-markup.ts +71 -16
  52. package/src/ui/WordReviewEditor.tsx +142 -237
  53. package/src/ui/editor-command-bag.ts +14 -0
  54. package/src/ui/editor-runtime-boundary.ts +115 -12
  55. package/src/ui/editor-shell-view.tsx +10 -0
  56. package/src/ui/editor-surface-controller.tsx +5 -0
  57. package/src/ui/headless/selection-helpers.ts +10 -0
  58. package/src/ui/runtime-shortcut-dispatch.ts +28 -68
  59. package/src/ui-tailwind/chrome/chrome-preset-toolbar.tsx +62 -2
  60. package/src/ui-tailwind/chrome/collab-top-nav-container.tsx +281 -0
  61. package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +48 -0
  62. package/src/ui-tailwind/editor-surface/paste-plain-text.ts +72 -0
  63. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +118 -8
  64. package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +76 -165
  65. package/src/ui-tailwind/editor-surface/pm-schema.ts +170 -4
  66. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +58 -7
  67. package/src/ui-tailwind/editor-surface/surface-build-keys.ts +2 -0
  68. package/src/ui-tailwind/editor-surface/tw-page-block-view.helpers.ts +265 -0
  69. package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +8 -255
  70. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +47 -0
  71. package/src/ui-tailwind/index.ts +5 -1
  72. package/src/ui-tailwind/page-stack/tw-endnote-area.tsx +57 -0
  73. package/src/ui-tailwind/page-stack/tw-footnote-area.tsx +71 -0
  74. package/src/ui-tailwind/page-stack/tw-page-footer-band.tsx +73 -0
  75. package/src/ui-tailwind/page-stack/tw-page-header-band.tsx +74 -0
  76. package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +477 -0
  77. package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +374 -0
  78. package/src/ui-tailwind/page-stack/use-visible-block-range.ts +157 -0
  79. package/src/ui-tailwind/review/comment-markdown-renderer.tsx +155 -0
  80. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +77 -16
  81. package/src/ui-tailwind/theme/editor-theme.css +47 -14
  82. package/src/ui-tailwind/tw-review-workspace.tsx +303 -123
@@ -97,14 +97,15 @@ import {
97
97
  } from "../core/commands/style-commands.ts";
98
98
  import {
99
99
  continueNumbering as continueListNumbering,
100
- backspaceAtListStart,
101
- indentListItems,
102
- outdentListItems,
103
100
  restartNumbering as restartListNumbering,
104
- splitListParagraph,
105
101
  toggleBulletedList,
106
102
  toggleNumberedList,
107
103
  } from "../core/commands/list-commands.ts";
104
+ import {
105
+ dispatchTextCommand,
106
+ type DispatchContext,
107
+ type DispatchTextCommand,
108
+ } from "../runtime/edit-dispatch/index.ts";
108
109
  import {
109
110
  resolveActiveParagraphIndex,
110
111
  setActiveParagraphIndentation,
@@ -242,6 +243,12 @@ const BROWSER_SAFE_PREVIEW_TYPES = new Set([
242
243
  "image/gif",
243
244
  "image/webp",
244
245
  "image/bmp",
246
+ // SVG is served through `<img src="data:image/svg+xml;base64,...">` by
247
+ // `createImageDataUrl`. Chromium sandboxes SVGs loaded via <img> — scripts
248
+ // don't execute, external references are blocked, XSS surface matches PNG.
249
+ // Needed for docs/plans/lane-5-charts.md Stage 0B synthesized chart previews and
250
+ // any host that ships .svg inside `word/media/` as a logo or figure.
251
+ "image/svg+xml",
245
252
  ]);
246
253
 
247
254
  const ACCESSIBLE_REGION_ORDER = [
@@ -938,6 +945,22 @@ export function __createWordReviewEditorRefBridge(
938
945
  pendingConflicts,
939
946
  });
940
947
  },
948
+ // Schema 1.2 — EditorStateChannel delegation.
949
+ configureEditorStatePolicy: (policy) => {
950
+ runtime.configureEditorStatePolicy(policy);
951
+ },
952
+ registerEditorStateResolver: (resolver) => {
953
+ runtime.registerEditorStateResolver(resolver);
954
+ },
955
+ registerEditorStatePersister: (persister) => {
956
+ runtime.registerEditorStatePersister(persister);
957
+ },
958
+ getEditorStateKey: (namespace) => {
959
+ return runtime.getEditorStateKey(namespace);
960
+ },
961
+ retryPendingPersist: async (namespace) => {
962
+ await runtime.retryPendingPersist(namespace);
963
+ },
941
964
  setHostAnnotationOverlay: (overlay) => {
942
965
  runtime.setHostAnnotationOverlay(clonePublicValue(overlay));
943
966
  },
@@ -985,16 +1008,9 @@ export function __createWordReviewEditorRefBridge(
985
1008
 
986
1009
  export function __applyRuntimeTextCommand(
987
1010
  runtime: WordReviewEditorRuntime,
988
- command:
989
- | { type: "insert-text"; text: string }
990
- | { type: "delete-backward" }
991
- | { type: "delete-forward" }
992
- | { type: "insert-tab" }
993
- | { type: "outdent-tab" }
994
- | { type: "insert-hard-break" }
995
- | { type: "split-paragraph" },
1011
+ command: DispatchTextCommand,
996
1012
  ): void {
997
- applyRuntimeTextCommand(runtime, command);
1013
+ dispatchTextCommand(runtime, command, DISPATCH_CONTEXT);
998
1014
  }
999
1015
 
1000
1016
  export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditorProps>(
@@ -1023,6 +1039,9 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
1023
1039
  onWarning,
1024
1040
  onReviewSidebarTrackedChanges,
1025
1041
  onReviewSidebarComments,
1042
+ onFindRequested,
1043
+ onPrintRequested,
1044
+ onZoomRequested,
1026
1045
  readOnly = false,
1027
1046
  reviewMode = "review",
1028
1047
  suggestionsEnabled = false,
@@ -1961,6 +1980,22 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
1961
1980
  pendingConflicts: metadataConflictPendingRef.current,
1962
1981
  });
1963
1982
  },
1983
+ // Schema 1.2 — EditorStateChannel delegation.
1984
+ configureEditorStatePolicy: (policy) => {
1985
+ activeRuntime.configureEditorStatePolicy(policy);
1986
+ },
1987
+ registerEditorStateResolver: (resolver) => {
1988
+ activeRuntime.registerEditorStateResolver(resolver);
1989
+ },
1990
+ registerEditorStatePersister: (persister) => {
1991
+ activeRuntime.registerEditorStatePersister(persister);
1992
+ },
1993
+ getEditorStateKey: (namespace) => {
1994
+ return activeRuntime.getEditorStateKey(namespace);
1995
+ },
1996
+ retryPendingPersist: async (namespace) => {
1997
+ await activeRuntime.retryPendingPersist(namespace);
1998
+ },
1964
1999
  setHostAnnotationOverlay: (overlay) => {
1965
2000
  setHostAnnotationOverlayState(clonePublicValue(overlay));
1966
2001
  },
@@ -2109,7 +2144,7 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
2109
2144
 
2110
2145
  function addReviewComment(): string | null {
2111
2146
  try {
2112
- const commentId = activeRuntime.addComment({
2147
+ const { commentId } = activeRuntime.addComment({
2113
2148
  anchor: snapshot.selection.activeRange,
2114
2149
  body: "",
2115
2150
  authorId: currentUser.userId,
@@ -2538,7 +2573,46 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
2538
2573
  },
2539
2574
  );
2540
2575
 
2541
- if (shortcut.kind === "none" || shortcut.kind === "delegate") {
2576
+ if (shortcut.kind === "none") {
2577
+ return;
2578
+ }
2579
+
2580
+ if (shortcut.kind === "delegate") {
2581
+ // Host-delegated shortcuts: if the host has wired a typed
2582
+ // callback we call it and suppress the browser default; if
2583
+ // not, the event falls through to the browser (Ctrl+F opens
2584
+ // Find, Ctrl+Plus zooms, etc.) — matching the legacy behavior.
2585
+ let handled = false;
2586
+ if (shortcut.shortcut === "find" && onFindRequested) {
2587
+ // selectionText is intentionally empty — hosts that need the
2588
+ // selected text already receive it via the selection_changed
2589
+ // event + canonicalDocument they have via onEvent listeners.
2590
+ // The range is the load-bearing field so host Find panels can
2591
+ // scope their search to the selection or pre-populate from it.
2592
+ onFindRequested({
2593
+ selectionText: "",
2594
+ selectionRange: snapshot.selection,
2595
+ });
2596
+ handled = true;
2597
+ } else if (shortcut.shortcut === "print" && onPrintRequested) {
2598
+ onPrintRequested();
2599
+ handled = true;
2600
+ } else if (
2601
+ (shortcut.shortcut === "zoom-in" ||
2602
+ shortcut.shortcut === "zoom-out" ||
2603
+ shortcut.shortcut === "zoom-reset") &&
2604
+ onZoomRequested
2605
+ ) {
2606
+ const direction =
2607
+ shortcut.shortcut === "zoom-in" ? "in" :
2608
+ shortcut.shortcut === "zoom-out" ? "out" : "reset";
2609
+ onZoomRequested(direction);
2610
+ handled = true;
2611
+ }
2612
+ if (handled) {
2613
+ event.preventDefault();
2614
+ event.stopPropagation();
2615
+ }
2542
2616
  return;
2543
2617
  }
2544
2618
 
@@ -2604,13 +2678,13 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
2604
2678
  onFocus: handleSurfaceFocus,
2605
2679
  onBlur: handleSurfaceBlur,
2606
2680
  onSelectionChange: dispatchSelection,
2607
- onInsertText: (text: string) => applyRuntimeTextCommand(activeRuntime, { type: "insert-text", text }),
2608
- onDeleteBackward: () => applyRuntimeTextCommand(activeRuntime, { type: "delete-backward" }),
2609
- onDeleteForward: () => applyRuntimeTextCommand(activeRuntime, { type: "delete-forward" }),
2610
- onInsertTab: () => applyRuntimeTextCommand(activeRuntime, { type: "insert-tab" }),
2611
- onOutdentTab: () => applyRuntimeTextCommand(activeRuntime, { type: "outdent-tab" }),
2612
- onInsertHardBreak: () => applyRuntimeTextCommand(activeRuntime, { type: "insert-hard-break" }),
2613
- onSplitParagraph: () => applyRuntimeTextCommand(activeRuntime, { type: "split-paragraph" }),
2681
+ onInsertText: (text: string) => dispatchTextCommand(activeRuntime, { type: "insert-text", text }, DISPATCH_CONTEXT),
2682
+ onDeleteBackward: () => dispatchTextCommand(activeRuntime, { type: "delete-backward" }, DISPATCH_CONTEXT),
2683
+ onDeleteForward: () => dispatchTextCommand(activeRuntime, { type: "delete-forward" }, DISPATCH_CONTEXT),
2684
+ onInsertTab: () => dispatchTextCommand(activeRuntime, { type: "insert-tab" }, DISPATCH_CONTEXT),
2685
+ onOutdentTab: () => dispatchTextCommand(activeRuntime, { type: "outdent-tab" }, DISPATCH_CONTEXT),
2686
+ onInsertHardBreak: () => dispatchTextCommand(activeRuntime, { type: "insert-hard-break" }, DISPATCH_CONTEXT),
2687
+ onSplitParagraph: () => dispatchTextCommand(activeRuntime, { type: "split-paragraph" }, DISPATCH_CONTEXT),
2614
2688
  onUndo: () => activeRuntime.undo(),
2615
2689
  onRedo: () => activeRuntime.redo(),
2616
2690
  onBlockedInput: (command: "paste" | "drop", message: string) =>
@@ -2618,6 +2692,19 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
2618
2692
  code: "unsupported_surface",
2619
2693
  message,
2620
2694
  }]),
2695
+ onPasteApplied: (meta: {
2696
+ segmentCount: number;
2697
+ charCount: number;
2698
+ source: "paste" | "drop";
2699
+ }) => {
2700
+ onEventRef.current?.({
2701
+ type: "paste_applied",
2702
+ documentId: props.documentId,
2703
+ segmentCount: meta.segmentCount,
2704
+ charCount: meta.charCount,
2705
+ source: meta.source,
2706
+ });
2707
+ },
2621
2708
  };
2622
2709
 
2623
2710
  const reviewCallbacks = {
@@ -2763,14 +2850,21 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
2763
2850
  applyRuntimeImageResize(activeRuntime, mediaId, dimensions),
2764
2851
  onSetImageFrame: (mediaId, offsets) =>
2765
2852
  applyRuntimeImageReposition(activeRuntime, mediaId, offsets),
2766
- onOpenHeaderStory: () =>
2767
- openDefaultStoryVariant(activeRuntime, snapshot.pageLayout, documentNavigation, "header"),
2768
- onOpenFooterStory: () =>
2769
- openDefaultStoryVariant(activeRuntime, snapshot.pageLayout, documentNavigation, "footer"),
2853
+ // P8.11 — `onOpenHeaderStory` / `onOpenFooterStory` retired from the
2854
+ // WordReviewEditor wiring. Per-page header / footer bands rendered
2855
+ // by `TwPageStackChromeLayer` call `onOpenStory(target)` with the
2856
+ // exact `EditorStoryTarget` they represent, so the variant /
2857
+ // relationship resolution happens inside the layout facet instead
2858
+ // of the UI. The deprecated props remain in the workspace type
2859
+ // with a mount-time `console.warn`; hosts that still pass them can
2860
+ // migrate to `onOpenStory` at their leisure.
2770
2861
  onOpenHeaderStoryForPage: (pageIndex: number) =>
2771
2862
  openStoryForPage(activeRuntime, pageIndex, "header"),
2772
2863
  onOpenFooterStoryForPage: (pageIndex: number) =>
2773
2864
  openStoryForPage(activeRuntime, pageIndex, "footer"),
2865
+ onOpenStory: (target) => {
2866
+ activeRuntime.openStory(target);
2867
+ },
2774
2868
  onDeleteSectionBreak: (sectionIndex) =>
2775
2869
  applyRuntimeDeleteSectionBreak(activeRuntime, sectionIndex),
2776
2870
  onUpdateSectionLayout: (sectionIndex, patch) =>
@@ -2901,6 +2995,17 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
2901
2995
  interactionGuardSnapshot={interactionGuardSnapshot}
2902
2996
  chromePreset={effectiveChromePreset}
2903
2997
  chromeOptions={chromeOptions}
2998
+ {...(props.collabSession ? { collabSession: props.collabSession } : {})}
2999
+ {...(props.collabTransportStatus
3000
+ ? { collabTransportStatus: props.collabTransportStatus }
3001
+ : {})}
3002
+ {...(props.activeCommentId !== undefined
3003
+ ? { activeCommentId: props.activeCommentId }
3004
+ : {})}
3005
+ collabActorId={currentUser.userId}
3006
+ {...(props.collabSendBaseline
3007
+ ? { collabSendBaseline: props.collabSendBaseline }
3008
+ : {})}
2904
3009
  reviewQueue={reviewQueueSnapshot}
2905
3010
  documentContextAnalytics={documentContextAnalytics}
2906
3011
  selectionContextAnalytics={selectionContextAnalytics}
@@ -3231,7 +3336,11 @@ function applyRuntimeListToggle(
3231
3336
  dispatchStoryMutationResult(
3232
3337
  runtime,
3233
3338
  context,
3234
- createListMutationResult(result, context.localSnapshot.selection),
3339
+ {
3340
+ changed: result.affectedParagraphIndexes.length > 0,
3341
+ document: result.document,
3342
+ selection: toRuntimeSelectionSnapshot(context.localSnapshot.selection),
3343
+ },
3235
3344
  context.timestamp,
3236
3345
  );
3237
3346
  }
@@ -4355,175 +4464,12 @@ function buildTablesFacet(
4355
4464
 
4356
4465
  export { buildTablesFacet as __buildTablesFacet };
4357
4466
 
4358
- function applyRuntimeTextCommand(
4359
- runtime: WordReviewEditorRuntime,
4360
- command:
4361
- | { type: "insert-text"; text: string }
4362
- | { type: "delete-backward" }
4363
- | { type: "delete-forward" }
4364
- | { type: "insert-tab" }
4365
- | { type: "outdent-tab" }
4366
- | { type: "insert-hard-break" }
4367
- | { type: "split-paragraph" },
4368
- ): void {
4369
- const snapshot = runtime.getRenderSnapshot();
4370
- const context = getStoryMutationContext(runtime, getMountedTextCommandName(command));
4371
- if (!context) {
4372
- return;
4373
- }
4374
-
4375
- const effectiveSelectionMode = runtime.getInteractionGuardSnapshot().effectiveMode;
4376
- const listAwareResult = applyListAwareTextCommand(context, command);
4377
- if (effectiveSelectionMode === "suggest" && listAwareResult) {
4378
- runtime.emitBlockedCommand(getMountedTextCommandName(command), [{
4379
- code: "suggesting_unsupported",
4380
- message: "List structure changes are not supported in suggesting mode.",
4381
- }]);
4382
- return;
4383
- }
4384
-
4385
- if (listAwareResult) {
4386
- dispatchStoryMutationResult(runtime, context, listAwareResult, context.timestamp);
4387
- return;
4388
- }
4389
-
4390
- switch (command.type) {
4391
- case "insert-text":
4392
- runtime.applyActiveStoryTextCommand({ type: "text.insert", text: command.text });
4393
- return;
4394
- case "delete-backward":
4395
- runtime.applyActiveStoryTextCommand({ type: "text.delete-backward" });
4396
- return;
4397
- case "delete-forward":
4398
- runtime.applyActiveStoryTextCommand({ type: "text.delete-forward" });
4399
- return;
4400
- case "insert-tab":
4401
- runtime.applyActiveStoryTextCommand({ type: "text.insert-tab" });
4402
- return;
4403
- case "outdent-tab":
4404
- return;
4405
- case "insert-hard-break":
4406
- runtime.applyActiveStoryTextCommand({ type: "text.insert-hard-break" });
4407
- return;
4408
- case "split-paragraph":
4409
- runtime.applyActiveStoryTextCommand({ type: "paragraph.split" });
4410
- return;
4411
- }
4412
- }
4413
-
4414
- function getMountedTextCommandName(
4415
- command:
4416
- | { type: "insert-text"; text: string }
4417
- | { type: "delete-backward" }
4418
- | { type: "delete-forward" }
4419
- | { type: "insert-tab" }
4420
- | { type: "outdent-tab" }
4421
- | { type: "insert-hard-break" }
4422
- | { type: "split-paragraph" },
4423
- ): string {
4424
- switch (command.type) {
4425
- case "insert-text":
4426
- return "text.insert";
4427
- case "delete-backward":
4428
- return "text.delete-backward";
4429
- case "delete-forward":
4430
- return "text.delete-forward";
4431
- case "insert-tab":
4432
- case "outdent-tab":
4433
- return "text.insert-tab";
4434
- case "insert-hard-break":
4435
- return "text.insert-hard-break";
4436
- case "split-paragraph":
4437
- return "paragraph.split";
4438
- }
4439
- }
4440
-
4441
- function applyListAwareTextCommand(
4442
- context: NonNullable<ReturnType<typeof getStoryMutationContext>>,
4443
- command:
4444
- | { type: "insert-text"; text: string }
4445
- | { type: "delete-backward" }
4446
- | { type: "delete-forward" }
4447
- | { type: "insert-tab" }
4448
- | { type: "outdent-tab" }
4449
- | { type: "insert-hard-break" }
4450
- | { type: "split-paragraph" },
4451
- ): {
4452
- changed: boolean;
4453
- document: EditorSessionState["canonicalDocument"];
4454
- selection: InternalSelectionSnapshot;
4455
- } | null {
4456
- const paragraphContext = resolveActiveParagraphContext(context.localSnapshot);
4457
- if (!paragraphContext?.paragraph.numbering) {
4458
- return null;
4459
- }
4460
-
4461
- switch (command.type) {
4462
- case "insert-tab": {
4463
- const result = indentListItems(
4464
- context.localDocument,
4465
- [paragraphContext.paragraphIndex],
4466
- { timestamp: context.timestamp },
4467
- );
4468
- return createListMutationResult(result, context.localSnapshot.selection);
4469
- }
4470
- case "outdent-tab": {
4471
- const result = outdentListItems(
4472
- context.localDocument,
4473
- [paragraphContext.paragraphIndex],
4474
- { timestamp: context.timestamp },
4475
- );
4476
- return createListMutationResult(result, context.localSnapshot.selection);
4477
- }
4478
- case "delete-backward": {
4479
- if (!paragraphContext.atParagraphStart || !context.localSnapshot.selection.isCollapsed) {
4480
- return null;
4481
- }
4482
- const result = backspaceAtListStart(
4483
- context.localDocument,
4484
- paragraphContext.paragraphIndex,
4485
- { timestamp: context.timestamp },
4486
- );
4487
- return result.handled
4488
- ? createListMutationResult(result, context.localSnapshot.selection)
4489
- : null;
4490
- }
4491
- case "split-paragraph": {
4492
- if (!context.localSnapshot.selection.isCollapsed || !paragraphContext.isEmpty) {
4493
- return null;
4494
- }
4495
- const result = splitListParagraph(
4496
- context.localDocument,
4497
- paragraphContext.paragraphIndex,
4498
- true,
4499
- { timestamp: context.timestamp },
4500
- );
4501
- return result.action === "split"
4502
- ? null
4503
- : createListMutationResult(result, context.localSnapshot.selection);
4504
- }
4505
- default:
4506
- return null;
4507
- }
4508
- }
4509
-
4510
- function createListMutationResult(
4511
- result: {
4512
- document: EditorSessionState["canonicalDocument"];
4513
- affectedParagraphIndexes: number[];
4514
- },
4515
- selection: RuntimeRenderSnapshot["selection"],
4516
- ): {
4517
- changed: boolean;
4518
- document: EditorSessionState["canonicalDocument"];
4519
- selection: InternalSelectionSnapshot;
4520
- } {
4521
- return {
4522
- changed: result.affectedParagraphIndexes.length > 0,
4523
- document: result.document,
4524
- selection: toRuntimeSelectionSnapshot(selection),
4525
- };
4526
- }
4467
+ const DISPATCH_CONTEXT: DispatchContext = {
4468
+ getStoryMutationContext,
4469
+ dispatchStoryMutationResult,
4470
+ resolveActiveParagraphContext,
4471
+ toRuntimeSelectionSnapshot,
4472
+ };
4527
4473
 
4528
4474
  function resolveActiveParagraphContext(
4529
4475
  snapshot: Pick<RuntimeRenderSnapshot, "surface" | "selection">,
@@ -4866,47 +4812,6 @@ function openStoryForPage(
4866
4812
  runtime.openStory(target);
4867
4813
  }
4868
4814
 
4869
- function openDefaultStoryVariant(
4870
- runtime: WordReviewEditorRuntime,
4871
- pageLayout: PageLayoutSnapshot | undefined,
4872
- navigation: ReturnType<WordReviewEditorRuntime["getDocumentNavigationSnapshot"]> | undefined,
4873
- kind: "header" | "footer",
4874
- ): void {
4875
- const variants =
4876
- kind === "header"
4877
- ? pageLayout?.headerVariants
4878
- : pageLayout?.footerVariants;
4879
- const activePage = navigation?.pages[navigation.activePageIndex];
4880
- const isFirstPageInSection =
4881
- activePage !== undefined &&
4882
- activePage.sectionIndex === pageLayout?.sectionIndex &&
4883
- activePage.pageInSection === 0;
4884
- const isEvenDocumentPage = activePage !== undefined && (activePage.pageIndex + 1) % 2 === 0;
4885
-
4886
- let variant =
4887
- pageLayout?.differentFirstPage && isFirstPageInSection
4888
- ? variants?.find((entry) => entry.variant === "first")
4889
- : undefined;
4890
-
4891
- if (!variant && pageLayout?.differentOddEvenPages && isEvenDocumentPage) {
4892
- variant = variants?.find((entry) => entry.variant === "even");
4893
- }
4894
-
4895
- if (!variant) {
4896
- variant = variants?.find((entry) => entry.variant === "default") ?? variants?.[0];
4897
- }
4898
-
4899
- if (!variant) {
4900
- return;
4901
- }
4902
- runtime.openStory({
4903
- kind,
4904
- relationshipId: variant.relationshipId,
4905
- variant: variant.variant,
4906
- sectionIndex: pageLayout?.sectionIndex,
4907
- });
4908
- }
4909
-
4910
4815
  function searchRuntimeDocument(
4911
4816
  runtime: WordReviewEditorRuntime,
4912
4817
  mountedSurface: TwProseMirrorSurfaceRef | null,
@@ -2,6 +2,7 @@ import { useMemo, useRef } from "react";
2
2
 
3
3
  import type {
4
4
  CommentSidebarThreadSnapshot,
5
+ EditorStoryTarget,
5
6
  FormattingAlignment,
6
7
  HeaderFooterLinkPatch,
7
8
  InsertImageOptions,
@@ -87,12 +88,25 @@ export interface EditorCommandBag {
87
88
  onAcceptAllChanges(): void;
88
89
  onRejectAllChanges(): void;
89
90
  onCloseStory?(): void;
91
+ /**
92
+ * @deprecated P8.11 — see the matching prop on `TwReviewWorkspaceProps`.
93
+ * Kept optional for back-compat; per-page bands use `onOpenStory`.
94
+ */
90
95
  onOpenHeaderStory?(): void;
96
+ /**
97
+ * @deprecated P8.11 — see `onOpenHeaderStory`.
98
+ */
91
99
  onOpenFooterStory?(): void;
92
100
  /** Open the header story for a specific page (double-click on its band). */
93
101
  onOpenHeaderStoryForPage?(pageIndex: number): void;
94
102
  /** Open the footer story for a specific page (double-click on its band). */
95
103
  onOpenFooterStoryForPage?(pageIndex: number): void;
104
+ /**
105
+ * P8.11 — per-page header/footer band click handler. Receives the
106
+ * exact `EditorStoryTarget` the band represents; the command bag wires
107
+ * this to `runtime.openStory(target)`.
108
+ */
109
+ onOpenStory?(target: EditorStoryTarget): void;
96
110
  onSetParagraphIndentation?(indentation: {
97
111
  left?: number;
98
112
  right?: number;