@nous-research/ui 0.14.2 → 0.16.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 (216) hide show
  1. package/CHANGELOG.md +227 -0
  2. package/README.md +24 -4
  3. package/dist/fonts.js +1 -0
  4. package/dist/hooks/use-capped-frame.js +1 -0
  5. package/dist/hooks/use-css-var-dims.js +1 -0
  6. package/dist/hooks/use-gpu-tier.js +1 -0
  7. package/dist/hooks/use-render-loop.js +1 -0
  8. package/dist/hooks/use-smooth-controls.js +1 -0
  9. package/dist/index.js +1 -0
  10. package/dist/ui/basic-page.js +1 -0
  11. package/dist/ui/components/animated-count.js +1 -0
  12. package/dist/ui/components/ascii.js +1 -0
  13. package/dist/ui/components/badge.js +2 -1
  14. package/dist/ui/components/badges/nous-girl.js +1 -0
  15. package/dist/ui/components/blend-mode.js +1 -0
  16. package/dist/ui/components/blink.js +1 -0
  17. package/dist/ui/components/button.js +2 -1
  18. package/dist/ui/components/checkbox.js +1 -0
  19. package/dist/ui/components/command-block.js +4 -3
  20. package/dist/ui/components/cursor.js +1 -0
  21. package/dist/ui/components/dropdown-menu.js +1 -0
  22. package/dist/ui/components/fit-text/index.js +1 -0
  23. package/dist/ui/components/graphs/bar-chart.js +1 -0
  24. package/dist/ui/components/graphs/index.js +1 -0
  25. package/dist/ui/components/graphs/line-chart.js +1 -0
  26. package/dist/ui/components/graphs/utils.js +1 -0
  27. package/dist/ui/components/grid/index.js +1 -0
  28. package/dist/ui/components/hover-bg.js +1 -0
  29. package/dist/ui/components/icons/arrow.js +1 -0
  30. package/dist/ui/components/icons/check.js +1 -0
  31. package/dist/ui/components/icons/chevron.js +1 -0
  32. package/dist/ui/components/icons/discord.js +1 -0
  33. package/dist/ui/components/icons/eye.js +1 -0
  34. package/dist/ui/components/icons/gear.js +1 -0
  35. package/dist/ui/components/icons/github.js +1 -0
  36. package/dist/ui/components/icons/hamburger.js +1 -0
  37. package/dist/ui/components/icons/heart.js +1 -0
  38. package/dist/ui/components/icons/index.js +1 -0
  39. package/dist/ui/components/icons/link.js +1 -0
  40. package/dist/ui/components/icons/minus.js +1 -0
  41. package/dist/ui/components/icons/search.js +1 -0
  42. package/dist/ui/components/image-distortion.js +1 -0
  43. package/dist/ui/components/leva-client.js +1 -0
  44. package/dist/ui/components/list-item.js +3 -2
  45. package/dist/ui/components/modal/index.js +1 -0
  46. package/dist/ui/components/modal/modal.css +1 -1
  47. package/dist/ui/components/overlays/blend-modes.js +1 -0
  48. package/dist/ui/components/overlays/glitch.js +1 -0
  49. package/dist/ui/components/overlays/greys.js +1 -0
  50. package/dist/ui/components/overlays/index.js +1 -0
  51. package/dist/ui/components/overlays/lens-layers.js +1 -0
  52. package/dist/ui/components/overlays/lens.js +1 -0
  53. package/dist/ui/components/overlays/noise.js +1 -0
  54. package/dist/ui/components/overlays/vignette.js +1 -0
  55. package/dist/ui/components/poster.js +1 -0
  56. package/dist/ui/components/progress.js +1 -0
  57. package/dist/ui/components/scene-canvas.js +1 -0
  58. package/dist/ui/components/scramble.js +1 -0
  59. package/dist/ui/components/segmented.js +5 -4
  60. package/dist/ui/components/select.js +1 -0
  61. package/dist/ui/components/selection-switcher.js +1 -0
  62. package/dist/ui/components/shader.js +1 -0
  63. package/dist/ui/components/socials.js +1 -0
  64. package/dist/ui/components/spinner.js +1 -0
  65. package/dist/ui/components/stats.js +2 -1
  66. package/dist/ui/components/switch.js +1 -0
  67. package/dist/ui/components/tabs.js +4 -3
  68. package/dist/ui/components/terminal-demo.js +2 -1
  69. package/dist/ui/components/theme-toggle.js +1 -0
  70. package/dist/ui/components/tier-card.js +2 -1
  71. package/dist/ui/components/tv.js +1 -0
  72. package/dist/ui/components/typography/h1.js +1 -0
  73. package/dist/ui/components/typography/h2.js +1 -0
  74. package/dist/ui/components/typography/index.js +1 -0
  75. package/dist/ui/components/typography/legend.js +1 -0
  76. package/dist/ui/components/typography/small.js +1 -0
  77. package/dist/ui/components/watchlist.js +2 -1
  78. package/dist/ui/footer.js +1 -0
  79. package/dist/ui/globals.css +33 -1
  80. package/dist/ui/header.js +1 -0
  81. package/dist/ui/layout-wrapper.js +2 -1
  82. package/dist/utils/color.js +1 -0
  83. package/dist/utils/index.js +1 -0
  84. package/dist/utils/poly.js +1 -0
  85. package/package.json +4 -2
  86. package/src/assets/filler-bg0.webp +0 -0
  87. package/src/assets.d.ts +38 -0
  88. package/src/fonts/Collapse-Bold.woff2 +0 -0
  89. package/src/fonts/Collapse-BoldItalic.woff2 +0 -0
  90. package/src/fonts/Collapse-Italic.woff2 +0 -0
  91. package/src/fonts/Collapse-Light.woff2 +0 -0
  92. package/src/fonts/Collapse-LightItalic.woff2 +0 -0
  93. package/src/fonts/Collapse-Regular.woff2 +0 -0
  94. package/src/fonts/Collapse-Thin.woff2 +0 -0
  95. package/src/fonts/Collapse-ThinItalic.woff2 +0 -0
  96. package/src/fonts/Mondwest-Regular.woff2 +0 -0
  97. package/src/fonts/Neuebit-Bold.woff2 +0 -0
  98. package/src/fonts/RulesCompressed-Medium.woff2 +0 -0
  99. package/src/fonts/RulesCompressed-Regular.woff2 +0 -0
  100. package/src/fonts/RulesExpanded-Bold.woff2 +0 -0
  101. package/src/fonts/RulesExpanded-Regular.woff2 +0 -0
  102. package/src/fonts.ts +6 -0
  103. package/src/hooks/use-capped-frame.ts +18 -0
  104. package/src/hooks/use-css-var-dims.ts +39 -0
  105. package/src/hooks/use-gpu-tier.ts +165 -0
  106. package/src/hooks/use-render-loop.ts +121 -0
  107. package/src/hooks/use-smooth-controls.ts +318 -0
  108. package/src/index.ts +109 -0
  109. package/src/ui/basic-page.tsx +34 -0
  110. package/src/ui/build.css +4 -0
  111. package/src/ui/components/animated-count.stories.tsx +67 -0
  112. package/src/ui/components/animated-count.tsx +168 -0
  113. package/src/ui/components/ascii.stories.tsx +30 -0
  114. package/src/ui/components/ascii.tsx +110 -0
  115. package/src/ui/components/badge.stories.tsx +31 -0
  116. package/src/ui/components/badge.tsx +60 -0
  117. package/src/ui/components/badges/nous-girl.tsx +52 -0
  118. package/src/ui/components/blend-mode.stories.tsx +33 -0
  119. package/src/ui/components/blend-mode.tsx +129 -0
  120. package/src/ui/components/blink.stories.tsx +32 -0
  121. package/src/ui/components/blink.tsx +21 -0
  122. package/src/ui/components/button.stories.tsx +68 -0
  123. package/src/ui/components/button.tsx +170 -0
  124. package/src/ui/components/checkbox.stories.tsx +113 -0
  125. package/src/ui/components/checkbox.tsx +36 -0
  126. package/src/ui/components/command-block.stories.tsx +52 -0
  127. package/src/ui/components/command-block.tsx +86 -0
  128. package/src/ui/components/cursor.tsx +115 -0
  129. package/src/ui/components/dropdown-menu.stories.tsx +52 -0
  130. package/src/ui/components/dropdown-menu.tsx +117 -0
  131. package/src/ui/components/fit-text/fit-text.css +42 -0
  132. package/src/ui/components/fit-text/index.stories.tsx +33 -0
  133. package/src/ui/components/fit-text/index.tsx +45 -0
  134. package/src/ui/components/graphs/bar-chart.tsx +153 -0
  135. package/src/ui/components/graphs/index.stories.tsx +64 -0
  136. package/src/ui/components/graphs/index.tsx +4 -0
  137. package/src/ui/components/graphs/line-chart.tsx +213 -0
  138. package/src/ui/components/graphs/utils.tsx +265 -0
  139. package/src/ui/components/grid/grid.css +79 -0
  140. package/src/ui/components/grid/index.tsx +19 -0
  141. package/src/ui/components/hover-bg.stories.tsx +29 -0
  142. package/src/ui/components/hover-bg.tsx +15 -0
  143. package/src/ui/components/icons/arrow.tsx +42 -0
  144. package/src/ui/components/icons/check.tsx +14 -0
  145. package/src/ui/components/icons/chevron.tsx +45 -0
  146. package/src/ui/components/icons/discord.tsx +16 -0
  147. package/src/ui/components/icons/eye.tsx +12 -0
  148. package/src/ui/components/icons/gear.tsx +51 -0
  149. package/src/ui/components/icons/github.tsx +16 -0
  150. package/src/ui/components/icons/hamburger.tsx +52 -0
  151. package/src/ui/components/icons/heart.tsx +12 -0
  152. package/src/ui/components/icons/index.ts +12 -0
  153. package/src/ui/components/icons/link.tsx +14 -0
  154. package/src/ui/components/icons/minus.tsx +14 -0
  155. package/src/ui/components/icons/search.tsx +28 -0
  156. package/src/ui/components/image-distortion.stories.tsx +120 -0
  157. package/src/ui/components/image-distortion.tsx +498 -0
  158. package/src/ui/components/leva-client.tsx +14 -0
  159. package/src/ui/components/list-item.stories.tsx +83 -0
  160. package/src/ui/components/list-item.tsx +37 -0
  161. package/src/ui/components/modal/index.stories.tsx +46 -0
  162. package/src/ui/components/modal/index.tsx +48 -0
  163. package/src/ui/components/modal/modal.css +36 -0
  164. package/src/ui/components/overlays/blend-modes.ts +13 -0
  165. package/src/ui/components/overlays/glitch.tsx +243 -0
  166. package/src/ui/components/overlays/greys.tsx +386 -0
  167. package/src/ui/components/overlays/index.tsx +47 -0
  168. package/src/ui/components/overlays/lens-layers.tsx +119 -0
  169. package/src/ui/components/overlays/lens.ts +91 -0
  170. package/src/ui/components/overlays/noise.tsx +174 -0
  171. package/src/ui/components/overlays/vignette.tsx +60 -0
  172. package/src/ui/components/poster.stories.tsx +513 -0
  173. package/src/ui/components/poster.tsx +411 -0
  174. package/src/ui/components/progress.stories.tsx +48 -0
  175. package/src/ui/components/progress.tsx +56 -0
  176. package/src/ui/components/scene-canvas.tsx +254 -0
  177. package/src/ui/components/scramble.stories.tsx +49 -0
  178. package/src/ui/components/scramble.tsx +95 -0
  179. package/src/ui/components/segmented.stories.tsx +101 -0
  180. package/src/ui/components/segmented.tsx +81 -0
  181. package/src/ui/components/select.stories.tsx +88 -0
  182. package/src/ui/components/select.tsx +267 -0
  183. package/src/ui/components/selection-switcher.tsx +44 -0
  184. package/src/ui/components/shader.tsx +83 -0
  185. package/src/ui/components/socials.tsx +42 -0
  186. package/src/ui/components/spinner.stories.tsx +101 -0
  187. package/src/ui/components/spinner.tsx +60 -0
  188. package/src/ui/components/stats.stories.tsx +24 -0
  189. package/src/ui/components/stats.tsx +53 -0
  190. package/src/ui/components/switch.stories.tsx +77 -0
  191. package/src/ui/components/switch.tsx +48 -0
  192. package/src/ui/components/tabs.stories.tsx +101 -0
  193. package/src/ui/components/tabs.tsx +66 -0
  194. package/src/ui/components/terminal-demo.stories.tsx +67 -0
  195. package/src/ui/components/terminal-demo.tsx +189 -0
  196. package/src/ui/components/theme-toggle.stories.tsx +47 -0
  197. package/src/ui/components/theme-toggle.tsx +66 -0
  198. package/src/ui/components/tier-card.stories.tsx +217 -0
  199. package/src/ui/components/tier-card.tsx +190 -0
  200. package/src/ui/components/tv.stories.tsx +37 -0
  201. package/src/ui/components/tv.tsx +257 -0
  202. package/src/ui/components/typography/h1.tsx +18 -0
  203. package/src/ui/components/typography/h2.tsx +18 -0
  204. package/src/ui/components/typography/index.tsx +54 -0
  205. package/src/ui/components/typography/legend.tsx +24 -0
  206. package/src/ui/components/typography/small.tsx +11 -0
  207. package/src/ui/components/watchlist.stories.tsx +33 -0
  208. package/src/ui/components/watchlist.tsx +105 -0
  209. package/src/ui/fonts.css +63 -0
  210. package/src/ui/footer.tsx +111 -0
  211. package/src/ui/globals.css +383 -0
  212. package/src/ui/header.tsx +398 -0
  213. package/src/ui/layout-wrapper.tsx +11 -0
  214. package/src/utils/color.ts +21 -0
  215. package/src/utils/index.ts +62 -0
  216. package/src/utils/poly.ts +26 -0
