@beyondwork/docx-react-component 1.0.67 → 1.0.70

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 (384) hide show
  1. package/README.md +75 -932
  2. package/package.json +26 -27
  3. package/src/api/anchor-conversion.ts +43 -0
  4. package/src/api/editor-state-types.ts +2 -1
  5. package/src/api/public-types.ts +504 -101
  6. package/src/api/session-state.ts +4 -0
  7. package/src/api/v3/README.md +91 -0
  8. package/src/api/v3/_create.ts +146 -0
  9. package/src/api/v3/_layer-metadata.ts +362 -0
  10. package/src/api/v3/_mocks.ts +84 -0
  11. package/src/api/v3/_runtime-handle.ts +162 -0
  12. package/src/api/v3/_ux-response.ts +73 -0
  13. package/src/api/v3/ai/_metadata-audit.ts +225 -0
  14. package/src/api/v3/ai/attach.ts +235 -0
  15. package/src/api/v3/ai/bundle.ts +132 -0
  16. package/src/api/v3/ai/explain.ts +144 -0
  17. package/src/api/v3/ai/export.ts +54 -0
  18. package/src/api/v3/ai/inspect.ts +118 -0
  19. package/src/api/v3/ai/policy.ts +77 -0
  20. package/src/api/v3/ai/replacement.ts +341 -0
  21. package/src/api/v3/ai/resolve.ts +133 -0
  22. package/src/api/v3/index.ts +79 -0
  23. package/src/api/v3/runtime/chart.ts +310 -0
  24. package/src/api/v3/runtime/clipboard.ts +81 -0
  25. package/src/api/v3/runtime/collab.ts +331 -0
  26. package/src/api/v3/runtime/content.ts +236 -0
  27. package/src/api/v3/runtime/document.ts +282 -0
  28. package/src/api/v3/runtime/formatting.ts +186 -0
  29. package/src/api/v3/runtime/geometry.ts +349 -0
  30. package/src/api/v3/runtime/layout.ts +108 -0
  31. package/src/api/v3/runtime/review.ts +129 -0
  32. package/src/api/v3/runtime/search.ts +74 -0
  33. package/src/api/v3/runtime/table.ts +63 -0
  34. package/src/api/v3/runtime/workflow.ts +434 -0
  35. package/src/api/v3/ui/_context.ts +86 -0
  36. package/src/api/v3/ui/_create.ts +65 -0
  37. package/src/api/v3/ui/_types.ts +520 -0
  38. package/src/api/v3/ui/chrome-composition.ts +342 -0
  39. package/src/{ui-tailwind/chrome → api/v3/ui}/chrome-preset-model.ts +11 -1
  40. package/src/api/v3/ui/chrome.ts +476 -0
  41. package/src/api/v3/ui/debug.ts +124 -0
  42. package/src/api/v3/ui/index.ts +64 -0
  43. package/src/api/v3/ui/overlays-visibility.ts +170 -0
  44. package/src/api/v3/ui/overlays.ts +427 -0
  45. package/src/api/v3/ui/scope.ts +71 -0
  46. package/src/api/v3/ui/session.ts +100 -0
  47. package/src/api/v3/ui/surface.ts +170 -0
  48. package/src/api/v3/ui/viewport.ts +303 -0
  49. package/src/core/commands/index.ts +28 -6
  50. package/src/core/commands/list-commands.ts +3 -2
  51. package/src/core/commands/section-layout-commands.ts +9 -8
  52. package/src/core/schema/text-schema.ts +16 -0
  53. package/src/core/selection/mapping.ts +33 -72
  54. package/src/core/state/editor-state.ts +96 -189
  55. package/src/index.ts +23 -4
  56. package/src/io/chart-preview-resolver.ts +1 -1
  57. package/src/io/docx-session.ts +36 -4797
  58. package/src/io/export/build-app-properties-xml.ts +1 -1
  59. package/src/io/export/serialize-comments.ts +1 -1
  60. package/src/io/export/serialize-headers-footers.ts +6 -1
  61. package/src/io/export/serialize-main-document.ts +45 -0
  62. package/src/io/export/serialize-run-formatting.ts +17 -2
  63. package/src/io/export/twip.ts +1 -1
  64. package/src/io/normalize/normalize-text.ts +27 -20
  65. package/src/io/ooxml/chart/parse-series.ts +1 -1
  66. package/src/io/ooxml/chart/resolve-color.ts +2 -2
  67. package/src/io/ooxml/chart/types.ts +1 -1
  68. package/src/io/ooxml/classify-embedding.ts +83 -33
  69. package/src/io/ooxml/parse-fill.ts +1 -1
  70. package/src/io/ooxml/parse-main-document.ts +71 -1
  71. package/src/io/ooxml/parse-object.ts +14 -10
  72. package/src/io/ooxml/parse-run-formatting.ts +47 -1
  73. package/src/io/ooxml/property-grab-bag.ts +2 -2
  74. package/src/io/ooxml/units.ts +11 -0
  75. package/src/io/ooxml/workflow-payload.ts +282 -7
  76. package/src/model/anchor.ts +85 -0
  77. package/src/model/canonical-document.ts +351 -15
  78. package/src/model/chart-types.ts +1 -1
  79. package/src/model/layout/index.ts +83 -0
  80. package/src/model/layout/page-graph-types.ts +181 -0
  81. package/src/model/layout/page-layout-snapshot.ts +105 -0
  82. package/src/model/layout/resolved-layout-types.ts +47 -0
  83. package/src/model/layout/runtime-page-graph-types.ts +102 -0
  84. package/src/model/paragraph-scope-ids.ts +72 -0
  85. package/src/model/review/comment-types.ts +112 -0
  86. package/src/model/review/index.ts +2 -0
  87. package/src/model/review/revision-types.ts +215 -0
  88. package/src/model/snapshot.ts +32 -0
  89. package/src/review/store/comment-store.ts +21 -47
  90. package/src/review/store/revision-types.ts +40 -198
  91. package/src/runtime/collab/base-doc-fingerprint.ts +6 -1
  92. package/src/runtime/collab/runtime-collab-sync.ts +13 -3
  93. package/src/runtime/collab-session.ts +1 -1
  94. package/src/runtime/debug/build-debug-inspector-snapshot.ts +686 -0
  95. package/src/runtime/debug/event-ring-buffer.ts +64 -0
  96. package/src/runtime/debug/probability-sampler.ts +18 -0
  97. package/src/runtime/debug/runtime-debug-facet.ts +67 -0
  98. package/src/runtime/debug/stage-tokens.ts +31 -0
  99. package/src/runtime/debug/telemetry-bus.ts +271 -0
  100. package/src/runtime/debug/types.ts +275 -0
  101. package/src/runtime/debug/wrap-ref-for-telemetry.ts +118 -0
  102. package/src/runtime/document-layout.ts +8 -6
  103. package/src/runtime/document-runtime.ts +843 -1141
  104. package/src/runtime/document-search.ts +1 -1
  105. package/src/runtime/edit-ops/index.ts +1 -1
  106. package/src/runtime/external-send-runtime.ts +1 -1
  107. package/src/runtime/formatting/document-lookup.ts +235 -0
  108. package/src/runtime/formatting/field/registry.ts +41 -0
  109. package/src/runtime/{field-resolver.ts → formatting/field/resolver.ts} +27 -2
  110. package/src/runtime/formatting/font-resolution.ts +83 -0
  111. package/src/runtime/formatting/formatting-context.ts +903 -0
  112. package/src/runtime/formatting/formatting-types.ts +157 -0
  113. package/src/runtime/{hyperlink-color-resolver.ts → formatting/hyperlink-color.ts} +2 -2
  114. package/src/runtime/formatting/index.ts +125 -0
  115. package/src/runtime/{resolved-numbering-geometry.ts → formatting/numbering/geometry.ts} +1 -1
  116. package/src/runtime/{numbering-prefix.ts → formatting/numbering/prefix.ts} +170 -3
  117. package/src/runtime/formatting/paragraph-style-resolver.ts +92 -0
  118. package/src/runtime/formatting/projector.ts +75 -0
  119. package/src/runtime/formatting/resolve-effective.ts +407 -0
  120. package/src/runtime/formatting/revision-display.ts +105 -0
  121. package/src/runtime/{paragraph-style-resolver.ts → formatting/style-cascade.ts} +84 -141
  122. package/src/runtime/{table-style-resolver.ts → formatting/table-style-resolver.ts} +1 -1
  123. package/src/runtime/formatting/telemetry-bridge.ts +106 -0
  124. package/src/runtime/{theme-color-resolver.ts → formatting/theme-color.ts} +2 -30
  125. package/src/runtime/geometry/caret-geometry.ts +164 -0
  126. package/src/runtime/geometry/geometry-facet.ts +364 -0
  127. package/src/runtime/geometry/geometry-types.ts +256 -0
  128. package/src/runtime/geometry/hit-test.ts +125 -0
  129. package/src/runtime/geometry/index.ts +71 -0
  130. package/src/runtime/geometry/inert-geometry-facet.ts +43 -0
  131. package/src/runtime/geometry/invalidation.ts +35 -0
  132. package/src/runtime/geometry/object-handles.ts +77 -0
  133. package/src/runtime/geometry/overlay-rects.ts +85 -0
  134. package/src/runtime/geometry/project-anchors.ts +100 -0
  135. package/src/runtime/geometry/project-fragments.ts +216 -0
  136. package/src/runtime/geometry/projector.ts +129 -0
  137. package/src/runtime/geometry/replacement-envelope.ts +130 -0
  138. package/src/runtime/geometry/viewport.ts +218 -0
  139. package/src/runtime/layout/compat-input-ledger.ts +211 -0
  140. package/src/runtime/layout/index.ts +6 -1
  141. package/src/runtime/layout/inert-layout-facet.ts +12 -7
  142. package/src/runtime/layout/layout-engine-instance.ts +189 -11
  143. package/src/runtime/layout/layout-engine-version.ts +450 -1
  144. package/src/runtime/layout/layout-facet-types.ts +60 -0
  145. package/src/runtime/layout/layout-measurement-provider.ts +13 -0
  146. package/src/runtime/layout/measurement-backend-canvas.ts +14 -2
  147. package/src/runtime/layout/measurement-backend-empirical.ts +23 -4
  148. package/src/runtime/layout/page-graph.ts +62 -209
  149. package/src/runtime/layout/page-story-resolver.ts +7 -12
  150. package/src/runtime/layout/paginated-layout-engine.ts +186 -11
  151. package/src/runtime/layout/project-block-fragments.ts +11 -0
  152. package/src/runtime/layout/projector.ts +90 -0
  153. package/src/runtime/layout/public-facet.ts +187 -442
  154. package/src/runtime/layout/resolved-formatting-state.ts +158 -26
  155. package/src/runtime/layout/table-render-plan.ts +1 -1
  156. package/src/runtime/prerender/cache-envelope.ts +6 -1
  157. package/src/runtime/prerender/prerender-document.ts +18 -23
  158. package/src/runtime/render/decoration-resolver.ts +1 -1
  159. package/src/runtime/render/render-frame-types.ts +20 -0
  160. package/src/runtime/render/render-kernel.ts +94 -25
  161. package/src/runtime/scopes/_formatting-seam.ts +262 -0
  162. package/src/runtime/scopes/_scope-dependencies.ts +49 -0
  163. package/src/runtime/scopes/action-validation.ts +356 -0
  164. package/src/runtime/scopes/attach-explanation.ts +102 -0
  165. package/src/runtime/scopes/audit-bundle.ts +71 -0
  166. package/src/runtime/scopes/compile-scope-bundle.ts +163 -0
  167. package/src/runtime/scopes/compile-scope.ts +262 -0
  168. package/src/runtime/scopes/compiler-service.ts +431 -0
  169. package/src/runtime/scopes/create-issue.ts +107 -0
  170. package/src/runtime/scopes/enumerate-scopes.ts +543 -0
  171. package/src/runtime/scopes/evidence.ts +233 -0
  172. package/src/runtime/scopes/index.ts +150 -0
  173. package/src/runtime/scopes/position-map.ts +214 -0
  174. package/src/runtime/scopes/preservation-boundary.ts +91 -0
  175. package/src/runtime/scopes/projector.ts +49 -0
  176. package/src/runtime/scopes/replaceability.ts +87 -0
  177. package/src/runtime/scopes/replacement/apply.ts +228 -0
  178. package/src/runtime/scopes/replacement/compile.ts +59 -0
  179. package/src/runtime/scopes/replacement/propose.ts +42 -0
  180. package/src/runtime/scopes/resolve-reference.ts +347 -0
  181. package/src/runtime/scopes/review-bundle.ts +141 -0
  182. package/src/runtime/scopes/scope-kinds/_paragraph-text.ts +57 -0
  183. package/src/runtime/scopes/scope-kinds/_table-text.ts +42 -0
  184. package/src/runtime/scopes/scope-kinds/comment-thread.ts +59 -0
  185. package/src/runtime/scopes/scope-kinds/field.ts +65 -0
  186. package/src/runtime/scopes/scope-kinds/heading.ts +84 -0
  187. package/src/runtime/scopes/scope-kinds/list-item.ts +77 -0
  188. package/src/runtime/scopes/scope-kinds/paragraph.ts +182 -0
  189. package/src/runtime/scopes/scope-kinds/revision.ts +62 -0
  190. package/src/runtime/scopes/scope-kinds/table-cell.ts +57 -0
  191. package/src/runtime/scopes/scope-kinds/table-row.ts +61 -0
  192. package/src/runtime/scopes/scope-kinds/table.ts +55 -0
  193. package/src/runtime/scopes/scope-range.ts +208 -0
  194. package/src/runtime/scopes/semantic-scope-types.ts +454 -0
  195. package/src/runtime/scopes/workflow-overlap.ts +92 -0
  196. package/src/runtime/selection/index.ts +1 -1
  197. package/src/runtime/structure-ops/fragment-insert.ts +1 -1
  198. package/src/runtime/structure-ops/index.ts +1 -1
  199. package/src/runtime/surface-projection.ts +232 -262
  200. package/src/runtime/units.ts +4 -2
  201. package/src/runtime/workflow/coordinator.ts +1348 -0
  202. package/src/runtime/workflow/derived-scope-resolver.ts +125 -0
  203. package/src/runtime/workflow/index.ts +25 -0
  204. package/src/runtime/workflow/markup-mode-policy.ts +98 -0
  205. package/src/runtime/{workflow-markup.ts → workflow/markup.ts} +6 -6
  206. package/src/runtime/workflow/metadata-persistence.ts +306 -0
  207. package/src/runtime/workflow/metadata-writer.ts +123 -0
  208. package/src/runtime/workflow/overlay-store.ts +690 -0
  209. package/src/runtime/workflow/projector.ts +127 -0
  210. package/src/runtime/{query-scopes.ts → workflow/query-scopes.ts} +3 -3
  211. package/src/runtime/{workflow-rail-segments.ts → workflow/rail/compose.ts} +60 -165
  212. package/src/runtime/workflow/rail/types.ts +198 -0
  213. package/src/runtime/workflow/scope-rail-composer.ts +39 -0
  214. package/src/runtime/{scope-resolver.ts → workflow/scope-resolver.ts} +3 -3
  215. package/src/runtime/workflow/scope-writer.ts +188 -0
  216. package/src/runtime/{tamper-gate.ts → workflow/tamper-gate.ts} +1 -1
  217. package/src/runtime/workflow/visibility-policy.ts +129 -0
  218. package/src/session/_sync-legacy.ts +66 -0
  219. package/src/session/export/embedded-reconstitute.ts +104 -0
  220. package/src/session/export/export-diagnostics.ts +85 -0
  221. package/src/session/export/export-validation.ts +110 -0
  222. package/src/session/export/index.ts +34 -0
  223. package/src/session/export/preservation-reattach.ts +30 -0
  224. package/src/session/export/serialize-dispatch.ts +165 -0
  225. package/src/session/export/stateful-export-pipeline.ts +432 -0
  226. package/src/session/export/stateful-export.ts +684 -0
  227. package/src/session/import/canonical-assembly.ts +227 -0
  228. package/src/session/import/diagnostics-session.ts +54 -0
  229. package/src/session/import/embedded-discovery.ts +225 -0
  230. package/src/session/import/embedded-offload.ts +337 -0
  231. package/src/session/import/import-diagnostics.ts +69 -0
  232. package/src/session/import/loader-types.ts +313 -0
  233. package/src/session/import/loader.ts +1834 -0
  234. package/src/session/import/normalize.ts +195 -0
  235. package/src/session/import/package-parts.ts +217 -0
  236. package/src/session/import/package-read.ts +195 -0
  237. package/src/session/import/parse-orchestration.ts +105 -0
  238. package/src/session/import/part-constants.ts +70 -0
  239. package/src/session/import/part-discovery.ts +94 -0
  240. package/src/session/import/preservation-index.ts +46 -0
  241. package/src/{runtime/read-only-diagnostics-runtime.ts → session/import/read-only-diagnostics.ts} +24 -3
  242. package/src/session/import/review-import.ts +508 -0
  243. package/src/session/import/styles-consolidation.ts +281 -0
  244. package/src/session/import/workflow-scope-import.ts +256 -0
  245. package/src/session/index.ts +37 -0
  246. package/src/session/session-state.ts +69 -0
  247. package/src/session/session.ts +532 -0
  248. package/src/session/shared/protection.ts +228 -0
  249. package/src/session/shared/session-utils.ts +82 -0
  250. package/src/session/types.ts +499 -0
  251. package/src/shell/chart-snapshots.ts +96 -0
  252. package/src/shell/media-previews.ts +85 -0
  253. package/src/shell/overlay-anchor-bridge.ts +53 -0
  254. package/src/shell/paste-adapter.ts +23 -0
  255. package/src/shell/ref-commands.ts +1697 -0
  256. package/src/shell/ref-utilities.ts +48 -0
  257. package/src/shell/search.ts +51 -0
  258. package/src/{ui/editor-runtime-boundary.ts → shell/session-bootstrap.ts} +243 -67
  259. package/src/shell/ui-subscriber-channels.ts +81 -0
  260. package/src/shell/use-collab-sync.ts +116 -0
  261. package/src/ui/WordReviewEditor.tsx +496 -2051
  262. package/src/ui/editor-shell-view.tsx +30 -1
  263. package/src/ui/editor-surface-controller.tsx +49 -1
  264. package/src/ui/headless/revision-decoration-model.ts +83 -0
  265. package/src/{ui-tailwind/chrome → ui/headless}/role-action-sets.ts +1 -1
  266. package/src/ui/headless/scoped-chrome-policy.ts +2 -2
  267. package/src/ui/headless/selection-tool-context.ts +1 -1
  268. package/src/ui/headless/selection-tool-resolver.ts +1 -1
  269. package/src/ui/runtime-shortcut-dispatch.ts +46 -1
  270. package/src/ui/ui-controller-factory.ts +221 -0
  271. package/src/ui-tailwind/chart/ChartSurface.tsx +2 -2
  272. package/src/ui-tailwind/chart/layout/legend-layout.ts +1 -1
  273. package/src/ui-tailwind/chart/layout/plot-area.ts +2 -2
  274. package/src/ui-tailwind/chart/layout/title-layout.ts +1 -1
  275. package/src/ui-tailwind/chart/render/area.tsx +3 -3
  276. package/src/ui-tailwind/chart/render/bar-column.tsx +3 -3
  277. package/src/ui-tailwind/chart/render/bubble.tsx +3 -3
  278. package/src/ui-tailwind/chart/render/combo.tsx +2 -2
  279. package/src/ui-tailwind/chart/render/data-labels.tsx +2 -2
  280. package/src/ui-tailwind/chart/render/font-metrics.ts +2 -2
  281. package/src/ui-tailwind/chart/render/line.tsx +3 -3
  282. package/src/ui-tailwind/chart/render/pie.tsx +6 -6
  283. package/src/ui-tailwind/chart/render/scatter.tsx +3 -3
  284. package/src/ui-tailwind/chart/render/svg-primitives.ts +3 -3
  285. package/src/ui-tailwind/chart/render/unsupported.tsx +2 -2
  286. package/src/ui-tailwind/chrome/build-context-menu-entries.ts +88 -0
  287. package/src/ui-tailwind/chrome/chrome-preset-toolbar.tsx +1 -1
  288. package/src/ui-tailwind/chrome/collab-send-to-supplier-button.tsx +1 -1
  289. package/src/ui-tailwind/chrome/collab-tamper-banner.tsx +1 -1
  290. package/src/ui-tailwind/chrome/collab-top-nav-container.tsx +1 -1
  291. package/src/ui-tailwind/chrome/editor-action-registry.ts +553 -0
  292. package/src/ui-tailwind/chrome/editor-actions-to-palette.ts +182 -0
  293. package/src/ui-tailwind/chrome/local-surface-arbiter.ts +534 -0
  294. package/src/ui-tailwind/chrome/resolve-target-kind.ts +226 -0
  295. package/src/ui-tailwind/chrome/tw-alert-banner.tsx +38 -4
  296. package/src/ui-tailwind/chrome/tw-context-band.tsx +125 -0
  297. package/src/ui-tailwind/chrome/tw-context-menu-portal.tsx +248 -0
  298. package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +42 -1
  299. package/src/ui-tailwind/chrome/tw-selection-anchor-resolver.ts +8 -7
  300. package/src/ui-tailwind/chrome/tw-selection-tool-blocked.tsx +38 -4
  301. package/src/ui-tailwind/chrome/tw-selection-tool-comment.tsx +104 -6
  302. package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +66 -7
  303. package/src/ui-tailwind/chrome/tw-selection-tool-workflow.tsx +54 -8
  304. package/src/ui-tailwind/chrome/tw-shortcut-hint.tsx +7 -1
  305. package/src/ui-tailwind/chrome/tw-suggestion-card.tsx +33 -0
  306. package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +78 -1
  307. package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +16 -8
  308. package/src/ui-tailwind/chrome/tw-workspace-chrome-host.tsx +276 -0
  309. package/src/ui-tailwind/chrome/use-context-menu-controller.ts +201 -0
  310. package/src/ui-tailwind/chrome-overlay/chrome-overlay-projector.ts +1 -1
  311. package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +22 -4
  312. package/src/ui-tailwind/chrome-overlay/tw-comment-balloon-layer.tsx +1 -1
  313. package/src/ui-tailwind/chrome-overlay/tw-locked-block-layer.tsx +1 -1
  314. package/src/ui-tailwind/chrome-overlay/tw-object-selection-overlay.tsx +11 -5
  315. package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +197 -3
  316. package/src/ui-tailwind/chrome-overlay/tw-revision-margin-bar-layer.tsx +1 -1
  317. package/src/ui-tailwind/chrome-overlay/tw-scope-card-layer.tsx +35 -6
  318. package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +24 -16
  319. package/src/ui-tailwind/chrome-overlay/tw-table-continuation-header.tsx +1 -1
  320. package/src/ui-tailwind/debug/README.md +57 -0
  321. package/src/ui-tailwind/debug/index.ts +3 -0
  322. package/src/ui-tailwind/debug/tw-debug-overlay.tsx +186 -0
  323. package/src/ui-tailwind/debug/tw-debug-presentation.tsx +80 -0
  324. package/src/ui-tailwind/debug/tw-debug-top-bar.tsx +83 -0
  325. package/src/ui-tailwind/editor-surface/chart-node-view.tsx +2 -2
  326. package/src/ui-tailwind/editor-surface/float-wrap-resolver.ts +1 -1
  327. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +135 -10
  328. package/src/ui-tailwind/editor-surface/pm-decorations.ts +40 -13
  329. package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +1 -1
  330. package/src/ui-tailwind/editor-surface/pm-schema.ts +1 -1
  331. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +3 -3
  332. package/src/ui-tailwind/editor-surface/predicted-tag-preflight.ts +1 -1
  333. package/src/ui-tailwind/editor-surface/remote-cursor-plugin.ts +2 -2
  334. package/src/ui-tailwind/editor-surface/scroll-anchor.ts +91 -9
  335. package/src/ui-tailwind/editor-surface/shape-renderer.ts +1 -1
  336. package/src/ui-tailwind/editor-surface/surface-layer.ts +1 -1
  337. package/src/ui-tailwind/editor-surface/tw-opaque-block.tsx +1 -1
  338. package/src/ui-tailwind/editor-surface/tw-page-block-view.helpers.ts +23 -6
  339. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +132 -22
  340. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +1 -1
  341. package/src/ui-tailwind/index.ts +0 -5
  342. package/src/ui-tailwind/overlay-anchor-bridge-context.tsx +33 -0
  343. package/src/ui-tailwind/page-stack/floating-image-overlay-model.ts +66 -29
  344. package/src/ui-tailwind/page-stack/tw-floating-image-layer.tsx +25 -2
  345. package/src/ui-tailwind/review/comment-markdown-renderer.tsx +15 -0
  346. package/src/ui-tailwind/review/tw-review-rail.tsx +92 -4
  347. package/src/ui-tailwind/review/tw-workflow-tab.tsx +1 -1
  348. package/src/ui-tailwind/review-workspace/page-chrome.ts +210 -0
  349. package/src/ui-tailwind/review-workspace/page-shell-metrics.ts +101 -0
  350. package/src/ui-tailwind/review-workspace/paragraph-layout.ts +115 -0
  351. package/src/ui-tailwind/review-workspace/selection-toolbar-placement.ts +97 -0
  352. package/src/ui-tailwind/review-workspace/tw-review-workspace-navigator.tsx +130 -0
  353. package/src/ui-tailwind/review-workspace/tw-review-workspace-page-toolbar.tsx +240 -0
  354. package/src/ui-tailwind/review-workspace/tw-review-workspace-rail.tsx +59 -0
  355. package/src/ui-tailwind/review-workspace/types.ts +408 -0
  356. package/src/ui-tailwind/review-workspace/use-chrome-policy.ts +104 -0
  357. package/src/ui-tailwind/review-workspace/use-derived-view-state.ts +151 -0
  358. package/src/ui-tailwind/review-workspace/use-diagnostics-signal.ts +70 -0
  359. package/src/ui-tailwind/review-workspace/use-grabbed-segment-offsets.ts +40 -0
  360. package/src/ui-tailwind/review-workspace/use-layout-facet-render-signal.ts +55 -0
  361. package/src/ui-tailwind/review-workspace/use-page-markers.ts +130 -0
  362. package/src/ui-tailwind/review-workspace/use-pm-surface-capture.ts +60 -0
  363. package/src/ui-tailwind/review-workspace/use-review-rail-state.ts +63 -0
  364. package/src/ui-tailwind/review-workspace/use-scope-card-state.ts +170 -0
  365. package/src/ui-tailwind/review-workspace/use-scroll-root-capture.ts +28 -0
  366. package/src/ui-tailwind/review-workspace/use-selection-toolbar-placement.ts +113 -0
  367. package/src/ui-tailwind/review-workspace/use-shell-selection-anchor-bridge.ts +120 -0
  368. package/src/ui-tailwind/review-workspace/use-status-bar-page-facts.ts +55 -0
  369. package/src/ui-tailwind/review-workspace/use-viewport-dimensions.ts +43 -0
  370. package/src/ui-tailwind/review-workspace/use-workspace-arbiter.ts +25 -0
  371. package/src/ui-tailwind/review-workspace/use-workspace-composition.ts +86 -0
  372. package/src/ui-tailwind/review-workspace/use-workspace-side-effects.ts +150 -0
  373. package/src/ui-tailwind/theme/editor-theme.css +25 -0
  374. package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +2 -2
  375. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +61 -98
  376. package/src/ui-tailwind/tw-review-workspace.tsx +521 -1802
  377. package/src/ui-tailwind/ui-api-context.tsx +43 -0
  378. package/src/ui-tailwind/ui-shell-channels-context.tsx +49 -0
  379. package/src/validation/compatibility-engine.ts +6 -6
  380. package/src/runtime/styles-cascade.ts +0 -33
  381. package/src/ui-tailwind/chrome/tw-mode-dock.tsx +0 -85
  382. /package/src/runtime/{page-number-format.ts → formatting/field/page-number-format.ts} +0 -0
  383. /package/src/runtime/{ai-action-policy.ts → workflow/ai-action-policy.ts} +0 -0
  384. /package/src/runtime/{scope-tag-registry.ts → workflow/scope-tag-registry.ts} +0 -0
