@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.
package/registry/card.tsx CHANGED
@@ -15,33 +15,17 @@ import { cn } from "@/lib/utils"
15
15
  * </Card>
16
16
  */
17
17
 
18
- interface CardBaseProps {
19
- className?: string
20
- children?: React.ReactNode
21
- }
22
-
23
- const Card = React.forwardRef(
24
- <T extends React.ElementType = "div">(
25
- {
26
- as,
27
- className,
28
- ...props
29
- }: CardBaseProps & { as?: T } & Omit<React.ComponentPropsWithoutRef<T>, keyof CardBaseProps | "as">,
30
- ref: React.ForwardedRef<React.ElementRef<T>>
31
- ) => {
32
- const Comp = as || "div"
33
-
34
- return (
35
- <Comp
36
- ref={ref as any}
37
- className={cn(
38
- "rounded-xl border bg-card text-card-foreground shadow",
39
- className
40
- )}
41
- {...props}
42
- />
43
- )
44
- }
18
+ const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
19
+ ({ className, ...props }, ref) => (
20
+ <div
21
+ ref={ref}
22
+ className={cn(
23
+ "rounded-xl border bg-card text-card-foreground shadow",
24
+ className
25
+ )}
26
+ {...props}
27
+ />
28
+ )
45
29
  )
46
30
  Card.displayName = "Card"
47
31
 
@@ -57,26 +41,16 @@ const CardHeader = React.forwardRef<
57
41
  ))
58
42
  CardHeader.displayName = "CardHeader"
59
43
 
60
- const CardTitle = React.forwardRef(
61
- <T extends React.ElementType = "h3">(
62
- {
63
- as,
64
- className,
65
- ...props
66
- }: CardBaseProps & { as?: T } & Omit<React.ComponentPropsWithoutRef<T>, keyof CardBaseProps | "as">,
67
- ref: React.ForwardedRef<React.ElementRef<T>>
68
- ) => {
69
- const Comp = as || "h3"
70
-
71
- return (
72
- <Comp
73
- ref={ref as any}
74
- className={cn("font-semibold leading-none tracking-tight", className)}
75
- {...props}
76
- />
77
- )
78
- }
79
- )
44
+ const CardTitle = React.forwardRef<
45
+ HTMLHeadingElement,
46
+ React.HTMLAttributes<HTMLHeadingElement>
47
+ >(({ className, ...props }, ref) => (
48
+ <h3
49
+ ref={ref}
50
+ className={cn("font-semibold leading-none tracking-tight", className)}
51
+ {...props}
52
+ />
53
+ ))
80
54
  CardTitle.displayName = "CardTitle"
81
55
 
82
56
  const CardDescription = React.forwardRef<
