@nous-research/ui 0.15.0 → 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,398 @@
1
+ 'use client'
2
+
3
+ import { AnimatePresence, motion } from 'motion/react'
4
+ import { createElement, useCallback, useRef, useState } from 'react'
5
+
6
+ import { useCssVarDims } from '../hooks/use-css-var-dims'
7
+ import { useGpuTier } from '../hooks/use-gpu-tier'
8
+ import { cn } from '../utils'
9
+
10
+ import { Blink } from './components/blink'
11
+ import { Cell, Grid } from './components/grid'
12
+ import { HoverBg } from './components/hover-bg'
13
+ import { HamburgerIcon } from './components/icons/hamburger'
14
+ import { Scramble } from './components/scramble'
15
+ import { Socials, type SocialLink } from './components/socials'
16
+ import { ThemeToggle } from './components/theme-toggle'
17
+ import { H2 } from './components/typography/h2'
18
+ import { Small } from './components/typography/small'
19
+
20
+ const DEFAULT_BRAND = (
21
+ <hgroup className="flex flex-col gap-2">
22
+ <Small>Nous</Small>
23
+
24
+ <H2>Research</H2>
25
+ </hgroup>
26
+ )
27
+
28
+ const DEFAULT_LINKS: HeaderLink[] = [
29
+ { href: '/projects', label: 'Projects' },
30
+ { href: '/participants', label: 'Participants' },
31
+ { href: '/provenance', label: 'Provenance' },
32
+ { href: '/contribute', label: 'Contribute' }
33
+ ]
34
+
35
+ export function Header({
36
+ brand = DEFAULT_BRAND,
37
+ brandHref = '/',
38
+ className,
39
+ desktopGridStyle,
40
+ links = DEFAULT_LINKS,
41
+ LinkComponent = 'a',
42
+ scramble: scrambleProp = true,
43
+ socials,
44
+ socialsLabel = 'Socials',
45
+ style,
46
+ themeLabel = 'Theme',
47
+ themeToggle = false
48
+ }: HeaderProps) {
49
+ const ref = useRef<HTMLElement>(null)
50
+ useCssVarDims('header', ref)
51
+
52
+ // Skip the hover-Scramble rAF loop on tier-0 devices (no GPU / software
53
+ // renderer / `prefers-reduced-motion: reduce`) regardless of the prop.
54
+ const gpuTier = useGpuTier()
55
+ const scramble = scrambleProp && gpuTier > 0
56
+
57
+ const [open, setOpen] = useState(false)
58
+ const close = useCallback(() => setOpen(false), [])
59
+
60
+ const hasSocials = (socials?.length ?? 0) > 0
61
+ const hasMobileChrome = themeToggle || hasSocials
62
+
63
+ return (
64
+ <header className={className} ref={ref} style={style}>
65
+ <Grid
66
+ className="hidden border-t border-b lg:grid"
67
+ style={desktopGridStyle}
68
+ >
69
+ <BrandCell
70
+ brand={brand}
71
+ href={brandHref}
72
+ LinkComponent={LinkComponent}
73
+ />
74
+
75
+ {links.map(link => (
76
+ <NavCell
77
+ key={link.href}
78
+ link={link}
79
+ LinkComponent={LinkComponent}
80
+ scramble={scramble}
81
+ />
82
+ ))}
83
+
84
+ {hasSocials && (
85
+ <Cell className="flex items-start justify-between">
86
+ <Small className="opacity-50">{socialsLabel}</Small>
87
+
88
+ <Socials items={socials!} />
89
+ </Cell>
90
+ )}
91
+
92
+ {themeToggle && (
93
+ <Cell className="flex items-start justify-between">
94
+ <Small className="opacity-50">{themeLabel}</Small>
95
+
96
+ <ThemeToggle />
97
+ </Cell>
98
+ )}
99
+ </Grid>
100
+
101
+ <div
102
+ className={cn(
103
+ 'flex items-center justify-between border border-current/20 p-4',
104
+ 'lg:hidden'
105
+ )}
106
+ >
107
+ <BrandLink
108
+ brand={brand}
109
+ href={brandHref}
110
+ LinkComponent={LinkComponent}
111
+ />
112
+
113
+ <div className="flex items-center gap-3">
114
+ {themeToggle && <ThemeToggle />}
115
+
116
+ <button
117
+ aria-label={open ? 'Close menu' : 'Open menu'}
118
+ className="relative z-50 cursor-pointer bg-transparent p-2"
119
+ onClick={() => setOpen(v => !v)}
120
+ type="button"
121
+ >
122
+ <HamburgerIcon open={open} />
123
+ </button>
124
+ </div>
125
+ </div>
126
+
127
+ <AnimatePresence>
128
+ {open && (
129
+ <motion.div
130
+ animate={{ opacity: 1 }}
131
+ className={cn(
132
+ 'bg-background/95 fixed inset-0 z-50 flex flex-col backdrop-blur-sm',
133
+ 'p-8 lg:hidden'
134
+ )}
135
+ exit={{ opacity: 0 }}
136
+ initial={{ opacity: 0 }}
137
+ transition={{ duration: 0.2 }}
138
+ >
139
+ <div className="flex flex-col border border-current/20">
140
+ <div className="flex items-center justify-between border-b border-current/20 p-4">
141
+ <BrandLink
142
+ brand={brand}
143
+ href={brandHref}
144
+ LinkComponent={LinkComponent}
145
+ onClick={close}
146
+ />
147
+
148
+ <button
149
+ aria-label="Close menu"
150
+ className="cursor-pointer bg-transparent p-2"
151
+ onClick={close}
152
+ type="button"
153
+ >
154
+ <HamburgerIcon open />
155
+ </button>
156
+ </div>
157
+
158
+ {links.map(link => (
159
+ <MobileNavLink
160
+ key={link.href}
161
+ link={link}
162
+ LinkComponent={LinkComponent}
163
+ onNavigate={close}
164
+ scramble={scramble}
165
+ />
166
+ ))}
167
+
168
+ {hasMobileChrome && (
169
+ <div className="flex items-center gap-3 border-b border-current/20 p-4">
170
+ {hasSocials && (
171
+ <>
172
+ <Small className="opacity-50">{socialsLabel}</Small>
173
+
174
+ <Socials items={socials!} onNavigate={close} />
175
+ </>
176
+ )}
177
+
178
+ {themeToggle && hasSocials && <span className="flex-1" />}
179
+
180
+ {themeToggle && (
181
+ <>
182
+ <Small className="opacity-50">{themeLabel}</Small>
183
+
184
+ <ThemeToggle />
185
+ </>
186
+ )}
187
+ </div>
188
+ )}
189
+ </div>
190
+ </motion.div>
191
+ )}
192
+ </AnimatePresence>
193
+ </header>
194
+ )
195
+ }
196
+
197
+ function BrandCell({ brand, href, LinkComponent }: BrandSlotProps) {
198
+ return isExternal(href) ? (
199
+ <Cell href={href} {...EXTERNAL_REL} as="a">
200
+ {brand}
201
+ </Cell>
202
+ ) : (
203
+ <Cell as={LinkComponent} href={href}>
204
+ {brand}
205
+ </Cell>
206
+ )
207
+ }
208
+
209
+ function BrandLink({ brand, href, LinkComponent, onClick }: BrandLinkProps) {
210
+ if (isExternal(href)) {
211
+ return (
212
+ <a href={href} onClick={onClick} {...EXTERNAL_REL}>
213
+ {brand}
214
+ </a>
215
+ )
216
+ }
217
+
218
+ return createElement(
219
+ LinkComponent,
220
+ { href, onClick } as Record<string, unknown>,
221
+ brand
222
+ )
223
+ }
224
+
225
+ function NavCell({ link, LinkComponent, scramble }: NavCellProps) {
226
+ const ref = useRef<HTMLAnchorElement>(null)
227
+ const isExt = link.external ?? isExternal(link.href)
228
+
229
+ const inner = (
230
+ <>
231
+ <Small>
232
+ {scramble ? (
233
+ <Scramble target={ref}>{link.label}</Scramble>
234
+ ) : (
235
+ link.label
236
+ )}
237
+
238
+ <Blink />
239
+ </Small>
240
+
241
+ <HoverBg />
242
+ </>
243
+ )
244
+
245
+ if (isExt) {
246
+ return (
247
+ <Cell
248
+ as="a"
249
+ className="group relative cursor-pointer"
250
+ href={link.href}
251
+ onClick={link.onClick}
252
+ ref={ref}
253
+ {...EXTERNAL_REL}
254
+ >
255
+ {inner}
256
+ </Cell>
257
+ )
258
+ }
259
+
260
+ return (
261
+ <Cell
262
+ as={LinkComponent}
263
+ className="group relative cursor-pointer"
264
+ href={link.href}
265
+ onClick={link.onClick}
266
+ ref={ref}
267
+ >
268
+ {inner}
269
+ </Cell>
270
+ )
271
+ }
272
+
273
+ function MobileNavLink({
274
+ link,
275
+ LinkComponent,
276
+ onNavigate,
277
+ scramble
278
+ }: MobileNavLinkProps) {
279
+ const ref = useRef<HTMLAnchorElement>(null)
280
+ const isExt = link.external ?? isExternal(link.href)
281
+
282
+ const className = cn(
283
+ 'group relative flex cursor-pointer items-center border-b border-current/20 p-4'
284
+ )
285
+
286
+ const onClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
287
+ link.onClick?.(e)
288
+ onNavigate()
289
+ }
290
+
291
+ const children = (
292
+ <>
293
+ <Small>
294
+ {scramble ? (
295
+ <Scramble target={ref}>{link.label}</Scramble>
296
+ ) : (
297
+ link.label
298
+ )}
299
+
300
+ <Blink />
301
+ </Small>
302
+
303
+ <HoverBg />
304
+ </>
305
+ )
306
+
307
+ if (isExt) {
308
+ return (
309
+ <a
310
+ className={className}
311
+ href={link.href}
312
+ onClick={onClick}
313
+ ref={ref}
314
+ {...EXTERNAL_REL}
315
+ >
316
+ {children}
317
+ </a>
318
+ )
319
+ }
320
+
321
+ return createElement(
322
+ LinkComponent,
323
+ { className, href: link.href, onClick, ref } as Record<string, unknown>,
324
+ children
325
+ )
326
+ }
327
+
328
+ const EXTERNAL_REL = {
329
+ rel: 'noopener noreferrer',
330
+ target: '_blank'
331
+ } as const
332
+
333
+ const isExternal = (href: string) => /^(https?:|mailto:|tel:)/i.test(href)
334
+
335
+ interface BrandLinkProps extends BrandSlotProps {
336
+ onClick?: React.MouseEventHandler
337
+ }
338
+
339
+ interface BrandSlotProps {
340
+ brand: React.ReactNode
341
+ href: string
342
+ LinkComponent: React.ElementType
343
+ }
344
+
345
+ export interface HeaderLink {
346
+ external?: boolean
347
+ href: string
348
+ label: string
349
+ onClick?: React.MouseEventHandler
350
+ }
351
+
352
+ export interface HeaderProps {
353
+ brand?: React.ReactNode
354
+ brandHref?: string
355
+ className?: string
356
+ /**
357
+ * Inline `style` for the desktop `Grid` only — useful for overriding
358
+ * `grid-template-columns` (e.g. to align with a sidebar track) without
359
+ * affecting the mobile bar or drawer.
360
+ */
361
+ desktopGridStyle?: React.CSSProperties
362
+ links?: HeaderLink[]
363
+ LinkComponent?: React.ElementType
364
+ /**
365
+ * Apply the hover-Scramble effect to nav link labels. Defaults to `true`,
366
+ * automatically suppressed on tier-0 GPUs and when the user has
367
+ * `prefers-reduced-motion: reduce`.
368
+ */
369
+ scramble?: boolean
370
+ /**
371
+ * Optional socials shown in a trailing chrome cell on desktop and in the
372
+ * mobile drawer's chrome row. For nav-heavy products (≥ 5 links) prefer
373
+ * passing socials to `<Footer>` instead — the desktop `Grid` only ships
374
+ * column rules through `grid-cols-6`, so brand + many links + chrome can
375
+ * overflow.
376
+ */
377
+ socials?: SocialLink[]
378
+ socialsLabel?: string
379
+ style?: React.CSSProperties
380
+ themeLabel?: string
381
+ themeToggle?: boolean
382
+ }
383
+
384
+ /** @deprecated Use `SocialLink` from `@nous-research/ui`. Same shape. */
385
+ export type HeaderSocial = SocialLink
386
+
387
+ interface MobileNavLinkProps {
388
+ link: HeaderLink
389
+ LinkComponent: React.ElementType
390
+ onNavigate: () => void
391
+ scramble: boolean
392
+ }
393
+
394
+ interface NavCellProps {
395
+ link: HeaderLink
396
+ LinkComponent: React.ElementType
397
+ scramble: boolean
398
+ }
@@ -0,0 +1,11 @@
1
+ export function LayoutWrapper({
2
+ children
3
+ }: Readonly<React.PropsWithChildren>) {
4
+ return (
5
+ <html lang="en">
6
+ <body className="text-text-primary bg-black antialiased">
7
+ {children}
8
+ </body>
9
+ </html>
10
+ )
11
+ }
@@ -0,0 +1,21 @@
1
+ export const hexToRgb = (hex: string) => [
2
+ parseInt(hex.slice(1, 3), 16),
3
+ parseInt(hex.slice(3, 5), 16),
4
+ parseInt(hex.slice(5, 7), 16)
5
+ ]
6
+
7
+ export const rgbToHex = (r: number, g: number, b: number) =>
8
+ `#${[r, g, b].map(v => v.toString(16).padStart(2, '0')).join('')}`
9
+
10
+ export const colorDodge = (base: string, blend: string) => {
11
+ const [br, bg, bb] = hexToRgb(base)
12
+ const [lr, lg, lb] = hexToRgb(blend)
13
+
14
+ const d = (b: number, l: number) =>
15
+ l === 255 ? 255 : Math.min(255, Math.floor((b * 255) / (255 - l)))
16
+
17
+ return rgbToHex(d(br, lr), d(bg, lg), d(bb, lb))
18
+ }
19
+
20
+ export const colorMix = (color: string, alpha: number) =>
21
+ `color-mix(in srgb, ${color} ${Math.round(alpha * 100)}%, transparent)`
@@ -0,0 +1,62 @@
1
+ import { type ClassValue, clsx } from 'clsx'
2
+ import sanitize from 'sanitize-html'
3
+ import { twMerge } from 'tailwind-merge'
4
+ import * as THREE from 'three'
5
+
6
+ import { hexToRgb, rgbToHex } from './color'
7
+ import { polyRef } from './poly'
8
+ import type { PolyComponent, PolyProps, PolyRef } from './poly'
9
+
10
+ export { hexToRgb, polyRef, rgbToHex }
11
+ export type { PolyComponent, PolyProps, PolyRef }
12
+
13
+ export const cn = (...inputs: ClassValue[]) => twMerge(clsx(inputs))
14
+
15
+ export const clamp = (v: number, min = 0, max = 1) =>
16
+ Math.min(max, Math.max(min, Number.isFinite(v) ? v : min))
17
+
18
+ export const smoothstep = (edge0: number, edge1: number, x: number) => {
19
+ const t = clamp((x - edge0) / (edge1 - edge0))
20
+
21
+ return t * t * (3 - 2 * t)
22
+ }
23
+
24
+ export const hexToVec3 = (hex: string) => {
25
+ const [r, g, b] = hexToRgb(hex)
26
+
27
+ return new THREE.Vector3(r / 255, g / 255, b / 255)
28
+ }
29
+
30
+ export const truncate = (text: string, options: { length: number }) =>
31
+ text.length > options.length ? `${text.slice(0, options.length)}...` : text
32
+
33
+ export const stripWpStyles = (html: string) =>
34
+ sanitize(html, {
35
+ allowedAttributes: {
36
+ a: ['href', 'target', 'rel', 'name'],
37
+ audio: ['src', 'controls'],
38
+ iframe: ['src', 'width', 'height', 'frameborder', 'allowfullscreen'],
39
+ img: ['src', 'alt', 'width', 'height', 'loading'],
40
+ source: ['src', 'type', 'srcset'],
41
+ td: ['colspan', 'rowspan'],
42
+ th: ['colspan', 'rowspan'],
43
+ video: ['src', 'controls', 'width', 'height', 'poster']
44
+ },
45
+ allowedIframeHostnames: [
46
+ 'www.youtube.com',
47
+ 'youtube.com',
48
+ 'player.vimeo.com'
49
+ ],
50
+ allowedSchemes: ['http', 'https', 'mailto'],
51
+ allowedTags: [
52
+ ...sanitize.defaults.allowedTags,
53
+ 'img',
54
+ 'figure',
55
+ 'figcaption',
56
+ 'iframe',
57
+ 'video',
58
+ 'audio',
59
+ 'source',
60
+ 'picture'
61
+ ]
62
+ })
@@ -0,0 +1,26 @@
1
+ import { forwardRef } from 'react'
2
+
3
+ export type PolyProps<
4
+ T extends React.ElementType,
5
+ Own extends object = object
6
+ > = { as?: T } & Own & Omit<React.ComponentPropsWithoutRef<T>, 'as' | keyof Own>
7
+
8
+ export type PolyRef<T extends React.ElementType> =
9
+ React.ComponentPropsWithRef<T>['ref']
10
+
11
+ export type PolyComponent<
12
+ D extends React.ElementType,
13
+ Own extends object = object
14
+ > = <T extends React.ElementType = D>(
15
+ props: PolyProps<T, Own> & { ref?: PolyRef<T> }
16
+ ) => null | React.ReactElement
17
+
18
+ export const polyRef = forwardRef as <
19
+ D extends React.ElementType,
20
+ Own extends object = object
21
+ >(
22
+ render: <T extends React.ElementType = D>(
23
+ props: PolyProps<T, Own>,
24
+ ref: PolyRef<T>
25
+ ) => null | React.ReactElement
26
+ ) => PolyComponent<D, Own>