astro-component-kit 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/README.md +159 -176
  2. package/package.json +2 -2
  3. package/src/components/index.ts +16 -202
  4. package/src/components/lib/buttons/NeonBorderButton.astro +162 -0
  5. package/src/components/lib/buttons/RippleButton.astro +199 -0
  6. package/src/components/lib/buttons/SlideRevealButton.astro +159 -0
  7. package/src/components/lib/buttons/index.ts +4 -0
  8. package/src/components/lib/cards/GithubRepoCard.astro +176 -0
  9. package/src/components/lib/cards/PricingCompareMiniCard.astro +196 -0
  10. package/src/components/lib/cards/StatsTrendCard.astro +149 -0
  11. package/src/components/lib/cards/index.ts +3 -0
  12. package/src/components/lib/charts/ActivityRings.astro +105 -0
  13. package/src/components/lib/charts/BoxPlotChart.astro +109 -0
  14. package/src/components/lib/charts/BubbleChart.astro +101 -0
  15. package/src/components/lib/charts/BulletChart.astro +160 -0
  16. package/src/components/lib/charts/BumpChart.astro +92 -0
  17. package/src/components/lib/charts/CandlestickChart.astro +105 -0
  18. package/src/components/lib/charts/ChartFrame.astro +373 -0
  19. package/src/components/lib/charts/DivergingBarChart.astro +98 -0
  20. package/src/components/lib/charts/DumbbellChart.astro +92 -0
  21. package/src/components/lib/charts/ForecastLineChart.astro +125 -0
  22. package/src/components/lib/charts/FunnelChart.astro +122 -0
  23. package/src/components/lib/charts/GaugeChart.astro +111 -0
  24. package/src/components/lib/charts/GoalProgressChart.astro +97 -0
  25. package/src/components/lib/charts/GroupedBarChart.astro +125 -0
  26. package/src/components/lib/charts/HeatmapChart.astro +131 -0
  27. package/src/components/lib/charts/HistogramChart.astro +111 -0
  28. package/src/components/lib/charts/HorizontalBarChart.astro +155 -0
  29. package/src/components/lib/charts/KpiStatTile.astro +116 -0
  30. package/src/components/lib/charts/KpiTrendGrid.astro +60 -0
  31. package/src/components/lib/charts/LineChart.astro +160 -0
  32. package/src/components/lib/charts/LinearProgressGauge.astro +136 -0
  33. package/src/components/lib/charts/LollipopChart.astro +100 -0
  34. package/src/components/lib/charts/MeterGauge.astro +122 -0
  35. package/src/components/lib/charts/MetricDeltaTable.astro +125 -0
  36. package/src/components/lib/charts/PieChart.astro +135 -0
  37. package/src/components/lib/charts/PolarAreaChart.astro +85 -0
  38. package/src/components/lib/charts/PopulationPyramid.astro +122 -0
  39. package/src/components/lib/charts/PunchCardChart.astro +89 -0
  40. package/src/components/lib/charts/RadarChart.astro +94 -0
  41. package/src/components/lib/charts/RadarMetricChart.astro +168 -0
  42. package/src/components/lib/charts/RadialBarChart.astro +124 -0
  43. package/src/components/lib/charts/RangeAreaChart.astro +113 -0
  44. package/src/components/lib/charts/RetentionCohortTable.astro +126 -0
  45. package/src/components/lib/charts/SankeyChart.astro +128 -0
  46. package/src/components/lib/charts/ScatterPlot.astro +139 -0
  47. package/src/components/lib/charts/SegmentedBarChart.astro +119 -0
  48. package/src/components/lib/charts/SlopeChart.astro +105 -0
  49. package/src/components/lib/charts/SparklineChart.astro +87 -0
  50. package/src/components/lib/charts/StackedAreaChart.astro +130 -0
  51. package/src/components/lib/charts/StackedBarChart.astro +136 -0
  52. package/src/components/lib/charts/SunburstChart.astro +103 -0
  53. package/src/components/lib/charts/TreemapChart.astro +94 -0
  54. package/src/components/lib/charts/WaffleChart.astro +121 -0
  55. package/src/components/lib/charts/WaterfallChart.astro +118 -0
  56. package/src/components/lib/charts/chart-tooltip.ts +75 -0
  57. package/src/components/lib/charts/chart-utils.ts +271 -0
  58. package/src/components/lib/charts/index.ts +46 -0
  59. package/src/components/lib/data/ActivityHeatmapGrid.astro +157 -0
  60. package/src/components/lib/data/MetricComparisonRow.astro +109 -0
  61. package/src/components/lib/data/StatusTimelineFeed.astro +168 -0
  62. package/src/components/lib/data/index.ts +3 -0
  63. package/src/components/lib/feedback/ProgressRingCounter.astro +118 -0
  64. package/src/components/lib/feedback/ToastNotificationGroup.astro +173 -0
  65. package/src/components/lib/feedback/index.ts +2 -0
  66. package/src/components/lib/forms/GlassInlineToggleGroup.astro +161 -0
  67. package/src/components/lib/forms/PasswordStrengthInput.astro +242 -0
  68. package/src/components/lib/forms/PhoneCountryInput.astro +145 -0
  69. package/src/components/lib/forms/TagsAutocompleteInput.astro +249 -0
  70. package/src/components/lib/forms/index.ts +5 -0
  71. package/src/components/lib/layout/DiagonalSectionDivider.astro +69 -0
  72. package/src/components/lib/layout/HolyGrailGrid.astro +125 -0
  73. package/src/components/lib/layout/index.ts +3 -1
  74. package/src/components/lib/navigation/QuickJumpDock.astro +125 -0
  75. package/src/components/lib/navigation/StepBreadcrumbBar.astro +151 -0
  76. package/src/components/lib/navigation/SubNavbarPills.astro +102 -0
  77. package/src/components/lib/navigation/index.ts +3 -0
  78. package/src/components/lib/overlays/CookiePreferencesDrawer.astro +226 -0
  79. package/src/components/lib/overlays/FloatingFeedbackWidget.astro +203 -0
  80. package/src/components/lib/overlays/ImageCompareSlider.astro +172 -0
  81. package/src/components/lib/overlays/index.ts +3 -0
  82. package/src/components/lib/sections/BlogGridSection.astro +131 -0
  83. package/src/components/lib/sections/CareersOpenings.astro +139 -0
  84. package/src/components/lib/sections/ChangelogSection.astro +144 -0
  85. package/src/components/lib/sections/ContactSplitMap.astro +139 -0
  86. package/src/components/lib/sections/CtaNewsletterSection.astro +163 -0
  87. package/src/components/lib/sections/CtaSplitImage.astro +147 -0
  88. package/src/components/lib/sections/FaqTwoColumn.astro +119 -0
  89. package/src/components/lib/sections/FeatureAlternatingRows.astro +190 -0
  90. package/src/components/lib/sections/FeatureIconGrid.astro +155 -0
  91. package/src/components/lib/sections/FeatureTabsShowcase.astro +180 -0
  92. package/src/components/lib/sections/FooterMegaSection.astro +155 -0
  93. package/src/components/lib/sections/HeroDashboardPreview.astro +180 -0
  94. package/src/components/lib/sections/HeroMinimalCentered.astro +175 -0
  95. package/src/components/lib/sections/HeroShowcaseSplit.astro +304 -0
  96. package/src/components/lib/sections/HeroTerminal.astro +251 -0
  97. package/src/components/lib/sections/HeroWaitlist.astro +229 -0
  98. package/src/components/lib/sections/HowItWorksSteps.astro +178 -0
  99. package/src/components/lib/sections/IntegrationsGrid.astro +167 -0
  100. package/src/components/lib/sections/LogoCloudSection.astro +96 -0
  101. package/src/components/lib/sections/LogoWallGrid.astro +91 -0
  102. package/src/components/lib/sections/PricingSinglePlan.astro +157 -0
  103. package/src/components/lib/sections/PricingToggleCards.astro +171 -0
  104. package/src/components/lib/sections/StatsCounterBand.astro +122 -0
  105. package/src/components/lib/sections/StatsDashboardSection.astro +83 -0
  106. package/src/components/lib/sections/TestimonialCarousel.astro +165 -0
  107. package/src/components/lib/sections/TestimonialFeatured.astro +120 -0
  108. package/src/components/lib/sections/TestimonialGridSection.astro +188 -0
  109. package/src/components/lib/sections/index.ts +29 -0
  110. package/src/components/lib/typography/AnimatedCounterText.astro +102 -0
  111. package/src/components/lib/typography/CurvedPathHeading.astro +86 -0
  112. package/src/components/lib/typography/GradientTextMarquee.astro +105 -0
  113. package/src/components/lib/typography/HackerDecodeText.astro +85 -0
  114. package/src/components/lib/typography/index.ts +4 -0
  115. package/src/styles/global.css +18 -1
