@nous-research/ui 0.16.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 (86) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/dist/hooks/use-below-breakpoint.d.ts +2 -0
  3. package/dist/hooks/use-below-breakpoint.js +17 -0
  4. package/dist/hooks/use-confirm-delete.d.ts +10 -0
  5. package/dist/hooks/use-confirm-delete.js +35 -0
  6. package/dist/hooks/use-toast.d.ts +7 -0
  7. package/dist/hooks/use-toast.js +21 -0
  8. package/dist/index.d.ts +11 -1
  9. package/dist/index.js +23 -2
  10. package/dist/ui/components/bottom-sheet.d.ts +15 -0
  11. package/dist/ui/components/bottom-sheet.js +192 -0
  12. package/dist/ui/components/card.d.ts +5 -0
  13. package/dist/ui/components/card.js +74 -0
  14. package/dist/ui/components/checkbox.d.ts +1 -1
  15. package/dist/ui/components/checkbox.js +2 -2
  16. package/dist/ui/components/confirm-dialog.d.ts +13 -0
  17. package/dist/ui/components/confirm-dialog.js +113 -0
  18. package/dist/ui/components/dialog.d.ts +15 -0
  19. package/dist/ui/components/dialog.js +171 -0
  20. package/dist/ui/components/input.d.ts +1 -0
  21. package/dist/ui/components/input.js +21 -0
  22. package/dist/ui/components/label.d.ts +1 -0
  23. package/dist/ui/components/label.js +18 -0
  24. package/dist/ui/components/separator.d.ts +5 -0
  25. package/dist/ui/components/separator.js +22 -0
  26. package/dist/ui/components/toast.d.ts +8 -0
  27. package/dist/ui/components/toast.js +39 -0
  28. package/dist/ui/globals.css +14 -2
  29. package/package.json +2 -2
  30. package/src/hooks/use-below-breakpoint.ts +21 -0
  31. package/src/hooks/use-confirm-delete.ts +43 -0
  32. package/src/hooks/use-toast.ts +29 -0
  33. package/src/index.ts +22 -1
  34. package/src/ui/components/animated-count.stories.tsx +1 -1
  35. package/src/ui/components/ascii.stories.tsx +1 -1
  36. package/src/ui/components/badge.stories.tsx +1 -1
  37. package/src/ui/components/blend-mode.stories.tsx +1 -1
  38. package/src/ui/components/blink.stories.tsx +1 -1
  39. package/src/ui/components/bottom-sheet.stories.tsx +43 -0
  40. package/src/ui/components/bottom-sheet.tsx +227 -0
  41. package/src/ui/components/button.stories.tsx +1 -1
  42. package/src/ui/components/card.stories.tsx +63 -0
  43. package/src/ui/components/card.tsx +85 -0
  44. package/src/ui/components/checkbox.stories.tsx +1 -1
  45. package/src/ui/components/checkbox.tsx +1 -1
  46. package/src/ui/components/command-block.stories.tsx +1 -1
  47. package/src/ui/components/confirm-dialog.stories.tsx +91 -0
  48. package/src/ui/components/confirm-dialog.tsx +130 -0
  49. package/src/ui/components/dialog.stories.tsx +169 -0
  50. package/src/ui/components/dialog.tsx +177 -0
  51. package/src/ui/components/dropdown-menu.stories.tsx +1 -1
  52. package/src/ui/components/fit-text/index.stories.tsx +1 -1
  53. package/src/ui/components/forms.stories.tsx +173 -0
  54. package/src/ui/components/graphs/index.stories.tsx +1 -1
  55. package/src/ui/components/hover-bg.stories.tsx +1 -1
  56. package/src/ui/components/image-distortion.stories.tsx +1 -1
  57. package/src/ui/components/input.stories.tsx +39 -0
  58. package/src/ui/components/input.tsx +20 -0
  59. package/src/ui/components/label.stories.tsx +26 -0
  60. package/src/ui/components/label.tsx +16 -0
  61. package/src/ui/components/list-item.stories.tsx +1 -1
  62. package/src/ui/components/poster.stories.tsx +1 -1
  63. package/src/ui/components/progress.stories.tsx +1 -1
  64. package/src/ui/components/scramble.stories.tsx +1 -1
  65. package/src/ui/components/segmented.stories.tsx +1 -1
  66. package/src/ui/components/select.stories.tsx +1 -1
  67. package/src/ui/components/separator.stories.tsx +33 -0
  68. package/src/ui/components/separator.tsx +24 -0
  69. package/src/ui/components/spinner.stories.tsx +1 -1
  70. package/src/ui/components/stats.stories.tsx +1 -1
  71. package/src/ui/components/switch.stories.tsx +1 -1
  72. package/src/ui/components/tabs.stories.tsx +1 -1
  73. package/src/ui/components/terminal-demo.stories.tsx +1 -1
  74. package/src/ui/components/theme-toggle.stories.tsx +1 -1
  75. package/src/ui/components/tier-card.stories.tsx +1 -1
  76. package/src/ui/components/toast.stories.tsx +55 -0
  77. package/src/ui/components/toast.tsx +49 -0
  78. package/src/ui/components/tv.stories.tsx +1 -1
  79. package/src/ui/components/watchlist.stories.tsx +1 -1
  80. package/src/ui/globals.css +14 -2
  81. package/dist/ui/components/modal/index.d.ts +0 -8
  82. package/dist/ui/components/modal/index.js +0 -35
  83. package/dist/ui/components/modal/modal.css +0 -36
  84. package/src/ui/components/modal/index.stories.tsx +0 -46
  85. package/src/ui/components/modal/index.tsx +0 -48
  86. package/src/ui/components/modal/modal.css +0 -36
