@flamingo-stack/openframe-frontend-core 0.0.490 → 0.0.492-1506.3956.1

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 (353) hide show
  1. package/dist/chat-protocol/decode.d.ts +58 -0
  2. package/dist/chat-protocol/decode.d.ts.map +1 -0
  3. package/dist/chat-protocol/encode.d.ts +44 -0
  4. package/dist/chat-protocol/encode.d.ts.map +1 -0
  5. package/dist/chat-protocol/env-flag.d.ts +35 -0
  6. package/dist/chat-protocol/env-flag.d.ts.map +1 -0
  7. package/dist/chat-protocol/events.d.ts +207 -0
  8. package/dist/chat-protocol/events.d.ts.map +1 -0
  9. package/dist/chat-protocol/frames.d.ts +213 -0
  10. package/dist/chat-protocol/frames.d.ts.map +1 -0
  11. package/dist/chat-protocol/index.cjs +500 -0
  12. package/dist/chat-protocol/index.cjs.map +1 -0
  13. package/dist/chat-protocol/index.d.ts +18 -0
  14. package/dist/chat-protocol/index.d.ts.map +1 -0
  15. package/dist/chat-protocol/index.js +484 -0
  16. package/dist/chat-protocol/index.js.map +1 -0
  17. package/dist/chat-protocol/ip-normalize.d.ts +44 -0
  18. package/dist/chat-protocol/ip-normalize.d.ts.map +1 -0
  19. package/dist/chat-protocol/nats-decoder.d.ts +26 -0
  20. package/dist/chat-protocol/nats-decoder.d.ts.map +1 -0
  21. package/dist/{chunk-WLM3F3WU.js → chunk-34TDA22K.js} +3 -3
  22. package/dist/{chunk-EGXCWZYP.js → chunk-3LKB722U.js} +3 -3
  23. package/dist/{chunk-RW2LPL72.js → chunk-6WTCJI5R.js} +2 -2
  24. package/dist/{chunk-VUXNXWOQ.cjs → chunk-7N7YZCHR.cjs} +7503 -5167
  25. package/dist/chunk-7N7YZCHR.cjs.map +1 -0
  26. package/dist/{chunk-AW63ET2A.cjs → chunk-A6FPYAUD.cjs} +19 -19
  27. package/dist/{chunk-AW63ET2A.cjs.map → chunk-A6FPYAUD.cjs.map} +1 -1
  28. package/dist/{chunk-BZMOEWWS.js → chunk-BEEPU44B.js} +12 -12
  29. package/dist/chunk-BEEPU44B.js.map +1 -0
  30. package/dist/{chunk-EJQUYZXF.cjs → chunk-C65NCIMV.cjs} +12 -12
  31. package/dist/{chunk-EJQUYZXF.cjs.map → chunk-C65NCIMV.cjs.map} +1 -1
  32. package/dist/{chunk-VL2YMDPH.js → chunk-J3VBVR4W.js} +6 -6
  33. package/dist/{chunk-AIRNG27G.js → chunk-JHEIOXXZ.js} +8 -7
  34. package/dist/chunk-JHEIOXXZ.js.map +1 -0
  35. package/dist/{chunk-D5LQMRPU.cjs → chunk-JSEJ5MH5.cjs} +5 -5
  36. package/dist/{chunk-D5LQMRPU.cjs.map → chunk-JSEJ5MH5.cjs.map} +1 -1
  37. package/dist/{chunk-FY5XAW3H.cjs → chunk-KX3K2EH4.cjs} +18 -18
  38. package/dist/{chunk-FY5XAW3H.cjs.map → chunk-KX3K2EH4.cjs.map} +1 -1
  39. package/dist/{chunk-3IEVA5AO.cjs → chunk-M6FEGPQJ.cjs} +93 -93
  40. package/dist/{chunk-3IEVA5AO.cjs.map → chunk-M6FEGPQJ.cjs.map} +1 -1
  41. package/dist/{chunk-7I42V2OD.cjs → chunk-MEIGDK7M.cjs} +74 -74
  42. package/dist/chunk-MEIGDK7M.cjs.map +1 -0
  43. package/dist/{chunk-OQIMH2WQ.js → chunk-MEMRQTOI.js} +12263 -9927
  44. package/dist/chunk-MEMRQTOI.js.map +1 -0
  45. package/dist/{chunk-GUTYUMGV.cjs → chunk-MPUQFRJG.cjs} +12 -12
  46. package/dist/{chunk-GUTYUMGV.cjs.map → chunk-MPUQFRJG.cjs.map} +1 -1
  47. package/dist/{chunk-UZZT65PQ.cjs → chunk-MSL5NE3N.cjs} +40 -40
  48. package/dist/{chunk-UZZT65PQ.cjs.map → chunk-MSL5NE3N.cjs.map} +1 -1
  49. package/dist/{chunk-6CJ2K7BD.js → chunk-MZRIRQYV.js} +3 -3
  50. package/dist/{chunk-NGEFYPY7.js → chunk-NCAN6VUF.js} +4 -4
  51. package/dist/{chunk-6QK3ONDD.js → chunk-OL3MJKUI.js} +2 -2
  52. package/dist/{chunk-MPYDX5WX.js → chunk-PLB6CQSO.js} +4 -4
  53. package/dist/{chunk-6CFFISUC.js → chunk-PTXZB2KW.js} +3 -3
  54. package/dist/{chunk-GMPYR3MX.cjs → chunk-QDGB2DKY.cjs} +33 -33
  55. package/dist/{chunk-GMPYR3MX.cjs.map → chunk-QDGB2DKY.cjs.map} +1 -1
  56. package/dist/{chunk-2MKUHJ3O.cjs → chunk-QG2EBIH5.cjs} +40 -40
  57. package/dist/{chunk-2MKUHJ3O.cjs.map → chunk-QG2EBIH5.cjs.map} +1 -1
  58. package/dist/{chunk-GZK7FWJJ.js → chunk-QKWOBV55.js} +12 -12
  59. package/dist/chunk-QKWOBV55.js.map +1 -0
  60. package/dist/{chunk-STYA77PH.cjs → chunk-SGL6URIJ.cjs} +12 -12
  61. package/dist/chunk-SGL6URIJ.cjs.map +1 -0
  62. package/dist/{chunk-KP5KRRD6.cjs → chunk-THC3K262.cjs} +3 -3
  63. package/dist/{chunk-KP5KRRD6.cjs.map → chunk-THC3K262.cjs.map} +1 -1
  64. package/dist/{chunk-BIKOJ4FH.cjs → chunk-TTYJAZ7P.cjs} +58 -32
  65. package/dist/chunk-TTYJAZ7P.cjs.map +1 -0
  66. package/dist/{chunk-IEZ3YBAP.cjs → chunk-W56JYDSC.cjs} +34 -33
  67. package/dist/chunk-W56JYDSC.cjs.map +1 -0
  68. package/dist/{chunk-PB3BTZDU.js → chunk-WEUNPE3S.js} +139 -113
  69. package/dist/chunk-WEUNPE3S.js.map +1 -0
  70. package/dist/{chunk-VDZO6IDR.js → chunk-XOFE2RT4.js} +5 -5
  71. package/dist/components/case-studies/index.cjs +10 -10
  72. package/dist/components/case-studies/index.js +4 -4
  73. package/dist/components/chat/chat-message-enhanced.d.ts.map +1 -1
  74. package/dist/components/chat/chat-message-list.d.ts.map +1 -1
  75. package/dist/components/chat/hooks/index.d.ts +0 -1
  76. package/dist/components/chat/hooks/index.d.ts.map +1 -1
  77. package/dist/components/chat/hooks/use-chat.d.ts.map +1 -1
  78. package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts +15 -48
  79. package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts.map +1 -1
  80. package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts +10 -57
  81. package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts.map +1 -1
  82. package/dist/components/chat/index.cjs +12 -4
  83. package/dist/components/chat/index.cjs.map +1 -1
  84. package/dist/components/chat/index.d.ts +1 -0
  85. package/dist/components/chat/index.d.ts.map +1 -1
  86. package/dist/components/chat/index.js +23 -15
  87. package/dist/components/chat/stream/chat-dialog-store.d.ts +184 -0
  88. package/dist/components/chat/stream/chat-dialog-store.d.ts.map +1 -0
  89. package/dist/components/chat/stream/chat-stream-reducer.d.ts +367 -0
  90. package/dist/components/chat/stream/chat-stream-reducer.d.ts.map +1 -0
  91. package/dist/components/chat/stream/delta-batcher.d.ts +51 -0
  92. package/dist/components/chat/stream/delta-batcher.d.ts.map +1 -0
  93. package/dist/components/chat/stream/index.d.ts +17 -0
  94. package/dist/components/chat/stream/index.d.ts.map +1 -0
  95. package/dist/components/chat/stream/message-mutations.d.ts +77 -0
  96. package/dist/components/chat/stream/message-mutations.d.ts.map +1 -0
  97. package/dist/components/chat/stream/use-chat-stream-reducer.d.ts +22 -0
  98. package/dist/components/chat/stream/use-chat-stream-reducer.d.ts.map +1 -0
  99. package/dist/components/chat/types/api.types.d.ts +1 -81
  100. package/dist/components/chat/types/api.types.d.ts.map +1 -1
  101. package/dist/components/chat/types/processing.types.d.ts +2 -93
  102. package/dist/components/chat/types/processing.types.d.ts.map +1 -1
  103. package/dist/components/chat/utils/extract-incomplete-message-state.d.ts +34 -6
  104. package/dist/components/chat/utils/extract-incomplete-message-state.d.ts.map +1 -1
  105. package/dist/components/chat/utils/index.d.ts +2 -3
  106. package/dist/components/chat/utils/index.d.ts.map +1 -1
  107. package/dist/components/chat/utils/process-historical-messages.d.ts +59 -11
  108. package/dist/components/chat/utils/process-historical-messages.d.ts.map +1 -1
  109. package/dist/components/contact/index.cjs +5 -5
  110. package/dist/components/contact/index.js +4 -4
  111. package/dist/components/docs/index.cjs +7 -7
  112. package/dist/components/docs/index.js +6 -6
  113. package/dist/components/embeds/index.cjs +5 -5
  114. package/dist/components/embeds/index.js +4 -4
  115. package/dist/components/faq/index.cjs +6 -6
  116. package/dist/components/faq/index.js +5 -5
  117. package/dist/components/features/index.cjs +4 -4
  118. package/dist/components/features/index.js +3 -3
  119. package/dist/components/features/select-button.d.ts.map +1 -1
  120. package/dist/components/help-center-pages/index.cjs +23 -23
  121. package/dist/components/help-center-pages/index.js +14 -14
  122. package/dist/components/index.cjs +203 -161
  123. package/dist/components/index.cjs.map +1 -1
  124. package/dist/components/index.js +66 -24
  125. package/dist/components/index.js.map +1 -1
  126. package/dist/components/layout/page-layout.d.ts +7 -1
  127. package/dist/components/layout/page-layout.d.ts.map +1 -1
  128. package/dist/components/layout/title-block.d.ts +23 -1
  129. package/dist/components/layout/title-block.d.ts.map +1 -1
  130. package/dist/components/navigation/index.cjs +4 -4
  131. package/dist/components/navigation/index.js +3 -3
  132. package/dist/components/onboarding-guides/index.cjs +8 -8
  133. package/dist/components/onboarding-guides/index.js +7 -7
  134. package/dist/components/onboarding-guides/onboarding-guide-detail-view.d.ts.map +1 -1
  135. package/dist/components/related-content/index.cjs +6 -6
  136. package/dist/components/related-content/index.js +5 -5
  137. package/dist/components/shared/product-release/release-detail-page.d.ts.map +1 -1
  138. package/dist/components/tickets/index.cjs +7 -7
  139. package/dist/components/tickets/index.js +6 -6
  140. package/dist/components/ui/autocomplete.d.ts.map +1 -1
  141. package/dist/components/ui/checkbox-block.d.ts.map +1 -1
  142. package/dist/components/ui/date-picker.d.ts.map +1 -1
  143. package/dist/components/ui/field-wrapper.d.ts +10 -1
  144. package/dist/components/ui/field-wrapper.d.ts.map +1 -1
  145. package/dist/components/ui/file-manager/index.cjs +50 -50
  146. package/dist/components/ui/file-manager/index.js +1 -1
  147. package/dist/components/ui/floating-tooltip.d.ts +8 -1
  148. package/dist/components/ui/floating-tooltip.d.ts.map +1 -1
  149. package/dist/components/ui/index.cjs +46 -4
  150. package/dist/components/ui/index.cjs.map +1 -1
  151. package/dist/components/ui/index.d.ts +1 -2
  152. package/dist/components/ui/index.d.ts.map +1 -1
  153. package/dist/components/ui/index.js +57 -15
  154. package/dist/components/ui/input-trigger.d.ts.map +1 -1
  155. package/dist/components/ui/input.d.ts.map +1 -1
  156. package/dist/components/ui/markdown/base-components.d.ts +51 -0
  157. package/dist/components/ui/markdown/base-components.d.ts.map +1 -0
  158. package/dist/components/ui/markdown/engine.d.ts +99 -0
  159. package/dist/components/ui/markdown/engine.d.ts.map +1 -0
  160. package/dist/components/ui/markdown/heading-ids.d.ts +65 -0
  161. package/dist/components/ui/markdown/heading-ids.d.ts.map +1 -0
  162. package/dist/components/ui/markdown/index.d.ts +18 -0
  163. package/dist/components/ui/markdown/index.d.ts.map +1 -0
  164. package/dist/components/ui/markdown/mermaid-diagram.d.ts +79 -0
  165. package/dist/components/ui/markdown/mermaid-diagram.d.ts.map +1 -0
  166. package/dist/components/ui/markdown/rich/embed-overrides.d.ts +14 -0
  167. package/dist/components/ui/markdown/rich/embed-overrides.d.ts.map +1 -0
  168. package/dist/components/ui/markdown/rich/rich-markdown-renderer.d.ts +40 -0
  169. package/dist/components/ui/markdown/rich/rich-markdown-renderer.d.ts.map +1 -0
  170. package/dist/components/ui/markdown/rich/shortcodes.d.ts +8 -0
  171. package/dist/components/ui/markdown/rich/shortcodes.d.ts.map +1 -0
  172. package/dist/components/ui/markdown/sanitize.d.ts +153 -0
  173. package/dist/components/ui/markdown/sanitize.d.ts.map +1 -0
  174. package/dist/components/ui/markdown/simple-markdown-renderer.d.ts +23 -0
  175. package/dist/components/ui/markdown/simple-markdown-renderer.d.ts.map +1 -0
  176. package/dist/components/ui/markdown/streaming.d.ts +78 -0
  177. package/dist/components/ui/markdown/streaming.d.ts.map +1 -0
  178. package/dist/components/ui/markdown/text-size.d.ts +34 -0
  179. package/dist/components/ui/markdown/text-size.d.ts.map +1 -0
  180. package/dist/components/ui/page-actions.d.ts +7 -1
  181. package/dist/components/ui/page-actions.d.ts.map +1 -1
  182. package/dist/components/ui/radio-group.d.ts.map +1 -1
  183. package/dist/components/ui/search-input.d.ts.map +1 -1
  184. package/dist/components/ui/select.d.ts.map +1 -1
  185. package/dist/components/ui/tab-navigation.d.ts +10 -1
  186. package/dist/components/ui/tab-navigation.d.ts.map +1 -1
  187. package/dist/components/ui/tag-search-input.d.ts.map +1 -1
  188. package/dist/components/ui/textarea.d.ts.map +1 -1
  189. package/dist/components/ui/truncate-text.d.ts +2 -2
  190. package/dist/components/ui/truncate-text.d.ts.map +1 -1
  191. package/dist/hooks/index.cjs +4 -2
  192. package/dist/hooks/index.cjs.map +1 -1
  193. package/dist/hooks/index.js +3 -1
  194. package/dist/hooks/ui/index.d.ts +1 -0
  195. package/dist/hooks/ui/index.d.ts.map +1 -1
  196. package/dist/hooks/ui/use-is-truncated.d.ts +33 -0
  197. package/dist/hooks/ui/use-is-truncated.d.ts.map +1 -0
  198. package/dist/index.cjs +64 -4
  199. package/dist/index.cjs.map +1 -1
  200. package/dist/index.js +75 -15
  201. package/dist/utils/index.cjs +169 -40
  202. package/dist/utils/index.cjs.map +1 -1
  203. package/dist/utils/index.d.ts +1 -0
  204. package/dist/utils/index.d.ts.map +1 -1
  205. package/dist/utils/index.js +162 -41
  206. package/dist/utils/index.js.map +1 -1
  207. package/dist/utils/markdown-fences.d.ts +42 -0
  208. package/dist/utils/markdown-fences.d.ts.map +1 -0
  209. package/dist/utils/markdown-heading-id.d.ts +85 -0
  210. package/dist/utils/markdown-heading-id.d.ts.map +1 -0
  211. package/dist/utils/markdown-section-extractor.d.ts.map +1 -1
  212. package/package.json +7 -1
  213. package/src/chat-protocol/__tests__/__snapshots__/nats-decoder-golden.test.ts.snap +317 -0
  214. package/src/chat-protocol/__tests__/chat-protocol.test.ts +516 -0
  215. package/src/chat-protocol/__tests__/env-flag.test.ts +52 -0
  216. package/src/chat-protocol/__tests__/ip-normalize.test.ts +136 -0
  217. package/src/chat-protocol/__tests__/nats-decoder-golden.test.ts +232 -0
  218. package/src/chat-protocol/decode.ts +278 -0
  219. package/src/chat-protocol/encode.ts +71 -0
  220. package/src/chat-protocol/env-flag.ts +39 -0
  221. package/src/chat-protocol/events.ts +245 -0
  222. package/src/chat-protocol/frames.ts +245 -0
  223. package/src/chat-protocol/index.ts +21 -0
  224. package/src/chat-protocol/ip-normalize.ts +146 -0
  225. package/src/chat-protocol/nats-decoder.ts +258 -0
  226. package/src/components/chat/__tests__/chat-message-list.test.tsx +293 -5
  227. package/src/components/chat/__tests__/chat-message-streaming-memo.test.tsx +207 -0
  228. package/src/components/chat/__tests__/chat-pending-turn.test.tsx +230 -0
  229. package/src/components/chat/chat-message-enhanced.tsx +122 -20
  230. package/src/components/chat/chat-message-list.tsx +358 -109
  231. package/src/components/chat/embeddable-chat.tsx +6 -0
  232. package/src/components/chat/hooks/.index.md +30 -33
  233. package/src/components/chat/hooks/.use-nats-chat-adapter.md +36 -55
  234. package/src/components/chat/hooks/__tests__/__snapshots__/conversation-id-persistence-golden.test.ts.snap +37 -0
  235. package/src/components/chat/hooks/__tests__/__snapshots__/sse-stream-golden.test.ts.snap +0 -0
  236. package/src/components/chat/hooks/__tests__/conversation-id-persistence-golden.test.ts +438 -0
  237. package/src/components/chat/hooks/__tests__/sse-stream-golden.test.ts +318 -0
  238. package/src/components/chat/hooks/index.ts +0 -1
  239. package/src/components/chat/hooks/use-chat.ts +8 -0
  240. package/src/components/chat/hooks/use-nats-chat-adapter.ts +243 -825
  241. package/src/components/chat/hooks/use-sse-chat-adapter.ts +380 -684
  242. package/src/components/chat/index.ts +5 -0
  243. package/src/components/chat/stream/__tests__/__snapshots__/chat-stream-reducer-golden.test.ts.snap +940 -0
  244. package/src/components/chat/stream/__tests__/chat-dialog-store.test.ts +756 -0
  245. package/src/components/chat/stream/__tests__/chat-stream-reducer-golden.test.ts +460 -0
  246. package/src/components/chat/stream/__tests__/chat-stream-reducer.test.ts +973 -0
  247. package/src/components/chat/stream/__tests__/delta-batcher.test.ts +255 -0
  248. package/src/components/chat/stream/__tests__/use-chat-stream-reducer.test.ts +98 -0
  249. package/src/components/chat/stream/chat-dialog-store.ts +536 -0
  250. package/src/components/chat/stream/chat-stream-reducer.ts +1808 -0
  251. package/src/components/chat/stream/delta-batcher.ts +168 -0
  252. package/src/components/chat/stream/index.ts +55 -0
  253. package/src/components/chat/stream/message-mutations.ts +340 -0
  254. package/src/components/chat/stream/use-chat-stream-reducer.ts +126 -0
  255. package/src/components/chat/thinking-display.tsx +1 -1
  256. package/src/components/chat/types/.api.types.md +39 -49
  257. package/src/components/chat/types/.processing.types.md +29 -58
  258. package/src/components/chat/types/api.types.ts +6 -73
  259. package/src/components/chat/types/processing.types.ts +11 -53
  260. package/src/components/chat/utils/.index.md +31 -40
  261. package/src/components/chat/utils/__tests__/__snapshots__/process-historical-messages-golden.test.ts.snap +420 -0
  262. package/src/components/chat/utils/__tests__/__snapshots__/segment-accumulator-golden.test.ts.snap +605 -0
  263. package/src/components/chat/utils/__tests__/process-historical-messages-golden.test.ts +394 -0
  264. package/src/components/chat/utils/__tests__/segment-accumulator-golden.test.ts +270 -0
  265. package/src/components/chat/utils/extract-incomplete-message-state.ts +63 -5
  266. package/src/components/chat/utils/index.ts +5 -8
  267. package/src/components/chat/utils/process-historical-messages.ts +358 -386
  268. package/src/components/features/select-button.tsx +4 -1
  269. package/src/components/layout/page-layout.tsx +16 -0
  270. package/src/components/layout/title-block.tsx +106 -7
  271. package/src/components/onboarding-guides/onboarding-guide-detail-view.tsx +22 -4
  272. package/src/components/shared/legal-document/legal-document-page.tsx +1 -1
  273. package/src/components/shared/product-release/release-detail-page.tsx +19 -4
  274. package/src/components/ui/.field-wrapper.md +5 -4
  275. package/src/components/ui/.tab-navigation.md +14 -1
  276. package/src/components/ui/__tests__/__snapshots__/markdown-parity.test.tsx.snap +2420 -0
  277. package/src/components/ui/__tests__/markdown-parity.test.tsx +2528 -0
  278. package/src/components/ui/autocomplete.tsx +13 -1
  279. package/src/components/ui/button/button.tsx +1 -1
  280. package/src/components/ui/checkbox-block.tsx +27 -3
  281. package/src/components/ui/date-picker.tsx +15 -1
  282. package/src/components/ui/field-wrapper.tsx +12 -6
  283. package/src/components/ui/floating-tooltip.tsx +9 -1
  284. package/src/components/ui/index.ts +1 -2
  285. package/src/components/ui/input-trigger.tsx +8 -1
  286. package/src/components/ui/input.tsx +8 -1
  287. package/src/components/ui/markdown/__tests__/mermaid-security.test.ts +115 -0
  288. package/src/components/ui/markdown/__tests__/mermaid-stale-render.test.tsx +95 -0
  289. package/src/components/ui/markdown/__tests__/sanitize-invariant.test.ts +309 -0
  290. package/src/components/ui/markdown/__tests__/streaming.test.tsx +388 -0
  291. package/src/components/ui/markdown/base-components.tsx +360 -0
  292. package/src/components/ui/markdown/engine.tsx +315 -0
  293. package/src/components/ui/markdown/heading-ids.ts +239 -0
  294. package/src/components/ui/markdown/index.ts +49 -0
  295. package/src/components/ui/markdown/mermaid-diagram.tsx +291 -0
  296. package/src/components/ui/markdown/rich/embed-overrides.tsx +199 -0
  297. package/src/components/ui/markdown/rich/rich-markdown-renderer.tsx +184 -0
  298. package/src/components/ui/markdown/rich/shortcodes.ts +170 -0
  299. package/src/components/ui/markdown/sanitize.ts +3499 -0
  300. package/src/components/ui/markdown/simple-markdown-renderer.tsx +28 -0
  301. package/src/components/ui/markdown/streaming.ts +362 -0
  302. package/src/components/ui/markdown/text-size.ts +106 -0
  303. package/src/components/ui/page-actions.tsx +142 -30
  304. package/src/components/ui/radio-group.tsx +28 -3
  305. package/src/components/ui/release-changelog-section.tsx +1 -1
  306. package/src/components/ui/search-input.tsx +2 -1
  307. package/src/components/ui/select.tsx +7 -0
  308. package/src/components/ui/tab-navigation.tsx +26 -4
  309. package/src/components/ui/tag-search-input.tsx +9 -2
  310. package/src/components/ui/textarea.tsx +8 -1
  311. package/src/components/ui/ticket-info-section.tsx +1 -1
  312. package/src/components/ui/truncate-text.tsx +3 -18
  313. package/src/hooks/ui/index.ts +1 -0
  314. package/src/hooks/ui/use-is-truncated.ts +66 -0
  315. package/src/utils/index.ts +1 -0
  316. package/src/utils/markdown-fences.ts +127 -0
  317. package/src/utils/markdown-heading-id.ts +348 -0
  318. package/src/utils/markdown-section-extractor.ts +41 -56
  319. package/dist/chunk-7I42V2OD.cjs.map +0 -1
  320. package/dist/chunk-AIRNG27G.js.map +0 -1
  321. package/dist/chunk-BIKOJ4FH.cjs.map +0 -1
  322. package/dist/chunk-BZMOEWWS.js.map +0 -1
  323. package/dist/chunk-GZK7FWJJ.js.map +0 -1
  324. package/dist/chunk-IEZ3YBAP.cjs.map +0 -1
  325. package/dist/chunk-OQIMH2WQ.js.map +0 -1
  326. package/dist/chunk-PB3BTZDU.js.map +0 -1
  327. package/dist/chunk-STYA77PH.cjs.map +0 -1
  328. package/dist/chunk-VUXNXWOQ.cjs.map +0 -1
  329. package/dist/components/chat/hooks/use-realtime-chunk-processor.d.ts +0 -6
  330. package/dist/components/chat/hooks/use-realtime-chunk-processor.d.ts.map +0 -1
  331. package/dist/components/chat/utils/chunk-parser.d.ts +0 -25
  332. package/dist/components/chat/utils/chunk-parser.d.ts.map +0 -1
  333. package/dist/components/ui/rich-markdown-renderer.d.ts +0 -34
  334. package/dist/components/ui/rich-markdown-renderer.d.ts.map +0 -1
  335. package/dist/components/ui/simple-markdown-renderer.d.ts +0 -73
  336. package/dist/components/ui/simple-markdown-renderer.d.ts.map +0 -1
  337. package/src/components/chat/hooks/.use-realtime-chunk-processor.md +0 -77
  338. package/src/components/chat/hooks/use-realtime-chunk-processor.ts +0 -489
  339. package/src/components/chat/utils/.chunk-parser.md +0 -62
  340. package/src/components/chat/utils/chunk-parser.ts +0 -262
  341. package/src/components/ui/.simple-markdown-renderer.md +0 -52
  342. package/src/components/ui/rich-markdown-renderer.tsx +0 -1223
  343. package/src/components/ui/simple-markdown-renderer.tsx +0 -964
  344. /package/dist/{chunk-WLM3F3WU.js.map → chunk-34TDA22K.js.map} +0 -0
  345. /package/dist/{chunk-EGXCWZYP.js.map → chunk-3LKB722U.js.map} +0 -0
  346. /package/dist/{chunk-RW2LPL72.js.map → chunk-6WTCJI5R.js.map} +0 -0
  347. /package/dist/{chunk-VL2YMDPH.js.map → chunk-J3VBVR4W.js.map} +0 -0
  348. /package/dist/{chunk-6CJ2K7BD.js.map → chunk-MZRIRQYV.js.map} +0 -0
  349. /package/dist/{chunk-NGEFYPY7.js.map → chunk-NCAN6VUF.js.map} +0 -0
  350. /package/dist/{chunk-6QK3ONDD.js.map → chunk-OL3MJKUI.js.map} +0 -0
  351. /package/dist/{chunk-MPYDX5WX.js.map → chunk-PLB6CQSO.js.map} +0 -0
  352. /package/dist/{chunk-6CFFISUC.js.map → chunk-PTXZB2KW.js.map} +0 -0
  353. /package/dist/{chunk-VDZO6IDR.js.map → chunk-XOFE2RT4.js.map} +0 -0
