@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
@@ -6,12 +6,23 @@ import type {
6
6
  SurfaceDrawingAnchor,
7
7
  SurfaceInlineSegment,
8
8
  } from "../../api/public-types.ts";
9
- import type { WordReviewEditorLayoutFacet } from "../../runtime/layout/index.ts";
9
+ import type { WordReviewEditorLayoutFacet } from "../../api/public-types.ts";
10
10
  import { storyTargetKey } from "../../runtime/story-targeting.ts";
11
- import { EMU_PER_PX } from "../../runtime/units.ts";
11
+ import { EMU_PER_PX, TWIPS_PER_PX } from "../../api/public-types.ts";
12
12
  import type { PageOverlayRect } from "../chrome-overlay/tw-page-stack-overlay-layer.tsx";
13
13
 
14
- const FRAME_PX_PER_TWIP_AT_96DPI = 96 / 1440;
14
+ /**
15
+ * Default twip → pixel conversion at 96 dpi / 100% zoom.
16
+ * `pxPerTwip = 96 / 1440 = 1 / TWIPS_PER_PX`.
17
+ *
18
+ * Callers that render the page stack at a non-baseline zoom (e.g.
19
+ * 75% / 125% / 150%) must pass `pxPerTwip` derived from
20
+ * `geometryFacet.getRenderZoom()?.pxPerTwip` so floating-image rects
21
+ * track the rendered page frame. Otherwise the overlay math drifts
22
+ * — a 150%-zoomed page is 1.5× bigger, but floats computed with the
23
+ * 96 dpi baseline land at 66% of the target position.
24
+ */
25
+ const BASELINE_PX_PER_TWIP = 1 / TWIPS_PER_PX;
15
26
 
