Update Next.js to v15.5.6 and upgrade dependencies

Bump Next.js from 15.4.5 to 15.5.6 and update related dependencies.

Also upgrade other packages to latest compatible versions including: -
Radix UI components (all minor version updates) - Tiptap editor (3.0.7 →
3.11.0) - React and React DOM (19.1.1 → 19.2.0) - TanStack Query (5.84.0
→ 5.90.10) - TypeScript and ESLint ecosystem - Tailwind CSS (4.1.11 →
4.1.17) - Various other patch and minor updates

Additionally add theme support with next-themes and multiple color
schemes (light, dark, sunset, forest).
This commit is contained in:
2025-11-25 01:54:23 -05:00
parent a69b8f029b
commit 75ce36cf9c
31 changed files with 974 additions and 1085 deletions

624
bun.lock

File diff suppressed because it is too large Load Diff

View File

@@ -10,7 +10,6 @@
"db:migrate": "drizzle-kit migrate", "db:migrate": "drizzle-kit migrate",
"db:push": "drizzle-kit push", "db:push": "drizzle-kit push",
"db:studio": "drizzle-kit studio", "db:studio": "drizzle-kit studio",
"docker-up": "colima start && docker-compose up -d", "docker-up": "colima start && docker-compose up -d",
"docker-down": "docker-compose down && colima stop", "docker-down": "docker-compose down && colima stop",
"deploy": "drizzle-kit push && next build", "deploy": "drizzle-kit push && next build",
@@ -24,89 +23,85 @@
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit"
}, },
"dependencies": { "dependencies": {
"@auth/drizzle-adapter": "^1.10.0", "@auth/drizzle-adapter": "^1.11.1",
"@dnd-kit/core": "^6.3.1", "@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0", "@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2", "@dnd-kit/utilities": "^3.2.2",
"@radix-ui/react-alert-dialog": "^1.1.14", "@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-checkbox": "^1.3.2", "@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-collapsible": "^1.1.11", "@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-dialog": "^1.1.14", "@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.7", "@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-navigation-menu": "^1.2.13", "@radix-ui/react-navigation-menu": "^1.2.14",
"@radix-ui/react-popover": "^1.1.14", "@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-progress": "^1.1.7", "@radix-ui/react-progress": "^1.1.8",
"@radix-ui/react-select": "^2.2.5", "@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-switch": "^1.2.5", "@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tabs": "^1.1.12", "@radix-ui/react-tabs": "^1.1.13",
"@react-pdf/renderer": "^4.3.0", "@react-pdf/renderer": "^4.3.1",
"@t3-oss/env-nextjs": "^0.12.0", "@t3-oss/env-nextjs": "^0.12.0",
"@tanstack/react-query": "^5.84.0", "@tanstack/react-query": "^5.90.10",
"@tanstack/react-table": "^8.21.3", "@tanstack/react-table": "^8.21.3",
"@tiptap/extension-color": "^3.0.7", "@tiptap/extension-color": "^3.11.0",
"@tiptap/extension-list-item": "^3.0.7", "@tiptap/extension-list-item": "^3.11.0",
"@tiptap/extension-text-align": "^3.0.7", "@tiptap/extension-text-align": "^3.11.0",
"@tiptap/extension-text-style": "^3.0.7", "@tiptap/extension-text-style": "^3.11.0",
"@tiptap/react": "^3.0.7", "@tiptap/react": "^3.11.0",
"@tiptap/starter-kit": "^3.0.7", "@tiptap/starter-kit": "^3.11.0",
"@trpc/client": "^11.4.3", "@trpc/client": "^11.7.2",
"@trpc/react-query": "^11.4.3", "@trpc/react-query": "^11.7.2",
"@trpc/server": "^11.4.3", "@trpc/server": "^11.7.2",
"@vercel/analytics": "^1.5.0", "@vercel/analytics": "^1.5.0",
"bcryptjs": "^3.0.2", "bcryptjs": "^3.0.3",
"chrono-node": "^2.8.3", "chrono-node": "^2.9.0",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"drizzle-orm": "^0.44.4", "drizzle-orm": "^0.44.7",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"framer-motion": "^12.23.12", "framer-motion": "^12.23.24",
"lucide-react": "^0.525.0", "lucide-react": "^0.525.0",
"next": "^15.4.5", "next": "^15.5.6",
"next-auth": "5.0.0-beta.25", "next-auth": "5.0.0-beta.25",
"next-themes": "^0.3.0",
"pg": "^8.16.3", "pg": "^8.16.3",
"react": "^19.1.1", "react": "^19.2.0",
"react-day-picker": "^9.8.1", "react-day-picker": "^9.11.2",
"react-dom": "^19.1.1", "react-dom": "^19.2.0",
"react-dropzone": "^14.3.8", "react-dropzone": "^14.3.8",
"recharts": "^3.1.0", "recharts": "^3.5.0",
"resend": "^4.7.0", "resend": "^4.8.0",
"server-only": "^0.0.1", "server-only": "^0.0.1",
"sonner": "^2.0.6", "sonner": "^2.0.7",
"superjson": "^2.2.2", "superjson": "^2.2.5",
"tailwind-merge": "^3.3.1", "tailwind-merge": "^3.4.0",
"zod": "^3.25.76" "zod": "^3.25.76"
}, },
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3.3.1", "@eslint/eslintrc": "^3.3.1",
"@tailwindcss/postcss": "^4.1.11", "@tailwindcss/postcss": "^4.1.17",
"@types/bcryptjs": "^2.4.6", "@types/bcryptjs": "^2.4.6",
"@types/file-saver": "^2.0.7", "@types/file-saver": "^2.0.7",
"@types/pg": "^8.15.5", "@types/pg": "^8.15.6",
"@types/node": "^20.19.9", "@types/node": "^20.19.25",
"@types/raf": "^3.4.3", "@types/raf": "^3.4.3",
"@types/react": "^19.1.9", "@types/react": "^19.2.7",
"@types/react-dom": "^19.1.7", "@types/react-dom": "^19.2.3",
"drizzle-kit": "^0.30.6", "drizzle-kit": "^0.30.6",
"eslint": "^9.32.0", "eslint": "^9.39.1",
"eslint-config-next": "^15.4.5", "eslint-config-next": "^15.5.6",
"eslint-plugin-drizzle": "^0.2.3", "eslint-plugin-drizzle": "^0.2.3",
"postcss": "^8.5.6", "postcss": "^8.5.6",
"prettier": "^3.6.2", "prettier": "^3.6.2",
"prettier-plugin-tailwindcss": "^0.6.14", "prettier-plugin-tailwindcss": "^0.6.14",
"tailwindcss": "^4.1.11", "tailwindcss": "^4.1.17",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"tw-animate-css": "^1.3.6", "tw-animate-css": "^1.4.0",
"typescript": "^5.9.2", "typescript": "^5.9.3",
"typescript-eslint": "^8.38.0" "typescript-eslint": "^8.48.0"
}, },
"ct3aMetadata": { "ct3aMetadata": {
"initVersion": "7.39.3" "initVersion": "7.39.3"

Binary file not shown.

Binary file not shown.

View File

@@ -55,7 +55,7 @@ export function AnimatedStatsCard({
void numericValue; void numericValue;
return ( return (
<Card className="stats-card"> <Card>
<CardContent className="p-6"> <CardContent className="p-6">
<div className="flex items-center justify-between space-y-0 pb-2"> <div className="flex items-center justify-between space-y-0 pb-2">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">

View File

@@ -77,12 +77,12 @@ export function PDFDownloadButton({
> >
{isGenerating ? ( {isGenerating ? (
<> <>
<Loader2 className="h-5 w-5 animate-spin" /> <Loader2 className="mr-2 h-5 w-5 animate-spin" />
<span>Generating PDF...</span> <span>Generating PDF...</span>
</> </>
) : ( ) : (
<> <>
<Download className="h-5 w-5" /> <Download className="mr-2 h-5 w-5" />
<span>Download PDF</span> <span>Download PDF</span>
</> </>
)} )}

View File

@@ -136,7 +136,7 @@ function InvoiceViewContent({ invoiceId }: { invoiceId: string }) {
/> />
<Button asChild variant="default" className="hover-lift"> <Button asChild variant="default" className="hover-lift">
<Link href={`/dashboard/invoices/${invoice.id}/edit`}> <Link href={`/dashboard/invoices/${invoice.id}/edit`}>
<Edit className="h-5 w-5" /> <Edit className="mr-2 h-5 w-5" />
<span>Edit</span> <span>Edit</span>
</Link> </Link>
</Button> </Button>
@@ -329,7 +329,7 @@ function InvoiceViewContent({ invoiceId }: { invoiceId: string }) {
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
{invoice.items.map((item, _index) => ( {invoice.items.map((item, _index) => (
<Card key={item.id} className="invoice-item card-secondary"> <Card key={item.id} className="invoice-item bg-secondary">
<CardContent className="p-3"> <CardContent className="p-3">
<div className="space-y-3"> <div className="space-y-3">
<div className="flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between"> <div className="flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between">
@@ -364,7 +364,7 @@ function InvoiceViewContent({ invoiceId }: { invoiceId: string }) {
))} ))}
{/* Totals */} {/* Totals */}
<div className="bg-muted/30 rounded-lg p-4"> <div className="bg-secondary rounded-lg p-4">
<div className="space-y-3"> <div className="space-y-3">
<div className="flex flex-wrap justify-between gap-x-4 gap-y-1"> <div className="flex flex-wrap justify-between gap-x-4 gap-y-1">
<span className="text-muted-foreground">Subtotal:</span> <span className="text-muted-foreground">Subtotal:</span>
@@ -411,7 +411,7 @@ function InvoiceViewContent({ invoiceId }: { invoiceId: string }) {
{/* Right Column - Actions */} {/* Right Column - Actions */}
<div className="space-y-6"> <div className="space-y-6">
<Card className="sticky top-6"> <Card className="sticky top-20">
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2"> <CardTitle className="flex items-center gap-2">
<Check className="h-5 w-5" /> <Check className="h-5 w-5" />
@@ -419,7 +419,7 @@ function InvoiceViewContent({ invoiceId }: { invoiceId: string }) {
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent className="space-y-3"> <CardContent className="space-y-3">
<Button asChild variant="outline" className="w-full"> <Button asChild variant="secondary" className="w-full">
<Link href={`/dashboard/invoices/${invoice.id}/edit`}> <Link href={`/dashboard/invoices/${invoice.id}/edit`}>
<Edit className="mr-2 h-4 w-4" /> <Edit className="mr-2 h-4 w-4" />
Edit Invoice Edit Invoice
@@ -427,7 +427,11 @@ function InvoiceViewContent({ invoiceId }: { invoiceId: string }) {
</Button> </Button>
{invoice.items && invoice.client && ( {invoice.items && invoice.client && (
<PDFDownloadButton invoiceId={invoice.id} className="w-full" /> <PDFDownloadButton
invoiceId={invoice.id}
className="w-full"
variant="secondary"
/>
)} )}
{/* Send Invoice Button - Show for draft, sent, and overdue */} {/* Send Invoice Button - Show for draft, sent, and overdue */}
@@ -435,6 +439,7 @@ function InvoiceViewContent({ invoiceId }: { invoiceId: string }) {
<EnhancedSendInvoiceButton <EnhancedSendInvoiceButton
invoiceId={invoice.id} invoiceId={invoice.id}
className="w-full" className="w-full"
variant="secondary"
/> />
)} )}
@@ -444,6 +449,7 @@ function InvoiceViewContent({ invoiceId }: { invoiceId: string }) {
invoiceId={invoice.id} invoiceId={invoice.id}
className="w-full" className="w-full"
showResend={true} showResend={true}
variant="secondary"
/> />
)} )}
@@ -453,7 +459,8 @@ function InvoiceViewContent({ invoiceId }: { invoiceId: string }) {
<Button <Button
onClick={handleMarkAsPaid} onClick={handleMarkAsPaid}
disabled={updateStatus.isPending} disabled={updateStatus.isPending}
className="bg-primary text-primary-foreground hover:bg-primary/90 w-full" variant="secondary"
className="w-full"
> >
{updateStatus.isPending ? ( {updateStatus.isPending ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> <Loader2 className="mr-2 h-4 w-4 animate-spin" />
@@ -465,7 +472,7 @@ function InvoiceViewContent({ invoiceId }: { invoiceId: string }) {
)} )}
<Button <Button
variant="outline" variant="secondary"
onClick={handleDelete} onClick={handleDelete}
disabled={deleteInvoice.isPending} disabled={deleteInvoice.isPending}
className="text-destructive hover:bg-destructive/10 w-full" className="text-destructive hover:bg-destructive/10 w-full"

View File

@@ -12,6 +12,7 @@ import {
FileUp, FileUp,
Info, Info,
Key, Key,
Palette,
Shield, Shield,
Upload, Upload,
User, User,
@@ -61,6 +62,7 @@ import { Textarea } from "~/components/ui/textarea";
import { api } from "~/trpc/react"; import { api } from "~/trpc/react";
import { Switch } from "~/components/ui/switch"; import { Switch } from "~/components/ui/switch";
import { Slider } from "~/components/ui/slider"; import { Slider } from "~/components/ui/slider";
import { ThemeSelector } from "./theme-selector";
import { useAnimationPreferences } from "~/components/providers/animation-preferences-provider"; import { useAnimationPreferences } from "~/components/providers/animation-preferences-provider";
export function SettingsContent() { export function SettingsContent() {
@@ -389,7 +391,7 @@ export function SettingsContent() {
return ( return (
<div <div
key={item.label} key={item.label}
className="hover-lift bg-card border p-4 transition-shadow hover:shadow-sm" className="bg-card rounded-lg border p-4 transition-shadow hover:shadow-sm"
style={{ animationDelay: `${index * 100}ms` }} style={{ animationDelay: `${index * 100}ms` }}
> >
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
@@ -615,11 +617,27 @@ export function SettingsContent() {
</CardContent> </CardContent>
</Card> </Card>
{/* Appearance Settings */}
<Card className="bg-card border-border border">
<CardHeader>
<CardTitle className="text-foreground flex items-center gap-2">
<Palette className="text-primary h-5 w-5" />
Appearance
</CardTitle>
<CardDescription>
Customize the look and feel of the application
</CardDescription>
</CardHeader>
<CardContent>
<ThemeSelector />
</CardContent>
</Card>
{/* Data Management */} {/* Data Management */}
<Card className="bg-card border-border border"> <Card className="bg-card border-border border">
<CardHeader> <CardHeader>
<CardTitle className="text-foreground flex items-center gap-2"> <CardTitle className="text-foreground flex items-center gap-2">
<Shield className="text-icon-indigo h-5 w-5" /> <Shield className="text-primary h-5 w-5" />
Data Management Data Management
</CardTitle> </CardTitle>
<CardDescription> <CardDescription>
@@ -785,10 +803,10 @@ export function SettingsContent() {
</Card> </Card>
{/* Danger Zone */} {/* Danger Zone */}
<Card className="bg-card border-border border border-l-4 border-l-red-500"> <Card className="bg-card border-border border-l-destructive border border-l-4">
<CardHeader> <CardHeader>
<CardTitle className="text-destructive flex items-center gap-2"> <CardTitle className="text-destructive flex items-center gap-2">
<AlertTriangle className="text-icon-red h-5 w-5" /> <AlertTriangle className="text-destructive h-5 w-5" />
Danger Zone Danger Zone
</CardTitle> </CardTitle>
<CardDescription> <CardDescription>
@@ -823,7 +841,7 @@ export function SettingsContent() {
This action cannot be undone. This will permanently delete This action cannot be undone. This will permanently delete
all your: all your:
</div> </div>
<ul className="border-border bg-muted/50 list-inside list-disc space-y-1 border p-3 text-sm"> <ul className="border-border bg-muted/50 list-inside list-disc space-y-1 rounded-lg border p-3 text-sm">
<li>Client information and contact details</li> <li>Client information and contact details</li>
<li>Business profiles and settings</li> <li>Business profiles and settings</li>
<li>Invoices and invoice line items</li> <li>Invoices and invoice line items</li>

View File

@@ -0,0 +1,56 @@
"use client";
import { useTheme } from "next-themes";
import { Check, Palette } from "lucide-react";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "~/components/ui/dropdown-menu";
import { Button } from "~/components/ui/button";
import { cn } from "~/lib/utils";
export function ThemeSelector() {
const { theme, setTheme } = useTheme();
const themes = [
{ name: "light", label: "Light" },
{ name: "dark", label: "Dark" },
{ name: "theme-sunset", label: "Sunset" },
{ name: "theme-forest", label: "Forest" },
];
return (
<div className="flex items-center justify-between">
<div className="space-y-1.5">
<label className="font-medium">Theme</label>
<p className="text-muted-foreground text-xs leading-snug">
Select a theme for the application.
</p>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="w-40 justify-between">
<span>
{themes.find((t) => t.name === theme)?.label ?? "Light"}
</span>
<Palette className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
{themes.map((t) => (
<DropdownMenuItem
key={t.name}
className="flex justify-between"
onClick={() => setTheme(t.name)}
>
<span>{t.label}</span>
{theme === t.name && <Check className="h-4 w-4" />}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</div>
);
}

View File

@@ -8,6 +8,8 @@ import { TRPCReactProvider } from "~/trpc/react";
import { Toaster } from "~/components/ui/sonner"; import { Toaster } from "~/components/ui/sonner";
import { AnimationPreferencesProvider } from "~/components/providers/animation-preferences-provider"; import { AnimationPreferencesProvider } from "~/components/providers/animation-preferences-provider";
import { ThemeProvider } from "~/components/providers/theme-provider";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "beenvoice - Invoicing Made Simple", title: "beenvoice - Invoicing Made Simple",
description: description:
@@ -36,12 +38,19 @@ export default function RootLayout({
</head> </head>
<Analytics /> <Analytics />
<body className="bg-background text-foreground relative min-h-screen overflow-x-hidden font-sans antialiased"> <body className="bg-background text-foreground relative min-h-screen overflow-x-hidden font-sans antialiased">
<TRPCReactProvider> <ThemeProvider
<AnimationPreferencesProvider> attribute="class"
{children} defaultTheme="system"
</AnimationPreferencesProvider> enableSystem
</TRPCReactProvider> disableTransitionOnChange
<Toaster /> >
<TRPCReactProvider>
<AnimationPreferencesProvider>
{children}
</AnimationPreferencesProvider>
<Toaster />
</TRPCReactProvider>
</ThemeProvider>
</body> </body>
</html> </html>
); );

View File

@@ -192,10 +192,10 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
<FileText className="text-primary h-6 w-6" /> <FileText className="text-primary h-6 w-6" />
</div> </div>
<div> <div>
<h2 className="text-2xl font-bold text-gray-900 dark:text-white"> <h2 className="text-foreground text-2xl font-bold">
{invoice.invoiceNumber} {invoice.invoiceNumber}
</h2> </h2>
<p className="text-gray-600 dark:text-gray-300"> <p className="text-muted-foreground">
Professional Invoice Professional Invoice
</p> </p>
</div> </div>
@@ -203,18 +203,14 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
<div className="grid grid-cols-2 gap-6 text-sm"> <div className="grid grid-cols-2 gap-6 text-sm">
<div> <div>
<span className="text-gray-500 dark:text-gray-400"> <span className="text-muted-foreground">Issue Date</span>
Issue Date <p className="text-foreground font-medium">
</span>
<p className="font-medium text-gray-900 dark:text-white">
{formatDate(invoice.issueDate)} {formatDate(invoice.issueDate)}
</p> </p>
</div> </div>
<div> <div>
<span className="text-gray-500 dark:text-gray-400"> <span className="text-muted-foreground">Due Date</span>
Due Date <p className="text-foreground font-medium">
</span>
<p className="font-medium text-gray-900 dark:text-white">
{formatDate(invoice.dueDate)} {formatDate(invoice.dueDate)}
</p> </p>
</div> </div>
@@ -234,7 +230,7 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
<Button <Button
onClick={handlePDFExport} onClick={handlePDFExport}
disabled={isExportingPDF} disabled={isExportingPDF}
variant="brand" variant="default"
className="transform-none" className="transform-none"
> >
{isExportingPDF ? ( {isExportingPDF ? (
@@ -264,29 +260,29 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<div> <div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white"> <h3 className="text-foreground text-lg font-semibold">
{invoice.client?.name} {invoice.client?.name}
</h3> </h3>
</div> </div>
<div className="grid grid-cols-1 gap-4 text-sm md:grid-cols-2"> <div className="grid grid-cols-1 gap-4 text-sm md:grid-cols-2">
{invoice.client?.email && ( {invoice.client?.email && (
<div className="flex items-center gap-2 text-gray-600 dark:text-gray-300"> <div className="text-muted-foreground flex items-center gap-2">
<Mail className="h-4 w-4 text-gray-400 dark:text-gray-500" /> <Mail className="text-muted-foreground h-4 w-4" />
{invoice.client.email} {invoice.client.email}
</div> </div>
)} )}
{invoice.client?.phone && ( {invoice.client?.phone && (
<div className="flex items-center gap-2 text-gray-600 dark:text-gray-300"> <div className="text-muted-foreground flex items-center gap-2">
<Phone className="h-4 w-4 text-gray-400 dark:text-gray-500" /> <Phone className="text-muted-foreground h-4 w-4" />
{invoice.client.phone} {invoice.client.phone}
</div> </div>
)} )}
{(invoice.client?.addressLine1 ?? {(invoice.client?.addressLine1 ??
invoice.client?.city ?? invoice.client?.city ??
invoice.client?.state) && ( invoice.client?.state) && (
<div className="flex items-start gap-2 text-gray-600 md:col-span-2 dark:text-gray-300"> <div className="text-muted-foreground flex items-start gap-2 md:col-span-2">
<MapPin className="mt-0.5 h-4 w-4 flex-shrink-0 text-gray-400 dark:text-gray-500" /> <MapPin className="text-muted-foreground mt-0.5 h-4 w-4 flex-shrink-0" />
<div> <div>
{invoice.client?.addressLine1 && ( {invoice.client?.addressLine1 && (
<div>{invoice.client.addressLine1}</div> <div>{invoice.client.addressLine1}</div>
@@ -318,7 +314,7 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
</Card> </Card>
{/* Invoice Items */} {/* Invoice Items */}
<Card className="bg-card border-border border"> <Card className="bg-secondary border-border border">
<CardHeader> <CardHeader>
<CardTitle className="text-primary flex items-center gap-2"> <CardTitle className="text-primary flex items-center gap-2">
<Clock className="h-5 w-5" /> <Clock className="h-5 w-5" />
@@ -326,52 +322,25 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="border-border overflow-hidden border"> <div className="space-y-2">
<table className="w-full"> {invoice.items?.map((item, index) => (
<thead className="bg-muted"> <div
<tr> key={item.id || index}
<th className="text-muted-foreground px-4 py-3 text-left text-sm font-semibold"> className="bg-background flex items-center justify-between rounded-lg p-4"
Date >
</th> <div className="flex items-center gap-4">
<th className="text-muted-foreground px-4 py-3 text-left text-sm font-semibold"> <div className="text-muted-foreground text-sm">
Description {formatDate(item.date)}
</th> </div>
<th className="text-muted-foreground px-4 py-3 text-right text-sm font-semibold"> <div className="text-foreground font-medium">
Hours {item.description}
</th> </div>
<th className="text-muted-foreground px-4 py-3 text-right text-sm font-semibold"> </div>
Rate <div className="text-foreground text-right font-medium">
</th> {formatCurrency(item.amount)}
<th className="text-muted-foreground px-4 py-3 text-right text-sm font-semibold"> </div>
Amount </div>
</th> ))}
</tr>
</thead>
<tbody>
{invoice.items?.map((item, index) => (
<tr
key={item.id || index}
className="border-border hover:bg-muted/50 border-t"
>
<td className="text-foreground px-4 py-3 text-sm">
{formatDate(item.date)}
</td>
<td className="text-foreground px-4 py-3 text-sm">
{item.description}
</td>
<td className="text-foreground px-4 py-3 text-right text-sm">
{item.hours}
</td>
<td className="text-foreground px-4 py-3 text-right text-sm">
{formatCurrency(item.rate)}
</td>
<td className="text-foreground px-4 py-3 text-right text-sm font-medium">
{formatCurrency(item.amount)}
</td>
</tr>
))}
</tbody>
</table>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
@@ -383,7 +352,7 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
<CardTitle className="text-primary">Notes</CardTitle> <CardTitle className="text-primary">Notes</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<p className="whitespace-pre-wrap text-gray-700 dark:text-gray-300"> <p className="text-muted-foreground whitespace-pre-wrap">
{invoice.notes} {invoice.notes}
</p> </p>
</CardContent> </CardContent>
@@ -394,7 +363,7 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
{/* Sidebar */} {/* Sidebar */}
<div className="space-y-6"> <div className="space-y-6">
{/* Status Actions */} {/* Status Actions */}
<Card className="bg-card border-border border"> <Card className="bg-secondary border-border border">
<CardHeader> <CardHeader>
<CardTitle className="text-primary">Status Actions</CardTitle> <CardTitle className="text-primary">Status Actions</CardTitle>
</CardHeader> </CardHeader>
@@ -403,7 +372,7 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
<Button <Button
onClick={() => handleStatusUpdate("sent")} onClick={() => handleStatusUpdate("sent")}
disabled={updateStatus.isPending} disabled={updateStatus.isPending}
className="bg-primary text-primary-foreground hover:bg-primary/90 w-full" className="w-full"
> >
<Send className="mr-2 h-4 w-4" /> <Send className="mr-2 h-4 w-4" />
Mark as Sent Mark as Sent
@@ -414,7 +383,7 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
<Button <Button
onClick={() => handleStatusUpdate("paid")} onClick={() => handleStatusUpdate("paid")}
disabled={updateStatus.isPending} disabled={updateStatus.isPending}
className="bg-primary text-primary-foreground hover:bg-primary/90 w-full" className="w-full"
> >
<DollarSign className="mr-2 h-4 w-4" /> <DollarSign className="mr-2 h-4 w-4" />
Mark as Paid Mark as Paid
@@ -425,7 +394,7 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
<Button <Button
onClick={() => handleStatusUpdate("paid")} onClick={() => handleStatusUpdate("paid")}
disabled={updateStatus.isPending} disabled={updateStatus.isPending}
className="bg-primary text-primary-foreground hover:bg-primary/90 w-full" className="w-full"
> >
<DollarSign className="mr-2 h-4 w-4" /> <DollarSign className="mr-2 h-4 w-4" />
Mark as Paid Mark as Paid
@@ -449,20 +418,18 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<div className="space-y-3"> <div className="space-y-3">
<div className="flex justify-between text-sm"> <div className="flex justify-between text-sm">
<span className="text-gray-600 dark:text-gray-300"> <span className="text-muted-foreground">Subtotal</span>
Subtotal <span className="text-foreground font-medium">
</span>
<span className="font-medium dark:text-white">
{formatCurrency(invoice.totalAmount)} {formatCurrency(invoice.totalAmount)}
</span> </span>
</div> </div>
<div className="flex justify-between text-sm"> <div className="flex justify-between text-sm">
<span className="text-gray-600 dark:text-gray-300">Tax</span> <span className="text-muted-foreground">Tax</span>
<span className="font-medium dark:text-white">$0.00</span> <span className="text-foreground font-medium">$0.00</span>
</div> </div>
<Separator /> <Separator />
<div className="flex justify-between text-lg font-bold"> <div className="flex justify-between text-lg font-bold">
<span className="dark:text-white">Total</span> <span className="text-foreground">Total</span>
<span className="text-primary"> <span className="text-primary">
{formatCurrency(invoice.totalAmount)} {formatCurrency(invoice.totalAmount)}
</span> </span>
@@ -486,8 +453,8 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
<CardContent> <CardContent>
<Button <Button
onClick={handleDelete} onClick={handleDelete}
variant="outline" variant="destructive"
className="border-destructive/20 text-destructive hover:bg-destructive/10 w-full" className="w-full"
> >
<Trash2 className="mr-2 h-4 w-4" /> <Trash2 className="mr-2 h-4 w-4" />
Delete Invoice Delete Invoice
@@ -501,10 +468,10 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}> <Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<DialogContent className="bg-card border-border border"> <DialogContent className="bg-card border-border border">
<DialogHeader> <DialogHeader>
<DialogTitle className="text-xl font-bold text-gray-800 dark:text-white"> <DialogTitle className="text-foreground text-xl font-bold">
Delete Invoice Delete Invoice
</DialogTitle> </DialogTitle>
<DialogDescription className="text-gray-600 dark:text-gray-300"> <DialogDescription className="text-muted-foreground">
Are you sure you want to delete this invoice? This action cannot Are you sure you want to delete this invoice? This action cannot
be undone and will permanently remove the invoice and all its be undone and will permanently remove the invoice and all its
data. data.

View File

@@ -507,21 +507,16 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) {
> >
{invoiceId && invoiceId !== "new" && ( {invoiceId && invoiceId !== "new" && (
<Button <Button
variant="outline" variant="secondary"
onClick={handleDelete} onClick={handleDelete}
disabled={loading || deleteInvoice.isPending} disabled={loading || deleteInvoice.isPending}
className="hover-lift text-destructive hover:bg-destructive/10 shadow-sm" className="text-destructive hover:bg-destructive/10"
> >
<Trash2 className="h-4 w-4 sm:mr-2" /> <Trash2 className="h-4 w-4 sm:mr-2" />
<span className="hidden sm:inline">Delete Invoice</span> <span className="hidden sm:inline">Delete Invoice</span>
</Button> </Button>
)} )}
<Button <Button onClick={handleSubmit} disabled={loading} variant="secondary">
onClick={handleSubmit}
disabled={loading}
variant="default"
className="hover-lift"
>
{loading ? ( {loading ? (
<> <>
<Clock className="h-4 w-4 animate-spin sm:mr-2" /> <Clock className="h-4 w-4 animate-spin sm:mr-2" />
@@ -564,7 +559,6 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) {
value={formData.invoiceNumber} value={formData.invoiceNumber}
placeholder="INV-2024-001" placeholder="INV-2024-001"
disabled disabled
className="bg-muted/50"
/> />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
@@ -831,16 +825,16 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) {
<FloatingActionBar <FloatingActionBar
leftContent={ leftContent={
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
<div className="bg-primary/10 p-2"> <div className="p-2">
<FileText className="text-primary h-5 w-5" /> <FileText className="text-primary h-5 w-5" />
</div> </div>
<div> <div>
<p className="font-medium text-gray-900 dark:text-gray-100"> <p className="text-foreground font-medium">
{invoiceId && invoiceId !== "new" {invoiceId && invoiceId !== "new"
? "Edit Invoice" ? "Edit Invoice"
: "Create Invoice"} : "Create Invoice"}
</p> </p>
<p className="text-sm text-gray-600 dark:text-gray-300"> <p className="text-muted-foreground text-sm">
Update invoice details Update invoice details
</p> </p>
</div> </div>
@@ -849,7 +843,7 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) {
> >
{invoiceId && invoiceId !== "new" && ( {invoiceId && invoiceId !== "new" && (
<Button <Button
variant="outline" variant="secondary"
size="sm" size="sm"
onClick={handleDelete} onClick={handleDelete}
disabled={loading || deleteInvoice.isPending} disabled={loading || deleteInvoice.isPending}
@@ -862,7 +856,7 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) {
<Button <Button
onClick={handleSubmit} onClick={handleSubmit}
disabled={loading} disabled={loading}
variant="default" variant="secondary"
size="sm" size="sm"
> >
{loading ? ( {loading ? (

View File

@@ -124,7 +124,7 @@ function SortableLineItem({
exit={{ opacity: 0, y: -20 }} exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.2, ease: "easeOut" }} transition={{ duration: 0.2, ease: "easeOut" }}
className={cn( className={cn(
"bg-card border-border hidden border p-4 md:block", "bg-secondary hidden rounded-lg p-4 md:block",
isDragging && "opacity-50", isDragging && "opacity-50",
)} )}
> >
@@ -144,12 +144,7 @@ function SortableLineItem({
variant="ghost" variant="ghost"
size="sm" size="sm"
onClick={() => onMoveUp(index)} onClick={() => onMoveUp(index)}
className={cn( className="h-6 w-6 p-0"
"h-6 w-6 p-0 transition-colors",
isFirst
? "text-muted-foreground/50 cursor-not-allowed"
: "text-muted-foreground hover:text-foreground",
)}
disabled={isFirst} disabled={isFirst}
aria-label="Move up" aria-label="Move up"
> >
@@ -160,12 +155,7 @@ function SortableLineItem({
variant="ghost" variant="ghost"
size="sm" size="sm"
onClick={() => onMoveDown(index)} onClick={() => onMoveDown(index)}
className={cn( className="h-6 w-6 p-0"
"h-6 w-6 p-0 transition-colors",
isLast
? "text-muted-foreground/50 cursor-not-allowed"
: "text-muted-foreground hover:text-foreground",
)}
disabled={isLast} disabled={isLast}
aria-label="Move down" aria-label="Move down"
> >
@@ -232,10 +222,7 @@ function SortableLineItem({
variant="ghost" variant="ghost"
size="sm" size="sm"
onClick={() => onRemove(index)} onClick={() => onRemove(index)}
className={cn( className="text-muted-foreground hover:text-destructive h-8 w-8 p-0"
"text-muted-foreground hover:text-destructive h-8 w-8 p-0 transition-colors",
!canRemove && "cursor-not-allowed opacity-50",
)}
disabled={!canRemove} disabled={!canRemove}
aria-label="Remove item" aria-label="Remove item"
> >
@@ -266,7 +253,7 @@ function MobileLineItem({
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }} exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.2, ease: "easeOut" }} transition={{ duration: 0.2, ease: "easeOut" }}
className="bg-card border-border space-y-3 border md:hidden" className="border-border bg-card overflow-hidden rounded-lg border md:hidden"
> >
<div className="bg-secondary space-y-3 p-4"> <div className="bg-secondary space-y-3 p-4">
{/* Description */} {/* Description */}
@@ -317,19 +304,14 @@ function MobileLineItem({
</div> </div>
{/* Bottom section with controls, item name, and total */} {/* Bottom section with controls, item name, and total */}
<div className="flex items-center justify-between rounded-b-lg border-t border-gray-400/60 bg-gray-200/30 px-4 py-2 dark:border-gray-500/60 dark:bg-gray-600/40"> <div className="border-border bg-muted/50 flex items-center justify-between border-t px-4 py-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Button <Button
type="button" type="button"
variant="ghost" variant="ghost"
size="sm" size="sm"
onClick={() => onMoveUp(index)} onClick={() => onMoveUp(index)}
className={cn( className="h-8 w-8 p-0"
"h-8 w-8 p-0 transition-colors",
isFirst
? "text-muted-foreground/50 cursor-not-allowed"
: "text-muted-foreground hover:text-foreground",
)}
disabled={isFirst} disabled={isFirst}
aria-label="Move up" aria-label="Move up"
> >
@@ -340,12 +322,7 @@ function MobileLineItem({
variant="ghost" variant="ghost"
size="sm" size="sm"
onClick={() => onMoveDown(index)} onClick={() => onMoveDown(index)}
className={cn( className="h-8 w-8 p-0"
"h-8 w-8 p-0 transition-colors",
isLast
? "text-muted-foreground/50 cursor-not-allowed"
: "text-muted-foreground hover:text-foreground",
)}
disabled={isLast} disabled={isLast}
aria-label="Move down" aria-label="Move down"
> >
@@ -356,10 +333,7 @@ function MobileLineItem({
variant="ghost" variant="ghost"
size="sm" size="sm"
onClick={() => onRemove(index)} onClick={() => onRemove(index)}
className={cn( className="text-muted-foreground hover:text-destructive h-8 w-8 p-0"
"text-muted-foreground hover:text-destructive h-8 w-8 p-0 transition-colors",
!canRemove && "cursor-not-allowed opacity-50",
)}
disabled={!canRemove} disabled={!canRemove}
aria-label="Remove item" aria-label="Remove item"
> >

View File

@@ -71,7 +71,7 @@ export function FloatingActionBar({
<CardContent className="flex items-center justify-between p-4"> <CardContent className="flex items-center justify-between p-4">
{/* Left content */} {/* Left content */}
{leftContent && ( {leftContent && (
<div className="animate-fade-in flex flex-1 items-center gap-3"> <div className="text-card-foreground animate-fade-in flex flex-1 items-center gap-3">
{leftContent} {leftContent}
</div> </div>
)} )}

View File

@@ -52,7 +52,7 @@ export function PageHeader({
)} )}
</div> </div>
{children && ( {children && (
<div className="animate-slide-in-right animate-delay-200 flex flex-shrink-0 gap-2 sm:gap-3 [&>*]:h-8 [&>*]:px-2 [&>*]:text-sm sm:[&>*]:h-10 sm:[&>*]:px-4 sm:[&>*]:text-base [&>*>span]:hidden sm:[&>*>span]:inline [&>*>svg]:mr-0 sm:[&>*>svg]:mr-2"> <div className="animate-slide-in-right animate-delay-200 flex flex-shrink-0 gap-2 sm:gap-3">
{children} {children}
</div> </div>
)} )}

View File

@@ -44,9 +44,9 @@ export function Sidebar() {
key={link.href} key={link.href}
href={link.href} href={link.href}
aria-current={pathname === link.href ? "page" : undefined} aria-current={pathname === link.href ? "page" : undefined}
className={`flex items-center gap-3 px-3 py-2.5 text-sm font-medium transition-colors ${ className={`flex items-center gap-3 rounded-md px-3 py-2.5 text-sm font-medium transition-colors ${
pathname === link.href pathname === link.href
? "bg-sidebar-primary text-sidebar-primary-foreground" ? "bg-sidebar-accent text-sidebar-accent-foreground"
: "text-sidebar-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground" : "text-sidebar-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
}`} }`}
> >

View File

@@ -0,0 +1,16 @@
"use client";
import * as React from "react";
import { ThemeProvider as NextThemesProvider } from "next-themes";
import { type ThemeProviderProps } from "next-themes/dist/types";
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return (
<NextThemesProvider
{...props}
themes={["light", "dark", "theme-sunset", "theme-forest"]}
>
{children}
</NextThemesProvider>
);
}

View File

@@ -0,0 +1,40 @@
"use client";
import * as React from "react";
import { Moon, Sun } from "lucide-react";
import { useTheme } from "next-themes";
import { Button } from "~/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "~/components/ui/dropdown-menu";
export function ThemeSwitcher() {
const { setTheme } = useTheme();
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="icon">
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setTheme("light")}>
Light
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("dark")}>
Dark
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("system")}>
System
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}

View File

@@ -1,23 +1,25 @@
import * as React from "react" import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority" import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "~/lib/utils" import { cn } from "~/lib/utils";
const alertVariants = cva( const alertVariants = cva(
"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", "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",
{ {
variants: { variants: {
variant: { variant: {
default: "bg-background text-foreground", default: "bg-background text-foreground",
destructive: destructive:
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
success:
"border-success/50 text-success dark:border-success [&>svg]:text-success",
}, },
}, },
defaultVariants: { defaultVariants: {
variant: "default", variant: "default",
}, },
} },
) );
const Alert = React.forwardRef< const Alert = React.forwardRef<
HTMLDivElement, HTMLDivElement,
@@ -29,8 +31,8 @@ const Alert = React.forwardRef<
className={cn(alertVariants({ variant }), className)} className={cn(alertVariants({ variant }), className)}
{...props} {...props}
/> />
)) ));
Alert.displayName = "Alert" Alert.displayName = "Alert";
const AlertTitle = React.forwardRef< const AlertTitle = React.forwardRef<
HTMLParagraphElement, HTMLParagraphElement,
@@ -38,11 +40,11 @@ const AlertTitle = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<h5 <h5
ref={ref} ref={ref}
className={cn("mb-1 font-medium leading-none tracking-tight", className)} className={cn("mb-1 leading-none font-medium tracking-tight", className)}
{...props} {...props}
/> />
)) ));
AlertTitle.displayName = "AlertTitle" AlertTitle.displayName = "AlertTitle";
const AlertDescription = React.forwardRef< const AlertDescription = React.forwardRef<
HTMLParagraphElement, HTMLParagraphElement,
@@ -53,7 +55,7 @@ const AlertDescription = React.forwardRef<
className={cn("text-sm [&_p]:leading-relaxed", className)} className={cn("text-sm [&_p]:leading-relaxed", className)}
{...props} {...props}
/> />
)) ));
AlertDescription.displayName = "AlertDescription" AlertDescription.displayName = "AlertDescription";
export { Alert, AlertTitle, AlertDescription } export { Alert, AlertTitle, AlertDescription };

View File

@@ -5,29 +5,17 @@ import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "~/lib/utils"; import { cn } from "~/lib/utils";
const badgeVariants = cva( const badgeVariants = cva(
"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", "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{ {
variants: { variants: {
variant: { variant: {
default: default:
"border-slate-300 bg-slate-200 text-slate-800 shadow-sm dark:border-slate-600 dark:bg-slate-700 dark:text-slate-200", "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
secondary: secondary:
"border-slate-300 bg-slate-200/80 text-slate-700 shadow-sm dark:border-slate-600 dark:bg-slate-700/80 dark:text-slate-300", "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive: destructive:
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
outline: outline: "text-foreground",
"border-2 border-slate-300 bg-transparent text-slate-700 dark:border-slate-600 dark:text-slate-300",
success: "border-transparent bg-status-success [a&]:hover:opacity-90",
warning: "border-transparent bg-status-warning [a&]:hover:opacity-90",
error: "border-transparent bg-status-error [a&]:hover:opacity-90",
info: "border-transparent bg-status-info [a&]:hover:opacity-90",
// Outlined variants for status badges
"outline-draft":
"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-destructive/40 text-destructive bg-transparent",
}, },
}, },
defaultVariants: { defaultVariants: {
@@ -39,18 +27,10 @@ const badgeVariants = cva(
function Badge({ function Badge({
className, className,
variant, variant,
asChild = false,
...props ...props
}: React.ComponentProps<"span"> & }: React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof badgeVariants>) {
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "span";
return ( return (
<Comp <div className={cn(badgeVariants({ variant }), className)} {...props} />
data-slot="badge"
className={cn(badgeVariants({ variant }), className)}
{...props}
/>
); );
} }

View File

@@ -5,29 +5,26 @@ import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "~/lib/utils"; import { cn } from "~/lib/utils";
const buttonVariants = cva( const buttonVariants = cva(
"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", "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
{ {
variants: { variants: {
variant: { variant: {
default: default:
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", "bg-primary text-primary-foreground shadow hover:bg-primary/90",
brand:
"bg-primary text-primary-foreground shadow-lg hover:bg-primary/90 hover:shadow-xl font-medium",
destructive: 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", "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
outline: outline:
"border border-border/40 bg-background/60 shadow-sm hover:bg-accent/50 hover:text-foreground-foreground hover:border-border/60 transition-colors duration-150", "border border-input bg-transparent shadow-sm hover:bg-accent hover:text-accent-foreground",
secondary: secondary:
"bg-secondary text-muted-foreground-foreground shadow-xs hover:bg-secondary/80", "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: ghost: "hover:bg-accent hover:text-accent-foreground",
"hover:bg-accent hover:text-foreground-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline", link: "text-primary underline-offset-4 hover:underline",
}, },
size: { size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3", default: "h-9 px-4 py-2",
sm: "h-8 gap-1.5 px-3 has-[>svg]:px-2.5", sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 px-6 has-[>svg]:px-4", lg: "h-10 rounded-md px-8",
icon: "size-9", icon: "h-9 w-9",
}, },
}, },
defaultVariants: { defaultVariants: {
@@ -37,25 +34,24 @@ const buttonVariants = cva(
}, },
); );
function Button({ export interface ButtonProps
className, extends React.ButtonHTMLAttributes<HTMLButtonElement>,
variant, VariantProps<typeof buttonVariants> {
size, asChild?: boolean;
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean;
}) {
const Comp = asChild ? Slot : "button";
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
);
} }
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
);
},
);
Button.displayName = "Button";
export { Button, buttonVariants }; export { Button, buttonVariants };

View File

@@ -7,7 +7,7 @@ function Card({ className, ...props }: React.ComponentProps<"div">) {
<div <div
data-slot="card" data-slot="card"
className={cn( className={cn(
"bg-card text-card-foreground border-border/40 flex flex-col border shadow-lg", "bg-card text-card-foreground border-border/40 flex flex-col rounded-lg border shadow-lg",
className, className,
)} )}
{...props} {...props}

View File

@@ -1,22 +1,24 @@
import { cn } from "~/lib/utils";
import * as React from "react"; import * as React from "react";
import { cn } from "~/lib/utils"; export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
function Input({ className, type, ...props }: React.ComponentProps<"input">) { const Input = React.forwardRef<HTMLInputElement, InputProps>(
return ( ({ className, type, ...props }, ref) => {
<input return (
type={type} <input
data-slot="input" type={type}
className={cn( 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 border px-3 py-2 text-sm shadow-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", "border-input placeholder:text-muted-foreground focus-visible:ring-ring flex h-9 w-full rounded-md border bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-1 focus-visible:outline-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]", className,
"hover:border-border/60 hover:bg-background/60", )}
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", ref={ref}
className, {...props}
)} />
{...props} );
/> },
); );
} Input.displayName = "Input";
export { Input }; export { Input };

View File

@@ -76,7 +76,7 @@ export function NumberInput({
return ( return (
<div <div
className={cn( className={cn(
"bg-background flex h-9 items-center justify-center text-sm shadow-none", "bg-background border-input flex h-9 items-center justify-center rounded-md border text-sm shadow-none",
widthClass, widthClass,
disabled && "cursor-not-allowed opacity-50", disabled && "cursor-not-allowed opacity-50",
className, className,
@@ -86,7 +86,7 @@ export function NumberInput({
type="button" type="button"
onClick={handleDecrement} onClick={handleDecrement}
disabled={disabled || value <= min} disabled={disabled || value <= min}
className="text-muted-foreground hover:text-foreground flex h-6 w-6 items-center justify-center disabled:cursor-not-allowed disabled:opacity-50" className="text-muted-foreground hover:text-foreground flex h-full w-8 items-center justify-center rounded-l-md disabled:cursor-not-allowed disabled:opacity-50"
> >
</button> </button>
@@ -113,7 +113,7 @@ export function NumberInput({
type="button" type="button"
onClick={handleIncrement} onClick={handleIncrement}
disabled={disabled || (max !== undefined && value >= max)} disabled={disabled || (max !== undefined && value >= max)}
className="text-muted-foreground hover:text-foreground flex h-6 w-6 items-center justify-center disabled:cursor-not-allowed disabled:opacity-50" className="text-muted-foreground hover:text-foreground flex h-full w-8 items-center justify-center rounded-r-md disabled:cursor-not-allowed disabled:opacity-50"
> >
+ +
</button> </button>

View File

@@ -42,7 +42,7 @@ function SelectTrigger({
data-slot="select-trigger" data-slot="select-trigger"
data-size={size} data-size={size}
className={cn( className={cn(
"data-[placeholder]:text-muted-foreground border-input bg-background text-foreground focus-visible:border-ring focus-visible:ring-ring/50 relative flex h-10 w-full items-center justify-start gap-2 border px-3 py-2 pr-8 text-left 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 relative flex h-10 w-full items-center justify-start gap-2 rounded-md border px-3 py-2 pr-8 text-left text-sm shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className, className,
)} )}
{...props} {...props}
@@ -68,7 +68,7 @@ function SelectContent({
<SelectPrimitive.Content <SelectPrimitive.Content
data-slot="select-content" data-slot="select-content"
className={cn( 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 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 rounded-md border-0 shadow-md",
position === "popper" && 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", "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, className,
@@ -212,7 +212,7 @@ function SelectContentWithSearch({
<SelectPrimitive.Content <SelectPrimitive.Content
data-slot="select-content" data-slot="select-content"
className={cn( 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 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 rounded-md border-0 shadow-md",
position === "popper" && 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", "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, className,

View File

@@ -6,55 +6,17 @@ const Toaster = ({ ...props }: ToasterProps) => {
return ( return (
<Sonner <Sonner
className="toaster group" className="toaster group"
position="top-center" position="bottom-right"
closeButton closeButton
richColors={false} richColors
expand={true}
duration={4000}
style={
{
"--normal-bg": "hsl(var(--card))",
"--normal-text": "hsl(var(--foreground))",
"--normal-border": "hsl(var(--border))",
"--success-bg": "hsl(var(--card))",
"--success-text": "hsl(var(--foreground))",
"--success-border": "hsl(142 76% 36%)",
"--error-bg": "hsl(var(--card))",
"--error-text": "hsl(var(--foreground))",
"--error-border": "hsl(0 84% 60%)",
"--warning-bg": "hsl(var(--card))",
"--warning-text": "hsl(var(--foreground))",
"--warning-border": "hsl(38 92% 50%)",
"--info-bg": "hsl(var(--card))",
"--info-text": "hsl(var(--foreground))",
"--info-border": "hsl(221 83% 53%)",
backgroundColor: "hsl(var(--card))",
} as React.CSSProperties
}
toastOptions={{ toastOptions={{
classNames: { classNames: {
toast: toast: "group toast",
"group toast group-[.toaster]:bg-card group-[.toaster]:text-foreground group-[.toaster]:border group-[.toaster]:border-border group-[.toaster]:shadow-lg group-[.toaster]:rounded-none group-[.toaster]:font-mono !bg-card", description: "group-[.toast]:text-muted-foreground",
description:
"group-[.toast]:text-muted-foreground group-[.toast]:text-sm",
actionButton: actionButton:
"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground group-[.toast]:hover:bg-primary/90 group-[.toast]:rounded-none group-[.toast]:border-none group-[.toast]:font-mono group-[.toast]:font-medium", "group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
cancelButton: cancelButton:
"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground group-[.toast]:hover:bg-muted/80 group-[.toast]:rounded-none group-[.toast]:border group-[.toast]:border-border group-[.toast]:font-mono", "group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
closeButton:
"group-[.toast]:bg-background group-[.toast]:text-foreground group-[.toast]:border group-[.toast]:border-border group-[.toast]:hover:bg-muted group-[.toast]:rounded-none group-[.toast]:absolute group-[.toast]:top-1/2 group-[.toast]:right-2 group-[.toast]:-translate-y-1/2 group-[.toast]:w-5 group-[.toast]:h-5 group-[.toast]:flex-shrink-0",
success:
"group-[.toaster]:bg-card group-[.toaster]:text-foreground group-[.toaster]:border group-[.toaster]:border-border group-[.toaster]:border-l-4 group-[.toaster]:border-l-green-500 group-[.toaster]:shadow-lg",
error:
"group-[.toaster]:bg-card group-[.toaster]:text-foreground group-[.toaster]:border group-[.toaster]:border-border group-[.toaster]:border-l-4 group-[.toaster]:border-l-red-500 group-[.toaster]:shadow-lg",
warning:
"group-[.toaster]:bg-card group-[.toaster]:text-foreground group-[.toaster]:border group-[.toaster]:border-border group-[.toaster]:border-l-4 group-[.toaster]:border-l-yellow-500 group-[.toaster]:shadow-lg",
info: "group-[.toaster]:bg-card group-[.toaster]:text-foreground group-[.toaster]:border group-[.toaster]:border-border group-[.toaster]:border-l-4 group-[.toaster]:border-l-blue-500 group-[.toaster]:shadow-lg",
title:
"group-[.toast]:text-foreground group-[.toast]:font-semibold group-[.toast]:text-sm group-[.toast]:font-mono",
},
style: {
fontFamily: "var(--font-geist-mono, ui-monospace, monospace)",
}, },
}} }}
{...props} {...props}

View File

@@ -1,31 +1,31 @@
"use client"; "use client";
import * as React from "react";
import * as SwitchPrimitive from "@radix-ui/react-switch"; import * as SwitchPrimitive from "@radix-ui/react-switch";
import { Check, X } from "lucide-react";
import * as React from "react";
import { cn } from "~/lib/utils"; import { cn } from "~/lib/utils";
function Switch({ const Switch = React.forwardRef<
className, React.ElementRef<typeof SwitchPrimitive.Root>,
...props React.ComponentPropsWithoutRef<typeof SwitchPrimitive.Root>
}: React.ComponentProps<typeof SwitchPrimitive.Root>) { >(({ className, ...props }, ref) => (
return ( <SwitchPrimitive.Root
<SwitchPrimitive.Root className={cn(
data-slot="switch" "peer focus-visible:ring-ring focus-visible:ring-offset-background data-[state=checked]:bg-primary data-[state=unchecked]:bg-input inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
ref={ref}
>
<SwitchPrimitive.Thumb
className={cn( className={cn(
"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", "bg-background pointer-events-none block h-5 w-5 rounded-full shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0",
className,
)} )}
{...props}
> >
<SwitchPrimitive.Thumb <Check className="text-primary absolute inset-0 m-auto h-4 w-4 opacity-0 transition-opacity data-[state=checked]:opacity-100" />
data-slot="switch-thumb" </SwitchPrimitive.Thumb>
className={cn( </SwitchPrimitive.Root>
"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", ));
)} Switch.displayName = SwitchPrimitive.Root.displayName;
/>
</SwitchPrimitive.Root>
);
}
export { Switch }; export { Switch };

View File

@@ -1,66 +1,65 @@
"use client" "use client";
import * as React from "react" import * as React from "react";
import * as TabsPrimitive from "@radix-ui/react-tabs" import * as TabsPrimitive from "@radix-ui/react-tabs";
import { cn } from "~/lib/utils" import { cn } from "~/lib/utils";
function Tabs({ const Tabs = React.forwardRef<
className, React.ElementRef<typeof TabsPrimitive.Root>,
...props React.ComponentPropsWithoutRef<typeof TabsPrimitive.Root>
}: React.ComponentProps<typeof TabsPrimitive.Root>) { >(({ className, ...props }, ref) => (
return ( <TabsPrimitive.Root
<TabsPrimitive.Root ref={ref}
data-slot="tabs" className={cn("flex flex-col gap-2", className)}
className={cn("flex flex-col gap-2", className)} {...props}
{...props} />
/> ));
) Tabs.displayName = TabsPrimitive.Root.displayName;
}
function TabsList({ const TabsList = React.forwardRef<
className, React.ElementRef<typeof TabsPrimitive.List>,
...props React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
}: React.ComponentProps<typeof TabsPrimitive.List>) { >(({ className, ...props }, ref) => (
return ( <TabsPrimitive.List
<TabsPrimitive.List ref={ref}
data-slot="tabs-list" className={cn(
className={cn( "bg-muted text-muted-foreground inline-flex h-9 items-center justify-center rounded-lg p-1",
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center p-[3px]", className,
className )}
)} {...props}
{...props} />
/> ));
) TabsList.displayName = TabsPrimitive.List.displayName;
}
function TabsTrigger({ const TabsTrigger = React.forwardRef<
className, React.ElementRef<typeof TabsPrimitive.Trigger>,
...props React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) { >(({ className, ...props }, ref) => (
return ( <TabsPrimitive.Trigger
<TabsPrimitive.Trigger ref={ref}
data-slot="tabs-trigger" className={cn(
className={cn( "ring-offset-background focus-visible:ring-ring data-[state=active]:bg-background data-[state=active]:text-foreground inline-flex items-center justify-center rounded-md px-3 py-1 text-sm font-medium whitespace-nowrap transition-all focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow",
"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,
className )}
)} {...props}
{...props} />
/> ));
) TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
}
function TabsContent({ const TabsContent = React.forwardRef<
className, React.ElementRef<typeof TabsPrimitive.Content>,
...props React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
}: React.ComponentProps<typeof TabsPrimitive.Content>) { >(({ className, ...props }, ref) => (
return ( <TabsPrimitive.Content
<TabsPrimitive.Content ref={ref}
data-slot="tabs-content" className={cn(
className={cn("flex-1 outline-none", className)} "ring-offset-background focus-visible:ring-ring mt-2 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none",
{...props} className,
/> )}
) {...props}
} />
));
TabsContent.displayName = TabsPrimitive.Content.displayName;
export { Tabs, TabsList, TabsTrigger, TabsContent } export { Tabs, TabsList, TabsTrigger, TabsContent };

View File

@@ -2,17 +2,23 @@ import * as React from "react";
import { cn } from "~/lib/utils"; import { cn } from "~/lib/utils";
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) { export interface TextareaProps
return ( extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
<textarea
data-slot="textarea" const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
className={cn( ({ className, ...props }, ref) => {
"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", return (
className, <textarea
)} className={cn(
{...props} "border-input placeholder:text-muted-foreground focus-visible:ring-ring flex min-h-[60px] w-full rounded-md border bg-transparent px-3 py-2 text-sm shadow-sm focus-visible:ring-1 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50",
/> className,
); )}
} ref={ref}
{...props}
/>
);
},
);
Textarea.displayName = "Textarea";
export { Textarea }; export { Textarea };

View File

@@ -2,124 +2,242 @@
@import "tw-animate-css"; @import "tw-animate-css";
:root { :root {
--background: oklch(0.98 0 0); --background: oklch(0.98 0.01 230);
--foreground: oklch(0.15 0 0); --foreground: oklch(0.2 0.03 230);
--card: oklch(1 0 0); --card: oklch(1 0 0);
--card-foreground: oklch(0.15 0 0); --card-foreground: oklch(0.2 0.03 230);
--popover: oklch(1 0 0); --popover: oklch(1 0 0);
--popover-foreground: oklch(0.15 0 0); --popover-foreground: oklch(0.2 0.03 230);
--primary: oklch(0.55 0 0); --primary: oklch(0.6 0.15 220);
--primary-foreground: oklch(0.98 0 0); --primary-foreground: oklch(0.98 0.01 230);
--secondary: oklch(0.94 0 0); --secondary: oklch(0.92 0.02 230);
--secondary-foreground: oklch(0.2 0 0); --secondary-foreground: oklch(0.2 0.03 230);
--muted: oklch(0.94 0 0); --muted: oklch(0.92 0.02 230);
--muted-foreground: oklch(0.55 0 0); --muted-foreground: oklch(0.5 0.03 230);
--accent: oklch(0.94 0 0); --accent: oklch(0.94 0.02 230);
--accent-foreground: oklch(0.2 0 0); --accent-foreground: oklch(0.2 0.03 230);
--destructive: oklch(0.58 0.24 28); --destructive: oklch(0.58 0.24 28);
--destructive-foreground: oklch(0.98 0 0); --destructive-foreground: oklch(0.98 0.01 230);
--success: oklch(0.55 0.15 142); --success: oklch(0.55 0.15 142);
--success-foreground: oklch(0.98 0 0); --success-foreground: oklch(0.98 0.01 230);
--warning: oklch(0.65 0.15 38); --warning: oklch(0.65 0.15 38);
--warning-foreground: oklch(0.1 0 0); --warning-foreground: oklch(0.2 0.03 230);
--border: oklch(0.9 0 0); --border: oklch(0.9 0.02 230);
--input: oklch(0.9 0 0); --input: oklch(0.9 0.02 230);
--ring: oklch(0.71 0 0); --ring: oklch(0.6 0.15 220);
--chart-1: oklch(0.65 0.15 258); --chart-1: oklch(0.6 0.15 220);
--chart-2: oklch(0.7 0.14 142); --chart-2: oklch(0.7 0.14 142);
--chart-3: oklch(0.65 0.2 27); --chart-3: oklch(0.65 0.2 27);
--chart-4: oklch(0.6 0.18 302); --chart-4: oklch(0.6 0.18 302);
--chart-5: oklch(0.62 0.16 197); --chart-5: oklch(0.62 0.16 197);
--sidebar: oklch(0.96 0 0); --sidebar: oklch(0.96 0.01 230);
--sidebar-foreground: oklch(0.15 0 0); --sidebar-foreground: oklch(0.2 0.03 230);
--sidebar-primary: oklch(0.2 0 0); --sidebar-primary: oklch(0.2 0.03 230);
--sidebar-primary-foreground: oklch(0.98 0 0); --sidebar-primary-foreground: oklch(0.98 0.01 230);
--sidebar-accent: oklch(0.92 0 0); --sidebar-accent: oklch(0.92 0.02 230);
--sidebar-accent-foreground: oklch(0.2 0 0); --sidebar-accent-foreground: oklch(0.2 0.03 230);
--sidebar-border: oklch(0.88 0 0); --sidebar-border: oklch(0.88 0.02 230);
--sidebar-ring: oklch(0.71 0 0); --sidebar-ring: oklch(0.6 0.15 220);
--navbar: oklch(0.96 0 0); --navbar: oklch(0.96 0.01 230);
--navbar-foreground: oklch(0.15 0 0); --navbar-foreground: oklch(0.2 0.03 230);
--navbar-border: oklch(0.88 0 0); --navbar-border: oklch(0.88 0.02 230);
--font-sans: Geist Mono, monospace; --font-geist-sans: "Geist", sans-serif;
--font-serif: Geist Mono, monospace; --font-geist-mono: "Geist Mono", monospace;
--font-mono: Geist Mono, monospace; --font-sans: "Geist", sans-serif;
--radius: 0rem; --font-mono: "Geist Mono", monospace;
--shadow-2xs: 0px 1px 0px 0px hsl(0 0% 0% / 0); --font-serif: "Instrument Serif", serif;
--shadow-xs: 0px 1px 0px 0px hsl(0 0% 0% / 0); --radius: 0.8rem;
--shadow-sm: --shadow-2xs: 0px 1px 2px 0px hsl(0 0% 0% / 0.05);
0px 1px 0px 0px hsl(0 0% 0% / 0), 0px 1px 2px -1px hsl(0 0% 0% / 0); --shadow-xs: 0px 1px 2px 0px hsl(0 0% 0% / 0.05);
--shadow: 0px 1px 0px 0px hsl(0 0% 0% / 0), 0px 1px 2px -1px hsl(0 0% 0% / 0); --shadow-sm: 0px 1px 2px 0px hsl(0 0% 0% / 0.05);
--shadow:
0px 1px 3px 0px hsl(0 0% 0% / 0.1), 0px 1px 2px 0px hsl(0 0% 0% / 0.06);
--shadow-md: --shadow-md:
0px 1px 0px 0px hsl(0 0% 0% / 0), 0px 2px 4px -1px hsl(0 0% 0% / 0); 0px 4px 6px -1px hsl(0 0% 0% / 0.1), 0px 2px 4px -1px hsl(0 0% 0% / 0.06);
--shadow-lg: --shadow-lg:
0px 1px 0px 0px hsl(0 0% 0% / 0), 0px 4px 6px -1px hsl(0 0% 0% / 0); 0px 10px 15px -3px hsl(0 0% 0% / 0.1), 0px 4px 6px -2px hsl(0 0% 0% / 0.05);
--shadow-xl: --shadow-xl:
0px 1px 0px 0px hsl(0 0% 0% / 0), 0px 8px 10px -1px hsl(0 0% 0% / 0); 0px 20px 25px -5px hsl(0 0% 0% / 0.1),
--shadow-2xl: 0px 1px 0px 0px hsl(0 0% 0% / 0); 0px 10px 10px -5px hsl(0 0% 0% / 0.04);
--shadow-2xl: 0px 25px 50px -12px hsl(0 0% 0% / 0.25);
--tracking-normal: 0em; --tracking-normal: 0em;
--spacing: 0.25rem; --spacing: 0.25rem;
} }
@media (prefers-color-scheme: dark) { .dark {
:root { --background: oklch(0.15 0.03 230);
--background: oklch(0.12 0 0); --foreground: oklch(0.9 0.01 230);
--foreground: oklch(0.98 0 0); --card: oklch(0.2 0.03 230);
--card: oklch(0.18 0 0); --card-foreground: oklch(0.9 0.01 230);
--card-foreground: oklch(0.98 0 0); --popover: oklch(0.22 0.03 230);
--popover: oklch(0.22 0 0); --popover-foreground: oklch(0.9 0.01 230);
--popover-foreground: oklch(0.98 0 0); --primary: oklch(0.6 0.15 220);
--primary: oklch(0.55 0 0); --primary-foreground: oklch(0.98 0.01 230);
--primary-foreground: oklch(0.98 0 0); --secondary: oklch(0.25 0.03 230);
--secondary: oklch(0.22 0 0); --secondary-foreground: oklch(0.9 0.01 230);
--secondary-foreground: oklch(0.98 0 0); --muted: oklch(0.25 0.03 230);
--muted: oklch(0.22 0 0); --muted-foreground: oklch(0.7 0.01 230);
--muted-foreground: oklch(0.71 0 0); --accent: oklch(0.3 0.03 230);
--accent: oklch(0.3 0 0); --accent-foreground: oklch(0.9 0.01 230);
--accent-foreground: oklch(0.98 0 0); --destructive: oklch(0.7 0.19 22);
--destructive: oklch(0.7 0.19 22); --destructive-foreground: oklch(0.2 0.03 230);
--destructive-foreground: oklch(0.22 0 0); --success: oklch(0.6 0.15 142);
--success: oklch(0.6 0.15 142); --success-foreground: oklch(0.98 0.01 230);
--success-foreground: oklch(0.98 0 0); --warning: oklch(0.7 0.15 38);
--warning: oklch(0.7 0.15 38); --warning-foreground: oklch(0.2 0.03 230);
--warning-foreground: oklch(0.1 0 0); --border: oklch(0.28 0.03 230);
--border: oklch(0.28 0 0); --input: oklch(0.35 0.03 230);
--input: oklch(0.35 0 0); --ring: oklch(0.6 0.15 220);
--ring: oklch(0.55 0 0); --chart-1: oklch(0.6 0.15 220);
--chart-1: oklch(0.7 0.15 258); --chart-2: oklch(0.75 0.14 142);
--chart-2: oklch(0.75 0.14 142); --chart-3: oklch(0.7 0.2 27);
--chart-3: oklch(0.7 0.2 27); --chart-4: oklch(0.65 0.18 302);
--chart-4: oklch(0.65 0.18 302); --chart-5: oklch(0.67 0.16 197);
--chart-5: oklch(0.67 0.16 197); --sidebar: oklch(0.1 0.03 230);
--sidebar: oklch(0.08 0 0); --sidebar-foreground: oklch(0.9 0.01 230);
--sidebar-foreground: oklch(0.98 0 0); --sidebar-primary: oklch(0.9 0.01 230);
--sidebar-primary: oklch(0.98 0 0); --sidebar-primary-foreground: oklch(0.1 0.03 230);
--sidebar-primary-foreground: oklch(0.08 0 0); --sidebar-accent: oklch(0.2 0.03 230);
--sidebar-accent: oklch(0.15 0 0); --sidebar-accent-foreground: oklch(0.9 0.01 230);
--sidebar-accent-foreground: oklch(0.98 0 0); --sidebar-border: oklch(0.25 0.03 230);
--sidebar-border: oklch(0.25 0 0); --sidebar-ring: oklch(0.35 0.03 230);
--sidebar-ring: oklch(0.35 0 0); --navbar: oklch(0.1 0.03 230);
--navbar: oklch(0.08 0 0); --navbar-foreground: oklch(0.9 0.01 230);
--navbar-foreground: oklch(0.98 0 0); --navbar-border: oklch(0.25 0.03 230);
--navbar-border: oklch(0.25 0 0); --font-sans: "Geist", sans-serif;
--font-sans: Geist Mono, monospace; --font-mono: "Geist Mono", monospace;
--font-serif: Geist Mono, monospace; --font-serif: "Instrument Serif", serif;
--font-mono: Geist Mono, monospace; --radius: 0.8rem;
--radius: 0rem; --shadow-2xs: 0px 1px 2px 0px hsl(0 0% 0% / 0.05);
--shadow-2xs: 0px 1px 0px 0px hsl(0 0% 0% / 0); --shadow-xs: 0px 1px 2px 0px hsl(0 0% 0% / 0.05);
--shadow-xs: 0px 1px 0px 0px hsl(0 0% 0% / 0); --shadow-sm: 0px 1px 2px 0px hsl(0 0% 0% / 0.05);
--shadow-sm: --shadow:
0px 1px 0px 0px hsl(0 0% 0% / 0), 0px 1px 2px -1px hsl(0 0% 0% / 0); 0px 1px 3px 0px hsl(0 0% 0% / 0.1), 0px 1px 2px 0px hsl(0 0% 0% / 0.06);
--shadow: --shadow-md:
0px 1px 0px 0px hsl(0 0% 0% / 0), 0px 1px 2px -1px hsl(0 0% 0% / 0); 0px 4px 6px -1px hsl(0 0% 0% / 0.1), 0px 2px 4px -1px hsl(0 0% 0% / 0.06);
--shadow-md: --shadow-lg:
0px 1px 0px 0px hsl(0 0% 0% / 0), 0px 2px 4px -1px hsl(0 0% 0% / 0); 0px 10px 15px -3px hsl(0 0% 0% / 0.1), 0px 4px 6px -2px hsl(0 0% 0% / 0.05);
--shadow-lg: --shadow-xl:
0px 1px 0px 0px hsl(0 0% 0% / 0), 0px 4px 6px -1px hsl(0 0% 0% / 0); 0px 20px 25px -5px hsl(0 0% 0% / 0.1),
--shadow-xl: 0px 10px 10px -5px hsl(0 0% 0% / 0.04);
0px 1px 0px 0px hsl(0 0% 0% / 0), 0px 8px 10px -1px hsl(0 0% 0% / 0); --shadow-2xl: 0px 25px 50px -12px hsl(0 0% 0% / 0.25);
--shadow-2xl: 0px 1px 0px 0px hsl(0 0% 0% / 0); }
}
.light {
--background: oklch(0.98 0.01 230);
--foreground: oklch(0.2 0.03 230);
--card: oklch(1 0 0);
--card-foreground: oklch(0.2 0.03 230);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.2 0.03 230);
--primary: oklch(0.6 0.15 220);
--primary-foreground: oklch(0.98 0.01 230);
--secondary: oklch(0.92 0.02 230);
--secondary-foreground: oklch(0.2 0.03 230);
--muted: oklch(0.92 0.02 230);
--muted-foreground: oklch(0.5 0.03 230);
--accent: oklch(0.94 0.02 230);
--accent-foreground: oklch(0.2 0.03 230);
--destructive: oklch(0.58 0.24 28);
--destructive-foreground: oklch(0.98 0.01 230);
--success: oklch(0.55 0.15 142);
--success-foreground: oklch(0.98 0.01 230);
--warning: oklch(0.65 0.15 38);
--warning-foreground: oklch(0.2 0.03 230);
--border: oklch(0.9 0.02 230);
--input: oklch(0.9 0.02 230);
--ring: oklch(0.6 0.15 220);
--chart-1: oklch(0.6 0.15 220);
--chart-2: oklch(0.7 0.14 142);
--chart-3: oklch(0.65 0.2 27);
--chart-4: oklch(0.6 0.18 302);
--chart-5: oklch(0.62 0.16 197);
--sidebar: oklch(0.96 0.01 230);
--sidebar-foreground: oklch(0.2 0.03 230);
--sidebar-primary: oklch(0.2 0.03 230);
--sidebar-primary-foreground: oklch(0.98 0.01 230);
--sidebar-accent: oklch(0.92 0.02 230);
--sidebar-accent-foreground: oklch(0.2 0.03 230);
--sidebar-border: oklch(0.88 0.02 230);
--sidebar-ring: oklch(0.6 0.15 220);
--navbar: oklch(0.96 0.01 230);
--navbar-foreground: oklch(0.2 0.03 230);
--navbar-border: oklch(0.88 0.02 230);
--radius: 0.8rem;
}
.theme-sunset {
--background: oklch(0.98 0.01 40);
--foreground: oklch(0.2 0.05 40);
--card: oklch(1 0 0);
--card-foreground: oklch(0.2 0.05 40);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.2 0.05 40);
--primary: oklch(0.7 0.2 50);
--primary-foreground: oklch(0.98 0.01 40);
--secondary: oklch(0.94 0.02 40);
--secondary-foreground: oklch(0.2 0.05 40);
--muted: oklch(0.94 0.02 40);
--muted-foreground: oklch(0.5 0.05 40);
--accent: oklch(0.94 0.02 40);
--accent-foreground: oklch(0.2 0.05 40);
--destructive: oklch(0.58 0.24 28);
--destructive-foreground: oklch(0.98 0.01 40);
--success: oklch(0.55 0.15 142);
--success-foreground: oklch(0.98 0.01 40);
--warning: oklch(0.65 0.15 38);
--warning-foreground: oklch(0.2 0.05 40);
--border: oklch(0.9 0.02 40);
--input: oklch(0.9 0.02 40);
--ring: oklch(0.7 0.2 50);
--sidebar: oklch(0.96 0.01 40);
--sidebar-foreground: oklch(0.2 0.05 40);
--sidebar-primary: oklch(0.2 0.05 40);
--sidebar-primary-foreground: oklch(0.98 0.01 40);
--sidebar-accent: oklch(0.92 0.02 40);
--sidebar-accent-foreground: oklch(0.2 0.05 40);
--sidebar-border: oklch(0.88 0.02 40);
--sidebar-ring: oklch(0.7 0.2 50);
--navbar: oklch(0.96 0.01 40);
--navbar-foreground: oklch(0.2 0.05 40);
--navbar-border: oklch(0.88 0.02 40);
}
.theme-forest {
--background: oklch(0.98 0.01 140);
--foreground: oklch(0.2 0.05 140);
--card: oklch(1 0 0);
--card-foreground: oklch(0.2 0.05 140);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.2 0.05 140);
--primary: oklch(0.5 0.1 150);
--primary-foreground: oklch(0.98 0.01 140);
--secondary: oklch(0.94 0.02 140);
--secondary-foreground: oklch(0.2 0.05 140);
--muted: oklch(0.94 0.02 140);
--muted-foreground: oklch(0.5 0.05 140);
--accent: oklch(0.94 0.02 140);
--accent-foreground: oklch(0.2 0.05 140);
--destructive: oklch(0.58 0.24 28);
--destructive-foreground: oklch(0.98 0.01 140);
--success: oklch(0.55 0.15 142);
--success-foreground: oklch(0.98 0.01 140);
--warning: oklch(0.65 0.15 38);
--warning-foreground: oklch(0.2 0.05 140);
--border: oklch(0.9 0.02 140);
--input: oklch(0.9 0.02 140);
--ring: oklch(0.5 0.1 150);
--sidebar: oklch(0.96 0.01 140);
--sidebar-foreground: oklch(0.2 0.05 140);
--sidebar-primary: oklch(0.2 0.05 140);
--sidebar-primary-foreground: oklch(0.98 0.01 140);
--sidebar-accent: oklch(0.92 0.02 140);
--sidebar-accent-foreground: oklch(0.2 0.05 140);
--sidebar-border: oklch(0.88 0.02 140);
--sidebar-ring: oklch(0.5 0.1 150);
--navbar: oklch(0.96 0.01 140);
--navbar-foreground: oklch(0.2 0.05 140);
--navbar-border: oklch(0.88 0.02 140);
} }
@theme inline { @theme inline {
@@ -268,163 +386,58 @@ body {
} }
/* Sonner Toast Styles for beenvoice */ /* Sonner Toast Styles for beenvoice */
[data-sonner-toaster] { .toaster [data-sonner-toast] {
font-family: var(--font-geist-mono, ui-monospace, monospace) !important; background: oklch(var(--card));
--width: 380px; border-color: oklch(var(--border));
color: oklch(var(--foreground));
border-radius: var(--radius-lg);
font-family: var(--font-mono);
font-size: 0.875rem;
padding: 1rem;
} }
[data-sonner-toast] { .toaster [data-sonner-toast] [data-icon] {
background: hsl(var(--card)) !important; color: oklch(var(--primary));
background-color: hsl(var(--card)) !important;
border: 1px solid hsl(var(--border)) !important;
color: hsl(var(--foreground)) !important;
border-radius: 0 !important;
box-shadow:
0 1px 3px 0 rgb(0 0 0 / 0.1),
0 1px 2px -1px rgb(0 0 0 / 0.1) !important;
font-size: 14px !important;
padding: 16px 40px 16px 16px !important;
font-family: var(--font-geist-mono, ui-monospace, monospace) !important;
min-height: 56px !important;
position: relative !important;
} }
/* Toast types with colored left borders */ .toaster [data-sonner-toast][data-type="success"] [data-icon] {
[data-sonner-toast][data-type="success"] { color: oklch(var(--success));
border-left: 4px solid hsl(142 76% 36%) !important;
} }
[data-sonner-toast][data-type="error"] { .toaster [data-sonner-toast][data-type="error"] [data-icon] {
border-left: 4px solid hsl(0 84% 60%) !important; color: oklch(var(--destructive));
} }
[data-sonner-toast][data-type="warning"] { .toaster [data-sonner-toast][data-type="warning"] [data-icon] {
border-left: 4px solid hsl(38 92% 50%) !important; color: oklch(var(--warning));
} }
[data-sonner-toast][data-type="info"] { .toaster [data-sonner-toast] [data-title] {
border-left: 4px solid hsl(221 83% 53%) !important; font-weight: 600;
} }
/* Close button styling - comprehensive selectors */ .toaster [data-sonner-toast] [data-description] {
[data-sonner-toast] [data-close-button], color: oklch(var(--muted-foreground));
[data-sonner-toast] button[data-close-button],
[data-sonner-toast] button[aria-label="Close toast"],
[data-sonner-toast] > button,
li[data-sonner-toast] [data-close-button],
li[data-sonner-toast] button,
.sonner [data-close-button],
.sonner button[aria-label="Close toast"],
[data-sonner-toaster] [data-close-button],
[data-sonner-toaster] button[aria-label="Close toast"] {
background: hsl(var(--background)) !important;
border: 1px solid hsl(var(--border)) !important;
color: hsl(var(--foreground)) !important;
border-radius: 0 !important;
width: 20px !important;
height: 20px !important;
position: absolute !important;
top: 50% !important;
right: 8px !important;
left: auto !important;
transform: translateY(-50%) !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
cursor: pointer !important;
font-size: 12px !important;
flex-shrink: 0 !important;
margin: 0 !important;
z-index: 100 !important;
inset-inline-end: 8px !important;
inset-inline-start: auto !important;
} }
[data-sonner-toast] [data-close-button]:hover, .toaster [data-sonner-toast] [data-close-button] {
[data-sonner-toast] button[data-close-button]:hover, background: oklch(var(--muted));
[data-sonner-toast] button[aria-label="Close toast"]:hover, border-color: oklch(var(--border));
[data-sonner-toast] > button:hover, color: oklch(var(--muted-foreground));
li[data-sonner-toast] [data-close-button]:hover, border-radius: var(--radius-sm);
li[data-sonner-toast] button:hover, transition: all 0.2s ease-in-out;
.sonner [data-close-button]:hover,
.sonner button[aria-label="Close toast"]:hover,
[data-sonner-toaster] [data-close-button]:hover,
[data-sonner-toaster] button[aria-label="Close toast"]:hover {
background: hsl(var(--muted)) !important;
} }
/* Very specific override for close button positioning */ .toaster [data-sonner-toast] [data-close-button]:hover {
[data-sonner-toast] > div > button, background: oklch(var(--muted) / 0.8);
[data-sonner-toast] button:last-child, color: oklch(var(--foreground));
[data-sonner-toast] button:only-child {
position: absolute !important;
top: 12px !important;
right: 12px !important;
left: unset !important;
transform: none !important;
margin-left: auto !important;
margin-right: 0 !important;
float: right !important;
padding-right: 40px !important;
} }
/* Override any flex or grid positioning that puts button on left */
[data-sonner-toast] {
display: flex !important;
align-items: flex-start !important;
padding-left: 48px !important;
}
[data-sonner-toast] > div {
position: relative !important;
padding-right: 40px !important;
flex: 1 !important;
min-width: 0 !important;
}
/* Toast icon positioning */
[data-sonner-toast] [data-icon] {
position: absolute !important;
left: 16px !important;
top: 50% !important;
transform: translateY(-50%) !important;
flex-shrink: 0 !important;
width: 20px !important;
height: 20px !important;
}
/* Content styling */
[data-sonner-toast] [data-title] {
color: hsl(var(--foreground)) !important;
font-weight: 600 !important;
font-size: 14px !important;
font-family: var(--font-geist-mono, ui-monospace, monospace) !important;
margin: 0 !important;
line-height: 1.4 !important;
}
[data-sonner-toast] [data-description] {
color: hsl(var(--muted-foreground)) !important;
font-size: 13px !important;
font-family: var(--font-geist-mono, ui-monospace, monospace) !important;
margin: 0 !important;
line-height: 1.4 !important;
}
/* Content wrapper to avoid icon overlap */
[data-sonner-toast] [data-content] {
flex: 1 !important;
min-width: 0 !important;
padding-left: 0 !important;
margin-left: 0 !important;
}
/* Mobile responsive */
@media (max-width: 640px) { @media (max-width: 640px) {
[data-sonner-toaster] { .toaster [data-sonner-toaster] {
--width: calc(100vw - 32px); --width: calc(100vw - 32px);
left: 16px !important; left: 16px;
right: 16px !important; right: 16px;
} }
} }
@@ -839,26 +852,6 @@ html.user-reduce-motion *::after {
COMPONENT-SPECIFIC ANIMATIONS COMPONENT-SPECIFIC ANIMATIONS
======================================== */ ======================================== */
/* Stats Cards */
.stats-card {
animation: fadeInUp var(--animation-speed-slow) var(--animation-easing)
forwards;
opacity: 0;
}
.stats-card:nth-child(1) {
animation-delay: 0ms;
}
.stats-card:nth-child(2) {
animation-delay: 100ms;
}
.stats-card:nth-child(3) {
animation-delay: 200ms;
}
.stats-card:nth-child(4) {
animation-delay: 300ms;
}
/* Invoice Items */ /* Invoice Items */
.invoice-item { .invoice-item {
animation: fadeInUp var(--animation-speed-normal) var(--animation-easing) animation: fadeInUp var(--animation-speed-normal) var(--animation-easing)

View File

@@ -2,7 +2,7 @@ import type { Config } from "tailwindcss";
import animate from "tailwindcss-animate"; import animate from "tailwindcss-animate";
export default { export default {
darkMode: "media", darkMode: "class",
content: [ content: [
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}", "./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
"./src/components/**/*.{js,ts,jsx,tsx,mdx}", "./src/components/**/*.{js,ts,jsx,tsx,mdx}",
@@ -14,9 +14,9 @@ export default {
xs: "475px", xs: "475px",
}, },
fontFamily: { fontFamily: {
sans: ["var(--font-geist-mono)", "monospace"], sans: ["var(--font-geist-sans)", "sans-serif"],
mono: ["var(--font-geist-mono)", "monospace"], mono: ["var(--font-geist-mono)", "monospace"],
serif: ["var(--font-geist-mono)", "monospace"], serif: ["var(--font-serif)", "serif"],
}, },
colors: { colors: {
navbar: "var(--navbar)", navbar: "var(--navbar)",
@@ -37,6 +37,11 @@ export default {
"accordion-down": "accordion-down 0.2s ease-out", "accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out", "accordion-up": "accordion-up 0.2s ease-out",
}, },
borderRadius: {
lg: `var(--radius)`,
md: `calc(var(--radius) - 2px)`,
sm: `calc(var(--radius) - 4px)`,
},
}, },
}, },
plugins: [animate], plugins: [animate],