@beyondwork/docx-react-component 1.0.67 → 1.0.70

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (384) hide show
  1. package/README.md +75 -932
  2. package/package.json +26 -27
  3. package/src/api/anchor-conversion.ts +43 -0
  4. package/src/api/editor-state-types.ts +2 -1
  5. package/src/api/public-types.ts +504 -101
  6. package/src/api/session-state.ts +4 -0
  7. package/src/api/v3/README.md +91 -0
  8. package/src/api/v3/_create.ts +146 -0
  9. package/src/api/v3/_layer-metadata.ts +362 -0
  10. package/src/api/v3/_mocks.ts +84 -0
  11. package/src/api/v3/_runtime-handle.ts +162 -0
  12. package/src/api/v3/_ux-response.ts +73 -0
  13. package/src/api/v3/ai/_metadata-audit.ts +225 -0
  14. package/src/api/v3/ai/attach.ts +235 -0
  15. package/src/api/v3/ai/bundle.ts +132 -0
  16. package/src/api/v3/ai/explain.ts +144 -0
  17. package/src/api/v3/ai/export.ts +54 -0
  18. package/src/api/v3/ai/inspect.ts +118 -0
  19. package/src/api/v3/ai/policy.ts +77 -0
  20. package/src/api/v3/ai/replacement.ts +341 -0
  21. package/src/api/v3/ai/resolve.ts +133 -0
  22. package/src/api/v3/index.ts +79 -0
  23. package/src/api/v3/runtime/chart.ts +310 -0
  24. package/src/api/v3/runtime/clipboard.ts +81 -0
  25. package/src/api/v3/runtime/collab.ts +331 -0
  26. package/src/api/v3/runtime/content.ts +236 -0
  27. package/src/api/v3/runtime/document.ts +282 -0
  28. package/src/api/v3/runtime/formatting.ts +186 -0
  29. package/src/api/v3/runtime/geometry.ts +349 -0
  30. package/src/api/v3/runtime/layout.ts +108 -0
  31. package/src/api/v3/runtime/review.ts +129 -0
  32. package/src/api/v3/runtime/search.ts +74 -0
  33. package/src/api/v3/runtime/table.ts +63 -0
  34. package/src/api/v3/runtime/workflow.ts +434 -0
  35. package/src/api/v3/ui/_context.ts +86 -0
  36. package/src/api/v3/ui/_create.ts +65 -0
  37. package/src/api/v3/ui/_types.ts +520 -0
  38. package/src/api/v3/ui/chrome-composition.ts +342 -0
  39. package/src/{ui-tailwind/chrome → api/v3/ui}/chrome-preset-model.ts +11 -1
  40. package/src/api/v3/ui/chrome.ts +476 -0
  41. package/src/api/v3/ui/debug.ts +124 -0
  42. package/src/api/v3/ui/index.ts +64 -0
  43. package/src/api/v3/ui/overlays-visibility.ts +170 -0
  44. package/src/api/v3/ui/overlays.ts +427 -0
  45. package/src/api/v3/ui/scope.ts +71 -0
  46. package/src/api/v3/ui/session.ts +100 -0
  47. package/src/api/v3/ui/surface.ts +170 -0
  48. package/src/api/v3/ui/viewport.ts +303 -0
  49. package/src/core/commands/index.ts +28 -6
  50. package/src/core/commands/list-commands.ts +3 -2
  51. package/src/core/commands/section-layout-commands.ts +9 -8
  52. package/src/core/schema/text-schema.ts +16 -0
  53. package/src/core/selection/mapping.ts +33 -72
  54. package/src/core/state/editor-state.ts +96 -189
  55. package/src/index.ts +23 -4
  56. package/src/io/chart-preview-resolver.ts +1 -1
  57. package/src/io/docx-session.ts +36 -4797
  58. package/src/io/export/build-app-properties-xml.ts +1 -1
  59. package/src/io/export/serialize-comments.ts +1 -1
  60. package/src/io/export/serialize-headers-footers.ts +6 -1
  61. package/src/io/export/serialize-main-document.ts +45 -0
  62. package/src/io/export/serialize-run-formatting.ts +17 -2
  63. package/src/io/export/twip.ts +1 -1
  64. package/src/io/normalize/normalize-text.ts +27 -20
  65. package/src/io/ooxml/chart/parse-series.ts +1 -1
  66. package/src/io/ooxml/chart/resolve-color.ts +2 -2
  67. package/src/io/ooxml/chart/types.ts +1 -1
  68. package/src/io/ooxml/classify-embedding.ts +83 -33
  69. package/src/io/ooxml/parse-fill.ts +1 -1
  70. package/src/io/ooxml/parse-main-document.ts +71 -1
  71. package/src/io/ooxml/parse-object.ts +14 -10
  72. package/src/io/ooxml/parse-run-formatting.ts +47 -1
  73. package/src/io/ooxml/property-grab-bag.ts +2 -2
  74. package/src/io/ooxml/units.ts +11 -0
  75. package/src/io/ooxml/workflow-payload.ts +282 -7
  76. package/src/model/anchor.ts +85 -0
  77. package/src/model/canonical-document.ts +351 -15
  78. package/src/model/chart-types.ts +1 -1
  79. package/src/model/layout/index.ts +83 -0
  80. package/src/model/layout/page-graph-types.ts +181 -0
  81. package/src/model/layout/page-layout-snapshot.ts +105 -0
  82. package/src/model/layout/resolved-layout-types.ts +47 -0
  83. package/src/model/layout/runtime-page-graph-types.ts +102 -0
  84. package/src/model/paragraph-scope-ids.ts +72 -0
  85. package/src/model/review/comment-types.ts +112 -0
  86. package/src/model/review/index.ts +2 -0
  87. package/src/model/review/revision-types.ts +215 -0
  88. package/src/model/snapshot.ts +32 -0
  89. package/src/review/store/comment-store.ts +21 -47
  90. package/src/review/store/revision-types.ts +40 -198
  91. package/src/runtime/collab/base-doc-fingerprint.ts +6 -1
  92. package/src/runtime/collab/runtime-collab-sync.ts +13 -3
  93. package/src/runtime/collab-session.ts +1 -1
  94. package/src/runtime/debug/build-debug-inspector-snapshot.ts +686 -0
  95. package/src/runtime/debug/event-ring-buffer.ts +64 -0
  96. package/src/runtime/debug/probability-sampler.ts +18 -0
  97. package/src/runtime/debug/runtime-debug-facet.ts +67 -0
  98. package/src/runtime/debug/stage-tokens.ts +31 -0
  99. package/src/runtime/debug/telemetry-bus.ts +271 -0
  100. package/src/runtime/debug/types.ts +275 -0
  101. package/src/runtime/debug/wrap-ref-for-telemetry.ts +118 -0
  102. package/src/runtime/document-layout.ts +8 -6
  103. package/src/runtime/document-runtime.ts +843 -1141
  104. package/src/runtime/document-search.ts +1 -1
  105. package/src/runtime/edit-ops/index.ts +1 -1
  106. package/src/runtime/external-send-runtime.ts +1 -1
  107. package/src/runtime/formatting/document-lookup.ts +235 -0
  108. package/src/runtime/formatting/field/registry.ts +41 -0
  109. package/src/runtime/{field-resolver.ts → formatting/field/resolver.ts} +27 -2
  110. package/src/runtime/formatting/font-resolution.ts +83 -0
  111. package/src/runtime/formatting/formatting-context.ts +903 -0
  112. package/src/runtime/formatting/formatting-types.ts +157 -0
  113. package/src/runtime/{hyperlink-color-resolver.ts → formatting/hyperlink-color.ts} +2 -2
  114. package/src/runtime/formatting/index.ts +125 -0
  115. package/src/runtime/{resolved-numbering-geometry.ts → formatting/numbering/geometry.ts} +1 -1
  116. package/src/runtime/{numbering-prefix.ts → formatting/numbering/prefix.ts} +170 -3
  117. package/src/runtime/formatting/paragraph-style-resolver.ts +92 -0
  118. package/src/runtime/formatting/projector.ts +75 -0
  119. package/src/runtime/formatting/resolve-effective.ts +407 -0
  120. package/src/runtime/formatting/revision-display.ts +105 -0
  121. package/src/runtime/{paragraph-style-resolver.ts → formatting/style-cascade.ts} +84 -141
  122. package/src/runtime/{table-style-resolver.ts → formatting/table-style-resolver.ts} +1 -1
  123. package/src/runtime/formatting/telemetry-bridge.ts +106 -0
  124. package/src/runtime/{theme-color-resolver.ts → formatting/theme-color.ts} +2 -30
  125. package/src/runtime/geometry/caret-geometry.ts +164 -0
  126. package/src/runtime/geometry/geometry-facet.ts +364 -0
  127. package/src/runtime/geometry/geometry-types.ts +256 -0
  128. package/src/runtime/geometry/hit-test.ts +125 -0
  129. package/src/runtime/geometry/index.ts +71 -0
  130. package/src/runtime/geometry/inert-geometry-facet.ts +43 -0
  131. package/src/runtime/geometry/invalidation.ts +35 -0
  132. package/src/runtime/geometry/object-handles.ts +77 -0
  133. package/src/runtime/geometry/overlay-rects.ts +85 -0
  134. package/src/runtime/geometry/project-anchors.ts +100 -0
  135. package/src/runtime/geometry/project-fragments.ts +216 -0
  136. package/src/runtime/geometry/projector.ts +129 -0
  137. package/src/runtime/geometry/replacement-envelope.ts +130 -0
  138. package/src/runtime/geometry/viewport.ts +218 -0
  139. package/src/runtime/layout/compat-input-ledger.ts +211 -0
  140. package/src/runtime/layout/index.ts +6 -1
  141. package/src/runtime/layout/inert-layout-facet.ts +12 -7
  142. package/src/runtime/layout/layout-engine-instance.ts +189 -11
  143. package/src/runtime/layout/layout-engine-version.ts +450 -1
  144. package/src/runtime/layout/layout-facet-types.ts +60 -0
  145. package/src/runtime/layout/layout-measurement-provider.ts +13 -0
  146. package/src/runtime/layout/measurement-backend-canvas.ts +14 -2
  147. package/src/runtime/layout/measurement-backend-empirical.ts +23 -4
  148. package/src/runtime/layout/page-graph.ts +62 -209
  149. package/src/runtime/layout/page-story-resolver.ts +7 -12
  150. package/src/runtime/layout/paginated-layout-engine.ts +186 -11
  151. package/src/runtime/layout/project-block-fragments.ts +11 -0
  152. package/src/runtime/layout/projector.ts +90 -0
  153. package/src/runtime/layout/public-facet.ts +187 -442
  154. package/src/runtime/layout/resolved-formatting-state.ts +158 -26
  155. package/src/runtime/layout/table-render-plan.ts +1 -1
  156. package/src/runtime/prerender/cache-envelope.ts +6 -1
  157. package/src/runtime/prerender/prerender-document.ts +18 -23
  158. package/src/runtime/render/decoration-resolver.ts +1 -1
  159. package/src/runtime/render/render-frame-types.ts +20 -0
  160. package/src/runtime/render/render-kernel.ts +94 -25
  161. package/src/runtime/scopes/_formatting-seam.ts +262 -0
  162. package/src/runtime/scopes/_scope-dependencies.ts +49 -0
  163. package/src/runtime/scopes/action-validation.ts +356 -0
  164. package/src/runtime/scopes/attach-explanation.ts +102 -0
  165. package/src/runtime/scopes/audit-bundle.ts +71 -0
  166. package/src/runtime/scopes/compile-scope-bundle.ts +163 -0
  167. package/src/runtime/scopes/compile-scope.ts +262 -0
  168. package/src/runtime/scopes/compiler-service.ts +431 -0
  169. package/src/runtime/scopes/create-issue.ts +107 -0
  170. package/src/runtime/scopes/enumerate-scopes.ts +543 -0
  171. package/src/runtime/scopes/evidence.ts +233 -0
  172. package/src/runtime/scopes/index.ts +150 -0
  173. package/src/runtime/scopes/position-map.ts +214 -0
  174. package/src/runtime/scopes/preservation-boundary.ts +91 -0
  175. package/src/runtime/scopes/projector.ts +49 -0
  176. package/src/runtime/scopes/replaceability.ts +87 -0
  177. package/src/runtime/scopes/replacement/apply.ts +228 -0
  178. package/src/runtime/scopes/replacement/compile.ts +59 -0
  179. package/src/runtime/scopes/replacement/propose.ts +42 -0
  180. package/src/runtime/scopes/resolve-reference.ts +347 -0
  181. package/src/runtime/scopes/review-bundle.ts +141 -0
  182. package/src/runtime/scopes/scope-kinds/_paragraph-text.ts +57 -0
  183. package/src/runtime/scopes/scope-kinds/_table-text.ts +42 -0
  184. package/src/runtime/scopes/scope-kinds/comment-thread.ts +59 -0
  185. package/src/runtime/scopes/scope-kinds/field.ts +65 -0
  186. package/src/runtime/scopes/scope-kinds/heading.ts +84 -0
  187. package/src/runtime/scopes/scope-kinds/list-item.ts +77 -0
  188. package/src/runtime/scopes/scope-kinds/paragraph.ts +182 -0
  189. package/src/runtime/scopes/scope-kinds/revision.ts +62 -0
  190. package/src/runtime/scopes/scope-kinds/table-cell.ts +57 -0
  191. package/src/runtime/scopes/scope-kinds/table-row.ts +61 -0
  192. package/src/runtime/scopes/scope-kinds/table.ts +55 -0
  193. package/src/runtime/scopes/scope-range.ts +208 -0
  194. package/src/runtime/scopes/semantic-scope-types.ts +454 -0
  195. package/src/runtime/scopes/workflow-overlap.ts +92 -0
  196. package/src/runtime/selection/index.ts +1 -1
  197. package/src/runtime/structure-ops/fragment-insert.ts +1 -1
  198. package/src/runtime/structure-ops/index.ts +1 -1
  199. package/src/runtime/surface-projection.ts +232 -262
  200. package/src/runtime/units.ts +4 -2
  201. package/src/runtime/workflow/coordinator.ts +1348 -0
  202. package/src/runtime/workflow/derived-scope-resolver.ts +125 -0
  203. package/src/runtime/workflow/index.ts +25 -0
  204. package/src/runtime/workflow/markup-mode-policy.ts +98 -0
  205. package/src/runtime/{workflow-markup.ts → workflow/markup.ts} +6 -6
  206. package/src/runtime/workflow/metadata-persistence.ts +306 -0
  207. package/src/runtime/workflow/metadata-writer.ts +123 -0
  208. package/src/runtime/workflow/overlay-store.ts +690 -0
  209. package/src/runtime/workflow/projector.ts +127 -0
  210. package/src/runtime/{query-scopes.ts → workflow/query-scopes.ts} +3 -3
  211. package/src/runtime/{workflow-rail-segments.ts → workflow/rail/compose.ts} +60 -165
  212. package/src/runtime/workflow/rail/types.ts +198 -0
  213. package/src/runtime/workflow/scope-rail-composer.ts +39 -0
  214. package/src/runtime/{scope-resolver.ts → workflow/scope-resolver.ts} +3 -3
  215. package/src/runtime/workflow/scope-writer.ts +188 -0
  216. package/src/runtime/{tamper-gate.ts → workflow/tamper-gate.ts} +1 -1
  217. package/src/runtime/workflow/visibility-policy.ts +129 -0
  218. package/src/session/_sync-legacy.ts +66 -0
  219. package/src/session/export/embedded-reconstitute.ts +104 -0
  220. package/src/session/export/export-diagnostics.ts +85 -0
  221. package/src/session/export/export-validation.ts +110 -0
  222. package/src/session/export/index.ts +34 -0
  223. package/src/session/export/preservation-reattach.ts +30 -0
  224. package/src/session/export/serialize-dispatch.ts +165 -0
  225. package/src/session/export/stateful-export-pipeline.ts +432 -0
  226. package/src/session/export/stateful-export.ts +684 -0
  227. package/src/session/import/canonical-assembly.ts +227 -0
  228. package/src/session/import/diagnostics-session.ts +54 -0
  229. package/src/session/import/embedded-discovery.ts +225 -0
  230. package/src/session/import/embedded-offload.ts +337 -0
  231. package/src/session/import/import-diagnostics.ts +69 -0
  232. package/src/session/import/loader-types.ts +313 -0
  233. package/src/session/import/loader.ts +1834 -0
  234. package/src/session/import/normalize.ts +195 -0
  235. package/src/session/import/package-parts.ts +217 -0
  236. package/src/session/import/package-read.ts +195 -0
  237. package/src/session/import/parse-orchestration.ts +105 -0
  238. package/src/session/import/part-constants.ts +70 -0
  239. package/src/session/import/part-discovery.ts +94 -0
  240. package/src/session/import/preservation-index.ts +46 -0
  241. package/src/{runtime/read-only-diagnostics-runtime.ts → session/import/read-only-diagnostics.ts} +24 -3
  242. package/src/session/import/review-import.ts +508 -0
  243. package/src/session/import/styles-consolidation.ts +281 -0
  244. package/src/session/import/workflow-scope-import.ts +256 -0
  245. package/src/session/index.ts +37 -0
  246. package/src/session/session-state.ts +69 -0
  247. package/src/session/session.ts +532 -0
  248. package/src/session/shared/protection.ts +228 -0
  249. package/src/session/shared/session-utils.ts +82 -0
  250. package/src/session/types.ts +499 -0
  251. package/src/shell/chart-snapshots.ts +96 -0
  252. package/src/shell/media-previews.ts +85 -0
  253. package/src/shell/overlay-anchor-bridge.ts +53 -0
  254. package/src/shell/paste-adapter.ts +23 -0
  255. package/src/shell/ref-commands.ts +1697 -0
  256. package/src/shell/ref-utilities.ts +48 -0
  257. package/src/shell/search.ts +51 -0
  258. package/src/{ui/editor-runtime-boundary.ts → shell/session-bootstrap.ts} +243 -67
  259. package/src/shell/ui-subscriber-channels.ts +81 -0
  260. package/src/shell/use-collab-sync.ts +116 -0
  261. package/src/ui/WordReviewEditor.tsx +496 -2051
  262. package/src/ui/editor-shell-view.tsx +30 -1
  263. package/src/ui/editor-surface-controller.tsx +49 -1
  264. package/src/ui/headless/revision-decoration-model.ts +83 -0
  265. package/src/{ui-tailwind/chrome → ui/headless}/role-action-sets.ts +1 -1
  266. package/src/ui/headless/scoped-chrome-policy.ts +2 -2
  267. package/src/ui/headless/selection-tool-context.ts +1 -1
  268. package/src/ui/headless/selection-tool-resolver.ts +1 -1
  269. package/src/ui/runtime-shortcut-dispatch.ts +46 -1
  270. package/src/ui/ui-controller-factory.ts +221 -0
  271. package/src/ui-tailwind/chart/ChartSurface.tsx +2 -2
  272. package/src/ui-tailwind/chart/layout/legend-layout.ts +1 -1
  273. package/src/ui-tailwind/chart/layout/plot-area.ts +2 -2
  274. package/src/ui-tailwind/chart/layout/title-layout.ts +1 -1
  275. package/src/ui-tailwind/chart/render/area.tsx +3 -3
  276. package/src/ui-tailwind/chart/render/bar-column.tsx +3 -3
  277. package/src/ui-tailwind/chart/render/bubble.tsx +3 -3
  278. package/src/ui-tailwind/chart/render/combo.tsx +2 -2
  279. package/src/ui-tailwind/chart/render/data-labels.tsx +2 -2
  280. package/src/ui-tailwind/chart/render/font-metrics.ts +2 -2
  281. package/src/ui-tailwind/chart/render/line.tsx +3 -3
  282. package/src/ui-tailwind/chart/render/pie.tsx +6 -6
  283. package/src/ui-tailwind/chart/render/scatter.tsx +3 -3
  284. package/src/ui-tailwind/chart/render/svg-primitives.ts +3 -3
  285. package/src/ui-tailwind/chart/render/unsupported.tsx +2 -2
  286. package/src/ui-tailwind/chrome/build-context-menu-entries.ts +88 -0
  287. package/src/ui-tailwind/chrome/chrome-preset-toolbar.tsx +1 -1
  288. package/src/ui-tailwind/chrome/collab-send-to-supplier-button.tsx +1 -1
  289. package/src/ui-tailwind/chrome/collab-tamper-banner.tsx +1 -1
  290. package/src/ui-tailwind/chrome/collab-top-nav-container.tsx +1 -1
  291. package/src/ui-tailwind/chrome/editor-action-registry.ts +553 -0
  292. package/src/ui-tailwind/chrome/editor-actions-to-palette.ts +182 -0
  293. package/src/ui-tailwind/chrome/local-surface-arbiter.ts +534 -0
  294. package/src/ui-tailwind/chrome/resolve-target-kind.ts +226 -0
  295. package/src/ui-tailwind/chrome/tw-alert-banner.tsx +38 -4
  296. package/src/ui-tailwind/chrome/tw-context-band.tsx +125 -0
  297. package/src/ui-tailwind/chrome/tw-context-menu-portal.tsx +248 -0
  298. package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +42 -1
  299. package/src/ui-tailwind/chrome/tw-selection-anchor-resolver.ts +8 -7
  300. package/src/ui-tailwind/chrome/tw-selection-tool-blocked.tsx +38 -4
  301. package/src/ui-tailwind/chrome/tw-selection-tool-comment.tsx +104 -6
  302. package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +66 -7
  303. package/src/ui-tailwind/chrome/tw-selection-tool-workflow.tsx +54 -8
  304. package/src/ui-tailwind/chrome/tw-shortcut-hint.tsx +7 -1
  305. package/src/ui-tailwind/chrome/tw-suggestion-card.tsx +33 -0
  306. package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +78 -1
  307. package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +16 -8
  308. package/src/ui-tailwind/chrome/tw-workspace-chrome-host.tsx +276 -0
  309. package/src/ui-tailwind/chrome/use-context-menu-controller.ts +201 -0
  310. package/src/ui-tailwind/chrome-overlay/chrome-overlay-projector.ts +1 -1
  311. package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +22 -4
  312. package/src/ui-tailwind/chrome-overlay/tw-comment-balloon-layer.tsx +1 -1
  313. package/src/ui-tailwind/chrome-overlay/tw-locked-block-layer.tsx +1 -1
  314. package/src/ui-tailwind/chrome-overlay/tw-object-selection-overlay.tsx +11 -5
  315. package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +197 -3
  316. package/src/ui-tailwind/chrome-overlay/tw-revision-margin-bar-layer.tsx +1 -1
  317. package/src/ui-tailwind/chrome-overlay/tw-scope-card-layer.tsx +35 -6
  318. package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +24 -16
  319. package/src/ui-tailwind/chrome-overlay/tw-table-continuation-header.tsx +1 -1
  320. package/src/ui-tailwind/debug/README.md +57 -0
  321. package/src/ui-tailwind/debug/index.ts +3 -0
  322. package/src/ui-tailwind/debug/tw-debug-overlay.tsx +186 -0
  323. package/src/ui-tailwind/debug/tw-debug-presentation.tsx +80 -0
  324. package/src/ui-tailwind/debug/tw-debug-top-bar.tsx +83 -0
  325. package/src/ui-tailwind/editor-surface/chart-node-view.tsx +2 -2
  326. package/src/ui-tailwind/editor-surface/float-wrap-resolver.ts +1 -1
  327. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +135 -10
  328. package/src/ui-tailwind/editor-surface/pm-decorations.ts +40 -13
  329. package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +1 -1
  330. package/src/ui-tailwind/editor-surface/pm-schema.ts +1 -1
  331. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +3 -3
  332. package/src/ui-tailwind/editor-surface/predicted-tag-preflight.ts +1 -1
  333. package/src/ui-tailwind/editor-surface/remote-cursor-plugin.ts +2 -2
  334. package/src/ui-tailwind/editor-surface/scroll-anchor.ts +91 -9
  335. package/src/ui-tailwind/editor-surface/shape-renderer.ts +1 -1
  336. package/src/ui-tailwind/editor-surface/surface-layer.ts +1 -1
  337. package/src/ui-tailwind/editor-surface/tw-opaque-block.tsx +1 -1
  338. package/src/ui-tailwind/editor-surface/tw-page-block-view.helpers.ts +23 -6
  339. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +132 -22
  340. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +1 -1
  341. package/src/ui-tailwind/index.ts +0 -5
  342. package/src/ui-tailwind/overlay-anchor-bridge-context.tsx +33 -0
  343. package/src/ui-tailwind/page-stack/floating-image-overlay-model.ts +66 -29
  344. package/src/ui-tailwind/page-stack/tw-floating-image-layer.tsx +25 -2
  345. package/src/ui-tailwind/review/comment-markdown-renderer.tsx +15 -0
  346. package/src/ui-tailwind/review/tw-review-rail.tsx +92 -4
  347. package/src/ui-tailwind/review/tw-workflow-tab.tsx +1 -1
  348. package/src/ui-tailwind/review-workspace/page-chrome.ts +210 -0
  349. package/src/ui-tailwind/review-workspace/page-shell-metrics.ts +101 -0
  350. package/src/ui-tailwind/review-workspace/paragraph-layout.ts +115 -0
  351. package/src/ui-tailwind/review-workspace/selection-toolbar-placement.ts +97 -0
  352. package/src/ui-tailwind/review-workspace/tw-review-workspace-navigator.tsx +130 -0
  353. package/src/ui-tailwind/review-workspace/tw-review-workspace-page-toolbar.tsx +240 -0
  354. package/src/ui-tailwind/review-workspace/tw-review-workspace-rail.tsx +59 -0
  355. package/src/ui-tailwind/review-workspace/types.ts +408 -0
  356. package/src/ui-tailwind/review-workspace/use-chrome-policy.ts +104 -0
  357. package/src/ui-tailwind/review-workspace/use-derived-view-state.ts +151 -0
  358. package/src/ui-tailwind/review-workspace/use-diagnostics-signal.ts +70 -0
  359. package/src/ui-tailwind/review-workspace/use-grabbed-segment-offsets.ts +40 -0
  360. package/src/ui-tailwind/review-workspace/use-layout-facet-render-signal.ts +55 -0
  361. package/src/ui-tailwind/review-workspace/use-page-markers.ts +130 -0
  362. package/src/ui-tailwind/review-workspace/use-pm-surface-capture.ts +60 -0
  363. package/src/ui-tailwind/review-workspace/use-review-rail-state.ts +63 -0
  364. package/src/ui-tailwind/review-workspace/use-scope-card-state.ts +170 -0
  365. package/src/ui-tailwind/review-workspace/use-scroll-root-capture.ts +28 -0
  366. package/src/ui-tailwind/review-workspace/use-selection-toolbar-placement.ts +113 -0
  367. package/src/ui-tailwind/review-workspace/use-shell-selection-anchor-bridge.ts +120 -0
  368. package/src/ui-tailwind/review-workspace/use-status-bar-page-facts.ts +55 -0
  369. package/src/ui-tailwind/review-workspace/use-viewport-dimensions.ts +43 -0
  370. package/src/ui-tailwind/review-workspace/use-workspace-arbiter.ts +25 -0
  371. package/src/ui-tailwind/review-workspace/use-workspace-composition.ts +86 -0
  372. package/src/ui-tailwind/review-workspace/use-workspace-side-effects.ts +150 -0
  373. package/src/ui-tailwind/theme/editor-theme.css +25 -0
  374. package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +2 -2
  375. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +61 -98
  376. package/src/ui-tailwind/tw-review-workspace.tsx +521 -1802
  377. package/src/ui-tailwind/ui-api-context.tsx +43 -0
  378. package/src/ui-tailwind/ui-shell-channels-context.tsx +49 -0
  379. package/src/validation/compatibility-engine.ts +6 -6
  380. package/src/runtime/styles-cascade.ts +0 -33
  381. package/src/ui-tailwind/chrome/tw-mode-dock.tsx +0 -85
  382. /package/src/runtime/{page-number-format.ts → formatting/field/page-number-format.ts} +0 -0
  383. /package/src/runtime/{ai-action-policy.ts → workflow/ai-action-policy.ts} +0 -0
  384. /package/src/runtime/{scope-tag-registry.ts → workflow/scope-tag-registry.ts} +0 -0
