@flamingo-stack/openframe-frontend-core 0.0.313-snapshot.20260623203621 → 0.0.313
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/dist/{chunk-ZPK5HW7B.cjs → chunk-2ZHDP22R.cjs} +3 -3
- package/dist/{chunk-ZPK5HW7B.cjs.map → chunk-2ZHDP22R.cjs.map} +1 -1
- package/dist/{chunk-NH2RY6VM.js → chunk-66JU4VP2.js} +3 -15
- package/dist/{chunk-NH2RY6VM.js.map → chunk-66JU4VP2.js.map} +1 -1
- package/dist/{chunk-OD3BEWDQ.js → chunk-6FZD5KFP.js} +3 -3
- package/dist/{chunk-WJCOWYAP.cjs → chunk-BFNTSETG.cjs} +14 -14
- package/dist/{chunk-WJCOWYAP.cjs.map → chunk-BFNTSETG.cjs.map} +1 -1
- package/dist/{chunk-BHOGI57O.cjs → chunk-BFSKFC6S.cjs} +38 -38
- package/dist/{chunk-BHOGI57O.cjs.map → chunk-BFSKFC6S.cjs.map} +1 -1
- package/dist/{chunk-JWX6NIQ4.js → chunk-BVDHLEP2.js} +2 -2
- package/dist/{chunk-46UZAYUT.cjs → chunk-DDLJTRF4.cjs} +29 -29
- package/dist/{chunk-46UZAYUT.cjs.map → chunk-DDLJTRF4.cjs.map} +1 -1
- package/dist/{chunk-VCJOLKED.cjs → chunk-DE2BQKQV.cjs} +12 -12
- package/dist/{chunk-VCJOLKED.cjs.map → chunk-DE2BQKQV.cjs.map} +1 -1
- package/dist/{chunk-XKVSR3IV.js → chunk-E6BHNYTA.js} +4 -4
- package/dist/{chunk-UO27TVAO.js → chunk-FXTT7K3A.js} +3 -3
- package/dist/{chunk-5ATH263N.cjs → chunk-G56GYN7Z.cjs} +1 -4
- package/dist/chunk-G56GYN7Z.cjs.map +1 -0
- package/dist/{chunk-DD35H7HA.cjs → chunk-GHK2FCIM.cjs} +40 -40
- package/dist/{chunk-DD35H7HA.cjs.map → chunk-GHK2FCIM.cjs.map} +1 -1
- package/dist/{chunk-E4CQ4RUG.js → chunk-JQ2EYXWR.js} +1 -4
- package/dist/chunk-JQ2EYXWR.js.map +1 -0
- package/dist/{chunk-TRSDXD23.js → chunk-JQLC2FVM.js} +2 -2
- package/dist/{chunk-BJ6JXN5Z.js → chunk-OZYAAJHM.js} +5 -5
- package/dist/{chunk-UNKIRZVY.cjs → chunk-REHUG3RH.cjs} +19 -19
- package/dist/{chunk-UNKIRZVY.cjs.map → chunk-REHUG3RH.cjs.map} +1 -1
- package/dist/{chunk-E2LC43T3.js → chunk-RM3M3SZ6.js} +2 -2
- package/dist/{chunk-ZW3NHMG7.js → chunk-SPFGSUNE.js} +3 -3
- package/dist/{chunk-EC4DGRN6.cjs → chunk-SQQXCVZZ.cjs} +53 -65
- package/dist/chunk-SQQXCVZZ.cjs.map +1 -0
- package/dist/{chunk-AD7TII2A.cjs → chunk-XHGE5XBH.cjs} +5 -5
- package/dist/{chunk-AD7TII2A.cjs.map → chunk-XHGE5XBH.cjs.map} +1 -1
- package/dist/{chunk-L7BROXZ7.js → chunk-YI2ACYRX.js} +2 -2
- package/dist/{chunk-2LFQJYLQ.cjs → chunk-YSMKPNYZ.cjs} +7 -7
- package/dist/{chunk-2LFQJYLQ.cjs.map → chunk-YSMKPNYZ.cjs.map} +1 -1
- package/dist/components/case-studies/index.cjs +9 -9
- package/dist/components/case-studies/index.js +3 -3
- package/dist/components/chat/index.cjs +3 -3
- package/dist/components/chat/index.js +2 -2
- package/dist/components/contact/index.cjs +4 -4
- package/dist/components/contact/index.js +3 -3
- package/dist/components/docs/index.cjs +6 -6
- package/dist/components/docs/index.js +5 -5
- package/dist/components/embeds/index.cjs +4 -4
- package/dist/components/embeds/index.js +3 -3
- package/dist/components/faq/index.cjs +5 -5
- package/dist/components/faq/index.js +4 -4
- package/dist/components/features/index.cjs +3 -3
- package/dist/components/features/index.js +2 -2
- package/dist/components/index.cjs +188 -190
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +10 -12
- package/dist/components/index.js.map +1 -1
- package/dist/components/layout/page-layout.d.ts +1 -1
- package/dist/components/layout/page-layout.d.ts.map +1 -1
- package/dist/components/layout/title-block.d.ts +0 -10
- package/dist/components/layout/title-block.d.ts.map +1 -1
- package/dist/components/navigation/index.cjs +3 -3
- package/dist/components/navigation/index.js +2 -2
- package/dist/components/onboarding-guides/index.cjs +29 -29
- package/dist/components/onboarding-guides/index.js +5 -5
- package/dist/components/related-content/index.cjs +5 -5
- package/dist/components/related-content/index.js +4 -4
- package/dist/components/tickets/help-center-list.d.ts +1 -5
- package/dist/components/tickets/help-center-list.d.ts.map +1 -1
- package/dist/components/tickets/index.cjs +73 -84
- package/dist/components/tickets/index.cjs.map +1 -1
- package/dist/components/tickets/index.js +10 -21
- package/dist/components/tickets/index.js.map +1 -1
- package/dist/components/tool-icon.d.ts.map +1 -1
- package/dist/components/ui/index.cjs +3 -5
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.js +2 -4
- package/dist/hooks/index.cjs +2 -2
- package/dist/hooks/index.js +1 -1
- package/dist/index.cjs +3 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +2 -4
- package/dist/types/index.cjs +0 -2
- package/dist/types/index.cjs.map +1 -1
- package/dist/types/index.js +0 -2
- package/dist/types/index.js.map +1 -1
- package/dist/types/tool.types.d.ts +0 -1
- package/dist/types/tool.types.d.ts.map +1 -1
- package/dist/utils/index.cjs +0 -11
- package/dist/utils/index.cjs.map +1 -1
- package/dist/utils/index.js +0 -11
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/tool-utils.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/layout/page-layout.tsx +1 -1
- package/src/components/layout/title-block.tsx +1 -12
- package/src/components/tickets/help-center-list.tsx +4 -16
- package/src/components/tool-icon.tsx +0 -1
- package/src/types/tool.types.ts +0 -2
- package/src/utils/tool-utils.ts +0 -11
- package/dist/chunk-5ATH263N.cjs.map +0 -1
- package/dist/chunk-E4CQ4RUG.js.map +0 -1
- package/dist/chunk-EC4DGRN6.cjs.map +0 -1
- /package/dist/{chunk-OD3BEWDQ.js.map → chunk-6FZD5KFP.js.map} +0 -0
- /package/dist/{chunk-JWX6NIQ4.js.map → chunk-BVDHLEP2.js.map} +0 -0
- /package/dist/{chunk-XKVSR3IV.js.map → chunk-E6BHNYTA.js.map} +0 -0
- /package/dist/{chunk-UO27TVAO.js.map → chunk-FXTT7K3A.js.map} +0 -0
- /package/dist/{chunk-TRSDXD23.js.map → chunk-JQLC2FVM.js.map} +0 -0
- /package/dist/{chunk-BJ6JXN5Z.js.map → chunk-OZYAAJHM.js.map} +0 -0
- /package/dist/{chunk-E2LC43T3.js.map → chunk-RM3M3SZ6.js.map} +0 -0
- /package/dist/{chunk-ZW3NHMG7.js.map → chunk-SPFGSUNE.js.map} +0 -0
- /package/dist/{chunk-L7BROXZ7.js.map → chunk-YI2ACYRX.js.map} +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/home/runner/work/openframe-oss-lib/openframe-oss-lib/openframe-frontend-core/dist/components/index.cjs","../../src/components/announcement-bar.tsx","../../src/components/icon-utils.tsx","../../src/components/openframe-logo.tsx","../../src/utils/announcement-storage.ts","../../src/components/categories-cart.tsx","../../src/components/vendor-icon.tsx","../../src/utils/url-fix.ts","../../src/utils/vendor-media-stub.ts","../../src/components/category-card.tsx","../../src/components/comment-card.tsx","../../src/components/vendor-display-button.tsx","../../src/components/auth-stub.tsx","../../src/components/user-summary-stub.tsx","../../src/components/content-loading-container.tsx","../../src/components/dynamic-skeleton.tsx","../../src/hooks/use-theme-aware.ts","../../src/components/filter-chip.tsx","../../src/components/footer.tsx","../../src/components/social-icon-row.tsx","../../src/components/unified-filter-logic.tsx","../../src/utils/format-text-stub.ts","../../src/components/index.ts","../../src/components/footer-waitlist-button.tsx","../../src/components/hero-image-uploader.tsx","../../src/components/icons-block.tsx","../../src/components/icons-stub.tsx","../../src/components/image-cropper.tsx","../../src/components/metric-value.tsx","../../src/components/msp-display.tsx","../../src/components/square-avatar.tsx","../../src/components/open-source-features.tsx","../../src/components/persistent-pagination.tsx","../../src/components/pricing-display.tsx","../../src/components/results-count.tsx","../../src/components/vendor-tag.tsx","../../src/components/selection-source-badge.tsx","../../src/components/user-display.tsx","../../src/components/loading/content-skeleton.tsx","../../src/components/profile/ProfileLoadingSkeleton.tsx","../../src/components/loading/msp-profile-form-skeleton.tsx","../../src/components/loading/category-card-skeleton.tsx","../../src/components/loading/category-vendor-selector-skeleton.tsx","../../src/components/loading/wizard-layout-skeleton.tsx","../../src/components/loading/margin-report-skeleton.tsx","../../src/components/loading/users-grid-skeleton.tsx","../../src/components/loading/organization-icon-skeleton.tsx","../../src/components/loading/organization-card-skeleton.tsx","../../src/components/loading/device-card-skeleton.tsx","../../src/components/vendor-page-skeleton.tsx","../../src/components/why-it-matters.tsx","../../src/components/yes-no-display.tsx","../../src/components/made-with-love.tsx","../../src/components/date-time-picker.tsx","../../src/components/shared/product-release/product-releases-view.tsx","../../src/components/shared/product-release/release-detail-page.tsx","../../src/components/shared/media-gallery-strip.tsx","../../src/components/shared/product-release/release-detail-skeleton.tsx","../../src/components/shared/roadmap/roadmap-grid.tsx","../../src/components/shared/roadmap/use-roadmap-voting.ts","../../src/components/shared/roadmap/roadmap-grid-skeleton.tsx","../../src/components/shared/roadmap/roadmap-view.tsx","../../src/components/shared/delivery/delivery-lists.tsx","../../src/components/shared/delivery/delivery-table.tsx","../../src/components/shared/legal-document/legal-document-page.tsx","../../src/components/shared/legal-document/use-legal-docs.ts","../../src/components/authors/author-detail-view.tsx"],"names":["OpenFrameLogo","renderSvgIcon","jsx","OpenmspLogo","FlamingoLogo","jsxs","getProxiedImageUrl","Fragment","useState","ImageIcon","VendorDirectoryIcon","OpenSourceIcon","CommunityHubIcon","VendorsIcon","CommunityIcon","CompareIcon","SunIcon","MoonIcon","CheckCircleIcon","GitHubIcon","PlusCircleIcon","useCallback","SquareAvatar","DollarSign","mainValue","backgroundClasses","DatePicker","useEffect","useRef","useMemo","DEFAULT_ENDPOINT","DEFAULT_SEARCH_PARAM_KEY","DEFAULT_STATUS_PARAM_KEY"],"mappings":"AAAA,6xBAAY;AACZ,YAAY;AACZ;AACE;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;AACF,yDAA8B;AAC9B,iCAA8B;AAC9B;AACE;AACA;AACA;AACF,yDAA8B;AAC9B;AACE;AACA;AACA;AACF,yDAA8B;AAC9B;AACE;AACA;AACF,yDAA8B;AAC9B;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,yDAA8B;AAC9B;AACE;AACA;AACF,yDAA8B;AAC9B;AACE;AACA;AACA;AACA;AACA;AACA;AACF,yDAA8B;AAC9B;AACE;AACF,yDAA8B;AAC9B,iCAA8B;AAC9B;AACE;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;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;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;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;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;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;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;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;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;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;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;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;AACF,yDAA8B;AAC9B;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,yDAA8B;AAC9B;AACE;AACA;AACF,yDAA8B;AAC9B,iCAA8B;AAC9B;AACE;AACA;AACA;AACA;AACF,yDAA8B;AAC9B;AACE;AACF,yDAA8B;AAC9B;AACE;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;AACF,yDAA8B;AAC9B;AACE;AACA;AACF,yDAA8B;AAC9B;AACE;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;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,yDAA8B;AAC9B;AACE;AACA;AACA;AACF,yDAA8B;AAC9B,iCAA8B;AAC9B,iCAA8B;AAC9B,iCAA8B;AAC9B,iCAA8B;AAC9B;AACE;AACF,yDAA8B;AAC9B;AACA;ACnhCA,4EAAoC;AAGpC,4CAAA,CAAA;AADA,2CAAkB;ADqhClB;AACA;AEzhCA;AACE;AACA;AACA;AACA;AACA;AACA;AAAA;AF4hCF;AACA;AGlhCQ,+CAAA;AAhBD,IAAMA,eAAAA,EAAgB,CAAC,EAAE,SAAA,EAAW,cAAA,EAAgB,cAAA,EAAiB,GAAG,MAAM,CAAA,EAAA,GAAgH;AACnM,EAAA,uBACE,6BAAA;AAAA,IAAC,KAAA;AAAA,IAAA;AAAA,MACE,GAAG,KAAA;AAAA,MACJ,SAAA;AAAA,MACA,KAAA,EAAM,IAAA;AAAA,MACN,MAAA,EAAO,IAAA;AAAA,MACP,OAAA,EAAQ,WAAA;AAAA,MACR,IAAA,EAAK,MAAA;AAAA,MACL,KAAA,EAAM,4BAAA;AAAA,MACN,KAAA,EAAO;AAAA,QACL,UAAA,EAAY,cAAA;AAAA,QACZ,GAAG,KAAA,CAAM;AAAA,MACX,CAAA;AAAA,MAEA,QAAA,kBAAA,8BAAA,GAAC,EAAA,EACC,QAAA,EA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;AACJ,QAAA;AAEJ,MAAA;AACF,IAAA;AAGEA,IAAAA;AAK0B,IAAA;AAQ3B,IAAA;AACH,EAAA;AAEJ;AlE4oLiC;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;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;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;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;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;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;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;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;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;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;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;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;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;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;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;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;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/components/index.cjs","sourcesContent":[null,"\"use client\";\n\nimport { useState, useEffect } from 'react';\nimport Image from '../embed-shims/next-image';\nimport { X } from 'lucide-react';\nimport { Button } from './ui/button';\nimport { renderSvgIcon } from './icon-utils';\nimport {\n setStoredAnnouncement,\n getStoredAnnouncement,\n clearStoredAnnouncement,\n} from '../utils/announcement-storage';\nimport { Announcement } from '../types/announcement';\nimport { getAppType } from '../utils/app-config';\nimport { useEndpointsRuntime } from '../contexts/endpoints-runtime-context';\n\n// Helper that defers to renderSvgIcon so we don't need local icon imports\nconst getSvgIcon = (\n name: string,\n size: 'main' | 'cta' = 'main',\n extra: Record<string, any> = {}\n) => {\n const cls =\n size === 'cta'\n ? 'relative shrink-0 w-3 h-3 md:w-4 md:h-4'\n : 'relative shrink-0 w-6 h-6 md:w-8 md:h-8';\n return renderSvgIcon(name, { className: cls, ...extra });\n};\n\nexport function AnnouncementBar() {\n const [announcement, setAnnouncement] = useState<Announcement | null>(null);\n const [isVisible, setIsVisible] = useState<boolean>(false);\n\n // Get the platform type for platform-specific localStorage keys\n const platform = getAppType();\n\n // Optional endpoint runtime: when no provider is mounted (e.g. on a\n // bare React-tree page that doesn't wrap with HubRuntimeProvider),\n // the bar silently skips its fetch instead of throwing. Apps that DO\n // mount the provider get the configured URL — typically\n // '/api/announcements/active' in the hub, or a proxied path in an\n // embedded host.\n const endpoints = useEndpointsRuntime();\n const announcementsUrl = endpoints?.announcementsUrl;\n\n // Helper to determine dismissal key for localStorage\n const getDismissKey = (id: string) => `${platform}-announcement-${id}-dismissed`;\n \n // Helper to get platform-specific cache key\n const getCacheKey = () => `${platform}-announcement-cache`;\n\n // Fetch active announcement from API and update state + LS\n const fetchActiveAnnouncement = async () => {\n // No provider mounted → no URL configured → skip fetch silently.\n // Cached announcement from previous sessions still renders if present.\n if (!announcementsUrl) return;\n try {\n // Server-side platform injection - no URL parameter needed\n const response = await fetch(announcementsUrl);\n \n if (response.ok) {\n const data = await response.json();\n if (data.announcement) {\n setAnnouncement(data.announcement);\n\n // persist latest announcement for quick future loads with platform-specific key\n setStoredAnnouncement(getCacheKey(), data.announcement);\n\n // Check if this specific announcement was dismissed\n const isDismissed = localStorage.getItem(getDismissKey(data.announcement.id));\n setIsVisible(!isDismissed);\n } else {\n // No announcement available - clean up localStorage and hide bar\n setAnnouncement(null);\n setIsVisible(false);\n \n // Use utility function to properly clear platform-specific announcement data\n clearStoredAnnouncement(getCacheKey());\n }\n } else {\n // Network or other error - hide announcement and clean up\n console.error(`❌ [${platform.toUpperCase()}] Error fetching announcement: ${response.status}`);\n setAnnouncement(null);\n setIsVisible(false);\n \n // Clear stale data on network errors too\n clearStoredAnnouncement(getCacheKey());\n }\n } catch (error) {\n console.error('Error fetching active announcement:', error);\n setAnnouncement(null);\n setIsVisible(false);\n \n // Clear stale data on exceptions too\n clearStoredAnnouncement(getCacheKey());\n }\n };\n\n // Initial load: use cached announcement synchronously for instant paint\n useEffect(() => {\n const cached = getStoredAnnouncement(getCacheKey());\n if (cached) {\n const isDismissed = localStorage.getItem(getDismissKey(cached.id));\n setAnnouncement(cached);\n setIsVisible(!isDismissed);\n }\n\n // No provider mounted → no URL → no fetch / no polling. Cached\n // announcement still painted above. Skip scheduling the 5-min\n // interval entirely to avoid an idle timer + repeated short-circuit\n // calls.\n if (!announcementsUrl) return;\n\n // Always fetch latest on mount\n fetchActiveAnnouncement();\n\n // Schedule refresh every 5 minutes. When announcementsUrl flips\n // (e.g. provider value swap), the effect re-runs and restarts the\n // interval against the new URL — no stale captured fetch.\n const interval = setInterval(fetchActiveAnnouncement, 300_000);\n return () => clearInterval(interval);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [announcementsUrl]);\n\n // helpers\n const handleDismiss = () => {\n if (!announcement) return;\n localStorage.setItem(getDismissKey(announcement.id), 'true');\n setIsVisible(false);\n };\n\n const handleCtaClick = () => {\n if (!announcement?.cta_url) return;\n announcement.cta_target === '_blank'\n ? window.open(announcement.cta_url, '_blank', 'noopener,noreferrer')\n : (window.location.href = announcement.cta_url);\n };\n\n const renderIcon = () => {\n if (!announcement) return null;\n\n if (announcement.icon_type === 'png' && announcement.icon_png_url) {\n return (\n <Image\n src={announcement.icon_png_url}\n alt=\"Announcement icon\"\n width={32}\n height={32}\n className=\"relative shrink-0\"\n aria-hidden\n />\n );\n }\n\n return getSvgIcon(\n announcement.icon_svg_name || 'openframe-logo',\n 'main',\n announcement.icon_svg_props ?? {}\n );\n };\n\n // If no announcement or dismissed => render nothing\n if (!announcement || !isVisible) return null;\n\n return (\n <div\n className=\"relative w-full z-50\"\n style={{ backgroundColor: announcement.background_color }}\n data-announcement-bar\n >\n <div className=\"flex items-center w-full max-w-full\">\n {/* Mobile: Clickable content area, Desktop: Regular content */}\n <div\n className={`flex flex-row gap-2 md:gap-4 items-center pl-4 md:pl-6 py-1.5 md:py-2 flex-1 min-w-0 ${\n announcement.cta_enabled && announcement.cta_url ? 'md:cursor-default cursor-pointer' : ''\n }`}\n onClick={(e) => {\n // Only handle click on mobile (< 768px) and if CTA is enabled\n if (window.innerWidth < 768 && announcement.cta_enabled && announcement.cta_url) {\n e.preventDefault();\n handleCtaClick();\n }\n }}\n >\n {renderIcon()}\n\n <div className=\"flex-1 min-w-0 max-w-full\">\n <p className=\"font-body font-bold text-[14px] md:text-[18px] leading-tight tracking-tight mb-0 text-[#1A1A1A] truncate\">\n {announcement.title}\n </p>\n <p className=\"font-body text-[12px] md:text-[18px] leading-tight hidden md:block text-[#1A1A1A] truncate\">\n {announcement.description}\n </p>\n </div>\n\n {/* CTA Button - Hidden on mobile, shown on desktop */}\n {announcement.cta_enabled && announcement.cta_text && announcement.cta_url && (\n <div className=\"hidden md:flex flex-shrink-0 ml-1 md:ml-2\">\n <Button\n onClick={handleCtaClick}\n variant=\"outline\"\n size=\"small-legacy\"\n leftIcon={\n announcement.cta_show_icon && announcement.cta_icon\n ? getSvgIcon(\n announcement.cta_icon,\n 'cta',\n announcement.cta_icon_props ?? {}\n )\n : undefined\n }\n className=\"transition-opacity hover:opacity-90 text-xs md:text-sm whitespace-nowrap\"\n style={{\n backgroundColor: announcement.cta_button_background_color || undefined,\n color: announcement.cta_button_text_color || undefined,\n borderColor: announcement.cta_button_background_color || undefined,\n }}\n >\n {announcement.cta_text}\n </Button>\n </div>\n )}\n </div>\n\n {/* Dismiss button - always visible */}\n <button\n onClick={(e) => {\n e.stopPropagation(); // Prevent triggering the mobile CTA click\n handleDismiss();\n }}\n className=\"flex-shrink-0 w-8 h-8 md:w-10 md:h-10 flex items-center justify-center hover:bg-[#1A1A1A]/10 focus:outline-none focus:ring-2 focus:ring-[#1A1A1A] mr-2 md:mr-4\"\n aria-label=\"Dismiss announcement\"\n type=\"button\"\n >\n <X className=\"w-4 h-4 text-[#1A1A1A]\" strokeWidth={2} />\n </button>\n </div>\n </div>\n );\n}\n","import React from 'react';\nimport {\n Megaphone,\n Bell,\n Info,\n Star,\n Rocket,\n Package,\n} from 'lucide-react';\nimport { OpenFrameLogo } from './openframe-logo';\nimport { OpenmspLogo } from './openmsp-logo';\nimport { FlamingoLogo } from './flamingo-logo';\n\n/**\n * Returns a JSX element for a known icon name, spreading any extra props.\n * Falls back to Megaphone if the name is not recognised.\n */\nexport function renderSvgIcon(\n name: string,\n props: React.SVGProps<SVGSVGElement | SVGElement> = {}\n): React.ReactElement {\n const map: Record<string, (p: any) => React.ReactElement> = {\n megaphone: (p) => <Megaphone {...p} />,\n bell: (p) => <Bell {...p} />,\n info: (p) => <Info {...p} />,\n star: (p) => <Star {...p} />,\n rocket: (p) => <Rocket {...p} />,\n package: (p) => <Package {...p} />,\n 'openframe-logo': (p) => <OpenFrameLogo {...p} />,\n 'openmsp-logo': (p) => <OpenmspLogo {...p} />,\n 'flamingo': (p)=> <FlamingoLogo {...p} />,\n };\n\n const renderer = map[name] || map['megaphone'];\n return renderer(props);\n} ","import React from 'react';\n\nexport const OpenFrameLogo = ({ className, lowerPathColor, upperPathColor, ...props }: { className?: string, lowerPathColor?: string, upperPathColor?: string } & React.SVGProps<SVGSVGElement>) => {\n return (\n <svg\n {...props}\n className={className}\n width=\"32\"\n height=\"32\"\n viewBox=\"0 0 32 32\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n style={{\n '--fill-0': 'currentColor',\n ...props.style\n } as React.CSSProperties}\n >\n <g>\n <g>\n {/* White squares - top right, bottom left, bottom right, bottom center */}\n <path\n d=\"M21.3333 16.5333C21.3333 16.2388 21.5721 16 21.8667 16H26.1333C26.4279 16 26.6667 16.2388 26.6667 16.5333V20.8C26.6667 21.0946 26.4279 21.3333 26.1333 21.3333H21.8667C21.5721 21.3333 21.3333 21.0946 21.3333 20.8V16.5333Z\"\n fill={lowerPathColor ?? '#ffffff'}\n />\n <path\n d=\"M16 21.8667C16 21.5721 16.2388 21.3333 16.5333 21.3333H20.8C21.0946 21.3333 21.3333 21.5721 21.3333 21.8667V26.1333C21.3333 26.4279 21.0946 26.6667 20.8 26.6667H16.5333C16.2388 26.6667 16 26.4279 16 26.1333V21.8667Z\"\n fill={lowerPathColor ?? '#ffffff'}\n />\n <path\n d=\"M26.6667 21.8667C26.6667 21.5721 26.9054 21.3333 27.2 21.3333H31.4667C31.7612 21.3333 32 21.5721 32 21.8667V26.1333C32 26.4279 31.7612 26.6667 31.4667 26.6667H27.2C26.9054 26.6667 26.6667 26.4279 26.6667 26.1333V21.8667Z\"\n fill={lowerPathColor ?? '#ffffff'}\n />\n <path\n d=\"M21.3333 27.2C21.3333 26.9054 21.5721 26.6667 21.8667 26.6667H26.1333C26.4279 26.6667 26.6667 26.9054 26.6667 27.2V31.4667C26.6667 31.7612 26.4279 32 26.1333 32H21.8667C21.5721 32 21.3333 31.7612 21.3333 31.4667V27.2Z\"\n fill={lowerPathColor ?? '#ffffff'}\n />\n </g>\n {/* Black frame/border */}\n <path\n d=\"M30.9333 0H1.06667C0.477563 0 0 0.477564 0 1.06667V30.9333C0 31.5224 0.477563 32 1.06667 32H14.9333C15.5224 32 16 31.5224 16 30.9333V27.7333C16 27.1442 15.5224 26.6667 14.9333 26.6667H5.86667C5.57211 26.6667 5.33333 26.4279 5.33333 26.1333V5.86667C5.33333 5.57211 5.57211 5.33333 5.86667 5.33333H26.1333C26.4279 5.33333 26.6667 5.57212 26.6667 5.86667V14.9333C26.6667 15.5224 27.1442 16 27.7333 16H30.9333C31.5224 16 32 15.5224 32 14.9333V1.06667C32 0.477563 31.5224 0 30.9333 0Z\"\n fill={upperPathColor ?? '#1A1A1A'}\n />\n </g>\n </svg>\n );\n};","// Stub for announcement storage\nexport interface AnnouncementStorageOptions {\n key: string;\n defaultValue?: any;\n}\n\nexport function getStoredAnnouncement(key: string): any {\n if (typeof window !== 'undefined') {\n try {\n return JSON.parse(localStorage.getItem(key) || 'null');\n } catch {\n return null;\n }\n }\n return null;\n}\n\nexport function setStoredAnnouncement(key: string, value: any): void {\n if (typeof window !== 'undefined') {\n try {\n localStorage.setItem(key, JSON.stringify(value));\n } catch {\n // Ignore storage errors\n }\n }\n}\n\nexport function clearStoredAnnouncement(key: string = 'announcement'): void {\n if (typeof window !== 'undefined') {\n try {\n localStorage.removeItem(key);\n } catch {\n // Ignore storage errors\n }\n }\n}","\"use client\"\n\nimport React from 'react';\nimport Link from '../embed-shims/next-link';\nimport { ChevronRight } from 'lucide-react';\nimport { VendorIcon } from './vendor-icon';\nimport type { CategoryCardProps, RealCategoryCardProps } from '../types/category';\n\n\n// Component that receives vendor and subcategory data as props\nexport function CategoriesCart({ \n category, \n vendors = [], \n vendorCount = 0, \n subcategoryCount = 0, \n isLoading = false,\n className = '' \n}: RealCategoryCardProps) {\n return (\n <Link\n href={`/vendors?category=${category.slug}`}\n className={`block bg-ods-card border border-ods-border rounded-lg p-3 md:p-4 pb-4 md:pb-6 hover:border-[#FFC008] transition-colors group relative ${className}`}\n >\n <div className=\"flex flex-col gap-4 md:gap-6\">\n {/* Vendor Icons Grid */}\n <div className=\"relative w-full h-8 md:h-10 overflow-hidden\">\n <div className=\"flex gap-2 md:gap-3 w-full\">\n {isLoading ? (\n // Skeleton loading for vendor icons\n Array.from({ length: 20 }).map((_, index) => (\n <div\n key={index}\n className=\"w-8 h-8 md:w-10 md:h-10 bg-ods-border rounded animate-pulse flex-shrink-0\"\n />\n ))\n ) : vendors && vendors.length > 0 ? (\n vendors.map((vendor: any) => (\n <VendorIcon\n key={vendor.id}\n vendor={vendor}\n size=\"md\"\n className=\"rounded overflow-hidden filter grayscale opacity-60\"\n />\n ))\n ) : (\n // No vendors found - show placeholder icons\n Array.from({ length: 6 }).map((_, index) => (\n <div\n key={index}\n className=\"w-8 h-8 md:w-10 md:h-10 bg-ods-border rounded flex items-center justify-center opacity-30 flex-shrink-0\"\n >\n <div className=\"w-4 h-4 md:w-6 md:h-6 bg-[#888888] rounded-sm\" />\n </div>\n ))\n )}\n </div>\n\n {/* Gradient overlays for fade effect */}\n <div className=\"absolute inset-y-0 left-0 w-4 md:w-6 bg-gradient-to-r from-[#212121] to-transparent pointer-events-none\" />\n <div className=\"absolute inset-y-0 right-0 w-4 md:w-6 bg-gradient-to-l from-[#212121] to-transparent pointer-events-none\" />\n </div>\n\n {/* Category Information - Updated to use real data */}\n <div className=\"flex flex-col gap-2\">\n <div className=\"flex flex-col gap-1\">\n <h3 className=\"text-xl md:text-2xl font-bold text-ods-text-primary group-hover:text-ods-accent transition-colors leading-[1.33] font-body\">\n {category.name}\n </h3>\n <p className=\"text-sm md:text-base font-medium text-ods-text-secondary leading-[1.43] font-body\">\n {subcategoryCount || 0} Subcategories • {vendorCount || 0} Products\n </p>\n </div>\n\n <div className=\"flex items-start md:items-end justify-between gap-4 md:gap-6\">\n <p className=\"font-['DM_Sans'] font-medium text-lg leading-[1.33] text-ods-text-primary flex-1\">\n {category.description}\n </p>\n\n {/* Arrow Button */}\n <div\n className=\"flex items-center justify-center w-10 h-10 md:w-12 md:h-12 bg-transparent border border-ods-border rounded-md group-hover:bg-[#FFC008] transition-colors flex-shrink-0\"\n aria-label={`View ${category.name} category`}\n > \n <ChevronRight className=\"w-5 h-5 md:w-6 md:h-6 text-ods-text-primary group-hover:text-black transition-colors\" />\n </div>\n </div>\n </div>\n </div>\n </Link>\n );\n}\n\n","\"use client\"\n\nimport React from 'react'\nimport Image from '../embed-shims/next-image'\nimport { cn } from \"../utils/cn\"\nimport { getVendorLogo, VendorWithMedia } from '../utils/vendor-media-stub'\nimport { getProxiedImageUrl } from '../utils/image-proxy-stub'\n\ninterface VendorIconProps {\n vendor: VendorWithMedia & {\n id?: number\n title: string\n slug?: string\n logo?: string | null\n }\n size?: 'xs' | 'sm' | 'md' | 'lg' | 'l' | 'xl'\n className?: string\n showBackground?: boolean\n backgroundStyle?: 'dark' | 'light' | 'white'\n}\n\nconst sizeClasses = {\n xs: 'w-6 h-6',\n sm: 'w-8 h-8', \n md: 'w-10 h-10',\n lg: 'w-12 h-12',\n l: 'w-14 h-14',\n xl: 'w-16 h-16'\n}\n\nconst imageSizeMap = {\n xs: { width: 16, height: 16 },\n sm: { width: 20, height: 20 },\n md: { width: 32, height: 32 },\n lg: { width: 40, height: 40 },\n l: { width: 38, height: 38 },\n xl: { width: 40, height: 40 }\n}\n\nconst backgroundClasses = {\n dark: 'bg-ods-bg border border-ods-border',\n light: 'bg-ods-card border border-ods-border',\n white: 'bg-white border border-[#E5E5E5]'\n}\n\n/**\n * Common VendorIcon component for displaying vendor logos consistently across the platform\n * Extracted from vendor-card.tsx for reuse in comparison tables, dropdowns, etc.\n */\nexport function VendorIcon({ \n vendor, \n size = 'md', \n className = '',\n showBackground = true,\n backgroundStyle = 'dark'\n}: VendorIconProps) {\n const logoUrl = getVendorLogo(vendor)\n const { width, height } = imageSizeMap[size]\n \n const containerClasses = cn(\n sizeClasses[size],\n 'rounded-lg flex items-center justify-center flex-shrink-0',\n showBackground && backgroundClasses[backgroundStyle],\n !showBackground && 'overflow-hidden',\n className\n )\n\n return (\n <div className={containerClasses}>\n {logoUrl ? (\n <Image\n src={getProxiedImageUrl(logoUrl) || logoUrl}\n alt={`${vendor.title} logo`}\n width={width}\n height={height}\n className={cn(\n 'object-contain',\n showBackground ? 'p-1' : 'w-full h-full'\n )}\n />\n ) : (\n <div className={cn(\n 'flex items-center justify-center text-xs font-medium uppercase',\n backgroundStyle === 'white' ? 'text-[#333333]' : 'text-ods-text-secondary'\n )}>\n {vendor.title?.substring(0, 2) || '??'}\n </div>\n )}\n </div>\n )\n} ","/**\n * Utility functions for fixing URL construction issues\n */\n\n/**\n * Fix double slashes in URLs while preserving protocol slashes\n * @param url The URL to fix\n * @returns Fixed URL with single slashes in path\n */\nexport function fixUrlDoubleSlashes(url: string): string {\n if (!url) return url;\n \n // Split on protocol to preserve it\n const protocolMatch = url.match(/^(https?:\\/\\/)/);\n if (protocolMatch) {\n const protocol = protocolMatch[1];\n const rest = url.substring(protocol.length);\n // Replace multiple consecutive slashes with single slash in the path part\n const fixedRest = rest.replace(/\\/+/g, '/');\n return protocol + fixedRest;\n }\n \n // For relative URLs, just fix multiple slashes\n return url.replace(/\\/+/g, '/');\n}\n\n/**\n * Properly join URL path segments without creating double slashes\n * @param segments Path segments to join\n * @returns Properly joined path\n */\nexport function joinUrlPath(...segments: string[]): string {\n return segments\n .map((segment, index) => {\n // Remove leading slash from all but first segment\n if (index > 0 && segment.startsWith('/')) {\n segment = segment.substring(1);\n }\n // Remove trailing slash from all but last segment\n if (index < segments.length - 1 && segment.endsWith('/')) {\n segment = segment.substring(0, segment.length - 1);\n }\n return segment;\n })\n .filter(segment => segment.length > 0)\n .join('/');\n}\n\n/**\n * Fix Supabase storage URLs specifically\n * @param url Supabase storage URL\n * @returns Fixed URL\n */\nexport function fixSupabaseStorageUrl(url: string): string {\n // Early return if nothing to process\n if (!url) return url;\n\n /**\n * We previously restricted the fix to URLs that contained a specific Supabase\n * domain (\"supabase.co\"). However, we now serve assets from a custom domain\n * (e.g. app.openmsp.ai) that still uses the same `/storage/v1/object/public/`\n * path structure. The old guard clause prevented those URLs from being\n * cleaned which resulted in paths like:\n * https://app.openmsp.ai/storage/v1/object/public/logos///Ansible.png\n * Googlebot treats the triple-slash as a distinct resource and reports 404\n * errors which in turn causes the \"URL will be indexed only if certain\n * conditions are met\" warning in Search Console.\n *\n * Solution: detect the canonical Supabase storage path segment instead of\n * the host name. Whenever we see that path we safely collapse multiple\n * consecutive slashes to a single slash while preserving the protocol\n * (`https://`).\n */\n\n const SUPABASE_STORAGE_SEGMENT = '/storage/v1/object/public/';\n\n if (url.includes(SUPABASE_STORAGE_SEGMENT)) {\n return fixUrlDoubleSlashes(url);\n }\n\n // No known storage segment – return the original URL untouched\n return url;\n}","// Vendor media utilities for UI kit\n// Real implementation copied from main project\n\nimport { fixSupabaseStorageUrl } from './url-fix';\n\nexport interface VendorMedia {\n media_type: 'logo' | 'image' | 'video';\n media_url: string;\n}\n\nexport interface VendorWithMedia {\n id?: number;\n title: string;\n slug?: string;\n logo?: string | null;\n logo_url?: string; // Support direct logo_url field from lightweight API\n vendor_media?: VendorMedia[];\n}\n\n/**\n * Get the logo URL from vendor_media array or direct logo_url field\n */\nexport function getVendorLogo(vendor: VendorWithMedia): string | null {\n // First check for direct logo_url field (from lightweight API)\n if (vendor.logo_url) {\n return fixSupabaseStorageUrl(vendor.logo_url);\n }\n \n // Check for legacy logo field\n if (vendor.logo) {\n return fixSupabaseStorageUrl(vendor.logo);\n }\n \n // Fallback to vendor_media array (from detailed API)\n const logoMedia = vendor.vendor_media?.find(m => m.media_type === 'logo');\n if (logoMedia?.media_url) {\n return fixSupabaseStorageUrl(logoMedia.media_url);\n }\n \n return null;\n}\n\n/**\n * Get the main image URL from vendor_media array\n */\nexport function getVendorImage(vendor: VendorWithMedia): string | null {\n const imageMedia = vendor.vendor_media?.find(m => m.media_type === 'image');\n return imageMedia?.media_url ? fixSupabaseStorageUrl(imageMedia.media_url) : null;\n}\n\n/**\n * Get the video URL from vendor_media array\n */\nexport function getVendorVideo(vendor: VendorWithMedia): string | null {\n const videoMedia = vendor.vendor_media?.find(m => m.media_type === 'video');\n return videoMedia?.media_url ? fixSupabaseStorageUrl(videoMedia.media_url) : null;\n}\n\n/**\n * Get all media URLs of a specific type\n */\nexport function getVendorMediaByType(vendor: VendorWithMedia, type: 'logo' | 'image' | 'video'): string[] {\n return vendor.vendor_media?.filter(m => m.media_type === type).map(m => fixSupabaseStorageUrl(m.media_url)) || [];\n}\n\n/**\n * Get all media items grouped by type\n */\nexport function getVendorMediaGrouped(vendor: VendorWithMedia): {\n logos: string[];\n images: string[];\n videos: string[];\n} {\n const media = vendor.vendor_media || [];\n \n return {\n logos: media.filter(m => m.media_type === 'logo').map(m => fixSupabaseStorageUrl(m.media_url)),\n images: media.filter(m => m.media_type === 'image').map(m => fixSupabaseStorageUrl(m.media_url)),\n videos: media.filter(m => m.media_type === 'video').map(m => fixSupabaseStorageUrl(m.media_url))\n };\n}\n\n/**\n * Add new media to vendor_media array (useful for admin interfaces)\n */\nexport function addVendorMedia(vendor: VendorWithMedia, type: 'logo' | 'image' | 'video', url: string): VendorMedia[] {\n const existingMedia = vendor.vendor_media || [];\n const newMedia: VendorMedia = { media_type: type, media_url: url };\n return [...existingMedia, newMedia];\n}\n\n/**\n * Remove media from vendor_media array\n */\nexport function removeVendorMedia(vendor: VendorWithMedia, url: string): VendorMedia[] {\n return vendor.vendor_media?.filter(m => m.media_url !== url) || [];\n}\n\n/**\n * Check if vendor has media of a specific type\n */\nexport function hasVendorMedia(vendor: VendorWithMedia, type: 'logo' | 'image' | 'video'): boolean {\n return vendor.vendor_media?.some(m => m.media_type === type) || false;\n}\n\n/**\n * Get media count by type\n */\nexport function getVendorMediaCount(vendor: VendorWithMedia): {\n logos: number;\n images: number;\n videos: number;\n total: number;\n} {\n const media = vendor.vendor_media || [];\n \n const logos = media.filter(m => m.media_type === 'logo').length;\n const images = media.filter(m => m.media_type === 'image').length;\n const videos = media.filter(m => m.media_type === 'video').length;\n \n return {\n logos,\n images,\n videos,\n total: media.length\n };\n}","\"use client\"\n\nimport React from \"react\";\n\ninterface CategoryCardProps {\n name: string;\n description: string;\n categoryCount: number;\n productCount: number;\n icons?: React.ReactNode[];\n}\n\nexport function CategoryCard({ name, description, categoryCount, productCount, icons = [] }: CategoryCardProps) {\n return (\n <article className=\"bg-[#1A1A1A] border border-[#424242] rounded-[12px] p-8 flex flex-col min-w-0 box-border\">\n <div className=\"flex gap-6 mb-8 justify-center items-center\">\n {(icons.length > 0 ? icons : Array(10).fill(null)).map((icon, i) => (\n <div key={i} className=\"w-10 h-10 bg-gray-700 rounded flex items-center justify-center\">\n {icon}\n </div>\n ))}\n </div>\n <div className=\"flex-1 flex flex-col\">\n <h2 className=\"text-[24px] font-bold text-ods-text-primary mb-2 text-left leading-tight\">{name}</h2>\n <div className=\"text-[16px] text-ods-text-secondary mb-4 text-left\">\n {categoryCount} Categories • {productCount} Products\n </div>\n <div className=\"flex flex-row items-start\">\n <p className=\"text-[16px] text-ods-text-primary text-left leading-snug flex-1\">{description}</p>\n <button\n className=\"w-12 h-12 flex items-center justify-center border border-ods-border rounded-[6px] bg-transparent hover:bg-[#FFC008] transition-colors ml-4\"\n style={{ minWidth: 48, minHeight: 48 }}\n aria-label={`View ${name}`}\n >\n <svg width=\"24\" height=\"24\" fill=\"none\" stroke=\"#FAFAFA\" strokeWidth=\"2\" viewBox=\"0 0 24 24\"><path d=\"M9 18l6-6-6-6\"/></svg>\n </button>\n </div>\n </div>\n </article>\n );\n} ","\"use client\"\n\nimport { Button } from \"./ui/button\"\nimport { VendorDisplayButton } from \"./vendor-display-button\"\nimport { useAuth } from \"./auth-stub\"\nimport { formatRelativeTime } from \"../utils/date-utils\"\nimport { UserSummary } from \"./user-summary-stub\"\n\ninterface CommentCardProps {\n comment: {\n id: string\n content: string\n title?: string\n type?: 'pro' | 'con'\n importance?: 'Critical' | 'Important' | 'Nice-to-have'\n createdAt: string\n vendor?: {\n id: number\n title: string\n slug: string\n logo: string | null\n category?: string | null\n }\n user?: {\n id: string\n name: string\n profilePicture: string | null\n msp?: {\n id: string | number\n name?: string | null\n icon_url?: string | null\n }\n }\n canDelete?: boolean\n }\n onViewProduct?: (vendorSlug: string) => void\n onDeleteComment?: (commentId: string) => void\n showVendorInfo?: boolean\n compact?: boolean\n context: 'profile' | 'vendor'\n}\n\nexport function CommentCard({ \n comment, \n onViewProduct, \n onDeleteComment, \n showVendorInfo = true,\n compact = false,\n context = 'profile'\n}: CommentCardProps) {\n const { user: currentUser } = useAuth()\n \n // Use unified date formatting function\n const formatActivityTime = (timestamp: string) => {\n return formatRelativeTime(timestamp);\n }\n\n // Check if current user can delete this comment\n const canUserDeleteComment = () => {\n // If not authenticated, can't delete\n if (!currentUser) return false\n \n // For profile context, use the existing canDelete prop (since it's the user's own comments)\n if (context === 'profile') {\n return comment.canDelete !== false\n }\n \n // For vendor context, check if current user is the comment creator\n if (context === 'vendor' && comment.user) {\n return currentUser.id === comment.user.id\n }\n \n return false\n }\n\n const showDeleteButton = canUserDeleteComment()\n\n return (\n <div className=\"bg-ods-card border border-ods-border rounded-lg p-4 hover:border-ods-accent transition-all group overflow-hidden w-full max-w-full box-border\" style={{ maxWidth: '100%', wordBreak: 'break-word' }}>\n {/* Comment Header */}\n <div className=\"flex flex-col gap-3 mb-3 min-[420px]:flex-row min-[420px]:items-center min-[420px]:justify-between min-[420px]:gap-2 w-full\">\n {/* Row 1: Info + Timestamp */}\n <div className=\"flex items-center gap-2 min-w-0 flex-1\">\n {context === 'profile' && comment.vendor ? (\n <>\n {/* Vendor Button - Icon + Name */}\n <VendorDisplayButton\n vendor={comment.vendor}\n onClick={onViewProduct || (() => {})}\n />\n \n {/* Timestamp */}\n <span className=\"font-['DM_Sans'] font-medium text-[14px] leading-[1.43] text-ods-text-secondary whitespace-nowrap flex-shrink-0\">\n {formatActivityTime(comment.createdAt)}\n </span>\n </>\n ) : context === 'vendor' && comment.user ? (\n <UserSummary\n name={comment.user.name}\n email=\"\"\n compact\n avatarSize={48}\n subtitle={formatActivityTime(comment.createdAt)}\n avatarUrl={comment.user.profilePicture ?? null}\n mspPreview={comment.user.msp ? {\n name: comment.user.msp.name ?? null,\n logoUrl: comment.user.msp.icon_url ?? null,\n seatCount: null,\n technicianCount: null,\n annualRevenue: null,\n } : null}\n showEditButton={false}\n authProviders={[]}\n />\n ) : null}\n </div>\n \n {/* Row 2: Action Buttons */}\n <div className=\"flex gap-2 justify-start min-[420px]:justify-end\">\n {/* Delete Button - shown when user can delete the comment */}\n {showDeleteButton && onDeleteComment && (\n <Button\n onClick={() => onDeleteComment(comment.id)}\n variant=\"outline\"\n size=\"icon\"\n className=\"text-ods-text-primary\"\n >\n <svg width=\"22\" height=\"24\" viewBox=\"0 0 22 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" className=\"text-ods-text-primary\">\n <path d=\"M18.6025 4.37939C19.221 4.43597 19.6768 4.98399 19.6206 5.60254L18.5176 17.7358C18.4492 18.4886 18.3924 19.1183 18.3052 19.6299C18.2155 20.1552 18.0809 20.6447 17.8101 21.1021C17.4444 21.7195 16.9245 22.2292 16.3042 22.583L16.0317 22.7251C15.5519 22.9533 15.0526 23.0438 14.5215 23.0854C14.0042 23.126 13.3727 23.125 12.6172 23.125H9.38281C8.62726 23.125 7.9958 23.126 7.47852 23.0854C7.01357 23.049 6.57289 22.975 6.14844 22.8042L5.96826 22.7251C5.32037 22.4169 4.76543 21.9454 4.35693 21.3599L4.18994 21.1021C3.91912 20.6447 3.78445 20.1552 3.69482 19.6299C3.65123 19.3743 3.61556 19.0894 3.58203 18.7744L3.48242 17.7358L2.37939 5.60254L2.375 5.48682C2.38099 4.91684 2.81768 4.43244 3.39746 4.37939C3.97776 4.32664 4.496 4.72462 4.60449 5.28467L4.62061 5.39746L5.72363 17.5322C5.79528 18.3203 5.84358 18.8475 5.9126 19.252C5.97923 19.6424 6.05232 19.8298 6.12646 19.9551L6.28467 20.1836C6.45963 20.3987 6.68118 20.5732 6.93359 20.6934L7.04639 20.7388C7.17478 20.7822 7.35781 20.8195 7.6543 20.8428C8.06325 20.8749 8.59186 20.875 9.38281 20.875H12.6172C13.4081 20.875 13.9367 20.8749 14.3457 20.8428C14.7409 20.8117 14.9349 20.7559 15.0664 20.6934L15.3066 20.5557C15.5369 20.4008 15.7309 20.1958 15.8735 19.9551C15.9477 19.8298 16.0208 19.6424 16.0874 19.252C16.1564 18.8475 16.2047 18.3203 16.2764 17.5322L17.3794 5.39746C17.436 4.77903 17.984 4.32316 18.6025 4.37939Z\" fill=\"currentColor\"/>\n <path d=\"M7.37451 15.9995V11C7.37451 10.3788 7.87842 9.87526 8.49951 9.875C9.12083 9.875 9.62451 10.3787 9.62451 11V15.9995C9.62451 16.6208 9.12083 17.1245 8.49951 17.1245C7.87842 17.1242 7.37451 16.6207 7.37451 15.9995ZM12.3755 15.9995V11C12.3755 10.3787 12.8792 9.875 13.5005 9.875C14.1216 9.87526 14.6255 10.3788 14.6255 11V15.9995C14.6255 16.6207 14.1216 17.1242 13.5005 17.1245C12.8792 17.1245 12.3755 16.6208 12.3755 15.9995ZM12.585 0.875C13.0174 0.879157 13.425 0.903301 13.8096 1.02881L14.1157 1.14746C14.4155 1.28159 14.6931 1.46221 14.9375 1.68359L15.0811 1.82568C15.4034 2.16872 15.6379 2.59872 15.8984 3.05469L16.6528 4.37451H20L20.1157 4.38037C20.6825 4.43826 21.1248 4.91747 21.125 5.49951C21.125 6.08174 20.6827 6.5607 20.1157 6.61865L20 6.62451H2C1.37868 6.62451 0.875 6.12083 0.875 5.49951C0.875264 4.87842 1.37884 4.37451 2 4.37451H5.34717L6.32129 2.6709C6.53952 2.29735 6.76267 1.95525 7.0625 1.68359L7.31738 1.47705C7.58262 1.28327 7.87687 1.1312 8.19043 1.02881L8.38525 0.974609C8.71199 0.896341 9.05467 0.878464 9.41504 0.875H12.585ZM9.85742 3.125C9.29191 3.125 9.08123 3.13201 8.97266 3.14844L8.88916 3.16748C8.83075 3.18655 8.77503 3.21301 8.72217 3.24365L8.57275 3.35205C8.53704 3.38448 8.50137 3.42629 8.42773 3.54102L8.05566 4.1709L7.93848 4.37451H14.0615L13.9443 4.1709C13.6645 3.68114 13.554 3.50141 13.4858 3.41504L13.4272 3.35205C13.3815 3.31062 13.3309 3.27443 13.2778 3.24365L13.1108 3.16748C13.019 3.13753 12.8946 3.125 12.1426 3.125H9.85742Z\" fill=\"currentColor\"/>\n </svg>\n </Button>\n )}\n </div>\n </div>\n \n {/* Comment Content */}\n <div className=\"space-y-2 w-full max-w-full overflow-hidden\" style={{ maxWidth: '100%', wordBreak: 'break-word' }}>\n {comment.title && (\n <div className={compact ? \"h-[20px] flex items-center\" : \"h-[24px] flex items-center\"}>\n <h4 className=\"text-h3 tracking-[-0.02em] text-ods-text-primary group-hover:text-ods-accent transition-colors line-clamp-1\" style={{ \n wordBreak: 'break-word', \n overflowWrap: 'break-word', \n maxWidth: '100%',\n textOverflow: 'ellipsis',\n overflow: 'hidden',\n display: '-webkit-box',\n WebkitBoxOrient: 'vertical',\n WebkitLineClamp: 1\n }}>\n {comment.title}\n </h4>\n </div>\n )}\n <div className={compact ? \"h-[60px] flex items-center\" : \"h-[72px] flex items-center\"}>\n <p className=\"text-h4 text-ods-text-primary line-clamp-3\" style={{ \n wordBreak: 'break-word', \n overflowWrap: 'break-word', \n maxWidth: '100%',\n textOverflow: 'ellipsis',\n overflow: 'hidden',\n display: '-webkit-box',\n WebkitBoxOrient: 'vertical',\n WebkitLineClamp: 3\n }}>\n {comment.content}\n </p>\n </div>\n </div>\n </div>\n )\n}","\"use client\"\n\nimport { Button } from \"./ui/button\"\nimport { getVendorLogo, VendorWithMedia } from \"../utils/vendor-media-stub\"\nimport Image from \"../embed-shims/next-image\"\nimport { getProxiedImageUrl } from \"../utils/image-proxy-stub\"\n\ninterface VendorDisplayButtonProps {\n vendor: VendorWithMedia\n onClick?: (vendorSlug: string) => void\n variant?: 'default' | 'compact'\n externalUrl?: string\n}\n\nexport function VendorDisplayButton({ vendor, onClick, variant = 'default', externalUrl }: VendorDisplayButtonProps) {\n const handleClick = () => {\n if (externalUrl && vendor.slug) {\n // `externalUrl` is the caller-resolved platform base URL (the openmsp SSOT via\n // getPlatformProductionUrl, scheme-normalized). The old `process.env.NEXT_PUBLIC_OPENMSP_URL`\n // override is gone — it's stored scheme-less, which made this a relative window.open().\n window.open(`${externalUrl}/vendor/${vendor.slug}`, '_blank', 'noopener,noreferrer')\n } else if (onClick && vendor.slug) {\n onClick(vendor.slug)\n }\n }\n\n // Compact variant for flamingo-teaser\n if (variant === 'compact') {\n const logoUrl = getVendorLogo(vendor)\n \n return (\n <button \n onClick={handleClick}\n className=\"inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-ods-card border border-ods-border hover:border-ods-accent/50 transition-colors\"\n >\n {logoUrl ? (\n <div className=\"w-5 h-5 rounded overflow-hidden flex-shrink-0\">\n <Image\n src={getProxiedImageUrl(logoUrl) || logoUrl}\n alt={`${vendor.title} logo`}\n width={20}\n height={20}\n className=\"w-full h-full object-cover\"\n />\n </div>\n ) : (\n <div className=\"w-5 h-5 rounded bg-ods-border flex items-center justify-center flex-shrink-0\">\n <span className=\"text-ods-text-secondary text-[10px] font-medium\">\n {vendor.title.charAt(0).toUpperCase()}\n </span>\n </div>\n )}\n <span className=\"text-sm font-medium text-ods-text-primary\">\n {vendor.title}\n </span>\n </button>\n )\n }\n\n // Default variant\n return (\n <button \n onClick={handleClick}\n className=\"flex items-center gap-2 bg-ods-card border border-ods-border rounded-lg py-2 px-3 hover:border-ods-accent transition-colors max-w-full overflow-hidden\"\n >\n {getVendorLogo(vendor) ? (\n <div className=\"w-8 h-8 bg-ods-card border border-ods-border rounded-lg flex items-center justify-center flex-shrink-0\">\n <Image\n src={getProxiedImageUrl(getVendorLogo(vendor)!) || getVendorLogo(vendor)!}\n alt={`${vendor.title} logo`}\n width={24}\n height={24}\n className=\"rounded object-cover\"\n />\n </div>\n ) : (\n <div className=\"w-8 h-8 bg-ods-border border border-ods-border rounded-lg flex items-center justify-center flex-shrink-0\">\n <span className=\"text-ods-text-primary text-[12px] font-medium\">\n {vendor.title.charAt(0)}\n </span>\n </div>\n )}\n <span className=\"text-h4 text-ods-text-primary truncate min-w-0\" title={vendor.title}>\n {vendor.title}\n </span>\n </button>\n )\n} ","\"use client\"\n\n// Stub auth provider and hooks\nimport { createContext, useContext } from 'react';\n\ninterface AuthContextType {\n user: any | null;\n isLoading: boolean;\n}\n\nconst AuthContext = createContext<AuthContextType>({\n user: null,\n isLoading: false,\n});\n\n// Global reference to real auth hook when available\nlet realUseAuth: (() => any) | null = null;\n\nexport function setRealAuthHook(authHook: () => any) {\n realUseAuth = authHook;\n}\n\nexport function useAuth() {\n // Use real auth hook if available (when used in main app)\n if (realUseAuth) {\n try {\n const realAuth = realUseAuth();\n if (realAuth && realAuth.user) {\n return realAuth;\n }\n } catch (error) {\n // Fallback if real auth fails\n }\n }\n\n // Fallback mock user for UI kit context\n return {\n user: { id: 'mock-user-id', name: 'Mock User' },\n isLoading: false\n };\n}\n\nexport function AuthProvider({ children }: { children: React.ReactNode }) {\n return (\n <AuthContext.Provider value={{ user: { id: 'mock-user-id', name: 'Mock User' }, isLoading: false }}>\n {children as any}\n </AuthContext.Provider>\n );\n}","\"use client\";\n\nimport Image from \"../embed-shims/next-image\";\nimport { getProxiedImageUrl } from \"../utils/image-proxy-stub\";\n\ninterface Props {\n name: string;\n email: string;\n avatarUrl?: string | null;\n /** Optional subtitle text (e.g., relative time) to replace email line */\n subtitle?: string | null;\n /** Authentication provider names (e.g. [\"google\", \"microsoft\"]) */\n authProviders?: string[];\n /** Show an outline Edit Profile button that routes to editHref */\n showEditButton?: boolean;\n /** Path to navigate when Edit button clicked (default \"/profile/edit\") */\n editHref?: string;\n /** Optional userId/profile passed through to EditProfileButton (for analytics) */\n userId?: string;\n profileData?: any;\n /** Optional MSP preview info to render below email */\n mspPreview?: {\n name?: string | null;\n seatCount?: number | null;\n technicianCount?: number | null;\n annualRevenue?: number | null;\n logoUrl?: string | null;\n } | null;\n /** Compact mode (avatar + name row, used in comment headers) */\n compact?: boolean;\n /** Avatar size in px for compact mode (defaults 40) */\n avatarSize?: number;\n\n /** When true, replaces the static avatar with the ProfilePhotoUpload widget */\n editablePhoto?: boolean;\n /** Required when editablePhoto=true – receives new photo URL */\n onPhotoChange?: (url: string | null) => void;\n}\n\nconst getAuthProviderIcon = (provider: string) => {\n const p = provider.toLowerCase();\n switch (p) {\n case \"google\":\n return <Image src=\"/icons/google-logo.svg\" alt=\"Google\" width={16} height={16} className=\"w-4 h-4\" />;\n case \"microsoft\":\n case \"azure\":\n return <Image src=\"/icons/microsoft-logo.svg\" alt=\"Microsoft\" width={16} height={16} className=\"w-4 h-4\" />;\n case \"slack\":\n case \"slack_oidc\":\n return <div className=\"w-4 h-4 bg-ods-text-secondary rounded-full\" />;\n default:\n return <div className=\"w-4 h-4 bg-ods-text-secondary rounded-full\" />;\n }\n};\n\n// Abbreviate large numbers: 1 200 → 1.2K , 15 000 → 15K , 2 000 000 → 2M\nconst formatNumber = (n: number) => {\n if (n >= 1_000_000_000) {\n const value = n / 1_000_000_000;\n return `${Number.isInteger(value) ? value.toFixed(0) : value.toFixed(1)}B`;\n }\n if (n >= 1_000_000) {\n const value = n / 1_000_000;\n return `${Number.isInteger(value) ? value.toFixed(0) : value.toFixed(1)}M`;\n }\n if (n >= 1_000) {\n return `${Math.round(n / 1_000)}K`;\n }\n return n.toLocaleString();\n};\n\nexport function UserSummary({\n name,\n email,\n subtitle = null,\n avatarUrl,\n authProviders,\n showEditButton = false,\n editHref = \"/profile/edit\",\n userId,\n profileData,\n mspPreview,\n compact = false,\n avatarSize = 40,\n editablePhoto = false,\n onPhotoChange,\n}: Props) {\n // Compact variant: minimal horizontal row\n if (compact) {\n return (\n <div className=\"flex items-center gap-3 min-w-0\">\n <div className=\"relative shrink-0\">\n {avatarUrl ? (\n <Image src={getProxiedImageUrl(avatarUrl) ?? avatarUrl} alt={name} width={avatarSize} height={avatarSize} className=\"object-cover rounded-lg\" />\n ) : (\n <div className=\"rounded-lg bg-ods-accent flex items-center justify-center text-ods-text-on-accent font-heading font-bold\" style={{ width: avatarSize, height: avatarSize }}>\n {name.split(' ').map((n: string) => n.charAt(0)).join('').slice(0, 2)}\n </div>\n )}\n {mspPreview && mspPreview.logoUrl && (\n <Image src={getProxiedImageUrl(mspPreview.logoUrl) ?? mspPreview.logoUrl} alt={mspPreview.name || 'MSP'} width={24} height={24} className=\"absolute -bottom-1 -right-1 size-6 rounded-full object-cover select-none z-10\" />\n )}\n </div>\n <div className=\"min-w-0 flex-1\">\n <p\n className=\"text-h4 text-ods-text-primary truncate\"\n title={mspPreview?.name ? `${name} • ${mspPreview.name}` : name}\n >\n {name}\n {mspPreview?.name && (\n <span className=\"text-ods-text-secondary\"> • {mspPreview.name}</span>\n )}\n </p>\n <p className=\"text-h6 text-ods-text-secondary truncate\" title={subtitle && subtitle.trim().length > 0 ? subtitle : (email && email.trim().length > 0 ? email : '\\u00A0')}>\n {subtitle && subtitle.trim().length > 0 ? subtitle : (email && email.trim().length > 0 ? email : '\\u00A0')}\n </p>\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"flex flex-col gap-4 w-full\">\n {/* Header Row */}\n <div className=\"flex gap-6 w-full items-start\">\n {/* Avatar with badge wrapper */}\n <div className=\"relative shrink-0 h-24 w-24 overflow-visible\">\n {avatarUrl ? (\n <Image src={getProxiedImageUrl(avatarUrl) ?? avatarUrl} alt={name} width={96} height={96} className=\"object-cover rounded-full\" />\n ) : (\n <div className=\"rounded-full bg-ods-card border border-ods-border w-full h-full flex items-center justify-center text-3xl text-ods-text-secondary font-heading\">\n {name.charAt(0).toUpperCase()}\n </div>\n )}\n\n {/* MSP logo badge (show only when MSP exists) */}\n {mspPreview && (\n <div className=\"absolute -bottom-1 -right-1 size-10 rounded-full bg-ods-bg ring-2 ring-ods-border overflow-hidden flex items-center justify-center select-none\">\n {mspPreview.logoUrl ? (\n <Image\n src={getProxiedImageUrl(mspPreview.logoUrl) ?? mspPreview.logoUrl}\n alt={mspPreview.name || 'MSP Logo'}\n width={40}\n height={40}\n className=\"object-cover\"\n />\n ) : (\n <span className=\"text-ods-text-primary font-heading text-sm font-bold\">\n {mspPreview.name?.charAt(0).toUpperCase() || '?'}\n </span>\n )}\n </div>\n )}\n </div>\n\n {/* Info + actions block */}\n <div className=\"flex-1 grid grid-cols-[1fr_auto] gap-4\">\n {/* LEFT : text stack */}\n <div className=\"min-h-[6rem] flex flex-col justify-center space-y-3 truncate\">\n <p className=\"text-h2 text-ods-text-primary leading-none truncate\" title={name}>\n {name}\n </p>\n <p className=\"text-h4 text-ods-text-secondary break-all truncate\" title={(subtitle && subtitle.trim().length > 0) ? subtitle : (email && email.trim().length > 0 ? email : '\\u00A0')}>\n {(subtitle && subtitle.trim().length > 0) ? subtitle : (email && email.trim().length > 0 ? email : '\\u00A0')}\n </p>\n {mspPreview && (() => {\n const mspSegments = [\n mspPreview.name ?? '—',\n typeof mspPreview.seatCount === 'number'\n ? `${formatNumber(mspPreview.seatCount)} Seats`\n : null,\n typeof mspPreview.technicianCount === 'number'\n ? `${formatNumber(mspPreview.technicianCount)} Technicians`\n : null,\n typeof mspPreview.annualRevenue === 'number'\n ? `$${formatNumber(mspPreview.annualRevenue)}`\n : null,\n ].filter(Boolean) as string[];\n const mspTitle = mspSegments.join(' • ');\n return (\n <p className=\"text-h6 text-ods-text-primary truncate\" title={mspTitle}>\n {/* Build string with separators */}\n {mspSegments\n .flatMap((txt, idx) => (idx === 0 ? [txt] : [' • ', txt]))\n .map((seg, idx) => (\n <span key={idx} className={seg === ' • ' ? 'text-ods-text-secondary' : ''}>{seg}</span>\n ))}\n </p>\n );\n })()}\n </div>\n\n {/* RIGHT (desktop) */}\n {(authProviders?.length || showEditButton) && (\n <div className=\"hidden md:flex flex-col items-end justify-between flex-shrink-0 min-h-[6rem]\">\n {/* top part */}\n {authProviders && authProviders.length > 0 && (\n <div className=\"flex items-center gap-2\">\n <span className=\"text-xs text-ods-text-secondary whitespace-nowrap select-none\">\n Authorized by\n </span>\n <div className=\"flex items-center gap-2\">\n {authProviders.map((p) => (\n <div key={p} className=\"flex items-center justify-center w-4 h-4\">\n {getAuthProviderIcon(p)}\n </div>\n ))}\n </div>\n </div>\n )}\n\n {/* bottom part - Edit button would go here */}\n {showEditButton && (\n <div className=\"text-xs text-ods-text-secondary\">Edit Profile</div>\n )}\n </div>\n )}\n </div>\n </div>\n\n {/* Mobile row: Authorized by left, Edit btn right */}\n {(authProviders?.length || showEditButton) && (\n <div className=\"flex md:hidden items-center justify-between w-full gap-4\">\n {authProviders && authProviders.length > 0 && (\n <div className=\"flex items-center gap-2\">\n <span className=\"text-xs text-ods-text-secondary whitespace-nowrap select-none\">Authorized by</span>\n <div className=\"flex items-center gap-2\">\n {authProviders.map((p) => (\n <div key={p} className=\"flex items-center justify-center w-4 h-4\">\n {getAuthProviderIcon(p)}\n </div>\n ))}\n </div>\n </div>\n )}\n\n {showEditButton && (\n <div className=\"text-xs text-ods-text-secondary\">Edit Profile</div>\n )}\n </div>\n )}\n </div>\n );\n}","\"use client\"\n\nimport { ReactNode } from \"react\"\nimport { cn } from \"../utils/cn\"\n\ninterface ContentLoadingContainerProps {\n /**\n * Whether the content is currently loading\n */\n isLoading: boolean\n /**\n * The actual content to display when not loading\n */\n children: ReactNode\n /**\n * The skeleton component to show during loading\n */\n skeletonComponent: ReactNode\n /**\n * Additional CSS classes\n */\n className?: string\n /**\n * Minimum height to prevent layout jumps\n */\n minHeight?: string\n /**\n * Loading overlay opacity (0-1)\n */\n loadingOpacity?: number\n /**\n * Transition duration in milliseconds\n */\n transitionDuration?: number\n}\n\n/**\n * ContentLoadingContainer\n * \n * A unified loading container that wraps card grids and manages loading states \n * while keeping UI controls persistent. This component:\n * \n * - Shows skeleton loading only in content area\n * - Maintains consistent layout dimensions during loading\n * - Provides smooth transitions between loading and loaded states\n * - Prevents layout jumps by preserving container height\n * - Supports customizable skeleton components for different content types\n * \n * Usage:\n * ```tsx\n * <ContentLoadingContainer\n * isLoading={isLoadingVendors}\n * skeletonComponent={<CardSkeletonGrid variant=\"vendor\" count={12} />}\n * minHeight=\"min-h-[800px]\"\n * >\n * <VendorGrid vendors={vendors} />\n * </ContentLoadingContainer>\n * ```\n */\nexport function ContentLoadingContainer({\n isLoading,\n children,\n skeletonComponent,\n className,\n minHeight = \"min-h-[300px] md:min-h-[800px]\",\n loadingOpacity = 1,\n transitionDuration = 300,\n}: ContentLoadingContainerProps) {\n return (\n <div \n className={cn(\n \"relative w-full transition-all ease-in-out\",\n minHeight,\n className\n )}\n style={{\n transitionDuration: `${transitionDuration}ms`\n }}\n role=\"region\"\n aria-label={isLoading ? \"Loading content\" : \"Content loaded\"}\n aria-live=\"polite\"\n >\n {/* Loading Skeleton Overlay */}\n {isLoading && (\n <div \n className=\"absolute inset-0 z-10 bg-ods-bg\"\n style={{ \n opacity: loadingOpacity,\n transition: `opacity ${transitionDuration}ms ease-in-out`\n }}\n role=\"status\"\n aria-label=\"Loading content\"\n >\n {skeletonComponent}\n </div>\n )}\n\n {/* Actual Content */}\n <div \n className={cn(\n \"relative transition-opacity ease-in-out\",\n isLoading && \"opacity-0\"\n )}\n style={{\n transitionDuration: `${transitionDuration}ms`\n }}\n aria-hidden={isLoading}\n >\n {children}\n </div>\n </div>\n )\n}\n\n/**\n * Hook for managing content loading states with common patterns\n */\nexport function useContentLoading(isLoading: boolean) {\n const containerProps = {\n 'aria-busy': isLoading,\n 'data-loading': isLoading\n }\n\n const getSkeletonCount = (contentType: 'vendor' | 'blog') => {\n return contentType === 'vendor' ? 12 : 6\n }\n\n const getMinHeight = (contentType: 'vendor' | 'blog') => {\n // Vendor cards are typically taller, need more space\n return contentType === 'vendor' \n ? \"min-h-[400px] md:min-h-[900px]\" \n : \"min-h-[300px] md:min-h-[800px]\"\n }\n\n return {\n containerProps,\n getSkeletonCount,\n getMinHeight\n }\n} ","\"use client\";\n\nimport React from 'react';\nimport { cn } from \"../utils/cn\";\nimport { useDynamicTheming } from '../hooks/use-dynamic-theming';\nimport { useThemeAware } from '../hooks/use-theme-aware';\n\ninterface DynamicSkeletonProps {\n /**\n * Type of skeleton animation\n */\n animation?: 'pulse' | 'shimmer' | 'wave' | 'static';\n \n /**\n * Skeleton variant based on content type\n */\n variant?: 'text' | 'avatar' | 'card' | 'button' | 'image' | 'custom';\n \n /**\n * Size preset for common elements\n */\n size?: 'sm' | 'md' | 'lg' | 'xl';\n \n /**\n * Custom dimensions\n */\n width?: string | number;\n height?: string | number;\n \n /**\n * Number of lines for text skeleton\n */\n lines?: number;\n \n /**\n * Platform-aware styling\n */\n platformAware?: boolean;\n \n /**\n * Accessibility enhancements\n */\n includeAriaLabel?: boolean;\n \n /**\n * Custom CSS classes\n */\n className?: string;\n \n /**\n * Child elements (for container skeletons)\n */\n children?: React.ReactNode;\n}\n\nconst sizePresets = {\n sm: { width: '4rem', height: '1rem' },\n md: { width: '8rem', height: '1.5rem' },\n lg: { width: '12rem', height: '2rem' },\n xl: { width: '16rem', height: '2.5rem' }\n};\n\nconst variantStyles = {\n text: 'rounded',\n avatar: 'rounded-full aspect-square',\n card: 'rounded-lg',\n button: 'rounded-md',\n image: 'rounded-lg aspect-video',\n custom: ''\n};\n\nexport function DynamicSkeleton({\n animation = 'shimmer',\n variant = 'text',\n size = 'md',\n width,\n height,\n lines = 1,\n platformAware = true,\n includeAriaLabel = true,\n className,\n children\n}: DynamicSkeletonProps) {\n const { platform, isDark, accentColor } = useThemeAware();\n\n // Calculate dimensions\n const dimensions = React.useMemo(() => {\n if (width || height) {\n return {\n width: typeof width === 'number' ? `${width}px` : width,\n height: typeof height === 'number' ? `${height}px` : height\n };\n }\n \n if (variant === 'avatar') {\n const avatarSize = sizePresets[size].height;\n return { width: avatarSize, height: avatarSize };\n }\n \n return sizePresets[size];\n }, [width, height, size, variant]);\n\n // Platform-specific skeleton colors\n const platformStyles = React.useMemo(() => {\n if (!platformAware) return {};\n\n const baseOpacity = isDark ? 0.1 : 0.15;\n const accentOpacity = isDark ? 0.05 : 0.08;\n\n return {\n '--skeleton-base': `color-mix(in srgb, var(--color-bg-skeleton) ${baseOpacity * 100}%, transparent)`,\n '--skeleton-highlight': `color-mix(in srgb, ${accentColor} ${accentOpacity * 100}%, var(--color-bg-skeleton))`,\n '--skeleton-accent': `color-mix(in srgb, ${accentColor} 10%, transparent)`\n } as React.CSSProperties;\n }, [platformAware, isDark, accentColor]);\n\n // Animation classes\n const animationClasses = {\n pulse: 'animate-pulse',\n shimmer: 'ods-loading-dynamic',\n wave: 'skeleton-wave',\n static: ''\n };\n\n // Base skeleton classes\n const baseClasses = cn(\n 'bg-ods-skeleton',\n variantStyles[variant],\n animationClasses[animation],\n platformAware && `skeleton-platform-${platform}`,\n className\n );\n\n // For text skeletons with multiple lines\n if (variant === 'text' && lines > 1) {\n return (\n <div \n className=\"space-y-2\"\n style={platformStyles}\n role=\"status\"\n aria-label={includeAriaLabel ? \"Loading content...\" : undefined}\n >\n {Array.from({ length: lines }, (_, index) => (\n <div\n key={index}\n className={cn(\n baseClasses,\n index === lines - 1 && 'w-3/4' // Last line shorter\n )}\n style={{\n ...dimensions,\n width: index === lines - 1 ? '75%' : dimensions.width\n }}\n />\n ))}\n </div>\n );\n }\n\n // Single skeleton element\n return (\n <div\n className={baseClasses}\n style={{ ...dimensions, ...platformStyles }}\n role=\"status\"\n aria-label={includeAriaLabel ? \"Loading content...\" : undefined}\n >\n {children}\n </div>\n );\n}\n\n/**\n * Pre-configured skeleton for common UI patterns\n */\nexport const SkeletonPresets = {\n /**\n * Card skeleton with header, content, and actions\n */\n Card: ({ showActions = true, showImage = false }: { showActions?: boolean; showImage?: boolean }) => (\n <div className=\"bg-ods-card border border-ods-border rounded-lg p-6 space-y-4\">\n {showImage && (\n <DynamicSkeleton variant=\"image\" className=\"w-full h-40\" />\n )}\n <div className=\"space-y-2\">\n <DynamicSkeleton variant=\"text\" size=\"lg\" />\n <DynamicSkeleton variant=\"text\" lines={2} size=\"md\" />\n </div>\n {showActions && (\n <div className=\"flex gap-2 pt-2\">\n <DynamicSkeleton variant=\"button\" width=\"6rem\" height=\"2.5rem\" />\n <DynamicSkeleton variant=\"button\" width=\"4rem\" height=\"2.5rem\" />\n </div>\n )}\n </div>\n ),\n\n /**\n * User profile skeleton\n */\n Profile: () => (\n <div className=\"flex items-start gap-4 p-4\">\n <DynamicSkeleton variant=\"avatar\" size=\"lg\" />\n <div className=\"flex-1 space-y-2\">\n <DynamicSkeleton variant=\"text\" size=\"lg\" width=\"8rem\" />\n <DynamicSkeleton variant=\"text\" size=\"sm\" width=\"12rem\" />\n <DynamicSkeleton variant=\"text\" lines={2} size=\"md\" />\n </div>\n </div>\n ),\n\n /**\n * Navigation skeleton\n */\n Navigation: ({ items = 5 }: { items?: number }) => (\n <nav className=\"space-y-2 p-4\">\n {Array.from({ length: items }, (_, index) => (\n <div key={index} className=\"flex items-center gap-3\">\n <DynamicSkeleton variant=\"avatar\" size=\"sm\" />\n <DynamicSkeleton variant=\"text\" size=\"md\" />\n </div>\n ))}\n </nav>\n ),\n\n /**\n * Table skeleton\n */\n Table: ({ rows = 5, columns = 4 }: { rows?: number; columns?: number }) => (\n <div className=\"space-y-3\">\n {/* Header */}\n <div className=\"grid gap-4\" style={{ gridTemplateColumns: `repeat(${columns}, 1fr)` }}>\n {Array.from({ length: columns }, (_, index) => (\n <DynamicSkeleton key={`header-${index}`} variant=\"text\" size=\"md\" />\n ))}\n </div>\n {/* Rows */}\n {Array.from({ length: rows }, (_, rowIndex) => (\n <div key={`row-${rowIndex}`} className=\"grid gap-4\" style={{ gridTemplateColumns: `repeat(${columns}, 1fr)` }}>\n {Array.from({ length: columns }, (_, colIndex) => (\n <DynamicSkeleton key={`cell-${rowIndex}-${colIndex}`} variant=\"text\" size=\"sm\" />\n ))}\n </div>\n ))}\n </div>\n ),\n\n /**\n * Vendor grid skeleton (specific to the app)\n */\n VendorGrid: ({ items = 6 }: { items?: number }) => (\n <div className=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6\">\n {Array.from({ length: items }, (_, index) => (\n <div key={index} className=\"bg-ods-card border border-ods-border rounded-lg p-4 space-y-3\">\n <div className=\"flex items-center gap-3\">\n <DynamicSkeleton variant=\"avatar\" size=\"md\" />\n <div className=\"flex-1\">\n <DynamicSkeleton variant=\"text\" size=\"md\" width=\"6rem\" />\n <DynamicSkeleton variant=\"text\" size=\"sm\" width=\"4rem\" className=\"mt-1\" />\n </div>\n </div>\n <DynamicSkeleton variant=\"text\" lines={2} size=\"sm\" />\n <div className=\"flex gap-2\">\n <DynamicSkeleton variant=\"custom\" width=\"3rem\" height=\"1.25rem\" className=\"rounded-full\" />\n <DynamicSkeleton variant=\"custom\" width=\"4rem\" height=\"1.25rem\" className=\"rounded-full\" />\n </div>\n </div>\n ))}\n </div>\n ),\n\n /**\n * Blog post skeleton\n */\n BlogPost: () => (\n <article className=\"space-y-6\">\n <div className=\"space-y-2\">\n <DynamicSkeleton variant=\"text\" size=\"xl\" width=\"75%\" />\n <div className=\"flex gap-4 text-sm\">\n <DynamicSkeleton variant=\"text\" size=\"sm\" width=\"4rem\" />\n <DynamicSkeleton variant=\"text\" size=\"sm\" width=\"6rem\" />\n </div>\n </div>\n <DynamicSkeleton variant=\"image\" className=\"w-full h-64\" />\n <div className=\"space-y-3\">\n {Array.from({ length: 4 }, (_, index) => (\n <DynamicSkeleton key={index} variant=\"text\" lines={3} size=\"md\" />\n ))}\n </div>\n </article>\n )\n};\n\n/**\n * Platform-aware loading container that shows different skeletons based on platform\n */\nexport function PlatformSkeletonContainer({ \n children, \n isLoading, \n skeletonType = 'Card',\n skeletonProps = {}\n}: {\n children: React.ReactNode;\n isLoading: boolean;\n skeletonType?: keyof typeof SkeletonPresets;\n skeletonProps?: any;\n}) {\n const { platform } = useThemeAware();\n\n if (!isLoading) {\n return <>{children}</>;\n }\n\n const SkeletonComponent = SkeletonPresets[skeletonType];\n\n return (\n <div className={`platform-skeleton skeleton-${platform}`}>\n <SkeletonComponent {...skeletonProps} />\n </div>\n );\n}\n\n/**\n * Enhanced skeleton with progressive enhancement\n */\nexport function ProgressiveSkeleton({\n stages = ['basic', 'detailed', 'interactive'],\n currentStage = 0,\n children,\n ...props\n}: DynamicSkeletonProps & {\n stages?: string[];\n currentStage?: number;\n}) {\n const { platform } = useThemeAware();\n\n // Show more detailed skeleton as loading progresses\n const stageConfig = {\n 0: { animation: 'pulse' as const, variant: 'text' as const },\n 1: { animation: 'shimmer' as const, variant: 'card' as const },\n 2: { animation: 'wave' as const, variant: 'custom' as const }\n };\n\n const config = stageConfig[currentStage as keyof typeof stageConfig] || stageConfig[0];\n\n return (\n <DynamicSkeleton\n {...props}\n {...config}\n className={cn(\n props.className,\n `skeleton-stage-${currentStage}`,\n `skeleton-platform-${platform}`\n )}\n >\n {children}\n </DynamicSkeleton>\n );\n}","// Stub implementation for useThemeAware hook\n\nexport function useThemeAware() {\n return {\n themeColors: {},\n platformContext: 'default',\n isDarkMode: false,\n platform: 'default',\n isDark: false,\n accentColor: '#FFC008'\n };\n}","\"use client\"\n\nimport { cn } from \"../utils/cn\"\nimport { X } from \"lucide-react\"\n\n// Unified FilterChip component for consistent styling across the application\ninterface FilterChipProps {\n id: string\n label: string\n variant?: 'selected' | 'unselected' | 'category' | 'subcategory' | 'tag' | 'info'\n size?: 'sm' | 'md'\n removable?: boolean\n onRemove?: () => void\n onClick?: () => void\n disabled?: boolean\n className?: string\n}\n\nexport function FilterChip({\n id,\n label,\n variant = 'unselected',\n size = 'md',\n removable = false,\n onRemove,\n onClick,\n disabled = false,\n className\n}: FilterChipProps) {\n const baseClasses = cn(\n \"inline-flex items-center justify-center rounded-full font-medium transition-all duration-200 shrink-0 group cursor-pointer\",\n \"hover:shadow-md hover:scale-105 focus:outline-none focus:ring-2 focus:ring-offset-2\",\n \"font-['DM_Sans'] leading-none\",\n // Size variants - enhanced mobile sizing for better visibility and touch targets\n size === 'sm' \n ? \"text-sm pl-3 pr-3 py-1 md:text-sm md:pl-3 md:pr-3 md:py-1\"\n : \"text-sm pl-3 pr-3 py-2 md:text-sm md:pl-3 md:pr-3 md:py-2\",\n // Add gap only if removable (has X button) - placed after text\n removable && \"gap-1 md:gap-1\",\n // Disabled state\n disabled && \"opacity-50 cursor-not-allowed hover:scale-100 hover:shadow-none\"\n )\n\n const variantClasses = {\n // Legacy variants (for backward compatibility)\n selected: \"bg-[#2A2A2A] text-[#E8E8E8] border border-[#FFC008] hover:bg-ods-border hover:border-[#FFD951] focus:ring-[#FFC008] focus:ring-offset-[#161616]\",\n unselected: \"bg-[#2A2A2A] text-[#CCCCCC] border border-[#4A4A4A] hover:bg-ods-border hover:border-[#5A5A5A] hover:text-ods-text-primary focus:ring-[#4A4A4A] focus:ring-offset-[#161616]\",\n info: \"bg-ods-border text-[#CCCCCC] border border-[#5A5A5A] cursor-default hover:scale-100 hover:shadow-none focus:ring-[#5A5A5A] focus:ring-offset-[#161616]\",\n \n // New subtle selected variants - same backgrounds/text, only border colors different\n category: \"bg-[#2A2A2A] text-[#E8E8E8] border border-[#FFC008]/40 hover:bg-ods-border hover:border-[#FFC008]/60 hover:text-ods-text-primary focus:ring-[#FFC008]/40 focus:ring-offset-[#161616]\",\n subcategory: \"bg-[#2A2A2A] text-[#E8E8E8] border border-[#FFC008]/60 hover:bg-ods-border hover:border-[#FFC008]/80 hover:text-ods-text-primary focus:ring-[#FFC008]/60 focus:ring-offset-[#161616]\",\n tag: \"bg-[#2A2A2A] text-ods-text-primary border border-[#FFC008]/20 hover:bg-ods-border hover:border-[#FFC008]/30 hover:text-ods-text-primary focus:ring-[#FFC008]/20 focus:ring-offset-[#161616]\",\n }\n\n return (\n <div\n className={cn(baseClasses, variantClasses[variant], className)}\n onClick={disabled ? undefined : (e) => {\n e.preventDefault();\n e.stopPropagation();\n onClick?.();\n }}\n role={onClick ? \"button\" : undefined}\n tabIndex={onClick && !disabled ? 0 : undefined}\n aria-pressed={onClick && variant === 'selected' ? true : undefined}\n aria-disabled={disabled}\n >\n <span className={cn(\n \"truncate font-['DM_Sans'] font-medium leading-none text-center\",\n size === 'sm' ? \"max-w-[100px] md:max-w-[120px]\" : \"max-w-[120px] md:max-w-[140px]\"\n )} title={label}>\n {label}\n </span>\n {removable && onRemove && (\n <button\n type=\"button\"\n onClick={(e) => {\n e.preventDefault()\n e.stopPropagation()\n if (!disabled) onRemove()\n }}\n disabled={disabled}\n className={cn(\n \"flex items-center justify-center w-4 h-4 md:w-4 md:h-4 rounded-full\",\n \"transition-all duration-200 shrink-0\",\n \"group-hover:scale-110 focus:outline-none focus:ring-2 focus:ring-offset-1\",\n variant === 'category'\n ? \"hover:bg-[#4A4A4A] text-[#E8E8E8] focus:ring-[#E8E8E8] focus:ring-offset-[#2A2A2A]\"\n : variant === 'subcategory'\n ? \"hover:bg-[#4A4A4A] text-[#E8E8E8] focus:ring-[#E8E8E8] focus:ring-offset-[#2A2A2A]\"\n : variant === 'tag'\n ? \"hover:bg-[#4A4A4A] text-[#E8E8E8] focus:ring-[#E8E8E8] focus:ring-offset-[#2A2A2A]\"\n : \"hover:bg-[#4A4A4A] text-[#E8E8E8] focus:ring-[#E8E8E8] focus:ring-offset-[#2A2A2A]\",\n disabled && \"opacity-50 cursor-not-allowed hover:scale-100\"\n )}\n aria-label={`Remove ${label} filter`}\n tabIndex={disabled ? -1 : 0}\n >\n <X className=\"h-2 w-2 md:h-2 md:w-2\" />\n </button>\n )}\n </div>\n )\n} ","\"use client\"\n\nimport { Suspense } from \"react\"\nimport { Skeleton } from \"./ui/skeleton\"\nimport { SocialIconRow } from \"./social-icon-row\"\n\ninterface FooterLink {\n href: string\n label: string\n}\n\ninterface FooterSection {\n title: string\n links: FooterLink[]\n}\n\ninterface FooterConfig {\n name: string\n legalName: string\n description: string\n logo?: React.ReactNode\n sections: FooterSection[]\n customComponent?: React.ReactNode // Inject any custom component here\n nameElement?: React.ReactNode // Custom element for platform name with specific font\n hideSocialRow?: boolean // Hide the default social row\n rightColumnContent?: React.ReactNode // Custom content for right column\n belowDescriptionContent?: React.ReactNode // Custom content below description\n moveDescriptionToRight?: boolean // Move description and belowDescriptionContent to right column\n keepBelowDescriptionLeft?: boolean // Keep belowDescriptionContent on left even when moveDescriptionToRight is true\n backgroundColor?: string // ODS background color (e.g., 'bg-ods-bg-card', 'bg-ods-system-greys-black')\n social?: {\n github?: string\n twitter?: string\n linkedin?: string\n reddit?: string\n youtube?: string\n instagram?: string\n facebook?: string\n discord?: string\n telegram?: string\n whatsapp?: string\n }\n}\n\ninterface FooterProps {\n config?: FooterConfig\n renderLink?: (link: FooterLink) => React.ReactNode\n}\n\nfunction NavLinkSkeleton() {\n return <Skeleton className=\"h-5 md:h-6 w-20 md:w-24\" />\n}\n\n/**\n * Platform-Aware Footer Component\n * Accepts configuration from app-config.ts\n */\nexport function Footer({ config, renderLink }: FooterProps) {\n // Config is required - no hardcoded fallbacks\n if (!config) {\n console.warn('Footer: No config provided')\n return null\n }\n \n return <UniversalFooter config={config} renderLink={renderLink} />\n}\n\n/**\n * Universal Footer Component\n * Renders footer based on provided config\n */\nfunction UniversalFooter({ config, renderLink }: { config: FooterConfig; renderLink?: (link: FooterLink) => React.ReactNode }) {\n const defaultRenderLink = (link: FooterLink) => (\n <a href={link.href} className=\"font-body font-medium text-md md:text-md leading-[1.33] text-ods-text-primary hover:text-ods-accent-primary transition-colors\">\n {link.label}\n </a>\n )\n \n const linkRenderer = renderLink || defaultRenderLink\n \n return (\n <footer className={`w-full flex flex-col justify-center items-center ${config.backgroundColor || 'bg-ods-bg-card'} px-6 py-10 relative gap-6 md:gap-6 min-h-[auto] md:min-h-[248px] z-50 border-t border-ods-border`}>\n <div className=\"w-full grid grid-cols-2 lg:grid-cols-4 gap-6 md:gap-8 items-start\">\n \n {/* Column 1: Logo and optionally description */}\n <div className=\"flex flex-col gap-4 md:gap-6 items-start text-left col-span-2 md:col-span-1 lg:col-span-1\">\n {/* Logo and name */}\n <div className=\"flex items-center gap-2\">\n {config.logo && (\n <Suspense fallback={<Skeleton className=\"w-8 h-8\" />}>\n {config.logo as any}\n </Suspense>\n )}\n {config.nameElement || <span className=\"font-heading text-heading-5 font-bold text-ods-text-primary whitespace-nowrap\">{config.name}</span>}\n </div>\n \n {/* Only show description here if NOT moving to right */}\n {!config.moveDescriptionToRight && (\n <>\n <p className=\"font-body font-medium text-sm md:text-sm leading-[1.43] text-ods-text-primary\">\n {config.description}\n </p>\n \n {/* Custom content below description */}\n {config.belowDescriptionContent && (\n <Suspense fallback={<Skeleton className=\"h-8 w-full\" />}>\n {config.belowDescriptionContent as any}\n </Suspense>\n )}\n \n {/* Conditional social row - show by default unless hideSocialRow is true */}\n {!config.hideSocialRow && (\n <SocialIconRow\n className=\"pt-2\"\n links={config.social ? Object.entries(config.social)\n .filter(([_, href]) => href)\n .map(([platform, href]) => ({ platform, href: href as string }))\n : undefined\n }\n />\n )}\n </>\n )}\n \n {/* Show belowDescriptionContent on left even when description is moved to right */}\n {config.moveDescriptionToRight && config.keepBelowDescriptionLeft && config.belowDescriptionContent && (\n <Suspense fallback={<Skeleton className=\"h-8 w-full\" />}>\n {config.belowDescriptionContent as any}\n </Suspense>\n )}\n </div>\n \n {/* Dynamic sections - 1 column each on all screens */}\n {config.sections.map((section, index) => (\n <div key={index} className=\"flex flex-col gap-3 items-start text-left col-span-1\">\n <h3 className=\"text-h5 tracking-[-0.02em] text-ods-text-muted\">\n {section.title}\n </h3>\n <div className=\"flex flex-col gap-3\">\n {section.links.map((link, linkIndex) => (\n <Suspense key={linkIndex} fallback={<NavLinkSkeleton />}>\n {linkRenderer(link) as any}\n </Suspense>\n ))}\n </div>\n </div>\n ))}\n \n {/* Custom component column - full width on mobile and medium, 1 column on large */}\n {config.customComponent && (\n <div className=\"flex flex-col col-span-2 md:col-span-1 lg:col-span-1 justify-center\">\n <Suspense fallback={<Skeleton className=\"h-32 w-full\" />}>\n {config.customComponent as any}\n </Suspense>\n </div>\n )}\n \n {/* Right column content - shows if rightColumnContent is provided OR if moving description to right */}\n {(config.rightColumnContent || config.moveDescriptionToRight) && (\n <div className=\"flex flex-col col-span-2 md:col-span-1 lg:col-span-1 justify-start gap-4 md:gap-6\">\n {/* Show description in right column if moveDescriptionToRight is true */}\n {config.moveDescriptionToRight && (\n <>\n <p className=\"font-body font-medium text-sm md:text-sm leading-[1.43] text-ods-text-primary\">\n {config.description}\n </p>\n \n {/* Custom content below description - only if NOT keeping it on left */}\n {config.belowDescriptionContent && !config.keepBelowDescriptionLeft && (\n <Suspense fallback={<Skeleton className=\"h-8 w-full\" />}>\n {config.belowDescriptionContent as any}\n </Suspense>\n )}\n </>\n )}\n \n {/* Regular right column content */}\n {config.rightColumnContent && (\n <Suspense fallback={<Skeleton className=\"h-32 w-full\" />}>\n {config.rightColumnContent as any}\n </Suspense>\n )}\n </div>\n )}\n </div>\n \n {/* Copyright */}\n <p className=\"font-body font-medium text-md md:text-md leading-[1.33] text-center w-full text-ods-text-muted pt-4 md:pt-0\">\n © {new Date().getFullYear()} {config.legalName}. All rights reserved.\n </p>\n </footer>\n )\n}\n\n","\"use client\"\n\nimport { Button } from './ui/button';\nimport { GitHubIcon, RedditIcon, XLogo, LinkedInIcon, LumaIcon, WhatsAppIcon, GlobeIcon, MessageCircleIcon, TelegramIcon, YouTubeIcon, InstagramIcon, FacebookIcon, SlackIcon, CopyIcon } from './icons';\n\n/** Exactly ONE of `href` (anchor, target _blank) or `onClick` (action\n * button — share popups via window.open inside the click gesture,\n * copy-to-clipboard) — the discriminated union makes a dead no-action\n * entry unrepresentable. */\ntype SocialLink = {\n platform: string;\n label?: string;\n} & (\n | { href: string; onClick?: never }\n | { onClick: () => void; href?: never }\n);\n\ninterface SocialIconRowProps {\n className?: string;\n links?: SocialLink[];\n variant?: \"accent\" | \"outline\" | \"transparent\" | \"destructive\" | null | undefined;\n /** Quiet metadata row for page-level identity/share slots: 32px ghost\n * icon buttons (size=\"icon-sm\", 16px glyphs), gap-2, w-fit container,\n * variant defaulting to \"transparent\" (an explicit `variant` still wins).\n * Default false: 44/48px buttons stretching across the container —\n * the original card-width behavior (TMCG member cards, footers). */\n compact?: boolean;\n}\n\nconst defaultLinks: SocialLink[] = [\n { platform: 'github', href: 'https://github.com/flamingo-stack', label: 'GitHub' },\n { platform: 'linkedin', href: 'https://linkedin.com/company/flamingo.run', label: 'LinkedIn' },\n { platform: 'facebook', href: 'https://www.facebook.com/flamingoai.msp', label: 'Facebook' }\n];\n\nfunction renderSocialIcon(platform: string) {\n const normalizedPlatform = platform.toLowerCase().trim();\n\n switch (normalizedPlatform) {\n case 'github':\n return <GitHubIcon className=\"w-5 h-5\" />;\n case 'twitter':\n case 'x':\n return <XLogo className=\"w-5 h-5\" />;\n case 'reddit':\n return <RedditIcon className=\"w-5 h-5\" variant=\"white\" />;\n case 'linkedin':\n return <LinkedInIcon className=\"w-5 h-5\" />;\n case 'luma':\n return <LumaIcon className=\"w-5 h-5\" />;\n case 'whatsapp':\n return <WhatsAppIcon className=\"w-5 h-5\" />;\n case 'website':\n case 'web':\n case 'url':\n return <GlobeIcon className=\"w-5 h-5\" />;\n case 'slack':\n return <SlackIcon className=\"w-5 h-5\" injectedColor=\"white\" />;\n case 'discord':\n return <MessageCircleIcon className=\"w-5 h-5\" />;\n case 'telegram':\n return <TelegramIcon className=\"w-5 h-5\" />;\n case 'youtube':\n case 'yt':\n return <YouTubeIcon className=\"w-5 h-5\" />;\n case 'instagram':\n case 'ig':\n return <InstagramIcon className=\"w-5 h-5\" />;\n case 'facebook':\n case 'fb':\n return <FacebookIcon className=\"w-5 h-5\" />;\n case 'copy':\n // CopyIcon's default fill is grey and would mismatch its row-mates —\n // force the themed foreground via the ODS token (white on the dark\n // theme, tracking the theme unlike the literal the reddit/slack cases\n // still carry).\n return <CopyIcon className=\"w-5 h-5\" color=\"var(--ods-text-primary)\" />;\n default:\n return <GlobeIcon className=\"w-5 h-5\" />;\n }\n}\n\nexport function SocialIconRow({ className = '', links = defaultLinks, variant, compact = false }: SocialIconRowProps) {\n // ── Compact design rationale ──────────────────────────────────────────\n // Page-level identity/share rows read as METADATA, not CTAs. The major\n // design systems converge on one recipe for this slot: a ~32px ghost icon\n // button with a ~16px glyph, tight 8px gap, transparent at rest, subtle\n // background state-layer on hover (Carbon \"ghost\" sm, Primer \"invisible\"\n // medium, shadcn ghost+icon-sm — all 32px; Material 3 \"standard\" icon\n // button = state-layer hover). Author headers on content platforms\n // (Medium / dev.to / Substack) use the same quiet treatment. Hence\n // compact: size=\"icon-sm\" + variant defaulting to \"transparent\" — the\n // hover affordance comes from the bg state layer (hover:bg-ods-bg-hover\n // inside the variant) because the brand icons carry fixed fills, not\n // currentColor. An explicit `variant` prop still wins (e.g. outline\n // chips). Non-compact keeps the legacy outline default + full-width\n // stretch untouched.\n const resolvedVariant = variant !== undefined ? variant : (compact ? 'transparent' : 'outline');\n return (\n <div className={`flex flex-row ${compact ? 'gap-2 w-fit' : 'gap-3 w-full'} ${className}`}>\n {links.map((link, index) => {\n const ariaLabel = link.label || link.platform;\n return link.onClick ? (\n <Button\n key={index}\n type=\"button\"\n variant={resolvedVariant}\n size={compact ? 'icon-sm' : 'icon'}\n className={compact ? undefined : 'flex-1'}\n aria-label={ariaLabel}\n onClick={link.onClick}\n >\n {renderSocialIcon(link.platform)}\n </Button>\n ) : (\n // Props-based linking — Button renders the anchor itself\n // (openInNewTab carries target=\"_blank\" + rel=\"noopener noreferrer\");\n // no asChild/<a> nesting.\n <Button\n key={index}\n variant={resolvedVariant}\n size={compact ? 'icon-sm' : 'icon'}\n className={compact ? undefined : 'flex-1'}\n href={link.href}\n openInNewTab\n aria-label={ariaLabel}\n >\n {renderSocialIcon(link.platform)}\n </Button>\n );\n })}\n </div>\n );\n}","\"use client\"\n\nimport { useSearchParams, useRouter } from \"../embed-shims/next-navigation\"\nimport { useTransition } from \"react\"\n\n/**\n * Unified AND Filter Logic\n * \n * This module provides common filtering logic that uses AND operations\n * instead of OR operations for all filter combinations.\n */\n\nexport interface FilterState {\n search?: string\n categories?: string[]\n subcategories?: string[]\n tags?: string[]\n filters?: string[]\n pricing?: string\n page?: number\n}\n\nexport interface FilterConfig {\n basePath: string\n supportedFilters: {\n categories?: boolean\n subcategories?: boolean\n tags?: boolean\n filters?: boolean\n pricing?: boolean\n search?: boolean\n }\n}\n\n/**\n * Hook for managing AND-based filter logic\n */\nexport function useUnifiedFiltering(config: FilterConfig) {\n const router = useRouter()\n const searchParams = useSearchParams()\n const [isPending, startTransition] = useTransition()\n\n // Parse current filter state from URL\n const getCurrentFilterState = (): FilterState => {\n const search = searchParams?.get('search') || undefined\n const categories = searchParams?.get('category')?.split(',').filter(Boolean) || []\n const subcategories = searchParams?.get('subcategory')?.split(',').filter(Boolean) || []\n const tags = searchParams?.get('tags')?.split(',').filter(Boolean) || []\n const filters = searchParams?.getAll('filter') || []\n const pricing = searchParams?.get('pricing') || undefined\n const page = parseInt(searchParams?.get('page') || \"1\")\n\n return {\n search,\n categories,\n subcategories,\n tags,\n filters,\n pricing,\n page\n }\n }\n\n // Update filters with AND logic\n const updateFilters = (newFilters: Partial<FilterState>, preserveScroll = false) => {\n const currentState = getCurrentFilterState()\n const updatedState = { ...currentState, ...newFilters }\n \n // Always reset to page 1 when filters change (unless explicitly preserving page)\n if (!newFilters.hasOwnProperty('page')) {\n updatedState.page = 1\n }\n\n const params = new URLSearchParams()\n\n // Add search parameter\n if (updatedState.search && config.supportedFilters.search) {\n params.set('search', updatedState.search)\n }\n\n // Add category parameters (AND logic - all must match)\n if (updatedState.categories && updatedState.categories.length > 0 && config.supportedFilters.categories) {\n params.set('category', updatedState.categories.join(','))\n }\n\n // Add subcategory parameters (AND logic - all must match)\n if (updatedState.subcategories && updatedState.subcategories.length > 0 && config.supportedFilters.subcategories) {\n params.set('subcategory', updatedState.subcategories.join(','))\n }\n\n // Add tag parameters (AND logic - all must match). Unified `?tags=` param\n // (matches blog/case-study/media/publication).\n if (updatedState.tags && updatedState.tags.length > 0 && config.supportedFilters.tags) {\n params.set('tags', updatedState.tags.join(','))\n }\n\n // Add filter parameters (AND logic - all must match)\n if (updatedState.filters && updatedState.filters.length > 0 && config.supportedFilters.filters) {\n updatedState.filters.forEach(filter => params.append('filter', filter))\n }\n\n // Add pricing parameter\n if (updatedState.pricing && config.supportedFilters.pricing) {\n params.set('pricing', updatedState.pricing)\n }\n\n // Add page parameter (only if not page 1)\n if (updatedState.page && updatedState.page > 1) {\n params.set('page', updatedState.page.toString())\n }\n\n const currentScrollY = preserveScroll ? window.scrollY : 0\n const newUrl = `${config.basePath}?${params.toString()}`\n\n startTransition(() => {\n router.push(newUrl, { scroll: false })\n \n if (preserveScroll) {\n setTimeout(() => {\n window.scrollTo({\n top: currentScrollY,\n behavior: 'smooth'\n })\n }, 100)\n }\n })\n }\n\n // Add a filter (AND logic)\n const addFilter = (\n type: keyof FilterState,\n value: string,\n preserveScroll = false\n ) => {\n const currentState = getCurrentFilterState()\n const updates: Partial<FilterState> = {}\n\n switch (type) {\n case 'categories':\n updates.categories = [...(currentState.categories || []), value]\n break\n case 'subcategories':\n updates.subcategories = [...(currentState.subcategories || []), value]\n break\n case 'tags':\n updates.tags = [...(currentState.tags || []), value]\n break\n case 'filters':\n updates.filters = [...(currentState.filters || []), value]\n break\n case 'pricing':\n updates.pricing = value\n break\n case 'search':\n updates.search = value\n break\n }\n\n updateFilters(updates, preserveScroll)\n }\n\n // Remove a filter (AND logic)\n const removeFilter = (\n type: keyof FilterState,\n value?: string,\n preserveScroll = true\n ) => {\n const currentState = getCurrentFilterState()\n const updates: Partial<FilterState> = {}\n\n switch (type) {\n case 'categories':\n updates.categories = value \n ? (currentState.categories || []).filter(c => c !== value)\n : []\n break\n case 'subcategories':\n updates.subcategories = value \n ? (currentState.subcategories || []).filter(s => s !== value)\n : []\n break\n case 'tags':\n updates.tags = value \n ? (currentState.tags || []).filter(t => t !== value)\n : []\n break\n case 'filters':\n updates.filters = value \n ? (currentState.filters || []).filter(f => f !== value)\n : []\n break\n case 'pricing':\n updates.pricing = undefined\n break\n case 'search':\n updates.search = undefined\n break\n }\n\n updateFilters(updates, preserveScroll)\n }\n\n // Toggle a filter (AND logic)\n const toggleFilter = (\n type: keyof FilterState,\n value: string,\n preserveScroll = false\n ) => {\n const currentState = getCurrentFilterState()\n let currentValues: string[] = []\n\n switch (type) {\n case 'categories':\n currentValues = currentState.categories || []\n break\n case 'subcategories':\n currentValues = currentState.subcategories || []\n break\n case 'tags':\n currentValues = currentState.tags || []\n break\n case 'filters':\n currentValues = currentState.filters || []\n break\n case 'pricing':\n // Pricing is single-select, so toggle on/off\n if (currentState.pricing === value) {\n removeFilter('pricing', value, preserveScroll)\n } else {\n addFilter('pricing', value, preserveScroll)\n }\n return\n case 'search':\n // Search is single-value, so just update\n updateFilters({ search: value }, preserveScroll)\n return\n }\n\n if (currentValues.includes(value)) {\n removeFilter(type, value, preserveScroll)\n } else {\n addFilter(type, value, preserveScroll)\n }\n }\n\n // Clear all filters\n const clearAllFilters = () => {\n startTransition(() => {\n router.push(config.basePath, { scroll: false })\n })\n }\n\n // Get filter chips for display\n const getFilterChips = (categories?: any[], tags?: any[]) => {\n const currentState = getCurrentFilterState()\n const chips: Array<{\n id: string\n label: string\n type: 'category' | 'subcategory' | 'tag' | 'filter' | 'pricing'\n }> = []\n\n // Category chips\n if (currentState.categories && categories) {\n currentState.categories.forEach(categorySlug => {\n const category = categories.find((c: any) => c.slug === categorySlug)\n if (category) {\n chips.push({\n id: `category-${categorySlug}`,\n label: category.name,\n type: 'category'\n })\n }\n })\n }\n\n // Subcategory chips\n if (currentState.subcategories && categories) {\n currentState.subcategories.forEach(subcategorySlug => {\n // Find subcategory across all categories\n for (const category of categories) {\n if (category.subcategories) {\n const subcategory = category.subcategories.find((s: any) => s.slug === subcategorySlug)\n if (subcategory) {\n chips.push({\n id: `subcategory-${subcategorySlug}`,\n label: subcategory.name,\n type: 'subcategory'\n })\n break\n }\n }\n }\n })\n }\n\n // Tag chips\n if (currentState.tags && tags) {\n currentState.tags.forEach(tagSlug => {\n const tag = tags.find((t: any) => t.slug === tagSlug)\n if (tag) {\n chips.push({\n id: `tag-${tagSlug}`,\n label: tag.name,\n type: 'tag'\n })\n }\n })\n }\n\n // Filter chips\n if (currentState.filters) {\n currentState.filters.forEach(filterKey => {\n chips.push({\n id: `filter-${filterKey}`,\n label: getFilterLabel(filterKey),\n type: 'filter'\n })\n })\n }\n\n // Pricing chip\n if (currentState.pricing) {\n chips.push({\n id: `pricing-${currentState.pricing}`,\n label: getPricingLabel(currentState.pricing),\n type: 'pricing'\n })\n }\n\n return chips\n }\n\n // Handle filter chip removal\n const handleFilterChipRemove = (chipId: string) => {\n const [type, ...idParts] = chipId.split('-')\n const id = idParts.join('-')\n\n switch (type) {\n case 'category':\n removeFilter('categories', id)\n break\n case 'subcategory':\n removeFilter('subcategories', id)\n break\n case 'tag':\n removeFilter('tags', id)\n break\n case 'filter':\n removeFilter('filters', id)\n break\n case 'pricing':\n removeFilter('pricing', id)\n break\n }\n }\n\n return {\n getCurrentFilterState,\n updateFilters,\n addFilter,\n removeFilter,\n toggleFilter,\n clearAllFilters,\n getFilterChips,\n handleFilterChipRemove,\n isPending\n }\n}\n\nimport { formatClassification, formatPricingModel } from '../utils/format-text-stub'\n\n// Helper functions for filter labels\nfunction getFilterLabel(filterKey: string): string {\n const customLabels: Record<string, string> = {\n 'recommended': 'Recommended',\n 'featured': 'Featured'\n }\n return customLabels[filterKey] || formatClassification(filterKey)\n}\n\nfunction getPricingLabel(pricingKey: string): string {\n return formatPricingModel(pricingKey)\n}\n\n/**\n * Vendor-specific filter configuration\n */\nexport const vendorFilterConfig: FilterConfig = {\n basePath: '/vendors',\n supportedFilters: {\n categories: true,\n subcategories: true,\n tags: false,\n filters: true,\n pricing: true,\n search: true\n }\n}\n\n/**\n * Blog-specific filter configuration\n */\nexport const blogFilterConfig: FilterConfig = {\n basePath: '/blog',\n supportedFilters: {\n categories: true,\n subcategories: false,\n tags: true,\n filters: false,\n pricing: false,\n search: true\n }\n} ","// Stub for format-text utilities\nexport function formatText(text: string): string {\n return text;\n}\n\nexport function truncateText(text: string, maxLength: number): string {\n if (text.length <= maxLength) return text;\n return text.slice(0, maxLength) + '...';\n}\n\nexport function formatClassification(classification: string): string {\n return classification.replace(/_/g, ' ').replace(/\\b\\w/g, l => l.toUpperCase());\n}\n\nexport function formatPricingModel(pricing: string): string {\n return pricing.replace(/_/g, ' ').replace(/\\b\\w/g, l => l.toUpperCase());\n}","\"use client\"\n\n// Components exports\nexport * from './chat'\nexport * from './docs'\nexport * from './embeds'\nexport * from './features'\nexport * from './icons'\nexport * from './navigation'\nexport * from './platform'\nexport * from './ui'\n\n// Individual component exports\nexport * from './announcement-bar'\nexport * from './hover-card'\nexport * from './categories-cart'\nexport * from './category-card'\nexport * from './comment-card'\nexport * from './content-loading-container'\nexport * from './dynamic-skeleton'\nexport * from './empty-state'\nexport * from './faq-accordion'\n// FaqSection sub-folder. Also exposed via the \"./components/faq\" subpath export\n// in package.json — that subpath is ALSO `\"use client\"` (tsup banner), so it\n// avoids dragging the rest of this root barrel but is NOT server-safe.\n//\n// Server Components that need the pure-fn JSON-LD builder MUST import from the\n// dedicated server-safe subpath \"./components/faq/json-ld\" (built without the\n// client banner under the server/universal block of tsup.config.ts).\nexport * from './faq'\nexport * from './related-content'\nexport * from './filter-chip'\nexport * from './footer'\nexport * from './unified-filter-logic'\nexport * from './unified-pagination'\nexport * from './footer-waitlist-button'\nexport * from './hero-image-uploader'\nexport * from './icons-block'\nexport * from './image-cropper'\nexport * from './media-carousel'\nexport * from './metric-value'\nexport * from './msp-display'\nexport * from './open-source-features'\nexport * from './pagination'\nexport * from './persistent-filter-controls'\nexport * from './persistent-pagination'\nexport * from './pricing-display'\nexport * from './results-count'\nexport * from './selection-source-badge'\nexport * from './social-icon-row'\nexport * from './user-display'\nexport * from './vendor-display-button'\nexport * from './vendor-icon'\nexport * from './vendor-page-skeleton'\nexport * from './vendor-tag'\nexport * from './why-it-matters'\nexport * from './yes-no-display'\n// Removed duplicate PageContainer export - already exported from './ui/page-container'\nexport * from './made-with-love'\n\n// Loading components\nexport * from './loading'\n\n// Auth-related exports\nexport * from './auth-stub'\n\n// Date/Time components\nexport * from './date-time-picker'\n\n// Chat components\nexport * from './chat'\n\n// Onboarding components\nexport * from './shared/onboarding'\n\n// Doc-search bar — unified RAG-search dropdown used by the data-room\n// sidebar AND the onboarding-guide catalog. Pure presentation; hosts\n// own the `useDocSearch` hook and pass results in as props.\nexport * from './shared/doc-search'\n\n// Product Release components\nexport * from './shared/product-release'\n\n// Dev-center shared components (Roadmap / Delivery / DevSectionView chrome)\nexport * from './shared/dev-section'\nexport * from './shared/roadmap'\nexport * from './shared/delivery'\n\n// Legal-document shared component (privacy policy, terms of service)\nexport * from './shared/legal-document'\n\n// Detail Page Skeleton\nexport { DetailPageSkeleton, type DetailPageSkeletonProps } from './shared/detail-page-skeleton'\n\n// Author byline card (end-of-article author description) — embeddable via\n// embed-shims + optional-runtime avatar proxying; hosts pass their own\n// fallbackBio copy (the hub uses defaultAuthorFallbackBio()).\nexport { ArticleAuthorByline, type ArticleAuthorBylineProps } from './shared/article-author-byline'\n\n// Read-only media gallery strip (horizontal scroll; images → lightbox, clips →\n// inline Video). Single source of truth for the detail-page media gallery —\n// used by product-release + What I Shipped detail pages.\nexport { MediaGalleryStrip, type MediaGalleryStripItem, type MediaGalleryStripProps } from './shared/media-gallery-strip'\n\n// Author detail-page body (identity + socials + bio + expertise, rail as\n// children) — the one implementation behind /authors/[slug] and embedded\n// author pages.\nexport { AuthorDetailView, type AuthorDetailViewProps } from './authors/author-detail-view'\n\n// Priority UI components that exist in main components directory\n// Note: These are re-exported from ./ui already, no need to duplicate\n","'use client';\n\nimport { useRouter } from '../embed-shims/next-navigation';\nimport { useChatRuntime } from '../contexts/chat-runtime-context';\nimport { executeNavigationImperative } from './chat/utils/execute-navigation';\nimport { useCallback } from 'react';\nimport { OpenFrameLogo } from './icons';\nimport { Button } from './ui/button';\n\nexport interface FooterWaitlistButtonProps {\n className?: string;\n}\n\n/**\n * Small wrapper around JoinWaitlistButton for use inside the footer.\n *\n * Routes through the host's unified-navigation hook\n * (`runtime.navigation.navigate`) when a `ChatRuntimeContext` is\n * mounted — that's the same path EVERY other in-app navigation\n * surface uses (source chips, inline cards, search-autocomplete,\n * action cards). One rule, one decision tree across the whole app.\n * The hub's `HubRuntimeProvider` wires `navigate` to its `useUnifiedNav`\n * helper, so this button picks up cross-platform new-tab decisions,\n * same-URL re-scroll handling, embed-mode short-circuiting, and any\n * future host-side nav rules for free.\n *\n * Falls back to the embed-shim's `router.push` when no runtime is\n * mounted (third-party embedders who haven't set up\n * `ChatRuntimeContext` — the lib stays usable without forcing them\n * to wire the full chat-runtime).\n *\n * Target URL: `/waitlist#top`. `#top` is the canonical \"scroll to\n * page top\" anchor — the destination page has an explicit\n * `<div id=\"top\">` at the top of `<main>` so native browser anchor\n * scroll works in every browser regardless of the HTML5 magic-anchor\n * behavior.\n */\nexport function FooterWaitlistButton({ className }: FooterWaitlistButtonProps) {\n const router = useRouter();\n const runtime = useChatRuntime();\n\n const handleClick = useCallback(() => {\n // The unified nav primitive: host `navigation.navigate` if wired, else the\n // embed-shim router; new-tab/embed decision handled internally.\n executeNavigationImperative({ runtime, href: '/waitlist#top', fallbackNavigate: router.push });\n }, [router, runtime]);\n\n return (\n <Button \n onClick={handleClick} \n className={className}\n leftIcon={<OpenFrameLogo />}\n >\n Join Waitlist\n </Button>\n );\n} ","\"use client\";\n\nimport { useRef, useState } from 'react';\nimport { Loader2, Image as ImageIcon, Upload, X } from 'lucide-react';\nimport { Button } from \"./ui/button\";\nimport { useToast } from \"../hooks/use-toast\";\n\ninterface HeroImageUploaderProps {\n /** Current image URL if one already exists */\n imageUrl?: string;\n /** Callback fired with new image URL (or undefined if removed) */\n onChange: (url: string | undefined) => void;\n /** Upload endpoint (required) */\n uploadEndpoint: string;\n /** Height of drop-zone. Number treated as pixels, string passed directly (e.g. '100%') */\n height?: number | string;\n /** Image object-fit, defaults to cover */\n objectFit?: 'cover' | 'contain' | 'fill' | 'none' | 'scale-down';\n /** Show a replace/upload button overlay in addition to remove (default true for parity with blog editor) */\n showReplaceButton?: boolean;\n /** If true, skip the actual upload and just return a base64 data URL preview. Useful for unauthenticated flows – the caller can upload later. */\n deferUpload?: boolean;\n /** Optional custom upload handler for authenticated uploads. If provided, this will be used instead of the default fetch */\n onUpload?: (file: File) => Promise<string>;\n /** Optional custom delete handler for authenticated deletion. If provided, this will be used instead of just clearing the image */\n onDelete?: () => Promise<void>;\n}\n\n/**\n * Reusable dashed hero-style image uploader identical to Blog Editor's hero picker.\n * Handles client-side validation (JPEG/PNG/WebP/GIF up to 5 MB), upload, preview & removal.\n */\nexport function HeroImageUploader({ imageUrl, onChange, uploadEndpoint, height = 300, objectFit = 'cover', showReplaceButton = true, deferUpload = false, onUpload, onDelete }: HeroImageUploaderProps) {\n const inputRef = useRef<HTMLInputElement>(null);\n const { toast } = useToast();\n const [uploading, setUploading] = useState(false);\n\n const ALLOWED_TYPES = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp', 'image/gif'];\n const MAX_SIZE = 5 * 1024 * 1024; // 5MB\n\n const openDialog = () => inputRef.current?.click();\n\n async function handleFile(file?: File) {\n if (!file) return;\n if (!ALLOWED_TYPES.includes(file.type)) {\n toast({ title: 'Invalid file', description: 'Upload JPEG, PNG, WebP, or GIF', variant: 'destructive' });\n return;\n }\n if (file.size > MAX_SIZE) {\n toast({ title: 'File too large', description: 'Max 5MB', variant: 'destructive' });\n return;\n }\n\n if (deferUpload) {\n // Immediately convert to data URL for preview and postpone real upload\n try {\n setUploading(true);\n const reader = new FileReader();\n reader.onload = () => {\n const dataUrl = reader.result as string;\n onChange(dataUrl); // Return data URL so parent can preview & store locally\n setUploading(false);\n };\n reader.onerror = () => {\n toast({ title: 'File error', description: 'Failed to read image file', variant: 'destructive' });\n setUploading(false);\n };\n reader.readAsDataURL(file);\n } catch (err: any) {\n toast({ title: 'File error', description: err.message || 'Failed to process image', variant: 'destructive' });\n setUploading(false);\n } finally {\n if (inputRef.current) inputRef.current.value = '';\n }\n return;\n }\n\n // Upload flow - use custom handler if provided, otherwise use default fetch\n setUploading(true);\n try {\n let uploadedUrl: string;\n \n if (onUpload) {\n // Use custom upload handler (e.g., for authenticated uploads)\n uploadedUrl = await onUpload(file);\n } else {\n // Default upload flow\n const fd = new FormData();\n fd.append('file', file);\n const res = await fetch(uploadEndpoint, { method: 'POST', body: fd });\n if (!res.ok) throw new Error('Upload failed');\n const json = await res.json();\n uploadedUrl = (json.data && json.data.url) || json.url || json.file_url;\n if (!uploadedUrl) throw new Error('Invalid upload response');\n }\n \n onChange(uploadedUrl);\n } catch (err: any) {\n toast({ title: 'Upload error', description: err.message || 'Failed to upload', variant: 'destructive' });\n } finally {\n setUploading(false);\n if (inputRef.current) inputRef.current.value = '';\n }\n }\n\n const handleSelect = (e: React.ChangeEvent<HTMLInputElement>) => {\n handleFile(e.target.files?.[0]);\n };\n\n const handleRemove = async () => {\n if (onDelete) {\n try {\n await onDelete();\n } catch (error) {\n // onDelete handler should handle its own error reporting\n return;\n }\n }\n onChange(undefined);\n };\n\n const heightStyle = typeof height === 'number' ? `${height}px` : height;\n\n return (\n <div className=\"w-full h-full max-h-full space-y-2 min-h-[300px]\">\n {imageUrl ? (\n <div className=\"relative group w-full aspect-square md:aspect-auto h-auto md:h-full flex items-center justify-center overflow-hidden\" style={{ height: heightStyle }}>\n <img src={imageUrl} className={`absolute inset-0 w-full h-full object-${objectFit}`} alt=\"Cover\" />\n <div className=\"absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 flex items-center justify-center gap-4 transition-opacity rounded-lg\">\n {showReplaceButton && (\n <Button\n variant=\"outline\"\n size=\"icon\"\n onClick={openDialog}\n className=\"bg-white text-black hover:bg-gray-100 rounded-full w-12 h-12\"\n >\n <Upload className=\"h-5 w-5\" />\n </Button>\n )}\n <Button\n variant=\"outline\"\n size=\"icon\"\n onClick={() => handleRemove()}\n className=\"bg-white text-black hover:bg-gray-100 rounded-full w-12 h-12\"\n >\n <X className=\"h-5 w-5\" />\n </Button>\n </div>\n </div>\n ) : (\n <div\n className={`w-full h-full border-2 border-dashed ${uploading ? 'border-ods-accent' : 'border-ods-border hover:border-ods-accent'} rounded-lg flex flex-col items-center justify-center cursor-pointer bg-ods-bg`}\n style={{ height: heightStyle }}\n onClick={openDialog}\n >\n {uploading ? (\n <Loader2 className=\"h-8 w-8 animate-spin text-ods-accent\" />\n ) : (\n <>\n <ImageIcon className=\"h-12 w-12 text-ods-text-secondary\" />\n <span className=\"text-ods-text-primary font-['DM_Sans'] text-[16px] font-medium mt-2\">Upload cover image</span>\n <span className=\"text-ods-text-secondary font-['DM_Sans'] text-[14px] mt-1\">Click to upload or drag and drop</span>\n <span className=\"text-ods-text-secondary font-['DM_Sans'] text-[12px]\">PNG, JPEG, WebP, GIF up to 5MB</span>\n </>\n )}\n </div>\n )}\n\n {/* hidden input */}\n <input ref={inputRef} type=\"file\" accept=\"image/*\" onChange={handleSelect} className=\"hidden\" />\n </div>\n );\n} ","\"use client\";\n\nimport React, { useState, useEffect, useRef } from 'react';\nimport {\n VendorDirectoryIcon,\n OpenSourceIcon,\n CommunityHubIcon,\n VendorsIcon,\n CommunityIcon,\n CompareIcon,\n} from './icons-stub';\nimport { Sun, Moon, CheckCircle, Github, PlusCircle } from 'lucide-react';\n\n// Map lucide icons\nconst SunIcon = Sun;\nconst MoonIcon = Moon;\nconst CheckCircleIcon = CheckCircle;\nconst GitHubIcon = Github;\nconst PlusCircleIcon = PlusCircle;\nconst OpenmspLogo = () => <div>Logo</div>;\n\ninterface IconsBlockProps {\n /**\n * When true, always render the loading placeholder bar regardless of whether the grid is ready.\n * Default behaviour renders placeholder only until the grid is generated.\n */\n loading?: boolean;\n}\n\n// Available icons array - moved outside component to prevent recreating on each render\nconst availableIcons = [\n VendorDirectoryIcon,\n OpenSourceIcon,\n CommunityHubIcon,\n VendorsIcon,\n CommunityIcon,\n CompareIcon,\n SunIcon,\n MoonIcon,\n CheckCircleIcon,\n GitHubIcon,\n PlusCircleIcon,\n OpenmspLogo\n];\n\nexport function ResponsiveIconsBlock({ loading = false }: IconsBlockProps) {\n const [columns, setColumns] = useState(24);\n const [iconGrid, setIconGrid] = useState<Array<Array<React.ComponentType<{ width?: number; height?: number; className?: string }>>>>([]);\n const [iconsLoaded, setIconsLoaded] = useState(false);\n const randomSeedRef = useRef<number>(0);\n\n useEffect(() => {\n function calculateColumns() {\n const cols = Math.ceil(window.innerWidth / 56) + 4;\n setColumns(cols);\n }\n\n calculateColumns();\n window.addEventListener('resize', calculateColumns);\n \n setIconsLoaded(true);\n \n return () => window.removeEventListener('resize', calculateColumns);\n }, []);\n\n // Generate icon grid only on client side\n useEffect(() => {\n if (randomSeedRef.current === 0) {\n randomSeedRef.current = Date.now();\n }\n\n const grid: any[][] = [];\n const totalCells = columns * 2;\n\n for (let idx = 0; idx < totalCells; idx++) {\n const col = idx % columns;\n const row = Math.floor(idx / columns);\n\n // Initialize row if needed\n if (!grid[row]) grid[row] = [];\n\n // Get adjacent icons to avoid\n const adjacentIcons = new Set();\n\n // Check left neighbor (same row, previous column)\n if (col > 0 && grid[row][col - 1]) {\n adjacentIcons.add(grid[row][col - 1]);\n }\n\n // Check top neighbor (previous row, same column)\n if (row > 0 && grid[row - 1] && grid[row - 1][col]) {\n adjacentIcons.add(grid[row - 1][col]);\n }\n\n // Filter available icons to exclude adjacent ones\n const availableOptions = availableIcons.filter(icon => !adjacentIcons.has(icon));\n\n // Generate random selection from available options using the stable seed\n const seed = randomSeedRef.current + idx;\n const pseudoRandom = (seed * 9301 + 49297) % 233280;\n const normalized = pseudoRandom / 233280;\n const iconIndex = Math.floor(normalized * availableOptions.length);\n\n // Assign selected icon to grid\n grid[row][col] = availableOptions[iconIndex] || availableIcons[0]; // Fallback to first icon\n }\n\n setIconGrid(grid);\n }, [columns]);\n\n const displayColumns = columns;\n\n // Get icon for specific position from pre-generated grid\n const getIconForIndex = (index: number) => {\n const col = index % displayColumns;\n const row = Math.floor(index / displayColumns);\n return iconGrid[row]?.[col] || availableIcons[0];\n };\n\n // When explicit loading prop true OR grid not ready → show placeholder\n if (loading || iconGrid.length === 0) {\n return (\n <div\n className=\"w-full h-[80px] md:h-[112px] bg-[#1A1A1A] relative overflow-hidden\"\n role=\"presentation\"\n aria-hidden=\"true\"\n >\n {/* subtle pulse bar */}\n <div className=\"absolute inset-0 animate-pulse bg-[#2A2A2A]/60\" />\n </div>\n );\n }\n\n return (\n <div\n className=\"w-full h-[80px] md:h-[112px] overflow-hidden bg-[#1A1A1A] relative z-10\"\n style={{ margin: 0, padding: 0 }}\n role=\"presentation\"\n aria-hidden=\"true\"\n >\n <style>{`\n .icons-block svg,\n .icons-block svg * {\n filter: grayscale(100%) brightness(0) invert(1) brightness(0.4) !important;\n fill: currentColor !important;\n }\n `}</style>\n <div\n className=\"grid h-full icons-block w-full\"\n style={{\n gridTemplateColumns: `repeat(${displayColumns}, 56px)`,\n gridTemplateRows: 'repeat(2, 1fr)',\n }}\n >\n {Array.from({ length: displayColumns * 2 }).map((_, idx) => {\n const col = idx % displayColumns;\n const row = Math.floor(idx / displayColumns);\n const IconComponent = getIconForIndex(idx);\n\n return (\n <div\n key={idx}\n className=\"flex items-center justify-center w-full h-full\"\n style={{\n background: '#1A1A1A',\n borderRight: col !== displayColumns - 1 ? '0.5px solid rgba(66, 66, 66, 0.5)' : undefined,\n borderBottom: row === 0 ? '0.5px solid rgba(66, 66, 66, 0.5)' : undefined,\n margin: 0,\n padding: '8px',\n boxSizing: 'border-box',\n }}\n role=\"presentation\"\n >\n <IconComponent\n width={16}\n height={16}\n className=\"text-[#666666] md:w-5 md:h-5\"\n aria-hidden=\"true\"\n />\n </div>\n );\n })}\n </div>\n </div>\n );\n} ","// Stub implementations for missing icons\nimport { DollarSign, Code, Users, Building, GitCompare, MessageSquare } from \"lucide-react\";\n\nexport const OpenSourceIcon = Code;\nexport const CoinsIcon = DollarSign;\nexport const VendorDirectoryIcon = Building;\nexport const CommunityHubIcon = Users;\nexport const VendorsIcon = Building;\nexport const CommunityIcon = MessageSquare;\nexport const CompareIcon = GitCompare;\n\n// OpenmspLogo moved to real implementation (should be imported from main project)\n\n// OpenFrameLogo moved to real implementation in ./openframe-logo.tsx","/*\n ImageCropper.tsx\n ----------------\n Reusable React component for interactive image cropping with:\n • Drag / resize cropping via react-easy-crop\n • Custom aspect ratio or free-form\n • Optional circular crop overlay (avatar mode)\n • Automatic scaling so the exported image never exceeds maxSizePx\n • Returns PNG data URL + Blob on confirm\n • Responsive / accessible with shadcn/ui Button & Slider components\n\n Styling relies on Tailwind + shadcn design tokens.\n*/\n\n\"use client\"\n\nimport React, { useCallback, useEffect, useMemo, useRef, useState } from \"react\"\nimport Cropper from \"react-easy-crop\"\nimport { Button } from \"./ui/button\"\nimport { Slider } from \"./ui/slider\"\nimport { cn } from \"../utils/cn\"\nimport { Check, RotateCcw } from \"lucide-react\"\n\n/* ------------------------------------------------------------\n * Types\n * ----------------------------------------------------------*/\n\nexport interface ImageCropperResult {\n /** Cropped PNG data URL */\n dataUrl: string\n /** Corresponding PNG Blob */\n blob: Blob\n}\n\nexport interface ImageCropperProps {\n /** Source image (URL or data URI) */\n src: string\n /** Called when user confirms crop */\n onComplete(result: ImageCropperResult): void\n /** Called when user cancels crop */\n onCancel?(): void\n /** Aspect ratio (width / height). If omitted, free-form */\n aspectRatio?: number\n /** Enable circular crop overlay for avatars */\n circular?: boolean\n /** Maximum width/height for the exported PNG (defaults 512) */\n maxSizePx?: number\n /** Optional className for wrapper */\n className?: string\n}\n\n/* ------------------------------------------------------------\n * Helpers\n * ----------------------------------------------------------*/\n\nfunction degToRad(deg: number) {\n return (deg * Math.PI) / 180\n}\n\n/** Util to create an HTMLImageElement that resolves when loaded */\nfunction loadImage(src: string): Promise<HTMLImageElement> {\n return new Promise((resolve, reject) => {\n const img = new Image()\n img.crossOrigin = \"anonymous\" // prevent canvas tainting\n img.onload = () => resolve(img)\n img.onerror = () => reject(new Error(\"Failed to load image\"))\n img.src = src\n })\n}\n\n/* ------------------------------------------------------------\n * Component\n * ----------------------------------------------------------*/\n\nexport const ImageCropper: React.FC<ImageCropperProps> = ({\n src,\n onComplete,\n onCancel,\n aspectRatio,\n circular = false,\n maxSizePx = 512,\n className,\n}) => {\n const [crop, setCrop] = useState({ x: 0, y: 0 })\n const [zoom, setZoom] = useState(1)\n const [rotation, setRotation] = useState(0)\n const [croppedAreaPixels, setCroppedAreaPixels] = useState<\n | { x: number; y: number; width: number; height: number }\n | null\n >(null)\n\n /* ------------------ crop complete callback -----------------*/\n const onCropComplete = useCallback((_: any, area: any) => {\n setCroppedAreaPixels(area)\n }, [])\n\n /* ------------------ Build checkered background --------------*/\n const checkerBg =\n \"bg-[length:16px_16px] bg-[linear-gradient(45deg,transparent_25%,#2a2a2a_25%,#2a2a2a_75%,transparent_75%,transparent),linear-gradient(45deg,#2a2a2a_25%,transparent_25%,transparent_75%,#2a2a2a_75%,#2a2a2a)]\"\n\n /* ------------------ Export logic ---------------------------*/\n const exportCrop = useCallback(async () => {\n if (!croppedAreaPixels) return undefined\n\n const img = await loadImage(src)\n\n // Create canvas the size of crop\n const canvas = document.createElement(\"canvas\")\n\n // Scale crop to fit maxSizePx\n const scale = Math.min(1, maxSizePx / Math.max(croppedAreaPixels.width, croppedAreaPixels.height))\n const outputW = Math.round(croppedAreaPixels.width * scale)\n const outputH = Math.round(croppedAreaPixels.height * scale)\n\n canvas.width = outputW\n canvas.height = outputH\n const ctx = canvas.getContext(\"2d\")!\n\n // Draw cropped portion\n ctx.drawImage(\n img,\n croppedAreaPixels.x,\n croppedAreaPixels.y,\n croppedAreaPixels.width,\n croppedAreaPixels.height,\n 0,\n 0,\n outputW,\n outputH,\n )\n\n // If circular mode, clip to circle\n if (circular) {\n const temp = document.createElement(\"canvas\")\n temp.width = outputW\n temp.height = outputH\n const tctx = temp.getContext(\"2d\")!\n tctx.beginPath()\n tctx.arc(outputW / 2, outputH / 2, outputW / 2, 0, Math.PI * 2)\n tctx.closePath()\n tctx.clip()\n tctx.drawImage(canvas, 0, 0)\n canvas.width = outputW\n canvas.height = outputH\n ctx.clearRect(0, 0, outputW, outputH)\n ctx.drawImage(temp, 0, 0)\n }\n\n return new Promise<ImageCropperResult>((resolve) => {\n canvas.toBlob(\n (blob) => {\n if (!blob) throw new Error(\"Canvas export failed\")\n const reader = new FileReader()\n reader.onloadend = () => {\n resolve({ dataUrl: reader.result as string, blob })\n }\n reader.readAsDataURL(blob)\n },\n \"image/png\",\n )\n })\n }, [croppedAreaPixels, circular, maxSizePx, src]) as () => Promise<ImageCropperResult | undefined>\n\n /* ------------------ Keyboard accessibility -----------------*/\n const handleKey = (e: React.KeyboardEvent) => {\n // Enter to confirm, Esc to cancel\n if (e.key === \"Enter\") {\n e.preventDefault()\n void exportCrop().then((res) => {\n if (res) onComplete(res)\n })\n } else if (e.key === \"Escape\") {\n e.preventDefault()\n onCancel?.()\n }\n }\n\n /* ------------------ Render ---------------------------------*/\n return (\n <div\n className={cn(\n \"relative flex flex-col gap-4 w-full\",\n className,\n )}\n onKeyDown={handleKey}\n tabIndex={0}\n aria-label=\"Image cropper\"\n >\n {/* Cropper container */}\n <div\n className={cn(\n \"relative w-full aspect-square md:aspect-video rounded-md overflow-hidden\",\n checkerBg,\n )}\n >\n <Cropper\n image={src}\n crop={crop}\n zoom={zoom}\n rotation={rotation}\n aspect={aspectRatio}\n cropShape={circular ? \"round\" : \"rect\"}\n showGrid={false}\n onCropChange={setCrop}\n onZoomChange={setZoom}\n onRotationChange={setRotation}\n onCropComplete={onCropComplete}\n objectFit=\"contain\"\n />\n </div>\n\n {/* Controls */}\n <div className=\"flex flex-col gap-4\">\n {/* Zoom */}\n <div className=\"flex items-center gap-3\">\n <span className=\"min-w-[60px] text-sm\">Zoom</span>\n <Slider\n min={1}\n max={3}\n step={0.01}\n value={[zoom]}\n onValueChange={(v) => setZoom(v[0])}\n aria-label=\"Zoom\"\n className=\"flex-1\"\n />\n </div>\n {/* Rotate */}\n <div className=\"flex items-center gap-3\">\n <span className=\"min-w-[60px] text-sm\">Rotate</span>\n <Slider\n min={0}\n max={360}\n step={1}\n value={[rotation]}\n onValueChange={(v) => setRotation(v[0])}\n aria-label=\"Rotation\"\n className=\"flex-1\"\n />\n <Button\n variant=\"outline\"\n size=\"icon\"\n onClick={() => setRotation(0)}\n aria-label=\"Reset rotation\"\n >\n <RotateCcw className=\"h-4 w-4\" />\n </Button>\n </div>\n </div>\n\n {/* Action buttons */}\n <div className=\"flex justify-end gap-2 mt-2\">\n {onCancel && (\n <Button variant=\"outline\" onClick={onCancel} aria-label=\"Cancel crop\">\n Cancel\n </Button>\n )}\n <Button\n variant=\"accent\"\n onClick={async () => {\n const result = await exportCrop()\n if (result) onComplete(result)\n }}\n leftIcon={<Check className=\"h-4 w-4\" />}\n aria-label=\"Apply crop\"\n >\n Apply\n </Button>\n </div>\n </div>\n )\n} ","\"use client\"\n\nimport React from 'react';\nimport { cn } from \"../utils/cn\";\n\ninterface MetricValueProps {\n value: string | number;\n label: string;\n className?: string;\n}\n\n/**\n * Displays a numeric/short textual value followed by a smaller grey label.\n * Example: 30s Generation Time\n */\nexport function MetricValue({ value, label, className }: MetricValueProps) {\n return (\n <div className={cn('flex items-end gap-2 whitespace-nowrap font-[\"DM_Sans\"] text-lg leading-[24px] text-ods-text-primary', className)}>\n {value}\n <span className=\"text-sm text-ods-text-secondary\">\n {label}\n </span>\n </div>\n );\n} ","\"use client\"\n\nimport React from 'react';\nimport { cn } from \"../utils/cn\";\nimport { SquareAvatar } from './square-avatar';\n\ninterface MSPDisplayProps {\n name: string;\n logoUrl?: string | null;\n size?: number; // avatar size in px (square)\n className?: string;\n}\n\nexport function MSPDisplay({ name, logoUrl, size = 40, className }: MSPDisplayProps) {\n return (\n <div className={cn('flex items-center gap-2 min-w-0', className)}>\n <SquareAvatar src={logoUrl ?? undefined} fallbackName={name} size={size} />\n <h2 className=\"truncate pl-2\">\n {name}\n </h2>\n </div>\n );\n} ","\"use client\";\n\nimport React from 'react';\nimport { cn } from \"../utils/cn\";\nimport { getProxiedImageUrl } from '../utils/image-proxy';\n\ninterface SquareAvatarProps {\n /** Image URL (if null/undefined, renders initials fallback) */\n src?: string | null;\n /** Fallback name used to derive initials when no image */\n fallbackName?: string;\n /** Size in px (applied to width & height). Defaults to 56 (Tailwind w-14 h-14). */\n size?: number;\n /** If true, avatar takes full width with square aspect ratio */\n fullWidth?: boolean;\n className?: string;\n}\n\n/**\n * Square avatar with rounded edges used across cards / dashboards.\n * Automatically shows image (via proxied URL) or initials fallback.\n */\nexport function SquareAvatar({ src, fallbackName = '', size = 56, fullWidth = false, className }: SquareAvatarProps) {\n const initials = React.useMemo(() => {\n if (!fallbackName) return '';\n return fallbackName\n .split(' ')\n .map((n) => n.charAt(0))\n .join('')\n .slice(0, 2)\n .toUpperCase();\n }, [fallbackName]);\n\n const style: React.CSSProperties = fullWidth ? {} : { width: size, height: size };\n\n return (\n <div\n className={cn(\n 'rounded-lg border border-ods-border flex items-center justify-center overflow-hidden bg-ods-bg-secondary',\n fullWidth ? 'w-full aspect-square' : 'flex-shrink-0',\n className,\n )}\n style={style}\n >\n {src ? (\n // eslint-disable-next-line @next/next/no-img-element\n <img src={getProxiedImageUrl(src) || src} alt=\"Avatar\" className=\"object-cover w-full h-full\" />\n ) : (\n <span className={cn(\n \"font-['DM_Sans'] text-ods-text-primary font-bold\",\n fullWidth ? 'text-4xl' : 'text-lg'\n )}>\n {initials}\n </span>\n )}\n </div>\n );\n} ","\"use client\"\n\nimport React from 'react';\nimport { Terminal, DollarSign, Network, Users } from 'lucide-react';\nimport { SECTION_HEADING_CLASS } from './layout/page-heading';\n\ninterface FeatureCardProps {\n icon: React.ReactNode;\n title: string;\n description: string;\n}\n\nconst FeatureCard: React.FC<FeatureCardProps> = ({ icon, title, description }) => {\n return (\n <div className=\"bg-ods-card border border-ods-border rounded-3xl p-6 flex flex-col gap-6 h-full hover:bg-ods-bg-hover transition-colors duration-200\">\n {/* Icon Container */}\n <div className=\"w-12 h-12 bg-ods-bg border border-ods-border rounded flex items-center justify-center\">\n <div className=\"w-6 h-6 text-ods-text-secondary\">\n {icon}\n </div>\n </div>\n \n {/* Text Container */}\n <div className=\"flex flex-col gap-2\">\n <h3 className=\"text-h3 text-ods-text-primary tracking-[-0.36px]\">\n {title}\n </h3>\n <p className=\"text-h4 text-ods-text-primary\">\n {description}\n </p>\n </div>\n </div>\n );\n};\n\nconst OpenSourceFeatures: React.FC = () => {\n const features = [\n {\n icon: <Terminal className=\"w-6 h-6\" />,\n title: \"Built on FOSS\",\n description: \"No black boxes. No hidden fees. Just transparent, community-driven software you control.\"\n },\n {\n icon: <DollarSign className=\"w-6 h-6\" />,\n title: \"Own Your Stack\",\n description: \"Replace overpriced, proprietary tools with open, auditable, and customizable components.\"\n },\n {\n icon: <Network className=\"w-6 h-6\" />,\n title: \"Modular by Design\",\n description: \"Add, remove, or extend features with ease — OpenFrame adapts to how you work.\"\n },\n {\n icon: <Users className=\"w-6 h-6\" />,\n title: \"Community-Powered\",\n description: \"Developed with and for MSPs by a global open-source community. You're not just a user — you're part of the roadmap.\"\n }\n ];\n\n return (\n <section className=\"w-full bg-ods-bg py-12 md:py-16 lg:py-20\">\n <div className=\"w-full max-w-[1920px] mx-auto px-6 md:px-20\">\n {/* Section Title */}\n <div className=\"flex flex-col items-center gap-10\">\n <h2 className={`${SECTION_HEADING_CLASS} text-center w-full`}>\n <span className=\"text-ods-accent\">100%</span>\n <span> Open-Source. </span>\n <span className=\"text-ods-accent\">0%</span>\n <span> Bullsh*t.</span>\n </h2>\n \n {/* Features Grid */}\n <div className=\"w-full\">\n <div className=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6\">\n {features.map((feature, index) => (\n <FeatureCard\n key={index}\n icon={feature.icon}\n title={feature.title}\n description={feature.description}\n />\n ))}\n </div>\n </div>\n </div>\n </div>\n </section>\n );\n};\n\nexport default OpenSourceFeatures; ","\"use client\"\n\nimport { ReactNode } from \"react\"\nimport { cn } from \"../utils/cn\"\n\ninterface PersistentPaginationProps {\n /**\n * Whether pagination is currently in a loading state\n */\n isLoading: boolean\n /**\n * The pagination component\n */\n children: ReactNode\n /**\n * Current page number\n */\n currentPage: number\n /**\n * Total number of pages\n */\n totalPages: number\n /**\n * Additional CSS classes\n */\n className?: string\n /**\n * Disabled opacity (0-1)\n */\n disabledOpacity?: number\n /**\n * Transition duration in milliseconds\n */\n transitionDuration?: number\n /**\n * Whether to show loading state in pagination\n */\n showLoadingState?: boolean\n}\n\n/**\n * PersistentPagination\n * \n * A wrapper component that keeps pagination visible during loading states\n * but provides visual feedback that it's temporarily disabled.\n * \n * Features:\n * - Maintains pagination visibility during content loading\n * - Disables interaction but preserves layout\n * - Shows current page context even when loading\n * - Provides accessibility support for loading states\n * - Smooth transitions between enabled/disabled states\n * \n * Usage:\n * ```tsx\n * <PersistentPagination\n * isLoading={isLoadingVendors}\n * currentPage={currentPage}\n * totalPages={totalPages}\n * >\n * <Pagination currentPage={currentPage} totalPages={totalPages} />\n * </PersistentPagination>\n * ```\n */\nexport function PersistentPagination({\n isLoading,\n children,\n currentPage,\n totalPages,\n className,\n disabledOpacity = 0.5,\n transitionDuration = 300,\n showLoadingState = true,\n}: PersistentPaginationProps) {\n // ALWAYS show pagination for predictable layout - just gray it out when not needed\n // Removed condition that hides pagination completely\n\n return (\n <div \n className={cn(\n \"relative transition-all ease-in-out\",\n \"flex justify-center items-center\",\n isLoading && \"pointer-events-none\",\n className\n )}\n style={{\n opacity: isLoading ? disabledOpacity : 1,\n transitionDuration: `${transitionDuration}ms`\n }}\n role=\"navigation\"\n aria-label=\"Pagination\"\n aria-busy={isLoading}\n data-loading={isLoading}\n >\n {/* Loading state announcement for screen readers */}\n {isLoading && (\n <div \n className=\"sr-only\" \n role=\"status\" \n aria-live=\"polite\"\n >\n Pagination temporarily disabled while loading page {currentPage} of {totalPages}\n </div>\n )}\n\n {/* Pagination controls with disabled state */}\n <div \n className={cn(\n \"relative transition-all ease-in-out\",\n isLoading && \"cursor-not-allowed\"\n )}\n style={{\n transitionDuration: `${transitionDuration}ms`\n }}\n aria-hidden={isLoading}\n >\n {children}\n </div>\n\n {/* REMOVED: Loading overlay - only card skeletons should show during loading */}\n </div>\n )\n}\n\n/**\n * Hook for managing pagination loading states\n */\nexport function usePaginationLoading(\n isLoading: boolean,\n currentPage: number,\n totalPages: number\n) {\n const shouldShowPagination = totalPages > 1 || isLoading\n \n const paginationProps = {\n 'aria-busy': isLoading,\n 'data-loading': isLoading,\n 'data-current-page': currentPage,\n 'data-total-pages': totalPages,\n }\n\n const getLoadingMessage = () => {\n if (isLoading) {\n return `Loading page ${currentPage} of ${totalPages}`\n }\n return `Page ${currentPage} of ${totalPages}`\n }\n\n return {\n shouldShowPagination,\n paginationProps,\n getLoadingMessage,\n }\n}\n\n/**\n * Enhanced pagination component that includes loading states\n */\ninterface PersistentPaginationWrapperProps {\n isLoading: boolean\n currentPage: number\n totalPages: number\n onPageChange?: (page: number) => void\n className?: string\n variant?: 'vendor' | 'blog'\n}\n\nexport function PersistentPaginationWrapper({\n isLoading,\n currentPage,\n totalPages,\n onPageChange,\n className,\n variant = 'vendor'\n}: PersistentPaginationWrapperProps) {\n const { getLoadingMessage } = usePaginationLoading(\n isLoading,\n currentPage,\n totalPages\n )\n\n // ALWAYS show pagination for predictable layout\n // For no results (totalPages = 0), show grayed out pagination\n const hasResults = totalPages > 0\n const displayTotalPages = hasResults ? totalPages : 1\n const displayCurrentPage = hasResults ? currentPage : 1\n // Only disable during loading, but still show when no results (just grayed out)\n const isPaginationDisabled = isLoading\n const hasNoResults = !hasResults && !isLoading\n\n // Use UnifiedPagination directly from ui-kit instead of importing from consuming apps\n // Both Pagination and BlogPagination in multi-platform-hub are just wrappers around UnifiedPagination\n const PaginationComponent = require('./unified-pagination').UnifiedPagination\n\n return (\n <div \n className={cn(\n \"relative transition-all ease-in-out flex justify-center items-center\",\n (isPaginationDisabled || hasNoResults) && \"pointer-events-none\",\n className\n )}\n style={{\n opacity: isPaginationDisabled ? 0.3 : hasNoResults ? 0.5 : 1,\n transitionDuration: \"300ms\"\n }}\n role=\"navigation\"\n aria-label=\"Pagination\"\n aria-busy={isLoading}\n data-loading={isLoading}\n data-has-results={hasResults}\n >\n {/* Loading/no results state announcement for screen readers */}\n {(isLoading || !hasResults) && (\n <div \n className=\"sr-only\" \n role=\"status\" \n aria-live=\"polite\"\n >\n {isLoading \n ? `Pagination temporarily disabled while loading page ${displayCurrentPage} of ${displayTotalPages}`\n : `No results available - pagination disabled`\n }\n </div>\n )}\n\n {/* Pagination controls with disabled state */}\n <div \n className={cn(\n \"relative transition-all ease-in-out\",\n isPaginationDisabled && \"cursor-not-allowed\"\n )}\n style={{\n transitionDuration: \"300ms\"\n }}\n aria-hidden={isPaginationDisabled}\n >\n <PaginationComponent\n currentPage={displayCurrentPage}\n totalPages={displayTotalPages}\n onPageChange={hasResults ? onPageChange : () => {}} // Provide empty function instead of undefined\n />\n </div>\n\n {/* REMOVED: Loading overlays - only card skeletons should show during loading */}\n </div>\n )\n} ","\"use client\"\n\nimport { StructuredPricingSummary as StructuredPricingItem } from '../utils/compare-utils-stub'\n\n// Using StructuredPricingItem from compare-utils instead of local interface\n\ninterface PricingStyleConfig {\n priceTextSize: string\n priceTextColor: string\n secondaryTextSize: string\n secondaryTextColor: string\n showTildePrefix: boolean\n fontFamily?: string\n}\n\ninterface PricingDisplayProps {\n pricing: StructuredPricingItem[] | string | StructuredPricingItem // Support both new structure and legacy string\n className?: string\n styleConfig?: PricingStyleConfig\n}\n\n// Default style configurations for different contexts\nexport const PRICING_STYLES = {\n // Comparison table style (current default)\n comparison: {\n priceTextSize: 'text-[16px]',\n priceTextColor: 'text-ods-text-primary',\n secondaryTextSize: 'text-[16px]',\n secondaryTextColor: 'text-ods-text-secondary',\n showTildePrefix: false,\n fontFamily: \"font-['DM_Sans']\"\n },\n // Vendor dropdown compact style (Figma design)\n compact: {\n priceTextSize: 'text-[12px]',\n priceTextColor: 'text-ods-text-secondary',\n secondaryTextSize: 'text-[12px]',\n secondaryTextColor: 'text-ods-text-secondary',\n showTildePrefix: true,\n fontFamily: \"font-['DM_Sans']\"\n },\n // Card style for vendor cards\n card: {\n priceTextSize: 'text-[14px]',\n priceTextColor: 'text-ods-text-primary',\n secondaryTextSize: 'text-[14px]',\n secondaryTextColor: 'text-ods-text-secondary',\n showTildePrefix: true,\n fontFamily: \"font-['DM_Sans']\"\n }\n} as const\n\n/**\n * Shared component for consistent pricing display with configurable styling\n * Now accepts structured pricing data and style configuration for better control\n */\nexport function PricingDisplay({ \n pricing, \n className = \"\", \n styleConfig = PRICING_STYLES.comparison \n}: PricingDisplayProps) {\n // Legacy support for string input\n if (typeof pricing === 'string') {\n return <LegacyPricingDisplay pricing={pricing} className={className} styleConfig={styleConfig} />\n }\n \n // Convert to array if single item\n const pricingArray = Array.isArray(pricing) ? pricing : [pricing];\n \n // Handle empty pricing\n if (!pricingArray || pricingArray.length === 0) {\n return (\n <span className={`${styleConfig.priceTextColor} ${styleConfig.priceTextSize} ${styleConfig.fontFamily} ${className}`}>\n No pricing data\n </span>\n )\n }\n \n // Handle single pricing item\n if (pricingArray.length === 1) {\n const item = pricingArray[0]\n const price = item.ranges?.[0]?.min || 0;\n const unit = item.ranges?.[0]?.unit;\n return (\n <span className={`${styleConfig.fontFamily} ${className}`}>\n <span className={`${styleConfig.priceTextColor} ${styleConfig.priceTextSize}`}>\n {formatPriceValue(price, styleConfig.showTildePrefix)}\n </span>\n {unit && (\n <span className={`${styleConfig.secondaryTextColor} ${styleConfig.secondaryTextSize}`}>\n /{unit}\n </span>\n )}\n </span>\n )\n }\n \n // Handle multiple pricing items\n const priceValues = pricingArray.map(item => formatPriceValue(item.ranges?.[0]?.min || 0, styleConfig.showTildePrefix))\n \n // Find the first item that has unit info\n const itemWithUnit = pricingArray.find(item => item.ranges?.[0]?.unit)\n \n return (\n <span className={`${styleConfig.fontFamily} ${className}`}>\n <span className={`${styleConfig.priceTextColor} ${styleConfig.priceTextSize}`}>\n {priceValues.join(' | ')}\n </span>\n {itemWithUnit && itemWithUnit.ranges?.[0]?.unit && (\n <span className={`${styleConfig.secondaryTextColor} ${styleConfig.secondaryTextSize}`}>\n /{itemWithUnit.ranges[0].unit}\n </span>\n )}\n </span>\n )\n}\n\n/**\n * Format price value consistently with configurable tilde prefix\n */\nfunction formatPriceValue(price: number | 'Free' | 'Contact', showTildePrefix: boolean = false): string {\n if (price === 'Free' || price === 'Contact') {\n return price\n }\n if (price === 0) {\n return 'Free'\n }\n return showTildePrefix ? `~$${price}` : `$${price}`\n}\n\n/**\n * Legacy component for backward compatibility with string input\n */\nfunction LegacyPricingDisplay({ \n pricing, \n className = \"\", \n styleConfig = PRICING_STYLES.comparison \n}: { \n pricing: string; \n className?: string; \n styleConfig?: PricingStyleConfig \n}) {\n // Handle \"Free\" case\n if (pricing === 'Free' || pricing === 'No pricing data') {\n return (\n <span className={`${styleConfig.priceTextColor} ${styleConfig.priceTextSize} ${styleConfig.fontFamily} ${className}`}>\n {pricing}\n </span>\n )\n }\n \n // Parse pricing string to separate main price from unit/cycle info\n const parsePricing = (pricingStr: string) => {\n // Handle comma-separated format like \"$10/device/month, $120/device/year\"\n if (pricingStr.includes(', ')) {\n // Split by comma and parse each part separately\n const parts = pricingStr.split(', ')\n const parsedParts = parts.map(part => {\n const match = part.trim().match(/^(\\$\\d+(?:-\\$\\d+)?|\\d+(?:-\\d+)?|Free)(.*)$/)\n if (match) {\n const price = match[1].startsWith('$') ? match[1] : `$${match[1]}`\n return {\n price,\n suffix: match[2]\n }\n }\n return { price: part.trim(), suffix: '' }\n })\n \n // Reconstruct with proper styling\n return {\n mainValue: parsedParts.map(p => p.price).join(', '),\n secondaryInfo: parsedParts.length > 0 && parsedParts[0].suffix ? parsedParts[0].suffix : ''\n }\n }\n \n // Handle pipe-separated format like \"$529/site/year | Free\"\n if (pricingStr.includes(' | ')) {\n const parts = pricingStr.split(' | ')\n const prices = parts.map(part => {\n const match = part.trim().match(/^(\\$\\d+(?:-\\$\\d+)?|\\d+(?:-\\d+)?|Free)(.*)$/)\n return match ? match[1] : part.trim()\n })\n \n return {\n mainValue: prices.join(' | '),\n secondaryInfo: ''\n }\n }\n \n // Handle multi-cycle format like \"$10/$120/device/month/year\"\n if (pricingStr.includes('/$')) {\n // Find all price patterns\n const pricePattern = /\\$\\d+(?:-\\$\\d+)?/g\n const matches = [...pricingStr.matchAll(pricePattern)]\n \n if (matches.length > 0) {\n const lastMatch = matches[matches.length - 1]\n const lastPriceEnd = lastMatch.index! + lastMatch[0].length\n \n const mainValue = pricingStr.substring(0, lastPriceEnd)\n const secondaryInfo = pricingStr.substring(lastPriceEnd)\n \n return { mainValue, secondaryInfo }\n }\n }\n \n // Handle single price format like \"$529/site/year\" or \"$0-$529/site/year\"\n const singlePriceMatch = pricingStr.match(/^(\\$\\d+(?:-\\$\\d+)?|Free)(.*)$/)\n if (singlePriceMatch) {\n return {\n mainValue: singlePriceMatch[1],\n secondaryInfo: singlePriceMatch[2]\n }\n }\n \n // Fallback - treat entire string as main value\n return {\n mainValue: pricingStr,\n secondaryInfo: ''\n }\n }\n \n const { mainValue, secondaryInfo } = parsePricing(pricing)\n \n return (\n <span className={`${styleConfig.fontFamily} ${className}`}>\n <span className={`${styleConfig.priceTextColor} ${styleConfig.priceTextSize}`}>{mainValue}</span>\n {secondaryInfo && (\n <span className={`${styleConfig.secondaryTextColor} ${styleConfig.secondaryTextSize}`}>{secondaryInfo}</span>\n )}\n </span>\n )\n}\n\n/**\n * Utility function to format pricing for display in the PricingDisplay component\n * This can be used to pre-process pricing strings if needed\n */\nexport function formatPricingForDisplay(pricing: string): string {\n return pricing\n} ","\"use client\"\n\nexport interface ResultsCountProps {\n currentPage: number\n pageSize: number\n totalResults: number\n resultType: 'vendors' | 'posts'\n sortingMessage?: string\n}\n\nexport function ResultsCount({\n currentPage,\n pageSize,\n totalResults,\n resultType,\n sortingMessage\n}: ResultsCountProps) {\n if (totalResults === 0) {\n return null\n }\n\n const startIndex = ((currentPage - 1) * pageSize) + 1\n const endIndex = Math.min(currentPage * pageSize, totalResults)\n const plural = resultType === 'vendors' ? 'vendors' : 'posts'\n const singular = resultType === 'vendors' ? 'vendor' : 'post'\n const displayType = totalResults === 1 ? singular : plural\n\n return (\n <div className=\"mb-6\">\n <p className=\"text-ods-text-secondary text-sm font-['DM_Sans']\">\n {totalResults > 0 && (\n <>\n Showing {startIndex}-{endIndex} of {totalResults} {displayType}\n {sortingMessage && (\n <span className=\"ml-2 text-ods-accent\">• {sortingMessage}</span>\n )}\n </>\n )}\n </p>\n </div>\n )\n} ","\"use client\"\n\nimport { OpenSourceIcon, CoinsIcon } from \"./icons-stub\"\nimport { OpenFrameLogo } from \"./openframe-logo\"\nimport { cn } from \"../utils/cn\"\nimport { Hand, Sparkles } from \"lucide-react\"\n\nexport interface VendorTagProps {\n type: 'open-source' | 'commercial' | 'free' | 'freemium' | 'paid' | 'enterprise' | 'recommended' | 'classification' | 'ai' | 'manual' | 'openframe_selected' | 'placeholder'\n text?: string\n className?: string\n size?: 'sm' | 'md'\n hidden?: boolean\n accentColor?: string\n}\n\nexport function VendorTag({\n type,\n text,\n className = \"\",\n hidden = false,\n size = 'md',\n accentColor\n}: VendorTagProps) {\n // Base classes for the tag container\n const baseClasses = cn(\n \"flex items-center gap-1.5 bg-ods-bg border border-ods-border rounded whitespace-nowrap\",\n size === 'sm' ? \"px-2 py-1\" : \"px-2.5 py-1.5\"\n )\n\n\n // Get display text and styling based on type\n const getTagContent = () => {\n switch (type) {\n case 'placeholder':\n return {\n text: \"Placeholder\",\n textColor: \"text-ods-text-primary\",\n icon: (\n <div className=\"w-4 h-4 rounded-sm flex items-center justify-center flex-shrink-0\">\n <Sparkles width={10} height={10} className=\"text-ods-text-primary\" />\n </div>\n )\n }\n case 'ai':\n return {\n text: \"AI Selected\",\n textColor: \"text-ods-text-primary\",\n icon: (\n <div className=\"w-4 h-4 rounded-sm flex items-center justify-center flex-shrink-0\">\n <Sparkles width={10} height={10} className=\"text-ods-text-primary\" />\n </div>\n )\n }\n case 'manual':\n return {\n text: \"Manually Selected\",\n textColor: \"text-ods-text-secondary\",\n icon: (\n <div className=\"w-4 h-4 rounded-sm flex items-center justify-center flex-shrink-0\">\n <Hand width={10} height={10} className=\"text-ods-text-secondary\" />\n </div>\n )\n }\n case 'open-source':\n return {\n text: text || \"Open Source\",\n icon: (\n <div \n className=\"w-4 h-4 bg-ods-accent rounded-sm flex items-center justify-center flex-shrink-0\"\n style={accentColor ? { backgroundColor: accentColor } : undefined}\n >\n <OpenSourceIcon width={10} height={10} className=\"text-[#1A1A1A]\" />\n </div>\n )\n }\n case 'commercial':\n return {\n text: text || \"Commercial Vendor\",\n icon: (\n <div className=\"w-4 h-4 bg-ods-border rounded-sm flex items-center justify-center flex-shrink-0\">\n <CoinsIcon width={10} height={10} className=\"text-ods-text-secondary\" />\n </div>\n )\n }\n case 'free':\n return {\n text: text || \"Free\",\n icon: (\n <div className=\"w-4 h-4 bg-ods-accent rounded-sm flex items-center justify-center flex-shrink-0\">\n <span className=\"text-[#1A1A1A] text-[8px] font-bold\">$</span>\n </div>\n )\n }\n case 'freemium':\n return {\n text: text || \"Freemium\",\n icon: (\n <div className=\"w-4 h-4 bg-ods-accent rounded-sm flex items-center justify-center flex-shrink-0\">\n <span className=\"text-[#1A1A1A] text-[8px] font-bold\">$</span>\n </div>\n )\n }\n case 'paid':\n return {\n text: text || \"Paid\",\n icon: (\n <div className=\"w-4 h-4 bg-ods-border rounded-sm flex items-center justify-center flex-shrink-0\">\n <CoinsIcon width={10} height={10} className=\"text-ods-text-secondary\" />\n </div>\n )\n }\n case 'enterprise':\n return {\n text: text || \"Enterprise\",\n icon: (\n <div className=\"w-4 h-4 bg-ods-accent rounded-sm flex items-center justify-center flex-shrink-0\">\n <span className=\"text-[#1A1A1A] text-[8px] font-bold\">E</span>\n </div>\n )\n }\n case 'recommended':\n return {\n text: text || \"Recommended\",\n icon: (\n <div className=\"w-4 h-4 bg-ods-accent rounded-sm flex items-center justify-center flex-shrink-0\">\n <span className=\"text-[#1A1A1A] text-[8px] font-bold\">★</span>\n </div>\n )\n }\n case 'classification':\n // Handle specific classification types based on the text value\n const classificationType = text?.toLowerCase()\n\n if (classificationType === 'open_source') {\n return {\n text: \"Open Source\",\n icon: (\n <div className=\"w-4 h-4 bg-ods-accent rounded-sm flex items-center justify-center flex-shrink-0\">\n <OpenSourceIcon width={10} height={10} className=\"text-[#1A1A1A]\" />\n </div>\n )\n }\n } else if (classificationType === 'commercial') {\n return {\n text: \"Commercial Vendor\",\n icon: (\n <div className=\"w-4 h-4 bg-ods-border rounded-sm flex items-center justify-center flex-shrink-0\">\n <CoinsIcon width={10} height={10} className=\"text-ods-text-secondary\" />\n </div>\n )\n }\n } else if (classificationType === 'openframe_selected') {\n return {\n text: \"OpenFrame Selected\",\n icon: <OpenFrameLogo lowerPathColor=\"currentColor\" upperPathColor=\"currentColor\" className=\"h-4 w-4 text-ods-accent\" />\n }\n } else {\n // Fallback for unknown classification types\n return {\n text: text || \"Classification\",\n icon: (\n <div className=\"w-4 h-4 bg-ods-accent rounded-sm flex items-center justify-center flex-shrink-0\">\n <span className=\"text-[#1A1A1A] text-[8px] font-bold\">C</span>\n </div>\n )\n }\n }\n case 'openframe_selected':\n return {\n text: text || \"OpenFrame Selected\",\n icon: <OpenFrameLogo lowerPathColor=\"currentColor\" upperPathColor=\"currentColor\" className=\"h-4 w-4 text-ods-accent\" />\n }\n default:\n return {\n text: text || type,\n icon: null\n }\n }\n }\n\n const { text: displayText, icon, textColor } = getTagContent()\n\n return (\n <div className={cn(baseClasses, className, hidden && \"invisible\")}>\n {icon}\n <span className={cn(\n \"font-mono font-semibold uppercase\",\n textColor ? textColor : \"text-ods-text-primary\",\n size === 'sm' ? \"text-[10px]\" : \"text-xs\"\n )}>\n {displayText}\n </span>\n </div>\n )\n} ","\"use client\"\n\nimport React from 'react';\nimport { cn } from \"../utils/cn\";\nimport { VendorTag } from './vendor-tag';\n\nexport type SelectionSourceType = 'ai' | 'manual' | 'placeholder';\n\ninterface SelectionSourceBadgeProps {\n source: SelectionSourceType;\n className?: string;\n hidden?: boolean;\n}\n\n/**\n * Small pill badge indicating whether a vendor was selected manually or by AI.\n * Colors follow OpenMSP design tokens.\n */\nexport function SelectionSourceBadge({ source, hidden = false }: SelectionSourceBadgeProps) {\n if (!source) {\n return null;\n }\n \n return (\n <VendorTag\n key={`source-${source}`}\n type={source?.toLowerCase() as 'ai' | 'manual' | 'placeholder'}\n size=\"sm\"\n hidden={hidden}\n />\n );\n} ","\"use client\";\n\nimport React from 'react';\nimport { cn } from \"../utils/cn\";\nimport { SquareAvatar } from './square-avatar';\n\ninterface UserDisplayProps {\n name: string;\n avatarUrl?: string | null;\n /** optional secondary text (e.g., relative timestamp) */\n subtitle?: string | null;\n /** Avatar size in px (defaults 32) */\n size?: number;\n className?: string;\n}\n\n/**\n * Reusable horizontal avatar + name (+ optional subtitle) row that follows\n * the visual pattern used in CommentCard headers.\n */\nexport function UserDisplay({ name, avatarUrl, subtitle, size = 32, className }: UserDisplayProps) {\n return (\n <div className={cn('flex items-center gap-2 min-w-0', className)}>\n <SquareAvatar src={avatarUrl ?? undefined} fallbackName={name} size={size} />\n <div className=\"min-w-0 flex-1\">\n <p className=\"font-['DM_Sans'] text-lg leading-[22px] text-ods-text-primary truncate\">\n {name}\n </p>\n {subtitle && (\n <span className=\"font-['DM_Sans'] text-md leading-[16px] text-ods-text-secondary truncate\">\n {subtitle}\n </span>\n )}\n </div>\n </div>\n );\n} ","import type React from \"react\"\nimport { cn } from \"../../utils/cn\"\nimport { UnifiedSkeleton, TextSkeleton, MediaSkeleton, InteractiveSkeleton } from \"./unified-skeleton\"\n\ninterface ContentSkeletonProps {\n className?: string\n}\n\n/**\n * Paragraph skeleton with varying line lengths for natural appearance\n */\nexport function ParagraphSkeleton({ \n className,\n lines = 4 \n}: ContentSkeletonProps & { lines?: number }) {\n const lineWidths = ['w-full', 'w-full', 'w-5/6', 'w-3/4', 'w-4/5', 'w-2/3']\n \n return (\n <div className={cn(\"space-y-2\", className)}>\n {Array.from({ length: lines }).map((_, index) => (\n <TextSkeleton.Body \n key={index}\n className={lineWidths[index % lineWidths.length]}\n />\n ))}\n </div>\n )\n}\n\n/**\n * List skeleton for navigation menus, categories, etc.\n */\nexport function ListSkeleton({ \n className,\n items = 5,\n showIcons = false,\n showActions = false \n}: ContentSkeletonProps & { \n items?: number\n showIcons?: boolean\n showActions?: boolean\n}) {\n return (\n <div className={cn(\"space-y-3\", className)}>\n {Array.from({ length: items }).map((_, index) => (\n <div key={index} className=\"flex items-center justify-between\">\n <div className=\"flex items-center gap-3\">\n {showIcons && <MediaSkeleton.Icon size=\"sm\" />}\n <TextSkeleton.Body className=\"w-32 md:w-48\" />\n </div>\n {showActions && (\n <div className=\"flex items-center gap-2\">\n <TextSkeleton.Caption className=\"w-8\" />\n <UnifiedSkeleton variant=\"default\" className=\"w-4 h-4\" />\n </div>\n )}\n </div>\n ))}\n </div>\n )\n}\n\n/**\n * Table skeleton for data displays\n */\nexport function TableSkeleton({ \n className,\n rows = 5,\n columns = 4 \n}: ContentSkeletonProps & { \n rows?: number\n columns?: number\n}) {\n return (\n <div className={cn(\"space-y-4\", className)}>\n {/* Table header */}\n <div className=\"grid gap-4\" style={{ gridTemplateColumns: `repeat(${columns}, 1fr)` }}>\n {Array.from({ length: columns }).map((_, index) => (\n <TextSkeleton.Subheading key={index} className=\"w-3/4\" />\n ))}\n </div>\n \n {/* Table rows */}\n <div className=\"space-y-3\">\n {Array.from({ length: rows }).map((_, rowIndex) => (\n <div \n key={rowIndex} \n className=\"grid gap-4 py-2 border-b border-ods-divider\"\n style={{ gridTemplateColumns: `repeat(${columns}, 1fr)` }}\n >\n {Array.from({ length: columns }).map((_, colIndex) => (\n <TextSkeleton.Body key={colIndex} className=\"w-2/3\" />\n ))}\n </div>\n ))}\n </div>\n </div>\n )\n}\n\n/**\n * Form skeleton for input fields and form layouts\n */\nexport function FormSkeleton({ \n className,\n fields = 4 \n}: ContentSkeletonProps & { fields?: number }) {\n return (\n <div className={cn(\"space-y-6\", className)}>\n {Array.from({ length: fields }).map((_, index) => (\n <div key={index} className=\"space-y-2\">\n <TextSkeleton.Body className=\"w-24\" />\n <InteractiveSkeleton.Input />\n {index % 3 === 0 && (\n <TextSkeleton.Caption className=\"w-48\" />\n )}\n </div>\n ))}\n \n <div className=\"flex gap-4 pt-4\">\n <InteractiveSkeleton.Button />\n <InteractiveSkeleton.Button className=\"bg-ods-border\" />\n </div>\n </div>\n )\n}\n\n/**\n * Navigation menu skeleton\n */\nexport function NavigationSkeleton({ \n className,\n items = 6,\n horizontal = true \n}: ContentSkeletonProps & { \n items?: number\n horizontal?: boolean\n}) {\n return (\n <nav \n className={cn(\n \"flex gap-4 md:gap-6\",\n !horizontal && \"flex-col\",\n className\n )}\n role=\"status\"\n aria-label=\"Loading navigation\"\n >\n {Array.from({ length: items }).map((_, index) => (\n <TextSkeleton.Body key={index} className=\"w-16 md:w-20\" />\n ))}\n </nav>\n )\n}\n\n/**\n * Profile/user info skeleton\n */\nexport function ProfileSkeleton({ \n className,\n showBio = true,\n showStats = true \n}: ContentSkeletonProps & { \n showBio?: boolean\n showStats?: boolean\n}) {\n return (\n <div className={cn(\"space-y-4\", className)}>\n {/* Avatar and basic info */}\n <div className=\"flex items-start gap-4\">\n <MediaSkeleton.Avatar size=\"lg\" />\n <div className=\"flex-1 space-y-2\">\n <TextSkeleton.Subheading className=\"w-1/2\" />\n <TextSkeleton.Caption className=\"w-1/3\" />\n <TextSkeleton.Body className=\"w-2/3\" />\n </div>\n </div>\n\n {/* Bio/description */}\n {showBio && (\n <div className=\"space-y-2\">\n <TextSkeleton.Body className=\"w-full\" />\n <TextSkeleton.Body className=\"w-4/5\" />\n <TextSkeleton.Body className=\"w-3/4\" />\n </div>\n )}\n\n {/* Stats */}\n {showStats && (\n <div className=\"flex gap-6\">\n <div className=\"text-center\">\n <TextSkeleton.Subheading className=\"w-8 mx-auto\" />\n <TextSkeleton.Caption className=\"w-12 mx-auto\" />\n </div>\n <div className=\"text-center\">\n <TextSkeleton.Subheading className=\"w-8 mx-auto\" />\n <TextSkeleton.Caption className=\"w-16 mx-auto\" />\n </div>\n <div className=\"text-center\">\n <TextSkeleton.Subheading className=\"w-8 mx-auto\" />\n <TextSkeleton.Caption className=\"w-14 mx-auto\" />\n </div>\n </div>\n )}\n </div>\n )\n}\n\n/**\n * Comment/review skeleton for user-generated content\n */\nexport function CommentSkeleton({ \n className,\n showRating = false,\n showReplies = false \n}: ContentSkeletonProps & { \n showRating?: boolean\n showReplies?: boolean\n}) {\n return (\n <div className={cn(\"space-y-3\", className)}>\n {/* User info */}\n <div className=\"flex items-center gap-3\">\n <MediaSkeleton.Avatar size=\"sm\" />\n <div className=\"flex-1 flex items-center gap-4\">\n <TextSkeleton.Body className=\"w-24\" />\n <TextSkeleton.Caption className=\"w-16\" />\n {showRating && (\n <div className=\"flex gap-1\">\n {Array.from({ length: 5 }).map((_, i) => (\n <MediaSkeleton.Icon key={i} size=\"sm\" />\n ))}\n </div>\n )}\n </div>\n </div>\n\n {/* Comment content */}\n <div className=\"ml-11 space-y-2\">\n <TextSkeleton.Body className=\"w-full\" />\n <TextSkeleton.Body className=\"w-3/4\" />\n <TextSkeleton.Body className=\"w-1/2\" />\n </div>\n\n {/* Actions */}\n <div className=\"ml-11 flex gap-4\">\n <TextSkeleton.Caption className=\"w-12\" />\n <TextSkeleton.Caption className=\"w-16\" />\n <TextSkeleton.Caption className=\"w-8\" />\n </div>\n\n {/* Replies */}\n {showReplies && (\n <div className=\"ml-11 pl-4 border-l border-ods-divider space-y-4\">\n {Array.from({ length: 2 }).map((_, index) => (\n <div key={index} className=\"space-y-2\">\n <div className=\"flex items-center gap-3\">\n <MediaSkeleton.Avatar size=\"sm\" />\n <TextSkeleton.Body className=\"w-20\" />\n <TextSkeleton.Caption className=\"w-12\" />\n </div>\n <div className=\"ml-11\">\n <TextSkeleton.Body className=\"w-full\" />\n <TextSkeleton.Body className=\"w-2/3\" />\n </div>\n </div>\n ))}\n </div>\n )}\n </div>\n )\n}\n\n/**\n * Feature list skeleton for product features, specifications, etc.\n */\nexport function FeatureListSkeleton({ \n className,\n features = 6,\n showIcons = true,\n grouped = false \n}: ContentSkeletonProps & { \n features?: number\n showIcons?: boolean\n grouped?: boolean\n}) {\n if (grouped) {\n // Grouped features with sections\n return (\n <div className={cn(\"space-y-6\", className)}>\n {Array.from({ length: 3 }).map((_, groupIndex) => (\n <div key={groupIndex} className=\"space-y-3\">\n <TextSkeleton.Subheading className=\"w-1/3\" />\n <div className=\"space-y-2\">\n {Array.from({ length: features / 3 }).map((_, featureIndex) => (\n <div key={featureIndex} className=\"flex items-center gap-3\">\n {showIcons && <MediaSkeleton.Icon size=\"sm\" />}\n <TextSkeleton.Body className=\"w-2/3\" />\n </div>\n ))}\n </div>\n </div>\n ))}\n </div>\n )\n }\n\n // Simple feature list\n return (\n <div className={cn(\"space-y-3\", className)}>\n {Array.from({ length: features }).map((_, index) => (\n <div key={index} className=\"flex items-center gap-3\">\n {showIcons && <MediaSkeleton.Icon size=\"sm\" />}\n <TextSkeleton.Body className=\"w-2/3\" />\n </div>\n ))}\n </div>\n )\n}\n\n/**\n * Timeline/activity skeleton\n */\nexport function TimelineSkeleton({ \n className,\n items = 5 \n}: ContentSkeletonProps & { items?: number }) {\n return (\n <div className={cn(\"space-y-6\", className)}>\n {Array.from({ length: items }).map((_, index) => (\n <div key={index} className=\"flex gap-4\">\n <div className=\"flex flex-col items-center\">\n <MediaSkeleton.Icon size=\"sm\" />\n {index < items - 1 && (\n <div className=\"w-px h-12 bg-ods-border mt-2\" />\n )}\n </div>\n <div className=\"flex-1 space-y-2\">\n <div className=\"flex items-center gap-3\">\n <TextSkeleton.Body className=\"w-1/3\" />\n <TextSkeleton.Caption className=\"w-16\" />\n </div>\n <TextSkeleton.Body className=\"w-full\" />\n <TextSkeleton.Body className=\"w-3/4\" />\n </div>\n </div>\n ))}\n </div>\n )\n}\n\n/**\n * Pricing/plan skeleton\n */\nexport function PricingSkeleton({ \n className,\n plans = 3 \n}: ContentSkeletonProps & { plans?: number }) {\n return (\n <div className={cn(\n \"grid gap-6\",\n plans === 2 && \"grid-cols-1 md:grid-cols-2\",\n plans === 3 && \"grid-cols-1 md:grid-cols-3\",\n plans === 4 && \"grid-cols-1 md:grid-cols-2 lg:grid-cols-4\",\n className\n )}>\n {Array.from({ length: plans }).map((_, index) => (\n <div key={index} className=\"bg-ods-card border border-ods-border rounded-lg p-6\">\n <div className=\"space-y-4\">\n {/* Plan name */}\n <TextSkeleton.Subheading className=\"w-1/2\" />\n \n {/* Price */}\n <div className=\"space-y-1\">\n <TextSkeleton.Heading className=\"w-1/3\" />\n <TextSkeleton.Caption className=\"w-1/4\" />\n </div>\n\n {/* Features */}\n <div className=\"space-y-3 py-4\">\n {Array.from({ length: 5 }).map((_, featureIndex) => (\n <div key={featureIndex} className=\"flex items-center gap-2\">\n <MediaSkeleton.Icon size=\"sm\" />\n <TextSkeleton.Body className=\"w-3/4\" />\n </div>\n ))}\n </div>\n\n {/* CTA Button */}\n <InteractiveSkeleton.Button className=\"w-full\" />\n </div>\n </div>\n ))}\n </div>\n )\n} ","import React from 'react'\nimport { cn } from \"../../utils/cn\"\n\ninterface ProfileLoadingSkeletonProps {\n className?: string;\n}\n\nexport function ProfileLoadingSkeleton({ className }: ProfileLoadingSkeletonProps) {\n return (\n <div className={cn(\"space-y-6\", className)}>\n {/* Header skeleton */}\n <div className=\"flex items-center space-x-4\">\n <div className=\"h-16 w-16 bg-gray-200 rounded-full animate-pulse\" />\n <div className=\"space-y-2\">\n <div className=\"h-4 w-32 bg-gray-200 rounded animate-pulse\" />\n <div className=\"h-3 w-24 bg-gray-200 rounded animate-pulse\" />\n </div>\n </div>\n \n {/* Content skeleton */}\n <div className=\"space-y-4\">\n <div className=\"h-4 w-full bg-gray-200 rounded animate-pulse\" />\n <div className=\"h-4 w-3/4 bg-gray-200 rounded animate-pulse\" />\n <div className=\"h-4 w-1/2 bg-gray-200 rounded animate-pulse\" />\n </div>\n </div>\n )\n}","import React from 'react'\nimport { cn } from \"../../utils/cn\"\nimport {\n UnifiedSkeleton,\n TextSkeleton,\n InteractiveSkeleton\n} from './unified-skeleton'\n\ninterface SkeletonProps {\n className?: string\n /**\n * Number of input fields to render on the right column (defaults to 4).\n */\n fields?: number\n}\n\n/**\n * MspProfileFormSkeleton\n * -----------------------------------------------------------------------------\n * Loading state for the <MspProfileForm /> component used in the Share-Your-Stack\n * wizard (and any other area that re-uses the form).\n *\n * Design Notes:\n * – Two-column grid matching the live component (logo upload on the left,\n * text / numeric inputs on the right).\n * – Left column: square/circular media skeleton to represent logo uploader.\n * – Right column: label + input pair per field.\n * – Mobile (<768px) collapses to one column via grid existing classes.\n * – Uses UnifiedSkeleton system to stay consistent with global loading design.\n */\nexport function MspProfileFormSkeleton({\n className,\n fields = 4,\n}: SkeletonProps) {\n return (\n <div\n className={cn(\n 'grid grid-cols-1 md:grid-cols-2 gap-6 items-stretch',\n className,\n )}\n role=\"status\"\n aria-label=\"Loading MSP profile form\"\n >\n {/* Left – Logo uploader placeholder */}\n <div className=\"flex flex-col space-y-2 h-full\">\n {/* Label skeleton */}\n <TextSkeleton.Body className=\"w-28\" />\n <div className=\"flex-1 min-h-[180px] md:min-h-full\">\n <UnifiedSkeleton className=\"w-full h-full rounded-lg\" aria-label=\"Loading company logo\" />\n </div>\n </div>\n\n {/* Right – Input fields grid (mirrors form) */}\n <div className=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\n {Array.from({ length: fields }).map((_, idx) => (\n <div key={idx} className=\"space-y-2 flex flex-col\">\n <TextSkeleton.Body className=\"w-40\" />\n <InteractiveSkeleton.Input />\n </div>\n ))}\n </div>\n </div>\n )\n} ","import React from 'react'\nimport { cn } from \"../../utils/cn\"\nimport { UnifiedSkeleton, TextSkeleton } from './unified-skeleton'\n\ninterface CategoryCardSkeletonProps {\n className?: string\n}\n\nexport function CategoryCardSkeleton({ className }: CategoryCardSkeletonProps) {\n return (\n <article\n className={cn(\n 'bg-[#1A1A1A] border border-[#424242] rounded-[12px] p-8 flex flex-col min-w-0 box-border',\n className,\n )}\n role=\"status\"\n aria-label=\"Loading category card\"\n >\n {/* Icons row */}\n <div className=\"flex gap-6 mb-8 justify-center items-center\">\n {Array.from({ length: 10 }).map((_, i) => (\n <UnifiedSkeleton\n key={i}\n variant=\"circular\"\n className=\"w-10 h-10 flex-shrink-0\"\n aria-label=\"Loading icon\"\n />\n ))}\n </div>\n\n {/* Text block */}\n <div className=\"flex-1 flex flex-col space-y-3\">\n <TextSkeleton.Heading className=\"w-3/4\" />\n <TextSkeleton.Body className=\"w-1/2\" />\n <TextSkeleton.Body className=\"w-full\" />\n </div>\n\n {/* Arrow button placeholder */}\n <div className=\"mt-4 flex justify-end\">\n <UnifiedSkeleton className=\"w-12 h-12 rounded-[6px]\" aria-label=\"Loading button\" />\n </div>\n </article>\n )\n} ","import React from 'react'\nimport { cn } from \"../../utils/cn\"\nimport { TextSkeleton, UnifiedSkeleton } from './unified-skeleton'\n\ninterface CategoryVendorSelectorSkeletonProps {\n /** number of subcategory blocks to render */\n subcategories?: number\n className?: string\n}\n\nexport function CategoryVendorSelectorSkeleton({ subcategories = 3, className }: CategoryVendorSelectorSkeletonProps) {\n return (\n <div\n className={cn('bg-ods-card border border-ods-border rounded-lg p-6', className)}\n role=\"status\"\n aria-label=\"Loading category section\"\n >\n {/* Header */}\n <div className=\"mb-6 space-y-2 max-w-xl\">\n <TextSkeleton.Heading className=\"w-1/2\" />\n <TextSkeleton.Body className=\"w-3/4\" />\n </div>\n\n {/* Subcategory blocks */}\n <div className=\"space-y-6\">\n {Array.from({ length: subcategories }).map((_, idx) => (\n <div key={idx} className=\"space-y-2\">\n {/* Subcategory title */}\n <TextSkeleton.Subheading className=\"w-1/3\" />\n {/* Vendor slots container */}\n <div className=\"grid grid-cols-2 md:grid-cols-3 gap-4\">\n {Array.from({ length: 2 }).map((__, j) => (\n <UnifiedSkeleton key={j} className=\"h-20 rounded-lg\" />\n ))}\n </div>\n </div>\n ))}\n </div>\n </div>\n )\n} ","import React from 'react'\nimport { cn } from \"../../utils/cn\"\nimport { UnifiedSkeleton, TextSkeleton } from './unified-skeleton'\n\ninterface WizardLayoutSkeletonProps {\n steps?: number\n className?: string\n}\n\nexport function WizardLayoutSkeleton({ steps = 6, className }: WizardLayoutSkeletonProps) {\n return (\n <div className={cn('space-y-6', className)} role=\"status\" aria-label=\"Loading wizard layout\">\n {/* Progress bar */}\n <div className=\"flex flex-wrap items-center gap-2\">\n {Array.from({ length: steps }).map((_, i) => (\n <UnifiedSkeleton key={i} className=\"h-6 w-24 rounded\" />\n ))}\n </div>\n\n {/* Header */}\n <div className=\"space-y-2 max-w-2xl\">\n <TextSkeleton.Heading className=\"w-2/3\" />\n <TextSkeleton.Body className=\"w-1/2\" />\n </div>\n\n {/* Bottom navigation buttons */}\n <div className=\"flex justify-between mt-10\">\n <UnifiedSkeleton className=\"h-10 w-24 rounded\" />\n <UnifiedSkeleton className=\"h-10 w-32 rounded\" />\n </div>\n </div>\n )\n} ","import React from 'react';\n\ninterface MarginReportSkeletonProps {\n /** Enable pulse animation (default: true) */\n animate?: boolean;\n /** Optional explanation text shown above overlay content */\n description?: React.ReactNode;\n /** Optional React node displayed over skeleton (button, loader, etc.) */\n overlayContent?: React.ReactNode;\n}\n\nexport function MarginReportSkeleton({ animate = true, description, overlayContent }: MarginReportSkeletonProps) {\n return (\n <main className={`bg-ods-bg ${animate ? 'animate-pulse' : ''} relative min-h-screen`}>\n <div className=\"max-w-[1920px] px-6 md:px-20 py-6 md:py-10 mx-auto space-y-10\">\n {/* Header */}\n <div className=\"space-y-3\">\n <div className=\"h-10 w-72 bg-[#2A2A2A] rounded\" />\n <div className=\"h-4 w-80 bg-[#2A2A2A] rounded\" />\n </div>\n\n {/* Summary cards */}\n <div className=\"grid grid-cols-1 md:grid-cols-3 gap-6\">\n {Array.from({ length: 3 }).map((_, i) => (\n <div key={i} className=\"h-32 bg-ods-card border border-ods-border rounded\" />\n ))}\n </div>\n\n {/* MSP Profile & Report Info cards */}\n <div className=\"grid grid-cols-1 md:grid-cols-2 gap-6\">\n {/* MSP profile skeleton */}\n <div className=\"bg-ods-card border border-ods-border rounded-lg p-6 flex items-center gap-4 animate-pulse\">\n <div className=\"w-14 h-14 rounded-lg bg-[#2A2A2A]\" />\n <div className=\"flex-1 space-y-2\">\n <div className=\"h-4 bg-[#2A2A2A] rounded w-3/4\" />\n <div className=\"h-3 bg-[#2A2A2A] rounded w-1/2\" />\n </div>\n </div>\n\n {/* Report info skeleton */}\n <div className=\"bg-ods-card border border-ods-border rounded-lg p-6 flex flex-col gap-4 animate-pulse\">\n <div className=\"flex items-center justify-between gap-4\">\n <div className=\"flex items-center gap-2\">\n <div className=\"w-10 h-10 rounded-lg bg-[#2A2A2A]\" />\n <div className=\"space-y-1\">\n <div className=\"h-4 w-32 bg-[#2A2A2A] rounded\" />\n <div className=\"h-3 w-20 bg-[#2A2A2A] rounded\" />\n </div>\n </div>\n <div className=\"h-6 w-36 bg-[#2A2A2A] rounded\" />\n </div>\n <div className=\"h-4 w-40 bg-[#2A2A2A] rounded mt-4\" />\n </div>\n </div>\n\n {/* Vendor solution lists (Commercial & Open-Source) */}\n <div className=\"grid grid-cols-1 md:grid-cols-2 gap-6\">\n {['Commercial Stack', 'Open-Source Stack'].map((label, idx) => (\n <div key={idx} className=\"bg-ods-card border border-ods-border rounded-lg overflow-hidden flex flex-col animate-pulse\">\n {/* list header */}\n <div className=\"flex items-center justify-between px-6 py-4\">\n <div className=\"h-6 w-40 bg-[#2A2A2A] rounded\" />\n <div className=\"flex items-center gap-2\">\n <div className=\"h-5 w-20 bg-[#2A2A2A] rounded\" />\n <div className=\"h-4 w-10 bg-[#2A2A2A] rounded\" />\n </div>\n </div>\n\n {/* vendor rows */}\n <div className=\"flex-1 flex flex-col gap-3 p-3\">\n {Array.from({ length: 5 }).map((_, j) => (\n <div key={j} className=\"bg-ods-bg border border-ods-border rounded-lg px-4 py-3 flex items-center justify-between\">\n {/* left section: icon + text */}\n <div className=\"flex items-center gap-3 min-w-0\">\n <div className=\"w-12 h-12 bg-[#2A2A2A] rounded-lg flex-shrink-0\" />\n <div className=\"flex flex-col min-w-0\">\n <div className=\"h-4 w-32 bg-[#2A2A2A] rounded\" />\n <div className=\"hidden md:block h-3 w-24 bg-[#2A2A2A] rounded mt-1\" />\n </div>\n </div>\n {/* right addon: cost text */}\n <div className=\"flex items-center gap-1 flex-shrink-0\">\n <div className=\"h-4 w-16 bg-[#2A2A2A] rounded\" />\n <div className=\"h-3 w-8 bg-[#2A2A2A] rounded\" />\n </div>\n </div>\n ))}\n </div>\n </div>\n ))}\n </div>\n\n {/* Strategic Recommendations header placeholder */}\n <div className=\"h-6 w-60 bg-[#2A2A2A] rounded\" />\n\n {/* Recommendations grid */}\n <div className=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6\">\n {Array.from({ length: 3 }).map((_, i) => (\n <div key={i} className=\"h-48 bg-ods-card border border-ods-border rounded\" />\n ))}\n </div>\n\n {/* OpenFrame value section */}\n <div className=\"border border-ods-border rounded-3xl p-8 space-y-6\">\n {/* Section header */}\n <div className=\"flex items-start gap-6\">\n <div className=\"flex-1 space-y-2 min-w-0\">\n <div className=\"h-8 w-72 bg-[#2A2A2A] rounded\" />\n <div className=\"h-4 w-3/4 bg-[#2A2A2A] rounded\" />\n </div>\n {/* Logo placeholder */}\n <div className=\"w-12 h-12 bg-[#2A2A2A] rounded-md shrink-0\" />\n </div>\n\n {/* Value cards grid */}\n <div className=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6\">\n {Array.from({ length: 3 }).map((_, i) => (\n <div key={i} className=\"h-32 bg-ods-card border border-ods-border rounded\" />\n ))}\n </div>\n </div>\n </div>\n\n {/* CTA Overlay */}\n {overlayContent && (\n <div className=\"absolute inset-0 bg-ods-card/80 z-10 rounded-lg pointer-events-none\">\n {/* Button centered relative to viewport */}\n <div className=\"fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 pointer-events-auto flex flex-col items-center gap-4 text-center px-4\">\n {description && (\n <h3 >\n {description}\n </h3>\n )}\n {overlayContent}\n </div>\n </div>\n )}\n </main>\n );\n} ","import { cn } from \"../../utils/cn\"\n\ninterface Props {\n rows?: number\n className?: string\n}\n\nexport function UsersGridSkeleton({ rows = 10, className }: Props) {\n const skeletonRows = Array.from({ length: rows })\n return (\n <div className={cn('overflow-x-auto rounded-lg border border-ods-border bg-ods-card animate-pulse', className)}>\n <table className=\"min-w-full divide-y divide-ods-border\">\n <thead className=\"bg-[#2A2A2A]\">\n <tr>\n {['Name', 'Email', 'Role', 'Created', 'Last Sign-In'].map((h) => (\n <th key={h} className=\"px-4 py-3 text-left text-[12px] font-medium text-ods-text-primary font-['DM_Sans']\">{h}</th>\n ))}\n </tr>\n </thead>\n <tbody className=\"divide-y divide-ods-border\">\n {skeletonRows.map((_, idx) => (\n <tr key={idx}>\n {Array.from({ length: 5 }).map((__, cell) => (\n <td key={cell} className=\"px-4 py-3 whitespace-nowrap\">\n <div className=\"h-4 bg-ods-border rounded w-full\"></div>\n </td>\n ))}\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n )\n} ","import type React from \"react\"\nimport { cn } from \"../../utils/cn\"\n\nexport interface OrganizationIconSkeletonProps {\n /**\n * Size variant (matches OrganizationIcon/VendorIcon sizes)\n * - xs: 24px (w-6 h-6)\n * - sm: 32px (w-8 h-8) - for devices table\n * - md: 40px (w-10 h-10) - for organizations table, dashboard (default)\n * - lg: 48px (w-12 h-12)\n * - l: 56px (w-14 h-14)\n * - xl: 64px (w-16 h-16) - for detail views\n */\n size?: 'xs' | 'sm' | 'md' | 'lg' | 'l' | 'xl'\n\n /**\n * Additional CSS classes\n */\n className?: string\n\n /**\n * Show background container (default: true)\n */\n showBackground?: boolean\n\n /**\n * Background style variant (default: 'dark')\n */\n backgroundStyle?: 'dark' | 'light' | 'white'\n}\n\n/**\n * Size classes matching VendorIcon/OrganizationIcon exactly\n */\nconst sizeClasses = {\n xs: 'w-6 h-6',\n sm: 'w-8 h-8',\n md: 'w-10 h-10',\n lg: 'w-12 h-12',\n l: 'w-14 h-14',\n xl: 'w-16 h-16'\n}\n\n/**\n * Background style classes matching VendorIcon/OrganizationIcon exactly\n */\nconst backgroundClasses = {\n dark: 'bg-ods-bg border border-ods-border',\n light: 'bg-ods-card border border-ods-border',\n white: 'bg-white border border-[#E5E5E5]'\n}\n\n/**\n * OrganizationIconSkeleton - Loading skeleton for OrganizationIcon\n *\n * Matches VendorIcon styling exactly for 100% visual parity.\n * Matches the exact dimensions of OrganizationIcon to prevent layout jumps.\n * Use this in loading states before organization data is available.\n *\n * Usage:\n *\n * ```typescript\n * // In table skeleton (matches current usage)\n * <OrganizationIconSkeleton size=\"md\" />\n *\n * // In device card skeleton\n * <OrganizationIconSkeleton size=\"sm\" />\n *\n * // In detail view skeleton\n * <OrganizationIconSkeleton size=\"xl\" />\n *\n * // Without background\n * <OrganizationIconSkeleton size=\"sm\" showBackground={false} />\n * ```\n */\nexport function OrganizationIconSkeleton({\n size = 'md',\n className = '',\n showBackground = true,\n backgroundStyle = 'dark'\n}: OrganizationIconSkeletonProps) {\n const containerClasses = cn(\n sizeClasses[size],\n 'rounded-lg flex items-center justify-center flex-shrink-0',\n showBackground && backgroundClasses[backgroundStyle],\n !showBackground && 'overflow-hidden',\n className\n )\n\n return (\n <div\n className={containerClasses}\n role=\"status\"\n aria-label=\"Loading organization icon\"\n >\n <div className=\"w-1/2 h-1/2 bg-ods-border rounded-sm animate-pulse\" />\n </div>\n )\n}\n","import type React from \"react\"\nimport { cn } from \"../../utils/cn\"\nimport { OrganizationIconSkeleton } from \"./organization-icon-skeleton\"\nimport { TextSkeleton, MediaSkeleton, InteractiveSkeleton } from \"./unified-skeleton\"\n\nexport interface OrganizationCardSkeletonProps {\n /**\n * Additional CSS classes\n */\n className?: string\n\n /**\n * Show footer stats area\n */\n showFooter?: boolean\n\n /**\n * Show description area\n */\n showDescription?: boolean\n\n /** Optional tailwind classes to override the card container background & border */\n containerClassName?: string\n}\n\n/**\n * OrganizationCardSkeleton - Loading skeleton matching OrganizationCard exact layout\n *\n * Matches VendorCard skeleton structure for 100% visual parity.\n *\n * Structure:\n * - Header: 60x60px org logo + title + subtitle\n * - Description: Fixed 48px height with 2-line clamp\n * - Footer: Stats display area\n *\n * Prevents layout jumps by matching exact dimensions.\n */\nexport function OrganizationCardSkeleton({\n className,\n containerClassName,\n showFooter = true,\n showDescription = true\n}: OrganizationCardSkeletonProps) {\n return (\n <div\n className={cn(\n containerClassName || \"bg-ods-card border border-ods-border\",\n \"rounded-lg overflow-hidden h-full flex flex-col\",\n className\n )}\n role=\"status\"\n aria-label=\"Loading organization card\"\n >\n <div className=\"p-4 gap-3 flex flex-col\">\n {/* Header Section - Row layout matching OrganizationCard/VendorCard */}\n <div className=\"flex items-start gap-3 w-full\">\n {/* Logo Frame - 60px width fixed, matching actual structure */}\n <OrganizationIconSkeleton\n size=\"xl\"\n backgroundStyle=\"dark\"\n showBackground={true}\n className=\"w-[60px] h-[60px]\"\n />\n\n {/* Text Container - Column layout, matching actual structure */}\n <div className=\"flex-1 flex flex-col justify-center py-2 min-w-0 space-y-1\">\n {/* Title - Single line with proper width */}\n <TextSkeleton.Subheading className=\"w-3/4\" />\n {/* Subtitle (industry/tier) - Single line, shorter */}\n <TextSkeleton.Caption className=\"w-1/2\" />\n </div>\n </div>\n\n {/* Description Section - Fixed 48px height matching VendorCard */}\n {showDescription && (\n <div className=\"w-full h-12 overflow-hidden flex items-center\">\n <div className=\"space-y-1 w-full\">\n <TextSkeleton.Body className=\"w-full\" />\n <TextSkeleton.Body className=\"w-2/3\" />\n </div>\n </div>\n )}\n\n {/* Footer Section - Stats display */}\n {showFooter && (\n <div className=\"flex items-center justify-between gap-2 w-full min-w-0\">\n {/* Stats Container */}\n <div className=\"flex items-center gap-3 md:gap-4 min-w-0 flex-shrink\">\n {/* Stat 1 */}\n <div className=\"flex items-center gap-1 flex-shrink-0\">\n <MediaSkeleton.Icon size=\"sm\" className=\"w-5 h-5\" />\n <TextSkeleton.Caption className=\"w-8\" />\n </div>\n\n {/* Stat 2 */}\n <div className=\"flex items-center gap-1 flex-shrink-0\">\n <MediaSkeleton.Icon size=\"sm\" className=\"w-5 h-5\" />\n <TextSkeleton.Caption className=\"w-10\" />\n </div>\n </div>\n\n {/* Tag/Badge Section */}\n <div className=\"flex-shrink-0\">\n <div className=\"flex items-center gap-1.5 bg-ods-bg border border-ods-border rounded px-2.5 py-1.5\">\n <div className=\"w-4 h-4 bg-ods-border rounded-sm flex items-center justify-center flex-shrink-0\">\n <MediaSkeleton.Icon size=\"sm\" className=\"w-2.5 h-2.5\" />\n </div>\n <TextSkeleton.Caption className=\"w-16\" />\n </div>\n </div>\n </div>\n )}\n </div>\n </div>\n )\n}\n\n/**\n * OrganizationCardSkeletonGrid - Grid of organization card skeletons\n *\n * Matches responsive grid layout:\n * - Mobile: 1 column\n * - Tablet (md): 2 columns\n * - Desktop (xl): 3 columns\n */\nexport function OrganizationCardSkeletonGrid({\n count = 12,\n className,\n containerClassName,\n showFooter = true,\n showDescription = true\n}: {\n count?: number\n className?: string\n containerClassName?: string\n showFooter?: boolean\n showDescription?: boolean\n}) {\n return (\n <div\n className={cn(\n \"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 md:gap-6\",\n className\n )}\n role=\"status\"\n aria-label={`Loading ${count} organization cards`}\n >\n {Array.from({ length: count }, (_, index) => (\n <OrganizationCardSkeleton\n key={index}\n containerClassName={containerClassName}\n showFooter={showFooter}\n showDescription={showDescription}\n />\n ))}\n </div>\n )\n}\n","import type React from \"react\"\nimport { cn } from \"../../utils/cn\"\nimport { OrganizationIconSkeleton } from \"./organization-icon-skeleton\"\n\nexport interface DeviceCardSkeletonProps {\n /**\n * Additional CSS classes\n */\n className?: string\n}\n\n/**\n * DeviceCardSkeleton - Loading skeleton matching DeviceCard exact layout\n *\n * Matches the structure of DeviceCard:\n * - Row 1: Device icon + Device name + More button\n * - Row 2: OS badge + Organization icon + Organization name\n * - Row 3: Status badge + Last seen\n *\n * Prevents layout jumps by matching exact dimensions.\n */\nexport function DeviceCardSkeleton({ className }: DeviceCardSkeletonProps) {\n return (\n <div\n className={cn(\n \"bg-ods-card rounded-[6px] border border-ods-border h-full\",\n className\n )}\n role=\"status\"\n aria-label=\"Loading device card\"\n >\n {/* Row 1: Device icon + Device name + More button */}\n <div className=\"flex gap-4 items-center px-4 py-3\">\n {/* Device type icon (8x8 container) */}\n <div className=\"w-8 h-8 bg-ods-bg border border-ods-border rounded-[6px] flex items-center justify-center flex-shrink-0\">\n <div className=\"w-4 h-4 bg-ods-border rounded animate-pulse\" />\n </div>\n\n {/* Device name */}\n <div className=\"flex-1 min-w-0\">\n <div className=\"h-6 w-3/4 bg-ods-border rounded animate-pulse\" />\n </div>\n\n {/* More button */}\n <div className=\"w-12 h-12 bg-ods-border rounded-[6px] flex-shrink-0 animate-pulse\" />\n </div>\n\n {/* Row 2: OS badge + Organization */}\n <div className=\"flex gap-4 items-center px-4 py-2\">\n {/* OS badge */}\n <div className=\"w-24 h-6 bg-ods-border rounded flex-shrink-0 animate-pulse\" />\n\n {/* Organization icon */}\n <OrganizationIconSkeleton size=\"sm\" />\n\n {/* Organization name */}\n <div className=\"flex-1 min-w-0\">\n <div className=\"h-5 w-1/2 bg-ods-border rounded animate-pulse\" />\n </div>\n </div>\n\n {/* Row 3: Status badge + Last seen */}\n <div className=\"flex gap-4 items-center px-4 py-2\">\n {/* Status badge */}\n <div className=\"w-20 h-6 bg-ods-border rounded-full flex-shrink-0 animate-pulse\" />\n\n {/* Last seen */}\n <div className=\"flex-1\">\n <div className=\"h-5 w-40 bg-ods-border rounded animate-pulse\" />\n </div>\n </div>\n </div>\n )\n}\n\n/**\n * DeviceCardSkeletonGrid - Grid of device card skeletons\n *\n * Matches DevicesGrid layout with responsive columns:\n * - Mobile: 1 column\n * - Tablet (md): 2 columns\n * - Desktop (lg): 3 columns\n * - Large (xl): 4 columns\n */\nexport function DeviceCardSkeletonGrid({\n count = 12,\n className\n}: {\n count?: number\n className?: string\n}) {\n return (\n <div\n className={cn(\n \"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4\",\n className\n )}\n role=\"status\"\n aria-label={`Loading ${count} device cards`}\n >\n {Array.from({ length: count }, (_, index) => (\n <DeviceCardSkeleton key={index} />\n ))}\n </div>\n )\n}\n","\"use client\"\n\nimport { VendorDetailLayoutSkeleton } from './loading';\n\nexport function VendorPageSkeleton() {\n return <VendorDetailLayoutSkeleton />;\n}","\"use client\"\n\nimport React from 'react';\nimport { SECTION_HEADING_CLASS } from './layout/page-heading';\n\ninterface WhyItMattersItemProps {\n number: string;\n title: string;\n description: string;\n isLast?: boolean;\n}\n\nconst WhyItMattersItem: React.FC<WhyItMattersItemProps> = ({ number, title, description, isLast }) => {\n return (\n <li\n className={`\n flex flex-col md:flex-row items-start gap-6 p-10 w-full\n transition-colors duration-200 hover:bg-ods-bg-hover\n ${!isLast ? 'border-b border-ods-border' : ''}\n `}\n >\n <span className=\"text-h2 tracking-[-0.02em] text-ods-accent\">\n {number}\n </span>\n <div className=\"flex-1\">\n <h3 className=\"text-h2 tracking-[-0.02em] text-ods-text-primary\">\n {title}\n </h3>\n <p className=\"text-h4 text-ods-text-primary mt-4\">\n {description}\n </p>\n </div>\n </li>\n );\n};\n\n\nconst WhyItMatters = () => {\n const items = [\n {\n number: \"1.\",\n title: \"Cut Costs\",\n description: \"Eliminate vendor fees with proven open-source alternatives\",\n },\n {\n number: \"2.\",\n title: \"Stay in Control\",\n description: \"Full visibility and data ownership\",\n },\n {\n number: \"3.\",\n title: \"Build What You Need\",\n description: \"Customize without vendor limitations\",\n },\n {\n number: \"4.\",\n title: \"Scale Freely\",\n description: \"Designed for multi-tenant MSP environments\",\n },\n ];\n\n return (\n <section className=\"bg-ods-bg\">\n <div className=\"w-full max-w-[1920px] mx-auto px-6 md:px-20\">\n <h2 className={`${SECTION_HEADING_CLASS} text-center mb-6`}>\n Why It Matters\n </h2>\n <div className=\"bg-ods-card border border-ods-border rounded-3xl overflow-hidden w-full\">\n <ol>\n {items.map((item, index) => (\n <WhyItMattersItem\n key={item.number}\n number={item.number}\n title={item.title}\n description={item.description}\n isLast={index === items.length - 1}\n />\n ))}\n </ol>\n </div>\n </div>\n </section>\n );\n};\n\nexport default WhyItMatters; ","\"use client\"\n\nimport { CheckCircleIcon as LucideCheckCircleIcon, XCircleIcon } from './ui/custom-icons'\n\ninterface YesNoDisplayProps {\n value: boolean\n yesText?: string\n noText?: string\n customText?: string // For displaying custom text instead of Yes/No\n className?: string\n}\n\nexport function YesNoDisplay({ \n value, \n yesText = \"Yes\", \n noText = \"No\", \n customText,\n className = \"\" \n}: YesNoDisplayProps) {\n // If custom text is provided, display it with appropriate icon\n if (customText) {\n return (\n <div className={`flex items-center gap-2 ${className}`}>\n <LucideCheckCircleIcon className=\"h-6 w-6\" />\n <span className=\"text-ods-text-primary text-[16px]\">{customText}</span>\n </div>\n )\n }\n \n // Standard Yes/No display\n if (value) {\n return (\n <div className={`flex items-center gap-2 ${className}`}>\n <LucideCheckCircleIcon className=\"h-6 w-6\" />\n <span className=\"text-ods-text-primary text-[16px]\">{yesText}</span>\n </div>\n )\n }\n \n return (\n <div className={`flex items-center gap-2 ${className}`}>\n <XCircleIcon className=\"h-6 w-6\" />\n <span className=\"text-ods-text-secondary text-[16px]\">{noText}</span>\n </div>\n )\n}\n\n/**\n * Unified logic to determine if a feature value should be considered \"Yes\" or \"No\"\n */\nexport function evaluateFeatureValue(\n value: string | null | undefined,\n dataType: 'boolean' | 'text' | 'number'\n): boolean {\n // Handle truly empty/null values\n if (value === null || value === undefined || value === '') {\n return false\n }\n \n const stringValue = String(value).toLowerCase().trim()\n \n // Handle explicit null/empty string values\n if (stringValue === 'null' || stringValue === 'n/a' || stringValue === 'none' || stringValue === '-') {\n return false\n }\n \n // Boolean data type\n if (dataType === 'boolean') {\n return stringValue === 'true' || \n stringValue === '1' || \n stringValue === 'yes' || \n stringValue === '✓' || \n stringValue === '✅'\n }\n \n // Text data type\n if (dataType === 'text') {\n // Handle emoji values\n if (stringValue === '✅' || stringValue === '✓') {\n return true\n }\n if (stringValue === '❌' || stringValue === '✗') {\n return false\n }\n \n // Handle explicit no values\n if (stringValue === 'no' || stringValue === 'false' || stringValue === '0') {\n return false\n }\n \n // Any other non-empty text is considered \"yes\"\n return stringValue.length > 0\n }\n \n // Number data type\n if (dataType === 'number') {\n const numValue = parseFloat(stringValue)\n return !isNaN(numValue) && numValue > 0\n }\n \n // Default fallback\n return false\n} ","'use client'\n\nimport React, { useState, useEffect } from 'react';\nimport { FlamingoLogo } from './flamingo-logo';\nimport { getBaseUrl } from '../utils';\n\ninterface MadeWithLoveProps {\n /** Custom class name for the container */\n className?: string;\n /** Size variant for the component */\n size?: 'sm' | 'md' | 'lg';\n /** Whether to show on mobile (responsive) */\n showOnMobile?: boolean;\n}\n\nexport function MadeWithLove({\n className = '',\n size = 'md',\n showOnMobile = true\n}: MadeWithLoveProps) {\n const [isMobile, setIsMobile] = useState(false);\n const [isHovered, setIsHovered] = useState(false);\n\n // Detect mobile/desktop for responsive behavior\n useEffect(() => {\n const checkMobile = () => {\n setIsMobile(window.innerWidth < 640); // 640px = sm breakpoint\n };\n\n checkMobile();\n window.addEventListener('resize', checkMobile);\n return () => window.removeEventListener('resize', checkMobile);\n }, []);\n\n // Size configuration with pixel values\n const sizeConfig = {\n sm: {\n logoSize: 14,\n fontSizeMobile: '12px',\n fontSizeDesktop: '14px',\n gap: '4px'\n },\n md: {\n logoSize: 16,\n fontSizeMobile: '14px',\n fontSizeDesktop: '16px',\n gap: '4px'\n },\n lg: {\n logoSize: 20,\n fontSizeMobile: '16px',\n fontSizeDesktop: '18px',\n gap: '6px'\n }\n };\n\n const config = sizeConfig[size];\n const flamingoUrl = getBaseUrl('flamingo');\n\n // Container styles using primitive CSS\n const containerStyle: React.CSSProperties = {\n display: (!showOnMobile && isMobile) ? 'none' : 'inline-flex',\n alignItems: 'center',\n gap: config.gap,\n fontSize: isMobile ? config.fontSizeMobile : config.fontSizeDesktop,\n lineHeight: 1.5,\n color: '#ffffff',\n fontFamily: 'inherit'\n };\n\n // Button/link styles using primitive CSS\n const linkStyle: React.CSSProperties = {\n display: 'inline-flex',\n alignItems: 'center',\n gap: '2px',\n padding: 0,\n margin: 0,\n background: 'transparent',\n border: 'none',\n cursor: 'pointer',\n textDecoration: isHovered ? 'underline' : 'none',\n color: '#ffffff',\n fontSize: '14px',\n fontFamily: '\"Azeret Mono\", monospace',\n transition: 'text-decoration 0.2s ease',\n outline: 'none'\n };\n\n // Logo container styles\n const logoStyle: React.CSSProperties = {\n marginLeft: '2px',\n flexShrink: 0,\n display: 'flex',\n alignItems: 'center'\n };\n\n // Text span styles\n const textStyle: React.CSSProperties = {\n color: '#ffffff',\n marginLeft: '4px',\n textDecoration: 'none'\n };\n\n return (\n <div\n style={containerStyle}\n className={className}\n >\n Made with love by\n <a\n href={flamingoUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n style={linkStyle}\n onMouseEnter={() => setIsHovered(true)}\n onMouseLeave={() => setIsHovered(false)}\n >\n <div style={logoStyle}>\n <FlamingoLogo\n size={config.logoSize}\n color=\"#f357bb\" // Direct hex instead of CSS variable for cross-framework compatibility\n />\n </div>\n <span style={textStyle}>Flamingo</span>\n </a>\n </div>\n );\n}","\"use client\";\n\nimport { Label } from \"./ui/label\";\nimport DatePicker from 'react-datepicker';\nimport { cn } from '../utils/cn';\n\ninterface DateTimePickerProps {\n value?: Date;\n onChange: (date: Date | null) => void;\n disabled?: boolean;\n label?: string;\n placeholder?: string;\n className?: string;\n showTimeSelect?: boolean;\n timeFormat?: string;\n timeIntervals?: number;\n dateFormat?: string;\n}\n\n// Shared input styling that matches Input component pattern\nconst inputClassName = cn(\n \"flex h-10 w-full rounded-md border px-3 py-2 text-sm ring-offset-background\",\n \"file:border-0 file:bg-transparent file:text-sm file:font-medium\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2\",\n \"disabled:cursor-not-allowed disabled:opacity-50 touch-manipulation\",\n // Admin theme colors\n \"bg-ods-card border-ods-border text-ods-text-primary\",\n \"placeholder:text-[#767676] focus-visible:ring-[#FFC008]\",\n \"md:text-sm text-base\" // Mobile zoom prevention\n);\n\nexport function DateTimePicker({\n value,\n onChange,\n disabled = false,\n label,\n placeholder = \"Select date and time\",\n className,\n showTimeSelect = true,\n timeFormat = \"HH:mm\",\n timeIntervals = 15,\n dateFormat = \"MMMM d, yyyy h:mm aa\"\n}: DateTimePickerProps) {\n return (\n <div className={cn(\"space-y-2\", className)}>\n {label && (\n <Label className=\"font-['Azeret_Mono'] text-[12px] font-semibold leading-[1em] tracking-[-0.02em] text-ods-text-secondary uppercase\">\n {label}\n </Label>\n )}\n\n <div className=\"w-full\">\n <DatePicker\n selected={value}\n onChange={onChange}\n showTimeSelect={showTimeSelect}\n timeFormat={timeFormat}\n timeIntervals={timeIntervals}\n dateFormat={dateFormat}\n placeholderText={placeholder}\n disabled={disabled}\n wrapperClassName=\"w-full\"\n className={inputClassName}\n />\n </div>\n \n <style dangerouslySetInnerHTML={{ __html: `\n .react-datepicker-wrapper {\n width: 100% !important;\n display: block !important;\n }\n\n .react-datepicker__input-container {\n width: 100% !important;\n }\n\n .react-datepicker__input-container input {\n width: 100% !important;\n }\n\n /* Dark theme for calendar popup */\n .react-datepicker-popper {\n z-index: 9999 !important;\n }\n\n .react-datepicker {\n background-color: #212121 !important;\n border: 1px solid #3A3A3A !important;\n color: #FAFAFA !important;\n z-index: 9999 !important;\n }\n\n .react-datepicker__header {\n background-color: #2A2A2A !important;\n border-bottom: 1px solid #3A3A3A !important;\n color: #FAFAFA !important;\n }\n\n .react-datepicker__current-month {\n color: #FAFAFA !important;\n }\n\n .react-datepicker__day-name {\n color: #888888 !important;\n }\n\n .react-datepicker__day {\n color: #FAFAFA !important;\n }\n\n .react-datepicker__day:hover {\n background-color: #3A3A3A !important;\n }\n\n .react-datepicker__day--selected {\n background-color: #FFC008 !important;\n color: #000000 !important;\n }\n\n .react-datepicker__day--keyboard-selected {\n background-color: #FFC008 !important;\n color: #000000 !important;\n }\n\n .react-datepicker__time-container {\n border-left: 1px solid #3A3A3A !important;\n }\n\n .react-datepicker__time {\n background: #212121 !important;\n }\n\n .react-datepicker__time-box {\n width: 85px !important;\n }\n\n .react-datepicker-time__header {\n background-color: #2A2A2A !important;\n color: #FAFAFA !important;\n }\n\n .react-datepicker__time-list-item {\n color: #FAFAFA !important;\n }\n\n .react-datepicker__time-list-item:hover {\n background-color: #3A3A3A !important;\n }\n\n .react-datepicker__time-list-item--selected {\n background-color: #FFC008 !important;\n color: #000000 !important;\n }\n\n .react-datepicker__navigation {\n top: 1rem !important;\n }\n\n .react-datepicker__navigation--previous {\n border-right-color: #888888 !important;\n }\n\n .react-datepicker__navigation--next {\n border-left-color: #888888 !important;\n }\n ` }} />\n </div>\n );\n} ","'use client'\n\n/**\n * `<ProductReleasesView />` — the shared, SELF-CONTAINED releases LIST surface.\n *\n * The host configures only two things: the **api route** (`endpoint`, default\n * `/api/releases`) and the **internal page route** for a release's detail page\n * (via `runtime.composeContentUrl` / `basePath`). Everything else — reading the\n * search / status / page URL params the section chrome writes, fetching the\n * page, pagination, empty / error / loading states, card composition + nav —\n * happens INSIDE the lib. Mirrors the `DeliveryLists` internal-fetch pattern\n * (plain `fetch` + `useEffect`/`useState`, no react-query dependency).\n *\n * SSR: pass `initialData` to hydrate the first page without a client round-trip\n * (the hub can server-fetch); embedders omit it and the view fetches on mount.\n *\n * Card props: `buildCardProps` defaults to the lib's RICH `buildProductReleaseCardProps`\n * (full lg metadata — Type / Status / author / changelog counts); pass your own to customize.\n */\n\nimport * as React from 'react'\n\nimport { useRouter, useSearchParams, usePathname } from '../../../embed-shims'\nimport { useSelfFetch } from '../../../hooks/use-self-fetch'\nimport { useChatRuntime } from '../../../contexts/chat-runtime-context'\nimport type { ProductRelease, ProductReleaseListResponse } from '../../../types/product-release'\nimport { cn } from '../../../utils/cn'\nimport { resolveContentHref } from '../../../utils/content-href'\nimport { isModifierClick } from '../../chat/utils/chat-nav-resolution'\nimport { executeNavigationImperative } from '../../chat/utils/execute-navigation'\nimport { useEntityCardLink } from '../../chat/entity-cards/use-entity-card-link'\nimport { buildProductReleaseCardProps } from '../../chat/entity-cards/product-release-card-defaults'\nimport { EmptyState } from '../../empty-state'\nimport { LoadError } from '../../ui/error-state'\nimport { PersistentPaginationWrapper } from '../../persistent-pagination'\nimport { ProductReleaseCard, type ProductReleaseCardProps } from './product-release-card'\nimport { ProductReleaseCardSkeleton } from './product-release-card-skeleton'\nimport { DEV_SECTION_PARAM_KEYS } from '../../../utils/dev-sections/dev-section-param-keys'\n\nconst DEFAULT_ENDPOINT = '/api/releases'\n// Param keys sourced from the shared registry (see RoadmapView) — single source for the\n// chrome's written `?key=` and this view's read.\nconst DEFAULT_SEARCH_PARAM_KEY = DEV_SECTION_PARAM_KEYS.search\nconst DEFAULT_STATUS_PARAM_KEY = DEV_SECTION_PARAM_KEYS.releaseStatus\nconst DEFAULT_PAGE_PARAM_KEY = 'page'\n\n/** The `<ProductReleaseCard>` props the caller derives per release — the card's\n * own data fields minus the ones this view supplies (title/summary/version/\n * size/nav). `formattedDate` stays required so the spread satisfies the card. */\nexport type ProductReleaseCardExtras = Omit<\n ProductReleaseCardProps,\n 'title' | 'summary' | 'version' | 'size' | 'anchorProps' | 'onClick' | 'className'\n>\n\nexport interface ProductReleasesViewProps {\n /** GET endpoint for the releases list (the api route). The view appends\n * `?limit&offset&<search>&<release_status>`. Default `/api/releases`. */\n endpoint?: string\n /** Optional SSR hydrate for the first page — skips the initial client fetch. */\n initialData?: ProductReleaseListResponse\n /** Page size → fixed slot count + offset math. Default 5. */\n itemsPerPage?: number\n /** Fallback detail-href prefix when `runtime.composeContentUrl` is not wired\n * (single-platform embedders). Default `/releases`. */\n basePath?: string\n /** Derive the per-card prop bundle. Defaults to the lib's RICH lg builder\n * (`buildProductReleaseCardProps` — full Type/Status/author/changelog metadata)\n * so embedders get the complete card with zero config; pass your own to customize. */\n buildCardProps?: (release: ProductRelease) => ProductReleaseCardExtras\n /** URL param key for the search input. MUST match the chrome that writes it.\n * Default `'search'` (also the outbound query-param name). */\n searchParamKey?: string\n /** URL param key for the status filter. Default `'release_status'`. */\n statusParamKey?: string\n /** URL param key for the page number. Default `'page'`. */\n pageParamKey?: string\n className?: string\n}\n\n/**\n * Per-row child — owns the per-release hooks (`useChatRuntime` +\n * `useEntityCardLink` + `useRouter`) so they run at a component top level, NOT\n * inside the parent `.map()` (Rules-of-Hooks).\n */\nfunction ReleaseRow({\n release,\n basePath,\n buildCardProps,\n}: {\n release: ProductRelease\n basePath: string\n buildCardProps: (release: ProductRelease) => ProductReleaseCardExtras\n}) {\n const runtime = useChatRuntime()\n const router = useRouter()\n // Pass the HYDRATED junction (`product_release_platforms` — the release DAL\n // flattens each platform's `name` onto it), like the blog / case-study /\n // interview grids. The composer reads `name` → resolves to the CURRENT\n // platform when the release belongs to it → relative same-tab href.\n const cta = resolveContentHref(runtime?.composeContentUrl, {\n type: 'product_release',\n slug: release.slug,\n basePath,\n platforms: release.product_release_platforms,\n })\n const { target, rel } = useEntityCardLink({ href: cta.href, targetPlatform: cta.targetPlatform })\n\n const onClick = (e: React.MouseEvent<HTMLAnchorElement>) => {\n // Let the browser handle modifier / middle / new-tab clicks (shared\n // `isModifierClick` rule). Otherwise soft same-origin nav: the host's\n // `navigate` (hub docNav) if wired, else the registered embed-shims router.\n if (e.defaultPrevented || isModifierClick(e) || target === '_blank') return\n e.preventDefault()\n executeNavigationImperative({\n runtime,\n href: cta.href,\n targetPlatform: cta.targetPlatform,\n fallbackNavigate: router.push,\n })\n }\n\n return (\n <ProductReleaseCard\n size=\"lg\"\n title={release.title}\n summary={release.summary}\n version={release.version}\n {...buildCardProps(release)}\n anchorProps={{ href: cta.href, target, rel, onClick }}\n />\n )\n}\n\nexport function ProductReleasesView({\n endpoint = DEFAULT_ENDPOINT,\n initialData,\n itemsPerPage = 5,\n basePath = '/releases',\n buildCardProps = buildProductReleaseCardProps,\n searchParamKey = DEFAULT_SEARCH_PARAM_KEY,\n statusParamKey = DEFAULT_STATUS_PARAM_KEY,\n pageParamKey = DEFAULT_PAGE_PARAM_KEY,\n className,\n}: ProductReleasesViewProps = {}) {\n const searchParams = useSearchParams()\n const router = useRouter()\n const pathname = usePathname()\n\n // Filter / page state from the URL (written by the section chrome above).\n const search = searchParams.get(searchParamKey) || ''\n const status = searchParams.get(statusParamKey) || 'all'\n const currentPage = Math.max(1, parseInt(searchParams.get(pageParamKey) || '1', 10) || 1)\n const offset = (currentPage - 1) * itemsPerPage\n\n // Fold every query param into the url so it IS the fetch key.\n const listParams = new URLSearchParams({ limit: String(itemsPerPage), offset: String(offset) })\n if (search) listParams.set(searchParamKey, search)\n if (status && status !== 'all') listParams.set(statusParamKey, status)\n const { data, isLoading, error, reload } = useSelfFetch<ProductReleaseListResponse>(\n `${endpoint}?${listParams.toString()}`,\n { initialData },\n )\n\n const releases = data?.data ?? []\n const totalCount = data?.count ?? 0\n const totalPages = Math.ceil(totalCount / itemsPerPage)\n const hasActiveFilters = search !== '' || status !== 'all'\n const showEmpty = !isLoading && !error && releases.length === 0\n\n const goToPage = (page: number) => {\n const params = new URLSearchParams(searchParams.toString())\n params.set(pageParamKey, String(page))\n router.replace(`${pathname}?${params.toString()}`, { scroll: false })\n }\n const resetFilters = () => {\n const params = new URLSearchParams(searchParams.toString())\n params.delete(searchParamKey)\n params.delete(statusParamKey)\n params.delete(pageParamKey)\n router.replace(`${pathname}?${params.toString()}`, { scroll: false })\n }\n\n if (error) {\n return (\n <div className={cn('w-full', className)}>\n <LoadError message=\"Failed to load releases.\" onRetry={reload} />\n </div>\n )\n }\n\n return (\n <div className={cn('w-full flex flex-col gap-[40px]', className)}>\n <div className=\"min-h-[600px]\">\n {showEmpty ? (\n <div className=\"h-[600px] flex items-center justify-center\">\n {hasActiveFilters ? (\n <EmptyState\n type=\"search\"\n title=\"No releases found\"\n description=\"No releases match your current filters. Try adjusting your search or status filter.\"\n showCTA\n ctaText=\"Reset Filters\"\n onCtaClick={resetFilters}\n />\n ) : (\n <EmptyState\n type=\"generic\"\n title=\"No releases available\"\n description=\"Check back soon for product updates!\"\n showCTA={false}\n />\n )}\n </div>\n ) : (\n <>\n {/* ALWAYS render `itemsPerPage` slots — visibility toggles so the\n list height is stable across loading ↔ loaded. */}\n <div className=\"flex flex-col gap-6\">\n {Array.from({ length: itemsPerPage }).map((_, i) => {\n const release = releases[i]\n const hasData = !!release\n return (\n <div\n key={release?.id ?? `slot-${i}`}\n style={{ visibility: isLoading || hasData ? 'visible' : 'hidden' }}\n >\n {isLoading ? (\n <ProductReleaseCardSkeleton size=\"lg\" />\n ) : release ? (\n <ReleaseRow release={release} basePath={basePath} buildCardProps={buildCardProps} />\n ) : (\n <ProductReleaseCardSkeleton size=\"lg\" />\n )}\n </div>\n )\n })}\n </div>\n\n {/* Pagination — always present at the bottom for consistent spacing. */}\n <div className=\"mt-6 md:mt-8 flex justify-center\">\n {isLoading ? (\n <div className=\"h-12 m-3 w-64\" />\n ) : releases.length > 0 && totalPages > 1 ? (\n <PersistentPaginationWrapper\n isLoading={false}\n currentPage={currentPage}\n totalPages={totalPages}\n onPageChange={goToPage}\n variant=\"blog\"\n />\n ) : (\n <div className=\"h-12 m-3 w-64\" style={{ visibility: 'hidden' }} />\n )}\n </div>\n </>\n )}\n </div>\n </div>\n )\n}\n","\"use client\";\n\nimport { useState, useEffect, ComponentType } from 'react';\nimport Link from '../../../embed-shims/next-link';\nimport { useRouter } from '../../../embed-shims/next-navigation';\nimport { Card, CardContent } from '../../ui/card';\n// PageShell (wide) — match the related-content/FAQ rail container the hub\n// renders below this view (was ArticleDetailLayout, 1280px — narrower than\n// the rail, see hub detail-container alignment decision 2026-06-10).\nimport { PageShell } from '../../layout/article-detail-layout';\nimport { PageLayout } from '../../layout/page-layout';\nimport { ReleaseChangelogSection } from '../../ui/release-changelog-section';\nimport { RichMarkdownRenderer } from '../../ui/rich-markdown-renderer';\nimport { EntityTagBadges } from '../../features/entity-tag-badges';\nimport { EntityMetadataAuthorCell } from '../../chat/entity-cards/entity-author-card';\nimport type { EntityAuthor } from '../../../types/entity-author';\nimport { MediaGalleryStrip } from '../media-gallery-strip';\nimport { GitHubIcon } from '../../icons/github-icon';\nimport { AlertTriangle, ExternalLink, BookMarked, Sparkles, TrendingUp, Wrench } from 'lucide-react';\nimport { formatReleaseDate } from '../../../utils/date-formatters';\nimport { contentFetch } from '../../../utils/embed-content-fetch';\nimport { Video } from '../../features/video';\nimport { DetailPageSkeleton } from '../detail-page-skeleton';\nimport type { ChangelogEntry } from '../../../types/product-release';\nimport type { TagAssoc } from '../../../types/blog';\nimport type { VideoTeaser } from '../../../types/video-processing';\n\n// Types for injectable components\nexport interface MarkdownRendererProps {\n content: string;\n}\n\n// Canonical RoadmapItem shape lives in chat entity types — see\n// `src/components/chat/types/entities/roadmap-item.ts`. The product-release\n// detail page previously declared a structural placeholder\n// (`{ id; [k: string]: unknown }`) that conflicted with the canonical\n// shape once the entities barrel was added; re-exporting the canonical\n// type fixes the collision while keeping the same import path for\n// downstream consumers of `./release-detail-page`.\nimport type { RoadmapItem } from '../../chat/types/entities/roadmap-item';\nimport type { DeliveryResponse } from '../../../types/delivery';\n// Re-export both types for source-compat with consumers importing\n// through this module. Canonical sources:\n// - RoadmapItem → `../../chat/types/entities/roadmap-item`\n// - DeliveryResponse → `../../../types/delivery` (single source of\n// truth, shared with the lib `<DeliveryLists>` / `<DeliveryTable>`\n// components and the new types barrel).\nexport type { RoadmapItem, DeliveryResponse };\n\nexport interface RoadmapSectionProps {\n items: RoadmapItem[];\n isLoading: boolean;\n onItemUpdate?: (item: RoadmapItem) => void;\n}\n\nexport interface DeliverySectionProps {\n data: DeliveryResponse | null;\n isLoading: boolean;\n}\n\nexport interface VideoSectionProps {\n bites: VideoTeaser[];\n title?: string;\n filterPublished?: boolean;\n}\n\n// Type for the useRelease hook result\nexport interface UseReleaseResult {\n data: unknown;\n error: Error | null;\n isLoading: boolean;\n}\n\nexport interface VideoDisplaySectionProps {\n mainVideoUrl?: string | null;\n youtubeUrl?: string | null;\n highlightVideoUrl?: string | null;\n highlightVideoThumbnail?: string | null;\n mainVideoPoster?: string | null;\n title?: string;\n videoSummary?: string | null;\n videoBites?: VideoTeaser[];\n bitesTitle?: string;\n filterPublishedBites?: boolean;\n srtContent?: string | null;\n captionsUrl?: string | null;\n}\n\nexport interface ReleaseDetailPageProps {\n slug: string;\n initialData?: unknown; // Optional pre-fetched data for admin preview\n // Required: Hook for fetching release data (must be from app-level to use correct QueryClient)\n useRelease: (slug: string | undefined) => UseReleaseResult;\n // Injectable components for app-specific rendering\n MarkdownRenderer?: ComponentType<MarkdownRendererProps>;\n RoadmapSection?: ComponentType<RoadmapSectionProps>;\n DeliverySection?: ComponentType<DeliverySectionProps>;\n VideoSection?: ComponentType<VideoSectionProps>;\n /** Injectable video display section with tabs for full/highlight video + summary + bites */\n VideoDisplaySection?: ComponentType<VideoDisplaySectionProps>;\n // API endpoints for fetching linked tasks\n roadmapApiEndpoint?: string;\n deliveryApiEndpoint?: string;\n /** Back-button config — same pattern as `DevSectionPage` /\n * `LegalDocumentPage`. Pass `false` to hide. Default\n * `{ label: 'Back to home', href: '/' }`. */\n backButton?: { label?: string; href?: string } | false;\n /** Link target for the author name in the metadata grid — the host\n * computes it (public author page; absent ⇒ plain text). */\n authorHref?: string;\n}\n\n// Default renderer = the lib's `RichMarkdownRenderer` so out-of-the-box\n// release pages get full rich-link previews, embedded media, social cards,\n// etc. Hosts that want a different rendering (or a Supabase-aware preset)\n// override via the `MarkdownRenderer` prop.\nconst DefaultMarkdownRenderer = RichMarkdownRenderer;\n\nexport function ReleaseDetailPage({\n authorHref,\n slug,\n initialData,\n useRelease,\n MarkdownRenderer = DefaultMarkdownRenderer,\n RoadmapSection,\n DeliverySection,\n VideoSection,\n VideoDisplaySection,\n roadmapApiEndpoint = '/api/roadmap',\n deliveryApiEndpoint = '/api/delivery',\n backButton\n}: ReleaseDetailPageProps) {\n const router = useRouter();\n // Use pre-fetched data if provided (admin preview), otherwise fetch via hook (public)\n const { data: fetchedRelease, error, isLoading } = useRelease(initialData ? undefined : slug);\n const release = (initialData || fetchedRelease) as Record<string, unknown> | undefined;\n\n // Back-button config — mirrors DevSectionPage / LegalDocumentPage.\n // Default: { label: 'Back to home', href: '/' }. Pass `false` to hide\n // (e.g. embed-mode where the host owns navigation chrome).\n // Narrowing note: `backButton &&` already eliminates the `false` branch,\n // so the inner expressions are typed as `{ label?, href? } | undefined`.\n // Don't re-compare to `false` here — tsc TS2367s on the dead branch.\n const showBackButton = backButton !== false;\n const backLabel = (backButton ? backButton.label : undefined) ?? 'Back to home';\n const backHref = (backButton ? backButton.href : undefined) ?? '/';\n\n // Fetch roadmap and delivery tasks if linked to this release\n const [roadmapTasks, setRoadmapTasks] = useState<RoadmapItem[]>([]);\n const [deliveryData, setDeliveryData] = useState<DeliveryResponse | null>(null);\n const [roadmapLoading, setRoadmapLoading] = useState(false);\n const [deliveryLoading, setDeliveryLoading] = useState(false);\n\n useEffect(() => {\n async function fetchLinkedTasks() {\n if (!release) return;\n\n try {\n // Fetch roadmap tasks if linked\n const roadmapTasksData = release.clickup_roadmap_tasks as Array<{ clickup_task_id: string }> | undefined;\n if (roadmapTasksData && roadmapTasksData.length > 0 && RoadmapSection) {\n setRoadmapLoading(true);\n const roadmapIds = roadmapTasksData.map(t => t.clickup_task_id).join(',');\n const roadmapResponse = await contentFetch(`${roadmapApiEndpoint}?task_ids=${roadmapIds}`);\n const roadmapData = await roadmapResponse.json();\n setRoadmapTasks(roadmapData.items || []);\n setRoadmapLoading(false);\n }\n\n // Fetch delivery tasks if linked\n const deliveryTasksData = release.clickup_delivery_tasks as Array<{ clickup_task_id: string }> | undefined;\n if (deliveryTasksData && deliveryTasksData.length > 0 && DeliverySection) {\n setDeliveryLoading(true);\n const deliveryIds = deliveryTasksData.map(t => t.clickup_task_id).join(',');\n const deliveryResponse = await contentFetch(`${deliveryApiEndpoint}?task_ids=${deliveryIds}`);\n const deliveryResponseData = await deliveryResponse.json();\n setDeliveryData(deliveryResponseData);\n setDeliveryLoading(false);\n }\n } catch (err) {\n console.error('Error fetching linked tasks:', err);\n setRoadmapLoading(false);\n setDeliveryLoading(false);\n }\n }\n\n fetchLinkedTasks();\n }, [release, RoadmapSection, DeliverySection, roadmapApiEndpoint, deliveryApiEndpoint]);\n\n // Don't show loading skeleton if we have initialData\n if (!initialData && isLoading) {\n // `bare` + `PageShell` so the loading state matches the loaded page's full\n // width / padding / min-height (the wrapper supplies the page chrome).\n return (\n <PageShell>\n {/* Match the loaded page's top offset (TitleBlock's\n `pt-[var(--spacing-system-l)]`) so content doesn't jump on load. */}\n <div className=\"pt-[var(--spacing-system-l)]\">\n <DetailPageSkeleton bare metadataColumns={4} showImageGallery={true} />\n </div>\n </PageShell>\n );\n }\n\n if (error || !release) {\n return (\n <PageShell>\n <div className=\"text-center py-16\">\n <h1 className=\"text-4xl font-bold text-ods-text-primary mb-4\">Release Not Found</h1>\n <p className=\"text-xl text-ods-text-secondary\">The release you're looking for doesn't exist.</p>\n </div>\n </PageShell>\n );\n }\n\n const hasBreakingChanges = Array.isArray(release.breaking_changes) && release.breaking_changes.length > 0;\n\n // Type assertions for release data\n const releaseTitle = release.title as string;\n const releaseVersion = release.version as string;\n const releaseSummary = release.summary as string | null;\n const releaseContent = release.content as string | null;\n const releaseDate = release.release_date as string;\n const releaseType = release.release_type as string;\n const releaseStatus = release.release_status as string;\n const releaseMedia = release.release_media as Array<{ id?: string; media_type: string; media_url: string; title?: string }> | undefined;\n // Field-cast per this file's loose-release idiom (release_type etc. above)\n // — but to the SHARED EntityAuthor, never an inline shadow author shape.\n const author = release.author as EntityAuthor | undefined;\n const githubReleases = release.github_releases as Array<{ id: string; github_release_url: string }> | undefined;\n const knowledgeBaseLinks = release.knowledge_base_links as Array<{ id?: string; kb_article_path: string }> | string[] | undefined;\n const migrationGuideUrl = release.migration_guide_url as string | undefined;\n const documentationUrl = release.documentation_url as string | undefined;\n const youtubeUrl = release.youtube_url as string | undefined;\n const mainVideoUrl = release.main_video_url as string | undefined;\n const videoBites = release.video_bites as VideoTeaser[] | undefined;\n const highlightVideoUrl = release.highlight_video_url as string | undefined;\n const highlightVideoThumbnail = release.highlight_video_thumbnail as string | undefined;\n const breakingChanges = release.breaking_changes as ChangelogEntry[] | undefined;\n const featuresAdded = release.features_added as ChangelogEntry[] | undefined;\n const bugFixed = release.bugs_fixed as ChangelogEntry[] | undefined;\n const improvements = release.improvements as ChangelogEntry[] | undefined;\n\n return (\n <PageShell>\n <PageLayout\n backButton={\n showBackButton ? { label: backLabel, onClick: () => router.push(backHref) } : undefined\n }\n >\n <div className=\"space-y-6 md:space-y-8\">\n {/* Title Block */}\n <div className=\"flex flex-col md:flex-row md:items-end gap-4 w-full\">\n <div className=\"flex-1 flex flex-col gap-2\">\n {/* Title */}\n <h1 className=\"text-h1 tracking-[-1.12px] text-ods-text-primary\">\n {releaseTitle}\n </h1>\n\n {/* Version */}\n <p className=\"text-h4 text-ods-text-secondary\">\n Version: {releaseVersion}\n </p>\n </div>\n </div>\n\n {/* Tags — flat product_release_tags[] from entity_tags */}\n <EntityTagBadges tags={release.product_release_tags as TagAssoc[] | undefined} />\n\n {/* Metadata Grid */}\n <div className=\"grid grid-cols-1 md:grid-cols-4 border border-ods-border rounded-md overflow-hidden w-full\">\n {/* Release Type */}\n <div className=\"bg-ods-card border-b md:border-b-0 md:border-r border-ods-border p-4 flex flex-col gap-3\">\n <div className=\"flex flex-col gap-0\">\n <p className=\"text-h4 text-ods-text-primary\">\n {releaseType.toLocaleUpperCase()}\n </p>\n <p className=\"font-['DM_Sans'] font-medium text-[14px] leading-[20px] text-ods-text-secondary\">\n Release Type\n </p>\n </div>\n </div>\n\n {/* Release Status */}\n <div className=\"bg-ods-card border-b md:border-b-0 md:border-r border-ods-border p-4 flex flex-col gap-3\">\n <div className=\"flex flex-col gap-0\">\n <p className=\"text-h4 text-ods-text-primary\">\n {releaseStatus.toLocaleUpperCase()}\n </p>\n <p className=\"font-['DM_Sans'] font-medium text-[14px] leading-[20px] text-ods-text-secondary\">\n Release Status\n </p>\n </div>\n </div>\n\n {/* Release Date */}\n <div className=\"bg-ods-card border-b md:border-b-0 md:border-r border-ods-border p-4 flex flex-col gap-3\">\n <div className=\"flex flex-col gap-0\">\n <p className=\"text-h4 text-ods-text-primary\">\n {formatReleaseDate(releaseDate)}\n </p>\n <p className=\"font-['DM_Sans'] font-medium text-[14px] leading-[20px] text-ods-text-secondary\">\n Release Date\n </p>\n </div>\n </div>\n\n {/* Author — the shared metadata-grid author cell (it was extracted\n FROM this page; rendering the export instead of an inline copy\n keeps the two in lockstep). The stub author preserves the legacy\n \"Unknown Author\" rendering for author-less releases; the\n job_title-over-roleLabel rule matches the guide detail page. */}\n <EntityMetadataAuthorCell\n author={author ?? { full_name: null, avatar_url: null }}\n authorHref={author?.full_name ? authorHref : undefined}\n />\n </div>\n\n {/* Image gallery — shared strip + lightbox (images) / inline clips. */}\n <MediaGalleryStrip items={releaseMedia ?? []} maxDisplay={5} />\n\n {/* Summary */}\n {releaseSummary && (\n <div className=\"text-h4 text-ods-text-primary\">\n <p>{releaseSummary}</p>\n </div>\n )}\n\n {/* Video Display Section - Injectable or fallback */}\n {VideoDisplaySection ? (\n <VideoDisplaySection\n mainVideoUrl={mainVideoUrl}\n youtubeUrl={youtubeUrl}\n highlightVideoUrl={highlightVideoUrl}\n highlightVideoThumbnail={highlightVideoThumbnail}\n title={releaseTitle}\n videoBites={videoBites}\n bitesTitle=\"Video Clips\"\n filterPublishedBites={true}\n srtContent={release?.srt_content as string | null | undefined}\n captionsUrl={release?.captionsUrl as string | undefined}\n />\n ) : (\n <>\n {/*\n Fallback when no `VideoDisplaySection` is injected. `<Video>` is the\n SSoT for every video surface — single source of truth across YouTube,\n HLS, and MP4 paths.\n */}\n {youtubeUrl && (\n <Video\n kind=\"youtube\"\n url={youtubeUrl}\n title={`${releaseTitle} - Video`}\n layout=\"native\"\n />\n )}\n {!youtubeUrl && mainVideoUrl && (\n <Video\n url={mainVideoUrl}\n srtContent={release?.srt_content as string | undefined}\n captionsUrl={release?.captionsUrl as string | undefined}\n layout=\"centered\"\n />\n )}\n {highlightVideoUrl && (\n <Video\n url={highlightVideoUrl}\n poster={highlightVideoThumbnail}\n layout=\"centered\"\n />\n )}\n </>\n )}\n\n {/* Content */}\n {releaseContent && (\n <div className=\"text-h4 text-ods-text-primary\">\n <MarkdownRenderer content={releaseContent} />\n </div>\n )}\n\n {/* Breaking Changes Warning */}\n {hasBreakingChanges && (\n <Card className=\"border-red-500 bg-red-500/10\">\n <CardContent className=\"p-6\">\n <div className=\"flex items-center gap-3\">\n <AlertTriangle className=\"h-6 w-6 text-red-500\" />\n <div>\n <h3 className=\"font-bold text-red-500 text-lg\">Breaking Changes</h3>\n <p className=\"text-ods-text-secondary\">This release contains breaking changes. Review carefully before upgrading.</p>\n </div>\n </div>\n </CardContent>\n </Card>\n )}\n\n {/* Changelog Sections — icons match the catalog card's changelog\n strip taxonomy (Sparkles/Wrench/TrendingUp/AlertTriangle) so the\n user sees a consistent visual signature across catalog → detail. */}\n <ReleaseChangelogSection\n title=\"Breaking Changes\"\n entries={breakingChanges || []}\n isBreaking\n hideTitle\n icon={<AlertTriangle className=\"h-6 w-6\" />}\n SimpleMarkdownRenderer={MarkdownRenderer}\n />\n {/* Features / Bugs / Improvements use `previewFirst` — same\n progressive-disclosure pattern as the investor-update detail\n page's Key Highlights / Financial Notes sections. Shows the\n first entry in full + fade-masks the rest, with a \"Show N\n more / Show less\" toggle. Breaking Changes (above) stays\n fully expanded — it's critical info, not skim-friendly. */}\n <ReleaseChangelogSection\n title=\"Features Added\"\n entries={featuresAdded || []}\n icon={<Sparkles className=\"h-6 w-6\" />}\n previewFirst\n SimpleMarkdownRenderer={MarkdownRenderer}\n />\n <ReleaseChangelogSection\n title=\"Bugs Fixed\"\n entries={bugFixed || []}\n icon={<Wrench className=\"h-6 w-6\" />}\n previewFirst\n SimpleMarkdownRenderer={MarkdownRenderer}\n />\n <ReleaseChangelogSection\n title=\"Improvements\"\n entries={improvements || []}\n icon={<TrendingUp className=\"h-6 w-6\" />}\n previewFirst\n SimpleMarkdownRenderer={MarkdownRenderer}\n />\n\n {/* Video Bites Section - Only when VideoDisplaySection is not handling it */}\n {!VideoDisplaySection && VideoSection && videoBites && videoBites.length > 0 && (\n <VideoSection\n bites={videoBites}\n title=\"Video Clips\"\n filterPublished={true}\n />\n )}\n\n {/* Related Roadmap Items */}\n {RoadmapSection && (roadmapLoading || roadmapTasks.length > 0) && (\n <div className=\"space-y-4 w-full\">\n <p className=\"text-h5 tracking-[-0.28px] text-ods-text-secondary\">\n Related Roadmap Items\n </p>\n <RoadmapSection\n items={roadmapTasks}\n isLoading={roadmapLoading}\n onItemUpdate={(updatedItem) => {\n setRoadmapTasks(prevTasks =>\n prevTasks.map(task =>\n task.id === updatedItem.id ? updatedItem : task\n )\n );\n }}\n />\n </div>\n )}\n\n {/* Bug-fixes & Enhancements Section */}\n {DeliverySection && (deliveryLoading || (deliveryData && (deliveryData.completed.length > 0 || deliveryData.inProgress.length > 0))) && (\n <div className=\"w-full space-y-4\">\n <p className=\"text-h5 tracking-[-0.28px] text-ods-text-secondary\">\n Related Enhancements and Bug-fixes\n </p>\n <DeliverySection\n data={deliveryData}\n isLoading={deliveryLoading}\n />\n </div>\n )}\n\n {/* Related Links */}\n {(githubReleases?.length || knowledgeBaseLinks?.length || migrationGuideUrl || documentationUrl) && (\n <div className=\"space-y-1 w-full\">\n <p className=\"text-h5 tracking-[-0.28px] text-ods-text-secondary\">\n Related Links\n </p>\n <Card className=\"bg-ods-card border-ods-border p-6\">\n <div className=\"space-y-4\">\n {/* GitHub Releases */}\n {githubReleases && githubReleases.length > 0 && (\n <>\n {githubReleases.map((ghRelease) => (\n <div key={ghRelease.id} className=\"flex items-start gap-1\">\n <GitHubIcon className=\"shrink-0\" width={24} height={24} color=\"var(--color-text-secondary)\" />\n <span className=\"text-h4 text-ods-text-primary\">\n Github Release\n </span>\n <a\n href={ghRelease.github_release_url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-h4 text-[#ffc008] hover:underline\"\n >\n {ghRelease.github_release_url.split('/').pop()}\n </a>\n <ExternalLink className=\"h-6 w-6 text-[#ffc008] shrink-0\" />\n </div>\n ))}\n </>\n )}\n\n {/* Knowledge Base Links */}\n {knowledgeBaseLinks && knowledgeBaseLinks.length > 0 && (\n <>\n {knowledgeBaseLinks.map((linkObj) => {\n const path = typeof linkObj === 'string' ? linkObj : linkObj.kb_article_path;\n const linkId = typeof linkObj === 'string' ? path : linkObj.id || path;\n return (\n <div key={linkId} className=\"flex items-start gap-1\">\n <BookMarked className=\"h-6 w-6 text-ods-text-secondary shrink-0\" />\n <span className=\"text-h4 text-ods-text-primary\">\n Knowledge Base\n </span>\n <Link\n href={path.startsWith('http') ? path : `/knowledge-base${path.startsWith('/') ? '' : '/'}${path}`}\n className=\"text-h4 text-[#ffc008] hover:underline\"\n >\n {path.replace(/^\\//, '').split('/').pop()?.replace(/-/g, ' ') || 'View Article'}\n </Link>\n <ExternalLink className=\"h-6 w-6 text-[#ffc008] shrink-0\" />\n </div>\n );\n })}\n </>\n )}\n\n {/* Migration Guide */}\n {migrationGuideUrl && (\n <div className=\"flex items-start gap-1\">\n <BookMarked className=\"h-6 w-6 text-ods-text-secondary shrink-0\" />\n <a\n href={migrationGuideUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-h4 text-[#ffc008] hover:underline\"\n >\n 📖 Migration Guide\n </a>\n <ExternalLink className=\"h-6 w-6 text-[#ffc008] shrink-0\" />\n </div>\n )}\n\n {/* Documentation */}\n {documentationUrl && (\n <div className=\"flex items-start gap-1\">\n <BookMarked className=\"h-6 w-6 text-ods-text-secondary shrink-0\" />\n <a\n href={documentationUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-h4 text-[#ffc008] hover:underline\"\n >\n 📚 Documentation\n </a>\n <ExternalLink className=\"h-6 w-6 text-[#ffc008] shrink-0\" />\n </div>\n )}\n </div>\n </Card>\n </div>\n )}\n </div>\n </PageLayout>\n </PageShell>\n );\n}\n","\"use client\";\n\nimport React, { useState } from 'react';\nimport { Video } from '../features/video';\nimport { ImageGalleryModal } from '../ui/image-gallery-modal';\n\nexport interface MediaGalleryStripItem {\n media_type: string;\n media_url: string;\n title?: string | null;\n /** Stable key when present (e.g. release_media rows); falls back to index. */\n id?: string;\n}\n\n/** Still-images open the lightbox; clips ('video'/'demo') play inline. */\nfunction isGalleryImage(mediaType: string): boolean {\n return mediaType !== 'video' && mediaType !== 'demo';\n}\n\nexport interface MediaGalleryStripProps {\n items: MediaGalleryStripItem[];\n /** Optional cap on tiles shown (e.g. product-release shows 5). Default: all. */\n maxDisplay?: number;\n className?: string;\n}\n\n/**\n * Read-only media gallery — a horizontal-scroll strip of 240×200 tiles where\n * still-images open an {@link ImageGalleryModal} lightbox and video clips play\n * inline via {@link Video}. The lightbox index is the tile's rank among the\n * IMAGE-only items (clips are skipped), so the modal opens on the correct image.\n *\n * Single source of truth for the detail-page media strip — replaces the markup\n * that was hand-rolled inline in the product-release and \"What I Shipped\" detail\n * pages (the release copy also had a raw-index lightbox bug this component fixes).\n */\nexport function MediaGalleryStrip({ items, maxDisplay, className }: MediaGalleryStripProps) {\n const [galleryOpen, setGalleryOpen] = useState(false);\n const [galleryIndex, setGalleryIndex] = useState(0);\n\n if (!items || items.length === 0) return null;\n\n // Explicit numeric check (clamped): `maxDisplay={0}` means \"show none\", which a\n // truthy check would wrongly treat as \"no cap → show all\".\n const display = typeof maxDisplay === 'number' ? items.slice(0, Math.max(0, maxDisplay)) : items;\n const galleryImages = display.filter((m) => isGalleryImage(m.media_type)).map((m) => m.media_url);\n\n const tileClass =\n 'shrink-0 w-[240px] h-[200px] rounded-md overflow-hidden border border-ods-border bg-black transition-opacity';\n\n return (\n <>\n <div className={`flex gap-6 overflow-x-auto w-full ${className ?? ''}`}>\n {display.map((mediaItem, index) => {\n // Image tiles open the lightbox, so they're real <button>s — keyboard\n // focusable + Enter/Space activatable. Clips render in <Video>, which\n // owns its own controls, so their tile stays a plain container.\n if (isGalleryImage(mediaItem.media_type)) {\n return (\n <button\n key={mediaItem.id || index}\n type=\"button\"\n className={`${tileClass} cursor-pointer hover:opacity-80`}\n aria-label={`Open ${mediaItem.title || `media ${index + 1}`} in gallery`}\n onClick={() => {\n // Lightbox position = rank among image-only items (clips skipped).\n setGalleryIndex(display.slice(0, index).filter((m) => isGalleryImage(m.media_type)).length);\n setGalleryOpen(true);\n }}\n >\n <img src={mediaItem.media_url} alt={mediaItem.title || `Media ${index + 1}`} className=\"w-full h-full object-cover\" />\n </button>\n );\n }\n return (\n <div key={mediaItem.id || index} className={tileClass}>\n <Video url={mediaItem.media_url} layout=\"native\" />\n </div>\n );\n })}\n </div>\n\n {galleryImages.length > 0 && (\n <ImageGalleryModal\n images={galleryImages}\n isOpen={galleryOpen}\n onClose={() => setGalleryOpen(false)}\n initialIndex={galleryIndex}\n />\n )}\n </>\n );\n}\n","\"use client\";\n\nimport { DetailPageSkeleton } from '../detail-page-skeleton';\n\nexport function ReleaseDetailSkeleton() {\n return <DetailPageSkeleton metadataColumns={4} showImageGallery={true} />;\n}\n","'use client';\n\n/**\n * RoadmapGrid — the shared roadmap LIST surface.\n *\n * Two modes (one component, one voting state):\n * - `groupByQuarter` (DEFAULT): buckets items by `item.quarter`, sorts the\n * quarters chronologically, and renders each in a collapsible `<Accordion>`\n * — the same quarter grouping the hub's roadmap page has, now shared so\n * every embedder gets it for free. Quarters at/after the current quarter\n * (and within `quartersToKeepClosed` of it) open by default; older ones\n * collapse. When `hasActiveFilters`, all quarters expand.\n * - `groupByQuarter={false}`: a flat 2-col grid (related-content rails that\n * pass a small pre-filtered `items` slice).\n *\n * Voting state (`useRoadmapVoting` + the in-flight set) lives ONCE at this\n * level and is shared across every quarter's grid, so a vote in Q3 and a vote\n * in Q4 can't race separate states. A successful vote triggers a single-task\n * refresh (`buildRefreshUrl`) and patches the parent list via `onItemUpdate`.\n *\n * Hydration: `expandedQuarters` starts `[]` and is populated in a client-only\n * effect (mirrors the hub) so SSR markup matches first client paint.\n */\n\nimport { useEffect, useRef, useState } from 'react';\nimport { RoadmapCard } from '../../chat/entity-cards/roadmap-card';\nimport { useRoadmapVoting, type UseRoadmapVotingOptions } from './use-roadmap-voting';\nimport { EmptyState } from '../../empty-state';\nimport {\n Accordion,\n AccordionItem,\n AccordionTrigger,\n AccordionContent,\n} from '../../ui';\nimport { cn } from '../../../utils/cn';\nimport { devSectionAnchorId } from '../../../utils/dev-sections/dev-section-param-keys';\nimport { contentFetch } from '../../../utils/embed-content-fetch';\nimport type { RoadmapItem } from '../../chat/types/entities/roadmap-item';\n\nconst DEFAULT_BUILD_REFRESH_URL = (taskId: string) => `/api/roadmap/${taskId}`;\nconst BACKLOG = 'Backlog';\n\n// ── Quarter helpers (pure; lifted from the hub roadmap section) ──────────────\n\n/** Status → sort priority (Complete → Working → Review → To-Do → other). */\nfunction getStatusPriority(status: string): number {\n const s = (status || '').toLowerCase();\n if (s.includes('complete') || s.includes('done')) return 1;\n if (s.includes('working') || s.includes('progress')) return 2;\n if (s.includes('review')) return 3;\n if (s.includes('to do') || s.includes('plan')) return 4;\n return 5;\n}\n\n/** Parse a `\"Q<n> <year>\"` label → {quarter, year}; `null` when unparseable. */\nfunction parseQuarterString(q: string): { quarter: number; year: number } | null {\n const match = q.match(/Q(\\d+)\\s+(\\d+)/);\n if (!match) return null;\n return { quarter: parseInt(match[1], 10), year: parseInt(match[2], 10) };\n}\n\nfunction compareQuarters(\n a: { quarter: number; year: number },\n b: { quarter: number; year: number },\n): number {\n if (a.year !== b.year) return a.year - b.year;\n return a.quarter - b.quarter;\n}\n\n/** Today's quarter — client-only (called from an effect, never during SSR). */\nfunction getCurrentQuarter(): { quarter: number; year: number } {\n const now = new Date();\n return { quarter: Math.floor(now.getMonth() / 3) + 1, year: now.getFullYear() };\n}\n\n/** Quarters open by default: current + future, plus the recent past within\n * `quartersToKeepClosed` of the current quarter; Backlog always open. */\nfunction computeDefaultExpandedQuarters(quarters: string[], quartersToKeepClosed: number): string[] {\n const currentQ = getCurrentQuarter();\n const out: string[] = [];\n for (const q of quarters) {\n if (q === BACKLOG) continue;\n const parsed = parseQuarterString(q);\n if (!parsed) continue;\n const diff = compareQuarters(parsed, currentQ);\n if (diff >= 0) {\n out.push(q);\n } else {\n const quartersAgo = currentQ.year * 4 + currentQ.quarter - (parsed.year * 4 + parsed.quarter);\n if (quartersAgo < quartersToKeepClosed) out.push(q);\n }\n }\n if (quarters.includes(BACKLOG)) out.push(BACKLOG);\n return out;\n}\n\nexport interface RoadmapGridProps {\n items: RoadmapItem[];\n onItemUpdate?: (updatedItem: RoadmapItem) => void;\n /** Show the desktop left margin (~120px) that aligns the grid with the page\n * hero. Default `true`. Related-content rails pass `false`. */\n showLeftMargin?: boolean;\n /** URL builder for the per-task refresh call after a successful vote. */\n buildRefreshUrl?: (taskId: string) => string;\n /** Voting hook options (vote endpoint + storage key). */\n votingOptions?: UseRoadmapVotingOptions;\n /** Group items into collapsible per-quarter accordions. Default `false`\n * (flat grid) so EXISTING callers — the hub's per-quarter RoadmapGrid calls\n * and related-content rails — stay unchanged. `RoadmapView` / full-page\n * callers pass `true` to get the shared quarter grouping. */\n groupByQuarter?: boolean;\n /** When true (search/filter active), every quarter expands so results aren't\n * hidden in collapsed sections. Threaded from `RoadmapView`. */\n hasActiveFilters?: boolean;\n /** Past quarters within this window of the current quarter stay open by\n * default; older ones collapse. Default `2`. */\n quartersToKeepClosed?: number;\n}\n\n/** Internal flat 2-col grid for ONE set of items. Voting comes from the parent\n * so the state is shared across quarters. */\nfunction RoadmapGridSingle({\n items,\n showLeftMargin,\n getVote,\n onVote,\n votingTasks,\n}: {\n items: RoadmapItem[];\n showLeftMargin: boolean;\n getVote: (taskId: string) => 'up' | 'down' | null;\n onVote: (taskId: string, voteType: 'up' | 'down') => void;\n votingTasks: Set<string>;\n}) {\n return (\n <div className={`grid grid-cols-1 md:grid-cols-2 gap-6 ${showLeftMargin ? 'md:ml-[120px]' : ''}`}>\n {items.map((item) => (\n // DOM id + sticky-header scroll offset live ON RoadmapCard's own\n // outer element (no wrapper div). Anchor mirrors\n // `buildDevSectionUrl('roadmap', <id>)` → `#roadmap-<external_id>`;\n // `useScrollToHash` in `roadmap-view.tsx` finds the card by id\n // and scrolls.\n <RoadmapCard\n key={item.id}\n item={item}\n id={devSectionAnchorId('roadmap', item.id)}\n userVote={getVote(item.id)}\n onVote={(voteType) => onVote(item.id, voteType)}\n isVoting={votingTasks.has(item.id)}\n />\n ))}\n </div>\n );\n}\n\nexport function RoadmapGrid({\n items,\n onItemUpdate,\n showLeftMargin = true,\n buildRefreshUrl = DEFAULT_BUILD_REFRESH_URL,\n votingOptions,\n groupByQuarter = false,\n hasActiveFilters = false,\n quartersToKeepClosed = 2,\n}: RoadmapGridProps) {\n // ── Voting (shared across all quarters) ──\n const { getVote, toggleVote } = useRoadmapVoting(votingOptions);\n const [votingTasks, setVotingTasks] = useState<Set<string>>(new Set());\n\n const handleVote = async (taskId: string, voteType: 'up' | 'down') => {\n if (votingTasks.has(taskId)) return;\n setVotingTasks((prev) => new Set(prev).add(taskId));\n try {\n const result = await toggleVote(taskId, voteType);\n if (result.success) {\n const response = await contentFetch(buildRefreshUrl(taskId));\n if (response.ok) {\n const data = await response.json();\n if (data.item && onItemUpdate) onItemUpdate(data.item);\n }\n }\n } finally {\n setVotingTasks((prev) => {\n const next = new Set(prev);\n next.delete(taskId);\n return next;\n });\n }\n };\n\n // ── Quarter bucketing + chronological sort (recomputed each render; cheap) ──\n const itemsByQuarter = items.reduce<Record<string, RoadmapItem[]>>((acc, item) => {\n const q = item.quarter || BACKLOG;\n (acc[q] ||= []).push(item);\n return acc;\n }, {});\n for (const q of Object.keys(itemsByQuarter)) {\n itemsByQuarter[q].sort((a, b) => getStatusPriority(a.status) - getStatusPriority(b.status));\n }\n const sortedQuarters = Object.keys(itemsByQuarter).sort((a, b) => {\n if (a === BACKLOG) return 1;\n if (b === BACKLOG) return -1;\n const aD = parseQuarterString(a) ?? { quarter: 0, year: 0 };\n const bD = parseQuarterString(b) ?? { quarter: 0, year: 0 };\n return compareQuarters(aD, bD);\n });\n const sortedQuartersKey = sortedQuarters.join(',');\n\n // ── Accordion expand state (hydration-safe: start [], populate in effects) ──\n const [expandedQuarters, setExpandedQuarters] = useState<string[]>([]);\n const [isInitialized, setIsInitialized] = useState(false);\n const hasSetInitialState = useRef(false);\n const prevItemsLength = useRef(0);\n\n // Initial expand state once data loads (runs once, or when data first arrives).\n useEffect(() => {\n const itemsJustLoaded = prevItemsLength.current === 0 && items.length > 0;\n prevItemsLength.current = items.length;\n if (sortedQuarters.length > 0 && (!hasSetInitialState.current || itemsJustLoaded)) {\n hasSetInitialState.current = true;\n setExpandedQuarters(\n hasActiveFilters\n ? [...sortedQuarters]\n : computeDefaultExpandedQuarters(sortedQuarters, quartersToKeepClosed),\n );\n setIsInitialized(true);\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [sortedQuarters.length, items.length]);\n\n // React to filter toggles AFTER init: filters on → expand all; off → defaults.\n useEffect(() => {\n if (!isInitialized || sortedQuarters.length === 0) return;\n setExpandedQuarters(\n hasActiveFilters\n ? [...sortedQuarters]\n : computeDefaultExpandedQuarters(sortedQuarters, quartersToKeepClosed),\n );\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [hasActiveFilters, sortedQuartersKey, isInitialized, quartersToKeepClosed]);\n\n if (items.length === 0) {\n return (\n <EmptyState\n type=\"generic\"\n title=\"No roadmap items\"\n description=\"Check back soon for upcoming features and improvements!\"\n />\n );\n }\n\n // Flat mode (related-content rails) — no accordion.\n if (!groupByQuarter) {\n return (\n <RoadmapGridSingle\n items={items}\n showLeftMargin={showLeftMargin}\n getVote={getVote}\n onVote={handleVote}\n votingTasks={votingTasks}\n />\n );\n }\n\n return (\n <Accordion\n type=\"multiple\"\n value={expandedQuarters}\n onValueChange={setExpandedQuarters}\n className=\"flex flex-col gap-10\"\n >\n {sortedQuarters.map((quarter) => {\n const itemCount = itemsByQuarter[quarter]?.length || 0;\n const isExpanded = expandedQuarters.includes(quarter);\n return (\n <AccordionItem\n key={quarter}\n value={quarter}\n id={`quarter-${quarter.replace(/\\s+/g, '-').toLowerCase()}`}\n className=\"border-0\"\n >\n <AccordionTrigger className=\"w-full p-0 hover:no-underline [&>svg]:h-5 [&>svg]:w-5 [&>svg]:text-ods-text-secondary [&>svg]:ml-auto [&>svg]:shrink-0\">\n <div className=\"flex items-center gap-3\">\n <h3\n className={cn(\n \"font-['Azeret_Mono'] font-semibold text-[24px] md:text-[28px] lg:text-[32px] leading-[32px] md:leading-[36px] lg:leading-[40px] text-ods-text-primary tracking-[-0.48px] md:tracking-[-0.56px] lg:tracking-[-0.64px] transition-opacity\",\n isExpanded ? 'opacity-100' : 'opacity-60',\n )}\n >\n {quarter}\n <span className=\"text-ods-accent\">:</span>\n </h3>\n <span\n className={cn(\n 'text-sm font-medium transition-opacity',\n isExpanded ? 'text-ods-text-secondary opacity-100' : 'text-ods-text-tertiary opacity-60',\n )}\n >\n {itemCount} {itemCount === 1 ? 'item' : 'items'}\n {isInitialized && !isExpanded && <span className=\"ml-2 text-ods-accent\">Click to expand</span>}\n </span>\n </div>\n </AccordionTrigger>\n <AccordionContent className=\"pt-4 pb-0 overflow-hidden data-[state=closed]:animate-none data-[state=open]:animate-none\">\n <RoadmapGridSingle\n items={itemsByQuarter[quarter]}\n showLeftMargin={showLeftMargin}\n getVote={getVote}\n onVote={handleVote}\n votingTasks={votingTasks}\n />\n </AccordionContent>\n </AccordionItem>\n );\n })}\n </Accordion>\n );\n}\n","'use client';\n\n/**\n * useRoadmapVoting — localStorage-backed optimistic voting for roadmap cards.\n *\n * One vote per task per user (storage key scoped per `storageKey` option,\n * default `'roadmap_votes_v1'`). Toggling the same vote removes it;\n * switching directions sends a remove + add pair so the server's running\n * totals stay correct.\n *\n * Endpoint configuration — `voteApiEndpoint`:\n * The hook posts to ONE endpoint (default `/api/roadmap/vote`) for\n * BOTH the optimistic add AND the opposite-vote remove. Reverse-proxy\n * embedders override this with their proxied path; lib otherwise\n * matches the hub's pre-migration call shape.\n */\n\nimport { useState, useEffect, useCallback } from 'react';\nimport { contentFetch } from '../../../utils/embed-content-fetch';\n\nexport type VoteType = 'up' | 'down' | null;\n\nexport interface VoteState {\n [taskId: string]: VoteType;\n}\n\nexport interface UseRoadmapVotingOptions {\n /** Vote endpoint URL. Default `/api/roadmap/vote`. */\n voteApiEndpoint?: string;\n /** localStorage key. Default `'roadmap_votes_v1'`. Embedders mounting\n * multiple roadmap surfaces in the same origin can scope per-surface\n * (e.g. `'roadmap_votes_v1_main'` vs `'roadmap_votes_v1_admin'`) so\n * votes don't cross-contaminate. */\n storageKey?: string;\n}\n\nconst DEFAULT_VOTE_ENDPOINT = '/api/roadmap/vote';\nconst DEFAULT_STORAGE_KEY = 'roadmap_votes_v1';\n\nexport function useRoadmapVoting(options: UseRoadmapVotingOptions = {}) {\n const voteApiEndpoint = options.voteApiEndpoint ?? DEFAULT_VOTE_ENDPOINT;\n const storageKey = options.storageKey ?? DEFAULT_STORAGE_KEY;\n\n const [votes, setVotes] = useState<VoteState>({});\n const [isLoading, setIsLoading] = useState(true);\n\n // Load votes from localStorage. Runs on mount AND whenever `storageKey`\n // changes — when the key changes mid-lifecycle (e.g. an embedder\n // remounts with a new namespace), we MUST reset state first so the\n // save-effect below doesn't write the old key's data into the new\n // key. We also re-enter the loading phase so the load completes\n // before any save runs.\n useEffect(() => {\n setIsLoading(true);\n setVotes({});\n try {\n const stored = localStorage.getItem(storageKey);\n if (stored) {\n setVotes(JSON.parse(stored));\n }\n } catch (error) {\n console.error('[Voting] Error loading votes from localStorage:', error);\n } finally {\n setIsLoading(false);\n }\n }, [storageKey]);\n\n // Save votes to localStorage whenever they change\n useEffect(() => {\n if (!isLoading) {\n try {\n localStorage.setItem(storageKey, JSON.stringify(votes));\n } catch (error) {\n console.error('[Voting] Error saving votes to localStorage:', error);\n }\n }\n }, [votes, isLoading, storageKey]);\n\n const getVote = useCallback(\n (taskId: string): VoteType => {\n return votes[taskId] || null;\n },\n [votes]\n );\n\n const toggleVote = useCallback(\n async (\n taskId: string,\n voteType: 'up' | 'down'\n ): Promise<{ success: boolean; newVote: VoteType; action: 'add' | 'remove' }> => {\n const currentVote = votes[taskId];\n\n let newVote: VoteType = null;\n let action: 'add' | 'remove' = 'add';\n\n if (currentVote === voteType) {\n // User clicked same vote - remove it\n newVote = null;\n action = 'remove';\n } else {\n // User clicked different vote - set it. If they had an opposite\n // vote, remove that first so the server totals stay consistent.\n if (currentVote) {\n await contentFetch(voteApiEndpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n taskId,\n voteType: currentVote,\n action: 'remove',\n }),\n }).catch(err => console.error('[Voting] Error removing opposite vote:', err));\n }\n\n newVote = voteType;\n action = 'add';\n }\n\n // Optimistic update\n setVotes(prev => ({\n ...prev,\n [taskId]: newVote,\n }));\n\n try {\n const response = await contentFetch(voteApiEndpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ taskId, voteType, action }),\n });\n\n if (!response.ok) {\n throw new Error('Vote API request failed');\n }\n\n return { success: true, newVote, action };\n } catch (error) {\n console.error('[Voting] API error:', error);\n\n // Revert optimistic update on error\n setVotes(prev => ({\n ...prev,\n [taskId]: currentVote,\n }));\n\n return { success: false, newVote: currentVote, action };\n }\n },\n [votes, voteApiEndpoint]\n );\n\n const clearVotes = useCallback(() => {\n setVotes({});\n localStorage.removeItem(storageKey);\n }, [storageKey]);\n\n return {\n votes,\n isLoading,\n getVote,\n toggleVote,\n clearVotes,\n };\n}\n","/**\n * RoadmapGridSkeleton — loading state for the `/roadmap` grid view.\n *\n * Pure JSX (no hooks, no events) — `'use client'` not strictly required\n * here; tsup's client-entry banner injects it automatically when this\n * file is bundled into the client output. We match the playbook's\n * skeleton-file convention (no directive when no hooks).\n *\n * NOTE: lib's `chat/entity-cards/roadmap-card.tsx` also exports a\n * `RoadmapCardSkeleton` — that one is the COMPACT 56px chat-card\n * variant. This file's internal card-skeleton (340px grid card)\n * intentionally stays file-internal to avoid the naming collision;\n * only `RoadmapGridSkeleton` is exported.\n */\n\nfunction RoadmapCardSkeleton() {\n return (\n <div className=\"bg-ods-card border border-ods-border rounded-[6px] p-[24px] flex flex-col gap-[16px] min-h-[340px] relative\">\n {/* Status Badge Skeleton - Top Right */}\n <div className=\"absolute top-[24px] right-[24px]\">\n <div className=\"h-[20px] w-[80px] bg-ods-border rounded animate-pulse\"></div>\n </div>\n\n {/* Icon and title skeleton */}\n <div className=\"flex items-center gap-[16px] pr-[120px]\">\n <div className=\"w-[80px] h-[80px] bg-ods-border rounded-lg flex-shrink-0 animate-pulse\"></div>\n <div className=\"flex-1 min-w-0 flex flex-col gap-1\">\n <div className=\"min-h-[48px] flex items-center\">\n <div className=\"h-[24px] w-full bg-ods-border rounded animate-pulse\"></div>\n </div>\n <div className=\"min-h-[20px] flex items-center\">\n <div className=\"h-[14px] w-1/2 bg-ods-border rounded animate-pulse\"></div>\n </div>\n </div>\n </div>\n\n {/* Description skeleton - exactly 3 lines */}\n <div className=\"min-h-[72px] flex items-center\">\n <div className=\"w-full space-y-2\">\n <div className=\"h-[24px] bg-ods-border rounded animate-pulse\"></div>\n <div className=\"h-[24px] bg-ods-border rounded animate-pulse\"></div>\n <div className=\"h-[24px] w-4/5 bg-ods-border rounded animate-pulse\"></div>\n </div>\n </div>\n\n <div className=\"flex-1\"></div>\n\n {/* Bottom skeleton */}\n <div className=\"flex items-center justify-between\">\n <div className=\"h-[48px] w-[120px] bg-ods-border rounded animate-pulse\"></div>\n <div className=\"h-[32px] w-[100px] bg-ods-border rounded animate-pulse\"></div>\n </div>\n </div>\n );\n}\n\nexport interface RoadmapGridSkeletonProps {\n /** Number of skeleton cards to show. Default 4. */\n count?: number;\n /** Show the desktop left margin (~120px) that aligns the grid with\n * the page hero's title block. Default `true`. Related-content rails\n * inside narrower surfaces (e.g. the release detail page) pass `false`. */\n showLeftMargin?: boolean;\n}\n\nexport function RoadmapGridSkeleton({ count = 4, showLeftMargin = true }: RoadmapGridSkeletonProps) {\n return (\n <div className={`grid grid-cols-1 md:grid-cols-2 gap-6 ${showLeftMargin ? 'md:ml-[120px]' : ''}`}>\n {Array.from({ length: count }).map((_, i) => (\n <RoadmapCardSkeleton key={i} />\n ))}\n </div>\n );\n}\n","'use client'\n\n/**\n * `<RoadmapView />` — the SELF-CONTAINED roadmap LIST surface.\n *\n * Fetches the roadmap list via the shared `useSelfFetch` hook and renders the\n * pure controlled `<RoadmapGrid>` (kept controlled so related-content rails can\n * still pass `items`). The host configures only **api routes**: the list\n * `endpoint` (default `/api/roadmap`), the per-task `buildRefreshUrl`, and the\n * vote endpoint via `votingOptions`. Optional `initialItems` hydrates SSR.\n */\n\nimport { useMemo } from 'react'\n\nimport { useSearchParams } from '../../../embed-shims'\nimport { LoadError } from '../../ui/error-state'\nimport { useSelfFetch } from '../../../hooks/use-self-fetch'\nimport { useScrollToHash } from '../../../hooks/use-scroll-to-hash'\nimport type { RoadmapItem } from '../../chat/types/entities/roadmap-item'\nimport { RoadmapGrid } from './roadmap-grid'\nimport { RoadmapGridSkeleton } from './roadmap-grid-skeleton'\nimport type { UseRoadmapVotingOptions } from './use-roadmap-voting'\nimport { DEV_SECTION_PARAM_KEYS } from '../../../utils/dev-sections/dev-section-param-keys'\nimport { STICKY_HEADER_OFFSET_PX } from '../../../utils/same-page-hash-nav'\n\nconst DEFAULT_ENDPOINT = '/api/roadmap'\n// Defaults sourced from the ONE param-key registry the chrome (OPENFRAME_DEV_SECTIONS) also\n// reads, so the chrome's written `?key=` and this view's read can't silently diverge.\nconst DEFAULT_SEARCH_PARAM_KEY = DEV_SECTION_PARAM_KEYS.search\nconst DEFAULT_STATUS_PARAM_KEY = DEV_SECTION_PARAM_KEYS.status\n\nexport interface RoadmapViewProps {\n /** GET list endpoint (the api route). Returns `{ items }`. Default\n * `/api/roadmap`. */\n endpoint?: string\n /** Optional SSR hydrate — skips the initial client fetch. */\n initialItems?: RoadmapItem[]\n showLeftMargin?: boolean\n /** Per-task refresh URL builder (after a vote). Default `/api/roadmap/<id>`. */\n buildRefreshUrl?: (taskId: string) => string\n /** Voting hook options (vote endpoint + storage key). */\n votingOptions?: UseRoadmapVotingOptions\n /** URL param key for the search input — MUST match the section chrome\n * (`DevSectionView`) that writes it. Default `'search'`. */\n searchParamKey?: string\n /** URL param key for the status filter. Default `'status'` (the roadmap\n * section's `filter.paramKey`). `'all'` means no filter. */\n statusParamKey?: string\n}\n\nexport function RoadmapView({\n endpoint = DEFAULT_ENDPOINT,\n initialItems,\n showLeftMargin,\n buildRefreshUrl,\n votingOptions,\n searchParamKey = DEFAULT_SEARCH_PARAM_KEY,\n statusParamKey = DEFAULT_STATUS_PARAM_KEY,\n}: RoadmapViewProps = {}) {\n // Read the search + status params the section chrome (`DevSectionView`) writes\n // and fold them INTO the fetch url so the url IS the cache key — the list\n // refetches filtered whenever the controls change. Mirrors `ProductReleasesView`.\n const searchParams = useSearchParams()\n const search = searchParams.get(searchParamKey) || ''\n const status = searchParams.get(statusParamKey) || 'all'\n const listParams = new URLSearchParams()\n if (search) listParams.set(searchParamKey, search)\n if (status && status !== 'all') listParams.set(statusParamKey, status)\n const qs = listParams.toString()\n const url = qs ? `${endpoint}?${qs}` : endpoint\n\n // Memoize so the SSR `initialItems` wrapper keeps a STABLE identity — else the\n // hook's initialData re-sync effect fires every render and clobbers the\n // optimistic vote patch below.\n const initialData = useMemo(() => (initialItems ? { items: initialItems } : undefined), [initialItems])\n const { data, setData, isLoading, error, reload } = useSelfFetch<{ items?: RoadmapItem[] }>(\n url,\n { initialData },\n )\n const items = data?.items ?? []\n\n // Deep-link hash dispatch — `?search=<id>#roadmap-<id>` from a chat card.\n // Shared hook owns the poll-until-mount + hashchange-listener wiring\n // (same instance used by DeliveryLists). The rAF poll inside the hook\n // handles the Radix `<AccordionItem>` lazy-unmount: on first paint\n // every quarter is collapsed; an effect in `roadmap-grid.tsx` expands\n // them when `hasActiveFilters` is true (chat URL carries `?search=<id>`).\n // The card mounts one tick after `data` lands; the hook waits.\n useScrollToHash(data, { headerOffset: STICKY_HEADER_OFFSET_PX })\n\n if (error) {\n return <LoadError message=\"Failed to load roadmap.\" onRetry={reload} />\n }\n // Skeleton only while the FIRST fetch is in flight (no data yet) — a malformed\n // body lacking `items` renders the grid (empty), never a stuck skeleton.\n if (isLoading && !data) {\n return <RoadmapGridSkeleton showLeftMargin={showLeftMargin} />\n }\n\n return (\n <RoadmapGrid\n items={items}\n showLeftMargin={showLeftMargin}\n buildRefreshUrl={buildRefreshUrl}\n votingOptions={votingOptions}\n // Full-page roadmap → collapsible quarter grouping (the shared RoadmapGrid\n // capability; flat grids stay the RoadmapGrid default). When the chrome's\n // search / status filter is active, expand every quarter so matches aren't hidden.\n groupByQuarter\n hasActiveFilters={search !== '' || status !== 'all'}\n // After a vote refreshes a single task, patch it into the fetched list so\n // the displayed counts stay live.\n onItemUpdate={(updated) =>\n setData((prev) =>\n prev\n ? { ...prev, items: (prev.items ?? []).map((it) => (it.id === updated.id ? updated : it)) }\n : prev,\n )\n }\n />\n )\n}\n","'use client';\n\n/**\n * DeliveryLists — the delivery section body (two tables: recently\n * completed + active). Reads `search` and `task_type` URL params\n * written by the shared `<DevSectionView>` chrome and refetches on\n * change.\n *\n * Endpoint configuration:\n * - `completedApiEndpoint` / `inProgressApiEndpoint` are the two\n * per-bucket GET endpoints. Defaults match the hub's pre-migration\n * routes (`/api/delivery/completed`, `/api/delivery/in-progress`).\n *\n * Coupling constraint — `searchParamKey` / `taskTypeParamKey`:\n * These props serve TWO purposes:\n * 1. URL READS — keys this component reads via `useSearchParams()`.\n * MUST match the consuming chrome's `section.search.paramKey` /\n * `section.filter.paramKey` (the chrome WRITES the URL params).\n * 2. API WRITES — keys this component sends as query params on the\n * outbound fetch to `{completedApiEndpoint,inProgressApiEndpoint}`.\n * The hub API contract uses `'search'` / `'task_type'`; embedders\n * reverse-proxying those routes must preserve the same names OR\n * rewrite the inbound query string on the proxy side.\n *\n * Defaults align with `OPENFRAME_DEV_SECTIONS.delivery.{search.paramKey,filter.paramKey}`\n * AND the hub API contract, so the OpenFrame zero-config case \"just\n * works\". Custom chrome overriding the param keys must override BOTH\n * ends consistently AND ensure the backend reads the same names.\n */\n\nimport { useEffect, useState } from 'react';\nimport { useSearchParams, useRouter, usePathname } from '../../../embed-shims';\nimport type { DeliveryResponse } from '../../../types/delivery';\nimport { DeliveryTable } from './delivery-table';\nimport { EmptyState } from '../../empty-state';\nimport { LoadError } from '../../ui/error-state';\nimport { DEV_SECTION_PARAM_KEYS } from '../../../utils/dev-sections/dev-section-param-keys';\nimport { STICKY_HEADER_OFFSET_PX } from '../../../utils/same-page-hash-nav';\nimport { contentFetch } from '../../../utils/embed-content-fetch';\nimport { useScrollToHash } from '../../../hooks/use-scroll-to-hash';\n\nconst DEFAULT_COMPLETED_ENDPOINT = '/api/delivery/completed';\nconst DEFAULT_IN_PROGRESS_ENDPOINT = '/api/delivery/in-progress';\n// Param keys sourced from the shared registry (see RoadmapView) — single source for the\n// chrome's written `?key=` and this view's read.\nconst DEFAULT_SEARCH_PARAM_KEY = DEV_SECTION_PARAM_KEYS.search;\nconst DEFAULT_TASK_TYPE_PARAM_KEY = DEV_SECTION_PARAM_KEYS.deliveryTaskType;\n\nexport interface DeliveryListsProps {\n /** GET endpoint for the \"Recently Completed\" bucket. Default\n * `/api/delivery/completed`. */\n completedApiEndpoint?: string;\n /** GET endpoint for the \"Active Tasks\" bucket. Default\n * `/api/delivery/in-progress`. */\n inProgressApiEndpoint?: string;\n /** URL param key for the search input. MUST match the consuming\n * chrome's `section.search.paramKey`. Default `'search'`. */\n searchParamKey?: string;\n /** URL param key for the task-type filter. MUST match the consuming\n * chrome's `section.filter.paramKey`. Default `'task_type'`. */\n taskTypeParamKey?: string;\n}\n\nexport function DeliveryLists({\n completedApiEndpoint = DEFAULT_COMPLETED_ENDPOINT,\n inProgressApiEndpoint = DEFAULT_IN_PROGRESS_ENDPOINT,\n searchParamKey = DEFAULT_SEARCH_PARAM_KEY,\n taskTypeParamKey = DEFAULT_TASK_TYPE_PARAM_KEY,\n}: DeliveryListsProps = {}) {\n const searchParams = useSearchParams();\n const router = useRouter();\n const pathname = usePathname();\n\n const [data, setData] = useState<DeliveryResponse | null>(null);\n const [isLoading, setIsLoading] = useState(true);\n const [error, setError] = useState<string | null>(null);\n\n // Get filter state from URL\n const searchQuery = searchParams.get(searchParamKey) || '';\n const taskTypeFilter = searchParams.get(taskTypeParamKey) || 'all';\n\n useEffect(() => {\n async function fetchDeliveryData() {\n try {\n setIsLoading(true);\n setError(null);\n\n // Build query parameters for filtering. The outbound key names\n // mirror the inbound URL-param keys — see \"Coupling constraint\"\n // in the file docblock for why.\n const params = new URLSearchParams();\n if (searchQuery) {\n params.set(searchParamKey, searchQuery);\n }\n if (taskTypeFilter && taskTypeFilter !== 'all') {\n params.set(taskTypeParamKey, taskTypeFilter);\n }\n const queryString = params.toString();\n const queryParam = queryString ? `?${queryString}` : '';\n\n // Fetch completed and in-progress tasks separately with filters\n const [completedResponse, inProgressResponse] = await Promise.all([\n contentFetch(`${completedApiEndpoint}${queryParam}`),\n contentFetch(`${inProgressApiEndpoint}${queryParam}`),\n ]);\n\n if (!completedResponse.ok || !inProgressResponse.ok) {\n throw new Error('Failed to fetch delivery items');\n }\n\n const [completedResult, inProgressResult] = await Promise.all([\n completedResponse.json(),\n inProgressResponse.json(),\n ]);\n\n setData({\n completed: completedResult.items || [],\n inProgress: inProgressResult.items || [],\n });\n } catch (err) {\n console.error('Error fetching delivery items:', err);\n setError('Failed to load delivery items. Please try again later.');\n } finally {\n setIsLoading(false);\n }\n }\n\n fetchDeliveryData();\n }, [searchQuery, taskTypeFilter, completedApiEndpoint, inProgressApiEndpoint, searchParamKey, taskTypeParamKey]);\n\n const filteredCompleted = data?.completed || [];\n const filteredInProgress = data?.inProgress || [];\n\n // Deep-link hash dispatch — `?search=<id>#delivery-<id>` from a chat\n // card or a linked-delivery card on a ticket. Shared hook owns the\n // poll-until-mount + hashchange-listener wiring (same instance used\n // by RoadmapView). 96 matches the sticky-header offset every\n // hash-scroll surface in the app uses.\n useScrollToHash(data, { headerOffset: STICKY_HEADER_OFFSET_PX });\n\n const showCompleted = true;\n const showInProgress = true;\n\n const hasActiveFilters = searchQuery !== '' || taskTypeFilter !== 'all';\n const hasResults = (showCompleted && filteredCompleted.length > 0) || (showInProgress && filteredInProgress.length > 0);\n\n // Error state — consume lib's canonical LoadError so ODS tokens +\n // retry affordance stay in lockstep with every other surface.\n if (error) {\n return (\n <div className=\"w-full\">\n <LoadError message={error} onRetry={() => window.location.reload()} />\n </div>\n );\n }\n\n return (\n <div className=\"w-full flex flex-col gap-[40px]\">\n {/* Empty state if no results after filtering */}\n {!isLoading && !hasResults && (\n hasActiveFilters ? (\n <EmptyState\n type=\"search\"\n title=\"No tasks found\"\n description=\"No tasks match your current filters. Try adjusting your search or status filter.\"\n showCTA={true}\n ctaText=\"Reset Filters\"\n onCtaClick={() => {\n const params = new URLSearchParams(searchParams.toString());\n params.delete(searchParamKey);\n params.delete(taskTypeParamKey);\n router.replace(`${pathname}?${params.toString()}`, { scroll: false });\n }}\n />\n ) : (\n <EmptyState\n type=\"generic\"\n title=\"No tasks available\"\n description=\"Check back soon for upcoming tasks!\"\n showCTA={false}\n />\n )\n )}\n\n {/* Completed Tasks Table */}\n {showCompleted && (hasResults || isLoading) && (\n <div className=\"w-full\">\n <h3 className=\"text-h2 text-ods-text-primary tracking-[-0.48px] md:tracking-[-0.56px] lg:tracking-[-0.64px] mb-4\">\n Recently Completed<span className=\"text-ods-accent\">:</span>\n </h3>\n <DeliveryTable\n items={filteredCompleted}\n isLoading={isLoading}\n />\n </div>\n )}\n\n {/* In Progress Tasks Table */}\n {showInProgress && (hasResults || isLoading) && (\n <div className=\"w-full\">\n <h3 className=\"text-h2 text-ods-text-primary tracking-[-0.48px] md:tracking-[-0.56px] lg:tracking-[-0.64px] mb-4\">\n Active Tasks<span className=\"text-ods-accent\">:</span>\n </h3>\n <DeliveryTable\n items={filteredInProgress}\n isLoading={isLoading}\n />\n </div>\n )}\n </div>\n );\n}\n","'use client';\n\n/**\n * DeliveryTable — bordered card containing one `<DeliveryRow />` per\n * item. Visual rendering of each row lives in `delivery-row.tsx` so the\n * exact same primitive can be composed elsewhere (notably the linked-\n * delivery surface inside `<TicketDetailDrawer>`).\n *\n * Props:\n * - `items` — flat list of `DeliveryItem`. Two buckets (completed +\n * in-progress) are rendered as two separate `DeliveryTable`s by\n * the parent `DeliveryLists`.\n * - `isLoading` — skeleton rows.\n * - `focusId` — `?focus=<id>` URL param. Marks the matching row\n * `id=\"delivery-<id>\"` and applies the highlight ring so the\n * deep-link from a ticket's linked-card scrolls + flashes the\n * right row.\n */\n\nimport { DeliveryRow } from './delivery-row';\nimport type { DeliveryItem } from '../../../types/delivery';\nimport { devSectionAnchorId } from '../../../utils/dev-sections/dev-section-param-keys';\n\ninterface DeliveryTableProps {\n items: DeliveryItem[];\n isLoading?: boolean;\n}\n\n/**\n * Skeleton loader for rows - matching responsive structure\n */\nfunction SkeletonRow() {\n return (\n <div className=\"border-b border-ods-border last:border-b-0 p-[12px] md:p-[16px]\">\n <div className=\"flex flex-col md:flex-row items-start justify-between gap-[12px] md:gap-[16px] w-full\">\n {/* Left: Title, subtitle, and description skeleton */}\n <div className=\"flex-1 min-w-0 w-full md:w-auto flex flex-col gap-[12px] md:gap-[16px]\">\n {/* Title skeleton - responsive */}\n <div className=\"min-h-[24px] flex items-center\">\n <div className=\"h-[20px] bg-ods-border rounded animate-pulse w-full\"></div>\n </div>\n {/* Subtitle skeleton - 1 line */}\n <div className=\"min-h-[20px] flex items-center\">\n <div className=\"h-[20px] bg-ods-border rounded animate-pulse w-1/2\"></div>\n </div>\n {/* Description skeleton - 3 lines */}\n <div className=\"min-h-[72px] flex items-center\">\n <div className=\"flex-1 space-y-1\">\n <div className=\"h-[20px] bg-ods-border rounded animate-pulse w-full\"></div>\n <div className=\"h-[20px] bg-ods-border rounded animate-pulse w-full\"></div>\n <div className=\"h-[20px] bg-ods-border rounded animate-pulse w-2/3\"></div>\n </div>\n </div>\n </div>\n\n {/* Right: Badge skeleton - two stacked badges */}\n <div className=\"flex-shrink-0 self-start flex flex-col gap-2\">\n <div className=\"h-[32px] w-[100px] bg-ods-border rounded animate-pulse\"></div>\n <div className=\"h-[32px] w-[120px] bg-ods-border rounded animate-pulse\"></div>\n </div>\n </div>\n </div>\n );\n}\n\n/**\n * DeliveryTable Component\n * Displays bug fixes and enhancements with fixed-height rows\n */\nexport function DeliveryTable({ items, isLoading = false }: DeliveryTableProps) {\n // Show skeletons while loading\n if (isLoading) {\n return (\n <div className=\"bg-ods-card border border-ods-border rounded-[6px] overflow-hidden w-full\">\n <div className=\"w-full\">\n {[1, 2, 3, 4, 5].map((i) => (\n <SkeletonRow key={i} />\n ))}\n </div>\n </div>\n );\n }\n\n // Empty state\n if (items.length === 0) {\n return (\n <div className=\"bg-ods-card border border-ods-border rounded-[6px] p-[40px] text-center w-full\">\n <p className=\"text-ods-text-secondary text-[14px] font-['DM_Sans'] font-medium\">\n No tasks available\n </p>\n </div>\n );\n }\n\n return (\n <div className=\"bg-ods-card border border-ods-border rounded-[6px] overflow-hidden w-full\">\n <div className=\"w-full\">\n {items.map((item) => (\n // DOM id lives on DeliveryRow's own outer element (no wrapper\n // div). Anchor mirrors `buildDevSectionUrl('delivery', <id>)`\n // → `#delivery-<external_id>`; `useScrollToHash` in\n // `delivery-lists.tsx` finds the row by id and scrolls. The\n // outer wrapper here ONLY exists for the row separators.\n <div\n key={item.id}\n className=\"border-b border-ods-border last:border-b-0\"\n >\n <DeliveryRow item={item} id={devSectionAnchorId('delivery', item.id)} />\n </div>\n ))}\n </div>\n </div>\n );\n}\n","'use client';\n\n/**\n * LegalDocumentPage — unified UI for privacy-policy, terms-of-service,\n * and any other markdown-backed legal document.\n *\n * Replaces two near-identical hub components (`PrivacyPolicyPage` +\n * `TermsOfServicePage`) that differed only in title, contact email,\n * and copy strings. Caller passes those as props.\n *\n * Markdown rendering: defaults to lib's `SimpleMarkdownRenderer`\n * (sufficient for plain-markdown legal docs). Embedders that need\n * richer markdown (embeds, video, OG previews) pass their own via\n * the `MarkdownRenderer` prop — same injection pattern as\n * `ReleaseDetailPage`.\n *\n * Endpoint configuration: forwarded to `useLegalDocs(docType, { apiEndpoint })`.\n */\n\nimport type { ComponentType } from 'react';\nimport { PageShell, PageLayout, PageHeading } from '../../ui';\nimport { RichMarkdownRenderer } from '../../ui/rich-markdown-renderer';\nimport { useRouter } from '../../../embed-shims/next-navigation';\nimport { useLegalDocs, type LegalDocument } from './use-legal-docs';\nimport { formatLegalDate } from '../../../utils/format';\n\nexport interface LegalDocumentMarkdownRendererProps {\n content: string;\n sectionIds?: Array<{ id: string; title: string; level: number }>;\n demoteMarkdownH1ToH2?: boolean;\n}\n\nexport interface LegalDocumentPageProps {\n /** Document type identifier — drives the default API endpoint\n * `/api/legal/<docType>` AND the error-log prefix. Common values:\n * `'privacy'`, `'terms'`. Embedders may use any string. */\n docType: string;\n /** Heading text (e.g. \"Privacy Policy\", \"Terms of Service\"). */\n title: string;\n /** Fallback subtitle shown when no `lastUpdated` date is available\n * (e.g. \"Our privacy policy and data protection practices\"). */\n fallbackDescription: string;\n /** Email shown in the error + empty-state copy\n * (e.g. `'privacy@openframe.io'`, `'legal@openframe.io'`). */\n contactEmail: string;\n /** Prompt shown above the contact link in the error state\n * (e.g. \"For privacy-related questions, please contact:\"). */\n errorContactPrompt: string;\n /** Title for the error block (e.g. \"Unable to load privacy policy\"). */\n errorTitle: string;\n /** Sentence shown when the API returns no document\n * (e.g. \"Privacy policy content is not available at this time.\"). */\n emptyStateMessage: string;\n /** SSR-prepared document, if available. */\n initialData?: LegalDocument | null;\n /** SSR-prepared formatted \"Last Updated\" label. Stable across hydration. */\n initialLastUpdatedLabel?: string | null;\n /** Override the default `/api/legal/<docType>` endpoint\n * (reverse-proxy embedders, alternate API paths). */\n apiEndpoint?: string;\n /** Override the default markdown renderer. */\n MarkdownRenderer?: ComponentType<LegalDocumentMarkdownRendererProps>;\n /** Back-button config — same pattern as `DevSectionPage`. Pass `false`\n * to hide. Default `{ label: 'Back to home', href: '/' }`. */\n backButton?: { label?: string; href?: string } | false;\n}\n\nexport function LegalDocumentPage({\n docType,\n title,\n fallbackDescription,\n contactEmail,\n errorContactPrompt,\n errorTitle,\n emptyStateMessage,\n initialData = null,\n initialLastUpdatedLabel = null,\n apiEndpoint,\n MarkdownRenderer = RichMarkdownRenderer,\n backButton,\n}: LegalDocumentPageProps) {\n const router = useRouter();\n const { data, isLoading, error } = useLegalDocs(docType, { initialData, apiEndpoint });\n\n // Back-button config — mirrors DevSectionPage's `{ label: 'Back to home',\n // onClick: () => router.push('/') }`. Hide entirely when caller passes\n // `false` (e.g. embed-mode where the host owns navigation chrome).\n const backCfg =\n backButton === false\n ? undefined\n : {\n label: backButton?.label ?? 'Back to home',\n onClick: () => router.push(backButton?.href ?? '/'),\n };\n\n const fallbackLastUpdatedLabel =\n data?.lastSynced != null ? formatLegalDate(data.lastSynced) : null;\n const effectiveLastUpdatedLabel = initialLastUpdatedLabel ?? fallbackLastUpdatedLabel;\n\n // Title with accent-colon trailing dot — matches knowledge-hub typography\n const customTitle = (\n <div className=\"flex flex-col gap-4\">\n <PageHeading>\n <span>{title}</span>\n <span className=\"text-ods-accent\">.</span>\n </PageHeading>\n <p className=\"font-['DM_Sans'] text-base md:text-lg text-ods-text-secondary max-w-2xl\">\n {effectiveLastUpdatedLabel ? `Last Updated: ${effectiveLastUpdatedLabel}` : fallbackDescription}\n {data?.sourceFile && (\n <span className=\"block text-sm mt-1 opacity-75\">Source: {data.sourceFile}</span>\n )}\n </p>\n </div>\n );\n\n return (\n <PageShell>\n <PageLayout backButton={backCfg}>\n <div className=\"flex flex-col gap-4\">{customTitle}</div>\n\n <div className=\"flex flex-col lg:flex-row gap-6 lg:gap-10 items-start flex-1\">\n <div className=\"flex-1\">\n <div className=\"w-full\">\n <article className=\"space-y-2\">\n {isLoading ? (\n // Loading skeleton matching Knowledge Hub pattern\n <div className=\"space-y-6\">\n <div className=\"h-10 bg-ods-skeleton rounded-lg w-3/4 animate-pulse\"></div>\n <div className=\"space-y-4\">\n <div className=\"h-4 bg-ods-skeleton rounded w-full animate-pulse\"></div>\n <div className=\"h-4 bg-ods-skeleton rounded w-full animate-pulse\"></div>\n <div className=\"h-4 bg-ods-skeleton rounded w-5/6 animate-pulse\"></div>\n </div>\n <div className=\"h-32 bg-ods-card border border-ods-border rounded-lg animate-pulse\"></div>\n <div className=\"space-y-4\">\n <div className=\"h-4 bg-ods-skeleton rounded w-full animate-pulse\"></div>\n <div className=\"h-4 bg-ods-skeleton rounded w-4/5 animate-pulse\"></div>\n </div>\n </div>\n ) : error ? (\n <div className=\"text-center space-y-4\">\n <div className=\"bg-red-900/20 border border-red-700 rounded-lg p-6\">\n <p className=\"text-red-400 mb-2\">{errorTitle}</p>\n <p className=\"text-red-300 text-sm\">{error}</p>\n </div>\n <div className=\"text-ods-text-secondary\">\n <p>{errorContactPrompt}</p>\n <a href={`mailto:${contactEmail}`} className=\"text-ods-accent hover:underline\">\n {contactEmail}\n </a>\n </div>\n </div>\n ) : data ? (\n <MarkdownRenderer\n content={data.content}\n sectionIds={data.sections || []}\n demoteMarkdownH1ToH2\n />\n ) : (\n <div className=\"text-center text-ods-text-secondary py-16\">\n <p className=\"text-xl\">{emptyStateMessage}</p>\n <p className=\"mt-2\">\n Please contact{' '}\n <a href={`mailto:${contactEmail}`} className=\"text-ods-accent hover:underline\">\n {contactEmail}\n </a>{' '}\n for more information.\n </p>\n </div>\n )}\n </article>\n </div>\n </div>\n </div>\n </PageLayout>\n </PageShell>\n );\n}\n","'use client';\n\n/**\n * useLegalDocs — fetches a legal document (privacy policy, terms of\n * service, or any other markdown-backed legal page) from a hub API.\n *\n * Endpoint configuration — `apiEndpoint`:\n * Default `/api/legal/<docType>`. Reverse-proxy embedders override\n * with their proxied path (e.g. `/proxy/legal/privacy`).\n *\n * Data shape mirrors the hub's `lib/data/legal-utils.ts:LegalDocument`\n * server type. The hook intentionally re-declares the type here so\n * lib consumers don't need to import a server-side type.\n */\n\nimport { useState, useEffect, useCallback } from 'react';\nimport { contentFetch } from '../../../utils/embed-content-fetch';\n\nexport interface LegalDocument {\n title: string;\n content: string;\n sourceFile: string;\n lastSynced: string | null;\n githubSha: string | null;\n sections: Array<{ id: string; title: string; level: number }>;\n docType: string;\n meta: {\n sectionsCount: number;\n contentLength: number;\n lastSyncedAgo: string;\n };\n}\n\nexport interface UseLegalDocsReturn {\n data: LegalDocument | null;\n isLoading: boolean;\n error: string | null;\n refetch: () => void;\n}\n\nexport interface UseLegalDocsOptions {\n /** Optional pre-fetched payload from server (SSR / RSC). When set,\n * the hook skips the initial client fetch. */\n initialData?: LegalDocument | null;\n /** Full GET endpoint URL. Default `/api/legal/<docType>`. */\n apiEndpoint?: string;\n}\n\n/**\n * Hook to fetch a legal document.\n * @param docType — short identifier for the document (drives the\n * default endpoint path AND the error-log prefix). Common values:\n * `'privacy'` (SECURITY.md), `'terms'` (LICENSE). Embedders may use\n * any string — the hook treats it as opaque.\n */\nexport function useLegalDocs(\n docType: string,\n options: UseLegalDocsOptions = {}\n): UseLegalDocsReturn {\n const { initialData = null, apiEndpoint } = options;\n const effectiveEndpoint = apiEndpoint ?? `/api/legal/${docType}`;\n\n const [data, setData] = useState<LegalDocument | null>(initialData ?? null);\n const [isLoading, setIsLoading] = useState(!initialData);\n const [error, setError] = useState<string | null>(null);\n\n const fetchDocument = useCallback(async () => {\n try {\n setIsLoading(true);\n setError(null);\n\n const response = await contentFetch(effectiveEndpoint);\n\n if (!response.ok) {\n throw new Error(\n `Failed to fetch ${docType} document: ${response.status} ${response.statusText}`\n );\n }\n\n const result = await response.json();\n\n // Validate the response has required fields\n if (!result.content) {\n throw new Error(`${docType} document content is empty`);\n }\n\n setData(result);\n } catch (err) {\n const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';\n // `docType` is externally controlled (URL path segment in embedders), so it must NOT sit in\n // console.error's FIRST (format-string) argument — Node interprets %s/%j/%o there\n // (CodeQL js/tainted-format-string). Keep the format string constant; pass docType + err as\n // plain trailing args.\n console.error('Error fetching legal document:', docType, err);\n setError(errorMessage);\n } finally {\n setIsLoading(false);\n }\n }, [docType, effectiveEndpoint]);\n\n // Reset cached data when docType changes — otherwise an embedder using\n // the same hook instance for sequential docTypes (privacy → terms)\n // would briefly render the OLD doc's content while the new fetch is\n // in-flight. Not currently triggered by hub's per-route SSR (each\n // docType mounts in a fresh component), but enforces the contract.\n useEffect(() => {\n setData(initialData ?? null);\n setError(null);\n setIsLoading(!initialData);\n }, [docType, initialData]);\n\n // Fetch on mount (only if we don't already have server-provided initialData)\n useEffect(() => {\n if (initialData) return;\n fetchDocument();\n }, [fetchDocument, initialData]);\n\n const refetch = () => {\n fetchDocument();\n };\n\n return {\n data,\n isLoading,\n error,\n refetch,\n };\n}\n","'use client'\n\n/**\n * Author detail-page body — identity header (avatar → name → job title →\n * socials), bio, expertise badges, and a `children` slot for the authored-\n * content rail. THE one implementation behind the hub's `/authors/[slug]`\n * page AND embedded author pages (react-embedding-example) — hosts fetch\n * the `AuthorProfile` however they like and drill it in.\n *\n * Embed-readiness contract (same rules as ArticleAuthorByline):\n * - `Image` renders through the embed shim (plain `<img>` in non-Next\n * hosts; the real `next/image` once registered at app init).\n * - Avatar proxying rides the OPTIONAL ambient `ChatRuntime`\n * (`endpoints.imageProxyUrlPrefix`) — no provider ⇒ raw URL, never\n * throws. `proxyImageUrl` prop wins over the runtime.\n * - The authored-content rail is a SLOT (`children`), not a baked-in\n * fetch: the hub passes its pre-bound `RelatedContentSection` with SSR\n * `initialItems`; embedders pass the lib section with `apiBaseUrl`.\n * The view stays data-in, markup-out.\n *\n * Heading semantics are the HOST's: the view renders the name as a styled\n * `<p>` (like the hub profile header) so a hosting page can own its `<h1>`\n * (the hub uses an sr-only h1 for crawlers).\n */\n\nimport React from 'react'\nimport Image from '../../embed-shims/next-image'\nimport { cn } from '../../utils/cn'\nimport { formatBioText } from '../../utils/format'\nimport { getProxiedImageUrl } from '../../utils/image-proxy'\nimport { useChatRuntime } from '../../contexts/chat-runtime-context'\nimport { SocialIconRow } from '../social-icon-row'\nimport { StatusBadge } from '../ui/status-badge'\nimport type { AuthorProfile } from '../../types/entity-author'\n\nexport interface AuthorDetailViewProps {\n author: AuthorProfile\n /** Host-injected avatar-URL mapper — wins over the ambient runtime. */\n proxyImageUrl?: (url: string) => string\n /** Authored-content rail (or anything else) rendered below the profile. */\n children?: React.ReactNode\n className?: string\n}\n\nexport function AuthorDetailView({\n author,\n proxyImageUrl,\n children,\n className,\n}: AuthorDetailViewProps) {\n // Optional runtime — null-safe outside a provider (bare embeds render the\n // raw avatar URL; the hub's app-wide HubRuntimeProvider supplies the proxy).\n const runtime = useChatRuntime()\n\n const proxiedAvatar = author.avatarUrl\n ? proxyImageUrl\n ? proxyImageUrl(author.avatarUrl)\n : (getProxiedImageUrl(author.avatarUrl, {\n proxyPrefix: runtime?.endpoints.imageProxyUrlPrefix,\n skipDomains: runtime?.endpoints.imageProxySkipDomains,\n directHttps: true,\n }) ?? author.avatarUrl)\n : null\n\n const subtitle = [author.jobTitle, author.company].filter(Boolean).join(' @ ')\n const bioText = formatBioText(author.about)\n\n return (\n <div className={cn('flex flex-col gap-5 md:gap-6', className)}>\n {/* Identity header: avatar left; name → subtitle → socials stacked right. */}\n <div className=\"flex gap-6 w-full items-start\">\n <div className=\"relative shrink-0 h-24 w-24\">\n <div className=\"rounded-full overflow-hidden bg-ods-bg-secondary border border-ods-border w-full h-full relative\">\n {proxiedAvatar ? (\n <Image\n src={proxiedAvatar}\n alt={author.fullName}\n fill\n className=\"object-cover\"\n unoptimized\n />\n ) : (\n <div className=\"flex items-center justify-center h-full w-full text-3xl text-ods-text-secondary font-['Azeret_Mono']\">\n {author.fullName.charAt(0).toUpperCase()}\n </div>\n )}\n </div>\n </div>\n\n <div className=\"min-w-0 flex flex-col justify-center gap-2\">\n <p className=\"text-h2 text-ods-text-primary leading-none truncate\">\n {author.fullName}\n </p>\n {subtitle && (\n <p className=\"font-body text-lg text-ods-text-secondary leading-none truncate\">\n {subtitle}\n </p>\n )}\n {author.socialLinks.length > 0 && (\n // Ghost compact row (transparent 32px) — metadata, not CTAs.\n <SocialIconRow\n compact\n links={author.socialLinks.map((link) => ({\n platform: link.platform,\n href: link.url,\n label: `${author.fullName}'s ${link.platform}`,\n }))}\n />\n )}\n </div>\n </div>\n\n {bioText && (\n <p className=\"font-body text-base leading-relaxed text-ods-text-secondary\">\n {bioText}\n </p>\n )}\n\n {author.knowsAbout.length > 0 && (\n <div className=\"flex flex-wrap gap-2\">\n {author.knowsAbout.map((topic) => (\n <StatusBadge key={topic} text={topic} variant=\"button\" colorScheme=\"cyan\" singleLine />\n ))}\n </div>\n )}\n\n {children}\n </div>\n )\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["/home/runner/work/openframe-oss-lib/openframe-oss-lib/openframe-frontend-core/dist/components/index.cjs","../../src/components/announcement-bar.tsx","../../src/components/icon-utils.tsx","../../src/components/openframe-logo.tsx","../../src/utils/announcement-storage.ts","../../src/components/categories-cart.tsx","../../src/components/vendor-icon.tsx","../../src/utils/url-fix.ts","../../src/utils/vendor-media-stub.ts","../../src/components/category-card.tsx","../../src/components/comment-card.tsx","../../src/components/vendor-display-button.tsx","../../src/components/auth-stub.tsx","../../src/components/user-summary-stub.tsx","../../src/components/content-loading-container.tsx","../../src/components/dynamic-skeleton.tsx","../../src/hooks/use-theme-aware.ts","../../src/components/filter-chip.tsx","../../src/components/footer.tsx","../../src/components/social-icon-row.tsx","../../src/components/unified-filter-logic.tsx","../../src/utils/format-text-stub.ts","../../src/components/index.ts","../../src/components/footer-waitlist-button.tsx","../../src/components/hero-image-uploader.tsx","../../src/components/icons-block.tsx","../../src/components/icons-stub.tsx","../../src/components/image-cropper.tsx","../../src/components/metric-value.tsx","../../src/components/msp-display.tsx","../../src/components/square-avatar.tsx","../../src/components/open-source-features.tsx","../../src/components/persistent-pagination.tsx","../../src/components/pricing-display.tsx","../../src/components/results-count.tsx","../../src/components/vendor-tag.tsx","../../src/components/selection-source-badge.tsx","../../src/components/user-display.tsx","../../src/components/loading/content-skeleton.tsx","../../src/components/profile/ProfileLoadingSkeleton.tsx","../../src/components/loading/msp-profile-form-skeleton.tsx","../../src/components/loading/category-card-skeleton.tsx","../../src/components/loading/category-vendor-selector-skeleton.tsx","../../src/components/loading/wizard-layout-skeleton.tsx","../../src/components/loading/margin-report-skeleton.tsx","../../src/components/loading/users-grid-skeleton.tsx","../../src/components/loading/organization-icon-skeleton.tsx","../../src/components/loading/organization-card-skeleton.tsx","../../src/components/loading/device-card-skeleton.tsx","../../src/components/vendor-page-skeleton.tsx","../../src/components/why-it-matters.tsx","../../src/components/yes-no-display.tsx","../../src/components/made-with-love.tsx","../../src/components/date-time-picker.tsx","../../src/components/shared/product-release/product-releases-view.tsx","../../src/components/shared/product-release/release-detail-page.tsx","../../src/components/shared/media-gallery-strip.tsx","../../src/components/shared/product-release/release-detail-skeleton.tsx","../../src/components/shared/roadmap/roadmap-grid.tsx","../../src/components/shared/roadmap/use-roadmap-voting.ts","../../src/components/shared/roadmap/roadmap-grid-skeleton.tsx","../../src/components/shared/roadmap/roadmap-view.tsx","../../src/components/shared/delivery/delivery-lists.tsx","../../src/components/shared/delivery/delivery-table.tsx","../../src/components/shared/legal-document/legal-document-page.tsx","../../src/components/shared/legal-document/use-legal-docs.ts","../../src/components/authors/author-detail-view.tsx"],"names":["OpenFrameLogo","renderSvgIcon","jsx","OpenmspLogo","FlamingoLogo","jsxs","getProxiedImageUrl","Fragment","useState","ImageIcon","VendorDirectoryIcon","OpenSourceIcon","CommunityHubIcon","VendorsIcon","CommunityIcon","CompareIcon","SunIcon","MoonIcon","CheckCircleIcon","GitHubIcon","PlusCircleIcon","useCallback","SquareAvatar","DollarSign","mainValue","backgroundClasses","DatePicker","useEffect","useRef","useMemo","DEFAULT_ENDPOINT","DEFAULT_SEARCH_PARAM_KEY","DEFAULT_STATUS_PARAM_KEY"],"mappings":"AAAA,6xBAAY;AACZ,YAAY;AACZ;AACE;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;AACF,yDAA8B;AAC9B,iCAA8B;AAC9B;AACE;AACA;AACA;AACF,yDAA8B;AAC9B;AACE;AACA;AACA;AACF,yDAA8B;AAC9B;AACE;AACA;AACF,yDAA8B;AAC9B;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,yDAA8B;AAC9B;AACE;AACA;AACF,yDAA8B;AAC9B;AACE;AACA;AACA;AACA;AACA;AACA;AACF,yDAA8B;AAC9B;AACE;AACF,yDAA8B;AAC9B,iCAA8B;AAC9B;AACE;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;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;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;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;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;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;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;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;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;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;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;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;AACF,yDAA8B;AAC9B;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,yDAA8B;AAC9B;AACE;AACA;AACF,yDAA8B;AAC9B,iCAA8B;AAC9B;AACE;AACA;AACA;AACA;AACF,yDAA8B;AAC9B;AACE;AACF,yDAA8B;AAC9B;AACE;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;AACF,yDAA8B;AAC9B;AACE;AACA;AACF,yDAA8B;AAC9B;AACE;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;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,yDAA8B;AAC9B;AACE;AACA;AACA;AACF,yDAA8B;AAC9B,iCAA8B;AAC9B,iCAA8B;AAC9B,iCAA8B;AAC9B,iCAA8B;AAC9B;AACE;AACF,yDAA8B;AAC9B;AACA;AClhCA,4EAAoC;AAGpC,4CAAA,CAAA;AADA,2CAAkB;ADohClB;AACA;AExhCA;AACE;AACA;AACA;AACA;AACA;AACA;AAAA;AF2hCF;AACA;AGjhCQ,+CAAA;AAhBD,IAAMA,eAAAA,EAAgB,CAAC,EAAE,SAAA,EAAW,cAAA,EAAgB,cAAA,EAAiB,GAAG,MAAM,CAAA,EAAA,GAAgH;AACnM,EAAA,uBACE,6BAAA;AAAA,IAAC,KAAA;AAAA,IAAA;AAAA,MACE,GAAG,KAAA;AAAA,MACJ,SAAA;AAAA,MACA,KAAA,EAAM,IAAA;AAAA,MACN,MAAA,EAAO,IAAA;AAAA,MACP,OAAA,EAAQ,WAAA;AAAA,MACR,IAAA,EAAK,MAAA;AAAA,MACL,KAAA,EAAM,4BAAA;AAAA,MACN,KAAA,EAAO;AAAA,QACL,UAAA,EAAY,cAAA;AAAA,QACZ,GAAG,KAAA,CAAM;AAAA,MACX,CAAA;AAAA,MAEA,QAAA,kBAAA,8BAAA,GAAC,EAAA,EACC,QAAA,EAAA;AA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,QAAA;AAEJ,MAAA;AACF,IAAA;AAGEA,IAAAA;AAK0B,IAAA;AAQ3B,IAAA;AACH,EAAA;AAEJ;AlE2oLiC;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;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;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;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;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;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;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;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;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;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;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;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;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;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;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;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;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/components/index.cjs","sourcesContent":[null,"\"use client\";\n\nimport { useState, useEffect } from 'react';\nimport Image from '../embed-shims/next-image';\nimport { X } from 'lucide-react';\nimport { Button } from './ui/button';\nimport { renderSvgIcon } from './icon-utils';\nimport {\n setStoredAnnouncement,\n getStoredAnnouncement,\n clearStoredAnnouncement,\n} from '../utils/announcement-storage';\nimport { Announcement } from '../types/announcement';\nimport { getAppType } from '../utils/app-config';\nimport { useEndpointsRuntime } from '../contexts/endpoints-runtime-context';\n\n// Helper that defers to renderSvgIcon so we don't need local icon imports\nconst getSvgIcon = (\n name: string,\n size: 'main' | 'cta' = 'main',\n extra: Record<string, any> = {}\n) => {\n const cls =\n size === 'cta'\n ? 'relative shrink-0 w-3 h-3 md:w-4 md:h-4'\n : 'relative shrink-0 w-6 h-6 md:w-8 md:h-8';\n return renderSvgIcon(name, { className: cls, ...extra });\n};\n\nexport function AnnouncementBar() {\n const [announcement, setAnnouncement] = useState<Announcement | null>(null);\n const [isVisible, setIsVisible] = useState<boolean>(false);\n\n // Get the platform type for platform-specific localStorage keys\n const platform = getAppType();\n\n // Optional endpoint runtime: when no provider is mounted (e.g. on a\n // bare React-tree page that doesn't wrap with HubRuntimeProvider),\n // the bar silently skips its fetch instead of throwing. Apps that DO\n // mount the provider get the configured URL — typically\n // '/api/announcements/active' in the hub, or a proxied path in an\n // embedded host.\n const endpoints = useEndpointsRuntime();\n const announcementsUrl = endpoints?.announcementsUrl;\n\n // Helper to determine dismissal key for localStorage\n const getDismissKey = (id: string) => `${platform}-announcement-${id}-dismissed`;\n \n // Helper to get platform-specific cache key\n const getCacheKey = () => `${platform}-announcement-cache`;\n\n // Fetch active announcement from API and update state + LS\n const fetchActiveAnnouncement = async () => {\n // No provider mounted → no URL configured → skip fetch silently.\n // Cached announcement from previous sessions still renders if present.\n if (!announcementsUrl) return;\n try {\n // Server-side platform injection - no URL parameter needed\n const response = await fetch(announcementsUrl);\n \n if (response.ok) {\n const data = await response.json();\n if (data.announcement) {\n setAnnouncement(data.announcement);\n\n // persist latest announcement for quick future loads with platform-specific key\n setStoredAnnouncement(getCacheKey(), data.announcement);\n\n // Check if this specific announcement was dismissed\n const isDismissed = localStorage.getItem(getDismissKey(data.announcement.id));\n setIsVisible(!isDismissed);\n } else {\n // No announcement available - clean up localStorage and hide bar\n setAnnouncement(null);\n setIsVisible(false);\n \n // Use utility function to properly clear platform-specific announcement data\n clearStoredAnnouncement(getCacheKey());\n }\n } else {\n // Network or other error - hide announcement and clean up\n console.error(`❌ [${platform.toUpperCase()}] Error fetching announcement: ${response.status}`);\n setAnnouncement(null);\n setIsVisible(false);\n \n // Clear stale data on network errors too\n clearStoredAnnouncement(getCacheKey());\n }\n } catch (error) {\n console.error('Error fetching active announcement:', error);\n setAnnouncement(null);\n setIsVisible(false);\n \n // Clear stale data on exceptions too\n clearStoredAnnouncement(getCacheKey());\n }\n };\n\n // Initial load: use cached announcement synchronously for instant paint\n useEffect(() => {\n const cached = getStoredAnnouncement(getCacheKey());\n if (cached) {\n const isDismissed = localStorage.getItem(getDismissKey(cached.id));\n setAnnouncement(cached);\n setIsVisible(!isDismissed);\n }\n\n // No provider mounted → no URL → no fetch / no polling. Cached\n // announcement still painted above. Skip scheduling the 5-min\n // interval entirely to avoid an idle timer + repeated short-circuit\n // calls.\n if (!announcementsUrl) return;\n\n // Always fetch latest on mount\n fetchActiveAnnouncement();\n\n // Schedule refresh every 5 minutes. When announcementsUrl flips\n // (e.g. provider value swap), the effect re-runs and restarts the\n // interval against the new URL — no stale captured fetch.\n const interval = setInterval(fetchActiveAnnouncement, 300_000);\n return () => clearInterval(interval);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [announcementsUrl]);\n\n // helpers\n const handleDismiss = () => {\n if (!announcement) return;\n localStorage.setItem(getDismissKey(announcement.id), 'true');\n setIsVisible(false);\n };\n\n const handleCtaClick = () => {\n if (!announcement?.cta_url) return;\n announcement.cta_target === '_blank'\n ? window.open(announcement.cta_url, '_blank', 'noopener,noreferrer')\n : (window.location.href = announcement.cta_url);\n };\n\n const renderIcon = () => {\n if (!announcement) return null;\n\n if (announcement.icon_type === 'png' && announcement.icon_png_url) {\n return (\n <Image\n src={announcement.icon_png_url}\n alt=\"Announcement icon\"\n width={32}\n height={32}\n className=\"relative shrink-0\"\n aria-hidden\n />\n );\n }\n\n return getSvgIcon(\n announcement.icon_svg_name || 'openframe-logo',\n 'main',\n announcement.icon_svg_props ?? {}\n );\n };\n\n // If no announcement or dismissed => render nothing\n if (!announcement || !isVisible) return null;\n\n return (\n <div\n className=\"relative w-full z-50\"\n style={{ backgroundColor: announcement.background_color }}\n data-announcement-bar\n >\n <div className=\"flex items-center w-full max-w-full\">\n {/* Mobile: Clickable content area, Desktop: Regular content */}\n <div\n className={`flex flex-row gap-2 md:gap-4 items-center pl-4 md:pl-6 py-1.5 md:py-2 flex-1 min-w-0 ${\n announcement.cta_enabled && announcement.cta_url ? 'md:cursor-default cursor-pointer' : ''\n }`}\n onClick={(e) => {\n // Only handle click on mobile (< 768px) and if CTA is enabled\n if (window.innerWidth < 768 && announcement.cta_enabled && announcement.cta_url) {\n e.preventDefault();\n handleCtaClick();\n }\n }}\n >\n {renderIcon()}\n\n <div className=\"flex-1 min-w-0 max-w-full\">\n <p className=\"font-body font-bold text-[14px] md:text-[18px] leading-tight tracking-tight mb-0 text-[#1A1A1A] truncate\">\n {announcement.title}\n </p>\n <p className=\"font-body text-[12px] md:text-[18px] leading-tight hidden md:block text-[#1A1A1A] truncate\">\n {announcement.description}\n </p>\n </div>\n\n {/* CTA Button - Hidden on mobile, shown on desktop */}\n {announcement.cta_enabled && announcement.cta_text && announcement.cta_url && (\n <div className=\"hidden md:flex flex-shrink-0 ml-1 md:ml-2\">\n <Button\n onClick={handleCtaClick}\n variant=\"outline\"\n size=\"small-legacy\"\n leftIcon={\n announcement.cta_show_icon && announcement.cta_icon\n ? getSvgIcon(\n announcement.cta_icon,\n 'cta',\n announcement.cta_icon_props ?? {}\n )\n : undefined\n }\n className=\"transition-opacity hover:opacity-90 text-xs md:text-sm whitespace-nowrap\"\n style={{\n backgroundColor: announcement.cta_button_background_color || undefined,\n color: announcement.cta_button_text_color || undefined,\n borderColor: announcement.cta_button_background_color || undefined,\n }}\n >\n {announcement.cta_text}\n </Button>\n </div>\n )}\n </div>\n\n {/* Dismiss button - always visible */}\n <button\n onClick={(e) => {\n e.stopPropagation(); // Prevent triggering the mobile CTA click\n handleDismiss();\n }}\n className=\"flex-shrink-0 w-8 h-8 md:w-10 md:h-10 flex items-center justify-center hover:bg-[#1A1A1A]/10 focus:outline-none focus:ring-2 focus:ring-[#1A1A1A] mr-2 md:mr-4\"\n aria-label=\"Dismiss announcement\"\n type=\"button\"\n >\n <X className=\"w-4 h-4 text-[#1A1A1A]\" strokeWidth={2} />\n </button>\n </div>\n </div>\n );\n}\n","import React from 'react';\nimport {\n Megaphone,\n Bell,\n Info,\n Star,\n Rocket,\n Package,\n} from 'lucide-react';\nimport { OpenFrameLogo } from './openframe-logo';\nimport { OpenmspLogo } from './openmsp-logo';\nimport { FlamingoLogo } from './flamingo-logo';\n\n/**\n * Returns a JSX element for a known icon name, spreading any extra props.\n * Falls back to Megaphone if the name is not recognised.\n */\nexport function renderSvgIcon(\n name: string,\n props: React.SVGProps<SVGSVGElement | SVGElement> = {}\n): React.ReactElement {\n const map: Record<string, (p: any) => React.ReactElement> = {\n megaphone: (p) => <Megaphone {...p} />,\n bell: (p) => <Bell {...p} />,\n info: (p) => <Info {...p} />,\n star: (p) => <Star {...p} />,\n rocket: (p) => <Rocket {...p} />,\n package: (p) => <Package {...p} />,\n 'openframe-logo': (p) => <OpenFrameLogo {...p} />,\n 'openmsp-logo': (p) => <OpenmspLogo {...p} />,\n 'flamingo': (p)=> <FlamingoLogo {...p} />,\n };\n\n const renderer = map[name] || map['megaphone'];\n return renderer(props);\n} ","import React from 'react';\n\nexport const OpenFrameLogo = ({ className, lowerPathColor, upperPathColor, ...props }: { className?: string, lowerPathColor?: string, upperPathColor?: string } & React.SVGProps<SVGSVGElement>) => {\n return (\n <svg\n {...props}\n className={className}\n width=\"32\"\n height=\"32\"\n viewBox=\"0 0 32 32\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n style={{\n '--fill-0': 'currentColor',\n ...props.style\n } as React.CSSProperties}\n >\n <g>\n <g>\n {/* White squares - top right, bottom left, bottom right, bottom center */}\n <path\n d=\"M21.3333 16.5333C21.3333 16.2388 21.5721 16 21.8667 16H26.1333C26.4279 16 26.6667 16.2388 26.6667 16.5333V20.8C26.6667 21.0946 26.4279 21.3333 26.1333 21.3333H21.8667C21.5721 21.3333 21.3333 21.0946 21.3333 20.8V16.5333Z\"\n fill={lowerPathColor ?? '#ffffff'}\n />\n <path\n d=\"M16 21.8667C16 21.5721 16.2388 21.3333 16.5333 21.3333H20.8C21.0946 21.3333 21.3333 21.5721 21.3333 21.8667V26.1333C21.3333 26.4279 21.0946 26.6667 20.8 26.6667H16.5333C16.2388 26.6667 16 26.4279 16 26.1333V21.8667Z\"\n fill={lowerPathColor ?? '#ffffff'}\n />\n <path\n d=\"M26.6667 21.8667C26.6667 21.5721 26.9054 21.3333 27.2 21.3333H31.4667C31.7612 21.3333 32 21.5721 32 21.8667V26.1333C32 26.4279 31.7612 26.6667 31.4667 26.6667H27.2C26.9054 26.6667 26.6667 26.4279 26.6667 26.1333V21.8667Z\"\n fill={lowerPathColor ?? '#ffffff'}\n />\n <path\n d=\"M21.3333 27.2C21.3333 26.9054 21.5721 26.6667 21.8667 26.6667H26.1333C26.4279 26.6667 26.6667 26.9054 26.6667 27.2V31.4667C26.6667 31.7612 26.4279 32 26.1333 32H21.8667C21.5721 32 21.3333 31.7612 21.3333 31.4667V27.2Z\"\n fill={lowerPathColor ?? '#ffffff'}\n />\n </g>\n {/* Black frame/border */}\n <path\n d=\"M30.9333 0H1.06667C0.477563 0 0 0.477564 0 1.06667V30.9333C0 31.5224 0.477563 32 1.06667 32H14.9333C15.5224 32 16 31.5224 16 30.9333V27.7333C16 27.1442 15.5224 26.6667 14.9333 26.6667H5.86667C5.57211 26.6667 5.33333 26.4279 5.33333 26.1333V5.86667C5.33333 5.57211 5.57211 5.33333 5.86667 5.33333H26.1333C26.4279 5.33333 26.6667 5.57212 26.6667 5.86667V14.9333C26.6667 15.5224 27.1442 16 27.7333 16H30.9333C31.5224 16 32 15.5224 32 14.9333V1.06667C32 0.477563 31.5224 0 30.9333 0Z\"\n fill={upperPathColor ?? '#1A1A1A'}\n />\n </g>\n </svg>\n );\n};","// Stub for announcement storage\nexport interface AnnouncementStorageOptions {\n key: string;\n defaultValue?: any;\n}\n\nexport function getStoredAnnouncement(key: string): any {\n if (typeof window !== 'undefined') {\n try {\n return JSON.parse(localStorage.getItem(key) || 'null');\n } catch {\n return null;\n }\n }\n return null;\n}\n\nexport function setStoredAnnouncement(key: string, value: any): void {\n if (typeof window !== 'undefined') {\n try {\n localStorage.setItem(key, JSON.stringify(value));\n } catch {\n // Ignore storage errors\n }\n }\n}\n\nexport function clearStoredAnnouncement(key: string = 'announcement'): void {\n if (typeof window !== 'undefined') {\n try {\n localStorage.removeItem(key);\n } catch {\n // Ignore storage errors\n }\n }\n}","\"use client\"\n\nimport React from 'react';\nimport Link from '../embed-shims/next-link';\nimport { ChevronRight } from 'lucide-react';\nimport { VendorIcon } from './vendor-icon';\nimport type { CategoryCardProps, RealCategoryCardProps } from '../types/category';\n\n\n// Component that receives vendor and subcategory data as props\nexport function CategoriesCart({ \n category, \n vendors = [], \n vendorCount = 0, \n subcategoryCount = 0, \n isLoading = false,\n className = '' \n}: RealCategoryCardProps) {\n return (\n <Link\n href={`/vendors?category=${category.slug}`}\n className={`block bg-ods-card border border-ods-border rounded-lg p-3 md:p-4 pb-4 md:pb-6 hover:border-[#FFC008] transition-colors group relative ${className}`}\n >\n <div className=\"flex flex-col gap-4 md:gap-6\">\n {/* Vendor Icons Grid */}\n <div className=\"relative w-full h-8 md:h-10 overflow-hidden\">\n <div className=\"flex gap-2 md:gap-3 w-full\">\n {isLoading ? (\n // Skeleton loading for vendor icons\n Array.from({ length: 20 }).map((_, index) => (\n <div\n key={index}\n className=\"w-8 h-8 md:w-10 md:h-10 bg-ods-border rounded animate-pulse flex-shrink-0\"\n />\n ))\n ) : vendors && vendors.length > 0 ? (\n vendors.map((vendor: any) => (\n <VendorIcon\n key={vendor.id}\n vendor={vendor}\n size=\"md\"\n className=\"rounded overflow-hidden filter grayscale opacity-60\"\n />\n ))\n ) : (\n // No vendors found - show placeholder icons\n Array.from({ length: 6 }).map((_, index) => (\n <div\n key={index}\n className=\"w-8 h-8 md:w-10 md:h-10 bg-ods-border rounded flex items-center justify-center opacity-30 flex-shrink-0\"\n >\n <div className=\"w-4 h-4 md:w-6 md:h-6 bg-[#888888] rounded-sm\" />\n </div>\n ))\n )}\n </div>\n\n {/* Gradient overlays for fade effect */}\n <div className=\"absolute inset-y-0 left-0 w-4 md:w-6 bg-gradient-to-r from-[#212121] to-transparent pointer-events-none\" />\n <div className=\"absolute inset-y-0 right-0 w-4 md:w-6 bg-gradient-to-l from-[#212121] to-transparent pointer-events-none\" />\n </div>\n\n {/* Category Information - Updated to use real data */}\n <div className=\"flex flex-col gap-2\">\n <div className=\"flex flex-col gap-1\">\n <h3 className=\"text-xl md:text-2xl font-bold text-ods-text-primary group-hover:text-ods-accent transition-colors leading-[1.33] font-body\">\n {category.name}\n </h3>\n <p className=\"text-sm md:text-base font-medium text-ods-text-secondary leading-[1.43] font-body\">\n {subcategoryCount || 0} Subcategories • {vendorCount || 0} Products\n </p>\n </div>\n\n <div className=\"flex items-start md:items-end justify-between gap-4 md:gap-6\">\n <p className=\"font-['DM_Sans'] font-medium text-lg leading-[1.33] text-ods-text-primary flex-1\">\n {category.description}\n </p>\n\n {/* Arrow Button */}\n <div\n className=\"flex items-center justify-center w-10 h-10 md:w-12 md:h-12 bg-transparent border border-ods-border rounded-md group-hover:bg-[#FFC008] transition-colors flex-shrink-0\"\n aria-label={`View ${category.name} category`}\n > \n <ChevronRight className=\"w-5 h-5 md:w-6 md:h-6 text-ods-text-primary group-hover:text-black transition-colors\" />\n </div>\n </div>\n </div>\n </div>\n </Link>\n );\n}\n\n","\"use client\"\n\nimport React from 'react'\nimport Image from '../embed-shims/next-image'\nimport { cn } from \"../utils/cn\"\nimport { getVendorLogo, VendorWithMedia } from '../utils/vendor-media-stub'\nimport { getProxiedImageUrl } from '../utils/image-proxy-stub'\n\ninterface VendorIconProps {\n vendor: VendorWithMedia & {\n id?: number\n title: string\n slug?: string\n logo?: string | null\n }\n size?: 'xs' | 'sm' | 'md' | 'lg' | 'l' | 'xl'\n className?: string\n showBackground?: boolean\n backgroundStyle?: 'dark' | 'light' | 'white'\n}\n\nconst sizeClasses = {\n xs: 'w-6 h-6',\n sm: 'w-8 h-8', \n md: 'w-10 h-10',\n lg: 'w-12 h-12',\n l: 'w-14 h-14',\n xl: 'w-16 h-16'\n}\n\nconst imageSizeMap = {\n xs: { width: 16, height: 16 },\n sm: { width: 20, height: 20 },\n md: { width: 32, height: 32 },\n lg: { width: 40, height: 40 },\n l: { width: 38, height: 38 },\n xl: { width: 40, height: 40 }\n}\n\nconst backgroundClasses = {\n dark: 'bg-ods-bg border border-ods-border',\n light: 'bg-ods-card border border-ods-border',\n white: 'bg-white border border-[#E5E5E5]'\n}\n\n/**\n * Common VendorIcon component for displaying vendor logos consistently across the platform\n * Extracted from vendor-card.tsx for reuse in comparison tables, dropdowns, etc.\n */\nexport function VendorIcon({ \n vendor, \n size = 'md', \n className = '',\n showBackground = true,\n backgroundStyle = 'dark'\n}: VendorIconProps) {\n const logoUrl = getVendorLogo(vendor)\n const { width, height } = imageSizeMap[size]\n \n const containerClasses = cn(\n sizeClasses[size],\n 'rounded-lg flex items-center justify-center flex-shrink-0',\n showBackground && backgroundClasses[backgroundStyle],\n !showBackground && 'overflow-hidden',\n className\n )\n\n return (\n <div className={containerClasses}>\n {logoUrl ? (\n <Image\n src={getProxiedImageUrl(logoUrl) || logoUrl}\n alt={`${vendor.title} logo`}\n width={width}\n height={height}\n className={cn(\n 'object-contain',\n showBackground ? 'p-1' : 'w-full h-full'\n )}\n />\n ) : (\n <div className={cn(\n 'flex items-center justify-center text-xs font-medium uppercase',\n backgroundStyle === 'white' ? 'text-[#333333]' : 'text-ods-text-secondary'\n )}>\n {vendor.title?.substring(0, 2) || '??'}\n </div>\n )}\n </div>\n )\n} ","/**\n * Utility functions for fixing URL construction issues\n */\n\n/**\n * Fix double slashes in URLs while preserving protocol slashes\n * @param url The URL to fix\n * @returns Fixed URL with single slashes in path\n */\nexport function fixUrlDoubleSlashes(url: string): string {\n if (!url) return url;\n \n // Split on protocol to preserve it\n const protocolMatch = url.match(/^(https?:\\/\\/)/);\n if (protocolMatch) {\n const protocol = protocolMatch[1];\n const rest = url.substring(protocol.length);\n // Replace multiple consecutive slashes with single slash in the path part\n const fixedRest = rest.replace(/\\/+/g, '/');\n return protocol + fixedRest;\n }\n \n // For relative URLs, just fix multiple slashes\n return url.replace(/\\/+/g, '/');\n}\n\n/**\n * Properly join URL path segments without creating double slashes\n * @param segments Path segments to join\n * @returns Properly joined path\n */\nexport function joinUrlPath(...segments: string[]): string {\n return segments\n .map((segment, index) => {\n // Remove leading slash from all but first segment\n if (index > 0 && segment.startsWith('/')) {\n segment = segment.substring(1);\n }\n // Remove trailing slash from all but last segment\n if (index < segments.length - 1 && segment.endsWith('/')) {\n segment = segment.substring(0, segment.length - 1);\n }\n return segment;\n })\n .filter(segment => segment.length > 0)\n .join('/');\n}\n\n/**\n * Fix Supabase storage URLs specifically\n * @param url Supabase storage URL\n * @returns Fixed URL\n */\nexport function fixSupabaseStorageUrl(url: string): string {\n // Early return if nothing to process\n if (!url) return url;\n\n /**\n * We previously restricted the fix to URLs that contained a specific Supabase\n * domain (\"supabase.co\"). However, we now serve assets from a custom domain\n * (e.g. app.openmsp.ai) that still uses the same `/storage/v1/object/public/`\n * path structure. The old guard clause prevented those URLs from being\n * cleaned which resulted in paths like:\n * https://app.openmsp.ai/storage/v1/object/public/logos///Ansible.png\n * Googlebot treats the triple-slash as a distinct resource and reports 404\n * errors which in turn causes the \"URL will be indexed only if certain\n * conditions are met\" warning in Search Console.\n *\n * Solution: detect the canonical Supabase storage path segment instead of\n * the host name. Whenever we see that path we safely collapse multiple\n * consecutive slashes to a single slash while preserving the protocol\n * (`https://`).\n */\n\n const SUPABASE_STORAGE_SEGMENT = '/storage/v1/object/public/';\n\n if (url.includes(SUPABASE_STORAGE_SEGMENT)) {\n return fixUrlDoubleSlashes(url);\n }\n\n // No known storage segment – return the original URL untouched\n return url;\n}","// Vendor media utilities for UI kit\n// Real implementation copied from main project\n\nimport { fixSupabaseStorageUrl } from './url-fix';\n\nexport interface VendorMedia {\n media_type: 'logo' | 'image' | 'video';\n media_url: string;\n}\n\nexport interface VendorWithMedia {\n id?: number;\n title: string;\n slug?: string;\n logo?: string | null;\n logo_url?: string; // Support direct logo_url field from lightweight API\n vendor_media?: VendorMedia[];\n}\n\n/**\n * Get the logo URL from vendor_media array or direct logo_url field\n */\nexport function getVendorLogo(vendor: VendorWithMedia): string | null {\n // First check for direct logo_url field (from lightweight API)\n if (vendor.logo_url) {\n return fixSupabaseStorageUrl(vendor.logo_url);\n }\n \n // Check for legacy logo field\n if (vendor.logo) {\n return fixSupabaseStorageUrl(vendor.logo);\n }\n \n // Fallback to vendor_media array (from detailed API)\n const logoMedia = vendor.vendor_media?.find(m => m.media_type === 'logo');\n if (logoMedia?.media_url) {\n return fixSupabaseStorageUrl(logoMedia.media_url);\n }\n \n return null;\n}\n\n/**\n * Get the main image URL from vendor_media array\n */\nexport function getVendorImage(vendor: VendorWithMedia): string | null {\n const imageMedia = vendor.vendor_media?.find(m => m.media_type === 'image');\n return imageMedia?.media_url ? fixSupabaseStorageUrl(imageMedia.media_url) : null;\n}\n\n/**\n * Get the video URL from vendor_media array\n */\nexport function getVendorVideo(vendor: VendorWithMedia): string | null {\n const videoMedia = vendor.vendor_media?.find(m => m.media_type === 'video');\n return videoMedia?.media_url ? fixSupabaseStorageUrl(videoMedia.media_url) : null;\n}\n\n/**\n * Get all media URLs of a specific type\n */\nexport function getVendorMediaByType(vendor: VendorWithMedia, type: 'logo' | 'image' | 'video'): string[] {\n return vendor.vendor_media?.filter(m => m.media_type === type).map(m => fixSupabaseStorageUrl(m.media_url)) || [];\n}\n\n/**\n * Get all media items grouped by type\n */\nexport function getVendorMediaGrouped(vendor: VendorWithMedia): {\n logos: string[];\n images: string[];\n videos: string[];\n} {\n const media = vendor.vendor_media || [];\n \n return {\n logos: media.filter(m => m.media_type === 'logo').map(m => fixSupabaseStorageUrl(m.media_url)),\n images: media.filter(m => m.media_type === 'image').map(m => fixSupabaseStorageUrl(m.media_url)),\n videos: media.filter(m => m.media_type === 'video').map(m => fixSupabaseStorageUrl(m.media_url))\n };\n}\n\n/**\n * Add new media to vendor_media array (useful for admin interfaces)\n */\nexport function addVendorMedia(vendor: VendorWithMedia, type: 'logo' | 'image' | 'video', url: string): VendorMedia[] {\n const existingMedia = vendor.vendor_media || [];\n const newMedia: VendorMedia = { media_type: type, media_url: url };\n return [...existingMedia, newMedia];\n}\n\n/**\n * Remove media from vendor_media array\n */\nexport function removeVendorMedia(vendor: VendorWithMedia, url: string): VendorMedia[] {\n return vendor.vendor_media?.filter(m => m.media_url !== url) || [];\n}\n\n/**\n * Check if vendor has media of a specific type\n */\nexport function hasVendorMedia(vendor: VendorWithMedia, type: 'logo' | 'image' | 'video'): boolean {\n return vendor.vendor_media?.some(m => m.media_type === type) || false;\n}\n\n/**\n * Get media count by type\n */\nexport function getVendorMediaCount(vendor: VendorWithMedia): {\n logos: number;\n images: number;\n videos: number;\n total: number;\n} {\n const media = vendor.vendor_media || [];\n \n const logos = media.filter(m => m.media_type === 'logo').length;\n const images = media.filter(m => m.media_type === 'image').length;\n const videos = media.filter(m => m.media_type === 'video').length;\n \n return {\n logos,\n images,\n videos,\n total: media.length\n };\n}","\"use client\"\n\nimport React from \"react\";\n\ninterface CategoryCardProps {\n name: string;\n description: string;\n categoryCount: number;\n productCount: number;\n icons?: React.ReactNode[];\n}\n\nexport function CategoryCard({ name, description, categoryCount, productCount, icons = [] }: CategoryCardProps) {\n return (\n <article className=\"bg-[#1A1A1A] border border-[#424242] rounded-[12px] p-8 flex flex-col min-w-0 box-border\">\n <div className=\"flex gap-6 mb-8 justify-center items-center\">\n {(icons.length > 0 ? icons : Array(10).fill(null)).map((icon, i) => (\n <div key={i} className=\"w-10 h-10 bg-gray-700 rounded flex items-center justify-center\">\n {icon}\n </div>\n ))}\n </div>\n <div className=\"flex-1 flex flex-col\">\n <h2 className=\"text-[24px] font-bold text-ods-text-primary mb-2 text-left leading-tight\">{name}</h2>\n <div className=\"text-[16px] text-ods-text-secondary mb-4 text-left\">\n {categoryCount} Categories • {productCount} Products\n </div>\n <div className=\"flex flex-row items-start\">\n <p className=\"text-[16px] text-ods-text-primary text-left leading-snug flex-1\">{description}</p>\n <button\n className=\"w-12 h-12 flex items-center justify-center border border-ods-border rounded-[6px] bg-transparent hover:bg-[#FFC008] transition-colors ml-4\"\n style={{ minWidth: 48, minHeight: 48 }}\n aria-label={`View ${name}`}\n >\n <svg width=\"24\" height=\"24\" fill=\"none\" stroke=\"#FAFAFA\" strokeWidth=\"2\" viewBox=\"0 0 24 24\"><path d=\"M9 18l6-6-6-6\"/></svg>\n </button>\n </div>\n </div>\n </article>\n );\n} ","\"use client\"\n\nimport { Button } from \"./ui/button\"\nimport { VendorDisplayButton } from \"./vendor-display-button\"\nimport { useAuth } from \"./auth-stub\"\nimport { formatRelativeTime } from \"../utils/date-utils\"\nimport { UserSummary } from \"./user-summary-stub\"\n\ninterface CommentCardProps {\n comment: {\n id: string\n content: string\n title?: string\n type?: 'pro' | 'con'\n importance?: 'Critical' | 'Important' | 'Nice-to-have'\n createdAt: string\n vendor?: {\n id: number\n title: string\n slug: string\n logo: string | null\n category?: string | null\n }\n user?: {\n id: string\n name: string\n profilePicture: string | null\n msp?: {\n id: string | number\n name?: string | null\n icon_url?: string | null\n }\n }\n canDelete?: boolean\n }\n onViewProduct?: (vendorSlug: string) => void\n onDeleteComment?: (commentId: string) => void\n showVendorInfo?: boolean\n compact?: boolean\n context: 'profile' | 'vendor'\n}\n\nexport function CommentCard({ \n comment, \n onViewProduct, \n onDeleteComment, \n showVendorInfo = true,\n compact = false,\n context = 'profile'\n}: CommentCardProps) {\n const { user: currentUser } = useAuth()\n \n // Use unified date formatting function\n const formatActivityTime = (timestamp: string) => {\n return formatRelativeTime(timestamp);\n }\n\n // Check if current user can delete this comment\n const canUserDeleteComment = () => {\n // If not authenticated, can't delete\n if (!currentUser) return false\n \n // For profile context, use the existing canDelete prop (since it's the user's own comments)\n if (context === 'profile') {\n return comment.canDelete !== false\n }\n \n // For vendor context, check if current user is the comment creator\n if (context === 'vendor' && comment.user) {\n return currentUser.id === comment.user.id\n }\n \n return false\n }\n\n const showDeleteButton = canUserDeleteComment()\n\n return (\n <div className=\"bg-ods-card border border-ods-border rounded-lg p-4 hover:border-ods-accent transition-all group overflow-hidden w-full max-w-full box-border\" style={{ maxWidth: '100%', wordBreak: 'break-word' }}>\n {/* Comment Header */}\n <div className=\"flex flex-col gap-3 mb-3 min-[420px]:flex-row min-[420px]:items-center min-[420px]:justify-between min-[420px]:gap-2 w-full\">\n {/* Row 1: Info + Timestamp */}\n <div className=\"flex items-center gap-2 min-w-0 flex-1\">\n {context === 'profile' && comment.vendor ? (\n <>\n {/* Vendor Button - Icon + Name */}\n <VendorDisplayButton\n vendor={comment.vendor}\n onClick={onViewProduct || (() => {})}\n />\n \n {/* Timestamp */}\n <span className=\"font-['DM_Sans'] font-medium text-[14px] leading-[1.43] text-ods-text-secondary whitespace-nowrap flex-shrink-0\">\n {formatActivityTime(comment.createdAt)}\n </span>\n </>\n ) : context === 'vendor' && comment.user ? (\n <UserSummary\n name={comment.user.name}\n email=\"\"\n compact\n avatarSize={48}\n subtitle={formatActivityTime(comment.createdAt)}\n avatarUrl={comment.user.profilePicture ?? null}\n mspPreview={comment.user.msp ? {\n name: comment.user.msp.name ?? null,\n logoUrl: comment.user.msp.icon_url ?? null,\n seatCount: null,\n technicianCount: null,\n annualRevenue: null,\n } : null}\n showEditButton={false}\n authProviders={[]}\n />\n ) : null}\n </div>\n \n {/* Row 2: Action Buttons */}\n <div className=\"flex gap-2 justify-start min-[420px]:justify-end\">\n {/* Delete Button - shown when user can delete the comment */}\n {showDeleteButton && onDeleteComment && (\n <Button\n onClick={() => onDeleteComment(comment.id)}\n variant=\"outline\"\n size=\"icon\"\n className=\"text-ods-text-primary\"\n >\n <svg width=\"22\" height=\"24\" viewBox=\"0 0 22 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" className=\"text-ods-text-primary\">\n <path d=\"M18.6025 4.37939C19.221 4.43597 19.6768 4.98399 19.6206 5.60254L18.5176 17.7358C18.4492 18.4886 18.3924 19.1183 18.3052 19.6299C18.2155 20.1552 18.0809 20.6447 17.8101 21.1021C17.4444 21.7195 16.9245 22.2292 16.3042 22.583L16.0317 22.7251C15.5519 22.9533 15.0526 23.0438 14.5215 23.0854C14.0042 23.126 13.3727 23.125 12.6172 23.125H9.38281C8.62726 23.125 7.9958 23.126 7.47852 23.0854C7.01357 23.049 6.57289 22.975 6.14844 22.8042L5.96826 22.7251C5.32037 22.4169 4.76543 21.9454 4.35693 21.3599L4.18994 21.1021C3.91912 20.6447 3.78445 20.1552 3.69482 19.6299C3.65123 19.3743 3.61556 19.0894 3.58203 18.7744L3.48242 17.7358L2.37939 5.60254L2.375 5.48682C2.38099 4.91684 2.81768 4.43244 3.39746 4.37939C3.97776 4.32664 4.496 4.72462 4.60449 5.28467L4.62061 5.39746L5.72363 17.5322C5.79528 18.3203 5.84358 18.8475 5.9126 19.252C5.97923 19.6424 6.05232 19.8298 6.12646 19.9551L6.28467 20.1836C6.45963 20.3987 6.68118 20.5732 6.93359 20.6934L7.04639 20.7388C7.17478 20.7822 7.35781 20.8195 7.6543 20.8428C8.06325 20.8749 8.59186 20.875 9.38281 20.875H12.6172C13.4081 20.875 13.9367 20.8749 14.3457 20.8428C14.7409 20.8117 14.9349 20.7559 15.0664 20.6934L15.3066 20.5557C15.5369 20.4008 15.7309 20.1958 15.8735 19.9551C15.9477 19.8298 16.0208 19.6424 16.0874 19.252C16.1564 18.8475 16.2047 18.3203 16.2764 17.5322L17.3794 5.39746C17.436 4.77903 17.984 4.32316 18.6025 4.37939Z\" fill=\"currentColor\"/>\n <path d=\"M7.37451 15.9995V11C7.37451 10.3788 7.87842 9.87526 8.49951 9.875C9.12083 9.875 9.62451 10.3787 9.62451 11V15.9995C9.62451 16.6208 9.12083 17.1245 8.49951 17.1245C7.87842 17.1242 7.37451 16.6207 7.37451 15.9995ZM12.3755 15.9995V11C12.3755 10.3787 12.8792 9.875 13.5005 9.875C14.1216 9.87526 14.6255 10.3788 14.6255 11V15.9995C14.6255 16.6207 14.1216 17.1242 13.5005 17.1245C12.8792 17.1245 12.3755 16.6208 12.3755 15.9995ZM12.585 0.875C13.0174 0.879157 13.425 0.903301 13.8096 1.02881L14.1157 1.14746C14.4155 1.28159 14.6931 1.46221 14.9375 1.68359L15.0811 1.82568C15.4034 2.16872 15.6379 2.59872 15.8984 3.05469L16.6528 4.37451H20L20.1157 4.38037C20.6825 4.43826 21.1248 4.91747 21.125 5.49951C21.125 6.08174 20.6827 6.5607 20.1157 6.61865L20 6.62451H2C1.37868 6.62451 0.875 6.12083 0.875 5.49951C0.875264 4.87842 1.37884 4.37451 2 4.37451H5.34717L6.32129 2.6709C6.53952 2.29735 6.76267 1.95525 7.0625 1.68359L7.31738 1.47705C7.58262 1.28327 7.87687 1.1312 8.19043 1.02881L8.38525 0.974609C8.71199 0.896341 9.05467 0.878464 9.41504 0.875H12.585ZM9.85742 3.125C9.29191 3.125 9.08123 3.13201 8.97266 3.14844L8.88916 3.16748C8.83075 3.18655 8.77503 3.21301 8.72217 3.24365L8.57275 3.35205C8.53704 3.38448 8.50137 3.42629 8.42773 3.54102L8.05566 4.1709L7.93848 4.37451H14.0615L13.9443 4.1709C13.6645 3.68114 13.554 3.50141 13.4858 3.41504L13.4272 3.35205C13.3815 3.31062 13.3309 3.27443 13.2778 3.24365L13.1108 3.16748C13.019 3.13753 12.8946 3.125 12.1426 3.125H9.85742Z\" fill=\"currentColor\"/>\n </svg>\n </Button>\n )}\n </div>\n </div>\n \n {/* Comment Content */}\n <div className=\"space-y-2 w-full max-w-full overflow-hidden\" style={{ maxWidth: '100%', wordBreak: 'break-word' }}>\n {comment.title && (\n <div className={compact ? \"h-[20px] flex items-center\" : \"h-[24px] flex items-center\"}>\n <h4 className=\"text-h3 tracking-[-0.02em] text-ods-text-primary group-hover:text-ods-accent transition-colors line-clamp-1\" style={{ \n wordBreak: 'break-word', \n overflowWrap: 'break-word', \n maxWidth: '100%',\n textOverflow: 'ellipsis',\n overflow: 'hidden',\n display: '-webkit-box',\n WebkitBoxOrient: 'vertical',\n WebkitLineClamp: 1\n }}>\n {comment.title}\n </h4>\n </div>\n )}\n <div className={compact ? \"h-[60px] flex items-center\" : \"h-[72px] flex items-center\"}>\n <p className=\"text-h4 text-ods-text-primary line-clamp-3\" style={{ \n wordBreak: 'break-word', \n overflowWrap: 'break-word', \n maxWidth: '100%',\n textOverflow: 'ellipsis',\n overflow: 'hidden',\n display: '-webkit-box',\n WebkitBoxOrient: 'vertical',\n WebkitLineClamp: 3\n }}>\n {comment.content}\n </p>\n </div>\n </div>\n </div>\n )\n}","\"use client\"\n\nimport { Button } from \"./ui/button\"\nimport { getVendorLogo, VendorWithMedia } from \"../utils/vendor-media-stub\"\nimport Image from \"../embed-shims/next-image\"\nimport { getProxiedImageUrl } from \"../utils/image-proxy-stub\"\n\ninterface VendorDisplayButtonProps {\n vendor: VendorWithMedia\n onClick?: (vendorSlug: string) => void\n variant?: 'default' | 'compact'\n externalUrl?: string\n}\n\nexport function VendorDisplayButton({ vendor, onClick, variant = 'default', externalUrl }: VendorDisplayButtonProps) {\n const handleClick = () => {\n if (externalUrl && vendor.slug) {\n // `externalUrl` is the caller-resolved platform base URL (the openmsp SSOT via\n // getPlatformProductionUrl, scheme-normalized). The old `process.env.NEXT_PUBLIC_OPENMSP_URL`\n // override is gone — it's stored scheme-less, which made this a relative window.open().\n window.open(`${externalUrl}/vendor/${vendor.slug}`, '_blank', 'noopener,noreferrer')\n } else if (onClick && vendor.slug) {\n onClick(vendor.slug)\n }\n }\n\n // Compact variant for flamingo-teaser\n if (variant === 'compact') {\n const logoUrl = getVendorLogo(vendor)\n \n return (\n <button \n onClick={handleClick}\n className=\"inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-ods-card border border-ods-border hover:border-ods-accent/50 transition-colors\"\n >\n {logoUrl ? (\n <div className=\"w-5 h-5 rounded overflow-hidden flex-shrink-0\">\n <Image\n src={getProxiedImageUrl(logoUrl) || logoUrl}\n alt={`${vendor.title} logo`}\n width={20}\n height={20}\n className=\"w-full h-full object-cover\"\n />\n </div>\n ) : (\n <div className=\"w-5 h-5 rounded bg-ods-border flex items-center justify-center flex-shrink-0\">\n <span className=\"text-ods-text-secondary text-[10px] font-medium\">\n {vendor.title.charAt(0).toUpperCase()}\n </span>\n </div>\n )}\n <span className=\"text-sm font-medium text-ods-text-primary\">\n {vendor.title}\n </span>\n </button>\n )\n }\n\n // Default variant\n return (\n <button \n onClick={handleClick}\n className=\"flex items-center gap-2 bg-ods-card border border-ods-border rounded-lg py-2 px-3 hover:border-ods-accent transition-colors max-w-full overflow-hidden\"\n >\n {getVendorLogo(vendor) ? (\n <div className=\"w-8 h-8 bg-ods-card border border-ods-border rounded-lg flex items-center justify-center flex-shrink-0\">\n <Image\n src={getProxiedImageUrl(getVendorLogo(vendor)!) || getVendorLogo(vendor)!}\n alt={`${vendor.title} logo`}\n width={24}\n height={24}\n className=\"rounded object-cover\"\n />\n </div>\n ) : (\n <div className=\"w-8 h-8 bg-ods-border border border-ods-border rounded-lg flex items-center justify-center flex-shrink-0\">\n <span className=\"text-ods-text-primary text-[12px] font-medium\">\n {vendor.title.charAt(0)}\n </span>\n </div>\n )}\n <span className=\"text-h4 text-ods-text-primary truncate min-w-0\" title={vendor.title}>\n {vendor.title}\n </span>\n </button>\n )\n} ","\"use client\"\n\n// Stub auth provider and hooks\nimport { createContext, useContext } from 'react';\n\ninterface AuthContextType {\n user: any | null;\n isLoading: boolean;\n}\n\nconst AuthContext = createContext<AuthContextType>({\n user: null,\n isLoading: false,\n});\n\n// Global reference to real auth hook when available\nlet realUseAuth: (() => any) | null = null;\n\nexport function setRealAuthHook(authHook: () => any) {\n realUseAuth = authHook;\n}\n\nexport function useAuth() {\n // Use real auth hook if available (when used in main app)\n if (realUseAuth) {\n try {\n const realAuth = realUseAuth();\n if (realAuth && realAuth.user) {\n return realAuth;\n }\n } catch (error) {\n // Fallback if real auth fails\n }\n }\n\n // Fallback mock user for UI kit context\n return {\n user: { id: 'mock-user-id', name: 'Mock User' },\n isLoading: false\n };\n}\n\nexport function AuthProvider({ children }: { children: React.ReactNode }) {\n return (\n <AuthContext.Provider value={{ user: { id: 'mock-user-id', name: 'Mock User' }, isLoading: false }}>\n {children as any}\n </AuthContext.Provider>\n );\n}","\"use client\";\n\nimport Image from \"../embed-shims/next-image\";\nimport { getProxiedImageUrl } from \"../utils/image-proxy-stub\";\n\ninterface Props {\n name: string;\n email: string;\n avatarUrl?: string | null;\n /** Optional subtitle text (e.g., relative time) to replace email line */\n subtitle?: string | null;\n /** Authentication provider names (e.g. [\"google\", \"microsoft\"]) */\n authProviders?: string[];\n /** Show an outline Edit Profile button that routes to editHref */\n showEditButton?: boolean;\n /** Path to navigate when Edit button clicked (default \"/profile/edit\") */\n editHref?: string;\n /** Optional userId/profile passed through to EditProfileButton (for analytics) */\n userId?: string;\n profileData?: any;\n /** Optional MSP preview info to render below email */\n mspPreview?: {\n name?: string | null;\n seatCount?: number | null;\n technicianCount?: number | null;\n annualRevenue?: number | null;\n logoUrl?: string | null;\n } | null;\n /** Compact mode (avatar + name row, used in comment headers) */\n compact?: boolean;\n /** Avatar size in px for compact mode (defaults 40) */\n avatarSize?: number;\n\n /** When true, replaces the static avatar with the ProfilePhotoUpload widget */\n editablePhoto?: boolean;\n /** Required when editablePhoto=true – receives new photo URL */\n onPhotoChange?: (url: string | null) => void;\n}\n\nconst getAuthProviderIcon = (provider: string) => {\n const p = provider.toLowerCase();\n switch (p) {\n case \"google\":\n return <Image src=\"/icons/google-logo.svg\" alt=\"Google\" width={16} height={16} className=\"w-4 h-4\" />;\n case \"microsoft\":\n case \"azure\":\n return <Image src=\"/icons/microsoft-logo.svg\" alt=\"Microsoft\" width={16} height={16} className=\"w-4 h-4\" />;\n case \"slack\":\n case \"slack_oidc\":\n return <div className=\"w-4 h-4 bg-ods-text-secondary rounded-full\" />;\n default:\n return <div className=\"w-4 h-4 bg-ods-text-secondary rounded-full\" />;\n }\n};\n\n// Abbreviate large numbers: 1 200 → 1.2K , 15 000 → 15K , 2 000 000 → 2M\nconst formatNumber = (n: number) => {\n if (n >= 1_000_000_000) {\n const value = n / 1_000_000_000;\n return `${Number.isInteger(value) ? value.toFixed(0) : value.toFixed(1)}B`;\n }\n if (n >= 1_000_000) {\n const value = n / 1_000_000;\n return `${Number.isInteger(value) ? value.toFixed(0) : value.toFixed(1)}M`;\n }\n if (n >= 1_000) {\n return `${Math.round(n / 1_000)}K`;\n }\n return n.toLocaleString();\n};\n\nexport function UserSummary({\n name,\n email,\n subtitle = null,\n avatarUrl,\n authProviders,\n showEditButton = false,\n editHref = \"/profile/edit\",\n userId,\n profileData,\n mspPreview,\n compact = false,\n avatarSize = 40,\n editablePhoto = false,\n onPhotoChange,\n}: Props) {\n // Compact variant: minimal horizontal row\n if (compact) {\n return (\n <div className=\"flex items-center gap-3 min-w-0\">\n <div className=\"relative shrink-0\">\n {avatarUrl ? (\n <Image src={getProxiedImageUrl(avatarUrl) ?? avatarUrl} alt={name} width={avatarSize} height={avatarSize} className=\"object-cover rounded-lg\" />\n ) : (\n <div className=\"rounded-lg bg-ods-accent flex items-center justify-center text-ods-text-on-accent font-heading font-bold\" style={{ width: avatarSize, height: avatarSize }}>\n {name.split(' ').map((n: string) => n.charAt(0)).join('').slice(0, 2)}\n </div>\n )}\n {mspPreview && mspPreview.logoUrl && (\n <Image src={getProxiedImageUrl(mspPreview.logoUrl) ?? mspPreview.logoUrl} alt={mspPreview.name || 'MSP'} width={24} height={24} className=\"absolute -bottom-1 -right-1 size-6 rounded-full object-cover select-none z-10\" />\n )}\n </div>\n <div className=\"min-w-0 flex-1\">\n <p\n className=\"text-h4 text-ods-text-primary truncate\"\n title={mspPreview?.name ? `${name} • ${mspPreview.name}` : name}\n >\n {name}\n {mspPreview?.name && (\n <span className=\"text-ods-text-secondary\"> • {mspPreview.name}</span>\n )}\n </p>\n <p className=\"text-h6 text-ods-text-secondary truncate\" title={subtitle && subtitle.trim().length > 0 ? subtitle : (email && email.trim().length > 0 ? email : '\\u00A0')}>\n {subtitle && subtitle.trim().length > 0 ? subtitle : (email && email.trim().length > 0 ? email : '\\u00A0')}\n </p>\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"flex flex-col gap-4 w-full\">\n {/* Header Row */}\n <div className=\"flex gap-6 w-full items-start\">\n {/* Avatar with badge wrapper */}\n <div className=\"relative shrink-0 h-24 w-24 overflow-visible\">\n {avatarUrl ? (\n <Image src={getProxiedImageUrl(avatarUrl) ?? avatarUrl} alt={name} width={96} height={96} className=\"object-cover rounded-full\" />\n ) : (\n <div className=\"rounded-full bg-ods-card border border-ods-border w-full h-full flex items-center justify-center text-3xl text-ods-text-secondary font-heading\">\n {name.charAt(0).toUpperCase()}\n </div>\n )}\n\n {/* MSP logo badge (show only when MSP exists) */}\n {mspPreview && (\n <div className=\"absolute -bottom-1 -right-1 size-10 rounded-full bg-ods-bg ring-2 ring-ods-border overflow-hidden flex items-center justify-center select-none\">\n {mspPreview.logoUrl ? (\n <Image\n src={getProxiedImageUrl(mspPreview.logoUrl) ?? mspPreview.logoUrl}\n alt={mspPreview.name || 'MSP Logo'}\n width={40}\n height={40}\n className=\"object-cover\"\n />\n ) : (\n <span className=\"text-ods-text-primary font-heading text-sm font-bold\">\n {mspPreview.name?.charAt(0).toUpperCase() || '?'}\n </span>\n )}\n </div>\n )}\n </div>\n\n {/* Info + actions block */}\n <div className=\"flex-1 grid grid-cols-[1fr_auto] gap-4\">\n {/* LEFT : text stack */}\n <div className=\"min-h-[6rem] flex flex-col justify-center space-y-3 truncate\">\n <p className=\"text-h2 text-ods-text-primary leading-none truncate\" title={name}>\n {name}\n </p>\n <p className=\"text-h4 text-ods-text-secondary break-all truncate\" title={(subtitle && subtitle.trim().length > 0) ? subtitle : (email && email.trim().length > 0 ? email : '\\u00A0')}>\n {(subtitle && subtitle.trim().length > 0) ? subtitle : (email && email.trim().length > 0 ? email : '\\u00A0')}\n </p>\n {mspPreview && (() => {\n const mspSegments = [\n mspPreview.name ?? '—',\n typeof mspPreview.seatCount === 'number'\n ? `${formatNumber(mspPreview.seatCount)} Seats`\n : null,\n typeof mspPreview.technicianCount === 'number'\n ? `${formatNumber(mspPreview.technicianCount)} Technicians`\n : null,\n typeof mspPreview.annualRevenue === 'number'\n ? `$${formatNumber(mspPreview.annualRevenue)}`\n : null,\n ].filter(Boolean) as string[];\n const mspTitle = mspSegments.join(' • ');\n return (\n <p className=\"text-h6 text-ods-text-primary truncate\" title={mspTitle}>\n {/* Build string with separators */}\n {mspSegments\n .flatMap((txt, idx) => (idx === 0 ? [txt] : [' • ', txt]))\n .map((seg, idx) => (\n <span key={idx} className={seg === ' • ' ? 'text-ods-text-secondary' : ''}>{seg}</span>\n ))}\n </p>\n );\n })()}\n </div>\n\n {/* RIGHT (desktop) */}\n {(authProviders?.length || showEditButton) && (\n <div className=\"hidden md:flex flex-col items-end justify-between flex-shrink-0 min-h-[6rem]\">\n {/* top part */}\n {authProviders && authProviders.length > 0 && (\n <div className=\"flex items-center gap-2\">\n <span className=\"text-xs text-ods-text-secondary whitespace-nowrap select-none\">\n Authorized by\n </span>\n <div className=\"flex items-center gap-2\">\n {authProviders.map((p) => (\n <div key={p} className=\"flex items-center justify-center w-4 h-4\">\n {getAuthProviderIcon(p)}\n </div>\n ))}\n </div>\n </div>\n )}\n\n {/* bottom part - Edit button would go here */}\n {showEditButton && (\n <div className=\"text-xs text-ods-text-secondary\">Edit Profile</div>\n )}\n </div>\n )}\n </div>\n </div>\n\n {/* Mobile row: Authorized by left, Edit btn right */}\n {(authProviders?.length || showEditButton) && (\n <div className=\"flex md:hidden items-center justify-between w-full gap-4\">\n {authProviders && authProviders.length > 0 && (\n <div className=\"flex items-center gap-2\">\n <span className=\"text-xs text-ods-text-secondary whitespace-nowrap select-none\">Authorized by</span>\n <div className=\"flex items-center gap-2\">\n {authProviders.map((p) => (\n <div key={p} className=\"flex items-center justify-center w-4 h-4\">\n {getAuthProviderIcon(p)}\n </div>\n ))}\n </div>\n </div>\n )}\n\n {showEditButton && (\n <div className=\"text-xs text-ods-text-secondary\">Edit Profile</div>\n )}\n </div>\n )}\n </div>\n );\n}","\"use client\"\n\nimport { ReactNode } from \"react\"\nimport { cn } from \"../utils/cn\"\n\ninterface ContentLoadingContainerProps {\n /**\n * Whether the content is currently loading\n */\n isLoading: boolean\n /**\n * The actual content to display when not loading\n */\n children: ReactNode\n /**\n * The skeleton component to show during loading\n */\n skeletonComponent: ReactNode\n /**\n * Additional CSS classes\n */\n className?: string\n /**\n * Minimum height to prevent layout jumps\n */\n minHeight?: string\n /**\n * Loading overlay opacity (0-1)\n */\n loadingOpacity?: number\n /**\n * Transition duration in milliseconds\n */\n transitionDuration?: number\n}\n\n/**\n * ContentLoadingContainer\n * \n * A unified loading container that wraps card grids and manages loading states \n * while keeping UI controls persistent. This component:\n * \n * - Shows skeleton loading only in content area\n * - Maintains consistent layout dimensions during loading\n * - Provides smooth transitions between loading and loaded states\n * - Prevents layout jumps by preserving container height\n * - Supports customizable skeleton components for different content types\n * \n * Usage:\n * ```tsx\n * <ContentLoadingContainer\n * isLoading={isLoadingVendors}\n * skeletonComponent={<CardSkeletonGrid variant=\"vendor\" count={12} />}\n * minHeight=\"min-h-[800px]\"\n * >\n * <VendorGrid vendors={vendors} />\n * </ContentLoadingContainer>\n * ```\n */\nexport function ContentLoadingContainer({\n isLoading,\n children,\n skeletonComponent,\n className,\n minHeight = \"min-h-[300px] md:min-h-[800px]\",\n loadingOpacity = 1,\n transitionDuration = 300,\n}: ContentLoadingContainerProps) {\n return (\n <div \n className={cn(\n \"relative w-full transition-all ease-in-out\",\n minHeight,\n className\n )}\n style={{\n transitionDuration: `${transitionDuration}ms`\n }}\n role=\"region\"\n aria-label={isLoading ? \"Loading content\" : \"Content loaded\"}\n aria-live=\"polite\"\n >\n {/* Loading Skeleton Overlay */}\n {isLoading && (\n <div \n className=\"absolute inset-0 z-10 bg-ods-bg\"\n style={{ \n opacity: loadingOpacity,\n transition: `opacity ${transitionDuration}ms ease-in-out`\n }}\n role=\"status\"\n aria-label=\"Loading content\"\n >\n {skeletonComponent}\n </div>\n )}\n\n {/* Actual Content */}\n <div \n className={cn(\n \"relative transition-opacity ease-in-out\",\n isLoading && \"opacity-0\"\n )}\n style={{\n transitionDuration: `${transitionDuration}ms`\n }}\n aria-hidden={isLoading}\n >\n {children}\n </div>\n </div>\n )\n}\n\n/**\n * Hook for managing content loading states with common patterns\n */\nexport function useContentLoading(isLoading: boolean) {\n const containerProps = {\n 'aria-busy': isLoading,\n 'data-loading': isLoading\n }\n\n const getSkeletonCount = (contentType: 'vendor' | 'blog') => {\n return contentType === 'vendor' ? 12 : 6\n }\n\n const getMinHeight = (contentType: 'vendor' | 'blog') => {\n // Vendor cards are typically taller, need more space\n return contentType === 'vendor' \n ? \"min-h-[400px] md:min-h-[900px]\" \n : \"min-h-[300px] md:min-h-[800px]\"\n }\n\n return {\n containerProps,\n getSkeletonCount,\n getMinHeight\n }\n} ","\"use client\";\n\nimport React from 'react';\nimport { cn } from \"../utils/cn\";\nimport { useDynamicTheming } from '../hooks/use-dynamic-theming';\nimport { useThemeAware } from '../hooks/use-theme-aware';\n\ninterface DynamicSkeletonProps {\n /**\n * Type of skeleton animation\n */\n animation?: 'pulse' | 'shimmer' | 'wave' | 'static';\n \n /**\n * Skeleton variant based on content type\n */\n variant?: 'text' | 'avatar' | 'card' | 'button' | 'image' | 'custom';\n \n /**\n * Size preset for common elements\n */\n size?: 'sm' | 'md' | 'lg' | 'xl';\n \n /**\n * Custom dimensions\n */\n width?: string | number;\n height?: string | number;\n \n /**\n * Number of lines for text skeleton\n */\n lines?: number;\n \n /**\n * Platform-aware styling\n */\n platformAware?: boolean;\n \n /**\n * Accessibility enhancements\n */\n includeAriaLabel?: boolean;\n \n /**\n * Custom CSS classes\n */\n className?: string;\n \n /**\n * Child elements (for container skeletons)\n */\n children?: React.ReactNode;\n}\n\nconst sizePresets = {\n sm: { width: '4rem', height: '1rem' },\n md: { width: '8rem', height: '1.5rem' },\n lg: { width: '12rem', height: '2rem' },\n xl: { width: '16rem', height: '2.5rem' }\n};\n\nconst variantStyles = {\n text: 'rounded',\n avatar: 'rounded-full aspect-square',\n card: 'rounded-lg',\n button: 'rounded-md',\n image: 'rounded-lg aspect-video',\n custom: ''\n};\n\nexport function DynamicSkeleton({\n animation = 'shimmer',\n variant = 'text',\n size = 'md',\n width,\n height,\n lines = 1,\n platformAware = true,\n includeAriaLabel = true,\n className,\n children\n}: DynamicSkeletonProps) {\n const { platform, isDark, accentColor } = useThemeAware();\n\n // Calculate dimensions\n const dimensions = React.useMemo(() => {\n if (width || height) {\n return {\n width: typeof width === 'number' ? `${width}px` : width,\n height: typeof height === 'number' ? `${height}px` : height\n };\n }\n \n if (variant === 'avatar') {\n const avatarSize = sizePresets[size].height;\n return { width: avatarSize, height: avatarSize };\n }\n \n return sizePresets[size];\n }, [width, height, size, variant]);\n\n // Platform-specific skeleton colors\n const platformStyles = React.useMemo(() => {\n if (!platformAware) return {};\n\n const baseOpacity = isDark ? 0.1 : 0.15;\n const accentOpacity = isDark ? 0.05 : 0.08;\n\n return {\n '--skeleton-base': `color-mix(in srgb, var(--color-bg-skeleton) ${baseOpacity * 100}%, transparent)`,\n '--skeleton-highlight': `color-mix(in srgb, ${accentColor} ${accentOpacity * 100}%, var(--color-bg-skeleton))`,\n '--skeleton-accent': `color-mix(in srgb, ${accentColor} 10%, transparent)`\n } as React.CSSProperties;\n }, [platformAware, isDark, accentColor]);\n\n // Animation classes\n const animationClasses = {\n pulse: 'animate-pulse',\n shimmer: 'ods-loading-dynamic',\n wave: 'skeleton-wave',\n static: ''\n };\n\n // Base skeleton classes\n const baseClasses = cn(\n 'bg-ods-skeleton',\n variantStyles[variant],\n animationClasses[animation],\n platformAware && `skeleton-platform-${platform}`,\n className\n );\n\n // For text skeletons with multiple lines\n if (variant === 'text' && lines > 1) {\n return (\n <div \n className=\"space-y-2\"\n style={platformStyles}\n role=\"status\"\n aria-label={includeAriaLabel ? \"Loading content...\" : undefined}\n >\n {Array.from({ length: lines }, (_, index) => (\n <div\n key={index}\n className={cn(\n baseClasses,\n index === lines - 1 && 'w-3/4' // Last line shorter\n )}\n style={{\n ...dimensions,\n width: index === lines - 1 ? '75%' : dimensions.width\n }}\n />\n ))}\n </div>\n );\n }\n\n // Single skeleton element\n return (\n <div\n className={baseClasses}\n style={{ ...dimensions, ...platformStyles }}\n role=\"status\"\n aria-label={includeAriaLabel ? \"Loading content...\" : undefined}\n >\n {children}\n </div>\n );\n}\n\n/**\n * Pre-configured skeleton for common UI patterns\n */\nexport const SkeletonPresets = {\n /**\n * Card skeleton with header, content, and actions\n */\n Card: ({ showActions = true, showImage = false }: { showActions?: boolean; showImage?: boolean }) => (\n <div className=\"bg-ods-card border border-ods-border rounded-lg p-6 space-y-4\">\n {showImage && (\n <DynamicSkeleton variant=\"image\" className=\"w-full h-40\" />\n )}\n <div className=\"space-y-2\">\n <DynamicSkeleton variant=\"text\" size=\"lg\" />\n <DynamicSkeleton variant=\"text\" lines={2} size=\"md\" />\n </div>\n {showActions && (\n <div className=\"flex gap-2 pt-2\">\n <DynamicSkeleton variant=\"button\" width=\"6rem\" height=\"2.5rem\" />\n <DynamicSkeleton variant=\"button\" width=\"4rem\" height=\"2.5rem\" />\n </div>\n )}\n </div>\n ),\n\n /**\n * User profile skeleton\n */\n Profile: () => (\n <div className=\"flex items-start gap-4 p-4\">\n <DynamicSkeleton variant=\"avatar\" size=\"lg\" />\n <div className=\"flex-1 space-y-2\">\n <DynamicSkeleton variant=\"text\" size=\"lg\" width=\"8rem\" />\n <DynamicSkeleton variant=\"text\" size=\"sm\" width=\"12rem\" />\n <DynamicSkeleton variant=\"text\" lines={2} size=\"md\" />\n </div>\n </div>\n ),\n\n /**\n * Navigation skeleton\n */\n Navigation: ({ items = 5 }: { items?: number }) => (\n <nav className=\"space-y-2 p-4\">\n {Array.from({ length: items }, (_, index) => (\n <div key={index} className=\"flex items-center gap-3\">\n <DynamicSkeleton variant=\"avatar\" size=\"sm\" />\n <DynamicSkeleton variant=\"text\" size=\"md\" />\n </div>\n ))}\n </nav>\n ),\n\n /**\n * Table skeleton\n */\n Table: ({ rows = 5, columns = 4 }: { rows?: number; columns?: number }) => (\n <div className=\"space-y-3\">\n {/* Header */}\n <div className=\"grid gap-4\" style={{ gridTemplateColumns: `repeat(${columns}, 1fr)` }}>\n {Array.from({ length: columns }, (_, index) => (\n <DynamicSkeleton key={`header-${index}`} variant=\"text\" size=\"md\" />\n ))}\n </div>\n {/* Rows */}\n {Array.from({ length: rows }, (_, rowIndex) => (\n <div key={`row-${rowIndex}`} className=\"grid gap-4\" style={{ gridTemplateColumns: `repeat(${columns}, 1fr)` }}>\n {Array.from({ length: columns }, (_, colIndex) => (\n <DynamicSkeleton key={`cell-${rowIndex}-${colIndex}`} variant=\"text\" size=\"sm\" />\n ))}\n </div>\n ))}\n </div>\n ),\n\n /**\n * Vendor grid skeleton (specific to the app)\n */\n VendorGrid: ({ items = 6 }: { items?: number }) => (\n <div className=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6\">\n {Array.from({ length: items }, (_, index) => (\n <div key={index} className=\"bg-ods-card border border-ods-border rounded-lg p-4 space-y-3\">\n <div className=\"flex items-center gap-3\">\n <DynamicSkeleton variant=\"avatar\" size=\"md\" />\n <div className=\"flex-1\">\n <DynamicSkeleton variant=\"text\" size=\"md\" width=\"6rem\" />\n <DynamicSkeleton variant=\"text\" size=\"sm\" width=\"4rem\" className=\"mt-1\" />\n </div>\n </div>\n <DynamicSkeleton variant=\"text\" lines={2} size=\"sm\" />\n <div className=\"flex gap-2\">\n <DynamicSkeleton variant=\"custom\" width=\"3rem\" height=\"1.25rem\" className=\"rounded-full\" />\n <DynamicSkeleton variant=\"custom\" width=\"4rem\" height=\"1.25rem\" className=\"rounded-full\" />\n </div>\n </div>\n ))}\n </div>\n ),\n\n /**\n * Blog post skeleton\n */\n BlogPost: () => (\n <article className=\"space-y-6\">\n <div className=\"space-y-2\">\n <DynamicSkeleton variant=\"text\" size=\"xl\" width=\"75%\" />\n <div className=\"flex gap-4 text-sm\">\n <DynamicSkeleton variant=\"text\" size=\"sm\" width=\"4rem\" />\n <DynamicSkeleton variant=\"text\" size=\"sm\" width=\"6rem\" />\n </div>\n </div>\n <DynamicSkeleton variant=\"image\" className=\"w-full h-64\" />\n <div className=\"space-y-3\">\n {Array.from({ length: 4 }, (_, index) => (\n <DynamicSkeleton key={index} variant=\"text\" lines={3} size=\"md\" />\n ))}\n </div>\n </article>\n )\n};\n\n/**\n * Platform-aware loading container that shows different skeletons based on platform\n */\nexport function PlatformSkeletonContainer({ \n children, \n isLoading, \n skeletonType = 'Card',\n skeletonProps = {}\n}: {\n children: React.ReactNode;\n isLoading: boolean;\n skeletonType?: keyof typeof SkeletonPresets;\n skeletonProps?: any;\n}) {\n const { platform } = useThemeAware();\n\n if (!isLoading) {\n return <>{children}</>;\n }\n\n const SkeletonComponent = SkeletonPresets[skeletonType];\n\n return (\n <div className={`platform-skeleton skeleton-${platform}`}>\n <SkeletonComponent {...skeletonProps} />\n </div>\n );\n}\n\n/**\n * Enhanced skeleton with progressive enhancement\n */\nexport function ProgressiveSkeleton({\n stages = ['basic', 'detailed', 'interactive'],\n currentStage = 0,\n children,\n ...props\n}: DynamicSkeletonProps & {\n stages?: string[];\n currentStage?: number;\n}) {\n const { platform } = useThemeAware();\n\n // Show more detailed skeleton as loading progresses\n const stageConfig = {\n 0: { animation: 'pulse' as const, variant: 'text' as const },\n 1: { animation: 'shimmer' as const, variant: 'card' as const },\n 2: { animation: 'wave' as const, variant: 'custom' as const }\n };\n\n const config = stageConfig[currentStage as keyof typeof stageConfig] || stageConfig[0];\n\n return (\n <DynamicSkeleton\n {...props}\n {...config}\n className={cn(\n props.className,\n `skeleton-stage-${currentStage}`,\n `skeleton-platform-${platform}`\n )}\n >\n {children}\n </DynamicSkeleton>\n );\n}","// Stub implementation for useThemeAware hook\n\nexport function useThemeAware() {\n return {\n themeColors: {},\n platformContext: 'default',\n isDarkMode: false,\n platform: 'default',\n isDark: false,\n accentColor: '#FFC008'\n };\n}","\"use client\"\n\nimport { cn } from \"../utils/cn\"\nimport { X } from \"lucide-react\"\n\n// Unified FilterChip component for consistent styling across the application\ninterface FilterChipProps {\n id: string\n label: string\n variant?: 'selected' | 'unselected' | 'category' | 'subcategory' | 'tag' | 'info'\n size?: 'sm' | 'md'\n removable?: boolean\n onRemove?: () => void\n onClick?: () => void\n disabled?: boolean\n className?: string\n}\n\nexport function FilterChip({\n id,\n label,\n variant = 'unselected',\n size = 'md',\n removable = false,\n onRemove,\n onClick,\n disabled = false,\n className\n}: FilterChipProps) {\n const baseClasses = cn(\n \"inline-flex items-center justify-center rounded-full font-medium transition-all duration-200 shrink-0 group cursor-pointer\",\n \"hover:shadow-md hover:scale-105 focus:outline-none focus:ring-2 focus:ring-offset-2\",\n \"font-['DM_Sans'] leading-none\",\n // Size variants - enhanced mobile sizing for better visibility and touch targets\n size === 'sm' \n ? \"text-sm pl-3 pr-3 py-1 md:text-sm md:pl-3 md:pr-3 md:py-1\"\n : \"text-sm pl-3 pr-3 py-2 md:text-sm md:pl-3 md:pr-3 md:py-2\",\n // Add gap only if removable (has X button) - placed after text\n removable && \"gap-1 md:gap-1\",\n // Disabled state\n disabled && \"opacity-50 cursor-not-allowed hover:scale-100 hover:shadow-none\"\n )\n\n const variantClasses = {\n // Legacy variants (for backward compatibility)\n selected: \"bg-[#2A2A2A] text-[#E8E8E8] border border-[#FFC008] hover:bg-ods-border hover:border-[#FFD951] focus:ring-[#FFC008] focus:ring-offset-[#161616]\",\n unselected: \"bg-[#2A2A2A] text-[#CCCCCC] border border-[#4A4A4A] hover:bg-ods-border hover:border-[#5A5A5A] hover:text-ods-text-primary focus:ring-[#4A4A4A] focus:ring-offset-[#161616]\",\n info: \"bg-ods-border text-[#CCCCCC] border border-[#5A5A5A] cursor-default hover:scale-100 hover:shadow-none focus:ring-[#5A5A5A] focus:ring-offset-[#161616]\",\n \n // New subtle selected variants - same backgrounds/text, only border colors different\n category: \"bg-[#2A2A2A] text-[#E8E8E8] border border-[#FFC008]/40 hover:bg-ods-border hover:border-[#FFC008]/60 hover:text-ods-text-primary focus:ring-[#FFC008]/40 focus:ring-offset-[#161616]\",\n subcategory: \"bg-[#2A2A2A] text-[#E8E8E8] border border-[#FFC008]/60 hover:bg-ods-border hover:border-[#FFC008]/80 hover:text-ods-text-primary focus:ring-[#FFC008]/60 focus:ring-offset-[#161616]\",\n tag: \"bg-[#2A2A2A] text-ods-text-primary border border-[#FFC008]/20 hover:bg-ods-border hover:border-[#FFC008]/30 hover:text-ods-text-primary focus:ring-[#FFC008]/20 focus:ring-offset-[#161616]\",\n }\n\n return (\n <div\n className={cn(baseClasses, variantClasses[variant], className)}\n onClick={disabled ? undefined : (e) => {\n e.preventDefault();\n e.stopPropagation();\n onClick?.();\n }}\n role={onClick ? \"button\" : undefined}\n tabIndex={onClick && !disabled ? 0 : undefined}\n aria-pressed={onClick && variant === 'selected' ? true : undefined}\n aria-disabled={disabled}\n >\n <span className={cn(\n \"truncate font-['DM_Sans'] font-medium leading-none text-center\",\n size === 'sm' ? \"max-w-[100px] md:max-w-[120px]\" : \"max-w-[120px] md:max-w-[140px]\"\n )} title={label}>\n {label}\n </span>\n {removable && onRemove && (\n <button\n type=\"button\"\n onClick={(e) => {\n e.preventDefault()\n e.stopPropagation()\n if (!disabled) onRemove()\n }}\n disabled={disabled}\n className={cn(\n \"flex items-center justify-center w-4 h-4 md:w-4 md:h-4 rounded-full\",\n \"transition-all duration-200 shrink-0\",\n \"group-hover:scale-110 focus:outline-none focus:ring-2 focus:ring-offset-1\",\n variant === 'category'\n ? \"hover:bg-[#4A4A4A] text-[#E8E8E8] focus:ring-[#E8E8E8] focus:ring-offset-[#2A2A2A]\"\n : variant === 'subcategory'\n ? \"hover:bg-[#4A4A4A] text-[#E8E8E8] focus:ring-[#E8E8E8] focus:ring-offset-[#2A2A2A]\"\n : variant === 'tag'\n ? \"hover:bg-[#4A4A4A] text-[#E8E8E8] focus:ring-[#E8E8E8] focus:ring-offset-[#2A2A2A]\"\n : \"hover:bg-[#4A4A4A] text-[#E8E8E8] focus:ring-[#E8E8E8] focus:ring-offset-[#2A2A2A]\",\n disabled && \"opacity-50 cursor-not-allowed hover:scale-100\"\n )}\n aria-label={`Remove ${label} filter`}\n tabIndex={disabled ? -1 : 0}\n >\n <X className=\"h-2 w-2 md:h-2 md:w-2\" />\n </button>\n )}\n </div>\n )\n} ","\"use client\"\n\nimport { Suspense } from \"react\"\nimport { Skeleton } from \"./ui/skeleton\"\nimport { SocialIconRow } from \"./social-icon-row\"\n\ninterface FooterLink {\n href: string\n label: string\n}\n\ninterface FooterSection {\n title: string\n links: FooterLink[]\n}\n\ninterface FooterConfig {\n name: string\n legalName: string\n description: string\n logo?: React.ReactNode\n sections: FooterSection[]\n customComponent?: React.ReactNode // Inject any custom component here\n nameElement?: React.ReactNode // Custom element for platform name with specific font\n hideSocialRow?: boolean // Hide the default social row\n rightColumnContent?: React.ReactNode // Custom content for right column\n belowDescriptionContent?: React.ReactNode // Custom content below description\n moveDescriptionToRight?: boolean // Move description and belowDescriptionContent to right column\n keepBelowDescriptionLeft?: boolean // Keep belowDescriptionContent on left even when moveDescriptionToRight is true\n backgroundColor?: string // ODS background color (e.g., 'bg-ods-bg-card', 'bg-ods-system-greys-black')\n social?: {\n github?: string\n twitter?: string\n linkedin?: string\n reddit?: string\n youtube?: string\n instagram?: string\n facebook?: string\n discord?: string\n telegram?: string\n whatsapp?: string\n }\n}\n\ninterface FooterProps {\n config?: FooterConfig\n renderLink?: (link: FooterLink) => React.ReactNode\n}\n\nfunction NavLinkSkeleton() {\n return <Skeleton className=\"h-5 md:h-6 w-20 md:w-24\" />\n}\n\n/**\n * Platform-Aware Footer Component\n * Accepts configuration from app-config.ts\n */\nexport function Footer({ config, renderLink }: FooterProps) {\n // Config is required - no hardcoded fallbacks\n if (!config) {\n console.warn('Footer: No config provided')\n return null\n }\n \n return <UniversalFooter config={config} renderLink={renderLink} />\n}\n\n/**\n * Universal Footer Component\n * Renders footer based on provided config\n */\nfunction UniversalFooter({ config, renderLink }: { config: FooterConfig; renderLink?: (link: FooterLink) => React.ReactNode }) {\n const defaultRenderLink = (link: FooterLink) => (\n <a href={link.href} className=\"font-body font-medium text-md md:text-md leading-[1.33] text-ods-text-primary hover:text-ods-accent-primary transition-colors\">\n {link.label}\n </a>\n )\n \n const linkRenderer = renderLink || defaultRenderLink\n \n return (\n <footer className={`w-full flex flex-col justify-center items-center ${config.backgroundColor || 'bg-ods-bg-card'} px-6 py-10 relative gap-6 md:gap-6 min-h-[auto] md:min-h-[248px] z-50 border-t border-ods-border`}>\n <div className=\"w-full grid grid-cols-2 lg:grid-cols-4 gap-6 md:gap-8 items-start\">\n \n {/* Column 1: Logo and optionally description */}\n <div className=\"flex flex-col gap-4 md:gap-6 items-start text-left col-span-2 md:col-span-1 lg:col-span-1\">\n {/* Logo and name */}\n <div className=\"flex items-center gap-2\">\n {config.logo && (\n <Suspense fallback={<Skeleton className=\"w-8 h-8\" />}>\n {config.logo as any}\n </Suspense>\n )}\n {config.nameElement || <span className=\"font-heading text-heading-5 font-bold text-ods-text-primary whitespace-nowrap\">{config.name}</span>}\n </div>\n \n {/* Only show description here if NOT moving to right */}\n {!config.moveDescriptionToRight && (\n <>\n <p className=\"font-body font-medium text-sm md:text-sm leading-[1.43] text-ods-text-primary\">\n {config.description}\n </p>\n \n {/* Custom content below description */}\n {config.belowDescriptionContent && (\n <Suspense fallback={<Skeleton className=\"h-8 w-full\" />}>\n {config.belowDescriptionContent as any}\n </Suspense>\n )}\n \n {/* Conditional social row - show by default unless hideSocialRow is true */}\n {!config.hideSocialRow && (\n <SocialIconRow\n className=\"pt-2\"\n links={config.social ? Object.entries(config.social)\n .filter(([_, href]) => href)\n .map(([platform, href]) => ({ platform, href: href as string }))\n : undefined\n }\n />\n )}\n </>\n )}\n \n {/* Show belowDescriptionContent on left even when description is moved to right */}\n {config.moveDescriptionToRight && config.keepBelowDescriptionLeft && config.belowDescriptionContent && (\n <Suspense fallback={<Skeleton className=\"h-8 w-full\" />}>\n {config.belowDescriptionContent as any}\n </Suspense>\n )}\n </div>\n \n {/* Dynamic sections - 1 column each on all screens */}\n {config.sections.map((section, index) => (\n <div key={index} className=\"flex flex-col gap-3 items-start text-left col-span-1\">\n <h3 className=\"text-h5 tracking-[-0.02em] text-ods-text-muted\">\n {section.title}\n </h3>\n <div className=\"flex flex-col gap-3\">\n {section.links.map((link, linkIndex) => (\n <Suspense key={linkIndex} fallback={<NavLinkSkeleton />}>\n {linkRenderer(link) as any}\n </Suspense>\n ))}\n </div>\n </div>\n ))}\n \n {/* Custom component column - full width on mobile and medium, 1 column on large */}\n {config.customComponent && (\n <div className=\"flex flex-col col-span-2 md:col-span-1 lg:col-span-1 justify-center\">\n <Suspense fallback={<Skeleton className=\"h-32 w-full\" />}>\n {config.customComponent as any}\n </Suspense>\n </div>\n )}\n \n {/* Right column content - shows if rightColumnContent is provided OR if moving description to right */}\n {(config.rightColumnContent || config.moveDescriptionToRight) && (\n <div className=\"flex flex-col col-span-2 md:col-span-1 lg:col-span-1 justify-start gap-4 md:gap-6\">\n {/* Show description in right column if moveDescriptionToRight is true */}\n {config.moveDescriptionToRight && (\n <>\n <p className=\"font-body font-medium text-sm md:text-sm leading-[1.43] text-ods-text-primary\">\n {config.description}\n </p>\n \n {/* Custom content below description - only if NOT keeping it on left */}\n {config.belowDescriptionContent && !config.keepBelowDescriptionLeft && (\n <Suspense fallback={<Skeleton className=\"h-8 w-full\" />}>\n {config.belowDescriptionContent as any}\n </Suspense>\n )}\n </>\n )}\n \n {/* Regular right column content */}\n {config.rightColumnContent && (\n <Suspense fallback={<Skeleton className=\"h-32 w-full\" />}>\n {config.rightColumnContent as any}\n </Suspense>\n )}\n </div>\n )}\n </div>\n \n {/* Copyright */}\n <p className=\"font-body font-medium text-md md:text-md leading-[1.33] text-center w-full text-ods-text-muted pt-4 md:pt-0\">\n © {new Date().getFullYear()} {config.legalName}. All rights reserved.\n </p>\n </footer>\n )\n}\n\n","\"use client\"\n\nimport { Button } from './ui/button';\nimport { GitHubIcon, RedditIcon, XLogo, LinkedInIcon, LumaIcon, WhatsAppIcon, GlobeIcon, MessageCircleIcon, TelegramIcon, YouTubeIcon, InstagramIcon, FacebookIcon, SlackIcon, CopyIcon } from './icons';\n\n/** Exactly ONE of `href` (anchor, target _blank) or `onClick` (action\n * button — share popups via window.open inside the click gesture,\n * copy-to-clipboard) — the discriminated union makes a dead no-action\n * entry unrepresentable. */\ntype SocialLink = {\n platform: string;\n label?: string;\n} & (\n | { href: string; onClick?: never }\n | { onClick: () => void; href?: never }\n);\n\ninterface SocialIconRowProps {\n className?: string;\n links?: SocialLink[];\n variant?: \"accent\" | \"outline\" | \"transparent\" | \"destructive\" | null | undefined;\n /** Quiet metadata row for page-level identity/share slots: 32px ghost\n * icon buttons (size=\"icon-sm\", 16px glyphs), gap-2, w-fit container,\n * variant defaulting to \"transparent\" (an explicit `variant` still wins).\n * Default false: 44/48px buttons stretching across the container —\n * the original card-width behavior (TMCG member cards, footers). */\n compact?: boolean;\n}\n\nconst defaultLinks: SocialLink[] = [\n { platform: 'github', href: 'https://github.com/flamingo-stack', label: 'GitHub' },\n { platform: 'linkedin', href: 'https://linkedin.com/company/flamingo.run', label: 'LinkedIn' },\n { platform: 'facebook', href: 'https://www.facebook.com/flamingoai.msp', label: 'Facebook' }\n];\n\nfunction renderSocialIcon(platform: string) {\n const normalizedPlatform = platform.toLowerCase().trim();\n\n switch (normalizedPlatform) {\n case 'github':\n return <GitHubIcon className=\"w-5 h-5\" />;\n case 'twitter':\n case 'x':\n return <XLogo className=\"w-5 h-5\" />;\n case 'reddit':\n return <RedditIcon className=\"w-5 h-5\" variant=\"white\" />;\n case 'linkedin':\n return <LinkedInIcon className=\"w-5 h-5\" />;\n case 'luma':\n return <LumaIcon className=\"w-5 h-5\" />;\n case 'whatsapp':\n return <WhatsAppIcon className=\"w-5 h-5\" />;\n case 'website':\n case 'web':\n case 'url':\n return <GlobeIcon className=\"w-5 h-5\" />;\n case 'slack':\n return <SlackIcon className=\"w-5 h-5\" injectedColor=\"white\" />;\n case 'discord':\n return <MessageCircleIcon className=\"w-5 h-5\" />;\n case 'telegram':\n return <TelegramIcon className=\"w-5 h-5\" />;\n case 'youtube':\n case 'yt':\n return <YouTubeIcon className=\"w-5 h-5\" />;\n case 'instagram':\n case 'ig':\n return <InstagramIcon className=\"w-5 h-5\" />;\n case 'facebook':\n case 'fb':\n return <FacebookIcon className=\"w-5 h-5\" />;\n case 'copy':\n // CopyIcon's default fill is grey and would mismatch its row-mates —\n // force the themed foreground via the ODS token (white on the dark\n // theme, tracking the theme unlike the literal the reddit/slack cases\n // still carry).\n return <CopyIcon className=\"w-5 h-5\" color=\"var(--ods-text-primary)\" />;\n default:\n return <GlobeIcon className=\"w-5 h-5\" />;\n }\n}\n\nexport function SocialIconRow({ className = '', links = defaultLinks, variant, compact = false }: SocialIconRowProps) {\n // ── Compact design rationale ──────────────────────────────────────────\n // Page-level identity/share rows read as METADATA, not CTAs. The major\n // design systems converge on one recipe for this slot: a ~32px ghost icon\n // button with a ~16px glyph, tight 8px gap, transparent at rest, subtle\n // background state-layer on hover (Carbon \"ghost\" sm, Primer \"invisible\"\n // medium, shadcn ghost+icon-sm — all 32px; Material 3 \"standard\" icon\n // button = state-layer hover). Author headers on content platforms\n // (Medium / dev.to / Substack) use the same quiet treatment. Hence\n // compact: size=\"icon-sm\" + variant defaulting to \"transparent\" — the\n // hover affordance comes from the bg state layer (hover:bg-ods-bg-hover\n // inside the variant) because the brand icons carry fixed fills, not\n // currentColor. An explicit `variant` prop still wins (e.g. outline\n // chips). Non-compact keeps the legacy outline default + full-width\n // stretch untouched.\n const resolvedVariant = variant !== undefined ? variant : (compact ? 'transparent' : 'outline');\n return (\n <div className={`flex flex-row ${compact ? 'gap-2 w-fit' : 'gap-3 w-full'} ${className}`}>\n {links.map((link, index) => {\n const ariaLabel = link.label || link.platform;\n return link.onClick ? (\n <Button\n key={index}\n type=\"button\"\n variant={resolvedVariant}\n size={compact ? 'icon-sm' : 'icon'}\n className={compact ? undefined : 'flex-1'}\n aria-label={ariaLabel}\n onClick={link.onClick}\n >\n {renderSocialIcon(link.platform)}\n </Button>\n ) : (\n // Props-based linking — Button renders the anchor itself\n // (openInNewTab carries target=\"_blank\" + rel=\"noopener noreferrer\");\n // no asChild/<a> nesting.\n <Button\n key={index}\n variant={resolvedVariant}\n size={compact ? 'icon-sm' : 'icon'}\n className={compact ? undefined : 'flex-1'}\n href={link.href}\n openInNewTab\n aria-label={ariaLabel}\n >\n {renderSocialIcon(link.platform)}\n </Button>\n );\n })}\n </div>\n );\n}","\"use client\"\n\nimport { useSearchParams, useRouter } from \"../embed-shims/next-navigation\"\nimport { useTransition } from \"react\"\n\n/**\n * Unified AND Filter Logic\n * \n * This module provides common filtering logic that uses AND operations\n * instead of OR operations for all filter combinations.\n */\n\nexport interface FilterState {\n search?: string\n categories?: string[]\n subcategories?: string[]\n tags?: string[]\n filters?: string[]\n pricing?: string\n page?: number\n}\n\nexport interface FilterConfig {\n basePath: string\n supportedFilters: {\n categories?: boolean\n subcategories?: boolean\n tags?: boolean\n filters?: boolean\n pricing?: boolean\n search?: boolean\n }\n}\n\n/**\n * Hook for managing AND-based filter logic\n */\nexport function useUnifiedFiltering(config: FilterConfig) {\n const router = useRouter()\n const searchParams = useSearchParams()\n const [isPending, startTransition] = useTransition()\n\n // Parse current filter state from URL\n const getCurrentFilterState = (): FilterState => {\n const search = searchParams?.get('search') || undefined\n const categories = searchParams?.get('category')?.split(',').filter(Boolean) || []\n const subcategories = searchParams?.get('subcategory')?.split(',').filter(Boolean) || []\n const tags = searchParams?.get('tags')?.split(',').filter(Boolean) || []\n const filters = searchParams?.getAll('filter') || []\n const pricing = searchParams?.get('pricing') || undefined\n const page = parseInt(searchParams?.get('page') || \"1\")\n\n return {\n search,\n categories,\n subcategories,\n tags,\n filters,\n pricing,\n page\n }\n }\n\n // Update filters with AND logic\n const updateFilters = (newFilters: Partial<FilterState>, preserveScroll = false) => {\n const currentState = getCurrentFilterState()\n const updatedState = { ...currentState, ...newFilters }\n \n // Always reset to page 1 when filters change (unless explicitly preserving page)\n if (!newFilters.hasOwnProperty('page')) {\n updatedState.page = 1\n }\n\n const params = new URLSearchParams()\n\n // Add search parameter\n if (updatedState.search && config.supportedFilters.search) {\n params.set('search', updatedState.search)\n }\n\n // Add category parameters (AND logic - all must match)\n if (updatedState.categories && updatedState.categories.length > 0 && config.supportedFilters.categories) {\n params.set('category', updatedState.categories.join(','))\n }\n\n // Add subcategory parameters (AND logic - all must match)\n if (updatedState.subcategories && updatedState.subcategories.length > 0 && config.supportedFilters.subcategories) {\n params.set('subcategory', updatedState.subcategories.join(','))\n }\n\n // Add tag parameters (AND logic - all must match). Unified `?tags=` param\n // (matches blog/case-study/media/publication).\n if (updatedState.tags && updatedState.tags.length > 0 && config.supportedFilters.tags) {\n params.set('tags', updatedState.tags.join(','))\n }\n\n // Add filter parameters (AND logic - all must match)\n if (updatedState.filters && updatedState.filters.length > 0 && config.supportedFilters.filters) {\n updatedState.filters.forEach(filter => params.append('filter', filter))\n }\n\n // Add pricing parameter\n if (updatedState.pricing && config.supportedFilters.pricing) {\n params.set('pricing', updatedState.pricing)\n }\n\n // Add page parameter (only if not page 1)\n if (updatedState.page && updatedState.page > 1) {\n params.set('page', updatedState.page.toString())\n }\n\n const currentScrollY = preserveScroll ? window.scrollY : 0\n const newUrl = `${config.basePath}?${params.toString()}`\n\n startTransition(() => {\n router.push(newUrl, { scroll: false })\n \n if (preserveScroll) {\n setTimeout(() => {\n window.scrollTo({\n top: currentScrollY,\n behavior: 'smooth'\n })\n }, 100)\n }\n })\n }\n\n // Add a filter (AND logic)\n const addFilter = (\n type: keyof FilterState,\n value: string,\n preserveScroll = false\n ) => {\n const currentState = getCurrentFilterState()\n const updates: Partial<FilterState> = {}\n\n switch (type) {\n case 'categories':\n updates.categories = [...(currentState.categories || []), value]\n break\n case 'subcategories':\n updates.subcategories = [...(currentState.subcategories || []), value]\n break\n case 'tags':\n updates.tags = [...(currentState.tags || []), value]\n break\n case 'filters':\n updates.filters = [...(currentState.filters || []), value]\n break\n case 'pricing':\n updates.pricing = value\n break\n case 'search':\n updates.search = value\n break\n }\n\n updateFilters(updates, preserveScroll)\n }\n\n // Remove a filter (AND logic)\n const removeFilter = (\n type: keyof FilterState,\n value?: string,\n preserveScroll = true\n ) => {\n const currentState = getCurrentFilterState()\n const updates: Partial<FilterState> = {}\n\n switch (type) {\n case 'categories':\n updates.categories = value \n ? (currentState.categories || []).filter(c => c !== value)\n : []\n break\n case 'subcategories':\n updates.subcategories = value \n ? (currentState.subcategories || []).filter(s => s !== value)\n : []\n break\n case 'tags':\n updates.tags = value \n ? (currentState.tags || []).filter(t => t !== value)\n : []\n break\n case 'filters':\n updates.filters = value \n ? (currentState.filters || []).filter(f => f !== value)\n : []\n break\n case 'pricing':\n updates.pricing = undefined\n break\n case 'search':\n updates.search = undefined\n break\n }\n\n updateFilters(updates, preserveScroll)\n }\n\n // Toggle a filter (AND logic)\n const toggleFilter = (\n type: keyof FilterState,\n value: string,\n preserveScroll = false\n ) => {\n const currentState = getCurrentFilterState()\n let currentValues: string[] = []\n\n switch (type) {\n case 'categories':\n currentValues = currentState.categories || []\n break\n case 'subcategories':\n currentValues = currentState.subcategories || []\n break\n case 'tags':\n currentValues = currentState.tags || []\n break\n case 'filters':\n currentValues = currentState.filters || []\n break\n case 'pricing':\n // Pricing is single-select, so toggle on/off\n if (currentState.pricing === value) {\n removeFilter('pricing', value, preserveScroll)\n } else {\n addFilter('pricing', value, preserveScroll)\n }\n return\n case 'search':\n // Search is single-value, so just update\n updateFilters({ search: value }, preserveScroll)\n return\n }\n\n if (currentValues.includes(value)) {\n removeFilter(type, value, preserveScroll)\n } else {\n addFilter(type, value, preserveScroll)\n }\n }\n\n // Clear all filters\n const clearAllFilters = () => {\n startTransition(() => {\n router.push(config.basePath, { scroll: false })\n })\n }\n\n // Get filter chips for display\n const getFilterChips = (categories?: any[], tags?: any[]) => {\n const currentState = getCurrentFilterState()\n const chips: Array<{\n id: string\n label: string\n type: 'category' | 'subcategory' | 'tag' | 'filter' | 'pricing'\n }> = []\n\n // Category chips\n if (currentState.categories && categories) {\n currentState.categories.forEach(categorySlug => {\n const category = categories.find((c: any) => c.slug === categorySlug)\n if (category) {\n chips.push({\n id: `category-${categorySlug}`,\n label: category.name,\n type: 'category'\n })\n }\n })\n }\n\n // Subcategory chips\n if (currentState.subcategories && categories) {\n currentState.subcategories.forEach(subcategorySlug => {\n // Find subcategory across all categories\n for (const category of categories) {\n if (category.subcategories) {\n const subcategory = category.subcategories.find((s: any) => s.slug === subcategorySlug)\n if (subcategory) {\n chips.push({\n id: `subcategory-${subcategorySlug}`,\n label: subcategory.name,\n type: 'subcategory'\n })\n break\n }\n }\n }\n })\n }\n\n // Tag chips\n if (currentState.tags && tags) {\n currentState.tags.forEach(tagSlug => {\n const tag = tags.find((t: any) => t.slug === tagSlug)\n if (tag) {\n chips.push({\n id: `tag-${tagSlug}`,\n label: tag.name,\n type: 'tag'\n })\n }\n })\n }\n\n // Filter chips\n if (currentState.filters) {\n currentState.filters.forEach(filterKey => {\n chips.push({\n id: `filter-${filterKey}`,\n label: getFilterLabel(filterKey),\n type: 'filter'\n })\n })\n }\n\n // Pricing chip\n if (currentState.pricing) {\n chips.push({\n id: `pricing-${currentState.pricing}`,\n label: getPricingLabel(currentState.pricing),\n type: 'pricing'\n })\n }\n\n return chips\n }\n\n // Handle filter chip removal\n const handleFilterChipRemove = (chipId: string) => {\n const [type, ...idParts] = chipId.split('-')\n const id = idParts.join('-')\n\n switch (type) {\n case 'category':\n removeFilter('categories', id)\n break\n case 'subcategory':\n removeFilter('subcategories', id)\n break\n case 'tag':\n removeFilter('tags', id)\n break\n case 'filter':\n removeFilter('filters', id)\n break\n case 'pricing':\n removeFilter('pricing', id)\n break\n }\n }\n\n return {\n getCurrentFilterState,\n updateFilters,\n addFilter,\n removeFilter,\n toggleFilter,\n clearAllFilters,\n getFilterChips,\n handleFilterChipRemove,\n isPending\n }\n}\n\nimport { formatClassification, formatPricingModel } from '../utils/format-text-stub'\n\n// Helper functions for filter labels\nfunction getFilterLabel(filterKey: string): string {\n const customLabels: Record<string, string> = {\n 'recommended': 'Recommended',\n 'featured': 'Featured'\n }\n return customLabels[filterKey] || formatClassification(filterKey)\n}\n\nfunction getPricingLabel(pricingKey: string): string {\n return formatPricingModel(pricingKey)\n}\n\n/**\n * Vendor-specific filter configuration\n */\nexport const vendorFilterConfig: FilterConfig = {\n basePath: '/vendors',\n supportedFilters: {\n categories: true,\n subcategories: true,\n tags: false,\n filters: true,\n pricing: true,\n search: true\n }\n}\n\n/**\n * Blog-specific filter configuration\n */\nexport const blogFilterConfig: FilterConfig = {\n basePath: '/blog',\n supportedFilters: {\n categories: true,\n subcategories: false,\n tags: true,\n filters: false,\n pricing: false,\n search: true\n }\n} ","// Stub for format-text utilities\nexport function formatText(text: string): string {\n return text;\n}\n\nexport function truncateText(text: string, maxLength: number): string {\n if (text.length <= maxLength) return text;\n return text.slice(0, maxLength) + '...';\n}\n\nexport function formatClassification(classification: string): string {\n return classification.replace(/_/g, ' ').replace(/\\b\\w/g, l => l.toUpperCase());\n}\n\nexport function formatPricingModel(pricing: string): string {\n return pricing.replace(/_/g, ' ').replace(/\\b\\w/g, l => l.toUpperCase());\n}","\"use client\"\n\n// Components exports\nexport * from './chat'\nexport * from './docs'\nexport * from './embeds'\nexport * from './features'\nexport * from './icons'\nexport * from './navigation'\nexport * from './platform'\nexport * from './ui'\n\n// Individual component exports\nexport * from './announcement-bar'\nexport * from './hover-card'\nexport * from './categories-cart'\nexport * from './category-card'\nexport * from './comment-card'\nexport * from './content-loading-container'\nexport * from './dynamic-skeleton'\nexport * from './empty-state'\nexport * from './faq-accordion'\n// FaqSection sub-folder. Also exposed via the \"./components/faq\" subpath export\n// in package.json — that subpath is ALSO `\"use client\"` (tsup banner), so it\n// avoids dragging the rest of this root barrel but is NOT server-safe.\n//\n// Server Components that need the pure-fn JSON-LD builder MUST import from the\n// dedicated server-safe subpath \"./components/faq/json-ld\" (built without the\n// client banner under the server/universal block of tsup.config.ts).\nexport * from './faq'\nexport * from './related-content'\nexport * from './filter-chip'\nexport * from './footer'\nexport * from './unified-filter-logic'\nexport * from './unified-pagination'\nexport * from './footer-waitlist-button'\nexport * from './hero-image-uploader'\nexport * from './icons-block'\nexport * from './image-cropper'\nexport * from './media-carousel'\nexport * from './metric-value'\nexport * from './msp-display'\nexport * from './open-source-features'\nexport * from './pagination'\nexport * from './persistent-filter-controls'\nexport * from './persistent-pagination'\nexport * from './pricing-display'\nexport * from './results-count'\nexport * from './selection-source-badge'\nexport * from './social-icon-row'\nexport * from './user-display'\nexport * from './vendor-display-button'\nexport * from './vendor-icon'\nexport * from './vendor-page-skeleton'\nexport * from './vendor-tag'\nexport * from './why-it-matters'\nexport * from './yes-no-display'\n// Removed duplicate PageContainer export - already exported from './ui/page-container'\nexport * from './made-with-love'\n\n// Loading components\nexport * from './loading'\n\n// Auth-related exports\nexport * from './auth-stub'\n\n// Date/Time components\nexport * from './date-time-picker'\n\n// Chat components\nexport * from './chat'\n\n// Onboarding components\nexport * from './shared/onboarding'\n\n// Doc-search bar — unified RAG-search dropdown used by the data-room\n// sidebar AND the onboarding-guide catalog. Pure presentation; hosts\n// own the `useDocSearch` hook and pass results in as props.\nexport * from './shared/doc-search'\n\n// Product Release components\nexport * from './shared/product-release'\n\n// Dev-center shared components (Roadmap / Delivery / DevSectionView chrome)\nexport * from './shared/dev-section'\nexport * from './shared/roadmap'\nexport * from './shared/delivery'\n\n// Legal-document shared component (privacy policy, terms of service)\nexport * from './shared/legal-document'\n\n// Detail Page Skeleton\nexport { DetailPageSkeleton, type DetailPageSkeletonProps } from './shared/detail-page-skeleton'\n\n// Author byline card (end-of-article author description) — embeddable via\n// embed-shims + optional-runtime avatar proxying; hosts pass their own\n// fallbackBio copy (the hub uses defaultAuthorFallbackBio()).\nexport { ArticleAuthorByline, type ArticleAuthorBylineProps } from './shared/article-author-byline'\n\n// Read-only media gallery strip (horizontal scroll; images → lightbox, clips →\n// inline Video). Single source of truth for the detail-page media gallery —\n// used by product-release + What I Shipped detail pages.\nexport { MediaGalleryStrip, type MediaGalleryStripItem, type MediaGalleryStripProps } from './shared/media-gallery-strip'\n\n// Author detail-page body (identity + socials + bio + expertise, rail as\n// children) — the one implementation behind /authors/[slug] and embedded\n// author pages.\nexport { AuthorDetailView, type AuthorDetailViewProps } from './authors/author-detail-view'\n\n// Priority UI components that exist in main components directory\n// Note: These are re-exported from ./ui already, no need to duplicate\n","'use client';\n\nimport { useRouter } from '../embed-shims/next-navigation';\nimport { useChatRuntime } from '../contexts/chat-runtime-context';\nimport { executeNavigationImperative } from './chat/utils/execute-navigation';\nimport { useCallback } from 'react';\nimport { OpenFrameLogo } from './icons';\nimport { Button } from './ui/button';\n\nexport interface FooterWaitlistButtonProps {\n className?: string;\n}\n\n/**\n * Small wrapper around JoinWaitlistButton for use inside the footer.\n *\n * Routes through the host's unified-navigation hook\n * (`runtime.navigation.navigate`) when a `ChatRuntimeContext` is\n * mounted — that's the same path EVERY other in-app navigation\n * surface uses (source chips, inline cards, search-autocomplete,\n * action cards). One rule, one decision tree across the whole app.\n * The hub's `HubRuntimeProvider` wires `navigate` to its `useUnifiedNav`\n * helper, so this button picks up cross-platform new-tab decisions,\n * same-URL re-scroll handling, embed-mode short-circuiting, and any\n * future host-side nav rules for free.\n *\n * Falls back to the embed-shim's `router.push` when no runtime is\n * mounted (third-party embedders who haven't set up\n * `ChatRuntimeContext` — the lib stays usable without forcing them\n * to wire the full chat-runtime).\n *\n * Target URL: `/waitlist#top`. `#top` is the canonical \"scroll to\n * page top\" anchor — the destination page has an explicit\n * `<div id=\"top\">` at the top of `<main>` so native browser anchor\n * scroll works in every browser regardless of the HTML5 magic-anchor\n * behavior.\n */\nexport function FooterWaitlistButton({ className }: FooterWaitlistButtonProps) {\n const router = useRouter();\n const runtime = useChatRuntime();\n\n const handleClick = useCallback(() => {\n // The unified nav primitive: host `navigation.navigate` if wired, else the\n // embed-shim router; new-tab/embed decision handled internally.\n executeNavigationImperative({ runtime, href: '/waitlist#top', fallbackNavigate: router.push });\n }, [router, runtime]);\n\n return (\n <Button \n onClick={handleClick} \n className={className}\n leftIcon={<OpenFrameLogo />}\n >\n Join Waitlist\n </Button>\n );\n} ","\"use client\";\n\nimport { useRef, useState } from 'react';\nimport { Loader2, Image as ImageIcon, Upload, X } from 'lucide-react';\nimport { Button } from \"./ui/button\";\nimport { useToast } from \"../hooks/use-toast\";\n\ninterface HeroImageUploaderProps {\n /** Current image URL if one already exists */\n imageUrl?: string;\n /** Callback fired with new image URL (or undefined if removed) */\n onChange: (url: string | undefined) => void;\n /** Upload endpoint (required) */\n uploadEndpoint: string;\n /** Height of drop-zone. Number treated as pixels, string passed directly (e.g. '100%') */\n height?: number | string;\n /** Image object-fit, defaults to cover */\n objectFit?: 'cover' | 'contain' | 'fill' | 'none' | 'scale-down';\n /** Show a replace/upload button overlay in addition to remove (default true for parity with blog editor) */\n showReplaceButton?: boolean;\n /** If true, skip the actual upload and just return a base64 data URL preview. Useful for unauthenticated flows – the caller can upload later. */\n deferUpload?: boolean;\n /** Optional custom upload handler for authenticated uploads. If provided, this will be used instead of the default fetch */\n onUpload?: (file: File) => Promise<string>;\n /** Optional custom delete handler for authenticated deletion. If provided, this will be used instead of just clearing the image */\n onDelete?: () => Promise<void>;\n}\n\n/**\n * Reusable dashed hero-style image uploader identical to Blog Editor's hero picker.\n * Handles client-side validation (JPEG/PNG/WebP/GIF up to 5 MB), upload, preview & removal.\n */\nexport function HeroImageUploader({ imageUrl, onChange, uploadEndpoint, height = 300, objectFit = 'cover', showReplaceButton = true, deferUpload = false, onUpload, onDelete }: HeroImageUploaderProps) {\n const inputRef = useRef<HTMLInputElement>(null);\n const { toast } = useToast();\n const [uploading, setUploading] = useState(false);\n\n const ALLOWED_TYPES = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp', 'image/gif'];\n const MAX_SIZE = 5 * 1024 * 1024; // 5MB\n\n const openDialog = () => inputRef.current?.click();\n\n async function handleFile(file?: File) {\n if (!file) return;\n if (!ALLOWED_TYPES.includes(file.type)) {\n toast({ title: 'Invalid file', description: 'Upload JPEG, PNG, WebP, or GIF', variant: 'destructive' });\n return;\n }\n if (file.size > MAX_SIZE) {\n toast({ title: 'File too large', description: 'Max 5MB', variant: 'destructive' });\n return;\n }\n\n if (deferUpload) {\n // Immediately convert to data URL for preview and postpone real upload\n try {\n setUploading(true);\n const reader = new FileReader();\n reader.onload = () => {\n const dataUrl = reader.result as string;\n onChange(dataUrl); // Return data URL so parent can preview & store locally\n setUploading(false);\n };\n reader.onerror = () => {\n toast({ title: 'File error', description: 'Failed to read image file', variant: 'destructive' });\n setUploading(false);\n };\n reader.readAsDataURL(file);\n } catch (err: any) {\n toast({ title: 'File error', description: err.message || 'Failed to process image', variant: 'destructive' });\n setUploading(false);\n } finally {\n if (inputRef.current) inputRef.current.value = '';\n }\n return;\n }\n\n // Upload flow - use custom handler if provided, otherwise use default fetch\n setUploading(true);\n try {\n let uploadedUrl: string;\n \n if (onUpload) {\n // Use custom upload handler (e.g., for authenticated uploads)\n uploadedUrl = await onUpload(file);\n } else {\n // Default upload flow\n const fd = new FormData();\n fd.append('file', file);\n const res = await fetch(uploadEndpoint, { method: 'POST', body: fd });\n if (!res.ok) throw new Error('Upload failed');\n const json = await res.json();\n uploadedUrl = (json.data && json.data.url) || json.url || json.file_url;\n if (!uploadedUrl) throw new Error('Invalid upload response');\n }\n \n onChange(uploadedUrl);\n } catch (err: any) {\n toast({ title: 'Upload error', description: err.message || 'Failed to upload', variant: 'destructive' });\n } finally {\n setUploading(false);\n if (inputRef.current) inputRef.current.value = '';\n }\n }\n\n const handleSelect = (e: React.ChangeEvent<HTMLInputElement>) => {\n handleFile(e.target.files?.[0]);\n };\n\n const handleRemove = async () => {\n if (onDelete) {\n try {\n await onDelete();\n } catch (error) {\n // onDelete handler should handle its own error reporting\n return;\n }\n }\n onChange(undefined);\n };\n\n const heightStyle = typeof height === 'number' ? `${height}px` : height;\n\n return (\n <div className=\"w-full h-full max-h-full space-y-2 min-h-[300px]\">\n {imageUrl ? (\n <div className=\"relative group w-full aspect-square md:aspect-auto h-auto md:h-full flex items-center justify-center overflow-hidden\" style={{ height: heightStyle }}>\n <img src={imageUrl} className={`absolute inset-0 w-full h-full object-${objectFit}`} alt=\"Cover\" />\n <div className=\"absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 flex items-center justify-center gap-4 transition-opacity rounded-lg\">\n {showReplaceButton && (\n <Button\n variant=\"outline\"\n size=\"icon\"\n onClick={openDialog}\n className=\"bg-white text-black hover:bg-gray-100 rounded-full w-12 h-12\"\n >\n <Upload className=\"h-5 w-5\" />\n </Button>\n )}\n <Button\n variant=\"outline\"\n size=\"icon\"\n onClick={() => handleRemove()}\n className=\"bg-white text-black hover:bg-gray-100 rounded-full w-12 h-12\"\n >\n <X className=\"h-5 w-5\" />\n </Button>\n </div>\n </div>\n ) : (\n <div\n className={`w-full h-full border-2 border-dashed ${uploading ? 'border-ods-accent' : 'border-ods-border hover:border-ods-accent'} rounded-lg flex flex-col items-center justify-center cursor-pointer bg-ods-bg`}\n style={{ height: heightStyle }}\n onClick={openDialog}\n >\n {uploading ? (\n <Loader2 className=\"h-8 w-8 animate-spin text-ods-accent\" />\n ) : (\n <>\n <ImageIcon className=\"h-12 w-12 text-ods-text-secondary\" />\n <span className=\"text-ods-text-primary font-['DM_Sans'] text-[16px] font-medium mt-2\">Upload cover image</span>\n <span className=\"text-ods-text-secondary font-['DM_Sans'] text-[14px] mt-1\">Click to upload or drag and drop</span>\n <span className=\"text-ods-text-secondary font-['DM_Sans'] text-[12px]\">PNG, JPEG, WebP, GIF up to 5MB</span>\n </>\n )}\n </div>\n )}\n\n {/* hidden input */}\n <input ref={inputRef} type=\"file\" accept=\"image/*\" onChange={handleSelect} className=\"hidden\" />\n </div>\n );\n} ","\"use client\";\n\nimport React, { useState, useEffect, useRef } from 'react';\nimport {\n VendorDirectoryIcon,\n OpenSourceIcon,\n CommunityHubIcon,\n VendorsIcon,\n CommunityIcon,\n CompareIcon,\n} from './icons-stub';\nimport { Sun, Moon, CheckCircle, Github, PlusCircle } from 'lucide-react';\n\n// Map lucide icons\nconst SunIcon = Sun;\nconst MoonIcon = Moon;\nconst CheckCircleIcon = CheckCircle;\nconst GitHubIcon = Github;\nconst PlusCircleIcon = PlusCircle;\nconst OpenmspLogo = () => <div>Logo</div>;\n\ninterface IconsBlockProps {\n /**\n * When true, always render the loading placeholder bar regardless of whether the grid is ready.\n * Default behaviour renders placeholder only until the grid is generated.\n */\n loading?: boolean;\n}\n\n// Available icons array - moved outside component to prevent recreating on each render\nconst availableIcons = [\n VendorDirectoryIcon,\n OpenSourceIcon,\n CommunityHubIcon,\n VendorsIcon,\n CommunityIcon,\n CompareIcon,\n SunIcon,\n MoonIcon,\n CheckCircleIcon,\n GitHubIcon,\n PlusCircleIcon,\n OpenmspLogo\n];\n\nexport function ResponsiveIconsBlock({ loading = false }: IconsBlockProps) {\n const [columns, setColumns] = useState(24);\n const [iconGrid, setIconGrid] = useState<Array<Array<React.ComponentType<{ width?: number; height?: number; className?: string }>>>>([]);\n const [iconsLoaded, setIconsLoaded] = useState(false);\n const randomSeedRef = useRef<number>(0);\n\n useEffect(() => {\n function calculateColumns() {\n const cols = Math.ceil(window.innerWidth / 56) + 4;\n setColumns(cols);\n }\n\n calculateColumns();\n window.addEventListener('resize', calculateColumns);\n \n setIconsLoaded(true);\n \n return () => window.removeEventListener('resize', calculateColumns);\n }, []);\n\n // Generate icon grid only on client side\n useEffect(() => {\n if (randomSeedRef.current === 0) {\n randomSeedRef.current = Date.now();\n }\n\n const grid: any[][] = [];\n const totalCells = columns * 2;\n\n for (let idx = 0; idx < totalCells; idx++) {\n const col = idx % columns;\n const row = Math.floor(idx / columns);\n\n // Initialize row if needed\n if (!grid[row]) grid[row] = [];\n\n // Get adjacent icons to avoid\n const adjacentIcons = new Set();\n\n // Check left neighbor (same row, previous column)\n if (col > 0 && grid[row][col - 1]) {\n adjacentIcons.add(grid[row][col - 1]);\n }\n\n // Check top neighbor (previous row, same column)\n if (row > 0 && grid[row - 1] && grid[row - 1][col]) {\n adjacentIcons.add(grid[row - 1][col]);\n }\n\n // Filter available icons to exclude adjacent ones\n const availableOptions = availableIcons.filter(icon => !adjacentIcons.has(icon));\n\n // Generate random selection from available options using the stable seed\n const seed = randomSeedRef.current + idx;\n const pseudoRandom = (seed * 9301 + 49297) % 233280;\n const normalized = pseudoRandom / 233280;\n const iconIndex = Math.floor(normalized * availableOptions.length);\n\n // Assign selected icon to grid\n grid[row][col] = availableOptions[iconIndex] || availableIcons[0]; // Fallback to first icon\n }\n\n setIconGrid(grid);\n }, [columns]);\n\n const displayColumns = columns;\n\n // Get icon for specific position from pre-generated grid\n const getIconForIndex = (index: number) => {\n const col = index % displayColumns;\n const row = Math.floor(index / displayColumns);\n return iconGrid[row]?.[col] || availableIcons[0];\n };\n\n // When explicit loading prop true OR grid not ready → show placeholder\n if (loading || iconGrid.length === 0) {\n return (\n <div\n className=\"w-full h-[80px] md:h-[112px] bg-[#1A1A1A] relative overflow-hidden\"\n role=\"presentation\"\n aria-hidden=\"true\"\n >\n {/* subtle pulse bar */}\n <div className=\"absolute inset-0 animate-pulse bg-[#2A2A2A]/60\" />\n </div>\n );\n }\n\n return (\n <div\n className=\"w-full h-[80px] md:h-[112px] overflow-hidden bg-[#1A1A1A] relative z-10\"\n style={{ margin: 0, padding: 0 }}\n role=\"presentation\"\n aria-hidden=\"true\"\n >\n <style>{`\n .icons-block svg,\n .icons-block svg * {\n filter: grayscale(100%) brightness(0) invert(1) brightness(0.4) !important;\n fill: currentColor !important;\n }\n `}</style>\n <div\n className=\"grid h-full icons-block w-full\"\n style={{\n gridTemplateColumns: `repeat(${displayColumns}, 56px)`,\n gridTemplateRows: 'repeat(2, 1fr)',\n }}\n >\n {Array.from({ length: displayColumns * 2 }).map((_, idx) => {\n const col = idx % displayColumns;\n const row = Math.floor(idx / displayColumns);\n const IconComponent = getIconForIndex(idx);\n\n return (\n <div\n key={idx}\n className=\"flex items-center justify-center w-full h-full\"\n style={{\n background: '#1A1A1A',\n borderRight: col !== displayColumns - 1 ? '0.5px solid rgba(66, 66, 66, 0.5)' : undefined,\n borderBottom: row === 0 ? '0.5px solid rgba(66, 66, 66, 0.5)' : undefined,\n margin: 0,\n padding: '8px',\n boxSizing: 'border-box',\n }}\n role=\"presentation\"\n >\n <IconComponent\n width={16}\n height={16}\n className=\"text-[#666666] md:w-5 md:h-5\"\n aria-hidden=\"true\"\n />\n </div>\n );\n })}\n </div>\n </div>\n );\n} ","// Stub implementations for missing icons\nimport { DollarSign, Code, Users, Building, GitCompare, MessageSquare } from \"lucide-react\";\n\nexport const OpenSourceIcon = Code;\nexport const CoinsIcon = DollarSign;\nexport const VendorDirectoryIcon = Building;\nexport const CommunityHubIcon = Users;\nexport const VendorsIcon = Building;\nexport const CommunityIcon = MessageSquare;\nexport const CompareIcon = GitCompare;\n\n// OpenmspLogo moved to real implementation (should be imported from main project)\n\n// OpenFrameLogo moved to real implementation in ./openframe-logo.tsx","/*\n ImageCropper.tsx\n ----------------\n Reusable React component for interactive image cropping with:\n • Drag / resize cropping via react-easy-crop\n • Custom aspect ratio or free-form\n • Optional circular crop overlay (avatar mode)\n • Automatic scaling so the exported image never exceeds maxSizePx\n • Returns PNG data URL + Blob on confirm\n • Responsive / accessible with shadcn/ui Button & Slider components\n\n Styling relies on Tailwind + shadcn design tokens.\n*/\n\n\"use client\"\n\nimport React, { useCallback, useEffect, useMemo, useRef, useState } from \"react\"\nimport Cropper from \"react-easy-crop\"\nimport { Button } from \"./ui/button\"\nimport { Slider } from \"./ui/slider\"\nimport { cn } from \"../utils/cn\"\nimport { Check, RotateCcw } from \"lucide-react\"\n\n/* ------------------------------------------------------------\n * Types\n * ----------------------------------------------------------*/\n\nexport interface ImageCropperResult {\n /** Cropped PNG data URL */\n dataUrl: string\n /** Corresponding PNG Blob */\n blob: Blob\n}\n\nexport interface ImageCropperProps {\n /** Source image (URL or data URI) */\n src: string\n /** Called when user confirms crop */\n onComplete(result: ImageCropperResult): void\n /** Called when user cancels crop */\n onCancel?(): void\n /** Aspect ratio (width / height). If omitted, free-form */\n aspectRatio?: number\n /** Enable circular crop overlay for avatars */\n circular?: boolean\n /** Maximum width/height for the exported PNG (defaults 512) */\n maxSizePx?: number\n /** Optional className for wrapper */\n className?: string\n}\n\n/* ------------------------------------------------------------\n * Helpers\n * ----------------------------------------------------------*/\n\nfunction degToRad(deg: number) {\n return (deg * Math.PI) / 180\n}\n\n/** Util to create an HTMLImageElement that resolves when loaded */\nfunction loadImage(src: string): Promise<HTMLImageElement> {\n return new Promise((resolve, reject) => {\n const img = new Image()\n img.crossOrigin = \"anonymous\" // prevent canvas tainting\n img.onload = () => resolve(img)\n img.onerror = () => reject(new Error(\"Failed to load image\"))\n img.src = src\n })\n}\n\n/* ------------------------------------------------------------\n * Component\n * ----------------------------------------------------------*/\n\nexport const ImageCropper: React.FC<ImageCropperProps> = ({\n src,\n onComplete,\n onCancel,\n aspectRatio,\n circular = false,\n maxSizePx = 512,\n className,\n}) => {\n const [crop, setCrop] = useState({ x: 0, y: 0 })\n const [zoom, setZoom] = useState(1)\n const [rotation, setRotation] = useState(0)\n const [croppedAreaPixels, setCroppedAreaPixels] = useState<\n | { x: number; y: number; width: number; height: number }\n | null\n >(null)\n\n /* ------------------ crop complete callback -----------------*/\n const onCropComplete = useCallback((_: any, area: any) => {\n setCroppedAreaPixels(area)\n }, [])\n\n /* ------------------ Build checkered background --------------*/\n const checkerBg =\n \"bg-[length:16px_16px] bg-[linear-gradient(45deg,transparent_25%,#2a2a2a_25%,#2a2a2a_75%,transparent_75%,transparent),linear-gradient(45deg,#2a2a2a_25%,transparent_25%,transparent_75%,#2a2a2a_75%,#2a2a2a)]\"\n\n /* ------------------ Export logic ---------------------------*/\n const exportCrop = useCallback(async () => {\n if (!croppedAreaPixels) return undefined\n\n const img = await loadImage(src)\n\n // Create canvas the size of crop\n const canvas = document.createElement(\"canvas\")\n\n // Scale crop to fit maxSizePx\n const scale = Math.min(1, maxSizePx / Math.max(croppedAreaPixels.width, croppedAreaPixels.height))\n const outputW = Math.round(croppedAreaPixels.width * scale)\n const outputH = Math.round(croppedAreaPixels.height * scale)\n\n canvas.width = outputW\n canvas.height = outputH\n const ctx = canvas.getContext(\"2d\")!\n\n // Draw cropped portion\n ctx.drawImage(\n img,\n croppedAreaPixels.x,\n croppedAreaPixels.y,\n croppedAreaPixels.width,\n croppedAreaPixels.height,\n 0,\n 0,\n outputW,\n outputH,\n )\n\n // If circular mode, clip to circle\n if (circular) {\n const temp = document.createElement(\"canvas\")\n temp.width = outputW\n temp.height = outputH\n const tctx = temp.getContext(\"2d\")!\n tctx.beginPath()\n tctx.arc(outputW / 2, outputH / 2, outputW / 2, 0, Math.PI * 2)\n tctx.closePath()\n tctx.clip()\n tctx.drawImage(canvas, 0, 0)\n canvas.width = outputW\n canvas.height = outputH\n ctx.clearRect(0, 0, outputW, outputH)\n ctx.drawImage(temp, 0, 0)\n }\n\n return new Promise<ImageCropperResult>((resolve) => {\n canvas.toBlob(\n (blob) => {\n if (!blob) throw new Error(\"Canvas export failed\")\n const reader = new FileReader()\n reader.onloadend = () => {\n resolve({ dataUrl: reader.result as string, blob })\n }\n reader.readAsDataURL(blob)\n },\n \"image/png\",\n )\n })\n }, [croppedAreaPixels, circular, maxSizePx, src]) as () => Promise<ImageCropperResult | undefined>\n\n /* ------------------ Keyboard accessibility -----------------*/\n const handleKey = (e: React.KeyboardEvent) => {\n // Enter to confirm, Esc to cancel\n if (e.key === \"Enter\") {\n e.preventDefault()\n void exportCrop().then((res) => {\n if (res) onComplete(res)\n })\n } else if (e.key === \"Escape\") {\n e.preventDefault()\n onCancel?.()\n }\n }\n\n /* ------------------ Render ---------------------------------*/\n return (\n <div\n className={cn(\n \"relative flex flex-col gap-4 w-full\",\n className,\n )}\n onKeyDown={handleKey}\n tabIndex={0}\n aria-label=\"Image cropper\"\n >\n {/* Cropper container */}\n <div\n className={cn(\n \"relative w-full aspect-square md:aspect-video rounded-md overflow-hidden\",\n checkerBg,\n )}\n >\n <Cropper\n image={src}\n crop={crop}\n zoom={zoom}\n rotation={rotation}\n aspect={aspectRatio}\n cropShape={circular ? \"round\" : \"rect\"}\n showGrid={false}\n onCropChange={setCrop}\n onZoomChange={setZoom}\n onRotationChange={setRotation}\n onCropComplete={onCropComplete}\n objectFit=\"contain\"\n />\n </div>\n\n {/* Controls */}\n <div className=\"flex flex-col gap-4\">\n {/* Zoom */}\n <div className=\"flex items-center gap-3\">\n <span className=\"min-w-[60px] text-sm\">Zoom</span>\n <Slider\n min={1}\n max={3}\n step={0.01}\n value={[zoom]}\n onValueChange={(v) => setZoom(v[0])}\n aria-label=\"Zoom\"\n className=\"flex-1\"\n />\n </div>\n {/* Rotate */}\n <div className=\"flex items-center gap-3\">\n <span className=\"min-w-[60px] text-sm\">Rotate</span>\n <Slider\n min={0}\n max={360}\n step={1}\n value={[rotation]}\n onValueChange={(v) => setRotation(v[0])}\n aria-label=\"Rotation\"\n className=\"flex-1\"\n />\n <Button\n variant=\"outline\"\n size=\"icon\"\n onClick={() => setRotation(0)}\n aria-label=\"Reset rotation\"\n >\n <RotateCcw className=\"h-4 w-4\" />\n </Button>\n </div>\n </div>\n\n {/* Action buttons */}\n <div className=\"flex justify-end gap-2 mt-2\">\n {onCancel && (\n <Button variant=\"outline\" onClick={onCancel} aria-label=\"Cancel crop\">\n Cancel\n </Button>\n )}\n <Button\n variant=\"accent\"\n onClick={async () => {\n const result = await exportCrop()\n if (result) onComplete(result)\n }}\n leftIcon={<Check className=\"h-4 w-4\" />}\n aria-label=\"Apply crop\"\n >\n Apply\n </Button>\n </div>\n </div>\n )\n} ","\"use client\"\n\nimport React from 'react';\nimport { cn } from \"../utils/cn\";\n\ninterface MetricValueProps {\n value: string | number;\n label: string;\n className?: string;\n}\n\n/**\n * Displays a numeric/short textual value followed by a smaller grey label.\n * Example: 30s Generation Time\n */\nexport function MetricValue({ value, label, className }: MetricValueProps) {\n return (\n <div className={cn('flex items-end gap-2 whitespace-nowrap font-[\"DM_Sans\"] text-lg leading-[24px] text-ods-text-primary', className)}>\n {value}\n <span className=\"text-sm text-ods-text-secondary\">\n {label}\n </span>\n </div>\n );\n} ","\"use client\"\n\nimport React from 'react';\nimport { cn } from \"../utils/cn\";\nimport { SquareAvatar } from './square-avatar';\n\ninterface MSPDisplayProps {\n name: string;\n logoUrl?: string | null;\n size?: number; // avatar size in px (square)\n className?: string;\n}\n\nexport function MSPDisplay({ name, logoUrl, size = 40, className }: MSPDisplayProps) {\n return (\n <div className={cn('flex items-center gap-2 min-w-0', className)}>\n <SquareAvatar src={logoUrl ?? undefined} fallbackName={name} size={size} />\n <h2 className=\"truncate pl-2\">\n {name}\n </h2>\n </div>\n );\n} ","\"use client\";\n\nimport React from 'react';\nimport { cn } from \"../utils/cn\";\nimport { getProxiedImageUrl } from '../utils/image-proxy';\n\ninterface SquareAvatarProps {\n /** Image URL (if null/undefined, renders initials fallback) */\n src?: string | null;\n /** Fallback name used to derive initials when no image */\n fallbackName?: string;\n /** Size in px (applied to width & height). Defaults to 56 (Tailwind w-14 h-14). */\n size?: number;\n /** If true, avatar takes full width with square aspect ratio */\n fullWidth?: boolean;\n className?: string;\n}\n\n/**\n * Square avatar with rounded edges used across cards / dashboards.\n * Automatically shows image (via proxied URL) or initials fallback.\n */\nexport function SquareAvatar({ src, fallbackName = '', size = 56, fullWidth = false, className }: SquareAvatarProps) {\n const initials = React.useMemo(() => {\n if (!fallbackName) return '';\n return fallbackName\n .split(' ')\n .map((n) => n.charAt(0))\n .join('')\n .slice(0, 2)\n .toUpperCase();\n }, [fallbackName]);\n\n const style: React.CSSProperties = fullWidth ? {} : { width: size, height: size };\n\n return (\n <div\n className={cn(\n 'rounded-lg border border-ods-border flex items-center justify-center overflow-hidden bg-ods-bg-secondary',\n fullWidth ? 'w-full aspect-square' : 'flex-shrink-0',\n className,\n )}\n style={style}\n >\n {src ? (\n // eslint-disable-next-line @next/next/no-img-element\n <img src={getProxiedImageUrl(src) || src} alt=\"Avatar\" className=\"object-cover w-full h-full\" />\n ) : (\n <span className={cn(\n \"font-['DM_Sans'] text-ods-text-primary font-bold\",\n fullWidth ? 'text-4xl' : 'text-lg'\n )}>\n {initials}\n </span>\n )}\n </div>\n );\n} ","\"use client\"\n\nimport React from 'react';\nimport { Terminal, DollarSign, Network, Users } from 'lucide-react';\nimport { SECTION_HEADING_CLASS } from './layout/page-heading';\n\ninterface FeatureCardProps {\n icon: React.ReactNode;\n title: string;\n description: string;\n}\n\nconst FeatureCard: React.FC<FeatureCardProps> = ({ icon, title, description }) => {\n return (\n <div className=\"bg-ods-card border border-ods-border rounded-3xl p-6 flex flex-col gap-6 h-full hover:bg-ods-bg-hover transition-colors duration-200\">\n {/* Icon Container */}\n <div className=\"w-12 h-12 bg-ods-bg border border-ods-border rounded flex items-center justify-center\">\n <div className=\"w-6 h-6 text-ods-text-secondary\">\n {icon}\n </div>\n </div>\n \n {/* Text Container */}\n <div className=\"flex flex-col gap-2\">\n <h3 className=\"text-h3 text-ods-text-primary tracking-[-0.36px]\">\n {title}\n </h3>\n <p className=\"text-h4 text-ods-text-primary\">\n {description}\n </p>\n </div>\n </div>\n );\n};\n\nconst OpenSourceFeatures: React.FC = () => {\n const features = [\n {\n icon: <Terminal className=\"w-6 h-6\" />,\n title: \"Built on FOSS\",\n description: \"No black boxes. No hidden fees. Just transparent, community-driven software you control.\"\n },\n {\n icon: <DollarSign className=\"w-6 h-6\" />,\n title: \"Own Your Stack\",\n description: \"Replace overpriced, proprietary tools with open, auditable, and customizable components.\"\n },\n {\n icon: <Network className=\"w-6 h-6\" />,\n title: \"Modular by Design\",\n description: \"Add, remove, or extend features with ease — OpenFrame adapts to how you work.\"\n },\n {\n icon: <Users className=\"w-6 h-6\" />,\n title: \"Community-Powered\",\n description: \"Developed with and for MSPs by a global open-source community. You're not just a user — you're part of the roadmap.\"\n }\n ];\n\n return (\n <section className=\"w-full bg-ods-bg py-12 md:py-16 lg:py-20\">\n <div className=\"w-full max-w-[1920px] mx-auto px-6 md:px-20\">\n {/* Section Title */}\n <div className=\"flex flex-col items-center gap-10\">\n <h2 className={`${SECTION_HEADING_CLASS} text-center w-full`}>\n <span className=\"text-ods-accent\">100%</span>\n <span> Open-Source. </span>\n <span className=\"text-ods-accent\">0%</span>\n <span> Bullsh*t.</span>\n </h2>\n \n {/* Features Grid */}\n <div className=\"w-full\">\n <div className=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6\">\n {features.map((feature, index) => (\n <FeatureCard\n key={index}\n icon={feature.icon}\n title={feature.title}\n description={feature.description}\n />\n ))}\n </div>\n </div>\n </div>\n </div>\n </section>\n );\n};\n\nexport default OpenSourceFeatures; ","\"use client\"\n\nimport { ReactNode } from \"react\"\nimport { cn } from \"../utils/cn\"\n\ninterface PersistentPaginationProps {\n /**\n * Whether pagination is currently in a loading state\n */\n isLoading: boolean\n /**\n * The pagination component\n */\n children: ReactNode\n /**\n * Current page number\n */\n currentPage: number\n /**\n * Total number of pages\n */\n totalPages: number\n /**\n * Additional CSS classes\n */\n className?: string\n /**\n * Disabled opacity (0-1)\n */\n disabledOpacity?: number\n /**\n * Transition duration in milliseconds\n */\n transitionDuration?: number\n /**\n * Whether to show loading state in pagination\n */\n showLoadingState?: boolean\n}\n\n/**\n * PersistentPagination\n * \n * A wrapper component that keeps pagination visible during loading states\n * but provides visual feedback that it's temporarily disabled.\n * \n * Features:\n * - Maintains pagination visibility during content loading\n * - Disables interaction but preserves layout\n * - Shows current page context even when loading\n * - Provides accessibility support for loading states\n * - Smooth transitions between enabled/disabled states\n * \n * Usage:\n * ```tsx\n * <PersistentPagination\n * isLoading={isLoadingVendors}\n * currentPage={currentPage}\n * totalPages={totalPages}\n * >\n * <Pagination currentPage={currentPage} totalPages={totalPages} />\n * </PersistentPagination>\n * ```\n */\nexport function PersistentPagination({\n isLoading,\n children,\n currentPage,\n totalPages,\n className,\n disabledOpacity = 0.5,\n transitionDuration = 300,\n showLoadingState = true,\n}: PersistentPaginationProps) {\n // ALWAYS show pagination for predictable layout - just gray it out when not needed\n // Removed condition that hides pagination completely\n\n return (\n <div \n className={cn(\n \"relative transition-all ease-in-out\",\n \"flex justify-center items-center\",\n isLoading && \"pointer-events-none\",\n className\n )}\n style={{\n opacity: isLoading ? disabledOpacity : 1,\n transitionDuration: `${transitionDuration}ms`\n }}\n role=\"navigation\"\n aria-label=\"Pagination\"\n aria-busy={isLoading}\n data-loading={isLoading}\n >\n {/* Loading state announcement for screen readers */}\n {isLoading && (\n <div \n className=\"sr-only\" \n role=\"status\" \n aria-live=\"polite\"\n >\n Pagination temporarily disabled while loading page {currentPage} of {totalPages}\n </div>\n )}\n\n {/* Pagination controls with disabled state */}\n <div \n className={cn(\n \"relative transition-all ease-in-out\",\n isLoading && \"cursor-not-allowed\"\n )}\n style={{\n transitionDuration: `${transitionDuration}ms`\n }}\n aria-hidden={isLoading}\n >\n {children}\n </div>\n\n {/* REMOVED: Loading overlay - only card skeletons should show during loading */}\n </div>\n )\n}\n\n/**\n * Hook for managing pagination loading states\n */\nexport function usePaginationLoading(\n isLoading: boolean,\n currentPage: number,\n totalPages: number\n) {\n const shouldShowPagination = totalPages > 1 || isLoading\n \n const paginationProps = {\n 'aria-busy': isLoading,\n 'data-loading': isLoading,\n 'data-current-page': currentPage,\n 'data-total-pages': totalPages,\n }\n\n const getLoadingMessage = () => {\n if (isLoading) {\n return `Loading page ${currentPage} of ${totalPages}`\n }\n return `Page ${currentPage} of ${totalPages}`\n }\n\n return {\n shouldShowPagination,\n paginationProps,\n getLoadingMessage,\n }\n}\n\n/**\n * Enhanced pagination component that includes loading states\n */\ninterface PersistentPaginationWrapperProps {\n isLoading: boolean\n currentPage: number\n totalPages: number\n onPageChange?: (page: number) => void\n className?: string\n variant?: 'vendor' | 'blog'\n}\n\nexport function PersistentPaginationWrapper({\n isLoading,\n currentPage,\n totalPages,\n onPageChange,\n className,\n variant = 'vendor'\n}: PersistentPaginationWrapperProps) {\n const { getLoadingMessage } = usePaginationLoading(\n isLoading,\n currentPage,\n totalPages\n )\n\n // ALWAYS show pagination for predictable layout\n // For no results (totalPages = 0), show grayed out pagination\n const hasResults = totalPages > 0\n const displayTotalPages = hasResults ? totalPages : 1\n const displayCurrentPage = hasResults ? currentPage : 1\n // Only disable during loading, but still show when no results (just grayed out)\n const isPaginationDisabled = isLoading\n const hasNoResults = !hasResults && !isLoading\n\n // Use UnifiedPagination directly from ui-kit instead of importing from consuming apps\n // Both Pagination and BlogPagination in multi-platform-hub are just wrappers around UnifiedPagination\n const PaginationComponent = require('./unified-pagination').UnifiedPagination\n\n return (\n <div \n className={cn(\n \"relative transition-all ease-in-out flex justify-center items-center\",\n (isPaginationDisabled || hasNoResults) && \"pointer-events-none\",\n className\n )}\n style={{\n opacity: isPaginationDisabled ? 0.3 : hasNoResults ? 0.5 : 1,\n transitionDuration: \"300ms\"\n }}\n role=\"navigation\"\n aria-label=\"Pagination\"\n aria-busy={isLoading}\n data-loading={isLoading}\n data-has-results={hasResults}\n >\n {/* Loading/no results state announcement for screen readers */}\n {(isLoading || !hasResults) && (\n <div \n className=\"sr-only\" \n role=\"status\" \n aria-live=\"polite\"\n >\n {isLoading \n ? `Pagination temporarily disabled while loading page ${displayCurrentPage} of ${displayTotalPages}`\n : `No results available - pagination disabled`\n }\n </div>\n )}\n\n {/* Pagination controls with disabled state */}\n <div \n className={cn(\n \"relative transition-all ease-in-out\",\n isPaginationDisabled && \"cursor-not-allowed\"\n )}\n style={{\n transitionDuration: \"300ms\"\n }}\n aria-hidden={isPaginationDisabled}\n >\n <PaginationComponent\n currentPage={displayCurrentPage}\n totalPages={displayTotalPages}\n onPageChange={hasResults ? onPageChange : () => {}} // Provide empty function instead of undefined\n />\n </div>\n\n {/* REMOVED: Loading overlays - only card skeletons should show during loading */}\n </div>\n )\n} ","\"use client\"\n\nimport { StructuredPricingSummary as StructuredPricingItem } from '../utils/compare-utils-stub'\n\n// Using StructuredPricingItem from compare-utils instead of local interface\n\ninterface PricingStyleConfig {\n priceTextSize: string\n priceTextColor: string\n secondaryTextSize: string\n secondaryTextColor: string\n showTildePrefix: boolean\n fontFamily?: string\n}\n\ninterface PricingDisplayProps {\n pricing: StructuredPricingItem[] | string | StructuredPricingItem // Support both new structure and legacy string\n className?: string\n styleConfig?: PricingStyleConfig\n}\n\n// Default style configurations for different contexts\nexport const PRICING_STYLES = {\n // Comparison table style (current default)\n comparison: {\n priceTextSize: 'text-[16px]',\n priceTextColor: 'text-ods-text-primary',\n secondaryTextSize: 'text-[16px]',\n secondaryTextColor: 'text-ods-text-secondary',\n showTildePrefix: false,\n fontFamily: \"font-['DM_Sans']\"\n },\n // Vendor dropdown compact style (Figma design)\n compact: {\n priceTextSize: 'text-[12px]',\n priceTextColor: 'text-ods-text-secondary',\n secondaryTextSize: 'text-[12px]',\n secondaryTextColor: 'text-ods-text-secondary',\n showTildePrefix: true,\n fontFamily: \"font-['DM_Sans']\"\n },\n // Card style for vendor cards\n card: {\n priceTextSize: 'text-[14px]',\n priceTextColor: 'text-ods-text-primary',\n secondaryTextSize: 'text-[14px]',\n secondaryTextColor: 'text-ods-text-secondary',\n showTildePrefix: true,\n fontFamily: \"font-['DM_Sans']\"\n }\n} as const\n\n/**\n * Shared component for consistent pricing display with configurable styling\n * Now accepts structured pricing data and style configuration for better control\n */\nexport function PricingDisplay({ \n pricing, \n className = \"\", \n styleConfig = PRICING_STYLES.comparison \n}: PricingDisplayProps) {\n // Legacy support for string input\n if (typeof pricing === 'string') {\n return <LegacyPricingDisplay pricing={pricing} className={className} styleConfig={styleConfig} />\n }\n \n // Convert to array if single item\n const pricingArray = Array.isArray(pricing) ? pricing : [pricing];\n \n // Handle empty pricing\n if (!pricingArray || pricingArray.length === 0) {\n return (\n <span className={`${styleConfig.priceTextColor} ${styleConfig.priceTextSize} ${styleConfig.fontFamily} ${className}`}>\n No pricing data\n </span>\n )\n }\n \n // Handle single pricing item\n if (pricingArray.length === 1) {\n const item = pricingArray[0]\n const price = item.ranges?.[0]?.min || 0;\n const unit = item.ranges?.[0]?.unit;\n return (\n <span className={`${styleConfig.fontFamily} ${className}`}>\n <span className={`${styleConfig.priceTextColor} ${styleConfig.priceTextSize}`}>\n {formatPriceValue(price, styleConfig.showTildePrefix)}\n </span>\n {unit && (\n <span className={`${styleConfig.secondaryTextColor} ${styleConfig.secondaryTextSize}`}>\n /{unit}\n </span>\n )}\n </span>\n )\n }\n \n // Handle multiple pricing items\n const priceValues = pricingArray.map(item => formatPriceValue(item.ranges?.[0]?.min || 0, styleConfig.showTildePrefix))\n \n // Find the first item that has unit info\n const itemWithUnit = pricingArray.find(item => item.ranges?.[0]?.unit)\n \n return (\n <span className={`${styleConfig.fontFamily} ${className}`}>\n <span className={`${styleConfig.priceTextColor} ${styleConfig.priceTextSize}`}>\n {priceValues.join(' | ')}\n </span>\n {itemWithUnit && itemWithUnit.ranges?.[0]?.unit && (\n <span className={`${styleConfig.secondaryTextColor} ${styleConfig.secondaryTextSize}`}>\n /{itemWithUnit.ranges[0].unit}\n </span>\n )}\n </span>\n )\n}\n\n/**\n * Format price value consistently with configurable tilde prefix\n */\nfunction formatPriceValue(price: number | 'Free' | 'Contact', showTildePrefix: boolean = false): string {\n if (price === 'Free' || price === 'Contact') {\n return price\n }\n if (price === 0) {\n return 'Free'\n }\n return showTildePrefix ? `~$${price}` : `$${price}`\n}\n\n/**\n * Legacy component for backward compatibility with string input\n */\nfunction LegacyPricingDisplay({ \n pricing, \n className = \"\", \n styleConfig = PRICING_STYLES.comparison \n}: { \n pricing: string; \n className?: string; \n styleConfig?: PricingStyleConfig \n}) {\n // Handle \"Free\" case\n if (pricing === 'Free' || pricing === 'No pricing data') {\n return (\n <span className={`${styleConfig.priceTextColor} ${styleConfig.priceTextSize} ${styleConfig.fontFamily} ${className}`}>\n {pricing}\n </span>\n )\n }\n \n // Parse pricing string to separate main price from unit/cycle info\n const parsePricing = (pricingStr: string) => {\n // Handle comma-separated format like \"$10/device/month, $120/device/year\"\n if (pricingStr.includes(', ')) {\n // Split by comma and parse each part separately\n const parts = pricingStr.split(', ')\n const parsedParts = parts.map(part => {\n const match = part.trim().match(/^(\\$\\d+(?:-\\$\\d+)?|\\d+(?:-\\d+)?|Free)(.*)$/)\n if (match) {\n const price = match[1].startsWith('$') ? match[1] : `$${match[1]}`\n return {\n price,\n suffix: match[2]\n }\n }\n return { price: part.trim(), suffix: '' }\n })\n \n // Reconstruct with proper styling\n return {\n mainValue: parsedParts.map(p => p.price).join(', '),\n secondaryInfo: parsedParts.length > 0 && parsedParts[0].suffix ? parsedParts[0].suffix : ''\n }\n }\n \n // Handle pipe-separated format like \"$529/site/year | Free\"\n if (pricingStr.includes(' | ')) {\n const parts = pricingStr.split(' | ')\n const prices = parts.map(part => {\n const match = part.trim().match(/^(\\$\\d+(?:-\\$\\d+)?|\\d+(?:-\\d+)?|Free)(.*)$/)\n return match ? match[1] : part.trim()\n })\n \n return {\n mainValue: prices.join(' | '),\n secondaryInfo: ''\n }\n }\n \n // Handle multi-cycle format like \"$10/$120/device/month/year\"\n if (pricingStr.includes('/$')) {\n // Find all price patterns\n const pricePattern = /\\$\\d+(?:-\\$\\d+)?/g\n const matches = [...pricingStr.matchAll(pricePattern)]\n \n if (matches.length > 0) {\n const lastMatch = matches[matches.length - 1]\n const lastPriceEnd = lastMatch.index! + lastMatch[0].length\n \n const mainValue = pricingStr.substring(0, lastPriceEnd)\n const secondaryInfo = pricingStr.substring(lastPriceEnd)\n \n return { mainValue, secondaryInfo }\n }\n }\n \n // Handle single price format like \"$529/site/year\" or \"$0-$529/site/year\"\n const singlePriceMatch = pricingStr.match(/^(\\$\\d+(?:-\\$\\d+)?|Free)(.*)$/)\n if (singlePriceMatch) {\n return {\n mainValue: singlePriceMatch[1],\n secondaryInfo: singlePriceMatch[2]\n }\n }\n \n // Fallback - treat entire string as main value\n return {\n mainValue: pricingStr,\n secondaryInfo: ''\n }\n }\n \n const { mainValue, secondaryInfo } = parsePricing(pricing)\n \n return (\n <span className={`${styleConfig.fontFamily} ${className}`}>\n <span className={`${styleConfig.priceTextColor} ${styleConfig.priceTextSize}`}>{mainValue}</span>\n {secondaryInfo && (\n <span className={`${styleConfig.secondaryTextColor} ${styleConfig.secondaryTextSize}`}>{secondaryInfo}</span>\n )}\n </span>\n )\n}\n\n/**\n * Utility function to format pricing for display in the PricingDisplay component\n * This can be used to pre-process pricing strings if needed\n */\nexport function formatPricingForDisplay(pricing: string): string {\n return pricing\n} ","\"use client\"\n\nexport interface ResultsCountProps {\n currentPage: number\n pageSize: number\n totalResults: number\n resultType: 'vendors' | 'posts'\n sortingMessage?: string\n}\n\nexport function ResultsCount({\n currentPage,\n pageSize,\n totalResults,\n resultType,\n sortingMessage\n}: ResultsCountProps) {\n if (totalResults === 0) {\n return null\n }\n\n const startIndex = ((currentPage - 1) * pageSize) + 1\n const endIndex = Math.min(currentPage * pageSize, totalResults)\n const plural = resultType === 'vendors' ? 'vendors' : 'posts'\n const singular = resultType === 'vendors' ? 'vendor' : 'post'\n const displayType = totalResults === 1 ? singular : plural\n\n return (\n <div className=\"mb-6\">\n <p className=\"text-ods-text-secondary text-sm font-['DM_Sans']\">\n {totalResults > 0 && (\n <>\n Showing {startIndex}-{endIndex} of {totalResults} {displayType}\n {sortingMessage && (\n <span className=\"ml-2 text-ods-accent\">• {sortingMessage}</span>\n )}\n </>\n )}\n </p>\n </div>\n )\n} ","\"use client\"\n\nimport { OpenSourceIcon, CoinsIcon } from \"./icons-stub\"\nimport { OpenFrameLogo } from \"./openframe-logo\"\nimport { cn } from \"../utils/cn\"\nimport { Hand, Sparkles } from \"lucide-react\"\n\nexport interface VendorTagProps {\n type: 'open-source' | 'commercial' | 'free' | 'freemium' | 'paid' | 'enterprise' | 'recommended' | 'classification' | 'ai' | 'manual' | 'openframe_selected' | 'placeholder'\n text?: string\n className?: string\n size?: 'sm' | 'md'\n hidden?: boolean\n accentColor?: string\n}\n\nexport function VendorTag({\n type,\n text,\n className = \"\",\n hidden = false,\n size = 'md',\n accentColor\n}: VendorTagProps) {\n // Base classes for the tag container\n const baseClasses = cn(\n \"flex items-center gap-1.5 bg-ods-bg border border-ods-border rounded whitespace-nowrap\",\n size === 'sm' ? \"px-2 py-1\" : \"px-2.5 py-1.5\"\n )\n\n\n // Get display text and styling based on type\n const getTagContent = () => {\n switch (type) {\n case 'placeholder':\n return {\n text: \"Placeholder\",\n textColor: \"text-ods-text-primary\",\n icon: (\n <div className=\"w-4 h-4 rounded-sm flex items-center justify-center flex-shrink-0\">\n <Sparkles width={10} height={10} className=\"text-ods-text-primary\" />\n </div>\n )\n }\n case 'ai':\n return {\n text: \"AI Selected\",\n textColor: \"text-ods-text-primary\",\n icon: (\n <div className=\"w-4 h-4 rounded-sm flex items-center justify-center flex-shrink-0\">\n <Sparkles width={10} height={10} className=\"text-ods-text-primary\" />\n </div>\n )\n }\n case 'manual':\n return {\n text: \"Manually Selected\",\n textColor: \"text-ods-text-secondary\",\n icon: (\n <div className=\"w-4 h-4 rounded-sm flex items-center justify-center flex-shrink-0\">\n <Hand width={10} height={10} className=\"text-ods-text-secondary\" />\n </div>\n )\n }\n case 'open-source':\n return {\n text: text || \"Open Source\",\n icon: (\n <div \n className=\"w-4 h-4 bg-ods-accent rounded-sm flex items-center justify-center flex-shrink-0\"\n style={accentColor ? { backgroundColor: accentColor } : undefined}\n >\n <OpenSourceIcon width={10} height={10} className=\"text-[#1A1A1A]\" />\n </div>\n )\n }\n case 'commercial':\n return {\n text: text || \"Commercial Vendor\",\n icon: (\n <div className=\"w-4 h-4 bg-ods-border rounded-sm flex items-center justify-center flex-shrink-0\">\n <CoinsIcon width={10} height={10} className=\"text-ods-text-secondary\" />\n </div>\n )\n }\n case 'free':\n return {\n text: text || \"Free\",\n icon: (\n <div className=\"w-4 h-4 bg-ods-accent rounded-sm flex items-center justify-center flex-shrink-0\">\n <span className=\"text-[#1A1A1A] text-[8px] font-bold\">$</span>\n </div>\n )\n }\n case 'freemium':\n return {\n text: text || \"Freemium\",\n icon: (\n <div className=\"w-4 h-4 bg-ods-accent rounded-sm flex items-center justify-center flex-shrink-0\">\n <span className=\"text-[#1A1A1A] text-[8px] font-bold\">$</span>\n </div>\n )\n }\n case 'paid':\n return {\n text: text || \"Paid\",\n icon: (\n <div className=\"w-4 h-4 bg-ods-border rounded-sm flex items-center justify-center flex-shrink-0\">\n <CoinsIcon width={10} height={10} className=\"text-ods-text-secondary\" />\n </div>\n )\n }\n case 'enterprise':\n return {\n text: text || \"Enterprise\",\n icon: (\n <div className=\"w-4 h-4 bg-ods-accent rounded-sm flex items-center justify-center flex-shrink-0\">\n <span className=\"text-[#1A1A1A] text-[8px] font-bold\">E</span>\n </div>\n )\n }\n case 'recommended':\n return {\n text: text || \"Recommended\",\n icon: (\n <div className=\"w-4 h-4 bg-ods-accent rounded-sm flex items-center justify-center flex-shrink-0\">\n <span className=\"text-[#1A1A1A] text-[8px] font-bold\">★</span>\n </div>\n )\n }\n case 'classification':\n // Handle specific classification types based on the text value\n const classificationType = text?.toLowerCase()\n\n if (classificationType === 'open_source') {\n return {\n text: \"Open Source\",\n icon: (\n <div className=\"w-4 h-4 bg-ods-accent rounded-sm flex items-center justify-center flex-shrink-0\">\n <OpenSourceIcon width={10} height={10} className=\"text-[#1A1A1A]\" />\n </div>\n )\n }\n } else if (classificationType === 'commercial') {\n return {\n text: \"Commercial Vendor\",\n icon: (\n <div className=\"w-4 h-4 bg-ods-border rounded-sm flex items-center justify-center flex-shrink-0\">\n <CoinsIcon width={10} height={10} className=\"text-ods-text-secondary\" />\n </div>\n )\n }\n } else if (classificationType === 'openframe_selected') {\n return {\n text: \"OpenFrame Selected\",\n icon: <OpenFrameLogo lowerPathColor=\"currentColor\" upperPathColor=\"currentColor\" className=\"h-4 w-4 text-ods-accent\" />\n }\n } else {\n // Fallback for unknown classification types\n return {\n text: text || \"Classification\",\n icon: (\n <div className=\"w-4 h-4 bg-ods-accent rounded-sm flex items-center justify-center flex-shrink-0\">\n <span className=\"text-[#1A1A1A] text-[8px] font-bold\">C</span>\n </div>\n )\n }\n }\n case 'openframe_selected':\n return {\n text: text || \"OpenFrame Selected\",\n icon: <OpenFrameLogo lowerPathColor=\"currentColor\" upperPathColor=\"currentColor\" className=\"h-4 w-4 text-ods-accent\" />\n }\n default:\n return {\n text: text || type,\n icon: null\n }\n }\n }\n\n const { text: displayText, icon, textColor } = getTagContent()\n\n return (\n <div className={cn(baseClasses, className, hidden && \"invisible\")}>\n {icon}\n <span className={cn(\n \"font-mono font-semibold uppercase\",\n textColor ? textColor : \"text-ods-text-primary\",\n size === 'sm' ? \"text-[10px]\" : \"text-xs\"\n )}>\n {displayText}\n </span>\n </div>\n )\n} ","\"use client\"\n\nimport React from 'react';\nimport { cn } from \"../utils/cn\";\nimport { VendorTag } from './vendor-tag';\n\nexport type SelectionSourceType = 'ai' | 'manual' | 'placeholder';\n\ninterface SelectionSourceBadgeProps {\n source: SelectionSourceType;\n className?: string;\n hidden?: boolean;\n}\n\n/**\n * Small pill badge indicating whether a vendor was selected manually or by AI.\n * Colors follow OpenMSP design tokens.\n */\nexport function SelectionSourceBadge({ source, hidden = false }: SelectionSourceBadgeProps) {\n if (!source) {\n return null;\n }\n \n return (\n <VendorTag\n key={`source-${source}`}\n type={source?.toLowerCase() as 'ai' | 'manual' | 'placeholder'}\n size=\"sm\"\n hidden={hidden}\n />\n );\n} ","\"use client\";\n\nimport React from 'react';\nimport { cn } from \"../utils/cn\";\nimport { SquareAvatar } from './square-avatar';\n\ninterface UserDisplayProps {\n name: string;\n avatarUrl?: string | null;\n /** optional secondary text (e.g., relative timestamp) */\n subtitle?: string | null;\n /** Avatar size in px (defaults 32) */\n size?: number;\n className?: string;\n}\n\n/**\n * Reusable horizontal avatar + name (+ optional subtitle) row that follows\n * the visual pattern used in CommentCard headers.\n */\nexport function UserDisplay({ name, avatarUrl, subtitle, size = 32, className }: UserDisplayProps) {\n return (\n <div className={cn('flex items-center gap-2 min-w-0', className)}>\n <SquareAvatar src={avatarUrl ?? undefined} fallbackName={name} size={size} />\n <div className=\"min-w-0 flex-1\">\n <p className=\"font-['DM_Sans'] text-lg leading-[22px] text-ods-text-primary truncate\">\n {name}\n </p>\n {subtitle && (\n <span className=\"font-['DM_Sans'] text-md leading-[16px] text-ods-text-secondary truncate\">\n {subtitle}\n </span>\n )}\n </div>\n </div>\n );\n} ","import type React from \"react\"\nimport { cn } from \"../../utils/cn\"\nimport { UnifiedSkeleton, TextSkeleton, MediaSkeleton, InteractiveSkeleton } from \"./unified-skeleton\"\n\ninterface ContentSkeletonProps {\n className?: string\n}\n\n/**\n * Paragraph skeleton with varying line lengths for natural appearance\n */\nexport function ParagraphSkeleton({ \n className,\n lines = 4 \n}: ContentSkeletonProps & { lines?: number }) {\n const lineWidths = ['w-full', 'w-full', 'w-5/6', 'w-3/4', 'w-4/5', 'w-2/3']\n \n return (\n <div className={cn(\"space-y-2\", className)}>\n {Array.from({ length: lines }).map((_, index) => (\n <TextSkeleton.Body \n key={index}\n className={lineWidths[index % lineWidths.length]}\n />\n ))}\n </div>\n )\n}\n\n/**\n * List skeleton for navigation menus, categories, etc.\n */\nexport function ListSkeleton({ \n className,\n items = 5,\n showIcons = false,\n showActions = false \n}: ContentSkeletonProps & { \n items?: number\n showIcons?: boolean\n showActions?: boolean\n}) {\n return (\n <div className={cn(\"space-y-3\", className)}>\n {Array.from({ length: items }).map((_, index) => (\n <div key={index} className=\"flex items-center justify-between\">\n <div className=\"flex items-center gap-3\">\n {showIcons && <MediaSkeleton.Icon size=\"sm\" />}\n <TextSkeleton.Body className=\"w-32 md:w-48\" />\n </div>\n {showActions && (\n <div className=\"flex items-center gap-2\">\n <TextSkeleton.Caption className=\"w-8\" />\n <UnifiedSkeleton variant=\"default\" className=\"w-4 h-4\" />\n </div>\n )}\n </div>\n ))}\n </div>\n )\n}\n\n/**\n * Table skeleton for data displays\n */\nexport function TableSkeleton({ \n className,\n rows = 5,\n columns = 4 \n}: ContentSkeletonProps & { \n rows?: number\n columns?: number\n}) {\n return (\n <div className={cn(\"space-y-4\", className)}>\n {/* Table header */}\n <div className=\"grid gap-4\" style={{ gridTemplateColumns: `repeat(${columns}, 1fr)` }}>\n {Array.from({ length: columns }).map((_, index) => (\n <TextSkeleton.Subheading key={index} className=\"w-3/4\" />\n ))}\n </div>\n \n {/* Table rows */}\n <div className=\"space-y-3\">\n {Array.from({ length: rows }).map((_, rowIndex) => (\n <div \n key={rowIndex} \n className=\"grid gap-4 py-2 border-b border-ods-divider\"\n style={{ gridTemplateColumns: `repeat(${columns}, 1fr)` }}\n >\n {Array.from({ length: columns }).map((_, colIndex) => (\n <TextSkeleton.Body key={colIndex} className=\"w-2/3\" />\n ))}\n </div>\n ))}\n </div>\n </div>\n )\n}\n\n/**\n * Form skeleton for input fields and form layouts\n */\nexport function FormSkeleton({ \n className,\n fields = 4 \n}: ContentSkeletonProps & { fields?: number }) {\n return (\n <div className={cn(\"space-y-6\", className)}>\n {Array.from({ length: fields }).map((_, index) => (\n <div key={index} className=\"space-y-2\">\n <TextSkeleton.Body className=\"w-24\" />\n <InteractiveSkeleton.Input />\n {index % 3 === 0 && (\n <TextSkeleton.Caption className=\"w-48\" />\n )}\n </div>\n ))}\n \n <div className=\"flex gap-4 pt-4\">\n <InteractiveSkeleton.Button />\n <InteractiveSkeleton.Button className=\"bg-ods-border\" />\n </div>\n </div>\n )\n}\n\n/**\n * Navigation menu skeleton\n */\nexport function NavigationSkeleton({ \n className,\n items = 6,\n horizontal = true \n}: ContentSkeletonProps & { \n items?: number\n horizontal?: boolean\n}) {\n return (\n <nav \n className={cn(\n \"flex gap-4 md:gap-6\",\n !horizontal && \"flex-col\",\n className\n )}\n role=\"status\"\n aria-label=\"Loading navigation\"\n >\n {Array.from({ length: items }).map((_, index) => (\n <TextSkeleton.Body key={index} className=\"w-16 md:w-20\" />\n ))}\n </nav>\n )\n}\n\n/**\n * Profile/user info skeleton\n */\nexport function ProfileSkeleton({ \n className,\n showBio = true,\n showStats = true \n}: ContentSkeletonProps & { \n showBio?: boolean\n showStats?: boolean\n}) {\n return (\n <div className={cn(\"space-y-4\", className)}>\n {/* Avatar and basic info */}\n <div className=\"flex items-start gap-4\">\n <MediaSkeleton.Avatar size=\"lg\" />\n <div className=\"flex-1 space-y-2\">\n <TextSkeleton.Subheading className=\"w-1/2\" />\n <TextSkeleton.Caption className=\"w-1/3\" />\n <TextSkeleton.Body className=\"w-2/3\" />\n </div>\n </div>\n\n {/* Bio/description */}\n {showBio && (\n <div className=\"space-y-2\">\n <TextSkeleton.Body className=\"w-full\" />\n <TextSkeleton.Body className=\"w-4/5\" />\n <TextSkeleton.Body className=\"w-3/4\" />\n </div>\n )}\n\n {/* Stats */}\n {showStats && (\n <div className=\"flex gap-6\">\n <div className=\"text-center\">\n <TextSkeleton.Subheading className=\"w-8 mx-auto\" />\n <TextSkeleton.Caption className=\"w-12 mx-auto\" />\n </div>\n <div className=\"text-center\">\n <TextSkeleton.Subheading className=\"w-8 mx-auto\" />\n <TextSkeleton.Caption className=\"w-16 mx-auto\" />\n </div>\n <div className=\"text-center\">\n <TextSkeleton.Subheading className=\"w-8 mx-auto\" />\n <TextSkeleton.Caption className=\"w-14 mx-auto\" />\n </div>\n </div>\n )}\n </div>\n )\n}\n\n/**\n * Comment/review skeleton for user-generated content\n */\nexport function CommentSkeleton({ \n className,\n showRating = false,\n showReplies = false \n}: ContentSkeletonProps & { \n showRating?: boolean\n showReplies?: boolean\n}) {\n return (\n <div className={cn(\"space-y-3\", className)}>\n {/* User info */}\n <div className=\"flex items-center gap-3\">\n <MediaSkeleton.Avatar size=\"sm\" />\n <div className=\"flex-1 flex items-center gap-4\">\n <TextSkeleton.Body className=\"w-24\" />\n <TextSkeleton.Caption className=\"w-16\" />\n {showRating && (\n <div className=\"flex gap-1\">\n {Array.from({ length: 5 }).map((_, i) => (\n <MediaSkeleton.Icon key={i} size=\"sm\" />\n ))}\n </div>\n )}\n </div>\n </div>\n\n {/* Comment content */}\n <div className=\"ml-11 space-y-2\">\n <TextSkeleton.Body className=\"w-full\" />\n <TextSkeleton.Body className=\"w-3/4\" />\n <TextSkeleton.Body className=\"w-1/2\" />\n </div>\n\n {/* Actions */}\n <div className=\"ml-11 flex gap-4\">\n <TextSkeleton.Caption className=\"w-12\" />\n <TextSkeleton.Caption className=\"w-16\" />\n <TextSkeleton.Caption className=\"w-8\" />\n </div>\n\n {/* Replies */}\n {showReplies && (\n <div className=\"ml-11 pl-4 border-l border-ods-divider space-y-4\">\n {Array.from({ length: 2 }).map((_, index) => (\n <div key={index} className=\"space-y-2\">\n <div className=\"flex items-center gap-3\">\n <MediaSkeleton.Avatar size=\"sm\" />\n <TextSkeleton.Body className=\"w-20\" />\n <TextSkeleton.Caption className=\"w-12\" />\n </div>\n <div className=\"ml-11\">\n <TextSkeleton.Body className=\"w-full\" />\n <TextSkeleton.Body className=\"w-2/3\" />\n </div>\n </div>\n ))}\n </div>\n )}\n </div>\n )\n}\n\n/**\n * Feature list skeleton for product features, specifications, etc.\n */\nexport function FeatureListSkeleton({ \n className,\n features = 6,\n showIcons = true,\n grouped = false \n}: ContentSkeletonProps & { \n features?: number\n showIcons?: boolean\n grouped?: boolean\n}) {\n if (grouped) {\n // Grouped features with sections\n return (\n <div className={cn(\"space-y-6\", className)}>\n {Array.from({ length: 3 }).map((_, groupIndex) => (\n <div key={groupIndex} className=\"space-y-3\">\n <TextSkeleton.Subheading className=\"w-1/3\" />\n <div className=\"space-y-2\">\n {Array.from({ length: features / 3 }).map((_, featureIndex) => (\n <div key={featureIndex} className=\"flex items-center gap-3\">\n {showIcons && <MediaSkeleton.Icon size=\"sm\" />}\n <TextSkeleton.Body className=\"w-2/3\" />\n </div>\n ))}\n </div>\n </div>\n ))}\n </div>\n )\n }\n\n // Simple feature list\n return (\n <div className={cn(\"space-y-3\", className)}>\n {Array.from({ length: features }).map((_, index) => (\n <div key={index} className=\"flex items-center gap-3\">\n {showIcons && <MediaSkeleton.Icon size=\"sm\" />}\n <TextSkeleton.Body className=\"w-2/3\" />\n </div>\n ))}\n </div>\n )\n}\n\n/**\n * Timeline/activity skeleton\n */\nexport function TimelineSkeleton({ \n className,\n items = 5 \n}: ContentSkeletonProps & { items?: number }) {\n return (\n <div className={cn(\"space-y-6\", className)}>\n {Array.from({ length: items }).map((_, index) => (\n <div key={index} className=\"flex gap-4\">\n <div className=\"flex flex-col items-center\">\n <MediaSkeleton.Icon size=\"sm\" />\n {index < items - 1 && (\n <div className=\"w-px h-12 bg-ods-border mt-2\" />\n )}\n </div>\n <div className=\"flex-1 space-y-2\">\n <div className=\"flex items-center gap-3\">\n <TextSkeleton.Body className=\"w-1/3\" />\n <TextSkeleton.Caption className=\"w-16\" />\n </div>\n <TextSkeleton.Body className=\"w-full\" />\n <TextSkeleton.Body className=\"w-3/4\" />\n </div>\n </div>\n ))}\n </div>\n )\n}\n\n/**\n * Pricing/plan skeleton\n */\nexport function PricingSkeleton({ \n className,\n plans = 3 \n}: ContentSkeletonProps & { plans?: number }) {\n return (\n <div className={cn(\n \"grid gap-6\",\n plans === 2 && \"grid-cols-1 md:grid-cols-2\",\n plans === 3 && \"grid-cols-1 md:grid-cols-3\",\n plans === 4 && \"grid-cols-1 md:grid-cols-2 lg:grid-cols-4\",\n className\n )}>\n {Array.from({ length: plans }).map((_, index) => (\n <div key={index} className=\"bg-ods-card border border-ods-border rounded-lg p-6\">\n <div className=\"space-y-4\">\n {/* Plan name */}\n <TextSkeleton.Subheading className=\"w-1/2\" />\n \n {/* Price */}\n <div className=\"space-y-1\">\n <TextSkeleton.Heading className=\"w-1/3\" />\n <TextSkeleton.Caption className=\"w-1/4\" />\n </div>\n\n {/* Features */}\n <div className=\"space-y-3 py-4\">\n {Array.from({ length: 5 }).map((_, featureIndex) => (\n <div key={featureIndex} className=\"flex items-center gap-2\">\n <MediaSkeleton.Icon size=\"sm\" />\n <TextSkeleton.Body className=\"w-3/4\" />\n </div>\n ))}\n </div>\n\n {/* CTA Button */}\n <InteractiveSkeleton.Button className=\"w-full\" />\n </div>\n </div>\n ))}\n </div>\n )\n} ","import React from 'react'\nimport { cn } from \"../../utils/cn\"\n\ninterface ProfileLoadingSkeletonProps {\n className?: string;\n}\n\nexport function ProfileLoadingSkeleton({ className }: ProfileLoadingSkeletonProps) {\n return (\n <div className={cn(\"space-y-6\", className)}>\n {/* Header skeleton */}\n <div className=\"flex items-center space-x-4\">\n <div className=\"h-16 w-16 bg-gray-200 rounded-full animate-pulse\" />\n <div className=\"space-y-2\">\n <div className=\"h-4 w-32 bg-gray-200 rounded animate-pulse\" />\n <div className=\"h-3 w-24 bg-gray-200 rounded animate-pulse\" />\n </div>\n </div>\n \n {/* Content skeleton */}\n <div className=\"space-y-4\">\n <div className=\"h-4 w-full bg-gray-200 rounded animate-pulse\" />\n <div className=\"h-4 w-3/4 bg-gray-200 rounded animate-pulse\" />\n <div className=\"h-4 w-1/2 bg-gray-200 rounded animate-pulse\" />\n </div>\n </div>\n )\n}","import React from 'react'\nimport { cn } from \"../../utils/cn\"\nimport {\n UnifiedSkeleton,\n TextSkeleton,\n InteractiveSkeleton\n} from './unified-skeleton'\n\ninterface SkeletonProps {\n className?: string\n /**\n * Number of input fields to render on the right column (defaults to 4).\n */\n fields?: number\n}\n\n/**\n * MspProfileFormSkeleton\n * -----------------------------------------------------------------------------\n * Loading state for the <MspProfileForm /> component used in the Share-Your-Stack\n * wizard (and any other area that re-uses the form).\n *\n * Design Notes:\n * – Two-column grid matching the live component (logo upload on the left,\n * text / numeric inputs on the right).\n * – Left column: square/circular media skeleton to represent logo uploader.\n * – Right column: label + input pair per field.\n * – Mobile (<768px) collapses to one column via grid existing classes.\n * – Uses UnifiedSkeleton system to stay consistent with global loading design.\n */\nexport function MspProfileFormSkeleton({\n className,\n fields = 4,\n}: SkeletonProps) {\n return (\n <div\n className={cn(\n 'grid grid-cols-1 md:grid-cols-2 gap-6 items-stretch',\n className,\n )}\n role=\"status\"\n aria-label=\"Loading MSP profile form\"\n >\n {/* Left – Logo uploader placeholder */}\n <div className=\"flex flex-col space-y-2 h-full\">\n {/* Label skeleton */}\n <TextSkeleton.Body className=\"w-28\" />\n <div className=\"flex-1 min-h-[180px] md:min-h-full\">\n <UnifiedSkeleton className=\"w-full h-full rounded-lg\" aria-label=\"Loading company logo\" />\n </div>\n </div>\n\n {/* Right – Input fields grid (mirrors form) */}\n <div className=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\n {Array.from({ length: fields }).map((_, idx) => (\n <div key={idx} className=\"space-y-2 flex flex-col\">\n <TextSkeleton.Body className=\"w-40\" />\n <InteractiveSkeleton.Input />\n </div>\n ))}\n </div>\n </div>\n )\n} ","import React from 'react'\nimport { cn } from \"../../utils/cn\"\nimport { UnifiedSkeleton, TextSkeleton } from './unified-skeleton'\n\ninterface CategoryCardSkeletonProps {\n className?: string\n}\n\nexport function CategoryCardSkeleton({ className }: CategoryCardSkeletonProps) {\n return (\n <article\n className={cn(\n 'bg-[#1A1A1A] border border-[#424242] rounded-[12px] p-8 flex flex-col min-w-0 box-border',\n className,\n )}\n role=\"status\"\n aria-label=\"Loading category card\"\n >\n {/* Icons row */}\n <div className=\"flex gap-6 mb-8 justify-center items-center\">\n {Array.from({ length: 10 }).map((_, i) => (\n <UnifiedSkeleton\n key={i}\n variant=\"circular\"\n className=\"w-10 h-10 flex-shrink-0\"\n aria-label=\"Loading icon\"\n />\n ))}\n </div>\n\n {/* Text block */}\n <div className=\"flex-1 flex flex-col space-y-3\">\n <TextSkeleton.Heading className=\"w-3/4\" />\n <TextSkeleton.Body className=\"w-1/2\" />\n <TextSkeleton.Body className=\"w-full\" />\n </div>\n\n {/* Arrow button placeholder */}\n <div className=\"mt-4 flex justify-end\">\n <UnifiedSkeleton className=\"w-12 h-12 rounded-[6px]\" aria-label=\"Loading button\" />\n </div>\n </article>\n )\n} ","import React from 'react'\nimport { cn } from \"../../utils/cn\"\nimport { TextSkeleton, UnifiedSkeleton } from './unified-skeleton'\n\ninterface CategoryVendorSelectorSkeletonProps {\n /** number of subcategory blocks to render */\n subcategories?: number\n className?: string\n}\n\nexport function CategoryVendorSelectorSkeleton({ subcategories = 3, className }: CategoryVendorSelectorSkeletonProps) {\n return (\n <div\n className={cn('bg-ods-card border border-ods-border rounded-lg p-6', className)}\n role=\"status\"\n aria-label=\"Loading category section\"\n >\n {/* Header */}\n <div className=\"mb-6 space-y-2 max-w-xl\">\n <TextSkeleton.Heading className=\"w-1/2\" />\n <TextSkeleton.Body className=\"w-3/4\" />\n </div>\n\n {/* Subcategory blocks */}\n <div className=\"space-y-6\">\n {Array.from({ length: subcategories }).map((_, idx) => (\n <div key={idx} className=\"space-y-2\">\n {/* Subcategory title */}\n <TextSkeleton.Subheading className=\"w-1/3\" />\n {/* Vendor slots container */}\n <div className=\"grid grid-cols-2 md:grid-cols-3 gap-4\">\n {Array.from({ length: 2 }).map((__, j) => (\n <UnifiedSkeleton key={j} className=\"h-20 rounded-lg\" />\n ))}\n </div>\n </div>\n ))}\n </div>\n </div>\n )\n} ","import React from 'react'\nimport { cn } from \"../../utils/cn\"\nimport { UnifiedSkeleton, TextSkeleton } from './unified-skeleton'\n\ninterface WizardLayoutSkeletonProps {\n steps?: number\n className?: string\n}\n\nexport function WizardLayoutSkeleton({ steps = 6, className }: WizardLayoutSkeletonProps) {\n return (\n <div className={cn('space-y-6', className)} role=\"status\" aria-label=\"Loading wizard layout\">\n {/* Progress bar */}\n <div className=\"flex flex-wrap items-center gap-2\">\n {Array.from({ length: steps }).map((_, i) => (\n <UnifiedSkeleton key={i} className=\"h-6 w-24 rounded\" />\n ))}\n </div>\n\n {/* Header */}\n <div className=\"space-y-2 max-w-2xl\">\n <TextSkeleton.Heading className=\"w-2/3\" />\n <TextSkeleton.Body className=\"w-1/2\" />\n </div>\n\n {/* Bottom navigation buttons */}\n <div className=\"flex justify-between mt-10\">\n <UnifiedSkeleton className=\"h-10 w-24 rounded\" />\n <UnifiedSkeleton className=\"h-10 w-32 rounded\" />\n </div>\n </div>\n )\n} ","import React from 'react';\n\ninterface MarginReportSkeletonProps {\n /** Enable pulse animation (default: true) */\n animate?: boolean;\n /** Optional explanation text shown above overlay content */\n description?: React.ReactNode;\n /** Optional React node displayed over skeleton (button, loader, etc.) */\n overlayContent?: React.ReactNode;\n}\n\nexport function MarginReportSkeleton({ animate = true, description, overlayContent }: MarginReportSkeletonProps) {\n return (\n <main className={`bg-ods-bg ${animate ? 'animate-pulse' : ''} relative min-h-screen`}>\n <div className=\"max-w-[1920px] px-6 md:px-20 py-6 md:py-10 mx-auto space-y-10\">\n {/* Header */}\n <div className=\"space-y-3\">\n <div className=\"h-10 w-72 bg-[#2A2A2A] rounded\" />\n <div className=\"h-4 w-80 bg-[#2A2A2A] rounded\" />\n </div>\n\n {/* Summary cards */}\n <div className=\"grid grid-cols-1 md:grid-cols-3 gap-6\">\n {Array.from({ length: 3 }).map((_, i) => (\n <div key={i} className=\"h-32 bg-ods-card border border-ods-border rounded\" />\n ))}\n </div>\n\n {/* MSP Profile & Report Info cards */}\n <div className=\"grid grid-cols-1 md:grid-cols-2 gap-6\">\n {/* MSP profile skeleton */}\n <div className=\"bg-ods-card border border-ods-border rounded-lg p-6 flex items-center gap-4 animate-pulse\">\n <div className=\"w-14 h-14 rounded-lg bg-[#2A2A2A]\" />\n <div className=\"flex-1 space-y-2\">\n <div className=\"h-4 bg-[#2A2A2A] rounded w-3/4\" />\n <div className=\"h-3 bg-[#2A2A2A] rounded w-1/2\" />\n </div>\n </div>\n\n {/* Report info skeleton */}\n <div className=\"bg-ods-card border border-ods-border rounded-lg p-6 flex flex-col gap-4 animate-pulse\">\n <div className=\"flex items-center justify-between gap-4\">\n <div className=\"flex items-center gap-2\">\n <div className=\"w-10 h-10 rounded-lg bg-[#2A2A2A]\" />\n <div className=\"space-y-1\">\n <div className=\"h-4 w-32 bg-[#2A2A2A] rounded\" />\n <div className=\"h-3 w-20 bg-[#2A2A2A] rounded\" />\n </div>\n </div>\n <div className=\"h-6 w-36 bg-[#2A2A2A] rounded\" />\n </div>\n <div className=\"h-4 w-40 bg-[#2A2A2A] rounded mt-4\" />\n </div>\n </div>\n\n {/* Vendor solution lists (Commercial & Open-Source) */}\n <div className=\"grid grid-cols-1 md:grid-cols-2 gap-6\">\n {['Commercial Stack', 'Open-Source Stack'].map((label, idx) => (\n <div key={idx} className=\"bg-ods-card border border-ods-border rounded-lg overflow-hidden flex flex-col animate-pulse\">\n {/* list header */}\n <div className=\"flex items-center justify-between px-6 py-4\">\n <div className=\"h-6 w-40 bg-[#2A2A2A] rounded\" />\n <div className=\"flex items-center gap-2\">\n <div className=\"h-5 w-20 bg-[#2A2A2A] rounded\" />\n <div className=\"h-4 w-10 bg-[#2A2A2A] rounded\" />\n </div>\n </div>\n\n {/* vendor rows */}\n <div className=\"flex-1 flex flex-col gap-3 p-3\">\n {Array.from({ length: 5 }).map((_, j) => (\n <div key={j} className=\"bg-ods-bg border border-ods-border rounded-lg px-4 py-3 flex items-center justify-between\">\n {/* left section: icon + text */}\n <div className=\"flex items-center gap-3 min-w-0\">\n <div className=\"w-12 h-12 bg-[#2A2A2A] rounded-lg flex-shrink-0\" />\n <div className=\"flex flex-col min-w-0\">\n <div className=\"h-4 w-32 bg-[#2A2A2A] rounded\" />\n <div className=\"hidden md:block h-3 w-24 bg-[#2A2A2A] rounded mt-1\" />\n </div>\n </div>\n {/* right addon: cost text */}\n <div className=\"flex items-center gap-1 flex-shrink-0\">\n <div className=\"h-4 w-16 bg-[#2A2A2A] rounded\" />\n <div className=\"h-3 w-8 bg-[#2A2A2A] rounded\" />\n </div>\n </div>\n ))}\n </div>\n </div>\n ))}\n </div>\n\n {/* Strategic Recommendations header placeholder */}\n <div className=\"h-6 w-60 bg-[#2A2A2A] rounded\" />\n\n {/* Recommendations grid */}\n <div className=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6\">\n {Array.from({ length: 3 }).map((_, i) => (\n <div key={i} className=\"h-48 bg-ods-card border border-ods-border rounded\" />\n ))}\n </div>\n\n {/* OpenFrame value section */}\n <div className=\"border border-ods-border rounded-3xl p-8 space-y-6\">\n {/* Section header */}\n <div className=\"flex items-start gap-6\">\n <div className=\"flex-1 space-y-2 min-w-0\">\n <div className=\"h-8 w-72 bg-[#2A2A2A] rounded\" />\n <div className=\"h-4 w-3/4 bg-[#2A2A2A] rounded\" />\n </div>\n {/* Logo placeholder */}\n <div className=\"w-12 h-12 bg-[#2A2A2A] rounded-md shrink-0\" />\n </div>\n\n {/* Value cards grid */}\n <div className=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6\">\n {Array.from({ length: 3 }).map((_, i) => (\n <div key={i} className=\"h-32 bg-ods-card border border-ods-border rounded\" />\n ))}\n </div>\n </div>\n </div>\n\n {/* CTA Overlay */}\n {overlayContent && (\n <div className=\"absolute inset-0 bg-ods-card/80 z-10 rounded-lg pointer-events-none\">\n {/* Button centered relative to viewport */}\n <div className=\"fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 pointer-events-auto flex flex-col items-center gap-4 text-center px-4\">\n {description && (\n <h3 >\n {description}\n </h3>\n )}\n {overlayContent}\n </div>\n </div>\n )}\n </main>\n );\n} ","import { cn } from \"../../utils/cn\"\n\ninterface Props {\n rows?: number\n className?: string\n}\n\nexport function UsersGridSkeleton({ rows = 10, className }: Props) {\n const skeletonRows = Array.from({ length: rows })\n return (\n <div className={cn('overflow-x-auto rounded-lg border border-ods-border bg-ods-card animate-pulse', className)}>\n <table className=\"min-w-full divide-y divide-ods-border\">\n <thead className=\"bg-[#2A2A2A]\">\n <tr>\n {['Name', 'Email', 'Role', 'Created', 'Last Sign-In'].map((h) => (\n <th key={h} className=\"px-4 py-3 text-left text-[12px] font-medium text-ods-text-primary font-['DM_Sans']\">{h}</th>\n ))}\n </tr>\n </thead>\n <tbody className=\"divide-y divide-ods-border\">\n {skeletonRows.map((_, idx) => (\n <tr key={idx}>\n {Array.from({ length: 5 }).map((__, cell) => (\n <td key={cell} className=\"px-4 py-3 whitespace-nowrap\">\n <div className=\"h-4 bg-ods-border rounded w-full\"></div>\n </td>\n ))}\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n )\n} ","import type React from \"react\"\nimport { cn } from \"../../utils/cn\"\n\nexport interface OrganizationIconSkeletonProps {\n /**\n * Size variant (matches OrganizationIcon/VendorIcon sizes)\n * - xs: 24px (w-6 h-6)\n * - sm: 32px (w-8 h-8) - for devices table\n * - md: 40px (w-10 h-10) - for organizations table, dashboard (default)\n * - lg: 48px (w-12 h-12)\n * - l: 56px (w-14 h-14)\n * - xl: 64px (w-16 h-16) - for detail views\n */\n size?: 'xs' | 'sm' | 'md' | 'lg' | 'l' | 'xl'\n\n /**\n * Additional CSS classes\n */\n className?: string\n\n /**\n * Show background container (default: true)\n */\n showBackground?: boolean\n\n /**\n * Background style variant (default: 'dark')\n */\n backgroundStyle?: 'dark' | 'light' | 'white'\n}\n\n/**\n * Size classes matching VendorIcon/OrganizationIcon exactly\n */\nconst sizeClasses = {\n xs: 'w-6 h-6',\n sm: 'w-8 h-8',\n md: 'w-10 h-10',\n lg: 'w-12 h-12',\n l: 'w-14 h-14',\n xl: 'w-16 h-16'\n}\n\n/**\n * Background style classes matching VendorIcon/OrganizationIcon exactly\n */\nconst backgroundClasses = {\n dark: 'bg-ods-bg border border-ods-border',\n light: 'bg-ods-card border border-ods-border',\n white: 'bg-white border border-[#E5E5E5]'\n}\n\n/**\n * OrganizationIconSkeleton - Loading skeleton for OrganizationIcon\n *\n * Matches VendorIcon styling exactly for 100% visual parity.\n * Matches the exact dimensions of OrganizationIcon to prevent layout jumps.\n * Use this in loading states before organization data is available.\n *\n * Usage:\n *\n * ```typescript\n * // In table skeleton (matches current usage)\n * <OrganizationIconSkeleton size=\"md\" />\n *\n * // In device card skeleton\n * <OrganizationIconSkeleton size=\"sm\" />\n *\n * // In detail view skeleton\n * <OrganizationIconSkeleton size=\"xl\" />\n *\n * // Without background\n * <OrganizationIconSkeleton size=\"sm\" showBackground={false} />\n * ```\n */\nexport function OrganizationIconSkeleton({\n size = 'md',\n className = '',\n showBackground = true,\n backgroundStyle = 'dark'\n}: OrganizationIconSkeletonProps) {\n const containerClasses = cn(\n sizeClasses[size],\n 'rounded-lg flex items-center justify-center flex-shrink-0',\n showBackground && backgroundClasses[backgroundStyle],\n !showBackground && 'overflow-hidden',\n className\n )\n\n return (\n <div\n className={containerClasses}\n role=\"status\"\n aria-label=\"Loading organization icon\"\n >\n <div className=\"w-1/2 h-1/2 bg-ods-border rounded-sm animate-pulse\" />\n </div>\n )\n}\n","import type React from \"react\"\nimport { cn } from \"../../utils/cn\"\nimport { OrganizationIconSkeleton } from \"./organization-icon-skeleton\"\nimport { TextSkeleton, MediaSkeleton, InteractiveSkeleton } from \"./unified-skeleton\"\n\nexport interface OrganizationCardSkeletonProps {\n /**\n * Additional CSS classes\n */\n className?: string\n\n /**\n * Show footer stats area\n */\n showFooter?: boolean\n\n /**\n * Show description area\n */\n showDescription?: boolean\n\n /** Optional tailwind classes to override the card container background & border */\n containerClassName?: string\n}\n\n/**\n * OrganizationCardSkeleton - Loading skeleton matching OrganizationCard exact layout\n *\n * Matches VendorCard skeleton structure for 100% visual parity.\n *\n * Structure:\n * - Header: 60x60px org logo + title + subtitle\n * - Description: Fixed 48px height with 2-line clamp\n * - Footer: Stats display area\n *\n * Prevents layout jumps by matching exact dimensions.\n */\nexport function OrganizationCardSkeleton({\n className,\n containerClassName,\n showFooter = true,\n showDescription = true\n}: OrganizationCardSkeletonProps) {\n return (\n <div\n className={cn(\n containerClassName || \"bg-ods-card border border-ods-border\",\n \"rounded-lg overflow-hidden h-full flex flex-col\",\n className\n )}\n role=\"status\"\n aria-label=\"Loading organization card\"\n >\n <div className=\"p-4 gap-3 flex flex-col\">\n {/* Header Section - Row layout matching OrganizationCard/VendorCard */}\n <div className=\"flex items-start gap-3 w-full\">\n {/* Logo Frame - 60px width fixed, matching actual structure */}\n <OrganizationIconSkeleton\n size=\"xl\"\n backgroundStyle=\"dark\"\n showBackground={true}\n className=\"w-[60px] h-[60px]\"\n />\n\n {/* Text Container - Column layout, matching actual structure */}\n <div className=\"flex-1 flex flex-col justify-center py-2 min-w-0 space-y-1\">\n {/* Title - Single line with proper width */}\n <TextSkeleton.Subheading className=\"w-3/4\" />\n {/* Subtitle (industry/tier) - Single line, shorter */}\n <TextSkeleton.Caption className=\"w-1/2\" />\n </div>\n </div>\n\n {/* Description Section - Fixed 48px height matching VendorCard */}\n {showDescription && (\n <div className=\"w-full h-12 overflow-hidden flex items-center\">\n <div className=\"space-y-1 w-full\">\n <TextSkeleton.Body className=\"w-full\" />\n <TextSkeleton.Body className=\"w-2/3\" />\n </div>\n </div>\n )}\n\n {/* Footer Section - Stats display */}\n {showFooter && (\n <div className=\"flex items-center justify-between gap-2 w-full min-w-0\">\n {/* Stats Container */}\n <div className=\"flex items-center gap-3 md:gap-4 min-w-0 flex-shrink\">\n {/* Stat 1 */}\n <div className=\"flex items-center gap-1 flex-shrink-0\">\n <MediaSkeleton.Icon size=\"sm\" className=\"w-5 h-5\" />\n <TextSkeleton.Caption className=\"w-8\" />\n </div>\n\n {/* Stat 2 */}\n <div className=\"flex items-center gap-1 flex-shrink-0\">\n <MediaSkeleton.Icon size=\"sm\" className=\"w-5 h-5\" />\n <TextSkeleton.Caption className=\"w-10\" />\n </div>\n </div>\n\n {/* Tag/Badge Section */}\n <div className=\"flex-shrink-0\">\n <div className=\"flex items-center gap-1.5 bg-ods-bg border border-ods-border rounded px-2.5 py-1.5\">\n <div className=\"w-4 h-4 bg-ods-border rounded-sm flex items-center justify-center flex-shrink-0\">\n <MediaSkeleton.Icon size=\"sm\" className=\"w-2.5 h-2.5\" />\n </div>\n <TextSkeleton.Caption className=\"w-16\" />\n </div>\n </div>\n </div>\n )}\n </div>\n </div>\n )\n}\n\n/**\n * OrganizationCardSkeletonGrid - Grid of organization card skeletons\n *\n * Matches responsive grid layout:\n * - Mobile: 1 column\n * - Tablet (md): 2 columns\n * - Desktop (xl): 3 columns\n */\nexport function OrganizationCardSkeletonGrid({\n count = 12,\n className,\n containerClassName,\n showFooter = true,\n showDescription = true\n}: {\n count?: number\n className?: string\n containerClassName?: string\n showFooter?: boolean\n showDescription?: boolean\n}) {\n return (\n <div\n className={cn(\n \"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 md:gap-6\",\n className\n )}\n role=\"status\"\n aria-label={`Loading ${count} organization cards`}\n >\n {Array.from({ length: count }, (_, index) => (\n <OrganizationCardSkeleton\n key={index}\n containerClassName={containerClassName}\n showFooter={showFooter}\n showDescription={showDescription}\n />\n ))}\n </div>\n )\n}\n","import type React from \"react\"\nimport { cn } from \"../../utils/cn\"\nimport { OrganizationIconSkeleton } from \"./organization-icon-skeleton\"\n\nexport interface DeviceCardSkeletonProps {\n /**\n * Additional CSS classes\n */\n className?: string\n}\n\n/**\n * DeviceCardSkeleton - Loading skeleton matching DeviceCard exact layout\n *\n * Matches the structure of DeviceCard:\n * - Row 1: Device icon + Device name + More button\n * - Row 2: OS badge + Organization icon + Organization name\n * - Row 3: Status badge + Last seen\n *\n * Prevents layout jumps by matching exact dimensions.\n */\nexport function DeviceCardSkeleton({ className }: DeviceCardSkeletonProps) {\n return (\n <div\n className={cn(\n \"bg-ods-card rounded-[6px] border border-ods-border h-full\",\n className\n )}\n role=\"status\"\n aria-label=\"Loading device card\"\n >\n {/* Row 1: Device icon + Device name + More button */}\n <div className=\"flex gap-4 items-center px-4 py-3\">\n {/* Device type icon (8x8 container) */}\n <div className=\"w-8 h-8 bg-ods-bg border border-ods-border rounded-[6px] flex items-center justify-center flex-shrink-0\">\n <div className=\"w-4 h-4 bg-ods-border rounded animate-pulse\" />\n </div>\n\n {/* Device name */}\n <div className=\"flex-1 min-w-0\">\n <div className=\"h-6 w-3/4 bg-ods-border rounded animate-pulse\" />\n </div>\n\n {/* More button */}\n <div className=\"w-12 h-12 bg-ods-border rounded-[6px] flex-shrink-0 animate-pulse\" />\n </div>\n\n {/* Row 2: OS badge + Organization */}\n <div className=\"flex gap-4 items-center px-4 py-2\">\n {/* OS badge */}\n <div className=\"w-24 h-6 bg-ods-border rounded flex-shrink-0 animate-pulse\" />\n\n {/* Organization icon */}\n <OrganizationIconSkeleton size=\"sm\" />\n\n {/* Organization name */}\n <div className=\"flex-1 min-w-0\">\n <div className=\"h-5 w-1/2 bg-ods-border rounded animate-pulse\" />\n </div>\n </div>\n\n {/* Row 3: Status badge + Last seen */}\n <div className=\"flex gap-4 items-center px-4 py-2\">\n {/* Status badge */}\n <div className=\"w-20 h-6 bg-ods-border rounded-full flex-shrink-0 animate-pulse\" />\n\n {/* Last seen */}\n <div className=\"flex-1\">\n <div className=\"h-5 w-40 bg-ods-border rounded animate-pulse\" />\n </div>\n </div>\n </div>\n )\n}\n\n/**\n * DeviceCardSkeletonGrid - Grid of device card skeletons\n *\n * Matches DevicesGrid layout with responsive columns:\n * - Mobile: 1 column\n * - Tablet (md): 2 columns\n * - Desktop (lg): 3 columns\n * - Large (xl): 4 columns\n */\nexport function DeviceCardSkeletonGrid({\n count = 12,\n className\n}: {\n count?: number\n className?: string\n}) {\n return (\n <div\n className={cn(\n \"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4\",\n className\n )}\n role=\"status\"\n aria-label={`Loading ${count} device cards`}\n >\n {Array.from({ length: count }, (_, index) => (\n <DeviceCardSkeleton key={index} />\n ))}\n </div>\n )\n}\n","\"use client\"\n\nimport { VendorDetailLayoutSkeleton } from './loading';\n\nexport function VendorPageSkeleton() {\n return <VendorDetailLayoutSkeleton />;\n}","\"use client\"\n\nimport React from 'react';\nimport { SECTION_HEADING_CLASS } from './layout/page-heading';\n\ninterface WhyItMattersItemProps {\n number: string;\n title: string;\n description: string;\n isLast?: boolean;\n}\n\nconst WhyItMattersItem: React.FC<WhyItMattersItemProps> = ({ number, title, description, isLast }) => {\n return (\n <li\n className={`\n flex flex-col md:flex-row items-start gap-6 p-10 w-full\n transition-colors duration-200 hover:bg-ods-bg-hover\n ${!isLast ? 'border-b border-ods-border' : ''}\n `}\n >\n <span className=\"text-h2 tracking-[-0.02em] text-ods-accent\">\n {number}\n </span>\n <div className=\"flex-1\">\n <h3 className=\"text-h2 tracking-[-0.02em] text-ods-text-primary\">\n {title}\n </h3>\n <p className=\"text-h4 text-ods-text-primary mt-4\">\n {description}\n </p>\n </div>\n </li>\n );\n};\n\n\nconst WhyItMatters = () => {\n const items = [\n {\n number: \"1.\",\n title: \"Cut Costs\",\n description: \"Eliminate vendor fees with proven open-source alternatives\",\n },\n {\n number: \"2.\",\n title: \"Stay in Control\",\n description: \"Full visibility and data ownership\",\n },\n {\n number: \"3.\",\n title: \"Build What You Need\",\n description: \"Customize without vendor limitations\",\n },\n {\n number: \"4.\",\n title: \"Scale Freely\",\n description: \"Designed for multi-tenant MSP environments\",\n },\n ];\n\n return (\n <section className=\"bg-ods-bg\">\n <div className=\"w-full max-w-[1920px] mx-auto px-6 md:px-20\">\n <h2 className={`${SECTION_HEADING_CLASS} text-center mb-6`}>\n Why It Matters\n </h2>\n <div className=\"bg-ods-card border border-ods-border rounded-3xl overflow-hidden w-full\">\n <ol>\n {items.map((item, index) => (\n <WhyItMattersItem\n key={item.number}\n number={item.number}\n title={item.title}\n description={item.description}\n isLast={index === items.length - 1}\n />\n ))}\n </ol>\n </div>\n </div>\n </section>\n );\n};\n\nexport default WhyItMatters; ","\"use client\"\n\nimport { CheckCircleIcon as LucideCheckCircleIcon, XCircleIcon } from './ui/custom-icons'\n\ninterface YesNoDisplayProps {\n value: boolean\n yesText?: string\n noText?: string\n customText?: string // For displaying custom text instead of Yes/No\n className?: string\n}\n\nexport function YesNoDisplay({ \n value, \n yesText = \"Yes\", \n noText = \"No\", \n customText,\n className = \"\" \n}: YesNoDisplayProps) {\n // If custom text is provided, display it with appropriate icon\n if (customText) {\n return (\n <div className={`flex items-center gap-2 ${className}`}>\n <LucideCheckCircleIcon className=\"h-6 w-6\" />\n <span className=\"text-ods-text-primary text-[16px]\">{customText}</span>\n </div>\n )\n }\n \n // Standard Yes/No display\n if (value) {\n return (\n <div className={`flex items-center gap-2 ${className}`}>\n <LucideCheckCircleIcon className=\"h-6 w-6\" />\n <span className=\"text-ods-text-primary text-[16px]\">{yesText}</span>\n </div>\n )\n }\n \n return (\n <div className={`flex items-center gap-2 ${className}`}>\n <XCircleIcon className=\"h-6 w-6\" />\n <span className=\"text-ods-text-secondary text-[16px]\">{noText}</span>\n </div>\n )\n}\n\n/**\n * Unified logic to determine if a feature value should be considered \"Yes\" or \"No\"\n */\nexport function evaluateFeatureValue(\n value: string | null | undefined,\n dataType: 'boolean' | 'text' | 'number'\n): boolean {\n // Handle truly empty/null values\n if (value === null || value === undefined || value === '') {\n return false\n }\n \n const stringValue = String(value).toLowerCase().trim()\n \n // Handle explicit null/empty string values\n if (stringValue === 'null' || stringValue === 'n/a' || stringValue === 'none' || stringValue === '-') {\n return false\n }\n \n // Boolean data type\n if (dataType === 'boolean') {\n return stringValue === 'true' || \n stringValue === '1' || \n stringValue === 'yes' || \n stringValue === '✓' || \n stringValue === '✅'\n }\n \n // Text data type\n if (dataType === 'text') {\n // Handle emoji values\n if (stringValue === '✅' || stringValue === '✓') {\n return true\n }\n if (stringValue === '❌' || stringValue === '✗') {\n return false\n }\n \n // Handle explicit no values\n if (stringValue === 'no' || stringValue === 'false' || stringValue === '0') {\n return false\n }\n \n // Any other non-empty text is considered \"yes\"\n return stringValue.length > 0\n }\n \n // Number data type\n if (dataType === 'number') {\n const numValue = parseFloat(stringValue)\n return !isNaN(numValue) && numValue > 0\n }\n \n // Default fallback\n return false\n} ","'use client'\n\nimport React, { useState, useEffect } from 'react';\nimport { FlamingoLogo } from './flamingo-logo';\nimport { getBaseUrl } from '../utils';\n\ninterface MadeWithLoveProps {\n /** Custom class name for the container */\n className?: string;\n /** Size variant for the component */\n size?: 'sm' | 'md' | 'lg';\n /** Whether to show on mobile (responsive) */\n showOnMobile?: boolean;\n}\n\nexport function MadeWithLove({\n className = '',\n size = 'md',\n showOnMobile = true\n}: MadeWithLoveProps) {\n const [isMobile, setIsMobile] = useState(false);\n const [isHovered, setIsHovered] = useState(false);\n\n // Detect mobile/desktop for responsive behavior\n useEffect(() => {\n const checkMobile = () => {\n setIsMobile(window.innerWidth < 640); // 640px = sm breakpoint\n };\n\n checkMobile();\n window.addEventListener('resize', checkMobile);\n return () => window.removeEventListener('resize', checkMobile);\n }, []);\n\n // Size configuration with pixel values\n const sizeConfig = {\n sm: {\n logoSize: 14,\n fontSizeMobile: '12px',\n fontSizeDesktop: '14px',\n gap: '4px'\n },\n md: {\n logoSize: 16,\n fontSizeMobile: '14px',\n fontSizeDesktop: '16px',\n gap: '4px'\n },\n lg: {\n logoSize: 20,\n fontSizeMobile: '16px',\n fontSizeDesktop: '18px',\n gap: '6px'\n }\n };\n\n const config = sizeConfig[size];\n const flamingoUrl = getBaseUrl('flamingo');\n\n // Container styles using primitive CSS\n const containerStyle: React.CSSProperties = {\n display: (!showOnMobile && isMobile) ? 'none' : 'inline-flex',\n alignItems: 'center',\n gap: config.gap,\n fontSize: isMobile ? config.fontSizeMobile : config.fontSizeDesktop,\n lineHeight: 1.5,\n color: '#ffffff',\n fontFamily: 'inherit'\n };\n\n // Button/link styles using primitive CSS\n const linkStyle: React.CSSProperties = {\n display: 'inline-flex',\n alignItems: 'center',\n gap: '2px',\n padding: 0,\n margin: 0,\n background: 'transparent',\n border: 'none',\n cursor: 'pointer',\n textDecoration: isHovered ? 'underline' : 'none',\n color: '#ffffff',\n fontSize: '14px',\n fontFamily: '\"Azeret Mono\", monospace',\n transition: 'text-decoration 0.2s ease',\n outline: 'none'\n };\n\n // Logo container styles\n const logoStyle: React.CSSProperties = {\n marginLeft: '2px',\n flexShrink: 0,\n display: 'flex',\n alignItems: 'center'\n };\n\n // Text span styles\n const textStyle: React.CSSProperties = {\n color: '#ffffff',\n marginLeft: '4px',\n textDecoration: 'none'\n };\n\n return (\n <div\n style={containerStyle}\n className={className}\n >\n Made with love by\n <a\n href={flamingoUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n style={linkStyle}\n onMouseEnter={() => setIsHovered(true)}\n onMouseLeave={() => setIsHovered(false)}\n >\n <div style={logoStyle}>\n <FlamingoLogo\n size={config.logoSize}\n color=\"#f357bb\" // Direct hex instead of CSS variable for cross-framework compatibility\n />\n </div>\n <span style={textStyle}>Flamingo</span>\n </a>\n </div>\n );\n}","\"use client\";\n\nimport { Label } from \"./ui/label\";\nimport DatePicker from 'react-datepicker';\nimport { cn } from '../utils/cn';\n\ninterface DateTimePickerProps {\n value?: Date;\n onChange: (date: Date | null) => void;\n disabled?: boolean;\n label?: string;\n placeholder?: string;\n className?: string;\n showTimeSelect?: boolean;\n timeFormat?: string;\n timeIntervals?: number;\n dateFormat?: string;\n}\n\n// Shared input styling that matches Input component pattern\nconst inputClassName = cn(\n \"flex h-10 w-full rounded-md border px-3 py-2 text-sm ring-offset-background\",\n \"file:border-0 file:bg-transparent file:text-sm file:font-medium\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2\",\n \"disabled:cursor-not-allowed disabled:opacity-50 touch-manipulation\",\n // Admin theme colors\n \"bg-ods-card border-ods-border text-ods-text-primary\",\n \"placeholder:text-[#767676] focus-visible:ring-[#FFC008]\",\n \"md:text-sm text-base\" // Mobile zoom prevention\n);\n\nexport function DateTimePicker({\n value,\n onChange,\n disabled = false,\n label,\n placeholder = \"Select date and time\",\n className,\n showTimeSelect = true,\n timeFormat = \"HH:mm\",\n timeIntervals = 15,\n dateFormat = \"MMMM d, yyyy h:mm aa\"\n}: DateTimePickerProps) {\n return (\n <div className={cn(\"space-y-2\", className)}>\n {label && (\n <Label className=\"font-['Azeret_Mono'] text-[12px] font-semibold leading-[1em] tracking-[-0.02em] text-ods-text-secondary uppercase\">\n {label}\n </Label>\n )}\n\n <div className=\"w-full\">\n <DatePicker\n selected={value}\n onChange={onChange}\n showTimeSelect={showTimeSelect}\n timeFormat={timeFormat}\n timeIntervals={timeIntervals}\n dateFormat={dateFormat}\n placeholderText={placeholder}\n disabled={disabled}\n wrapperClassName=\"w-full\"\n className={inputClassName}\n />\n </div>\n \n <style dangerouslySetInnerHTML={{ __html: `\n .react-datepicker-wrapper {\n width: 100% !important;\n display: block !important;\n }\n\n .react-datepicker__input-container {\n width: 100% !important;\n }\n\n .react-datepicker__input-container input {\n width: 100% !important;\n }\n\n /* Dark theme for calendar popup */\n .react-datepicker-popper {\n z-index: 9999 !important;\n }\n\n .react-datepicker {\n background-color: #212121 !important;\n border: 1px solid #3A3A3A !important;\n color: #FAFAFA !important;\n z-index: 9999 !important;\n }\n\n .react-datepicker__header {\n background-color: #2A2A2A !important;\n border-bottom: 1px solid #3A3A3A !important;\n color: #FAFAFA !important;\n }\n\n .react-datepicker__current-month {\n color: #FAFAFA !important;\n }\n\n .react-datepicker__day-name {\n color: #888888 !important;\n }\n\n .react-datepicker__day {\n color: #FAFAFA !important;\n }\n\n .react-datepicker__day:hover {\n background-color: #3A3A3A !important;\n }\n\n .react-datepicker__day--selected {\n background-color: #FFC008 !important;\n color: #000000 !important;\n }\n\n .react-datepicker__day--keyboard-selected {\n background-color: #FFC008 !important;\n color: #000000 !important;\n }\n\n .react-datepicker__time-container {\n border-left: 1px solid #3A3A3A !important;\n }\n\n .react-datepicker__time {\n background: #212121 !important;\n }\n\n .react-datepicker__time-box {\n width: 85px !important;\n }\n\n .react-datepicker-time__header {\n background-color: #2A2A2A !important;\n color: #FAFAFA !important;\n }\n\n .react-datepicker__time-list-item {\n color: #FAFAFA !important;\n }\n\n .react-datepicker__time-list-item:hover {\n background-color: #3A3A3A !important;\n }\n\n .react-datepicker__time-list-item--selected {\n background-color: #FFC008 !important;\n color: #000000 !important;\n }\n\n .react-datepicker__navigation {\n top: 1rem !important;\n }\n\n .react-datepicker__navigation--previous {\n border-right-color: #888888 !important;\n }\n\n .react-datepicker__navigation--next {\n border-left-color: #888888 !important;\n }\n ` }} />\n </div>\n );\n} ","'use client'\n\n/**\n * `<ProductReleasesView />` — the shared, SELF-CONTAINED releases LIST surface.\n *\n * The host configures only two things: the **api route** (`endpoint`, default\n * `/api/releases`) and the **internal page route** for a release's detail page\n * (via `runtime.composeContentUrl` / `basePath`). Everything else — reading the\n * search / status / page URL params the section chrome writes, fetching the\n * page, pagination, empty / error / loading states, card composition + nav —\n * happens INSIDE the lib. Mirrors the `DeliveryLists` internal-fetch pattern\n * (plain `fetch` + `useEffect`/`useState`, no react-query dependency).\n *\n * SSR: pass `initialData` to hydrate the first page without a client round-trip\n * (the hub can server-fetch); embedders omit it and the view fetches on mount.\n *\n * Card props: `buildCardProps` defaults to the lib's RICH `buildProductReleaseCardProps`\n * (full lg metadata — Type / Status / author / changelog counts); pass your own to customize.\n */\n\nimport * as React from 'react'\n\nimport { useRouter, useSearchParams, usePathname } from '../../../embed-shims'\nimport { useSelfFetch } from '../../../hooks/use-self-fetch'\nimport { useChatRuntime } from '../../../contexts/chat-runtime-context'\nimport type { ProductRelease, ProductReleaseListResponse } from '../../../types/product-release'\nimport { cn } from '../../../utils/cn'\nimport { resolveContentHref } from '../../../utils/content-href'\nimport { isModifierClick } from '../../chat/utils/chat-nav-resolution'\nimport { executeNavigationImperative } from '../../chat/utils/execute-navigation'\nimport { useEntityCardLink } from '../../chat/entity-cards/use-entity-card-link'\nimport { buildProductReleaseCardProps } from '../../chat/entity-cards/product-release-card-defaults'\nimport { EmptyState } from '../../empty-state'\nimport { LoadError } from '../../ui/error-state'\nimport { PersistentPaginationWrapper } from '../../persistent-pagination'\nimport { ProductReleaseCard, type ProductReleaseCardProps } from './product-release-card'\nimport { ProductReleaseCardSkeleton } from './product-release-card-skeleton'\nimport { DEV_SECTION_PARAM_KEYS } from '../../../utils/dev-sections/dev-section-param-keys'\n\nconst DEFAULT_ENDPOINT = '/api/releases'\n// Param keys sourced from the shared registry (see RoadmapView) — single source for the\n// chrome's written `?key=` and this view's read.\nconst DEFAULT_SEARCH_PARAM_KEY = DEV_SECTION_PARAM_KEYS.search\nconst DEFAULT_STATUS_PARAM_KEY = DEV_SECTION_PARAM_KEYS.releaseStatus\nconst DEFAULT_PAGE_PARAM_KEY = 'page'\n\n/** The `<ProductReleaseCard>` props the caller derives per release — the card's\n * own data fields minus the ones this view supplies (title/summary/version/\n * size/nav). `formattedDate` stays required so the spread satisfies the card. */\nexport type ProductReleaseCardExtras = Omit<\n ProductReleaseCardProps,\n 'title' | 'summary' | 'version' | 'size' | 'anchorProps' | 'onClick' | 'className'\n>\n\nexport interface ProductReleasesViewProps {\n /** GET endpoint for the releases list (the api route). The view appends\n * `?limit&offset&<search>&<release_status>`. Default `/api/releases`. */\n endpoint?: string\n /** Optional SSR hydrate for the first page — skips the initial client fetch. */\n initialData?: ProductReleaseListResponse\n /** Page size → fixed slot count + offset math. Default 5. */\n itemsPerPage?: number\n /** Fallback detail-href prefix when `runtime.composeContentUrl` is not wired\n * (single-platform embedders). Default `/releases`. */\n basePath?: string\n /** Derive the per-card prop bundle. Defaults to the lib's RICH lg builder\n * (`buildProductReleaseCardProps` — full Type/Status/author/changelog metadata)\n * so embedders get the complete card with zero config; pass your own to customize. */\n buildCardProps?: (release: ProductRelease) => ProductReleaseCardExtras\n /** URL param key for the search input. MUST match the chrome that writes it.\n * Default `'search'` (also the outbound query-param name). */\n searchParamKey?: string\n /** URL param key for the status filter. Default `'release_status'`. */\n statusParamKey?: string\n /** URL param key for the page number. Default `'page'`. */\n pageParamKey?: string\n className?: string\n}\n\n/**\n * Per-row child — owns the per-release hooks (`useChatRuntime` +\n * `useEntityCardLink` + `useRouter`) so they run at a component top level, NOT\n * inside the parent `.map()` (Rules-of-Hooks).\n */\nfunction ReleaseRow({\n release,\n basePath,\n buildCardProps,\n}: {\n release: ProductRelease\n basePath: string\n buildCardProps: (release: ProductRelease) => ProductReleaseCardExtras\n}) {\n const runtime = useChatRuntime()\n const router = useRouter()\n // Pass the HYDRATED junction (`product_release_platforms` — the release DAL\n // flattens each platform's `name` onto it), like the blog / case-study /\n // interview grids. The composer reads `name` → resolves to the CURRENT\n // platform when the release belongs to it → relative same-tab href.\n const cta = resolveContentHref(runtime?.composeContentUrl, {\n type: 'product_release',\n slug: release.slug,\n basePath,\n platforms: release.product_release_platforms,\n })\n const { target, rel } = useEntityCardLink({ href: cta.href, targetPlatform: cta.targetPlatform })\n\n const onClick = (e: React.MouseEvent<HTMLAnchorElement>) => {\n // Let the browser handle modifier / middle / new-tab clicks (shared\n // `isModifierClick` rule). Otherwise soft same-origin nav: the host's\n // `navigate` (hub docNav) if wired, else the registered embed-shims router.\n if (e.defaultPrevented || isModifierClick(e) || target === '_blank') return\n e.preventDefault()\n executeNavigationImperative({\n runtime,\n href: cta.href,\n targetPlatform: cta.targetPlatform,\n fallbackNavigate: router.push,\n })\n }\n\n return (\n <ProductReleaseCard\n size=\"lg\"\n title={release.title}\n summary={release.summary}\n version={release.version}\n {...buildCardProps(release)}\n anchorProps={{ href: cta.href, target, rel, onClick }}\n />\n )\n}\n\nexport function ProductReleasesView({\n endpoint = DEFAULT_ENDPOINT,\n initialData,\n itemsPerPage = 5,\n basePath = '/releases',\n buildCardProps = buildProductReleaseCardProps,\n searchParamKey = DEFAULT_SEARCH_PARAM_KEY,\n statusParamKey = DEFAULT_STATUS_PARAM_KEY,\n pageParamKey = DEFAULT_PAGE_PARAM_KEY,\n className,\n}: ProductReleasesViewProps = {}) {\n const searchParams = useSearchParams()\n const router = useRouter()\n const pathname = usePathname()\n\n // Filter / page state from the URL (written by the section chrome above).\n const search = searchParams.get(searchParamKey) || ''\n const status = searchParams.get(statusParamKey) || 'all'\n const currentPage = Math.max(1, parseInt(searchParams.get(pageParamKey) || '1', 10) || 1)\n const offset = (currentPage - 1) * itemsPerPage\n\n // Fold every query param into the url so it IS the fetch key.\n const listParams = new URLSearchParams({ limit: String(itemsPerPage), offset: String(offset) })\n if (search) listParams.set(searchParamKey, search)\n if (status && status !== 'all') listParams.set(statusParamKey, status)\n const { data, isLoading, error, reload } = useSelfFetch<ProductReleaseListResponse>(\n `${endpoint}?${listParams.toString()}`,\n { initialData },\n )\n\n const releases = data?.data ?? []\n const totalCount = data?.count ?? 0\n const totalPages = Math.ceil(totalCount / itemsPerPage)\n const hasActiveFilters = search !== '' || status !== 'all'\n const showEmpty = !isLoading && !error && releases.length === 0\n\n const goToPage = (page: number) => {\n const params = new URLSearchParams(searchParams.toString())\n params.set(pageParamKey, String(page))\n router.replace(`${pathname}?${params.toString()}`, { scroll: false })\n }\n const resetFilters = () => {\n const params = new URLSearchParams(searchParams.toString())\n params.delete(searchParamKey)\n params.delete(statusParamKey)\n params.delete(pageParamKey)\n router.replace(`${pathname}?${params.toString()}`, { scroll: false })\n }\n\n if (error) {\n return (\n <div className={cn('w-full', className)}>\n <LoadError message=\"Failed to load releases.\" onRetry={reload} />\n </div>\n )\n }\n\n return (\n <div className={cn('w-full flex flex-col gap-[40px]', className)}>\n <div className=\"min-h-[600px]\">\n {showEmpty ? (\n <div className=\"h-[600px] flex items-center justify-center\">\n {hasActiveFilters ? (\n <EmptyState\n type=\"search\"\n title=\"No releases found\"\n description=\"No releases match your current filters. Try adjusting your search or status filter.\"\n showCTA\n ctaText=\"Reset Filters\"\n onCtaClick={resetFilters}\n />\n ) : (\n <EmptyState\n type=\"generic\"\n title=\"No releases available\"\n description=\"Check back soon for product updates!\"\n showCTA={false}\n />\n )}\n </div>\n ) : (\n <>\n {/* ALWAYS render `itemsPerPage` slots — visibility toggles so the\n list height is stable across loading ↔ loaded. */}\n <div className=\"flex flex-col gap-6\">\n {Array.from({ length: itemsPerPage }).map((_, i) => {\n const release = releases[i]\n const hasData = !!release\n return (\n <div\n key={release?.id ?? `slot-${i}`}\n style={{ visibility: isLoading || hasData ? 'visible' : 'hidden' }}\n >\n {isLoading ? (\n <ProductReleaseCardSkeleton size=\"lg\" />\n ) : release ? (\n <ReleaseRow release={release} basePath={basePath} buildCardProps={buildCardProps} />\n ) : (\n <ProductReleaseCardSkeleton size=\"lg\" />\n )}\n </div>\n )\n })}\n </div>\n\n {/* Pagination — always present at the bottom for consistent spacing. */}\n <div className=\"mt-6 md:mt-8 flex justify-center\">\n {isLoading ? (\n <div className=\"h-12 m-3 w-64\" />\n ) : releases.length > 0 && totalPages > 1 ? (\n <PersistentPaginationWrapper\n isLoading={false}\n currentPage={currentPage}\n totalPages={totalPages}\n onPageChange={goToPage}\n variant=\"blog\"\n />\n ) : (\n <div className=\"h-12 m-3 w-64\" style={{ visibility: 'hidden' }} />\n )}\n </div>\n </>\n )}\n </div>\n </div>\n )\n}\n","\"use client\";\n\nimport { useState, useEffect, ComponentType } from 'react';\nimport Link from '../../../embed-shims/next-link';\nimport { useRouter } from '../../../embed-shims/next-navigation';\nimport { Card, CardContent } from '../../ui/card';\n// PageShell (wide) — match the related-content/FAQ rail container the hub\n// renders below this view (was ArticleDetailLayout, 1280px — narrower than\n// the rail, see hub detail-container alignment decision 2026-06-10).\nimport { PageShell } from '../../layout/article-detail-layout';\nimport { PageLayout } from '../../layout/page-layout';\nimport { ReleaseChangelogSection } from '../../ui/release-changelog-section';\nimport { RichMarkdownRenderer } from '../../ui/rich-markdown-renderer';\nimport { EntityTagBadges } from '../../features/entity-tag-badges';\nimport { EntityMetadataAuthorCell } from '../../chat/entity-cards/entity-author-card';\nimport type { EntityAuthor } from '../../../types/entity-author';\nimport { MediaGalleryStrip } from '../media-gallery-strip';\nimport { GitHubIcon } from '../../icons/github-icon';\nimport { AlertTriangle, ExternalLink, BookMarked, Sparkles, TrendingUp, Wrench } from 'lucide-react';\nimport { formatReleaseDate } from '../../../utils/date-formatters';\nimport { contentFetch } from '../../../utils/embed-content-fetch';\nimport { Video } from '../../features/video';\nimport { DetailPageSkeleton } from '../detail-page-skeleton';\nimport type { ChangelogEntry } from '../../../types/product-release';\nimport type { TagAssoc } from '../../../types/blog';\nimport type { VideoTeaser } from '../../../types/video-processing';\n\n// Types for injectable components\nexport interface MarkdownRendererProps {\n content: string;\n}\n\n// Canonical RoadmapItem shape lives in chat entity types — see\n// `src/components/chat/types/entities/roadmap-item.ts`. The product-release\n// detail page previously declared a structural placeholder\n// (`{ id; [k: string]: unknown }`) that conflicted with the canonical\n// shape once the entities barrel was added; re-exporting the canonical\n// type fixes the collision while keeping the same import path for\n// downstream consumers of `./release-detail-page`.\nimport type { RoadmapItem } from '../../chat/types/entities/roadmap-item';\nimport type { DeliveryResponse } from '../../../types/delivery';\n// Re-export both types for source-compat with consumers importing\n// through this module. Canonical sources:\n// - RoadmapItem → `../../chat/types/entities/roadmap-item`\n// - DeliveryResponse → `../../../types/delivery` (single source of\n// truth, shared with the lib `<DeliveryLists>` / `<DeliveryTable>`\n// components and the new types barrel).\nexport type { RoadmapItem, DeliveryResponse };\n\nexport interface RoadmapSectionProps {\n items: RoadmapItem[];\n isLoading: boolean;\n onItemUpdate?: (item: RoadmapItem) => void;\n}\n\nexport interface DeliverySectionProps {\n data: DeliveryResponse | null;\n isLoading: boolean;\n}\n\nexport interface VideoSectionProps {\n bites: VideoTeaser[];\n title?: string;\n filterPublished?: boolean;\n}\n\n// Type for the useRelease hook result\nexport interface UseReleaseResult {\n data: unknown;\n error: Error | null;\n isLoading: boolean;\n}\n\nexport interface VideoDisplaySectionProps {\n mainVideoUrl?: string | null;\n youtubeUrl?: string | null;\n highlightVideoUrl?: string | null;\n highlightVideoThumbnail?: string | null;\n mainVideoPoster?: string | null;\n title?: string;\n videoSummary?: string | null;\n videoBites?: VideoTeaser[];\n bitesTitle?: string;\n filterPublishedBites?: boolean;\n srtContent?: string | null;\n captionsUrl?: string | null;\n}\n\nexport interface ReleaseDetailPageProps {\n slug: string;\n initialData?: unknown; // Optional pre-fetched data for admin preview\n // Required: Hook for fetching release data (must be from app-level to use correct QueryClient)\n useRelease: (slug: string | undefined) => UseReleaseResult;\n // Injectable components for app-specific rendering\n MarkdownRenderer?: ComponentType<MarkdownRendererProps>;\n RoadmapSection?: ComponentType<RoadmapSectionProps>;\n DeliverySection?: ComponentType<DeliverySectionProps>;\n VideoSection?: ComponentType<VideoSectionProps>;\n /** Injectable video display section with tabs for full/highlight video + summary + bites */\n VideoDisplaySection?: ComponentType<VideoDisplaySectionProps>;\n // API endpoints for fetching linked tasks\n roadmapApiEndpoint?: string;\n deliveryApiEndpoint?: string;\n /** Back-button config — same pattern as `DevSectionPage` /\n * `LegalDocumentPage`. Pass `false` to hide. Default\n * `{ label: 'Back to home', href: '/' }`. */\n backButton?: { label?: string; href?: string } | false;\n /** Link target for the author name in the metadata grid — the host\n * computes it (public author page; absent ⇒ plain text). */\n authorHref?: string;\n}\n\n// Default renderer = the lib's `RichMarkdownRenderer` so out-of-the-box\n// release pages get full rich-link previews, embedded media, social cards,\n// etc. Hosts that want a different rendering (or a Supabase-aware preset)\n// override via the `MarkdownRenderer` prop.\nconst DefaultMarkdownRenderer = RichMarkdownRenderer;\n\nexport function ReleaseDetailPage({\n authorHref,\n slug,\n initialData,\n useRelease,\n MarkdownRenderer = DefaultMarkdownRenderer,\n RoadmapSection,\n DeliverySection,\n VideoSection,\n VideoDisplaySection,\n roadmapApiEndpoint = '/api/roadmap',\n deliveryApiEndpoint = '/api/delivery',\n backButton\n}: ReleaseDetailPageProps) {\n const router = useRouter();\n // Use pre-fetched data if provided (admin preview), otherwise fetch via hook (public)\n const { data: fetchedRelease, error, isLoading } = useRelease(initialData ? undefined : slug);\n const release = (initialData || fetchedRelease) as Record<string, unknown> | undefined;\n\n // Back-button config — mirrors DevSectionPage / LegalDocumentPage.\n // Default: { label: 'Back to home', href: '/' }. Pass `false` to hide\n // (e.g. embed-mode where the host owns navigation chrome).\n // Narrowing note: `backButton &&` already eliminates the `false` branch,\n // so the inner expressions are typed as `{ label?, href? } | undefined`.\n // Don't re-compare to `false` here — tsc TS2367s on the dead branch.\n const showBackButton = backButton !== false;\n const backLabel = (backButton ? backButton.label : undefined) ?? 'Back to home';\n const backHref = (backButton ? backButton.href : undefined) ?? '/';\n\n // Fetch roadmap and delivery tasks if linked to this release\n const [roadmapTasks, setRoadmapTasks] = useState<RoadmapItem[]>([]);\n const [deliveryData, setDeliveryData] = useState<DeliveryResponse | null>(null);\n const [roadmapLoading, setRoadmapLoading] = useState(false);\n const [deliveryLoading, setDeliveryLoading] = useState(false);\n\n useEffect(() => {\n async function fetchLinkedTasks() {\n if (!release) return;\n\n try {\n // Fetch roadmap tasks if linked\n const roadmapTasksData = release.clickup_roadmap_tasks as Array<{ clickup_task_id: string }> | undefined;\n if (roadmapTasksData && roadmapTasksData.length > 0 && RoadmapSection) {\n setRoadmapLoading(true);\n const roadmapIds = roadmapTasksData.map(t => t.clickup_task_id).join(',');\n const roadmapResponse = await contentFetch(`${roadmapApiEndpoint}?task_ids=${roadmapIds}`);\n const roadmapData = await roadmapResponse.json();\n setRoadmapTasks(roadmapData.items || []);\n setRoadmapLoading(false);\n }\n\n // Fetch delivery tasks if linked\n const deliveryTasksData = release.clickup_delivery_tasks as Array<{ clickup_task_id: string }> | undefined;\n if (deliveryTasksData && deliveryTasksData.length > 0 && DeliverySection) {\n setDeliveryLoading(true);\n const deliveryIds = deliveryTasksData.map(t => t.clickup_task_id).join(',');\n const deliveryResponse = await contentFetch(`${deliveryApiEndpoint}?task_ids=${deliveryIds}`);\n const deliveryResponseData = await deliveryResponse.json();\n setDeliveryData(deliveryResponseData);\n setDeliveryLoading(false);\n }\n } catch (err) {\n console.error('Error fetching linked tasks:', err);\n setRoadmapLoading(false);\n setDeliveryLoading(false);\n }\n }\n\n fetchLinkedTasks();\n }, [release, RoadmapSection, DeliverySection, roadmapApiEndpoint, deliveryApiEndpoint]);\n\n // Don't show loading skeleton if we have initialData\n if (!initialData && isLoading) {\n // `bare` + `PageShell` so the loading state matches the loaded page's full\n // width / padding / min-height (the wrapper supplies the page chrome).\n return (\n <PageShell>\n {/* Match the loaded page's top offset (TitleBlock's\n `pt-[var(--spacing-system-l)]`) so content doesn't jump on load. */}\n <div className=\"pt-[var(--spacing-system-l)]\">\n <DetailPageSkeleton bare metadataColumns={4} showImageGallery={true} />\n </div>\n </PageShell>\n );\n }\n\n if (error || !release) {\n return (\n <PageShell>\n <div className=\"text-center py-16\">\n <h1 className=\"text-4xl font-bold text-ods-text-primary mb-4\">Release Not Found</h1>\n <p className=\"text-xl text-ods-text-secondary\">The release you're looking for doesn't exist.</p>\n </div>\n </PageShell>\n );\n }\n\n const hasBreakingChanges = Array.isArray(release.breaking_changes) && release.breaking_changes.length > 0;\n\n // Type assertions for release data\n const releaseTitle = release.title as string;\n const releaseVersion = release.version as string;\n const releaseSummary = release.summary as string | null;\n const releaseContent = release.content as string | null;\n const releaseDate = release.release_date as string;\n const releaseType = release.release_type as string;\n const releaseStatus = release.release_status as string;\n const releaseMedia = release.release_media as Array<{ id?: string; media_type: string; media_url: string; title?: string }> | undefined;\n // Field-cast per this file's loose-release idiom (release_type etc. above)\n // — but to the SHARED EntityAuthor, never an inline shadow author shape.\n const author = release.author as EntityAuthor | undefined;\n const githubReleases = release.github_releases as Array<{ id: string; github_release_url: string }> | undefined;\n const knowledgeBaseLinks = release.knowledge_base_links as Array<{ id?: string; kb_article_path: string }> | string[] | undefined;\n const migrationGuideUrl = release.migration_guide_url as string | undefined;\n const documentationUrl = release.documentation_url as string | undefined;\n const youtubeUrl = release.youtube_url as string | undefined;\n const mainVideoUrl = release.main_video_url as string | undefined;\n const videoBites = release.video_bites as VideoTeaser[] | undefined;\n const highlightVideoUrl = release.highlight_video_url as string | undefined;\n const highlightVideoThumbnail = release.highlight_video_thumbnail as string | undefined;\n const breakingChanges = release.breaking_changes as ChangelogEntry[] | undefined;\n const featuresAdded = release.features_added as ChangelogEntry[] | undefined;\n const bugFixed = release.bugs_fixed as ChangelogEntry[] | undefined;\n const improvements = release.improvements as ChangelogEntry[] | undefined;\n\n return (\n <PageShell>\n <PageLayout\n backButton={\n showBackButton ? { label: backLabel, onClick: () => router.push(backHref) } : undefined\n }\n >\n <div className=\"space-y-6 md:space-y-8\">\n {/* Title Block */}\n <div className=\"flex flex-col md:flex-row md:items-end gap-4 w-full\">\n <div className=\"flex-1 flex flex-col gap-2\">\n {/* Title */}\n <h1 className=\"text-h1 tracking-[-1.12px] text-ods-text-primary\">\n {releaseTitle}\n </h1>\n\n {/* Version */}\n <p className=\"text-h4 text-ods-text-secondary\">\n Version: {releaseVersion}\n </p>\n </div>\n </div>\n\n {/* Tags — flat product_release_tags[] from entity_tags */}\n <EntityTagBadges tags={release.product_release_tags as TagAssoc[] | undefined} />\n\n {/* Metadata Grid */}\n <div className=\"grid grid-cols-1 md:grid-cols-4 border border-ods-border rounded-md overflow-hidden w-full\">\n {/* Release Type */}\n <div className=\"bg-ods-card border-b md:border-b-0 md:border-r border-ods-border p-4 flex flex-col gap-3\">\n <div className=\"flex flex-col gap-0\">\n <p className=\"text-h4 text-ods-text-primary\">\n {releaseType.toLocaleUpperCase()}\n </p>\n <p className=\"font-['DM_Sans'] font-medium text-[14px] leading-[20px] text-ods-text-secondary\">\n Release Type\n </p>\n </div>\n </div>\n\n {/* Release Status */}\n <div className=\"bg-ods-card border-b md:border-b-0 md:border-r border-ods-border p-4 flex flex-col gap-3\">\n <div className=\"flex flex-col gap-0\">\n <p className=\"text-h4 text-ods-text-primary\">\n {releaseStatus.toLocaleUpperCase()}\n </p>\n <p className=\"font-['DM_Sans'] font-medium text-[14px] leading-[20px] text-ods-text-secondary\">\n Release Status\n </p>\n </div>\n </div>\n\n {/* Release Date */}\n <div className=\"bg-ods-card border-b md:border-b-0 md:border-r border-ods-border p-4 flex flex-col gap-3\">\n <div className=\"flex flex-col gap-0\">\n <p className=\"text-h4 text-ods-text-primary\">\n {formatReleaseDate(releaseDate)}\n </p>\n <p className=\"font-['DM_Sans'] font-medium text-[14px] leading-[20px] text-ods-text-secondary\">\n Release Date\n </p>\n </div>\n </div>\n\n {/* Author — the shared metadata-grid author cell (it was extracted\n FROM this page; rendering the export instead of an inline copy\n keeps the two in lockstep). The stub author preserves the legacy\n \"Unknown Author\" rendering for author-less releases; the\n job_title-over-roleLabel rule matches the guide detail page. */}\n <EntityMetadataAuthorCell\n author={author ?? { full_name: null, avatar_url: null }}\n authorHref={author?.full_name ? authorHref : undefined}\n />\n </div>\n\n {/* Image gallery — shared strip + lightbox (images) / inline clips. */}\n <MediaGalleryStrip items={releaseMedia ?? []} maxDisplay={5} />\n\n {/* Summary */}\n {releaseSummary && (\n <div className=\"text-h4 text-ods-text-primary\">\n <p>{releaseSummary}</p>\n </div>\n )}\n\n {/* Video Display Section - Injectable or fallback */}\n {VideoDisplaySection ? (\n <VideoDisplaySection\n mainVideoUrl={mainVideoUrl}\n youtubeUrl={youtubeUrl}\n highlightVideoUrl={highlightVideoUrl}\n highlightVideoThumbnail={highlightVideoThumbnail}\n title={releaseTitle}\n videoBites={videoBites}\n bitesTitle=\"Video Clips\"\n filterPublishedBites={true}\n srtContent={release?.srt_content as string | null | undefined}\n captionsUrl={release?.captionsUrl as string | undefined}\n />\n ) : (\n <>\n {/*\n Fallback when no `VideoDisplaySection` is injected. `<Video>` is the\n SSoT for every video surface — single source of truth across YouTube,\n HLS, and MP4 paths.\n */}\n {youtubeUrl && (\n <Video\n kind=\"youtube\"\n url={youtubeUrl}\n title={`${releaseTitle} - Video`}\n layout=\"native\"\n />\n )}\n {!youtubeUrl && mainVideoUrl && (\n <Video\n url={mainVideoUrl}\n srtContent={release?.srt_content as string | undefined}\n captionsUrl={release?.captionsUrl as string | undefined}\n layout=\"centered\"\n />\n )}\n {highlightVideoUrl && (\n <Video\n url={highlightVideoUrl}\n poster={highlightVideoThumbnail}\n layout=\"centered\"\n />\n )}\n </>\n )}\n\n {/* Content */}\n {releaseContent && (\n <div className=\"text-h4 text-ods-text-primary\">\n <MarkdownRenderer content={releaseContent} />\n </div>\n )}\n\n {/* Breaking Changes Warning */}\n {hasBreakingChanges && (\n <Card className=\"border-red-500 bg-red-500/10\">\n <CardContent className=\"p-6\">\n <div className=\"flex items-center gap-3\">\n <AlertTriangle className=\"h-6 w-6 text-red-500\" />\n <div>\n <h3 className=\"font-bold text-red-500 text-lg\">Breaking Changes</h3>\n <p className=\"text-ods-text-secondary\">This release contains breaking changes. Review carefully before upgrading.</p>\n </div>\n </div>\n </CardContent>\n </Card>\n )}\n\n {/* Changelog Sections — icons match the catalog card's changelog\n strip taxonomy (Sparkles/Wrench/TrendingUp/AlertTriangle) so the\n user sees a consistent visual signature across catalog → detail. */}\n <ReleaseChangelogSection\n title=\"Breaking Changes\"\n entries={breakingChanges || []}\n isBreaking\n hideTitle\n icon={<AlertTriangle className=\"h-6 w-6\" />}\n SimpleMarkdownRenderer={MarkdownRenderer}\n />\n {/* Features / Bugs / Improvements use `previewFirst` — same\n progressive-disclosure pattern as the investor-update detail\n page's Key Highlights / Financial Notes sections. Shows the\n first entry in full + fade-masks the rest, with a \"Show N\n more / Show less\" toggle. Breaking Changes (above) stays\n fully expanded — it's critical info, not skim-friendly. */}\n <ReleaseChangelogSection\n title=\"Features Added\"\n entries={featuresAdded || []}\n icon={<Sparkles className=\"h-6 w-6\" />}\n previewFirst\n SimpleMarkdownRenderer={MarkdownRenderer}\n />\n <ReleaseChangelogSection\n title=\"Bugs Fixed\"\n entries={bugFixed || []}\n icon={<Wrench className=\"h-6 w-6\" />}\n previewFirst\n SimpleMarkdownRenderer={MarkdownRenderer}\n />\n <ReleaseChangelogSection\n title=\"Improvements\"\n entries={improvements || []}\n icon={<TrendingUp className=\"h-6 w-6\" />}\n previewFirst\n SimpleMarkdownRenderer={MarkdownRenderer}\n />\n\n {/* Video Bites Section - Only when VideoDisplaySection is not handling it */}\n {!VideoDisplaySection && VideoSection && videoBites && videoBites.length > 0 && (\n <VideoSection\n bites={videoBites}\n title=\"Video Clips\"\n filterPublished={true}\n />\n )}\n\n {/* Related Roadmap Items */}\n {RoadmapSection && (roadmapLoading || roadmapTasks.length > 0) && (\n <div className=\"space-y-4 w-full\">\n <p className=\"text-h5 tracking-[-0.28px] text-ods-text-secondary\">\n Related Roadmap Items\n </p>\n <RoadmapSection\n items={roadmapTasks}\n isLoading={roadmapLoading}\n onItemUpdate={(updatedItem) => {\n setRoadmapTasks(prevTasks =>\n prevTasks.map(task =>\n task.id === updatedItem.id ? updatedItem : task\n )\n );\n }}\n />\n </div>\n )}\n\n {/* Bug-fixes & Enhancements Section */}\n {DeliverySection && (deliveryLoading || (deliveryData && (deliveryData.completed.length > 0 || deliveryData.inProgress.length > 0))) && (\n <div className=\"w-full space-y-4\">\n <p className=\"text-h5 tracking-[-0.28px] text-ods-text-secondary\">\n Related Enhancements and Bug-fixes\n </p>\n <DeliverySection\n data={deliveryData}\n isLoading={deliveryLoading}\n />\n </div>\n )}\n\n {/* Related Links */}\n {(githubReleases?.length || knowledgeBaseLinks?.length || migrationGuideUrl || documentationUrl) && (\n <div className=\"space-y-1 w-full\">\n <p className=\"text-h5 tracking-[-0.28px] text-ods-text-secondary\">\n Related Links\n </p>\n <Card className=\"bg-ods-card border-ods-border p-6\">\n <div className=\"space-y-4\">\n {/* GitHub Releases */}\n {githubReleases && githubReleases.length > 0 && (\n <>\n {githubReleases.map((ghRelease) => (\n <div key={ghRelease.id} className=\"flex items-start gap-1\">\n <GitHubIcon className=\"shrink-0\" width={24} height={24} color=\"var(--color-text-secondary)\" />\n <span className=\"text-h4 text-ods-text-primary\">\n Github Release\n </span>\n <a\n href={ghRelease.github_release_url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-h4 text-[#ffc008] hover:underline\"\n >\n {ghRelease.github_release_url.split('/').pop()}\n </a>\n <ExternalLink className=\"h-6 w-6 text-[#ffc008] shrink-0\" />\n </div>\n ))}\n </>\n )}\n\n {/* Knowledge Base Links */}\n {knowledgeBaseLinks && knowledgeBaseLinks.length > 0 && (\n <>\n {knowledgeBaseLinks.map((linkObj) => {\n const path = typeof linkObj === 'string' ? linkObj : linkObj.kb_article_path;\n const linkId = typeof linkObj === 'string' ? path : linkObj.id || path;\n return (\n <div key={linkId} className=\"flex items-start gap-1\">\n <BookMarked className=\"h-6 w-6 text-ods-text-secondary shrink-0\" />\n <span className=\"text-h4 text-ods-text-primary\">\n Knowledge Base\n </span>\n <Link\n href={path.startsWith('http') ? path : `/knowledge-base${path.startsWith('/') ? '' : '/'}${path}`}\n className=\"text-h4 text-[#ffc008] hover:underline\"\n >\n {path.replace(/^\\//, '').split('/').pop()?.replace(/-/g, ' ') || 'View Article'}\n </Link>\n <ExternalLink className=\"h-6 w-6 text-[#ffc008] shrink-0\" />\n </div>\n );\n })}\n </>\n )}\n\n {/* Migration Guide */}\n {migrationGuideUrl && (\n <div className=\"flex items-start gap-1\">\n <BookMarked className=\"h-6 w-6 text-ods-text-secondary shrink-0\" />\n <a\n href={migrationGuideUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-h4 text-[#ffc008] hover:underline\"\n >\n 📖 Migration Guide\n </a>\n <ExternalLink className=\"h-6 w-6 text-[#ffc008] shrink-0\" />\n </div>\n )}\n\n {/* Documentation */}\n {documentationUrl && (\n <div className=\"flex items-start gap-1\">\n <BookMarked className=\"h-6 w-6 text-ods-text-secondary shrink-0\" />\n <a\n href={documentationUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-h4 text-[#ffc008] hover:underline\"\n >\n 📚 Documentation\n </a>\n <ExternalLink className=\"h-6 w-6 text-[#ffc008] shrink-0\" />\n </div>\n )}\n </div>\n </Card>\n </div>\n )}\n </div>\n </PageLayout>\n </PageShell>\n );\n}\n","\"use client\";\n\nimport React, { useState } from 'react';\nimport { Video } from '../features/video';\nimport { ImageGalleryModal } from '../ui/image-gallery-modal';\n\nexport interface MediaGalleryStripItem {\n media_type: string;\n media_url: string;\n title?: string | null;\n /** Stable key when present (e.g. release_media rows); falls back to index. */\n id?: string;\n}\n\n/** Still-images open the lightbox; clips ('video'/'demo') play inline. */\nfunction isGalleryImage(mediaType: string): boolean {\n return mediaType !== 'video' && mediaType !== 'demo';\n}\n\nexport interface MediaGalleryStripProps {\n items: MediaGalleryStripItem[];\n /** Optional cap on tiles shown (e.g. product-release shows 5). Default: all. */\n maxDisplay?: number;\n className?: string;\n}\n\n/**\n * Read-only media gallery — a horizontal-scroll strip of 240×200 tiles where\n * still-images open an {@link ImageGalleryModal} lightbox and video clips play\n * inline via {@link Video}. The lightbox index is the tile's rank among the\n * IMAGE-only items (clips are skipped), so the modal opens on the correct image.\n *\n * Single source of truth for the detail-page media strip — replaces the markup\n * that was hand-rolled inline in the product-release and \"What I Shipped\" detail\n * pages (the release copy also had a raw-index lightbox bug this component fixes).\n */\nexport function MediaGalleryStrip({ items, maxDisplay, className }: MediaGalleryStripProps) {\n const [galleryOpen, setGalleryOpen] = useState(false);\n const [galleryIndex, setGalleryIndex] = useState(0);\n\n if (!items || items.length === 0) return null;\n\n // Explicit numeric check (clamped): `maxDisplay={0}` means \"show none\", which a\n // truthy check would wrongly treat as \"no cap → show all\".\n const display = typeof maxDisplay === 'number' ? items.slice(0, Math.max(0, maxDisplay)) : items;\n const galleryImages = display.filter((m) => isGalleryImage(m.media_type)).map((m) => m.media_url);\n\n const tileClass =\n 'shrink-0 w-[240px] h-[200px] rounded-md overflow-hidden border border-ods-border bg-black transition-opacity';\n\n return (\n <>\n <div className={`flex gap-6 overflow-x-auto w-full ${className ?? ''}`}>\n {display.map((mediaItem, index) => {\n // Image tiles open the lightbox, so they're real <button>s — keyboard\n // focusable + Enter/Space activatable. Clips render in <Video>, which\n // owns its own controls, so their tile stays a plain container.\n if (isGalleryImage(mediaItem.media_type)) {\n return (\n <button\n key={mediaItem.id || index}\n type=\"button\"\n className={`${tileClass} cursor-pointer hover:opacity-80`}\n aria-label={`Open ${mediaItem.title || `media ${index + 1}`} in gallery`}\n onClick={() => {\n // Lightbox position = rank among image-only items (clips skipped).\n setGalleryIndex(display.slice(0, index).filter((m) => isGalleryImage(m.media_type)).length);\n setGalleryOpen(true);\n }}\n >\n <img src={mediaItem.media_url} alt={mediaItem.title || `Media ${index + 1}`} className=\"w-full h-full object-cover\" />\n </button>\n );\n }\n return (\n <div key={mediaItem.id || index} className={tileClass}>\n <Video url={mediaItem.media_url} layout=\"native\" />\n </div>\n );\n })}\n </div>\n\n {galleryImages.length > 0 && (\n <ImageGalleryModal\n images={galleryImages}\n isOpen={galleryOpen}\n onClose={() => setGalleryOpen(false)}\n initialIndex={galleryIndex}\n />\n )}\n </>\n );\n}\n","\"use client\";\n\nimport { DetailPageSkeleton } from '../detail-page-skeleton';\n\nexport function ReleaseDetailSkeleton() {\n return <DetailPageSkeleton metadataColumns={4} showImageGallery={true} />;\n}\n","'use client';\n\n/**\n * RoadmapGrid — the shared roadmap LIST surface.\n *\n * Two modes (one component, one voting state):\n * - `groupByQuarter` (DEFAULT): buckets items by `item.quarter`, sorts the\n * quarters chronologically, and renders each in a collapsible `<Accordion>`\n * — the same quarter grouping the hub's roadmap page has, now shared so\n * every embedder gets it for free. Quarters at/after the current quarter\n * (and within `quartersToKeepClosed` of it) open by default; older ones\n * collapse. When `hasActiveFilters`, all quarters expand.\n * - `groupByQuarter={false}`: a flat 2-col grid (related-content rails that\n * pass a small pre-filtered `items` slice).\n *\n * Voting state (`useRoadmapVoting` + the in-flight set) lives ONCE at this\n * level and is shared across every quarter's grid, so a vote in Q3 and a vote\n * in Q4 can't race separate states. A successful vote triggers a single-task\n * refresh (`buildRefreshUrl`) and patches the parent list via `onItemUpdate`.\n *\n * Hydration: `expandedQuarters` starts `[]` and is populated in a client-only\n * effect (mirrors the hub) so SSR markup matches first client paint.\n */\n\nimport { useEffect, useRef, useState } from 'react';\nimport { RoadmapCard } from '../../chat/entity-cards/roadmap-card';\nimport { useRoadmapVoting, type UseRoadmapVotingOptions } from './use-roadmap-voting';\nimport { EmptyState } from '../../empty-state';\nimport {\n Accordion,\n AccordionItem,\n AccordionTrigger,\n AccordionContent,\n} from '../../ui';\nimport { cn } from '../../../utils/cn';\nimport { devSectionAnchorId } from '../../../utils/dev-sections/dev-section-param-keys';\nimport { contentFetch } from '../../../utils/embed-content-fetch';\nimport type { RoadmapItem } from '../../chat/types/entities/roadmap-item';\n\nconst DEFAULT_BUILD_REFRESH_URL = (taskId: string) => `/api/roadmap/${taskId}`;\nconst BACKLOG = 'Backlog';\n\n// ── Quarter helpers (pure; lifted from the hub roadmap section) ──────────────\n\n/** Status → sort priority (Complete → Working → Review → To-Do → other). */\nfunction getStatusPriority(status: string): number {\n const s = (status || '').toLowerCase();\n if (s.includes('complete') || s.includes('done')) return 1;\n if (s.includes('working') || s.includes('progress')) return 2;\n if (s.includes('review')) return 3;\n if (s.includes('to do') || s.includes('plan')) return 4;\n return 5;\n}\n\n/** Parse a `\"Q<n> <year>\"` label → {quarter, year}; `null` when unparseable. */\nfunction parseQuarterString(q: string): { quarter: number; year: number } | null {\n const match = q.match(/Q(\\d+)\\s+(\\d+)/);\n if (!match) return null;\n return { quarter: parseInt(match[1], 10), year: parseInt(match[2], 10) };\n}\n\nfunction compareQuarters(\n a: { quarter: number; year: number },\n b: { quarter: number; year: number },\n): number {\n if (a.year !== b.year) return a.year - b.year;\n return a.quarter - b.quarter;\n}\n\n/** Today's quarter — client-only (called from an effect, never during SSR). */\nfunction getCurrentQuarter(): { quarter: number; year: number } {\n const now = new Date();\n return { quarter: Math.floor(now.getMonth() / 3) + 1, year: now.getFullYear() };\n}\n\n/** Quarters open by default: current + future, plus the recent past within\n * `quartersToKeepClosed` of the current quarter; Backlog always open. */\nfunction computeDefaultExpandedQuarters(quarters: string[], quartersToKeepClosed: number): string[] {\n const currentQ = getCurrentQuarter();\n const out: string[] = [];\n for (const q of quarters) {\n if (q === BACKLOG) continue;\n const parsed = parseQuarterString(q);\n if (!parsed) continue;\n const diff = compareQuarters(parsed, currentQ);\n if (diff >= 0) {\n out.push(q);\n } else {\n const quartersAgo = currentQ.year * 4 + currentQ.quarter - (parsed.year * 4 + parsed.quarter);\n if (quartersAgo < quartersToKeepClosed) out.push(q);\n }\n }\n if (quarters.includes(BACKLOG)) out.push(BACKLOG);\n return out;\n}\n\nexport interface RoadmapGridProps {\n items: RoadmapItem[];\n onItemUpdate?: (updatedItem: RoadmapItem) => void;\n /** Show the desktop left margin (~120px) that aligns the grid with the page\n * hero. Default `true`. Related-content rails pass `false`. */\n showLeftMargin?: boolean;\n /** URL builder for the per-task refresh call after a successful vote. */\n buildRefreshUrl?: (taskId: string) => string;\n /** Voting hook options (vote endpoint + storage key). */\n votingOptions?: UseRoadmapVotingOptions;\n /** Group items into collapsible per-quarter accordions. Default `false`\n * (flat grid) so EXISTING callers — the hub's per-quarter RoadmapGrid calls\n * and related-content rails — stay unchanged. `RoadmapView` / full-page\n * callers pass `true` to get the shared quarter grouping. */\n groupByQuarter?: boolean;\n /** When true (search/filter active), every quarter expands so results aren't\n * hidden in collapsed sections. Threaded from `RoadmapView`. */\n hasActiveFilters?: boolean;\n /** Past quarters within this window of the current quarter stay open by\n * default; older ones collapse. Default `2`. */\n quartersToKeepClosed?: number;\n}\n\n/** Internal flat 2-col grid for ONE set of items. Voting comes from the parent\n * so the state is shared across quarters. */\nfunction RoadmapGridSingle({\n items,\n showLeftMargin,\n getVote,\n onVote,\n votingTasks,\n}: {\n items: RoadmapItem[];\n showLeftMargin: boolean;\n getVote: (taskId: string) => 'up' | 'down' | null;\n onVote: (taskId: string, voteType: 'up' | 'down') => void;\n votingTasks: Set<string>;\n}) {\n return (\n <div className={`grid grid-cols-1 md:grid-cols-2 gap-6 ${showLeftMargin ? 'md:ml-[120px]' : ''}`}>\n {items.map((item) => (\n // DOM id + sticky-header scroll offset live ON RoadmapCard's own\n // outer element (no wrapper div). Anchor mirrors\n // `buildDevSectionUrl('roadmap', <id>)` → `#roadmap-<external_id>`;\n // `useScrollToHash` in `roadmap-view.tsx` finds the card by id\n // and scrolls.\n <RoadmapCard\n key={item.id}\n item={item}\n id={devSectionAnchorId('roadmap', item.id)}\n userVote={getVote(item.id)}\n onVote={(voteType) => onVote(item.id, voteType)}\n isVoting={votingTasks.has(item.id)}\n />\n ))}\n </div>\n );\n}\n\nexport function RoadmapGrid({\n items,\n onItemUpdate,\n showLeftMargin = true,\n buildRefreshUrl = DEFAULT_BUILD_REFRESH_URL,\n votingOptions,\n groupByQuarter = false,\n hasActiveFilters = false,\n quartersToKeepClosed = 2,\n}: RoadmapGridProps) {\n // ── Voting (shared across all quarters) ──\n const { getVote, toggleVote } = useRoadmapVoting(votingOptions);\n const [votingTasks, setVotingTasks] = useState<Set<string>>(new Set());\n\n const handleVote = async (taskId: string, voteType: 'up' | 'down') => {\n if (votingTasks.has(taskId)) return;\n setVotingTasks((prev) => new Set(prev).add(taskId));\n try {\n const result = await toggleVote(taskId, voteType);\n if (result.success) {\n const response = await contentFetch(buildRefreshUrl(taskId));\n if (response.ok) {\n const data = await response.json();\n if (data.item && onItemUpdate) onItemUpdate(data.item);\n }\n }\n } finally {\n setVotingTasks((prev) => {\n const next = new Set(prev);\n next.delete(taskId);\n return next;\n });\n }\n };\n\n // ── Quarter bucketing + chronological sort (recomputed each render; cheap) ──\n const itemsByQuarter = items.reduce<Record<string, RoadmapItem[]>>((acc, item) => {\n const q = item.quarter || BACKLOG;\n (acc[q] ||= []).push(item);\n return acc;\n }, {});\n for (const q of Object.keys(itemsByQuarter)) {\n itemsByQuarter[q].sort((a, b) => getStatusPriority(a.status) - getStatusPriority(b.status));\n }\n const sortedQuarters = Object.keys(itemsByQuarter).sort((a, b) => {\n if (a === BACKLOG) return 1;\n if (b === BACKLOG) return -1;\n const aD = parseQuarterString(a) ?? { quarter: 0, year: 0 };\n const bD = parseQuarterString(b) ?? { quarter: 0, year: 0 };\n return compareQuarters(aD, bD);\n });\n const sortedQuartersKey = sortedQuarters.join(',');\n\n // ── Accordion expand state (hydration-safe: start [], populate in effects) ──\n const [expandedQuarters, setExpandedQuarters] = useState<string[]>([]);\n const [isInitialized, setIsInitialized] = useState(false);\n const hasSetInitialState = useRef(false);\n const prevItemsLength = useRef(0);\n\n // Initial expand state once data loads (runs once, or when data first arrives).\n useEffect(() => {\n const itemsJustLoaded = prevItemsLength.current === 0 && items.length > 0;\n prevItemsLength.current = items.length;\n if (sortedQuarters.length > 0 && (!hasSetInitialState.current || itemsJustLoaded)) {\n hasSetInitialState.current = true;\n setExpandedQuarters(\n hasActiveFilters\n ? [...sortedQuarters]\n : computeDefaultExpandedQuarters(sortedQuarters, quartersToKeepClosed),\n );\n setIsInitialized(true);\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [sortedQuarters.length, items.length]);\n\n // React to filter toggles AFTER init: filters on → expand all; off → defaults.\n useEffect(() => {\n if (!isInitialized || sortedQuarters.length === 0) return;\n setExpandedQuarters(\n hasActiveFilters\n ? [...sortedQuarters]\n : computeDefaultExpandedQuarters(sortedQuarters, quartersToKeepClosed),\n );\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [hasActiveFilters, sortedQuartersKey, isInitialized, quartersToKeepClosed]);\n\n if (items.length === 0) {\n return (\n <EmptyState\n type=\"generic\"\n title=\"No roadmap items\"\n description=\"Check back soon for upcoming features and improvements!\"\n />\n );\n }\n\n // Flat mode (related-content rails) — no accordion.\n if (!groupByQuarter) {\n return (\n <RoadmapGridSingle\n items={items}\n showLeftMargin={showLeftMargin}\n getVote={getVote}\n onVote={handleVote}\n votingTasks={votingTasks}\n />\n );\n }\n\n return (\n <Accordion\n type=\"multiple\"\n value={expandedQuarters}\n onValueChange={setExpandedQuarters}\n className=\"flex flex-col gap-10\"\n >\n {sortedQuarters.map((quarter) => {\n const itemCount = itemsByQuarter[quarter]?.length || 0;\n const isExpanded = expandedQuarters.includes(quarter);\n return (\n <AccordionItem\n key={quarter}\n value={quarter}\n id={`quarter-${quarter.replace(/\\s+/g, '-').toLowerCase()}`}\n className=\"border-0\"\n >\n <AccordionTrigger className=\"w-full p-0 hover:no-underline [&>svg]:h-5 [&>svg]:w-5 [&>svg]:text-ods-text-secondary [&>svg]:ml-auto [&>svg]:shrink-0\">\n <div className=\"flex items-center gap-3\">\n <h3\n className={cn(\n \"font-['Azeret_Mono'] font-semibold text-[24px] md:text-[28px] lg:text-[32px] leading-[32px] md:leading-[36px] lg:leading-[40px] text-ods-text-primary tracking-[-0.48px] md:tracking-[-0.56px] lg:tracking-[-0.64px] transition-opacity\",\n isExpanded ? 'opacity-100' : 'opacity-60',\n )}\n >\n {quarter}\n <span className=\"text-ods-accent\">:</span>\n </h3>\n <span\n className={cn(\n 'text-sm font-medium transition-opacity',\n isExpanded ? 'text-ods-text-secondary opacity-100' : 'text-ods-text-tertiary opacity-60',\n )}\n >\n {itemCount} {itemCount === 1 ? 'item' : 'items'}\n {isInitialized && !isExpanded && <span className=\"ml-2 text-ods-accent\">Click to expand</span>}\n </span>\n </div>\n </AccordionTrigger>\n <AccordionContent className=\"pt-4 pb-0 overflow-hidden data-[state=closed]:animate-none data-[state=open]:animate-none\">\n <RoadmapGridSingle\n items={itemsByQuarter[quarter]}\n showLeftMargin={showLeftMargin}\n getVote={getVote}\n onVote={handleVote}\n votingTasks={votingTasks}\n />\n </AccordionContent>\n </AccordionItem>\n );\n })}\n </Accordion>\n );\n}\n","'use client';\n\n/**\n * useRoadmapVoting — localStorage-backed optimistic voting for roadmap cards.\n *\n * One vote per task per user (storage key scoped per `storageKey` option,\n * default `'roadmap_votes_v1'`). Toggling the same vote removes it;\n * switching directions sends a remove + add pair so the server's running\n * totals stay correct.\n *\n * Endpoint configuration — `voteApiEndpoint`:\n * The hook posts to ONE endpoint (default `/api/roadmap/vote`) for\n * BOTH the optimistic add AND the opposite-vote remove. Reverse-proxy\n * embedders override this with their proxied path; lib otherwise\n * matches the hub's pre-migration call shape.\n */\n\nimport { useState, useEffect, useCallback } from 'react';\nimport { contentFetch } from '../../../utils/embed-content-fetch';\n\nexport type VoteType = 'up' | 'down' | null;\n\nexport interface VoteState {\n [taskId: string]: VoteType;\n}\n\nexport interface UseRoadmapVotingOptions {\n /** Vote endpoint URL. Default `/api/roadmap/vote`. */\n voteApiEndpoint?: string;\n /** localStorage key. Default `'roadmap_votes_v1'`. Embedders mounting\n * multiple roadmap surfaces in the same origin can scope per-surface\n * (e.g. `'roadmap_votes_v1_main'` vs `'roadmap_votes_v1_admin'`) so\n * votes don't cross-contaminate. */\n storageKey?: string;\n}\n\nconst DEFAULT_VOTE_ENDPOINT = '/api/roadmap/vote';\nconst DEFAULT_STORAGE_KEY = 'roadmap_votes_v1';\n\nexport function useRoadmapVoting(options: UseRoadmapVotingOptions = {}) {\n const voteApiEndpoint = options.voteApiEndpoint ?? DEFAULT_VOTE_ENDPOINT;\n const storageKey = options.storageKey ?? DEFAULT_STORAGE_KEY;\n\n const [votes, setVotes] = useState<VoteState>({});\n const [isLoading, setIsLoading] = useState(true);\n\n // Load votes from localStorage. Runs on mount AND whenever `storageKey`\n // changes — when the key changes mid-lifecycle (e.g. an embedder\n // remounts with a new namespace), we MUST reset state first so the\n // save-effect below doesn't write the old key's data into the new\n // key. We also re-enter the loading phase so the load completes\n // before any save runs.\n useEffect(() => {\n setIsLoading(true);\n setVotes({});\n try {\n const stored = localStorage.getItem(storageKey);\n if (stored) {\n setVotes(JSON.parse(stored));\n }\n } catch (error) {\n console.error('[Voting] Error loading votes from localStorage:', error);\n } finally {\n setIsLoading(false);\n }\n }, [storageKey]);\n\n // Save votes to localStorage whenever they change\n useEffect(() => {\n if (!isLoading) {\n try {\n localStorage.setItem(storageKey, JSON.stringify(votes));\n } catch (error) {\n console.error('[Voting] Error saving votes to localStorage:', error);\n }\n }\n }, [votes, isLoading, storageKey]);\n\n const getVote = useCallback(\n (taskId: string): VoteType => {\n return votes[taskId] || null;\n },\n [votes]\n );\n\n const toggleVote = useCallback(\n async (\n taskId: string,\n voteType: 'up' | 'down'\n ): Promise<{ success: boolean; newVote: VoteType; action: 'add' | 'remove' }> => {\n const currentVote = votes[taskId];\n\n let newVote: VoteType = null;\n let action: 'add' | 'remove' = 'add';\n\n if (currentVote === voteType) {\n // User clicked same vote - remove it\n newVote = null;\n action = 'remove';\n } else {\n // User clicked different vote - set it. If they had an opposite\n // vote, remove that first so the server totals stay consistent.\n if (currentVote) {\n await contentFetch(voteApiEndpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n taskId,\n voteType: currentVote,\n action: 'remove',\n }),\n }).catch(err => console.error('[Voting] Error removing opposite vote:', err));\n }\n\n newVote = voteType;\n action = 'add';\n }\n\n // Optimistic update\n setVotes(prev => ({\n ...prev,\n [taskId]: newVote,\n }));\n\n try {\n const response = await contentFetch(voteApiEndpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ taskId, voteType, action }),\n });\n\n if (!response.ok) {\n throw new Error('Vote API request failed');\n }\n\n return { success: true, newVote, action };\n } catch (error) {\n console.error('[Voting] API error:', error);\n\n // Revert optimistic update on error\n setVotes(prev => ({\n ...prev,\n [taskId]: currentVote,\n }));\n\n return { success: false, newVote: currentVote, action };\n }\n },\n [votes, voteApiEndpoint]\n );\n\n const clearVotes = useCallback(() => {\n setVotes({});\n localStorage.removeItem(storageKey);\n }, [storageKey]);\n\n return {\n votes,\n isLoading,\n getVote,\n toggleVote,\n clearVotes,\n };\n}\n","/**\n * RoadmapGridSkeleton — loading state for the `/roadmap` grid view.\n *\n * Pure JSX (no hooks, no events) — `'use client'` not strictly required\n * here; tsup's client-entry banner injects it automatically when this\n * file is bundled into the client output. We match the playbook's\n * skeleton-file convention (no directive when no hooks).\n *\n * NOTE: lib's `chat/entity-cards/roadmap-card.tsx` also exports a\n * `RoadmapCardSkeleton` — that one is the COMPACT 56px chat-card\n * variant. This file's internal card-skeleton (340px grid card)\n * intentionally stays file-internal to avoid the naming collision;\n * only `RoadmapGridSkeleton` is exported.\n */\n\nfunction RoadmapCardSkeleton() {\n return (\n <div className=\"bg-ods-card border border-ods-border rounded-[6px] p-[24px] flex flex-col gap-[16px] min-h-[340px] relative\">\n {/* Status Badge Skeleton - Top Right */}\n <div className=\"absolute top-[24px] right-[24px]\">\n <div className=\"h-[20px] w-[80px] bg-ods-border rounded animate-pulse\"></div>\n </div>\n\n {/* Icon and title skeleton */}\n <div className=\"flex items-center gap-[16px] pr-[120px]\">\n <div className=\"w-[80px] h-[80px] bg-ods-border rounded-lg flex-shrink-0 animate-pulse\"></div>\n <div className=\"flex-1 min-w-0 flex flex-col gap-1\">\n <div className=\"min-h-[48px] flex items-center\">\n <div className=\"h-[24px] w-full bg-ods-border rounded animate-pulse\"></div>\n </div>\n <div className=\"min-h-[20px] flex items-center\">\n <div className=\"h-[14px] w-1/2 bg-ods-border rounded animate-pulse\"></div>\n </div>\n </div>\n </div>\n\n {/* Description skeleton - exactly 3 lines */}\n <div className=\"min-h-[72px] flex items-center\">\n <div className=\"w-full space-y-2\">\n <div className=\"h-[24px] bg-ods-border rounded animate-pulse\"></div>\n <div className=\"h-[24px] bg-ods-border rounded animate-pulse\"></div>\n <div className=\"h-[24px] w-4/5 bg-ods-border rounded animate-pulse\"></div>\n </div>\n </div>\n\n <div className=\"flex-1\"></div>\n\n {/* Bottom skeleton */}\n <div className=\"flex items-center justify-between\">\n <div className=\"h-[48px] w-[120px] bg-ods-border rounded animate-pulse\"></div>\n <div className=\"h-[32px] w-[100px] bg-ods-border rounded animate-pulse\"></div>\n </div>\n </div>\n );\n}\n\nexport interface RoadmapGridSkeletonProps {\n /** Number of skeleton cards to show. Default 4. */\n count?: number;\n /** Show the desktop left margin (~120px) that aligns the grid with\n * the page hero's title block. Default `true`. Related-content rails\n * inside narrower surfaces (e.g. the release detail page) pass `false`. */\n showLeftMargin?: boolean;\n}\n\nexport function RoadmapGridSkeleton({ count = 4, showLeftMargin = true }: RoadmapGridSkeletonProps) {\n return (\n <div className={`grid grid-cols-1 md:grid-cols-2 gap-6 ${showLeftMargin ? 'md:ml-[120px]' : ''}`}>\n {Array.from({ length: count }).map((_, i) => (\n <RoadmapCardSkeleton key={i} />\n ))}\n </div>\n );\n}\n","'use client'\n\n/**\n * `<RoadmapView />` — the SELF-CONTAINED roadmap LIST surface.\n *\n * Fetches the roadmap list via the shared `useSelfFetch` hook and renders the\n * pure controlled `<RoadmapGrid>` (kept controlled so related-content rails can\n * still pass `items`). The host configures only **api routes**: the list\n * `endpoint` (default `/api/roadmap`), the per-task `buildRefreshUrl`, and the\n * vote endpoint via `votingOptions`. Optional `initialItems` hydrates SSR.\n */\n\nimport { useMemo } from 'react'\n\nimport { useSearchParams } from '../../../embed-shims'\nimport { LoadError } from '../../ui/error-state'\nimport { useSelfFetch } from '../../../hooks/use-self-fetch'\nimport { useScrollToHash } from '../../../hooks/use-scroll-to-hash'\nimport type { RoadmapItem } from '../../chat/types/entities/roadmap-item'\nimport { RoadmapGrid } from './roadmap-grid'\nimport { RoadmapGridSkeleton } from './roadmap-grid-skeleton'\nimport type { UseRoadmapVotingOptions } from './use-roadmap-voting'\nimport { DEV_SECTION_PARAM_KEYS } from '../../../utils/dev-sections/dev-section-param-keys'\nimport { STICKY_HEADER_OFFSET_PX } from '../../../utils/same-page-hash-nav'\n\nconst DEFAULT_ENDPOINT = '/api/roadmap'\n// Defaults sourced from the ONE param-key registry the chrome (OPENFRAME_DEV_SECTIONS) also\n// reads, so the chrome's written `?key=` and this view's read can't silently diverge.\nconst DEFAULT_SEARCH_PARAM_KEY = DEV_SECTION_PARAM_KEYS.search\nconst DEFAULT_STATUS_PARAM_KEY = DEV_SECTION_PARAM_KEYS.status\n\nexport interface RoadmapViewProps {\n /** GET list endpoint (the api route). Returns `{ items }`. Default\n * `/api/roadmap`. */\n endpoint?: string\n /** Optional SSR hydrate — skips the initial client fetch. */\n initialItems?: RoadmapItem[]\n showLeftMargin?: boolean\n /** Per-task refresh URL builder (after a vote). Default `/api/roadmap/<id>`. */\n buildRefreshUrl?: (taskId: string) => string\n /** Voting hook options (vote endpoint + storage key). */\n votingOptions?: UseRoadmapVotingOptions\n /** URL param key for the search input — MUST match the section chrome\n * (`DevSectionView`) that writes it. Default `'search'`. */\n searchParamKey?: string\n /** URL param key for the status filter. Default `'status'` (the roadmap\n * section's `filter.paramKey`). `'all'` means no filter. */\n statusParamKey?: string\n}\n\nexport function RoadmapView({\n endpoint = DEFAULT_ENDPOINT,\n initialItems,\n showLeftMargin,\n buildRefreshUrl,\n votingOptions,\n searchParamKey = DEFAULT_SEARCH_PARAM_KEY,\n statusParamKey = DEFAULT_STATUS_PARAM_KEY,\n}: RoadmapViewProps = {}) {\n // Read the search + status params the section chrome (`DevSectionView`) writes\n // and fold them INTO the fetch url so the url IS the cache key — the list\n // refetches filtered whenever the controls change. Mirrors `ProductReleasesView`.\n const searchParams = useSearchParams()\n const search = searchParams.get(searchParamKey) || ''\n const status = searchParams.get(statusParamKey) || 'all'\n const listParams = new URLSearchParams()\n if (search) listParams.set(searchParamKey, search)\n if (status && status !== 'all') listParams.set(statusParamKey, status)\n const qs = listParams.toString()\n const url = qs ? `${endpoint}?${qs}` : endpoint\n\n // Memoize so the SSR `initialItems` wrapper keeps a STABLE identity — else the\n // hook's initialData re-sync effect fires every render and clobbers the\n // optimistic vote patch below.\n const initialData = useMemo(() => (initialItems ? { items: initialItems } : undefined), [initialItems])\n const { data, setData, isLoading, error, reload } = useSelfFetch<{ items?: RoadmapItem[] }>(\n url,\n { initialData },\n )\n const items = data?.items ?? []\n\n // Deep-link hash dispatch — `?search=<id>#roadmap-<id>` from a chat card.\n // Shared hook owns the poll-until-mount + hashchange-listener wiring\n // (same instance used by DeliveryLists). The rAF poll inside the hook\n // handles the Radix `<AccordionItem>` lazy-unmount: on first paint\n // every quarter is collapsed; an effect in `roadmap-grid.tsx` expands\n // them when `hasActiveFilters` is true (chat URL carries `?search=<id>`).\n // The card mounts one tick after `data` lands; the hook waits.\n useScrollToHash(data, { headerOffset: STICKY_HEADER_OFFSET_PX })\n\n if (error) {\n return <LoadError message=\"Failed to load roadmap.\" onRetry={reload} />\n }\n // Skeleton only while the FIRST fetch is in flight (no data yet) — a malformed\n // body lacking `items` renders the grid (empty), never a stuck skeleton.\n if (isLoading && !data) {\n return <RoadmapGridSkeleton showLeftMargin={showLeftMargin} />\n }\n\n return (\n <RoadmapGrid\n items={items}\n showLeftMargin={showLeftMargin}\n buildRefreshUrl={buildRefreshUrl}\n votingOptions={votingOptions}\n // Full-page roadmap → collapsible quarter grouping (the shared RoadmapGrid\n // capability; flat grids stay the RoadmapGrid default). When the chrome's\n // search / status filter is active, expand every quarter so matches aren't hidden.\n groupByQuarter\n hasActiveFilters={search !== '' || status !== 'all'}\n // After a vote refreshes a single task, patch it into the fetched list so\n // the displayed counts stay live.\n onItemUpdate={(updated) =>\n setData((prev) =>\n prev\n ? { ...prev, items: (prev.items ?? []).map((it) => (it.id === updated.id ? updated : it)) }\n : prev,\n )\n }\n />\n )\n}\n","'use client';\n\n/**\n * DeliveryLists — the delivery section body (two tables: recently\n * completed + active). Reads `search` and `task_type` URL params\n * written by the shared `<DevSectionView>` chrome and refetches on\n * change.\n *\n * Endpoint configuration:\n * - `completedApiEndpoint` / `inProgressApiEndpoint` are the two\n * per-bucket GET endpoints. Defaults match the hub's pre-migration\n * routes (`/api/delivery/completed`, `/api/delivery/in-progress`).\n *\n * Coupling constraint — `searchParamKey` / `taskTypeParamKey`:\n * These props serve TWO purposes:\n * 1. URL READS — keys this component reads via `useSearchParams()`.\n * MUST match the consuming chrome's `section.search.paramKey` /\n * `section.filter.paramKey` (the chrome WRITES the URL params).\n * 2. API WRITES — keys this component sends as query params on the\n * outbound fetch to `{completedApiEndpoint,inProgressApiEndpoint}`.\n * The hub API contract uses `'search'` / `'task_type'`; embedders\n * reverse-proxying those routes must preserve the same names OR\n * rewrite the inbound query string on the proxy side.\n *\n * Defaults align with `OPENFRAME_DEV_SECTIONS.delivery.{search.paramKey,filter.paramKey}`\n * AND the hub API contract, so the OpenFrame zero-config case \"just\n * works\". Custom chrome overriding the param keys must override BOTH\n * ends consistently AND ensure the backend reads the same names.\n */\n\nimport { useEffect, useState } from 'react';\nimport { useSearchParams, useRouter, usePathname } from '../../../embed-shims';\nimport type { DeliveryResponse } from '../../../types/delivery';\nimport { DeliveryTable } from './delivery-table';\nimport { EmptyState } from '../../empty-state';\nimport { LoadError } from '../../ui/error-state';\nimport { DEV_SECTION_PARAM_KEYS } from '../../../utils/dev-sections/dev-section-param-keys';\nimport { STICKY_HEADER_OFFSET_PX } from '../../../utils/same-page-hash-nav';\nimport { contentFetch } from '../../../utils/embed-content-fetch';\nimport { useScrollToHash } from '../../../hooks/use-scroll-to-hash';\n\nconst DEFAULT_COMPLETED_ENDPOINT = '/api/delivery/completed';\nconst DEFAULT_IN_PROGRESS_ENDPOINT = '/api/delivery/in-progress';\n// Param keys sourced from the shared registry (see RoadmapView) — single source for the\n// chrome's written `?key=` and this view's read.\nconst DEFAULT_SEARCH_PARAM_KEY = DEV_SECTION_PARAM_KEYS.search;\nconst DEFAULT_TASK_TYPE_PARAM_KEY = DEV_SECTION_PARAM_KEYS.deliveryTaskType;\n\nexport interface DeliveryListsProps {\n /** GET endpoint for the \"Recently Completed\" bucket. Default\n * `/api/delivery/completed`. */\n completedApiEndpoint?: string;\n /** GET endpoint for the \"Active Tasks\" bucket. Default\n * `/api/delivery/in-progress`. */\n inProgressApiEndpoint?: string;\n /** URL param key for the search input. MUST match the consuming\n * chrome's `section.search.paramKey`. Default `'search'`. */\n searchParamKey?: string;\n /** URL param key for the task-type filter. MUST match the consuming\n * chrome's `section.filter.paramKey`. Default `'task_type'`. */\n taskTypeParamKey?: string;\n}\n\nexport function DeliveryLists({\n completedApiEndpoint = DEFAULT_COMPLETED_ENDPOINT,\n inProgressApiEndpoint = DEFAULT_IN_PROGRESS_ENDPOINT,\n searchParamKey = DEFAULT_SEARCH_PARAM_KEY,\n taskTypeParamKey = DEFAULT_TASK_TYPE_PARAM_KEY,\n}: DeliveryListsProps = {}) {\n const searchParams = useSearchParams();\n const router = useRouter();\n const pathname = usePathname();\n\n const [data, setData] = useState<DeliveryResponse | null>(null);\n const [isLoading, setIsLoading] = useState(true);\n const [error, setError] = useState<string | null>(null);\n\n // Get filter state from URL\n const searchQuery = searchParams.get(searchParamKey) || '';\n const taskTypeFilter = searchParams.get(taskTypeParamKey) || 'all';\n\n useEffect(() => {\n async function fetchDeliveryData() {\n try {\n setIsLoading(true);\n setError(null);\n\n // Build query parameters for filtering. The outbound key names\n // mirror the inbound URL-param keys — see \"Coupling constraint\"\n // in the file docblock for why.\n const params = new URLSearchParams();\n if (searchQuery) {\n params.set(searchParamKey, searchQuery);\n }\n if (taskTypeFilter && taskTypeFilter !== 'all') {\n params.set(taskTypeParamKey, taskTypeFilter);\n }\n const queryString = params.toString();\n const queryParam = queryString ? `?${queryString}` : '';\n\n // Fetch completed and in-progress tasks separately with filters\n const [completedResponse, inProgressResponse] = await Promise.all([\n contentFetch(`${completedApiEndpoint}${queryParam}`),\n contentFetch(`${inProgressApiEndpoint}${queryParam}`),\n ]);\n\n if (!completedResponse.ok || !inProgressResponse.ok) {\n throw new Error('Failed to fetch delivery items');\n }\n\n const [completedResult, inProgressResult] = await Promise.all([\n completedResponse.json(),\n inProgressResponse.json(),\n ]);\n\n setData({\n completed: completedResult.items || [],\n inProgress: inProgressResult.items || [],\n });\n } catch (err) {\n console.error('Error fetching delivery items:', err);\n setError('Failed to load delivery items. Please try again later.');\n } finally {\n setIsLoading(false);\n }\n }\n\n fetchDeliveryData();\n }, [searchQuery, taskTypeFilter, completedApiEndpoint, inProgressApiEndpoint, searchParamKey, taskTypeParamKey]);\n\n const filteredCompleted = data?.completed || [];\n const filteredInProgress = data?.inProgress || [];\n\n // Deep-link hash dispatch — `?search=<id>#delivery-<id>` from a chat\n // card or a linked-delivery card on a ticket. Shared hook owns the\n // poll-until-mount + hashchange-listener wiring (same instance used\n // by RoadmapView). 96 matches the sticky-header offset every\n // hash-scroll surface in the app uses.\n useScrollToHash(data, { headerOffset: STICKY_HEADER_OFFSET_PX });\n\n const showCompleted = true;\n const showInProgress = true;\n\n const hasActiveFilters = searchQuery !== '' || taskTypeFilter !== 'all';\n const hasResults = (showCompleted && filteredCompleted.length > 0) || (showInProgress && filteredInProgress.length > 0);\n\n // Error state — consume lib's canonical LoadError so ODS tokens +\n // retry affordance stay in lockstep with every other surface.\n if (error) {\n return (\n <div className=\"w-full\">\n <LoadError message={error} onRetry={() => window.location.reload()} />\n </div>\n );\n }\n\n return (\n <div className=\"w-full flex flex-col gap-[40px]\">\n {/* Empty state if no results after filtering */}\n {!isLoading && !hasResults && (\n hasActiveFilters ? (\n <EmptyState\n type=\"search\"\n title=\"No tasks found\"\n description=\"No tasks match your current filters. Try adjusting your search or status filter.\"\n showCTA={true}\n ctaText=\"Reset Filters\"\n onCtaClick={() => {\n const params = new URLSearchParams(searchParams.toString());\n params.delete(searchParamKey);\n params.delete(taskTypeParamKey);\n router.replace(`${pathname}?${params.toString()}`, { scroll: false });\n }}\n />\n ) : (\n <EmptyState\n type=\"generic\"\n title=\"No tasks available\"\n description=\"Check back soon for upcoming tasks!\"\n showCTA={false}\n />\n )\n )}\n\n {/* Completed Tasks Table */}\n {showCompleted && (hasResults || isLoading) && (\n <div className=\"w-full\">\n <h3 className=\"text-h2 text-ods-text-primary tracking-[-0.48px] md:tracking-[-0.56px] lg:tracking-[-0.64px] mb-4\">\n Recently Completed<span className=\"text-ods-accent\">:</span>\n </h3>\n <DeliveryTable\n items={filteredCompleted}\n isLoading={isLoading}\n />\n </div>\n )}\n\n {/* In Progress Tasks Table */}\n {showInProgress && (hasResults || isLoading) && (\n <div className=\"w-full\">\n <h3 className=\"text-h2 text-ods-text-primary tracking-[-0.48px] md:tracking-[-0.56px] lg:tracking-[-0.64px] mb-4\">\n Active Tasks<span className=\"text-ods-accent\">:</span>\n </h3>\n <DeliveryTable\n items={filteredInProgress}\n isLoading={isLoading}\n />\n </div>\n )}\n </div>\n );\n}\n","'use client';\n\n/**\n * DeliveryTable — bordered card containing one `<DeliveryRow />` per\n * item. Visual rendering of each row lives in `delivery-row.tsx` so the\n * exact same primitive can be composed elsewhere (notably the linked-\n * delivery surface inside `<TicketDetailDrawer>`).\n *\n * Props:\n * - `items` — flat list of `DeliveryItem`. Two buckets (completed +\n * in-progress) are rendered as two separate `DeliveryTable`s by\n * the parent `DeliveryLists`.\n * - `isLoading` — skeleton rows.\n * - `focusId` — `?focus=<id>` URL param. Marks the matching row\n * `id=\"delivery-<id>\"` and applies the highlight ring so the\n * deep-link from a ticket's linked-card scrolls + flashes the\n * right row.\n */\n\nimport { DeliveryRow } from './delivery-row';\nimport type { DeliveryItem } from '../../../types/delivery';\nimport { devSectionAnchorId } from '../../../utils/dev-sections/dev-section-param-keys';\n\ninterface DeliveryTableProps {\n items: DeliveryItem[];\n isLoading?: boolean;\n}\n\n/**\n * Skeleton loader for rows - matching responsive structure\n */\nfunction SkeletonRow() {\n return (\n <div className=\"border-b border-ods-border last:border-b-0 p-[12px] md:p-[16px]\">\n <div className=\"flex flex-col md:flex-row items-start justify-between gap-[12px] md:gap-[16px] w-full\">\n {/* Left: Title, subtitle, and description skeleton */}\n <div className=\"flex-1 min-w-0 w-full md:w-auto flex flex-col gap-[12px] md:gap-[16px]\">\n {/* Title skeleton - responsive */}\n <div className=\"min-h-[24px] flex items-center\">\n <div className=\"h-[20px] bg-ods-border rounded animate-pulse w-full\"></div>\n </div>\n {/* Subtitle skeleton - 1 line */}\n <div className=\"min-h-[20px] flex items-center\">\n <div className=\"h-[20px] bg-ods-border rounded animate-pulse w-1/2\"></div>\n </div>\n {/* Description skeleton - 3 lines */}\n <div className=\"min-h-[72px] flex items-center\">\n <div className=\"flex-1 space-y-1\">\n <div className=\"h-[20px] bg-ods-border rounded animate-pulse w-full\"></div>\n <div className=\"h-[20px] bg-ods-border rounded animate-pulse w-full\"></div>\n <div className=\"h-[20px] bg-ods-border rounded animate-pulse w-2/3\"></div>\n </div>\n </div>\n </div>\n\n {/* Right: Badge skeleton - two stacked badges */}\n <div className=\"flex-shrink-0 self-start flex flex-col gap-2\">\n <div className=\"h-[32px] w-[100px] bg-ods-border rounded animate-pulse\"></div>\n <div className=\"h-[32px] w-[120px] bg-ods-border rounded animate-pulse\"></div>\n </div>\n </div>\n </div>\n );\n}\n\n/**\n * DeliveryTable Component\n * Displays bug fixes and enhancements with fixed-height rows\n */\nexport function DeliveryTable({ items, isLoading = false }: DeliveryTableProps) {\n // Show skeletons while loading\n if (isLoading) {\n return (\n <div className=\"bg-ods-card border border-ods-border rounded-[6px] overflow-hidden w-full\">\n <div className=\"w-full\">\n {[1, 2, 3, 4, 5].map((i) => (\n <SkeletonRow key={i} />\n ))}\n </div>\n </div>\n );\n }\n\n // Empty state\n if (items.length === 0) {\n return (\n <div className=\"bg-ods-card border border-ods-border rounded-[6px] p-[40px] text-center w-full\">\n <p className=\"text-ods-text-secondary text-[14px] font-['DM_Sans'] font-medium\">\n No tasks available\n </p>\n </div>\n );\n }\n\n return (\n <div className=\"bg-ods-card border border-ods-border rounded-[6px] overflow-hidden w-full\">\n <div className=\"w-full\">\n {items.map((item) => (\n // DOM id lives on DeliveryRow's own outer element (no wrapper\n // div). Anchor mirrors `buildDevSectionUrl('delivery', <id>)`\n // → `#delivery-<external_id>`; `useScrollToHash` in\n // `delivery-lists.tsx` finds the row by id and scrolls. The\n // outer wrapper here ONLY exists for the row separators.\n <div\n key={item.id}\n className=\"border-b border-ods-border last:border-b-0\"\n >\n <DeliveryRow item={item} id={devSectionAnchorId('delivery', item.id)} />\n </div>\n ))}\n </div>\n </div>\n );\n}\n","'use client';\n\n/**\n * LegalDocumentPage — unified UI for privacy-policy, terms-of-service,\n * and any other markdown-backed legal document.\n *\n * Replaces two near-identical hub components (`PrivacyPolicyPage` +\n * `TermsOfServicePage`) that differed only in title, contact email,\n * and copy strings. Caller passes those as props.\n *\n * Markdown rendering: defaults to lib's `SimpleMarkdownRenderer`\n * (sufficient for plain-markdown legal docs). Embedders that need\n * richer markdown (embeds, video, OG previews) pass their own via\n * the `MarkdownRenderer` prop — same injection pattern as\n * `ReleaseDetailPage`.\n *\n * Endpoint configuration: forwarded to `useLegalDocs(docType, { apiEndpoint })`.\n */\n\nimport type { ComponentType } from 'react';\nimport { PageShell, PageLayout, PageHeading } from '../../ui';\nimport { RichMarkdownRenderer } from '../../ui/rich-markdown-renderer';\nimport { useRouter } from '../../../embed-shims/next-navigation';\nimport { useLegalDocs, type LegalDocument } from './use-legal-docs';\nimport { formatLegalDate } from '../../../utils/format';\n\nexport interface LegalDocumentMarkdownRendererProps {\n content: string;\n sectionIds?: Array<{ id: string; title: string; level: number }>;\n demoteMarkdownH1ToH2?: boolean;\n}\n\nexport interface LegalDocumentPageProps {\n /** Document type identifier — drives the default API endpoint\n * `/api/legal/<docType>` AND the error-log prefix. Common values:\n * `'privacy'`, `'terms'`. Embedders may use any string. */\n docType: string;\n /** Heading text (e.g. \"Privacy Policy\", \"Terms of Service\"). */\n title: string;\n /** Fallback subtitle shown when no `lastUpdated` date is available\n * (e.g. \"Our privacy policy and data protection practices\"). */\n fallbackDescription: string;\n /** Email shown in the error + empty-state copy\n * (e.g. `'privacy@openframe.io'`, `'legal@openframe.io'`). */\n contactEmail: string;\n /** Prompt shown above the contact link in the error state\n * (e.g. \"For privacy-related questions, please contact:\"). */\n errorContactPrompt: string;\n /** Title for the error block (e.g. \"Unable to load privacy policy\"). */\n errorTitle: string;\n /** Sentence shown when the API returns no document\n * (e.g. \"Privacy policy content is not available at this time.\"). */\n emptyStateMessage: string;\n /** SSR-prepared document, if available. */\n initialData?: LegalDocument | null;\n /** SSR-prepared formatted \"Last Updated\" label. Stable across hydration. */\n initialLastUpdatedLabel?: string | null;\n /** Override the default `/api/legal/<docType>` endpoint\n * (reverse-proxy embedders, alternate API paths). */\n apiEndpoint?: string;\n /** Override the default markdown renderer. */\n MarkdownRenderer?: ComponentType<LegalDocumentMarkdownRendererProps>;\n /** Back-button config — same pattern as `DevSectionPage`. Pass `false`\n * to hide. Default `{ label: 'Back to home', href: '/' }`. */\n backButton?: { label?: string; href?: string } | false;\n}\n\nexport function LegalDocumentPage({\n docType,\n title,\n fallbackDescription,\n contactEmail,\n errorContactPrompt,\n errorTitle,\n emptyStateMessage,\n initialData = null,\n initialLastUpdatedLabel = null,\n apiEndpoint,\n MarkdownRenderer = RichMarkdownRenderer,\n backButton,\n}: LegalDocumentPageProps) {\n const router = useRouter();\n const { data, isLoading, error } = useLegalDocs(docType, { initialData, apiEndpoint });\n\n // Back-button config — mirrors DevSectionPage's `{ label: 'Back to home',\n // onClick: () => router.push('/') }`. Hide entirely when caller passes\n // `false` (e.g. embed-mode where the host owns navigation chrome).\n const backCfg =\n backButton === false\n ? undefined\n : {\n label: backButton?.label ?? 'Back to home',\n onClick: () => router.push(backButton?.href ?? '/'),\n };\n\n const fallbackLastUpdatedLabel =\n data?.lastSynced != null ? formatLegalDate(data.lastSynced) : null;\n const effectiveLastUpdatedLabel = initialLastUpdatedLabel ?? fallbackLastUpdatedLabel;\n\n // Title with accent-colon trailing dot — matches knowledge-hub typography\n const customTitle = (\n <div className=\"flex flex-col gap-4\">\n <PageHeading>\n <span>{title}</span>\n <span className=\"text-ods-accent\">.</span>\n </PageHeading>\n <p className=\"font-['DM_Sans'] text-base md:text-lg text-ods-text-secondary max-w-2xl\">\n {effectiveLastUpdatedLabel ? `Last Updated: ${effectiveLastUpdatedLabel}` : fallbackDescription}\n {data?.sourceFile && (\n <span className=\"block text-sm mt-1 opacity-75\">Source: {data.sourceFile}</span>\n )}\n </p>\n </div>\n );\n\n return (\n <PageShell>\n <PageLayout backButton={backCfg}>\n <div className=\"flex flex-col gap-4\">{customTitle}</div>\n\n <div className=\"flex flex-col lg:flex-row gap-6 lg:gap-10 items-start flex-1\">\n <div className=\"flex-1\">\n <div className=\"w-full\">\n <article className=\"space-y-2\">\n {isLoading ? (\n // Loading skeleton matching Knowledge Hub pattern\n <div className=\"space-y-6\">\n <div className=\"h-10 bg-ods-skeleton rounded-lg w-3/4 animate-pulse\"></div>\n <div className=\"space-y-4\">\n <div className=\"h-4 bg-ods-skeleton rounded w-full animate-pulse\"></div>\n <div className=\"h-4 bg-ods-skeleton rounded w-full animate-pulse\"></div>\n <div className=\"h-4 bg-ods-skeleton rounded w-5/6 animate-pulse\"></div>\n </div>\n <div className=\"h-32 bg-ods-card border border-ods-border rounded-lg animate-pulse\"></div>\n <div className=\"space-y-4\">\n <div className=\"h-4 bg-ods-skeleton rounded w-full animate-pulse\"></div>\n <div className=\"h-4 bg-ods-skeleton rounded w-4/5 animate-pulse\"></div>\n </div>\n </div>\n ) : error ? (\n <div className=\"text-center space-y-4\">\n <div className=\"bg-red-900/20 border border-red-700 rounded-lg p-6\">\n <p className=\"text-red-400 mb-2\">{errorTitle}</p>\n <p className=\"text-red-300 text-sm\">{error}</p>\n </div>\n <div className=\"text-ods-text-secondary\">\n <p>{errorContactPrompt}</p>\n <a href={`mailto:${contactEmail}`} className=\"text-ods-accent hover:underline\">\n {contactEmail}\n </a>\n </div>\n </div>\n ) : data ? (\n <MarkdownRenderer\n content={data.content}\n sectionIds={data.sections || []}\n demoteMarkdownH1ToH2\n />\n ) : (\n <div className=\"text-center text-ods-text-secondary py-16\">\n <p className=\"text-xl\">{emptyStateMessage}</p>\n <p className=\"mt-2\">\n Please contact{' '}\n <a href={`mailto:${contactEmail}`} className=\"text-ods-accent hover:underline\">\n {contactEmail}\n </a>{' '}\n for more information.\n </p>\n </div>\n )}\n </article>\n </div>\n </div>\n </div>\n </PageLayout>\n </PageShell>\n );\n}\n","'use client';\n\n/**\n * useLegalDocs — fetches a legal document (privacy policy, terms of\n * service, or any other markdown-backed legal page) from a hub API.\n *\n * Endpoint configuration — `apiEndpoint`:\n * Default `/api/legal/<docType>`. Reverse-proxy embedders override\n * with their proxied path (e.g. `/proxy/legal/privacy`).\n *\n * Data shape mirrors the hub's `lib/data/legal-utils.ts:LegalDocument`\n * server type. The hook intentionally re-declares the type here so\n * lib consumers don't need to import a server-side type.\n */\n\nimport { useState, useEffect, useCallback } from 'react';\nimport { contentFetch } from '../../../utils/embed-content-fetch';\n\nexport interface LegalDocument {\n title: string;\n content: string;\n sourceFile: string;\n lastSynced: string | null;\n githubSha: string | null;\n sections: Array<{ id: string; title: string; level: number }>;\n docType: string;\n meta: {\n sectionsCount: number;\n contentLength: number;\n lastSyncedAgo: string;\n };\n}\n\nexport interface UseLegalDocsReturn {\n data: LegalDocument | null;\n isLoading: boolean;\n error: string | null;\n refetch: () => void;\n}\n\nexport interface UseLegalDocsOptions {\n /** Optional pre-fetched payload from server (SSR / RSC). When set,\n * the hook skips the initial client fetch. */\n initialData?: LegalDocument | null;\n /** Full GET endpoint URL. Default `/api/legal/<docType>`. */\n apiEndpoint?: string;\n}\n\n/**\n * Hook to fetch a legal document.\n * @param docType — short identifier for the document (drives the\n * default endpoint path AND the error-log prefix). Common values:\n * `'privacy'` (SECURITY.md), `'terms'` (LICENSE). Embedders may use\n * any string — the hook treats it as opaque.\n */\nexport function useLegalDocs(\n docType: string,\n options: UseLegalDocsOptions = {}\n): UseLegalDocsReturn {\n const { initialData = null, apiEndpoint } = options;\n const effectiveEndpoint = apiEndpoint ?? `/api/legal/${docType}`;\n\n const [data, setData] = useState<LegalDocument | null>(initialData ?? null);\n const [isLoading, setIsLoading] = useState(!initialData);\n const [error, setError] = useState<string | null>(null);\n\n const fetchDocument = useCallback(async () => {\n try {\n setIsLoading(true);\n setError(null);\n\n const response = await contentFetch(effectiveEndpoint);\n\n if (!response.ok) {\n throw new Error(\n `Failed to fetch ${docType} document: ${response.status} ${response.statusText}`\n );\n }\n\n const result = await response.json();\n\n // Validate the response has required fields\n if (!result.content) {\n throw new Error(`${docType} document content is empty`);\n }\n\n setData(result);\n } catch (err) {\n const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';\n // `docType` is externally controlled (URL path segment in embedders), so it must NOT sit in\n // console.error's FIRST (format-string) argument — Node interprets %s/%j/%o there\n // (CodeQL js/tainted-format-string). Keep the format string constant; pass docType + err as\n // plain trailing args.\n console.error('Error fetching legal document:', docType, err);\n setError(errorMessage);\n } finally {\n setIsLoading(false);\n }\n }, [docType, effectiveEndpoint]);\n\n // Reset cached data when docType changes — otherwise an embedder using\n // the same hook instance for sequential docTypes (privacy → terms)\n // would briefly render the OLD doc's content while the new fetch is\n // in-flight. Not currently triggered by hub's per-route SSR (each\n // docType mounts in a fresh component), but enforces the contract.\n useEffect(() => {\n setData(initialData ?? null);\n setError(null);\n setIsLoading(!initialData);\n }, [docType, initialData]);\n\n // Fetch on mount (only if we don't already have server-provided initialData)\n useEffect(() => {\n if (initialData) return;\n fetchDocument();\n }, [fetchDocument, initialData]);\n\n const refetch = () => {\n fetchDocument();\n };\n\n return {\n data,\n isLoading,\n error,\n refetch,\n };\n}\n","'use client'\n\n/**\n * Author detail-page body — identity header (avatar → name → job title →\n * socials), bio, expertise badges, and a `children` slot for the authored-\n * content rail. THE one implementation behind the hub's `/authors/[slug]`\n * page AND embedded author pages (react-embedding-example) — hosts fetch\n * the `AuthorProfile` however they like and drill it in.\n *\n * Embed-readiness contract (same rules as ArticleAuthorByline):\n * - `Image` renders through the embed shim (plain `<img>` in non-Next\n * hosts; the real `next/image` once registered at app init).\n * - Avatar proxying rides the OPTIONAL ambient `ChatRuntime`\n * (`endpoints.imageProxyUrlPrefix`) — no provider ⇒ raw URL, never\n * throws. `proxyImageUrl` prop wins over the runtime.\n * - The authored-content rail is a SLOT (`children`), not a baked-in\n * fetch: the hub passes its pre-bound `RelatedContentSection` with SSR\n * `initialItems`; embedders pass the lib section with `apiBaseUrl`.\n * The view stays data-in, markup-out.\n *\n * Heading semantics are the HOST's: the view renders the name as a styled\n * `<p>` (like the hub profile header) so a hosting page can own its `<h1>`\n * (the hub uses an sr-only h1 for crawlers).\n */\n\nimport React from 'react'\nimport Image from '../../embed-shims/next-image'\nimport { cn } from '../../utils/cn'\nimport { formatBioText } from '../../utils/format'\nimport { getProxiedImageUrl } from '../../utils/image-proxy'\nimport { useChatRuntime } from '../../contexts/chat-runtime-context'\nimport { SocialIconRow } from '../social-icon-row'\nimport { StatusBadge } from '../ui/status-badge'\nimport type { AuthorProfile } from '../../types/entity-author'\n\nexport interface AuthorDetailViewProps {\n author: AuthorProfile\n /** Host-injected avatar-URL mapper — wins over the ambient runtime. */\n proxyImageUrl?: (url: string) => string\n /** Authored-content rail (or anything else) rendered below the profile. */\n children?: React.ReactNode\n className?: string\n}\n\nexport function AuthorDetailView({\n author,\n proxyImageUrl,\n children,\n className,\n}: AuthorDetailViewProps) {\n // Optional runtime — null-safe outside a provider (bare embeds render the\n // raw avatar URL; the hub's app-wide HubRuntimeProvider supplies the proxy).\n const runtime = useChatRuntime()\n\n const proxiedAvatar = author.avatarUrl\n ? proxyImageUrl\n ? proxyImageUrl(author.avatarUrl)\n : (getProxiedImageUrl(author.avatarUrl, {\n proxyPrefix: runtime?.endpoints.imageProxyUrlPrefix,\n skipDomains: runtime?.endpoints.imageProxySkipDomains,\n directHttps: true,\n }) ?? author.avatarUrl)\n : null\n\n const subtitle = [author.jobTitle, author.company].filter(Boolean).join(' @ ')\n const bioText = formatBioText(author.about)\n\n return (\n <div className={cn('flex flex-col gap-5 md:gap-6', className)}>\n {/* Identity header: avatar left; name → subtitle → socials stacked right. */}\n <div className=\"flex gap-6 w-full items-start\">\n <div className=\"relative shrink-0 h-24 w-24\">\n <div className=\"rounded-full overflow-hidden bg-ods-bg-secondary border border-ods-border w-full h-full relative\">\n {proxiedAvatar ? (\n <Image\n src={proxiedAvatar}\n alt={author.fullName}\n fill\n className=\"object-cover\"\n unoptimized\n />\n ) : (\n <div className=\"flex items-center justify-center h-full w-full text-3xl text-ods-text-secondary font-['Azeret_Mono']\">\n {author.fullName.charAt(0).toUpperCase()}\n </div>\n )}\n </div>\n </div>\n\n <div className=\"min-w-0 flex flex-col justify-center gap-2\">\n <p className=\"text-h2 text-ods-text-primary leading-none truncate\">\n {author.fullName}\n </p>\n {subtitle && (\n <p className=\"font-body text-lg text-ods-text-secondary leading-none truncate\">\n {subtitle}\n </p>\n )}\n {author.socialLinks.length > 0 && (\n // Ghost compact row (transparent 32px) — metadata, not CTAs.\n <SocialIconRow\n compact\n links={author.socialLinks.map((link) => ({\n platform: link.platform,\n href: link.url,\n label: `${author.fullName}'s ${link.platform}`,\n }))}\n />\n )}\n </div>\n </div>\n\n {bioText && (\n <p className=\"font-body text-base leading-relaxed text-ods-text-secondary\">\n {bioText}\n </p>\n )}\n\n {author.knowsAbout.length > 0 && (\n <div className=\"flex flex-wrap gap-2\">\n {author.knowsAbout.map((topic) => (\n <StatusBadge key={topic} text={topic} variant=\"button\" colorScheme=\"cyan\" singleLine />\n ))}\n </div>\n )}\n\n {children}\n </div>\n )\n}\n"]}
|