@@ -0,0 +1,29 @@
1
+ 'use client'
2
+
3
+ import { useCallback, useEffect, useRef, useState } from 'react'
4
+
5
+ export function useToast(duration = 3000) {
6
+ const [toast, setToast] = useState<{
7
+ message: string
8
+ type: 'error' | 'success'
9
+ } | null>(null)
10
+
11
+ const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
12
+
13
+ useEffect(() => {
14
+ return () => {
15
+ if (timerRef.current) clearTimeout(timerRef.current)
16
+ }
17
+ }, [])
18
+
19
+ const showToast = useCallback(
20
+ (message: string, type: 'error' | 'success') => {
21
+ if (timerRef.current) clearTimeout(timerRef.current)
22
+ setToast({ message, type })
23
+ timerRef.current = setTimeout(() => setToast(null), duration)
24
+ },
25
+ [duration]
26
+ )
27
+
28
+ return { showToast, toast }
29
+ }
package/src/index.ts CHANGED
@@ -1,13 +1,16 @@
1
1
  export { AnimatedCount, useAnimatedCount } from './ui/components/animated-count'
2
2
  export { AsciiSkeleton, Scramble as AsciiScramble } from './ui/components/ascii'
3
3
  export { Badge } from './ui/components/badge'
4
+ export { BottomSheet } from './ui/components/bottom-sheet'
4
5
  export { NousGirlBadge } from './ui/components/badges/nous-girl'
5
6
  export { BlendMode, useBlendMode, withBlendMode } from './ui/components/blend-mode'
6
7
  export type { BlendModeProps } from './ui/components/blend-mode'
7
8
  export { Blink } from './ui/components/blink'
8
9
  export { Button } from './ui/components/button'
10
+ export { Card, CardContent, CardDescription, CardHeader, CardTitle } from './ui/components/card'
9
11
  export { Checkbox } from './ui/components/checkbox'
10
12
  export { CommandBlock, CopyButton } from './ui/components/command-block'
13
+ export { ConfirmDialog } from './ui/components/confirm-dialog'
11
14
  export { Cursor } from './ui/components/cursor'
12
15
  export { DropdownMenu } from './ui/components/dropdown-menu'
