@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,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
+ }
@@ -0,0 +1,77 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { useState } from 'react'
3
+
4
+ import { Switch } from './switch'
5
+ import { Small } from './typography/small'
6
+
7
+ function Demo({ disabled }: { disabled?: boolean }) {
8
+ const [checked, setChecked] = useState(false)
9
+
10
+ return (
11
+ <Switch checked={checked} disabled={disabled} onCheckedChange={setChecked} />
12
+ )
13
+ }
14
+
15
+ const meta: Meta<typeof Switch> = {
16
+ component: Switch,
17
+ title: 'Components/Forms/Switch'
18
+ }
19
+
20
+ export default meta
21
+
22
+ type Story = StoryObj<typeof Switch>
23
+
24
+ export const Playground: Story = { render: () => <Demo /> }
25
+
26
+ export const Disabled: Story = { render: () => <Demo disabled /> }
27
+
28
+ export const NextToLabel: Story = {
29
+ render: () => {
30
+ function LabeledDemo() {
31
+ const [checked, setChecked] = useState(true)
32
+
33
+ return (
34
+ <label className="flex items-center gap-3">
35
+ <Small className="opacity-60 uppercase tracking-wider">
36
+ Enable feature
37
+ </Small>
38
+
39
+ <Switch checked={checked} onCheckedChange={setChecked} />
40
+ </label>
41
+ )
42
+ }
43
+
44
+ return <LabeledDemo />
45
+ }
46
+ }
47
+
48
+ export const Stack: Story = {
49
+ render: () => {
50
+ function StackDemo() {
51
+ const [a, setA] = useState(true)
52
+ const [b, setB] = useState(false)
53
+ const [c, setC] = useState(true)
54
+
55
+ return (
56
+ <div className="grid w-64 gap-3">
57
+ <label className="flex items-center justify-between">
58
+ <Small className="opacity-60 uppercase tracking-wider">Logging</Small>
59
+ <Switch checked={a} onCheckedChange={setA} />
60
+ </label>
61
+
62
+ <label className="flex items-center justify-between">
63
+ <Small className="opacity-60 uppercase tracking-wider">Telemetry</Small>
64
+ <Switch checked={b} onCheckedChange={setB} />
65
+ </label>
66
+
67
+ <label className="flex items-center justify-between">
68
+ <Small className="opacity-60 uppercase tracking-wider">Auto-update</Small>
69
+ <Switch checked={c} onCheckedChange={setC} />
70
+ </label>
71
+ </div>
72
+ )
73
+ }
74
+
75
+ return <StackDemo />
76
+ }
77
+ }
@@ -0,0 +1,48 @@
1
+ 'use client'
2
+
3
+ import { type ButtonHTMLAttributes, forwardRef } from 'react'
4
+
5
+ import { cn } from '../../utils'
6
+
7
+ export const Switch = forwardRef<HTMLButtonElement, SwitchProps>(function Switch(
8
+ { checked, className, disabled, id, onCheckedChange, ...props },
9
+ ref
10
+ ) {
11
+ return (
12
+ <button
13
+ aria-checked={checked}
14
+ className={cn(
15
+ 'peer inline-flex h-5 w-9 shrink-0 items-center border transition-colors cursor-pointer',
16
+ 'focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-midground/30',
17
+ 'disabled:cursor-not-allowed disabled:opacity-50',
18
+ checked
19
+ ? 'bg-midground/15 border-midground/30'
20
+ : 'bg-background border-midground/20',
21
+ className
22
+ )}
23
+ disabled={disabled}
24
+ id={id}
25
+ onClick={() => onCheckedChange(!checked)}
26
+ ref={ref}
27
+ role="switch"
28
+ type="button"
29
+ {...props}
30
+ >
31
+ <span
32
+ aria-hidden
33
+ className={cn(
34
+ 'pointer-events-none block h-3.5 w-3.5 transition-transform',
35
+ checked
36
+ ? 'translate-x-4 bg-midground'
37
+ : 'translate-x-0.5 bg-midground/40'
38
+ )}
39
+ />
40
+ </button>
41
+ )
42
+ })
43
+
44
+ interface SwitchProps
45
+ extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'onChange'> {
46
+ checked: boolean
47
+ onCheckedChange: (checked: boolean) => void
48
+ }
@@ -0,0 +1,101 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+
3
+ import { Tabs, TabsList, TabsTrigger } from './tabs'
4
+ import { Small } from './typography/small'
5
+
6
+ const meta: Meta<typeof Tabs> = {
7
+ component: Tabs,
8
+ title: 'Components/Forms/Tabs'
9
+ }
10
+
11
+ export default meta
12
+
13
+ type Story = StoryObj<typeof Tabs>
14
+
15
+ export const Playground: Story = {
16
+ render: () => (
17
+ <div className="w-[28rem]">
18
+ <Tabs defaultValue="overview">
19
+ {(active, setActive) => (
20
+ <>
21
+ <TabsList>
22
+ <TabsTrigger
23
+ active={active === 'overview'}
24
+ onClick={() => setActive('overview')}
25
+ value="overview"
26
+ >
27
+ Overview
28
+ </TabsTrigger>
29
+
30
+ <TabsTrigger
31
+ active={active === 'metrics'}
32
+ onClick={() => setActive('metrics')}
33
+ value="metrics"
34
+ >
35
+ Metrics
36
+ </TabsTrigger>
37
+
38
+ <TabsTrigger
39
+ active={active === 'logs'}
40
+ onClick={() => setActive('logs')}
41
+ value="logs"
42
+ >
43
+ Logs
44
+ </TabsTrigger>
45
+
46
+ <TabsTrigger
47
+ active={active === 'settings'}
48
+ onClick={() => setActive('settings')}
49
+ value="settings"
50
+ >
51
+ Settings
52
+ </TabsTrigger>
53
+ </TabsList>
54
+
55
+ <div className="p-4 border border-midground/10 bg-background-base">
56
+ <Small className="opacity-60">
57
+ Active panel: <span className="opacity-100">{active}</span>
58
+ </Small>
59
+ </div>
60
+ </>
61
+ )}
62
+ </Tabs>
63
+ </div>
64
+ )
65
+ }
66
+
67
+ export const TwoTabs: Story = {
68
+ render: () => (
69
+ <div className="w-80">
70
+ <Tabs defaultValue="local">
71
+ {(active, setActive) => (
72
+ <>
73
+ <TabsList>
74
+ <TabsTrigger
75
+ active={active === 'local'}
76
+ onClick={() => setActive('local')}
77
+ value="local"
78
+ >
79
+ Local
80
+ </TabsTrigger>
81
+
82
+ <TabsTrigger
83
+ active={active === 'remote'}
84
+ onClick={() => setActive('remote')}
85
+ value="remote"
86
+ >
87
+ Remote
88
+ </TabsTrigger>
89
+ </TabsList>
90
+
91
+ <div className="p-4 border border-midground/10 bg-background-base">
92
+ <Small className="opacity-60">
93
+ Active panel: <span className="opacity-100">{active}</span>
94
+ </Small>
95
+ </div>
96
+ </>
97
+ )}
98
+ </Tabs>
99
+ </div>
100
+ )
101
+ }
@@ -0,0 +1,66 @@
1
+ 'use client'
2
+
3
+ import {
4
+ type ButtonHTMLAttributes,
5
+ type HTMLAttributes,
6
+ type ReactNode,
7
+ useState
8
+ } from 'react'
9
+
10
+ import { cn } from '../../utils'
11
+
12
+ export function Tabs({ children, className, defaultValue }: TabsProps) {
13
+ const [active, setActive] = useState(defaultValue)
14
+
15
+ return (
16
+ <div className={cn('flex flex-col gap-4', className)}>
17
+ {children(active, setActive)}
18
+ </div>
19
+ )
20
+ }
21
+
22
+ export function TabsList({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
23
+ return (
24
+ <div
25
+ className={cn(
26
+ 'inline-flex h-9 items-center justify-start border-b border-midground/15 text-text-secondary',
27
+ className
28
+ )}
29
+ {...props}
30
+ />
31
+ )
32
+ }
33
+
34
+ export function TabsTrigger({
35
+ active,
36
+ className,
37
+ value: _value,
38
+ ...props
39
+ }: TabsTriggerProps) {
40
+ return (
41
+ <button
42
+ className={cn(
43
+ 'relative inline-flex items-center justify-center whitespace-nowrap px-3 py-1.5',
44
+ 'font-mondwest text-display text-xs tracking-[0.1em] transition-all cursor-pointer',
45
+ 'focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-midground/30',
46
+ active
47
+ ? 'text-midground after:absolute after:bottom-0 after:left-0 after:right-0 after:h-px after:bg-midground'
48
+ : 'text-text-secondary hover:text-midground',
49
+ className
50
+ )}
51
+ type="button"
52
+ {...props}
53
+ />
54
+ )
55
+ }
56
+
57
+ interface TabsProps {
58
+ children: (active: string, setActive: (value: string) => void) => ReactNode
59
+ className?: string
60
+ defaultValue: string
61
+ }
62
+
63
+ interface TabsTriggerProps extends ButtonHTMLAttributes<HTMLButtonElement> {
64
+ active: boolean
65
+ value: string
66
+ }
@@ -0,0 +1,67 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+
3
+ import {
4
+ TerminalDemo,
5
+ type TerminalDemoStep
6
+ } from './terminal-demo'
7
+
8
+ const SEQUENCE: TerminalDemoStep[] = [
9
+ { text: '❯ ', type: 'prompt' },
10
+ {
11
+ delay: 30,
12
+ text: 'Research the latest approaches to GRPO training and write a summary',
13
+ type: 'type'
14
+ },
15
+ { ms: 600, type: 'pause' },
16
+ {
17
+ lines: [
18
+ '',
19
+ '<span class="opacity-50"> web_search "GRPO reinforcement learning" 1.2s</span>',
20
+ '<span class="opacity-50"> web_extract arxiv.org/abs/2402.03300 3.1s</span>',
21
+ '<span class="opacity-50"> write_file ~/research/grpo-summary.md 0.1s</span>'
22
+ ],
23
+ type: 'output'
24
+ },
25
+ { ms: 500, type: 'pause' },
26
+ {
27
+ lines: [
28
+ '',
29
+ '<span class="opacity-70">Done! I\'ve written a summary covering:</span>',
30
+ '',
31
+ '<span class="opacity-70"> <span class="text-midground">✓</span> GRPO\'s group-relative advantage</span>',
32
+ '<span class="opacity-70"> <span class="text-midground">✓</span> Comparison with PPO/DPO</span>',
33
+ '',
34
+ '<span class="opacity-70">Saved to</span> <span class="text-midground">~/research/grpo-summary.md</span>'
35
+ ],
36
+ type: 'output'
37
+ },
38
+ { ms: 2500, type: 'pause' },
39
+ { type: 'clear' }
40
+ ]
41
+
42
+ const meta: Meta<typeof TerminalDemo> = {
43
+ args: { label: 'Hermes', sequence: SEQUENCE },
44
+ component: TerminalDemo,
45
+ title: 'Components/Data Display/TerminalDemo'
46
+ }
47
+
48
+ export default meta
49
+
50
+ type Story = StoryObj<typeof TerminalDemo>
51
+
52
+ export const Default: Story = {
53
+ render: args => (
54
+ <div className="w-[640px]">
55
+ <TerminalDemo {...args} />
56
+ </div>
57
+ )
58
+ }
59
+
60
+ export const TallerWindow: Story = {
61
+ args: { height: 480, label: 'shell' },
62
+ render: args => (
63
+ <div className="w-[640px]">
64
+ <TerminalDemo {...args} />
65
+ </div>
66
+ )
67
+ }
@@ -0,0 +1,189 @@
1
+ 'use client'
2
+
3
+ import { useCallback, useEffect, useRef, useState } from 'react'
4
+
5
+ import { cn } from '../../utils'
6
+
7
+ function sleep(ms: number) {
8
+ return new Promise<void>(resolve => setTimeout(resolve, ms))
9
+ }
10
+
11
+ export function TerminalDemo({
12
+ ariaLabel = 'Terminal Demo',
13
+ className,
14
+ height = 320,
15
+ label = 'Terminal',
16
+ loopDelayMs = 1000,
17
+ outputLineDelayMs = 50,
18
+ sequence
19
+ }: TerminalDemoProps) {
20
+ const bodyRef = useRef<HTMLDivElement>(null)
21
+ const startedRef = useRef(false)
22
+ const [html, setHtml] = useState('')
23
+
24
+ const runDemo = useCallback(async () => {
25
+ if (startedRef.current) {
26
+ return
27
+ }
28
+
29
+ startedRef.current = true
30
+ let content = ''
31
+
32
+ const render = (h: string) => {
33
+ content = h
34
+ setHtml(h)
35
+ }
36
+
37
+ for (;;) {
38
+ for (const step of sequence) {
39
+ switch (step.type) {
40
+ case 'clear':
41
+ content = ''
42
+ render('')
43
+
44
+ break
45
+
46
+ case 'output':
47
+ for (const line of step.lines) {
48
+ render(content + '\n' + line)
49
+ await sleep(outputLineDelayMs)
50
+ }
51
+
52
+ break
53
+
54
+ case 'pause':
55
+ await sleep(step.ms)
56
+
57
+ break
58
+
59
+ case 'prompt':
60
+ render(content + `<span class="text-midground">${step.text}</span>`)
61
+
62
+ break
63
+
64
+ case 'type':
65
+ for (const char of step.text) {
66
+ render(content + char)
67
+ await sleep(step.delay ?? 30)
68
+ }
69
+
70
+ break
71
+ }
72
+ }
73
+
74
+ content = ''
75
+ render('')
76
+ await sleep(loopDelayMs)
77
+ }
78
+ }, [loopDelayMs, outputLineDelayMs, sequence])
79
+
80
+ useEffect(() => {
81
+ const el = bodyRef.current?.closest('[data-demo-root]')
82
+
83
+ if (!el) {
84
+ return
85
+ }
86
+
87
+ const observer = new IntersectionObserver(
88
+ entries => {
89
+ entries.forEach(entry => {
90
+ if (entry.isIntersecting) {
91
+ runDemo()
92
+ }
93
+ })
94
+ },
95
+ { threshold: 0.3 }
96
+ )
97
+
98
+ observer.observe(el)
99
+
100
+ return () => observer.disconnect()
101
+ }, [runDemo])
102
+
103
+ useEffect(() => {
104
+ if (bodyRef.current) {
105
+ bodyRef.current.scrollTop = bodyRef.current.scrollHeight
106
+ }
107
+ }, [html])
108
+
109
+ return (
110
+ <div
111
+ aria-label={ariaLabel}
112
+ className={cn('border-4 border-double border-inherit', className)}
113
+ data-demo-root
114
+ role="img"
115
+ >
116
+ <div className="flex items-center gap-3 border-b border-current/10 px-3 py-2">
117
+ <div className="flex gap-1.5">
118
+ <span
119
+ className="bg-midground size-2 rounded-full"
120
+ style={{ mixBlendMode: 'plus-lighter' }}
121
+ />
122
+
123
+ <span className="bg-midground/60 size-2 rounded-full" />
124
+ <span className="bg-midground/30 size-2 rounded-full" />
125
+ </div>
126
+
127
+ <span className="font-courier text-display text-xs tracking-widest text-text-tertiary">
128
+ {label}
129
+ </span>
130
+ </div>
131
+
132
+ <div
133
+ className={cn(
134
+ 'overflow-x-hidden overflow-y-auto whitespace-pre-wrap',
135
+ 'font-courier p-4 text-[0.75rem] leading-[1.7] normal-case'
136
+ )}
137
+ dangerouslySetInnerHTML={{
138
+ __html:
139
+ html +
140
+ '<span class="blink inline-block dither ml-0.5 h-[1em] w-[1ch]"></span>'
141
+ }}
142
+ ref={bodyRef}
143
+ style={{ height }}
144
+ />
145
+ </div>
146
+ )
147
+ }
148
+
149
+ interface ClearStep {
150
+ type: 'clear'
151
+ }
152
+
153
+ interface OutputStep {
154
+ lines: string[]
155
+ type: 'output'
156
+ }
157
+
158
+ interface PauseStep {
159
+ ms: number
160
+ type: 'pause'
161
+ }
162
+
163
+ interface PromptStep {
164
+ text: string
165
+ type: 'prompt'
166
+ }
167
+
168
+ interface TerminalDemoProps {
169
+ ariaLabel?: string
170
+ className?: string
171
+ height?: number | string
172
+ label?: string
173
+ loopDelayMs?: number
174
+ outputLineDelayMs?: number
175
+ sequence: TerminalDemoStep[]
176
+ }
177
+
178
+ export type TerminalDemoStep =
179
+ | ClearStep
180
+ | OutputStep
181
+ | PauseStep
182
+ | PromptStep
183
+ | TypeStep
184
+
185
+ interface TypeStep {
186
+ delay?: number
187
+ text: string
188
+ type: 'type'
189
+ }
@@ -0,0 +1,47 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { useState } from 'react'
3
+
4
+ import { HamburgerIcon } from './icons'
5
+ import { ThemeToggle } from './theme-toggle'
6
+ import { Small } from './typography/small'
7
+
8
+ const meta: Meta<typeof ThemeToggle> = {
9
+ component: ThemeToggle,
10
+ title: 'Components/Layout/ThemeToggle'
11
+ }
12
+
13
+ export default meta
14
+
15
+ type Story = StoryObj<typeof ThemeToggle>
16
+
17
+ export const Default: Story = {
18
+ render: () => (
19
+ <div className="flex items-center gap-3">
20
+ <Small className="opacity-50">Theme</Small>
21
+
22
+ <ThemeToggle />
23
+ </div>
24
+ )
25
+ }
26
+
27
+ export const WithHamburger: Story = {
28
+ name: 'Beside Hamburger',
29
+ render: () => {
30
+ const [open, setOpen] = useState(false)
31
+
32
+ return (
33
+ <div className="flex items-center gap-3">
34
+ <ThemeToggle />
35
+
36
+ <button
37
+ aria-label={open ? 'Close menu' : 'Open menu'}
38
+ className="cursor-pointer bg-transparent p-2"
39
+ onClick={() => setOpen(v => !v)}
40
+ type="button"
41
+ >
42
+ <HamburgerIcon open={open} />
43
+ </button>
44
+ </div>
45
+ )
46
+ }
47
+ }