@srcroot/ui 0.0.1 → 0.0.3

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.
@@ -0,0 +1,221 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { cn } from "@/lib/utils"
5
+
6
+ // Context Menu Context
7
+ interface ContextMenuContextValue {
8
+ open: boolean
9
+ position: { x: number; y: number }
10
+ onOpenChange: (open: boolean) => void
11
+ }
12
+
13
+ const ContextMenuContext = React.createContext<ContextMenuContextValue | null>(null)
14
+
15
+ function useContextMenu() {
16
+ const context = React.useContext(ContextMenuContext)
17
+ if (!context) {
18
+ throw new Error("useContextMenu must be used within a ContextMenu")
19
+ }
20
+ return context
21
+ }
22
+
23
+ // ContextMenu Root
24
+ interface ContextMenuProps {
25
+ children: React.ReactNode
26
+ }
27
+
28
+ const ContextMenu = ({ children }: ContextMenuProps) => {
29
+ const [open, setOpen] = React.useState(false)
30
+ const [position, setPosition] = React.useState({ x: 0, y: 0 })
31
+
32
+ const handleContextMenu = React.useCallback((e: React.MouseEvent) => {
33
+ e.preventDefault()
34
+ setPosition({ x: e.clientX, y: e.clientY })
35
+ setOpen(true)
36
+ }, [])
37
+
38
+ // Close on outside click
39
+ React.useEffect(() => {
40
+ if (!open) return
41
+
42
+ const handleClick = () => setOpen(false)
43
+ const handleEscape = (e: KeyboardEvent) => {
44
+ if (e.key === "Escape") setOpen(false)
45
+ }
46
+
47
+ document.addEventListener("click", handleClick)
48
+ document.addEventListener("keydown", handleEscape)
49
+ return () => {
50
+ document.removeEventListener("click", handleClick)
51
+ document.removeEventListener("keydown", handleEscape)
52
+ }
53
+ }, [open])
54
+
55
+ return (
56
+ <ContextMenuContext.Provider value={{ open, position, onOpenChange: setOpen }}>
57
+ <div onContextMenu={handleContextMenu}>
58
+ {children}
59
+ </div>
60
+ </ContextMenuContext.Provider>
61
+ )
62
+ }
63
+
64
+ // ContextMenu Trigger (the area that can be right-clicked)
65
+ interface ContextMenuTriggerProps {
66
+ children: React.ReactNode
67
+ className?: string
68
+ }
69
+
70
+ const ContextMenuTrigger = React.forwardRef<HTMLDivElement, ContextMenuTriggerProps>(
71
+ ({ children, className, ...props }, ref) => {
72
+ return (
73
+ <div ref={ref} className={className} {...props}>
74
+ {children}
75
+ </div>
76
+ )
77
+ }
78
+ )
79
+ ContextMenuTrigger.displayName = "ContextMenuTrigger"
80
+
81
+ // ContextMenu Content
82
+ interface ContextMenuContentProps {
83
+ children: React.ReactNode
84
+ className?: string
85
+ }
86
+
87
+ const ContextMenuContent = React.forwardRef<HTMLDivElement, ContextMenuContentProps>(
88
+ ({ children, className, ...props }, ref) => {
89
+ const { open, position } = useContextMenu()
90
+ const contentRef = React.useRef<HTMLDivElement>(null)
91
+ const [adjustedPosition, setAdjustedPosition] = React.useState(position)
92
+
93
+ // Adjust position to keep menu in viewport
94
+ React.useEffect(() => {
95
+ if (!open || !contentRef.current) return
96
+
97
+ const rect = contentRef.current.getBoundingClientRect()
98
+ const viewportWidth = window.innerWidth
99
+ const viewportHeight = window.innerHeight
100
+
101
+ let x = position.x
102
+ let y = position.y
103
+
104
+ // Adjust if overflowing right
105
+ if (x + rect.width > viewportWidth) {
106
+ x = viewportWidth - rect.width - 8
107
+ }
108
+ // Adjust if overflowing bottom
109
+ if (y + rect.height > viewportHeight) {
110
+ y = viewportHeight - rect.height - 8
111
+ }
112
+
113
+ setAdjustedPosition({ x: Math.max(8, x), y: Math.max(8, y) })
114
+ }, [open, position])
115
+
116
+ if (!open) return null
117
+
118
+ return (
119
+ <div
120
+ ref={contentRef}
121
+ className={cn(
122
+ "fixed z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
123
+ "animate-in fade-in-0 zoom-in-95",
124
+ className
125
+ )}
126
+ style={{
127
+ left: adjustedPosition.x,
128
+ top: adjustedPosition.y,
129
+ }}
130
+ onClick={(e) => e.stopPropagation()}
131
+ {...props}
132
+ >
133
+ {children}
134
+ </div>
135
+ )
136
+ }
137
+ )
138
+ ContextMenuContent.displayName = "ContextMenuContent"
139
+
140
+ // ContextMenu Item
141
+ interface ContextMenuItemProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
142
+ inset?: boolean
143
+ }
144
+
145
+ const ContextMenuItem = React.forwardRef<HTMLButtonElement, ContextMenuItemProps>(
146
+ ({ className, inset, children, ...props }, ref) => {
147
+ const { onOpenChange } = useContextMenu()
148
+
149
+ return (
150
+ <button
151
+ ref={ref}
152
+ className={cn(
153
+ "relative flex w-full cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors",
154
+ "focus:bg-accent focus:text-accent-foreground hover:bg-accent hover:text-accent-foreground",
155
+ "disabled:pointer-events-none disabled:opacity-50",
156
+ inset && "pl-8",
157
+ className
158
+ )}
159
+ onClick={() => onOpenChange(false)}
160
+ {...props}
161
+ >
162
+ {children}
163
+ </button>
164
+ )
165
+ }
166
+ )
167
+ ContextMenuItem.displayName = "ContextMenuItem"
168
+
169
+ // ContextMenu Separator
170
+ const ContextMenuSeparator = React.forwardRef<
171
+ HTMLDivElement,
172
+ React.HTMLAttributes<HTMLDivElement>
173
+ >(({ className, ...props }, ref) => (
174
+ <div
175
+ ref={ref}
176
+ className={cn("-mx-1 my-1 h-px bg-muted", className)}
177
+ {...props}
178
+ />
179
+ ))
180
+ ContextMenuSeparator.displayName = "ContextMenuSeparator"
181
+
182
+ // ContextMenu Label
183
+ const ContextMenuLabel = React.forwardRef<
184
+ HTMLDivElement,
185
+ React.HTMLAttributes<HTMLDivElement> & { inset?: boolean }
186
+ >(({ className, inset, ...props }, ref) => (
187
+ <div
188
+ ref={ref}
189
+ className={cn(
190
+ "px-2 py-1.5 text-sm font-semibold text-foreground",
191
+ inset && "pl-8",
192
+ className
193
+ )}
194
+ {...props}
195
+ />
196
+ ))
197
+ ContextMenuLabel.displayName = "ContextMenuLabel"
198
+
199
+ // ContextMenu Shortcut
200
+ const ContextMenuShortcut = ({
201
+ className,
202
+ ...props
203
+ }: React.HTMLAttributes<HTMLSpanElement>) => {
204
+ return (
205
+ <span
206
+ className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)}
207
+ {...props}
208
+ />
209
+ )
210
+ }
211
+ ContextMenuShortcut.displayName = "ContextMenuShortcut"
212
+
213
+ export {
214
+ ContextMenu,
215
+ ContextMenuTrigger,
216
+ ContextMenuContent,
217
+ ContextMenuItem,
218
+ ContextMenuSeparator,
219
+ ContextMenuLabel,
220
+ ContextMenuShortcut,
221
+ }
@@ -0,0 +1,179 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { Calendar } from "./calendar"
5
+ import { Popover, PopoverContent, PopoverTrigger } from "./popover"
6
+ import { Button } from "./button"
7
+ import { cn } from "@/lib/utils"
8
+
9
+ // Calendar icon
10
+ const CalendarIcon = () => (
11
+ <svg
12
+ xmlns="http://www.w3.org/2000/svg"
13
+ width="16"
14
+ height="16"
15
+ viewBox="0 0 24 24"
16
+ fill="none"
17
+ stroke="currentColor"
18
+ strokeWidth="2"
19
+ strokeLinecap="round"
20
+ strokeLinejoin="round"
21
+ className="mr-2 h-4 w-4 opacity-50"
22
+ >
23
+ <rect width="18" height="18" x="3" y="4" rx="2" ry="2" />
24
+ <line x1="16" x2="16" y1="2" y2="6" />
25
+ <line x1="8" x2="8" y1="2" y2="6" />
26
+ <line x1="3" x2="21" y1="10" y2="10" />
27
+ </svg>
28
+ )
29
+
30
+ // Format helpers
31
+ const formatDate = (date: Date) => {
32
+ return date.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" })
33
+ }
34
+
35
+ const formatRange = (dates: Date[]) => {
36
+ if (dates.length === 0) return null
37
+ if (dates.length === 1) return formatDate(dates[0])
38
+ return `${formatDate(dates[0])} → ${formatDate(dates[1])}`
39
+ }
40
+
41
+ const formatMultiple = (dates: Date[]) => {
42
+ if (dates.length === 0) return null
43
+ return `${dates.length} date${dates.length > 1 ? 's' : ''} selected`
44
+ }
45
+
46
+ // DatePicker Props
47
+ interface DatePickerBaseProps {
48
+ /** Placeholder text when no date selected */
49
+ placeholder?: string
50
+ /** Whether the picker is disabled */
51
+ disabled?: boolean
52
+ /** Custom class name for the trigger button */
53
+ className?: string
54
+ /** Number of months to display */
55
+ numberOfMonths?: 1 | 2
56
+ /** Calendar size */
57
+ size?: "xs" | "sm" | "default" | "md" | "lg"
58
+ }
59
+
60
+ interface DatePickerSingleProps extends DatePickerBaseProps {
61
+ mode?: "single"
62
+ selected?: Date
63
+ onSelect?: (date: Date | undefined) => void
64
+ }
65
+
66
+ interface DatePickerMultipleProps extends DatePickerBaseProps {
67
+ mode: "multiple"
68
+ selected?: Date[]
69
+ onSelect?: (dates: Date[]) => void
70
+ }
71
+
72
+ interface DatePickerRangeProps extends DatePickerBaseProps {
73
+ mode: "range"
74
+ selected?: Date[]
75
+ onSelect?: (dates: Date[]) => void
76
+ }
77
+
78
+ type DatePickerProps = DatePickerSingleProps | DatePickerMultipleProps | DatePickerRangeProps
79
+
80
+ /**
81
+ * DatePicker - A complete date picker component
82
+ *
83
+ * Combines Calendar + Popover for a ready-to-use date selection experience.
84
+ * Supports single, multiple, and range selection modes.
85
+ *
86
+ * @example
87
+ * // Single date
88
+ * <DatePicker selected={date} onSelect={setDate} />
89
+ *
90
+ * // Multiple dates
91
+ * <DatePicker mode="multiple" selected={dates} onSelect={setDates} />
92
+ *
93
+ * // Date range with dual months
94
+ * <DatePicker mode="range" numberOfMonths={2} selected={range} onSelect={setRange} />
95
+ */
96
+ const DatePicker = React.forwardRef<HTMLButtonElement, DatePickerProps>(
97
+ ({
98
+ mode = "single",
99
+ selected,
100
+ onSelect,
101
+ placeholder,
102
+ disabled = false,
103
+ className,
104
+ numberOfMonths = 1,
105
+ size = "default",
106
+ ...props
107
+ }, ref) => {
108
+ const [open, setOpen] = React.useState(false)
109
+
110
+ // Determine display text
111
+ const getDisplayText = () => {
112
+ if (mode === "single") {
113
+ return selected ? formatDate(selected as Date) : null
114
+ } else if (mode === "multiple") {
115
+ const dates = (selected as Date[]) || []
116
+ return dates.length > 0 ? formatMultiple(dates) : null
117
+ } else {
118
+ const dates = (selected as Date[]) || []
119
+ return dates.length > 0 ? formatRange(dates) : null
120
+ }
121
+ }
122
+
123
+ const displayText = getDisplayText()
124
+ const defaultPlaceholder = mode === "single" ? "Pick a date"
125
+ : mode === "multiple" ? "Pick dates"
126
+ : "Pick a date range"
127
+
128
+ // Handle selection
129
+ const handleSelect = (value: any) => {
130
+ if (mode === "single") {
131
+ (onSelect as ((date: Date | undefined) => void))?.(value)
132
+ setOpen(false) // Close on single selection
133
+ } else if (mode === "multiple") {
134
+ (onSelect as ((dates: Date[]) => void))?.(value || [])
135
+ } else {
136
+ const dates = value || []
137
+ ; (onSelect as ((dates: Date[]) => void))?.(dates)
138
+ // Close when range is complete (2 dates)
139
+ if (dates.length === 2) {
140
+ setOpen(false)
141
+ }
142
+ }
143
+ }
144
+
145
+ return (
146
+ <Popover open={open} onOpenChange={setOpen}>
147
+ <PopoverTrigger asChild>
148
+ <Button
149
+ ref={ref}
150
+ variant="outline"
151
+ disabled={disabled}
152
+ className={cn(
153
+ "w-[280px] justify-start text-left font-normal",
154
+ !displayText && "text-muted-foreground",
155
+ numberOfMonths === 2 && "w-[320px]",
156
+ className
157
+ )}
158
+ >
159
+ <CalendarIcon />
160
+ {displayText || <span>{placeholder || defaultPlaceholder}</span>}
161
+ </Button>
162
+ </PopoverTrigger>
163
+ <PopoverContent className="w-auto p-0">
164
+ <Calendar
165
+ mode={mode}
166
+ numberOfMonths={numberOfMonths}
167
+ size={size}
168
+ selected={selected}
169
+ onSelect={handleSelect}
170
+ className="rounded-md border-0 shadow-none"
171
+ />
172
+ </PopoverContent>
173
+ </Popover>
174
+ )
175
+ }
176
+ )
177
+ DatePicker.displayName = "DatePicker"
178
+
179
+ export { DatePicker, type DatePickerProps }
@@ -0,0 +1,241 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { cn } from "@/lib/utils"
5
+
6
+ // Drawer Context
7
+ interface DrawerContextValue {
8
+ open: boolean
9
+ onOpenChange: (open: boolean) => void
10
+ }
11
+
12
+ const DrawerContext = React.createContext<DrawerContextValue | null>(null)
13
+
14
+ function useDrawer() {
15
+ const context = React.useContext(DrawerContext)
16
+ if (!context) {
17
+ throw new Error("useDrawer must be used within a Drawer")
18
+ }
19
+ return context
20
+ }
21
+
22
+ // Drawer Root
23
+ interface DrawerProps {
24
+ children: React.ReactNode
25
+ open?: boolean
26
+ onOpenChange?: (open: boolean) => void
27
+ defaultOpen?: boolean
28
+ }
29
+
30
+ const Drawer = ({ children, open: controlledOpen, onOpenChange, defaultOpen = false }: DrawerProps) => {
31
+ const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen)
32
+ const open = controlledOpen ?? uncontrolledOpen
33
+ const setOpen = onOpenChange ?? setUncontrolledOpen
34
+
35
+ return (
36
+ <DrawerContext.Provider value={{ open, onOpenChange: setOpen }}>
37
+ {children}
38
+ </DrawerContext.Provider>
39
+ )
40
+ }
41
+
42
+ // Drawer Trigger
43
+ interface DrawerTriggerProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
44
+ asChild?: boolean
45
+ }
46
+
47
+ const DrawerTrigger = React.forwardRef<HTMLButtonElement, DrawerTriggerProps>(
48
+ ({ children, asChild, onClick, ...props }, ref) => {
49
+ const { onOpenChange } = useDrawer()
50
+
51
+ const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
52
+ onClick?.(e)
53
+ onOpenChange(true)
54
+ }
55
+
56
+ if (asChild && React.isValidElement(children)) {
57
+ return React.cloneElement(children as React.ReactElement<any>, {
58
+ onClick: handleClick,
59
+ ref,
60
+ })
61
+ }
62
+
63
+ return (
64
+ <button ref={ref} onClick={handleClick} {...props}>
65
+ {children}
66
+ </button>
67
+ )
68
+ }
69
+ )
70
+ DrawerTrigger.displayName = "DrawerTrigger"
71
+
72
+ // Drawer Close
73
+ const DrawerClose = React.forwardRef<HTMLButtonElement, React.ButtonHTMLAttributes<HTMLButtonElement>>(
74
+ ({ children, onClick, ...props }, ref) => {
75
+ const { onOpenChange } = useDrawer()
76
+
77
+ const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
78
+ onClick?.(e)
79
+ onOpenChange(false)
80
+ }
81
+
82
+ return (
83
+ <button ref={ref} onClick={handleClick} {...props}>
84
+ {children}
85
+ </button>
86
+ )
87
+ }
88
+ )
89
+ DrawerClose.displayName = "DrawerClose"
90
+
91
+ // Drawer Overlay (internal)
92
+ interface DrawerOverlayProps extends React.HTMLAttributes<HTMLDivElement> {
93
+ isAnimating: boolean
94
+ }
95
+
96
+ const DrawerOverlay = React.forwardRef<HTMLDivElement, DrawerOverlayProps>(
97
+ ({ className, isAnimating, ...props }, ref) => {
98
+ const { onOpenChange } = useDrawer()
99
+
100
+ return (
101
+ <div
102
+ ref={ref}
103
+ className={cn(
104
+ "fixed inset-0 z-50 bg-black/80 transition-opacity duration-300",
105
+ isAnimating ? "opacity-100" : "opacity-0",
106
+ className
107
+ )}
108
+ onClick={() => onOpenChange(false)}
109
+ {...props}
110
+ />
111
+ )
112
+ }
113
+ )
114
+ DrawerOverlay.displayName = "DrawerOverlay"
115
+
116
+ // Drawer Content
117
+ interface DrawerContentProps extends React.HTMLAttributes<HTMLDivElement> {
118
+ side?: "bottom" | "top"
119
+ }
120
+
121
+ const DrawerContent = React.forwardRef<HTMLDivElement, DrawerContentProps>(
122
+ ({ className, children, side = "bottom", ...props }, ref) => {
123
+ const { open, onOpenChange } = useDrawer()
124
+ const [isVisible, setIsVisible] = React.useState(false)
125
+ const [isAnimating, setIsAnimating] = React.useState(false)
126
+
127
+ React.useEffect(() => {
128
+ if (open) {
129
+ // First make visible (off-screen)
130
+ setIsVisible(true)
131
+ // Use a small timeout to ensure the browser has painted the initial state
132
+ const timer = setTimeout(() => {
133
+ setIsAnimating(true)
134
+ }, 10)
135
+ return () => clearTimeout(timer)
136
+ } else {
137
+ // Start close animation
138
+ setIsAnimating(false)
139
+ // Wait for animation to complete before hiding
140
+ const timer = setTimeout(() => {
141
+ setIsVisible(false)
142
+ }, 300)
143
+ return () => clearTimeout(timer)
144
+ }
145
+ }, [open])
146
+
147
+ // Close on Escape
148
+ React.useEffect(() => {
149
+ if (!open) return
150
+ const handleEscape = (e: KeyboardEvent) => {
151
+ if (e.key === "Escape") onOpenChange(false)
152
+ }
153
+ document.addEventListener("keydown", handleEscape)
154
+ return () => document.removeEventListener("keydown", handleEscape)
155
+ }, [open, onOpenChange])
156
+
157
+ // Prevent body scroll when open
158
+ React.useEffect(() => {
159
+ if (open) {
160
+ document.body.style.overflow = "hidden"
161
+ } else {
162
+ document.body.style.overflow = ""
163
+ }
164
+ return () => {
165
+ document.body.style.overflow = ""
166
+ }
167
+ }, [open])
168
+
169
+ if (!isVisible) return null
170
+
171
+ return (
172
+ <>
173
+ <DrawerOverlay isAnimating={isAnimating} />
174
+ <div
175
+ ref={ref}
176
+ className={cn(
177
+ "fixed z-50 flex flex-col bg-background shadow-lg",
178
+ "transition-transform duration-300 ease-out",
179
+ side === "bottom" && "inset-x-0 bottom-0 rounded-t-xl border-t",
180
+ side === "top" && "inset-x-0 top-0 rounded-b-xl border-b",
181
+ // Animation states
182
+ side === "bottom" && (isAnimating ? "translate-y-0" : "translate-y-full"),
183
+ side === "top" && (isAnimating ? "translate-y-0" : "-translate-y-full"),
184
+ className
185
+ )}
186
+ {...props}
187
+ >
188
+ {/* Handle indicator */}
189
+ {side === "bottom" && (
190
+ <div className="mx-auto mt-4 h-1.5 w-12 rounded-full bg-muted" />
191
+ )}
192
+ {children}
193
+ {side === "top" && (
194
+ <div className="mx-auto mb-4 h-1.5 w-12 rounded-full bg-muted" />
195
+ )}
196
+ </div>
197
+ </>
198
+ )
199
+ }
200
+ )
201
+ DrawerContent.displayName = "DrawerContent"
202
+
203
+ // Drawer Header
204
+ const DrawerHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
205
+ <div className={cn("flex flex-col space-y-1.5 p-4 text-center sm:text-left", className)} {...props} />
206
+ )
207
+ DrawerHeader.displayName = "DrawerHeader"
208
+
209
+ // Drawer Footer
210
+ const DrawerFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
211
+ <div className={cn("flex flex-col-reverse gap-2 p-4 sm:flex-row sm:justify-end", className)} {...props} />
212
+ )
213
+ DrawerFooter.displayName = "DrawerFooter"
214
+
215
+ // Drawer Title
216
+ const DrawerTitle = React.forwardRef<HTMLHeadingElement, React.HTMLAttributes<HTMLHeadingElement>>(
217
+ ({ className, ...props }, ref) => (
218
+ <h2 ref={ref} className={cn("text-lg font-semibold leading-none tracking-tight", className)} {...props} />
219
+ )
220
+ )
221
+ DrawerTitle.displayName = "DrawerTitle"
222
+
223
+ // Drawer Description
224
+ const DrawerDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
225
+ ({ className, ...props }, ref) => (
226
+ <p ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
227
+ )
228
+ )
229
+ DrawerDescription.displayName = "DrawerDescription"
230
+
231
+ export {
232
+ Drawer,
233
+ DrawerTrigger,
234
+ DrawerClose,
235
+ DrawerOverlay,
236
+ DrawerContent,
237
+ DrawerHeader,
238
+ DrawerFooter,
239
+ DrawerTitle,
240
+ DrawerDescription,
241
+ }