@beyondwork/docx-react-component 1.0.66 → 1.0.69

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 -931
  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 -4795
  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
@@ -85,83 +85,53 @@ import {
85
85
  persistedSnapshotFromEditorSessionState,
86
86
  } from "../api/session-state.ts";
87
87
  import {
88
- createDetachedAnchor,
89
- createNodeAnchor,
90
- createRangeAnchor,
91
88
  storyTargetsEqual,
92
- type TransactionMapping,
93
- } from "../core/selection/mapping.ts";
94
- import {
95
- applyFormattingOperationToDocument,
96
89
  getFormattingStateFromRenderSnapshot,
97
- } from "../core/commands/formatting-commands.ts";
98
- import {
99
- applyParagraphStyleToDocument,
100
- applyTableStyleToDocument,
101
- } from "../core/commands/style-commands.ts";
102
- import {
103
- continueNumbering as continueListNumbering,
104
- restartNumbering as restartListNumbering,
105
- toggleBulletedList,
106
- toggleNumberedList,
107
- } from "../core/commands/list-commands.ts";
108
- import {
109
90
  dispatchTextCommand,
110
- type DispatchContext,
111
91
  type DispatchTextCommand,
112
- } from "../runtime/edit-dispatch/index.ts";
113
- import {
114
- resolveActiveParagraphIndex,
115
- setActiveParagraphIndentation,
116
- setActiveParagraphTabStops,
117
- } from "../core/commands/paragraph-layout-commands.ts";
118
- import {
119
- deleteSectionBreakAtSectionIndex,
120
- insertSectionBreakAfterSectionIndex,
121
- setHeaderFooterLinkAtSectionIndex,
122
- setSectionPageNumberingAtSectionIndex,
123
- updateSectionLayoutAtSectionIndex,
124
- } from "../core/commands/section-layout-commands.ts";
125
- import {
126
- insertImage as insertImageInDocument,
127
- resizeImage as resizeImageInCatalog,
128
- repositionFloatingImage as repositionFloatingImageInDocument,
129
- } from "../core/commands/image-commands.ts";
130
- import {
131
- applyTableStructureOperation,
132
92
  getTableStructureContext,
133
- type TableStructureOperation,
134
- } from "../core/commands/table-structure-commands.ts";
135
- import {
136
- deleteSelectionOrBackward,
137
- deleteSelectionOrForward,
138
- insertHardBreak as insertHardBreakInDocument,
139
- insertPageBreak as insertPageBreakInDocument,
140
- insertTab as insertTabInDocument,
141
- insertText as insertTextInDocument,
142
- insertTable as insertTableInDocument,
143
- splitParagraph as splitParagraphInDocument,
144
- } from "../core/commands/text-commands.ts";
145
- import { type SelectionSnapshot as InternalSelectionSnapshot } from "../core/state/editor-state.ts";
146
- import {
147
93
  getStoryBlocks,
148
- replaceStoryBlocks,
149
- } from "../runtime/story-targeting.ts";
94
+ } from "../shell/ref-utilities.ts";
150
95
  import {
151
- decodePersistedSourcePackageBytes,
152
- hasValidPersistedSourcePackageDigest,
153
- } from "../io/source-package-provenance.ts";
154
- import { readOpcPackage } from "../io/opc/package-reader.ts";
155
- import { deriveCapabilities } from "../runtime/session-capabilities";
156
- import { findTextMatches, searchDocument } from "../runtime/document-search.ts";
96
+ DISPATCH_CONTEXT,
97
+ applyRuntimeDeleteComment,
98
+ applyRuntimeDeleteSectionBreak,
99
+ applyRuntimeFormattingOperation,
100
+ applyRuntimeImageReposition,
101
+ applyRuntimeImageResize,
102
+ applyRuntimeInsertImage,
103
+ applyRuntimeInsertPageBreak,
104
+ applyRuntimeInsertSectionBreak,
105
+ applyRuntimeInsertTable,
106
+ applyRuntimeListToggle,
107
+ applyRuntimeNumberingFlow,
108
+ applyRuntimeParagraphIndentation,
109
+ applyRuntimeParagraphStyle,
110
+ applyRuntimeParagraphTabStops,
111
+ applyRuntimeSelection,
112
+ applyRuntimeSetHeaderFooterLink,
113
+ applyRuntimeSetSectionPageNumbering,
114
+ applyRuntimeTableStructureOperation,
115
+ applyRuntimeTableStyle,
116
+ applyRuntimeUpdateSectionLayout,
117
+ applySuggestionGroupAction,
118
+ createCollapsedPublicSelection,
119
+ createSelectionFromAnchor,
120
+ getRuntimeStyleCatalog,
121
+ normalizeRequestedSelection,
122
+ publicTableOpToInternal,
123
+ toRuntimeSelectionSnapshot,
124
+ type TableStructureOperation,
125
+ } from "../shell/ref-commands.ts";
126
+ import { buildMediaPreviews } from "../shell/media-previews.ts";
157
127
  import {
128
+ deriveCapabilities,
158
129
  resolveCurrentContextAnalyticsQuery,
159
130
  runtimeContextAnalyticsSnapshotsEqual,
160
- } from "../runtime/context-analytics.ts";
161
- import {
162
131
  createEditorViewStateSnapshot,
163
132
  createViewState,
164
- } from "../runtime/view-state.ts";
133
+ } from "../shell/ref-utilities.ts";
134
+ import { findTextMatchesForRuntime, searchRuntimeDocument } from "../shell/search.ts";
165
135
  import {
166
136
  type TwProseMirrorSurfaceRef,
167
137
  } from "../ui-tailwind/editor-surface/tw-prosemirror-surface";
@@ -207,35 +177,77 @@ import {
207
177
  persistSession as persistSessionFromBoundary,
208
178
  rejectExportWhileLoading as rejectExportWhileLoadingFromBoundary,
209
179
  useEditorRuntimeBoundary,
210
- } from "./editor-runtime-boundary.ts";
180
+ } from "../shell/session-bootstrap.ts";
211
181
  import {
212
182
  downloadExportResult,
213
183
  withExportDelivery,
214
184
  } from "./browser-export";
215
185
  import { EditorShellView } from "./editor-shell-view.tsx";
186
+ import { TwDebugPresentation } from "../ui-tailwind/debug/index.ts";
187
+ import { shellPasteFragmentParser as SHELL_PASTE_FRAGMENT_PARSER } from "../shell/paste-adapter.ts";
216
188
  import { EditorSurfaceController } from "./editor-surface-controller.tsx";
189
+ import type { TwWorkspaceChromeHostController } from "../ui-tailwind/chrome/tw-workspace-chrome-host";
217
190
  import {
218
191
  resolveChromePreset,
219
192
  resolveChromeVisibilityForPreset,
220
- } from "../ui-tailwind/chrome/chrome-preset-model.ts";
193
+ } from "../api/v3/ui/chrome-preset-model.ts";
221
194
  import { TwRuntimeReplDialog } from "../ui-tailwind/chrome/tw-runtime-repl-dialog.tsx";
222
- import { createRuntimeCollabSync } from "../runtime/collab/runtime-collab-sync.ts";
223
195
  import {
224
- clearLocalCursorState,
225
- getCursorColorForUser,
226
- setLocalCursorState,
227
- } from "../runtime/collab/remote-cursor-awareness.ts";
196
+ useRemoteCursorPublish,
197
+ useRuntimeCollabSync,
198
+ } from "../shell/use-collab-sync.ts";
199
+ import {
200
+ collectChartSnapshots,
201
+ lookupChartSnapshot,
202
+ } from "../shell/chart-snapshots.ts";
203
+ import { wrapRefForTelemetry } from "../shell/ref-utilities.ts";
204
+ import { createApiV3, type ApiV3 } from "../api/v3/index.ts";
205
+ import { UiApiProvider } from "../ui-tailwind/ui-api-context.tsx";
206
+ import {
207
+ UiShellChannelsProvider,
208
+ type UiShellChannels,
209
+ } from "../ui-tailwind/ui-shell-channels-context.tsx";
210
+ import { OverlayAnchorBridgeProvider } from "../ui-tailwind/overlay-anchor-bridge-context.tsx";
228
211
  import {
229
- stableChartId,
230
- } from "../runtime/chart/chart-model-store.ts";
212
+ createShellOverlayAnchorBridge,
213
+ type ShellOverlayAnchorBridge,
214
+ } from "../shell/overlay-anchor-bridge.ts";
231
215
  import {
232
- projectChartSnapshot,
233
- } from "../runtime/chart/chart-snapshot.ts";
216
+ createUiSubscriberChannel,
217
+ type UiSubscriberChannel,
218
+ } from "../shell/ui-subscriber-channels.ts";
219
+ import {
220
+ deriveShellPinnedSurfaces,
221
+ makeShellUiControllerFactory,
222
+ shellUiControllerFactory,
223
+ } from "./ui-controller-factory.ts";
224
+ import type {
225
+ ChromeHostPosture,
226
+ ChromePosture,
227
+ OverlayAnchorQuery,
228
+ ViewportState,
229
+ } from "../api/v3/ui/_types.ts";
234
230
 
235
231
  export {
236
232
  __createFallbackRuntime,
237
233
  __resolveWordReviewEditorSource,
238
- } from "./editor-runtime-boundary.ts";
234
+ } from "../shell/session-bootstrap.ts";
235
+
236
+ /**
237
+ * Map a host-facing `markupDisplay` prop value to the narrow
238
+ * `ChromeMarkupDisplay` enum on `ChromeHostPosture` (`"final" | "markup"
239
+ * | "original"`). Prop shape is richer for legacy reasons; `ui.chrome.
240
+ * getPosture` normalizes to the narrow enum.
241
+ */
242
+ function normalizeHostMarkupDisplay(
243
+ raw: WordReviewEditorProps["markupDisplay"],
244
+ ): ChromeHostPosture["markupDisplay"] | undefined {
245
+ if (raw === undefined) return undefined;
246
+ if (raw === "original") return "original";
247
+ if (raw === "no-markup" || raw === "clean") return "final";
248
+ // "all-markup" | "simple-markup" | "simple" | "all" → markup
249
+ return "markup";
250
+ }
239
251
 
