@beyondwork/docx-react-component 1.0.67 → 1.0.70

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (384) hide show
  1. package/README.md +75 -932
  2. package/package.json +26 -27
  3. package/src/api/anchor-conversion.ts +43 -0
  4. package/src/api/editor-state-types.ts +2 -1
  5. package/src/api/public-types.ts +504 -101
  6. package/src/api/session-state.ts +4 -0
  7. package/src/api/v3/README.md +91 -0
  8. package/src/api/v3/_create.ts +146 -0
  9. package/src/api/v3/_layer-metadata.ts +362 -0
  10. package/src/api/v3/_mocks.ts +84 -0
  11. package/src/api/v3/_runtime-handle.ts +162 -0
  12. package/src/api/v3/_ux-response.ts +73 -0
  13. package/src/api/v3/ai/_metadata-audit.ts +225 -0
  14. package/src/api/v3/ai/attach.ts +235 -0
  15. package/src/api/v3/ai/bundle.ts +132 -0
  16. package/src/api/v3/ai/explain.ts +144 -0
  17. package/src/api/v3/ai/export.ts +54 -0
  18. package/src/api/v3/ai/inspect.ts +118 -0
  19. package/src/api/v3/ai/policy.ts +77 -0
  20. package/src/api/v3/ai/replacement.ts +341 -0
  21. package/src/api/v3/ai/resolve.ts +133 -0
  22. package/src/api/v3/index.ts +79 -0
  23. package/src/api/v3/runtime/chart.ts +310 -0
  24. package/src/api/v3/runtime/clipboard.ts +81 -0
  25. package/src/api/v3/runtime/collab.ts +331 -0
  26. package/src/api/v3/runtime/content.ts +236 -0
  27. package/src/api/v3/runtime/document.ts +282 -0
  28. package/src/api/v3/runtime/formatting.ts +186 -0
  29. package/src/api/v3/runtime/geometry.ts +349 -0
  30. package/src/api/v3/runtime/layout.ts +108 -0
  31. package/src/api/v3/runtime/review.ts +129 -0
  32. package/src/api/v3/runtime/search.ts +74 -0
  33. package/src/api/v3/runtime/table.ts +63 -0
  34. package/src/api/v3/runtime/workflow.ts +434 -0
  35. package/src/api/v3/ui/_context.ts +86 -0
  36. package/src/api/v3/ui/_create.ts +65 -0
  37. package/src/api/v3/ui/_types.ts +520 -0
  38. package/src/api/v3/ui/chrome-composition.ts +342 -0
  39. package/src/{ui-tailwind/chrome → api/v3/ui}/chrome-preset-model.ts +11 -1
  40. package/src/api/v3/ui/chrome.ts +476 -0
  41. package/src/api/v3/ui/debug.ts +124 -0
  42. package/src/api/v3/ui/index.ts +64 -0
  43. package/src/api/v3/ui/overlays-visibility.ts +170 -0
  44. package/src/api/v3/ui/overlays.ts +427 -0
  45. package/src/api/v3/ui/scope.ts +71 -0
  46. package/src/api/v3/ui/session.ts +100 -0
  47. package/src/api/v3/ui/surface.ts +170 -0
  48. package/src/api/v3/ui/viewport.ts +303 -0
  49. package/src/core/commands/index.ts +28 -6
  50. package/src/core/commands/list-commands.ts +3 -2
  51. package/src/core/commands/section-layout-commands.ts +9 -8
  52. package/src/core/schema/text-schema.ts +16 -0
  53. package/src/core/selection/mapping.ts +33 -72
  54. package/src/core/state/editor-state.ts +96 -189
  55. package/src/index.ts +23 -4
  56. package/src/io/chart-preview-resolver.ts +1 -1
  57. package/src/io/docx-session.ts +36 -4797
  58. package/src/io/export/build-app-properties-xml.ts +1 -1
  59. package/src/io/export/serialize-comments.ts +1 -1
  60. package/src/io/export/serialize-headers-footers.ts +6 -1
  61. package/src/io/export/serialize-main-document.ts +45 -0
  62. package/src/io/export/serialize-run-formatting.ts +17 -2
  63. package/src/io/export/twip.ts +1 -1
  64. package/src/io/normalize/normalize-text.ts +27 -20
  65. package/src/io/ooxml/chart/parse-series.ts +1 -1
  66. package/src/io/ooxml/chart/resolve-color.ts +2 -2
  67. package/src/io/ooxml/chart/types.ts +1 -1
  68. package/src/io/ooxml/classify-embedding.ts +83 -33
  69. package/src/io/ooxml/parse-fill.ts +1 -1
  70. package/src/io/ooxml/parse-main-document.ts +71 -1
  71. package/src/io/ooxml/parse-object.ts +14 -10
  72. package/src/io/ooxml/parse-run-formatting.ts +47 -1
  73. package/src/io/ooxml/property-grab-bag.ts +2 -2
  74. package/src/io/ooxml/units.ts +11 -0
  75. package/src/io/ooxml/workflow-payload.ts +282 -7
  76. package/src/model/anchor.ts +85 -0
  77. package/src/model/canonical-document.ts +351 -15
  78. package/src/model/chart-types.ts +1 -1
  79. package/src/model/layout/index.ts +83 -0
  80. package/src/model/layout/page-graph-types.ts +181 -0
  81. package/src/model/layout/page-layout-snapshot.ts +105 -0
  82. package/src/model/layout/resolved-layout-types.ts +47 -0
  83. package/src/model/layout/runtime-page-graph-types.ts +102 -0
  84. package/src/model/paragraph-scope-ids.ts +72 -0
  85. package/src/model/review/comment-types.ts +112 -0
  86. package/src/model/review/index.ts +2 -0
  87. package/src/model/review/revision-types.ts +215 -0
  88. package/src/model/snapshot.ts +32 -0
  89. package/src/review/store/comment-store.ts +21 -47
  90. package/src/review/store/revision-types.ts +40 -198
  91. package/src/runtime/collab/base-doc-fingerprint.ts +6 -1
  92. package/src/runtime/collab/runtime-collab-sync.ts +13 -3
  93. package/src/runtime/collab-session.ts +1 -1
  94. package/src/runtime/debug/build-debug-inspector-snapshot.ts +686 -0
  95. package/src/runtime/debug/event-ring-buffer.ts +64 -0
  96. package/src/runtime/debug/probability-sampler.ts +18 -0
  97. package/src/runtime/debug/runtime-debug-facet.ts +67 -0
  98. package/src/runtime/debug/stage-tokens.ts +31 -0
  99. package/src/runtime/debug/telemetry-bus.ts +271 -0
  100. package/src/runtime/debug/types.ts +275 -0
  101. package/src/runtime/debug/wrap-ref-for-telemetry.ts +118 -0
  102. package/src/runtime/document-layout.ts +8 -6
  103. package/src/runtime/document-runtime.ts +843 -1141
  104. package/src/runtime/document-search.ts +1 -1
  105. package/src/runtime/edit-ops/index.ts +1 -1
  106. package/src/runtime/external-send-runtime.ts +1 -1
  107. package/src/runtime/formatting/document-lookup.ts +235 -0
  108. package/src/runtime/formatting/field/registry.ts +41 -0
  109. package/src/runtime/{field-resolver.ts → formatting/field/resolver.ts} +27 -2
  110. package/src/runtime/formatting/font-resolution.ts +83 -0
  111. package/src/runtime/formatting/formatting-context.ts +903 -0
  112. package/src/runtime/formatting/formatting-types.ts +157 -0
  113. package/src/runtime/{hyperlink-color-resolver.ts → formatting/hyperlink-color.ts} +2 -2
  114. package/src/runtime/formatting/index.ts +125 -0
  115. package/src/runtime/{resolved-numbering-geometry.ts → formatting/numbering/geometry.ts} +1 -1
  116. package/src/runtime/{numbering-prefix.ts → formatting/numbering/prefix.ts} +170 -3
  117. package/src/runtime/formatting/paragraph-style-resolver.ts +92 -0
  118. package/src/runtime/formatting/projector.ts +75 -0
  119. package/src/runtime/formatting/resolve-effective.ts +407 -0
  120. package/src/runtime/formatting/revision-display.ts +105 -0
  121. package/src/runtime/{paragraph-style-resolver.ts → formatting/style-cascade.ts} +84 -141
  122. package/src/runtime/{table-style-resolver.ts → formatting/table-style-resolver.ts} +1 -1
  123. package/src/runtime/formatting/telemetry-bridge.ts +106 -0
  124. package/src/runtime/{theme-color-resolver.ts → formatting/theme-color.ts} +2 -30
  125. package/src/runtime/geometry/caret-geometry.ts +164 -0
  126. package/src/runtime/geometry/geometry-facet.ts +364 -0
  127. package/src/runtime/geometry/geometry-types.ts +256 -0
  128. package/src/runtime/geometry/hit-test.ts +125 -0
  129. package/src/runtime/geometry/index.ts +71 -0
  130. package/src/runtime/geometry/inert-geometry-facet.ts +43 -0
  131. package/src/runtime/geometry/invalidation.ts +35 -0
  132. package/src/runtime/geometry/object-handles.ts +77 -0
  133. package/src/runtime/geometry/overlay-rects.ts +85 -0
  134. package/src/runtime/geometry/project-anchors.ts +100 -0
  135. package/src/runtime/geometry/project-fragments.ts +216 -0
  136. package/src/runtime/geometry/projector.ts +129 -0
  137. package/src/runtime/geometry/replacement-envelope.ts +130 -0
  138. package/src/runtime/geometry/viewport.ts +218 -0
  139. package/src/runtime/layout/compat-input-ledger.ts +211 -0
  140. package/src/runtime/layout/index.ts +6 -1
  141. package/src/runtime/layout/inert-layout-facet.ts +12 -7
  142. package/src/runtime/layout/layout-engine-instance.ts +189 -11
  143. package/src/runtime/layout/layout-engine-version.ts +450 -1
  144. package/src/runtime/layout/layout-facet-types.ts +60 -0
  145. package/src/runtime/layout/layout-measurement-provider.ts +13 -0
  146. package/src/runtime/layout/measurement-backend-canvas.ts +14 -2
  147. package/src/runtime/layout/measurement-backend-empirical.ts +23 -4
  148. package/src/runtime/layout/page-graph.ts +62 -209
  149. package/src/runtime/layout/page-story-resolver.ts +7 -12
  150. package/src/runtime/layout/paginated-layout-engine.ts +186 -11
  151. package/src/runtime/layout/project-block-fragments.ts +11 -0
  152. package/src/runtime/layout/projector.ts +90 -0
  153. package/src/runtime/layout/public-facet.ts +187 -442
  154. package/src/runtime/layout/resolved-formatting-state.ts +158 -26
  155. package/src/runtime/layout/table-render-plan.ts +1 -1
  156. package/src/runtime/prerender/cache-envelope.ts +6 -1
  157. package/src/runtime/prerender/prerender-document.ts +18 -23
  158. package/src/runtime/render/decoration-resolver.ts +1 -1
  159. package/src/runtime/render/render-frame-types.ts +20 -0
  160. package/src/runtime/render/render-kernel.ts +94 -25
  161. package/src/runtime/scopes/_formatting-seam.ts +262 -0
  162. package/src/runtime/scopes/_scope-dependencies.ts +49 -0
  163. package/src/runtime/scopes/action-validation.ts +356 -0
  164. package/src/runtime/scopes/attach-explanation.ts +102 -0
  165. package/src/runtime/scopes/audit-bundle.ts +71 -0
  166. package/src/runtime/scopes/compile-scope-bundle.ts +163 -0
  167. package/src/runtime/scopes/compile-scope.ts +262 -0
  168. package/src/runtime/scopes/compiler-service.ts +431 -0
  169. package/src/runtime/scopes/create-issue.ts +107 -0
  170. package/src/runtime/scopes/enumerate-scopes.ts +543 -0
  171. package/src/runtime/scopes/evidence.ts +233 -0
  172. package/src/runtime/scopes/index.ts +150 -0
  173. package/src/runtime/scopes/position-map.ts +214 -0
  174. package/src/runtime/scopes/preservation-boundary.ts +91 -0
  175. package/src/runtime/scopes/projector.ts +49 -0
  176. package/src/runtime/scopes/replaceability.ts +87 -0
  177. package/src/runtime/scopes/replacement/apply.ts +228 -0
  178. package/src/runtime/scopes/replacement/compile.ts +59 -0
  179. package/src/runtime/scopes/replacement/propose.ts +42 -0
  180. package/src/runtime/scopes/resolve-reference.ts +347 -0
  181. package/src/runtime/scopes/review-bundle.ts +141 -0
  182. package/src/runtime/scopes/scope-kinds/_paragraph-text.ts +57 -0
  183. package/src/runtime/scopes/scope-kinds/_table-text.ts +42 -0
  184. package/src/runtime/scopes/scope-kinds/comment-thread.ts +59 -0
  185. package/src/runtime/scopes/scope-kinds/field.ts +65 -0
  186. package/src/runtime/scopes/scope-kinds/heading.ts +84 -0
  187. package/src/runtime/scopes/scope-kinds/list-item.ts +77 -0
  188. package/src/runtime/scopes/scope-kinds/paragraph.ts +182 -0
  189. package/src/runtime/scopes/scope-kinds/revision.ts +62 -0
  190. package/src/runtime/scopes/scope-kinds/table-cell.ts +57 -0
  191. package/src/runtime/scopes/scope-kinds/table-row.ts +61 -0
  192. package/src/runtime/scopes/scope-kinds/table.ts +55 -0
  193. package/src/runtime/scopes/scope-range.ts +208 -0
  194. package/src/runtime/scopes/semantic-scope-types.ts +454 -0
  195. package/src/runtime/scopes/workflow-overlap.ts +92 -0
  196. package/src/runtime/selection/index.ts +1 -1
  197. package/src/runtime/structure-ops/fragment-insert.ts +1 -1
  198. package/src/runtime/structure-ops/index.ts +1 -1
  199. package/src/runtime/surface-projection.ts +232 -262
  200. package/src/runtime/units.ts +4 -2
  201. package/src/runtime/workflow/coordinator.ts +1348 -0
  202. package/src/runtime/workflow/derived-scope-resolver.ts +125 -0
  203. package/src/runtime/workflow/index.ts +25 -0
  204. package/src/runtime/workflow/markup-mode-policy.ts +98 -0
  205. package/src/runtime/{workflow-markup.ts → workflow/markup.ts} +6 -6
  206. package/src/runtime/workflow/metadata-persistence.ts +306 -0
  207. package/src/runtime/workflow/metadata-writer.ts +123 -0
  208. package/src/runtime/workflow/overlay-store.ts +690 -0
  209. package/src/runtime/workflow/projector.ts +127 -0
  210. package/src/runtime/{query-scopes.ts → workflow/query-scopes.ts} +3 -3
  211. package/src/runtime/{workflow-rail-segments.ts → workflow/rail/compose.ts} +60 -165
  212. package/src/runtime/workflow/rail/types.ts +198 -0
  213. package/src/runtime/workflow/scope-rail-composer.ts +39 -0
  214. package/src/runtime/{scope-resolver.ts → workflow/scope-resolver.ts} +3 -3
  215. package/src/runtime/workflow/scope-writer.ts +188 -0
  216. package/src/runtime/{tamper-gate.ts → workflow/tamper-gate.ts} +1 -1
  217. package/src/runtime/workflow/visibility-policy.ts +129 -0
  218. package/src/session/_sync-legacy.ts +66 -0
  219. package/src/session/export/embedded-reconstitute.ts +104 -0
  220. package/src/session/export/export-diagnostics.ts +85 -0
  221. package/src/session/export/export-validation.ts +110 -0
  222. package/src/session/export/index.ts +34 -0
  223. package/src/session/export/preservation-reattach.ts +30 -0
  224. package/src/session/export/serialize-dispatch.ts +165 -0
  225. package/src/session/export/stateful-export-pipeline.ts +432 -0
  226. package/src/session/export/stateful-export.ts +684 -0
  227. package/src/session/import/canonical-assembly.ts +227 -0
  228. package/src/session/import/diagnostics-session.ts +54 -0
  229. package/src/session/import/embedded-discovery.ts +225 -0
  230. package/src/session/import/embedded-offload.ts +337 -0
  231. package/src/session/import/import-diagnostics.ts +69 -0
  232. package/src/session/import/loader-types.ts +313 -0
  233. package/src/session/import/loader.ts +1834 -0
  234. package/src/session/import/normalize.ts +195 -0
  235. package/src/session/import/package-parts.ts +217 -0
  236. package/src/session/import/package-read.ts +195 -0
  237. package/src/session/import/parse-orchestration.ts +105 -0
  238. package/src/session/import/part-constants.ts +70 -0
  239. package/src/session/import/part-discovery.ts +94 -0
  240. package/src/session/import/preservation-index.ts +46 -0
  241. package/src/{runtime/read-only-diagnostics-runtime.ts → session/import/read-only-diagnostics.ts} +24 -3
  242. package/src/session/import/review-import.ts +508 -0
  243. package/src/session/import/styles-consolidation.ts +281 -0
  244. package/src/session/import/workflow-scope-import.ts +256 -0
  245. package/src/session/index.ts +37 -0
  246. package/src/session/session-state.ts +69 -0
  247. package/src/session/session.ts +532 -0
  248. package/src/session/shared/protection.ts +228 -0
  249. package/src/session/shared/session-utils.ts +82 -0
  250. package/src/session/types.ts +499 -0
  251. package/src/shell/chart-snapshots.ts +96 -0
  252. package/src/shell/media-previews.ts +85 -0
  253. package/src/shell/overlay-anchor-bridge.ts +53 -0
  254. package/src/shell/paste-adapter.ts +23 -0
  255. package/src/shell/ref-commands.ts +1697 -0
  256. package/src/shell/ref-utilities.ts +48 -0
  257. package/src/shell/search.ts +51 -0
  258. package/src/{ui/editor-runtime-boundary.ts → shell/session-bootstrap.ts} +243 -67
  259. package/src/shell/ui-subscriber-channels.ts +81 -0
  260. package/src/shell/use-collab-sync.ts +116 -0
  261. package/src/ui/WordReviewEditor.tsx +496 -2051
  262. package/src/ui/editor-shell-view.tsx +30 -1
  263. package/src/ui/editor-surface-controller.tsx +49 -1
  264. package/src/ui/headless/revision-decoration-model.ts +83 -0
  265. package/src/{ui-tailwind/chrome → ui/headless}/role-action-sets.ts +1 -1
  266. package/src/ui/headless/scoped-chrome-policy.ts +2 -2
  267. package/src/ui/headless/selection-tool-context.ts +1 -1
  268. package/src/ui/headless/selection-tool-resolver.ts +1 -1
  269. package/src/ui/runtime-shortcut-dispatch.ts +46 -1
  270. package/src/ui/ui-controller-factory.ts +221 -0
  271. package/src/ui-tailwind/chart/ChartSurface.tsx +2 -2
  272. package/src/ui-tailwind/chart/layout/legend-layout.ts +1 -1
  273. package/src/ui-tailwind/chart/layout/plot-area.ts +2 -2
  274. package/src/ui-tailwind/chart/layout/title-layout.ts +1 -1
  275. package/src/ui-tailwind/chart/render/area.tsx +3 -3
  276. package/src/ui-tailwind/chart/render/bar-column.tsx +3 -3
  277. package/src/ui-tailwind/chart/render/bubble.tsx +3 -3
  278. package/src/ui-tailwind/chart/render/combo.tsx +2 -2
  279. package/src/ui-tailwind/chart/render/data-labels.tsx +2 -2
  280. package/src/ui-tailwind/chart/render/font-metrics.ts +2 -2
  281. package/src/ui-tailwind/chart/render/line.tsx +3 -3
  282. package/src/ui-tailwind/chart/render/pie.tsx +6 -6
  283. package/src/ui-tailwind/chart/render/scatter.tsx +3 -3
  284. package/src/ui-tailwind/chart/render/svg-primitives.ts +3 -3
  285. package/src/ui-tailwind/chart/render/unsupported.tsx +2 -2
  286. package/src/ui-tailwind/chrome/build-context-menu-entries.ts +88 -0
  287. package/src/ui-tailwind/chrome/chrome-preset-toolbar.tsx +1 -1
  288. package/src/ui-tailwind/chrome/collab-send-to-supplier-button.tsx +1 -1
  289. package/src/ui-tailwind/chrome/collab-tamper-banner.tsx +1 -1
  290. package/src/ui-tailwind/chrome/collab-top-nav-container.tsx +1 -1
  291. package/src/ui-tailwind/chrome/editor-action-registry.ts +553 -0
  292. package/src/ui-tailwind/chrome/editor-actions-to-palette.ts +182 -0
  293. package/src/ui-tailwind/chrome/local-surface-arbiter.ts +534 -0
  294. package/src/ui-tailwind/chrome/resolve-target-kind.ts +226 -0
  295. package/src/ui-tailwind/chrome/tw-alert-banner.tsx +38 -4
  296. package/src/ui-tailwind/chrome/tw-context-band.tsx +125 -0
  297. package/src/ui-tailwind/chrome/tw-context-menu-portal.tsx +248 -0
  298. package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +42 -1
  299. package/src/ui-tailwind/chrome/tw-selection-anchor-resolver.ts +8 -7
  300. package/src/ui-tailwind/chrome/tw-selection-tool-blocked.tsx +38 -4
  301. package/src/ui-tailwind/chrome/tw-selection-tool-comment.tsx +104 -6
  302. package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +66 -7
  303. package/src/ui-tailwind/chrome/tw-selection-tool-workflow.tsx +54 -8
  304. package/src/ui-tailwind/chrome/tw-shortcut-hint.tsx +7 -1
  305. package/src/ui-tailwind/chrome/tw-suggestion-card.tsx +33 -0
  306. package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +78 -1
  307. package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +16 -8
  308. package/src/ui-tailwind/chrome/tw-workspace-chrome-host.tsx +276 -0
  309. package/src/ui-tailwind/chrome/use-context-menu-controller.ts +201 -0
  310. package/src/ui-tailwind/chrome-overlay/chrome-overlay-projector.ts +1 -1
  311. package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +22 -4
  312. package/src/ui-tailwind/chrome-overlay/tw-comment-balloon-layer.tsx +1 -1
  313. package/src/ui-tailwind/chrome-overlay/tw-locked-block-layer.tsx +1 -1
  314. package/src/ui-tailwind/chrome-overlay/tw-object-selection-overlay.tsx +11 -5
  315. package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +197 -3
  316. package/src/ui-tailwind/chrome-overlay/tw-revision-margin-bar-layer.tsx +1 -1
  317. package/src/ui-tailwind/chrome-overlay/tw-scope-card-layer.tsx +35 -6
  318. package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +24 -16
  319. package/src/ui-tailwind/chrome-overlay/tw-table-continuation-header.tsx +1 -1
  320. package/src/ui-tailwind/debug/README.md +57 -0
  321. package/src/ui-tailwind/debug/index.ts +3 -0
  322. package/src/ui-tailwind/debug/tw-debug-overlay.tsx +186 -0
  323. package/src/ui-tailwind/debug/tw-debug-presentation.tsx +80 -0
  324. package/src/ui-tailwind/debug/tw-debug-top-bar.tsx +83 -0
  325. package/src/ui-tailwind/editor-surface/chart-node-view.tsx +2 -2
  326. package/src/ui-tailwind/editor-surface/float-wrap-resolver.ts +1 -1
  327. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +135 -10
  328. package/src/ui-tailwind/editor-surface/pm-decorations.ts +40 -13
  329. package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +1 -1
  330. package/src/ui-tailwind/editor-surface/pm-schema.ts +1 -1
  331. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +3 -3
  332. package/src/ui-tailwind/editor-surface/predicted-tag-preflight.ts +1 -1
  333. package/src/ui-tailwind/editor-surface/remote-cursor-plugin.ts +2 -2
  334. package/src/ui-tailwind/editor-surface/scroll-anchor.ts +91 -9
  335. package/src/ui-tailwind/editor-surface/shape-renderer.ts +1 -1
  336. package/src/ui-tailwind/editor-surface/surface-layer.ts +1 -1
  337. package/src/ui-tailwind/editor-surface/tw-opaque-block.tsx +1 -1
  338. package/src/ui-tailwind/editor-surface/tw-page-block-view.helpers.ts +23 -6
  339. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +132 -22
  340. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +1 -1
  341. package/src/ui-tailwind/index.ts +0 -5
  342. package/src/ui-tailwind/overlay-anchor-bridge-context.tsx +33 -0
  343. package/src/ui-tailwind/page-stack/floating-image-overlay-model.ts +66 -29
  344. package/src/ui-tailwind/page-stack/tw-floating-image-layer.tsx +25 -2
  345. package/src/ui-tailwind/review/comment-markdown-renderer.tsx +15 -0
  346. package/src/ui-tailwind/review/tw-review-rail.tsx +92 -4
  347. package/src/ui-tailwind/review/tw-workflow-tab.tsx +1 -1
  348. package/src/ui-tailwind/review-workspace/page-chrome.ts +210 -0
  349. package/src/ui-tailwind/review-workspace/page-shell-metrics.ts +101 -0
  350. package/src/ui-tailwind/review-workspace/paragraph-layout.ts +115 -0
  351. package/src/ui-tailwind/review-workspace/selection-toolbar-placement.ts +97 -0
  352. package/src/ui-tailwind/review-workspace/tw-review-workspace-navigator.tsx +130 -0
  353. package/src/ui-tailwind/review-workspace/tw-review-workspace-page-toolbar.tsx +240 -0
  354. package/src/ui-tailwind/review-workspace/tw-review-workspace-rail.tsx +59 -0
  355. package/src/ui-tailwind/review-workspace/types.ts +408 -0
  356. package/src/ui-tailwind/review-workspace/use-chrome-policy.ts +104 -0
  357. package/src/ui-tailwind/review-workspace/use-derived-view-state.ts +151 -0
  358. package/src/ui-tailwind/review-workspace/use-diagnostics-signal.ts +70 -0
  359. package/src/ui-tailwind/review-workspace/use-grabbed-segment-offsets.ts +40 -0
  360. package/src/ui-tailwind/review-workspace/use-layout-facet-render-signal.ts +55 -0
  361. package/src/ui-tailwind/review-workspace/use-page-markers.ts +130 -0
  362. package/src/ui-tailwind/review-workspace/use-pm-surface-capture.ts +60 -0
  363. package/src/ui-tailwind/review-workspace/use-review-rail-state.ts +63 -0
  364. package/src/ui-tailwind/review-workspace/use-scope-card-state.ts +170 -0
  365. package/src/ui-tailwind/review-workspace/use-scroll-root-capture.ts +28 -0
  366. package/src/ui-tailwind/review-workspace/use-selection-toolbar-placement.ts +113 -0
  367. package/src/ui-tailwind/review-workspace/use-shell-selection-anchor-bridge.ts +120 -0
  368. package/src/ui-tailwind/review-workspace/use-status-bar-page-facts.ts +55 -0
  369. package/src/ui-tailwind/review-workspace/use-viewport-dimensions.ts +43 -0
  370. package/src/ui-tailwind/review-workspace/use-workspace-arbiter.ts +25 -0
  371. package/src/ui-tailwind/review-workspace/use-workspace-composition.ts +86 -0
  372. package/src/ui-tailwind/review-workspace/use-workspace-side-effects.ts +150 -0
  373. package/src/ui-tailwind/theme/editor-theme.css +25 -0
  374. package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +2 -2
  375. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +61 -98
  376. package/src/ui-tailwind/tw-review-workspace.tsx +521 -1802
  377. package/src/ui-tailwind/ui-api-context.tsx +43 -0
  378. package/src/ui-tailwind/ui-shell-channels-context.tsx +49 -0
  379. package/src/validation/compatibility-engine.ts +6 -6
  380. package/src/runtime/styles-cascade.ts +0 -33
  381. package/src/ui-tailwind/chrome/tw-mode-dock.tsx +0 -85
  382. /package/src/runtime/{page-number-format.ts → formatting/field/page-number-format.ts} +0 -0
  383. /package/src/runtime/{ai-action-policy.ts → workflow/ai-action-policy.ts} +0 -0
  384. /package/src/runtime/{scope-tag-registry.ts → workflow/scope-tag-registry.ts} +0 -0
