Merge pull request #13 from Tria-plc/ui-comps

UI comps
This commit is contained in:
Nathnael Wondisha
2026-05-20 11:41:25 +03:00
committed by GitHub
34 changed files with 1676 additions and 445 deletions

View File

@@ -1,42 +1 @@
@import "tailwindcss";
@custom-variant dark (&:where(.dark, .dark *));
* {
scrollbar-width: thin;
scrollbar-color: rgb(203 213 225 / 0.6) transparent;
}
*::-webkit-scrollbar {
width: 8px;
height: 10px;
}
*::-webkit-scrollbar-track {
background: transparent;
}
*::-webkit-scrollbar-thumb {
background-color: rgb(203 213 225 / 0.7);
border-radius: 9999px;
}
*::-webkit-scrollbar-thumb:hover {
background-color: rgb(16 185 129 / 0.5);
}
*::-webkit-scrollbar-corner {
background: transparent;
}
.dark * {
scrollbar-color: rgb(71 85 105 / 0.6) transparent;
}
.dark *::-webkit-scrollbar-thumb {
background-color: rgb(71 85 105 / 0.6);
}
.dark *::-webkit-scrollbar-thumb:hover {
background-color: rgb(16 185 129 / 0.7);
}

View File

@@ -1,13 +1,15 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>EDR Freight Portal</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
<link rel="stylesheet" href="/index.css" />
</body>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>EDR Freight Portal</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@@ -2,12 +2,13 @@ import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import "../index.css";
import "@tria-plc/iamui-common/styles.css";
import "@edr/ui-common/styles.css";
import App from "./App";
import {
AuthProvider,
BrandingProvider,
configureIam,
UserProvider,
axiosInstance,
@@ -67,15 +68,13 @@ if (!rootElement) {
createRoot(document.getElementById("root")!).render(
<StrictMode>
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<BrandingProvider>
<BrowserRouter>
<AuthProvider>
<UserProvider>
<App />
<App />
</UserProvider>
</AuthProvider>
</BrandingProvider>
</BrowserRouter>
</BrowserRouter>
</QueryClientProvider>
</StrictMode>,
);

View File

@@ -1,8 +1,6 @@
import { useMemo, useState } from "react";
import { useMemo } from "react";
import { Link } from "react-router-dom";
import {
ChevronLeft,
ChevronRight,
Clock3,
Eye,
Filter,
@@ -18,63 +16,155 @@ import {
import Breadcrumbs from "@/components/Breadcrumbs";
import NewCustomerPage from "./NewCustomerPage";
import DeleteCustomerDialog from "./DeleteCustomerDialog";
import { customers, type CustomerStatus } from "./customers.mock";
const PAGE_SIZE_OPTIONS = [5, 10, 25, 50];
import {
customers,
type CustomerStatus,
type Customer,
} from "./customers.mock";
import {
DataTable,
DataTableFooter,
type ColumnDef,
usePagination,
Button,
Card,
CardHeader,
CardTitle,
CardDescription,
CardContent,
Input,
} from "@edr/ui-common";
export default function CustomerPage() {
const [pageSize, setPageSize] = useState(10);
const [page, setPage] = useState(1);
const { pagination, setPagination } = usePagination({ pageSize: 10 });
const total = customers.length;
const totalPages = Math.max(1, Math.ceil(total / pageSize));
const safePage = Math.min(page, totalPages);
const start = (safePage - 1) * pageSize;
const end = Math.min(start + pageSize, total);
const paginated = useMemo(
const pageCount = Math.ceil(total / pagination.pageSize);
const start = pagination.pageIndex * pagination.pageSize;
const end = Math.min(start + pagination.pageSize, total);
const paginatedData = useMemo(
() => customers.slice(start, end),
[start, end],
);
const columns: ColumnDef<Customer>[] = [
{
accessorKey: "name",
header: "Customer",
cell: ({ row }) => {
const customer = row.original;
return (
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-secondary text-secondary-foreground border">
<User className="h-5 w-5" />
</div>
<div>
<p className="font-medium text-slate-900">{customer.name}</p>
<p className="text-sm text-slate-500">ID #{customer.id}</p>
</div>
</div>
);
},
},
{
accessorKey: "company",
header: "Company",
},
{
accessorKey: "email",
header: "Email",
cell: ({ row }) => (
<span className="text-sm text-slate-700">{row.original.email}</span>
),
},
{
accessorKey: "status",
header: "Status",
cell: ({ row }) => <StatusBadge status={row.original.status} />,
},
{
id: "actions",
header: () => <div className="text-right">Actions</div>,
cell: ({ row }) => {
const customer = row.original;
return (
<div className="flex justify-end gap-2">
<Link
to={`/customers/${customer.id}`}
className="rounded-xl border border-slate-200 p-2 text-slate-600 transition hover:border-[#33578D]/30 hover:bg-[#33578D]/10 hover:text-[#33578D]"
>
<Eye className="h-4 w-4" />
</Link>
<NewCustomerPage
mode="edit"
customer={{
companyName: customer.company,
customerType: customer.customerType,
contactPerson: customer.name,
email: customer.email,
phone: customer.phone,
tinNumber: customer.tinNumber,
city: customer.city,
country: customer.country,
address: customer.address,
notes: customer.notes,
}}
>
<Button variant="outline">
<Pencil />
</Button>
</NewCustomerPage>
<DeleteCustomerDialog customerName={customer.name}>
<button
type="button"
className="rounded-xl border border-red-200 p-2 text-red-600 transition hover:bg-red-50"
>
<Trash2 className="h-4 w-4" />
</button>
</DeleteCustomerDialog>
</div>
);
},
},
];
return (
<div className="min-h-screen bg-slate-50 p-6">
<div className="mx-auto max-w-7xl space-y-6">
<div className="min-h-screen p-6">
<div className="space-y-6">
<Breadcrumbs items={[{ label: "Customers" }]} />
{/* Header */}
<div className="flex flex-col gap-4 rounded-3xl bg-white p-6 shadow-sm md:flex-row md:items-center md:justify-between">
<Card className="p-6 flex-row justify-between ">
<div>
<h1 className="text-3xl font-bold tracking-tight text-slate-900">
Customers
</h1>
<p className="mt-1 text-sm text-slate-500">
<p className="mt-1 text-sm text-secondary-foreground ">
Manage and monitor your customer records.
</p>
</div>
<div className="flex flex-col items-stretch gap-3 sm:flex-row sm:items-center">
<div className="relative w-full sm:w-80">
<Search className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-slate-400" />
<input
<Search className="pointer-events-none absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-slate-400" />
<Input
type="search"
placeholder="Search customers..."
className="h-10 w-full rounded-2xl border border-slate-200 bg-white pl-10 pr-4 text-sm text-slate-700 outline-none transition placeholder:text-slate-400 focus:border-[#10B981]/50 focus:ring-2 focus:ring-[#10B981]/20"
className="pl-8!"
/>
</div>
<NewCustomerPage>
<button
type="button"
className="inline-flex items-center justify-center gap-2 rounded-2xl bg-[#10B981] px-4 py-2 text-sm font-medium text-white transition hover:bg-[#10B981]/90"
>
<Plus className="h-4 w-4" />
<Button>
<Plus />
Add Customer
</button>
</Button>
</NewCustomerPage>
</div>
</div>
</Card>
{/* Stats */}
<div className="grid gap-4 md:grid-cols-3">
<StatCard
title="Total Customers"
@@ -95,221 +185,42 @@ export default function CustomerPage() {
/>
</div>
{/* Customer Table */}
<div className="overflow-hidden rounded-3xl bg-white shadow-sm">
<div className="flex items-center justify-between border-b border-slate-100 px-6 py-4">
<Card className="gap-0">
<CardHeader className="flex flex-row items-center justify-between border-b ">
<div>
<h2 className="text-lg font-semibold text-slate-900">
Customer List
</h2>
<p className="text-sm text-slate-500">
<CardTitle>Customer List</CardTitle>
<CardDescription>
Recent customer activities and records.
</p>
</CardDescription>
</div>
<button className="inline-flex items-center gap-2 rounded-2xl border border-slate-200 px-4 py-2 text-sm font-medium text-slate-700 transition hover:border-[#10B981]/30 hover:bg-[#10B981]/10 hover:text-[#10B981]">
<Filter className="h-4 w-4" />
<Button variant="secondary" size="sm">
<Filter />
Filter
</button>
</div>
</Button>
</CardHeader>
<div className="overflow-x-auto">
<table className="w-full min-w-[700px] whitespace-nowrap text-left">
<thead className="bg-slate-50 text-sm text-slate-500">
<tr>
<th className="px-6 py-4 font-medium">Customer</th>
<th className="px-6 py-4 font-medium">Company</th>
<th className="px-6 py-4 font-medium">Email</th>
<th className="px-6 py-4 font-medium">Status</th>
<th className="px-6 py-4 font-medium text-right">Actions</th>
</tr>
</thead>
<tbody>
{paginated.map((customer) => (
<tr
key={customer.id}
className="border-t border-slate-100 transition hover:bg-[#10B981]/5"
>
<td className="px-6 py-4">
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-[#10B981] text-white">
<User className="h-5 w-5" />
</div>
<div>
<p className="font-medium text-slate-900">
{customer.name}
</p>
<p className="text-sm text-slate-500">
ID #{customer.id}
</p>
</div>
</div>
</td>
<td className="px-6 py-4 text-sm text-slate-700">
{customer.company}
</td>
<td className="px-6 py-4 text-sm text-slate-700">
{customer.email}
</td>
<td className="px-6 py-4">
<StatusBadge status={customer.status} />
</td>
<td className="px-6 py-4">
<div className="flex justify-end gap-2">
<Link
to={`/customers/${customer.id}`}
className="rounded-xl border border-slate-200 p-2 text-slate-600 transition hover:border-[#10B981]/30 hover:bg-[#10B981]/10 hover:text-[#10B981]"
>
<Eye className="h-4 w-4" />
</Link>
<NewCustomerPage
mode="edit"
customer={{
companyName: customer.company,
customerType: customer.customerType,
contactPerson: customer.name,
email: customer.email,
phone: customer.phone,
tinNumber: customer.tinNumber,
city: customer.city,
country: customer.country,
address: customer.address,
notes: customer.notes,
}}
>
<button
type="button"
className="rounded-xl border border-slate-200 p-2 text-slate-600 transition hover:border-[#10B981]/30 hover:bg-[#10B981]/10 hover:text-[#10B981]"
>
<Pencil className="h-4 w-4" />
</button>
</NewCustomerPage>
<DeleteCustomerDialog customerName={customer.name}>
<button
type="button"
className="rounded-xl border border-red-200 p-2 text-red-600 transition hover:bg-red-50"
>
<Trash2 className="h-4 w-4" />
</button>
</DeleteCustomerDialog>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
<Pagination
page={safePage}
pageSize={pageSize}
total={total}
totalPages={totalPages}
start={start}
end={end}
onPageChange={setPage}
onPageSizeChange={(size) => {
setPageSize(size);
setPage(1);
}}
/>
</div>
</div>
</div>
);
}
function Pagination({
page,
pageSize,
total,
totalPages,
start,
end,
onPageChange,
onPageSizeChange,
}: {
page: number;
pageSize: number;
total: number;
totalPages: number;
start: number;
end: number;
onPageChange: (page: number) => void;
onPageSizeChange: (size: number) => void;
}) {
const showingFrom = total === 0 ? 0 : start + 1;
return (
<div className="flex flex-col gap-3 border-t border-slate-100 px-6 py-4 md:flex-row md:items-center md:justify-between">
<div className="flex items-center gap-3 text-sm text-slate-500">
<label htmlFor="page-size" className="font-medium text-slate-700">
Rows per page
</label>
<select
id="page-size"
value={pageSize}
onChange={(e) => onPageSizeChange(Number(e.target.value))}
className="h-9 rounded-xl border border-slate-200 bg-white px-3 text-sm text-slate-700 outline-none transition focus:border-[#10B981]/50 focus:ring-2 focus:ring-[#10B981]/20"
>
{PAGE_SIZE_OPTIONS.map((size) => (
<option key={size} value={size}>
{size}
</option>
))}
</select>
<span>
Showing {showingFrom}{end} of {total}
</span>
</div>
<div className="flex items-center gap-1">
<button
type="button"
onClick={() => onPageChange(page - 1)}
disabled={page <= 1}
className="inline-flex h-9 items-center gap-1 rounded-xl border border-slate-200 px-3 text-sm font-medium text-slate-700 transition hover:border-[#10B981]/30 hover:bg-[#10B981]/10 hover:text-[#10B981] disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:bg-transparent disabled:hover:text-slate-700"
>
<ChevronLeft className="h-4 w-4" />
Prev
</button>
{Array.from({ length: totalPages }, (_, i) => i + 1).map((p) => {
const isActive = p === page;
return (
<button
key={p}
type="button"
onClick={() => onPageChange(p)}
aria-current={isActive ? "page" : undefined}
className={
isActive
? "h-9 w-9 rounded-xl bg-[#10B981] text-sm font-medium text-white"
: "h-9 w-9 rounded-xl border border-slate-200 text-sm font-medium text-slate-700 transition hover:border-[#10B981]/30 hover:bg-[#10B981]/10 hover:text-[#10B981]"
}
>
{p}
</button>
);
})}
<button
type="button"
onClick={() => onPageChange(page + 1)}
disabled={page >= totalPages}
className="inline-flex h-9 items-center gap-1 rounded-xl border border-slate-200 px-3 text-sm font-medium text-slate-700 transition hover:border-[#10B981]/30 hover:bg-[#10B981]/10 hover:text-[#10B981] disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:bg-transparent disabled:hover:text-slate-700"
>
Next
<ChevronRight className="h-4 w-4" />
</button>
<CardContent className="px-0">
<DataTable
columns={columns}
data={paginatedData}
status="success"
onRowClick={() => { }}
pagination={{
pageIndex: pagination.pageIndex,
pageSize: pagination.pageSize,
pageCount: pageCount,
totalCount: total,
}}
tableOptions={{
state: { pagination },
onPaginationChange: setPagination,
}}
containerClassName="border-b shadow-none"
footer={DataTableFooter}
/>
</CardContent>
</Card>
</div>
</div>
);
@@ -325,8 +236,8 @@ function StatCard({
icon: React.ReactNode;
}) {
return (
<div className="rounded-3xl bg-white p-6 shadow-sm transition hover:shadow-md hover:ring-1 hover:ring-[#10B981]/20">
<div className="flex items-center justify-between">
<Card className="transition hover:shadow-md hover:ring-1 hover:ring-[#33578D]/20">
<CardContent className="flex items-center justify-between">
<div>
<p className="text-sm text-slate-500">{title}</p>
<h3 className="mt-2 text-3xl font-bold text-slate-900">{value}</h3>
@@ -335,8 +246,8 @@ function StatCard({
<div className="flex h-12 w-12 items-center justify-center rounded-2xl bg-[#10B981] text-white">
{icon}
</div>
</div>
</div>
</CardContent>
</Card>
);
}

View File

@@ -9,9 +9,9 @@ import {
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
} from "@edr/ui-common";
import { Button } from "@/components/ui/button";
import { Button } from "@edr/ui-common";
export interface DeleteCustomerDialogProps {
customerName: string;

View File

@@ -4,7 +4,7 @@
"version": "0.0.0",
"scripts": {
"dev": "turbo run dev",
"dev:freight": "turbo run dev --filter=@edr/freight-api... --filter=@edr/freight-portal... --filter=@edr/freight-backoffice...",
"dev:freight": "turbo run dev --filter=@edr/freight-api... --filter=@edr/freight-portal... --filter=@edr/freight-backoffice... --filter=@edr/ui-common...",
"dev:passenger": "turbo run dev --filter=@edr/passenger-api... --filter=@edr/passenger-portal... --filter=@edr/passenger-backoffice...",
"build": "turbo run build",
"build:freight": "turbo run build --filter=@edr/freight-api... --filter=@edr/freight-portal... --filter=@edr/freight-backoffice...",

View File

@@ -6,9 +6,13 @@
"main": "./src/index.ts",
"types": "./src/index.ts",
"exports": {
".": "./src/index.ts"
".": "./src/index.ts",
"./styles.css": "./dist/index.css"
},
"scripts": {
"build:styles": "tailwindcss -i ./src/styles/index.css -o ./dist/index.css",
"check-types": "tsc --noEmit",
"dev:styles": "tailwindcss -i ./src/styles/index.css -o ./dist/index.css --watch",
"type-check": "tsc --noEmit",
"lint": "eslint src"
},
@@ -18,6 +22,7 @@
},
"dependencies": {
"@edr/types": "workspace:*",
"@tanstack/react-table": "^8.21.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^1.14.0",
@@ -29,10 +34,13 @@
"devDependencies": {
"@edr/eslint-config": "workspace:*",
"@edr/tsconfig": "workspace:*",
"@tailwindcss/cli": "^4.3.0",
"@types/react": "^18.3.11",
"@types/react-dom": "^18.3.0",
"postcss": "^8.4.47",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"tailwindcss": "^4.3.0",
"typescript": "^5.5.4"
},
"imports": {

View File

@@ -0,0 +1,6 @@
// Optional PostCSS configuration for applications that need it
export const postcssConfig = {
plugins: {
"@tailwindcss/postcss": {},
},
};

View File

@@ -1,57 +0,0 @@
import { ButtonHTMLAttributes, forwardRef } from "react";
import clsx from "clsx";
export type ButtonVariant = "primary" | "secondary" | "ghost" | "danger";
export type ButtonSize = "sm" | "md" | "lg";
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: ButtonVariant;
size?: ButtonSize;
isLoading?: boolean;
}
const variantClasses: Record<ButtonVariant, string> = {
primary: "bg-blue-600 text-white hover:bg-blue-700 disabled:bg-blue-300",
secondary: "bg-gray-100 text-gray-900 hover:bg-gray-200 disabled:bg-gray-50",
ghost: "bg-transparent text-gray-900 hover:bg-gray-100",
danger: "bg-red-600 text-white hover:bg-red-700 disabled:bg-red-300",
};
const sizeClasses: Record<ButtonSize, string> = {
sm: "px-3 py-1.5 text-sm",
md: "px-4 py-2 text-base",
lg: "px-6 py-3 text-lg",
};
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
(
{
variant = "primary",
size = "md",
isLoading,
disabled,
className,
children,
...rest
},
ref,
) => (
<button
ref={ref}
disabled={disabled || isLoading}
className={clsx(
"inline-flex items-center justify-center rounded-md font-medium transition-colors disabled:cursor-not-allowed",
variantClasses[variant],
sizeClasses[size],
className,
)}
{...rest}
>
{isLoading ? "Loading..." : children}
</button>
),
);
Button.displayName = "Button";
export default Button;

View File

@@ -1,2 +0,0 @@
export { default } from "./Button";
export type { ButtonProps, ButtonVariant, ButtonSize } from "./Button";

View File

@@ -123,7 +123,7 @@ const DashboardLayout = ({
) : null;
return (
<div className="flex h-screen overflow-hidden bg-slate-50 dark:bg-slate-950">
<div className="flex min-h-screen">
<Sidebar
title={title}
items={sidebarItems}
@@ -131,11 +131,9 @@ const DashboardLayout = ({
onNavigate={onNavigate}
headerExtra={themeToggleButton}
/>
<div className="flex min-w-0 flex-1 flex-col">
<header className="flex h-16 items-center justify-between border-b border-slate-200 bg-white px-6 dark:border-slate-800 dark:bg-slate-900">
<div className="text-base font-medium text-slate-700 dark:text-slate-200">
{title}
</div>
<div className="flex flex-1 flex-col">
<header className="flex h-16 items-center justify-between border-b px-6 ">
<div className="text-base font-medium ">{title}</div>
<div className="flex items-center gap-2">
<button
@@ -170,9 +168,8 @@ const DashboardLayout = ({
{userName}
</span>
<ChevronDown
className={`h-4 w-4 text-slate-400 transition dark:text-slate-500 ${
isUserMenuOpen ? "rotate-180 text-[#10B981]" : ""
}`}
className={`h-4 w-4 text-slate-400 transition dark:text-slate-500 ${isUserMenuOpen ? "rotate-180 text-[#33578D]" : ""
}`}
/>
</button>
@@ -220,9 +217,7 @@ const DashboardLayout = ({
</div>
</header>
<main className="flex-1 overflow-auto bg-slate-50 p-6 dark:bg-slate-950">
{children}
</main>
<main className="flex-1 overflow-auto bg-background ">{children}</main>
</div>
</div>
);

View File

@@ -28,11 +28,11 @@ const Sidebar = ({
onNavigate,
headerExtra,
}: SidebarProps) => (
<aside className="flex h-full w-64 shrink-0 flex-col gap-1 overflow-y-auto border-r border-slate-200 bg-[#10B981]/10 px-3 py-5 dark:border-slate-800 dark:bg-slate-900">
<aside className="flex w-64 flex-col gap-1 border-r border-sidebar-border bg-sidebar px-3 py-5 ">
{title ? (
<div className="flex items-center justify-between gap-2 px-3 pb-4">
<div className="flex items-center gap-2">
<div className="flex h-9 w-9 items-center justify-center rounded-xl bg-[#10B981] text-sm font-bold text-white">
<div className="flex h-9 w-9 items-center justify-center rounded-xl bg-sidebar-primary text-sm font-bold text-white">
{title.charAt(0)}
</div>
<div className="text-base font-semibold text-slate-800 dark:text-slate-100">
@@ -45,8 +45,7 @@ const Sidebar = ({
<nav className="flex flex-col gap-1">
{items.map((item) => {
const isActive =
activeHref?.toLowerCase() === item.href.toLowerCase();
const isActive = activeHref?.toLowerCase() === item.href.toLowerCase();
return (
<a
@@ -60,10 +59,10 @@ const Sidebar = ({
}}
aria-current={isActive ? "page" : undefined}
className={clsx(
"group flex items-center gap-3 rounded-xl px-3 py-2.5 text-sm font-medium transition",
"group flex items-center gap-3 rounded-md px-3 py-2.5 text-sm font-medium transition",
isActive
? "bg-[#10B981] text-white shadow-sm shadow-[#10B981]/25"
: "text-slate-700 hover:bg-[#10B981]/10 hover:text-[#10B981] dark:text-slate-300 dark:hover:bg-[#10B981]/20 dark:hover:text-white",
? "bg-sidebar-primary text-sidebar-primary-foreground shadow-sm "
: "text-sidebar-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
)}
>
{item.icon ? (

View File

@@ -0,0 +1,48 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { Slot } from "radix-ui"
import { cn } from "../lib/utils"
const badgeVariants = cva(
"inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
secondary:
"bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
destructive:
"bg-destructive text-white focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40 [a&]:hover:bg-destructive/90",
outline:
"border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
ghost: "[a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
link: "text-primary underline-offset-4 [a&]:hover:underline",
},
},
defaultVariants: {
variant: "default",
},
}
)
function Badge({
className,
variant = "default",
asChild = false,
...props
}: React.ComponentProps<"span"> &
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot.Root : "span"
return (
<Comp
data-slot="badge"
data-variant={variant}
className={cn(badgeVariants({ variant }), className)}
{...props}
/>
)
}
export { Badge, badgeVariants }

View File

@@ -0,0 +1,92 @@
import * as React from "react";
import { cn } from "../lib/utils";
function Card({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card"
className={cn(
"flex flex-col gap-6 rounded-xl border bg-card py-6 text-card-foreground shadow-xs",
className,
)}
{...props}
/>
);
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
className,
)}
{...props}
/>
);
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn("leading-none font-semibold", className)}
{...props}
/>
);
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
);
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className,
)}
{...props}
/>
);
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-6", className)}
{...props}
/>
);
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
{...props}
/>
);
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
};

View File

@@ -0,0 +1,40 @@
import { Table as TanstackTable } from "@tanstack/react-table";
import { TableCell, TableRow } from "#components/table";
import { Button } from "#components/button.tsx";
export function DataTableError({
table,
message,
description,
onRetry,
}: {
table: TanstackTable<any>;
message?: string;
description?: string;
onRetry?: () => void;
}) {
return (
<TableRow>
<TableCell colSpan={table.getVisibleFlatColumns().length}>
<div className="flex items-center my-6 justify-center">
<div className="text-center">
<div className="my-4">
<h2 className="text-xl font-semibold ">
{message ?? "No results"}
</h2>
<p className="mt-2 text-sm text-secondary-foreground">
{description ?? "No results found"}
</p>
</div>
{onRetry && (
<Button variant={"outline"} onClick={onRetry}>
Retry
</Button>
)}
</div>
</div>
</TableCell>
</TableRow>
);
}

View File

@@ -0,0 +1,119 @@
import { Button } from "../button";
import { DataTableFooterProps } from "./types";
export interface DataTableFooterOptions {
pageSizeOptions?: number[];
showPageSizeSelector?: boolean;
showRowCount?: boolean;
showPagination?: boolean;
labels?: {
rowsPerPage?: string;
page?: string;
of?: string;
showing?: string;
ofLabel?: string;
items?: string;
previous?: string;
next?: string;
};
}
interface DataTableFooterComponentProps<
TData,
> extends DataTableFooterProps<TData> {
options?: DataTableFooterOptions;
}
const defaultOptions: DataTableFooterOptions = {
pageSizeOptions: [5, 10, 25, 50],
showPageSizeSelector: true,
showRowCount: true,
showPagination: true,
labels: {
rowsPerPage: "Rows per page",
page: "Page",
of: "of",
showing: "Showing",
ofLabel: "of",
items: "items",
previous: "Previous",
next: "Next",
},
};
export function DataTableFooter<TData>({
table,
pagination,
options = {},
}: DataTableFooterComponentProps<TData>) {
const opts = { ...defaultOptions, ...options };
const labels = { ...defaultOptions.labels, ...options.labels };
const pageIndex = pagination.pageIndex ?? 0;
const pageSize = pagination.pageSize ?? 10;
const totalCount = pagination.totalCount ?? 0;
const start = totalCount === 0 ? 0 : pageIndex * pageSize + 1;
const end = Math.min((pageIndex + 1) * pageSize, totalCount);
const handlePageSizeChange = (newPageSize: number) => {
table?.setPageSize(newPageSize);
};
return (
<div className="flex flex-wrap items-center justify-between gap-4 p-4 max-sm:flex-col max-sm:items-start">
{(opts.showPageSizeSelector || opts.showRowCount) && (
<div className="flex flex-wrap items-center gap-3 text-sm text-slate-500">
{opts.showPageSizeSelector && (
<>
<label htmlFor="page-size" className="font-medium text-slate-700">
{labels.rowsPerPage}
</label>
<select
id="page-size"
value={pageSize}
onChange={(e) => handlePageSizeChange(Number(e.target.value))}
className="h-9 rounded-xl border border-slate-200 bg-white px-3 text-sm text-slate-700 outline-none transition focus:border-[#33578D]/50 focus:ring-2 focus:ring-[#33578D]/20"
>
{opts.pageSizeOptions?.map((size) => (
<option key={size} value={size}>
{size}
</option>
))}
</select>
</>
)}
{opts.showRowCount && (
<span>
{labels.showing} {start}{end} {labels.ofLabel} {totalCount}{" "}
{labels.items}
</span>
)}
</div>
)}
{opts.showPagination && (
<div className="flex items-center justify-end space-x-2">
<div className="space-x-2">
<Button
variant="outline"
size="sm"
onClick={() => table?.previousPage()}
disabled={!table?.getCanPreviousPage()}
>
{labels.previous}
</Button>
<Button
variant="outline"
size="sm"
onClick={() => table?.nextPage()}
disabled={!table?.getCanNextPage()}
>
{labels.next}
</Button>
</div>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,25 @@
import { PaginationState } from "@tanstack/react-table";
import { useState } from "react";
export const usePagination = ({
pageSize,
pageIndex,
}: {
pageSize?: number;
pageIndex?: number;
} = {}) => {
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: pageIndex ?? 0,
pageSize: pageSize ?? 10,
});
const setPage = (pageIndex: number) => {
setPagination((prev) => ({ ...prev, pageIndex }));
};
return {
pagination,
setPagination,
setPage,
};
};

View File

@@ -0,0 +1,12 @@
export { DataTableError } from "./error";
export { DataTableSkeleton } from "./skeleton";
export { DataTableFooter, type DataTableFooterOptions } from "./footer";
export type {
DataTableProps,
DataTablePagination,
DataTableFooterProps,
DataTableFooterComponent,
} from "./types";
export { usePagination } from "./hooks";
export { DataTable } from "./table";
export * from "@tanstack/react-table";

View File

@@ -0,0 +1,16 @@
import { Table as TanstackTable } from "@tanstack/react-table";
import { Skeleton } from "../skeleton";
import { TableCell, TableRow } from "#components/table";
export function DataTableSkeleton({ table }: { table: TanstackTable<any> }) {
return Array.from({ length: 10 }).map((_, i) => (
<TableRow key={i}>
{table.getAllColumns().map((column) => (
<TableCell key={column.id}>
<Skeleton className="h-8" />
</TableCell>
))}
</TableRow>
));
}

View File

@@ -0,0 +1,149 @@
import {
flexRender,
getCoreRowModel,
getPaginationRowModel,
useReactTable,
} from "@tanstack/react-table";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "#components/table";
import { DataTableProps } from "./types";
import { DataTableSkeleton } from "./skeleton";
import { DataTableError } from "./error";
import { DataTableFooter } from "./footer";
export function DataTable<TData, TValue>({
columns,
data,
status,
onRowClick,
tableOptions,
pagination,
footer,
footerClassName,
containerClassName,
error,
emptyMessage,
}: DataTableProps<TData, TValue>) {
const { state, ...otherOptions } = tableOptions ?? {};
const baseState = state ?? {};
if (pagination) {
baseState.pagination = {
pageIndex: pagination.pageIndex ?? 0,
pageSize: pagination.pageSize ?? 1,
};
}
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
manualPagination: true,
...(pagination && {
getPaginationRowModel: getPaginationRowModel(),
pageCount: pagination.pageCount,
}),
state: baseState,
...otherOptions,
});
return (
<>
<div className={containerClassName}>
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead
key={header.id}
className={
(header.column.columnDef.meta as Record<string, any>)
?.headerClassName ?? ""
}
style={{ width: `${header.getSize()}px` }}
>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
);
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{status === "loading" && <DataTableSkeleton table={table} />}
{status === "error" && (
<DataTableError
table={table}
message={error?.message ?? "Something went wrong"}
description={error?.description ?? "Please try again"}
onRetry={error?.onRetry}
/>
)}
{status === "success" &&
(table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
onClick={() => onRowClick?.(row.original)}
role={onRowClick ? "button" : ""}
className={
onRowClick
? "cursor-pointer hover:bg-accent hover:text-foreground "
: ""
}
>
{row.getVisibleCells().map((cell) => (
<TableCell
key={cell.id}
className={
(cell.column.columnDef.meta as Record<string, any>)
?.cellClassName ?? ""
}
>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={table.getVisibleFlatColumns().length}
className="h-24 text-center"
>
{emptyMessage ?? "No data"}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
{pagination && (
<div className={footerClassName}>
{footer ? (
footer({ table, pagination })
) : (
<DataTableFooter table={table} pagination={pagination} />
)}
</div>
)}
</>
);
}

View File

@@ -0,0 +1,38 @@
import type { Table as TanstackTable, TableOptions, ColumnDef } from "@tanstack/react-table";
export interface DataTablePagination {
pageSize?: number;
pageIndex?: number;
pageCount?: number;
totalCount?: number;
}
export interface DataTableFooterProps<TData> {
table: TanstackTable<TData>;
pagination: DataTablePagination;
}
export type DataTableFooterComponent<TData> = (
props: DataTableFooterProps<TData>
) => React.ReactNode;
export interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[];
data: TData[];
status?: "loading" | "error" | "success";
onRowClick?: (row: TData) => void;
tableOptions?: Omit<
TableOptions<TData>,
"data" | "columns" | "getCoreRowModel"
>;
pagination?: DataTablePagination;
footer?: DataTableFooterComponent<TData>;
footerClassName?: string;
containerClassName?: string;
emptyMessage?: string;
error?: {
message: string;
description?: string;
onRetry?: () => void;
};
}

View File

@@ -0,0 +1,156 @@
import * as React from "react"
import { XIcon } from "lucide-react"
import { Dialog as DialogPrimitive } from "radix-ui"
import { cn } from "../lib/utils"
import { Button } from "./button"
function Dialog({
...props
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />
}
function DialogTrigger({
...props
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
}
function DialogPortal({
...props
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
}
function DialogClose({
...props
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
}
function DialogOverlay({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
return (
<DialogPrimitive.Overlay
data-slot="dialog-overlay"
className={cn(
"fixed inset-0 z-50 bg-black/50 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0",
className
)}
{...props}
/>
)
}
function DialogContent({
className,
children,
showCloseButton = true,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean
}) {
return (
<DialogPortal data-slot="dialog-portal">
<DialogOverlay />
<DialogPrimitive.Content
data-slot="dialog-content"
className={cn(
"fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border bg-background p-6 shadow-lg duration-200 outline-none data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 sm:max-w-lg",
className
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close
data-slot="dialog-close"
className="absolute top-4 right-4 rounded-xs opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
>
<XIcon />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Content>
</DialogPortal>
)
}
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
/>
)
}
function DialogFooter({
className,
showCloseButton = false,
children,
...props
}: React.ComponentProps<"div"> & {
showCloseButton?: boolean
}) {
return (
<div
data-slot="dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close asChild>
<Button variant="outline">Close</Button>
</DialogPrimitive.Close>
)}
</div>
)
}
function DialogTitle({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn("text-lg leading-none font-semibold", className)}
{...props}
/>
)
}
function DialogDescription({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
return (
<DialogPrimitive.Description
data-slot="dialog-description"
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
)
}
export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
}

View File

@@ -0,0 +1,21 @@
import * as React from "react"
import { cn } from "../lib/utils"
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
<input
type={type}
data-slot="input"
className={cn(
"h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30",
"focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
"aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40",
className
)}
{...props}
/>
)
}
export { Input }

View File

@@ -0,0 +1,190 @@
"use client"
import * as React from "react"
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
import { Select as SelectPrimitive } from "radix-ui"
import { cn } from "../lib/utils"
function Select({
...props
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
return <SelectPrimitive.Root data-slot="select" {...props} />
}
function SelectGroup({
...props
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
return <SelectPrimitive.Group data-slot="select-group" {...props} />
}
function SelectValue({
...props
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
return <SelectPrimitive.Value data-slot="select-value" {...props} />
}
function SelectTrigger({
className,
size = "default",
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
size?: "sm" | "default"
}) {
return (
<SelectPrimitive.Trigger
data-slot="select-trigger"
data-size={size}
className={cn(
"flex w-fit items-center justify-between gap-2 rounded-md border border-input bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[placeholder]:text-muted-foreground data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDownIcon className="size-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
)
}
function SelectContent({
className,
children,
position = "item-aligned",
align = "center",
...props
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
return (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
data-slot="select-content"
className={cn(
"relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border bg-popover text-popover-foreground shadow-md data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
align={align}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
)
}
function SelectLabel({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
return (
<SelectPrimitive.Label
data-slot="select-label"
className={cn("px-2 py-1.5 text-xs text-muted-foreground", className)}
{...props}
/>
)
}
function SelectItem({
className,
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
return (
<SelectPrimitive.Item
data-slot="select-item"
className={cn(
"relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className
)}
{...props}
>
<span
data-slot="select-item-indicator"
className="absolute right-2 flex size-3.5 items-center justify-center"
>
<SelectPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
)
}
function SelectSeparator({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
return (
<SelectPrimitive.Separator
data-slot="select-separator"
className={cn("pointer-events-none -mx-1 my-1 h-px bg-border", className)}
{...props}
/>
)
}
function SelectScrollUpButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
return (
<SelectPrimitive.ScrollUpButton
data-slot="select-scroll-up-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronUpIcon className="size-4" />
</SelectPrimitive.ScrollUpButton>
)
}
function SelectScrollDownButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
return (
<SelectPrimitive.ScrollDownButton
data-slot="select-scroll-down-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronDownIcon className="size-4" />
</SelectPrimitive.ScrollDownButton>
)
}
export {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectScrollDownButton,
SelectScrollUpButton,
SelectSeparator,
SelectTrigger,
SelectValue,
}

View File

@@ -0,0 +1,13 @@
import { cn } from "../lib/utils"
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="skeleton"
className={cn("animate-pulse rounded-md bg-accent", className)}
{...props}
/>
)
}
export { Skeleton }

View File

@@ -0,0 +1,114 @@
import * as React from "react";
import { cn } from "../lib/utils";
function Table({ className, ...props }: React.ComponentProps<"table">) {
return (
<div
data-slot="table-container"
className="relative w-full overflow-x-auto"
>
<table
data-slot="table"
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
);
}
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
return (
<thead
data-slot="table-header"
className={cn("[&_tr]:border-b outline-ring/50", className)}
{...props}
/>
);
}
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
return (
<tbody
data-slot="table-body"
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
);
}
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
return (
<tfoot
data-slot="table-footer"
className={cn(
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
className,
)}
{...props}
/>
);
}
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
return (
<tr
data-slot="table-row"
className={cn(
"border-b transition-colors bg-background hover:bg-muted/50 has-aria-expanded:bg-muted/50 data-[state=selected]:bg-muted",
className,
)}
{...props}
/>
);
}
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
return (
<th
data-slot="table-head"
className={cn(
"h-10 bg-muted first-of-type:pl-4 last-of-type:pr-4 first-of-type: p-2 py-4 text-left align-middle font-medium whitespace-nowrap text-secondary-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className,
)}
{...props}
/>
);
}
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
return (
<td
data-slot="table-cell"
className={cn(
"p-2 align-middle first-of-type:pl-4 last-of-type:pr-4 whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className,
)}
{...props}
/>
);
}
function TableCaption({
className,
...props
}: React.ComponentProps<"caption">) {
return (
<caption
data-slot="table-caption"
className={cn("mt-4 text-sm text-muted-foreground", className)}
{...props}
/>
);
}
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
};

View File

@@ -1,9 +1,3 @@
export type {
ButtonProps,
ButtonVariant,
ButtonSize,
} from "./components/Button";
export { default as Table } from "./components/Table";
export type { TableProps, TableColumn } from "./components/Table";
@@ -24,6 +18,10 @@ export type {
DashboardLayoutProps,
} from "./components/Layout";
export { Button } from "./components/button";
export * from "./components/button";
export * from "./components/input";
export * from "./theme";
export * from "./components/card";
export * from "./components/data-table";
export * from "./components/dialog";

View File

@@ -1,2 +0,0 @@
@import "tailwindcss";
@import "tw-animate-css";

View File

@@ -0,0 +1,235 @@
@import "tailwindcss";
@import "tw-animate-css";
@custom-variant dark (&:where(.dark, .dark *));
:root {
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(0.99 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.596 0.1274 163.23);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.9753 0.0148 149.37);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--destructive-foreground: oklch(1 0 0);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.81 0.1 252);
--chart-2: oklch(0.62 0.19 260);
--chart-3: oklch(0.55 0.22 263);
--chart-4: oklch(0.49 0.22 264);
--chart-5: oklch(0.42 0.18 266);
--sidebar: oklch(0.986 0.0068 174.38);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.596 0.1274 163.23);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
--font-sans:
ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif,
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
--font-mono:
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
"Courier New", monospace;
--shadow-x: 0;
--shadow-y: 1px;
--shadow-blur: 3px;
--shadow-spread: 0px;
--shadow-opacity: 0.1;
--shadow-color: oklch(0 0 0);
--shadow-2xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
--shadow-xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
--shadow-sm:
0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 1px 2px -1px hsl(0 0% 0% / 0.1);
--shadow: 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 1px 2px -1px hsl(0 0% 0% / 0.1);
--shadow-md:
0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 2px 4px -1px hsl(0 0% 0% / 0.1);
--shadow-lg:
0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 4px 6px -1px hsl(0 0% 0% / 0.1);
--shadow-xl:
0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 8px 10px -1px hsl(0 0% 0% / 0.1);
--shadow-2xl: 0 1px 3px 0px hsl(0 0% 0% / 0.25);
--tracking-normal: 0em;
--spacing: 0.25rem;
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.269 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: #0d5c2c;
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.371 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--destructive-foreground: oklch(0.985 0 0);
--border: oklch(0.275 0 0);
--input: oklch(0.325 0 0);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.81 0.1 252);
--chart-2: oklch(0.62 0.19 260);
--chart-3: oklch(0.55 0.22 263);
--chart-4: oklch(0.49 0.22 264);
--chart-5: oklch(0.42 0.18 266);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(0.275 0 0);
--sidebar-ring: oklch(0.439 0 0);
--font-sans:
ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif,
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
--font-mono:
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
"Courier New", monospace;
--shadow-x: 0;
--shadow-y: 1px;
--shadow-blur: 3px;
--shadow-spread: 0px;
--shadow-opacity: 0.1;
--shadow-color: oklch(0 0 0);
--shadow-2xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
--shadow-xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
--shadow-sm:
0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 1px 2px -1px hsl(0 0% 0% / 0.1);
--shadow: 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 1px 2px -1px hsl(0 0% 0% / 0.1);
--shadow-md:
0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 2px 4px -1px hsl(0 0% 0% / 0.1);
--shadow-lg:
0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 4px 6px -1px hsl(0 0% 0% / 0.1);
--shadow-xl:
0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 8px 10px -1px hsl(0 0% 0% / 0.1);
--shadow-2xl: 0 1px 3px 0px hsl(0 0% 0% / 0.25);
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
--font-sans: var(--font-sans);
--font-mono: var(--font-mono);
--font-serif: var(--font-serif);
--radius: 0.8rem;
--radius-sm: 0.48rem;
--radius-md: 0.64rem;
--radius-lg: 1rem;
--radius-xl: 1.12rem;
--radius-2xl: 1.44rem;
--radius-3xl: 1.76rem;
--radius-4xl: 2.08rem;
--shadow-2xs: var(--shadow-2xs);
--shadow-xs: var(--shadow-xs);
--shadow-sm: var(--shadow-sm);
--shadow: var(--shadow);
--shadow-md: var(--shadow-md);
--shadow-lg: var(--shadow-lg);
--shadow-xl: var(--shadow-xl);
--shadow-2xl: var(--shadow-2xl);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}
* {
scrollbar-width: thin;
scrollbar-color: rgb(203 213 225 / 0.6) transparent;
border-color: var(--color-border);
}
*::-webkit-scrollbar {
width: 8px;
height: 10px;
}
*::-webkit-scrollbar-track {
background: transparent;
}
*::-webkit-scrollbar-thumb {
background-color: rgb(203 213 225 / 0.7);
border-radius: 9999px;
}
*::-webkit-scrollbar-thumb:hover {
background-color: rgb(51 87 141 / 0.5);
}
*::-webkit-scrollbar-corner {
background: transparent;
}
.dark * {
scrollbar-color: rgb(71 85 105 / 0.6) transparent;
}
.dark *::-webkit-scrollbar-thumb {
background-color: rgb(71 85 105 / 0.6);
}
.dark *::-webkit-scrollbar-thumb:hover {
background-color: rgb(51 87 141 / 0.7);
}

View File

@@ -1,24 +0,0 @@
export const colors = {
primary: {
50: "#eff6ff",
100: "#dbeafe",
500: "#3b82f6",
600: "#2563eb",
700: "#1d4ed8",
900: "#1e3a8a",
},
neutral: {
50: "#f9fafb",
100: "#f3f4f6",
200: "#e5e7eb",
400: "#9ca3af",
600: "#4b5563",
900: "#111827",
},
success: "#16a34a",
warning: "#f59e0b",
danger: "#dc2626",
info: "#0ea5e9",
} as const;
export type Colors = typeof colors;

View File

@@ -1,2 +0,0 @@
export * from "./colors";
export * from "./typography";

View File

@@ -1,28 +0,0 @@
export const typography = {
fontFamily: {
sans: '"Inter", "Segoe UI", sans-serif',
mono: '"Fira Code", "Menlo", monospace',
},
fontSize: {
xs: "0.75rem",
sm: "0.875rem",
base: "1rem",
lg: "1.125rem",
xl: "1.25rem",
"2xl": "1.5rem",
"3xl": "1.875rem",
},
fontWeight: {
regular: 400,
medium: 500,
semibold: 600,
bold: 700,
},
lineHeight: {
tight: 1.25,
normal: 1.5,
relaxed: 1.75,
},
} as const;
export type Typography = typeof typography;

View File

@@ -0,0 +1,22 @@
{
"extends": ["//"],
"tasks": {
"build": {
"dependsOn": ["build:styles"]
},
"build:styles": {
"outputs": ["dist/**"]
},
"dev": {
"with": ["dev:styles",]
},
"dev:styles": {
"cache": false,
"persistent": true
},
"dev:components": {
"cache": false,
"persistent": true
}
}
}

179
pnpm-lock.yaml generated
View File

@@ -622,6 +622,9 @@ importers:
'@edr/types':
specifier: workspace:*
version: link:../types
'@tanstack/react-table':
specifier: ^8.21.3
version: 8.21.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
class-variance-authority:
specifier: ^0.7.1
version: 0.7.1
@@ -650,18 +653,27 @@ importers:
'@edr/tsconfig':
specifier: workspace:*
version: link:../config/tsconfig
'@tailwindcss/cli':
specifier: ^4.3.0
version: 4.3.0
'@types/react':
specifier: ^18.3.11
version: 18.3.28
'@types/react-dom':
specifier: ^18.3.0
version: 18.3.7(@types/react@18.3.28)
postcss:
specifier: ^8.4.47
version: 8.5.14
react:
specifier: 18.3.1
version: 18.3.1
react-dom:
specifier: 18.3.1
version: 18.3.1(react@18.3.1)
tailwindcss:
specifier: ^4.3.0
version: 4.3.0
typescript:
specifier: ^5.5.4
version: 5.9.3
@@ -2041,6 +2053,88 @@ packages:
'@paralleldrive/cuid2@2.3.1':
resolution: {integrity: sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==}
'@parcel/watcher-android-arm64@2.5.6':
resolution: {integrity: sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==}
engines: {node: '>= 10.0.0'}
cpu: [arm64]
os: [android]
'@parcel/watcher-darwin-arm64@2.5.6':
resolution: {integrity: sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==}
engines: {node: '>= 10.0.0'}
cpu: [arm64]
os: [darwin]
'@parcel/watcher-darwin-x64@2.5.6':
resolution: {integrity: sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==}
engines: {node: '>= 10.0.0'}
cpu: [x64]
os: [darwin]
'@parcel/watcher-freebsd-x64@2.5.6':
resolution: {integrity: sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==}
engines: {node: '>= 10.0.0'}
cpu: [x64]
os: [freebsd]
'@parcel/watcher-linux-arm-glibc@2.5.6':
resolution: {integrity: sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==}
engines: {node: '>= 10.0.0'}
cpu: [arm]
os: [linux]
'@parcel/watcher-linux-arm-musl@2.5.6':
resolution: {integrity: sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==}
engines: {node: '>= 10.0.0'}
cpu: [arm]
os: [linux]
'@parcel/watcher-linux-arm64-glibc@2.5.6':
resolution: {integrity: sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==}
engines: {node: '>= 10.0.0'}
cpu: [arm64]
os: [linux]
'@parcel/watcher-linux-arm64-musl@2.5.6':
resolution: {integrity: sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==}
engines: {node: '>= 10.0.0'}
cpu: [arm64]
os: [linux]
'@parcel/watcher-linux-x64-glibc@2.5.6':
resolution: {integrity: sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==}
engines: {node: '>= 10.0.0'}
cpu: [x64]
os: [linux]
'@parcel/watcher-linux-x64-musl@2.5.6':
resolution: {integrity: sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==}
engines: {node: '>= 10.0.0'}
cpu: [x64]
os: [linux]
'@parcel/watcher-win32-arm64@2.5.6':
resolution: {integrity: sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==}
engines: {node: '>= 10.0.0'}
cpu: [arm64]
os: [win32]
'@parcel/watcher-win32-ia32@2.5.6':
resolution: {integrity: sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==}
engines: {node: '>= 10.0.0'}
cpu: [ia32]
os: [win32]
'@parcel/watcher-win32-x64@2.5.6':
resolution: {integrity: sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==}
engines: {node: '>= 10.0.0'}
cpu: [x64]
os: [win32]
'@parcel/watcher@2.5.6':
resolution: {integrity: sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==}
engines: {node: '>= 10.0.0'}
'@phc/format@1.0.0':
resolution: {integrity: sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==}
engines: {node: '>=10'}
@@ -3169,6 +3263,10 @@ packages:
'@tabler/icons@3.44.0':
resolution: {integrity: sha512-Wn0AOZG9sg0L+bjfMqq4eNhC6pQjIrk94LvvWYNYkY8KH8wC3YILRzQlrnVJc4FUeMxH/AK97QsYCX35H3LndA==}
'@tailwindcss/cli@4.3.0':
resolution: {integrity: sha512-X9kdlqyMopO9fewbgHsEeuy31YzMHbdZ9VsKt004tB+mxSg1CNbyhZYCzvhciN0AM4R4b5lvIprPjtNq7iQxpQ==}
hasBin: true
'@tailwindcss/node@4.3.0':
resolution: {integrity: sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==}
@@ -8203,6 +8301,10 @@ packages:
motion-utils@12.36.0:
resolution: {integrity: sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==}
mri@1.2.0:
resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==}
engines: {node: '>=4'}
ms@2.0.0:
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
@@ -8304,6 +8406,9 @@ packages:
node-abort-controller@3.1.1:
resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==}
node-addon-api@7.1.1:
resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
node-addon-api@8.7.0:
resolution: {integrity: sha512-9MdFxmkKaOYVTV+XVRG8ArDwwQ77XIgIPyKASB1k3JPq3M8fGQQQE3YpMOrKm6g//Ktx8ivZr8xo1Qmtqub+GA==}
engines: {node: ^18 || ^20 || >= 21}
@@ -12994,6 +13099,66 @@ snapshots:
dependencies:
'@noble/hashes': 1.8.0
'@parcel/watcher-android-arm64@2.5.6':
optional: true
'@parcel/watcher-darwin-arm64@2.5.6':
optional: true
'@parcel/watcher-darwin-x64@2.5.6':
optional: true
'@parcel/watcher-freebsd-x64@2.5.6':
optional: true
'@parcel/watcher-linux-arm-glibc@2.5.6':
optional: true
'@parcel/watcher-linux-arm-musl@2.5.6':
optional: true
'@parcel/watcher-linux-arm64-glibc@2.5.6':
optional: true
'@parcel/watcher-linux-arm64-musl@2.5.6':
optional: true
'@parcel/watcher-linux-x64-glibc@2.5.6':
optional: true
'@parcel/watcher-linux-x64-musl@2.5.6':
optional: true
'@parcel/watcher-win32-arm64@2.5.6':
optional: true
'@parcel/watcher-win32-ia32@2.5.6':
optional: true
'@parcel/watcher-win32-x64@2.5.6':
optional: true
'@parcel/watcher@2.5.6':
dependencies:
detect-libc: 2.1.2
is-glob: 4.0.3
node-addon-api: 7.1.1
picomatch: 4.0.4
optionalDependencies:
'@parcel/watcher-android-arm64': 2.5.6
'@parcel/watcher-darwin-arm64': 2.5.6
'@parcel/watcher-darwin-x64': 2.5.6
'@parcel/watcher-freebsd-x64': 2.5.6
'@parcel/watcher-linux-arm-glibc': 2.5.6
'@parcel/watcher-linux-arm-musl': 2.5.6
'@parcel/watcher-linux-arm64-glibc': 2.5.6
'@parcel/watcher-linux-arm64-musl': 2.5.6
'@parcel/watcher-linux-x64-glibc': 2.5.6
'@parcel/watcher-linux-x64-musl': 2.5.6
'@parcel/watcher-win32-arm64': 2.5.6
'@parcel/watcher-win32-ia32': 2.5.6
'@parcel/watcher-win32-x64': 2.5.6
'@phc/format@1.0.0': {}
'@pkgjs/parseargs@0.11.0':
@@ -14203,6 +14368,16 @@ snapshots:
'@tabler/icons@3.44.0': {}
'@tailwindcss/cli@4.3.0':
dependencies:
'@parcel/watcher': 2.5.6
'@tailwindcss/node': 4.3.0
'@tailwindcss/oxide': 4.3.0
enhanced-resolve: 5.21.3
mri: 1.2.0
picocolors: 1.1.1
tailwindcss: 4.3.0
'@tailwindcss/node@4.3.0':
dependencies:
'@jridgewell/remapping': 2.3.5
@@ -20734,6 +20909,8 @@ snapshots:
motion-utils@12.36.0: {}
mri@1.2.0: {}
ms@2.0.0: {}
ms@2.1.3: {}
@@ -20884,6 +21061,8 @@ snapshots:
node-abort-controller@3.1.1: {}
node-addon-api@7.1.1: {}
node-addon-api@8.7.0: {}
node-domexception@1.0.0: {}