@@ -0,0 +1,1697 @@
1
+ /**
2
+ * Shell helper — ref-method command dispatch.
3
+ *
4
+ * Extracted from `src/ui/WordReviewEditor.tsx` by refactor/11 Slice 4
5
+ * (Track A, "ref-bridge carve-out"). Every `applyRuntime*` /
6
+ * `applySuggesting*` helper these exports correspond to is an
7
+ * imperative ref-verb dispatch path: it takes the mounted
8
+ * `WordReviewEditorRuntime`, consults interaction-guard / workflow
9
+ * state, optionally rewrites the canonical document through a core
10
+ * command, and dispatches the result via `runtime.dispatch(...)`.
11
+ *
12
+ * These helpers have zero React dependencies — they operate on the
13
+ * runtime handle + public-API request shapes + substrate commands.
14
+ * They lived in WRE for historical reasons (WRE compiled the
15
+ * `ref.current` surface inline) and are moved here so WRE is no
16
+ * longer the single largest `src/ui/**` entry in the Layer 11
17
+ * boundary register (see
18
+ * `docs/plans/refactor/11-presentation-surfaces-boundary-exceptions.json`).
19
+ *
20
+ * Retirement path: these functions are the component-facing
21
+ * implementations of the v3 Runtime API mutation verbs. As refactor/07
22
+ * and refactor/10 ship their respective API façades, each helper
23
+ * graduates to `src/api/v3/runtime/**` and the shell file loses that
24
+ * import — the goal is to drop the count here to 0 over time.
25
+ *
26
+ * See `docs/architecture/11-presentation-surfaces.md` §8.8 ("mounted
27
+ * shell allowed to touch substrate") for the layering rationale.
28
+ */
29
+
30
+ import type {
31
+ EditorSessionState,
32
+ EditorStoryTarget,
33
+ FormattingAlignment,
34
+ HeaderFooterLinkPatch,
35
+ InsertImageOptions,
36
+ InsertTableOptions,
37
+ RuntimeRenderSnapshot,
38
+ SectionBreakType,
39
+ SectionLayoutPatch,
40
+ SectionPageNumberingPatch,
41
+ SelectionSnapshot as PublicSelectionSnapshot,
42
+ StyleCatalogSnapshot,
43
+ SurfaceBlockSnapshot,
44
+ SurfaceInlineSegment,
45
+ TableOp,
46
+ } from "../api/public-types";
47
+ import {
48
+ createDetachedAnchor,
49
+ createNodeAnchor,
50
+ createRangeAnchor,
51
+ storyTargetsEqual,
52
+ type TransactionMapping,
53
+ } from "../core/selection/mapping.ts";
54
+ import { applyFormattingOperationToDocument } from "../core/commands/formatting-commands.ts";
55
+ import {
56
+ applyParagraphStyleToDocument,
57
+ applyTableStyleToDocument,
58
+ } from "../core/commands/style-commands.ts";
59
+ import {
60
+ continueNumbering as continueListNumbering,
61
+ restartNumbering as restartListNumbering,
62
+ toggleBulletedList,
63
+ toggleNumberedList,
64
+ } from "../core/commands/list-commands.ts";
65
+ import { type DispatchContext } from "../runtime/edit-dispatch/index.ts";
66
+ import {
67
+ resolveActiveParagraphIndex,
68
+ setActiveParagraphIndentation,
69
+ setActiveParagraphTabStops,
70
+ } from "../core/commands/paragraph-layout-commands.ts";
71
+ import {
72
+ deleteSectionBreakAtSectionIndex,
73
+ insertSectionBreakAfterSectionIndex,
74
+ setHeaderFooterLinkAtSectionIndex,
75
+ setSectionPageNumberingAtSectionIndex,
76
+ updateSectionLayoutAtSectionIndex,
77
+ } from "../core/commands/section-layout-commands.ts";
78
+ import {
79
+ insertImage as insertImageInDocument,
80
+ resizeImage as resizeImageInCatalog,
81
+ repositionFloatingImage as repositionFloatingImageInDocument,
82
+ } from "../core/commands/image-commands.ts";
83
+ import {
84
+ applyTableStructureOperation as applyTableStructureOperationInDocument,
85
+ type TableStructureOperation,
86
+ } from "../core/commands/table-structure-commands.ts";
87
+ import {
88
+ insertPageBreak as insertPageBreakInDocument,
89
+ insertTable as insertTableInDocument,
90
+ } from "../core/commands/text-commands.ts";
91
+ import { type SelectionSnapshot as InternalSelectionSnapshot } from "../core/state/editor-state.ts";
92
+ import {
93
+ getStoryBlocks,
94
+ replaceStoryBlocks,
95
+ } from "../runtime/story-targeting.ts";
96
+ import type { TwProseMirrorSurfaceRef } from "../ui-tailwind/editor-surface/tw-prosemirror-surface";
97
+ import type { WordReviewEditorRuntime } from "./session-bootstrap.ts";
98
+
99
+ // ---------------------------------------------------------------------------
100
+ // Re-exports so WRE (and tests) can thread the operation discriminator
101
+ // through without re-importing substrate.
102
+ // ---------------------------------------------------------------------------
103
+
104
+ export type { TableStructureOperation };
105
+
106
+ // ---------------------------------------------------------------------------
107
+ // Suggestion-group fan-out
108
+ // ---------------------------------------------------------------------------
109
+
110
+ /**
111
+ * R3 — best-effort suggestion-group accept/reject fan-out. Resolves
112
+ * the group's suggestions from the current snapshot, then fans out
113
+ * `acceptChange` / `rejectChange` across every changeId in each
114
+ * group member. P2 batches these in rapid succession; the runtime
115
+ * commit boundary collapses them into a single logical transaction.
116
+ * A future phase adds true atomicity at the runtime level.
117
+ */
118
+ export function applySuggestionGroupAction(
119
+ runtime: WordReviewEditorRuntime,
120
+ groupId: string,
121
+ action: "accept" | "reject",
122
+ ): void {
123
+ const snapshot = runtime.getSuggestionsSnapshot();
124
+ const group = snapshot.groups?.find((entry) => entry.groupId === groupId);
125
+ const op = action === "accept" ? "acceptSuggestionGroup" : "rejectSuggestionGroup";
126
+ if (!group) {
127
+ runtime.emitTransientWarning({
128
+ warningId: `suggestion-group-unknown-${groupId}-${Date.now()}`,
129
+ code: "review_target_not_found",
130
+ severity: "info",
131
+ message: `${op}("${groupId}") skipped: unknown groupId.`,
132
+ source: "review",
133
+ details: { op, targetId: groupId, reason: "group_unknown" },
134
+ });
135
+ return;
136
+ }
137
+ const byId = new Map(
138
+ snapshot.suggestions.map((entry) => [entry.suggestionId, entry]),
139
+ );
140
+ const skippedSuggestions: string[] = [];
141
+ for (const suggestionId of group.suggestionIds) {
142
+ const suggestion = byId.get(suggestionId);
143
+ if (!suggestion) {
144
+ skippedSuggestions.push(suggestionId);
145
+ continue;
146
+ }
147
+ for (const changeId of suggestion.changeIds) {
148
+ if (action === "accept") {
149
+ runtime.acceptChange(changeId);
150
+ } else {
151
+ runtime.rejectChange(changeId);
152
+ }
153
+ }
154
+ }
155
+ if (skippedSuggestions.length > 0) {
156
+ runtime.emitTransientWarning({
157
+ warningId: `suggestion-group-stale-${groupId}-${Date.now()}`,
158
+ code: "review_target_not_found",
159
+ severity: "info",
160
+ message: `${op}("${groupId}") partially skipped: ${skippedSuggestions.length} suggestion(s) no longer in snapshot.`,
161
+ source: "review",
162
+ details: {
163
+ op,
164
+ targetId: groupId,
165
+ reason: "suggestion_stale",
166
+ skippedSuggestionIds: skippedSuggestions,
167
+ },
168
+ });
169
+ }
170
+ }
171
+
172
+ // ---------------------------------------------------------------------------
173
+ // Style-catalog projection
174
+ // ---------------------------------------------------------------------------
175
+
176
+ export function getRuntimeStyleCatalog(
177
+ input:
178
+ | WordReviewEditorRuntime
179
+ | EditorSessionState["canonicalDocument"]["styles"],
180
+ ): StyleCatalogSnapshot {
181
+ const styles =
182
+ "getSessionState" in input
183
+ ? input.getSessionState().canonicalDocument.styles
184
+ : input;
185
+ const mapRecord = <
186
+ T extends {
187
+ styleId: string;
188
+ displayName: string;
189
+ kind: "paragraph" | "character" | "table";
190
+ isDefault: boolean;
191
+ basedOn?: string;
192
+ nextStyle?: string;
193
+ },
194
+ >(
195
+ record: Record<string, T>,
196
+ ) =>
197
+ Object.values(record)
198
+ .map((entry) => ({
199
+ styleId: entry.styleId,
200
+ displayName: entry.displayName,
201
+ kind: entry.kind,
202
+ isDefault: entry.isDefault,
203
+ ...(entry.basedOn ? { basedOn: entry.basedOn } : {}),
204
+ ...(entry.nextStyle ? { nextStyle: entry.nextStyle } : {}),
205
+ }))
206
+ .sort((left, right) =>
207
+ left.displayName.localeCompare(right.displayName) ||
208
+ left.styleId.localeCompare(right.styleId),
209
+ );
210
+
211
+ return {
212
+ paragraphs: mapRecord(styles.paragraphs),
213
+ characters: mapRecord(styles.characters),
214
+ tables: mapRecord(styles.tables),
215
+ fromPackage: styles.fromPackage === true,
216
+ };
217
+ }
218
+
219
+ // ---------------------------------------------------------------------------
220
+ // Formatting (flat + suggesting-mode)
221
+ // ---------------------------------------------------------------------------
222
+
223
+ type FormattingOperation =
224
+ | { type: "toggle"; mark: "bold" | "italic" | "underline" | "strikethrough" | "superscript" | "subscript" }
225
+ | { type: "set-font-family"; fontFamily: string | null }
226
+ | { type: "set-font-size"; size: number | null }
227
+ | { type: "set-text-color"; color: string | null }
228
+ | { type: "set-highlight-color"; color: string | null }
229
+ | { type: "set-alignment"; alignment: FormattingAlignment }
230
+ | { type: "indent" }
231
+ | { type: "outdent" };
232
+
233
+ export function applyRuntimeFormattingOperation(
234
+ runtime: WordReviewEditorRuntime,
235
+ operation: FormattingOperation,
236
+ ): void {
237
+ if (isSelectionSuggesting(runtime)) {
238
+ if (applySuggestingFormattingOperation(runtime, operation)) {
239
+ return;
240
+ }
241
+ }
242
+ if (emitSuggestingUnsupportedMutation(runtime, getFormattingOperationCommandName(operation))) {
243
+ return;
244
+ }
245
+ const context = getStoryMutationContext(runtime, getFormattingOperationCommandName(operation));
246
+ if (!context) {
247
+ return;
248
+ }
249
+
250
+ const result = applyFormattingOperationToDocument(
251
+ context.localDocument,
252
+ context.localSnapshot,
253
+ operation,
254
+ );
255
+ dispatchStoryMutationResult(
256
+ runtime,
257
+ context,
258
+ {
259
+ ...result,
260
+ selection: toRuntimeSelectionSnapshot(result.selection),
261
+ },
262
+ context.timestamp,
263
+ );
264
+ }
265
+
266
+ function applySuggestingFormattingOperation(
267
+ runtime: WordReviewEditorRuntime,
268
+ operation: FormattingOperation,
269
+ ): boolean {
270
+ const commandName = getFormattingOperationCommandName(operation);
271
+ const context = getStoryMutationContext(runtime, commandName);
272
+ if (!context) {
273
+ return true;
274
+ }
275
+ if (context.activeStory.kind !== "main") {
276
+ runtime.emitBlockedCommand(commandName, [{
277
+ code: "suggesting_unsupported",
278
+ message: `"${commandName}" is not supported in suggesting mode for this story.`,
279
+ }]);
280
+ return true;
281
+ }
282
+
283
+ if (operation.type === "set-alignment" || operation.type === "indent" || operation.type === "outdent") {
284
+ const paragraphContext = resolveActiveParagraphContext(context.localSnapshot);
285
+ if (!paragraphContext) {
286
+ return true;
287
+ }
288
+ const beforeXml = buildParagraphPropertyBeforeXml(paragraphContext.paragraph);
289
+ const result = applyFormattingOperationToDocument(
290
+ context.localDocument,
291
+ context.localSnapshot,
292
+ operation,
293
+ );
294
+ if (!result.changed) {
295
+ return true;
296
+ }
297
+ const nextDocument = appendPropertyChangeSuggestion(
298
+ result.document,
299
+ {
300
+ from: paragraphContext.paragraph.from,
301
+ to: paragraphContext.paragraph.to,
302
+ },
303
+ {
304
+ originalRevisionType: "pPrChange",
305
+ xmlTag: "pPrChange",
306
+ beforeXml,
307
+ semanticKind: "paragraph-property-change",
308
+ storyTarget: context.activeStory,
309
+ authorId: runtime.getDefaultAuthorId?.(),
310
+ },
311
+ context.timestamp,
312
+ );
313
+ dispatchStoryMutationResult(
314
+ runtime,
315
+ context,
316
+ {
317
+ changed: true,
318
+ document: nextDocument,
319
+ selection: toRuntimeSelectionSnapshot(result.selection),
320
+ },
321
+ context.timestamp,
322
+ );
323
+ return true;
324
+ }
325
+
326
+ const segment = findSingleSelectedTextSegment(context.localSnapshot);
327
+ if (!segment) {
328
+ runtime.emitBlockedCommand(commandName, [{
329
+ code: "suggesting_unsupported",
330
+ message: `"${commandName}" requires one bounded text segment in suggesting mode.`,
331
+ }]);
332
+ return true;
333
+ }
334
+ const beforeXml = buildRunPropertyBeforeXml(segment);
335
+ const result = applyFormattingOperationToDocument(
336
+ context.localDocument,
337
+ context.localSnapshot,
338
+ operation,
339
+ );
340
+ if (!result.changed) {
341
+ return true;
342
+ }
343
+ const nextDocument = appendPropertyChangeSuggestion(
344
+ result.document,
345
+ {
346
+ from: segment.from,
347
+ to: segment.to,
348
+ },
349
+ {
350
+ originalRevisionType: "rPrChange",
351
+ xmlTag: "rPrChange",
352
+ beforeXml,
353
+ semanticKind: "formatting-change",
354
+ storyTarget: context.activeStory,
355
+ authorId: runtime.getDefaultAuthorId?.(),
356
+ },
357
+ context.timestamp,
358
+ );
359
+ dispatchStoryMutationResult(
360
+ runtime,
361
+ context,
362
+ {
363
+ changed: true,
364
+ document: nextDocument,
365
+ selection: toRuntimeSelectionSnapshot(result.selection),
366
+ },
367
+ context.timestamp,
368
+ );
369
+ return true;
370
+ }
371
+
372
+ function getFormattingOperationCommandName(operation: FormattingOperation): string {
373
+ switch (operation.type) {
374
+ case "toggle":
375
+ return `toggle${operation.mark.charAt(0).toUpperCase()}${operation.mark.slice(1)}`;
376
+ case "set-font-family":
377
+ return "setFontFamily";
378
+ case "set-font-size":
379
+ return "setFontSize";
380
+ case "set-text-color":
381
+ return "setTextColor";
382
+ case "set-highlight-color":
383
+ return "setHighlightColor";
384
+ case "set-alignment":
385
+ return "setAlignment";
386
+ case "indent":
387
+ return "indent";
388
+ case "outdent":
389
+ return "outdent";
390
+ }
391
+ }
392
+
393
+ // ---------------------------------------------------------------------------
394
+ // Suggesting-mode property-change plumbing
395
+ // ---------------------------------------------------------------------------
396
+
397
+ function emitSuggestingUnsupportedMutation(
398
+ runtime: WordReviewEditorRuntime,
399
+ command: string,
400
+ ): boolean {
401
+ if (!isSelectionSuggesting(runtime)) {
402
+ return false;
403
+ }
404
+
405
+ runtime.emitBlockedCommand(command, [{
406
+ code: "suggesting_unsupported",
407
+ message: `"${command}" is not supported in suggesting mode.`,
408
+ }]);
409
+ return true;
410
+ }
411
+
412
+ function appendPropertyChangeSuggestion(
413
+ document: EditorSessionState["canonicalDocument"],
414
+ anchor: { from: number; to: number },
415
+ input: {
416
+ originalRevisionType: "rPrChange" | "pPrChange";
417
+ xmlTag: "rPrChange" | "pPrChange";
418
+ beforeXml: string;
419
+ semanticKind: "formatting-change" | "paragraph-property-change";
420
+ storyTarget: EditorStoryTarget;
421
+ authorId?: string;
422
+ },
423
+ timestamp: string,
424
+ ): EditorSessionState["canonicalDocument"] {
425
+ const existing = document.review.revisions;
426
+ const changeId = createRuntimeSuggestionChangeId(existing, timestamp);
427
+ const resolvedAuthorId = input.authorId ?? "unknown";
428
+ return {
429
+ ...document,
430
+ review: {
431
+ ...document.review,
432
+ revisions: {
433
+ ...existing,
434
+ [changeId]: {
435
+ changeId,
436
+ kind: "property-change",
437
+ anchor: createRangeAnchor(anchor.from, anchor.to, { start: 1, end: -1 }),
438
+ authorId: resolvedAuthorId,
439
+ createdAt: timestamp,
440
+ warningIds: [],
441
+ metadata: {
442
+ source: "runtime",
443
+ storyTarget: input.storyTarget,
444
+ suggestionId: changeId,
445
+ semanticKind: input.semanticKind,
446
+ originalRevisionType: input.originalRevisionType,
447
+ propertyChangeData: {
448
+ xmlTag: input.xmlTag,
449
+ beforeXml: input.beforeXml,
450
+ },
451
+ },
452
+ status: "open",
453
+ },
454
+ },
455
+ },
456
+ };
457
+ }
458
+
459
+ function createRuntimeSuggestionChangeId(
460
+ existing: EditorSessionState["canonicalDocument"]["review"]["revisions"],
461
+ timestamp: string,
462
+ ): string {
463
+ const base = `change-${timestamp.replace(/[^0-9]/gu, "")}`;
464
+ let counter = Object.keys(existing).length + 1;
465
+ let candidate = `${base}-p${counter}`;
466
+ while (existing[candidate]) {
467
+ counter += 1;
468
+ candidate = `${base}-p${counter}`;
469
+ }
470
+ return candidate;
471
+ }
472
+
473
+ function findSingleSelectedTextSegment(
474
+ snapshot: Pick<RuntimeRenderSnapshot, "surface" | "selection">,
475
+ ): Extract<SurfaceInlineSegment, { kind: "text" }> | null {
476
+ if (!snapshot.surface || snapshot.selection.activeRange.kind !== "range" || snapshot.selection.isCollapsed) {
477
+ return null;
478
+ }
479
+ const selectionFrom = Math.min(snapshot.selection.anchor, snapshot.selection.head);
480
+ const selectionTo = Math.max(snapshot.selection.anchor, snapshot.selection.head);
481
+ const segments = collectSelectedTextSegments(snapshot.surface.blocks, selectionFrom, selectionTo);
482
+ if (segments.length !== 1) {
483
+ return null;
484
+ }
485
+ const [segment] = segments;
486
+ if (!segment || segment.from !== selectionFrom || segment.to !== selectionTo) {
487
+ return null;
488
+ }
489
+ return segment;
490
+ }
491
+
492
+ function collectSelectedTextSegments(
493
+ blocks: readonly SurfaceBlockSnapshot[],
494
+ selectionFrom: number,
495
+ selectionTo: number,
496
+ output: Array<Extract<SurfaceInlineSegment, { kind: "text" }>> = [],
497
+ ): Array<Extract<SurfaceInlineSegment, { kind: "text" }>> {
498
+ for (const block of blocks) {
499
+ if (block.kind === "paragraph") {
500
+ for (const segment of block.segments) {
501
+ if (
502
+ segment.kind === "text" &&
503
+ rangesOverlap(selectionFrom, selectionTo, segment.from, segment.to)
504
+ ) {
505
+ output.push(segment);
506
+ }
507
+ }
508
+ continue;
509
+ }
510
+ if (block.kind === "table") {
511
+ for (const row of block.rows) {
512
+ for (const cell of row.cells) {
513
+ collectSelectedTextSegments(cell.content, selectionFrom, selectionTo, output);
514
+ }
515
+ }
516
+ continue;
517
+ }
518
+ if (block.kind === "sdt_block") {
519
+ collectSelectedTextSegments(block.children, selectionFrom, selectionTo, output);
520
+ }
521
+ }
522
+ return output;
523
+ }
524
+
525
+ function rangesOverlap(
526
+ leftFrom: number,
527
+ leftTo: number,
528
+ rightFrom: number,
529
+ rightTo: number,
530
+ ): boolean {
531
+ return leftFrom < rightTo && rightFrom < leftTo;
532
+ }
533
+
534
+ function buildRunPropertyBeforeXml(
535
+ segment: Extract<SurfaceInlineSegment, { kind: "text" }>,
536
+ ): string {
537
+ const parts: string[] = [];
538
+ const marks = new Set(segment.marks ?? []);
539
+ if (marks.has("bold")) parts.push("<w:b/>");
540
+ if (marks.has("italic")) parts.push("<w:i/>");
541
+ if (marks.has("underline")) parts.push("<w:u w:val=\"single\"/>");
542
+ if (marks.has("strikethrough")) parts.push("<w:strike/>");
543
+ if (marks.has("superscript")) parts.push("<w:vertAlign w:val=\"superscript\"/>");
544
+ if (marks.has("subscript")) parts.push("<w:vertAlign w:val=\"subscript\"/>");
545
+ if (segment.markAttrs?.fontFamily) {
546
+ parts.push(`<w:rFonts w:ascii="${escapeAttributeXml(segment.markAttrs.fontFamily)}" w:hAnsi="${escapeAttributeXml(segment.markAttrs.fontFamily)}"/>`);
547
+ }
548
+ if (segment.markAttrs?.fontSize !== undefined) {
549
+ parts.push(`<w:sz w:val="${segment.markAttrs.fontSize}"/>`);
550
+ }
551
+ if (segment.markAttrs?.textColor) {
552
+ parts.push(`<w:color w:val="${escapeAttributeXml(segment.markAttrs.textColor)}"/>`);
553
+ }
554
+ if (segment.markAttrs?.backgroundColor) {
555
+ parts.push(`<w:shd w:val="clear" w:color="auto" w:fill="${escapeAttributeXml(segment.markAttrs.backgroundColor)}"/>`);
556
+ }
557
+ return `<w:rPr>${parts.join("")}</w:rPr>`;
558
+ }
559
+
560
+ function buildParagraphPropertyBeforeXml(
561
+ paragraph: Extract<SurfaceBlockSnapshot, { kind: "paragraph" }>,
562
+ ): string {
563
+ const parts: string[] = [];
564
+ if (paragraph.styleId) {
565
+ parts.push(`<w:pStyle w:val="${escapeAttributeXml(paragraph.styleId)}"/>`);
566
+ }
567
+ if (paragraph.numbering) {
568
+ parts.push(
569
+ `<w:numPr><w:ilvl w:val="${paragraph.numbering.level}"/><w:numId w:val="${escapeAttributeXml(
570
+ paragraph.numbering.numberingInstanceId.replace(/^num:/u, ""),
571
+ )}"/></w:numPr>`,
572
+ );
573
+ }
574
+ if (paragraph.alignment) {
575
+ parts.push(`<w:jc w:val="${escapeAttributeXml(paragraph.alignment)}"/>`);
576
+ }
577
+ if (paragraph.indentation) {
578
+ const attrs: string[] = [];
579
+ if (paragraph.indentation.left !== undefined) attrs.push(`w:left="${paragraph.indentation.left}"`);
580
+ if (paragraph.indentation.right !== undefined) attrs.push(`w:right="${paragraph.indentation.right}"`);
581
+ if (paragraph.indentation.firstLine !== undefined) attrs.push(`w:firstLine="${paragraph.indentation.firstLine}"`);
582
+ if (paragraph.indentation.hanging !== undefined) attrs.push(`w:hanging="${paragraph.indentation.hanging}"`);
583
+ if (attrs.length > 0) {
584
+ parts.push(`<w:ind ${attrs.join(" ")}/>`);
585
+ }
586
+ }
587
+ return `<w:pPr>${parts.join("")}</w:pPr>`;
588
+ }
589
+
590
+ function escapeAttributeXml(value: string): string {
591
+ return value
592
+ .replace(/&/g, "&amp;")
593
+ .replace(/</g, "&lt;")
594
+ .replace(/>/g, "&gt;")
595
+ .replace(/"/g, "&quot;");
596
+ }
597
+
598
+ // ---------------------------------------------------------------------------
599
+ // List toggle / paragraph style / table style
600
+ // ---------------------------------------------------------------------------
601
+
602
+ export function applyRuntimeListToggle(
603
+ runtime: WordReviewEditorRuntime,
604
+ kind: "bulleted" | "numbered",
605
+ ): void {
606
+ const commandName =
607
+ kind === "bulleted" ? "toggleBulletedList" : "toggleNumberedList";
608
+ if (emitSuggestingUnsupportedMutation(runtime, commandName)) {
609
+ return;
610
+ }
611
+ const context = getStoryMutationContext(runtime, commandName);
612
+ if (!context) {
613
+ return;
614
+ }
615
+
616
+ const paragraphContext = resolveActiveParagraphContext(context.localSnapshot);
617
+ if (!paragraphContext) {
618
+ return;
619
+ }
620
+
621
+ const result =
622
+ kind === "bulleted"
623
+ ? toggleBulletedList(
624
+ context.localDocument,
625
+ [paragraphContext.paragraphIndex],
626
+ { timestamp: context.timestamp },
627
+ )
628
+ : toggleNumberedList(
629
+ context.localDocument,
630
+ [paragraphContext.paragraphIndex],
631
+ { timestamp: context.timestamp },
632
+ );
633
+ dispatchStoryMutationResult(
634
+ runtime,
635
+ context,
636
+ {
637
+ changed: result.affectedParagraphIndexes.length > 0,
638
+ document: result.document,
639
+ selection: toRuntimeSelectionSnapshot(context.localSnapshot.selection),
640
+ },
641
+ context.timestamp,
642
+ );
643
+ }
644
+
645
+ export function applyRuntimeParagraphStyle(
646
+ runtime: WordReviewEditorRuntime,
647
+ styleId: string | null,
648
+ ): void {
649
+ if (emitSuggestingUnsupportedMutation(runtime, "setParagraphStyle")) {
650
+ return;
651
+ }
652
+ const context = getStoryMutationContext(runtime, "setParagraphStyle");
653
+ if (!context) {
654
+ return;
655
+ }
656
+
657
+ const result = applyParagraphStyleToDocument(
658
+ context.localDocument,
659
+ context.localSnapshot,
660
+ styleId,
661
+ );
662
+ dispatchStoryMutationResult(
663
+ runtime,
664
+ context,
665
+ {
666
+ ...result,
667
+ selection: toRuntimeSelectionSnapshot(result.selection),
668
+ },
669
+ context.timestamp,
670
+ );
671
+ }
672
+
673
+ export function applyRuntimeTableStyle(
674
+ runtime: WordReviewEditorRuntime,
675
+ styleId: string | null,
676
+ ): void {
677
+ if (emitSuggestingUnsupportedMutation(runtime, "setTableStyle")) {
678
+ return;
679
+ }
680
+ const context = getStoryMutationContext(runtime, "setTableStyle");
681
+ if (!context) {
682
+ return;
683
+ }
684
+
685
+ const result = applyTableStyleToDocument(
686
+ context.localDocument,
687
+ context.localSnapshot,
688
+ styleId,
689
+ );
690
+ dispatchStoryMutationResult(
691
+ runtime,
692
+ context,
693
+ {
694
+ ...result,
695
+ selection: toRuntimeSelectionSnapshot(result.selection),
696
+ },
697
+ context.timestamp,
698
+ );
699
+ }
700
+
701
+ // ---------------------------------------------------------------------------
702
+ // Paragraph layout / numbering flow
703
+ // ---------------------------------------------------------------------------
704
+
705
+ export function applyRuntimeParagraphIndentation(
706
+ runtime: WordReviewEditorRuntime,
707
+ indentation: {
708
+ left?: number;
709
+ right?: number;
710
+ firstLine?: number;
711
+ hanging?: number;
712
+ },
713
+ ): void {
714
+ if (emitSuggestingUnsupportedMutation(runtime, "setParagraphIndentation")) {
715
+ return;
716
+ }
717
+ const context = getStoryMutationContext(runtime, "setParagraphIndentation");
718
+ if (!context) {
719
+ return;
720
+ }
721
+
722
+ const result = setActiveParagraphIndentation(
723
+ context.localDocument,
724
+ context.localSnapshot,
725
+ indentation,
726
+ { timestamp: context.timestamp },
727
+ );
728
+ dispatchStoryMutationResult(
729
+ runtime,
730
+ context,
731
+ {
732
+ ...result,
733
+ selection: toRuntimeSelectionSnapshot(result.selection),
734
+ },
735
+ context.timestamp,
736
+ );
737
+ }
738
+
739
+ export function applyRuntimeParagraphTabStops(
740
+ runtime: WordReviewEditorRuntime,
741
+ tabStops: Array<{ pos: number; val?: string; leader?: string }>,
742
+ ): void {
743
+ if (emitSuggestingUnsupportedMutation(runtime, "setParagraphTabStops")) {
744
+ return;
745
+ }
746
+ const context = getStoryMutationContext(runtime, "setParagraphTabStops");
747
+ if (!context) {
748
+ return;
749
+ }
750
+
751
+ const result = setActiveParagraphTabStops(
752
+ context.localDocument,
753
+ context.localSnapshot,
754
+ tabStops,
755
+ { timestamp: context.timestamp },
756
+ );
757
+ dispatchStoryMutationResult(
758
+ runtime,
759
+ context,
760
+ {
761
+ ...result,
762
+ selection: toRuntimeSelectionSnapshot(result.selection),
763
+ },
764
+ context.timestamp,
765
+ );
766
+ }
767
+
768
+ export function applyRuntimeNumberingFlow(
769
+ runtime: WordReviewEditorRuntime,
770
+ operation: { type: "restart"; startAt?: number } | { type: "continue" },
771
+ ): void {
772
+ if (
773
+ emitSuggestingUnsupportedMutation(
774
+ runtime,
775
+ operation.type === "restart" ? "restartNumbering" : "continueNumbering",
776
+ )
777
+ ) {
778
+ return;
779
+ }
780
+ const context = getStoryMutationContext(
781
+ runtime,
782
+ operation.type === "restart" ? "restartNumbering" : "continueNumbering",
783
+ );
784
+ if (!context) {
785
+ return;
786
+ }
787
+
788
+ const paragraphContext = resolveActiveParagraphContext(context.localSnapshot);
789
+ if (!paragraphContext?.paragraph.numbering) {
790
+ return;
791
+ }
792
+
793
+ const result =
794
+ operation.type === "restart"
795
+ ? restartListNumbering(
796
+ context.localDocument,
797
+ paragraphContext.paragraphIndex,
798
+ { timestamp: context.timestamp },
799
+ operation.startAt,
800
+ )
801
+ : continueListNumbering(
802
+ context.localDocument,
803
+ paragraphContext.paragraphIndex,
804
+ { timestamp: context.timestamp },
805
+ );
806
+
807
+ dispatchStoryMutationResult(
808
+ runtime,
809
+ context,
810
+ {
811
+ changed: result.affectedParagraphIndexes.length > 0,
812
+ document: result.document,
813
+ selection: toRuntimeSelectionSnapshot(context.localSnapshot.selection),
814
+ },
815
+ context.timestamp,
816
+ );
817
+ }
818
+
819
+ // ---------------------------------------------------------------------------
820
+ // Section break / section layout
821
+ // ---------------------------------------------------------------------------
822
+
823
+ export function applyRuntimeInsertSectionBreak(
824
+ runtime: WordReviewEditorRuntime,
825
+ breakType: SectionBreakType,
826
+ options?: { afterSectionIndex?: number },
827
+ ): void {
828
+ const snapshot = runtime.getRenderSnapshot();
829
+ if (!canApplyRuntimeMutation(snapshot) || snapshot.activeStory.kind !== "main") {
830
+ return;
831
+ }
832
+ if (emitWorkflowBlockedMutation(runtime, "insertSectionBreak")) {
833
+ return;
834
+ }
835
+ if (isSelectionSuggesting(runtime)) {
836
+ runtime.emitBlockedCommand("insertSectionBreak", [{
837
+ code: "unsupported_surface",
838
+ message: "Section break insertion is not supported in suggesting mode.",
839
+ }]);
840
+ return;
841
+ }
842
+
843
+ const sessionState = runtime.getSessionState();
844
+ const timestamp = new Date().toISOString();
845
+ const result =
846
+ typeof options?.afterSectionIndex === "number"
847
+ ? insertSectionBreakAfterSectionIndex(
848
+ sessionState.canonicalDocument,
849
+ options.afterSectionIndex,
850
+ breakType,
851
+ { timestamp },
852
+ )
853
+ : insertSectionBreakAfterSectionIndex(
854
+ sessionState.canonicalDocument,
855
+ runtime.getDocumentNavigationSnapshot().activeSectionIndex,
856
+ breakType,
857
+ { timestamp },
858
+ );
859
+
860
+ dispatchRuntimeDocumentMutation(
861
+ runtime,
862
+ {
863
+ changed: result.changed,
864
+ document: result.document,
865
+ selection: toRuntimeSelectionSnapshot(result.selection),
866
+ },
867
+ timestamp,
868
+ );
869
+ }
870
+
871
+ export function applyRuntimeDeleteSectionBreak(
872
+ runtime: WordReviewEditorRuntime,
873
+ sectionIndex: number,
874
+ ): void {
875
+ const snapshot = runtime.getRenderSnapshot();
876
+ if (!canApplyRuntimeMutation(snapshot) || snapshot.activeStory.kind !== "main") {
877
+ return;
878
+ }
879
+ if (emitWorkflowBlockedMutation(runtime, "deleteSectionBreak")) {
880
+ return;
881
+ }
882
+ if (isSelectionSuggesting(runtime)) {
883
+ runtime.emitBlockedCommand("deleteSectionBreak", [{
884
+ code: "unsupported_surface",
885
+ message: "Section break deletion is not supported in suggesting mode.",
886
+ }]);
887
+ return;
888
+ }
889
+
890
+ const sessionState = runtime.getSessionState();
891
+ const timestamp = new Date().toISOString();
892
+ const result = deleteSectionBreakAtSectionIndex(
893
+ sessionState.canonicalDocument,
894
+ sectionIndex,
895
+ { timestamp },
896
+ );
897
+
898
+ dispatchRuntimeDocumentMutation(
899
+ runtime,
900
+ {
901
+ changed: result.changed,
902
+ document: result.document,
903
+ selection: toRuntimeSelectionSnapshot(result.selection),
904
+ },
905
+ timestamp,
906
+ );
907
+ }
908
+
909
+ export function applyRuntimeUpdateSectionLayout(
910
+ runtime: WordReviewEditorRuntime,
911
+ sectionIndex: number,
912
+ patch: SectionLayoutPatch,
913
+ ): void {
914
+ const snapshot = runtime.getRenderSnapshot();
915
+ if (!canApplyRuntimeMutation(snapshot) || snapshot.activeStory.kind !== "main") {
916
+ return;
917
+ }
918
+ if (emitWorkflowBlockedMutation(runtime, "updateSectionLayout")) {
919
+ return;
920
+ }
921
+ if (isSelectionSuggesting(runtime)) {
922
+ runtime.emitBlockedCommand("updateSectionLayout", [{
923
+ code: "unsupported_surface",
924
+ message: "Section layout updates are not supported in suggesting mode.",
925
+ }]);
926
+ return;
927
+ }
928
+
929
+ const sessionState = runtime.getSessionState();
930
+ const timestamp = new Date().toISOString();
931
+ const result = updateSectionLayoutAtSectionIndex(
932
+ sessionState.canonicalDocument,
933
+ sectionIndex,
934
+ {
935
+ ...(patch.pageSize ? { pageSize: patch.pageSize } : {}),
936
+ ...(patch.pageMargins ? { pageMargins: patch.pageMargins } : {}),
937
+ ...(patch.columns ? { columns: patch.columns } : {}),
938
+ ...(patch.titlePage !== undefined ? { titlePage: patch.titlePage } : {}),
939
+ ...(patch.sectionType ? { sectionType: patch.sectionType } : {}),
940
+ },
941
+ { timestamp },
942
+ );
943
+
944
+ dispatchRuntimeDocumentMutation(
945
+ runtime,
946
+ {
947
+ changed: result.changed,
948
+ document: result.document,
949
+ selection: toRuntimeSelectionSnapshot(result.selection),
950
+ },
951
+ timestamp,
952
+ );
953
+ }
954
+
955
+ export function applyRuntimeSetSectionPageNumbering(
956
+ runtime: WordReviewEditorRuntime,
957
+ sectionIndex: number,
958
+ patch: SectionPageNumberingPatch | null,
959
+ ): void {
960
+ const snapshot = runtime.getRenderSnapshot();
961
+ if (!canApplyRuntimeMutation(snapshot) || snapshot.activeStory.kind !== "main") {
962
+ return;
963
+ }
964
+ if (emitWorkflowBlockedMutation(runtime, "setSectionPageNumbering")) {
965
+ return;
966
+ }
967
+ if (isSelectionSuggesting(runtime)) {
968
+ runtime.emitBlockedCommand("setSectionPageNumbering", [{
969
+ code: "unsupported_surface",
970
+ message: "Section page numbering updates are not supported in suggesting mode.",
971
+ }]);
972
+ return;
973
+ }
974
+
975
+ const sessionState = runtime.getSessionState();
976
+ const timestamp = new Date().toISOString();
977
+ const normalizedPatch =
978
+ patch === null
979
+ ? null
980
+ : {
981
+ ...(patch.format !== undefined
982
+ ? { format: patch.format ?? undefined }
983
+ : {}),
984
+ ...(patch.start !== undefined
985
+ ? { start: patch.start ?? undefined }
986
+ : {}),
987
+ ...(patch.chapterStyle !== undefined
988
+ ? { chapStyle: patch.chapterStyle ?? undefined }
989
+ : {}),
990
+ ...(patch.chapterSeparator !== undefined
991
+ ? { chapSep: patch.chapterSeparator ?? undefined }
992
+ : {}),
993
+ };
994
+ const result = setSectionPageNumberingAtSectionIndex(
995
+ sessionState.canonicalDocument,
996
+ sectionIndex,
997
+ normalizedPatch,
998
+ { timestamp },
999
+ );
1000
+
1001
+ dispatchRuntimeDocumentMutation(
1002
+ runtime,
1003
+ {
1004
+ changed: result.changed,
1005
+ document: result.document,
1006
+ selection: toRuntimeSelectionSnapshot(result.selection),
1007
+ },
1008
+ timestamp,
1009
+ );
1010
+ }
1011
+
1012
+ export function applyRuntimeSetHeaderFooterLink(
1013
+ runtime: WordReviewEditorRuntime,
1014
+ sectionIndex: number,
1015
+ patch: HeaderFooterLinkPatch,
1016
+ ): void {
1017
+ const snapshot = runtime.getRenderSnapshot();
1018
+ if (!canApplyRuntimeMutation(snapshot) || snapshot.activeStory.kind !== "main") {
1019
+ return;
1020
+ }
1021
+ if (emitWorkflowBlockedMutation(runtime, "setHeaderFooterLink")) {
1022
+ return;
1023
+ }
1024
+ if (isSelectionSuggesting(runtime)) {
1025
+ runtime.emitBlockedCommand("setHeaderFooterLink", [{
1026
+ code: "unsupported_surface",
1027
+ message: "Header and footer linkage updates are not supported in suggesting mode.",
1028
+ }]);
1029
+ return;
1030
+ }
1031
+
1032
+ const sessionState = runtime.getSessionState();
1033
+ const timestamp = new Date().toISOString();
1034
+ const result = setHeaderFooterLinkAtSectionIndex(
1035
+ sessionState.canonicalDocument,
1036
+ sectionIndex,
1037
+ patch,
1038
+ { timestamp },
1039
+ );
1040
+
1041
+ dispatchRuntimeDocumentMutation(
1042
+ runtime,
1043
+ {
1044
+ changed: result.changed,
1045
+ document: result.document,
1046
+ selection: toRuntimeSelectionSnapshot(result.selection),
1047
+ },
1048
+ timestamp,
1049
+ );
1050
+ }
1051
+
1052
+ // ---------------------------------------------------------------------------
1053
+ // Page break / table insert / image insert + resize + reposition
1054
+ // ---------------------------------------------------------------------------
1055
+
1056
+ export function applyRuntimeInsertPageBreak(runtime: WordReviewEditorRuntime): void {
1057
+ if (isSelectionSuggesting(runtime)) {
1058
+ runtime.emitBlockedCommand("insertPageBreak", [{
1059
+ code: "unsupported_surface",
1060
+ message: "Page break insertion is not supported in suggesting mode.",
1061
+ }]);
1062
+ return;
1063
+ }
1064
+ const context = getStoryMutationContext(runtime, "insertPageBreak");
1065
+ if (!context) {
1066
+ return;
1067
+ }
1068
+
1069
+ const result = insertPageBreakInDocument(
1070
+ context.localDocument,
1071
+ toRuntimeSelectionSnapshot(context.localSnapshot.selection),
1072
+ { timestamp: context.timestamp },
1073
+ );
1074
+ dispatchStoryMutationResult(runtime, context, result, context.timestamp);
1075
+ }
1076
+
1077
+ export function applyRuntimeInsertTable(
1078
+ runtime: WordReviewEditorRuntime,
1079
+ options: InsertTableOptions,
1080
+ ): void {
1081
+ if (isSelectionSuggesting(runtime)) {
1082
+ runtime.emitBlockedCommand("insertTable", [{
1083
+ code: "unsupported_surface",
1084
+ message: "Table insertion is not supported in suggesting mode.",
1085
+ }]);
1086
+ return;
1087
+ }
1088
+ const context = getStoryMutationContext(runtime, "insertTable");
1089
+ if (!context) {
1090
+ return;
1091
+ }
1092
+
1093
+ const result = insertTableInDocument(
1094
+ context.localDocument,
1095
+ toRuntimeSelectionSnapshot(context.localSnapshot.selection),
1096
+ options,
1097
+ { timestamp: context.timestamp },
1098
+ );
1099
+ dispatchStoryMutationResult(runtime, context, result, context.timestamp);
1100
+ }
1101
+
1102
+ export function applyRuntimeInsertImage(
1103
+ runtime: WordReviewEditorRuntime,
1104
+ options: InsertImageOptions,
1105
+ ): void {
1106
+ if (isSelectionSuggesting(runtime)) {
1107
+ runtime.emitBlockedCommand("insertImage", [{
1108
+ code: "unsupported_surface",
1109
+ message: "Image insertion is not supported in suggesting mode.",
1110
+ }]);
1111
+ return;
1112
+ }
1113
+ const context = getStoryMutationContext(runtime, "insertImage");
1114
+ if (!context) {
1115
+ return;
1116
+ }
1117
+
1118
+ try {
1119
+ const result = insertImageInDocument(
1120
+ context.localDocument,
1121
+ toRuntimeSelectionSnapshot(context.localSnapshot.selection),
1122
+ options.data,
1123
+ options.mimeType,
1124
+ options.width,
1125
+ options.height,
1126
+ {
1127
+ timestamp: context.timestamp,
1128
+ altText: options.altText,
1129
+ },
1130
+ );
1131
+ dispatchStoryMutationResult(runtime, context, {
1132
+ changed: true,
1133
+ document: result.document,
1134
+ selection: result.selection,
1135
+ mapping: result.mapping,
1136
+ }, context.timestamp);
1137
+ } catch {
1138
+ return;
1139
+ }
1140
+ }
1141
+
1142
+ export function applyRuntimeImageResize(
1143
+ runtime: WordReviewEditorRuntime,
1144
+ mediaId: string,
1145
+ dimensions: { widthEmu: number; heightEmu: number },
1146
+ ): void {
1147
+ const snapshot = runtime.getRenderSnapshot();
1148
+ if (!canApplyRuntimeMutation(snapshot)) {
1149
+ return;
1150
+ }
1151
+ if (emitWorkflowBlockedMutation(runtime, "setImageLayout")) {
1152
+ return;
1153
+ }
1154
+ if (isSelectionSuggesting(runtime)) {
1155
+ runtime.emitBlockedCommand("setImageLayout", [{
1156
+ code: "unsupported_surface",
1157
+ message: "Image resize is not supported in suggesting mode.",
1158
+ }]);
1159
+ return;
1160
+ }
1161
+
1162
+ try {
1163
+ const sessionState = runtime.getSessionState();
1164
+ const result = resizeImageInCatalog(
1165
+ sessionState.canonicalDocument,
1166
+ mediaId,
1167
+ dimensions,
1168
+ );
1169
+ runtime.dispatch({
1170
+ type: "document.replace",
1171
+ document: result.document,
1172
+ selection: toRuntimeSelectionSnapshot(snapshot.selection),
1173
+ origin: { source: "api", timestamp: new Date().toISOString() },
1174
+ });
1175
+ } catch {
1176
+ return;
1177
+ }
1178
+ }
1179
+
1180
+ export function applyRuntimeImageReposition(
1181
+ runtime: WordReviewEditorRuntime,
1182
+ mediaId: string,
1183
+ offsets: { horizontalOffsetEmu?: number; verticalOffsetEmu?: number },
1184
+ ): void {
1185
+ if (emitWorkflowBlockedMutation(runtime, "setImageFrame")) {
1186
+ return;
1187
+ }
1188
+ if (isSelectionSuggesting(runtime)) {
1189
+ runtime.emitBlockedCommand("setImageFrame", [{
1190
+ code: "unsupported_surface",
1191
+ message: "Image reposition is not supported in suggesting mode.",
1192
+ }]);
1193
+ return;
1194
+ }
1195
+ const context = getStoryMutationContext(runtime, "setImageFrame");
1196
+ if (!context) {
1197
+ return;
1198
+ }
1199
+
1200
+ try {
1201
+ const result = repositionFloatingImageInDocument(
1202
+ context.localDocument,
1203
+ mediaId,
1204
+ offsets,
1205
+ context.timestamp,
1206
+ );
1207
+ dispatchStoryMutationResult(
1208
+ runtime,
1209
+ context,
1210
+ {
1211
+ changed: true,
1212
+ document: result.document,
1213
+ selection: toRuntimeSelectionSnapshot(context.localSnapshot.selection),
1214
+ },
1215
+ context.timestamp,
1216
+ );
1217
+ } catch {
1218
+ return;
1219
+ }
1220
+ }
1221
+
1222
+ // ---------------------------------------------------------------------------
1223
+ // Table structure dispatch
1224
+ // ---------------------------------------------------------------------------
1225
+
1226
+ export function applyRuntimeTableStructureOperation(
1227
+ runtime: WordReviewEditorRuntime,
1228
+ mountedSurface: TwProseMirrorSurfaceRef | null | undefined,
1229
+ operation: TableStructureOperation,
1230
+ ): { changed: boolean; coercedReason: string | null } {
1231
+ if (isSelectionSuggesting(runtime)) {
1232
+ const coercedReason = `Table operation "${operation.type}" is not supported in suggesting mode.`;
1233
+ runtime.emitBlockedCommand(`table.${operation.type}`, [{
1234
+ code: "unsupported_surface",
1235
+ message: coercedReason,
1236
+ }]);
1237
+ return { changed: false, coercedReason };
1238
+ }
1239
+ const context = getStoryMutationContext(runtime, `table.${operation.type}`);
1240
+ if (!context) {
1241
+ return { changed: false, coercedReason: "No active mutation context." };
1242
+ }
1243
+
1244
+ const result = applyTableStructureOperationInDocument(
1245
+ context.localDocument,
1246
+ context.localSnapshot,
1247
+ mountedSurface?.getTableSelection() ?? null,
1248
+ operation,
1249
+ );
1250
+ dispatchStoryMutationResult(runtime, context, result, context.timestamp);
1251
+ return {
1252
+ changed: result.changed,
1253
+ coercedReason: result.changed ? null : "Op was a no-op against the active selection.",
1254
+ };
1255
+ }
1256
+
1257
+ /**
1258
+ * Translate a public-API `TableOp` (kebab-case `kind` discriminator)
1259
+ * to the internal `TableStructureOperation` (`type` discriminator).
1260
+ * The shape values are identical aside from the discriminator name.
1261
+ */
1262
+ export function publicTableOpToInternal(op: TableOp): TableStructureOperation {
1263
+ const { kind, ...rest } = op as { kind: string } & Record<string, unknown>;
1264
+ if (kind === "insert") {
1265
+ throw new Error(
1266
+ "TableOp kind \"insert\" is not routed through ref.tables.apply; use ref.insertTable(...).",
1267
+ );
1268
+ }
1269
+ return { type: kind, ...rest } as TableStructureOperation;
1270
+ }
1271
+
1272
+ // ---------------------------------------------------------------------------
1273
+ // Selection routing + paragraph resolution
1274
+ // ---------------------------------------------------------------------------
1275
+
1276
+ export function applyRuntimeSelection(
1277
+ runtime: WordReviewEditorRuntime,
1278
+ selection: PublicSelectionSnapshot,
1279
+ ): void {
1280
+ const requestedStory = selection.storyTarget ?? { kind: "main" };
1281
+ if (requestedStory.kind === "main") {
1282
+ runtime.closeStory();
1283
+ } else if (!storyTargetsEqual(runtime.getActiveStory(), requestedStory)) {
1284
+ if (!runtime.openStory(requestedStory)) {
1285
+ return;
1286
+ }
1287
+ }
1288
+
1289
+ runtime.dispatch({
1290
+ type: "selection.set",
1291
+ selection: toRuntimeSelectionSnapshot(stripStoryTarget(selection)),
1292
+ });
1293
+ }
1294
+
1295
+ function resolveActiveParagraphContext(
1296
+ snapshot: Pick<RuntimeRenderSnapshot, "surface" | "selection">,
1297
+ ): {
1298
+ paragraphIndex: number;
1299
+ paragraph: Extract<SurfaceBlockSnapshot, { kind: "paragraph" }>;
1300
+ atParagraphStart: boolean;
1301
+ isEmpty: boolean;
1302
+ } | null {
1303
+ if (!snapshot.surface) {
1304
+ return null;
1305
+ }
1306
+
1307
+ const paragraphIndex = resolveActiveParagraphIndex(
1308
+ snapshot.surface.blocks,
1309
+ snapshot.selection,
1310
+ );
1311
+ if (paragraphIndex === null) {
1312
+ return null;
1313
+ }
1314
+
1315
+ const selectionPosition =
1316
+ snapshot.selection.activeRange.kind === "node"
1317
+ ? snapshot.selection.activeRange.at
1318
+ : snapshot.selection.head;
1319
+ const paragraph = findSurfaceParagraphAtPosition(snapshot.surface.blocks, selectionPosition);
1320
+ if (!paragraph) {
1321
+ return null;
1322
+ }
1323
+
1324
+ return {
1325
+ paragraphIndex,
1326
+ paragraph,
1327
+ atParagraphStart:
1328
+ snapshot.selection.isCollapsed &&
1329
+ snapshot.selection.activeRange.kind !== "node" &&
1330
+ snapshot.selection.anchor === snapshot.selection.head &&
1331
+ snapshot.selection.head === paragraph.from,
1332
+ isEmpty: isSurfaceParagraphEmpty(paragraph),
1333
+ };
1334
+ }
1335
+
1336
+ function findSurfaceParagraphAtPosition(
1337
+ blocks: readonly SurfaceBlockSnapshot[],
1338
+ position: number,
1339
+ ): Extract<SurfaceBlockSnapshot, { kind: "paragraph" }> | null {
1340
+ for (const block of blocks) {
1341
+ if (position < block.from || position > block.to) {
1342
+ continue;
1343
+ }
1344
+ if (block.kind === "paragraph") {
1345
+ return block;
1346
+ }
1347
+ if (block.kind === "table") {
1348
+ for (const row of block.rows) {
1349
+ for (const cell of row.cells) {
1350
+ const paragraph = findSurfaceParagraphAtPosition(cell.content, position);
1351
+ if (paragraph) {
1352
+ return paragraph;
1353
+ }
1354
+ }
1355
+ }
1356
+ continue;
1357
+ }
1358
+ if (block.kind === "sdt_block") {
1359
+ const paragraph = findSurfaceParagraphAtPosition(block.children, position);
1360
+ if (paragraph) {
1361
+ return paragraph;
1362
+ }
1363
+ }
1364
+ }
1365
+ return null;
1366
+ }
1367
+
1368
+ function isSurfaceParagraphEmpty(
1369
+ paragraph: Extract<SurfaceBlockSnapshot, { kind: "paragraph" }>,
1370
+ ): boolean {
1371
+ if (paragraph.segments.length === 0) {
1372
+ return true;
1373
+ }
1374
+ return paragraph.segments.every((segment) => segment.kind === "text" && segment.text.length === 0);
1375
+ }
1376
+
1377
+ // ---------------------------------------------------------------------------
1378
+ // Shared dispatch helpers
1379
+ // ---------------------------------------------------------------------------
1380
+
1381
+ function canApplyRuntimeMutation(snapshot: RuntimeRenderSnapshot): boolean {
1382
+ return snapshot.isReady && !snapshot.readOnly && !snapshot.fatalError;
1383
+ }
1384
+
1385
+ function emitWorkflowBlockedMutation(
1386
+ runtime: WordReviewEditorRuntime,
1387
+ command: string,
1388
+ ): boolean {
1389
+ const interactionGuardSnapshot = runtime.getInteractionGuardSnapshot();
1390
+ if (interactionGuardSnapshot.blockedReasons.length === 0) {
1391
+ return false;
1392
+ }
1393
+ runtime.emitBlockedCommand(command, interactionGuardSnapshot.blockedReasons);
1394
+ return true;
1395
+ }
1396
+
1397
+ function getStoryMutationContext(
1398
+ runtime: WordReviewEditorRuntime,
1399
+ command?: string,
1400
+ ): {
1401
+ timestamp: string;
1402
+ activeStory: EditorStoryTarget;
1403
+ persistedDocument: EditorSessionState["canonicalDocument"];
1404
+ localDocument: EditorSessionState["canonicalDocument"];
1405
+ localSnapshot: RuntimeRenderSnapshot;
1406
+ } | null {
1407
+ const snapshot = runtime.getRenderSnapshot();
1408
+ if (!canApplyRuntimeMutation(snapshot)) {
1409
+ return null;
1410
+ }
1411
+ if (command && emitWorkflowBlockedMutation(runtime, command)) {
1412
+ return null;
1413
+ }
1414
+
1415
+ const persistedDocument = runtime.getSessionState().canonicalDocument;
1416
+ const activeStory = snapshot.activeStory;
1417
+ if (activeStory.kind === "main") {
1418
+ return {
1419
+ timestamp: new Date().toISOString(),
1420
+ activeStory,
1421
+ persistedDocument,
1422
+ localDocument: persistedDocument,
1423
+ localSnapshot: snapshot,
1424
+ };
1425
+ }
1426
+
1427
+ return {
1428
+ timestamp: new Date().toISOString(),
1429
+ activeStory,
1430
+ persistedDocument,
1431
+ localDocument: {
1432
+ ...persistedDocument,
1433
+ content: {
1434
+ type: "doc",
1435
+ children: [...getStoryBlocks(persistedDocument, activeStory)],
1436
+ },
1437
+ },
1438
+ localSnapshot: {
1439
+ ...snapshot,
1440
+ activeStory: { kind: "main" },
1441
+ selection: stripStoryTarget(snapshot.selection),
1442
+ },
1443
+ };
1444
+ }
1445
+
1446
+ function dispatchStoryMutationResult(
1447
+ runtime: WordReviewEditorRuntime,
1448
+ context: {
1449
+ activeStory: EditorStoryTarget;
1450
+ persistedDocument: EditorSessionState["canonicalDocument"];
1451
+ },
1452
+ result: {
1453
+ changed: boolean;
1454
+ document: EditorSessionState["canonicalDocument"];
1455
+ selection: InternalSelectionSnapshot;
1456
+ mapping?: TransactionMapping;
1457
+ },
1458
+ timestamp: string,
1459
+ ): void {
1460
+ if (context.activeStory.kind === "main") {
1461
+ dispatchRuntimeDocumentMutation(runtime, result, timestamp);
1462
+ return;
1463
+ }
1464
+
1465
+ if (!result.changed) {
1466
+ return;
1467
+ }
1468
+
1469
+ const nextDocument = replaceStoryBlocks(
1470
+ context.persistedDocument,
1471
+ context.activeStory,
1472
+ result.document.content.children,
1473
+ );
1474
+ dispatchRuntimeDocumentMutation(
1475
+ runtime,
1476
+ {
1477
+ changed: true,
1478
+ document: nextDocument,
1479
+ selection: result.selection,
1480
+ },
1481
+ timestamp,
1482
+ );
1483
+ }
1484
+
1485
+ function dispatchRuntimeDocumentMutation(
1486
+ runtime: WordReviewEditorRuntime,
1487
+ result: {
1488
+ changed: boolean;
1489
+ document: EditorSessionState["canonicalDocument"];
1490
+ selection: InternalSelectionSnapshot;
1491
+ mapping?: TransactionMapping;
1492
+ },
1493
+ timestamp: string,
1494
+ ): void {
1495
+ if (!result.changed) {
1496
+ return;
1497
+ }
1498
+
1499
+ runtime.dispatch({
1500
+ type: "document.replace",
1501
+ document: {
1502
+ ...result.document,
1503
+ updatedAt: timestamp,
1504
+ },
1505
+ mapping: result.mapping,
1506
+ selection: result.selection,
1507
+ origin: {
1508
+ source: "api",
1509
+ timestamp,
1510
+ },
1511
+ });
1512
+ }
1513
+
1514
+ function isSelectionSuggesting(runtime: WordReviewEditorRuntime): boolean {
1515
+ return runtime.getInteractionGuardSnapshot().effectiveMode === "suggest";
1516
+ }
1517
+
1518
+ function stripStoryTarget(
1519
+ selection: PublicSelectionSnapshot,
1520
+ ): PublicSelectionSnapshot {
1521
+ const { storyTarget: _storyTarget, ...rest } = selection;
1522
+ return rest;
1523
+ }
1524
+
1525
+ // ---------------------------------------------------------------------------
1526
+ // Comment deletion
1527
+ // ---------------------------------------------------------------------------
1528
+
1529
+ export function applyRuntimeDeleteComment(
1530
+ runtime: WordReviewEditorRuntime,
1531
+ commentId: string,
1532
+ ): void {
1533
+ const snapshot = runtime.getRenderSnapshot();
1534
+ // Pre-ready / fatal states stay silent: the host called too early, and
1535
+ // there is no meaningful document yet to signal against. Emitting a
1536
+ // warning here would only add noise to load-time error handling.
1537
+ if (!snapshot.isReady || snapshot.fatalError) {
1538
+ return;
1539
+ }
1540
+ if (snapshot.readOnly) {
1541
+ runtime.emitTransientWarning({
1542
+ warningId: `delete-comment-readonly-${commentId}-${Date.now()}`,
1543
+ code: "review_target_not_found",
1544
+ severity: "info",
1545
+ message: `deleteComment("${commentId}") skipped: editor is read-only.`,
1546
+ source: "review",
1547
+ details: { op: "deleteComment", targetId: commentId, reason: "read_only" },
1548
+ });
1549
+ return;
1550
+ }
1551
+
1552
+ const sessionState = runtime.getSessionState();
1553
+ if (!sessionState.canonicalDocument.review.comments[commentId]) {
1554
+ runtime.emitTransientWarning({
1555
+ warningId: `delete-comment-unknown-${commentId}-${Date.now()}`,
1556
+ code: "review_target_not_found",
1557
+ severity: "info",
1558
+ message: `deleteComment("${commentId}") skipped: unknown commentId.`,
1559
+ source: "review",
1560
+ details: { op: "deleteComment", targetId: commentId, reason: "comment_unknown" },
1561
+ });
1562
+ return;
1563
+ }
1564
+
1565
+ const nextComments = {
1566
+ ...sessionState.canonicalDocument.review.comments,
1567
+ };
1568
+ delete nextComments[commentId];
1569
+
1570
+ runtime.dispatch({
1571
+ type: "document.replace",
1572
+ document: {
1573
+ ...sessionState.canonicalDocument,
1574
+ review: {
1575
+ ...sessionState.canonicalDocument.review,
1576
+ comments: nextComments,
1577
+ },
1578
+ },
1579
+ selection: toRuntimeSelectionSnapshot(snapshot.selection),
1580
+ origin: {
1581
+ source: "api",
1582
+ timestamp: new Date().toISOString(),
1583
+ },
1584
+ });
1585
+ }
1586
+
1587
+ // ---------------------------------------------------------------------------
1588
+ // Public-selection construction utilities
1589
+ // ---------------------------------------------------------------------------
1590
+
1591
+ export function normalizeRequestedSelection(
1592
+ snapshot: RuntimeRenderSnapshot,
1593
+ selection: PublicSelectionSnapshot | null,
1594
+ ): PublicSelectionSnapshot {
1595
+ return (
1596
+ selection ??
1597
+ createCollapsedPublicSelection(
1598
+ snapshot.selection.head,
1599
+ snapshot.activeStory.kind === "main" ? undefined : snapshot.activeStory,
1600
+ )
1601
+ );
1602
+ }
1603
+
1604
+ export function createCollapsedPublicSelection(
1605
+ position: number,
1606
+ storyTarget?: EditorStoryTarget,
1607
+ ): PublicSelectionSnapshot {
1608
+ return {
1609
+ anchor: position,
1610
+ head: position,
1611
+ isCollapsed: true,
1612
+ activeRange: {
1613
+ kind: "range",
1614
+ from: position,
1615
+ to: position,
1616
+ assoc: {
1617
+ start: -1,
1618
+ end: 1,
1619
+ },
1620
+ },
1621
+ ...(storyTarget ? { storyTarget } : {}),
1622
+ };
1623
+ }
1624
+
1625
+ export function createSelectionFromAnchor(
1626
+ anchor: PublicSelectionSnapshot["activeRange"],
1627
+ storyTarget?: EditorStoryTarget,
1628
+ ): PublicSelectionSnapshot {
1629
+ switch (anchor.kind) {
1630
+ case "range":
1631
+ return {
1632
+ anchor: anchor.from,
1633
+ head: anchor.to,
1634
+ isCollapsed: anchor.from === anchor.to,
1635
+ activeRange: anchor,
1636
+ ...(storyTarget ? { storyTarget } : {}),
1637
+ };
1638
+ case "node":
1639
+ return {
1640
+ anchor: anchor.at,
1641
+ head: anchor.at,
1642
+ isCollapsed: true,
1643
+ activeRange: anchor,
1644
+ ...(storyTarget ? { storyTarget } : {}),
1645
+ };
1646
+ case "detached":
1647
+ return {
1648
+ anchor: anchor.lastKnownRange.from,
1649
+ head: anchor.lastKnownRange.to,
1650
+ isCollapsed: anchor.lastKnownRange.from === anchor.lastKnownRange.to,
1651
+ activeRange: anchor,
1652
+ ...(storyTarget ? { storyTarget } : {}),
1653
+ };
1654
+ }
1655
+ }
1656
+
1657
+ // ---------------------------------------------------------------------------
1658
+ // Internal <-> public selection conversion
1659
+ // ---------------------------------------------------------------------------
1660
+
1661
+ export function toRuntimeSelectionSnapshot(selection: PublicSelectionSnapshot) {
1662
+ return {
1663
+ anchor: selection.anchor,
1664
+ head: selection.head,
1665
+ isCollapsed: selection.isCollapsed,
1666
+ activeRange:
1667
+ selection.activeRange.kind === "range"
1668
+ ? createRangeAnchor(
1669
+ selection.activeRange.from,
1670
+ selection.activeRange.to,
1671
+ selection.activeRange.assoc,
1672
+ )
1673
+ : selection.activeRange.kind === "node"
1674
+ ? createNodeAnchor(selection.activeRange.at, selection.activeRange.assoc)
1675
+ : createDetachedAnchor(
1676
+ selection.activeRange.lastKnownRange,
1677
+ selection.activeRange.reason,
1678
+ ),
1679
+ };
1680
+ }
1681
+
1682
+ // ---------------------------------------------------------------------------
1683
+ // Shared dispatch-context handle for the text-command bridge.
1684
+ //
1685
+ // `dispatchTextCommand` (in `src/runtime/edit-dispatch/`) expects a
1686
+ // `DispatchContext` with the four mutation primitives it needs to rewrite
1687
+ // the canonical document on text events. WRE used to build this inline —
1688
+ // it now imports the same const from the shell module so both surfaces
1689
+ // dispatch through one codepath.
1690
+ // ---------------------------------------------------------------------------
1691
+
1692
+ export const DISPATCH_CONTEXT: DispatchContext = {
1693
+ getStoryMutationContext,
1694
+ dispatchStoryMutationResult,
1695
+ resolveActiveParagraphContext,
1696
+ toRuntimeSelectionSnapshot,
1697
+ };