@beyondwork/docx-react-component 1.0.56 → 1.0.58

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 (113) hide show
  1. package/README.md +1 -1
  2. package/package.json +1 -1
  3. package/src/api/public-types.ts +330 -0
  4. package/src/compare/diff-engine.ts +3 -0
  5. package/src/core/commands/formatting-commands.ts +1 -0
  6. package/src/core/commands/index.ts +17 -11
  7. package/src/core/selection/mapping.ts +18 -1
  8. package/src/core/selection/review-anchors.ts +29 -18
  9. package/src/io/chart-preview-resolver.ts +175 -41
  10. package/src/io/docx-session.ts +57 -2
  11. package/src/io/export/serialize-main-document.ts +82 -0
  12. package/src/io/export/serialize-styles.ts +61 -3
  13. package/src/io/export/table-properties-xml.ts +19 -4
  14. package/src/io/normalize/normalize-text.ts +33 -0
  15. package/src/io/ooxml/parse-anchor.ts +182 -0
  16. package/src/io/ooxml/parse-drawing.ts +319 -0
  17. package/src/io/ooxml/parse-fields.ts +115 -2
  18. package/src/io/ooxml/parse-fill.ts +215 -0
  19. package/src/io/ooxml/parse-font-table.ts +190 -0
  20. package/src/io/ooxml/parse-footnotes.ts +52 -1
  21. package/src/io/ooxml/parse-main-document.ts +241 -1
  22. package/src/io/ooxml/parse-numbering.ts +96 -0
  23. package/src/io/ooxml/parse-picture.ts +158 -0
  24. package/src/io/ooxml/parse-settings.ts +34 -0
  25. package/src/io/ooxml/parse-shapes.ts +87 -0
  26. package/src/io/ooxml/parse-solid-fill.ts +11 -0
  27. package/src/io/ooxml/parse-styles.ts +74 -1
  28. package/src/io/ooxml/parse-theme.ts +60 -0
  29. package/src/io/paste/html-clipboard.ts +449 -0
  30. package/src/io/paste/word-clipboard.ts +5 -1
  31. package/src/legal/_document-root.ts +26 -0
  32. package/src/legal/bookmarks.ts +4 -3
  33. package/src/legal/cross-references.ts +3 -2
  34. package/src/legal/defined-terms.ts +2 -1
  35. package/src/legal/signature-blocks.ts +2 -1
  36. package/src/model/canonical-document.ts +421 -3
  37. package/src/runtime/chart/chart-model-store.ts +73 -10
  38. package/src/runtime/document-runtime.ts +760 -41
  39. package/src/runtime/document-search.ts +61 -0
  40. package/src/runtime/edit-ops/index.ts +129 -0
  41. package/src/runtime/event-refresh-hints.ts +7 -0
  42. package/src/runtime/field-resolver.ts +341 -0
  43. package/src/runtime/footnote-resolver.ts +55 -0
  44. package/src/runtime/hyperlink-color-resolver.ts +13 -10
  45. package/src/runtime/object-grab/index.ts +51 -0
  46. package/src/runtime/paragraph-style-resolver.ts +105 -0
  47. package/src/runtime/query-scopes.ts +186 -0
  48. package/src/runtime/resolved-numbering-geometry.ts +12 -0
  49. package/src/runtime/scope-resolver.ts +60 -0
  50. package/src/runtime/selection/cursor-ops.ts +186 -15
  51. package/src/runtime/selection/index.ts +17 -1
  52. package/src/runtime/structure-ops/index.ts +77 -0
  53. package/src/runtime/styles-cascade.ts +33 -0
  54. package/src/runtime/surface-projection.ts +192 -12
  55. package/src/runtime/theme-color-resolver.ts +189 -44
  56. package/src/runtime/units.ts +46 -0
  57. package/src/runtime/view-state.ts +13 -2
  58. package/src/ui/WordReviewEditor.tsx +239 -11
  59. package/src/ui/editor-runtime-boundary.ts +97 -1
  60. package/src/ui/editor-shell-view.tsx +1 -1
  61. package/src/ui/runtime-shortcut-dispatch.ts +17 -3
  62. package/src/ui-tailwind/chart/ChartSurface.tsx +36 -10
  63. package/src/ui-tailwind/chart/layout/plot-area.ts +120 -45
  64. package/src/ui-tailwind/chart/render/area.tsx +22 -4
  65. package/src/ui-tailwind/chart/render/bar-column.tsx +37 -11
  66. package/src/ui-tailwind/chart/render/bubble.tsx +6 -2
  67. package/src/ui-tailwind/chart/render/combo.tsx +37 -4
  68. package/src/ui-tailwind/chart/render/line.tsx +28 -5
  69. package/src/ui-tailwind/chart/render/pie.tsx +36 -16
  70. package/src/ui-tailwind/chart/render/progressive-render.ts +8 -1
  71. package/src/ui-tailwind/chart/render/scatter.tsx +9 -4
  72. package/src/ui-tailwind/chrome/avatar-initials.ts +15 -0
  73. package/src/ui-tailwind/chrome/tw-comment-preview.tsx +3 -1
  74. package/src/ui-tailwind/chrome/tw-context-menu.tsx +14 -0
  75. package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +3 -2
  76. package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +30 -11
  77. package/src/ui-tailwind/chrome/tw-shortcut-hint.tsx +15 -2
  78. package/src/ui-tailwind/chrome/tw-suggestion-card.tsx +1 -1
  79. package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +24 -7
  80. package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +31 -12
  81. package/src/ui-tailwind/chrome-overlay/page-border-resolver.ts +211 -0
  82. package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +24 -0
  83. package/src/ui-tailwind/chrome-overlay/tw-comment-balloon-layer.tsx +74 -0
  84. package/src/ui-tailwind/chrome-overlay/tw-locked-block-layer.tsx +65 -0
  85. package/src/ui-tailwind/chrome-overlay/tw-object-selection-overlay.tsx +157 -0
  86. package/src/ui-tailwind/chrome-overlay/tw-page-border-overlay.tsx +233 -0
  87. package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +135 -13
  88. package/src/ui-tailwind/chrome-overlay/tw-revision-margin-bar-layer.tsx +51 -0
  89. package/src/ui-tailwind/chrome-overlay/tw-scope-card-layer.tsx +12 -4
  90. package/src/ui-tailwind/chrome-overlay/tw-scope-card.tsx +32 -12
  91. package/src/ui-tailwind/chrome-overlay/tw-toc-outline-sidebar.tsx +133 -0
  92. package/src/ui-tailwind/editor-surface/chart-node-view.tsx +49 -10
  93. package/src/ui-tailwind/editor-surface/float-wrap-resolver.ts +119 -0
  94. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +236 -9
  95. package/src/ui-tailwind/editor-surface/pm-schema.ts +214 -11
  96. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +32 -2
  97. package/src/ui-tailwind/editor-surface/shape-renderer.ts +206 -0
  98. package/src/ui-tailwind/editor-surface/surface-layer.ts +66 -0
  99. package/src/ui-tailwind/editor-surface/tw-inline-token.tsx +29 -0
  100. package/src/ui-tailwind/editor-surface/tw-segment-view.tsx +7 -1
  101. package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +22 -6
  102. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +10 -16
  103. package/src/ui-tailwind/review/tw-health-panel.tsx +0 -25
  104. package/src/ui-tailwind/review/tw-rail-card.tsx +38 -17
  105. package/src/ui-tailwind/review/tw-review-rail.tsx +2 -2
  106. package/src/ui-tailwind/review/tw-revision-sidebar.tsx +5 -12
  107. package/src/ui-tailwind/review/tw-workflow-tab.tsx +2 -2
  108. package/src/ui-tailwind/theme/editor-theme.css +1 -0
  109. package/src/ui-tailwind/theme/tokens.css +6 -0
  110. package/src/ui-tailwind/theme/tokens.ts +10 -0
  111. package/src/ui-tailwind/tw-review-workspace.tsx +23 -0
  112. package/src/validation/compatibility-engine.ts +2 -0
  113. package/src/validation/docx-comment-proof.ts +12 -3
