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