@nous-research/ui 0.15.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (258) hide show
  1. package/CHANGELOG.md +266 -0
  2. package/README.md +24 -4
  3. package/dist/fonts.js +1 -0
  4. package/dist/hooks/use-below-breakpoint.d.ts +2 -0
  5. package/dist/hooks/use-below-breakpoint.js +17 -0
  6. package/dist/hooks/use-capped-frame.js +1 -0
  7. package/dist/hooks/use-confirm-delete.d.ts +10 -0
  8. package/dist/hooks/use-confirm-delete.js +35 -0
  9. package/dist/hooks/use-css-var-dims.js +1 -0
  10. package/dist/hooks/use-gpu-tier.js +1 -0
  11. package/dist/hooks/use-render-loop.js +1 -0
  12. package/dist/hooks/use-smooth-controls.js +1 -0
  13. package/dist/hooks/use-toast.d.ts +7 -0
  14. package/dist/hooks/use-toast.js +21 -0
  15. package/dist/index.d.ts +11 -1
  16. package/dist/index.js +23 -1
  17. package/dist/ui/basic-page.js +1 -0
  18. package/dist/ui/components/animated-count.js +1 -0
  19. package/dist/ui/components/ascii.js +1 -0
  20. package/dist/ui/components/badge.js +2 -1
  21. package/dist/ui/components/badges/nous-girl.js +1 -0
  22. package/dist/ui/components/blend-mode.js +1 -0
  23. package/dist/ui/components/blink.js +1 -0
  24. package/dist/ui/components/bottom-sheet.d.ts +15 -0
  25. package/dist/ui/components/bottom-sheet.js +192 -0
  26. package/dist/ui/components/button.js +2 -1
  27. package/dist/ui/components/card.d.ts +5 -0
  28. package/dist/ui/components/card.js +74 -0
  29. package/dist/ui/components/checkbox.d.ts +1 -1
  30. package/dist/ui/components/checkbox.js +2 -1
  31. package/dist/ui/components/command-block.js +4 -3
  32. package/dist/ui/components/confirm-dialog.d.ts +13 -0
  33. package/dist/ui/components/confirm-dialog.js +113 -0
  34. package/dist/ui/components/cursor.js +1 -0
  35. package/dist/ui/components/dialog.d.ts +15 -0
  36. package/dist/ui/components/dialog.js +171 -0
  37. package/dist/ui/components/dropdown-menu.js +1 -0
  38. package/dist/ui/components/fit-text/index.js +1 -0
  39. package/dist/ui/components/graphs/bar-chart.js +1 -0
  40. package/dist/ui/components/graphs/index.js +1 -0
  41. package/dist/ui/components/graphs/line-chart.js +1 -0
  42. package/dist/ui/components/graphs/utils.js +1 -0
  43. package/dist/ui/components/grid/index.js +1 -0
  44. package/dist/ui/components/hover-bg.js +1 -0
  45. package/dist/ui/components/icons/arrow.js +1 -0
  46. package/dist/ui/components/icons/check.js +1 -0
  47. package/dist/ui/components/icons/chevron.js +1 -0
  48. package/dist/ui/components/icons/discord.js +1 -0
  49. package/dist/ui/components/icons/eye.js +1 -0
  50. package/dist/ui/components/icons/gear.js +1 -0
  51. package/dist/ui/components/icons/github.js +1 -0
  52. package/dist/ui/components/icons/hamburger.js +1 -0
  53. package/dist/ui/components/icons/heart.js +1 -0
  54. package/dist/ui/components/icons/index.js +1 -0
  55. package/dist/ui/components/icons/link.js +1 -0
  56. package/dist/ui/components/icons/minus.js +1 -0
  57. package/dist/ui/components/icons/search.js +1 -0
  58. package/dist/ui/components/image-distortion.js +1 -0
  59. package/dist/ui/components/input.d.ts +1 -0
  60. package/dist/ui/components/input.js +21 -0
  61. package/dist/ui/components/label.d.ts +1 -0
  62. package/dist/ui/components/label.js +18 -0
  63. package/dist/ui/components/leva-client.js +1 -0
  64. package/dist/ui/components/list-item.js +3 -2
  65. package/dist/ui/components/overlays/blend-modes.js +1 -0
  66. package/dist/ui/components/overlays/glitch.js +1 -0
  67. package/dist/ui/components/overlays/greys.js +1 -0
  68. package/dist/ui/components/overlays/index.js +1 -0
  69. package/dist/ui/components/overlays/lens-layers.js +1 -0
  70. package/dist/ui/components/overlays/lens.js +1 -0
  71. package/dist/ui/components/overlays/noise.js +1 -0
  72. package/dist/ui/components/overlays/vignette.js +1 -0
  73. package/dist/ui/components/poster.js +1 -0
  74. package/dist/ui/components/progress.js +1 -0
  75. package/dist/ui/components/scene-canvas.js +1 -0
  76. package/dist/ui/components/scramble.js +1 -0
  77. package/dist/ui/components/segmented.js +5 -4
  78. package/dist/ui/components/select.js +1 -0
  79. package/dist/ui/components/selection-switcher.js +1 -0
  80. package/dist/ui/components/separator.d.ts +5 -0
  81. package/dist/ui/components/separator.js +22 -0
  82. package/dist/ui/components/shader.js +1 -0
  83. package/dist/ui/components/socials.js +1 -0
  84. package/dist/ui/components/spinner.js +1 -0
  85. package/dist/ui/components/stats.js +2 -1
  86. package/dist/ui/components/switch.js +1 -0
  87. package/dist/ui/components/tabs.js +4 -3
  88. package/dist/ui/components/terminal-demo.js +2 -1
  89. package/dist/ui/components/theme-toggle.js +1 -0
  90. package/dist/ui/components/tier-card.js +2 -1
  91. package/dist/ui/components/toast.d.ts +8 -0
  92. package/dist/ui/components/toast.js +39 -0
  93. package/dist/ui/components/tv.js +1 -0
  94. package/dist/ui/components/typography/h1.js +1 -0
  95. package/dist/ui/components/typography/h2.js +1 -0
  96. package/dist/ui/components/typography/index.js +1 -0
  97. package/dist/ui/components/typography/legend.js +1 -0
  98. package/dist/ui/components/typography/small.js +1 -0
  99. package/dist/ui/components/watchlist.js +2 -1
  100. package/dist/ui/footer.js +1 -0
  101. package/dist/ui/globals.css +47 -3
  102. package/dist/ui/header.js +1 -0
  103. package/dist/ui/layout-wrapper.js +2 -1
  104. package/dist/utils/color.js +1 -0
  105. package/dist/utils/index.js +1 -0
  106. package/dist/utils/poly.js +1 -0
  107. package/package.json +5 -3
  108. package/src/assets/filler-bg0.webp +0 -0
  109. package/src/assets.d.ts +38 -0
  110. package/src/fonts/Collapse-Bold.woff2 +0 -0
  111. package/src/fonts/Collapse-BoldItalic.woff2 +0 -0
  112. package/src/fonts/Collapse-Italic.woff2 +0 -0
  113. package/src/fonts/Collapse-Light.woff2 +0 -0
  114. package/src/fonts/Collapse-LightItalic.woff2 +0 -0
  115. package/src/fonts/Collapse-Regular.woff2 +0 -0
  116. package/src/fonts/Collapse-Thin.woff2 +0 -0
  117. package/src/fonts/Collapse-ThinItalic.woff2 +0 -0
  118. package/src/fonts/Mondwest-Regular.woff2 +0 -0
  119. package/src/fonts/Neuebit-Bold.woff2 +0 -0
  120. package/src/fonts/RulesCompressed-Medium.woff2 +0 -0
  121. package/src/fonts/RulesCompressed-Regular.woff2 +0 -0
  122. package/src/fonts/RulesExpanded-Bold.woff2 +0 -0
  123. package/src/fonts/RulesExpanded-Regular.woff2 +0 -0
  124. package/src/fonts.ts +6 -0
  125. package/src/hooks/use-below-breakpoint.ts +21 -0
  126. package/src/hooks/use-capped-frame.ts +18 -0
  127. package/src/hooks/use-confirm-delete.ts +43 -0
  128. package/src/hooks/use-css-var-dims.ts +39 -0
  129. package/src/hooks/use-gpu-tier.ts +165 -0
  130. package/src/hooks/use-render-loop.ts +121 -0
  131. package/src/hooks/use-smooth-controls.ts +318 -0
  132. package/src/hooks/use-toast.ts +29 -0
  133. package/src/index.ts +130 -0
  134. package/src/ui/basic-page.tsx +34 -0
  135. package/src/ui/build.css +4 -0
  136. package/src/ui/components/animated-count.stories.tsx +67 -0
  137. package/src/ui/components/animated-count.tsx +168 -0
  138. package/src/ui/components/ascii.stories.tsx +30 -0
  139. package/src/ui/components/ascii.tsx +110 -0
  140. package/src/ui/components/badge.stories.tsx +31 -0
  141. package/src/ui/components/badge.tsx +60 -0
  142. package/src/ui/components/badges/nous-girl.tsx +52 -0
  143. package/src/ui/components/blend-mode.stories.tsx +33 -0
  144. package/src/ui/components/blend-mode.tsx +129 -0
  145. package/src/ui/components/blink.stories.tsx +32 -0
  146. package/src/ui/components/blink.tsx +21 -0
  147. package/src/ui/components/bottom-sheet.stories.tsx +43 -0
  148. package/src/ui/components/bottom-sheet.tsx +227 -0
  149. package/src/ui/components/button.stories.tsx +68 -0
  150. package/src/ui/components/button.tsx +170 -0
  151. package/src/ui/components/card.stories.tsx +63 -0
  152. package/src/ui/components/card.tsx +85 -0
  153. package/src/ui/components/checkbox.stories.tsx +113 -0
  154. package/src/ui/components/checkbox.tsx +36 -0
  155. package/src/ui/components/command-block.stories.tsx +52 -0
  156. package/src/ui/components/command-block.tsx +86 -0
  157. package/src/ui/components/confirm-dialog.stories.tsx +91 -0
  158. package/src/ui/components/confirm-dialog.tsx +130 -0
  159. package/src/ui/components/cursor.tsx +115 -0
  160. package/src/ui/components/dialog.stories.tsx +169 -0
  161. package/src/ui/components/dialog.tsx +177 -0
  162. package/src/ui/components/dropdown-menu.stories.tsx +52 -0
  163. package/src/ui/components/dropdown-menu.tsx +117 -0
  164. package/src/ui/components/fit-text/fit-text.css +42 -0
  165. package/src/ui/components/fit-text/index.stories.tsx +33 -0
  166. package/src/ui/components/fit-text/index.tsx +45 -0
  167. package/src/ui/components/forms.stories.tsx +173 -0
  168. package/src/ui/components/graphs/bar-chart.tsx +153 -0
  169. package/src/ui/components/graphs/index.stories.tsx +64 -0
  170. package/src/ui/components/graphs/index.tsx +4 -0
  171. package/src/ui/components/graphs/line-chart.tsx +213 -0
  172. package/src/ui/components/graphs/utils.tsx +265 -0
  173. package/src/ui/components/grid/grid.css +79 -0
  174. package/src/ui/components/grid/index.tsx +19 -0
  175. package/src/ui/components/hover-bg.stories.tsx +29 -0
  176. package/src/ui/components/hover-bg.tsx +15 -0
  177. package/src/ui/components/icons/arrow.tsx +42 -0
  178. package/src/ui/components/icons/check.tsx +14 -0
  179. package/src/ui/components/icons/chevron.tsx +45 -0
  180. package/src/ui/components/icons/discord.tsx +16 -0
  181. package/src/ui/components/icons/eye.tsx +12 -0
  182. package/src/ui/components/icons/gear.tsx +51 -0
  183. package/src/ui/components/icons/github.tsx +16 -0
  184. package/src/ui/components/icons/hamburger.tsx +52 -0
  185. package/src/ui/components/icons/heart.tsx +12 -0
  186. package/src/ui/components/icons/index.ts +12 -0
  187. package/src/ui/components/icons/link.tsx +14 -0
  188. package/src/ui/components/icons/minus.tsx +14 -0
  189. package/src/ui/components/icons/search.tsx +28 -0
  190. package/src/ui/components/image-distortion.stories.tsx +120 -0
  191. package/src/ui/components/image-distortion.tsx +498 -0
  192. package/src/ui/components/input.stories.tsx +39 -0
  193. package/src/ui/components/input.tsx +20 -0
  194. package/src/ui/components/label.stories.tsx +26 -0
  195. package/src/ui/components/label.tsx +16 -0
  196. package/src/ui/components/leva-client.tsx +14 -0
  197. package/src/ui/components/list-item.stories.tsx +83 -0
  198. package/src/ui/components/list-item.tsx +37 -0
  199. package/src/ui/components/overlays/blend-modes.ts +13 -0
  200. package/src/ui/components/overlays/glitch.tsx +243 -0
  201. package/src/ui/components/overlays/greys.tsx +386 -0
  202. package/src/ui/components/overlays/index.tsx +47 -0
  203. package/src/ui/components/overlays/lens-layers.tsx +119 -0
  204. package/src/ui/components/overlays/lens.ts +91 -0
  205. package/src/ui/components/overlays/noise.tsx +174 -0
  206. package/src/ui/components/overlays/vignette.tsx +60 -0
  207. package/src/ui/components/poster.stories.tsx +513 -0
  208. package/src/ui/components/poster.tsx +411 -0
  209. package/src/ui/components/progress.stories.tsx +48 -0
  210. package/src/ui/components/progress.tsx +56 -0
  211. package/src/ui/components/scene-canvas.tsx +254 -0
  212. package/src/ui/components/scramble.stories.tsx +49 -0
  213. package/src/ui/components/scramble.tsx +95 -0
  214. package/src/ui/components/segmented.stories.tsx +101 -0
  215. package/src/ui/components/segmented.tsx +81 -0
  216. package/src/ui/components/select.stories.tsx +88 -0
  217. package/src/ui/components/select.tsx +267 -0
  218. package/src/ui/components/selection-switcher.tsx +44 -0
  219. package/src/ui/components/separator.stories.tsx +33 -0
  220. package/src/ui/components/separator.tsx +24 -0
  221. package/src/ui/components/shader.tsx +83 -0
  222. package/src/ui/components/socials.tsx +42 -0
  223. package/src/ui/components/spinner.stories.tsx +101 -0
  224. package/src/ui/components/spinner.tsx +60 -0
  225. package/src/ui/components/stats.stories.tsx +24 -0
  226. package/src/ui/components/stats.tsx +53 -0
  227. package/src/ui/components/switch.stories.tsx +77 -0
  228. package/src/ui/components/switch.tsx +48 -0
  229. package/src/ui/components/tabs.stories.tsx +101 -0
  230. package/src/ui/components/tabs.tsx +66 -0
  231. package/src/ui/components/terminal-demo.stories.tsx +67 -0
  232. package/src/ui/components/terminal-demo.tsx +189 -0
  233. package/src/ui/components/theme-toggle.stories.tsx +47 -0
  234. package/src/ui/components/theme-toggle.tsx +66 -0
  235. package/src/ui/components/tier-card.stories.tsx +217 -0
  236. package/src/ui/components/tier-card.tsx +190 -0
  237. package/src/ui/components/toast.stories.tsx +55 -0
  238. package/src/ui/components/toast.tsx +49 -0
  239. package/src/ui/components/tv.stories.tsx +37 -0
  240. package/src/ui/components/tv.tsx +257 -0
  241. package/src/ui/components/typography/h1.tsx +18 -0
  242. package/src/ui/components/typography/h2.tsx +18 -0
  243. package/src/ui/components/typography/index.tsx +54 -0
  244. package/src/ui/components/typography/legend.tsx +24 -0
  245. package/src/ui/components/typography/small.tsx +11 -0
  246. package/src/ui/components/watchlist.stories.tsx +33 -0
  247. package/src/ui/components/watchlist.tsx +105 -0
  248. package/src/ui/fonts.css +63 -0
  249. package/src/ui/footer.tsx +111 -0
  250. package/src/ui/globals.css +395 -0
  251. package/src/ui/header.tsx +398 -0
  252. package/src/ui/layout-wrapper.tsx +11 -0
  253. package/src/utils/color.ts +21 -0
  254. package/src/utils/index.ts +62 -0
  255. package/src/utils/poly.ts +26 -0
  256. package/dist/ui/components/modal/index.d.ts +0 -8
  257. package/dist/ui/components/modal/index.js +0 -34
  258. package/dist/ui/components/modal/modal.css +0 -36