240
252
  const VISUALLY_HIDDEN_STYLES: React.CSSProperties = {
241
253
  position: "absolute",
@@ -249,21 +261,6 @@ const VISUALLY_HIDDEN_STYLES: React.CSSProperties = {
249
261
  border: 0,
250
262
  };
251
263
 
252
- const BROWSER_SAFE_PREVIEW_TYPES = new Set([
253
- "image/png",
254
- "image/jpeg",
255
- "image/jpg",
256
- "image/gif",
257
- "image/webp",
258
- "image/bmp",
259
- // SVG is served through `<img src="data:image/svg+xml;base64,...">` by
260
- // `createImageDataUrl`. Chromium sandboxes SVGs loaded via <img> — scripts
261
- // don't execute, external references are blocked, XSS surface matches PNG.
262
- // Needed for docs/plans/lane-5-charts.md Stage 0B synthesized chart previews and
263
- // any host that ships .svg inside `word/media/` as a logo or figure.
264
- "image/svg+xml",
265
- ]);
266
-
267
264
  const ACCESSIBLE_REGION_ORDER = [
268
265
  "toolbar",
269
266
  "document",
@@ -280,279 +277,26 @@ type SelectionToolbarDismissReason =
280
277
  | "escape";
281
278
 
282
279
  // ---------------------------------------------------------------------------
283
- // P17 module-level helpersmetadata persistence
280
+ // P17 metadata persistenceimported from Layer 06
281
+ //
282
+ // The conflict-key function, persistence-mode cascade, pending-conflict
283
+ // type, conflict emitter, and internal/external conversion routines used
284
+ // to live in this file as ~200 lines of UI-layer code. They had no UI
285
+ // dependencies — they operated on the runtime's workflow metadata
286
+ // snapshot + a host-supplied resolver — so Layer-06 Slice 4 moved them
287
+ // to `src/runtime/workflow/metadata-persistence.ts` (W9-purity
288
+ // compliant). The UI now delegates via the imports below.
284
289
  // ---------------------------------------------------------------------------
285
290
 
286
- function conflictKey(input: {
287
- scopeId?: string;
288
- entryId?: string;
289
- fieldKey?: string;
290
- }): string {
291
- return `${input.scopeId ?? ""}|${input.entryId ?? ""}|${input.fieldKey ?? ""}`;
292
- }
293
-
294
- function resolveEffective(input: {
295
- overlay?: MetadataPersistenceMode;
296
- scope?: ScopeMetadataPersistence;
297
- field?: ScopeMetadataPersistence;
298
- }): "internal" | "external" {
299
- if (input.field === "internal" || input.field === "external") return input.field;
300
- if (input.scope === "internal" || input.scope === "external") return input.scope;
301
- return input.overlay ?? "internal";
302
- }
303
-
304
- /**
305
- * A pending conflict record held in `metadataConflictPendingRef` while
306
- * the host decides how to resolve. Keyed by `conflictKey(...)`.
307
- */
308
- interface PendingConflict {
309
- scopeId?: string;
310
- entryId?: string;
311
- fieldKey?: string;
312
- embedded: { value?: Record<string, unknown>; version?: number } | null;
313
- external: { value?: Record<string, unknown>; version?: number } | null;
314
- defaultPolicy: "prefer-latest";
315
- }
316
-
317
- /** Emit a single `metadata_conflict_detected` event and register the pending conflict. */
318
- function registerAndEmitConflict(args: {
319
- onEvent: ((event: WordReviewEditorEvent) => void) | undefined;
320
- documentId: string;
321
- conflict: PendingConflict;
322
- pendingConflicts: Map<string, PendingConflict>;
323
- }): void {
324
- const key = conflictKey(args.conflict);
325
- // Guard: do not emit duplicate events for the same key in a single pass.
326
- if (args.pendingConflicts.has(key)) return;
327
- args.pendingConflicts.set(key, args.conflict);
328
- args.onEvent?.({
329
- type: "metadata_conflict_detected",
330
- documentId: args.documentId,
331
- scopeId: args.conflict.scopeId,
332
- entryId: args.conflict.entryId,
333
- fieldKey: args.conflict.fieldKey,
334
- embedded: args.conflict.embedded,
335
- external: args.conflict.external,
336
- defaultPolicy: args.conflict.defaultPolicy,
337
- });
338
- }
339
-
340
- async function runConvertScopesToInternal(args: {
341
- runtime: WordReviewEditorRuntime;
342
- scopeIds: string[];
343
- resolver: ScopeMetadataResolver | null;
344
- /** When provided, version mismatches emit `metadata_conflict_detected`. */
345
- documentId?: string;
346
- onEvent?: (event: WordReviewEditorEvent) => void;
347
- pendingConflicts?: Map<string, PendingConflict>;
348
- }): Promise<void> {
349
- if (!args.resolver) throw new MetadataResolverMissingError();
350
- const snapshot = args.runtime.getWorkflowMetadataSnapshot();
351
- const overlay = args.runtime.getWorkflowOverlay();
352
-
353
- const nextEntries = await Promise.all(
354
- snapshot.entries.map(async (entry) => {
355
- if (!entry.scopeId || !args.scopeIds.includes(entry.scopeId)) return entry;
356
- const scope = overlay?.scopes.find((s) => s.scopeId === entry.scopeId);
357
- const effective = resolveEffective({
358
- overlay: overlay?.metadataPersistence,
359
- scope: scope?.metadataPersistence,
360
- field: entry.metadataPersistence,
361
- });
362
- if (effective !== "external" || !entry.storageRef) return entry;
363
- const resolved = await args.resolver!.resolve(entry.storageRef);
364
- if (!resolved) return entry;
365
-
366
- // Conflict detection: compare embedded metadataVersion vs resolver version.
367
- if (
368
- args.pendingConflicts &&
369
- args.documentId !== undefined &&
370
- entry.metadataVersion !== undefined &&
371
- resolved.version !== undefined &&
372
- entry.metadataVersion !== resolved.version
373
- ) {
374
- registerAndEmitConflict({
375
- onEvent: args.onEvent,
376
- documentId: args.documentId,
377
- pendingConflicts: args.pendingConflicts,
378
- conflict: {
379
- scopeId: entry.scopeId,
380
- entryId: entry.entryId,
381
- // External entries have no inline value; embedded side carries version only.
382
- embedded: { version: entry.metadataVersion },
383
- external: { value: resolved.value, version: resolved.version },
384
- defaultPolicy: "prefer-latest",
385
- },
386
- });
387
- }
388
-
389
- // Default behavior: always apply the resolved value (host can override via resolveMetadataConflict).
390
- return {
391
- ...entry,
392
- value: resolved.value,
393
- metadataPersistence: "internal" as const,
394
- storageRef: undefined,
395
- metadataVersion: resolved.version ?? entry.metadataVersion,
396
- };
397
- }),
398
- );
399
-
400
- args.runtime.setWorkflowMetadataEntries(nextEntries);
401
- }
402
-
403
- async function runConvertScopesToExternal(args: {
404
- runtime: WordReviewEditorRuntime;
405
- scopeIds: string[];
406
- resolver: ScopeMetadataResolver | null;
407
- /** When provided, version-conflict publish errors emit `metadata_conflict_detected`. */
408
- documentId?: string;
409
- onEvent?: (event: WordReviewEditorEvent) => void;
410
- pendingConflicts?: Map<string, PendingConflict>;
411
- }): Promise<void> {
412
- if (!args.resolver) throw new MetadataResolverMissingError();
413
- const snapshot = args.runtime.getWorkflowMetadataSnapshot();
414
- const overlay = args.runtime.getWorkflowOverlay();
415
-
416
- const nextEntries = await Promise.all(
417
- snapshot.entries.map(async (entry) => {
418
- if (!entry.scopeId || !args.scopeIds.includes(entry.scopeId)) return entry;
419
- const scope = overlay?.scopes.find((s) => s.scopeId === entry.scopeId);
420
- const effective = resolveEffective({
421
- overlay: overlay?.metadataPersistence,
422
- scope: scope?.metadataPersistence,
423
- field: entry.metadataPersistence,
424
- });
425
- if (effective === "external") return entry;
426
-
427
- try {
428
- const { ref, version } = await args.resolver!.publish({
429
- scopeId: entry.scopeId,
430
- metadataId: entry.metadataId,
431
- entryId: entry.entryId,
432
- value: entry.value ?? {},
433
- expectedVersion: entry.metadataVersion,
434
- });
435
- return {
436
- ...entry,
437
- value: undefined,
438
- metadataPersistence: "external" as const,
439
- storageRef: ref,
440
- metadataVersion: version,
441
- };
442
- } catch (err: unknown) {
443
- // Duck-type version-conflict errors (HarnessVersionConflictError or compatible shapes).
444
- if (
445
- args.pendingConflicts &&
446
- args.documentId !== undefined &&
447
- err instanceof Error &&
448
- (err.name === "HarnessVersionConflictError" ||
449
- ("ref" in err && "expected" in err && "actual" in err))
450
- ) {
451
- const conflictErr = err as Error & { ref?: string; expected?: number; actual?: number };
452
- registerAndEmitConflict({
453
- onEvent: args.onEvent,
454
- documentId: args.documentId,
455
- pendingConflicts: args.pendingConflicts,
456
- conflict: {
457
- scopeId: entry.scopeId,
458
- entryId: entry.entryId,
459
- // Embedded side: the entry's current inline value and metadataVersion.
460
- embedded: { value: entry.value, version: entry.metadataVersion },
461
- // External side: only the rowstore's actual version (no value available from error).
462
- external: { version: conflictErr.actual },
463
- defaultPolicy: "prefer-latest",
464
- },
465
- });
466
- // Skip this entry — do not publish; leave it unchanged.
467
- return entry;
468
- }
469
- // Non-conflict errors propagate normally.
470
- throw err;
471
- }
472
- }),
473
- );
474
-
475
- args.runtime.setWorkflowMetadataEntries(nextEntries);
476
- }
291
+ import {
292
+ conflictKey,
293
+ convertScopesToInternal as runConvertScopesToInternal,
294
+ convertScopesToExternal as runConvertScopesToExternal,
295
+ type PendingConflict,
296
+ } from "../shell/ref-utilities.ts";
477
297
 
478
298
  // ---------------------------------------------------------------------------
479
299
 
480
- type CanonicalDocType = ReturnType<WordReviewEditorRuntime["getCanonicalDocument"]>;
481
-
482
- function collectChartSnapshots(doc: CanonicalDocType): import("../api/public-types").ChartSnapshot[] {
483
- const results: import("../api/public-types").ChartSnapshot[] = [];
484
- collectChartSnapshotsFromBlocks(doc.content.children, results);
485
- return results;
486
- }
487
-
488
- /**
489
- * Walk the canonical document, compute each chart_preview's stableChartId,
490
- * and project a snapshot for the first matching id. Short-circuits on
491
- * match so the happy path is O(k) in blocks-until-match rather than the
492
- * O(N) the `collect().find()` fallback incurred for every ref call. For
493
- * hosts that call `getChartSnapshot` in a tight loop over many chartIds,
494
- * this reduces the cost from O(N²) to O(N·k).
495
- */
496
- function lookupChartSnapshot(
497
- doc: CanonicalDocType,
498
- chartId: string,
499
- ): import("../api/public-types").ChartSnapshot | null {
500
- return lookupChartSnapshotInBlocks(doc.content.children, chartId);
501
- }
502
-
503
- function lookupChartSnapshotInBlocks(
504
- blocks: CanonicalDocType["content"]["children"],
505
- chartId: string,
506
- ): import("../api/public-types").ChartSnapshot | null {
507
- for (const block of blocks) {
508
- if (block.type === "paragraph") {
509
- for (const inline of block.children) {
510
- if (inline.type === "chart_preview" && inline.parsedData) {
511
- const id = stableChartId(inline.rawXml);
512
- if (id === chartId) {
513
- return projectChartSnapshot(id, inline.parsedData);
514
- }
515
- }
516
- }
517
- } else if (block.type === "table") {
518
- for (const row of block.rows) {
519
- for (const cell of row.cells) {
520
- const found = lookupChartSnapshotInBlocks(cell.children, chartId);
521
- if (found) return found;
522
- }
523
- }
524
- } else if (block.type === "sdt" || block.type === "custom_xml") {
525
- const found = lookupChartSnapshotInBlocks(block.children, chartId);
526
- if (found) return found;
527
- }
528
- }
529
- return null;
530
- }
531
-
532
- function collectChartSnapshotsFromBlocks(
533
- blocks: CanonicalDocType["content"]["children"],
534
- results: import("../api/public-types").ChartSnapshot[],
535
- ): void {
536
- for (const block of blocks) {
537
- if (block.type === "paragraph") {
538
- for (const inline of block.children) {
539
- if (inline.type === "chart_preview" && inline.parsedData) {
540
- const chartId = stableChartId(inline.rawXml);
541
- results.push(projectChartSnapshot(chartId, inline.parsedData));
542
- }
543
- }
544
- } else if (block.type === "table") {
545
- for (const row of block.rows) {
546
- for (const cell of row.cells) {
547
- collectChartSnapshotsFromBlocks(cell.children, results);
548
- }
549
- }
550
- } else if (block.type === "sdt" || block.type === "custom_xml") {
551
- collectChartSnapshotsFromBlocks(block.children, results);
552
- }
553
- }
554
- }
555
-
556
300
  // ---------------------------------------------------------------------------
557
301
 
558
302
  export function __createWordReviewEditorRefBridge(
@@ -579,7 +323,44 @@ export function __createWordReviewEditorRefBridge(
579
323
  return r;
580
324
  });
581
325
 
326
+ // L07 closure pass — cache the V3 surface per-bridge. `runtime` is stable
327
+ // across this bridge's lifetime; `createApiV3` freezes the result, so a
328
+ // single shared instance is safe and avoids re-running family factories
329
+ // on every `ref.getApiV3()` call.
330
+ //
331
+ // refactor/11 Slice 4 — wire the UI factory here too so the
332
+ // non-mounted ref path (headless harness, tests, services) returns an
333
+ // `api` with the `ui` namespace in lockstep with the mounted shell path.
334
+ let cachedApiV3: ApiV3 | null = null;
335
+
582
336
  const refValue: WordReviewEditorRef = {
337
+ getApiV3: () => {
338
+ if (cachedApiV3 === null) {
339
+ const built = createApiV3(runtime, { ui: shellUiControllerFactory });
340
+ cachedApiV3 = built;
341
+ // L03 §5b activation — hand the runtime a callback that
342
+ // returns the composed effective markup mode (L06 policy ×
343
+ // L10 local preference). Closure captures the freshly-built
344
+ // api so subsequent `ui.viewport.setLocalMarkupMode` calls
345
+ // flip the value observed on the next projection. The paired
346
+ // subscribe below invalidates the surface cache when the
347
+ // composed mode changes due to a class-C preference flip,
348
+ // so the new posture reaches the render without waiting for
349
+ // the next mutation. Both hooks are guarded on the `ui`
350
+ // family being present — headless consumers that bypass the
351
+ // factory skip the wire.
352
+ const uiApi = built.ui;
353
+ if (uiApi) {
354
+ runtime.setEffectiveMarkupModeProvider(() =>
355
+ uiApi.viewport.getEffectiveMarkupMode(),
356
+ );
357
+ uiApi.viewport.subscribeEffectiveMarkupMode(() => {
358
+ runtime.invalidateForMarkupModeChange();
359
+ });
360
+ }
361
+ }
362
+ return cachedApiV3;
363
+ },
583
364
  focus: () => runtime.focus(),
584
365
  blur: () => runtime.blur(),
585
366
  undo: () => runtime.undo(),
@@ -929,6 +710,30 @@ export function __createWordReviewEditorRefBridge(
929
710
  getWorkflowMetadataSnapshot: () => {
930
711
  return clonePublicValue(runtime.getWorkflowMetadataSnapshot());
931
712
  },
713
+ getVisibilityPolicy: (kind) => {
714
+ return clonePublicValue(runtime.getVisibilityPolicy(kind));
715
+ },
716
+ getVisibilityPolicies: () => {
717
+ return clonePublicValue(runtime.getVisibilityPolicies());
718
+ },
719
+ setVisibilityPolicy: (policy) => {
720
+ return runtime.setVisibilityPolicy(clonePublicValue(policy));
721
+ },
722
+ clearVisibilityPolicy: (kind) => {
723
+ return runtime.clearVisibilityPolicy(kind);
724
+ },
725
+ subscribeVisibilityPolicy: (listener) => {
726
+ return runtime.subscribeVisibilityPolicy(listener);
727
+ },
728
+ getMarkupModePolicy: () => {
729
+ return clonePublicValue(runtime.getMarkupModePolicy());
730
+ },
731
+ setMarkupModePolicy: (policy) => {
732
+ return runtime.setMarkupModePolicy(policy ? clonePublicValue(policy) : null);
733
+ },
734
+ subscribeMarkupModePolicy: (listener) => {
735
+ return runtime.subscribeMarkupModePolicy(listener);
736
+ },
932
737
  queryScopes: (filter) => {
933
738
  return clonePublicValue(runtime.queryScopes(filter));
934
739
  },
@@ -1164,8 +969,15 @@ export function __createWordReviewEditorRefBridge(
1164
969
  },
1165
970
  ...projections,
1166
971
  };
1167
- refHolder.current = refValue;
1168
- return refValue;
972
+ // Phase 1 api-channel — Proxy wrap so every invocation emits when enabled.
973
+ const telemetryBus = runtime.debug?.bus;
974
+ if (!telemetryBus) {
975
+ refHolder.current = refValue;
976
+ return refValue;
977
+ }
978
+ const tracedRef = wrapRefForTelemetry(refValue, telemetryBus);
979
+ refHolder.current = tracedRef;
980
+ return tracedRef;
1169
981
  }
1170
982
 
1171
983
  export function __applyRuntimeTextCommand(
@@ -1218,7 +1030,11 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
1218
1030
  showReviewPanel = true,
1219
1031
  chromeVisibility,
1220
1032
  density,
1033
+ editorActionHost,
1034
+ chromeControllerRef,
1035
+ commandPaletteDisabled,
1221
1036
  customSelectionTools,
1037
+ debugMode = "off",
1222
1038
  } = props;
1223
1039
 
1224
1040
  const [activeRailTab, setActiveRailTab] = useState<ReviewRailTab>("comments");
@@ -1239,6 +1055,11 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
1239
1055
  const surfaceRef = useRef<TwProseMirrorSurfaceRef | null>(null);
1240
1056
  const selectionToolbarElementRef = useRef<HTMLDivElement | null>(null);
1241
1057
  const shellRef = useRef<HTMLDivElement | null>(null);
1058
+ // §3.1 — internal handle on the workspace chrome controller so the
1059
+ // PM surface's `onContextMenuRequested` can dispatch to it. Teed
1060
+ // to the host-supplied `chromeControllerRef` below so both the
1061
+ // internal wire and any external consumer see the same instance.
1062
+ const internalChromeControllerRef = useRef<TwWorkspaceChromeHostController | null>(null);
1242
1063
  const editorRefForRepl = useRef<WordReviewEditorRef | null>(null);
1243
1064
  const lastSelectionToolbarKeyRef = useRef<string | null>(null);
1244
1065
  const lastAnnouncedErrorIdRef = useRef<string | null>(null);
@@ -1266,6 +1087,167 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
1266
1087
  lastSavedRevisionTokenRef,
1267
1088
  runtimeViewStateSeedRef,
1268
1089
  } = useEditorRuntimeBoundary(props);
1090
+
1091
+ // refactor/10 Task 1 — the mounted `UiController` reads live shell
1092
+ // state via refs. Refs hold a pointer to the current render's host
1093
+ // posture + preset input so the factory's closure getters always
1094
+ // return current values (React-native stable-getter pattern — the
1095
+ // factory is memoized on `activeRuntime`, the ref body mutates per
1096
+ // render).
1097
+ const shellUiStateRef = useRef<{
1098
+ hostPosture: ChromeHostPosture;
1099
+ chromePresetInput: {
1100
+ chromePreset: WordReviewEditorChromePreset;
1101
+ chromeOptions?: Partial<WordReviewEditorChromeOptions>;
1102
+ };
1103
+ }>({
1104
+ hostPosture: {},
1105
+ chromePresetInput: { chromePreset: "advanced" },
1106
+ });
1107
+ shellUiStateRef.current = {
1108
+ hostPosture: {
1109
+ reviewMode: reviewMode === "review" ? "reviewer" : "author",
1110
+ markupDisplay: normalizeHostMarkupDisplay(markupDisplay),
1111
+ debugMode,
1112
+ chromePreset,
1113
+ },
1114
+ chromePresetInput: {
1115
+ chromePreset: resolveChromePreset(chromePreset, reviewMode),
1116
+ chromeOptions,
1117
+ },
1118
+ };
1119
+
1120
+ // refactor/11 Slice 4 — construct the v3 API with the UI namespace
1121
+ // wired so `<UiApiProvider>` can expose `api.ui` to chrome hooks that
1122
+ // read via `useUiApi()`. Memoized on `activeRuntime` (stable across
1123
+ // PM transactions per Performance Invariant 8); `createApiV3` freezes
1124
+ // the returned record so sharing it between the provider + the ref
1125
+ // (see useImperativeHandle below) is safe. Called once per runtime
1126
+ // bridge; re-runs only when the editor receives a new runtime handle.
1127
+ //
1128
+ // DS-C1 (designsystem.md §8.8.1 "Selection toolbar" row) — the shell
1129
+ // overlay-anchor bridge is a stable `useRef` box consulted by the
1130
+ // controller's `getOverlayAnchor` hook so
1131
+ // `ui.overlays.getAnchor({ kind: "selection" })` returns a real rect
1132
+ // without re-creating the API on every selection change. The factory
1133
+ // closes over the ref box identity (never changes); per-render updates
1134
+ // to the box contents land via `useShellSelectionAnchorBridge` inside
1135
+ // the review workspace.
1136
+ const overlayAnchorBridgeRef = useRef<ShellOverlayAnchorBridge>(
1137
+ createShellOverlayAnchorBridge(),
1138
+ );
1139
+
1140
+ // refactor/10 chrome-contract Slice 2 (2026-04-23) — subscriber
1141
+ // channels for `ui.chrome.subscribe` / `ui.viewport.subscribe` /
1142
+ // `ui.overlays.subscribe`. Each is a stable `useRef` emitter the
1143
+ // factory closes over. Emission is driven from React effects below
1144
+ // on the appropriate state-source changes. Before this slice those
1145
+ // subscribe methods threw on mount ("controller lacks hook");
1146
+ // Playwright drivers + debug service couldn't observe posture /
1147
+ // viewport / overlay-invalidation over time on the mounted editor.
1148
+ const chromeChannelRef = useRef<UiSubscriberChannel<ChromePosture> | null>(
1149
+ null,
1150
+ );
1151
+ if (chromeChannelRef.current === null) {
1152
+ chromeChannelRef.current = createUiSubscriberChannel<ChromePosture>();
1153
+ }
1154
+ const viewportChannelRef = useRef<UiSubscriberChannel<ViewportState> | null>(
1155
+ null,
1156
+ );
1157
+ if (viewportChannelRef.current === null) {
1158
+ viewportChannelRef.current = createUiSubscriberChannel<ViewportState>();
1159
+ }
1160
+ const overlaysChannelRef = useRef<
1161
+ UiSubscriberChannel<OverlayAnchorQuery> | null
1162
+ >(null);
1163
+ if (overlaysChannelRef.current === null) {
1164
+ overlaysChannelRef.current = createUiSubscriberChannel<OverlayAnchorQuery>();
1165
+ }
1166
+
1167
+ // refactor/10 Task 1 — mounted factory populates getHostPosture +
1168
+ // getPinnedSurfaces + the DS-C1 selection bridge + the three
1169
+ // subscriber channels so every `api.ui.*.subscribe` on the mounted
1170
+ // path is a real subscription, not a throw.
1171
+ const api = useMemo(
1172
+ () =>
1173
+ createApiV3(activeRuntime, {
1174
+ ui: makeShellUiControllerFactory({
1175
+ getHostPosture: () => shellUiStateRef.current.hostPosture,
1176
+ getPinnedSurfaces: () =>
1177
+ deriveShellPinnedSurfaces(
1178
+ shellUiStateRef.current.chromePresetInput.chromePreset,
1179
+ shellUiStateRef.current.chromePresetInput.chromeOptions,
1180
+ ),
1181
+ getOverlayAnchor: (query) => {
1182
+ if (query.kind === "selection") {
1183
+ return overlayAnchorBridgeRef.current.getSelectionAnchor?.() ?? null;
1184
+ }
1185
+ return null;
1186
+ },
1187
+ subscribeChrome: (listener) =>
1188
+ chromeChannelRef.current!.subscribe(listener),
1189
+ subscribeViewport: (listener) =>
1190
+ viewportChannelRef.current!.subscribe(listener),
1191
+ subscribeOverlays: (listener) =>
1192
+ overlaysChannelRef.current!.subscribe(listener),
1193
+ }),
1194
+ }),
1195
+ [activeRuntime],
1196
+ );
1197
+
1198
+ // L03 §5b activation (mounted-shell path). Hand the runtime a
1199
+ // callback that returns the composed effective markup mode (L06
1200
+ // policy × L10 local preference). Subscribe to class-C preference
1201
+ // changes so the surface cache busts and the new posture reaches
1202
+ // the render without waiting for a mutation. Re-wires when
1203
+ // `activeRuntime` or `api` changes (effect cleanup clears the
1204
+ // previous provider + unsubscribes).
1205
+ useEffect(() => {
1206
+ const uiApi = api.ui;
1207
+ if (!uiApi) return;
1208
+ activeRuntime.setEffectiveMarkupModeProvider(() =>
1209
+ uiApi.viewport.getEffectiveMarkupMode(),
1210
+ );
1211
+ const unsubscribe = uiApi.viewport.subscribeEffectiveMarkupMode(() => {
1212
+ activeRuntime.invalidateForMarkupModeChange();
1213
+ });
1214
+ return () => {
1215
+ unsubscribe();
1216
+ activeRuntime.setEffectiveMarkupModeProvider(undefined);
1217
+ };
1218
+ }, [activeRuntime, api]);
1219
+
1220
+ // Chrome emit — fire posture subscribers whenever a host-posture
1221
+ // source changes. Runtime-owned posture fields (effectiveMode,
1222
+ // blockedReasons, documentMode, readOnly) are captured via the
1223
+ // snapshot-slice subscriptions below — they trigger re-renders that
1224
+ // in turn re-run this effect on dep changes.
1225
+ useEffect(() => {
1226
+ if (!api.ui) return;
1227
+ chromeChannelRef.current?.emit(api.ui.chrome.getPosture());
1228
+ }, [
1229
+ api,
1230
+ reviewMode,
1231
+ markupDisplay,
1232
+ debugMode,
1233
+ chromePreset,
1234
+ readOnly,
1235
+ ]);
1236
+
1237
+ // Channel teardown on unmount — belt-and-suspenders vs stale
1238
+ // listeners after a later remount. The refs are per-instance so
1239
+ // lifetime is bounded, but clearing avoids a latent leak if a
1240
+ // host re-mounts the editor in the same tab.
1241
+ useEffect(() => {
1242
+ const chrome = chromeChannelRef.current;
1243
+ const viewport = viewportChannelRef.current;
1244
+ const overlays = overlaysChannelRef.current;
1245
+ return () => {
1246
+ chrome?.dispose();
1247
+ viewport?.dispose();
1248
+ overlays?.dispose();
1249
+ };
1250
+ }, []);
1269
1251
  const metaSlice = useRuntimeSnapshotSlice(
1270
1252
  runtime,
1271
1253
  fallbackSnapshot,
@@ -1586,54 +1568,21 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
1586
1568
  [activeReviewQueueItemId, activeRuntime, reviewQueueSnapshot],
1587
1569
  );
1588
1570
 
1589
- useEffect(() => {
1590
- if (!ydoc || !runtime) return;
1591
- const handle = createRuntimeCollabSync({
1592
- ydoc,
1593
- runtime,
1594
- authorId: currentUser.userId,
1595
- commandAppliedBridge,
1596
- });
1597
- return () => handle.destroy();
1598
- }, [commandAppliedBridge, currentUser.userId, runtime, ydoc]);
1599
-
1600
- useEffect(() => {
1601
- if (!awareness) {
1602
- return;
1603
- }
1604
- return () => clearLocalCursorState(awareness);
1605
- }, [awareness]);
1606
-
1607
- useEffect(() => {
1608
- if (!awareness) {
1609
- return;
1610
- }
1611
- if (!runtime) {
1612
- clearLocalCursorState(awareness);
1613
- return;
1614
- }
1571
+ useRuntimeCollabSync({
1572
+ ydoc,
1573
+ runtime,
1574
+ authorId: currentUser.userId,
1575
+ commandAppliedBridge,
1576
+ });
1615
1577
 
1616
- if (suppressNextAwarenessPublishRef.current) {
1617
- suppressNextAwarenessPublishRef.current = false;
1618
- } else {
1619
- setLocalCursorState(awareness, {
1620
- userId: currentUser.userId,
1621
- displayName: currentUser.displayName,
1622
- color: getCursorColorForUser(currentUser.userId),
1623
- anchor: snapshot.selection.anchor,
1624
- head: snapshot.selection.head,
1625
- storyTarget: snapshot.activeStory,
1626
- });
1627
- }
1628
- }, [
1578
+ useRemoteCursorPublish({
1629
1579
  awareness,
1630
- currentUser.displayName,
1631
- currentUser.userId,
1632
1580
  runtime,
1633
- snapshot.activeStory,
1634
- snapshot.selection.anchor,
1635
- snapshot.selection.head,
1636
- ]);
1581
+ currentUser,
1582
+ selection: { anchor: snapshot.selection.anchor, head: snapshot.selection.head },
1583
+ activeStory: snapshot.activeStory,
1584
+ suppressNextPublishRef: suppressNextAwarenessPublishRef,
1585
+ });
1637
1586
 
1638
1587
  useEffect(() => {
1639
1588
  runtimeViewStateSeedRef.current = {
@@ -1659,7 +1608,14 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
1659
1608
  if (!r) throw new Error("ref projection used before ref initialization");
1660
1609
  return r;
1661
1610
  });
1611
+ // L07 closure pass — share the v3 surface with the render-time
1612
+ // `api` memo (refactor/11 Slice 4). The provider mounts the same
1613
+ // frozen ApiV3 that `ref.getApiV3()` returns; consumers reading
1614
+ // through `useUiApi()` see the identical `api.ui` the host-side
1615
+ // ref exposes, so provider path and ref path cannot drift.
1616
+ const cachedApiV3: ApiV3 = api;
1662
1617
  const refValue: WordReviewEditorRef = ({
1618
+ getApiV3: () => cachedApiV3,
1663
1619
  focus: () => activeRuntime.focus(),
1664
1620
  blur: () => activeRuntime.blur(),
1665
1621
  undo: () => activeRuntime.undo(),
@@ -2081,6 +2037,30 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
2081
2037
  getWorkflowMetadataSnapshot: () => {
2082
2038
  return clonePublicValue(activeRuntime.getWorkflowMetadataSnapshot());
2083
2039
  },
2040
+ getVisibilityPolicy: (kind) => {
2041
+ return clonePublicValue(activeRuntime.getVisibilityPolicy(kind));
2042
+ },
2043
+ getVisibilityPolicies: () => {
2044
+ return clonePublicValue(activeRuntime.getVisibilityPolicies());
2045
+ },
2046
+ setVisibilityPolicy: (policy) => {
2047
+ return activeRuntime.setVisibilityPolicy(clonePublicValue(policy));
2048
+ },
2049
+ clearVisibilityPolicy: (kind) => {
2050
+ return activeRuntime.clearVisibilityPolicy(kind);
2051
+ },
2052
+ subscribeVisibilityPolicy: (listener) => {
2053
+ return activeRuntime.subscribeVisibilityPolicy(listener);
2054
+ },
2055
+ getMarkupModePolicy: () => {
2056
+ return clonePublicValue(activeRuntime.getMarkupModePolicy());
2057
+ },
2058
+ setMarkupModePolicy: (policy) => {
2059
+ return activeRuntime.setMarkupModePolicy(policy ? clonePublicValue(policy) : null);
2060
+ },
2061
+ subscribeMarkupModePolicy: (listener) => {
2062
+ return activeRuntime.subscribeMarkupModePolicy(listener);
2063
+ },
2084
2064
  queryScopes: (filter) => {
2085
2065
  return clonePublicValue(activeRuntime.queryScopes(filter));
2086
2066
  },
@@ -2307,12 +2287,21 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
2307
2287
  },
2308
2288
  ...projections,
2309
2289
  }) as WordReviewEditorRef;
2310
- refHolder.current = refValue;
2311
- editorRefForRepl.current = refValue;
2312
- return refValue;
2290
+ // Phase 1 api-channel — Proxy wrap for trace events when enabled.
2291
+ const telemetryBus = activeRuntime.debug?.bus;
2292
+ if (!telemetryBus) {
2293
+ refHolder.current = refValue;
2294
+ editorRefForRepl.current = refValue;
2295
+ return refValue;
2296
+ }
2297
+ const tracedRef = wrapRefForTelemetry(refValue, telemetryBus);
2298
+ refHolder.current = tracedRef;
2299
+ editorRefForRepl.current = tracedRef;
2300
+ return tracedRef;
2313
2301
  },
