@srcroot/ui 0.0.2 → 0.0.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@srcroot/ui",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "description": "A shadcn-style CLI UI library with polymorphic, accessible React components",
5
5
  "type": "module",
6
6
  "bin": {
@@ -5,6 +5,7 @@ interface AccordionContextValue {
5
5
  value: string[]
6
6
  onValueChange: (value: string[]) => void
7
7
  type: "single" | "multiple"
8
+ collapsible: boolean
8
9
  }
9
10
 
10
11
  const AccordionContext = React.createContext<AccordionContextValue | null>(null)
@@ -37,14 +38,14 @@ interface AccordionProps extends React.HTMLAttributes<HTMLDivElement> {
37
38
  * </Accordion>
38
39
  */
39
40
  const Accordion = React.forwardRef<HTMLDivElement, AccordionProps>(
40
- ({ className, type = "single", value: controlledValue, onValueChange, defaultValue = [], children, ...props }, ref) => {
41
+ ({ className, type = "single", value: controlledValue, onValueChange, defaultValue = [], collapsible = false, children, ...props }, ref) => {
41
42
  const [uncontrolledValue, setUncontrolledValue] = React.useState(defaultValue)
42
43
 
43
44
  const value = controlledValue !== undefined ? controlledValue : uncontrolledValue
44
45
  const setValue = onValueChange || setUncontrolledValue
45
46
 
46
47
  return (
47
- <AccordionContext.Provider value={{ value, onValueChange: setValue, type }}>
48
+ <AccordionContext.Provider value={{ value, onValueChange: setValue, type, collapsible }}>
48
49
  <div ref={ref} className={cn("", className)} {...props}>
49
50
  {children}
50
51
  </div>
@@ -54,6 +55,7 @@ const Accordion = React.forwardRef<HTMLDivElement, AccordionProps>(
54
55
  )
55
56
  Accordion.displayName = "Accordion"
56
57
 
58
+
57
59
  interface AccordionItemProps extends React.HTMLAttributes<HTMLDivElement> {
58
60
  value: string
59
61
  }
@@ -67,6 +69,8 @@ const AccordionItem = React.forwardRef<HTMLDivElement, AccordionItemProps>(
67
69
 
68
70
  const toggle = () => {
69
71
  if (context.type === "single") {
72
+ // In single mode with collapsible=false, don't allow closing
73
+ if (isOpen && !context.collapsible) return
70
74
  context.onValueChange(isOpen ? [] : [value])
71
75
  } else {
72
76
  context.onValueChange(
@@ -22,15 +22,12 @@ const badgeVariants = cva(
22
22
  }
23
23
  )
24
24
 
25
- type BadgeVariants = VariantProps<typeof badgeVariants>
26
-
27
- interface BadgeBaseProps extends BadgeVariants {
28
- className?: string
29
- children?: React.ReactNode
30
- }
25
+ interface BadgeProps
26
+ extends React.HTMLAttributes<HTMLSpanElement>,
27
+ VariantProps<typeof badgeVariants> { }
31
28
 
32
29
  /**
33
- * Polymorphic Badge component for status indicators
30
+ * Badge component for status indicators
34
31
  *
35
32
  * @example
36
33
  * // Default badge
@@ -38,26 +35,13 @@ interface BadgeBaseProps extends BadgeVariants {
38
35
  *
39
36
  * // Destructive variant
40
37
  * <Badge variant="destructive">Error</Badge>
41
- *
42
- * // As a link
43
- * <Badge as="a" href="/status">View Status</Badge>
44
38
  */
45
- const Badge = React.forwardRef(
46
- <T extends React.ElementType = "span">(
47
- {
48
- as,
49
- className,
50
- variant,
51
- ...props
52
- }: BadgeBaseProps & { as?: T } & Omit<React.ComponentPropsWithoutRef<T>, keyof BadgeBaseProps | "as">,
53
- ref: React.ForwardedRef<React.ElementRef<T>>
54
- ) => {
55
- const Comp = as || "span"
56
-
39
+ const Badge = React.forwardRef<HTMLSpanElement, BadgeProps>(
40
+ ({ className, variant, ...props }, ref) => {
57
41
  return (
58
- <Comp
59
- ref={ref as any}
60
- className={cn(badgeVariants({ variant, className }))}
42
+ <span
43
+ ref={ref}
44
+ className={cn(badgeVariants({ variant }), className)}
61
45
  {...props}
62
46
  />
63
47
  )
@@ -28,7 +28,7 @@ Breadcrumb.displayName = "Breadcrumb"
28
28
 
29
29
  const BreadcrumbList = React.forwardRef<
30
30
  HTMLOListElement,
31
- React.OListHTMLAttributes<HTMLOListElement>
31
+ React.OlHTMLAttributes<HTMLOListElement>
32
32
  >(({ className, ...props }, ref) => (
33
33
  <ol
34
34
  ref={ref}
@@ -33,15 +33,12 @@ const buttonGroupVariants = cva("inline-flex", {
33
33
  },
34
34
  })
35
35
 
36
- type ButtonGroupVariants = VariantProps<typeof buttonGroupVariants>
37
-
38
- interface ButtonGroupBaseProps extends ButtonGroupVariants {
39
- className?: string
40
- children?: React.ReactNode
41
- }
36
+ interface ButtonGroupProps
37
+ extends React.HTMLAttributes<HTMLDivElement>,
38
+ VariantProps<typeof buttonGroupVariants> { }
42
39
 
43
40
  /**
44
- * Polymorphic ButtonGroup to group buttons together
41
+ * ButtonGroup to group buttons together
45
42
  *
46
43
  * @example
47
44
  * <ButtonGroup>
@@ -49,31 +46,14 @@ interface ButtonGroupBaseProps extends ButtonGroupVariants {
49
46
  * <Button>Center</Button>
50
47
  * <Button>Right</Button>
51
48
  * </ButtonGroup>
52
- *
53
- * @example
54
- * <ButtonGroup attached={false}>
55
- * <Button>Spaced</Button>
56
- * <Button>Buttons</Button>
57
- * </ButtonGroup>
58
49
  */
59
- const ButtonGroup = React.forwardRef(
60
- <T extends React.ElementType = "div">(
61
- {
62
- as,
63
- className,
64
- orientation,
65
- attached,
66
- ...props
67
- }: ButtonGroupBaseProps & { as?: T } & Omit<React.ComponentPropsWithoutRef<T>, keyof ButtonGroupBaseProps | "as">,
68
- ref: React.ForwardedRef<React.ElementRef<T>>
69
- ) => {
70
- const Comp = as || "div"
71
-
50
+ const ButtonGroup = React.forwardRef<HTMLDivElement, ButtonGroupProps>(
51
+ ({ className, orientation, attached, ...props }, ref) => {
72
52
  return (
73
- <Comp
74
- ref={ref as any}
53
+ <div
54
+ ref={ref}
75
55
  role="group"
76
- className={cn(buttonGroupVariants({ orientation, attached, className }))}
56
+ className={cn(buttonGroupVariants({ orientation, attached }), className)}
77
57
  {...props}
78
58
  />
79
59
  )
@@ -32,68 +32,42 @@ const buttonVariants = cva(
32
32
  }
33
33
  )
34
34
 
35
- type ButtonVariants = VariantProps<typeof buttonVariants>
36
-
37
- interface ButtonBaseProps extends ButtonVariants {
38
- className?: string
39
- children?: React.ReactNode
35
+ interface ButtonProps
36
+ extends React.ButtonHTMLAttributes<HTMLButtonElement>,
37
+ VariantProps<typeof buttonVariants> {
38
+ asChild?: boolean
40
39
  }
41
40
 
42
41
  /**
43
- * Polymorphic Button component
42
+ * Button component
44
43
  *
45
44
  * @example
46
- * // As a button (default)
45
+ * // Default button
47
46
  * <Button variant="outline">Click me</Button>
48
47
  *
49
- * // As a link
50
- * <Button as="a" href="/home" variant="link">Go Home</Button>
51
- *
52
48
  * // With loading state
53
49
  * <Button disabled>
54
50
  * <LoadingSpinner /> Processing...
55
51
  * </Button>
56
52
  */
57
- const Button = React.forwardRef(
58
- <T extends React.ElementType = "button">(
59
- {
60
- as,
61
- className,
62
- variant,
63
- size,
64
- ...props
65
- }: ButtonBaseProps & { as?: T } & Omit<React.ComponentPropsWithoutRef<T>, keyof ButtonBaseProps | "as">,
66
- ref: React.ForwardedRef<React.ElementRef<T>>
67
- ) => {
68
- const Comp = as || "button"
69
-
70
- // Ensure proper keyboard handling for non-button elements
71
- const handleKeyDown = (e: React.KeyboardEvent) => {
72
- if (Comp !== "button" && (e.key === "Enter" || e.key === " ")) {
73
- e.preventDefault()
74
- ; (e.currentTarget as HTMLElement).click()
75
- }
76
- // Call original onKeyDown if provided
77
- const originalOnKeyDown = (props as any).onKeyDown
78
- if (originalOnKeyDown) {
79
- originalOnKeyDown(e)
80
- }
53
+ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
54
+ ({ className, variant, size, asChild = false, children, ...props }, ref) => {
55
+ if (asChild && React.isValidElement(children)) {
56
+ return React.cloneElement(children as React.ReactElement<any>, {
57
+ ref,
58
+ className: cn(buttonVariants({ variant, size }), className),
59
+ ...props,
60
+ })
81
61
  }
82
62
 
83
- // Add role="button" for non-button elements
84
- const accessibilityProps = Comp !== "button" ? {
85
- role: "button",
86
- tabIndex: 0,
87
- onKeyDown: handleKeyDown,
88
- } : {}
89
-
90
63
  return (
91
- <Comp
92
- ref={ref as any}
93
- className={cn(buttonVariants({ variant, size, className }))}
94
- {...accessibilityProps}
64
+ <button
65
+ ref={ref}
66
+ className={cn(buttonVariants({ variant, size }), className)}
95
67
  {...props}
96
- />
68
+ >
69
+ {children}
70
+ </button>
97
71
  )
98
72
  }
99
73
  )
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<
@@ -120,9 +120,6 @@ export function Combobox({
120
120
  )
121
121
  ) : (
122
122
  <div className="flex items-center gap-2">
123
- {selectedOptions[0].icon && (
124
- <selectedOptions[0].icon className="h-4 w-4 text-muted-foreground" />
125
- ) /* this effectively forces me to use a variable too */}
126
123
  {(() => {
127
124
  const Icon = selectedOptions[0].icon
128
125
  return Icon ? <Icon className="h-4 w-4 text-muted-foreground" /> : null
@@ -156,11 +156,13 @@ const CommandEmpty = React.forwardRef<HTMLDivElement, CommandEmptyProps>(
156
156
  ({ className, children, ...props }, ref) => {
157
157
  const { search, items } = useCommandContext()
158
158
 
159
- // Check if any items are visible
160
- const hasVisibleItems = items.length > 0
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
161
163
 
162
- if (hasVisibleItems && search) {
163
- // This will be handled by filtering in CommandItem
164
+ // Only show empty message when user has typed something but no results match
165
+ if (!search || visibleCount > 0) {
164
166
  return null
165
167
  }
166
168
 
@@ -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
  )
@@ -88,19 +88,21 @@ const DrawerClose = React.forwardRef<HTMLButtonElement, React.ButtonHTMLAttribut
88
88
  )
89
89
  DrawerClose.displayName = "DrawerClose"
90
90
 
91
- // Drawer Overlay
92
- const DrawerOverlay = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
93
- ({ className, ...props }, ref) => {
94
- const { open, onOpenChange } = useDrawer()
91
+ // Drawer Overlay (internal)
92
+ interface DrawerOverlayProps extends React.HTMLAttributes<HTMLDivElement> {
93
+ isAnimating: boolean
94
+ }
95
95
 
96
- if (!open) return null
96
+ const DrawerOverlay = React.forwardRef<HTMLDivElement, DrawerOverlayProps>(
97
+ ({ className, isAnimating, ...props }, ref) => {
98
+ const { onOpenChange } = useDrawer()
97
99
 
98
100
  return (
99
101
  <div
100
102
  ref={ref}
101
103
  className={cn(
102
- "fixed inset-0 z-50 bg-black/80",
103
- "animate-in fade-in-0",
104
+ "fixed inset-0 z-50 bg-black/80 transition-opacity duration-300",
105
+ isAnimating ? "opacity-100" : "opacity-0",
104
106
  className
105
107
  )}
106
108
  onClick={() => onOpenChange(false)}
@@ -119,6 +121,28 @@ interface DrawerContentProps extends React.HTMLAttributes<HTMLDivElement> {
119
121
  const DrawerContent = React.forwardRef<HTMLDivElement, DrawerContentProps>(
120
122
  ({ className, children, side = "bottom", ...props }, ref) => {
121
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])
122
146
 
123
147
  // Close on Escape
124
148
  React.useEffect(() => {
@@ -142,11 +166,11 @@ const DrawerContent = React.forwardRef<HTMLDivElement, DrawerContentProps>(
142
166
  }
143
167
  }, [open])
144
168
 
145
- if (!open) return null
169
+ if (!isVisible) return null
146
170
 
147
171
  return (
148
172
  <>
149
- <DrawerOverlay />
173
+ <DrawerOverlay isAnimating={isAnimating} />
150
174
  <div
151
175
  ref={ref}
152
176
  className={cn(
@@ -154,9 +178,9 @@ const DrawerContent = React.forwardRef<HTMLDivElement, DrawerContentProps>(
154
178
  "transition-transform duration-300 ease-out",
155
179
  side === "bottom" && "inset-x-0 bottom-0 rounded-t-xl border-t",
156
180
  side === "top" && "inset-x-0 top-0 rounded-b-xl border-b",
157
- // Animation
158
- side === "bottom" && "animate-in slide-in-from-bottom",
159
- side === "top" && "animate-in slide-in-from-top",
181
+ // Animation states
182
+ side === "bottom" && (isAnimating ? "translate-y-0" : "translate-y-full"),
183
+ side === "top" && (isAnimating ? "translate-y-0" : "-translate-y-full"),
160
184
  className
161
185
  )}
162
186
  {...props}