@@ -0,0 +1,39 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+
3
+ import { Input } from './input'
4
+ import { Label } from './label'
5
+
6
+ const meta: Meta<typeof Input> = {
7
+ component: Input,
8
+ title: 'Components/Forms/Input'
9
+ }
10
+
11
+ export default meta
12
+
13
+ type Story = StoryObj<typeof Input>
14
+
15
+ export const Playground: Story = {
16
+ render: () => <Input placeholder="Enter a value…" />
17
+ }
18
+
19
+ export const Disabled: Story = {
20
+ render: () => <Input disabled placeholder="Disabled" value="locked" />
21
+ }
22
+
23
+ export const WithLabel: Story = {
24
+ render: () => (
25
+ <div className="grid w-64 gap-1.5">
26
+ <Label htmlFor="demo-input">Model name</Label>
27
+ <Input id="demo-input" placeholder="e.g. gpt-4o" />
28
+ </div>
29
+ )
30
+ }
31
+
32
+ export const NumberInput: Story = {
33
+ render: () => (
34
+ <div className="grid w-40 gap-1.5">
35
+ <Label htmlFor="demo-number">Temperature</Label>
36
+ <Input id="demo-number" type="number" step={0.1} defaultValue={0.7} />
37
+ </div>
38
+ )
39
+ }
@@ -0,0 +1,20 @@
1
+ import { cn } from '../../utils'
2
+
3
+ export function Input({
4
+ className,
5
+ ...props
6
+ }: React.InputHTMLAttributes<HTMLInputElement>) {
7
+ return (
8
+ <input
9
+ className={cn(
10
+ 'flex h-9 w-full border border-midground/15 bg-background/40 px-3 py-1 font-courier text-sm transition-colors',
11
+ 'placeholder:text-midground/50',
12
+ 'focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-midground/30 focus-visible:border-midground/25',
13
+ 'disabled:cursor-not-allowed disabled:opacity-50',
14
+ className
15
+ )}
16
+ {...props}
17
+ />
18
+ )
19
+ }
20
+
@@ -0,0 +1,26 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+
3
+ import { Input } from './input'
4
+ import { Label } from './label'
5
+
6
+ const meta: Meta<typeof Label> = {
7
+ component: Label,
8
+ title: 'Components/Forms/Label'
9
+ }
10
+
11
+ export default meta
12
+
13
+ type Story = StoryObj<typeof Label>
14
+
15
+ export const Playground: Story = {
16
+ render: () => <Label>Field label</Label>
17
+ }
18
+
19
+ export const WithInput: Story = {
20
+ render: () => (
21
+ <div className="grid w-64 gap-1.5">
22
+ <Label htmlFor="label-demo">API key</Label>
23
+ <Input id="label-demo" type="password" placeholder="sk-…" />
24
+ </div>
25
+ )
26
+ }
@@ -0,0 +1,16 @@
1
+ import { cn } from '../../utils'
2
+
3
+ export function Label({
4
+ className,
5
+ ...props
6
+ }: React.LabelHTMLAttributes<HTMLLabelElement>) {
7
+ return (
8
+ <label
9
+ className={cn(
10
+ 'font-mondwest text-xs tracking-[0.1em] uppercase leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
11
+ className
12
+ )}
13
+ {...props}
14
+ />
15
+ )
16
+ }
@@ -0,0 +1,14 @@
1
+ 'use client'
2
+
3
+ import { Leva } from 'leva'
4
+ import { useEffect, useState } from 'react'
5
+
6
+ export function LevaClient() {
7
+ const [hidden, setHidden] = useState(true)
8
+
9
+ useEffect(() => {
10
+ setHidden(!new URLSearchParams(window.location.search).has('dev'))
11
+ }, [])
12
+
13
+ return <Leva {...{ hidden }} />
14
+ }
@@ -0,0 +1,83 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { useState } from 'react'
3
+
4
+ import { ListItem } from './list-item'
5
+
6
+ const PROVIDERS = [
7
+ { count: 412, name: 'OpenAI', slug: 'openai' },
8
+ { count: 38, name: 'Anthropic', slug: 'anthropic' },
9
+ { count: 124, name: 'Google', slug: 'google' },
10
+ { count: 7, name: 'Mistral', slug: 'mistral' },
11
+ { count: 4, name: 'xAI', slug: 'xai' }
12
+ ]
13
+
14
+ function Demo() {
15
+ const [active, setActive] = useState<string>('anthropic')
16
+
17
+ return (
18
+ <div className="w-72 border border-midground/15 bg-background-base">
19
+ {PROVIDERS.map(p => (
20
+ <ListItem
21
+ active={p.slug === active}
22
+ key={p.slug}
23
+ onClick={() => setActive(p.slug)}
24
+ >
25
+ <span className="flex-1 truncate">{p.name}</span>
26
+ <span className="text-[0.65rem] tabular-nums text-midground/50">
27
+ {p.count}
28
+ </span>
29
+ </ListItem>
30
+ ))}
31
+ </div>
32
+ )
33
+ }
34
+
35
+ const meta: Meta<typeof ListItem> = {
36
+ component: ListItem,
37
+ title: 'Components/Data Display/ListItem'
38
+ }
39
+
40
+ export default meta
41
+
42
+ type Story = StoryObj<typeof ListItem>
43
+
44
+ export const Playground: Story = { render: () => <Demo /> }
45
+
46
+ export const WithSubtitle: Story = {
47
+ render: () => {
48
+ function MultiLineDemo() {
49
+ const [active, setActive] = useState<string>('anthropic')
50
+
51
+ return (
52
+ <div className="w-80 border border-midground/15 bg-background-base">
53
+ {PROVIDERS.map(p => (
54
+ <ListItem
55
+ active={p.slug === active}
56
+ key={p.slug}
57
+ onClick={() => setActive(p.slug)}
58
+ >
59
+ <div className="flex-1 min-w-0">
60
+ <div className="truncate font-medium">{p.name}</div>
61
+ <div className="truncate text-[0.65rem] text-midground/60">
62
+ {p.slug} · {p.count} models
63
+ </div>
64
+ </div>
65
+ </ListItem>
66
+ ))}
67
+ </div>
68
+ )
69
+ }
70
+
71
+ return <MultiLineDemo />
72
+ }
73
+ }
74
+
75
+ export const Disabled: Story = {
76
+ render: () => (
77
+ <div className="w-72 border border-midground/15 bg-background-base">
78
+ <ListItem>Enabled item</ListItem>
79
+ <ListItem disabled>Disabled item</ListItem>
80
+ <ListItem active>Active item</ListItem>
81
+ </div>
82
+ )
83
+ }
@@ -0,0 +1,37 @@
1
+ 'use client'
2
+
3
+ import { forwardRef, type ButtonHTMLAttributes } from 'react'
4
+
5
+ import { cn } from '../../utils'
6
+
7
+ export const ListItem = forwardRef<HTMLButtonElement, ListItemProps>(
8
+ function ListItem(
9
+ { active = false, children, className, type = 'button', ...props },
10
+ ref
11
+ ) {
12
+ return (
13
+ <button
14
+ className={cn(
15
+ 'group relative flex w-full items-center gap-2 px-3 py-2 text-left',
16
+ 'font-courier text-sm transition-colors cursor-pointer',
17
+ 'focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-midground/30',
18
+ 'disabled:cursor-not-allowed disabled:text-text-disabled',
19
+ active
20
+ ? 'bg-midground/10 text-midground'
21
+ : 'text-text-secondary hover:text-midground hover:bg-midground/5',
22
+ className
23
+ )}
24
+ data-active={active || undefined}
25
+ ref={ref}
26
+ type={type}
27
+ {...props}
28
+ >
29
+ {children}
30
+ </button>
31
+ )
32
+ }
33
+ )
34
+
35
+ interface ListItemProps extends ButtonHTMLAttributes<HTMLButtonElement> {
36
+ active?: boolean
37
+ }
@@ -0,0 +1,13 @@
1
+ export const BLEND_MODES = [
2
+ 'overlay',
3
+ 'multiply',
4
+ 'screen',
5
+ 'difference',
6
+ 'exclusion',
7
+ 'color-dodge',
8
+ 'color-burn',
9
+ 'hard-light',
10
+ 'soft-light',
11
+ 'darken',
12
+ 'lighten'
13
+ ] as unknown as React.CSSProperties['mixBlendMode'][]
@@ -0,0 +1,243 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useRef } from 'react'
4
+ import * as THREE from 'three'
5
+
6
+ import { $gpuTier, useGpuTier } from '../../../hooks/use-gpu-tier'
7
+ import { runRenderLoop } from '../../../hooks/use-render-loop'
8
+ import { useSmoothControls } from '../../../hooks/use-smooth-controls'
9
+ import { cn } from '../../../utils'
10
+
11
+ import { BLEND_MODES } from './blend-modes'
12
+
13
+ const vert = /*glsl*/ `
14
+ varying vec2 vUv;
15
+ void main() {
16
+ vUv = uv;
17
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
18
+ }
19
+ `
20
+
21
+ const frag = /*glsl*/ `
22
+ uniform float uTime, uAlpha, uIntensity, uChroma, uSpeed, uSparsity;
23
+ uniform vec3 uColor;
24
+ varying vec2 vUv;
25
+
26
+ float hash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); }
27
+
28
+ vec2 hash2(vec2 p) {
29
+ vec3 q = fract(vec3(p.xyx) * vec3(.1031, .1030, .0973));
30
+ q += dot(q, q.yzx + 33.33);
31
+ return fract((q.xx + q.yz) * q.zy);
32
+ }
33
+
34
+ float dither(vec2 p, float a) {
35
+ return step(mod(floor(p.x) + floor(p.y) * 2.0, 4.0) / 4.0, a);
36
+ }
37
+
38
+ void main() {
39
+ vec3 col = vec3(0.0);
40
+ float t = uTime * uSpeed;
41
+ float tSlow = floor(t / 3.0);
42
+ float dit = dither(gl_FragCoord.xy * 0.5, 0.7);
43
+
44
+ for (float i = 0.0; i < 6.0; i++) {
45
+ float seed = i * 137.3;
46
+ float epoch = floor((t + hash(vec2(seed, 77.7)) * 200.0) / (4.0 + hash(vec2(seed, 0.0)) * 6.0));
47
+ float life = fract((t + hash(vec2(seed, 77.7)) * 200.0) / (4.0 + hash(vec2(seed, 0.0)) * 6.0));
48
+
49
+ if (hash(vec2(epoch, seed)) > 1.0 - uSparsity * 0.7) continue;
50
+
51
+ vec2 center = vec2(hash(vec2(epoch, seed + 13.1)), hash(vec2(epoch, seed + 27.3)));
52
+ vec2 size = vec2(0.015 + hash(vec2(epoch, seed + 41.5)) * 0.08, 0.008 + hash(vec2(epoch, seed + 53.7)) * 0.04);
53
+ vec2 d = abs(vUv - center);
54
+
55
+ if (d.x < size.x && d.y < size.y) {
56
+ float fade = smoothstep(0.0, 0.05, life) * smoothstep(1.0, 0.95, life);
57
+ vec2 gUV = vUv + (hash2(vec2(epoch, seed + 200.0)) - 0.5) * 0.08 * uIntensity;
58
+ float shift = uChroma * 0.015 * (sin(t * 2.0 + hash(vec2(epoch, seed)) * 6.28) * 0.3 + 0.7);
59
+
60
+ col += uColor * vec3(
61
+ hash(gUV * 50.0 + vec2(shift, 0.0) + epoch),
62
+ hash(gUV * 50.0 + epoch),
63
+ hash(gUV * 50.0 - vec2(shift, 0.0) + epoch)
64
+ ) * dither(gl_FragCoord.xy * 0.5, fade * 0.8 + 0.2) * uIntensity * 0.7;
65
+ }
66
+ }
67
+
68
+ for (float i = 0.0; i < 12.0; i++) {
69
+ float seed = i * 211.7 + 1000.0;
70
+ float epoch = floor((t + hash(vec2(seed, 77.7)) * 150.0) / (3.0 + hash(vec2(seed, 0.0)) * 5.0));
71
+ float life = fract((t + hash(vec2(seed, 77.7)) * 150.0) / (3.0 + hash(vec2(seed, 0.0)) * 5.0));
72
+
73
+ if (hash(vec2(epoch, seed)) > 1.0 - uSparsity * 0.5) continue;
74
+
75
+ vec2 pos = vec2(hash(vec2(epoch, seed + 13.1)), hash(vec2(epoch, seed + 27.3)));
76
+ float px = 0.003 + hash(vec2(epoch, seed + 41.5)) * 0.008;
77
+
78
+ if (abs(vUv.x - pos.x) < px && abs(vUv.y - pos.y) < px) {
79
+ float fade = smoothstep(0.0, 0.1, life) * smoothstep(1.0, 0.9, life);
80
+ vec3 c = uColor;
81
+ float cs = hash(vec2(epoch, seed + 700.0));
82
+
83
+ if (cs < 0.2) c.r *= 1.8 * uChroma;
84
+ else if (cs < 0.4) c.b *= 1.8 * uChroma;
85
+
86
+ col += c * dither(gl_FragCoord.xy * 0.5, fade * 0.9) * uIntensity;
87
+ }
88
+ }
89
+
90
+ float tearSize = 25.0 + uSparsity * 10.0;
91
+ float tearThresh = 0.85 + uSparsity * 0.1;
92
+
93
+ float hY = floor(vUv.y * tearSize);
94
+ if (step(tearThresh, hash(vec2(hY, tSlow))) > 0.0) {
95
+ float shift = (hash(vec2(hY, tSlow + 50.0)) - 0.5) * 0.25 * uIntensity;
96
+ col += uColor * step(0.4, hash(vec2(vUv.x + shift, hY + tSlow))) * dit * uIntensity * 0.5;
97
+ }
98
+
99
+ float vX = floor(vUv.x * tearSize);
100
+ if (step(tearThresh, hash(vec2(vX, tSlow + 100.0))) > 0.0) {
101
+ float shift = (hash(vec2(vX, tSlow + 150.0)) - 0.5) * 0.25 * uIntensity;
102
+ col += uColor * step(0.4, hash(vec2(vX + tSlow, vUv.y + shift))) * dit * uIntensity * 0.5;
103
+ }
104
+
105
+ gl_FragColor = vec4(col * uAlpha, max(col.r, max(col.g, col.b)) * uAlpha);
106
+ }
107
+ `
108
+
109
+ export function Glitch({ className, style }: GlitchProps) {
110
+ const gpuTier = useGpuTier()
111
+
112
+ const c = useSmoothControls(
113
+ 'Effects/Glitch',
114
+ {
115
+ alpha: { max: 2, min: 0, step: 0.01, value: 0.25 },
116
+ blend: { options: BLEND_MODES, value: 'difference' },
117
+ chroma: { max: 3, min: 0, step: 0.01, value: 1.17 },
118
+ color: { value: '#ffe6cb' },
119
+ enabled: { value: true },
120
+ intensity: { max: 1, min: 0, step: 0.01, value: 0.59 },
121
+ sparsity: { max: 1, min: 0, step: 0.01, value: 0.21 },
122
+ speed: { max: 10, min: 0.1, step: 0.1, value: 1 }
123
+ },
124
+ { collapsed: true }
125
+ )
126
+
127
+ const ref = useRef<HTMLCanvasElement>(null)
128
+ const cRef = useRef(c)
129
+ cRef.current = c
130
+
131
+ const enabled = c.enabled && gpuTier > 0
132
+
133
+ useEffect(() => {
134
+ if (!ref.current || !enabled) {
135
+ return
136
+ }
137
+
138
+ let renderer: THREE.WebGLRenderer
139
+
140
+ try {
141
+ renderer = new THREE.WebGLRenderer({
142
+ alpha: true,
143
+ canvas: ref.current
144
+ })
145
+ } catch {
146
+ // See note in noise.tsx — eager gpu-tier detection should keep us
147
+ // out of here, but if the driver fails the renderer constructor
148
+ // anyway, downgrade so other overlays stop trying too.
149
+ $gpuTier.set(0)
150
+
151
+ return
152
+ }
153
+
154
+ const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1)
155
+ const geo = new THREE.PlaneGeometry(2, 2)
156
+ const scene = new THREE.Scene()
157
+
158
+ const mat = new THREE.ShaderMaterial({
159
+ fragmentShader: frag,
160
+ transparent: true,
161
+ uniforms: {
162
+ uAlpha: { value: c.alpha },
163
+ uChroma: { value: c.chroma },
164
+ uColor: { value: new THREE.Color(c.color) },
165
+ uIntensity: { value: c.intensity },
166
+ uSparsity: { value: c.sparsity },
167
+ uSpeed: { value: c.speed },
168
+ uTime: { value: 0 }
169
+ },
170
+ vertexShader: vert
171
+ })
172
+
173
+ scene.add(new THREE.Mesh(geo, mat))
174
+
175
+ const resize = () => {
176
+ renderer.setSize(innerWidth, innerHeight)
177
+ // Cap DPR at 1.5 — at full retina (2x) the glitch shader is one
178
+ // of the heaviest fillrate consumers in the app, and the visual
179
+ // difference is tiny because it's a chromatic-noise effect.
180
+ renderer.setPixelRatio(Math.min(devicePixelRatio, 1.5))
181
+ }
182
+
183
+ resize()
184
+ window.addEventListener('resize', resize)
185
+
186
+ let time = 0
187
+
188
+ // gpu-tier 1 → ~10fps (legacy), gpu-tier 2 → ~30fps (was 60fps).
189
+ // Glitch is a background ambient effect; users won't notice 30 vs
190
+ // 60 but the GPU absolutely will.
191
+ const minIntervalMs = gpuTier === 1 ? 100 : 33
192
+
193
+ const dispose = runRenderLoop({
194
+ el: ref.current,
195
+ minIntervalMs,
196
+ onFrame: deltaSeconds => {
197
+ time += deltaSeconds
198
+
199
+ const v = cRef.current
200
+
201
+ mat.uniforms.uTime.value = time
202
+ mat.uniforms.uAlpha.value = v.alpha
203
+ mat.uniforms.uChroma.value = v.chroma
204
+ mat.uniforms.uIntensity.value = v.intensity
205
+ mat.uniforms.uSparsity.value = v.sparsity
206
+ mat.uniforms.uSpeed.value = v.speed
207
+ mat.uniforms.uColor.value.set(v.color)
208
+
209
+ renderer.render(scene, camera)
210
+ }
211
+ })
212
+
213
+ return () => {
214
+ window.removeEventListener('resize', resize)
215
+ dispose()
216
+
217
+ mat.dispose()
218
+ geo.dispose()
219
+ renderer.dispose()
220
+ }
221
+ // eslint-disable-next-line react-hooks/exhaustive-deps
222
+ }, [enabled, gpuTier])
223
+
224
+ if (!enabled) {
225
+ return null
226
+ }
227
+
228
+ return (
229
+ <canvas
230
+ className={cn('h-full w-full', className)}
231
+ ref={ref}
232
+ style={{
233
+ mixBlendMode: c.blend as React.CSSProperties['mixBlendMode'],
234
+ ...style
235
+ }}
236
+ />
237
+ )
238
+ }
239
+
240
+ interface GlitchProps {
241
+ className?: string
242
+ style?: React.CSSProperties
243
+ }