@@ -0,0 +1,171 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { Check, ChevronsUpDown, X } from "lucide-react"
5
+
6
+ import { cn } from "@/lib/utils"
7
+ import { Button } from "@/components/ui/button"
8
+ import {
9
+ Command,
10
+ CommandEmpty,
11
+ CommandGroup,
12
+ CommandInput,
13
+ CommandItem,
14
+ CommandList,
15
+ } from "@/components/ui/command"
16
+ import {
17
+ Popover,
18
+ PopoverContent,
19
+ PopoverTrigger,
20
+ } from "@/components/ui/popover"
21
+ import { Badge } from "@/components/ui/badge"
22
+
23
+ export type ComboboxOption = {
24
+ value: string
25
+ label: string
26
+ icon?: React.ComponentType<{ className?: string }>
27
+ }
28
+
29
+ interface ComboboxProps {
30
+ options: ComboboxOption[]
31
+ value?: string | string[]
32
+ onValueChange?: (value: string | string[]) => void
33
+ placeholder?: string
34
+ searchPlaceholder?: string
35
+ emptyMessage?: string
36
+ multiple?: boolean
37
+ className?: string
38
+ disabled?: boolean
39
+ }
40
+
41
+ export function Combobox({
42
+ options,
43
+ value,
44
+ onValueChange,
45
+ placeholder = "Select option...",
46
+ searchPlaceholder = "Search...",
47
+ emptyMessage = "No option found.",
48
+ multiple = false,
49
+ className,
50
+ disabled = false,
51
+ }: ComboboxProps) {
52
+ const [open, setOpen] = React.useState(false)
53
+
54
+ // Helper to handle selection
55
+ const handleSelect = React.useCallback(
56
+ (currentValue: string) => {
57
+ if (multiple) {
58
+ const currentValues = Array.isArray(value) ? value : []
59
+ const newValues = currentValues.includes(currentValue)
60
+ ? currentValues.filter((v) => v !== currentValue)
61
+ : [...currentValues, currentValue]
62
+ onValueChange?.(newValues)
63
+ } else {
64
+ onValueChange?.(currentValue === value ? "" : currentValue)
65
+ setOpen(false)
66
+ }
67
+ },
68
+ [multiple, value, onValueChange]
69
+ )
70
+
71
+ // Derived state for display
72
+ const selectedOptions = React.useMemo(() => {
73
+ if (multiple) {
74
+ const currentValues = Array.isArray(value) ? value : []
75
+ return currentValues
76
+ .map((v) => options.find((opt) => opt.value === v))
77
+ .filter(Boolean) as ComboboxOption[]
78
+ } else {
79
+ const option = options.find((opt) => opt.value === value)
80
+ return option ? [option] : []
81
+ }
82
+ }, [multiple, value, options])
83
+
84
+ return (
85
+ <Popover open={open} onOpenChange={setOpen}>
86
+ <PopoverTrigger asChild>
87
+ <Button
88
+ variant="outline"
89
+ role="combobox"
90
+ aria-expanded={open}
91
+ className={cn("w-full justify-between h-auto min-h-10", className)}
92
+ disabled={disabled}
93
+ >
94
+ <div className="flex gap-1 flex-wrap items-center text-left">
95
+ {selectedOptions.length > 0 ? (
96
+ multiple ? (
97
+ selectedOptions.length > 3 ? (
98
+ <Badge variant="secondary" className="rounded-sm px-1 font-normal">
99
+ {selectedOptions.length} selected
100
+ </Badge>
101
+ ) : (
102
+ selectedOptions.map((opt) => {
103
+ const Icon = opt.icon
104
+ return (
105
+ <Badge
106
+ variant="secondary"
107
+ key={opt.value}
108
+ className="rounded-sm px-1 font-normal items-center gap-1"
109
+ onClick={(e: React.MouseEvent) => {
110
+ e.stopPropagation()
111
+ handleSelect(opt.value)
112
+ }}
113
+ >
114
+ {Icon && <Icon className="h-3 w-3" />}
115
+ {opt.label}
116
+ <X className="h-3 w-3 text-muted-foreground hover:text-foreground ml-0.5" />
117
+ </Badge>
118
+ )
119
+ })
120
+ )
121
+ ) : (
122
+ <div className="flex items-center gap-2">
123
+ {(() => {
124
+ const Icon = selectedOptions[0].icon
125
+ return Icon ? <Icon className="h-4 w-4 text-muted-foreground" /> : null
126
+ })()}
127
+ <span>{selectedOptions[0].label}</span>
128
+ </div>
129
+ )
130
+ ) : (
131
+ <span className="text-muted-foreground">{placeholder}</span>
132
+ )}
133
+ </div>
134
+ <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
135
+ </Button>
136
+ </PopoverTrigger>
137
+ <PopoverContent className="w-full min-w-[200px] p-0">
138
+ <Command>
139
+ <CommandInput placeholder={searchPlaceholder} />
140
+ <CommandList>
141
+ <CommandEmpty>{emptyMessage}</CommandEmpty>
142
+ <CommandGroup>
143
+ {options.map((option) => (
144
+ <CommandItem
145
+ key={option.value}
146
+ value={option.label}
147
+ onSelect={() => handleSelect(option.value)}
148
+ >
149
+ <Check
150
+ className={cn(
151
+ "mr-2 h-4 w-4",
152
+ multiple
153
+ ? (Array.isArray(value) && value.includes(option.value)
154
+ ? "opacity-100"
155
+ : "opacity-0")
156
+ : value === option.value
157
+ ? "opacity-100"
158
+ : "opacity-0"
159
+ )}
160
+ />
161
+ {option.icon && <option.icon className="mr-2 h-4 w-4 text-muted-foreground" />}
162
+ {option.label}
163
+ </CommandItem>
164
+ ))}
165
+ </CommandGroup>
166
+ </CommandList>
167
+ </Command>
168
+ </PopoverContent>
169
+ </Popover>
170
+ )
171
+ }
@@ -0,0 +1,300 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { Search } from "lucide-react"
5
+ import { cn } from "@/lib/utils"
6
+ import { Dialog, DialogContent } from "@/components/ui/dialog"
7
+
8
+ interface CommandContextValue {
9
+ search: string
10
+ setSearch: (search: string) => void
11
+ selectedIndex: number
12
+ setSelectedIndex: (index: number) => void
13
+ items: string[]
14
+ registerItem: (value: string) => void
15
+ unregisterItem: (value: string) => void
16
+ }
17
+
18
+ const CommandContext = React.createContext<CommandContextValue | null>(null)
19
+
20
+ function useCommandContext() {
21
+ const context = React.useContext(CommandContext)
22
+ if (!context) {
23
+ throw new Error("Command components must be used within Command")
24
+ }
25
+ return context
26
+ }
27
+
28
+ interface CommandProps extends React.HTMLAttributes<HTMLDivElement> {
29
+ children: React.ReactNode
30
+ }
31
+
32
+ const Command = React.forwardRef<HTMLDivElement, CommandProps>(
33
+ ({ className, children, ...props }, ref) => {
34
+ const [search, setSearch] = React.useState("")
35
+ const [selectedIndex, setSelectedIndex] = React.useState(0)
36
+ const [items, setItems] = React.useState<string[]>([])
37
+
38
+ const registerItem = React.useCallback((value: string) => {
39
+ setItems((prev) => [...prev, value])
40
+ }, [])
41
+
42
+ const unregisterItem = React.useCallback((value: string) => {
43
+ setItems((prev) => prev.filter((item) => item !== value))
44
+ }, [])
45
+
46
+ // Handle keyboard navigation
47
+ const handleKeyDown = (e: React.KeyboardEvent) => {
48
+ switch (e.key) {
49
+ case "ArrowDown":
50
+ e.preventDefault()
51
+ setSelectedIndex((prev) => Math.min(prev + 1, items.length - 1))
52
+ break
53
+ case "ArrowUp":
54
+ e.preventDefault()
55
+ setSelectedIndex((prev) => Math.max(prev - 1, 0))
56
+ break
57
+ case "Enter":
58
+ e.preventDefault()
59
+ const selectedItem = document.querySelector('[data-selected="true"]') as HTMLElement
60
+ selectedItem?.click()
61
+ break
62
+ }
63
+ }
64
+
65
+ return (
66
+ <CommandContext.Provider
67
+ value={{
68
+ search,
69
+ setSearch,
70
+ selectedIndex,
71
+ setSelectedIndex,
72
+ items,
73
+ registerItem,
74
+ unregisterItem,
75
+ }}
76
+ >
77
+ <div
78
+ ref={ref}
79
+ className={cn(
80
+ "flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
81
+ className
82
+ )}
83
+ onKeyDown={handleKeyDown}
84
+ {...props}
85
+ >
86
+ {children}
87
+ </div>
88
+ </CommandContext.Provider>
89
+ )
90
+ }
91
+ )
92
+ Command.displayName = "Command"
93
+
94
+ interface CommandDialogProps {
95
+ children: React.ReactNode
96
+ open?: boolean
97
+ onOpenChange?: (open: boolean) => void
98
+ }
99
+
100
+ const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
101
+ return (
102
+ <Dialog {...props}>
103
+ <DialogContent className="overflow-hidden p-0 shadow-lg">
104
+ <Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground">
105
+ {children}
106
+ </Command>
107
+ </DialogContent>
108
+ </Dialog>
109
+ )
110
+ }
111
+
112
+ interface CommandInputProps extends React.InputHTMLAttributes<HTMLInputElement> { }
113
+
114
+ const CommandInput = React.forwardRef<HTMLInputElement, CommandInputProps>(
115
+ ({ className, ...props }, ref) => {
116
+ const { search, setSearch, setSelectedIndex } = useCommandContext()
117
+
118
+ return (
119
+ <div className="flex items-center border-b px-3">
120
+ <Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
121
+ <input
122
+ ref={ref}
123
+ value={search}
124
+ onChange={(e) => {
125
+ setSearch(e.target.value)
126
+ setSelectedIndex(0)
127
+ }}
128
+ className={cn(
129
+ "flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
130
+ className
131
+ )}
132
+ {...props}
133
+ />
134
+ </div>
135
+ )
136
+ }
137
+ )
138
+ CommandInput.displayName = "CommandInput"
139
+
140
+ interface CommandListProps extends React.HTMLAttributes<HTMLDivElement> { }
141
+
142
+ const CommandList = React.forwardRef<HTMLDivElement, CommandListProps>(
143
+ ({ className, ...props }, ref) => (
144
+ <div
145
+ ref={ref}
146
+ className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
147
+ {...props}
148
+ />
149
+ )
150
+ )
151
+ CommandList.displayName = "CommandList"
152
+
153
+ interface CommandEmptyProps extends React.HTMLAttributes<HTMLDivElement> { }
154
+
155
+ const CommandEmpty = React.forwardRef<HTMLDivElement, CommandEmptyProps>(
156
+ ({ className, children, ...props }, ref) => {
157
+ const { search, items } = useCommandContext()
158
+
159
+ // Count how many items would be visible with the current search
160
+ const visibleCount = search
161
+ ? items.filter(item => item.toLowerCase().includes(search.toLowerCase())).length
162
+ : items.length
163
+
164
+ // Only show empty message when user has typed something but no results match
165
+ if (!search || visibleCount > 0) {
166
+ return null
167
+ }
168
+
169
+ return (
170
+ <div
171
+ ref={ref}
172
+ className={cn("py-6 text-center text-sm", className)}
173
+ {...props}
174
+ >
175
+ {children}
176
+ </div>
177
+ )
178
+ }
179
+ )
180
+ CommandEmpty.displayName = "CommandEmpty"
181
+
182
+ interface CommandGroupProps extends React.HTMLAttributes<HTMLDivElement> {
183
+ heading?: string
184
+ }
185
+
186
+ const CommandGroup = React.forwardRef<HTMLDivElement, CommandGroupProps>(
187
+ ({ className, heading, children, ...props }, ref) => (
188
+ <div
189
+ ref={ref}
190
+ className={cn(
191
+ "overflow-hidden p-1 text-foreground",
192
+ className
193
+ )}
194
+ {...props}
195
+ >
196
+ {heading && (
197
+ <div className="px-2 py-1.5 text-xs font-medium text-muted-foreground">
198
+ {heading}
199
+ </div>
200
+ )}
201
+ {children}
202
+ </div>
203
+ )
204
+ )
205
+ CommandGroup.displayName = "CommandGroup"
206
+
207
+ interface CommandSeparatorProps extends React.HTMLAttributes<HTMLDivElement> { }
208
+
209
+ const CommandSeparator = React.forwardRef<HTMLDivElement, CommandSeparatorProps>(
210
+ ({ className, ...props }, ref) => (
211
+ <div
212
+ ref={ref}
213
+ className={cn("-mx-1 h-px bg-border", className)}
214
+ {...props}
215
+ />
216
+ )
217
+ )
218
+ CommandSeparator.displayName = "CommandSeparator"
219
+
220
+ interface CommandItemProps extends React.HTMLAttributes<HTMLDivElement> {
221
+ value?: string
222
+ onSelect?: () => void
223
+ disabled?: boolean
224
+ }
225
+
226
+ const CommandItem = React.forwardRef<HTMLDivElement, CommandItemProps>(
227
+ ({ className, value, onSelect, disabled, children, ...props }, ref) => {
228
+ const { search, selectedIndex, setSelectedIndex, registerItem, unregisterItem, items } = useCommandContext()
229
+ const itemValue = value || (typeof children === "string" ? children : "")
230
+
231
+ // Register/unregister on mount
232
+ React.useEffect(() => {
233
+ registerItem(itemValue)
234
+ return () => unregisterItem(itemValue)
235
+ }, [itemValue, registerItem, unregisterItem])
236
+
237
+ // Filter based on search
238
+ const isVisible = !search || itemValue.toLowerCase().includes(search.toLowerCase())
239
+
240
+ if (!isVisible) return null
241
+
242
+ const itemIndex = items.indexOf(itemValue)
243
+ const isSelected = itemIndex === selectedIndex
244
+
245
+ return (
246
+ <div
247
+ ref={ref}
248
+ role="option"
249
+ aria-selected={isSelected}
250
+ data-selected={isSelected}
251
+ data-disabled={disabled}
252
+ className={cn(
253
+ "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none",
254
+ isSelected && "bg-accent text-accent-foreground",
255
+ disabled && "pointer-events-none opacity-50",
256
+ !disabled && "cursor-pointer",
257
+ className
258
+ )}
259
+ onClick={() => {
260
+ if (!disabled) {
261
+ onSelect?.()
262
+ }
263
+ }}
264
+ onMouseEnter={() => setSelectedIndex(itemIndex)}
265
+ {...props}
266
+ >
267
+ {children}
268
+ </div>
269
+ )
270
+ }
271
+ )
272
+ CommandItem.displayName = "CommandItem"
273
+
274
+ const CommandShortcut = ({
275
+ className,
276
+ ...props
277
+ }: React.HTMLAttributes<HTMLSpanElement>) => {
278
+ return (
279
+ <span
280
+ className={cn(
281
+ "ml-auto text-xs tracking-widest text-muted-foreground",
282
+ className
283
+ )}
284
+ {...props}
285
+ />
286
+ )
287
+ }
288
+ CommandShortcut.displayName = "CommandShortcut"
289
+
290
+ export {
291
+ Command,
292
+ CommandDialog,
293
+ CommandInput,
294
+ CommandList,
295
+ CommandEmpty,
296
+ CommandGroup,
297
+ CommandItem,
298
+ CommandShortcut,
299
+ CommandSeparator,
300
+ }
@@ -18,38 +18,22 @@ const containerVariants = cva("mx-auto w-full px-4", {
18
18
  },
19
19
  })
