veloria-ui 0.1.2
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/CHANGELOG.md +206 -0
- package/LICENSE +21 -0
- package/README.md +253 -0
- package/dist/cli/index.js +511 -0
- package/dist/index.d.mts +1317 -0
- package/dist/index.d.ts +1317 -0
- package/dist/index.js +5373 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +5130 -0
- package/dist/index.mjs.map +1 -0
- package/dist/provider.d.mts +15 -0
- package/dist/provider.d.ts +15 -0
- package/dist/provider.js +1197 -0
- package/dist/provider.js.map +1 -0
- package/dist/provider.mjs +1161 -0
- package/dist/provider.mjs.map +1 -0
- package/dist/tailwind.d.ts +25 -0
- package/dist/tailwind.js +129 -0
- package/package.json +138 -0
- package/src/cli/index.ts +303 -0
- package/src/cli/registry.ts +139 -0
- package/src/components/advanced-forms/index.tsx +975 -0
- package/src/components/basic/Button.tsx +135 -0
- package/src/components/basic/IconButton.tsx +69 -0
- package/src/components/basic/index.tsx +446 -0
- package/src/components/data-display/index.tsx +1158 -0
- package/src/components/feedback/index.tsx +1051 -0
- package/src/components/forms/index.tsx +476 -0
- package/src/components/layout/index.tsx +296 -0
- package/src/components/media/index.tsx +437 -0
- package/src/components/navigation/index.tsx +484 -0
- package/src/components/overlay/index.tsx +473 -0
- package/src/components/utility/index.tsx +566 -0
- package/src/hooks/index.ts +602 -0
- package/src/hooks/use-toast.tsx +74 -0
- package/src/index.ts +396 -0
- package/src/provider.tsx +54 -0
- package/src/styles/atlas.css +252 -0
- package/src/tailwind.ts +124 -0
- package/src/types/index.ts +95 -0
- package/src/utils/cn.ts +66 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Slot } from "@radix-ui/react-slot";
|
|
3
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
4
|
+
import { cn } from "../../utils/cn";
|
|
5
|
+
|
|
6
|
+
const buttonVariants = cva(
|
|
7
|
+
[
|
|
8
|
+
"atlas-button",
|
|
9
|
+
"inline-flex items-center justify-center gap-2",
|
|
10
|
+
"whitespace-nowrap rounded-md font-medium",
|
|
11
|
+
"ring-offset-background transition-all duration-150",
|
|
12
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
13
|
+
"disabled:pointer-events-none disabled:opacity-50",
|
|
14
|
+
"select-none",
|
|
15
|
+
],
|
|
16
|
+
{
|
|
17
|
+
variants: {
|
|
18
|
+
variant: {
|
|
19
|
+
solid:
|
|
20
|
+
"bg-primary text-primary-foreground shadow hover:bg-primary/90 active:scale-[0.98]",
|
|
21
|
+
outline:
|
|
22
|
+
"border border-input bg-transparent text-foreground hover:bg-accent hover:text-accent-foreground",
|
|
23
|
+
ghost:
|
|
24
|
+
"bg-transparent text-foreground hover:bg-accent hover:text-accent-foreground",
|
|
25
|
+
soft: "bg-primary/10 text-primary hover:bg-primary/20",
|
|
26
|
+
link: "text-primary underline-offset-4 hover:underline p-0 h-auto",
|
|
27
|
+
danger:
|
|
28
|
+
"bg-destructive text-destructive-foreground shadow hover:bg-destructive/90",
|
|
29
|
+
success:
|
|
30
|
+
"bg-success text-success-foreground shadow hover:bg-success/90",
|
|
31
|
+
},
|
|
32
|
+
size: {
|
|
33
|
+
xs: "h-6 px-2 text-xs rounded",
|
|
34
|
+
sm: "h-8 px-3 text-sm",
|
|
35
|
+
md: "h-9 px-4 text-sm",
|
|
36
|
+
lg: "h-10 px-6 text-base",
|
|
37
|
+
xl: "h-12 px-8 text-base",
|
|
38
|
+
icon: "h-9 w-9",
|
|
39
|
+
},
|
|
40
|
+
fullWidth: {
|
|
41
|
+
true: "w-full",
|
|
42
|
+
},
|
|
43
|
+
loading: {
|
|
44
|
+
true: "cursor-wait",
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
defaultVariants: {
|
|
48
|
+
variant: "solid",
|
|
49
|
+
size: "md",
|
|
50
|
+
},
|
|
51
|
+
}
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
export interface ButtonProps
|
|
55
|
+
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
56
|
+
VariantProps<typeof buttonVariants> {
|
|
57
|
+
/** Render as child element (Radix Slot pattern) */
|
|
58
|
+
asChild?: boolean;
|
|
59
|
+
/** Show loading spinner */
|
|
60
|
+
loading?: boolean;
|
|
61
|
+
/** Left icon slot */
|
|
62
|
+
leftIcon?: React.ReactNode;
|
|
63
|
+
/** Right icon slot */
|
|
64
|
+
rightIcon?: React.ReactNode;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
68
|
+
(
|
|
69
|
+
{
|
|
70
|
+
className,
|
|
71
|
+
variant,
|
|
72
|
+
size,
|
|
73
|
+
fullWidth,
|
|
74
|
+
loading,
|
|
75
|
+
asChild = false,
|
|
76
|
+
leftIcon,
|
|
77
|
+
rightIcon,
|
|
78
|
+
children,
|
|
79
|
+
disabled,
|
|
80
|
+
...props
|
|
81
|
+
},
|
|
82
|
+
ref
|
|
83
|
+
) => {
|
|
84
|
+
const Comp = asChild ? Slot : "button";
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<Comp
|
|
88
|
+
ref={ref}
|
|
89
|
+
className={cn(buttonVariants({ variant, size, fullWidth, loading, className }))}
|
|
90
|
+
disabled={disabled || loading === true}
|
|
91
|
+
aria-busy={loading}
|
|
92
|
+
{...props}
|
|
93
|
+
>
|
|
94
|
+
{loading && (
|
|
95
|
+
<svg
|
|
96
|
+
className="animate-spin -ml-0.5 h-4 w-4"
|
|
97
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
98
|
+
fill="none"
|
|
99
|
+
viewBox="0 0 24 24"
|
|
100
|
+
aria-hidden="true"
|
|
101
|
+
>
|
|
102
|
+
<circle
|
|
103
|
+
className="opacity-25"
|
|
104
|
+
cx="12"
|
|
105
|
+
cy="12"
|
|
106
|
+
r="10"
|
|
107
|
+
stroke="currentColor"
|
|
108
|
+
strokeWidth="4"
|
|
109
|
+
/>
|
|
110
|
+
<path
|
|
111
|
+
className="opacity-75"
|
|
112
|
+
fill="currentColor"
|
|
113
|
+
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
|
|
114
|
+
/>
|
|
115
|
+
</svg>
|
|
116
|
+
)}
|
|
117
|
+
{!loading && leftIcon && (
|
|
118
|
+
<span className="shrink-0" aria-hidden="true">
|
|
119
|
+
{leftIcon}
|
|
120
|
+
</span>
|
|
121
|
+
)}
|
|
122
|
+
{children}
|
|
123
|
+
{rightIcon && !loading && (
|
|
124
|
+
<span className="shrink-0" aria-hidden="true">
|
|
125
|
+
{rightIcon}
|
|
126
|
+
</span>
|
|
127
|
+
)}
|
|
128
|
+
</Comp>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
Button.displayName = "Button";
|
|
134
|
+
|
|
135
|
+
export { Button, buttonVariants };
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Slot } from "@radix-ui/react-slot";
|
|
3
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
4
|
+
import { cn } from "../../utils/cn";
|
|
5
|
+
|
|
6
|
+
const iconButtonVariants = cva(
|
|
7
|
+
[
|
|
8
|
+
"atlas-icon-button",
|
|
9
|
+
"inline-flex items-center justify-center rounded-md",
|
|
10
|
+
"transition-all duration-150 shrink-0",
|
|
11
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
12
|
+
"disabled:pointer-events-none disabled:opacity-50",
|
|
13
|
+
],
|
|
14
|
+
{
|
|
15
|
+
variants: {
|
|
16
|
+
variant: {
|
|
17
|
+
solid: "bg-primary text-primary-foreground hover:bg-primary/90 active:scale-[0.96]",
|
|
18
|
+
outline: "border border-input bg-transparent hover:bg-accent hover:text-accent-foreground",
|
|
19
|
+
ghost: "bg-transparent hover:bg-accent hover:text-accent-foreground",
|
|
20
|
+
soft: "bg-primary/10 text-primary hover:bg-primary/20",
|
|
21
|
+
danger: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
|
22
|
+
},
|
|
23
|
+
size: {
|
|
24
|
+
xs: "h-6 w-6 [&>svg]:h-3 [&>svg]:w-3",
|
|
25
|
+
sm: "h-7 w-7 [&>svg]:h-3.5 [&>svg]:w-3.5",
|
|
26
|
+
md: "h-9 w-9 [&>svg]:h-4 [&>svg]:w-4",
|
|
27
|
+
lg: "h-10 w-10 [&>svg]:h-5 [&>svg]:w-5",
|
|
28
|
+
xl: "h-12 w-12 [&>svg]:h-6 [&>svg]:w-6",
|
|
29
|
+
},
|
|
30
|
+
shape: {
|
|
31
|
+
square: "rounded-md",
|
|
32
|
+
circle: "rounded-full",
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
defaultVariants: {
|
|
36
|
+
variant: "ghost",
|
|
37
|
+
size: "md",
|
|
38
|
+
shape: "square",
|
|
39
|
+
},
|
|
40
|
+
}
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
export interface IconButtonProps
|
|
44
|
+
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
45
|
+
VariantProps<typeof iconButtonVariants> {
|
|
46
|
+
/** Accessible label — required for screen readers */
|
|
47
|
+
"aria-label": string;
|
|
48
|
+
asChild?: boolean;
|
|
49
|
+
icon?: React.ReactNode;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const IconButton = React.forwardRef<HTMLButtonElement, IconButtonProps>(
|
|
53
|
+
({ className, variant, size, shape, asChild = false, icon, children, ...props }, ref) => {
|
|
54
|
+
const Comp = asChild ? Slot : "button";
|
|
55
|
+
return (
|
|
56
|
+
<Comp
|
|
57
|
+
ref={ref}
|
|
58
|
+
className={cn(iconButtonVariants({ variant, size, shape, className }))}
|
|
59
|
+
{...props}
|
|
60
|
+
>
|
|
61
|
+
{icon ?? children}
|
|
62
|
+
</Comp>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
IconButton.displayName = "IconButton";
|
|
68
|
+
|
|
69
|
+
export { IconButton, iconButtonVariants };
|
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
// ─── Link ─────────────────────────────────────────────────────────────────
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Slot } from "@radix-ui/react-slot";
|
|
5
|
+
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
|
6
|
+
import * as AvatarPrimitive from "@radix-ui/react-avatar";
|
|
7
|
+
import * as SeparatorPrimitive from "@radix-ui/react-separator";
|
|
8
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
9
|
+
import { cn } from "../../utils/cn";
|
|
10
|
+
|
|
11
|
+
// ─── Link ─────────────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
export interface LinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
|
|
14
|
+
asChild?: boolean;
|
|
15
|
+
external?: boolean;
|
|
16
|
+
underline?: "always" | "hover" | "none";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const Link = React.forwardRef<HTMLAnchorElement, LinkProps>(
|
|
20
|
+
({ className, asChild, external, underline = "hover", children, ...props }, ref) => {
|
|
21
|
+
const Comp = asChild ? Slot : "a";
|
|
22
|
+
const externalProps = external
|
|
23
|
+
? { target: "_blank", rel: "noopener noreferrer" }
|
|
24
|
+
: {};
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<Comp
|
|
28
|
+
ref={ref}
|
|
29
|
+
className={cn(
|
|
30
|
+
"atlas-link text-primary transition-colors",
|
|
31
|
+
underline === "always" && "underline underline-offset-4",
|
|
32
|
+
underline === "hover" && "hover:underline underline-offset-4",
|
|
33
|
+
underline === "none" && "no-underline",
|
|
34
|
+
className
|
|
35
|
+
)}
|
|
36
|
+
{...externalProps}
|
|
37
|
+
{...props}
|
|
38
|
+
>
|
|
39
|
+
{children}
|
|
40
|
+
{external && (
|
|
41
|
+
<svg
|
|
42
|
+
className="inline-block ml-0.5 h-3 w-3 align-super"
|
|
43
|
+
fill="none"
|
|
44
|
+
stroke="currentColor"
|
|
45
|
+
viewBox="0 0 24 24"
|
|
46
|
+
aria-hidden="true"
|
|
47
|
+
>
|
|
48
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
|
49
|
+
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
|
|
50
|
+
/>
|
|
51
|
+
</svg>
|
|
52
|
+
)}
|
|
53
|
+
</Comp>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
);
|
|
57
|
+
Link.displayName = "Link";
|
|
58
|
+
|
|
59
|
+
// ─── Badge ─────────────────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
const badgeVariants = cva(
|
|
62
|
+
"atlas-badge inline-flex items-center gap-1 rounded-full font-medium transition-colors",
|
|
63
|
+
{
|
|
64
|
+
variants: {
|
|
65
|
+
variant: {
|
|
66
|
+
solid: "bg-primary text-primary-foreground",
|
|
67
|
+
outline: "border border-current bg-transparent",
|
|
68
|
+
soft: "bg-primary/10 text-primary",
|
|
69
|
+
neutral: "bg-muted text-muted-foreground",
|
|
70
|
+
},
|
|
71
|
+
size: {
|
|
72
|
+
sm: "px-1.5 py-0.5 text-[10px]",
|
|
73
|
+
md: "px-2 py-0.5 text-xs",
|
|
74
|
+
lg: "px-2.5 py-1 text-sm",
|
|
75
|
+
},
|
|
76
|
+
color: {
|
|
77
|
+
primary: "",
|
|
78
|
+
success: "",
|
|
79
|
+
warning: "",
|
|
80
|
+
danger: "",
|
|
81
|
+
info: "",
|
|
82
|
+
neutral: "",
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
compoundVariants: [
|
|
86
|
+
{ variant: "solid", color: "success", className: "bg-success text-success-foreground" },
|
|
87
|
+
{ variant: "solid", color: "warning", className: "bg-warning text-warning-foreground" },
|
|
88
|
+
{ variant: "solid", color: "danger", className: "bg-destructive text-destructive-foreground" },
|
|
89
|
+
{ variant: "solid", color: "info", className: "bg-info text-info-foreground" },
|
|
90
|
+
{ variant: "soft", color: "success", className: "bg-success/10 text-success" },
|
|
91
|
+
{ variant: "soft", color: "warning", className: "bg-warning/10 text-warning" },
|
|
92
|
+
{ variant: "soft", color: "danger", className: "bg-destructive/10 text-destructive" },
|
|
93
|
+
{ variant: "soft", color: "info", className: "bg-info/10 text-info" },
|
|
94
|
+
],
|
|
95
|
+
defaultVariants: {
|
|
96
|
+
variant: "soft",
|
|
97
|
+
size: "md",
|
|
98
|
+
color: "primary",
|
|
99
|
+
},
|
|
100
|
+
}
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
export interface BadgeProps
|
|
104
|
+
extends Omit<React.HTMLAttributes<HTMLSpanElement>, "color">,
|
|
105
|
+
VariantProps<typeof badgeVariants> {
|
|
106
|
+
dot?: boolean;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const Badge = React.forwardRef<HTMLSpanElement, BadgeProps>(
|
|
110
|
+
({ className, variant, size, color, dot, children, ...props }, ref) => (
|
|
111
|
+
<span ref={ref} className={cn(badgeVariants({ variant, size, color, className }))} {...props}>
|
|
112
|
+
{dot && (
|
|
113
|
+
<span className="h-1.5 w-1.5 rounded-full bg-current" aria-hidden="true" />
|
|
114
|
+
)}
|
|
115
|
+
{children}
|
|
116
|
+
</span>
|
|
117
|
+
)
|
|
118
|
+
);
|
|
119
|
+
Badge.displayName = "Badge";
|
|
120
|
+
|
|
121
|
+
// ─── Avatar ────────────────────────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
const avatarSizes = {
|
|
124
|
+
xs: "h-6 w-6 text-[10px]",
|
|
125
|
+
sm: "h-8 w-8 text-xs",
|
|
126
|
+
md: "h-10 w-10 text-sm",
|
|
127
|
+
lg: "h-12 w-12 text-base",
|
|
128
|
+
xl: "h-16 w-16 text-lg",
|
|
129
|
+
"2xl": "h-20 w-20 text-xl",
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
export interface AvatarProps
|
|
133
|
+
extends React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root> {
|
|
134
|
+
src?: string;
|
|
135
|
+
alt?: string;
|
|
136
|
+
fallback?: string;
|
|
137
|
+
size?: keyof typeof avatarSizes;
|
|
138
|
+
shape?: "circle" | "square";
|
|
139
|
+
status?: "online" | "offline" | "busy" | "away";
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const Avatar = React.forwardRef<
|
|
143
|
+
React.ElementRef<typeof AvatarPrimitive.Root>,
|
|
144
|
+
AvatarProps
|
|
145
|
+
>(({ className, src, alt, fallback, size = "md", shape = "circle", status, ...props }, ref) => (
|
|
146
|
+
<div className="atlas-avatar relative inline-flex shrink-0">
|
|
147
|
+
<AvatarPrimitive.Root
|
|
148
|
+
ref={ref}
|
|
149
|
+
className={cn(
|
|
150
|
+
"relative flex shrink-0 overflow-hidden",
|
|
151
|
+
avatarSizes[size],
|
|
152
|
+
shape === "circle" ? "rounded-full" : "rounded-md",
|
|
153
|
+
className
|
|
154
|
+
)}
|
|
155
|
+
{...props}
|
|
156
|
+
>
|
|
157
|
+
<AvatarPrimitive.Image
|
|
158
|
+
src={src}
|
|
159
|
+
alt={alt}
|
|
160
|
+
className="aspect-square h-full w-full object-cover"
|
|
161
|
+
/>
|
|
162
|
+
<AvatarPrimitive.Fallback
|
|
163
|
+
className={cn(
|
|
164
|
+
"flex h-full w-full items-center justify-center",
|
|
165
|
+
"bg-muted font-medium text-muted-foreground uppercase"
|
|
166
|
+
)}
|
|
167
|
+
>
|
|
168
|
+
{fallback ?? (alt ? alt.slice(0, 2) : "?")}
|
|
169
|
+
</AvatarPrimitive.Fallback>
|
|
170
|
+
</AvatarPrimitive.Root>
|
|
171
|
+
{status && (
|
|
172
|
+
<span
|
|
173
|
+
aria-label={`Status: ${status}`}
|
|
174
|
+
className={cn(
|
|
175
|
+
"absolute bottom-0 right-0 block rounded-full ring-2 ring-background",
|
|
176
|
+
size === "xs" || size === "sm" ? "h-2 w-2" : "h-2.5 w-2.5",
|
|
177
|
+
status === "online" && "bg-success",
|
|
178
|
+
status === "offline" && "bg-muted-foreground",
|
|
179
|
+
status === "busy" && "bg-destructive",
|
|
180
|
+
status === "away" && "bg-warning"
|
|
181
|
+
)}
|
|
182
|
+
/>
|
|
183
|
+
)}
|
|
184
|
+
</div>
|
|
185
|
+
));
|
|
186
|
+
Avatar.displayName = "Avatar";
|
|
187
|
+
|
|
188
|
+
// ─── AvatarGroup ──────────────────────────────────────────────────────────
|
|
189
|
+
|
|
190
|
+
export interface AvatarGroupProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "size"> {
|
|
191
|
+
max?: number;
|
|
192
|
+
size?: AvatarProps["size"];
|
|
193
|
+
spacing?: "tight" | "normal" | "loose";
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const AvatarGroup = React.forwardRef<HTMLDivElement, AvatarGroupProps>(
|
|
197
|
+
({ className, children, max, size = "md", spacing = "tight", ...props }, ref) => {
|
|
198
|
+
const validChildren = React.Children.toArray(children).filter(React.isValidElement);
|
|
199
|
+
const visible = max ? validChildren.slice(0, max) : validChildren;
|
|
200
|
+
const overflow = max ? validChildren.length - max : 0;
|
|
201
|
+
|
|
202
|
+
const spacingMap = { tight: "-space-x-2", normal: "-space-x-1", loose: "space-x-1" };
|
|
203
|
+
|
|
204
|
+
return (
|
|
205
|
+
<div
|
|
206
|
+
ref={ref}
|
|
207
|
+
className={cn("atlas-avatar-group flex items-center", spacingMap[spacing], className)}
|
|
208
|
+
{...props}
|
|
209
|
+
>
|
|
210
|
+
{visible.map((child, i) =>
|
|
211
|
+
React.cloneElement(child as React.ReactElement<AvatarProps>, {
|
|
212
|
+
key: i,
|
|
213
|
+
size,
|
|
214
|
+
className: cn(
|
|
215
|
+
"ring-2 ring-background",
|
|
216
|
+
(child as React.ReactElement<AvatarProps>).props.className
|
|
217
|
+
),
|
|
218
|
+
})
|
|
219
|
+
)}
|
|
220
|
+
{overflow > 0 && (
|
|
221
|
+
<span
|
|
222
|
+
className={cn(
|
|
223
|
+
"atlas-avatar relative flex shrink-0 items-center justify-center rounded-full",
|
|
224
|
+
"bg-muted text-muted-foreground font-medium ring-2 ring-background",
|
|
225
|
+
avatarSizes[size]
|
|
226
|
+
)}
|
|
227
|
+
aria-label={`${overflow} more`}
|
|
228
|
+
>
|
|
229
|
+
+{overflow}
|
|
230
|
+
</span>
|
|
231
|
+
)}
|
|
232
|
+
</div>
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
);
|
|
236
|
+
AvatarGroup.displayName = "AvatarGroup";
|
|
237
|
+
|
|
238
|
+
// ─── Divider ──────────────────────────────────────────────────────────────
|
|
239
|
+
|
|
240
|
+
export interface DividerProps
|
|
241
|
+
extends React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root> {
|
|
242
|
+
label?: React.ReactNode;
|
|
243
|
+
labelPosition?: "left" | "center" | "right";
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const Divider = React.forwardRef<
|
|
247
|
+
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
|
248
|
+
DividerProps
|
|
249
|
+
>(
|
|
250
|
+
({ className, orientation = "horizontal", label, labelPosition = "center", ...props }, ref) => {
|
|
251
|
+
if (!label) {
|
|
252
|
+
return (
|
|
253
|
+
<SeparatorPrimitive.Root
|
|
254
|
+
ref={ref}
|
|
255
|
+
orientation={orientation}
|
|
256
|
+
className={cn(
|
|
257
|
+
"atlas-divider shrink-0 bg-border",
|
|
258
|
+
orientation === "horizontal" ? "h-px w-full" : "h-full w-px",
|
|
259
|
+
className
|
|
260
|
+
)}
|
|
261
|
+
{...props}
|
|
262
|
+
/>
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return (
|
|
267
|
+
<div
|
|
268
|
+
className={cn(
|
|
269
|
+
"atlas-divider relative flex items-center gap-3 w-full",
|
|
270
|
+
className
|
|
271
|
+
)}
|
|
272
|
+
role="separator"
|
|
273
|
+
>
|
|
274
|
+
{labelPosition !== "left" && <span className="flex-1 bg-border h-px" />}
|
|
275
|
+
<span className="text-xs text-muted-foreground whitespace-nowrap">{label}</span>
|
|
276
|
+
{labelPosition !== "right" && <span className="flex-1 bg-border h-px" />}
|
|
277
|
+
</div>
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
);
|
|
281
|
+
Divider.displayName = "Divider";
|
|
282
|
+
|
|
283
|
+
// ─── Tag ──────────────────────────────────────────────────────────────────
|
|
284
|
+
|
|
285
|
+
export interface TagProps extends Omit<React.HTMLAttributes<HTMLSpanElement>, "color" | "size"> {
|
|
286
|
+
closable?: boolean;
|
|
287
|
+
onClose?: () => void;
|
|
288
|
+
icon?: React.ReactNode;
|
|
289
|
+
size?: "sm" | "md" | "lg";
|
|
290
|
+
variant?: "solid" | "outline" | "soft";
|
|
291
|
+
color?: "primary" | "success" | "warning" | "danger" | "neutral";
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const Tag = React.forwardRef<HTMLSpanElement, TagProps>(
|
|
295
|
+
({ className, closable, onClose, icon, size = "md", variant = "soft", color = "neutral", children, ...props }, ref) => (
|
|
296
|
+
<span
|
|
297
|
+
ref={ref}
|
|
298
|
+
className={cn(
|
|
299
|
+
"atlas-tag inline-flex items-center gap-1 rounded font-medium",
|
|
300
|
+
size === "sm" && "px-1.5 py-0.5 text-[10px]",
|
|
301
|
+
size === "md" && "px-2 py-1 text-xs",
|
|
302
|
+
size === "lg" && "px-3 py-1.5 text-sm",
|
|
303
|
+
variant === "soft" && color === "neutral" && "bg-muted text-muted-foreground",
|
|
304
|
+
variant === "soft" && color === "primary" && "bg-primary/10 text-primary",
|
|
305
|
+
variant === "soft" && color === "success" && "bg-success/10 text-success",
|
|
306
|
+
variant === "soft" && color === "warning" && "bg-warning/10 text-warning",
|
|
307
|
+
variant === "soft" && color === "danger" && "bg-destructive/10 text-destructive",
|
|
308
|
+
variant === "outline" && "border border-current bg-transparent",
|
|
309
|
+
variant === "solid" && color === "neutral" && "bg-muted-foreground text-background",
|
|
310
|
+
className
|
|
311
|
+
)}
|
|
312
|
+
{...props}
|
|
313
|
+
>
|
|
314
|
+
{icon && <span className="shrink-0" aria-hidden="true">{icon}</span>}
|
|
315
|
+
{children}
|
|
316
|
+
{closable && (
|
|
317
|
+
<button
|
|
318
|
+
type="button"
|
|
319
|
+
onClick={onClose}
|
|
320
|
+
className="ml-0.5 rounded-full hover:bg-black/10 dark:hover:bg-white/10 p-0.5 transition-colors"
|
|
321
|
+
aria-label="Remove tag"
|
|
322
|
+
>
|
|
323
|
+
<svg className="h-2.5 w-2.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
324
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M6 18L18 6M6 6l12 12" />
|
|
325
|
+
</svg>
|
|
326
|
+
</button>
|
|
327
|
+
)}
|
|
328
|
+
</span>
|
|
329
|
+
)
|
|
330
|
+
);
|
|
331
|
+
Tag.displayName = "Tag";
|
|
332
|
+
|
|
333
|
+
// ─── Chip ──────────────────────────────────────────────────────────────────
|
|
334
|
+
|
|
335
|
+
export interface ChipProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
336
|
+
selected?: boolean;
|
|
337
|
+
avatar?: React.ReactNode;
|
|
338
|
+
icon?: React.ReactNode;
|
|
339
|
+
closable?: boolean;
|
|
340
|
+
onClose?: (e: React.MouseEvent) => void;
|
|
341
|
+
size?: "sm" | "md" | "lg";
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const Chip = React.forwardRef<HTMLButtonElement, ChipProps>(
|
|
345
|
+
({ className, selected, avatar, icon, closable, onClose, size = "md", children, ...props }, ref) => (
|
|
346
|
+
<button
|
|
347
|
+
ref={ref}
|
|
348
|
+
type="button"
|
|
349
|
+
className={cn(
|
|
350
|
+
"atlas-chip inline-flex items-center gap-1.5 rounded-full font-medium",
|
|
351
|
+
"border transition-all duration-150 cursor-pointer",
|
|
352
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
353
|
+
"disabled:pointer-events-none disabled:opacity-50",
|
|
354
|
+
size === "sm" && "h-6 px-2 text-xs",
|
|
355
|
+
size === "md" && "h-8 px-3 text-sm",
|
|
356
|
+
size === "lg" && "h-9 px-4 text-sm",
|
|
357
|
+
selected
|
|
358
|
+
? "bg-primary text-primary-foreground border-primary"
|
|
359
|
+
: "bg-background text-foreground border-border hover:bg-accent",
|
|
360
|
+
className
|
|
361
|
+
)}
|
|
362
|
+
aria-pressed={selected}
|
|
363
|
+
{...props}
|
|
364
|
+
>
|
|
365
|
+
{avatar && <span className="shrink-0 -ml-0.5">{avatar}</span>}
|
|
366
|
+
{icon && !avatar && <span className="shrink-0 [&>svg]:h-3.5 [&>svg]:w-3.5" aria-hidden="true">{icon}</span>}
|
|
367
|
+
{children}
|
|
368
|
+
{closable && (
|
|
369
|
+
<span
|
|
370
|
+
role="button"
|
|
371
|
+
tabIndex={0}
|
|
372
|
+
onClick={(e) => { e.stopPropagation(); onClose?.(e); }}
|
|
373
|
+
className="shrink-0 -mr-0.5 rounded-full hover:bg-black/10 dark:hover:bg-white/10 p-0.5"
|
|
374
|
+
aria-label="Remove"
|
|
375
|
+
>
|
|
376
|
+
<svg className="h-3 w-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
377
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M6 18L18 6M6 6l12 12" />
|
|
378
|
+
</svg>
|
|
379
|
+
</span>
|
|
380
|
+
)}
|
|
381
|
+
</button>
|
|
382
|
+
)
|
|
383
|
+
);
|
|
384
|
+
Chip.displayName = "Chip";
|
|
385
|
+
|
|
386
|
+
// ─── Tooltip ──────────────────────────────────────────────────────────────
|
|
387
|
+
|
|
388
|
+
const TooltipProvider = TooltipPrimitive.Provider;
|
|
389
|
+
|
|
390
|
+
const TooltipRoot = TooltipPrimitive.Root;
|
|
391
|
+
|
|
392
|
+
const TooltipTrigger = TooltipPrimitive.Trigger;
|
|
393
|
+
|
|
394
|
+
const TooltipContent = React.forwardRef<
|
|
395
|
+
React.ElementRef<typeof TooltipPrimitive.Content>,
|
|
396
|
+
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
|
397
|
+
>(({ className, sideOffset = 4, ...props }, ref) => (
|
|
398
|
+
<TooltipPrimitive.Portal>
|
|
399
|
+
<TooltipPrimitive.Content
|
|
400
|
+
ref={ref}
|
|
401
|
+
sideOffset={sideOffset}
|
|
402
|
+
className={cn(
|
|
403
|
+
"atlas-tooltip z-50 overflow-hidden rounded-md",
|
|
404
|
+
"bg-foreground px-3 py-1.5 text-xs text-background shadow-md",
|
|
405
|
+
"animate-in fade-in-0 zoom-in-95",
|
|
406
|
+
"data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
|
|
407
|
+
"data-[side=bottom]:slide-in-from-top-2",
|
|
408
|
+
"data-[side=left]:slide-in-from-right-2",
|
|
409
|
+
"data-[side=right]:slide-in-from-left-2",
|
|
410
|
+
"data-[side=top]:slide-in-from-bottom-2",
|
|
411
|
+
className
|
|
412
|
+
)}
|
|
413
|
+
{...props}
|
|
414
|
+
/>
|
|
415
|
+
</TooltipPrimitive.Portal>
|
|
416
|
+
));
|
|
417
|
+
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
|
|
418
|
+
|
|
419
|
+
export interface TooltipProps {
|
|
420
|
+
content: React.ReactNode;
|
|
421
|
+
children: React.ReactNode;
|
|
422
|
+
side?: "top" | "right" | "bottom" | "left";
|
|
423
|
+
delayDuration?: number;
|
|
424
|
+
className?: string;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const Tooltip = ({ content, children, side = "top", delayDuration = 300, className }: TooltipProps) => (
|
|
428
|
+
<TooltipProvider>
|
|
429
|
+
<TooltipRoot delayDuration={delayDuration}>
|
|
430
|
+
<TooltipTrigger asChild>{children}</TooltipTrigger>
|
|
431
|
+
<TooltipContent side={side} className={className}>{content}</TooltipContent>
|
|
432
|
+
</TooltipRoot>
|
|
433
|
+
</TooltipProvider>
|
|
434
|
+
);
|
|
435
|
+
Tooltip.displayName = "Tooltip";
|
|
436
|
+
|
|
437
|
+
export {
|
|
438
|
+
Link,
|
|
439
|
+
Badge, badgeVariants,
|
|
440
|
+
Avatar,
|
|
441
|
+
AvatarGroup,
|
|
442
|
+
Divider,
|
|
443
|
+
Tag,
|
|
444
|
+
Chip,
|
|
445
|
+
Tooltip, TooltipProvider, TooltipRoot, TooltipTrigger, TooltipContent,
|
|
446
|
+
};
|