@flamingo-stack/openframe-frontend-core 0.0.296 → 0.0.297
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.
- package/README.md +9 -0
- package/dist/{chunk-7RIYT7ZH.js → chunk-2QG57XOJ.js} +1067 -205
- package/dist/chunk-2QG57XOJ.js.map +1 -0
- package/dist/{chunk-WHMATDVP.js → chunk-3JIQVE7T.js} +9 -15
- package/dist/{chunk-WHMATDVP.js.map → chunk-3JIQVE7T.js.map} +1 -1
- package/dist/{chunk-GLLDTKZK.cjs → chunk-4PSQS3SW.cjs} +7 -9
- package/dist/chunk-4PSQS3SW.cjs.map +1 -0
- package/dist/{chunk-OY7OF7E7.js → chunk-4TLE6VLU.js} +30 -24
- package/dist/chunk-4TLE6VLU.js.map +1 -0
- package/dist/{chunk-W6M2FLLT.cjs → chunk-53FUMSZ5.cjs} +40 -46
- package/dist/chunk-53FUMSZ5.cjs.map +1 -0
- package/dist/{chunk-D3LEFMOA.cjs → chunk-54KNMC2R.cjs} +3 -3
- package/dist/{chunk-D3LEFMOA.cjs.map → chunk-54KNMC2R.cjs.map} +1 -1
- package/dist/{chunk-EYEW6PTA.cjs → chunk-6C526VNN.cjs} +358 -118
- package/dist/chunk-6C526VNN.cjs.map +1 -0
- package/dist/{chunk-XREEV72C.cjs → chunk-7OVGB2DQ.cjs} +19 -25
- package/dist/chunk-7OVGB2DQ.cjs.map +1 -0
- package/dist/{chunk-6GCI7JOE.js → chunk-AD6C23QY.js} +8 -7
- package/dist/{chunk-6GCI7JOE.js.map → chunk-AD6C23QY.js.map} +1 -1
- package/dist/chunk-F5OB2YAL.cjs +144 -0
- package/dist/chunk-F5OB2YAL.cjs.map +1 -0
- package/dist/chunk-FBWXMMRB.cjs +2 -0
- package/dist/chunk-FBWXMMRB.cjs.map +1 -0
- package/dist/{chunk-YIGPRLQY.cjs → chunk-FCDQNTDG.cjs} +21 -20
- package/dist/chunk-FCDQNTDG.cjs.map +1 -0
- package/dist/{chunk-IE6OU3WQ.cjs → chunk-FQOTC3UU.cjs} +318 -16
- package/dist/chunk-FQOTC3UU.cjs.map +1 -0
- package/dist/{chunk-QHIXS3W2.cjs → chunk-GUTS7HGA.cjs} +11590 -2105
- package/dist/chunk-GUTS7HGA.cjs.map +1 -0
- package/dist/chunk-GZ4C3XW6.js +2 -0
- package/dist/chunk-GZ4C3XW6.js.map +1 -0
- package/dist/{chunk-5P3B2LZW.js → chunk-IL47XWV5.js} +8 -14
- package/dist/{chunk-5P3B2LZW.js.map → chunk-IL47XWV5.js.map} +1 -1
- package/dist/{chunk-LCNMR277.js → chunk-IZ7JSBFP.js} +1 -1
- package/dist/chunk-IZ7JSBFP.js.map +1 -0
- package/dist/{chunk-EL6QLAWX.js → chunk-JALO4TAZ.js} +357 -55
- package/dist/chunk-JALO4TAZ.js.map +1 -0
- package/dist/{chunk-AQOWFSMB.cjs → chunk-L6PSSIUQ.cjs} +1 -1
- package/dist/chunk-L6PSSIUQ.cjs.map +1 -0
- package/dist/{chunk-MBFWU2EM.js → chunk-L7ULJKG7.js} +6 -10
- package/dist/{chunk-MBFWU2EM.js.map → chunk-L7ULJKG7.js.map} +1 -1
- package/dist/{chunk-K2PFPBMF.js → chunk-PC746XCO.js} +15050 -5565
- package/dist/chunk-PC746XCO.js.map +1 -0
- package/dist/{chunk-3ZXUQQL4.js → chunk-PI4WSYQV.js} +2 -2
- package/dist/{chunk-E4XABBSU.js → chunk-PWQUAVA3.js} +338 -98
- package/dist/chunk-PWQUAVA3.js.map +1 -0
- package/dist/chunk-SA2WPJVO.js +144 -0
- package/dist/chunk-SA2WPJVO.js.map +1 -0
- package/dist/{chunk-X6BV7MB7.cjs → chunk-UNVE2SDJ.cjs} +37 -31
- package/dist/chunk-UNVE2SDJ.cjs.map +1 -0
- package/dist/{chunk-5E2HOSSH.cjs → chunk-WMSTJAZT.cjs} +913 -51
- package/dist/chunk-WMSTJAZT.cjs.map +1 -0
- package/dist/{chunk-ZP4AVIZP.js → chunk-X4DOXQRT.js} +4 -6
- package/dist/{chunk-ZP4AVIZP.js.map → chunk-X4DOXQRT.js.map} +1 -1
- package/dist/{chunk-X647HY3F.cjs → chunk-YBYI62OE.cjs} +33 -37
- package/dist/chunk-YBYI62OE.cjs.map +1 -0
- package/dist/components/case-studies/index.cjs +126 -0
- package/dist/components/case-studies/index.cjs.map +1 -0
- package/dist/components/case-studies/index.d.ts +2 -0
- package/dist/components/case-studies/index.d.ts.map +1 -0
- package/dist/components/case-studies/index.js +126 -0
- package/dist/components/case-studies/index.js.map +1 -0
- package/dist/components/case-studies/share-experience-section.d.ts +48 -0
- package/dist/components/case-studies/share-experience-section.d.ts.map +1 -0
- package/dist/components/chat/index.cjs +8 -18
- package/dist/components/chat/index.cjs.map +1 -1
- package/dist/components/chat/index.js +75 -85
- package/dist/components/contact/index.cjs +8 -15
- package/dist/components/contact/index.cjs.map +1 -1
- package/dist/components/contact/index.js +7 -14
- package/dist/components/docs/doc-viewer.d.ts +39 -2
- package/dist/components/docs/doc-viewer.d.ts.map +1 -1
- package/dist/components/docs/docs-hub-page.d.ts +46 -0
- package/dist/components/docs/docs-hub-page.d.ts.map +1 -0
- package/dist/components/docs/index.cjs +17 -9
- package/dist/components/docs/index.cjs.map +1 -1
- package/dist/components/docs/index.d.ts +4 -0
- package/dist/components/docs/index.d.ts.map +1 -1
- package/dist/components/docs/index.js +16 -8
- package/dist/components/docs/skeletons.d.ts +32 -0
- package/dist/components/docs/skeletons.d.ts.map +1 -0
- package/dist/components/docs/use-docs-resolve-link.d.ts +20 -0
- package/dist/components/docs/use-docs-resolve-link.d.ts.map +1 -0
- package/dist/components/docs/use-document-tree.d.ts.map +1 -1
- package/dist/components/embeds/embed-container.d.ts +37 -0
- package/dist/components/embeds/embed-container.d.ts.map +1 -0
- package/dist/components/embeds/embed-iframe.d.ts.map +1 -1
- package/dist/components/embeds/file-download-card.d.ts +18 -0
- package/dist/components/embeds/file-download-card.d.ts.map +1 -0
- package/dist/components/embeds/index.cjs +38 -15
- package/dist/components/embeds/index.cjs.map +1 -1
- package/dist/components/embeds/index.d.ts +8 -0
- package/dist/components/embeds/index.d.ts.map +1 -1
- package/dist/components/embeds/index.js +40 -17
- package/dist/components/embeds/linkedin-embed-client.d.ts +8 -0
- package/dist/components/embeds/linkedin-embed-client.d.ts.map +1 -0
- package/dist/components/embeds/markdown-image.d.ts +5 -0
- package/dist/components/embeds/markdown-image.d.ts.map +1 -0
- package/dist/components/embeds/reddit-embed-client.d.ts +7 -0
- package/dist/components/embeds/reddit-embed-client.d.ts.map +1 -0
- package/dist/components/embeds/rich-markdown-runtime.d.ts +46 -0
- package/dist/components/embeds/rich-markdown-runtime.d.ts.map +1 -0
- package/dist/components/embeds/twitter-embed-client.d.ts +8 -0
- package/dist/components/embeds/twitter-embed-client.d.ts.map +1 -0
- package/dist/components/faq/index.cjs +9 -16
- package/dist/components/faq/index.cjs.map +1 -1
- package/dist/components/faq/index.js +8 -15
- package/dist/components/features/index.cjs +8 -16
- package/dist/components/features/index.cjs.map +1 -1
- package/dist/components/features/index.js +24 -32
- package/dist/components/index.cjs +257 -452
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +781 -976
- package/dist/components/index.js.map +1 -1
- package/dist/components/layout/page-header.d.ts +78 -0
- package/dist/components/layout/page-header.d.ts.map +1 -0
- package/dist/components/layout/page-layout.d.ts +10 -1
- package/dist/components/layout/page-layout.d.ts.map +1 -1
- package/dist/components/layout/page-with-header.d.ts +67 -0
- package/dist/components/layout/page-with-header.d.ts.map +1 -0
- package/dist/components/layout/title-block.d.ts +17 -1
- package/dist/components/layout/title-block.d.ts.map +1 -1
- package/dist/components/navigation/index.cjs +7 -15
- package/dist/components/navigation/index.cjs.map +1 -1
- package/dist/components/navigation/index.js +9 -17
- package/dist/components/onboarding-guides/index.cjs +35 -36
- package/dist/components/onboarding-guides/index.cjs.map +1 -1
- package/dist/components/onboarding-guides/index.js +13 -14
- package/dist/components/onboarding-guides/index.js.map +1 -1
- package/dist/components/onboarding-guides/onboarding-guide-detail-view.d.ts +1 -1
- package/dist/components/onboarding-guides/onboarding-guide-detail-view.d.ts.map +1 -1
- package/dist/components/related-content/index.cjs +9 -16
- package/dist/components/related-content/index.cjs.map +1 -1
- package/dist/components/related-content/index.js +8 -15
- package/dist/components/shared/dev-section/dev-section-page.d.ts +9 -0
- package/dist/components/shared/dev-section/dev-section-page.d.ts.map +1 -1
- package/dist/components/shared/dev-section/dev-section-view.d.ts.map +1 -1
- package/dist/components/shared/dev-section/index.d.ts +1 -1
- package/dist/components/shared/dev-section/index.d.ts.map +1 -1
- package/dist/components/shared/doc-search/use-doc-search.d.ts.map +1 -1
- package/dist/components/shared/legal-document/legal-document-page.d.ts.map +1 -1
- package/dist/components/shared/product-release/release-detail-page.d.ts.map +1 -1
- package/dist/components/tickets/index.cjs +100 -112
- package/dist/components/tickets/index.cjs.map +1 -1
- package/dist/components/tickets/index.js +20 -32
- package/dist/components/tickets/index.js.map +1 -1
- package/dist/components/ui/file-manager/index.cjs +50 -52
- package/dist/components/ui/file-manager/index.cjs.map +1 -1
- package/dist/components/ui/file-manager/index.js +4 -6
- package/dist/components/ui/file-manager/index.js.map +1 -1
- package/dist/components/ui/index.cjs +13 -19
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.d.ts +2 -0
- package/dist/components/ui/index.d.ts.map +1 -1
- package/dist/components/ui/index.js +133 -139
- package/dist/components/ui/release-changelog-section.d.ts +6 -2
- package/dist/components/ui/release-changelog-section.d.ts.map +1 -1
- package/dist/components/ui/rich-markdown-renderer.d.ts +34 -0
- package/dist/components/ui/rich-markdown-renderer.d.ts.map +1 -0
- package/dist/components/ui/simple-markdown-renderer.d.ts +2 -8
- package/dist/components/ui/simple-markdown-renderer.d.ts.map +1 -1
- package/dist/contexts/chat-runtime-context.d.ts +14 -0
- package/dist/contexts/chat-runtime-context.d.ts.map +1 -1
- package/dist/contexts/index.cjs +3 -3
- package/dist/contexts/index.js +5 -5
- package/dist/embed-shims/index.cjs +3 -3
- package/dist/embed-shims/index.cjs.map +1 -1
- package/dist/embed-shims/index.js +4 -4
- package/dist/hooks/index.cjs +4 -9
- package/dist/hooks/index.cjs.map +1 -1
- package/dist/hooks/index.js +6 -11
- package/dist/index.cjs +14 -20
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +362 -368
- package/dist/types/doc-source.d.ts +31 -1
- package/dist/types/doc-source.d.ts.map +1 -1
- package/dist/utils/index.cjs +4 -0
- package/dist/utils/index.cjs.map +1 -1
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +4 -1
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/page-header-constants.d.ts +15 -0
- package/dist/utils/page-header-constants.d.ts.map +1 -0
- package/dist/utils/social-embed-cache.d.ts +29 -0
- package/dist/utils/social-embed-cache.d.ts.map +1 -0
- package/package.json +7 -1
- package/src/components/case-studies/index.ts +4 -0
- package/src/components/case-studies/share-experience-section.tsx +185 -0
- package/src/components/chat/embeddable-chat.tsx +1 -1
- package/src/components/docs/doc-viewer.tsx +111 -19
- package/src/components/docs/docs-hub-page.tsx +149 -0
- package/src/components/docs/index.ts +17 -0
- package/src/components/docs/skeletons.tsx +138 -0
- package/src/components/docs/use-docs-resolve-link.ts +52 -0
- package/src/components/docs/use-document-tree.ts +21 -0
- package/src/components/embeds/embed-container.tsx +80 -0
- package/src/components/embeds/embed-iframe.tsx +7 -9
- package/src/components/embeds/file-download-card.tsx +54 -0
- package/src/components/embeds/index.ts +30 -0
- package/src/components/embeds/linkedin-embed-client.tsx +100 -0
- package/src/components/embeds/markdown-image.tsx +88 -0
- package/src/components/embeds/og-link-preview.tsx +13 -13
- package/src/components/embeds/reddit-embed-client.tsx +550 -0
- package/src/components/embeds/rich-markdown-runtime.tsx +79 -0
- package/src/components/embeds/twitter-embed-client.tsx +308 -0
- package/src/components/layout/page-header.tsx +182 -0
- package/src/components/layout/page-layout.tsx +14 -1
- package/src/components/layout/page-with-header.tsx +110 -0
- package/src/components/layout/title-block.tsx +40 -62
- package/src/components/onboarding-guides/onboarding-guide-detail-view.tsx +3 -3
- package/src/components/shared/dev-section/dev-section-page.tsx +9 -1
- package/src/components/shared/dev-section/dev-section-view.tsx +14 -9
- package/src/components/shared/dev-section/index.ts +1 -1
- package/src/components/shared/doc-search/use-doc-search.ts +7 -3
- package/src/components/shared/legal-document/legal-document-page.tsx +2 -2
- package/src/components/shared/product-release/release-detail-page.tsx +6 -4
- package/src/components/ui/index.ts +2 -0
- package/src/components/ui/release-changelog-section.tsx +7 -2
- package/src/components/ui/rich-markdown-renderer.tsx +1203 -0
- package/src/components/ui/simple-markdown-renderer.tsx +7 -11
- package/src/contexts/chat-runtime-context.tsx +14 -0
- package/src/types/doc-source.ts +33 -1
- package/src/utils/index.ts +1 -0
- package/src/utils/page-header-constants.ts +15 -0
- package/src/utils/social-embed-cache.ts +391 -0
- package/dist/chunk-26PKDALD.js +0 -2379
- package/dist/chunk-26PKDALD.js.map +0 -1
- package/dist/chunk-3MCHAFHB.js +0 -89
- package/dist/chunk-3MCHAFHB.js.map +0 -1
- package/dist/chunk-5E2HOSSH.cjs.map +0 -1
- package/dist/chunk-66AANIOC.cjs +0 -619
- package/dist/chunk-66AANIOC.cjs.map +0 -1
- package/dist/chunk-6JINAOI7.cjs +0 -311
- package/dist/chunk-6JINAOI7.cjs.map +0 -1
- package/dist/chunk-7RIYT7ZH.js.map +0 -1
- package/dist/chunk-AQOWFSMB.cjs.map +0 -1
- package/dist/chunk-BOCFIKYS.cjs +0 -3009
- package/dist/chunk-BOCFIKYS.cjs.map +0 -1
- package/dist/chunk-D652TJBQ.js +0 -3009
- package/dist/chunk-D652TJBQ.js.map +0 -1
- package/dist/chunk-E4XABBSU.js.map +0 -1
- package/dist/chunk-EL6QLAWX.js.map +0 -1
- package/dist/chunk-EYEW6PTA.cjs.map +0 -1
- package/dist/chunk-FQJK446R.js +0 -1606
- package/dist/chunk-FQJK446R.js.map +0 -1
- package/dist/chunk-GLLDTKZK.cjs.map +0 -1
- package/dist/chunk-IE6OU3WQ.cjs.map +0 -1
- package/dist/chunk-J54Z3OCR.cjs +0 -1606
- package/dist/chunk-J54Z3OCR.cjs.map +0 -1
- package/dist/chunk-K2PFPBMF.js.map +0 -1
- package/dist/chunk-KXCRGTRN.cjs +0 -2379
- package/dist/chunk-KXCRGTRN.cjs.map +0 -1
- package/dist/chunk-LCNMR277.js.map +0 -1
- package/dist/chunk-LFGGF7OT.cjs +0 -449
- package/dist/chunk-LFGGF7OT.cjs.map +0 -1
- package/dist/chunk-M2OCXTNT.js +0 -311
- package/dist/chunk-M2OCXTNT.js.map +0 -1
- package/dist/chunk-ME4EVDFP.js +0 -619
- package/dist/chunk-ME4EVDFP.js.map +0 -1
- package/dist/chunk-OQ6X7ZOC.js +0 -449
- package/dist/chunk-OQ6X7ZOC.js.map +0 -1
- package/dist/chunk-OY7OF7E7.js.map +0 -1
- package/dist/chunk-POKKCWKF.js +0 -354
- package/dist/chunk-POKKCWKF.js.map +0 -1
- package/dist/chunk-QHIXS3W2.cjs.map +0 -1
- package/dist/chunk-TFSYSWPS.cjs +0 -89
- package/dist/chunk-TFSYSWPS.cjs.map +0 -1
- package/dist/chunk-W6M2FLLT.cjs.map +0 -1
- package/dist/chunk-X647HY3F.cjs.map +0 -1
- package/dist/chunk-X6BV7MB7.cjs.map +0 -1
- package/dist/chunk-XREEV72C.cjs.map +0 -1
- package/dist/chunk-YETA25JW.cjs +0 -354
- package/dist/chunk-YETA25JW.cjs.map +0 -1
- package/dist/chunk-YIGPRLQY.cjs.map +0 -1
- /package/dist/{chunk-3ZXUQQL4.js.map → chunk-PI4WSYQV.js.map} +0 -0
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["/home/runner/work/openframe-oss-lib/openframe-oss-lib/openframe-frontend-core/dist/chunk-BOCFIKYS.cjs","../src/components/ui/card.tsx","../src/components/ui/tabs.tsx","../src/components/ui/status-badge.tsx","../src/components/ui/simple-markdown-renderer.tsx","../src/components/ui/square-avatar.tsx","../src/components/ui/actions-menu.tsx","../node_modules/@radix-ui/react-use-controllable-state/src/use-controllable-state.tsx","../node_modules/@radix-ui/react-use-layout-effect/src/use-layout-effect.tsx","../node_modules/@radix-ui/react-use-controllable-state/src/use-controllable-state-reducer.tsx","../src/components/chat/utils/nav-anchor-props.ts","../src/components/chat/entity-cards/entity-author-card.tsx","../src/components/chat/entity-cards/blog-image-placeholder.tsx","../src/components/features/video.tsx","../src/components/features/video-ratio-tabs.tsx","../src/components/features/video-bites-display.tsx","../src/components/features/entity-video-section.tsx","../src/components/chat/entity-cards/onboarding-guide-card.tsx","../src/components/chat/entity-cards/use-entity-card-link.ts","../src/components/chat/entity-cards/use-entity-card-placeholder.ts","../src/components/ui/page-actions.tsx","../src/components/ui/entity-image.tsx","../src/components/layout/title-block.tsx","../src/components/layout/back-button.tsx","../src/components/layout/page-layout.tsx","../src/components/layout/article-detail-layout.tsx","../src/components/ui/error-state.tsx","../src/components/features/entity-tag-badges.tsx","../src/components/features/mux-origins.ts","../src/components/features/use-video-warmup.ts","../src/components/features/captions-url.ts","../src/components/ui/filter-pill-row.tsx"],"names":["jsx","React","useCallback","jsxs","useEffect","useRef","Fragment","useMemo"],"mappings":"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AA;AACA,IAAA;AACgB,MAAA;AACZ,IAAA;AACN,MAAA;AACF,IAAA;AACoB,IAAA;AAEP,IAAA;AACF,IAAA;AACD,IAAA;AACE,IAAA;AACO,IAAA;AAIf,IAAA;AACF,MAAA;AAAwD,MAAA;AAC1D,IAAA;AACc,IAAA;AAED,IAAA;AACC,MAAA;AACd,IAAA;AACoB,EAAA;AAED,EAAA;AACvB;A7ByqFwB;AACA;A8Bx0FtB;AAIiB,EAAA;AACD,EAAA;AACT,EAAA;AACT;A9Bu0FwB;AACA;A+Bj1FxB;AADuB;AAiCjB;AAVwB;AAC5B,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACqB;AAEnB,EAAA;AACED,oBAAAA;AACEH,sBAAAA;AACAA,sBAAAA;AACF,IAAA;AAEU,IAAA;AACL,MAAA;AAAA,MAAA;AAEM,QAAA;AACI,QAAA;AACJ,QAAA;AACU,QAAA;AACL,QAAA;AAEL,QAAA;AAAA,MAAA;AAPI,MAAA;AASZ,IAAA;AAED,IAAA;AAIJ,EAAA;AAEJ;A/ByzFwB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"/home/runner/work/openframe-oss-lib/openframe-oss-lib/openframe-frontend-core/dist/chunk-BOCFIKYS.cjs","sourcesContent":[null,"\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"../../utils/cn\"\n\nconst Card = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n <div\n ref={ref}\n className={cn(\n \"rounded-lg border bg-card text-card-foreground shadow-sm\",\n className\n )}\n {...props}\n />\n))\nCard.displayName = \"Card\"\n\nconst CardHeader = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n <div\n ref={ref}\n className={cn(\"flex flex-col space-y-1.5 p-6\", className)}\n {...props}\n />\n))\nCardHeader.displayName = \"CardHeader\"\n\nconst CardTitle = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n <div\n ref={ref}\n className={cn(\n \"text-2xl font-semibold leading-none tracking-tight\",\n className\n )}\n {...props}\n />\n))\nCardTitle.displayName = \"CardTitle\"\n\nconst CardDescription = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n <div\n ref={ref}\n className={cn(\"text-sm text-muted-foreground\", className)}\n {...props}\n />\n))\nCardDescription.displayName = \"CardDescription\"\n\nconst CardContent = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n <div ref={ref} className={cn(\"p-6 pt-0\", className)} {...props} />\n))\nCardContent.displayName = \"CardContent\"\n\nconst CardFooter = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n <div\n ref={ref}\n className={cn(\"flex items-center p-6 pt-0\", className)}\n {...props}\n />\n))\nCardFooter.displayName = \"CardFooter\"\n\n// Unified horizontal card for homepage category section\ninterface CardHorizontalProps {\n icon: React.ReactNode;\n title: string;\n description: string;\n className?: string;\n borderLeft?: boolean;\n}\n\nexport function CardHorizontal({ icon, title, description, className = '', borderLeft = true }: CardHorizontalProps) {\n return (\n <div\n className={cn(\n 'w-full flex flex-row items-center gap-3 md:gap-4 bg-ods-card p-4 md:p-6 min-h-[80px]',\n borderLeft ? 'border-l border-ods-border' : '',\n className\n )}\n >\n <div className=\"w-5 h-5 flex-shrink-0\">{icon}</div>\n <div className=\"flex flex-col min-w-0\">\n <span className=\"font-medium text-sm md:text-sm text-ods-text-primary leading-tight mb-0.5 text-left\">{title}</span>\n <span className=\"text-xs md:text-sm text-ods-text-secondary leading-tight text-left\">{description}</span>\n </div>\n </div>\n );\n}\n\nexport { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }","\"use client\"\n\nimport * as React from \"react\"\nimport * as TabsPrimitive from \"@radix-ui/react-tabs\"\n\nimport { cn } from \"../../utils/cn\"\n\nconst Tabs = TabsPrimitive.Root\n\nconst TabsList = React.forwardRef<\n React.ElementRef<typeof TabsPrimitive.List>,\n React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>\n>(({ className, ...props }, ref) => (\n <TabsPrimitive.List\n ref={ref}\n className={cn(\n \"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground\",\n className\n )}\n {...props}\n />\n))\nTabsList.displayName = TabsPrimitive.List.displayName\n\nconst TabsTrigger = React.forwardRef<\n React.ElementRef<typeof TabsPrimitive.Trigger>,\n React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>\n>(({ className, ...props }, ref) => (\n <TabsPrimitive.Trigger\n ref={ref}\n className={cn(\n \"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm\",\n className\n )}\n {...props}\n />\n))\nTabsTrigger.displayName = TabsPrimitive.Trigger.displayName\n\nconst TabsContent = React.forwardRef<\n React.ElementRef<typeof TabsPrimitive.Content>,\n React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>\n>(({ className, ...props }, ref) => (\n <TabsPrimitive.Content\n ref={ref}\n className={cn(\n \"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\",\n className\n )}\n {...props}\n />\n))\nTabsContent.displayName = TabsPrimitive.Content.displayName\n\nexport { Tabs, TabsList, TabsTrigger, TabsContent }\n","\"use client\"\n\nimport React from 'react';\nimport { cn } from '../../utils/cn';\nimport { cva, type VariantProps } from 'class-variance-authority';\n\nconst statusBadgeVariants = cva(\n \"inline-flex items-center justify-center rounded font-mono font-medium uppercase tracking-wide\",\n {\n variants: {\n variant: {\n card: \"px-3 py-1.5 text-sm\",\n button: \"px-2 py-0.5 text-[10px] leading-none\",\n },\n colorScheme: {\n cyan: \"bg-[var(--ods-flamingo-cyan-base)] text-ods-text-on-accent\",\n pink: \"bg-[var(--ods-flamingo-pink-base)] text-ods-text-on-accent\",\n yellow: \"bg-[var(--ods-flamingo-yellow-base)] text-ods-text-on-accent border border-[var(--ods-system-greys-black)]\",\n green: \"bg-[var(--ods-flamingo-green-base)] text-ods-text-on-accent\",\n purple: \"bg-[var(--ods-flamingo-purple-base)] text-ods-text-on-accent\",\n success: \"bg-[var(--ods-attention-green-success-secondary)] text-[var(--ods-attention-green-success)]\",\n error: \"bg-[var(--ods-attention-red-error-secondary)] text-[var(--ods-attention-red-error)]\",\n warning: \"bg-[var(--ods-attention-yellow-warning-secondary)] text-[var(--ods-attention-yellow-warning)]\",\n default: \"bg-ods-bg-secondary text-ods-text-primary\",\n // Border-only variants (no background) - for task type badges\n accentBorder: \"bg-transparent border-2 text-ods-accent border-ods-accent\",\n errorBorder: \"bg-transparent border-2 text-[var(--ods-attention-red-error)] border-[var(--ods-attention-red-error)]\",\n whiteBorder: \"bg-transparent border-2 text-ods-text-primary border-ods-text-primary\",\n },\n },\n defaultVariants: {\n variant: \"card\",\n colorScheme: \"default\",\n },\n }\n);\n\nexport interface StatusBadgeProps\n extends React.HTMLAttributes<HTMLSpanElement>,\n VariantProps<typeof statusBadgeVariants> {\n text: string;\n /**\n * When true, renders `text` verbatim on a single line, bypassing the\n * default multi-word vertical-stack behavior used by `variant=\"button\"`.\n * Use this for compact inline contexts (e.g. chat-inline roadmap cards)\n * where the stamp-like stacked layout is undesirable.\n */\n singleLine?: boolean;\n}\n\nfunction StatusBadge({\n text,\n variant,\n colorScheme,\n className,\n singleLine,\n ...props\n}: StatusBadgeProps) {\n // Outer element is `<span>` so the badge is HTML-valid in any inline\n // context (e.g. inside a markdown `<p>` next to a compact chat card,\n // or inside an `<a>`). The `inline-flex` base class in\n // `statusBadgeVariants` keeps the layout identical to the previous\n // `<div>` outer — only the element name changed.\n //\n // Escape hatch: callers can pass `singleLine` to opt out of the\n // multi-word stacking applied for `variant=\"button\"`. This is needed\n // for compact inline contexts (chat-inline roadmap cards) where the\n // default stamp-like vertical stack (\"TO\" / \"DO\") breaks layout.\n const renderText = () => {\n if (singleLine) return text;\n if (variant === 'button' && text.includes(' ')) {\n const words = text.split(' ');\n return (\n <span className=\"flex flex-col items-center justify-center text-center gap-0\">\n {words.map((word, index) => (\n <span key={index} className=\"block\">{word}</span>\n ))}\n </span>\n );\n }\n return text;\n };\n\n return (\n <span\n className={cn(statusBadgeVariants({ variant, colorScheme }), className)}\n {...props}\n >\n {renderText()}\n </span>\n );\n}\n\nexport { StatusBadge, statusBadgeVariants };","\"use client\";\n\nimport React, { useEffect, useState, useRef, useMemo, useCallback, memo } from 'react';\nimport ReactMarkdown, { type Components, defaultUrlTransform } from 'react-markdown';\nimport type { PluggableList } from 'unified';\nimport remarkGfm from 'remark-gfm';\nimport remarkBreaks from 'remark-breaks';\nimport rehypeHighlight from 'rehype-highlight';\nimport rehypeRaw from 'rehype-raw';\nimport { visit } from 'unist-util-visit';\nimport Image from '../../embed-shims/next-image';\nimport { AlertCircleIcon } from '../icons-v2-generated';\nimport { cn } from '../../utils/cn';\n\n// ---------------------------------------------------------------------------\n// rehype HAST sanitizer — runs AFTER rehype-raw to strip XSS vectors\n// ---------------------------------------------------------------------------\n/**\n * Minimal HAST sanitizer. Runs AFTER `rehype-raw` (which parses raw HTML\n * embedded in markdown) and BEFORE `rehype-highlight`. Strips the\n * attack surfaces that rehype-raw leaves wide open:\n *\n * - `on*` event handlers (onerror, onload, onclick, …) on ANY element\n * - href / src / formaction / xlink:href / poster pointing at\n * `javascript:` (case + whitespace tolerant)\n * - `iframe srcdoc` (full-document XSS)\n * - `<script>`, `<style>`, `<noscript>`, `<noembed>` elements (drop)\n * - `data:` URIs on src-ish attrs (SVG-with-embedded-JS class of bug)\n *\n * Why custom (vs `rehype-sanitize`): the OSS-lib build environment\n * doesn't have `rehype-sanitize` in its `node_modules` (sandbox\n * restriction), but `unist-util-visit` is already a transitive dep of\n * `rehype-raw`. This plugin is ~60 lines, ships nothing new, and is\n * tighter than the default sanitize schema for our threat model\n * (we want LLM-emitted markdown to be safe; we don't need full HTML5\n * fidelity).\n *\n * The text-level `escapeUnknownHtmlTags` pre-pass below is still useful\n * for catching `<their>`-style accidental tag emissions that React 19\n * rejects, but it is NOT a security boundary. THIS is.\n */\nconst EVENT_HANDLER_ATTR_RE = /^on[a-z]+$/i\nconst JAVASCRIPT_URL_RE = /^[\\s\\x00-\\x1f]*javascript:/i\nconst DATA_URL_RE = /^[\\s\\x00-\\x1f]*data:/i\nconst URL_ATTRS = new Set([\n 'href',\n 'src',\n // `srcset` accepts `javascript:` on legacy browsers — same guard\n // as the canonical hast-util-sanitize default schema's attr set.\n // Multi-candidate scanning lives in `srcsetHasUnsafeCandidate` below;\n // a single-URL check would miss a malicious second candidate like\n // `\"https://safe.png 1x, javascript:alert(1) 2x\"`.\n 'srcset',\n 'formaction',\n 'xlink:href',\n 'poster',\n 'data',\n 'action',\n 'background',\n])\n\n/**\n * Returns true if any candidate in an `srcset` attribute has a dangerous\n * URL scheme. Per the HTML spec, srcset is a comma-separated list of\n * candidates; each candidate is `<url> <descriptor>?` (e.g.\n * `\"https://x/a.png 2x\"`). The single-URL `JAVASCRIPT_URL_RE.test(v)`\n * check only inspects the FIRST candidate — so a malicious second\n * candidate (`\"https://safe 1x, javascript:alert(1) 2x\"`) would slip\n * through. This helper splits on `,`, trims each candidate, takes the\n * leading whitespace-delimited token (the URL), and tests that.\n *\n * Splitting on every comma over-matches the rare-in-practice case of\n * commas inside URL paths. That's the correct error bias for a\n * sanitizer: over-strip is safe, under-strip is not.\n */\nfunction srcsetHasUnsafeCandidate(srcset: string): boolean {\n for (const candidate of srcset.split(',')) {\n const url = candidate.trim().split(/\\s+/)[0] ?? ''\n if (JAVASCRIPT_URL_RE.test(url) || DATA_URL_RE.test(url)) return true\n }\n return false\n}\n// Element-level drop list. The text-pre-pass `escapeUnknownHtmlTags`\n// already escapes any `<tag>` whose name isn't on its allow-list, so\n// `<object>`, `<embed>`, `<applet>`, `<base>`, `<meta>` normally never\n// reach `rehype-raw` as elements. Re-stripping at the HAST layer\n// removes the cross-layer dependency: this sanitizer is sufficient\n// on its own even if the text pre-pass is bypassed.\nconst STRIP_ELEMENTS = new Set([\n 'script',\n 'style',\n 'noscript',\n 'noembed',\n 'object',\n 'embed',\n 'applet',\n 'base',\n 'meta',\n])\n\nfunction rehypeStripUnsafe() {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return (tree: any) => {\n visit(tree, 'element', (node: any, index: number | undefined, parent: any) => {\n const tag = String(node.tagName ?? '').toLowerCase()\n if (STRIP_ELEMENTS.has(tag)) {\n if (parent && typeof index === 'number') {\n parent.children.splice(index, 1)\n // Return the numeric index (`unist-util-visit` treats it as\n // `[CONTINUE, index]`) so the walker resumes at the slot the\n // removed node vacated. Don't return `[SKIP, index]`:\n // skip-descendants is meaningless for a node we just removed,\n // and SKIP+index conflates two signals.\n return index\n }\n // Root-level strip element (parent undefined). Rare, but\n // possible if `rehype-raw` ever lifts one. Neutralize in\n // place by clearing children + retagging as a blank span.\n node.children = []\n node.tagName = 'span'\n node.properties = {}\n return\n }\n if (!node.properties || typeof node.properties !== 'object') return\n for (const key of Object.keys(node.properties)) {\n // 1. event handlers\n if (EVENT_HANDLER_ATTR_RE.test(key)) {\n delete node.properties[key]\n continue\n }\n // 2. dangerous URL schemes on URL-bearing attrs. `srcset` is\n // multi-candidate so it routes through `srcsetHasUnsafeCandidate`\n // which inspects every URL in the list (not just the first).\n if (URL_ATTRS.has(key.toLowerCase())) {\n const raw = node.properties[key]\n const v = Array.isArray(raw) ? raw[0] : raw\n if (typeof v === 'string') {\n const unsafe =\n key.toLowerCase() === 'srcset'\n ? srcsetHasUnsafeCandidate(v)\n : JAVASCRIPT_URL_RE.test(v) || DATA_URL_RE.test(v)\n if (unsafe) {\n delete node.properties[key]\n continue\n }\n }\n }\n // 3. iframe srcdoc — full-document XSS vector\n if (tag === 'iframe' && key.toLowerCase() === 'srcdoc') {\n delete node.properties[key]\n }\n }\n })\n }\n}\n\n/**\n * URL transformer that extends react-markdown's default safe-protocol\n * allowlist with the two internal schemes the chat remark plugins emit:\n * - `card://` — `remarkCardLinks`, for inline chat-card markers.\n * - `mention://` — `remarkMentionChips`, for inline `@marker:id` AI mentions.\n *\n * Without this, react-markdown 10's `defaultUrlTransform` strips the URL\n * to `\"\"` before the `<a>` component override runs (the override's\n * `href.startsWith('card://')` / `'mention://'` check then fails and the\n * marker leaks through — as literal text, or as an empty-href link that the\n * host's `NavLinkAnchor` resolves to a base URL). All other URL schemes still\n * go through the default sanitizer — `javascript:`, `vbscript:`, `data:`\n * (non-image), etc. remain blocked.\n *\n * Scope: both schemes are allowed ONLY for `href` attributes. If the LLM\n * accidentally emits `` the URL still goes through\n * `defaultUrlTransform` (which strips it to `\"\"`) so an `<img src=\"card://...\">`\n * never reaches the DOM — broken request avoided. Per v6.1 §B.2.4: these\n * schemes are internal — never network-fetched, never written to attributes\n * other than `href` for renderer dispatch.\n */\nfunction cardAwareUrlTransform(url: string, key: string): string {\n if (key === 'href' && typeof url === 'string' && (url.startsWith('card://') || url.startsWith('mention://')))\n return url;\n return defaultUrlTransform(url);\n}\n\n// ---------------------------------------------------------------------------\n// LLM-output sanitizer — escape unknown HTML-style tags\n// ---------------------------------------------------------------------------\n/**\n * Tags `rehype-raw` is allowed to forward to React as-is. Anything\n * outside this set gets its angle brackets escaped so it renders as\n * plain text rather than reaching React as an unrecognized custom\n * element.\n *\n * Why this matters: chat output is LLM-generated markdown. An LLM that\n * accidentally wraps a word in angle brackets (\"share <their> settings\")\n * or echoes a system-prompt XML tag (\"the <ticket> element above\")\n * makes `rehype-raw` mint a `<their>` / `<ticket>` JSX element. React\n * 18 logs \"tag is unrecognized in this browser\" for every unknown tag;\n * React 19 throws on tags with reserved kebab-case forms. We pre-escape\n * to keep the renderer pristine without losing legitimate inline HTML\n * (details/summary, video, iframe, kbd, mark, etc.).\n *\n * The allow-list mirrors HTML5 + the elements the chat shell wires\n * component overrides for. Kept lower-case; matched case-insensitively.\n */\nconst SAFE_HTML_TAGS = new Set([\n // Block + inline text\n 'a', 'abbr', 'address', 'article', 'aside', 'b', 'bdi', 'bdo', 'blockquote',\n 'br', 'caption', 'cite', 'code', 'col', 'colgroup', 'data', 'dd', 'del',\n 'details', 'dfn', 'div', 'dl', 'dt', 'em', 'figcaption', 'figure', 'footer',\n 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hgroup', 'hr', 'i', 'ins',\n 'kbd', 'li', 'main', 'mark', 'nav', 'ol', 'p', 'pre', 'q', 'rp', 'rt',\n 'ruby', 's', 'samp', 'section', 'small', 'span', 'strong', 'sub', 'summary',\n 'sup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'time', 'tr', 'u',\n 'ul', 'var', 'wbr',\n // Media\n 'img', 'picture', 'source', 'audio', 'video', 'iframe', 'track',\n // Forms (rehype-raw allows them; mostly harmless for chat output)\n 'button', 'input', 'label', 'select', 'option', 'optgroup', 'textarea', 'form', 'fieldset', 'legend',\n])\n\n/**\n * Match an opening / closing HTML tag in markdown source. Captures:\n * 1 — optional `/` for closing tags\n * 2 — tag name (must start with a letter)\n * 3 — everything between the name and the closing `>` (attrs etc.)\n * 4 — optional `/` for void-element self-close\n */\n// ReDoS-safe shape (CodeQL polynomial-regex hardening):\n//\n// Every quantifier is hard-bounded so the engine has no possible\n// polynomial path. The original `[a-zA-Z0-9-]*` + `[^>]*?` admitted\n// O(n²) backtracking on `<A-----…` because both classes include `-`;\n// CodeQL flagged that as a polynomial regex. Bounding the tag-name\n// at 63 chars (covers every real HTML tag — `animateTransform` at 16\n// is the longest in practice) and the rest at 4096 chars (sane upper\n// bound for `<a href=\"…\">` attribute strings) means matching is\n// constant-time per tag, NOT proportional to input length.\n//\n// Lazy `{0,4096}?` is deliberately lazy (not greedy) so the trailing\n// `(\\/?)>` still captures the self-close `/` — greedy would swallow\n// the slash into the rest. Bounded lazy backtracking is constant-time\n// per match (capped at 4096 expansions), so the laziness is safe.\n//\n// Anything longer than 4159 chars per tag simply doesn't match —\n// falls through as plain text, the safe-degrade behavior for\n// HTML-in-markdown.\n//\n// Tag shapes still covered: `<a>`, `<a/>`, `<a class=\"x\">`,\n// `<a class=\"x\" />`, `</a>`, `<input type=\"text\" />`,\n// `<custom-element data-foo=\"bar\">`, `<animateTransform>`, etc.\nconst TAG_LIKE_REGEX = /<(\\/?)([a-zA-Z][a-zA-Z0-9-]{0,63})((?:\\s[^>]{0,4096}?)?)(\\/?)>/g\n\nfunction escapeUnknownHtmlTags(text: string): string {\n if (!text || text.indexOf('<') === -1) return text\n // Carve out fenced code blocks AND inline-backtick spans so we don't\n // corrupt `<their>` examples that legitimately live inside code. The\n // regex matches in priority order: triple-fence first (greediest),\n // then inline single-backtick. Each protected span is preserved\n // verbatim; everything between/around them runs through\n // `escapeOutsideFences`.\n const parts: string[] = []\n let cursor = 0\n // Triple-fence ``` … ``` OR single-backtick `…` (one line, no\n // embedded newlines). Backtick group is non-greedy and forbids\n // inner newlines per CommonMark inline-code semantics.\n const PROTECTED_SPAN_RE = /```[\\s\\S]*?```|`[^`\\n]+`/g\n let span: RegExpExecArray | null\n while ((span = PROTECTED_SPAN_RE.exec(text)) !== null) {\n if (span.index > cursor) {\n parts.push(escapeOutsideFences(text.slice(cursor, span.index)))\n }\n parts.push(span[0])\n cursor = span.index + span[0].length\n }\n if (cursor < text.length) {\n parts.push(escapeOutsideFences(text.slice(cursor)))\n }\n return parts.join('')\n}\n\nfunction escapeOutsideFences(segment: string): string {\n return segment.replace(TAG_LIKE_REGEX, (match, slash, tag, rest, selfClose) => {\n const lower = (tag as string).toLowerCase()\n if (SAFE_HTML_TAGS.has(lower)) return match\n // Unknown tag — escape so it renders as text instead of reaching\n // React as a custom element.\n return `<${slash}${tag}${rest}${selfClose}>`\n })\n}\n\n// ---------------------------------------------------------------------------\n// Mermaid styles (responsive)\n// ---------------------------------------------------------------------------\nconst mermaidStyles = `\n .mermaid-svg-container svg {\n max-width: 100% !important;\n height: auto !important;\n min-height: 200px;\n font-family: 'DM Sans', sans-serif !important;\n font-size: 14px !important;\n }\n @media (min-width: 1520px) {\n .mermaid-svg-container svg {\n max-width: 900px !important;\n max-height: 700px !important;\n min-height: 300px;\n font-size: 16px !important;\n }\n }\n @media (min-width: 768px) and (max-width: 1519px) {\n .mermaid-svg-container svg {\n max-width: 700px !important;\n max-height: 600px !important;\n min-height: 250px;\n font-size: 15px !important;\n }\n }\n @media (max-width: 767px) {\n .mermaid-svg-container svg {\n max-width: 90vw !important;\n max-height: 400px !important;\n min-height: 200px;\n font-size: 13px !important;\n }\n }\n .mermaid-svg-container svg[width] { width: 100% !important; }\n .mermaid-svg-container .node rect,\n .mermaid-svg-container .node circle,\n .mermaid-svg-container .node ellipse,\n .mermaid-svg-container .node polygon { stroke-width: 2px !important; }\n .mermaid-svg-container .edgePath path { stroke-width: 2px !important; }\n @media (min-width: 768px) {\n .mermaid-svg-container .node text,\n .mermaid-svg-container .edgeLabel text { font-size: 14px !important; }\n }\n @media (min-width: 1520px) {\n .mermaid-svg-container .node text,\n .mermaid-svg-container .edgeLabel text { font-size: 16px !important; }\n }\n`;\n\n// ---------------------------------------------------------------------------\n// MermaidDiagram\n// ---------------------------------------------------------------------------\nconst MermaidDiagram: React.FC<{ chart: string }> = ({ chart }) => {\n const [svg, setSvg] = useState<string>('');\n const [error, setError] = useState<string>('');\n const [isLoading, setIsLoading] = useState<boolean>(true);\n const [mounted, setMounted] = useState(false);\n\n useEffect(() => { setMounted(true); }, []);\n\n useEffect(() => {\n const renderMermaid = async () => {\n try {\n setIsLoading(true);\n // @ts-ignore -- mermaid is an optional runtime dependency, dynamically imported by the consumer\n const { default: mermaid } = await import('mermaid');\n\n mermaid.initialize({\n startOnLoad: false,\n theme: 'dark' as const,\n themeVariables: {\n primaryColor: '#FFC008',\n primaryTextColor: '#FAFAFA',\n primaryBorderColor: '#3A3A3A',\n lineColor: '#888888',\n secondaryColor: '#212121',\n tertiaryColor: '#2A2A2A',\n background: 'transparent',\n mainBkg: 'transparent',\n secondBkg: 'transparent',\n tertiaryBkg: 'transparent',\n cScale0: '#FFC008', cScale1: '#4ECDC4', cScale2: '#45B7D1',\n cScale3: '#96CEB4', cScale4: '#FFEAA7', cScale5: '#DDA0DD',\n cScale6: '#98D8C8', cScale7: '#F7DC6F', cScale8: '#BB8FCE',\n cScale9: '#85C1E9',\n taskTextColor: '#FAFAFA',\n taskTextOutsideColor: '#FAFAFA',\n activeTaskTextColor: '#1A1A1A',\n nodeTextColor: '#FAFAFA',\n },\n flowchart: { useMaxWidth: true, htmlLabels: true, rankSpacing: 50, nodeSpacing: 30, curve: 'basis' },\n sequence: { useMaxWidth: true, width: 150 },\n pie: { useMaxWidth: true, useWidth: undefined },\n fontFamily: 'DM Sans, sans-serif',\n fontSize: 14,\n securityLevel: 'loose',\n });\n\n const { svg: renderedSvg } = await mermaid.render(`mermaid-${Date.now()}`, chart);\n setSvg(renderedSvg);\n setIsLoading(false);\n } catch (err) {\n console.error('Mermaid rendering error:', err);\n setError(`Failed to render diagram: ${err instanceof Error ? err.message : 'Unknown error'}`);\n setIsLoading(false);\n }\n };\n\n if (mounted) { renderMermaid(); }\n }, [chart, mounted]);\n\n if (error) {\n return (\n <div className=\"error-state bg-ods-card border border-ods-border rounded-lg p-6 my-6\">\n <div className=\"error-icon flex justify-center mb-4\">\n <AlertCircleIcon className=\"w-12 h-12 text-ods-error\" />\n </div>\n <div className=\"error-title text-center font-sans font-semibold text-lg text-ods-error mb-2\">\n Diagram Error\n </div>\n <div className=\"error-description text-center font-sans text-sm text-ods-text-secondary mb-4 break-words overflow-hidden max-w-full\">\n <div className=\"overflow-x-auto\">\n <pre className=\"whitespace-pre-wrap break-words text-xs\">{error}</pre>\n </div>\n </div>\n </div>\n );\n }\n\n if (isLoading || !svg) {\n return (\n <div className=\"skeleton-code bg-ods-card border border-ods-border rounded-lg p-6 min-h-[120px] flex items-center justify-center\">\n <div className=\"animate-pulse text-ods-text-tertiary font-sans\">\n {isLoading ? 'Loading diagram renderer...' : 'Rendering diagram...'}\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"mermaid-container rounded-lg p-4 md:p-6 lg:p-8 my-6 overflow-x-auto bg-ods-card border border-ods-border\">\n <div className=\"flex justify-center items-center w-full min-h-[200px] md:min-h-[250px] lg:min-h-[300px]\">\n <div\n className=\"mermaid-svg-container w-full flex justify-center max-w-full\"\n style={{ fontSize: '14px' }}\n dangerouslySetInnerHTML={{\n __html: svg.replace(/<svg[^>]*>/, (match) =>\n match.replace(/width=\"[^\"]*\"/, 'width=\"100%\"').replace(/height=\"[^\"]*\"/, 'height=\"auto\"')\n ),\n }}\n />\n </div>\n </div>\n );\n};\n\n// ---------------------------------------------------------------------------\n// Utility: extract plain text from React children\n// ---------------------------------------------------------------------------\nfunction extractText(node: any): string {\n if (typeof node === 'string') return node;\n if (Array.isArray(node)) return node.map(extractText).join('');\n if (node?.props?.children) return extractText(node.props.children);\n return '';\n}\n\n// ---------------------------------------------------------------------------\n// Text size configuration\n// ---------------------------------------------------------------------------\nexport type TextSizeElement =\n | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'\n | 'p' | 'li' | 'blockquote' | 'code' | 'th' | 'td';\n\nexport type TextSizeClassMap = Partial<Record<TextSizeElement, string>>;\nexport type TextSizePreset = 'default' | 'compact' | 'large';\nexport type TextSizeConfig =\n | TextSizePreset\n | TextSizeClassMap\n | { preset: TextSizePreset; overrides: TextSizeClassMap };\n\nconst TEXT_SIZE_PRESETS: Record<TextSizePreset, Record<TextSizeElement, string>> = {\n default: {\n h1: 'text-heading-1',\n h2: 'text-heading-2',\n h3: 'text-2xl md:text-3xl',\n h4: 'text-xl',\n h5: 'text-lg md:text-xl',\n h6: 'text-base md:text-lg',\n p: 'md:text-h4 lg:text-h4',\n li: 'text-base md:text-lg',\n blockquote: 'text-lg',\n code: 'text-[14px]',\n th: 'text-xs md:text-sm',\n td: 'text-xs md:text-sm',\n },\n compact: {\n h1: 'text-heading-2',\n h2: 'text-heading-3',\n h3: 'text-xl md:text-2xl',\n h4: 'text-lg md:text-xl',\n h5: 'text-base md:text-lg',\n h6: 'text-sm md:text-base',\n p: 'text-base md:text-lg',\n li: 'text-base md:text-lg',\n blockquote: 'text-base md:text-lg',\n code: 'text-[13px]',\n th: 'text-xs md:text-sm',\n td: 'text-xs md:text-sm',\n },\n large: {\n h1: 'text-heading-1',\n h2: 'text-heading-1',\n h3: 'text-heading-2',\n h4: 'text-2xl md:text-3xl',\n h5: 'text-xl md:text-2xl',\n h6: 'text-lg md:text-xl',\n p: 'text-h3',\n li: 'text-lg md:text-xl',\n blockquote: 'text-xl md:text-2xl',\n code: 'text-[16px]',\n th: 'text-sm md:text-base',\n td: 'text-sm md:text-base',\n },\n};\n\nfunction resolveTextSizeConfig(config?: TextSizeConfig): Record<TextSizeElement, string> {\n const defaultSizes = TEXT_SIZE_PRESETS.default;\n if (!config) return defaultSizes;\n if (typeof config === 'string') return TEXT_SIZE_PRESETS[config];\n if ('preset' in config) return { ...TEXT_SIZE_PRESETS[config.preset], ...config.overrides };\n return { ...defaultSizes, ...config };\n}\n\n// ---------------------------------------------------------------------------\n// Resolved link result used by onResolveLink callback\n// ---------------------------------------------------------------------------\nexport interface ResolveLinkResult {\n success: boolean;\n resolvedPath?: string;\n type?: string;\n action?: string;\n error?: string;\n message?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Props\n// ---------------------------------------------------------------------------\nexport interface SimpleMarkdownRendererProps {\n content: string;\n className?: string;\n /** Backend-provided heading IDs for deep-link anchors */\n sectionIds?: Array<{ id: string; title: string; level: number }>;\n /** When the page already has an H1, render markdown `#` as `<h2>` */\n demoteMarkdownH1ToH2?: boolean;\n /** List of broken link hrefs detected server-side (shown with [BROKEN] badge) */\n brokenLinks?: string[];\n /** Callback for internal (non-http, non-anchor) link clicks */\n onInternalLinkClick?: (path: string, options?: { expandFolder?: boolean; fromInternalLink?: boolean }) => void;\n /** Current documentation path — enables internal-link mode when set */\n currentPath?: string;\n /**\n * Resolve an internal link href to a navigation path.\n * Called when a user clicks an internal doc link.\n */\n onResolveLink?: (href: string, currentPath: string) => Promise<ResolveLinkResult>;\n /** Pre-process the raw markdown string before rendering (e.g. shortcode expansion) */\n preprocessContent?: (content: string) => string;\n /** Merge additional or override react-markdown component renderers */\n componentOverrides?: Partial<Components>;\n /**\n * Extra remark plugins appended after the built-in `remarkGfm` and\n * `remarkBreaks`. Used by chat consumers to inject `remarkCardLinks`,\n * which converts `[card://<type>:<id>]` markers into synthetic `link`\n * nodes; the host's `componentOverrides.a` then swaps those for whatever\n * inline entity-card component it provides.\n *\n * Each entry is either a Plugin reference or a [Plugin, options] tuple\n * — same shape as react-markdown's own `remarkPlugins` prop. Pass an\n * empty array (or omit) to keep the default plugin set.\n */\n additionalRemarkPlugins?: PluggableList;\n /**\n * Configure text sizing for all rendered elements.\n * - `\"default\"` — current behavior (large article-style typography)\n * - `\"compact\"` — smaller sizes for sidebars, cards, changelogs\n * - `\"large\"` — extra-large sizes for hero/landing content\n * - `{ p: \"text-sm\", h1: \"text-2xl\" }` — per-element overrides (merged onto default)\n * - `{ preset: \"compact\", overrides: { h1: \"text-heading-1\" } }` — preset + tweaks\n */\n textSize?: TextSizeConfig;\n}\n\n// ---------------------------------------------------------------------------\n// Component\n// ---------------------------------------------------------------------------\nconst SimpleMarkdownRendererImpl: React.FC<SimpleMarkdownRendererProps> = ({\n content,\n className = \"\",\n sectionIds,\n demoteMarkdownH1ToH2 = false,\n brokenLinks = [],\n onInternalLinkClick,\n currentPath: propCurrentPath,\n onResolveLink,\n preprocessContent,\n componentOverrides,\n textSize,\n additionalRemarkPlugins,\n}) => {\n const idCountsRef = useRef<Record<string, number>>({});\n\n // ---- text sizes ----\n const textSizes = useMemo(() => resolveTextSizeConfig(textSize), [textSize]);\n\n // ---- section ID map ----\n const sectionIdMap = useMemo(() => {\n const map = new Map<string, string>();\n if (sectionIds) {\n sectionIds.forEach(section => {\n const cleanTitle = section.title\n .replace(/[\\u{1F300}-\\u{1F9FF}]|[\\u{2600}-\\u{26FF}]|[\\u{2700}-\\u{27BF}]/gu, '')\n .trim()\n .toLowerCase();\n map.set(section.title.toLowerCase(), section.id);\n map.set(cleanTitle, section.id);\n map.set(section.title, section.id);\n });\n }\n return map;\n }, [sectionIds]);\n\n // ---- heading ID generation ----\n const generateHeadingId = useCallback((text: string, level: number): string => {\n if (sectionIds && (level === 1 || level === 2)) {\n const variations = [\n text, text.toLowerCase(),\n text.replace(/[\\u{1F300}-\\u{1F9FF}]|[\\u{2600}-\\u{26FF}]|[\\u{2700}-\\u{27BF}]/gu, '').trim(),\n text.replace(/[\\u{1F300}-\\u{1F9FF}]|[\\u{2600}-\\u{26FF}]|[\\u{2700}-\\u{27BF}]/gu, '').trim().toLowerCase(),\n ];\n for (const v of variations) {\n const id = sectionIdMap.get(v);\n if (id) return id;\n }\n }\n const baseId = text\n .replace(/[\\u{1F300}-\\u{1F9FF}]|[\\u{2600}-\\u{26FF}]|[\\u{2700}-\\u{27BF}]/gu, '')\n .trim().toLowerCase()\n .replace(/[^\\w\\s-]/g, '').replace(/\\s+/g, '-').replace(/^-+|-+$/g, '');\n const cleanId = baseId || `section-${Object.keys(idCountsRef.current).length + 1}`;\n if (idCountsRef.current[cleanId]) {\n idCountsRef.current[cleanId]++;\n return `${cleanId}-${idCountsRef.current[cleanId]}`;\n }\n idCountsRef.current[cleanId] = 1;\n return cleanId;\n }, [sectionIds, sectionIdMap]);\n\n // ---- preprocess ----\n // Run the optional caller-supplied transform first, then defensively\n // escape unknown HTML-style tags so an LLM that wrote `<their>` or\n // accidentally echoed a system-prompt XML tag like `<ticket>` doesn't\n // make `rehype-raw` hand React an unrecognized custom element (which\n // logs a noisy \"tag is unrecognized in this browser\" warning AND can\n // crash the SimpleMarkdownRenderer when the tag has a kebab-case\n // form React rejects outright). Allow-listed tags pass through\n // unchanged so legitimate inline HTML (details/summary, video,\n // iframe, etc.) still renders.\n const processedContent = useMemo(\n () => escapeUnknownHtmlTags(preprocessContent ? preprocessContent(content) : content),\n [preprocessContent, content],\n );\n\n // ---- heading factory ----\n const makeHeading = useCallback(\n (Tag: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6', level: number, headingClassName: string) =>\n ({ children }: any) => {\n const text = extractText(children);\n const effectiveLevel = Tag === 'h1' && demoteMarkdownH1ToH2 ? 2 : level;\n const id = generateHeadingId(text, effectiveLevel);\n const EffectiveTag = Tag === 'h1' && demoteMarkdownH1ToH2 ? 'h2' : Tag;\n return <EffectiveTag id={id} className={headingClassName}>{children}</EffectiveTag>;\n },\n [generateHeadingId, demoteMarkdownH1ToH2],\n );\n\n // ---- components ----\n const components: Components = useMemo(() => ({\n // --- code ---\n code: ({ node, inline, className: codeClassName, children, ...props }: any) => {\n const match = /language-(\\w+)/.exec(codeClassName || '');\n const language = match ? match[1] : '';\n\n if (!inline && language === 'mermaid') {\n return <MermaidDiagram chart={String(children).replace(/\\n$/, '')} />;\n }\n\n if (!inline && match) {\n return (\n <div className=\"code-block-container border rounded-lg my-6 overflow-hidden bg-ods-card border-ods-border\">\n <div className=\"code-header border-b px-4 py-2 bg-ods-card border-ods-border\">\n <span className=\"font-sans text-xs uppercase tracking-wide text-ods-text-tertiary\">\n {language || 'code'}\n </span>\n </div>\n <div className=\"p-4\">\n <pre className=\"overflow-x-auto\">\n <code\n className={cn(`language-${language} hljs`, textSizes.code)}\n style={{\n fontFamily: \"JetBrains Mono', 'SF Mono', Consolas, monospace\",\n background: 'transparent',\n color: 'var(--ods-text-primary)',\n }}\n {...props}\n >\n {children}\n </code>\n </pre>\n </div>\n </div>\n );\n }\n\n return (\n <code className=\"font-mono text-[0.9em] px-1.5 py-0.5 rounded border bg-ods-card text-ods-text-primary border-ods-border\" {...props}>\n {children}\n </code>\n );\n },\n\n // --- div (pass-through, overridable for embeds) ---\n div: ({ node, className: divClassName, children, ...props }: any) => (\n <div className={divClassName} {...props}>{children}</div>\n ),\n\n // --- blockquote ---\n blockquote: ({ children }: any) => (\n <blockquote className=\"border-l-4 border-ods-accent ml-0 pl-6 my-8 py-4 rounded-r-lg bg-ods-bg-secondary\">\n <div className={cn('font-sans leading-relaxed text-ods-text-secondary', textSizes.blockquote)}>\n {children}\n </div>\n </blockquote>\n ),\n\n // --- headings ---\n h1: makeHeading('h1', 1, cn('font-sans font-bold mt-8 mb-4 first:mt-0 text-ods-text-primary', textSizes.h1)),\n h2: makeHeading('h2', 2, cn('font-sans font-semibold mt-8 mb-4 pb-2 border-b text-ods-text-primary border-ods-border', textSizes.h2)),\n h3: makeHeading('h3', 3, cn('font-sans font-semibold mt-6 mb-3 text-ods-text-primary', textSizes.h3)),\n h4: makeHeading('h4', 4, cn('font-sans font-semibold mt-4 mb-2 text-ods-text-primary', textSizes.h4)),\n h5: makeHeading('h5', 5, cn('font-sans font-semibold mt-3 mb-2 text-ods-text-primary', textSizes.h5)),\n h6: makeHeading('h6', 6, cn('font-sans font-semibold mt-3 mb-1 text-ods-text-primary', textSizes.h6)),\n\n // --- paragraph ---\n p: ({ children }: any) => (\n <p className={cn('leading-relaxed mb-4 first:mt-0 last:mb-0 text-ods-text-primary', textSizes.p)}>\n {children}\n </p>\n ),\n\n // --- links ---\n a: ({ href, children, className: linkClassName }: any) => {\n const isBroken = brokenLinks.includes(href);\n const isInternalDocLink =\n propCurrentPath !== undefined &&\n propCurrentPath !== null &&\n href &&\n !href.startsWith('http') &&\n !href.startsWith('#');\n\n if (isBroken) {\n return (\n <span className=\"text-ods-accent cursor-not-allowed\">\n {children}\n <sup className=\"ml-1 text-xs font-bold text-ods-attention-red-error\">[BROKEN]</sup>\n </span>\n );\n }\n\n if (isInternalDocLink && onInternalLinkClick) {\n const currentPath = propCurrentPath ?? '';\n return (\n <span\n className=\"text-ods-accent no-underline relative transition-colors duration-200 hover:after:w-full after:content-[''] after:absolute after:w-0 after:h-0.5 after:-bottom-0.5 after:left-0 after:bg-ods-accent after:transition-all after:duration-300 cursor-pointer\"\n onClick={async (e) => {\n e.preventDefault();\n e.stopPropagation();\n if (onResolveLink) {\n try {\n const result = await onResolveLink(href, currentPath);\n if (result.type === 'folder-no-readme' && result.action === 'expand_folder') {\n onInternalLinkClick(result.resolvedPath!, { expandFolder: true, fromInternalLink: true });\n } else if (result.type === 'not-found') {\n return;\n } else if (result.success && result.resolvedPath) {\n onInternalLinkClick(result.resolvedPath, { fromInternalLink: true });\n }\n } catch (error) {\n console.error('Error resolving link:', error);\n }\n } else {\n onInternalLinkClick(href, { fromInternalLink: true });\n }\n }}\n role=\"link\"\n tabIndex={0}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n (e.currentTarget as HTMLElement).click();\n }\n }}\n >\n {children}\n </span>\n );\n }\n\n return (\n <a\n href={href}\n className={`text-ods-accent no-underline relative transition-colors duration-200 hover:after:w-full after:content-[''] after:absolute after:w-0 after:h-0.5 after:-bottom-0.5 after:left-0 after:bg-ods-accent after:transition-all after:duration-300 ${linkClassName || ''}`}\n target={href?.startsWith('http') ? '_blank' : undefined}\n rel={href?.startsWith('http') ? 'noopener noreferrer' : undefined}\n >\n {children}\n </a>\n );\n },\n\n // --- images ---\n // Inline content image renderer. Used by blog posts, docs, AND chat\n // messages (where users may attach screenshots / photos via the +\n // attachment button).\n //\n // Sizing rules (2025-2026 best practice — Claude.ai, ChatGPT,\n // iMessage, Slack, Discord inline image patterns):\n //\n // - CAP max-width at 400px so inline images don't blow out the\n // message column on wide panels. Click-to-expand opens a\n // full-resolution modal for users who need detail.\n // - CAP max-height at 400px so portrait-orientation images\n // don't dominate vertical space (a 1000x3000 phone screenshot\n // would otherwise push the next message off-screen).\n // - Small images render at NATURAL pixel size — a 64x64\n // thumbnail stays 64x64, not stretched to fill the column.\n // - `object-contain` preserves aspect ratio when both dimensions\n // are constrained (long landscape, tall portrait).\n //\n // Implementation: Next.js `<Image>` — the project's canonical\n // image primitive. Gives us:\n // - WebP/AVIF format conversion for modern browsers (smaller\n // bytes for the same visual quality).\n // - Responsive `srcset` via the `sizes` prop (browser picks the\n // right variant for the viewport).\n // - Automatic lazy-loading (`loading=\"lazy\"` by default, mid-\n // page images skipped until they near the viewport).\n // - Automatic `decoding=\"async\"` so image decode doesn't block\n // paint.\n //\n // `width={400} height={400}` props are REQUIRED by Next.js\n // `<Image>` (non-`fill` mode throws without them) but they're\n // effectively a CEILING here, not the display size — the CSS\n // overrides (`w-auto h-auto max-w-full max-h-[400px]`) drive\n // the actual rendered size. The inline `style={{ width: 'auto',\n // height: 'auto' }}` is belt-and-suspenders: Next.js Image sets\n // matching HTML `width`/`height` attributes on the rendered\n // `<img>` and inline style wins over both HTML attributes AND\n // utility classes regardless of CSS-specificity surprises.\n //\n // Layout reservation trade-off: until image bytes arrive, the\n // browser may reserve a placeholder box up to 400x400 (the props'\n // intrinsic-ratio hint). Once loaded, the box collapses to the\n // natural size if smaller. This is the standard Next.js Image\n // behavior across the codebase — accepted for the optimizer +\n // responsive-srcset benefits. Chat attachments hosted in side\n // panels see this only on first render of a fresh attachment\n // (cached re-renders pop in without a perceptible shift).\n img: ({ src, alt }: any) => {\n if (!src || typeof src !== 'string' || src.trim() === '') return null;\n return (\n <Image\n src={src}\n alt={alt ?? 'No image available'}\n width={400}\n height={400}\n sizes=\"(max-width: 400px) 100vw, 400px\"\n className=\"max-w-full max-h-[400px] w-auto h-auto rounded-lg object-contain\"\n style={{ width: 'auto', height: 'auto' }}\n />\n );\n },\n\n // --- lists ---\n ul: ({ children }: any) => (\n <ul className=\"list-disc list-outside my-4 ml-8 space-y-2 text-ods-text-primary\">{children}</ul>\n ),\n ol: ({ children }: any) => (\n <ol className=\"list-decimal list-outside my-4 ml-8 space-y-2 text-ods-text-primary\">{children}</ol>\n ),\n li: ({ children }: any) => (\n <li className={cn('leading-relaxed pl-2', textSizes.li)}>{children}</li>\n ),\n\n // --- tables ---\n table: ({ children }: any) => (\n <div className=\"table-container my-6 overflow-x-auto\">\n <div className=\"min-w-full border rounded-lg border-ods-border bg-ods-card\">\n <table className=\"w-full table-fixed md:table-auto\">{children}</table>\n </div>\n </div>\n ),\n thead: ({ children }: any) => (\n <thead className=\"bg-ods-bg-secondary\">{children}</thead>\n ),\n th: ({ children }: any) => (\n <th className={cn('px-2 md:px-4 py-3 text-left font-semibold text-ods-accent border-r last:border-r-0 break-words border-ods-border', textSizes.th)}>\n {children}\n </th>\n ),\n td: ({ children }: any) => (\n <td className={cn('px-2 md:px-4 py-3 border-r last:border-r-0 border-b break-words whitespace-normal text-ods-text-primary border-ods-border', textSizes.td)}>\n {children}\n </td>\n ),\n\n // --- horizontal rule ---\n hr: () => <hr className=\"border-0 border-t my-8 border-ods-border\" />,\n\n // --- merge overrides ---\n ...componentOverrides,\n }), [makeHeading, brokenLinks, propCurrentPath, onInternalLinkClick, onResolveLink, componentOverrides, textSizes]);\n\n return (\n <div className={`simple-markdown-renderer ${className}`}>\n <style dangerouslySetInnerHTML={{ __html: mermaidStyles }} />\n <div className=\"content-wrapper max-w-none break-words\">\n <article className=\"prose prose-lg max-w-none\">\n <ReactMarkdown\n remarkPlugins={[remarkGfm, remarkBreaks, ...(additionalRemarkPlugins ?? [])]}\n rehypePlugins={[\n // ORDER MATTERS: rehype-raw parses the raw HTML embedded\n // in the source markdown into HAST nodes; rehypeStripUnsafe\n // then walks the HAST tree and drops XSS vectors (on*\n // event handlers, javascript: URLs, script/style/iframe-srcdoc,\n // data: URIs). Reversing the order would have nothing to\n // sanitize (raw HTML would still be strings).\n rehypeRaw,\n rehypeStripUnsafe,\n [rehypeHighlight, { detect: true, ignoreMissing: true }],\n ]}\n urlTransform={cardAwareUrlTransform}\n components={components}\n >\n {processedContent}\n </ReactMarkdown>\n </article>\n </div>\n </div>\n );\n};\n\n/**\n * Memoized so a parent re-render with UNCHANGED props (same `content` string,\n * same memoized `componentOverrides`/plugins) does NOT re-parse the markdown.\n * Re-parsing rebuilds the entire react-markdown subtree, which RE-MOUNTS any\n * embedded inline entity cards — closing their open menus and re-triggering\n * their fetch on every chat re-render (streaming chunk AND scroll). With\n * stable props the renderer bails, so completed messages' cards stay mounted.\n */\nexport const SimpleMarkdownRenderer = memo(SimpleMarkdownRendererImpl);\n","\"use client\"\n\nimport * as React from \"react\";\nimport Image from \"../../embed-shims/next-image\";\nimport { cn } from \"../../utils/cn\";\nimport { getFirstLastInitials } from \"../../utils/format\";\n\ninterface SquareAvatarProps extends React.HTMLAttributes<HTMLDivElement> {\n src?: string;\n alt?: string;\n size?: 'sm' | 'md' | 'lg' | 'xl';\n fallback?: string;\n variant?: 'square' | 'round';\n /** Override the initials-fallback styling (font size/color). Merged over the\n * defaults (`text-xs font-medium text-ods-text-primary`) via tailwind-merge,\n * so callers can shrink/recolor the initials for compact avatars. */\n initialsClassName?: string;\n}\n\nconst SquareAvatar = React.memo(React.forwardRef<HTMLDivElement, SquareAvatarProps>(\n ({ className, src, alt, size = 'md', fallback, variant = 'square', initialsClassName, ...props }, ref) => {\n const sizeClasses = {\n sm: 'h-8 w-8',\n md: 'h-10 w-10',\n lg: 'h-12 w-12',\n xl: 'h-16 w-16'\n };\n\n const sizePx = {\n sm: 32,\n md: 40,\n lg: 48,\n xl: 64\n };\n\n const variantClasses = {\n square: 'rounded-md',\n round: 'rounded-full'\n };\n\n return (\n <div\n className={cn(\n \"relative flex items-center justify-center shrink-0 overflow-hidden border border-ods-border bg-ods-bg\",\n sizeClasses[size],\n variantClasses[variant],\n className\n )}\n ref={ref}\n {...props}\n >\n <div className={cn(\n // Initials default to `--color-text-primary` (the old\n // `text-ods-text-primary` value) so they stay readable on the default\n // `bg-ods-bg` AND on the brand accent fills (`bg-ods-flamingo-pink`\n // for the current user, `bg-ods-flamingo-cyan` for Mingo). The color\n // resolves through `--ods-avatar-initials` with that fallback, so a\n // host themed with a custom avatar fill can override the var with a\n // contrast-correct value (e.g. `getReadableTextColor(accent)`) WITHOUT\n // regressing any avatar that leaves the var unset. A caller passing\n // its own `initialsClassName` text color still wins (tailwind-merge\n // keeps the later class).\n 'flex items-center justify-center text-xs font-medium text-[color:var(--ods-avatar-initials,var(--color-text-primary))]',\n initialsClassName,\n src && 'hidden'\n )}>\n {getFirstLastInitials(fallback || alt) || '?'}\n </div>\n {src && (\n <Image\n className=\"absolute inset-0 h-full w-full object-cover\"\n src={src}\n alt={alt || ''}\n width={sizePx[size]}\n height={sizePx[size]}\n onError={(e) => {\n e.currentTarget.style.display = 'none';\n const el = e.currentTarget.previousElementSibling as HTMLElement;\n if (el) el.classList.remove('hidden');\n }}\n />\n )}\n </div>\n )\n }\n))\nSquareAvatar.displayName = \"SquareAvatar\"\n\nexport { SquareAvatar };\n","\"use client\";\n\nimport * as DropdownMenuPrimitive from \"@radix-ui/react-dropdown-menu\";\nimport { useControllableState } from \"@radix-ui/react-use-controllable-state\";\nimport { Check } from \"lucide-react\";\nimport Link from \"../../embed-shims/next-link\";\nimport React, { useCallback } from \"react\";\nimport { Chevron02RightIcon, Ellipsis01Icon } from \"../icons-v2-generated\";\nimport { cn } from \"../../utils/cn\";\nimport { Button } from \"./button\";\nimport {\n\tDropdownMenu,\n\tDropdownMenuContent,\n\tDropdownMenuTrigger,\n} from \"./dropdown-menu\";\n\nexport interface ActionsMenuItemIconAction {\n\ticon: React.ReactNode;\n\t\"aria-label\": string;\n\tonClick?: () => void;\n\thref?: string;\n\topenInNewTab?: boolean;\n\tdisabled?: boolean;\n}\n\nexport interface ActionsMenuItem {\n\tid: string;\n\tlabel: string;\n\ticon?: React.ReactNode;\n\tonClick?: () => void;\n\tdisabled?: boolean;\n\ttype?: \"item\" | \"checkbox\" | \"submenu\" | \"separator\";\n\tchecked?: boolean;\n\t/**\n\t * Keep the dropdown open after this item is clicked instead of closing it\n\t * (e.g. multi-select add). Only affects ActionsMenuDropdown; `checkbox` and\n\t * `submenu` items always keep the menu open regardless. Defaults to closing.\n\t */\n\tcloseOnSelect?: boolean;\n\tsubmenu?: ActionsMenuItem[];\n\t/** Render the row in the error/destructive color (label + icon). */\n\tdanger?: boolean;\n\t/** Optional URL for navigation items */\n\thref?: string;\n\t/**\n\t * Optional secondary action — a 40px-wide button on the right of the row\n\t * with a vertical divider. The main row keeps its primary click target;\n\t * the secondary is independently clickable (e.g. \"open in new tab\").\n\t */\n\ticonAction?: ActionsMenuItemIconAction;\n}\n\nexport interface ActionsMenuGroup {\n\tid?: string;\n\titems: ActionsMenuItem[];\n\tseparator?: boolean;\n}\n\nexport interface ActionsMenuProps {\n\tgroups: ActionsMenuGroup[];\n\tclassName?: string;\n\tonItemClick?: (item: ActionsMenuItem) => void;\n}\n\ninterface MenuItemProps {\n\titem: ActionsMenuItem;\n\tonItemClick?: (item: ActionsMenuItem) => void;\n}\n\nconst ROW_CLASSES =\n\t\"flex flex-1 min-w-0 items-center gap-[var(--spacing-system-xsf)] p-[var(--spacing-system-s)] cursor-pointer transition-colors bg-ods-bg outline-none\";\nconst WRAPPER_CLASSES =\n\t\"relative flex items-stretch border-b border-ods-border last:border-b-0\";\n\nconst SECONDARY_ACTION_CLASSES =\n\t\"flex p-[var(--spacing-system-s)] shrink-0 items-center justify-center self-stretch border-l border-ods-border transition-colors hover:bg-ods-bg-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ods-focus [&_svg]:w-4 [&_svg]:h-4 md:[&_svg]:w-6 md:[&_svg]:h-6\";\n\nconst SecondaryAction: React.FC<{ action: ActionsMenuItemIconAction }> = ({ action }) => {\n\tconst handleClick = useCallback(\n\t\t(e: React.MouseEvent) => {\n\t\t\te.stopPropagation();\n\t\t\tif (action.disabled) {\n\t\t\t\te.preventDefault();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\taction.onClick?.();\n\t\t},\n\t\t[action],\n\t);\n\n\tconst classes = cn(\n\t\tSECONDARY_ACTION_CLASSES,\n\t\taction.disabled && \"cursor-not-allowed opacity-60 pointer-events-none\",\n\t);\n\n\tif (action.href) {\n\t\treturn (\n\t\t\t<Link\n\t\t\t\thref={action.href}\n\t\t\t\tprefetch={false}\n\t\t\t\ttarget={action.openInNewTab ? \"_blank\" : undefined}\n\t\t\t\trel={action.openInNewTab ? \"noopener noreferrer\" : undefined}\n\t\t\t\taria-label={action[\"aria-label\"]}\n\t\t\t\taria-disabled={action.disabled || undefined}\n\t\t\t\ttabIndex={action.disabled ? -1 : undefined}\n\t\t\t\tclassName={classes}\n\t\t\t\tonClick={handleClick}\n\t\t\t>\n\t\t\t\t{action.icon}\n\t\t\t</Link>\n\t\t);\n\t}\n\n\treturn (\n\t\t<button\n\t\t\ttype=\"button\"\n\t\t\taria-label={action[\"aria-label\"]}\n\t\t\tdisabled={action.disabled}\n\t\t\tclassName={classes}\n\t\t\tonClick={handleClick}\n\t\t>\n\t\t\t{action.icon}\n\t\t</button>\n\t);\n};\n\nconst MenuItem: React.FC<MenuItemProps> = ({ item, onItemClick }) => {\n\tconst activate = useCallback(() => {\n\t\tif (item.disabled) return;\n\t\tif (item.type === \"checkbox\") {\n\t\t\titem.onClick?.();\n\t\t\tonItemClick?.(item);\n\t\t\treturn;\n\t\t}\n\t\tif (item.type === \"submenu\") return;\n\t\titem.onClick?.();\n\t\tonItemClick?.(item);\n\t}, [item, onItemClick]);\n\n\tconst handleClick = useCallback(\n\t\t(e: React.MouseEvent) => {\n\t\t\te.stopPropagation();\n\t\t\te.preventDefault();\n\t\t\tactivate();\n\t\t},\n\t\t[activate],\n\t);\n\n\tconst handleKeyDown = useCallback(\n\t\t(e: React.KeyboardEvent) => {\n\t\t\tif (e.key !== \"Enter\" && e.key !== \" \") return;\n\t\t\te.preventDefault();\n\t\t\te.stopPropagation();\n\t\t\tactivate();\n\t\t},\n\t\t[activate],\n\t);\n\n\tconst handleLinkClick = useCallback(\n\t\t(e: React.MouseEvent<HTMLAnchorElement>) => {\n\t\t\tif (item.disabled) {\n\t\t\t\te.preventDefault();\n\t\t\t\te.stopPropagation();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\titem.onClick?.();\n\t\t\tonItemClick?.(item);\n\t\t},\n\t\t[item, onItemClick],\n\t);\n\n\tif (item.type === \"separator\") {\n\t\treturn <div className=\"bg-ods-system-greys-soft-grey h-1 w-full\" />;\n\t}\n\n\tconst itemClasses = cn(\n\t\tROW_CLASSES,\n\t\titem.disabled\n\t\t\t? \"text-ods-text-secondary cursor-not-allowed pointer-events-none opacity-60\"\n\t\t\t: \"text-ods-text-primary hover:bg-ods-bg-hover\",\n\t);\n\n\tconst subTriggerClasses = cn(\n\t\titemClasses,\n\t\t\"data-[state=open]:bg-ods-bg-active focus:bg-ods-bg-hover\",\n\t);\n\n\tconst renderAsLink =\n\t\t!!item.href && item.type !== \"submenu\" && item.type !== \"checkbox\";\n\n\tconst rowContent = (\n\t\t<>\n\t\t\t{item.icon && (\n\t\t\t\t<div\n\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\"w-4 h-4 md:w-6 md:h-6 flex-shrink-0 flex items-center justify-center\",\n\t\t\t\t\t\titem.danger && \"text-ods-error\",\n\t\t\t\t\t\titem.disabled && \"opacity-50\",\n\t\t\t\t\t)}\n\t\t\t\t>\n\t\t\t\t\t{item.icon}\n\t\t\t\t</div>\n\t\t\t)}\n\n\t\t\t<span\n\t\t\t\tclassName={cn(\n\t\t\t\t\t\"flex-1 text-h4 font-medium leading-6\",\n\t\t\t\t\titem.disabled\n\t\t\t\t\t\t? \"text-ods-text-secondary\"\n\t\t\t\t\t\t: item.danger\n\t\t\t\t\t\t\t? \"text-ods-error\"\n\t\t\t\t\t\t\t: \"text-ods-text-primary\",\n\t\t\t\t)}\n\t\t\t>\n\t\t\t\t{item.label}\n\t\t\t</span>\n\n\t\t\t{item.type === \"checkbox\" && (\n\t\t\t\t<div\n\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\"w-4 h-4 md:w-6 md:h-6 flex items-center justify-center rounded-md transition-colors\",\n\t\t\t\t\t\titem.checked\n\t\t\t\t\t\t\t? \"bg-ods-accent\"\n\t\t\t\t\t\t\t: \"border-2 border-ods-border bg-transparent\",\n\t\t\t\t\t)}\n\t\t\t\t>\n\t\t\t\t\t{item.checked && (\n\t\t\t\t\t\t<Check className=\"w-3 h-3 md:w-4 md:h-4 text-ods-text-on-accent\" strokeWidth={3} />\n\t\t\t\t\t)}\n\t\t\t\t</div>\n\t\t\t)}\n\n\t\t\t{item.type === \"submenu\" && (\n\t\t\t\t<Chevron02RightIcon className=\"w-4 h-4 md:w-6 md:h-6 text-ods-text-secondary\" />\n\t\t\t)}\n\t\t</>\n\t);\n\n\tif (renderAsLink && item.href) {\n\t\treturn (\n\t\t\t<div className={WRAPPER_CLASSES}>\n\t\t\t\t<Link\n\t\t\t\t\thref={item.href}\n\t\t\t\t\tprefetch={false}\n\t\t\t\t\tclassName={itemClasses}\n\t\t\t\t\tonClick={handleLinkClick}\n\t\t\t\t\taria-disabled={item.disabled}\n\t\t\t\t\ttabIndex={item.disabled ? -1 : undefined}\n\t\t\t\t>\n\t\t\t\t\t{rowContent}\n\t\t\t\t</Link>\n\t\t\t\t{item.iconAction && <SecondaryAction action={item.iconAction} />}\n\t\t\t</div>\n\t\t);\n\t}\n\n\tif (item.type === \"submenu\" && item.submenu) {\n\t\treturn (\n\t\t\t<div className={WRAPPER_CLASSES}>\n\t\t\t\t<DropdownMenuPrimitive.Sub>\n\t\t\t\t\t<DropdownMenuPrimitive.SubTrigger\n\t\t\t\t\t\tdisabled={item.disabled}\n\t\t\t\t\t\tclassName={subTriggerClasses}\n\t\t\t\t\t>\n\t\t\t\t\t\t{rowContent}\n\t\t\t\t\t</DropdownMenuPrimitive.SubTrigger>\n\t\t\t\t\t<DropdownMenuPrimitive.Portal>\n\t\t\t\t\t\t<DropdownMenuPrimitive.SubContent\n\t\t\t\t\t\t\tsideOffset={4}\n\t\t\t\t\t\t\tclassName=\"z-[1500] min-w-[256px] max-h-[var(--radix-popper-available-height)] bg-ods-bg border border-ods-border rounded-md shadow-xl overflow-y-auto p-0\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{item.submenu.map((subItem, index) => (\n\t\t\t\t\t\t\t\t<MenuItem\n\t\t\t\t\t\t\t\t\tkey={subItem.id || index}\n\t\t\t\t\t\t\t\t\titem={subItem}\n\t\t\t\t\t\t\t\t\tonItemClick={onItemClick}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t</DropdownMenuPrimitive.SubContent>\n\t\t\t\t\t</DropdownMenuPrimitive.Portal>\n\t\t\t\t</DropdownMenuPrimitive.Sub>\n\t\t\t\t{item.iconAction && <SecondaryAction action={item.iconAction} />}\n\t\t\t</div>\n\t\t);\n\t}\n\n\treturn (\n\t\t<div className={WRAPPER_CLASSES}>\n\t\t\t<div\n\t\t\t\trole=\"menuitem\"\n\t\t\t\ttabIndex={item.disabled ? -1 : 0}\n\t\t\t\taria-disabled={item.disabled}\n\t\t\t\tclassName={itemClasses}\n\t\t\t\tonClick={handleClick}\n\t\t\t\tonKeyDown={handleKeyDown}\n\t\t\t>\n\t\t\t\t{rowContent}\n\t\t\t</div>\n\t\t\t{item.iconAction && <SecondaryAction action={item.iconAction} />}\n\t\t</div>\n\t);\n};\n\nconst GroupSeparator: React.FC = () => (\n\t<div className=\"bg-ods-bg-surface h-[3px] w-full\" />\n);\n\nexport const ActionsMenu: React.FC<ActionsMenuProps> = ({\n\tgroups,\n\tclassName = \"\",\n\tonItemClick,\n}) => {\n\treturn (\n\t\t<div\n\t\t\tclassName={`relative min-w-[256px] max-h-[var(--radix-popper-available-height)] bg-ods-bg border border-ods-border rounded-md shadow-lg overflow-y-auto ${className}`}\n\t\t>\n\t\t\t{groups.map((group, groupIndex) => {\n\t\t\t\tconst groupKey = group.id || group.items.map((i) => i.id).join(\"|\");\n\t\t\t\treturn (\n\t\t\t\t\t<React.Fragment key={groupKey}>\n\t\t\t\t\t\t{group.items.map((item, itemIndex) => (\n\t\t\t\t\t\t\t<MenuItem\n\t\t\t\t\t\t\t\tkey={item.id || `${groupKey}-${itemIndex}`}\n\t\t\t\t\t\t\t\titem={item}\n\t\t\t\t\t\t\t\tonItemClick={onItemClick}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t))}\n\t\t\t\t\t\t{group.separator && groupIndex < groups.length - 1 && (\n\t\t\t\t\t\t\t<GroupSeparator />\n\t\t\t\t\t\t)}\n\t\t\t\t\t</React.Fragment>\n\t\t\t\t);\n\t\t\t})}\n\t\t</div>\n\t);\n};\n\nexport interface ActionsMenuDropdownProps extends ActionsMenuProps {\n\ttrigger?: React.ReactNode;\n\t/** Replace the entire default trigger button. When set, rendered directly as the DropdownMenuTrigger child. */\n\tcustomTrigger?: React.ReactNode;\n\ttriggerAriaLabel?: string;\n\ttriggerClassName?: string;\n\tcontentClassName?: string;\n\talign?: \"start\" | \"center\" | \"end\";\n\tside?: \"top\" | \"right\" | \"bottom\" | \"left\";\n\tsideOffset?: number;\n\t/** Controlled open state. Pair with `onOpenChange`. Uncontrolled by default. */\n\topen?: boolean;\n\t/** Open-state change handler (also fires when an item closes the menu). */\n\tonOpenChange?: (open: boolean) => void;\n\t/** Forwarded to the dropdown content — e.g. `e.preventDefault()` to stop\n\t * Radix returning focus (and its focus ring) to the trigger on close. */\n\tonCloseAutoFocus?: (event: Event) => void;\n}\n\nexport const ActionsMenuDropdown: React.FC<ActionsMenuDropdownProps> = ({\n\tgroups,\n\tonItemClick,\n\tclassName,\n\ttrigger,\n\tcustomTrigger,\n\ttriggerAriaLabel = \"More actions\",\n\ttriggerClassName,\n\tcontentClassName,\n\talign = \"end\",\n\tside = \"bottom\",\n\tsideOffset = 6,\n\topen: openProp,\n\tonOpenChange,\n\tonCloseAutoFocus,\n}) => {\n\tconst [open = false, setOpen] = useControllableState({\n\t\tprop: openProp,\n\t\tdefaultProp: false,\n\t\tonChange: onOpenChange,\n\t});\n\n\tconst handleItemClick = useCallback(\n\t\t(item: ActionsMenuItem) => {\n\t\t\tonItemClick?.(item);\n\t\t\tif (\n\t\t\t\titem.type !== \"checkbox\" &&\n\t\t\t\titem.type !== \"submenu\" &&\n\t\t\t\titem.closeOnSelect !== false\n\t\t\t) {\n\t\t\t\tsetOpen(false);\n\t\t\t}\n\t\t},\n\t\t[onItemClick, setOpen],\n\t);\n\n\treturn (\n\t\t<DropdownMenu open={open} onOpenChange={setOpen} modal={false}>\n\t\t\t<DropdownMenuTrigger asChild>\n\t\t\t\t{customTrigger ?? (\n\t\t\t\t\t<Button\n\t\t\t\t\t\tvariant=\"outline\"\n\t\t\t\t\t\tsize=\"icon\"\n\t\t\t\t\t\taria-label={triggerAriaLabel}\n\t\t\t\t\t\tclassName={\n\t\t\t\t\t\t\ttriggerClassName ||\n\t\t\t\t\t\t\t\"bg-ods-card border-ods-border hover:bg-ods-bg-hover flex items-center justify-center focus-visible:ring-0\"\n\t\t\t\t\t\t}\n\t\t\t\t\t\tleftIcon={\n\t\t\t\t\t\t\ttrigger ?? (\n\t\t\t\t\t\t\t\t<Ellipsis01Icon size={24} className=\"text-ods-text-primary\" />\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t}\n\t\t\t\t\t/>\n\t\t\t\t)}\n\t\t\t</DropdownMenuTrigger>\n\t\t\t<DropdownMenuContent\n\t\t\t\talign={align}\n\t\t\t\tside={side}\n\t\t\t\tsideOffset={sideOffset}\n\t\t\t\tonCloseAutoFocus={onCloseAutoFocus}\n\t\t\t\tclassName={cn(\n\t\t\t\t\t\"p-0 border-0 bg-transparent shadow-none overflow-visible\",\n\t\t\t\t\tcontentClassName,\n\t\t\t\t)}\n\t\t\t>\n\t\t\t\t<ActionsMenu\n\t\t\t\t\tgroups={groups}\n\t\t\t\t\tonItemClick={handleItemClick}\n\t\t\t\t\tclassName={className}\n\t\t\t\t/>\n\t\t\t</DropdownMenuContent>\n\t\t</DropdownMenu>\n\t);\n};\n\nexport default ActionsMenu;\n","import * as React from 'react';\nimport { useLayoutEffect } from '@radix-ui/react-use-layout-effect';\n\n// Prevent bundlers from trying to optimize the import\nconst useInsertionEffect: typeof useLayoutEffect =\n (React as any)[' useInsertionEffect '.trim().toString()] || useLayoutEffect;\n\ntype ChangeHandler<T> = (state: T) => void;\ntype SetStateFn<T> = React.Dispatch<React.SetStateAction<T>>;\n\ninterface UseControllableStateParams<T> {\n prop?: T | undefined;\n defaultProp: T;\n onChange?: ChangeHandler<T>;\n caller?: string;\n}\n\nexport function useControllableState<T>({\n prop,\n defaultProp,\n onChange = () => {},\n caller,\n}: UseControllableStateParams<T>): [T, SetStateFn<T>] {\n const [uncontrolledProp, setUncontrolledProp, onChangeRef] = useUncontrolledState({\n defaultProp,\n onChange,\n });\n const isControlled = prop !== undefined;\n const value = isControlled ? prop : uncontrolledProp;\n\n // OK to disable conditionally calling hooks here because they will always run\n // consistently in the same environment. Bundlers should be able to remove the\n // code block entirely in production.\n /* eslint-disable react-hooks/rules-of-hooks */\n if (process.env.NODE_ENV !== 'production') {\n const isControlledRef = React.useRef(prop !== undefined);\n React.useEffect(() => {\n const wasControlled = isControlledRef.current;\n if (wasControlled !== isControlled) {\n const from = wasControlled ? 'controlled' : 'uncontrolled';\n const to = isControlled ? 'controlled' : 'uncontrolled';\n console.warn(\n `${caller} is changing from ${from} to ${to}. Components should not switch from controlled to uncontrolled (or vice versa). Decide between using a controlled or uncontrolled value for the lifetime of the component.`,\n );\n }\n isControlledRef.current = isControlled;\n }, [isControlled, caller]);\n }\n /* eslint-enable react-hooks/rules-of-hooks */\n\n const setValue = React.useCallback<SetStateFn<T>>(\n (nextValue) => {\n if (isControlled) {\n const value = isFunction(nextValue) ? nextValue(prop) : nextValue;\n if (value !== prop) {\n onChangeRef.current?.(value);\n }\n } else {\n setUncontrolledProp(nextValue);\n }\n },\n [isControlled, prop, setUncontrolledProp, onChangeRef],\n );\n\n return [value, setValue];\n}\n\nfunction useUncontrolledState<T>({\n defaultProp,\n onChange,\n}: Omit<UseControllableStateParams<T>, 'prop'>): [\n Value: T,\n setValue: React.Dispatch<React.SetStateAction<T>>,\n OnChangeRef: React.RefObject<ChangeHandler<T> | undefined>,\n] {\n const [value, setValue] = React.useState(defaultProp);\n const prevValueRef = React.useRef(value);\n\n const onChangeRef = React.useRef(onChange);\n useInsertionEffect(() => {\n onChangeRef.current = onChange;\n }, [onChange]);\n\n React.useEffect(() => {\n if (prevValueRef.current !== value) {\n onChangeRef.current?.(value);\n prevValueRef.current = value;\n }\n }, [value, prevValueRef]);\n\n return [value, setValue, onChangeRef];\n}\n\nfunction isFunction(value: unknown): value is (...args: any[]) => any {\n return typeof value === 'function';\n}\n","import * as React from 'react';\n\n/**\n * On the server, React emits a warning when calling `useLayoutEffect`.\n * This is because neither `useLayoutEffect` nor `useEffect` run on the server.\n * We use this safe version which suppresses the warning by replacing it with a noop on the server.\n *\n * See: https://reactjs.org/docs/hooks-reference.html#uselayouteffect\n */\nconst useLayoutEffect = globalThis?.document ? React.useLayoutEffect : () => {};\n\nexport { useLayoutEffect };\n","import * as React from 'react';\nimport { useEffectEvent } from '@radix-ui/react-use-effect-event';\n\ntype ChangeHandler<T> = (state: T) => void;\n\ninterface UseControllableStateParams<T> {\n prop: T | undefined;\n defaultProp: T;\n onChange: ChangeHandler<T> | undefined;\n caller: string;\n}\n\ninterface AnyAction {\n type: string;\n}\n\nconst SYNC_STATE = Symbol('RADIX:SYNC_STATE');\n\ninterface SyncStateAction<T> {\n type: typeof SYNC_STATE;\n state: T;\n}\n\nexport function useControllableStateReducer<T, S extends {}, A extends AnyAction>(\n reducer: (prevState: S & { state: T }, action: A) => S & { state: T },\n userArgs: UseControllableStateParams<T>,\n initialState: S,\n): [S & { state: T }, React.Dispatch<A>];\n\nexport function useControllableStateReducer<T, S extends {}, I, A extends AnyAction>(\n reducer: (prevState: S & { state: T }, action: A) => S & { state: T },\n userArgs: UseControllableStateParams<T>,\n initialArg: I,\n init: (i: I & { state: T }) => S,\n): [S & { state: T }, React.Dispatch<A>];\n\nexport function useControllableStateReducer<T, S extends {}, A extends AnyAction>(\n reducer: (prevState: S & { state: T }, action: A) => S & { state: T },\n userArgs: UseControllableStateParams<T>,\n initialArg: any,\n init?: (i: any) => Omit<S, 'state'>,\n): [S & { state: T }, React.Dispatch<A>] {\n const { prop: controlledState, defaultProp, onChange: onChangeProp, caller } = userArgs;\n const isControlled = controlledState !== undefined;\n\n const onChange = useEffectEvent(onChangeProp);\n\n // OK to disable conditionally calling hooks here because they will always run\n // consistently in the same environment. Bundlers should be able to remove the\n // code block entirely in production.\n /* eslint-disable react-hooks/rules-of-hooks */\n if (process.env.NODE_ENV !== 'production') {\n const isControlledRef = React.useRef(controlledState !== undefined);\n React.useEffect(() => {\n const wasControlled = isControlledRef.current;\n if (wasControlled !== isControlled) {\n const from = wasControlled ? 'controlled' : 'uncontrolled';\n const to = isControlled ? 'controlled' : 'uncontrolled';\n console.warn(\n `${caller} is changing from ${from} to ${to}. Components should not switch from controlled to uncontrolled (or vice versa). Decide between using a controlled or uncontrolled value for the lifetime of the component.`,\n );\n }\n isControlledRef.current = isControlled;\n }, [isControlled, caller]);\n }\n /* eslint-enable react-hooks/rules-of-hooks */\n\n type InternalState = S & { state: T };\n const args: [InternalState] = [{ ...initialArg, state: defaultProp }];\n if (init) {\n // @ts-expect-error\n args.push(init);\n }\n\n const [internalState, dispatch] = React.useReducer(\n (state: InternalState, action: A | SyncStateAction<T>): InternalState => {\n if (action.type === SYNC_STATE) {\n return { ...state, state: action.state };\n }\n\n const next = reducer(state, action);\n if (isControlled && !Object.is(next.state, state.state)) {\n onChange(next.state);\n }\n return next;\n },\n ...args,\n );\n\n const uncontrolledState = internalState.state;\n const prevValueRef = React.useRef(uncontrolledState);\n React.useEffect(() => {\n if (prevValueRef.current !== uncontrolledState) {\n prevValueRef.current = uncontrolledState;\n if (!isControlled) {\n onChange(uncontrolledState);\n }\n }\n }, [uncontrolledState, prevValueRef, isControlled]);\n\n const state = React.useMemo(() => {\n const isControlled = controlledState !== undefined;\n if (isControlled) {\n return { ...internalState, state: controlledState };\n }\n\n return internalState;\n }, [internalState, controlledState]);\n\n React.useEffect(() => {\n // Sync internal state for controlled components so that reducer is called\n // with the correct state values\n if (isControlled && !Object.is(controlledState, internalState.state)) {\n dispatch({ type: SYNC_STATE, state: controlledState });\n }\n }, [controlledState, internalState.state, isControlled]);\n\n return [state, dispatch as React.Dispatch<A>];\n}\n","/**\n * Pure helpers for the new-tab decision + anchor attribute pair used\n * by chat-rendered links.\n *\n * After the navigation unification (ChatCardLoader pre-resolves\n * `chatRef.url` via `resolveSourceRowCTA` + `resolveHrefForRuntime`,\n * and `ChatCardNavWrap` handles every primary click via `onClickCapture`\n * → `handleChatNavClick`), the per-card wrapper only needs\n * `{href, target, rel}` on its inner `<a>` so modifier-click /\n * hover-preview / copy-link work natively.\n *\n * `computeIsNewTab` is the SINGLE source of the new-tab rule across\n * inline cards, source chips, and `NavLinkAnchorViaRuntime`. Do NOT\n * inline the `runtime.navigation.mode === 'embed'` short-circuit + the\n * `decideNewTab` fallback chain anywhere else.\n *\n * Rules:\n * - `embed` mode → new-tab, EXCEPT for same-origin absolute hrefs. The chat\n * lives on the embedder origin and most content lives on the hub (→ new\n * tab), but an embedder may host a few content types in-app (e.g. OpenFrame\n * serves releases / roadmap / guides under `/help-center`). Those are\n * emitted as ABSOLUTE same-origin URLs by the embedder's `composeContentUrl`;\n * keep them same-tab so they soft-nav in-app instead of opening a redundant\n * new tab on the same origin. Relative hrefs stay new-tab — in embed mode\n * they're hub-relative (absolutized against `defaultContentOrigin`).\n * - `host` mode → defer to the runtime's `decideNewTab` callback\n * (cross-platform → new-tab) or the lib's default.\n */\n\nimport type { ChatRuntime } from '../../../contexts/chat-runtime-context'\nimport { decideNewTab as libDecideNewTab } from './decide-new-tab'\n\n/** An ABSOLUTE `http(s)` URL on the current page's origin. Relative hrefs return\n * false (in embed mode they're hub-relative, not in-app), as do cross-origin\n * and non-http URLs. SSR-safe: no `window` → false. */\nfunction isSameOriginAbsoluteHref(href: string): boolean {\n if (typeof window === 'undefined') return false\n if (!/^https?:\\/\\//i.test(href)) return false\n try {\n return new URL(href).origin === window.location.origin\n } catch {\n return false\n }\n}\n\nexport function computeIsNewTab(\n runtime: ChatRuntime,\n href: string | null | undefined,\n targetPlatform: string | null,\n): boolean {\n if (!href) return false\n if (runtime.navigation.mode === 'embed') return !isSameOriginAbsoluteHref(href)\n return (\n runtime.navigation.decideNewTab?.({ href, targetPlatform }) ??\n libDecideNewTab({\n href,\n targetPlatform,\n currentSource: runtime.source ?? '',\n })\n )\n}\n\n/**\n * Returns the `target` + `rel` attribute pair for an inner `<a>` based\n * on the new-tab decision. Spread directly into JSX:\n * <a href={href} {...newTabAnchorAttrs(isNewTab)} />\n *\n * Single helper so chat anchors stay consistent — new-tab links always\n * pair `_blank` with `noopener noreferrer` (defense against window.opener\n * tab-nabbing) and same-tab links don't render either attribute.\n */\nexport function newTabAnchorAttrs(isNewTab: boolean): {\n target?: '_blank'\n rel?: 'noopener noreferrer'\n} {\n return isNewTab\n ? { target: '_blank' as const, rel: 'noopener noreferrer' as const }\n : {}\n}\n\n/**\n * Combine `href` + new-tab attrs into a card's `anchorProps` slot.\n * Returns `undefined` (not a falsy object) when there's no URL, so card\n * components can branch on `anchorProps != null` to render either a\n * clickable `<a>` or a static `<span>`.\n *\n * Replaces the repeated inline\n * `chatRef.url ? { href: chatRef.url, ...newTabAnchorAttrs(isNewTab) } : undefined`\n * pattern across the 7 dispatcher wrappers.\n */\nexport function buildAnchorProps(\n href: string | null | undefined,\n isNewTab: boolean,\n): { href: string; target?: '_blank'; rel?: 'noopener noreferrer' } | undefined {\n return href ? { href, ...newTabAnchorAttrs(isNewTab) } : undefined\n}\n","import Link from '../../../embed-shims/next-link'\nimport type { EntityAuthor } from '../../../types/entity-author'\nimport { SquareAvatar } from '../../ui/square-avatar'\nimport { formatDate, nameInitials } from '../../../utils/format'\n\n/**\n * Author + publication metadata, rendered as the SAME 2-or-N-cell grid pattern\n * the release detail page uses.\n *\n * Each cell sits inside a single bordered/rounded container, separated by\n * `border-r` dividers — byte-identical visual contract to the release page.\n *\n * Cell taxonomy:\n * - `<EntityMetadataValueCell>` — top: large value (`text-h4` uppercase),\n * bottom: small label (`DM_Sans` 14px secondary). Used for Type / Status\n * / Date.\n * - `<EntityMetadataAuthorCell>` — `<SquareAvatar>` + name + label. Used\n * for the Author column.\n *\n * `<EntityAuthorCard>` is the convenience composer for the common\n * \"Published + Author\" pair. Pass `publishedAt` to get both cells; omit it\n * to get just the Author card (single-cell mode used by surfaces with no\n * separate publication-date concept).\n *\n * Renders null when `author` is null/empty unless `renderEmptyAuthor` is set.\n */\n/**\n * Documented empty-author placeholder for CHAT CARD surfaces (\"—\" name,\n * \"Unknown\" role). The release detail page deliberately keeps its legacy\n * empty rendering instead (\"Unknown Author\"/\"Author\" via a null-field stub\n * — see release-detail-page.tsx) — two documented empty styles, one cell.\n */\nexport const EMPTY_AUTHOR_PLACEHOLDER = {\n full_name: '—',\n avatar_url: null,\n job_title: 'Unknown',\n} as const\n\nexport interface EntityAuthorCardProps {\n author: EntityAuthor | null | undefined\n /** Role label rendered under the name. Defaults to \"Author\". Override\n * to e.g. \"Presenter\" / \"Contributor\" if semantics differ. */\n roleLabel?: string\n /** Link target for the author name (e.g. the public author page). The\n * HOST computes it (route availability is host knowledge) — absent ⇒\n * plain text, exactly the prior render. */\n authorHref?: string\n /** Optional publication date. When provided, the component renders as a\n * 2-cell grid (Published | Author). When omitted, only the Author cell\n * renders inside the same bordered container. */\n publishedAt?: string | Date | null\n /** Label for the Published cell. Defaults to \"Published\". */\n publishedLabel?: string\n /**\n * Extra value cells inserted into the metadata grid BEFORE the\n * Published cell. Each item renders as an `<EntityMetadataValueCell>`\n * with a large `text-h4` value and a small `DM_Sans` 14px label.\n *\n * Use for entity-specific labels (e.g. onboarding-guide section/step,\n * webinar host, customer-interview customer). The grid auto-sizes via\n * `grid-cols-N`.\n */\n extraCells?: Array<{ value: string; label: string; uppercase?: boolean }>\n /** When true, render the author cell even when `author?.full_name` is\n * missing — using the `EMPTY_AUTHOR_PLACEHOLDER` shape above. Used by\n * catalog grids that must keep a fixed shape so skeleton alignment\n * holds. Defaults to false. */\n renderEmptyAuthor?: boolean\n className?: string\n}\n\n\n/**\n * Single value cell — top: large `text-h4` value (uppercase), bottom: small\n * `DM_Sans` 14px secondary label.\n */\nexport function EntityMetadataValueCell({\n value,\n label,\n className,\n uppercase = true,\n}: {\n value: string\n label: string\n className?: string\n uppercase?: boolean\n}) {\n return (\n <div className={`bg-ods-card p-4 flex flex-col gap-3 ${className ?? ''}`}>\n <div className=\"flex flex-col gap-0\">\n <p className=\"text-h4 text-ods-text-primary\">\n {uppercase ? value.toLocaleUpperCase() : value}\n </p>\n <p className=\"font-['DM_Sans'] font-medium text-[14px] leading-[20px] text-ods-text-secondary\">\n {label}\n </p>\n </div>\n </div>\n )\n}\n\n/**\n * Author cell — `<SquareAvatar>` + name + role label. The containing border /\n * divider is the caller's responsibility — this is just the cell content.\n */\nexport function EntityMetadataAuthorCell({\n author,\n roleLabel = 'Author',\n authorHref,\n className,\n}: {\n author: NonNullable<EntityAuthorCardProps['author']>\n roleLabel?: string\n authorHref?: string\n className?: string\n}) {\n // Trimmed real name gates the LINK — a placeholder/unknown label must\n // never render as a clickable author (an authorHref passed alongside an\n // empty name would link \"Unknown Author\").\n const trimmedName = typeof author.full_name === 'string' ? author.full_name.trim() : ''\n const fullName = trimmedName || 'Unknown Author'\n return (\n <div className={`bg-ods-card p-4 flex items-center gap-3 ${className ?? ''}`}>\n <SquareAvatar\n src={author.avatar_url || ''}\n alt={fullName}\n fallback={nameInitials(fullName, '')}\n size=\"md\"\n variant=\"round\"\n />\n <div className=\"flex flex-col gap-0 flex-1 min-w-0\">\n {/* title = full-name tooltip on truncation (carried over from the\n release page's cell when it adopted this shared one). */}\n <p className=\"text-h3 tracking-[-0.36px] text-ods-text-primary truncate\" title={fullName}>\n {authorHref && trimmedName ? (\n <Link href={authorHref} className=\"hover:text-ods-accent transition-colors\">\n {fullName}\n </Link>\n ) : (\n fullName\n )}\n </p>\n <p className=\"font-['DM_Sans'] font-medium text-[14px] leading-[20px] text-ods-text-secondary\">\n {author.job_title || roleLabel}\n </p>\n </div>\n </div>\n )\n}\n\nexport function EntityAuthorCard({\n author,\n roleLabel = 'Author',\n authorHref,\n publishedAt,\n publishedLabel = 'Published',\n extraCells,\n renderEmptyAuthor = false,\n className,\n}: EntityAuthorCardProps) {\n const hasAuthor = !!author?.full_name\n if (!hasAuthor && !renderEmptyAuthor) return null\n const effectiveAuthor = hasAuthor ? (author as NonNullable<EntityAuthorCardProps['author']>) : EMPTY_AUTHOR_PLACEHOLDER\n\n // Resolve the date label. `formatDate` returns \"Invalid Date\" for\n // unparseable inputs; collapse that to empty so the cell is hidden, not\n // literal text.\n const formatted = publishedAt ? formatDate(publishedAt as Date | string) : ''\n const dateLabel = formatted === 'Invalid Date' ? '' : formatted\n\n const showDateCell = !!dateLabel\n const extras = extraCells ?? []\n // Total cell count = extras + (optional Published) + Author. Map to\n // an explicit tailwind `md:grid-cols-N` class string so JIT picks it\n // up at build time.\n const totalCells = extras.length + (showDateCell ? 1 : 0) + 1\n const gridColsClass =\n totalCells >= 4 ? 'md:grid-cols-4'\n : totalCells === 3 ? 'md:grid-cols-3'\n : totalCells === 2 ? 'md:grid-cols-2'\n : 'md:grid-cols-1'\n\n // Helper — every cell EXCEPT the last needs the dividers (bottom on\n // mobile, right on desktop). The last cell (Author) closes the grid\n // without a trailing divider so the rounded corner stays clean.\n const dividerClass = 'border-b md:border-b-0 md:border-r border-ods-border'\n\n return (\n <div\n className={`grid grid-cols-1 ${gridColsClass} border border-ods-border rounded-md overflow-hidden w-full ${className ?? ''}`}\n >\n {extras.map((cell, i) => (\n <EntityMetadataValueCell\n key={`${cell.label}-${i}`}\n value={cell.value}\n label={cell.label}\n uppercase={cell.uppercase ?? true}\n className={dividerClass}\n />\n ))}\n {showDateCell && (\n <EntityMetadataValueCell\n value={dateLabel}\n label={publishedLabel}\n uppercase={false}\n className={dividerClass}\n />\n )}\n <EntityMetadataAuthorCell author={effectiveAuthor} roleLabel={roleLabel} authorHref={authorHref} />\n </div>\n )\n}\n","'use client'\n\nimport React from 'react'\n\ninterface BlogImagePlaceholderProps {\n /** Cover-image URL. The hub passes a `useOgPlaceholder(title, category)`\n * result; embedders pass their own pre-resolved URL. When null, the\n * component renders nothing. */\n imageUrl: string | null\n /** Used for the `alt` attribute. */\n title: string\n className?: string\n}\n\n/**\n * Pure presentation wrapper for a cover-image / OG-placeholder fallback.\n *\n * Outer must be inline-content-model so this placeholder is HTML-valid\n * when rendered inside a markdown `<p>` (e.g. via the chat shell's\n * compact `BlogCard` fallback). `<span className=\"block\">` keeps the\n * same visual behavior as the prior `<div>` while satisfying the\n * phrasing-content constraint of its parent.\n *\n * If the `imageUrl` itself 404s (cold cache, transient failure), the\n * `onError` handler hides the broken-image icon so the parent's\n * `bg-ods-bg` shows through cleanly. Same recovery pattern every\n * cover-image render path uses.\n */\nexport function BlogImagePlaceholder({\n imageUrl,\n title,\n className = '',\n}: BlogImagePlaceholderProps) {\n if (!imageUrl) return null\n\n return (\n <span className={`relative block w-full h-full overflow-hidden bg-ods-bg ${className}`}>\n {/* eslint-disable-next-line @next/next/no-img-element -- this is a\n dynamically-generated placeholder image with a query string;\n next/image's loader configuration adds nothing here. */}\n <img\n src={imageUrl}\n alt={`Cover image for ${title}`}\n className=\"block w-full h-full object-contain\"\n loading=\"lazy\"\n onError={(e) => {\n (e.currentTarget as HTMLImageElement).style.display = 'none'\n }}\n />\n </span>\n )\n}\n","\"use client\";\n\n/**\n * <Video> — single source of truth for every public video surface\n * across every Flamingo platform consumer of this lib.\n *\n * One component, three sources, three layouts. Replaces and deletes\n * the previous lib primitives:\n *\n * - `<VideoPlayer>` (react-player wrapper, ~900 LOC custom controls)\n * - `<YouTubeEmbed>` (separate lite-youtube facade)\n *\n * Routing (`kind` discriminant, default `'auto'`):\n *\n * kind=\"youtube\" → inline lite-youtube facade (poster + click→iframe)\n * kind=\"file\" → <MuxPlayer> (HLS + MP4 + Mux Data + CMCD all in one)\n * kind=\"auto\" → strict URL parse:\n * bare 11-char id → youtube\n * YouTube hostname → youtube\n * anything else → file\n *\n * `<MuxPlayer>` handles both `.m3u8` (HLS via hls.js, native on Safari)\n * AND plain `.mp4` (uses the underlying `<video>` element). One component,\n * both paths — no internal \"HLS vs MP4\" branch needed. Captions are\n * rendered as native `<track>` children when `captionsUrl` is passed.\n *\n * Layouts:\n * layout=\"centered\" → max-w-3xl centered wrapper. Detail-page surface.\n * layout=\"fill\" → absolute inset-0 w-full h-full. Carousel slides.\n * layout=\"native\" → intrinsic aspect ratio. Bites grid, blog cards.\n */\n\nimport React, { useEffect, useMemo, useRef, useState } from 'react';\nimport MuxPlayer from '@mux/mux-player-react';\nimport { PlayIcon } from '../icons-v2-generated/media-playback/play-icon';\nimport { fetchPriorityProp } from '../../utils/fetch-priority';\n\n// =============================================================================\n// Suppress Google Cast SDK loading (CSP-friendly)\n// =============================================================================\n//\n// Why: MuxPlayer is built on `media-chrome`, which is built on `castable-video`.\n// `castable-video`'s `loadCastFramework()` UNCONDITIONALLY injects a\n// `<script src=\"https://www.gstatic.com/cv/.../cast_sender.js?loadCastFramework=1\">`\n// whenever Chrome is detected. That script then internally loads further\n// scripts from `http://www.gstatic.com/eureka/clank/...` and\n// `http://www.gstatic.com/cast/sdk/libs/...` — over **HTTP, not HTTPS** —\n// which can never pass any reasonable CSP. Result: every video render\n// emits 3+ \"Loading the script ... violates CSP\" errors in the browser\n// console.\n//\n// The loader has a single early-exit: `if (globalThis.chrome?.cast)\n// return;`. So we make `chrome.cast` truthy (with `isAvailable: false`\n// so existing apps that consult that flag still see \"no cast\") BEFORE\n// MuxPlayer's `castable-mixin` runs. Module-level code in this file\n// executes during the import that brings `MuxPlayer` into the bundle —\n// safely before any instance mounts.\n//\n// We don't use Chromecast anywhere in the hub. If we ever do, replace\n// this block with explicit cast initialization at the call site.\nif (typeof window !== 'undefined') {\n const w = window as unknown as { chrome?: { cast?: unknown } };\n if (!w.chrome?.cast) {\n w.chrome = { ...(w.chrome ?? {}), cast: { isAvailable: false } };\n }\n}\n\n// =============================================================================\n// Suppress benign \"Media Chrome: No style sheet found ...\" warning.\n// =============================================================================\n//\n// Why: MuxPlayer's UI is built on `media-chrome`, which uses custom\n// elements with shadow-DOM `<style>` tags. `media-chrome`'s internal\n// `insertCSSRule()` helper queries `style.sheet` during the element's\n// `connectedCallback`. When the player mounts inside a React portal\n// (the chat panel renders `<Video>` inside Radix's `<Dialog.Portal>`),\n// the element is moved across DOM trees BEFORE the browser hydrates\n// the shadow `<style>`'s `.sheet`. The helper then logs:\n// \"Media Chrome: No style sheet found on style tag of #shadow-root (open)\"\n// and returns a no-op style shim. The warning is purely cosmetic —\n// playback, controls, and theming all render correctly because the\n// shadow stylesheet does hydrate on the next paint.\n//\n// Until upstream `media-chrome` either retries on the next microtask\n// or downgrades the warning (tracked in their issue tracker), patch\n// `console.warn` once at module load to drop only THIS exact string.\n// All other `console.warn` calls pass through unchanged.\n//\n// Why patch instead of opting out: `@mux/mux-player-react` exposes no\n// prop to disable internal style queries, importing CSS explicitly\n// doesn't seed the shadow-root stylesheets (they're per-element), and\n// the warning fires before any consumer can intercept the element.\nif (typeof window !== 'undefined' && typeof console !== 'undefined') {\n const w = window as unknown as { __MEDIA_CHROME_WARN_PATCHED__?: boolean };\n if (!w.__MEDIA_CHROME_WARN_PATCHED__) {\n w.__MEDIA_CHROME_WARN_PATCHED__ = true;\n const MEDIA_CHROME_NO_STYLESHEET_PREFIX = 'Media Chrome: No style sheet found on style tag of';\n const originalWarn = console.warn.bind(console);\n console.warn = (...args: unknown[]): void => {\n if (typeof args[0] === 'string' && args[0].startsWith(MEDIA_CHROME_NO_STYLESHEET_PREFIX)) {\n return;\n }\n originalWarn(...args);\n };\n }\n}\n\n// =============================================================================\n// URL classifiers (private — `<Video>` is the only consumer)\n// =============================================================================\n\nconst YT_HOSTS = new Set([\n 'youtube.com', 'www.youtube.com', 'm.youtube.com',\n 'youtu.be',\n 'youtube-nocookie.com', 'www.youtube-nocookie.com',\n]);\n\n/** Strict YouTube URL detection — parses the URL and checks the hostname. */\nfunction isYouTubeUrl(url: string): boolean {\n try {\n return YT_HOSTS.has(new URL(url, 'http://placeholder.local').hostname.toLowerCase());\n } catch {\n return false;\n }\n}\n\n// `youtube.com/(embed|v|shorts)/<id>` — anchored, no `.*`, ReDoS-safe.\nconst YT_PATH_RE = /^\\/(?:embed|v|shorts)\\/([^/]+)\\/?$/;\n\n// Bare 11-char YouTube id — base62 alphabet `[A-Za-z0-9_-]`.\n// Anchored, no `.*`, ReDoS-safe; rejects anything that contains `/` or `:`.\nconst BARE_YT_ID_RE = /^[A-Za-z0-9_-]{11}$/;\n\n/**\n * Extract the YouTube video id from any common URL shape OR from a\n * bare 11-char id (carousels and some admin shapes pass that form\n * directly).\n *\n * Uses strict `URL` parsing + an anchored pathname regex — NOT the\n * legacy `.*v=` pattern that CodeQL flagged for polynomial-time\n * backtracking on adversarial input like `youtube.com/watch?`\n * repeated N times. Bare-id fallback uses an anchored character-class\n * regex so it can never ReDoS either.\n *\n * Exported so admin tooling and carousel thumbnail logic can validate\n * a URL without rendering the full `<Video>` component.\n */\nexport function extractYouTubeId(url: string): string | null {\n if (!url) return null;\n // Bare-id form first — `new URL('dQw4w9WgXcQ')` throws, so this MUST\n // run before the URL parse. The 11-char anchored regex rejects URLs\n // (which always contain `:` or `/`).\n if (BARE_YT_ID_RE.test(url)) return url;\n let u: URL;\n // Match `isYouTubeUrl`'s relative-safe parsing — a base URL with a\n // placeholder origin lets us handle protocol-relative inputs (`//youtube.com/...`)\n // and protocol-less inputs (`youtube.com/...`) without throwing.\n try { u = new URL(url, 'http://placeholder.local'); } catch { return null; }\n if (!YT_HOSTS.has(u.hostname.toLowerCase())) return null;\n // `youtu.be/<id>` — id is the first non-empty path segment.\n if (u.hostname.toLowerCase().endsWith('youtu.be')) {\n return u.pathname.split('/').filter(Boolean)[0] ?? null;\n }\n // `youtube.com/watch?v=<id>` — query parameter.\n const v = u.searchParams.get('v');\n if (v) return v;\n // `youtube.com/(embed|v|shorts)/<id>` — anchored pathname match.\n const m = u.pathname.match(YT_PATH_RE);\n return m ? m[1] : null;\n}\n\n// =============================================================================\n// Props\n// =============================================================================\n\nexport type VideoLayout = 'centered' | 'fill' | 'native';\n\ninterface VideoCommonProps {\n /** Layout wrapper. Detail pages pass `\"centered\"`. Default `\"native\"`. */\n layout?: VideoLayout;\n /** Poster / thumbnail. */\n poster?: string | null;\n /** Mute by default — for autoplay carousels. */\n muted?: boolean;\n /** LCP hint — YouTube facade poster gets `fetchpriority=\"high\"`. */\n priority?: boolean;\n /** Tailwind classes applied to the underlying player root. */\n className?: string;\n /** Accessible label (used as YT facade title; ignored for file branch). */\n title?: string;\n /**\n * YouTube-only: hide YT player chrome (controls, info, fullscreen, related\n * videos, keyboard shortcuts). Used for marketing/landing-page embeds that\n * want a minimal look. No-op for file (MP4/HLS) branches.\n */\n minimalControls?: boolean;\n}\n\ninterface VideoFileProps extends VideoCommonProps {\n kind: 'file';\n url: string;\n /**\n * SRT raw content. Deprecated: pass `captionsUrl` (VTT) instead.\n * Native `<track>` requires a URL; raw SRT can't be rendered without\n * a custom overlay. Setting this without `captionsUrl` is a no-op\n * with a dev warning.\n */\n srtContent?: string | null;\n /** HTTPS URL to a VTT captions file. Rendered as a native `<track>`. */\n captionsUrl?: string | null;\n}\n\ninterface VideoYouTubeProps extends VideoCommonProps {\n kind: 'youtube';\n /** Either a full YT URL or just the video id. */\n url: string;\n}\n\ninterface VideoAutoProps extends VideoCommonProps {\n kind?: 'auto';\n url: string;\n srtContent?: string | null;\n captionsUrl?: string | null;\n}\n\nexport type VideoProps = VideoFileProps | VideoYouTubeProps | VideoAutoProps;\n\n// =============================================================================\n// Component\n// =============================================================================\n\nexport function Video(props: VideoProps): React.ReactElement | null {\n const url = props.url;\n if (!url) return null;\n\n const effectiveKind = resolveKind(props, url);\n const layout = props.layout ?? 'native';\n\n const inner =\n effectiveKind === 'youtube' ? (\n <YouTubeFacade\n url={url}\n title={props.title}\n priority={props.priority}\n className={props.className}\n minimalControls={props.minimalControls}\n />\n ) : (\n <FilePlayer\n url={url}\n poster={props.poster}\n muted={props.muted}\n srtContent={'srtContent' in props ? props.srtContent : null}\n captionsUrl={'captionsUrl' in props ? props.captionsUrl : null}\n className={props.className}\n />\n );\n\n return wrapWithLayout(inner, layout);\n}\n\n// =============================================================================\n// Internals — never imported by call sites; `<Video>` is the only entry.\n// =============================================================================\n\n/**\n * Resolve the rendering branch. `'auto'` (or no `kind`) inspects the URL:\n * YouTube host (or a bare 11-char video id) → 'youtube', anything else →\n * 'file' (HLS / MP4 both handled by MuxPlayer). Type-safe — no `as` casts.\n */\nfunction resolveKind(props: VideoProps, url: string): 'youtube' | 'file' {\n if ('kind' in props) {\n if (props.kind === 'youtube') return 'youtube';\n if (props.kind === 'file') return 'file';\n // kind === 'auto' falls through to URL-based detection\n }\n // Bare 11-char YouTube id — use the same anchored regex as\n // `extractYouTubeId` so both code paths agree on which strings are\n // bare ids vs. URLs (avoids the regression where `videos/clip`\n // length-11 strings were mis-routed to YouTube).\n if (BARE_YT_ID_RE.test(url)) return 'youtube';\n return isYouTubeUrl(url) ? 'youtube' : 'file';\n}\n\nfunction wrapWithLayout(\n inner: React.ReactElement,\n layout: VideoLayout,\n): React.ReactElement {\n switch (layout) {\n case 'centered':\n // `aspect-video` (16:9) reserves the box from first paint so MuxPlayer\n // doesn't flicker tiny→full while video metadata loads. Both branches\n // are sized to fill 100% of this container (MuxPlayer via `style`,\n // YouTube facade via internal `paddingBottom: 56.25%` which compounds\n // harmlessly inside an already-16:9 box). `rounded-lg overflow-hidden\n // border border-ods-border` is on the wrapper (not the inner) so BOTH\n // branches end up with the same rounded card look — YouTube's facade\n // also paints its own internal rounded-lg on the button + iframe,\n // matching the outer radius (so the corners stay sharp through the\n // poster → iframe transition); MuxPlayer renders flat and inherits\n // the wrapper's rounded corners via `overflow-hidden` clipping.\n return (\n <div className=\"flex justify-center w-full\">\n <div className=\"w-full max-w-3xl aspect-video rounded-lg overflow-hidden border border-ods-border\">{inner}</div>\n </div>\n );\n case 'fill':\n return <div className=\"absolute inset-0 w-full h-full\">{inner}</div>;\n case 'native':\n default:\n // `native` callers (LazyBite in `<VideoBitesDisplay>`, blog cards) are\n // expected to provide their own aspect-ratio container so the layout\n // primitive doesn't override portrait/square/landscape bites with 16:9.\n return inner;\n }\n}\n\n// -----------------------------------------------------------------------------\n// File branch — MuxPlayer (handles both .m3u8 HLS and plain .mp4)\n// -----------------------------------------------------------------------------\n\ninterface FilePlayerProps {\n url: string;\n poster?: string | null;\n muted?: boolean;\n srtContent?: string | null;\n captionsUrl?: string | null;\n className?: string;\n}\n\nfunction FilePlayer({\n url,\n poster,\n muted,\n srtContent,\n captionsUrl,\n className,\n}: FilePlayerProps): React.ReactElement {\n // Raw SRT text is unusable without a custom overlay — and we just deleted\n // the 900-LOC custom-controls layer that owned that overlay. Consumers\n // pass `captionsUrl` (the API-side VTT conversion) alongside `srtContent`\n // anyway. Warn in dev if the deprecated prop is the only one supplied\n // so a single-prop call site doesn't silently lose captions.\n if (process.env.NODE_ENV !== 'production' && srtContent && !captionsUrl) {\n // eslint-disable-next-line no-console\n console.warn(\n '[Video] srtContent supplied without captionsUrl — captions will not render. ' +\n 'Pass captionsUrl (the VTT URL) instead; raw SRT text overlays are no longer supported.',\n );\n }\n\n return (\n <MuxPlayer\n src={url}\n poster={poster || undefined}\n streamType=\"on-demand\"\n playsInline\n muted={muted}\n preferCmcd=\"header\"\n // MuxPlayer's built-in default is `#fa50b5` (Mux brand pink) — when\n // its `--media-accent-color` resolves to nothing the player falls\n // through to that hardcoded pink. The `var(--ods-accent,\n // var(--color-accent-primary))` chain hits the platform-aware\n // ODS token first, then the semantic accent alias if `--ods-accent`\n // is ever undefined on a `data-app-type` we haven't themed yet.\n // NEVER let Mux pink leak onto a non-Flamingo platform.\n accentColor=\"var(--ods-accent, var(--color-accent-primary))\"\n className={className}\n // Fill the wrapping aspect-ratio container instead of MuxPlayer's\n // intrinsic size. Without this, MuxPlayer renders at its default\n // dimensions before video metadata loads, then grows to its\n // metadata-derived size — that's the \"starts super small and\n // flickers and grows\" CLS we're killing. With `aspect-video` on\n // the centered wrapper and `width/height: 100%` here, the box is\n // 16:9 from first paint and stays put.\n style={{ width: '100%', height: '100%' }}\n >\n {captionsUrl ? (\n <track\n kind=\"captions\"\n src={captionsUrl}\n srcLang=\"en\"\n label=\"English\"\n default\n />\n ) : null}\n </MuxPlayer>\n );\n}\n\n// -----------------------------------------------------------------------------\n// YouTube facade — inlined lite-youtube-embed pattern\n// -----------------------------------------------------------------------------\n\ninterface YouTubeFacadeProps {\n url: string;\n title?: string;\n priority?: boolean;\n className?: string;\n minimalControls?: boolean;\n}\n\nfunction YouTubeFacade({\n url,\n title = 'YouTube Video',\n priority,\n className,\n minimalControls,\n}: YouTubeFacadeProps): React.ReactElement | null {\n // `extractYouTubeId` handles both bare 11-char ids AND full URLs in a\n // single call site, so the resolution logic lives in exactly one place.\n const videoId = extractYouTubeId(url);\n if (!videoId) return null;\n\n return <YouTubeFacadeInner videoId={videoId} title={title} priority={priority} className={className} minimalControls={minimalControls} />;\n}\n\ninterface YouTubeFacadeInnerProps {\n videoId: string;\n title: string;\n priority?: boolean;\n className?: string;\n minimalControls?: boolean;\n}\n\nconst YT_NOCOOKIE_ORIGIN = 'https://www.youtube-nocookie.com';\n\n// YouTube IFrame Player API state codes — documented integers.\n// https://developers.google.com/youtube/iframe_api_reference#Playback_status\nconst YT_STATE_ENDED = 0;\nconst YT_STATE_PLAYING = 1;\n\n// Sub-second delay before we blur the iframe after PLAYING. Zero would\n// cancel YouTube's mount-time \"controls visible\" intro flash entirely\n// (jarring); ~1s lets the user briefly see playback started, then we\n// kick YouTube's internal idle timer by removing DOM focus from the\n// iframe. Net result: controls fade ~1s after playback begins,\n// matching the user-locked target.\nconst YT_PLAYING_BLUR_DELAY_MS = 1000;\n\ninterface YouTubeInfoDeliveryMessage {\n event?: string;\n info?: { playerState?: number };\n}\n\nfunction YouTubeFacadeInner({\n videoId,\n title,\n priority,\n className,\n minimalControls,\n}: YouTubeFacadeInnerProps): React.ReactElement {\n const [activated, setActivated] = useState(false);\n const iframeRef = useRef<HTMLIFrameElement | null>(null);\n\n // Embed URL + poster URLs only change when `videoId` or `minimalControls`\n // do — memoize so we don't rebuild URLSearchParams on every render.\n //\n // `enablejsapi=1` opens the postMessage state channel we subscribe to\n // below — without it, YouTube ignores `event:listening` messages and\n // we can't detect PLAYING / ENDED to drive the auto-hide accelerator.\n //\n // `origin=<parent-page-origin>` is REQUIRED when `enablejsapi=1` is set.\n // Without it, the YouTube widget inside the iframe defaults its\n // `postMessage` targetOrigin to its OWN origin (`youtube-nocookie.com`)\n // when emitting state-change events back to the parent. The browser\n // then drops every message and logs:\n // \"Failed to execute 'postMessage' on 'DOMWindow': The target origin\n // provided ('https://www.youtube-nocookie.com') does not match the\n // recipient window's origin ('https://www.<our-site>')\"\n // Setting `origin` tells the widget the real parent host so it sends\n // with the matching targetOrigin. Documented in YouTube's IFrame Player\n // API reference (developers.google.com/youtube/iframe_api_reference).\n // SSR-safe: the URL is rebuilt client-side in the same useMemo on\n // hydration when `window` becomes available; the first SSR pass emits\n // the URL without `origin` (no jsapi traffic yet — no iframe mounted).\n const { embedUrl, posterJpg, posterWebp } = useMemo(() => {\n const params = new URLSearchParams({\n autoplay: '1',\n rel: '0',\n modestbranding: '1',\n playsinline: '1',\n enablejsapi: '1',\n });\n if (typeof window !== 'undefined') {\n params.set('origin', window.location.origin);\n }\n if (minimalControls) {\n params.set('controls', '0');\n params.set('fs', '0');\n params.set('iv_load_policy', '3');\n params.set('cc_load_policy', '0');\n params.set('disablekb', '1');\n }\n return {\n embedUrl: `${YT_NOCOOKIE_ORIGIN}/embed/${videoId}?${params.toString()}`,\n posterJpg: `https://i.ytimg.com/vi/${videoId}/mqdefault.jpg`,\n posterWebp: `https://i.ytimg.com/vi_webp/${videoId}/mqdefault.webp`,\n };\n }, [videoId, minimalControls]);\n\n // ---------------------------------------------------------------------------\n // YouTube control-fade accelerator (user-locked target: ~1s).\n //\n // YouTube's native idle timer for the bottom control bar is 5–10s when\n // the iframe holds DOM focus. The IFrame Player API has no public\n // `hideControls` command (full `func` list verified — none expose\n // visibility). The one legal lever from outside the iframe is\n // `iframe.blur()`, which kicks YouTube into its post-focus idle path\n // (~2s minimum).\n //\n // Subscribe to YouTube's state channel via the documented lite-mode\n // postMessage handshake — no full `iframe_api.js` library needed:\n // <https://developers.google.com/youtube/iframe_api_reference>.\n //\n // - PLAYING (1) arrives once autoplay kicks in. Wait ~1s (so the user\n // briefly sees that the player started), then blur the iframe so\n // YouTube's idle timer fires immediately.\n //\n // - ENDED (0) → tear down the iframe. Playback is already over so\n // there's nothing to interrupt, and removing the iframe kills the\n // residual \"More videos\" suggestion grid YouTube leaves on screen.\n //\n // PAUSED (2) is deliberately unhandled — the user paused on purpose,\n // leave YouTube's UI alone so they can resume.\n //\n // Playback is NEVER stopped by anything in this facade. Outside-click,\n // Escape, tab-switch — all no-ops. The only state flip is on natural\n // end-of-video (ENDED).\n // ---------------------------------------------------------------------------\n useEffect(() => {\n if (!activated) return;\n const iframe = iframeRef.current;\n if (!iframe) return;\n\n function subscribe() {\n iframe?.contentWindow?.postMessage(\n '{\"event\":\"listening\"}',\n YT_NOCOOKIE_ORIGIN,\n );\n }\n\n iframe.addEventListener('load', subscribe);\n subscribe();\n\n let blurTimer: ReturnType<typeof setTimeout> | null = null;\n\n function handleMessage(event: MessageEvent) {\n if (event.origin !== YT_NOCOOKIE_ORIGIN) return;\n if (typeof event.data !== 'string') return;\n let payload: YouTubeInfoDeliveryMessage | null = null;\n try {\n payload = JSON.parse(event.data);\n } catch {\n return;\n }\n if (!payload || payload.event !== 'infoDelivery') return;\n const state = payload.info?.playerState;\n if (typeof state !== 'number') return;\n\n if (state === YT_STATE_PLAYING) {\n if (blurTimer !== null) return;\n blurTimer = setTimeout(() => {\n blurTimer = null;\n iframeRef.current?.blur();\n }, YT_PLAYING_BLUR_DELAY_MS);\n return;\n }\n if (state === YT_STATE_ENDED) {\n setActivated(false);\n }\n }\n\n window.addEventListener('message', handleMessage);\n return () => {\n iframe.removeEventListener('load', subscribe);\n window.removeEventListener('message', handleMessage);\n if (blurTimer !== null) clearTimeout(blurTimer);\n };\n }, [activated]);\n\n const wrapperClass = `relative w-full ${className ?? ''}`;\n const wrapperStyle = { paddingBottom: '56.25%' as const };\n\n if (activated) {\n return (\n <div className={wrapperClass} style={wrapperStyle}>\n <iframe\n ref={iframeRef}\n src={embedUrl}\n allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share\"\n allowFullScreen\n title={title}\n className=\"absolute inset-0 w-full h-full border-0 rounded-lg\"\n />\n </div>\n );\n }\n\n return (\n <div className={wrapperClass} style={wrapperStyle}>\n <button\n type=\"button\"\n aria-label={`Play: ${title}`}\n onClick={() => setActivated(true)}\n className=\"group absolute inset-0 p-0 m-0 border border-ods-border rounded-lg overflow-hidden bg-ods-card cursor-pointer\"\n >\n <picture>\n <source type=\"image/webp\" srcSet={posterWebp} />\n <img\n src={posterJpg}\n alt={title}\n loading=\"lazy\"\n // React 18 wants lowercase (`fetchpriority` DOM attribute);\n // React 19 wants camelCase (`fetchPriority` prop). Detect at\n // module load and spread the right shape so both runtimes\n // render the DOM attribute cleanly with no console warnings.\n {...fetchPriorityProp(priority)}\n decoding={priority ? 'sync' : 'async'}\n className=\"absolute inset-0 w-full h-full object-cover\"\n />\n </picture>\n <div className=\"absolute inset-0 flex items-center justify-center bg-ods-bg-inverse bg-opacity-20 transition-opacity duration-200 group-hover:bg-opacity-30\">\n <span className=\"flex items-center justify-center w-16 h-16 rounded-full bg-ods-accent text-ods-text-on-accent shadow-lg transition-transform duration-200 group-hover:scale-110\">\n <PlayIcon size={24} color=\"currentColor\" className=\"ml-1\" />\n </span>\n </div>\n </button>\n </div>\n );\n}\n","\"use client\";\n\n/**\n * Aspect-ratio tab + grouping primitives for video grids.\n *\n * Used by `<VideoBitesDisplay>` and by app-level admin editors\n * (VideoBitesEditor, VideoLibraryGrid) so portrait / square / landscape\n * clips render in cohesive groups.\n *\n * Why these live here in the lib (not the hub): `<VideoBitesDisplay>`\n * was moved into the lib for SSoT, and it depends on these primitives.\n * The hub's admin editors re-export from here.\n */\n\nimport {\n Tabs,\n TabsList,\n TabsTrigger,\n TabsContent,\n} from '../ui/tabs';\nimport type { VideoTeaser } from '../../types/video-processing';\n\n/**\n * Vizard clip extraction aspect ratios. Mirrors `lib/types/aspect-ratio.ts`\n * in the hub — narrow on purpose. If the hub's `VizardAspectRatio` adds a\n * value, mirror it here.\n */\nexport type VizardAspectRatio = '9:16' | '16:9' | '1:1';\n\n/**\n * Extended VideoTeaser with aspect_ratio metadata from Vizard.\n * The lib `VideoTeaser` is the canonical type but doesn't include\n * `aspect_ratio` (stored in JSONB, preserved through spreads).\n * Import this when you need the ratio at compile time.\n */\nexport interface VideoTeaserWithRatio extends VideoTeaser {\n aspect_ratio?: VizardAspectRatio;\n confidence?: number;\n viral_reason?: string;\n start_time_ms?: number;\n end_time_ms?: number;\n}\n\n/** Ratio category used for grid layout and tab grouping. */\nexport type RatioCategory = 'portrait' | 'square' | 'landscape';\n\n// Shared tab trigger class — matches `<EntityVideoSection>`'s pattern.\nconst TAB_TRIGGER_CLASS =\n 'rounded-none border-b-2 border-transparent data-[state=active]:border-ods-accent data-[state=active]:bg-transparent data-[state=active]:shadow-none px-4 py-2 text-sm text-ods-text-secondary data-[state=active]:text-ods-text-primary';\n\n/** Grid class for each aspect ratio (admin editors — narrower columns). */\nexport const RATIO_GRID_CLASS: Record<RatioCategory, string> = {\n portrait: 'grid grid-cols-2 md:grid-cols-3 gap-4',\n square: 'grid grid-cols-2 md:grid-cols-3 gap-4',\n landscape: 'grid grid-cols-1 md:grid-cols-2 gap-4',\n};\n\n/** Grid class for public display (wider grids on detail pages). */\nexport const RATIO_DISPLAY_GRID_CLASS: Record<RatioCategory, string> = {\n portrait: 'grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4',\n square: 'grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4',\n landscape: 'grid grid-cols-1 md:grid-cols-2 gap-6',\n};\n\nconst RATIO_TAB_CONFIG: { key: RatioCategory; label: string }[] = [\n { key: 'portrait', label: 'Portrait 9:16' },\n { key: 'square', label: 'Square 1:1' },\n { key: 'landscape', label: 'Landscape 16:9' },\n];\n\ninterface RatioTabsProps {\n groups: Record<RatioCategory, { count: number; render: () => React.ReactNode }>;\n defaultTab?: RatioCategory;\n className?: string;\n}\n\n/**\n * RatioTabs — shared aspect-ratio tab wrapper.\n *\n * Only renders tabs that have content. `forceMount` + `data-[state=inactive]:hidden`\n * keeps inactive tabs in the DOM so switching back doesn't scroll-jump.\n */\nexport function RatioTabs({\n groups,\n defaultTab,\n className = '',\n}: RatioTabsProps) {\n const activeTabs = RATIO_TAB_CONFIG.filter(t => groups[t.key].count > 0);\n\n // If only one tab has content, don't show tabs.\n if (activeTabs.length <= 1) {\n const active = activeTabs[0];\n return active ? <>{groups[active.key].render()}</> : null;\n }\n\n const firstTab =\n defaultTab && groups[defaultTab].count > 0 ? defaultTab : activeTabs[0].key;\n\n return (\n <Tabs defaultValue={firstTab} className={`w-full ${className}`}>\n <TabsList className=\"inline-flex justify-start rounded-none bg-transparent h-auto p-0 gap-0 mb-2\">\n {activeTabs.map(t => (\n <TabsTrigger key={t.key} value={t.key} className={TAB_TRIGGER_CLASS}>\n {t.label} ({groups[t.key].count})\n </TabsTrigger>\n ))}\n </TabsList>\n {activeTabs.map(t => (\n <TabsContent\n key={t.key}\n value={t.key}\n forceMount\n className=\"data-[state=inactive]:hidden\"\n >\n {groups[t.key].render()}\n </TabsContent>\n ))}\n </Tabs>\n );\n}\n\n/**\n * Detect aspect ratio from a Vizard ratio string, falling back to\n * inferring from width/height if the string is missing or unknown.\n * Default: portrait (`'9:16'`).\n */\nexport function detectAspectRatio(\n ratioString?: string,\n width?: number,\n height?: number,\n): VizardAspectRatio {\n if (ratioString === '16:9') return '16:9';\n if (ratioString === '1:1') return '1:1';\n if (ratioString === '9:16') return '9:16';\n if (width && height) {\n if (Math.abs(width - height) < Math.min(width, height) * 0.1) return '1:1';\n if (width > height) return '16:9';\n }\n return '9:16';\n}\n\n/** Map a `VizardAspectRatio` to its `RatioCategory` for grouping. */\nexport function ratioToCategory(ratio: VizardAspectRatio): RatioCategory {\n if (ratio === '16:9') return 'landscape';\n if (ratio === '1:1') return 'square';\n return 'portrait';\n}\n\n/**\n * Group items by aspect ratio into portrait / square / landscape buckets.\n * `hasMultiple` is true when 2+ buckets are non-empty (drives tab vs. flat\n * grid rendering downstream).\n */\nexport function groupByAspectRatio<T>(\n items: T[],\n getAspectRatio: (item: T) => VizardAspectRatio,\n): {\n portrait: T[];\n square: T[];\n landscape: T[];\n hasMultiple: boolean;\n} {\n const portrait: T[] = [];\n const square: T[] = [];\n const landscape: T[] = [];\n for (const item of items) {\n const cat = ratioToCategory(getAspectRatio(item));\n if (cat === 'landscape') landscape.push(item);\n else if (cat === 'square') square.push(item);\n else portrait.push(item);\n }\n const filled = [portrait, square, landscape].filter(a => a.length > 0).length;\n return { portrait, square, landscape, hasMultiple: filled > 1 };\n}\n","\"use client\";\n\nimport React, { useMemo } from 'react';\nimport { Card } from '../ui/card';\nimport { useNearViewport } from '../../hooks/use-near-viewport';\nimport type { VideoTeaser } from '../../types/video-processing';\nimport { Video } from './video';\nimport { SECTION_HEADING_CLASS } from '../layout/page-heading';\nimport {\n RatioTabs,\n groupByAspectRatio,\n detectAspectRatio,\n ratioToCategory,\n RATIO_DISPLAY_GRID_CLASS,\n type VideoTeaserWithRatio,\n type RatioCategory,\n} from './video-ratio-tabs';\n\n/**\n * <VideoBitesDisplay> — public grid of short video bites grouped by\n * aspect ratio.\n *\n * Goes through `<Video>` for every clip — bites are no longer carved\n * out to raw `<VideoPlayer>`. The previous carve-out existed because\n * react-player's hls.js bundle was ~80KB and short clips didn't need\n * adaptive bitrate. With MuxPlayer, the player loads its HLS engine\n * lazily — for a plain MP4 source it's a thin shell around `<video>`,\n * so the carve-out costs more in complexity than it saves in bytes.\n *\n * `LazyBite` defers mount until the wrapper enters the IO `500px`\n * margin so off-screen bites don't even render their player. Same\n * `useNearViewport` singleton used everywhere else in the lib.\n */\n\n// =============================================================================\n// LazyBite — viewport-gated wrapper for video bite cards\n// =============================================================================\n\n/** Aspect-ratio-aware placeholder prevents CLS across the grid. */\nconst RATIO_TO_CSS_ASPECT: Record<RatioCategory, string> = {\n portrait: '9 / 16',\n square: '1 / 1',\n landscape: '16 / 9',\n};\n\ninterface LazyBiteProps {\n /** Aspect ratio of the wrapped bite — placeholder keeps layout stable. */\n ratio: RatioCategory;\n /** Card rendered once the wrapper enters (within `500px` of) the viewport. */\n children: React.ReactNode;\n}\n\n/**\n * Defers mounting an off-screen video bite until it scrolls within\n * ~500px of the viewport. Renders an aspect-ratio-matched placeholder\n * beforehand so layout stays stable.\n *\n * Placeholder bg matches the wrapped `<Card>` background (`bg-ods-card`)\n * so the swap-in is visually seamless and avoids a flash on hydration.\n */\nfunction LazyBite({ ratio, children }: LazyBiteProps) {\n const { ref, isNear } = useNearViewport<HTMLDivElement>('500px');\n\n return (\n <div ref={ref} style={{ aspectRatio: RATIO_TO_CSS_ASPECT[ratio] }}>\n {isNear ? children : <div className=\"w-full h-full bg-ods-card rounded-md\" />}\n </div>\n );\n}\n\n// =============================================================================\n// Public component\n// =============================================================================\n\nexport interface VideoBitesDisplayProps {\n /** Array of video bites/teasers to display. */\n bites: VideoTeaser[];\n /** Title for the section. */\n title?: string;\n /** Whether to filter to only show published bites. Default `true`. */\n filterPublished?: boolean;\n /** Whether to show the title section heading. Default `true`. */\n showTitle?: boolean;\n}\n\n/**\n * Unified video-bites grid.\n *\n * Groups by aspect ratio when multiple ratios are present, otherwise\n * renders a flat grid. Each bite mounts lazily via `LazyBite` so\n * off-screen players don't cost the page.\n */\nexport function VideoBitesDisplay({\n bites,\n title = 'Video Highlights',\n filterPublished = true,\n showTitle = true,\n}: VideoBitesDisplayProps) {\n const grouped = useMemo(() => {\n const filtered = filterPublished ? bites.filter(b => b.published) : bites;\n\n const sorted = [...filtered].sort((a, b) => {\n if (!a.created_at && !b.created_at) return 0;\n if (!a.created_at) return 1;\n if (!b.created_at) return -1;\n return new Date(b.created_at).getTime() - new Date(a.created_at).getTime();\n });\n\n return groupByAspectRatio(sorted, b =>\n detectAspectRatio((b as VideoTeaserWithRatio).aspect_ratio),\n );\n }, [bites, filterPublished]);\n\n const totalCount = grouped.portrait.length + grouped.square.length + grouped.landscape.length;\n if (totalCount === 0) return null;\n\n return (\n <div className=\"flex flex-col gap-6 w-full min-w-0\">\n {showTitle && (\n <h2 className={`${SECTION_HEADING_CLASS} break-words`}>\n {title}\n </h2>\n )}\n\n {grouped.hasMultiple ? (\n <RatioTabs\n groups={{\n portrait: {\n count: grouped.portrait.length,\n render: () => <BiteGrid bites={grouped.portrait} ratio=\"portrait\" />,\n },\n square: {\n count: grouped.square.length,\n render: () => <BiteGrid bites={grouped.square} ratio=\"square\" />,\n },\n landscape: {\n count: grouped.landscape.length,\n render: () => <BiteGrid bites={grouped.landscape} ratio=\"landscape\" />,\n },\n }}\n />\n ) : (\n <BiteGrid\n bites={\n grouped.portrait.length > 0\n ? grouped.portrait\n : grouped.square.length > 0\n ? grouped.square\n : grouped.landscape\n }\n ratio={\n grouped.portrait.length > 0\n ? 'portrait'\n : grouped.square.length > 0\n ? 'square'\n : 'landscape'\n }\n />\n )}\n </div>\n );\n}\n\n// =============================================================================\n// Internals\n// =============================================================================\n\n/**\n * Renders a grid of bite cards with ratio-appropriate column layout.\n * Each card is wrapped in `LazyBite` so off-screen bites don't mount\n * their player until they scroll near the viewport.\n */\nfunction BiteGrid({ bites, ratio }: { bites: VideoTeaser[]; ratio: RatioCategory }) {\n return (\n <div className={RATIO_DISPLAY_GRID_CLASS[ratio]}>\n {bites.map((bite, index) => (\n <LazyBite key={bite.url || index} ratio={ratio}>\n <VideoBiteCard url={bite.url} title={bite.title} thumbnailUrl={bite.thumbnail_url} />\n </LazyBite>\n ))}\n </div>\n );\n}\n\ninterface VideoBiteCardProps {\n url: string;\n title?: string | null;\n thumbnailUrl?: string | null;\n}\n\n/**\n * Individual bite card — routes through `<Video>` so the SSoT player\n * is the only video primitive in the lib.\n *\n * Layout: `LazyBite` sets the OUTER `aspectRatio` (portrait/square/landscape),\n * but `<Card>` between LazyBite and `<Video>` has no intrinsic height, so\n * we wrap `<Video>` in `layout=\"fill\"` + an explicit `relative` parent so\n * the player fills the bite's aspect box from first paint. Otherwise\n * MuxPlayer renders at its intrinsic default size and grows once metadata\n * loads — the same CLS that hits the centered layout.\n */\nfunction VideoBiteCard({ url, title, thumbnailUrl }: VideoBiteCardProps) {\n return (\n <Card className=\"overflow-hidden border border-ods-border bg-ods-card hover:border-ods-accent transition-colors flex flex-col h-full\">\n <div className=\"relative flex-1 min-h-0\">\n <Video url={url} poster={thumbnailUrl || undefined} layout=\"fill\" />\n </div>\n {title && (\n <div className=\"p-4\">\n <p className=\"text-h4 text-ods-text-primary line-clamp-2\" title={title}>{title}</p>\n </div>\n )}\n </Card>\n );\n}\n\nexport { VideoBiteCard };\n","\"use client\";\n\nimport { ComponentType } from 'react';\nimport {\n Tabs,\n TabsList,\n TabsTrigger,\n TabsContent,\n} from '../ui/tabs';\nimport type { VideoTeaser } from '../../types/video-processing';\nimport { Video } from './video';\nimport { VideoBitesDisplay } from './video-bites-display';\nimport { SECTION_HEADING_CLASS } from '../layout/page-heading';\n\n/**\n * <EntityVideoSection> — public detail-page video block.\n *\n * Tabbed Full Video / Highlights when both exist, plus optional\n * markdown summary + video bites grid. The actual video rendering\n * (YouTube facade, Mux HLS, MP4 fallback) is delegated to `<Video>` —\n * the single source of truth.\n *\n * YouTube takes precedence over the uploaded video when both\n * `youtubeUrl` and `mainVideoUrl` are present. That precedence is\n * resolved here (in the section wrapper) rather than inside `<Video>`,\n * so the underlying primitive stays single-source-per-render.\n */\n\ninterface MarkdownRendererProps {\n content: string;\n}\n\nexport interface EntityVideoSectionProps {\n /** Main uploaded video URL. */\n mainVideoUrl?: string | null;\n /** YouTube URL (takes priority over `mainVideoUrl` for display). */\n youtubeUrl?: string | null;\n /** AI-generated highlight video URL. */\n highlightVideoUrl?: string | null;\n /** Thumbnail for highlight video. */\n highlightVideoThumbnail?: string | null;\n /** Poster/thumbnail for main video. */\n mainVideoPoster?: string | null;\n /** Title for YouTube embed. */\n title?: string;\n /** AI-generated video summary (markdown). */\n videoSummary?: string | null;\n /** Video bites/teasers array. */\n videoBites?: VideoTeaser[];\n /** Title for the video bites section. */\n bitesTitle?: string;\n /** Whether to filter bites to published only. */\n filterPublishedBites?: boolean;\n /** Markdown renderer component injected by the host app. */\n MarkdownRenderer?: ComponentType<MarkdownRendererProps>;\n /**\n * Raw SRT content. Deprecated — pass `captionsUrl` instead.\n * Forwarded to `<Video>` for the dev-only warning.\n */\n srtContent?: string | null;\n /** HTTPS URL to a VTT captions file (rendered as native `<track>`). */\n captionsUrl?: string | null;\n /** LCP hint — when true, the full-video tab's poster eager-loads. */\n priority?: boolean;\n}\n\nexport function EntityVideoSection({\n mainVideoUrl,\n youtubeUrl,\n highlightVideoUrl,\n highlightVideoThumbnail,\n mainVideoPoster,\n title = 'Video',\n videoSummary,\n videoBites,\n bitesTitle = 'Video Highlights',\n filterPublishedBites = true,\n MarkdownRenderer,\n srtContent,\n captionsUrl,\n priority = false,\n}: EntityVideoSectionProps) {\n const hasFullVideo = !!(youtubeUrl || mainVideoUrl);\n const hasHighlight = !!highlightVideoUrl;\n const hasVideo = hasFullVideo || hasHighlight;\n\n if (!hasVideo && !videoSummary && (!videoBites || videoBites.length === 0)) {\n return null;\n }\n\n // YouTube wins when both URLs are present.\n const fullVideoUrl = youtubeUrl || mainVideoUrl || null;\n const fullVideoKind: 'youtube' | 'auto' = youtubeUrl ? 'youtube' : 'auto';\n\n return (\n <>\n {hasVideo &&\n (hasFullVideo && hasHighlight ? (\n <Tabs defaultValue=\"full-video\" className=\"w-full\">\n <TabsList className=\"inline-flex justify-start rounded-none bg-transparent h-auto p-0 gap-0\">\n <TabsTrigger\n value=\"full-video\"\n className=\"rounded-none border-b-2 border-transparent data-[state=active]:border-ods-accent data-[state=active]:bg-transparent data-[state=active]:shadow-none px-4 md:px-6 py-3 text-ods-text-secondary data-[state=active]:text-ods-text-primary\"\n >\n Full Video\n </TabsTrigger>\n <TabsTrigger\n value=\"highlights\"\n className=\"rounded-none border-b-2 border-transparent data-[state=active]:border-ods-accent data-[state=active]:bg-transparent data-[state=active]:shadow-none px-4 md:px-6 py-3 text-ods-text-secondary data-[state=active]:text-ods-text-primary\"\n >\n Highlights\n </TabsTrigger>\n </TabsList>\n\n <TabsContent value=\"full-video\" className=\"mt-4\">\n <Video\n kind={fullVideoKind}\n url={fullVideoUrl!}\n poster={mainVideoPoster}\n title={title}\n srtContent={srtContent}\n captionsUrl={captionsUrl}\n layout=\"centered\"\n priority={priority}\n />\n </TabsContent>\n\n <TabsContent value=\"highlights\" className=\"mt-4\">\n <Video\n url={highlightVideoUrl!}\n poster={highlightVideoThumbnail}\n layout=\"centered\"\n />\n </TabsContent>\n </Tabs>\n ) : hasFullVideo ? (\n <Video\n kind={fullVideoKind}\n url={fullVideoUrl!}\n poster={mainVideoPoster}\n title={title}\n srtContent={srtContent}\n captionsUrl={captionsUrl}\n layout=\"centered\"\n priority={priority}\n />\n ) : (\n <Video\n url={highlightVideoUrl!}\n poster={highlightVideoThumbnail}\n layout=\"centered\"\n priority={priority}\n />\n ))}\n\n {videoSummary && MarkdownRenderer && (\n <div className=\"flex flex-col gap-6 w-full min-w-0\">\n <h2 className={`${SECTION_HEADING_CLASS} break-words`}>\n Summary\n </h2>\n <div className=\"text-h4 text-ods-text-primary break-words overflow-hidden\">\n <MarkdownRenderer content={videoSummary} />\n </div>\n </div>\n )}\n\n {videoBites && videoBites.length > 0 && (\n <VideoBitesDisplay\n bites={videoBites}\n title={bitesTitle}\n filterPublished={filterPublishedBites}\n />\n )}\n </>\n );\n}\n","'use client'\n\n/**\n * OnboardingGuideCard (pure presentation + runtime-derived link attrs).\n *\n * Three variants:\n * - `catalog`: rich detail card (hero + author grid) for the public catalog\n * page.\n * - `default`: horizontal step-numbered card for \"More in {section}\" rail.\n * - `sm`: compact horizontal card for chat-inline rendering.\n *\n * Link semantics: the card derives `target`/`rel` from `ChatRuntime.navigation\n * .decideNewTab` (hub-wired via `HubRuntimeProvider`) and the placeholder\n * image from `ChatRuntime.resolvePlaceholderUrl`. Explicit `target` / `rel`\n * / `placeholderUrl` props always WIN — chat dispatch and tests can\n * pre-resolve. No runtime mounted → same-tab + no placeholder.\n */\n\nimport React from 'react'\nimport Image from '../../../embed-shims/next-image'\nimport Link from '../../../embed-shims/next-link'\nimport { Clock, ExternalLink, GraduationCap, Play } from 'lucide-react'\nimport { cn } from '../../../utils/cn'\nimport { BlogImagePlaceholder } from './blog-image-placeholder'\nimport { EntityAuthorCard } from './entity-author-card'\nimport { formatDurationMMSS } from '../../../utils/format'\nimport type { OnboardingGuide } from '../types/entities/onboarding-guide'\nimport { useEntityCardLink } from './use-entity-card-link'\nimport { useEntityCardPlaceholder } from './use-entity-card-placeholder'\nimport {\n COMPACT_CARD_OUTER,\n COMPACT_CARD_IMAGE_SLOT,\n COMPACT_CARD_SKELETON_IMAGE_SLOT,\n COMPACT_CARD_SKELETON_OUTER,\n COMPACT_CARD_TEXT_COL,\n COMPACT_CARD_TITLE_ROW,\n COMPACT_CARD_TITLE,\n COMPACT_CARD_META_ROW_BOX,\n COMPACT_CARD_SUMMARY,\n COMPACT_CARD_ROW_FILLER,\n} from '../utils/compact-card-classes'\n\nexport interface OnboardingGuideCardProps {\n guide: OnboardingGuide\n /** Detail URL resolved by the caller. */\n href: string\n /** When `_blank`, opens in a new tab. Set by chat dispatch via\n * `computeIsNewTab`. Defaults to same-tab. */\n target?: '_blank'\n rel?: 'noopener noreferrer'\n targetPlatform?: string | null\n /** OG placeholder URL used by the catalog + sm variants when no cover. */\n placeholderUrl?: string | null\n size?: 'catalog' | 'default' | 'sm'\n className?: string\n}\n\n/** Markdown source → clean one-line preview prose. The guide cards preview\n * `video_summary || content`, and `content` is raw markdown — without this,\n * the clamped summary shows literal `**bold**` / `## heading` noise. */\nfunction stripMarkdownPreview(text: string): string {\n return text\n .replace(/```[\\s\\S]*?```/g, ' ')\n .replace(/^#{1,6}\\s+/gm, '')\n .replace(/!?\\[([^\\]]*)\\]\\([^)]*\\)/g, '$1')\n .replace(/[*_~`>]/g, '')\n .replace(/^\\s*[-+]\\s+/gm, '')\n .replace(/-{3,}/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim()\n // Hard cap — line-clamp is the visual truncation, but a CSS-order\n // conflict (display utilities vs -webkit-box) once silently killed it\n // and dumped whole guides into the card. Never ship more than ~2 lines\n // of source text regardless of CSS.\n .replace(/^([\\s\\S]{240})[\\s\\S]+$/, '$1…')\n}\n\nconst HORIZONTAL_SIZE_TOKENS = {\n default: {\n padding: 'p-4',\n step: 'w-8 h-8 text-sm',\n title: 'text-h5',\n summaryClamp: 'line-clamp-2',\n },\n} as const\n\nexport function OnboardingGuideCardSkeleton({ size = 'default' }: { size?: 'catalog' | 'default' | 'sm' }) {\n if (size === 'catalog') {\n return (\n <div className=\"bg-ods-system-greys-black border border-ods-border rounded-lg overflow-hidden flex flex-col p-6 gap-4 animate-pulse\">\n <div className=\"flex flex-col md:flex-row gap-4 md:gap-6\">\n <div className=\"w-full md:w-[256px] aspect-[1200/630] bg-ods-border rounded-lg flex-shrink-0\" />\n <div className=\"flex-1 min-w-0 flex flex-col\">\n <div className=\"min-h-[60px] md:min-h-[72px] flex flex-col gap-1.5 justify-start mb-3\">\n <div className=\"h-[25px] md:h-[30px] w-3/4 bg-ods-border rounded\" />\n <div className=\"h-[25px] md:h-[30px] w-1/2 bg-ods-border rounded\" />\n </div>\n <div className=\"min-h-[46px] md:min-h-[52px] flex flex-col gap-2 justify-start\">\n <div className=\"h-3 w-full bg-ods-border/70 rounded\" />\n <div className=\"h-3 w-5/6 bg-ods-border/70 rounded\" />\n </div>\n </div>\n </div>\n <div className=\"grid grid-cols-1 md:grid-cols-3 border border-ods-border rounded-md overflow-hidden w-full\">\n {[0, 1].map((i) => (\n <div\n key={`cell-${i}`}\n className=\"bg-ods-card p-4 flex flex-col gap-3 border-b md:border-b-0 md:border-r border-ods-border\"\n >\n <div className=\"flex flex-col gap-2\">\n <div className=\"h-6 w-32 bg-ods-bg rounded\" />\n <div className=\"h-3 w-20 bg-ods-bg/60 rounded\" />\n </div>\n </div>\n ))}\n <div className=\"bg-ods-card p-4 flex items-center gap-3\">\n <div className=\"h-10 w-10 rounded-full bg-ods-bg shrink-0\" />\n <div className=\"flex flex-col gap-2 flex-1 min-w-0\">\n <div className=\"h-4 w-3/4 bg-ods-bg rounded\" />\n <div className=\"h-3 w-1/2 bg-ods-bg/60 rounded\" />\n </div>\n </div>\n </div>\n </div>\n )\n }\n if (size === 'sm') {\n return (\n <span className={COMPACT_CARD_SKELETON_OUTER}>\n <span className={COMPACT_CARD_SKELETON_IMAGE_SLOT} />\n <span className={COMPACT_CARD_TEXT_COL}>\n <span className={COMPACT_CARD_TITLE_ROW}>\n <span className=\"h-3.5 w-3/5 rounded bg-ods-bg\" />\n </span>\n <span className={COMPACT_CARD_META_ROW_BOX}>\n <span className=\"h-3 w-2/5 rounded bg-ods-bg/70\" />\n </span>\n <span className={COMPACT_CARD_META_ROW_BOX}>\n <span className=\"h-3 w-11/12 rounded bg-ods-bg/40\" />\n </span>\n </span>\n <span className=\"flex shrink-0 items-center self-start h-5\">\n <span className=\"h-3.5 w-3.5 rounded bg-ods-bg\" />\n </span>\n </span>\n )\n }\n const t = HORIZONTAL_SIZE_TOKENS.default\n return (\n <span\n className={`flex items-start gap-3 rounded-md border border-ods-border bg-ods-card ${t.padding} animate-pulse`}\n >\n <span className={`shrink-0 inline-flex items-center justify-center rounded-full bg-ods-bg ${t.step}`} />\n <span className=\"flex flex-col gap-2 flex-1 min-w-0\">\n <span className=\"block h-4 w-2/3 rounded bg-ods-bg\" />\n <span className=\"block h-3 w-1/3 rounded bg-ods-bg/70\" />\n <span className=\"block h-3 w-full rounded bg-ods-bg/40\" />\n </span>\n </span>\n )\n}\n\nexport function OnboardingGuideCard({\n guide,\n href,\n target: targetProp,\n rel: relProp,\n targetPlatform,\n placeholderUrl: placeholderUrlProp,\n size = 'default',\n className,\n}: OnboardingGuideCardProps) {\n const { target, rel } = useEntityCardLink({\n href,\n targetPlatform,\n target: targetProp,\n rel: relProp,\n })\n const placeholderUrl = useEntityCardPlaceholder({\n title: guide.title,\n placeholderUrl: placeholderUrlProp,\n aspect: size === 'sm' ? 'square' : 'wide',\n })\n\n if (size === 'catalog') {\n const coverImage =\n guide.featured_image ||\n guide.main_video_thumbnail ||\n guide.og_image_url ||\n null\n const hasVideoCover = !!(guide.main_video_thumbnail || guide.highlight_video_thumbnail)\n const stepLabel =\n typeof guide.step_order === 'number'\n ? String(guide.step_order).padStart(2, '0')\n : '—'\n const durationLabel =\n typeof guide.highlight_video_duration_ms === 'number' && guide.highlight_video_duration_ms > 0\n ? formatDurationMMSS(Math.floor(guide.highlight_video_duration_ms / 1000))\n : ''\n\n return (\n <Link\n href={href}\n target={target}\n rel={rel}\n prefetch={false}\n className={cn(\n 'group block no-underline bg-ods-system-greys-black',\n 'border border-ods-border rounded-lg overflow-hidden',\n 'transition-all duration-300 ease-out',\n 'transform hover:translate-y-[-2px]',\n 'hover:border-ods-accent hover:shadow-lg hover:shadow-ods-accent/[0.08]',\n className,\n )}\n aria-label={`Open ${guide.title}`}\n >\n <div className=\"flex flex-col p-6 gap-4\">\n <div className=\"flex flex-col md:flex-row gap-4 md:gap-6\">\n <div className=\"w-full md:w-[256px] flex-shrink-0\">\n <div className=\"relative rounded-lg overflow-hidden w-full aspect-[1200/630] bg-ods-bg\">\n {coverImage ? (\n <Image\n src={coverImage}\n alt={guide.title}\n fill\n sizes=\"(max-width: 768px) 100vw, 256px\"\n className=\"object-cover\"\n unoptimized\n />\n ) : (\n <BlogImagePlaceholder\n title={guide.title}\n imageUrl={placeholderUrl ?? null}\n className=\"absolute inset-0\"\n />\n )}\n {hasVideoCover && coverImage && (\n <span className=\"absolute inset-0 flex items-center justify-center bg-black/30\">\n <Play className=\"w-10 h-10 text-white\" fill=\"white\" />\n </span>\n )}\n {durationLabel && (\n <span className=\"absolute bottom-2 right-2 inline-flex items-center gap-1 px-2 py-1 rounded bg-black/60 text-white text-xs font-medium font-mono\">\n <Clock className=\"w-3 h-3\" />\n {durationLabel}\n </span>\n )}\n </div>\n </div>\n\n <div className=\"flex-1 min-w-0 flex flex-col\">\n <div className=\"min-h-[60px] md:min-h-[72px] flex items-start mb-3\">\n <h3 className=\"font-['Azeret_Mono'] font-semibold text-xl md:text-2xl text-ods-text-primary leading-tight line-clamp-2\">\n {guide.title}\n </h3>\n </div>\n <div className=\"min-h-[46px] md:min-h-[52px]\">\n <p className=\"font-['DM_Sans'] text-sm md:text-base text-ods-text-secondary leading-relaxed line-clamp-2\">\n {stripMarkdownPreview(guide.video_summary || guide.content || '')}\n </p>\n </div>\n </div>\n </div>\n\n <EntityAuthorCard\n author={guide.author}\n publishedAt={guide.published_at}\n renderEmptyAuthor\n extraCells={[\n {\n value: `${guide.section} · Step ${stepLabel}`,\n label: 'Section',\n uppercase: false,\n },\n ]}\n />\n </div>\n </Link>\n )\n }\n\n if (size === 'sm') {\n const coverImage = guide.featured_image || guide.main_video_thumbnail || guide.og_image_url || null\n const compactCover = coverImage || placeholderUrl || null\n const hasVideoCover = !guide.featured_image && !!guide.main_video_thumbnail\n const summary = stripMarkdownPreview(guide.video_summary || guide.content || '')\n const author = guide.author?.full_name?.trim() || ''\n const subtitleParts = [\n `Step ${guide.step_order}`,\n guide.section,\n author,\n ].filter((s): s is string => typeof s === 'string' && s.length > 0)\n return (\n <Link href={href} target={target} rel={rel} prefetch={false} className={cn(COMPACT_CARD_OUTER, className)}>\n <span className={COMPACT_CARD_IMAGE_SLOT}>\n {compactCover ? (\n <Image\n src={compactCover}\n alt={guide.title}\n fill\n sizes=\"56px\"\n className=\"object-contain\"\n unoptimized\n />\n ) : (\n <span className=\"flex h-full w-full items-center justify-center text-ods-accent\">\n <GraduationCap className=\"w-4 h-4\" />\n </span>\n )}\n {hasVideoCover && compactCover && (\n <span className=\"absolute inset-0 flex items-center justify-center bg-black/30\">\n <Play className=\"h-4 w-4 text-white\" fill=\"white\" />\n </span>\n )}\n </span>\n <span className={COMPACT_CARD_TEXT_COL}>\n <span className={COMPACT_CARD_TITLE_ROW}>\n <span className={cn(COMPACT_CARD_TITLE, \"font-['Azeret_Mono']\")}>\n {guide.title}\n </span>\n </span>\n <span className={COMPACT_CARD_META_ROW_BOX}>\n <span className=\"truncate text-[11px] leading-4 text-[var(--color-accent-primary)]\">\n {subtitleParts.join(' · ')}\n </span>\n </span>\n <span className={COMPACT_CARD_META_ROW_BOX}>\n <span className={COMPACT_CARD_SUMMARY}>\n {summary || COMPACT_CARD_ROW_FILLER}\n </span>\n </span>\n </span>\n <span className=\"flex shrink-0 items-center self-start h-5 text-ods-text-secondary\">\n <ExternalLink className=\"w-3.5 h-3.5\" />\n </span>\n </Link>\n )\n }\n\n // size === 'default' — horizontal step-numbered card for related-rail.\n const t = HORIZONTAL_SIZE_TOKENS.default\n const summary = stripMarkdownPreview(guide.video_summary || guide.content || '')\n\n return (\n <Link\n href={href}\n target={target}\n rel={rel}\n prefetch={false}\n className={cn(\n `flex items-start gap-3 rounded-md border border-ods-border bg-ods-card hover:border-ods-accent transition-colors ${t.padding}`,\n className,\n )}\n >\n <span\n className={`shrink-0 inline-flex items-center justify-center rounded-full bg-ods-accent/10 text-ods-accent font-semibold ${t.step}`}\n aria-hidden=\"true\"\n >\n {guide.step_order}\n </span>\n <span className=\"flex flex-col gap-0.5 flex-1 min-w-0\">\n <span className={`block ${t.title} text-ods-text-primary truncate`}>{guide.title}</span>\n <span className=\"inline-flex items-center gap-1 font-['DM_Sans'] text-[12px] leading-[16px] text-ods-text-secondary\">\n <GraduationCap className=\"h-3 w-3 shrink-0\" />\n <span className=\"truncate\">{guide.section}</span>\n </span>\n {summary && (\n <span className={`font-['DM_Sans'] text-[14px] leading-[20px] text-ods-text-secondary ${t.summaryClamp}`}>\n {summary}\n </span>\n )}\n </span>\n </Link>\n )\n}\n","'use client'\n\n/**\n * Shared link-attribute resolver for every entity card.\n *\n * Pure-presentation cards (BlogCard, OnboardingGuideCard, etc.)\n * historically required callers to pre-compute `target`/`rel` via a\n * hub-side `useNavLink` wrapper because the same-tab-vs-new-tab\n * decision depends on `currentPlatform()` and the cross-platform\n * URL topology — neither of which the lib knows.\n *\n * This hook moves that decision INTO the card via the\n * `ChatRuntime.navigation.decideNewTab` callback already wired by\n * `HubRuntimeProvider` (and overridable per-embedder). The wrapping\n * `*-card-item.tsx` files in the hub become unnecessary — every card\n * derives its own `target`/`rel` from runtime.\n *\n * Backwards compat: explicit `target` / `rel` props always WIN over\n * runtime-derived values. Chat dispatcher callsites that already pass\n * pre-resolved attributes are unaffected.\n *\n * No runtime mounted? Returns `{ target: undefined, rel: undefined }`\n * (same-tab) — matches the documented embed-shim fallback.\n */\n\nimport { useMemo } from 'react'\nimport { useChatRuntime } from '../../../contexts/chat-runtime-context'\nimport { computeIsNewTab } from '../utils/nav-anchor-props'\n\nexport interface UseEntityCardLinkArgs {\n href: string\n targetPlatform?: string | null\n /** Explicit override. When set, runtime decision is skipped. */\n target?: '_blank'\n /** Explicit override. When set, runtime decision is skipped. */\n rel?: 'noopener noreferrer'\n}\n\nexport interface EntityCardLinkProps {\n target: '_blank' | undefined\n rel: 'noopener noreferrer' | undefined\n}\n\nexport function useEntityCardLink({\n href,\n targetPlatform,\n target,\n rel,\n}: UseEntityCardLinkArgs): EntityCardLinkProps {\n const runtime = useChatRuntime()\n return useMemo(() => {\n // Explicit prop wins — preserves the chat-dispatcher path that\n // pre-computes attrs from `computeIsNewTab`. When `target='_blank'`\n // is passed without `rel`, auto-pair with `noopener noreferrer`\n // to close the tabnabbing vector (window.opener access from the\n // new tab back to the parent).\n if (target !== undefined || rel !== undefined) {\n const safeRel =\n rel ?? (target === '_blank' ? 'noopener noreferrer' : undefined)\n return { target, rel: safeRel }\n }\n // Use the UNIFIED new-tab decision so the rendered anchor's target/rel ALWAYS\n // agrees with the click handler (executeNavigation also calls computeIsNewTab) —\n // including the embed-mode short-circuit + the origin/platform fallback.\n // Previously this used `decideNewTab ?? false`, which diverged from the click\n // when decideNewTab was unwired (embed mode rendered a same-tab anchor that\n // clicked into a new tab). Hub behavior is unchanged: it wires decideNewTab,\n // which computeIsNewTab consumes identically.\n const newTab = runtime ? computeIsNewTab(runtime, href, targetPlatform ?? null) : false\n return newTab\n ? { target: '_blank' as const, rel: 'noopener noreferrer' as const }\n : { target: undefined, rel: undefined }\n }, [target, rel, href, targetPlatform, runtime])\n}\n","'use client'\n\n/**\n * Shared OG-placeholder resolver for every entity card.\n *\n * Pure-presentation cards historically required callers to\n * pre-compute `placeholderUrl` via a hub-side `useOgPlaceholder`\n * wrapper that injected the hub's `buildOgPlaceholderUrl` (resolves\n * CSS-var ODS colors to hex via the static map).\n *\n * This hook moves that resolution INTO the card via the\n * `ChatRuntime.resolvePlaceholderUrl` callback. Embedders that don't\n * wire the callback get no placeholder (the card's empty-state path\n * activates) — same fallback semantics as before.\n *\n * Backwards compat: explicit `placeholderUrl` prop ALWAYS wins over\n * runtime-derived value. Callers that pre-resolve (chat dispatch,\n * tests) are unaffected.\n */\n\nimport { useChatRuntime } from '../../../contexts/chat-runtime-context'\nimport { useOgPlaceholder } from '../../../hooks/use-og-placeholder'\n\nexport interface UseEntityCardPlaceholderArgs {\n /** Entity title — used as the placeholder label. */\n title: string | undefined | null\n /** Explicit override. When set, runtime resolver is skipped. */\n placeholderUrl?: string | null\n /** Site name shown under the title. Optional. */\n siteName?: string\n /** Output aspect ratio. `'wide'` (default) for catalog cards,\n * `'square'` for compact chat-inline cards. */\n aspect?: 'wide' | 'square'\n}\n\nconst NO_OP_BUILDER = () => ''\n\nexport function useEntityCardPlaceholder({\n title,\n placeholderUrl,\n siteName = '',\n aspect = 'wide',\n}: UseEntityCardPlaceholderArgs): string | null {\n const runtime = useChatRuntime()\n const builder = runtime?.resolvePlaceholderUrl ?? NO_OP_BUILDER\n const enabled = placeholderUrl === undefined && !!runtime?.resolvePlaceholderUrl\n const derived = useOgPlaceholder(builder, title, siteName, enabled, aspect)\n // Explicit prop (including explicit null) wins; `undefined` falls back to derived.\n return placeholderUrl !== undefined ? placeholderUrl : derived\n}\n","'use client'\n\nimport React from 'react'\nimport { cn } from '../../utils/cn'\nimport { Chevron02DownIcon } from '../icons-v2-generated'\nimport { ActionsMenuDropdown, type ActionsMenuGroup, type ActionsMenuItem } from './actions-menu'\nimport type { ButtonProps, SplitButtonIconAction } from './button'\nimport { Button, SplitButton } from './button'\n\nexport type PageActionButton = {\n /** Button label. Omit to render an icon-only button. */\n label?: string\n /** Accessible name. Required for icon-only buttons (when `label` is omitted). */\n ariaLabel?: string\n /** Click handler. Optional when `href` or `submenu` is provided. */\n onClick?: () => void\n icon?: React.ReactNode\n variant?: ButtonProps['variant']\n disabled?: boolean\n /**\n * For SplitButton actions (when `iconAction` is set): disables only the main\n * half. Combine with `iconAction.disabled` for icon-only disable. Ignored\n * for non-SplitButton actions.\n */\n mainDisabled?: boolean\n loading?: boolean\n /** Show action only on mobile (below md). Default: visible on all screens. */\n showOnlyMobile?: boolean\n /**\n * Render the desktop button as icon-only (label hidden, icon centered). The full\n * label still appears in the mobile \"...\" dropdown. The desktop icon is forced to\n * `text-ods-text-primary`; the mobile row keeps the caller-provided icon color.\n */\n iconOnlyOnDesktop?: boolean\n /** Render as a link (next/link). Mutually exclusive with `submenu`. */\n href?: string\n /** Forwarded to next/link's prefetch. Only applies when `href` is set. */\n prefetch?: boolean\n /** Open link in a new tab. Only applies when `href` is set. */\n openInNewTab?: boolean\n /**\n * Render the action as a `SplitButton` (two independent click targets).\n * The main half runs `onClick`/`href`; the icon half runs its own action.\n * Mutually exclusive with `submenu`.\n */\n iconAction?: SplitButtonIconAction\n /**\n * Render a button with a chevron that opens a dropdown. The whole button is\n * a single click target — clicking anywhere opens the menu.\n * Mutually exclusive with `iconAction` and `href`/`onClick`.\n */\n submenu?: ActionsMenuItem[]\n}\n\nfunction actionKey(action: PageActionButton, idx: number) {\n return `${action.label ?? action.ariaLabel ?? 'action'}-${idx}`\n}\n\nfunction actionToMenuItems(action: PageActionButton, idx: number): ActionsMenuItem[] {\n if (action.submenu && action.submenu.length > 0) {\n // When a split-button action collapses into the merged mobile \"...\" menu,\n // its chevron disappears and its children become sibling rows. Prefix\n // each child with the parent label.\n if (!action.label) return action.submenu\n return action.submenu.map(item => ({\n ...item,\n label: `${action.label} (${item.label})`,\n }))\n }\n\n if (!action.label) return []\n return [{\n id: `action-${idx}`,\n label: action.label,\n icon: action.icon,\n onClick: action.onClick,\n disabled: action.disabled,\n href: action.href,\n iconAction: action.iconAction\n ? {\n icon: action.iconAction.icon,\n 'aria-label': action.iconAction['aria-label'],\n onClick: action.iconAction.onClick as (() => void) | undefined,\n href: action.iconAction.href,\n openInNewTab: action.iconAction.openInNewTab,\n disabled: action.iconAction.disabled,\n }\n : undefined,\n }]\n}\n\ninterface RenderOptions {\n /** Force the rendered button to be icon-only (label hidden). */\n iconOnly?: boolean\n /** Stretch the button to fill flex parent (used in mobile bottom bar). */\n fullWidth?: boolean\n}\n\nfunction renderActionButton(action: PageActionButton, opts: RenderOptions = {}): React.ReactNode {\n // Two-target SplitButton — primary action + secondary icon action.\n if (action.iconAction) {\n return (\n <SplitButton\n variant={action.variant ?? undefined}\n href={action.href}\n prefetch={action.prefetch}\n openInNewTab={action.openInNewTab}\n onClick={action.onClick}\n disabled={action.disabled}\n mainDisabled={action.mainDisabled}\n leftIcon={action.icon}\n fullWidth={opts.fullWidth}\n iconAction={action.iconAction}\n >\n {action.label}\n </SplitButton>\n )\n }\n\n // Submenu — single click target with a trailing chevron divider.\n if (action.submenu && action.submenu.length > 0) {\n return (\n <ActionsMenuDropdown\n groups={[{ items: action.submenu }]}\n customTrigger={\n <Button\n variant=\"outline\"\n disabled={action.disabled}\n loading={action.loading}\n leftIcon={action.icon}\n splitIcon={<Chevron02DownIcon className=\"h-4 w-4\" />}\n className={opts.fullWidth ? 'flex-1' : undefined}\n >\n {action.label}\n </Button>\n }\n />\n )\n }\n\n // Icon-only button (no label, or explicitly icon-only on desktop).\n const isIconOnly = opts.iconOnly || !action.label || action.iconOnlyOnDesktop\n if (isIconOnly) {\n const iconNode = action.iconOnlyOnDesktop\n ? <span className=\"inline-flex [&_svg]:!text-ods-text-primary\">{action.icon}</span>\n : action.icon\n return (\n <Button\n variant={action.variant}\n size=\"icon\"\n href={action.href}\n prefetch={action.prefetch}\n openInNewTab={action.openInNewTab}\n onClick={action.onClick}\n disabled={action.disabled}\n loading={action.loading}\n leftIcon={iconNode}\n aria-label={action.label ?? action.ariaLabel}\n />\n )\n }\n\n // Default labeled button.\n return (\n <Button\n variant={action.variant}\n href={action.href}\n prefetch={action.prefetch}\n openInNewTab={action.openInNewTab}\n onClick={action.onClick}\n disabled={action.disabled}\n loading={action.loading}\n leftIcon={action.icon}\n className={opts.fullWidth ? 'flex-1' : undefined}\n >\n {action.label}\n </Button>\n )\n}\n\nexport interface PageActionsProps {\n variant?: 'icon-buttons' | 'primary-buttons' | 'menu-primary'\n actions: PageActionButton[]\n menuActions?: ActionsMenuGroup[]\n /**\n * Desktop-only slot rendered before the action buttons (e.g. a `TabSelector`\n * for view-mode toggles). Hidden on mobile and never merged into the \"…\" menu.\n * Honored by the `icon-buttons` and `menu-primary` variants.\n */\n selector?: React.ReactNode\n className?: string\n}\n\nconst ACTIONS_GAP = 'gap-[var(--spacing-system-xs)]'\n\nexport function PageActions({\n variant = 'icon-buttons',\n actions,\n menuActions,\n selector,\n className,\n}: PageActionsProps) {\n if (variant === 'icon-buttons') {\n return <IconButtonsVariant actions={actions} menuActions={menuActions} selector={selector} className={className} />\n }\n\n if (variant === 'menu-primary') {\n return <MenuPrimaryVariant actions={actions} menuActions={menuActions || []} selector={selector} className={className} />\n }\n\n return <PrimaryButtonsVariant actions={actions} className={className} />\n}\n\nfunction IconButtonsVariant({\n actions,\n menuActions,\n selector,\n className,\n}: {\n actions: PageActionButton[]\n menuActions?: ActionsMenuGroup[]\n selector?: React.ReactNode\n className?: string\n}) {\n const desktopActions = actions.filter(a => !a.showOnlyMobile)\n const hasMenuActions = !!menuActions && menuActions.some(g => g.items.length > 0)\n\n const isSingleAction = actions.length === 1 && !actions[0].submenu?.length\n const singleAction = isSingleAction ? actions[0] : null\n const useSingleActionMobile = isSingleAction && !hasMenuActions\n\n return (\n <>\n {/* Desktop: every action as an icon button + optional overflow menu */}\n <div className={cn('hidden md:flex items-center', ACTIONS_GAP, className)}>\n {selector}\n {desktopActions.map((action, idx) => (\n <React.Fragment key={actionKey(action, idx)}>\n {renderActionButton(action)}\n </React.Fragment>\n ))}\n {hasMenuActions && <ActionsMenuDropdown groups={menuActions} />}\n </div>\n\n {/* Mobile: single icon button OR all actions merged into one \"...\" menu */}\n <div className={cn('flex md:hidden', className)}>\n {useSingleActionMobile && singleAction ? (\n renderActionButton(singleAction, { iconOnly: true })\n ) : (\n <ActionsMenuDropdown\n groups={[\n { items: actions.flatMap(actionToMenuItems) },\n ...(menuActions ?? []),\n ]}\n />\n )}\n </div>\n </>\n )\n}\n\n/**\n * Primary buttons variant — primary + outline buttons on desktop,\n * fixed bottom bar on mobile.\n */\nfunction PrimaryButtonsVariant({\n actions,\n className,\n}: {\n actions: PageActionButton[]\n className?: string\n}) {\n // Sort: outline first, accent last (rightmost on desktop).\n const sortedActions = [...actions].sort((a, b) => {\n if (a.variant === 'accent' && b.variant !== 'accent') return 1\n if (a.variant !== 'accent' && b.variant === 'accent') return -1\n return 0\n })\n\n const desktopActions = sortedActions.filter(a => !a.showOnlyMobile)\n\n return (\n <>\n <div className={cn('hidden md:flex items-center', ACTIONS_GAP, className)}>\n {desktopActions.map((action, idx) => (\n <React.Fragment key={`desktop-${actionKey(action, idx)}`}>\n {renderActionButton(action)}\n </React.Fragment>\n ))}\n </div>\n\n <MobileBottomActions actions={sortedActions} />\n </>\n )\n}\n\n/**\n * Menu + primary variant — \"...\" menu + primary buttons on desktop,\n * all actions merged into a single \"...\" menu on mobile.\n */\nfunction MenuPrimaryVariant({\n actions,\n menuActions,\n selector,\n className,\n}: {\n actions: PageActionButton[]\n menuActions: ActionsMenuGroup[]\n selector?: React.ReactNode\n className?: string\n}) {\n const desktopActions = actions.filter(a => !a.showOnlyMobile)\n const hasMenuActions = menuActions.some(g => g.items.length > 0)\n\n return (\n <>\n <div className={cn('hidden md:flex items-center', ACTIONS_GAP, className)}>\n {selector}\n {hasMenuActions && <ActionsMenuDropdown groups={menuActions} />}\n {desktopActions.map((action, idx) => (\n <React.Fragment key={`desktop-${actionKey(action, idx)}`}>\n {renderActionButton({ ...action, variant: action.variant || 'accent' })}\n </React.Fragment>\n ))}\n </div>\n\n <div className={cn('flex md:hidden', className)}>\n <ActionsMenuDropdown\n groups={[\n { items: actions.flatMap(actionToMenuItems) },\n ...menuActions,\n ]}\n />\n </div>\n </>\n )\n}\n\nfunction MobileBottomActions({ actions }: { actions: PageActionButton[] }) {\n return (\n <div className={cn(\n 'fixed md:hidden bottom-0 left-0 right-0 z-50',\n 'bg-ods-card border-t border-ods-border',\n 'flex items-start pt-6 pb-6 px-6',\n ACTIONS_GAP,\n )}>\n {actions.map((action, idx) => (\n <React.Fragment key={`mobile-${actionKey(action, idx)}`}>\n {renderActionButton(action, { fullWidth: !!action.label })}\n </React.Fragment>\n ))}\n </div>\n )\n}\n\nexport function usePageActionsBottomPadding(variant: PageActionsProps['variant']) {\n return variant === 'primary-buttons' || variant === 'menu-primary' ? 'pb-40 md:pb-0' : ''\n}\n\nexport default PageActions\n","'use client'\n\nimport React from 'react'\nimport { cn } from '../../utils/cn'\nimport { getFirstLastInitials } from '../../utils/format'\n\nexport interface EntityImageProps {\n src?: string | null\n alt?: string\n /** Overrides the initials source. Defaults to `alt`. */\n fallbackText?: string\n className?: string\n}\n\nexport function EntityImage({ src, alt, fallbackText, className }: EntityImageProps) {\n const [imageFailed, setImageFailed] = React.useState(false)\n\n React.useEffect(() => {\n setImageFailed(false)\n }, [src])\n\n const showFallback = imageFailed || !src\n const initials = getFirstLastInitials(fallbackText ?? alt)\n\n if (showFallback) {\n return (\n <div\n aria-label={alt}\n className={cn(\n 'size-[52px] md:size-[60px] shrink-0 rounded-md border border-ods-border bg-ods-bg flex items-center justify-center text-ods-text-secondary text-h4 select-none',\n className,\n )}\n >\n {initials || '?'}\n </div>\n )\n }\n\n return (\n <img\n src={src ?? undefined}\n alt={alt ?? ''}\n onError={() => setImageFailed(true)}\n className={cn(\n 'size-[52px] md:size-[60px] shrink-0 rounded-md border border-ods-border object-contain',\n className,\n )}\n />\n )\n}\n","'use client'\n\nimport React from 'react'\nimport { cn } from '../../utils/cn'\nimport type { ActionsMenuGroup } from '../ui/actions-menu'\nimport { EntityImage } from '../ui/entity-image'\nimport { PageActions, type PageActionButton } from '../ui/page-actions'\nimport { BackButton } from './back-button'\n\nexport interface TitleBlockProps {\n title?: string\n subtitle?: string\n image?: { src: string; alt?: string }\n backButton?: { label?: string; onClick: () => void }\n actions?: PageActionButton[]\n actionsVariant?: 'icon-buttons' | 'primary-buttons' | 'menu-primary'\n menuActions?: ActionsMenuGroup[]\n /** Desktop-only slot (e.g. a `TabSelector`) rendered with the actions. Hidden on mobile. */\n selector?: React.ReactNode\n /**\n * Visual variant.\n * - `plain` (default): transparent background, no border.\n * - `card`: card background, border, and padding on mobile only — collapses to plain on md+.\n */\n variant?: 'plain' | 'card'\n className?: string\n}\n\nexport function TitleBlock({\n title,\n subtitle,\n image,\n backButton,\n actions,\n actionsVariant = 'icon-buttons',\n menuActions,\n selector,\n variant = 'plain',\n className,\n}: TitleBlockProps) {\n const hasActions = actions && actions.length > 0\n const hasMenuActions = !!menuActions && menuActions.some(g => g.items.length > 0)\n\n return (\n <div\n className={cn(\n 'flex items-end justify-between gap-[var(--spacing-system-m)]',\n 'md:flex-col md:items-start md:justify-start lg:flex-row lg:items-end lg:justify-between',\n 'pt-[var(--spacing-system-l)]',\n variant === 'card'\n ? cn(\n 'bg-ods-card border-b border-ods-border',\n 'px-[var(--spacing-system-l)] pb-[var(--spacing-system-l)]',\n 'md:bg-transparent md:border-b-0',\n 'md:px-0 md:pb-0',\n 'md:mb-[var(--spacing-system-l)]',\n )\n : 'mb-[var(--spacing-system-l)]',\n className,\n )}\n >\n <div className=\"flex flex-col gap-[var(--spacing-system-xs)] flex-1 min-w-0\">\n {backButton && (\n <BackButton\n onClick={backButton.onClick}\n label={backButton.label}\n className=\"hidden md:inline-flex\"\n />\n )}\n {(image || subtitle) ? (\n <div className=\"flex items-center gap-[var(--spacing-system-m)] min-w-0 w-full\">\n {image && (\n <EntityImage\n src={image.src}\n alt={image.alt}\n fallbackText={image.alt || title}\n />\n )}\n <div className=\"flex flex-col justify-center min-w-0 flex-1\">\n {title && (\n <h1 className=\"text-h2 text-ods-text-primary truncate\" title={title}>{title}</h1>\n )}\n {subtitle && (\n <p className=\"text-h6 text-ods-text-secondary truncate\" title={subtitle}>{subtitle}</p>\n )}\n </div>\n </div>\n ) : (\n title && <h1 className=\"text-h2 text-ods-text-primary\">{title}</h1>\n )}\n </div>\n\n {(hasActions || hasMenuActions || selector) && (\n <div className=\"flex gap-2 items-center shrink-0\">\n <PageActions\n variant={actionsVariant}\n actions={actions ?? []}\n menuActions={menuActions}\n selector={selector}\n />\n </div>\n )}\n </div>\n )\n}\n\nexport default TitleBlock\n","'use client'\n\nimport React from 'react'\nimport { cn } from '../../utils/cn'\nimport { Chevron02LeftIcon } from '../icons-v2-generated/arrows/chevron-02-left-icon'\n\nexport interface BackButtonProps\n extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'children'> {\n label?: string\n onClick?: React.MouseEventHandler<HTMLButtonElement>\n}\n\nexport function BackButton({ label = 'Back', className, type = 'button', ...props }: BackButtonProps) {\n return (\n <button\n type={type}\n className={cn(\n 'group inline-flex items-center justify-center self-start rounded-md',\n 'gap-[var(--spacing-system-xsf)] py-[var(--spacing-system-sf)]',\n 'text-ods-text-secondary hover:text-ods-text-primary',\n 'transition-colors duration-200',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ods-focus',\n className,\n )}\n {...props}\n >\n <Chevron02LeftIcon className=\"size-6 shrink-0\" />\n <span className=\"text-h4\">{label}</span>\n </button>\n )\n}\n\nexport default BackButton\n","'use client'\n\nimport React from 'react'\nimport { cn } from '../../utils/cn'\nimport type { ActionsMenuGroup } from '../ui/actions-menu'\nimport { type PageActionButton } from '../ui/page-actions'\nimport { TitleBlock } from './title-block'\n\nexport interface PageLayoutProps {\n children: React.ReactNode\n title?: string\n subtitle?: string\n image?: { src: string; alt?: string }\n backButton?: { label?: string; onClick: () => void }\n actions?: PageActionButton[]\n actionsVariant?: 'icon-buttons' | 'primary-buttons' | 'menu-primary'\n menuActions?: ActionsMenuGroup[]\n /** Desktop-only slot (e.g. a `TabSelector`) rendered with the actions. Hidden on mobile. */\n selector?: React.ReactNode\n /** Header visual variant. `card` adds a card background, border, and padding on mobile. */\n headerVariant?: 'plain' | 'card'\n className?: string\n contentClassName?: string\n showHeader?: boolean\n}\n\n/**\n * Page layout container with consistent spacing, header, and actions.\n *\n * Uses `--spacing-system-l` as the gap between sections.\n */\nexport function PageLayout({\n children,\n title,\n subtitle,\n image,\n backButton,\n actions,\n actionsVariant = 'icon-buttons',\n menuActions,\n selector,\n headerVariant,\n className,\n contentClassName,\n showHeader = true,\n}: PageLayoutProps) {\n const hasActions = actions && actions.length > 0\n const needsBottomPadding = hasActions && actionsVariant === 'primary-buttons'\n const hasHeader = showHeader && (title || subtitle || image || backButton || hasActions || selector)\n\n return (\n <div className={cn('flex flex-col w-full', className)}>\n {hasHeader && (\n <TitleBlock\n title={title}\n subtitle={subtitle}\n image={image}\n backButton={backButton}\n actions={actions}\n actionsVariant={actionsVariant}\n menuActions={menuActions}\n selector={selector}\n variant={headerVariant}\n />\n )}\n\n <div className={cn('flex flex-col flex-1 gap-[var(--spacing-system-l)]', needsBottomPadding && 'pb-28 md:pb-0', contentClassName)}>\n {children}\n </div>\n </div>\n )\n}\n\nexport type { PageActionButton } from '../ui/page-actions'\nexport { TitleBlock } from './title-block'\nexport type { TitleBlockProps } from './title-block'\nexport default PageLayout\n","import React from 'react';\nimport { cn } from '../../utils/cn';\n\n/**\n * Unified page layout components.\n *\n * PageShell: full-width <main> for list pages, dashboards, and other wide\n * layouts (max-w-[1920px]) — bg + min-height + centered, max-width content box.\n * ArticleDetailLayout: the same shell constrained to a readable article width\n * (max-w-[1280px]) — blog posts, release notes, case studies, etc.\n * Child components render content only; no layout wrappers needed.\n *\n * Outer padding is driven by CSS custom properties (see\n * `styles/ods-page-shell.css`), so an embedder matches its host grid by setting\n * the `--page-shell-*` vars on ANY ancestor — no prop-threading through the lib\n * wrappers (DevSectionPage / LegalDocumentPage / ReleaseDetailPage / …), no\n * imperative global, no context. The cascade scopes each override to its own\n * subtree. Per-instance escape hatch: `contentClassName` (Tailwind padding\n * utilities there win over the class defaults).\n */\n\ninterface LayoutProps {\n children: React.ReactNode;\n /** JSON-LD schema script elements (breadcrumbs, article schema, etc.) */\n schemas?: React.ReactNode;\n /** Per-instance class override on the content box (wins over the var defaults). */\n contentClassName?: string;\n}\n\n/**\n * Full-width page shell for list pages, dashboards, and other wide layouts.\n * Max width: 1920px.\n */\nexport function PageShell({ children, schemas, contentClassName }: LayoutProps) {\n return (\n <main className=\"bg-ods-bg min-h-screen\">\n {schemas}\n <div className={cn('page-shell-content max-w-[1920px] mx-auto', contentClassName)}>\n {children}\n </div>\n </main>\n );\n}\n\n/**\n * Constrained layout for article/detail pages (max-w-[1280px]) — readable\n * content width for blog posts, release notes, case studies, investor updates.\n * Fixed hub padding (`px-6 md:px-20 py-6 md:py-10`); pass `contentClassName` to\n * override per instance. (PageShell's `--page-shell-*` var system does NOT apply\n * here — this layout keeps its own fixed spacing.)\n */\nexport function ArticleDetailLayout({ children, schemas, contentClassName }: LayoutProps) {\n return (\n <main className=\"bg-ods-bg min-h-screen\">\n {schemas}\n <div className={cn('max-w-[1280px] mx-auto px-6 md:px-20 py-6 md:py-10', contentClassName)}>\n {children}\n </div>\n </main>\n );\n}\n","\"use client\"\n\nimport React from 'react'\nimport { AlertTriangle, RefreshCw, Home } from 'lucide-react'\nimport { Button } from './button'\nimport { cn } from '../../utils/cn'\n\ninterface ErrorStateProps {\n title?: string\n message: string\n variant?: 'error' | 'warning' | 'info'\n showIcon?: boolean\n showRetry?: boolean\n showHome?: boolean\n onRetry?: () => void\n onHome?: () => void\n className?: string\n containerClassName?: string\n}\n\nexport function ErrorState({\n title = 'Error',\n message,\n variant = 'error',\n showIcon = true,\n showRetry = false,\n showHome = false,\n onRetry,\n onHome,\n className,\n containerClassName\n}: ErrorStateProps) {\n const getVariantStyles = () => {\n switch (variant) {\n case 'error':\n return {\n bg: 'bg-ods-attention-red-error/20',\n border: 'border-ods-attention-red-error',\n text: 'text-ods-attention-red-error',\n icon: 'text-ods-attention-red-error'\n }\n case 'warning':\n return {\n bg: 'bg-ods-attention-yellow-warning/20',\n border: 'border-ods-attention-yellow-warning',\n text: 'text-ods-attention-yellow-warning',\n icon: 'text-ods-attention-yellow-warning'\n }\n case 'info':\n return {\n bg: 'bg-ods-bg-surface',\n border: 'border-ods-border',\n text: 'text-ods-text-secondary',\n icon: 'text-ods-text-secondary'\n }\n }\n }\n\n const styles = getVariantStyles()\n\n return (\n <div className={cn(\"p-6\", containerClassName)}>\n <div className={cn(\n \"rounded-lg p-4 border\",\n styles.bg,\n styles.border,\n className\n )}>\n <div className=\"flex items-start gap-3\">\n {showIcon && (\n <AlertTriangle className={cn(\"h-5 w-5 mt-0.5 flex-shrink-0\", styles.icon)} />\n )}\n <div className=\"flex-1\">\n <h3 className={cn(\"font-semibold mb-1\", styles.text)}>\n {title}\n </h3>\n <p className={cn(\"text-sm\", styles.text)}>\n {message}\n </p>\n {(showRetry || showHome) && (\n <div className=\"flex gap-2 mt-3\">\n {showRetry && onRetry && (\n <Button\n onClick={onRetry}\n variant=\"outline\"\n size=\"small-legacy\"\n className=\"h-8\"\n leftIcon={<RefreshCw className=\"h-4 w-4\" />}\n >\n Try Again\n </Button>\n )}\n {showHome && onHome && (\n <Button\n onClick={onHome}\n variant=\"outline\"\n size=\"small-legacy\"\n className=\"h-8\"\n leftIcon={<Home className=\"h-4 w-4\" />}\n >\n Go Home\n </Button>\n )}\n </div>\n )}\n </div>\n </div>\n </div>\n </div>\n )\n}\n\n// Convenience components for common error scenarios\nexport function PageError({ message, onRetry, onHome }: { message: string; onRetry?: () => void; onHome?: () => void }) {\n return (\n <ErrorState\n title=\"Page Error\"\n message={message}\n variant=\"error\"\n showRetry={!!onRetry}\n showHome={!!onHome}\n onRetry={onRetry}\n onHome={onHome}\n />\n )\n}\n\nexport function LoadError({ message, onRetry }: { message: string; onRetry?: () => void }) {\n return (\n <ErrorState\n title=\"Loading Error\"\n message={message}\n variant=\"error\"\n showRetry={!!onRetry}\n onRetry={onRetry}\n />\n )\n}\n\nexport function NotFoundError({ message = \"The requested item was not found\", onHome }: { message?: string; onHome?: () => void }) {\n return (\n <ErrorState\n title=\"Not Found\"\n message={message}\n variant=\"warning\"\n showHome={!!onHome}\n onHome={onHome}\n />\n )\n}","import Link from 'next/link'\nimport { StatusBadge } from '../ui/status-badge'\nimport type { TagAssoc } from '../../types/blog'\n\ninterface EntityTagBadgesProps {\n /** Flat `<entity>_tags[]` association array (from entity_tags hydration). */\n tags?: TagAssoc[] | null\n /**\n * When set, each badge links to `${basePath}?tags=${slug}` (clickable, SPA nav\n * via next/link). Omit for a non-interactive display (badges with no slug also\n * render non-interactive even when basePath is set).\n */\n basePath?: string\n /** Cap visible badges; the remainder collapse into a \"+N\" badge. */\n max?: number\n /** Extra classes on the wrapper row. */\n className?: string\n}\n\n// The ONE tag-badge skin (OpenFrame design): rounded-rect, font-mono uppercase\n// StatusBadge on ods-card + ods-border. Clickable badges add the accent hover.\n// Exported so other tag-shaped chips (e.g. FaqAccordion's section badge)\n// render the IDENTICAL skin without re-declaring it.\nexport const TAG_BADGE_CLASS = 'bg-ods-card border border-ods-border'\nconst BADGE_CLASS = TAG_BADGE_CLASS\n\n/**\n * THE single tag-badge renderer for the whole product (hub + lib). Renders the\n * OpenFrame `StatusBadge` chip skin for a flat `<entity>_tags[]` array, optionally\n * clickable (links each tag to its filtered list via `basePath`). The hub's\n * `EntityTagList` delegates here so there is exactly one tag-display design.\n * Renders nothing when there are no tags.\n */\nexport function EntityTagBadges({ tags, basePath, max, className }: EntityTagBadgesProps) {\n const items = (tags || []).filter((t): t is TagAssoc => !!t && !!t.name)\n if (items.length === 0) return null\n\n // Only null/undefined means \"no cap\"; max={0} shows none, negatives clamp to 0.\n const visibleLimit = max == null ? items.length : Math.max(0, max)\n const shown = items.slice(0, visibleLimit)\n const overflow = items.length - shown.length\n\n return (\n <div className={`flex flex-wrap items-center gap-2 w-full ${className || ''}`}>\n {shown.map((tag) => {\n const key = tag.tag_id ?? tag.id ?? tag.slug ?? tag.name\n const label = (tag.name || '').toUpperCase()\n if (basePath && tag.slug) {\n return (\n <Link key={key} href={`${basePath}?tags=${tag.slug}`} className=\"inline-flex\">\n <StatusBadge\n text={label}\n variant=\"card\"\n className={`${BADGE_CLASS} cursor-pointer transition-colors hover:border-ods-accent`}\n />\n </Link>\n )\n }\n return <StatusBadge key={key} text={label} variant=\"card\" className={BADGE_CLASS} />\n })}\n {overflow > 0 && (\n <StatusBadge text={`+${overflow}`} variant=\"card\" className={`${BADGE_CLASS} text-ods-text-secondary`} />\n )}\n </div>\n )\n}\n","/**\n * Mux CDN origin constants — single source of truth, server-safe.\n *\n * Lives in its own NON-`'use client'` module so server-side hub\n * modules (webhook handlers, URL builders, hostname comparisons in\n * `lib/config/mux-config.ts`) can import these strings without\n * tripping Next.js's client-reference poisoning. Re-exported from\n * `use-video-warmup.ts` for backward-compat with client-side callers.\n *\n * Bug history (2026-05-29): when these constants lived in\n * `use-video-warmup.ts` (which is `'use client'`), the hub's\n * server-side `new URL(MUX_STREAM_ORIGIN).hostname` evaluation crashed\n * at Vercel build with `TypeError: Invalid URL` — Next.js had\n * replaced the constant with a client-function stub that throws\n * \"Attempted to call ... from the server\" when stringified. Splitting\n * the constants into this module restores the server-safe path.\n *\n * These hostnames are part of Mux's public API contract and are\n * stable. A future change to the Mux CDN architecture (extremely\n * unlikely) would be a single-line edit here.\n */\n\n/** HLS playback (`/{playback_id}.m3u8` + segments + per-asset MP4 renditions). */\nexport const MUX_STREAM_ORIGIN = 'https://stream.mux.com'\n\n/** Server-generated thumbnails (`/{playback_id}/thumbnail.jpg`). */\nexport const MUX_IMAGE_ORIGIN = 'https://image.mux.com'\n","'use client'\n\n/**\n * `useVideoWarmup` — single-source-of-truth hook for warming the\n * network path to a public entity's main video so click→first-frame\n * lands in sub-second on Fast 4G.\n *\n * Behavior:\n *\n * 1. **Preconnect on every render** (`ReactDOM.preconnect`) — buys\n * the TCP / TLS handshakes to the video-bearing origins. React\n * 19 de-dupes identical preconnects, so this is safe to call\n * on every render.\n *\n * 2. **Preload the video bytes** (`<link rel=\"preload\" as=\"video\">`)\n * ONLY when:\n * - the consumer's container scrolls within `nearMargin` of\n * the viewport (gated via the lib's IO singleton hook), AND\n * - `navigator.connection?.saveData !== true`, AND\n * - the URL is on the Supabase storage origin (Mux HLS warms\n * via its own manifest fetch when MuxPlayer mounts; YouTube\n * has its own origin pool, no preload benefit).\n *\n * Origin configuration:\n * - Mux origins (`stream.mux.com` / `image.mux.com`) are public\n * Mux CDN hostnames and stable across the Mux API contract —\n * hardcoded here.\n * - Supabase storage origin varies per-deployment (different\n * project per env). Threaded via the `supabaseStorageOrigin`\n * argument so the lib stays env-agnostic; hub callers pass\n * `getSupabaseStorageOrigin()` from their env config, or read\n * it from `ChatRuntime.endpoints.supabaseStorageOrigin`.\n *\n * Lifted from hub `hooks/use-video-warmup.ts`. The Mux constants\n * and the IO-gated preload semantics are byte-equivalent.\n */\n\nimport { useEffect } from 'react'\nimport ReactDOM from 'react-dom'\nimport { useNearViewport } from '../../hooks/use-near-viewport'\nimport { useChatRuntime } from '../../contexts/chat-runtime-context'\n// Re-export from the server-safe `mux-origins.ts` module so the\n// constants are NOT bound to this `'use client'` file. See the\n// JSDoc in `mux-origins.ts` for the bug history. Backward-compat:\n// existing imports that read `MUX_STREAM_ORIGIN` from\n// `@flamingo-stack/openframe-frontend-core/components/features`\n// continue to resolve through this re-export.\nexport { MUX_STREAM_ORIGIN, MUX_IMAGE_ORIGIN } from './mux-origins'\nimport { MUX_STREAM_ORIGIN, MUX_IMAGE_ORIGIN } from './mux-origins'\n\n/**\n * Preconnect-only variant — fires the three video-bearing origin\n * preconnects (Supabase Storage + Mux stream + Mux image) without\n * setting up the IntersectionObserver subscription or the preload\n * `<link>` injection.\n *\n * Use this when the consumer can't attach a `ref` to the video\n * container (e.g. release detail page, which delegates the player\n * render to a sibling component). Calling the full `useVideoWarmup`\n * from there would subscribe to a never-mounted ref and ship dead\n * preload machinery in the bundle.\n *\n * For consumers that own the video container, use `useVideoWarmup`\n * (which composes this hook + the IO-gated preload step).\n *\n * Reads `supabaseStorageOrigin` from `ChatRuntime.endpoints` by\n * default — callers in hosts that mount `HubRuntimeProvider` (or\n * any equivalent provider that wires the field) get the origin\n * automatically. The explicit `supabaseStorageOrigin` argument\n * overrides the runtime value when set.\n */\nexport function useVideoOriginPreconnect({\n supabaseStorageOrigin,\n}: { supabaseStorageOrigin?: string } = {}): void {\n const runtime = useChatRuntime()\n const resolvedOrigin =\n supabaseStorageOrigin ?? runtime?.endpoints.supabaseStorageOrigin\n try {\n ReactDOM.preconnect(MUX_STREAM_ORIGIN, { crossOrigin: 'anonymous' })\n ReactDOM.preconnect(MUX_IMAGE_ORIGIN, { crossOrigin: 'anonymous' })\n if (resolvedOrigin) {\n ReactDOM.preconnect(resolvedOrigin, { crossOrigin: 'anonymous' })\n }\n } catch (err) {\n if (process.env.NODE_ENV !== 'production') {\n // eslint-disable-next-line no-console\n console.warn('[useVideoOriginPreconnect] preconnect failed:', err)\n }\n }\n}\n\ninterface UseVideoWarmupOptions {\n /**\n * Effective video URL the page renders. Pass null/undefined when\n * there's no video yet (the hook still preconnects). Only URLs on\n * `supabaseStorageOrigin` are preloaded — Mux HLS and YouTube are\n * no-ops on the preload side.\n */\n videoUrl?: string | null\n /**\n * Supabase storage origin (e.g. `https://xyz.supabase.co`). When\n * omitted, falls back to `ChatRuntime.endpoints.supabaseStorageOrigin`\n * — hosts that mount `HubRuntimeProvider` (or any equivalent\n * provider) get the origin automatically. When neither is set, the\n * preload step is skipped (preconnect to Mux still fires).\n */\n supabaseStorageOrigin?: string\n /**\n * IO root margin gate for the preload step. Default `'1000px'` —\n * about one viewport's worth of lookahead on desktop.\n */\n nearMargin?: string\n}\n\nexport interface UseVideoWarmupResult<T extends Element = HTMLDivElement> {\n ref: (node: T | null) => void\n isNear: boolean\n}\n\nexport function useVideoWarmup<T extends Element = HTMLDivElement>({\n videoUrl,\n supabaseStorageOrigin,\n nearMargin = '1000px',\n}: UseVideoWarmupOptions = {}): UseVideoWarmupResult<T> {\n // Resolve origin once — runtime fallback so callers in hosts that\n // mount `HubRuntimeProvider` don't need to thread it themselves.\n const runtime = useChatRuntime()\n const resolvedOrigin =\n supabaseStorageOrigin ?? runtime?.endpoints.supabaseStorageOrigin\n\n // Preconnect on every render — React 19 dedupes. Delegates to the\n // shared preconnect-only variant so the origin list is a single\n // source of truth.\n useVideoOriginPreconnect({ supabaseStorageOrigin: resolvedOrigin })\n\n const { ref, isNear } = useNearViewport<T>(nearMargin)\n\n useEffect(() => {\n if (!isNear || !videoUrl || !resolvedOrigin) return\n\n // Save-Data gate — metered connections skip preload.\n type Connection = { saveData?: boolean }\n const conn = (navigator as Navigator & { connection?: Connection }).connection\n if (conn?.saveData === true) return\n\n // Origin gate: only preload Supabase-hosted MP4s. Mux HLS warms\n // via the manifest fetch when MuxPlayer mounts; YouTube has no\n // preload benefit.\n let videoOrigin: string\n try {\n videoOrigin = new URL(videoUrl, 'http://placeholder.local').origin\n } catch {\n return\n }\n if (videoOrigin !== resolvedOrigin) return\n\n const link = document.createElement('link')\n link.rel = 'preload'\n link.as = 'video'\n link.href = videoUrl\n link.crossOrigin = 'anonymous'\n // `fetchPriority='low'` matches the plan — the hint should not\n // steal network from the LCP image; the click→first-frame win is\n // in milliseconds, not the first paint.\n if ('fetchPriority' in link) {\n ;(link as HTMLLinkElement & { fetchPriority?: string }).fetchPriority = 'low'\n }\n document.head.appendChild(link)\n\n return () => {\n link.remove()\n }\n }, [isNear, videoUrl, resolvedOrigin])\n\n return { ref, isNear }\n}\n","/**\n * Build the captions API URL for a video entity.\n *\n * Returns the HTTPS URL to the `/api/captions/[entityType]/[entityId]` endpoint\n * which serves VTT content for iOS native fullscreen subtitles.\n * Returns undefined if entity has no srt_content.\n *\n * Cache-busting hash derived from the srt_content length so iOS Safari\n * fetches fresh VTT when subtitles are regenerated (Safari aggressively caches\n * <track> src URLs even with short Cache-Control max-age).\n *\n * Lifted from hub `lib/utils/captions-url.ts`. The hub's hard-coded\n * `VideoEnabledEntityType` enum is widened to `string` here — embedders\n * pass whatever entity-type discriminator their reverse-proxied\n * `/api/captions/...` route expects.\n */\nexport function getCaptionsUrl(\n entityType: string,\n entityId: string | number,\n srtContent?: string | null,\n): string | undefined {\n if (!srtContent) return undefined\n const hash = `${srtContent.length}-${srtContent.slice(0, 8).replace(/\\s/g, '')}`\n return `/api/captions/${entityType}/${entityId}?v=${hash}`\n}\n","'use client'\n\n/**\n * Shared filter section wrapper with responsive layout.\n *\n * Provides the consistent card container for any filter row (status,\n * platform, section, etc.) Single row on desktop with flex-wrap,\n * stacks naturally on mobile.\n *\n * Lifted from hub `components/admin/shared/filter-section.tsx` so\n * lib catalog views can render their own filter pills without an\n * injected slot.\n */\n\nimport { Filter } from 'lucide-react'\nimport { Button } from './button'\n\nexport interface FilterPillRowOption {\n value: string\n label: string\n}\n\nexport interface FilterPillRowProps {\n /** Label shown next to the filter icon, e.g. \"Section\", \"Platform\". */\n label: string\n /** The currently selected filter value. */\n selectedValue: string\n /** Callback when a filter option is selected. */\n onValueChange: (value: string) => void\n /** Available filter options. */\n options: FilterPillRowOption[]\n /** Optional count label, e.g. \"Showing 1-10 of 42 items\". */\n countLabel?: string\n /** Optional children to render instead of options (for custom filter content). */\n children?: React.ReactNode\n}\n\nexport function FilterPillRow({\n label,\n selectedValue,\n onValueChange,\n options,\n countLabel,\n children,\n}: FilterPillRowProps) {\n return (\n <div className=\"flex flex-wrap items-center gap-3 p-4 bg-ods-card border border-ods-border rounded-lg\">\n <div className=\"flex items-center gap-2\">\n <Filter className=\"h-4 w-4 text-ods-accent\" />\n <span className=\"text-h5 text-ods-text-secondary\">{label}</span>\n </div>\n {children ||\n options.map((opt) => (\n <Button\n key={opt.value}\n type=\"button\"\n variant={selectedValue === opt.value ? 'accent' : 'outline'}\n size=\"small-legacy\"\n onClick={() => onValueChange(opt.value)}\n className=\"text-h3\"\n >\n {opt.label}\n </Button>\n ))}\n {countLabel && (\n <div className=\"ml-auto text-[12px] font-['DM_Sans'] text-ods-text-secondary shrink-0\">\n {countLabel}\n </div>\n )}\n </div>\n )\n}\n"]}
|