@nous-research/ui 0.15.0 → 0.17.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 (258) hide show
  1. package/CHANGELOG.md +266 -0
  2. package/README.md +24 -4
  3. package/dist/fonts.js +1 -0
  4. package/dist/hooks/use-below-breakpoint.d.ts +2 -0
  5. package/dist/hooks/use-below-breakpoint.js +17 -0
  6. package/dist/hooks/use-capped-frame.js +1 -0
  7. package/dist/hooks/use-confirm-delete.d.ts +10 -0
  8. package/dist/hooks/use-confirm-delete.js +35 -0
  9. package/dist/hooks/use-css-var-dims.js +1 -0
  10. package/dist/hooks/use-gpu-tier.js +1 -0
  11. package/dist/hooks/use-render-loop.js +1 -0
  12. package/dist/hooks/use-smooth-controls.js +1 -0
  13. package/dist/hooks/use-toast.d.ts +7 -0
  14. package/dist/hooks/use-toast.js +21 -0
  15. package/dist/index.d.ts +11 -1
  16. package/dist/index.js +23 -1
  17. package/dist/ui/basic-page.js +1 -0
  18. package/dist/ui/components/animated-count.js +1 -0
  19. package/dist/ui/components/ascii.js +1 -0
  20. package/dist/ui/components/badge.js +2 -1
  21. package/dist/ui/components/badges/nous-girl.js +1 -0
  22. package/dist/ui/components/blend-mode.js +1 -0
  23. package/dist/ui/components/blink.js +1 -0
  24. package/dist/ui/components/bottom-sheet.d.ts +15 -0
  25. package/dist/ui/components/bottom-sheet.js +192 -0
  26. package/dist/ui/components/button.js +2 -1
  27. package/dist/ui/components/card.d.ts +5 -0
  28. package/dist/ui/components/card.js +74 -0
  29. package/dist/ui/components/checkbox.d.ts +1 -1
  30. package/dist/ui/components/checkbox.js +2 -1
  31. package/dist/ui/components/command-block.js +4 -3
  32. package/dist/ui/components/confirm-dialog.d.ts +13 -0
  33. package/dist/ui/components/confirm-dialog.js +113 -0
  34. package/dist/ui/components/cursor.js +1 -0
  35. package/dist/ui/components/dialog.d.ts +15 -0
  36. package/dist/ui/components/dialog.js +171 -0
  37. package/dist/ui/components/dropdown-menu.js +1 -0
  38. package/dist/ui/components/fit-text/index.js +1 -0
  39. package/dist/ui/components/graphs/bar-chart.js +1 -0
  40. package/dist/ui/components/graphs/index.js +1 -0
  41. package/dist/ui/components/graphs/line-chart.js +1 -0
  42. package/dist/ui/components/graphs/utils.js +1 -0
  43. package/dist/ui/components/grid/index.js +1 -0
  44. package/dist/ui/components/hover-bg.js +1 -0
  45. package/dist/ui/components/icons/arrow.js +1 -0
  46. package/dist/ui/components/icons/check.js +1 -0
  47. package/dist/ui/components/icons/chevron.js +1 -0
  48. package/dist/ui/components/icons/discord.js +1 -0
  49. package/dist/ui/components/icons/eye.js +1 -0
  50. package/dist/ui/components/icons/gear.js +1 -0
  51. package/dist/ui/components/icons/github.js +1 -0
  52. package/dist/ui/components/icons/hamburger.js +1 -0
  53. package/dist/ui/components/icons/heart.js +1 -0
  54. package/dist/ui/components/icons/index.js +1 -0
  55. package/dist/ui/components/icons/link.js +1 -0
  56. package/dist/ui/components/icons/minus.js +1 -0
  57. package/dist/ui/components/icons/search.js +1 -0
  58. package/dist/ui/components/image-distortion.js +1 -0
  59. package/dist/ui/components/input.d.ts +1 -0
  60. package/dist/ui/components/input.js +21 -0
  61. package/dist/ui/components/label.d.ts +1 -0
  62. package/dist/ui/components/label.js +18 -0
  63. package/dist/ui/components/leva-client.js +1 -0
  64. package/dist/ui/components/list-item.js +3 -2
  65. package/dist/ui/components/overlays/blend-modes.js +1 -0
  66. package/dist/ui/components/overlays/glitch.js +1 -0
  67. package/dist/ui/components/overlays/greys.js +1 -0
  68. package/dist/ui/components/overlays/index.js +1 -0
  69. package/dist/ui/components/overlays/lens-layers.js +1 -0
  70. package/dist/ui/components/overlays/lens.js +1 -0
  71. package/dist/ui/components/overlays/noise.js +1 -0
  72. package/dist/ui/components/overlays/vignette.js +1 -0
  73. package/dist/ui/components/poster.js +1 -0
  74. package/dist/ui/components/progress.js +1 -0
  75. package/dist/ui/components/scene-canvas.js +1 -0
  76. package/dist/ui/components/scramble.js +1 -0
  77. package/dist/ui/components/segmented.js +5 -4
  78. package/dist/ui/components/select.js +1 -0
  79. package/dist/ui/components/selection-switcher.js +1 -0
  80. package/dist/ui/components/separator.d.ts +5 -0
  81. package/dist/ui/components/separator.js +22 -0
  82. package/dist/ui/components/shader.js +1 -0
  83. package/dist/ui/components/socials.js +1 -0
  84. package/dist/ui/components/spinner.js +1 -0
  85. package/dist/ui/components/stats.js +2 -1
  86. package/dist/ui/components/switch.js +1 -0
  87. package/dist/ui/components/tabs.js +4 -3
  88. package/dist/ui/components/terminal-demo.js +2 -1
  89. package/dist/ui/components/theme-toggle.js +1 -0
  90. package/dist/ui/components/tier-card.js +2 -1
  91. package/dist/ui/components/toast.d.ts +8 -0
  92. package/dist/ui/components/toast.js +39 -0
  93. package/dist/ui/components/tv.js +1 -0
  94. package/dist/ui/components/typography/h1.js +1 -0
  95. package/dist/ui/components/typography/h2.js +1 -0
  96. package/dist/ui/components/typography/index.js +1 -0
  97. package/dist/ui/components/typography/legend.js +1 -0
  98. package/dist/ui/components/typography/small.js +1 -0
  99. package/dist/ui/components/watchlist.js +2 -1
  100. package/dist/ui/footer.js +1 -0
  101. package/dist/ui/globals.css +47 -3
  102. package/dist/ui/header.js +1 -0
  103. package/dist/ui/layout-wrapper.js +2 -1
  104. package/dist/utils/color.js +1 -0
  105. package/dist/utils/index.js +1 -0
  106. package/dist/utils/poly.js +1 -0
  107. package/package.json +5 -3
  108. package/src/assets/filler-bg0.webp +0 -0
  109. package/src/assets.d.ts +38 -0
  110. package/src/fonts/Collapse-Bold.woff2 +0 -0
  111. package/src/fonts/Collapse-BoldItalic.woff2 +0 -0
  112. package/src/fonts/Collapse-Italic.woff2 +0 -0
  113. package/src/fonts/Collapse-Light.woff2 +0 -0
  114. package/src/fonts/Collapse-LightItalic.woff2 +0 -0
  115. package/src/fonts/Collapse-Regular.woff2 +0 -0
  116. package/src/fonts/Collapse-Thin.woff2 +0 -0
  117. package/src/fonts/Collapse-ThinItalic.woff2 +0 -0
  118. package/src/fonts/Mondwest-Regular.woff2 +0 -0
  119. package/src/fonts/Neuebit-Bold.woff2 +0 -0
  120. package/src/fonts/RulesCompressed-Medium.woff2 +0 -0
  121. package/src/fonts/RulesCompressed-Regular.woff2 +0 -0
  122. package/src/fonts/RulesExpanded-Bold.woff2 +0 -0
  123. package/src/fonts/RulesExpanded-Regular.woff2 +0 -0
  124. package/src/fonts.ts +6 -0
  125. package/src/hooks/use-below-breakpoint.ts +21 -0
  126. package/src/hooks/use-capped-frame.ts +18 -0
  127. package/src/hooks/use-confirm-delete.ts +43 -0
  128. package/src/hooks/use-css-var-dims.ts +39 -0
  129. package/src/hooks/use-gpu-tier.ts +165 -0
  130. package/src/hooks/use-render-loop.ts +121 -0
  131. package/src/hooks/use-smooth-controls.ts +318 -0
  132. package/src/hooks/use-toast.ts +29 -0
  133. package/src/index.ts +130 -0
  134. package/src/ui/basic-page.tsx +34 -0
  135. package/src/ui/build.css +4 -0
  136. package/src/ui/components/animated-count.stories.tsx +67 -0
  137. package/src/ui/components/animated-count.tsx +168 -0
  138. package/src/ui/components/ascii.stories.tsx +30 -0
  139. package/src/ui/components/ascii.tsx +110 -0
  140. package/src/ui/components/badge.stories.tsx +31 -0
  141. package/src/ui/components/badge.tsx +60 -0
  142. package/src/ui/components/badges/nous-girl.tsx +52 -0
  143. package/src/ui/components/blend-mode.stories.tsx +33 -0
  144. package/src/ui/components/blend-mode.tsx +129 -0
  145. package/src/ui/components/blink.stories.tsx +32 -0
  146. package/src/ui/components/blink.tsx +21 -0
  147. package/src/ui/components/bottom-sheet.stories.tsx +43 -0
  148. package/src/ui/components/bottom-sheet.tsx +227 -0
  149. package/src/ui/components/button.stories.tsx +68 -0
  150. package/src/ui/components/button.tsx +170 -0
  151. package/src/ui/components/card.stories.tsx +63 -0
  152. package/src/ui/components/card.tsx +85 -0
  153. package/src/ui/components/checkbox.stories.tsx +113 -0
  154. package/src/ui/components/checkbox.tsx +36 -0
  155. package/src/ui/components/command-block.stories.tsx +52 -0
  156. package/src/ui/components/command-block.tsx +86 -0
  157. package/src/ui/components/confirm-dialog.stories.tsx +91 -0
  158. package/src/ui/components/confirm-dialog.tsx +130 -0
  159. package/src/ui/components/cursor.tsx +115 -0
  160. package/src/ui/components/dialog.stories.tsx +169 -0
  161. package/src/ui/components/dialog.tsx +177 -0
  162. package/src/ui/components/dropdown-menu.stories.tsx +52 -0
  163. package/src/ui/components/dropdown-menu.tsx +117 -0
  164. package/src/ui/components/fit-text/fit-text.css +42 -0
  165. package/src/ui/components/fit-text/index.stories.tsx +33 -0
  166. package/src/ui/components/fit-text/index.tsx +45 -0
  167. package/src/ui/components/forms.stories.tsx +173 -0
  168. package/src/ui/components/graphs/bar-chart.tsx +153 -0
  169. package/src/ui/components/graphs/index.stories.tsx +64 -0
  170. package/src/ui/components/graphs/index.tsx +4 -0
  171. package/src/ui/components/graphs/line-chart.tsx +213 -0
  172. package/src/ui/components/graphs/utils.tsx +265 -0
  173. package/src/ui/components/grid/grid.css +79 -0
  174. package/src/ui/components/grid/index.tsx +19 -0
  175. package/src/ui/components/hover-bg.stories.tsx +29 -0
  176. package/src/ui/components/hover-bg.tsx +15 -0
  177. package/src/ui/components/icons/arrow.tsx +42 -0
  178. package/src/ui/components/icons/check.tsx +14 -0
  179. package/src/ui/components/icons/chevron.tsx +45 -0
  180. package/src/ui/components/icons/discord.tsx +16 -0
  181. package/src/ui/components/icons/eye.tsx +12 -0
  182. package/src/ui/components/icons/gear.tsx +51 -0
  183. package/src/ui/components/icons/github.tsx +16 -0
  184. package/src/ui/components/icons/hamburger.tsx +52 -0
  185. package/src/ui/components/icons/heart.tsx +12 -0
  186. package/src/ui/components/icons/index.ts +12 -0
  187. package/src/ui/components/icons/link.tsx +14 -0
  188. package/src/ui/components/icons/minus.tsx +14 -0
  189. package/src/ui/components/icons/search.tsx +28 -0
  190. package/src/ui/components/image-distortion.stories.tsx +120 -0
  191. package/src/ui/components/image-distortion.tsx +498 -0
  192. package/src/ui/components/input.stories.tsx +39 -0
  193. package/src/ui/components/input.tsx +20 -0
  194. package/src/ui/components/label.stories.tsx +26 -0
  195. package/src/ui/components/label.tsx +16 -0
  196. package/src/ui/components/leva-client.tsx +14 -0
  197. package/src/ui/components/list-item.stories.tsx +83 -0
  198. package/src/ui/components/list-item.tsx +37 -0
  199. package/src/ui/components/overlays/blend-modes.ts +13 -0
  200. package/src/ui/components/overlays/glitch.tsx +243 -0
  201. package/src/ui/components/overlays/greys.tsx +386 -0
  202. package/src/ui/components/overlays/index.tsx +47 -0
  203. package/src/ui/components/overlays/lens-layers.tsx +119 -0
  204. package/src/ui/components/overlays/lens.ts +91 -0
  205. package/src/ui/components/overlays/noise.tsx +174 -0
  206. package/src/ui/components/overlays/vignette.tsx +60 -0
  207. package/src/ui/components/poster.stories.tsx +513 -0
  208. package/src/ui/components/poster.tsx +411 -0
  209. package/src/ui/components/progress.stories.tsx +48 -0
  210. package/src/ui/components/progress.tsx +56 -0
  211. package/src/ui/components/scene-canvas.tsx +254 -0
  212. package/src/ui/components/scramble.stories.tsx +49 -0
  213. package/src/ui/components/scramble.tsx +95 -0
  214. package/src/ui/components/segmented.stories.tsx +101 -0
  215. package/src/ui/components/segmented.tsx +81 -0
  216. package/src/ui/components/select.stories.tsx +88 -0
  217. package/src/ui/components/select.tsx +267 -0
  218. package/src/ui/components/selection-switcher.tsx +44 -0
  219. package/src/ui/components/separator.stories.tsx +33 -0
  220. package/src/ui/components/separator.tsx +24 -0
  221. package/src/ui/components/shader.tsx +83 -0
  222. package/src/ui/components/socials.tsx +42 -0
  223. package/src/ui/components/spinner.stories.tsx +101 -0
  224. package/src/ui/components/spinner.tsx +60 -0
  225. package/src/ui/components/stats.stories.tsx +24 -0
  226. package/src/ui/components/stats.tsx +53 -0
  227. package/src/ui/components/switch.stories.tsx +77 -0
  228. package/src/ui/components/switch.tsx +48 -0
  229. package/src/ui/components/tabs.stories.tsx +101 -0
  230. package/src/ui/components/tabs.tsx +66 -0
  231. package/src/ui/components/terminal-demo.stories.tsx +67 -0
  232. package/src/ui/components/terminal-demo.tsx +189 -0
  233. package/src/ui/components/theme-toggle.stories.tsx +47 -0
  234. package/src/ui/components/theme-toggle.tsx +66 -0
  235. package/src/ui/components/tier-card.stories.tsx +217 -0
  236. package/src/ui/components/tier-card.tsx +190 -0
  237. package/src/ui/components/toast.stories.tsx +55 -0
  238. package/src/ui/components/toast.tsx +49 -0
  239. package/src/ui/components/tv.stories.tsx +37 -0
  240. package/src/ui/components/tv.tsx +257 -0
  241. package/src/ui/components/typography/h1.tsx +18 -0
  242. package/src/ui/components/typography/h2.tsx +18 -0
  243. package/src/ui/components/typography/index.tsx +54 -0
  244. package/src/ui/components/typography/legend.tsx +24 -0
  245. package/src/ui/components/typography/small.tsx +11 -0
  246. package/src/ui/components/watchlist.stories.tsx +33 -0
  247. package/src/ui/components/watchlist.tsx +105 -0
  248. package/src/ui/fonts.css +63 -0
  249. package/src/ui/footer.tsx +111 -0
  250. package/src/ui/globals.css +395 -0
  251. package/src/ui/header.tsx +398 -0
  252. package/src/ui/layout-wrapper.tsx +11 -0
  253. package/src/utils/color.ts +21 -0
  254. package/src/utils/index.ts +62 -0
  255. package/src/utils/poly.ts +26 -0
  256. package/dist/ui/components/modal/index.d.ts +0 -8
  257. package/dist/ui/components/modal/index.js +0 -34
  258. package/dist/ui/components/modal/modal.css +0 -36
