Compare commits

...

3 Commits

45 changed files with 850 additions and 459 deletions

View File

@@ -11,6 +11,7 @@ import {
import { useBankingMetadata, useCategoryStats } from "@/lib/hooks";
import { useQueryClient } from "@tanstack/react-query";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import {
Dialog,
DialogContent,
@@ -43,7 +44,7 @@ export default function CategoriesPage() {
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [editingCategory, setEditingCategory] = useState<Category | null>(null);
const [expandedParents, setExpandedParents] = useState<Set<string>>(
new Set(),
new Set()
);
const [formData, setFormData] = useState({
name: "",
@@ -54,7 +55,7 @@ export default function CategoriesPage() {
});
const [searchQuery, setSearchQuery] = useState("");
const [recatResults, setRecatResults] = useState<RecategorizationResult[]>(
[],
[]
);
const [isRecatDialogOpen, setIsRecatDialogOpen] = useState(false);
const [isRecategorizing, setIsRecategorizing] = useState(false);
@@ -76,7 +77,7 @@ export default function CategoriesPage() {
};
const parents = metadata.categories.filter(
(c: Category) => c.parentId === null,
(c: Category) => c.parentId === null
);
const children: Record<string, Category[]> = {};
const orphans: Category[] = [];
@@ -85,7 +86,7 @@ export default function CategoriesPage() {
.filter((c: Category) => c.parentId !== null)
.forEach((child: Category) => {
const parentExists = parents.some(
(p: Category) => p.id === child.parentId,
(p: Category) => p.id === child.parentId
);
if (parentExists) {
if (!children[child.parentId!]) {
@@ -108,7 +109,9 @@ export default function CategoriesPage() {
useEffect(() => {
if (parentCategories.length > 0 && expandedParents.size === 0) {
if (expandAllByDefault) {
setExpandedParents(new Set(parentCategories.map((p: Category) => p.id)));
setExpandedParents(
new Set(parentCategories.map((p: Category) => p.id))
);
} else {
setExpandedParents(new Set());
}
@@ -147,7 +150,7 @@ export default function CategoriesPage() {
return { total, count };
},
[categoryStats, childrenByParent],
[categoryStats, childrenByParent]
);
if (isLoadingMetadata || !metadata || isLoadingStats || !categoryStats) {
@@ -261,7 +264,7 @@ export default function CategoriesPage() {
try {
// Fetch uncategorized transactions
const uncategorizedResponse = await fetch(
"/api/banking/transactions?limit=1000&offset=0&includeUncategorized=true",
"/api/banking/transactions?limit=1000&offset=0&includeUncategorized=true"
);
if (!uncategorizedResponse.ok) {
throw new Error("Failed to fetch uncategorized transactions");
@@ -274,11 +277,11 @@ export default function CategoriesPage() {
for (const transaction of uncategorized) {
const categoryId = autoCategorize(
transaction.description + " " + (transaction.memo || ""),
metadata.categories,
metadata.categories
);
if (categoryId) {
const category = metadata.categories.find(
(c: Category) => c.id === categoryId,
(c: Category) => c.id === categoryId
);
if (category) {
results.push({ transaction, category });
@@ -312,9 +315,9 @@ export default function CategoriesPage() {
return children.some(
(c) =>
c.name.toLowerCase().includes(query) ||
c.keywords.some((k) => k.toLowerCase().includes(query)),
c.keywords.some((k) => k.toLowerCase().includes(query))
);
},
}
);
return (
@@ -359,9 +362,9 @@ export default function CategoriesPage() {
(c) =>
c.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
c.keywords.some((k) =>
k.toLowerCase().includes(searchQuery.toLowerCase()),
k.toLowerCase().includes(searchQuery.toLowerCase())
) ||
parent.name.toLowerCase().includes(searchQuery.toLowerCase()),
parent.name.toLowerCase().includes(searchQuery.toLowerCase())
)
: allChildren;
const stats = getCategoryStats(parent.id, true);
@@ -448,7 +451,7 @@ export default function CategoriesPage() {
</p>
<p className="text-xs text-muted-foreground truncate">
{new Date(result.transaction.date).toLocaleDateString(
"fr-FR",
"fr-FR"
)}
{" • "}
{new Intl.NumberFormat("fr-FR", {

File diff suppressed because it is too large Load Diff

View File

@@ -5,6 +5,7 @@ import "./globals.css";
import { AuthSessionProvider } from "@/components/providers/session-provider";
import { QueryProvider } from "@/components/providers/query-provider";
import { BackgroundProvider } from "@/components/providers/background-provider";
import { ThemeProvider } from "@/components/theme-provider";
const _geist = Geist({ subsets: ["latin"] });
const _geistMono = Geist_Mono({ subsets: ["latin"] });
@@ -22,12 +23,19 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
<html lang="fr">
<html lang="fr" suppressHydrationWarning>
<body className="font-sans antialiased">
<BackgroundProvider />
<QueryProvider>
<AuthSessionProvider>{children}</AuthSessionProvider>
</QueryProvider>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
<BackgroundProvider />
<QueryProvider>
<AuthSessionProvider>{children}</AuthSessionProvider>
</QueryProvider>
</ThemeProvider>
</body>
</html>
);

View File

@@ -8,7 +8,6 @@ import { AccountsSummary } from "@/components/dashboard/accounts-summary";
import { CategoryBreakdown } from "@/components/dashboard/category-breakdown";
import { OFXImportDialog } from "@/components/import/ofx-import-dialog";
import { AccountFilterCombobox } from "@/components/ui/account-filter-combobox";
import { Card, CardContent } from "@/components/ui/card";
import { useBankingData } from "@/lib/hooks";
import { Button } from "@/components/ui/button";
import { Upload } from "lucide-react";

View File

@@ -39,7 +39,6 @@ export default function RulesPage() {
const {
data: transactionsData,
isLoading: isLoadingTransactions,
invalidate: invalidateTransactions,
} = useTransactions(
{
limit: 10000, // Large limit to get all uncategorized
@@ -49,7 +48,7 @@ export default function RulesPage() {
!!metadata,
);
const refresh = useCallback(() => {
const _refresh = useCallback(() => {
invalidateAllTransactionQueries(queryClient);
invalidateAllCategoryQueries(queryClient);
}, [queryClient]);

View File

@@ -188,7 +188,7 @@ export function AccountCard({
? "text-base"
: "text-xl",
!compact && !isMobile && "mb-1.5",
realBalance >= 0 ? "text-emerald-600" : "text-red-600",
realBalance >= 0 ? "text-success" : "text-destructive",
)}
>
{formatCurrency(realBalance)}

View File

@@ -125,7 +125,7 @@ export function ParentCategoryRow({
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => onDelete(parent.id)}
className="text-red-600"
className="text-destructive"
>
<Trash2 className="w-4 h-4 mr-2" />
Supprimer

View File

@@ -89,7 +89,7 @@ export function AccountsSummary({ data }: AccountsSummaryProps) {
<span
className={cn(
"text-xs font-semibold tabular-nums ml-auto",
folderTotal >= 0 ? "text-emerald-600" : "text-red-600",
folderTotal >= 0 ? "text-success" : "text-destructive",
)}
>
{formatCurrency(folderTotal)}
@@ -131,8 +131,8 @@ export function AccountsSummary({ data }: AccountsSummaryProps) {
className={cn(
"font-bold tabular-nums text-base",
realBalance >= 0
? "text-emerald-600 dark:text-emerald-400"
: "text-red-600 dark:text-red-400",
? "text-success"
: "text-destructive",
)}
>
{formatCurrency(realBalance)}
@@ -204,7 +204,7 @@ export function AccountsSummary({ data }: AccountsSummaryProps) {
<span
className={cn(
"text-xs font-semibold tabular-nums ml-auto",
orphanTotal >= 0 ? "text-emerald-600" : "text-red-600",
orphanTotal >= 0 ? "text-success" : "text-destructive",
)}
>
{formatCurrency(orphanTotal)}
@@ -245,8 +245,8 @@ export function AccountsSummary({ data }: AccountsSummaryProps) {
className={cn(
"font-bold tabular-nums text-base",
realBalance >= 0
? "text-emerald-600 dark:text-emerald-400"
: "text-red-600 dark:text-red-400",
? "text-success"
: "text-destructive",
)}
>
{formatCurrency(realBalance)}

View File

@@ -60,7 +60,7 @@ export function OverviewCards({ data }: OverviewCardsProps) {
<div className="grid gap-4 sm:gap-6 grid-cols-1 sm:grid-cols-2 lg:grid-cols-5">
<Card className="stat-card-gradient-1 card-hover group relative overflow-hidden">
{/* Icône en arrière-plan */}
<div className="absolute bottom-2 right-2 opacity-[0.04] dark:opacity-[0.03] z-0 pointer-events-none">
<div className="absolute bottom-2 right-2 opacity-[0.04] z-0 pointer-events-none">
<Wallet className="h-20 w-20 sm:h-24 sm:w-24 md:h-28 md:w-28 text-primary" strokeWidth={1} />
</div>
<CardHeader className="flex flex-row items-start justify-between space-y-0 pb-3 px-5 pt-5 sm:px-6 sm:pt-6 relative z-10">
@@ -73,8 +73,8 @@ export function OverviewCards({ data }: OverviewCardsProps) {
className={cn(
"text-2xl sm:text-3xl md:text-3xl lg:text-2xl xl:text-3xl font-black tracking-tight mb-3 leading-tight break-words",
totalBalance >= 0
? "text-emerald-600 dark:text-emerald-400"
: "text-red-600 dark:text-red-400"
? "text-success"
: "text-destructive"
)}
>
{formatCurrency(totalBalance)}
@@ -87,7 +87,7 @@ export function OverviewCards({ data }: OverviewCardsProps) {
<Card className="stat-card-gradient-2 card-hover group relative overflow-hidden">
{/* Icône en arrière-plan */}
<div className="absolute bottom-2 right-2 opacity-[0.04] dark:opacity-[0.03] z-0 pointer-events-none">
<div className="absolute bottom-2 right-2 opacity-[0.04] z-0 pointer-events-none">
<TrendingUp className="h-20 w-20 sm:h-24 sm:w-24 md:h-28 md:w-28 text-success" strokeWidth={1} />
</div>
<CardHeader className="flex flex-row items-start justify-between space-y-0 pb-3 px-5 pt-5 sm:px-6 sm:pt-6 relative z-10">
@@ -110,7 +110,7 @@ export function OverviewCards({ data }: OverviewCardsProps) {
<Card className="stat-card-gradient-3 card-hover group relative overflow-hidden">
{/* Icône en arrière-plan */}
<div className="absolute bottom-2 right-2 opacity-[0.04] dark:opacity-[0.03] z-0 pointer-events-none">
<div className="absolute bottom-2 right-2 opacity-[0.04] z-0 pointer-events-none">
<TrendingDown className="h-20 w-20 sm:h-24 sm:w-24 md:h-28 md:w-28 text-destructive" strokeWidth={1} />
</div>
<CardHeader className="flex flex-row items-start justify-between space-y-0 pb-3 px-5 pt-5 sm:px-6 sm:pt-6 relative z-10">
@@ -133,7 +133,7 @@ export function OverviewCards({ data }: OverviewCardsProps) {
<Card className="stat-card-gradient-4 card-hover group relative overflow-hidden">
{/* Icône en arrière-plan */}
<div className="absolute bottom-2 right-2 opacity-[0.04] dark:opacity-[0.03] z-0 pointer-events-none">
<div className="absolute bottom-2 right-2 opacity-[0.04] z-0 pointer-events-none">
<CreditCard className="h-20 w-20 sm:h-24 sm:w-24 md:h-28 md:w-28 text-chart-4" strokeWidth={1} />
</div>
<CardHeader className="flex flex-row items-start justify-between space-y-0 pb-3 px-5 pt-5 sm:px-6 sm:pt-6 relative z-10">
@@ -153,7 +153,7 @@ export function OverviewCards({ data }: OverviewCardsProps) {
<Card className="stat-card-gradient-5 card-hover group relative overflow-hidden">
{/* Icône en arrière-plan */}
<div className="absolute bottom-2 right-2 opacity-[0.04] dark:opacity-[0.03] z-0 pointer-events-none">
<div className="absolute bottom-2 right-2 opacity-[0.04] z-0 pointer-events-none">
<Tag className="h-20 w-20 sm:h-24 sm:w-24 md:h-28 md:w-28 text-chart-5" strokeWidth={1} />
</div>
<CardHeader className="flex flex-row items-start justify-between space-y-0 pb-3 px-5 pt-5 sm:px-6 sm:pt-6 relative z-10">

View File

@@ -88,8 +88,8 @@ export function RecentTransactions({ data }: RecentTransactionsProps) {
className={cn(
"font-black tabular-nums text-sm md:text-base shrink-0 md:hidden",
transaction.amount >= 0
? "text-emerald-600 dark:text-emerald-400"
: "text-red-600 dark:text-red-400"
? "text-success"
: "text-destructive"
)}
>
{transaction.amount >= 0 ? "+" : ""}
@@ -130,8 +130,8 @@ export function RecentTransactions({ data }: RecentTransactionsProps) {
className={cn(
"font-black tabular-nums text-base md:text-lg shrink-0 hidden md:block leading-tight",
transaction.amount >= 0
? "text-emerald-600 dark:text-emerald-400"
: "text-red-600 dark:text-red-400"
? "text-success"
: "text-destructive"
)}
>
{transaction.amount >= 0 ? "+" : ""}

View File

@@ -77,31 +77,33 @@ function SidebarContent({
</div>
)}
<nav className={cn("flex-1 space-y-2", collapsed ? "p-2" : "p-4")}>
<nav className={cn("flex-1 pt-2", collapsed ? "p-2" : "p-4")}>
{navItems.map((item) => {
const isActive = pathname === item.href;
return (
<Link key={item.href} href={item.href} onClick={handleLinkClick}>
<Link
key={item.href}
href={item.href}
onClick={handleLinkClick}
className="block mb-2 first:mt-0"
>
<Button
variant={isActive ? "secondary" : "ghost"}
className={cn(
"w-full justify-start gap-4 h-12 rounded-2xl",
"w-full justify-start gap-3 h-12 rounded-2xl px-3",
isActive &&
"bg-gradient-to-r from-primary/15 via-primary/10 to-primary/5 border-2 border-primary/30 shadow-lg shadow-primary/10 backdrop-blur-sm",
collapsed && "justify-center px-2 w-12 mx-auto",
collapsed && "justify-center px-2 w-12 mx-auto"
)}
>
<item.icon
className={cn(
"w-5 h-5 shrink-0",
isActive && "text-primary",
)}
className={cn("w-5 h-5 shrink-0", isActive && "text-primary")}
/>
{!collapsed && (
<span
className={cn(
"font-semibold text-sm",
isActive && "text-primary font-bold",
isActive && "text-primary font-bold"
)}
>
{item.label}
@@ -115,16 +117,16 @@ function SidebarContent({
<div
className={cn(
"border-t border-border/30 space-y-2",
collapsed ? "p-2" : "p-4",
"border-t border-border/30 pt-2",
collapsed ? "p-2" : "p-4"
)}
>
<Link href="/settings" onClick={handleLinkClick}>
<Link href="/settings" onClick={handleLinkClick} className="block mb-2">
<Button
variant="ghost"
className={cn(
"w-full justify-start gap-4 h-12 rounded-2xl",
collapsed && "justify-center px-2 w-12 mx-auto",
"w-full justify-start gap-3 h-12 rounded-2xl px-3",
collapsed && "justify-center px-2 w-12 mx-auto"
)}
>
<Settings className="w-5 h-5 shrink-0" />
@@ -137,9 +139,9 @@ function SidebarContent({
variant="ghost"
onClick={handleSignOut}
className={cn(
"w-full justify-start gap-4 h-12 rounded-2xl",
"w-full justify-start gap-3 h-12 rounded-2xl px-3 mb-2",
"text-destructive",
collapsed && "justify-center px-2 w-12 mx-auto",
collapsed && "justify-center px-2 w-12 mx-auto"
)}
>
<LogOut className="w-5 h-5 shrink-0" />
@@ -164,7 +166,10 @@ export function Sidebar({ open, onOpenChange }: SidebarProps) {
if (isMobile) {
return (
<Sheet open={open} onOpenChange={onOpenChange}>
<SheetContent side="left" className="w-64 p-0">
<SheetContent
side="left"
className="w-64 p-0 bg-sidebar text-sidebar-foreground border-sidebar-border"
>
<SheetTitle className="sr-only">Navigation</SheetTitle>
<div className="flex flex-col h-full">
<SidebarContent
@@ -180,15 +185,15 @@ export function Sidebar({ open, onOpenChange }: SidebarProps) {
return (
<aside
className={cn(
"hidden md:flex flex-col h-screen glass border-r border-border",
"hidden md:flex flex-col h-screen bg-sidebar text-sidebar-foreground border-r border-sidebar-border",
"backdrop-blur-xl",
collapsed ? "w-16" : "w-64",
collapsed ? "w-16" : "w-64"
)}
>
<div
className={cn(
"flex items-center border-b border-border/30",
collapsed ? "justify-center p-4" : "justify-between p-6",
collapsed ? "justify-center p-4" : "justify-between p-6"
)}
>
{!collapsed && (
@@ -205,10 +210,7 @@ export function Sidebar({ open, onOpenChange }: SidebarProps) {
variant="ghost"
size="icon"
onClick={() => setCollapsed(!collapsed)}
className={cn(
"rounded-xl",
collapsed ? "" : "ml-auto",
)}
className={cn("rounded-xl", collapsed ? "" : "ml-auto")}
>
{collapsed ? (
<ChevronRight className="w-5 h-5" />

View File

@@ -74,7 +74,7 @@ export function DraggableAccountItem({
<span
className={cn(
"text-sm tabular-nums",
realBalance >= 0 ? "text-emerald-600" : "text-red-600",
realBalance >= 0 ? "text-success" : "text-destructive",
)}
>
{formatCurrency(realBalance)}

View File

@@ -120,7 +120,7 @@ export function DraggableFolderItem({
<span
className={cn(
"text-sm font-semibold tabular-nums",
folderTotal >= 0 ? "text-emerald-600" : "text-red-600",
folderTotal >= 0 ? "text-success" : "text-destructive",
)}
>
{formatCurrency(folderTotal)}
@@ -145,7 +145,7 @@ export function DraggableFolderItem({
{folder.id !== "folder-root" && (
<DropdownMenuItem
onClick={() => onDelete(folder.id)}
className="text-red-600"
className="text-destructive"
>
<Trash2 className="w-4 h-4 mr-2" />
Supprimer

View File

@@ -505,9 +505,9 @@ export function OFXImportDialog({
{importResults.map((result, i) => (
<div key={i} className="flex items-center gap-2">
{result.error ? (
<AlertCircle className="w-4 h-4 text-red-500 flex-shrink-0" />
<AlertCircle className="w-4 h-4 text-destructive flex-shrink-0" />
) : (
<CheckCircle2 className="w-4 h-4 text-emerald-500 flex-shrink-0" />
<CheckCircle2 className="w-4 h-4 text-success flex-shrink-0" />
)}
<span className="truncate">{result.fileName}</span>
{!result.error && (
@@ -524,16 +524,16 @@ export function OFXImportDialog({
{step === "success" && (
<div className="py-4">
<CheckCircle2 className="w-16 h-16 mx-auto mb-4 text-emerald-600" />
<CheckCircle2 className="w-16 h-16 mx-auto mb-4 text-success" />
{importResults.length > 1 && (
<div className="max-h-48 overflow-auto space-y-1 text-sm mb-4 border rounded-lg p-2">
{importResults.map((result, i) => (
<div key={i} className="flex items-center gap-2 py-1">
{result.error ? (
<AlertCircle className="w-4 h-4 text-red-500 flex-shrink-0" />
<AlertCircle className="w-4 h-4 text-destructive flex-shrink-0" />
) : (
<CheckCircle2 className="w-4 h-4 text-emerald-500 flex-shrink-0" />
<CheckCircle2 className="w-4 h-4 text-success flex-shrink-0" />
)}
<div className="flex-1 min-w-0">
<p className="truncate font-medium">
@@ -544,7 +544,7 @@ export function OFXImportDialog({
</p>
</div>
{result.error ? (
<span className="text-xs text-red-500 flex-shrink-0">
<span className="text-xs text-destructive flex-shrink-0">
{result.error}
</span>
) : (
@@ -559,7 +559,7 @@ export function OFXImportDialog({
)}
{errorCount > 0 && (
<p className="text-sm text-red-600 mb-4 text-center">
<p className="text-sm text-destructive mb-4 text-center">
{errorCount} fichier{errorCount > 1 ? "s" : ""} en erreur
</p>
)}
@@ -572,7 +572,7 @@ export function OFXImportDialog({
{step === "error" && (
<div className="text-center py-4">
<AlertCircle className="w-16 h-16 mx-auto mb-4 text-red-600" />
<AlertCircle className="w-16 h-16 mx-auto mb-4 text-destructive" />
<Button onClick={() => setStep("upload")}>Réessayer</Button>
</div>
)}

View File

@@ -1,6 +1,6 @@
"use client";
import { ReactNode } from "react";
import { ReactNode, useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import { Menu } from "lucide-react";
import { useSidebarContext } from "@/components/layout/sidebar-context";
@@ -21,6 +21,30 @@ export function PageHeader({
}: PageHeaderProps) {
const { setOpen } = useSidebarContext();
const isMobile = useIsMobile();
const [textColor, setTextColor] = useState("var(--foreground)");
useEffect(() => {
const checkDarkBackground = () => {
const pageBackground = document.querySelector(".page-background");
if (pageBackground?.classList.contains("bg-solid-dark")) {
setTextColor("#f5f5f5");
} else {
setTextColor("var(--foreground)");
}
};
checkDarkBackground();
const observer = new MutationObserver(checkDarkBackground);
const pageBackground = document.querySelector(".page-background");
if (pageBackground) {
observer.observe(pageBackground, {
attributes: true,
attributeFilter: ["class"],
});
}
return () => observer.disconnect();
}, []);
return (
<div className="flex flex-col gap-4 mb-2">
@@ -37,7 +61,13 @@ export function PageHeader({
)}
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between gap-2 mb-2">
<h1 className="text-2xl md:text-4xl lg:text-5xl font-black text-foreground tracking-tight leading-tight flex-1 min-w-0">
<h1
className="text-2xl md:text-4xl lg:text-5xl font-black tracking-tight leading-tight flex-1 min-w-0"
style={{
color: textColor,
WebkitTextFillColor: textColor,
}}
>
{title}
</h1>
{rightContent && <div className="shrink-0">{rightContent}</div>}

View File

@@ -133,7 +133,7 @@ export function RuleCreateDialog({
catégorisées.
</p>
{existingCategory && (
<div className="flex items-center gap-2 text-xs text-amber-600 dark:text-amber-400">
<div className="flex items-center gap-2 text-xs text-warning">
<AlertCircle className="h-3 w-3" />
<span>
Ce mot-clé existe déjà dans &quot;{existingCategory.name}

View File

@@ -35,42 +35,49 @@ const DEFAULT_BACKGROUNDS: Array<{
value: BackgroundType;
label: string;
preview: string;
description?: string;
}> = [
{
value: "default",
label: "Par défaut",
preview:
"linear-gradient(135deg, oklch(0.98 0.01 280) 0%, oklch(0.97 0.012 270) 50%, oklch(0.98 0.01 290) 100%)",
label: "Neutre",
preview: "linear-gradient(135deg, oklch(0.985 0 0) 0%, oklch(0.97 0.005 260) 50%, oklch(0.985 0 0) 100%)",
description: "Fond neutre et élégant",
},
{
value: "gradient-blue",
label: "Dégradé bleu",
preview: "linear-gradient(135deg, #e0f2fe 0%, #bae6fd 50%, #7dd3fc 100%)",
label: "Océan",
preview: "linear-gradient(135deg, oklch(0.95 0.03 230) 0%, oklch(0.88 0.08 225) 50%, oklch(0.78 0.12 220) 100%)",
description: "Dégradé bleu apaisant",
},
{
value: "gradient-purple",
label: "Dégradé violet",
preview: "linear-gradient(135deg, #f3e8ff 0%, #e9d5ff 50%, #d8b4fe 100%)",
label: "Améthyste",
preview: "linear-gradient(135deg, oklch(0.95 0.04 300) 0%, oklch(0.88 0.1 295) 50%, oklch(0.78 0.15 290) 100%)",
description: "Dégradé violet sophistiqué",
},
{
value: "gradient-green",
label: "Dégradé vert",
preview: "linear-gradient(135deg, #dcfce7 0%, #bbf7d0 50%, #86efac 100%)",
label: "Forêt",
preview: "linear-gradient(135deg, oklch(0.95 0.04 160) 0%, oklch(0.88 0.1 155) 50%, oklch(0.78 0.14 150) 100%)",
description: "Dégradé vert naturel",
},
{
value: "gradient-orange",
label: "Dégradé orange",
preview: "linear-gradient(135deg, #fff7ed 0%, #ffedd5 50%, #fed7aa 100%)",
label: "Aurore",
preview: "linear-gradient(135deg, oklch(0.97 0.03 80) 0%, oklch(0.92 0.08 60) 50%, oklch(0.85 0.14 45) 100%)",
description: "Dégradé orange chaleureux",
},
{
value: "solid-light",
label: "Solide clair",
preview: "#ffffff",
label: "Lumineux",
preview: "linear-gradient(135deg, oklch(1 0 0) 0%, oklch(0.98 0.005 260) 100%)",
description: "Fond blanc épuré",
},
{
value: "solid-dark",
label: "Solide sombre",
preview: "#1e293b",
label: "Minuit",
preview: "linear-gradient(135deg, oklch(0.18 0.02 260) 0%, oklch(0.08 0.015 250) 100%)",
description: "Fond sombre immersif",
},
];

View File

@@ -46,7 +46,7 @@ export function DangerZoneCard({
const result = await onDeduplicate();
if (result.deletedCount > 0) {
alert(
`${result.deletedCount} transaction${result.deletedCount > 1 ? "s" : ""} en double supprimée${result.deletedCount > 1 ? "s" : ""}`,
`${result.deletedCount} transaction${result.deletedCount > 1 ? "s" : ""} en double supprimée${result.deletedCount > 1 ? "s" : ""}`
);
} else {
alert("Aucun doublon trouvé");
@@ -59,9 +59,9 @@ export function DangerZoneCard({
}
};
return (
<Card className="border-red-200">
<Card className="border-destructive/30 card-hover">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-red-600">
<CardTitle className="flex items-center gap-2 text-destructive">
<Trash2 className="w-5 h-5" />
Zone dangereuse
</CardTitle>

View File

@@ -31,7 +31,15 @@ export function CategoryBarChart({
const displayData = data.slice(0, maxItems).reverse(); // Reverse pour avoir le plus grand en haut
// Custom tick component for clickable labels
const CustomYAxisTick = ({ x, y, payload }: any) => {
const CustomYAxisTick = ({
x,
y,
payload,
}: {
x: number;
y: number;
payload: { value: string };
}) => {
const categoryName = payload.value;
const item = displayData.find((d) => d.name === categoryName);

View File

@@ -36,13 +36,13 @@ export function SavingsTrendChart({
<CardTitle>Évolution des économies</CardTitle>
<div className="flex items-center gap-2">
{isPositive ? (
<TrendingUp className="w-4 h-4 text-emerald-600" />
<TrendingUp className="w-4 h-4 text-success" />
) : (
<TrendingDown className="w-4 h-4 text-red-600" />
<TrendingDown className="w-4 h-4 text-destructive" />
)}
<span
className={`text-sm font-semibold ${
isPositive ? "text-emerald-600" : "text-red-600"
isPositive ? "text-success" : "text-destructive"
}`}
>
{formatCurrency(latestSavings)}

View File

@@ -23,8 +23,8 @@ export function StatsSummaryCards({
<div className="grid gap-3 md:gap-4 grid-cols-2 md:grid-cols-4">
<Card className="stat-card-textured relative overflow-hidden">
{/* Icône en arrière-plan */}
<div className="absolute bottom-2 right-2 opacity-[0.04] dark:opacity-[0.03] z-0 pointer-events-none">
<TrendingUp className="h-16 w-16 md:h-20 md:w-20 text-emerald-600 dark:text-emerald-400" strokeWidth={1} />
<div className="absolute bottom-2 right-2 opacity-[0.04] z-0 pointer-events-none">
<TrendingUp className="h-16 w-16 md:h-20 md:w-20 text-success" strokeWidth={1} />
</div>
<CardHeader className="pb-3 px-5 pt-5 relative z-10">
<CardTitle className="text-[10px] md:text-xs font-semibold text-muted-foreground/80 uppercase tracking-wider">
@@ -32,7 +32,7 @@ export function StatsSummaryCards({
</CardTitle>
</CardHeader>
<CardContent className="px-5 pb-5 pt-0 relative z-10">
<div className="text-xl md:text-2xl font-black text-emerald-600 dark:text-emerald-400 tracking-tight">
<div className="text-xl md:text-2xl font-black text-success tracking-tight">
{formatCurrency(totalIncome)}
</div>
</CardContent>
@@ -40,8 +40,8 @@ export function StatsSummaryCards({
<Card className="stat-card-textured relative overflow-hidden">
{/* Icône en arrière-plan */}
<div className="absolute bottom-2 right-2 opacity-[0.04] dark:opacity-[0.03] z-0 pointer-events-none">
<TrendingDown className="h-16 w-16 md:h-20 md:w-20 text-red-600 dark:text-red-400" strokeWidth={1} />
<div className="absolute bottom-2 right-2 opacity-[0.04] z-0 pointer-events-none">
<TrendingDown className="h-16 w-16 md:h-20 md:w-20 text-destructive" strokeWidth={1} />
</div>
<CardHeader className="pb-3 px-5 pt-5 relative z-10">
<CardTitle className="text-[10px] md:text-xs font-semibold text-muted-foreground/80 uppercase tracking-wider">
@@ -49,7 +49,7 @@ export function StatsSummaryCards({
</CardTitle>
</CardHeader>
<CardContent className="px-5 pb-5 pt-0 relative z-10">
<div className="text-xl md:text-2xl font-black text-red-600 dark:text-red-400 tracking-tight">
<div className="text-xl md:text-2xl font-black text-destructive tracking-tight">
{formatCurrency(totalExpenses)}
</div>
</CardContent>
@@ -57,7 +57,7 @@ export function StatsSummaryCards({
<Card className="stat-card-textured relative overflow-hidden">
{/* Icône en arrière-plan */}
<div className="absolute bottom-2 right-2 opacity-[0.04] dark:opacity-[0.03] z-0 pointer-events-none">
<div className="absolute bottom-2 right-2 opacity-[0.04] z-0 pointer-events-none">
<ArrowRight className="h-16 w-16 md:h-20 md:w-20 text-muted-foreground/40" strokeWidth={1} />
</div>
<CardHeader className="pb-3 px-5 pt-5 relative z-10">
@@ -74,12 +74,12 @@ export function StatsSummaryCards({
<Card className="stat-card-textured relative overflow-hidden">
{/* Icône en arrière-plan */}
<div className="absolute bottom-2 right-2 opacity-[0.04] dark:opacity-[0.03] z-0 pointer-events-none">
<div className="absolute bottom-2 right-2 opacity-[0.04] z-0 pointer-events-none">
<div className={cn(
"h-16 w-16 md:h-20 md:w-20 rounded-full border-2",
savings >= 0
? "border-emerald-600 dark:border-emerald-400"
: "border-red-600 dark:border-red-400"
? "border-success"
: "border-destructive"
)} />
</div>
<CardHeader className="pb-3 px-5 pt-5 relative z-10">
@@ -91,7 +91,7 @@ export function StatsSummaryCards({
<div
className={cn(
"text-xl md:text-2xl font-black tracking-tight",
savings >= 0 ? "text-emerald-600 dark:text-emerald-400" : "text-red-600 dark:text-red-400",
savings >= 0 ? "text-success" : "text-destructive",
)}
>
{formatCurrency(savings)}

View File

@@ -45,7 +45,7 @@ export function TopExpensesList({
<p className="font-medium text-xs md:text-sm truncate flex-1">
{expense.description}
</p>
<div className="text-red-600 font-semibold tabular-nums text-xs md:text-sm shrink-0">
<div className="text-destructive font-semibold tabular-nums text-xs md:text-sm shrink-0">
{formatCurrency(expense.amount)}
</div>
</div>

View File

@@ -227,7 +227,7 @@ export function TransactionTable({
);
return (
<Card className="overflow-hidden w-full">
<Card className="overflow-hidden w-full card-hover">
<CardContent className="p-0 w-full">
{transactions.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12">
@@ -323,8 +323,8 @@ export function TransactionTable({
className={cn(
"font-semibold tabular-nums text-base md:text-base shrink-0",
transaction.amount >= 0
? "text-emerald-600"
: "text-red-600"
? "text-success"
: "text-destructive"
)}
>
{transaction.amount >= 0 ? "+" : ""}
@@ -398,7 +398,7 @@ export function TransactionTable({
onDelete(transaction.id);
}
}}
className="text-red-600 focus:text-red-600"
className="text-destructive focus:text-destructive"
>
<Trash2 className="w-4 h-4 mr-2" />
Supprimer
@@ -576,8 +576,8 @@ export function TransactionTable({
className={cn(
"p-3 text-right font-semibold tabular-nums",
transaction.amount >= 0
? "text-emerald-600"
: "text-red-600"
? "text-success"
: "text-destructive"
)}
>
{transaction.amount >= 0 ? "+" : ""}
@@ -592,7 +592,7 @@ export function TransactionTable({
className="p-1 hover:bg-muted rounded"
>
{transaction.isReconciled ? (
<CheckCircle2 className="w-5 h-5 text-emerald-600" />
<CheckCircle2 className="w-5 h-5 text-success" />
) : (
<Circle className="w-5 h-5 text-muted-foreground" />
)}
@@ -650,7 +650,7 @@ export function TransactionTable({
onDelete(transaction.id);
}
}}
className="text-red-600 focus:text-red-600"
className="text-destructive focus:text-destructive"
>
<Trash2 className="w-4 h-4 mr-2" />
Supprimer

View File

@@ -5,7 +5,7 @@ import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const badgeVariants = cva(
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/30 aria-invalid:border-destructive overflow-hidden",
{
variants: {
variant: {
@@ -14,7 +14,7 @@ const badgeVariants = cva(
secondary:
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
destructive:
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
"border-transparent bg-destructive text-destructive-foreground [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/30",
outline:
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
},
@@ -22,7 +22,7 @@ const badgeVariants = cva(
defaultVariants: {
variant: "default",
},
},
}
);
function Badge({

View File

@@ -5,16 +5,16 @@ import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-xl text-sm font-semibold 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:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-xl text-sm font-semibold 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:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/30 aria-invalid:border-destructive",
{
variants: {
variant: {
default:
"bg-gradient-to-r from-primary via-primary/95 to-primary/90 text-primary-foreground shadow-xl shadow-primary/30 backdrop-blur-sm border border-primary/20",
destructive:
"bg-gradient-to-r from-destructive via-destructive/95 to-destructive/90 text-white shadow-xl shadow-destructive/30 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 backdrop-blur-sm border border-destructive/20",
"bg-gradient-to-r from-destructive via-destructive/95 to-destructive/90 text-destructive-foreground shadow-xl shadow-destructive/30 focus-visible:ring-destructive/30 backdrop-blur-sm border border-destructive/20",
outline:
"border-2 bg-background/95 backdrop-blur-md shadow-md dark:bg-input/40 dark:border-input",
"border-2 bg-background/95 backdrop-blur-md shadow-md border-border",
secondary:
"bg-gradient-to-r from-secondary via-secondary/95 to-secondary/90 text-secondary-foreground backdrop-blur-sm",
ghost: "backdrop-blur-sm",

View File

@@ -201,7 +201,7 @@ function CalendarDayButton({
data-range-end={modifiers.range_end}
data-range-middle={modifiers.range_middle}
className={cn(
"data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 dark:hover:text-accent-foreground flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md [&>span]:text-xs [&>span]:opacity-70",
"data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 hover:text-accent-foreground flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md [&>span]:text-xs [&>span]:opacity-70",
defaultClassNames.day,
className,
)}

View File

@@ -14,8 +14,8 @@ function Checkbox({
<CheckboxPrimitive.Root
data-slot="checkbox"
className={cn(
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className,
"peer border-input bg-input data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/30 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>

View File

@@ -126,7 +126,7 @@ function ContextMenuItem({
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/15 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}

View File

@@ -39,7 +39,7 @@ function DialogOverlay({
data-slot="dialog-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className,
className
)}
{...props}
/>
@@ -60,8 +60,8 @@ function DialogContent({
<DialogPrimitive.Content
data-slot="dialog-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 flex flex-col w-full max-w-[calc(100%-2rem)] max-h-[calc(100vh-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 overflow-y-auto sm:max-w-lg",
className,
"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 fixed top-[50%] left-[50%] z-50 flex flex-col w-full max-w-[calc(100%-2rem)] max-h-[calc(100vh-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 overflow-y-auto sm:max-w-lg",
className
)}
{...props}
>
@@ -96,7 +96,7 @@ function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
data-slot="dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className,
className
)}
{...props}
/>

View File

@@ -76,7 +76,7 @@ function DropdownMenuItem({
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/15 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}

View File

@@ -117,7 +117,7 @@ function FieldLabel({
className={cn(
"group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50",
"has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&>*]:data-[slot=field]:p-4",
"has-data-[state=checked]:bg-primary/5 has-data-[state=checked]:border-primary dark:has-data-[state=checked]:bg-primary/10",
"has-data-[state=checked]:bg-primary/8 has-data-[state=checked]:border-primary",
className,
)}
{...props}

View File

@@ -13,7 +13,7 @@ function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
data-slot="input-group"
role="group"
className={cn(
"group/input-group border-input dark:bg-input/30 relative flex w-full items-center rounded-md border shadow-xs transition-[color,box-shadow] outline-none",
"group/input-group border-input bg-input relative flex w-full items-center rounded-md border shadow-xs outline-none",
"h-9 has-[>textarea]:h-auto",
// Variants based on alignment.
@@ -26,7 +26,7 @@ function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
"has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot=input-group-control]:focus-visible]:ring-[3px]",
// Error state.
"has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40",
"has-[[data-slot][aria-invalid=true]]:ring-destructive/30 has-[[data-slot][aria-invalid=true]]:border-destructive",
className,
)}
@@ -135,7 +135,7 @@ function InputGroupInput({
<Input
data-slot="input-group-control"
className={cn(
"flex-1 rounded-none border-0 bg-transparent shadow-none focus-visible:ring-0 dark:bg-transparent",
"flex-1 rounded-none border-0 bg-transparent shadow-none focus-visible:ring-0",
className,
)}
{...props}
@@ -151,7 +151,7 @@ function InputGroupTextarea({
<Textarea
data-slot="input-group-control"
className={cn(
"flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none focus-visible:ring-0 dark:bg-transparent",
"flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none focus-visible:ring-0",
className,
)}
{...props}

View File

@@ -51,7 +51,7 @@ function InputOTPSlot({
data-slot="input-otp-slot"
data-active={isActive}
className={cn(
"data-[active=true]:border-ring data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[active=true]:aria-invalid:border-destructive dark:bg-input/30 border-input relative flex h-9 w-9 items-center justify-center border-y border-r text-sm shadow-xs transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md data-[active=true]:z-10 data-[active=true]:ring-[3px]",
"data-[active=true]:border-ring data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:ring-destructive/30 aria-invalid:border-destructive data-[active=true]:aria-invalid:border-destructive bg-input border-input relative flex h-9 w-9 items-center justify-center border-y border-r text-sm shadow-xs outline-none first:rounded-l-md first:border-l last:rounded-r-md data-[active=true]:z-10 data-[active=true]:ring-[3px]",
className,
)}
{...props}

View File

@@ -9,14 +9,14 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
data-slot="input"
className={cn(
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground",
"dark:bg-input/40 border-input h-9 w-full min-w-0 rounded-lg border bg-background/50 backdrop-blur-sm px-3 py-1 text-base",
"shadow-sm transition-all duration-200 outline-none",
"bg-input border-border h-9 w-full min-w-0 rounded-lg border backdrop-blur-sm px-3 py-1 text-base",
"shadow-sm 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 md:text-sm",
"focus-visible:border-primary/50 focus-visible:ring-primary/20 focus-visible:ring-[3px] focus-visible:shadow-md focus-visible:shadow-primary/10",
"hover:border-primary/30 hover:shadow-sm",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className,
"aria-invalid:ring-destructive/30 aria-invalid:border-destructive",
className
)}
{...props}
/>

View File

@@ -7,8 +7,8 @@ function Kbd({ className, ...props }: React.ComponentProps<"kbd">) {
className={cn(
"bg-muted w-fit text-muted-foreground pointer-events-none inline-flex h-5 min-w-5 items-center justify-center gap-1 rounded-sm px-1 font-sans text-xs font-medium select-none",
"[&_svg:not([class*='size-'])]:size-3",
"[[data-slot=tooltip-content]_&]:bg-background/20 [[data-slot=tooltip-content]_&]:text-background dark:[[data-slot=tooltip-content]_&]:bg-background/10",
className,
"[[data-slot=tooltip-content]_&]:bg-background/15 [[data-slot=tooltip-content]_&]:text-background",
className
)}
{...props}
/>

View File

@@ -103,7 +103,7 @@ function MenubarItem({
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/15 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}

View File

@@ -27,7 +27,7 @@ function RadioGroupItem({
<RadioGroupPrimitive.Item
data-slot="radio-group-item"
className={cn(
"border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
"border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/30 aria-invalid:border-destructive bg-input aspect-square size-4 shrink-0 rounded-full border shadow-xs outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}

View File

@@ -37,7 +37,7 @@ function SelectTrigger({
data-slot="select-trigger"
data-size={size}
className={cn(
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]: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 dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/30 aria-invalid:border-destructive bg-input hover:bg-input/80 flex w-fit items-center justify-between gap-2 rounded-md border px-3 py-2 text-sm whitespace-nowrap shadow-xs outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}

View File

@@ -13,7 +13,7 @@ function Switch({
<SwitchPrimitive.Root
data-slot="switch"
className={cn(
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
@@ -21,7 +21,7 @@ function Switch({
<SwitchPrimitive.Thumb
data-slot="switch-thumb"
className={
"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0"
"bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0"
}
/>
</SwitchPrimitive.Root>

View File

@@ -42,7 +42,7 @@ function TabsTrigger({
<TabsPrimitive.Trigger
data-slot="tabs-trigger"
className={cn(
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"data-[state=active]:bg-background data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring data-[state=active]:border-input text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap 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}

View File

@@ -8,11 +8,11 @@ function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
data-slot="textarea"
className={cn(
"border-input placeholder:text-muted-foreground",
"dark:bg-input/40 flex field-sizing-content min-h-16 w-full rounded-lg border bg-background/50 backdrop-blur-sm px-3 py-2 text-base",
"shadow-sm transition-all duration-200 outline-none",
"bg-input flex field-sizing-content min-h-16 w-full rounded-lg border backdrop-blur-sm px-3 py-2 text-base",
"shadow-sm outline-none",
"focus-visible:border-primary/50 focus-visible:ring-primary/20 focus-visible:ring-[3px] focus-visible:shadow-md focus-visible:shadow-primary/10",
"hover:border-primary/30 hover:shadow-sm",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
"aria-invalid:ring-destructive/30 aria-invalid:border-destructive",
"disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className,
)}

View File

@@ -7,7 +7,7 @@ import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const toggleVariants = cva(
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap",
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none aria-invalid:ring-destructive/30 aria-invalid:border-destructive whitespace-nowrap",
{
variants: {
variant: {

View File

@@ -164,7 +164,7 @@ export function useTransactionsChartData({
);
return monthlyChartData;
}, [transactionsData]);
}, [transactionsData, metadata]);
// Calculate category chart data (expenses only)
const categoryData = useMemo(() => {

View File

@@ -213,7 +213,7 @@ export function useTransactionsPage() {
} else {
setIsCustomDatePickerOpen(true);
}
}, []);
}, [setPeriod]);
const handleCustomStartDateChange = useCallback((date: Date | undefined) => {
setCustomStartDate(date);

View File

@@ -1,125 +0,0 @@
@import "tailwindcss";
@import "tw-animate-css";
@custom-variant dark (&:is(.dark *));
:root {
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--destructive-foreground: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--radius: 0.625rem;
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.145 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.145 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.985 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.396 0.141 25.723);
--destructive-foreground: oklch(0.637 0.237 25.331);
--border: oklch(0.269 0 0);
--input: oklch(0.269 0 0);
--ring: oklch(0.439 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(0.269 0 0);
--sidebar-ring: oklch(0.439 0 0);
}
@theme inline {
--font-sans: "Geist", "Geist Fallback";
--font-mono: "Geist Mono", "Geist Mono Fallback";
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}