@srcroot/ui 0.0.61 → 0.0.62

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.
@@ -1,33 +1,35 @@
1
- import * as React from "react"
2
- import { Label } from "@/components/ui/label"
3
- import { cn } from "@/lib/utils"
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { Label } from "@/components/ui/label";
5
+ import { cn } from "@/lib/utils";
4
6
 
5
7
  export interface FormFieldProps extends React.HTMLAttributes<HTMLDivElement> {
6
- /**
7
- * The label for the field
8
- */
9
- label?: React.ReactNode
10
- /**
11
- * Helper text description
12
- */
13
- description?: React.ReactNode
14
- /**
15
- * Error message
16
- */
17
- error?: React.ReactNode
18
- /**
19
- * Whether the field is required (shows asterisk)
20
- */
21
- required?: boolean
22
- /**
23
- * The ID of the input element, used for label association
24
- */
25
- htmlFor?: string
8
+ /**
9
+ * The label for the field
10
+ */
11
+ label?: React.ReactNode;
12
+ /**
13
+ * Helper text description
14
+ */
15
+ description?: React.ReactNode;
16
+ /**
17
+ * Error message
18
+ */
19
+ error?: React.ReactNode;
20
+ /**
21
+ * Whether the field is required (shows asterisk)
22
+ */
23
+ required?: boolean;
24
+ /**
25
+ * The ID of the input element, used for label association
26
+ */
27
+ htmlFor?: string;
26
28
  }
27
29
 
28
30
  /**
29
31
  * FormField wrapper for consistent label and error placement
30
- *
32
+ *
31
33
  * @example
32
34
  * <FormField
33
35
  * label="Email"
@@ -39,53 +41,62 @@ export interface FormFieldProps extends React.HTMLAttributes<HTMLDivElement> {
39
41
  * </FormField>
40
42
  */
41
43
  const FormField = React.forwardRef<HTMLDivElement, FormFieldProps>(
42
- ({ className, label, description, error, required, htmlFor, children, ...props }, ref) => {
43
- const id = htmlFor || React.useId()
44
+ (
45
+ {
46
+ className,
47
+ label,
48
+ description,
49
+ error,
50
+ required,
51
+ htmlFor,
52
+ children,
53
+ ...props
54
+ },
55
+ ref,
56
+ ) => {
57
+ const id = htmlFor || React.useId();
44
58
 
45
- // Clone child to inject id and error state if it's a valid React element
46
- const childWithProps = React.isValidElement(children)
47
- ? React.cloneElement(children as React.ReactElement<any>, {
48
- id: id,
49
- error: !!error,
50
- "aria-describedby": error ? `${id}-error` : description ? `${id}-desc` : undefined,
51
- })
52
- : children
59
+ // Clone child to inject id and error state if it's a valid React element
60
+ const childWithProps = React.isValidElement(children)
61
+ ? React.cloneElement(children as React.ReactElement<any>, {
62
+ id: id,
63
+ error: !!error,
64
+ "aria-describedby": error
65
+ ? `${id}-error`
66
+ : description
67
+ ? `${id}-desc`
68
+ : undefined,
69
+ })
70
+ : children;
53
71
 
54
- return (
55
- <div
56
- ref={ref}
57
- className={cn("space-y-2", className)}
58
- {...props}
59
- >
60
- {label && (
61
- <Label htmlFor={id} required={required}>
62
- {label}
63
- </Label>
64
- )}
72
+ return (
73
+ <div ref={ref} className={cn("space-y-2", className)} {...props}>
74
+ {label && (
75
+ <Label htmlFor={id} required={required}>
76
+ {label}
77
+ </Label>
78
+ )}
65
79
 
66
- {childWithProps}
80
+ {childWithProps}
67
81
 
68
- {description && !error && (
69
- <p
70
- id={`${id}-desc`}
71
- className="text-[0.8rem] text-muted-foreground"
72
- >
73
- {description}
74
- </p>
75
- )}
82
+ {description && !error && (
83
+ <p id={`${id}-desc`} className="text-[0.8rem] text-muted-foreground">
84
+ {description}
85
+ </p>
86
+ )}
76
87
 
77
- {error && (
78
- <p
79
- id={`${id}-error`}
80
- className="text-[0.8rem] font-medium text-destructive"
81
- >
82
- {error}
83
- </p>
84
- )}
85
- </div>
86
- )
87
- }
88
- )
89
- FormField.displayName = "FormField"
88
+ {error && (
89
+ <p
90
+ id={`${id}-error`}
91
+ className="text-[0.8rem] font-medium text-destructive"
92
+ >
93
+ {error}
94
+ </p>
95
+ )}
96
+ </div>
97
+ );
98
+ },
99
+ );
100
+ FormField.displayName = "FormField";
90
101
 
