@a4ui/core 0.31.1 → 0.33.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.
- package/dist/NumberInput-Bnh2u7pp.js +381 -0
- package/dist/charts/BarList.d.ts +33 -0
- package/dist/charts/CategoryBar.d.ts +23 -0
- package/dist/charts/GaugeChart.d.ts +43 -0
- package/dist/charts/LineChart.d.ts +39 -0
- package/dist/charts/RadarChart.d.ts +34 -0
- package/dist/charts/StatusTracker.d.ts +27 -0
- package/dist/charts/index.d.ts +6 -0
- package/dist/charts.js +693 -100
- package/dist/commerce.js +2 -2
- package/dist/elements.css +206 -0
- package/dist/full.css +206 -0
- package/dist/index.d.ts +18 -1
- package/dist/index.js +5086 -3915
- package/dist/motion-jYMWmZqB.js +119 -0
- package/dist/ui/AnimatedBeam.d.ts +34 -0
- package/dist/ui/BentoGrid.d.ts +39 -0
- package/dist/ui/BorderBeam.d.ts +27 -0
- package/dist/ui/Callout.d.ts +27 -0
- package/dist/ui/Confetti.d.ts +46 -0
- package/dist/ui/CursorTrail.d.ts +25 -0
- package/dist/ui/DiffViewer.d.ts +33 -0
- package/dist/ui/Dock.d.ts +31 -0
- package/dist/ui/Globe.d.ts +44 -0
- package/dist/ui/Meteors.d.ts +23 -0
- package/dist/ui/ModelPicker.d.ts +38 -0
- package/dist/ui/ReasoningTrace.d.ts +28 -0
- package/dist/ui/Snippet.d.ts +19 -0
- package/dist/ui/SuggestionChips.d.ts +25 -0
- package/dist/ui/ToolCallTimeline.d.ts +34 -0
- package/dist/ui/UsageMeter.d.ts +26 -0
- package/dist/ui/WorldMap.d.ts +36 -0
- package/package.json +1 -1
- package/src/charts/BarList.tsx +83 -0
- package/src/charts/CategoryBar.tsx +95 -0
- package/src/charts/GaugeChart.tsx +175 -0
- package/src/charts/LineChart.tsx +264 -0
- package/src/charts/RadarChart.tsx +215 -0
- package/src/charts/StatusTracker.tsx +81 -0
- package/src/charts/index.ts +6 -0
- package/src/index.ts +31 -1
- package/src/ui/AnimatedBeam.tsx +187 -0
- package/src/ui/BentoGrid.tsx +89 -0
- package/src/ui/BorderBeam.tsx +96 -0
- package/src/ui/Callout.tsx +66 -0
- package/src/ui/Confetti.tsx +131 -0
- package/src/ui/CursorTrail.tsx +88 -0
- package/src/ui/DiffViewer.tsx +166 -0
- package/src/ui/Dock.tsx +124 -0
- package/src/ui/Globe.tsx +317 -0
- package/src/ui/Meteors.tsx +109 -0
- package/src/ui/ModelPicker.tsx +119 -0
- package/src/ui/ReasoningTrace.tsx +83 -0
- package/src/ui/Snippet.tsx +64 -0
- package/src/ui/SuggestionChips.tsx +65 -0
- package/src/ui/ToolCallTimeline.tsx +137 -0
- package/src/ui/UsageMeter.tsx +71 -0
- package/src/ui/WorldMap.tsx +191 -0
- package/dist/NumberInput-BQhVucw-.js +0 -491
- package/dist/cn-B6yFEsav.js +0 -8
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
// RadarChart — a native SVG spider/radar chart (no charting library). Axes
|
|
2
|
+
// are laid out clockwise starting at 12 o'clock, one spoke per dimension.
|
|
3
|
+
// A faint concentric polygon grid marks reference levels, and each series is
|
|
4
|
+
// drawn as a translucent-filled polygon connecting its normalized values.
|
|
5
|
+
// Colors come from theme tokens, either per-series `tone` or, when omitted,
|
|
6
|
+
// cycled across `--primary` / `--accent` by index.
|
|
7
|
+
import { For, Show, type JSX } from 'solid-js'
|
|
8
|
+
|
|
9
|
+
import { cn } from '../lib/cn'
|
|
10
|
+
|
|
11
|
+
export interface RadarSeries {
|
|
12
|
+
name?: string
|
|
13
|
+
tone?: 'primary' | 'accent'
|
|
14
|
+
values: number[]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface RadarChartProps {
|
|
18
|
+
axes: string[]
|
|
19
|
+
series: RadarSeries[]
|
|
20
|
+
/** Value that reaches the outer edge of the grid. Default: max across all series. */
|
|
21
|
+
max?: number
|
|
22
|
+
/** Overall SVG width/height in px (square). Default 240. */
|
|
23
|
+
size?: number
|
|
24
|
+
class?: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Explicit tone -> CSS custom property name.
|
|
28
|
+
const TONE_TOKENS: Record<'primary' | 'accent', string> = {
|
|
29
|
+
primary: '--primary',
|
|
30
|
+
accent: '--accent',
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Fallback rotation used when a series has no explicit `tone`.
|
|
34
|
+
const CYCLE_TOKENS = ['--primary', '--accent'] as const
|
|
35
|
+
|
|
36
|
+
/** Resolve the CSS custom property a series should stroke/fill with. */
|
|
37
|
+
function tokenFor(series: RadarSeries, index: number): string {
|
|
38
|
+
if (series.tone) return TONE_TOKENS[series.tone]
|
|
39
|
+
return CYCLE_TOKENS[index % CYCLE_TOKENS.length]
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const RING_LEVELS = 4
|
|
43
|
+
const LABEL_OFFSET = 14
|
|
44
|
+
// Extra viewBox margin (all sides) so axis-label text isn't clipped.
|
|
45
|
+
const LABEL_PAD = 34
|
|
46
|
+
|
|
47
|
+
/** Angle (in degrees) of axis `index` of `count`, starting at 12 o'clock and running clockwise. */
|
|
48
|
+
function axisAngle(index: number, count: number): number {
|
|
49
|
+
return -90 + (360 / count) * index
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Convert a polar coordinate (center + radius + angle in degrees) to cartesian `{ x, y }`. */
|
|
53
|
+
function polarToPoint(cx: number, cy: number, r: number, angleDeg: number): { x: number; y: number } {
|
|
54
|
+
const rad = (angleDeg * Math.PI) / 180
|
|
55
|
+
return { x: cx + r * Math.cos(rad), y: cy + r * Math.sin(rad) }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Build the `x,y ...` points attribute for a polygon at `radius` across all axes. */
|
|
59
|
+
function ringPoints(cx: number, cy: number, radius: number, axisCount: number): string {
|
|
60
|
+
return Array.from({ length: axisCount }, (_, i) => {
|
|
61
|
+
const p = polarToPoint(cx, cy, radius, axisAngle(i, axisCount))
|
|
62
|
+
return `${p.x},${p.y}`
|
|
63
|
+
}).join(' ')
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Text anchor for a label sitting outside the vertex at `angleDeg`. */
|
|
67
|
+
function labelAnchor(angleDeg: number): 'start' | 'middle' | 'end' {
|
|
68
|
+
const cos = Math.cos((angleDeg * Math.PI) / 180)
|
|
69
|
+
if (cos > 0.1) return 'start'
|
|
70
|
+
if (cos < -0.1) return 'end'
|
|
71
|
+
return 'middle'
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Dominant baseline for a label sitting outside the vertex at `angleDeg`. */
|
|
75
|
+
function labelBaseline(angleDeg: number): 'auto' | 'hanging' | 'middle' {
|
|
76
|
+
const sin = Math.sin((angleDeg * Math.PI) / 180)
|
|
77
|
+
if (sin < -0.3) return 'auto'
|
|
78
|
+
if (sin > 0.3) return 'hanging'
|
|
79
|
+
return 'middle'
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* SVG spider/radar chart: a concentric polygon grid with one spoke per axis,
|
|
84
|
+
* and one translucent-filled polygon per series connecting its values
|
|
85
|
+
* (normalized by `max`, default the max value across all series). Series
|
|
86
|
+
* colors come from each datum's `tone`, or cycle through `primary`/`accent`
|
|
87
|
+
* by index when omitted. Shows a small legend when 2+ series have a `name`.
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* ```tsx
|
|
91
|
+
* <RadarChart
|
|
92
|
+
* axes={['Speed', 'Power', 'Range', 'Defense', 'Utility']}
|
|
93
|
+
* series={[
|
|
94
|
+
* { name: 'Model A', tone: 'primary', values: [80, 60, 90, 40, 70] },
|
|
95
|
+
* { name: 'Model B', tone: 'accent', values: [50, 85, 60, 75, 55] },
|
|
96
|
+
* ]}
|
|
97
|
+
* />
|
|
98
|
+
* ```
|
|
99
|
+
*/
|
|
100
|
+
export function RadarChart(props: RadarChartProps): JSX.Element {
|
|
101
|
+
const size = () => props.size ?? 240
|
|
102
|
+
const center = () => size() / 2
|
|
103
|
+
const outerRadius = () => Math.max(size() / 2 - LABEL_OFFSET * 2, 0)
|
|
104
|
+
const axisCount = () => props.axes.length
|
|
105
|
+
const max = () => props.max ?? Math.max(1, ...props.series.flatMap((s) => s.values))
|
|
106
|
+
|
|
107
|
+
const rings = () =>
|
|
108
|
+
Array.from({ length: RING_LEVELS }, (_, i) => {
|
|
109
|
+
const level = i + 1
|
|
110
|
+
return ringPoints(center(), center(), outerRadius() * (level / RING_LEVELS), axisCount())
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
const spokes = () =>
|
|
114
|
+
props.axes.map((_, i) => polarToPoint(center(), center(), outerRadius(), axisAngle(i, axisCount())))
|
|
115
|
+
|
|
116
|
+
const labels = () =>
|
|
117
|
+
props.axes.map((label, i) => {
|
|
118
|
+
const angle = axisAngle(i, axisCount())
|
|
119
|
+
const point = polarToPoint(center(), center(), outerRadius() + LABEL_OFFSET, angle)
|
|
120
|
+
return { label, x: point.x, y: point.y, anchor: labelAnchor(angle), baseline: labelBaseline(angle) }
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
const seriesPolygons = () => {
|
|
124
|
+
const grandMax = max()
|
|
125
|
+
return props.series.map((series, index) => {
|
|
126
|
+
const points = series.values
|
|
127
|
+
.map((value, i) => {
|
|
128
|
+
const fraction = grandMax > 0 ? Math.max(value, 0) / grandMax : 0
|
|
129
|
+
const p = polarToPoint(center(), center(), outerRadius() * fraction, axisAngle(i, axisCount()))
|
|
130
|
+
return `${p.x},${p.y}`
|
|
131
|
+
})
|
|
132
|
+
.join(' ')
|
|
133
|
+
return { series, index, points, token: tokenFor(series, index) }
|
|
134
|
+
})
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const namedSeries = () => props.series.filter((s) => s.name)
|
|
138
|
+
const showLegend = () => namedSeries().length >= 2
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
<div class={cn('inline-flex flex-col items-center gap-2', props.class)}>
|
|
142
|
+
<svg
|
|
143
|
+
width={size()}
|
|
144
|
+
height={size()}
|
|
145
|
+
// Pad the coordinate space so axis-label text near the left/right
|
|
146
|
+
// vertices isn't clipped by the viewBox edge (the plot scales down a
|
|
147
|
+
// touch to fit; the CSS footprint stays `size`).
|
|
148
|
+
viewBox={`${-LABEL_PAD} ${-LABEL_PAD} ${size() + LABEL_PAD * 2} ${size() + LABEL_PAD * 2}`}
|
|
149
|
+
role="img"
|
|
150
|
+
aria-label={`Radar chart comparing ${props.series.length} series across ${axisCount()} axes: ${props.axes.join(', ')}`}
|
|
151
|
+
>
|
|
152
|
+
<For each={rings()}>
|
|
153
|
+
{(points) => <polygon points={points} fill="none" stroke="hsl(var(--border))" stroke-width={1} />}
|
|
154
|
+
</For>
|
|
155
|
+
<For each={spokes()}>
|
|
156
|
+
{(point) => (
|
|
157
|
+
<line
|
|
158
|
+
x1={center()}
|
|
159
|
+
y1={center()}
|
|
160
|
+
x2={point.x}
|
|
161
|
+
y2={point.y}
|
|
162
|
+
stroke="hsl(var(--border))"
|
|
163
|
+
stroke-width={1}
|
|
164
|
+
/>
|
|
165
|
+
)}
|
|
166
|
+
</For>
|
|
167
|
+
<For each={seriesPolygons()}>
|
|
168
|
+
{({ series, points, token }) => (
|
|
169
|
+
<polygon
|
|
170
|
+
points={points}
|
|
171
|
+
fill={`hsl(var(${token}) / 0.15)`}
|
|
172
|
+
stroke={`hsl(var(${token}))`}
|
|
173
|
+
stroke-width={2}
|
|
174
|
+
stroke-linejoin="round"
|
|
175
|
+
>
|
|
176
|
+
<Show when={series.name}>
|
|
177
|
+
<title>{series.name}</title>
|
|
178
|
+
</Show>
|
|
179
|
+
</polygon>
|
|
180
|
+
)}
|
|
181
|
+
</For>
|
|
182
|
+
<For each={labels()}>
|
|
183
|
+
{(item) => (
|
|
184
|
+
<text
|
|
185
|
+
x={item.x}
|
|
186
|
+
y={item.y}
|
|
187
|
+
text-anchor={item.anchor}
|
|
188
|
+
dominant-baseline={item.baseline}
|
|
189
|
+
class="text-xs fill-muted-foreground"
|
|
190
|
+
>
|
|
191
|
+
{item.label}
|
|
192
|
+
</text>
|
|
193
|
+
)}
|
|
194
|
+
</For>
|
|
195
|
+
</svg>
|
|
196
|
+
<Show when={showLegend()}>
|
|
197
|
+
<div class="flex flex-wrap items-center justify-center gap-x-3 gap-y-1">
|
|
198
|
+
<For each={seriesPolygons()}>
|
|
199
|
+
{({ series, token }) => (
|
|
200
|
+
<Show when={series.name}>
|
|
201
|
+
<span class="inline-flex items-center gap-1.5 text-xs text-muted-foreground">
|
|
202
|
+
<span
|
|
203
|
+
class="inline-block size-2 rounded-full"
|
|
204
|
+
style={{ 'background-color': `hsl(var(${token}))` }}
|
|
205
|
+
/>
|
|
206
|
+
{series.name}
|
|
207
|
+
</span>
|
|
208
|
+
</Show>
|
|
209
|
+
)}
|
|
210
|
+
</For>
|
|
211
|
+
</div>
|
|
212
|
+
</Show>
|
|
213
|
+
</div>
|
|
214
|
+
)
|
|
215
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// StatusTracker — a status-page style history bar built from plain flex
|
|
2
|
+
// `div`s (no SVG, no charting library). Each segment is a thin rounded bar
|
|
3
|
+
// colored by its status, with a native tooltip carrying the label. `ok`/
|
|
4
|
+
// `degraded` reuse the same success/warning tones as `Alert`/`Badge`
|
|
5
|
+
// (`emerald-500`/`amber-500` — the repo has no dedicated CSS token for
|
|
6
|
+
// them); `down` uses the `--destructive` semantic token.
|
|
7
|
+
import { For, Show, type JSX } from 'solid-js'
|
|
8
|
+
|
|
9
|
+
import { cn } from '../lib/cn'
|
|
10
|
+
|
|
11
|
+
export interface StatusSegment {
|
|
12
|
+
status: 'ok' | 'degraded' | 'down'
|
|
13
|
+
label?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface StatusTrackerProps {
|
|
17
|
+
segments: StatusSegment[]
|
|
18
|
+
class?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const STATUS_CLASSES: Record<StatusSegment['status'], string> = {
|
|
22
|
+
ok: 'bg-emerald-500',
|
|
23
|
+
degraded: 'bg-amber-500',
|
|
24
|
+
down: 'bg-destructive',
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const STATUS_LABEL: Record<StatusSegment['status'], string> = {
|
|
28
|
+
ok: 'Operational',
|
|
29
|
+
degraded: 'Degraded',
|
|
30
|
+
down: 'Down',
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Share of `segments` that are `'ok'`, formatted as e.g. `"98.7% uptime"`; `undefined` when empty. */
|
|
34
|
+
function uptimeSummary(segments: StatusSegment[]): string | undefined {
|
|
35
|
+
if (segments.length === 0) return undefined
|
|
36
|
+
const ok = segments.filter((s) => s.status === 'ok').length
|
|
37
|
+
return `${((ok / segments.length) * 100).toFixed(1)}% uptime`
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Status-page style history bar: a row of thin rounded segments colored by
|
|
42
|
+
* `status` (ok/degraded/down), each with a native hover tooltip showing its
|
|
43
|
+
* label and status, plus a summary uptime line above.
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```tsx
|
|
47
|
+
* <StatusTracker
|
|
48
|
+
* segments={[
|
|
49
|
+
* { status: 'ok', label: 'Jul 1' },
|
|
50
|
+
* { status: 'degraded', label: 'Jul 2' },
|
|
51
|
+
* { status: 'ok', label: 'Jul 3' },
|
|
52
|
+
* { status: 'down', label: 'Jul 4' },
|
|
53
|
+
* ]}
|
|
54
|
+
* />
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
export function StatusTracker(props: StatusTrackerProps): JSX.Element {
|
|
58
|
+
const summary = () => uptimeSummary(props.segments)
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<div class={cn('flex w-full flex-col gap-1.5', props.class)}>
|
|
62
|
+
<Show when={summary()}>
|
|
63
|
+
<span class="text-xs text-muted-foreground">{summary()}</span>
|
|
64
|
+
</Show>
|
|
65
|
+
<div class="flex w-full items-stretch gap-1">
|
|
66
|
+
<For each={props.segments}>
|
|
67
|
+
{(segment) => (
|
|
68
|
+
<div
|
|
69
|
+
title={
|
|
70
|
+
segment.label
|
|
71
|
+
? `${segment.label}: ${STATUS_LABEL[segment.status]}`
|
|
72
|
+
: STATUS_LABEL[segment.status]
|
|
73
|
+
}
|
|
74
|
+
class={cn('h-6 flex-1 rounded-sm transition-colors', STATUS_CLASSES[segment.status])}
|
|
75
|
+
/>
|
|
76
|
+
)}
|
|
77
|
+
</For>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
)
|
|
81
|
+
}
|
package/src/charts/index.ts
CHANGED
|
@@ -3,3 +3,9 @@
|
|
|
3
3
|
export { Sparkline, type SparklineProps } from './Sparkline'
|
|
4
4
|
export { BarChart, type BarDatum, type BarChartProps } from './BarChart'
|
|
5
5
|
export { DonutChart, type DonutSegment, type DonutChartProps } from './DonutChart'
|
|
6
|
+
export { LineChart, type LineSeries, type LineChartProps } from './LineChart'
|
|
7
|
+
export { GaugeChart, type GaugeThreshold, type GaugeChartProps } from './GaugeChart'
|
|
8
|
+
export { RadarChart, type RadarSeries, type RadarChartProps } from './RadarChart'
|
|
9
|
+
export { BarList, type BarListDatum, type BarListProps } from './BarList'
|
|
10
|
+
export { StatusTracker, type StatusSegment, type StatusTrackerProps } from './StatusTracker'
|
|
11
|
+
export { CategoryBar, type CategoryBarTone, type CategoryBarProps } from './CategoryBar'
|
package/src/index.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// import '@a4ui/core/styles.css'
|
|
9
9
|
// import { Button, Card, Modal } from '@a4ui/core'
|
|
10
10
|
|
|
11
|
-
export const A4UI_VERSION = '0.
|
|
11
|
+
export const A4UI_VERSION = '0.33.0'
|
|
12
12
|
|
|
13
13
|
// Helpers (src/lib) — generic, framework-level utilities.
|
|
14
14
|
export { cn } from './lib/cn'
|
|
@@ -179,6 +179,36 @@ export { FocusBlurGroup, type FocusBlurGroupProps } from './ui/FocusBlurGroup'
|
|
|
179
179
|
export { CardSpread, type CardSpreadProps, type CardSpreadLayout } from './ui/CardSpread'
|
|
180
180
|
export { Carousel3D, type Carousel3DProps, type Carousel3DVariant } from './ui/Carousel3D'
|
|
181
181
|
export { TimeMachineStack, type TimeMachineStackProps } from './ui/TimeMachineStack'
|
|
182
|
+
|
|
183
|
+
// Spatial "wow" primitives (bento, dock, beams, meteors).
|
|
184
|
+
export { BentoGrid, BentoCard, type BentoGridProps, type BentoCardProps } from './ui/BentoGrid'
|
|
185
|
+
export { Dock, type DockItem, type DockProps } from './ui/Dock'
|
|
186
|
+
export { AnimatedBeam, type AnimatedBeamProps } from './ui/AnimatedBeam'
|
|
187
|
+
export { BorderBeam, type BorderBeamProps } from './ui/BorderBeam'
|
|
188
|
+
export { Meteors, type MeteorsProps } from './ui/Meteors'
|
|
189
|
+
|
|
190
|
+
// AI agent surface + chat affordances.
|
|
191
|
+
export { ReasoningTrace, type ReasoningTraceProps } from './ui/ReasoningTrace'
|
|
192
|
+
export {
|
|
193
|
+
ToolCallTimeline,
|
|
194
|
+
type ToolCall,
|
|
195
|
+
type ToolCallStatus,
|
|
196
|
+
type ToolCallTimelineProps,
|
|
197
|
+
} from './ui/ToolCallTimeline'
|
|
198
|
+
export { DiffViewer, type DiffLine, type DiffViewerProps } from './ui/DiffViewer'
|
|
199
|
+
export { ModelPicker, type ModelOption, type ModelPickerProps } from './ui/ModelPicker'
|
|
200
|
+
export { UsageMeter, type UsageMeterProps } from './ui/UsageMeter'
|
|
201
|
+
export { SuggestionChips, type SuggestionChipsProps } from './ui/SuggestionChips'
|
|
202
|
+
|
|
203
|
+
// Content blocks.
|
|
204
|
+
export { Callout, type CalloutTone, type CalloutProps } from './ui/Callout'
|
|
205
|
+
export { Snippet, type SnippetProps } from './ui/Snippet'
|
|
206
|
+
|
|
207
|
+
// Spatial showpieces + motion extras.
|
|
208
|
+
export { Globe, type GlobeMarker, type GlobeArc, type GlobeProps } from './ui/Globe'
|
|
209
|
+
export { WorldMap, type WorldMapPoint, type WorldMapConnection, type WorldMapProps } from './ui/WorldMap'
|
|
210
|
+
export { Confetti, fireConfetti, type ConfettiProps } from './ui/Confetti'
|
|
211
|
+
export { CursorTrail, type CursorTrailProps } from './ui/CursorTrail'
|
|
182
212
|
export {
|
|
183
213
|
CopyButton,
|
|
184
214
|
type CopyButtonProps,
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// AnimatedBeam — an absolutely-positioned SVG overlay that draws a curved
|
|
2
|
+
// line between two elements (`fromRef` → `toRef`) inside a positioned
|
|
3
|
+
// `containerRef`, with a faint static stroke under a traveling
|
|
4
|
+
// primary→accent gradient segment that loops along the path. Path geometry
|
|
5
|
+
// is recomputed on mount, on window resize, and via a ResizeObserver on the
|
|
6
|
+
// container. No travel (static stroke only) under reduced motion.
|
|
7
|
+
import { createEffect, createSignal, createUniqueId, onCleanup, onMount, type JSX } from 'solid-js'
|
|
8
|
+
|
|
9
|
+
import { cn } from '../lib/cn'
|
|
10
|
+
import { animate, motionReduced } from '../lib/motion'
|
|
11
|
+
|
|
12
|
+
export interface AnimatedBeamProps {
|
|
13
|
+
containerRef: HTMLElement | undefined
|
|
14
|
+
fromRef: HTMLElement | undefined
|
|
15
|
+
toRef: HTMLElement | undefined
|
|
16
|
+
/** Px the curve bows away from a straight line. @default 0 (straight) */
|
|
17
|
+
curvature?: number
|
|
18
|
+
/** Travel from `toRef` to `fromRef` instead of the default direction. */
|
|
19
|
+
reverse?: boolean
|
|
20
|
+
/** Seconds for one travel of the gradient segment. @default 3 */
|
|
21
|
+
duration?: number
|
|
22
|
+
class?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface Point {
|
|
26
|
+
x: number
|
|
27
|
+
y: number
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Renders an animated SVG "beam" connecting two elements: a faint static
|
|
32
|
+
* base stroke under a traveling gradient segment that loops from `fromRef`
|
|
33
|
+
* to `toRef` (or the reverse, with `reverse`). Absolutely positioned over
|
|
34
|
+
* `containerRef`, whose bounds establish the coordinate space both
|
|
35
|
+
* endpoints are measured against. Static (no travel) under reduced motion.
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```tsx
|
|
39
|
+
* let container: HTMLDivElement | undefined
|
|
40
|
+
* let fromEl: HTMLDivElement | undefined
|
|
41
|
+
* let toEl: HTMLDivElement | undefined
|
|
42
|
+
*
|
|
43
|
+
* <div ref={container} class="relative flex items-center justify-between p-8">
|
|
44
|
+
* <div ref={fromEl} class="size-10 rounded-full bg-card" />
|
|
45
|
+
* <div ref={toEl} class="size-10 rounded-full bg-card" />
|
|
46
|
+
* <AnimatedBeam containerRef={container} fromRef={fromEl} toRef={toEl} curvature={40} />
|
|
47
|
+
* </div>
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
export function AnimatedBeam(props: AnimatedBeamProps): JSX.Element {
|
|
51
|
+
const [pathD, setPathD] = createSignal('')
|
|
52
|
+
const [size, setSize] = createSignal({ w: 0, h: 0 })
|
|
53
|
+
const gradientId = `beam-gradient-${createUniqueId()}`
|
|
54
|
+
|
|
55
|
+
let gradientEl: SVGLinearGradientElement | undefined
|
|
56
|
+
let start: Point = { x: 0, y: 0 }
|
|
57
|
+
let end: Point = { x: 0, y: 0 }
|
|
58
|
+
let control: Point = { x: 0, y: 0 }
|
|
59
|
+
|
|
60
|
+
const recompute = (): void => {
|
|
61
|
+
const container = props.containerRef
|
|
62
|
+
const from = props.fromRef
|
|
63
|
+
const to = props.toRef
|
|
64
|
+
if (!container || !from || !to) return
|
|
65
|
+
|
|
66
|
+
const containerRect = container.getBoundingClientRect()
|
|
67
|
+
const fromRect = from.getBoundingClientRect()
|
|
68
|
+
const toRect = to.getBoundingClientRect()
|
|
69
|
+
|
|
70
|
+
start = {
|
|
71
|
+
x: fromRect.left - containerRect.left + fromRect.width / 2,
|
|
72
|
+
y: fromRect.top - containerRect.top + fromRect.height / 2,
|
|
73
|
+
}
|
|
74
|
+
end = {
|
|
75
|
+
x: toRect.left - containerRect.left + toRect.width / 2,
|
|
76
|
+
y: toRect.top - containerRect.top + toRect.height / 2,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const curvature = props.curvature ?? 0
|
|
80
|
+
const mx = (start.x + end.x) / 2
|
|
81
|
+
const my = (start.y + end.y) / 2
|
|
82
|
+
const dx = end.x - start.x
|
|
83
|
+
const dy = end.y - start.y
|
|
84
|
+
const length = Math.hypot(dx, dy) || 1
|
|
85
|
+
control = {
|
|
86
|
+
x: mx + (-dy / length) * curvature,
|
|
87
|
+
y: my + (dx / length) * curvature,
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
setSize({ w: containerRect.width, h: containerRect.height })
|
|
91
|
+
setPathD(`M ${start.x} ${start.y} Q ${control.x} ${control.y} ${end.x} ${end.y}`)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Point at parameter `t` (0..1) along the quadratic curve. */
|
|
95
|
+
const pointAt = (t: number): Point => {
|
|
96
|
+
const it = 1 - t
|
|
97
|
+
return {
|
|
98
|
+
x: it * it * start.x + 2 * it * t * control.x + t * t * end.x,
|
|
99
|
+
y: it * it * start.y + 2 * it * t * control.y + t * t * end.y,
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
createEffect(() => {
|
|
104
|
+
// Track the refs reactively — recompute picks up the initial mount and
|
|
105
|
+
// any later change (e.g. a ref that resolves after this beam mounts).
|
|
106
|
+
void props.containerRef
|
|
107
|
+
void props.fromRef
|
|
108
|
+
void props.toRef
|
|
109
|
+
recompute()
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
onMount(() => {
|
|
113
|
+
const handleResize = (): void => recompute()
|
|
114
|
+
window.addEventListener('resize', handleResize)
|
|
115
|
+
|
|
116
|
+
let resizeObserver: ResizeObserver | undefined
|
|
117
|
+
if (props.containerRef && typeof ResizeObserver !== 'undefined') {
|
|
118
|
+
resizeObserver = new ResizeObserver(() => recompute())
|
|
119
|
+
resizeObserver.observe(props.containerRef)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
let controls: ReturnType<typeof animate> | undefined
|
|
123
|
+
if (!motionReduced()) {
|
|
124
|
+
const segment = 0.2
|
|
125
|
+
controls = animate(0, 1, {
|
|
126
|
+
duration: props.duration ?? 3,
|
|
127
|
+
repeat: Infinity,
|
|
128
|
+
ease: 'linear',
|
|
129
|
+
onUpdate: (p: number) => {
|
|
130
|
+
const t = props.reverse ? 1 - p : p
|
|
131
|
+
const lead = props.reverse ? Math.min(1, t + segment) : t
|
|
132
|
+
const trail = props.reverse ? t : Math.max(0, t - segment)
|
|
133
|
+
const from = pointAt(trail)
|
|
134
|
+
const to = pointAt(lead)
|
|
135
|
+
gradientEl?.setAttribute('x1', String(from.x))
|
|
136
|
+
gradientEl?.setAttribute('y1', String(from.y))
|
|
137
|
+
gradientEl?.setAttribute('x2', String(to.x))
|
|
138
|
+
gradientEl?.setAttribute('y2', String(to.y))
|
|
139
|
+
},
|
|
140
|
+
})
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
onCleanup(() => {
|
|
144
|
+
window.removeEventListener('resize', handleResize)
|
|
145
|
+
resizeObserver?.disconnect()
|
|
146
|
+
controls?.stop()
|
|
147
|
+
})
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
<svg
|
|
152
|
+
aria-hidden="true"
|
|
153
|
+
class={cn('pointer-events-none absolute inset-0', props.class)}
|
|
154
|
+
width={size().w}
|
|
155
|
+
height={size().h}
|
|
156
|
+
viewBox={`0 0 ${size().w} ${size().h}`}
|
|
157
|
+
fill="none"
|
|
158
|
+
>
|
|
159
|
+
<path d={pathD()} stroke="hsl(var(--border))" stroke-width="2" stroke-opacity="0.4" fill="none" />
|
|
160
|
+
{!motionReduced() && (
|
|
161
|
+
<>
|
|
162
|
+
<defs>
|
|
163
|
+
<linearGradient
|
|
164
|
+
ref={(el) => {
|
|
165
|
+
gradientEl = el
|
|
166
|
+
}}
|
|
167
|
+
id={gradientId}
|
|
168
|
+
gradientUnits="userSpaceOnUse"
|
|
169
|
+
>
|
|
170
|
+
<stop offset="0%" stop-color="hsl(var(--primary))" stop-opacity="0" />
|
|
171
|
+
<stop offset="50%" stop-color="hsl(var(--primary))" />
|
|
172
|
+
<stop offset="50%" stop-color="hsl(var(--accent))" />
|
|
173
|
+
<stop offset="100%" stop-color="hsl(var(--accent))" stop-opacity="0" />
|
|
174
|
+
</linearGradient>
|
|
175
|
+
</defs>
|
|
176
|
+
<path
|
|
177
|
+
d={pathD()}
|
|
178
|
+
stroke={`url(#${gradientId})`}
|
|
179
|
+
stroke-width="2"
|
|
180
|
+
stroke-linecap="round"
|
|
181
|
+
fill="none"
|
|
182
|
+
/>
|
|
183
|
+
</>
|
|
184
|
+
)}
|
|
185
|
+
</svg>
|
|
186
|
+
)
|
|
187
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// BentoGrid + BentoCard — responsive grid of glass tiles with variable spans.
|
|
2
|
+
import type { JSX } from 'solid-js'
|
|
3
|
+
import { splitProps } from 'solid-js'
|
|
4
|
+
|
|
5
|
+
import { cn } from '../lib/cn'
|
|
6
|
+
|
|
7
|
+
export interface BentoGridProps {
|
|
8
|
+
children: JSX.Element
|
|
9
|
+
class?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface BentoCardProps {
|
|
13
|
+
children: JSX.Element
|
|
14
|
+
/** Number of grid columns to span at `sm`+ (2) and `lg`+ (3). Defaults to 1. */
|
|
15
|
+
colSpan?: 1 | 2 | 3
|
|
16
|
+
/** Number of grid rows to span. Defaults to 1. */
|
|
17
|
+
rowSpan?: 1 | 2
|
|
18
|
+
class?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Static lookup so Tailwind's content scanner can see the full class names
|
|
22
|
+
// (dynamic `col-span-${n}` strings are invisible to it and get purged).
|
|
23
|
+
const COL_SPAN_CLASSES: Record<NonNullable<BentoCardProps['colSpan']>, string> = {
|
|
24
|
+
1: '',
|
|
25
|
+
2: 'sm:col-span-2',
|
|
26
|
+
3: 'sm:col-span-2 lg:col-span-3',
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const ROW_SPAN_CLASSES: Record<NonNullable<BentoCardProps['rowSpan']>, string> = {
|
|
30
|
+
1: '',
|
|
31
|
+
2: 'row-span-2',
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Responsive CSS grid for bento-style layouts — 1 column on mobile, 2 at
|
|
36
|
+
* `sm`, 3 at `lg`. Compose with {@link BentoCard} tiles, using `colSpan`/
|
|
37
|
+
* `rowSpan` on each tile to create the varied-size bento look.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```tsx
|
|
41
|
+
* <BentoGrid>
|
|
42
|
+
* <BentoCard colSpan={2} rowSpan={2}>
|
|
43
|
+
* <h3 class="text-lg font-semibold">Overview</h3>
|
|
44
|
+
* </BentoCard>
|
|
45
|
+
* <BentoCard>
|
|
46
|
+
* <h3 class="text-lg font-semibold">Uptime</h3>
|
|
47
|
+
* </BentoCard>
|
|
48
|
+
* <BentoCard>
|
|
49
|
+
* <h3 class="text-lg font-semibold">Latency</h3>
|
|
50
|
+
* </BentoCard>
|
|
51
|
+
* <BentoCard colSpan={3}>
|
|
52
|
+
* <h3 class="text-lg font-semibold">Recent activity</h3>
|
|
53
|
+
* </BentoCard>
|
|
54
|
+
* </BentoGrid>
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
export function BentoGrid(props: BentoGridProps): JSX.Element {
|
|
58
|
+
const [local, rest] = splitProps(props, ['class', 'children'])
|
|
59
|
+
return (
|
|
60
|
+
<div
|
|
61
|
+
class={cn(
|
|
62
|
+
'grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 auto-rows-[minmax(11rem,auto)]',
|
|
63
|
+
local.class,
|
|
64
|
+
)}
|
|
65
|
+
{...rest}
|
|
66
|
+
>
|
|
67
|
+
{local.children}
|
|
68
|
+
</div>
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Glass tile for a {@link BentoGrid}, spanning columns/rows via `colSpan`/`rowSpan`. */
|
|
73
|
+
export function BentoCard(props: BentoCardProps): JSX.Element {
|
|
74
|
+
const [local, rest] = splitProps(props, ['class', 'children', 'colSpan', 'rowSpan'])
|
|
75
|
+
return (
|
|
76
|
+
<div
|
|
77
|
+
class={cn(
|
|
78
|
+
'rounded-2xl border border-border bg-card p-5 text-card-foreground overflow-hidden',
|
|
79
|
+
'transition-transform hover:-translate-y-0.5',
|
|
80
|
+
COL_SPAN_CLASSES[local.colSpan ?? 1],
|
|
81
|
+
ROW_SPAN_CLASSES[local.rowSpan ?? 1],
|
|
82
|
+
local.class,
|
|
83
|
+
)}
|
|
84
|
+
{...rest}
|
|
85
|
+
>
|
|
86
|
+
{local.children}
|
|
87
|
+
</div>
|
|
88
|
+
)
|
|
89
|
+
}
|