@@ -52,6 +52,7 @@ import type {
52
52
  SurfaceBlockSnapshot,
53
53
  SurfaceInlineSegment,
54
54
  TableOp,
55
+ CanonicalDocumentFragment,
55
56
  TableOpResult,
56
57
  PublicTableEvent,
57
58
  PublicTableRenderPlan,
@@ -150,7 +151,7 @@ import {
150
151
  } from "../io/source-package-provenance.ts";
151
152
  import { readOpcPackage } from "../io/opc/package-reader.ts";
152
153
  import { deriveCapabilities } from "../runtime/session-capabilities";
153
- import { searchDocument } from "../runtime/document-search.ts";
154
+ import { findTextMatches, searchDocument } from "../runtime/document-search.ts";
154
155
  import {
155
156
  resolveCurrentContextAnalyticsQuery,
156
157
  runtimeContextAnalyticsSnapshotsEqual,
@@ -480,6 +481,50 @@ function collectChartSnapshots(doc: CanonicalDocType): import("../api/public-typ
480
481
  return results;
481
482
  }
482
483
 
484
+ /**
485
+ * Walk the canonical document, compute each chart_preview's stableChartId,
486
+ * and project a snapshot for the first matching id. Short-circuits on
487
+ * match so the happy path is O(k) in blocks-until-match rather than the
488
+ * O(N) the `collect().find()` fallback incurred for every ref call. For
489
+ * hosts that call `getChartSnapshot` in a tight loop over many chartIds,
490
+ * this reduces the cost from O(N²) to O(N·k).
491
+ */
492
+ function lookupChartSnapshot(
493
+ doc: CanonicalDocType,
494
+ chartId: string,
495
+ ): import("../api/public-types").ChartSnapshot | null {
496
+ return lookupChartSnapshotInBlocks(doc.content.children, chartId);
497
+ }
498
+
499
+ function lookupChartSnapshotInBlocks(
500
+ blocks: CanonicalDocType["content"]["children"],
501
+ chartId: string,
502
+ ): import("../api/public-types").ChartSnapshot | null {
503
+ for (const block of blocks) {
504
+ if (block.type === "paragraph") {
505
+ for (const inline of block.children) {
506
+ if (inline.type === "chart_preview" && inline.parsedData) {
507
+ const id = stableChartId(inline.rawXml);
508
+ if (id === chartId) {
509
+ return projectChartSnapshot(id, inline.parsedData);
510
+ }
511
+ }
512
+ }
513
+ } else if (block.type === "table") {
514
+ for (const row of block.rows) {
515
+ for (const cell of row.cells) {
516
+ const found = lookupChartSnapshotInBlocks(cell.children, chartId);
517
+ if (found) return found;
518
+ }
519
+ }
520
+ } else if (block.type === "sdt" || block.type === "custom_xml") {
521
+ const found = lookupChartSnapshotInBlocks(block.children, chartId);
522
+ if (found) return found;
523
+ }
524
+ }
525
+ return null;
526
+ }
527
+
483
528
  function collectChartSnapshotsFromBlocks(
484
529
  blocks: CanonicalDocType["content"]["children"],
485
530
  results: import("../api/public-types").ChartSnapshot[],
@@ -528,6 +573,16 @@ export function __createWordReviewEditorRefBridge(
528
573
  redo: () => runtime.redo(),
529
574
  replaceText: (text, target, formatting) => runtime.replaceText(text, target, formatting),
530
575
  insertFragment: (fragment, target) => runtime.insertFragment(fragment, target),
576
+ copy: (target) => runtime.copy(target),
577
+ cut: (target) => runtime.cut(target),
578
+ getClipboardBuffer: () => runtime.getClipboardBuffer(),
579
+ getClipboardWireFormats: () => runtime.getClipboardWireFormats(),
580
+ selectObject: (objectId) => runtime.selectObject(objectId),
581
+ deselectObject: () => runtime.deselectObject(),
582
+ getGrabbedObject: () => runtime.getGrabbedObject(),
583
+ startAction: (name) => runtime.startAction(name),
584
+ endAction: () => runtime.endAction(),
585
+ isInAction: () => runtime.isInAction(),
531
586
  addComment: (params) => runtime.addComment(params),
532
587
  openComment: (commentId) => runtime.openComment(commentId),
533
588
  resolveComment: (commentId) => runtime.resolveComment(commentId),
@@ -554,11 +609,7 @@ export function __createWordReviewEditorRefBridge(
554
609
  getRenderSnapshot: () => clonePublicValue(runtime.getRenderSnapshot()),
555
610
  getCompatibilityReport: () => runtime.getCompatibilityReport(),
556
611
  getWarnings: () => runtime.getWarnings(),
557
- getChartSnapshot: (chartId) => {
558
- return collectChartSnapshots(runtime.getCanonicalDocument()).find(
559
- (s) => s.chartId === chartId,
560
- ) ?? null;
561
- },
612
+ getChartSnapshot: (chartId) => lookupChartSnapshot(runtime.getCanonicalDocument(), chartId),
562
613
  getChartSnapshots: () => collectChartSnapshots(runtime.getCanonicalDocument()),
563
614
  getCommentSidebarSnapshot: () =>
564
615
  clonePublicValue(runtime.getRenderSnapshot().comments),
@@ -858,6 +909,34 @@ export function __createWordReviewEditorRefBridge(
858
909
  getWorkflowMetadataSnapshot: () => {
859
910
  return clonePublicValue(runtime.getWorkflowMetadataSnapshot());
860
911
  },
912
+ queryScopes: (filter) => {
913
+ return clonePublicValue(runtime.queryScopes(filter));
914
+ },
915
+ findScopesAt: (position, options) => {
916
+ return clonePublicValue(runtime.findScopesAt(position, options));
917
+ },
918
+ findScopesIntersecting: (range, options) => {
919
+ return clonePublicValue(runtime.findScopesIntersecting(range, options));
920
+ },
921
+ findFirstText: (query, opts) => {
922
+ const hits = findTextMatchesForRuntime(runtime, query, opts);
923
+ return hits.length > 0 ? (hits[0] ?? null) : null;
924
+ },
925
+ findAllText: (query, opts) => {
926
+ return findTextMatchesForRuntime(runtime, query, opts);
927
+ },
928
+ selectFirstText: (query, opts) => {
929
+ const hits = findTextMatchesForRuntime(runtime, query, opts);
930
+ if (hits.length === 0) return false;
931
+ applyRuntimeSelection(runtime, createSelectionFromAnchor(hits[0]!));
932
+ return true;
933
+ },
934
+ selectAllText: (query, opts) => {
935
+ const hits = findTextMatchesForRuntime(runtime, query, opts);
936
+ if (hits.length === 0) return 0;
937
+ applyRuntimeSelection(runtime, createSelectionFromAnchor(hits[0]!));
938
+ return hits.length;
939
+ },
861
940
  // P17 — metadata persistence toggle + convert methods.
862
941
  setMetadataPersistenceMode: (mode) => {
863
942
  if (mode === "external" && !(options?.resolverRef?.current ?? null)) {
@@ -1528,6 +1607,16 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
1528
1607
  redo: () => activeRuntime.redo(),
1529
1608
  replaceText: (text, target, formatting) => activeRuntime.replaceText(text, target, formatting),
1530
1609
  insertFragment: (fragment, target) => activeRuntime.insertFragment(fragment, target),
1610
+ copy: (target) => activeRuntime.copy(target),
1611
+ cut: (target) => activeRuntime.cut(target),
1612
+ getClipboardBuffer: () => activeRuntime.getClipboardBuffer(),
1613
+ getClipboardWireFormats: () => activeRuntime.getClipboardWireFormats(),
1614
+ selectObject: (objectId) => activeRuntime.selectObject(objectId),
1615
+ deselectObject: () => activeRuntime.deselectObject(),
1616
+ getGrabbedObject: () => activeRuntime.getGrabbedObject(),
1617
+ startAction: (name) => activeRuntime.startAction(name),
1618
+ endAction: () => activeRuntime.endAction(),
1619
+ isInAction: () => activeRuntime.isInAction(),
1531
1620
  addComment: (params) =>
1532
1621
  activeRuntime.addComment({
1533
1622
  ...params,
@@ -1584,9 +1673,7 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
1584
1673
  getCompatibilityReport: () => activeRuntime.getCompatibilityReport(),
1585
1674
  getWarnings: () => activeRuntime.getWarnings(),
1586
1675
  getChartSnapshot: (chartId) =>
1587
- collectChartSnapshots(activeRuntime.getCanonicalDocument()).find(
1588
- (s) => s.chartId === chartId,
1589
- ) ?? null,
1676
+ lookupChartSnapshot(activeRuntime.getCanonicalDocument(), chartId),
1590
1677
  getChartSnapshots: () => collectChartSnapshots(activeRuntime.getCanonicalDocument()),
1591
1678
  getCommentSidebarSnapshot: () =>
1592
1679
  clonePublicValue(activeRuntime.getRenderSnapshot().comments),
@@ -1917,6 +2004,34 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
1917
2004
  getWorkflowMetadataSnapshot: () => {
1918
2005
  return clonePublicValue(activeRuntime.getWorkflowMetadataSnapshot());
1919
2006
  },
2007
+ queryScopes: (filter) => {
2008
+ return clonePublicValue(activeRuntime.queryScopes(filter));
2009
+ },
2010
+ findScopesAt: (position, options) => {
2011
+ return clonePublicValue(activeRuntime.findScopesAt(position, options));
2012
+ },
2013
+ findScopesIntersecting: (range, options) => {
2014
+ return clonePublicValue(activeRuntime.findScopesIntersecting(range, options));
2015
+ },
2016
+ findFirstText: (query, opts) => {
2017
+ const hits = findTextMatchesForRuntime(activeRuntime, query, opts);
2018
+ return hits.length > 0 ? (hits[0] ?? null) : null;
2019
+ },
2020
+ findAllText: (query, opts) => {
2021
+ return findTextMatchesForRuntime(activeRuntime, query, opts);
2022
+ },
2023
+ selectFirstText: (query, opts) => {
2024
+ const hits = findTextMatchesForRuntime(activeRuntime, query, opts);
2025
+ if (hits.length === 0) return false;
2026
+ applyRuntimeSelection(activeRuntime, createSelectionFromAnchor(hits[0]!));
2027
+ return true;
2028
+ },
2029
+ selectAllText: (query, opts) => {
2030
+ const hits = findTextMatchesForRuntime(activeRuntime, query, opts);
2031
+ if (hits.length === 0) return 0;
2032
+ applyRuntimeSelection(activeRuntime, createSelectionFromAnchor(hits[0]!));
2033
+ return hits.length;
2034
+ },
1920
2035
  // P17 — metadata persistence toggle + convert methods.
1921
2036
  setMetadataPersistenceMode: (mode) => {
1922
2037
  if (mode === "external" && scopeMetadataResolverRef.current === null) {
@@ -2777,10 +2892,84 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
2777
2892
  onBlur: handleSurfaceBlur,
2778
2893
  onSelectionChange: dispatchSelection,
2779
2894
  onInsertText: (text: string) => dispatchTextCommand(activeRuntime, { type: "insert-text", text }, DISPATCH_CONTEXT),
2780
- onDeleteBackward: () => dispatchTextCommand(activeRuntime, { type: "delete-backward" }, DISPATCH_CONTEXT),
2781
- onDeleteForward: () => dispatchTextCommand(activeRuntime, { type: "delete-forward" }, DISPATCH_CONTEXT),
2895
+ // v5 follow-up Delete/Backspace when a grab is active (R.3
2896
+ // ObjectGrabLayer). LO reference: `SwFEShell::DeleteSelection()` is
2897
+ // grab-aware; without grab-awareness, Delete would run
2898
+ // paragraph-delete on whichever paragraph the text caret happened to
2899
+ // be in — a silent data-loss footgun.
2900
+ //
2901
+ // Minimum safe behavior until a proper `object.delete` runtime
2902
+ // command + Lane 6 P11 chrome land: swallow the key (don't run
2903
+ // paragraph-delete) and deselect the grab so the user can see that
2904
+ // the key was recognized. The actual object removal follows when
2905
+ // chrome wires resize/delete drag handles; this gate just prevents
2906
+ // the paragraph-delete footgun.
2907
+ onDeleteBackward: () => {
2908
+ const grabbed = activeRuntime.getGrabbedObject();
2909
+ if (grabbed) {
2910
+ activeRuntime.deselectObject();
2911
+ return;
2912
+ }
2913
+ dispatchTextCommand(activeRuntime, { type: "delete-backward" }, DISPATCH_CONTEXT);
2914
+ },
2915
+ onDeleteForward: () => {
2916
+ const grabbed = activeRuntime.getGrabbedObject();
2917
+ if (grabbed) {
2918
+ activeRuntime.deselectObject();
2919
+ return;
2920
+ }
2921
+ dispatchTextCommand(activeRuntime, { type: "delete-forward" }, DISPATCH_CONTEXT);
2922
+ },
2782
2923
  onInsertTab: () => dispatchTextCommand(activeRuntime, { type: "insert-tab" }, DISPATCH_CONTEXT),
2783
2924
  onOutdentTab: () => dispatchTextCommand(activeRuntime, { type: "outdent-tab" }, DISPATCH_CONTEXT),
2925
+ // I3 widening tail — Tab at last cell: dispatch `add-row-after` via the
2926
+ // same table-structure-operation path every other row-insert caller uses
2927
+ // (toolbar, context menu, ref method), so track-changes + collab replay
2928
+ // stay consistent. See src/ui-tailwind/editor-surface/pm-command-bridge.ts
2929
+ // `isAtLastCellOfTable` for the detection criterion.
2930
+ onTableInsertRowBelow: () =>
2931
+ applyRuntimeTableStructureOperation(activeRuntime, surfaceRef.current, {
2932
+ type: "add-row-after",
2933
+ }),
2934
+ // I2 Tier B Slice 5 — drag-to-move / drag-to-copy.
2935
+ // v5 close-out: bridge now emits `dragstart`/`dragend` so same-editor
2936
+ // drag-to-move gets `sourceRange` populated. On move-effect drops we
2937
+ // insert the fragment AND delete the source range — the full Word
2938
+ // behavior. Both ops are wrapped in one action bracket via the
2939
+ // auto-bracketing in insertFragment + our explicit startAction around
2940
+ // the delete, so the undo history sees a single atomic action.
2941
+ onDropFragment: (meta: {
2942
+ fragment: CanonicalDocumentFragment;
2943
+ effect: "move" | "copy";
2944
+ sourceRange?: { from: number; to: number };
2945
+ }) => {
2946
+ if (meta.effect === "move" && meta.sourceRange) {
2947
+ // Delete source FIRST, then insert at the drop position. This order
2948
+ // is correctness-critical: if the drop was AFTER the source, the
2949
+ // insertion would shift the source offsets forward and a subsequent
2950
+ // delete would target the wrong (post-insertion) positions. By
2951
+ // deleting first and letting `insertFragment` land on the current
2952
+ // selection, both orderings behave correctly. The runtime's
2953
+ // selection validator (R.5.b) clamps the drop-site selection if
2954
+ // the source delete shifted it. Word degenerates move to copy
2955
+ // when drop is inside source; we pass through unchanged.
2956
+ activeRuntime.startAction("drag-move");
2957
+ try {
2958
+ activeRuntime.replaceText("", {
2959
+ kind: "range",
2960
+ from: meta.sourceRange.from,
2961
+ to: meta.sourceRange.to,
2962
+ assoc: { start: -1, end: 1 },
2963
+ });
2964
+ activeRuntime.insertFragment(meta.fragment);
2965
+ } finally {
2966
+ activeRuntime.endAction();
2967
+ }
2968
+ } else {
2969
+ // Copy-effect OR external drag (no sourceRange) — just insert.
2970
+ activeRuntime.insertFragment(meta.fragment);
2971
+ }
2972
+ },
2784
2973
  onInsertHardBreak: () => dispatchTextCommand(activeRuntime, { type: "insert-hard-break" }, DISPATCH_CONTEXT),
2785
2974
  onSplitParagraph: () => dispatchTextCommand(activeRuntime, { type: "split-paragraph" }, DISPATCH_CONTEXT),
2786
2975
  onUndo: () => activeRuntime.undo(),
@@ -2803,6 +2992,31 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
2803
2992
  source: meta.source,
2804
2993
  });
2805
2994
  },
2995
+ // v5 close-out: wire rich-paste fragment routing to insertFragment so
2996
+ // Office / HTML clipboard payloads land in the mounted editor. The
2997
+ // insertFragment auto-bracket (R.5.a Phase 2) makes the paste a
2998
+ // single-undo action.
2999
+ onPasteFragment: (meta: {
3000
+ fragment: CanonicalDocumentFragment;
3001
+ source: "wordml" | "html";
3002
+ }) => {
3003
+ activeRuntime.insertFragment(meta.fragment);
3004
+ },
3005
+ // v5 close-out: image paste via existing insertImage ref method.
3006
+ // Width/height are omitted so the renderer picks sensible defaults
3007
+ // based on the decoded bitmap dimensions (existing insertImage
3008
+ // behavior); hosts that need specific dimensions can intercept this
3009
+ // callback on their own CommandBridgeCallbacks override.
3010
+ onPasteImage: (meta: {
3011
+ data: Uint8Array;
3012
+ mimeType: string;
3013
+ source: "paste" | "drop";
3014
+ }) => {
3015
+ applyRuntimeInsertImage(activeRuntime, {
3016
+ data: meta.data,
3017
+ mimeType: meta.mimeType,
3018
+ });
3019
+ },
2806
3020
  };
2807
3021
 
2808
3022
  const reviewCallbacks = {
@@ -4898,6 +5112,20 @@ function clonePublicValue<T>(value: T): T {
4898
5112
  return structuredClone(value);
4899
5113
  }
4900
5114
 
5115
+ function findTextMatchesForRuntime(
5116
+ runtime: WordReviewEditorRuntime,
5117
+ query: string,
5118
+ options: SearchOptions | undefined,
5119
+ ): EditorAnchorProjection[] {
5120
+ const snapshot = runtime.getRenderSnapshot();
5121
+ return findTextMatches(
5122
+ runtime.getSessionState().canonicalDocument,
5123
+ snapshot.selection,
5124
+ query,
5125
+ options ?? {},
5126
+ );
5127
+ }
5128
+
4901
5129
  /**
4902
5130
  * Open the correct header/footer story for a specific page. The page's
4903
5131
  * resolved `stories.header` / `stories.footer` already carries the
@@ -122,6 +122,12 @@ export interface WordReviewEditorRuntime extends DocumentRuntime {
122
122
  ): void;
123
123
  }
124
124
 
125
+ type InternalWordReviewEditorRuntime = WordReviewEditorRuntime & {
126
+ hydrateChartPreviews?: (
127
+ resolvedDocument: EditorSessionState["canonicalDocument"],
128
+ ) => boolean;
129
+ };
130
+
125
131
  type PackageBackedDocxSession = ReturnType<typeof loadDocxEditorSession>;
126
132
 
127
133
  interface SnapshotExportBarrier {
@@ -313,6 +319,9 @@ export function useEditorRuntimeBoundary(
313
319
  progressiveSurfaceRef.current = progressiveSurface;
314
320
  const runtimeRef = useRef<WordReviewEditorRuntime | null>(null);
315
321
  const pendingReadySourceRef = useRef<"docx" | "session" | "snapshot" | null>(null);
322
+ const activeLoadTokenRef = useRef<symbol | null>(null);
323
+ const pendingChartPreviewDocRef =
324
+ useRef<EditorSessionState["canonicalDocument"] | null>(null);
316
325
  const autosaveTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
317
326
  const lastSavedRevisionTokenRef = useRef<string | null>(null);
318
327
  // Fastload P6: scheduler used by the DOM-side async docx loader. Held
@@ -386,6 +395,9 @@ export function useEditorRuntimeBoundary(
386
395
  async function loadRuntime(): Promise<void> {
387
396
  setLoadError(null);
388
397
  pendingReadySourceRef.current = null;
398
+ pendingChartPreviewDocRef.current = null;
399
+ const loadToken = Symbol(documentId);
400
+ activeLoadTokenRef.current = loadToken;
389
401
  if (autosaveTimerRef.current) {
390
402
  clearTimeout(autosaveTimerRef.current);
391
403
  autosaveTimerRef.current = null;
@@ -448,12 +460,34 @@ export function useEditorRuntimeBoundary(
448
460
  return;
449
461
  }
450
462
  recordPerfSample("loadSession.laycacheProbe");
463
+ const handleChartPreviewsReady = (
464
+ resolvedDocument: EditorSessionState["canonicalDocument"],
465
+ ): void => {
466
+ if (cancelled || activeLoadTokenRef.current !== loadToken) {
467
+ return;
468
+ }
469
+ pendingChartPreviewDocRef.current = resolvedDocument;
470
+ if (source.preloadedDocxSession) {
471
+ source.preloadedDocxSession.initialSessionState = {
472
+ ...source.preloadedDocxSession.initialSessionState,
473
+ canonicalDocument: resolvedDocument,
474
+ };
475
+ source.preloadedDocxSession.initialSnapshot = {
476
+ ...source.preloadedDocxSession.initialSnapshot,
477
+ canonicalDocument: resolvedDocument,
478
+ };
479
+ }
480
+ (
481
+ runtimeRef.current as InternalWordReviewEditorRuntime | null
482
+ )?.hydrateChartPreviews?.(resolvedDocument);
483
+ };
451
484
  const preloaded = await loadDocxEditorSessionAsync({
452
485
  documentId,
453
486
  sourceLabel: source.sourceLabel,
454
487
  bytes: source.initialDocx,
455
488
  editorBuild: "dev",
456
489
  scheduler,
490
+ onChartPreviewsReady: handleChartPreviewsReady,
457
491
  ...(probeResult ? { laycacheEnvelope: probeResult.envelope } : {
458
492
  // C2c: on the cold path (no cached envelope), emit the first
459
493
  // viewport surface as soon as body-normalize completes so the
@@ -475,6 +509,16 @@ export function useEditorRuntimeBoundary(
475
509
  return;
476
510
  }
477
511
  source.preloadedDocxSession = preloaded;
512
+ if (pendingChartPreviewDocRef.current) {
513
+ source.preloadedDocxSession.initialSessionState = {
514
+ ...source.preloadedDocxSession.initialSessionState,
515
+ canonicalDocument: pendingChartPreviewDocRef.current,
516
+ };
517
+ source.preloadedDocxSession.initialSnapshot = {
518
+ ...source.preloadedDocxSession.initialSnapshot,
519
+ canonicalDocument: pendingChartPreviewDocRef.current,
520
+ };
521
+ }
478
522
  if (probeResult) {
479
523
  source.preloadedLaycacheGraph = probeResult.envelope.graph;
480
524
  }
@@ -499,6 +543,11 @@ export function useEditorRuntimeBoundary(
499
543
  );
500
544
  recordPerfSample("runtime.create");
501
545
  runtimeRef.current = nextRuntime;
546
+ if (pendingChartPreviewDocRef.current) {
547
+ (
548
+ nextRuntime as InternalWordReviewEditorRuntime
549
+ ).hydrateChartPreviews?.(pendingChartPreviewDocRef.current);
550
+ }
502
551
  pendingReadySourceRef.current = source.source;
503
552
  // C2c: clear the transient progressive surface — the full runtime
504
553
  // snapshot supersedes it. No need for startTransition here since
@@ -530,6 +579,8 @@ export function useEditorRuntimeBoundary(
530
579
 
531
580
  return () => {
532
581
  cancelled = true;
582
+ pendingChartPreviewDocRef.current = null;
583
+ activeLoadTokenRef.current = null;
533
584
  };
534
585
  }, [
535
586
  documentId,
@@ -753,6 +804,11 @@ function createRuntime(
753
804
  defaultAuthorId: args.currentUserId,
754
805
  onCommandApplied: args.commandAppliedBridge?.onCommandApplied,
755
806
  });
807
+ const internalBaseRuntime = baseRuntime as DocumentRuntime & {
808
+ hydrateCanonicalDocumentInternally?: (
809
+ document: EditorSessionState["canonicalDocument"],
810
+ ) => boolean;
811
+ };
756
812
 
757
813
  // Schema 1.2: drive load-path hydration from the parsed envelope.
758
814
  if (docxSession?.initialEditorStatePayload) {
@@ -784,7 +840,7 @@ function createRuntime(
784
840
  });
785
841
  }
786
842
 
787
- const runtime: WordReviewEditorRuntime = Object.assign(baseRuntime, {
843
+ const runtime: InternalWordReviewEditorRuntime = Object.assign(baseRuntime, {
788
844
  drainBootstrapEvents: () => bootstrapEvents.splice(0, bootstrapEvents.length),
789
845
  emitBlockedCommand: (
790
846
  command: string,
@@ -802,6 +858,25 @@ function createRuntime(
802
858
  },
803
859
  });
804
860
  },
861
+ hydrateChartPreviews: (
862
+ resolvedDocument: EditorSessionState["canonicalDocument"],
863
+ ): boolean => {
864
+ if (docxSession) {
865
+ docxSession.initialSessionState = {
866
+ ...docxSession.initialSessionState,
867
+ canonicalDocument: resolvedDocument,
868
+ };
869
+ docxSession.initialSnapshot = {
870
+ ...docxSession.initialSnapshot,
871
+ canonicalDocument: resolvedDocument,
872
+ };
873
+ }
874
+ return (
875
+ internalBaseRuntime.hydrateCanonicalDocumentInternally?.(
876
+ resolvedDocument,
877
+ ) ?? false
878
+ );
879
+ },
805
880
  });
806
881
 
807
882
  return runtime;
@@ -947,8 +1022,21 @@ function createLoadingRuntimeBridge(input: {
947
1022
  },
948
1023
  getCanonicalDocument: () => input.sessionState.canonicalDocument,
949
1024
  getSourcePackage: () => input.sessionState.sourcePackage,
1025
+ getFontTable: () => input.sessionState.canonicalDocument.fontTable,
1026
+ getFontEntry: (name: string) =>
1027
+ input.sessionState.canonicalDocument.fontTable?.fonts[name],
950
1028
  replaceText: () => undefined,
951
1029
  insertFragment: () => undefined,
1030
+ copy: () => undefined,
1031
+ cut: () => undefined,
1032
+ getClipboardBuffer: () => null,
1033
+ getClipboardWireFormats: () => null,
1034
+ selectObject: () => undefined,
1035
+ deselectObject: () => undefined,
1036
+ getGrabbedObject: () => null,
1037
+ startAction: () => undefined,
1038
+ endAction: () => undefined,
1039
+ isInAction: () => false,
952
1040
  applyActiveStoryTextCommand: () => ({
953
1041
  kind: "rejected",
954
1042
  newRevisionToken: "",
@@ -1016,6 +1104,11 @@ function createLoadingRuntimeBridge(input: {
1016
1104
  changeKinds: ["content"],
1017
1105
  }),
1018
1106
  getFieldSnapshot: () => emptyFieldSnapshot,
1107
+ getFieldResolver: () => ({
1108
+ resolve: () => undefined,
1109
+ cacheKey: "loading",
1110
+ }),
1111
+ getFootnoteResolver: () => undefined,
1019
1112
  updateFields: () => emptyFieldResult,
1020
1113
  updateTableOfContents: () => emptyTocResult,
1021
1114
  getSessionState: () => {
@@ -1061,6 +1154,9 @@ function createLoadingRuntimeBridge(input: {
1061
1154
  definitions: [],
1062
1155
  entries: [],
1063
1156
  }),
1157
+ queryScopes: () => [],
1158
+ findScopesAt: () => [],
1159
+ findScopesIntersecting: () => [],
1064
1160
  setHostAnnotationOverlay: () => undefined,
1065
1161
  clearHostAnnotationOverlay: () => undefined,
1066
1162
  getHostAnnotationSnapshot: () => ({
@@ -154,7 +154,7 @@ export function EditorShellView(props: EditorShellViewProps) {
154
154
  aria-describedby={`${accessibilityInstructionsId} ${accessibilityStatusId}${
155
155
  diagnosticsModeMessage ? ` ${accessibilityAlertId}` : ""
156
156
  }`}
157
- className="relative h-full"
157
+ className="wre-editor relative h-full"
158
158
  onKeyDownCapture={onShellKeyDownCapture}
159
159
  >
160
160
  <p id={accessibilityInstructionsId} style={visuallyHiddenStyles}>
@@ -19,6 +19,14 @@ export interface ShellShortcutContext {
19
19
 
20
20
  export interface SurfaceShortcutContext {
21
21
  inTable: boolean;
22
+ /**
23
+ * I3 widening tail — when the caret is in the last cell of the last row of
24
+ * a table, Tab (without shift) triggers implicit row-insert-below instead
25
+ * of cell navigation. Matches Word's Tab-at-table-tail behavior. Flag is
26
+ * computed by the PM bridge at dispatch time (bridge knows cell position);
27
+ * defaults to false for callers that don't supply it.
28
+ */
29
+ isAtTableTail?: boolean;
22
30
  }
23
31
 
24
32
  export type ShellShortcutResolution =
@@ -46,6 +54,7 @@ export type SurfaceShortcutResolution =
46
54
  | { kind: "insert-tab" }
47
55
  | { kind: "outdent-tab" }
48
56
  | { kind: "navigate-table-cell"; direction: 1 | -1 }
57
+ | { kind: "table-insert-row-below" }
49
58
  | { kind: "history"; history: "undo" | "redo" };
50
59
 
51
60
  export function resolveShellShortcut(
@@ -190,9 +199,14 @@ export function resolveSurfaceShortcut(
190
199
  }
191
200
 
192
201
  if (key === "tab" && !hasAnyModifiers(input)) {
193
- return context.inTable
194
- ? { kind: "navigate-table-cell", direction: 1 }
195
- : { kind: "insert-tab" };
202
+ if (context.inTable) {
203
+ // I3 widening tail — Tab at the last cell inserts a new row below
204
+ // (Word behavior). Shift+Tab continues to navigate regardless.
205
+ return context.isAtTableTail
206
+ ? { kind: "table-insert-row-below" }
207
+ : { kind: "navigate-table-cell", direction: 1 };
208
+ }
209
+ return { kind: "insert-tab" };
196
210
  }
197
211
 
198
212
  return { kind: "none" };
@@ -24,7 +24,7 @@
24
24
  import React from "react";
25
25
  import type { ChartModel } from "../../io/ooxml/chart/types.ts";
26
26
  import type { ResolvedTheme } from "../../model/canonical-document.ts";
27
- import { layoutPlotArea, type PlotAreaLayout } from "./layout/plot-area.ts";
27
+ import { layoutPlotArea, CHROME_PAINTED_DEFAULT, type PlotAreaLayout } from "./layout/plot-area.ts";
28
28
  import { AreaChart } from "./render/area.tsx";
29
29
  import { BarColumnChart } from "./render/bar-column.tsx";
30
30
  import { BubbleChart } from "./render/bubble.tsx";
@@ -63,7 +63,15 @@ function ChartSurfaceImpl({
63
63
  resolveMediaUrl,
64
64
  previewMediaId,
65
65
  }: ChartSurfaceProps): React.ReactElement {
66
- const resolvedLayout = layout ?? layoutPlotArea({ w: width, h: height }, model, theme);
66
+ // Graceful degradation: pass CHROME_PAINTED_DEFAULT so title / legend /
67
+ // axis bands are NOT reserved by the layout pass. No component paints
68
+ // them today (documented gap in docs/wiki/images-and-media.md §"Known
69
+ // gaps"), so reserving space would leave empty margins around the plot.
70
+ // When Stage-C chrome wire-up lands, flip the matching flag here.
71
+ const resolvedLayout =
72
+ layout ?? layoutPlotArea({ w: width, h: height }, model, theme, {
73
+ paintedChrome: CHROME_PAINTED_DEFAULT,
74
+ });
67
75
  const defs = new DefsRegistry();
68
76
 
69
77
  const body = dispatchBody({
@@ -187,20 +195,38 @@ function renderDefs(defs: DefsRegistry): React.ReactElement[] {
187
195
 
188
196
  /**
189
197
  * Convert a gradient angle (degrees, OOXML clockwise-from-horizontal)
190
- * to SVG `x1/y1/x2/y2` percentage values. = left-to-right,
191
- * 90° = top-to-bottom.
198
+ * to SVG `x1/y1/x2/y2` percentage values on the object bounding box.
199
+ * 0° = left-to-right, 90° = top-to-bottom. Returned percentages are
200
+ * continuous in the angle rather than snapped to the four corners —
201
+ * non-cardinal gradients (45°, 60°, 120°, …) render with the correct
202
+ * direction instead of collapsing to the nearest corner-pair.
192
203
  */
193
204
  function angleToCoords(angleDeg: number): {
194
205
  x1: string; y1: string; x2: string; y2: string;
195
206
  } {
196
- const rad = (angleDeg * Math.PI) / 180;
207
+ const normalized = ((angleDeg % 360) + 360) % 360;
208
+ const rad = (normalized * Math.PI) / 180;
197
209
  const dx = Math.cos(rad);
198
210
  const dy = Math.sin(rad);
199
- const x1 = dx < 0 ? "100%" : "0%";
200
- const x2 = dx < 0 ? "0%" : "100%";
201
- const y1 = dy < 0 ? "100%" : "0%";
202
- const y2 = dy < 0 ? "0%" : "100%";
203
- return { x1, y1, x2, y2 };
211
+
212
+ // Center + half-length projection onto the [0,1]² bounding box. Scale
213
+ // so the dominant component reaches 0.5 (half the box side), keeping
214
+ // the gradient within the object bbox and edge-to-edge along the axis.
215
+ const cx = 0.5;
216
+ const cy = 0.5;
217
+ const absDx = Math.abs(dx);
218
+ const absDy = Math.abs(dy);
219
+ const scale = absDx === 0 && absDy === 0 ? 0 : 0.5 / Math.max(absDx, absDy);
220
+ const hx = dx * scale;
221
+ const hy = dy * scale;
222
+
223
+ const fmtPct = (n: number): string => `${(n * 100).toFixed(2)}%`;
224
+ return {
225
+ x1: fmtPct(cx - hx),
226
+ y1: fmtPct(cy - hy),
227
+ x2: fmtPct(cx + hx),
228
+ y2: fmtPct(cy + hy),
229
+ };
204
230
  }
205
231
 
206
232
  // ---------------------------------------------------------------------------