16
27
  export interface FloatingImagePreviewDescriptor {
17
28
  src: string;
@@ -66,12 +77,22 @@ export function collectFloatingImageOverlayItems(input: {
66
77
  facet: WordReviewEditorLayoutFacet;
67
78
  pageRects: readonly PageOverlayRect[];
68
79
  mediaPreviews?: Record<string, FloatingImagePreviewDescriptor>;
80
+ /**
81
+ * Live `pxPerTwip` from the geometry facet
82
+ * (`geometryFacet.getRenderZoom()?.pxPerTwip`). When absent, defaults
83
+ * to the 96 dpi baseline (100% zoom). Supply this whenever the page
84
+ * stack is rendered at a non-baseline zoom — otherwise floating-image
85
+ * rects drift off the rendered page frame.
86
+ */
87
+ pxPerTwip?: number;
69
88
  }): FloatingImageOverlayItem[] {
70
89
  const { surface, activeStory, facet } = input;
71
90
  if (!surface) {
72
91
  return [];
73
92
  }
74
93
 
94
+ const pxPerTwip = input.pxPerTwip ?? BASELINE_PX_PER_TWIP;
95
+
75
96
  const rectByPageIndex = new Map<number, PageOverlayRect>(
76
97
  input.pageRects.map((rect) => [rect.pageIndex, rect]),
77
98
  );
@@ -88,7 +109,7 @@ export function collectFloatingImageOverlayItems(input: {
88
109
  if (!pageRect) {
89
110
  continue;
90
111
  }
91
- const localRect = resolveFloatingImageLocalRect(page, activeStory, segment);
112
+ const localRect = resolveFloatingImageLocalRect(page, activeStory, segment, pxPerTwip);
92
113
  if (!localRect) {
93
114
  continue;
94
115
  }
@@ -164,6 +185,7 @@ function resolveFloatingImageLocalRect(
164
185
  page: PublicPageNode,
165
186
  activeStory: EditorStoryTarget,
166
187
  segment: Extract<SurfaceInlineSegment, { kind: "image" }>,
188
+ pxPerTwip: number,
167
189
  ): {
168
190
  topPx: number;
169
191
  leftPx: number;
@@ -175,18 +197,18 @@ function resolveFloatingImageLocalRect(
175
197
  return null;
176
198
  }
177
199
 
178
- const widthPx = Math.max(24, Math.round(anchor.extent.widthEmu / EMU_PER_PX));
179
- const heightPx = Math.max(24, Math.round(anchor.extent.heightEmu / EMU_PER_PX));
180
- const horizontalSpace = resolveHorizontalSpace(page, activeStory, anchor);
181
- const verticalSpace = resolveVerticalSpace(page, activeStory, anchor);
200
+ const widthPx = Math.max(24, Math.round(emuToPx(anchor.extent.widthEmu, pxPerTwip)));
201
+ const heightPx = Math.max(24, Math.round(emuToPx(anchor.extent.heightEmu, pxPerTwip)));
202
+ const horizontalSpace = resolveHorizontalSpace(page, activeStory, anchor, pxPerTwip);
203
+ const verticalSpace = resolveVerticalSpace(page, activeStory, anchor, pxPerTwip);
182
204
 
183
205
  if (!horizontalSpace || !verticalSpace) {
184
206
  return null;
185
207
  }
186
208
 
187
209
  return {
188
- topPx: resolveAxisPosition(verticalSpace, heightPx, anchor.positionV, page, "vertical"),
189
- leftPx: resolveAxisPosition(horizontalSpace, widthPx, anchor.positionH, page, "horizontal"),
210
+ topPx: resolveAxisPosition(verticalSpace, heightPx, anchor.positionV, page, "vertical", pxPerTwip),
211
+ leftPx: resolveAxisPosition(horizontalSpace, widthPx, anchor.positionH, page, "horizontal", pxPerTwip),
190
212
  widthPx,
191
213
  heightPx,
192
214
  };
@@ -196,9 +218,10 @@ function resolveHorizontalSpace(
196
218
  page: PublicPageNode,
197
219
  activeStory: EditorStoryTarget,
198
220
  anchor: SurfaceDrawingAnchor,
221
+ pxPerTwip: number,
199
222
  ): { startPx: number; sizePx: number } | null {
200
- const pageWidthPx = twipsToPx(page.layout.pageWidth);
201
- const storyHost = resolveStoryHostSpace(page, activeStory);
223
+ const pageWidthPx = twipsToPx(page.layout.pageWidth, pxPerTwip);
224
+ const storyHost = resolveStoryHostSpace(page, activeStory, pxPerTwip);
202
225
  switch (anchor.positionH?.relativeFrom) {
203
226
  case "page":
204
227
  return { startPx: 0, sizePx: pageWidthPx };
@@ -213,9 +236,10 @@ function resolveVerticalSpace(
213
236
  page: PublicPageNode,
214
237
  activeStory: EditorStoryTarget,
215
238
  anchor: SurfaceDrawingAnchor,
239
+ pxPerTwip: number,
216
240
  ): { startPx: number; sizePx: number } | null {
217
- const pageHeightPx = twipsToPx(page.layout.pageHeight);
218
- const storyHost = resolveStoryHostSpace(page, activeStory);
241
+ const pageHeightPx = twipsToPx(page.layout.pageHeight, pxPerTwip);
242
+ const storyHost = resolveStoryHostSpace(page, activeStory, pxPerTwip);
219
243
  switch (anchor.positionV?.relativeFrom) {
220
244
  case "page":
221
245
  return { startPx: 0, sizePx: pageHeightPx };
@@ -229,31 +253,32 @@ function resolveVerticalSpace(
229
253
  function resolveStoryHostSpace(
230
254
  page: PublicPageNode,
231
255
  activeStory: EditorStoryTarget,
256
+ pxPerTwip: number,
232
257
  ): { topPx: number; leftPx: number; widthPx: number; heightPx: number } | null {
233
258
  switch (activeStory.kind) {
234
259
  case "main":
235
260
  return {
236
- topPx: twipsToPx(page.regions.body.originTwips),
237
- leftPx: twipsToPx(page.layout.marginLeft),
238
- widthPx: twipsToPx(page.regions.body.widthTwips),
239
- heightPx: twipsToPx(page.regions.body.heightTwips),
261
+ topPx: twipsToPx(page.regions.body.originTwips, pxPerTwip),
262
+ leftPx: twipsToPx(page.layout.marginLeft, pxPerTwip),
263
+ widthPx: twipsToPx(page.regions.body.widthTwips, pxPerTwip),
264
+ heightPx: twipsToPx(page.regions.body.heightTwips, pxPerTwip),
240
265
  };
241
266
  case "header":
242
267
  return page.regions.header
243
268
  ? {
244
- topPx: twipsToPx(page.regions.header.originTwips),
245
- leftPx: twipsToPx(page.layout.marginLeft),
246
- widthPx: twipsToPx(page.regions.header.widthTwips),
247
- heightPx: twipsToPx(page.regions.header.heightTwips),
269
+ topPx: twipsToPx(page.regions.header.originTwips, pxPerTwip),
270
+ leftPx: twipsToPx(page.layout.marginLeft, pxPerTwip),
271
+ widthPx: twipsToPx(page.regions.header.widthTwips, pxPerTwip),
272
+ heightPx: twipsToPx(page.regions.header.heightTwips, pxPerTwip),
248
273
  }
249
274
  : null;
250
275
  case "footer":
251
276
  return page.regions.footer
252
277
  ? {
253
- topPx: twipsToPx(page.regions.footer.originTwips),
254
- leftPx: twipsToPx(page.layout.marginLeft),
255
- widthPx: twipsToPx(page.regions.footer.widthTwips),
256
- heightPx: twipsToPx(page.regions.footer.heightTwips),
278
+ topPx: twipsToPx(page.regions.footer.originTwips, pxPerTwip),
279
+ leftPx: twipsToPx(page.layout.marginLeft, pxPerTwip),
280
+ widthPx: twipsToPx(page.regions.footer.widthTwips, pxPerTwip),
281
+ heightPx: twipsToPx(page.regions.footer.heightTwips, pxPerTwip),
257
282
  }
258
283
  : null;
259
284
  default:
@@ -269,6 +294,7 @@ function resolveAxisPosition(
269
294
  | undefined,
270
295
  page: PublicPageNode,
271
296
  orientation: "horizontal" | "vertical",
297
+ pxPerTwip: number,
272
298
  ): number {
273
299
  if (!axis) {
274
300
  return space.startPx;
@@ -277,7 +303,7 @@ function resolveAxisPosition(
277
303
  return alignAxisPosition(space, objectSizePx, axis.align, page, orientation);
278
304
  }
279
305
  if (axis.offset !== undefined) {
280
- return space.startPx + Math.round(axis.offset / EMU_PER_PX);
306
+ return space.startPx + Math.round(emuToPx(axis.offset, pxPerTwip));
281
307
  }
282
308
  return space.startPx;
283
309
  }
@@ -314,6 +340,17 @@ function alignAxisPosition(
314
340
  }
315
341
  }
316
342
 
317
- function twipsToPx(value: number): number {
318
- return value * FRAME_PX_PER_TWIP_AT_96DPI;
343
+ function twipsToPx(value: number, pxPerTwip: number): number {
344
+ return value * pxPerTwip;
345
+ }
346
+
347
+ /**
348
+ * Convert EMU → zoom-aware CSS pixels. The baseline is 96 dpi
349
+ * (`EMU_PER_PX = 9525`); at zoom `z`, a 1-inch object renders as
350
+ * `z * 96 px`. We scale by `pxPerTwip / BASELINE_PX_PER_TWIP`,
351
+ * which equals the zoom factor.
352
+ */
353
+ function emuToPx(emu: number, pxPerTwip: number): number {
354
+ const zoomFactor = pxPerTwip / BASELINE_PX_PER_TWIP;
355
+ return (emu / EMU_PER_PX) * zoomFactor;
319
356
  }
@@ -4,7 +4,10 @@ import type {
4
4
  EditorStoryTarget,
5
5
  RuntimeRenderSnapshot,
6
6
  } from "../../api/public-types.ts";
7
- import type { WordReviewEditorLayoutFacet } from "../../runtime/layout/index.ts";
7
+ import type {
8
+ GeometryFacet,
9
+ WordReviewEditorLayoutFacet,
10
+ } from "../../api/public-types";
8
11
  import { preserveEditorSelectionMouseDown } from "../../ui/headless/preserve-editor-selection.ts";
9
12
  import {
10
13
  measureWidgetsViaBoundingRect,
@@ -19,6 +22,14 @@ import {
19
22
 
20
23
  export interface TwFloatingImageLayerProps {
21
24
  facet: WordReviewEditorLayoutFacet;
25
+ /**
26
+ * Geometry facet supplies live `pxPerTwip` so floating-image rects
27
+ * track the rendered page frame at non-baseline zoom (75% / 125% /
28
+ * 150% / …). When absent, the overlay falls back to the 96 dpi
29
+ * baseline (100% zoom) — safe for tests and for consumers that
30
+ * haven't migrated yet, but visibly drifts in the zoomed UI.
31
+ */
32
+ geometryFacet?: GeometryFacet;
22
33
  scrollRoot: HTMLElement | null;
23
34
  renderFrameRevision: number;
24
35
  visiblePageIndexRange?: VisiblePageIndexRange | null;
@@ -35,6 +46,7 @@ export interface TwFloatingImageLayerProps {
35
46
 
36
47
  export const TwFloatingImageLayer: React.FC<TwFloatingImageLayerProps> = ({
37
48
  facet,
49
+ geometryFacet,
38
50
  scrollRoot,
39
51
  renderFrameRevision,
40
52
  visiblePageIndexRange,
@@ -154,17 +166,28 @@ export const TwFloatingImageLayer: React.FC<TwFloatingImageLayerProps> = ({
154
166
  }, [refreshPageRects, scrollRoot]);
155
167
 
156
168
  const items = React.useMemo(() => {
169
+ const pxPerTwip = geometryFacet?.getRenderZoom()?.pxPerTwip;
157
170
  const allItems = collectFloatingImageOverlayItems({
158
171
  surface: snapshot.surface,
159
172
  activeStory: snapshot.activeStory,
160
173
  facet,
161
174
  pageRects,
162
175
  mediaPreviews,
176
+ pxPerTwip,
163
177
  });
164
178
  return allItems.filter((item) =>
165
179
  plane === "behind" ? item.behindDoc : !item.behindDoc,
166
180
  );
167
- }, [facet, mediaPreviews, pageRects, plane, snapshot.activeStory, snapshot.surface]);
181
+ }, [
182
+ facet,
183
+ geometryFacet,
184
+ mediaPreviews,
185
+ pageRects,
186
+ plane,
187
+ renderFrameRevision,
188
+ snapshot.activeStory,
189
+ snapshot.surface,
190
+ ]);
168
191
 
169
192
  return (
170
193
  <div
@@ -131,6 +131,18 @@ function renderAttachmentImage(
131
131
  return <span key={key}>{alt || attachment?.displayName || ""}</span>;
132
132
  }
133
133
 
134
+ // SEC-UI-02 (2026-04-23): scheme allowlist on comment-renderer hrefs.
135
+ // Defense-in-depth against the sanitizer. Any `target` that does not match
136
+ // one of the safe schemes renders as a plain <span>, never as a clickable
137
+ // <a href>. Prevents `javascript:alert(1)`, `data:text/html,<script>...`,
138
+ // and similar vectors from leaking through the renderer even if the
139
+ // sanitizer regresses.
140
+ const SAFE_LINK_SCHEME_RE = /^(?:https?|mailto):/i;
141
+
142
+ function isSafeHref(target: string): boolean {
143
+ return SAFE_LINK_SCHEME_RE.test(target);
144
+ }
145
+
134
146
  function renderLinkOrMention(
135
147
  key: number,
136
148
  label: string,
@@ -147,6 +159,9 @@ function renderLinkOrMention(
147
159
  </span>
148
160
  );
149
161
  }
162
+ if (!isSafeHref(target)) {
163
+ return <span key={key}>{label}</span>;
164
+ }
150
165
  return (
151
166
  <a key={key} href={target} target="_blank" rel="noopener noreferrer">
152
167
  {label}
@@ -11,20 +11,25 @@ import type {
11
11
  RuntimeContextAnalyticsSnapshot,
12
12
  TrackedChangesSnapshot,
13
13
  TrackedChangeEntrySnapshot,
14
+ WorkflowBlockedCommandReason,
14
15
  } from "../../api/public-types";
15
- import type { ScopeRailSegment } from "../../runtime/layout";
16
+ import type { ScopeRailSegment } from "../../api/public-types";
16
17
  import type { MarkupDisplay } from "../../ui/headless/comment-decoration-model";
17
18
  import { TwCommentSidebar } from "./tw-comment-sidebar";
18
19
  import { TwRevisionSidebar } from "./tw-revision-sidebar";
19
20
  import { TwWorkflowTab } from "./tw-workflow-tab";
21
+ import { TwHealthPanel } from "./tw-health-panel";
20
22
  import {
21
23
  TwReviewRailFooter,
22
24
  type TwReviewRailFooterProps,
23
25
  } from "./tw-review-rail-footer";
24
26
 
25
27
  /**
26
- * Review rail with three tabs (Comments / Changes / Workflow) that matches
27
- * the editorial reference mock while preserving the shipped R3a surface.
28
+ * Review rail with up to four tabs (Workflow / Comments / Changes / Health).
29
+ * Health is opt-in via `showHealthTab` Phase E of the chrome-composition
30
+ * refactor routes diagnostics detail into the rail so the toolbar can shrink
31
+ * to a signal-only chip. When `showHealthTab` is absent the rail retains the
32
+ * shipped three-tab layout for back-compat.
28
33
  *
29
34
  * The Workflow tab reads `scopeRailSegments` from the runtime facet — that
30
35
  * stays the default path. For hosts that need to override the Workflow card
@@ -40,7 +45,7 @@ import {
40
45
  * - `railFooter?: TwReviewRailFooterProps` mounts the SEARCH / HELP footer.
41
46
  */
42
47
 
43
- export type ReviewRailTab = "comments" | "changes" | "workflow";
48
+ export type ReviewRailTab = "comments" | "changes" | "workflow" | "health";
44
49
 
45
50
  export interface TwReviewRailProps {
46
51
  activeTab: ReviewRailTab;
@@ -81,6 +86,23 @@ export interface TwReviewRailProps {
81
86
  intelligenceHeader?: boolean;
82
87
  /** Utility footer with SEARCH / HELP links. Hides when unset. */
83
88
  railFooter?: TwReviewRailFooterProps;
89
+ /**
90
+ * Phase E — mount the fourth "Health" tab with the diagnostics panel
91
+ * inside. Hosts typically derive this from
92
+ * `composition.rail.visibleTabs.has("health")`. Omit (default `false`)
93
+ * to preserve the shipped three-tab layout.
94
+ */
95
+ showHealthTab?: boolean;
96
+ /** Count rendered as the Health tab's badge when > 0. */
97
+ healthIssueCount?: number;
98
+ /**
99
+ * Severity tier of the diagnostics signal — drives the Health tab
100
+ * badge tone via `designsystem.md §3.8` tokens (info / warning /
101
+ * error / success). Defaults to `"warning"` when omitted.
102
+ */
103
+ healthSeverity?: "info" | "warning" | "blocked";
104
+ /** Forwarded to the Health tab's `TwHealthPanel` to surface blocked reasons. */
105
+ workflowBlockedReasons?: WorkflowBlockedCommandReason[];
84
106
 
85
107
  onActiveTabChange: (tab: ReviewRailTab) => void;
86
108
  onOpenComment?: (thread: CommentSidebarThreadSnapshot) => void;
@@ -177,6 +199,7 @@ export function TwReviewRail(props: TwReviewRailProps) {
177
199
  >
178
200
  <Tabs.Trigger
179
201
  value="workflow"
202
+ data-rail-tab="workflow"
180
203
  className={editorial ? UNDERLINE_TRIGGER_CLASS : PILL_TRIGGER_CLASS}
181
204
  >
182
205
  {editorial ? "Workflow" : "Workflow "}
@@ -188,6 +211,7 @@ export function TwReviewRail(props: TwReviewRailProps) {
188
211
  </Tabs.Trigger>
189
212
  <Tabs.Trigger
190
213
  value="comments"
214
+ data-rail-tab="comments"
191
215
  className={editorial ? UNDERLINE_TRIGGER_CLASS : PILL_TRIGGER_CLASS}
192
216
  >
193
217
  {editorial ? "Comments" : "Comments "}
@@ -199,6 +223,7 @@ export function TwReviewRail(props: TwReviewRailProps) {
199
223
  </Tabs.Trigger>
200
224
  <Tabs.Trigger
201
225
  value="changes"
226
+ data-rail-tab="changes"
202
227
  className={editorial ? UNDERLINE_TRIGGER_CLASS : PILL_TRIGGER_CLASS}
203
228
  >
204
229
  {editorial ? "Changes" : "Changes "}
@@ -208,6 +233,25 @@ export function TwReviewRail(props: TwReviewRailProps) {
208
233
  </span>
209
234
  ) : null}
210
235
  </Tabs.Trigger>
236
+ {props.showHealthTab ? (
237
+ <Tabs.Trigger
238
+ value="health"
239
+ data-rail-tab="health"
240
+ className={editorial ? UNDERLINE_TRIGGER_CLASS : PILL_TRIGGER_CLASS}
241
+ >
242
+ {editorial ? "Health" : "Health "}
243
+ {props.healthIssueCount !== undefined &&
244
+ props.healthIssueCount > 0 ? (
245
+ <span
246
+ className="ml-1 inline-flex min-w-[14px] items-center justify-center rounded-full px-1.5 py-px text-[10px] font-medium"
247
+ data-severity={props.healthSeverity ?? "warning"}
248
+ style={resolveHealthBadgeStyle(props.healthSeverity)}
249
+ >
250
+ {props.healthIssueCount}
251
+ </span>
252
+ ) : null}
253
+ </Tabs.Trigger>
254
+ ) : null}
211
255
  </Tabs.List>
212
256
 
213
257
  <ScrollArea.Root className="flex-1 min-h-0">
@@ -247,6 +291,18 @@ export function TwReviewRail(props: TwReviewRailProps) {
247
291
  onRejectAllChanges={props.onRejectAllChanges}
248
292
  />
249
293
  </Tabs.Content>
294
+
295
+ {props.showHealthTab ? (
296
+ <Tabs.Content value="health" className="p-3 outline-none">
297
+ <TwHealthPanel
298
+ compatibility={props.compatibility}
299
+ warnings={props.warnings}
300
+ {...(props.workflowBlockedReasons
301
+ ? { blockedReasons: props.workflowBlockedReasons }
302
+ : {})}
303
+ />
304
+ </Tabs.Content>
305
+ ) : null}
250
306
  </ScrollArea.Viewport>
251
307
  <ScrollArea.Scrollbar
252
308
  orientation="vertical"
@@ -262,6 +318,35 @@ export function TwReviewRail(props: TwReviewRailProps) {
262
318
  );
263
319
  }
264
320
 
321
+ /**
322
+ * Health tab badge tones per `designsystem.md §3.8` severity ladder.
323
+ * - `info` → semantic.info soft/solid
324
+ * - `warning` → semantic.warning soft/solid (default)
325
+ * - `blocked` → semantic.error soft/solid
326
+ */
327
+ function resolveHealthBadgeStyle(
328
+ severity: "info" | "warning" | "blocked" | undefined,
329
+ ): React.CSSProperties {
330
+ switch (severity) {
331
+ case "info":
332
+ return {
333
+ backgroundColor: "var(--color-semantic-info-soft)",
334
+ color: "var(--color-semantic-info)",
335
+ };
336
+ case "blocked":
337
+ return {
338
+ backgroundColor: "var(--color-semantic-error-soft)",
339
+ color: "var(--color-semantic-error)",
340
+ };
341
+ case "warning":
342
+ default:
343
+ return {
344
+ backgroundColor: "var(--color-semantic-warning-soft)",
345
+ color: "var(--color-semantic-warning)",
346
+ };
347
+ }
348
+ }
349
+
265
350
  function resolveHeaderTitle(props: TwReviewRailProps): string {
266
351
  if (props.workflowScopesTitle) {
267
352
  return props.workflowScopesTitle;
@@ -275,5 +360,8 @@ function resolveHeaderTitle(props: TwReviewRailProps): string {
275
360
  if (props.activeTab === "changes") {
276
361
  return "Tracked Changes";
277
362
  }
363
+ if (props.activeTab === "health") {
364
+ return "Document Health";
365
+ }
278
366
  return "Review";
279
367
  }
@@ -9,7 +9,7 @@
9
9
  */
10
10
 
11
11
  import React from "react";
12
- import type { ScopeRailSegment, ScopeRailPosture } from "../../runtime/layout";
12
+ import type { ScopeRailSegment, ScopeRailPosture } from "../../api/public-types";
13
13
 
14
14
  export interface TwWorkflowTabProps {
15
15
  segments: readonly ScopeRailSegment[];
@@ -0,0 +1,210 @@
1
+ import type { CSSProperties } from "react";
2
+
3
+ import type {
4
+ DocumentNavigationSnapshot,
5
+ RuntimeRenderSnapshot,
6
+ } from "../../api/public-types";
7
+ import { DEFAULT_PAGE_ESTIMATE_PX_PER_TWIP } from "../../runtime/page-layout-estimation.ts";
8
+ import { computeLineMarkersIfEnabled } from "../page-chrome-model.ts";
9
+
10
+ export interface PageChromeModel {
11
+ lineNumberingEnabled: boolean;
12
+ gutterWidthPx: number;
13
+ lineMarkers: Array<{ id: string; label: string; topPx: number }>;
14
+ showPageBorder: boolean;
15
+ pageBorderDisplay: string;
16
+ pageBorderStyle: CSSProperties | undefined;
17
+ documentGridType: string;
18
+ documentGridStyle: CSSProperties | undefined;
19
+ }
20
+
21
+ export const EMPTY_PAGE_CHROME_MODEL: PageChromeModel = {
22
+ lineNumberingEnabled: false,
23
+ gutterWidthPx: 0,
24
+ lineMarkers: [],
25
+ showPageBorder: false,
26
+ pageBorderDisplay: "none",
27
+ pageBorderStyle: undefined,
28
+ documentGridType: "none",
29
+ documentGridStyle: undefined,
30
+ };
31
+
32
+ export function buildPageChromeModel(
33
+ surface: RuntimeRenderSnapshot["surface"] | undefined,
34
+ pageLayout: RuntimeRenderSnapshot["pageLayout"] | undefined,
35
+ navigation: DocumentNavigationSnapshot | undefined,
36
+ activeStory: RuntimeRenderSnapshot["activeStory"],
37
+ ): PageChromeModel {
38
+ if (!surface || !pageLayout || !navigation || activeStory.kind !== "main") {
39
+ return EMPTY_PAGE_CHROME_MODEL;
40
+ }
41
+
42
+ const lineMarkers = computeLineMarkersIfEnabled({
43
+ pageLayout,
44
+ surfaceBlocks: surface.blocks,
45
+ pages: navigation.pages,
46
+ });
47
+ const lineNumberingEnabled =
48
+ Boolean(pageLayout.lineNumbering) && lineMarkers.length > 0;
49
+ const distance = pageLayout.lineNumbering?.distance ?? 0;
50
+ const gutterWidthPx = lineNumberingEnabled
51
+ ? Math.max(40, Math.min(88, 24 + Math.round(distance * DEFAULT_PAGE_ESTIMATE_PX_PER_TWIP)))
52
+ : 0;
53
+ const showPageBorder = shouldRenderPageBorder(
54
+ pageLayout,
55
+ navigation.pages,
56
+ navigation.activePageIndex,
57
+ );
58
+
59
+ return {
60
+ lineNumberingEnabled,
61
+ gutterWidthPx,
62
+ lineMarkers,
63
+ showPageBorder,
64
+ pageBorderDisplay: pageLayout.pageBorders?.display ?? "none",
65
+ pageBorderStyle: showPageBorder ? buildPageBorderStyle(pageLayout) : undefined,
66
+ documentGridType: pageLayout.documentGrid?.type ?? "none",
67
+ documentGridStyle: buildDocumentGridStyle(pageLayout.documentGrid),
68
+ };
69
+ }
70
+
71
+ function shouldRenderPageBorder(
72
+ pageLayout: RuntimeRenderSnapshot["pageLayout"],
73
+ pages: ReadonlyArray<DocumentNavigationSnapshot["pages"][number]>,
74
+ activePageIndex: number,
75
+ ): boolean {
76
+ const display = pageLayout?.pageBorders?.display ?? "allPages";
77
+ const activePage = pages[activePageIndex];
78
+ if (!pageLayout?.pageBorders || !activePage) {
79
+ return false;
80
+ }
81
+
82
+ switch (display) {
83
+ case "firstPage":
84
+ return activePage.pageInSection === 0;
85
+ case "notFirstPage":
86
+ return activePage.pageInSection > 0;
87
+ default:
88
+ return true;
89
+ }
90
+ }
91
+
92
+ function buildPageBorderStyle(
93
+ pageLayout: NonNullable<RuntimeRenderSnapshot["pageLayout"]>,
94
+ ): CSSProperties | undefined {
95
+ const pageBorders = pageLayout.pageBorders;
96
+ if (!pageBorders) {
97
+ return undefined;
98
+ }
99
+
100
+ const leftInset = createInsetValue(
101
+ pageBorders.left?.space,
102
+ pageBorders.offsetFrom === "text"
103
+ ? (pageLayout.marginLeft / Math.max(1, pageLayout.pageWidth)) * 100
104
+ : 1.25,
105
+ );
106
+ const rightInset = createInsetValue(
107
+ pageBorders.right?.space,
108
+ pageBorders.offsetFrom === "text"
109
+ ? (pageLayout.marginRight / Math.max(1, pageLayout.pageWidth)) * 100
110
+ : 1.25,
111
+ );
112
+ const topInset = createInsetValue(
113
+ pageBorders.top?.space,
114
+ pageBorders.offsetFrom === "text"
115
+ ? (pageLayout.marginTop / Math.max(1, pageLayout.pageHeight)) * 100
116
+ : 1.5,
117
+ );
118
+ const bottomInset = createInsetValue(
119
+ pageBorders.bottom?.space,
120
+ pageBorders.offsetFrom === "text"
121
+ ? (pageLayout.marginBottom / Math.max(1, pageLayout.pageHeight)) * 100
122
+ : 1.5,
123
+ );
124
+
125
+ return {
126
+ top: topInset,
127
+ right: rightInset,
128
+ bottom: bottomInset,
129
+ left: leftInset,
130
+ borderTop: toBorderCss(pageBorders.top),
131
+ borderRight: toBorderCss(pageBorders.right),
132
+ borderBottom: toBorderCss(pageBorders.bottom),
133
+ borderLeft: toBorderCss(pageBorders.left),
134
+ boxSizing: "border-box",
135
+ mixBlendMode: pageBorders.zOrder === "back" ? "multiply" : undefined,
136
+ };
137
+ }
138
+
139
+ function buildDocumentGridStyle(
140
+ documentGrid: NonNullable<RuntimeRenderSnapshot["pageLayout"]>["documentGrid"] | undefined,
141
+ ): CSSProperties | undefined {
142
+ if (!documentGrid || !documentGrid.type || documentGrid.type === "default") {
143
+ return undefined;
144
+ }
145
+
146
+ const linePitchPx = Math.max(
147
+ 18,
148
+ Math.round((documentGrid.linePitch ?? 360) * DEFAULT_PAGE_ESTIMATE_PX_PER_TWIP),
149
+ );
150
+ const charSpacePx = Math.max(
151
+ 12,
152
+ Math.round((documentGrid.charSpace ?? 204) * DEFAULT_PAGE_ESTIMATE_PX_PER_TWIP),
153
+ );
154
+ const gridColor = "rgba(15, 23, 42, 0.06)";
155
+ const backgrounds: string[] = [];
156
+
157
+ if (
158
+ documentGrid.type === "lines" ||
159
+ documentGrid.type === "linesAndChars" ||
160
+ documentGrid.type === "snapToChars"
161
+ ) {
162
+ backgrounds.push(
163
+ `repeating-linear-gradient(to bottom, ${gridColor} 0, ${gridColor} 1px, transparent 1px, transparent ${linePitchPx}px)`,
164
+ );
165
+ }
166
+ if (
167
+ documentGrid.type === "linesAndChars" ||
168
+ documentGrid.type === "snapToChars"
169
+ ) {
170
+ backgrounds.push(
171
+ `repeating-linear-gradient(to right, rgba(15, 23, 42, 0.04) 0, rgba(15, 23, 42, 0.04) 1px, transparent 1px, transparent ${charSpacePx}px)`,
172
+ );
173
+ }
174
+
175
+ if (backgrounds.length === 0) {
176
+ return undefined;
177
+ }
178
+
179
+ return {
180
+ backgroundImage: backgrounds.join(", "),
181
+ backgroundOrigin: "content-box",
182
+ };
183
+ }
184
+
185
+ function createInsetValue(spaceTwips: number | undefined, percent: number): string {
186
+ const spacingPx = Math.max(0, Math.round((spaceTwips ?? 0) * DEFAULT_PAGE_ESTIMATE_PX_PER_TWIP));
187
+ return `calc(${percent.toFixed(2)}% + ${spacingPx}px)`;
188
+ }
189
+
190
+ function toBorderCss(
191
+ border:
192
+ | NonNullable<NonNullable<RuntimeRenderSnapshot["pageLayout"]>["pageBorders"]>["top"]
193
+ | undefined,
194
+ ): string | undefined {
195
+ if (!border || border.value === "none" || border.value === "nil") {
196
+ return undefined;
197
+ }
198
+
199
+ const width = border.size ? `${Math.max(1, Math.round(border.size / 8))}px` : "1px";
200
+ const style =
201
+ border.value === "double"
202
+ ? "double"
203
+ : border.value === "dotted"
204
+ ? "dotted"
205
+ : border.value === "dashed" || border.value === "dashSmallGap"
206
+ ? "dashed"
207
+ : "solid";
208
+ const color = border.color && border.color !== "auto" ? `#${border.color}` : "rgba(31, 31, 31, 0.28)";
209
+ return `${width} ${style} ${color}`;
210
+ }