astro-component-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.
- package/README.md +133 -36
- package/package.json +17 -4
- package/src/components/index.ts +18 -202
- package/src/components/lib/Galaxy3D.astro +249 -137
- package/src/components/lib/app/AppSidebar.astro +249 -0
- package/src/components/lib/app/AppTopbar.astro +284 -0
- package/src/components/lib/app/ArrivalsBoard.astro +176 -0
- package/src/components/lib/app/AuthCard.astro +213 -0
- package/src/components/lib/app/DataTable.astro +346 -0
- package/src/components/lib/app/GuestProfileCard.astro +133 -0
- package/src/components/lib/app/HousekeepingBoard.astro +189 -0
- package/src/components/lib/app/PageHeader.astro +157 -0
- package/src/components/lib/app/PlanUsageCard.astro +112 -0
- package/src/components/lib/app/RoomRack.astro +207 -0
- package/src/components/lib/app/SettingsSection.astro +96 -0
- package/src/components/lib/app/SwitchRow.astro +71 -0
- package/src/components/lib/app/TeamMembers.astro +190 -0
- package/src/components/lib/app/app-icons.ts +52 -0
- package/src/components/lib/app/index.ts +19 -0
- package/src/components/lib/buttons/NeonBorderButton.astro +162 -0
- package/src/components/lib/buttons/RippleButton.astro +199 -0
- package/src/components/lib/buttons/SlideRevealButton.astro +159 -0
- package/src/components/lib/buttons/index.ts +4 -0
- package/src/components/lib/cards/GithubRepoCard.astro +176 -0
- package/src/components/lib/cards/PricingCompareMiniCard.astro +196 -0
- package/src/components/lib/cards/StatsTrendCard.astro +149 -0
- package/src/components/lib/cards/index.ts +3 -0
- package/src/components/lib/charts/ActivityRings.astro +105 -0
- package/src/components/lib/charts/AreaChart.astro +4 -4
- package/src/components/lib/charts/BarChart.astro +3 -3
- package/src/components/lib/charts/BoxPlotChart.astro +109 -0
- package/src/components/lib/charts/BubbleChart.astro +101 -0
- package/src/components/lib/charts/BulletChart.astro +160 -0
- package/src/components/lib/charts/BumpChart.astro +92 -0
- package/src/components/lib/charts/CandlestickChart.astro +105 -0
- package/src/components/lib/charts/ChartFrame.astro +373 -0
- package/src/components/lib/charts/DivergingBarChart.astro +98 -0
- package/src/components/lib/charts/DonutChart.astro +2 -2
- package/src/components/lib/charts/DumbbellChart.astro +92 -0
- package/src/components/lib/charts/ForecastLineChart.astro +125 -0
- package/src/components/lib/charts/FunnelChart.astro +122 -0
- package/src/components/lib/charts/GaugeChart.astro +111 -0
- package/src/components/lib/charts/GoalProgressChart.astro +97 -0
- package/src/components/lib/charts/GroupedBarChart.astro +125 -0
- package/src/components/lib/charts/HeatmapChart.astro +131 -0
- package/src/components/lib/charts/HistogramChart.astro +111 -0
- package/src/components/lib/charts/HorizontalBarChart.astro +155 -0
- package/src/components/lib/charts/KpiStatTile.astro +116 -0
- package/src/components/lib/charts/KpiTrendGrid.astro +60 -0
- package/src/components/lib/charts/LineChart.astro +160 -0
- package/src/components/lib/charts/LinearProgressGauge.astro +136 -0
- package/src/components/lib/charts/LollipopChart.astro +100 -0
- package/src/components/lib/charts/MeterGauge.astro +122 -0
- package/src/components/lib/charts/MetricDeltaTable.astro +125 -0
- package/src/components/lib/charts/PieChart.astro +135 -0
- package/src/components/lib/charts/PolarAreaChart.astro +85 -0
- package/src/components/lib/charts/PopulationPyramid.astro +122 -0
- package/src/components/lib/charts/PunchCardChart.astro +89 -0
- package/src/components/lib/charts/RadarChart.astro +94 -0
- package/src/components/lib/charts/RadarMetricChart.astro +168 -0
- package/src/components/lib/charts/RadialBarChart.astro +124 -0
- package/src/components/lib/charts/RangeAreaChart.astro +113 -0
- package/src/components/lib/charts/RetentionCohortTable.astro +126 -0
- package/src/components/lib/charts/SankeyChart.astro +128 -0
- package/src/components/lib/charts/ScatterPlot.astro +139 -0
- package/src/components/lib/charts/SegmentedBarChart.astro +119 -0
- package/src/components/lib/charts/SlopeChart.astro +105 -0
- package/src/components/lib/charts/SparklineChart.astro +87 -0
- package/src/components/lib/charts/StackedAreaChart.astro +130 -0
- package/src/components/lib/charts/StackedBarChart.astro +136 -0
- package/src/components/lib/charts/SunburstChart.astro +103 -0
- package/src/components/lib/charts/TreemapChart.astro +94 -0
- package/src/components/lib/charts/WaffleChart.astro +121 -0
- package/src/components/lib/charts/WaterfallChart.astro +118 -0
- package/src/components/lib/charts/chart-tooltip.ts +75 -0
- package/src/components/lib/charts/chart-utils.ts +271 -0
- package/src/components/lib/charts/index.ts +46 -0
- package/src/components/lib/data/ActivityHeatmapGrid.astro +157 -0
- package/src/components/lib/data/MetricComparisonRow.astro +109 -0
- package/src/components/lib/data/StatusTimelineFeed.astro +168 -0
- package/src/components/lib/data/index.ts +3 -0
- package/src/components/lib/ecommerce/CartDrawer.astro +483 -0
- package/src/components/lib/ecommerce/CartPageList.astro +290 -0
- package/src/components/lib/ecommerce/CartToast.astro +276 -0
- package/src/components/lib/ecommerce/CategoryTiles.astro +143 -0
- package/src/components/lib/ecommerce/CheckoutForm.astro +423 -0
- package/src/components/lib/ecommerce/CountdownBanner.astro +230 -0
- package/src/components/lib/ecommerce/FilterBar.astro +278 -0
- package/src/components/lib/ecommerce/FilterSidebar.astro +405 -0
- package/src/components/lib/ecommerce/LiveActivityToast.astro +213 -0
- package/src/components/lib/ecommerce/OrderSummary.astro +295 -0
- package/src/components/lib/ecommerce/ProductBuyBox.astro +247 -0
- package/src/components/lib/ecommerce/ProductCardMinimal.astro +229 -0
- package/src/components/lib/ecommerce/ProductGallery.astro +145 -0
- package/src/components/lib/ecommerce/ProductGrid.astro +118 -0
- package/src/components/lib/ecommerce/ProductMedia.astro +113 -0
- package/src/components/lib/ecommerce/ProductRating.astro +78 -0
- package/src/components/lib/ecommerce/ProductToolbar.astro +93 -0
- package/src/components/lib/ecommerce/PromoBar.astro +145 -0
- package/src/components/lib/ecommerce/StockIndicator.astro +127 -0
- package/src/components/lib/ecommerce/StoreHeader.astro +446 -0
- package/src/components/lib/ecommerce/StoreHero.astro +260 -0
- package/src/components/lib/ecommerce/StoreProductCard.astro +307 -0
- package/src/components/lib/ecommerce/TrustBadges.astro +101 -0
- package/src/components/lib/ecommerce/VariantPicker.astro +145 -0
- package/src/components/lib/ecommerce/cart-store.ts +151 -0
- package/src/components/lib/ecommerce/catalog-filter.ts +62 -0
- package/src/components/lib/ecommerce/index.ts +34 -0
- package/src/components/lib/ecommerce/product-icons.ts +28 -0
- package/src/components/lib/ecommerce/types.ts +70 -0
- package/src/components/lib/feedback/ProgressRingCounter.astro +118 -0
- package/src/components/lib/feedback/ToastNotificationGroup.astro +173 -0
- package/src/components/lib/feedback/index.ts +2 -0
- package/src/components/lib/forms/AnimatedInput.astro +4 -2
- package/src/components/lib/forms/CustomRadioGroup.astro +3 -1
- package/src/components/lib/forms/GlassInlineToggleGroup.astro +161 -0
- package/src/components/lib/forms/PasswordStrengthInput.astro +242 -0
- package/src/components/lib/forms/PhoneCountryInput.astro +145 -0
- package/src/components/lib/forms/TagsAutocompleteInput.astro +249 -0
- package/src/components/lib/forms/index.ts +5 -0
- package/src/components/lib/index.ts +2 -0
- package/src/components/lib/layout/BentoGrid3.astro +6 -4
- package/src/components/lib/layout/CenterFixedBox.astro +2 -2
- package/src/components/lib/layout/CenteredContainer.astro +4 -2
- package/src/components/lib/layout/ClusterHorizontal.astro +2 -2
- package/src/components/lib/layout/DiagonalSectionDivider.astro +69 -0
- package/src/components/lib/layout/FeatureHero.astro +4 -2
- package/src/components/lib/layout/FooterCenteredMinimal.astro +4 -2
- package/src/components/lib/layout/FullBleedSection.astro +2 -2
- package/src/components/lib/layout/GalleryGrid4.astro +2 -2
- package/src/components/lib/layout/HolyGrailGrid.astro +127 -0
- package/src/components/lib/layout/LandingSectionSplit.astro +5 -3
- package/src/components/lib/layout/MasonryGrid.astro +2 -2
- package/src/components/lib/layout/MinimalFooterGrid.astro +1 -1
- package/src/components/lib/layout/ScrollSnapSection.astro +1 -1
- package/src/components/lib/layout/SidebarLayoutWrapper.astro +4 -2
- package/src/components/lib/layout/SplitScreenLayout.astro +3 -3
- package/src/components/lib/layout/StepLayoutVertical.astro +1 -1
- package/src/components/lib/layout/StickySidebarWrapper.astro +4 -2
- package/src/components/lib/layout/index.ts +3 -1
- package/src/components/lib/navigation/QuickJumpDock.astro +125 -0
- package/src/components/lib/navigation/StepBreadcrumbBar.astro +151 -0
- package/src/components/lib/navigation/SubNavbarPills.astro +102 -0
- package/src/components/lib/navigation/index.ts +3 -0
- package/src/components/lib/overlays/CookiePreferencesDrawer.astro +226 -0
- package/src/components/lib/overlays/FloatingFeedbackWidget.astro +203 -0
- package/src/components/lib/overlays/ImageCompareSlider.astro +172 -0
- package/src/components/lib/overlays/index.ts +3 -0
- package/src/components/lib/sections/AgencyContactBrief.astro +194 -0
- package/src/components/lib/sections/AgencyFooterCta.astro +94 -0
- package/src/components/lib/sections/AgencyHeader.astro +161 -0
- package/src/components/lib/sections/AgencyHeroKinetic.astro +172 -0
- package/src/components/lib/sections/AgencyServicesList.astro +113 -0
- package/src/components/lib/sections/AgencyTeamGrid.astro +111 -0
- package/src/components/lib/sections/AgencyWorkGrid.astro +147 -0
- package/src/components/lib/sections/BlogGridSection.astro +133 -0
- package/src/components/lib/sections/ButtonsShowcase.astro +20 -18
- package/src/components/lib/sections/CardsShowcase.astro +28 -26
- package/src/components/lib/sections/CareersOpenings.astro +141 -0
- package/src/components/lib/sections/ChangelogSection.astro +151 -0
- package/src/components/lib/sections/ContactGlassForm.astro +10 -8
- package/src/components/lib/sections/ContactSplitMap.astro +141 -0
- package/src/components/lib/sections/CtaAppDownload.astro +13 -11
- package/src/components/lib/sections/CtaGlowBanner.astro +12 -10
- package/src/components/lib/sections/CtaNewsletterSection.astro +165 -0
- package/src/components/lib/sections/CtaSplitImage.astro +149 -0
- package/src/components/lib/sections/DataShowcase.astro +23 -21
- package/src/components/lib/sections/FaqAccordionSection.astro +8 -8
- package/src/components/lib/sections/FaqTwoColumn.astro +121 -0
- package/src/components/lib/sections/FeatureAlternatingRows.astro +192 -0
- package/src/components/lib/sections/FeatureBentoGrid.astro +13 -11
- package/src/components/lib/sections/FeatureIconGrid.astro +155 -0
- package/src/components/lib/sections/FeatureTabsShowcase.astro +182 -0
- package/src/components/lib/sections/FeatureTimelineFlow.astro +8 -6
- package/src/components/lib/sections/FeedbackShowcase.astro +15 -13
- package/src/components/lib/sections/FooterMegaSection.astro +157 -0
- package/src/components/lib/sections/FormsShowcase.astro +22 -20
- package/src/components/lib/sections/HeroDashboardPreview.astro +182 -0
- package/src/components/lib/sections/HeroGlass.astro +22 -20
- package/src/components/lib/sections/HeroGradient.astro +15 -13
- package/src/components/lib/sections/HeroMinimalCentered.astro +175 -0
- package/src/components/lib/sections/HeroShowcaseSplit.astro +306 -0
- package/src/components/lib/sections/HeroSplit.astro +13 -11
- package/src/components/lib/sections/HeroTerminal.astro +253 -0
- package/src/components/lib/sections/HeroWaitlist.astro +231 -0
- package/src/components/lib/sections/HowItWorksSteps.astro +180 -0
- package/src/components/lib/sections/IntegrationsGrid.astro +167 -0
- package/src/components/lib/sections/LayoutShowcase.astro +21 -19
- package/src/components/lib/sections/LogoCloudSection.astro +96 -0
- package/src/components/lib/sections/LogoWallGrid.astro +93 -0
- package/src/components/lib/sections/NavigationShowcase.astro +15 -13
- package/src/components/lib/sections/OverlaysShowcase.astro +21 -19
- package/src/components/lib/sections/PricingComparisonMatrix.astro +15 -13
- package/src/components/lib/sections/PricingSinglePlan.astro +159 -0
- package/src/components/lib/sections/PricingTierCards.astro +11 -9
- package/src/components/lib/sections/PricingToggleCards.astro +171 -0
- package/src/components/lib/sections/ShopCategoryHero.astro +103 -0
- package/src/components/lib/sections/ShopCollectionMosaic.astro +131 -0
- package/src/components/lib/sections/ShopFooterWordmark.astro +100 -0
- package/src/components/lib/sections/ShopHeaderEditorial.astro +396 -0
- package/src/components/lib/sections/ShopHeroEditorial.astro +151 -0
- package/src/components/lib/sections/ShopLookbook.astro +250 -0
- package/src/components/lib/sections/ShopNewsletterEditorial.astro +128 -0
- package/src/components/lib/sections/ShopProductRail.astro +150 -0
- package/src/components/lib/sections/ShopReviewsEditorial.astro +133 -0
- package/src/components/lib/sections/ShopValuesMarquee.astro +91 -0
- package/src/components/lib/sections/StatsCounterBand.astro +124 -0
- package/src/components/lib/sections/StatsDashboardSection.astro +85 -0
- package/src/components/lib/sections/StatsMetricShowcase.astro +9 -7
- package/src/components/lib/sections/TeamGridSection.astro +12 -10
- package/src/components/lib/sections/TestimonialCarousel.astro +167 -0
- package/src/components/lib/sections/TestimonialFeatured.astro +122 -0
- package/src/components/lib/sections/TestimonialGridSection.astro +188 -0
- package/src/components/lib/sections/TestimonialWallGlass.astro +11 -9
- package/src/components/lib/sections/TypographyShowcase.astro +10 -8
- package/src/components/lib/sections/index.ts +39 -0
- package/src/components/lib/templates/ecommerce/StoreCategoryPage.astro +160 -0
- package/src/components/lib/templates/ecommerce/StoreCheckoutPage.astro +98 -0
- package/src/components/lib/templates/ecommerce/StoreHomePage.astro +191 -0
- package/src/components/lib/templates/ecommerce/StoreLayout.astro +169 -0
- package/src/components/lib/templates/ecommerce/StoreProductPage.astro +165 -0
- package/src/components/lib/templates/ecommerce/data.ts +187 -0
- package/src/components/lib/templates/ecommerce/index.ts +14 -0
- package/src/components/lib/templates/ecommerce-editorial/EditorialBagPage.astro +52 -0
- package/src/components/lib/templates/ecommerce-editorial/EditorialCheckoutPage.astro +123 -0
- package/src/components/lib/templates/ecommerce-editorial/EditorialHomePage.astro +102 -0
- package/src/components/lib/templates/ecommerce-editorial/EditorialLayout.astro +216 -0
- package/src/components/lib/templates/ecommerce-editorial/EditorialProductPage.astro +205 -0
- package/src/components/lib/templates/ecommerce-editorial/EditorialShopPage.astro +112 -0
- package/src/components/lib/templates/ecommerce-editorial/data.ts +125 -0
- package/src/components/lib/templates/ecommerce-editorial/index.ts +16 -0
- package/src/components/lib/templates/saas-hotel/AppLayout.astro +83 -0
- package/src/components/lib/templates/saas-hotel/DashboardPage.astro +66 -0
- package/src/components/lib/templates/saas-hotel/GuestsPage.astro +65 -0
- package/src/components/lib/templates/saas-hotel/LandingPage.astro +213 -0
- package/src/components/lib/templates/saas-hotel/ReportsPage.astro +63 -0
- package/src/components/lib/templates/saas-hotel/ReservationsPage.astro +49 -0
- package/src/components/lib/templates/saas-hotel/RoomsPage.astro +48 -0
- package/src/components/lib/templates/saas-hotel/SettingsPage.astro +115 -0
- package/src/components/lib/templates/saas-hotel/SignInPage.astro +50 -0
- package/src/components/lib/templates/saas-hotel/data.ts +229 -0
- package/src/components/lib/templates/saas-hotel/index.ts +19 -0
- package/src/components/lib/templates/saas-hotel/lobby-theme.css +90 -0
- package/src/components/lib/typography/AnimatedCounterText.astro +102 -0
- package/src/components/lib/typography/CurvedPathHeading.astro +86 -0
- package/src/components/lib/typography/GradientTextMarquee.astro +105 -0
- package/src/components/lib/typography/HackerDecodeText.astro +85 -0
- package/src/components/lib/typography/index.ts +4 -0
- package/src/styles/global.css +113 -1
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* ShopValuesMarquee — Brand promise band: an oversized italic marquee followed by numbered
|
|
4
|
+
* value columns (materials, making, after-care…).
|
|
5
|
+
*
|
|
6
|
+
* @param {string[]} words - Marquee words. Empty hides the marquee.
|
|
7
|
+
* @param {Array<{title: string, text: string}>} values - 2–4 value columns.
|
|
8
|
+
* @param {number} speed - Marquee loop duration in seconds. Default 36.
|
|
9
|
+
* @param {string} variant - Style (plain|inverted). Default 'plain'.
|
|
10
|
+
* @param {string} size - Marquee scale (sm|md|lg). Default 'md'.
|
|
11
|
+
* @param {string} className - Extra class.
|
|
12
|
+
*
|
|
13
|
+
* Accessibility: the marquee is decorative (aria-hidden) and pauses on hover and with reduced
|
|
14
|
+
* motion; values are a real list with headings.
|
|
15
|
+
*/
|
|
16
|
+
interface Props {
|
|
17
|
+
words?: string[];
|
|
18
|
+
values?: Array<{ title: string; text: string }>;
|
|
19
|
+
speed?: number;
|
|
20
|
+
variant?: 'plain' | 'inverted';
|
|
21
|
+
size?: 'sm' | 'md' | 'lg';
|
|
22
|
+
className?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const {
|
|
26
|
+
words = ['Made slowly', 'Natural fibres', 'Small workshops', 'Repaired for life'],
|
|
27
|
+
values = [
|
|
28
|
+
{ title: 'Honest materials', text: 'Linen, wool, organic cotton and stoneware — traceable to the mill and the kiln.' },
|
|
29
|
+
{ title: 'Made in small runs', text: 'Each piece is produced in batches of a few hundred, never mass-manufactured.' },
|
|
30
|
+
{ title: 'Kept for years', text: 'Free repairs on everything we make, and a buy-back for pieces you have outgrown.' },
|
|
31
|
+
],
|
|
32
|
+
speed = 36,
|
|
33
|
+
variant = 'plain',
|
|
34
|
+
size = 'md',
|
|
35
|
+
className = '',
|
|
36
|
+
} = Astro.props;
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
<section class:list={['svm', `svm--${variant}`, `svm--${size}`, className]} style={`--svm-speed: ${speed}s`}>
|
|
40
|
+
{words.length > 0 && (
|
|
41
|
+
<div class="svm__marquee" aria-hidden="true">
|
|
42
|
+
<div class="svm__track">
|
|
43
|
+
{[0, 1].map(() => words.map((w) => <span>{w}<i>✺</i></span>))}
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
)}
|
|
47
|
+
{values.length > 0 && (
|
|
48
|
+
<ol class="svm__values">
|
|
49
|
+
{values.map((v, i) => (
|
|
50
|
+
<li>
|
|
51
|
+
<span class="svm__num" aria-hidden="true">{String(i + 1).padStart(2, '0')}</span>
|
|
52
|
+
<h3>{v.title}</h3>
|
|
53
|
+
<p>{v.text}</p>
|
|
54
|
+
</li>
|
|
55
|
+
))}
|
|
56
|
+
</ol>
|
|
57
|
+
)}
|
|
58
|
+
</section>
|
|
59
|
+
|
|
60
|
+
<style>
|
|
61
|
+
.svm { container-type: inline-size; --svm-fs: clamp(3rem, 9vw, 7.5rem); color: var(--c-text-1, #fff); overflow: hidden; }
|
|
62
|
+
.svm--sm { --svm-fs: clamp(2rem, 6vw, 4.5rem); }
|
|
63
|
+
.svm--lg { --svm-fs: clamp(4rem, 12vw, 10rem); }
|
|
64
|
+
.svm--inverted { background: var(--c-text-1, #fff); color: var(--c-bg, #050505); }
|
|
65
|
+
.svm__marquee { border-block: 1px solid currentColor; overflow: hidden; }
|
|
66
|
+
.svm__track { display: flex; width: max-content; animation: svm-run var(--svm-speed) linear infinite; }
|
|
67
|
+
.svm:hover .svm__track { animation-play-state: paused; }
|
|
68
|
+
.svm__track span { display: inline-flex; align-items: center; gap: 0.35em; padding: 0.1em 0 0.15em 0.35em; font-family: var(--font-display, Georgia, 'Times New Roman', serif); font-size: var(--svm-fs); font-style: italic; letter-spacing: -0.04em; line-height: 1.05; white-space: nowrap; }
|
|
69
|
+
.svm__track i { font-style: normal; font-size: 0.4em; color: var(--shop-accent, var(--c-primary-light, #d4a017)); }
|
|
70
|
+
@keyframes svm-run { to { transform: translateX(-50%); } }
|
|
71
|
+
|
|
72
|
+
.svm__values {
|
|
73
|
+
display: grid;
|
|
74
|
+
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
|
75
|
+
margin: 0 auto;
|
|
76
|
+
max-width: 1440px;
|
|
77
|
+
padding: clamp(2rem, 5vw, 4rem) clamp(1rem, 3vw, 2.5rem);
|
|
78
|
+
list-style: none;
|
|
79
|
+
gap: 0;
|
|
80
|
+
}
|
|
81
|
+
.svm__values li { padding: 0 var(--sp-6, 1.5rem) var(--sp-4, 1rem); border-left: 1px solid var(--c-border, rgba(255, 255, 255, 0.12)); }
|
|
82
|
+
.svm--inverted .svm__values li { border-left-color: rgb(var(--paper-rgb, 5 5 5) / 0.2); }
|
|
83
|
+
.svm__num { font-size: 0.75rem; font-variant-numeric: tabular-nums; color: var(--shop-accent, var(--c-primary-light, #d4a017)); }
|
|
84
|
+
.svm__values h3 { margin: var(--sp-3, 0.75rem) 0 var(--sp-2, 0.5rem); font-family: var(--font-display, Georgia, serif); font-size: 1.5rem; font-weight: 400; letter-spacing: -0.02em; }
|
|
85
|
+
.svm__values p { margin: 0; max-width: 22rem; font-size: 0.92rem; line-height: 1.65; opacity: 0.75; }
|
|
86
|
+
|
|
87
|
+
@container (max-width: 640px) {
|
|
88
|
+
.svm__values li { padding: var(--sp-4, 1rem) 0; border-left: 0; border-top: 1px solid var(--c-border, rgba(255, 255, 255, 0.12)); }
|
|
89
|
+
}
|
|
90
|
+
@media (prefers-reduced-motion: reduce) { .svm__track { animation: none; } }
|
|
91
|
+
</style>
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* StatsCounterBand — Full-width band of headline numbers that count up when
|
|
4
|
+
* scrolled into view. Final values are in the HTML, so no-JS and screen
|
|
5
|
+
* readers always get the real number; motion respects reduced-motion.
|
|
6
|
+
*
|
|
7
|
+
* @param {string} title - Optional heading (visually hidden when `hideTitle`).
|
|
8
|
+
* @param {boolean} hideTitle - Visually hide the heading.
|
|
9
|
+
* @param {Array<{value: number, prefix?: string, suffix?: string, label: string, decimals?: number}>} stats - Numbers.
|
|
10
|
+
* @param {string} variant - Band style (divided|cards|plain).
|
|
11
|
+
* @param {string} size - Vertical rhythm (sm|md|lg).
|
|
12
|
+
* @param {string} className - Extra CSS class.
|
|
13
|
+
*/
|
|
14
|
+
interface Stat { value: number; prefix?: string; suffix?: string; label: string; decimals?: number }
|
|
15
|
+
interface Props {
|
|
16
|
+
title?: string;
|
|
17
|
+
hideTitle?: boolean;
|
|
18
|
+
stats?: Stat[];
|
|
19
|
+
variant?: 'divided' | 'cards' | 'plain';
|
|
20
|
+
size?: 'sm' | 'md' | 'lg';
|
|
21
|
+
className?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const {
|
|
25
|
+
title = 'Trusted at scale',
|
|
26
|
+
hideTitle = false,
|
|
27
|
+
stats = [
|
|
28
|
+
{ value: 12000, suffix: '+', label: 'Developers using the kit' },
|
|
29
|
+
{ value: 250, suffix: '+', label: 'Components & sections' },
|
|
30
|
+
{ value: 99.98, suffix: '%', label: 'CDN uptime last 12 months', decimals: 2 },
|
|
31
|
+
{ value: 48, prefix: '<', suffix: 'ms', label: 'Median time to first byte' },
|
|
32
|
+
],
|
|
33
|
+
variant = 'divided',
|
|
34
|
+
size = 'md',
|
|
35
|
+
className = '',
|
|
36
|
+
} = Astro.props;
|
|
37
|
+
|
|
38
|
+
const id = `scb-${Math.random().toString(36).slice(2, 8)}`;
|
|
39
|
+
const fmt = (s: Stat) => `${s.prefix ?? ''}${s.value.toLocaleString('en-US', { minimumFractionDigits: s.decimals ?? 0, maximumFractionDigits: s.decimals ?? 0 })}${s.suffix ?? ''}`;
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
<div class="cq-stats-counter-band-cq">
|
|
43
|
+
<section class={`scb scb--${variant} scb--${size} ${className}`} aria-labelledby={id}>
|
|
44
|
+
<div class="scb__inner">
|
|
45
|
+
<h2 id={id} class:list={['scb__title', { 'scb__sr': hideTitle }]}>{title}</h2>
|
|
46
|
+
<dl class="scb__grid">
|
|
47
|
+
{stats.map((s) => (
|
|
48
|
+
<div class="scb__item">
|
|
49
|
+
<dt class="scb__label">{s.label}</dt>
|
|
50
|
+
<dd class="scb__value" data-count={s.value} data-decimals={s.decimals ?? 0} data-prefix={s.prefix ?? ''} data-suffix={s.suffix ?? ''}>{fmt(s)}</dd>
|
|
51
|
+
</div>
|
|
52
|
+
))}
|
|
53
|
+
</dl>
|
|
54
|
+
</div>
|
|
55
|
+
</section>
|
|
56
|
+
</div>
|
|
57
|
+
<script>
|
|
58
|
+
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
59
|
+
const els = document.querySelectorAll<HTMLElement>('.scb__value[data-count]');
|
|
60
|
+
if (!reduce && 'IntersectionObserver' in window) {
|
|
61
|
+
const io = new IntersectionObserver((entries) => {
|
|
62
|
+
entries.forEach((entry) => {
|
|
63
|
+
if (!entry.isIntersecting) return;
|
|
64
|
+
const el = entry.target as HTMLElement;
|
|
65
|
+
io.unobserve(el);
|
|
66
|
+
const target = Number(el.dataset.count);
|
|
67
|
+
const dec = Number(el.dataset.decimals);
|
|
68
|
+
const pre = el.dataset.prefix || '';
|
|
69
|
+
const suf = el.dataset.suffix || '';
|
|
70
|
+
const final = el.textContent;
|
|
71
|
+
el.setAttribute('aria-label', final || '');
|
|
72
|
+
const start = performance.now();
|
|
73
|
+
const dur = 1400;
|
|
74
|
+
const tick = (now: number) => {
|
|
75
|
+
const t = Math.min(1, (now - start) / dur);
|
|
76
|
+
const eased = 1 - Math.pow(1 - t, 3);
|
|
77
|
+
const v = target * eased;
|
|
78
|
+
el.textContent = `${pre}${v.toLocaleString('en-US', { minimumFractionDigits: dec, maximumFractionDigits: dec })}${suf}`;
|
|
79
|
+
if (t < 1) requestAnimationFrame(tick);
|
|
80
|
+
else el.textContent = final;
|
|
81
|
+
};
|
|
82
|
+
requestAnimationFrame(tick);
|
|
83
|
+
});
|
|
84
|
+
}, { threshold: 0.4 });
|
|
85
|
+
els.forEach((el) => io.observe(el));
|
|
86
|
+
}
|
|
87
|
+
</script>
|
|
88
|
+
|
|
89
|
+
<style>
|
|
90
|
+
.cq-stats-counter-band-cq { container-type: inline-size; }
|
|
91
|
+
.scb {
|
|
92
|
+
--scb-py: 4.5rem;
|
|
93
|
+
padding: var(--scb-py) var(--sp-6, 1.5rem);
|
|
94
|
+
background: linear-gradient(180deg, transparent, rgb(var(--ink-rgb, 255 255 255) / 0.02) 50%, transparent), var(--c-bg, #050505);
|
|
95
|
+
}
|
|
96
|
+
.scb--sm { --scb-py: 3rem; }
|
|
97
|
+
.scb--lg { --scb-py: 6.5rem; }
|
|
98
|
+
.scb__inner { max-width: 1180px; margin: 0 auto; }
|
|
99
|
+
.scb__title { margin: 0 0 2.5rem; text-align: center; font-size: 0.875rem; font-weight: 650; letter-spacing: 0.1em; text-transform: uppercase; color: var(--c-text-2, #a3a3a3); }
|
|
100
|
+
.scb__sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; margin: 0; }
|
|
101
|
+
.scb__grid { margin: 0; display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); }
|
|
102
|
+
.scb__item { display: flex; flex-direction: column-reverse; align-items: center; gap: 0.5rem; padding: 1.5rem 1rem; text-align: center; }
|
|
103
|
+
.scb--divided .scb__item + .scb__item { border-left: 1px solid rgb(var(--ink-rgb, 255 255 255) / 0.08); }
|
|
104
|
+
.scb--cards .scb__grid { gap: 1rem; }
|
|
105
|
+
.scb--cards .scb__item { border-radius: var(--r-lg, 20px); background: rgb(var(--ink-rgb, 255 255 255) / 0.03); border: 1px solid var(--c-border, rgb(var(--ink-rgb, 255 255 255) / 0.07)); }
|
|
106
|
+
.scb__value {
|
|
107
|
+
margin: 0;
|
|
108
|
+
font-size: clamp(2.25rem, 5vw, 3.25rem);
|
|
109
|
+
font-weight: 800;
|
|
110
|
+
letter-spacing: -0.04em;
|
|
111
|
+
line-height: 1;
|
|
112
|
+
font-variant-numeric: tabular-nums;
|
|
113
|
+
background: linear-gradient(180deg, var(--c-text-1, #fff), rgb(var(--ink-rgb, 255 255 255) / 0.65));
|
|
114
|
+
-webkit-background-clip: text;
|
|
115
|
+
background-clip: text;
|
|
116
|
+
color: transparent;
|
|
117
|
+
}
|
|
118
|
+
.scb__label { font-size: 0.9375rem; color: var(--c-text-2, #a3a3a3); max-width: 220px; }
|
|
119
|
+
@container (max-width: 640px) {
|
|
120
|
+
.scb__grid { grid-template-columns: 1fr 1fr; }
|
|
121
|
+
.scb--divided .scb__item + .scb__item { border-left: 0; }
|
|
122
|
+
.scb--divided .scb__item:nth-child(even) { border-left: 1px solid rgb(var(--ink-rgb, 255 255 255) / 0.08); }
|
|
123
|
+
}
|
|
124
|
+
</style>
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* StatsDashboardSection — Complete analytics section: heading, KPI tile row and
|
|
4
|
+
* two charts (trend + ranked breakdown) composed from the kit's SVG charts.
|
|
5
|
+
*
|
|
6
|
+
* @param {string} eyebrow - Small label above the heading.
|
|
7
|
+
* @param {string} title - Section heading.
|
|
8
|
+
* @param {string} subtitle - Section description.
|
|
9
|
+
* @param {Array<{label: string, value: string, change: number, trend?: number[], invert?: boolean}>} kpis - KPI tiles.
|
|
10
|
+
* @param {string} variant - Chart card style (glass|solid|ghost).
|
|
11
|
+
* @param {string} size - Vertical rhythm (sm|md|lg).
|
|
12
|
+
* @param {string} className - Extra CSS class.
|
|
13
|
+
*/
|
|
14
|
+
import KpiStatTile from '../charts/KpiStatTile.astro';
|
|
15
|
+
import StackedAreaChart from '../charts/StackedAreaChart.astro';
|
|
16
|
+
import HorizontalBarChart from '../charts/HorizontalBarChart.astro';
|
|
17
|
+
|
|
18
|
+
interface Kpi { label: string; value: string; change: number; trend?: number[]; invert?: boolean }
|
|
19
|
+
interface Props {
|
|
20
|
+
eyebrow?: string;
|
|
21
|
+
title?: string;
|
|
22
|
+
subtitle?: string;
|
|
23
|
+
kpis?: Kpi[];
|
|
24
|
+
variant?: 'glass' | 'solid' | 'ghost';
|
|
25
|
+
size?: 'sm' | 'md' | 'lg';
|
|
26
|
+
className?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const {
|
|
30
|
+
eyebrow = 'Live metrics',
|
|
31
|
+
title = 'Growth you can see',
|
|
32
|
+
subtitle = 'Real numbers from teams running on the kit, updated every build.',
|
|
33
|
+
kpis = [
|
|
34
|
+
{ label: 'Monthly visitors', value: '1.2M', change: 18.4, trend: [60, 64, 70, 72, 80, 88, 95, 104, 120] },
|
|
35
|
+
{ label: 'Conversion rate', value: '4.6%', change: 0.9, trend: [3.4, 3.6, 3.7, 3.9, 4, 4.2, 4.3, 4.5, 4.6] },
|
|
36
|
+
{ label: 'Avg. load time', value: '0.8s', change: -22, invert: true, trend: [1.3, 1.25, 1.1, 1.05, 1, 0.95, 0.9, 0.85, 0.8] },
|
|
37
|
+
{ label: 'Bounce rate', value: '31%', change: -4.2, invert: true, trend: [38, 37, 36, 35, 34, 33, 33, 32, 31] },
|
|
38
|
+
],
|
|
39
|
+
variant = 'glass',
|
|
40
|
+
size = 'md',
|
|
41
|
+
className = '',
|
|
42
|
+
} = Astro.props;
|
|
43
|
+
|
|
44
|
+
const id = `sds-${Math.random().toString(36).slice(2, 8)}`;
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
<div class="cq-stats-dashboard-section-cq">
|
|
48
|
+
<section class={`sds sds--${size} ${className}`} aria-labelledby={id}>
|
|
49
|
+
<div class="sds__inner">
|
|
50
|
+
<header class="sds__head">
|
|
51
|
+
<p class="sds__eyebrow"><span class="sds__live" aria-hidden="true"></span>{eyebrow}</p>
|
|
52
|
+
<h2 id={id} class="sds__title">{title}</h2>
|
|
53
|
+
<p class="sds__subtitle">{subtitle}</p>
|
|
54
|
+
</header>
|
|
55
|
+
<div class="sds__kpis">
|
|
56
|
+
{kpis.map((k) => (
|
|
57
|
+
<KpiStatTile label={k.label} value={k.value} change={k.change} trend={k.trend} invert={k.invert} period="vs last quarter" variant={variant} size="sm" />
|
|
58
|
+
))}
|
|
59
|
+
</div>
|
|
60
|
+
<div class="sds__charts">
|
|
61
|
+
<StackedAreaChart title="Sessions by channel" subtitle="Thousands per month" variant={variant} />
|
|
62
|
+
<HorizontalBarChart title="Top landing pages" subtitle="Visitors, last 30 days" variant={variant} />
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
</section>
|
|
66
|
+
</div>
|
|
67
|
+
<style>
|
|
68
|
+
.cq-stats-dashboard-section-cq { container-type: inline-size; }
|
|
69
|
+
.sds {
|
|
70
|
+
--sds-py: 5.5rem;
|
|
71
|
+
padding: var(--sds-py) var(--sp-6, 1.5rem);
|
|
72
|
+
background: radial-gradient(ellipse 60% 40% at 50% 0%, rgba(57, 135, 229, 0.08), transparent 70%), var(--c-bg, #050505);
|
|
73
|
+
}
|
|
74
|
+
.sds--sm { --sds-py: 3.5rem; }
|
|
75
|
+
.sds--lg { --sds-py: 7.5rem; }
|
|
76
|
+
.sds__inner { max-width: 1180px; margin: 0 auto; display: grid; gap: 1.25rem; }
|
|
77
|
+
.sds__head { text-align: center; max-width: 640px; margin: 0 auto 1.75rem; }
|
|
78
|
+
.sds__eyebrow { display: inline-flex; align-items: center; gap: 0.5rem; margin: 0 0 0.75rem; font-size: 0.8125rem; font-weight: 650; letter-spacing: 0.08em; text-transform: uppercase; color: var(--c-text-2, #a3a3a3); }
|
|
79
|
+
.sds__live { width: 8px; height: 8px; border-radius: 50%; background: var(--chart-pos, #34c38f); box-shadow: 0 0 10px var(--chart-pos, #34c38f); }
|
|
80
|
+
.sds__title { margin: 0 0 0.75rem; font-size: clamp(1.9rem, 4vw, 2.75rem); font-weight: 800; letter-spacing: -0.03em; color: var(--c-text-1, #fff); }
|
|
81
|
+
.sds__subtitle { margin: 0; font-size: 1.0625rem; color: var(--c-text-2, #a3a3a3); }
|
|
82
|
+
.sds__kpis { display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); gap: 1.25rem; }
|
|
83
|
+
.sds__charts { display: grid; grid-template-columns: 1.4fr 1fr; gap: 1.25rem; }
|
|
84
|
+
@container (max-width: 900px) { .sds__charts { grid-template-columns: 1fr; } }
|
|
85
|
+
</style>
|
|
@@ -52,6 +52,7 @@ const stats = [
|
|
|
52
52
|
];
|
|
53
53
|
---
|
|
54
54
|
|
|
55
|
+
<div class="cq-stats-metric-showcase-cq">
|
|
55
56
|
<section class={`stats-section ${className}`} aria-labelledby="stats-title">
|
|
56
57
|
<div class="stats-bg" aria-hidden="true">
|
|
57
58
|
<div class="stats-glow"></div>
|
|
@@ -105,8 +106,9 @@ const stats = [
|
|
|
105
106
|
</div>
|
|
106
107
|
</div>
|
|
107
108
|
</section>
|
|
108
|
-
|
|
109
|
+
</div>
|
|
109
110
|
<style>
|
|
111
|
+
.cq-stats-metric-showcase-cq { container-type: inline-size; }
|
|
110
112
|
.stats-section {
|
|
111
113
|
position: relative;
|
|
112
114
|
padding: var(--sp-20, 5rem) var(--sp-6, 1.5rem);
|
|
@@ -185,7 +187,7 @@ const stats = [
|
|
|
185
187
|
.stat-card {
|
|
186
188
|
background: rgba(15, 23, 42, 0.6);
|
|
187
189
|
backdrop-filter: blur(20px);
|
|
188
|
-
border: 1px solid
|
|
190
|
+
border: 1px solid rgb(var(--ink-rgb, 255 255 255) / 0.08);
|
|
189
191
|
border-radius: var(--r-xl, 20px);
|
|
190
192
|
padding: 2rem 1.75rem;
|
|
191
193
|
display: flex;
|
|
@@ -196,7 +198,7 @@ const stats = [
|
|
|
196
198
|
|
|
197
199
|
.stat-card:hover {
|
|
198
200
|
transform: translateY(-4px);
|
|
199
|
-
border-color:
|
|
201
|
+
border-color: rgb(var(--ink-rgb, 255 255 255) / 0.2);
|
|
200
202
|
}
|
|
201
203
|
|
|
202
204
|
.stat-top {
|
|
@@ -239,7 +241,7 @@ const stats = [
|
|
|
239
241
|
|
|
240
242
|
.stat-chart-bar {
|
|
241
243
|
height: 4px;
|
|
242
|
-
background:
|
|
244
|
+
background: rgb(var(--ink-rgb, 255 255 255) / 0.08);
|
|
243
245
|
border-radius: 9999px;
|
|
244
246
|
overflow: hidden;
|
|
245
247
|
}
|
|
@@ -258,7 +260,7 @@ const stats = [
|
|
|
258
260
|
gap: 2rem;
|
|
259
261
|
padding: 1.25rem 2rem;
|
|
260
262
|
background: rgba(10, 15, 30, 0.8);
|
|
261
|
-
border: 1px solid
|
|
263
|
+
border: 1px solid rgb(var(--ink-rgb, 255 255 255) / 0.08);
|
|
262
264
|
border-radius: 16px;
|
|
263
265
|
flex-wrap: wrap;
|
|
264
266
|
}
|
|
@@ -313,13 +315,13 @@ const stats = [
|
|
|
313
315
|
font-weight: 600;
|
|
314
316
|
}
|
|
315
317
|
|
|
316
|
-
@
|
|
318
|
+
@container (max-width: 1024px) {
|
|
317
319
|
.stats-grid {
|
|
318
320
|
grid-template-columns: repeat(2, 1fr);
|
|
319
321
|
}
|
|
320
322
|
}
|
|
321
323
|
|
|
322
|
-
@
|
|
324
|
+
@container (max-width: 640px) {
|
|
323
325
|
.stats-grid {
|
|
324
326
|
grid-template-columns: 1fr;
|
|
325
327
|
}
|
|
@@ -60,6 +60,7 @@ const members = [
|
|
|
60
60
|
];
|
|
61
61
|
---
|
|
62
62
|
|
|
63
|
+
<div class="cq-team-grid-section-cq">
|
|
63
64
|
<section class={`team-section ${className}`} aria-labelledby="team-title">
|
|
64
65
|
<div class="team-bg" aria-hidden="true">
|
|
65
66
|
<div class="team-glow"></div>
|
|
@@ -106,8 +107,9 @@ const members = [
|
|
|
106
107
|
</div>
|
|
107
108
|
</div>
|
|
108
109
|
</section>
|
|
109
|
-
|
|
110
|
+
</div>
|
|
110
111
|
<style>
|
|
112
|
+
.cq-team-grid-section-cq { container-type: inline-size; }
|
|
111
113
|
.team-section {
|
|
112
114
|
position: relative;
|
|
113
115
|
padding: var(--sp-20, 5rem) var(--sp-6, 1.5rem);
|
|
@@ -186,7 +188,7 @@ const members = [
|
|
|
186
188
|
.member-card {
|
|
187
189
|
background: rgba(15, 23, 42, 0.55);
|
|
188
190
|
backdrop-filter: blur(20px);
|
|
189
|
-
border: 1px solid
|
|
191
|
+
border: 1px solid rgb(var(--ink-rgb, 255 255 255) / 0.08);
|
|
190
192
|
border-radius: var(--r-xl, 20px);
|
|
191
193
|
padding: 2rem 1.5rem;
|
|
192
194
|
text-align: center;
|
|
@@ -198,7 +200,7 @@ const members = [
|
|
|
198
200
|
|
|
199
201
|
.member-card:hover {
|
|
200
202
|
transform: translateY(-4px);
|
|
201
|
-
border-color:
|
|
203
|
+
border-color: rgb(var(--ink-rgb, 255 255 255) / 0.2);
|
|
202
204
|
}
|
|
203
205
|
|
|
204
206
|
.member-avatar {
|
|
@@ -211,7 +213,7 @@ const members = [
|
|
|
211
213
|
font-weight: 800;
|
|
212
214
|
color: #fff;
|
|
213
215
|
margin-bottom: 1.25rem;
|
|
214
|
-
border: 2px solid
|
|
216
|
+
border: 2px solid rgb(var(--ink-rgb, 255 255 255) / 0.2);
|
|
215
217
|
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.4);
|
|
216
218
|
}
|
|
217
219
|
|
|
@@ -246,8 +248,8 @@ const members = [
|
|
|
246
248
|
width: 34px;
|
|
247
249
|
height: 34px;
|
|
248
250
|
border-radius: 8px;
|
|
249
|
-
background:
|
|
250
|
-
border: 1px solid
|
|
251
|
+
background: rgb(var(--ink-rgb, 255 255 255) / 0.05);
|
|
252
|
+
border: 1px solid rgb(var(--ink-rgb, 255 255 255) / 0.08);
|
|
251
253
|
display: flex;
|
|
252
254
|
align-items: center;
|
|
253
255
|
justify-content: center;
|
|
@@ -258,8 +260,8 @@ const members = [
|
|
|
258
260
|
|
|
259
261
|
.social-icon:hover {
|
|
260
262
|
color: #fff;
|
|
261
|
-
background:
|
|
262
|
-
border-color:
|
|
263
|
+
background: rgb(var(--ink-rgb, 255 255 255) / 0.12);
|
|
264
|
+
border-color: rgb(var(--ink-rgb, 255 255 255) / 0.2);
|
|
263
265
|
}
|
|
264
266
|
|
|
265
267
|
/* Hiring Banner */
|
|
@@ -314,13 +316,13 @@ const members = [
|
|
|
314
316
|
box-shadow: 0 6px 20px rgba(168, 85, 247, 0.45);
|
|
315
317
|
}
|
|
316
318
|
|
|
317
|
-
@
|
|
319
|
+
@container (max-width: 1024px) {
|
|
318
320
|
.team-grid {
|
|
319
321
|
grid-template-columns: repeat(2, 1fr);
|
|
320
322
|
}
|
|
321
323
|
}
|
|
322
324
|
|
|
323
|
-
@
|
|
325
|
+
@container (max-width: 640px) {
|
|
324
326
|
.team-grid {
|
|
325
327
|
grid-template-columns: 1fr;
|
|
326
328
|
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* TestimonialCarousel — Scroll-snap testimonial carousel with previous/next
|
|
4
|
+
* buttons, keyboard support and no autoplay (respects user control).
|
|
5
|
+
*
|
|
6
|
+
* @param {string} title - Section heading.
|
|
7
|
+
* @param {Array<{quote: string, name: string, role: string, rating?: number}>} items - Testimonials.
|
|
8
|
+
* @param {string} variant - Card style (glass|solid|quote).
|
|
9
|
+
* @param {string} size - Vertical rhythm (sm|md|lg).
|
|
10
|
+
* @param {string} className - Extra CSS class.
|
|
11
|
+
*/
|
|
12
|
+
interface Item { quote: string; name: string; role: string; rating?: number }
|
|
13
|
+
interface Props {
|
|
14
|
+
title?: string;
|
|
15
|
+
items?: Item[];
|
|
16
|
+
variant?: 'glass' | 'solid' | 'quote';
|
|
17
|
+
size?: 'sm' | 'md' | 'lg';
|
|
18
|
+
className?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const {
|
|
22
|
+
title = 'Loved by builders worldwide',
|
|
23
|
+
items = [
|
|
24
|
+
{ quote: 'We rebuilt our marketing site in two days. The sections are gorgeous and Lighthouse went straight to 100.', name: 'Alicia Gómez', role: 'Head of Growth, Northwind', rating: 5 },
|
|
25
|
+
{ quote: 'The SVG charts replaced a 200 kB library in our dashboard. Same features, zero runtime.', name: 'Marcus Lee', role: 'Staff Engineer, Lumen', rating: 5 },
|
|
26
|
+
{ quote: 'Finally a component kit that takes accessibility seriously. Our audit came back clean.', name: 'Priya Natarajan', role: 'Accessibility Lead, Civic', rating: 5 },
|
|
27
|
+
{ quote: 'Copy, paste, tweak tokens, done. It feels like a design system we built ourselves.', name: 'Tom Becker', role: 'Founder, Driftly', rating: 4 },
|
|
28
|
+
{ quote: 'Our agency ships every client landing page on top of this kit now.', name: 'Sofía Ruiz', role: 'Creative Director, Studio Alba', rating: 5 },
|
|
29
|
+
],
|
|
30
|
+
variant = 'glass',
|
|
31
|
+
size = 'md',
|
|
32
|
+
className = '',
|
|
33
|
+
} = Astro.props;
|
|
34
|
+
|
|
35
|
+
const id = `tc-${Math.random().toString(36).slice(2, 8)}`;
|
|
36
|
+
const initials = (n: string) => n.split(' ').map((p) => p[0]).slice(0, 2).join('');
|
|
37
|
+
const hues = ['var(--chart-1, #3987e5)', 'var(--chart-2, #d95926)', 'var(--chart-3, #199e70)', 'var(--chart-5, #d55181)', 'var(--chart-7, #9085e9)'];
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
<div class="cq-testimonial-carousel-cq">
|
|
41
|
+
<section class={`tc tc--${variant} tc--${size} ${className}`} aria-labelledby={id} aria-roledescription="carousel" data-tc>
|
|
42
|
+
<div class="tc__inner">
|
|
43
|
+
<header class="tc__head">
|
|
44
|
+
<h2 id={id} class="tc__title">{title}</h2>
|
|
45
|
+
<div class="tc__controls">
|
|
46
|
+
<button type="button" class="tc__btn" data-dir="-1" aria-label="Previous testimonials">
|
|
47
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M15 18l-6-6 6-6" /></svg>
|
|
48
|
+
</button>
|
|
49
|
+
<button type="button" class="tc__btn" data-dir="1" aria-label="Next testimonials">
|
|
50
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M9 18l6-6-6-6" /></svg>
|
|
51
|
+
</button>
|
|
52
|
+
</div>
|
|
53
|
+
</header>
|
|
54
|
+
|
|
55
|
+
<ul class="tc__track" tabindex="0" aria-label="Testimonials, scroll horizontally">
|
|
56
|
+
{items.map((t, i) => (
|
|
57
|
+
<li class="tc__slide" aria-roledescription="slide" aria-label={`${i + 1} of ${items.length}`}>
|
|
58
|
+
<figure class="tc__card">
|
|
59
|
+
{t.rating && (
|
|
60
|
+
<div class="tc__stars" role="img" aria-label={`${t.rating} out of 5 stars`}>
|
|
61
|
+
{Array.from({ length: 5 }, (_, s) => (
|
|
62
|
+
<svg width="16" height="16" viewBox="0 0 24 24" aria-hidden="true" class={(t.rating ?? 0) > s ? 'is-on' : ''}><path d="m12 2 3.1 6.3 6.9 1-5 4.9 1.2 6.8L12 17.8 5.8 21l1.2-6.8-5-4.9 6.9-1z" fill="currentColor" /></svg>
|
|
63
|
+
))}
|
|
64
|
+
</div>
|
|
65
|
+
)}
|
|
66
|
+
<blockquote class="tc__quote"><p>“{t.quote}”</p></blockquote>
|
|
67
|
+
<figcaption class="tc__author">
|
|
68
|
+
<span class="tc__avatar" style={`background:${hues[i % hues.length]}`} aria-hidden="true">{initials(t.name)}</span>
|
|
69
|
+
<span><strong>{t.name}</strong><span class="tc__role">{t.role}</span></span>
|
|
70
|
+
</figcaption>
|
|
71
|
+
</figure>
|
|
72
|
+
</li>
|
|
73
|
+
))}
|
|
74
|
+
</ul>
|
|
75
|
+
</div>
|
|
76
|
+
</section>
|
|
77
|
+
</div>
|
|
78
|
+
<script>
|
|
79
|
+
document.querySelectorAll<HTMLElement>('[data-tc]').forEach((root) => {
|
|
80
|
+
const track = root.querySelector<HTMLElement>('.tc__track');
|
|
81
|
+
const btns = root.querySelectorAll<HTMLButtonElement>('.tc__btn');
|
|
82
|
+
if (!track) return;
|
|
83
|
+
const update = () => {
|
|
84
|
+
const max = track.scrollWidth - track.clientWidth - 2;
|
|
85
|
+
btns[0].disabled = track.scrollLeft <= 2;
|
|
86
|
+
btns[1].disabled = track.scrollLeft >= max;
|
|
87
|
+
};
|
|
88
|
+
const step = () => (track.querySelector<HTMLElement>('.tc__slide')?.offsetWidth ?? 300) + 20;
|
|
89
|
+
btns.forEach((b) => b.addEventListener('click', () => {
|
|
90
|
+
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
91
|
+
track.scrollBy({ left: Number(b.dataset.dir) * step(), behavior: reduce ? 'auto' : 'smooth' });
|
|
92
|
+
}));
|
|
93
|
+
track.addEventListener('scroll', update, { passive: true });
|
|
94
|
+
window.addEventListener('resize', update);
|
|
95
|
+
update();
|
|
96
|
+
});
|
|
97
|
+
</script>
|
|
98
|
+
|
|
99
|
+
<style>
|
|
100
|
+
.cq-testimonial-carousel-cq { container-type: inline-size; }
|
|
101
|
+
.tc {
|
|
102
|
+
--tc-py: 5.5rem;
|
|
103
|
+
padding: var(--tc-py) var(--sp-6, 1.5rem);
|
|
104
|
+
background: var(--c-bg, #050505);
|
|
105
|
+
overflow: hidden;
|
|
106
|
+
}
|
|
107
|
+
.tc--sm { --tc-py: 3.5rem; }
|
|
108
|
+
.tc--lg { --tc-py: 7.5rem; }
|
|
109
|
+
.tc__inner { max-width: 1180px; margin: 0 auto; }
|
|
110
|
+
.tc__head { display: flex; justify-content: space-between; align-items: flex-end; gap: 1.5rem; margin-bottom: 2rem; }
|
|
111
|
+
.tc__title { margin: 0; font-size: clamp(1.75rem, 4vw, 2.5rem); font-weight: 800; letter-spacing: -0.03em; color: var(--c-text-1, #fff); max-width: 560px; }
|
|
112
|
+
.tc__controls { display: flex; gap: 0.5rem; flex-shrink: 0; }
|
|
113
|
+
.tc__btn {
|
|
114
|
+
display: grid;
|
|
115
|
+
place-items: center;
|
|
116
|
+
width: 48px;
|
|
117
|
+
height: 48px;
|
|
118
|
+
border-radius: 50%;
|
|
119
|
+
border: 1px solid rgb(var(--ink-rgb, 255 255 255) / 0.14);
|
|
120
|
+
background: rgb(var(--ink-rgb, 255 255 255) / 0.03);
|
|
121
|
+
color: var(--c-text-1, #fff);
|
|
122
|
+
cursor: pointer;
|
|
123
|
+
transition: background 0.2s ease, border-color 0.2s ease, opacity 0.2s ease;
|
|
124
|
+
}
|
|
125
|
+
.tc__btn:hover:not(:disabled) { background: rgb(var(--ink-rgb, 255 255 255) / 0.08); border-color: rgb(var(--ink-rgb, 255 255 255) / 0.3); }
|
|
126
|
+
.tc__btn:disabled { opacity: 0.35; cursor: not-allowed; }
|
|
127
|
+
.tc__btn:focus-visible { outline: 2px solid var(--c-primary-light, #d4a017); outline-offset: 2px; }
|
|
128
|
+
.tc__track {
|
|
129
|
+
list-style: none;
|
|
130
|
+
margin: 0;
|
|
131
|
+
padding: 0.25rem 0 1rem;
|
|
132
|
+
display: grid;
|
|
133
|
+
grid-auto-flow: column;
|
|
134
|
+
grid-auto-columns: min(380px, 85%);
|
|
135
|
+
gap: 20px;
|
|
136
|
+
overflow-x: auto;
|
|
137
|
+
scroll-snap-type: x mandatory;
|
|
138
|
+
scrollbar-width: none;
|
|
139
|
+
}
|
|
140
|
+
.tc__track::-webkit-scrollbar { display: none; }
|
|
141
|
+
.tc__track:focus-visible { outline: 2px solid var(--c-primary-light, #d4a017); outline-offset: 4px; border-radius: 12px; }
|
|
142
|
+
.tc__slide { scroll-snap-align: start; }
|
|
143
|
+
.tc__card {
|
|
144
|
+
height: 100%;
|
|
145
|
+
margin: 0;
|
|
146
|
+
display: flex;
|
|
147
|
+
flex-direction: column;
|
|
148
|
+
gap: 1.25rem;
|
|
149
|
+
padding: 1.75rem;
|
|
150
|
+
border-radius: var(--r-lg, 20px);
|
|
151
|
+
box-sizing: border-box;
|
|
152
|
+
}
|
|
153
|
+
.tc--glass .tc__card { background: linear-gradient(160deg, rgb(var(--ink-rgb, 255 255 255) / 0.05), rgb(var(--ink-rgb, 255 255 255) / 0.015)); border: 1px solid var(--c-border, rgb(var(--ink-rgb, 255 255 255) / 0.07)); }
|
|
154
|
+
.tc--solid .tc__card { background: var(--c-bg-2, #111); border: 1px solid rgb(var(--ink-rgb, 255 255 255) / 0.05); }
|
|
155
|
+
.tc--quote .tc__card { border-left: 2px solid var(--c-primary-light, #d4a017); border-radius: 0; padding-left: 1.5rem; }
|
|
156
|
+
.tc__stars { display: flex; gap: 2px; color: rgb(var(--ink-rgb, 255 255 255) / 0.15); }
|
|
157
|
+
.tc__stars .is-on { color: var(--c-primary-light, #d4a017); }
|
|
158
|
+
.tc__quote { margin: 0; flex: 1; }
|
|
159
|
+
.tc__quote p { margin: 0; font-size: 1.0625rem; line-height: 1.6; color: var(--c-text-1, #fff); }
|
|
160
|
+
.tc__author { display: flex; align-items: center; gap: 0.75rem; font-size: 0.875rem; }
|
|
161
|
+
.tc__author strong { display: block; color: var(--c-text-1, #fff); font-weight: 650; }
|
|
162
|
+
.tc__role { display: block; color: var(--c-text-2, #a3a3a3); font-size: 0.8125rem; }
|
|
163
|
+
.tc__avatar { display: grid; place-items: center; width: 40px; height: 40px; border-radius: 50%; color: #fff; font-weight: 700; font-size: 0.8125rem; flex-shrink: 0; }
|
|
164
|
+
@container (max-width: 560px) {
|
|
165
|
+
.tc__head { flex-direction: column; align-items: flex-start; }
|
|
166
|
+
}
|
|
167
|
+
</style>
|