2314
2302
  [
2315
2303
  activeRuntime,
2304
+ api,
2316
2305
  clearReviewSectionMarkById,
2317
2306
  currentUser.userId,
2318
2307
  documentId,
@@ -2515,38 +2504,10 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
2515
2504
  )
2516
2505
  .sort()
2517
2506
  .join("|");
2518
- const mediaPreviews = useMemo(() => {
2519
- if (!sourcePackage) {
2520
- return {} as Record<string, MediaPreviewDescriptor>;
2521
- }
2522
- try {
2523
- const bytes = decodePersistedSourcePackageBytes(sourcePackage);
2524
- if (!hasValidPersistedSourcePackageDigest(sourcePackage, bytes)) {
2525
- return {} as Record<string, MediaPreviewDescriptor>;
2526
- }
2527
- const opc = readOpcPackage(bytes);
2528
- const previews: Record<string, MediaPreviewDescriptor> = {};
2529
- for (const item of Object.values(canonicalDocument.media.items)) {
2530
- const contentType = item.contentType?.toLowerCase();
2531
- const part = opc.parts.get(item.packagePartName);
2532
- if (
2533
- !part?.bytes ||
2534
- !contentType ||
2535
- !BROWSER_SAFE_PREVIEW_TYPES.has(contentType)
2536
- ) {
2537
- continue;
2538
- }
2539
- previews[item.mediaId] = {
2540
- src: createImageDataUrl(contentType, part.bytes),
2541
- ...(item.widthEmu !== undefined ? { widthEmu: item.widthEmu } : {}),
2542
- ...(item.heightEmu !== undefined ? { heightEmu: item.heightEmu } : {}),
2543
- };
2544
- }
2545
- return previews;
2546
- } catch {
2547
- return {} as Record<string, MediaPreviewDescriptor>;
2548
- }
2549
- }, [mediaPreviewCatalogKey, sourcePackage?.sha256Hex]);
2507
+ const mediaPreviews = useMemo(
2508
+ () => buildMediaPreviews(sourcePackage, canonicalDocument),
2509
+ [mediaPreviewCatalogKey, sourcePackage?.sha256Hex],
2510
+ );
2550
2511
  const activeObjectContext = useMemo(
2551
2512
  () =>
2552
2513
  viewState.activeObjectFrame && viewState.activeObjectFrame.kind !== "image"
@@ -2791,6 +2752,36 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
2791
2752
  [],
2792
2753
  );
