@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,173 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { useState } from 'react'
3
+
4
+ import { Button } from './button'
5
+ import { Checkbox } from './checkbox'
6
+ import { Input } from './input'
7
+ import { Label } from './label'
8
+ import { Select, SelectOption } from './select'
9
+ import { Separator } from './separator'
10
+ import { Switch } from './switch'
11
+
12
+ const meta: Meta = {
13
+ title: 'Components/Forms/All Forms'
14
+ }
15
+
16
+ export default meta
17
+
18
+ type Story = StoryObj
19
+
20
+ export const AllFormControls: Story = {
21
+ render: () => {
22
+ function FormDemo() {
23
+ const [name, setName] = useState('Hermes')
24
+ const [email, setEmail] = useState('hermes@nousresearch.com')
25
+ const [provider, setProvider] = useState('anthropic')
26
+ const [logging, setLogging] = useState(true)
27
+ const [telemetry, setTelemetry] = useState(false)
28
+ const [terms, setTerms] = useState(false)
29
+ const [newsletter, setNewsletter] = useState(true)
30
+
31
+ return (
32
+ <div className="flex w-full max-w-lg flex-col gap-6">
33
+ <div className="flex flex-col gap-1">
34
+ <h2 className="font-expanded text-sm font-bold tracking-[0.08em] uppercase">
35
+ Form Controls
36
+ </h2>
37
+
38
+ <p className="font-mondwest text-xs text-midground/60">
39
+ All form primitives from the design system.
40
+ </p>
41
+ </div>
42
+
43
+ <Separator />
44
+
45
+ <div className="flex flex-col gap-4">
46
+ <Label className="text-midground/50">Text Inputs</Label>
47
+
48
+ <div className="flex flex-col gap-1.5">
49
+ <Label htmlFor="form-name">Name</Label>
50
+ <Input
51
+ id="form-name"
52
+ onChange={e => setName(e.target.value)}
53
+ placeholder="Enter your name"
54
+ value={name}
55
+ />
56
+ </div>
57
+
58
+ <div className="flex flex-col gap-1.5">
59
+ <Label htmlFor="form-email">Email</Label>
60
+ <Input
61
+ id="form-email"
62
+ onChange={e => setEmail(e.target.value)}
63
+ placeholder="Enter your email"
64
+ type="email"
65
+ value={email}
66
+ />
67
+ </div>
68
+
69
+ <div className="flex flex-col gap-1.5">
70
+ <Label htmlFor="form-disabled">Disabled Input</Label>
71
+ <Input
72
+ disabled
73
+ id="form-disabled"
74
+ placeholder="Cannot edit"
75
+ value="Read-only value"
76
+ />
77
+ </div>
78
+ </div>
79
+
80
+ <Separator />
81
+
82
+ <div className="flex flex-col gap-4">
83
+ <Label className="text-midground/50">Select</Label>
84
+
85
+ <div className="flex flex-col gap-1.5">
86
+ <Label htmlFor="form-provider">Provider</Label>
87
+
88
+ <Select
89
+ onValueChange={setProvider}
90
+ placeholder="Choose a provider…"
91
+ value={provider}
92
+ >
93
+ <SelectOption value="openai">OpenAI</SelectOption>
94
+ <SelectOption value="anthropic">Anthropic</SelectOption>
95
+ <SelectOption value="google">Google</SelectOption>
96
+ <SelectOption value="mistral">Mistral</SelectOption>
97
+ </Select>
98
+ </div>
99
+ </div>
100
+
101
+ <Separator />
102
+
103
+ <div className="flex flex-col gap-4">
104
+ <Label className="text-midground/50">Switches</Label>
105
+
106
+ <label className="flex items-center justify-between">
107
+ <span className="text-sm">Enable logging</span>
108
+ <Switch checked={logging} onCheckedChange={setLogging} />
109
+ </label>
110
+
111
+ <label className="flex items-center justify-between">
112
+ <span className="text-sm">Send telemetry</span>
113
+ <Switch checked={telemetry} onCheckedChange={setTelemetry} />
114
+ </label>
115
+ </div>
116
+
117
+ <Separator />
118
+
119
+ <div className="flex flex-col gap-4">
120
+ <Label className="text-midground/50">Checkboxes</Label>
121
+
122
+ <div className="flex items-center gap-2.5">
123
+ <Checkbox
124
+ checked={terms}
125
+ id="form-terms"
126
+ onCheckedChange={setTerms}
127
+ />
128
+
129
+ <label className="cursor-pointer text-sm" htmlFor="form-terms">
130
+ Accept terms and conditions
131
+ </label>
132
+ </div>
133
+
134
+ <div className="flex items-center gap-2.5">
135
+ <Checkbox
136
+ checked={newsletter}
137
+ id="form-newsletter"
138
+ onCheckedChange={setNewsletter}
139
+ />
140
+
141
+ <label className="cursor-pointer text-sm" htmlFor="form-newsletter">
142
+ Subscribe to newsletter
143
+ </label>
144
+ </div>
145
+ </div>
146
+
147
+ <Separator />
148
+
149
+ <div className="flex flex-col gap-4">
150
+ <Label className="text-midground/50">Buttons</Label>
151
+
152
+ <div className="flex flex-wrap gap-2">
153
+ <Button>Primary</Button>
154
+ <Button outlined>Outlined</Button>
155
+ <Button invert>Inverted</Button>
156
+ <Button destructive>Destructive</Button>
157
+ <Button disabled>Disabled</Button>
158
+ </div>
159
+ </div>
160
+
161
+ <Separator />
162
+
163
+ <div className="flex items-center justify-end gap-2">
164
+ <Button outlined>Cancel</Button>
165
+ <Button>Save Changes</Button>
166
+ </div>
167
+ </div>
168
+ )
169
+ }
170
+
171
+ return <FormDemo />
172
+ }
173
+ }
@@ -0,0 +1,153 @@
1
+ 'use client'
2
+
3
+ import * as Plot from '@observablehq/plot'
4
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
5
+
6
+ import { cn } from '../../../utils'
7
+
8
+ import {
9
+ accessor,
10
+ CHART_MARGINS,
11
+ CHART_STYLE,
12
+ type ChartProps,
13
+ Crosshair,
14
+ type CrosshairState,
15
+ type DataPoint,
16
+ setupCrosshair,
17
+ stylePlot,
18
+ useDims,
19
+ withChartBlend
20
+ } from './utils'
21
+
22
+ export const BarChart = withChartBlend(
23
+ <T extends DataPoint>({
24
+ backgroundColor: _,
25
+ className,
26
+ color: fillColor,
27
+ data = [],
28
+ formatTooltip,
29
+ formatX: formatXProp,
30
+ formatY: formatYProp,
31
+ x = 'label' as keyof T,
32
+ xDomain,
33
+ xTicks = [0, 50000, 100000],
34
+ y = 'value' as keyof T,
35
+ yDomain = [0, 10],
36
+ yTicks = [10, 8, 4, 2],
37
+ ...props
38
+ }: BarChartProps<T> & { backgroundColor?: string; color?: string }) => {
39
+ const ref = useRef<HTMLDivElement>(null)
40
+ const plotRef = useRef<HTMLDivElement>(null)
41
+ const [crosshair, setCrosshair] = useState<CrosshairState>({ x: null })
42
+ const dims = useDims(ref)
43
+
44
+ const formatX = useCallback(
45
+ (v: unknown) =>
46
+ formatXProp?.(v) ??
47
+ (typeof v === 'number' ? v.toLocaleString('en-US') : String(v)),
48
+ [formatXProp]
49
+ )
50
+
51
+ const formatY = useCallback(
52
+ (v: number) => formatYProp?.(v) ?? String(v),
53
+ [formatYProp]
54
+ )
55
+
56
+ const getX = useMemo(() => accessor<T, unknown>(x), [x])
57
+ const getY = useMemo(() => accessor<T, number>(y), [y])
58
+
59
+ useEffect(() => {
60
+ if (
61
+ !ref.current ||
62
+ !plotRef.current ||
63
+ !data.length ||
64
+ !dims.h ||
65
+ !dims.w
66
+ ) {
67
+ return
68
+ }
69
+
70
+ plotRef.current.innerHTML = ''
71
+
72
+ const [xMin, xMax] = [
73
+ xDomain?.[0] ?? 0,
74
+ xDomain?.[1] ?? Math.max(...data.map(d => getX(d) as number))
75
+ ]
76
+
77
+ const plot = Plot.plot({
78
+ ...CHART_MARGINS,
79
+ height: dims.h,
80
+ marks: [
81
+ Plot.rectY(data, {
82
+ fill: fillColor ?? 'currentColor',
83
+ fillOpacity: 0.3,
84
+ interval: (xMax - xMin) / data.length,
85
+ x: getX as (d: T) => unknown,
86
+ y: getY
87
+ }),
88
+ Plot.axisX({ tickFormat: formatX, ticks: xTicks })
89
+ ],
90
+ style: CHART_STYLE,
91
+ width: dims.w,
92
+ x: { domain: [xMin, xMax], label: null, type: 'linear' },
93
+ y: {
94
+ domain: yDomain,
95
+ grid: true,
96
+ label: null,
97
+ tickFormat: formatY,
98
+ ticks: yTicks
99
+ }
100
+ })
101
+
102
+ stylePlot(plot as HTMLElement)
103
+ plotRef.current.appendChild(plot)
104
+
105
+ const cleanup = setupCrosshair(
106
+ ref.current,
107
+ data,
108
+ d => getX(d) as number,
109
+ getY,
110
+ yDomain,
111
+ d => formatTooltip?.(d) ?? `${formatX(getX(d))}: ${formatY(getY(d))}`,
112
+ setCrosshair
113
+ )
114
+
115
+ return cleanup
116
+ }, [
117
+ data,
118
+ dims.h,
119
+ dims.w,
120
+ fillColor,
121
+ formatTooltip,
122
+ formatX,
123
+ formatY,
124
+ getX,
125
+ getY,
126
+ xDomain,
127
+ xTicks,
128
+ yDomain,
129
+ yTicks
130
+ ])
131
+
132
+ return (
133
+ <div
134
+ className={cn('relative aspect-4/1 w-full overflow-clip', className)}
135
+ ref={ref}
136
+ {...props}
137
+ >
138
+ <div className="absolute inset-0" ref={plotRef} />
139
+
140
+ <Crosshair
141
+ color={fillColor}
142
+ containerWidth={dims.w}
143
+ height={dims.h}
144
+ {...crosshair}
145
+ />
146
+ </div>
147
+ )
148
+ }
149
+ )
150
+
151
+ interface BarChartProps<T extends DataPoint> extends ChartProps<T> {
152
+ xDomain?: [number, number]
153
+ }
@@ -0,0 +1,64 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+
3
+ import { BarChart, LineChart } from '../graphs'
4
+ import { Small } from '../typography/small'
5
+
6
+ const LINE_DATA = (['primary', 'secondary', 'tertiary'] as const).flatMap(
7
+ (series, si) =>
8
+ [0, 50000, 100000, 150000].map((label, i) => ({
9
+ label,
10
+ series,
11
+ value: 0.15 + si * 0.1 + (i % 2) * 0.05 + Math.sin(i + si) * 0.08
12
+ }))
13
+ )
14
+
15
+ const BAR_DATA = (() => {
16
+ let x = 42
17
+ const f = () => (x = (1103515245 * x + 12345) % 0x80000000) / 0x80000000
18
+
19
+ return Array.from({ length: 100 }, (_, i) => ({
20
+ label: (i / 99) * 150000,
21
+ value: f() * 10
22
+ }))
23
+ })()
24
+
25
+ const meta = {
26
+ parameters: { layout: 'padded' },
27
+ title: 'Components/Data Display/Graphs'
28
+ } satisfies Meta
29
+
30
+ export default meta
31
+
32
+ type Story = StoryObj
33
+
34
+ export const Line: Story = {
35
+ render: () => (
36
+ <div>
37
+ <Small className="mb-5 block opacity-50">LineChart</Small>
38
+
39
+ <LineChart
40
+ data={LINE_DATA}
41
+ series="series"
42
+ x="label"
43
+ y="value"
44
+ yDomain={[0, 0.5]}
45
+ />
46
+ </div>
47
+ )
48
+ }
49
+
50
+ export const Bar: Story = {
51
+ render: () => (
52
+ <div>
53
+ <Small className="mb-5 block opacity-50">BarChart</Small>
54
+
55
+ <BarChart
56
+ data={BAR_DATA}
57
+ x="label"
58
+ xDomain={[0, 150000]}
59
+ y="value"
60
+ yDomain={[0, 10]}
61
+ />
62
+ </div>
63
+ )
64
+ }
@@ -0,0 +1,4 @@
1
+ export { BarChart } from './bar-chart'
2
+ export { LineChart } from './line-chart'
3
+
4
+ export * from './utils'
@@ -0,0 +1,213 @@
1
+ 'use client'
2
+
3
+ import * as Plot from '@observablehq/plot'
4
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
5
+
6
+ import { cn } from '../../../utils'
7
+
8
+ import {
9
+ accessor,
10
+ CHART_MARGINS,
11
+ CHART_STYLE,
12
+ type ChartProps,
13
+ Crosshair,
14
+ type CrosshairState,
15
+ type DataPoint,
16
+ setupCrosshair,
17
+ stylePlot,
18
+ useDims,
19
+ withChartBlend
20
+ } from './utils'
21
+
22
+ export const LineChart = withChartBlend(
23
+ <T extends DataPoint>({
24
+ backgroundColor: _,
25
+ className,
26
+ color: strokeColor,
27
+ curve = 'natural',
28
+ data = [],
29
+ formatTooltip,
30
+ formatX: formatXProp,
31
+ formatY: formatYProp,
32
+ series = 'series' as keyof T,
33
+ showArea = false,
34
+ x = 'label' as keyof T,
35
+ xTicks,
36
+ y = 'value' as keyof T,
37
+ yDomain = [0, 0.5],
38
+ yTicks = 4,
39
+ ...props
40
+ }: LineChartProps<T> & { backgroundColor?: string; color?: string }) => {
41
+ const ref = useRef<HTMLDivElement>(null)
42
+ const plotRef = useRef<HTMLDivElement>(null)
43
+ const [hovered, setHovered] = useState<null | T>(null)
44
+ const [crosshair, setCrosshair] = useState<CrosshairState>({ x: null })
45
+ const dims = useDims(ref)
46
+
47
+ const formatX = useCallback(
48
+ (v: unknown) =>
49
+ formatXProp?.(v) ??
50
+ ((v as number) >= 1e3 ? `${(v as number) / 1e3}k` : `${v}`),
51
+ [formatXProp]
52
+ )
53
+
54
+ const formatY = useCallback(
55
+ (v: number) => formatYProp?.(v) ?? `${Math.round(v * 100)}%`,
56
+ [formatYProp]
57
+ )
58
+
59
+ const getX = useMemo(() => accessor<T, unknown>(x), [x])
60
+ const getY = useMemo(() => accessor<T, number>(y), [y])
61
+ const getZ = useCallback((d: T) => d[series], [series])
62
+
63
+ useEffect(() => {
64
+ if (
65
+ !ref.current ||
66
+ !plotRef.current ||
67
+ !data.length ||
68
+ !dims.h ||
69
+ !dims.w
70
+ ) {
71
+ return
72
+ }
73
+
74
+ plotRef.current.innerHTML = ''
75
+
76
+ const hasSeries = data.some(d => d[series] !== undefined)
77
+
78
+ const seriesIdx = hasSeries
79
+ ? data.reduce(
80
+ (acc, d, i) => ((acc[d[series] as string] ??= i), acc),
81
+ {} as Record<string, number>
82
+ )
83
+ : {}
84
+
85
+ const n = Object.keys(seriesIdx).length
86
+
87
+ const opacity = (d: T) => {
88
+ if (!hasSeries) {
89
+ return 1
90
+ }
91
+
92
+ if (hovered) {
93
+ return d[series] === hovered[series] ? 1 : 0.2
94
+ }
95
+
96
+ return 1 - (seriesIdx[d[series] as string] / Math.max(n - 1, 1)) * 0.2
97
+ }
98
+
99
+ const lineOpts = {
100
+ curve,
101
+ x: getX as (d: T) => unknown,
102
+ y: getY,
103
+ ...(hasSeries && { z: getZ as (d: T) => unknown })
104
+ }
105
+
106
+ const plot = Plot.plot({
107
+ ...CHART_MARGINS,
108
+ height: dims.h,
109
+ marks: [
110
+ ...(showArea
111
+ ? [
112
+ Plot.areaY(data, {
113
+ ...lineOpts,
114
+ fill: strokeColor,
115
+ fillOpacity: 0.15,
116
+ y1: yDomain[0]
117
+ })
118
+ ]
119
+ : []),
120
+ Plot.lineY(data, {
121
+ ...lineOpts,
122
+ stroke: 'transparent',
123
+ strokeWidth: 16
124
+ }),
125
+ Plot.lineY(data, {
126
+ ...lineOpts,
127
+ stroke: strokeColor,
128
+ strokeOpacity: opacity,
129
+ strokeWidth: 1.5
130
+ })
131
+ ],
132
+ style: { ...CHART_STYLE, fontStretch: 'expanded' },
133
+ width: dims.w,
134
+ x: { label: null, tickFormat: formatX, ticks: xTicks },
135
+ y: {
136
+ domain: yDomain,
137
+ grid: true,
138
+ label: null,
139
+ tickFormat: formatY,
140
+ ticks: yTicks
141
+ }
142
+ })
143
+
144
+ plot.addEventListener('input', () => setHovered(plot.value as null | T))
145
+ stylePlot(plot as HTMLElement)
146
+
147
+ plot.querySelectorAll('g[aria-label="line"] path').forEach(el =>
148
+ Object.assign((el as SVGPathElement).style, {
149
+ transition: 'stroke-opacity 0.2s'
150
+ })
151
+ )
152
+
153
+ plotRef.current.appendChild(plot)
154
+
155
+ const cleanup = setupCrosshair(
156
+ ref.current,
157
+ data,
158
+ d => getX(d) as number,
159
+ getY,
160
+ yDomain,
161
+ d => formatTooltip?.(d) ?? `${formatX(getX(d))}: ${formatY(getY(d))}`,
162
+ setCrosshair,
163
+ hasSeries ? d => getZ(d) : undefined
164
+ )
165
+
166
+ return () => {
167
+ cleanup()
168
+ plot.parentNode && plot.remove()
169
+ }
170
+ }, [
171
+ curve,
172
+ data,
173
+ dims.h,
174
+ dims.w,
175
+ formatTooltip,
176
+ formatX,
177
+ formatY,
178
+ getX,
179
+ getY,
180
+ getZ,
181
+ hovered,
182
+ series,
183
+ showArea,
184
+ strokeColor,
185
+ xTicks,
186
+ yDomain,
187
+ yTicks
188
+ ])
189
+
190
+ return (
191
+ <div
192
+ className={cn('relative aspect-4/1 w-full overflow-clip', className)}
193
+ ref={ref}
194
+ {...props}
195
+ >
196
+ <div className="absolute inset-0" ref={plotRef} />
197
+
198
+ <Crosshair
199
+ color={strokeColor}
200
+ containerWidth={dims.w}
201
+ height={dims.h}
202
+ {...crosshair}
203
+ />
204
+ </div>
205
+ )
206
+ }
207
+ )
208
+
209
+ interface LineChartProps<T extends DataPoint> extends ChartProps<T> {
210
+ curve?: 'basis' | 'catmull-rom' | 'linear' | 'natural' | 'step'
211
+ series?: keyof T
212
+ showArea?: boolean
213
+ }