astro-component-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 +33 -14
- package/package.json +2 -2
- package/src/components/index.ts +16 -202
- 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/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/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/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/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/layout/DiagonalSectionDivider.astro +69 -0
- package/src/components/lib/layout/HolyGrailGrid.astro +125 -0
- 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/BlogGridSection.astro +131 -0
- package/src/components/lib/sections/CareersOpenings.astro +139 -0
- package/src/components/lib/sections/ChangelogSection.astro +144 -0
- package/src/components/lib/sections/ContactSplitMap.astro +139 -0
- package/src/components/lib/sections/CtaNewsletterSection.astro +163 -0
- package/src/components/lib/sections/CtaSplitImage.astro +147 -0
- package/src/components/lib/sections/FaqTwoColumn.astro +119 -0
- package/src/components/lib/sections/FeatureAlternatingRows.astro +190 -0
- package/src/components/lib/sections/FeatureIconGrid.astro +155 -0
- package/src/components/lib/sections/FeatureTabsShowcase.astro +180 -0
- package/src/components/lib/sections/FooterMegaSection.astro +155 -0
- package/src/components/lib/sections/HeroDashboardPreview.astro +180 -0
- package/src/components/lib/sections/HeroMinimalCentered.astro +175 -0
- package/src/components/lib/sections/HeroShowcaseSplit.astro +304 -0
- package/src/components/lib/sections/HeroTerminal.astro +251 -0
- package/src/components/lib/sections/HeroWaitlist.astro +229 -0
- package/src/components/lib/sections/HowItWorksSteps.astro +178 -0
- package/src/components/lib/sections/IntegrationsGrid.astro +167 -0
- package/src/components/lib/sections/LogoCloudSection.astro +96 -0
- package/src/components/lib/sections/LogoWallGrid.astro +91 -0
- package/src/components/lib/sections/PricingSinglePlan.astro +157 -0
- package/src/components/lib/sections/PricingToggleCards.astro +171 -0
- package/src/components/lib/sections/StatsCounterBand.astro +122 -0
- package/src/components/lib/sections/StatsDashboardSection.astro +83 -0
- package/src/components/lib/sections/TestimonialCarousel.astro +165 -0
- package/src/components/lib/sections/TestimonialFeatured.astro +120 -0
- package/src/components/lib/sections/TestimonialGridSection.astro +188 -0
- package/src/components/lib/sections/index.ts +29 -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 +18 -1
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* LogoWallGrid — Static grid of customer wordmarks with hairline dividers and
|
|
4
|
+
* an optional headline stat. Logos render as text wordmarks by default; pass
|
|
5
|
+
* image URLs to use real logos.
|
|
6
|
+
*
|
|
7
|
+
* @param {string} title - Small heading above the grid.
|
|
8
|
+
* @param {Array<{name: string, src?: string, href?: string}>} logos - Customers.
|
|
9
|
+
* @param {number} columns - Columns on desktop (3|4|6).
|
|
10
|
+
* @param {string} variant - Grid style (divided|cards|plain).
|
|
11
|
+
* @param {string} size - Vertical rhythm (sm|md|lg).
|
|
12
|
+
* @param {string} className - Extra CSS class.
|
|
13
|
+
*/
|
|
14
|
+
interface Logo { name: string; src?: string; href?: string }
|
|
15
|
+
interface Props {
|
|
16
|
+
title?: string;
|
|
17
|
+
logos?: Logo[];
|
|
18
|
+
columns?: 3 | 4 | 6;
|
|
19
|
+
variant?: 'divided' | 'cards' | 'plain';
|
|
20
|
+
size?: 'sm' | 'md' | 'lg';
|
|
21
|
+
className?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const {
|
|
25
|
+
title = 'Powering 12,000+ teams, from startups to the Fortune 500',
|
|
26
|
+
logos = [
|
|
27
|
+
{ name: 'Northwind' }, { name: 'Lumen' }, { name: 'Orbitly' }, { name: 'Civic' },
|
|
28
|
+
{ name: 'Driftly' }, { name: 'Acme' }, { name: 'Helix' }, { name: 'Nimbus' },
|
|
29
|
+
],
|
|
30
|
+
columns = 4,
|
|
31
|
+
variant = 'divided',
|
|
32
|
+
size = 'md',
|
|
33
|
+
className = '',
|
|
34
|
+
} = Astro.props;
|
|
35
|
+
|
|
36
|
+
const id = `lwg-${Math.random().toString(36).slice(2, 8)}`;
|
|
37
|
+
const glyphs = [
|
|
38
|
+
'<circle cx="12" cy="12" r="9"/><circle cx="12" cy="12" r="3.5" fill="currentColor"/>',
|
|
39
|
+
'<path d="M12 3 21 20H3z"/>',
|
|
40
|
+
'<rect x="4" y="4" width="16" height="16" rx="4"/><path d="M4 12h16"/>',
|
|
41
|
+
'<path d="M4 18 12 4l8 14M8 12h8"/>',
|
|
42
|
+
];
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
<section class={`lwg lwg--${variant} lwg--${size} ${className}`} aria-labelledby={id} style={`--lwg-cols:${columns}`}>
|
|
46
|
+
<div class="lwg__inner">
|
|
47
|
+
<h2 id={id} class="lwg__title">{title}</h2>
|
|
48
|
+
<ul class="lwg__grid">
|
|
49
|
+
{logos.map((l, i) => {
|
|
50
|
+
const inner = l.src
|
|
51
|
+
? <img src={l.src} alt={l.name} loading="lazy" decoding="async" />
|
|
52
|
+
: (
|
|
53
|
+
<span class="lwg__word">
|
|
54
|
+
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round" aria-hidden="true" set:html={glyphs[i % glyphs.length]}></svg>
|
|
55
|
+
{l.name}
|
|
56
|
+
</span>
|
|
57
|
+
);
|
|
58
|
+
return (
|
|
59
|
+
<li class="lwg__cell">
|
|
60
|
+
{l.href ? <a href={l.href} class="lwg__link">{inner}</a> : inner}
|
|
61
|
+
</li>
|
|
62
|
+
);
|
|
63
|
+
})}
|
|
64
|
+
</ul>
|
|
65
|
+
</div>
|
|
66
|
+
</section>
|
|
67
|
+
|
|
68
|
+
<style>
|
|
69
|
+
.lwg {
|
|
70
|
+
--lwg-py: 4.5rem;
|
|
71
|
+
padding: var(--lwg-py) var(--sp-6, 1.5rem);
|
|
72
|
+
background: var(--c-bg, #050505);
|
|
73
|
+
}
|
|
74
|
+
.lwg--sm { --lwg-py: 3rem; }
|
|
75
|
+
.lwg--lg { --lwg-py: 6.5rem; }
|
|
76
|
+
.lwg__inner { max-width: 1120px; margin: 0 auto; }
|
|
77
|
+
.lwg__title { margin: 0 auto 2.5rem; max-width: 560px; text-align: center; font-size: 1rem; font-weight: 550; line-height: 1.5; color: var(--c-text-2, #a3a3a3); text-wrap: balance; }
|
|
78
|
+
.lwg__grid { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(var(--lwg-cols), minmax(0, 1fr)); }
|
|
79
|
+
.lwg--divided .lwg__grid { gap: 1px; background: rgba(255, 255, 255, 0.07); border: 1px solid rgba(255, 255, 255, 0.07); border-radius: var(--r-lg, 20px); overflow: hidden; }
|
|
80
|
+
.lwg--cards .lwg__grid { gap: 0.75rem; }
|
|
81
|
+
.lwg__cell { display: grid; place-items: center; min-height: 112px; padding: 1.25rem; background: var(--c-bg, #050505); }
|
|
82
|
+
.lwg--cards .lwg__cell { border-radius: var(--r-md, 14px); background: rgba(255, 255, 255, 0.025); border: 1px solid var(--c-border, rgba(255, 255, 255, 0.07)); }
|
|
83
|
+
.lwg--plain .lwg__cell { background: transparent; }
|
|
84
|
+
.lwg__word { display: inline-flex; align-items: center; gap: 0.5rem; font-size: 1.25rem; font-weight: 750; letter-spacing: -0.03em; color: var(--c-text-2, #a3a3a3); transition: color 0.25s ease; }
|
|
85
|
+
.lwg__cell:hover .lwg__word { color: var(--c-text-1, #fff); }
|
|
86
|
+
.lwg__cell img { max-width: 140px; max-height: 40px; object-fit: contain; filter: grayscale(1) brightness(1.6); opacity: 0.75; transition: opacity 0.25s ease, filter 0.25s ease; }
|
|
87
|
+
.lwg__cell:hover img { opacity: 1; filter: none; }
|
|
88
|
+
.lwg__link { display: grid; place-items: center; width: 100%; height: 100%; min-height: 44px; text-decoration: none; border-radius: 8px; }
|
|
89
|
+
.lwg__link:focus-visible { outline: 2px solid var(--c-primary-light, #d4a017); outline-offset: 2px; }
|
|
90
|
+
@media (max-width: 760px) { .lwg__grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
|
91
|
+
</style>
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* PricingSinglePlan — One-plan pricing section: price card on one side,
|
|
4
|
+
* full feature checklist on the other, with a money-back guarantee note.
|
|
5
|
+
*
|
|
6
|
+
* @param {string} title - Section heading.
|
|
7
|
+
* @param {string} subtitle - Section description.
|
|
8
|
+
* @param {string} planName - Plan name.
|
|
9
|
+
* @param {string} price - Formatted price.
|
|
10
|
+
* @param {string} period - Billing period label.
|
|
11
|
+
* @param {string} originalPrice - Optional struck-through price.
|
|
12
|
+
* @param {string[]} features - Included features.
|
|
13
|
+
* @param {{label: string, href: string}} cta - CTA button.
|
|
14
|
+
* @param {string} guarantee - Reassurance note under the CTA.
|
|
15
|
+
* @param {string} variant - Card style (glow|glass|minimal).
|
|
16
|
+
* @param {string} size - Vertical rhythm (sm|md|lg).
|
|
17
|
+
* @param {string} className - Extra CSS class.
|
|
18
|
+
*/
|
|
19
|
+
interface Props {
|
|
20
|
+
title?: string;
|
|
21
|
+
subtitle?: string;
|
|
22
|
+
planName?: string;
|
|
23
|
+
price?: string;
|
|
24
|
+
period?: string;
|
|
25
|
+
originalPrice?: string;
|
|
26
|
+
features?: string[];
|
|
27
|
+
cta?: { label: string; href: string };
|
|
28
|
+
guarantee?: string;
|
|
29
|
+
variant?: 'glow' | 'glass' | 'minimal';
|
|
30
|
+
size?: 'sm' | 'md' | 'lg';
|
|
31
|
+
className?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const {
|
|
35
|
+
title = 'One price. Everything included.',
|
|
36
|
+
subtitle = 'Lifetime access to every component, section and future update.',
|
|
37
|
+
planName = 'Lifetime license',
|
|
38
|
+
price = '$149',
|
|
39
|
+
period = 'one-time payment',
|
|
40
|
+
originalPrice = '$249',
|
|
41
|
+
features = [
|
|
42
|
+
'250+ components & sections',
|
|
43
|
+
'45 dependency-free SVG charts',
|
|
44
|
+
'Lifetime updates',
|
|
45
|
+
'Unlimited projects',
|
|
46
|
+
'Commercial license',
|
|
47
|
+
'Figma design kit',
|
|
48
|
+
'Private community',
|
|
49
|
+
'Priority email support',
|
|
50
|
+
],
|
|
51
|
+
cta = { label: 'Get lifetime access', href: '#' },
|
|
52
|
+
guarantee = '30-day money-back guarantee. No questions asked.',
|
|
53
|
+
variant = 'glow',
|
|
54
|
+
size = 'md',
|
|
55
|
+
className = '',
|
|
56
|
+
} = Astro.props;
|
|
57
|
+
|
|
58
|
+
const id = `psp-${Math.random().toString(36).slice(2, 8)}`;
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
<section class={`psp psp--${variant} psp--${size} ${className}`} aria-labelledby={id}>
|
|
62
|
+
<div class="psp__inner">
|
|
63
|
+
<header class="psp__head">
|
|
64
|
+
<h2 id={id} class="psp__title">{title}</h2>
|
|
65
|
+
<p class="psp__subtitle">{subtitle}</p>
|
|
66
|
+
</header>
|
|
67
|
+
<div class="psp__card">
|
|
68
|
+
<div class="psp__price-col">
|
|
69
|
+
<p class="psp__plan">{planName}</p>
|
|
70
|
+
<p class="psp__price">
|
|
71
|
+
{originalPrice && <s class="psp__orig"><span class="psp__sr">Was </span>{originalPrice}</s>}
|
|
72
|
+
<span class="psp__amount"><span class="psp__sr">Now </span>{price}</span>
|
|
73
|
+
</p>
|
|
74
|
+
<p class="psp__period">{period}</p>
|
|
75
|
+
<a class="psp__cta" href={cta.href}>{cta.label}</a>
|
|
76
|
+
<p class="psp__guarantee">
|
|
77
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" /><path d="m9 12 2 2 4-4" /></svg>
|
|
78
|
+
{guarantee}
|
|
79
|
+
</p>
|
|
80
|
+
</div>
|
|
81
|
+
<div class="psp__features-col">
|
|
82
|
+
<h3 class="psp__features-title">What’s included</h3>
|
|
83
|
+
<ul class="psp__features">
|
|
84
|
+
{features.map((f) => (
|
|
85
|
+
<li><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5" /></svg>{f}</li>
|
|
86
|
+
))}
|
|
87
|
+
</ul>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
</section>
|
|
92
|
+
|
|
93
|
+
<style>
|
|
94
|
+
.psp {
|
|
95
|
+
--psp-py: 5.5rem;
|
|
96
|
+
padding: var(--psp-py) var(--sp-6, 1.5rem);
|
|
97
|
+
background: var(--c-bg, #050505);
|
|
98
|
+
}
|
|
99
|
+
.psp--sm { --psp-py: 3.5rem; }
|
|
100
|
+
.psp--lg { --psp-py: 7.5rem; }
|
|
101
|
+
.psp__inner { max-width: 960px; margin: 0 auto; }
|
|
102
|
+
.psp__head { text-align: center; margin-bottom: 3rem; }
|
|
103
|
+
.psp__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); }
|
|
104
|
+
.psp__subtitle { margin: 0; font-size: 1.0625rem; color: var(--c-text-2, #a3a3a3); }
|
|
105
|
+
.psp__card {
|
|
106
|
+
position: relative;
|
|
107
|
+
display: grid;
|
|
108
|
+
grid-template-columns: 1fr 1.3fr;
|
|
109
|
+
border-radius: var(--r-xl, 28px);
|
|
110
|
+
overflow: hidden;
|
|
111
|
+
border: 1px solid var(--c-border, rgba(255, 255, 255, 0.08));
|
|
112
|
+
background: rgba(255, 255, 255, 0.02);
|
|
113
|
+
}
|
|
114
|
+
.psp--glow .psp__card { border-color: rgba(212, 160, 23, 0.35); box-shadow: 0 0 0 1px rgba(212, 160, 23, 0.08), 0 40px 100px -40px rgba(212, 160, 23, 0.4); }
|
|
115
|
+
.psp--glass .psp__card { backdrop-filter: blur(16px); background: linear-gradient(160deg, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0.015)); }
|
|
116
|
+
.psp--minimal .psp__card { background: transparent; }
|
|
117
|
+
.psp__price-col {
|
|
118
|
+
display: flex;
|
|
119
|
+
flex-direction: column;
|
|
120
|
+
justify-content: center;
|
|
121
|
+
padding: clamp(2rem, 5vw, 3rem);
|
|
122
|
+
background: radial-gradient(circle at 30% 0%, var(--c-primary-glow, rgba(184, 134, 11, 0.18)), transparent 70%), rgba(255, 255, 255, 0.02);
|
|
123
|
+
border-right: 1px solid rgba(255, 255, 255, 0.06);
|
|
124
|
+
}
|
|
125
|
+
.psp__plan { margin: 0 0 1rem; font-size: 0.875rem; font-weight: 650; letter-spacing: 0.06em; text-transform: uppercase; color: var(--c-primary-light, #d4a017); }
|
|
126
|
+
.psp__price { display: flex; align-items: baseline; gap: 0.75rem; margin: 0; }
|
|
127
|
+
.psp__orig { font-size: 1.25rem; color: var(--c-text-3, #8a8a8a); }
|
|
128
|
+
.psp__amount { font-size: clamp(3rem, 7vw, 4rem); font-weight: 800; letter-spacing: -0.04em; color: var(--c-text-1, #fff); line-height: 1; }
|
|
129
|
+
.psp__period { margin: 0.5rem 0 2rem; font-size: 0.9rem; color: var(--c-text-2, #a3a3a3); }
|
|
130
|
+
.psp__cta {
|
|
131
|
+
display: inline-flex;
|
|
132
|
+
align-items: center;
|
|
133
|
+
justify-content: center;
|
|
134
|
+
min-height: 52px;
|
|
135
|
+
border-radius: var(--r-md, 12px);
|
|
136
|
+
background: var(--c-primary-light, #d4a017);
|
|
137
|
+
color: #0a0a0a;
|
|
138
|
+
font-weight: 700;
|
|
139
|
+
text-decoration: none;
|
|
140
|
+
transition: background 0.2s ease, transform 0.2s ease;
|
|
141
|
+
}
|
|
142
|
+
.psp__cta:hover { background: var(--c-accent, #fef3c7); color: #0a0a0a; transform: translateY(-1px); }
|
|
143
|
+
.psp__cta:focus-visible { outline: 2px solid var(--c-primary-light, #d4a017); outline-offset: 3px; }
|
|
144
|
+
.psp__guarantee { display: flex; align-items: flex-start; gap: 0.5rem; margin: 1.25rem 0 0; font-size: 0.8125rem; line-height: 1.5; color: var(--c-text-2, #a3a3a3); }
|
|
145
|
+
.psp__guarantee svg { color: var(--chart-pos, #34c38f); flex-shrink: 0; margin-top: 1px; }
|
|
146
|
+
.psp__features-col { padding: clamp(2rem, 5vw, 3rem); }
|
|
147
|
+
.psp__features-title { margin: 0 0 1.5rem; font-size: 1rem; font-weight: 650; color: var(--c-text-1, #fff); }
|
|
148
|
+
.psp__features { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: 1fr 1fr; gap: 1rem 1.5rem; }
|
|
149
|
+
.psp__features li { display: flex; align-items: flex-start; gap: 0.6rem; font-size: 0.9375rem; line-height: 1.4; color: var(--c-text-1, #fff); }
|
|
150
|
+
.psp__features svg { color: var(--c-primary-light, #d4a017); flex-shrink: 0; }
|
|
151
|
+
.psp__sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
|
|
152
|
+
@media (max-width: 760px) {
|
|
153
|
+
.psp__card { grid-template-columns: 1fr; }
|
|
154
|
+
.psp__price-col { border-right: 0; border-bottom: 1px solid rgba(255, 255, 255, 0.06); }
|
|
155
|
+
.psp__features { grid-template-columns: 1fr; }
|
|
156
|
+
}
|
|
157
|
+
</style>
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* PricingToggleCards — Three pricing tiers with an accessible monthly/yearly
|
|
4
|
+
* switch (role="switch"), savings badge and a highlighted recommended plan.
|
|
5
|
+
*
|
|
6
|
+
* @param {string} title - Section heading.
|
|
7
|
+
* @param {string} subtitle - Section description.
|
|
8
|
+
* @param {Array<{name: string, monthly: number, yearly: number, description: string, features: string[], cta: string, href: string, featured?: boolean}>} plans - Tiers.
|
|
9
|
+
* @param {string} currency - Currency symbol.
|
|
10
|
+
* @param {string} savingsLabel - Badge next to the yearly option.
|
|
11
|
+
* @param {string} variant - Card style (glass|solid|outline).
|
|
12
|
+
* @param {string} size - Vertical rhythm (sm|md|lg).
|
|
13
|
+
* @param {string} className - Extra CSS class.
|
|
14
|
+
*/
|
|
15
|
+
interface Plan { name: string; monthly: number; yearly: number; description: string; features: string[]; cta: string; href: string; featured?: boolean }
|
|
16
|
+
interface Props {
|
|
17
|
+
title?: string;
|
|
18
|
+
subtitle?: string;
|
|
19
|
+
plans?: Plan[];
|
|
20
|
+
currency?: string;
|
|
21
|
+
savingsLabel?: string;
|
|
22
|
+
variant?: 'glass' | 'solid' | 'outline';
|
|
23
|
+
size?: 'sm' | 'md' | 'lg';
|
|
24
|
+
className?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const {
|
|
28
|
+
title = 'Simple pricing that scales with you',
|
|
29
|
+
subtitle = 'Start free. Upgrade when your team grows. Cancel anytime.',
|
|
30
|
+
plans = [
|
|
31
|
+
{ name: 'Starter', monthly: 0, yearly: 0, description: 'For side projects and prototypes.', features: ['All components', 'Community support', '1 project'], cta: 'Get started', href: '#' },
|
|
32
|
+
{ name: 'Pro', monthly: 24, yearly: 19, description: 'For professionals shipping client work.', features: ['Everything in Starter', 'Premium sections', 'Unlimited projects', 'Priority support'], cta: 'Start 14-day trial', href: '#', featured: true },
|
|
33
|
+
{ name: 'Team', monthly: 79, yearly: 63, description: 'For agencies and product teams.', features: ['Everything in Pro', '10 seats included', 'Figma kit', 'Private Discord'], cta: 'Contact sales', href: '#' },
|
|
34
|
+
],
|
|
35
|
+
currency = '$',
|
|
36
|
+
savingsLabel = 'Save 20%',
|
|
37
|
+
variant = 'glass',
|
|
38
|
+
size = 'md',
|
|
39
|
+
className = '',
|
|
40
|
+
} = Astro.props;
|
|
41
|
+
|
|
42
|
+
const id = `ptc-${Math.random().toString(36).slice(2, 8)}`;
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
<section class={`ptc ptc--${variant} ptc--${size} ${className}`} aria-labelledby={id} data-ptc data-period="monthly">
|
|
46
|
+
<div class="ptc__inner">
|
|
47
|
+
<header class="ptc__head">
|
|
48
|
+
<h2 id={id} class="ptc__title">{title}</h2>
|
|
49
|
+
<p class="ptc__subtitle">{subtitle}</p>
|
|
50
|
+
<div class="ptc__toggle">
|
|
51
|
+
<span class="ptc__period" id={`${id}-m`}>Monthly</span>
|
|
52
|
+
<button type="button" class="ptc__switch" role="switch" aria-checked="false" aria-labelledby={`${id}-y`}>
|
|
53
|
+
<span class="ptc__knob" aria-hidden="true"></span>
|
|
54
|
+
</button>
|
|
55
|
+
<span class="ptc__period" id={`${id}-y`}>Yearly <span class="ptc__save">{savingsLabel}</span></span>
|
|
56
|
+
</div>
|
|
57
|
+
</header>
|
|
58
|
+
|
|
59
|
+
<ul class="ptc__grid">
|
|
60
|
+
{plans.map((p) => (
|
|
61
|
+
<li class={`ptc__card ${p.featured ? 'ptc__card--featured' : ''}`}>
|
|
62
|
+
{p.featured && <span class="ptc__ribbon">Most popular</span>}
|
|
63
|
+
<h3 class="ptc__name">{p.name}</h3>
|
|
64
|
+
<p class="ptc__desc">{p.description}</p>
|
|
65
|
+
<p class="ptc__price">
|
|
66
|
+
<span class="ptc__amount" data-monthly={`${currency}${p.monthly}`} data-yearly={`${currency}${p.yearly}`}>{currency}{p.monthly}</span>
|
|
67
|
+
<span class="ptc__per">/ month<span class="ptc__billed" data-yearly-only>, billed yearly</span></span>
|
|
68
|
+
</p>
|
|
69
|
+
<a class="ptc__cta" href={p.href}>{p.cta}</a>
|
|
70
|
+
<ul class="ptc__features">
|
|
71
|
+
{p.features.map((f) => (
|
|
72
|
+
<li><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5" /></svg>{f}</li>
|
|
73
|
+
))}
|
|
74
|
+
</ul>
|
|
75
|
+
</li>
|
|
76
|
+
))}
|
|
77
|
+
</ul>
|
|
78
|
+
<p class="ptc__live" role="status" aria-live="polite"></p>
|
|
79
|
+
</div>
|
|
80
|
+
</section>
|
|
81
|
+
|
|
82
|
+
<script>
|
|
83
|
+
document.querySelectorAll<HTMLElement>('[data-ptc]').forEach((root) => {
|
|
84
|
+
const sw = root.querySelector<HTMLButtonElement>('.ptc__switch');
|
|
85
|
+
const live = root.querySelector('.ptc__live');
|
|
86
|
+
sw?.addEventListener('click', () => {
|
|
87
|
+
const yearly = sw.getAttribute('aria-checked') !== 'true';
|
|
88
|
+
sw.setAttribute('aria-checked', String(yearly));
|
|
89
|
+
root.dataset.period = yearly ? 'yearly' : 'monthly';
|
|
90
|
+
root.querySelectorAll<HTMLElement>('.ptc__amount').forEach((el) => {
|
|
91
|
+
el.textContent = (yearly ? el.dataset.yearly : el.dataset.monthly) || '';
|
|
92
|
+
});
|
|
93
|
+
if (live) live.textContent = yearly ? 'Showing yearly prices' : 'Showing monthly prices';
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
</script>
|
|
97
|
+
|
|
98
|
+
<style>
|
|
99
|
+
.ptc {
|
|
100
|
+
--ptc-py: 5.5rem;
|
|
101
|
+
padding: var(--ptc-py) var(--sp-6, 1.5rem);
|
|
102
|
+
background: radial-gradient(ellipse 60% 40% at 50% 0%, var(--c-primary-glow, rgba(184, 134, 11, 0.1)), transparent 70%), var(--c-bg, #050505);
|
|
103
|
+
}
|
|
104
|
+
.ptc--sm { --ptc-py: 3.5rem; }
|
|
105
|
+
.ptc--lg { --ptc-py: 7.5rem; }
|
|
106
|
+
.ptc__inner { max-width: 1120px; margin: 0 auto; }
|
|
107
|
+
.ptc__head { text-align: center; margin-bottom: 3rem; }
|
|
108
|
+
.ptc__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); }
|
|
109
|
+
.ptc__subtitle { margin: 0 0 2rem; font-size: 1.0625rem; color: var(--c-text-2, #a3a3a3); }
|
|
110
|
+
.ptc__toggle { display: inline-flex; align-items: center; gap: 0.85rem; font-size: 0.9375rem; color: var(--c-text-2, #a3a3a3); }
|
|
111
|
+
.ptc__switch {
|
|
112
|
+
position: relative;
|
|
113
|
+
width: 52px;
|
|
114
|
+
height: 30px;
|
|
115
|
+
border: 1px solid rgba(255, 255, 255, 0.15);
|
|
116
|
+
border-radius: 999px;
|
|
117
|
+
background: rgba(255, 255, 255, 0.08);
|
|
118
|
+
cursor: pointer;
|
|
119
|
+
transition: background 0.25s ease;
|
|
120
|
+
}
|
|
121
|
+
.ptc__switch::after { content: ''; position: absolute; inset: -8px; }
|
|
122
|
+
.ptc__switch[aria-checked='true'] { background: var(--c-primary, #b8860b); border-color: transparent; }
|
|
123
|
+
.ptc__switch:focus-visible { outline: 2px solid var(--c-primary-light, #d4a017); outline-offset: 3px; }
|
|
124
|
+
.ptc__knob { position: absolute; top: 3px; left: 3px; width: 22px; height: 22px; border-radius: 50%; background: #fff; transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1); }
|
|
125
|
+
.ptc__switch[aria-checked='true'] .ptc__knob { transform: translateX(22px); }
|
|
126
|
+
[data-period='monthly'] .ptc__period:first-child,
|
|
127
|
+
[data-period='yearly'] .ptc__period:last-child { color: var(--c-text-1, #fff); }
|
|
128
|
+
.ptc__save { margin-left: 0.35rem; padding: 0.15rem 0.5rem; border-radius: 999px; background: rgba(52, 195, 143, 0.15); color: #7ee2b8; font-size: 0.75rem; font-weight: 650; }
|
|
129
|
+
|
|
130
|
+
.ptc__grid { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1.25rem; align-items: stretch; }
|
|
131
|
+
.ptc__card {
|
|
132
|
+
position: relative;
|
|
133
|
+
display: flex;
|
|
134
|
+
flex-direction: column;
|
|
135
|
+
padding: 2rem;
|
|
136
|
+
border-radius: var(--r-xl, 24px);
|
|
137
|
+
}
|
|
138
|
+
.ptc--glass .ptc__card { background: linear-gradient(160deg, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0.015)); border: 1px solid var(--c-border, rgba(255, 255, 255, 0.07)); backdrop-filter: blur(12px); }
|
|
139
|
+
.ptc--solid .ptc__card { background: var(--c-bg-2, #111); border: 1px solid rgba(255, 255, 255, 0.06); }
|
|
140
|
+
.ptc--outline .ptc__card { border: 1px solid rgba(255, 255, 255, 0.12); }
|
|
141
|
+
.ptc__card--featured { border-color: rgba(212, 160, 23, 0.55) !important; box-shadow: 0 30px 80px -30px rgba(212, 160, 23, 0.35); }
|
|
142
|
+
.ptc__ribbon { position: absolute; top: -12px; left: 2rem; padding: 0.25rem 0.75rem; border-radius: 999px; background: var(--c-primary-light, #d4a017); color: #0a0a0a; font-size: 0.75rem; font-weight: 700; }
|
|
143
|
+
.ptc__name { margin: 0 0 0.35rem; font-size: 1.125rem; font-weight: 650; color: var(--c-text-1, #fff); }
|
|
144
|
+
.ptc__desc { margin: 0 0 1.5rem; font-size: 0.9rem; color: var(--c-text-2, #a3a3a3); }
|
|
145
|
+
.ptc__price { display: flex; align-items: baseline; gap: 0.4rem; margin: 0 0 1.5rem; }
|
|
146
|
+
.ptc__amount { font-size: 3rem; font-weight: 800; letter-spacing: -0.04em; color: var(--c-text-1, #fff); font-variant-numeric: tabular-nums; line-height: 1; }
|
|
147
|
+
.ptc__per { font-size: 0.875rem; color: var(--c-text-3, #8a8a8a); }
|
|
148
|
+
.ptc__billed { display: none; }
|
|
149
|
+
[data-period='yearly'] .ptc__billed { display: inline; }
|
|
150
|
+
.ptc__cta {
|
|
151
|
+
display: inline-flex;
|
|
152
|
+
align-items: center;
|
|
153
|
+
justify-content: center;
|
|
154
|
+
min-height: 48px;
|
|
155
|
+
border-radius: var(--r-md, 12px);
|
|
156
|
+
border: 1px solid rgba(255, 255, 255, 0.14);
|
|
157
|
+
color: var(--c-text-1, #fff);
|
|
158
|
+
font-weight: 650;
|
|
159
|
+
text-decoration: none;
|
|
160
|
+
transition: background 0.2s ease, border-color 0.2s ease;
|
|
161
|
+
}
|
|
162
|
+
.ptc__cta:hover { border-color: rgba(255, 255, 255, 0.3); color: var(--c-text-1, #fff); }
|
|
163
|
+
.ptc__card--featured .ptc__cta { background: var(--c-primary-light, #d4a017); color: #0a0a0a; border-color: transparent; }
|
|
164
|
+
.ptc__card--featured .ptc__cta:hover { background: var(--c-accent, #fef3c7); color: #0a0a0a; }
|
|
165
|
+
.ptc__cta:focus-visible { outline: 2px solid var(--c-primary-light, #d4a017); outline-offset: 3px; }
|
|
166
|
+
.ptc__features { list-style: none; margin: 1.75rem 0 0; padding: 1.5rem 0 0; border-top: 1px solid rgba(255, 255, 255, 0.07); display: grid; gap: 0.7rem; }
|
|
167
|
+
.ptc__features li { display: flex; align-items: center; gap: 0.6rem; font-size: 0.9rem; color: var(--c-text-1, #fff); }
|
|
168
|
+
.ptc__features svg { color: var(--c-primary-light, #d4a017); flex-shrink: 0; }
|
|
169
|
+
.ptc__live { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }
|
|
170
|
+
@media (prefers-reduced-motion: reduce) { .ptc__knob, .ptc__switch { transition: none; } }
|
|
171
|
+
</style>
|
|
@@ -0,0 +1,122 @@
|
|
|
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
|
+
<section class={`scb scb--${variant} scb--${size} ${className}`} aria-labelledby={id}>
|
|
43
|
+
<div class="scb__inner">
|
|
44
|
+
<h2 id={id} class:list={['scb__title', { 'scb__sr': hideTitle }]}>{title}</h2>
|
|
45
|
+
<dl class="scb__grid">
|
|
46
|
+
{stats.map((s) => (
|
|
47
|
+
<div class="scb__item">
|
|
48
|
+
<dt class="scb__label">{s.label}</dt>
|
|
49
|
+
<dd class="scb__value" data-count={s.value} data-decimals={s.decimals ?? 0} data-prefix={s.prefix ?? ''} data-suffix={s.suffix ?? ''}>{fmt(s)}</dd>
|
|
50
|
+
</div>
|
|
51
|
+
))}
|
|
52
|
+
</dl>
|
|
53
|
+
</div>
|
|
54
|
+
</section>
|
|
55
|
+
|
|
56
|
+
<script>
|
|
57
|
+
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
58
|
+
const els = document.querySelectorAll<HTMLElement>('.scb__value[data-count]');
|
|
59
|
+
if (!reduce && 'IntersectionObserver' in window) {
|
|
60
|
+
const io = new IntersectionObserver((entries) => {
|
|
61
|
+
entries.forEach((entry) => {
|
|
62
|
+
if (!entry.isIntersecting) return;
|
|
63
|
+
const el = entry.target as HTMLElement;
|
|
64
|
+
io.unobserve(el);
|
|
65
|
+
const target = Number(el.dataset.count);
|
|
66
|
+
const dec = Number(el.dataset.decimals);
|
|
67
|
+
const pre = el.dataset.prefix || '';
|
|
68
|
+
const suf = el.dataset.suffix || '';
|
|
69
|
+
const final = el.textContent;
|
|
70
|
+
el.setAttribute('aria-label', final || '');
|
|
71
|
+
const start = performance.now();
|
|
72
|
+
const dur = 1400;
|
|
73
|
+
const tick = (now: number) => {
|
|
74
|
+
const t = Math.min(1, (now - start) / dur);
|
|
75
|
+
const eased = 1 - Math.pow(1 - t, 3);
|
|
76
|
+
const v = target * eased;
|
|
77
|
+
el.textContent = `${pre}${v.toLocaleString('en-US', { minimumFractionDigits: dec, maximumFractionDigits: dec })}${suf}`;
|
|
78
|
+
if (t < 1) requestAnimationFrame(tick);
|
|
79
|
+
else el.textContent = final;
|
|
80
|
+
};
|
|
81
|
+
requestAnimationFrame(tick);
|
|
82
|
+
});
|
|
83
|
+
}, { threshold: 0.4 });
|
|
84
|
+
els.forEach((el) => io.observe(el));
|
|
85
|
+
}
|
|
86
|
+
</script>
|
|
87
|
+
|
|
88
|
+
<style>
|
|
89
|
+
.scb {
|
|
90
|
+
--scb-py: 4.5rem;
|
|
91
|
+
padding: var(--scb-py) var(--sp-6, 1.5rem);
|
|
92
|
+
background: linear-gradient(180deg, transparent, rgba(255, 255, 255, 0.02) 50%, transparent), var(--c-bg, #050505);
|
|
93
|
+
}
|
|
94
|
+
.scb--sm { --scb-py: 3rem; }
|
|
95
|
+
.scb--lg { --scb-py: 6.5rem; }
|
|
96
|
+
.scb__inner { max-width: 1180px; margin: 0 auto; }
|
|
97
|
+
.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); }
|
|
98
|
+
.scb__sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; margin: 0; }
|
|
99
|
+
.scb__grid { margin: 0; display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); }
|
|
100
|
+
.scb__item { display: flex; flex-direction: column-reverse; align-items: center; gap: 0.5rem; padding: 1.5rem 1rem; text-align: center; }
|
|
101
|
+
.scb--divided .scb__item + .scb__item { border-left: 1px solid rgba(255, 255, 255, 0.08); }
|
|
102
|
+
.scb--cards .scb__grid { gap: 1rem; }
|
|
103
|
+
.scb--cards .scb__item { border-radius: var(--r-lg, 20px); background: rgba(255, 255, 255, 0.03); border: 1px solid var(--c-border, rgba(255, 255, 255, 0.07)); }
|
|
104
|
+
.scb__value {
|
|
105
|
+
margin: 0;
|
|
106
|
+
font-size: clamp(2.25rem, 5vw, 3.25rem);
|
|
107
|
+
font-weight: 800;
|
|
108
|
+
letter-spacing: -0.04em;
|
|
109
|
+
line-height: 1;
|
|
110
|
+
font-variant-numeric: tabular-nums;
|
|
111
|
+
background: linear-gradient(180deg, #fff, rgba(255, 255, 255, 0.65));
|
|
112
|
+
-webkit-background-clip: text;
|
|
113
|
+
background-clip: text;
|
|
114
|
+
color: transparent;
|
|
115
|
+
}
|
|
116
|
+
.scb__label { font-size: 0.9375rem; color: var(--c-text-2, #a3a3a3); max-width: 220px; }
|
|
117
|
+
@media (max-width: 640px) {
|
|
118
|
+
.scb__grid { grid-template-columns: 1fr 1fr; }
|
|
119
|
+
.scb--divided .scb__item + .scb__item { border-left: 0; }
|
|
120
|
+
.scb--divided .scb__item:nth-child(even) { border-left: 1px solid rgba(255, 255, 255, 0.08); }
|
|
121
|
+
}
|
|
122
|
+
</style>
|
|
@@ -0,0 +1,83 @@
|
|
|
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
|
+
<section class={`sds sds--${size} ${className}`} aria-labelledby={id}>
|
|
48
|
+
<div class="sds__inner">
|
|
49
|
+
<header class="sds__head">
|
|
50
|
+
<p class="sds__eyebrow"><span class="sds__live" aria-hidden="true"></span>{eyebrow}</p>
|
|
51
|
+
<h2 id={id} class="sds__title">{title}</h2>
|
|
52
|
+
<p class="sds__subtitle">{subtitle}</p>
|
|
53
|
+
</header>
|
|
54
|
+
<div class="sds__kpis">
|
|
55
|
+
{kpis.map((k) => (
|
|
56
|
+
<KpiStatTile label={k.label} value={k.value} change={k.change} trend={k.trend} invert={k.invert} period="vs last quarter" variant={variant} size="sm" />
|
|
57
|
+
))}
|
|
58
|
+
</div>
|
|
59
|
+
<div class="sds__charts">
|
|
60
|
+
<StackedAreaChart title="Sessions by channel" subtitle="Thousands per month" variant={variant} />
|
|
61
|
+
<HorizontalBarChart title="Top landing pages" subtitle="Visitors, last 30 days" variant={variant} />
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
</section>
|
|
65
|
+
|
|
66
|
+
<style>
|
|
67
|
+
.sds {
|
|
68
|
+
--sds-py: 5.5rem;
|
|
69
|
+
padding: var(--sds-py) var(--sp-6, 1.5rem);
|
|
70
|
+
background: radial-gradient(ellipse 60% 40% at 50% 0%, rgba(57, 135, 229, 0.08), transparent 70%), var(--c-bg, #050505);
|
|
71
|
+
}
|
|
72
|
+
.sds--sm { --sds-py: 3.5rem; }
|
|
73
|
+
.sds--lg { --sds-py: 7.5rem; }
|
|
74
|
+
.sds__inner { max-width: 1180px; margin: 0 auto; display: grid; gap: 1.25rem; }
|
|
75
|
+
.sds__head { text-align: center; max-width: 640px; margin: 0 auto 1.75rem; }
|
|
76
|
+
.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); }
|
|
77
|
+
.sds__live { width: 8px; height: 8px; border-radius: 50%; background: var(--chart-pos, #34c38f); box-shadow: 0 0 10px var(--chart-pos, #34c38f); }
|
|
78
|
+
.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); }
|
|
79
|
+
.sds__subtitle { margin: 0; font-size: 1.0625rem; color: var(--c-text-2, #a3a3a3); }
|
|
80
|
+
.sds__kpis { display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); gap: 1.25rem; }
|
|
81
|
+
.sds__charts { display: grid; grid-template-columns: 1.4fr 1fr; gap: 1.25rem; }
|
|
82
|
+
@media (max-width: 900px) { .sds__charts { grid-template-columns: 1fr; } }
|
|
83
|
+
</style>
|