91
- export { FormField }
102
+ export { FormField };
@@ -1,144 +1,152 @@
1
- import * as React from "react"
2
- import { cva, type VariantProps } from "class-variance-authority"
3
- import { cn } from "@/lib/utils"
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { cva, type VariantProps } from "class-variance-authority";
5
+ import { cn } from "@/lib/utils";
4
6
 
5
7
  const imageVariants = cva("", {
6
- variants: {
7
- rounded: {
8
- none: "rounded-none",
9
- sm: "rounded-sm",
10
- default: "rounded-md",
11
- md: "rounded-md",
12
- lg: "rounded-lg",
13
- xl: "rounded-xl",
14
- full: "rounded-full",
15
- },
16
- objectFit: {
17
- cover: "object-cover",
18
- contain: "object-contain",
19
- fill: "object-fill",
20
- none: "object-none",
21
- },
8
+ variants: {
9
+ rounded: {
10
+ none: "rounded-none",
11
+ sm: "rounded-sm",
12
+ default: "rounded-md",
13
+ md: "rounded-md",
14
+ lg: "rounded-lg",
15
+ xl: "rounded-xl",
16
+ full: "rounded-full",
22
17
  },
23
- defaultVariants: {
24
- rounded: "default",
25
- objectFit: "cover",
18
+ objectFit: {
19
+ cover: "object-cover",
20
+ contain: "object-contain",
21
+ fill: "object-fill",
22
+ none: "object-none",
26
23
  },
27
- })
24
+ },
25
+ defaultVariants: {
26
+ rounded: "default",
27
+ objectFit: "cover",
28
+ },
29
+ });
28
30
 
29
- type ImageVariants = VariantProps<typeof imageVariants>
31
+ type ImageVariants = VariantProps<typeof imageVariants>;
30
32
 
31
33
  interface ImageProps
32
- extends Omit<React.ImgHTMLAttributes<HTMLImageElement>, "onLoad" | "onError">,
34
+ extends
35
+ Omit<React.ImgHTMLAttributes<HTMLImageElement>, "onLoad" | "onError">,
33
36
  ImageVariants {
34
- /** Fallback content or URL when image fails to load */
35
- fallback?: React.ReactNode | string
36
- /** Show skeleton loading state */
37
- showSkeleton?: boolean
38
- /** Aspect ratio (width/height) */
39
- aspectRatio?: number
37
+ /** Fallback content or URL when image fails to load */
38
+ fallback?: React.ReactNode | string;
39
+ /** Show skeleton loading state */
40
+ showSkeleton?: boolean;
41
+ /** Aspect ratio (width/height) */
42
+ aspectRatio?: number;
40
43
  }
41
44
 
42
45
  /**
43
46
  * Enhanced Image with loading states and fallback
44
- *
47
+ *
45
48
  * @example
46
49
  * <Image src="/photo.jpg" alt="Photo" aspectRatio={16/9} />
47
50
  * <Image src="/avatar.jpg" alt="User" rounded="full" fallback="JD" />
48
51
  */