13
16
  export { FitText } from './ui/components/fit-text'
@@ -19,9 +22,22 @@ export { DiscordIcon } from './ui/components/icons/discord'
19
22
  export { GitHubIcon } from './ui/components/icons/github'
20
23
  export { ImageDistortion } from './ui/components/image-distortion'
21
24
  export type { AutoPlayPattern } from './ui/components/image-distortion'
25
+ export { Input } from './ui/components/input'
26
+ export { Label } from './ui/components/label'
22
27
  export { LevaClient } from './ui/components/leva-client'
23
28
  export { ListItem } from './ui/components/list-item'
24
- export { Modal } from './ui/components/modal'
29
+ export {
30
+ Dialog,
31
+ DialogClose,
32
+ DialogContent,
33
+ DialogDescription,
34
+ DialogFooter,
35
+ DialogHeader,
36
+ DialogOverlay,
37
+ DialogPortal,
38
+ DialogTitle,
39
+ DialogTrigger
40
+ } from './ui/components/dialog'
25
41
  export { FilterGroup, Segmented } from './ui/components/segmented'
26
42
  export { Switch } from './ui/components/switch'
27
43
  export { Tabs, TabsList, TabsTrigger } from './ui/components/tabs'
@@ -56,12 +72,14 @@ export { SceneCanvas } from './ui/components/scene-canvas'
56
72
  export { Scramble } from './ui/components/scramble'
57
73
  export { Select, SelectOption } from './ui/components/select'
58
74
  export { SelectionSwitcher } from './ui/components/selection-switcher'
75
+ export { Separator } from './ui/components/separator'
59
76
  export { Spinner } from './ui/components/spinner'
60
77
  export { Stats } from './ui/components/stats'
61
78
  export { TerminalDemo } from './ui/components/terminal-demo'
62
79
  export type { TerminalDemoStep } from './ui/components/terminal-demo'
63
80
  export { ThemeToggle } from './ui/components/theme-toggle'
64
81
  export { TierCard } from './ui/components/tier-card'
82
+ export { Toast } from './ui/components/toast'
65
83
  export type { TierCardPrice, TierCardProps } from './ui/components/tier-card'
66
84
  export { TV } from './ui/components/tv'
67
85
  export { Watchlist } from './ui/components/watchlist'
@@ -99,7 +117,9 @@ export { polyRef } from './utils'
99
117
  export type { PolyComponent, PolyProps, PolyRef } from './utils'
100
118
  export { hexToRgb, rgbToHex, colorDodge, colorMix } from './utils/color'
101
119
 
120
+ export { useBelowBreakpoint } from './hooks/use-below-breakpoint'
102
121
  export { useCappedFrame } from './hooks/use-capped-frame'
122
+ export { useConfirmDelete } from './hooks/use-confirm-delete'
103
123
  export { useCssVarDims } from './hooks/use-css-var-dims'
104
124
  export { $gpuTier, useGpuTier } from './hooks/use-gpu-tier'
