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

View File

@@ -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>
{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 className="space-y-2">
{invoice.items?.map((item, index) => (
<div
key={item.id || index}
className="bg-background flex items-center justify-between rounded-lg p-4"
>
<div className="flex items-center gap-4">
<div className="text-muted-foreground text-sm">
{formatDate(item.date)}
</div>
<div className="text-foreground font-medium">
{item.description}
</div>
</div>
<div className="text-foreground text-right font-medium">
{formatCurrency(item.amount)}
</div>
</div>
))}
</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.

View File

@@ -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 ? (

View File

@@ -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"
>

View File

@@ -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>
)}

View File

@@ -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>
)}

View File

@@ -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"
}`}
>

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 { 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 };

View File

@@ -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} />
);
}

View File

@@ -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> & {
asChild?: boolean;
}) {
const Comp = asChild ? Slot : "button";
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
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 };

View File

@@ -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}

View File

@@ -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">) {
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",
className,
)}
{...props}
/>
);
}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"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 };

View File

@@ -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>

View File

@@ -42,7 +42,7 @@ function SelectTrigger({
data-slot="select-trigger"
data-size={size}
className={cn(
"data-[placeholder]:text-muted-foreground border-input bg-background text-foreground focus-visible:border-ring focus-visible:ring-ring/50 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,

View File

@@ -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}

View File

@@ -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 (
<SwitchPrimitive.Root
data-slot="switch"
const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitive.Root>
>(({ className, ...props }, ref) => (
<SwitchPrimitive.Root
className={cn(
"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(
"peer data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 data-[state=checked]:bg-primary inline-flex h-5 w-9 shrink-0 items-center border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className,
"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",
)}
{...props}
>
<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",
)}
/>
</SwitchPrimitive.Root>
);
}
<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 };

View File

@@ -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 (
<TabsPrimitive.Root
data-slot="tabs"
className={cn("flex flex-col gap-2", className)}
{...props}
/>
)
}
const Tabs = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Root>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Root
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 (
<TabsPrimitive.List
data-slot="tabs-list"
className={cn(
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center p-[3px]",
className
)}
{...props}
/>
)
}
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
"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 (
<TabsPrimitive.Trigger
data-slot="tabs-trigger"
className={cn(
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
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",
className,
)}
{...props}
/>
));
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
function TabsContent({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
return (
<TabsPrimitive.Content
data-slot="tabs-content"
className={cn("flex-1 outline-none", className)}
{...props}
/>
)
}
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
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 };

View File

@@ -2,17 +2,23 @@ import * as React from "react";
import { cn } from "~/lib/utils";
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
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",
className,
)}
{...props}
/>
);
}
export interface TextareaProps
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, ...props }, ref) => {
return (
<textarea
className={cn(
"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 };