@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.
- package/CHANGELOG.md +227 -0
- package/README.md +24 -4
- package/dist/fonts.js +1 -0
- package/dist/hooks/use-capped-frame.js +1 -0
- package/dist/hooks/use-css-var-dims.js +1 -0
- package/dist/hooks/use-gpu-tier.js +1 -0
- package/dist/hooks/use-render-loop.js +1 -0
- package/dist/hooks/use-smooth-controls.js +1 -0
- package/dist/index.js +1 -0
- package/dist/ui/basic-page.js +1 -0
- package/dist/ui/components/animated-count.js +1 -0
- package/dist/ui/components/ascii.js +1 -0
- package/dist/ui/components/badge.js +2 -1
- package/dist/ui/components/badges/nous-girl.js +1 -0
- package/dist/ui/components/blend-mode.js +1 -0
- package/dist/ui/components/blink.js +1 -0
- package/dist/ui/components/button.js +2 -1
- package/dist/ui/components/checkbox.js +1 -0
- package/dist/ui/components/command-block.js +4 -3
- package/dist/ui/components/cursor.js +1 -0
- package/dist/ui/components/dropdown-menu.js +1 -0
- package/dist/ui/components/fit-text/index.js +1 -0
- package/dist/ui/components/graphs/bar-chart.js +1 -0
- package/dist/ui/components/graphs/index.js +1 -0
- package/dist/ui/components/graphs/line-chart.js +1 -0
- package/dist/ui/components/graphs/utils.js +1 -0
- package/dist/ui/components/grid/index.js +1 -0
- package/dist/ui/components/hover-bg.js +1 -0
- package/dist/ui/components/icons/arrow.js +1 -0
- package/dist/ui/components/icons/check.js +1 -0
- package/dist/ui/components/icons/chevron.js +1 -0
- package/dist/ui/components/icons/discord.js +1 -0
- package/dist/ui/components/icons/eye.js +1 -0
- package/dist/ui/components/icons/gear.js +1 -0
- package/dist/ui/components/icons/github.js +1 -0
- package/dist/ui/components/icons/hamburger.js +1 -0
- package/dist/ui/components/icons/heart.js +1 -0
- package/dist/ui/components/icons/index.js +1 -0
- package/dist/ui/components/icons/link.js +1 -0
- package/dist/ui/components/icons/minus.js +1 -0
- package/dist/ui/components/icons/search.js +1 -0
- package/dist/ui/components/image-distortion.js +1 -0
- package/dist/ui/components/leva-client.js +1 -0
- package/dist/ui/components/list-item.js +3 -2
- package/dist/ui/components/modal/index.js +1 -0
- package/dist/ui/components/modal/modal.css +1 -1
- package/dist/ui/components/overlays/blend-modes.js +1 -0
- package/dist/ui/components/overlays/glitch.js +1 -0
- package/dist/ui/components/overlays/greys.js +1 -0
- package/dist/ui/components/overlays/index.js +1 -0
- package/dist/ui/components/overlays/lens-layers.js +1 -0
- package/dist/ui/components/overlays/lens.js +1 -0
- package/dist/ui/components/overlays/noise.js +1 -0
- package/dist/ui/components/overlays/vignette.js +1 -0
- package/dist/ui/components/poster.js +1 -0
- package/dist/ui/components/progress.js +1 -0
- package/dist/ui/components/scene-canvas.js +1 -0
- package/dist/ui/components/scramble.js +1 -0
- package/dist/ui/components/segmented.js +5 -4
- package/dist/ui/components/select.js +1 -0
- package/dist/ui/components/selection-switcher.js +1 -0
- package/dist/ui/components/shader.js +1 -0
- package/dist/ui/components/socials.js +1 -0
- package/dist/ui/components/spinner.js +1 -0
- package/dist/ui/components/stats.js +2 -1
- package/dist/ui/components/switch.js +1 -0
- package/dist/ui/components/tabs.js +4 -3
- package/dist/ui/components/terminal-demo.js +2 -1
- package/dist/ui/components/theme-toggle.js +1 -0
- package/dist/ui/components/tier-card.js +2 -1
- package/dist/ui/components/tv.js +1 -0
- package/dist/ui/components/typography/h1.js +1 -0
- package/dist/ui/components/typography/h2.js +1 -0
- package/dist/ui/components/typography/index.js +1 -0
- package/dist/ui/components/typography/legend.js +1 -0
- package/dist/ui/components/typography/small.js +1 -0
- package/dist/ui/components/watchlist.js +2 -1
- package/dist/ui/footer.js +1 -0
- package/dist/ui/globals.css +33 -1
- package/dist/ui/header.js +1 -0
- package/dist/ui/layout-wrapper.js +2 -1
- package/dist/utils/color.js +1 -0
- package/dist/utils/index.js +1 -0
- package/dist/utils/poly.js +1 -0
- package/package.json +4 -2
- package/src/assets/filler-bg0.webp +0 -0
- package/src/assets.d.ts +38 -0
- package/src/fonts/Collapse-Bold.woff2 +0 -0
- package/src/fonts/Collapse-BoldItalic.woff2 +0 -0
- package/src/fonts/Collapse-Italic.woff2 +0 -0
- package/src/fonts/Collapse-Light.woff2 +0 -0
- package/src/fonts/Collapse-LightItalic.woff2 +0 -0
- package/src/fonts/Collapse-Regular.woff2 +0 -0
- package/src/fonts/Collapse-Thin.woff2 +0 -0
- package/src/fonts/Collapse-ThinItalic.woff2 +0 -0
- package/src/fonts/Mondwest-Regular.woff2 +0 -0
- package/src/fonts/Neuebit-Bold.woff2 +0 -0
- package/src/fonts/RulesCompressed-Medium.woff2 +0 -0
- package/src/fonts/RulesCompressed-Regular.woff2 +0 -0
- package/src/fonts/RulesExpanded-Bold.woff2 +0 -0
- package/src/fonts/RulesExpanded-Regular.woff2 +0 -0
- package/src/fonts.ts +6 -0
- package/src/hooks/use-capped-frame.ts +18 -0
- package/src/hooks/use-css-var-dims.ts +39 -0
- package/src/hooks/use-gpu-tier.ts +165 -0
- package/src/hooks/use-render-loop.ts +121 -0
- package/src/hooks/use-smooth-controls.ts +318 -0
- package/src/index.ts +109 -0
- package/src/ui/basic-page.tsx +34 -0
- package/src/ui/build.css +4 -0
- package/src/ui/components/animated-count.stories.tsx +67 -0
- package/src/ui/components/animated-count.tsx +168 -0
- package/src/ui/components/ascii.stories.tsx +30 -0
- package/src/ui/components/ascii.tsx +110 -0
- package/src/ui/components/badge.stories.tsx +31 -0
- package/src/ui/components/badge.tsx +60 -0
- package/src/ui/components/badges/nous-girl.tsx +52 -0
- package/src/ui/components/blend-mode.stories.tsx +33 -0
- package/src/ui/components/blend-mode.tsx +129 -0
- package/src/ui/components/blink.stories.tsx +32 -0
- package/src/ui/components/blink.tsx +21 -0
- package/src/ui/components/button.stories.tsx +68 -0
- package/src/ui/components/button.tsx +170 -0
- package/src/ui/components/checkbox.stories.tsx +113 -0
- package/src/ui/components/checkbox.tsx +36 -0
- package/src/ui/components/command-block.stories.tsx +52 -0
- package/src/ui/components/command-block.tsx +86 -0
- package/src/ui/components/cursor.tsx +115 -0
- package/src/ui/components/dropdown-menu.stories.tsx +52 -0
- package/src/ui/components/dropdown-menu.tsx +117 -0
- package/src/ui/components/fit-text/fit-text.css +42 -0
- package/src/ui/components/fit-text/index.stories.tsx +33 -0
- package/src/ui/components/fit-text/index.tsx +45 -0
- package/src/ui/components/graphs/bar-chart.tsx +153 -0
- package/src/ui/components/graphs/index.stories.tsx +64 -0
- package/src/ui/components/graphs/index.tsx +4 -0
- package/src/ui/components/graphs/line-chart.tsx +213 -0
- package/src/ui/components/graphs/utils.tsx +265 -0
- package/src/ui/components/grid/grid.css +79 -0
- package/src/ui/components/grid/index.tsx +19 -0
- package/src/ui/components/hover-bg.stories.tsx +29 -0
- package/src/ui/components/hover-bg.tsx +15 -0
- package/src/ui/components/icons/arrow.tsx +42 -0
- package/src/ui/components/icons/check.tsx +14 -0
- package/src/ui/components/icons/chevron.tsx +45 -0
- package/src/ui/components/icons/discord.tsx +16 -0
- package/src/ui/components/icons/eye.tsx +12 -0
- package/src/ui/components/icons/gear.tsx +51 -0
- package/src/ui/components/icons/github.tsx +16 -0
- package/src/ui/components/icons/hamburger.tsx +52 -0
- package/src/ui/components/icons/heart.tsx +12 -0
- package/src/ui/components/icons/index.ts +12 -0
- package/src/ui/components/icons/link.tsx +14 -0
- package/src/ui/components/icons/minus.tsx +14 -0
- package/src/ui/components/icons/search.tsx +28 -0
- package/src/ui/components/image-distortion.stories.tsx +120 -0
- package/src/ui/components/image-distortion.tsx +498 -0
- package/src/ui/components/leva-client.tsx +14 -0
- package/src/ui/components/list-item.stories.tsx +83 -0
- package/src/ui/components/list-item.tsx +37 -0
- package/src/ui/components/modal/index.stories.tsx +46 -0
- package/src/ui/components/modal/index.tsx +48 -0
- package/src/ui/components/modal/modal.css +36 -0
- package/src/ui/components/overlays/blend-modes.ts +13 -0
- package/src/ui/components/overlays/glitch.tsx +243 -0
- package/src/ui/components/overlays/greys.tsx +386 -0
- package/src/ui/components/overlays/index.tsx +47 -0
- package/src/ui/components/overlays/lens-layers.tsx +119 -0
- package/src/ui/components/overlays/lens.ts +91 -0
- package/src/ui/components/overlays/noise.tsx +174 -0
- package/src/ui/components/overlays/vignette.tsx +60 -0
- package/src/ui/components/poster.stories.tsx +513 -0
- package/src/ui/components/poster.tsx +411 -0
- package/src/ui/components/progress.stories.tsx +48 -0
- package/src/ui/components/progress.tsx +56 -0
- package/src/ui/components/scene-canvas.tsx +254 -0
- package/src/ui/components/scramble.stories.tsx +49 -0
- package/src/ui/components/scramble.tsx +95 -0
- package/src/ui/components/segmented.stories.tsx +101 -0
- package/src/ui/components/segmented.tsx +81 -0
- package/src/ui/components/select.stories.tsx +88 -0
- package/src/ui/components/select.tsx +267 -0
- package/src/ui/components/selection-switcher.tsx +44 -0
- package/src/ui/components/shader.tsx +83 -0
- package/src/ui/components/socials.tsx +42 -0
- package/src/ui/components/spinner.stories.tsx +101 -0
- package/src/ui/components/spinner.tsx +60 -0
- package/src/ui/components/stats.stories.tsx +24 -0
- package/src/ui/components/stats.tsx +53 -0
- package/src/ui/components/switch.stories.tsx +77 -0
- package/src/ui/components/switch.tsx +48 -0
- package/src/ui/components/tabs.stories.tsx +101 -0
- package/src/ui/components/tabs.tsx +66 -0
- package/src/ui/components/terminal-demo.stories.tsx +67 -0
- package/src/ui/components/terminal-demo.tsx +189 -0
- package/src/ui/components/theme-toggle.stories.tsx +47 -0
- package/src/ui/components/theme-toggle.tsx +66 -0
- package/src/ui/components/tier-card.stories.tsx +217 -0
- package/src/ui/components/tier-card.tsx +190 -0
- package/src/ui/components/tv.stories.tsx +37 -0
- package/src/ui/components/tv.tsx +257 -0
- package/src/ui/components/typography/h1.tsx +18 -0
- package/src/ui/components/typography/h2.tsx +18 -0
- package/src/ui/components/typography/index.tsx +54 -0
- package/src/ui/components/typography/legend.tsx +24 -0
- package/src/ui/components/typography/small.tsx +11 -0
- package/src/ui/components/watchlist.stories.tsx +33 -0
- package/src/ui/components/watchlist.tsx +105 -0
- package/src/ui/fonts.css +63 -0
- package/src/ui/footer.tsx +111 -0
- package/src/ui/globals.css +383 -0
- package/src/ui/header.tsx +398 -0
- package/src/ui/layout-wrapper.tsx +11 -0
- package/src/utils/color.ts +21 -0
- package/src/utils/index.ts +62 -0
- 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,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>
|