@@ -0,0 +1,226 @@
1
+ /**
2
+ * resolveTargetKind — resolve a `contextmenu` event target to one or more
3
+ * `TargetKind` values so the shared editor-action-registry can return the
4
+ * correct action set.
5
+ *
6
+ * Per DESIGN-EDITOR.md §6.4, right-click + inline "More…" + command
7
+ * palette must read from the same registry. To do that they need a
8
+ * consistent way to identify the clicked target. The bridge hook
9
+ * (Phase C.β) hands us a raw `EventTarget | null`; this helper walks to
10
+ * the nearest Element and inspects the real `data-*` attributes the
11
+ * editor emits + a few tag-based fallbacks.
12
+ *
13
+ * Marker contract — the attributes / tags checked here MUST stay in
14
+ * sync with what the decoration renderers emit. Every marker cited
15
+ * below has a `grep -rn` anchor you can verify against:
16
+ *
17
+ * - comment-anchor → `data-comment-id`
18
+ * emitted at `src/ui-tailwind/editor-surface/pm-decorations.ts:399`
19
+ * and `src/ui-tailwind/editor-surface/tw-segment-view.tsx:97`;
20
+ * consumed by `pm-contextual-ui.ts:15`.
21
+ * - suggestion → `data-revision-id`
22
+ * emitted at `pm-decorations.ts:421/438/463/484`; consumed by
23
+ * `pm-contextual-ui.ts:21`.
24
+ * - scope-anchor → `data-workflow-scope-id` OR `data-scope-id`
25
+ * emitted at `pm-decorations.ts:518/531`, the rail layer, and
26
+ * scope cards.
27
+ * - object → `data-object-id` OR `data-object-selection`
28
+ * emitted at `chrome-overlay/tw-object-selection-overlay.tsx:106-107`.
29
+ * - image → `<img>` tag OR `data-floating-image-id`
30
+ * (page-stack/tw-floating-image-layer.tsx:189) OR
31
+ * `data-preview-media-id` (pm-schema.ts inline images).
32
+ * - table-cell → `<td>` / `<th>` tag.
33
+ * - hyperlink → `<a>` tag.
34
+ * - opaque-block → `data-block-kind="opaque"`
35
+ * added to `TwOpaqueBlock` in Phase D.5 as the explicit marker.
36
+ *
37
+ * Multiple kinds can apply — a table cell that contains a hyperlink
38
+ * returns both `table-cell` and `hyperlink`. Caller merges the
39
+ * resulting action sets. Always includes `"plain-text"` as a baseline
40
+ * so clipboard (cut/copy/paste) remains available.
41
+ *
42
+ * Perf discipline: synchronous DOM traversal, but only fires on
43
+ * user-initiated `contextmenu` events — never on the edit path
44
+ * (invariant 7). No `getBoundingClientRect` / `offsetTop` reads.
45
+ */
46
+
47
+ import type { TargetKind } from "./editor-action-registry";
48
+
49
+ /**
50
+ * Duck-typed Node.nodeType values so this module doesn't depend on
51
+ * the `Node` / `Element` browser globals (the pm-command-bridge test
52
+ * fixtures run in bare node where those globals aren't installed).
53
+ */
54
+ const NODE_TYPE_ELEMENT = 1;
55
+
56
+ /**
57
+ * Cast an `EventTarget` to the nearest containing `Element`. Text
58
+ * nodes return their parent; null / non-Node targets return null.
59
+ * Duck-typed so tests without DOM globals still work.
60
+ */
61
+ function toElement(target: EventTarget | null): Element | null {
62
+ if (!target) return null;
63
+ const node = target as Partial<Node> & Partial<Element>;
64
+ if (typeof node.nodeType !== "number") return null;
65
+ if (node.nodeType === NODE_TYPE_ELEMENT) return target as Element;
66
+ return node.parentElement ?? null;
67
+ }
68
+
69
+ /**
70
+ * Check whether `el` OR any ancestor (up to `root` if provided) has
71
+ * any of the listed attributes set (with a non-empty value).
72
+ */
73
+ function hasAncestorAttribute(
74
+ el: Element,
75
+ attributeNames: readonly string[],
76
+ root: Element | undefined,
77
+ ): boolean {
78
+ let cursor: Element | null = el;
79
+ while (cursor) {
80
+ for (const attr of attributeNames) {
81
+ if (cursor.hasAttribute?.(attr)) {
82
+ const value = cursor.getAttribute(attr);
83
+ // Empty-attr markers (like `data-object-selection=""`) count as
84
+ // present — hasAttribute returns true for empty strings. Only
85
+ // filter explicitly null, which shouldn't happen but guards
86
+ // against duck-typed test doubles.
87
+ if (value !== null) return true;
88
+ }
89
+ }
90
+ if (cursor === root) return false;
91
+ cursor = cursor.parentElement;
92
+ }
93
+ return false;
94
+ }
95
+
96
+ /**
97
+ * Check whether `el` OR any ancestor has a given tag name.
98
+ */
99
+ function hasAncestorTag(
100
+ el: Element,
101
+ tagName: string,
102
+ root: Element | undefined,
103
+ ): boolean {
104
+ const upper = tagName.toUpperCase();
105
+ let cursor: Element | null = el;
106
+ while (cursor) {
107
+ if (cursor.tagName === upper) return true;
108
+ if (cursor === root) return false;
109
+ cursor = cursor.parentElement;
110
+ }
111
+ return false;
112
+ }
113
+
114
+ /**
115
+ * Check whether `el` OR any ancestor has a given attribute whose value
116
+ * matches `expected`. Used for the `data-block-kind="opaque"` marker
117
+ * where an attribute with the wrong value (e.g. `data-block-kind="code"`)
118
+ * should NOT classify as opaque.
119
+ */
120
+ function hasAncestorAttributeValue(
121
+ el: Element,
122
+ attribute: string,
123
+ expected: string,
124
+ root: Element | undefined,
125
+ ): boolean {
126
+ let cursor: Element | null = el;
127
+ while (cursor) {
128
+ if (cursor.getAttribute?.(attribute) === expected) return true;
129
+ if (cursor === root) return false;
130
+ cursor = cursor.parentElement;
131
+ }
132
+ return false;
133
+ }
134
+
135
+ export interface ResolveTargetKindOptions {
136
+ /**
137
+ * Optional root — traversal stops here so a right-click inside the
138
+ * editor surface doesn't walk out into the shell header / rail.
139
+ * Typically the editor DOM root.
140
+ */
141
+ readonly root?: Element;
142
+ }
143
+
144
+ /**
145
+ * Return every target kind that applies to the given DOM target,
146
+ * always including `"plain-text"` as a fallback so clipboard actions
147
+ * are available. Order is stable: more specific kinds come before
148
+ * `"plain-text"`.
149
+ */
150
+ export function resolveTargetKind(
151
+ target: EventTarget | null,
152
+ options: ResolveTargetKindOptions = {},
153
+ ): readonly TargetKind[] {
154
+ const kinds: TargetKind[] = [];
155
+ const el = toElement(target);
156
+
157
+ if (el) {
158
+ const { root } = options;
159
+
160
+ // Tag-based checks — an <a> is unambiguously a hyperlink, etc.
161
+ if (hasAncestorTag(el, "a", root)) kinds.push("hyperlink");
162
+ if (hasAncestorTag(el, "img", root)) {
163
+ kinds.push("image");
164
+ }
165
+ if (hasAncestorTag(el, "td", root) || hasAncestorTag(el, "th", root)) {
166
+ kinds.push("table-cell");
167
+ }
168
+
169
+ // Real `data-*` markers — see the module header for emitter citations.
170
+
171
+ // Comment decorations — pm-decorations.ts:399 + tw-segment-view.tsx:97.
172
+ if (hasAncestorAttribute(el, ["data-comment-id"], root)) {
173
+ kinds.push("comment-anchor");
174
+ }
175
+
176
+ // Revision decorations — pm-decorations.ts:421/438/463/484.
177
+ if (hasAncestorAttribute(el, ["data-revision-id"], root)) {
178
+ kinds.push("suggestion");
179
+ }
180
+
181
+ // Workflow scope — pm-decorations.ts:518/531 + chrome-overlay cards.
182
+ if (
183
+ hasAncestorAttribute(
184
+ el,
185
+ ["data-workflow-scope-id", "data-scope-id"],
186
+ root,
187
+ )
188
+ ) {
189
+ kinds.push("scope-anchor");
190
+ }
191
+
192
+ // Object selection overlay — chrome-overlay/tw-object-selection-overlay.tsx.
193
+ if (
194
+ hasAncestorAttribute(el, ["data-object-id", "data-object-selection"], root)
195
+ ) {
196
+ kinds.push("object");
197
+ }
198
+
199
+ // Inline + floating image markers. The <img> tag above covers plain
200
+ // images; these cover floating overlays and PM-schema preview nodes.
201
+ if (
202
+ !kinds.includes("image") &&
203
+ hasAncestorAttribute(
204
+ el,
205
+ ["data-floating-image-id", "data-preview-media-id"],
206
+ root,
207
+ )
208
+ ) {
209
+ kinds.push("image");
210
+ }
211
+
212
+ // Opaque / preserved block — TwOpaqueBlock emits data-block-kind="opaque"
213
+ // (Phase D.5 addition). Value-matched so data-block-kind="code" etc.
214
+ // doesn't classify as opaque.
215
+ if (hasAncestorAttributeValue(el, "data-block-kind", "opaque", root)) {
216
+ kinds.push("opaque-block");
217
+ }
218
+ }
219
+
220
+ // Plain-text baseline so clipboard remains available regardless of
221
+ // target. Placed last so specific kinds dominate when the menu
222
+ // builder dedupes actions by id.
223
+ if (!kinds.includes("plain-text")) kinds.push("plain-text");
224
+
225
+ return kinds;
226
+ }
@@ -10,6 +10,14 @@ export interface TwAlertBannerProps {
10
10
  snapshot: RuntimeRenderSnapshot;
11
11
  preserveOnlyCount: number;
12
12
  workflowBlockedReasons?: WorkflowBlockedCommandReason[];
13
+ /**
14
+ * Phase E.4 — optional handoff to the rail `health` tab. When supplied
15
+ * the banner renders a "Show detail" chip the user can click to reveal
16
+ * full diagnostics in the management rail. Hosts typically wire this
17
+ * to the chrome-host controller's `openRailTab("health")`. Omitting
18
+ * the prop preserves the signal-only banner.
19
+ */
20
+ onShowDetail?: () => void;
13
21
  }
