@sonordev/agency-site-kit 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +302 -0
- package/bin/setup.mjs +79 -0
- package/dist/{chunk-RXBK2EOE.js → chunk-4IES23GV.js} +3 -3
- package/dist/{chunk-RXBK2EOE.js.map → chunk-4IES23GV.js.map} +1 -1
- package/dist/chunk-4N7ENJ7L.cjs +108 -0
- package/dist/chunk-4N7ENJ7L.cjs.map +1 -0
- package/dist/{chunk-WNR4KNG4.cjs → chunk-5MEZU3OG.cjs} +4 -4
- package/dist/{chunk-WNR4KNG4.cjs.map → chunk-5MEZU3OG.cjs.map} +1 -1
- package/dist/chunk-7ZHRMWJU.cjs +306 -0
- package/dist/chunk-7ZHRMWJU.cjs.map +1 -0
- package/dist/chunk-CVT7I5BQ.js +71 -0
- package/dist/chunk-CVT7I5BQ.js.map +1 -0
- package/dist/chunk-HQGAX2LI.cjs +8 -0
- package/dist/chunk-HQGAX2LI.cjs.map +1 -0
- package/dist/chunk-KXLROAO5.js +291 -0
- package/dist/chunk-KXLROAO5.js.map +1 -0
- package/dist/chunk-L4EOQVRJ.js +6 -0
- package/dist/chunk-L4EOQVRJ.js.map +1 -0
- package/dist/chunk-L7E3UHQC.cjs +535 -0
- package/dist/chunk-L7E3UHQC.cjs.map +1 -0
- package/dist/chunk-NI4FVCLS.cjs +32 -0
- package/dist/chunk-NI4FVCLS.cjs.map +1 -0
- package/dist/chunk-NIZJLONK.js +106 -0
- package/dist/chunk-NIZJLONK.js.map +1 -0
- package/dist/chunk-QDXZ27R7.js +33 -0
- package/dist/chunk-QDXZ27R7.js.map +1 -0
- package/dist/{chunk-TVFXN6W6.js → chunk-QL6KVIFR.js} +8 -4
- package/dist/chunk-QL6KVIFR.js.map +1 -0
- package/dist/chunk-UNTOEWQL.js +504 -0
- package/dist/chunk-UNTOEWQL.js.map +1 -0
- package/dist/chunk-XJ434N7I.js +30 -0
- package/dist/chunk-XJ434N7I.js.map +1 -0
- package/dist/chunk-XLK77USA.js +45 -0
- package/dist/chunk-XLK77USA.js.map +1 -0
- package/dist/{chunk-EE75WRCM.cjs → chunk-XOWSXZYW.cjs} +8 -4
- package/dist/chunk-XOWSXZYW.cjs.map +1 -0
- package/dist/chunk-ZINKVJQU.js +67 -0
- package/dist/chunk-ZINKVJQU.js.map +1 -0
- package/dist/index.cjs +29 -31
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +17 -9
- package/dist/index.d.ts +17 -9
- package/dist/index.js +7 -9
- package/dist/index.js.map +1 -1
- package/dist/layout/index.cjs +3 -3
- package/dist/layout/index.d.cts +1 -1
- package/dist/layout/index.d.ts +1 -1
- package/dist/layout/index.js +2 -2
- package/dist/portfolio/client.cjs +342 -12
- package/dist/portfolio/client.cjs.map +1 -1
- package/dist/portfolio/client.d.cts +11 -88
- package/dist/portfolio/client.d.ts +11 -88
- package/dist/portfolio/client.js +150 -4
- package/dist/portfolio/client.js.map +1 -1
- package/dist/portfolio/components/PortfolioAnalytics.cjs +87 -0
- package/dist/portfolio/components/PortfolioAnalytics.cjs.map +1 -0
- package/dist/portfolio/components/PortfolioAnalytics.d.cts +5 -0
- package/dist/portfolio/components/PortfolioAnalytics.d.ts +5 -0
- package/dist/portfolio/components/PortfolioAnalytics.js +9 -0
- package/dist/portfolio/components/PortfolioAnalytics.js.map +1 -0
- package/dist/portfolio/components/PortfolioErrorFallback.cjs +65 -0
- package/dist/portfolio/components/PortfolioErrorFallback.cjs.map +1 -0
- package/dist/portfolio/components/PortfolioErrorFallback.d.cts +22 -0
- package/dist/portfolio/components/PortfolioErrorFallback.d.ts +22 -0
- package/dist/portfolio/components/PortfolioErrorFallback.js +9 -0
- package/dist/portfolio/components/PortfolioErrorFallback.js.map +1 -0
- package/dist/portfolio/components/PortfolioLeadCTA.cjs +91 -0
- package/dist/portfolio/components/PortfolioLeadCTA.cjs.map +1 -0
- package/dist/portfolio/components/PortfolioLeadCTA.d.cts +34 -0
- package/dist/portfolio/components/PortfolioLeadCTA.d.ts +34 -0
- package/dist/portfolio/components/PortfolioLeadCTA.js +9 -0
- package/dist/portfolio/components/PortfolioLeadCTA.js.map +1 -0
- package/dist/portfolio/index.cjs +19 -20
- package/dist/portfolio/index.d.cts +133 -7
- package/dist/portfolio/index.d.ts +133 -7
- package/dist/portfolio/index.js +6 -7
- package/dist/portfolio/primitives/AnimatedCounter.cjs +88 -0
- package/dist/portfolio/primitives/AnimatedCounter.cjs.map +1 -0
- package/dist/portfolio/primitives/AnimatedCounter.d.cts +22 -0
- package/dist/portfolio/primitives/AnimatedCounter.d.ts +22 -0
- package/dist/portfolio/primitives/AnimatedCounter.js +68 -0
- package/dist/portfolio/primitives/AnimatedCounter.js.map +1 -0
- package/dist/portfolio/primitives/AnnotatedImage.cjs +320 -0
- package/dist/portfolio/primitives/AnnotatedImage.cjs.map +1 -0
- package/dist/portfolio/primitives/AnnotatedImage.d.cts +18 -0
- package/dist/portfolio/primitives/AnnotatedImage.d.ts +18 -0
- package/dist/portfolio/primitives/AnnotatedImage.js +264 -0
- package/dist/portfolio/primitives/AnnotatedImage.js.map +1 -0
- package/dist/portfolio/primitives/BeforeAfterSlider.cjs +348 -0
- package/dist/portfolio/primitives/BeforeAfterSlider.cjs.map +1 -0
- package/dist/portfolio/primitives/BeforeAfterSlider.d.cts +13 -0
- package/dist/portfolio/primitives/BeforeAfterSlider.d.ts +13 -0
- package/dist/portfolio/primitives/BeforeAfterSlider.js +292 -0
- package/dist/portfolio/primitives/BeforeAfterSlider.js.map +1 -0
- package/dist/portfolio/primitives/DeviceTrifolio.cjs +298 -0
- package/dist/portfolio/primitives/DeviceTrifolio.cjs.map +1 -0
- package/dist/portfolio/primitives/DeviceTrifolio.d.cts +14 -0
- package/dist/portfolio/primitives/DeviceTrifolio.d.ts +14 -0
- package/dist/portfolio/primitives/DeviceTrifolio.js +242 -0
- package/dist/portfolio/primitives/DeviceTrifolio.js.map +1 -0
- package/dist/portfolio/primitives/FrequencyBarSkeleton.cjs +88 -0
- package/dist/portfolio/primitives/FrequencyBarSkeleton.cjs.map +1 -0
- package/dist/portfolio/primitives/FrequencyBarSkeleton.d.cts +10 -0
- package/dist/portfolio/primitives/FrequencyBarSkeleton.d.ts +10 -0
- package/dist/portfolio/primitives/FrequencyBarSkeleton.js +68 -0
- package/dist/portfolio/primitives/FrequencyBarSkeleton.js.map +1 -0
- package/dist/portfolio/primitives/GlassCard.cjs +45 -0
- package/dist/portfolio/primitives/GlassCard.cjs.map +1 -0
- package/dist/portfolio/primitives/GlassCard.d.cts +11 -0
- package/dist/portfolio/primitives/GlassCard.d.ts +11 -0
- package/dist/portfolio/primitives/GlassCard.js +43 -0
- package/dist/portfolio/primitives/GlassCard.js.map +1 -0
- package/dist/portfolio/primitives/MetricBadge.cjs +74 -0
- package/dist/portfolio/primitives/MetricBadge.cjs.map +1 -0
- package/dist/portfolio/primitives/MetricBadge.d.cts +9 -0
- package/dist/portfolio/primitives/MetricBadge.d.ts +9 -0
- package/dist/portfolio/primitives/MetricBadge.js +54 -0
- package/dist/portfolio/primitives/MetricBadge.js.map +1 -0
- package/dist/portfolio/primitives/SVGChart.cjs +316 -0
- package/dist/portfolio/primitives/SVGChart.cjs.map +1 -0
- package/dist/portfolio/primitives/SVGChart.d.cts +18 -0
- package/dist/portfolio/primitives/SVGChart.d.ts +18 -0
- package/dist/portfolio/primitives/SVGChart.js +296 -0
- package/dist/portfolio/primitives/SVGChart.js.map +1 -0
- package/dist/portfolio/primitives/ScrollReveal.cjs +97 -0
- package/dist/portfolio/primitives/ScrollReveal.cjs.map +1 -0
- package/dist/portfolio/primitives/ScrollReveal.d.cts +27 -0
- package/dist/portfolio/primitives/ScrollReveal.d.ts +27 -0
- package/dist/portfolio/primitives/ScrollReveal.js +77 -0
- package/dist/portfolio/primitives/ScrollReveal.js.map +1 -0
- package/dist/portfolio/primitives/SplitHeadline.cjs +107 -0
- package/dist/portfolio/primitives/SplitHeadline.cjs.map +1 -0
- package/dist/portfolio/primitives/SplitHeadline.d.cts +24 -0
- package/dist/portfolio/primitives/SplitHeadline.d.ts +24 -0
- package/dist/portfolio/primitives/SplitHeadline.js +87 -0
- package/dist/portfolio/primitives/SplitHeadline.js.map +1 -0
- package/dist/portfolio/sections/BeforeAfterSection.cjs +271 -0
- package/dist/portfolio/sections/BeforeAfterSection.cjs.map +1 -0
- package/dist/portfolio/sections/BeforeAfterSection.d.cts +9 -0
- package/dist/portfolio/sections/BeforeAfterSection.d.ts +9 -0
- package/dist/{BeforeAfterSection-6BHFLY4Y.js → portfolio/sections/BeforeAfterSection.js} +66 -27
- package/dist/portfolio/sections/BeforeAfterSection.js.map +1 -0
- package/dist/portfolio/sections/CTASection.cjs +123 -0
- package/dist/portfolio/sections/CTASection.cjs.map +1 -0
- package/dist/portfolio/sections/CTASection.d.cts +9 -0
- package/dist/portfolio/sections/CTASection.d.ts +9 -0
- package/dist/portfolio/sections/CTASection.js +93 -0
- package/dist/portfolio/sections/CTASection.js.map +1 -0
- package/dist/{ChallengesSection-IZ3DHECS.cjs → portfolio/sections/ChallengesSection.cjs} +12 -6
- package/dist/portfolio/sections/ChallengesSection.cjs.map +1 -0
- package/dist/portfolio/sections/ChallengesSection.d.cts +9 -0
- package/dist/portfolio/sections/ChallengesSection.d.ts +9 -0
- package/dist/{ChallengesSection-GEQGVSJN.js → portfolio/sections/ChallengesSection.js} +5 -4
- package/dist/portfolio/sections/ChallengesSection.js.map +1 -0
- package/dist/portfolio/sections/ConversionFunnelSection.cjs +263 -0
- package/dist/portfolio/sections/ConversionFunnelSection.cjs.map +1 -0
- package/dist/portfolio/sections/ConversionFunnelSection.d.cts +9 -0
- package/dist/portfolio/sections/ConversionFunnelSection.d.ts +9 -0
- package/dist/{ConversionFunnelSection-D3GE4NKE.js → portfolio/sections/ConversionFunnelSection.js} +60 -56
- package/dist/portfolio/sections/ConversionFunnelSection.js.map +1 -0
- package/dist/{DesignSystemSection-KXIQXITF.cjs → portfolio/sections/DesignSystemSection.cjs} +13 -7
- package/dist/portfolio/sections/DesignSystemSection.cjs.map +1 -0
- package/dist/portfolio/sections/DesignSystemSection.d.cts +32 -0
- package/dist/portfolio/sections/DesignSystemSection.d.ts +32 -0
- package/dist/{DesignSystemSection-2R5BRBGO.js → portfolio/sections/DesignSystemSection.js} +5 -4
- package/dist/portfolio/sections/DesignSystemSection.js.map +1 -0
- package/dist/{DetailsSection-TTUZAPZZ.cjs → portfolio/sections/DetailsSection.cjs} +12 -6
- package/dist/portfolio/sections/DetailsSection.cjs.map +1 -0
- package/dist/portfolio/sections/DetailsSection.d.cts +9 -0
- package/dist/portfolio/sections/DetailsSection.d.ts +9 -0
- package/dist/{DetailsSection-A6PZQUQL.js → portfolio/sections/DetailsSection.js} +5 -4
- package/dist/portfolio/sections/DetailsSection.js.map +1 -0
- package/dist/portfolio/sections/FeatureSpotlightSection.cjs +267 -0
- package/dist/portfolio/sections/FeatureSpotlightSection.cjs.map +1 -0
- package/dist/portfolio/sections/FeatureSpotlightSection.d.cts +9 -0
- package/dist/portfolio/sections/FeatureSpotlightSection.d.ts +9 -0
- package/dist/{FeatureSpotlightSection-B7P3JGNL.js → portfolio/sections/FeatureSpotlightSection.js} +39 -33
- package/dist/portfolio/sections/FeatureSpotlightSection.js.map +1 -0
- package/dist/portfolio/sections/GallerySection.cjs +321 -0
- package/dist/portfolio/sections/GallerySection.cjs.map +1 -0
- package/dist/portfolio/sections/GallerySection.d.cts +9 -0
- package/dist/portfolio/sections/GallerySection.d.ts +9 -0
- package/dist/{GallerySection-VMKORC47.js → portfolio/sections/GallerySection.js} +65 -18
- package/dist/portfolio/sections/GallerySection.js.map +1 -0
- package/dist/portfolio/sections/HeroSection.cjs +180 -0
- package/dist/portfolio/sections/HeroSection.cjs.map +1 -0
- package/dist/portfolio/sections/HeroSection.d.cts +15 -0
- package/dist/portfolio/sections/HeroSection.d.ts +15 -0
- package/dist/portfolio/sections/HeroSection.js +171 -0
- package/dist/portfolio/sections/HeroSection.js.map +1 -0
- package/dist/{MetricsTimelineSection-4L6DUHJ5.cjs → portfolio/sections/MetricsTimelineSection.cjs} +102 -61
- package/dist/portfolio/sections/MetricsTimelineSection.cjs.map +1 -0
- package/dist/portfolio/sections/MetricsTimelineSection.d.cts +9 -0
- package/dist/portfolio/sections/MetricsTimelineSection.d.ts +9 -0
- package/dist/{MetricsTimelineSection-6BT5GNFV.js → portfolio/sections/MetricsTimelineSection.js} +45 -29
- package/dist/portfolio/sections/MetricsTimelineSection.js.map +1 -0
- package/dist/{PerformanceSection-24TVVFZA.cjs → portfolio/sections/PerformanceSection.cjs} +136 -93
- package/dist/portfolio/sections/PerformanceSection.cjs.map +1 -0
- package/dist/portfolio/sections/PerformanceSection.d.cts +22 -0
- package/dist/portfolio/sections/PerformanceSection.d.ts +22 -0
- package/dist/{PerformanceSection-MGCEIXDX.js → portfolio/sections/PerformanceSection.js} +58 -40
- package/dist/portfolio/sections/PerformanceSection.js.map +1 -0
- package/dist/{ResultsSection-XLGMMQKY.cjs → portfolio/sections/ResultsSection.cjs} +22 -10
- package/dist/portfolio/sections/ResultsSection.cjs.map +1 -0
- package/dist/portfolio/sections/ResultsSection.d.cts +13 -0
- package/dist/portfolio/sections/ResultsSection.d.ts +13 -0
- package/dist/{ResultsSection-DFUJ5U6M.js → portfolio/sections/ResultsSection.js} +13 -7
- package/dist/portfolio/sections/ResultsSection.js.map +1 -0
- package/dist/portfolio/sections/ServicesSection.cjs +129 -0
- package/dist/portfolio/sections/ServicesSection.cjs.map +1 -0
- package/dist/portfolio/sections/ServicesSection.d.cts +9 -0
- package/dist/portfolio/sections/ServicesSection.d.ts +9 -0
- package/dist/portfolio/sections/ServicesSection.js +122 -0
- package/dist/portfolio/sections/ServicesSection.js.map +1 -0
- package/dist/{SiteArchitectureSection-J7BPBERH.cjs → portfolio/sections/SiteArchitectureSection.cjs} +116 -92
- package/dist/portfolio/sections/SiteArchitectureSection.cjs.map +1 -0
- package/dist/portfolio/sections/SiteArchitectureSection.d.cts +21 -0
- package/dist/portfolio/sections/SiteArchitectureSection.d.ts +21 -0
- package/dist/{SiteArchitectureSection-TE22OAWV.js → portfolio/sections/SiteArchitectureSection.js} +46 -47
- package/dist/portfolio/sections/SiteArchitectureSection.js.map +1 -0
- package/dist/{SpeedComparisonSection-EZKFQVGW.cjs → portfolio/sections/SpeedComparisonSection.cjs} +12 -6
- package/dist/portfolio/sections/SpeedComparisonSection.cjs.map +1 -0
- package/dist/portfolio/sections/SpeedComparisonSection.d.cts +20 -0
- package/dist/portfolio/sections/SpeedComparisonSection.d.ts +20 -0
- package/dist/{SpeedComparisonSection-Y3K7OFZQ.js → portfolio/sections/SpeedComparisonSection.js} +5 -4
- package/dist/portfolio/sections/SpeedComparisonSection.js.map +1 -0
- package/dist/portfolio/sections/StrategySection.cjs +233 -0
- package/dist/portfolio/sections/StrategySection.cjs.map +1 -0
- package/dist/portfolio/sections/StrategySection.d.cts +9 -0
- package/dist/portfolio/sections/StrategySection.d.ts +9 -0
- package/dist/{StrategySection-DI5RSCJU.js → portfolio/sections/StrategySection.js} +60 -26
- package/dist/portfolio/sections/StrategySection.js.map +1 -0
- package/dist/portfolio/sections/TeamSection.cjs +162 -0
- package/dist/portfolio/sections/TeamSection.cjs.map +1 -0
- package/dist/portfolio/sections/TeamSection.d.cts +9 -0
- package/dist/portfolio/sections/TeamSection.d.ts +9 -0
- package/dist/portfolio/sections/TeamSection.js +106 -0
- package/dist/portfolio/sections/TeamSection.js.map +1 -0
- package/dist/portfolio/sections/TechStackSection.cjs +106 -0
- package/dist/portfolio/sections/TechStackSection.cjs.map +1 -0
- package/dist/portfolio/sections/TechStackSection.d.cts +9 -0
- package/dist/portfolio/sections/TechStackSection.d.ts +9 -0
- package/dist/portfolio/sections/TechStackSection.js +99 -0
- package/dist/portfolio/sections/TechStackSection.js.map +1 -0
- package/dist/{TestimonialSection-XPTFUQIN.cjs → portfolio/sections/TestimonialSection.cjs} +47 -28
- package/dist/portfolio/sections/TestimonialSection.cjs.map +1 -0
- package/dist/portfolio/sections/TestimonialSection.d.cts +9 -0
- package/dist/portfolio/sections/TestimonialSection.d.ts +9 -0
- package/dist/{TestimonialSection-6RGSMXQB.js → portfolio/sections/TestimonialSection.js} +40 -27
- package/dist/portfolio/sections/TestimonialSection.js.map +1 -0
- package/dist/portfolio/sections/VideoSection.cjs +181 -0
- package/dist/portfolio/sections/VideoSection.cjs.map +1 -0
- package/dist/portfolio/sections/VideoSection.d.cts +9 -0
- package/dist/portfolio/sections/VideoSection.d.ts +9 -0
- package/dist/{VideoSection-4A2HC6K6.js → portfolio/sections/VideoSection.js} +21 -13
- package/dist/portfolio/sections/VideoSection.js.map +1 -0
- package/dist/portfolio/server.cjs +56 -144
- package/dist/portfolio/server.cjs.map +1 -1
- package/dist/portfolio/server.d.cts +157 -7
- package/dist/portfolio/server.d.ts +157 -7
- package/dist/portfolio/server.js +3 -142
- package/dist/portfolio/server.js.map +1 -1
- package/dist/preview.cjs +56 -0
- package/dist/preview.cjs.map +1 -0
- package/dist/preview.d.cts +31 -0
- package/dist/preview.d.ts +31 -0
- package/dist/preview.js +54 -0
- package/dist/preview.js.map +1 -0
- package/dist/{types-DL4t_Cfa.d.ts → types-0tVPy4pU.d.cts} +7 -1
- package/dist/{types-DL4t_Cfa.d.cts → types-0tVPy4pU.d.ts} +7 -1
- package/dist/types-D0nb6KAx.d.cts +299 -0
- package/dist/types-D0nb6KAx.d.ts +299 -0
- package/package.json +38 -14
- package/templates/api-portfolio-preview.ts +6 -0
- package/templates/api-revalidate.ts +5 -0
- package/templates/work-category-page.tsx +40 -0
- package/templates/work-error.tsx +3 -0
- package/templates/work-loading.tsx +1 -0
- package/templates/work-page.tsx +37 -0
- package/templates/work-slug-loading.tsx +1 -0
- package/templates/work-slug-page.tsx +41 -0
- package/dist/BeforeAfterSection-6BHFLY4Y.js.map +0 -1
- package/dist/BeforeAfterSection-JTORBR3A.cjs +0 -181
- package/dist/BeforeAfterSection-JTORBR3A.cjs.map +0 -1
- package/dist/CTASection-4JKLXEUF.cjs +0 -111
- package/dist/CTASection-4JKLXEUF.cjs.map +0 -1
- package/dist/CTASection-BJA72XIL.js +0 -106
- package/dist/CTASection-BJA72XIL.js.map +0 -1
- package/dist/ChallengesSection-GEQGVSJN.js.map +0 -1
- package/dist/ChallengesSection-IZ3DHECS.cjs.map +0 -1
- package/dist/ConversionFunnelSection-AUUSJ5HQ.cjs +0 -209
- package/dist/ConversionFunnelSection-AUUSJ5HQ.cjs.map +0 -1
- package/dist/ConversionFunnelSection-D3GE4NKE.js.map +0 -1
- package/dist/DesignSystemSection-2R5BRBGO.js.map +0 -1
- package/dist/DesignSystemSection-KXIQXITF.cjs.map +0 -1
- package/dist/DetailsSection-A6PZQUQL.js.map +0 -1
- package/dist/DetailsSection-TTUZAPZZ.cjs.map +0 -1
- package/dist/FeatureSpotlightSection-B7P3JGNL.js.map +0 -1
- package/dist/FeatureSpotlightSection-WRHXS7TU.cjs +0 -210
- package/dist/FeatureSpotlightSection-WRHXS7TU.cjs.map +0 -1
- package/dist/GallerySection-VMKORC47.js.map +0 -1
- package/dist/GallerySection-WJ4PQDBI.cjs +0 -219
- package/dist/GallerySection-WJ4PQDBI.cjs.map +0 -1
- package/dist/MetricsTimelineSection-4L6DUHJ5.cjs.map +0 -1
- package/dist/MetricsTimelineSection-6BT5GNFV.js.map +0 -1
- package/dist/PerformanceSection-24TVVFZA.cjs.map +0 -1
- package/dist/PerformanceSection-MGCEIXDX.js.map +0 -1
- package/dist/ResultsSection-DFUJ5U6M.js.map +0 -1
- package/dist/ResultsSection-XLGMMQKY.cjs.map +0 -1
- package/dist/ServicesSection-D5V3Q4GR.js +0 -118
- package/dist/ServicesSection-D5V3Q4GR.js.map +0 -1
- package/dist/ServicesSection-WJMGK2MF.cjs +0 -120
- package/dist/ServicesSection-WJMGK2MF.cjs.map +0 -1
- package/dist/SiteArchitectureSection-J7BPBERH.cjs.map +0 -1
- package/dist/SiteArchitectureSection-TE22OAWV.js.map +0 -1
- package/dist/SpeedComparisonSection-EZKFQVGW.cjs.map +0 -1
- package/dist/SpeedComparisonSection-Y3K7OFZQ.js.map +0 -1
- package/dist/StrategySection-CJ7Y6OFQ.cjs +0 -174
- package/dist/StrategySection-CJ7Y6OFQ.cjs.map +0 -1
- package/dist/StrategySection-DI5RSCJU.js.map +0 -1
- package/dist/TeamSection-DZVSNZE6.cjs +0 -112
- package/dist/TeamSection-DZVSNZE6.cjs.map +0 -1
- package/dist/TeamSection-HGKFW6PQ.js +0 -107
- package/dist/TeamSection-HGKFW6PQ.js.map +0 -1
- package/dist/TechStackSection-2AQ7RGY3.js +0 -93
- package/dist/TechStackSection-2AQ7RGY3.js.map +0 -1
- package/dist/TechStackSection-VTNNZR5V.cjs +0 -95
- package/dist/TechStackSection-VTNNZR5V.cjs.map +0 -1
- package/dist/TestimonialSection-6RGSMXQB.js.map +0 -1
- package/dist/TestimonialSection-XPTFUQIN.cjs.map +0 -1
- package/dist/VideoSection-4A2HC6K6.js.map +0 -1
- package/dist/VideoSection-G3DFS7UH.cjs +0 -118
- package/dist/VideoSection-G3DFS7UH.cjs.map +0 -1
- package/dist/chunk-2Y4O3LWM.js +0 -53
- package/dist/chunk-2Y4O3LWM.js.map +0 -1
- package/dist/chunk-633UMQQH.cjs +0 -31
- package/dist/chunk-633UMQQH.cjs.map +0 -1
- package/dist/chunk-7CFFAKDM.js +0 -74
- package/dist/chunk-7CFFAKDM.js.map +0 -1
- package/dist/chunk-APG2QSMB.js +0 -131
- package/dist/chunk-APG2QSMB.js.map +0 -1
- package/dist/chunk-AVCH4LMG.cjs +0 -620
- package/dist/chunk-AVCH4LMG.cjs.map +0 -1
- package/dist/chunk-AYBUHRFS.js +0 -613
- package/dist/chunk-AYBUHRFS.js.map +0 -1
- package/dist/chunk-EE75WRCM.cjs.map +0 -1
- package/dist/chunk-IKBK7HYX.cjs +0 -79
- package/dist/chunk-IKBK7HYX.cjs.map +0 -1
- package/dist/chunk-KEOHORIH.cjs +0 -79
- package/dist/chunk-KEOHORIH.cjs.map +0 -1
- package/dist/chunk-OA5ZM4OA.cjs +0 -139
- package/dist/chunk-OA5ZM4OA.cjs.map +0 -1
- package/dist/chunk-TVFXN6W6.js.map +0 -1
- package/dist/chunk-VVTK4OL6.js +0 -28
- package/dist/chunk-VVTK4OL6.js.map +0 -1
- package/dist/chunk-XCKXHK44.js +0 -15
- package/dist/chunk-XCKXHK44.js.map +0 -1
- package/dist/chunk-XONXEFJY.cjs +0 -58
- package/dist/chunk-XONXEFJY.cjs.map +0 -1
- package/dist/chunk-XQNJED46.cjs +0 -19
- package/dist/chunk-XQNJED46.cjs.map +0 -1
- package/dist/chunk-YB4B3OMC.js +0 -74
- package/dist/chunk-YB4B3OMC.js.map +0 -1
- package/dist/portfolio/sections.cjs +0 -20
- package/dist/portfolio/sections.cjs.map +0 -1
- package/dist/portfolio/sections.d.cts +0 -42
- package/dist/portfolio/sections.d.ts +0 -42
- package/dist/portfolio/sections.js +0 -4
- package/dist/portfolio/sections.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/portfolio/components/primitives/DeviceTrifolio.tsx","../../../src/shared/images.ts"],"sourcesContent":["'use client';\n\nimport React, { useRef, useEffect, useState, useCallback } from 'react';\nimport Image from 'next/image';\nimport { resolvePortfolioImage } from '../../../shared/images';\n\n/**\n * Screenshot layer inside a device frame. Sits above the live iframe and\n * fades out once the iframe verifies. The desktop shot is a likely LCP\n * element on desktop viewports, so it loads with priority.\n */\nfunction ScreenshotOverlay({\n src,\n alt,\n sizes,\n priority = false,\n hidden,\n}: {\n src?: string;\n alt: string;\n sizes: string;\n priority?: boolean;\n hidden: boolean;\n}) {\n const resolved = resolvePortfolioImage(src);\n if (!resolved) return null;\n return (\n <Image\n src={resolved.url}\n alt={alt}\n fill\n sizes={sizes}\n priority={priority}\n fetchPriority={priority ? 'high' : undefined}\n style={{\n objectFit: 'cover',\n objectPosition: 'top center',\n zIndex: 2,\n opacity: hidden ? 0 : 1,\n transition: 'opacity 0.8s ease',\n pointerEvents: 'none' as const,\n }}\n />\n );\n}\n\ninterface DeviceTrifolioProps {\n url?: string;\n screenshots?: {\n desktop?: string;\n tablet?: string;\n mobile?: string;\n };\n className?: string;\n}\n\n// ─── Hooks ───────────────────────────────────────────────────────────────────\n\nfunction useScaledIframe(viewportWidth: number) {\n const containerRef = useRef<HTMLDivElement>(null);\n const [scale, setScale] = useState(1);\n\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n const updateScale = () => {\n const containerWidth = container.offsetWidth;\n setScale(containerWidth / viewportWidth);\n };\n\n updateScale();\n const resizeObserver = new ResizeObserver(updateScale);\n resizeObserver.observe(container);\n return () => resizeObserver.disconnect();\n }, [viewportWidth]);\n\n return { containerRef, scale };\n}\n\nfunction useScrollSafeIframe() {\n const [isLoaded, setIsLoaded] = useState(false);\n const [iframeFailed, setIframeFailed] = useState(false);\n const iframeRef = useRef<HTMLIFrameElement>(null);\n const loadTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n const handleLoad = useCallback(() => {\n const iframe = iframeRef.current;\n if (!iframe) return;\n\n // Wait for iframe to paint, then verify it rendered real content\n loadTimerRef.current = setTimeout(() => {\n let isCrossOrigin = false;\n\n try {\n // Try accessing contentDocument — cross-origin sites throw here\n const doc = iframe.contentDocument;\n if (!doc || !doc.body) {\n setIframeFailed(true);\n return;\n }\n\n // Same-origin access succeeded. This means either:\n // 1. A same-origin site loaded (e.g., our own site in dev)\n // 2. Browser rendered its own error page (X-Frame-Options block, connection refused, etc.)\n //\n // Browser error pages are same-origin and have short/generic content.\n // Real sites have <title>, many DOM nodes, scripts, styles, etc.\n const title = doc.title || '';\n const bodyText = doc.body?.innerText?.trim() || '';\n const numElements = doc.querySelectorAll('*').length;\n\n // Heuristic: real sites have a meaningful title AND many DOM elements\n // Browser error pages have very few elements and generic/empty titles\n if (numElements > 20 && title.length > 0 && bodyText.length > 50) {\n setIsLoaded(true);\n } else {\n setIframeFailed(true);\n }\n } catch {\n isCrossOrigin = true;\n }\n\n if (isCrossOrigin) {\n // Cross-origin: the real remote site loaded (can't be a browser error page\n // since those are always same-origin). This is the success case.\n setIsLoaded(true);\n }\n }, 2000);\n }, []);\n\n const handleError = useCallback(() => {\n setIframeFailed(true);\n if (loadTimerRef.current) clearTimeout(loadTimerRef.current);\n }, []);\n\n // Cleanup timer on unmount\n useEffect(() => {\n return () => {\n if (loadTimerRef.current) clearTimeout(loadTimerRef.current);\n };\n }, []);\n\n const wrapperStyle: React.CSSProperties = {\n position: 'absolute',\n left: 0,\n top: 0,\n zIndex: 1,\n overflow: 'hidden',\n width: '100%',\n height: '100%',\n };\n\n const iframeStyle: React.CSSProperties = {};\n\n return { iframeRef, handleLoad, handleError, wrapperStyle, iframeStyle, isLoaded, iframeFailed };\n}\n\n// ─── Device Frames ───────────────────────────────────────────────────────────\n\n/**\n * Hook that detects if an iframe successfully loaded real content.\n * Cross-origin = success (real site loaded). Same-origin with minimal DOM = blocked/error.\n */\nfunction useIframeLoadDetection() {\n const [loaded, setLoaded] = useState(false);\n const iframeRef = useRef<HTMLIFrameElement>(null);\n\n const onLoad = useCallback(() => {\n const iframe = iframeRef.current;\n if (!iframe) return;\n\n // Delay check to let the iframe paint\n setTimeout(() => {\n try {\n // If we can access contentDocument, it's same-origin.\n // Real sites are cross-origin and throw here → catch block → success.\n const doc = iframe.contentDocument;\n if (!doc?.body) { return; } // blocked\n const numElements = doc.querySelectorAll('*').length;\n // Browser error pages have very few elements. Real same-origin sites have many.\n if (numElements > 20) setLoaded(true);\n } catch {\n // Cross-origin error = the real remote site loaded successfully\n setLoaded(true);\n }\n }, 1500);\n }, []);\n\n return { iframeRef, loaded, onLoad };\n}\n\nfunction MacBookFrame({ url, screenshot }: { url: string; screenshot?: string }) {\n const viewportWidth = 1440;\n const viewportHeight = 900;\n const { containerRef, scale } = useScaledIframe(viewportWidth);\n const { iframeRef, loaded: iframeLoaded, onLoad: onIframeLoad } = useIframeLoadDetection();\n\n return (\n <div className=\"w-full\">\n <div\n className=\"relative rounded-t-[10px] p-[4px] pb-0\"\n style={{\n background: 'linear-gradient(135deg, #e8e8ed 0%, #d1d1d6 100%)',\n boxShadow: '0 -4px 30px rgba(0,0,0,0.3), inset 0 1px 0 rgba(255,255,255,0.9)',\n }}\n >\n <div\n className=\"relative rounded-t-[7px] px-[8px] pt-[8px] pb-0\"\n style={{ background: '#0a0a0a' }}\n >\n <div\n className=\"absolute top-0 left-1/2 -translate-x-1/2 w-[70px] h-[18px] flex items-center justify-center z-10\"\n style={{ background: '#0a0a0a', borderRadius: '0 0 10px 10px' }}\n >\n <div className=\"w-[5px] h-[5px] rounded-full\" style={{ background: '#2a3a2a', boxShadow: '0 0 0 1px #1a1a1a' }} />\n </div>\n\n <div ref={containerRef} className=\"relative overflow-hidden rounded-t-[3px]\" style={{ aspectRatio: `${viewportWidth}/${viewportHeight}`, background: '#0a0a0a' }}>\n <ScreenshotOverlay src={screenshot} alt=\"Desktop preview\" sizes=\"(max-width: 1024px) 90vw, 640px\" priority hidden={iframeLoaded} />\n {url && <iframe src={url} title=\"Desktop\" loading=\"eager\" scrolling=\"no\" tabIndex={-1} sandbox=\"allow-scripts allow-same-origin\" ref={iframeRef} onLoad={onIframeLoad} className=\"border-0 pointer-events-none\" style={{ position: 'absolute', top: 0, left: 0, width: `${viewportWidth}px`, height: `${viewportHeight}px`, transformOrigin: 'top left', transform: `scale(${scale})` }} />}\n </div>\n </div>\n </div>\n\n {/* Black chin */}\n <div className=\"h-[8px]\" style={{ background: '#0a0a0a', boxShadow: '0 1px 4px rgba(0,0,0,0.3)' }} />\n\n {/* Silver base */}\n <div\n className=\"relative h-[12px] rounded-b-[10px]\"\n style={{\n background: 'linear-gradient(to bottom, #e0e0e5, #c8c8cd 20%, #b0b0b5 80%, #a0a0a5)',\n boxShadow: '0 4px 15px rgba(0,0,0,0.3)',\n }}\n >\n <div\n className=\"absolute top-0 left-1/2 -translate-x-1/2 w-[25%] h-[4px] rounded-b-[4px]\"\n style={{ background: 'linear-gradient(to bottom, #909095, #a8a8ad)' }}\n />\n </div>\n </div>\n );\n}\n\nfunction IPadFrame({ url, screenshot }: { url: string; screenshot?: string }) {\n const viewportWidth = 768;\n const viewportHeight = 1024;\n const { containerRef, scale } = useScaledIframe(viewportWidth);\n const { iframeRef, loaded: iframeLoaded, onLoad: onIframeLoad } = useIframeLoadDetection();\n\n return (\n <div className=\"relative p-[1.5%]\" style={{ background: 'linear-gradient(135deg, #e8e8ed 0%, #d1d1d6 100%)', borderRadius: '8% / 6%', boxShadow: '0 30px 60px -15px rgba(0,0,0,0.4), 0 0 0 1px rgba(255,255,255,0.9) inset, inset 0 1px 0 rgba(255,255,255,0.8)' }}>\n <div className=\"relative p-[2%]\" style={{ background: '#0a0a0a', borderRadius: '6% / 4.5%' }}>\n <div className=\"absolute top-[1.2%] left-1/2 -translate-x-1/2 w-[1.8%] aspect-square rounded-full z-10\" style={{ background: '#1a1a1f' }} />\n <div ref={containerRef} className=\"relative overflow-hidden\" style={{ borderRadius: '2%', aspectRatio: `${viewportWidth}/${viewportHeight}`, background: '#0a0a0a' }}>\n <ScreenshotOverlay src={screenshot} alt=\"Tablet preview\" sizes=\"(max-width: 1024px) 40vw, 240px\" hidden={iframeLoaded} />\n {url && <iframe src={url} title=\"Tablet\" loading=\"eager\" scrolling=\"no\" tabIndex={-1} sandbox=\"allow-scripts allow-same-origin\" ref={iframeRef} onLoad={onIframeLoad} className=\"border-0 pointer-events-none\" style={{ position: 'absolute', top: 0, left: 0, width: `${viewportWidth}px`, height: `${viewportHeight}px`, transformOrigin: 'top left', transform: `scale(${scale})` }} />}\n </div>\n </div>\n </div>\n );\n}\n\nfunction IPhoneFrame({ url, screenshot }: { url: string; screenshot?: string }) {\n const viewportWidth = 393;\n const viewportHeight = 852;\n const { containerRef, scale } = useScaledIframe(viewportWidth);\n const { iframeRef, loaded: iframeLoaded, onLoad: onIframeLoad } = useIframeLoadDetection();\n\n return (\n <div\n className=\"relative p-[2.5%]\"\n style={{\n background: 'linear-gradient(135deg, #e8e8ed 0%, #d1d1d6 100%)',\n borderRadius: '18% / 8%',\n boxShadow:\n '0 30px 60px -15px rgba(0,0,0,0.4), 0 0 0 1px rgba(255,255,255,0.9) inset, inset 0 1px 0 rgba(255,255,255,0.8)',\n }}\n >\n {/* Side buttons */}\n <div className=\"absolute left-0 top-[18%] w-[1%] h-[6%] rounded-l-sm\" style={{ background: '#c8c8cd' }} />\n <div className=\"absolute left-0 top-[26%] w-[1%] h-[10%] rounded-l-sm\" style={{ background: '#c8c8cd' }} />\n <div className=\"absolute left-0 top-[38%] w-[1%] h-[10%] rounded-l-sm\" style={{ background: '#c8c8cd' }} />\n <div className=\"absolute right-0 top-[22%] w-[1%] h-[12%] rounded-r-sm\" style={{ background: '#c8c8cd' }} />\n\n {/* Black bezel */}\n <div\n className=\"relative p-[3%]\"\n style={{ background: '#0a0a0a', borderRadius: '14% / 6%' }}\n >\n {/* Dynamic Island */}\n <div\n className=\"absolute top-[2.5%] left-1/2 -translate-x-1/2 w-[28%] h-[3.5%] rounded-full z-10\"\n style={{ background: '#1a1a1a' }}\n />\n\n <div ref={containerRef} className=\"relative overflow-hidden\" style={{ borderRadius: '8% / 4%', aspectRatio: `${viewportWidth}/${viewportHeight}`, background: '#0a0a0a' }}>\n <ScreenshotOverlay src={screenshot} alt=\"Mobile preview\" sizes=\"(max-width: 1024px) 25vw, 150px\" hidden={iframeLoaded} />\n {url && <iframe src={url} title=\"Mobile\" loading=\"eager\" scrolling=\"no\" tabIndex={-1} sandbox=\"allow-scripts allow-same-origin\" ref={iframeRef} onLoad={onIframeLoad} className=\"border-0 pointer-events-none\" style={{ position: 'absolute', top: 0, left: 0, width: `${viewportWidth}px`, height: `${viewportHeight}px`, transformOrigin: 'top left', transform: `scale(${scale})` }} />}\n </div>\n\n {/* Home indicator */}\n <div\n className=\"absolute bottom-[2%] left-1/2 -translate-x-1/2 w-[34%] h-[1%] rounded-full\"\n style={{ background: 'rgba(255,255,255,0.25)' }}\n />\n </div>\n </div>\n );\n}\n\n// ─── Main Component ──────────────────────────────────────────────────────────\n\nexport default function DeviceTrifolio({\n url,\n screenshots,\n className = '',\n}: DeviceTrifolioProps) {\n // Pass URL directly — iframes render immediately\n const frameUrl = url || '';\n\n // No entrance animation: the trifolio sits above the fold in the hero, and\n // hiding it post-hydration re-hides the desktop LCP candidate (house LCP\n // rule: above-fold content renders fully static).\n\n return (\n <div\n data-screenshot-target=\"device-trifolio\"\n className={`relative w-full min-w-0 max-w-full overflow-x-clip ${className}`}\n style={{ aspectRatio: '11/5', minHeight: '200px' }}\n >\n {/* MacBook Pro - Center Back */}\n <div\n data-device=\"macbook\"\n className=\"absolute left-1/2 -translate-x-1/2 bottom-0 w-[65%] z-10\"\n >\n <MacBookFrame url={frameUrl} screenshot={screenshots?.desktop} />\n </div>\n\n {/* iPad - Front Left */}\n <div\n data-device=\"ipad\"\n className=\"absolute left-[8%] bottom-0 w-[28%] z-20\"\n >\n <IPadFrame url={frameUrl} screenshot={screenshots?.tablet} />\n </div>\n\n {/* iPhone - Front Right */}\n <div\n data-device=\"iphone\"\n className=\"absolute right-[10%] bottom-0 w-[14%] z-30\"\n >\n <IPhoneFrame url={frameUrl} screenshot={screenshots?.mobile} />\n </div>\n </div>\n );\n}\n","/**\n * Portfolio image resolution — single source of truth for turning the mixed\n * image shapes the API sends (plain URL strings, Supabase Storage URLs,\n * Sanity image objects) into something safe to render.\n *\n * IMPORTANT: a raw Sanity asset _ref (`image-{id}-{W}x{H}-{fmt}`) is an asset\n * ID, NOT a URL. Rendering it as `src` issues a garbage relative request\n * against the agency site's origin. Resolving refs to cdn.sanity.io URLs is\n * the API's job (it owns the Sanity project config); this helper refuses refs\n * so a broken image can never render, and warns once so the gap is visible.\n *\n * When a URL carries `-{W}x{H}.{ext}` dimensions (Sanity CDN URLs always do),\n * they're parsed out so callers can give next/image exact intrinsic sizes.\n */\n\nexport interface ResolvedPortfolioImage {\n url: string;\n width?: number;\n height?: number;\n}\n\n/** Raw Sanity asset reference — an ID, not a URL. */\nconst SANITY_ASSET_REF = /^image-[A-Za-z0-9]+-\\d+x\\d+-[a-z0-9]+$/;\n\n/** `…-1200x800.png` (optionally with a query string) → intrinsic dimensions. */\nconst FILENAME_DIMS = /-(\\d{2,5})x(\\d{2,5})\\.[a-z0-9]+(?:\\?.*)?$/i;\n\ninterface SanityImageLike {\n asset?: { _ref?: string };\n}\n\nlet warnedAboutRefs = false;\n\n/**\n * Resolve a portfolio image input into a renderable URL (+ intrinsic\n * dimensions when they can be derived). Returns null for empty values and\n * for unresolved Sanity refs — callers skip rendering in that case.\n */\nexport function resolvePortfolioImage(\n input: string | SanityImageLike | null | undefined,\n): ResolvedPortfolioImage | null {\n const raw =\n typeof input === 'string' ? input : input?.asset?._ref ?? '';\n const value = (raw || '').trim();\n if (!value) return null;\n\n if (SANITY_ASSET_REF.test(value)) {\n if (!warnedAboutRefs && typeof console !== 'undefined') {\n warnedAboutRefs = true;\n console.warn(\n '[@sonordev/agency-site-kit] Received a raw Sanity asset ref instead of an ' +\n 'image URL — skipping render. The Sonor API should resolve image refs to ' +\n 'cdn.sanity.io URLs before returning portfolio sections.',\n );\n }\n return null;\n }\n\n // Only absolute http(s) URLs or site-rooted paths are renderable.\n if (!/^https?:\\/\\//i.test(value) && !value.startsWith('/')) return null;\n\n const dims = value.match(FILENAME_DIMS);\n if (dims) {\n const width = parseInt(dims[1], 10);\n const height = parseInt(dims[2], 10);\n if (width > 0 && height > 0) return { url: value, width, height };\n }\n\n return { url: value };\n}\n"],"mappings":"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","names":["Image"]}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
interface DeviceTrifolioProps {
|
|
4
|
+
url?: string;
|
|
5
|
+
screenshots?: {
|
|
6
|
+
desktop?: string;
|
|
7
|
+
tablet?: string;
|
|
8
|
+
mobile?: string;
|
|
9
|
+
};
|
|
10
|
+
className?: string;
|
|
11
|
+
}
|
|
12
|
+
declare function DeviceTrifolio({ url, screenshots, className, }: DeviceTrifolioProps): React.JSX.Element;
|
|
13
|
+
|
|
14
|
+
export { DeviceTrifolio as default };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
interface DeviceTrifolioProps {
|
|
4
|
+
url?: string;
|
|
5
|
+
screenshots?: {
|
|
6
|
+
desktop?: string;
|
|
7
|
+
tablet?: string;
|
|
8
|
+
mobile?: string;
|
|
9
|
+
};
|
|
10
|
+
className?: string;
|
|
11
|
+
}
|
|
12
|
+
declare function DeviceTrifolio({ url, screenshots, className, }: DeviceTrifolioProps): React.JSX.Element;
|
|
13
|
+
|
|
14
|
+
export { DeviceTrifolio as default };
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
"use client";
|
|
3
|
+
import {
|
|
4
|
+
resolvePortfolioImage
|
|
5
|
+
} from "../../chunk-QDXZ27R7.js";
|
|
6
|
+
|
|
7
|
+
// src/portfolio/components/primitives/DeviceTrifolio.tsx
|
|
8
|
+
import { useRef, useEffect, useState, useCallback } from "react";
|
|
9
|
+
import Image from "next/image";
|
|
10
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
11
|
+
function ScreenshotOverlay({
|
|
12
|
+
src,
|
|
13
|
+
alt,
|
|
14
|
+
sizes,
|
|
15
|
+
priority = false,
|
|
16
|
+
hidden
|
|
17
|
+
}) {
|
|
18
|
+
const resolved = resolvePortfolioImage(src);
|
|
19
|
+
if (!resolved) return null;
|
|
20
|
+
return /* @__PURE__ */ jsx(
|
|
21
|
+
Image,
|
|
22
|
+
{
|
|
23
|
+
src: resolved.url,
|
|
24
|
+
alt,
|
|
25
|
+
fill: true,
|
|
26
|
+
sizes,
|
|
27
|
+
priority,
|
|
28
|
+
fetchPriority: priority ? "high" : void 0,
|
|
29
|
+
style: {
|
|
30
|
+
objectFit: "cover",
|
|
31
|
+
objectPosition: "top center",
|
|
32
|
+
zIndex: 2,
|
|
33
|
+
opacity: hidden ? 0 : 1,
|
|
34
|
+
transition: "opacity 0.8s ease",
|
|
35
|
+
pointerEvents: "none"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
function useScaledIframe(viewportWidth) {
|
|
41
|
+
const containerRef = useRef(null);
|
|
42
|
+
const [scale, setScale] = useState(1);
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
const container = containerRef.current;
|
|
45
|
+
if (!container) return;
|
|
46
|
+
const updateScale = () => {
|
|
47
|
+
const containerWidth = container.offsetWidth;
|
|
48
|
+
setScale(containerWidth / viewportWidth);
|
|
49
|
+
};
|
|
50
|
+
updateScale();
|
|
51
|
+
const resizeObserver = new ResizeObserver(updateScale);
|
|
52
|
+
resizeObserver.observe(container);
|
|
53
|
+
return () => resizeObserver.disconnect();
|
|
54
|
+
}, [viewportWidth]);
|
|
55
|
+
return { containerRef, scale };
|
|
56
|
+
}
|
|
57
|
+
function useIframeLoadDetection() {
|
|
58
|
+
const [loaded, setLoaded] = useState(false);
|
|
59
|
+
const iframeRef = useRef(null);
|
|
60
|
+
const onLoad = useCallback(() => {
|
|
61
|
+
const iframe = iframeRef.current;
|
|
62
|
+
if (!iframe) return;
|
|
63
|
+
setTimeout(() => {
|
|
64
|
+
try {
|
|
65
|
+
const doc = iframe.contentDocument;
|
|
66
|
+
if (!doc?.body) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const numElements = doc.querySelectorAll("*").length;
|
|
70
|
+
if (numElements > 20) setLoaded(true);
|
|
71
|
+
} catch {
|
|
72
|
+
setLoaded(true);
|
|
73
|
+
}
|
|
74
|
+
}, 1500);
|
|
75
|
+
}, []);
|
|
76
|
+
return { iframeRef, loaded, onLoad };
|
|
77
|
+
}
|
|
78
|
+
function MacBookFrame({ url, screenshot }) {
|
|
79
|
+
const viewportWidth = 1440;
|
|
80
|
+
const viewportHeight = 900;
|
|
81
|
+
const { containerRef, scale } = useScaledIframe(viewportWidth);
|
|
82
|
+
const { iframeRef, loaded: iframeLoaded, onLoad: onIframeLoad } = useIframeLoadDetection();
|
|
83
|
+
return /* @__PURE__ */ jsxs("div", { className: "w-full", children: [
|
|
84
|
+
/* @__PURE__ */ jsx(
|
|
85
|
+
"div",
|
|
86
|
+
{
|
|
87
|
+
className: "relative rounded-t-[10px] p-[4px] pb-0",
|
|
88
|
+
style: {
|
|
89
|
+
background: "linear-gradient(135deg, #e8e8ed 0%, #d1d1d6 100%)",
|
|
90
|
+
boxShadow: "0 -4px 30px rgba(0,0,0,0.3), inset 0 1px 0 rgba(255,255,255,0.9)"
|
|
91
|
+
},
|
|
92
|
+
children: /* @__PURE__ */ jsxs(
|
|
93
|
+
"div",
|
|
94
|
+
{
|
|
95
|
+
className: "relative rounded-t-[7px] px-[8px] pt-[8px] pb-0",
|
|
96
|
+
style: { background: "#0a0a0a" },
|
|
97
|
+
children: [
|
|
98
|
+
/* @__PURE__ */ jsx(
|
|
99
|
+
"div",
|
|
100
|
+
{
|
|
101
|
+
className: "absolute top-0 left-1/2 -translate-x-1/2 w-[70px] h-[18px] flex items-center justify-center z-10",
|
|
102
|
+
style: { background: "#0a0a0a", borderRadius: "0 0 10px 10px" },
|
|
103
|
+
children: /* @__PURE__ */ jsx("div", { className: "w-[5px] h-[5px] rounded-full", style: { background: "#2a3a2a", boxShadow: "0 0 0 1px #1a1a1a" } })
|
|
104
|
+
}
|
|
105
|
+
),
|
|
106
|
+
/* @__PURE__ */ jsxs("div", { ref: containerRef, className: "relative overflow-hidden rounded-t-[3px]", style: { aspectRatio: `${viewportWidth}/${viewportHeight}`, background: "#0a0a0a" }, children: [
|
|
107
|
+
/* @__PURE__ */ jsx(ScreenshotOverlay, { src: screenshot, alt: "Desktop preview", sizes: "(max-width: 1024px) 90vw, 640px", priority: true, hidden: iframeLoaded }),
|
|
108
|
+
url && /* @__PURE__ */ jsx("iframe", { src: url, title: "Desktop", loading: "eager", scrolling: "no", tabIndex: -1, sandbox: "allow-scripts allow-same-origin", ref: iframeRef, onLoad: onIframeLoad, className: "border-0 pointer-events-none", style: { position: "absolute", top: 0, left: 0, width: `${viewportWidth}px`, height: `${viewportHeight}px`, transformOrigin: "top left", transform: `scale(${scale})` } })
|
|
109
|
+
] })
|
|
110
|
+
]
|
|
111
|
+
}
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
),
|
|
115
|
+
/* @__PURE__ */ jsx("div", { className: "h-[8px]", style: { background: "#0a0a0a", boxShadow: "0 1px 4px rgba(0,0,0,0.3)" } }),
|
|
116
|
+
/* @__PURE__ */ jsx(
|
|
117
|
+
"div",
|
|
118
|
+
{
|
|
119
|
+
className: "relative h-[12px] rounded-b-[10px]",
|
|
120
|
+
style: {
|
|
121
|
+
background: "linear-gradient(to bottom, #e0e0e5, #c8c8cd 20%, #b0b0b5 80%, #a0a0a5)",
|
|
122
|
+
boxShadow: "0 4px 15px rgba(0,0,0,0.3)"
|
|
123
|
+
},
|
|
124
|
+
children: /* @__PURE__ */ jsx(
|
|
125
|
+
"div",
|
|
126
|
+
{
|
|
127
|
+
className: "absolute top-0 left-1/2 -translate-x-1/2 w-[25%] h-[4px] rounded-b-[4px]",
|
|
128
|
+
style: { background: "linear-gradient(to bottom, #909095, #a8a8ad)" }
|
|
129
|
+
}
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
)
|
|
133
|
+
] });
|
|
134
|
+
}
|
|
135
|
+
function IPadFrame({ url, screenshot }) {
|
|
136
|
+
const viewportWidth = 768;
|
|
137
|
+
const viewportHeight = 1024;
|
|
138
|
+
const { containerRef, scale } = useScaledIframe(viewportWidth);
|
|
139
|
+
const { iframeRef, loaded: iframeLoaded, onLoad: onIframeLoad } = useIframeLoadDetection();
|
|
140
|
+
return /* @__PURE__ */ jsx("div", { className: "relative p-[1.5%]", style: { background: "linear-gradient(135deg, #e8e8ed 0%, #d1d1d6 100%)", borderRadius: "8% / 6%", boxShadow: "0 30px 60px -15px rgba(0,0,0,0.4), 0 0 0 1px rgba(255,255,255,0.9) inset, inset 0 1px 0 rgba(255,255,255,0.8)" }, children: /* @__PURE__ */ jsxs("div", { className: "relative p-[2%]", style: { background: "#0a0a0a", borderRadius: "6% / 4.5%" }, children: [
|
|
141
|
+
/* @__PURE__ */ jsx("div", { className: "absolute top-[1.2%] left-1/2 -translate-x-1/2 w-[1.8%] aspect-square rounded-full z-10", style: { background: "#1a1a1f" } }),
|
|
142
|
+
/* @__PURE__ */ jsxs("div", { ref: containerRef, className: "relative overflow-hidden", style: { borderRadius: "2%", aspectRatio: `${viewportWidth}/${viewportHeight}`, background: "#0a0a0a" }, children: [
|
|
143
|
+
/* @__PURE__ */ jsx(ScreenshotOverlay, { src: screenshot, alt: "Tablet preview", sizes: "(max-width: 1024px) 40vw, 240px", hidden: iframeLoaded }),
|
|
144
|
+
url && /* @__PURE__ */ jsx("iframe", { src: url, title: "Tablet", loading: "eager", scrolling: "no", tabIndex: -1, sandbox: "allow-scripts allow-same-origin", ref: iframeRef, onLoad: onIframeLoad, className: "border-0 pointer-events-none", style: { position: "absolute", top: 0, left: 0, width: `${viewportWidth}px`, height: `${viewportHeight}px`, transformOrigin: "top left", transform: `scale(${scale})` } })
|
|
145
|
+
] })
|
|
146
|
+
] }) });
|
|
147
|
+
}
|
|
148
|
+
function IPhoneFrame({ url, screenshot }) {
|
|
149
|
+
const viewportWidth = 393;
|
|
150
|
+
const viewportHeight = 852;
|
|
151
|
+
const { containerRef, scale } = useScaledIframe(viewportWidth);
|
|
152
|
+
const { iframeRef, loaded: iframeLoaded, onLoad: onIframeLoad } = useIframeLoadDetection();
|
|
153
|
+
return /* @__PURE__ */ jsxs(
|
|
154
|
+
"div",
|
|
155
|
+
{
|
|
156
|
+
className: "relative p-[2.5%]",
|
|
157
|
+
style: {
|
|
158
|
+
background: "linear-gradient(135deg, #e8e8ed 0%, #d1d1d6 100%)",
|
|
159
|
+
borderRadius: "18% / 8%",
|
|
160
|
+
boxShadow: "0 30px 60px -15px rgba(0,0,0,0.4), 0 0 0 1px rgba(255,255,255,0.9) inset, inset 0 1px 0 rgba(255,255,255,0.8)"
|
|
161
|
+
},
|
|
162
|
+
children: [
|
|
163
|
+
/* @__PURE__ */ jsx("div", { className: "absolute left-0 top-[18%] w-[1%] h-[6%] rounded-l-sm", style: { background: "#c8c8cd" } }),
|
|
164
|
+
/* @__PURE__ */ jsx("div", { className: "absolute left-0 top-[26%] w-[1%] h-[10%] rounded-l-sm", style: { background: "#c8c8cd" } }),
|
|
165
|
+
/* @__PURE__ */ jsx("div", { className: "absolute left-0 top-[38%] w-[1%] h-[10%] rounded-l-sm", style: { background: "#c8c8cd" } }),
|
|
166
|
+
/* @__PURE__ */ jsx("div", { className: "absolute right-0 top-[22%] w-[1%] h-[12%] rounded-r-sm", style: { background: "#c8c8cd" } }),
|
|
167
|
+
/* @__PURE__ */ jsxs(
|
|
168
|
+
"div",
|
|
169
|
+
{
|
|
170
|
+
className: "relative p-[3%]",
|
|
171
|
+
style: { background: "#0a0a0a", borderRadius: "14% / 6%" },
|
|
172
|
+
children: [
|
|
173
|
+
/* @__PURE__ */ jsx(
|
|
174
|
+
"div",
|
|
175
|
+
{
|
|
176
|
+
className: "absolute top-[2.5%] left-1/2 -translate-x-1/2 w-[28%] h-[3.5%] rounded-full z-10",
|
|
177
|
+
style: { background: "#1a1a1a" }
|
|
178
|
+
}
|
|
179
|
+
),
|
|
180
|
+
/* @__PURE__ */ jsxs("div", { ref: containerRef, className: "relative overflow-hidden", style: { borderRadius: "8% / 4%", aspectRatio: `${viewportWidth}/${viewportHeight}`, background: "#0a0a0a" }, children: [
|
|
181
|
+
/* @__PURE__ */ jsx(ScreenshotOverlay, { src: screenshot, alt: "Mobile preview", sizes: "(max-width: 1024px) 25vw, 150px", hidden: iframeLoaded }),
|
|
182
|
+
url && /* @__PURE__ */ jsx("iframe", { src: url, title: "Mobile", loading: "eager", scrolling: "no", tabIndex: -1, sandbox: "allow-scripts allow-same-origin", ref: iframeRef, onLoad: onIframeLoad, className: "border-0 pointer-events-none", style: { position: "absolute", top: 0, left: 0, width: `${viewportWidth}px`, height: `${viewportHeight}px`, transformOrigin: "top left", transform: `scale(${scale})` } })
|
|
183
|
+
] }),
|
|
184
|
+
/* @__PURE__ */ jsx(
|
|
185
|
+
"div",
|
|
186
|
+
{
|
|
187
|
+
className: "absolute bottom-[2%] left-1/2 -translate-x-1/2 w-[34%] h-[1%] rounded-full",
|
|
188
|
+
style: { background: "rgba(255,255,255,0.25)" }
|
|
189
|
+
}
|
|
190
|
+
)
|
|
191
|
+
]
|
|
192
|
+
}
|
|
193
|
+
)
|
|
194
|
+
]
|
|
195
|
+
}
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
function DeviceTrifolio({
|
|
199
|
+
url,
|
|
200
|
+
screenshots,
|
|
201
|
+
className = ""
|
|
202
|
+
}) {
|
|
203
|
+
const frameUrl = url || "";
|
|
204
|
+
return /* @__PURE__ */ jsxs(
|
|
205
|
+
"div",
|
|
206
|
+
{
|
|
207
|
+
"data-screenshot-target": "device-trifolio",
|
|
208
|
+
className: `relative w-full min-w-0 max-w-full overflow-x-clip ${className}`,
|
|
209
|
+
style: { aspectRatio: "11/5", minHeight: "200px" },
|
|
210
|
+
children: [
|
|
211
|
+
/* @__PURE__ */ jsx(
|
|
212
|
+
"div",
|
|
213
|
+
{
|
|
214
|
+
"data-device": "macbook",
|
|
215
|
+
className: "absolute left-1/2 -translate-x-1/2 bottom-0 w-[65%] z-10",
|
|
216
|
+
children: /* @__PURE__ */ jsx(MacBookFrame, { url: frameUrl, screenshot: screenshots?.desktop })
|
|
217
|
+
}
|
|
218
|
+
),
|
|
219
|
+
/* @__PURE__ */ jsx(
|
|
220
|
+
"div",
|
|
221
|
+
{
|
|
222
|
+
"data-device": "ipad",
|
|
223
|
+
className: "absolute left-[8%] bottom-0 w-[28%] z-20",
|
|
224
|
+
children: /* @__PURE__ */ jsx(IPadFrame, { url: frameUrl, screenshot: screenshots?.tablet })
|
|
225
|
+
}
|
|
226
|
+
),
|
|
227
|
+
/* @__PURE__ */ jsx(
|
|
228
|
+
"div",
|
|
229
|
+
{
|
|
230
|
+
"data-device": "iphone",
|
|
231
|
+
className: "absolute right-[10%] bottom-0 w-[14%] z-30",
|
|
232
|
+
children: /* @__PURE__ */ jsx(IPhoneFrame, { url: frameUrl, screenshot: screenshots?.mobile })
|
|
233
|
+
}
|
|
234
|
+
)
|
|
235
|
+
]
|
|
236
|
+
}
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
export {
|
|
240
|
+
DeviceTrifolio as default
|
|
241
|
+
};
|
|
242
|
+
//# sourceMappingURL=DeviceTrifolio.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/portfolio/components/primitives/DeviceTrifolio.tsx"],"sourcesContent":["'use client';\n\nimport React, { useRef, useEffect, useState, useCallback } from 'react';\nimport Image from 'next/image';\nimport { resolvePortfolioImage } from '../../../shared/images';\n\n/**\n * Screenshot layer inside a device frame. Sits above the live iframe and\n * fades out once the iframe verifies. The desktop shot is a likely LCP\n * element on desktop viewports, so it loads with priority.\n */\nfunction ScreenshotOverlay({\n src,\n alt,\n sizes,\n priority = false,\n hidden,\n}: {\n src?: string;\n alt: string;\n sizes: string;\n priority?: boolean;\n hidden: boolean;\n}) {\n const resolved = resolvePortfolioImage(src);\n if (!resolved) return null;\n return (\n <Image\n src={resolved.url}\n alt={alt}\n fill\n sizes={sizes}\n priority={priority}\n fetchPriority={priority ? 'high' : undefined}\n style={{\n objectFit: 'cover',\n objectPosition: 'top center',\n zIndex: 2,\n opacity: hidden ? 0 : 1,\n transition: 'opacity 0.8s ease',\n pointerEvents: 'none' as const,\n }}\n />\n );\n}\n\ninterface DeviceTrifolioProps {\n url?: string;\n screenshots?: {\n desktop?: string;\n tablet?: string;\n mobile?: string;\n };\n className?: string;\n}\n\n// ─── Hooks ───────────────────────────────────────────────────────────────────\n\nfunction useScaledIframe(viewportWidth: number) {\n const containerRef = useRef<HTMLDivElement>(null);\n const [scale, setScale] = useState(1);\n\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n const updateScale = () => {\n const containerWidth = container.offsetWidth;\n setScale(containerWidth / viewportWidth);\n };\n\n updateScale();\n const resizeObserver = new ResizeObserver(updateScale);\n resizeObserver.observe(container);\n return () => resizeObserver.disconnect();\n }, [viewportWidth]);\n\n return { containerRef, scale };\n}\n\nfunction useScrollSafeIframe() {\n const [isLoaded, setIsLoaded] = useState(false);\n const [iframeFailed, setIframeFailed] = useState(false);\n const iframeRef = useRef<HTMLIFrameElement>(null);\n const loadTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n const handleLoad = useCallback(() => {\n const iframe = iframeRef.current;\n if (!iframe) return;\n\n // Wait for iframe to paint, then verify it rendered real content\n loadTimerRef.current = setTimeout(() => {\n let isCrossOrigin = false;\n\n try {\n // Try accessing contentDocument — cross-origin sites throw here\n const doc = iframe.contentDocument;\n if (!doc || !doc.body) {\n setIframeFailed(true);\n return;\n }\n\n // Same-origin access succeeded. This means either:\n // 1. A same-origin site loaded (e.g., our own site in dev)\n // 2. Browser rendered its own error page (X-Frame-Options block, connection refused, etc.)\n //\n // Browser error pages are same-origin and have short/generic content.\n // Real sites have <title>, many DOM nodes, scripts, styles, etc.\n const title = doc.title || '';\n const bodyText = doc.body?.innerText?.trim() || '';\n const numElements = doc.querySelectorAll('*').length;\n\n // Heuristic: real sites have a meaningful title AND many DOM elements\n // Browser error pages have very few elements and generic/empty titles\n if (numElements > 20 && title.length > 0 && bodyText.length > 50) {\n setIsLoaded(true);\n } else {\n setIframeFailed(true);\n }\n } catch {\n isCrossOrigin = true;\n }\n\n if (isCrossOrigin) {\n // Cross-origin: the real remote site loaded (can't be a browser error page\n // since those are always same-origin). This is the success case.\n setIsLoaded(true);\n }\n }, 2000);\n }, []);\n\n const handleError = useCallback(() => {\n setIframeFailed(true);\n if (loadTimerRef.current) clearTimeout(loadTimerRef.current);\n }, []);\n\n // Cleanup timer on unmount\n useEffect(() => {\n return () => {\n if (loadTimerRef.current) clearTimeout(loadTimerRef.current);\n };\n }, []);\n\n const wrapperStyle: React.CSSProperties = {\n position: 'absolute',\n left: 0,\n top: 0,\n zIndex: 1,\n overflow: 'hidden',\n width: '100%',\n height: '100%',\n };\n\n const iframeStyle: React.CSSProperties = {};\n\n return { iframeRef, handleLoad, handleError, wrapperStyle, iframeStyle, isLoaded, iframeFailed };\n}\n\n// ─── Device Frames ───────────────────────────────────────────────────────────\n\n/**\n * Hook that detects if an iframe successfully loaded real content.\n * Cross-origin = success (real site loaded). Same-origin with minimal DOM = blocked/error.\n */\nfunction useIframeLoadDetection() {\n const [loaded, setLoaded] = useState(false);\n const iframeRef = useRef<HTMLIFrameElement>(null);\n\n const onLoad = useCallback(() => {\n const iframe = iframeRef.current;\n if (!iframe) return;\n\n // Delay check to let the iframe paint\n setTimeout(() => {\n try {\n // If we can access contentDocument, it's same-origin.\n // Real sites are cross-origin and throw here → catch block → success.\n const doc = iframe.contentDocument;\n if (!doc?.body) { return; } // blocked\n const numElements = doc.querySelectorAll('*').length;\n // Browser error pages have very few elements. Real same-origin sites have many.\n if (numElements > 20) setLoaded(true);\n } catch {\n // Cross-origin error = the real remote site loaded successfully\n setLoaded(true);\n }\n }, 1500);\n }, []);\n\n return { iframeRef, loaded, onLoad };\n}\n\nfunction MacBookFrame({ url, screenshot }: { url: string; screenshot?: string }) {\n const viewportWidth = 1440;\n const viewportHeight = 900;\n const { containerRef, scale } = useScaledIframe(viewportWidth);\n const { iframeRef, loaded: iframeLoaded, onLoad: onIframeLoad } = useIframeLoadDetection();\n\n return (\n <div className=\"w-full\">\n <div\n className=\"relative rounded-t-[10px] p-[4px] pb-0\"\n style={{\n background: 'linear-gradient(135deg, #e8e8ed 0%, #d1d1d6 100%)',\n boxShadow: '0 -4px 30px rgba(0,0,0,0.3), inset 0 1px 0 rgba(255,255,255,0.9)',\n }}\n >\n <div\n className=\"relative rounded-t-[7px] px-[8px] pt-[8px] pb-0\"\n style={{ background: '#0a0a0a' }}\n >\n <div\n className=\"absolute top-0 left-1/2 -translate-x-1/2 w-[70px] h-[18px] flex items-center justify-center z-10\"\n style={{ background: '#0a0a0a', borderRadius: '0 0 10px 10px' }}\n >\n <div className=\"w-[5px] h-[5px] rounded-full\" style={{ background: '#2a3a2a', boxShadow: '0 0 0 1px #1a1a1a' }} />\n </div>\n\n <div ref={containerRef} className=\"relative overflow-hidden rounded-t-[3px]\" style={{ aspectRatio: `${viewportWidth}/${viewportHeight}`, background: '#0a0a0a' }}>\n <ScreenshotOverlay src={screenshot} alt=\"Desktop preview\" sizes=\"(max-width: 1024px) 90vw, 640px\" priority hidden={iframeLoaded} />\n {url && <iframe src={url} title=\"Desktop\" loading=\"eager\" scrolling=\"no\" tabIndex={-1} sandbox=\"allow-scripts allow-same-origin\" ref={iframeRef} onLoad={onIframeLoad} className=\"border-0 pointer-events-none\" style={{ position: 'absolute', top: 0, left: 0, width: `${viewportWidth}px`, height: `${viewportHeight}px`, transformOrigin: 'top left', transform: `scale(${scale})` }} />}\n </div>\n </div>\n </div>\n\n {/* Black chin */}\n <div className=\"h-[8px]\" style={{ background: '#0a0a0a', boxShadow: '0 1px 4px rgba(0,0,0,0.3)' }} />\n\n {/* Silver base */}\n <div\n className=\"relative h-[12px] rounded-b-[10px]\"\n style={{\n background: 'linear-gradient(to bottom, #e0e0e5, #c8c8cd 20%, #b0b0b5 80%, #a0a0a5)',\n boxShadow: '0 4px 15px rgba(0,0,0,0.3)',\n }}\n >\n <div\n className=\"absolute top-0 left-1/2 -translate-x-1/2 w-[25%] h-[4px] rounded-b-[4px]\"\n style={{ background: 'linear-gradient(to bottom, #909095, #a8a8ad)' }}\n />\n </div>\n </div>\n );\n}\n\nfunction IPadFrame({ url, screenshot }: { url: string; screenshot?: string }) {\n const viewportWidth = 768;\n const viewportHeight = 1024;\n const { containerRef, scale } = useScaledIframe(viewportWidth);\n const { iframeRef, loaded: iframeLoaded, onLoad: onIframeLoad } = useIframeLoadDetection();\n\n return (\n <div className=\"relative p-[1.5%]\" style={{ background: 'linear-gradient(135deg, #e8e8ed 0%, #d1d1d6 100%)', borderRadius: '8% / 6%', boxShadow: '0 30px 60px -15px rgba(0,0,0,0.4), 0 0 0 1px rgba(255,255,255,0.9) inset, inset 0 1px 0 rgba(255,255,255,0.8)' }}>\n <div className=\"relative p-[2%]\" style={{ background: '#0a0a0a', borderRadius: '6% / 4.5%' }}>\n <div className=\"absolute top-[1.2%] left-1/2 -translate-x-1/2 w-[1.8%] aspect-square rounded-full z-10\" style={{ background: '#1a1a1f' }} />\n <div ref={containerRef} className=\"relative overflow-hidden\" style={{ borderRadius: '2%', aspectRatio: `${viewportWidth}/${viewportHeight}`, background: '#0a0a0a' }}>\n <ScreenshotOverlay src={screenshot} alt=\"Tablet preview\" sizes=\"(max-width: 1024px) 40vw, 240px\" hidden={iframeLoaded} />\n {url && <iframe src={url} title=\"Tablet\" loading=\"eager\" scrolling=\"no\" tabIndex={-1} sandbox=\"allow-scripts allow-same-origin\" ref={iframeRef} onLoad={onIframeLoad} className=\"border-0 pointer-events-none\" style={{ position: 'absolute', top: 0, left: 0, width: `${viewportWidth}px`, height: `${viewportHeight}px`, transformOrigin: 'top left', transform: `scale(${scale})` }} />}\n </div>\n </div>\n </div>\n );\n}\n\nfunction IPhoneFrame({ url, screenshot }: { url: string; screenshot?: string }) {\n const viewportWidth = 393;\n const viewportHeight = 852;\n const { containerRef, scale } = useScaledIframe(viewportWidth);\n const { iframeRef, loaded: iframeLoaded, onLoad: onIframeLoad } = useIframeLoadDetection();\n\n return (\n <div\n className=\"relative p-[2.5%]\"\n style={{\n background: 'linear-gradient(135deg, #e8e8ed 0%, #d1d1d6 100%)',\n borderRadius: '18% / 8%',\n boxShadow:\n '0 30px 60px -15px rgba(0,0,0,0.4), 0 0 0 1px rgba(255,255,255,0.9) inset, inset 0 1px 0 rgba(255,255,255,0.8)',\n }}\n >\n {/* Side buttons */}\n <div className=\"absolute left-0 top-[18%] w-[1%] h-[6%] rounded-l-sm\" style={{ background: '#c8c8cd' }} />\n <div className=\"absolute left-0 top-[26%] w-[1%] h-[10%] rounded-l-sm\" style={{ background: '#c8c8cd' }} />\n <div className=\"absolute left-0 top-[38%] w-[1%] h-[10%] rounded-l-sm\" style={{ background: '#c8c8cd' }} />\n <div className=\"absolute right-0 top-[22%] w-[1%] h-[12%] rounded-r-sm\" style={{ background: '#c8c8cd' }} />\n\n {/* Black bezel */}\n <div\n className=\"relative p-[3%]\"\n style={{ background: '#0a0a0a', borderRadius: '14% / 6%' }}\n >\n {/* Dynamic Island */}\n <div\n className=\"absolute top-[2.5%] left-1/2 -translate-x-1/2 w-[28%] h-[3.5%] rounded-full z-10\"\n style={{ background: '#1a1a1a' }}\n />\n\n <div ref={containerRef} className=\"relative overflow-hidden\" style={{ borderRadius: '8% / 4%', aspectRatio: `${viewportWidth}/${viewportHeight}`, background: '#0a0a0a' }}>\n <ScreenshotOverlay src={screenshot} alt=\"Mobile preview\" sizes=\"(max-width: 1024px) 25vw, 150px\" hidden={iframeLoaded} />\n {url && <iframe src={url} title=\"Mobile\" loading=\"eager\" scrolling=\"no\" tabIndex={-1} sandbox=\"allow-scripts allow-same-origin\" ref={iframeRef} onLoad={onIframeLoad} className=\"border-0 pointer-events-none\" style={{ position: 'absolute', top: 0, left: 0, width: `${viewportWidth}px`, height: `${viewportHeight}px`, transformOrigin: 'top left', transform: `scale(${scale})` }} />}\n </div>\n\n {/* Home indicator */}\n <div\n className=\"absolute bottom-[2%] left-1/2 -translate-x-1/2 w-[34%] h-[1%] rounded-full\"\n style={{ background: 'rgba(255,255,255,0.25)' }}\n />\n </div>\n </div>\n );\n}\n\n// ─── Main Component ──────────────────────────────────────────────────────────\n\nexport default function DeviceTrifolio({\n url,\n screenshots,\n className = '',\n}: DeviceTrifolioProps) {\n // Pass URL directly — iframes render immediately\n const frameUrl = url || '';\n\n // No entrance animation: the trifolio sits above the fold in the hero, and\n // hiding it post-hydration re-hides the desktop LCP candidate (house LCP\n // rule: above-fold content renders fully static).\n\n return (\n <div\n data-screenshot-target=\"device-trifolio\"\n className={`relative w-full min-w-0 max-w-full overflow-x-clip ${className}`}\n style={{ aspectRatio: '11/5', minHeight: '200px' }}\n >\n {/* MacBook Pro - Center Back */}\n <div\n data-device=\"macbook\"\n className=\"absolute left-1/2 -translate-x-1/2 bottom-0 w-[65%] z-10\"\n >\n <MacBookFrame url={frameUrl} screenshot={screenshots?.desktop} />\n </div>\n\n {/* iPad - Front Left */}\n <div\n data-device=\"ipad\"\n className=\"absolute left-[8%] bottom-0 w-[28%] z-20\"\n >\n <IPadFrame url={frameUrl} screenshot={screenshots?.tablet} />\n </div>\n\n {/* iPhone - Front Right */}\n <div\n data-device=\"iphone\"\n className=\"absolute right-[10%] bottom-0 w-[14%] z-30\"\n >\n <IPhoneFrame url={frameUrl} screenshot={screenshots?.mobile} />\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
"use strict";
|
|
3
|
+
"use client";
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
21
|
+
|
|
22
|
+
// src/portfolio/components/primitives/FrequencyBarSkeleton.tsx
|
|
23
|
+
var FrequencyBarSkeleton_exports = {};
|
|
24
|
+
__export(FrequencyBarSkeleton_exports, {
|
|
25
|
+
default: () => FrequencyBarSkeleton
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(FrequencyBarSkeleton_exports);
|
|
28
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
29
|
+
var FREQ_BAR_CSS = `
|
|
30
|
+
@keyframes sk-freq-bounce{0%,100%{transform:scaleY(var(--sk-freq-min,.3))}50%{transform:scaleY(var(--sk-freq-max,.8))}}
|
|
31
|
+
.sk-freq-bar{transform:scaleY(var(--sk-freq-min,.3));animation:sk-freq-bounce var(--sk-freq-dur,1.2s) ease-in-out var(--sk-freq-delay,0s) infinite}
|
|
32
|
+
@media (prefers-reduced-motion:reduce){.sk-freq-bar{animation:none;transform:scaleY(var(--sk-freq-max,.8))}}
|
|
33
|
+
`;
|
|
34
|
+
function pseudoRandom(index, salt) {
|
|
35
|
+
const x = Math.sin(index * 127.1 + salt * 311.7) * 43758.5453;
|
|
36
|
+
return x - Math.floor(x);
|
|
37
|
+
}
|
|
38
|
+
function FrequencyBarSkeleton({
|
|
39
|
+
className = "",
|
|
40
|
+
barCount = 5,
|
|
41
|
+
height = 40
|
|
42
|
+
}) {
|
|
43
|
+
const opacities = Array.from({ length: barCount }, (_, i) => {
|
|
44
|
+
const center = (barCount - 1) / 2;
|
|
45
|
+
const distance = Math.abs(i - center) / center;
|
|
46
|
+
return 1 - distance * 0.5;
|
|
47
|
+
});
|
|
48
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
49
|
+
"div",
|
|
50
|
+
{
|
|
51
|
+
className,
|
|
52
|
+
style: {
|
|
53
|
+
display: "flex",
|
|
54
|
+
alignItems: "flex-end",
|
|
55
|
+
justifyContent: "center",
|
|
56
|
+
gap: "3px",
|
|
57
|
+
height: `${height}px`
|
|
58
|
+
},
|
|
59
|
+
role: "status",
|
|
60
|
+
"aria-label": "Loading",
|
|
61
|
+
children: [
|
|
62
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("style", { href: "sk-freq-bar", precedence: "default", children: FREQ_BAR_CSS }),
|
|
63
|
+
Array.from({ length: barCount }, (_, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
64
|
+
"div",
|
|
65
|
+
{
|
|
66
|
+
className: "sk-freq-bar",
|
|
67
|
+
style: {
|
|
68
|
+
width: `${Math.max(3, Math.round(28 / barCount))}px`,
|
|
69
|
+
height: "100%",
|
|
70
|
+
borderRadius: "2px",
|
|
71
|
+
background: "var(--sk-primary, #6366f1)",
|
|
72
|
+
opacity: opacities[i],
|
|
73
|
+
transformOrigin: "bottom",
|
|
74
|
+
// Bounce range 0.2-0.4 → 0.6-1.0, full cycle 0.8-1.6s, 0.1s
|
|
75
|
+
// per-bar stagger — the same envelope as the old loop.
|
|
76
|
+
"--sk-freq-min": (0.2 + pseudoRandom(i, 1) * 0.2).toFixed(3),
|
|
77
|
+
"--sk-freq-max": (0.6 + pseudoRandom(i, 2) * 0.4).toFixed(3),
|
|
78
|
+
"--sk-freq-dur": `${((0.4 + pseudoRandom(i, 3) * 0.4) * 2).toFixed(3)}s`,
|
|
79
|
+
"--sk-freq-delay": `${(i * 0.1).toFixed(1)}s`
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
i
|
|
83
|
+
))
|
|
84
|
+
]
|
|
85
|
+
}
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=FrequencyBarSkeleton.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/portfolio/components/primitives/FrequencyBarSkeleton.tsx"],"sourcesContent":["'use client';\n\nimport React from 'react';\n\ninterface FrequencyBarSkeletonProps {\n className?: string;\n barCount?: number;\n height?: number;\n}\n\n// EQ bounce loop — pure CSS keyframes. Each bar oscillates between its own\n// min/max scaleY (set via custom properties) on its own duration and delay,\n// so the bars stay out of phase. The base transform holds each bar at its\n// min scale while its stagger delay elapses; ease-in-out ≈ the old sine\n// wave. Reduced motion freezes the EQ at the bars' max scales.\nconst FREQ_BAR_CSS = `\n@keyframes sk-freq-bounce{0%,100%{transform:scaleY(var(--sk-freq-min,.3))}50%{transform:scaleY(var(--sk-freq-max,.8))}}\n.sk-freq-bar{transform:scaleY(var(--sk-freq-min,.3));animation:sk-freq-bounce var(--sk-freq-dur,1.2s) ease-in-out var(--sk-freq-delay,0s) infinite}\n@media (prefers-reduced-motion:reduce){.sk-freq-bar{animation:none;transform:scaleY(var(--sk-freq-max,.8))}}\n`;\n\n// Deterministic per-bar \"random\" — SSR and client must render identical\n// style attributes, so a real Math.random() in render would cause\n// hydration mismatches.\nfunction pseudoRandom(index: number, salt: number): number {\n const x = Math.sin(index * 127.1 + salt * 311.7) * 43758.5453;\n return x - Math.floor(x);\n}\n\nexport default function FrequencyBarSkeleton({\n className = '',\n barCount = 5,\n height = 40,\n}: FrequencyBarSkeletonProps) {\n const opacities = Array.from({ length: barCount }, (_, i) => {\n // Create a bell curve of opacities, highest in center\n const center = (barCount - 1) / 2;\n const distance = Math.abs(i - center) / center;\n return 1 - distance * 0.5;\n });\n\n return (\n <div\n className={className}\n style={{\n display: 'flex',\n alignItems: 'flex-end',\n justifyContent: 'center',\n gap: '3px',\n height: `${height}px`,\n }}\n role=\"status\"\n aria-label=\"Loading\"\n >\n {/* React 19 hoists + dedupes via href/precedence; React 18 renders it\n inline per skeleton (harmless duplication). */}\n <style href=\"sk-freq-bar\" precedence=\"default\">\n {FREQ_BAR_CSS}\n </style>\n {Array.from({ length: barCount }, (_, i) => (\n <div\n key={i}\n className=\"sk-freq-bar\"\n style={\n {\n width: `${Math.max(3, Math.round(28 / barCount))}px`,\n height: '100%',\n borderRadius: '2px',\n background: 'var(--sk-primary, #6366f1)',\n opacity: opacities[i],\n transformOrigin: 'bottom',\n // Bounce range 0.2-0.4 → 0.6-1.0, full cycle 0.8-1.6s, 0.1s\n // per-bar stagger — the same envelope as the old loop.\n '--sk-freq-min': (0.2 + pseudoRandom(i, 1) * 0.2).toFixed(3),\n '--sk-freq-max': (0.6 + pseudoRandom(i, 2) * 0.4).toFixed(3),\n '--sk-freq-dur': `${((0.4 + pseudoRandom(i, 3) * 0.4) * 2).toFixed(3)}s`,\n '--sk-freq-delay': `${(i * 0.1).toFixed(1)}s`,\n } as React.CSSProperties\n }\n />\n ))}\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AA0CI;AA3BJ,IAAM,eAAe;AAAA;AAAA;AAAA;AAAA;AASrB,SAAS,aAAa,OAAe,MAAsB;AACzD,QAAM,IAAI,KAAK,IAAI,QAAQ,QAAQ,OAAO,KAAK,IAAI;AACnD,SAAO,IAAI,KAAK,MAAM,CAAC;AACzB;AAEe,SAAR,qBAAsC;AAAA,EAC3C,YAAY;AAAA,EACZ,WAAW;AAAA,EACX,SAAS;AACX,GAA8B;AAC5B,QAAM,YAAY,MAAM,KAAK,EAAE,QAAQ,SAAS,GAAG,CAAC,GAAG,MAAM;AAE3D,UAAM,UAAU,WAAW,KAAK;AAChC,UAAM,WAAW,KAAK,IAAI,IAAI,MAAM,IAAI;AACxC,WAAO,IAAI,WAAW;AAAA,EACxB,CAAC;AAED,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,OAAO;AAAA,QACL,SAAS;AAAA,QACT,YAAY;AAAA,QACZ,gBAAgB;AAAA,QAChB,KAAK;AAAA,QACL,QAAQ,GAAG,MAAM;AAAA,MACnB;AAAA,MACA,MAAK;AAAA,MACL,cAAW;AAAA,MAIX;AAAA,oDAAC,WAAM,MAAK,eAAc,YAAW,WAClC,wBACH;AAAA,QACC,MAAM,KAAK,EAAE,QAAQ,SAAS,GAAG,CAAC,GAAG,MACpC;AAAA,UAAC;AAAA;AAAA,YAEC,WAAU;AAAA,YACV,OACE;AAAA,cACE,OAAO,GAAG,KAAK,IAAI,GAAG,KAAK,MAAM,KAAK,QAAQ,CAAC,CAAC;AAAA,cAChD,QAAQ;AAAA,cACR,cAAc;AAAA,cACd,YAAY;AAAA,cACZ,SAAS,UAAU,CAAC;AAAA,cACpB,iBAAiB;AAAA;AAAA;AAAA,cAGjB,kBAAkB,MAAM,aAAa,GAAG,CAAC,IAAI,KAAK,QAAQ,CAAC;AAAA,cAC3D,kBAAkB,MAAM,aAAa,GAAG,CAAC,IAAI,KAAK,QAAQ,CAAC;AAAA,cAC3D,iBAAiB,KAAK,MAAM,aAAa,GAAG,CAAC,IAAI,OAAO,GAAG,QAAQ,CAAC,CAAC;AAAA,cACrE,mBAAmB,IAAI,IAAI,KAAK,QAAQ,CAAC,CAAC;AAAA,YAC5C;AAAA;AAAA,UAhBG;AAAA,QAkBP,CACD;AAAA;AAAA;AAAA,EACH;AAEJ;","names":[]}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
interface FrequencyBarSkeletonProps {
|
|
4
|
+
className?: string;
|
|
5
|
+
barCount?: number;
|
|
6
|
+
height?: number;
|
|
7
|
+
}
|
|
8
|
+
declare function FrequencyBarSkeleton({ className, barCount, height, }: FrequencyBarSkeletonProps): React.JSX.Element;
|
|
9
|
+
|
|
10
|
+
export { FrequencyBarSkeleton as default };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
interface FrequencyBarSkeletonProps {
|
|
4
|
+
className?: string;
|
|
5
|
+
barCount?: number;
|
|
6
|
+
height?: number;
|
|
7
|
+
}
|
|
8
|
+
declare function FrequencyBarSkeleton({ className, barCount, height, }: FrequencyBarSkeletonProps): React.JSX.Element;
|
|
9
|
+
|
|
10
|
+
export { FrequencyBarSkeleton as default };
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
"use client";
|
|
3
|
+
|
|
4
|
+
// src/portfolio/components/primitives/FrequencyBarSkeleton.tsx
|
|
5
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
+
var FREQ_BAR_CSS = `
|
|
7
|
+
@keyframes sk-freq-bounce{0%,100%{transform:scaleY(var(--sk-freq-min,.3))}50%{transform:scaleY(var(--sk-freq-max,.8))}}
|
|
8
|
+
.sk-freq-bar{transform:scaleY(var(--sk-freq-min,.3));animation:sk-freq-bounce var(--sk-freq-dur,1.2s) ease-in-out var(--sk-freq-delay,0s) infinite}
|
|
9
|
+
@media (prefers-reduced-motion:reduce){.sk-freq-bar{animation:none;transform:scaleY(var(--sk-freq-max,.8))}}
|
|
10
|
+
`;
|
|
11
|
+
function pseudoRandom(index, salt) {
|
|
12
|
+
const x = Math.sin(index * 127.1 + salt * 311.7) * 43758.5453;
|
|
13
|
+
return x - Math.floor(x);
|
|
14
|
+
}
|
|
15
|
+
function FrequencyBarSkeleton({
|
|
16
|
+
className = "",
|
|
17
|
+
barCount = 5,
|
|
18
|
+
height = 40
|
|
19
|
+
}) {
|
|
20
|
+
const opacities = Array.from({ length: barCount }, (_, i) => {
|
|
21
|
+
const center = (barCount - 1) / 2;
|
|
22
|
+
const distance = Math.abs(i - center) / center;
|
|
23
|
+
return 1 - distance * 0.5;
|
|
24
|
+
});
|
|
25
|
+
return /* @__PURE__ */ jsxs(
|
|
26
|
+
"div",
|
|
27
|
+
{
|
|
28
|
+
className,
|
|
29
|
+
style: {
|
|
30
|
+
display: "flex",
|
|
31
|
+
alignItems: "flex-end",
|
|
32
|
+
justifyContent: "center",
|
|
33
|
+
gap: "3px",
|
|
34
|
+
height: `${height}px`
|
|
35
|
+
},
|
|
36
|
+
role: "status",
|
|
37
|
+
"aria-label": "Loading",
|
|
38
|
+
children: [
|
|
39
|
+
/* @__PURE__ */ jsx("style", { href: "sk-freq-bar", precedence: "default", children: FREQ_BAR_CSS }),
|
|
40
|
+
Array.from({ length: barCount }, (_, i) => /* @__PURE__ */ jsx(
|
|
41
|
+
"div",
|
|
42
|
+
{
|
|
43
|
+
className: "sk-freq-bar",
|
|
44
|
+
style: {
|
|
45
|
+
width: `${Math.max(3, Math.round(28 / barCount))}px`,
|
|
46
|
+
height: "100%",
|
|
47
|
+
borderRadius: "2px",
|
|
48
|
+
background: "var(--sk-primary, #6366f1)",
|
|
49
|
+
opacity: opacities[i],
|
|
50
|
+
transformOrigin: "bottom",
|
|
51
|
+
// Bounce range 0.2-0.4 → 0.6-1.0, full cycle 0.8-1.6s, 0.1s
|
|
52
|
+
// per-bar stagger — the same envelope as the old loop.
|
|
53
|
+
"--sk-freq-min": (0.2 + pseudoRandom(i, 1) * 0.2).toFixed(3),
|
|
54
|
+
"--sk-freq-max": (0.6 + pseudoRandom(i, 2) * 0.4).toFixed(3),
|
|
55
|
+
"--sk-freq-dur": `${((0.4 + pseudoRandom(i, 3) * 0.4) * 2).toFixed(3)}s`,
|
|
56
|
+
"--sk-freq-delay": `${(i * 0.1).toFixed(1)}s`
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
i
|
|
60
|
+
))
|
|
61
|
+
]
|
|
62
|
+
}
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
export {
|
|
66
|
+
FrequencyBarSkeleton as default
|
|
67
|
+
};
|
|
68
|
+
//# sourceMappingURL=FrequencyBarSkeleton.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/portfolio/components/primitives/FrequencyBarSkeleton.tsx"],"sourcesContent":["'use client';\n\nimport React from 'react';\n\ninterface FrequencyBarSkeletonProps {\n className?: string;\n barCount?: number;\n height?: number;\n}\n\n// EQ bounce loop — pure CSS keyframes. Each bar oscillates between its own\n// min/max scaleY (set via custom properties) on its own duration and delay,\n// so the bars stay out of phase. The base transform holds each bar at its\n// min scale while its stagger delay elapses; ease-in-out ≈ the old sine\n// wave. Reduced motion freezes the EQ at the bars' max scales.\nconst FREQ_BAR_CSS = `\n@keyframes sk-freq-bounce{0%,100%{transform:scaleY(var(--sk-freq-min,.3))}50%{transform:scaleY(var(--sk-freq-max,.8))}}\n.sk-freq-bar{transform:scaleY(var(--sk-freq-min,.3));animation:sk-freq-bounce var(--sk-freq-dur,1.2s) ease-in-out var(--sk-freq-delay,0s) infinite}\n@media (prefers-reduced-motion:reduce){.sk-freq-bar{animation:none;transform:scaleY(var(--sk-freq-max,.8))}}\n`;\n\n// Deterministic per-bar \"random\" — SSR and client must render identical\n// style attributes, so a real Math.random() in render would cause\n// hydration mismatches.\nfunction pseudoRandom(index: number, salt: number): number {\n const x = Math.sin(index * 127.1 + salt * 311.7) * 43758.5453;\n return x - Math.floor(x);\n}\n\nexport default function FrequencyBarSkeleton({\n className = '',\n barCount = 5,\n height = 40,\n}: FrequencyBarSkeletonProps) {\n const opacities = Array.from({ length: barCount }, (_, i) => {\n // Create a bell curve of opacities, highest in center\n const center = (barCount - 1) / 2;\n const distance = Math.abs(i - center) / center;\n return 1 - distance * 0.5;\n });\n\n return (\n <div\n className={className}\n style={{\n display: 'flex',\n alignItems: 'flex-end',\n justifyContent: 'center',\n gap: '3px',\n height: `${height}px`,\n }}\n role=\"status\"\n aria-label=\"Loading\"\n >\n {/* React 19 hoists + dedupes via href/precedence; React 18 renders it\n inline per skeleton (harmless duplication). */}\n <style href=\"sk-freq-bar\" precedence=\"default\">\n {FREQ_BAR_CSS}\n </style>\n {Array.from({ length: barCount }, (_, i) => (\n <div\n key={i}\n className=\"sk-freq-bar\"\n style={\n {\n width: `${Math.max(3, Math.round(28 / barCount))}px`,\n height: '100%',\n borderRadius: '2px',\n background: 'var(--sk-primary, #6366f1)',\n opacity: opacities[i],\n transformOrigin: 'bottom',\n // Bounce range 0.2-0.4 → 0.6-1.0, full cycle 0.8-1.6s, 0.1s\n // per-bar stagger — the same envelope as the old loop.\n '--sk-freq-min': (0.2 + pseudoRandom(i, 1) * 0.2).toFixed(3),\n '--sk-freq-max': (0.6 + pseudoRandom(i, 2) * 0.4).toFixed(3),\n '--sk-freq-dur': `${((0.4 + pseudoRandom(i, 3) * 0.4) * 2).toFixed(3)}s`,\n '--sk-freq-delay': `${(i * 0.1).toFixed(1)}s`,\n } as React.CSSProperties\n }\n />\n ))}\n </div>\n );\n}\n"],"mappings":";;;;AA0CI,SAcE,KAdF;AA3BJ,IAAM,eAAe;AAAA;AAAA;AAAA;AAAA;AASrB,SAAS,aAAa,OAAe,MAAsB;AACzD,QAAM,IAAI,KAAK,IAAI,QAAQ,QAAQ,OAAO,KAAK,IAAI;AACnD,SAAO,IAAI,KAAK,MAAM,CAAC;AACzB;AAEe,SAAR,qBAAsC;AAAA,EAC3C,YAAY;AAAA,EACZ,WAAW;AAAA,EACX,SAAS;AACX,GAA8B;AAC5B,QAAM,YAAY,MAAM,KAAK,EAAE,QAAQ,SAAS,GAAG,CAAC,GAAG,MAAM;AAE3D,UAAM,UAAU,WAAW,KAAK;AAChC,UAAM,WAAW,KAAK,IAAI,IAAI,MAAM,IAAI;AACxC,WAAO,IAAI,WAAW;AAAA,EACxB,CAAC;AAED,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,OAAO;AAAA,QACL,SAAS;AAAA,QACT,YAAY;AAAA,QACZ,gBAAgB;AAAA,QAChB,KAAK;AAAA,QACL,QAAQ,GAAG,MAAM;AAAA,MACnB;AAAA,MACA,MAAK;AAAA,MACL,cAAW;AAAA,MAIX;AAAA,4BAAC,WAAM,MAAK,eAAc,YAAW,WAClC,wBACH;AAAA,QACC,MAAM,KAAK,EAAE,QAAQ,SAAS,GAAG,CAAC,GAAG,MACpC;AAAA,UAAC;AAAA;AAAA,YAEC,WAAU;AAAA,YACV,OACE;AAAA,cACE,OAAO,GAAG,KAAK,IAAI,GAAG,KAAK,MAAM,KAAK,QAAQ,CAAC,CAAC;AAAA,cAChD,QAAQ;AAAA,cACR,cAAc;AAAA,cACd,YAAY;AAAA,cACZ,SAAS,UAAU,CAAC;AAAA,cACpB,iBAAiB;AAAA;AAAA;AAAA,cAGjB,kBAAkB,MAAM,aAAa,GAAG,CAAC,IAAI,KAAK,QAAQ,CAAC;AAAA,cAC3D,kBAAkB,MAAM,aAAa,GAAG,CAAC,IAAI,KAAK,QAAQ,CAAC;AAAA,cAC3D,iBAAiB,KAAK,MAAM,aAAa,GAAG,CAAC,IAAI,OAAO,GAAG,QAAQ,CAAC,CAAC;AAAA,cACrE,mBAAmB,IAAI,IAAI,KAAK,QAAQ,CAAC,CAAC;AAAA,YAC5C;AAAA;AAAA,UAhBG;AAAA,QAkBP,CACD;AAAA;AAAA;AAAA,EACH;AAEJ;","names":[]}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
4
|
+
|
|
5
|
+
// src/portfolio/components/primitives/GlassCard.tsx
|
|
6
|
+
var paddingMap = {
|
|
7
|
+
sm: "16px",
|
|
8
|
+
md: "24px",
|
|
9
|
+
lg: "32px"
|
|
10
|
+
};
|
|
11
|
+
var HOVER_CSS = `
|
|
12
|
+
.sk-glass-card{transition:transform .3s cubic-bezier(.25,.46,.45,.94),box-shadow .3s cubic-bezier(.25,.46,.45,.94)}
|
|
13
|
+
.sk-glass-card--hover:hover{transform:translateY(-6px) scale(1.015);box-shadow:0 20px 40px -12px rgba(0,0,0,.25),0 0 0 1px var(--sk-primary,#6366f1)}
|
|
14
|
+
@media (prefers-reduced-motion:reduce){.sk-glass-card{transition:none}.sk-glass-card--hover:hover{transform:none}}
|
|
15
|
+
`;
|
|
16
|
+
function GlassCard({
|
|
17
|
+
children,
|
|
18
|
+
className = "",
|
|
19
|
+
hover = true,
|
|
20
|
+
padding = "md"
|
|
21
|
+
}) {
|
|
22
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
23
|
+
"div",
|
|
24
|
+
{
|
|
25
|
+
className: `sk-glass-card ${hover ? "sk-glass-card--hover" : ""} ${className}`,
|
|
26
|
+
style: {
|
|
27
|
+
padding: paddingMap[padding],
|
|
28
|
+
background: "var(--sk-surface, rgba(255,255,255,0.05))",
|
|
29
|
+
backdropFilter: "blur(16px)",
|
|
30
|
+
WebkitBackdropFilter: "blur(16px)",
|
|
31
|
+
border: "1px solid var(--sk-border, rgba(255,255,255,0.1))",
|
|
32
|
+
borderRadius: "16px",
|
|
33
|
+
boxShadow: "0 4px 16px -4px rgba(0,0,0,0.1), 0 0 0 1px var(--sk-border, rgba(255,255,255,0.1))"
|
|
34
|
+
},
|
|
35
|
+
children: [
|
|
36
|
+
/* @__PURE__ */ jsxRuntime.jsx("style", { href: "sk-glass-card", precedence: "default", children: HOVER_CSS }),
|
|
37
|
+
children
|
|
38
|
+
]
|
|
39
|
+
}
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = GlassCard;
|
|
44
|
+
//# sourceMappingURL=GlassCard.cjs.map
|
|
45
|
+
//# sourceMappingURL=GlassCard.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/portfolio/components/primitives/GlassCard.tsx"],"names":["jsxs","jsx"],"mappings":";;;;;AAeA,IAAM,UAAA,GAAa;AAAA,EACjB,EAAA,EAAI,MAAA;AAAA,EACJ,EAAA,EAAI,MAAA;AAAA,EACJ,EAAA,EAAI;AACN,CAAA;AAEA,IAAM,SAAA,GAAY;AAAA;AAAA;AAAA;AAAA,CAAA;AAMH,SAAR,SAAA,CAA2B;AAAA,EAChC,QAAA;AAAA,EACA,SAAA,GAAY,EAAA;AAAA,EACZ,KAAA,GAAQ,IAAA;AAAA,EACR,OAAA,GAAU;AACZ,CAAA,EAAmB;AACjB,EAAA,uBACEA,eAAA;AAAA,IAAC,KAAA;AAAA,IAAA;AAAA,MACC,WAAW,CAAA,cAAA,EAAiB,KAAA,GAAQ,sBAAA,GAAyB,EAAE,IAAI,SAAS,CAAA,CAAA;AAAA,MAC5E,KAAA,EAAO;AAAA,QACL,OAAA,EAAS,WAAW,OAAO,CAAA;AAAA,QAC3B,UAAA,EAAY,2CAAA;AAAA,QACZ,cAAA,EAAgB,YAAA;AAAA,QAChB,oBAAA,EAAsB,YAAA;AAAA,QACtB,MAAA,EAAQ,mDAAA;AAAA,QACR,YAAA,EAAc,MAAA;AAAA,QACd,SAAA,EAAW;AAAA,OACb;AAAA,MAIA,QAAA,EAAA;AAAA,wBAAAC,cAAA,CAAC,OAAA,EAAA,EAAM,IAAA,EAAK,eAAA,EAAgB,UAAA,EAAW,WACpC,QAAA,EAAA,SAAA,EACH,CAAA;AAAA,QACC;AAAA;AAAA;AAAA,GACH;AAEJ","file":"GlassCard.cjs","sourcesContent":["// Shared component — NO 'use client' directive on purpose: GlassCard has no\n// hooks, handlers, or browser APIs, so it renders in BOTH graphs. Server\n// sections use it server-side (zero JS); client sections pull it into their\n// graph. The hover lift is pure CSS. Keep it dependency-free — adding a hook\n// here would force every server section that uses it back to the client.\n\nimport React from 'react';\n\ninterface GlassCardProps {\n children: React.ReactNode;\n className?: string;\n hover?: boolean;\n padding?: 'sm' | 'md' | 'lg';\n}\n\nconst paddingMap = {\n sm: '16px',\n md: '24px',\n lg: '32px',\n} as const;\n\nconst HOVER_CSS = `\n.sk-glass-card{transition:transform .3s cubic-bezier(.25,.46,.45,.94),box-shadow .3s cubic-bezier(.25,.46,.45,.94)}\n.sk-glass-card--hover:hover{transform:translateY(-6px) scale(1.015);box-shadow:0 20px 40px -12px rgba(0,0,0,.25),0 0 0 1px var(--sk-primary,#6366f1)}\n@media (prefers-reduced-motion:reduce){.sk-glass-card{transition:none}.sk-glass-card--hover:hover{transform:none}}\n`;\n\nexport default function GlassCard({\n children,\n className = '',\n hover = true,\n padding = 'md',\n}: GlassCardProps) {\n return (\n <div\n className={`sk-glass-card ${hover ? 'sk-glass-card--hover' : ''} ${className}`}\n style={{\n padding: paddingMap[padding],\n background: 'var(--sk-surface, rgba(255,255,255,0.05))',\n backdropFilter: 'blur(16px)',\n WebkitBackdropFilter: 'blur(16px)',\n border: '1px solid var(--sk-border, rgba(255,255,255,0.1))',\n borderRadius: '16px',\n boxShadow: '0 4px 16px -4px rgba(0,0,0,0.1), 0 0 0 1px var(--sk-border, rgba(255,255,255,0.1))',\n }}\n >\n {/* React 19 hoists + dedupes via href/precedence; React 18 renders it\n inline per card (harmless duplication). */}\n <style href=\"sk-glass-card\" precedence=\"default\">\n {HOVER_CSS}\n </style>\n {children}\n </div>\n );\n}\n"]}
|