@srcroot/ui 0.0.1

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 (44) hide show
  1. package/README.md +151 -0
  2. package/dist/index.d.ts +2 -0
  3. package/dist/index.js +640 -0
  4. package/package.json +43 -0
  5. package/registry/accordion.tsx +158 -0
  6. package/registry/alert-dialog.tsx +206 -0
  7. package/registry/alert.tsx +73 -0
  8. package/registry/aspect-ratio.tsx +44 -0
  9. package/registry/avatar.tsx +94 -0
  10. package/registry/badge.tsx +68 -0
  11. package/registry/breadcrumb.tsx +151 -0
  12. package/registry/button-group.tsx +84 -0
  13. package/registry/button.tsx +102 -0
  14. package/registry/calendar.tsx +238 -0
  15. package/registry/card.tsx +114 -0
  16. package/registry/carousel.tsx +169 -0
  17. package/registry/checkbox.tsx +79 -0
  18. package/registry/collapsible.tsx +110 -0
  19. package/registry/container.tsx +60 -0
  20. package/registry/dialog.tsx +264 -0
  21. package/registry/dropdown-menu.tsx +387 -0
  22. package/registry/image.tsx +144 -0
  23. package/registry/input.tsx +44 -0
  24. package/registry/label.tsx +34 -0
  25. package/registry/loading-spinner.tsx +108 -0
  26. package/registry/otp-input.tsx +152 -0
  27. package/registry/pagination.tsx +146 -0
  28. package/registry/popover.tsx +135 -0
  29. package/registry/progress.tsx +49 -0
  30. package/registry/radio.tsx +99 -0
  31. package/registry/search.tsx +146 -0
  32. package/registry/select.tsx +190 -0
  33. package/registry/separator.tsx +44 -0
  34. package/registry/sheet.tsx +180 -0
  35. package/registry/skeleton.tsx +26 -0
  36. package/registry/slider.tsx +115 -0
  37. package/registry/star-rating.tsx +131 -0
  38. package/registry/switch.tsx +70 -0
  39. package/registry/table.tsx +136 -0
  40. package/registry/tabs.tsx +122 -0
  41. package/registry/text.tsx +70 -0
  42. package/registry/textarea.tsx +39 -0
  43. package/registry/toast.tsx +95 -0
  44. package/registry/tooltip.tsx +122 -0
