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.
Files changed (115) hide show
  1. package/README.md +33 -14
  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,105 @@
1
+ ---
2
+ /**
3
+ * SlopeChart — Before/after comparison of several items between two points in
4
+ * time. Rising lines use the positive status color, falling lines the negative.
5
+ *
6
+ * @param {string} fromLabel - Left column label.
7
+ * @param {string} toLabel - Right column label.
8
+ * @param {Array<{label: string, from: number, to: number}>} items - Items to compare.
9
+ * @param {string} highlight - Optional item label to emphasise (others dim).
10
+ * @param {string} prefix - Value prefix.
11
+ * @param {string} suffix - Value suffix.
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, formatValue, POSITIVE, NEGATIVE } from './chart-utils';
17
+
18
+ interface Props {
19
+ title?: string;
20
+ subtitle?: string;
21
+ fromLabel?: string;
22
+ toLabel?: string;
23
+ items?: Array<{ label: string; from: number; to: number }>;
24
+ highlight?: string;
25
+ prefix?: string;
26
+ suffix?: string;
27
+ height?: number;
28
+ variant?: 'glass' | 'solid' | 'ghost';
29
+ size?: 'sm' | 'md' | 'lg';
30
+ className?: string;
31
+ }
32
+
33
+ const {
34
+ title = 'Conversion rate by plan',
35
+ subtitle = 'Q1 vs Q2',
36
+ fromLabel = 'Q1',
37
+ toLabel = 'Q2',
38
+ items = [
39
+ { label: 'Enterprise', from: 7.8, to: 9.6 },
40
+ { label: 'Team', from: 6.1, to: 6.9 },
41
+ { label: 'Pro', from: 5.4, to: 4.6 },
42
+ { label: 'Starter', from: 3.2, to: 4.1 },
43
+ { label: 'Free trial', from: 2.4, to: 1.9 },
44
+ ],
45
+ highlight,
46
+ prefix = '',
47
+ suffix = '%',
48
+ height = 300,
49
+ variant = 'glass',
50
+ size = 'md',
51
+ className = '',
52
+ } = Astro.props;
53
+
54
+ const W = 640;
55
+ const H = height;
56
+ const m = { t: 34, b: 20 };
57
+ const x0 = 190;
58
+ const x1 = W - 190;
59
+ const vals = items.flatMap((i) => [i.from, i.to]);
60
+ const y = scaleLinear(Math.min(...vals), Math.max(...vals), H - m.b, m.t);
61
+ const fmt = (v: number) => formatValue(v, prefix, suffix);
62
+ ---
63
+
64
+ <ChartFrame
65
+ title={title}
66
+ subtitle={subtitle}
67
+ variant={variant}
68
+ size={size}
69
+ className={className}
70
+ table={{
71
+ headers: ['Item', fromLabel, toLabel, 'Change'],
72
+ rows: items.map((it) => [it.label, fmt(it.from), fmt(it.to), `${it.to >= it.from ? '+' : ''}${(it.to - it.from).toFixed(1)}`]),
73
+ }}
74
+ >
75
+ <svg viewBox={`0 0 ${W} ${H}`} role="img" aria-label={`${title}: slope chart from ${fromLabel} to ${toLabel}`}>
76
+ <line class="cf-axis" x1={x0} x2={x0} y1={m.t - 8} y2={H - m.b + 8} />
77
+ <line class="cf-axis" x1={x1} x2={x1} y1={m.t - 8} y2={H - m.b + 8} />
78
+ <text class="cf-label cf-label--strong" x={x0} y={14} text-anchor="middle">{fromLabel}</text>
79
+ <text class="cf-label cf-label--strong" x={x1} y={14} text-anchor="middle">{toLabel}</text>
80
+
81
+ {items.map((it) => {
82
+ const up = it.to >= it.from;
83
+ const c = up ? POSITIVE : NEGATIVE;
84
+ const dim = highlight && highlight !== it.label;
85
+ return (
86
+ <g
87
+ class="cf-mark cf-dim"
88
+ tabindex="0"
89
+ role="img"
90
+ opacity={dim ? 0.35 : 1}
91
+ aria-label={`${it.label}: ${fmt(it.from)} to ${fmt(it.to)} (${up ? 'up' : 'down'})`}
92
+ data-tip-title={it.label}
93
+ data-tip={`${fromLabel}: ${fmt(it.from)}|${toLabel}: ${fmt(it.to)}|${c}::${up ? 'Up' : 'Down'} ${Math.abs(it.to - it.from).toFixed(1)}`}
94
+ >
95
+ <line x1={x0} y1={y(it.from)} x2={x1} y2={y(it.to)} stroke={c} stroke-width="2" />
96
+ <line x1={x0} y1={y(it.from)} x2={x1} y2={y(it.to)} stroke="transparent" stroke-width="14" />
97
+ <circle cx={x0} cy={y(it.from)} r="4.5" fill={c} stroke="var(--chart-surface, #0d0d0f)" stroke-width="2" />
98
+ <circle cx={x1} cy={y(it.to)} r="4.5" fill={c} stroke="var(--chart-surface, #0d0d0f)" stroke-width="2" />
99
+ <text class="cf-label" x={x0 - 12} y={y(it.from) + 4} text-anchor="end">{it.label} <tspan class="cf-label--strong">{fmt(it.from)}</tspan></text>
100
+ <text class="cf-label" x={x1 + 12} y={y(it.to) + 4}><tspan class="cf-label--strong">{fmt(it.to)}</tspan> {it.label}</text>
101
+ </g>
102
+ );
103
+ })}
104
+ </svg>
105
+ </ChartFrame>
@@ -0,0 +1,87 @@
1
+ ---
2
+ /**
3
+ * SparklineChart — Compact inline trend line for tables, KPI tiles and text.
4
+ * Marks the last point and optionally the min/max.
5
+ *
6
+ * @param {number[]} data - Values to plot.
7
+ * @param {string} label - Accessible description (e.g. 'Revenue, last 30 days').
8
+ * @param {string} variant - Visual style (line|area|bars).
9
+ * @param {string} size - Dimensions (sm|md|lg).
10
+ * @param {string} color - Stroke/fill color; defaults to trend status color.
11
+ * @param {boolean} showExtremes - Mark the min and max points.
12
+ * @param {string} className - Extra CSS class.
13
+ */
14
+ import { scaleLinear, smoothPath, areaPath, POSITIVE, NEGATIVE, type Point } from './chart-utils';
15
+
16
+ interface Props {
17
+ data?: number[];
18
+ label?: string;
19
+ variant?: 'line' | 'area' | 'bars';
20
+ size?: 'sm' | 'md' | 'lg';
21
+ color?: string;
22
+ showExtremes?: boolean;
23
+ className?: string;
24
+ }
25
+
26
+ const {
27
+ data = [12, 15, 11, 18, 17, 22, 19, 25, 24, 29, 27, 33],
28
+ label = 'Trend, last 12 periods',
29
+ variant = 'area',
30
+ size = 'md',
31
+ color,
32
+ showExtremes = false,
33
+ className = '',
34
+ } = Astro.props;
35
+
36
+ const dims = { sm: [80, 24], md: [140, 36], lg: [220, 56] }[size];
37
+ const [W, H] = dims;
38
+ const pad = 3;
39
+ const trendUp = data[data.length - 1] >= data[0];
40
+ const stroke = color ?? (trendUp ? POSITIVE : NEGATIVE);
41
+ const y = scaleLinear(Math.min(...data), Math.max(...data), H - pad, pad);
42
+ const x = (i: number) => pad + (i / Math.max(1, data.length - 1)) * (W - pad * 2);
43
+ const pts: Point[] = data.map((v, i) => [x(i), y(v)]);
44
+ const last = pts[pts.length - 1];
45
+ const minI = data.indexOf(Math.min(...data));
46
+ const maxI = data.indexOf(Math.max(...data));
47
+ const barW = (W - pad * 2) / data.length - 2;
48
+ const change = data[0] ? (((data[data.length - 1] - data[0]) / Math.abs(data[0])) * 100).toFixed(0) : '0';
49
+ ---
50
+
51
+ <svg
52
+ class={`spark spark--${size} ${className}`}
53
+ viewBox={`0 0 ${W} ${H}`}
54
+ width={W}
55
+ height={H}
56
+ role="img"
57
+ aria-label={`${label}: from ${data[0]} to ${data[data.length - 1]} (${trendUp ? '+' : ''}${change}%)`}
58
+ >
59
+ {variant === 'bars' ? (
60
+ data.map((v, i) => {
61
+ const top = y(v);
62
+ return <rect x={pad + i * (barW + 2)} y={top} width={Math.max(1, barW)} height={Math.max(1, H - pad - top)} rx="1.5" fill={stroke} opacity={i === data.length - 1 ? 1 : 0.55} />;
63
+ })
64
+ ) : (
65
+ <>
66
+ {variant === 'area' && <path d={areaPath(pts, H)} fill={stroke} fill-opacity="0.16" />}
67
+ <path d={smoothPath(pts)} fill="none" stroke={stroke} stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" />
68
+ {showExtremes && (
69
+ <>
70
+ <circle cx={pts[minI][0]} cy={pts[minI][1]} r="2.5" fill="var(--c-text-3, #8a8a8a)" />
71
+ <circle cx={pts[maxI][0]} cy={pts[maxI][1]} r="2.5" fill="var(--c-text-1, #fff)" />
72
+ </>
73
+ )}
74
+ <circle cx={last[0]} cy={last[1]} r="3" fill={stroke} stroke="var(--chart-surface, #0d0d0f)" stroke-width="1.5" />
75
+ </>
76
+ )}
77
+ </svg>
78
+
79
+ <style>
80
+ .spark {
81
+ display: inline-block;
82
+ vertical-align: middle;
83
+ overflow: visible;
84
+ max-width: 100%;
85
+ height: auto;
86
+ }
87
+ </style>
@@ -0,0 +1,130 @@
1
+ ---
2
+ /**
3
+ * StackedAreaChart — Stacked (or 100% normalised) SVG area chart for showing how
4
+ * parts contribute to a changing total over time.
5
+ *
6
+ * @param {string[]} categories - X-axis labels.
7
+ * @param {Array<{name: string, data: number[]}>} series - Stacked layers, bottom first (max 8).
8
+ * @param {boolean} normalized - Show each period as 100% share instead of absolute totals.
9
+ * @param {string} curve - Interpolation (smooth|linear|step).
10
+ * @param {string} prefix - Value prefix.
11
+ * @param {string} suffix - Value suffix.
12
+ * @param {number} height - SVG height in viewBox units (width is 640).
13
+ * @param {string} variant - Card style (glass|solid|ghost).
14
+ * @param {string} size - Density (sm|md|lg).
15
+ */
16
+ import ChartFrame from './ChartFrame.astro';
17
+ import { colorAt, scaleLinear, niceTicks, bandPath, curvePath, formatValue, type Point } from './chart-utils';
18
+
19
+ interface Props {
20
+ title?: string;
21
+ subtitle?: string;
22
+ categories?: string[];
23
+ series?: Array<{ name: string; data: number[] }>;
24
+ normalized?: boolean;
25
+ curve?: 'smooth' | 'linear' | 'step';
26
+ prefix?: string;
27
+ suffix?: string;
28
+ height?: number;
29
+ colors?: string[];
30
+ variant?: 'glass' | 'solid' | 'ghost';
31
+ size?: 'sm' | 'md' | 'lg';
32
+ className?: string;
33
+ }
34
+
35
+ const {
36
+ title = 'Traffic by channel',
37
+ subtitle = 'Sessions per month, stacked',
38
+ categories = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep'],
39
+ series = [
40
+ { name: 'Organic', data: [320, 340, 390, 420, 460, 480, 530, 560, 610] },
41
+ { name: 'Paid', data: [180, 210, 200, 240, 260, 250, 270, 300, 290] },
42
+ { name: 'Referral', data: [90, 110, 120, 130, 125, 150, 170, 160, 190] },
43
+ ],
44
+ normalized = false,
45
+ curve = 'smooth',
46
+ prefix = '',
47
+ suffix = '',
48
+ height = 280,
49
+ colors,
50
+ variant = 'glass',
51
+ size = 'md',
52
+ className = '',
53
+ } = Astro.props;
54
+
55
+ const W = 640;
56
+ const H = height;
57
+ const m = { t: 12, r: 16, b: 28, l: 44 };
58
+ const n = categories.length;
59
+ const totals = categories.map((_, i) => series.reduce((a, s) => a + (s.data[i] ?? 0), 0));
60
+ const val = (si: number, i: number) => {
61
+ const v = series[si].data[i] ?? 0;
62
+ return normalized ? (v / (totals[i] || 1)) * 100 : v;
63
+ };
64
+ const ticks = normalized ? [0, 25, 50, 75, 100] : niceTicks(0, Math.max(...totals), 4);
65
+ const y = scaleLinear(0, ticks[ticks.length - 1], H - m.b, m.t);
66
+ const x = (i: number) => m.l + (n <= 1 ? 0 : (i / (n - 1)) * (W - m.l - m.r));
67
+ const step = n > 1 ? (W - m.l - m.r) / (n - 1) : W;
68
+
69
+ const running = categories.map(() => 0);
70
+ const layers = series.slice(0, 8).map((s, si) => {
71
+ const lower: Point[] = categories.map((_, i) => [x(i), y(running[i])]);
72
+ categories.forEach((_, i) => { running[i] += val(si, i); });
73
+ const upper: Point[] = categories.map((_, i) => [x(i), y(running[i])]);
74
+ return { name: s.name, color: colorAt(si, colors), d: bandPath(upper, lower, curve), top: curvePath(upper, curve) };
75
+ });
76
+
77
+ const fmt = (v: number) => (normalized ? `${v.toFixed(0)}%` : formatValue(v, prefix, suffix));
78
+ const fmtRaw = (v: number) => formatValue(v, prefix, suffix);
79
+ ---
80
+
81
+ <ChartFrame
82
+ title={title}
83
+ subtitle={subtitle}
84
+ variant={variant}
85
+ size={size}
86
+ className={className}
87
+ legend={layers.map((l) => ({ label: l.name, color: l.color }))}
88
+ table={{
89
+ headers: ['Period', ...series.map((s) => s.name), 'Total'],
90
+ rows: categories.map((c, i) => [c, ...series.map((s) => fmtRaw(s.data[i])), fmtRaw(totals[i])]),
91
+ }}
92
+ >
93
+ <svg viewBox={`0 0 ${W} ${H}`} role="img" aria-label={`${title}: stacked area chart`}>
94
+ {ticks.map((t) => (
95
+ <g>
96
+ <line class="cf-grid" x1={m.l} x2={W - m.r} y1={y(t)} y2={y(t)} />
97
+ <text class="cf-label" x={m.l - 8} y={y(t) + 4} text-anchor="end">{fmt(t)}</text>
98
+ </g>
99
+ ))}
100
+ {categories.map((c, i) => <text class="cf-label" x={x(i)} y={H - 8} text-anchor="middle">{c}</text>)}
101
+
102
+ {layers.map((l) => (
103
+ <g aria-hidden="true">
104
+ <path d={l.d} fill={l.color} fill-opacity="0.32" />
105
+ <path d={l.top} fill="none" stroke={l.color} stroke-width="2" />
106
+ </g>
107
+ ))}
108
+
109
+ {categories.map((c, i) => (
110
+ <g class="cf-hit-group">
111
+ <line class="cf-cross" x1={x(i)} x2={x(i)} y1={m.t} y2={H - m.b} />
112
+ <rect
113
+ class="cf-hit"
114
+ x={x(i) - step / 2}
115
+ y={m.t}
116
+ width={step}
117
+ height={H - m.t - m.b}
118
+ tabindex="0"
119
+ role="img"
120
+ aria-label={`${c}: ${series.map((s) => `${s.name} ${fmtRaw(s.data[i])}`).join(', ')}; total ${fmtRaw(totals[i])}`}
121
+ data-tip-title={`${c} · ${fmtRaw(totals[i])}`}
122
+ data-tip={[...layers].reverse().map((l, ri) => {
123
+ const si = layers.length - 1 - ri;
124
+ return `${l.color}::${l.name}: ${fmtRaw(series[si].data[i])}${normalized ? ` (${val(si, i).toFixed(0)}%)` : ''}`;
125
+ }).join('|')}
126
+ />
127
+ </g>
128
+ ))}
129
+ </svg>
130
+ </ChartFrame>
@@ -0,0 +1,136 @@
1
+ ---
2
+ /**
3
+ * StackedBarChart — Vertical or horizontal stacked bars, with an optional 100%
4
+ * mode. Segments are separated by a 2px surface gap instead of borders.
5
+ *
6
+ * @param {string[]} categories - Bar labels.
7
+ * @param {Array<{name: string, data: number[]}>} series - Stack layers (max 8).
8
+ * @param {boolean} horizontal - Draw bars horizontally.
9
+ * @param {boolean} normalized - Show each bar as a 100% share.
10
+ * @param {string} prefix - Value prefix.
11
+ * @param {string} suffix - Value suffix.
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
+ horizontal?: boolean;
24
+ normalized?: boolean;
25
+ prefix?: string;
26
+ suffix?: string;
27
+ height?: number;
28
+ colors?: string[];
29
+ variant?: 'glass' | 'solid' | 'ghost';
30
+ size?: 'sm' | 'md' | 'lg';
31
+ className?: string;
32
+ }
33
+
34
+ const {
35
+ title = 'Tickets by status',
36
+ subtitle = 'Per sprint',
37
+ categories = ['S18', 'S19', 'S20', 'S21', 'S22', 'S23'],
38
+ series = [
39
+ { name: 'Done', data: [42, 48, 51, 46, 58, 63] },
40
+ { name: 'In review', data: [12, 9, 14, 11, 10, 8] },
41
+ { name: 'Blocked', data: [5, 7, 3, 6, 4, 2] },
42
+ ],
43
+ horizontal = false,
44
+ normalized = false,
45
+ prefix = '',
46
+ suffix = '',
47
+ height = 280,
48
+ colors,
49
+ variant = 'glass',
50
+ size = 'md',
51
+ className = '',
52
+ } = Astro.props;
53
+
54
+ const W = 640;
55
+ const H = horizontal ? Math.max(height, categories.length * 40 + 40) : height;
56
+ const m = horizontal ? { t: 8, r: 16, b: 24, l: 72 } : { t: 12, r: 12, b: 28, l: 44 };
57
+ const totals = categories.map((_, i) => series.reduce((a, s) => a + (s.data[i] ?? 0), 0));
58
+ const ticks = normalized ? [0, 25, 50, 75, 100] : niceTicks(0, Math.max(...totals), 4);
59
+ const maxT = ticks[ticks.length - 1];
60
+ const v = horizontal ? scaleLinear(0, maxT, m.l, W - m.r) : scaleLinear(0, maxT, H - m.b, m.t);
61
+ const band = horizontal ? (H - m.t - m.b) / categories.length : (W - m.l - m.r) / categories.length;
62
+ const thick = band * 0.6;
63
+ const GAP = 2;
64
+ const fmt = (n: number) => formatValue(n, prefix, suffix);
65
+ const fmtAxis = (n: number) => (normalized ? `${n}%` : fmt(n));
66
+
67
+ const bars = categories.map((c, ci) => {
68
+ let acc = 0;
69
+ const segs = series.slice(0, 8).map((s, si) => {
70
+ const raw = s.data[ci] ?? 0;
71
+ const val = normalized ? (raw / (totals[ci] || 1)) * 100 : raw;
72
+ const start = acc;
73
+ acc += val;
74
+ return { name: s.name, raw, val, start, end: acc, color: colorAt(si, colors) };
75
+ });
76
+ return { c, ci, segs };
77
+ });
78
+ ---
79
+
80
+ <ChartFrame
81
+ title={title}
82
+ subtitle={subtitle}
83
+ variant={variant}
84
+ size={size}
85
+ className={className}
86
+ legend={series.map((s, i) => ({ label: s.name, color: colorAt(i, colors) }))}
87
+ table={{ headers: ['Category', ...series.map((s) => s.name), 'Total'], rows: categories.map((c, i) => [c, ...series.map((s) => fmt(s.data[i])), fmt(totals[i])]) }}
88
+ >
89
+ <svg viewBox={`0 0 ${W} ${H}`} role="img" aria-label={`${title}: ${normalized ? '100% ' : ''}stacked bar chart`}>
90
+ {ticks.map((t) => horizontal ? (
91
+ <g>
92
+ <line class={t === 0 ? 'cf-axis' : 'cf-grid'} x1={v(t)} x2={v(t)} y1={m.t} y2={H - m.b} />
93
+ <text class="cf-label" x={v(t)} y={H - 6} text-anchor="middle">{fmtAxis(t)}</text>
94
+ </g>
95
+ ) : (
96
+ <g>
97
+ <line class={t === 0 ? 'cf-axis' : 'cf-grid'} x1={m.l} x2={W - m.r} y1={v(t)} y2={v(t)} />
98
+ <text class="cf-label" x={m.l - 8} y={v(t) + 4} text-anchor="end">{fmtAxis(t)}</text>
99
+ </g>
100
+ ))}
101
+
102
+ {bars.map(({ c, ci, segs }) => {
103
+ const pos = (horizontal ? m.t : m.l) + ci * band + (band - thick) / 2;
104
+ return (
105
+ <g>
106
+ {horizontal
107
+ ? <text class="cf-label" x={m.l - 8} y={pos + thick / 2 + 4} text-anchor="end">{c}</text>
108
+ : <text class="cf-label" x={pos + thick / 2} y={H - 8} text-anchor="middle">{c}</text>}
109
+ {segs.map((s, si) => {
110
+ const a = v(s.start);
111
+ const b = v(s.end);
112
+ const isLast = si === segs.length - 1;
113
+ const len = Math.abs(b - a) - (isLast ? 0 : GAP);
114
+ if (!(len > 0)) return null;
115
+ const rect = horizontal
116
+ ? { x: a, y: pos, width: len, height: thick }
117
+ : { x: pos, y: b + (isLast ? 0 : GAP), width: thick, height: len };
118
+ return (
119
+ <rect
120
+ class="cf-mark cf-dim"
121
+ {...rect}
122
+ rx={isLast ? 3 : 0}
123
+ fill={s.color}
124
+ tabindex="0"
125
+ role="img"
126
+ aria-label={`${c}, ${s.name}: ${fmt(s.raw)}${normalized ? ` (${s.val.toFixed(0)}%)` : ''}`}
127
+ data-tip-title={c}
128
+ data-tip={`${s.color}::${s.name}: ${fmt(s.raw)}${normalized ? ` · ${s.val.toFixed(0)}%` : ''}`}
129
+ />
130
+ );
131
+ })}
132
+ </g>
133
+ );
134
+ })}
135
+ </svg>
136
+ </ChartFrame>
@@ -0,0 +1,103 @@
1
+ ---
2
+ /**
3
+ * SunburstChart — Two-level hierarchical donut: inner ring for parents, outer
4
+ * ring for children (shaded variants of the parent hue).
5
+ *
6
+ * @param {Array<{label: string, children: Array<{label: string, value: number}>}>} data - Hierarchy.
7
+ * @param {string} centerLabel - Caption in the middle.
8
+ * @param {string} prefix - Value prefix.
9
+ * @param {string} suffix - Value suffix.
10
+ * @param {string[]} colors - Optional per-parent colors.
11
+ * @param {string} variant - Card style (glass|solid|ghost).
12
+ * @param {string} size - Density (sm|md|lg).
13
+ */
14
+ import ChartFrame from './ChartFrame.astro';
15
+ import { arcPath, colorAt, formatValue, sum } from './chart-utils';
16
+
17
+ interface Props {
18
+ title?: string;
19
+ subtitle?: string;
20
+ data?: Array<{ label: string; children: Array<{ label: string; value: number }> }>;
21
+ centerLabel?: string;
22
+ prefix?: string;
23
+ suffix?: string;
24
+ colors?: string[];
25
+ variant?: 'glass' | 'solid' | 'ghost';
26
+ size?: 'sm' | 'md' | 'lg';
27
+ className?: string;
28
+ }
29
+
30
+ const {
31
+ title = 'Cloud spend breakdown',
32
+ subtitle = 'Service → resource',
33
+ data = [
34
+ { label: 'Compute', children: [{ label: 'VMs', value: 4200 }, { label: 'Functions', value: 1800 }, { label: 'Containers', value: 2600 }] },
35
+ { label: 'Storage', children: [{ label: 'Object', value: 1900 }, { label: 'Block', value: 1100 }] },
36
+ { label: 'Network', children: [{ label: 'CDN', value: 1400 }, { label: 'Egress', value: 900 }] },
37
+ { label: 'Data', children: [{ label: 'Postgres', value: 1600 }, { label: 'Redis', value: 500 }] },
38
+ ],
39
+ centerLabel = 'Monthly spend',
40
+ prefix = '$',
41
+ suffix = '',
42
+ colors,
43
+ variant = 'glass',
44
+ size = 'md',
45
+ className = '',
46
+ } = Astro.props;
47
+
48
+ const S = 300;
49
+ const c = S / 2;
50
+ const total = sum(data.flatMap((d) => d.children.map((ch) => ch.value))) || 1;
51
+ const GAP = 0.01;
52
+ let a = 0;
53
+ const parents = data.slice(0, 8).map((d, pi) => {
54
+ const pv = sum(d.children.map((ch) => ch.value));
55
+ const a0 = a;
56
+ const sweep = (pv / total) * Math.PI * 2;
57
+ a += sweep;
58
+ const color = colorAt(pi, colors);
59
+ let ca = a0;
60
+ const kids = d.children.map((ch, ci) => {
61
+ const s = (ch.value / total) * Math.PI * 2;
62
+ const k0 = ca;
63
+ ca += s;
64
+ const mix = 100 - ci * (40 / Math.max(1, d.children.length - 1));
65
+ return { ...ch, d: arcPath(c, c, 146, 104, k0 + GAP / 2, k0 + s - GAP / 2), color: `color-mix(in srgb, ${color} ${mix}%, #0d0d0f)` };
66
+ });
67
+ return { label: d.label, value: pv, color, d: arcPath(c, c, 100, 62, a0 + GAP / 2, a0 + sweep - GAP / 2), kids };
68
+ });
69
+ const fmt = (v: number) => formatValue(v, prefix, suffix);
70
+ const pct = (v: number) => `${((v / total) * 100).toFixed(1)}%`;
71
+ ---
72
+
73
+ <ChartFrame
74
+ title={title}
75
+ subtitle={subtitle}
76
+ variant={variant}
77
+ size={size}
78
+ className={className}
79
+ legend={parents.map((p) => ({ label: p.label, color: p.color }))}
80
+ table={{ headers: ['Group', 'Item', 'Value', 'Share'], rows: parents.flatMap((p) => p.kids.map((k) => [p.label, k.label, fmt(k.value), pct(k.value)])) }}
81
+ >
82
+ <div class="sbt">
83
+ <svg viewBox={`0 0 ${S} ${S}`} role="img" aria-label={`${title}: sunburst chart, total ${fmt(total)}`}>
84
+ {parents.map((p) => (
85
+ <g>
86
+ <path class="cf-mark cf-dim" d={p.d} fill={p.color} tabindex="0" role="img" aria-label={`${p.label}: ${fmt(p.value)} (${pct(p.value)})`}
87
+ data-tip-title={p.label} data-tip={`${p.color}::${fmt(p.value)} · ${pct(p.value)}`} />
88
+ {p.kids.map((k) => (
89
+ <path class="cf-mark cf-dim" d={k.d} fill={k.color} tabindex="0" role="img" aria-label={`${p.label} / ${k.label}: ${fmt(k.value)}`}
90
+ data-tip-title={`${p.label} › ${k.label}`} data-tip={`${k.color}::${fmt(k.value)} · ${pct(k.value)}`} />
91
+ ))}
92
+ </g>
93
+ ))}
94
+ <text x={c} y={c + 2} text-anchor="middle" class="sbt__total">{fmt(total)}</text>
95
+ <text x={c} y={c + 20} text-anchor="middle" class="cf-label">{centerLabel}</text>
96
+ </svg>
97
+ </div>
98
+ </ChartFrame>
99
+
100
+ <style>
101
+ .sbt { max-width: 340px; margin: 0 auto; }
102
+ .sbt__total { fill: var(--c-text-1, #fff); font-size: 22px; font-weight: 700; font-variant-numeric: tabular-nums; }
103
+ </style>
@@ -0,0 +1,94 @@
1
+ ---
2
+ /**
3
+ * TreemapChart — Squarified treemap where each rectangle's area is proportional
4
+ * to its value. Labels appear on tiles large enough to hold them.
5
+ *
6
+ * @param {Array<{label: string, value: number, group?: number}>} items - Tiles (group = color slot index).
7
+ * @param {string} prefix - Value prefix.
8
+ * @param {string} suffix - Value suffix.
9
+ * @param {number} height - SVG height in viewBox units (width is 640).
10
+ * @param {string[]} colors - Optional color overrides.
11
+ * @param {string} variant - Card style (glass|solid|ghost).
12
+ * @param {string} size - Density (sm|md|lg).
13
+ */
14
+ import ChartFrame from './ChartFrame.astro';
15
+ import { squarify, colorAt, formatValue, sum } from './chart-utils';
16
+
17
+ interface Props {
18
+ title?: string;
19
+ subtitle?: string;
20
+ items?: Array<{ label: string; value: number; group?: number }>;
21
+ groups?: string[];
22
+ prefix?: string;
23
+ suffix?: string;
24
+ height?: number;
25
+ colors?: string[];
26
+ variant?: 'glass' | 'solid' | 'ghost';
27
+ size?: 'sm' | 'md' | 'lg';
28
+ className?: string;
29
+ }
30
+
31
+ const {
32
+ title = 'Bundle size by module',
33
+ subtitle = 'Gzipped KB',
34
+ items = [
35
+ { label: 'react-dom', value: 42, group: 0 },
36
+ { label: 'astro runtime', value: 18, group: 1 },
37
+ { label: 'charts', value: 24, group: 2 },
38
+ { label: 'date-fns', value: 12, group: 0 },
39
+ { label: 'router', value: 9, group: 1 },
40
+ { label: 'icons', value: 15, group: 2 },
41
+ { label: 'i18n', value: 7, group: 1 },
42
+ { label: 'utils', value: 5, group: 2 },
43
+ { label: 'zod', value: 11, group: 0 },
44
+ { label: 'analytics', value: 6, group: 0 },
45
+ ],
46
+ groups = ['Vendor', 'Framework', 'App'],
47
+ prefix = '',
48
+ suffix = ' KB',
49
+ height = 320,
50
+ colors,
51
+ variant = 'glass',
52
+ size = 'md',
53
+ className = '',
54
+ } = Astro.props;
55
+
56
+ const W = 640;
57
+ const H = height;
58
+ const rects = squarify(items.map((i) => i.value), { x: 0, y: 0, w: W, h: H });
59
+ const total = sum(items.map((i) => i.value)) || 1;
60
+ const fmt = (v: number) => formatValue(v, prefix, suffix, false);
61
+ const GAP = 2;
62
+ ---
63
+
64
+ <ChartFrame
65
+ title={title}
66
+ subtitle={subtitle}
67
+ variant={variant}
68
+ size={size}
69
+ className={className}
70
+ legend={groups.map((g, i) => ({ label: g, color: colorAt(i, colors) }))}
71
+ table={{ headers: ['Item', 'Group', 'Value', 'Share'], rows: items.map((i) => [i.label, groups[i.group ?? 0] ?? '—', fmt(i.value), `${((i.value / total) * 100).toFixed(1)}%`]) }}
72
+ >
73
+ <svg viewBox={`0 0 ${W} ${H}`} role="img" aria-label={`${title}: treemap of ${items.length} items`}>
74
+ {items.map((it, i) => {
75
+ const r = rects[i];
76
+ const c = colorAt(it.group ?? 0, colors);
77
+ const w = Math.max(0, r.w - GAP);
78
+ const h = Math.max(0, r.h - GAP);
79
+ const showLabel = w > 64 && h > 34;
80
+ return (
81
+ <g class="cf-mark cf-dim" tabindex="0" role="img" aria-label={`${it.label}: ${fmt(it.value)}`}
82
+ data-tip-title={it.label} data-tip={`${c}::${fmt(it.value)} · ${((it.value / total) * 100).toFixed(1)}%`}>
83
+ <rect x={r.x + GAP / 2} y={r.y + GAP / 2} width={w} height={h} rx="6" fill={c} fill-opacity="0.85" />
84
+ {showLabel && (
85
+ <>
86
+ <text x={r.x + 10} y={r.y + 20} fill="#fff" font-size="12" font-weight="650">{it.label}</text>
87
+ <text x={r.x + 10} y={r.y + 36} fill="rgba(255,255,255,0.8)" font-size="11">{fmt(it.value)}</text>
88
+ </>
89
+ )}
90
+ </g>
91
+ );
92
+ })}
93
+ </svg>
94
+ </ChartFrame>