@@ -0,0 +1,267 @@
1
+ 'use client'
2
+
3
+ import {
4
+ Children,
5
+ isValidElement,
6
+ useCallback,
7
+ useEffect,
8
+ useMemo,
9
+ useRef,
10
+ useState,
11
+ type CSSProperties,
12
+ type KeyboardEvent,
13
+ type ReactElement,
14
+ type ReactNode
15
+ } from 'react'
16
+
17
+ import { cn } from '../../utils'
18
+
19
+ const TRIGGER_CN =
20
+ 'flex h-9 w-full items-center justify-between gap-2 ' +
21
+ 'border border-midground/15 bg-background/40 px-3 py-1 ' +
22
+ 'font-courier text-sm text-left text-midground transition-colors ' +
23
+ 'hover:border-midground/25 ' +
24
+ 'focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-midground/30 focus-visible:border-midground/30 ' +
25
+ 'disabled:cursor-not-allowed disabled:opacity-50 ' +
26
+ 'cursor-pointer'
27
+
28
+ const LISTBOX_CN =
29
+ 'absolute z-50 mt-1 w-full max-h-60 overflow-auto ' +
30
+ 'border border-midground/15 bg-background-base text-midground shadow-lg'
31
+
32
+ export function Select({
33
+ children,
34
+ className,
35
+ disabled,
36
+ id,
37
+ onValueChange,
38
+ placeholder,
39
+ style,
40
+ value
41
+ }: SelectProps) {
42
+ const [open, setOpen] = useState(false)
43
+ const [highlightedIndex, setHighlightedIndex] = useState(-1)
44
+ const containerRef = useRef<HTMLDivElement>(null)
45
+ const listRef = useRef<HTMLDivElement>(null)
46
+
47
+ const options = useMemo(() => collectOptions(children), [children])
48
+ const selected = options.find(o => o.value === value)
49
+ const displayLabel = selected?.label ?? placeholder ?? value ?? ''
50
+
51
+ const close = useCallback(() => {
52
+ setOpen(false)
53
+ setHighlightedIndex(-1)
54
+ }, [])
55
+
56
+ useEffect(() => {
57
+ if (!open) return
58
+ const ac = new AbortController()
59
+ document.addEventListener(
60
+ 'mousedown',
61
+ e => {
62
+ if (!containerRef.current?.contains(e.target as Node)) close()
63
+ },
64
+ { signal: ac.signal }
65
+ )
66
+ return () => ac.abort()
67
+ }, [open, close])
68
+
69
+ useEffect(() => {
70
+ if (!open || highlightedIndex < 0) return
71
+ const el = listRef.current?.children[highlightedIndex] as
72
+ | HTMLElement
73
+ | undefined
74
+ el?.scrollIntoView({ block: 'nearest' })
75
+ }, [open, highlightedIndex])
76
+
77
+ const handleKeyDown = (e: KeyboardEvent) => {
78
+ if (disabled) return
79
+ switch (e.key) {
80
+ case 'Enter':
81
+ case ' ':
82
+ e.preventDefault()
83
+ if (!open) {
84
+ setOpen(true)
85
+ setHighlightedIndex(options.findIndex(o => o.value === value))
86
+ } else if (highlightedIndex >= 0 && options[highlightedIndex]) {
87
+ onValueChange?.(options[highlightedIndex].value)
88
+ close()
89
+ }
90
+ break
91
+ case 'ArrowDown':
92
+ e.preventDefault()
93
+ if (!open) {
94
+ setOpen(true)
95
+ setHighlightedIndex(options.findIndex(o => o.value === value))
96
+ } else {
97
+ setHighlightedIndex(i => Math.min(i + 1, options.length - 1))
98
+ }
99
+ break
100
+ case 'ArrowUp':
101
+ e.preventDefault()
102
+ if (open) setHighlightedIndex(i => Math.max(i - 1, 0))
103
+ break
104
+ case 'Home':
105
+ if (open) {
106
+ e.preventDefault()
107
+ setHighlightedIndex(0)
108
+ }
109
+ break
110
+ case 'End':
111
+ if (open) {
112
+ e.preventDefault()
113
+ setHighlightedIndex(options.length - 1)
114
+ }
115
+ break
116
+ case 'Escape':
117
+ e.preventDefault()
118
+ close()
119
+ break
120
+ }
121
+ }
122
+
123
+ return (
124
+ <div
125
+ className={cn('relative', className)}
126
+ id={id}
127
+ ref={containerRef}
128
+ style={style}
129
+ >
130
+ <button
131
+ aria-expanded={open}
132
+ aria-haspopup="listbox"
133
+ className={TRIGGER_CN}
134
+ disabled={disabled}
135
+ onClick={() => !disabled && setOpen(o => !o)}
136
+ onKeyDown={handleKeyDown}
137
+ role="combobox"
138
+ type="button"
139
+ >
140
+ <span className={cn('truncate', !selected && 'text-midground/50')}>
141
+ {displayLabel}
142
+ </span>
143
+
144
+ <ChevronDownGlyph
145
+ className={cn(
146
+ 'size-3 shrink-0 text-midground/60 transition-transform',
147
+ open && 'rotate-180'
148
+ )}
149
+ />
150
+ </button>
151
+
152
+ {open && (
153
+ <div className={LISTBOX_CN} ref={listRef} role="listbox">
154
+ {options.map((opt, i) => {
155
+ const isSelected = opt.value === value
156
+ const isHighlighted = i === highlightedIndex
157
+
158
+ return (
159
+ <div
160
+ aria-selected={isSelected}
161
+ className={cn(
162
+ 'flex cursor-pointer items-center gap-2 px-3 py-2',
163
+ 'font-courier text-sm transition-colors',
164
+ isHighlighted && 'bg-midground/10',
165
+ isSelected ? 'text-midground' : 'text-midground/70'
166
+ )}
167
+ key={opt.value}
168
+ onClick={() => {
169
+ onValueChange?.(opt.value)
170
+ close()
171
+ }}
172
+ onMouseEnter={() => setHighlightedIndex(i)}
173
+ role="option"
174
+ >
175
+ <CheckGlyph
176
+ className={cn(
177
+ 'size-3 shrink-0',
178
+ isSelected ? 'opacity-100' : 'opacity-0'
179
+ )}
180
+ />
181
+ <span className="truncate">{opt.label}</span>
182
+ </div>
183
+ )
184
+ })}
185
+ </div>
186
+ )}
187
+ </div>
188
+ )
189
+ }
190
+
191
+ // Marker component — `Select` reads `value`/`children` from its tree.
192
+ // Renders nothing on its own.
193
+ export function SelectOption(_props: SelectOptionProps) {
194
+ return null
195
+ }
196
+
197
+ const ChevronDownGlyph = ({ className }: { className?: string }) => (
198
+ <svg
199
+ aria-hidden
200
+ className={className}
201
+ fill="none"
202
+ stroke="currentColor"
203
+ strokeLinecap="square"
204
+ strokeWidth={1.5}
205
+ viewBox="0 0 12 12"
206
+ >
207
+ <path d="M2.5 4.5 6 8l3.5-3.5" />
208
+ </svg>
209
+ )
210
+
211
+ const CheckGlyph = ({ className }: { className?: string }) => (
212
+ <svg
213
+ aria-hidden
214
+ className={className}
215
+ fill="none"
216
+ stroke="currentColor"
217
+ strokeLinecap="square"
218
+ strokeWidth={1.5}
219
+ viewBox="0 0 12 12"
220
+ >
221
+ <path d="m2.5 6.5 2.5 2.5L9.5 3.5" />
222
+ </svg>
223
+ )
224
+
225
+ function collectOptions(children: ReactNode): SelectOptionData[] {
226
+ const out: SelectOptionData[] = []
227
+ Children.forEach(children, child => {
228
+ if (!isValidElement(child)) return
229
+ const el = child as ReactElement<{
230
+ children?: ReactNode
231
+ value?: unknown
232
+ }>
233
+ if (el.props.value !== undefined) {
234
+ out.push({
235
+ label:
236
+ typeof el.props.children === 'string'
237
+ ? el.props.children
238
+ : String(el.props.value),
239
+ value: String(el.props.value)
240
+ })
241
+ } else if (el.props.children) {
242
+ out.push(...collectOptions(el.props.children))
243
+ }
244
+ })
245
+ return out
246
+ }
247
+
248
+ interface SelectOptionData {
249
+ label: string
250
+ value: string
251
+ }
252
+
253
+ interface SelectOptionProps {
254
+ children: ReactNode
255
+ value: string
256
+ }
257
+
258
+ interface SelectProps {
259
+ children?: ReactNode
260
+ className?: string
261
+ disabled?: boolean
262
+ id?: string
263
+ onValueChange?: (value: string) => void
264
+ placeholder?: string
265
+ style?: CSSProperties
266
+ value?: string
267
+ }
@@ -0,0 +1,44 @@
1
+ 'use client'
2
+
3
+ import { useEffect } from 'react'
4
+
5
+ const colors = [
6
+ 'oklch(85% 0.12 330)',
7
+ 'oklch(85% 0.12 300)',
8
+ 'oklch(85% 0.12 270)',
9
+ 'oklch(85% 0.12 230)',
10
+ 'oklch(85% 0.12 180)',
11
+ 'oklch(85% 0.12 150)',
12
+ 'oklch(85% 0.12 120)',
13
+ 'oklch(85% 0.12 90)',
14
+ 'oklch(85% 0.12 60)',
15
+ 'oklch(85% 0.12 30)',
16
+ 'oklch(88% 0.10 80)'
17
+ ] as const
18
+
19
+ export function SelectionSwitcher() {
20
+ useEffect(() => {
21
+ const ac = new AbortController()
22
+ const { signal } = ac
23
+
24
+ let idx = 0
25
+
26
+ const cycle = () =>
27
+ requestAnimationFrame(() =>
28
+ document.documentElement.style.setProperty(
29
+ '--selection-bg',
30
+ colors[(idx = (idx + 1) % colors.length)]
31
+ )
32
+ )
33
+
34
+ const onKey = (e: KeyboardEvent) =>
35
+ e.key.toLowerCase() === 'a' && (e.metaKey || e.ctrlKey) && cycle()
36
+
37
+ document.addEventListener('selectstart', cycle, { signal })
38
+ document.addEventListener('keydown', onKey, { signal })
39
+
40
+ return () => ac.abort()
41
+ }, [])
42
+
43
+ return null
44
+ }
@@ -0,0 +1,33 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+
3
+ import { Separator } from './separator'
4
+ import { Small } from './typography/small'
5
+
6
+ const meta: Meta<typeof Separator> = {
7
+ component: Separator,
8
+ title: 'Components/Layout/Separator'
9
+ }
10
+
11
+ export default meta
12
+
13
+ type Story = StoryObj<typeof Separator>
14
+
15
+ export const Horizontal: Story = {
16
+ render: () => (
17
+ <div className="grid w-64 gap-3">
18
+ <Small className="opacity-60 uppercase tracking-wider">Section A</Small>
19
+ <Separator />
20
+ <Small className="opacity-60 uppercase tracking-wider">Section B</Small>
21
+ </div>
22
+ )
23
+ }
24
+
25
+ export const Vertical: Story = {
26
+ render: () => (
27
+ <div className="flex h-8 items-center gap-3">
28
+ <Small className="opacity-60 uppercase tracking-wider">Left</Small>
29
+ <Separator orientation="vertical" />
30
+ <Small className="opacity-60 uppercase tracking-wider">Right</Small>
31
+ </div>
32
+ )
33
+ }
@@ -0,0 +1,24 @@
1
+ import { cn } from '../../utils'
2
+
3
+ export function Separator({
4
+ className,
5
+ orientation = 'horizontal',
6
+ ...props
7
+ }: SeparatorProps) {
8
+ return (
9
+ <div
10
+ aria-orientation={orientation}
11
+ role="separator"
12
+ className={cn(
13
+ 'shrink-0 bg-midground/15',
14
+ orientation === 'horizontal' ? 'h-px w-full' : 'h-full w-px',
15
+ className
16
+ )}
17
+ {...props}
18
+ />
19
+ )
20
+ }
21
+
22
+ interface SeparatorProps extends React.HTMLAttributes<HTMLDivElement> {
23
+ orientation?: 'horizontal' | 'vertical'
24
+ }
@@ -0,0 +1,83 @@
1
+ 'use client'
2
+
3
+ import { type ThreeElements, useThree } from '@react-three/fiber'
4
+ import { useEffect, useRef } from 'react'
5
+ import type { ReactNode } from 'react'
6
+ import * as THREE from 'three'
7
+
8
+ import { useCappedFrame } from '../../hooks/use-capped-frame'
9
+
10
+ const defaultUniforms = {
11
+ uResolution: new THREE.Uniform(new THREE.Vector4()),
12
+ uTime: new THREE.Uniform(0)
13
+ }
14
+
15
+ export function Shader({
16
+ children,
17
+ defines,
18
+ depthTest,
19
+ fragmentShader,
20
+ uniforms,
21
+ vertexShader,
22
+ ...props
23
+ }: ShaderProps) {
24
+ const invalidate = useThree(st => st.invalidate)
25
+ const { size, viewport } = useThree()
26
+
27
+ const isMobile = size.width < 1024
28
+
29
+ const uniformsRef = useRef({
30
+ ...defaultUniforms,
31
+ ...(uniforms ?? {})
32
+ })
33
+
34
+ useCappedFrame(
35
+ ({ clock }) => {
36
+ const w = size.width * viewport.dpr
37
+ const h = size.height * viewport.dpr
38
+
39
+ uniformsRef.current.uTime.value = clock.getElapsedTime()
40
+ uniformsRef.current.uResolution.value.copy(
41
+ new THREE.Vector4(w, h, w / h, viewport.dpr)
42
+ )
43
+ },
44
+ isMobile ? 0 : 80
45
+ )
46
+
47
+ useEffect(
48
+ () => void (uniforms && Object.assign(uniformsRef.current, uniforms)),
49
+ [uniforms]
50
+ )
51
+
52
+ useEffect(() => void (isMobile && invalidate(80)), [invalidate, isMobile])
53
+
54
+ const materialProps = {
55
+ defines: defines ?? {},
56
+ depthTest,
57
+ fragmentShader,
58
+ side: THREE.DoubleSide,
59
+ transparent: true,
60
+ uniforms: uniformsRef.current,
61
+ vertexShader
62
+ } satisfies ThreeElements['shaderMaterial']
63
+
64
+ if (typeof children === 'function') {
65
+ return children(<shaderMaterial {...materialProps} />)
66
+ }
67
+
68
+ return (
69
+ <mesh {...props}>
70
+ {children ?? <planeGeometry />}
71
+ <shaderMaterial {...materialProps} />
72
+ </mesh>
73
+ )
74
+ }
75
+
76
+ interface ShaderProps
77
+ extends Omit<ThreeElements['mesh'], 'children'>,
78
+ Pick<
79
+ ThreeElements['shaderMaterial'],
80
+ 'defines' | 'depthTest' | 'fragmentShader' | 'uniforms' | 'vertexShader'
81
+ > {
82
+ children?: ((material: ReactNode) => ReactNode) | ReactNode
83
+ }
@@ -0,0 +1,42 @@
1
+ import { cn } from '../../utils'
2
+
3
+ export function Socials({ className, items, onNavigate, ...rest }: SocialsProps) {
4
+ return (
5
+ <div className={cn('flex items-center gap-3', className)} {...rest}>
6
+ {items.map(({ external = true, href, icon: Icon, label, onClick }) => (
7
+ <a
8
+ className="opacity-60 transition-opacity hover:opacity-100"
9
+ href={href}
10
+ key={label}
11
+ onClick={e => {
12
+ onClick?.(e)
13
+ onNavigate?.()
14
+ }}
15
+ rel={external ? 'noopener noreferrer' : undefined}
16
+ target={external ? '_blank' : undefined}
17
+ title={label}
18
+ >
19
+ <Icon />
20
+ </a>
21
+ ))}
22
+ </div>
23
+ )
24
+ }
25
+
26
+ export interface SocialLink {
27
+ external?: boolean
28
+ href: string
29
+ icon: React.ComponentType<{ className?: string }>
30
+ label: string
31
+ onClick?: React.MouseEventHandler
32
+ }
33
+
34
+ interface SocialsProps extends React.HTMLAttributes<HTMLDivElement> {
35
+ items: SocialLink[]
36
+ /**
37
+ * Called *in addition* to each link's `onClick` after a click — useful in
38
+ * mobile drawer / dialog contexts where clicking a link should also close
39
+ * the surrounding overlay.
40
+ */
41
+ onNavigate?: () => void
42
+ }
@@ -0,0 +1,101 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+
3
+ import { Spinner } from './spinner'
4
+ import { Small } from './typography/small'
5
+
6
+ const NAMES = [
7
+ 'braille',
8
+ 'braillewave',
9
+ 'dna',
10
+ 'scan',
11
+ 'rain',
12
+ 'scanline',
13
+ 'pulse',
14
+ 'snake',
15
+ 'sparkle',
16
+ 'cascade',
17
+ 'columns',
18
+ 'orbit',
19
+ 'breathe',
20
+ 'waverows',
21
+ 'checkerboard',
22
+ 'helix',
23
+ 'fillsweep',
24
+ 'diagswipe'
25
+ ] as const
26
+
27
+ const meta: Meta<typeof Spinner> = {
28
+ component: Spinner,
29
+ title: 'Components/Feedback/Spinner'
30
+ }
31
+
32
+ export default meta
33
+
34
+ type Story = StoryObj<typeof Spinner>
35
+
36
+ export const Playground: Story = { render: () => <Spinner /> }
37
+
38
+ export const InlineWithText: Story = {
39
+ render: () => (
40
+ <div className="flex items-center gap-2 text-sm text-midground/70">
41
+ <Spinner /> Loading model info…
42
+ </div>
43
+ )
44
+ }
45
+
46
+ export const Sizes: Story = {
47
+ render: () => (
48
+ <div className="flex items-end gap-6">
49
+ <div className="flex flex-col items-center gap-2">
50
+ <Spinner className="text-xs" />
51
+ <Small className="opacity-50">xs</Small>
52
+ </div>
53
+
54
+ <div className="flex flex-col items-center gap-2">
55
+ <Spinner className="text-sm" />
56
+ <Small className="opacity-50">sm</Small>
57
+ </div>
58
+
59
+ <div className="flex flex-col items-center gap-2">
60
+ <Spinner className="text-base" />
61
+ <Small className="opacity-50">base</Small>
62
+ </div>
63
+
64
+ <div className="flex flex-col items-center gap-2">
65
+ <Spinner className="text-2xl" />
66
+ <Small className="opacity-50">2xl</Small>
67
+ </div>
68
+
69
+ <div className="flex flex-col items-center gap-2">
70
+ <Spinner className="text-4xl" />
71
+ <Small className="opacity-50">4xl</Small>
72
+ </div>
73
+ </div>
74
+ )
75
+ }
76
+
77
+ export const Tones: Story = {
78
+ render: () => (
79
+ <div className="flex items-center gap-4 text-base">
80
+ <Spinner />
81
+ <Spinner className="text-warning" />
82
+ <Spinner className="text-success" />
83
+ <Spinner className="text-destructive" />
84
+ <Spinner className="text-midground/40" />
85
+ </div>
86
+ )
87
+ }
88
+
89
+ export const Gallery: Story = {
90
+ render: () => (
91
+ <div className="grid grid-cols-3 gap-x-8 gap-y-3 text-base">
92
+ {NAMES.map(name => (
93
+ <div className="flex items-center gap-3" key={name}>
94
+ <Spinner name={name} />
95
+
96
+ <Small className="font-mono opacity-60">{name}</Small>
97
+ </div>
98
+ ))}
99
+ </div>
100
+ )
101
+ }
@@ -0,0 +1,60 @@
1
+ 'use client'
2
+
3
+ import {
4
+ type CSSProperties,
5
+ type HTMLAttributes,
6
+ useEffect,
7
+ useState
8
+ } from 'react'
9
+ import spinners, { type BrailleSpinnerName } from 'unicode-animations'
10
+
11
+ import { cn } from '../../utils'
12
+
13
+ /**
14
+ * Braille unicode spinner. Renders the active frame of a `unicode-animations`
15
+ * sequence inside an inline `<span>`, advancing on the spinner's own interval.
16
+ *
17
+ * Inherits font color and font size from its parent — apply Tailwind utilities
18
+ * (e.g. `text-warning`, `text-base`) via `className` to style.
19
+ *
20
+ * Decorative by default. Pass `aria-label` (and optionally `role="status"`) when
21
+ * the spinner has no surrounding loading text and screen readers need to know
22
+ * something is loading.
23
+ */
24
+ export function Spinner({
25
+ className,
26
+ name = 'braille',
27
+ style,
28
+ ...props
29
+ }: SpinnerProps) {
30
+ const [frame, setFrame] = useState(0)
31
+ const animation = spinners[name]
32
+
33
+ useEffect(() => {
34
+ const id = setInterval(
35
+ () => setFrame(f => (f + 1) % animation.frames.length),
36
+ animation.interval
37
+ )
38
+ return () => clearInterval(id)
39
+ }, [animation.frames.length, animation.interval])
40
+
41
+ return (
42
+ <span
43
+ aria-hidden={props['aria-label'] ? undefined : true}
44
+ className={cn(
45
+ 'font-mono inline-block leading-none tabular-nums',
46
+ className
47
+ )}
48
+ style={style}
49
+ {...props}
50
+ >
51
+ {animation.frames[frame]}
52
+ </span>
53
+ )
54
+ }
55
+
56
+ interface SpinnerProps extends HTMLAttributes<HTMLSpanElement> {
57
+ className?: string
58
+ name?: BrailleSpinnerName
59
+ style?: CSSProperties
60
+ }
@@ -0,0 +1,24 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+
3
+ import { Stats } from './stats'
4
+
5
+ const meta = {
6
+ component: Stats,
7
+ title: 'Components/Feedback/Stats'
8
+ } satisfies Meta<typeof Stats>
9
+
10
+ export default meta
11
+
12
+ type Story = StoryObj<typeof meta>
13
+
14
+ export const Default: Story = {
15
+ args: {
16
+ items: [
17
+ { label: 'Parameters', value: '36.2b' },
18
+ { label: 'Checkpoint', value: 'Scratch' },
19
+ { label: 'HfAuto', value: 'Auto' },
20
+ { label: 'Type', value: 'Text' },
21
+ { label: 'Loss', value: '0.56' }
22
+ ]
23
+ }
24
+ }