14
22
 
15
23
  /**
@@ -31,10 +39,11 @@ interface BannerRender {
31
39
  icon: ReactNode;
32
40
  message: ReactNode;
33
41
  testid: string;
42
+ onShowDetail?: () => void;
34
43
  }
35
44
 
36
45
  function renderBanner(variant: BannerRender): React.ReactElement {
37
- const { severity, icon, message, testid } = variant;
46
+ const { severity, icon, message, testid, onShowDetail } = variant;
38
47
  const toneClass =
39
48
  severity === "error"
40
49
  ? "bg-[var(--color-semantic-error-soft)] text-[var(--color-semantic-error)]"
@@ -52,7 +61,17 @@ function renderBanner(variant: BannerRender): React.ReactElement {
52
61
  ].join(" ")}
53
62
  >
54
63
  {icon}
55
- <span>{message}</span>
64
+ <span className="flex-1">{message}</span>
65
+ {onShowDetail ? (
66
+ <button
67
+ type="button"
68
+ data-testid="tw-alert-banner__show-detail"
69
+ onClick={onShowDetail}
70
+ className="shrink-0 underline underline-offset-2 hover:no-underline focus-visible:outline-none focus-visible:shadow-[var(--shadow-focus)] rounded-[var(--radius-sm)]"
71
+ >
72
+ Show detail
73
+ </button>
74
+ ) : null}
56
75
  </div>
57
76
  );
58
77
  }
@@ -60,9 +79,21 @@ function renderBanner(variant: BannerRender): React.ReactElement {
60
79
  export function TwAlertBanner(
61
80
  props: TwAlertBannerProps,
62
81
  ): React.ReactElement | null {
63
- const { snapshot, preserveOnlyCount, workflowBlockedReasons = [] } = props;
82
+ const {
83
+ snapshot,
84
+ preserveOnlyCount,
85
+ workflowBlockedReasons = [],
86
+ onShowDetail,
87
+ } = props;
64
88
 
65
- // 1. Fatal runtime error — highest precedence.
89
+ const showDetailProp: Pick<BannerRender, "onShowDetail"> = onShowDetail
90
+ ? { onShowDetail }
91
+ : {};
92
+
93
+ // 1. Fatal runtime error — highest precedence. The rail's health tab
94
+ // is unlikely to have actionable detail for a session-level failure
95
+ // (the runtime may not even be mountable), so suppress `onShowDetail`
96
+ // on this variant — §6.17 fatal-error row is informational only.
66
97
  if (snapshot.fatalError) {
67
98
  return renderBanner({
68
99
  severity: "error",
@@ -85,6 +116,7 @@ export function TwAlertBanner(
85
116
  </>
86
117
  ),
87
118
  testid: "tw-alert-banner__block-export",
119
+ ...showDetailProp,
88
120
  });
89
121
  }
90
122
 
@@ -105,6 +137,7 @@ export function TwAlertBanner(
105
137
  </>
106
138
  ),
107
139
  testid: "tw-alert-banner__workflow-blocked",
140
+ ...showDetailProp,
108
141
  });
109
142
  }
110
143
 
@@ -122,6 +155,7 @@ export function TwAlertBanner(
122
155
  </>
123
156
  ),
124
157
  testid: "tw-alert-banner__preserve-only",
158
+ ...showDetailProp,
125
159
  });
126
160
  }
127
161
 
@@ -0,0 +1,125 @@
1
+ /**
2
+ * TwContextBand — the composition-level context band that carries mode-
3
+ * owned actions (see docs/plans/chrome-composition-audit.md §2.5,
4
+ * DESIGN-EDITOR.md §6.3).
5
+ *
6
+ * Before this phase: role-specific actions (review prev/next, workflow
7
+ * claim/skip, markup-display toggle) lived inline inside `TwToolbar` as a
8
+ * center segment. The audit flags this as `RC-2`: the workspace row
9
+ * looked substantially the same across modes, which broke the
10
+ * "chrome materially changes by mode" principle.
11
+ *
12
+ * This component is the visual frame: a distinct band with its own
13
+ * background, border-top/bottom, mode-aware eyebrow, and padding. Its
14
+ * children are whatever the mode owner decides to render inside:
15
+ * - review mode → review prev/next + accept/reject + markup selector
16
+ * - workflow mode → scope prev/next + claim/skip/complete
17
+ * - edit mode → (filled in by Phase D — paragraph style + compact
18
+ * formatting cluster)
19
+ * - more mode → diagnostics links + command search affordance
20
+ *
21
+ * Phase B.3 moves the band OUT of the toolbar so the workspace row is:
22
+ * [ toolbar left cluster ] · [ context band ] · [ toolbar right cluster ]
23
+ *
24
+ * Perf discipline (CLAUDE.md §Performance Invariants):
25
+ * - Pure presentational component; no DOM reads, no runtime calls.
26
+ * - No state. Re-renders only when props change.
27
+ * - No new geometry reads on the edit path (invariant 7).
28
+ */
29
+
30
+ import React from "react";
31
+
32
+ import type { EditorChromeMode } from "../../api/v3/ui/chrome-composition";
33
+
34
+ export interface TwContextBandProps {
35
+ /**
36
+ * Composition mode per `ChromeComposition.mode`. Drives the eyebrow
37
+ * label and the accent tint.
38
+ */
39
+ mode: EditorChromeMode;
40
+ /**
41
+ * Band contents. Phase B.3 passes `<TwRoleActionRegion />` here for
42
+ * review/workflow modes. Edit mode passes formatting cluster (Phase D).
43
+ */
44
+ children?: React.ReactNode;
45
+ /**
46
+ * Optional custom eyebrow label. Defaults to the uppercased mode name.
47
+ * Hosts that want a scope-specific label (e.g. "REVIEW · §4.2 Claims")
48
+ * pass it through this prop; Phase B.3 wires it from the composition.
49
+ */
50
+ eyebrow?: string;
51
+ className?: string;
52
+ }
53
+
54
+ /**
55
+ * Default eyebrow copy per mode — uppercase per designsystem.md §4.1
56
+ * text.xs tracking 0.1em convention. Kept short so the band's horizontal
57
+ * budget goes to the action content, not the label.
58
+ */
59
+ const DEFAULT_EYEBROW: Record<EditorChromeMode, string> = {
60
+ edit: "Edit",
61
+ review: "Review",
62
+ workflow: "Workflow",
63
+ more: "More",
64
+ };
65
+
66
+ /**
67
+ * Accent tint classes per mode. Review and workflow get a quiet
68
+ * `color.accent.soft` tint so the band visually differs from the
69
+ * neutral toolbar clusters flanking it; edit mode stays neutral per
70
+ * DESIGN-EDITOR.md §3 ("Edit mode receives chrome to normal density,
71
+ * no review emphasis").
72
+ *
73
+ * "more" mode stays neutral (`bg-chrome`) because the More posture
74
+ * opens diagnostics / compatibility drawers — the emphasis belongs on
75
+ * those drawers, not on the band itself. Keeping the band quiet in
76
+ * More mode matches designsystem.md §2.1 principle 2 (quiet
77
+ * precision) and prevents two simultaneous emphasis signals.
78
+ */
79
+ const MODE_TINT_CLASS: Record<EditorChromeMode, string> = {
80
+ edit: "bg-chrome",
81
+ review: "bg-accent-soft",
82
+ workflow: "bg-accent-soft",
83
+ more: "bg-chrome",
84
+ };
85
+
86
+ export function TwContextBand(props: TwContextBandProps): React.JSX.Element {
87
+ const eyebrow = props.eyebrow ?? DEFAULT_EYEBROW[props.mode];
88
+
89
+ const classes = [
90
+ // Layout — flex row, fixed height family matches designsystem.md §4.2
91
+ // space.10 (40px). Horizontal padding ~12px per §6.2 cluster gap.
92
+ "flex items-center gap-3 h-10 px-3",
93
+ // Borders top + bottom so the band has clear start + end boundaries
94
+ // within the workspace row (audit §2.5 requires "clear start and end
95
+ // boundaries").
96
+ "border-y border-subtle",
97
+ // Mode-aware tint. Quiet precision (§2.1 principle 2): low-saturation
98
+ // accent-soft for review/workflow so the mode is legible but not loud.
99
+ MODE_TINT_CLASS[props.mode],
100
+ props.className ?? "",
101
+ ]
102
+ .filter(Boolean)
103
+ .join(" ");
104
+
105
+ return (
106
+ <div
107
+ className={classes}
108
+ role="group"
109
+ aria-label={`${eyebrow} actions`}
110
+ data-testid="context-band"
111
+ data-mode={props.mode}
112
+ data-context-band-owner={props.mode}
113
+ >
114
+ <span
115
+ className="text-xs font-semibold uppercase tracking-[0.1em] text-tertiary select-none"
116
+ data-testid="context-band-eyebrow"
117
+ >
118
+ {eyebrow}
119
+ </span>
120
+ <div className="flex items-center gap-1 flex-1 min-w-0">
121
+ {props.children}
122
+ </div>
123
+ </div>
124
+ );
125
+ }
@@ -0,0 +1,248 @@
1
+ /**
2
+ * TwContextMenuPortal — positioned portal wrapper that mounts a
3
+ * `TwContextMenu` at a given client (x, y) coordinate and handles
4
+ * outside-click + Escape dismissal.
5
+ *
6
+ * This is the composition-level mount point consumed by Phase C.γ.3
7
+ * inside `TwReviewWorkspace`. It owns:
8
+ * - viewport-clamped positioning (so the menu never renders off
9
+ * the edge of the visible area)
10
+ * - outside-click dismissal via a document-level listener
11
+ * - Escape dismissal
12
+ * - focus-return contract (caller supplies `returnFocusTo` — typically
13
+ * the editor surface DOM node)
14
+ *
15
+ * It does NOT own target-kind resolution, menu-entry building, or
16
+ * action dispatch — those live in `resolve-target-kind.ts`,
17
+ * `build-context-menu-entries.ts`, and `editor-action-registry.ts`
18
+ * respectively, so the single-registry invariant (DESIGN-EDITOR.md
19
+ * §6.4) stays intact.
20
+ *
21
+ * Perf discipline (CLAUDE.md §Performance Invariants):
22
+ * - Listeners attach on mount, detach on unmount. No polling,
23
+ * no rAF. Dismissal latency is the native browser event loop.
24
+ * - No DOM geometry reads on the edit path (invariant 7).
25
+ * - Positioning uses `position: fixed` + direct client coordinates
26
+ * — no `getBoundingClientRect` on mount. The only layout read
27
+ * is a post-mount clamp (read-once, inside requestAnimationFrame).
28
+ */
29
+
30
+ import React, { useCallback, useEffect, useLayoutEffect, useRef } from "react";
31
+ import { createPortal } from "react-dom";
32
+
33
+ import {
34
+ type LocalSurfaceKind,
35
+ useLocalSurfaceArbiterState,
36
+ } from "./local-surface-arbiter";
37
+ import {
38
+ TwContextMenu,
39
+ type ContextMenuContext,
40
+ type ContextMenuEntry,
41
+ } from "./tw-context-menu";
42
+
43
+ /**
44
+ * Map a `LocalSurfaceKind` to the `ContextMenuContext` flag it should
45
+ * raise so the menu's dedupe filter (`filterContextMenuEntries`) can
46
+ * suppress the matching group. Used to project the arbiter state
47
+ * (active + pinned slots) into the dedupe context — Chrome Closure
48
+ * Pass · Task 2 (designsystem.md §6.24): the menu **must always
49
+ * open** on right-click; the arbiter's role is filtering content,
50
+ * not preempting visibility.
51
+ */
52
+ function projectArbiterKindToContext(
53
+ kind: LocalSurfaceKind | null,
54
+ ctx: ContextMenuContext,
55
+ ): ContextMenuContext {
56
+ if (!kind) return ctx;
57
+ switch (kind) {
58
+ case "selection-format":
59
+ return { ...ctx, selectionToolbarVisible: true };
60
+ case "suggestion-card":
61
+ return { ...ctx, suggestionCardVisible: true };
62
+ case "comment-preview":
63
+ return { ...ctx, commentCardVisible: true };
64
+ case "scope-card":
65
+ case "blocked-card":
66
+ case "workflow-card":
67
+ case "context-menu":
68
+ // These have no dedicated dedupe flag — the menu's group set
69
+ // does not duplicate scope/blocked/workflow content today.
70
+ return ctx;
71
+ }
72
+ }
73
+
74
+ export interface TwContextMenuPortalProps {
75
+ /**
76
+ * When falsy the portal renders nothing. Typically driven by the
77
+ * host's contextmenu-request state.
78
+ */
79
+ open: boolean;
80
+ /** Client coordinates at which to anchor the menu's top-left corner. */
81
+ clientX: number;
82
+ clientY: number;
83
+ /** Entries to render (already built via `buildContextMenuEntries`). */
84
+ entries: ContextMenuEntry[];
85
+ /** Optional dedupe context passed through to TwContextMenu. */
86
+ context?: Partial<ContextMenuContext>;
87
+ /** Optional platform hint for shortcut rendering. */
88
+ platform?: "mac" | "win";
89
+ /** Invoked on Escape / outside-click / row-select. */
90
+ onDismiss: () => void;
91
+ /**
92
+ * Element to return focus to on dismissal. Typically the PM editor
93
+ * surface. Skipped if null/undefined.
94
+ */
95
+ returnFocusTo?: HTMLElement | null;
96
+ /** Optional aria-label override for the menu container. */
97
+ "aria-label"?: string;
98
+ }
99
+
100
+ /**
101
+ * Minimum viewport margin — matches the selection-toolbar clamp in
102
+ * designsystem.md §6.4 (1.5 rem ≈ 24 px).
103
+ */
104
+ const VIEWPORT_MARGIN = 24;
105
+
106
+ export function TwContextMenuPortal(
107
+ props: TwContextMenuPortalProps,
108
+ ): React.JSX.Element | null {
109
+ const {
110
+ open,
111
+ clientX,
112
+ clientY,
113
+ entries,
114
+ context,
115
+ platform,
116
+ onDismiss,
117
+ returnFocusTo,
118
+ } = props;
119
+
120
+ const menuRef = useRef<HTMLDivElement | null>(null);
121
+
122
+ // Escape + outside-click dismissal.
123
+ useEffect(() => {
124
+ if (!open) return;
125
+ const onKeyDown = (ev: KeyboardEvent) => {
126
+ if (ev.key === "Escape") {
127
+ ev.preventDefault();
128
+ onDismiss();
129
+ }
130
+ };
131
+ const onPointerDown = (ev: MouseEvent) => {
132
+ const menu = menuRef.current;
133
+ if (!menu) return;
134
+ const target = ev.target;
135
+ if (target instanceof Node && menu.contains(target)) return;
136
+ onDismiss();
137
+ };
138
+ document.addEventListener("keydown", onKeyDown);
139
+ document.addEventListener("mousedown", onPointerDown);
140
+ return () => {
141
+ document.removeEventListener("keydown", onKeyDown);
142
+ document.removeEventListener("mousedown", onPointerDown);
143
+ };
144
+ }, [open, onDismiss]);
145
+
146
+ // Focus-return on close. Fires only on the open → closed edge.
147
+ const wasOpenRef = useRef(false);
148
+ useEffect(() => {
149
+ if (wasOpenRef.current && !open && returnFocusTo) {
150
+ returnFocusTo.focus();
151
+ }
152
+ wasOpenRef.current = open;
153
+ }, [open, returnFocusTo]);
154
+
155
+ // B2 fix — positioning is owned ENTIRELY by this layoutEffect; React
156
+ // does not set `left`/`top` via the style prop, so re-renders can't
157
+ // clobber the clamped coordinates. The layoutEffect writes initial
158
+ // placement + clamp on mount and on any prop change that would move
159
+ // the menu. Single layout read per pass; no polling, no rAF loop.
160
+ useLayoutEffect(() => {
161
+ if (!open) return;
162
+ const menu = menuRef.current;
163
+ if (!menu) return;
164
+ // Apply raw coordinates first so getBoundingClientRect measures the
165
+ // menu at its intended anchor; then clamp if that would overflow.
166
+ menu.style.left = `${clientX}px`;
167
+ menu.style.top = `${clientY}px`;
168
+
169
+ const rect = menu.getBoundingClientRect();
170
+ const viewportWidth = window.innerWidth;
171
+ const viewportHeight = window.innerHeight;
172
+
173
+ let left = clientX;
174
+ let top = clientY;
175
+
176
+ if (left + rect.width > viewportWidth - VIEWPORT_MARGIN) {
177
+ left = Math.max(VIEWPORT_MARGIN, viewportWidth - VIEWPORT_MARGIN - rect.width);
178
+ }
179
+ if (top + rect.height > viewportHeight - VIEWPORT_MARGIN) {
180
+ top = Math.max(VIEWPORT_MARGIN, viewportHeight - VIEWPORT_MARGIN - rect.height);
181
+ }
182
+
183
+ if (left !== clientX) menu.style.left = `${left}px`;
184
+ if (top !== clientY) menu.style.top = `${top}px`;
185
+ }, [open, clientX, clientY, entries]);
186
+
187
+ const wrappedDismiss = useCallback(() => {
188
+ onDismiss();
189
+ }, [onDismiss]);
190
+
191
+ // Chrome Closure Pass · Task 2 (designsystem.md §6.24) — the
192
+ // context menu must always open on right-click. The pre-Task-2
193
+ // arbiter gate (`useLocalSurfaceRequest({kind:"context-menu", …})`)
194
+ // preempted the menu whenever a higher-priority local surface was
195
+ // visible, which made right-click silently disappear in normal
196
+ // contextual states. Replace the gate with a content-filter: read
197
+ // arbiter state (active + pinned) and project it into the dedupe
198
+ // `ContextMenuContext` so `filterContextMenuEntries` drops only the
199
+ // groups already represented by visible chrome — the menu itself
200
+ // stays open.
201
+ const arbiterState = useLocalSurfaceArbiterState();
202
+ const dedupeContext: ContextMenuContext = projectArbiterKindToContext(
203
+ arbiterState.activeKind,
204
+ projectArbiterKindToContext(arbiterState.pinnedKind, {
205
+ selectionToolbarVisible: false,
206
+ suggestionCardVisible: false,
207
+ commentCardVisible: false,
208
+ tableToolbarVisible: false,
209
+ ...context,
210
+ }),
211
+ );
212
+
213
+ if (!open) return null;
214
+
215
+ const content = (
216
+ <div
217
+ ref={menuRef}
218
+ // left/top intentionally absent — set imperatively by the
219
+ // layoutEffect above so React re-renders don't clobber the clamp.
220
+ style={{
221
+ position: "fixed",
222
+ zIndex: 1000,
223
+ }}
224
+ onClick={(ev) => {
225
+ // Any click on a menu row should dismiss — the row's onSelect
226
+ // already fires the action before bubbling. Guard against
227
+ // separators / non-item clicks so we don't dismiss on noise
228
+ // inside the menu chrome.
229
+ const target = ev.target as HTMLElement | null;
230
+ if (target?.closest('[role="menuitem"]')) {
231
+ wrappedDismiss();
232
+ }
233
+ }}
234
+ >
235
+ <TwContextMenu
236
+ entries={entries}
237
+ context={dedupeContext}
238
+ {...(platform ? { platform } : {})}
239
+ {...(props["aria-label"] ? { "aria-label": props["aria-label"] } : {})}
240
+ data-testid="tw-context-menu-portal"
241
+ />
242
+ </div>
243
+ );
244
+
245
+ // Portal into document.body so the menu escapes any overflow:hidden
246
+ // ancestors in the editor layout.
247
+ return createPortal(content, document.body);
248
+ }