mirror of
https://github.com/soconnor0919/beenvoice.git
synced 2026-02-05 00:06:36 -05:00
Add global animation system and entrance effects to UI
This commit is contained in:
@@ -361,7 +361,7 @@ export function DataTable<TData, TValue>({
|
||||
key={row.id}
|
||||
data-state={row.getIsSelected() && "selected"}
|
||||
className={cn(
|
||||
"hover:bg-muted/20 data-[state=selected]:bg-muted/50 border-border/40 border-b transition-colors",
|
||||
"hover:bg-muted/20 data-[state=selected]:bg-muted/50 border-border/40 table-row border-b transition-colors",
|
||||
onRowClick && "cursor-pointer",
|
||||
)}
|
||||
onClick={(event) =>
|
||||
|
||||
@@ -51,7 +51,15 @@ export function StatusBadge({
|
||||
const label = children ?? statusLabelMap[status];
|
||||
|
||||
return (
|
||||
<Badge className={cn(statusClass, className)} {...props}>
|
||||
<Badge
|
||||
className={cn(
|
||||
statusClass,
|
||||
"transition-all duration-200 hover:scale-105",
|
||||
status === "sent" && "animate-pulse",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{label}
|
||||
</Badge>
|
||||
);
|
||||
|
||||
@@ -493,7 +493,7 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="space-y-6 pb-32">
|
||||
<div className="page-enter space-y-6 pb-32">
|
||||
<PageHeader
|
||||
title={
|
||||
invoiceId && invoiceId !== "new" ? "Edit Invoice" : "Create Invoice"
|
||||
@@ -510,13 +510,18 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) {
|
||||
variant="outline"
|
||||
onClick={handleDelete}
|
||||
disabled={loading || deleteInvoice.isPending}
|
||||
className="text-destructive hover:bg-destructive/10 shadow-sm"
|
||||
className="hover-lift text-destructive hover:bg-destructive/10 shadow-sm"
|
||||
>
|
||||
<Trash2 className="h-4 w-4 sm:mr-2" />
|
||||
<span className="hidden sm:inline">Delete Invoice</span>
|
||||
</Button>
|
||||
)}
|
||||
<Button onClick={handleSubmit} disabled={loading} variant="default">
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
disabled={loading}
|
||||
variant="default"
|
||||
className="hover-lift"
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<Clock className="h-4 w-4 animate-spin sm:mr-2" />
|
||||
|
||||
@@ -55,6 +55,8 @@ export function FloatingActionBar({
|
||||
isDocked ? "flex justify-center" : "",
|
||||
// Dynamic bottom positioning
|
||||
isDocked ? "bottom-4" : "bottom-0",
|
||||
// Add entrance animation
|
||||
"animate-slide-in-bottom",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
@@ -65,17 +67,19 @@ export function FloatingActionBar({
|
||||
isDocked ? "mx-auto mb-0 px-4" : "mb-4 px-4",
|
||||
)}
|
||||
>
|
||||
<Card className="bg-card border-border border">
|
||||
<Card className="hover-lift bg-card border-border border shadow-lg">
|
||||
<CardContent className="flex items-center justify-between p-4">
|
||||
{/* Left content */}
|
||||
{leftContent && (
|
||||
<div className="flex flex-1 items-center gap-3">
|
||||
<div className="animate-fade-in flex flex-1 items-center gap-3">
|
||||
{leftContent}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Right actions */}
|
||||
<div className="flex items-center gap-2 sm:gap-3">{children}</div>
|
||||
<div className="animate-fade-in animate-delay-100 flex items-center gap-2 sm:gap-3">
|
||||
{children}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -39,22 +39,24 @@ export function PageHeader({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`mb-8 ${className}`}>
|
||||
<div className={`animate-fade-in-down mb-8 ${className}`}>
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<h1 className={titleClassName ?? getTitleClasses()}>{title}</h1>
|
||||
<div className="animate-fade-in-up space-y-1">
|
||||
<h1 className={titleClassName ?? getTitleClasses()}>{title}</h1>
|
||||
{description && (
|
||||
<p
|
||||
className={`animate-fade-in-up animate-delay-100 text-muted-foreground ${getDescriptionSpacing()} text-lg`}
|
||||
>
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{children && (
|
||||
<div className="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 [&>*]: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">
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{description && (
|
||||
<p
|
||||
className={`text-muted-foreground ${getDescriptionSpacing()} text-lg`}
|
||||
>
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
352
src/components/ui/skeletons.tsx
Normal file
352
src/components/ui/skeletons.tsx
Normal file
@@ -0,0 +1,352 @@
|
||||
import { cn } from "~/lib/utils";
|
||||
|
||||
interface SkeletonProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function Skeleton({ className }: SkeletonProps) {
|
||||
return (
|
||||
<div className={cn("skeleton bg-muted animate-pulse rounded", className)} />
|
||||
);
|
||||
}
|
||||
|
||||
// Page Header Skeleton
|
||||
export function PageHeaderSkeleton() {
|
||||
return (
|
||||
<div className="mb-8">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-8 w-48" />
|
||||
<Skeleton className="h-5 w-96" />
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Skeleton className="h-10 w-24" />
|
||||
<Skeleton className="h-10 w-32" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Invoice Items Skeleton
|
||||
export function InvoiceItemsSkeleton() {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{Array.from({ length: 3 }).map((_, i) => (
|
||||
<div key={i} className="rounded-lg border p-4">
|
||||
<div className="space-y-3">
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div className="min-w-0 flex-1 space-y-2">
|
||||
<Skeleton className="h-5 w-3/4" />
|
||||
<div className="flex flex-wrap gap-x-4 gap-y-1">
|
||||
<Skeleton className="h-4 w-20" />
|
||||
<Skeleton className="h-4 w-16" />
|
||||
<Skeleton className="h-4 w-24" />
|
||||
</div>
|
||||
</div>
|
||||
<Skeleton className="h-6 w-20" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Recent Activity Skeleton
|
||||
export function RecentActivitySkeleton() {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{Array.from({ length: 5 }).map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="bg-muted/50 border-foreground/20 rounded-lg border p-3"
|
||||
>
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<Skeleton className="h-8 w-8 rounded" />
|
||||
<div className="space-y-1">
|
||||
<Skeleton className="h-4 w-24" />
|
||||
<Skeleton className="h-3 w-32" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Skeleton className="h-5 w-12 rounded-full" />
|
||||
<Skeleton className="h-4 w-16" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Current Work Skeleton
|
||||
export function CurrentWorkSkeleton() {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Skeleton className="h-5 w-5" />
|
||||
<Skeleton className="h-6 w-24" />
|
||||
</div>
|
||||
<Skeleton className="h-6 w-20 rounded-full" />
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
<Skeleton className="h-6 w-32" />
|
||||
<Skeleton className="h-8 w-24" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1 sm:flex-row sm:items-center sm:justify-between">
|
||||
<Skeleton className="h-4 w-28" />
|
||||
<Skeleton className="h-4 w-20" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Skeleton className="h-8 flex-1" />
|
||||
<Skeleton className="h-8 flex-1" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Client Info Skeleton
|
||||
export function ClientInfoSkeleton() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div className="flex items-center space-x-3">
|
||||
<Skeleton className="h-8 w-8" />
|
||||
<div className="space-y-1">
|
||||
<Skeleton className="h-4 w-12" />
|
||||
<Skeleton className="h-4 w-32" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
<Skeleton className="h-8 w-8" />
|
||||
<div className="space-y-1">
|
||||
<Skeleton className="h-4 w-12" />
|
||||
<Skeleton className="h-4 w-24" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Skeleton className="mb-4 h-6 w-32" />
|
||||
<div className="flex items-start space-x-3">
|
||||
<Skeleton className="h-8 w-8" />
|
||||
<div className="space-y-1">
|
||||
<Skeleton className="h-4 w-40" />
|
||||
<Skeleton className="h-4 w-36" />
|
||||
<Skeleton className="h-4 w-28" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Stats Summary Skeleton
|
||||
export function StatsSummarySkeleton() {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2 text-center">
|
||||
<Skeleton className="mx-auto h-8 w-24" />
|
||||
<Skeleton className="mx-auto h-4 w-20" />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4 text-center">
|
||||
<div className="space-y-1">
|
||||
<Skeleton className="mx-auto h-6 w-8" />
|
||||
<Skeleton className="mx-auto h-3 w-12" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Skeleton className="mx-auto h-6 w-8" />
|
||||
<Skeleton className="mx-auto h-3 w-16" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Recent Invoices Skeleton
|
||||
export function RecentInvoicesSkeleton() {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{Array.from({ length: 3 }).map((_, i) => (
|
||||
<div key={i} className="card-secondary hover:bg-muted/50 border p-3">
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="min-w-0 space-y-1">
|
||||
<Skeleton className="h-4 w-24" />
|
||||
<Skeleton className="h-3 w-20" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Skeleton className="h-4 w-16" />
|
||||
<Skeleton className="h-5 w-12 rounded-full" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Settings Form Skeleton
|
||||
export function SettingsFormSkeleton() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-4 w-16" />
|
||||
<Skeleton className="h-10 w-full" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-4 w-20" />
|
||||
<Skeleton className="h-10 w-full" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Skeleton className="h-10 w-24" />
|
||||
<Skeleton className="h-10 w-20" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Data Stats Skeleton
|
||||
export function DataStatsSkeleton() {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{Array.from({ length: 4 }).map((_, i) => (
|
||||
<div key={i} className="bg-card border p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<Skeleton className="h-8 w-8" />
|
||||
<Skeleton className="h-4 w-20" />
|
||||
</div>
|
||||
<Skeleton className="h-6 w-8" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Invoice Summary Skeleton
|
||||
export function InvoiceSummarySkeleton() {
|
||||
return (
|
||||
<div className="bg-muted/30 rounded-lg p-4">
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between">
|
||||
<Skeleton className="h-4 w-16" />
|
||||
<Skeleton className="h-4 w-20" />
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<Skeleton className="h-4 w-20" />
|
||||
<Skeleton className="h-4 w-16" />
|
||||
</div>
|
||||
<div className="bg-border h-px" />
|
||||
<div className="flex justify-between">
|
||||
<Skeleton className="h-5 w-12" />
|
||||
<Skeleton className="h-5 w-24" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Actions Sidebar Skeleton
|
||||
export function ActionsSidebarSkeleton() {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<Skeleton className="h-10 w-full" />
|
||||
<Skeleton className="h-10 w-full" />
|
||||
<Skeleton className="h-10 w-full" />
|
||||
<Skeleton className="h-10 w-full" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Form Section Skeleton
|
||||
export function FormSectionSkeleton() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-4 w-20" />
|
||||
<Skeleton className="h-10 w-full" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-4 w-16" />
|
||||
<Skeleton className="h-10 w-full" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-4 w-24" />
|
||||
<Skeleton className="h-32 w-full" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Line Items Table Skeleton
|
||||
export function LineItemsTableSkeleton() {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{Array.from({ length: 4 }).map((_, i) => (
|
||||
<div key={i} className="grid grid-cols-5 gap-4 rounded border p-3">
|
||||
<Skeleton className="h-8 w-full" />
|
||||
<Skeleton className="h-8 w-full" />
|
||||
<Skeleton className="h-8 w-full" />
|
||||
<Skeleton className="h-8 w-full" />
|
||||
<Skeleton className="h-8 w-full" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Business Card Skeleton
|
||||
export function BusinessCardSkeleton() {
|
||||
return (
|
||||
<div className="space-y-4 rounded-lg border p-4">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-5 w-32" />
|
||||
<Skeleton className="h-4 w-24" />
|
||||
</div>
|
||||
<Skeleton className="h-5 w-16 rounded-full" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-4 w-40" />
|
||||
<Skeleton className="h-4 w-28" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Generic Card Grid Skeleton
|
||||
export function CardGridSkeleton({ count = 6 }: { count?: number }) {
|
||||
return (
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{Array.from({ length: count }).map((_, i) => (
|
||||
<div key={i} className="rounded-lg border p-4">
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<Skeleton className="h-8 w-8" />
|
||||
<Skeleton className="h-5 w-24" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-4 w-full" />
|
||||
<Skeleton className="h-4 w-3/4" />
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<Skeleton className="h-4 w-16" />
|
||||
<Skeleton className="h-8 w-20" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user