@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,227 @@
1
+ 'use client'
2
+
3
+ import {
4
+ type PointerEvent as ReactPointerEvent,
5
+ type ReactNode,
6
+ useEffect,
7
+ useRef,
8
+ useState
9
+ } from 'react'
10
+ import { createPortal } from 'react-dom'
11
+
12
+ import { cn } from '../../utils'
13
+ import { Typography } from './typography'
14
+
15
+ const CLOSE_DRAG_MIN_PX = 72
16
+ const CLOSE_DRAG_RATIO = 0.18
17
+ const SHEET_TRANSITION_MS = 280
18
+
19
+ /**
20
+ * Mobile-first bottom sheet with slide + fade enter/exit, drag-to-dismiss
21
+ * handle, body scroll lock, and reduced-motion support. Portaled to
22
+ * `document.body` to escape ancestor stacking contexts.
23
+ */
24
+ export function BottomSheet({
25
+ backdropDismissLabel = 'Dismiss',
26
+ children,
27
+ onClose,
28
+ open,
29
+ title
30
+ }: BottomSheetProps) {
31
+ const [renderPortal, setRenderPortal] = useState(open)
32
+ const [entered, setEntered] = useState(false)
33
+ const [dragOffsetPx, setDragOffsetPx] = useState(0)
34
+ const [dragActive, setDragActive] = useState(false)
35
+
36
+ const closeTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
37
+ const sheetRef = useRef<HTMLDivElement>(null)
38
+ const dragTrackingRef = useRef(false)
39
+ const dragStartYRef = useRef(0)
40
+ const dragOffsetRef = useRef(0)
41
+
42
+ const reducedMotion =
43
+ typeof window !== 'undefined' &&
44
+ window.matchMedia('(prefers-reduced-motion: reduce)').matches
45
+
46
+ const syncDragPx = (next: number) => {
47
+ dragOffsetRef.current = next
48
+ setDragOffsetPx(next)
49
+ }
50
+
51
+ useEffect(() => {
52
+ if (closeTimerRef.current) {
53
+ clearTimeout(closeTimerRef.current)
54
+ closeTimerRef.current = null
55
+ }
56
+
57
+ const ms = reducedMotion ? 0 : SHEET_TRANSITION_MS
58
+
59
+ let openRafId = 0
60
+ let exitRafId = 0
61
+
62
+ if (open) {
63
+ openRafId = requestAnimationFrame(() => {
64
+ dragTrackingRef.current = false
65
+ dragOffsetRef.current = 0
66
+ setDragActive(false)
67
+ setDragOffsetPx(0)
68
+ setRenderPortal(true)
69
+ requestAnimationFrame(() => {
70
+ requestAnimationFrame(() => setEntered(true))
71
+ })
72
+ })
73
+ } else {
74
+ exitRafId = requestAnimationFrame(() => {
75
+ dragTrackingRef.current = false
76
+ setDragActive(false)
77
+ setEntered(false)
78
+ closeTimerRef.current = window.setTimeout(() => {
79
+ dragOffsetRef.current = 0
80
+ setDragOffsetPx(0)
81
+ setRenderPortal(false)
82
+ closeTimerRef.current = null
83
+ }, ms)
84
+ })
85
+ }
86
+
87
+ return () => {
88
+ cancelAnimationFrame(openRafId)
89
+ cancelAnimationFrame(exitRafId)
90
+ if (closeTimerRef.current) {
91
+ clearTimeout(closeTimerRef.current)
92
+ closeTimerRef.current = null
93
+ }
94
+ }
95
+ }, [open, reducedMotion])
96
+
97
+ useEffect(() => {
98
+ if (!renderPortal) return
99
+ const prev = document.body.style.overflow
100
+ document.body.style.overflow = 'hidden'
101
+ return () => {
102
+ document.body.style.overflow = prev
103
+ }
104
+ }, [renderPortal])
105
+
106
+ if (!renderPortal || typeof document === 'undefined') return null
107
+
108
+ const durationClass = reducedMotion ? 'duration-0' : 'duration-[280ms]'
109
+ const draggingVisual = dragActive || dragOffsetPx > 0
110
+
111
+ const onDragPointerDown = (e: ReactPointerEvent<HTMLDivElement>) => {
112
+ if (reducedMotion || !entered) return
113
+ if (e.pointerType === 'mouse' && e.button !== 0) return
114
+
115
+ dragTrackingRef.current = true
116
+ setDragActive(true)
117
+ dragStartYRef.current = e.clientY
118
+ syncDragPx(0)
119
+ e.currentTarget.setPointerCapture(e.pointerId)
120
+ }
121
+
122
+ const onDragPointerMove = (e: ReactPointerEvent<HTMLDivElement>) => {
123
+ if (!dragTrackingRef.current) return
124
+ const dy = e.clientY - dragStartYRef.current
125
+ const next = Math.max(0, dy)
126
+ const sheetH = sheetRef.current?.offsetHeight ?? 560
127
+ syncDragPx(Math.min(next, sheetH))
128
+ }
129
+
130
+ const endDrag = (e: ReactPointerEvent<HTMLDivElement>) => {
131
+ if (!dragTrackingRef.current) return
132
+ dragTrackingRef.current = false
133
+ setDragActive(false)
134
+ try {
135
+ e.currentTarget.releasePointerCapture(e.pointerId)
136
+ } catch {
137
+ /* already released */
138
+ }
139
+
140
+ const sheetH = sheetRef.current?.offsetHeight ?? 560
141
+ const threshold = Math.max(CLOSE_DRAG_MIN_PX, sheetH * CLOSE_DRAG_RATIO)
142
+ const d = dragOffsetRef.current
143
+
144
+ if (d >= threshold) {
145
+ onClose()
146
+ return
147
+ }
148
+ syncDragPx(0)
149
+ }
150
+
151
+ return createPortal(
152
+ <div className="fixed inset-0 z-[200] flex flex-col justify-end">
153
+ <button
154
+ aria-label={backdropDismissLabel}
155
+ className={cn(
156
+ 'absolute inset-0 bg-black/55 backdrop-blur-[2px]',
157
+ 'transition-opacity ease-out motion-reduce:transition-none',
158
+ durationClass,
159
+ entered ? 'opacity-100' : 'opacity-0'
160
+ )}
161
+ onClick={onClose}
162
+ type="button"
163
+ />
164
+
165
+ <div
166
+ aria-label={title}
167
+ aria-modal="true"
168
+ className={cn(
169
+ 'relative flex max-h-[85dvh] min-h-0 flex-col rounded-t-xl border border-current/20',
170
+ 'bg-background-base/98 pb-[max(1rem,env(safe-area-inset-bottom))]',
171
+ 'shadow-[0_-12px_40px_-8px_rgba(0,0,0,0.55)] backdrop-blur-md',
172
+ 'ease-out motion-reduce:transition-none transform-gpu',
173
+ draggingVisual
174
+ ? 'transition-none'
175
+ : cn('transition-transform', durationClass),
176
+ entered ? 'translate-y-0' : 'translate-y-full'
177
+ )}
178
+ ref={sheetRef}
179
+ role="dialog"
180
+ style={
181
+ entered && dragOffsetPx > 0
182
+ ? { transform: `translateY(${dragOffsetPx}px)` }
183
+ : undefined
184
+ }
185
+ >
186
+ <div
187
+ className={cn(
188
+ 'flex shrink-0 flex-col gap-2 border-b border-current/15 px-4 pb-3 pt-2',
189
+ 'touch-none select-none',
190
+ reducedMotion
191
+ ? 'cursor-default'
192
+ : 'cursor-grab active:cursor-grabbing'
193
+ )}
194
+ onPointerCancel={endDrag}
195
+ onPointerDown={onDragPointerDown}
196
+ onPointerMove={onDragPointerMove}
197
+ onPointerUp={endDrag}
198
+ >
199
+ <div
200
+ aria-hidden
201
+ className="mx-auto h-1 w-10 shrink-0 rounded-full bg-current/20"
202
+ />
203
+
204
+ <Typography
205
+ className="text-[0.65rem] tracking-[0.15em] uppercase text-midground/70"
206
+ mondwest
207
+ >
208
+ {title}
209
+ </Typography>
210
+ </div>
211
+
212
+ <div className="min-h-0 flex-1 overflow-y-auto overscroll-contain">
213
+ {children}
214
+ </div>
215
+ </div>
216
+ </div>,
217
+ document.body
218
+ )
219
+ }
220
+
221
+ interface BottomSheetProps {
222
+ backdropDismissLabel?: string
223
+ children: ReactNode
224
+ onClose: () => void
225
+ open: boolean
226
+ title: string
227
+ }
@@ -0,0 +1,68 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { expect } from 'storybook/test'
3
+
4
+ import { Button } from './button'
5
+ import { ArrowIcon, LinkIcon, SearchIcon } from './icons'
6
+
7
+ const meta: Meta<typeof Button> = {
8
+ argTypes: {
9
+ children: { control: 'text' },
10
+ disabled: { control: 'boolean' },
11
+ invert: { control: 'boolean' },
12
+ outlined: { control: 'boolean' }
13
+ },
14
+ args: { children: 'Normal', disabled: false, invert: false, outlined: false },
15
+ component: Button,
16
+ title: 'Components/Forms/Button'
17
+ }
18
+
19
+ export default meta
20
+
21
+ type Story = StoryObj<typeof Button>
22
+
23
+ export const Playground: Story = {
24
+ args: { prefix: <ArrowIcon direction="right" /> }
25
+ }
26
+
27
+ export const AllVariants: Story = {
28
+ render: () => (
29
+ <div className="flex flex-wrap items-center gap-2">
30
+ <Button prefix={<ArrowIcon direction="right" />}>Normal</Button>
31
+ <Button invert prefix={<ArrowIcon direction="right" />}>
32
+ Inverted
33
+ </Button>
34
+ <Button outlined prefix={<ArrowIcon direction="right" />}>
35
+ Outlined
36
+ </Button>
37
+ <Button invert outlined prefix={<ArrowIcon direction="right" />}>
38
+ Out + Inv
39
+ </Button>
40
+ </div>
41
+ )
42
+ }
43
+
44
+ export const WithIcons: Story = {
45
+ render: () => (
46
+ <div className="flex flex-wrap items-center gap-2">
47
+ <Button suffix={<LinkIcon />}>Suffix</Button>
48
+ <Button
49
+ prefix={<SearchIcon />}
50
+ suffix={<ArrowIcon direction="right" />}
51
+ >
52
+ Prefix + Suffix
53
+ </Button>
54
+ <Button disabled prefix={<ArrowIcon direction="right" />}>
55
+ Disabled
56
+ </Button>
57
+ </div>
58
+ )
59
+ }
60
+
61
+ export const CssCheck: Story = {
62
+ args: { children: 'Danger', destructive: true },
63
+ play: async ({ canvas }) => {
64
+ const button = canvas.getByRole('button', { name: /danger/i })
65
+
66
+ await expect(getComputedStyle(button).backgroundColor).toBe('rgb(251, 44, 54)')
67
+ }
68
+ }
@@ -0,0 +1,170 @@
1
+ import { cva, type VariantProps } from 'class-variance-authority'
2
+ import { cloneElement } from 'react'
3
+
4
+ import { cn } from '../../utils'
5
+
6
+ import { Typography } from './typography'
7
+
8
+ const SHADOW_DEFAULT =
9
+ 'shadow-[inset_-1px_-1px_0_0_#00000080,inset_1px_1px_0_0_#ffffff80]'
10
+ const SHADOW_INVERT =
11
+ 'shadow-[inset_-1px_-1px_0_0_#00000080,inset_1px_1px_0_0_#ffffff29]'
12
+ const SHADOW_INVERT_OUTLINED =
13
+ 'shadow-[inset_-1px_-1px_0_0_#ffffff12,inset_1px_1px_0_0_#ffffff29]'
14
+ const ACTIVE_FILTER =
15
+ 'active:[filter:invert(1)_brightness(calc(100-99*var(--foreground-alpha,0)))]'
16
+
17
+ const buttonVariants = cva(
18
+ [
19
+ 'group relative grid cursor-pointer grid-cols-[auto_1fr_auto] items-center',
20
+ 'text-display leading-0 font-bold tracking-[0.2em]',
21
+ 'disabled:pointer-events-none disabled:bg-midground/15 disabled:text-midground disabled:shadow-none'
22
+ ],
23
+ {
24
+ compoundVariants: [
25
+ // ── invert × outlined matrix (default surface, no ghost/destructive) ──
26
+ {
27
+ class: `bg-midground text-background-base active:invert ${SHADOW_DEFAULT}`,
28
+ destructive: false,
29
+ ghost: false,
30
+ invert: false,
31
+ outlined: false
32
+ },
33
+ {
34
+ class: `bg-midground/15 text-midground ${SHADOW_INVERT} ${ACTIVE_FILTER}`,
35
+ destructive: false,
36
+ ghost: false,
37
+ invert: true,
38
+ outlined: false
39
+ },
40
+ {
41
+ class: `shadow-midground ${SHADOW_DEFAULT} ${ACTIVE_FILTER}`,
42
+ destructive: false,
43
+ ghost: false,
44
+ invert: false,
45
+ outlined: true
46
+ },
47
+ {
48
+ class: `${SHADOW_INVERT_OUTLINED} ${ACTIVE_FILTER}`,
49
+ destructive: false,
50
+ ghost: false,
51
+ invert: true,
52
+ outlined: true
53
+ },
54
+ // ── ghost: no chrome, hover bg only ──
55
+ {
56
+ class: 'bg-transparent text-current hover:bg-midground/10 shadow-none',
57
+ destructive: false,
58
+ ghost: true
59
+ },
60
+ {
61
+ class:
62
+ 'bg-transparent text-destructive hover:bg-destructive/10 shadow-none',
63
+ destructive: true,
64
+ ghost: true
65
+ },
66
+ // ── solid destructive ──
67
+ {
68
+ class: `bg-destructive text-destructive-foreground hover:bg-destructive/90 ${SHADOW_INVERT}`,
69
+ destructive: true,
70
+ ghost: false,
71
+ outlined: false
72
+ },
73
+ // ── outlined destructive ──
74
+ {
75
+ class:
76
+ 'border border-destructive/40 bg-transparent text-destructive hover:bg-destructive/10 shadow-none',
77
+ destructive: true,
78
+ ghost: false,
79
+ outlined: true
80
+ }
81
+ ],
82
+ defaultVariants: {
83
+ destructive: false,
84
+ ghost: false,
85
+ invert: false,
86
+ outlined: false,
87
+ size: 'default'
88
+ },
89
+ variants: {
90
+ destructive: { true: '' },
91
+ ghost: { true: '' },
92
+ invert: { true: '' },
93
+ outlined: { true: 'text-midground bg-transparent' },
94
+ size: {
95
+ default: 'px-[.9em_.75em] py-[1.25em]',
96
+ icon: 'p-2 aspect-square grid-cols-1 place-items-center [&>svg]:size-3.5',
97
+ sm: 'px-3 py-1.5 text-[0.7rem] tracking-[0.15em] [&>svg]:size-3',
98
+ xs: 'p-1 aspect-square grid-cols-1 place-items-center [&>svg]:size-3'
99
+ }
100
+ }
101
+ }
102
+ )
103
+
104
+ const IconSlot = ({
105
+ icon,
106
+ side
107
+ }: {
108
+ icon: React.ReactNode
109
+ side: 'left' | 'right'
110
+ }) => (
111
+ <>
112
+ <span className="w-5" />
113
+
114
+ <span
115
+ className={cn(
116
+ 'absolute top-1/2 -translate-y-1/2',
117
+ side === 'left' ? 'left-3' : 'right-3'
118
+ )}
119
+ >
120
+ {typeof icon === 'object'
121
+ ? cloneElement(icon as React.ReactElement<any>, {
122
+ className: 'size-3.5'
123
+ })
124
+ : icon}
125
+ </span>
126
+ </>
127
+ )
128
+
129
+ export const Button = ({
130
+ children,
131
+ className,
132
+ destructive,
133
+ ghost,
134
+ invert,
135
+ outlined,
136
+ prefix,
137
+ size,
138
+ suffix,
139
+ ...props
140
+ }: ButtonProps) => (
141
+ <Typography
142
+ as="button"
143
+ className={cn(
144
+ buttonVariants({ destructive, ghost, invert, outlined, size }),
145
+ className
146
+ )}
147
+ mono
148
+ {...props}
149
+ >
150
+ {!ghost && (
151
+ <span
152
+ aria-hidden
153
+ className="arc-border opacity-0 transition-opacity duration-200 group-hover:opacity-100 group-focus-visible:opacity-100 group-active:opacity-100"
154
+ />
155
+ )}
156
+ {prefix && <IconSlot icon={prefix} side="left" />}
157
+ {children}
158
+ {suffix && <IconSlot icon={suffix} side="right" />}
159
+ </Typography>
160
+ )
161
+
162
+ interface ButtonProps
163
+ extends Omit<
164
+ React.ButtonHTMLAttributes<HTMLButtonElement>,
165
+ 'prefix' | 'suffix'
166
+ >,
167
+ VariantProps<typeof buttonVariants> {
168
+ prefix?: React.ReactNode
169
+ suffix?: React.ReactNode
170
+ }
@@ -0,0 +1,63 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+
3
+ import { Button } from './button'
4
+ import {
5
+ Card,
6
+ CardContent,
7
+ CardDescription,
8
+ CardHeader,
9
+ CardTitle
10
+ } from './card'
11
+ import { Input } from './input'
12
+ import { Label } from './label'
13
+ import { Separator } from './separator'
14
+
15
+ const meta: Meta<typeof Card> = {
16
+ component: Card,
17
+ title: 'Components/Data Display/Card'
18
+ }
19
+
20
+ export default meta
21
+
22
+ type Story = StoryObj<typeof Card>
23
+
24
+ export const Default: Story = {
25
+ render: () => (
26
+ <Card className="max-w-sm">
27
+ <CardHeader>
28
+ <CardTitle>Card title</CardTitle>
29
+ <CardDescription>A brief description of this card.</CardDescription>
30
+ </CardHeader>
31
+ <CardContent>
32
+ <p className="text-sm text-midground/70">
33
+ Card body content goes here.
34
+ </p>
35
+ </CardContent>
36
+ </Card>
37
+ )
38
+ }
39
+
40
+ export const WithForm: Story = {
41
+ render: () => (
42
+ <Card className="max-w-sm">
43
+ <CardHeader>
44
+ <CardTitle>Settings</CardTitle>
45
+ <CardDescription>Configure your preferences.</CardDescription>
46
+ </CardHeader>
47
+ <CardContent>
48
+ <div className="grid gap-3">
49
+ <div className="grid gap-1.5">
50
+ <Label htmlFor="card-name">Name</Label>
51
+ <Input id="card-name" placeholder="Enter name…" />
52
+ </div>
53
+
54
+ <Separator />
55
+
56
+ <div className="flex justify-end">
57
+ <Button>Save</Button>
58
+ </div>
59
+ </div>
60
+ </CardContent>
61
+ </Card>
62
+ )
63
+ }
@@ -0,0 +1,85 @@
1
+ import { cn } from '../../utils'
2
+
3
+ /**
4
+ * Themeable card primitive. Themes can restyle every card by setting CSS
5
+ * custom properties:
6
+ *
7
+ * --component-card-clip-path
8
+ * --component-card-border-image
9
+ * --component-card-background
10
+ * --component-card-box-shadow
11
+ *
12
+ * All are optional — unset vars compute to their CSS initial value.
13
+ */
14
+ const CARD_STYLE: React.CSSProperties = {
15
+ background: 'var(--component-card-background)',
16
+ borderImage: 'var(--component-card-border-image)',
17
+ boxShadow: 'var(--component-card-box-shadow)',
18
+ clipPath: 'var(--component-card-clip-path)'
19
+ }
20
+
21
+ export function Card({
22
+ className,
23
+ style,
24
+ ...props
25
+ }: React.HTMLAttributes<HTMLDivElement>) {
26
+ return (
27
+ <div
28
+ className={cn(
29
+ 'border border-midground/15 bg-background-base/80 text-midground w-full',
30
+ className
31
+ )}
32
+ style={{ ...CARD_STYLE, ...style }}
33
+ {...props}
34
+ />
35
+ )
36
+ }
37
+
38
+ export function CardHeader({
39
+ className,
40
+ ...props
41
+ }: React.HTMLAttributes<HTMLDivElement>) {
42
+ return (
43
+ <div
44
+ className={cn(
45
+ 'flex flex-col gap-1.5 p-4 border-b border-midground/15',
46
+ className
47
+ )}
48
+ {...props}
49
+ />
50
+ )
51
+ }
52
+
53
+ export function CardTitle({
54
+ className,
55
+ ...props
56
+ }: React.HTMLAttributes<HTMLHeadingElement>) {
57
+ return (
58
+ <h3
59
+ className={cn(
60
+ 'font-expanded text-sm font-bold tracking-[0.08em] uppercase',
61
+ className
62
+ )}
63
+ {...props}
64
+ />
65
+ )
66
+ }
67
+
68
+ export function CardDescription({
69
+ className,
70
+ ...props
71
+ }: React.HTMLAttributes<HTMLParagraphElement>) {
72
+ return (
73
+ <p
74
+ className={cn('font-mondwest text-xs text-midground/60', className)}
75
+ {...props}
76
+ />
77
+ )
78
+ }
79
+
80
+ export function CardContent({
81
+ className,
82
+ ...props
83
+ }: React.HTMLAttributes<HTMLDivElement>) {
84
+ return <div className={cn('p-4', className)} {...props} />
85
+ }