@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.
Files changed (60) hide show
  1. package/dist/NumberInput-Bnh2u7pp.js +381 -0
  2. package/dist/charts/BarList.d.ts +33 -0
  3. package/dist/charts/CategoryBar.d.ts +23 -0
  4. package/dist/charts/GaugeChart.d.ts +43 -0
  5. package/dist/charts/LineChart.d.ts +39 -0
  6. package/dist/charts/RadarChart.d.ts +34 -0
  7. package/dist/charts/StatusTracker.d.ts +27 -0
  8. package/dist/charts/index.d.ts +6 -0
  9. package/dist/charts.js +693 -100
  10. package/dist/commerce.js +2 -2
  11. package/dist/elements.css +206 -0
  12. package/dist/full.css +206 -0
  13. package/dist/index.d.ts +18 -1
  14. package/dist/index.js +5086 -3915
  15. package/dist/motion-jYMWmZqB.js +119 -0
  16. package/dist/ui/AnimatedBeam.d.ts +34 -0
  17. package/dist/ui/BentoGrid.d.ts +39 -0
  18. package/dist/ui/BorderBeam.d.ts +27 -0
  19. package/dist/ui/Callout.d.ts +27 -0
  20. package/dist/ui/Confetti.d.ts +46 -0
  21. package/dist/ui/CursorTrail.d.ts +25 -0
  22. package/dist/ui/DiffViewer.d.ts +33 -0
  23. package/dist/ui/Dock.d.ts +31 -0
  24. package/dist/ui/Globe.d.ts +44 -0
  25. package/dist/ui/Meteors.d.ts +23 -0
  26. package/dist/ui/ModelPicker.d.ts +38 -0
  27. package/dist/ui/ReasoningTrace.d.ts +28 -0
  28. package/dist/ui/Snippet.d.ts +19 -0
  29. package/dist/ui/SuggestionChips.d.ts +25 -0
  30. package/dist/ui/ToolCallTimeline.d.ts +34 -0
  31. package/dist/ui/UsageMeter.d.ts +26 -0
  32. package/dist/ui/WorldMap.d.ts +36 -0
  33. package/package.json +1 -1
  34. package/src/charts/BarList.tsx +83 -0
  35. package/src/charts/CategoryBar.tsx +95 -0
  36. package/src/charts/GaugeChart.tsx +175 -0
  37. package/src/charts/LineChart.tsx +264 -0
  38. package/src/charts/RadarChart.tsx +215 -0
  39. package/src/charts/StatusTracker.tsx +81 -0
  40. package/src/charts/index.ts +6 -0
  41. package/src/index.ts +31 -1
  42. package/src/ui/AnimatedBeam.tsx +187 -0
  43. package/src/ui/BentoGrid.tsx +89 -0
  44. package/src/ui/BorderBeam.tsx +96 -0
  45. package/src/ui/Callout.tsx +66 -0
  46. package/src/ui/Confetti.tsx +131 -0
  47. package/src/ui/CursorTrail.tsx +88 -0
  48. package/src/ui/DiffViewer.tsx +166 -0
  49. package/src/ui/Dock.tsx +124 -0
  50. package/src/ui/Globe.tsx +317 -0
  51. package/src/ui/Meteors.tsx +109 -0
  52. package/src/ui/ModelPicker.tsx +119 -0
  53. package/src/ui/ReasoningTrace.tsx +83 -0
  54. package/src/ui/Snippet.tsx +64 -0
  55. package/src/ui/SuggestionChips.tsx +65 -0
  56. package/src/ui/ToolCallTimeline.tsx +137 -0
  57. package/src/ui/UsageMeter.tsx +71 -0
  58. package/src/ui/WorldMap.tsx +191 -0
  59. package/dist/NumberInput-BQhVucw-.js +0 -491
  60. 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
+ }
@@ -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.31.1'
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
+ }