@@ -0,0 +1,146 @@
1
+ import * as React from "react"
2
+ import { cn } from "@/lib/utils"
3
+
4
+ interface SearchProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "onChange"> {
5
+ /** Callback when search value changes */
6
+ onSearch?: (value: string) => void
7
+ /** Debounce delay in ms */
8
+ debounceMs?: number
9
+ /** Show clear button */
10
+ showClear?: boolean
11
+ /** Loading state */
12
+ loading?: boolean
13
+ }
14
+
15
+ /**
16
+ * Search input with optional debounce and clear button
17
+ *
18
+ * @example
19
+ * <Search
20
+ * placeholder="Search..."
21
+ * onSearch={(value) => fetchResults(value)}
22
+ * debounceMs={300}
23
+ * />
24
+ */
25
+ const Search = React.forwardRef<HTMLInputElement, SearchProps>(
26
+ (
27
+ {
28
+ className,
29
+ onSearch,
30
+ debounceMs = 0,
31
+ showClear = true,
32
+ loading,
33
+ defaultValue = "",
34
+ ...props
35
+ },
36
+ ref
37
+ ) => {
38
+ const [value, setValue] = React.useState(String(defaultValue))
39
+ const debounceRef = React.useRef<NodeJS.Timeout>()
40
+
41
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
42
+ const newValue = e.target.value
43
+ setValue(newValue)
44
+
45
+ if (debounceMs > 0) {
46
+ clearTimeout(debounceRef.current)
47
+ debounceRef.current = setTimeout(() => {
48
+ onSearch?.(newValue)
49
+ }, debounceMs)
50
+ } else {
51
+ onSearch?.(newValue)
52
+ }
53
+ }
54
+
55
+ const handleClear = () => {
56
+ setValue("")
57
+ onSearch?.("")
58
+ }
59
+
60
+ const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
61
+ if (e.key === "Escape") {
62
+ handleClear()
63
+ }
64
+ }
65
+
66
+ React.useEffect(() => {
67
+ return () => {
68
+ clearTimeout(debounceRef.current)
69
+ }
70
+ }, [])
71
+
72
+ return (
73
+ <div className={cn("relative", className)}>
74
+ {/* Search Icon */}
75
+ <svg
76
+ className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground"
77
+ fill="none"
78
+ viewBox="0 0 24 24"
79
+ stroke="currentColor"
80
+ strokeWidth={2}
81
+ >
82
+ <path
83
+ strokeLinecap="round"
84
+ strokeLinejoin="round"
85
+ d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
86
+ />
87
+ </svg>
88
+
89
+ <input
90
+ ref={ref}
91
+ type="search"
92
+ role="searchbox"
93
+ value={value}
94
+ onChange={handleChange}
95
+ onKeyDown={handleKeyDown}
96
+ className={cn(
97
+ "flex h-10 w-full rounded-md border border-input bg-transparent pl-10 pr-10 py-2 text-sm shadow-sm transition-colors",
98
+ "placeholder:text-muted-foreground",
99
+ "focus:outline-none focus:ring-1 focus:ring-ring",
100
+ "disabled:cursor-not-allowed disabled:opacity-50"
101
+ )}
102
+ {...props}
103
+ />
104
+
105
+ {/* Loading or Clear */}
106
+ <div className="absolute right-3 top-1/2 -translate-y-1/2">
107
+ {loading ? (
108
+ <svg
109
+ className="h-4 w-4 animate-spin text-muted-foreground"
110
+ fill="none"
111
+ viewBox="0 0 24 24"
112
+ >
113
+ <circle
114
+ className="opacity-25"
115
+ cx="12"
116
+ cy="12"
117
+ r="10"
118
+ stroke="currentColor"
119
+ strokeWidth="4"
120
+ />
121
+ <path
122
+ className="opacity-75"
123
+ fill="currentColor"
124
+ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
125
+ />
126
+ </svg>
127
+ ) : showClear && value ? (
128
+ <button
129
+ type="button"
130
+ onClick={handleClear}
131
+ className="text-muted-foreground hover:text-foreground"
132
+ aria-label="Clear search"
133
+ >
134
+ <svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
135
+ <path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
136
+ </svg>
137
+ </button>
138
+ ) : null}
139
+ </div>
140
+ </div>
141
+ )
142
+ }
143
+ )
144
+ Search.displayName = "Search"
145
+
146
+ export { Search }
@@ -0,0 +1,190 @@
1
+ import * as React from "react"
2
+ import { cn } from "@/lib/utils"
3
+
4
+ interface SelectContextValue {
5
+ value: string
6
+ onValueChange: (value: string) => void
7
+ open: boolean
8
+ setOpen: (open: boolean) => void
9
+ }
10
+
11
+ const SelectContext = React.createContext<SelectContextValue | null>(null)
12
+
13
+ interface SelectProps {
14
+ children: React.ReactNode
15
+ value?: string
16
+ onValueChange?: (value: string) => void
17
+ defaultValue?: string
18
+ }
19
+
20
+ /**
21
+ * Custom Select dropdown with keyboard navigation
22
+ *
23
+ * @example
24
+ * <Select value={value} onValueChange={setValue}>
25
+ * <SelectTrigger>
26
+ * <SelectValue placeholder="Select option" />
27
+ * </SelectTrigger>
28
+ * <SelectContent>
29
+ * <SelectItem value="option1">Option 1</SelectItem>
30
+ * <SelectItem value="option2">Option 2</SelectItem>
31
+ * </SelectContent>
32
+ * </Select>
33
+ */
34
+ function Select({ children, value: controlledValue, onValueChange, defaultValue = "" }: SelectProps) {
35
+ const [uncontrolledValue, setUncontrolledValue] = React.useState(defaultValue)
36
+ const [open, setOpen] = React.useState(false)
37
+
38
+ const value = controlledValue !== undefined ? controlledValue : uncontrolledValue
39
+ const setValue = onValueChange || setUncontrolledValue
40
+
41
+ return (
42
+ <SelectContext.Provider value={{ value, onValueChange: setValue, open, setOpen }}>
43
+ <div className="relative">
44
+ {children}
45
+ </div>
46
+ </SelectContext.Provider>
47
+ )
48
+ }
49
+
50
+ const SelectTrigger = React.forwardRef<
51
+ HTMLButtonElement,
52
+ React.ButtonHTMLAttributes<HTMLButtonElement>
53
+ >(({ className, children, ...props }, ref) => {
54
+ const context = React.useContext(SelectContext)
55
+ if (!context) throw new Error("SelectTrigger must be used within Select")
56
+
57
+ return (
58
+ <button
59
+ ref={ref}
60
+ type="button"
61
+ role="combobox"
62
+ aria-expanded={context.open}
63
+ aria-haspopup="listbox"
64
+ className={cn(
65
+ "flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
66
+ className
67
+ )}
68
+ onClick={() => context.setOpen(!context.open)}
69
+ {...props}
70
+ >
71
+ {children}
72
+ <svg
73
+ className={cn("h-4 w-4 opacity-50 transition-transform", context.open && "rotate-180")}
74
+ fill="none"
75
+ viewBox="0 0 24 24"
76
+ stroke="currentColor"
77
+ strokeWidth={2}
78
+ >
79
+ <path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
80
+ </svg>
81
+ </button>
82
+ )
83
+ })
84
+ SelectTrigger.displayName = "SelectTrigger"
85
+
86
+ interface SelectValueProps {
87
+ placeholder?: string
88
+ }
89
+
90
+ function SelectValue({ placeholder }: SelectValueProps) {
91
+ const context = React.useContext(SelectContext)
92
+ if (!context) throw new Error("SelectValue must be used within Select")
93
+
94
+ return (
95
+ <span className={cn(!context.value && "text-muted-foreground")}>
96
+ {context.value || placeholder}
97
+ </span>
98
+ )
99
+ }
100
+
101
+ const SelectContent = React.forwardRef<
102
+ HTMLDivElement,
103
+ React.HTMLAttributes<HTMLDivElement>
104
+ >(({ className, children, ...props }, ref) => {
105
+ const context = React.useContext(SelectContext)
106
+ if (!context) throw new Error("SelectContent must be used within Select")
107
+
108
+ React.useEffect(() => {
109
+ const handleClickOutside = (e: MouseEvent) => {
110
+ if (context.open) {
111
+ context.setOpen(false)
112
+ }
113
+ }
114
+
115
+ const handleEscape = (e: KeyboardEvent) => {
116
+ if (e.key === "Escape" && context.open) {
117
+ context.setOpen(false)
118
+ }
119
+ }
120
+
121
+ document.addEventListener("mousedown", handleClickOutside)
122
+ document.addEventListener("keydown", handleEscape)
123
+
124
+ return () => {
125
+ document.removeEventListener("mousedown", handleClickOutside)
126
+ document.removeEventListener("keydown", handleEscape)
127
+ }
128
+ }, [context.open, context])
129
+
130
+ if (!context.open) return null
131
+
132
+ return (
133
+ <div
134
+ ref={ref}
135
+ role="listbox"
136
+ className={cn(
137
+ "absolute z-50 mt-1 max-h-60 w-full overflow-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
138
+ className
139
+ )}
140
+ onClick={(e) => e.stopPropagation()}
141
+ {...props}
142
+ >
143
+ {children}
144
+ </div>
145
+ )
146
+ })
147
+ SelectContent.displayName = "SelectContent"
148
+
149
+ interface SelectItemProps extends React.HTMLAttributes<HTMLDivElement> {
150
+ value: string
151
+ }
152
+
153
+ const SelectItem = React.forwardRef<HTMLDivElement, SelectItemProps>(
154
+ ({ className, children, value, ...props }, ref) => {
155
+ const context = React.useContext(SelectContext)
156
+ if (!context) throw new Error("SelectItem must be used within Select")
157
+
158
+ const isSelected = context.value === value
159
+
160
+ return (
161
+ <div
162
+ ref={ref}
163
+ role="option"
164
+ aria-selected={isSelected}
165
+ className={cn(
166
+ "relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground",
167
+ isSelected && "bg-accent text-accent-foreground",
168
+ className
169
+ )}
170
+ onClick={() => {
171
+ context.onValueChange(value)
172
+ context.setOpen(false)
173
+ }}
174
+ {...props}
175
+ >
176
+ {children}
177
+ {isSelected && (
178
+ <span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
179
+ <svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
180
+ <path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
181
+ </svg>
182
+ </span>
183
+ )}
184
+ </div>
185
+ )
186
+ }
187
+ )
188
+ SelectItem.displayName = "SelectItem"
189
+
190
+ export { Select, SelectTrigger, SelectValue, SelectContent, SelectItem }
@@ -0,0 +1,44 @@
1
+ import * as React from "react"
2
+ import { cn } from "@/lib/utils"
3
+
4
+ interface SeparatorProps extends React.HTMLAttributes<HTMLDivElement> {
5
+ /**
6
+ * Orientation of the separator
7
+ * @default "horizontal"
8
+ */
9
+ orientation?: "horizontal" | "vertical"
10
+ /**
11
+ * Whether the separator is decorative (no semantic meaning)
12
+ * @default true
13
+ */
14
+ decorative?: boolean
15
+ }
16
+
17
+ /**
18
+ * Separator component for visual division
19
+ *
20
+ * @example
21
+ * // Horizontal separator
22
+ * <Separator />
23
+ *
24
+ * // Vertical separator
25
+ * <Separator orientation="vertical" className="h-4" />
26
+ */
27
+ const Separator = React.forwardRef<HTMLDivElement, SeparatorProps>(
28
+ ({ className, orientation = "horizontal", decorative = true, ...props }, ref) => (
29
+ <div
30
+ ref={ref}
31
+ role={decorative ? "none" : "separator"}
32
+ aria-orientation={decorative ? undefined : orientation}
33
+ className={cn(
34
+ "shrink-0 bg-border",
35
+ orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
36
+ className
37
+ )}
38
+ {...props}
39
+ />
40
+ )
41
+ )
42
+ Separator.displayName = "Separator"
43
+
44
+ export { Separator }
@@ -0,0 +1,180 @@
1
+ import * as React from "react"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+ import { cn } from "@/lib/utils"
4
+
5
+ interface SheetContextValue {
6
+ open: boolean
7
+ onOpenChange: (open: boolean) => void
8
+ }
9
+
10
+ const SheetContext = React.createContext<SheetContextValue | null>(null)
11
+
12
+ interface SheetProps {
13
+ children: React.ReactNode
14
+ open?: boolean
15
+ onOpenChange?: (open: boolean) => void
16
+ defaultOpen?: boolean
17
+ }
18
+
19
+ /**
20
+ * Sheet (slide-in panel) component
21
+ *
22
+ * @example
23
+ * <Sheet>
24
+ * <SheetTrigger asChild>
25
+ * <Button>Open Sheet</Button>
26
+ * </SheetTrigger>
27
+ * <SheetContent side="right">
28
+ * <SheetHeader>
29
+ * <SheetTitle>Title</SheetTitle>
30
+ * <SheetDescription>Description</SheetDescription>
31
+ * </SheetHeader>
32
+ * </SheetContent>
33
+ * </Sheet>
34
+ */
35
+ function Sheet({ children, open: controlledOpen, onOpenChange, defaultOpen = false }: SheetProps) {
36
+ const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen)
37
+
38
+ const open = controlledOpen !== undefined ? controlledOpen : uncontrolledOpen
39
+ const setOpen = onOpenChange || setUncontrolledOpen
40
+
41
+ return (
42
+ <SheetContext.Provider value={{ open, onOpenChange: setOpen }}>
43
+ {children}
44
+ </SheetContext.Provider>
45
+ )
46
+ }
47
+
48
+ interface SheetTriggerProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
49
+ asChild?: boolean
50
+ }
51
+
52
+ const SheetTrigger = React.forwardRef<HTMLButtonElement, SheetTriggerProps>(
53
+ ({ onClick, asChild, children, ...props }, ref) => {
54
+ const context = React.useContext(SheetContext)
55
+ if (!context) throw new Error("SheetTrigger must be used within Sheet")
56
+
57
+ const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
58
+ onClick?.(e)
59
+ context.onOpenChange(true)
60
+ }
61
+
62
+ if (asChild && React.isValidElement(children)) {
63
+ return React.cloneElement(children as React.ReactElement<any>, {
64
+ onClick: handleClick,
65
+ ref,
66
+ })
67
+ }
68
+
69
+ return (
70
+ <button ref={ref} onClick={handleClick} {...props}>
71
+ {children}
72
+ </button>
73
+ )
74
+ }
75
+ )
76
+ SheetTrigger.displayName = "SheetTrigger"
77
+
78
+ const sheetVariants = cva(
79
+ "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out",
80
+ {
81
+ variants: {
82
+ side: {
83
+ top: "inset-x-0 top-0 border-b",
84
+ bottom: "inset-x-0 bottom-0 border-t",
85
+ left: "inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm",
86
+ right: "inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm",
87
+ },
88
+ },
89
+ defaultVariants: {
90
+ side: "right",
91
+ },
92
+ }
93
+ )
94
+
95
+ interface SheetContentProps
96
+ extends React.HTMLAttributes<HTMLDivElement>,
97
+ VariantProps<typeof sheetVariants> { }
98
+
99
+ const SheetContent = React.forwardRef<HTMLDivElement, SheetContentProps>(
100
+ ({ side = "right", className, children, ...props }, ref) => {
101
+ const context = React.useContext(SheetContext)
102
+ if (!context) throw new Error("SheetContent must be used within Sheet")
103
+
104
+ React.useEffect(() => {
105
+ const handleEscape = (e: KeyboardEvent) => {
106
+ if (e.key === "Escape") {
107
+ context.onOpenChange(false)
108
+ }
109
+ }
110
+
111
+ if (context.open) {
112
+ document.addEventListener("keydown", handleEscape)
113
+ document.body.style.overflow = "hidden"
114
+ }
115
+
116
+ return () => {
117
+ document.removeEventListener("keydown", handleEscape)
118
+ document.body.style.overflow = ""
119
+ }
120
+ }, [context.open, context])
121
+
122
+ if (!context.open) return null
123
+
124
+ return (
125
+ <>
126
+ <div
127
+ className="fixed inset-0 z-50 bg-black/80"
128
+ onClick={() => context.onOpenChange(false)}
129
+ />
130
+ <div
131
+ ref={ref}
132
+ role="dialog"
133
+ aria-modal="true"
134
+ className={cn(sheetVariants({ side }), className)}
135
+ {...props}
136
+ >
137
+ {children}
138
+ <button
139
+ className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
140
+ onClick={() => context.onOpenChange(false)}
141
+ >
142
+ <svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
143
+ <path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
144
+ </svg>
145
+ <span className="sr-only">Close</span>
146
+ </button>
147
+ </div>
148
+ </>
149
+ )
150
+ }
151
+ )
152
+ SheetContent.displayName = "SheetContent"
153
+
154
+ const SheetHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
155
+ <div className={cn("flex flex-col space-y-2 text-center sm:text-left", className)} {...props} />
156
+ )
157
+ SheetHeader.displayName = "SheetHeader"
158
+
159
+ const SheetFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
160
+ <div className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)} {...props} />
161
+ )
162
+ SheetFooter.displayName = "SheetFooter"
163
+
164
+ const SheetTitle = React.forwardRef<
165
+ HTMLHeadingElement,
166
+ React.HTMLAttributes<HTMLHeadingElement>
167
+ >(({ className, ...props }, ref) => (
168
+ <h2 ref={ref} className={cn("text-lg font-semibold text-foreground", className)} {...props} />
169
+ ))
170
+ SheetTitle.displayName = "SheetTitle"
171
+
172
+ const SheetDescription = React.forwardRef<
173
+ HTMLParagraphElement,
174
+ React.HTMLAttributes<HTMLParagraphElement>
175
+ >(({ className, ...props }, ref) => (
176
+ <p ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
177
+ ))
178
+ SheetDescription.displayName = "SheetDescription"
179
+
180
+ export { Sheet, SheetTrigger, SheetContent, SheetHeader, SheetFooter, SheetTitle, SheetDescription }
@@ -0,0 +1,26 @@
1
+ import * as React from "react"
2
+ import { cn } from "@/lib/utils"
3
+
4
+ /**
5
+ * Skeleton loading placeholder
6
+ *
7
+ * @example
8
+ * <Skeleton className="h-4 w-[200px]" />
9
+ *
10
+ * @example
11
+ * // Circle skeleton for avatars
12
+ * <Skeleton className="h-12 w-12 rounded-full" />
13
+ */
14
+ const Skeleton = React.forwardRef<
15
+ HTMLDivElement,
16
+ React.HTMLAttributes<HTMLDivElement>
17
+ >(({ className, ...props }, ref) => (
18
+ <div
19
+ ref={ref}
20
+ className={cn("animate-pulse rounded-md bg-primary/10", className)}
21
+ {...props}
22
+ />
23
+ ))
24
+ Skeleton.displayName = "Skeleton"
25
+
26
+ export { Skeleton }