49
52
  const Image = React.forwardRef<HTMLImageElement, ImageProps>(
50
- (
51
- {
52
- className,
53
- src,
54
- alt,
55
- fallback,
56
- showSkeleton = true,
57
- aspectRatio,
58
- rounded,
59
- objectFit,
60
- style,
61
- ...props
62
- },
63
- ref
64
- ) => {
65
- const [status, setStatus] = React.useState<"loading" | "loaded" | "error">("loading")
53
+ (
54
+ {
55
+ className,
56
+ src,
57
+ alt,
58
+ fallback,
59
+ showSkeleton = true,
60
+ aspectRatio,
61
+ rounded,
62
+ objectFit,
63
+ style,
64
+ ...props
65
+ },
66
+ ref,
67
+ ) => {
68
+ const [status, setStatus] = React.useState<"loading" | "loaded" | "error">(
69
+ "loading",
70
+ );
66
71
 
67
- React.useEffect(() => {
68
- setStatus("loading")
69
- }, [src])
72
+ React.useEffect(() => {
73
+ setStatus("loading");
74
+ }, [src]);
70
75
 
71
- const containerStyle: React.CSSProperties = aspectRatio
72
- ? { paddingBottom: `${100 / aspectRatio}%`, ...(style || {}) }
73
- : (style || {})
76
+ const containerStyle: React.CSSProperties = aspectRatio
77
+ ? { paddingBottom: `${100 / aspectRatio}%`, ...(style || {}) }
78
+ : style || {};
74
79
 
75
- // Render fallback
76
- if (status === "error" && fallback) {
77
- if (typeof fallback === "string") {
78
- // If fallback is a string, check if it's a URL or initials
79
- if (fallback.startsWith("http") || fallback.startsWith("/")) {
80
- return (
81
- <img
82
- ref={ref}
83
- src={fallback}
84
- alt={alt}
85
- className={cn(imageVariants({ rounded, objectFit }), className)}
86
- style={style}
87
- {...props}
88
- />
89
- )
90
- }
91
- // Initials fallback
92
- return (
93
- <div
94
- className={cn(
95
- "flex items-center justify-center bg-muted text-muted-foreground font-medium",
96
- imageVariants({ rounded }),
97
- className
98
- )}
99
- style={containerStyle}
100
- >
101
- {fallback}
102
- </div>
103
- )
104
- }
105
- return <>{fallback}</>
80
+ // Render fallback
81
+ if (status === "error" && fallback) {
82
+ if (typeof fallback === "string") {
83
+ // If fallback is a string, check if it's a URL or initials
84
+ if (fallback.startsWith("http") || fallback.startsWith("/")) {
85
+ return (
86
+ <img
87
+ ref={ref}
88
+ src={fallback}
89
+ alt={alt}
90
+ className={cn(imageVariants({ rounded, objectFit }), className)}
91
+ style={style}
92
+ {...props}
93
+ />
94
+ );
106
95
  }
107
-
96
+ // Initials fallback
108
97
  return (
109
- <div
110
- className={cn("relative overflow-hidden", aspectRatio && "w-full")}
111
- style={aspectRatio ? { paddingBottom: `${100 / aspectRatio}%` } : undefined}
112
- >
113
- {/* Skeleton */}
114
- {status === "loading" && showSkeleton && (
115
- <div
116
- className={cn(
117
- "absolute inset-0 animate-pulse bg-muted",
118
- imageVariants({ rounded })
119
- )}
120
- />
121
- )}
122
-
123
- <img
124
- ref={ref}
125
- src={src}
126
- alt={alt}
127
- className={cn(
128
- imageVariants({ rounded, objectFit }),
129
- aspectRatio && "absolute inset-0 h-full w-full",
130
- status === "loading" && "opacity-0",
131
- status === "loaded" && "opacity-100 transition-opacity duration-300",
132
- className
133
- )}
134
- onLoad={() => setStatus("loaded")}
135
- onError={() => setStatus("error")}
136
- {...props}
137
- />
138
- </div>
139
- )
98
+ <div
99
+ className={cn(
100
+ "flex items-center justify-center bg-muted text-muted-foreground font-medium",
101
+ imageVariants({ rounded }),
102
+ className,
103
+ )}
104
+ style={containerStyle}
105
+ >
106
+ {fallback}
107
+ </div>
108
+ );
109
+ }
110
+ return <>{fallback}</>;
140
111
  }
141
- )
142
- Image.displayName = "Image"
143
112
 
144
- export { Image, imageVariants }
113
+ return (
114
+ <div
115
+ className={cn("relative overflow-hidden", aspectRatio && "w-full")}
116
+ style={
117
+ aspectRatio ? { paddingBottom: `${100 / aspectRatio}%` } : undefined
118
+ }
119
+ >
120
+ {/* Skeleton */}
121
+ {status === "loading" && showSkeleton && (
122
+ <div
123
+ className={cn(
124
+ "absolute inset-0 animate-pulse bg-muted",
125
+ imageVariants({ rounded }),
126
+ )}
127
+ />
128
+ )}
129
+
130
+ <img
131
+ ref={ref}
132
+ src={src}
133
+ alt={alt}
134
+ className={cn(
135
+ imageVariants({ rounded, objectFit }),
136
+ aspectRatio && "absolute inset-0 h-full w-full",
137
+ status === "loading" && "opacity-0",
138
+ status === "loaded" &&
139
+ "opacity-100 transition-opacity duration-300",
140
+ className,
141
+ )}
142
+ onLoad={() => setStatus("loaded")}
143
+ onError={() => setStatus("error")}
144
+ {...props}
145
+ />
146
+ </div>
147
+ );
148
+ },
149
+ );
150
+ Image.displayName = "Image";
151
+
152
+ export { Image, imageVariants };
@@ -1,82 +1,88 @@
1
- import * as React from "react"
2
- import { cn } from "@/lib/utils"
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { cn } from "@/lib/utils";
3
5
 