2793
2754
 
2755
+ // §3.1 — merged ref that writes to the internal ref AND forwards to
2756
+ // the host-supplied `chromeControllerRef` (when present). Supports
2757
+ // both callback refs and mutable ref objects.
2758
+ const teedChromeControllerRef = useCallback(
2759
+ (instance: TwWorkspaceChromeHostController | null) => {
2760
+ internalChromeControllerRef.current = instance;
2761
+ if (!chromeControllerRef) return;
2762
+ if (typeof chromeControllerRef === "function") {
2763
+ chromeControllerRef(instance);
2764
+ } else {
2765
+ (chromeControllerRef as React.MutableRefObject<
2766
+ TwWorkspaceChromeHostController | null
2767
+ >).current = instance;
2768
+ }
2769
+ },
2770
+ [chromeControllerRef],
2771
+ );
2772
+
2773
+ // §3.1 — contextmenu dispatch: when `editorActionHost` is wired,
2774
+ // forward PM's `contextmenu` DOM events to the workspace chrome
2775
+ // host's controller, which resolves target kinds and mounts the
2776
+ // menu portal. Without `editorActionHost` the chrome host is
2777
+ // unmounted, so we leave the native menu alone (back-compat).
2778
+ const handleContextMenuRequested = useCallback(
2779
+ (event: { clientX: number; clientY: number; target: EventTarget | null }) => {
2780
+ internalChromeControllerRef.current?.handleContextMenuRequest(event);
2781
+ },
2782
+ [],
2783
+ );
2784
+
2794
2785
  useEffect(() => {
2795
2786
  if (!selectionToolbarSelectionKey) {
2796
2787
  setSuppressedSuggestionRevisionId(null);
@@ -3327,6 +3318,9 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
3327
3318
  ref={surfaceRef}
3328
3319
  currentUser={currentUser}
3329
3320
  awareness={awareness}
3321
+ pasteFragmentParser={SHELL_PASTE_FRAGMENT_PARSER}
3322
+ runtimeSearchDocument={api.runtime.search.searchDocument}
3323
+ runtimeGetTableSelectionDescriptor={api.runtime.table.getSelectionDescriptor}
3330
3324
  snapshot={snapshot}
3331
3325
  canonicalDocument={canonicalDocument}
3332
3326
  documentNavigation={documentNavigation}
@@ -3347,11 +3341,13 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
3347
3341
  activeWorkflowScopeIds={workflowScopeSnapshot?.activeWorkItem?.scopeIds ?? []}
3348
3342
  workflowMetadata={workflowMarkupSnapshot?.metadata}
3349
3343
  onSelectionToolbarAnchorChange={handleSelectionToolbarAnchorChange}
3344
+ {...(editorActionHost ? { onContextMenuRequested: handleContextMenuRequested } : {})}
3350
3345
  {...editorCallbacks}
3351
3346
  dispatchRuntimeCommand={(command) =>
3352
3347
  activeRuntime.applyActiveStoryTextCommand(command as never)
3353
3348
  }
3354
3349
  layoutFacet={activeRuntime.layout}
3350
+ geometryFacet={activeRuntime.geometry}
3355
3351
  pageChromeHeaderBandPx={isPageWorkspace ? 32 : 0}
3356
3352
  pageChromeFooterBandPx={isPageWorkspace ? 32 : 0}
3357
3353
  pageChromeInterGapPx={isPageWorkspace ? 24 : 16}
@@ -3372,7 +3368,16 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
3372
3368
  />
3373
3369
  );
3374
3370
 
3371
+ const shellChannels: UiShellChannels = {
3372
+ chrome: chromeChannelRef.current!,
3373
+ viewport: viewportChannelRef.current!,
3374
+ overlays: overlaysChannelRef.current!,
3375
+ };
3376
+
3375
3377
  return (
3378
+ <UiApiProvider value={api.ui ?? null}>
3379
+ <UiShellChannelsProvider value={shellChannels}>
3380
+ <OverlayAnchorBridgeProvider value={overlayAnchorBridgeRef.current}>
3376
3381
  <>
3377
3382
  <EditorShellView
3378
3383
  shellRef={shellRef}
@@ -3391,6 +3396,7 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
3391
3396
  onShellKeyDownCapture={handleShellKeyDownCapture}
3392
3397
  shellHeader={shellHeader}
3393
3398
  viewState={viewState}
3399
+ onEditorRoleChange={(role) => activeRuntime.setEditorRole(role)}
3394
3400
  markupDisplay={liveMarkupDisplay}
3395
3401
  currentUserId={currentUser.userId}
3396
3402
  capabilities={capabilities}
@@ -3408,9 +3414,16 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
3408
3414
  showTrackedChanges={showTrackedChanges}
3409
3415
  workflowScopeSnapshot={workflowScopeSnapshot}
3410
3416
  layoutFacet={activeRuntime.layout}
3417
+ geometryFacet={activeRuntime.geometry}
3418
+ workflowFacet={activeRuntime.workflow}
3411
3419
  interactionGuardSnapshot={interactionGuardSnapshot}
3412
3420
  chromePreset={effectiveChromePreset}
3413
3421
  chromeOptions={chromeOptions}
3422
+ {...(editorActionHost ? { editorActionHost } : {})}
3423
+ {...(editorActionHost ? { chromeControllerRef: teedChromeControllerRef } : {})}
3424
+ {...(commandPaletteDisabled !== undefined
3425
+ ? { commandPaletteDisabled }
3426
+ : {})}
3414
3427
  {...(props.collabSession ? { collabSession: props.collabSession } : {})}
3415
3428
  {...(props.collabTransportStatus
3416
3429
  ? { collabTransportStatus: props.collabTransportStatus }
@@ -3511,13 +3524,11 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
3511
3524
  }}
3512
3525
  onDeselectObject={() => activeRuntime.deselectObject()}
3513
3526
  onScopeAskAgent={(payload) => {
3514
- // Resolve the scope's anchor + story from the facet's card
3515
- // model so the agent request carries the canonical range.
3516
- const facet = activeRuntime.layout;
3517
- const models =
3518
- facet && typeof facet.getAllScopeCardModels === "function"
3519
- ? facet.getAllScopeCardModels()
3520
- : [];
3527
+ // Resolve the scope's anchor + story from the workflow
3528
+ // facet's card model so the agent request carries the
3529
+ // canonical range. Layer-06 Slice 4 made `runtime.workflow`
3530
+ // the canonical source for scope-card data.
3531
+ const models = activeRuntime.workflow.getAllScopeCardModels();
3521
3532
  const model = models.find((entry) => entry.scopeId === payload.scopeId);
3522
3533
  if (!model) return;
3523
3534
  const scopeSnapshot = activeRuntime.getWorkflowScopeSnapshot();
@@ -3546,1182 +3557,52 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
3546
3557
  }}
3547
3558
  />
3548
3559
  <TwRuntimeReplDialog runtime={activeRuntime} editorRef={editorRefForRepl} />
3560
+ <TwDebugPresentation
3561
+ mode={debugMode}
3562
+ sessionId={documentId}
3563
+ />
3549
3564
  </>
3565
+ </OverlayAnchorBridgeProvider>
3566
+ </UiShellChannelsProvider>
3567
+ </UiApiProvider>
3550
3568
  );
3551
3569
  },
3552
3570
  );
3553
3571
 
3554
3572
  /**
3555
- * R3 best-effort suggestion-group accept/reject fan-out. Resolves
3556
- * the group's suggestions from the current snapshot, then fans out
3557
- * `acceptChange` / `rejectChange` across every changeId in each
3558
- * group member. P2 batches these in rapid succession; the runtime
3559
- * commit boundary collapses them into a single logical transaction.
3560
- * A future phase adds true atomicity at the runtime level.
3573
+ * Translate a public-API `TableOp` (kebab-case `kind` discriminator) to
3574
+ * the internal `TableStructureOperation` (`type` discriminator). Thin
3575
+ * re-export of the shell `publicTableOpToInternal` helper so legacy
3576
+ * tests keep their existing import path
3577
+ * (`test/api/table-op-translation.test.ts`).
3561
3578
  */
