@payglocal_ui/flux-ui 0.1.0
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 +72 -0
- package/src/accordion.tsx +68 -0
- package/src/alert.tsx +107 -0
- package/src/avatar-group.tsx +96 -0
- package/src/avatar-tag.tsx +136 -0
- package/src/avatar.tsx +39 -0
- package/src/badge.tsx +98 -0
- package/src/blanket.tsx +61 -0
- package/src/breadcrumb.tsx +119 -0
- package/src/button-group.tsx +218 -0
- package/src/button.tsx +83 -0
- package/src/calendar.tsx +227 -0
- package/src/callout.tsx +68 -0
- package/src/card.tsx +103 -0
- package/src/chart-templates.tsx +587 -0
- package/src/chart.tsx +379 -0
- package/src/checkbox-select.tsx +239 -0
- package/src/checkbox.tsx +54 -0
- package/src/code.tsx +154 -0
- package/src/command.tsx +77 -0
- package/src/country-select.tsx +242 -0
- package/src/currency-amount-input.tsx +72 -0
- package/src/data-table.tsx +378 -0
- package/src/date-picker.tsx +317 -0
- package/src/dialog.tsx +81 -0
- package/src/drawer.tsx +91 -0
- package/src/dropdown-menu.tsx +174 -0
- package/src/empty-state.tsx +32 -0
- package/src/field.tsx +243 -0
- package/src/flag.tsx +265 -0
- package/src/form.tsx +168 -0
- package/src/grid-flex.tsx +241 -0
- package/src/heading.tsx +202 -0
- package/src/icon-button.tsx +93 -0
- package/src/index.ts +332 -0
- package/src/inline-dialog.tsx +153 -0
- package/src/inline-edit.tsx +212 -0
- package/src/input-group.tsx +151 -0
- package/src/input.tsx +28 -0
- package/src/label.tsx +21 -0
- package/src/layout.tsx +119 -0
- package/src/link.tsx +80 -0
- package/src/lozenge.tsx +61 -0
- package/src/menu.tsx +146 -0
- package/src/otp-input.tsx +117 -0
- package/src/page-header.tsx +28 -0
- package/src/pagination.tsx +185 -0
- package/src/password-input.tsx +34 -0
- package/src/popover.tsx +31 -0
- package/src/progress-indicator.tsx +94 -0
- package/src/progress.tsx +95 -0
- package/src/radio-group.tsx +46 -0
- package/src/responsive.tsx +276 -0
- package/src/scroll-area.tsx +39 -0
- package/src/section-message.tsx +119 -0
- package/src/select.tsx +144 -0
- package/src/separator.tsx +26 -0
- package/src/side-nav.tsx +264 -0
- package/src/skeleton.tsx +74 -0
- package/src/slider.tsx +25 -0
- package/src/sonner.tsx +32 -0
- package/src/spinner.tsx +54 -0
- package/src/spotlight.tsx +141 -0
- package/src/status-badge.tsx +86 -0
- package/src/switch.tsx +70 -0
- package/src/tabs.tsx +57 -0
- package/src/tag.tsx +52 -0
- package/src/textarea.tsx +25 -0
- package/src/time-picker.tsx +443 -0
- package/src/tooltip.tsx +29 -0
- package/src/utils.ts +6 -0
- package/src/visually-hidden.tsx +25 -0
package/package.json
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@payglocal_ui/flux-ui",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Flux UI primitives — inputs, fields, dialog, data table, charts, calendar, and more (Tailwind v4 + Radix).",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"main": "./src/index.ts",
|
|
9
|
+
"types": "./src/index.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./src/index.ts",
|
|
13
|
+
"default": "./src/index.ts"
|
|
14
|
+
},
|
|
15
|
+
"./package.json": "./package.json"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"src",
|
|
19
|
+
"README.md"
|
|
20
|
+
],
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"react",
|
|
26
|
+
"components",
|
|
27
|
+
"tailwind",
|
|
28
|
+
"radix-ui",
|
|
29
|
+
"design-system",
|
|
30
|
+
"flux"
|
|
31
|
+
],
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
34
|
+
"react-dom": "^18.0.0 || ^19.0.0"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@radix-ui/react-accordion": "^1.2.13",
|
|
38
|
+
"@radix-ui/react-avatar": "^1.1.11",
|
|
39
|
+
"@radix-ui/react-checkbox": "^1.3.4",
|
|
40
|
+
"@radix-ui/react-dialog": "^1.1.15",
|
|
41
|
+
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
|
42
|
+
"@radix-ui/react-label": "^2.1.8",
|
|
43
|
+
"@radix-ui/react-popover": "^1.1.15",
|
|
44
|
+
"@radix-ui/react-progress": "^1.1.9",
|
|
45
|
+
"@radix-ui/react-radio-group": "^1.4.0",
|
|
46
|
+
"@radix-ui/react-scroll-area": "^1.2.10",
|
|
47
|
+
"@radix-ui/react-select": "^2.2.6",
|
|
48
|
+
"@radix-ui/react-separator": "^1.1.8",
|
|
49
|
+
"@radix-ui/react-slider": "^1.3.5",
|
|
50
|
+
"@radix-ui/react-switch": "^1.3.0",
|
|
51
|
+
"@radix-ui/react-tabs": "^1.1.13",
|
|
52
|
+
"@radix-ui/react-tooltip": "^1.2.8",
|
|
53
|
+
"class-variance-authority": "^0.7.1",
|
|
54
|
+
"clsx": "^2.1.1",
|
|
55
|
+
"date-fns": "^4.1.0",
|
|
56
|
+
"framer-motion": "^12.37.0",
|
|
57
|
+
"lucide-react": "^0.577.0",
|
|
58
|
+
"next-themes": "^0.4.6",
|
|
59
|
+
"react-day-picker": "^9.14.0",
|
|
60
|
+
"recharts": "^3.8.0",
|
|
61
|
+
"sonner": "^2.0.7",
|
|
62
|
+
"tailwind-merge": "^3.5.0"
|
|
63
|
+
},
|
|
64
|
+
"devDependencies": {
|
|
65
|
+
"@types/react": "^19",
|
|
66
|
+
"@types/react-dom": "^19",
|
|
67
|
+
"typescript": "^5"
|
|
68
|
+
},
|
|
69
|
+
"scripts": {
|
|
70
|
+
"check-types": "tsc --noEmit"
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import * as AccordionPrimitive from "@radix-ui/react-accordion";
|
|
5
|
+
import { ChevronDown } from "lucide-react";
|
|
6
|
+
import { cn } from "./utils";
|
|
7
|
+
|
|
8
|
+
const Accordion = AccordionPrimitive.Root;
|
|
9
|
+
|
|
10
|
+
const AccordionItem = React.forwardRef<
|
|
11
|
+
React.ElementRef<typeof AccordionPrimitive.Item>,
|
|
12
|
+
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
|
13
|
+
>(({ className, ...props }, ref) => (
|
|
14
|
+
<AccordionPrimitive.Item
|
|
15
|
+
ref={ref}
|
|
16
|
+
className={cn("border-b border-border last:border-b-0", className)}
|
|
17
|
+
{...props}
|
|
18
|
+
/>
|
|
19
|
+
));
|
|
20
|
+
AccordionItem.displayName = "AccordionItem";
|
|
21
|
+
|
|
22
|
+
const AccordionTrigger = React.forwardRef<
|
|
23
|
+
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
|
24
|
+
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
|
25
|
+
>(({ className, children, ...props }, ref) => (
|
|
26
|
+
<AccordionPrimitive.Header className="flex">
|
|
27
|
+
<AccordionPrimitive.Trigger
|
|
28
|
+
ref={ref}
|
|
29
|
+
className={cn(
|
|
30
|
+
"flex w-full items-center justify-between py-4 text-sm font-medium text-foreground",
|
|
31
|
+
"hover:text-primary transition-colors duration-pg-fast ease-pg-standard",
|
|
32
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/35",
|
|
33
|
+
"[&[data-state=open]>svg]:rotate-180",
|
|
34
|
+
className
|
|
35
|
+
)}
|
|
36
|
+
{...props}
|
|
37
|
+
>
|
|
38
|
+
{children}
|
|
39
|
+
<ChevronDown className="h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-pg-fast ease-pg-standard" />
|
|
40
|
+
</AccordionPrimitive.Trigger>
|
|
41
|
+
</AccordionPrimitive.Header>
|
|
42
|
+
));
|
|
43
|
+
AccordionTrigger.displayName = "AccordionTrigger";
|
|
44
|
+
|
|
45
|
+
const AccordionContent = React.forwardRef<
|
|
46
|
+
React.ElementRef<typeof AccordionPrimitive.Content>,
|
|
47
|
+
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
|
48
|
+
>(({ className, children, ...props }, ref) => (
|
|
49
|
+
<AccordionPrimitive.Content
|
|
50
|
+
ref={ref}
|
|
51
|
+
className={cn(
|
|
52
|
+
"overflow-hidden text-sm text-muted-foreground",
|
|
53
|
+
"data-[state=open]:animate-accordion-down data-[state=closed]:animate-accordion-up",
|
|
54
|
+
className
|
|
55
|
+
)}
|
|
56
|
+
style={
|
|
57
|
+
{
|
|
58
|
+
"--accordion-content-height": "var(--radix-accordion-content-height)",
|
|
59
|
+
} as React.CSSProperties
|
|
60
|
+
}
|
|
61
|
+
{...props}
|
|
62
|
+
>
|
|
63
|
+
<div className={cn("pb-4 pt-0", className)}>{children}</div>
|
|
64
|
+
</AccordionPrimitive.Content>
|
|
65
|
+
));
|
|
66
|
+
AccordionContent.displayName = "AccordionContent";
|
|
67
|
+
|
|
68
|
+
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
|
package/src/alert.tsx
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { X, Info, CheckCircle2, AlertTriangle, XCircle } from "lucide-react";
|
|
5
|
+
import { cn } from "./utils";
|
|
6
|
+
|
|
7
|
+
export type AlertVariant = "info" | "success" | "warning" | "error" | "neutral";
|
|
8
|
+
|
|
9
|
+
export interface AlertProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
10
|
+
variant?: AlertVariant;
|
|
11
|
+
dismissible?: boolean;
|
|
12
|
+
onDismiss?: () => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const variantStyles: Record<AlertVariant, string> = {
|
|
16
|
+
info: "bg-blue-50 border-blue-200 text-blue-900 dark:bg-blue-950/30 dark:border-blue-800 dark:text-blue-200",
|
|
17
|
+
success: "bg-green-50 border-green-200 text-green-900 dark:bg-green-950/30 dark:border-green-800 dark:text-green-200",
|
|
18
|
+
warning: "bg-amber-50 border-amber-200 text-amber-900 dark:bg-amber-950/30 dark:border-amber-800 dark:text-amber-200",
|
|
19
|
+
error: "bg-red-50 border-red-200 text-red-900 dark:bg-red-950/30 dark:border-red-800 dark:text-red-200",
|
|
20
|
+
neutral: "bg-muted border-border text-foreground",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const variantIcons: Record<AlertVariant, React.ElementType> = {
|
|
24
|
+
info: Info,
|
|
25
|
+
success: CheckCircle2,
|
|
26
|
+
warning: AlertTriangle,
|
|
27
|
+
error: XCircle,
|
|
28
|
+
neutral: Info,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const Alert = React.forwardRef<HTMLDivElement, AlertProps>(
|
|
32
|
+
({ className, variant = "info", dismissible, onDismiss, children, ...props }, ref) => {
|
|
33
|
+
const Icon = variantIcons[variant];
|
|
34
|
+
return (
|
|
35
|
+
<div
|
|
36
|
+
ref={ref}
|
|
37
|
+
role="alert"
|
|
38
|
+
className={cn("relative flex gap-3 rounded-xl border p-4 shadow-sm", variantStyles[variant], className)}
|
|
39
|
+
{...props}
|
|
40
|
+
>
|
|
41
|
+
<Icon className="mt-0.5 size-4 shrink-0" aria-hidden />
|
|
42
|
+
<div className="flex-1 min-w-0">{children}</div>
|
|
43
|
+
{dismissible && (
|
|
44
|
+
<button
|
|
45
|
+
type="button"
|
|
46
|
+
onClick={onDismiss}
|
|
47
|
+
className="shrink-0 rounded-md p-0.5 opacity-60 hover:opacity-100 transition-opacity focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/35"
|
|
48
|
+
aria-label="Dismiss"
|
|
49
|
+
>
|
|
50
|
+
<X className="size-4" />
|
|
51
|
+
</button>
|
|
52
|
+
)}
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
);
|
|
57
|
+
Alert.displayName = "Alert";
|
|
58
|
+
|
|
59
|
+
const AlertTitle = React.forwardRef<HTMLHeadingElement, React.HTMLAttributes<HTMLHeadingElement>>(
|
|
60
|
+
({ className, ...props }, ref) => (
|
|
61
|
+
<h5 ref={ref} className={cn("mb-1 text-sm font-semibold leading-none", className)} {...props} />
|
|
62
|
+
)
|
|
63
|
+
);
|
|
64
|
+
AlertTitle.displayName = "AlertTitle";
|
|
65
|
+
|
|
66
|
+
const AlertDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
|
|
67
|
+
({ className, ...props }, ref) => (
|
|
68
|
+
<p ref={ref} className={cn("text-sm opacity-90 leading-relaxed", className)} {...props} />
|
|
69
|
+
)
|
|
70
|
+
);
|
|
71
|
+
AlertDescription.displayName = "AlertDescription";
|
|
72
|
+
|
|
73
|
+
export interface BannerProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
74
|
+
variant?: AlertVariant;
|
|
75
|
+
dismissible?: boolean;
|
|
76
|
+
onDismiss?: () => void;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const Banner = React.forwardRef<HTMLDivElement, BannerProps>(
|
|
80
|
+
({ className, variant = "info", dismissible, onDismiss, children, ...props }, ref) => {
|
|
81
|
+
const Icon = variantIcons[variant];
|
|
82
|
+
return (
|
|
83
|
+
<div
|
|
84
|
+
ref={ref}
|
|
85
|
+
role="banner"
|
|
86
|
+
className={cn("flex items-center gap-3 border-b px-4 py-3", variantStyles[variant], className)}
|
|
87
|
+
{...props}
|
|
88
|
+
>
|
|
89
|
+
<Icon className="size-4 shrink-0" aria-hidden />
|
|
90
|
+
<div className="flex-1 min-w-0 text-sm">{children}</div>
|
|
91
|
+
{dismissible && (
|
|
92
|
+
<button
|
|
93
|
+
type="button"
|
|
94
|
+
onClick={onDismiss}
|
|
95
|
+
className="shrink-0 rounded-md p-0.5 opacity-60 hover:opacity-100 transition-opacity focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/35"
|
|
96
|
+
aria-label="Dismiss"
|
|
97
|
+
>
|
|
98
|
+
<X className="size-4" />
|
|
99
|
+
</button>
|
|
100
|
+
)}
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
);
|
|
105
|
+
Banner.displayName = "Banner";
|
|
106
|
+
|
|
107
|
+
export { Alert, AlertTitle, AlertDescription, Banner };
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { cn } from "./utils";
|
|
5
|
+
import { Avatar, AvatarImage, AvatarFallback } from "./avatar";
|
|
6
|
+
|
|
7
|
+
export interface AvatarGroupItem {
|
|
8
|
+
src?: string;
|
|
9
|
+
fallback: string;
|
|
10
|
+
alt?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface AvatarGroupProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
14
|
+
avatars: AvatarGroupItem[];
|
|
15
|
+
max?: number;
|
|
16
|
+
size?: "sm" | "md" | "lg";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const sizeClasses: Record<NonNullable<AvatarGroupProps["size"]>, string> = {
|
|
20
|
+
sm: "h-6 w-6",
|
|
21
|
+
md: "h-8 w-8",
|
|
22
|
+
lg: "h-10 w-10",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const fontSizeClasses: Record<NonNullable<AvatarGroupProps["size"]>, string> = {
|
|
26
|
+
sm: "text-[10px]",
|
|
27
|
+
md: "text-xs",
|
|
28
|
+
lg: "text-xs",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const overlapClasses: Record<NonNullable<AvatarGroupProps["size"]>, string> = {
|
|
32
|
+
sm: "-ml-2",
|
|
33
|
+
md: "-ml-2",
|
|
34
|
+
lg: "-ml-2",
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const AvatarGroup = React.forwardRef<HTMLDivElement, AvatarGroupProps>(
|
|
38
|
+
({ avatars, max = 4, size = "md", className, ...props }, ref) => {
|
|
39
|
+
const visible = avatars.slice(0, max);
|
|
40
|
+
const overflowCount = avatars.length - max;
|
|
41
|
+
const totalSlots = visible.length + (overflowCount > 0 ? 1 : 0);
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<div
|
|
45
|
+
ref={ref}
|
|
46
|
+
className={cn("flex items-center", className)}
|
|
47
|
+
{...props}
|
|
48
|
+
>
|
|
49
|
+
{visible.map((avatar, index) => {
|
|
50
|
+
const zIndex = totalSlots - index;
|
|
51
|
+
return (
|
|
52
|
+
<Avatar
|
|
53
|
+
key={index}
|
|
54
|
+
className={cn(
|
|
55
|
+
sizeClasses[size],
|
|
56
|
+
overlapClasses[size],
|
|
57
|
+
"ring-2 ring-background border border-border shrink-0 transition-[margin] duration-[var(--duration-pg-fast,150ms)] ease-[var(--ease-pg-standard,cubic-bezier(0.4,0,0.2,1))] first:ml-0",
|
|
58
|
+
)}
|
|
59
|
+
style={{ zIndex }}
|
|
60
|
+
>
|
|
61
|
+
{avatar.src && (
|
|
62
|
+
<AvatarImage src={avatar.src} alt={avatar.alt ?? avatar.fallback} />
|
|
63
|
+
)}
|
|
64
|
+
<AvatarFallback
|
|
65
|
+
className={cn(
|
|
66
|
+
fontSizeClasses[size],
|
|
67
|
+
"font-medium",
|
|
68
|
+
)}
|
|
69
|
+
>
|
|
70
|
+
{avatar.fallback}
|
|
71
|
+
</AvatarFallback>
|
|
72
|
+
</Avatar>
|
|
73
|
+
);
|
|
74
|
+
})}
|
|
75
|
+
|
|
76
|
+
{overflowCount > 0 && (
|
|
77
|
+
<div
|
|
78
|
+
className={cn(
|
|
79
|
+
sizeClasses[size],
|
|
80
|
+
overlapClasses[size],
|
|
81
|
+
fontSizeClasses[size],
|
|
82
|
+
"relative shrink-0 flex items-center justify-center rounded-full border border-border bg-muted text-muted-foreground font-medium ring-2 ring-background transition-[margin] duration-[var(--duration-pg-fast,150ms)] ease-[var(--ease-pg-standard,cubic-bezier(0.4,0,0.2,1))]",
|
|
83
|
+
)}
|
|
84
|
+
style={{ zIndex: 0 }}
|
|
85
|
+
>
|
|
86
|
+
+{overflowCount}
|
|
87
|
+
</div>
|
|
88
|
+
)}
|
|
89
|
+
</div>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
AvatarGroup.displayName = "AvatarGroup";
|
|
95
|
+
|
|
96
|
+
export { AvatarGroup };
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import * as AvatarPrimitive from "@radix-ui/react-avatar";
|
|
5
|
+
import { X } from "lucide-react";
|
|
6
|
+
import { cn } from "./utils";
|
|
7
|
+
|
|
8
|
+
export type AvatarTagSize = "sm" | "md" | "lg";
|
|
9
|
+
|
|
10
|
+
export interface AvatarTagProps extends React.HTMLAttributes<HTMLSpanElement> {
|
|
11
|
+
/** Display label for the tag */
|
|
12
|
+
label: string;
|
|
13
|
+
/** Optional image src for the avatar */
|
|
14
|
+
src?: string;
|
|
15
|
+
/** Alt text for the avatar image */
|
|
16
|
+
alt?: string;
|
|
17
|
+
/** Size variant */
|
|
18
|
+
size?: AvatarTagSize;
|
|
19
|
+
/** Called when the remove button is clicked */
|
|
20
|
+
onRemove?: () => void;
|
|
21
|
+
/** Disables interaction */
|
|
22
|
+
disabled?: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function getInitials(label: string): string {
|
|
26
|
+
const words = label.trim().split(/\s+/);
|
|
27
|
+
if (words.length === 1) {
|
|
28
|
+
return words[0].slice(0, 2).toUpperCase();
|
|
29
|
+
}
|
|
30
|
+
return (words[0][0] + words[words.length - 1][0]).toUpperCase();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const sizeConfig: Record<
|
|
34
|
+
AvatarTagSize,
|
|
35
|
+
{ avatar: string; text: string; icon: string; padding: string }
|
|
36
|
+
> = {
|
|
37
|
+
sm: {
|
|
38
|
+
avatar: "h-[18px] w-[18px]",
|
|
39
|
+
text: "text-[10px]",
|
|
40
|
+
icon: "size-2.5",
|
|
41
|
+
padding: "px-1.5 py-0.5 gap-1",
|
|
42
|
+
},
|
|
43
|
+
md: {
|
|
44
|
+
avatar: "h-5 w-5",
|
|
45
|
+
text: "text-xs",
|
|
46
|
+
icon: "size-3",
|
|
47
|
+
padding: "px-2 py-0.5 gap-1",
|
|
48
|
+
},
|
|
49
|
+
lg: {
|
|
50
|
+
avatar: "h-6 w-6",
|
|
51
|
+
text: "text-sm",
|
|
52
|
+
icon: "size-3.5",
|
|
53
|
+
padding: "px-2.5 py-1 gap-1.5",
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const AvatarTag = React.forwardRef<HTMLSpanElement, AvatarTagProps>(
|
|
58
|
+
(
|
|
59
|
+
{
|
|
60
|
+
label,
|
|
61
|
+
src,
|
|
62
|
+
alt,
|
|
63
|
+
size = "md",
|
|
64
|
+
onRemove,
|
|
65
|
+
disabled = false,
|
|
66
|
+
className,
|
|
67
|
+
...props
|
|
68
|
+
},
|
|
69
|
+
ref
|
|
70
|
+
) => {
|
|
71
|
+
const cfg = sizeConfig[size];
|
|
72
|
+
const initials = getInitials(label);
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<span
|
|
76
|
+
ref={ref}
|
|
77
|
+
className={cn(
|
|
78
|
+
"inline-flex items-center rounded-full border border-border bg-card font-medium transition-colors duration-[var(--duration-pg-fast,150ms)] ease-[var(--ease-pg-standard,cubic-bezier(0.4,0,0.2,1))]",
|
|
79
|
+
cfg.padding,
|
|
80
|
+
cfg.text,
|
|
81
|
+
disabled && "opacity-50 cursor-not-allowed",
|
|
82
|
+
className
|
|
83
|
+
)}
|
|
84
|
+
{...props}
|
|
85
|
+
>
|
|
86
|
+
{/* Avatar */}
|
|
87
|
+
<AvatarPrimitive.Root
|
|
88
|
+
className={cn(
|
|
89
|
+
"relative shrink-0 overflow-hidden rounded-full",
|
|
90
|
+
cfg.avatar
|
|
91
|
+
)}
|
|
92
|
+
>
|
|
93
|
+
<AvatarPrimitive.Image
|
|
94
|
+
src={src}
|
|
95
|
+
alt={alt ?? label}
|
|
96
|
+
className="aspect-square h-full w-full object-cover"
|
|
97
|
+
/>
|
|
98
|
+
<AvatarPrimitive.Fallback
|
|
99
|
+
className={cn(
|
|
100
|
+
"flex h-full w-full items-center justify-center rounded-full bg-primary/10 text-primary font-semibold leading-none",
|
|
101
|
+
cfg.text
|
|
102
|
+
)}
|
|
103
|
+
>
|
|
104
|
+
{initials}
|
|
105
|
+
</AvatarPrimitive.Fallback>
|
|
106
|
+
</AvatarPrimitive.Root>
|
|
107
|
+
|
|
108
|
+
{/* Label */}
|
|
109
|
+
<span className="leading-none select-none">{label}</span>
|
|
110
|
+
|
|
111
|
+
{/* Remove button */}
|
|
112
|
+
{onRemove && (
|
|
113
|
+
<button
|
|
114
|
+
type="button"
|
|
115
|
+
onClick={(e) => {
|
|
116
|
+
e.stopPropagation();
|
|
117
|
+
if (!disabled) onRemove();
|
|
118
|
+
}}
|
|
119
|
+
disabled={disabled}
|
|
120
|
+
className={cn(
|
|
121
|
+
"flex items-center justify-center rounded-full opacity-60 transition-opacity duration-[var(--duration-pg-fast,150ms)] hover:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/35",
|
|
122
|
+
disabled && "pointer-events-none"
|
|
123
|
+
)}
|
|
124
|
+
aria-label={`Remove ${label}`}
|
|
125
|
+
>
|
|
126
|
+
<X className={cfg.icon} />
|
|
127
|
+
</button>
|
|
128
|
+
)}
|
|
129
|
+
</span>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
AvatarTag.displayName = "AvatarTag";
|
|
135
|
+
|
|
136
|
+
export { AvatarTag };
|
package/src/avatar.tsx
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import * as AvatarPrimitive from "@radix-ui/react-avatar";
|
|
5
|
+
import { cn } from "./utils";
|
|
6
|
+
|
|
7
|
+
const Avatar = React.forwardRef<
|
|
8
|
+
React.ElementRef<typeof AvatarPrimitive.Root>,
|
|
9
|
+
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
|
|
10
|
+
>(({ className, ...props }, ref) => (
|
|
11
|
+
<AvatarPrimitive.Root
|
|
12
|
+
ref={ref}
|
|
13
|
+
className={cn("relative flex h-9 w-9 shrink-0 overflow-hidden rounded-full border border-border bg-muted", className)}
|
|
14
|
+
{...props}
|
|
15
|
+
/>
|
|
16
|
+
));
|
|
17
|
+
Avatar.displayName = AvatarPrimitive.Root.displayName;
|
|
18
|
+
|
|
19
|
+
const AvatarImage = React.forwardRef<
|
|
20
|
+
React.ElementRef<typeof AvatarPrimitive.Image>,
|
|
21
|
+
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
|
|
22
|
+
>(({ className, ...props }, ref) => (
|
|
23
|
+
<AvatarPrimitive.Image ref={ref} className={cn("aspect-square h-full w-full object-cover", className)} {...props} />
|
|
24
|
+
));
|
|
25
|
+
AvatarImage.displayName = AvatarPrimitive.Image.displayName;
|
|
26
|
+
|
|
27
|
+
const AvatarFallback = React.forwardRef<
|
|
28
|
+
React.ElementRef<typeof AvatarPrimitive.Fallback>,
|
|
29
|
+
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
|
|
30
|
+
>(({ className, ...props }, ref) => (
|
|
31
|
+
<AvatarPrimitive.Fallback
|
|
32
|
+
ref={ref}
|
|
33
|
+
className={cn("flex h-full w-full items-center justify-center rounded-full bg-muted text-xs font-medium text-muted-foreground", className)}
|
|
34
|
+
{...props}
|
|
35
|
+
/>
|
|
36
|
+
));
|
|
37
|
+
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
|
|
38
|
+
|
|
39
|
+
export { Avatar, AvatarImage, AvatarFallback };
|
package/src/badge.tsx
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { cn } from "./utils";
|
|
4
|
+
import { forwardRef, type HTMLAttributes, type ReactNode } from "react";
|
|
5
|
+
|
|
6
|
+
export interface BadgeProps extends HTMLAttributes<HTMLSpanElement> {
|
|
7
|
+
variant?: "default" | "secondary" | "success" | "warning" | "error" | "outline";
|
|
8
|
+
size?: "sm" | "md" | "lg";
|
|
9
|
+
square?: boolean;
|
|
10
|
+
dot?: boolean;
|
|
11
|
+
leftIcon?: ReactNode;
|
|
12
|
+
rightIcon?: ReactNode;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const variantClasses: Record<NonNullable<BadgeProps["variant"]>, string> = {
|
|
16
|
+
default:
|
|
17
|
+
"bg-primary/10 text-primary border border-primary/20",
|
|
18
|
+
secondary:
|
|
19
|
+
"bg-muted text-muted-foreground border border-border",
|
|
20
|
+
success:
|
|
21
|
+
"bg-green-500/10 text-green-700 border border-green-500/20 dark:text-green-400",
|
|
22
|
+
warning:
|
|
23
|
+
"bg-amber-500/10 text-amber-700 border border-amber-500/20 dark:text-amber-400",
|
|
24
|
+
error:
|
|
25
|
+
"bg-red-500/10 text-red-700 border border-red-500/20 dark:text-red-400",
|
|
26
|
+
outline:
|
|
27
|
+
"bg-transparent text-foreground border border-border",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const sizeClasses: Record<NonNullable<BadgeProps["size"]>, string> = {
|
|
31
|
+
sm: "text-[10px] px-1.5 py-0.5 gap-1",
|
|
32
|
+
md: "text-xs px-2 py-0.5 gap-1",
|
|
33
|
+
lg: "text-sm px-2.5 py-1 gap-1.5",
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const dotColorClasses: Record<NonNullable<BadgeProps["variant"]>, string> = {
|
|
37
|
+
default: "bg-primary",
|
|
38
|
+
secondary: "bg-muted-foreground",
|
|
39
|
+
success: "bg-green-500",
|
|
40
|
+
warning: "bg-amber-500",
|
|
41
|
+
error: "bg-red-500",
|
|
42
|
+
outline: "bg-foreground",
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const dotSizeClasses: Record<NonNullable<BadgeProps["size"]>, string> = {
|
|
46
|
+
sm: "size-1.5",
|
|
47
|
+
md: "size-1.5",
|
|
48
|
+
lg: "size-2",
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const Badge = forwardRef<HTMLSpanElement, BadgeProps>(
|
|
52
|
+
(
|
|
53
|
+
{
|
|
54
|
+
variant = "default",
|
|
55
|
+
size = "md",
|
|
56
|
+
square = false,
|
|
57
|
+
dot = false,
|
|
58
|
+
leftIcon,
|
|
59
|
+
rightIcon,
|
|
60
|
+
children,
|
|
61
|
+
className,
|
|
62
|
+
...props
|
|
63
|
+
},
|
|
64
|
+
ref
|
|
65
|
+
) => {
|
|
66
|
+
return (
|
|
67
|
+
<span
|
|
68
|
+
ref={ref}
|
|
69
|
+
className={cn(
|
|
70
|
+
"inline-flex items-center font-medium whitespace-nowrap transition-colors duration-pg-fast ease-pg-standard",
|
|
71
|
+
square ? "rounded-md" : "rounded-full",
|
|
72
|
+
variantClasses[variant],
|
|
73
|
+
sizeClasses[size],
|
|
74
|
+
className
|
|
75
|
+
)}
|
|
76
|
+
{...props}
|
|
77
|
+
>
|
|
78
|
+
{dot && (
|
|
79
|
+
<span
|
|
80
|
+
className={cn(
|
|
81
|
+
"rounded-full flex-shrink-0",
|
|
82
|
+
dotSizeClasses[size],
|
|
83
|
+
dotColorClasses[variant]
|
|
84
|
+
)}
|
|
85
|
+
/>
|
|
86
|
+
)}
|
|
87
|
+
{!dot && leftIcon && (
|
|
88
|
+
<span className="inline-flex items-center flex-shrink-0">{leftIcon}</span>
|
|
89
|
+
)}
|
|
90
|
+
{children}
|
|
91
|
+
{rightIcon && (
|
|
92
|
+
<span className="inline-flex items-center flex-shrink-0">{rightIcon}</span>
|
|
93
|
+
)}
|
|
94
|
+
</span>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
);
|
|
98
|
+
Badge.displayName = "Badge";
|
package/src/blanket.tsx
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { cn } from "./utils";
|
|
5
|
+
|
|
6
|
+
export interface BlanketProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
7
|
+
/** Controls visibility of the blanket */
|
|
8
|
+
open?: boolean;
|
|
9
|
+
/** Adds semi-transparent black background (default: true) */
|
|
10
|
+
isTinted?: boolean;
|
|
11
|
+
/** Makes blanket completely transparent — only blocks pointer events */
|
|
12
|
+
isTransparent?: boolean;
|
|
13
|
+
/** Called when the blanket is clicked */
|
|
14
|
+
onClick?: React.MouseEventHandler<HTMLDivElement>;
|
|
15
|
+
/** When true, pointer events pass through the blanket */
|
|
16
|
+
shouldAllowClickThrough?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const Blanket = React.forwardRef<HTMLDivElement, BlanketProps>(
|
|
20
|
+
(
|
|
21
|
+
{
|
|
22
|
+
open = true,
|
|
23
|
+
isTinted = true,
|
|
24
|
+
isTransparent = false,
|
|
25
|
+
onClick,
|
|
26
|
+
shouldAllowClickThrough = false,
|
|
27
|
+
className,
|
|
28
|
+
...props
|
|
29
|
+
},
|
|
30
|
+
ref
|
|
31
|
+
) => {
|
|
32
|
+
return (
|
|
33
|
+
<div
|
|
34
|
+
ref={ref}
|
|
35
|
+
data-open={open}
|
|
36
|
+
aria-hidden="true"
|
|
37
|
+
onClick={onClick}
|
|
38
|
+
className={cn(
|
|
39
|
+
"fixed inset-0 z-40",
|
|
40
|
+
// Animate in/out
|
|
41
|
+
"data-[open=true]:animate-in data-[open=false]:animate-out",
|
|
42
|
+
"data-[open=true]:fade-in-0 data-[open=false]:fade-out-0",
|
|
43
|
+
"transition-colors duration-pg-fast ease-pg-standard",
|
|
44
|
+
// Visibility
|
|
45
|
+
!open && "pointer-events-none",
|
|
46
|
+
// Background
|
|
47
|
+
!isTransparent && isTinted && "bg-black/50 backdrop-blur-sm",
|
|
48
|
+
isTransparent && "bg-transparent",
|
|
49
|
+
// Click-through
|
|
50
|
+
shouldAllowClickThrough && "pointer-events-none",
|
|
51
|
+
className
|
|
52
|
+
)}
|
|
53
|
+
{...props}
|
|
54
|
+
/>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
Blanket.displayName = "Blanket";
|
|
60
|
+
|
|
61
|
+
export { Blanket };
|