@@ -0,0 +1,373 @@
1
+ ---
2
+ /**
3
+ * ChartFrame — Shared accessible shell for every SVG chart in the kit.
4
+ * Renders the card surface, header, legend, hover tooltip and a
5
+ * "View data" table fallback so no chart relies on color or hover alone.
6
+ *
7
+ * @param {string} title - Chart heading (names the series when there is only one).
8
+ * @param {string} subtitle - Supporting description under the title.
9
+ * @param {string} value - Optional headline figure shown on the right of the header.
10
+ * @param {string} delta - Optional change label next to the headline figure.
11
+ * @param {boolean} deltaPositive - Good news (green ▲) or bad (red ▼); omit for a neutral caption.
12
+ * @param {Array<{label: string, color: string}>} legend - Legend items (shown when ≥ 2 series).
13
+ * @param {{headers: string[], rows: Array<Array<string|number>>}} table - Data for the table view.
14
+ * @param {string} variant - Surface style (glass|solid|ghost).
15
+ * @param {string} size - Density (sm|md|lg).
16
+ * @param {string} className - Extra CSS class.
17
+ *
18
+ * Accessibility: the figure is labelled by its title, every mark is focusable via
19
+ * the chart itself, and a native <details> data table is always available.
20
+ */
21
+ interface Props {
22
+ title?: string;
23
+ subtitle?: string;
24
+ value?: string;
25
+ delta?: string;
26
+ deltaPositive?: boolean;
27
+ legend?: Array<{ label: string; color: string }>;
28
+ table?: { headers: string[]; rows: Array<Array<string | number>> };
29
+ variant?: 'glass' | 'solid' | 'ghost';
30
+ size?: 'sm' | 'md' | 'lg';
31
+ className?: string;
32
+ }
33
+
34
+ const {
35
+ title,
36
+ subtitle,
37
+ value,
38
+ delta,
39
+ deltaPositive,
40
+ legend = [],
41
+ table,
42
+ variant = 'glass',
43
+ size = 'md',
44
+ className = '',
45
+ } = Astro.props;
46
+
47
+ const titleId = `cf-${Math.random().toString(36).slice(2, 9)}`;
48
+ ---
49
+
50
+ <figure
51
+ class={`cf cf--${variant} cf--${size} ${className}`}
52
+ data-chart
53
+ aria-labelledby={title ? titleId : undefined}
54
+ >
55
+ {(title || value) && (
56
+ <header class="cf__header">
57
+ <div class="cf__heading">
58
+ {title && <figcaption id={titleId} class="cf__title">{title}</figcaption>}
59
+ {subtitle && <p class="cf__subtitle">{subtitle}</p>}
60
+ </div>
61
+ {value && (
62
+ <div class="cf__headline">
63
+ <span class="cf__value">{value}</span>
64
+ {delta && (
65
+ <span class:list={['cf__delta', { 'cf__delta--pos': deltaPositive === true, 'cf__delta--neg': deltaPositive === false, 'cf__delta--neutral': deltaPositive === undefined }]}>
66
+ {deltaPositive !== undefined && (
67
+ <svg width="10" height="10" viewBox="0 0 10 10" aria-hidden="true">
68
+ <path d={deltaPositive ? 'M5 1 L9 7 H1 Z' : 'M5 9 L9 3 H1 Z'} fill="currentColor" />
69
+ </svg>
70
+ )}
71
+ {delta}
72
+ </span>
73
+ )}
74
+ </div>
75
+ )}
76
+ </header>
77
+ )}
78
+
79
+ {legend.length > 1 && (
80
+ <ul class="cf__legend" aria-label="Legend">
81
+ {legend.map((item) => (
82
+ <li class="cf__legend-item">
83
+ <span class="cf__swatch" style={`background:${item.color}`} aria-hidden="true"></span>
84
+ {item.label}
85
+ </li>
86
+ ))}
87
+ </ul>
88
+ )}
89
+
90
+ <div class="cf__plot">
91
+ <slot />
92
+ </div>
93
+
94
+ <slot name="footer" />
95
+
96
+ {table && table.rows.length > 0 && (
97
+ <details class="cf__data">
98
+ <summary>View data</summary>
99
+ <div class="cf__table-wrap">
100
+ <table>
101
+ <thead>
102
+ <tr>{table.headers.map((h) => <th scope="col">{h}</th>)}</tr>
103
+ </thead>
104
+ <tbody>
105
+ {table.rows.map((row) => (
106
+ <tr>{row.map((cell, i) => (i === 0 ? <th scope="row">{cell}</th> : <td>{cell}</td>))}</tr>
107
+ ))}
108
+ </tbody>
109
+ </table>
110
+ </div>
111
+ </details>
112
+ )}
113
+
114
+ <div class="cf__tip" data-chart-tip aria-hidden="true"></div>
115
+ </figure>
116
+
117
+ <script>
118
+ import './chart-tooltip';
119
+ </script>
120
+
121
+ <style>
122
+ .cf {
123
+ --cf-pad: var(--sp-6, 1.5rem);
124
+ --cf-title: 1.0625rem;
125
+ position: relative;
126
+ margin: 0;
127
+ width: 100%;
128
+ min-width: 0;
129
+ box-sizing: border-box;
130
+ padding: var(--cf-pad);
131
+ border-radius: var(--r-lg, 20px);
132
+ color: var(--c-text-2, #a3a3a3);
133
+ font-family: var(--font-sans, system-ui, sans-serif);
134
+ }
135
+ .cf--glass {
136
+ background: linear-gradient(160deg, rgba(255, 255, 255, 0.045), rgba(255, 255, 255, 0.015));
137
+ border: 1px solid var(--c-border, rgba(255, 255, 255, 0.07));
138
+ backdrop-filter: blur(14px);
139
+ -webkit-backdrop-filter: blur(14px);
140
+ }
141
+ .cf--solid {
142
+ background: var(--chart-surface, #0d0d0f);
143
+ border: 1px solid rgba(255, 255, 255, 0.08);
144
+ }
145
+ .cf--ghost {
146
+ background: transparent;
147
+ border: 1px solid transparent;
148
+ padding-inline: 0;
149
+ }
150
+ .cf--sm { --cf-pad: var(--sp-4, 1rem); --cf-title: 0.9375rem; }
151
+ .cf--lg { --cf-pad: var(--sp-8, 2rem); --cf-title: 1.25rem; }
152
+
153
+ .cf__header {
154
+ display: flex;
155
+ justify-content: space-between;
156
+ align-items: flex-start;
157
+ gap: var(--sp-4, 1rem);
158
+ margin-bottom: var(--sp-4, 1rem);
159
+ }
160
+ .cf__title {
161
+ margin: 0;
162
+ font-size: var(--cf-title);
163
+ font-weight: 650;
164
+ color: var(--c-text-1, #fff);
165
+ letter-spacing: -0.01em;
166
+ }
167
+ .cf__subtitle {
168
+ margin: 0.25rem 0 0;
169
+ font-size: 0.8125rem;
170
+ color: var(--c-text-2, #a3a3a3);
171
+ line-height: 1.45;
172
+ }
173
+ .cf__headline {
174
+ display: flex;
175
+ flex-direction: column;
176
+ align-items: flex-end;
177
+ gap: 0.2rem;
178
+ flex-shrink: 0;
179
+ }
180
+ .cf__value {
181
+ font-size: 1.5rem;
182
+ font-weight: 700;
183
+ color: var(--c-text-1, #fff);
184
+ font-variant-numeric: tabular-nums;
185
+ line-height: 1.1;
186
+ }
187
+ .cf__delta {
188
+ display: inline-flex;
189
+ align-items: center;
190
+ gap: 0.3rem;
191
+ font-size: 0.75rem;
192
+ font-weight: 600;
193
+ color: var(--c-text-1, #fff);
194
+ }
195
+ .cf__delta--pos svg { color: var(--chart-pos, #34c38f); }
196
+ .cf__delta--neg svg { color: var(--chart-neg, #f06a6a); }
197
+ .cf__delta--neutral { color: var(--c-text-3, #8a8a8a); font-weight: 500; }
198
+
199
+ .cf__legend {
200
+ display: flex;
201
+ flex-wrap: wrap;
202
+ gap: 0.4rem 1rem;
203
+ list-style: none;
204
+ margin: 0 0 var(--sp-3, 0.75rem);
205
+ padding: 0;
206
+ }
207
+ .cf__legend-item {
208
+ display: inline-flex;
209
+ align-items: center;
210
+ gap: 0.45rem;
211
+ font-size: 0.8125rem;
212
+ color: var(--c-text-2, #a3a3a3);
213
+ }
214
+ .cf__swatch {
215
+ width: 10px;
216
+ height: 10px;
217
+ border-radius: 3px;
218
+ }
219
+
220
+ .cf__plot {
221
+ position: relative;
222
+ width: 100%;
223
+ }
224
+ /* Plot SVGs scale fluidly; small inline icons keep their explicit width/height. */
225
+ .cf__plot :global(svg:not([width])) {
226
+ display: block;
227
+ width: 100%;
228
+ height: auto;
229
+ overflow: visible;
230
+ }
231
+
232
+ /* ── Shared mark vocabulary for every chart ───────────── */
233
+ .cf :global(.cf-grid) {
234
+ stroke: var(--chart-grid, rgba(255, 255, 255, 0.07));
235
+ stroke-width: 1;
236
+ shape-rendering: crispEdges;
237
+ }
238
+ .cf :global(.cf-axis) {
239
+ stroke: var(--chart-axis, rgba(255, 255, 255, 0.18));
240
+ stroke-width: 1;
241
+ shape-rendering: crispEdges;
242
+ }
243
+ .cf :global(.cf-label) {
244
+ fill: var(--c-text-3, #8a8a8a);
245
+ font-size: 11px;
246
+ font-family: var(--font-sans, system-ui, sans-serif);
247
+ font-variant-numeric: tabular-nums;
248
+ }
249
+ .cf :global(.cf-label--strong) {
250
+ fill: var(--c-text-1, #fff);
251
+ font-weight: 600;
252
+ }
253
+ .cf :global(.cf-mark) {
254
+ transition: opacity 0.2s ease, filter 0.2s ease;
255
+ outline: none;
256
+ }
257
+ .cf :global(.cf-mark:focus-visible) {
258
+ stroke: var(--c-text-1, #fff);
259
+ stroke-width: 2px;
260
+ }
261
+ .cf :global(svg:hover .cf-dim),
262
+ .cf :global(svg:focus-within .cf-dim) {
263
+ opacity: 0.45;
264
+ }
265
+ .cf :global(svg .cf-dim:hover),
266
+ .cf :global(svg .cf-dim:focus-visible) {
267
+ opacity: 1;
268
+ }
269
+ .cf :global(.cf-hit) {
270
+ fill: transparent;
271
+ cursor: crosshair;
272
+ outline: none;
273
+ }
274
+ .cf :global(.cf-cross) {
275
+ stroke: rgba(255, 255, 255, 0.35);
276
+ stroke-width: 1;
277
+ opacity: 0;
278
+ pointer-events: none;
279
+ transition: opacity 0.15s ease;
280
+ }
281
+ .cf :global(.cf-hit-group:hover .cf-cross),
282
+ .cf :global(.cf-hit-group:focus-within .cf-cross) {
283
+ opacity: 1;
284
+ }
285
+ .cf :global(.cf-hit:focus-visible) {
286
+ fill: rgba(255, 255, 255, 0.04);
287
+ }
288
+
289
+ /* ── Tooltip ─────────────────────────────────────────── */
290
+ .cf__tip {
291
+ position: absolute;
292
+ top: 0;
293
+ left: 0;
294
+ z-index: 5;
295
+ pointer-events: none;
296
+ opacity: 0;
297
+ display: flex;
298
+ flex-direction: column;
299
+ gap: 0.2rem;
300
+ min-width: 7rem;
301
+ max-width: 16rem;
302
+ padding: 0.55rem 0.7rem;
303
+ border-radius: var(--r-sm, 8px);
304
+ background: rgba(12, 12, 14, 0.94);
305
+ border: 1px solid rgba(255, 255, 255, 0.12);
306
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.45);
307
+ font-size: 0.75rem;
308
+ color: var(--c-text-1, #fff);
309
+ transition: opacity 0.15s ease;
310
+ }
311
+ .cf__tip :global(strong) {
312
+ font-size: 0.75rem;
313
+ font-weight: 650;
314
+ margin-bottom: 0.15rem;
315
+ }
316
+ .cf__tip :global(span) {
317
+ display: flex;
318
+ align-items: center;
319
+ gap: 0.4rem;
320
+ color: var(--c-text-2, #c4c4c4);
321
+ font-variant-numeric: tabular-nums;
322
+ }
323
+ .cf__tip :global(i) {
324
+ width: 8px;
325
+ height: 8px;
326
+ border-radius: 2px;
327
+ flex-shrink: 0;
328
+ }
329
+
330
+ /* ── Data table fallback ─────────────────────────────── */
331
+ .cf__data {
332
+ margin-top: var(--sp-3, 0.75rem);
333
+ font-size: 0.8125rem;
334
+ }
335
+ .cf__data summary {
336
+ display: inline-flex;
337
+ align-items: center;
338
+ min-height: 32px;
339
+ cursor: pointer;
340
+ color: var(--c-text-3, #8a8a8a);
341
+ border-radius: 6px;
342
+ list-style-position: inside;
343
+ }
344
+ .cf__data summary:hover { color: var(--c-text-1, #fff); }
345
+ .cf__data summary:focus-visible {
346
+ outline: 2px solid var(--c-primary-light, #d4a017);
347
+ outline-offset: 2px;
348
+ }
349
+ .cf__table-wrap {
350
+ overflow-x: auto;
351
+ margin-top: 0.5rem;
352
+ }
353
+ .cf__data table {
354
+ width: 100%;
355
+ border-collapse: collapse;
356
+ font-variant-numeric: tabular-nums;
357
+ }
358
+ .cf__data th,
359
+ .cf__data td {
360
+ padding: 0.4rem 0.6rem;
361
+ text-align: right;
362
+ border-bottom: 1px solid rgba(255, 255, 255, 0.06);
363
+ white-space: nowrap;
364
+ }
365
+ .cf__data th:first-child { text-align: left; }
366
+ .cf__data thead th { color: var(--c-text-3, #8a8a8a); font-weight: 600; }
367
+ .cf__data tbody th { color: var(--c-text-1, #fff); font-weight: 500; }
368
+ .cf__data td { color: var(--c-text-2, #a3a3a3); }
369
+
370
+ @media (prefers-reduced-motion: reduce) {
371
+ .cf :global(*) { transition: none !important; animation: none !important; }
372
+ }
373
+ </style>
@@ -0,0 +1,98 @@
1
+ ---
2
+ /**
3
+ * DivergingBarChart — Horizontal bars extending left (negative) or right
4
+ * (positive) from a zero line. Great for sentiment, variance and P&L deltas.
5
+ *
6
+ * @param {Array<{label: string, value: number}>} items - Signed values.
7
+ * @param {string} positiveLabel - Legend label for positive values.
8
+ * @param {string} negativeLabel - Legend label for negative values.
9
+ * @param {string} prefix - Value prefix.
10
+ * @param {string} suffix - Value suffix.
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 { scaleLinear, formatValue, niceTicks } from './chart-utils';
16
+
17
+ interface Props {
18
+ title?: string;
19
+ subtitle?: string;
20
+ items?: Array<{ label: string; value: number }>;
21
+ positiveLabel?: string;
22
+ negativeLabel?: string;
23
+ prefix?: string;
24
+ suffix?: string;
25
+ positiveColor?: string;
26
+ negativeColor?: string;
27
+ variant?: 'glass' | 'solid' | 'ghost';
28
+ size?: 'sm' | 'md' | 'lg';
29
+ className?: string;
30
+ }
31
+
32
+ const {
33
+ title = 'Budget variance by team',
34
+ subtitle = 'Actual minus plan, Q3',
35
+ items = [
36
+ { label: 'Engineering', value: -42 },
37
+ { label: 'Marketing', value: 28 },
38
+ { label: 'Sales', value: 64 },
39
+ { label: 'Support', value: -12 },
40
+ { label: 'Design', value: 9 },
41
+ { label: 'Ops', value: -31 },
42
+ ],
43
+ positiveLabel = 'Under budget',
44
+ negativeLabel = 'Over budget',
45
+ prefix = '$',
46
+ suffix = 'k',
47
+ positiveColor = 'var(--chart-1, #3987e5)',
48
+ negativeColor = 'var(--chart-2, #d95926)',
49
+ variant = 'glass',
50
+ size = 'md',
51
+ className = '',
52
+ } = Astro.props;
53
+
54
+ const W = 640;
55
+ const rowH = 34;
56
+ const m = { t: 8, r: 56, b: 26, l: 110 };
57
+ const H = items.length * rowH + m.t + m.b;
58
+ const ext = Math.max(...items.map((i) => Math.abs(i.value)));
59
+ const ticks = niceTicks(-ext, ext, 4);
60
+ const x = scaleLinear(ticks[0], ticks[ticks.length - 1], m.l, W - m.r);
61
+ const fmt = (v: number) => `${v > 0 ? '+' : v < 0 ? '−' : ''}${formatValue(Math.abs(v), prefix, suffix)}`;
62
+ ---
63
+
64
+ <ChartFrame
65
+ title={title}
66
+ subtitle={subtitle}
67
+ variant={variant}
68
+ size={size}
69
+ className={className}
70
+ legend={[{ label: positiveLabel, color: positiveColor }, { label: negativeLabel, color: negativeColor }]}
71
+ table={{ headers: ['Item', 'Value'], rows: items.map((i) => [i.label, fmt(i.value)]) }}
72
+ >
73
+ <svg viewBox={`0 0 ${W} ${H}`} role="img" aria-label={`${title}: diverging bar chart`}>
74
+ {ticks.map((t) => (
75
+ <g>
76
+ <line class={t === 0 ? 'cf-axis' : 'cf-grid'} x1={x(t)} x2={x(t)} y1={m.t} y2={H - m.b} />
77
+ <text class="cf-label" x={x(t)} y={H - 8} text-anchor="middle">{t === 0 ? '0' : fmt(t)}</text>
78
+ </g>
79
+ ))}
80
+ {items.map((it, i) => {
81
+ const cy = m.t + i * rowH + rowH / 2;
82
+ const pos = it.value >= 0;
83
+ const x0 = x(0);
84
+ const x1 = x(it.value);
85
+ const c = pos ? positiveColor : negativeColor;
86
+ const w = Math.abs(x1 - x0);
87
+ return (
88
+ <g>
89
+ <text class="cf-label" x={m.l - 10} y={cy + 4} text-anchor="end">{it.label}</text>
90
+ <rect class="cf-mark cf-dim" x={Math.min(x0, x1)} y={cy - 9} width={Math.max(1, w)} height="18" rx="4" fill={c}
91
+ tabindex="0" role="img" aria-label={`${it.label}: ${fmt(it.value)}`} data-tip-title={it.label}
92
+ data-tip={`${c}::${pos ? positiveLabel : negativeLabel}: ${fmt(it.value)}`} />
93
+ <text class="cf-label cf-label--strong" x={pos ? x1 + 6 : x1 - 6} y={cy + 4} text-anchor={pos ? 'start' : 'end'}>{fmt(it.value)}</text>
94
+ </g>
95
+ );
96
+ })}
97
+ </svg>
98
+ </ChartFrame>
@@ -0,0 +1,92 @@
1
+ ---
2
+ /**
3
+ * DumbbellChart — Two values per row connected by a bar, showing the gap between
4
+ * a "before" and "after" (or two groups) for each item.
5
+ *
6
+ * @param {Array<{label: string, a: number, b: number}>} items - Pairs to compare.
7
+ * @param {string} aLabel - Name of the first value.
8
+ * @param {string} bLabel - Name of the second value.
9
+ * @param {string} prefix - Value prefix.
10
+ * @param {string} suffix - Value suffix.
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 { scaleLinear, niceTicks, formatValue, colorAt } from './chart-utils';
16
+
17
+ interface Props {
18
+ title?: string;
19
+ subtitle?: string;
20
+ items?: Array<{ label: string; a: number; b: number }>;
21
+ aLabel?: string;
22
+ bLabel?: string;
23
+ prefix?: string;
24
+ suffix?: string;
25
+ colors?: string[];
26
+ variant?: 'glass' | 'solid' | 'ghost';
27
+ size?: 'sm' | 'md' | 'lg';
28
+ className?: string;
29
+ }
30
+
31
+ const {
32
+ title = 'Time to first value',
33
+ subtitle = 'Minutes, before vs after onboarding redesign',
34
+ items = [
35
+ { label: 'Solo devs', a: 34, b: 12 },
36
+ { label: 'Startups', a: 41, b: 18 },
37
+ { label: 'Agencies', a: 52, b: 26 },
38
+ { label: 'Mid-market', a: 63, b: 37 },
39
+ { label: 'Enterprise', a: 88, b: 54 },
40
+ ],
41
+ aLabel = 'Before',
42
+ bLabel = 'After',
43
+ prefix = '',
44
+ suffix = 'm',
45
+ colors,
46
+ variant = 'glass',
47
+ size = 'md',
48
+ className = '',
49
+ } = Astro.props;
50
+
51
+ const W = 640;
52
+ const rowH = 40;
53
+ const m = { t: 8, r: 24, b: 26, l: 110 };
54
+ const H = items.length * rowH + m.t + m.b;
55
+ const ticks = niceTicks(0, Math.max(...items.flatMap((i) => [i.a, i.b])), 5);
56
+ const x = scaleLinear(0, ticks[ticks.length - 1], m.l, W - m.r);
57
+ const ca = colorAt(0, colors);
58
+ const cb = colorAt(1, colors);
59
+ const fmt = (v: number) => formatValue(v, prefix, suffix);
60
+ ---
61
+
62
+ <ChartFrame
63
+ title={title}
64
+ subtitle={subtitle}
65
+ variant={variant}
66
+ size={size}
67
+ className={className}
68
+ legend={[{ label: aLabel, color: ca }, { label: bLabel, color: cb }]}
69
+ table={{ headers: ['Item', aLabel, bLabel, 'Difference'], rows: items.map((i) => [i.label, fmt(i.a), fmt(i.b), fmt(i.b - i.a)]) }}
70
+ >
71
+ <svg viewBox={`0 0 ${W} ${H}`} role="img" aria-label={`${title}: dumbbell chart comparing ${aLabel} and ${bLabel}`}>
72
+ {ticks.map((t) => (
73
+ <g>
74
+ <line class="cf-grid" x1={x(t)} x2={x(t)} y1={m.t} y2={H - m.b} />
75
+ <text class="cf-label" x={x(t)} y={H - 8} text-anchor="middle">{fmt(t)}</text>
76
+ </g>
77
+ ))}
78
+ {items.map((it, i) => {
79
+ const cy = m.t + i * rowH + rowH / 2;
80
+ return (
81
+ <g class="cf-mark cf-dim" tabindex="0" role="img" aria-label={`${it.label}: ${aLabel} ${fmt(it.a)}, ${bLabel} ${fmt(it.b)}`}
82
+ data-tip-title={it.label} data-tip={`${ca}::${aLabel}: ${fmt(it.a)}|${cb}::${bLabel}: ${fmt(it.b)}|Δ ${fmt(it.b - it.a)}`}>
83
+ <rect x={m.l} y={cy - rowH / 2} width={W - m.l - m.r} height={rowH} fill="transparent" />
84
+ <text class="cf-label" x={m.l - 10} y={cy + 4} text-anchor="end">{it.label}</text>
85
+ <line x1={x(it.a)} x2={x(it.b)} y1={cy} y2={cy} stroke="rgba(255,255,255,0.22)" stroke-width="4" stroke-linecap="round" />
86
+ <circle cx={x(it.a)} cy={cy} r="7" fill={ca} stroke="var(--chart-surface, #0d0d0f)" stroke-width="2" />
87
+ <circle cx={x(it.b)} cy={cy} r="7" fill={cb} stroke="var(--chart-surface, #0d0d0f)" stroke-width="2" />
88
+ </g>
89
+ );
90
+ })}
91
+ </svg>
92
+ </ChartFrame>
@@ -0,0 +1,125 @@
1
+ ---
2
+ /**
3
+ * ForecastLineChart — Actuals as a solid line, projection as a dashed line with
4
+ * a widening confidence cone and a "today" divider.
5
+ *
6
+ * @param {string[]} categories - X-axis labels covering actual + forecast periods.
7
+ * @param {number[]} actual - Observed values (first N periods).
8
+ * @param {number[]} forecast - Projected values for the remaining periods.
9
+ * @param {number} confidence - Relative spread of the cone (0.1 = ±10% at the end).
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, niceTicks, linePath, bandPath, formatValue, type Point } from './chart-utils';
17
+
18
+ interface Props {
19
+ title?: string;
20
+ subtitle?: string;
21
+ categories?: string[];
22
+ actual?: number[];
23
+ forecast?: number[];
24
+ confidence?: number;
25
+ prefix?: string;
26
+ suffix?: string;
27
+ height?: number;
28
+ color?: string;
29
+ variant?: 'glass' | 'solid' | 'ghost';
30
+ size?: 'sm' | 'md' | 'lg';
31
+ className?: string;
32
+ }
33
+
34
+ const {
35
+ title = 'MRR forecast',
36
+ subtitle = 'Actuals vs. projected, with 80% confidence',
37
+ categories = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
38
+ actual = [48, 51, 55, 54, 59, 63, 68],
39
+ forecast = [72, 76, 80, 85, 90],
40
+ confidence = 0.14,
41
+ prefix = '$',
42
+ suffix = 'k',
43
+ height = 280,
44
+ color = 'var(--chart-1, #3987e5)',
45
+ variant = 'glass',
46
+ size = 'md',
47
+ className = '',
48
+ } = Astro.props;
49
+
50
+ const W = 640;
51
+ const H = height;
52
+ const m = { t: 16, r: 16, b: 28, l: 48 };
53
+ const n = categories.length;
54
+ const a = actual.length;
55
+ const all = [...actual, ...forecast];
56
+ const hi = forecast.map((v, i) => v * (1 + confidence * ((i + 1) / forecast.length)));
57
+ const lo = forecast.map((v, i) => v * (1 - confidence * ((i + 1) / forecast.length)));
58
+ const ticks = niceTicks(Math.min(...all, ...lo) * 0.9, Math.max(...hi), 4);
59
+ const y = scaleLinear(ticks[0], ticks[ticks.length - 1], H - m.b, m.t);
60
+ const x = (i: number) => m.l + (i / Math.max(1, n - 1)) * (W - m.l - m.r);
61
+ const step = (W - m.l - m.r) / Math.max(1, n - 1);
62
+
63
+ const actualPts: Point[] = actual.map((v, i) => [x(i), y(v)]);
64
+ const anchor = actualPts[a - 1];
65
+ const fcPts: Point[] = [anchor, ...forecast.map((v, i): Point => [x(a + i), y(v)])];
66
+ const conePts = {
67
+ up: [anchor, ...hi.map((v, i): Point => [x(a + i), y(v)])],
68
+ down: [anchor, ...lo.map((v, i): Point => [x(a + i), y(v)])],
69
+ };
70
+ const growth = ((forecast[forecast.length - 1] - actual[a - 1]) / actual[a - 1]) * 100;
71
+ const fmt = (v: number) => formatValue(v, prefix, suffix);
72
+ ---
73
+
74
+ <ChartFrame
75
+ title={title}
76
+ subtitle={subtitle}
77
+ value={fmt(forecast[forecast.length - 1])}
78
+ delta={`${growth >= 0 ? '+' : ''}${growth.toFixed(0)}% projected`}
79
+ deltaPositive={growth >= 0}
80
+ variant={variant}
81
+ size={size}
82
+ className={className}
83
+ legend={[{ label: 'Actual', color }, { label: 'Forecast', color: `color-mix(in srgb, ${color} 55%, transparent)` }]}
84
+ table={{
85
+ headers: ['Period', 'Actual', 'Forecast', 'Low', 'High'],
86
+ rows: categories.map((c, i) => a > i
87
+ ? [c, fmt(actual[i]), '—', '—', '—']
88
+ : [c, '—', fmt(forecast[i - a]), fmt(lo[i - a]), fmt(hi[i - a])]),
89
+ }}
90
+ >
91
+ <svg viewBox={`0 0 ${W} ${H}`} role="img" aria-label={`${title}: actuals and forecast line chart`}>
92
+ {ticks.map((t) => (
93
+ <g>
94
+ <line class="cf-grid" x1={m.l} x2={W - m.r} y1={y(t)} y2={y(t)} />
95
+ <text class="cf-label" x={m.l - 8} y={y(t) + 4} text-anchor="end">{fmt(t)}</text>
96
+ </g>
97
+ ))}
98
+ {categories.map((c, i) => <text class="cf-label" x={x(i)} y={H - 8} text-anchor="middle">{c}</text>)}
99
+
100
+ <rect x={anchor[0]} y={m.t} width={W - m.r - anchor[0]} height={H - m.t - m.b} fill="rgba(255,255,255,0.025)" aria-hidden="true" />
101
+ <line class="cf-axis" x1={anchor[0]} x2={anchor[0]} y1={m.t} y2={H - m.b} />
102
+ <text class="cf-label" x={anchor[0] + 6} y={m.t + 10}>Today</text>
103
+
104
+ <path d={bandPath(conePts.up, conePts.down, 'linear')} fill={color} fill-opacity="0.14" aria-hidden="true" />
105
+ <path d={linePath(fcPts)} fill="none" stroke={color} stroke-width="2" stroke-dasharray="5 5" stroke-opacity="0.85" aria-hidden="true" />
106
+ <path d={linePath(actualPts)} fill="none" stroke={color} stroke-width="2" stroke-linejoin="round" aria-hidden="true" />
107
+ <circle cx={anchor[0]} cy={anchor[1]} r="4.5" fill={color} stroke="var(--chart-surface, #0d0d0f)" stroke-width="2" aria-hidden="true" />
108
+
109
+ {categories.map((c, i) => {
110
+ const isF = i >= a;
111
+ const v = isF ? forecast[i - a] : actual[i];
112
+ const tip = isF
113
+ ? `${color}::Forecast: ${fmt(v)}|Range: ${fmt(lo[i - a])} – ${fmt(hi[i - a])}`
114
+ : `${color}::Actual: ${fmt(v)}`;
115
+ return (
116
+ <g class="cf-hit-group">
117
+ <line class="cf-cross" x1={x(i)} x2={x(i)} y1={m.t} y2={H - m.b} />
118
+ <circle class="cf-cross" cx={x(i)} cy={y(v)} r="4.5" fill={color} stroke="#fff" stroke-width="1.5" />
119
+ <rect class="cf-hit" x={x(i) - step / 2} y={m.t} width={step} height={H - m.t - m.b} tabindex="0" role="img"
120
+ aria-label={`${c}: ${isF ? 'forecast' : 'actual'} ${fmt(v)}`} data-tip-title={c} data-tip={tip} />
121
+ </g>
122
+ );
123
+ })}
124
+ </svg>
125
+ </ChartFrame>