astro-component-kit 0.3.0 → 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.
Files changed (115) hide show
  1. package/README.md +159 -176
  2. package/package.json +2 -2
  3. package/src/components/index.ts +16 -202
  4. package/src/components/lib/buttons/NeonBorderButton.astro +162 -0
  5. package/src/components/lib/buttons/RippleButton.astro +199 -0
  6. package/src/components/lib/buttons/SlideRevealButton.astro +159 -0
  7. package/src/components/lib/buttons/index.ts +4 -0
  8. package/src/components/lib/cards/GithubRepoCard.astro +176 -0
  9. package/src/components/lib/cards/PricingCompareMiniCard.astro +196 -0
  10. package/src/components/lib/cards/StatsTrendCard.astro +149 -0
  11. package/src/components/lib/cards/index.ts +3 -0
  12. package/src/components/lib/charts/ActivityRings.astro +105 -0
  13. package/src/components/lib/charts/BoxPlotChart.astro +109 -0
  14. package/src/components/lib/charts/BubbleChart.astro +101 -0
  15. package/src/components/lib/charts/BulletChart.astro +160 -0
  16. package/src/components/lib/charts/BumpChart.astro +92 -0
  17. package/src/components/lib/charts/CandlestickChart.astro +105 -0
  18. package/src/components/lib/charts/ChartFrame.astro +373 -0
  19. package/src/components/lib/charts/DivergingBarChart.astro +98 -0
  20. package/src/components/lib/charts/DumbbellChart.astro +92 -0
  21. package/src/components/lib/charts/ForecastLineChart.astro +125 -0
  22. package/src/components/lib/charts/FunnelChart.astro +122 -0
  23. package/src/components/lib/charts/GaugeChart.astro +111 -0
  24. package/src/components/lib/charts/GoalProgressChart.astro +97 -0
  25. package/src/components/lib/charts/GroupedBarChart.astro +125 -0
  26. package/src/components/lib/charts/HeatmapChart.astro +131 -0
  27. package/src/components/lib/charts/HistogramChart.astro +111 -0
  28. package/src/components/lib/charts/HorizontalBarChart.astro +155 -0
  29. package/src/components/lib/charts/KpiStatTile.astro +116 -0
  30. package/src/components/lib/charts/KpiTrendGrid.astro +60 -0
  31. package/src/components/lib/charts/LineChart.astro +160 -0
  32. package/src/components/lib/charts/LinearProgressGauge.astro +136 -0
  33. package/src/components/lib/charts/LollipopChart.astro +100 -0
  34. package/src/components/lib/charts/MeterGauge.astro +122 -0
  35. package/src/components/lib/charts/MetricDeltaTable.astro +125 -0
  36. package/src/components/lib/charts/PieChart.astro +135 -0
  37. package/src/components/lib/charts/PolarAreaChart.astro +85 -0
  38. package/src/components/lib/charts/PopulationPyramid.astro +122 -0
  39. package/src/components/lib/charts/PunchCardChart.astro +89 -0
  40. package/src/components/lib/charts/RadarChart.astro +94 -0
  41. package/src/components/lib/charts/RadarMetricChart.astro +168 -0
  42. package/src/components/lib/charts/RadialBarChart.astro +124 -0
  43. package/src/components/lib/charts/RangeAreaChart.astro +113 -0
  44. package/src/components/lib/charts/RetentionCohortTable.astro +126 -0
  45. package/src/components/lib/charts/SankeyChart.astro +128 -0
  46. package/src/components/lib/charts/ScatterPlot.astro +139 -0
  47. package/src/components/lib/charts/SegmentedBarChart.astro +119 -0
  48. package/src/components/lib/charts/SlopeChart.astro +105 -0
  49. package/src/components/lib/charts/SparklineChart.astro +87 -0
  50. package/src/components/lib/charts/StackedAreaChart.astro +130 -0
  51. package/src/components/lib/charts/StackedBarChart.astro +136 -0
  52. package/src/components/lib/charts/SunburstChart.astro +103 -0
  53. package/src/components/lib/charts/TreemapChart.astro +94 -0
  54. package/src/components/lib/charts/WaffleChart.astro +121 -0
  55. package/src/components/lib/charts/WaterfallChart.astro +118 -0
  56. package/src/components/lib/charts/chart-tooltip.ts +75 -0
  57. package/src/components/lib/charts/chart-utils.ts +271 -0
  58. package/src/components/lib/charts/index.ts +46 -0
  59. package/src/components/lib/data/ActivityHeatmapGrid.astro +157 -0
  60. package/src/components/lib/data/MetricComparisonRow.astro +109 -0
  61. package/src/components/lib/data/StatusTimelineFeed.astro +168 -0
  62. package/src/components/lib/data/index.ts +3 -0
  63. package/src/components/lib/feedback/ProgressRingCounter.astro +118 -0
  64. package/src/components/lib/feedback/ToastNotificationGroup.astro +173 -0
  65. package/src/components/lib/feedback/index.ts +2 -0
  66. package/src/components/lib/forms/GlassInlineToggleGroup.astro +161 -0
  67. package/src/components/lib/forms/PasswordStrengthInput.astro +242 -0
  68. package/src/components/lib/forms/PhoneCountryInput.astro +145 -0
  69. package/src/components/lib/forms/TagsAutocompleteInput.astro +249 -0
  70. package/src/components/lib/forms/index.ts +5 -0
  71. package/src/components/lib/layout/DiagonalSectionDivider.astro +69 -0
  72. package/src/components/lib/layout/HolyGrailGrid.astro +125 -0
  73. package/src/components/lib/layout/index.ts +3 -1
  74. package/src/components/lib/navigation/QuickJumpDock.astro +125 -0
  75. package/src/components/lib/navigation/StepBreadcrumbBar.astro +151 -0
  76. package/src/components/lib/navigation/SubNavbarPills.astro +102 -0
  77. package/src/components/lib/navigation/index.ts +3 -0
  78. package/src/components/lib/overlays/CookiePreferencesDrawer.astro +226 -0
  79. package/src/components/lib/overlays/FloatingFeedbackWidget.astro +203 -0
  80. package/src/components/lib/overlays/ImageCompareSlider.astro +172 -0
  81. package/src/components/lib/overlays/index.ts +3 -0
  82. package/src/components/lib/sections/BlogGridSection.astro +131 -0
  83. package/src/components/lib/sections/CareersOpenings.astro +139 -0
  84. package/src/components/lib/sections/ChangelogSection.astro +144 -0
  85. package/src/components/lib/sections/ContactSplitMap.astro +139 -0
  86. package/src/components/lib/sections/CtaNewsletterSection.astro +163 -0
  87. package/src/components/lib/sections/CtaSplitImage.astro +147 -0
  88. package/src/components/lib/sections/FaqTwoColumn.astro +119 -0
  89. package/src/components/lib/sections/FeatureAlternatingRows.astro +190 -0
  90. package/src/components/lib/sections/FeatureIconGrid.astro +155 -0
  91. package/src/components/lib/sections/FeatureTabsShowcase.astro +180 -0
  92. package/src/components/lib/sections/FooterMegaSection.astro +155 -0
  93. package/src/components/lib/sections/HeroDashboardPreview.astro +180 -0
  94. package/src/components/lib/sections/HeroMinimalCentered.astro +175 -0
  95. package/src/components/lib/sections/HeroShowcaseSplit.astro +304 -0
  96. package/src/components/lib/sections/HeroTerminal.astro +251 -0
  97. package/src/components/lib/sections/HeroWaitlist.astro +229 -0
  98. package/src/components/lib/sections/HowItWorksSteps.astro +178 -0
  99. package/src/components/lib/sections/IntegrationsGrid.astro +167 -0
  100. package/src/components/lib/sections/LogoCloudSection.astro +96 -0
  101. package/src/components/lib/sections/LogoWallGrid.astro +91 -0
  102. package/src/components/lib/sections/PricingSinglePlan.astro +157 -0
  103. package/src/components/lib/sections/PricingToggleCards.astro +171 -0
  104. package/src/components/lib/sections/StatsCounterBand.astro +122 -0
  105. package/src/components/lib/sections/StatsDashboardSection.astro +83 -0
  106. package/src/components/lib/sections/TestimonialCarousel.astro +165 -0
  107. package/src/components/lib/sections/TestimonialFeatured.astro +120 -0
  108. package/src/components/lib/sections/TestimonialGridSection.astro +188 -0
  109. package/src/components/lib/sections/index.ts +29 -0
  110. package/src/components/lib/typography/AnimatedCounterText.astro +102 -0
  111. package/src/components/lib/typography/CurvedPathHeading.astro +86 -0
  112. package/src/components/lib/typography/GradientTextMarquee.astro +105 -0
  113. package/src/components/lib/typography/HackerDecodeText.astro +85 -0
  114. package/src/components/lib/typography/index.ts +4 -0
  115. package/src/styles/global.css +18 -1
