@sonordev/agency-site-kit 0.3.1 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (374) hide show
  1. package/README.md +302 -0
  2. package/bin/setup.mjs +79 -0
  3. package/dist/{chunk-RXBK2EOE.js → chunk-4IES23GV.js} +3 -3
  4. package/dist/{chunk-RXBK2EOE.js.map → chunk-4IES23GV.js.map} +1 -1
  5. package/dist/{chunk-WNR4KNG4.cjs → chunk-5MEZU3OG.cjs} +4 -4
  6. package/dist/{chunk-WNR4KNG4.cjs.map → chunk-5MEZU3OG.cjs.map} +1 -1
  7. package/dist/chunk-7D4H36UF.js +13 -0
  8. package/dist/chunk-7D4H36UF.js.map +1 -0
  9. package/dist/chunk-7ZHRMWJU.cjs +306 -0
  10. package/dist/chunk-7ZHRMWJU.cjs.map +1 -0
  11. package/dist/chunk-CVT7I5BQ.js +71 -0
  12. package/dist/chunk-CVT7I5BQ.js.map +1 -0
  13. package/dist/chunk-HQGAX2LI.cjs +8 -0
  14. package/dist/chunk-HQGAX2LI.cjs.map +1 -0
  15. package/dist/chunk-K2BGTDSN.js +45 -0
  16. package/dist/chunk-K2BGTDSN.js.map +1 -0
  17. package/dist/chunk-KXLROAO5.js +291 -0
  18. package/dist/chunk-KXLROAO5.js.map +1 -0
  19. package/dist/chunk-L4EOQVRJ.js +6 -0
  20. package/dist/chunk-L4EOQVRJ.js.map +1 -0
  21. package/dist/chunk-NI4FVCLS.cjs +32 -0
  22. package/dist/chunk-NI4FVCLS.cjs.map +1 -0
  23. package/dist/chunk-NLGUPJZA.js +507 -0
  24. package/dist/chunk-NLGUPJZA.js.map +1 -0
  25. package/dist/chunk-QDXZ27R7.js +33 -0
  26. package/dist/chunk-QDXZ27R7.js.map +1 -0
  27. package/dist/chunk-QGSW43TM.cjs +538 -0
  28. package/dist/chunk-QGSW43TM.cjs.map +1 -0
  29. package/dist/{chunk-TVFXN6W6.js → chunk-QL6KVIFR.js} +8 -4
  30. package/dist/chunk-QL6KVIFR.js.map +1 -0
  31. package/dist/chunk-RWQ675N6.js +165 -0
  32. package/dist/chunk-RWQ675N6.js.map +1 -0
  33. package/dist/chunk-UXUU7FVT.cjs +171 -0
  34. package/dist/chunk-UXUU7FVT.cjs.map +1 -0
  35. package/dist/chunk-XJ434N7I.js +30 -0
  36. package/dist/chunk-XJ434N7I.js.map +1 -0
  37. package/dist/{chunk-EE75WRCM.cjs → chunk-XOWSXZYW.cjs} +8 -4
  38. package/dist/chunk-XOWSXZYW.cjs.map +1 -0
  39. package/dist/chunk-ZBPF7TLZ.cjs +16 -0
  40. package/dist/chunk-ZBPF7TLZ.cjs.map +1 -0
  41. package/dist/chunk-ZINKVJQU.js +67 -0
  42. package/dist/chunk-ZINKVJQU.js.map +1 -0
  43. package/dist/index.cjs +53 -30
  44. package/dist/index.cjs.map +1 -1
  45. package/dist/index.d.cts +17 -9
  46. package/dist/index.d.ts +17 -9
  47. package/dist/index.js +8 -9
  48. package/dist/index.js.map +1 -1
  49. package/dist/layout/index.cjs +3 -3
  50. package/dist/layout/index.d.cts +1 -1
  51. package/dist/layout/index.d.ts +1 -1
  52. package/dist/layout/index.js +2 -2
  53. package/dist/portfolio/client.cjs +376 -12
  54. package/dist/portfolio/client.cjs.map +1 -1
  55. package/dist/portfolio/client.d.cts +27 -85
  56. package/dist/portfolio/client.d.ts +27 -85
  57. package/dist/portfolio/client.js +182 -4
  58. package/dist/portfolio/client.js.map +1 -1
  59. package/dist/portfolio/components/PortfolioAnalytics.cjs +87 -0
  60. package/dist/portfolio/components/PortfolioAnalytics.cjs.map +1 -0
  61. package/dist/portfolio/components/PortfolioAnalytics.d.cts +5 -0
  62. package/dist/portfolio/components/PortfolioAnalytics.d.ts +5 -0
  63. package/dist/portfolio/components/PortfolioAnalytics.js +9 -0
  64. package/dist/portfolio/components/PortfolioAnalytics.js.map +1 -0
  65. package/dist/portfolio/components/PortfolioErrorFallback.cjs +65 -0
  66. package/dist/portfolio/components/PortfolioErrorFallback.cjs.map +1 -0
  67. package/dist/portfolio/components/PortfolioErrorFallback.d.cts +22 -0
  68. package/dist/portfolio/components/PortfolioErrorFallback.d.ts +22 -0
  69. package/dist/portfolio/components/PortfolioErrorFallback.js +9 -0
  70. package/dist/portfolio/components/PortfolioErrorFallback.js.map +1 -0
  71. package/dist/portfolio/components/PortfolioLeadCTA.cjs +91 -0
  72. package/dist/portfolio/components/PortfolioLeadCTA.cjs.map +1 -0
  73. package/dist/portfolio/components/PortfolioLeadCTA.d.cts +34 -0
  74. package/dist/portfolio/components/PortfolioLeadCTA.d.ts +34 -0
  75. package/dist/portfolio/components/PortfolioLeadCTA.js +9 -0
  76. package/dist/portfolio/components/PortfolioLeadCTA.js.map +1 -0
  77. package/dist/portfolio/index.cjs +43 -19
  78. package/dist/portfolio/index.d.cts +210 -7
  79. package/dist/portfolio/index.d.ts +210 -7
  80. package/dist/portfolio/index.js +7 -7
  81. package/dist/portfolio/primitives/AnimatedCounter.cjs +88 -0
  82. package/dist/portfolio/primitives/AnimatedCounter.cjs.map +1 -0
  83. package/dist/portfolio/primitives/AnimatedCounter.d.cts +22 -0
  84. package/dist/portfolio/primitives/AnimatedCounter.d.ts +22 -0
  85. package/dist/portfolio/primitives/AnimatedCounter.js +68 -0
  86. package/dist/portfolio/primitives/AnimatedCounter.js.map +1 -0
  87. package/dist/portfolio/primitives/AnnotatedImage.cjs +320 -0
  88. package/dist/portfolio/primitives/AnnotatedImage.cjs.map +1 -0
  89. package/dist/portfolio/primitives/AnnotatedImage.d.cts +18 -0
  90. package/dist/portfolio/primitives/AnnotatedImage.d.ts +18 -0
  91. package/dist/portfolio/primitives/AnnotatedImage.js +264 -0
  92. package/dist/portfolio/primitives/AnnotatedImage.js.map +1 -0
  93. package/dist/portfolio/primitives/BeforeAfterSlider.cjs +348 -0
  94. package/dist/portfolio/primitives/BeforeAfterSlider.cjs.map +1 -0
  95. package/dist/portfolio/primitives/BeforeAfterSlider.d.cts +13 -0
  96. package/dist/portfolio/primitives/BeforeAfterSlider.d.ts +13 -0
  97. package/dist/portfolio/primitives/BeforeAfterSlider.js +292 -0
  98. package/dist/portfolio/primitives/BeforeAfterSlider.js.map +1 -0
  99. package/dist/portfolio/primitives/DeviceTrifolio.cjs +298 -0
  100. package/dist/portfolio/primitives/DeviceTrifolio.cjs.map +1 -0
  101. package/dist/portfolio/primitives/DeviceTrifolio.d.cts +14 -0
  102. package/dist/portfolio/primitives/DeviceTrifolio.d.ts +14 -0
  103. package/dist/portfolio/primitives/DeviceTrifolio.js +242 -0
  104. package/dist/portfolio/primitives/DeviceTrifolio.js.map +1 -0
  105. package/dist/portfolio/primitives/FrequencyBarSkeleton.cjs +88 -0
  106. package/dist/portfolio/primitives/FrequencyBarSkeleton.cjs.map +1 -0
  107. package/dist/portfolio/primitives/FrequencyBarSkeleton.d.cts +10 -0
  108. package/dist/portfolio/primitives/FrequencyBarSkeleton.d.ts +10 -0
  109. package/dist/portfolio/primitives/FrequencyBarSkeleton.js +68 -0
  110. package/dist/portfolio/primitives/FrequencyBarSkeleton.js.map +1 -0
  111. package/dist/portfolio/primitives/GlassCard.cjs +45 -0
  112. package/dist/portfolio/primitives/GlassCard.cjs.map +1 -0
  113. package/dist/portfolio/primitives/GlassCard.d.cts +11 -0
  114. package/dist/portfolio/primitives/GlassCard.d.ts +11 -0
  115. package/dist/portfolio/primitives/GlassCard.js +43 -0
  116. package/dist/portfolio/primitives/GlassCard.js.map +1 -0
  117. package/dist/portfolio/primitives/MetricBadge.cjs +74 -0
  118. package/dist/portfolio/primitives/MetricBadge.cjs.map +1 -0
  119. package/dist/portfolio/primitives/MetricBadge.d.cts +9 -0
  120. package/dist/portfolio/primitives/MetricBadge.d.ts +9 -0
  121. package/dist/portfolio/primitives/MetricBadge.js +54 -0
  122. package/dist/portfolio/primitives/MetricBadge.js.map +1 -0
  123. package/dist/portfolio/primitives/SVGChart.cjs +316 -0
  124. package/dist/portfolio/primitives/SVGChart.cjs.map +1 -0
  125. package/dist/portfolio/primitives/SVGChart.d.cts +18 -0
  126. package/dist/portfolio/primitives/SVGChart.d.ts +18 -0
  127. package/dist/portfolio/primitives/SVGChart.js +296 -0
  128. package/dist/portfolio/primitives/SVGChart.js.map +1 -0
  129. package/dist/portfolio/primitives/ScrollReveal.cjs +97 -0
  130. package/dist/portfolio/primitives/ScrollReveal.cjs.map +1 -0
  131. package/dist/portfolio/primitives/ScrollReveal.d.cts +27 -0
  132. package/dist/portfolio/primitives/ScrollReveal.d.ts +27 -0
  133. package/dist/portfolio/primitives/ScrollReveal.js +77 -0
  134. package/dist/portfolio/primitives/ScrollReveal.js.map +1 -0
  135. package/dist/portfolio/primitives/SplitHeadline.cjs +107 -0
  136. package/dist/portfolio/primitives/SplitHeadline.cjs.map +1 -0
  137. package/dist/portfolio/primitives/SplitHeadline.d.cts +24 -0
  138. package/dist/portfolio/primitives/SplitHeadline.d.ts +24 -0
  139. package/dist/portfolio/primitives/SplitHeadline.js +87 -0
  140. package/dist/portfolio/primitives/SplitHeadline.js.map +1 -0
  141. package/dist/portfolio/sections/BeforeAfterSection.cjs +271 -0
  142. package/dist/portfolio/sections/BeforeAfterSection.cjs.map +1 -0
  143. package/dist/portfolio/sections/BeforeAfterSection.d.cts +9 -0
  144. package/dist/portfolio/sections/BeforeAfterSection.d.ts +9 -0
  145. package/dist/{BeforeAfterSection-6BHFLY4Y.js → portfolio/sections/BeforeAfterSection.js} +66 -27
  146. package/dist/portfolio/sections/BeforeAfterSection.js.map +1 -0
  147. package/dist/portfolio/sections/CTASection.cjs +123 -0
  148. package/dist/portfolio/sections/CTASection.cjs.map +1 -0
  149. package/dist/portfolio/sections/CTASection.d.cts +9 -0
  150. package/dist/portfolio/sections/CTASection.d.ts +9 -0
  151. package/dist/portfolio/sections/CTASection.js +93 -0
  152. package/dist/portfolio/sections/CTASection.js.map +1 -0
  153. package/dist/{ChallengesSection-IZ3DHECS.cjs → portfolio/sections/ChallengesSection.cjs} +13 -7
  154. package/dist/portfolio/sections/ChallengesSection.cjs.map +1 -0
  155. package/dist/portfolio/sections/ChallengesSection.d.cts +9 -0
  156. package/dist/portfolio/sections/ChallengesSection.d.ts +9 -0
  157. package/dist/{ChallengesSection-GEQGVSJN.js → portfolio/sections/ChallengesSection.js} +6 -5
  158. package/dist/portfolio/sections/ChallengesSection.js.map +1 -0
  159. package/dist/portfolio/sections/ConversionFunnelSection.cjs +263 -0
  160. package/dist/portfolio/sections/ConversionFunnelSection.cjs.map +1 -0
  161. package/dist/portfolio/sections/ConversionFunnelSection.d.cts +9 -0
  162. package/dist/portfolio/sections/ConversionFunnelSection.d.ts +9 -0
  163. package/dist/{ConversionFunnelSection-D3GE4NKE.js → portfolio/sections/ConversionFunnelSection.js} +60 -56
  164. package/dist/portfolio/sections/ConversionFunnelSection.js.map +1 -0
  165. package/dist/{DesignSystemSection-KXIQXITF.cjs → portfolio/sections/DesignSystemSection.cjs} +13 -7
  166. package/dist/portfolio/sections/DesignSystemSection.cjs.map +1 -0
  167. package/dist/portfolio/sections/DesignSystemSection.d.cts +32 -0
  168. package/dist/portfolio/sections/DesignSystemSection.d.ts +32 -0
  169. package/dist/{DesignSystemSection-2R5BRBGO.js → portfolio/sections/DesignSystemSection.js} +5 -4
  170. package/dist/portfolio/sections/DesignSystemSection.js.map +1 -0
  171. package/dist/{DetailsSection-TTUZAPZZ.cjs → portfolio/sections/DetailsSection.cjs} +12 -6
  172. package/dist/portfolio/sections/DetailsSection.cjs.map +1 -0
  173. package/dist/portfolio/sections/DetailsSection.d.cts +9 -0
  174. package/dist/portfolio/sections/DetailsSection.d.ts +9 -0
  175. package/dist/{DetailsSection-A6PZQUQL.js → portfolio/sections/DetailsSection.js} +5 -4
  176. package/dist/portfolio/sections/DetailsSection.js.map +1 -0
  177. package/dist/portfolio/sections/FeatureSpotlightSection.cjs +267 -0
  178. package/dist/portfolio/sections/FeatureSpotlightSection.cjs.map +1 -0
  179. package/dist/portfolio/sections/FeatureSpotlightSection.d.cts +9 -0
  180. package/dist/portfolio/sections/FeatureSpotlightSection.d.ts +9 -0
  181. package/dist/{FeatureSpotlightSection-B7P3JGNL.js → portfolio/sections/FeatureSpotlightSection.js} +39 -33
  182. package/dist/portfolio/sections/FeatureSpotlightSection.js.map +1 -0
  183. package/dist/portfolio/sections/GallerySection.cjs +321 -0
  184. package/dist/portfolio/sections/GallerySection.cjs.map +1 -0
  185. package/dist/portfolio/sections/GallerySection.d.cts +9 -0
  186. package/dist/portfolio/sections/GallerySection.d.ts +9 -0
  187. package/dist/{GallerySection-VMKORC47.js → portfolio/sections/GallerySection.js} +65 -18
  188. package/dist/portfolio/sections/GallerySection.js.map +1 -0
  189. package/dist/portfolio/sections/HeroSection.cjs +180 -0
  190. package/dist/portfolio/sections/HeroSection.cjs.map +1 -0
  191. package/dist/portfolio/sections/HeroSection.d.cts +15 -0
  192. package/dist/portfolio/sections/HeroSection.d.ts +15 -0
  193. package/dist/portfolio/sections/HeroSection.js +171 -0
  194. package/dist/portfolio/sections/HeroSection.js.map +1 -0
  195. package/dist/{MetricsTimelineSection-4L6DUHJ5.cjs → portfolio/sections/MetricsTimelineSection.cjs} +102 -61
  196. package/dist/portfolio/sections/MetricsTimelineSection.cjs.map +1 -0
  197. package/dist/portfolio/sections/MetricsTimelineSection.d.cts +9 -0
  198. package/dist/portfolio/sections/MetricsTimelineSection.d.ts +9 -0
  199. package/dist/{MetricsTimelineSection-6BT5GNFV.js → portfolio/sections/MetricsTimelineSection.js} +45 -29
  200. package/dist/portfolio/sections/MetricsTimelineSection.js.map +1 -0
  201. package/dist/portfolio/sections/PerformanceSection.cjs +336 -0
  202. package/dist/portfolio/sections/PerformanceSection.cjs.map +1 -0
  203. package/dist/portfolio/sections/PerformanceSection.d.cts +22 -0
  204. package/dist/portfolio/sections/PerformanceSection.d.ts +22 -0
  205. package/dist/{PerformanceSection-MGCEIXDX.js → portfolio/sections/PerformanceSection.js} +59 -104
  206. package/dist/portfolio/sections/PerformanceSection.js.map +1 -0
  207. package/dist/{ResultsSection-XLGMMQKY.cjs → portfolio/sections/ResultsSection.cjs} +23 -10
  208. package/dist/portfolio/sections/ResultsSection.cjs.map +1 -0
  209. package/dist/portfolio/sections/ResultsSection.d.cts +13 -0
  210. package/dist/portfolio/sections/ResultsSection.d.ts +13 -0
  211. package/dist/{ResultsSection-DFUJ5U6M.js → portfolio/sections/ResultsSection.js} +14 -7
  212. package/dist/portfolio/sections/ResultsSection.js.map +1 -0
  213. package/dist/portfolio/sections/ServicesSection.cjs +129 -0
  214. package/dist/portfolio/sections/ServicesSection.cjs.map +1 -0
  215. package/dist/portfolio/sections/ServicesSection.d.cts +9 -0
  216. package/dist/portfolio/sections/ServicesSection.d.ts +9 -0
  217. package/dist/portfolio/sections/ServicesSection.js +122 -0
  218. package/dist/portfolio/sections/ServicesSection.js.map +1 -0
  219. package/dist/{SiteArchitectureSection-J7BPBERH.cjs → portfolio/sections/SiteArchitectureSection.cjs} +117 -93
  220. package/dist/portfolio/sections/SiteArchitectureSection.cjs.map +1 -0
  221. package/dist/portfolio/sections/SiteArchitectureSection.d.cts +21 -0
  222. package/dist/portfolio/sections/SiteArchitectureSection.d.ts +21 -0
  223. package/dist/{SiteArchitectureSection-TE22OAWV.js → portfolio/sections/SiteArchitectureSection.js} +47 -48
  224. package/dist/portfolio/sections/SiteArchitectureSection.js.map +1 -0
  225. package/dist/{SpeedComparisonSection-EZKFQVGW.cjs → portfolio/sections/SpeedComparisonSection.cjs} +13 -7
  226. package/dist/portfolio/sections/SpeedComparisonSection.cjs.map +1 -0
  227. package/dist/portfolio/sections/SpeedComparisonSection.d.cts +20 -0
  228. package/dist/portfolio/sections/SpeedComparisonSection.d.ts +20 -0
  229. package/dist/{SpeedComparisonSection-Y3K7OFZQ.js → portfolio/sections/SpeedComparisonSection.js} +6 -5
  230. package/dist/portfolio/sections/SpeedComparisonSection.js.map +1 -0
  231. package/dist/portfolio/sections/StrategySection.cjs +233 -0
  232. package/dist/portfolio/sections/StrategySection.cjs.map +1 -0
  233. package/dist/portfolio/sections/StrategySection.d.cts +9 -0
  234. package/dist/portfolio/sections/StrategySection.d.ts +9 -0
  235. package/dist/{StrategySection-DI5RSCJU.js → portfolio/sections/StrategySection.js} +60 -26
  236. package/dist/portfolio/sections/StrategySection.js.map +1 -0
  237. package/dist/portfolio/sections/TeamSection.cjs +162 -0
  238. package/dist/portfolio/sections/TeamSection.cjs.map +1 -0
  239. package/dist/portfolio/sections/TeamSection.d.cts +9 -0
  240. package/dist/portfolio/sections/TeamSection.d.ts +9 -0
  241. package/dist/portfolio/sections/TeamSection.js +106 -0
  242. package/dist/portfolio/sections/TeamSection.js.map +1 -0
  243. package/dist/portfolio/sections/TechStackSection.cjs +106 -0
  244. package/dist/portfolio/sections/TechStackSection.cjs.map +1 -0
  245. package/dist/portfolio/sections/TechStackSection.d.cts +9 -0
  246. package/dist/portfolio/sections/TechStackSection.d.ts +9 -0
  247. package/dist/portfolio/sections/TechStackSection.js +99 -0
  248. package/dist/portfolio/sections/TechStackSection.js.map +1 -0
  249. package/dist/{TestimonialSection-XPTFUQIN.cjs → portfolio/sections/TestimonialSection.cjs} +47 -28
  250. package/dist/portfolio/sections/TestimonialSection.cjs.map +1 -0
  251. package/dist/portfolio/sections/TestimonialSection.d.cts +9 -0
  252. package/dist/portfolio/sections/TestimonialSection.d.ts +9 -0
  253. package/dist/{TestimonialSection-6RGSMXQB.js → portfolio/sections/TestimonialSection.js} +40 -27
  254. package/dist/portfolio/sections/TestimonialSection.js.map +1 -0
  255. package/dist/portfolio/sections/VideoSection.cjs +181 -0
  256. package/dist/portfolio/sections/VideoSection.cjs.map +1 -0
  257. package/dist/portfolio/sections/VideoSection.d.cts +9 -0
  258. package/dist/portfolio/sections/VideoSection.d.ts +9 -0
  259. package/dist/{VideoSection-4A2HC6K6.js → portfolio/sections/VideoSection.js} +21 -13
  260. package/dist/portfolio/sections/VideoSection.js.map +1 -0
  261. package/dist/portfolio/server.cjs +56 -144
  262. package/dist/portfolio/server.cjs.map +1 -1
  263. package/dist/portfolio/server.d.cts +157 -7
  264. package/dist/portfolio/server.d.ts +157 -7
  265. package/dist/portfolio/server.js +3 -142
  266. package/dist/portfolio/server.js.map +1 -1
  267. package/dist/preview.cjs +56 -0
  268. package/dist/preview.cjs.map +1 -0
  269. package/dist/preview.d.cts +31 -0
  270. package/dist/preview.d.ts +31 -0
  271. package/dist/preview.js +54 -0
  272. package/dist/preview.js.map +1 -0
  273. package/dist/{types-DL4t_Cfa.d.ts → types-0tVPy4pU.d.cts} +7 -1
  274. package/dist/{types-DL4t_Cfa.d.cts → types-0tVPy4pU.d.ts} +7 -1
  275. package/dist/types-D0nb6KAx.d.cts +299 -0
  276. package/dist/types-D0nb6KAx.d.ts +299 -0
  277. package/package.json +38 -14
  278. package/templates/api-portfolio-preview.ts +6 -0
  279. package/templates/api-revalidate.ts +5 -0
  280. package/templates/work-category-page.tsx +40 -0
  281. package/templates/work-error.tsx +3 -0
  282. package/templates/work-loading.tsx +1 -0
  283. package/templates/work-page.tsx +37 -0
  284. package/templates/work-slug-loading.tsx +1 -0
  285. package/templates/work-slug-page.tsx +41 -0
  286. package/dist/BeforeAfterSection-6BHFLY4Y.js.map +0 -1
  287. package/dist/BeforeAfterSection-JTORBR3A.cjs +0 -181
  288. package/dist/BeforeAfterSection-JTORBR3A.cjs.map +0 -1
  289. package/dist/CTASection-4JKLXEUF.cjs +0 -111
  290. package/dist/CTASection-4JKLXEUF.cjs.map +0 -1
  291. package/dist/CTASection-BJA72XIL.js +0 -106
  292. package/dist/CTASection-BJA72XIL.js.map +0 -1
  293. package/dist/ChallengesSection-GEQGVSJN.js.map +0 -1
  294. package/dist/ChallengesSection-IZ3DHECS.cjs.map +0 -1
  295. package/dist/ConversionFunnelSection-AUUSJ5HQ.cjs +0 -209
  296. package/dist/ConversionFunnelSection-AUUSJ5HQ.cjs.map +0 -1
  297. package/dist/ConversionFunnelSection-D3GE4NKE.js.map +0 -1
  298. package/dist/DesignSystemSection-2R5BRBGO.js.map +0 -1
  299. package/dist/DesignSystemSection-KXIQXITF.cjs.map +0 -1
  300. package/dist/DetailsSection-A6PZQUQL.js.map +0 -1
  301. package/dist/DetailsSection-TTUZAPZZ.cjs.map +0 -1
  302. package/dist/FeatureSpotlightSection-B7P3JGNL.js.map +0 -1
  303. package/dist/FeatureSpotlightSection-WRHXS7TU.cjs +0 -210
  304. package/dist/FeatureSpotlightSection-WRHXS7TU.cjs.map +0 -1
  305. package/dist/GallerySection-VMKORC47.js.map +0 -1
  306. package/dist/GallerySection-WJ4PQDBI.cjs +0 -219
  307. package/dist/GallerySection-WJ4PQDBI.cjs.map +0 -1
  308. package/dist/MetricsTimelineSection-4L6DUHJ5.cjs.map +0 -1
  309. package/dist/MetricsTimelineSection-6BT5GNFV.js.map +0 -1
  310. package/dist/PerformanceSection-24TVVFZA.cjs +0 -356
  311. package/dist/PerformanceSection-24TVVFZA.cjs.map +0 -1
  312. package/dist/PerformanceSection-MGCEIXDX.js.map +0 -1
  313. package/dist/ResultsSection-DFUJ5U6M.js.map +0 -1
  314. package/dist/ResultsSection-XLGMMQKY.cjs.map +0 -1
  315. package/dist/ServicesSection-D5V3Q4GR.js +0 -118
  316. package/dist/ServicesSection-D5V3Q4GR.js.map +0 -1
  317. package/dist/ServicesSection-WJMGK2MF.cjs +0 -120
  318. package/dist/ServicesSection-WJMGK2MF.cjs.map +0 -1
  319. package/dist/SiteArchitectureSection-J7BPBERH.cjs.map +0 -1
  320. package/dist/SiteArchitectureSection-TE22OAWV.js.map +0 -1
  321. package/dist/SpeedComparisonSection-EZKFQVGW.cjs.map +0 -1
  322. package/dist/SpeedComparisonSection-Y3K7OFZQ.js.map +0 -1
  323. package/dist/StrategySection-CJ7Y6OFQ.cjs +0 -174
  324. package/dist/StrategySection-CJ7Y6OFQ.cjs.map +0 -1
  325. package/dist/StrategySection-DI5RSCJU.js.map +0 -1
  326. package/dist/TeamSection-DZVSNZE6.cjs +0 -112
  327. package/dist/TeamSection-DZVSNZE6.cjs.map +0 -1
  328. package/dist/TeamSection-HGKFW6PQ.js +0 -107
  329. package/dist/TeamSection-HGKFW6PQ.js.map +0 -1
  330. package/dist/TechStackSection-2AQ7RGY3.js +0 -93
  331. package/dist/TechStackSection-2AQ7RGY3.js.map +0 -1
  332. package/dist/TechStackSection-VTNNZR5V.cjs +0 -95
  333. package/dist/TechStackSection-VTNNZR5V.cjs.map +0 -1
  334. package/dist/TestimonialSection-6RGSMXQB.js.map +0 -1
  335. package/dist/TestimonialSection-XPTFUQIN.cjs.map +0 -1
  336. package/dist/VideoSection-4A2HC6K6.js.map +0 -1
  337. package/dist/VideoSection-G3DFS7UH.cjs +0 -118
  338. package/dist/VideoSection-G3DFS7UH.cjs.map +0 -1
  339. package/dist/chunk-2Y4O3LWM.js +0 -53
  340. package/dist/chunk-2Y4O3LWM.js.map +0 -1
  341. package/dist/chunk-633UMQQH.cjs +0 -31
  342. package/dist/chunk-633UMQQH.cjs.map +0 -1
  343. package/dist/chunk-7CFFAKDM.js +0 -74
  344. package/dist/chunk-7CFFAKDM.js.map +0 -1
  345. package/dist/chunk-APG2QSMB.js +0 -131
  346. package/dist/chunk-APG2QSMB.js.map +0 -1
  347. package/dist/chunk-AVCH4LMG.cjs +0 -620
  348. package/dist/chunk-AVCH4LMG.cjs.map +0 -1
  349. package/dist/chunk-AYBUHRFS.js +0 -613
  350. package/dist/chunk-AYBUHRFS.js.map +0 -1
  351. package/dist/chunk-EE75WRCM.cjs.map +0 -1
  352. package/dist/chunk-IKBK7HYX.cjs +0 -79
  353. package/dist/chunk-IKBK7HYX.cjs.map +0 -1
  354. package/dist/chunk-KEOHORIH.cjs +0 -79
  355. package/dist/chunk-KEOHORIH.cjs.map +0 -1
  356. package/dist/chunk-OA5ZM4OA.cjs +0 -139
  357. package/dist/chunk-OA5ZM4OA.cjs.map +0 -1
  358. package/dist/chunk-TVFXN6W6.js.map +0 -1
  359. package/dist/chunk-VVTK4OL6.js +0 -28
  360. package/dist/chunk-VVTK4OL6.js.map +0 -1
  361. package/dist/chunk-XCKXHK44.js +0 -15
  362. package/dist/chunk-XCKXHK44.js.map +0 -1
  363. package/dist/chunk-XONXEFJY.cjs +0 -58
  364. package/dist/chunk-XONXEFJY.cjs.map +0 -1
  365. package/dist/chunk-XQNJED46.cjs +0 -19
  366. package/dist/chunk-XQNJED46.cjs.map +0 -1
  367. package/dist/chunk-YB4B3OMC.js +0 -74
  368. package/dist/chunk-YB4B3OMC.js.map +0 -1
  369. package/dist/portfolio/sections.cjs +0 -20
  370. package/dist/portfolio/sections.cjs.map +0 -1
  371. package/dist/portfolio/sections.d.cts +0 -42
  372. package/dist/portfolio/sections.d.ts +0 -42
  373. package/dist/portfolio/sections.js +0 -4
  374. package/dist/portfolio/sections.js.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/portfolio/components/PortfolioPage.tsx","../src/portfolio/components/PortfolioSchema.tsx","../src/portfolio/components/PortfolioCard.tsx","../src/portfolio/components/PortfolioGrid.tsx","../src/portfolio/components/PortfolioIndex.tsx","../src/portfolio/components/PortfolioSkeletons.tsx","../src/types.ts"],"names":["dynamic","jsx","HeroSection","ChallengesSection","ResultsSection","TechStackSection","ServicesSection","TestimonialSection","SpeedComparisonSection","sanitizeSiteArchitectureData","DesignSystemSection","DetailsSection","curatePortfolioProof","jsxs","PortfolioAnalytics","curateHeroKpis","Link","GlassCard","Image","formatCategoryLabel","ScrollReveal","getPortfolioCategories","slugifyCategory","getPortfolioItems"],"mappings":"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","file":"chunk-QGSW43TM.cjs","sourcesContent":["/**\n * PortfolioPage — Server component that renders a full portfolio case study.\n *\n * Maps each section in item.sections to the correct renderer component.\n * All sections below the hero are loaded lazily via next/dynamic.\n *\n * NOTE: This file is intentionally NOT a client component — it is designed\n * to be used as an RSC in a Next.js app directory page.\n *\n * Sections MUST be imported via `@sonordev/agency-site-kit/portfolio/sections/*`\n * (self-referencing subpaths, external in tsup's server build), never via\n * relative paths: a relative import would bundle the client sections into\n * this server chunk, the 'use client' boundary would be lost, and the whole\n * PortfolioItemFull would serialize into the RSC flight payload again.\n *\n * Whether a section is a server or client component is decided by the\n * section file itself (its 'use client' directive; tsup routes it to the\n * matching build). Server sections are imported statically — they ship zero\n * client JS, so there is nothing to lazy-load. Client sections load through\n * next/dynamic so each becomes its own browser chunk, fetched only when the\n * case study actually renders that section type.\n */\n\nimport React from 'react';\nimport dynamic from 'next/dynamic';\nimport type {\n PortfolioItemFull,\n PortfolioSection,\n PortfolioHeroData,\n PortfolioChallengesData,\n PortfolioStrategyData,\n PortfolioResultsData,\n PortfolioTechStackData,\n PortfolioServicesData,\n PortfolioTestimonialData,\n PortfolioGalleryData,\n PortfolioVideoData,\n PortfolioTeamData,\n PortfolioFeatureSpotlightData,\n PortfolioBeforeAfterData,\n PortfolioMetricsTimelineData,\n PortfolioConversionFunnelData,\n PortfolioDetailsData,\n PortfolioCTAData,\n} from '../../types';\n\n// Hero is loaded eagerly (above the fold, client — it animates)\nimport HeroSection from '@sonordev/agency-site-kit/portfolio/sections/HeroSection';\n\n// Server sections — no 'use client' in their files, so they render on the\n// server and ship ZERO component JS to the browser. Static imports: there is\n// no client chunk to defer.\nimport ChallengesSection from '@sonordev/agency-site-kit/portfolio/sections/ChallengesSection';\nimport ResultsSection from '@sonordev/agency-site-kit/portfolio/sections/ResultsSection';\nimport TechStackSection from '@sonordev/agency-site-kit/portfolio/sections/TechStackSection';\nimport ServicesSection from '@sonordev/agency-site-kit/portfolio/sections/ServicesSection';\nimport TestimonialSection from '@sonordev/agency-site-kit/portfolio/sections/TestimonialSection';\nimport SpeedComparisonSection from '@sonordev/agency-site-kit/portfolio/sections/SpeedComparisonSection';\nimport DesignSystemSection from '@sonordev/agency-site-kit/portfolio/sections/DesignSystemSection';\nimport DetailsSection from '@sonordev/agency-site-kit/portfolio/sections/DetailsSection';\n\n// Client sections — animated/interactive; lazily loaded so each becomes its\n// own browser chunk, fetched only when rendered.\nconst StrategySection = dynamic(() => import('@sonordev/agency-site-kit/portfolio/sections/StrategySection'), { ssr: true });\nconst GallerySection = dynamic(() => import('@sonordev/agency-site-kit/portfolio/sections/GallerySection'), { ssr: true });\nconst VideoSection = dynamic(() => import('@sonordev/agency-site-kit/portfolio/sections/VideoSection'), { ssr: true });\nconst TeamSection = dynamic(() => import('@sonordev/agency-site-kit/portfolio/sections/TeamSection'), { ssr: true });\nconst FeatureSpotlightSection = dynamic(() => import('@sonordev/agency-site-kit/portfolio/sections/FeatureSpotlightSection'), { ssr: true });\nconst BeforeAfterSection = dynamic(() => import('@sonordev/agency-site-kit/portfolio/sections/BeforeAfterSection'), { ssr: true });\nconst MetricsTimelineSection = dynamic(() => import('@sonordev/agency-site-kit/portfolio/sections/MetricsTimelineSection'), { ssr: true });\nconst ConversionFunnelSection = dynamic(() => import('@sonordev/agency-site-kit/portfolio/sections/ConversionFunnelSection'), { ssr: true });\nconst PerformanceSection = dynamic(() => import('@sonordev/agency-site-kit/portfolio/sections/PerformanceSection'), { ssr: true });\nconst SiteArchitectureSection = dynamic(() => import('@sonordev/agency-site-kit/portfolio/sections/SiteArchitectureSection'), { ssr: true });\nconst CTASection = dynamic(() => import('@sonordev/agency-site-kit/portfolio/sections/CTASection'), { ssr: true });\n\ninterface PortfolioPageProps {\n item: PortfolioItemFull;\n}\n\n// Defense-in-depth only — the enforcing sanitize pass runs in\n// getPortfolioItem (portfolio/server.ts), the last genuinely-server-side hop.\n// See src/portfolio/sanitize.ts for the single source of truth and the story.\nimport { sanitizeSiteArchitectureData } from '../sanitize';\n\n// Display policy for proof points (Lighthouse dedupe, results curation) —\n// see src/portfolio/curate-proof.ts for the single source of truth.\nimport { curatePortfolioProof } from '../curate-proof';\n\n// Engagement-tracking island (client) — crosses the boundary via subpath.\nimport PortfolioAnalytics from '@sonordev/agency-site-kit/portfolio/components/PortfolioAnalytics';\n\nfunction renderSection(section: PortfolioSection, item: PortfolioItemFull, index: number) {\n const key = `${section.sectionType}-${index}`;\n\n switch (section.sectionType) {\n case 'portfolioHero':\n return (\n <HeroSection\n key={key}\n data={section.data as PortfolioHeroData}\n screenshots={item.hero_screenshots ?? undefined}\n liveUrl={item.live_url ?? undefined}\n />\n );\n case 'portfolioChallenges':\n return <ChallengesSection key={key} data={section.data as PortfolioChallengesData} />;\n case 'portfolioStrategy':\n return <StrategySection key={key} data={section.data as PortfolioStrategyData} />;\n case 'portfolioResults':\n return (\n <ResultsSection\n key={key}\n data={section.data as PortfolioResultsData}\n metricsDelta={item.metricsDelta}\n metricsRefreshedAt={item.metrics_last_refreshed_at}\n />\n );\n case 'portfolioTechStack':\n return <TechStackSection key={key} data={section.data as PortfolioTechStackData} />;\n case 'portfolioServices':\n return <ServicesSection key={key} data={section.data as PortfolioServicesData} />;\n case 'portfolioTestimonial':\n return <TestimonialSection key={key} data={section.data as PortfolioTestimonialData} />;\n case 'portfolioGallery':\n return <GallerySection key={key} data={section.data as PortfolioGalleryData} />;\n case 'portfolioVideo':\n return <VideoSection key={key} data={section.data as PortfolioVideoData} />;\n case 'portfolioTeam':\n return <TeamSection key={key} data={section.data as PortfolioTeamData} />;\n case 'portfolioFeatureSpotlight':\n return <FeatureSpotlightSection key={key} data={section.data as PortfolioFeatureSpotlightData} />;\n case 'portfolioBeforeAfter':\n return <BeforeAfterSection key={key} data={section.data as PortfolioBeforeAfterData} />;\n case 'portfolioMetricsTimeline':\n return <MetricsTimelineSection key={key} data={section.data as PortfolioMetricsTimelineData} />;\n case 'portfolioConversionFunnel':\n return <ConversionFunnelSection key={key} data={section.data as PortfolioConversionFunnelData} />;\n case 'portfolioPerformance':\n return <PerformanceSection key={key} data={section.data as any} />;\n case 'portfolioSpeedComparison':\n return <SpeedComparisonSection key={key} data={section.data as any} />;\n case 'portfolioSiteArchitecture':\n return <SiteArchitectureSection key={key} data={sanitizeSiteArchitectureData(section.data) as any} />;\n case 'portfolioDesignSystem':\n return <DesignSystemSection key={key} data={section.data as any} />;\n case 'portfolioDetails':\n return <DetailsSection key={key} data={section.data as PortfolioDetailsData} />;\n case 'portfolioCTA':\n return <CTASection key={key} data={section.data as PortfolioCTAData} />;\n case 'portfolioSeo':\n // SEO section is metadata-only; nothing to render visually.\n return null;\n default:\n return null;\n }\n}\n\n// Canonical display order — ensures consistent rendering regardless of API\n// order. Keys must match PortfolioSectionType exactly (note 'portfolioCTA' —\n// a lowercased 'portfolioCta' key silently fell through to the ?? 99 fallback\n// for years). Unknown/future types sort last via the fallback.\nconst SECTION_ORDER: Record<string, number> = {\n portfolioHero: 0,\n portfolioChallenges: 1,\n portfolioStrategy: 2,\n portfolioResults: 3,\n portfolioPerformance: 4,\n portfolioSpeedComparison: 5,\n portfolioSiteArchitecture: 6,\n portfolioTechStack: 7,\n portfolioDesignSystem: 8,\n portfolioServices: 9,\n portfolioBeforeAfter: 10,\n portfolioTestimonial: 11,\n portfolioMetricsTimeline: 12,\n portfolioConversionFunnel: 13,\n portfolioDetails: 14,\n portfolioSeo: 15,\n portfolioCTA: 16,\n};\n\nexport default function PortfolioPage({ item: rawItem }: PortfolioPageProps) {\n const item = curatePortfolioProof(rawItem);\n\n // Section order: once an agency has deliberately reordered in the\n // dashboard (item.sectionsOrdered), the array order IS the narrative —\n // respect it, with the hero pinned first (it's the LCP shell). Items\n // never reordered keep the canonical order below, so nothing shifts\n // out from under existing case studies.\n const sortedSections = item.sectionsOrdered\n ? [...item.sections].sort(\n (a, b) =>\n (a.sectionType === 'portfolioHero' ? 0 : 1) -\n (b.sectionType === 'portfolioHero' ? 0 : 1),\n )\n : [...item.sections].sort(\n (a, b) => (SECTION_ORDER[a.sectionType] ?? 99) - (SECTION_ORDER[b.sectionType] ?? 99)\n );\n\n return (\n <article className=\"flex w-full min-w-0 max-w-full flex-col overflow-x-clip\">\n {sortedSections.map((section, index) => {\n const rendered = renderSection(section, item, index);\n if (!rendered) return null;\n return (\n <div\n key={`wrapper-${section.sectionType}-${index}`}\n className=\"w-full\"\n data-portfolio-section={section.sectionType}\n >\n {rendered}\n </div>\n );\n })}\n {/* Engagement tracking — childless idle-deferred sibling, never a wrapper */}\n <PortfolioAnalytics slug={item.slug} />\n </article>\n );\n}\n","/**\n * PortfolioSchema — JSON-LD structured data for a case study page.\n *\n * Server component: renders a single <script type=\"application/ld+json\">\n * describing the case study as a schema.org Article (there is no dedicated\n * CaseStudy type; Article is what Google documents for editorial content).\n * Ships zero client JS and adds rich-result eligibility + entity clarity for\n * the agency's portfolio pages.\n *\n * Usage (in the case study page, next to <PortfolioPage />):\n *\n * ```tsx\n * <PortfolioSchema\n * item={item}\n * url={`https://youragency.com/work/${item.slug}`}\n * publisher={{ name: 'Your Agency', url: 'https://youragency.com' }}\n * />\n * ```\n *\n * Only reads fields from the already-sanitized PortfolioItemFull (see\n * getPortfolioItem / sanitize.ts) — never section internals.\n */\n\nimport React from 'react';\nimport type { PortfolioItemFull } from '../../types';\n\nexport interface PortfolioSchemaProps {\n item: PortfolioItemFull;\n /** Canonical URL of this case study page. */\n url: string;\n /** The agency, emitted as author + publisher Organization. */\n publisher?: {\n name: string;\n url?: string;\n /** Absolute URL to a logo image. */\n logo?: string;\n };\n}\n\n/** JSON-LD-safe serialization — escape `<` so `</script>` can never break out. */\nfunction jsonLd(value: unknown): string {\n return JSON.stringify(value).replace(/</g, '\\\\u003c');\n}\n\nexport default function PortfolioSchema({ item, url, publisher }: PortfolioSchemaProps) {\n const headline = item.seo?.metaTitle || item.title;\n const description = item.seo?.metaDescription || item.description;\n\n const keywords = [\n ...(item.seo?.keywords ?? []),\n item.category,\n ...item.services,\n ].filter(Boolean);\n\n const org = publisher\n ? {\n '@type': 'Organization',\n name: publisher.name,\n ...(publisher.url ? { url: publisher.url } : {}),\n ...(publisher.logo\n ? { logo: { '@type': 'ImageObject', url: publisher.logo } }\n : {}),\n }\n : undefined;\n\n const schema = {\n '@context': 'https://schema.org',\n '@type': 'Article',\n headline,\n ...(description ? { description } : {}),\n ...(item.hero_image ? { image: [item.hero_image] } : {}),\n url,\n mainEntityOfPage: { '@type': 'WebPage', '@id': url },\n ...(item.published_at ? { datePublished: item.published_at } : {}),\n ...(item.metrics_last_refreshed_at || item.published_at\n ? { dateModified: item.metrics_last_refreshed_at || item.published_at }\n : {}),\n ...(keywords.length ? { keywords: keywords.join(', ') } : {}),\n ...(org ? { author: org, publisher: org } : {}),\n ...(item.details?.industry ? { about: item.details.industry } : {}),\n };\n\n return (\n <script\n type=\"application/ld+json\"\n dangerouslySetInnerHTML={{ __html: jsonLd(schema) }}\n />\n );\n}\n","// Shared component — no directive on purpose: hook-free, so it renders\n// server-side (zero JS) from PortfolioIndex and stays usable from client\n// code via the /portfolio/client barrel.\nimport React from 'react';\nimport Image from 'next/image';\nimport Link from 'next/link';\nimport type { PortfolioItem } from '../../types';\nimport GlassCard from '@sonordev/agency-site-kit/portfolio/primitives/GlassCard';\nimport { curateHeroKpis } from '../curate-proof';\nimport { formatCategoryLabel, formatServiceTag } from '../format';\n\ninterface PortfolioCardProps {\n item: PortfolioItem;\n className?: string;\n /** Base path for portfolio detail links (default: '/work') */\n basePath?: string;\n}\n\nexport default function PortfolioCard({ item, className = '', basePath = '/work' }: PortfolioCardProps) {\n // Use hero_screenshots desktop → hero_image → trifolio screenshot convention\n const thumbnailSrc =\n item.hero_screenshots?.desktop ||\n item.hero_image ||\n (item.project_id ? `/portfolio-previews/${item.slug}.png` : undefined);\n const kpis = Array.isArray(item.kpis) ? item.kpis : [];\n const services = Array.isArray(item.services) ? item.services : [];\n // Collapse per-category Lighthouse KPIs so a card never reads\n // \"100/100 · 100/100 · 100/100\" — business outcomes lead the preview.\n const kpiPreview = curateHeroKpis(kpis).slice(0, 3);\n const servicePreview = services.slice(0, 3);\n\n return (\n <Link href={`${basePath}/${item.slug}`} className={`block group ${className}`}>\n <GlassCard padding=\"sm\" hover>\n <div className=\"flex flex-col gap-4\">\n {/* Thumbnail */}\n {thumbnailSrc && (\n <div\n className=\"relative w-full overflow-hidden rounded-xl\"\n style={{ aspectRatio: '16/10' }}\n >\n <Image\n src={thumbnailSrc}\n alt={item.hero_image_alt || item.title}\n fill\n sizes=\"(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 33vw\"\n className=\"object-cover object-top transition-transform duration-500 group-hover:scale-105\"\n />\n {/* Category badge */}\n <span\n className=\"absolute top-3 left-3 px-3 py-1 rounded-full text-[11px] uppercase tracking-wider font-semibold\"\n style={{\n background: 'color-mix(in srgb, var(--sk-primary, #6366f1) 80%, transparent)',\n color: '#ffffff',\n backdropFilter: 'blur(8px)',\n }}\n >\n {formatCategoryLabel(item.category)}\n </span>\n </div>\n )}\n\n {/* Text content */}\n <div className=\"flex flex-col gap-2 px-2 pb-2\">\n <h3\n className=\"text-lg font-semibold line-clamp-1\"\n style={{ color: 'var(--sk-text-primary, #ffffff)' }}\n >\n {item.title}\n </h3>\n\n {item.subtitle && (\n <p\n className=\"text-sm line-clamp-2\"\n style={{ color: 'var(--sk-text-tertiary, #71717a)' }}\n >\n {item.subtitle}\n </p>\n )}\n\n {/* KPI preview */}\n {kpiPreview.length > 0 && (\n <div className=\"flex items-center gap-4 mt-1\">\n {kpiPreview.map((kpi, i) => (\n <div key={i} className=\"flex flex-col\">\n <span\n className=\"text-base font-bold\"\n style={{ color: 'var(--sk-primary, #6366f1)' }}\n >\n {kpi.prefix || ''}\n {kpi.value.toLocaleString()}\n {kpi.suffix}\n </span>\n <span\n className=\"text-[10px] uppercase tracking-wider font-medium\"\n style={{ color: 'var(--sk-text-tertiary, #71717a)' }}\n >\n {kpi.label}\n </span>\n </div>\n ))}\n </div>\n )}\n\n {/* Services tags */}\n {servicePreview.length > 0 && (\n <div className=\"flex flex-wrap gap-1.5 mt-2\">\n {servicePreview.map((service) => (\n <span\n key={service}\n className=\"px-2.5 py-0.5 rounded-full text-[11px] font-medium\"\n style={{\n background: 'var(--sk-surface, rgba(255,255,255,0.05))',\n color: 'var(--sk-text-secondary, #a1a1aa)',\n border: '1px solid var(--sk-border, rgba(255,255,255,0.1))',\n }}\n >\n {formatServiceTag(service)}\n </span>\n ))}\n {services.length > 3 && (\n <span\n className=\"px-2.5 py-0.5 rounded-full text-[11px] font-medium\"\n style={{\n background: 'var(--sk-surface, rgba(255,255,255,0.05))',\n color: 'var(--sk-text-tertiary, #71717a)',\n }}\n >\n +{item.services.length - 3}\n </span>\n )}\n </div>\n )}\n </div>\n </div>\n </GlassCard>\n </Link>\n );\n}\n","// Shared component — no directive on purpose: hook-free, so it renders\n// server-side (zero JS) from PortfolioIndex and stays usable from client\n// code via the /portfolio/client barrel.\nimport React from 'react';\nimport type { PortfolioItem } from '../../types';\nimport ScrollReveal from '@sonordev/agency-site-kit/portfolio/primitives/ScrollReveal';\nimport PortfolioCard from './PortfolioCard';\n\ninterface PortfolioGridProps {\n items: PortfolioItem[];\n className?: string;\n /** Base path for portfolio detail links (default: '/work') */\n basePath?: string;\n}\n\nexport default function PortfolioGrid({ items, className = '', basePath = '/work' }: PortfolioGridProps) {\n return (\n <ScrollReveal stagger={0.1}>\n <div\n className={`grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 ${className}`}\n style={{\n gap: 'var(--sk-grid-gap, 24px)',\n }}\n >\n {items.map((item) => (\n <PortfolioCard key={item.id} item={item} basePath={basePath} />\n ))}\n </div>\n </ScrollReveal>\n );\n}\n","/**\n * PortfolioIndex — turnkey, server-rendered /work index.\n *\n * Fetches items + categories and renders: category nav (real links —\n * crawlable, no client state), the portfolio grid, pagination, and an\n * intentional empty state. Ships zero component JS.\n *\n * Category filtering is route-based for SEO:\n * /work → all items\n * /work/category/[category] → one category (slug via slugifyCategory)\n * Pagination via ?page= on either route.\n */\n\nimport React from 'react';\nimport {\n getPortfolioCategories,\n getPortfolioItems,\n slugifyCategory,\n} from '../server';\nimport PortfolioGrid from './PortfolioGrid';\n\nexport interface PortfolioIndexProps {\n /** Route prefix for case studies + category links. Defaults to \"/work\". */\n basePath?: string;\n /** Active category SLUG (from the [category] route param). */\n category?: string | null;\n /** 1-based page number (from ?page=). */\n page?: number;\n /** Items per page. Defaults to 12. */\n pageSize?: number;\n /** Optional heading block rendered above the category nav. */\n heading?: React.ReactNode;\n /** Override the empty state. */\n emptyState?: React.ReactNode;\n /** Hide the category nav (e.g. single-category portfolios). */\n showCategories?: boolean;\n className?: string;\n}\n\nfunction CategoryLink({\n href,\n label,\n active,\n}: {\n href: string;\n label: string;\n active: boolean;\n}) {\n return (\n <a\n href={href}\n aria-current={active ? 'page' : undefined}\n className=\"px-4 py-1.5 rounded-full text-sm font-medium transition-colors\"\n style={{\n background: active\n ? 'color-mix(in srgb, var(--sk-primary, #6366f1) 15%, transparent)'\n : 'var(--sk-surface, rgba(255,255,255,0.05))',\n color: active ? 'var(--sk-primary, #6366f1)' : 'var(--sk-text-secondary, #a1a1aa)',\n border: `1px solid ${\n active\n ? 'color-mix(in srgb, var(--sk-primary, #6366f1) 25%, transparent)'\n : 'var(--sk-border, rgba(255,255,255,0.1))'\n }`,\n }}\n >\n {label}\n </a>\n );\n}\n\nfunction PageLink({ href, label, disabled }: { href: string; label: string; disabled: boolean }) {\n if (disabled) {\n return (\n <span\n aria-disabled=\"true\"\n className=\"px-4 py-2 rounded-xl text-sm font-medium opacity-40\"\n style={{\n background: 'var(--sk-surface, rgba(255,255,255,0.05))',\n color: 'var(--sk-text-tertiary, #71717a)',\n border: '1px solid var(--sk-border, rgba(255,255,255,0.1))',\n }}\n >\n {label}\n </span>\n );\n }\n return (\n <a\n href={href}\n className=\"px-4 py-2 rounded-xl text-sm font-medium transition-colors\"\n style={{\n background: 'var(--sk-surface, rgba(255,255,255,0.05))',\n color: 'var(--sk-text-secondary, #a1a1aa)',\n border: '1px solid var(--sk-border, rgba(255,255,255,0.1))',\n }}\n >\n {label}\n </a>\n );\n}\n\nexport default async function PortfolioIndex({\n basePath = '/work',\n category = null,\n page = 1,\n pageSize = 12,\n heading,\n emptyState,\n showCategories = true,\n className = '',\n}: PortfolioIndexProps) {\n const currentPage = Math.max(1, Math.floor(page));\n const categories = await getPortfolioCategories();\n\n // Route param is a slug; the API filter wants the original category value.\n const activeCategory = category\n ? categories.find((c) => slugifyCategory(c) === category) ?? null\n : null;\n\n const { items, total } = await getPortfolioItems({\n category: activeCategory ?? undefined,\n limit: pageSize,\n offset: (currentPage - 1) * pageSize,\n });\n\n const totalPages = Math.max(1, Math.ceil(total / pageSize));\n const pageHref = (p: number) => {\n const base = activeCategory\n ? `${basePath}/category/${slugifyCategory(activeCategory)}`\n : basePath;\n return p <= 1 ? base : `${base}?page=${p}`;\n };\n\n return (\n <section className={`w-full ${className}`}>\n <div className=\"max-w-7xl mx-auto px-6\">\n {heading}\n\n {/* Category nav — real links, crawlable */}\n {showCategories && categories.length > 1 && (\n <nav aria-label=\"Portfolio categories\" className=\"flex flex-wrap gap-2 mb-10\">\n <CategoryLink href={basePath} label=\"All\" active={!activeCategory} />\n {categories.map((c) => (\n <CategoryLink\n key={c}\n href={`${basePath}/category/${slugifyCategory(c)}`}\n label={c}\n active={activeCategory === c}\n />\n ))}\n </nav>\n )}\n\n {items.length > 0 ? (\n <PortfolioGrid items={items} basePath={basePath} />\n ) : (\n emptyState ?? (\n <div\n className=\"flex flex-col items-center gap-3 py-24 rounded-2xl text-center\"\n style={{\n background: 'var(--sk-surface, rgba(255,255,255,0.03))',\n border: '1px dashed var(--sk-border, rgba(255,255,255,0.1))',\n }}\n >\n <span\n className=\"text-lg font-semibold\"\n style={{ color: 'var(--sk-text-primary, #ffffff)' }}\n >\n Case studies coming soon\n </span>\n <span className=\"text-sm\" style={{ color: 'var(--sk-text-tertiary, #71717a)' }}>\n {activeCategory\n ? `Nothing published in ${activeCategory} yet.`\n : \"We're writing up our latest work right now.\"}\n </span>\n </div>\n )\n )}\n\n {/* Pagination */}\n {totalPages > 1 && (\n <nav\n aria-label=\"Portfolio pages\"\n className=\"flex items-center justify-center gap-3 mt-12\"\n >\n <PageLink\n href={pageHref(currentPage - 1)}\n label=\"← Previous\"\n disabled={currentPage <= 1}\n />\n <span className=\"text-sm\" style={{ color: 'var(--sk-text-tertiary, #71717a)' }}>\n Page {currentPage} of {totalPages}\n </span>\n <PageLink\n href={pageHref(currentPage + 1)}\n label=\"Next →\"\n disabled={currentPage >= totalPages}\n />\n </nav>\n )}\n </div>\n </section>\n );\n}\n","/**\n * Loading skeletons for portfolio routes — pure CSS pulse, server-safe,\n * zero JS. Drop into `loading.tsx`:\n *\n * ```tsx\n * export { PortfolioIndexSkeleton as default } from '@sonordev/agency-site-kit/portfolio';\n * ```\n */\n\nimport React from 'react';\n\nconst PULSE_CSS = `\n.sk-skeleton{animation:sk-skeleton-pulse 1.6s ease-in-out infinite;background:var(--sk-surface,rgba(255,255,255,0.06));border:1px solid var(--sk-border,rgba(255,255,255,0.08));border-radius:16px}\n@keyframes sk-skeleton-pulse{0%,100%{opacity:.55}50%{opacity:1}}\n@media (prefers-reduced-motion:reduce){.sk-skeleton{animation:none}}\n`;\n\nfunction SkeletonStyles() {\n return (\n <style href=\"sk-skeletons\" precedence=\"default\">\n {PULSE_CSS}\n </style>\n );\n}\n\n/** Grid of pulsing card placeholders for the /work index. */\nexport function PortfolioIndexSkeleton({ cards = 6 }: { cards?: number }) {\n return (\n <section className=\"w-full py-16\">\n <SkeletonStyles />\n <div className=\"max-w-7xl mx-auto px-6\">\n <div className=\"sk-skeleton h-9 w-56 mb-4\" />\n <div className=\"sk-skeleton h-5 w-80 mb-10\" />\n <div className=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6\">\n {Array.from({ length: cards }).map((_, i) => (\n <div key={i} className=\"flex flex-col gap-3\">\n <div className=\"sk-skeleton w-full\" style={{ aspectRatio: '16/10' }} />\n <div className=\"sk-skeleton h-5 w-3/4\" />\n <div className=\"sk-skeleton h-4 w-1/2\" />\n </div>\n ))}\n </div>\n </div>\n </section>\n );\n}\n\n/** Hero-shaped placeholder for /work/[slug] case study pages. */\nexport function PortfolioPageSkeleton() {\n return (\n <article className=\"w-full\">\n <SkeletonStyles />\n <div className=\"max-w-7xl mx-auto px-6 py-24\">\n <div className=\"grid grid-cols-1 lg:grid-cols-2 gap-12 items-start\">\n <div className=\"flex flex-col gap-5\">\n <div className=\"sk-skeleton h-7 w-32\" style={{ borderRadius: '9999px' }} />\n <div className=\"sk-skeleton h-12 w-full\" />\n <div className=\"sk-skeleton h-12 w-4/5\" />\n <div className=\"sk-skeleton h-5 w-full\" />\n <div className=\"sk-skeleton h-5 w-2/3\" />\n </div>\n <div className=\"sk-skeleton w-full\" style={{ aspectRatio: '11/5' }} />\n </div>\n </div>\n </article>\n );\n}\n","/**\n * @sonordev/agency-site-kit — Type definitions\n *\n * These types mirror the Portal API's cms/types.ts definitions for portfolio\n * content. Keep them in sync when modifying the API response shapes.\n */\n\n// ---------------------------------------------------------------------------\n// Brand Configuration\n// ---------------------------------------------------------------------------\n\nexport interface BrandConfig {\n /** Primary brand color (hex) */\n primary: string;\n /** Secondary/accent color (hex) */\n secondary: string;\n /** Background color (hex) */\n background: string;\n /** Elevated background — cards, modals (hex) */\n backgroundElevated: string;\n /** Surface color — interactive containers (hex) */\n surface: string;\n /** Surface hover state (hex) */\n surfaceHover: string;\n /** Surface border color (hex) */\n surfaceBorder: string;\n /** Primary text color (hex) */\n textPrimary: string;\n /** Secondary text color (hex) */\n textSecondary: string;\n /** Tertiary/muted text color (hex) */\n textTertiary: string;\n /** Heading font family */\n fontHeading: string;\n /** Body font family */\n fontBody: string;\n /** Logo URL (absolute or relative) */\n logoUrl?: string;\n /** Logo alt text */\n logoAlt?: string;\n /** Dark mode overrides — partial, merged with base values */\n darkMode?: Partial<Omit<BrandConfig, 'fontHeading' | 'fontBody' | 'logoUrl' | 'logoAlt' | 'darkMode'>>;\n /** Border radius tokens */\n radius?: {\n sm: string;\n md: string;\n lg: string;\n };\n}\n\n// ---------------------------------------------------------------------------\n// Sanity Image Reference\n// ---------------------------------------------------------------------------\n\nexport interface SanityImageRef {\n _type: 'image';\n asset: { _type: 'reference'; _ref: string };\n hotspot?: { x: number; y: number; height: number; width: number };\n crop?: { top: number; bottom: number; left: number; right: number };\n alt?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Metrics\n// ---------------------------------------------------------------------------\n\nexport type MetricSource = 'measured' | 'estimated';\n\nexport interface PortfolioKPI {\n label: string;\n value: number;\n suffix: string;\n prefix?: string;\n description: string;\n source: MetricSource;\n}\n\nexport interface MetricsDelta {\n metric: string;\n baseline: number;\n current: number;\n delta: number;\n deltaPercent: number;\n direction: 'up' | 'down' | 'flat';\n timespan: string;\n}\n\nexport interface MetricsSnapshot {\n seo: {\n clicks_28d?: number;\n impressions_28d?: number;\n avg_position?: number;\n ctr?: number;\n };\n cwv: {\n lcp_ms?: number;\n inp_ms?: number;\n cls?: number;\n performance_score?: number;\n };\n analytics: {\n sessions_28d?: number;\n page_views_28d?: number;\n conversion_rate?: number;\n bounce_rate?: number;\n };\n pages: {\n total_indexed?: number;\n total_crawled?: number;\n total_pages?: number;\n };\n}\n\n// ---------------------------------------------------------------------------\n// Portfolio Section Data Types\n// ---------------------------------------------------------------------------\n\nexport interface PortfolioHeroData {\n headline: string;\n subheadline: string;\n description: string;\n category: string;\n services: string[];\n liveUrl?: string;\n screenshots: {\n desktop?: string;\n tablet?: string;\n mobile?: string;\n };\n kpis: PortfolioKPI[];\n}\n\nexport interface PortfolioChallengeItem {\n title: string;\n description: string;\n solution: string;\n result?: string;\n icon?: string;\n}\n\nexport interface PortfolioChallengesData {\n items: PortfolioChallengeItem[];\n}\n\nexport interface PortfolioStrategyPhase {\n number: number;\n title: string;\n description: string;\n deliverables?: string[];\n icon?: string;\n timeline?: string;\n}\n\nexport interface PortfolioStrategyData {\n phases: PortfolioStrategyPhase[];\n}\n\nexport interface PortfolioResultItem {\n title: string;\n description: string;\n metric?: {\n value: number;\n suffix: string;\n prefix?: string;\n };\n source: MetricSource;\n icon?: string;\n}\n\nexport interface PortfolioResultsData {\n items: PortfolioResultItem[];\n}\n\nexport interface PortfolioTechItem {\n name: string;\n category: string;\n icon?: string;\n description?: string;\n}\n\nexport interface PortfolioTechStackData {\n technologies: PortfolioTechItem[];\n}\n\nexport interface PortfolioServiceItem {\n title: string;\n description: string;\n features: string[];\n icon?: string;\n}\n\nexport interface PortfolioServicesData {\n items: PortfolioServiceItem[];\n}\n\nexport interface PortfolioTestimonialData {\n quote: string;\n author: string;\n title: string;\n company: string;\n avatar?: SanityImageRef;\n rating?: number;\n}\n\nexport interface PortfolioGalleryImage {\n image: SanityImageRef;\n caption?: string;\n type?: 'screenshot' | 'before' | 'after' | 'design';\n}\n\nexport interface PortfolioGalleryData {\n images: PortfolioGalleryImage[];\n layout?: 'grid' | 'masonry';\n}\n\nexport interface PortfolioVideoData {\n url?: string;\n embedUrl?: string;\n thumbnail: SanityImageRef;\n title: string;\n duration?: string;\n platform?: 'youtube' | 'vimeo' | 'custom';\n}\n\nexport interface PortfolioTeamMember {\n name: string;\n role: string;\n avatar?: SanityImageRef;\n}\n\nexport interface PortfolioTeamData {\n members: PortfolioTeamMember[];\n}\n\nexport interface PortfolioAnnotation {\n x: number;\n y: number;\n label: string;\n description: string;\n icon?: string;\n}\n\nexport interface PortfolioFeatureSpotlightData {\n image: SanityImageRef;\n title: string;\n description: string;\n layout: 'left' | 'right' | 'full';\n annotations: PortfolioAnnotation[];\n}\n\nexport interface PortfolioBeforeAfterData {\n before: SanityImageRef;\n after: SanityImageRef;\n title?: string;\n description?: string;\n defaultPosition?: number;\n}\n\nexport interface PortfolioMetricsDataPoint {\n month: string;\n label: string;\n metrics: {\n traffic?: number;\n conversions?: number;\n revenue?: number;\n rankings?: number;\n };\n}\n\nexport interface PortfolioMetricsTimelineData {\n dataPoints: PortfolioMetricsDataPoint[];\n baselineDate: string;\n title?: string;\n description?: string;\n}\n\nexport interface PortfolioFunnelStage {\n label: string;\n value: number;\n icon?: string;\n color?: string;\n}\n\nexport interface PortfolioConversionFunnelData {\n stages: PortfolioFunnelStage[];\n title?: string;\n description?: string;\n}\n\nexport interface PortfolioDetailsData {\n industry: string;\n location?: string;\n website?: string;\n timeline?: string;\n launchDate?: string;\n budgetRange?: string;\n}\n\nexport interface PortfolioSeoData {\n metaTitle: string;\n metaDescription: string;\n keywords: string[];\n ogImage?: SanityImageRef;\n}\n\nexport interface PortfolioCTAData {\n headline: string;\n description: string;\n buttonText: string;\n buttonUrl: string;\n style?: 'primary' | 'glass';\n}\n\n// ---------------------------------------------------------------------------\n// Portfolio Sections\n// ---------------------------------------------------------------------------\n\nexport const PORTFOLIO_SECTION_TYPES = [\n 'portfolioHero',\n 'portfolioChallenges',\n 'portfolioStrategy',\n 'portfolioResults',\n 'portfolioTechStack',\n 'portfolioServices',\n 'portfolioTestimonial',\n 'portfolioGallery',\n 'portfolioVideo',\n 'portfolioTeam',\n 'portfolioFeatureSpotlight',\n 'portfolioBeforeAfter',\n 'portfolioMetricsTimeline',\n 'portfolioConversionFunnel',\n 'portfolioPerformance',\n 'portfolioSpeedComparison',\n 'portfolioSiteArchitecture',\n 'portfolioDesignSystem',\n 'portfolioDetails',\n 'portfolioSeo',\n 'portfolioCTA',\n] as const;\n\nexport type PortfolioSectionType = (typeof PORTFOLIO_SECTION_TYPES)[number];\n\nexport type PortfolioSectionData =\n | PortfolioHeroData\n | PortfolioChallengesData\n | PortfolioStrategyData\n | PortfolioResultsData\n | PortfolioTechStackData\n | PortfolioServicesData\n | PortfolioTestimonialData\n | PortfolioGalleryData\n | PortfolioVideoData\n | PortfolioTeamData\n | PortfolioFeatureSpotlightData\n | PortfolioBeforeAfterData\n | PortfolioMetricsTimelineData\n | PortfolioConversionFunnelData\n | PortfolioDetailsData\n | PortfolioSeoData\n | PortfolioCTAData;\n\nexport interface PortfolioSectionDataMap {\n portfolioHero: PortfolioHeroData;\n portfolioChallenges: PortfolioChallengesData;\n portfolioStrategy: PortfolioStrategyData;\n portfolioResults: PortfolioResultsData;\n portfolioTechStack: PortfolioTechStackData;\n portfolioServices: PortfolioServicesData;\n portfolioTestimonial: PortfolioTestimonialData;\n portfolioGallery: PortfolioGalleryData;\n portfolioVideo: PortfolioVideoData;\n portfolioTeam: PortfolioTeamData;\n portfolioFeatureSpotlight: PortfolioFeatureSpotlightData;\n portfolioBeforeAfter: PortfolioBeforeAfterData;\n portfolioMetricsTimeline: PortfolioMetricsTimelineData;\n portfolioConversionFunnel: PortfolioConversionFunnelData;\n portfolioDetails: PortfolioDetailsData;\n portfolioSeo: PortfolioSeoData;\n portfolioCTA: PortfolioCTAData;\n}\n\nexport interface PortfolioSection<T extends PortfolioSectionType = PortfolioSectionType> {\n sectionType: T;\n displayName: string;\n data: T extends keyof PortfolioSectionDataMap ? PortfolioSectionDataMap[T] : PortfolioSectionData;\n}\n\n// ---------------------------------------------------------------------------\n// Portfolio Item (mirrors public API response)\n// ---------------------------------------------------------------------------\n\nexport interface PortfolioItem {\n id: string;\n slug: string;\n title: string;\n subtitle: string;\n category: string;\n services: string[];\n description: string;\n hero_image: string;\n hero_image_alt: string;\n live_url: string | null;\n kpis: PortfolioKPI[];\n details: PortfolioDetailsData | null;\n seo: PortfolioSeoData | null;\n featured: boolean;\n order?: number;\n published_at: string | null;\n /** Sonor project ID (present for items generated from a managed project) */\n project_id: string | null;\n hero_screenshots: {\n desktop?: string;\n tablet?: string;\n mobile?: string;\n } | null;\n baseline_metrics: MetricsSnapshot | null;\n current_metrics: MetricsSnapshot | null;\n metrics_last_refreshed_at: string | null;\n}\n\nexport interface PortfolioItemFull extends PortfolioItem {\n sections: PortfolioSection[];\n metricsDelta: MetricsDelta[];\n /**\n * True once the agency deliberately reordered sections in the dashboard.\n * PortfolioPage respects the sections array order when set (hero pinned\n * first); otherwise it applies the canonical order.\n */\n sectionsOrdered?: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// API Responses\n// ---------------------------------------------------------------------------\n\nexport interface PortfolioListResponse {\n items: PortfolioItem[];\n total: number;\n limit: number;\n offset: number;\n}\n\nexport interface PortfolioConfigResponse {\n brand: BrandConfig;\n orgName: string;\n orgSlug: string;\n}\n"]}
