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,85 @@
1
+ ---
2
+ /**
3
+ * PolarAreaChart — Equal-angle wedges whose radius encodes value (area-correct
4
+ * via square-root scaling). Good for cyclical categories like months or hours.
5
+ *
6
+ * @param {Array<{label: string, value: number}>} items - Wedges (max 12).
7
+ * @param {string} color - Wedge hue (single series).
8
+ * @param {string} suffix - Value suffix.
9
+ * @param {string} variant - Card style (glass|solid|ghost).
10
+ * @param {string} size - Density (sm|md|lg).
11
+ */
12
+ import ChartFrame from './ChartFrame.astro';
13
+ import { arcPath, polar, formatValue } from './chart-utils';
14
+
15
+ interface Props {
16
+ title?: string;
17
+ subtitle?: string;
18
+ items?: Array<{ label: string; value: number }>;
19
+ color?: string;
20
+ prefix?: string;
21
+ suffix?: string;
22
+ variant?: 'glass' | 'solid' | 'ghost';
23
+ size?: 'sm' | 'md' | 'lg';
24
+ className?: string;
25
+ }
26
+
27
+ const {
28
+ title = 'Orders by month',
29
+ subtitle = 'Seasonality at a glance',
30
+ items = [
31
+ { label: 'Jan', value: 320 }, { label: 'Feb', value: 280 }, { label: 'Mar', value: 360 },
32
+ { label: 'Apr', value: 410 }, { label: 'May', value: 470 }, { label: 'Jun', value: 520 },
33
+ { label: 'Jul', value: 490 }, { label: 'Aug', value: 450 }, { label: 'Sep', value: 430 },
34
+ { label: 'Oct', value: 510 }, { label: 'Nov', value: 690 }, { label: 'Dec', value: 820 },
35
+ ],
36
+ color = 'var(--chart-7, #9085e9)',
37
+ prefix = '',
38
+ suffix = '',
39
+ variant = 'glass',
40
+ size = 'md',
41
+ className = '',
42
+ } = Astro.props;
43
+
44
+ const S = 320;
45
+ const c = S / 2;
46
+ const R = S / 2 - 26;
47
+ const max = Math.max(...items.map((i) => i.value)) || 1;
48
+ const n = Math.min(items.length, 12);
49
+ const sweep = (Math.PI * 2) / n;
50
+ const rings = [0.25, 0.5, 0.75, 1];
51
+ const fmt = (v: number) => formatValue(v, prefix, suffix);
52
+ ---
53
+
54
+ <ChartFrame
55
+ title={title}
56
+ subtitle={subtitle}
57
+ variant={variant}
58
+ size={size}
59
+ className={className}
60
+ table={{ headers: ['Item', 'Value'], rows: items.map((i) => [i.label, fmt(i.value)]) }}
61
+ >
62
+ <div class="pa">
63
+ <svg viewBox={`0 0 ${S} ${S}`} role="img" aria-label={`${title}: polar area chart of ${n} categories`}>
64
+ {rings.map((f) => <circle class="cf-grid" cx={c} cy={c} r={R * Math.sqrt(f)} fill="none" />)}
65
+ {items.slice(0, n).map((it, i) => {
66
+ const r = R * Math.sqrt(it.value / max);
67
+ const a0 = i * sweep + 0.012;
68
+ const a1 = (i + 1) * sweep - 0.012;
69
+ const [lx, ly] = polar(c, c, R + 14, i * sweep + sweep / 2);
70
+ const isMax = it.value === max;
71
+ return (
72
+ <g>
73
+ <path class="cf-mark cf-dim" d={arcPath(c, c, r, 0, a0, a1)} fill={color} fill-opacity={0.35 + 0.6 * (it.value / max)}
74
+ tabindex="0" role="img" aria-label={`${it.label}: ${fmt(it.value)}`} data-tip-title={it.label} data-tip={`${color}::${fmt(it.value)}`} />
75
+ <text class={isMax ? 'cf-label cf-label--strong' : 'cf-label'} x={lx} y={ly + 4} text-anchor="middle">{it.label}</text>
76
+ </g>
77
+ );
78
+ })}
79
+ </svg>
80
+ </div>
81
+ </ChartFrame>
82
+
83
+ <style>
84
+ .pa { max-width: 360px; margin: 0 auto; }
85
+ </style>
@@ -0,0 +1,122 @@
1
+ ---
2
+ /**
3
+ * PopulationPyramid — Back-to-back horizontal bars comparing two groups across
4
+ * ordered bands (age groups, tiers, price brackets).
5
+ *
6
+ * @param {string[]} bands - Row labels, top to bottom.
7
+ * @param {number[]} left - Values for the left group.
8
+ * @param {number[]} right - Values for the right group.
9
+ * @param {string} leftLabel - Left group name.
10
+ * @param {string} rightLabel - Right group name.
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 { formatValue, colorAt } from './chart-utils';
17
+
18
+ interface Props {
19
+ title?: string;
20
+ subtitle?: string;
21
+ bands?: string[];
22
+ left?: number[];
23
+ right?: number[];
24
+ leftLabel?: string;
25
+ rightLabel?: string;
26
+ prefix?: string;
27
+ suffix?: string;
28
+ colors?: string[];
29
+ variant?: 'glass' | 'solid' | 'ghost';
30
+ size?: 'sm' | 'md' | 'lg';
31
+ className?: string;
32
+ }
33
+
34
+ const {
35
+ title = 'Audience by age',
36
+ subtitle = 'Share of users per age band',
37
+ bands = ['65+', '55–64', '45–54', '35–44', '25–34', '18–24'],
38
+ left = [2.1, 4.8, 8.2, 12.6, 15.4, 7.9],
39
+ right = [1.7, 4.1, 7.4, 11.8, 16.2, 8.8],
40
+ leftLabel = 'Women',
41
+ rightLabel = 'Men',
42
+ prefix = '',
43
+ suffix = '%',
44
+ colors,
45
+ variant = 'glass',
46
+ size = 'md',
47
+ className = '',
48
+ } = Astro.props;
49
+
50
+ const max = Math.max(...left, ...right) || 1;
51
+ const cl = colorAt(0, colors);
52
+ const cr = colorAt(1, colors);
53
+ const fmt = (v: number) => formatValue(v, prefix, suffix);
54
+ ---
55
+
56
+ <ChartFrame
57
+ title={title}
58
+ subtitle={subtitle}
59
+ variant={variant}
60
+ size={size}
61
+ className={className}
62
+ legend={[{ label: leftLabel, color: cl }, { label: rightLabel, color: cr }]}
63
+ table={{ headers: ['Band', leftLabel, rightLabel], rows: bands.map((b, i) => [b, fmt(left[i]), fmt(right[i])]) }}
64
+ >
65
+ <div class="pp" role="list">
66
+ {bands.map((b, i) => (
67
+ <div class="pp__row" role="listitem">
68
+ <div class="pp__side pp__side--l">
69
+ <span class="pp__val">{fmt(left[i])}</span>
70
+ <span class="pp__bar cf-mark" tabindex="0" style={`width:${(left[i] / max) * 100}%; background:${cl}`}
71
+ aria-label={`${b}, ${leftLabel}: ${fmt(left[i])}`} data-tip-title={`${b} · ${leftLabel}`} data-tip={`${cl}::${fmt(left[i])}`}></span>
72
+ </div>
73
+ <span class="pp__band">{b}</span>
74
+ <div class="pp__side pp__side--r">
75
+ <span class="pp__bar cf-mark" tabindex="0" style={`width:${(right[i] / max) * 100}%; background:${cr}`}
76
+ aria-label={`${b}, ${rightLabel}: ${fmt(right[i])}`} data-tip-title={`${b} · ${rightLabel}`} data-tip={`${cr}::${fmt(right[i])}`}></span>
77
+ <span class="pp__val">{fmt(right[i])}</span>
78
+ </div>
79
+ </div>
80
+ ))}
81
+ </div>
82
+ </ChartFrame>
83
+
84
+ <style>
85
+ .pp { display: grid; gap: 4px; }
86
+ .pp__row {
87
+ display: grid;
88
+ grid-template-columns: 1fr 4rem 1fr;
89
+ align-items: center;
90
+ gap: 0.5rem;
91
+ }
92
+ .pp__side {
93
+ display: flex;
94
+ align-items: center;
95
+ gap: 0.5rem;
96
+ height: 24px;
97
+ }
98
+ .pp__side--l { justify-content: flex-end; }
99
+ .pp__bar {
100
+ height: 100%;
101
+ min-width: 2px;
102
+ max-width: calc(100% - 3rem);
103
+ transition: opacity 0.2s ease;
104
+ }
105
+ .pp__side--l .pp__bar { border-radius: 4px 0 0 4px; }
106
+ .pp__side--r .pp__bar { border-radius: 0 4px 4px 0; }
107
+ .pp__bar:hover { opacity: 0.85; }
108
+ .pp__bar:focus-visible { outline: 2px solid var(--c-text-1, #fff); outline-offset: 1px; }
109
+ .pp__band {
110
+ text-align: center;
111
+ font-size: 0.75rem;
112
+ font-weight: 600;
113
+ color: var(--c-text-1, #fff);
114
+ }
115
+ .pp__val {
116
+ font-size: 0.75rem;
117
+ color: var(--c-text-2, #a3a3a3);
118
+ font-variant-numeric: tabular-nums;
119
+ min-width: 2.5rem;
120
+ }
121
+ .pp__side--l .pp__val { text-align: right; }
122
+ </style>
@@ -0,0 +1,89 @@
1
+ ---
2
+ /**
3
+ * PunchCardChart — Day × hour dot matrix where dot area encodes volume.
4
+ * Classic "when are people active" view.
5
+ *
6
+ * @param {string[]} days - Row labels.
7
+ * @param {number[]} hours - Column hours (0–23).
8
+ * @param {number[][]} values - values[day][hourIndex].
9
+ * @param {string} unit - Unit noun for tooltips (e.g. 'commits').
10
+ * @param {string} color - Dot color.
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
+
16
+ interface Props {
17
+ title?: string;
18
+ subtitle?: string;
19
+ days?: string[];
20
+ hours?: number[];
21
+ values?: number[][];
22
+ unit?: string;
23
+ color?: string;
24
+ variant?: 'glass' | 'solid' | 'ghost';
25
+ size?: 'sm' | 'md' | 'lg';
26
+ className?: string;
27
+ }
28
+
29
+ const defaultHours = [0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22];
30
+ const shape = [0.1, 0.05, 0.02, 0.1, 0.6, 0.95, 0.8, 1, 0.9, 0.55, 0.35, 0.2];
31
+ const dayW = [0.9, 1, 1, 0.95, 0.75, 0.25, 0.2];
32
+
33
+ const {
34
+ title = 'Deploy activity',
35
+ subtitle = 'Deploys by weekday and hour (UTC)',
36
+ days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
37
+ hours = defaultHours,
38
+ values = dayW.map((d, di) => shape.map((s, hi) => Math.round(d * s * 42 + ((di * 7 + hi * 3) % 5)))),
39
+ unit = 'deploys',
40
+ color = 'var(--chart-3, #199e70)',
41
+ variant = 'glass',
42
+ size = 'md',
43
+ className = '',
44
+ } = Astro.props;
45
+
46
+ const W = 640;
47
+ const m = { t: 10, r: 10, b: 26, l: 46 };
48
+ const cols = hours.length;
49
+ const cellW = (W - m.l - m.r) / cols;
50
+ const cellH = 34;
51
+ const H = m.t + days.length * cellH + m.b;
52
+ const max = Math.max(...values.flat()) || 1;
53
+ const rMax = Math.min(cellW, cellH) / 2 - 2;
54
+ const pad = (h: number) => `${String(h).padStart(2, '0')}:00`;
55
+ const total = values.flat().reduce((a, b) => a + b, 0);
56
+ ---
57
+
58
+ <ChartFrame
59
+ title={title}
60
+ subtitle={subtitle}
61
+ value={total.toLocaleString('en-US')}
62
+ delta={`total ${unit}`}
63
+ variant={variant}
64
+ size={size}
65
+ className={className}
66
+ table={{ headers: ['Day', ...hours.map(pad)], rows: days.map((d, di) => [d, ...values[di]]) }}
67
+ >
68
+ <svg viewBox={`0 0 ${W} ${H}`} role="img" aria-label={`${title}: punch card of ${unit} by day and hour`}>
69
+ {days.map((d, di) => (
70
+ <g>
71
+ <line class="cf-grid" x1={m.l} x2={W - m.r} y1={m.t + di * cellH + cellH / 2} y2={m.t + di * cellH + cellH / 2} />
72
+ <text class="cf-label" x={m.l - 10} y={m.t + di * cellH + cellH / 2 + 4} text-anchor="end">{d}</text>
73
+ </g>
74
+ ))}
75
+ {hours.map((h, hi) => <text class="cf-label" x={m.l + hi * cellW + cellW / 2} y={H - 8} text-anchor="middle">{pad(h)}</text>)}
76
+ {days.map((d, di) => hours.map((h, hi) => {
77
+ const v = values[di]?.[hi] ?? 0;
78
+ const rad = v ? 2 + Math.sqrt(v / max) * (rMax - 2) : 0;
79
+ const cx = m.l + hi * cellW + cellW / 2;
80
+ const cy = m.t + di * cellH + cellH / 2;
81
+ return (
82
+ <g class="cf-mark cf-dim" tabindex="0" role="img" aria-label={`${d} ${pad(h)}: ${v} ${unit}`} data-tip-title={`${d} · ${pad(h)}`} data-tip={`${color}::${v} ${unit}`}>
83
+ <rect x={cx - cellW / 2} y={cy - cellH / 2} width={cellW} height={cellH} fill="transparent" />
84
+ {rad > 0 && <circle cx={cx} cy={cy} r={rad} fill={color} fill-opacity={0.35 + (v / max) * 0.65} />}
85
+ </g>
86
+ );
87
+ }))}
88
+ </svg>
89
+ </ChartFrame>
@@ -0,0 +1,94 @@
1
+ ---
2
+ /**
3
+ * RadarChart — Pure SVG spider chart comparing up to 3 entities across 5–8
4
+ * axes on a shared 0–max scale, with value tooltips on every vertex.
5
+ *
6
+ * @param {string[]} axes - Attribute names (5–8 recommended).
7
+ * @param {Array<{name: string, data: number[]}>} series - Entities (max 3).
8
+ * @param {number} max - Scale maximum.
9
+ * @param {string[]} colors - Optional per-series colors.
10
+ * @param {string} variant - Card style (glass|solid|ghost).
11
+ * @param {string} size - Density (sm|md|lg).
12
+ */
13
+ import ChartFrame from './ChartFrame.astro';
14
+ import { polar, linePath, colorAt, type Point } from './chart-utils';
15
+
16
+ interface Props {
17
+ title?: string;
18
+ subtitle?: string;
19
+ axes?: string[];
20
+ series?: Array<{ name: string; data: number[] }>;
21
+ max?: number;
22
+ colors?: string[];
23
+ variant?: 'glass' | 'solid' | 'ghost';
24
+ size?: 'sm' | 'md' | 'lg';
25
+ className?: string;
26
+ }
27
+
28
+ const {
29
+ title = 'Framework comparison',
30
+ subtitle = 'Scored 0–10 by the team',
31
+ axes = ['Performance', 'DX', 'Ecosystem', 'SEO', 'Learning curve', 'Flexibility'],
32
+ series = [
33
+ { name: 'Astro', data: [9.5, 8.5, 7, 9.5, 8.5, 8] },
34
+ { name: 'Next', data: [7.5, 8, 9.5, 8, 6.5, 8.5] },
35
+ ],
36
+ max = 10,
37
+ colors,
38
+ variant = 'glass',
39
+ size = 'md',
40
+ className = '',
41
+ } = Astro.props;
42
+
43
+ const S = 340;
44
+ const c = S / 2;
45
+ const R = S / 2 - 54;
46
+ const n = axes.length;
47
+ const ang = (i: number) => (i / n) * Math.PI * 2;
48
+ const levels = [0.25, 0.5, 0.75, 1];
49
+ const shapes = series.slice(0, 3).map((s, si) => {
50
+ const pts: Point[] = s.data.map((v, i) => polar(c, c, R * Math.max(0, Math.min(1, v / max)), ang(i)));
51
+ return { ...s, pts, d: `${linePath(pts)} Z`, color: colorAt(si, colors) };
52
+ });
53
+ ---
54
+
55
+ <ChartFrame
56
+ title={title}
57
+ subtitle={subtitle}
58
+ variant={variant}
59
+ size={size}
60
+ className={className}
61
+ legend={shapes.map((s) => ({ label: s.name, color: s.color }))}
62
+ table={{ headers: ['Axis', ...series.map((s) => s.name)], rows: axes.map((a, i) => [a, ...series.map((s) => s.data[i])]) }}
63
+ >
64
+ <div class="rd">
65
+ <svg viewBox={`0 0 ${S} ${S}`} role="img" aria-label={`${title}: radar chart across ${n} axes`}>
66
+ {levels.map((l) => (
67
+ <path class="cf-grid" fill="none" d={`${linePath(axes.map((_, i) => polar(c, c, R * l, ang(i))))} Z`} />
68
+ ))}
69
+ {axes.map((a, i) => {
70
+ const [x, y] = polar(c, c, R, ang(i));
71
+ const [lx, ly] = polar(c, c, R + 18, ang(i));
72
+ const anchor = 4 > Math.abs(lx - c) ? 'middle' : lx > c ? 'start' : 'end';
73
+ return (
74
+ <g>
75
+ <line class="cf-grid" x1={c} y1={c} x2={x} y2={y} />
76
+ <text class="cf-label" x={lx} y={ly + 4} text-anchor={anchor}>{a}</text>
77
+ </g>
78
+ );
79
+ })}
80
+ {shapes.map((s) => (
81
+ <path d={s.d} fill={s.color} fill-opacity="0.18" stroke={s.color} stroke-width="2" stroke-linejoin="round" aria-hidden="true" />
82
+ ))}
83
+ {shapes.map((s) => s.pts.map(([x, y], i) => (
84
+ <circle class="cf-mark" cx={x} cy={y} r="4.5" fill={s.color} stroke="var(--chart-surface, #0d0d0f)" stroke-width="2"
85
+ tabindex="0" role="img" aria-label={`${s.name}, ${axes[i]}: ${s.data[i]} of ${max}`}
86
+ data-tip-title={axes[i]} data-tip={shapes.map((o) => `${o.color}::${o.name}: ${o.data[i]}`).join('|')} />
87
+ )))}
88
+ </svg>
89
+ </div>
90
+ </ChartFrame>
91
+
92
+ <style>
93
+ .rd { max-width: 400px; margin: 0 auto; }
94
+ </style>
@@ -0,0 +1,168 @@
1
+ ---
2
+ /**
3
+ * RadarMetricChart — Pure SVG radar / spider chart for multi-axis skill and performance visualization.
4
+ *
5
+ * @param {string} title - Chart title. Default is 'Performance Metrics'.
6
+ * @param {Array<{axis: string, value: number}>} data - 5 or 6 axis points (values 0 to 100).
7
+ * @param {string} color - Primary chart fill & stroke color. Default is '#d4a017'.
8
+ * @param {number} size - Pixel width/height of the radar. Default is 320.
9
+ */
10
+ interface Props {
11
+ title?: string;
12
+ data?: Array<{ axis: string; value: number }>;
13
+ color?: string;
14
+ size?: number;
15
+ }
16
+
17
+ const {
18
+ title = 'System Capabilities',
19
+ data = [
20
+ { axis: 'Speed', value: 95 },
21
+ { axis: 'A11y', value: 98 },
22
+ { axis: 'SEO', value: 100 },
23
+ { axis: 'Design', value: 92 },
24
+ { axis: 'Zero-JS', value: 96 },
25
+ { axis: 'Security', value: 90 },
26
+ ],
27
+ color = '#d4a017',
28
+ size = 320,
29
+ } = Astro.props;
30
+
31
+ const center = size / 2;
32
+ const radius = (size - 70) / 2;
33
+ const totalAxes = data.length;
34
+ const angleSlice = (Math.PI * 2) / totalAxes;
35
+
36
+ // Concentric polygon grid levels (20%, 40%, 60%, 80%, 100%)
37
+ const levels = [0.2, 0.4, 0.6, 0.8, 1.0];
38
+ const gridPolygons = levels.map((lvl) => {
39
+ return data
40
+ .map((_, i) => {
41
+ const r = radius * lvl;
42
+ const x = center + r * Math.cos(angleSlice * i - Math.PI / 2);
43
+ const y = center + r * Math.sin(angleSlice * i - Math.PI / 2);
44
+ return `${x},${y}`;
45
+ })
46
+ .join(' ');
47
+ });
48
+
49
+ // Data polygon points
50
+ const dataPoints = data
51
+ .map((d, i) => {
52
+ const r = radius * (Math.min(100, Math.max(0, d.value)) / 100);
53
+ const x = center + r * Math.cos(angleSlice * i - Math.PI / 2);
54
+ const y = center + r * Math.sin(angleSlice * i - Math.PI / 2);
55
+ return `${x},${y}`;
56
+ })
57
+ .join(' ');
58
+ ---
59
+
60
+ <div class="radar-card" style={`--radar-size: ${size}px; --radar-color: ${color};`}>
61
+ {title && <h5 class="radar-card__title">{title}</h5>}
62
+
63
+ <div class="radar-card__wrap">
64
+ <svg width={size} height={size} class="radar-card__svg">
65
+ <!-- Background concentric webs -->
66
+ {gridPolygons.map((pts) => (
67
+ <polygon points={pts} class="radar-web" />
68
+ ))}
69
+
70
+ <!-- Axes radial spokes -->
71
+ {data.map((_, i) => {
72
+ const x2 = center + radius * Math.cos(angleSlice * i - Math.PI / 2);
73
+ const y2 = center + radius * Math.sin(angleSlice * i - Math.PI / 2);
74
+ return <line x1={center} y1={center} x2={x2} y2={y2} class="radar-spoke" />;
75
+ })}
76
+
77
+ <!-- Data polygon -->
78
+ <polygon points={dataPoints} class="radar-data" />
79
+
80
+ <!-- Data nodes -->
81
+ {data.map((d, i) => {
82
+ const r = radius * (d.value / 100);
83
+ const cx = center + r * Math.cos(angleSlice * i - Math.PI / 2);
84
+ const cy = center + r * Math.sin(angleSlice * i - Math.PI / 2);
85
+ return <circle cx={cx} cy={cy} r="4" class="radar-node" />;
86
+ })}
87
+
88
+ <!-- Axis Labels -->
89
+ {data.map((d, i) => {
90
+ const labelRadius = radius + 22;
91
+ const lx = center + labelRadius * Math.cos(angleSlice * i - Math.PI / 2);
92
+ const ly = center + labelRadius * Math.sin(angleSlice * i - Math.PI / 2);
93
+ return (
94
+ <text
95
+ x={lx}
96
+ y={ly}
97
+ class="radar-label"
98
+ text-anchor="middle"
99
+ dominant-baseline="central"
100
+ >
101
+ {d.axis} ({d.value})
102
+ </text>
103
+ );
104
+ })}
105
+ </svg>
106
+ </div>
107
+ </div>
108
+
109
+ <style>
110
+ .radar-card {
111
+ display: flex;
112
+ flex-direction: column;
113
+ align-items: center;
114
+ padding: var(--sp-6, 1.5rem);
115
+ background: rgba(255, 255, 255, 0.02);
116
+ backdrop-filter: blur(16px);
117
+ border: 1px solid rgba(255, 255, 255, 0.08);
118
+ border-radius: var(--r-xl, 24px);
119
+ width: 100%;
120
+ max-width: fit-content;
121
+ font-family: var(--font-sans, system-ui, sans-serif);
122
+ }
123
+
124
+ .radar-card__title {
125
+ font-size: 0.95rem;
126
+ font-weight: 800;
127
+ color: var(--c-text-1, #ffffff);
128
+ margin: 0 0 12px 0;
129
+ }
130
+
131
+ .radar-card__wrap {
132
+ display: flex;
133
+ align-items: center;
134
+ justify-content: center;
135
+ }
136
+
137
+ .radar-web {
138
+ fill: none;
139
+ stroke: rgba(255, 255, 255, 0.08);
140
+ stroke-width: 1;
141
+ }
142
+
143
+ .radar-spoke {
144
+ stroke: rgba(255, 255, 255, 0.08);
145
+ stroke-width: 1;
146
+ }
147
+
148
+ .radar-data {
149
+ fill: rgba(184, 134, 11, 0.25);
150
+ stroke: var(--radar-color);
151
+ stroke-width: 2.5;
152
+ transition: all 0.4s ease;
153
+ }
154
+
155
+ .radar-node {
156
+ fill: var(--radar-color);
157
+ stroke: #050505;
158
+ stroke-width: 2;
159
+ }
160
+
161
+ .radar-label {
162
+ fill: var(--c-text-2, #a3a3a3);
163
+ font-size: 10.5px;
164
+ font-weight: 700;
165
+ text-transform: uppercase;
166
+ letter-spacing: 0.05em;
167
+ }
168
+ </style>
@@ -0,0 +1,124 @@
1
+ ---
2
+ /**
3
+ * RadialBarChart — Concentric progress arcs (one per item) sharing a 0–100%
4
+ * scale. Labels sit at each arc's start so identity never relies on color.
5
+ *
6
+ * @param {Array<{label: string, value: number, max?: number}>} items - Values (default max 100).
7
+ * @param {number} sweep - Arc sweep in degrees (180–360).
8
+ * @param {string[]} colors - Optional per-item colors.
9
+ * @param {string} variant - Card style (glass|solid|ghost).
10
+ * @param {string} size - Density (sm|md|lg).
11
+ */
12
+ import ChartFrame from './ChartFrame.astro';
13
+ import { arcStroke, colorAt } from './chart-utils';
14
+
15
+ interface Props {
16
+ title?: string;
17
+ subtitle?: string;
18
+ items?: Array<{ label: string; value: number; max?: number }>;
19
+ sweep?: number;
20
+ colors?: string[];
21
+ variant?: 'glass' | 'solid' | 'ghost';
22
+ size?: 'sm' | 'md' | 'lg';
23
+ className?: string;
24
+ }
25
+
26
+ const {
27
+ title = 'Feature adoption',
28
+ subtitle = 'Share of active workspaces',
29
+ items = [
30
+ { label: 'Dashboards', value: 86 },
31
+ { label: 'Alerts', value: 64 },
32
+ { label: 'API', value: 47 },
33
+ { label: 'SSO', value: 29 },
34
+ ],
35
+ sweep = 270,
36
+ colors,
37
+ variant = 'glass',
38
+ size = 'md',
39
+ className = '',
40
+ } = Astro.props;
41
+
42
+ const S = 300;
43
+ const cx = S / 2;
44
+ const cy = S / 2;
45
+ const stroke = 16;
46
+ const gap = 8;
47
+ const outer = S / 2 - 14;
48
+ const sweepRad = (Math.min(360, Math.max(180, sweep)) * Math.PI) / 180;
49
+ const start = -sweepRad / 2 - (sweep === 360 ? 0 : 0);
50
+ const rings = items.slice(0, 8).map((it, i) => {
51
+ const r = outer - i * (stroke + gap);
52
+ const pct = Math.max(0, Math.min(1, it.value / (it.max ?? 100)));
53
+ return { ...it, r, pct, color: colorAt(i, colors) };
54
+ });
55
+ ---
56
+
57
+ <ChartFrame
58
+ title={title}
59
+ subtitle={subtitle}
60
+ variant={variant}
61
+ size={size}
62
+ className={className}
63
+ table={{ headers: ['Item', 'Value'], rows: items.map((i) => [i.label, `${i.value}${i.max ? ` / ${i.max}` : '%'}`]) }}
64
+ >
65
+ <div class="rb">
66
+ <svg viewBox={`0 0 ${S} ${S}`} role="img" aria-label={`${title}: radial bar chart`}>
67
+ {rings.map((r) => (
68
+ <g class="cf-mark cf-dim" tabindex="0" role="img" aria-label={`${r.label}: ${Math.round(r.pct * 100)}%`}
69
+ data-tip-title={r.label} data-tip={`${r.color}::${r.value}${r.max ? ` / ${r.max}` : '%'}`}>
70
+ <path d={arcStroke(cx, cy, r.r, start, start + sweepRad)} fill="none" stroke="rgba(255,255,255,0.06)" stroke-width={stroke} stroke-linecap="round" />
71
+ {r.pct > 0 && (
72
+ <path class="rb__arc" d={arcStroke(cx, cy, r.r, start, start + sweepRad * r.pct)} fill="none" stroke={r.color} stroke-width={stroke} stroke-linecap="round" />
73
+ )}
74
+ </g>
75
+ ))}
76
+ </svg>
77
+ <ul class="rb__labels" aria-hidden="true">
78
+ {rings.map((r) => (
79
+ <li><span style={`background:${r.color}`}></span>{r.label}<b>{Math.round(r.pct * 100)}%</b></li>
80
+ ))}
81
+ </ul>
82
+ </div>
83
+ </ChartFrame>
84
+
85
+ <style>
86
+ .rb {
87
+ display: grid;
88
+ grid-template-columns: minmax(0, 300px) 1fr;
89
+ gap: 1.5rem;
90
+ align-items: center;
91
+ }
92
+ .rb__arc {
93
+ stroke-dasharray: 1200;
94
+ stroke-dashoffset: 1200;
95
+ animation: rb-draw 1.1s cubic-bezier(0.4, 0, 0.2, 1) forwards;
96
+ }
97
+ @keyframes rb-draw { to { stroke-dashoffset: 0; } }
98
+ .rb__labels {
99
+ list-style: none;
100
+ margin: 0;
101
+ padding: 0;
102
+ display: grid;
103
+ gap: 0.75rem;
104
+ }
105
+ .rb__labels li {
106
+ display: flex;
107
+ align-items: center;
108
+ gap: 0.6rem;
109
+ font-size: 0.875rem;
110
+ color: var(--c-text-2, #a3a3a3);
111
+ }
112
+ .rb__labels span { width: 10px; height: 10px; border-radius: 3px; flex-shrink: 0; }
113
+ .rb__labels b {
114
+ margin-left: auto;
115
+ color: var(--c-text-1, #fff);
116
+ font-variant-numeric: tabular-nums;
117
+ }
118
+ @media (max-width: 560px) {
119
+ .rb { grid-template-columns: 1fr; }
120
+ }
121
+ @media (prefers-reduced-motion: reduce) {
122
+ .rb__arc { animation: none; stroke-dashoffset: 0; }
123
+ }
124
+ </style>