@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,169 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { useState } from 'react'
3
+
4
+ import { Button } from './button'
5
+ import {
6
+ Dialog,
7
+ DialogClose,
8
+ DialogContent,
9
+ DialogDescription,
10
+ DialogFooter,
11
+ DialogHeader,
12
+ DialogTitle,
13
+ DialogTrigger
14
+ } from './dialog'
15
+ import { Input } from './input'
16
+ import { Label } from './label'
17
+
18
+ const meta: Meta<typeof Dialog> = {
19
+ component: Dialog,
20
+ title: 'Components/Overlays/Dialog'
21
+ }
22
+
23
+ export default meta
24
+
25
+ type Story = StoryObj<typeof Dialog>
26
+
27
+ export const Default: Story = {
28
+ render: () => (
29
+ <Dialog>
30
+ <DialogTrigger asChild>
31
+ <Button>Open Dialog</Button>
32
+ </DialogTrigger>
33
+
34
+ <DialogContent>
35
+ <DialogHeader>
36
+ <DialogTitle>Dialog Title</DialogTitle>
37
+ <DialogDescription>
38
+ A description of the dialog content and its purpose.
39
+ </DialogDescription>
40
+ </DialogHeader>
41
+
42
+ <div className="p-4">
43
+ <p className="font-courier text-sm text-midground/80">
44
+ This is a general-purpose dialog built on Radix UI primitives. It
45
+ handles focus trapping, ESC to close, and backdrop click
46
+ automatically.
47
+ </p>
48
+ </div>
49
+
50
+ <DialogFooter>
51
+ <DialogClose asChild>
52
+ <Button outlined>Close</Button>
53
+ </DialogClose>
54
+ </DialogFooter>
55
+ </DialogContent>
56
+ </Dialog>
57
+ )
58
+ }
59
+
60
+ export const Controlled: Story = {
61
+ render: () => {
62
+ function Demo() {
63
+ const [open, setOpen] = useState(false)
64
+
65
+ return (
66
+ <>
67
+ <Button onClick={() => setOpen(true)}>Controlled Open</Button>
68
+
69
+ <Dialog onOpenChange={setOpen} open={open}>
70
+ <DialogContent>
71
+ <DialogHeader>
72
+ <DialogTitle>Controlled Dialog</DialogTitle>
73
+ <DialogDescription>
74
+ This dialog is controlled via external state.
75
+ </DialogDescription>
76
+ </DialogHeader>
77
+
78
+ <div className="p-4">
79
+ <p className="font-courier text-sm text-midground/80">
80
+ Open state is managed by the parent component. Useful when
81
+ you need to open the dialog programmatically.
82
+ </p>
83
+ </div>
84
+
85
+ <DialogFooter>
86
+ <Button onClick={() => setOpen(false)} outlined>
87
+ Cancel
88
+ </Button>
89
+
90
+ <Button onClick={() => setOpen(false)}>
91
+ Save
92
+ </Button>
93
+ </DialogFooter>
94
+ </DialogContent>
95
+ </Dialog>
96
+ </>
97
+ )
98
+ }
99
+
100
+ return <Demo />
101
+ }
102
+ }
103
+
104
+ export const WithForm: Story = {
105
+ render: () => (
106
+ <Dialog>
107
+ <DialogTrigger asChild>
108
+ <Button>Edit Profile</Button>
109
+ </DialogTrigger>
110
+
111
+ <DialogContent>
112
+ <DialogHeader>
113
+ <DialogTitle>Edit Profile</DialogTitle>
114
+ <DialogDescription>
115
+ Make changes to your profile. Click save when you are done.
116
+ </DialogDescription>
117
+ </DialogHeader>
118
+
119
+ <div className="flex flex-col gap-4 p-4">
120
+ <div className="flex flex-col gap-1.5">
121
+ <Label htmlFor="name">Name</Label>
122
+ <Input defaultValue="Hermes" id="name" />
123
+ </div>
124
+
125
+ <div className="flex flex-col gap-1.5">
126
+ <Label htmlFor="email">Email</Label>
127
+ <Input defaultValue="hermes@nousresearch.com" id="email" type="email" />
128
+ </div>
129
+ </div>
130
+
131
+ <DialogFooter>
132
+ <DialogClose asChild>
133
+ <Button outlined>Cancel</Button>
134
+ </DialogClose>
135
+
136
+ <DialogClose asChild>
137
+ <Button>Save Changes</Button>
138
+ </DialogClose>
139
+ </DialogFooter>
140
+ </DialogContent>
141
+ </Dialog>
142
+ )
143
+ }
144
+
145
+ export const NoCloseButton: Story = {
146
+ render: () => (
147
+ <Dialog>
148
+ <DialogTrigger asChild>
149
+ <Button>Without Close Button</Button>
150
+ </DialogTrigger>
151
+
152
+ <DialogContent showCloseButton={false}>
153
+ <DialogHeader>
154
+ <DialogTitle>Minimal Dialog</DialogTitle>
155
+ <DialogDescription>
156
+ This dialog hides the X close button. Users can still close it
157
+ by pressing ESC or clicking the backdrop.
158
+ </DialogDescription>
159
+ </DialogHeader>
160
+
161
+ <DialogFooter>
162
+ <DialogClose asChild>
163
+ <Button>Got it</Button>
164
+ </DialogClose>
165
+ </DialogFooter>
166
+ </DialogContent>
167
+ </Dialog>
168
+ )
169
+ }
@@ -0,0 +1,177 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import { Dialog as DialogPrimitive } from 'radix-ui'
5
+
6
+ import { cn } from '../../utils'
7
+
8
+ function Dialog({ ...props }: React.ComponentProps<typeof DialogPrimitive.Root>) {
9
+ return <DialogPrimitive.Root data-slot="dialog" {...props} />
10
+ }
11
+
12
+ function DialogTrigger({ ...props }: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
13
+ return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
14
+ }
15
+
16
+ function DialogPortal({ ...props }: React.ComponentProps<typeof DialogPrimitive.Portal>) {
17
+ return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
18
+ }
19
+
20
+ function DialogClose({ ...props }: React.ComponentProps<typeof DialogPrimitive.Close>) {
21
+ return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
22
+ }
23
+
24
+ function DialogOverlay({
25
+ className,
26
+ ...props
27
+ }: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
28
+ return (
29
+ <DialogPrimitive.Overlay
30
+ className={cn(
31
+ 'fixed inset-0 z-50',
32
+ 'bg-black/60 backdrop-blur-sm',
33
+ 'data-[state=open]:animate-in data-[state=open]:fade-in-0',
34
+ 'data-[state=closed]:animate-out data-[state=closed]:fade-out-0',
35
+ className
36
+ )}
37
+ data-slot="dialog-overlay"
38
+ {...props}
39
+ />
40
+ )
41
+ }
42
+
43
+ function DialogContent({
44
+ className,
45
+ children,
46
+ showCloseButton = true,
47
+ ...props
48
+ }: React.ComponentProps<typeof DialogPrimitive.Content> & {
49
+ showCloseButton?: boolean
50
+ }) {
51
+ return (
52
+ <DialogPortal>
53
+ <DialogOverlay />
54
+
55
+ <DialogPrimitive.Content
56
+ className={cn(
57
+ 'fixed top-1/2 left-1/2 z-50 -translate-x-1/2 -translate-y-1/2',
58
+ 'grid w-full max-w-md gap-0',
59
+ 'border border-midground/15 bg-background-base text-foreground-base shadow-lg outline-none',
60
+ 'data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
61
+ 'data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95',
62
+ 'duration-150',
63
+ className
64
+ )}
65
+ data-slot="dialog-content"
66
+ {...props}
67
+ >
68
+ {children}
69
+
70
+ {showCloseButton && (
71
+ <DialogPrimitive.Close
72
+ className={cn(
73
+ 'absolute top-3 right-3',
74
+ 'flex h-6 w-6 items-center justify-center',
75
+ 'text-midground/50 transition-colors hover:text-midground',
76
+ 'focus:outline-none focus-visible:ring-1 focus-visible:ring-midground/30',
77
+ 'disabled:pointer-events-none'
78
+ )}
79
+ data-slot="dialog-close"
80
+ >
81
+ <XIcon className="h-3.5 w-3.5" />
82
+ <span className="sr-only">Close</span>
83
+ </DialogPrimitive.Close>
84
+ )}
85
+ </DialogPrimitive.Content>
86
+ </DialogPortal>
87
+ )
88
+ }
89
+
90
+ function DialogHeader({ className, ...props }: React.ComponentProps<'div'>) {
91
+ return (
92
+ <div
93
+ className={cn(
94
+ 'flex flex-col gap-1 p-4 border-b border-midground/15',
95
+ className
96
+ )}
97
+ data-slot="dialog-header"
98
+ {...props}
99
+ />
100
+ )
101
+ }
102
+
103
+ function DialogFooter({ className, ...props }: React.ComponentProps<'div'>) {
104
+ return (
105
+ <div
106
+ className={cn(
107
+ 'flex items-center justify-end gap-2 p-3',
108
+ className
109
+ )}
110
+ data-slot="dialog-footer"
111
+ {...props}
112
+ />
113
+ )
114
+ }
115
+
116
+ function DialogTitle({
117
+ className,
118
+ ...props
119
+ }: React.ComponentProps<typeof DialogPrimitive.Title>) {
120
+ return (
121
+ <DialogPrimitive.Title
122
+ className={cn(
123
+ 'font-expanded text-sm font-bold tracking-[0.08em] uppercase',
124
+ className
125
+ )}
126
+ data-slot="dialog-title"
127
+ {...props}
128
+ />
129
+ )
130
+ }
131
+
132
+ function DialogDescription({
133
+ className,
134
+ ...props
135
+ }: React.ComponentProps<typeof DialogPrimitive.Description>) {
136
+ return (
137
+ <DialogPrimitive.Description
138
+ className={cn(
139
+ 'font-mondwest text-xs text-midground/60 leading-relaxed',
140
+ className
141
+ )}
142
+ data-slot="dialog-description"
143
+ {...props}
144
+ />
145
+ )
146
+ }
147
+
148
+ function XIcon({ className }: { className?: string }) {
149
+ return (
150
+ <svg
151
+ aria-hidden
152
+ className={className}
153
+ fill="none"
154
+ stroke="currentColor"
155
+ strokeLinecap="round"
156
+ strokeLinejoin="round"
157
+ strokeWidth={2}
158
+ viewBox="0 0 24 24"
159
+ >
160
+ <line x1="18" x2="6" y1="6" y2="18" />
161
+ <line x1="6" x2="18" y1="6" y2="18" />
162
+ </svg>
163
+ )
164
+ }
165
+
166
+ export {
167
+ Dialog,
168
+ DialogClose,
169
+ DialogContent,
170
+ DialogDescription,
171
+ DialogFooter,
172
+ DialogHeader,
173
+ DialogOverlay,
174
+ DialogPortal,
175
+ DialogTitle,
176
+ DialogTrigger
177
+ }
@@ -0,0 +1,52 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { useState } from 'react'
3
+
4
+ import { DropdownMenu } from './dropdown-menu'
5
+ import { Small } from './typography/small'
6
+
7
+ const OPTIONS = [
8
+ { label: 'Option A', value: 'a' as const },
9
+ { label: 'Option B', value: 'b' as const },
10
+ { label: 'Option C', value: 'c' as const }
11
+ ]
12
+
13
+ function Demo({ direction }: { direction: 'down' | 'left' | 'right' | 'up' }) {
14
+ const [value, setValue] = useState<'a' | 'b' | 'c'>('a')
15
+
16
+ return (
17
+ <DropdownMenu
18
+ direction={direction}
19
+ onChange={setValue}
20
+ options={OPTIONS}
21
+ value={value}
22
+ />
23
+ )
24
+ }
25
+
26
+ const meta: Meta<typeof DropdownMenu> = {
27
+ component: DropdownMenu,
28
+ title: 'Components/Overlays/DropdownMenu'
29
+ }
30
+
31
+ export default meta
32
+
33
+ type Story = StoryObj<typeof DropdownMenu>
34
+
35
+ export const Down: Story = { render: () => <Demo direction="down" /> }
36
+ export const Up: Story = { render: () => <Demo direction="up" /> }
37
+ export const Right: Story = { render: () => <Demo direction="right" /> }
38
+ export const Left: Story = { render: () => <Demo direction="left" /> }
39
+
40
+ export const AllDirections: Story = {
41
+ render: () => (
42
+ <div className="flex gap-10">
43
+ {(['down', 'up', 'right', 'left'] as const).map(direction => (
44
+ <div className="flex flex-col gap-1" key={direction}>
45
+ <Small className="capitalize opacity-40">{direction}</Small>
46
+
47
+ <Demo direction={direction} />
48
+ </div>
49
+ ))}
50
+ </div>
51
+ )
52
+ }
@@ -0,0 +1,117 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useId, useRef, useState } from 'react'
4
+
5
+ import { cn } from '../../utils'
6
+
7
+ const font = 'font-mondwest text-[.9375rem] leading-[1.4] tracking-[0.1875rem]'
8
+
9
+ type Direction = 'down' | 'up' | 'left' | 'right'
10
+
11
+ type AnchorStyle = React.CSSProperties & Record<string, string | number>
12
+
13
+ export function DropdownMenu<T extends string>({
14
+ className,
15
+ direction = 'down',
16
+ onChange,
17
+ options,
18
+ value
19
+ }: {
20
+ className?: string
21
+ direction?: Direction
22
+ onChange: (value: T) => void
23
+ options: { label: string; value: T }[]
24
+ value: T
25
+ }) {
26
+ const id = useId()
27
+ const [open, setOpen] = useState(false)
28
+ const ref = useRef<HTMLSpanElement>(null)
29
+
30
+ const anchor = `--dropdown-${id.replace(/:/g, '')}`
31
+
32
+ const panelStyle: AnchorStyle = {
33
+ position: 'fixed',
34
+ positionAnchor: anchor,
35
+ positionTryFallbacks:
36
+ direction === 'left' || direction === 'right'
37
+ ? 'flip-inline, flip-block'
38
+ : 'flip-block, flip-inline',
39
+ ...(direction === 'up' && {
40
+ left: 'calc(anchor(left) - 0.5rem)',
41
+ top: 'calc(anchor(top) + 1rem)',
42
+ transform: 'translateY(-100%)'
43
+ }),
44
+ ...(direction === 'right' && {
45
+ left: 'calc(anchor(right))',
46
+ top: 'calc(anchor(top) - 0.5rem)'
47
+ }),
48
+ ...(direction === 'left' && {
49
+ left: 'calc(anchor(left) - 1px)',
50
+ top: 'calc(anchor(top) - 0.5rem)',
51
+ transform: 'translateX(-100%)'
52
+ }),
53
+ ...(direction === 'down' && {
54
+ left: 'calc(anchor(left) - 0.5rem)',
55
+ top: 'calc(anchor(top) - 0.5rem)'
56
+ })
57
+ }
58
+
59
+ useEffect(() => {
60
+ if (!open) {
61
+ return
62
+ }
63
+
64
+ const ac = new AbortController()
65
+ document.addEventListener(
66
+ 'mousedown',
67
+ e => {
68
+ if (!ref.current?.contains(e.target as Node)) {
69
+ setOpen(false)
70
+ }
71
+ },
72
+ { signal: ac.signal }
73
+ )
74
+
75
+ return () => ac.abort()
76
+ }, [open])
77
+
78
+ return (
79
+ <span
80
+ className={cn('relative inline-block align-top', className)}
81
+ ref={ref}
82
+ >
83
+ <span
84
+ className={cn(font, 'inline-block cursor-pointer hover:underline')}
85
+ onClick={() => setOpen(!open)}
86
+ style={{ anchorName: anchor } as AnchorStyle}
87
+ >
88
+ {options.find(o => o.value === value)?.label ?? value}{' '}
89
+ {open ? '↑' : '↓'}
90
+ </span>
91
+
92
+ {open && (
93
+ <div
94
+ className="bg-background-base z-50 flex flex-col"
95
+ style={panelStyle}
96
+ >
97
+ {options.map(o => (
98
+ <span
99
+ className={cn(
100
+ font,
101
+ 'block cursor-pointer p-2 whitespace-nowrap',
102
+ o.value === value ? 'underline' : 'hover:bg-midground/10'
103
+ )}
104
+ key={o.value}
105
+ onClick={() => {
106
+ onChange(o.value)
107
+ setOpen(false)
108
+ }}
109
+ >
110
+ {o.label}
111
+ </span>
112
+ ))}
113
+ </div>
114
+ )}
115
+ </span>
116
+ )
117
+ }
@@ -0,0 +1,42 @@
1
+ .fit-text {
2
+ --fit-captured-length: initial;
3
+ --fit-support-sentinel: var(--fit-captured-length, 9999px);
4
+
5
+ display: flex;
6
+ container-type: inline-size;
7
+
8
+ > [aria-hidden] {
9
+ visibility: hidden;
10
+ }
11
+
12
+ > :not([aria-hidden]) {
13
+ flex-grow: 1;
14
+ container-type: inline-size;
15
+
16
+ --fit-captured-length: 100cqi;
17
+ --fit-available-space: var(--fit-captured-length);
18
+
19
+ > * {
20
+ --fit-support-sentinel: inherit;
21
+ --fit-captured-length: 100cqi;
22
+ --fit-ratio: tan(
23
+ atan2(
24
+ var(--fit-available-space),
25
+ var(--fit-available-space) - var(--fit-captured-length)
26
+ )
27
+ );
28
+
29
+ display: block;
30
+ inline-size: var(--fit-available-space);
31
+ font-size: clamp(
32
+ var(--fit-min, 1em),
33
+ 1em * var(--fit-ratio),
34
+ var(--fit-max, infinity * 1px) - var(--fit-support-sentinel)
35
+ );
36
+
37
+ @container (inline-size > 0) {
38
+ white-space: nowrap;
39
+ }
40
+ }
41
+ }
42
+ }
@@ -0,0 +1,33 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+
3
+ import { FitText } from '../fit-text'
4
+
5
+ const meta: Meta<typeof FitText> = {
6
+ args: { children: 'Fit Text', max: 'infinity * 1px', min: '1em' },
7
+ component: FitText,
8
+ title: 'Components/Effects/FitText'
9
+ }
10
+
11
+ export default meta
12
+
13
+ type Story = StoryObj<typeof FitText>
14
+
15
+ export const Playground: Story = {}
16
+
17
+ export const Fills: Story = {
18
+ render: () => (
19
+ <div className="w-full">
20
+ <FitText className="font-sans font-bold">Design System</FitText>
21
+ </div>
22
+ )
23
+ }
24
+
25
+ export const CappedMax: Story = {
26
+ render: () => (
27
+ <div className="w-full">
28
+ <FitText className="font-mondwest" max="4rem">
29
+ Capped max at 4rem
30
+ </FitText>
31
+ </div>
32
+ )
33
+ }
@@ -0,0 +1,45 @@
1
+ 'use client'
2
+
3
+ import { createElement } from 'react'
4
+
5
+ import { cn, type PolyProps, polyRef } from '../../../utils'
6
+
7
+ export const FitText = polyRef<'span', OwnProps>(
8
+ (
9
+ { as, children, className, max, min = '1em', style: baseStyle, ...rest },
10
+ ref
11
+ ) => {
12
+ if (typeof children !== 'string') {
13
+ return null
14
+ }
15
+
16
+ const style = {
17
+ '--fit-max': max ?? 'infinity * 1px',
18
+ '--fit-min': min,
19
+ ...baseStyle
20
+ } as React.CSSProperties
21
+
22
+ return createElement(
23
+ (as ?? 'span') as React.ElementType,
24
+ { ...rest, className: cn('fit-text', className), ref, style },
25
+ <>
26
+ <span>
27
+ <span>{children}</span>
28
+ </span>
29
+
30
+ <span aria-hidden="true">{children}</span>
31
+ </>
32
+ )
33
+ }
34
+ )
35
+
36
+ interface OwnProps {
37
+ children: string
38
+ max?: string
39
+ min?: string
40
+ }
41
+
42
+ export type FitTextProps<T extends React.ElementType = 'span'> = PolyProps<
43
+ T,
44
+ OwnProps
45
+ >