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,136 @@
1
+ ---
2
+ /**
3
+ * LinearProgressGauge — Multi-segmented horizontal memory or budget progress bar with breakdown legend.
4
+ *
5
+ * @param {string} title - Gauge header label. Default is 'Resource Allocation'.
6
+ * @param {Array<{label: string, value: number, color: string}>} segments - Proportion breakdown chunks.
7
+ * @param {string} total - Formatted total capacity string (e.g. '16 GB', '100%'). Default is '100%'.
8
+ */
9
+ interface Props {
10
+ title?: string;
11
+ segments?: Array<{ label: string; value: number; color: string }>;
12
+ total?: string;
13
+ }
14
+
15
+ const {
16
+ title = 'System Resource Allocation',
17
+ segments = [
18
+ { label: 'SSR Execution', value: 45, color: '#d4a017' },
19
+ { label: 'Asset Caching', value: 25, color: '#38bdf8' },
20
+ { label: 'Edge Worker', value: 15, color: '#a855f7' },
21
+ { label: 'Free Headroom', value: 15, color: 'rgba(255,255,255,0.1)' },
22
+ ],
23
+ total = '100%',
24
+ } = Astro.props;
25
+
26
+ const totalSum = segments.reduce((acc, s) => acc + s.value, 0) || 100;
27
+ ---
28
+
29
+ <div class="linear-gauge">
30
+ <div class="linear-gauge__header">
31
+ <span class="linear-gauge__title">{title}</span>
32
+ <span class="linear-gauge__total">{total}</span>
33
+ </div>
34
+
35
+ <div class="linear-gauge__bar" role="progressbar" aria-valuemax="100" aria-valuemin="0">
36
+ {segments.map((seg) => {
37
+ const pct = (seg.value / totalSum) * 100;
38
+ return (
39
+ <div
40
+ class="linear-gauge__chunk"
41
+ style={`width: ${pct}%; background-color: ${seg.color};`}
42
+ title={`${seg.label}: ${seg.value}%`}
43
+ />
44
+ );
45
+ })}
46
+ </div>
47
+
48
+ <div class="linear-gauge__legend">
49
+ {segments.map((seg) => (
50
+ <div class="legend-item">
51
+ <span class="legend-item__dot" style={`background-color: ${seg.color};`}></span>
52
+ <span class="legend-item__name">{seg.label}</span>
53
+ <span class="legend-item__val">{seg.value}%</span>
54
+ </div>
55
+ ))}
56
+ </div>
57
+ </div>
58
+
59
+ <style>
60
+ .linear-gauge {
61
+ display: flex;
62
+ flex-direction: column;
63
+ gap: 12px;
64
+ width: 100%;
65
+ max-width: 440px;
66
+ padding: var(--sp-6, 1.5rem);
67
+ background: rgba(255, 255, 255, 0.02);
68
+ backdrop-filter: blur(16px);
69
+ border: 1px solid rgba(255, 255, 255, 0.08);
70
+ border-radius: var(--r-xl, 20px);
71
+ font-family: var(--font-sans, system-ui, sans-serif);
72
+ }
73
+
74
+ .linear-gauge__header {
75
+ display: flex;
76
+ justify-content: space-between;
77
+ align-items: center;
78
+ }
79
+
80
+ .linear-gauge__title {
81
+ font-size: 0.88rem;
82
+ font-weight: 700;
83
+ color: var(--c-text-1, #ffffff);
84
+ }
85
+
86
+ .linear-gauge__total {
87
+ font-size: 0.82rem;
88
+ font-weight: 800;
89
+ color: var(--c-primary-light, #d4a017);
90
+ }
91
+
92
+ .linear-gauge__bar {
93
+ display: flex;
94
+ height: 10px;
95
+ border-radius: var(--r-full, 9999px);
96
+ overflow: hidden;
97
+ background: rgba(255, 255, 255, 0.05);
98
+ box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.5);
99
+ }
100
+
101
+ .linear-gauge__chunk {
102
+ height: 100%;
103
+ transition: width 0.4s ease;
104
+ }
105
+
106
+ .linear-gauge__legend {
107
+ display: grid;
108
+ grid-template-columns: repeat(2, 1fr);
109
+ gap: 8px 12px;
110
+ margin-top: 4px;
111
+ }
112
+
113
+ .legend-item {
114
+ display: flex;
115
+ align-items: center;
116
+ gap: 6px;
117
+ font-size: 0.75rem;
118
+ }
119
+
120
+ .legend-item__dot {
121
+ width: 8px;
122
+ height: 8px;
123
+ border-radius: 50%;
124
+ flex-shrink: 0;
125
+ }
126
+
127
+ .legend-item__name {
128
+ color: var(--c-text-2, #a3a3a3);
129
+ flex: 1;
130
+ }
131
+
132
+ .legend-item__val {
133
+ font-weight: 700;
134
+ color: var(--c-text-1, #ffffff);
135
+ }
136
+ </style>
@@ -0,0 +1,100 @@
1
+ ---
2
+ /**
3
+ * LollipopChart — Lightweight alternative to a bar chart: a thin stem with a dot
4
+ * at the value. Optionally draws a target/benchmark line.
5
+ *
6
+ * @param {Array<{label: string, value: number}>} items - Values to plot.
7
+ * @param {number} target - Optional benchmark value.
8
+ * @param {string} targetLabel - Label for the benchmark line.
9
+ * @param {string} prefix - Value prefix.
10
+ * @param {string} suffix - Value suffix.
11
+ * @param {string} color - Stem/dot 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, formatValue } from './chart-utils';
17
+
18
+ interface Props {
19
+ title?: string;
20
+ subtitle?: string;
21
+ items?: Array<{ label: string; value: number }>;
22
+ target?: number;
23
+ targetLabel?: string;
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 = 'Lighthouse performance',
35
+ subtitle = 'Score per page template',
36
+ items = [
37
+ { label: 'Home', value: 98 },
38
+ { label: 'Docs', value: 96 },
39
+ { label: 'Blog', value: 91 },
40
+ { label: 'Pricing', value: 88 },
41
+ { label: 'Dashboard', value: 74 },
42
+ { label: 'Checkout', value: 81 },
43
+ { label: 'Search', value: 69 },
44
+ ],
45
+ target = 90,
46
+ targetLabel = 'Goal',
47
+ prefix = '',
48
+ suffix = '',
49
+ height = 270,
50
+ color = 'var(--chart-1, #3987e5)',
51
+ variant = 'glass',
52
+ size = 'md',
53
+ className = '',
54
+ } = Astro.props;
55
+
56
+ const W = 640;
57
+ const H = height;
58
+ const m = { t: 16, r: 12, b: 30, l: 40 };
59
+ const ticks = niceTicks(0, Math.max(...items.map((i) => i.value), target ?? 0), 4);
60
+ const y = scaleLinear(0, ticks[ticks.length - 1], H - m.b, m.t);
61
+ const band = (W - m.l - m.r) / items.length;
62
+ const fmt = (v: number) => formatValue(v, prefix, suffix);
63
+ ---
64
+
65
+ <ChartFrame
66
+ title={title}
67
+ subtitle={subtitle}
68
+ variant={variant}
69
+ size={size}
70
+ className={className}
71
+ table={{ headers: ['Item', 'Value', targetLabel], rows: items.map((i) => [i.label, fmt(i.value), target !== undefined ? (i.value >= target ? 'Met' : 'Below') : '—']) }}
72
+ >
73
+ <svg viewBox={`0 0 ${W} ${H}`} role="img" aria-label={`${title}: lollipop chart`}>
74
+ {ticks.map((t) => (
75
+ <g>
76
+ <line class={t === 0 ? 'cf-axis' : 'cf-grid'} x1={m.l} x2={W - m.r} y1={y(t)} y2={y(t)} />
77
+ <text class="cf-label" x={m.l - 8} y={y(t) + 4} text-anchor="end">{fmt(t)}</text>
78
+ </g>
79
+ ))}
80
+ {target !== undefined && (
81
+ <g aria-hidden="true">
82
+ <line x1={m.l} x2={W - m.r} y1={y(target)} y2={y(target)} stroke="var(--c-text-1, #fff)" stroke-opacity="0.55" stroke-width="1" stroke-dasharray="4 4" />
83
+ <text class="cf-label" x={W - m.r} y={y(target) - 6} text-anchor="end">{targetLabel} {fmt(target)}</text>
84
+ </g>
85
+ )}
86
+ {items.map((it, i) => {
87
+ const cx = m.l + i * band + band / 2;
88
+ const below = target !== undefined && target > it.value;
89
+ return (
90
+ <g class="cf-mark cf-dim" tabindex="0" role="img" aria-label={`${it.label}: ${fmt(it.value)}${below ? ', below goal' : ''}`}
91
+ data-tip-title={it.label} data-tip={`${color}::${fmt(it.value)}${target !== undefined ? `|${below ? 'Below' : 'Meets'} ${targetLabel.toLowerCase()}` : ''}`}>
92
+ <line x1={cx} x2={cx} y1={y(0)} y2={y(it.value)} stroke={color} stroke-width="2" stroke-opacity={below ? 0.5 : 1} />
93
+ <circle cx={cx} cy={y(it.value)} r="7" fill={below ? 'var(--chart-surface, #0d0d0f)' : color} stroke={color} stroke-width="2.5" />
94
+ <rect x={cx - band / 2} y={m.t} width={band} height={H - m.t - m.b} fill="transparent" />
95
+ <text class="cf-label" x={cx} y={H - 10} text-anchor="middle">{it.label}</text>
96
+ </g>
97
+ );
98
+ })}
99
+ </svg>
100
+ </ChartFrame>
@@ -0,0 +1,122 @@
1
+ ---
2
+ /**
3
+ * MeterGauge — Speedometer-style dial with labelled threshold zones and a
4
+ * needle. Zones use status colors and every zone is named in text.
5
+ *
6
+ * @param {number} value - Current reading.
7
+ * @param {number} min - Scale minimum.
8
+ * @param {number} max - Scale maximum.
9
+ * @param {Array<{to: number, label: string, tone: 'good'|'warn'|'bad'}>} zones - Ascending zones.
10
+ * @param {string} unit - Unit label.
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 { arcStroke, polar } from './chart-utils';
16
+
17
+ interface Zone { to: number; label: string; tone: 'good' | 'warn' | 'bad' }
18
+ interface Props {
19
+ title?: string;
20
+ subtitle?: string;
21
+ value?: number;
22
+ min?: number;
23
+ max?: number;
24
+ zones?: Zone[];
25
+ unit?: string;
26
+ variant?: 'glass' | 'solid' | 'ghost';
27
+ size?: 'sm' | 'md' | 'lg';
28
+ className?: string;
29
+ }
30
+
31
+ const {
32
+ title = 'CPU load',
33
+ subtitle = 'Cluster average, live',
34
+ value = 68,
35
+ min = 0,
36
+ max = 100,
37
+ zones = [
38
+ { to: 60, label: 'Healthy', tone: 'good' },
39
+ { to: 85, label: 'Elevated', tone: 'warn' },
40
+ { to: 100, label: 'Critical', tone: 'bad' },
41
+ ],
42
+ unit = '%',
43
+ variant = 'glass',
44
+ size = 'md',
45
+ className = '',
46
+ } = Astro.props;
47
+
48
+ const toneColor = { good: 'var(--chart-pos, #34c38f)', warn: 'var(--chart-4, #c98500)', bad: 'var(--chart-neg, #f06a6a)' };
49
+ const W = 280;
50
+ const H = 170;
51
+ const cx = W / 2;
52
+ const cy = 150;
53
+ const r = 112;
54
+ const span = (Math.PI * 4) / 3; // 240°
55
+ const start = -span / 2;
56
+ const ang = (v: number) => start + ((Math.min(max, Math.max(min, v)) - min) / (max - min || 1)) * span;
57
+ let prev = min;
58
+ const arcs = zones.map((z) => {
59
+ const d = arcStroke(cx, cy - 20, r, ang(prev) + 0.015, ang(z.to) - 0.015);
60
+ prev = z.to;
61
+ return { ...z, d };
62
+ });
63
+ const current = zones.find((z) => value <= z.to) ?? zones[zones.length - 1];
64
+ const [nx, ny] = polar(cx, cy - 20, r - 24, ang(value));
65
+ const ticks = [min, min + (max - min) / 2, max];
66
+ ---
67
+
68
+ <ChartFrame
69
+ title={title}
70
+ subtitle={subtitle}
71
+ variant={variant}
72
+ size={size}
73
+ className={className}
74
+ table={{ headers: ['Zone', 'Up to'], rows: zones.map((z) => [z.label, `${z.to}${unit}`]) }}
75
+ >
76
+ <div class="mg" role="meter" aria-valuemin={min} aria-valuemax={max} aria-valuenow={value} aria-valuetext={`${value}${unit}, ${current.label}`} aria-label={title}>
77
+ <svg viewBox={`0 0 ${W} ${H}`} aria-hidden="true">
78
+ {arcs.map((a) => <path d={a.d} fill="none" stroke={toneColor[a.tone]} stroke-width="14" stroke-linecap="butt" opacity={a.label === current.label ? 1 : 0.3} />)}
79
+ {ticks.map((t) => {
80
+ const [tx, ty] = polar(cx, cy - 20, r + 18, ang(t));
81
+ return <text class="cf-label" x={tx} y={ty + 4} text-anchor="middle">{t}</text>;
82
+ })}
83
+ <line class="mg__needle" x1={cx} y1={cy - 20} x2={nx} y2={ny} stroke="var(--c-text-1, #fff)" stroke-width="3" stroke-linecap="round" />
84
+ <circle cx={cx} cy={cy - 20} r="7" fill="var(--c-text-1, #fff)" />
85
+ <circle cx={cx} cy={cy - 20} r="3" fill="var(--chart-surface, #0d0d0f)" />
86
+ </svg>
87
+ <div class="mg__readout">
88
+ <span class="mg__value">{value}<small>{unit}</small></span>
89
+ <span class="mg__zone" style={`--mg-c:${toneColor[current.tone]}`}>
90
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" aria-hidden="true">
91
+ {current.tone === 'good' ? <path d="M20 6 9 17l-5-5" /> : <><circle cx="12" cy="12" r="9" /><path d="M12 8v4M12 16h.01" /></>}
92
+ </svg>
93
+ {current.label}
94
+ </span>
95
+ </div>
96
+ </div>
97
+ </ChartFrame>
98
+
99
+ <style>
100
+ .mg { max-width: 320px; margin: 0 auto; text-align: center; }
101
+ .mg__needle {
102
+ transform-origin: 140px 130px;
103
+ animation: mg-swing 1s cubic-bezier(0.34, 1.56, 0.64, 1) both;
104
+ }
105
+ @keyframes mg-swing { from { transform: rotate(-120deg); } }
106
+ .mg__readout { display: flex; flex-direction: column; align-items: center; gap: 0.35rem; margin-top: -0.75rem; }
107
+ .mg__value { font-size: 2rem; font-weight: 750; color: var(--c-text-1, #fff); font-variant-numeric: tabular-nums; line-height: 1; }
108
+ .mg__value small { font-size: 1rem; color: var(--c-text-2, #a3a3a3); margin-left: 2px; }
109
+ .mg__zone {
110
+ display: inline-flex;
111
+ align-items: center;
112
+ gap: 0.3rem;
113
+ padding: 0.2rem 0.6rem;
114
+ border-radius: 999px;
115
+ font-size: 0.75rem;
116
+ font-weight: 600;
117
+ color: var(--c-text-1, #fff);
118
+ background: color-mix(in srgb, var(--mg-c) 20%, transparent);
119
+ }
120
+ .mg__zone svg { color: var(--mg-c); }
121
+ @media (prefers-reduced-motion: reduce) { .mg__needle { animation: none; } }
122
+ </style>
@@ -0,0 +1,125 @@
1
+ ---
2
+ /**
3
+ * MetricDeltaTable — Comparison table with inline bars, current vs previous
4
+ * values and signed deltas with direction icons. A "table + chart" hybrid.
5
+ *
6
+ * @param {Array<{label: string, current: number, previous: number, invert?: boolean}>} rows - Metrics.
7
+ * @param {string} currentLabel - Header for current values.
8
+ * @param {string} previousLabel - Header for previous values.
9
+ * @param {string} prefix - Value prefix.
10
+ * @param {string} suffix - Value suffix.
11
+ * @param {string} color - Inline bar 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 SparklineChart from './SparklineChart.astro';
17
+ import { formatValue } from './chart-utils';
18
+
19
+ interface Row { label: string; current: number; previous: number; invert?: boolean; trend?: number[] }
20
+ interface Props {
21
+ title?: string;
22
+ subtitle?: string;
23
+ rows?: Row[];
24
+ currentLabel?: string;
25
+ previousLabel?: string;
26
+ prefix?: string;
27
+ suffix?: string;
28
+ color?: string;
29
+ variant?: 'glass' | 'solid' | 'ghost';
30
+ size?: 'sm' | 'md' | 'lg';
31
+ className?: string;
32
+ }
33
+
34
+ const {
35
+ title = 'Channel performance',
36
+ subtitle = 'Signups this month vs last month',
37
+ rows = [
38
+ { label: 'Organic search', current: 4820, previous: 4310, trend: [30, 34, 33, 38, 41, 44, 48] },
39
+ { label: 'Direct', current: 3120, previous: 3290, trend: [36, 35, 34, 33, 33, 32, 31] },
40
+ { label: 'Newsletter', current: 1840, previous: 1220, trend: [10, 12, 13, 14, 16, 17, 18] },
41
+ { label: 'Paid social', current: 1310, previous: 1480, trend: [16, 15, 15, 14, 14, 13, 13] },
42
+ { label: 'Partners', current: 960, previous: 710, trend: [6, 7, 7, 8, 8, 9, 10] },
43
+ ],
44
+ currentLabel = 'This month',
45
+ previousLabel = 'Last month',
46
+ prefix = '',
47
+ suffix = '',
48
+ color = 'var(--chart-1, #3987e5)',
49
+ variant = 'glass',
50
+ size = 'md',
51
+ className = '',
52
+ } = Astro.props;
53
+
54
+ const max = Math.max(...rows.map((r) => r.current)) || 1;
55
+ const fmt = (v: number) => formatValue(v, prefix, suffix, false);
56
+ ---
57
+
58
+ <ChartFrame title={title} subtitle={subtitle} variant={variant} size={size} className={className}>
59
+ <div class="md">
60
+ <table class="md__table">
61
+ <thead>
62
+ <tr>
63
+ <th scope="col">Metric</th>
64
+ <th scope="col" class="md__num">{currentLabel}</th>
65
+ <th scope="col" class="md__num md__hide-sm">{previousLabel}</th>
66
+ <th scope="col" class="md__num">Change</th>
67
+ <th scope="col" class="md__hide-sm"><span class="sr-only">Trend</span></th>
68
+ </tr>
69
+ </thead>
70
+ <tbody>
71
+ {rows.map((r) => {
72
+ const d = r.previous ? ((r.current - r.previous) / r.previous) * 100 : 0;
73
+ const up = d >= 0;
74
+ const good = r.invert ? !up : up;
75
+ return (
76
+ <tr>
77
+ <th scope="row">
78
+ <span class="md__label">{r.label}</span>
79
+ <span class="md__bar" aria-hidden="true"><span style={`width:${(r.current / max) * 100}%; background:${color}`}></span></span>
80
+ </th>
81
+ <td class="md__num md__strong">{fmt(r.current)}</td>
82
+ <td class="md__num md__hide-sm">{fmt(r.previous)}</td>
83
+ <td class="md__num">
84
+ <span class={`md__delta ${good ? 'md__delta--good' : 'md__delta--bad'}`}>
85
+ <svg width="10" height="10" viewBox="0 0 10 10" aria-hidden="true"><path d={up ? 'M5 1 L9 7 H1 Z' : 'M5 9 L9 3 H1 Z'} fill="currentColor" /></svg>
86
+ <span class="sr-only">{up ? 'up' : 'down'}</span>
87
+ {Math.abs(d).toFixed(1)}%
88
+ </span>
89
+ </td>
90
+ <td class="md__hide-sm">{r.trend && <SparklineChart data={r.trend} size="sm" variant="line" label={`${r.label} trend`} color={good ? 'var(--chart-pos, #34c38f)' : 'var(--chart-neg, #f06a6a)'} />}</td>
91
+ </tr>
92
+ );
93
+ })}
94
+ </tbody>
95
+ </table>
96
+ </div>
97
+ </ChartFrame>
98
+
99
+ <style>
100
+ .md { overflow-x: auto; }
101
+ .md__table { width: 100%; border-collapse: collapse; font-size: 0.8125rem; font-variant-numeric: tabular-nums; }
102
+ .md__table th, .md__table td {
103
+ padding: 0.7rem 0.6rem;
104
+ border-bottom: 1px solid rgba(255, 255, 255, 0.06);
105
+ text-align: left;
106
+ vertical-align: middle;
107
+ }
108
+ .md__table thead th { font-size: 0.6875rem; font-weight: 600; color: var(--c-text-3, #8a8a8a); text-transform: uppercase; letter-spacing: 0.04em; }
109
+ .md__table tbody tr { transition: background 0.2s ease; }
110
+ .md__table tbody tr:hover { background: rgba(255, 255, 255, 0.03); }
111
+ .md__table tbody th { font-weight: 500; min-width: 9rem; }
112
+ .md__label { display: block; color: var(--c-text-1, #fff); margin-bottom: 0.35rem; }
113
+ .md__bar { display: block; height: 4px; border-radius: 2px; background: rgba(255, 255, 255, 0.05); overflow: hidden; }
114
+ .md__bar span { display: block; height: 100%; border-radius: 2px; }
115
+ .md__num { text-align: right !important; white-space: nowrap; color: var(--c-text-2, #a3a3a3); }
116
+ .md__strong { color: var(--c-text-1, #fff); font-weight: 650; }
117
+ .md__delta { display: inline-flex; align-items: center; gap: 0.3rem; color: var(--c-text-1, #fff); font-weight: 600; }
118
+ .md__delta--good svg { color: var(--chart-pos, #34c38f); }
119
+ .md__delta--bad svg { color: var(--chart-neg, #f06a6a); }
120
+ .sr-only {
121
+ position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
122
+ overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
123
+ }
124
+ @media (max-width: 560px) { .md__hide-sm { display: none; } }
125
+ </style>
@@ -0,0 +1,135 @@
1
+ ---
2
+ /**
3
+ * PieChart — Pure SVG pie or donut with a 2px surface gap between slices,
4
+ * centre total, and a side breakdown with values. Best for ≤ 6 slices.
5
+ *
6
+ * @param {Array<{label: string, value: number}>} slices - Parts of the whole.
7
+ * @param {boolean} donut - Render as a donut with centre label.
8
+ * @param {string} centerLabel - Caption under the centre total.
9
+ * @param {string} prefix - Value prefix.
10
+ * @param {string} suffix - Value suffix.
11
+ * @param {string[]} colors - Optional per-slice 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 { arcPath, colorAt, formatValue, sum } from './chart-utils';
17
+
18
+ interface Props {
19
+ title?: string;
20
+ subtitle?: string;
21
+ slices?: Array<{ label: string; value: number }>;
22
+ donut?: boolean;
23
+ centerLabel?: string;
24
+ prefix?: string;
25
+ suffix?: string;
26
+ colors?: string[];
27
+ variant?: 'glass' | 'solid' | 'ghost';
28
+ size?: 'sm' | 'md' | 'lg';
29
+ className?: string;
30
+ }
31
+
32
+ const {
33
+ title = 'Revenue by plan',
34
+ subtitle = 'Share of MRR',
35
+ slices = [
36
+ { label: 'Enterprise', value: 48200 },
37
+ { label: 'Team', value: 31400 },
38
+ { label: 'Pro', value: 18900 },
39
+ { label: 'Starter', value: 7600 },
40
+ ],
41
+ donut = true,
42
+ centerLabel = 'Total MRR',
43
+ prefix = '$',
44
+ suffix = '',
45
+ colors,
46
+ variant = 'glass',
47
+ size = 'md',
48
+ className = '',
49
+ } = Astro.props;
50
+
51
+ const S = 240;
52
+ const c = S / 2;
53
+ const rO = S / 2 - 4;
54
+ const rI = donut ? rO * 0.62 : 0;
55
+ const total = sum(slices.map((s) => s.value)) || 1;
56
+ const GAP = 0.012; // radians of surface gap between slices
57
+ let a = 0;
58
+ const arcs = slices.slice(0, 8).map((s, i) => {
59
+ const sweep = (s.value / total) * Math.PI * 2;
60
+ const a0 = a + (slices.length > 1 ? GAP / 2 : 0);
61
+ const a1 = a + sweep - (slices.length > 1 ? GAP / 2 : 0);
62
+ a += sweep;
63
+ return { ...s, d: arcPath(c, c, rO, rI, a0, Math.max(a0, a1)), pct: (s.value / total) * 100, color: colorAt(i, colors) };
64
+ });
65
+ const fmt = (v: number) => formatValue(v, prefix, suffix);
66
+ ---
67
+
68
+ <ChartFrame
69
+ title={title}
70
+ subtitle={subtitle}
71
+ variant={variant}
72
+ size={size}
73
+ className={className}
74
+ table={{ headers: ['Slice', 'Value', 'Share'], rows: arcs.map((s) => [s.label, fmt(s.value), `${s.pct.toFixed(1)}%`]) }}
75
+ >
76
+ <div class="pc">
77
+ <svg viewBox={`0 0 ${S} ${S}`} role="img" aria-label={`${title}: ${donut ? 'donut' : 'pie'} chart, total ${fmt(total)}`}>
78
+ {arcs.map((s) => (
79
+ <path class="cf-mark cf-dim pc__slice" d={s.d} fill={s.color} tabindex="0" role="img"
80
+ aria-label={`${s.label}: ${fmt(s.value)} (${s.pct.toFixed(1)}%)`}
81
+ data-tip-title={s.label} data-tip={`${s.color}::${fmt(s.value)} · ${s.pct.toFixed(1)}%`} />
82
+ ))}
83
+ {donut && (
84
+ <g aria-hidden="true">
85
+ <text x={c} y={c + 2} text-anchor="middle" class="pc__total">{fmt(total)}</text>
86
+ <text x={c} y={c + 22} text-anchor="middle" class="cf-label">{centerLabel}</text>
87
+ </g>
88
+ )}
89
+ </svg>
90
+ <ul class="pc__list">
91
+ {arcs.map((s) => (
92
+ <li>
93
+ <span class="pc__dot" style={`background:${s.color}`} aria-hidden="true"></span>
94
+ <span class="pc__name">{s.label}</span>
95
+ <span class="pc__pct">{s.pct.toFixed(0)}%</span>
96
+ <span class="pc__val">{fmt(s.value)}</span>
97
+ </li>
98
+ ))}
99
+ </ul>
100
+ </div>
101
+ </ChartFrame>
102
+
103
+ <style>
104
+ .pc {
105
+ display: grid;
106
+ grid-template-columns: minmax(0, 240px) 1fr;
107
+ gap: 2rem;
108
+ align-items: center;
109
+ }
110
+ .pc__slice { transform-origin: center; transform-box: view-box; transition: transform 0.2s ease, opacity 0.2s ease; }
111
+ .pc__slice:hover, .pc__slice:focus-visible { transform: scale(1.03); }
112
+ .pc__total {
113
+ fill: var(--c-text-1, #fff);
114
+ font-size: 24px;
115
+ font-weight: 700;
116
+ font-variant-numeric: tabular-nums;
117
+ }
118
+ .pc__list { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.7rem; }
119
+ .pc__list li {
120
+ display: grid;
121
+ grid-template-columns: 10px 1fr auto auto;
122
+ align-items: center;
123
+ gap: 0.6rem;
124
+ font-size: 0.875rem;
125
+ }
126
+ .pc__dot { width: 10px; height: 10px; border-radius: 3px; }
127
+ .pc__name { color: var(--c-text-2, #a3a3a3); }
128
+ .pc__pct { color: var(--c-text-3, #8a8a8a); font-size: 0.75rem; font-variant-numeric: tabular-nums; }
129
+ .pc__val { color: var(--c-text-1, #fff); font-weight: 600; font-variant-numeric: tabular-nums; min-width: 4rem; text-align: right; }
130
+ @media (max-width: 560px) {
131
+ .pc { grid-template-columns: 1fr; justify-items: center; }
132
+ .pc svg { max-width: 220px; }
133
+ .pc__list { width: 100%; }
134
+ }
135
+ </style>