3562
- function applySuggestionGroupAction(
3563
- runtime: WordReviewEditorRuntime,
3564
- groupId: string,
3565
- action: "accept" | "reject",
3566
- ): void {
3567
- const snapshot = runtime.getSuggestionsSnapshot();
3568
- const group = snapshot.groups?.find((entry) => entry.groupId === groupId);
3569
- const op = action === "accept" ? "acceptSuggestionGroup" : "rejectSuggestionGroup";
3570
- if (!group) {
3571
- runtime.emitTransientWarning({
3572
- warningId: `suggestion-group-unknown-${groupId}-${Date.now()}`,
3573
- code: "review_target_not_found",
3574
- severity: "info",
3575
- message: `${op}("${groupId}") skipped: unknown groupId.`,
3576
- source: "review",
3577
- details: { op, targetId: groupId, reason: "group_unknown" },
3578
- });
3579
- return;
3580
- }
3581
- const byId = new Map(
3582
- snapshot.suggestions.map((entry) => [entry.suggestionId, entry]),
3583
- );
3584
- const skippedSuggestions: string[] = [];
3585
- for (const suggestionId of group.suggestionIds) {
3586
- const suggestion = byId.get(suggestionId);
3587
- if (!suggestion) {
3588
- skippedSuggestions.push(suggestionId);
3589
- continue;
3590
- }
3591
- for (const changeId of suggestion.changeIds) {
3592
- if (action === "accept") {
3593
- runtime.acceptChange(changeId);
3594
- } else {
3595
- runtime.rejectChange(changeId);
3596
- }
3597
- }
3598
- }
3599
- if (skippedSuggestions.length > 0) {
3600
- runtime.emitTransientWarning({
3601
- warningId: `suggestion-group-stale-${groupId}-${Date.now()}`,
3602
- code: "review_target_not_found",
3603
- severity: "info",
3604
- message: `${op}("${groupId}") partially skipped: ${skippedSuggestions.length} suggestion(s) no longer in snapshot.`,
3605
- source: "review",
3606
- details: {
3607
- op,
3608
- targetId: groupId,
3609
- reason: "suggestion_stale",
3610
- skippedSuggestionIds: skippedSuggestions,
3611
- },
3612
- });
3613
- }
3579
+ export function __publicTableOpToInternal(op: TableOp): TableStructureOperation {
3580
+ return publicTableOpToInternal(op);
3614
3581
  }
3615
3582
 
3616
- function applyRuntimeFormattingOperation(
3583
+ /**
3584
+ * Build the `ref.tables` facet: a typed dispatch boundary + capability
3585
+ * read. Delegates every op through the same `applyRuntimeTableStructureOperation`
3586
+ * helper the flat ref verbs use, so there is exactly one server-side path
3587
+ * for every table mutation.
3588
+ */
3589
+ function buildTablesFacet(
3617
3590
  runtime: WordReviewEditorRuntime,
3618
- operation:
3619
- | { type: "toggle"; mark: "bold" | "italic" | "underline" | "strikethrough" | "superscript" | "subscript" }
3620
- | { type: "set-font-family"; fontFamily: string | null }
3621
- | { type: "set-font-size"; size: number | null }
3622
- | { type: "set-text-color"; color: string | null }
3623
- | { type: "set-highlight-color"; color: string | null }
3624
- | { type: "set-alignment"; alignment: FormattingAlignment }
3625
- | { type: "indent" }
3626
- | { type: "outdent" },
3627
- ): void {
3628
- if (isSelectionSuggesting(runtime)) {
3629
- if (applySuggestingFormattingOperation(runtime, operation)) {
3630
- return;
3631
- }
3632
- }
3633
- if (emitSuggestingUnsupportedMutation(runtime, getFormattingOperationCommandName(operation))) {
3634
- return;
3635
- }
3636
- const context = getStoryMutationContext(runtime, getFormattingOperationCommandName(operation));
3637
- if (!context) {
3638
- return;
3639
- }
3640
-
3641
- const result = applyFormattingOperationToDocument(
3642
- context.localDocument,
3643
- context.localSnapshot,
3644
- operation,
3645
- );
3646
- dispatchStoryMutationResult(
3647
- runtime,
3648
- context,
3649
- {
3650
- ...result,
3651
- selection: toRuntimeSelectionSnapshot(result.selection),
3652
- },
3653
- context.timestamp,
3654
- );
3655
- }
3656
-
3657
- function applySuggestingFormattingOperation(
3658
- runtime: WordReviewEditorRuntime,
3659
- operation:
3660
- | { type: "toggle"; mark: "bold" | "italic" | "underline" | "strikethrough" | "superscript" | "subscript" }
3661
- | { type: "set-font-family"; fontFamily: string | null }
3662
- | { type: "set-font-size"; size: number | null }
3663
- | { type: "set-text-color"; color: string | null }
3664
- | { type: "set-highlight-color"; color: string | null }
3665
- | { type: "set-alignment"; alignment: FormattingAlignment }
3666
- | { type: "indent" }
3667
- | { type: "outdent" },
3668
- ): boolean {
3669
- const commandName = getFormattingOperationCommandName(operation);
3670
- const context = getStoryMutationContext(runtime, commandName);
3671
- if (!context) {
3672
- return true;
3673
- }
3674
- if (context.activeStory.kind !== "main") {
3675
- runtime.emitBlockedCommand(commandName, [{
3676
- code: "suggesting_unsupported",
3677
- message: `"${commandName}" is not supported in suggesting mode for this story.`,
3678
- }]);
3679
- return true;
3680
- }
3681
-
3682
- if (operation.type === "set-alignment" || operation.type === "indent" || operation.type === "outdent") {
3683
- const paragraphContext = resolveActiveParagraphContext(context.localSnapshot);
3684
- if (!paragraphContext) {
3685
- return true;
3686
- }
3687
- const beforeXml = buildParagraphPropertyBeforeXml(paragraphContext.paragraph);
3688
- const result = applyFormattingOperationToDocument(
3689
- context.localDocument,
3690
- context.localSnapshot,
3691
- operation,
3692
- );
3693
- if (!result.changed) {
3694
- return true;
3695
- }
3696
- const nextDocument = appendPropertyChangeSuggestion(
3697
- result.document,
3698
- {
3699
- from: paragraphContext.paragraph.from,
3700
- to: paragraphContext.paragraph.to,
3701
- },
3702
- {
3703
- originalRevisionType: "pPrChange",
3704
- xmlTag: "pPrChange",
3705
- beforeXml,
3706
- semanticKind: "paragraph-property-change",
3707
- storyTarget: context.activeStory,
3708
- authorId: runtime.getDefaultAuthorId?.(),
3709
- },
3710
- context.timestamp,
3711
- );
3712
- dispatchStoryMutationResult(
3713
- runtime,
3714
- context,
3715
- {
3716
- changed: true,
3717
- document: nextDocument,
3718
- selection: toRuntimeSelectionSnapshot(result.selection),
3719
- },
3720
- context.timestamp,
3721
- );
3722
- return true;
3723
- }
3724
-
3725
- const segment = findSingleSelectedTextSegment(context.localSnapshot);
3726
- if (!segment) {
3727
- runtime.emitBlockedCommand(commandName, [{
3728
- code: "suggesting_unsupported",
3729
- message: `"${commandName}" requires one bounded text segment in suggesting mode.`,
3730
- }]);
3731
- return true;
3732
- }
3733
- const beforeXml = buildRunPropertyBeforeXml(segment);
3734
- const result = applyFormattingOperationToDocument(
3735
- context.localDocument,
3736
- context.localSnapshot,
3737
- operation,
3738
- );
3739
- if (!result.changed) {
3740
- return true;
3741
- }
3742
- const nextDocument = appendPropertyChangeSuggestion(
3743
- result.document,
3744
- {
3745
- from: segment.from,
3746
- to: segment.to,
3747
- },
3748
- {
3749
- originalRevisionType: "rPrChange",
3750
- xmlTag: "rPrChange",
3751
- beforeXml,
3752
- semanticKind: "formatting-change",
3753
- storyTarget: context.activeStory,
3754
- authorId: runtime.getDefaultAuthorId?.(),
3755
- },
3756
- context.timestamp,
3757
- );
3758
- dispatchStoryMutationResult(
3759
- runtime,
3760
- context,
3761
- {
3762
- changed: true,
3763
- document: nextDocument,
3764
- selection: toRuntimeSelectionSnapshot(result.selection),
3765
- },
3766
- context.timestamp,
3767
- );
3768
- return true;
3769
- }
3770
-
3771
- function applyRuntimeListToggle(
3772
- runtime: WordReviewEditorRuntime,
3773
- kind: "bulleted" | "numbered",
3774
- ): void {
3775
- const commandName =
3776
- kind === "bulleted" ? "toggleBulletedList" : "toggleNumberedList";
3777
- if (emitSuggestingUnsupportedMutation(runtime, commandName)) {
3778
- return;
3779
- }
3780
- const context = getStoryMutationContext(runtime, commandName);
3781
- if (!context) {
3782
- return;
3783
- }
3784
-
3785
- const paragraphContext = resolveActiveParagraphContext(context.localSnapshot);
3786
- if (!paragraphContext) {
3787
- return;
3788
- }
3789
-
3790
- const result =
3791
- kind === "bulleted"
3792
- ? toggleBulletedList(
3793
- context.localDocument,
3794
- [paragraphContext.paragraphIndex],
3795
- { timestamp: context.timestamp },
3796
- )
3797
- : toggleNumberedList(
3798
- context.localDocument,
3799
- [paragraphContext.paragraphIndex],
3800
- { timestamp: context.timestamp },
3801
- );
3802
- dispatchStoryMutationResult(
3803
- runtime,
3804
- context,
3805
- {
3806
- changed: result.affectedParagraphIndexes.length > 0,
3807
- document: result.document,
3808
- selection: toRuntimeSelectionSnapshot(context.localSnapshot.selection),
3809
- },
3810
- context.timestamp,
3811
- );
3812
- }
3813
-
3814
- function getRuntimeStyleCatalog(
3815
- input:
3816
- | WordReviewEditorRuntime
3817
- | EditorSessionState["canonicalDocument"]["styles"],
3818
- ): StyleCatalogSnapshot {
3819
- const styles =
3820
- "getSessionState" in input
3821
- ? input.getSessionState().canonicalDocument.styles
3822
- : input;
3823
- const mapRecord = <
3824
- T extends {
3825
- styleId: string;
3826
- displayName: string;
3827
- kind: "paragraph" | "character" | "table";
3828
- isDefault: boolean;
3829
- basedOn?: string;
3830
- nextStyle?: string;
3831
- },
3832
- >(
3833
- record: Record<string, T>,
3834
- ) =>
3835
- Object.values(record)
3836
- .map((entry) => ({
3837
- styleId: entry.styleId,
3838
- displayName: entry.displayName,
3839
- kind: entry.kind,
3840
- isDefault: entry.isDefault,
3841
- ...(entry.basedOn ? { basedOn: entry.basedOn } : {}),
3842
- ...(entry.nextStyle ? { nextStyle: entry.nextStyle } : {}),
3843
- }))
3844
- .sort((left, right) =>
3845
- left.displayName.localeCompare(right.displayName) ||
3846
- left.styleId.localeCompare(right.styleId),
3847
- );
3848
-
3849
- return {
3850
- paragraphs: mapRecord(styles.paragraphs),
3851
- characters: mapRecord(styles.characters),
3852
- tables: mapRecord(styles.tables),
3853
- fromPackage: styles.fromPackage === true,
3854
- };
3855
- }
3856
-
3857
- function applyRuntimeParagraphStyle(
3858
- runtime: WordReviewEditorRuntime,
3859
- styleId: string | null,
3860
- ): void {
3861
- if (emitSuggestingUnsupportedMutation(runtime, "setParagraphStyle")) {
3862
- return;
3863
- }
3864
- const context = getStoryMutationContext(runtime, "setParagraphStyle");
3865
- if (!context) {
3866
- return;
3867
- }
3868
-
3869
- const result = applyParagraphStyleToDocument(
3870
- context.localDocument,
3871
- context.localSnapshot,
3872
- styleId,
3873
- );
3874
- dispatchStoryMutationResult(
3875
- runtime,
3876
- context,
3877
- {
3878
- ...result,
3879
- selection: toRuntimeSelectionSnapshot(result.selection),
3880
- },
3881
- context.timestamp,
3882
- );
3883
- }
3884
-
3885
- function applyRuntimeTableStyle(
3886
- runtime: WordReviewEditorRuntime,
3887
- styleId: string | null,
3888
- ): void {
3889
- if (emitSuggestingUnsupportedMutation(runtime, "setTableStyle")) {
3890
- return;
3891
- }
3892
- const context = getStoryMutationContext(runtime, "setTableStyle");
3893
- if (!context) {
3894
- return;
3895
- }
3896
-
3897
- const result = applyTableStyleToDocument(
3898
- context.localDocument,
3899
- context.localSnapshot,
3900
- styleId,
3901
- );
3902
- dispatchStoryMutationResult(
3903
- runtime,
3904
- context,
3905
- {
3906
- ...result,
3907
- selection: toRuntimeSelectionSnapshot(result.selection),
3908
- },
3909
- context.timestamp,
3910
- );
3911
- }
3912
-
3913
- function applyRuntimeParagraphIndentation(
3914
- runtime: WordReviewEditorRuntime,
3915
- indentation: {
3916
- left?: number;
3917
- right?: number;
3918
- firstLine?: number;
3919
- hanging?: number;
3920
- },
3921
- ): void {
3922
- if (emitSuggestingUnsupportedMutation(runtime, "setParagraphIndentation")) {
3923
- return;
3924
- }
3925
- const context = getStoryMutationContext(runtime, "setParagraphIndentation");
3926
- if (!context) {
3927
- return;
3928
- }
3929
-
3930
- const result = setActiveParagraphIndentation(
3931
- context.localDocument,
3932
- context.localSnapshot,
3933
- indentation,
3934
- { timestamp: context.timestamp },
3935
- );
3936
- dispatchStoryMutationResult(
3937
- runtime,
3938
- context,
3939
- {
3940
- ...result,
3941
- selection: toRuntimeSelectionSnapshot(result.selection),
3942
- },
3943
- context.timestamp,
3944
- );
3945
- }
3946
-
3947
- function applyRuntimeParagraphTabStops(
3948
- runtime: WordReviewEditorRuntime,
3949
- tabStops: Array<{ pos: number; val?: string; leader?: string }>,
3950
- ): void {
3951
- if (emitSuggestingUnsupportedMutation(runtime, "setParagraphTabStops")) {
3952
- return;
3953
- }
3954
- const context = getStoryMutationContext(runtime, "setParagraphTabStops");
3955
- if (!context) {
3956
- return;
3957
- }
3958
-
3959
- const result = setActiveParagraphTabStops(
3960
- context.localDocument,
3961
- context.localSnapshot,
3962
- tabStops,
3963
- { timestamp: context.timestamp },
3964
- );
3965
- dispatchStoryMutationResult(
3966
- runtime,
3967
- context,
3968
- {
3969
- ...result,
3970
- selection: toRuntimeSelectionSnapshot(result.selection),
3971
- },
3972
- context.timestamp,
3973
- );
3974
- }
3975
-
3976
- function applyRuntimeNumberingFlow(
3977
- runtime: WordReviewEditorRuntime,
3978
- operation: { type: "restart"; startAt?: number } | { type: "continue" },
3979
- ): void {
3980
- if (
3981
- emitSuggestingUnsupportedMutation(
3982
- runtime,
3983
- operation.type === "restart" ? "restartNumbering" : "continueNumbering",
3984
- )
3985
- ) {
3986
- return;
3987
- }
3988
- const context = getStoryMutationContext(
3989
- runtime,
3990
- operation.type === "restart" ? "restartNumbering" : "continueNumbering",
3991
- );
3992
- if (!context) {
3993
- return;
3994
- }
3995
-
3996
- const paragraphContext = resolveActiveParagraphContext(context.localSnapshot);
3997
- if (!paragraphContext?.paragraph.numbering) {
3998
- return;
3999
- }
4000
-
4001
- const result =
4002
- operation.type === "restart"
4003
- ? restartListNumbering(
4004
- context.localDocument,
4005
- paragraphContext.paragraphIndex,
4006
- { timestamp: context.timestamp },
4007
- operation.startAt,
4008
- )
4009
- : continueListNumbering(
4010
- context.localDocument,
4011
- paragraphContext.paragraphIndex,
4012
- { timestamp: context.timestamp },
4013
- );
4014
-
4015
- dispatchStoryMutationResult(
4016
- runtime,
4017
- context,
4018
- {
4019
- changed: result.affectedParagraphIndexes.length > 0,
4020
- document: result.document,
4021
- selection: toRuntimeSelectionSnapshot(context.localSnapshot.selection),
4022
- },
4023
- context.timestamp,
4024
- );
4025
- }
4026
-
4027
- function applyRuntimeInsertSectionBreak(
4028
- runtime: WordReviewEditorRuntime,
4029
- breakType: SectionBreakType,
4030
- options?: { afterSectionIndex?: number },
4031
- ): void {
4032
- const snapshot = runtime.getRenderSnapshot();
4033
- if (!canApplyRuntimeMutation(snapshot) || snapshot.activeStory.kind !== "main") {
4034
- return;
4035
- }
4036
- if (emitWorkflowBlockedMutation(runtime, "insertSectionBreak")) {
4037
- return;
4038
- }
4039
- if (isSelectionSuggesting(runtime)) {
4040
- runtime.emitBlockedCommand("insertSectionBreak", [{
4041
- code: "unsupported_surface",
4042
- message: "Section break insertion is not supported in suggesting mode.",
4043
- }]);
4044
- return;
4045
- }
4046
-
4047
- const sessionState = runtime.getSessionState();
4048
- const timestamp = new Date().toISOString();
4049
- const result =
4050
- typeof options?.afterSectionIndex === "number"
4051
- ? insertSectionBreakAfterSectionIndex(
4052
- sessionState.canonicalDocument,
4053
- options.afterSectionIndex,
4054
- breakType,
4055
- { timestamp },
4056
- )
4057
- : insertSectionBreakAfterSectionIndex(
4058
- sessionState.canonicalDocument,
4059
- runtime.getDocumentNavigationSnapshot().activeSectionIndex,
4060
- breakType,
4061
- { timestamp },
4062
- );
4063
-
4064
- dispatchRuntimeDocumentMutation(
4065
- runtime,
4066
- {
4067
- changed: result.changed,
4068
- document: result.document,
4069
- selection: toRuntimeSelectionSnapshot(result.selection),
4070
- },
4071
- timestamp,
4072
- );
4073
- }
4074
-
4075
- function emitSuggestingUnsupportedMutation(
4076
- runtime: WordReviewEditorRuntime,
4077
- command: string,
4078
- ): boolean {
4079
- if (!isSelectionSuggesting(runtime)) {
4080
- return false;
4081
- }
4082
-
4083
- runtime.emitBlockedCommand(command, [{
4084
- code: "suggesting_unsupported",
4085
- message: `"${command}" is not supported in suggesting mode.`,
4086
- }]);
4087
- return true;
4088
- }
4089
-
4090
- function appendPropertyChangeSuggestion(
4091
- document: EditorSessionState["canonicalDocument"],
4092
- anchor: { from: number; to: number },
4093
- input: {
4094
- originalRevisionType: "rPrChange" | "pPrChange";
4095
- xmlTag: "rPrChange" | "pPrChange";
4096
- beforeXml: string;
4097
- semanticKind: "formatting-change" | "paragraph-property-change";
4098
- storyTarget: EditorStoryTarget;
4099
- authorId?: string;
4100
- },
4101
- timestamp: string,
4102
- ): EditorSessionState["canonicalDocument"] {
4103
- const existing = document.review.revisions;
4104
- const changeId = createRuntimeSuggestionChangeId(existing, timestamp);
4105
- const resolvedAuthorId = input.authorId ?? "unknown";
4106
- return {
4107
- ...document,
4108
- review: {
4109
- ...document.review,
4110
- revisions: {
4111
- ...existing,
4112
- [changeId]: {
4113
- changeId,
4114
- kind: "property-change",
4115
- anchor: createRangeAnchor(anchor.from, anchor.to, { start: 1, end: -1 }),
4116
- authorId: resolvedAuthorId,
4117
- createdAt: timestamp,
4118
- warningIds: [],
4119
- metadata: {
4120
- source: "runtime",
4121
- storyTarget: input.storyTarget,
4122
- suggestionId: changeId,
4123
- semanticKind: input.semanticKind,
4124
- originalRevisionType: input.originalRevisionType,
4125
- propertyChangeData: {
4126
- xmlTag: input.xmlTag,
4127
- beforeXml: input.beforeXml,
4128
- },
4129
- },
4130
- status: "open",
4131
- },
4132
- },
4133
- },
4134
- };
4135
- }
4136
-
4137
- function createRuntimeSuggestionChangeId(
4138
- existing: EditorSessionState["canonicalDocument"]["review"]["revisions"],
4139
- timestamp: string,
4140
- ): string {
4141
- const base = `change-${timestamp.replace(/[^0-9]/gu, "")}`;
4142
- let counter = Object.keys(existing).length + 1;
4143
- let candidate = `${base}-p${counter}`;
4144
- while (existing[candidate]) {
4145
- counter += 1;
4146
- candidate = `${base}-p${counter}`;
4147
- }
4148
- return candidate;
4149
- }
4150
-
4151
- function findSingleSelectedTextSegment(
4152
- snapshot: Pick<RuntimeRenderSnapshot, "surface" | "selection">,
4153
- ): Extract<SurfaceInlineSegment, { kind: "text" }> | null {
4154
- if (!snapshot.surface || snapshot.selection.activeRange.kind !== "range" || snapshot.selection.isCollapsed) {
4155
- return null;
4156
- }
4157
- const selectionFrom = Math.min(snapshot.selection.anchor, snapshot.selection.head);
4158
- const selectionTo = Math.max(snapshot.selection.anchor, snapshot.selection.head);
4159
- const segments = collectSelectedTextSegments(snapshot.surface.blocks, selectionFrom, selectionTo);
4160
- if (segments.length !== 1) {
4161
- return null;
4162
- }
4163
- const [segment] = segments;
4164
- if (!segment || segment.from !== selectionFrom || segment.to !== selectionTo) {
4165
- return null;
4166
- }
4167
- return segment;
4168
- }
4169
-
4170
- function collectSelectedTextSegments(
4171
- blocks: readonly SurfaceBlockSnapshot[],
4172
- selectionFrom: number,
4173
- selectionTo: number,
4174
- output: Array<Extract<SurfaceInlineSegment, { kind: "text" }>> = [],
4175
- ): Array<Extract<SurfaceInlineSegment, { kind: "text" }>> {
4176
- for (const block of blocks) {
4177
- if (block.kind === "paragraph") {
4178
- for (const segment of block.segments) {
4179
- if (
4180
- segment.kind === "text" &&
4181
- rangesOverlap(selectionFrom, selectionTo, segment.from, segment.to)
4182
- ) {
4183
- output.push(segment);
4184
- }
4185
- }
4186
- continue;
4187
- }
4188
- if (block.kind === "table") {
4189
- for (const row of block.rows) {
4190
- for (const cell of row.cells) {
4191
- collectSelectedTextSegments(cell.content, selectionFrom, selectionTo, output);
4192
- }
4193
- }
4194
- continue;
4195
- }
4196
- if (block.kind === "sdt_block") {
4197
- collectSelectedTextSegments(block.children, selectionFrom, selectionTo, output);
4198
- }
4199
- }
4200
- return output;
4201
- }
4202
-
4203
- function buildRunPropertyBeforeXml(
4204
- segment: Extract<SurfaceInlineSegment, { kind: "text" }>,
4205
- ): string {
4206
- const parts: string[] = [];
4207
- const marks = new Set(segment.marks ?? []);
4208
- if (marks.has("bold")) parts.push("<w:b/>");
4209
- if (marks.has("italic")) parts.push("<w:i/>");
4210
- if (marks.has("underline")) parts.push("<w:u w:val=\"single\"/>");
4211
- if (marks.has("strikethrough")) parts.push("<w:strike/>");
4212
- if (marks.has("superscript")) parts.push("<w:vertAlign w:val=\"superscript\"/>");
4213
- if (marks.has("subscript")) parts.push("<w:vertAlign w:val=\"subscript\"/>");
4214
- if (segment.markAttrs?.fontFamily) {
4215
- parts.push(`<w:rFonts w:ascii="${escapeAttributeXml(segment.markAttrs.fontFamily)}" w:hAnsi="${escapeAttributeXml(segment.markAttrs.fontFamily)}"/>`);
4216
- }
4217
- if (segment.markAttrs?.fontSize !== undefined) {
4218
- parts.push(`<w:sz w:val="${segment.markAttrs.fontSize}"/>`);
4219
- }
4220
- if (segment.markAttrs?.textColor) {
4221
- parts.push(`<w:color w:val="${escapeAttributeXml(segment.markAttrs.textColor)}"/>`);
4222
- }
4223
- if (segment.markAttrs?.backgroundColor) {
4224
- parts.push(`<w:shd w:val="clear" w:color="auto" w:fill="${escapeAttributeXml(segment.markAttrs.backgroundColor)}"/>`);
4225
- }
4226
- return `<w:rPr>${parts.join("")}</w:rPr>`;
4227
- }
4228
-
4229
- function buildParagraphPropertyBeforeXml(
4230
- paragraph: Extract<SurfaceBlockSnapshot, { kind: "paragraph" }>,
4231
- ): string {
4232
- const parts: string[] = [];
4233
- if (paragraph.styleId) {
4234
- parts.push(`<w:pStyle w:val="${escapeAttributeXml(paragraph.styleId)}"/>`);
4235
- }
4236
- if (paragraph.numbering) {
4237
- parts.push(
4238
- `<w:numPr><w:ilvl w:val="${paragraph.numbering.level}"/><w:numId w:val="${escapeAttributeXml(
4239
- paragraph.numbering.numberingInstanceId.replace(/^num:/u, ""),
4240
- )}"/></w:numPr>`,
4241
- );
4242
- }
4243
- if (paragraph.alignment) {
4244
- parts.push(`<w:jc w:val="${escapeAttributeXml(paragraph.alignment)}"/>`);
4245
- }
4246
- if (paragraph.indentation) {
4247
- const attrs: string[] = [];
4248
- if (paragraph.indentation.left !== undefined) attrs.push(`w:left="${paragraph.indentation.left}"`);
4249
- if (paragraph.indentation.right !== undefined) attrs.push(`w:right="${paragraph.indentation.right}"`);
4250
- if (paragraph.indentation.firstLine !== undefined) attrs.push(`w:firstLine="${paragraph.indentation.firstLine}"`);
4251
- if (paragraph.indentation.hanging !== undefined) attrs.push(`w:hanging="${paragraph.indentation.hanging}"`);
4252
- if (attrs.length > 0) {
4253
- parts.push(`<w:ind ${attrs.join(" ")}/>`);
4254
- }
4255
- }
4256
- return `<w:pPr>${parts.join("")}</w:pPr>`;
4257
- }
4258
-
4259
- function escapeAttributeXml(value: string): string {
4260
- return value
4261
- .replace(/&/g, "&amp;")
4262
- .replace(/</g, "&lt;")
4263
- .replace(/>/g, "&gt;")
4264
- .replace(/"/g, "&quot;");
4265
- }
4266
-
4267
- function isSelectionSuggesting(runtime: WordReviewEditorRuntime): boolean {
4268
- return runtime.getInteractionGuardSnapshot().effectiveMode === "suggest";
4269
- }
4270
-
4271
- function getFormattingOperationCommandName(
4272
- operation:
4273
- | { type: "toggle"; mark: "bold" | "italic" | "underline" | "strikethrough" | "superscript" | "subscript" }
4274
- | { type: "set-font-family"; fontFamily: string | null }
4275
- | { type: "set-font-size"; size: number | null }
4276
- | { type: "set-text-color"; color: string | null }
4277
- | { type: "set-highlight-color"; color: string | null }
4278
- | { type: "set-alignment"; alignment: FormattingAlignment }
4279
- | { type: "indent" }
4280
- | { type: "outdent" },
4281
- ): string {
4282
- switch (operation.type) {
4283
- case "toggle":
4284
- return `toggle${operation.mark.charAt(0).toUpperCase()}${operation.mark.slice(1)}`;
4285
- case "set-font-family":
4286
- return "setFontFamily";
4287
- case "set-font-size":
4288
- return "setFontSize";
4289
- case "set-text-color":
4290
- return "setTextColor";
4291
- case "set-highlight-color":
4292
- return "setHighlightColor";
4293
- case "set-alignment":
4294
- return "setAlignment";
4295
- case "indent":
4296
- return "indent";
4297
- case "outdent":
4298
- return "outdent";
4299
- }
4300
- }
4301
-
4302
- function applyRuntimeDeleteSectionBreak(
4303
- runtime: WordReviewEditorRuntime,
4304
- sectionIndex: number,
4305
- ): void {
4306
- const snapshot = runtime.getRenderSnapshot();
4307
- if (!canApplyRuntimeMutation(snapshot) || snapshot.activeStory.kind !== "main") {
4308
- return;
4309
- }
4310
- if (emitWorkflowBlockedMutation(runtime, "deleteSectionBreak")) {
4311
- return;
4312
- }
4313
- if (isSelectionSuggesting(runtime)) {
4314
- runtime.emitBlockedCommand("deleteSectionBreak", [{
4315
- code: "unsupported_surface",
4316
- message: "Section break deletion is not supported in suggesting mode.",
4317
- }]);
4318
- return;
4319
- }
4320
-
4321
- const sessionState = runtime.getSessionState();
4322
- const timestamp = new Date().toISOString();
4323
- const result = deleteSectionBreakAtSectionIndex(
4324
- sessionState.canonicalDocument,
4325
- sectionIndex,
4326
- { timestamp },
4327
- );
4328
-
4329
- dispatchRuntimeDocumentMutation(
4330
- runtime,
4331
- {
4332
- changed: result.changed,
4333
- document: result.document,
4334
- selection: toRuntimeSelectionSnapshot(result.selection),
4335
- },
4336
- timestamp,
4337
- );
4338
- }
4339
-
4340
- function applyRuntimeUpdateSectionLayout(
4341
- runtime: WordReviewEditorRuntime,
4342
- sectionIndex: number,
4343
- patch: SectionLayoutPatch,
4344
- ): void {
4345
- const snapshot = runtime.getRenderSnapshot();
4346
- if (!canApplyRuntimeMutation(snapshot) || snapshot.activeStory.kind !== "main") {
4347
- return;
4348
- }
4349
- if (emitWorkflowBlockedMutation(runtime, "updateSectionLayout")) {
4350
- return;
4351
- }
4352
- if (isSelectionSuggesting(runtime)) {
4353
- runtime.emitBlockedCommand("updateSectionLayout", [{
4354
- code: "unsupported_surface",
4355
- message: "Section layout updates are not supported in suggesting mode.",
4356
- }]);
4357
- return;
4358
- }
4359
-
4360
- const sessionState = runtime.getSessionState();
4361
- const timestamp = new Date().toISOString();
4362
- const result = updateSectionLayoutAtSectionIndex(
4363
- sessionState.canonicalDocument,
4364
- sectionIndex,
4365
- {
4366
- ...(patch.pageSize ? { pageSize: patch.pageSize } : {}),
4367
- ...(patch.pageMargins ? { pageMargins: patch.pageMargins } : {}),
4368
- ...(patch.columns ? { columns: patch.columns } : {}),
4369
- ...(patch.titlePage !== undefined ? { titlePage: patch.titlePage } : {}),
4370
- ...(patch.sectionType ? { sectionType: patch.sectionType } : {}),
4371
- },
4372
- { timestamp },
4373
- );
4374
-
4375
- dispatchRuntimeDocumentMutation(
4376
- runtime,
4377
- {
4378
- changed: result.changed,
4379
- document: result.document,
4380
- selection: toRuntimeSelectionSnapshot(result.selection),
4381
- },
4382
- timestamp,
4383
- );
4384
- }
4385
-
4386
- function applyRuntimeSetSectionPageNumbering(
4387
- runtime: WordReviewEditorRuntime,
4388
- sectionIndex: number,
4389
- patch: SectionPageNumberingPatch | null,
4390
- ): void {
4391
- const snapshot = runtime.getRenderSnapshot();
4392
- if (!canApplyRuntimeMutation(snapshot) || snapshot.activeStory.kind !== "main") {
4393
- return;
4394
- }
4395
- if (emitWorkflowBlockedMutation(runtime, "setSectionPageNumbering")) {
4396
- return;
4397
- }
4398
- if (isSelectionSuggesting(runtime)) {
4399
- runtime.emitBlockedCommand("setSectionPageNumbering", [{
4400
- code: "unsupported_surface",
4401
- message: "Section page numbering updates are not supported in suggesting mode.",
4402
- }]);
4403
- return;
4404
- }
4405
-
4406
- const sessionState = runtime.getSessionState();
4407
- const timestamp = new Date().toISOString();
4408
- const normalizedPatch =
4409
- patch === null
4410
- ? null
4411
- : {
4412
- ...(patch.format !== undefined
4413
- ? { format: patch.format ?? undefined }
4414
- : {}),
4415
- ...(patch.start !== undefined
4416
- ? { start: patch.start ?? undefined }
4417
- : {}),
4418
- ...(patch.chapterStyle !== undefined
4419
- ? { chapStyle: patch.chapterStyle ?? undefined }
4420
- : {}),
4421
- ...(patch.chapterSeparator !== undefined
4422
- ? { chapSep: patch.chapterSeparator ?? undefined }
4423
- : {}),
4424
- };
4425
- const result = setSectionPageNumberingAtSectionIndex(
4426
- sessionState.canonicalDocument,
4427
- sectionIndex,
4428
- normalizedPatch,
4429
- { timestamp },
4430
- );
4431
-
4432
- dispatchRuntimeDocumentMutation(
4433
- runtime,
4434
- {
4435
- changed: result.changed,
4436
- document: result.document,
4437
- selection: toRuntimeSelectionSnapshot(result.selection),
4438
- },
4439
- timestamp,
4440
- );
4441
- }
4442
-
4443
- function applyRuntimeSetHeaderFooterLink(
4444
- runtime: WordReviewEditorRuntime,
4445
- sectionIndex: number,
4446
- patch: HeaderFooterLinkPatch,
4447
- ): void {
4448
- const snapshot = runtime.getRenderSnapshot();
4449
- if (!canApplyRuntimeMutation(snapshot) || snapshot.activeStory.kind !== "main") {
4450
- return;
4451
- }
4452
- if (emitWorkflowBlockedMutation(runtime, "setHeaderFooterLink")) {
4453
- return;
4454
- }
4455
- if (isSelectionSuggesting(runtime)) {
4456
- runtime.emitBlockedCommand("setHeaderFooterLink", [{
4457
- code: "unsupported_surface",
4458
- message: "Header and footer linkage updates are not supported in suggesting mode.",
4459
- }]);
4460
- return;
4461
- }
4462
-
4463
- const sessionState = runtime.getSessionState();
4464
- const timestamp = new Date().toISOString();
4465
- const result = setHeaderFooterLinkAtSectionIndex(
4466
- sessionState.canonicalDocument,
4467
- sectionIndex,
4468
- patch,
4469
- { timestamp },
4470
- );
4471
-
4472
- dispatchRuntimeDocumentMutation(
4473
- runtime,
4474
- {
4475
- changed: result.changed,
4476
- document: result.document,
4477
- selection: toRuntimeSelectionSnapshot(result.selection),
4478
- },
4479
- timestamp,
4480
- );
4481
- }
4482
-
4483
- function applyRuntimeInsertPageBreak(runtime: WordReviewEditorRuntime): void {
4484
- if (isSelectionSuggesting(runtime)) {
4485
- runtime.emitBlockedCommand("insertPageBreak", [{
4486
- code: "unsupported_surface",
4487
- message: "Page break insertion is not supported in suggesting mode.",
4488
- }]);
4489
- return;
4490
- }
4491
- const context = getStoryMutationContext(runtime, "insertPageBreak");
4492
- if (!context) {
4493
- return;
4494
- }
4495
-
4496
- const result = insertPageBreakInDocument(
4497
- context.localDocument,
4498
- toRuntimeSelectionSnapshot(context.localSnapshot.selection),
4499
- { timestamp: context.timestamp },
4500
- );
4501
- dispatchStoryMutationResult(runtime, context, result, context.timestamp);
4502
- }
4503
-
4504
- function applyRuntimeInsertTable(
4505
- runtime: WordReviewEditorRuntime,
4506
- options: InsertTableOptions,
4507
- ): void {
4508
- if (isSelectionSuggesting(runtime)) {
4509
- runtime.emitBlockedCommand("insertTable", [{
4510
- code: "unsupported_surface",
4511
- message: "Table insertion is not supported in suggesting mode.",
4512
- }]);
4513
- return;
4514
- }
4515
- const context = getStoryMutationContext(runtime, "insertTable");
4516
- if (!context) {
4517
- return;
4518
- }
4519
-
4520
- const result = insertTableInDocument(
4521
- context.localDocument,
4522
- toRuntimeSelectionSnapshot(context.localSnapshot.selection),
4523
- options,
4524
- { timestamp: context.timestamp },
4525
- );
4526
- dispatchStoryMutationResult(runtime, context, result, context.timestamp);
4527
- }
4528
-
4529
- function applyRuntimeInsertImage(
4530
- runtime: WordReviewEditorRuntime,
4531
- options: InsertImageOptions,
4532
- ): void {
4533
- if (isSelectionSuggesting(runtime)) {
4534
- runtime.emitBlockedCommand("insertImage", [{
4535
- code: "unsupported_surface",
4536
- message: "Image insertion is not supported in suggesting mode.",
4537
- }]);
4538
- return;
4539
- }
4540
- const context = getStoryMutationContext(runtime, "insertImage");
4541
- if (!context) {
4542
- return;
4543
- }
4544
-
4545
- try {
4546
- const result = insertImageInDocument(
4547
- context.localDocument,
4548
- toRuntimeSelectionSnapshot(context.localSnapshot.selection),
4549
- options.data,
4550
- options.mimeType,
4551
- options.width,
4552
- options.height,
4553
- {
4554
- timestamp: context.timestamp,
4555
- altText: options.altText,
4556
- },
4557
- );
4558
- dispatchStoryMutationResult(runtime, context, {
4559
- changed: true,
4560
- document: result.document,
4561
- selection: result.selection,
4562
- mapping: result.mapping,
4563
- }, context.timestamp);
4564
- } catch {
4565
- return;
4566
- }
4567
- }
4568
-
4569
- function applyRuntimeImageResize(
4570
- runtime: WordReviewEditorRuntime,
4571
- mediaId: string,
4572
- dimensions: { widthEmu: number; heightEmu: number },
4573
- ): void {
4574
- const snapshot = runtime.getRenderSnapshot();
4575
- if (!canApplyRuntimeMutation(snapshot)) {
4576
- return;
4577
- }
4578
- if (emitWorkflowBlockedMutation(runtime, "setImageLayout")) {
4579
- return;
4580
- }
4581
- if (isSelectionSuggesting(runtime)) {
4582
- runtime.emitBlockedCommand("setImageLayout", [{
4583
- code: "unsupported_surface",
4584
- message: "Image resize is not supported in suggesting mode.",
4585
- }]);
4586
- return;
4587
- }
4588
-
4589
- try {
4590
- const sessionState = runtime.getSessionState();
4591
- const result = resizeImageInCatalog(
4592
- sessionState.canonicalDocument,
4593
- mediaId,
4594
- dimensions,
4595
- );
4596
- runtime.dispatch({
4597
- type: "document.replace",
4598
- document: result.document,
4599
- selection: toRuntimeSelectionSnapshot(snapshot.selection),
4600
- origin: { source: "api", timestamp: new Date().toISOString() },
4601
- });
4602
- } catch {
4603
- return;
4604
- }
4605
- }
4606
-
4607
- function applyRuntimeImageReposition(
4608
- runtime: WordReviewEditorRuntime,
4609
- mediaId: string,
4610
- offsets: { horizontalOffsetEmu?: number; verticalOffsetEmu?: number },
4611
- ): void {
4612
- if (emitWorkflowBlockedMutation(runtime, "setImageFrame")) {
4613
- return;
4614
- }
4615
- if (isSelectionSuggesting(runtime)) {
4616
- runtime.emitBlockedCommand("setImageFrame", [{
4617
- code: "unsupported_surface",
4618
- message: "Image reposition is not supported in suggesting mode.",
4619
- }]);
4620
- return;
4621
- }
4622
- const context = getStoryMutationContext(runtime, "setImageFrame");
4623
- if (!context) {
4624
- return;
4625
- }
4626
-
4627
- try {
4628
- const result = repositionFloatingImageInDocument(
4629
- context.localDocument,
4630
- mediaId,
4631
- offsets,
4632
- context.timestamp,
4633
- );
4634
- dispatchStoryMutationResult(
4635
- runtime,
4636
- context,
4637
- {
4638
- changed: true,
4639
- document: result.document,
4640
- selection: toRuntimeSelectionSnapshot(context.localSnapshot.selection),
4641
- },
4642
- context.timestamp,
4643
- );
4644
- } catch {
4645
- return;
4646
- }
4647
- }
4648
-
4649
- // deriveViewState removed — the runtime's getViewState() is now the single
4650
- // source of truth for EditorViewStateSnapshot, backed by view-state.ts.
4651
-
4652
- function applyRuntimeTableStructureOperation(
4653
- runtime: WordReviewEditorRuntime,
4654
- mountedSurface: TwProseMirrorSurfaceRef | null | undefined,
4655
- operation: TableStructureOperation,
4656
- ): { changed: boolean; coercedReason: string | null } {
4657
- if (isSelectionSuggesting(runtime)) {
4658
- const coercedReason = `Table operation "${operation.type}" is not supported in suggesting mode.`;
4659
- runtime.emitBlockedCommand(`table.${operation.type}`, [{
4660
- code: "unsupported_surface",
4661
- message: coercedReason,
4662
- }]);
4663
- return { changed: false, coercedReason };
4664
- }
4665
- const context = getStoryMutationContext(runtime, `table.${operation.type}`);
4666
- if (!context) {
4667
- return { changed: false, coercedReason: "No active mutation context." };
4668
- }
4669
-
4670
- const result = applyTableStructureOperation(
4671
- context.localDocument,
4672
- context.localSnapshot,
4673
- mountedSurface?.getTableSelection() ?? null,
4674
- operation,
4675
- );
4676
- dispatchStoryMutationResult(runtime, context, result, context.timestamp);
4677
- return {
4678
- changed: result.changed,
4679
- coercedReason: result.changed ? null : "Op was a no-op against the active selection.",
4680
- };
4681
- }
4682
-
4683
- /**
4684
- * Translate a public-API `TableOp` (kebab-case `kind` discriminator) to
4685
- * the internal `TableStructureOperation` (`type` discriminator). The
4686
- * shape values are identical aside from the discriminator name.
4687
- */
4688
- export function __publicTableOpToInternal(op: TableOp): TableStructureOperation {
4689
- return publicTableOpToInternal(op);
4690
- }
4691
-
4692
- function publicTableOpToInternal(op: TableOp): TableStructureOperation {
4693
- const { kind, ...rest } = op as { kind: string } & Record<string, unknown>;
4694
- if (kind === "insert") {
4695
- throw new Error(
4696
- "TableOp kind \"insert\" is not routed through ref.tables.apply; use ref.insertTable(...).",
4697
- );
4698
- }
4699
- return { type: kind, ...rest } as TableStructureOperation;
4700
- }
4701
-
4702
- /**
4703
- * Build the `ref.tables` facet: a typed dispatch boundary + capability
4704
- * read. Delegates every op through the same `applyRuntimeTableStructureOperation`
4705
- * helper the flat ref verbs use, so there is exactly one server-side path
4706
- * for every table mutation.
4707
- */
4708
- function buildTablesFacet(
4709
- runtime: WordReviewEditorRuntime,
4710
- mountedSurface: TwProseMirrorSurfaceRef | null,
4711
- ) {
4712
- const getCapabilities = () => {
4713
- const snapshot = runtime.getRenderSnapshot();
4714
- const document = runtime.getCanonicalDocument();
4715
- return (
4716
- clonePublicValue(
4717
- getTableStructureContext(
4718
- document,
4719
- snapshot,
4720
- mountedSurface?.getTableSelection() ?? null,
4721
- ),
4722
- ) ?? null
4723
- );
4724
- };
3591
+ mountedSurface: TwProseMirrorSurfaceRef | null,
3592
+ ) {
3593
+ const getCapabilities = () => {
3594
+ const snapshot = runtime.getRenderSnapshot();
3595
+ const document = runtime.getCanonicalDocument();
3596
+ return (
3597
+ clonePublicValue(
3598
+ getTableStructureContext(
3599
+ document,
3600
+ snapshot,
3601
+ mountedSurface?.getTableSelection() ?? null,
3602
+ ),
3603
+ ) ?? null
3604
+ );
3605
+ };
4725
3606
 
4726
3607
  const buildSummary = (
4727
3608
  table: Extract<ReturnType<typeof runtime.getCanonicalDocument>["content"]["children"][number], { type: "table" }>,
@@ -4930,324 +3811,8 @@ function buildTablesFacet(
4930
3811
 
4931
3812
  export { buildTablesFacet as __buildTablesFacet };
4932
3813
 
4933
- const DISPATCH_CONTEXT: DispatchContext = {
4934
- getStoryMutationContext,
4935
- dispatchStoryMutationResult,
4936
- resolveActiveParagraphContext,
4937
- toRuntimeSelectionSnapshot,
4938
- };
4939
-
4940
- function resolveActiveParagraphContext(
4941
- snapshot: Pick<RuntimeRenderSnapshot, "surface" | "selection">,
4942
- ): {
4943
- paragraphIndex: number;
4944
- paragraph: Extract<SurfaceBlockSnapshot, { kind: "paragraph" }>;
4945
- atParagraphStart: boolean;
4946
- isEmpty: boolean;
4947
- } | null {
4948
- if (!snapshot.surface) {
4949
- return null;
4950
- }
4951
-
4952
- const paragraphIndex = resolveActiveParagraphIndex(
4953
- snapshot.surface.blocks,
4954
- snapshot.selection,
4955
- );
4956
- if (paragraphIndex === null) {
4957
- return null;
4958
- }
4959
-
4960
- const selectionPosition =
4961
- snapshot.selection.activeRange.kind === "node"
4962
- ? snapshot.selection.activeRange.at
4963
- : snapshot.selection.head;
4964
- const paragraph = findSurfaceParagraphAtPosition(snapshot.surface.blocks, selectionPosition);
4965
- if (!paragraph) {
4966
- return null;
4967
- }
4968
-
4969
- return {
4970
- paragraphIndex,
4971
- paragraph,
4972
- atParagraphStart:
4973
- snapshot.selection.isCollapsed &&
4974
- snapshot.selection.activeRange.kind !== "node" &&
4975
- snapshot.selection.anchor === snapshot.selection.head &&
4976
- snapshot.selection.head === paragraph.from,
4977
- isEmpty: isSurfaceParagraphEmpty(paragraph),
4978
- };
4979
- }
4980
-
4981
- function findSurfaceParagraphAtPosition(
4982
- blocks: readonly SurfaceBlockSnapshot[],
4983
- position: number,
4984
- ): Extract<SurfaceBlockSnapshot, { kind: "paragraph" }> | null {
4985
- for (const block of blocks) {
4986
- if (position < block.from || position > block.to) {
4987
- continue;
4988
- }
4989
- if (block.kind === "paragraph") {
4990
- return block;
4991
- }
4992
- if (block.kind === "table") {
4993
- for (const row of block.rows) {
4994
- for (const cell of row.cells) {
4995
- const paragraph = findSurfaceParagraphAtPosition(cell.content, position);
4996
- if (paragraph) {
4997
- return paragraph;
4998
- }
4999
- }
5000
- }
5001
- continue;
5002
- }
5003
- if (block.kind === "sdt_block") {
5004
- const paragraph = findSurfaceParagraphAtPosition(block.children, position);
5005
- if (paragraph) {
5006
- return paragraph;
5007
- }
5008
- }
5009
- }
5010
- return null;
5011
- }
5012
-
5013
- function isSurfaceParagraphEmpty(
5014
- paragraph: Extract<SurfaceBlockSnapshot, { kind: "paragraph" }>,
5015
- ): boolean {
5016
- if (paragraph.segments.length === 0) {
5017
- return true;
5018
- }
5019
- return paragraph.segments.every((segment) => segment.kind === "text" && segment.text.length === 0);
5020
- }
5021
-
5022
- function applyRuntimeSelection(
5023
- runtime: WordReviewEditorRuntime,
5024
- selection: PublicSelectionSnapshot,
5025
- ): void {
5026
- const requestedStory = selection.storyTarget ?? { kind: "main" };
5027
- if (requestedStory.kind === "main") {
5028
- runtime.closeStory();
5029
- } else if (!storyTargetsEqual(runtime.getActiveStory(), requestedStory)) {
5030
- if (!runtime.openStory(requestedStory)) {
5031
- return;
5032
- }
5033
- }
5034
-
5035
- runtime.dispatch({
5036
- type: "selection.set",
5037
- selection: toRuntimeSelectionSnapshot(stripStoryTarget(selection)),
5038
- });
5039
- }
5040
-
5041
- function canApplyRuntimeMutation(snapshot: RuntimeRenderSnapshot): boolean {
5042
- return snapshot.isReady && !snapshot.readOnly && !snapshot.fatalError;
5043
- }
5044
-
5045
- function emitWorkflowBlockedMutation(
5046
- runtime: WordReviewEditorRuntime,
5047
- command: string,
5048
- ): boolean {
5049
- const interactionGuardSnapshot = runtime.getInteractionGuardSnapshot();
5050
- if (interactionGuardSnapshot.blockedReasons.length === 0) {
5051
- return false;
5052
- }
5053
- runtime.emitBlockedCommand(command, interactionGuardSnapshot.blockedReasons);
5054
- return true;
5055
- }
5056
-
5057
- function getStoryMutationContext(
5058
- runtime: WordReviewEditorRuntime,
5059
- command?: string,
5060
- ): {
5061
- timestamp: string;
5062
- activeStory: EditorStoryTarget;
5063
- persistedDocument: EditorSessionState["canonicalDocument"];
5064
- localDocument: EditorSessionState["canonicalDocument"];
5065
- localSnapshot: RuntimeRenderSnapshot;
5066
- } | null {
5067
- const snapshot = runtime.getRenderSnapshot();
5068
- if (!canApplyRuntimeMutation(snapshot)) {
5069
- return null;
5070
- }
5071
- if (command && emitWorkflowBlockedMutation(runtime, command)) {
5072
- return null;
5073
- }
5074
-
5075
- const persistedDocument = runtime.getSessionState().canonicalDocument;
5076
- const activeStory = snapshot.activeStory;
5077
- if (activeStory.kind === "main") {
5078
- return {
5079
- timestamp: new Date().toISOString(),
5080
- activeStory,
5081
- persistedDocument,
5082
- localDocument: persistedDocument,
5083
- localSnapshot: snapshot,
5084
- };
5085
- }
5086
-
5087
- return {
5088
- timestamp: new Date().toISOString(),
5089
- activeStory,
5090
- persistedDocument,
5091
- localDocument: {
5092
- ...persistedDocument,
5093
- content: {
5094
- type: "doc",
5095
- children: [...getStoryBlocks(persistedDocument, activeStory)],
5096
- },
5097
- },
5098
- localSnapshot: {
5099
- ...snapshot,
5100
- activeStory: { kind: "main" },
5101
- selection: stripStoryTarget(snapshot.selection),
5102
- },
5103
- };
5104
- }
5105
-
5106
- function dispatchStoryMutationResult(
5107
- runtime: WordReviewEditorRuntime,
5108
- context: {
5109
- activeStory: EditorStoryTarget;
5110
- persistedDocument: EditorSessionState["canonicalDocument"];
5111
- },
5112
- result: {
5113
- changed: boolean;
5114
- document: EditorSessionState["canonicalDocument"];
5115
- selection: InternalSelectionSnapshot;
5116
- mapping?: TransactionMapping;
5117
- },
5118
- timestamp: string,
5119
- ): void {
5120
- if (context.activeStory.kind === "main") {
5121
- dispatchRuntimeDocumentMutation(runtime, result, timestamp);
5122
- return;
5123
- }
5124
-
5125
- if (!result.changed) {
5126
- return;
5127
- }
5128
-
5129
- const nextDocument = replaceStoryBlocks(
5130
- context.persistedDocument,
5131
- context.activeStory,
5132
- result.document.content.children,
5133
- );
5134
- dispatchRuntimeDocumentMutation(
5135
- runtime,
5136
- {
5137
- changed: true,
5138
- document: nextDocument,
5139
- selection: result.selection,
5140
- },
5141
- timestamp,
5142
- );
5143
- }
5144
-
5145
- function dispatchRuntimeDocumentMutation(
5146
- runtime: WordReviewEditorRuntime,
5147
- result: {
5148
- changed: boolean;
5149
- document: EditorSessionState["canonicalDocument"];
5150
- selection: InternalSelectionSnapshot;
5151
- mapping?: TransactionMapping;
5152
- },
5153
- timestamp: string,
5154
- ): void {
5155
- if (!result.changed) {
5156
- return;
5157
- }
5158
-
5159
- runtime.dispatch({
5160
- type: "document.replace",
5161
- document: {
5162
- ...result.document,
5163
- updatedAt: timestamp,
5164
- },
5165
- mapping: result.mapping,
5166
- selection: result.selection,
5167
- origin: {
5168
- source: "api",
5169
- timestamp,
5170
- },
5171
- });
5172
- }
5173
-
5174
- function stripStoryTarget(
5175
- selection: PublicSelectionSnapshot,
5176
- ): PublicSelectionSnapshot {
5177
- const { storyTarget: _storyTarget, ...rest } = selection;
5178
- return rest;
5179
- }
5180
-
5181
- function applyRuntimeDeleteComment(
5182
- runtime: WordReviewEditorRuntime,
5183
- commentId: string,
5184
- ): void {
5185
- const snapshot = runtime.getRenderSnapshot();
5186
- // Pre-ready / fatal states stay silent: the host called too early, and
5187
- // there is no meaningful document yet to signal against. Emitting a
5188
- // warning here would only add noise to load-time error handling.
5189
- if (!snapshot.isReady || snapshot.fatalError) {
5190
- return;
5191
- }
5192
- if (snapshot.readOnly) {
5193
- runtime.emitTransientWarning({
5194
- warningId: `delete-comment-readonly-${commentId}-${Date.now()}`,
5195
- code: "review_target_not_found",
5196
- severity: "info",
5197
- message: `deleteComment("${commentId}") skipped: editor is read-only.`,
5198
- source: "review",
5199
- details: { op: "deleteComment", targetId: commentId, reason: "read_only" },
5200
- });
5201
- return;
5202
- }
5203
-
5204
- const sessionState = runtime.getSessionState();
5205
- if (!sessionState.canonicalDocument.review.comments[commentId]) {
5206
- runtime.emitTransientWarning({
5207
- warningId: `delete-comment-unknown-${commentId}-${Date.now()}`,
5208
- code: "review_target_not_found",
5209
- severity: "info",
5210
- message: `deleteComment("${commentId}") skipped: unknown commentId.`,
5211
- source: "review",
5212
- details: { op: "deleteComment", targetId: commentId, reason: "comment_unknown" },
5213
- });
5214
- return;
5215
- }
5216
3814
 
5217
- const nextComments = {
5218
- ...sessionState.canonicalDocument.review.comments,
5219
- };
5220
- delete nextComments[commentId];
5221
-
5222
- runtime.dispatch({
5223
- type: "document.replace",
5224
- document: {
5225
- ...sessionState.canonicalDocument,
5226
- review: {
5227
- ...sessionState.canonicalDocument.review,
5228
- comments: nextComments,
5229
- },
5230
- },
5231
- selection: toRuntimeSelectionSnapshot(snapshot.selection),
5232
- origin: {
5233
- source: "api",
5234
- timestamp: new Date().toISOString(),
5235
- },
5236
- });
5237
- }
5238
3815
 
5239
- function normalizeRequestedSelection(
5240
- snapshot: RuntimeRenderSnapshot,
5241
- selection: PublicSelectionSnapshot | null,
5242
- ): PublicSelectionSnapshot {
5243
- return (
5244
- selection ??
5245
- createCollapsedPublicSelection(
5246
- snapshot.selection.head,
5247
- snapshot.activeStory.kind === "main" ? undefined : snapshot.activeStory,
5248
- )
5249
- );
5250
- }
5251
3816
 
5252
3817
  export function __resolveLiveMarkupDisplay(
5253
3818
  requested: MarkupDisplay | undefined,
@@ -5256,45 +3821,11 @@ export function __resolveLiveMarkupDisplay(
5256
3821
  return requested ?? (isPageWorkspace ? "all" : "clean");
5257
3822
  }
5258
3823
 
5259
- function createCollapsedPublicSelection(
5260
- position: number,
5261
- storyTarget?: EditorStoryTarget,
5262
- ): PublicSelectionSnapshot {
5263
- return {
5264
- anchor: position,
5265
- head: position,
5266
- isCollapsed: true,
5267
- activeRange: {
5268
- kind: "range",
5269
- from: position,
5270
- to: position,
5271
- assoc: {
5272
- start: -1,
5273
- end: 1,
5274
- },
5275
- },
5276
- ...(storyTarget ? { storyTarget } : {}),
5277
- };
5278
- }
5279
3824
 
5280
3825
  function clonePublicValue<T>(value: T): T {
5281
3826
  return structuredClone(value);
5282
3827
  }
5283
3828
 
5284
- function findTextMatchesForRuntime(
5285
- runtime: WordReviewEditorRuntime,
5286
- query: string,
5287
- options: SearchOptions | undefined,
5288
- ): EditorAnchorProjection[] {
5289
- const snapshot = runtime.getRenderSnapshot();
5290
- return findTextMatches(
5291
- runtime.getSessionState().canonicalDocument,
5292
- snapshot.selection,
5293
- query,
5294
- options ?? {},
5295
- );
5296
- }
5297
-
5298
3829
  /**
5299
3830
  * Open the correct header/footer story for a specific page. The page's
5300
3831
  * resolved `stories.header` / `stories.footer` already carries the
@@ -5314,27 +3845,6 @@ function openStoryForPage(
5314
3845
  runtime.openStory(target);
5315
3846
  }
5316
3847
 
5317
- function searchRuntimeDocument(
5318
- runtime: WordReviewEditorRuntime,
5319
- mountedSurface: TwProseMirrorSurfaceRef | null,
5320
- query: string,
5321
- options: SearchOptions = {},
5322
- ): SearchResultSnapshot[] {
5323
- if (mountedSurface) {
5324
- return mountedSurface.search(query, options);
5325
- }
5326
-
5327
- const snapshot = runtime.getRenderSnapshot();
5328
- return searchDocument(
5329
- runtime.getSessionState().canonicalDocument,
5330
- snapshot.selection,
5331
- snapshot.activeStory,
5332
- runtime.getDocumentNavigationSnapshot(),
5333
- query,
5334
- options,
5335
- );
5336
- }
5337
-
5338
3848
  function applyRegionAttributes(shell: HTMLElement): void {
5339
3849
  const toolbar = shell.querySelector<HTMLElement>("header");
5340
3850
  if (toolbar) {
@@ -5528,58 +4038,6 @@ function resolveWordReviewEditorChromeVisibility(
5528
4038
  });
5529
4039
  }
5530
4040
 
5531
- function toRuntimeSelectionSnapshot(selection: PublicSelectionSnapshot) {
5532
- return {
5533
- anchor: selection.anchor,
5534
- head: selection.head,
5535
- isCollapsed: selection.isCollapsed,
5536
- activeRange:
5537
- selection.activeRange.kind === "range"
5538
- ? createRangeAnchor(
5539
- selection.activeRange.from,
5540
- selection.activeRange.to,
5541
- selection.activeRange.assoc,
5542
- )
5543
- : selection.activeRange.kind === "node"
5544
- ? createNodeAnchor(selection.activeRange.at, selection.activeRange.assoc)
5545
- : createDetachedAnchor(
5546
- selection.activeRange.lastKnownRange,
5547
- selection.activeRange.reason,
5548
- ),
5549
- };
5550
- }
5551
-
5552
- function createSelectionFromAnchor(
5553
- anchor: PublicSelectionSnapshot["activeRange"],
5554
- storyTarget?: EditorStoryTarget,
5555
- ): PublicSelectionSnapshot {
5556
- switch (anchor.kind) {
5557
- case "range":
5558
- return {
5559
- anchor: anchor.from,
5560
- head: anchor.to,
5561
- isCollapsed: anchor.from === anchor.to,
5562
- activeRange: anchor,
5563
- ...(storyTarget ? { storyTarget } : {}),
5564
- };
5565
- case "node":
5566
- return {
5567
- anchor: anchor.at,
5568
- head: anchor.at,
5569
- isCollapsed: true,
5570
- activeRange: anchor,
5571
- ...(storyTarget ? { storyTarget } : {}),
5572
- };
5573
- case "detached":
5574
- return {
5575
- anchor: anchor.lastKnownRange.from,
5576
- head: anchor.lastKnownRange.to,
5577
- isCollapsed: anchor.lastKnownRange.from === anchor.lastKnownRange.to,
5578
- activeRange: anchor,
5579
- ...(storyTarget ? { storyTarget } : {}),
5580
- };
5581
- }
5582
- }
5583
4041
 
5584
4042
  function estimateStoryLength(
5585
4043
  sessionStateOrSnapshot?: EditorSessionState | PersistedEditorSnapshot,
@@ -6381,19 +4839,6 @@ function findImageNodeInValue(
6381
4839
  return null;
6382
4840
  }
6383
4841
 
6384
- function createImageDataUrl(contentType: string, bytes: Uint8Array): string {
6385
- const base64 = bytesToBase64(bytes);
6386
- return `data:${contentType};base64,${base64}`;
6387
- }
6388
-
6389
- function bytesToBase64(bytes: Uint8Array): string {
6390
- let binary = "";
6391
- for (let index = 0; index < bytes.length; index += 1) {
6392
- binary += String.fromCharCode(bytes[index] ?? 0);
6393
- }
6394
- return btoa(binary);
6395
- }
6396
-
6397
4842
  function deriveReviewQueueSnapshot(input: {
6398
4843
  sections: Array<{
6399
4844
  sectionIndex: number;