@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
package/README.md CHANGED
@@ -238,7 +238,7 @@ Engineering work is organized into 9 lanes (5 active now + 2 later polish + 2 fi
238
238
  | 6b | [**Shell & Workspace Chrome**](docs/plans/lane-6b-shell-workspace-chrome.md) | **~15%** | Gated on 6a.S1+S2 — shell header + toolbar + status + alert banner + unsaved modal + collab chrome restyle; mode-dock decommission; TwCommandPalette |
239
239
  | 6c | [**Context & Review Surfaces**](docs/plans/lane-6c-context-review-surfaces.md) | **~15%** | Gated on 6a.S1+S2 — selection toolbar + suggestion card + rail + scope + context toolbars + health panel restyle; TwCommentPreview / TwEmptyState / TwShortcutHint |
240
240
  | 6d | [**Visual Fidelity**](docs/plans/lane-6d-visual-fidelity.md) | **~20%** | Gated on 6a.S1+S2 + external-lane deps — L8 A/B/C shipped; L8 Phase D + P11 overlays + P7 + P12 + P14 next |
241
- | 7a | [**Visual Fidelity Register**](docs/plans/lane-7a-visual-fidelity-register.md) | **0%** | LATER (gated on 6a–6d) — V5 covers, V6 REF/PAGEREF, V7 cascade audit |
241
+ | 7a | [**Visual Fidelity Register**](docs/plans/lane-7a-visual-fidelity-register.md) | **✅ 100%** | CLOSED — V5 covers (`6f8869b7`), V6 REF/PAGEREF baseline via CO3 + contract pin (`99b66a1f`), V7 × CO1 5-combo cascade audit (`df315488`) |
242
242
  | 7b | [**Revision & Redline Completion**](docs/plans/lane-7b-revision-redline-completion.md) | **0%** | LATER (gated on 6a–6d) — X4.a/b structural table revisions, X5 ffData, move-pairing |
243
243
  | 7c | [**Preservation & Format Coverage**](docs/plans/lane-7c-preservation-format-coverage.md) | **0%** | LATER (gated on 6a–6d) — O6 Strict, OLE preserve-only, picture-SDT, w14/kern/VML defer |
244
244
  | 7d | [**Platform Hardening & Hygiene**](docs/plans/lane-7d-platform-hardening-hygiene.md) | **0%** | LATER (gated on 6a–6d) — harness-crash-hardening, fastload activation, worktree consolidation |
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@beyondwork/docx-react-component",
3
3
  "publisher": "beyondwork",
4
- "version": "1.0.56",
4
+ "version": "1.0.58",
5
5
  "description": "Embeddable React Word (docx) editor with review, comments, tracked changes, and round-trip OOXML fidelity.",
6
6
  "packageManager": "pnpm@10.30.3",
7
7
  "type": "module",
@@ -482,6 +482,12 @@ export interface SearchOptions {
482
482
  matchCase?: boolean;
483
483
  wholeWord?: boolean;
484
484
  limit?: number;
485
+ /** Phase C §C3 — treat `query` as a JS regex pattern (compiled with `u` flag). */
486
+ regex?: boolean;
487
+ /** Phase C §C3 — restrict results to positions inside this scopeId's marker range. */
488
+ inScope?: string;
489
+ /** Phase C §C3 — restrict results to a specific story target (default: main). */
490
+ inStory?: EditorStoryTarget;
485
491
  }
486
492
 
487
493
  export interface SearchResultSnapshot {
@@ -717,11 +723,17 @@ export type SnapshotRefreshChangeKind =
717
723
  | "structure"
718
724
  | "checkpoint";
719
725
 
726
+ export interface TocRefreshTrigger {
727
+ headingContentChanged: boolean;
728
+ headingStructureChanged: boolean;
729
+ }
730
+
720
731
  export interface SnapshotRefreshHints {
721
732
  invalidate: SnapshotRefreshInvalidateTarget[];
722
733
  staleTargets: SnapshotRefreshStaleTarget[];
723
734
  changeKinds: SnapshotRefreshChangeKind[];
724
735
  checkpointType?: "session" | "snapshot" | "export";
736
+ tocRefreshTrigger?: TocRefreshTrigger;
725
737
  }
726
738
 
727
739
  export interface StyleCatalogEntrySnapshot {
@@ -798,6 +810,51 @@ export type SurfaceTextMark =
798
810
  | "smallCaps"
799
811
  | "allCaps";
800
812
 
813
+ /**
814
+ * V2c.4 / V2c.5 — DrawingFrame anchor geometry projected onto image + shape
815
+ * segments. Mirrors the canonical `AnchorGeometry` shape (defined in
816
+ * `src/model/canonical-document.ts`) with the fields chrome consumers need
817
+ * for float-wrap (Lane 6d N9), object-selection chrome (N6), and frame
818
+ * positioning. EMU values are kept verbatim — converters that need px
819
+ * apply 9525 EMU = 1 px at 96 dpi.
820
+ */
821
+ export interface SurfaceDrawingAnchor {
822
+ display: "inline" | "floating";
823
+ wrapMode: "none" | "square" | "tight" | "through" | "topAndBottom";
824
+ extent: { widthEmu: number; heightEmu: number };
825
+ positionH?: { relativeFrom: string; align?: string; offset?: number };
826
+ positionV?: { relativeFrom: string; align?: string; offset?: number };
827
+ distMargins?: { top?: number; bottom?: number; left?: number; right?: number };
828
+ relativeHeight?: number;
829
+ behindDoc?: boolean;
830
+ layoutInCell?: boolean;
831
+ allowOverlap?: boolean;
832
+ simplePos?: boolean;
833
+ /** docPr.id / .name / .descr — used for accessibility chrome and selection labels. */
834
+ docPr?: { id: string; name?: string; descr?: string };
835
+ }
836
+
837
+ /**
838
+ * V2c.4 — Picture-effect data (crop, rotation, flip, preset clip geometry).
839
+ * `srcRect` percentages match OOXML's `a:srcRect` semantics: 0 = no crop,
840
+ * 100 000 = fully cropped from that edge. `rotation` is in 60 000ths of a
841
+ * degree (OOXML `a:xfrm a:rot`).
842
+ */
843
+ export interface SurfacePictureEffects {
844
+ srcRect?: { top: number; bottom: number; left: number; right: number };
845
+ rotation?: number;
846
+ flipH?: boolean;
847
+ flipV?: boolean;
848
+ presetGeom?: string;
849
+ stretch?: boolean;
850
+ /** N11.b — a:softEdge feather radius in EMU. Maps to CSS `filter: blur(R)`. */
851
+ softEdgeRadius?: number;
852
+ /** N11.b — a:outerShdw drop shadow. `blurRad`/`dist` in EMU; `dir` in 60000ths of a degree. */
853
+ outerShadow?: { blurRad: number; dist: number; dir: number; color: string; colorType: "srgbClr" | "schemeClr" };
854
+ /** N11.b — a:glow ambient glow. `radius` in EMU. */
855
+ glow?: { radius: number; color: string; colorType: "srgbClr" | "schemeClr" };
856
+ }
857
+
801
858
  export type SurfaceInlineSegment =
802
859
  | {
803
860
  segmentId: string;
@@ -836,6 +893,19 @@ export type SurfaceInlineSegment =
836
893
  state: "editable" | "missing";
837
894
  display?: "inline" | "floating";
838
895
  detail?: string;
896
+ /**
897
+ * V2c.4 — DrawingFrame anchor geometry surfaced for Lane 6d float-wrap
898
+ * (P7) and chrome consumers. Present only when the canonical model
899
+ * carries non-trivial anchor metadata; inline pictures with default
900
+ * positioning omit it so the simple-image path stays cheap.
901
+ */
902
+ anchor?: SurfaceDrawingAnchor;
903
+ /**
904
+ * V2c.4 — Picture-effect data (crop, rotation, flip, preset geometry).
905
+ * Absent when none of the underlying `PictureContent` effect fields
906
+ * are set, so consumers can fast-path image rendering when undefined.
907
+ */
908
+ pictureEffects?: SurfacePictureEffects;
839
909
  }
840
910
  | {
841
911
  segmentId: string;
@@ -886,6 +956,44 @@ export type SurfaceInlineSegment =
886
956
  instruction: string;
887
957
  refreshStatus: FieldRefreshStatus;
888
958
  label: string;
959
+ }
960
+ | {
961
+ /**
962
+ * V2c.5 — DrawingFrame shape segment. Replaces the `complex_preview`
963
+ * fallback that used to swallow `wps:wsp` shapes. Carries the
964
+ * geometry preset, fill/line, optional textbox flag + first-paragraph
965
+ * preview, and (for floating shapes) anchor geometry. Lane 6d's N10
966
+ * shape rendering consumes this segment.
967
+ */
968
+ segmentId: string;
969
+ kind: "shape";
970
+ from: number;
971
+ to: number;
972
+ label: string;
973
+ detail: string;
974
+ anchor?: SurfaceDrawingAnchor;
975
+ geometry?: string;
976
+ fill?:
977
+ | { kind: "solid"; color: string; colorType: "srgbClr" | "schemeClr" }
978
+ | { kind: "none" }
979
+ | {
980
+ kind: "gradient";
981
+ stops: Array<{ pos: number; color: string; colorType: "srgbClr" | "schemeClr" }>;
982
+ direction:
983
+ | { kind: "linear"; angle: number; scaled?: boolean }
984
+ | { kind: "path"; path: "circle" | "rect" | "shape" };
985
+ rotWithShape?: boolean;
986
+ }
987
+ | {
988
+ kind: "pattern";
989
+ preset: string;
990
+ fg?: { color: string; colorType: "srgbClr" | "schemeClr" };
991
+ bg?: { color: string; colorType: "srgbClr" | "schemeClr" };
992
+ };
993
+ line?: { color?: string; widthEmu?: number; noLine?: boolean };
994
+ isTextBox?: boolean;
995
+ /** First-paragraph plain-text preview when `isTextBox` is true. */
996
+ txbxText?: string;
889
997
  };
890
998
 
891
999
  export interface SurfaceTableCellSnapshot {
@@ -1716,6 +1824,8 @@ export interface RuntimeRenderSnapshot {
1716
1824
  commandState: CommandStateSnapshot;
1717
1825
  surface?: EditorSurfaceSnapshot;
1718
1826
  protectionSnapshot: ProtectionSnapshot;
1827
+ /** R.3 — stable id of the currently grabbed image/shape, or null. Populated from grab state so chrome overlays re-render on selectObject/deselectObject. */
1828
+ grabbedObjectId?: string | null;
1719
1829
  }
1720
1830
 
1721
1831
  export interface EditorSessionState {
@@ -1946,6 +2056,67 @@ export interface WorkflowMetadataSnapshot {
1946
2056
  entries: WorkflowMetadataEntry[];
1947
2057
  }
1948
2058
 
2059
+ // ---------------------------------------------------------------------------
2060
+ // Phase C — host-side scope query surface (§C1, §C2)
2061
+ // ---------------------------------------------------------------------------
2062
+
2063
+ /**
2064
+ * §C1 — Filter passed to `queryScopes` / `findScopesAt` /
2065
+ * `findScopesIntersecting`. All fields are optional and AND'd together.
2066
+ * Snapshot-based; never triggers runtime mutation.
2067
+ */
2068
+ export interface ScopeQueryFilter {
2069
+ /** Match only scopes attached to any of these work items. */
2070
+ workItemIds?: string[];
2071
+ /** Match only scopes in one of these modes. */
2072
+ modes?: WorkflowScopeMode[];
2073
+ /** Match only scopes whose `domain` is one of these. */
2074
+ domains?: Array<NonNullable<WorkflowScope["domain"]>>;
2075
+ /**
2076
+ * Match scopes that carry at least one entry with this `metadataId`.
2077
+ * Uses the runtime `WorkflowMetadataSnapshot` joined by `entry.scopeId`.
2078
+ */
2079
+ metadataId?: string;
2080
+ /**
2081
+ * Match scopes that carry at least one entry whose `value` passes the
2082
+ * predicate. Entries with no `value` are skipped.
2083
+ */
2084
+ hasValue?: (value: Record<string, unknown>, entry: WorkflowMetadataEntry) => boolean;
2085
+ /** Label prefix (case-insensitive). */
2086
+ labelPrefix?: string;
2087
+ /**
2088
+ * Story target filter. Defaults to `{ kind: "main" }` when omitted. Pass
2089
+ * `"*"` for any story.
2090
+ */
2091
+ storyTarget?: EditorStoryTarget | "*";
2092
+ /** Max result count. Undefined = no cap. */
2093
+ limit?: number;
2094
+ /**
2095
+ * §C8 — include scopes with `visibility: "hidden"`. Default: false.
2096
+ * Accepted but has no effect until §C8 adds the `visibility` field.
2097
+ */
2098
+ includeHidden?: boolean;
2099
+ /**
2100
+ * §C8 — include scopes with `visibility: "invisible"`. Default: false.
2101
+ * Accepted but has no effect until §C8 adds the `visibility` field.
2102
+ */
2103
+ includeInvisible?: boolean;
2104
+ }
2105
+
2106
+ /**
2107
+ * §C1 — One result from `queryScopes`. Joins the scope record with the
2108
+ * metadata entries that point at it and the resolved work item (when
2109
+ * `scope.workItemId` matches one in the overlay).
2110
+ */
2111
+ export interface ScopeQueryResult {
2112
+ /** Scope record from the overlay. */
2113
+ scope: WorkflowScope;
2114
+ /** Metadata entries whose `entry.scopeId === scope.scopeId`. */
2115
+ entries: WorkflowMetadataEntry[];
2116
+ /** Resolved work item when `scope.workItemId` is set and present. */
2117
+ workItem: WorkflowWorkItem | null;
2118
+ }
2119
+
1949
2120
  // ---------------------------------------------------------------------------
1950
2121
  // R2 — issue metadata (scope-card-overlay P1)
1951
2122
  // ---------------------------------------------------------------------------
@@ -2563,6 +2734,12 @@ export type WordReviewEditorEvent =
2563
2734
  documentId: string;
2564
2735
  activeStory: EditorStoryTarget;
2565
2736
  }
2737
+ | {
2738
+ type: "toc_auto_refreshed";
2739
+ documentId: string;
2740
+ entryCount: number;
2741
+ trigger: TocRefreshTrigger;
2742
+ }
2566
2743
  | {
2567
2744
  type: "warning_added";
2568
2745
  documentId: string;
@@ -3060,6 +3237,61 @@ export interface WordReviewEditorRef {
3060
3237
  * add merge-intent + richer caret placement as paste parsers come online.
3061
3238
  */
3062
3239
  insertFragment(fragment: CanonicalDocumentFragment, target?: EditorAnchorProjection): void;
3240
+ /**
3241
+ * I2 Tier B Slice 4b — serialize `target` (or the current selection) to a
3242
+ * `CanonicalDocumentFragment` and store it in the editor's internal
3243
+ * clipboard buffer. No document mutation.
3244
+ */
3245
+ copy(target?: EditorAnchorProjection): void;
3246
+ /**
3247
+ * I2 Tier B Slice 4b — `copy(target)` + delete the range.
3248
+ */
3249
+ cut(target?: EditorAnchorProjection): void;
3250
+ /**
3251
+ * I2 Tier B Slice 4b — return the last fragment written by `cut` / `copy`,
3252
+ * or `null` if none has been captured yet. Hosts pair this with
3253
+ * `insertFragment` to implement paste while Slice 4b's async-Clipboard-API
3254
+ * write lands with Slice 5.
3255
+ */
3256
+ getClipboardBuffer(): CanonicalDocumentFragment | null;
3257
+ /**
3258
+ * v5 close-out — return the current clipboard buffer serialized to the
3259
+ * wire formats browsers/Word accept, or `null` when no clipboard op has
3260
+ * been performed. Hosts use this inside their own DOM `copy`/`cut` event
3261
+ * handler to feed `navigator.clipboard.write` (the editor does not
3262
+ * install the DOM handler itself).
3263
+ */
3264
+ getClipboardWireFormats(): { wordml: string; html: string; plainText: string } | null;
3265
+ /**
3266
+ * R.3 ObjectGrabLayer — grab an inline / floating object (image, shape)
3267
+ * by its stable id. Single-select; replaces any previously grabbed
3268
+ * object. Lane 6 P11 chrome reads the grab state to paint handles.
3269
+ */
3270
+ selectObject(objectId: string): void;
3271
+ /**
3272
+ * R.3 — release the grabbed object, if any. Safe to call when nothing
3273
+ * is grabbed.
3274
+ */
3275
+ deselectObject(): void;
3276
+ /**
3277
+ * R.3 — return the grabbed object's id, or `null` when nothing is
3278
+ * grabbed.
3279
+ */
3280
+ getGrabbedObject(): string | null;
3281
+ /**
3282
+ * R.5.a — open an action bracket. Hosts wrap compound edits (paste,
3283
+ * cut+paste, agent suggestion-apply) so snapshot emission + collab
3284
+ * broadcast + undo grouping see them as a single action.
3285
+ *
3286
+ * Phase 1: opt-in. Existing commands do not auto-bracket. Hosts that
3287
+ * want single-undo paste call `startAction("paste")` →
3288
+ * `insertFragment(fragment)` → `endAction()`.
3289
+ */
3290
+ startAction(name: string): void;
3291
+ /** R.5.a — close one level of action bracketing. Unbalanced calls are no-ops. */
3292
+ endAction(): void;
3293
+ /** R.5.a — `true` when the runtime is inside one or more action brackets. */
3294
+ isInAction(): boolean;
3063
3295
  toggleBulletedList(): void;
3064
3296
  toggleNumberedList(): void;
3065
3297
  toggleBold(): void;
@@ -3169,6 +3401,86 @@ export interface WordReviewEditorRef {
3169
3401
  setWorkflowMetadataEntries(entries: WorkflowMetadataEntry[]): void;
3170
3402
  clearWorkflowMetadataEntries(): void;
3171
3403
  getWorkflowMetadataSnapshot(): WorkflowMetadataSnapshot;
3404
+ /**
3405
+ * Phase C §C1 — filter + project the current workflow overlay into a
3406
+ * scope-joined view. Reads a snapshot of the current overlay + metadata
3407
+ * entries + work items; no mutation. Results are sorted by start-marker
3408
+ * document position, tie-broken by `scopeId` ascending.
3409
+ *
3410
+ * Defaults: `storyTarget` = `{ kind: "main" }` (pass `"*"` for any
3411
+ * story); `includeHidden` / `includeInvisible` default to `false`
3412
+ * (§C8 additive — accepted today but no-ops until `visibility` lands).
3413
+ *
3414
+ * Returns `[]` when no workflow overlay has been set.
3415
+ */
3416
+ queryScopes(filter?: ScopeQueryFilter): ScopeQueryResult[];
3417
+ /**
3418
+ * Phase C §C2 — every scope whose marker range contains `position.from`
3419
+ * (end-inclusive), ordered outermost → innermost. Pass a zero-length
3420
+ * range (`from === to`) for a point query. Returns the full
3421
+ * `ScopeQueryResult` (scope + entries + workItem) so results compose
3422
+ * directly with `setSelection`, `addScope`, `getLocationForAnchor`.
3423
+ *
3424
+ * Companion to `getInteractionGuardSnapshot().matchedScopeId` — that
3425
+ * API returns the innermost-only match; this one returns the whole
3426
+ * enclosing stack.
3427
+ */
3428
+ findScopesAt(
3429
+ position: EditorAnchorProjection,
3430
+ options?: { includeHidden?: boolean; includeInvisible?: boolean },
3431
+ ): ScopeQueryResult[];
3432
+ /**
3433
+ * Phase C §C2 — every scope whose marker range intersects `range`
3434
+ * (accepts a range anchor; non-range anchors yield `[]`). Default
3435
+ * `mode: "overlap"` matches any intersection including touching
3436
+ * endpoints; `mode: "contain"` requires the scope's entire range to
3437
+ * sit within `range`. Deterministic order by start-marker position.
3438
+ */
3439
+ findScopesIntersecting(
3440
+ range: EditorAnchorProjection,
3441
+ options?: {
3442
+ includeHidden?: boolean;
3443
+ includeInvisible?: boolean;
3444
+ mode?: "overlap" | "contain";
3445
+ },
3446
+ ): ScopeQueryResult[];
3447
+ /**
3448
+ * Phase C §C3 — find the first text match in the document.
3449
+ * Supports `regex`, `inScope`, `inStory` options.
3450
+ * Throws `EditorApiError({ code: "search_invalid_regex" })` synchronously
3451
+ * when `options.regex === true` and `query` is not a valid regex pattern.
3452
+ * Returns `null` when no match is found.
3453
+ */
3454
+ findFirstText(
3455
+ query: string,
3456
+ options?: SearchOptions,
3457
+ ): EditorAnchorProjection | null;
3458
+ /**
3459
+ * Phase C §C3 — find all text matches in the document.
3460
+ * Same options and error contract as `findFirstText`.
3461
+ * Returns `[]` when no matches are found.
3462
+ */
3463
+ findAllText(
3464
+ query: string,
3465
+ options?: SearchOptions,
3466
+ ): EditorAnchorProjection[];
3467
+ /**
3468
+ * Phase C §C3 — find first text match and select it. Returns `true` if a
3469
+ * match was found (and selection updated), `false` otherwise.
3470
+ */
3471
+ selectFirstText(
3472
+ query: string,
3473
+ options?: SearchOptions,
3474
+ ): boolean;
3475
+ /**
3476
+ * Phase C §C3 — select first text match and return the total match count.
3477
+ * Returns `0` when no matches are found. Multi-range selection deferred
3478
+ * to Lane 1 shipping `SelectionSnapshot.multi`.
3479
+ */
3480
+ selectAllText(
3481
+ query: string,
3482
+ options?: SearchOptions,
3483
+ ): number;
3172
3484
  /**
3173
3485
  * Schema 1.1 — set the overlay default for metadata persistence.
3174
3486
  * Author-only per collab-master-plan §7 role-gating matrix;
@@ -3706,6 +4018,24 @@ export interface ResolveMetadataConflictInput {
3706
4018
  }
3707
4019
 
3708
4020
  // ---------------------------------------------------------------------------
4021
+ // Phase C §C3 — generic editor API error
4022
+ // ---------------------------------------------------------------------------
4023
+
4024
+ /**
4025
+ * Thrown synchronously by query methods when the caller passes an invalid
4026
+ * argument. The `code` field identifies the specific failure:
4027
+ * - `"search_invalid_regex"` — `options.regex === true` and `query` is not
4028
+ * a valid JavaScript regular expression pattern.
4029
+ */
4030
+ export class EditorApiError extends Error {
4031
+ readonly code: string;
4032
+ constructor(params: { code: string; message?: string }) {
4033
+ super(params.message ?? `EditorApiError: ${params.code}`);
4034
+ this.name = "EditorApiError";
4035
+ this.code = params.code;
4036
+ }
4037
+ }
4038
+
3709
4039
  // Schema 1.1 — metadata-persistence errors (P17)
3710
4040
  // ---------------------------------------------------------------------------
3711
4041
 
@@ -522,6 +522,7 @@ function getInlineLength(node: InlineNode): number {
522
522
  case "shape":
523
523
  case "wordart":
524
524
  case "vml_shape":
525
+ case "drawing_frame":
525
526
  return 1;
526
527
  }
527
528
  }
@@ -581,6 +582,8 @@ function getInlineDisplayText(node: InlineNode): string {
581
582
  return node.text;
582
583
  case "vml_shape":
583
584
  return node.text ?? "[VML Shape]";
585
+ case "drawing_frame":
586
+ return node.content.type === "picture" ? "[Image]" : "[Drawing]";
584
587
  }
585
588
  }
586
589
 
@@ -1040,6 +1040,7 @@ function inlineNodeLength(node: InlineNode): number {
1040
1040
  case "shape":
1041
1041
  case "wordart":
1042
1042
  case "vml_shape":
1043
+ case "drawing_frame":
1043
1044
  return 1;
1044
1045
  case "field":
1045
1046
  return node.children.reduce<number>(
@@ -89,7 +89,8 @@ import {
89
89
  setHeaderFooterLinkAtSectionIndex,
90
90
  } from "./section-layout-commands.ts";
91
91
  import { insertPageBreak, insertTable } from "./text-commands.ts";
92
- import { applyFragmentInsert } from "../../runtime/structure-ops/fragment-insert.ts";
92
+ import { editLayer } from "../../runtime/edit-ops/index.ts";
93
+ import { structureLayer } from "../../runtime/structure-ops/index.ts";
93
94
  import type { TableSelectionDescriptor } from "../../runtime/table-commands.ts";
94
95
 
95
96
  export type ContentChildrenPatch =
@@ -575,8 +576,10 @@ export function executeEditorCommand(
575
576
  ? applySuggestingInsert(state, command.text, context)
576
577
  : undefined;
577
578
  if (suggestingResult) return suggestingResult;
579
+ // R.2 — dispatch via the named EditLayer entry point so the seam is the
580
+ // single site where R.5.a/b hooks will attach.
578
581
  return applyTextCommand(state, context.timestamp, (document, selection) =>
579
- insertText(document, selection, command.text, context, command.formatting),
582
+ editLayer.applyTextInsert(document, selection, command.text, context, command.formatting),
580
583
  );
581
584
  }
582
585
  case "text.delete-backward": {
@@ -585,7 +588,7 @@ export function executeEditorCommand(
585
588
  : undefined;
586
589
  if (suggestingResult) return suggestingResult;
587
590
  return applyTextCommand(state, context.timestamp, (document, selection) =>
588
- deleteSelectionOrBackward(document, selection, context),
591
+ editLayer.applyDeleteBackward(document, selection, context),
589
592
  );
590
593
  }
591
594
  case "text.delete-forward": {
@@ -594,7 +597,7 @@ export function executeEditorCommand(
594
597
  : undefined;
595
598
  if (suggestingResult) return suggestingResult;
596
599
  return applyTextCommand(state, context.timestamp, (document, selection) =>
597
- deleteSelectionOrForward(document, selection, context),
600
+ editLayer.applyDeleteForward(document, selection, context),
598
601
  );
599
602
  }
600
603
  case "text.insert-tab": {
@@ -603,7 +606,7 @@ export function executeEditorCommand(
603
606
  : undefined;
604
607
  if (suggestingResult) return suggestingResult;
605
608
  return applyTextCommand(state, context.timestamp, (document, selection) =>
606
- insertTab(document, selection, context),
609
+ editLayer.applyInsertTab(document, selection, context),
607
610
  );
608
611
  }
609
612
  case "text.outdent-tab": {
@@ -632,7 +635,7 @@ export function executeEditorCommand(
632
635
  : undefined;
633
636
  if (suggestingResult) return suggestingResult;
634
637
  return applyTextCommand(state, context.timestamp, (document, selection) =>
635
- insertHardBreak(document, selection, context),
638
+ editLayer.applyInsertHardBreak(document, selection, context),
636
639
  );
637
640
  }
638
641
  case "paragraph.split":
@@ -641,15 +644,18 @@ export function executeEditorCommand(
641
644
  if (suggestingResult) return suggestingResult;
642
645
  }
643
646
  return applyTextCommand(state, context.timestamp, (document, selection) =>
644
- splitParagraph(document, selection, context),
647
+ editLayer.applySplitParagraph(document, selection, context),
645
648
  );
646
649
  case "fragment.insert": {
647
- // I2 Tier B Slice 1 — route through the structure-ops splicer. No
650
+ // I2 Tier B Slice 1 + R.3 — route through the StructureLayer seam. No
648
651
  // suggesting-mode branch yet; fragment insertion always lands as a direct
649
652
  // edit. Future slices will gate behind track-changes when a fixture needs it.
650
- const result = applyFragmentInsert(state.document, state.selection, command.fragment, {
651
- timestamp: context.timestamp,
652
- });
653
+ const result = structureLayer.applyFragmentInsert(
654
+ state.document,
655
+ state.selection,
656
+ command.fragment,
657
+ { timestamp: context.timestamp },
658
+ );
653
659
  return buildDocumentReplaceTransaction(state, context, result);
654
660
  }
655
661
  case "runtime.set-read-only":
@@ -31,7 +31,24 @@ export interface DetachedAnchor {
31
31
  reason: "deleted" | "invalidatedByStructureChange" | "importAmbiguity";
32
32
  }
33
33
 
34
- export type EditorAnchorProjection = RangeAnchor | NodeAnchor | DetachedAnchor;
34
+ /**
35
+ * Internal representation of an anchor projection — uses `DocRange` so
36
+ * mapping helpers compose ranges independently of assoc semantics. The
37
+ * public-facing shape lives at `src/api/public-types.ts` (same simple name
38
+ * `EditorAnchorProjection`) and is flat (`{ kind: "range", from, to, assoc }`).
39
+ * Conversion between the two goes through `src/core/selection/anchor-conversion.ts`.
40
+ */
41
+ export type InternalEditorAnchorProjection = RangeAnchor | NodeAnchor | DetachedAnchor;
42
+
43
+ /**
44
+ * @deprecated X3 Phase 5 — prefer `InternalEditorAnchorProjection` to avoid
45
+ * name-collision with the public `EditorAnchorProjection` from
46
+ * `src/api/public-types.ts`. Internal call-sites have migrated (via
47
+ * `import type { EditorAnchorProjection as InternalEditorAnchorProjection }`
48
+ * aliasing in `document-runtime.ts`). This alias stays for one release cycle
49
+ * so third-party internal consumers don't break. Remove in v2.1+.
50
+ */
51
+ export type EditorAnchorProjection = InternalEditorAnchorProjection;
35
52
 
36
53
  export interface MappingStep {
37
54
  from: Position;
@@ -104,17 +104,22 @@ export function rangeStaysWithinSingleParagraph(
104
104
  }
105
105
 
106
106
  /**
107
- * I8 "mid-run-near-table" guard. Comment anchors whose endpoints
108
- * land strictly inside a paragraph that sits adjacent to a table
109
- * block are rejected: the serializer's per-paragraph offset walker
110
- * (Lane 3 §O8) produces invalid OOXML for these anchors. Removed
111
- * once O8 ships.
107
+ * Width of the paragraph-to-table proximity window used by
108
+ * `snapCommentAnchorAwayFromTable` to decide when to nudge endpoints to
109
+ * paragraph boundaries. Originally the I8 rejection threshold before Lane 3b
110
+ * §O8 shipped; now used only by the (opt-in) snap helper for hosts that
111
+ * prefer clean anchors.
112
112
  */
113
113
  export const TABLE_ADJACENT_WINDOW = 1;
114
114
 
115
- export type CommentAnchorRejectionReason =
116
- | "invalid_comment_anchor"
117
- | "comment_anchor_table_adjacent";
115
+ /**
116
+ * I8 guard removal (post-O8) — the `comment_anchor_table_adjacent` rejection
117
+ * reason has been retired now that Lane 3b §O8 fixed the serializer's
118
+ * per-paragraph offset walker. The type alias remains as a deprecated union
119
+ * stub so existing host code that narrows on it keeps compiling; new code
120
+ * should simply check against `"invalid_comment_anchor"`.
121
+ */
122
+ export type CommentAnchorRejectionReason = "invalid_comment_anchor";
118
123
 
119
124
  export function canCreateDocxCommentAnchor(
120
125
  content: unknown,
@@ -140,17 +145,18 @@ export function commentAnchorRejectionReason(
140
145
  return "invalid_comment_anchor";
141
146
  }
142
147
 
143
- if (rangeLandsMidRunNearTableBoundary(content, normalized)) {
144
- return "comment_anchor_table_adjacent";
145
- }
146
-
148
+ // I8 branch removed — mid-run-near-table anchors now serialize cleanly
149
+ // via Lane 3b §O8's `walkInlineNodeForBoundaries` fix.
147
150
  return null;
148
151
  }
149
152
 
150
153
  /**
151
- * I8.3 — Snap a rejected mid-run-near-table anchor to paragraph
152
- * boundaries so downstream serialization stays safe. Returns `null`
153
- * if the anchor cannot be rescued (e.g. crosses an opaque block).
154
+ * Snap a mid-run-near-table anchor to paragraph boundaries when a host opts
155
+ * into `snapToSafeBoundary`. Originally a defensive workaround for the pre-O8
156
+ * serializer bug; now a boundary-preference convenience for hosts that dislike
157
+ * mid-run comment anchors on principle. Returns `null` if the anchor cannot
158
+ * be rescued (e.g. crosses an opaque block); returns the input unchanged if
159
+ * it doesn't need snapping.
154
160
  */
155
161
  export function snapCommentAnchorAwayFromTable(
156
162
  content: unknown,
@@ -161,9 +167,14 @@ export function snapCommentAnchorAwayFromTable(
161
167
  const normalized = normalizeRange(anchor.range);
162
168
  if (normalized.from === normalized.to) return null;
163
169
 
164
- const reason = commentAnchorRejectionReason(content, anchor);
165
- if (reason === null) return anchor;
166
- if (reason !== "comment_anchor_table_adjacent") return null;
170
+ // If the anchor is already flagged invalid for other reasons (crosses an
171
+ // opaque block, etc.) we can't rescue it by snapping.
172
+ if (commentAnchorRejectionReason(content, anchor) !== null) return null;
173
+
174
+ // Detect mid-run-near-table via structural check directly (independent of
175
+ // rejection reason — the rejection branch is gone post-O8). If the anchor
176
+ // doesn't need snapping, pass it through unchanged.
177
+ if (!rangeLandsMidRunNearTableBoundary(content, normalized)) return anchor;
167
178
 
168
179
  const surfaceBlocks = readSurfaceBlocks(content);
169
180
  if (!surfaceBlocks) return null;