mirror of
https://github.com/soconnor0919/beenvoice.git
synced 2026-02-05 00:06:36 -05:00
Theme overhaul
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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", {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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> = {
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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")}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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 ? (
|
||||
<>
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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" />
|
||||
}
|
||||
Reference in New Issue
Block a user