@@ -0,0 +1,318 @@
1
+ 'use client'
2
+
3
+ import gsap from 'gsap'
4
+ import { buttonGroup, useControls } from 'leva'
5
+ import { atom, type WritableAtom } from 'nanostores'
6
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
7
+
8
+ const atomRegistry = new Map<string, Map<string, WritableAtom<any>>>()
9
+
10
+ const val = (v: any) =>
11
+ v && typeof v === 'object' && 'value' in v ? v.value : v
12
+
13
+ const isHex = (v: any) =>
14
+ /color/i.test(v?.type) || /^#[0-9a-f]{3,8}$/i.test(val(v))
15
+
16
+ const randHex = () =>
17
+ `#${Math.floor(Math.random() * 0xffffff)
18
+ .toString(16)
19
+ .padStart(6, '0')}`
20
+
21
+ const randNum = (v: any) =>
22
+ typeof v === 'object' && ('min' in v || 'max' in v)
23
+ ? gsap.utils.random(v.min ?? 0, v.max ?? 1, v.step ?? 0.01)
24
+ : gsap.utils.random(0, 1)
25
+
26
+ export function useSmoothControls<T extends Record<string, any>>(
27
+ label: string,
28
+ initialArgs: T,
29
+ options?: UseSmoothControlsOptions,
30
+ dependencies?: Parameters<typeof useControls>[3]
31
+ ) {
32
+ type R = { [K in keyof T]: T[K] extends { value: infer V } ? V : never }
33
+
34
+ const entries = useMemo(
35
+ () => Object.entries(initialArgs ?? {}),
36
+ [initialArgs]
37
+ )
38
+
39
+ const values = useMemo(
40
+ () => entries.filter(([, v]) => !/button|folder/i.test(v?.type)),
41
+ [entries]
42
+ )
43
+
44
+ // Tracks whether this component instance has mounted yet. When a remount
45
+ // happens (e.g. Storybook changing a `key` prop to force a lens reset), we
46
+ // want the module-scoped atoms to be reseeded from the new `initialArgs` so
47
+ // the first paint reflects the newly-selected preset — not leftover values
48
+ // from the previous mount.
49
+ const mountedRef = useRef(false)
50
+
51
+ const atoms = useMemo(() => {
52
+ const map = atomRegistry.get(label) ?? new Map<string, WritableAtom<any>>()
53
+
54
+ if (!atomRegistry.has(label)) {
55
+ atomRegistry.set(label, map)
56
+ }
57
+
58
+ const freshMount = !mountedRef.current
59
+
60
+ entries.forEach(([k, v]) => {
61
+ if (v?.schema) {
62
+ Object.keys(v.schema).forEach(sk => {
63
+ const key = `${k}.${sk}`
64
+
65
+ if (!map.has(key)) {
66
+ map.set(key, atom(val(v.schema[sk])))
67
+ } else if (freshMount) {
68
+ map.get(key)!.set(val(v.schema[sk]))
69
+ }
70
+ })
71
+ } else if (!map.has(k)) {
72
+ map.set(k, atom(val(v)))
73
+ } else if (freshMount) {
74
+ map.get(k)!.set(val(v))
75
+ }
76
+ })
77
+
78
+ return map
79
+ }, [label, entries])
80
+
81
+ useEffect(() => {
82
+ mountedRef.current = true
83
+ }, [])
84
+
85
+ const hydrate = useCallback(
86
+ () =>
87
+ Object.fromEntries(
88
+ entries.flatMap(([k, v]) =>
89
+ v?.schema
90
+ ? Object.entries(v.schema).map(([k0, v0]: [string, any]) => [
91
+ k0,
92
+ atoms.get(`${k}.${k0}`)?.get() ?? val(v0)
93
+ ])
94
+ : [[k, atoms.get(k)?.get() ?? val(v)]]
95
+ )
96
+ ) as R,
97
+ [entries, atoms]
98
+ )
99
+
100
+ const [args, update] = useState<R>(hydrate)
101
+ const setRef = useRef<((values: Partial<R>) => void) | null>(null)
102
+ const atomVals = useRef<Record<string, any>>({})
103
+ const fromAtom = useRef(false)
104
+ const fromControl = useRef<Set<string>>(new Set())
105
+
106
+ useEffect(() => {
107
+ if (Object.keys(args).length !== Object.keys(initialArgs).length) {
108
+ update(hydrate)
109
+ }
110
+ // eslint-disable-next-line react-hooks/exhaustive-deps
111
+ }, [initialArgs, args])
112
+
113
+ useEffect(() => {
114
+ if (!setRef.current) {
115
+ return
116
+ }
117
+
118
+ const unsubs: Array<() => void> = []
119
+ let ready = false
120
+ const initTimeout = setTimeout(() => (ready = true), 100)
121
+
122
+ const subscribe = (fullKey: string, updateFn: (v: any) => void) => {
123
+ const a = atoms.get(fullKey)
124
+
125
+ if (!a) {
126
+ return
127
+ }
128
+
129
+ unsubs.push(
130
+ a.subscribe(v => {
131
+ const prev = atomVals.current[fullKey]
132
+ atomVals.current[fullKey] = v
133
+
134
+ if (
135
+ setRef.current &&
136
+ ready &&
137
+ prev !== v &&
138
+ !fromControl.current.has(fullKey)
139
+ ) {
140
+ fromAtom.current = true
141
+
142
+ try {
143
+ updateFn(v)
144
+ } catch {
145
+ //
146
+ }
147
+
148
+ setTimeout(() => (fromAtom.current = false), 0)
149
+ }
150
+ })
151
+ )
152
+
153
+ atomVals.current[fullKey] = a.get()
154
+ }
155
+
156
+ entries.forEach(([k, v]) => {
157
+ if (v?.schema) {
158
+ Object.keys(v.schema).forEach(sk => {
159
+ subscribe(`${k}.${sk}`, v => {
160
+ try {
161
+ setRef.current!({
162
+ [k]: { ...((args[k] as any) ?? {}), [sk]: v }
163
+ } as Partial<R>)
164
+ } catch {
165
+ //
166
+ }
167
+
168
+ update(st => ({
169
+ ...st,
170
+ [k]: { ...((st[k] as any) ?? {}), [sk]: v }
171
+ }))
172
+ })
173
+ })
174
+ } else {
175
+ subscribe(k, v => {
176
+ try {
177
+ setRef.current!({ [k]: v } as Partial<R>)
178
+ } catch {
179
+ //
180
+ }
181
+
182
+ update(st => ({ ...st, [k]: v }))
183
+ })
184
+ }
185
+ })
186
+
187
+ return () => {
188
+ clearTimeout(initTimeout)
189
+ unsubs.forEach(fn => fn())
190
+ }
191
+ }, [label, entries, atoms, args])
192
+
193
+ const onChange =
194
+ (k: string, orig?: (e: any, k0?: string) => void) =>
195
+ (e: any, k0?: string) => {
196
+ if (fromAtom.current) {
197
+ return orig?.(e, k0)
198
+ }
199
+
200
+ const key = k0?.split('.')?.pop() ?? k
201
+ const fullKey = k0 ?? k
202
+ const a = atoms.get(fullKey)
203
+
204
+ fromControl.current.add(fullKey)
205
+
206
+ const sync = (v: any) => {
207
+ update(st => ({ ...st, [key]: v }))
208
+ a?.set(v)
209
+ orig?.(v, k0)
210
+ }
211
+
212
+ if (typeof e === 'number' && args[key] !== e) {
213
+ gsap.to(args, {
214
+ duration: options?.duration ?? 0.35,
215
+ ease: 'circ.out',
216
+ [key]: e,
217
+ onComplete: () => void fromControl.current.delete(fullKey),
218
+ onUpdate: () => {
219
+ fromControl.current.add(fullKey)
220
+ sync(args[key])
221
+ setTimeout(() => fromControl.current.delete(fullKey), 0)
222
+ }
223
+ })
224
+ } else {
225
+ sync(e)
226
+ setTimeout(() => fromControl.current.delete(fullKey), 0)
227
+ }
228
+ }
229
+
230
+ const [, set] = useControls(
231
+ label,
232
+ () => ({
233
+ ...Object.fromEntries(
234
+ entries.map(([k, v]) =>
235
+ v?.schema
236
+ ? [
237
+ k,
238
+ {
239
+ ...v,
240
+ schema: Object.fromEntries(
241
+ Object.entries(v.schema).map(([sk, sv]: [string, any]) => [
242
+ sk,
243
+ { ...sv!, onChange: onChange(k, sv?.onChange) }
244
+ ])
245
+ )
246
+ }
247
+ ]
248
+ : [k, { ...v, onChange: onChange(k, v?.onChange) }]
249
+ )
250
+ ),
251
+
252
+ ' ': buttonGroup({
253
+ flatten: () =>
254
+ void set(Object.fromEntries(values.map(([k]) => [k, 0]))),
255
+ randomize: () => {
256
+ set(
257
+ Object.fromEntries(
258
+ values.map(([k, v]) => [k, isHex(v) ? randHex() : randNum(v)])
259
+ )
260
+ )
261
+ options?.onRandomize?.()
262
+ },
263
+ reset: () => {
264
+ set(Object.fromEntries(values.map(([k, v]) => [k, val(v)])))
265
+ options?.onReset?.()
266
+ }
267
+ })
268
+ }),
269
+ { collapsed: true, ...options },
270
+ dependencies ?? []
271
+ )
272
+
273
+ setRef.current = set
274
+
275
+ return args
276
+ }
277
+
278
+ export const getControlAtom = <T = any>(
279
+ label: string,
280
+ key: string
281
+ ): undefined | WritableAtom<T> =>
282
+ atomRegistry.get(label)?.get(key) as undefined | WritableAtom<T>
283
+
284
+ export const setControlValue = <T = any>(
285
+ label: string,
286
+ key: string,
287
+ value: T,
288
+ options?: { animate?: boolean; duration?: number }
289
+ ) => {
290
+ const a = getControlAtom<T>(label, key)
291
+
292
+ if (!a) {
293
+ return
294
+ }
295
+
296
+ if (
297
+ options?.animate &&
298
+ typeof value === 'number' &&
299
+ typeof a.get() === 'number'
300
+ ) {
301
+ const t = { v: a.get() }
302
+
303
+ gsap.to(t, {
304
+ duration: options.duration ?? 0.35,
305
+ ease: 'circ.out',
306
+ onUpdate: () => a.set(t.v),
307
+ v: value
308
+ })
309
+ } else {
310
+ a.set(value)
311
+ }
312
+ }
313
+
314
+ type UseSmoothControlsOptions = Parameters<typeof useControls>[2] & {
315
+ duration?: number
316
+ onRandomize?: () => void
317
+ onReset?: () => void
318
+ }
package/src/index.ts ADDED
@@ -0,0 +1,109 @@
1
+ export { AnimatedCount, useAnimatedCount } from './ui/components/animated-count'
2
+ export { AsciiSkeleton, Scramble as AsciiScramble } from './ui/components/ascii'
3
+ export { Badge } from './ui/components/badge'
4
+ export { NousGirlBadge } from './ui/components/badges/nous-girl'
5
+ export { BlendMode, useBlendMode, withBlendMode } from './ui/components/blend-mode'
6
+ export type { BlendModeProps } from './ui/components/blend-mode'
7
+ export { Blink } from './ui/components/blink'
8
+ export { Button } from './ui/components/button'
9
+ export { Checkbox } from './ui/components/checkbox'
10
+ export { CommandBlock, CopyButton } from './ui/components/command-block'
11
+ export { Cursor } from './ui/components/cursor'
12
+ export { DropdownMenu } from './ui/components/dropdown-menu'
13
+ export { FitText } from './ui/components/fit-text'
14
+ export { BarChart, LineChart } from './ui/components/graphs'
15
+ export { Cell, Grid } from './ui/components/grid'
16
+ export { HoverBg } from './ui/components/hover-bg'
17
+ export * as Icons from './ui/components/icons'
18
+ export { DiscordIcon } from './ui/components/icons/discord'
19
+ export { GitHubIcon } from './ui/components/icons/github'
20
+ export { ImageDistortion } from './ui/components/image-distortion'
21
+ export type { AutoPlayPattern } from './ui/components/image-distortion'
22
+ export { LevaClient } from './ui/components/leva-client'
23
+ export { ListItem } from './ui/components/list-item'
24
+ export { Modal } from './ui/components/modal'
25
+ export { FilterGroup, Segmented } from './ui/components/segmented'
26
+ export { Switch } from './ui/components/switch'
27
+ export { Tabs, TabsList, TabsTrigger } from './ui/components/tabs'
28
+ export { Poster } from './ui/components/poster'
29
+ export type {
30
+ PosterAspect,
31
+ PosterProps,
32
+ PosterVariant
33
+ } from './ui/components/poster'
34
+ export {
35
+ applyLens,
36
+ BLEND_MODES,
37
+ LENSES,
38
+ LENS_0,
39
+ LENS_5I,
40
+ lens0,
41
+ lens5i,
42
+ toggleLens,
43
+ $lightMode
44
+ } from './ui/components/overlays'
45
+ export {
46
+ Glitch,
47
+ Greys,
48
+ Lens,
49
+ Noise,
50
+ Overlays,
51
+ Vignette
52
+ } from './ui/components/overlays'
53
+ export type { LensPreset } from './ui/components/overlays'
54
+ export { Progress } from './ui/components/progress'
55
+ export { SceneCanvas } from './ui/components/scene-canvas'
56
+ export { Scramble } from './ui/components/scramble'
57
+ export { Select, SelectOption } from './ui/components/select'
58
+ export { SelectionSwitcher } from './ui/components/selection-switcher'
59
+ export { Spinner } from './ui/components/spinner'
60
+ export { Stats } from './ui/components/stats'
61
+ export { TerminalDemo } from './ui/components/terminal-demo'
62
+ export type { TerminalDemoStep } from './ui/components/terminal-demo'
63
+ export { ThemeToggle } from './ui/components/theme-toggle'
64
+ export { TierCard } from './ui/components/tier-card'
65
+ export type { TierCardPrice, TierCardProps } from './ui/components/tier-card'
66
+ export { TV } from './ui/components/tv'
67
+ export { Watchlist } from './ui/components/watchlist'
68
+
69
+ export { Typography } from './ui/components/typography'
70
+ export type { TypographyProps } from './ui/components/typography'
71
+ export { H1 } from './ui/components/typography/h1'
72
+ export { H2 } from './ui/components/typography/h2'
73
+ export { Legend } from './ui/components/typography/legend'
74
+ export { Small } from './ui/components/typography/small'
75
+
76
+ export { BasicPage } from './ui/basic-page'
77
+ export { Header } from './ui/header'
78
+ export type { HeaderLink, HeaderProps, HeaderSocial } from './ui/header'
79
+ export { Footer } from './ui/footer'
80
+ export type {
81
+ FooterGroup,
82
+ FooterLink,
83
+ FooterProps
84
+ } from './ui/footer'
85
+ export { Socials } from './ui/components/socials'
86
+ export type { SocialLink } from './ui/components/socials'
87
+ export { LayoutWrapper } from './ui/layout-wrapper'
88
+
89
+ export {
90
+ FONT_SANS,
91
+ FONT_MONO,
92
+ FONT_RULES_COMPRESSED,
93
+ FONT_RULES_EXPANDED,
94
+ FONT_MONDWEST
95
+ } from './fonts'
96
+
97
+ export { cn, clamp, smoothstep, hexToVec3, truncate, stripWpStyles } from './utils'
98
+ export { polyRef } from './utils'
99
+ export type { PolyComponent, PolyProps, PolyRef } from './utils'
100
+ export { hexToRgb, rgbToHex, colorDodge, colorMix } from './utils/color'
101
+
102
+ export { useCappedFrame } from './hooks/use-capped-frame'
103
+ export { useCssVarDims } from './hooks/use-css-var-dims'
104
+ export { $gpuTier, useGpuTier } from './hooks/use-gpu-tier'
105
+ export {
106
+ useSmoothControls,
107
+ getControlAtom,
108
+ setControlValue
109
+ } from './hooks/use-smooth-controls'
@@ -0,0 +1,34 @@
1
+ import type { ReactNode } from 'react'
2
+
3
+ import { Cell, Grid } from './components/grid'
4
+ import { Progress } from './components/progress'
5
+ import { H1 } from './components/typography/h1'
6
+ import { Small } from './components/typography/small'
7
+
8
+ export function BasicPage({ children, subtitle, title }: BasicPageProps) {
9
+ return (
10
+ <>
11
+ <Grid>
12
+ <Cell>
13
+ <Progress value={0} />
14
+ </Cell>
15
+ </Grid>
16
+
17
+ <Grid className="lg:grid-cols-[max-content_1fr]">
18
+ <Cell className="-order-1">
19
+ <div className="sticky top-4 flex flex-col gap-4">
20
+ {title ? <H1 className="-mb-2 pr-10 opacity-90">{title}</H1> : null}
21
+ {subtitle ? <Small className="opacity-60">{subtitle}</Small> : null}
22
+ </div>
23
+ </Cell>
24
+
25
+ <Cell className="post bg-current/3">{children}</Cell>
26
+ </Grid>
27
+ </>
28
+ )
29
+ }
30
+
31
+ interface BasicPageProps extends React.PropsWithChildren {
32
+ subtitle?: string
33
+ title?: ReactNode
34
+ }
@@ -0,0 +1,4 @@
1
+ @import "tailwindcss";
2
+ @source "../";
3
+ @import "./fonts.css";
4
+ @import "./globals.css";
@@ -0,0 +1,67 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { useState } from 'react'
3
+
4
+ import {
5
+ AnimatedCount,
6
+ useAnimatedCount
7
+ } from './animated-count'
8
+ import { Button } from './button'
9
+ import { Typography } from './typography'
10
+ import { Small } from './typography/small'
11
+
12
+ const meta = {
13
+ args: { damping: 1, duration: 1600, value: 84210 },
14
+ component: AnimatedCount,
15
+ title: 'Components/AnimatedCount'
16
+ } satisfies Meta<typeof AnimatedCount>
17
+
18
+ export default meta
19
+
20
+ type Story = StoryObj<typeof meta>
21
+
22
+ export const Playground: Story = {
23
+ render: args => (
24
+ <Typography className="text-4xl font-bold tabular-nums" expanded>
25
+ <AnimatedCount {...args} />
26
+ </Typography>
27
+ )
28
+ }
29
+
30
+ function LiveCount() {
31
+ const ts = useState(() => new Date())[0]
32
+ const value = useAnimatedCount(1000, 12, ts)
33
+
34
+ return <AnimatedCount duration={500} value={value} />
35
+ }
36
+
37
+ export const Live: StoryObj = {
38
+ render: () => (
39
+ <div className="flex flex-col gap-2">
40
+ <Small className="opacity-40">Live ticker (rate = 12)</Small>
41
+
42
+ <Typography className="text-4xl font-bold tabular-nums" expanded>
43
+ <LiveCount />
44
+ </Typography>
45
+ </div>
46
+ )
47
+ }
48
+
49
+ export const Manual: StoryObj = {
50
+ render: () => {
51
+ const [value, setValue] = useState(100)
52
+
53
+ return (
54
+ <div className="flex flex-col gap-4">
55
+ <Typography className="text-4xl font-bold tabular-nums" expanded>
56
+ <AnimatedCount duration={800} value={value} />
57
+ </Typography>
58
+
59
+ <div className="flex gap-2">
60
+ <Button onClick={() => setValue(v => v + 1000)}>+1000</Button>
61
+ <Button onClick={() => setValue(v => v + 100)}>+100</Button>
62
+ <Button onClick={() => setValue(v => Math.max(0, v - 100))}>-100</Button>
63
+ </div>
64
+ </div>
65
+ )
66
+ }
67
+ }
@@ -0,0 +1,168 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useRef, useState } from 'react'
4
+
5
+ const ease = (t: number) => (t < 0.5 ? 4 * t ** 3 : 1 - (-2 * t + 2) ** 3 / 2)
6
+ const easeD = (t: number) => (t < 0.5 ? 12 * t ** 2 : 6 * (-2 * t + 2) ** 2)
7
+
8
+ export function useAnimatedCount(
9
+ from: number,
10
+ rate: number,
11
+ ts = new Date(),
12
+ pausedAt?: Date
13
+ ) {
14
+ const [value, setValue] = useState(from)
15
+ const current = useRef(from)
16
+
17
+ useEffect(() => {
18
+ if (!rate) {
19
+ return
20
+ }
21
+
22
+ let raf: number
23
+ let prev = Date.now()
24
+ let last = current.current
25
+
26
+ const target = () =>
27
+ from +
28
+ Math.floor(
29
+ ((pausedAt
30
+ ? Math.max(0, pausedAt.getTime() - ts.getTime())
31
+ : Date.now() - ts.getTime()) /
32
+ 1e3) *
33
+ rate *
34
+ 0.9
35
+ )
36
+
37
+ const tick = () => {
38
+ const now = Date.now()
39
+ current.current +=
40
+ (target() - current.current) * Math.min(1, ((now - prev) / 1e3) * 3)
41
+ prev = now
42
+
43
+ const rounded = Math.round(current.current)
44
+
45
+ if (rounded !== last) {
46
+ last = rounded
47
+ setValue(rounded)
48
+ }
49
+
50
+ if (!pausedAt || Math.abs(current.current - target()) > 0.5) {
51
+ raf = requestAnimationFrame(tick)
52
+ }
53
+ }
54
+
55
+ raf = requestAnimationFrame(tick)
56
+
57
+ return () => cancelAnimationFrame(raf)
58
+ }, [from, ts, rate, pausedAt])
59
+
60
+ return value
61
+ }
62
+
63
+ export function AnimatedCount({
64
+ damping = 1,
65
+ duration,
66
+ pausedAt,
67
+ rate = 0,
68
+ value
69
+ }: Props) {
70
+ const id = useRef(Math.random().toString(36).slice(2, 9))
71
+ const prev = useRef(value)
72
+ const [display, setDisplay] = useState(value)
73
+ const [velocity, setVelocity] = useState(rate)
74
+
75
+ useEffect(() => {
76
+ if (!duration) {
77
+ prev.current = value
78
+ return
79
+ }
80
+
81
+ const start = prev.current
82
+ const delta = value - start
83
+ const dur = duration * damping
84
+ prev.current = value
85
+
86
+ if (!delta) {
87
+ setVelocity(0)
88
+ return
89
+ }
90
+
91
+ const t0 = performance.now()
92
+
93
+ const tick = (now: number) => {
94
+ const t = Math.min((now - t0) / dur, 1)
95
+ setDisplay(Math.round(start + delta * ease(t)))
96
+ setVelocity(Math.abs((delta * easeD(t)) / dur) * 1000)
97
+ t < 1 ? requestAnimationFrame(tick) : setVelocity(0)
98
+ }
99
+
100
+ const raf = requestAnimationFrame(tick)
101
+
102
+ return () => cancelAnimationFrame(raf)
103
+ }, [value, duration, rate, damping])
104
+
105
+ const digits = Math.round(duration ? display : value).toLocaleString().split('')
106
+ const v = duration ? velocity : rate
107
+ const paused = !duration && pausedAt
108
+
109
+ const blurred = new Set(
110
+ digits
111
+ .map((c, i) => {
112
+ if (!/\d/.test(c) || v <= 0 || paused) {
113
+ return -1
114
+ }
115
+
116
+ const pos =
117
+ digits.filter(x => /\d/.test(x)).length -
118
+ digits.slice(0, i + 1).filter(x => /\d/.test(x)).length
119
+
120
+ return (10 ** Math.max(0, pos) / v) * 1e3 < 500 ? i : -1
121
+ })
122
+ .filter(i => i >= 0)
123
+ )
124
+
125
+ return (
126
+ <>
127
+ <svg className="pointer-events-none absolute size-0">
128
+ <defs>
129
+ {digits.map((_, i) => (
130
+ <filter
131
+ id={`blur-${id.current}-${i}`}
132
+ key={i}
133
+ suppressHydrationWarning
134
+ >
135
+ <feGaussianBlur
136
+ stdDeviation={`0 ${blurred.has(i) ? 1 + [...blurred].indexOf(i) * 0.6 : 0}`}
137
+ />
138
+ </filter>
139
+ ))}
140
+ </defs>
141
+ </svg>
142
+
143
+ <span className="inline-flex tabular-nums">
144
+ {digits.map((c, i) => (
145
+ <span
146
+ className="inline-block text-center"
147
+ key={i}
148
+ style={{
149
+ filter: blurred.has(i) ? `url(#blur-${id.current}-${i})` : 'none',
150
+ width: c === ',' ? '0.4ch' : '1ch'
151
+ }}
152
+ suppressHydrationWarning
153
+ >
154
+ {c}
155
+ </span>
156
+ ))}
157
+ </span>
158
+ </>
159
+ )
160
+ }
161
+
162
+ interface Props {
163
+ damping?: number
164
+ duration?: number
165
+ pausedAt?: Date
166
+ rate?: number
167
+ value: number
168
+ }