@@ -9,8 +9,21 @@
9
9
  * - `nextPage` (default) — new page set per section
10
10
  * - `evenPage` / `oddPage` — blank filler pages to meet parity
11
11
  * - `continuous` — the new section continues on the previous section's last page
12
- * - `nextColumn` — treated as `continuous` (column advance still TODO for
13
- * true multi-column typeset; see `paginateSectionBlocks`)
12
+ * - `nextColumn` — within a section, a block-level column break
13
+ * (`<w:br w:type="column"/>`) advances the active column; on column
14
+ * overflow the engine either advances to the next column or pushes
15
+ * a new page (which inherits the current section's column layout).
16
+ * At section granularity, a `<w:type w:val="nextColumn"/>` section
17
+ * break degrades to `continuous` — Word's behavior on single-column
18
+ * sections, preserved verbatim for multi-column sections in this
19
+ * slice so cross-section column-state carry is explicit follow-up
20
+ * work rather than implicit semantics.
21
+ *
22
+ * Column placement is tracked per block and surfaced via
23
+ * `PageStackResultWithSplits.columnByBlockIdByPageIndex`;
24
+ * `project-block-fragments.ts` consumes it to stamp
25
+ * `RuntimeBlockFragment.columnIndex`, and `buildRegions` routes fragments
26
+ * into `RuntimePageRegions.columns[i].fragmentIds`.
14
27
  *
15
28
  * Paragraph-level pagination hints:
16
29
  * - `keepNext` — already honored in `paginateSectionBlocks`
@@ -58,6 +71,7 @@ import {
58
71
  getUsablePageHeight,
59
72
  } from "../page-layout-estimation.ts";
60
73
  import {
74
+ buildRunFormattingMap,
61
75
  calculateParagraphHeight,
62
76
  resolveBlockFormatting,
63
77
  resolveCharsPerLine,
@@ -155,6 +169,14 @@ export interface PageStackResultWithSplits {
155
169
  * Parallel to `noteAllocationsByPageIndex`.
156
170
  */
157
171
  noteFragmentsByPageIndex?: ReadonlyMap<number, Array<Omit<RuntimeBlockFragment, "pageId">>>;
172
+ /**
173
+ * Refactor/04 Slice 4 — per-block column placement keyed by global page
174
+ * index. Inner map: `blockId → columnIndex`. Absent when every page on the
175
+ * graph is single-column; `project-block-fragments.ts` consumes this to
176
+ * stamp `RuntimeBlockFragment.columnIndex` and route fragments into
177
+ * `RuntimePageRegions.columns[i].fragmentIds`.
178
+ */
179
+ columnByBlockIdByPageIndex?: ReadonlyMap<number, ReadonlyMap<string, number>>;
158
180
  }
159
181
 
160
182
  // ---------------------------------------------------------------------------
@@ -207,6 +229,16 @@ export function buildPageStackWithSplits(
207
229
  number,
208
230
  Array<Omit<RuntimeBlockFragment, "pageId">>
209
231
  >();
232
+ // Refactor/04 Slice 4 — per-block column placement aggregated across all
233
+ // sections, keyed by global page index. Populated only for blocks that
234
+ // paginated under a multi-column layout; absent entries mean single-column.
235
+ const globalColumnByBlockIdByPageIndex = new Map<number, Map<string, number>>();
236
+ // Refactor/04 post-Slice-4 — end-column per global page, used by the
237
+ // next section's `nextColumn` handling to decide whether to seed
238
+ // section N at `prevEnd + 1` or start fresh on a new page.
239
+ // "End column" = the highest columnIndex any block on the page
240
+ // occupied. 0 on single-column pages.
241
+ const endColumnByGlobalPageIndex = new Map<number, number>();
210
242
  let globalPageIndex = 0;
211
243
  // A single cache lives for the whole pagination pass so cross-section
212
244
  // re-measurement (rare but possible through keepNext heuristics) still
@@ -234,7 +266,41 @@ export function buildPageStackWithSplits(
234
266
  const prevSection = sectionIdx > 0 ? sections[sectionIdx - 1] : undefined;
235
267
  const breakType = prevSection?.properties?.sectionType;
236
268
  const nextDisplayPage = globalPageIndex + 1; // 1-based
237
- const isContinuous = breakType === "continuous" || breakType === "nextColumn";
269
+
270
+ // Refactor/04 post-Slice-4 — cross-section column carry. A
271
+ // section-level `nextColumn` break degrades by case:
272
+ // - new section single-column (columns <= 1): merge like
273
+ // continuous (preserves F01 + CCEP single-column regression).
274
+ // - new + prev sections share a column count AND the prev
275
+ // section's last page still has room: merge like continuous
276
+ // AND seed the new section's starting columnIndex =
277
+ // prevEndColumn + 1.
278
+ // - column counts mismatch, or prev ended in the last column
279
+ // (no room): behave like `nextPage` — no merge, start fresh.
280
+ // `continuous` sections still behave exactly as before.
281
+ let nextColumnSeed = 0;
282
+ let nextColumnDegradesToContinuous = false;
283
+ if (breakType === "nextColumn" && prevSection) {
284
+ const newColumns = layout.columns;
285
+ if (newColumns <= 1) {
286
+ nextColumnDegradesToContinuous = true;
287
+ } else {
288
+ const lastPage = pages.length > 0 ? pages[pages.length - 1] : undefined;
289
+ const prevColumns = lastPage?.layout.columns ?? 1;
290
+ if (prevColumns === newColumns && lastPage) {
291
+ const prevEnd = endColumnByGlobalPageIndex.get(lastPage.pageIndex) ?? 0;
292
+ if (prevEnd + 1 < newColumns) {
293
+ nextColumnDegradesToContinuous = true;
294
+ nextColumnSeed = prevEnd + 1;
295
+ }
296
+ // else: prev ended in last column → fall through (no merge,
297
+ // start fresh on new page in column 0 — matches nextPage).
298
+ }
299
+ // else: mismatched layouts → fall through (nextPage-ish).
300
+ }
301
+ }
302
+ const isContinuous =
303
+ breakType === "continuous" || nextColumnDegradesToContinuous;
238
304
 
239
305
  if (breakType === "evenPage" && globalPageIndex > 0) {
240
306
  if (nextDisplayPage % 2 !== 0) {
@@ -272,6 +338,7 @@ export function buildPageStackWithSplits(
272
338
  measurementProvider,
273
339
  cache,
274
340
  defaultTabInterval,
341
+ nextColumnSeed,
275
342
  );
276
343
  const paginated = paginatedResult.pages;
277
344
 
@@ -346,6 +413,31 @@ export function buildPageStackWithSplits(
346
413
  const existing = globalNoteFragmentsByPageIndex.get(globalPageIdx) ?? [];
347
414
  globalNoteFragmentsByPageIndex.set(globalPageIdx, [...existing, ...sectionFrags]);
348
415
  }
416
+
417
+ // Refactor/04 Slice 4 — merge per-section column placements into the
418
+ // global map. Sections that shared a page via `continuous` / `nextColumn`
419
+ // merging each contribute their own block ids to the same global index.
420
+ for (const [pageInSec, sectionCols] of paginatedResult.columnByBlockIdByPageInSection) {
421
+ const globalPageIdx = pageInSectionToGlobal.get(pageInSec);
422
+ if (globalPageIdx === undefined) continue;
423
+ let forPage = globalColumnByBlockIdByPageIndex.get(globalPageIdx);
424
+ if (!forPage) {
425
+ forPage = new Map<string, number>();
426
+ globalColumnByBlockIdByPageIndex.set(globalPageIdx, forPage);
427
+ }
428
+ for (const [blockId, col] of sectionCols) {
429
+ if (!forPage.has(blockId)) forPage.set(blockId, col);
430
+ }
431
+ // Refactor/04 post-Slice-4 — track the end-column per global page
432
+ // so the NEXT section's `nextColumn` break can decide whether to
433
+ // seed into this page's remaining columns or push fresh. The
434
+ // highest columnIndex observed on each page is the "end column".
435
+ let maxCol = endColumnByGlobalPageIndex.get(globalPageIdx) ?? 0;
436
+ for (const col of sectionCols.values()) {
437
+ if (col > maxCol) maxCol = col;
438
+ }
439
+ endColumnByGlobalPageIndex.set(globalPageIdx, maxCol);
440
+ }
349
441
  }
350
442
 
351
443
  // Guarantee at least one page
@@ -375,6 +467,9 @@ export function buildPageStackWithSplits(
375
467
  noteFragmentsByPageIndex: globalNoteFragmentsByPageIndex.size > 0
376
468
  ? globalNoteFragmentsByPageIndex
377
469
  : undefined,
470
+ columnByBlockIdByPageIndex: globalColumnByBlockIdByPageIndex.size > 0
471
+ ? globalColumnByBlockIdByPageIndex
472
+ : undefined,
378
473
  };
379
474
  }
380
475
 
@@ -477,6 +572,15 @@ export function buildPageStackFromWithSplits(
477
572
  return {
478
573
  pages: tailPages,
479
574
  splits: { byBlockId: tailSplits, tablesByBlockId: tailTableSplits },
575
+ ...(full.columnByBlockIdByPageIndex && full.columnByBlockIdByPageIndex.size > 0
576
+ ? {
577
+ columnByBlockIdByPageIndex: new Map(
578
+ Array.from(full.columnByBlockIdByPageIndex.entries()).filter(
579
+ ([pi]) => pi >= dirtyPageNumberOffset,
580
+ ),
581
+ ),
582
+ }
583
+ : {}),
480
584
  };
481
585
  }
482
586
 
@@ -539,6 +643,16 @@ export function buildPageStackFromWithSplits(
539
643
  ]),
540
644
  )
541
645
  : undefined;
646
+ // Refactor/04 Slice 4 — shift per-block column map in lockstep with pages.
647
+ const shiftedColumnByBlockId =
648
+ tailResult.columnByBlockIdByPageIndex && tailResult.columnByBlockIdByPageIndex.size > 0
649
+ ? new Map<number, ReadonlyMap<string, number>>(
650
+ Array.from(tailResult.columnByBlockIdByPageIndex.entries()).map(([pi, cols]) => [
651
+ pi + dirtyPageNumberOffset,
652
+ cols,
653
+ ]),
654
+ )
655
+ : undefined;
542
656
 
543
657
  return {
544
658
  pages: shiftedPages,
@@ -549,6 +663,9 @@ export function buildPageStackFromWithSplits(
549
663
  ...(shiftedNoteFragments !== undefined
550
664
  ? { noteFragmentsByPageIndex: shiftedNoteFragments }
551
665
  : {}),
666
+ ...(shiftedColumnByBlockId !== undefined
667
+ ? { columnByBlockIdByPageIndex: shiftedColumnByBlockId }
668
+ : {}),
552
669
  };
553
670
  }
554
671
 
@@ -816,7 +933,7 @@ function measureBlockHeight(
816
933
  const measured = measurementProvider.measureLineFragments({
817
934
  block,
818
935
  formatting,
819
- runs: new Map(),
936
+ runs: buildRunFormattingMap(block),
820
937
  columnWidth,
821
938
  });
822
939
  cache?.setLineCount(block, columnWidth, measured.lineCount);
@@ -1024,12 +1141,13 @@ function measureParagraphLineCount(
1024
1141
  const measured = measurementProvider.measureLineFragments({
1025
1142
  block,
1026
1143
  formatting,
1027
- // The paginated pipeline currently resolves formatting at the
1028
- // paragraph level only; per-run formatting is not yet threaded
1029
- // through. Pass an empty map; the provider's empirical backend
1030
- // does not consult per-run metrics and the canvas backend falls
1031
- // back to the paragraph defaults when a run is missing.
1032
- runs: new Map(),
1144
+ // Per-run formatting threaded via `buildRunFormattingMap`
1145
+ // (Task 3, 2026-04-23). The canvas backend reads
1146
+ // `runs.get("${blockId}:${segmentId}")` for per-segment font
1147
+ // metrics; the empirical backend ignores `runs` (paragraph-
1148
+ // level metrics are sufficient) so the empirical numerical
1149
+ // output is unchanged.
1150
+ runs: buildRunFormattingMap(block),
1033
1151
  columnWidth,
1034
1152
  });
1035
1153
  return Math.max(1, measured.lineCount);
@@ -1158,6 +1276,12 @@ interface SectionPaginationResult {
1158
1276
  noteAllocationsByPageInSection: Map<number, RuntimeNoteAllocation[]>;
1159
1277
  /** P8.1b — per-page note body fragments keyed by pageInSection index. */
1160
1278
  noteFragmentsByPageInSection: Map<number, Array<Omit<RuntimeBlockFragment, "pageId">>>;
1279
+ /**
1280
+ * Refactor/04 Slice 4 — per-block column placement within a multi-column
1281
+ * section. Keyed by pageInSection → (blockId → columnIndex). Absent for
1282
+ * single-column sections; absent for note/footnote blocks.
1283
+ */
1284
+ columnByBlockIdByPageInSection: Map<number, Map<string, number>>;
1161
1285
  }
1162
1286
 
1163
1287
  /**
@@ -1193,6 +1317,16 @@ export function paginateSectionBlocksWithSplits(
1193
1317
  measurementProvider?: LayoutMeasurementProvider,
1194
1318
  cache?: MeasurementCache,
1195
1319
  defaultTabInterval = 720,
1320
+ /**
1321
+ * Refactor/04 post-Slice-4 — cross-section column carry seed. When
1322
+ * section N starts via `<w:type w:val="nextColumn"/>` from a
1323
+ * matching-layout previous section that ended in column K,
1324
+ * the outer pass seeds K+1 here so section N's first block lands in
1325
+ * the next available column rather than restarting at 0. Ignored
1326
+ * for single-column layouts (`maxColumns <= 1`) and clamped to
1327
+ * `[0, maxColumns-1]` defensively.
1328
+ */
1329
+ startColumnIndex = 0,
1196
1330
  ): SectionPaginationResult {
1197
1331
  if (blocks.length === 0) {
1198
1332
  return {
@@ -1208,6 +1342,7 @@ export function paginateSectionBlocksWithSplits(
1208
1342
  splits: { byBlockId: new Map() }, // section-local; global map includes tablesByBlockId via collectTableRowSlices
1209
1343
  noteAllocationsByPageInSection: new Map(),
1210
1344
  noteFragmentsByPageInSection: new Map(),
1345
+ columnByBlockIdByPageInSection: new Map(),
1211
1346
  };
1212
1347
  }
1213
1348
 
@@ -1216,12 +1351,39 @@ export function paginateSectionBlocksWithSplits(
1216
1351
  const usableHeight = getUsablePageHeight(layout);
1217
1352
  const columnMetrics = getUsableColumnMetrics(layout);
1218
1353
  const maxColumns = Math.max(1, columnMetrics.length);
1354
+ const isMultiColumn = maxColumns > 1;
1219
1355
  let pageStart = section.start;
1220
1356
  let columnHeight = 0;
1221
- let columnIndex = 0;
1357
+ // Refactor/04 post-Slice-4 — seed `columnIndex` from the outer pass
1358
+ // when a matching-layout `nextColumn` section break precedes this
1359
+ // section. Clamp so a malformed seed can't read past `maxColumns-1`.
1360
+ // Single-column layouts ignore the seed (there's no column to
1361
+ // advance into).
1362
+ let columnIndex = isMultiColumn
1363
+ ? Math.min(Math.max(0, startColumnIndex), maxColumns - 1)
1364
+ : 0;
1222
1365
  let pageInSection = 0;
1223
1366
  let reservedNoteHeight = 0;
1224
1367
  const reservedNotes = new Set<string>();
1368
+ // Refactor/04 Slice 4 — per-block column placement. Populated only when
1369
+ // `maxColumns > 1`; single-column sections skip the map allocation entirely
1370
+ // so the cold path pays nothing for documents Word treats as single-column
1371
+ // (F01 + 10 representative CCEP regression path).
1372
+ const columnByBlockIdByPageInSection = new Map<number, Map<string, number>>();
1373
+ const recordColumnPlacement = (blockId: string): void => {
1374
+ if (!isMultiColumn) return;
1375
+ let forPage = columnByBlockIdByPageInSection.get(pageInSection);
1376
+ if (!forPage) {
1377
+ forPage = new Map<string, number>();
1378
+ columnByBlockIdByPageInSection.set(pageInSection, forPage);
1379
+ }
1380
+ // Only record the FIRST placement — paragraphs placed atomically
1381
+ // emit one fragment; table blocks may re-enter the loop with
1382
+ // tableProgress bumps but the block still resolves to one column.
1383
+ if (!forPage.has(blockId)) {
1384
+ forPage.set(blockId, columnIndex);
1385
+ }
1386
+ };
1225
1387
  // P6.c: per-table progress when a table is being split row-by-row
1226
1388
  // across pages. Map<blockId, nextRowIndexToPlace>. Cleared once a
1227
1389
  // table is fully placed.
@@ -1430,6 +1592,7 @@ export function paginateSectionBlocksWithSplits(
1430
1592
 
1431
1593
  // Case 1: remainder fits — place and break.
1432
1594
  if (remainderHeight <= remainingForTable) {
1595
+ recordColumnPlacement(block.blockId);
1433
1596
  columnHeight += startRow > 0 ? remainderHeight : baseHeight;
1434
1597
  if (startRow > 0) tableProgress.delete(block.blockId);
1435
1598
  if (index === blocks.length - 1) pushPage(section.end);
@@ -1446,6 +1609,7 @@ export function paginateSectionBlocksWithSplits(
1446
1609
  startRow,
1447
1610
  });
1448
1611
  if (decision.rowsOnCurrentPage > 0) {
1612
+ recordColumnPlacement(block.blockId);
1449
1613
  tableProgress.set(block.blockId, decision.splitRowIndex);
1450
1614
  pushPage(rowOffset(decision.splitRowIndex));
1451
1615
  continue;
@@ -1462,6 +1626,7 @@ export function paginateSectionBlocksWithSplits(
1462
1626
  // Case 4: degraded atomic placement (table on a fresh page that
1463
1627
  // it doesn't fit on, AND the first row alone exceeds page
1464
1628
  // height). Preserve pre-P6.c semantics so offsets stay clean.
1629
+ recordColumnPlacement(block.blockId);
1465
1630
  columnHeight += baseHeight;
1466
1631
  if (index === blocks.length - 1) pushPage(section.end);
1467
1632
  break;
@@ -1577,6 +1742,10 @@ export function paginateSectionBlocksWithSplits(
1577
1742
  columnWidth,
1578
1743
  reservedNotes,
1579
1744
  );
1745
+ // Refactor/04 Slice 4 — record placement BEFORE the `hasColumnBreak`
1746
+ // branch below advances `columnIndex`; the block itself lives in the
1747
+ // PRE-advance column.
1748
+ recordColumnPlacement(block.blockId);
1580
1749
  columnHeight += baseHeight;
1581
1750
  reservedNoteHeight += effectiveNoteHeight;
1582
1751
  currentPageNoteIds(block).forEach((noteKey) => {
@@ -1634,6 +1803,7 @@ export function paginateSectionBlocksWithSplits(
1634
1803
  splits: { byBlockId: splitsByBlock },
1635
1804
  noteAllocationsByPageInSection,
1636
1805
  noteFragmentsByPageInSection,
1806
+ columnByBlockIdByPageInSection,
1637
1807
  };
1638
1808
  }
1639
1809
 
@@ -1682,6 +1852,11 @@ function estimateFootnoteReservation(
1682
1852
  const colonIdx = noteKey.indexOf(":");
1683
1853
  if (colonIdx === -1) continue;
1684
1854
  const noteKind = noteKey.slice(0, colonIdx) as "footnote" | "endnote";
1855
+ // Endnotes flow to the document-end endnote area, not the per-page
1856
+ // footnote area — they do not reserve page-local height. The
1857
+ // snapshotNoteAllocations loop already skips endnotes on emission;
1858
+ // this skip keeps the reservation math aligned.
1859
+ if (noteKind === "endnote") continue;
1685
1860
  const noteId = noteKey.slice(colonIdx + 1);
1686
1861
  // Use measureNoteBody so reservation math and emission share the same path.
1687
1862
  const { heightTwips } = measureNoteBody(noteKind, noteId, footnotes, columnWidth);
@@ -37,6 +37,7 @@ export function projectSurfaceBlocksToPageFragments(
37
37
  surface: EditorSurfaceSnapshot,
38
38
  pages: readonly DocumentPageSnapshot[],
39
39
  splits?: BlockSplits,
40
+ columnByBlockIdByPageIndex?: ReadonlyMap<number, ReadonlyMap<string, number>>,
40
41
  ): Map<number, FragmentWithoutPageId[]> {
41
42
  const byPage = new Map<number, FragmentWithoutPageId[]>();
42
43
  const perPageCounter = new Map<number, number>();
@@ -59,6 +60,10 @@ export function projectSurfaceBlocksToPageFragments(
59
60
  return n;
60
61
  };
61
62
 
63
+ // Refactor/04 Slice 4 — per-block column placement lookup.
64
+ const columnIndexFor = (pageIndex: number, blockId: string): number | undefined =>
65
+ columnByBlockIdByPageIndex?.get(pageIndex)?.get(blockId);
66
+
62
67
  for (const block of surface.blocks) {
63
68
  // R3: table split across pages — emit one fragment per row slice.
64
69
  // Consumers read `tableRowRange` and prepend header rows when from > 0.
@@ -69,9 +74,11 @@ export function projectSurfaceBlocksToPageFragments(
69
74
  block,
70
75
  tableSliceList,
71
76
  (pageIndex, fragment) => {
77
+ const columnIndex = columnIndexFor(pageIndex, block.blockId);
72
78
  pushFragment(pageIndex, {
73
79
  ...fragment,
74
80
  orderInRegion: nextOrder(pageIndex),
81
+ ...(columnIndex !== undefined ? { columnIndex } : {}),
75
82
  });
76
83
  },
77
84
  );
@@ -87,9 +94,11 @@ export function projectSurfaceBlocksToPageFragments(
87
94
  block,
88
95
  sliceList,
89
96
  (pageIndex, fragment) => {
97
+ const columnIndex = columnIndexFor(pageIndex, block.blockId);
90
98
  pushFragment(pageIndex, {
91
99
  ...fragment,
92
100
  orderInRegion: nextOrder(pageIndex),
101
+ ...(columnIndex !== undefined ? { columnIndex } : {}),
93
102
  });
94
103
  },
95
104
  );
@@ -101,6 +110,7 @@ export function projectSurfaceBlocksToPageFragments(
101
110
  const pageIndex = findPageIndexForOffset(pages, block.from);
102
111
  if (pageIndex === null) continue;
103
112
 
113
+ const columnIndex = columnIndexFor(pageIndex, block.blockId);
104
114
  const fragment: FragmentWithoutPageId = {
105
115
  fragmentId: `fragment-${block.blockId}`,
106
116
  blockId: block.blockId,
@@ -111,6 +121,7 @@ export function projectSurfaceBlocksToPageFragments(
111
121
  heightTwips: estimateBlockHeightFromSpan(block),
112
122
  ...deriveStyleMetadata(block),
113
123
  kind: "whole",
124
+ ...(columnIndex !== undefined ? { columnIndex } : {}),
114
125
  };
115
126
 
116
127
  pushFragment(pageIndex, fragment);
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Layer 04 — layout debug projector.
3
+ *
4
+ * Produces a `LayoutDebugEntry` for the debug-inspector snapshot when the
5
+ * `layout` telemetry channel is on. Architecture reference:
6
+ * docs/architecture/04-layout-semantics.md §"Projector and oracle surfaces".
7
+ *
8
+ * Contract with CLAUDE.md performance invariant 4:
9
+ * - Callers gate the call through `bus.isEnabled("layout")` so the
10
+ * cost is zero when the channel is off.
11
+ * - The projector never widens the wholesale-snapshot path. It reads
12
+ * four existing facet accessors (`getPageCount`, `getSections`,
13
+ * `getMeasurementFidelity`, `getDirtyFieldFamilies`) and performs no
14
+ * extra work beyond their return values.
15
+ * - Cache warming caveat: the layout facet is lazy. If the projector
16
+ * is the FIRST consumer to read `getPageCount()` on a runtime that
17
+ * has never paginated, the underlying engine will run `fullRebuild`
18
+ * as part of that call — this is the engine's documented lazy
19
+ * behavior, not something the projector causes. A SECOND projector
20
+ * call on the same facet is idempotent and adds no commit (see
21
+ * `test/runtime/layout/projector-on-demand.test.ts`). In real
22
+ * sessions the debug snapshot is invoked after the shell has already
23
+ * mounted and read the facet, so the cold-path cost does not fall
24
+ * on the debug path.
25
+ *
26
+ * The entry is intentionally small. Slice 3 ships the page-count +
27
+ * section-count + measurement-fidelity + dirty-fields summary plus the
28
+ * `LAYOUT_ENGINE_VERSION` discriminator so traces from different engine
29
+ * builds are distinguishable. Slice 4 (multi-column `nextColumn`) adds
30
+ * per-page column counts.
31
+ */
32
+
33
+ import type { LayoutFacet } from "./layout-facet-types.ts";
34
+ import { LAYOUT_ENGINE_VERSION } from "./layout-engine-version.ts";
35
+ import type { DocumentSettings } from "../../model/canonical-document.ts";
36
+ import {
37
+ resolveLayoutCompatInputs,
38
+ type LayoutCompatInputs,
39
+ } from "./compat-input-ledger.ts";
40
+
41
+ export interface LayoutDebugEntry {
42
+ readonly schemaVersion: 2;
43
+ /**
44
+ * `LAYOUT_ENGINE_VERSION` at projection time. Traces from different
45
+ * engine builds are distinguishable; persisted prerender caches key on
46
+ * this constant so its value at projection time is a reliable
47
+ * discriminator.
48
+ */
49
+ readonly engineVersion: number;
50
+ readonly pageCount: number;
51
+ readonly sectionCount: number;
52
+ readonly measurementFidelity: string;
53
+ readonly dirtyFieldFamilies: readonly string[];
54
+ /**
55
+ * Task 5 (2026-04-23) — compat-input ledger. Surfaces which
56
+ * pre-2013/legacy compat flags + `<w:compatSetting>` entries are
57
+ * active on the document so operators can tell at a glance which
58
+ * compat-conditional branches would fire. Absent when the document
59
+ * settings are unavailable (debug projection can be taken against a
60
+ * facet without an accompanying `DocumentSettings`).
61
+ */
62
+ readonly compatInputs?: LayoutCompatInputs;
63
+ }
64
+
65
+ export interface BuildLayoutDebugEntryInputs {
66
+ readonly facet: LayoutFacet;
67
+ /**
68
+ * Task 5 — when supplied, the projector resolves and attaches
69
+ * `compatInputs`. Optional so existing callers that never plumbed
70
+ * settings through continue working unchanged.
71
+ */
72
+ readonly settings?: DocumentSettings;
73
+ }
74
+
75
+ export function buildLayoutDebugEntry(
76
+ inputs: BuildLayoutDebugEntryInputs,
77
+ ): LayoutDebugEntry {
78
+ const { facet, settings } = inputs;
79
+ return {
80
+ schemaVersion: 2,
81
+ engineVersion: LAYOUT_ENGINE_VERSION,
82
+ pageCount: facet.getPageCount(),
83
+ sectionCount: facet.getSections().length,
84
+ measurementFidelity: facet.getMeasurementFidelity(),
85
+ dirtyFieldFamilies: facet.getDirtyFieldFamilies(),
86
+ ...(settings !== undefined
87
+ ? { compatInputs: resolveLayoutCompatInputs(settings) }
88
+ : {}),
89
+ };
90
+ }