@cortexasystem/ui 0.1.0 → 1.0.1
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/dist/index.cjs +1326 -643
- package/dist/index.d.cts +171 -13
- package/dist/index.d.ts +171 -13
- package/dist/index.js +1307 -646
- package/package.json +3 -3
- package/src/assets/isotipo-cortexa-dark.png +0 -0
- package/src/assets/isotipo-cortexa-light.png +0 -0
- package/src/components/ai/ai-chat.tsx +597 -0
- package/src/components/branding/brand-logo.tsx +77 -0
- package/src/components/data-display/icons.tsx +81 -0
- package/src/components/data-display/profile-avatar.tsx +154 -0
- package/src/components/data-display/typography.tsx +46 -0
- package/src/components/feedback/empty-state.tsx +63 -0
- package/src/components/feedback/loading-state.tsx +93 -0
- package/src/components/feedback/module-skeleton.tsx +76 -0
- package/src/components/feedback/notification.tsx +111 -0
- package/src/components/feedback/skeleton.tsx +9 -0
- package/src/components/feedback/spinner.tsx +18 -0
- package/src/components/feedback/status-badge.tsx +44 -0
- package/src/components/feedback/sync-status-badge.tsx +54 -0
- package/src/components/feedback/sync-status-bar.tsx +92 -0
- package/src/components/feedback/toaster.tsx +36 -0
- package/src/components/forms/searchable-select.tsx +206 -0
- package/src/components/forms/select.tsx +142 -0
- package/src/components/layout/app-shell.tsx +44 -0
- package/src/components/layout/form-section.tsx +21 -0
- package/src/components/layout/page-header.tsx +21 -0
- package/src/components/layout/theme-toggle.tsx +33 -0
- package/src/components/navigation/breadcrumb.tsx +87 -0
- package/src/components/navigation/header-user-menu.tsx +108 -0
- package/src/components/navigation/navbar.tsx +30 -0
- package/src/components/navigation/page-breadcrumb.tsx +44 -0
- package/src/components/navigation/sidebar.tsx +104 -0
- package/src/components/navigation/steps.tsx +82 -0
- package/src/components/overlays/dialog.tsx +94 -0
- package/src/components/overlays/drawer.tsx +85 -0
- package/src/components/overlays/dropdown-menu.tsx +179 -0
- package/src/components/overlays/sheet.tsx +110 -0
- package/src/components/primitives/alert.tsx +43 -0
- package/src/components/primitives/avatar.tsx +41 -0
- package/src/components/primitives/badge.tsx +26 -0
- package/src/components/primitives/button.tsx +49 -0
- package/src/components/primitives/card.tsx +97 -0
- package/src/components/primitives/checkbox.tsx +52 -0
- package/src/components/primitives/input.tsx +23 -0
- package/src/components/primitives/label.tsx +18 -0
- package/src/components/primitives/radio-group.tsx +57 -0
- package/src/components/primitives/separator.tsx +23 -0
- package/src/components/primitives/switch.tsx +75 -0
- package/src/components/primitives/textarea.tsx +18 -0
- package/src/components/tables/data-table.tsx +214 -0
- package/src/components/tables/data-table.types.ts +9 -0
- package/src/components/tables/table-row-actions.tsx +61 -0
- package/src/components/tables/table.tsx +88 -0
- package/src/declarations.d.ts +14 -0
- package/src/index.ts +50 -0
- package/src/lib/cn.ts +6 -0
- package/src/providers/theme-provider.tsx +90 -0
- package/src/styles.css +1 -1
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { type LucideIcon } from "lucide-react";
|
|
2
|
+
|
|
3
|
+
import { cn } from "../../lib/cn";
|
|
4
|
+
|
|
5
|
+
type IconTone = "brand" | "info" | "success" | "warning" | "danger" | "neutral";
|
|
6
|
+
type IconSize = "sm" | "md" | "lg";
|
|
7
|
+
|
|
8
|
+
const toneStyles: Record<IconTone, string> = {
|
|
9
|
+
brand: "bg-[var(--color-brand)]/10 text-[var(--color-brand)]",
|
|
10
|
+
info: "bg-[var(--color-info-bg)] text-[var(--color-accent-blue)]",
|
|
11
|
+
success: "bg-[var(--color-success-bg)] text-[var(--color-success)]",
|
|
12
|
+
warning: "bg-[var(--color-warning-bg)] text-[var(--color-warning)]",
|
|
13
|
+
danger: "bg-destructive/10 text-destructive",
|
|
14
|
+
neutral: "bg-muted text-muted-foreground"
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const sizeStyles: Record<IconSize, { wrapper: string; icon: string }> = {
|
|
18
|
+
sm: { wrapper: "h-8 w-8 rounded-lg", icon: "h-4 w-4" },
|
|
19
|
+
md: { wrapper: "h-10 w-10 rounded-lg", icon: "h-5 w-5" },
|
|
20
|
+
lg: { wrapper: "h-12 w-12 rounded-xl", icon: "h-6 w-6" }
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
interface FeatureIconProps {
|
|
24
|
+
className?: string;
|
|
25
|
+
icon: LucideIcon;
|
|
26
|
+
tone?: IconTone;
|
|
27
|
+
size?: IconSize;
|
|
28
|
+
iconClassName?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function FeatureIcon({ className, icon: Icon, tone = "brand", size = "md", iconClassName }: FeatureIconProps) {
|
|
32
|
+
const classes = sizeStyles[size];
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<span className={cn("inline-flex items-center justify-center", classes.wrapper, toneStyles[tone], className)}>
|
|
36
|
+
<Icon className={cn(classes.icon, iconClassName)} />
|
|
37
|
+
</span>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface ModuleIconButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
42
|
+
icon: LucideIcon;
|
|
43
|
+
label: string;
|
|
44
|
+
description?: string;
|
|
45
|
+
tone?: IconTone;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function ModuleIconButton({
|
|
49
|
+
className,
|
|
50
|
+
type = "button",
|
|
51
|
+
icon,
|
|
52
|
+
label,
|
|
53
|
+
description,
|
|
54
|
+
tone = "brand",
|
|
55
|
+
...props
|
|
56
|
+
}: ModuleIconButtonProps) {
|
|
57
|
+
return (
|
|
58
|
+
<button
|
|
59
|
+
type={type}
|
|
60
|
+
className={cn(
|
|
61
|
+
"group flex flex-col items-center gap-3 rounded-lg border border-border bg-card p-5 text-center transition-all hover:bg-accent/30 hover:border-[var(--color-accent-blue)]/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
|
62
|
+
className
|
|
63
|
+
)}
|
|
64
|
+
{...props}
|
|
65
|
+
>
|
|
66
|
+
<FeatureIcon
|
|
67
|
+
icon={icon}
|
|
68
|
+
tone={tone}
|
|
69
|
+
size="md"
|
|
70
|
+
className="transition-colors group-hover:bg-[var(--color-brand)]/20"
|
|
71
|
+
/>
|
|
72
|
+
<span className="grid gap-1">
|
|
73
|
+
<span className="text-sm font-medium text-foreground">{label}</span>
|
|
74
|
+
{description ? <span className="text-xs text-muted-foreground">{description}</span> : null}
|
|
75
|
+
</span>
|
|
76
|
+
</button>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export { FeatureIcon, ModuleIconButton };
|
|
81
|
+
export type { FeatureIconProps, ModuleIconButtonProps, IconSize, IconTone };
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
import { cn } from "../../lib/cn";
|
|
4
|
+
import { Avatar, AvatarFallback, AvatarImage } from "../primitives/avatar";
|
|
5
|
+
|
|
6
|
+
type ProfileAvatarSize = "xs" | "sm" | "md" | "lg";
|
|
7
|
+
type ProfileAvatarStatus = "online" | "away" | "offline" | "inactive";
|
|
8
|
+
|
|
9
|
+
const sizeMap: Record<ProfileAvatarSize, { avatar: string; text: string; dot: string }> = {
|
|
10
|
+
xs: { avatar: "h-8 w-8", text: "text-xs", dot: "h-2.5 w-2.5" },
|
|
11
|
+
sm: { avatar: "h-10 w-10", text: "text-sm", dot: "h-3 w-3" },
|
|
12
|
+
md: { avatar: "h-12 w-12", text: "text-base", dot: "h-3.5 w-3.5" },
|
|
13
|
+
lg: { avatar: "h-16 w-16", text: "text-lg", dot: "h-4 w-4" }
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const statusMap: Record<ProfileAvatarStatus, string> = {
|
|
17
|
+
online: "bg-[var(--color-success)]",
|
|
18
|
+
away: "bg-[var(--color-warning)]",
|
|
19
|
+
offline: "bg-muted-foreground",
|
|
20
|
+
inactive: "bg-border"
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function getInitials(name: string) {
|
|
24
|
+
return name
|
|
25
|
+
.trim()
|
|
26
|
+
.split(/\s+/)
|
|
27
|
+
.slice(0, 2)
|
|
28
|
+
.map((part) => part[0]?.toUpperCase() ?? "")
|
|
29
|
+
.join("");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface ProfileAvatarProps {
|
|
33
|
+
className?: string;
|
|
34
|
+
avatarClassName?: string;
|
|
35
|
+
fallbackClassName?: string;
|
|
36
|
+
name: string;
|
|
37
|
+
src?: string;
|
|
38
|
+
alt?: string;
|
|
39
|
+
initials?: string;
|
|
40
|
+
size?: ProfileAvatarSize;
|
|
41
|
+
status?: ProfileAvatarStatus;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function ProfileAvatar({
|
|
45
|
+
className,
|
|
46
|
+
avatarClassName,
|
|
47
|
+
fallbackClassName,
|
|
48
|
+
name,
|
|
49
|
+
src,
|
|
50
|
+
alt,
|
|
51
|
+
initials,
|
|
52
|
+
size = "sm",
|
|
53
|
+
status
|
|
54
|
+
}: ProfileAvatarProps) {
|
|
55
|
+
const classes = sizeMap[size];
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div className={cn("relative inline-flex", className)}>
|
|
59
|
+
<Avatar className={cn(classes.avatar, avatarClassName)}>
|
|
60
|
+
{src ? <AvatarImage src={src} alt={alt ?? name} /> : null}
|
|
61
|
+
<AvatarFallback className={cn("bg-[var(--color-brand)] text-white font-medium", classes.text, fallbackClassName)}>
|
|
62
|
+
{initials ?? getInitials(name)}
|
|
63
|
+
</AvatarFallback>
|
|
64
|
+
</Avatar>
|
|
65
|
+
{status ? (
|
|
66
|
+
<span
|
|
67
|
+
aria-hidden="true"
|
|
68
|
+
className={cn("absolute bottom-0 right-0 rounded-full border-2 border-card", classes.dot, statusMap[status])}
|
|
69
|
+
/>
|
|
70
|
+
) : null}
|
|
71
|
+
</div>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface AvatarGroupUser {
|
|
76
|
+
id?: string;
|
|
77
|
+
name: string;
|
|
78
|
+
src?: string;
|
|
79
|
+
initials?: string;
|
|
80
|
+
status?: ProfileAvatarStatus;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
interface AvatarGroupProps {
|
|
84
|
+
className?: string;
|
|
85
|
+
users: AvatarGroupUser[];
|
|
86
|
+
size?: Exclude<ProfileAvatarSize, "lg">;
|
|
87
|
+
limit?: number;
|
|
88
|
+
extraLabel?: ReactNode;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function AvatarGroup({ className, users, size = "xs", limit = 4, extraLabel }: AvatarGroupProps) {
|
|
92
|
+
const visibleUsers = users.slice(0, limit);
|
|
93
|
+
const hiddenCount = Math.max(0, users.length - limit);
|
|
94
|
+
const classes = sizeMap[size];
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<div className={cn("flex items-center gap-3", className)}>
|
|
98
|
+
<div className="flex -space-x-2">
|
|
99
|
+
{visibleUsers.map((user) => (
|
|
100
|
+
<ProfileAvatar
|
|
101
|
+
key={user.id ?? user.name}
|
|
102
|
+
name={user.name}
|
|
103
|
+
src={user.src}
|
|
104
|
+
initials={user.initials}
|
|
105
|
+
size={size}
|
|
106
|
+
status={user.status}
|
|
107
|
+
avatarClassName="border-2 border-card"
|
|
108
|
+
/>
|
|
109
|
+
))}
|
|
110
|
+
{hiddenCount > 0 ? (
|
|
111
|
+
<div
|
|
112
|
+
className={cn(
|
|
113
|
+
"flex items-center justify-center rounded-full border-2 border-card bg-muted text-muted-foreground font-medium",
|
|
114
|
+
classes.avatar,
|
|
115
|
+
classes.text
|
|
116
|
+
)}
|
|
117
|
+
>
|
|
118
|
+
+{hiddenCount}
|
|
119
|
+
</div>
|
|
120
|
+
) : null}
|
|
121
|
+
</div>
|
|
122
|
+
{extraLabel ? <div className="text-sm text-muted-foreground">{extraLabel}</div> : null}
|
|
123
|
+
</div>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
interface ProfileAvatarRowProps {
|
|
128
|
+
className?: string;
|
|
129
|
+
name: string;
|
|
130
|
+
role?: string;
|
|
131
|
+
src?: string;
|
|
132
|
+
initials?: string;
|
|
133
|
+
status?: ProfileAvatarStatus;
|
|
134
|
+
size?: ProfileAvatarSize;
|
|
135
|
+
aside?: ReactNode;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function ProfileAvatarRow({ className, name, role, src, initials, status, size = "sm", aside }: ProfileAvatarRowProps) {
|
|
139
|
+
return (
|
|
140
|
+
<div className={cn("flex items-center justify-between gap-3", className)}>
|
|
141
|
+
<div className="flex items-center gap-3">
|
|
142
|
+
<ProfileAvatar name={name} src={src} initials={initials} status={status} size={size} />
|
|
143
|
+
<div className="grid gap-0.5">
|
|
144
|
+
<p className="text-sm font-medium text-foreground">{name}</p>
|
|
145
|
+
{role ? <p className="text-xs text-muted-foreground">{role}</p> : null}
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
{aside}
|
|
149
|
+
</div>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export { ProfileAvatar, AvatarGroup, ProfileAvatarRow };
|
|
154
|
+
export type { AvatarGroupUser, ProfileAvatarProps, ProfileAvatarSize, ProfileAvatarStatus };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { type ComponentPropsWithoutRef, type ElementType, type ReactNode } from "react";
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
3
|
+
|
|
4
|
+
import { cn } from "../../lib/cn";
|
|
5
|
+
|
|
6
|
+
const typographyVariants = cva("text-foreground", {
|
|
7
|
+
variants: {
|
|
8
|
+
variant: {
|
|
9
|
+
h1: "font-display text-4xl font-bold tracking-tight",
|
|
10
|
+
h2: "font-display text-3xl font-bold tracking-tight",
|
|
11
|
+
h3: "font-display text-2xl font-semibold tracking-tight",
|
|
12
|
+
h4: "text-xl font-semibold",
|
|
13
|
+
lead: "text-lg text-muted-foreground",
|
|
14
|
+
body: "text-base",
|
|
15
|
+
small: "text-sm text-muted-foreground",
|
|
16
|
+
muted: "text-sm text-muted-foreground",
|
|
17
|
+
code: "font-mono text-sm"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
defaultVariants: {
|
|
21
|
+
variant: "body"
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
type TypographyProps<T extends ElementType> = {
|
|
26
|
+
as?: T;
|
|
27
|
+
children: ReactNode;
|
|
28
|
+
className?: string;
|
|
29
|
+
} & VariantProps<typeof typographyVariants> &
|
|
30
|
+
Omit<ComponentPropsWithoutRef<T>, "as" | "children" | "className">;
|
|
31
|
+
|
|
32
|
+
export function Typography<T extends ElementType = "p">({
|
|
33
|
+
as,
|
|
34
|
+
className,
|
|
35
|
+
variant,
|
|
36
|
+
children,
|
|
37
|
+
...props
|
|
38
|
+
}: TypographyProps<T>) {
|
|
39
|
+
const Component = as ?? "p";
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<Component className={cn(typographyVariants({ variant }), className)} {...props}>
|
|
43
|
+
{children}
|
|
44
|
+
</Component>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Inbox, type LucideIcon } from "lucide-react";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
import { cn } from "../../lib/cn";
|
|
5
|
+
|
|
6
|
+
interface EmptyStateProps {
|
|
7
|
+
className?: string;
|
|
8
|
+
icon?: LucideIcon;
|
|
9
|
+
title: string;
|
|
10
|
+
description?: ReactNode;
|
|
11
|
+
action?: ReactNode;
|
|
12
|
+
secondaryAction?: ReactNode;
|
|
13
|
+
align?: "left" | "center";
|
|
14
|
+
compact?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function EmptyState({
|
|
18
|
+
className,
|
|
19
|
+
icon: Icon = Inbox,
|
|
20
|
+
title,
|
|
21
|
+
description,
|
|
22
|
+
action,
|
|
23
|
+
secondaryAction,
|
|
24
|
+
align = "center",
|
|
25
|
+
compact = false
|
|
26
|
+
}: EmptyStateProps) {
|
|
27
|
+
const isCentered = align === "center";
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<section
|
|
31
|
+
className={cn(
|
|
32
|
+
"rounded-2xl border border-dashed bg-card text-card-foreground shadow-sm",
|
|
33
|
+
compact ? "p-5" : "p-8 sm:p-10",
|
|
34
|
+
className
|
|
35
|
+
)}
|
|
36
|
+
>
|
|
37
|
+
<div
|
|
38
|
+
className={cn(
|
|
39
|
+
"flex flex-col gap-5",
|
|
40
|
+
isCentered ? "items-center text-center" : "items-start text-left"
|
|
41
|
+
)}
|
|
42
|
+
>
|
|
43
|
+
<div className="flex h-14 w-14 items-center justify-center rounded-2xl bg-[var(--color-brand)]/10 text-[var(--color-brand)]">
|
|
44
|
+
<Icon className="h-6 w-6" />
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<div className="grid max-w-xl gap-2">
|
|
48
|
+
<p className="text-lg font-semibold text-foreground">{title}</p>
|
|
49
|
+
{description ? <div className="text-sm leading-6 text-muted-foreground">{description}</div> : null}
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
{action || secondaryAction ? (
|
|
53
|
+
<div className={cn("flex flex-wrap gap-3", isCentered && "justify-center")}>
|
|
54
|
+
{action}
|
|
55
|
+
{secondaryAction}
|
|
56
|
+
</div>
|
|
57
|
+
) : null}
|
|
58
|
+
</div>
|
|
59
|
+
</section>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export type { EmptyStateProps };
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
import { cn } from "../../lib/cn";
|
|
4
|
+
import { Card, CardContent } from "../primitives/card";
|
|
5
|
+
import { Skeleton } from "./skeleton";
|
|
6
|
+
import { Spinner } from "./spinner";
|
|
7
|
+
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../tables/table";
|
|
8
|
+
|
|
9
|
+
interface LoadingStateProps {
|
|
10
|
+
className?: string;
|
|
11
|
+
title?: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
size?: "sm" | "md" | "lg";
|
|
14
|
+
icon?: ReactNode;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function LoadingState({
|
|
18
|
+
className,
|
|
19
|
+
title = "Cargando datos",
|
|
20
|
+
description = "Estamos preparando la informacion para ti.",
|
|
21
|
+
size = "md",
|
|
22
|
+
icon
|
|
23
|
+
}: LoadingStateProps) {
|
|
24
|
+
return (
|
|
25
|
+
<div
|
|
26
|
+
role="status"
|
|
27
|
+
aria-live="polite"
|
|
28
|
+
className={cn("flex items-center gap-3 rounded-xl border border-border bg-card px-4 py-3 text-foreground", className)}
|
|
29
|
+
>
|
|
30
|
+
{icon ?? <Spinner size={size} className="text-primary" />}
|
|
31
|
+
<div className="grid gap-0.5">
|
|
32
|
+
<p className="text-sm font-medium">{title}</p>
|
|
33
|
+
<p className="text-xs text-muted-foreground">{description}</p>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface LoadingCardProps {
|
|
40
|
+
className?: string;
|
|
41
|
+
rows?: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function LoadingCard({ className, rows = 3 }: LoadingCardProps) {
|
|
45
|
+
return (
|
|
46
|
+
<Card className={className}>
|
|
47
|
+
<CardContent className="grid gap-3 p-5">
|
|
48
|
+
<Skeleton className="h-6 w-40" />
|
|
49
|
+
{Array.from({ length: rows }).map((_, index) => (
|
|
50
|
+
<Skeleton key={index} className={cn("h-4", index === rows - 1 ? "w-2/3" : "w-full")} />
|
|
51
|
+
))}
|
|
52
|
+
<Skeleton className="mt-1 h-10 w-full rounded-lg" />
|
|
53
|
+
</CardContent>
|
|
54
|
+
</Card>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface LoadingTableRowsProps {
|
|
59
|
+
className?: string;
|
|
60
|
+
columns?: number;
|
|
61
|
+
rows?: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function LoadingTableRows({ className, columns = 5, rows = 4 }: LoadingTableRowsProps) {
|
|
65
|
+
return (
|
|
66
|
+
<div className={cn("rounded-xl border", className)}>
|
|
67
|
+
<Table>
|
|
68
|
+
<TableHeader>
|
|
69
|
+
<TableRow>
|
|
70
|
+
{Array.from({ length: columns }).map((_, index) => (
|
|
71
|
+
<TableHead key={index}>
|
|
72
|
+
<Skeleton className="h-4 w-20" />
|
|
73
|
+
</TableHead>
|
|
74
|
+
))}
|
|
75
|
+
</TableRow>
|
|
76
|
+
</TableHeader>
|
|
77
|
+
<TableBody>
|
|
78
|
+
{Array.from({ length: rows }).map((_, rowIndex) => (
|
|
79
|
+
<TableRow key={rowIndex}>
|
|
80
|
+
{Array.from({ length: columns }).map((_, columnIndex) => (
|
|
81
|
+
<TableCell key={columnIndex}>
|
|
82
|
+
<Skeleton className={cn("h-4", columnIndex === 0 ? "w-24" : "w-full")} />
|
|
83
|
+
</TableCell>
|
|
84
|
+
))}
|
|
85
|
+
</TableRow>
|
|
86
|
+
))}
|
|
87
|
+
</TableBody>
|
|
88
|
+
</Table>
|
|
89
|
+
</div>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export { LoadingState, LoadingCard, LoadingTableRows };
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { Skeleton } from "./skeleton";
|
|
2
|
+
import { cn } from "../../lib/cn";
|
|
3
|
+
|
|
4
|
+
type ModuleSkeletonVariant = "cards" | "table" | "form";
|
|
5
|
+
|
|
6
|
+
interface ModuleSkeletonProps {
|
|
7
|
+
className?: string;
|
|
8
|
+
variant?: ModuleSkeletonVariant;
|
|
9
|
+
cardCount?: number;
|
|
10
|
+
showHeaderAction?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function ModuleSkeleton({
|
|
14
|
+
className,
|
|
15
|
+
variant = "cards",
|
|
16
|
+
cardCount = 6,
|
|
17
|
+
showHeaderAction = true
|
|
18
|
+
}: ModuleSkeletonProps) {
|
|
19
|
+
return (
|
|
20
|
+
<div role="status" aria-label="Cargando modulo" className={cn("grid gap-6 p-6", className)}>
|
|
21
|
+
<div className="flex flex-wrap items-center justify-between gap-4">
|
|
22
|
+
<div className="grid gap-2">
|
|
23
|
+
<Skeleton className="h-8 w-52" />
|
|
24
|
+
<Skeleton className="h-4 w-72 max-w-full" />
|
|
25
|
+
</div>
|
|
26
|
+
{showHeaderAction ? <Skeleton className="h-10 w-36" /> : null}
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
{variant === "cards" ? (
|
|
30
|
+
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
|
|
31
|
+
{Array.from({ length: cardCount }).map((_, index) => (
|
|
32
|
+
<Skeleton key={index} className="h-32 w-full rounded-xl" />
|
|
33
|
+
))}
|
|
34
|
+
</div>
|
|
35
|
+
) : null}
|
|
36
|
+
|
|
37
|
+
{variant === "table" ? (
|
|
38
|
+
<div className="rounded-xl border bg-card p-4">
|
|
39
|
+
<div className="mb-4 flex flex-wrap gap-3">
|
|
40
|
+
<Skeleton className="h-10 w-64 max-w-full" />
|
|
41
|
+
<Skeleton className="h-10 w-36" />
|
|
42
|
+
</div>
|
|
43
|
+
<div className="grid gap-3">
|
|
44
|
+
<Skeleton className="h-10 w-full rounded-lg" />
|
|
45
|
+
{Array.from({ length: 6 }).map((_, index) => (
|
|
46
|
+
<Skeleton key={index} className="h-14 w-full rounded-lg" />
|
|
47
|
+
))}
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
) : null}
|
|
51
|
+
|
|
52
|
+
{variant === "form" ? (
|
|
53
|
+
<div className="rounded-xl border bg-card p-5">
|
|
54
|
+
<div className="grid gap-5 md:grid-cols-2">
|
|
55
|
+
{Array.from({ length: 6 }).map((_, index) => (
|
|
56
|
+
<div key={index} className="grid gap-2">
|
|
57
|
+
<Skeleton className="h-4 w-28" />
|
|
58
|
+
<Skeleton className="h-10 w-full" />
|
|
59
|
+
</div>
|
|
60
|
+
))}
|
|
61
|
+
<div className="grid gap-2 md:col-span-2">
|
|
62
|
+
<Skeleton className="h-4 w-32" />
|
|
63
|
+
<Skeleton className="h-28 w-full rounded-xl" />
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
<div className="mt-6 flex flex-wrap justify-end gap-3">
|
|
67
|
+
<Skeleton className="h-10 w-24" />
|
|
68
|
+
<Skeleton className="h-10 w-36" />
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
) : null}
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export type { ModuleSkeletonProps, ModuleSkeletonVariant };
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { AlertCircle, AlertTriangle, CheckCircle2, Info, Loader2, type LucideIcon } from "lucide-react";
|
|
2
|
+
import { toast } from "sonner";
|
|
3
|
+
import { type ReactNode } from "react";
|
|
4
|
+
|
|
5
|
+
import { cn } from "../../lib/cn";
|
|
6
|
+
import { Button } from "../primitives/button";
|
|
7
|
+
|
|
8
|
+
type NotificationTone = "info" | "success" | "warning" | "danger" | "loading";
|
|
9
|
+
|
|
10
|
+
const notificationStyles: Record<NotificationTone, string> = {
|
|
11
|
+
info: "border-[var(--color-accent-blue)]/20 bg-[linear-gradient(135deg,rgba(59,130,246,0.14),rgba(239,246,255,0.96))] text-[var(--color-accent-blue)]",
|
|
12
|
+
success: "border-[var(--color-success)]/20 bg-[linear-gradient(135deg,rgba(22,163,74,0.14),rgba(220,252,231,0.96))] text-[var(--color-success)]",
|
|
13
|
+
warning: "border-[var(--color-warning)]/20 bg-[linear-gradient(135deg,rgba(217,119,6,0.16),rgba(255,251,235,0.96))] text-[var(--color-warning)]",
|
|
14
|
+
danger: "border-destructive/20 bg-[linear-gradient(135deg,rgba(220,74,65,0.16),rgba(254,242,242,0.96))] text-destructive",
|
|
15
|
+
loading: "border-[var(--color-accent-blue)]/20 bg-[linear-gradient(135deg,rgba(59,130,246,0.14),rgba(239,246,255,0.96))] text-[var(--color-accent-blue)]"
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const notificationIcons: Record<NotificationTone, LucideIcon> = {
|
|
19
|
+
info: Info,
|
|
20
|
+
success: CheckCircle2,
|
|
21
|
+
warning: AlertTriangle,
|
|
22
|
+
danger: AlertCircle,
|
|
23
|
+
loading: Loader2
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
interface NotificationMessageProps {
|
|
27
|
+
className?: string;
|
|
28
|
+
tone?: NotificationTone;
|
|
29
|
+
title: string;
|
|
30
|
+
description?: ReactNode;
|
|
31
|
+
action?: ReactNode;
|
|
32
|
+
icon?: LucideIcon;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function NotificationMessage({
|
|
36
|
+
className,
|
|
37
|
+
tone = "info",
|
|
38
|
+
title,
|
|
39
|
+
description,
|
|
40
|
+
action,
|
|
41
|
+
icon
|
|
42
|
+
}: NotificationMessageProps) {
|
|
43
|
+
const Icon = icon ?? notificationIcons[tone];
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<div
|
|
47
|
+
role="status"
|
|
48
|
+
className={cn(
|
|
49
|
+
"flex items-start justify-between gap-3 rounded-xl border px-4 py-3 shadow-sm",
|
|
50
|
+
notificationStyles[tone],
|
|
51
|
+
className
|
|
52
|
+
)}
|
|
53
|
+
>
|
|
54
|
+
<div className="flex items-start gap-3">
|
|
55
|
+
<div className="mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-white/70 shadow-sm">
|
|
56
|
+
<Icon className={cn("h-4 w-4", tone === "loading" && "animate-spin")} />
|
|
57
|
+
</div>
|
|
58
|
+
<div className="grid gap-1">
|
|
59
|
+
<p className="text-sm font-semibold">{title}</p>
|
|
60
|
+
{description ? <div className="text-xs leading-5 opacity-90">{description}</div> : null}
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
{action ? <div className="shrink-0">{action}</div> : null}
|
|
64
|
+
</div>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface NotifyPayload {
|
|
69
|
+
title: string;
|
|
70
|
+
description?: string;
|
|
71
|
+
actionLabel?: string;
|
|
72
|
+
onAction?: () => void;
|
|
73
|
+
duration?: number;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function buildToastOptions(payload: NotifyPayload) {
|
|
77
|
+
return {
|
|
78
|
+
description: payload.description,
|
|
79
|
+
duration: payload.duration,
|
|
80
|
+
action: payload.actionLabel
|
|
81
|
+
? {
|
|
82
|
+
label: payload.actionLabel,
|
|
83
|
+
onClick: payload.onAction ?? (() => undefined)
|
|
84
|
+
}
|
|
85
|
+
: undefined
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const notify = {
|
|
90
|
+
info: (payload: NotifyPayload) => toast.info(payload.title, buildToastOptions(payload)),
|
|
91
|
+
success: (payload: NotifyPayload) => toast.success(payload.title, buildToastOptions(payload)),
|
|
92
|
+
warning: (payload: NotifyPayload) => toast.warning(payload.title, buildToastOptions(payload)),
|
|
93
|
+
danger: (payload: NotifyPayload) => toast.error(payload.title, buildToastOptions(payload)),
|
|
94
|
+
error: (payload: NotifyPayload) => toast.error(payload.title, buildToastOptions(payload)),
|
|
95
|
+
loading: (payload: NotifyPayload) => toast.loading(payload.title, buildToastOptions(payload))
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
interface NotificationActionProps extends React.ComponentProps<typeof Button> {
|
|
99
|
+
children: ReactNode;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function NotificationAction({ children, variant = "outline", size = "sm", ...props }: NotificationActionProps) {
|
|
103
|
+
return (
|
|
104
|
+
<Button variant={variant} size={size} {...props}>
|
|
105
|
+
{children}
|
|
106
|
+
</Button>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export { NotificationMessage, NotificationAction, notify };
|
|
111
|
+
export type { NotificationMessageProps, NotificationTone };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { HTMLAttributes } from "react";
|
|
2
|
+
|
|
3
|
+
import { cn } from "../../lib/cn";
|
|
4
|
+
|
|
5
|
+
function Skeleton({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
|
|
6
|
+
return <div className={cn("animate-pulse rounded-md bg-muted", className)} {...props} />;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export { Skeleton };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Loader2 } from "lucide-react";
|
|
2
|
+
|
|
3
|
+
import { cn } from "../../lib/cn";
|
|
4
|
+
|
|
5
|
+
interface SpinnerProps {
|
|
6
|
+
className?: string;
|
|
7
|
+
size?: "sm" | "md" | "lg";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const sizeMap = {
|
|
11
|
+
sm: "h-4 w-4",
|
|
12
|
+
md: "h-5 w-5",
|
|
13
|
+
lg: "h-7 w-7"
|
|
14
|
+
} as const;
|
|
15
|
+
|
|
16
|
+
export function Spinner({ className, size = "md" }: SpinnerProps) {
|
|
17
|
+
return <Loader2 className={cn("animate-spin text-current", sizeMap[size], className)} />;
|
|
18
|
+
}
|