@@ -0,0 +1,122 @@
1
+ ---
2
+ /**
3
+ * FunnelChart — Conversion funnel with tapered stages, stage-to-stage drop-off
4
+ * and overall conversion. Uses an ordinal single-hue ramp (stages are ordered).
5
+ *
6
+ * @param {Array<{label: string, value: number}>} stages - Ordered stages, widest first.
7
+ * @param {string} color - Base hue for the ordinal ramp.
8
+ * @param {string} variant - Card style (glass|solid|ghost).
9
+ * @param {string} size - Density (sm|md|lg).
10
+ */
11
+ import ChartFrame from './ChartFrame.astro';
12
+ import { formatValue } from './chart-utils';
13
+
14
+ interface Props {
15
+ title?: string;
16
+ subtitle?: string;
17
+ stages?: Array<{ label: string; value: number }>;
18
+ color?: string;
19
+ variant?: 'glass' | 'solid' | 'ghost';
20
+ size?: 'sm' | 'md' | 'lg';
21
+ className?: string;
22
+ }
23
+
24
+ const {
25
+ title = 'Signup funnel',
26
+ subtitle = 'Last 30 days',
27
+ stages = [
28
+ { label: 'Visited site', value: 48200 },
29
+ { label: 'Viewed pricing', value: 19800 },
30
+ { label: 'Started trial', value: 6400 },
31
+ { label: 'Activated', value: 3900 },
32
+ { label: 'Paid', value: 1240 },
33
+ ],
34
+ color = 'var(--chart-1, #3987e5)',
35
+ variant = 'glass',
36
+ size = 'md',
37
+ className = '',
38
+ } = Astro.props;
39
+
40
+ const top = stages[0]?.value || 1;
41
+ const overall = ((stages[stages.length - 1]?.value ?? 0) / top) * 100;
42
+ const fmt = (v: number) => formatValue(v);
43
+ ---
44
+
45
+ <ChartFrame
46
+ title={title}
47
+ subtitle={subtitle}
48
+ value={`${overall.toFixed(1)}%`}
49
+ delta="overall conversion"
50
+ variant={variant}
51
+ size={size}
52
+ className={className}
53
+ table={{
54
+ headers: ['Stage', 'Users', '% of top', 'Step conversion'],
55
+ rows: stages.map((s, i) => [s.label, fmt(s.value), `${((s.value / top) * 100).toFixed(1)}%`, i ? `${((s.value / stages[i - 1].value) * 100).toFixed(1)}%` : '—']),
56
+ }}
57
+ >
58
+ <ol class="fn">
59
+ {stages.map((s, i) => {
60
+ const pct = (s.value / top) * 100;
61
+ const step = i ? (s.value / stages[i - 1].value) * 100 : 100;
62
+ const shade = 100 - i * (55 / Math.max(1, stages.length - 1));
63
+ return (
64
+ <li class="fn__stage">
65
+ <div class="fn__meta">
66
+ <span class="fn__label">{s.label}</span>
67
+ <span class="fn__value">{fmt(s.value)}</span>
68
+ </div>
69
+ <div class="fn__track">
70
+ <span
71
+ class="fn__bar cf-mark"
72
+ tabindex="0"
73
+ style={`width:${Math.max(2, pct)}%; background:color-mix(in srgb, ${color} ${shade}%, #0d0d0f)`}
74
+ aria-label={`${s.label}: ${fmt(s.value)}, ${pct.toFixed(1)}% of top${i ? `, ${step.toFixed(1)}% from previous stage` : ''}`}
75
+ data-tip-title={s.label}
76
+ data-tip={`${fmt(s.value)} users|${pct.toFixed(1)}% of top${i ? `|${step.toFixed(1)}% from previous` : ''}`}
77
+ ></span>
78
+ </div>
79
+ {i > 0 && (
80
+ <span class="fn__drop" aria-hidden="true">
81
+ <svg width="10" height="10" viewBox="0 0 10 10"><path d="M5 9 L9 3 H1 Z" fill="currentColor" /></svg>
82
+ {(100 - step).toFixed(0)}% drop
83
+ </span>
84
+ )}
85
+ </li>
86
+ );
87
+ })}
88
+ </ol>
89
+ </ChartFrame>
90
+
91
+ <style>
92
+ .fn { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.85rem; }
93
+ .fn__stage {
94
+ display: grid;
95
+ grid-template-columns: 1fr auto;
96
+ grid-template-areas: 'meta drop' 'track track';
97
+ gap: 0.35rem 1rem;
98
+ }
99
+ .fn__meta { grid-area: meta; display: flex; gap: 0.75rem; align-items: baseline; }
100
+ .fn__label { font-size: 0.8125rem; color: var(--c-text-2, #a3a3a3); }
101
+ .fn__value { font-size: 0.875rem; font-weight: 650; color: var(--c-text-1, #fff); font-variant-numeric: tabular-nums; }
102
+ .fn__drop {
103
+ grid-area: drop;
104
+ display: inline-flex;
105
+ align-items: center;
106
+ gap: 0.3rem;
107
+ font-size: 0.75rem;
108
+ color: var(--c-text-3, #8a8a8a);
109
+ }
110
+ .fn__drop svg { color: var(--chart-neg, #f06a6a); }
111
+ .fn__track { grid-area: track; display: flex; justify-content: center; }
112
+ .fn__bar {
113
+ display: block;
114
+ height: 30px;
115
+ border-radius: 6px;
116
+ transform-origin: center;
117
+ animation: fn-grow 0.7s cubic-bezier(0.4, 0, 0.2, 1) both;
118
+ }
119
+ .fn__bar:focus-visible { outline: 2px solid var(--c-text-1, #fff); outline-offset: 2px; }
120
+ @keyframes fn-grow { from { transform: scaleX(0.2); opacity: 0; } }
121
+ @media (prefers-reduced-motion: reduce) { .fn__bar { animation: none; } }
122
+ </style>
@@ -0,0 +1,111 @@
1
+ ---
2
+ /**
3
+ * GaugeChart — Semicircular progress gauge for a single KPI against a goal,
4
+ * with the number and % of goal always shown as text.
5
+ *
6
+ * @param {number} value - Current value.
7
+ * @param {number} max - Goal / maximum.
8
+ * @param {string} label - Caption under the number.
9
+ * @param {string} prefix - Value prefix.
10
+ * @param {string} suffix - Value suffix.
11
+ * @param {string} color - Arc color.
12
+ * @param {string} variant - Card style (glass|solid|ghost).
13
+ * @param {string} size - Density (sm|md|lg).
14
+ */
15
+ import ChartFrame from './ChartFrame.astro';
16
+ import { arcStroke, formatValue } from './chart-utils';
17
+
18
+ interface Props {
19
+ title?: string;
20
+ subtitle?: string;
21
+ value?: number;
22
+ max?: number;
23
+ label?: string;
24
+ prefix?: string;
25
+ suffix?: string;
26
+ color?: string;
27
+ variant?: 'glass' | 'solid' | 'ghost';
28
+ size?: 'sm' | 'md' | 'lg';
29
+ className?: string;
30
+ }
31
+
32
+ const {
33
+ title = 'Monthly sales goal',
34
+ subtitle = 'November',
35
+ value = 73400,
36
+ max = 100000,
37
+ label = 'of $100k goal',
38
+ prefix = '$',
39
+ suffix = '',
40
+ color = 'var(--chart-1, #3987e5)',
41
+ variant = 'glass',
42
+ size = 'md',
43
+ className = '',
44
+ } = Astro.props;
45
+
46
+ const W = 260;
47
+ const H = 150;
48
+ const cx = W / 2;
49
+ const cy = 132;
50
+ const r = 108;
51
+ const pct = Math.max(0, Math.min(1, value / (max || 1)));
52
+ const a0 = -Math.PI / 2;
53
+ const a1 = Math.PI / 2;
54
+ const fmt = (v: number) => formatValue(v, prefix, suffix);
55
+ ---
56
+
57
+ <ChartFrame
58
+ title={title}
59
+ subtitle={subtitle}
60
+ variant={variant}
61
+ size={size}
62
+ className={className}
63
+ table={{ headers: ['Metric', 'Value', 'Goal', '% of goal'], rows: [[title, fmt(value), fmt(max), `${(pct * 100).toFixed(1)}%`]] }}
64
+ >
65
+ <div class="gc" role="meter" aria-valuemin={0} aria-valuemax={max} aria-valuenow={value} aria-label={`${title}: ${fmt(value)} ${label}, ${(pct * 100).toFixed(0)}%`}>
66
+ <svg viewBox={`0 0 ${W} ${H}`} aria-hidden="true">
67
+ <path d={arcStroke(cx, cy, r, a0, a1)} fill="none" stroke="rgba(255,255,255,0.07)" stroke-width="18" stroke-linecap="round" />
68
+ {pct > 0 && <path class="gc__arc" d={arcStroke(cx, cy, r, a0, a0 + Math.PI * pct)} fill="none" stroke={color} stroke-width="18" stroke-linecap="round" />}
69
+ <text class="cf-label" x={cx - r} y={H - 2} text-anchor="middle">0</text>
70
+ <text class="cf-label" x={cx + r} y={H - 2} text-anchor="middle">{fmt(max)}</text>
71
+ </svg>
72
+ <div class="gc__center">
73
+ <span class="gc__value">{fmt(value)}</span>
74
+ <span class="gc__label">{(pct * 100).toFixed(0)}% {label}</span>
75
+ </div>
76
+ </div>
77
+ </ChartFrame>
78
+
79
+ <style>
80
+ .gc {
81
+ position: relative;
82
+ max-width: 340px;
83
+ margin: 0 auto;
84
+ }
85
+ .gc__arc {
86
+ stroke-dasharray: 400;
87
+ stroke-dashoffset: 400;
88
+ animation: gc-draw 1.1s cubic-bezier(0.4, 0, 0.2, 1) forwards;
89
+ }
90
+ @keyframes gc-draw { to { stroke-dashoffset: 0; } }
91
+ .gc__center {
92
+ position: absolute;
93
+ left: 0;
94
+ right: 0;
95
+ bottom: 6%;
96
+ display: flex;
97
+ flex-direction: column;
98
+ align-items: center;
99
+ }
100
+ .gc__value {
101
+ font-size: clamp(1.5rem, 5vw, 2rem);
102
+ font-weight: 750;
103
+ color: var(--c-text-1, #fff);
104
+ font-variant-numeric: tabular-nums;
105
+ line-height: 1.1;
106
+ }
107
+ .gc__label { font-size: 0.75rem; color: var(--c-text-2, #a3a3a3); }
108
+ @media (prefers-reduced-motion: reduce) {
109
+ .gc__arc { animation: none; stroke-dashoffset: 0; }
110
+ }
111
+ </style>
@@ -0,0 +1,97 @@
1
+ ---
2
+ /**
3
+ * GoalProgressChart — Cumulative progress toward a goal over a period, versus
4
+ * the ideal linear pace, with an "ahead/behind pace" readout.
5
+ *
6
+ * @param {string[]} periods - X-axis labels for the whole period.
7
+ * @param {number[]} values - Per-period amounts achieved so far (may be shorter than periods).
8
+ * @param {number} goal - Target for the end of the period.
9
+ * @param {string} prefix - Value prefix.
10
+ * @param {string} suffix - Value suffix.
11
+ * @param {string} color - Progress color.
12
+ * @param {string} variant - Card style (glass|solid|ghost).
13
+ * @param {string} size - Density (sm|md|lg).
14
+ */
15
+ import ChartFrame from './ChartFrame.astro';
16
+ import { scaleLinear, niceTicks, linePath, areaPath, formatValue, type Point } from './chart-utils';
17
+
18
+ interface Props {
19
+ title?: string;
20
+ subtitle?: string;
21
+ periods?: string[];
22
+ values?: number[];
23
+ goal?: number;
24
+ prefix?: string;
25
+ suffix?: string;
26
+ height?: number;
27
+ color?: string;
28
+ variant?: 'glass' | 'solid' | 'ghost';
29
+ size?: 'sm' | 'md' | 'lg';
30
+ className?: string;
31
+ }
32
+
33
+ const {
34
+ title = 'Q4 bookings goal',
35
+ subtitle = 'Cumulative vs. linear pace',
36
+ periods = ['W1', 'W2', 'W3', 'W4', 'W5', 'W6', 'W7', 'W8', 'W9', 'W10', 'W11', 'W12', 'W13'],
37
+ values = [22, 31, 28, 40, 36, 44, 51, 39],
38
+ goal = 520,
39
+ prefix = '$',
40
+ suffix = 'k',
41
+ height = 270,
42
+ color = 'var(--chart-1, #3987e5)',
43
+ variant = 'glass',
44
+ size = 'md',
45
+ className = '',
46
+ } = Astro.props;
47
+
48
+ const W = 640;
49
+ const H = height;
50
+ const m = { t: 14, r: 16, b: 28, l: 48 };
51
+ const n = periods.length;
52
+ const cum: number[] = [];
53
+ values.forEach((v, i) => cum.push((cum[i - 1] ?? 0) + v));
54
+ const achieved = cum[cum.length - 1] ?? 0;
55
+ const pace = (goal / n) * values.length;
56
+ const ahead = achieved >= pace;
57
+ const ticks = niceTicks(0, Math.max(goal, achieved), 4);
58
+ const y = scaleLinear(0, ticks[ticks.length - 1], H - m.b, m.t);
59
+ const x = (i: number) => m.l + ((i + 1) / n) * (W - m.l - m.r);
60
+ const x0 = m.l;
61
+ const pts: Point[] = [[x0, y(0)], ...cum.map((v, i): Point => [x(i), y(v)])];
62
+ const fmt = (v: number) => formatValue(v, prefix, suffix);
63
+ const pct = (achieved / goal) * 100;
64
+ ---
65
+
66
+ <ChartFrame
67
+ title={title}
68
+ subtitle={subtitle}
69
+ value={`${fmt(achieved)} / ${fmt(goal)}`}
70
+ delta={`${ahead ? 'Ahead of' : 'Behind'} pace by ${fmt(Math.abs(achieved - pace))}`}
71
+ deltaPositive={ahead}
72
+ variant={variant}
73
+ size={size}
74
+ className={className}
75
+ legend={[{ label: 'Achieved', color }, { label: 'Linear pace', color: 'var(--c-text-3, #8a8a8a)' }]}
76
+ table={{ headers: ['Period', 'Amount', 'Cumulative', 'Pace'], rows: periods.map((p, i) => [p, values[i] !== undefined ? fmt(values[i]) : '—', cum[i] !== undefined ? fmt(cum[i]) : '—', fmt((goal / n) * (i + 1))]) }}
77
+ >
78
+ <svg viewBox={`0 0 ${W} ${H}`} role="img" aria-label={`${title}: ${fmt(achieved)} of ${fmt(goal)} (${pct.toFixed(0)}%), ${ahead ? 'ahead of' : 'behind'} pace`}>
79
+ {ticks.map((t) => (
80
+ <g>
81
+ <line class="cf-grid" x1={m.l} x2={W - m.r} y1={y(t)} y2={y(t)} />
82
+ <text class="cf-label" x={m.l - 8} y={y(t) + 4} text-anchor="end">{fmt(t)}</text>
83
+ </g>
84
+ ))}
85
+ {periods.map((p, i) => i % 2 === 0 && <text class="cf-label" x={x(i)} y={H - 8} text-anchor="middle">{p}</text>)}
86
+ <line x1={m.l} x2={W - m.r} y1={y(goal)} y2={y(goal)} stroke="var(--chart-4, #c98500)" stroke-width="1.5" aria-hidden="true" />
87
+ <text class="cf-label cf-label--strong" x={W - m.r} y={y(goal) - 6} text-anchor="end">Goal {fmt(goal)}</text>
88
+ <line x1={x0} y1={y(0)} x2={x(n - 1)} y2={y(goal)} stroke="var(--c-text-3, #8a8a8a)" stroke-width="1.5" stroke-dasharray="5 5" aria-hidden="true" />
89
+ <path d={areaPath(pts, y(0), 'linear')} fill={color} fill-opacity="0.15" aria-hidden="true" />
90
+ <path d={linePath(pts)} fill="none" stroke={color} stroke-width="2.5" stroke-linejoin="round" aria-hidden="true" />
91
+ {cum.map((v, i) => (
92
+ <circle class="cf-mark" cx={x(i)} cy={y(v)} r={i === cum.length - 1 ? 6 : 3.5} fill={color} stroke="var(--chart-surface, #0d0d0f)" stroke-width="2"
93
+ tabindex="0" role="img" aria-label={`${periods[i]}: cumulative ${fmt(v)}`}
94
+ data-tip-title={periods[i]} data-tip={`${color}::Cumulative: ${fmt(v)}|This period: ${fmt(values[i])}|Pace: ${fmt((goal / n) * (i + 1))}`} />
95
+ ))}
96
+ </svg>
97
+ </ChartFrame>
@@ -0,0 +1,125 @@
1
+ ---
2
+ /**
3
+ * GroupedBarChart — Side-by-side vertical bars for comparing several series
4
+ * across categories. Bars have rounded data-ends anchored to the baseline.
5
+ *
6
+ * @param {string[]} categories - X-axis groups.
7
+ * @param {Array<{name: string, data: number[]}>} series - Series per group (max 8).
8
+ * @param {string} prefix - Value prefix.
9
+ * @param {string} suffix - Value suffix.
10
+ * @param {number} height - SVG height in viewBox units (width is 640).
11
+ * @param {string[]} colors - Optional per-series colors.
12
+ * @param {string} variant - Card style (glass|solid|ghost).
13
+ * @param {string} size - Density (sm|md|lg).
14
+ */
15
+ import ChartFrame from './ChartFrame.astro';
16
+ import { colorAt, scaleLinear, niceTicks, formatValue } from './chart-utils';
17
+
18
+ interface Props {
19
+ title?: string;
20
+ subtitle?: string;
21
+ categories?: string[];
22
+ series?: Array<{ name: string; data: number[] }>;
23
+ prefix?: string;
24
+ suffix?: string;
25
+ height?: number;
26
+ colors?: string[];
27
+ variant?: 'glass' | 'solid' | 'ghost';
28
+ size?: 'sm' | 'md' | 'lg';
29
+ className?: string;
30
+ }
31
+
32
+ const {
33
+ title = 'Revenue by region',
34
+ subtitle = 'This year vs last year',
35
+ categories = ['NA', 'EMEA', 'LATAM', 'APAC', 'ANZ'],
36
+ series = [
37
+ { name: '2025', data: [420, 310, 120, 260, 90] },
38
+ { name: '2026', data: [510, 365, 168, 340, 112] },
39
+ ],
40
+ prefix = '$',
41
+ suffix = 'k',
42
+ height = 280,
43
+ colors,
44
+ variant = 'glass',
45
+ size = 'md',
46
+ className = '',
47
+ } = Astro.props;
48
+
49
+ const W = 640;
50
+ const H = height;
51
+ const m = { t: 12, r: 12, b: 28, l: 48 };
52
+ const ticks = niceTicks(0, Math.max(...series.flatMap((s) => s.data)), 4);
53
+ const y = scaleLinear(0, ticks[ticks.length - 1], H - m.b, m.t);
54
+ const groupW = (W - m.l - m.r) / categories.length;
55
+ const inner = groupW * 0.72;
56
+ const gap = 2;
57
+ const barW = (inner - gap * (series.length - 1)) / series.length;
58
+ const R = Math.min(4, barW / 2);
59
+ const fmt = (v: number) => formatValue(v, prefix, suffix);
60
+
61
+ /** Bar with rounded top corners only (data-end), square at the baseline. */
62
+ const barPath = (bx: number, top: number, w: number, base: number) => {
63
+ const h = base - top;
64
+ if (h <= 0) return '';
65
+ const r = Math.min(R, h);
66
+ return `M${bx},${base} V${top + r} Q${bx},${top} ${bx + r},${top} H${bx + w - r} Q${bx + w},${top} ${bx + w},${top + r} V${base} Z`;
67
+ };
68
+ ---
69
+
70
+ <ChartFrame
71
+ title={title}
72
+ subtitle={subtitle}
73
+ variant={variant}
74
+ size={size}
75
+ className={className}
76
+ legend={series.map((s, i) => ({ label: s.name, color: colorAt(i, colors) }))}
77
+ table={{ headers: ['Category', ...series.map((s) => s.name)], rows: categories.map((c, i) => [c, ...series.map((s) => fmt(s.data[i]))]) }}
78
+ >
79
+ <svg viewBox={`0 0 ${W} ${H}`} role="img" aria-label={`${title}: grouped bar chart`}>
80
+ {ticks.map((t) => (
81
+ <g>
82
+ <line class={t === 0 ? 'cf-axis' : 'cf-grid'} x1={m.l} x2={W - m.r} y1={y(t)} y2={y(t)} />
83
+ <text class="cf-label" x={m.l - 8} y={y(t) + 4} text-anchor="end">{fmt(t)}</text>
84
+ </g>
85
+ ))}
86
+ {categories.map((c, ci) => {
87
+ const gx = m.l + ci * groupW + (groupW - inner) / 2;
88
+ return (
89
+ <g>
90
+ <text class="cf-label" x={m.l + ci * groupW + groupW / 2} y={H - 8} text-anchor="middle">{c}</text>
91
+ {series.slice(0, 8).map((s, si) => {
92
+ const bx = gx + si * (barW + gap);
93
+ const v = s.data[ci] ?? 0;
94
+ return (
95
+ <path
96
+ class="cf-mark cf-dim gb__bar"
97
+ d={barPath(bx, y(v), barW, y(0))}
98
+ fill={colorAt(si, colors)}
99
+ tabindex="0"
100
+ role="img"
101
+ aria-label={`${c}, ${s.name}: ${fmt(v)}`}
102
+ data-tip-title={c}
103
+ data-tip={`${colorAt(si, colors)}::${s.name}: ${fmt(v)}`}
104
+ />
105
+ );
106
+ })}
107
+ </g>
108
+ );
109
+ })}
110
+ </svg>
111
+ </ChartFrame>
112
+
113
+ <style>
114
+ .gb__bar {
115
+ transform-origin: bottom;
116
+ transform-box: fill-box;
117
+ animation: gb-grow 0.7s cubic-bezier(0.4, 0, 0.2, 1) both;
118
+ }
119
+ @keyframes gb-grow {
120
+ from { transform: scaleY(0); }
121
+ }
122
+ @media (prefers-reduced-motion: reduce) {
123
+ .gb__bar { animation: none; }
124
+ }
125
+ </style>
@@ -0,0 +1,131 @@
1
+ ---
2
+ /**
3
+ * HeatmapChart — Matrix of cells colored on a single-hue sequential ramp
4
+ * (light = low, saturated = high), with a scale legend.
5
+ *
6
+ * @param {string[]} rows - Row labels.
7
+ * @param {string[]} columns - Column labels.
8
+ * @param {number[][]} values - values[row][col].
9
+ * @param {boolean} showValues - Print values inside cells.
10
+ * @param {string} suffix - Value suffix.
11
+ * @param {string} color - Ramp hue.
12
+ * @param {string} variant - Card style (glass|solid|ghost).
13
+ * @param {string} size - Density (sm|md|lg).
14
+ */
15
+ import ChartFrame from './ChartFrame.astro';
16
+
17
+ interface Props {
18
+ title?: string;
19
+ subtitle?: string;
20
+ rows?: string[];
21
+ columns?: string[];
22
+ values?: number[][];
23
+ showValues?: boolean;
24
+ suffix?: string;
25
+ color?: string;
26
+ variant?: 'glass' | 'solid' | 'ghost';
27
+ size?: 'sm' | 'md' | 'lg';
28
+ className?: string;
29
+ }
30
+
31
+ const {
32
+ title = 'Error rate by service',
33
+ subtitle = 'Per weekday, % of requests',
34
+ rows = ['auth', 'billing', 'search', 'uploads', 'webhooks'],
35
+ columns = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
36
+ values = [
37
+ [0.2, 0.3, 0.2, 0.4, 0.6, 0.1, 0.1],
38
+ [0.8, 1.2, 0.9, 1.6, 2.4, 0.4, 0.3],
39
+ [0.4, 0.5, 0.7, 0.6, 0.9, 0.3, 0.2],
40
+ [1.1, 1.4, 1.9, 2.8, 3.2, 0.9, 0.6],
41
+ [0.3, 0.2, 0.4, 0.5, 0.4, 0.2, 0.1],
42
+ ],
43
+ showValues = true,
44
+ suffix = '%',
45
+ color = 'var(--chart-1, #3987e5)',
46
+ variant = 'glass',
47
+ size = 'md',
48
+ className = '',
49
+ } = Astro.props;
50
+
51
+ const flat = values.flat();
52
+ const min = Math.min(...flat);
53
+ const max = Math.max(...flat);
54
+ const t = (v: number) => (max === min ? 1 : (v - min) / (max - min));
55
+ const fill = (v: number) => `color-mix(in srgb, ${color} ${Math.round(12 + t(v) * 88)}%, #141416)`;
56
+ ---
57
+
58
+ <ChartFrame
59
+ title={title}
60
+ subtitle={subtitle}
61
+ variant={variant}
62
+ size={size}
63
+ className={className}
64
+ table={{ headers: ['', ...columns], rows: rows.map((r, ri) => [r, ...values[ri].map((v) => `${v}${suffix}`)]) }}
65
+ >
66
+ <div class="hm" style={`--hm-cols:${columns.length}`}>
67
+ <div class="hm__grid" role="grid" aria-label={title}>
68
+ <div class="hm__row" role="row">
69
+ <span class="hm__corner" role="columnheader"></span>
70
+ {columns.map((c) => <span class="hm__col" role="columnheader">{c}</span>)}
71
+ </div>
72
+ {rows.map((r, ri) => (
73
+ <div class="hm__row" role="row">
74
+ <span class="hm__rowlabel" role="rowheader">{r}</span>
75
+ {columns.map((c, ci) => {
76
+ const v = values[ri]?.[ci] ?? 0;
77
+ return (
78
+ <span class="hm__cell cf-mark" role="gridcell" tabindex="0" style={`background:${fill(v)}`}
79
+ aria-label={`${r}, ${c}: ${v}${suffix}`} data-tip-title={`${r} · ${c}`} data-tip={`${v}${suffix}`}>
80
+ {showValues && <span class={t(v) > 0.55 ? 'hm__v hm__v--strong' : 'hm__v'}>{v}</span>}
81
+ </span>
82
+ );
83
+ })}
84
+ </div>
85
+ ))}
86
+ </div>
87
+ <div class="hm__scale" aria-hidden="true">
88
+ <span>{min}{suffix}</span>
89
+ <span class="hm__ramp" style={`background:linear-gradient(90deg, ${fill(min)}, ${fill(max)})`}></span>
90
+ <span>{max}{suffix}</span>
91
+ </div>
92
+ </div>
93
+ </ChartFrame>
94
+
95
+ <style>
96
+ .hm { overflow-x: auto; }
97
+ .hm__grid { display: grid; gap: 3px; min-width: calc(5rem + var(--hm-cols) * 2.4rem); }
98
+ .hm__row {
99
+ display: grid;
100
+ grid-template-columns: 5rem repeat(var(--hm-cols), minmax(2.2rem, 1fr));
101
+ gap: 3px;
102
+ }
103
+ .hm__col { text-align: center; font-size: 0.6875rem; color: var(--c-text-3, #8a8a8a); padding-bottom: 0.2rem; }
104
+ .hm__rowlabel {
105
+ display: flex;
106
+ align-items: center;
107
+ font-size: 0.75rem;
108
+ color: var(--c-text-2, #a3a3a3);
109
+ font-family: var(--font-mono, monospace);
110
+ }
111
+ .hm__cell {
112
+ display: grid;
113
+ place-items: center;
114
+ min-height: 34px;
115
+ border-radius: 4px;
116
+ transition: transform 0.15s ease;
117
+ }
118
+ .hm__cell:hover { transform: scale(1.06); }
119
+ .hm__cell:focus-visible { outline: 2px solid var(--c-text-1, #fff); outline-offset: 1px; }
120
+ .hm__v { font-size: 0.6875rem; color: rgba(255, 255, 255, 0.7); font-variant-numeric: tabular-nums; }
121
+ .hm__v--strong { color: #fff; font-weight: 650; }
122
+ .hm__scale {
123
+ display: flex;
124
+ align-items: center;
125
+ gap: 0.6rem;
126
+ margin-top: 0.9rem;
127
+ font-size: 0.6875rem;
128
+ color: var(--c-text-3, #8a8a8a);
129
+ }
130
+ .hm__ramp { flex: 0 1 160px; height: 8px; border-radius: 4px; }
131
+ </style>