4
6
  const InputGroup = React.forwardRef<
5
- HTMLDivElement,
6
- React.HTMLAttributes<HTMLDivElement> & { error?: boolean }
7
+ HTMLDivElement,
8
+ React.HTMLAttributes<HTMLDivElement> & { error?: boolean }
7
9
  >(({ className, children, error, ...props }, ref) => {
8
- return (
9
- <div
10
- ref={ref}
11
- className={cn(
12
- "flex w-full items-center rounded-md",
13
- "focus-within:ring-1 focus-within:ring-ring",
14
- error && "focus-within:ring-destructive focus-within:border-destructive",
15
- // First child: rounded-l only, remove right border if not last
16
- "[&>*:first-child]:rounded-r-none",
17
- // Last child: rounded-r only, remove left border if not first
18
- "[&>*:last-child]:rounded-l-none",
19
- // Middle children: no rounding
20
- "[&>*:not(:first-child):not(:last-child)]:rounded-none",
21
- // Negative margin to merge borders
22
- "[&>*:not(:first-child)]:-ml-px",
23
- // Bring hovered element to front
24
- "[&>*:hover]:z-10",
25
- className
26
- )}
27
- {...props}
28
- >
29
- {React.Children.map(children, (child) => {
30
- if (!React.isValidElement(child)) return child
31
- const element = child as React.ReactElement<{ className?: string, error?: boolean }>
10
+ return (
11
+ <div
12
+ ref={ref}
13
+ className={cn(
14
+ "flex w-full items-center rounded-md",
15
+ "focus-within:ring-1 focus-within:ring-ring",
16
+ error &&
17
+ "focus-within:ring-destructive focus-within:border-destructive",
18
+ // First child: rounded-l only, remove right border if not last
19
+ "[&>*:first-child]:rounded-r-none",
20
+ // Last child: rounded-r only, remove left border if not first
21
+ "[&>*:last-child]:rounded-l-none",
22
+ // Middle children: no rounding
23
+ "[&>*:not(:first-child):not(:last-child)]:rounded-none",
24
+ // Negative margin to merge borders
25
+ "[&>*:not(:first-child)]:-ml-px",
26
+ // Bring hovered element to front
27
+ "[&>*:hover]:z-10",
28
+ className,
29
+ )}
30
+ {...props}
31
+ >
32
+ {React.Children.map(children, (child) => {
33
+ if (!React.isValidElement(child)) return child;
34
+ const element = child as React.ReactElement<{
35
+ className?: string;
36
+ error?: boolean;
37
+ }>;
32
38
 
33
- // Only pass error to custom components, not DOM elements
34
- const additionalProps: { className: string; error?: boolean } = {
35
- className: cn(
36
- element.props.className,
37
- "focus-visible:ring-0 focus-visible:ring-offset-0",
38
- error && "border-destructive focus-visible:ring-destructive"
39
- )
40
- }
39
+ // Only pass error to custom components, not DOM elements
40
+ const additionalProps: { className: string; error?: boolean } = {
41
+ className: cn(
42
+ element.props.className,
43
+ "focus-visible:ring-0 focus-visible:ring-offset-0",
44
+ error && "border-destructive focus-visible:ring-destructive",
45
+ ),
46
+ };
41
47
 
42
- if (typeof element.type !== "string") {
43
- additionalProps.error = error
44
- }
48
+ if (typeof element.type !== "string") {
49
+ additionalProps.error = error;
50
+ }
45
51
 
46
- return React.cloneElement(element, additionalProps)
47
- })}
48
- </div>
49
- )
50
- })
51
- InputGroup.displayName = "InputGroup"
52
+ return React.cloneElement(element, additionalProps);
53
+ })}
54
+ </div>
55
+ );
56
+ });
57
+ InputGroup.displayName = "InputGroup";
52
58
 