@@ -20,6 +20,9 @@ async function apiFetch(path, options = {}) {
20
20
  headers,
21
21
  apiKey,
22
22
  apiUrl,
23
+ retry = false,
24
+ maxAttempts,
25
+ requireKey = false,
23
26
  method,
24
27
  body,
25
28
  ...init
@@ -32,8 +35,9 @@ async function apiFetch(path, options = {}) {
32
35
  tags,
33
36
  apiKey,
34
37
  apiUrl,
35
- retry: false,
36
- requireKey: false,
38
+ retry,
39
+ maxAttempts,
40
+ requireKey,
37
41
  logPrefix: LOG_PREFIX,
38
42
  init
39
43
  });
@@ -48,5 +52,5 @@ async function apiGet(path, revalidate, apiKey, apiUrl) {
48
52
  }
49
53
 
50
54
  export { apiFetch, apiGet, getApiConfig };
51
- //# sourceMappingURL=chunk-TVFXN6W6.js.map
52
- //# sourceMappingURL=chunk-TVFXN6W6.js.map
55
+ //# sourceMappingURL=chunk-QL6KVIFR.js.map
56
+ //# sourceMappingURL=chunk-QL6KVIFR.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/shared/api.ts"],"names":[],"mappings":";;;AAyBA,IAAM,kBAAA,GAAqB,IAAA;AAC3B,IAAM,UAAA,GAAa,6BAAA;AASZ,SAAS,YAAA,GAAiC;AAC/C,EAAA,MAAM,SAAS,sBAAA,EAAuB;AAEtC,EAAA,IAAI,CAAC,MAAA,EAAQ;AACX,IAAA,OAAA,CAAQ,IAAA;AAAA,MACN,GAAG,UAAU,CAAA,gFAAA;AAAA,KAEf;AACA,IAAA,OAAO,IAAA;AAAA,EACT;AAEA,EAAA,OAAO,MAAA;AACT;AA0CA,eAAsB,QAAA,CACpB,IAAA,EACA,OAAA,GAA2B,EAAC,EACT;AACnB,EAAA,MAAM;AAAA,IACJ,UAAA,GAAa,kBAAA;AAAA,IACb,IAAA;AAAA,IACA,OAAA;AAAA,IACA,MAAA;AAAA,IACA,MAAA;AAAA,IACA,KAAA,GAAQ,KAAA;AAAA,IACR,WAAA;AAAA,IACA,UAAA,GAAa,KAAA;AAAA,IACb,MAAA;AAAA,IACA,IAAA;AAAA,IACA,GAAG;AAAA,GACL,GAAI,OAAA;AAEJ,EAAA,OAAO,eAAkB,IAAA,EAAM;AAAA,IAC7B,MAAA;AAAA,IACA,MAAM,IAAA,IAAQ,MAAA;AAAA,IACd,OAAA;AAAA,IACA,UAAA;AAAA,IACA,IAAA;AAAA,IACA,MAAA;AAAA,IACA,MAAA;AAAA,IACA,KAAA;AAAA,IACA,WAAA;AAAA,IACA,UAAA;AAAA,IACA,SAAA,EAAW,UAAA;AAAA,IACX;AAAA,GACD,CAAA;AACH;AAUA,eAAsB,MAAA,CACpB,IAAA,EACA,UAAA,EACA,MAAA,EACA,MAAA,EACmB;AACnB,EAAA,OAAO,SAAY,IAAA,EAAM;AAAA,IACvB,MAAA,EAAQ,KAAA;AAAA,IACR,UAAA;AAAA,IACA,MAAA;AAAA,IACA;AAAA,GACD,CAAA;AACH","file":"chunk-QL6KVIFR.js","sourcesContent":["/**\n * @sonordev/agency-site-kit — API fetch utilities\n *\n * Server-side only. Thin adapters over core `@sonordev/site-kit`'s shared server\n * fetcher (`@sonordev/site-kit/server`) — the single source of truth for the\n * Sonor server data plane. This package no longer ships its own `apiFetch` copy;\n * it reuses core's so the two can never drift (monorepo CLAUDE.md,\n * \"Consolidate Duplicated Logic\").\n *\n * Config resolution (SONOR_API_KEY → `x-api-key`, SONOR_API_URL, the legacy\n * UPTRADE_API_KEY fallback) and the `https://api.sonor.io` base all live in core.\n *\n * NOTE: importing this module pulls core's `server-only` guard, so it (and any\n * barrel that re-exports it, including the package root and AgencySiteKitLayout)\n * is server-only. Client components must import from\n * `@sonordev/agency-site-kit/portfolio/client`.\n */\n\nimport {\n resolveServerApiConfig,\n serverApiFetch,\n type ServerApiConfig,\n} from '@sonordev/site-kit/server';\n\n/** Portfolio fetches default to a 1h ISR window (overridable per call). */\nconst DEFAULT_REVALIDATE = 3600;\nconst LOG_PREFIX = '[@sonordev/agency-site-kit]';\n\ntype ApiConfig = ServerApiConfig;\n\n/**\n * Reads API configuration from overrides → environment (delegates to core's\n * `resolveServerApiConfig`). Returns null and warns when no API key is found, so\n * portfolio data degrades gracefully instead of throwing.\n */\nexport function getApiConfig(): ApiConfig | null {\n const config = resolveServerApiConfig();\n\n if (!config) {\n console.warn(\n `${LOG_PREFIX} Missing SONOR_API_KEY environment variable. ` +\n 'Portfolio data will not be fetched.',\n );\n return null;\n }\n\n return config;\n}\n\ninterface ApiFetchOptions extends Omit<RequestInit, 'headers'> {\n /** ISR revalidation interval in seconds. Defaults to 3600. Set to 0 for no caching. */\n revalidate?: number;\n /**\n * Next.js cache tags for on-demand invalidation. Portfolio fetches use\n * PORTFOLIO_CACHE_TAG so the Portal can bust list + detail + sitemap data\n * with a single revalidateTag call (see ./revalidate).\n */\n tags?: string[];\n /** Additional headers to merge with defaults */\n headers?: Record<string, string>;\n /** Override API key (e.g. passed from layout props) */\n apiKey?: string;\n /** Override API URL */\n apiUrl?: string;\n /**\n * Retry transient 429/502/503 + network errors with backoff (core fetcher).\n * Defaults to false to preserve apiFetch's original single-attempt behavior;\n * the portfolio content fetchers opt in.\n */\n retry?: boolean;\n /** Max attempts when retrying (default: core's 6). */\n maxAttempts?: number;\n /** Throw when no API key is configured instead of warn-and-null. */\n requireKey?: boolean;\n}\n\n/**\n * Fetch wrapper for the public portfolio API — delegates to core's\n * `serverApiFetch`.\n *\n * - Injects `x-api-key` automatically\n * - Parses JSON responses\n * - Uses Next.js ISR caching via `next.revalidate` (+ optional cache `tags`)\n * - Returns null on failure (or a missing key) instead of throwing\n *\n * Single-attempt and null-on-failure by default. The portfolio content\n * fetchers (portfolio/server.ts) opt into `retry` + `requireKey` and convert\n * null into a thrown PortfolioApiError — see the error contract there.\n */\nexport async function apiFetch<T>(\n path: string,\n options: ApiFetchOptions = {},\n): Promise<T | null> {\n const {\n revalidate = DEFAULT_REVALIDATE,\n tags,\n headers,\n apiKey,\n apiUrl,\n retry = false,\n maxAttempts,\n requireKey = false,\n method,\n body,\n ...init\n } = options;\n\n return serverApiFetch<T>(path, {\n method,\n body: body ?? undefined,\n headers,\n revalidate,\n tags,\n apiKey,\n apiUrl,\n retry,\n maxAttempts,\n requireKey,\n logPrefix: LOG_PREFIX,\n init: init as RequestInit,\n });\n}\n\n/**\n * GET shorthand for apiFetch.\n *\n * @param path API path (e.g. \"/api/public/portfolio/items\")\n * @param revalidate ISR revalidation in seconds (default 3600)\n * @param apiKey Optional override API key\n * @param apiUrl Optional override API URL\n */\nexport async function apiGet<T>(\n path: string,\n revalidate?: number,\n apiKey?: string,\n apiUrl?: string,\n): Promise<T | null> {\n return apiFetch<T>(path, {\n method: 'GET',\n revalidate,\n apiKey,\n apiUrl,\n });\n}\n"]}
@@ -0,0 +1,165 @@
1
+ import { jsxs, jsx } from 'react/jsx-runtime';
2
+
3
+ // src/portfolio/curate-proof.ts
4
+ var LIGHTHOUSE_RE = /lighthouse/i;
5
+ function isLighthouseKpi(kpi) {
6
+ return LIGHTHOUSE_RE.test(kpi.label);
7
+ }
8
+ function isLighthouseRestatement(item) {
9
+ if (!/score/i.test(item.title)) return false;
10
+ if (LIGHTHOUSE_RE.test(`${item.title} ${item.description}`)) return true;
11
+ return item.metric?.suffix === "/100";
12
+ }
13
+ function curateHeroKpis(kpis) {
14
+ const business = kpis.filter((k) => !isLighthouseKpi(k));
15
+ const lighthouse = kpis.filter(isLighthouseKpi);
16
+ if (lighthouse.length <= 1) return kpis;
17
+ const allPerfect = lighthouse.length >= 4 && lighthouse.every((k) => k.value === 100);
18
+ const summary = allPerfect ? {
19
+ ...lighthouse[0],
20
+ label: "Lighthouse, all 4 categories",
21
+ description: "Perfect scores across performance, SEO, accessibility, and best practices."
22
+ } : lighthouse.find((k) => /performance/i.test(k.label)) ?? lighthouse[0];
23
+ return [...business, summary];
24
+ }
25
+ function curateResultItems(items, hasPerformanceSection) {
26
+ if (!hasPerformanceSection) return items;
27
+ const curated = items.filter((item) => !isLighthouseRestatement(item));
28
+ return curated.length > 0 ? curated : items.slice(0, 1);
29
+ }
30
+ function gateMetricsDeltas(deltas) {
31
+ if (!deltas?.length) return [];
32
+ return deltas.filter((d) => {
33
+ if (d.direction !== "up") return false;
34
+ if (Math.abs(d.deltaPercent) < 10) return false;
35
+ if (/bounce/i.test(d.metric) && d.current > 70) return false;
36
+ return true;
37
+ });
38
+ }
39
+ function curatePortfolioProof(item) {
40
+ const hasPerformanceSection = item.sections.some(
41
+ (s) => s.sectionType === "portfolioPerformance"
42
+ );
43
+ return {
44
+ ...item,
45
+ sections: item.sections.map((section) => {
46
+ if (section.sectionType === "portfolioHero") {
47
+ const data = section.data;
48
+ return { ...section, data: { ...data, kpis: curateHeroKpis(data.kpis ?? []) } };
49
+ }
50
+ if (section.sectionType === "portfolioResults") {
51
+ const data = section.data;
52
+ return {
53
+ ...section,
54
+ data: { ...data, items: curateResultItems(data.items ?? [], hasPerformanceSection) }
55
+ };
56
+ }
57
+ return section;
58
+ })
59
+ };
60
+ }
61
+ var DIRECTION_COLOR = {
62
+ up: "#10b981",
63
+ down: "#ef4444",
64
+ flat: "var(--sk-text-tertiary, #71717a)"
65
+ };
66
+ function formatValue(v) {
67
+ return Number.isInteger(v) ? v.toLocaleString("en-US") : v.toLocaleString("en-US", { maximumFractionDigits: 2 });
68
+ }
69
+ function formatDate(iso) {
70
+ try {
71
+ return new Intl.DateTimeFormat("en-US", {
72
+ year: "numeric",
73
+ month: "long",
74
+ day: "numeric"
75
+ }).format(new Date(iso));
76
+ } catch {
77
+ return iso;
78
+ }
79
+ }
80
+ function MetricsProvenance({ deltas, refreshedAt }) {
81
+ const shown = gateMetricsDeltas(deltas);
82
+ if (!shown.length) return null;
83
+ return /* @__PURE__ */ jsxs(
84
+ "div",
85
+ {
86
+ className: "mt-10 rounded-2xl overflow-hidden",
87
+ style: {
88
+ background: "var(--sk-surface, rgba(255,255,255,0.05))",
89
+ border: "1px solid var(--sk-border, rgba(255,255,255,0.1))"
90
+ },
91
+ children: [
92
+ /* @__PURE__ */ jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3", children: shown.map((d, i) => {
93
+ const color = DIRECTION_COLOR[d.direction] ?? DIRECTION_COLOR.flat;
94
+ const arrow = d.current === d.baseline ? "\u2014" : d.current > d.baseline ? "\u25B2" : "\u25BC";
95
+ return /* @__PURE__ */ jsxs(
96
+ "div",
97
+ {
98
+ className: "flex flex-col gap-1 p-5",
99
+ style: { borderBottom: "1px solid var(--sk-border, rgba(255,255,255,0.06))" },
100
+ children: [
101
+ /* @__PURE__ */ jsx(
102
+ "span",
103
+ {
104
+ className: "text-xs font-semibold uppercase tracking-wider",
105
+ style: { color: "var(--sk-text-tertiary, #71717a)" },
106
+ children: d.metric
107
+ }
108
+ ),
109
+ /* @__PURE__ */ jsxs("span", { className: "flex items-baseline gap-2", children: [
110
+ /* @__PURE__ */ jsxs(
111
+ "span",
112
+ {
113
+ className: "text-lg font-bold",
114
+ style: { color: "var(--sk-text-primary, #ffffff)" },
115
+ children: [
116
+ formatValue(d.baseline),
117
+ " \u2192 ",
118
+ formatValue(d.current)
119
+ ]
120
+ }
121
+ ),
122
+ /* @__PURE__ */ jsxs("span", { className: "text-sm font-semibold", style: { color }, children: [
123
+ arrow,
124
+ " ",
125
+ Math.abs(d.deltaPercent),
126
+ "%"
127
+ ] })
128
+ ] }),
129
+ /* @__PURE__ */ jsx("span", { className: "text-xs", style: { color: "var(--sk-text-tertiary, #71717a)" }, children: d.timespan })
130
+ ]
131
+ },
132
+ `${d.metric}-${i}`
133
+ );
134
+ }) }),
135
+ /* @__PURE__ */ jsxs(
136
+ "div",
137
+ {
138
+ className: "flex items-center gap-2 px-5 py-3",
139
+ style: {
140
+ background: "color-mix(in srgb, #10b981 6%, transparent)",
141
+ color: "var(--sk-text-secondary, #a1a1aa)"
142
+ },
143
+ children: [
144
+ /* @__PURE__ */ jsx(
145
+ "span",
146
+ {
147
+ className: "inline-block w-2 h-2 rounded-full shrink-0",
148
+ style: { background: "#10b981" }
149
+ }
150
+ ),
151
+ /* @__PURE__ */ jsxs("span", { className: "text-xs font-medium", children: [
152
+ "Verified from live analytics",
153
+ refreshedAt ? ` \xB7 Refreshed ${formatDate(refreshedAt)}` : ""
154
+ ] })
155
+ ]
156
+ }
157
+ )
158
+ ]
159
+ }
160
+ );
161
+ }
162
+
163
+ export { MetricsProvenance, curateHeroKpis, curatePortfolioProof, curateResultItems, gateMetricsDeltas };
164
+ //# sourceMappingURL=chunk-RWQ675N6.js.map
165
+ //# sourceMappingURL=chunk-RWQ675N6.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/portfolio/curate-proof.ts","../src/portfolio/components/MetricsProvenance.tsx"],"names":[],"mappings":"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","file":"chunk-RWQ675N6.js","sourcesContent":["/**\n * curate-proof — single source of truth for which proof points a public case\n * study is allowed to display, and in what shape.\n *\n * Two classes of problem this guards against (both shipped to production on\n * upforge.io before this existed):\n *\n * 1. Repetition: generated items arrive with four \"100/100 Lighthouse X\"\n * KPIs in the hero, the same four restated as Results cards, AND a full\n * Performance section with score gauges. One differentiator repeated\n * twelve times per page stops differentiating. The Performance section is\n * the one home for the Lighthouse breakdown; hero/results/cards collapse\n * to at most one summary tile.\n *\n * 2. Self-harming live metrics: MetricsDelta rows are computed from raw\n * analytics baselines with no editorial judgment — a bounce rate of\n * \"100 → 99.69\" rendered as a green win. Marketing surfaces only show a\n * delta when it's a genuinely flattering, meaningful story.\n *\n * Used by PortfolioPage (hero KPIs + results items), PortfolioCard (KPI\n * preview), and MetricsProvenance (delta gate). Change display policy here,\n * nowhere else.\n */\n\nimport type {\n MetricsDelta,\n PortfolioHeroData,\n PortfolioItemFull,\n PortfolioKPI,\n PortfolioResultItem,\n PortfolioResultsData,\n} from '../types';\n\nconst LIGHTHOUSE_RE = /lighthouse/i;\n\n/** True when a KPI tile is a per-category Lighthouse score. */\nexport function isLighthouseKpi(kpi: PortfolioKPI): boolean {\n return LIGHTHOUSE_RE.test(kpi.label);\n}\n\n/**\n * True when a Results card merely restates a Lighthouse category score\n * (e.g. \"Perfect performance score … measured Lighthouse score of 100/100\").\n * Business outcomes (\"Complete 14-page rebuild\", \"682% session growth\")\n * never match: the title must be about a score AND the card must reference\n * Lighthouse or carry a x/100 metric.\n */\nexport function isLighthouseRestatement(item: PortfolioResultItem): boolean {\n if (!/score/i.test(item.title)) return false;\n if (LIGHTHOUSE_RE.test(`${item.title} ${item.description}`)) return true;\n return item.metric?.suffix === '/100';\n}\n\n/**\n * Collapse per-category Lighthouse KPIs to at most one summary tile, business\n * outcomes first. When every category is present at 100 the summary says so;\n * otherwise the Performance category (the one buyers know) represents the set.\n */\nexport function curateHeroKpis(kpis: PortfolioKPI[]): PortfolioKPI[] {\n const business = kpis.filter((k) => !isLighthouseKpi(k));\n const lighthouse = kpis.filter(isLighthouseKpi);\n if (lighthouse.length <= 1) return kpis;\n\n const allPerfect = lighthouse.length >= 4 && lighthouse.every((k) => k.value === 100);\n const summary: PortfolioKPI = allPerfect\n ? {\n ...lighthouse[0],\n label: 'Lighthouse, all 4 categories',\n description: 'Perfect scores across performance, SEO, accessibility, and best practices.',\n }\n : (lighthouse.find((k) => /performance/i.test(k.label)) ?? lighthouse[0]);\n\n return [...business, summary];\n}\n\n/**\n * Drop Results cards that restate Lighthouse scores when the case study also\n * renders a Performance section (the score breakdown's single home).\n */\nexport function curateResultItems(\n items: PortfolioResultItem[],\n hasPerformanceSection: boolean,\n): PortfolioResultItem[] {\n if (!hasPerformanceSection) return items;\n const curated = items.filter((item) => !isLighthouseRestatement(item));\n // Never curate a Results section into emptiness — if every card was a\n // Lighthouse restatement, keep the strongest single card instead.\n return curated.length > 0 ? curated : items.slice(0, 1);\n}\n\n/**\n * Editorial gate for live metric deltas (\"Verified from live analytics\").\n * `direction` comes from the platform with 'up' meaning IMPROVED (lower-is-\n * better metrics are inverted server-side). A public case study only shows a\n * delta when it's a win worth telling:\n * - improved ('up'), never a regression or flat noise\n * - moved by a double-digit relative margin (small drift isn't a story)\n * - bounce-rate-style metrics must also be respectable in absolute terms —\n * \"100 → 99.69\" is technically an improvement and still self-harm\n */\nexport function gateMetricsDeltas(deltas: MetricsDelta[] | undefined | null): MetricsDelta[] {\n if (!deltas?.length) return [];\n return deltas.filter((d) => {\n if (d.direction !== 'up') return false;\n if (Math.abs(d.deltaPercent) < 10) return false;\n if (/bounce/i.test(d.metric) && d.current > 70) return false;\n return true;\n });\n}\n\n/**\n * Apply the full display policy to a portfolio item before rendering.\n * Pure — returns a new item; the fetched payload is never mutated.\n */\nexport function curatePortfolioProof(item: PortfolioItemFull): PortfolioItemFull {\n const hasPerformanceSection = item.sections.some(\n (s) => s.sectionType === 'portfolioPerformance',\n );\n\n return {\n ...item,\n sections: item.sections.map((section) => {\n if (section.sectionType === 'portfolioHero') {\n const data = section.data as PortfolioHeroData;\n return { ...section, data: { ...data, kpis: curateHeroKpis(data.kpis ?? []) } };\n }\n if (section.sectionType === 'portfolioResults') {\n const data = section.data as PortfolioResultsData;\n return {\n ...section,\n data: { ...data, items: curateResultItems(data.items ?? [], hasPerformanceSection) },\n };\n }\n return section;\n }),\n };\n}\n","/**\n * MetricsProvenance — \"these numbers are real\" strip for case studies.\n *\n * Server component, zero JS. Renders the live metric deltas the platform\n * computes from GSC/analytics baselines (MetricsDelta[]) plus the refresh\n * timestamp — the trust signal a static agency portfolio can't fake.\n *\n * `direction` semantics come from the API: 'up' ALWAYS means \"improved\"\n * (lower-is-better metrics like Avg Position/LCP are inverted server-side\n * in metrics-baseline.service), so 'up' renders green. The ARROW glyph\n * follows the raw numbers instead — an improved bounce rate shows a green ▼,\n * never a green ▲ next to a value that visibly went down.\n *\n * Deltas pass through gateMetricsDeltas (curate-proof.ts) before display:\n * raw platform deltas carry no editorial judgment, and this strip only\n * exists to flatter — see that module for the policy.\n */\n\nimport React from 'react';\nimport type { MetricsDelta } from '../../types';\nimport { gateMetricsDeltas } from '../curate-proof';\n\ninterface MetricsProvenanceProps {\n deltas: MetricsDelta[];\n /** ISO timestamp of the last live-metrics refresh. */\n refreshedAt?: string | null;\n}\n\nconst DIRECTION_COLOR: Record<MetricsDelta['direction'], string> = {\n up: '#10b981',\n down: '#ef4444',\n flat: 'var(--sk-text-tertiary, #71717a)',\n};\n\nfunction formatValue(v: number): string {\n return Number.isInteger(v)\n ? v.toLocaleString('en-US')\n : v.toLocaleString('en-US', { maximumFractionDigits: 2 });\n}\n\nfunction formatDate(iso: string): string {\n try {\n return new Intl.DateTimeFormat('en-US', {\n year: 'numeric',\n month: 'long',\n day: 'numeric',\n }).format(new Date(iso));\n } catch {\n return iso;\n }\n}\n\nexport default function MetricsProvenance({ deltas, refreshedAt }: MetricsProvenanceProps) {\n const shown = gateMetricsDeltas(deltas);\n if (!shown.length) return null;\n\n return (\n <div\n className=\"mt-10 rounded-2xl overflow-hidden\"\n style={{\n background: 'var(--sk-surface, rgba(255,255,255,0.05))',\n border: '1px solid var(--sk-border, rgba(255,255,255,0.1))',\n }}\n >\n <div className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3\">\n {shown.map((d, i) => {\n const color = DIRECTION_COLOR[d.direction] ?? DIRECTION_COLOR.flat;\n // Arrow tracks the raw numbers; color tracks improved/worsened.\n const arrow = d.current === d.baseline ? '—' : d.current > d.baseline ? '▲' : '▼';\n return (\n <div\n key={`${d.metric}-${i}`}\n className=\"flex flex-col gap-1 p-5\"\n style={{ borderBottom: '1px solid var(--sk-border, rgba(255,255,255,0.06))' }}\n >\n <span\n className=\"text-xs font-semibold uppercase tracking-wider\"\n style={{ color: 'var(--sk-text-tertiary, #71717a)' }}\n >\n {d.metric}\n </span>\n <span className=\"flex items-baseline gap-2\">\n <span\n className=\"text-lg font-bold\"\n style={{ color: 'var(--sk-text-primary, #ffffff)' }}\n >\n {formatValue(d.baseline)} → {formatValue(d.current)}\n </span>\n <span className=\"text-sm font-semibold\" style={{ color }}>\n {arrow} {Math.abs(d.deltaPercent)}%\n </span>\n </span>\n <span className=\"text-xs\" style={{ color: 'var(--sk-text-tertiary, #71717a)' }}>\n {d.timespan}\n </span>\n </div>\n );\n })}\n </div>\n\n <div\n className=\"flex items-center gap-2 px-5 py-3\"\n style={{\n background: 'color-mix(in srgb, #10b981 6%, transparent)',\n color: 'var(--sk-text-secondary, #a1a1aa)',\n }}\n >\n <span\n className=\"inline-block w-2 h-2 rounded-full shrink-0\"\n style={{ background: '#10b981' }}\n />\n <span className=\"text-xs font-medium\">\n Verified from live analytics\n {refreshedAt ? ` · Refreshed ${formatDate(refreshedAt)}` : ''}\n </span>\n </div>\n </div>\n );\n}\n"]}
@@ -0,0 +1,171 @@
1
+ 'use strict';
2
+
3
+ var jsxRuntime = require('react/jsx-runtime');
4
+
5
+ // src/portfolio/curate-proof.ts
6
+ var LIGHTHOUSE_RE = /lighthouse/i;
7
+ function isLighthouseKpi(kpi) {
8
+ return LIGHTHOUSE_RE.test(kpi.label);
9
+ }
10
+ function isLighthouseRestatement(item) {
11
+ if (!/score/i.test(item.title)) return false;
12
+ if (LIGHTHOUSE_RE.test(`${item.title} ${item.description}`)) return true;
13
+ return item.metric?.suffix === "/100";
14
+ }
15
+ function curateHeroKpis(kpis) {
16
+ const business = kpis.filter((k) => !isLighthouseKpi(k));
17
+ const lighthouse = kpis.filter(isLighthouseKpi);
18
+ if (lighthouse.length <= 1) return kpis;
19
+ const allPerfect = lighthouse.length >= 4 && lighthouse.every((k) => k.value === 100);
20
+ const summary = allPerfect ? {
21
+ ...lighthouse[0],
22
+ label: "Lighthouse, all 4 categories",
23
+ description: "Perfect scores across performance, SEO, accessibility, and best practices."
24
+ } : lighthouse.find((k) => /performance/i.test(k.label)) ?? lighthouse[0];
25
+ return [...business, summary];
26
+ }
27
+ function curateResultItems(items, hasPerformanceSection) {
28
+ if (!hasPerformanceSection) return items;
29
+ const curated = items.filter((item) => !isLighthouseRestatement(item));
30
+ return curated.length > 0 ? curated : items.slice(0, 1);
31
+ }
32
+ function gateMetricsDeltas(deltas) {
33
+ if (!deltas?.length) return [];
34
+ return deltas.filter((d) => {
35
+ if (d.direction !== "up") return false;
36
+ if (Math.abs(d.deltaPercent) < 10) return false;
37
+ if (/bounce/i.test(d.metric) && d.current > 70) return false;
38
+ return true;
39
+ });
40
+ }
41
+ function curatePortfolioProof(item) {
42
+ const hasPerformanceSection = item.sections.some(
43
+ (s) => s.sectionType === "portfolioPerformance"
44
+ );
45
+ return {
46
+ ...item,
47
+ sections: item.sections.map((section) => {
48
+ if (section.sectionType === "portfolioHero") {
49
+ const data = section.data;
50
+ return { ...section, data: { ...data, kpis: curateHeroKpis(data.kpis ?? []) } };
51
+ }
52
+ if (section.sectionType === "portfolioResults") {
53
+ const data = section.data;
54
+ return {
55
+ ...section,
56
+ data: { ...data, items: curateResultItems(data.items ?? [], hasPerformanceSection) }
57
+ };
58
+ }
59
+ return section;
60
+ })
61
+ };
62
+ }
63
+ var DIRECTION_COLOR = {
64
+ up: "#10b981",
65
+ down: "#ef4444",
66
+ flat: "var(--sk-text-tertiary, #71717a)"
67
+ };
68
+ function formatValue(v) {
69
+ return Number.isInteger(v) ? v.toLocaleString("en-US") : v.toLocaleString("en-US", { maximumFractionDigits: 2 });
70
+ }
71
+ function formatDate(iso) {
72
+ try {
73
+ return new Intl.DateTimeFormat("en-US", {
74
+ year: "numeric",
75
+ month: "long",
76
+ day: "numeric"
77
+ }).format(new Date(iso));
78
+ } catch {
79
+ return iso;
80
+ }
81
+ }
82
+ function MetricsProvenance({ deltas, refreshedAt }) {
83
+ const shown = gateMetricsDeltas(deltas);
84
+ if (!shown.length) return null;
85
+ return /* @__PURE__ */ jsxRuntime.jsxs(
86
+ "div",
87
+ {
88
+ className: "mt-10 rounded-2xl overflow-hidden",
89
+ style: {
90
+ background: "var(--sk-surface, rgba(255,255,255,0.05))",
91
+ border: "1px solid var(--sk-border, rgba(255,255,255,0.1))"
92
+ },
93
+ children: [
94
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3", children: shown.map((d, i) => {
95
+ const color = DIRECTION_COLOR[d.direction] ?? DIRECTION_COLOR.flat;
96
+ const arrow = d.current === d.baseline ? "\u2014" : d.current > d.baseline ? "\u25B2" : "\u25BC";
97
+ return /* @__PURE__ */ jsxRuntime.jsxs(
98
+ "div",
99
+ {
100
+ className: "flex flex-col gap-1 p-5",
101
+ style: { borderBottom: "1px solid var(--sk-border, rgba(255,255,255,0.06))" },
102
+ children: [
103
+ /* @__PURE__ */ jsxRuntime.jsx(
104
+ "span",
105
+ {
106
+ className: "text-xs font-semibold uppercase tracking-wider",
107
+ style: { color: "var(--sk-text-tertiary, #71717a)" },
108
+ children: d.metric
109
+ }
110
+ ),
111
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-baseline gap-2", children: [
112
+ /* @__PURE__ */ jsxRuntime.jsxs(
113
+ "span",
114
+ {
115
+ className: "text-lg font-bold",
116
+ style: { color: "var(--sk-text-primary, #ffffff)" },
117
+ children: [
118
+ formatValue(d.baseline),
119
+ " \u2192 ",
120
+ formatValue(d.current)
121
+ ]
122
+ }
123
+ ),
124
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-sm font-semibold", style: { color }, children: [
125
+ arrow,
126
+ " ",
127
+ Math.abs(d.deltaPercent),
128
+ "%"
129
+ ] })
130
+ ] }),
131
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs", style: { color: "var(--sk-text-tertiary, #71717a)" }, children: d.timespan })
132
+ ]
133
+ },
134
+ `${d.metric}-${i}`
135
+ );
136
+ }) }),
137
+ /* @__PURE__ */ jsxRuntime.jsxs(
138
+ "div",
139
+ {
140
+ className: "flex items-center gap-2 px-5 py-3",
141
+ style: {
142
+ background: "color-mix(in srgb, #10b981 6%, transparent)",
143
+ color: "var(--sk-text-secondary, #a1a1aa)"
144
+ },
145
+ children: [
146
+ /* @__PURE__ */ jsxRuntime.jsx(
147
+ "span",
148
+ {
149
+ className: "inline-block w-2 h-2 rounded-full shrink-0",
150
+ style: { background: "#10b981" }
151
+ }
152
+ ),
153
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-xs font-medium", children: [
154
+ "Verified from live analytics",
155
+ refreshedAt ? ` \xB7 Refreshed ${formatDate(refreshedAt)}` : ""
156
+ ] })
157
+ ]
158
+ }
159
+ )
160
+ ]
161
+ }
162
+ );
163
+ }
164
+
165
+ exports.MetricsProvenance = MetricsProvenance;
166
+ exports.curateHeroKpis = curateHeroKpis;
167
+ exports.curatePortfolioProof = curatePortfolioProof;
168
+ exports.curateResultItems = curateResultItems;
169
+ exports.gateMetricsDeltas = gateMetricsDeltas;
170
+ //# sourceMappingURL=chunk-UXUU7FVT.cjs.map
171
+ //# sourceMappingURL=chunk-UXUU7FVT.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/portfolio/curate-proof.ts","../src/portfolio/components/MetricsProvenance.tsx"],"names":["jsxs","jsx"],"mappings":"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","file":"chunk-UXUU7FVT.cjs","sourcesContent":["/**\n * curate-proof — single source of truth for which proof points a public case\n * study is allowed to display, and in what shape.\n *\n * Two classes of problem this guards against (both shipped to production on\n * upforge.io before this existed):\n *\n * 1. Repetition: generated items arrive with four \"100/100 Lighthouse X\"\n * KPIs in the hero, the same four restated as Results cards, AND a full\n * Performance section with score gauges. One differentiator repeated\n * twelve times per page stops differentiating. The Performance section is\n * the one home for the Lighthouse breakdown; hero/results/cards collapse\n * to at most one summary tile.\n *\n * 2. Self-harming live metrics: MetricsDelta rows are computed from raw\n * analytics baselines with no editorial judgment — a bounce rate of\n * \"100 → 99.69\" rendered as a green win. Marketing surfaces only show a\n * delta when it's a genuinely flattering, meaningful story.\n *\n * Used by PortfolioPage (hero KPIs + results items), PortfolioCard (KPI\n * preview), and MetricsProvenance (delta gate). Change display policy here,\n * nowhere else.\n */\n\nimport type {\n MetricsDelta,\n PortfolioHeroData,\n PortfolioItemFull,\n PortfolioKPI,\n PortfolioResultItem,\n PortfolioResultsData,\n} from '../types';\n\nconst LIGHTHOUSE_RE = /lighthouse/i;\n\n/** True when a KPI tile is a per-category Lighthouse score. */\nexport function isLighthouseKpi(kpi: PortfolioKPI): boolean {\n return LIGHTHOUSE_RE.test(kpi.label);\n}\n\n/**\n * True when a Results card merely restates a Lighthouse category score\n * (e.g. \"Perfect performance score … measured Lighthouse score of 100/100\").\n * Business outcomes (\"Complete 14-page rebuild\", \"682% session growth\")\n * never match: the title must be about a score AND the card must reference\n * Lighthouse or carry a x/100 metric.\n */\nexport function isLighthouseRestatement(item: PortfolioResultItem): boolean {\n if (!/score/i.test(item.title)) return false;\n if (LIGHTHOUSE_RE.test(`${item.title} ${item.description}`)) return true;\n return item.metric?.suffix === '/100';\n}\n\n/**\n * Collapse per-category Lighthouse KPIs to at most one summary tile, business\n * outcomes first. When every category is present at 100 the summary says so;\n * otherwise the Performance category (the one buyers know) represents the set.\n */\nexport function curateHeroKpis(kpis: PortfolioKPI[]): PortfolioKPI[] {\n const business = kpis.filter((k) => !isLighthouseKpi(k));\n const lighthouse = kpis.filter(isLighthouseKpi);\n if (lighthouse.length <= 1) return kpis;\n\n const allPerfect = lighthouse.length >= 4 && lighthouse.every((k) => k.value === 100);\n const summary: PortfolioKPI = allPerfect\n ? {\n ...lighthouse[0],\n label: 'Lighthouse, all 4 categories',\n description: 'Perfect scores across performance, SEO, accessibility, and best practices.',\n }\n : (lighthouse.find((k) => /performance/i.test(k.label)) ?? lighthouse[0]);\n\n return [...business, summary];\n}\n\n/**\n * Drop Results cards that restate Lighthouse scores when the case study also\n * renders a Performance section (the score breakdown's single home).\n */\nexport function curateResultItems(\n items: PortfolioResultItem[],\n hasPerformanceSection: boolean,\n): PortfolioResultItem[] {\n if (!hasPerformanceSection) return items;\n const curated = items.filter((item) => !isLighthouseRestatement(item));\n // Never curate a Results section into emptiness — if every card was a\n // Lighthouse restatement, keep the strongest single card instead.\n return curated.length > 0 ? curated : items.slice(0, 1);\n}\n\n/**\n * Editorial gate for live metric deltas (\"Verified from live analytics\").\n * `direction` comes from the platform with 'up' meaning IMPROVED (lower-is-\n * better metrics are inverted server-side). A public case study only shows a\n * delta when it's a win worth telling:\n * - improved ('up'), never a regression or flat noise\n * - moved by a double-digit relative margin (small drift isn't a story)\n * - bounce-rate-style metrics must also be respectable in absolute terms —\n * \"100 → 99.69\" is technically an improvement and still self-harm\n */\nexport function gateMetricsDeltas(deltas: MetricsDelta[] | undefined | null): MetricsDelta[] {\n if (!deltas?.length) return [];\n return deltas.filter((d) => {\n if (d.direction !== 'up') return false;\n if (Math.abs(d.deltaPercent) < 10) return false;\n if (/bounce/i.test(d.metric) && d.current > 70) return false;\n return true;\n });\n}\n\n/**\n * Apply the full display policy to a portfolio item before rendering.\n * Pure — returns a new item; the fetched payload is never mutated.\n */\nexport function curatePortfolioProof(item: PortfolioItemFull): PortfolioItemFull {\n const hasPerformanceSection = item.sections.some(\n (s) => s.sectionType === 'portfolioPerformance',\n );\n\n return {\n ...item,\n sections: item.sections.map((section) => {\n if (section.sectionType === 'portfolioHero') {\n const data = section.data as PortfolioHeroData;\n return { ...section, data: { ...data, kpis: curateHeroKpis(data.kpis ?? []) } };\n }\n if (section.sectionType === 'portfolioResults') {\n const data = section.data as PortfolioResultsData;\n return {\n ...section,\n data: { ...data, items: curateResultItems(data.items ?? [], hasPerformanceSection) },\n };\n }\n return section;\n }),\n };\n}\n","/**\n * MetricsProvenance — \"these numbers are real\" strip for case studies.\n *\n * Server component, zero JS. Renders the live metric deltas the platform\n * computes from GSC/analytics baselines (MetricsDelta[]) plus the refresh\n * timestamp — the trust signal a static agency portfolio can't fake.\n *\n * `direction` semantics come from the API: 'up' ALWAYS means \"improved\"\n * (lower-is-better metrics like Avg Position/LCP are inverted server-side\n * in metrics-baseline.service), so 'up' renders green. The ARROW glyph\n * follows the raw numbers instead — an improved bounce rate shows a green ▼,\n * never a green ▲ next to a value that visibly went down.\n *\n * Deltas pass through gateMetricsDeltas (curate-proof.ts) before display:\n * raw platform deltas carry no editorial judgment, and this strip only\n * exists to flatter — see that module for the policy.\n */\n\nimport React from 'react';\nimport type { MetricsDelta } from '../../types';\nimport { gateMetricsDeltas } from '../curate-proof';\n\ninterface MetricsProvenanceProps {\n deltas: MetricsDelta[];\n /** ISO timestamp of the last live-metrics refresh. */\n refreshedAt?: string | null;\n}\n\nconst DIRECTION_COLOR: Record<MetricsDelta['direction'], string> = {\n up: '#10b981',\n down: '#ef4444',\n flat: 'var(--sk-text-tertiary, #71717a)',\n};\n\nfunction formatValue(v: number): string {\n return Number.isInteger(v)\n ? v.toLocaleString('en-US')\n : v.toLocaleString('en-US', { maximumFractionDigits: 2 });\n}\n\nfunction formatDate(iso: string): string {\n try {\n return new Intl.DateTimeFormat('en-US', {\n year: 'numeric',\n month: 'long',\n day: 'numeric',\n }).format(new Date(iso));\n } catch {\n return iso;\n }\n}\n\nexport default function MetricsProvenance({ deltas, refreshedAt }: MetricsProvenanceProps) {\n const shown = gateMetricsDeltas(deltas);\n if (!shown.length) return null;\n\n return (\n <div\n className=\"mt-10 rounded-2xl overflow-hidden\"\n style={{\n background: 'var(--sk-surface, rgba(255,255,255,0.05))',\n border: '1px solid var(--sk-border, rgba(255,255,255,0.1))',\n }}\n >\n <div className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3\">\n {shown.map((d, i) => {\n const color = DIRECTION_COLOR[d.direction] ?? DIRECTION_COLOR.flat;\n // Arrow tracks the raw numbers; color tracks improved/worsened.\n const arrow = d.current === d.baseline ? '—' : d.current > d.baseline ? '▲' : '▼';\n return (\n <div\n key={`${d.metric}-${i}`}\n className=\"flex flex-col gap-1 p-5\"\n style={{ borderBottom: '1px solid var(--sk-border, rgba(255,255,255,0.06))' }}\n >\n <span\n className=\"text-xs font-semibold uppercase tracking-wider\"\n style={{ color: 'var(--sk-text-tertiary, #71717a)' }}\n >\n {d.metric}\n </span>\n <span className=\"flex items-baseline gap-2\">\n <span\n className=\"text-lg font-bold\"\n style={{ color: 'var(--sk-text-primary, #ffffff)' }}\n >\n {formatValue(d.baseline)} → {formatValue(d.current)}\n </span>\n <span className=\"text-sm font-semibold\" style={{ color }}>\n {arrow} {Math.abs(d.deltaPercent)}%\n </span>\n </span>\n <span className=\"text-xs\" style={{ color: 'var(--sk-text-tertiary, #71717a)' }}>\n {d.timespan}\n </span>\n </div>\n );\n })}\n </div>\n\n <div\n className=\"flex items-center gap-2 px-5 py-3\"\n style={{\n background: 'color-mix(in srgb, #10b981 6%, transparent)',\n color: 'var(--sk-text-secondary, #a1a1aa)',\n }}\n >\n <span\n className=\"inline-block w-2 h-2 rounded-full shrink-0\"\n style={{ background: '#10b981' }}\n />\n <span className=\"text-xs font-medium\">\n Verified from live analytics\n {refreshedAt ? ` · Refreshed ${formatDate(refreshedAt)}` : ''}\n </span>\n </div>\n </div>\n );\n}\n"]}
@@ -0,0 +1,30 @@
1
+ // src/shared/images.ts
2
+ var SANITY_ASSET_REF = /^image-[A-Za-z0-9]+-\d+x\d+-[a-z0-9]+$/;
3
+ var FILENAME_DIMS = /-(\d{2,5})x(\d{2,5})\.[a-z0-9]+(?:\?.*)?$/i;
4
+ var warnedAboutRefs = false;
5
+ function resolvePortfolioImage(input) {
6
+ const raw = typeof input === "string" ? input : input?.asset?._ref ?? "";
7
+ const value = (raw || "").trim();
8
+ if (!value) return null;
9
+ if (SANITY_ASSET_REF.test(value)) {
10
+ if (!warnedAboutRefs && typeof console !== "undefined") {
11
+ warnedAboutRefs = true;
12
+ console.warn(
13
+ "[@sonordev/agency-site-kit] Received a raw Sanity asset ref instead of an image URL \u2014 skipping render. The Sonor API should resolve image refs to cdn.sanity.io URLs before returning portfolio sections."
14
+ );
15
+ }
16
+ return null;
17
+ }
18
+ if (!/^https?:\/\//i.test(value) && !value.startsWith("/")) return null;
19
+ const dims = value.match(FILENAME_DIMS);
20
+ if (dims) {
21
+ const width = parseInt(dims[1], 10);
22
+ const height = parseInt(dims[2], 10);
23
+ if (width > 0 && height > 0) return { url: value, width, height };
24
+ }
25
+ return { url: value };
26
+ }
27
+
28
+ export { resolvePortfolioImage };
29
+ //# sourceMappingURL=chunk-XJ434N7I.js.map
30
+ //# sourceMappingURL=chunk-XJ434N7I.js.map