105
125
  export {
@@ -107,3 +127,4 @@ export {
107
127
  getControlAtom,
108
128
  setControlValue
109
129
  } from './hooks/use-smooth-controls'
130
+ export { useToast } from './hooks/use-toast'
@@ -12,7 +12,7 @@ import { Small } from './typography/small'
12
12
  const meta = {
13
13
  args: { damping: 1, duration: 1600, value: 84210 },
14
14
  component: AnimatedCount,
15
- title: 'Components/AnimatedCount'
15
+ title: 'Components/Feedback/AnimatedCount'
16
16
  } satisfies Meta<typeof AnimatedCount>
17
17
 
18
18
  export default meta
@@ -4,7 +4,7 @@ import { AsciiSkeleton, Scramble } from './ascii'
4
4
  import { Typography } from './typography'
5
5
 
6
6
  const meta = {
7
- title: 'Components/Ascii'
7
+ title: 'Components/Effects/Ascii'
8
8
  } satisfies Meta
9
9
 
10
10
  export default meta
@@ -6,7 +6,7 @@ import { NousGirlBadge } from './badges/nous-girl'
6
6
  const meta = {
7
7
  args: { children: 'LIVE' },
8
8
  component: Badge,
9
- title: 'Components/Badge'
9
+ title: 'Components/Feedback/Badge'
10
10
  } satisfies Meta<typeof Badge>
11
11
 
12
12
  export default meta
@@ -4,7 +4,7 @@ import { BlendMode } from './blend-mode'
4
4
 
5
5
  const meta: Meta<typeof BlendMode> = {
6
6
  component: BlendMode,
7
- title: 'Components/BlendMode'
7
+ title: 'Components/Effects/BlendMode'
8
8
  }
9
9
 
10
10
  export default meta
@@ -6,7 +6,7 @@ import { Small } from './typography/small'
6
6
 
7
7
  const meta = {
8
8
  component: Blink,
9
- title: 'Components/Blink'
9
+ title: 'Components/Effects/Blink'
10
10
  } satisfies Meta<typeof Blink>
11
11
 
12
12
  export default meta
@@ -0,0 +1,43 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { useState } from 'react'
3
+
4
+ import { BottomSheet } from './bottom-sheet'
5
+ import { Button } from './button'
6
+ import { ListItem } from './list-item'
7
+
8
+ const meta: Meta<typeof BottomSheet> = {
9
+ component: BottomSheet,
10
+ title: 'Components/Overlays/BottomSheet'
11
+ }
12
+
13
+ export default meta
14
+
15
+ type Story = StoryObj<typeof BottomSheet>
16
+
17
+ export const Default: Story = {
18
+ render: () => {
19
+ function Demo() {
20
+ const [open, setOpen] = useState(false)
21
+
22
+ return (
23
+ <>
24
+ <Button onClick={() => setOpen(true)}>Open sheet</Button>
25
+
26
+ <BottomSheet
27
+ onClose={() => setOpen(false)}
28
+ open={open}
29
+ title="Pick an option"
30
+ >
31
+ {['Alpha', 'Beta', 'Gamma', 'Delta'].map(item => (
32
+ <ListItem key={item} onClick={() => setOpen(false)}>
33
+ {item}
34
+ </ListItem>
35
+ ))}
36
+ </BottomSheet>
37
+ </>
38
+ )
39
+ }
40
+
41
+ return <Demo />
42
+ }
43
+ }
@@ -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
+ }
@@ -13,7 +13,7 @@ const meta: Meta<typeof Button> = {
13
13
  },
14
14
  args: { children: 'Normal', disabled: false, invert: false, outlined: false },
15
15
  component: Button,
16
- title: 'Components/Button'
16
+ title: 'Components/Forms/Button'
17
17
  }
18
18
 
19
19
  export default meta
@@ -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
+ }
@@ -25,7 +25,7 @@ function Demo({ disabled }: { disabled?: boolean }) {
25
25
 
26
26
  const meta: Meta<typeof Checkbox> = {
27
27
  component: Checkbox,
28
- title: 'Components/Checkbox'
28
+ title: 'Components/Forms/Checkbox'
29
29
  }
30
30
 
31
31
  export default meta
@@ -1,7 +1,7 @@
1
1
  'use client'
2
2
 
3
- import * as CheckboxPrimitive from '@radix-ui/react-checkbox'
4
3
  import { forwardRef, type ComponentPropsWithoutRef, type ElementRef } from 'react'
4
+ import { Checkbox as CheckboxPrimitive } from 'radix-ui'
5
5
 
6
6
  import { cn } from '../../utils'
7
7
 
@@ -11,7 +11,7 @@ const meta: Meta<typeof CommandBlock> = {
11
11
  label: '1. Install'
12
12
  },
13
13
  component: CommandBlock,
14
- title: 'Components/CommandBlock'
14
+ title: 'Components/Data Display/CommandBlock'
15
15
  }
16
16
 
17
17
  export default meta