@flamingo-stack/openframe-frontend-core 0.0.418 → 0.0.419

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/dist/{chunk-HH6AJLPX.cjs → chunk-2AN4EHN6.cjs} +7 -7
  2. package/dist/{chunk-HH6AJLPX.cjs.map → chunk-2AN4EHN6.cjs.map} +1 -1
  3. package/dist/{chunk-BDXXP46V.js → chunk-3AJOKFJ4.js} +27 -4
  4. package/dist/chunk-3AJOKFJ4.js.map +1 -0
  5. package/dist/{chunk-ZQTVLFS6.js → chunk-4OZLRBOH.js} +4 -4
  6. package/dist/{chunk-XDJXRHGL.cjs → chunk-5DTE27A4.cjs} +11 -11
  7. package/dist/{chunk-XDJXRHGL.cjs.map → chunk-5DTE27A4.cjs.map} +1 -1
  8. package/dist/{chunk-PPU67VJR.cjs → chunk-AMUTZJZX.cjs} +9 -9
  9. package/dist/{chunk-PPU67VJR.cjs.map → chunk-AMUTZJZX.cjs.map} +1 -1
  10. package/dist/{chunk-VXOZXOOR.cjs → chunk-BXXXBZYY.cjs} +28 -5
  11. package/dist/chunk-BXXXBZYY.cjs.map +1 -0
  12. package/dist/{chunk-TLRWCDX6.js → chunk-G4W5D7L4.js} +2 -2
  13. package/dist/{chunk-MGFW2QXW.js → chunk-GC4355MW.js} +2 -2
  14. package/dist/{chunk-WD3PDTQP.js → chunk-J7WFE7WL.js} +2 -2
  15. package/dist/{chunk-FRDNZT33.js → chunk-KEWTMPHB.js} +2 -2
  16. package/dist/{chunk-5NV3BBFC.cjs → chunk-KLMLQ35Y.cjs} +37 -37
  17. package/dist/{chunk-5NV3BBFC.cjs.map → chunk-KLMLQ35Y.cjs.map} +1 -1
  18. package/dist/{chunk-CVG5QEWB.cjs → chunk-KPLRIPEH.cjs} +15 -15
  19. package/dist/{chunk-CVG5QEWB.cjs.map → chunk-KPLRIPEH.cjs.map} +1 -1
  20. package/dist/{chunk-AZGPEBKL.cjs → chunk-NDYJ37LY.cjs} +31 -31
  21. package/dist/{chunk-AZGPEBKL.cjs.map → chunk-NDYJ37LY.cjs.map} +1 -1
  22. package/dist/{chunk-S6J3CLBD.cjs → chunk-NIS5VXZJ.cjs} +26 -26
  23. package/dist/{chunk-S6J3CLBD.cjs.map → chunk-NIS5VXZJ.cjs.map} +1 -1
  24. package/dist/{chunk-AU53RWCJ.js → chunk-OBMYKFBO.js} +2 -2
  25. package/dist/{chunk-T35MGBXH.cjs → chunk-ORDDVXDS.cjs} +59 -59
  26. package/dist/{chunk-T35MGBXH.cjs.map → chunk-ORDDVXDS.cjs.map} +1 -1
  27. package/dist/{chunk-BFV2R5OA.js → chunk-PAF4EFRR.js} +2 -2
  28. package/dist/{chunk-PUKSGYO5.cjs → chunk-PHJ6335O.cjs} +5 -5
  29. package/dist/{chunk-PUKSGYO5.cjs.map → chunk-PHJ6335O.cjs.map} +1 -1
  30. package/dist/{chunk-XLBCOT73.cjs → chunk-PUXTMLKC.cjs} +62 -62
  31. package/dist/{chunk-XLBCOT73.cjs.map → chunk-PUXTMLKC.cjs.map} +1 -1
  32. package/dist/{chunk-7QDHNBLD.js → chunk-T55A3BQD.js} +4 -4
  33. package/dist/{chunk-ZNQ2QRQF.js → chunk-U7EUN5FZ.js} +4 -4
  34. package/dist/{chunk-6GUJGVPI.cjs → chunk-WBLTGO7G.cjs} +26 -26
  35. package/dist/{chunk-6GUJGVPI.cjs.map → chunk-WBLTGO7G.cjs.map} +1 -1
  36. package/dist/{chunk-C6R2FRK6.js → chunk-YYF2AYM3.js} +4 -4
  37. package/dist/{chunk-EBMGBZCB.js → chunk-ZZFLE5LG.js} +2 -2
  38. package/dist/components/case-studies/index.cjs +8 -8
  39. package/dist/components/case-studies/index.js +2 -2
  40. package/dist/components/chat/index.cjs +2 -2
  41. package/dist/components/chat/index.js +1 -1
  42. package/dist/components/contact/index.cjs +3 -3
  43. package/dist/components/contact/index.js +2 -2
  44. package/dist/components/docs/index.cjs +5 -5
  45. package/dist/components/docs/index.js +4 -4
  46. package/dist/components/embeds/index.cjs +3 -3
  47. package/dist/components/embeds/index.js +2 -2
  48. package/dist/components/faq/index.cjs +3 -3
  49. package/dist/components/faq/index.js +2 -2
  50. package/dist/components/features/index.cjs +2 -2
  51. package/dist/components/features/index.js +1 -1
  52. package/dist/components/help-center-pages/index.cjs +20 -20
  53. package/dist/components/help-center-pages/index.js +11 -11
  54. package/dist/components/index.cjs +132 -128
  55. package/dist/components/index.cjs.map +1 -1
  56. package/dist/components/index.js +13 -9
  57. package/dist/components/index.js.map +1 -1
  58. package/dist/components/navigation/index.cjs +2 -2
  59. package/dist/components/navigation/index.js +1 -1
  60. package/dist/components/onboarding-guides/index.cjs +5 -5
  61. package/dist/components/onboarding-guides/index.js +4 -4
  62. package/dist/components/related-content/index.cjs +3 -3
  63. package/dist/components/related-content/index.js +2 -2
  64. package/dist/components/tickets/index.cjs +5 -5
  65. package/dist/components/tickets/index.js +4 -4
  66. package/dist/components/ui/index.cjs +6 -2
  67. package/dist/components/ui/index.cjs.map +1 -1
  68. package/dist/components/ui/index.js +5 -1
  69. package/dist/components/ui/status-badge.d.ts +21 -1
  70. package/dist/components/ui/status-badge.d.ts.map +1 -1
  71. package/dist/index.cjs +6 -2
  72. package/dist/index.cjs.map +1 -1
  73. package/dist/index.js +5 -1
  74. package/package.json +1 -1
  75. package/src/components/chat/entity-cards/roadmap-card.tsx +2 -2
  76. package/src/components/ui/status-badge.tsx +48 -1
  77. package/dist/chunk-BDXXP46V.js.map +0 -1
  78. package/dist/chunk-VXOZXOOR.cjs.map +0 -1
  79. /package/dist/{chunk-ZQTVLFS6.js.map → chunk-4OZLRBOH.js.map} +0 -0
  80. /package/dist/{chunk-TLRWCDX6.js.map → chunk-G4W5D7L4.js.map} +0 -0
  81. /package/dist/{chunk-MGFW2QXW.js.map → chunk-GC4355MW.js.map} +0 -0
  82. /package/dist/{chunk-WD3PDTQP.js.map → chunk-J7WFE7WL.js.map} +0 -0
  83. /package/dist/{chunk-FRDNZT33.js.map → chunk-KEWTMPHB.js.map} +0 -0
  84. /package/dist/{chunk-AU53RWCJ.js.map → chunk-OBMYKFBO.js.map} +0 -0
  85. /package/dist/{chunk-BFV2R5OA.js.map → chunk-PAF4EFRR.js.map} +0 -0
  86. /package/dist/{chunk-7QDHNBLD.js.map → chunk-T55A3BQD.js.map} +0 -0
  87. /package/dist/{chunk-ZNQ2QRQF.js.map → chunk-U7EUN5FZ.js.map} +0 -0
  88. /package/dist/{chunk-C6R2FRK6.js.map → chunk-YYF2AYM3.js.map} +0 -0
  89. /package/dist/{chunk-EBMGBZCB.js.map → chunk-ZZFLE5LG.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/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/open-source-features.tsx","../../src/components/pricing-display.tsx","../../src/components/results-count.tsx","../../src/components/openframe-logo.tsx","../../src/components/vendor-tag.tsx","../../src/components/selection-source-badge.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/authors/author-detail-view.tsx"],"names":["jsx","jsxs","VendorDirectoryIcon","OpenSourceIcon","CommunityHubIcon","VendorsIcon","CommunityIcon","CompareIcon","SunIcon","MoonIcon","CheckCircleIcon","GitHubIcon","PlusCircleIcon","OpenmspLogo","FlamingoLogo","DatePicker"],"mappings":"AAAA,6xBAAY;AACZ,YAAY;AACZ,iCAA8B;AAC9B;AACE;AACA;AACF,yDAA8B;AAC9B;AACE;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,yDAA8B;AAC9B;AACE;AACA;AACA;AACF,yDAA8B;AAC9B;AACE;AACA;AACA;AACF,yDAA8B;AAC9B;AACE;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;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACF,yDAA8B;AAC9B,iCAA8B;AAC9B;AACE;AACA;AACF,yDAA8B;AAC9B,iCAA8B;AAC9B,iCAA8B;AAC9B;AACE;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,iCAA8B;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;AACA;AACA;AACF,yDAA8B;AAC9B,iCAA8B;AAC9B;AACE;AACA;AACA;AACF,yDAA8B;AAC9B,iCAA8B;AAC9B,iCAA8B;AAC9B;AACA;AC1lCA,4CAAA,CAAA;AAFA,4EAAqD;AACrD,2CAAkB;AAiNN,+CAAA;AAzKL,SAAS,eAAA,CAAgB;AAAA,EAC9B,mBAAA;AAAA,EACA,YAAA,EAAc,KAAA;AAAA,EACd;AACF,EAAA,EAA0B,CAAC,CAAA,EAAG;AAM5B,EAAA,MAAM,SAAA,EAAW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AA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAmGrC,MAAA;AACP,EAAA;AAEJ;AhDqtI0K;AACA;AiDn2I1K;AA+Cc;AA9BmB;AAC/B,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACwB;AAGO,EAAA;AAKa,EAAA;AACJ,IAAA;AACA,IAAA;AACnB,IAAA;AAEjB,EAAA;AAEyE,EAAA;AACnC,EAAA;AAKtC,EAAA;AACE,oBAAA;AACiB,sBAAA;AAEV,QAAA;AAAA,QAAA;AACM,UAAA;AACO,UAAA;AACR,UAAA;AACM,UAAA;AACC,UAAA;AAAA,QAAA;AAIV,MAAA;AAOP,sBAAA;AAEA,wBAAA;AAIE,QAAA;AAE2B,QAAA;AAE3Bf,wBAAAA;AAAC,UAAA;AAAA,UAAA;AACQ,YAAA;AACkC,YAAA;AACxB,cAAA;AACJ,cAAA;AACiC,cAAA;AAC5C,YAAA;AAAA,UAAA;AACJ,QAAA;AAEJ,MAAA;AACF,IAAA;AAKE,IAAA;AAMK,IAAA;AAKN,IAAA;AACH,EAAA;AAEJ;AjDgzI0K;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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 { useEffect, useMemo, useRef, useState } from 'react';\nimport { X } from 'lucide-react';\nimport { Button } from './ui/button';\nimport { EntityIcon } from './icon-display';\nimport {\n dismissAnnouncement,\n isAnnouncementDismissed,\n clearLegacyAnnouncementCache,\n} from '../utils/announcement-storage';\nimport { ANNOUNCEMENT_CTA_DEFAULTS } from '../types/announcement';\nimport type { Announcement, AnnouncementBarProps, AnnouncementResponse } from '../types/announcement';\nimport { getAppType } from '../utils/app-config';\nimport { useEndpointsRuntime } from '../contexts/endpoints-runtime-context';\nimport { useSelfFetch } from '../hooks/use-self-fetch';\nimport { pickReadableTextColor } from '../utils/color-analysis';\n\n/**\n * Platform announcement bar — DUAL MODE, works with or without SSR.\n *\n * SSR mode (Next hosts, e.g. the hub): the server resolves the announcement\n * and the dismissal cookie, then passes `initialAnnouncement` (nullable).\n * The bar renders in the first HTML byte (zero layout shift, no flash for\n * dismissed users) and skips the mount fetch.\n *\n * Client-only mode (bare React apps without SSR: Vite/CRA embeds — see\n * react-embedding-example): omit `initialAnnouncement` and mount the bar\n * PROP-LESS inside an EndpointsRuntime provider; it self-fetches from the\n * provider's `announcementsUrl` (a fixed suffix under the embedder's one\n * content base). No provider → renders nothing, never fetches (silent\n * no-op). Appearance animates in (grid 0fr→1fr) so there is no hard layout\n * jump. Deliberately NO per-component URL or platform knobs: the embedder\n * is platform-agnostic — each server returns ITS OWN announcement via\n * currentPlatform(); the platform is never sent as a parameter.\n *\n * Both modes: freshness is event-driven, no polling — `useSelfFetch`'s\n * visibility revalidation re-fetches on tab refocus when the held data is\n * >60s old (matching the server cache TTL by convention); idle tabs make\n * zero requests. Dismissal cookie is the SSOT (`announcement-storage.ts`);\n * storage reads happen ONLY in effects (a render-time read would desync\n * hydration in SSR mode).\n */\nexport function AnnouncementBar({\n initialAnnouncement,\n previewMode = false,\n className,\n}: AnnouncementBarProps = {}) {\n // Namespace for the dismissal cookie/legacy keys. Next hosts inline\n // NEXT_PUBLIC_APP_TYPE (matching the server's currentPlatform(), which the\n // hub layout uses for the SSR cookie read); platform-agnostic embeds get\n // the fallback — fine, cookies are domain-scoped and an embed domain\n // serves one platform's announcements.\n const platform = getAppType();\n\n // Optional endpoint runtime: no provider → no URL → fetching disabled.\n const endpoints = useEndpointsRuntime();\n const url = previewMode ? null : endpoints?.announcementsUrl ?? null;\n\n // MUST be memoized (the hook re-syncs on [initialData] identity — an inline\n // literal per render would setData-loop) and strict-undefined-mapped:\n // `null` (hub: dismissed / no active announcement) still seeds the hook and\n // skips the mount fetch; only an ABSENT prop (embeds) enables self-fetch.\n const initialData = useMemo<AnnouncementResponse | undefined>(\n () => (initialAnnouncement === undefined ? undefined : { announcement: initialAnnouncement }),\n [initialAnnouncement],\n );\n\n const { data } = useSelfFetch<AnnouncementResponse>(url, {\n initialData,\n revalidateOnVisibleAfterMs: 60_000,\n });\n const announcement = data?.announcement ?? null;\n\n // Hold the last non-null announcement so DEACTIVATION (a revalidation that\n // returns null) collapses with the same 1fr->0fr animation as dismissal\n // instead of unmounting in place (a hard 44px jump).\n const lastAnnouncementRef = useRef<Announcement | null>(initialAnnouncement ?? null);\n useEffect(() => {\n if (announcement) lastAnnouncementRef.current = announcement;\n }, [announcement]);\n const displayAnnouncement = announcement ?? lastAnnouncementRef.current;\n\n // Expanded (height) state — initial value is a pure function of props so\n // the SSR HTML and the hydration render agree for every cohort.\n const [expandedState, setExpandedState] = useState<boolean>(() => initialAnnouncement != null);\n // Preview always mirrors the draft directly (effects are disabled there).\n const expanded = previewMode ? announcement != null : expandedState;\n\n // One-time cleanup of the pre-refactor localStorage announcement cache.\n useEffect(() => {\n if (previewMode) return;\n clearLegacyAnnouncementCache(platform);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n // Visibility reconciliation — runs on the seed and after EVERY completed\n // fetch (keyed on `data` identity, not announcement id, so a refocus\n // revalidation that returns the same announcement still re-checks the\n // dismissal store: this is what keeps a bar dismissed seconds earlier in\n // another tab from resurrecting). Also the legacy-migration point: an\n // LS-only dismissal (no cookie — server couldn't see it) collapses once,\n // animated, and backfills the cookie so the next SSR skips the bar.\n useEffect(() => {\n if (previewMode) return;\n if (!announcement) {\n setExpandedState(false);\n return;\n }\n if (isAnnouncementDismissed(platform, announcement.id)) {\n dismissAnnouncement(platform, announcement.id); // idempotent cookie backfill\n setExpandedState(false);\n } else {\n setExpandedState(true);\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [data]);\n\n const handleDismiss = () => {\n if (previewMode || !announcement) return;\n dismissAnnouncement(platform, announcement.id);\n // Release focus BEFORE the wrapper goes aria-hidden — a focused descendant\n // inside an aria-hidden subtree strands screen-reader focus (and Chrome\n // refuses to apply the attribute).\n (document.activeElement as HTMLElement | null)?.blur?.();\n setExpandedState(false);\n };\n\n const handleCtaClick = () => {\n if (previewMode || !announcement?.cta_url) return;\n // Scheme guard: cta_url is admin-entered data — only http(s) (absolute or\n // relative) may navigate. A `javascript:` value would otherwise execute\n // via location.href / window.open (stored XSS).\n let safeUrl: string;\n try {\n // Resolve against the CURRENT page (not the origin) so path-relative and\n // fragment CTAs keep their pre-guard semantics.\n const parsed = new URL(announcement.cta_url, window.location.href);\n if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return;\n safeUrl = parsed.href;\n } catch {\n return;\n }\n announcement.cta_target === '_blank'\n ? window.open(safeUrl, '_blank', 'noopener,noreferrer')\n : (window.location.href = safeUrl);\n };\n\n // Never had anything to show: render nothing. Dismissal AND deactivation\n // keep the element mounted (displayAnnouncement holds the last content) at\n // 0fr so the collapse animates.\n if (!displayAnnouncement) return null;\n\n // Contrast is solved with the ODS THEME system, not color literals: the\n // admin background's tone selects which ODS theme scopes the bar's content\n // (light-toned background needs dark-on-light = the light theme's values;\n // dark-toned needs the dark theme's). `.theme-light` / `.theme-dark` are\n // the design system's own scoping classes (ods-colors.css) and flip every\n // Tier-1 `--ods-*` primitive for descendants.\n const themeScope = pickReadableTextColor(displayAnnouncement.background_color) === 'dark' ? 'theme-light' : 'theme-dark';\n\n // Inside the scope, colors reference Tier-1 theme primitives directly\n // (Tier-2 `--color-*` aliases resolve once at :root and do NOT re-resolve\n // in a nested theme scope — verified). `--ods-system-greys-white` is the\n // theme's primary-text primitive; `--ods-system-greys-black-hover/-action`\n // are its quiet hover/active surfaces. Every value comes from the active\n // ODS theme; nothing is hardcoded.\n const barButtonClasses =\n 'text-[color:var(--ods-system-greys-white)] hover:bg-[var(--ods-system-greys-black-hover)] active:bg-[var(--ods-system-greys-black-action)]';\n\n const hasCta = Boolean(displayAnnouncement.cta_enabled && displayAnnouncement.cta_url && displayAnnouncement.cta_text);\n\n return (\n <div\n role=\"region\"\n aria-label=\"Announcement\"\n aria-hidden={!expanded}\n // The marker means \"I am the PAGE-CHROME bar\" — `useHeaderHeight` sums\n // the first match with the header to offset fixed panels (e.g. the\n // sliding admin sidebar). Admin PREVIEW instances must NOT carry it, or\n // a preview card inflates that offset (phantom gap above the drawer).\n {...(previewMode ? {} : { 'data-announcement-bar': true })}\n className={`relative w-full grid transition-[grid-template-rows] duration-200 ease-out motion-reduce:transition-none ${previewMode ? '' : 'z-50'} ${className ?? ''}`}\n style={{ gridTemplateRows: expanded ? '1fr' : '0fr' }}\n >\n {/*\n Bar anatomy follows the announcement-bar industry standard: ONE line of\n text at 13-14px inside a 44px-tall strip (guides converge on 40-60px\n with a single sentence; two stacked 18px rows blew past that), title +\n description merged inline (description hidden on small screens), ONE\n compact CTA on the right, and a ghost-icon dismiss (design-system\n icon-sm: 32px target, >= the 24px WCAG 2.2 SC 2.5.8 AA floor).\n */}\n <div className={`min-h-0 overflow-hidden ${themeScope}`} style={{ backgroundColor: displayAnnouncement.background_color }}>\n <div className=\"flex items-center w-full max-w-full min-h-11 text-[color:var(--ods-system-greys-white)]\">\n {/* Mobile: whole-bar tap target (touch-first by design; the CTA\n Button below is the keyboard/AT path and is CSS-hidden < md —\n known tradeoff carried from the original bar). */}\n <div\n className={`flex flex-row gap-2 md:gap-3 items-center pl-4 md:pl-6 py-1.5 flex-1 min-w-0 ${\n hasCta ? '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 && hasCta) {\n e.preventDefault();\n handleCtaClick();\n }\n }}\n >\n {/* ONE unified icon path (shared with the chat): uploaded image URL\n wins, else a library glyph by name (+ props), via <EntityIcon>. */}\n <EntityIcon\n icon={{\n name: displayAnnouncement.icon_name || 'openframe-logo',\n url: displayAnnouncement.icon_url,\n props: displayAnnouncement.icon_props,\n }}\n size={24}\n className=\"relative shrink-0 w-5 h-5 md:w-6 md:h-6\"\n />\n\n {/* Single-line message: bold title + regular description inline,\n truncating as one unit. Separator is a middot (house rule: no\n en/em dashes in copy). */}\n <p className=\"font-body flex-1 min-w-0 max-w-full text-[13px] md:text-sm leading-snug truncate mb-0\">\n <span className=\"font-semibold\">{displayAnnouncement.title}</span>\n {displayAnnouncement.description && (\n <span className=\"hidden sm:inline opacity-80\"> · {displayAnnouncement.description}</span>\n )}\n </p>\n\n {/* CTA - the common Button carrying the ADMIN-CONFIGURED colors\n (cta_button_background_color / cta_button_text_color are an\n admin FEATURE, defaults from ANNOUNCEMENT_CTA_DEFAULTS —\n announcement colors are data, not token surfaces). Inline\n styles win over the variant's hover classes on every state,\n so hover feedback is opacity (the bar's original treatment);\n nothing can render dark-on-dark. Hidden on mobile, where the\n whole bar is the tap target.\n\n Geometry + type come from the design system's size=\"compact\"\n (24px caption-scale pill for slim strips — rationale documented\n on the variant in button.tsx). */}\n {hasCta && displayAnnouncement.cta_text && (\n <div className=\"hidden md:flex flex-shrink-0 ml-2 md:ml-4\">\n <Button\n onClick={handleCtaClick}\n variant=\"outline\"\n size=\"compact\"\n className=\"transition-opacity hover:opacity-90\"\n style={{\n backgroundColor: displayAnnouncement.cta_button_background_color || ANNOUNCEMENT_CTA_DEFAULTS.background,\n color: displayAnnouncement.cta_button_text_color || ANNOUNCEMENT_CTA_DEFAULTS.text,\n borderColor: displayAnnouncement.cta_button_background_color || ANNOUNCEMENT_CTA_DEFAULTS.background,\n }}\n tabIndex={expanded ? 0 : -1}\n leftIcon={\n displayAnnouncement.cta_show_icon && displayAnnouncement.cta_icon_name\n ? (\n <EntityIcon\n icon={{ name: displayAnnouncement.cta_icon_name, props: displayAnnouncement.cta_icon_props }}\n size={14}\n className=\"w-3.5 h-3.5\"\n />\n )\n : undefined\n }\n >\n {displayAnnouncement.cta_text}\n </Button>\n </div>\n )}\n </div>\n\n {/* Dismiss - the common Button in its ghost-icon treatment\n (size=\"icon-sm\": 32px target, >= the 24px WCAG 2.5.8 AA floor,\n 16px glyph) with the bar's quiet tint hover. Inert in\n previewMode. */}\n <div className=\"flex-shrink-0 ml-1 md:ml-2 mr-2 md:mr-4\">\n <Button\n onClick={(e) => {\n e.stopPropagation(); // Prevent triggering the mobile CTA click\n handleDismiss();\n }}\n variant=\"transparent\"\n size=\"icon-sm\"\n className={barButtonClasses}\n aria-label=\"Dismiss announcement\"\n type=\"button\"\n tabIndex={expanded ? 0 : -1}\n >\n <X strokeWidth={2} />\n </Button>\n </div>\n </div>\n </div>\n </div>\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-[44] 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 { Mail } from 'lucide-react';\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 /** `internal` links are only ever shown on surfaces that opt in (and must be\n * gated server-side); with `groupByVisibility` they render after a divider. */\n visibility?: \"external\" | \"internal\";\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 /** Render the `external` links and the `internal` links as two groups split\n * by a thin divider (only when BOTH groups are non-empty). Visibility is\n * presentation-only here — the caller must still gate which links it passes\n * server-side. Default false keeps a single flat row. */\n groupByVisibility?: 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 'email':\n case 'mail':\n return <Mail 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, groupByVisibility = 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\n const renderButton = (link: SocialLink, index: number) => {\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\n const rowClass = `flex flex-row items-center ${compact ? 'gap-2 w-fit' : 'gap-3 w-full'} ${className}`;\n\n // Two-group mode: external links, a thin divider, then internal links — but only\n // when both groups are present (otherwise it's just a flat row, no stray divider).\n if (groupByVisibility) {\n const external = links.filter((l) => l.visibility !== 'internal');\n const internal = links.filter((l) => l.visibility === 'internal');\n if (external.length > 0 && internal.length > 0) {\n return (\n <div className={rowClass}>\n {external.map((l, i) => renderButton(l, i))}\n <span aria-hidden className=\"self-stretch w-px my-1 bg-ods-border shrink-0\" />\n {internal.map((l, i) => renderButton(l, external.length + i))}\n </div>\n );\n }\n }\n\n return <div className={rowClass}>{links.map((link, index) => renderButton(link, index))}</div>;\n}","\"use client\"\n\nimport { useSearchParams, useRouter } from \"../embed-shims/next-navigation\"\nimport { useTransition } from \"react\"\nimport { scrollElementIntoView } from \"../utils/scroll-into-view\"\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 // Smooth-restore via the shared anchoring-proof tween: the filtered\n // list re-renders mid-scroll, which cancels a NATIVE smooth scrollTo\n // (browser scroll anchoring) — the tween re-asserts every frame.\n scrollElementIntoView(document.body, { adjustTargetY: () => currentScrollY })\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'\n// Explicit re-export disambiguates the star-export collision with the\n// features barrel's gallery-manager MediaItem: from THIS barrel, `MediaItem`\n// is the carousel item shape (the lib-wide SSOT the hub re-types against);\n// the gallery-manager shape stays reachable via `./components/features`.\nexport type { MediaItem } from '../utils/media-carousel-utils-stub'\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 './agent-mark'\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\nexport { EntityIcon, type EntityIconValue } from './icon-display'\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 { 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 { 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} ","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};","\"use client\"\n\nimport { OpenSourceIcon, CoinsIcon } from \"./icons-stub\"\nimport { OpenFrameLogo } from \"./openframe-logo\"\nimport { cn } from \"../utils/cn\"\nimport { Boxes, Database, Hand, Plug, ShieldCheck, Sparkles } from \"lucide-react\"\n\nexport interface VendorTagProps {\n type: 'open-source' | 'commercial' | 'free' | 'freemium' | 'paid' | 'enterprise' | 'recommended' | 'classification' | 'ai' | 'manual' | 'openframe_selected' | 'placeholder' | 'api' | 'data' | 'k8s' | 'secured'\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 // platform-capability tags — same neutral icon-box chrome as 'commercial'\n case 'api':\n return {\n text: text || \"API\",\n icon: (\n <div className=\"w-4 h-4 bg-ods-border rounded-sm flex items-center justify-center flex-shrink-0\">\n <Plug width={10} height={10} className=\"text-ods-text-secondary\" />\n </div>\n )\n }\n case 'data':\n return {\n text: text || \"Data\",\n icon: (\n <div className=\"w-4 h-4 bg-ods-border rounded-sm flex items-center justify-center flex-shrink-0\">\n <Database width={10} height={10} className=\"text-ods-text-secondary\" />\n </div>\n )\n }\n case 'k8s':\n return {\n text: text || \"K8s\",\n icon: (\n <div className=\"w-4 h-4 bg-ods-border rounded-sm flex items-center justify-center flex-shrink-0\">\n <Boxes width={10} height={10} className=\"text-ods-text-secondary\" />\n </div>\n )\n }\n case 'secured':\n return {\n text: text || \"Secured\",\n icon: (\n <div className=\"w-4 h-4 bg-ods-border rounded-sm flex items-center justify-center flex-shrink-0\">\n <ShieldCheck 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} ","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 * 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/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/open-source-features.tsx","../../src/components/pricing-display.tsx","../../src/components/results-count.tsx","../../src/components/openframe-logo.tsx","../../src/components/vendor-tag.tsx","../../src/components/selection-source-badge.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/authors/author-detail-view.tsx"],"names":["jsx","jsxs","VendorDirectoryIcon","OpenSourceIcon","CommunityHubIcon","VendorsIcon","CommunityIcon","CompareIcon","SunIcon","MoonIcon","CheckCircleIcon","GitHubIcon","PlusCircleIcon","OpenmspLogo","FlamingoLogo","DatePicker"],"mappings":"AAAA,6xBAAY;AACZ,YAAY;AACZ,iCAA8B;AAC9B;AACE;AACA;AACF,yDAA8B;AAC9B;AACE;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,yDAA8B;AAC9B;AACE;AACA;AACA;AACF,yDAA8B;AAC9B;AACE;AACA;AACA;AACF,yDAA8B;AAC9B;AACE;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;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACF,yDAA8B;AAC9B,iCAA8B;AAC9B;AACE;AACA;AACF,yDAA8B;AAC9B,iCAA8B;AAC9B,iCAA8B;AAC9B;AACE;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,iCAA8B;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;AACA;AACA;AACF,yDAA8B;AAC9B,iCAA8B;AAC9B;AACE;AACA;AACA;AACF,yDAA8B;AAC9B,iCAA8B;AAC9B,iCAA8B;AAC9B;AACA;AC5lCA,4CAAA,CAAA;AAFA,4EAAqD;AACrD,2CAAkB;AAiNN,+CAAA;AAzKL,SAAS,eAAA,CAAgB;AAAA,EAC9B,mBAAA;AAAA,EACA,YAAA,EAAc,KAAA;AAAA,EACd;AACF,EAAA,EAA0B,CAAC,CAAA,EAAG;AAM5B,EAAA,MAAM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AA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAmGrC,MAAA;AACP,EAAA;AAEJ;AhDutI0K;AACA;AiDr2I1K;AA+Cc;AA9BmB;AAC/B,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACwB;AAGO,EAAA;AAKa,EAAA;AACJ,IAAA;AACA,IAAA;AACnB,IAAA;AAEjB,EAAA;AAEyE,EAAA;AACnC,EAAA;AAKtC,EAAA;AACE,oBAAA;AACiB,sBAAA;AAEV,QAAA;AAAA,QAAA;AACM,UAAA;AACO,UAAA;AACR,UAAA;AACM,UAAA;AACC,UAAA;AAAA,QAAA;AAIV,MAAA;AAOP,sBAAA;AAEA,wBAAA;AAIE,QAAA;AAE2B,QAAA;AAE3Bf,wBAAAA;AAAC,UAAA;AAAA,UAAA;AACQ,YAAA;AACkC,YAAA;AACxB,cAAA;AACJ,cAAA;AACiC,cAAA;AAC5C,YAAA;AAAA,UAAA;AACJ,QAAA;AAEJ,MAAA;AACF,IAAA;AAKE,IAAA;AAMK,IAAA;AAKN,IAAA;AACH,EAAA;AAEJ;AjDkzI0K;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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 { useEffect, useMemo, useRef, useState } from 'react';\nimport { X } from 'lucide-react';\nimport { Button } from './ui/button';\nimport { EntityIcon } from './icon-display';\nimport {\n dismissAnnouncement,\n isAnnouncementDismissed,\n clearLegacyAnnouncementCache,\n} from '../utils/announcement-storage';\nimport { ANNOUNCEMENT_CTA_DEFAULTS } from '../types/announcement';\nimport type { Announcement, AnnouncementBarProps, AnnouncementResponse } from '../types/announcement';\nimport { getAppType } from '../utils/app-config';\nimport { useEndpointsRuntime } from '../contexts/endpoints-runtime-context';\nimport { useSelfFetch } from '../hooks/use-self-fetch';\nimport { pickReadableTextColor } from '../utils/color-analysis';\n\n/**\n * Platform announcement bar — DUAL MODE, works with or without SSR.\n *\n * SSR mode (Next hosts, e.g. the hub): the server resolves the announcement\n * and the dismissal cookie, then passes `initialAnnouncement` (nullable).\n * The bar renders in the first HTML byte (zero layout shift, no flash for\n * dismissed users) and skips the mount fetch.\n *\n * Client-only mode (bare React apps without SSR: Vite/CRA embeds — see\n * react-embedding-example): omit `initialAnnouncement` and mount the bar\n * PROP-LESS inside an EndpointsRuntime provider; it self-fetches from the\n * provider's `announcementsUrl` (a fixed suffix under the embedder's one\n * content base). No provider → renders nothing, never fetches (silent\n * no-op). Appearance animates in (grid 0fr→1fr) so there is no hard layout\n * jump. Deliberately NO per-component URL or platform knobs: the embedder\n * is platform-agnostic — each server returns ITS OWN announcement via\n * currentPlatform(); the platform is never sent as a parameter.\n *\n * Both modes: freshness is event-driven, no polling — `useSelfFetch`'s\n * visibility revalidation re-fetches on tab refocus when the held data is\n * >60s old (matching the server cache TTL by convention); idle tabs make\n * zero requests. Dismissal cookie is the SSOT (`announcement-storage.ts`);\n * storage reads happen ONLY in effects (a render-time read would desync\n * hydration in SSR mode).\n */\nexport function AnnouncementBar({\n initialAnnouncement,\n previewMode = false,\n className,\n}: AnnouncementBarProps = {}) {\n // Namespace for the dismissal cookie/legacy keys. Next hosts inline\n // NEXT_PUBLIC_APP_TYPE (matching the server's currentPlatform(), which the\n // hub layout uses for the SSR cookie read); platform-agnostic embeds get\n // the fallback — fine, cookies are domain-scoped and an embed domain\n // serves one platform's announcements.\n const platform = getAppType();\n\n // Optional endpoint runtime: no provider → no URL → fetching disabled.\n const endpoints = useEndpointsRuntime();\n const url = previewMode ? null : endpoints?.announcementsUrl ?? null;\n\n // MUST be memoized (the hook re-syncs on [initialData] identity — an inline\n // literal per render would setData-loop) and strict-undefined-mapped:\n // `null` (hub: dismissed / no active announcement) still seeds the hook and\n // skips the mount fetch; only an ABSENT prop (embeds) enables self-fetch.\n const initialData = useMemo<AnnouncementResponse | undefined>(\n () => (initialAnnouncement === undefined ? undefined : { announcement: initialAnnouncement }),\n [initialAnnouncement],\n );\n\n const { data } = useSelfFetch<AnnouncementResponse>(url, {\n initialData,\n revalidateOnVisibleAfterMs: 60_000,\n });\n const announcement = data?.announcement ?? null;\n\n // Hold the last non-null announcement so DEACTIVATION (a revalidation that\n // returns null) collapses with the same 1fr->0fr animation as dismissal\n // instead of unmounting in place (a hard 44px jump).\n const lastAnnouncementRef = useRef<Announcement | null>(initialAnnouncement ?? null);\n useEffect(() => {\n if (announcement) lastAnnouncementRef.current = announcement;\n }, [announcement]);\n const displayAnnouncement = announcement ?? lastAnnouncementRef.current;\n\n // Expanded (height) state — initial value is a pure function of props so\n // the SSR HTML and the hydration render agree for every cohort.\n const [expandedState, setExpandedState] = useState<boolean>(() => initialAnnouncement != null);\n // Preview always mirrors the draft directly (effects are disabled there).\n const expanded = previewMode ? announcement != null : expandedState;\n\n // One-time cleanup of the pre-refactor localStorage announcement cache.\n useEffect(() => {\n if (previewMode) return;\n clearLegacyAnnouncementCache(platform);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n // Visibility reconciliation — runs on the seed and after EVERY completed\n // fetch (keyed on `data` identity, not announcement id, so a refocus\n // revalidation that returns the same announcement still re-checks the\n // dismissal store: this is what keeps a bar dismissed seconds earlier in\n // another tab from resurrecting). Also the legacy-migration point: an\n // LS-only dismissal (no cookie — server couldn't see it) collapses once,\n // animated, and backfills the cookie so the next SSR skips the bar.\n useEffect(() => {\n if (previewMode) return;\n if (!announcement) {\n setExpandedState(false);\n return;\n }\n if (isAnnouncementDismissed(platform, announcement.id)) {\n dismissAnnouncement(platform, announcement.id); // idempotent cookie backfill\n setExpandedState(false);\n } else {\n setExpandedState(true);\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [data]);\n\n const handleDismiss = () => {\n if (previewMode || !announcement) return;\n dismissAnnouncement(platform, announcement.id);\n // Release focus BEFORE the wrapper goes aria-hidden — a focused descendant\n // inside an aria-hidden subtree strands screen-reader focus (and Chrome\n // refuses to apply the attribute).\n (document.activeElement as HTMLElement | null)?.blur?.();\n setExpandedState(false);\n };\n\n const handleCtaClick = () => {\n if (previewMode || !announcement?.cta_url) return;\n // Scheme guard: cta_url is admin-entered data — only http(s) (absolute or\n // relative) may navigate. A `javascript:` value would otherwise execute\n // via location.href / window.open (stored XSS).\n let safeUrl: string;\n try {\n // Resolve against the CURRENT page (not the origin) so path-relative and\n // fragment CTAs keep their pre-guard semantics.\n const parsed = new URL(announcement.cta_url, window.location.href);\n if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return;\n safeUrl = parsed.href;\n } catch {\n return;\n }\n announcement.cta_target === '_blank'\n ? window.open(safeUrl, '_blank', 'noopener,noreferrer')\n : (window.location.href = safeUrl);\n };\n\n // Never had anything to show: render nothing. Dismissal AND deactivation\n // keep the element mounted (displayAnnouncement holds the last content) at\n // 0fr so the collapse animates.\n if (!displayAnnouncement) return null;\n\n // Contrast is solved with the ODS THEME system, not color literals: the\n // admin background's tone selects which ODS theme scopes the bar's content\n // (light-toned background needs dark-on-light = the light theme's values;\n // dark-toned needs the dark theme's). `.theme-light` / `.theme-dark` are\n // the design system's own scoping classes (ods-colors.css) and flip every\n // Tier-1 `--ods-*` primitive for descendants.\n const themeScope = pickReadableTextColor(displayAnnouncement.background_color) === 'dark' ? 'theme-light' : 'theme-dark';\n\n // Inside the scope, colors reference Tier-1 theme primitives directly\n // (Tier-2 `--color-*` aliases resolve once at :root and do NOT re-resolve\n // in a nested theme scope — verified). `--ods-system-greys-white` is the\n // theme's primary-text primitive; `--ods-system-greys-black-hover/-action`\n // are its quiet hover/active surfaces. Every value comes from the active\n // ODS theme; nothing is hardcoded.\n const barButtonClasses =\n 'text-[color:var(--ods-system-greys-white)] hover:bg-[var(--ods-system-greys-black-hover)] active:bg-[var(--ods-system-greys-black-action)]';\n\n const hasCta = Boolean(displayAnnouncement.cta_enabled && displayAnnouncement.cta_url && displayAnnouncement.cta_text);\n\n return (\n <div\n role=\"region\"\n aria-label=\"Announcement\"\n aria-hidden={!expanded}\n // The marker means \"I am the PAGE-CHROME bar\" — `useHeaderHeight` sums\n // the first match with the header to offset fixed panels (e.g. the\n // sliding admin sidebar). Admin PREVIEW instances must NOT carry it, or\n // a preview card inflates that offset (phantom gap above the drawer).\n {...(previewMode ? {} : { 'data-announcement-bar': true })}\n className={`relative w-full grid transition-[grid-template-rows] duration-200 ease-out motion-reduce:transition-none ${previewMode ? '' : 'z-50'} ${className ?? ''}`}\n style={{ gridTemplateRows: expanded ? '1fr' : '0fr' }}\n >\n {/*\n Bar anatomy follows the announcement-bar industry standard: ONE line of\n text at 13-14px inside a 44px-tall strip (guides converge on 40-60px\n with a single sentence; two stacked 18px rows blew past that), title +\n description merged inline (description hidden on small screens), ONE\n compact CTA on the right, and a ghost-icon dismiss (design-system\n icon-sm: 32px target, >= the 24px WCAG 2.2 SC 2.5.8 AA floor).\n */}\n <div className={`min-h-0 overflow-hidden ${themeScope}`} style={{ backgroundColor: displayAnnouncement.background_color }}>\n <div className=\"flex items-center w-full max-w-full min-h-11 text-[color:var(--ods-system-greys-white)]\">\n {/* Mobile: whole-bar tap target (touch-first by design; the CTA\n Button below is the keyboard/AT path and is CSS-hidden < md —\n known tradeoff carried from the original bar). */}\n <div\n className={`flex flex-row gap-2 md:gap-3 items-center pl-4 md:pl-6 py-1.5 flex-1 min-w-0 ${\n hasCta ? '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 && hasCta) {\n e.preventDefault();\n handleCtaClick();\n }\n }}\n >\n {/* ONE unified icon path (shared with the chat): uploaded image URL\n wins, else a library glyph by name (+ props), via <EntityIcon>. */}\n <EntityIcon\n icon={{\n name: displayAnnouncement.icon_name || 'openframe-logo',\n url: displayAnnouncement.icon_url,\n props: displayAnnouncement.icon_props,\n }}\n size={24}\n className=\"relative shrink-0 w-5 h-5 md:w-6 md:h-6\"\n />\n\n {/* Single-line message: bold title + regular description inline,\n truncating as one unit. Separator is a middot (house rule: no\n en/em dashes in copy). */}\n <p className=\"font-body flex-1 min-w-0 max-w-full text-[13px] md:text-sm leading-snug truncate mb-0\">\n <span className=\"font-semibold\">{displayAnnouncement.title}</span>\n {displayAnnouncement.description && (\n <span className=\"hidden sm:inline opacity-80\"> · {displayAnnouncement.description}</span>\n )}\n </p>\n\n {/* CTA - the common Button carrying the ADMIN-CONFIGURED colors\n (cta_button_background_color / cta_button_text_color are an\n admin FEATURE, defaults from ANNOUNCEMENT_CTA_DEFAULTS —\n announcement colors are data, not token surfaces). Inline\n styles win over the variant's hover classes on every state,\n so hover feedback is opacity (the bar's original treatment);\n nothing can render dark-on-dark. Hidden on mobile, where the\n whole bar is the tap target.\n\n Geometry + type come from the design system's size=\"compact\"\n (24px caption-scale pill for slim strips — rationale documented\n on the variant in button.tsx). */}\n {hasCta && displayAnnouncement.cta_text && (\n <div className=\"hidden md:flex flex-shrink-0 ml-2 md:ml-4\">\n <Button\n onClick={handleCtaClick}\n variant=\"outline\"\n size=\"compact\"\n className=\"transition-opacity hover:opacity-90\"\n style={{\n backgroundColor: displayAnnouncement.cta_button_background_color || ANNOUNCEMENT_CTA_DEFAULTS.background,\n color: displayAnnouncement.cta_button_text_color || ANNOUNCEMENT_CTA_DEFAULTS.text,\n borderColor: displayAnnouncement.cta_button_background_color || ANNOUNCEMENT_CTA_DEFAULTS.background,\n }}\n tabIndex={expanded ? 0 : -1}\n leftIcon={\n displayAnnouncement.cta_show_icon && displayAnnouncement.cta_icon_name\n ? (\n <EntityIcon\n icon={{ name: displayAnnouncement.cta_icon_name, props: displayAnnouncement.cta_icon_props }}\n size={14}\n className=\"w-3.5 h-3.5\"\n />\n )\n : undefined\n }\n >\n {displayAnnouncement.cta_text}\n </Button>\n </div>\n )}\n </div>\n\n {/* Dismiss - the common Button in its ghost-icon treatment\n (size=\"icon-sm\": 32px target, >= the 24px WCAG 2.5.8 AA floor,\n 16px glyph) with the bar's quiet tint hover. Inert in\n previewMode. */}\n <div className=\"flex-shrink-0 ml-1 md:ml-2 mr-2 md:mr-4\">\n <Button\n onClick={(e) => {\n e.stopPropagation(); // Prevent triggering the mobile CTA click\n handleDismiss();\n }}\n variant=\"transparent\"\n size=\"icon-sm\"\n className={barButtonClasses}\n aria-label=\"Dismiss announcement\"\n type=\"button\"\n tabIndex={expanded ? 0 : -1}\n >\n <X strokeWidth={2} />\n </Button>\n </div>\n </div>\n </div>\n </div>\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-[44] 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 { Mail } from 'lucide-react';\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 /** `internal` links are only ever shown on surfaces that opt in (and must be\n * gated server-side); with `groupByVisibility` they render after a divider. */\n visibility?: \"external\" | \"internal\";\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 /** Render the `external` links and the `internal` links as two groups split\n * by a thin divider (only when BOTH groups are non-empty). Visibility is\n * presentation-only here — the caller must still gate which links it passes\n * server-side. Default false keeps a single flat row. */\n groupByVisibility?: 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 'email':\n case 'mail':\n return <Mail 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, groupByVisibility = 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\n const renderButton = (link: SocialLink, index: number) => {\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\n const rowClass = `flex flex-row items-center ${compact ? 'gap-2 w-fit' : 'gap-3 w-full'} ${className}`;\n\n // Two-group mode: external links, a thin divider, then internal links — but only\n // when both groups are present (otherwise it's just a flat row, no stray divider).\n if (groupByVisibility) {\n const external = links.filter((l) => l.visibility !== 'internal');\n const internal = links.filter((l) => l.visibility === 'internal');\n if (external.length > 0 && internal.length > 0) {\n return (\n <div className={rowClass}>\n {external.map((l, i) => renderButton(l, i))}\n <span aria-hidden className=\"self-stretch w-px my-1 bg-ods-border shrink-0\" />\n {internal.map((l, i) => renderButton(l, external.length + i))}\n </div>\n );\n }\n }\n\n return <div className={rowClass}>{links.map((link, index) => renderButton(link, index))}</div>;\n}","\"use client\"\n\nimport { useSearchParams, useRouter } from \"../embed-shims/next-navigation\"\nimport { useTransition } from \"react\"\nimport { scrollElementIntoView } from \"../utils/scroll-into-view\"\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 // Smooth-restore via the shared anchoring-proof tween: the filtered\n // list re-renders mid-scroll, which cancels a NATIVE smooth scrollTo\n // (browser scroll anchoring) — the tween re-asserts every frame.\n scrollElementIntoView(document.body, { adjustTargetY: () => currentScrollY })\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'\n// Explicit re-export disambiguates the star-export collision with the\n// features barrel's gallery-manager MediaItem: from THIS barrel, `MediaItem`\n// is the carousel item shape (the lib-wide SSOT the hub re-types against);\n// the gallery-manager shape stays reachable via `./components/features`.\nexport type { MediaItem } from '../utils/media-carousel-utils-stub'\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 './agent-mark'\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\nexport { EntityIcon, type EntityIconValue } from './icon-display'\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 { 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 { 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} ","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};","\"use client\"\n\nimport { OpenSourceIcon, CoinsIcon } from \"./icons-stub\"\nimport { OpenFrameLogo } from \"./openframe-logo\"\nimport { cn } from \"../utils/cn\"\nimport { Boxes, Database, Hand, Plug, ShieldCheck, Sparkles } from \"lucide-react\"\n\nexport interface VendorTagProps {\n type: 'open-source' | 'commercial' | 'free' | 'freemium' | 'paid' | 'enterprise' | 'recommended' | 'classification' | 'ai' | 'manual' | 'openframe_selected' | 'placeholder' | 'api' | 'data' | 'k8s' | 'secured'\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 // platform-capability tags — same neutral icon-box chrome as 'commercial'\n case 'api':\n return {\n text: text || \"API\",\n icon: (\n <div className=\"w-4 h-4 bg-ods-border rounded-sm flex items-center justify-center flex-shrink-0\">\n <Plug width={10} height={10} className=\"text-ods-text-secondary\" />\n </div>\n )\n }\n case 'data':\n return {\n text: text || \"Data\",\n icon: (\n <div className=\"w-4 h-4 bg-ods-border rounded-sm flex items-center justify-center flex-shrink-0\">\n <Database width={10} height={10} className=\"text-ods-text-secondary\" />\n </div>\n )\n }\n case 'k8s':\n return {\n text: text || \"K8s\",\n icon: (\n <div className=\"w-4 h-4 bg-ods-border rounded-sm flex items-center justify-center flex-shrink-0\">\n <Boxes width={10} height={10} className=\"text-ods-text-secondary\" />\n </div>\n )\n }\n case 'secured':\n return {\n text: text || \"Secured\",\n icon: (\n <div className=\"w-4 h-4 bg-ods-border rounded-sm flex items-center justify-center flex-shrink-0\">\n <ShieldCheck 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} ","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 * 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"]}