@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,254 @@
1
+ 'use client'
2
+
3
+ import { Canvas, useFrame, useThree } from '@react-three/fiber'
4
+ import {
5
+ Suspense,
6
+ useEffect,
7
+ useLayoutEffect,
8
+ useMemo,
9
+ useRef,
10
+ useState
11
+ } from 'react'
12
+ import * as THREE from 'three'
13
+
14
+ const GL = {
15
+ alpha: true,
16
+ antialias: true,
17
+ depth: true,
18
+ outputColorSpace: 'srgb',
19
+ powerPreference: 'high-performance',
20
+ stencil: false
21
+ } as const
22
+
23
+ const tmp = {
24
+ camDir: new THREE.Vector3(),
25
+ hit: new THREE.Vector3(),
26
+ ndc: new THREE.Vector2(),
27
+ origin: new THREE.Vector3(0, 0, 0),
28
+ plane: new THREE.Plane(),
29
+ ray: new THREE.Raycaster()
30
+ }
31
+
32
+ type Bounds = { height: number; pageX: number; pageY: number; width: number }
33
+
34
+ function useBounds(target: HTMLElement | null) {
35
+ const bounds = useRef<Bounds | null>(null)
36
+
37
+ useLayoutEffect(() => {
38
+ if (!target) {
39
+ return
40
+ }
41
+
42
+ const measure = () => {
43
+ const b = target.getBoundingClientRect()
44
+ bounds.current = {
45
+ height: b.height,
46
+ pageX: b.left + window.scrollX,
47
+ pageY: b.top + window.scrollY,
48
+ width: b.width
49
+ }
50
+ }
51
+
52
+ measure()
53
+
54
+ const ro = new ResizeObserver(measure)
55
+ ro.observe(target)
56
+ ro.observe(document.body)
57
+ window.addEventListener('resize', measure, { passive: true })
58
+
59
+ return () => {
60
+ ro.disconnect()
61
+ window.removeEventListener('resize', measure)
62
+ }
63
+ }, [target])
64
+
65
+ return bounds
66
+ }
67
+
68
+ function PositionedGroup({
69
+ baseZoom,
70
+ bounds,
71
+ children
72
+ }: React.PropsWithChildren<{
73
+ baseZoom: number
74
+ bounds: React.RefObject<Bounds | null>
75
+ }>) {
76
+ const ref = useRef<THREE.Group>(null)
77
+ const { camera, size, viewport } = useThree()
78
+
79
+ useFrame(() => {
80
+ const g = ref.current
81
+ const b = bounds.current
82
+
83
+ if (!g || !b) {
84
+ return
85
+ }
86
+
87
+ const left = b.pageX - window.scrollX
88
+ const top = b.pageY - window.scrollY
89
+
90
+ tmp.ndc.set(
91
+ ((left + b.width / 2) / size.width) * 2 - 1,
92
+ 1 - ((top + b.height / 2) / size.height) * 2
93
+ )
94
+
95
+ camera.getWorldDirection(tmp.camDir)
96
+ tmp.plane.setFromNormalAndCoplanarPoint(tmp.camDir, tmp.origin)
97
+ tmp.ray.setFromCamera(tmp.ndc, camera)
98
+
99
+ const hit = tmp.ray.ray.intersectPlane(tmp.plane, tmp.hit)
100
+
101
+ if (hit) {
102
+ g.position.copy(hit)
103
+ }
104
+
105
+ const zoom = (camera as THREE.Camera & { zoom?: number }).zoom ?? 1
106
+
107
+ g.scale.setScalar(
108
+ Math.min(
109
+ (b.width / size.width) * viewport.width,
110
+ (b.height / size.height) * viewport.height
111
+ ) * (baseZoom > 0 ? zoom / baseZoom : 1)
112
+ )
113
+ })
114
+
115
+ return <group ref={ref}>{children}</group>
116
+ }
117
+
118
+ export function SceneCanvas({
119
+ camera,
120
+ children,
121
+ className,
122
+ contained,
123
+ frameloop = 'always',
124
+ noEvents,
125
+ style
126
+ }: SceneCanvasProps) {
127
+ const [container, setContainer] = useState<HTMLDivElement | null>(null)
128
+ const baseZoom = camera?.zoom ?? 150
129
+
130
+ const bounds = useBounds(
131
+ contained ? (container?.parentElement ?? null) : null
132
+ )
133
+
134
+ useEffect(() => {
135
+ const el = contained && !noEvents ? container : null
136
+
137
+ if (!el) {
138
+ return
139
+ }
140
+
141
+ const lock = () => (document.body.style.userSelect = 'none')
142
+ const unlock = () => (document.body.style.userSelect = '')
143
+
144
+ el.addEventListener('pointerdown', lock)
145
+ window.addEventListener('pointerup', unlock)
146
+
147
+ return () => {
148
+ el.removeEventListener('pointerdown', lock)
149
+ window.removeEventListener('pointerup', unlock)
150
+ }
151
+ }, [container, contained, noEvents])
152
+
153
+ // Pause the R3F render loop when the tab is hidden. Even on
154
+ // `frameloop="always"` scenes we don't want the GPU running while the
155
+ // user can't see anything — this is the dominant fix for the "fans
156
+ // crank up after hours of idle" symptom.
157
+ const [pageHidden, setPageHidden] = useState(
158
+ typeof document !== 'undefined' && document.hidden
159
+ )
160
+
161
+ useEffect(() => {
162
+ const onVisibility = () => setPageHidden(document.hidden)
163
+ document.addEventListener('visibilitychange', onVisibility)
164
+
165
+ return () => document.removeEventListener('visibilitychange', onVisibility)
166
+ }, [])
167
+
168
+ const effectiveFrameloop = pageHidden ? 'never' : frameloop
169
+
170
+ const cam = useMemo(
171
+ () => ({
172
+ far: camera?.far ?? 100,
173
+ near: camera?.near ?? -100,
174
+ position: camera?.position ?? ([0, 0, 10] as [number, number, number]),
175
+ zoom: baseZoom * (contained ? 1 : 2)
176
+ }),
177
+ [baseZoom, camera, contained]
178
+ )
179
+
180
+ const canvas = (
181
+ <Canvas
182
+ camera={cam}
183
+ className={className}
184
+ dpr={[1, 2]}
185
+ eventPrefix={contained ? 'client' : 'offset'}
186
+ eventSource={contained ? (container ?? undefined) : undefined}
187
+ frameloop={effectiveFrameloop}
188
+ gl={GL}
189
+ orthographic
190
+ style={
191
+ contained
192
+ ? {
193
+ height: '100dvh',
194
+ inset: 0,
195
+ pointerEvents: 'none',
196
+ position: 'fixed',
197
+ width: '100dvw',
198
+ zIndex: 0,
199
+ ...style
200
+ }
201
+ : { height: '100%', width: '100%', ...style }
202
+ }
203
+ >
204
+ {contained ? (
205
+ <PositionedGroup baseZoom={baseZoom} bounds={bounds}>
206
+ {children()}
207
+ </PositionedGroup>
208
+ ) : (
209
+ children()
210
+ )}
211
+ </Canvas>
212
+ )
213
+
214
+ return contained ? (
215
+ <Suspense>
216
+ <div
217
+ ref={setContainer}
218
+ style={{
219
+ height: '100%',
220
+ inset: 0,
221
+ pointerEvents: noEvents ? 'none' : 'auto',
222
+ position: 'absolute',
223
+ width: '100%',
224
+ zIndex: 1
225
+ }}
226
+ />
227
+ {canvas}
228
+ </Suspense>
229
+ ) : (
230
+ canvas
231
+ )
232
+ }
233
+
234
+ interface SceneCanvasProps {
235
+ camera?: {
236
+ far?: number
237
+ near?: number
238
+ position?: [number, number, number]
239
+ zoom?: number
240
+ }
241
+ children: () => React.ReactNode
242
+ className?: string
243
+ contained?: boolean
244
+ /**
245
+ * R3F frame-loop mode. Defaults to `'always'` for backwards
246
+ * compatibility, but `'demand'` is strongly preferred for static
247
+ * scenes (use `invalidate()` from `useThree` to request frames). The
248
+ * canvas additionally pauses (forces `'never'`) while the document
249
+ * is hidden, regardless of this setting.
250
+ */
251
+ frameloop?: 'always' | 'demand' | 'never'
252
+ noEvents?: boolean
253
+ style?: React.CSSProperties
254
+ }
@@ -0,0 +1,49 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { useRef } from 'react'
3
+
4
+ import { Scramble } from './scramble'
5
+ import { Typography } from './typography'
6
+ import { Small } from './typography/small'
7
+
8
+ const meta: Meta<typeof Scramble> = {
9
+ component: Scramble,
10
+ title: 'Components/Effects/Scramble'
11
+ }
12
+
13
+ export default meta
14
+
15
+ type Story = StoryObj<typeof Scramble>
16
+
17
+ export const HoverToScramble: Story = {
18
+ render: () => {
19
+ const ref = useRef<HTMLDivElement>(null)
20
+
21
+ return (
22
+ <div className="flex flex-col gap-2" ref={ref}>
23
+ <Small className="opacity-40">Hover the container</Small>
24
+
25
+ <Typography as="div" className="text-lg" mono>
26
+ <Scramble target={ref}>HOVER TO SCRAMBLE THIS TEXT</Scramble>
27
+ </Typography>
28
+ </div>
29
+ )
30
+ }
31
+ }
32
+
33
+ export const Tuned: Story = {
34
+ render: () => {
35
+ const ref = useRef<HTMLDivElement>(null)
36
+
37
+ return (
38
+ <div className="flex flex-col gap-2" ref={ref}>
39
+ <Small className="opacity-40">dur=1200, spread=2</Small>
40
+
41
+ <Typography as="div" className="text-lg" mono>
42
+ <Scramble dur={1200} spread={2} target={ref}>
43
+ FASTER WAVE, TIGHTER SPREAD
44
+ </Scramble>
45
+ </Typography>
46
+ </div>
47
+ )
48
+ }
49
+ }
@@ -0,0 +1,95 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useRef, useState } from 'react'
4
+ import type { RefObject } from 'react'
5
+
6
+ const CHARS = '.,·-─~+:;=*π""┐┌┘┴┬╗╔╝╚╬╠╣╩╦║░▒▓█▄▀▌▐■!?&#$@0123456789*'
7
+
8
+ export function Scramble({
9
+ children,
10
+ dur = 666,
11
+ spread = 1,
12
+ target
13
+ }: ScrambleProps) {
14
+ const text = String(children)
15
+ const len = text.length
16
+ const [display, setDisplay] = useState(text)
17
+ const frame = useRef<null | number>(null)
18
+ const waves = useRef<{ pos: number; time: number }[]>([])
19
+
20
+ useEffect(() => {
21
+ const el = target?.current
22
+
23
+ if (!el) {
24
+ return
25
+ }
26
+
27
+ const animate = () => {
28
+ const t = Date.now()
29
+ waves.current = waves.current.filter(w => t - w.time < dur)
30
+
31
+ if (!waves.current.length) {
32
+ setDisplay(text)
33
+ frame.current = null
34
+
35
+ return
36
+ }
37
+
38
+ setDisplay(
39
+ text
40
+ .split('')
41
+ .map((c, i) => {
42
+ if (c === ' ') {
43
+ return c
44
+ }
45
+
46
+ for (const w of waves.current) {
47
+ const age = t - w.time
48
+
49
+ const rad =
50
+ (Math.min(age / dur, 1) *
51
+ (Math.max(w.pos, len - w.pos - 1) + 5)) /
52
+ spread
53
+
54
+ const dist = Math.abs(i - w.pos)
55
+ const int = rad - dist
56
+
57
+ if (dist <= rad && int > 0 && int <= 3) {
58
+ return CHARS[(dist * 3 + ((age / 40) | 0)) % CHARS.length]
59
+ }
60
+ }
61
+
62
+ return c
63
+ })
64
+ .join('')
65
+ )
66
+
67
+ frame.current = requestAnimationFrame(animate)
68
+ }
69
+
70
+ const onEnter = () => {
71
+ waves.current.push({ pos: len >> 1, time: Date.now() })
72
+ frame.current ??= requestAnimationFrame(animate)
73
+ }
74
+
75
+ el.addEventListener('mouseenter', onEnter)
76
+
77
+ return () => {
78
+ el.removeEventListener('mouseenter', onEnter)
79
+ frame.current && cancelAnimationFrame(frame.current)
80
+ }
81
+ }, [target, text, len, dur, spread])
82
+
83
+ useEffect(() => {
84
+ setDisplay(text)
85
+ }, [text])
86
+
87
+ return <>{display}</>
88
+ }
89
+
90
+ interface ScrambleProps {
91
+ children: string
92
+ dur?: number
93
+ spread?: number
94
+ target?: RefObject<HTMLElement | null>
95
+ }
@@ -0,0 +1,101 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { useState } from 'react'
3
+
4
+ import {
5
+ FilterGroup,
6
+ Segmented
7
+ } from './segmented'
8
+
9
+ type Density = 'compact' | 'comfortable' | 'spacious'
10
+ type Severity = 'all' | 'info' | 'warn' | 'error'
11
+
12
+ const DENSITY_OPTIONS: { label: string; value: Density }[] = [
13
+ { label: 'Compact', value: 'compact' },
14
+ { label: 'Comfortable', value: 'comfortable' },
15
+ { label: 'Spacious', value: 'spacious' }
16
+ ]
17
+
18
+ const SEVERITY_OPTIONS: { label: string; value: Severity }[] = [
19
+ { label: 'All', value: 'all' },
20
+ { label: 'Info', value: 'info' },
21
+ { label: 'Warn', value: 'warn' },
22
+ { label: 'Error', value: 'error' }
23
+ ]
24
+
25
+ function Demo({ size }: { size?: 'md' | 'sm' }) {
26
+ const [value, setValue] = useState<Density>('comfortable')
27
+
28
+ return (
29
+ <Segmented
30
+ onChange={setValue}
31
+ options={DENSITY_OPTIONS}
32
+ size={size}
33
+ value={value}
34
+ />
35
+ )
36
+ }
37
+
38
+ const meta: Meta<typeof Segmented> = {
39
+ component: Segmented,
40
+ title: 'Components/Forms/Segmented'
41
+ }
42
+
43
+ export default meta
44
+
45
+ type Story = StoryObj<typeof Segmented>
46
+
47
+ export const Playground: Story = { render: () => <Demo /> }
48
+
49
+ export const Medium: Story = { render: () => <Demo size="md" /> }
50
+
51
+ export const TwoOptions: Story = {
52
+ render: () => {
53
+ function TwoOptionsDemo() {
54
+ const [value, setValue] = useState<'on' | 'off'>('on')
55
+
56
+ return (
57
+ <Segmented
58
+ onChange={setValue}
59
+ options={[
60
+ { label: 'On', value: 'on' },
61
+ { label: 'Off', value: 'off' }
62
+ ]}
63
+ value={value}
64
+ />
65
+ )
66
+ }
67
+
68
+ return <TwoOptionsDemo />
69
+ }
70
+ }
71
+
72
+ export const InFilterGroup: Story = {
73
+ render: () => {
74
+ function FilterDemo() {
75
+ const [severity, setSeverity] = useState<Severity>('all')
76
+ const [density, setDensity] = useState<Density>('comfortable')
77
+
78
+ return (
79
+ <div className="flex flex-wrap items-center gap-6">
80
+ <FilterGroup label="Severity">
81
+ <Segmented
82
+ onChange={setSeverity}
83
+ options={SEVERITY_OPTIONS}
84
+ value={severity}
85
+ />
86
+ </FilterGroup>
87
+
88
+ <FilterGroup label="Density">
89
+ <Segmented
90
+ onChange={setDensity}
91
+ options={DENSITY_OPTIONS}
92
+ value={density}
93
+ />
94
+ </FilterGroup>
95
+ </div>
96
+ )
97
+ }
98
+
99
+ return <FilterDemo />
100
+ }
101
+ }
@@ -0,0 +1,81 @@
1
+ 'use client'
2
+
3
+ import { type ReactNode } from 'react'
4
+
5
+ import { cn } from '../../utils'
6
+
7
+ export function Segmented<T extends string>({
8
+ className,
9
+ onChange,
10
+ options,
11
+ size = 'sm',
12
+ value
13
+ }: SegmentedProps<T>) {
14
+ return (
15
+ <div
16
+ className={cn(
17
+ 'inline-flex border border-midground/15 bg-background/30',
18
+ className
19
+ )}
20
+ role="radiogroup"
21
+ >
22
+ {options.map(opt => {
23
+ const active = opt.value === value
24
+
25
+ return (
26
+ <button
27
+ aria-checked={active}
28
+ className={cn(
29
+ 'font-mondwest text-display tracking-[0.1em]',
30
+ 'transition-colors cursor-pointer whitespace-nowrap',
31
+ 'border-r border-midground/15 last:border-r-0',
32
+ 'focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-midground/30',
33
+ size === 'sm' && 'h-7 px-2.5 text-xs',
34
+ size === 'md' && 'h-8 px-3 text-xs',
35
+ active
36
+ ? 'bg-midground text-background'
37
+ : 'text-text-secondary hover:bg-midground/10 hover:text-midground'
38
+ )}
39
+ key={opt.value}
40
+ onClick={() => onChange(opt.value)}
41
+ role="radio"
42
+ type="button"
43
+ >
44
+ {opt.label}
45
+ </button>
46
+ )
47
+ })}
48
+ </div>
49
+ )
50
+ }
51
+
52
+ export function FilterGroup({ children, className, label }: FilterGroupProps) {
53
+ return (
54
+ <div className={cn('flex items-center gap-2', className)}>
55
+ <span className="font-mondwest text-display text-xs tracking-[0.12em] text-text-tertiary">
56
+ {label}
57
+ </span>
58
+
59
+ {children}
60
+ </div>
61
+ )
62
+ }
63
+
64
+ interface FilterGroupProps {
65
+ children: ReactNode
66
+ className?: string
67
+ label: string
68
+ }
69
+
70
+ interface SegmentedOption<T extends string> {
71
+ label: string
72
+ value: T
73
+ }
74
+
75
+ interface SegmentedProps<T extends string> {
76
+ className?: string
77
+ onChange: (value: T) => void
78
+ options: SegmentedOption<T>[]
79
+ size?: 'md' | 'sm'
80
+ value: T
81
+ }
@@ -0,0 +1,88 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { useState } from 'react'
3
+
4
+ import { Select, SelectOption } from './select'
5
+ import { Small } from './typography/small'
6
+
7
+ const PROVIDERS = [
8
+ { label: 'OpenAI', value: 'openai' },
9
+ { label: 'Anthropic', value: 'anthropic' },
10
+ { label: 'Google', value: 'google' },
11
+ { label: 'Mistral', value: 'mistral' },
12
+ { label: 'xAI', value: 'xai' }
13
+ ]
14
+
15
+ function Demo({
16
+ disabled,
17
+ placeholder
18
+ }: {
19
+ disabled?: boolean
20
+ placeholder?: string
21
+ }) {
22
+ const [value, setValue] = useState<string>('anthropic')
23
+
24
+ return (
25
+ <div className="w-72">
26
+ <Select
27
+ disabled={disabled}
28
+ onValueChange={setValue}
29
+ placeholder={placeholder}
30
+ value={value}
31
+ >
32
+ {PROVIDERS.map(p => (
33
+ <SelectOption key={p.value} value={p.value}>
34
+ {p.label}
35
+ </SelectOption>
36
+ ))}
37
+ </Select>
38
+ </div>
39
+ )
40
+ }
41
+
42
+ const meta: Meta<typeof Select> = {
43
+ component: Select,
44
+ title: 'Components/Forms/Select'
45
+ }
46
+
47
+ export default meta
48
+
49
+ type Story = StoryObj<typeof Select>
50
+
51
+ export const Playground: Story = { render: () => <Demo /> }
52
+
53
+ export const Disabled: Story = { render: () => <Demo disabled /> }
54
+
55
+ export const Empty: Story = {
56
+ render: () => {
57
+ function EmptyDemo() {
58
+ const [value, setValue] = useState<string>('')
59
+
60
+ return (
61
+ <div className="w-72">
62
+ <Select
63
+ onValueChange={setValue}
64
+ placeholder="Choose a provider…"
65
+ value={value}
66
+ >
67
+ {PROVIDERS.map(p => (
68
+ <SelectOption key={p.value} value={p.value}>
69
+ {p.label}
70
+ </SelectOption>
71
+ ))}
72
+ </Select>
73
+ </div>
74
+ )
75
+ }
76
+
77
+ return <EmptyDemo />
78
+ }
79
+ }
80
+
81
+ export const NextToLabel: Story = {
82
+ render: () => (
83
+ <div className="grid w-72 gap-2">
84
+ <Small className="opacity-60 uppercase tracking-wider">Provider</Small>
85
+ <Demo />
86
+ </div>
87
+ )
88
+ }