@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,113 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { useState } from 'react'
3
+
4
+ import { Checkbox } from './checkbox'
5
+ import { Small } from './typography/small'
6
+
7
+ function Demo({ disabled }: { disabled?: boolean }) {
8
+ const [checked, setChecked] = useState(false)
9
+
10
+ return (
11
+ <div className="flex items-center gap-2.5">
12
+ <Checkbox
13
+ checked={checked}
14
+ disabled={disabled}
15
+ id="checkbox-demo"
16
+ onCheckedChange={setChecked}
17
+ />
18
+
19
+ <label className="cursor-pointer text-sm" htmlFor="checkbox-demo">
20
+ Accept terms
21
+ </label>
22
+ </div>
23
+ )
24
+ }
25
+
26
+ const meta: Meta<typeof Checkbox> = {
27
+ component: Checkbox,
28
+ title: 'Components/Forms/Checkbox'
29
+ }
30
+
31
+ export default meta
32
+
33
+ type Story = StoryObj<typeof Checkbox>
34
+
35
+ export const Playground: Story = { render: () => <Demo /> }
36
+
37
+ export const Disabled: Story = { render: () => <Demo disabled /> }
38
+
39
+ export const Checked: Story = {
40
+ render: () => (
41
+ <div className="flex items-center gap-2.5">
42
+ <Checkbox defaultChecked id="checkbox-checked" />
43
+
44
+ <label className="cursor-pointer text-sm" htmlFor="checkbox-checked">
45
+ Clone from default profile
46
+ </label>
47
+ </div>
48
+ )
49
+ }
50
+
51
+ export const Stack: Story = {
52
+ render: () => {
53
+ function StackDemo() {
54
+ const [a, setA] = useState(true)
55
+ const [b, setB] = useState(false)
56
+ const [c, setC] = useState(true)
57
+
58
+ return (
59
+ <div className="grid w-72 gap-3">
60
+ <div className="flex items-center gap-2.5">
61
+ <Checkbox checked={a} id="opt-a" onCheckedChange={setA} />
62
+
63
+ <label className="cursor-pointer text-sm" htmlFor="opt-a">
64
+ Logging
65
+ </label>
66
+ </div>
67
+
68
+ <div className="flex items-center gap-2.5">
69
+ <Checkbox checked={b} id="opt-b" onCheckedChange={setB} />
70
+
71
+ <label className="cursor-pointer text-sm" htmlFor="opt-b">
72
+ Telemetry
73
+ </label>
74
+ </div>
75
+
76
+ <div className="flex items-center gap-2.5">
77
+ <Checkbox checked={c} id="opt-c" onCheckedChange={setC} />
78
+
79
+ <label className="cursor-pointer text-sm" htmlFor="opt-c">
80
+ Auto-update
81
+ </label>
82
+ </div>
83
+ </div>
84
+ )
85
+ }
86
+
87
+ return <StackDemo />
88
+ }
89
+ }
90
+
91
+ export const NextToLabel: Story = {
92
+ render: () => {
93
+ function LabeledDemo() {
94
+ const [checked, setChecked] = useState(true)
95
+
96
+ return (
97
+ <div className="flex items-center gap-3">
98
+ <Small className="opacity-60 uppercase tracking-wider">
99
+ Persist globally
100
+ </Small>
101
+
102
+ <Checkbox
103
+ checked={checked}
104
+ id="persist-global"
105
+ onCheckedChange={setChecked}
106
+ />
107
+ </div>
108
+ )
109
+ }
110
+
111
+ return <LabeledDemo />
112
+ }
113
+ }
@@ -0,0 +1,36 @@
1
+ 'use client'
2
+
3
+ import { forwardRef, type ComponentPropsWithoutRef, type ElementRef } from 'react'
4
+ import { Checkbox as CheckboxPrimitive } from 'radix-ui'
5
+
6
+ import { cn } from '../../utils'
7
+
8
+ import { CheckIcon } from './icons/check'
9
+
10
+ export const Checkbox = forwardRef<
11
+ ElementRef<typeof CheckboxPrimitive.Root>,
12
+ CheckboxProps
13
+ >(function Checkbox({ className, ...props }, ref) {
14
+ return (
15
+ <CheckboxPrimitive.Root
16
+ className={cn(
17
+ 'peer flex h-4 w-4 shrink-0 cursor-pointer items-center justify-center border transition-colors outline-none',
18
+ 'focus-visible:ring-1 focus-visible:ring-midground/30',
19
+ 'disabled:cursor-not-allowed disabled:opacity-50',
20
+ 'data-[state=unchecked]:border-midground/20 data-[state=unchecked]:bg-background',
21
+ 'data-[state=unchecked]:hover:border-midground/30',
22
+ 'data-[state=checked]:border-midground/30 data-[state=checked]:bg-midground/15',
23
+ 'data-[state=indeterminate]:border-midground/30 data-[state=indeterminate]:bg-midground/15',
24
+ className
25
+ )}
26
+ ref={ref}
27
+ {...props}
28
+ >
29
+ <CheckboxPrimitive.Indicator className="flex items-center justify-center text-current">
30
+ <CheckIcon className="h-3 w-3 text-midground" />
31
+ </CheckboxPrimitive.Indicator>
32
+ </CheckboxPrimitive.Root>
33
+ )
34
+ })
35
+
36
+ type CheckboxProps = ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
@@ -0,0 +1,52 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+
3
+ import {
4
+ CommandBlock,
5
+ CopyButton
6
+ } from './command-block'
7
+
8
+ const meta: Meta<typeof CommandBlock> = {
9
+ args: {
10
+ code: 'curl -fsSL https://hermes.nousresearch.com/install.sh | bash',
11
+ label: '1. Install'
12
+ },
13
+ component: CommandBlock,
14
+ title: 'Components/Data Display/CommandBlock'
15
+ }
16
+
17
+ export default meta
18
+
19
+ type Story = StoryObj<typeof CommandBlock>
20
+
21
+ export const Default: Story = {
22
+ render: args => (
23
+ <div className="w-[520px]">
24
+ <CommandBlock {...args} />
25
+ </div>
26
+ )
27
+ }
28
+
29
+ export const TwoStep: Story = {
30
+ render: () => (
31
+ <div className="flex w-[520px] flex-col gap-3">
32
+ <CommandBlock
33
+ code="curl -fsSL https://hermes.nousresearch.com/install.sh | bash"
34
+ label="1. Install"
35
+ />
36
+
37
+ <CommandBlock code="hermes setup" label="2. Configure" />
38
+ </div>
39
+ )
40
+ }
41
+
42
+ export const StandaloneButton: Story = {
43
+ render: () => (
44
+ <div className="flex items-center gap-3">
45
+ <span className="font-courier text-xs opacity-60">
46
+ echo &quot;hello world&quot;
47
+ </span>
48
+
49
+ <CopyButton text="echo 'hello world'" />
50
+ </div>
51
+ )
52
+ }
@@ -0,0 +1,86 @@
1
+ 'use client'
2
+
3
+ import { useCallback, useState } from 'react'
4
+
5
+ import { cn } from '../../utils'
6
+
7
+ import { Small } from './typography/small'
8
+
9
+ /**
10
+ * A "copy to clipboard" button that briefly shows a "Copied!" confirmation.
11
+ * Designed to sit alongside a short command string, not as a general button.
12
+ */
13
+ export function CopyButton({
14
+ children,
15
+ className,
16
+ copiedLabel = 'Copied!',
17
+ label = 'Copy',
18
+ resetDelayMs = 2000,
19
+ text
20
+ }: CopyButtonProps) {
21
+ const [copied, setCopied] = useState(false)
22
+
23
+ const handleCopy = useCallback(() => {
24
+ void navigator.clipboard.writeText(text).then(() => {
25
+ setCopied(true)
26
+ setTimeout(() => setCopied(false), resetDelayMs)
27
+ })
28
+ }, [resetDelayMs, text])
29
+
30
+ return (
31
+ <button
32
+ className={cn(
33
+ 'font-courier text-display cursor-pointer border-none bg-transparent text-xs',
34
+ 'tracking-widest',
35
+ 'hover:text-midground tap-highlight-transparent transition-colors',
36
+ 'flex items-center justify-center',
37
+ copied ? 'text-midground' : 'text-text-secondary',
38
+ className
39
+ )}
40
+ onClick={handleCopy}
41
+ type="button"
42
+ >
43
+ {children ?? (copied ? copiedLabel : label)}
44
+ </button>
45
+ )
46
+ }
47
+
48
+ /**
49
+ * A labeled, copy-able command (or code) display. Pairs `<CopyButton>` with
50
+ * a monospace code block. Used for install/setup instructions.
51
+ */
52
+ export function CommandBlock({ className, code, label }: CommandBlockProps) {
53
+ return (
54
+ <div className={cn('flex flex-col gap-1', className)}>
55
+ <div className="flex items-center justify-between">
56
+ <Small className="opacity-50">{label}</Small>
57
+
58
+ <CopyButton text={code} />
59
+ </div>
60
+
61
+ <div
62
+ className={cn(
63
+ 'bg-background/40 font-courier border border-current/20',
64
+ 'px-3 py-2 text-[0.6875rem] leading-relaxed lowercase'
65
+ )}
66
+ >
67
+ <code className="break-all">{code}</code>
68
+ </div>
69
+ </div>
70
+ )
71
+ }
72
+
73
+ interface CommandBlockProps {
74
+ className?: string
75
+ code: string
76
+ label: string
77
+ }
78
+
79
+ interface CopyButtonProps {
80
+ children?: React.ReactNode
81
+ className?: string
82
+ copiedLabel?: string
83
+ label?: string
84
+ resetDelayMs?: number
85
+ text: string
86
+ }
@@ -0,0 +1,91 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { useState } from 'react'
3
+
4
+ import { Button } from './button'
5
+ import { ConfirmDialog } from './confirm-dialog'
6
+
7
+ const meta: Meta<typeof ConfirmDialog> = {
8
+ component: ConfirmDialog,
9
+ title: 'Components/Overlays/ConfirmDialog'
10
+ }
11
+
12
+ export default meta
13
+
14
+ type Story = StoryObj<typeof ConfirmDialog>
15
+
16
+ export const Default: Story = {
17
+ render: () => {
18
+ function Demo() {
19
+ const [open, setOpen] = useState(false)
20
+
21
+ return (
22
+ <>
23
+ <Button onClick={() => setOpen(true)}>Open dialog</Button>
24
+
25
+ <ConfirmDialog
26
+ description="This action cannot be undone."
27
+ onCancel={() => setOpen(false)}
28
+ onConfirm={() => setOpen(false)}
29
+ open={open}
30
+ title="Are you sure?"
31
+ />
32
+ </>
33
+ )
34
+ }
35
+
36
+ return <Demo />
37
+ }
38
+ }
39
+
40
+ export const Destructive: Story = {
41
+ render: () => {
42
+ function Demo() {
43
+ const [open, setOpen] = useState(false)
44
+
45
+ return (
46
+ <>
47
+ <Button destructive onClick={() => setOpen(true)}>
48
+ Delete item
49
+ </Button>
50
+
51
+ <ConfirmDialog
52
+ confirmLabel="Delete"
53
+ description="This will permanently delete the item. This action cannot be undone."
54
+ destructive
55
+ onCancel={() => setOpen(false)}
56
+ onConfirm={() => setOpen(false)}
57
+ open={open}
58
+ title="Delete item?"
59
+ />
60
+ </>
61
+ )
62
+ }
63
+
64
+ return <Demo />
65
+ }
66
+ }
67
+
68
+ export const Loading: Story = {
69
+ render: () => {
70
+ function Demo() {
71
+ const [open, setOpen] = useState(false)
72
+
73
+ return (
74
+ <>
75
+ <Button onClick={() => setOpen(true)}>With loading state</Button>
76
+
77
+ <ConfirmDialog
78
+ description="Simulating a loading state."
79
+ loading
80
+ onCancel={() => setOpen(false)}
81
+ onConfirm={() => {}}
82
+ open={open}
83
+ title="Processing…"
84
+ />
85
+ </>
86
+ )
87
+ }
88
+
89
+ return <Demo />
90
+ }
91
+ }
@@ -0,0 +1,130 @@
1
+ 'use client'
2
+
3
+ import { useRef } from 'react'
4
+ import { AlertDialog as AlertDialogPrimitive } from 'radix-ui'
5
+
6
+ import { cn } from '../../utils'
7
+ import { Button } from './button'
8
+
9
+ function WarningTriangle({ className }: { className?: string }) {
10
+ return (
11
+ <svg
12
+ aria-hidden
13
+ className={className}
14
+ fill="none"
15
+ stroke="currentColor"
16
+ strokeLinecap="round"
17
+ strokeLinejoin="round"
18
+ strokeWidth={2}
19
+ viewBox="0 0 24 24"
20
+ >
21
+ <path d="m10.29 3.86-8.16 14a2 2 0 0 0 1.73 3h16.28a2 2 0 0 0 1.73-3l-8.16-14a2 2 0 0 0-3.46 0z" />
22
+ <line x1="12" x2="12" y1="9" y2="13" />
23
+ <line x1="12" x2="12.01" y1="17" y2="17" />
24
+ </svg>
25
+ )
26
+ }
27
+
28
+ export function ConfirmDialog({
29
+ cancelLabel = 'Cancel',
30
+ confirmLabel = 'Confirm',
31
+ description,
32
+ destructive = false,
33
+ loading = false,
34
+ onCancel,
35
+ onConfirm,
36
+ open,
37
+ title
38
+ }: ConfirmDialogProps) {
39
+ const confirmedRef = useRef(false)
40
+
41
+ return (
42
+ <AlertDialogPrimitive.Root
43
+ onOpenChange={v => {
44
+ if (!v && !confirmedRef.current) onCancel()
45
+ confirmedRef.current = false
46
+ }}
47
+ open={open}
48
+ >
49
+ <AlertDialogPrimitive.Portal>
50
+ <AlertDialogPrimitive.Overlay
51
+ className={cn(
52
+ 'fixed inset-0 z-50',
53
+ 'bg-black/60 backdrop-blur-sm',
54
+ 'data-[state=open]:animate-in data-[state=open]:fade-in-0',
55
+ 'data-[state=closed]:animate-out data-[state=closed]:fade-out-0'
56
+ )}
57
+ />
58
+
59
+ <AlertDialogPrimitive.Content
60
+ className={cn(
61
+ 'fixed top-1/2 left-1/2 z-50 -translate-x-1/2 -translate-y-1/2',
62
+ 'w-[calc(100%-2rem)] max-w-md',
63
+ 'border border-midground/15 bg-background-base text-foreground-base shadow-lg outline-none',
64
+ 'data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
65
+ 'data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95',
66
+ 'duration-150'
67
+ )}
68
+ >
69
+ <div className="flex items-start gap-3 p-4 border-b border-midground/15">
70
+ {destructive && (
71
+ <div aria-hidden className="mt-0.5 shrink-0 text-destructive">
72
+ <WarningTriangle className="h-4 w-4" />
73
+ </div>
74
+ )}
75
+
76
+ <div className="flex-1 min-w-0 flex flex-col gap-1">
77
+ <AlertDialogPrimitive.Title
78
+ className="font-expanded text-sm font-bold tracking-[0.08em] uppercase"
79
+ >
80
+ {title}
81
+ </AlertDialogPrimitive.Title>
82
+
83
+ {description && (
84
+ <AlertDialogPrimitive.Description
85
+ className="font-mondwest text-xs text-midground/60 leading-relaxed"
86
+ >
87
+ {description}
88
+ </AlertDialogPrimitive.Description>
89
+ )}
90
+ </div>
91
+ </div>
92
+
93
+ <div className="flex items-center justify-end gap-2 p-3">
94
+ <AlertDialogPrimitive.Cancel asChild>
95
+ <Button disabled={loading} outlined type="button">
96
+ {cancelLabel}
97
+ </Button>
98
+ </AlertDialogPrimitive.Cancel>
99
+
100
+ <AlertDialogPrimitive.Action asChild>
101
+ <Button
102
+ destructive={destructive}
103
+ disabled={loading}
104
+ onClick={() => {
105
+ confirmedRef.current = true
106
+ onConfirm()
107
+ }}
108
+ type="button"
109
+ >
110
+ {loading ? '…' : confirmLabel}
111
+ </Button>
112
+ </AlertDialogPrimitive.Action>
113
+ </div>
114
+ </AlertDialogPrimitive.Content>
115
+ </AlertDialogPrimitive.Portal>
116
+ </AlertDialogPrimitive.Root>
117
+ )
118
+ }
119
+
120
+ interface ConfirmDialogProps {
121
+ cancelLabel?: string
122
+ confirmLabel?: string
123
+ description?: string
124
+ destructive?: boolean
125
+ loading?: boolean
126
+ onCancel: () => void
127
+ onConfirm: () => void
128
+ open: boolean
129
+ title: string
130
+ }
@@ -0,0 +1,115 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useRef } from 'react'
4
+
5
+ const INTERACTIVE =
6
+ 'a, button, [role="button"], input, textarea, select, [data-cursor]'
7
+
8
+ const HAND =
9
+ 'M6.84 21.83c-.47-.6-1.05-1.82-2.07-3.34-.58-.83-2.01-2.41-2.45-3.23a2.1 2.1 0 0 1-.25-1.67 2.2 2.2 0 0 1 2.39-1.67c.85.18 1.63.6 2.25 1.2.43.41.82.85 1.18 1.32.27.34.33.47.63.85.3.39.5.77.35.2-.11-.83-.31-2.23-.6-3.48-.21-.95-.26-1.1-.46-1.82s-.32-1.32-.54-2.13c-.2-.8-.35-1.62-.46-2.44a4.7 4.7 0 0 1 .43-3.08c.58-.55 1.44-.7 2.17-.37a4.4 4.4 0 0 1 1.57 2.17c.43 1.07.72 2.19.86 3.33.27 1.67.79 4.1.8 4.6 0-.61-.11-1.91 0-2.5.12-.6.54-1.1 1.12-1.33.5-.15 1.02-.19 1.53-.1.52.1.98.4 1.29.83.38.98.6 2 .63 3.05.04-.91.2-1.82.47-2.7.28-.39.68-.67 1.15-.8.55-.1 1.11-.1 1.66 0 .46.15.85.44 1.14.82.35.88.56 1.82.63 2.77 0 .23.12-.65.48-1.24a1.67 1.67 0 1 1 3.17 1.07v3.77c-.06.97-.2 1.94-.4 2.9-.29.85-.7 1.65-1.2 2.38-.8.9-1.48 1.92-1.98 3.02a6.67 6.67 0 0 0 .03 3.2c-.68.07-1.37.07-2.05 0-.65-.1-1.45-1.4-1.67-1.8a.63.63 0 0 0-1.13 0c-.37.64-1.18 1.79-1.75 1.85-1.12.14-3.42 0-5.23 0 0 0 .3-1.66-.39-2.27-.68-.6-1.38-1.3-1.9-1.76l-1.4-1.6Z'
10
+
11
+ export function Cursor({ scale = 0.8 }: { scale?: number }) {
12
+ const $root = useRef<HTMLDivElement>(null)
13
+ const $arrow = useRef<HTMLDivElement>(null)
14
+ const $ptr = useRef<HTMLDivElement>(null)
15
+
16
+ useEffect(() => {
17
+ const [root, arrow, ptr] = [$root.current, $arrow.current, $ptr.current]
18
+
19
+ if (!root || !arrow || !ptr) {
20
+ return
21
+ }
22
+
23
+ const on = (
24
+ el: EventTarget,
25
+ ev: string,
26
+ fn: EventListener,
27
+ opts?: AddEventListenerOptions
28
+ ) => {
29
+ el.addEventListener(ev, fn, opts)
30
+
31
+ return () => el.removeEventListener(ev, fn)
32
+ }
33
+
34
+ return [
35
+ on(
36
+ document,
37
+ 'mousemove',
38
+ (e: Event) => {
39
+ const { clientX: x, clientY: y } = e as MouseEvent
40
+ root.style.translate = `${x}px ${y}px`
41
+ root.style.opacity = '1'
42
+ },
43
+ { passive: true }
44
+ ),
45
+
46
+ on(
47
+ document,
48
+ 'mouseover',
49
+ (e: Event) => {
50
+ const isPtr = !!(e.target as HTMLElement).closest?.(INTERACTIVE)
51
+ arrow.style.opacity = isPtr ? '0' : '1'
52
+ ptr.style.opacity = isPtr ? '1' : '0'
53
+ },
54
+ { passive: true }
55
+ ),
56
+
57
+ on(document, 'mousedown', () => {
58
+ root.style.transform = 'translate(1px, 1px)'
59
+ }),
60
+ on(document, 'mouseup', () => {
61
+ root.style.transform = ''
62
+ }),
63
+ on(document.documentElement, 'mouseleave', () => {
64
+ root.style.opacity = '0'
65
+ }),
66
+ on(document.documentElement, 'mouseenter', () => {
67
+ root.style.opacity = '1'
68
+ })
69
+ ].reduce((_, fn) => fn, undefined as unknown as void)
70
+ }, [])
71
+
72
+ return (
73
+ <div
74
+ aria-hidden
75
+ ref={$root}
76
+ style={{
77
+ filter: 'drop-shadow(1px 2px 0 #000)',
78
+ height: 32 * scale,
79
+ left: 0,
80
+ opacity: 0,
81
+ pointerEvents: 'none',
82
+ position: 'fixed',
83
+ top: 0,
84
+ width: 32 * scale,
85
+ willChange: 'translate',
86
+ zIndex: 9999
87
+ }}
88
+ >
89
+ <div ref={$arrow} style={{ inset: 0, position: 'absolute' }}>
90
+ <svg viewBox="0 0 16 16">
91
+ <path
92
+ d="M1 1L1 14L5 10L8 15L10 14L7 9L12 9L1 1Z"
93
+ fill="#fff"
94
+ stroke="#000"
95
+ strokeLinejoin="round"
96
+ strokeWidth={1}
97
+ />
98
+ </svg>
99
+ </div>
100
+
101
+ <div ref={$ptr} style={{ inset: 0, opacity: 0, position: 'absolute' }}>
102
+ <svg viewBox="0 0 28 29">
103
+ <path
104
+ d={HAND}
105
+ fill="#fff"
106
+ stroke="#000"
107
+ strokeLinejoin="round"
108
+ strokeWidth={2}
109
+ style={{ paintOrder: 'stroke fill' }}
110
+ />
111
+ </svg>
112
+ </div>
113
+ </div>
114
+ )
115
+ }