Theme overhaul

This commit is contained in:
2025-07-31 18:37:33 -04:00
parent a1616b161d
commit 8a2565adad
79 changed files with 2722 additions and 3917 deletions

View File

@@ -64,7 +64,7 @@ export function AddressAutocomplete({
onBlur={() => setTimeout(() => setShowSuggestions(false), 150)}
/>
{showSuggestions && suggestions.length > 0 && (
<Card className="card-primary absolute z-10 mt-1 max-h-60 w-full overflow-auto">
<Card className="bg-card border-border border absolute z-10 mt-1 max-h-60 w-full overflow-auto">
<ul>
{suggestions.map((s) => (
<li

View File

@@ -1,26 +1,72 @@
import Image from "next/image";
"use client";
import { motion } from "framer-motion";
import { cn } from "~/lib/utils";
interface LogoProps {
className?: string;
size?: "sm" | "md" | "lg";
size?: "sm" | "md" | "lg" | "xl";
animated?: boolean;
}
export function Logo({ className, size = "md" }: LogoProps) {
export function Logo({ className, size = "md", animated = true }: LogoProps) {
const sizeClasses = {
sm: { width: 120, height: 32 },
md: { width: 160, height: 42 },
lg: { width: 240, height: 64 },
sm: "text-sm",
md: "text-lg",
lg: "text-2xl",
xl: "text-4xl",
};
const { width, height } = sizeClasses[size];
const LogoContent = () => (
<div className={cn("flex items-center", sizeClasses[size], className)}>
<span className="text-primary font-bold tracking-tight">$</span>
<span className="inline-block w-2"></span>
<span className="text-foreground font-bold tracking-tight">been</span>
<span className="text-foreground/70 font-bold tracking-tight">voice</span>
</div>
);
if (!animated) {
return <LogoContent />;
}
return (
<Image
src="/beenvoice-logo.svg"
alt="beenvoice logo"
width={width}
height={height}
className={className}
priority
/>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.1, ease: "easeOut" }}
className={cn("flex items-center", sizeClasses[size], className)}
>
<motion.span
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.02, duration: 0.05, ease: "easeOut" }}
className="text-primary font-bold tracking-tight"
>
$
</motion.span>
<motion.span
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.03, duration: 0.05, ease: "easeOut" }}
className="inline-block w-2"
></motion.span>
<motion.span
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.04, duration: 0.05, ease: "easeOut" }}
className="text-foreground font-bold tracking-tight"
>
been
</motion.span>
<motion.span
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.06, duration: 0.05, ease: "easeOut" }}
className="text-foreground/70 font-bold tracking-tight"
>
voice
</motion.span>
</motion.div>
);
}

View File