@@ -36,9 +36,12 @@ export const SelectButton = React.forwardRef<HTMLButtonElement, SelectButtonProp
36
36
  "font-body text-left w-full",
37
37
  "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ods-focus",
38
38
  "disabled:pointer-events-none disabled:opacity-50",
39
+ // Hover / active move the BACKGROUND only, like Input — the accent
40
+ // border is reserved for the selected state, so an unselected button
41
+ // no longer flashes "selected" under the cursor.
39
42
  selected
40
43
  ? "bg-ods-open-yellow-light border-ods-accent hover:bg-ods-open-yellow-light-hover active:bg-ods-open-yellow-light-action"
41
- : "bg-ods-card border-ods-border hover:bg-ods-bg-hover hover:border-ods-accent active:bg-ods-bg-active active:border-ods-accent",
44
+ : "bg-ods-card border-ods-border hover:bg-ods-bg-hover active:bg-ods-bg-active",
42
45
  className,
43
46
  )}
44
47
  >
@@ -36,6 +36,12 @@
36
36
  * `titleWrap` prop, forwarded to `TitleBlock`. Defaults to `false` (frozen
37
37
  * single-line truncate). Content detail pages with CMS titles pass it to let
38
38
  * long titles wrap instead of ellipsis-clip. Additive + default-preserving.
