@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
@@ -0,0 +1,216 @@
1
+ /**
2
+ * Layer 05 — Fragment projection helpers.
3
+ *
4
+ * Owns:
5
+ * - `collectLineBoxesForRegion` — synthesize line boxes for header/footer/
6
+ * column/footnote-area regions (body regions carry line boxes on the page
7
+ * node already). Moved from `src/runtime/layout/public-facet.ts` in
8
+ * refactor/05 Slice 2b.
9
+ * - `resolveRegionEntry` — map a `PublicPageRegion["kind"]` + optional
10
+ * column index onto the corresponding `RuntimePageRegion` on a node.
11
+ * Moved from the same origin.
12
+ * - `getBlockRectsFromFrame` — walk a `RenderFrame` and collect every
13
+ * block fragment rect for a given `blockId`. New in Slice 2b; the
14
+ * layout facet does not currently expose block-rect lists. Required by
15
+ * `v3 runtime.geometry.getBlockRects` whose adapter promotes from
16
+ * `mock` to `live-with-adapter` in Slice 5.
17
+ *
18
+ * Contracts:
19
+ * - G5 — rects here are frame-local pixels (`space: "frame"`) because
20
+ * the underlying `RenderFrameRect` is kernel-zoomed. Consumers that
21
+ * need twip coordinates re-derive from layout, not from here.
22
+ * - G6 — callers must route through the `GeometryFacet`, not reach in.
23
+ *
24
+ * Slices 4–5 add caret / envelope projection in sibling files.
25
+ */
26
+
27
+ import type {
28
+ RuntimePageGraph,
29
+ RuntimePageNode,
30
+ RuntimePageRegion,
31
+ } from "../layout/page-graph.ts";
32
+ import type { PublicPageRegion } from "../layout/public-facet.ts";
33
+ import type { RenderFrame } from "../render/index.ts";
34
+ import type {
35
+ BlockGeometry,
36
+ GeometryRect,
37
+ PageGeometry,
38
+ } from "./geometry-types.ts";
39
+
40
+ // `lineBoxes` on `RuntimePageNode` is already strongly typed; alias it
41
+ // locally so we don't re-import the raw `RuntimeLineBox` type just to
42
+ // declare the helper's return shape.
43
+ export type RuntimeLineBoxAlias = RuntimePageNode["lineBoxes"][number];
44
+
45
+ export const EMPTY_LINE_BOXES: readonly RuntimeLineBoxAlias[] = Object.freeze(
46
+ [],
47
+ );
48
+
49
+ /**
50
+ * Collect raw line boxes for `region` on `node`. Body returns the node's
51
+ * precomputed `lineBoxes`; non-body regions synthesize line boxes from the
52
+ * region's fragment list (one entry per fragment, stacked vertically from a
53
+ * zero cursor). Multi-column layouts resolve against the requested
54
+ * `columnIndex`; footnote-area uses the first allocated footnote region on
55
+ * the page.
56
+ */
57
+ export function collectLineBoxesForRegion(
58
+ node: RuntimePageNode,
59
+ region: PublicPageRegion["kind"],
60
+ graph: RuntimePageGraph,
61
+ columnIndex: number | undefined,
62
+ ): readonly RuntimeLineBoxAlias[] {
63
+ if (region === "body") {
64
+ return node.lineBoxes;
65
+ }
66
+ const regionEntry = resolveRegionEntry(node, region, columnIndex);
67
+ if (!regionEntry || regionEntry.fragmentIds.length === 0) {
68
+ return EMPTY_LINE_BOXES;
69
+ }
70
+ const fragmentsById = new Map(
71
+ graph.fragments.map((f) => [f.fragmentId, f] as const),
72
+ );
73
+ const result: RuntimeLineBoxAlias[] = [];
74
+ let cursorTwips = 0;
75
+ let lineIndex = 0;
76
+ for (const fragmentId of regionEntry.fragmentIds) {
77
+ const fragment = fragmentsById.get(fragmentId);
78
+ if (!fragment) continue;
79
+ const heightTwips = Math.max(1, fragment.heightTwips);
80
+ result.push({
81
+ fragmentId,
82
+ lineIndex: lineIndex++,
83
+ baselineTwips: cursorTwips + heightTwips,
84
+ heightTwips,
85
+ widthTwips: regionEntry.widthTwips,
86
+ });
87
+ cursorTwips += heightTwips;
88
+ }
89
+ return result;
90
+ }
91
+
92
+ export function resolveRegionEntry(
93
+ node: RuntimePageNode,
94
+ region: PublicPageRegion["kind"],
95
+ columnIndex: number | undefined,
96
+ ): RuntimePageRegion | undefined {
97
+ switch (region) {
98
+ case "header":
99
+ return node.regions.header;
100
+ case "footer":
101
+ return node.regions.footer;
102
+ case "column": {
103
+ const columns = node.regions.columns ?? [];
104
+ if (columns.length === 0) return undefined;
105
+ const idx = columnIndex ?? 0;
106
+ return columns[idx];
107
+ }
108
+ case "footnote-area": {
109
+ // Footnote area sits at the bottom of the body region per OOXML.
110
+ // Returns the first footnote region when allocated (page layouts
111
+ // allocate one block of footnotes per page today; multi-block
112
+ // layouts come later).
113
+ const footnoteRegionList = node.regions.footnotes;
114
+ if (footnoteRegionList && footnoteRegionList.length > 0) {
115
+ return footnoteRegionList[0];
116
+ }
117
+ return undefined;
118
+ }
119
+ default:
120
+ return undefined;
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Collect every fragment rect for `blockId` from a `RenderFrame`. A block
126
+ * that spans multiple pages has one rect per page. Returns rects in
127
+ * page order, tagged `space: "frame"`. Empty when the frame is null, has
128
+ * no pages, or no fragment on any page carries the target blockId.
129
+ *
130
+ * New in Slice 2b. `v3 runtime.geometry.getBlockRects` promotes from
131
+ * `mock` to `live-with-adapter` against this implementation in Slice 5.
132
+ */
133
+ export function getBlockRectsFromFrame(
134
+ frame: RenderFrame | null,
135
+ blockId: string,
136
+ ): readonly GeometryRect[] {
137
+ if (!frame) return [];
138
+ const rects: GeometryRect[] = [];
139
+ for (const page of frame.pages) {
140
+ const regions = [
141
+ page.regions.body,
142
+ page.regions.header,
143
+ page.regions.footer,
144
+ ...(page.regions.columns ?? []),
145
+ ...(page.regions.footnotes ?? []),
146
+ ];
147
+ for (const region of regions) {
148
+ if (!region) continue;
149
+ for (const block of region.blocks) {
150
+ if (block.fragment.blockId !== blockId) continue;
151
+ rects.push({
152
+ leftPx: block.frame.leftPx,
153
+ topPx: block.frame.topPx,
154
+ widthPx: block.frame.widthPx,
155
+ heightPx: block.frame.heightPx,
156
+ space: "frame",
157
+ });
158
+ }
159
+ }
160
+ }
161
+ return rects;
162
+ }
163
+
164
+ /**
165
+ * Project a page from a `RenderFrame` into a `PageGeometry`. Accepts
166
+ * either a numeric `pageIndex` or a string `pageId` (the same overload
167
+ * the public facet's `getPage` exposes). Returns `null` when the frame
168
+ * is null or the target page isn't in the current frame.
169
+ *
170
+ * New in Slice 3a — chrome overlays read page 0's `frame` via the
171
+ * geometry facet instead of calling `getBoundingClientRect` on the
172
+ * overlay root, closing the G2 violation.
173
+ */
174
+ export function getPageFromFrame(
175
+ frame: RenderFrame | null,
176
+ target: number | string,
177
+ ): PageGeometry | null {
178
+ if (!frame) return null;
179
+ const page =
180
+ typeof target === "number"
181
+ ? frame.pages[target]
182
+ : frame.pages.find((p) => p.page.pageId === target);
183
+ if (!page) return null;
184
+ return {
185
+ pageId: page.page.pageId,
186
+ pageIndex: page.page.pageIndex,
187
+ frame: {
188
+ leftPx: page.frame.leftPx,
189
+ topPx: page.frame.topPx,
190
+ widthPx: page.frame.widthPx,
191
+ heightPx: page.frame.heightPx,
192
+ space: "frame",
193
+ },
194
+ };
195
+ }
196
+
197
+ /**
198
+ * Project a block from a `RenderFrame` into a `BlockGeometry` — wraps
199
+ * `getBlockRectsFromFrame` with the block id so consumers that want a
200
+ * named handle don't have to thread the id alongside the rect list.
201
+ * Returns `null` when the frame is null or no fragment on any page
202
+ * carries the target blockId.
203
+ *
204
+ * New in Slice 3a — scroll-anchor reads block rects via this helper
205
+ * (through the geometry facet) instead of walking the `offsetTop` /
206
+ * `offsetParent` chain, closing the G2 violation in
207
+ * `src/ui-tailwind/editor-surface/scroll-anchor.ts`.
208
+ */
209
+ export function getBlockGeometryFromFrame(
210
+ frame: RenderFrame | null,
211
+ blockId: string,
212
+ ): BlockGeometry | null {
213
+ const rects = getBlockRectsFromFrame(frame, blockId);
214
+ if (rects.length === 0) return null;
215
+ return { blockId, rects };
216
+ }
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Layer 05 — geometry debug projector.
3
+ *
4
+ * Produces a `GeometryDebugEntry` for the debug-inspector snapshot when
5
+ * the `layout` telemetry channel is on. Architecture reference:
6
+ * `docs/architecture/05-geometry-projection.md` §"Public facet".
7
+ *
8
+ * Mirrors `src/runtime/formatting/projector.ts` in spirit — strict about
9
+ * cost, gated by the bus channel toggle. Consumers must call
10
+ * `buildGeometryDebugEntry` only when `bus.isEnabled("layout")`.
11
+ *
12
+ * Slice-6 scope: minimum-viable summary — page count, frame revision,
13
+ * sampled block rects, caret resolution shape. Slice 6 does NOT wire
14
+ * the `geometry.projected` emit site on the layout-engine-instance
15
+ * recompute boundary — that is an edit-path perf concern (Performance
16
+ * Invariant 4) and lands alongside a perf-bench pair in a follow-up.
17
+ */
18
+
19
+ import type { GeometryFacet } from "./geometry-facet.ts";
20
+
21
+ export interface GeometryDebugCatalog {
22
+ /** Layout revision the snapshot was taken against (0 when unknown). */
23
+ readonly layoutRevision: number;
24
+ /** Number of pages in the kernel's current frame. */
25
+ readonly pageCount: number;
26
+ /** Total block rects across all pages sampled (bounded by `sampleLimit`). */
27
+ readonly sampledBlockRectCount: number;
28
+ /** Per-block pages the first sampled block spans (0 when no sample). */
29
+ readonly firstBlockPageSpan: number;
30
+ }
31
+
32
+ export interface GeometryDebugSample {
33
+ readonly blockId: string;
34
+ readonly pageCount: number;
35
+ readonly space: "twips" | "frame" | "overlay";
36
+ }
37
+
38
+ export interface GeometryDebugEntry {
39
+ readonly schemaVersion: 1;
40
+ readonly catalog: GeometryDebugCatalog;
41
+ /** Up to `sampleLimit` sampled blocks for the inspector UI. */
42
+ readonly samples: readonly GeometryDebugSample[];
43
+ /** Viewport snapshot at projection time (scrollTop/scrollLeft/dpr/zoom). */
44
+ readonly viewport: {
45
+ readonly scrollLeftPx: number;
46
+ readonly scrollTopPx: number;
47
+ readonly dpr: number;
48
+ readonly pxPerTwip: number;
49
+ };
50
+ }
51
+
52
+ export interface BuildGeometryDebugEntryInputs {
53
+ readonly facet: GeometryFacet;
54
+ /**
55
+ * Block ids to sample. When omitted, an empty sample list is emitted —
56
+ * consumers are expected to supply a bounded list (e.g. the visible
57
+ * page range's blocks) to keep the projector's cost proportional.
58
+ */
59
+ readonly sampleBlockIds?: readonly string[];
60
+ /**
61
+ * Maximum number of sampled rects materialized. Default 16 — enough
62
+ * for an inspector UI, small enough to keep projection O(1) even on
63
+ * large documents.
64
+ */
65
+ readonly sampleLimit?: number;
66
+ /**
67
+ * Layout revision the caller is inspecting. The facet does not expose
68
+ * the revision directly; pass it through from the layout engine when
69
+ * available. Defaults to 0.
70
+ */
71
+ readonly layoutRevision?: number;
72
+ /** Frame page count — passed in so the projector stays facet-only. */
73
+ readonly pageCount?: number;
74
+ }
75
+
76
+ const DEFAULT_SAMPLE_LIMIT = 16;
77
+
78
+ export function buildGeometryDebugEntry(
79
+ inputs: BuildGeometryDebugEntryInputs,
80
+ ): GeometryDebugEntry {
81
+ const {
82
+ facet,
83
+ sampleBlockIds,
84
+ sampleLimit = DEFAULT_SAMPLE_LIMIT,
85
+ layoutRevision = 0,
86
+ pageCount = 0,
87
+ } = inputs;
88
+
89
+ const samples: GeometryDebugSample[] = [];
90
+ let sampledRectTotal = 0;
91
+ let firstBlockPageSpan = 0;
92
+
93
+ if (sampleBlockIds && sampleBlockIds.length > 0) {
94
+ const limit = Math.max(0, Math.min(sampleBlockIds.length, sampleLimit));
95
+ for (let i = 0; i < limit; i += 1) {
96
+ const blockId = sampleBlockIds[i]!;
97
+ const geometry = facet.getBlock(blockId);
98
+ if (!geometry) continue;
99
+ const rectCount = geometry.rects.length;
100
+ sampledRectTotal += rectCount;
101
+ if (samples.length === 0) firstBlockPageSpan = rectCount;
102
+ const firstSpace = geometry.rects[0]?.space ?? "frame";
103
+ samples.push({
104
+ blockId,
105
+ pageCount: rectCount,
106
+ space: firstSpace,
107
+ });
108
+ }
109
+ }
110
+
111
+ const viewport = facet.getViewport();
112
+
113
+ return {
114
+ schemaVersion: 1,
115
+ catalog: {
116
+ layoutRevision,
117
+ pageCount,
118
+ sampledBlockRectCount: sampledRectTotal,
119
+ firstBlockPageSpan,
120
+ },
121
+ samples,
122
+ viewport: {
123
+ scrollLeftPx: viewport.scrollLeftPx,
124
+ scrollTopPx: viewport.scrollTopPx,
125
+ dpr: viewport.dpr,
126
+ pxPerTwip: viewport.pxPerTwip,
127
+ },
128
+ };
129
+ }
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Layer 05 — Replacement envelope projection.
3
+ *
4
+ * A **replacement envelope** answers "what rects does this scope
5
+ * currently occupy?" so agent-facing replacement tooling can position
6
+ * chrome (overlays, confirmation cards, diff highlights) and know
7
+ * whether the scope is still resolvable. Architecture 05 G7:
8
+ *
9
+ * > If scope S still resolves, `getReplacementEnvelope(S)` produces a
10
+ * > rect bundle describing "the area occupied by this scope right
11
+ * > now". This is the geometry input to layer 08 scope bundles.
12
+ *
13
+ * Slice-5 shape: a pure helper that accepts a resolved scope shape
14
+ * (`{kind: "range", from, to}` or `{kind: "detached", lastKnownRange}`)
15
+ * plus the render frame, and produces an `EnvelopeBundle`. The facet's
16
+ * `getReplacementEnvelope(scopeId)` combines `resolveScope(document,
17
+ * scopeId)` with this helper.
18
+ *
19
+ * Slice-5 substrate notes:
20
+ *
21
+ * - `scopeRects` routes through `resolveSelectionRects` — i.e. the
22
+ * same Slice-4 path that returns the kernel's union rect for
23
+ * non-collapsed ranges. When per-line anchors ship in a later
24
+ * slice, `scopeRects` upgrades to one-rect-per-line automatically.
25
+ * - `linesCrossed` reflects the number of rects the helper produced,
26
+ * which matches the plan's semantics ("how many lines the scope
27
+ * crosses") once per-line enumeration is real. For Slice 5 it's
28
+ * `1` whenever the scope resolved.
29
+ * - **Wrap-aware envelopes** (scope adjacent to a floating image) are
30
+ * deferred — the current render frame does not expose wrap region
31
+ * metadata. The plan's `replacement-envelope-wrap-aware.test.ts`
32
+ * is reserved for that slice.
33
+ */
34
+
35
+ import type {
36
+ GeometryRect,
37
+ EnvelopeBundle,
38
+ GeometrySpace,
39
+ } from "./geometry-types.ts";
40
+ import type { RenderFrame } from "../render/index.ts";
41
+ import { resolveSelectionRects } from "./caret-geometry.ts";
42
+ import type { EditorStoryTarget } from "../../api/public-types";
43
+
44
+ /**
45
+ * Scope shape the helper accepts. Mirrors a subset of
46
+ * `EditorAnchorProjection` from the runtime API so the helper stays
47
+ * dependency-free and consumers can pass either the runtime's
48
+ * resolveScope output directly or a hand-built range.
49
+ */
50
+ export type ReplacementScope =
51
+ | { kind: "range"; from: number; to: number }
52
+ | {
53
+ kind: "detached";
54
+ lastKnownRange: { from: number; to: number };
55
+ };
56
+
57
+ /**
58
+ * Resolve the replacement envelope for a scope against the current
59
+ * render frame. Returns `null` when `frame` is null or when the range
60
+ * is empty and the scope is not detached (detached scopes without a
61
+ * lastKnownRange still return null — nothing to anchor on).
62
+ *
63
+ * The envelope's `space` carries through from the rect projection; the
64
+ * attachPoint uses the same space.
65
+ */
66
+ export function resolveReplacementEnvelope(
67
+ frame: RenderFrame | null,
68
+ scope: ReplacementScope,
69
+ story?: EditorStoryTarget,
70
+ ): EnvelopeBundle | null {
71
+ if (!frame) return null;
72
+
73
+ if (scope.kind === "range") {
74
+ const { from, to } = scope;
75
+ if (!Number.isFinite(from) || !Number.isFinite(to)) return null;
76
+ const rects = resolveSelectionRects(frame, { from, to, story });
77
+ if (rects.length === 0) return null;
78
+ return buildBundle(rects, "exact");
79
+ }
80
+
81
+ // Detached: project the last-known range and tag confidence as such.
82
+ const { from, to } = scope.lastKnownRange;
83
+ if (!Number.isFinite(from) || !Number.isFinite(to)) return null;
84
+ const rects = resolveSelectionRects(frame, { from, to, story });
85
+ if (rects.length === 0) return null;
86
+ return buildBundle(rects, "detached");
87
+ }
88
+
89
+ function buildBundle(
90
+ rects: readonly GeometryRect[],
91
+ confidence: EnvelopeBundle["confidence"],
92
+ ): EnvelopeBundle {
93
+ const first = rects[0]!;
94
+ const space: GeometrySpace = first.space;
95
+ // Slice 7b (2026-04-22): derive envelope precision from the
96
+ // underlying rects — when `resolveSelectionRects` returned a single
97
+ // union rect (wrapped range) it carries `precision: "within-
98
+ // tolerance"`; when it returned a caret rect (collapsed range) the
99
+ // rect is untagged (`"exact"` by default). The envelope's own
100
+ // precision is the coarsest of its rects, biased toward
101
+ // `"heuristic"` when the scope is detached because the last-known
102
+ // range is a point-in-time snapshot that may no longer match the
103
+ // current doc.
104
+ const rectPrecision = coarsestRectPrecision(rects);
105
+ const envelopePrecision: EnvelopeBundle["precision"] =
106
+ confidence === "detached" ? "heuristic" : rectPrecision;
107
+ return {
108
+ scopeRects: rects,
109
+ attachPoint: {
110
+ xPx: first.leftPx,
111
+ yPx: first.topPx,
112
+ space,
113
+ },
114
+ confidence,
115
+ linesCrossed: rects.length,
116
+ precision: envelopePrecision,
117
+ };
118
+ }
119
+
120
+ function coarsestRectPrecision(
121
+ rects: readonly GeometryRect[],
122
+ ): "exact" | "within-tolerance" | "heuristic" {
123
+ let worst: "exact" | "within-tolerance" | "heuristic" = "exact";
124
+ for (const rect of rects) {
125
+ const p = rect.precision ?? "exact";
126
+ if (p === "heuristic") return "heuristic";
127
+ if (p === "within-tolerance" && worst === "exact") worst = "within-tolerance";
128
+ }
129
+ return worst;
130
+ }
@@ -0,0 +1,218 @@
1
+ /**
2
+ * Layer 05 — Viewport model.
3
+ *
4
+ * This module is the **only** place permitted to read DOM viewport state
5
+ * (`scrollTop` / `scrollLeft`) and DPR. Contract G2 in
6
+ * `docs/architecture/05-geometry-projection.md` forbids every other file
7
+ * under `src/runtime/geometry/**` from reading DOM layout; the CI guard at
8
+ * `scripts/ci-check-geometry-no-dom-authority.mjs` enforces this by
9
+ * rejecting `getBoundingClientRect`, `offsetTop/Left/Width/Height`,
10
+ * `clientHeight`, and `offsetParent` outside this file. Slice 3 additionally
11
+ * permits this module to read viewport geometry from a host-supplied
12
+ * editor-root element.
13
+ *
14
+ * Slice 1: stub `createViewport()` returning a frozen no-DPR value.
15
+ * Slice 3 (this slice): adds `createViewportFromRoot(root, { pxPerTwip })`
16
+ * which attaches passive scroll + resize listeners to the root, reads
17
+ * `scrollLeft` / `scrollTop` only — per G2's viewport exception — and
18
+ * emits a new `Viewport` to subscribers via rAF coalescing (performance
19
+ * invariant 1). The stub `createViewport()` remains for tests and for
20
+ * wiring sites that have not yet flipped to a real root.
21
+ */
22
+
23
+ import type { Viewport, ViewportListener } from "./geometry-types.ts";
24
+
25
+ export interface ViewportHandle {
26
+ getViewport(): Viewport;
27
+ subscribe(listener: ViewportListener): () => void;
28
+ /**
29
+ * Tear down any DOM listeners + cancel pending rAF frames. Always safe
30
+ * to call; no-op on the stub handle from `createViewport()`.
31
+ */
32
+ dispose(): void;
33
+ }
34
+
35
+ const DEFAULT_VIEWPORT: Viewport = {
36
+ scrollLeftPx: 0,
37
+ scrollTopPx: 0,
38
+ dpr: 1,
39
+ pxPerTwip: 0,
40
+ };
41
+
42
+ /**
43
+ * Slice 1 stub — preserved for tests and for Slice-2a-wired call sites
44
+ * that do not yet have a live editor root. Returns a frozen viewport and
45
+ * a no-op subscribe/dispose.
46
+ */
47
+ export function createViewport(): ViewportHandle {
48
+ return {
49
+ getViewport() {
50
+ return DEFAULT_VIEWPORT;
51
+ },
52
+ subscribe() {
53
+ return () => {};
54
+ },
55
+ dispose() {},
56
+ };
57
+ }
58
+
59
+ export interface CreateViewportFromRootInput {
60
+ /**
61
+ * Editor root element. `scrollLeft` / `scrollTop` are read from here on
62
+ * scroll and resize; no other DOM fields are read (per G2). When the
63
+ * argument is `null` or the element is detached, the handle falls back
64
+ * to the stub viewport and `subscribe` never fires — matches the
65
+ * degraded Slice-1 path.
66
+ */
67
+ root: HTMLElement | null;
68
+ /**
69
+ * Current `pxPerTwip` from the render kernel's zoom. The handle stores
70
+ * it so consumers reading `Viewport.pxPerTwip` see the zoom the kernel
71
+ * used for the latest frame. Update via `setPxPerTwip(next)` when the
72
+ * kernel emits a `zoom_changed` event.
73
+ */
74
+ pxPerTwip: number;
75
+ }
76
+
77
+ export interface RootViewportHandle extends ViewportHandle {
78
+ /**
79
+ * Push a new `pxPerTwip` value. Notifies subscribers immediately so
80
+ * chrome overlays re-project on zoom changes without waiting for a
81
+ * scroll/resize event.
82
+ */
83
+ setPxPerTwip(value: number): void;
84
+ }
85
+
86
+ /**
87
+ * Slice 3 — DOM-backed viewport. Listens for passive scroll + ResizeObserver
88
+ * resize events on `root`; rAF-coalesces reads into a single listener
89
+ * fan-out per animation frame (performance invariant 1). Reads only
90
+ * `scrollLeft` / `scrollTop`; no layout-flushing geometry reads.
91
+ */
92
+ export function createViewportFromRoot(
93
+ input: CreateViewportFromRootInput,
94
+ ): RootViewportHandle {
95
+ const { root } = input;
96
+ if (!root) {
97
+ // Degraded path — behaves like the Slice-1 stub but exposes the
98
+ // setPxPerTwip entry point so call sites don't have to branch on
99
+ // whether a root was available.
100
+ let pxPerTwip = input.pxPerTwip;
101
+ const listeners = new Set<ViewportListener>();
102
+ const readOnce = (): Viewport => ({
103
+ ...DEFAULT_VIEWPORT,
104
+ pxPerTwip,
105
+ });
106
+ return {
107
+ getViewport: readOnce,
108
+ subscribe(listener) {
109
+ listeners.add(listener);
110
+ return () => listeners.delete(listener);
111
+ },
112
+ dispose() {
113
+ listeners.clear();
114
+ },
115
+ setPxPerTwip(value) {
116
+ if (value === pxPerTwip) return;
117
+ pxPerTwip = value;
118
+ const snapshot = readOnce();
119
+ for (const listener of listeners) listener(snapshot);
120
+ },
121
+ };
122
+ }
123
+
124
+ const win = root.ownerDocument?.defaultView ?? null;
125
+ let pxPerTwip = input.pxPerTwip;
126
+ const listeners = new Set<ViewportListener>();
127
+ let rafHandle: number | null = null;
128
+ let disposed = false;
129
+
130
+ const read = (): Viewport => ({
131
+ // G2 exception: scroll position is a viewport input, not a document-
132
+ // structure input. This is the one place in `src/runtime/geometry/`
133
+ // that reads these fields.
134
+ scrollLeftPx: root.scrollLeft,
135
+ scrollTopPx: root.scrollTop,
136
+ dpr:
137
+ typeof win?.devicePixelRatio === "number" ? win.devicePixelRatio : 1,
138
+ pxPerTwip,
139
+ });
140
+
141
+ let current = read();
142
+
143
+ const fanOut = () => {
144
+ if (disposed) return;
145
+ const next = read();
146
+ if (
147
+ next.scrollLeftPx === current.scrollLeftPx &&
148
+ next.scrollTopPx === current.scrollTopPx &&
149
+ next.dpr === current.dpr &&
150
+ next.pxPerTwip === current.pxPerTwip
151
+ ) {
152
+ return;
153
+ }
154
+ current = next;
155
+ for (const listener of listeners) listener(next);
156
+ };
157
+
158
+ const schedule = () => {
159
+ if (disposed) return;
160
+ if (rafHandle !== null) return;
161
+ const raf = win?.requestAnimationFrame;
162
+ if (!raf) {
163
+ // Test / SSR / older jsdom — fire synchronously.
164
+ fanOut();
165
+ return;
166
+ }
167
+ rafHandle = raf.call(win, () => {
168
+ rafHandle = null;
169
+ fanOut();
170
+ });
171
+ };
172
+
173
+ const onScroll = () => schedule();
174
+
175
+ root.addEventListener("scroll", onScroll, { passive: true });
176
+
177
+ let resizeObserver: ResizeObserver | null = null;
178
+ const ResizeObserverCtor = (win as { ResizeObserver?: typeof ResizeObserver } | null)
179
+ ?.ResizeObserver;
180
+ if (ResizeObserverCtor) {
181
+ resizeObserver = new ResizeObserverCtor(() => schedule());
182
+ resizeObserver.observe(root);
183
+ }
184
+
185
+ return {
186
+ getViewport() {
187
+ return current;
188
+ },
189
+ subscribe(listener) {
190
+ listeners.add(listener);
191
+ return () => {
192
+ listeners.delete(listener);
193
+ };
194
+ },
195
+ dispose() {
196
+ if (disposed) return;
197
+ disposed = true;
198
+ root.removeEventListener("scroll", onScroll);
199
+ if (resizeObserver) {
200
+ resizeObserver.disconnect();
201
+ resizeObserver = null;
202
+ }
203
+ if (rafHandle !== null && win?.cancelAnimationFrame) {
204
+ win.cancelAnimationFrame(rafHandle);
205
+ rafHandle = null;
206
+ }
207
+ listeners.clear();
208
+ },
209
+ setPxPerTwip(value) {
210
+ if (value === pxPerTwip) return;
211
+ pxPerTwip = value;
212
+ // Zoom change is observable immediately — bypass rAF so consumers
213
+ // don't see a stale pxPerTwip on the next render frame.
214
+ current = { ...current, pxPerTwip };
215
+ for (const listener of listeners) listener(current);
216
+ },
217
+ };
218
+ }