@cortexasystem/ui 1.0.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/package.json +2 -2
- 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
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
import { cn } from "../../lib/cn";
|
|
4
|
+
|
|
5
|
+
const RadioGroup = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (
|
|
6
|
+
<div ref={ref} role="radiogroup" className={cn("grid gap-3", className)} {...props} />
|
|
7
|
+
));
|
|
8
|
+
|
|
9
|
+
RadioGroup.displayName = "RadioGroup";
|
|
10
|
+
|
|
11
|
+
type RadioGroupItemProps = Omit<React.InputHTMLAttributes<HTMLInputElement>, "type">;
|
|
12
|
+
|
|
13
|
+
const RadioGroupItem = React.forwardRef<HTMLInputElement, RadioGroupItemProps>(({ className, ...props }, ref) => (
|
|
14
|
+
<span className="relative inline-flex h-4 w-4 shrink-0">
|
|
15
|
+
<input
|
|
16
|
+
ref={ref}
|
|
17
|
+
type="radio"
|
|
18
|
+
className={cn(
|
|
19
|
+
"peer absolute inset-0 h-4 w-4 cursor-pointer appearance-none rounded-full border border-input bg-background shadow-sm ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 checked:border-primary",
|
|
20
|
+
className
|
|
21
|
+
)}
|
|
22
|
+
{...props}
|
|
23
|
+
/>
|
|
24
|
+
<span
|
|
25
|
+
aria-hidden="true"
|
|
26
|
+
className="pointer-events-none absolute left-1/2 top-1/2 h-2 w-2 -translate-x-1/2 -translate-y-1/2 rounded-full bg-primary opacity-0 transition-opacity peer-checked:opacity-100"
|
|
27
|
+
/>
|
|
28
|
+
</span>
|
|
29
|
+
));
|
|
30
|
+
|
|
31
|
+
RadioGroupItem.displayName = "RadioGroupItem";
|
|
32
|
+
|
|
33
|
+
interface RadioFieldProps extends RadioGroupItemProps {
|
|
34
|
+
label: React.ReactNode;
|
|
35
|
+
description?: React.ReactNode;
|
|
36
|
+
containerClassName?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function RadioField({ id, label, description, containerClassName, className, ...props }: RadioFieldProps) {
|
|
40
|
+
const generatedId = React.useId();
|
|
41
|
+
const inputId = id ?? generatedId;
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<label
|
|
45
|
+
htmlFor={inputId}
|
|
46
|
+
className={cn("flex items-center gap-3 rounded-lg border border-border bg-card p-3 transition-colors hover:bg-accent/30", containerClassName)}
|
|
47
|
+
>
|
|
48
|
+
<RadioGroupItem id={inputId} className={className} {...props} />
|
|
49
|
+
<span className="grid gap-1">
|
|
50
|
+
<span className="text-sm font-medium text-foreground">{label}</span>
|
|
51
|
+
{description ? <span className="text-xs text-muted-foreground">{description}</span> : null}
|
|
52
|
+
</span>
|
|
53
|
+
</label>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export { RadioGroup, RadioGroupItem, RadioField };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import * as SeparatorPrimitive from "@radix-ui/react-separator";
|
|
5
|
+
|
|
6
|
+
import { cn } from "../../lib/cn";
|
|
7
|
+
|
|
8
|
+
const Separator = React.forwardRef<
|
|
9
|
+
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
|
10
|
+
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
|
11
|
+
>(({ className, orientation = "horizontal", decorative = true, ...props }, ref) => (
|
|
12
|
+
<SeparatorPrimitive.Root
|
|
13
|
+
ref={ref}
|
|
14
|
+
decorative={decorative}
|
|
15
|
+
orientation={orientation}
|
|
16
|
+
className={cn("shrink-0 bg-border", orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]", className)}
|
|
17
|
+
{...props}
|
|
18
|
+
/>
|
|
19
|
+
));
|
|
20
|
+
|
|
21
|
+
Separator.displayName = SeparatorPrimitive.Root.displayName;
|
|
22
|
+
|
|
23
|
+
export { Separator };
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
import { cn } from "../../lib/cn";
|
|
4
|
+
|
|
5
|
+
type SwitchProps = Omit<React.InputHTMLAttributes<HTMLInputElement>, "type" | "size"> & {
|
|
6
|
+
size?: "default" | "sm";
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const Switch = React.forwardRef<HTMLInputElement, SwitchProps>(({ className, size = "default", ...props }, ref) => {
|
|
10
|
+
const isSm = size === "sm";
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<span
|
|
14
|
+
className={cn(
|
|
15
|
+
"relative inline-flex shrink-0",
|
|
16
|
+
isSm ? "h-4 w-7" : "h-6 w-10"
|
|
17
|
+
)}
|
|
18
|
+
>
|
|
19
|
+
<input
|
|
20
|
+
ref={ref}
|
|
21
|
+
type="checkbox"
|
|
22
|
+
role="switch"
|
|
23
|
+
className={cn(
|
|
24
|
+
"peer absolute inset-0 cursor-pointer appearance-none rounded-full border border-input bg-input",
|
|
25
|
+
"transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
|
|
26
|
+
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
27
|
+
"checked:border-primary checked:bg-primary",
|
|
28
|
+
className
|
|
29
|
+
)}
|
|
30
|
+
{...props}
|
|
31
|
+
/>
|
|
32
|
+
<span
|
|
33
|
+
aria-hidden="true"
|
|
34
|
+
className={cn(
|
|
35
|
+
"pointer-events-none absolute rounded-full bg-background shadow-sm transition-transform",
|
|
36
|
+
isSm
|
|
37
|
+
? "top-0.5 left-0.5 h-3 w-3 peer-checked:translate-x-3"
|
|
38
|
+
: "top-1 left-1 h-4 w-4 peer-checked:translate-x-4"
|
|
39
|
+
)}
|
|
40
|
+
/>
|
|
41
|
+
</span>
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
Switch.displayName = "Switch";
|
|
46
|
+
|
|
47
|
+
interface SwitchFieldProps extends SwitchProps {
|
|
48
|
+
label: React.ReactNode;
|
|
49
|
+
description?: React.ReactNode;
|
|
50
|
+
containerClassName?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function SwitchField({ id, label, description, containerClassName, size, className, ...props }: SwitchFieldProps) {
|
|
54
|
+
const generatedId = React.useId();
|
|
55
|
+
const inputId = id ?? generatedId;
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<label
|
|
59
|
+
htmlFor={inputId}
|
|
60
|
+
className={cn(
|
|
61
|
+
"flex cursor-pointer items-center justify-between gap-4 rounded-lg border border-border bg-card p-3 transition-colors hover:bg-accent/30",
|
|
62
|
+
props.disabled && "cursor-not-allowed opacity-50 hover:bg-card",
|
|
63
|
+
containerClassName
|
|
64
|
+
)}
|
|
65
|
+
>
|
|
66
|
+
<span className="grid gap-0.5">
|
|
67
|
+
<span className="text-sm font-medium text-foreground">{label}</span>
|
|
68
|
+
{description ? <span className="text-xs text-muted-foreground">{description}</span> : null}
|
|
69
|
+
</span>
|
|
70
|
+
<Switch id={inputId} size={size} className={className} {...props} />
|
|
71
|
+
</label>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export { Switch, SwitchField };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
import { cn } from "../../lib/cn";
|
|
4
|
+
|
|
5
|
+
const Textarea = React.forwardRef<HTMLTextAreaElement, React.ComponentProps<"textarea">>(({ className, ...props }, ref) => (
|
|
6
|
+
<textarea
|
|
7
|
+
ref={ref}
|
|
8
|
+
className={cn(
|
|
9
|
+
"flex min-h-24 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
|
10
|
+
className
|
|
11
|
+
)}
|
|
12
|
+
{...props}
|
|
13
|
+
/>
|
|
14
|
+
));
|
|
15
|
+
|
|
16
|
+
Textarea.displayName = "Textarea";
|
|
17
|
+
|
|
18
|
+
export { Textarea };
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { useMemo, useState, type ReactNode } from "react";
|
|
2
|
+
import { ChevronLeft, ChevronRight, Search } from "lucide-react";
|
|
3
|
+
|
|
4
|
+
import { cn } from "../../lib/cn";
|
|
5
|
+
import { Skeleton } from "../feedback/skeleton";
|
|
6
|
+
import { Button } from "../primitives/button";
|
|
7
|
+
import { Card, CardContent } from "../primitives/card";
|
|
8
|
+
import { Input } from "../primitives/input";
|
|
9
|
+
import {
|
|
10
|
+
Table,
|
|
11
|
+
TableBody,
|
|
12
|
+
TableCell,
|
|
13
|
+
TableHead,
|
|
14
|
+
TableHeader,
|
|
15
|
+
TableRow
|
|
16
|
+
} from "./table";
|
|
17
|
+
import type { ColumnDef } from "./data-table.types";
|
|
18
|
+
|
|
19
|
+
interface DataTableProps<T> {
|
|
20
|
+
data: T[];
|
|
21
|
+
columns: ColumnDef<T>[];
|
|
22
|
+
isLoading?: boolean;
|
|
23
|
+
actions?: (row: T) => ReactNode;
|
|
24
|
+
emptyMessage?: string;
|
|
25
|
+
pageSize?: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getCellContent<T>(row: T, column: ColumnDef<T>): ReactNode {
|
|
29
|
+
if (column.render) {
|
|
30
|
+
return column.render(row);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const value = (row as Record<string, unknown>)[column.key as string];
|
|
34
|
+
|
|
35
|
+
if (value === null || value === undefined) {
|
|
36
|
+
return <span className="text-muted-foreground/40">-</span>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (typeof value === "boolean") {
|
|
40
|
+
return value ? "Yes" : "No";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return String(value);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const SKELETON_ROWS = 5;
|
|
47
|
+
|
|
48
|
+
export function DataTable<T>({
|
|
49
|
+
data,
|
|
50
|
+
columns,
|
|
51
|
+
isLoading = false,
|
|
52
|
+
actions,
|
|
53
|
+
emptyMessage = "No records found",
|
|
54
|
+
pageSize = 10
|
|
55
|
+
}: DataTableProps<T>) {
|
|
56
|
+
const [search, setSearch] = useState("");
|
|
57
|
+
const [page, setPage] = useState(0);
|
|
58
|
+
|
|
59
|
+
const filtered = useMemo(() => {
|
|
60
|
+
const query = search.toLowerCase().trim();
|
|
61
|
+
|
|
62
|
+
if (!query) {
|
|
63
|
+
return data;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return data.filter((row) =>
|
|
67
|
+
Object.values(row as Record<string, unknown>).some(
|
|
68
|
+
(value) => value != null && String(value).toLowerCase().includes(query)
|
|
69
|
+
)
|
|
70
|
+
);
|
|
71
|
+
}, [data, search]);
|
|
72
|
+
|
|
73
|
+
const totalPages = Math.max(1, Math.ceil(filtered.length / pageSize));
|
|
74
|
+
const currentPage = Math.min(page, totalPages - 1);
|
|
75
|
+
const paginated = filtered.slice(currentPage * pageSize, (currentPage + 1) * pageSize);
|
|
76
|
+
const mobileColumns = columns.filter((column) => !column.mobileHide);
|
|
77
|
+
|
|
78
|
+
function handleSearch(value: string) {
|
|
79
|
+
setSearch(value);
|
|
80
|
+
setPage(0);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<div className="space-y-4">
|
|
85
|
+
<div className="relative">
|
|
86
|
+
<Search className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
|
87
|
+
<Input
|
|
88
|
+
placeholder="Search..."
|
|
89
|
+
value={search}
|
|
90
|
+
onChange={(event) => handleSearch(event.target.value)}
|
|
91
|
+
className="pl-9"
|
|
92
|
+
/>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
<div className="hidden rounded-lg border md:block">
|
|
96
|
+
<Table>
|
|
97
|
+
<TableHeader>
|
|
98
|
+
<TableRow>
|
|
99
|
+
{columns.map((column) => (
|
|
100
|
+
<TableHead key={String(column.key)}>{column.label}</TableHead>
|
|
101
|
+
))}
|
|
102
|
+
{actions ? <TableHead className="w-[100px] text-right">Actions</TableHead> : null}
|
|
103
|
+
</TableRow>
|
|
104
|
+
</TableHeader>
|
|
105
|
+
<TableBody>
|
|
106
|
+
{isLoading ? (
|
|
107
|
+
Array.from({ length: SKELETON_ROWS }).map((_, index) => (
|
|
108
|
+
<TableRow key={index}>
|
|
109
|
+
{columns.map((column) => (
|
|
110
|
+
<TableCell key={String(column.key)}>
|
|
111
|
+
<Skeleton className="h-4 w-full" />
|
|
112
|
+
</TableCell>
|
|
113
|
+
))}
|
|
114
|
+
{actions ? (
|
|
115
|
+
<TableCell>
|
|
116
|
+
<Skeleton className="ml-auto h-8 w-20" />
|
|
117
|
+
</TableCell>
|
|
118
|
+
) : null}
|
|
119
|
+
</TableRow>
|
|
120
|
+
))
|
|
121
|
+
) : paginated.length === 0 ? (
|
|
122
|
+
<TableRow>
|
|
123
|
+
<TableCell colSpan={columns.length + (actions ? 1 : 0)} className="py-12 text-center text-muted-foreground">
|
|
124
|
+
{emptyMessage}
|
|
125
|
+
</TableCell>
|
|
126
|
+
</TableRow>
|
|
127
|
+
) : (
|
|
128
|
+
paginated.map((row, index) => (
|
|
129
|
+
<TableRow key={index}>
|
|
130
|
+
{columns.map((column) => (
|
|
131
|
+
<TableCell key={String(column.key)}>{getCellContent(row, column)}</TableCell>
|
|
132
|
+
))}
|
|
133
|
+
{actions ? (
|
|
134
|
+
<TableCell>
|
|
135
|
+
<div className="flex justify-end gap-1.5">{actions(row)}</div>
|
|
136
|
+
</TableCell>
|
|
137
|
+
) : null}
|
|
138
|
+
</TableRow>
|
|
139
|
+
))
|
|
140
|
+
)}
|
|
141
|
+
</TableBody>
|
|
142
|
+
</Table>
|
|
143
|
+
</div>
|
|
144
|
+
|
|
145
|
+
<div className="space-y-3 md:hidden">
|
|
146
|
+
{isLoading ? (
|
|
147
|
+
Array.from({ length: SKELETON_ROWS }).map((_, index) => (
|
|
148
|
+
<Card key={index}>
|
|
149
|
+
<CardContent className="space-y-2 p-4">
|
|
150
|
+
<Skeleton className="h-5 w-1/2" />
|
|
151
|
+
<Skeleton className="h-4 w-2/3" />
|
|
152
|
+
<Skeleton className="h-4 w-1/3" />
|
|
153
|
+
</CardContent>
|
|
154
|
+
</Card>
|
|
155
|
+
))
|
|
156
|
+
) : paginated.length === 0 ? (
|
|
157
|
+
<p className="py-10 text-center text-muted-foreground">{emptyMessage}</p>
|
|
158
|
+
) : (
|
|
159
|
+
paginated.map((row, index) => (
|
|
160
|
+
<Card key={index}>
|
|
161
|
+
<CardContent className="space-y-3 p-4">
|
|
162
|
+
<div className="space-y-1.5">
|
|
163
|
+
{mobileColumns.map((column, mobileIndex) => (
|
|
164
|
+
<div
|
|
165
|
+
key={String(column.key)}
|
|
166
|
+
className={cn(
|
|
167
|
+
"flex items-center gap-1.5",
|
|
168
|
+
mobileIndex === 0 ? "text-sm font-medium text-foreground" : "text-xs text-muted-foreground"
|
|
169
|
+
)}
|
|
170
|
+
>
|
|
171
|
+
{mobileIndex > 0 ? (
|
|
172
|
+
<span className="shrink-0 font-medium text-foreground/50">{column.label}:</span>
|
|
173
|
+
) : null}
|
|
174
|
+
<span className="truncate">{getCellContent(row, column)}</span>
|
|
175
|
+
</div>
|
|
176
|
+
))}
|
|
177
|
+
</div>
|
|
178
|
+
{actions ? <div className="flex gap-2 border-t border-border pt-2">{actions(row)}</div> : null}
|
|
179
|
+
</CardContent>
|
|
180
|
+
</Card>
|
|
181
|
+
))
|
|
182
|
+
)}
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
{!isLoading && filtered.length > pageSize ? (
|
|
186
|
+
<div className="flex items-center justify-between">
|
|
187
|
+
<span className="text-sm text-muted-foreground">
|
|
188
|
+
{filtered.length} records · Page {currentPage + 1} of {totalPages}
|
|
189
|
+
</span>
|
|
190
|
+
<div className="flex gap-1">
|
|
191
|
+
<Button
|
|
192
|
+
variant="outline"
|
|
193
|
+
size="icon"
|
|
194
|
+
className="h-8 w-8"
|
|
195
|
+
onClick={() => setPage((previous) => Math.max(0, previous - 1))}
|
|
196
|
+
disabled={currentPage === 0}
|
|
197
|
+
>
|
|
198
|
+
<ChevronLeft className="h-4 w-4" />
|
|
199
|
+
</Button>
|
|
200
|
+
<Button
|
|
201
|
+
variant="outline"
|
|
202
|
+
size="icon"
|
|
203
|
+
className="h-8 w-8"
|
|
204
|
+
onClick={() => setPage((previous) => Math.min(totalPages - 1, previous + 1))}
|
|
205
|
+
disabled={currentPage >= totalPages - 1}
|
|
206
|
+
>
|
|
207
|
+
<ChevronRight className="h-4 w-4" />
|
|
208
|
+
</Button>
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
) : null}
|
|
212
|
+
</div>
|
|
213
|
+
);
|
|
214
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { MoreHorizontal } from "lucide-react";
|
|
2
|
+
import { type ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
DropdownMenu,
|
|
6
|
+
DropdownMenuContent,
|
|
7
|
+
DropdownMenuItem,
|
|
8
|
+
DropdownMenuLabel,
|
|
9
|
+
DropdownMenuSeparator,
|
|
10
|
+
DropdownMenuShortcut,
|
|
11
|
+
DropdownMenuTrigger
|
|
12
|
+
} from "../overlays/dropdown-menu";
|
|
13
|
+
import { Button } from "../primitives/button";
|
|
14
|
+
import { cn } from "../../lib/cn";
|
|
15
|
+
|
|
16
|
+
interface TableActionItem {
|
|
17
|
+
label: string;
|
|
18
|
+
icon?: ReactNode;
|
|
19
|
+
shortcut?: string;
|
|
20
|
+
disabled?: boolean;
|
|
21
|
+
destructive?: boolean;
|
|
22
|
+
onSelect?: () => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface TableRowActionsProps {
|
|
26
|
+
className?: string;
|
|
27
|
+
label?: string;
|
|
28
|
+
menuLabel?: string;
|
|
29
|
+
items: TableActionItem[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function TableRowActions({ className, label = "Abrir acciones", menuLabel = "Acciones", items }: TableRowActionsProps) {
|
|
33
|
+
return (
|
|
34
|
+
<DropdownMenu>
|
|
35
|
+
<DropdownMenuTrigger asChild>
|
|
36
|
+
<Button variant="ghost" size="icon" className={cn("h-8 w-8 shadow-none", className)} aria-label={label}>
|
|
37
|
+
<MoreHorizontal className="h-4 w-4" />
|
|
38
|
+
</Button>
|
|
39
|
+
</DropdownMenuTrigger>
|
|
40
|
+
<DropdownMenuContent align="end" className="w-48">
|
|
41
|
+
<DropdownMenuLabel>{menuLabel}</DropdownMenuLabel>
|
|
42
|
+
<DropdownMenuSeparator />
|
|
43
|
+
{items.map((item) => (
|
|
44
|
+
<DropdownMenuItem
|
|
45
|
+
key={item.label}
|
|
46
|
+
disabled={item.disabled}
|
|
47
|
+
onSelect={item.onSelect}
|
|
48
|
+
className={cn(item.destructive && "text-destructive focus:bg-destructive/10 focus:text-destructive")}
|
|
49
|
+
>
|
|
50
|
+
{item.icon}
|
|
51
|
+
{item.label}
|
|
52
|
+
{item.shortcut ? <DropdownMenuShortcut>{item.shortcut}</DropdownMenuShortcut> : null}
|
|
53
|
+
</DropdownMenuItem>
|
|
54
|
+
))}
|
|
55
|
+
</DropdownMenuContent>
|
|
56
|
+
</DropdownMenu>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export { TableRowActions };
|
|
61
|
+
export type { TableActionItem };
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
import { cn } from "../../lib/cn";
|
|
4
|
+
|
|
5
|
+
const Table = React.forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTableElement>>(({ className, ...props }, ref) => (
|
|
6
|
+
<div className="relative w-full overflow-auto">
|
|
7
|
+
<table ref={ref} className={cn("w-full caption-bottom text-sm", className)} {...props} />
|
|
8
|
+
</div>
|
|
9
|
+
));
|
|
10
|
+
|
|
11
|
+
Table.displayName = "Table";
|
|
12
|
+
|
|
13
|
+
const TableHeader = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
|
|
14
|
+
({ className, ...props }, ref) => <thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
TableHeader.displayName = "TableHeader";
|
|
18
|
+
|
|
19
|
+
const TableBody = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
|
|
20
|
+
({ className, ...props }, ref) => (
|
|
21
|
+
<tbody ref={ref} className={cn("[&_tr:last-child]:border-0", className)} {...props} />
|
|
22
|
+
)
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
TableBody.displayName = "TableBody";
|
|
26
|
+
|
|
27
|
+
const TableFooter = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
|
|
28
|
+
({ className, ...props }, ref) => (
|
|
29
|
+
<tfoot
|
|
30
|
+
ref={ref}
|
|
31
|
+
className={cn("border-t bg-muted/50 font-medium [&>tr]:last:border-b-0", className)}
|
|
32
|
+
{...props}
|
|
33
|
+
/>
|
|
34
|
+
)
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
TableFooter.displayName = "TableFooter";
|
|
38
|
+
|
|
39
|
+
const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTMLTableRowElement>>(
|
|
40
|
+
({ className, ...props }, ref) => (
|
|
41
|
+
<tr
|
|
42
|
+
ref={ref}
|
|
43
|
+
className={cn("border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted", className)}
|
|
44
|
+
{...props}
|
|
45
|
+
/>
|
|
46
|
+
)
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
TableRow.displayName = "TableRow";
|
|
50
|
+
|
|
51
|
+
const TableHead = React.forwardRef<HTMLTableCellElement, React.ThHTMLAttributes<HTMLTableCellElement>>(
|
|
52
|
+
({ className, ...props }, ref) => (
|
|
53
|
+
<th
|
|
54
|
+
ref={ref}
|
|
55
|
+
className={cn("h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0", className)}
|
|
56
|
+
{...props}
|
|
57
|
+
/>
|
|
58
|
+
)
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
TableHead.displayName = "TableHead";
|
|
62
|
+
|
|
63
|
+
const TableCell = React.forwardRef<HTMLTableCellElement, React.TdHTMLAttributes<HTMLTableCellElement>>(
|
|
64
|
+
({ className, ...props }, ref) => (
|
|
65
|
+
<td ref={ref} className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)} {...props} />
|
|
66
|
+
)
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
TableCell.displayName = "TableCell";
|
|
70
|
+
|
|
71
|
+
const TableCaption = React.forwardRef<HTMLTableCaptionElement, React.HTMLAttributes<HTMLTableCaptionElement>>(
|
|
72
|
+
({ className, ...props }, ref) => (
|
|
73
|
+
<caption ref={ref} className={cn("mt-4 text-sm text-muted-foreground", className)} {...props} />
|
|
74
|
+
)
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
TableCaption.displayName = "TableCaption";
|
|
78
|
+
|
|
79
|
+
export {
|
|
80
|
+
Table,
|
|
81
|
+
TableHeader,
|
|
82
|
+
TableBody,
|
|
83
|
+
TableFooter,
|
|
84
|
+
TableHead,
|
|
85
|
+
TableRow,
|
|
86
|
+
TableCell,
|
|
87
|
+
TableCaption
|
|
88
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export * from "./providers/theme-provider";
|
|
2
|
+
export * from "./lib/cn";
|
|
3
|
+
|
|
4
|
+
export * from "./components/ai/ai-chat";
|
|
5
|
+
export * from "./components/branding/brand-logo";
|
|
6
|
+
export * from "./components/data-display/icons";
|
|
7
|
+
export * from "./components/data-display/profile-avatar";
|
|
8
|
+
export * from "./components/data-display/typography";
|
|
9
|
+
export * from "./components/feedback/empty-state";
|
|
10
|
+
export * from "./components/feedback/loading-state";
|
|
11
|
+
export * from "./components/feedback/module-skeleton";
|
|
12
|
+
export * from "./components/feedback/notification";
|
|
13
|
+
export * from "./components/feedback/skeleton";
|
|
14
|
+
export * from "./components/feedback/spinner";
|
|
15
|
+
export * from "./components/feedback/status-badge";
|
|
16
|
+
export * from "./components/feedback/sync-status-badge";
|
|
17
|
+
export * from "./components/feedback/sync-status-bar";
|
|
18
|
+
export * from "./components/feedback/toaster";
|
|
19
|
+
export * from "./components/forms/searchable-select";
|
|
20
|
+
export * from "./components/forms/select";
|
|
21
|
+
export * from "./components/layout/app-shell";
|
|
22
|
+
export * from "./components/layout/form-section";
|
|
23
|
+
export * from "./components/layout/page-header";
|
|
24
|
+
export * from "./components/layout/theme-toggle";
|
|
25
|
+
export * from "./components/navigation/breadcrumb";
|
|
26
|
+
export * from "./components/navigation/header-user-menu";
|
|
27
|
+
export * from "./components/navigation/navbar";
|
|
28
|
+
export * from "./components/navigation/page-breadcrumb";
|
|
29
|
+
export * from "./components/navigation/sidebar";
|
|
30
|
+
export * from "./components/navigation/steps";
|
|
31
|
+
export * from "./components/overlays/dialog";
|
|
32
|
+
export * from "./components/overlays/drawer";
|
|
33
|
+
export * from "./components/overlays/dropdown-menu";
|
|
34
|
+
export * from "./components/overlays/sheet";
|
|
35
|
+
export * from "./components/primitives/alert";
|
|
36
|
+
export * from "./components/primitives/avatar";
|
|
37
|
+
export * from "./components/primitives/badge";
|
|
38
|
+
export * from "./components/primitives/button";
|
|
39
|
+
export * from "./components/primitives/card";
|
|
40
|
+
export * from "./components/primitives/checkbox";
|
|
41
|
+
export * from "./components/primitives/input";
|
|
42
|
+
export * from "./components/primitives/label";
|
|
43
|
+
export * from "./components/primitives/radio-group";
|
|
44
|
+
export * from "./components/primitives/separator";
|
|
45
|
+
export * from "./components/primitives/switch";
|
|
46
|
+
export * from "./components/primitives/textarea";
|
|
47
|
+
export * from "./components/tables/data-table";
|
|
48
|
+
export * from "./components/tables/data-table.types";
|
|
49
|
+
export * from "./components/tables/table";
|
|
50
|
+
export * from "./components/tables/table-row-actions";
|