20
20
 
21
- type ContainerVariants = VariantProps<typeof containerVariants>
22
-
23
- interface ContainerBaseProps extends ContainerVariants {
24
- className?: string
25
- children?: React.ReactNode
26
- }
21
+ interface ContainerProps
22
+ extends React.HTMLAttributes<HTMLDivElement>,
23
+ VariantProps<typeof containerVariants> { }
27
24
 
28
25
  /**
29
- * Polymorphic Container for max-width layouts
26
+ * Container for max-width layouts
30
27
  *
31
28
  * @example
32
29
  * <Container size="lg">Content</Container>
33
- *
34
- * @example
35
- * <Container as="section" size="md">Section content</Container>
36
30
  */
37
- const Container = React.forwardRef(
38
- <T extends React.ElementType = "div">(
39
- {
40
- as,
41
- className,
42
- size,
43
- ...props
44
- }: ContainerBaseProps & { as?: T } & Omit<React.ComponentPropsWithoutRef<T>, keyof ContainerBaseProps | "as">,
45
- ref: React.ForwardedRef<React.ElementRef<T>>
46
- ) => {
47
- const Comp = as || "div"
48
-
31
+ const Container = React.forwardRef<HTMLDivElement, ContainerProps>(
32
+ ({ className, size, ...props }, ref) => {
49
33
  return (
50
- <Comp
51
- ref={ref as any}
52
- className={cn(containerVariants({ size, className }))}
34
+ <div
35
+ ref={ref}
36
+ className={cn(containerVariants({ size }), className)}
53
37
  {...props}
54
38
  />
55
39
  )