53
59
  const InputAddon = React.forwardRef<
54
- HTMLDivElement,
55
- React.HTMLAttributes<HTMLDivElement> & { error?: boolean }
60
+ HTMLDivElement,
61
+ React.HTMLAttributes<HTMLDivElement> & { error?: boolean }
56
62
  >(({ className, children, error, ...props }, ref) => {
57
- return (
58
- <div
59
- ref={ref}
60
- className={cn(
61
- "flex h-9 items-center justify-center rounded-md border border-input bg-muted px-3 text-sm text-muted-foreground shadow-sm",
62
- error && "border-destructive",
63
- className
64
- )}
65
- {...props}
66
- >
67
- {React.Children.map(children, (child: React.ReactNode) => {
68
- if (!React.isValidElement(child)) return child
69
- const element = child as React.ReactElement<{ className?: string }>
70
- return React.cloneElement(element, {
71
- className: cn(
72
- element.props.className,
73
- "focus-visible:ring-0 focus-visible:ring-offset-0"
74
- ),
75
- })
76
- })}
77
- </div>
78
- )
79
- })
80
- InputAddon.displayName = "InputAddon"
63
+ return (
64
+ <div
65
+ ref={ref}
66
+ className={cn(
67
+ "flex h-9 items-center justify-center rounded-md border border-input bg-muted px-3 text-sm text-muted-foreground shadow-sm",
68
+ error && "border-destructive",
69
+ className,
70
+ )}
71
+ {...props}
72
+ >
73
+ {React.Children.map(children, (child: React.ReactNode) => {
74
+ if (!React.isValidElement(child)) return child;
75
+ const element = child as React.ReactElement<{ className?: string }>;
76
+ return React.cloneElement(element, {
77
+ className: cn(
78
+ element.props.className,
79
+ "focus-visible:ring-0 focus-visible:ring-offset-0",
80
+ ),
81
+ });
82
+ })}
83
+ </div>
84
+ );
85
+ });
86
+ InputAddon.displayName = "InputAddon";
81
87
 
82
- export { InputGroup, InputAddon }
88
+ export { InputGroup, InputAddon };