mirror of
https://github.com/soconnor0919/beenvoice.git
synced 2025-12-11 08:34:43 -05:00
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:
107
package.json
107
package.json
@@ -10,7 +10,6 @@
|
||||
"db:migrate": "drizzle-kit migrate",
|
||||
"db:push": "drizzle-kit push",
|
||||
"db:studio": "drizzle-kit studio",
|
||||
|
||||
"docker-up": "colima start && docker-compose up -d",
|
||||
"docker-down": "docker-compose down && colima stop",
|
||||
"deploy": "drizzle-kit push && next build",
|
||||
@@ -24,89 +23,85 @@
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@auth/drizzle-adapter": "^1.10.0",
|
||||
"@auth/drizzle-adapter": "^1.11.1",
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.14",
|
||||
"@radix-ui/react-checkbox": "^1.3.2",
|
||||
"@radix-ui/react-collapsible": "^1.1.11",
|
||||
"@radix-ui/react-dialog": "^1.1.14",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
"@radix-ui/react-navigation-menu": "^1.2.13",
|
||||
"@radix-ui/react-popover": "^1.1.14",
|
||||
"@radix-ui/react-progress": "^1.1.7",
|
||||
"@radix-ui/react-select": "^2.2.5",
|
||||
"@radix-ui/react-separator": "^1.1.7",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-switch": "^1.2.5",
|
||||
"@radix-ui/react-tabs": "^1.1.12",
|
||||
"@react-pdf/renderer": "^4.3.0",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||
"@radix-ui/react-checkbox": "^1.3.3",
|
||||
"@radix-ui/react-collapsible": "^1.1.12",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||
"@radix-ui/react-label": "^2.1.8",
|
||||
"@radix-ui/react-navigation-menu": "^1.2.14",
|
||||
"@radix-ui/react-popover": "^1.1.15",
|
||||
"@radix-ui/react-progress": "^1.1.8",
|
||||
"@radix-ui/react-select": "^2.2.6",
|
||||
"@radix-ui/react-separator": "^1.1.8",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@radix-ui/react-switch": "^1.2.6",
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@react-pdf/renderer": "^4.3.1",
|
||||
"@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",
|
||||
"@tiptap/extension-color": "^3.0.7",
|
||||
"@tiptap/extension-list-item": "^3.0.7",
|
||||
"@tiptap/extension-text-align": "^3.0.7",
|
||||
"@tiptap/extension-text-style": "^3.0.7",
|
||||
"@tiptap/react": "^3.0.7",
|
||||
"@tiptap/starter-kit": "^3.0.7",
|
||||
"@trpc/client": "^11.4.3",
|
||||
"@trpc/react-query": "^11.4.3",
|
||||
"@trpc/server": "^11.4.3",
|
||||
|
||||
"@tiptap/extension-color": "^3.11.0",
|
||||
"@tiptap/extension-list-item": "^3.11.0",
|
||||
"@tiptap/extension-text-align": "^3.11.0",
|
||||
"@tiptap/extension-text-style": "^3.11.0",
|
||||
"@tiptap/react": "^3.11.0",
|
||||
"@tiptap/starter-kit": "^3.11.0",
|
||||
"@trpc/client": "^11.7.2",
|
||||
"@trpc/react-query": "^11.7.2",
|
||||
"@trpc/server": "^11.7.2",
|
||||
"@vercel/analytics": "^1.5.0",
|
||||
"bcryptjs": "^3.0.2",
|
||||
"chrono-node": "^2.8.3",
|
||||
"bcryptjs": "^3.0.3",
|
||||
"chrono-node": "^2.9.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"drizzle-orm": "^0.44.4",
|
||||
"drizzle-orm": "^0.44.7",
|
||||
"file-saver": "^2.0.5",
|
||||
"framer-motion": "^12.23.12",
|
||||
|
||||
"framer-motion": "^12.23.24",
|
||||
"lucide-react": "^0.525.0",
|
||||
"next": "^15.4.5",
|
||||
"next": "^15.5.6",
|
||||
"next-auth": "5.0.0-beta.25",
|
||||
|
||||
"next-themes": "^0.3.0",
|
||||
"pg": "^8.16.3",
|
||||
"react": "^19.1.1",
|
||||
"react-day-picker": "^9.8.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"react": "^19.2.0",
|
||||
"react-day-picker": "^9.11.2",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-dropzone": "^14.3.8",
|
||||
"recharts": "^3.1.0",
|
||||
"resend": "^4.7.0",
|
||||
"recharts": "^3.5.0",
|
||||
"resend": "^4.8.0",
|
||||
"server-only": "^0.0.1",
|
||||
"sonner": "^2.0.6",
|
||||
"superjson": "^2.2.2",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
|
||||
|
||||
"sonner": "^2.0.7",
|
||||
"superjson": "^2.2.5",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.3.1",
|
||||
"@tailwindcss/postcss": "^4.1.11",
|
||||
"@tailwindcss/postcss": "^4.1.17",
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"@types/file-saver": "^2.0.7",
|
||||
"@types/pg": "^8.15.5",
|
||||
"@types/node": "^20.19.9",
|
||||
"@types/pg": "^8.15.6",
|
||||
"@types/node": "^20.19.25",
|
||||
"@types/raf": "^3.4.3",
|
||||
"@types/react": "^19.1.9",
|
||||
"@types/react-dom": "^19.1.7",
|
||||
"@types/react": "^19.2.7",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"drizzle-kit": "^0.30.6",
|
||||
"eslint": "^9.32.0",
|
||||
"eslint-config-next": "^15.4.5",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-config-next": "^15.5.6",
|
||||
"eslint-plugin-drizzle": "^0.2.3",
|
||||
"postcss": "^8.5.6",
|
||||
"prettier": "^3.6.2",
|
||||
"prettier-plugin-tailwindcss": "^0.6.14",
|
||||
"tailwindcss": "^4.1.11",
|
||||
"tailwindcss": "^4.1.17",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"tw-animate-css": "^1.3.6",
|
||||
"typescript": "^5.9.2",
|
||||
"typescript-eslint": "^8.38.0"
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.48.0"
|
||||
},
|
||||
"ct3aMetadata": {
|
||||
"initVersion": "7.39.3"
|
||||
|
||||
BIN
public/fonts/frutiger/Frutiger.ttf
Normal file
BIN
public/fonts/frutiger/Frutiger.ttf
Normal file
Binary file not shown.
BIN
public/fonts/frutiger/Frutiger_bold.ttf
Normal file
BIN
public/fonts/frutiger/Frutiger_bold.ttf
Normal file
Binary file not shown.
@@ -55,7 +55,7 @@ export function AnimatedStatsCard({
|
||||
void numericValue;
|
||||
|
||||
return (
|
||||
<Card className="stats-card">
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between space-y-0 pb-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
|
||||
@@ -77,12 +77,12 @@ export function PDFDownloadButton({
|
||||
>
|
||||
{isGenerating ? (
|
||||
<>
|
||||
<Loader2 className="h-5 w-5 animate-spin" />
|
||||
<Loader2 className="mr-2 h-5 w-5 animate-spin" />
|
||||
<span>Generating PDF...</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Download className="h-5 w-5" />
|
||||
<Download className="mr-2 h-5 w-5" />
|
||||
<span>Download PDF</span>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -136,7 +136,7 @@ function InvoiceViewContent({ invoiceId }: { invoiceId: string }) {
|
||||
/>
|
||||
<Button asChild variant="default" className="hover-lift">
|
||||
<Link href={`/dashboard/invoices/${invoice.id}/edit`}>
|
||||
<Edit className="h-5 w-5" />
|
||||
<Edit className="mr-2 h-5 w-5" />
|
||||
<span>Edit</span>
|
||||
</Link>
|
||||
</Button>
|
||||
@@ -329,7 +329,7 @@ function InvoiceViewContent({ invoiceId }: { invoiceId: string }) {
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{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">
|
||||
<div className="space-y-3">
|
||||
<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 */}
|
||||
<div className="bg-muted/30 rounded-lg p-4">
|
||||
<div className="bg-secondary rounded-lg p-4">
|
||||
<div className="space-y-3">
|
||||
<div className="flex flex-wrap justify-between gap-x-4 gap-y-1">
|
||||
<span className="text-muted-foreground">Subtotal:</span>
|
||||
@@ -411,7 +411,7 @@ function InvoiceViewContent({ invoiceId }: { invoiceId: string }) {
|
||||
|
||||
{/* Right Column - Actions */}
|
||||
<div className="space-y-6">
|
||||
<Card className="sticky top-6">
|
||||
<Card className="sticky top-20">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Check className="h-5 w-5" />
|
||||
@@ -419,7 +419,7 @@ function InvoiceViewContent({ invoiceId }: { invoiceId: string }) {
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<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`}>
|
||||
<Edit className="mr-2 h-4 w-4" />
|
||||
Edit Invoice
|
||||
@@ -427,7 +427,11 @@ function InvoiceViewContent({ invoiceId }: { invoiceId: string }) {
|
||||
</Button>
|
||||
|
||||
{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 */}
|
||||
@@ -435,6 +439,7 @@ function InvoiceViewContent({ invoiceId }: { invoiceId: string }) {
|
||||
<EnhancedSendInvoiceButton
|
||||
invoiceId={invoice.id}
|
||||
className="w-full"
|
||||
variant="secondary"
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -444,6 +449,7 @@ function InvoiceViewContent({ invoiceId }: { invoiceId: string }) {
|
||||
invoiceId={invoice.id}
|
||||
className="w-full"
|
||||
showResend={true}
|
||||
variant="secondary"
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -453,7 +459,8 @@ function InvoiceViewContent({ invoiceId }: { invoiceId: string }) {
|
||||
<Button
|
||||
onClick={handleMarkAsPaid}
|
||||
disabled={updateStatus.isPending}
|
||||
className="bg-primary text-primary-foreground hover:bg-primary/90 w-full"
|
||||
variant="secondary"
|
||||
className="w-full"
|
||||
>
|
||||
{updateStatus.isPending ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
@@ -465,7 +472,7 @@ function InvoiceViewContent({ invoiceId }: { invoiceId: string }) {
|
||||
)}
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
variant="secondary"
|
||||
onClick={handleDelete}
|
||||
disabled={deleteInvoice.isPending}
|
||||
className="text-destructive hover:bg-destructive/10 w-full"
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
FileUp,
|
||||
Info,
|
||||
Key,
|
||||
Palette,
|
||||
Shield,
|
||||
Upload,
|
||||
User,
|
||||
@@ -61,6 +62,7 @@ import { Textarea } from "~/components/ui/textarea";
|
||||
import { api } from "~/trpc/react";
|
||||
import { Switch } from "~/components/ui/switch";
|
||||
import { Slider } from "~/components/ui/slider";
|
||||
import { ThemeSelector } from "./theme-selector";
|
||||
import { useAnimationPreferences } from "~/components/providers/animation-preferences-provider";
|
||||
|
||||
export function SettingsContent() {
|
||||
@@ -389,7 +391,7 @@ export function SettingsContent() {
|
||||
return (
|
||||
<div
|
||||
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` }}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -615,11 +617,27 @@ export function SettingsContent() {
|
||||
</CardContent>
|
||||
</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 */}
|
||||
<Card className="bg-card border-border border">
|
||||
<CardHeader>
|
||||
<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
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
@@ -785,10 +803,10 @@ export function SettingsContent() {
|
||||
</Card>
|
||||
|
||||
{/* 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>
|
||||
<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
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
@@ -823,7 +841,7 @@ export function SettingsContent() {
|
||||
This action cannot be undone. This will permanently delete
|
||||
all your:
|
||||
</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>Business profiles and settings</li>
|
||||
<li>Invoices and invoice line items</li>
|
||||
|
||||
56
src/app/dashboard/settings/_components/theme-selector.tsx
Normal file
56
src/app/dashboard/settings/_components/theme-selector.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -8,6 +8,8 @@ import { TRPCReactProvider } from "~/trpc/react";
|
||||
import { Toaster } from "~/components/ui/sonner";
|
||||
import { AnimationPreferencesProvider } from "~/components/providers/animation-preferences-provider";
|
||||
|
||||
import { ThemeProvider } from "~/components/providers/theme-provider";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "beenvoice - Invoicing Made Simple",
|
||||
description:
|
||||
@@ -36,12 +38,19 @@ export default function RootLayout({
|
||||
</head>
|
||||
<Analytics />
|
||||
<body className="bg-background text-foreground relative min-h-screen overflow-x-hidden font-sans antialiased">
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="system"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
<TRPCReactProvider>
|
||||
<AnimationPreferencesProvider>
|
||||
{children}
|
||||
</AnimationPreferencesProvider>
|
||||
</TRPCReactProvider>
|
||||
<Toaster />
|
||||
</TRPCReactProvider>
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
@@ -192,10 +192,10 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
|
||||
<FileText className="text-primary h-6 w-6" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
<h2 className="text-foreground text-2xl font-bold">
|
||||
{invoice.invoiceNumber}
|
||||
</h2>
|
||||
<p className="text-gray-600 dark:text-gray-300">
|
||||
<p className="text-muted-foreground">
|
||||
Professional Invoice
|
||||
</p>
|
||||
</div>
|
||||
@@ -203,18 +203,14 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
|
||||
|
||||
<div className="grid grid-cols-2 gap-6 text-sm">
|
||||
<div>
|
||||
<span className="text-gray-500 dark:text-gray-400">
|
||||
Issue Date
|
||||
</span>
|
||||
<p className="font-medium text-gray-900 dark:text-white">
|
||||
<span className="text-muted-foreground">Issue Date</span>
|
||||
<p className="text-foreground font-medium">
|
||||
{formatDate(invoice.issueDate)}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-500 dark:text-gray-400">
|
||||
Due Date
|
||||
</span>
|
||||
<p className="font-medium text-gray-900 dark:text-white">
|
||||
<span className="text-muted-foreground">Due Date</span>
|
||||
<p className="text-foreground font-medium">
|
||||
{formatDate(invoice.dueDate)}
|
||||
</p>
|
||||
</div>
|
||||
@@ -234,7 +230,7 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
|
||||
<Button
|
||||
onClick={handlePDFExport}
|
||||
disabled={isExportingPDF}
|
||||
variant="brand"
|
||||
variant="default"
|
||||
className="transform-none"
|
||||
>
|
||||
{isExportingPDF ? (
|
||||
@@ -264,29 +260,29 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
<h3 className="text-foreground text-lg font-semibold">
|
||||
{invoice.client?.name}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 text-sm md:grid-cols-2">
|
||||
{invoice.client?.email && (
|
||||
<div className="flex items-center gap-2 text-gray-600 dark:text-gray-300">
|
||||
<Mail className="h-4 w-4 text-gray-400 dark:text-gray-500" />
|
||||
<div className="text-muted-foreground flex items-center gap-2">
|
||||
<Mail className="text-muted-foreground h-4 w-4" />
|
||||
{invoice.client.email}
|
||||
</div>
|
||||
)}
|
||||
{invoice.client?.phone && (
|
||||
<div className="flex items-center gap-2 text-gray-600 dark:text-gray-300">
|
||||
<Phone className="h-4 w-4 text-gray-400 dark:text-gray-500" />
|
||||
<div className="text-muted-foreground flex items-center gap-2">
|
||||
<Phone className="text-muted-foreground h-4 w-4" />
|
||||
{invoice.client.phone}
|
||||
</div>
|
||||
)}
|
||||
{(invoice.client?.addressLine1 ??
|
||||
invoice.client?.city ??
|
||||
invoice.client?.state) && (
|
||||
<div className="flex items-start gap-2 text-gray-600 md:col-span-2 dark:text-gray-300">
|
||||
<MapPin className="mt-0.5 h-4 w-4 flex-shrink-0 text-gray-400 dark:text-gray-500" />
|
||||
<div className="text-muted-foreground flex items-start gap-2 md:col-span-2">
|
||||
<MapPin className="text-muted-foreground mt-0.5 h-4 w-4 flex-shrink-0" />
|
||||
<div>
|
||||
{invoice.client?.addressLine1 && (
|
||||
<div>{invoice.client.addressLine1}</div>
|
||||
@@ -318,7 +314,7 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
|
||||
</Card>
|
||||
|
||||
{/* Invoice Items */}
|
||||
<Card className="bg-card border-border border">
|
||||
<Card className="bg-secondary border-border border">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-primary flex items-center gap-2">
|
||||
<Clock className="h-5 w-5" />
|
||||
@@ -326,52 +322,25 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="border-border overflow-hidden border">
|
||||
<table className="w-full">
|
||||
<thead className="bg-muted">
|
||||
<tr>
|
||||
<th className="text-muted-foreground px-4 py-3 text-left text-sm font-semibold">
|
||||
Date
|
||||
</th>
|
||||
<th className="text-muted-foreground px-4 py-3 text-left text-sm font-semibold">
|
||||
Description
|
||||
</th>
|
||||
<th className="text-muted-foreground px-4 py-3 text-right text-sm font-semibold">
|
||||
Hours
|
||||
</th>
|
||||
<th className="text-muted-foreground px-4 py-3 text-right text-sm font-semibold">
|
||||
Rate
|
||||
</th>
|
||||
<th className="text-muted-foreground px-4 py-3 text-right text-sm font-semibold">
|
||||
Amount
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<div className="space-y-2">
|
||||
{invoice.items?.map((item, index) => (
|
||||
<tr
|
||||
<div
|
||||
key={item.id || index}
|
||||
className="border-border hover:bg-muted/50 border-t"
|
||||
className="bg-background flex items-center justify-between rounded-lg p-4"
|
||||
>
|
||||
<td className="text-foreground px-4 py-3 text-sm">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="text-muted-foreground text-sm">
|
||||
{formatDate(item.date)}
|
||||
</td>
|
||||
<td className="text-foreground px-4 py-3 text-sm">
|
||||
</div>
|
||||
<div className="text-foreground font-medium">
|
||||
{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">
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-foreground text-right font-medium">
|
||||
{formatCurrency(item.amount)}
|
||||
</td>
|
||||
</tr>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -383,7 +352,7 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
|
||||
<CardTitle className="text-primary">Notes</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="whitespace-pre-wrap text-gray-700 dark:text-gray-300">
|
||||
<p className="text-muted-foreground whitespace-pre-wrap">
|
||||
{invoice.notes}
|
||||
</p>
|
||||
</CardContent>
|
||||
@@ -394,7 +363,7 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
|
||||
{/* Sidebar */}
|
||||
<div className="space-y-6">
|
||||
{/* Status Actions */}
|
||||
<Card className="bg-card border-border border">
|
||||
<Card className="bg-secondary border-border border">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-primary">Status Actions</CardTitle>
|
||||
</CardHeader>
|
||||
@@ -403,7 +372,7 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
|
||||
<Button
|
||||
onClick={() => handleStatusUpdate("sent")}
|
||||
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" />
|
||||
Mark as Sent
|
||||
@@ -414,7 +383,7 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
|
||||
<Button
|
||||
onClick={() => handleStatusUpdate("paid")}
|
||||
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" />
|
||||
Mark as Paid
|
||||
@@ -425,7 +394,7 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
|
||||
<Button
|
||||
onClick={() => handleStatusUpdate("paid")}
|
||||
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" />
|
||||
Mark as Paid
|
||||
@@ -449,20 +418,18 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-600 dark:text-gray-300">
|
||||
Subtotal
|
||||
</span>
|
||||
<span className="font-medium dark:text-white">
|
||||
<span className="text-muted-foreground">Subtotal</span>
|
||||
<span className="text-foreground font-medium">
|
||||
{formatCurrency(invoice.totalAmount)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-600 dark:text-gray-300">Tax</span>
|
||||
<span className="font-medium dark:text-white">$0.00</span>
|
||||
<span className="text-muted-foreground">Tax</span>
|
||||
<span className="text-foreground font-medium">$0.00</span>
|
||||
</div>
|
||||
<Separator />
|
||||
<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">
|
||||
{formatCurrency(invoice.totalAmount)}
|
||||
</span>
|
||||
@@ -486,8 +453,8 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
|
||||
<CardContent>
|
||||
<Button
|
||||
onClick={handleDelete}
|
||||
variant="outline"
|
||||
className="border-destructive/20 text-destructive hover:bg-destructive/10 w-full"
|
||||
variant="destructive"
|
||||
className="w-full"
|
||||
>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
Delete Invoice
|
||||
@@ -501,10 +468,10 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
|
||||
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||
<DialogContent className="bg-card border-border border">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-xl font-bold text-gray-800 dark:text-white">
|
||||
<DialogTitle className="text-foreground text-xl font-bold">
|
||||
Delete Invoice
|
||||
</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
|
||||
be undone and will permanently remove the invoice and all its
|
||||
data.
|
||||
|
||||
@@ -507,21 +507,16 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) {
|
||||
>
|
||||
{invoiceId && invoiceId !== "new" && (
|
||||
<Button
|
||||
variant="outline"
|
||||
variant="secondary"
|
||||
onClick={handleDelete}
|
||||
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" />
|
||||
<span className="hidden sm:inline">Delete Invoice</span>
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
disabled={loading}
|
||||
variant="default"
|
||||
className="hover-lift"
|
||||
>
|
||||
<Button onClick={handleSubmit} disabled={loading} variant="secondary">
|
||||
{loading ? (
|
||||
<>
|
||||
<Clock className="h-4 w-4 animate-spin sm:mr-2" />
|
||||
@@ -564,7 +559,6 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) {
|
||||
value={formData.invoiceNumber}
|
||||
placeholder="INV-2024-001"
|
||||
disabled
|
||||
className="bg-muted/50"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
@@ -831,16 +825,16 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) {
|
||||
<FloatingActionBar
|
||||
leftContent={
|
||||
<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" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-gray-900 dark:text-gray-100">
|
||||
<p className="text-foreground font-medium">
|
||||
{invoiceId && invoiceId !== "new"
|
||||
? "Edit Invoice"
|
||||
: "Create Invoice"}
|
||||
</p>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Update invoice details
|
||||
</p>
|
||||
</div>
|
||||
@@ -849,7 +843,7 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) {
|
||||
>
|
||||
{invoiceId && invoiceId !== "new" && (
|
||||
<Button
|
||||
variant="outline"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={handleDelete}
|
||||
disabled={loading || deleteInvoice.isPending}
|
||||
@@ -862,7 +856,7 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) {
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
disabled={loading}
|
||||
variant="default"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
>
|
||||
{loading ? (
|
||||
|
||||
@@ -124,7 +124,7 @@ function SortableLineItem({
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
transition={{ duration: 0.2, ease: "easeOut" }}
|
||||
className={cn(
|
||||
"bg-card border-border hidden border p-4 md:block",
|
||||
"bg-secondary hidden rounded-lg p-4 md:block",
|
||||
isDragging && "opacity-50",
|
||||
)}
|
||||
>
|
||||
@@ -144,12 +144,7 @@ function SortableLineItem({
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onMoveUp(index)}
|
||||
className={cn(
|
||||
"h-6 w-6 p-0 transition-colors",
|
||||
isFirst
|
||||
? "text-muted-foreground/50 cursor-not-allowed"
|
||||
: "text-muted-foreground hover:text-foreground",
|
||||
)}
|
||||
className="h-6 w-6 p-0"
|
||||
disabled={isFirst}
|
||||
aria-label="Move up"
|
||||
>
|
||||
@@ -160,12 +155,7 @@ function SortableLineItem({
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onMoveDown(index)}
|
||||
className={cn(
|
||||
"h-6 w-6 p-0 transition-colors",
|
||||
isLast
|
||||
? "text-muted-foreground/50 cursor-not-allowed"
|
||||
: "text-muted-foreground hover:text-foreground",
|
||||
)}
|
||||
className="h-6 w-6 p-0"
|
||||
disabled={isLast}
|
||||
aria-label="Move down"
|
||||
>
|
||||
@@ -232,10 +222,7 @@ function SortableLineItem({
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onRemove(index)}
|
||||
className={cn(
|
||||
"text-muted-foreground hover:text-destructive h-8 w-8 p-0 transition-colors",
|
||||
!canRemove && "cursor-not-allowed opacity-50",
|
||||
)}
|
||||
className="text-muted-foreground hover:text-destructive h-8 w-8 p-0"
|
||||
disabled={!canRemove}
|
||||
aria-label="Remove item"
|
||||
>
|
||||
@@ -266,7 +253,7 @@ function MobileLineItem({
|
||||
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"
|
||||
className="border-border bg-card overflow-hidden rounded-lg border md:hidden"
|
||||
>
|
||||
<div className="bg-secondary space-y-3 p-4">
|
||||
{/* Description */}
|
||||
@@ -317,19 +304,14 @@ function MobileLineItem({
|
||||
</div>
|
||||
|
||||
{/* 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">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onMoveUp(index)}
|
||||
className={cn(
|
||||
"h-8 w-8 p-0 transition-colors",
|
||||
isFirst
|
||||
? "text-muted-foreground/50 cursor-not-allowed"
|
||||
: "text-muted-foreground hover:text-foreground",
|
||||
)}
|
||||
className="h-8 w-8 p-0"
|
||||
disabled={isFirst}
|
||||
aria-label="Move up"
|
||||
>
|
||||
@@ -340,12 +322,7 @@ function MobileLineItem({
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onMoveDown(index)}
|
||||
className={cn(
|
||||
"h-8 w-8 p-0 transition-colors",
|
||||
isLast
|
||||
? "text-muted-foreground/50 cursor-not-allowed"
|
||||
: "text-muted-foreground hover:text-foreground",
|
||||
)}
|
||||
className="h-8 w-8 p-0"
|
||||
disabled={isLast}
|
||||
aria-label="Move down"
|
||||
>
|
||||
@@ -356,10 +333,7 @@ function MobileLineItem({
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onRemove(index)}
|
||||
className={cn(
|
||||
"text-muted-foreground hover:text-destructive h-8 w-8 p-0 transition-colors",
|
||||
!canRemove && "cursor-not-allowed opacity-50",
|
||||
)}
|
||||
className="text-muted-foreground hover:text-destructive h-8 w-8 p-0"
|
||||
disabled={!canRemove}
|
||||
aria-label="Remove item"
|
||||
>
|
||||
|
||||
@@ -71,7 +71,7 @@ export function FloatingActionBar({
|
||||
<CardContent className="flex items-center justify-between p-4">
|
||||
{/* Left content */}
|
||||
{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}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -52,7 +52,7 @@ export function PageHeader({
|
||||
)}
|
||||
</div>
|
||||
{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}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -44,9 +44,9 @@ export function Sidebar() {
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
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
|
||||
? "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"
|
||||
}`}
|
||||
>
|
||||
|
||||
16
src/components/providers/theme-provider.tsx
Normal file
16
src/components/providers/theme-provider.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
40
src/components/theme/theme-switcher.tsx
Normal file
40
src/components/theme/theme-switcher.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -1,23 +1,25 @@
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import * as React from "react";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "~/lib/utils"
|
||||
import { cn } from "~/lib/utils";
|
||||
|
||||
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: {
|
||||
variant: {
|
||||
default: "bg-background text-foreground",
|
||||
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: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
const Alert = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
@@ -29,8 +31,8 @@ const Alert = React.forwardRef<
|
||||
className={cn(alertVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Alert.displayName = "Alert"
|
||||
));
|
||||
Alert.displayName = "Alert";
|
||||
|
||||
const AlertTitle = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
@@ -38,11 +40,11 @@ const AlertTitle = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<h5
|
||||
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}
|
||||
/>
|
||||
))
|
||||
AlertTitle.displayName = "AlertTitle"
|
||||
));
|
||||
AlertTitle.displayName = "AlertTitle";
|
||||
|
||||
const AlertDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
@@ -53,7 +55,7 @@ const AlertDescription = React.forwardRef<
|
||||
className={cn("text-sm [&_p]:leading-relaxed", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertDescription.displayName = "AlertDescription"
|
||||
));
|
||||
AlertDescription.displayName = "AlertDescription";
|
||||
|
||||
export { Alert, AlertTitle, AlertDescription }
|
||||
export { Alert, AlertTitle, AlertDescription };
|
||||
|
||||
@@ -5,29 +5,17 @@ import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { cn } from "~/lib/utils";
|
||||
|
||||
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: {
|
||||
variant: {
|
||||
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:
|
||||
"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:
|
||||
"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",
|
||||
outline:
|
||||
"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",
|
||||
"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
|
||||
outline: "text-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
@@ -39,18 +27,10 @@ const badgeVariants = cva(
|
||||
function Badge({
|
||||
className,
|
||||
variant,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"span"> &
|
||||
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot : "span";
|
||||
|
||||
}: React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof badgeVariants>) {
|
||||
return (
|
||||
<Comp
|
||||
data-slot="badge"
|
||||
className={cn(badgeVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,29 +5,26 @@ 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 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: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
|
||||
brand:
|
||||
"bg-primary text-primary-foreground shadow-lg hover:bg-primary/90 hover:shadow-xl font-medium",
|
||||
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
||||
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:
|
||||
"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:
|
||||
"bg-secondary text-muted-foreground-foreground shadow-xs hover:bg-secondary/80",
|
||||
ghost:
|
||||
"hover:bg-accent hover:text-foreground-foreground dark:hover:bg-accent/50",
|
||||
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||
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",
|
||||
default: "h-9 px-4 py-2",
|
||||
sm: "h-8 rounded-md px-3 text-xs",
|
||||
lg: "h-10 rounded-md px-8",
|
||||
icon: "h-9 w-9",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
@@ -37,25 +34,24 @@ const buttonVariants = cva(
|
||||
},
|
||||
);
|
||||
|
||||
function Button({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> &
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean;
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
return (
|
||||
<Comp
|
||||
data-slot="button"
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
Button.displayName = "Button";
|
||||
|
||||
export { Button, buttonVariants };
|
||||
|
||||
@@ -7,7 +7,7 @@ function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||
<div
|
||||
data-slot="card"
|
||||
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,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -1,22 +1,24 @@
|
||||
import { cn } from "~/lib/utils";
|
||||
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>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<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 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",
|
||||
"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",
|
||||
"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",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
Input.displayName = "Input";
|
||||
|
||||
export { Input };
|
||||
|
||||
@@ -76,7 +76,7 @@ export function NumberInput({
|
||||
return (
|
||||
<div
|
||||
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,
|
||||
disabled && "cursor-not-allowed opacity-50",
|
||||
className,
|
||||
@@ -86,7 +86,7 @@ export function NumberInput({
|
||||
type="button"
|
||||
onClick={handleDecrement}
|
||||
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>
|
||||
@@ -113,7 +113,7 @@ export function NumberInput({
|
||||
type="button"
|
||||
onClick={handleIncrement}
|
||||
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>
|
||||
|
||||
@@ -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 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,
|
||||
)}
|
||||
{...props}
|
||||
@@ -68,7 +68,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 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" &&
|
||||
"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,
|
||||
@@ -212,7 +212,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 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" &&
|
||||
"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,
|
||||
|
||||
@@ -6,55 +6,17 @@ const Toaster = ({ ...props }: ToasterProps) => {
|
||||
return (
|
||||
<Sonner
|
||||
className="toaster group"
|
||||
position="top-center"
|
||||
position="bottom-right"
|
||||
closeButton
|
||||
richColors={false}
|
||||
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
|
||||
}
|
||||
richColors
|
||||
toastOptions={{
|
||||
classNames: {
|
||||
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 group-[.toast]:text-sm",
|
||||
toast: "group toast",
|
||||
description: "group-[.toast]:text-muted-foreground",
|
||||
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:
|
||||
"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",
|
||||
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)",
|
||||
"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
|
||||
},
|
||||
}}
|
||||
{...props}
|
||||
|
||||
@@ -1,31 +1,31 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as SwitchPrimitive from "@radix-ui/react-switch";
|
||||
|
||||
import { Check, X } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { cn } from "~/lib/utils";
|
||||
|
||||
function Switch({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SwitchPrimitive.Root>) {
|
||||
return (
|
||||
const Switch = React.forwardRef<
|
||||
React.ElementRef<typeof SwitchPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SwitchPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SwitchPrimitive.Root
|
||||
data-slot="switch"
|
||||
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",
|
||||
"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
|
||||
data-slot="switch-thumb"
|
||||
className={cn(
|
||||
"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",
|
||||
"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",
|
||||
)}
|
||||
/>
|
||||
>
|
||||
<Check className="text-primary absolute inset-0 m-auto h-4 w-4 opacity-0 transition-opacity data-[state=checked]:opacity-100" />
|
||||
</SwitchPrimitive.Thumb>
|
||||
</SwitchPrimitive.Root>
|
||||
);
|
||||
}
|
||||
));
|
||||
Switch.displayName = SwitchPrimitive.Root.displayName;
|
||||
|
||||
export { Switch };
|
||||
|
||||
@@ -1,66 +1,65 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import * as TabsPrimitive from "@radix-ui/react-tabs"
|
||||
import * as React from "react";
|
||||
import * as TabsPrimitive from "@radix-ui/react-tabs";
|
||||
|
||||
import { cn } from "~/lib/utils"
|
||||
import { cn } from "~/lib/utils";
|
||||
|
||||
function Tabs({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
|
||||
return (
|
||||
const Tabs = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Root
|
||||
data-slot="tabs"
|
||||
ref={ref}
|
||||
className={cn("flex flex-col gap-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
));
|
||||
Tabs.displayName = TabsPrimitive.Root.displayName;
|
||||
|
||||
function TabsList({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.List>) {
|
||||
return (
|
||||
const TabsList = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.List
|
||||
data-slot="tabs-list"
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center p-[3px]",
|
||||
className
|
||||
"bg-muted text-muted-foreground inline-flex h-9 items-center justify-center rounded-lg p-1",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
));
|
||||
TabsList.displayName = TabsPrimitive.List.displayName;
|
||||
|
||||
function TabsTrigger({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
|
||||
return (
|
||||
const TabsTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Trigger
|
||||
data-slot="tabs-trigger"
|
||||
ref={ref}
|
||||
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 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
|
||||
"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",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
));
|
||||
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
|
||||
|
||||
function TabsContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
|
||||
return (
|
||||
const TabsContent = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Content
|
||||
data-slot="tabs-content"
|
||||
className={cn("flex-1 outline-none", className)}
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"ring-offset-background focus-visible:ring-ring mt-2 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
));
|
||||
TabsContent.displayName = TabsPrimitive.Content.displayName;
|
||||
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent };
|
||||
|
||||
@@ -2,17 +2,23 @@ import * as React from "react";
|
||||
|
||||
import { cn } from "~/lib/utils";
|
||||
|
||||
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
||||
export interface TextareaProps
|
||||
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
|
||||
|
||||
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||
({ className, ...props }, ref) => {
|
||||
return (
|
||||
<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 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: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 };
|
||||
|
||||
@@ -2,124 +2,242 @@
|
||||
@import "tw-animate-css";
|
||||
|
||||
:root {
|
||||
--background: oklch(0.98 0 0);
|
||||
--foreground: oklch(0.15 0 0);
|
||||
--background: oklch(0.98 0.01 230);
|
||||
--foreground: oklch(0.2 0.03 230);
|
||||
--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-foreground: oklch(0.15 0 0);
|
||||
--primary: oklch(0.55 0 0);
|
||||
--primary-foreground: oklch(0.98 0 0);
|
||||
--secondary: oklch(0.94 0 0);
|
||||
--secondary-foreground: oklch(0.2 0 0);
|
||||
--muted: oklch(0.94 0 0);
|
||||
--muted-foreground: oklch(0.55 0 0);
|
||||
--accent: oklch(0.94 0 0);
|
||||
--accent-foreground: oklch(0.2 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 0);
|
||||
--destructive-foreground: oklch(0.98 0.01 230);
|
||||
--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-foreground: oklch(0.1 0 0);
|
||||
--border: oklch(0.9 0 0);
|
||||
--input: oklch(0.9 0 0);
|
||||
--ring: oklch(0.71 0 0);
|
||||
--chart-1: oklch(0.65 0.15 258);
|
||||
--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 0);
|
||||
--sidebar-foreground: oklch(0.15 0 0);
|
||||
--sidebar-primary: oklch(0.2 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.98 0 0);
|
||||
--sidebar-accent: oklch(0.92 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.2 0 0);
|
||||
--sidebar-border: oklch(0.88 0 0);
|
||||
--sidebar-ring: oklch(0.71 0 0);
|
||||
--navbar: oklch(0.96 0 0);
|
||||
--navbar-foreground: oklch(0.15 0 0);
|
||||
--navbar-border: oklch(0.88 0 0);
|
||||
--font-sans: Geist Mono, monospace;
|
||||
--font-serif: Geist Mono, monospace;
|
||||
--font-mono: Geist Mono, monospace;
|
||||
--radius: 0rem;
|
||||
--shadow-2xs: 0px 1px 0px 0px hsl(0 0% 0% / 0);
|
||||
--shadow-xs: 0px 1px 0px 0px hsl(0 0% 0% / 0);
|
||||
--shadow-sm:
|
||||
0px 1px 0px 0px hsl(0 0% 0% / 0), 0px 1px 2px -1px hsl(0 0% 0% / 0);
|
||||
--shadow: 0px 1px 0px 0px hsl(0 0% 0% / 0), 0px 1px 2px -1px hsl(0 0% 0% / 0);
|
||||
--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);
|
||||
--font-geist-sans: "Geist", sans-serif;
|
||||
--font-geist-mono: "Geist Mono", monospace;
|
||||
--font-sans: "Geist", sans-serif;
|
||||
--font-mono: "Geist Mono", monospace;
|
||||
--font-serif: "Instrument Serif", serif;
|
||||
--radius: 0.8rem;
|
||||
--shadow-2xs: 0px 1px 2px 0px hsl(0 0% 0% / 0.05);
|
||||
--shadow-xs: 0px 1px 2px 0px hsl(0 0% 0% / 0.05);
|
||||
--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:
|
||||
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:
|
||||
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:
|
||||
0px 1px 0px 0px hsl(0 0% 0% / 0), 0px 8px 10px -1px hsl(0 0% 0% / 0);
|
||||
--shadow-2xl: 0px 1px 0px 0px hsl(0 0% 0% / 0);
|
||||
0px 20px 25px -5px hsl(0 0% 0% / 0.1),
|
||||
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;
|
||||
--spacing: 0.25rem;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background: oklch(0.12 0 0);
|
||||
--foreground: oklch(0.98 0 0);
|
||||
--card: oklch(0.18 0 0);
|
||||
--card-foreground: oklch(0.98 0 0);
|
||||
--popover: oklch(0.22 0 0);
|
||||
--popover-foreground: oklch(0.98 0 0);
|
||||
--primary: oklch(0.55 0 0);
|
||||
--primary-foreground: oklch(0.98 0 0);
|
||||
--secondary: oklch(0.22 0 0);
|
||||
--secondary-foreground: oklch(0.98 0 0);
|
||||
--muted: oklch(0.22 0 0);
|
||||
--muted-foreground: oklch(0.71 0 0);
|
||||
--accent: oklch(0.3 0 0);
|
||||
--accent-foreground: oklch(0.98 0 0);
|
||||
.dark {
|
||||
--background: oklch(0.15 0.03 230);
|
||||
--foreground: oklch(0.9 0.01 230);
|
||||
--card: oklch(0.2 0.03 230);
|
||||
--card-foreground: oklch(0.9 0.01 230);
|
||||
--popover: oklch(0.22 0.03 230);
|
||||
--popover-foreground: oklch(0.9 0.01 230);
|
||||
--primary: oklch(0.6 0.15 220);
|
||||
--primary-foreground: oklch(0.98 0.01 230);
|
||||
--secondary: oklch(0.25 0.03 230);
|
||||
--secondary-foreground: oklch(0.9 0.01 230);
|
||||
--muted: oklch(0.25 0.03 230);
|
||||
--muted-foreground: oklch(0.7 0.01 230);
|
||||
--accent: oklch(0.3 0.03 230);
|
||||
--accent-foreground: oklch(0.9 0.01 230);
|
||||
--destructive: oklch(0.7 0.19 22);
|
||||
--destructive-foreground: oklch(0.22 0 0);
|
||||
--destructive-foreground: oklch(0.2 0.03 230);
|
||||
--success: oklch(0.6 0.15 142);
|
||||
--success-foreground: oklch(0.98 0 0);
|
||||
--success-foreground: oklch(0.98 0.01 230);
|
||||
--warning: oklch(0.7 0.15 38);
|
||||
--warning-foreground: oklch(0.1 0 0);
|
||||
--border: oklch(0.28 0 0);
|
||||
--input: oklch(0.35 0 0);
|
||||
--ring: oklch(0.55 0 0);
|
||||
--chart-1: oklch(0.7 0.15 258);
|
||||
--warning-foreground: oklch(0.2 0.03 230);
|
||||
--border: oklch(0.28 0.03 230);
|
||||
--input: oklch(0.35 0.03 230);
|
||||
--ring: oklch(0.6 0.15 220);
|
||||
--chart-1: oklch(0.6 0.15 220);
|
||||
--chart-2: oklch(0.75 0.14 142);
|
||||
--chart-3: oklch(0.7 0.2 27);
|
||||
--chart-4: oklch(0.65 0.18 302);
|
||||
--chart-5: oklch(0.67 0.16 197);
|
||||
--sidebar: oklch(0.08 0 0);
|
||||
--sidebar-foreground: oklch(0.98 0 0);
|
||||
--sidebar-primary: oklch(0.98 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.08 0 0);
|
||||
--sidebar-accent: oklch(0.15 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.98 0 0);
|
||||
--sidebar-border: oklch(0.25 0 0);
|
||||
--sidebar-ring: oklch(0.35 0 0);
|
||||
--navbar: oklch(0.08 0 0);
|
||||
--navbar-foreground: oklch(0.98 0 0);
|
||||
--navbar-border: oklch(0.25 0 0);
|
||||
--font-sans: Geist Mono, monospace;
|
||||
--font-serif: Geist Mono, monospace;
|
||||
--font-mono: Geist Mono, monospace;
|
||||
--radius: 0rem;
|
||||
--shadow-2xs: 0px 1px 0px 0px hsl(0 0% 0% / 0);
|
||||
--shadow-xs: 0px 1px 0px 0px hsl(0 0% 0% / 0);
|
||||
--shadow-sm:
|
||||
0px 1px 0px 0px hsl(0 0% 0% / 0), 0px 1px 2px -1px hsl(0 0% 0% / 0);
|
||||
--sidebar: oklch(0.1 0.03 230);
|
||||
--sidebar-foreground: oklch(0.9 0.01 230);
|
||||
--sidebar-primary: oklch(0.9 0.01 230);
|
||||
--sidebar-primary-foreground: oklch(0.1 0.03 230);
|
||||
--sidebar-accent: oklch(0.2 0.03 230);
|
||||
--sidebar-accent-foreground: oklch(0.9 0.01 230);
|
||||
--sidebar-border: oklch(0.25 0.03 230);
|
||||
--sidebar-ring: oklch(0.35 0.03 230);
|
||||
--navbar: oklch(0.1 0.03 230);
|
||||
--navbar-foreground: oklch(0.9 0.01 230);
|
||||
--navbar-border: oklch(0.25 0.03 230);
|
||||
--font-sans: "Geist", sans-serif;
|
||||
--font-mono: "Geist Mono", monospace;
|
||||
--font-serif: "Instrument Serif", serif;
|
||||
--radius: 0.8rem;
|
||||
--shadow-2xs: 0px 1px 2px 0px hsl(0 0% 0% / 0.05);
|
||||
--shadow-xs: 0px 1px 2px 0px hsl(0 0% 0% / 0.05);
|
||||
--shadow-sm: 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);
|
||||
0px 1px 3px 0px hsl(0 0% 0% / 0.1), 0px 1px 2px 0px hsl(0 0% 0% / 0.06);
|
||||
--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:
|
||||
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:
|
||||
0px 1px 0px 0px hsl(0 0% 0% / 0), 0px 8px 10px -1px hsl(0 0% 0% / 0);
|
||||
--shadow-2xl: 0px 1px 0px 0px hsl(0 0% 0% / 0);
|
||||
}
|
||||
0px 20px 25px -5px hsl(0 0% 0% / 0.1),
|
||||
0px 10px 10px -5px hsl(0 0% 0% / 0.04);
|
||||
--shadow-2xl: 0px 25px 50px -12px hsl(0 0% 0% / 0.25);
|
||||
}
|
||||
|
||||
.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 {
|
||||
@@ -268,163 +386,58 @@ body {
|
||||
}
|
||||
|
||||
/* Sonner Toast Styles for beenvoice */
|
||||
[data-sonner-toaster] {
|
||||
font-family: var(--font-geist-mono, ui-monospace, monospace) !important;
|
||||
--width: 380px;
|
||||
.toaster [data-sonner-toast] {
|
||||
background: oklch(var(--card));
|
||||
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] {
|
||||
background: hsl(var(--card)) !important;
|
||||
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;
|
||||
.toaster [data-sonner-toast] [data-icon] {
|
||||
color: oklch(var(--primary));
|
||||
}
|
||||
|
||||
/* Toast types with colored left borders */
|
||||
[data-sonner-toast][data-type="success"] {
|
||||
border-left: 4px solid hsl(142 76% 36%) !important;
|
||||
.toaster [data-sonner-toast][data-type="success"] [data-icon] {
|
||||
color: oklch(var(--success));
|
||||
}
|
||||
|
||||
[data-sonner-toast][data-type="error"] {
|
||||
border-left: 4px solid hsl(0 84% 60%) !important;
|
||||
.toaster [data-sonner-toast][data-type="error"] [data-icon] {
|
||||
color: oklch(var(--destructive));
|
||||
}
|
||||
|
||||
[data-sonner-toast][data-type="warning"] {
|
||||
border-left: 4px solid hsl(38 92% 50%) !important;
|
||||
.toaster [data-sonner-toast][data-type="warning"] [data-icon] {
|
||||
color: oklch(var(--warning));
|
||||
}
|
||||
|
||||
[data-sonner-toast][data-type="info"] {
|
||||
border-left: 4px solid hsl(221 83% 53%) !important;
|
||||
.toaster [data-sonner-toast] [data-title] {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Close button styling - comprehensive selectors */
|
||||
[data-sonner-toast] [data-close-button],
|
||||
[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;
|
||||
.toaster [data-sonner-toast] [data-description] {
|
||||
color: oklch(var(--muted-foreground));
|
||||
}
|
||||
|
||||
[data-sonner-toast] [data-close-button]:hover,
|
||||
[data-sonner-toast] button[data-close-button]:hover,
|
||||
[data-sonner-toast] button[aria-label="Close toast"]:hover,
|
||||
[data-sonner-toast] > button:hover,
|
||||
li[data-sonner-toast] [data-close-button]:hover,
|
||||
li[data-sonner-toast] button:hover,
|
||||
.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;
|
||||
.toaster [data-sonner-toast] [data-close-button] {
|
||||
background: oklch(var(--muted));
|
||||
border-color: oklch(var(--border));
|
||||
color: oklch(var(--muted-foreground));
|
||||
border-radius: var(--radius-sm);
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
/* Very specific override for close button positioning */
|
||||
[data-sonner-toast] > div > button,
|
||||
[data-sonner-toast] button:last-child,
|
||||
[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;
|
||||
.toaster [data-sonner-toast] [data-close-button]:hover {
|
||||
background: oklch(var(--muted) / 0.8);
|
||||
color: oklch(var(--foreground));
|
||||
}
|
||||
|
||||
/* 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) {
|
||||
[data-sonner-toaster] {
|
||||
.toaster [data-sonner-toaster] {
|
||||
--width: calc(100vw - 32px);
|
||||
left: 16px !important;
|
||||
right: 16px !important;
|
||||
left: 16px;
|
||||
right: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -839,26 +852,6 @@ html.user-reduce-motion *::after {
|
||||
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-item {
|
||||
animation: fadeInUp var(--animation-speed-normal) var(--animation-easing)
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { Config } from "tailwindcss";
|
||||
import animate from "tailwindcss-animate";
|
||||
|
||||
export default {
|
||||
darkMode: "media",
|
||||
darkMode: "class",
|
||||
content: [
|
||||
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
@@ -14,9 +14,9 @@ export default {
|
||||
xs: "475px",
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ["var(--font-geist-mono)", "monospace"],
|
||||
sans: ["var(--font-geist-sans)", "sans-serif"],
|
||||
mono: ["var(--font-geist-mono)", "monospace"],
|
||||
serif: ["var(--font-geist-mono)", "monospace"],
|
||||
serif: ["var(--font-serif)", "serif"],
|
||||
},
|
||||
colors: {
|
||||
navbar: "var(--navbar)",
|
||||
@@ -37,6 +37,11 @@ export default {
|
||||
"accordion-down": "accordion-down 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],
|
||||
|
||||
Reference in New Issue
Block a user