@@ -438,10 +438,10 @@ export function CSVImportPage() {
return (
<div className="space-y-6">
{/* Global Client Selection */}
<Card className="card-primary">
<Card className="bg-card border-border border">
<CardHeader>
<CardTitle className="card-title-secondary">
<Users className="text-icon-blue h-5 w-5" />
<CardTitle className="text-foreground flex items-center gap-2">
<Users className="text-primary h-5 w-5" />
Default Client
</CardTitle>
</CardHeader>
@@ -460,7 +460,7 @@ export function CSVImportPage() {
applyGlobalClient(newClientId);
}
}}
className="border-input bg-background ring-offset-background placeholder:text-muted-foreground focus:ring-ring flex h-12 w-full rounded-md border px-3 py-2 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium focus:ring-1 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
className="border-input bg-background ring-offset-background placeholder:text-muted-foreground focus:ring-ring flex h-12 w-full border px-3 py-2 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium focus:ring-1 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
disabled={loadingClients}
>
<option value="">No default client (select individually)</option>
@@ -479,10 +479,10 @@ export function CSVImportPage() {
</Card>
{/* File Upload Area */}
<Card className="card-primary">
<Card className="bg-card border-border border">
<CardHeader>
<CardTitle className="card-title-secondary">
<Upload className="text-icon-emerald h-5 w-5" />
<CardTitle className="text-foreground flex items-center gap-2">
<Upload className="text-primary h-5 w-5" />
Upload CSV Files
</CardTitle>
</CardHeader>
@@ -498,49 +498,45 @@ export function CSVImportPage() {
{/* Summary Card */}
{totalFiles > 0 && (
<Card className="card-primary">
<Card className="bg-card border-border border">
<CardHeader>
<CardTitle className="card-title-secondary">
<FileText className="text-icon-emerald h-5 w-5" />
<CardTitle className="text-foreground flex items-center gap-2">
<FileText className="text-primary h-5 w-5" />
Import Summary
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 gap-4 rounded-lg bg-green-50/50 p-4 md:grid-cols-4 dark:bg-green-900/10">
<div className="bg-primary/10 grid grid-cols-2 gap-4 p-4 md:grid-cols-4">
<div className="text-center">
<div className="text-2xl font-bold text-green-600">
<div className="text-primary text-2xl font-bold">
{totalFiles}
</div>
<div className="text-sm text-gray-600 dark:text-gray-400">
Files
</div>
<div className="text-muted-foreground text-sm">Files</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-green-600">
<div className="text-primary text-2xl font-bold">
{totalItems}
</div>
<div className="text-sm text-gray-600 dark:text-gray-400">
<div className="text-muted-foreground text-sm">
Total Items
</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-green-600">
<div className="text-primary text-2xl font-bold">
{totalAmount.toLocaleString("en-US", {
style: "currency",
currency: "USD",
})}
</div>
<div className="text-sm text-gray-600 dark:text-gray-400">
<div className="text-muted-foreground text-sm">
Total Amount
</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-green-600">
<div className="text-primary text-2xl font-bold">
{readyFiles}/{totalFiles}
</div>
<div className="text-sm text-gray-600 dark:text-gray-400">
Ready
</div>
<div className="text-muted-foreground text-sm">Ready</div>
</div>
</div>
</CardContent>
@@ -551,9 +547,9 @@ export function CSVImportPage() {
{/* File List */}
{files.length > 0 && (
<Card className="card-primary">
<Card className="bg-card border-border border">
<CardHeader>
<CardTitle className="card-title-secondary">
<CardTitle className="text-foreground flex items-center gap-2">
Uploaded Files
</CardTitle>
</CardHeader>
@@ -562,11 +558,11 @@ export function CSVImportPage() {
{files.map((fileData, index) => (
<div
key={index}
className="border-border bg-card rounded-lg border p-4"
className="border-border bg-card border p-4"
>
<div className="mb-4 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-3">
<FileText className="text-icon-emerald h-5 w-5" />
<FileText className="text-primary h-5 w-5" />
<div>
<h3 className="text-foreground truncate font-medium">
{fileData.file.name}
@@ -593,7 +589,7 @@ export function CSVImportPage() {
variant="outline"
size="sm"
onClick={() => removeFile(index)}
className="text-red-600 hover:text-red-700"
className="text-destructive hover:text-destructive/80"
>
<Trash2 className="mr-1 h-4 w-4" />
Remove
@@ -623,7 +619,7 @@ export function CSVImportPage() {
onChange={(e) =>
updateFileData(index, { clientId: e.target.value })
}
className="border-input bg-background ring-offset-background placeholder:text-muted-foreground focus:ring-ring flex h-9 w-full rounded-md border px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium focus:ring-1 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
className="border-input bg-background ring-offset-background placeholder:text-muted-foreground focus:ring-ring flex h-9 w-full border px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium focus:ring-1 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
disabled={loadingClients}
>
<option value="">Select Client</option>
@@ -666,20 +662,20 @@ export function CSVImportPage() {
{/* Error Display */}
{fileData.errors.length > 0 && (
<div className="mt-4 rounded-lg border border-red-200 bg-red-50 p-3 dark:border-red-800 dark:bg-red-900/20">
<div className="border-destructive/20 bg-destructive/10 mt-4 border p-3">
<div className="mb-2 flex items-center gap-2">
<AlertCircle className="h-4 w-4 text-red-600" />
<span className="text-sm font-medium text-red-800 dark:text-red-200">
<AlertCircle className="text-destructive h-4 w-4" />
<span className="text-destructive text-sm font-medium">
Issues Found
</span>
</div>
<ul className="space-y-1 text-sm text-red-700 dark:text-red-300">
<ul className="text-destructive space-y-1 text-sm">
{fileData.errors.map((error, errorIndex) => (
<li
key={errorIndex}
className="flex items-start gap-2"
>
<span className="text-red-600"></span>
<span className="text-destructive"></span>
<span>{error}</span>
</li>
))}
@@ -735,10 +731,10 @@ export function CSVImportPage() {
{/* Batch Actions */}
{files.length > 0 && (
<Card className="card-primary">
<Card className="bg-card border-border border">
<CardHeader>
<CardTitle className="card-title-secondary">
<DollarSign className="text-icon-green h-5 w-5" />
<CardTitle className="text-foreground flex items-center gap-2">
<DollarSign className="text-primary h-5 w-5" />
Create Invoices
</CardTitle>
</CardHeader>
@@ -762,7 +758,7 @@ export function CSVImportPage() {
<Button
onClick={processBatch}
disabled={readyFiles === 0 || isProcessing}
className="btn-brand-primary"
variant="default"
>
{isProcessing
? "Processing..."
@@ -776,10 +772,10 @@ export function CSVImportPage() {
{/* Preview Modal */}
<Dialog open={previewModalOpen} onOpenChange={setPreviewModalOpen}>
<DialogContent className="card-primary flex max-h-[90vh] max-w-4xl flex-col">
<DialogContent className="bg-card border-border border flex max-h-[90vh] max-w-4xl flex-col">
<DialogHeader className="flex-shrink-0">
<DialogTitle className="flex items-center gap-2 text-xl font-bold text-gray-800">
<FileText className="h-5 w-5 text-emerald-600" />
<DialogTitle className="text-foreground flex items-center gap-2 text-xl font-bold">
<FileText className="text-primary h-5 w-5" />
{selectedFileIndex !== null &&
files[selectedFileIndex]?.file.name}
</DialogTitle>
@@ -792,14 +788,14 @@ export function CSVImportPage() {
<div className="flex min-h-0 flex-1 flex-col space-y-4">
<div className="grid flex-shrink-0 grid-cols-1 gap-4 md:grid-cols-3">
<div className="flex items-center gap-2">
<FileText className="h-4 w-4 text-emerald-600" />
<span className="text-sm text-gray-600">
<FileText className="text-primary h-4 w-4" />
<span className="text-muted-foreground text-sm">
{files[selectedFileIndex].parsedItems.length} items
</span>
</div>
<div className="flex items-center gap-2">
<Clock className="h-4 w-4 text-emerald-600" />
<span className="text-sm text-gray-600">
<Clock className="text-primary h-4 w-4" />
<span className="text-muted-foreground text-sm">
{files[selectedFileIndex].parsedItems
.reduce((sum, item) => sum + item.hours, 0)
.toFixed(1)}{" "}
@@ -807,8 +803,8 @@ export function CSVImportPage() {
</span>
</div>
<div className="flex items-center gap-2">
<DollarSign className="h-4 w-4 text-emerald-600" />
<span className="text-sm font-medium text-gray-600">
<DollarSign className="text-primary h-4 w-4" />
<span className="text-muted-foreground text-sm font-medium">
{files[selectedFileIndex].parsedItems
.reduce((sum, item) => sum + item.amount, 0)
.toLocaleString("en-US", {

View File

@@ -73,7 +73,7 @@ export function ClientList() {
return (
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
{Array.from({ length: 3 }, (_, i: number) => (
<Card key={i} className="card-primary">
<Card key={i} className="bg-card border-border border">
<CardHeader>
<div className="h-4 animate-pulse rounded bg-gray-200" />
</CardHeader>
@@ -91,9 +91,9 @@ export function ClientList() {
if (!clients || clients.length === 0) {
return (
<Card className="card-primary">
<Card className="bg-card border-border border">
<CardHeader className="text-center">
<CardTitle className="text-brand-gradient text-2xl font-bold">
<CardTitle className="text-primary text-2xl font-bold">
No Clients Yet
</CardTitle>
<CardDescription className="text-lg">
@@ -142,20 +142,20 @@ export function ClientList() {
{filteredClients.map((client) => (
<Card
key={client.id}
className="group card-primary transition-all duration-300 hover:shadow-lg"
className="group bg-card border-border border transition-all duration-300 hover:shadow-lg"
>
<CardHeader>
<CardTitle className="flex items-center justify-between text-lg">
<span className="text-accent group-hover:text-icon-emerald font-semibold transition-colors">
<span className="text-foreground group-hover:text-primary font-semibold transition-colors">
{client.name}
</span>
<div className="flex space-x-1 opacity-0 transition-opacity group-hover:opacity-100">
<Link href={`/clients/${client.id}`}>
<Link href={`/dashboard/clients/${client.id}`}>
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
<Eye className="h-4 w-4" />
</Button>
</Link>
<Link href={`/clients/${client.id}/edit`}>
<Link href={`/dashboard/clients/${client.id}/edit`}>
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
<Edit className="h-4 w-4" />
</Button>
@@ -173,25 +173,25 @@ export function ClientList() {
</CardHeader>
<CardContent className="space-y-3">
{client.email && (
<div className="text-secondary flex items-center text-sm">
<div className="bg-brand-muted mr-3 rounded p-1.5">
<Mail className="text-icon-emerald h-3 w-3" />
<div className="text-muted-foreground flex items-center text-sm">
<div className="bg-muted mr-3 rounded p-1.5">
<Mail className="text-muted-foreground h-3 w-3" />
</div>
{client.email}
</div>
)}
{client.phone && (
<div className="text-secondary flex items-center text-sm">
<div className="bg-brand-muted-blue mr-3 rounded p-1.5">
<Phone className="text-icon-blue h-3 w-3" />
<div className="text-muted-foreground flex items-center text-sm">
<div className="bg-muted mr-3 rounded p-1.5">
<Phone className="text-muted-foreground h-3 w-3" />
</div>
{client.phone}
</div>
)}
{(client.addressLine1 ?? client.city ?? client.state) && (
<div className="text-secondary flex items-start text-sm">
<div className="bg-brand-muted-teal mt-0.5 mr-3 flex-shrink-0 rounded p-1.5">
<MapPin className="text-icon-teal h-3 w-3" />
<div className="text-muted-foreground flex items-start text-sm">
<div className="bg-muted mt-0.5 mr-3 flex-shrink-0 rounded p-1.5">
<MapPin className="text-muted-foreground h-3 w-3" />
</div>
<div className="min-w-0">
{client.addressLine1 && <div>{client.addressLine1}</div>}
@@ -213,12 +213,12 @@ export function ClientList() {
</div>
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<DialogContent className="card-primary">
<DialogContent className="bg-card border-border border">
<DialogHeader>
<DialogTitle className="text-accent text-xl font-bold">
<DialogTitle className="text-foreground text-xl font-bold">
Delete Client
</DialogTitle>
<DialogDescription className="text-secondary">
<DialogDescription className="text-muted-foreground">
Are you sure you want to delete this client? This action cannot be
undone.
</DialogDescription>
@@ -227,14 +227,14 @@ export function ClientList() {
<Button
variant="outline"
onClick={() => setDeleteDialogOpen(false)}
className="text-secondary"
className="text-muted-foreground"
>
Cancel
</Button>
<Button
variant="destructive"
onClick={confirmDelete}
className="bg-red-600 hover:bg-red-700"
className="bg-destructive hover:bg-destructive/90"
>
Delete
</Button>

View File

@@ -28,10 +28,10 @@ export function CurrentOpenInvoiceCard() {
if (isLoading) {
return (
<Card className="card-primary">
<Card className="bg-card border-border border">
<CardHeader className="pb-3">
<CardTitle className="card-title-secondary">
<FileText className="text-icon-emerald h-5 w-5" />
<CardTitle className="text-foreground flex items-center gap-2">
<FileText className="text-primary h-5 w-5" />
Current Open Invoice
</CardTitle>
</CardHeader>
@@ -49,10 +49,10 @@ export function CurrentOpenInvoiceCard() {
if (!currentInvoice) {
return (
<Card className="card-primary">
<Card className="bg-card border-border border">
<CardHeader className="pb-3">
<CardTitle className="card-title-secondary">
<FileText className="text-icon-emerald h-5 w-5" />
<CardTitle className="text-foreground flex items-center gap-2">
<FileText className="text-primary h-5 w-5" />
Current Open Invoice
</CardTitle>
</CardHeader>
@@ -80,10 +80,10 @@ export function CurrentOpenInvoiceCard() {
const totalAmount = currentInvoice.totalAmount;
return (
<Card className="card-primary">
<Card className="bg-card border-border border">
<CardHeader className="pb-3">
<CardTitle className="card-title-secondary">
<FileText className="text-icon-emerald h-5 w-5" />
<CardTitle className="text-foreground flex items-center gap-2">
<FileText className="text-primary h-5 w-5" />
Current Open Invoice
</CardTitle>
</CardHeader>
@@ -91,13 +91,13 @@ export function CurrentOpenInvoiceCard() {
<div className="space-y-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Badge className="badge-secondary text-xs">
<Badge className="bg-secondary text-secondary-foreground text-xs">
{currentInvoice.invoiceNumber}
</Badge>
<Badge className="badge-outline text-xs">Draft</Badge>
<Badge className="border text-xs">Draft</Badge>
</div>
<div className="text-right">
<p className="text-icon-emerald text-sm font-medium">
<p className="text-primary text-sm font-medium">
{formatCurrency(totalAmount)}
</p>
</div>

View File

@@ -215,12 +215,12 @@ export function DataTable<TData, TValue>({
{/* Filter Bar Card */}
{(showSearch || filterableColumns.length > 0 || showColumnVisibility) && (
<Card className="card-primary py-2">
<Card className="bg-card border-border border py-2">
<CardContent className="px-3 py-0">
<div className="flex items-center gap-2">
{showSearch && (
<div className="relative min-w-0 flex-1">
<Search className="text-muted-foreground absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2" />
<Search className="text-foreground absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2" />
<Input
placeholder={searchPlaceholder}
value={globalFilter ?? ""}
@@ -244,7 +244,7 @@ export function DataTable<TData, TValue>({
>
<SelectTrigger className="h-9 w-9 p-0 sm:w-[180px] sm:px-3 [&>svg]:hidden sm:[&>svg]:inline-flex">
<div className="flex w-full items-center justify-center">
<Filter className="h-4 w-4 sm:hidden" />
<Filter className="text-foreground h-4 w-4 sm:hidden" />
<span className="hidden sm:inline">
<SelectValue placeholder={column.title} />
</span>
@@ -272,7 +272,7 @@ export function DataTable<TData, TValue>({
>
<X className="h-4 w-4 sm:hidden" />
<span className="hidden sm:flex sm:items-center">
<Filter className="mr-2 h-3.5 w-3.5" />
<Filter className="text-foreground mr-2 h-3.5 w-3.5" />
Clear filters
</span>
</Button>
@@ -315,7 +315,7 @@ export function DataTable<TData, TValue>({
)}
{/* Table Content Card */}
<Card className="card-primary overflow-hidden p-0">
<Card className="bg-card border-border overflow-hidden border p-0">
<div className="w-full overflow-x-auto">
<Table>
<TableHeader>
@@ -400,7 +400,7 @@ export function DataTable<TData, TValue>({
{/* Pagination Bar Card */}
{showPagination && (
<Card className="card-primary py-2">
<Card className="bg-card border-border border py-2">
<CardContent className="px-3 py-0">
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2">
@@ -561,17 +561,17 @@ export function DataTableSkeleton({
return (
<div className="space-y-4">
{/* Filter bar skeleton */}
<Card className="card-primary py-2">
<Card className="bg-card border-border border py-2">
<CardContent className="px-3 py-0">
<div className="flex items-center gap-2">
<div className="bg-muted/30 h-9 w-full flex-1 animate-pulse rounded-md sm:max-w-sm"></div>
<div className="bg-muted/30 h-9 w-24 animate-pulse rounded-md"></div>
<div className="bg-muted/30 h-9 w-full flex-1 animate-pulse sm:max-w-sm"></div>
<div className="bg-muted/30 h-9 w-24 animate-pulse"></div>
</div>
</CardContent>
</Card>
{/* Table skeleton */}
<Card className="card-primary overflow-hidden p-0">
<Card className="bg-card border-border overflow-hidden border p-0">
<div className="w-full overflow-x-auto">
<Table>
<TableHeader>
@@ -632,7 +632,7 @@ export function DataTableSkeleton({
</Card>
{/* Pagination skeleton */}
<Card className="card-primary py-2">
<Card className="bg-card border-border border py-2">
<CardContent className="px-3 py-0">
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2">

View File

@@ -87,7 +87,7 @@ function SortableItem({
<div
ref={setNodeRef}
style={style}
className={`card-secondary rounded-lg transition-colors ${
className={`card-secondary transition-colors ${
isDragging ? "opacity-50 shadow-lg" : ""
}`}
>
@@ -154,7 +154,7 @@ function SortableItem({
{/* Amount */}
<div className="col-span-1">
<div className="bg-muted/30 flex h-9 items-center rounded-md border px-3 font-medium text-emerald-600">
<div className="bg-muted/30 text-primary flex h-9 items-center border px-3 font-medium">
${item.amount.toFixed(2)}
</div>
</div>
@@ -166,7 +166,7 @@ function SortableItem({
onClick={() => onRemove(index)}
variant="ghost"
size="sm"
className="h-9 w-9 p-0 text-red-600 hover:bg-red-50 hover:text-red-700"
className="text-destructive hover:bg-destructive/10 hover:text-destructive/80 h-9 w-9 p-0"
>
<Trash2 className="h-4 w-4" />
</Button>
@@ -206,7 +206,7 @@ function SortableItem({
onClick={() => onRemove(index)}
variant="ghost"
size="sm"
className="h-6 w-6 p-0 text-red-600 hover:bg-red-50 hover:text-red-700"
className="text-destructive hover:bg-destructive/10 hover:text-destructive/80 h-6 w-6 p-0"
>
<Trash2 className="h-3 w-3" />
</Button>
@@ -266,10 +266,10 @@ function SortableItem({
</div>
{/* Amount */}
<div className="bg-muted/20 rounded-md border p-3">
<div className="bg-muted/20 border p-3">
<div className="flex items-center justify-between">
<span className="text-muted-foreground text-sm">Total Amount:</span>
<span className="font-mono text-lg font-bold text-emerald-600">
<span className="text-primary font-mono text-lg font-bold">
${item.amount.toFixed(2)}
</span>
</div>
@@ -362,7 +362,7 @@ export function EditableInvoiceItems({
{items.map((item, _index) => (
<div
key={item.id}
className="card-secondary animate-pulse rounded-lg p-4"
className="card-secondary animate-pulse p-4"
>
{/* Desktop Skeleton */}
<div className="hidden grid-cols-12 gap-3 md:grid">

View File

@@ -150,12 +150,12 @@ export function InvoiceList() {
<CardTitle className="flex items-center justify-between">
<span className="truncate">{invoice.invoiceNumber}</span>
<div className="flex space-x-1">
<Link href={`/dashboard/invoices/${invoice.id}/view`}>
<Link href={`/dashboard/invoices/${invoice.id}`}>
<Button variant="ghost" size="sm">
<Eye className="h-4 w-4" />
</Button>
</Link>
<Link href={`/dashboard/invoices/${invoice.id}`}>
<Link href={`/dashboard/invoices/${invoice.id}/edit`}>
<Button variant="ghost" size="sm">
<Edit className="h-4 w-4" />
</Button>
@@ -171,7 +171,7 @@ export function InvoiceList() {
</CardTitle>
<div className="flex items-center justify-between">
<StatusBadge status={invoice.status as StatusType} />
<span className="text-icon-green text-lg font-bold">
<span className="text-primary text-lg font-bold">
{formatCurrency(invoice.totalAmount)}
</span>
</div>

View File

@@ -148,7 +148,7 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
return (
<div className="py-12 text-center">
<FileText className="text-muted mx-auto mb-4 h-12 w-12" />
<h3 className="text-accent mb-2 text-lg font-medium">
<h3 className="text-foreground mb-2 text-lg font-medium">
Invoice not found
</h3>
<p className="text-muted mb-4">
@@ -169,9 +169,9 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
<div className="space-y-6">
{/* Status Alert */}
{isOverdue && (
<Card className="border-red-200 bg-red-50 dark:border-red-800 dark:bg-red-900/20">
<Card className="border-destructive/20 bg-destructive/10">
<CardContent className="p-4">
<div className="text-error flex items-center gap-2">
<div className="text-destructive flex items-center gap-2">
<AlertCircle className="h-5 w-5" />
<span className="font-medium">This invoice is overdue</span>
</div>
@@ -183,13 +183,13 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
{/* Main Content */}
<div className="space-y-6 lg:col-span-2">
{/* Invoice Header Card */}
<Card className="card-primary">
<Card className="bg-card border-border border">
<CardContent>
<div className="flex items-start justify-between">
<div className="space-y-4">
<div className="flex items-center gap-3">
<div className="rounded-lg bg-emerald-100 p-2 dark:bg-emerald-900/30">
<FileText className="h-6 w-6 text-emerald-600 dark:text-emerald-400" />
<div className="bg-primary/10 p-2">
<FileText className="text-primary h-6 w-6" />
</div>
<div>
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">
@@ -228,7 +228,7 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
>
<StatusIcon className="mr-1 h-3 w-3" />
</StatusBadge>
<div className="text-3xl font-bold text-emerald-600 dark:text-emerald-400">
<div className="text-primary text-3xl font-bold">
{formatCurrency(invoice.totalAmount)}
</div>
<Button
@@ -239,7 +239,7 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
>
{isExportingPDF ? (
<>
<div className="mr-2 h-4 w-4 animate-spin rounded-full border-2 border-white border-t-transparent" />
<div className="mr-2 h-4 w-4 animate-spin border-2 border-white border-t-transparent" />
Generating PDF...
</>
) : (
@@ -255,9 +255,9 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
</Card>
{/* Client Information */}
<Card className="card-primary">
<Card className="bg-card border-border border">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-emerald-700 dark:text-emerald-400">
<CardTitle className="text-primary flex items-center gap-2">
<User className="h-5 w-5" />
Bill To
</CardTitle>
@@ -318,31 +318,31 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
</Card>
{/* Invoice Items */}
<Card className="card-primary">
<Card className="bg-card border-border border">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-emerald-700 dark:text-emerald-400">
<CardTitle className="text-primary flex items-center gap-2">
<Clock className="h-5 w-5" />
Invoice Items
</CardTitle>
</CardHeader>
<CardContent>
<div className="overflow-hidden rounded-lg border border-gray-200 dark:border-gray-700">
<div className="border-border overflow-hidden border">
<table className="w-full">
<thead className="bg-gray-50 dark:bg-gray-700">
<thead className="bg-muted">
<tr>
<th className="px-4 py-3 text-left text-sm font-semibold text-gray-700 dark:text-gray-300">
<th className="text-muted-foreground px-4 py-3 text-left text-sm font-semibold">
Date
</th>
<th className="px-4 py-3 text-left text-sm font-semibold text-gray-700 dark:text-gray-300">
<th className="text-muted-foreground px-4 py-3 text-left text-sm font-semibold">
Description
</th>
<th className="px-4 py-3 text-right text-sm font-semibold text-gray-700 dark:text-gray-300">
<th className="text-muted-foreground px-4 py-3 text-right text-sm font-semibold">
Hours
</th>
<th className="px-4 py-3 text-right text-sm font-semibold text-gray-700 dark:text-gray-300">
<th className="text-muted-foreground px-4 py-3 text-right text-sm font-semibold">
Rate
</th>
<th className="px-4 py-3 text-right text-sm font-semibold text-gray-700 dark:text-gray-300">
<th className="text-muted-foreground px-4 py-3 text-right text-sm font-semibold">
Amount
</th>
</tr>
@@ -351,21 +351,21 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
{invoice.items?.map((item, index) => (
<tr
key={item.id || index}
className="border-t border-gray-100 hover:bg-gray-50 dark:border-gray-600 dark:hover:bg-gray-700"
className="border-border hover:bg-muted/50 border-t"
>
<td className="px-4 py-3 text-sm text-gray-900 dark:text-gray-300">
<td className="text-foreground px-4 py-3 text-sm">
{formatDate(item.date)}
</td>
<td className="px-4 py-3 text-sm text-gray-900 dark:text-gray-300">
<td className="text-foreground px-4 py-3 text-sm">
{item.description}
</td>
<td className="px-4 py-3 text-right text-sm text-gray-900 dark:text-gray-300">
<td className="text-foreground px-4 py-3 text-right text-sm">
{item.hours}
</td>
<td className="px-4 py-3 text-right text-sm text-gray-900 dark:text-gray-300">
<td className="text-foreground px-4 py-3 text-right text-sm">
{formatCurrency(item.rate)}
</td>
<td className="px-4 py-3 text-right text-sm font-medium text-gray-900 dark:text-gray-300">
<td className="text-foreground px-4 py-3 text-right text-sm font-medium">
{formatCurrency(item.amount)}
</td>
</tr>
@@ -378,11 +378,9 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
{/* Notes */}
{invoice.notes && (
<Card className="card-primary">
<Card className="bg-card border-border border">
<CardHeader>
<CardTitle className="text-emerald-700 dark:text-emerald-400">
Notes
</CardTitle>
<CardTitle className="text-primary">Notes</CardTitle>
</CardHeader>
<CardContent>
<p className="whitespace-pre-wrap text-gray-700 dark:text-gray-300">
@@ -396,18 +394,16 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
{/* Sidebar */}
<div className="space-y-6">
{/* Status Actions */}
<Card className="card-primary">
<Card className="bg-card border-border border">
<CardHeader>
<CardTitle className="text-emerald-700 dark:text-emerald-400">
Status Actions
</CardTitle>
<CardTitle className="text-primary">Status Actions</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{invoice.status === "draft" && (
<Button
onClick={() => handleStatusUpdate("sent")}
disabled={updateStatus.isPending}
className="w-full bg-blue-600 text-white hover:bg-blue-700"
className="bg-primary text-primary-foreground hover:bg-primary/90 w-full"
>
<Send className="mr-2 h-4 w-4" />
Mark as Sent
@@ -418,7 +414,7 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
<Button
onClick={() => handleStatusUpdate("paid")}
disabled={updateStatus.isPending}
className="w-full bg-green-600 text-white hover:bg-green-700"
className="bg-primary text-primary-foreground hover:bg-primary/90 w-full"
>
<DollarSign className="mr-2 h-4 w-4" />
Mark as Paid
@@ -429,7 +425,7 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
<Button
onClick={() => handleStatusUpdate("paid")}
disabled={updateStatus.isPending}
className="w-full bg-green-600 text-white hover:bg-green-700"
className="bg-primary text-primary-foreground hover:bg-primary/90 w-full"
>
<DollarSign className="mr-2 h-4 w-4" />
Mark as Paid
@@ -438,21 +434,17 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
{invoice.status === "paid" && (
<div className="py-4 text-center">
<DollarSign className="mx-auto mb-2 h-8 w-8 text-green-600 dark:text-green-400" />
<p className="font-medium text-green-600 dark:text-green-400">
Invoice Paid
</p>
<DollarSign className="text-primary mx-auto mb-2 h-8 w-8" />
<p className="text-primary font-medium">Invoice Paid</p>
</div>
)}
</CardContent>
</Card>
{/* Invoice Summary */}
<Card className="card-primary">
<Card className="bg-card border-border border">
<CardHeader>
<CardTitle className="text-emerald-700 dark:text-emerald-400">
Summary
</CardTitle>
<CardTitle className="text-primary">Summary</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-3">
@@ -471,14 +463,14 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
<Separator />
<div className="flex justify-between text-lg font-bold">
<span className="dark:text-white">Total</span>
<span className="text-emerald-600 dark:text-emerald-400">
<span className="text-primary">
{formatCurrency(invoice.totalAmount)}
</span>
</div>
</div>
<div className="border-t border-gray-200 pt-4 text-center dark:border-gray-700">
<p className="text-sm text-gray-500 dark:text-gray-400">
<div className="border-border border-t pt-4 text-center">
<p className="text-muted-foreground text-sm">
{invoice.items?.length ?? 0} item
{invoice.items?.length !== 1 ? "s" : ""}
</p>
@@ -487,17 +479,15 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
</Card>
{/* Danger Zone */}
<Card className="card-primary border-red-200 dark:border-red-800">
<Card className="bg-card border-border border border-destructive/20">
<CardHeader>
<CardTitle className="text-red-700 dark:text-red-400">
Danger Zone
</CardTitle>
<CardTitle className="text-destructive">Danger Zone</CardTitle>
</CardHeader>
<CardContent>
<Button
onClick={handleDelete}
variant="outline"
className="w-full border-red-200 text-red-700 hover:bg-red-50 dark:border-red-800 dark:text-red-400 dark:hover:bg-red-900/20"
className="border-destructive/20 text-destructive hover:bg-destructive/10 w-full"
>
<Trash2 className="mr-2 h-4 w-4" />
Delete Invoice
@@ -509,7 +499,7 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
{/* Delete Confirmation Dialog */}
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<DialogContent className="card-primary">
<DialogContent className="bg-card border-border border">
<DialogHeader>
<DialogTitle className="text-xl font-bold text-gray-800 dark:text-white">
Delete Invoice
@@ -524,7 +514,7 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
<Button
variant="outline"
onClick={() => setDeleteDialogOpen(false)}
className="border-gray-300 text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-800"
className="border-border text-muted-foreground hover:bg-muted"
>
Cancel
</Button>
@@ -532,7 +522,7 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
variant="destructive"
onClick={confirmDelete}
disabled={deleteInvoice.isPending}
className="bg-red-600 hover:bg-red-700"
className="bg-destructive hover:bg-destructive/90"
>
{deleteInvoice.isPending ? "Deleting..." : "Delete Invoice"}
</Button>

View File

@@ -22,8 +22,8 @@ const variantStyles = {
background: "bg-muted/50",
},
success: {
icon: "text-status-success",
background: "bg-status-success-muted",
icon: "text-primary",
background: "bg-primary/10",
},
warning: {
icon: "text-status-warning",
@@ -67,9 +67,7 @@ export function StatsCard({
<span
className={cn(
"text-sm font-medium",
trend.isPositive
? "text-status-success"
: "text-status-error",
trend.isPositive ? "text-primary" : "text-destructive",
)}
>
{trend.isPositive ? "+" : ""}
@@ -82,7 +80,7 @@ export function StatsCard({
)}
</div>
{Icon && (
<div className={cn("rounded-full p-3", styles.background)}>
<div className={cn(" p-3", styles.background)}>
<Icon className={cn("h-6 w-6", styles.icon)} />
</div>
)}
@@ -94,7 +92,7 @@ export function StatsCard({
export function StatsCardSkeleton() {
return (
<Card className="card-primary">
<Card className="bg-card border-border border">
<CardContent className="p-6">
<div className="animate-pulse">
<div className="bg-muted mb-2 h-4 w-1/2 rounded"></div>

View File

@@ -19,19 +19,15 @@ interface StatusBadgeProps
}
const statusClassMap: Record<StatusType, string> = {
draft:
"border-slate-400 bg-slate-100/90 text-slate-800 shadow-md dark:border-slate-600 dark:bg-slate-700/90 dark:text-slate-200",
sent: "border-blue-400 bg-blue-100/90 text-blue-800 shadow-md dark:border-blue-600 dark:bg-blue-700/90 dark:text-blue-200",
paid: "border-green-400 bg-green-100/90 text-green-800 shadow-md dark:border-green-600 dark:bg-green-700/90 dark:text-green-200",
overdue:
"border-red-400 bg-red-100/90 text-red-800 shadow-md dark:border-red-600 dark:bg-red-700/90 dark:text-red-200",
success:
"border-green-400 bg-green-100/90 text-green-800 shadow-md dark:border-green-600 dark:bg-green-700/90 dark:text-green-200",
draft: "border-muted-foreground/40 bg-muted text-muted-foreground shadow-sm",
sent: "border-primary/40 bg-primary/10 text-primary shadow-sm",
paid: "border-primary/40 bg-primary/10 text-primary shadow-sm",
overdue: "border-destructive/40 bg-destructive/10 text-destructive shadow-sm",
success: "border-primary/40 bg-primary/10 text-primary shadow-sm",
warning:
"border-yellow-400 bg-yellow-100/90 text-yellow-800 shadow-md dark:border-yellow-600 dark:bg-yellow-700/90 dark:text-yellow-200",
error:
"border-red-400 bg-red-100/90 text-red-800 shadow-md dark:border-red-600 dark:bg-red-700/90 dark:text-red-200",
info: "border-blue-400 bg-blue-100/90 text-blue-800 shadow-md dark:border-blue-600 dark:bg-blue-700/90 dark:text-blue-200",
"border-muted-foreground/40 bg-muted text-muted-foreground shadow-sm",
error: "border-destructive/40 bg-destructive/10 text-destructive shadow-sm",
info: "border-primary/40 bg-primary/10 text-primary shadow-sm",
};
const statusLabelMap: Record<StatusType, string> = {

View File

@@ -414,7 +414,8 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
type="submit"
form="business-form"
disabled={isSubmitting}
className="btn-brand-primary shadow-md"
variant="default"
className="shadow-md"
>
{isSubmitting ? (
<>
@@ -438,11 +439,11 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
{/* Main Form Container - styled like data table */}
<div className="space-y-4">
{/* Basic Information */}
<Card className="card-primary">
<Card className="bg-card border-border border">
<CardHeader>
<div className="flex items-center gap-3">
<div className="bg-brand-muted flex h-10 w-10 items-center justify-center rounded-lg">
<Building className="text-brand-light h-5 w-5" />
<div className="bg-muted flex h-10 w-10 items-center justify-center ">
<Building className="text-muted-foreground h-5 w-5" />
</div>
<div>
<CardTitle>Basic Information</CardTitle>
@@ -565,12 +566,12 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
</Card>
{/* Address */}
<Card className="card-primary">
<Card className="bg-card border-border border">
<CardHeader>
<div className="flex items-center gap-3">
<div className="bg-brand-muted flex h-10 w-10 items-center justify-center rounded-lg">
<div className="bg-muted flex h-10 w-10 items-center justify-center ">
<svg
className="text-brand-light h-5 w-5"
className="text-muted-foreground h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
@@ -613,11 +614,11 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
</Card>
{/* Email Configuration */}
<Card className="card-primary">
<Card className="bg-card border-border border">
<CardHeader>
<div className="flex items-center gap-3">
<div className="bg-brand-muted flex h-10 w-10 items-center justify-center rounded-lg">
<Mail className="text-brand-light h-5 w-5" />
<div className="bg-muted flex h-10 w-10 items-center justify-center ">
<Mail className="text-muted-foreground h-5 w-5" />
</div>
<div>
<CardTitle>Email Configuration</CardTitle>
@@ -631,7 +632,7 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
<CardContent className="space-y-6">
{/* Current Status */}
{mode === "edit" && (
<div className="flex items-center justify-between rounded-lg bg-gray-50 p-4">
<div className="flex items-center justify-between bg-gray-50 p-4">
<div className="flex items-center gap-2">
<span className="text-sm font-medium">
Current Status:
@@ -639,7 +640,7 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
{emailConfig?.hasApiKey && emailConfig?.resendDomain ? (
<Badge
variant="default"
className="bg-green-100 text-green-800"
className="bg-primary/10 text-primary"
>
<Key className="mr-1 h-3 w-3" />
Custom Configuration Active
@@ -670,7 +671,7 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
href="https://resend.com"
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline"
className="text-primary hover:underline"
>
resend.com
</a>
@@ -709,7 +710,9 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
? "Enter new API key to update"
: "re_..."
}
className={errors.resendApiKey ? "border-red-500" : ""}
className={
errors.resendApiKey ? "border-destructive" : ""
}
/>
<Button
type="button"
@@ -726,7 +729,7 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
</Button>
</div>
{errors.resendApiKey && (
<p className="text-sm text-red-600">
<p className="text-destructive text-sm">
{errors.resendApiKey}
</p>
)}
@@ -749,10 +752,12 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
handleInputChange("resendDomain", e.target.value)
}
placeholder="yourdomain.com"
className={errors.resendDomain ? "border-red-500" : ""}
className={
errors.resendDomain ? "border-destructive" : ""
}
/>
{errors.resendDomain && (
<p className="text-sm text-red-600">
<p className="text-destructive text-sm">
{errors.resendDomain}
</p>
)}
@@ -779,10 +784,12 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
handleInputChange("emailFromName", e.target.value)
}
placeholder={formData.name || "Your Business Name"}
className={errors.emailFromName ? "border-red-500" : ""}
className={
errors.emailFromName ? "border-destructive" : ""
}
/>
{errors.emailFromName && (
<p className="text-sm text-red-600">
<p className="text-destructive text-sm">
{errors.emailFromName}
</p>
)}
@@ -796,11 +803,11 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
</Card>
{/* Settings */}
<Card className="card-primary">
<Card className="bg-card border-border border">
<CardHeader>
<div className="flex items-center gap-3">
<div className="bg-brand-muted flex h-10 w-10 items-center justify-center rounded-lg">
<Star className="text-brand-light h-5 w-5" />
<div className="bg-muted flex h-10 w-10 items-center justify-center ">
<Star className="text-muted-foreground h-5 w-5" />
</div>
<div>
<CardTitle>Settings</CardTitle>
@@ -811,7 +818,7 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
</div>
</CardHeader>
<CardContent className="space-y-4">
<div className="bg-brand-muted border-border/40 flex items-center justify-between rounded-xl border p-4">
<div className="bg-muted border-border/40 flex items-center justify-between border p-4">
<div className="space-y-0.5">
<Label
htmlFor="isDefault"
@@ -841,8 +848,8 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
<FloatingActionBar
leftContent={
<div className="flex items-center space-x-3">
<div className="rounded-lg bg-blue-100 p-2 dark:bg-blue-900/30">
<FileText className="h-5 w-5 text-blue-600 dark:text-blue-400" />
<div className="bg-primary/10 p-2">
<FileText className="text-primary h-5 w-5" />
</div>
<div>
<p className="font-medium text-gray-900 dark:text-gray-100">
@@ -873,7 +880,8 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
<Button
onClick={handleSubmit}
disabled={isSubmitting || !isDirty}
className="btn-brand-primary shadow-md"
variant="default"
className="shadow-md"
size="sm"
>
{isSubmitting ? (

View File

@@ -262,7 +262,8 @@ export function ClientForm({ clientId, mode }: ClientFormProps) {
type="submit"
form="client-form"
disabled={isSubmitting}
className="btn-brand-primary shadow-md"
variant="default"
className="shadow-md"
>
{isSubmitting ? (
<>
@@ -286,11 +287,11 @@ export function ClientForm({ clientId, mode }: ClientFormProps) {
{/* Main Form Container - styled like data table */}
<div className="space-y-4">
{/* Basic Information */}
<Card className="card-primary">
<Card className="bg-card border-border border">
<CardHeader>
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-gradient-to-r from-emerald-600/10 to-teal-600/10">
<UserPlus className="h-5 w-5 text-emerald-700 dark:text-emerald-400" />
<div className="bg-primary/10 flex h-10 w-10 items-center justify-center ">
<UserPlus className="text-primary h-5 w-5" />
</div>
<div>
<CardTitle>Basic Information</CardTitle>
@@ -367,12 +368,12 @@ export function ClientForm({ clientId, mode }: ClientFormProps) {
</Card>
{/* Address */}
<Card className="card-primary">
<Card className="bg-card border-border border">
<CardHeader>
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-gradient-to-r from-emerald-600/10 to-teal-600/10">
<div className="bg-primary/10 flex h-10 w-10 items-center justify-center ">
<svg
className="h-5 w-5 text-emerald-700 dark:text-emerald-400"
className="text-primary h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
@@ -415,11 +416,11 @@ export function ClientForm({ clientId, mode }: ClientFormProps) {
</Card>
{/* Billing Information */}
<Card className="card-primary">
<Card className="bg-card border-border border">
<CardHeader>
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-gradient-to-r from-emerald-600/10 to-teal-600/10">
<DollarSign className="h-5 w-5 text-emerald-700 dark:text-emerald-400" />
<div className="bg-primary/10 flex h-10 w-10 items-center justify-center ">
<DollarSign className="text-primary h-5 w-5" />
</div>
<div>
<CardTitle>Billing Information</CardTitle>
@@ -463,8 +464,8 @@ export function ClientForm({ clientId, mode }: ClientFormProps) {
<FloatingActionBar
leftContent={
<div className="flex items-center space-x-3">
<div className="rounded-lg bg-blue-100 p-2 dark:bg-blue-900/30">
<FileText className="h-5 w-5 text-blue-600 dark:text-blue-400" />
<div className="bg-primary/10 p-2">
<FileText className="text-primary h-5 w-5" />
</div>
<div>
<p className="font-medium text-gray-900 dark:text-gray-100">
@@ -495,7 +496,7 @@ export function ClientForm({ clientId, mode }: ClientFormProps) {
<Button
onClick={handleSubmit}
disabled={isSubmitting || !isDirty}
className="bg-gradient-to-r from-emerald-600 to-teal-600 shadow-md transition-all duration-200 hover:from-emerald-700 hover:to-teal-700 hover:shadow-lg"
variant="default"
size="sm"
>
{isSubmitting ? (

View File

@@ -101,7 +101,7 @@ export function EmailComposer({
editorProps: {
attributes: {
class:
"prose prose-sm sm:prose lg:prose-lg xl:prose-2xl mx-auto focus:outline-none min-h-[120px] p-4 border rounded-md bg-background",
"prose prose-sm sm:prose lg:prose-lg xl:prose-2xl mx-auto focus:outline-none min-h-[120px] p-4 border bg-background",
},
},
});
@@ -133,9 +133,9 @@ export function EmailComposer({
if (!editor) {
return (
<div className="bg-muted flex h-[200px] items-center justify-center rounded-md border">
<div className="bg-muted flex h-[200px] items-center justify-center border">
<div className="text-center">
<div className="border-primary mx-auto mb-2 h-4 w-4 animate-spin rounded-full border-2 border-t-transparent"></div>
<div className="border-primary mx-auto mb-2 h-4 w-4 animate-spin border-2 border-t-transparent"></div>
<p className="text-muted-foreground text-sm">Loading editor...</p>
</div>
</div>
@@ -145,7 +145,7 @@ export function EmailComposer({
return (
<div className={className}>
{/* Email Headers */}
<div className="bg-muted/20 space-y-4 rounded-lg border p-4">
<div className="bg-muted/20 space-y-4 border p-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="from-email" className="text-sm font-medium">
@@ -231,7 +231,7 @@ export function EmailComposer({
</div>
{/* Editor Toolbar */}
<div className="bg-muted/20 flex flex-wrap items-center gap-1 rounded-lg border p-2">
<div className="bg-muted/20 flex flex-wrap items-center gap-1 border p-2">
<MenuButton
onClick={() => editor.chain().focus().toggleBold().run()}
isActive={editor.isActive("bold")}

View File

@@ -95,7 +95,7 @@ export function EmailPreview({
return (
<div className={className}>
{/* Email Headers */}
<div className="bg-muted/20 mb-4 space-y-3 rounded-lg p-4">
<div className="bg-muted/20 mb-4 space-y-3 p-4">
<div className="grid grid-cols-1 gap-3 text-sm md:grid-cols-3">
<div>
<span className="text-muted-foreground block text-xs font-medium">
@@ -142,7 +142,7 @@ export function EmailPreview({
{/* Email Content */}
{emailTemplate ? (
<div className="rounded-lg border bg-gray-50 p-1 shadow-sm">
<div className=" border bg-gray-50 p-1 shadow-sm">
<iframe
srcDoc={emailTemplate.html}
className="h-[700px] w-full rounded border-0"

View File

@@ -34,9 +34,9 @@ function FilePreview({
const getStatusIcon = () => {
switch (status) {
case "success":
return <CheckCircle className="h-4 w-4 text-green-600" />;
return <CheckCircle className="text-primary h-4 w-4" />;
case "error":
return <AlertCircle className="h-4 w-4 text-red-600" />;
return <AlertCircle className="text-destructive h-4 w-4" />;
default:
return <FileText className="h-4 w-4 text-gray-400" />;
}
@@ -45,9 +45,9 @@ function FilePreview({
const getStatusColor = () => {
switch (status) {
case "success":
return "border-green-200 bg-green-50";
return "border-primary/20 bg-primary/10";
case "error":
return "border-red-200 bg-red-50";
return "border-destructive/20 bg-destructive/10";
default:
return "border-gray-200 bg-gray-50";
}
@@ -56,20 +56,20 @@ function FilePreview({
return (
<div
className={cn(
"flex items-center justify-between rounded-lg border p-3",
"flex items-center justify-between border p-3",
getStatusColor(),
)}
>
<div className="flex items-center gap-3">
{getStatusIcon()}
<div className="min-w-0 flex-1">
<p className="truncate text-sm font-medium text-gray-900">
<p className="text-foreground truncate text-sm font-medium">
{file.name}
</p>
<p className="text-xs text-gray-500">
<p className="text-muted-foreground text-xs">
{(file.size / 1024 / 1024).toFixed(2)} MB
</p>
{error && <p className="mt-1 text-xs text-red-600">{error}</p>}
{error && <p className="text-destructive mt-1 text-xs">{error}</p>}
</div>
</div>
<Button
@@ -152,28 +152,28 @@ export function FileUpload({
<div
{...getRootProps()}
className={cn(
"cursor-pointer rounded-lg border-2 border-dashed p-8 text-center transition-colors",
"hover:border-emerald-400 hover:bg-emerald-50/50",
isDragActive && "border-emerald-400 bg-emerald-50/50",
isDragReject && "border-red-400 bg-red-50/50",
"cursor-pointer border-2 border-dashed p-8 text-center transition-colors",
"hover:border-primary/40 hover:bg-primary/10",
isDragActive && "border-primary/40 bg-primary/10",
isDragReject && "border-destructive/40 bg-destructive/10",
disabled && "cursor-not-allowed opacity-50",
"bg-white/80 backdrop-blur-sm",
"bg-background/80 backdrop-blur-sm",
)}
>
<input {...getInputProps()} />
<div className="flex flex-col items-center gap-4">
<div
className={cn(
"rounded-full p-3 transition-colors",
isDragActive ? "bg-emerald-100" : "bg-gray-100",
isDragReject && "bg-red-100",
" p-3 transition-colors",
isDragActive ? "bg-primary/10" : "bg-muted",
isDragReject && "bg-destructive/10",
)}
>
<Upload
className={cn(
"h-6 w-6 transition-colors",
isDragActive ? "text-emerald-600" : "text-gray-400",
isDragReject && "text-red-600",
isDragActive ? "text-primary" : "text-muted-foreground",
isDragReject && "text-destructive",
)}
/>
</div>
@@ -181,8 +181,8 @@ export function FileUpload({
<p
className={cn(
"text-lg font-medium transition-colors",
isDragActive ? "text-emerald-600" : "text-gray-900",
isDragReject && "text-red-600",
isDragActive ? "text-primary" : "text-foreground",
isDragReject && "text-destructive",
)}
>
{isDragActive
@@ -222,17 +222,17 @@ export function FileUpload({
{/* Error Summary */}
{Object.keys(errors).length > 0 && (
<div className="rounded-lg border border-red-200 bg-red-50 p-3">
<div className="border-destructive/20 bg-destructive/10 border p-3">
<div className="mb-2 flex items-center gap-2">
<AlertCircle className="h-4 w-4 text-red-600" />
<span className="text-sm font-medium text-red-800">
<AlertCircle className="text-destructive h-4 w-4" />
<span className="text-destructive text-sm font-medium">
Upload Errors
</span>
</div>
<ul className="space-y-1 text-sm text-red-700">
<ul className="text-destructive space-y-1 text-sm">
{Object.entries(errors).map(([fileName, error]) => (
<li key={fileName} className="flex items-start gap-2">
<span className="text-red-600"></span>
<span className="text-destructive"></span>
<span>
<strong>{fileName}:</strong> {error}
</span>

View File

@@ -76,10 +76,10 @@ function InvoiceFormSkeleton() {
/>
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
<div className="space-y-6 lg:col-span-2">
<div className="bg-muted h-96 animate-pulse rounded-lg" />
<div className="bg-muted h-96 animate-pulse" />
</div>
<div className="space-y-6">
<div className="bg-muted h-64 animate-pulse rounded-lg" />
<div className="bg-muted h-64 animate-pulse" />
</div>
</div>
</div>
@@ -306,7 +306,7 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) {
onSuccess: (invoice) => {
toast.success("Invoice created successfully");
void utils.invoices.getAll.invalidate();
router.push(`/dashboard/invoices/${invoice.id}/view`);
router.push(`/dashboard/invoices/${invoice.id}`);
},
onError: (error) => {
toast.error(error.message || "Failed to create invoice");
@@ -319,7 +319,7 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) {
await utils.invoices.getAll.invalidate();
// The update mutation returns { success: true }, so we use the current invoiceId
if (invoiceId && invoiceId !== "new") {
router.push(`/dashboard/invoices/${invoiceId}/view`);
router.push(`/dashboard/invoices/${invoiceId}`);
} else {
router.push("/dashboard/invoices");
}
@@ -462,17 +462,13 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) {
variant="outline"
onClick={handleDelete}
disabled={loading || deleteInvoice.isPending}
className="text-red-700 shadow-sm hover:bg-red-50"
className="text-destructive hover:bg-destructive/10 shadow-sm"
>
<Trash2 className="h-4 w-4 sm:mr-2" />
<span className="hidden sm:inline">Delete Invoice</span>
</Button>
)}
<Button
onClick={handleSubmit}
disabled={loading}
className="bg-gradient-to-r from-emerald-600 to-teal-600 shadow-md transition-all duration-200 hover:from-emerald-700 hover:to-teal-700 hover:shadow-lg"
>
<Button onClick={handleSubmit} disabled={loading} variant="default">
{loading ? (
<>
<Clock className="h-4 w-4 animate-spin sm:mr-2" />
@@ -499,7 +495,7 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) {
</TabsList>
<TabsContent value="invoice-details">
<Card className="card-primary">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<FileText className="h-5 w-5" />
@@ -657,14 +653,14 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) {
</TabsContent>
<TabsContent value="invoice-items">
<Card className="card-primary">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<DollarSign className="h-5 w-5" />
Invoice Items
</CardTitle>
</CardHeader>
<CardContent className="px-3 py-0">
<CardContent className="px-3 pb-4">
<InvoiceLineItems
items={formData.items}
onAddItem={addItem}
@@ -681,7 +677,7 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) {
</div>
<div className="space-y-6">
<Card className="card-primary sticky top-6">
<Card className="sticky top-6">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Check className="h-5 w-5" />
@@ -747,8 +743,8 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) {
<FloatingActionBar
leftContent={
<div className="flex items-center space-x-3">
<div className="rounded-lg bg-green-100 p-2 dark:bg-green-900/30">
<FileText className="h-5 w-5 text-green-600 dark:text-green-400" />
<div className="bg-primary/10 p-2">
<FileText className="text-primary h-5 w-5" />
</div>
<div>
<p className="font-medium text-gray-900 dark:text-gray-100">
@@ -769,7 +765,7 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) {
size="sm"
onClick={handleDelete}
disabled={loading || deleteInvoice.isPending}
className="text-red-700 hover:bg-red-50"
className="text-destructive hover:bg-destructive/10"
>
<Trash2 className="h-4 w-4 sm:mr-2" />
<span className="hidden sm:inline">Delete</span>
@@ -778,7 +774,7 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) {
<Button
onClick={handleSubmit}
disabled={loading}
className="bg-gradient-to-r from-emerald-600 to-teal-600 shadow-md transition-all duration-200 hover:from-emerald-700 hover:to-teal-700 hover:shadow-lg"
variant="default"
size="sm"
>
{loading ? (

View File

@@ -25,6 +25,7 @@ import {
Trash2,
} from "lucide-react";
import * as React from "react";
import { motion, AnimatePresence } from "framer-motion";
import { Button } from "~/components/ui/button";
import { DatePicker } from "~/components/ui/date-picker";
import { Input } from "~/components/ui/input";
@@ -114,11 +115,16 @@ function SortableLineItem({
};
return (
<div
<motion.div
ref={setNodeRef}
style={style}
layout
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.2, ease: "easeOut" }}
className={cn(
"card-secondary hidden rounded-lg p-4 md:block",
"bg-card border-border hidden border p-4 md:block",
isDragging && "opacity-50",
)}
>
@@ -227,7 +233,7 @@ function SortableLineItem({
size="sm"
onClick={() => onRemove(index)}
className={cn(
"text-muted-foreground h-8 w-8 p-0 transition-colors hover:text-red-500",
"text-muted-foreground hover:text-destructive h-8 w-8 p-0 transition-colors",
!canRemove && "cursor-not-allowed opacity-50",
)}
disabled={!canRemove}
@@ -238,7 +244,7 @@ function SortableLineItem({
</div>
</div>
</div>
</div>
</motion.div>
);
}
@@ -254,8 +260,15 @@ function MobileLineItem({
isLast,
}: LineItemRowProps) {
return (
<div className="card-secondary space-y-3 rounded-lg md:hidden">
<div className="space-y-3 p-4">
<motion.div
layout
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.2, ease: "easeOut" }}
className="bg-card border-border space-y-3 border md:hidden"
>
<div className="bg-secondary space-y-3 p-4">
{/* Description */}
<div className="space-y-1">
<Label className="text-muted-foreground text-xs">Description</Label>
@@ -344,7 +357,7 @@ function MobileLineItem({
size="sm"
onClick={() => onRemove(index)}
className={cn(
"text-muted-foreground h-8 w-8 p-0 transition-colors hover:text-red-500",
"text-muted-foreground hover:text-destructive h-8 w-8 p-0 transition-colors",
!canRemove && "cursor-not-allowed opacity-50",
)}
disabled={!canRemove}
@@ -367,7 +380,7 @@ function MobileLineItem({
</span>
</div>
</div>
</div>
</motion.div>
);
}
@@ -414,51 +427,57 @@ export function InvoiceLineItems({
items={items.map((item) => item.id)}
strategy={verticalListSortingStrategy}
>
<div className="space-y-2">
{items.map((item, index) => (
<React.Fragment key={item.id}>
{/* Desktop/Tablet Card with Drag and Drop */}
<SortableLineItem
item={item}
index={index}
canRemove={canRemoveItems}
onRemove={onRemoveItem}
onUpdate={onUpdateItem}
onMoveUp={onMoveUp}
onMoveDown={onMoveDown}
isFirst={index === 0}
isLast={index === items.length - 1}
/>
<AnimatePresence>
<div className="space-y-2">
{items.map((item, index) => (
<React.Fragment key={item.id}>
{/* Desktop/Tablet Card with Drag and Drop */}
<SortableLineItem
item={item}
index={index}
canRemove={canRemoveItems}
onRemove={onRemoveItem}
onUpdate={onUpdateItem}
onMoveUp={onMoveUp}
onMoveDown={onMoveDown}
isFirst={index === 0}
isLast={index === items.length - 1}
/>
{/* Mobile Card */}
<MobileLineItem
item={item}
index={index}
canRemove={canRemoveItems}
onRemove={onRemoveItem}
onUpdate={onUpdateItem}
onMoveUp={onMoveUp}
onMoveDown={onMoveDown}
isFirst={index === 0}
isLast={index === items.length - 1}
/>
</React.Fragment>
))}
</div>
{/* Mobile Card */}
<MobileLineItem
item={item}
index={index}
canRemove={canRemoveItems}
onRemove={onRemoveItem}
onUpdate={onUpdateItem}
onMoveUp={onMoveUp}
onMoveDown={onMoveDown}
isFirst={index === 0}
isLast={index === items.length - 1}
/>
</React.Fragment>
))}
</div>
</AnimatePresence>
</SortableContext>
</DndContext>
{/* Add Item Button */}
<div className="px-3 pt-3">
<Button
type="button"
variant="outline"
onClick={onAddItem}
className="w-full"
>
<Plus className="mr-2 h-4 w-4" />
Add Another Item
</Button>
<div className="border-t pt-6">
<motion.div whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}>
<Button
type="button"
variant="outline"
onClick={onAddItem}
className="w-full"
>
<Plus className="mr-2 h-4 w-4" />
Add Line Item
</Button>
</motion.div>
</div>
</div>
</div>
);

View File

@@ -242,7 +242,7 @@ export function SendEmailDialog({
<DialogContent className="flex max-h-[90vh] max-w-4xl flex-col overflow-hidden">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Mail className="h-5 w-5 text-green-600" />
<Mail className="text-primary h-5 w-5" />
Send Invoice Email
</DialogTitle>
<DialogDescription>
@@ -419,7 +419,7 @@ export function SendEmailDialog({
<Button
onClick={handleSendEmail}
disabled={!canSend || isSending}
className="bg-green-600 hover:bg-green-700"
className="bg-primary hover:bg-primary/90"
>
{isSending ? (
<>

View File

@@ -50,7 +50,7 @@ export function FloatingActionBar({
// Base positioning - always at bottom
"fixed right-0 left-0 z-50",
// Safe area and sidebar adjustments
"pb-safe-area-inset-bottom md:left-[276px]",
"pb-safe-area-inset-bottom md:left-64",
// Conditional centering based on dock state
isDocked ? "flex justify-center" : "",
// Dynamic bottom positioning
@@ -65,7 +65,7 @@ export function FloatingActionBar({
isDocked ? "mx-auto mb-0 px-4" : "mb-4 px-4",
)}
>
<Card className="card-primary">
<Card className="bg-card border-border border">
<CardContent className="flex items-center justify-between p-4">
{/* Left content */}
{leftContent && (

View File

@@ -15,60 +15,59 @@ export function Navbar() {
// const { data: currentInvoice } = api.invoices.getCurrentOpen.useQuery();
return (
<header className="fixed top-2 right-2 left-2 z-30 md:top-3 md:right-3 md:left-3">
<div className="bg-background/60 border-border/40 relative rounded-2xl border shadow-lg backdrop-blur-xl backdrop-saturate-150">
<div className="flex h-14 items-center justify-between px-4 md:h-16 md:px-8">
<div className="flex items-center gap-4 md:gap-6">
<SidebarTrigger
isOpen={isMobileNavOpen}
onToggle={() => setIsMobileNavOpen(!isMobileNavOpen)}
/>
<Link href="/dashboard" className="flex items-center gap-2">
<Logo size="md" />
</Link>
</div>
<div className="flex items-center gap-2 md:gap-4">
{status === "loading" ? (
<>
<Skeleton className="bg-muted/20 hidden h-5 w-20 sm:inline" />
<Skeleton className="bg-muted/20 h-8 w-16" />
</>
) : session?.user ? (
<>
<span className="text-muted-foreground hidden text-xs font-medium sm:inline md:text-sm">
{session.user.name ?? session.user.email}
</span>
<header className="bg-navbar border-navbar-border text-navbar-foreground fixed top-0 right-0 left-0 z-30 border-b">
<div className="flex h-14 items-center justify-between px-4 md:h-16 md:px-8">
<div className="flex items-center gap-4 md:gap-6">
<SidebarTrigger
isOpen={isMobileNavOpen}
onToggle={() => setIsMobileNavOpen(!isMobileNavOpen)}
/>
<Link href="/dashboard" className="flex items-center gap-2">
<Logo size="md" />
</Link>
</div>
<div className="flex items-center gap-2 md:gap-4">
{status === "loading" ? (
<>
<Skeleton className="bg-muted/20 hidden h-5 w-20 sm:inline" />
<Skeleton className="bg-muted/20 h-8 w-16" />
</>
) : session?.user ? (
<>
<span className="text-muted-foreground hidden text-xs font-medium sm:inline md:text-sm">
{session.user.name ?? session.user.email}
</span>
<Button
variant="outline"
size="sm"
onClick={() => signOut({ callbackUrl: "/" })}
className="text-xs md:text-sm"
>
Sign Out
</Button>
</>
) : (
<>
<Link href="/auth/signin">
<Button
variant="outline"
variant="ghost"
size="sm"
onClick={() => signOut({ callbackUrl: "/" })}
className="border-border/40 hover:bg-accent/50 text-xs md:text-sm"
className="text-xs md:text-sm"
>
Sign Out
Sign In
</Button>
</>
) : (
<>
<Link href="/auth/signin">
<Button
variant="ghost"
size="sm"
className="hover:bg-accent/50 text-xs md:text-sm"
>
Sign In
</Button>
</Link>
<Link href="/auth/register">
<Button
size="sm"
className="bg-gradient-to-r from-emerald-600 to-teal-600 text-xs font-medium text-white shadow-md transition-all duration-200 hover:from-emerald-700 hover:to-teal-700 hover:shadow-lg md:text-sm"
>
Register
</Button>
</Link>
</>
)}
</div>
</Link>
<Link href="/auth/register">
<Button
size="sm"
variant="default"
className="text-xs font-medium md:text-sm"
>
Register
</Button>
</Link>
</>
)}
</div>
</div>
</header>

View File

@@ -132,7 +132,7 @@ export function EmptyState({
return (
<div className={cn("py-12 text-center", className)}>
{icon && (
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-muted/50">
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center bg-muted/50">
{icon}
</div>
)}

View File

@@ -35,10 +35,9 @@ const variantStyles = {
hoverBackground: "group-hover:bg-status-warning-muted/70",
},
purple: {
icon: "text-purple-600",
background: "bg-purple-100 dark:bg-purple-900/30",
hoverBackground:
"group-hover:bg-purple-200 dark:group-hover:bg-purple-900/50",
icon: "text-primary",
background: "bg-secondary",
hoverBackground: "group-hover:bg-secondary/80",
},
};
@@ -57,7 +56,7 @@ export function QuickActionCard({
<CardContent className="p-6 text-center">
<div
className={cn(
"mx-auto mb-3 flex h-12 w-12 items-center justify-center rounded-full transition-colors",
"mx-auto mb-3 flex h-12 w-12 items-center justify-center transition-colors",
styles.background,
styles.hoverBackground,
)}
@@ -99,10 +98,10 @@ export function QuickActionCard({
export function QuickActionCardSkeleton() {
return (
<Card className="card-primary">
<Card className="bg-card border-border border">
<CardContent className="p-6">
<div className="animate-pulse">
<div className="bg-muted mx-auto mb-3 h-12 w-12 rounded-full"></div>
<div className="bg-muted mx-auto mb-3 h-12 w-12 "></div>
<div className="bg-muted mx-auto mb-2 h-4 w-2/3 rounded"></div>
<div className="bg-muted mx-auto h-3 w-1/2 rounded"></div>
</div>

View File

@@ -2,23 +2,25 @@
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useSession } from "next-auth/react";
import { useSession, signOut } from "next-auth/react";
import { Skeleton } from "~/components/ui/skeleton";
import { Button } from "~/components/ui/button";
import { LogOut, User } from "lucide-react";
import { navigationConfig } from "~/lib/navigation";
export function Sidebar() {
const pathname = usePathname();
const { status } = useSession();
const { data: session, status } = useSession();
return (
<aside className="border-border/40 bg-background/60 fixed top-[5.75rem] bottom-3 left-3 z-20 hidden w-64 flex-col justify-between rounded-2xl border p-6 shadow-lg backdrop-blur-xl backdrop-saturate-150 md:flex">
<aside className="bg-sidebar border-sidebar-border text-sidebar-foreground fixed top-[4rem] bottom-0 left-0 z-20 hidden w-64 flex-col justify-between border-r p-6 md:flex">
<nav className="flex flex-col">
{navigationConfig.map((section, sectionIndex) => (
<div key={section.title} className={sectionIndex > 0 ? "mt-6" : ""}>
{sectionIndex > 0 && (
<div className="border-border/40 my-4 border-t" />
)}
<div className="text-muted-foreground mb-3 text-xs font-semibold tracking-wider uppercase">
<div className="text-sidebar-foreground/60 mb-3 text-xs font-semibold tracking-wider uppercase">
{section.title}
</div>
<div className="flex flex-col gap-0.5">
@@ -27,10 +29,10 @@ export function Sidebar() {
{Array.from({ length: section.links.length }).map((_, i) => (
<div
key={i}
className="flex items-center gap-3 rounded-lg px-3 py-2.5"
className="flex items-center gap-3 px-3 py-2.5"
>
<Skeleton className="bg-muted/20 h-4 w-4" />
<Skeleton className="bg-muted/20 h-4 w-20" />
<Skeleton className="bg-sidebar-accent/20 h-4 w-4" />
<Skeleton className="bg-sidebar-accent/20 h-4 w-20" />
</div>
))}
</>
@@ -42,10 +44,10 @@ export function Sidebar() {
key={link.href}
href={link.href}
aria-current={pathname === link.href ? "page" : undefined}
className={`flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-all duration-200 ${
className={`flex items-center gap-3 px-3 py-2.5 text-sm font-medium transition-colors ${
pathname === link.href
? "bg-gradient-to-r from-emerald-600/10 to-teal-600/10 text-emerald-700 shadow-sm dark:from-emerald-500/20 dark:to-teal-500/20 dark:text-emerald-400"
: "text-foreground hover:bg-accent/50 hover:text-accent-foreground"
? "bg-sidebar-primary text-sidebar-primary-foreground"
: "text-sidebar-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
}`}
>
<Icon className="h-4 w-4" />
@@ -58,6 +60,48 @@ export function Sidebar() {
</div>
))}
</nav>
{/* User Section */}
<div className="border-sidebar-border border-t pt-4">
{status === "loading" ? (
<div className="space-y-3">
<Skeleton className="bg-sidebar-accent/20 h-8 w-full" />
<Skeleton className="bg-sidebar-accent/20 h-8 w-full" />
<div className="flex items-center gap-3 p-3">
<Skeleton className="bg-sidebar-accent/20 h-8 w-8 rounded-full" />
<div className="flex-1">
<Skeleton className="bg-sidebar-accent/20 mb-1 h-4 w-24" />
<Skeleton className="bg-sidebar-accent/20 h-3 w-32" />
</div>
</div>
</div>
) : session?.user ? (
<div className="space-y-3">
<Button
variant="ghost"
size="sm"
onClick={() => signOut()}
className="text-sidebar-foreground/60 hover:text-sidebar-foreground hover:bg-sidebar-accent w-full justify-start px-3"
>
<LogOut className="mr-2 h-4 w-4" />
Sign Out
</Button>
<div className="flex items-center gap-3 px-3 pt-2">
<div className="bg-sidebar-accent flex h-8 w-8 items-center justify-center rounded-full">
<User className="h-4 w-4" />
</div>
<div className="min-w-0 flex-1">
<p className="text-sidebar-foreground truncate text-sm font-medium">
{session.user.name ?? "User"}
</p>
<p className="text-sidebar-foreground/60 truncate text-xs">
{session.user.email}
</p>
</div>
</div>
</div>
) : null}
</div>
</aside>
);
}

View File

@@ -7,8 +7,8 @@ import { cn } from "~/lib/utils";
const navigation = [
{ name: "Dashboard", href: "/dashboard" },
{ name: "Clients", href: "/clients" },
{ name: "Invoices", href: "/invoices" },
{ name: "Clients", href: "/dashboard/clients" },
{ name: "Invoices", href: "/dashboard/invoices" },
];
export function Navigation() {
@@ -24,7 +24,7 @@ export function Navigation() {
"transition-colors",
pathname === item.href
? "bg-primary text-primary-foreground"
: "hover:bg-muted"
: "hover:bg-muted",
)}
>
{item.name}
@@ -33,4 +33,4 @@ export function Navigation() {
))}
</nav>
);
}
}

View File

@@ -24,14 +24,14 @@ export function SidebarTrigger({ isOpen, onToggle }: SidebarTriggerProps) {
size="icon"
aria-label="Toggle navigation"
onClick={onToggle}
className="bg-card/80 h-8 w-8 shadow-lg backdrop-blur-sm md:hidden"
className="h-8 w-8 md:hidden"
>
{isOpen ? <X className="h-4 w-4" /> : <MenuIcon className="h-4 w-4" />}
</Button>
{/* Mobile dropdown navigation */}
{isOpen && (
<div className="bg-background/95 border-border/40 absolute top-full right-0 left-0 z-40 mt-2 rounded-2xl border shadow-2xl backdrop-blur-xl md:hidden">
<div className="bg-background border-border absolute top-full right-0 left-0 z-40 mt-1 border-t">
{/* Navigation content */}
<nav className="flex flex-col p-4">
{navigationConfig.map((section, sectionIndex) => (
@@ -52,7 +52,7 @@ export function SidebarTrigger({ isOpen, onToggle }: SidebarTriggerProps) {
(_, i) => (
<div
key={i}
className="flex items-center gap-3 rounded-lg px-3 py-2.5"
className="flex items-center gap-3 px-3 py-2.5"
>
<Skeleton className="bg-muted/20 h-4 w-4" />
<Skeleton className="bg-muted/20 h-4 w-20" />
@@ -70,10 +70,10 @@ export function SidebarTrigger({ isOpen, onToggle }: SidebarTriggerProps) {
aria-current={
pathname === link.href ? "page" : undefined
}
className={`flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-all duration-200 ${
className={`flex items-center gap-3 px-3 py-2.5 text-sm font-medium transition-colors ${
pathname === link.href
? "bg-gradient-to-r from-emerald-600/10 to-teal-600/10 text-emerald-700 shadow-sm dark:from-emerald-500/20 dark:to-teal-500/20 dark:text-emerald-400"
: "text-foreground hover:bg-accent/50 hover:text-accent-foreground"
? "bg-primary/10 text-primary"
: "text-foreground hover:bg-muted"
}`}
onClick={onToggle}
>

View File

@@ -54,7 +54,7 @@ function AlertDialogContent({
<AlertDialogPrimitive.Content
data-slot="alert-dialog-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 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 p-6 shadow-lg duration-200 sm:max-w-lg",
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 border p-6 shadow-lg duration-200 sm:max-w-lg",
className
)}
{...props}

View File

@@ -4,7 +4,7 @@ import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "~/lib/utils"
const alertVariants = cva(
"relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
"relative w-full border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
{
variants: {
variant: {

View File

@@ -5,7 +5,7 @@ import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "~/lib/utils";
const badgeVariants = cva(
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
"inline-flex items-center justify-center border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
{
variants: {
variant: {
@@ -23,13 +23,11 @@ const badgeVariants = cva(
info: "border-transparent bg-status-info [a&]:hover:opacity-90",
// Outlined variants for status badges
"outline-draft":
"border-gray-400 text-gray-600 dark:border-gray-500 dark:text-gray-300 bg-transparent",
"outline-sent":
"border-blue-400 text-blue-600 dark:border-blue-500 dark:text-blue-300 bg-transparent",
"outline-paid":
"border-green-400 text-green-600 dark:border-green-500 dark:text-green-300 bg-transparent",
"border-muted-foreground/40 text-muted-foreground bg-transparent",
"outline-sent": "border-primary/40 text-primary bg-transparent",
"outline-paid": "border-primary/40 text-primary bg-transparent",
"outline-overdue":
"border-red-400 text-red-600 dark:border-red-500 dark:text-red-300 bg-transparent",
"border-destructive/40 text-destructive bg-transparent",
},
},
defaultVariants: {

View File

@@ -5,28 +5,28 @@ import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "~/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors duration-150 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
"inline-flex items-center justify-center gap-2 whitespace-nowrap text-sm font-medium transition-colors duration-150 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
brand:
"bg-brand-gradient text-white shadow-lg hover:bg-brand-gradient hover:shadow-xl font-medium",
"bg-primary text-primary-foreground shadow-lg hover:bg-primary/90 hover:shadow-xl font-medium",
destructive:
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"border border-border/40 bg-background/60 shadow-sm backdrop-blur-sm hover:bg-accent/50 hover:text-accent-foreground hover:border-border/60 transition-colors duration-150",
"border border-border/40 bg-background/60 shadow-sm backdrop-blur-sm hover:bg-accent/50 hover:text-foreground-foreground hover:border-border/60 transition-colors duration-150",
secondary:
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
"bg-secondary text-muted-foreground-foreground shadow-xs hover:bg-secondary/80",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
"hover:bg-accent hover:text-foreground-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
sm: "h-8 gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 px-6 has-[>svg]:px-4",
icon: "size-9",
},
},

View File

@@ -100,7 +100,7 @@ function Calendar({
defaultClassNames.dropdowns,
),
dropdown_root: cn(
"relative has-focus:border-ring border border-input shadow-sm has-focus:ring-ring/50 has-focus:ring-2 rounded-md h-8",
"relative has-focus:border-ring border border-input shadow-sm has-focus:ring-ring/50 has-focus:ring-2 h-8",
defaultClassNames.dropdown_root,
),
dropdown: cn(
@@ -275,7 +275,7 @@ function CalendarDayButton({
data-range-end={modifiers.range_end}
data-range-middle={modifiers.range_middle}
className={cn(
"hover:bg-accent hover:text-accent-foreground flex aspect-square size-auto h-8 w-full min-w-8 items-center justify-center rounded-md border-0 text-sm leading-none font-normal shadow-none",
"hover:bg-accent hover:text-foreground-foreground flex aspect-square size-auto h-8 w-full min-w-8 items-center justify-center border-0 text-sm leading-none font-normal shadow-none",
modifiers.selected && "bg-primary text-primary-foreground",
modifiers.today && !modifiers.selected && "bg-accent font-semibold",
className,

View File

@@ -7,7 +7,7 @@ function Card({ className, ...props }: React.ComponentProps<"div">) {
<div
data-slot="card"
className={cn(
"bg-background/60 text-card-foreground border-border/40 flex flex-col gap-2 rounded-2xl border py-2 shadow-lg backdrop-blur-xl backdrop-saturate-150",
"bg-card text-card-foreground border-border/40 flex flex-col border shadow-lg backdrop-blur-xl backdrop-saturate-150",
className,
)}
{...props}
@@ -20,7 +20,7 @@ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
<div
data-slot="card-header"
className={cn(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 p-3 px-5 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-5 pt-4 pb-3 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
className,
)}
{...props}
@@ -65,7 +65,7 @@ function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-5 pb-3", className)}
className={cn("px-5 pb-4", className)}
{...props}
/>
);

View File

@@ -60,7 +60,7 @@ function DialogContent({
<DialogPrimitive.Content
data-slot="dialog-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 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 p-6 shadow-lg duration-200 sm:max-w-lg",
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 border p-6 shadow-lg duration-200 sm:max-w-lg",
className
)}
{...props}

View File

@@ -42,7 +42,7 @@ function DropdownMenuContent({
data-slot="dropdown-menu-content"
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 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 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border-0 shadow-md",
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 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 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto border-0 shadow-md",
className,
)}
{...props}
@@ -74,7 +74,7 @@ function DropdownMenuItem({
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"focus:bg-accent focus:text-foreground-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
@@ -92,7 +92,7 @@ function DropdownMenuCheckboxItem({
<DropdownMenuPrimitive.CheckboxItem
data-slot="dropdown-menu-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"focus:bg-accent focus:text-foreground-foreground relative flex cursor-default items-center gap-2 py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
checked={checked}
@@ -128,7 +128,7 @@ function DropdownMenuRadioItem({
<DropdownMenuPrimitive.RadioItem
data-slot="dropdown-menu-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"focus:bg-accent focus:text-foreground-foreground relative flex cursor-default items-center gap-2 py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
@@ -211,7 +211,7 @@ function DropdownMenuSubTrigger({
data-slot="dropdown-menu-sub-trigger"
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8",
"focus:bg-accent focus:text-foreground-foreground data-[state=open]:bg-accent data-[state=open]:text-foreground-foreground flex cursor-default items-center px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8",
className,
)}
{...props}
@@ -230,7 +230,7 @@ function DropdownMenuSubContent({
<DropdownMenuPrimitive.SubContent
data-slot="dropdown-menu-sub-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 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 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border-0 shadow-lg",
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 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 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden border-0 shadow-lg",
className,
)}
{...props}

View File

@@ -8,7 +8,7 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
type={type}
data-slot="input"
className={cn(
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground bg-background/50 text-foreground border-border/40 flex h-10 w-full min-w-0 rounded-md border px-3 py-2 text-sm shadow-sm backdrop-blur-sm transition-all duration-200 outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground bg-background/50 text-foreground border-border/40 flex h-10 w-full min-w-0 border px-3 py-2 text-sm shadow-sm backdrop-blur-sm transition-all duration-200 outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
"focus-visible:border-ring focus-visible:bg-background/80 focus-visible:ring-ring/20 focus-visible:ring-[3px]",
"hover:border-border/60 hover:bg-background/60",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",

View File

@@ -59,7 +59,7 @@ function NavigationMenuItem({
}
const navigationMenuTriggerStyle = cva(
"group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=open]:hover:bg-accent data-[state=open]:text-accent-foreground data-[state=open]:focus:bg-accent data-[state=open]:bg-accent/50 focus-visible:ring-ring/50 outline-none transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1"
"group inline-flex h-9 w-max items-center justify-center bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-foreground-foreground focus:bg-accent focus:text-foreground-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=open]:hover:bg-accent data-[state=open]:text-foreground-foreground data-[state=open]:focus:bg-accent data-[state=open]:bg-accent/50 focus-visible:ring-ring/50 outline-none transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1"
)
function NavigationMenuTrigger({
@@ -91,7 +91,7 @@ function NavigationMenuContent({
data-slot="navigation-menu-content"
className={cn(
"data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 top-0 left-0 w-full p-2 pr-2.5 md:absolute md:w-auto",
"group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:data-[state=open]:animate-in group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-out group-data-[viewport=false]/navigation-menu:data-[state=closed]:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:fade-in-0 group-data-[viewport=false]/navigation-menu:data-[state=closed]:fade-out-0 group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden group-data-[viewport=false]/navigation-menu:rounded-md group-data-[viewport=false]/navigation-menu:border group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:duration-200 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none",
"group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:data-[state=open]:animate-in group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-out group-data-[viewport=false]/navigation-menu:data-[state=closed]:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:fade-in-0 group-data-[viewport=false]/navigation-menu:data-[state=closed]:fade-out-0 group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden group-data-[viewport=false]/navigation-menu: group-data-[viewport=false]/navigation-menu:border group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:duration-200 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none",
className
)}
{...props}
@@ -112,7 +112,7 @@ function NavigationMenuViewport({
<NavigationMenuPrimitive.Viewport
data-slot="navigation-menu-viewport"
className={cn(
"origin-top-center bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border shadow md:w-[var(--radix-navigation-menu-viewport-width)]",
"origin-top-center bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden border shadow md:w-[var(--radix-navigation-menu-viewport-width)]",
className
)}
{...props}
@@ -129,7 +129,7 @@ function NavigationMenuLink({
<NavigationMenuPrimitive.Link
data-slot="navigation-menu-link"
className={cn(
"data-[active=true]:focus:bg-accent data-[active=true]:hover:bg-accent data-[active=true]:bg-accent/50 data-[active=true]:text-accent-foreground hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus-visible:ring-ring/50 [&_svg:not([class*='text-'])]:text-muted-foreground flex flex-col gap-1 rounded-sm p-2 text-sm transition-all outline-none focus-visible:ring-[3px] focus-visible:outline-1 [&_svg:not([class*='size-'])]:size-4",
"data-[active=true]:focus:bg-accent data-[active=true]:hover:bg-accent data-[active=true]:bg-accent/50 data-[active=true]:text-foreground-foreground hover:bg-accent hover:text-foreground-foreground focus:bg-accent focus:text-foreground-foreground focus-visible:ring-ring/50 [&_svg:not([class*='text-'])]:text-muted-foreground flex flex-col gap-1 p-2 text-sm transition-all outline-none focus-visible:ring-[3px] focus-visible:outline-1 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}

View File

@@ -76,7 +76,7 @@ export function NumberInput({
return (
<div
className={cn(
"bg-background flex h-9 items-center justify-center rounded-md text-sm shadow-none",
"bg-background flex h-9 items-center justify-center text-sm shadow-none",
widthClass,
disabled && "cursor-not-allowed opacity-50",
className,

View File

@@ -30,7 +30,7 @@ function PopoverContent({
align={align}
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 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 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 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 z-50 w-72 origin-(--radix-popover-content-transform-origin) border p-4 shadow-md outline-hidden",
className
)}
{...props}

View File

@@ -14,7 +14,7 @@ function Progress({
<ProgressPrimitive.Root
data-slot="progress"
className={cn(
"bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
"bg-primary/20 relative h-2 w-full overflow-hidden ",
className,
)}
{...props}

View File

@@ -42,7 +42,7 @@ function SelectTrigger({
data-slot="select-trigger"
data-size={size}
className={cn(
"data-[placeholder]:text-muted-foreground border-input bg-background text-foreground focus-visible:border-ring focus-visible:ring-ring/50 flex h-10 w-full items-center justify-between gap-2 rounded-md border px-3 py-2 text-sm shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
"data-[placeholder]:text-muted-foreground border-input bg-background text-foreground focus-visible:border-ring focus-visible:ring-ring/50 flex h-10 w-full items-center justify-between gap-2 border px-3 py-2 text-sm shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
@@ -66,7 +66,7 @@ function SelectContent({
<SelectPrimitive.Content
data-slot="select-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 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 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-0 shadow-md",
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 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 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 border-0 shadow-md",
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,
@@ -112,7 +112,7 @@ function SelectItem({
<SelectPrimitive.Item
data-slot="select-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground 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 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
"focus:bg-accent focus:text-foreground-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className,
)}
{...props}
@@ -210,7 +210,7 @@ function SelectContentWithSearch({
<SelectPrimitive.Content
data-slot="select-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 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 relative z-50 max-h-96 min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-hidden rounded-md border-0 shadow-md",
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 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 relative z-50 max-h-96 min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-hidden border-0 shadow-md",
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,
@@ -235,7 +235,7 @@ function SelectContentWithSearch({
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
<input
ref={searchInputRef}
className="placeholder:text-muted-foreground text-foreground flex h-8 w-full rounded-md border-0 bg-transparent py-2 text-sm outline-none focus:ring-0 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
className="placeholder:text-muted-foreground text-foreground flex h-8 w-full border-0 bg-transparent py-2 text-sm outline-none focus:ring-0 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
placeholder={searchPlaceholder}
value={searchValue}
onChange={(e) => onSearchChange(e.target.value)}

View File

@@ -6,7 +6,7 @@ function Skeleton({
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn("bg-muted animate-pulse rounded-md", className)}
className={cn("bg-muted animate-pulse ", className)}
{...props}
/>
);
@@ -19,10 +19,10 @@ export function DashboardStatsSkeleton() {
{Array.from({ length: 4 }).map((_, i) => (
<div
key={i}
className="rounded-lg border border-gray-100 bg-white p-6 shadow-sm"
className=" border border-gray-100 bg-white p-6 shadow-sm"
>
<div className="mb-4 flex items-center justify-between">
<Skeleton className="h-9 w-9 rounded-lg" />
<Skeleton className="h-9 w-9 " />
<Skeleton className="h-4 w-12" />
</div>
<div>
@@ -41,7 +41,7 @@ export function DashboardCardsSkeleton() {
{Array.from({ length: 2 }).map((_, i) => (
<div
key={i}
className="rounded-lg border border-gray-100 bg-white p-6 shadow-sm"
className=" border border-gray-100 bg-white p-6 shadow-sm"
>
<div className="mb-6 flex items-center justify-between">
<div className="flex items-center gap-2">
@@ -69,7 +69,7 @@ export function DashboardCardsSkeleton() {
export function DashboardActivitySkeleton() {
return (
<div className="rounded-lg border border-gray-100 bg-white p-6 shadow-sm">
<div className=" border border-gray-100 bg-white p-6 shadow-sm">
<div className="mb-6 flex items-center justify-between">
<div className="flex items-center gap-2">
<Skeleton className="h-5 w-5 rounded" />
@@ -81,17 +81,17 @@ export function DashboardActivitySkeleton() {
{Array.from({ length: 5 }).map((_, i) => (
<div
key={i}
className="flex items-center justify-between rounded-lg border border-gray-100 p-4"
className="flex items-center justify-between border border-gray-100 p-4"
>
<div className="flex items-center gap-3">
<Skeleton className="h-8 w-8 rounded-lg" />
<Skeleton className="h-8 w-8 " />
<div className="space-y-2">
<Skeleton className="h-4 w-24" />
<Skeleton className="h-3 w-32" />
</div>
</div>
<div className="flex items-center gap-3">
<Skeleton className="h-6 w-16 rounded-full" />
<Skeleton className="h-6 w-16 " />
<Skeleton className="h-4 w-16" />
<Skeleton className="h-8 w-8 rounded" />
</div>
@@ -115,14 +115,14 @@ export function DashboardHeroSkeleton() {
export function QuickActionsSkeleton() {
return (
<div className="rounded-lg border border-gray-100 bg-white p-6 shadow-sm">
<div className=" border border-gray-100 bg-white p-6 shadow-sm">
<div className="mb-4 flex items-center gap-2">
<Skeleton className="h-5 w-5 rounded" />
<Skeleton className="h-6 w-32" />
</div>
<div className="space-y-3">
{Array.from({ length: 3 }).map((_, i) => (
<div key={i} className="rounded-lg border border-gray-200 p-4">
<div key={i} className=" border border-gray-200 p-4">
<div className="flex items-center gap-3">
<Skeleton className="h-5 w-5" />
<div className="space-y-2">

View File

@@ -13,7 +13,7 @@ function Switch({
<SwitchPrimitive.Root
data-slot="switch"
className={cn(
"peer data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 inline-flex h-5 w-9 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-emerald-600",
"peer data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 data-[state=checked]:bg-primary inline-flex h-5 w-9 shrink-0 items-center border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
@@ -21,7 +21,7 @@ function Switch({
<SwitchPrimitive.Thumb
data-slot="switch-thumb"
className={cn(
"bg-background pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=checked]:bg-white data-[state=unchecked]:translate-x-0",
"bg-background pointer-events-none block size-4 ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=checked]:bg-white data-[state=unchecked]:translate-x-0",
)}
/>
</SwitchPrimitive.Root>

View File

@@ -26,7 +26,7 @@ function TabsList({
<TabsPrimitive.List
data-slot="tabs-list"
className={cn(
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]",
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center p-[3px]",
className
)}
{...props}
@@ -42,7 +42,7 @@ function TabsTrigger({
<TabsPrimitive.Trigger
data-slot="tabs-trigger"
className={cn(
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}

View File

@@ -7,7 +7,7 @@ function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
<textarea
data-slot="textarea"
className={cn(
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive bg-background text-foreground flex field-sizing-content min-h-16 w-full resize-y rounded-md border px-3 py-2 text-sm shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive bg-background text-foreground flex field-sizing-content min-h-16 w-full resize-y border px-3 py-2 text-sm shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}

View File

@@ -1,8 +0,0 @@
// Copied from shadcn/ui documentation
import * as React from "react"
import { Toaster as Sonner } from "sonner"
export function Toaster() {
return <Sonner richColors position="top-center" />
}