39
+ *
40
+ * SANCTIONED EXCEPTION (2026-07, explicit human sign-off): the OPTIONAL
41
+ * `subtitleRow` prop, forwarded to `TitleBlock`. Defaults to `'when-set'` (the
42
+ * subtitle row renders only when there is text for it). Pages whose subtitle is
43
+ * optional record data use it to choose how the row behaves while loading.
44
+ * Additive + default-preserving.
39
45
  * ========================================================================== */
40
46
 
41
47
  import React from 'react'
@@ -68,6 +74,12 @@ export interface PageLayoutProps {
68
74
  /** When true, the title/subtitle render as line-box-accurate skeleton bars (forwarded to
69
75
  * `TitleBlock`). Header height stays identical to the loaded state — for page skeletons. */
70
76
  loading?: boolean
77
+ /** Render the header ACTIONS as placeholders (their set depends on data still loading). */
78
+ loadingActions?: boolean
79
+ /** When the subtitle row occupies the layout (forwarded to `TitleBlock`):
80
+ * `'when-set'` (default), `'while-loading'` (skeleton bar while loading, collapses
81
+ * if the record has no subtitle), or `'always'` (never collapses). */
82
+ subtitleRow?: 'when-set' | 'while-loading' | 'always'
71
83
  /** When true, a long title wraps onto multiple lines instead of the frozen single-line
72
84
  * ellipsis clamp (forwarded to `TitleBlock`). For content detail pages whose h1 is CMS
73
85
  * data of arbitrary length. Additive + default-preserving. */
@@ -96,6 +108,8 @@ export function PageLayout({
96
108
  titleSize,
97
109
  titleAdornment,
98
110
  loading,
111
+ loadingActions,
112
+ subtitleRow,
99
113
  titleWrap,
100
114
  }: PageLayoutProps) {
101
115
  const hasActions = actions && actions.length > 0
@@ -118,6 +132,8 @@ export function PageLayout({
118
132
  titleSize={titleSize}
119
133
  titleAdornment={titleAdornment}
120
134
  loading={loading}
135
+ loadingActions={loadingActions}
136
+ subtitleRow={subtitleRow}
121
137
  titleWrap={titleWrap}
122
138
  />
123
139
  )}
@@ -40,6 +40,14 @@
40
40
  * byte-identical. Do NOT change defaults or touch anything else here.
41
41
  *
42
42
  * SANCTIONED EXCEPTION (2026-07, explicit human sign-off): the OPTIONAL
43
+ * `subtitleRow` prop. Defaults to `'when-set'` — the subtitle row keeps
44
+ * rendering only when `subtitle` is truthy, so every existing caller is
45
+ * byte-identical. The other two values decide how a page trades a header that
46
+ * never moves against a header that never shows an empty line; see the prop's
47
+ * own doc for which to pick. Additive + default-preserving; do NOT change the
48
+ * default.
49
+ *
50
+ * SANCTIONED EXCEPTION (2026-07, explicit human sign-off): the OPTIONAL
43
51
  * `titleWrap` prop. Defaults to `false` — the frozen single-line `truncate`
44
52
  * baseline is unchanged for every existing caller. Content DETAIL pages (whose
45
53
  * h1 is CMS data of arbitrary length — releases, legal docs, FAQ docs, dev
@@ -58,12 +66,27 @@
58
66
  * so its natural width drives the wrap while `truncate` still clamps a title
59
67
  * that alone exceeds the container. The mobile (base) layout is untouched.
60
68
  * This is the new frozen baseline; do NOT re-introduce breakpoint stacking.
69
+ *
70
+ * SANCTIONED EXCEPTION (2026-07, explicit human sign-off): overflow tooltips on
71
+ * the title and subtitle. This one CHANGES the baseline for every caller: the
72
+ * native `title=""` attribute on both is replaced by the ODS `FloatingTooltip`,
73
+ * armed only when the text is genuinely clipped (`useIsTruncated`). Both stay
74
+ * single-line — that is the design, and the tooltip is what makes a clipped
75
+ * title readable instead of merely pretty. Nothing is lost for assistive tech:
76
+ * `text-overflow: ellipsis` never hid the text from the accessibility tree, so
77
+ * the full string was always exposed; the attribute only ever served a mouse.
78
+ * The wrapper `div` the tooltip needs goes OUTSIDE the `h1`/`p` — never inside:
79
+ * a `div` inside a `p` is auto-closed by the HTML parser, which would split the
80
+ * subtitle paragraph and desync hydration. The `titleWrap` branch is untouched
81
+ * (wrapped text never clips, so it never needs a tooltip).
61
82
  * ========================================================================== */
62
83
 
63
84
  import React from 'react'
85
+ import { useIsTruncated } from '../../hooks/ui/use-is-truncated'
64
86
  import { cn } from '../../utils/cn'
65
87
  import type { ActionsMenuGroup } from '../ui/actions-menu'
66
88
  import { EntityImage } from '../ui/entity-image'
89
+ import { FloatingTooltip } from '../ui/floating-tooltip'
67
90
  import { PageActions, type PageActionButton } from '../ui/page-actions'
68
91
  import { BackButton } from './back-button'
69
92
 
@@ -106,6 +129,28 @@ export interface TitleBlockProps {
106
129
  * (typography + surrounding markup unchanged, so header height is identical to loaded).
107
130
  * Additive + default-preserving: omit it and every existing caller is byte-identical. */
108
131
  loading?: boolean
132
+ /**
133
+ * Render the ACTIONS as placeholders too. Separate from `loading` on purpose:
134
+ * a page whose action set is already final (it depends on the route, not on
135
+ * the record) keeps showing its real buttons while the title loads.
136
+ */
137
+ loadingActions?: boolean
138
+ /**
139
+ * When the subtitle row occupies the layout. The subtitle is often optional
140
+ * RECORD data — unknown while loading, sometimes absent afterwards — and there
141
+ * is no setting that both fills the loading header and never leaves a blank
142
+ * line, so the caller picks which one matters.
143
+ *
144
+ * - `'when-set'` (default) — only when `subtitle` has text.
145
+ * - `'while-loading'` — also during `loading`, as a skeleton bar. The header
146
+ * looks complete while it loads, and the row collapses if the record turns
147
+ * out to have no subtitle (so that case settles one line shorter).
148
+ * - `'always'` — the row never collapses; an empty subtitle holds its line
149
+ * with an invisible spacer. The header cannot change height. Use it when the
150
+ * loaded page ALWAYS has a subtitle — typically a standalone skeleton
151
+ * component matching a page whose subtitle is guaranteed.
152
+ */
153
+ subtitleRow?: 'when-set' | 'while-loading' | 'always'
109
154
  /** When true, a long title WRAPS onto multiple lines instead of the frozen single-line
110
155
  * ellipsis clamp. For content detail pages whose h1 is CMS data of arbitrary length.
111
156
  * Additive + default-preserving: omit it and every existing caller is byte-identical. */
@@ -129,6 +174,22 @@ function TitleTextSkeleton({ widthClass, heightClass }: { widthClass: string; he
129
174
  )
130
175
  }
131
176
 
177
+ /**
178
+ * Bar geometry per title size, following the ODS type scale across breakpoints
179
+ * (`md` = 800px, `lg` = 1280px here — the SAME widths the typography tokens
180
+ * switch at, so bar and text step together).
181
+ *
182
+ * `h2` needs no `lg` step because `text-h2` has none: it is 24/32 on mobile and
183
+ * 32/40 from tablet up. `text-h1` scales at both breakpoints (40 → 48 → 56), so
184
+ * its bar does too — reusing the h2 bar there would leave a stamp-sized smudge
185
+ * inside a 64px line. Heights stay well under the line box's ascent on purpose;
186
+ * see `TitleTextSkeleton` for why that is what keeps the header height honest.
187
+ */
188
+ const TITLE_SKELETON_SIZE = {
189
+ h1: { width: 'w-56 md:w-80 lg:w-96', height: 'h-7 md:h-8 lg:h-9' },
190
+ h2: { width: 'w-48 md:w-72', height: 'h-4 md:h-6' },
191
+ } as const
192
+
132
193
  export function TitleBlock({
133
194
  title,
134
195
  subtitle,
@@ -143,14 +204,28 @@ export function TitleBlock({
143
204
  titleSize = 'h2',
144
205
  titleAdornment,
145
206
  loading,
207
+ loadingActions,
208
+ subtitleRow = 'when-set',
146
209
  titleWrap = false,
147
210
  }: TitleBlockProps) {
211
+ const hasSubtitleRow =
212
+ !!subtitle || subtitleRow === 'always' || (subtitleRow === 'while-loading' && !!loading)
148
213
  const hasActions = actions && actions.length > 0
149
214
  const hasMenuActions = !!menuActions && menuActions.some(g => g.items.length > 0)
150
215
  const titleClass = titleSize === 'h1' ? 'text-h1' : 'text-h2'
151
216
  // Frozen baseline is the single-line `truncate`; `titleWrap` swaps it for
152
217
  // multi-line wrapping (break-words guards pathological unbroken tokens).
153
218
  const titleOverflowClass = titleWrap ? 'break-words' : 'truncate'
219
+ const skeletonSize = TITLE_SKELETON_SIZE[titleSize]
220
+
221
+ // Exactly one title element renders per pass, so one ref covers every branch.
222
+ // The tooltip arms itself only on real clipping — repeating a fully visible
223
+ // title back to the user is noise, and `titleWrap`/loading never clip at all.
224
+ const { ref: titleRef, isTruncated: titleTruncated } = useIsTruncated<HTMLHeadingElement>(loading ? null : title)
225
+ const { ref: subtitleRef, isTruncated: subtitleTruncated } = useIsTruncated<HTMLParagraphElement>(
226
+ loading ? null : subtitle,
227
+ )
228
+ const titleNode = loading ? <TitleTextSkeleton widthClass={skeletonSize.width} heightClass={skeletonSize.height} /> : title
154
229
 
155
230
  return (
156
231
  <div
@@ -183,7 +258,7 @@ export function TitleBlock({
183
258
  className="hidden md:inline-flex"
184
259
  />
185
260
  )}
186
- {(image || subtitle || loading) ? (
261
+ {(image || hasSubtitleRow || loading) ? (
187
262
  <div className="flex items-center gap-[var(--spacing-system-m)] min-w-0 w-full">
188
263
  {image && (
189
264
  <EntityImage
@@ -196,15 +271,32 @@ export function TitleBlock({
196
271
  {(loading || title) && (
197
272
  titleAdornment ? (
198
273
  <div className="flex items-center gap-[var(--spacing-system-m)] min-w-0 w-full">
199
- <h1 className={cn(titleClass, 'text-ods-text-primary min-w-0', titleOverflowClass)} title={title}>{loading ? <TitleTextSkeleton widthClass="w-48 md:w-72" heightClass="h-4 md:h-6" /> : title}</h1>
274
+ {/* `min-w-0` moves onto the tooltip's wrapper: the wrapper is
275
+ now the flex item, and without it the row refuses to
276
+ shrink and pushes the adornment out. */}
277
+ <FloatingTooltip content={title} side="bottom" disabled={!titleTruncated} triggerClassName="min-w-0">
278
+ <h1 ref={titleRef} className={cn(titleClass, 'text-ods-text-primary min-w-0', titleOverflowClass)}>{titleNode}</h1>
279
+ </FloatingTooltip>
200
280
  <span className="shrink-0">{titleAdornment}</span>
201
281
  </div>
202
282
  ) : (
203
- <h1 className={cn(titleClass, 'text-ods-text-primary', titleOverflowClass)} title={title}>{loading ? <TitleTextSkeleton widthClass="w-48 md:w-72" heightClass="h-4 md:h-6" /> : title}</h1>
283
+ <FloatingTooltip content={title} side="bottom" disabled={!titleTruncated}>
284
+ <h1 ref={titleRef} className={cn(titleClass, 'text-ods-text-primary', titleOverflowClass)}>{titleNode}</h1>
285
+ </FloatingTooltip>
204
286
  )
205
287
  )}
206
- {subtitle && (
207
- <p className="text-h6 text-ods-text-secondary truncate" title={subtitle}>{loading ? <TitleTextSkeleton widthClass="w-28 md:w-36" heightClass="h-2.5 md:h-3" /> : subtitle}</p>
288
+ {hasSubtitleRow && (
289
+ /* The NBSP is a pure spacer, reached only in `'always'` mode: an
290
+ empty `p` collapses to zero height (its only content would be
291
+ collapsible whitespace), which is exactly the shift that mode
292
+ exists to prevent. Hidden from AT — it carries no meaning. */
293
+ <FloatingTooltip content={subtitle} side="bottom" disabled={!subtitleTruncated}>
294
+ <p
295
+ ref={subtitleRef}
296
+ className="text-h6 text-ods-text-secondary truncate"
297
+ aria-hidden={!loading && !subtitle ? true : undefined}
298
+ >{loading ? <TitleTextSkeleton widthClass="w-28 md:w-36" heightClass="h-2.5 md:h-3" /> : (subtitle || '\u00A0')}</p>
299
+ </FloatingTooltip>
208
300
  )}
209
301
  </div>
210
302
  </div>
@@ -212,7 +304,9 @@ export function TitleBlock({
212
304
  title && (
213
305
  titleAdornment ? (
214
306
  <div className="flex items-center gap-[var(--spacing-system-m)] min-w-0 w-full">
215
- <h1 className={cn(titleClass, 'text-ods-text-primary min-w-0', titleOverflowClass)} title={title}>{title}</h1>
307
+ <FloatingTooltip content={title} side="bottom" disabled={!titleTruncated} triggerClassName="min-w-0">
308
+ <h1 ref={titleRef} className={cn(titleClass, 'text-ods-text-primary min-w-0', titleOverflowClass)}>{title}</h1>
309
+ </FloatingTooltip>
216
310
  <span className="shrink-0">{titleAdornment}</span>
217
311
  </div>
218
312
  ) : (
@@ -226,13 +320,18 @@ export function TitleBlock({
226
320
  )}
227
321
  </div>
228
322
 
229
- {(hasActions || hasMenuActions || selector) && (
323
+ {/* `loadingActions` opens this gate on its own: the case the flag exists for
324
+ is a page that does not YET know its action set, which is exactly the
325
+ page that passes `actions={[]}` — gating on `hasActions` alone would
326
+ render nothing and then pop the real buttons in. */}
327
+ {(hasActions || hasMenuActions || selector || loadingActions) && (
230
328
  <div className="flex gap-2 items-center shrink-0">
231
329
  <PageActions
232
330
  variant={actionsVariant}
233
331
  actions={actions ?? []}
234
332
  menuActions={menuActions}
235
333
  selector={selector}
334
+ loading={loadingActions}
236
335
  />
237
336
  </div>
238
337
  )}
@@ -15,7 +15,7 @@
15
15
  * renderer with extra plugins.
16
16
  */
17
17
 
18
- import { type ComponentType, type ReactNode } from 'react'
18
+ import { Fragment, type ComponentType, type ReactNode } from 'react'
19
19
  import { PageLayout } from '../layout/page-layout'
20
20
 
21
21
  import { useRouter } from '../../embed-shims'
@@ -30,7 +30,7 @@ import { VideoBitesStrip } from '../features/video-bites-strip'
30
30
  import { toStripProfile } from '../features/video-bites-shared'
31
31
  import { useVideoWarmup } from '../features/use-video-warmup'
32
32
  import { getCaptionsUrl } from '../features/captions-url'
33
- import { RichMarkdownRenderer } from '../ui/rich-markdown-renderer'
33
+ import { RichMarkdownRenderer } from '../ui/markdown'
34
34
  import { EntityTagBadges } from '../features/entity-tag-badges'
35
35
  import { LoadError } from '../ui/error-state'
36
36
  import { ArticleAuthorByline } from '../shared/article-author-byline'
@@ -244,8 +244,26 @@ export function OnboardingGuideDetailView({
244
244
  </div>
245
245
  )}
246
246
 
247
- {/* Host slot — cross-type related-content / FAQ rail. */}
248
- {relatedContent}
247
+ {/*
248
+ * Host slot — cross-type related-content / FAQ rail.
249
+ *
250
+ * KEYED DELIBERATELY. This is one member of PageLayout's static children
251
+ * array, and the only member whose element may be created in a SERVER
252
+ * component and crossed over the RSC boundary. Static JSX children are
253
+ * normally exempt from the key warning because `jsxs` runs
254
+ * `validateChildKeys` and stamps `_store.validated = 1` on each element —
255
+ * but a slot arriving over the wire is referenced as a lazy chunk wrapper,
256
+ * so validation marks the WRAPPER and the fulfilled path never copies the
257
+ * flag onto the revived element. It reaches the reconciler with
258
+ * `validated: 0, key: null` and warns, blaming PageLayout (the owner of the
259
+ * enclosing element) rather than anything actually at fault.
260
+ *
261
+ * A keyed Fragment emits no DOM, so rendered output is byte-identical.
262
+ * Do NOT "fix" this in PageLayout via `React.Children.toArray` — that would
263
+ * auto-key every consumer's children and change reconciliation identity for
264
+ * all of them, and that file carries an explicit FROZEN contract.
265
+ */}
266
+ <Fragment key="related-content">{relatedContent}</Fragment>
249
267
  </PageLayout>
250
268
  )
251
269
  }
@@ -19,7 +19,7 @@
19
19
 
20
20
  import type { ComponentType } from 'react';
21
21
  import { PageShell, PageLayout } from '../../ui';
22
- import { RichMarkdownRenderer } from '../../ui/rich-markdown-renderer';
22
+ import { RichMarkdownRenderer } from '../../ui/markdown';
23
23
  import { useRouter } from '../../../embed-shims/next-navigation';
24
24
  import { useLegalDocs, type LegalDocument } from './use-legal-docs';
25
25
  import { formatLegalDate } from '../../../utils/format';
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { useState, useEffect, ComponentType, type ReactNode } from 'react';
3
+ import { useState, useEffect, Fragment, ComponentType, type ReactNode } from 'react';
4
4
  import Link from '../../../embed-shims/next-link';
5
5
  import { useRouter } from '../../../embed-shims/next-navigation';
6
6
  import { Card, CardContent } from '../../ui/card';
@@ -11,7 +11,7 @@ import { PageShell } from '../../layout/article-detail-layout';
11
11
  import { PageLayout } from '../../layout/page-layout';
12
12
  import { FadePreview } from '../../ui/fade-preview';
13
13
  import { ReleaseChangelogSection } from '../../ui/release-changelog-section';
14
- import { RichMarkdownRenderer } from '../../ui/rich-markdown-renderer';
14
+ import { RichMarkdownRenderer } from '../../ui/markdown';
15
15
  import { EntityTagBadges } from '../../features/entity-tag-badges';
16
16
  import { EntityMetadataAuthorCell } from '../../chat/entity-cards/entity-author-card';
17
17
  import type { EntityAuthor } from '../../../types/entity-author';
@@ -582,8 +582,23 @@ export function ReleaseDetailPage({
582
582
  )}
583
583
  </div>
584
584
 
585
- {/* Host slot — end-of-article byline + related-content / FAQ rail. */}
586
- {relatedContent}
585
+ {/*
586
+ * Host slot — end-of-article byline + related-content / FAQ rail.
587
+ *
588
+ * KEYED for the same reason as `onboarding-guide-detail-view.tsx`: this is a
589
+ * member of PageLayout's static children array, and a slot element built in a
590
+ * SERVER component crosses the RSC boundary as a lazy chunk wrapper, so
591
+ * `validateChildKeys` marks the wrapper and the fulfilled path never copies
592
+ * `_store.validated` onto the revived element — it reaches the reconciler
593
+ * unvalidated and warns.
594
+ *
595
+ * Today the hub's only consumer of this view is a `'use client'` wrapper that
596
+ * builds the slot locally, so no element actually crosses the boundary and
597
+ * this is pre-emptive. It is kept because the trap is invisible from here: a
598
+ * future server-built slot would warn with no clue pointing at this line.
599
+ * A keyed Fragment emits no DOM, so output is byte-identical either way.
600
+ */}
601
+ <Fragment key="related-content">{relatedContent}</Fragment>
587
602
  </PageLayout>
588
603
  );
589
604
  }
@@ -1,4 +1,4 @@
1
- <!-- source-hash: bd15d4489bb0eca1b2faabacba129705 -->
1
+ <!-- source-hash: 674794aef9f624e4f74a99dae56b9617 -->
2
2
  A layout wrapper component that adds an optional label above and a status message below any form field, handling the visual chrome (label + feedback text) while letting the field itself remain unstyled.
3
3
 
4
4
  ## Key Components
@@ -11,15 +11,16 @@ A `forwardRef` component that wraps form fields with consistent label and feedba
11
11
  | Prop | Type | Default | Description |
12
12
  |------|------|---------|-------------|
13
13
  | `label` | `string` | — | Label text rendered above the field |
14
- | `error` | `string` | — | Status/feedback message rendered below the field |
14
+ | `error` | `string` | — | Status/feedback message rendered below the field (out of flow) |
15
15
  | `errorVariant` | `"error" \| "warning" \| "success" \| "muted"` | `"error"` | Color variant for the feedback message |
16
16
  | `className` | `string` | — | Additional class applied to the outer wrapper |
17
17
  | `children` | `React.ReactNode` | — | The field element(s) to wrap |
18
18
 
19
19
  **Behavior:**
20
20
  - When neither `label` nor `error` is present, the wrapper renders as `contents` (no layout box introduced).
21
- - When either exists, it renders as a full-width flex column.
22
- - The feedback message clamps to 2 lines with ellipsis; the full text is accessible via the native `title` tooltip.
21
+ - When either exists, it renders as a full-width **relative** flex column.
22
+ - The feedback message is a single line, ellipsized on overflow; the full text is accessible via the native `title` tooltip.
23
+ - The message is positioned **absolutely** (`bottom-0` + `translate-y-full`), so showing or hiding it never changes the field's height — no layout shift on validation. The trade-off: it hangs BELOW the wrapper box and overlaps whatever follows, so a form stacking fields must leave at least ~20px of vertical gap (e.g. `gap-[var(--spacing-system-lf)]`) for it to land in clear space. `CheckboxBlock` uses the same treatment.
23
24
 
24
25
  **Color Variants:**
25
26
 
@@ -1,4 +1,4 @@
1
- <!-- source-hash: 4f81360202d7fa9875b28cdae48c29a5 -->
1
+ <!-- source-hash: ef59a6aa7c2983608cd2d9c678c34323 -->
2
2
  Accessible, scrollable tab navigation bar component with optional URL synchronization, overflow scroll shadows, and support for both controlled and URL-driven active tab state.
3
3
 
4
4
  ## Key Components
@@ -46,6 +46,19 @@ Configuration object for URL synchronization:
46
46
  {(activeTab) => <ContentPanel tab={activeTab} />}
47
47
  </TabNavigation>
48
48
 
49
+ // Render prop with the pending flag. The tab BODY is deferred
50
+ // (`useDeferredValue`), so the underline moves instantly while the body keeps
51
+ // showing the previous tab until the new one is ready — no Suspense-fallback
52
+ // flash. `isStale` is true during that window; mark the body so stale data
53
+ // doesn't read as fresh data.
54
+ <TabNavigation tabs={tabs} urlSync>
55
+ {(activeTab, { isStale }) => (
56
+ <div className={cn(isStale && 'opacity-60 transition-opacity')}>
57
+ <ContentPanel tab={activeTab} />
58
+ </div>
59
+ )}
60
+ </TabNavigation>
61
+
49
62
  // Stretched tabs with forced gradients
50
63
  <TabNavigation
51
64
  tabs={tabs}