@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.
- package/CHANGELOG.md +39 -0
- package/dist/hooks/use-below-breakpoint.d.ts +2 -0
- package/dist/hooks/use-below-breakpoint.js +17 -0
- package/dist/hooks/use-confirm-delete.d.ts +10 -0
- package/dist/hooks/use-confirm-delete.js +35 -0
- package/dist/hooks/use-toast.d.ts +7 -0
- package/dist/hooks/use-toast.js +21 -0
- package/dist/index.d.ts +11 -1
- package/dist/index.js +23 -2
- package/dist/ui/components/bottom-sheet.d.ts +15 -0
- package/dist/ui/components/bottom-sheet.js +192 -0
- package/dist/ui/components/card.d.ts +5 -0
- package/dist/ui/components/card.js +74 -0
- package/dist/ui/components/checkbox.d.ts +1 -1
- package/dist/ui/components/checkbox.js +2 -2
- package/dist/ui/components/confirm-dialog.d.ts +13 -0
- package/dist/ui/components/confirm-dialog.js +113 -0
- package/dist/ui/components/dialog.d.ts +15 -0
- package/dist/ui/components/dialog.js +171 -0
- package/dist/ui/components/input.d.ts +1 -0
- package/dist/ui/components/input.js +21 -0
- package/dist/ui/components/label.d.ts +1 -0
- package/dist/ui/components/label.js +18 -0
- package/dist/ui/components/separator.d.ts +5 -0
- package/dist/ui/components/separator.js +22 -0
- package/dist/ui/components/toast.d.ts +8 -0
- package/dist/ui/components/toast.js +39 -0
- package/dist/ui/globals.css +14 -2
- package/package.json +2 -2
- package/src/hooks/use-below-breakpoint.ts +21 -0
- package/src/hooks/use-confirm-delete.ts +43 -0
- package/src/hooks/use-toast.ts +29 -0
- package/src/index.ts +22 -1
- package/src/ui/components/animated-count.stories.tsx +1 -1
- package/src/ui/components/ascii.stories.tsx +1 -1
- package/src/ui/components/badge.stories.tsx +1 -1
- package/src/ui/components/blend-mode.stories.tsx +1 -1
- package/src/ui/components/blink.stories.tsx +1 -1
- package/src/ui/components/bottom-sheet.stories.tsx +43 -0
- package/src/ui/components/bottom-sheet.tsx +227 -0
- package/src/ui/components/button.stories.tsx +1 -1
- package/src/ui/components/card.stories.tsx +63 -0
- package/src/ui/components/card.tsx +85 -0
- package/src/ui/components/checkbox.stories.tsx +1 -1
- package/src/ui/components/checkbox.tsx +1 -1
- package/src/ui/components/command-block.stories.tsx +1 -1
- package/src/ui/components/confirm-dialog.stories.tsx +91 -0
- package/src/ui/components/confirm-dialog.tsx +130 -0
- package/src/ui/components/dialog.stories.tsx +169 -0
- package/src/ui/components/dialog.tsx +177 -0
- package/src/ui/components/dropdown-menu.stories.tsx +1 -1
- package/src/ui/components/fit-text/index.stories.tsx +1 -1
- package/src/ui/components/forms.stories.tsx +173 -0
- package/src/ui/components/graphs/index.stories.tsx +1 -1
- package/src/ui/components/hover-bg.stories.tsx +1 -1
- package/src/ui/components/image-distortion.stories.tsx +1 -1
- package/src/ui/components/input.stories.tsx +39 -0
- package/src/ui/components/input.tsx +20 -0
- package/src/ui/components/label.stories.tsx +26 -0
- package/src/ui/components/label.tsx +16 -0
- package/src/ui/components/list-item.stories.tsx +1 -1
- package/src/ui/components/poster.stories.tsx +1 -1
- package/src/ui/components/progress.stories.tsx +1 -1
- package/src/ui/components/scramble.stories.tsx +1 -1
- package/src/ui/components/segmented.stories.tsx +1 -1
- package/src/ui/components/select.stories.tsx +1 -1
- package/src/ui/components/separator.stories.tsx +33 -0
- package/src/ui/components/separator.tsx +24 -0
- package/src/ui/components/spinner.stories.tsx +1 -1
- package/src/ui/components/stats.stories.tsx +1 -1
- package/src/ui/components/switch.stories.tsx +1 -1
- package/src/ui/components/tabs.stories.tsx +1 -1
- package/src/ui/components/terminal-demo.stories.tsx +1 -1
- package/src/ui/components/theme-toggle.stories.tsx +1 -1
- package/src/ui/components/tier-card.stories.tsx +1 -1
- package/src/ui/components/toast.stories.tsx +55 -0
- package/src/ui/components/toast.tsx +49 -0
- package/src/ui/components/tv.stories.tsx +1 -1
- package/src/ui/components/watchlist.stories.tsx +1 -1
- package/src/ui/globals.css +14 -2
- package/dist/ui/components/modal/index.d.ts +0 -8
- package/dist/ui/components/modal/index.js +0 -35
- package/dist/ui/components/modal/modal.css +0 -36
- package/src/ui/components/modal/index.stories.tsx +0 -46
- package/src/ui/components/modal/index.tsx +0 -48
- 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 {
|
|
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
|
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|