@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,317 @@
1
+ // A dotted 3D globe rendered on a single <canvas> with plain Canvas 2D — no
2
+ // three.js, no WebGL. The sphere's dots come from a Fibonacci sphere
3
+ // (deterministic, evenly distributed — no Math.random so the dot field is
4
+ // stable across renders/SSR hydration). Each frame: rotate every point around
5
+ // the Y axis, project orthographically (drop z), keep only front-facing
6
+ // points (z > 0), and scale alpha/size by depth for a cheap "3D" read.
7
+ // Markers/arcs project through the same pipeline so they stay glued to the
8
+ // sphere as it spins. Colors are read from the live theme tokens via
9
+ // getComputedStyle (canvas can't consume `hsl(var(--x))` directly). Follows
10
+ // SpaceBackground's onMount/onCleanup idiom: bind listeners into a cleanups
11
+ // array, cancel the rAF, tear everything down on unmount.
12
+ import { createEffect, onCleanup, onMount, type JSX } from 'solid-js'
13
+
14
+ import { cn } from '../lib/cn'
15
+ import { motionReduced } from '../lib/motion'
16
+
17
+ export interface GlobeMarker {
18
+ lat: number
19
+ lng: number
20
+ label?: string
21
+ }
22
+
23
+ export interface GlobeArc {
24
+ /** [lat, lng] */
25
+ from: [number, number]
26
+ /** [lat, lng] */
27
+ to: [number, number]
28
+ }
29
+
30
+ export interface GlobeProps {
31
+ markers?: GlobeMarker[]
32
+ arcs?: GlobeArc[]
33
+ /** Canvas size in CSS px (square). @default 400 */
34
+ size?: number
35
+ /** Spin automatically. @default true (ignored under reduced motion). */
36
+ autoRotate?: boolean
37
+ class?: string
38
+ }
39
+
40
+ interface Vec3 {
41
+ x: number
42
+ y: number
43
+ z: number
44
+ }
45
+
46
+ interface Projected {
47
+ x: number
48
+ y: number
49
+ z: number
50
+ front: boolean
51
+ }
52
+
53
+ const DOT_COUNT = 900
54
+ const ROTATE_SPEED = 0.12 // rad/s
55
+ const DRAG_SPEED = 0.012 // rad per px of pointer movement
56
+
57
+ // Fibonacci sphere: evenly-spaced points on a unit sphere, fully deterministic
58
+ // (no RNG) so the dot field is stable across renders and reproducible in tests.
59
+ function fibonacciSphere(count: number): Vec3[] {
60
+ const points: Vec3[] = []
61
+ const goldenAngle = Math.PI * (3 - Math.sqrt(5))
62
+ for (let i = 0; i < count; i++) {
63
+ const y = 1 - (i / (count - 1)) * 2 // 1..-1
64
+ const radius = Math.sqrt(Math.max(0, 1 - y * y))
65
+ const theta = goldenAngle * i
66
+ points.push({ x: Math.cos(theta) * radius, y, z: Math.sin(theta) * radius })
67
+ }
68
+ return points
69
+ }
70
+
71
+ const SPHERE_POINTS = fibonacciSphere(DOT_COUNT)
72
+
73
+ function latLngToVec3(lat: number, lng: number): Vec3 {
74
+ const latRad = (lat * Math.PI) / 180
75
+ const lngRad = (lng * Math.PI) / 180
76
+ return {
77
+ x: Math.cos(latRad) * Math.sin(lngRad),
78
+ y: Math.sin(latRad),
79
+ z: Math.cos(latRad) * Math.cos(lngRad),
80
+ }
81
+ }
82
+
83
+ // Rotate around the Y axis and project orthographically to a `radius`-sized
84
+ // disc centered at (cx, cy). z stays in [-1, 1] (front-facing when > 0).
85
+ function project(p: Vec3, angle: number, radius: number, cx: number, cy: number): Projected {
86
+ const cos = Math.cos(angle)
87
+ const sin = Math.sin(angle)
88
+ const x = p.x * cos + p.z * sin
89
+ const z = -p.x * sin + p.z * cos
90
+ const y = p.y
91
+ return { x: cx + x * radius, y: cy - y * radius, z, front: z > 0 }
92
+ }
93
+
94
+ function resolveColor(canvas: HTMLCanvasElement, token: string, fallback: string): string {
95
+ const raw = getComputedStyle(document.documentElement).getPropertyValue(token).trim()
96
+ return raw ? `hsl(${raw})` : fallback
97
+ }
98
+
99
+ /**
100
+ * A dotted 3D globe drawn on `<canvas>` with plain Canvas 2D (no three.js,
101
+ * no WebGL): dots come from a deterministic Fibonacci-sphere distribution,
102
+ * rotate around the Y axis, and project orthographically with depth-scaled
103
+ * alpha/size. `markers` render as brighter glowing dots (title = label);
104
+ * `arcs` draw as outward-bulging curves between two front-facing markers,
105
+ * with a small traveling highlight. Auto-rotates unless `autoRotate={false}`;
106
+ * drag with the pointer to spin manually. Renders a single static frame
107
+ * under `motionReduced()` (no auto-rotation; drag still works).
108
+ *
109
+ * @example
110
+ * ```tsx
111
+ * <Globe
112
+ * size={360}
113
+ * markers={[
114
+ * { lat: 40.7, lng: -74.0, label: 'New York' },
115
+ * { lat: 35.7, lng: 139.7, label: 'Tokyo' },
116
+ * ]}
117
+ * arcs={[{ from: [40.7, -74.0], to: [35.7, 139.7] }]}
118
+ * />
119
+ * ```
120
+ */
121
+ export function Globe(props: GlobeProps): JSX.Element {
122
+ let canvas!: HTMLCanvasElement
123
+ const size = () => props.size ?? 400
124
+
125
+ onMount(() => {
126
+ const ctx = canvas.getContext('2d')
127
+ if (!ctx) return
128
+
129
+ const cleanups: Array<() => void> = []
130
+ const bind = (
131
+ target: EventTarget,
132
+ type: string,
133
+ handler: EventListener,
134
+ opts?: AddEventListenerOptions,
135
+ ) => {
136
+ target.addEventListener(type, handler, opts)
137
+ cleanups.push(() => target.removeEventListener(type, handler, opts))
138
+ }
139
+
140
+ let angle = 0
141
+ let dragging = false
142
+ let lastX = 0
143
+ let rafId = 0
144
+ let lastTs = 0
145
+ // Screen-space marker positions from the last draw, for hover hit-testing
146
+ // (cheap: only a handful of markers, refreshed every frame anyway).
147
+ let markerHits: Array<{ x: number; y: number; label: string }> = []
148
+
149
+ const draw = (): void => {
150
+ const dpr = window.devicePixelRatio || 1
151
+ const cssSize = size()
152
+ const radius = cssSize * 0.42
153
+ const cx = cssSize / 2
154
+ const cy = cssSize / 2
155
+
156
+ if (canvas.width !== cssSize * dpr || canvas.height !== cssSize * dpr) {
157
+ canvas.width = cssSize * dpr
158
+ canvas.height = cssSize * dpr
159
+ canvas.style.width = `${cssSize}px`
160
+ canvas.style.height = `${cssSize}px`
161
+ }
162
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
163
+ ctx.clearRect(0, 0, cssSize, cssSize)
164
+
165
+ const dotColor = resolveColor(canvas, '--foreground', '210 20% 90%')
166
+ const markerColor = resolveColor(canvas, '--accent', '200 90% 60%')
167
+ const arcColor = resolveColor(canvas, '--primary', '250 80% 65%')
168
+
169
+ // Base sphere dots, back-to-front isn't needed (no overlap testing) —
170
+ // just skip anything facing away from the viewer.
171
+ for (const p of SPHERE_POINTS) {
172
+ const proj = project(p, angle, radius, cx, cy)
173
+ if (!proj.front) continue
174
+ const depth = proj.z // 0..1
175
+ ctx.beginPath()
176
+ ctx.fillStyle = dotColor
177
+ ctx.globalAlpha = 0.15 + depth * 0.55
178
+ ctx.arc(proj.x, proj.y, 0.6 + depth * 1.2, 0, Math.PI * 2)
179
+ ctx.fill()
180
+ }
181
+ ctx.globalAlpha = 1
182
+
183
+ // Arcs: only drawn when both endpoints are currently front-facing.
184
+ const arcs = props.arcs ?? []
185
+ for (const arc of arcs) {
186
+ const from = project(latLngToVec3(arc.from[0], arc.from[1]), angle, radius, cx, cy)
187
+ const to = project(latLngToVec3(arc.to[0], arc.to[1]), angle, radius, cx, cy)
188
+ if (!from.front || !to.front) continue
189
+
190
+ const mx = (from.x + to.x) / 2
191
+ const my = (from.y + to.y) / 2
192
+ // Bulge outward from the sphere center, proportional to the chord length.
193
+ const dx = mx - cx
194
+ const dy = my - cy
195
+ const dist = Math.hypot(dx, dy) || 1
196
+ const chord = Math.hypot(to.x - from.x, to.y - from.y)
197
+ const bulge = chord * 0.35
198
+ const ctrlX = mx + (dx / dist) * bulge
199
+ const ctrlY = my + (dy / dist) * bulge
200
+
201
+ ctx.beginPath()
202
+ ctx.strokeStyle = arcColor
203
+ ctx.globalAlpha = 0.55
204
+ ctx.lineWidth = 1.4
205
+ ctx.moveTo(from.x, from.y)
206
+ ctx.quadraticCurveTo(ctrlX, ctrlY, to.x, to.y)
207
+ ctx.stroke()
208
+ ctx.globalAlpha = 1
209
+
210
+ // Traveling highlight: a bright dot eased along the same quadratic,
211
+ // looping with the wall clock so it doesn't need its own rAF state.
212
+ if (!motionReduced()) {
213
+ const t = (performance.now() / 1600) % 1
214
+ const it = 1 - t
215
+ const hx = it * it * from.x + 2 * it * t * ctrlX + t * t * to.x
216
+ const hy = it * it * from.y + 2 * it * t * ctrlY + t * t * to.y
217
+ ctx.beginPath()
218
+ ctx.fillStyle = markerColor
219
+ ctx.arc(hx, hy, 2.4, 0, Math.PI * 2)
220
+ ctx.fill()
221
+ }
222
+ }
223
+
224
+ // Markers: brighter glowing dots, only when front-facing.
225
+ const markers = props.markers ?? []
226
+ const hits: typeof markerHits = []
227
+ for (const marker of markers) {
228
+ const proj = project(latLngToVec3(marker.lat, marker.lng), angle, radius, cx, cy)
229
+ if (!proj.front) continue
230
+ ctx.beginPath()
231
+ ctx.fillStyle = markerColor
232
+ ctx.shadowColor = markerColor
233
+ ctx.shadowBlur = 8
234
+ ctx.arc(proj.x, proj.y, 3, 0, Math.PI * 2)
235
+ ctx.fill()
236
+ ctx.shadowBlur = 0
237
+ if (marker.label) hits.push({ x: proj.x, y: proj.y, label: marker.label })
238
+ }
239
+ markerHits = hits
240
+ }
241
+
242
+ const tick = (ts: number): void => {
243
+ const dt = lastTs ? (ts - lastTs) / 1000 : 0
244
+ lastTs = ts
245
+ if (!dragging && !motionReduced() && props.autoRotate !== false) {
246
+ angle += ROTATE_SPEED * dt
247
+ }
248
+ draw()
249
+ // Keep animating while auto-rotating, or to refresh the traveling arc
250
+ // highlight — both are no-ops under reduced motion (frozen frame).
251
+ if (!motionReduced()) rafId = requestAnimationFrame(tick)
252
+ }
253
+
254
+ if (motionReduced()) {
255
+ draw() // single static frame
256
+ } else {
257
+ rafId = requestAnimationFrame(tick)
258
+ }
259
+
260
+ // Pointer drag to spin manually — works even under reduced motion.
261
+ const onPointerDown = (e: PointerEvent): void => {
262
+ dragging = true
263
+ lastX = e.clientX
264
+ canvas.setPointerCapture(e.pointerId)
265
+ }
266
+ const onPointerMove = (e: PointerEvent): void => {
267
+ if (!dragging) {
268
+ // Hover hit-test against the last drawn marker positions → native tooltip.
269
+ const rect = canvas.getBoundingClientRect()
270
+ const px = e.clientX - rect.left
271
+ const py = e.clientY - rect.top
272
+ const hit = markerHits.find((m) => Math.hypot(m.x - px, m.y - py) <= 6)
273
+ canvas.title = hit?.label ?? ''
274
+ return
275
+ }
276
+ const dx = e.clientX - lastX
277
+ lastX = e.clientX
278
+ angle += dx * DRAG_SPEED
279
+ if (motionReduced()) draw() // rAF loop is off; redraw explicitly per move
280
+ }
281
+ const onPointerUp = (e: PointerEvent): void => {
282
+ dragging = false
283
+ if (canvas.hasPointerCapture(e.pointerId)) canvas.releasePointerCapture(e.pointerId)
284
+ }
285
+ bind(canvas, 'pointerdown', onPointerDown as EventListener)
286
+ bind(canvas, 'pointermove', onPointerMove as EventListener)
287
+ bind(canvas, 'pointerup', onPointerUp as EventListener)
288
+ bind(canvas, 'pointercancel', onPointerUp as EventListener)
289
+
290
+ // Re-render (static frame) if props change while reduced motion holds the
291
+ // loop frozen — otherwise the rAF loop already picks up prop changes.
292
+ createEffect(() => {
293
+ // Track reactive deps: size + reference identity of markers/arcs.
294
+ void size()
295
+ void props.markers
296
+ void props.arcs
297
+ void props.autoRotate
298
+ if (motionReduced()) draw()
299
+ })
300
+
301
+ onCleanup(() => {
302
+ if (rafId) cancelAnimationFrame(rafId)
303
+ cleanups.forEach((fn) => fn())
304
+ })
305
+ })
306
+
307
+ return (
308
+ <canvas
309
+ ref={canvas}
310
+ class={cn('touch-none rounded-full select-none', props.class)}
311
+ width={size()}
312
+ height={size()}
313
+ role="img"
314
+ aria-label="Interactive globe"
315
+ />
316
+ )
317
+ }
@@ -0,0 +1,109 @@
1
+ // Meteors — Aceternity-style decorative meteor shower: thin diagonal streaks
2
+ // that fall down-left and fade, looping with per-meteor stagger. Pure CSS
3
+ // keyframes (injected once into the document head, like LoadingDots) so the
4
+ // motion costs nothing in JS; each meteor's position/delay/duration is derived
5
+ // deterministically from its index (no Math.random, so the layout is
6
+ // reproducible across renders and safe under SSR).
7
+ import { For, type JSX, Show } from 'solid-js'
8
+
9
+ import { cn } from '../lib/cn'
10
+ import { motionReduced } from '../lib/motion'
11
+
12
+ export interface MeteorsProps {
13
+ /** Number of meteor streaks to render. @default 20 */
14
+ count?: number
15
+ class?: string
16
+ }
17
+
18
+ const STYLE_ID = 'a4ui-meteors-style'
19
+
20
+ function ensureStyleInjected(): void {
21
+ if (typeof document === 'undefined' || document.getElementById(STYLE_ID)) return
22
+ const style = document.createElement('style')
23
+ style.id = STYLE_ID
24
+ style.textContent = `
25
+ @keyframes a4-meteor-fall {
26
+ 0% { transform: rotate(215deg) translateX(0); opacity: 1; }
27
+ 70% { opacity: 1; }
28
+ 100% { transform: rotate(215deg) translateX(-500px); opacity: 0; }
29
+ }
30
+ `
31
+ document.head.appendChild(style)
32
+ }
33
+
34
+ /**
35
+ * Deterministic pseudo-random value in [0, 1) derived from an index via a
36
+ * sine-based hash. Used to vary each meteor's position/timing without
37
+ * `Math.random`, so the layout is identical across renders (and SSR-safe).
38
+ */
39
+ function pseudoRandom(seed: number): number {
40
+ const x = Math.sin(seed * 12.9898) * 43758.5453
41
+ return x - Math.floor(x)
42
+ }
43
+
44
+ /**
45
+ * Decorative full-cover meteor shower: thin diagonal streaks that fall
46
+ * down-left and fade, looping with staggered delays and varied start
47
+ * positions/durations. Purely visual — absolute, `pointer-events-none`,
48
+ * `aria-hidden` — so it layers behind/inside a `relative overflow-hidden`
49
+ * container without affecting layout or a11y. Under {@link motionReduced} it
50
+ * swaps the falling streaks for a handful of static, faint dots.
51
+ *
52
+ * @example
53
+ * ```tsx
54
+ * <div class="relative overflow-hidden rounded-2xl border border-border p-8">
55
+ * <Meteors count={20} />
56
+ * <p class="relative">Content sits above the meteors.</p>
57
+ * </div>
58
+ * ```
59
+ */
60
+ export function Meteors(props: MeteorsProps): JSX.Element {
61
+ ensureStyleInjected()
62
+
63
+ const count = () => props.count ?? 20
64
+ const meteors = () => Array.from({ length: count() })
65
+ const staticDots = () => Array.from({ length: Math.min(6, count()) })
66
+
67
+ return (
68
+ <div class={cn('pointer-events-none absolute inset-0 overflow-hidden', props.class)} aria-hidden="true">
69
+ <Show
70
+ when={!motionReduced()}
71
+ fallback={
72
+ <For each={staticDots()}>
73
+ {(_, i) => (
74
+ <span
75
+ class="absolute h-0.5 w-0.5 rounded-full bg-foreground/30"
76
+ style={{
77
+ top: `${pseudoRandom(i() * 2 + 1) * 100}%`,
78
+ left: `${pseudoRandom(i() * 2 + 2) * 100}%`,
79
+ }}
80
+ />
81
+ )}
82
+ </For>
83
+ }
84
+ >
85
+ <For each={meteors()}>
86
+ {(_, i) => {
87
+ const left = pseudoRandom(i() * 2 + 1) * 100
88
+ const delay = pseudoRandom(i() * 2 + 2) * 8
89
+ const duration = 2 + pseudoRandom(i() * 3 + 5) * 5
90
+
91
+ return (
92
+ <span
93
+ class={cn(
94
+ 'absolute top-0 h-0.5 w-0.5 rotate-[215deg] rounded-full bg-foreground',
95
+ "before:absolute before:top-1/2 before:h-px before:w-12 before:-translate-y-1/2 before:bg-gradient-to-r before:from-foreground/60 before:to-transparent before:content-['']",
96
+ )}
97
+ style={{
98
+ left: `${left}%`,
99
+ animation: `a4-meteor-fall ${duration}s linear infinite`,
100
+ 'animation-delay': `${delay}s`,
101
+ }}
102
+ />
103
+ )
104
+ }}
105
+ </For>
106
+ </Show>
107
+ </div>
108
+ )
109
+ }
@@ -0,0 +1,119 @@
1
+ // AI model picker — dropdown trigger (icon + name + chevron) over a menu of
2
+ // models (icon + name + description + badge), selected one checkmarked.
3
+ // Built on Kobalte's DropdownMenu (same primitive as Dropdown.tsx), using its
4
+ // RadioGroup/RadioItem for single-select keyboard navigation and a11y
5
+ // (role="menuitemradio", aria-checked) — Dropdown.tsx's own item API only
6
+ // supports a plain string label, not this richer per-option content.
7
+ import { DropdownMenu } from '@kobalte/core/dropdown-menu'
8
+ import { Check, ChevronDown } from 'lucide-solid'
9
+ import type { JSX } from 'solid-js'
10
+ import { For, Show, createSignal, untrack } from 'solid-js'
11
+
12
+ import { cn } from '../lib/cn'
13
+
14
+ /** A single selectable model in a {@link ModelPicker} menu. */
15
+ export interface ModelOption {
16
+ id: string
17
+ name: string
18
+ icon?: JSX.Element
19
+ description?: string
20
+ badge?: JSX.Element
21
+ }
22
+
23
+ export interface ModelPickerProps {
24
+ models: ModelOption[]
25
+ /** Controlled selected model id. Omit and use `defaultValue` for uncontrolled use. */
26
+ value?: string
27
+ /** Initial selected model id when uncontrolled. Ignored if `value` is passed. */
28
+ defaultValue?: string
29
+ onChange?: (id: string) => void
30
+ class?: string
31
+ }
32
+
33
+ /**
34
+ * Dropdown trigger showing the selected model's icon + name + a chevron; the
35
+ * menu lists every model with icon, name, description, and optional badge,
36
+ * checkmarking the current selection. Controlled via `value`/`onChange` or
37
+ * uncontrolled via `defaultValue`, and fully keyboard-navigable (arrow keys,
38
+ * typeahead, Escape) via Kobalte's `DropdownMenu`.
39
+ *
40
+ * @example
41
+ * ```tsx
42
+ * <ModelPicker
43
+ * models={[
44
+ * { id: 'sonnet', name: 'Claude Sonnet', description: 'Balanced speed and quality' },
45
+ * { id: 'opus', name: 'Claude Opus', description: 'Most capable', badge: <Badge tone="info">New</Badge> },
46
+ * ]}
47
+ * defaultValue="sonnet"
48
+ * onChange={(id) => console.log('selected', id)}
49
+ * />
50
+ * ```
51
+ */
52
+ export function ModelPicker(props: ModelPickerProps): JSX.Element {
53
+ // Initial value only: read once (untracked), by design — this seeds the
54
+ // uncontrolled fallback and must not re-run when `models`/`defaultValue` change later.
55
+ const [internal, setInternal] = createSignal(untrack(() => props.defaultValue ?? props.models[0]?.id))
56
+ const selectedId = () => props.value ?? internal()
57
+ const selectedModel = () => props.models.find((m) => m.id === selectedId())
58
+
59
+ const handleChange = (id: string) => {
60
+ if (props.value === undefined) setInternal(id)
61
+ props.onChange?.(id)
62
+ }
63
+
64
+ return (
65
+ <DropdownMenu preventScroll={false}>
66
+ <DropdownMenu.Trigger
67
+ class={cn(
68
+ 'inline-flex items-center gap-2 rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground outline-none transition-colors a4-field',
69
+ props.class,
70
+ )}
71
+ aria-label="Select model"
72
+ >
73
+ <Show when={selectedModel()?.icon}>
74
+ <span class="flex h-4 w-4 shrink-0 items-center justify-center text-muted-foreground">
75
+ {selectedModel()?.icon}
76
+ </span>
77
+ </Show>
78
+ <span class="truncate font-medium">{selectedModel()?.name}</span>
79
+ <ChevronDown class="h-4 w-4 shrink-0 text-muted-foreground" />
80
+ </DropdownMenu.Trigger>
81
+ <DropdownMenu.Portal>
82
+ <DropdownMenu.Content class="z-50 min-w-64 overflow-hidden rounded-md border border-border bg-card p-1 text-card-foreground shadow-sm">
83
+ <DropdownMenu.RadioGroup value={selectedId()} onChange={handleChange}>
84
+ <For each={props.models}>
85
+ {(model) => (
86
+ <DropdownMenu.RadioItem
87
+ value={model.id}
88
+ class="flex cursor-pointer select-none items-center gap-2 rounded-sm px-2 py-1.5 outline-none hover:bg-muted data-[disabled]:pointer-events-none data-[disabled]:opacity-50"
89
+ >
90
+ <Show when={model.icon}>
91
+ <span class="flex h-4 w-4 shrink-0 items-center justify-center text-muted-foreground">
92
+ {model.icon}
93
+ </span>
94
+ </Show>
95
+ <span class="flex min-w-0 flex-1 flex-col">
96
+ <DropdownMenu.ItemLabel class="truncate text-sm text-foreground">
97
+ {model.name}
98
+ </DropdownMenu.ItemLabel>
99
+ <Show when={model.description}>
100
+ <DropdownMenu.ItemDescription class="truncate text-xs text-muted-foreground">
101
+ {model.description}
102
+ </DropdownMenu.ItemDescription>
103
+ </Show>
104
+ </span>
105
+ <Show when={model.badge}>{model.badge}</Show>
106
+ <span class="flex h-4 w-4 shrink-0 items-center justify-center text-primary">
107
+ <Show when={model.id === selectedId()}>
108
+ <Check class="h-4 w-4" />
109
+ </Show>
110
+ </span>
111
+ </DropdownMenu.RadioItem>
112
+ )}
113
+ </For>
114
+ </DropdownMenu.RadioGroup>
115
+ </DropdownMenu.Content>
116
+ </DropdownMenu.Portal>
117
+ </DropdownMenu>
118
+ )
119
+ }
@@ -0,0 +1,83 @@
1
+ // ReasoningTrace — collapsible "thinking" panel for AI surfaces. Mirrors
2
+ // Collapse.tsx's header/panel idiom (grid-rows transition, chevron rotate)
3
+ // but owns its own open state (seeded from `defaultOpen`) and adds a
4
+ // streaming pulse indicator for in-flight reasoning.
5
+ import { Brain, ChevronDown } from 'lucide-solid'
6
+ import { type JSX, Show, createSignal } from 'solid-js'
7
+
8
+ import { cn } from '../lib/cn'
9
+
10
+ export interface ReasoningTraceProps {
11
+ /** Reasoning body content; takes precedence over `text` when both are given. */
12
+ children?: JSX.Element
13
+ /** Plain-text reasoning body, rendered with preserved whitespace. */
14
+ text?: string
15
+ /** Header label. Defaults to `'Reasoning'`. */
16
+ label?: JSX.Element
17
+ /** Whether the panel starts expanded. Defaults to `false`. */
18
+ defaultOpen?: boolean
19
+ /** Show a pulsing dot in the header to indicate reasoning is still streaming. */
20
+ streaming?: boolean
21
+ class?: string
22
+ }
23
+
24
+ /**
25
+ * Collapsible panel for surfacing a model's intermediate "thinking"/reasoning
26
+ * trace. The header is a toggle button (brain icon + label + rotating
27
+ * chevron, plus a pulsing dot while `streaming`); the body is a muted,
28
+ * smaller-text block showing `text` or `children` with whitespace preserved.
29
+ * Collapsed by default unless `defaultOpen`. Height/opacity transition,
30
+ * instant under reduced motion.
31
+ *
32
+ * @example
33
+ * ```tsx
34
+ * <ReasoningTrace streaming text={reasoningSoFar()} />
35
+ * ```
36
+ */
37
+ export function ReasoningTrace(props: ReasoningTraceProps): JSX.Element {
38
+ const [open, setOpen] = createSignal(props.defaultOpen ?? false)
39
+
40
+ return (
41
+ <div class={cn('rounded-xl border border-border bg-card', props.class)}>
42
+ <button
43
+ type="button"
44
+ aria-expanded={open()}
45
+ onClick={() => setOpen(!open())}
46
+ class="flex w-full items-center gap-2 rounded-xl px-3 py-2 text-sm font-medium text-muted-foreground hover:bg-muted"
47
+ >
48
+ <Brain class="h-4 w-4 shrink-0" aria-hidden="true" />
49
+ <span class="flex-1 text-left">{props.label ?? 'Reasoning'}</span>
50
+ <Show when={props.streaming}>
51
+ <span class="relative flex h-1.5 w-1.5 shrink-0" aria-hidden="true">
52
+ <span class="absolute inline-flex h-full w-full animate-ping rounded-full bg-current opacity-75 motion-reduce:animate-none" />
53
+ <span class="relative inline-flex h-1.5 w-1.5 rounded-full bg-current" />
54
+ </span>
55
+ </Show>
56
+ <ChevronDown
57
+ class={cn(
58
+ 'h-4 w-4 shrink-0 transition-transform duration-200 motion-reduce:transition-none',
59
+ open() && 'rotate-180',
60
+ )}
61
+ aria-hidden="true"
62
+ />
63
+ </button>
64
+ <div
65
+ class={cn(
66
+ 'grid transition-[grid-template-rows] duration-200 ease-out motion-reduce:transition-none',
67
+ open() ? 'grid-rows-[1fr]' : 'grid-rows-[0fr]',
68
+ )}
69
+ >
70
+ <div class="overflow-hidden">
71
+ <div
72
+ class={cn(
73
+ 'whitespace-pre-wrap px-3 pb-3 text-xs leading-relaxed text-muted-foreground transition-opacity duration-200 motion-reduce:transition-none',
74
+ open() ? 'opacity-100' : 'opacity-0',
75
+ )}
76
+ >
77
+ {props.children ?? props.text}
78
+ </div>
79
+ </div>
80
+ </div>
81
+ </div>
82
+ )
83
+ }
@@ -0,0 +1,64 @@
1
+ // Single inline code block with a copy button, for a one-liner or short
2
+ // excerpt embedded in prose. Lighter than CodeTabs (which is a card with tabs
3
+ // for several samples) — Snippet is just one block, no chrome besides the
4
+ // copy affordance and an optional language tag.
5
+ import { Check, Copy } from 'lucide-solid'
6
+ import type { JSX } from 'solid-js'
7
+ import { createSignal, Show } from 'solid-js'
8
+
9
+ import { cn } from '../lib/cn'
10
+
11
+ const COPIED_TIMEOUT_MS = 1500
12
+
13
+ export interface SnippetProps {
14
+ /** Source text to display and copy. */
15
+ code: string
16
+ /** Optional language tag shown as a small badge, e.g. `'tsx'`. Informational only, no highlighting. */
17
+ language?: string
18
+ class?: string
19
+ }
20
+
21
+ /**
22
+ * Single inline code block (mono, bordered, scrollable) with a copy button
23
+ * that morphs into a checkmark for 1.5s after copying. For one code sample —
24
+ * use {@link CodeTabs} instead when you need several tabs/languages.
25
+ *
26
+ * @example
27
+ * ```tsx
28
+ * <Snippet language="bash" code="pnpm add @a4ui/core" />
29
+ * ```
30
+ */
31
+ export function Snippet(props: SnippetProps): JSX.Element {
32
+ const [copied, setCopied] = createSignal(false)
33
+
34
+ const copy = () => {
35
+ if (!navigator.clipboard) return
36
+ navigator.clipboard.writeText(props.code).then(() => {
37
+ setCopied(true)
38
+ setTimeout(() => setCopied(false), COPIED_TIMEOUT_MS)
39
+ })
40
+ }
41
+
42
+ return (
43
+ <div class={cn('relative rounded-lg border border-border bg-muted px-3 py-2', props.class)}>
44
+ <Show when={props.language}>
45
+ <span class="absolute right-9 top-2 rounded bg-card px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground">
46
+ {props.language}
47
+ </span>
48
+ </Show>
49
+ <button
50
+ type="button"
51
+ aria-label={copied() ? 'Copied' : 'Copy to clipboard'}
52
+ class="absolute right-2 top-2 inline-flex h-5 w-5 items-center justify-center rounded text-muted-foreground hover:text-foreground"
53
+ onClick={copy}
54
+ >
55
+ <Show when={copied()} fallback={<Copy class="h-3.5 w-3.5" />}>
56
+ <Check class="h-3.5 w-3.5" />
57
+ </Show>
58
+ </button>
59
+ <pre class="overflow-x-auto whitespace-pre pr-8 font-mono text-sm text-foreground">
60
+ <code>{props.code}</code>
61
+ </pre>
62
+ </div>
63
+ )
64
+ }