feat: topbar de navegação como experimento de UI (v1.7.0)
- Substitui header fixo por topbar com backdrop blur e navegação agrupada em 5 seções - Adiciona FerramentasDropdown consolidando calculadora e modo privacidade - NotificationBell expandida com orçamentos e pré-lançamentos - Remove logout-button, header-dashboard e privacy-mode-toggle como componentes separados - Logo refatorado com variante compact; topbar com links em lowercase - Adiciona dependência radix-ui ^1.4.3 - Atualiza CHANGELOG para v1.7.0 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
21
CHANGELOG.md
21
CHANGELOG.md
@@ -5,6 +5,27 @@ Todas as mudanças notáveis deste projeto serão documentadas neste arquivo.
|
|||||||
O formato é baseado em [Keep a Changelog](https://keepachangelog.com/pt-BR/1.1.0/),
|
O formato é baseado em [Keep a Changelog](https://keepachangelog.com/pt-BR/1.1.0/),
|
||||||
e este projeto adere ao [Versionamento Semântico](https://semver.org/lang/pt-BR/).
|
e este projeto adere ao [Versionamento Semântico](https://semver.org/lang/pt-BR/).
|
||||||
|
|
||||||
|
## [1.7.0] - 2026-02-24
|
||||||
|
|
||||||
|
### Adicionado
|
||||||
|
|
||||||
|
- **Topbar de navegação** como experimento: substitui o header fixo por uma topbar com backdrop blur (`bg-card/80`), agrupando os links de navegação em 5 grupos lógicos (Dashboard, Lançamentos, Cartões, Relatórios, Ferramentas)
|
||||||
|
- **Dropdown "Ferramentas"** na topbar: consolida calculadora e modo privacidade em um único menu (`components/topbar/ferramentas-dropdown.tsx`)
|
||||||
|
- **NotificationBell expandida**: passa a exibir notificações de orçamentos estourados e pré-lançamentos pendentes além das notificações gerais, com seções separadas por tipo e contagem agregada
|
||||||
|
|
||||||
|
### Alterado
|
||||||
|
|
||||||
|
- **Logo** refatorado com variante `compact` usada na topbar
|
||||||
|
- **TopbarUser**: incorpora o botão de logout (antes em `logout-button.tsx` separado)
|
||||||
|
- **Topbar**: links em lowercase; layout centralizado em `max-w-8xl`; `radix-ui ^1.4.3` adicionado como dependência
|
||||||
|
- **Gráfico de relatório de categorias** (`category-report-chart`): refatoração interna
|
||||||
|
|
||||||
|
### Removido
|
||||||
|
|
||||||
|
- `components/header-dashboard.tsx` — substituído pela topbar
|
||||||
|
- `components/auth/logout-button.tsx` — lógica incorporada ao `TopbarUser`
|
||||||
|
- `components/privacy-mode-toggle.tsx` — movido para o `FerramentasDropdown`
|
||||||
|
|
||||||
## [1.6.3] - 2026-02-19
|
## [1.6.3] - 2026-02-19
|
||||||
|
|
||||||
### Corrigido
|
### Corrigido
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export default function RootLayout({
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<section className="space-y-6 px-6">
|
<section className="space-y-6 pt-4">
|
||||||
<PageDescription
|
<PageDescription
|
||||||
icon={<RiSettings2Line />}
|
icon={<RiSettings2Line />}
|
||||||
title="Ajustes"
|
title="Ajustes"
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export default function RootLayout({
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<section className="space-y-6 px-6">
|
<section className="space-y-6 pt-4">
|
||||||
<PageDescription
|
<PageDescription
|
||||||
icon={<RiTodoLine />}
|
icon={<RiTodoLine />}
|
||||||
title="Notas"
|
title="Notas"
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export default function RootLayout({
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<section className="space-y-6 px-6">
|
<section className="space-y-6 pt-4">
|
||||||
<PageDescription
|
<PageDescription
|
||||||
icon={<RiCalendarEventLine />}
|
icon={<RiCalendarEventLine />}
|
||||||
title="Calendário"
|
title="Calendário"
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export default function FaturaLoading() {
|
|||||||
|
|
||||||
{/* Seção de lançamentos */}
|
{/* Seção de lançamentos */}
|
||||||
<section className="flex flex-col gap-4">
|
<section className="flex flex-col gap-4">
|
||||||
<div className="space-y-6">
|
<div className="space-y-6 pt-4">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<Skeleton className="h-8 w-48 rounded-2xl bg-foreground/10" />
|
<Skeleton className="h-8 w-48 rounded-2xl bg-foreground/10" />
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export default function RootLayout({
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<section className="space-y-6 px-6">
|
<section className="space-y-6 pt-4">
|
||||||
<PageDescription
|
<PageDescription
|
||||||
icon={<RiBankCard2Line />}
|
icon={<RiBankCard2Line />}
|
||||||
title="Cartões"
|
title="Cartões"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Skeleton } from "@/components/ui/skeleton";
|
|||||||
export default function CartoesLoading() {
|
export default function CartoesLoading() {
|
||||||
return (
|
return (
|
||||||
<main className="flex flex-col gap-6">
|
<main className="flex flex-col gap-6">
|
||||||
<div className="space-y-6">
|
<div className="space-y-6 pt-4">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<Skeleton className="h-8 w-32 rounded-2xl bg-foreground/10" />
|
<Skeleton className="h-8 w-32 rounded-2xl bg-foreground/10" />
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export default function RootLayout({
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<section className="space-y-6 px-6">
|
<section className="space-y-6 pt-4">
|
||||||
<PageDescription
|
<PageDescription
|
||||||
icon={<RiPriceTag3Line />}
|
icon={<RiPriceTag3Line />}
|
||||||
title="Categorias"
|
title="Categorias"
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export default function ExtratoLoading() {
|
|||||||
|
|
||||||
{/* Seção de lançamentos */}
|
{/* Seção de lançamentos */}
|
||||||
<section className="flex flex-col gap-4">
|
<section className="flex flex-col gap-4">
|
||||||
<div className="space-y-6">
|
<div className="space-y-6 pt-4">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<Skeleton className="h-8 w-48 rounded-2xl bg-foreground/10" />
|
<Skeleton className="h-8 w-48 rounded-2xl bg-foreground/10" />
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export default function RootLayout({
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<section className="space-y-6 px-6">
|
<section className="space-y-6 pt-4">
|
||||||
<PageDescription
|
<PageDescription
|
||||||
icon={<RiBankLine />}
|
icon={<RiBankLine />}
|
||||||
title="Contas"
|
title="Contas"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Skeleton } from "@/components/ui/skeleton";
|
|||||||
export default function ContasLoading() {
|
export default function ContasLoading() {
|
||||||
return (
|
return (
|
||||||
<main className="flex flex-col gap-6">
|
<main className="flex flex-col gap-6">
|
||||||
<div className="space-y-6">
|
<div className="space-y-6 pt-4">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<Skeleton className="h-8 w-32 rounded-2xl bg-foreground/10" />
|
<Skeleton className="h-8 w-32 rounded-2xl bg-foreground/10" />
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export default function RootLayout({
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<section className="space-y-6 px-6">
|
<section className="space-y-6 pt-4">
|
||||||
<PageDescription
|
<PageDescription
|
||||||
icon={<RiSecurePaymentLine />}
|
icon={<RiSecurePaymentLine />}
|
||||||
title="Análise de Parcelas"
|
title="Análise de Parcelas"
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export default async function Page({ searchParams }: PageProps) {
|
|||||||
const { disableMagnetlines, dashboardWidgets } = preferences;
|
const { disableMagnetlines, dashboardWidgets } = preferences;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="flex flex-col gap-4 px-6">
|
<main className="flex flex-col gap-4">
|
||||||
<DashboardWelcome
|
<DashboardWelcome
|
||||||
name={user.name}
|
name={user.name}
|
||||||
disableMagnetlines={disableMagnetlines}
|
disableMagnetlines={disableMagnetlines}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export default function RootLayout({
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<section className="space-y-6 px-6">
|
<section className="space-y-6 pt-4">
|
||||||
<PageDescription
|
<PageDescription
|
||||||
icon={<RiSparklingLine />}
|
icon={<RiSparklingLine />}
|
||||||
title="Insights"
|
title="Insights"
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { Skeleton } from "@/components/ui/skeleton";
|
|||||||
export default function InsightsLoading() {
|
export default function InsightsLoading() {
|
||||||
return (
|
return (
|
||||||
<main className="flex flex-col gap-6">
|
<main className="flex flex-col gap-6">
|
||||||
<div className="space-y-6">
|
<div className="space-y-6 pt-4">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Skeleton className="h-10 w-64 rounded-2xl bg-foreground/10" />
|
<Skeleton className="h-10 w-64 rounded-2xl bg-foreground/10" />
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export default function RootLayout({
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<section className="space-y-6 px-6">
|
<section className="space-y-6 pt-4">
|
||||||
<PageDescription
|
<PageDescription
|
||||||
icon={<RiArrowLeftRightLine />}
|
icon={<RiArrowLeftRightLine />}
|
||||||
title="Lançamentos"
|
title="Lançamentos"
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export default function LancamentosLoading() {
|
|||||||
{/* Month Picker placeholder */}
|
{/* Month Picker placeholder */}
|
||||||
<div className="h-[60px] animate-pulse rounded-2xl bg-foreground/10" />
|
<div className="h-[60px] animate-pulse rounded-2xl bg-foreground/10" />
|
||||||
|
|
||||||
<div className="space-y-6">
|
<div className="space-y-6 pt-4">
|
||||||
{/* Header com título e botão */}
|
{/* Header com título e botão */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<Skeleton className="h-8 w-48 rounded-2xl bg-foreground/10" />
|
<Skeleton className="h-8 w-48 rounded-2xl bg-foreground/10" />
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ export default async function DashboardLayout({
|
|||||||
/>
|
/>
|
||||||
<div className="flex flex-1 flex-col pt-14">
|
<div className="flex flex-1 flex-col pt-14">
|
||||||
<div className="@container/main flex flex-1 flex-col gap-2">
|
<div className="@container/main flex flex-1 flex-col gap-2">
|
||||||
<div className="flex flex-col gap-4 py-4 md:gap-6 w-full max-w-8xl mx-auto px-4">
|
<div className="flex flex-col gap-4 py-5 md:gap-6 w-full max-w-8xl mx-auto px-4">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export default function RootLayout({
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<section className="space-y-6 px-6">
|
<section className="space-y-6 pt-4">
|
||||||
<PageDescription
|
<PageDescription
|
||||||
icon={<RiFundsLine />}
|
icon={<RiFundsLine />}
|
||||||
title="Orçamentos"
|
title="Orçamentos"
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export default function OrcamentosLoading() {
|
|||||||
{/* Month Picker placeholder */}
|
{/* Month Picker placeholder */}
|
||||||
<div className="h-[60px] animate-pulse rounded-2xl bg-foreground/10" />
|
<div className="h-[60px] animate-pulse rounded-2xl bg-foreground/10" />
|
||||||
|
|
||||||
<div className="space-y-6">
|
<div className="space-y-6 pt-4">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export default function PagadorDetailsLoading() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tabs */}
|
{/* Tabs */}
|
||||||
<div className="space-y-6">
|
<div className="space-y-6 pt-4">
|
||||||
<div className="flex gap-2 border-b">
|
<div className="flex gap-2 border-b">
|
||||||
<Skeleton className="h-10 w-32 rounded-t-2xl bg-foreground/10" />
|
<Skeleton className="h-10 w-32 rounded-t-2xl bg-foreground/10" />
|
||||||
<Skeleton className="h-10 w-32 rounded-t-2xl bg-foreground/10" />
|
<Skeleton className="h-10 w-32 rounded-t-2xl bg-foreground/10" />
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export default function RootLayout({
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<section className="space-y-6 px-6">
|
<section className="space-y-6 pt-4">
|
||||||
<PageDescription
|
<PageDescription
|
||||||
icon={<RiGroupLine />}
|
icon={<RiGroupLine />}
|
||||||
title="Pagadores"
|
title="Pagadores"
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export default function RootLayout({
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<section className="space-y-6 px-6">
|
<section className="space-y-6 pt-4">
|
||||||
<PageDescription
|
<PageDescription
|
||||||
icon={<RiInboxLine />}
|
icon={<RiInboxLine />}
|
||||||
title="Pré-Lançamentos"
|
title="Pré-Lançamentos"
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export default function RootLayout({
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<section className="space-y-6 px-6">
|
<section className="space-y-6 pt-4">
|
||||||
<PageDescription
|
<PageDescription
|
||||||
icon={<RiFileChartLine />}
|
icon={<RiFileChartLine />}
|
||||||
title="Tendências"
|
title="Tendências"
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export default function RootLayout({
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<section className="space-y-6 px-6">
|
<section className="space-y-6 pt-4">
|
||||||
<PageDescription
|
<PageDescription
|
||||||
icon={<RiBankCard2Line />}
|
icon={<RiBankCard2Line />}
|
||||||
title="Uso de Cartões"
|
title="Uso de Cartões"
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export default function RootLayout({
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<section className="space-y-6 px-6">
|
<section className="space-y-6 pt-4">
|
||||||
<PageDescription
|
<PageDescription
|
||||||
icon={<RiStore2Line />}
|
icon={<RiStore2Line />}
|
||||||
title="Top Estabelecimentos"
|
title="Top Estabelecimentos"
|
||||||
|
|||||||
@@ -155,40 +155,38 @@ export default async function Page() {
|
|||||||
<div className="flex min-h-screen flex-col">
|
<div className="flex min-h-screen flex-col">
|
||||||
{/* Navigation */}
|
{/* Navigation */}
|
||||||
<header className="sticky top-0 z-50 bg-background/95 backdrop-blur supports-backdrop-filter:bg-background/60">
|
<header className="sticky top-0 z-50 bg-background/95 backdrop-blur supports-backdrop-filter:bg-background/60">
|
||||||
<div className="container flex h-16 items-center justify-between">
|
<div className="max-w-8xl mx-auto px-4 flex h-14 items-center justify-between">
|
||||||
<div className="flex items-center">
|
<Logo variant="compact" />
|
||||||
<Logo />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Center Navigation Links */}
|
{/* Center Navigation Links */}
|
||||||
<nav className="hidden md:flex items-center gap-6 absolute left-1/2 transform -translate-x-1/2">
|
<nav className="hidden md:flex items-center gap-6 absolute left-1/2 transform -translate-x-1/2">
|
||||||
<a
|
<a
|
||||||
href="#telas"
|
href="#telas"
|
||||||
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
className="rounded-full px-3 py-1.5 text-sm font-medium text-muted-foreground hover:text-foreground hover:bg-accent transition-colors"
|
||||||
>
|
>
|
||||||
Conheça as telas
|
Conheça as telas
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href="#funcionalidades"
|
href="#funcionalidades"
|
||||||
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
className="rounded-full px-3 py-1.5 text-sm font-medium text-muted-foreground hover:text-foreground hover:bg-accent transition-colors"
|
||||||
>
|
>
|
||||||
Funcionalidades
|
Funcionalidades
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href="#companion"
|
href="#companion"
|
||||||
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
className="rounded-full px-3 py-1.5 text-sm font-medium text-muted-foreground hover:text-foreground hover:bg-accent transition-colors"
|
||||||
>
|
>
|
||||||
Companion
|
Companion
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href="#stack"
|
href="#stack"
|
||||||
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
className="rounded-full px-3 py-1.5 text-sm font-medium text-muted-foreground hover:text-foreground hover:bg-accent transition-colors"
|
||||||
>
|
>
|
||||||
Stack
|
Stack
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href="#como-usar"
|
href="#como-usar"
|
||||||
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
className="rounded-full px-3 py-1.5 text-sm font-medium text-muted-foreground hover:text-foreground hover:bg-accent transition-colors"
|
||||||
>
|
>
|
||||||
Como usar
|
Como usar
|
||||||
</a>
|
</a>
|
||||||
@@ -231,7 +229,7 @@ export default async function Page() {
|
|||||||
{/* Background gradient */}
|
{/* Background gradient */}
|
||||||
<div className="pointer-events-none absolute inset-0 bg-linear-to-b from-primary/5 via-transparent to-transparent" />
|
<div className="pointer-events-none absolute inset-0 bg-linear-to-b from-primary/5 via-transparent to-transparent" />
|
||||||
|
|
||||||
<div className="container relative">
|
<div className="max-w-8xl mx-auto px-4 relative">
|
||||||
<div className="mx-auto flex max-w-5xl flex-col items-center text-center gap-5 md:gap-6">
|
<div className="mx-auto flex max-w-5xl flex-col items-center text-center gap-5 md:gap-6">
|
||||||
<Logo variant="small" className="h-12 w-12 mb-1" />
|
<Logo variant="small" className="h-12 w-12 mb-1" />
|
||||||
|
|
||||||
@@ -304,7 +302,7 @@ export default async function Page() {
|
|||||||
|
|
||||||
{/* Metrics Bar */}
|
{/* Metrics Bar */}
|
||||||
<section className="py-8 md:py-12 border-y">
|
<section className="py-8 md:py-12 border-y">
|
||||||
<div className="container">
|
<div className="max-w-8xl mx-auto px-4">
|
||||||
<div className="mx-auto max-w-4xl">
|
<div className="mx-auto max-w-4xl">
|
||||||
<div className="grid grid-cols-2 gap-4 md:grid-cols-4 md:gap-8">
|
<div className="grid grid-cols-2 gap-4 md:grid-cols-4 md:gap-8">
|
||||||
<div className="flex flex-col items-center text-center gap-1.5">
|
<div className="flex flex-col items-center text-center gap-1.5">
|
||||||
@@ -350,7 +348,7 @@ export default async function Page() {
|
|||||||
|
|
||||||
{/* Dashboard Preview Section */}
|
{/* Dashboard Preview Section */}
|
||||||
<section className="py-6 md:py-16">
|
<section className="py-6 md:py-16">
|
||||||
<div className="container">
|
<div className="max-w-8xl mx-auto px-4">
|
||||||
<div className="mx-auto max-w-6xl">
|
<div className="mx-auto max-w-6xl">
|
||||||
<AnimateOnScroll>
|
<AnimateOnScroll>
|
||||||
<div>
|
<div>
|
||||||
@@ -378,7 +376,7 @@ export default async function Page() {
|
|||||||
|
|
||||||
{/* Screenshots Gallery Section */}
|
{/* Screenshots Gallery Section */}
|
||||||
<section id="telas" className="py-12 md:py-24">
|
<section id="telas" className="py-12 md:py-24">
|
||||||
<div className="container">
|
<div className="max-w-8xl mx-auto px-4">
|
||||||
<div className="mx-auto max-w-6xl">
|
<div className="mx-auto max-w-6xl">
|
||||||
<AnimateOnScroll>
|
<AnimateOnScroll>
|
||||||
<div className="text-center mb-8 md:mb-12">
|
<div className="text-center mb-8 md:mb-12">
|
||||||
@@ -430,7 +428,7 @@ export default async function Page() {
|
|||||||
|
|
||||||
{/* Features Section */}
|
{/* Features Section */}
|
||||||
<section id="funcionalidades" className="py-12 md:py-24">
|
<section id="funcionalidades" className="py-12 md:py-24">
|
||||||
<div className="container">
|
<div className="max-w-8xl mx-auto px-4">
|
||||||
<div className="mx-auto max-w-5xl">
|
<div className="mx-auto max-w-5xl">
|
||||||
<AnimateOnScroll>
|
<AnimateOnScroll>
|
||||||
<div className="text-center mb-8 md:mb-12">
|
<div className="text-center mb-8 md:mb-12">
|
||||||
@@ -512,7 +510,7 @@ export default async function Page() {
|
|||||||
|
|
||||||
{/* Companion Section */}
|
{/* Companion Section */}
|
||||||
<section id="companion" className="py-12 md:py-24">
|
<section id="companion" className="py-12 md:py-24">
|
||||||
<div className="container">
|
<div className="max-w-8xl mx-auto px-4">
|
||||||
<div className="mx-auto max-w-5xl">
|
<div className="mx-auto max-w-5xl">
|
||||||
<AnimateOnScroll>
|
<AnimateOnScroll>
|
||||||
<div className="grid gap-8 md:gap-12 md:grid-cols-2 items-center">
|
<div className="grid gap-8 md:gap-12 md:grid-cols-2 items-center">
|
||||||
@@ -625,7 +623,7 @@ export default async function Page() {
|
|||||||
|
|
||||||
{/* Tech Stack Section */}
|
{/* Tech Stack Section */}
|
||||||
<section id="stack" className="py-12 md:py-24">
|
<section id="stack" className="py-12 md:py-24">
|
||||||
<div className="container">
|
<div className="max-w-8xl mx-auto px-4">
|
||||||
<div className="mx-auto max-w-5xl">
|
<div className="mx-auto max-w-5xl">
|
||||||
<AnimateOnScroll>
|
<AnimateOnScroll>
|
||||||
<div className="text-center mb-8 md:mb-12">
|
<div className="text-center mb-8 md:mb-12">
|
||||||
@@ -748,7 +746,7 @@ export default async function Page() {
|
|||||||
|
|
||||||
{/* How to run Section */}
|
{/* How to run Section */}
|
||||||
<section id="como-usar" className="py-12 md:py-24">
|
<section id="como-usar" className="py-12 md:py-24">
|
||||||
<div className="container">
|
<div className="max-w-8xl mx-auto px-4">
|
||||||
<div className="mx-auto max-w-3xl">
|
<div className="mx-auto max-w-3xl">
|
||||||
<AnimateOnScroll>
|
<AnimateOnScroll>
|
||||||
<div className="text-center mb-8 md:mb-12">
|
<div className="text-center mb-8 md:mb-12">
|
||||||
@@ -783,7 +781,7 @@ export default async function Page() {
|
|||||||
|
|
||||||
{/* Who is this for Section */}
|
{/* Who is this for Section */}
|
||||||
<section className="py-12 md:py-24">
|
<section className="py-12 md:py-24">
|
||||||
<div className="container">
|
<div className="max-w-8xl mx-auto px-4">
|
||||||
<div className="mx-auto max-w-3xl">
|
<div className="mx-auto max-w-3xl">
|
||||||
<AnimateOnScroll>
|
<AnimateOnScroll>
|
||||||
<div className="text-center mb-8 md:mb-12">
|
<div className="text-center mb-8 md:mb-12">
|
||||||
@@ -882,7 +880,7 @@ export default async function Page() {
|
|||||||
|
|
||||||
{/* CTA Section */}
|
{/* CTA Section */}
|
||||||
<section className="py-12 md:py-24">
|
<section className="py-12 md:py-24">
|
||||||
<div className="container">
|
<div className="max-w-8xl mx-auto px-4">
|
||||||
<AnimateOnScroll>
|
<AnimateOnScroll>
|
||||||
<div className="mx-auto max-w-3xl text-center px-4 sm:px-0">
|
<div className="mx-auto max-w-3xl text-center px-4 sm:px-0">
|
||||||
<h2 className="text-2xl sm:text-3xl md:text-4xl font-bold tracking-tight mb-3 md:mb-4">
|
<h2 className="text-2xl sm:text-3xl md:text-4xl font-bold tracking-tight mb-3 md:mb-4">
|
||||||
@@ -924,11 +922,11 @@ export default async function Page() {
|
|||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<footer className="border-t py-8 md:py-12 mt-auto">
|
<footer className="border-t py-8 md:py-12 mt-auto">
|
||||||
<div className="container">
|
<div className="max-w-8xl mx-auto px-4">
|
||||||
<div className="mx-auto max-w-5xl">
|
<div className="mx-auto max-w-5xl">
|
||||||
<div className="grid gap-8 sm:grid-cols-2 md:grid-cols-3">
|
<div className="grid gap-8 sm:grid-cols-2 md:grid-cols-3">
|
||||||
<div className="sm:col-span-2 md:col-span-1">
|
<div className="sm:col-span-2 md:col-span-1">
|
||||||
<Logo />
|
<Logo variant="compact" />
|
||||||
<p className="text-sm text-muted-foreground mt-3 md:mt-4">
|
<p className="text-sm text-muted-foreground mt-3 md:mt-4">
|
||||||
Projeto pessoal de gestão financeira. Open source e
|
Projeto pessoal de gestão financeira. Open source e
|
||||||
self-hosted.
|
self-hosted.
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export default function RootLayout({
|
|||||||
<head>
|
<head>
|
||||||
<meta name="apple-mobile-web-app-title" content="OpenMonetis" />
|
<meta name="apple-mobile-web-app-title" content="OpenMonetis" />
|
||||||
</head>
|
</head>
|
||||||
<body className="antialiased" suppressHydrationWarning>
|
<body className="subpixel-antialiased" suppressHydrationWarning>
|
||||||
<ThemeProvider attribute="class" defaultTheme="light">
|
<ThemeProvider attribute="class" defaultTheme="light">
|
||||||
{children}
|
{children}
|
||||||
<Toaster position="top-right" />
|
<Toaster position="top-right" />
|
||||||
|
|||||||
@@ -1,55 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
TooltipTrigger,
|
|
||||||
} from "@/components/ui/tooltip";
|
|
||||||
import { authClient } from "@/lib/auth/client";
|
|
||||||
import { Spinner } from "../ui/spinner";
|
|
||||||
|
|
||||||
export default function LogoutButton() {
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
|
|
||||||
async function handleLogOut() {
|
|
||||||
await authClient.signOut({
|
|
||||||
fetchOptions: {
|
|
||||||
onSuccess: () => {
|
|
||||||
router.push("/login");
|
|
||||||
},
|
|
||||||
onRequest: (_ctx) => {
|
|
||||||
setLoading(true);
|
|
||||||
},
|
|
||||||
onResponse: (_ctx) => {
|
|
||||||
setLoading(false);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="link"
|
|
||||||
size="sm"
|
|
||||||
aria-busy={loading}
|
|
||||||
data-loading={loading}
|
|
||||||
onClick={handleLogOut}
|
|
||||||
disabled={loading}
|
|
||||||
className="text-destructive transition-all duration-200 border hover:text-destructive focus-visible:ring-destructive/30 data-[loading=true]:opacity-90"
|
|
||||||
>
|
|
||||||
{loading && <Spinner className="size-3.5 text-destructive" />}
|
|
||||||
<span aria-live="polite">{loading ? "Saindo" : "Sair"}</span>
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side="bottom" sideOffset={8}>
|
|
||||||
Encerrar sessão
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -31,7 +31,7 @@ type CalculatorDialogButtonProps = {
|
|||||||
onSelectValue?: (value: string) => void;
|
onSelectValue?: (value: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
function CalculatorDialogContent({
|
export function CalculatorDialogContent({
|
||||||
open,
|
open,
|
||||||
onSelectValue,
|
onSelectValue,
|
||||||
}: {
|
}: {
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ export function CardItem({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="flex p-6 h-[300px] w-[440px]">
|
<Card className="flex flex-col p-6 w-full">
|
||||||
<CardHeader className="space-y-2 px-0 pb-0">
|
<CardHeader className="space-y-2 px-0 pb-0">
|
||||||
<div className="flex items-start justify-between gap-2">
|
<div className="flex items-start justify-between gap-2">
|
||||||
<div className="flex flex-1 items-center gap-2">
|
<div className="flex flex-1 items-center gap-2">
|
||||||
@@ -209,14 +209,14 @@ export function CardItem({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{brandAsset ? (
|
{brandAsset ? (
|
||||||
<div className="flex items-center justify-center rounded-lg py-1">
|
<div className="flex items-center justify-center py-1">
|
||||||
<Image
|
<Image
|
||||||
src={brandAsset}
|
src={brandAsset}
|
||||||
alt={`Bandeira ${brand}`}
|
alt={`Bandeira ${brand}`}
|
||||||
width={42}
|
width={36}
|
||||||
height={42}
|
height={36}
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-6 w-auto rounded-full",
|
"h-5 w-auto rounded",
|
||||||
isInactive && "grayscale opacity-40",
|
isInactive && "grayscale opacity-40",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ export function CardsPage({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-wrap gap-4">
|
<div className="grid gap-4 grid-cols-1 sm:grid-cols-2 xl:grid-cols-3">
|
||||||
{list.map((card) => (
|
{list.map((card) => (
|
||||||
<CardItem
|
<CardItem
|
||||||
key={card.id}
|
key={card.id}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { main_font } from "@/public/fonts/font_index";
|
|
||||||
import MagnetLines from "../magnet-lines";
|
import MagnetLines from "../magnet-lines";
|
||||||
import { Card } from "../ui/card";
|
import { Card } from "../ui/card";
|
||||||
|
|
||||||
@@ -54,9 +53,7 @@ export function DashboardWelcome({
|
|||||||
const greeting = getGreeting();
|
const greeting = getGreeting();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card className="relative px-6 py-12 bg-welcome-banner overflow-hidden">
|
||||||
className={`${main_font.className} relative px-6 py-12 bg-welcome-banner border-none shadow-none overflow-hidden`}
|
|
||||||
>
|
|
||||||
<div className="absolute inset-0 flex items-center justify-center opacity-20 pointer-events-none">
|
<div className="absolute inset-0 flex items-center justify-center opacity-20 pointer-events-none">
|
||||||
<MagnetLines
|
<MagnetLines
|
||||||
rows={8}
|
rows={8}
|
||||||
|
|||||||
@@ -270,7 +270,7 @@ export function InvoiceSummaryCard({
|
|||||||
alt={`Bandeira ${cardBrand}`}
|
alt={`Bandeira ${cardBrand}`}
|
||||||
width={32}
|
width={32}
|
||||||
height={32}
|
height={32}
|
||||||
className="h-5 w-auto rounded-full"
|
className="h-5 w-auto rounded"
|
||||||
/>
|
/>
|
||||||
<span className="truncate">{cardBrand}</span>
|
<span className="truncate">{cardBrand}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -64,36 +64,13 @@ const feedbackCategories = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export function FeedbackDialog() {
|
export function FeedbackDialogBody({ onClose }: { onClose?: () => void }) {
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
|
|
||||||
const handleCategoryClick = (url: string) => {
|
const handleCategoryClick = (url: string) => {
|
||||||
window.open(url, "_blank", "noopener,noreferrer");
|
window.open(url, "_blank", "noopener,noreferrer");
|
||||||
setOpen(false);
|
onClose?.();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className={cn(
|
|
||||||
buttonVariants({ variant: "ghost", size: "icon-sm" }),
|
|
||||||
"group relative text-muted-foreground transition-all duration-200",
|
|
||||||
"hover:text-foreground focus-visible:ring-2 focus-visible:ring-primary/40",
|
|
||||||
"data-[state=open]:bg-accent/60 data-[state=open]:text-foreground border",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<RiMessageLine className="h-5 w-5" />
|
|
||||||
</Button>
|
|
||||||
</DialogTrigger>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>Enviar Feedback</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<DialogContent className="sm:max-w-[500px]">
|
<DialogContent className="sm:max-w-[500px]">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="flex items-center gap-2">
|
<DialogTitle className="flex items-center gap-2">
|
||||||
@@ -149,6 +126,34 @@ export function FeedbackDialog() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FeedbackDialog() {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className={cn(
|
||||||
|
buttonVariants({ variant: "ghost", size: "icon-sm" }),
|
||||||
|
"group relative text-muted-foreground transition-all duration-200",
|
||||||
|
"hover:text-foreground focus-visible:ring-2 focus-visible:ring-primary/40",
|
||||||
|
"data-[state=open]:bg-accent/60 data-[state=open]:text-foreground border",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<RiMessageLine className="h-5 w-5" />
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>Enviar Feedback</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<FeedbackDialogBody onClose={() => setOpen(false)} />
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
import { FeedbackDialog } from "@/components/feedback/feedback-dialog";
|
|
||||||
import { NotificationBell } from "@/components/notificacoes/notification-bell";
|
|
||||||
import { SidebarTrigger } from "@/components/ui/sidebar";
|
|
||||||
import { getUser } from "@/lib/auth/server";
|
|
||||||
import type { DashboardNotificationsSnapshot } from "@/lib/dashboard/notifications";
|
|
||||||
import { AnimatedThemeToggler } from "./animated-theme-toggler";
|
|
||||||
import LogoutButton from "./auth/logout-button";
|
|
||||||
import { CalculatorDialogButton } from "./calculadora/calculator-dialog";
|
|
||||||
import { PrivacyModeToggle } from "./privacy-mode-toggle";
|
|
||||||
import { RefreshPageButton } from "./refresh-page-button";
|
|
||||||
|
|
||||||
type SiteHeaderProps = {
|
|
||||||
notificationsSnapshot: DashboardNotificationsSnapshot;
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function SiteHeader({ notificationsSnapshot }: SiteHeaderProps) {
|
|
||||||
const _user = await getUser();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<header className="fixed top-0 left-0 right-0 z-50 border-b bg-background md:static md:z-auto flex h-12 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-12">
|
|
||||||
<div className="flex w-full items-center gap-1 px-4 lg:gap-2 lg:px-6">
|
|
||||||
<SidebarTrigger className="-ml-1" />
|
|
||||||
<div className="ml-auto flex items-center gap-2">
|
|
||||||
<NotificationBell
|
|
||||||
notifications={notificationsSnapshot.notifications}
|
|
||||||
totalCount={notificationsSnapshot.totalCount}
|
|
||||||
/>
|
|
||||||
<CalculatorDialogButton withTooltip />
|
|
||||||
<RefreshPageButton />
|
|
||||||
<PrivacyModeToggle />
|
|
||||||
<AnimatedThemeToggler />
|
|
||||||
<span className="text-muted-foreground">|</span>
|
|
||||||
<FeedbackDialog />
|
|
||||||
<LogoutButton />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -46,7 +46,7 @@ export function PaymentMethodSection({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{!isUpdateMode ? (
|
{!isUpdateMode ? (
|
||||||
<div className="flex w-full flex-col gap-2 md:flex-row">
|
<div className="flex w-full flex-col gap-2 md:flex-row mt-3">
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"space-y-1 w-full",
|
"space-y-1 w-full",
|
||||||
|
|||||||
@@ -41,9 +41,9 @@ export function MobileNav({ isPublicDomain, isLoggedIn }: MobileNavProps) {
|
|||||||
|
|
||||||
<Sheet open={open} onOpenChange={setOpen}>
|
<Sheet open={open} onOpenChange={setOpen}>
|
||||||
<SheetContent side="right" className="w-72">
|
<SheetContent side="right" className="w-72">
|
||||||
<SheetHeader>
|
<SheetHeader className="border-b pb-4">
|
||||||
<SheetTitle>
|
<SheetTitle asChild>
|
||||||
<Logo />
|
<Logo variant="compact" />
|
||||||
</SheetTitle>
|
</SheetTitle>
|
||||||
</SheetHeader>
|
</SheetHeader>
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { cn } from "@/lib/utils/ui";
|
|||||||
import { version } from "@/package.json";
|
import { version } from "@/package.json";
|
||||||
|
|
||||||
interface LogoProps {
|
interface LogoProps {
|
||||||
variant?: "full" | "small";
|
variant?: "full" | "small" | "compact";
|
||||||
className?: string;
|
className?: string;
|
||||||
showVersion?: boolean;
|
showVersion?: boolean;
|
||||||
}
|
}
|
||||||
@@ -13,6 +13,29 @@ export function Logo({
|
|||||||
className,
|
className,
|
||||||
showVersion = false,
|
showVersion = false,
|
||||||
}: LogoProps) {
|
}: LogoProps) {
|
||||||
|
if (variant === "compact") {
|
||||||
|
return (
|
||||||
|
<div className={cn("flex items-center gap-1", className)}>
|
||||||
|
<Image
|
||||||
|
src="/logo_small.png"
|
||||||
|
alt="OpenMonetis"
|
||||||
|
width={32}
|
||||||
|
height={32}
|
||||||
|
className="object-contain"
|
||||||
|
priority
|
||||||
|
/>
|
||||||
|
<Image
|
||||||
|
src="/logo_text.png"
|
||||||
|
alt="OpenMonetis"
|
||||||
|
width={110}
|
||||||
|
height={32}
|
||||||
|
className="object-contain dark:invert hidden sm:block"
|
||||||
|
priority
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (variant === "small") {
|
if (variant === "small") {
|
||||||
return (
|
return (
|
||||||
<Image
|
<Image
|
||||||
@@ -45,8 +68,8 @@ export function Logo({
|
|||||||
priority
|
priority
|
||||||
/>
|
/>
|
||||||
{showVersion && (
|
{showVersion && (
|
||||||
<span className="text-[10px] font-medium text-muted-foreground">
|
<span className="text-[9px] font-medium text-muted-foreground">
|
||||||
v{version}
|
{version}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export default function MonthNavigation() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="w-full flex-row bg-card text-card-foreground p-4">
|
<Card className="w-full flex-row bg-card text-card-foreground p-4 sticky top-14 z-10">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<NavigationButton
|
<NavigationButton
|
||||||
direction="left"
|
direction="left"
|
||||||
|
|||||||
@@ -2,17 +2,23 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
RiAlertFill,
|
RiAlertFill,
|
||||||
|
RiArrowRightLine,
|
||||||
|
RiBankCardLine,
|
||||||
|
RiBarChart2Line,
|
||||||
RiCheckboxCircleFill,
|
RiCheckboxCircleFill,
|
||||||
|
RiErrorWarningLine,
|
||||||
|
RiFileListLine,
|
||||||
|
RiInboxLine,
|
||||||
RiNotification3Line,
|
RiNotification3Line,
|
||||||
RiTimeLine,
|
RiTimeLine,
|
||||||
} from "@remixicon/react";
|
} from "@remixicon/react";
|
||||||
|
import Link from "next/link";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { buttonVariants } from "@/components/ui/button";
|
import { buttonVariants } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuLabel,
|
DropdownMenuLabel,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
@@ -27,26 +33,26 @@ import {
|
|||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import type { DashboardNotification } from "@/lib/dashboard/notifications";
|
import type {
|
||||||
|
BudgetNotification,
|
||||||
|
DashboardNotification,
|
||||||
|
} from "@/lib/dashboard/notifications";
|
||||||
import { cn } from "@/lib/utils/ui";
|
import { cn } from "@/lib/utils/ui";
|
||||||
|
|
||||||
type NotificationBellProps = {
|
type NotificationBellProps = {
|
||||||
notifications: DashboardNotification[];
|
notifications: DashboardNotification[];
|
||||||
totalCount: number;
|
totalCount: number;
|
||||||
|
budgetNotifications: BudgetNotification[];
|
||||||
|
preLancamentosCount?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
function formatDate(dateString: string): string {
|
function formatDate(dateString: string): string {
|
||||||
// Parse manual para evitar problemas de timezone
|
|
||||||
// Formato esperado: "YYYY-MM-DD"
|
|
||||||
const [year, month, day] = dateString.split("-").map(Number);
|
const [year, month, day] = dateString.split("-").map(Number);
|
||||||
|
|
||||||
// Criar data em UTC usando os valores diretos
|
|
||||||
const date = new Date(Date.UTC(year, month - 1, day));
|
const date = new Date(Date.UTC(year, month - 1, day));
|
||||||
|
|
||||||
return date.toLocaleDateString("pt-BR", {
|
return date.toLocaleDateString("pt-BR", {
|
||||||
day: "2-digit",
|
day: "2-digit",
|
||||||
month: "short",
|
month: "short",
|
||||||
timeZone: "UTC", // Força uso de UTC para evitar conversão de timezone
|
timeZone: "UTC",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,13 +63,41 @@ function formatCurrency(amount: number): string {
|
|||||||
}).format(amount);
|
}).format(amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function SectionLabel({
|
||||||
|
icon,
|
||||||
|
title,
|
||||||
|
}: {
|
||||||
|
icon: React.ReactNode;
|
||||||
|
title: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-1.5 px-3 pb-1 pt-3">
|
||||||
|
<span className="text-muted-foreground/60">{icon}</span>
|
||||||
|
<span className="text-[10px] font-semibold uppercase tracking-widest text-muted-foreground/60">
|
||||||
|
{title}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function NotificationBell({
|
export function NotificationBell({
|
||||||
notifications,
|
notifications,
|
||||||
totalCount,
|
totalCount,
|
||||||
|
budgetNotifications,
|
||||||
|
preLancamentosCount = 0,
|
||||||
}: NotificationBellProps) {
|
}: NotificationBellProps) {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const displayCount = totalCount > 99 ? "99+" : totalCount.toString();
|
|
||||||
const hasNotifications = totalCount > 0;
|
const effectiveTotalCount =
|
||||||
|
totalCount + preLancamentosCount + budgetNotifications.length;
|
||||||
|
const displayCount =
|
||||||
|
effectiveTotalCount > 99 ? "99+" : effectiveTotalCount.toString();
|
||||||
|
const hasNotifications = effectiveTotalCount > 0;
|
||||||
|
|
||||||
|
const invoiceNotifications = notifications.filter(
|
||||||
|
(n) => n.type === "invoice",
|
||||||
|
);
|
||||||
|
const boletoNotifications = notifications.filter((n) => n.type === "boleto");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu open={open} onOpenChange={setOpen}>
|
<DropdownMenu open={open} onOpenChange={setOpen}>
|
||||||
@@ -74,7 +108,6 @@ export function NotificationBell({
|
|||||||
type="button"
|
type="button"
|
||||||
aria-label="Notificações"
|
aria-label="Notificações"
|
||||||
aria-expanded={open}
|
aria-expanded={open}
|
||||||
data-has-notifications={hasNotifications}
|
|
||||||
className={cn(
|
className={cn(
|
||||||
buttonVariants({ variant: "ghost", size: "icon-sm" }),
|
buttonVariants({ variant: "ghost", size: "icon-sm" }),
|
||||||
"group relative text-muted-foreground transition-all duration-200",
|
"group relative text-muted-foreground transition-all duration-200",
|
||||||
@@ -103,23 +136,27 @@ export function NotificationBell({
|
|||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="bottom" sideOffset={8}>
|
<TooltipContent side="bottom" sideOffset={8}>
|
||||||
Pagamentos para os próximos 5 dias.
|
Notificações
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<DropdownMenuContent
|
<DropdownMenuContent
|
||||||
align="end"
|
align="end"
|
||||||
sideOffset={12}
|
sideOffset={12}
|
||||||
className="w-80 max-h-[500px] overflow-hidden rounded-lg border border-border/60 bg-popover/95 p-0 shadow-lg backdrop-blur-lg supports-backdrop-filter:backdrop-blur-md"
|
className="w-76 overflow-hidden rounded-lg border border-border/60 bg-popover/95 p-0 shadow-lg backdrop-blur-lg supports-backdrop-filter:backdrop-blur-md"
|
||||||
>
|
>
|
||||||
<DropdownMenuLabel className="sticky top-0 z-10 flex items-center justify-between gap-2 border-b border-border/60 bg-linear-to-b from-background/95 to-background/80 px-4 py-3 text-sm font-semibold">
|
{/* Header */}
|
||||||
<span>Notificações | Próximos 5 dias.</span>
|
<DropdownMenuLabel className="flex items-center justify-between gap-2 border-b border-border/50 px-3 py-2.5 text-sm font-semibold">
|
||||||
|
<span>Notificações</span>
|
||||||
{hasNotifications && (
|
{hasNotifications && (
|
||||||
<Badge variant="outline" className="text-[10px] font-semibold">
|
<Badge variant="outline" className="text-[10px] font-semibold">
|
||||||
{totalCount} {totalCount === 1 ? "item" : "itens"}
|
{effectiveTotalCount}{" "}
|
||||||
|
{effectiveTotalCount === 1 ? "item" : "itens"}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
{notifications.length === 0 ? (
|
|
||||||
|
{!hasNotifications ? (
|
||||||
<div className="px-4 py-8">
|
<div className="px-4 py-8">
|
||||||
<Empty>
|
<Empty>
|
||||||
<EmptyMedia>
|
<EmptyMedia>
|
||||||
@@ -132,72 +169,170 @@ export function NotificationBell({
|
|||||||
</Empty>
|
</Empty>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="max-h-[400px] overflow-y-auto py-2">
|
<div className="max-h-[460px] overflow-y-auto pb-2">
|
||||||
{notifications.map((notification) => (
|
{/* Pré-lançamentos */}
|
||||||
<DropdownMenuItem
|
{preLancamentosCount > 0 && (
|
||||||
key={notification.id}
|
<div>
|
||||||
className={cn(
|
<SectionLabel
|
||||||
"group relative flex flex-col gap-2 rounded-none border-b border-dashed last:border-0 p-2.5",
|
icon={<RiInboxLine className="size-3" />}
|
||||||
"cursor-default focus:bg-transparent data-highlighted:bg-accent/60",
|
title="Pré-lançamentos"
|
||||||
)}
|
/>
|
||||||
|
<Link
|
||||||
|
href="/pre-lancamentos"
|
||||||
|
onClick={() => setOpen(false)}
|
||||||
|
className="group mx-1 mb-1 flex items-center gap-2 rounded-md px-2 py-2 transition-colors hover:bg-accent/60"
|
||||||
>
|
>
|
||||||
<div className="flex items-start justify-between w-full gap-2">
|
<p className="flex-1 text-xs leading-snug text-foreground">
|
||||||
<div className="flex items-start gap-2 flex-1 min-w-0">
|
{preLancamentosCount === 1
|
||||||
<div
|
? "1 pré-lançamento aguardando revisão"
|
||||||
className={cn(
|
: `${preLancamentosCount} pré-lançamentos aguardando revisão`}
|
||||||
"flex items-center justify-center text-sm transition-all duration-200",
|
</p>
|
||||||
)}
|
<RiArrowRightLine className="size-3 shrink-0 text-muted-foreground/50 transition-transform group-hover:translate-x-0.5" />
|
||||||
>
|
</Link>
|
||||||
{notification.status === "overdue" ? (
|
|
||||||
<RiAlertFill color="red" className="size-4" />
|
|
||||||
) : (
|
|
||||||
<RiTimeLine className="size-4" />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
)}
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
|
||||||
<span className="truncate text-sm font-medium">
|
|
||||||
{notification.name}
|
|
||||||
</span>
|
|
||||||
<Badge
|
|
||||||
variant="outline"
|
|
||||||
className="px-1.5 py-0 tracking-wide text-muted-foreground"
|
|
||||||
>
|
|
||||||
{notification.type === "invoice"
|
|
||||||
? "Cartão"
|
|
||||||
: "Boleto"}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
<div className="mt-1 flex flex-col items-start gap-2 text-xs text-muted-foreground">
|
|
||||||
<span className="font-no">
|
|
||||||
{notification.status === "overdue"
|
|
||||||
? "Venceu em "
|
|
||||||
: "Vence em "}
|
|
||||||
{formatDate(notification.dueDate)}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{notification.showAmount && notification.amount > 0 && (
|
{/* Orçamentos */}
|
||||||
<span className="font-medium">
|
{budgetNotifications.length > 0 && (
|
||||||
{formatCurrency(notification.amount)}
|
<div>
|
||||||
</span>
|
<SectionLabel
|
||||||
)}
|
icon={<RiBarChart2Line className="size-3" />}
|
||||||
</div>
|
title="Orçamentos"
|
||||||
</div>
|
/>
|
||||||
</div>
|
<div className="mx-1 mb-1 overflow-hidden rounded-md">
|
||||||
<Badge
|
{budgetNotifications.map((n) => (
|
||||||
variant={
|
<div
|
||||||
notification.status === "overdue" ? "destructive" : "info"
|
key={n.id}
|
||||||
}
|
className="flex items-start gap-2 px-2 py-2"
|
||||||
className={cn("shrink-0 px-2 py-0.5 tracking-wide")}
|
|
||||||
>
|
>
|
||||||
{notification.status === "overdue"
|
{n.status === "exceeded" ? (
|
||||||
? "Atrasado"
|
<RiAlertFill className="mt-0.5 size-3.5 shrink-0 text-destructive" />
|
||||||
: "Em breve"}
|
) : (
|
||||||
</Badge>
|
<RiErrorWarningLine className="mt-0.5 size-3.5 shrink-0 text-amber-500" />
|
||||||
|
)}
|
||||||
|
<p className="text-xs leading-snug">
|
||||||
|
{n.status === "exceeded" ? (
|
||||||
|
<>
|
||||||
|
Orçamento de <strong>{n.categoryName}</strong>{" "}
|
||||||
|
excedido —{" "}
|
||||||
|
<strong>{formatCurrency(n.spentAmount)}</strong> de{" "}
|
||||||
|
{formatCurrency(n.budgetAmount)} (
|
||||||
|
{Math.round(n.usedPercentage)}%)
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<strong>{n.categoryName}</strong> atingiu{" "}
|
||||||
|
<strong>{Math.round(n.usedPercentage)}%</strong> do
|
||||||
|
orçamento —{" "}
|
||||||
|
<strong>{formatCurrency(n.spentAmount)}</strong> de{" "}
|
||||||
|
{formatCurrency(n.budgetAmount)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuItem>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Cartão de Crédito */}
|
||||||
|
{invoiceNotifications.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<SectionLabel
|
||||||
|
icon={<RiBankCardLine className="size-3" />}
|
||||||
|
title="Cartão de Crédito"
|
||||||
|
/>
|
||||||
|
<div className="mx-1 mb-1 overflow-hidden rounded-md">
|
||||||
|
{invoiceNotifications.map((n) => (
|
||||||
|
<div
|
||||||
|
key={n.id}
|
||||||
|
className="flex items-start gap-2 px-2 py-2"
|
||||||
|
>
|
||||||
|
{n.status === "overdue" ? (
|
||||||
|
<RiAlertFill className="mt-0.5 size-3.5 shrink-0 text-destructive" />
|
||||||
|
) : (
|
||||||
|
<RiTimeLine className="mt-0.5 size-3.5 shrink-0 text-amber-500" />
|
||||||
|
)}
|
||||||
|
<p className="text-xs leading-snug">
|
||||||
|
{n.status === "overdue" ? (
|
||||||
|
<>
|
||||||
|
A fatura de <strong>{n.name}</strong> venceu em{" "}
|
||||||
|
{formatDate(n.dueDate)}
|
||||||
|
{n.showAmount && n.amount > 0 && (
|
||||||
|
<>
|
||||||
|
{" "}
|
||||||
|
— <strong>{formatCurrency(n.amount)}</strong>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
A fatura de <strong>{n.name}</strong> vence em{" "}
|
||||||
|
{formatDate(n.dueDate)}
|
||||||
|
{n.showAmount && n.amount > 0 && (
|
||||||
|
<>
|
||||||
|
{" "}
|
||||||
|
— <strong>{formatCurrency(n.amount)}</strong>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Boletos */}
|
||||||
|
{boletoNotifications.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<SectionLabel
|
||||||
|
icon={<RiFileListLine className="size-3" />}
|
||||||
|
title="Boletos"
|
||||||
|
/>
|
||||||
|
<div className="mx-1 mb-1 overflow-hidden rounded-md">
|
||||||
|
{boletoNotifications.map((n) => (
|
||||||
|
<div
|
||||||
|
key={n.id}
|
||||||
|
className="flex items-start gap-2 px-2 py-2"
|
||||||
|
>
|
||||||
|
{n.status === "overdue" ? (
|
||||||
|
<RiAlertFill className="mt-0.5 size-3.5 shrink-0 text-destructive" />
|
||||||
|
) : (
|
||||||
|
<RiTimeLine className="mt-0.5 size-3.5 shrink-0 text-amber-500" />
|
||||||
|
)}
|
||||||
|
<p className="text-xs leading-snug">
|
||||||
|
{n.status === "overdue" ? (
|
||||||
|
<>
|
||||||
|
O boleto <strong>{n.name}</strong>
|
||||||
|
{n.amount > 0 && (
|
||||||
|
<>
|
||||||
|
{" "}
|
||||||
|
— <strong>{formatCurrency(n.amount)}</strong>
|
||||||
|
</>
|
||||||
|
)}{" "}
|
||||||
|
venceu em {formatDate(n.dueDate)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
O boleto <strong>{n.name}</strong>
|
||||||
|
{n.amount > 0 && (
|
||||||
|
<>
|
||||||
|
{" "}
|
||||||
|
— <strong>{formatCurrency(n.amount)}</strong>
|
||||||
|
</>
|
||||||
|
)}{" "}
|
||||||
|
vence em {formatDate(n.dueDate)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
|||||||
@@ -1,78 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { RiEyeLine, RiEyeOffLine } from "@remixicon/react";
|
|
||||||
import { buttonVariants } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
TooltipTrigger,
|
|
||||||
} from "@/components/ui/tooltip";
|
|
||||||
import { cn } from "@/lib/utils/ui";
|
|
||||||
import { usePrivacyMode } from "./privacy-provider";
|
|
||||||
|
|
||||||
type PrivacyModeToggleProps = React.ComponentPropsWithoutRef<"button">;
|
|
||||||
|
|
||||||
export const PrivacyModeToggle = ({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: PrivacyModeToggleProps) => {
|
|
||||||
const { privacyMode, toggle } = usePrivacyMode();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<button
|
|
||||||
ref={undefined}
|
|
||||||
type="button"
|
|
||||||
onClick={toggle}
|
|
||||||
aria-pressed={privacyMode}
|
|
||||||
aria-label={
|
|
||||||
privacyMode
|
|
||||||
? "Desativar modo privacidade"
|
|
||||||
: "Ativar modo privacidade"
|
|
||||||
}
|
|
||||||
title={
|
|
||||||
privacyMode
|
|
||||||
? "Desativar modo privacidade"
|
|
||||||
: "Ativar modo privacidade"
|
|
||||||
}
|
|
||||||
data-state={privacyMode ? "active" : "inactive"}
|
|
||||||
className={cn(
|
|
||||||
buttonVariants({ variant: "ghost", size: "icon-sm" }),
|
|
||||||
"group relative text-muted-foreground transition-all duration-200",
|
|
||||||
"hover:text-foreground focus-visible:ring-2 focus-visible:ring-primary/40",
|
|
||||||
"data-[state=active]:bg-accent/60 data-[state=active]:text-foreground border",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
aria-hidden
|
|
||||||
className="pointer-events-none absolute inset-0 -z-10 opacity-0 transition-opacity duration-200 data-[state=active]:opacity-100"
|
|
||||||
>
|
|
||||||
<span className="absolute inset-0 bg-linear-to-br from-blue-500/5 via-transparent to-blue-500/15 dark:from-blue-500/10 dark:to-blue-500/30" />
|
|
||||||
</span>
|
|
||||||
{privacyMode ? (
|
|
||||||
<RiEyeOffLine
|
|
||||||
className="size-4 transition-transform duration-200"
|
|
||||||
aria-hidden
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<RiEyeLine
|
|
||||||
className="size-4 transition-transform duration-200"
|
|
||||||
aria-hidden
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<span className="sr-only">
|
|
||||||
{privacyMode
|
|
||||||
? "Modo privacidade ativo"
|
|
||||||
: "Modo privacidade inativo"}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side="bottom" sideOffset={8}>
|
|
||||||
{privacyMode ? "Desativar privacidade" : "Ativar privacidade"}
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -45,7 +45,10 @@ export function RefreshPageButton({
|
|||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<RiRefreshLine
|
<RiRefreshLine
|
||||||
className={cn("size-4 transition-transform duration-200", isPending && "animate-spin")}
|
className={cn(
|
||||||
|
"size-4 transition-transform duration-200",
|
||||||
|
isPending && "animate-spin",
|
||||||
|
)}
|
||||||
aria-hidden
|
aria-hidden
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,34 +1,135 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { RiPieChartLine } from "@remixicon/react";
|
import { RiPieChartLine } from "@remixicon/react";
|
||||||
import { useMemo } from "react";
|
import * as React from "react";
|
||||||
import {
|
import {
|
||||||
Area,
|
Area,
|
||||||
AreaChart,
|
AreaChart,
|
||||||
CartesianGrid,
|
CartesianGrid,
|
||||||
ResponsiveContainer,
|
type TooltipProps,
|
||||||
Tooltip,
|
|
||||||
XAxis,
|
XAxis,
|
||||||
YAxis,
|
|
||||||
} from "recharts";
|
} from "recharts";
|
||||||
import { EmptyState } from "@/components/empty-state";
|
import { EmptyState } from "@/components/empty-state";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
import {
|
||||||
|
type ChartConfig,
|
||||||
|
ChartContainer,
|
||||||
|
ChartLegend,
|
||||||
|
ChartLegendContent,
|
||||||
|
ChartTooltip,
|
||||||
|
} from "@/components/ui/chart";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
import { currencyFormatter } from "@/lib/lancamentos/formatting-helpers";
|
import { currencyFormatter } from "@/lib/lancamentos/formatting-helpers";
|
||||||
import type { CategoryChartData } from "@/lib/relatorios/fetch-category-chart-data";
|
import type { CategoryChartData } from "@/lib/relatorios/fetch-category-chart-data";
|
||||||
import { CATEGORY_COLORS } from "@/lib/utils/category-colors";
|
import { CATEGORY_COLORS } from "@/lib/utils/category-colors";
|
||||||
|
|
||||||
|
function AreaTooltip({ active, payload, label }: TooltipProps<number, string>) {
|
||||||
|
if (!active || !payload?.length) return null;
|
||||||
|
|
||||||
|
const items = payload
|
||||||
|
.filter((entry) => Number(entry.value) > 0)
|
||||||
|
.sort((a, b) => Number(b.value) - Number(a.value));
|
||||||
|
|
||||||
|
if (items.length === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-w-[210px] rounded-lg border border-border/50 bg-background px-3 py-2.5 shadow-xl">
|
||||||
|
<p className="mb-2.5 border-b border-border/50 pb-1.5 text-xs font-semibold text-foreground">
|
||||||
|
{label}
|
||||||
|
</p>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
{items.map((entry) => (
|
||||||
|
<div
|
||||||
|
key={entry.dataKey}
|
||||||
|
className="flex items-center justify-between gap-6"
|
||||||
|
>
|
||||||
|
<div className="flex min-w-0 items-center gap-1.5">
|
||||||
|
<span
|
||||||
|
className="size-2 shrink-0 rounded-full"
|
||||||
|
style={{ backgroundColor: entry.color }}
|
||||||
|
/>
|
||||||
|
<span className="truncate text-xs text-muted-foreground">
|
||||||
|
{entry.name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span className="shrink-0 text-xs font-semibold tabular-nums text-foreground">
|
||||||
|
{currencyFormatter.format(Number(entry.value))}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
interface CategoryReportChartProps {
|
interface CategoryReportChartProps {
|
||||||
data: CategoryChartData;
|
data: CategoryChartData;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CHART_COLORS = CATEGORY_COLORS;
|
const LIMIT_OPTIONS = [
|
||||||
|
{ value: "5", label: "Top 5" },
|
||||||
|
{ value: "10", label: "Top 10" },
|
||||||
|
{ value: "15", label: "Top 15" },
|
||||||
|
] as const;
|
||||||
|
|
||||||
const MAX_CATEGORIES_IN_CHART = 15;
|
const MAX_CATEGORIES = 15;
|
||||||
|
|
||||||
export function CategoryReportChart({ data }: CategoryReportChartProps) {
|
export function CategoryReportChart({ data }: CategoryReportChartProps) {
|
||||||
const { chartData, categories } = data;
|
const { chartData, categories } = data;
|
||||||
|
const [limit, setLimit] = React.useState("10");
|
||||||
|
|
||||||
|
const { topCategories, filteredChartData } = React.useMemo(() => {
|
||||||
|
const limitNum = Math.min(Number(limit), MAX_CATEGORIES);
|
||||||
|
|
||||||
|
const categoriesWithTotal = categories.map((category) => ({
|
||||||
|
...category,
|
||||||
|
total: chartData.reduce((sum, point) => {
|
||||||
|
const v = point[category.name];
|
||||||
|
return sum + (typeof v === "number" ? v : 0);
|
||||||
|
}, 0),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const sorted = categoriesWithTotal
|
||||||
|
.sort((a, b) => b.total - a.total)
|
||||||
|
.slice(0, limitNum);
|
||||||
|
|
||||||
|
const filtered = chartData.map((point) => {
|
||||||
|
const result: { month: string; [key: string]: number | string } = {
|
||||||
|
month: point.month,
|
||||||
|
};
|
||||||
|
for (const cat of sorted) {
|
||||||
|
result[cat.name] = (point[cat.name] as number) ?? 0;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
|
return { topCategories: sorted, filteredChartData: filtered };
|
||||||
|
}, [categories, chartData, limit]);
|
||||||
|
|
||||||
|
const chartConfig = React.useMemo<ChartConfig>(() => {
|
||||||
|
const config: ChartConfig = {};
|
||||||
|
for (let i = 0; i < topCategories.length; i++) {
|
||||||
|
const cat = topCategories[i];
|
||||||
|
config[cat.name] = {
|
||||||
|
label: cat.name,
|
||||||
|
color: CATEGORY_COLORS[i % CATEGORY_COLORS.length],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
}, [topCategories]);
|
||||||
|
|
||||||
// Check if there's no data
|
|
||||||
if (categories.length === 0 || chartData.length === 0) {
|
if (categories.length === 0 || chartData.length === 0) {
|
||||||
return (
|
return (
|
||||||
<EmptyState
|
<EmptyState
|
||||||
@@ -40,165 +141,91 @@ export function CategoryReportChart({ data }: CategoryReportChartProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get top 10 categories by total spending
|
const firstMonth = chartData[0]?.month ?? "";
|
||||||
const { topCategories, filteredChartData } = useMemo(() => {
|
const lastMonth = chartData[chartData.length - 1]?.month ?? "";
|
||||||
// Calculate total for each category across all periods
|
const periodLabel =
|
||||||
const categoriesWithTotal = categories.map((category) => {
|
firstMonth === lastMonth ? firstMonth : `${firstMonth} – ${lastMonth}`;
|
||||||
const total = chartData.reduce((sum, dataPoint) => {
|
|
||||||
const value = dataPoint[category.name];
|
|
||||||
return sum + (typeof value === "number" ? value : 0);
|
|
||||||
}, 0);
|
|
||||||
|
|
||||||
return { ...category, total };
|
|
||||||
});
|
|
||||||
|
|
||||||
// Sort by total (descending) and take top 10
|
|
||||||
const sorted = categoriesWithTotal
|
|
||||||
.sort((a, b) => b.total - a.total)
|
|
||||||
.slice(0, MAX_CATEGORIES_IN_CHART);
|
|
||||||
|
|
||||||
// Filter chartData to include only top categories
|
|
||||||
const _topCategoryNames = new Set(sorted.map((cat) => cat.name));
|
|
||||||
const filtered = chartData.map((dataPoint) => {
|
|
||||||
const filteredPoint: { month: string; [key: string]: number | string } = {
|
|
||||||
month: dataPoint.month,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Only include data for top categories
|
|
||||||
for (const cat of sorted) {
|
|
||||||
if (dataPoint[cat.name] !== undefined) {
|
|
||||||
filteredPoint[cat.name] = dataPoint[cat.name];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return filteredPoint;
|
|
||||||
});
|
|
||||||
|
|
||||||
return { topCategories: sorted, filteredChartData: filtered };
|
|
||||||
}, [categories, chartData]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card className="pt-0">
|
||||||
<CardHeader>
|
<CardHeader className="flex items-center gap-2 space-y-0 border-b py-5 sm:flex-row">
|
||||||
<CardTitle>
|
<div className="grid flex-1 gap-1">
|
||||||
Evolução por Categoria - Top {topCategories.length}
|
<CardTitle>Evolução por Categoria</CardTitle>
|
||||||
</CardTitle>
|
<CardDescription>{periodLabel}</CardDescription>
|
||||||
|
</div>
|
||||||
|
<Select value={limit} onValueChange={setLimit}>
|
||||||
|
<SelectTrigger
|
||||||
|
className="hidden w-[130px] rounded-lg sm:ml-auto sm:flex"
|
||||||
|
aria-label="Número de categorias"
|
||||||
|
>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent className="rounded-xl">
|
||||||
|
{LIMIT_OPTIONS.map((opt) => (
|
||||||
|
<SelectItem
|
||||||
|
key={opt.value}
|
||||||
|
value={opt.value}
|
||||||
|
className="rounded-lg"
|
||||||
|
>
|
||||||
|
{opt.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
|
||||||
<div className="h-[400px] w-full">
|
<CardContent className="px-2 pt-4 sm:px-6 sm:pt-6">
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
<ChartContainer
|
||||||
|
config={chartConfig}
|
||||||
|
className="aspect-auto h-[300px] w-full"
|
||||||
|
>
|
||||||
<AreaChart data={filteredChartData}>
|
<AreaChart data={filteredChartData}>
|
||||||
<defs>
|
<defs>
|
||||||
{topCategories.map((category, index) => {
|
{topCategories.map((cat, index) => {
|
||||||
const color = CHART_COLORS[index % CHART_COLORS.length];
|
const color = CATEGORY_COLORS[index % CATEGORY_COLORS.length];
|
||||||
return (
|
return (
|
||||||
<linearGradient
|
<linearGradient
|
||||||
key={category.id}
|
key={cat.id}
|
||||||
id={`gradient-${category.id}`}
|
id={`fill-${cat.id}`}
|
||||||
x1="0"
|
x1="0"
|
||||||
y1="0"
|
y1="0"
|
||||||
x2="0"
|
x2="0"
|
||||||
y2="1"
|
y2="1"
|
||||||
>
|
>
|
||||||
<stop offset="5%" stopColor={color} stopOpacity={0.3} />
|
<stop offset="5%" stopColor={color} stopOpacity={0.8} />
|
||||||
<stop offset="95%" stopColor={color} stopOpacity={0} />
|
<stop offset="95%" stopColor={color} stopOpacity={0.1} />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</defs>
|
</defs>
|
||||||
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
|
|
||||||
|
<CartesianGrid vertical={false} />
|
||||||
|
|
||||||
<XAxis
|
<XAxis
|
||||||
dataKey="month"
|
dataKey="month"
|
||||||
className="text-xs"
|
tickLine={false}
|
||||||
tick={{ fill: "hsl(var(--muted-foreground))" }}
|
axisLine={false}
|
||||||
|
tickMargin={8}
|
||||||
|
minTickGap={32}
|
||||||
/>
|
/>
|
||||||
<YAxis
|
|
||||||
className="text-xs"
|
|
||||||
tick={{ fill: "hsl(var(--muted-foreground))" }}
|
|
||||||
tickFormatter={(value) => {
|
|
||||||
if (value >= 1000) {
|
|
||||||
return `${(value / 1000).toFixed(0)}k`;
|
|
||||||
}
|
|
||||||
return value.toString();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Tooltip
|
|
||||||
content={({ active, payload }) => {
|
|
||||||
if (!active || !payload || payload.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
<ChartTooltip cursor={false} content={<AreaTooltip />} />
|
||||||
<div className="rounded-lg border bg-background p-3 shadow-md">
|
|
||||||
<div className="mb-2 font-semibold">
|
|
||||||
{payload[0]?.payload?.month}
|
|
||||||
</div>
|
|
||||||
<div className="space-y-1">
|
|
||||||
{payload.map((entry, index) => {
|
|
||||||
if (entry.dataKey === "month") return null;
|
|
||||||
|
|
||||||
return (
|
{topCategories.map((cat, index) => (
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
className="flex items-center justify-between gap-4 text-sm"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div
|
|
||||||
className="h-2 w-2 rounded-full"
|
|
||||||
style={{ backgroundColor: entry.color }}
|
|
||||||
/>
|
|
||||||
<span className="text-muted-foreground">
|
|
||||||
{entry.name}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<span className="font-medium">
|
|
||||||
{currencyFormatter.format(
|
|
||||||
Number(entry.value) || 0,
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{topCategories.map((category, index) => {
|
|
||||||
const color = CHART_COLORS[index % CHART_COLORS.length];
|
|
||||||
return (
|
|
||||||
<Area
|
<Area
|
||||||
key={category.id}
|
key={cat.id}
|
||||||
type="monotone"
|
dataKey={cat.name}
|
||||||
dataKey={category.name}
|
type="natural"
|
||||||
stroke={color}
|
fill={`url(#fill-${cat.id})`}
|
||||||
strokeWidth={2}
|
stroke={CATEGORY_COLORS[index % CATEGORY_COLORS.length]}
|
||||||
fill={`url(#gradient-${category.id})`}
|
strokeWidth={1.5}
|
||||||
fillOpacity={1}
|
stackId="a"
|
||||||
/>
|
/>
|
||||||
);
|
))}
|
||||||
})}
|
|
||||||
</AreaChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Legend */}
|
<ChartLegend content={<ChartLegendContent />} />
|
||||||
<div className="mt-4 flex flex-wrap gap-4">
|
</AreaChart>
|
||||||
{topCategories.map((category, index) => {
|
</ChartContainer>
|
||||||
const color = CHART_COLORS[index % CHART_COLORS.length];
|
|
||||||
return (
|
|
||||||
<div key={category.id} className="flex items-center gap-2">
|
|
||||||
<div
|
|
||||||
className="h-3 w-3 rounded-full"
|
|
||||||
style={{ backgroundColor: color }}
|
|
||||||
/>
|
|
||||||
<span className="text-sm text-muted-foreground">
|
|
||||||
{category.name}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
import Image from "next/image";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { AnimatedThemeToggler } from "@/components/animated-theme-toggler";
|
import { AnimatedThemeToggler } from "@/components/animated-theme-toggler";
|
||||||
import { CalculatorDialogButton } from "@/components/calculadora/calculator-dialog";
|
|
||||||
import { FeedbackDialog } from "@/components/feedback/feedback-dialog";
|
|
||||||
import { NotificationBell } from "@/components/notificacoes/notification-bell";
|
import { NotificationBell } from "@/components/notificacoes/notification-bell";
|
||||||
import { PrivacyModeToggle } from "@/components/privacy-mode-toggle";
|
|
||||||
import { RefreshPageButton } from "@/components/refresh-page-button";
|
import { RefreshPageButton } from "@/components/refresh-page-button";
|
||||||
import type { DashboardNotificationsSnapshot } from "@/lib/dashboard/notifications";
|
import type { DashboardNotificationsSnapshot } from "@/lib/dashboard/notifications";
|
||||||
|
import { Logo } from "../logo";
|
||||||
import { TopNavMenu } from "./top-nav-menu";
|
import { TopNavMenu } from "./top-nav-menu";
|
||||||
import { TopbarUser } from "./topbar-user";
|
import { TopbarUser } from "./topbar-user";
|
||||||
|
|
||||||
@@ -29,49 +26,26 @@ export function AppTopbar({
|
|||||||
notificationsSnapshot,
|
notificationsSnapshot,
|
||||||
}: AppTopbarProps) {
|
}: AppTopbarProps) {
|
||||||
return (
|
return (
|
||||||
<header className="fixed top-0 left-0 right-0 z-50 bg-card h-14 shrink-0 flex items-center shadow-xs">
|
<header className="fixed top-0 left-0 right-0 z-50 h-15 shrink-0 flex items-center bg-card/80 backdrop-blur-md supports-backdrop-filter:bg-card/70">
|
||||||
<div className="w-full max-w-8xl mx-auto px-4 flex items-center gap-4 h-full">
|
<div className="w-full max-w-8xl mx-auto px-4 flex items-center gap-4 h-full">
|
||||||
{/* Logo */}
|
{/* Logo */}
|
||||||
<Link
|
<Link href="/dashboard" className="shrink-0 mr-1">
|
||||||
href="/dashboard"
|
<Logo variant="compact" />
|
||||||
className="flex items-center gap-2 shrink-0 mr-1"
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
src="/logo_small.png"
|
|
||||||
alt="OpenMonetis"
|
|
||||||
width={28}
|
|
||||||
height={28}
|
|
||||||
className="object-contain"
|
|
||||||
priority
|
|
||||||
/>
|
|
||||||
<Image
|
|
||||||
src="/logo_text.png"
|
|
||||||
alt="OpenMonetis"
|
|
||||||
width={90}
|
|
||||||
height={28}
|
|
||||||
className="object-contain dark:invert hidden sm:block"
|
|
||||||
priority
|
|
||||||
/>
|
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{/* Navigation */}
|
{/* Navigation */}
|
||||||
<TopNavMenu preLancamentosCount={preLancamentosCount} />
|
<TopNavMenu />
|
||||||
|
|
||||||
{/* Right-side actions */}
|
{/* Right-side actions */}
|
||||||
<div className="ml-auto flex items-center gap-1">
|
<div className="ml-auto flex items-center gap-2">
|
||||||
<NotificationBell
|
<NotificationBell
|
||||||
notifications={notificationsSnapshot.notifications}
|
notifications={notificationsSnapshot.notifications}
|
||||||
totalCount={notificationsSnapshot.totalCount}
|
totalCount={notificationsSnapshot.totalCount}
|
||||||
|
budgetNotifications={notificationsSnapshot.budgetNotifications}
|
||||||
|
preLancamentosCount={preLancamentosCount}
|
||||||
/>
|
/>
|
||||||
<CalculatorDialogButton withTooltip />
|
|
||||||
<RefreshPageButton />
|
<RefreshPageButton />
|
||||||
<PrivacyModeToggle />
|
|
||||||
<AnimatedThemeToggler />
|
<AnimatedThemeToggler />
|
||||||
<span
|
|
||||||
aria-hidden
|
|
||||||
className="h-5 w-px bg-foreground/20 mx-1 hidden sm:block"
|
|
||||||
/>
|
|
||||||
<FeedbackDialog />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* User avatar */}
|
{/* User avatar */}
|
||||||
|
|||||||
105
components/topbar/ferramentas-dropdown.tsx
Normal file
105
components/topbar/ferramentas-dropdown.tsx
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { RiCalculatorLine, RiEyeLine, RiEyeOffLine } from "@remixicon/react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { CalculatorDialogContent } from "@/components/calculadora/calculator-dialog";
|
||||||
|
import { usePrivacyMode } from "@/components/privacy-provider";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Dialog, DialogTrigger } from "@/components/ui/dialog";
|
||||||
|
import { cn } from "@/lib/utils/ui";
|
||||||
|
|
||||||
|
const itemClass =
|
||||||
|
"flex w-full items-center gap-2.5 rounded-sm px-2 py-2 text-sm text-foreground hover:bg-accent transition-colors cursor-pointer";
|
||||||
|
|
||||||
|
export function FerramentasDropdownContent() {
|
||||||
|
const { privacyMode, toggle } = usePrivacyMode();
|
||||||
|
const [calcOpen, setCalcOpen] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={calcOpen} onOpenChange={setCalcOpen}>
|
||||||
|
<ul className="grid w-52 gap-0.5 p-2">
|
||||||
|
<li>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<button type="button" className={cn(itemClass)}>
|
||||||
|
<span className="text-muted-foreground shrink-0">
|
||||||
|
<RiCalculatorLine className="size-4" />
|
||||||
|
</span>
|
||||||
|
<span className="flex-1 text-left">calculadora</span>
|
||||||
|
</button>
|
||||||
|
</DialogTrigger>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button type="button" onClick={toggle} className={cn(itemClass)}>
|
||||||
|
<span className="text-muted-foreground shrink-0">
|
||||||
|
{privacyMode ? (
|
||||||
|
<RiEyeOffLine className="size-4" />
|
||||||
|
) : (
|
||||||
|
<RiEyeLine className="size-4" />
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<span className="flex-1 text-left">privacidade</span>
|
||||||
|
{privacyMode && (
|
||||||
|
<Badge
|
||||||
|
variant="secondary"
|
||||||
|
className="text-[10px] px-1.5 py-0 h-4"
|
||||||
|
>
|
||||||
|
Ativo
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<CalculatorDialogContent open={calcOpen} />
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type MobileFerramentasItemsProps = {
|
||||||
|
onClose: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function MobileFerramentasItems({
|
||||||
|
onClose,
|
||||||
|
}: MobileFerramentasItemsProps) {
|
||||||
|
const { privacyMode, toggle } = usePrivacyMode();
|
||||||
|
const [calcOpen, setCalcOpen] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={calcOpen} onOpenChange={setCalcOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="flex w-full items-center gap-3 rounded-md px-3 py-2 text-sm transition-colors text-muted-foreground hover:text-foreground hover:bg-accent"
|
||||||
|
>
|
||||||
|
<span className="text-muted-foreground shrink-0">
|
||||||
|
<RiCalculatorLine className="size-4" />
|
||||||
|
</span>
|
||||||
|
<span className="flex-1 text-left">calculadora</span>
|
||||||
|
</button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
toggle();
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
className="flex w-full items-center gap-3 rounded-md px-3 py-2 text-sm transition-colors text-muted-foreground hover:text-foreground hover:bg-accent"
|
||||||
|
>
|
||||||
|
<span className="text-muted-foreground shrink-0">
|
||||||
|
{privacyMode ? (
|
||||||
|
<RiEyeOffLine className="size-4" />
|
||||||
|
) : (
|
||||||
|
<RiEyeLine className="size-4" />
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<span className="flex-1 text-left">privacidade</span>
|
||||||
|
{privacyMode && (
|
||||||
|
<Badge variant="secondary" className="text-[10px] px-1.5 py-0 h-4">
|
||||||
|
Ativo
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
<CalculatorDialogContent open={calcOpen} />
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -33,33 +33,32 @@ import {
|
|||||||
} from "@/components/ui/sheet";
|
} from "@/components/ui/sheet";
|
||||||
import type { DropdownLinkItem } from "./dropdown-link-list";
|
import type { DropdownLinkItem } from "./dropdown-link-list";
|
||||||
import { DropdownLinkList } from "./dropdown-link-list";
|
import { DropdownLinkList } from "./dropdown-link-list";
|
||||||
|
import {
|
||||||
|
FerramentasDropdownContent,
|
||||||
|
MobileFerramentasItems,
|
||||||
|
} from "./ferramentas-dropdown";
|
||||||
import { MobileNavLink, MobileSectionLabel } from "./mobile-nav-link";
|
import { MobileNavLink, MobileSectionLabel } from "./mobile-nav-link";
|
||||||
import { triggerClass } from "./nav-styles";
|
import { triggerClass } from "./nav-styles";
|
||||||
import { SimpleNavLink } from "./simple-nav-link";
|
import { SimpleNavLink } from "./simple-nav-link";
|
||||||
|
|
||||||
type TopNavMenuProps = {
|
export function TopNavMenu() {
|
||||||
preLancamentosCount?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function TopNavMenu({ preLancamentosCount = 0 }: TopNavMenuProps) {
|
|
||||||
const [sheetOpen, setSheetOpen] = useState(false);
|
const [sheetOpen, setSheetOpen] = useState(false);
|
||||||
const close = () => setSheetOpen(false);
|
const close = () => setSheetOpen(false);
|
||||||
|
|
||||||
const lancamentosItems: DropdownLinkItem[] = [
|
const lancamentosItems: DropdownLinkItem[] = [
|
||||||
{
|
{
|
||||||
href: "/lancamentos",
|
href: "/lancamentos",
|
||||||
label: "Lançamentos",
|
label: "lançamentos",
|
||||||
icon: <RiArrowLeftRightLine className="size-4" />,
|
icon: <RiArrowLeftRightLine className="size-4" />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: "/pre-lancamentos",
|
href: "/pre-lancamentos",
|
||||||
label: "Pré-Lançamentos",
|
label: "pré-lançamentos",
|
||||||
icon: <RiInboxLine className="size-4" />,
|
icon: <RiInboxLine className="size-4" />,
|
||||||
badge: preLancamentosCount,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: "/calendario",
|
href: "/calendario",
|
||||||
label: "Calendário",
|
label: "calendário",
|
||||||
icon: <RiCalendarEventLine className="size-4" />,
|
icon: <RiCalendarEventLine className="size-4" />,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -67,17 +66,17 @@ export function TopNavMenu({ preLancamentosCount = 0 }: TopNavMenuProps) {
|
|||||||
const financasItems: DropdownLinkItem[] = [
|
const financasItems: DropdownLinkItem[] = [
|
||||||
{
|
{
|
||||||
href: "/cartoes",
|
href: "/cartoes",
|
||||||
label: "Cartões",
|
label: "cartões",
|
||||||
icon: <RiBankCard2Line className="size-4" />,
|
icon: <RiBankCard2Line className="size-4" />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: "/contas",
|
href: "/contas",
|
||||||
label: "Contas",
|
label: "contas",
|
||||||
icon: <RiBankLine className="size-4" />,
|
icon: <RiBankLine className="size-4" />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: "/orcamentos",
|
href: "/orcamentos",
|
||||||
label: "Orçamentos",
|
label: "orçamentos",
|
||||||
icon: <RiFundsLine className="size-4" />,
|
icon: <RiFundsLine className="size-4" />,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -85,17 +84,17 @@ export function TopNavMenu({ preLancamentosCount = 0 }: TopNavMenuProps) {
|
|||||||
const organizacaoItems: DropdownLinkItem[] = [
|
const organizacaoItems: DropdownLinkItem[] = [
|
||||||
{
|
{
|
||||||
href: "/pagadores",
|
href: "/pagadores",
|
||||||
label: "Pagadores",
|
label: "pagadores",
|
||||||
icon: <RiGroupLine className="size-4" />,
|
icon: <RiGroupLine className="size-4" />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: "/categorias",
|
href: "/categorias",
|
||||||
label: "Categorias",
|
label: "categorias",
|
||||||
icon: <RiPriceTag3Line className="size-4" />,
|
icon: <RiPriceTag3Line className="size-4" />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: "/anotacoes",
|
href: "/anotacoes",
|
||||||
label: "Anotações",
|
label: "anotações",
|
||||||
icon: <RiTodoLine className="size-4" />,
|
icon: <RiTodoLine className="size-4" />,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -103,17 +102,17 @@ export function TopNavMenu({ preLancamentosCount = 0 }: TopNavMenuProps) {
|
|||||||
const analiseItems: DropdownLinkItem[] = [
|
const analiseItems: DropdownLinkItem[] = [
|
||||||
{
|
{
|
||||||
href: "/insights",
|
href: "/insights",
|
||||||
label: "Insights",
|
label: "insights",
|
||||||
icon: <RiSparklingLine className="size-4" />,
|
icon: <RiSparklingLine className="size-4" />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: "/relatorios/tendencias",
|
href: "/relatorios/tendencias",
|
||||||
label: "Tendências",
|
label: "tendências",
|
||||||
icon: <RiFileChartLine className="size-4" />,
|
icon: <RiFileChartLine className="size-4" />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: "/relatorios/uso-cartoes",
|
href: "/relatorios/uso-cartoes",
|
||||||
label: "Uso de Cartões",
|
label: "uso de cartões",
|
||||||
icon: <RiBankCard2Line className="size-4" />,
|
icon: <RiBankCard2Line className="size-4" />,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -157,12 +156,21 @@ export function TopNavMenu({ preLancamentosCount = 0 }: TopNavMenuProps) {
|
|||||||
|
|
||||||
<NavigationMenuItem>
|
<NavigationMenuItem>
|
||||||
<NavigationMenuTrigger className={triggerClass}>
|
<NavigationMenuTrigger className={triggerClass}>
|
||||||
Análise
|
Relatórios
|
||||||
</NavigationMenuTrigger>
|
</NavigationMenuTrigger>
|
||||||
<NavigationMenuContent>
|
<NavigationMenuContent>
|
||||||
<DropdownLinkList items={analiseItems} />
|
<DropdownLinkList items={analiseItems} />
|
||||||
</NavigationMenuContent>
|
</NavigationMenuContent>
|
||||||
</NavigationMenuItem>
|
</NavigationMenuItem>
|
||||||
|
|
||||||
|
<NavigationMenuItem>
|
||||||
|
<NavigationMenuTrigger className={triggerClass}>
|
||||||
|
Ferramentas
|
||||||
|
</NavigationMenuTrigger>
|
||||||
|
<NavigationMenuContent>
|
||||||
|
<FerramentasDropdownContent />
|
||||||
|
</NavigationMenuContent>
|
||||||
|
</NavigationMenuItem>
|
||||||
</NavigationMenuList>
|
</NavigationMenuList>
|
||||||
</NavigationMenu>
|
</NavigationMenu>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -198,22 +206,21 @@ export function TopNavMenu({ preLancamentosCount = 0 }: TopNavMenuProps) {
|
|||||||
icon={<RiArrowLeftRightLine className="size-4" />}
|
icon={<RiArrowLeftRightLine className="size-4" />}
|
||||||
onClick={close}
|
onClick={close}
|
||||||
>
|
>
|
||||||
Lançamentos
|
lançamentos
|
||||||
</MobileNavLink>
|
</MobileNavLink>
|
||||||
<MobileNavLink
|
<MobileNavLink
|
||||||
href="/pre-lancamentos"
|
href="/pre-lancamentos"
|
||||||
icon={<RiInboxLine className="size-4" />}
|
icon={<RiInboxLine className="size-4" />}
|
||||||
onClick={close}
|
onClick={close}
|
||||||
badge={preLancamentosCount}
|
|
||||||
>
|
>
|
||||||
Pré-Lançamentos
|
pré-lançamentos
|
||||||
</MobileNavLink>
|
</MobileNavLink>
|
||||||
<MobileNavLink
|
<MobileNavLink
|
||||||
href="/calendario"
|
href="/calendario"
|
||||||
icon={<RiCalendarEventLine className="size-4" />}
|
icon={<RiCalendarEventLine className="size-4" />}
|
||||||
onClick={close}
|
onClick={close}
|
||||||
>
|
>
|
||||||
Calendário
|
calendário
|
||||||
</MobileNavLink>
|
</MobileNavLink>
|
||||||
|
|
||||||
<MobileSectionLabel label="Finanças" />
|
<MobileSectionLabel label="Finanças" />
|
||||||
@@ -222,21 +229,21 @@ export function TopNavMenu({ preLancamentosCount = 0 }: TopNavMenuProps) {
|
|||||||
icon={<RiBankCard2Line className="size-4" />}
|
icon={<RiBankCard2Line className="size-4" />}
|
||||||
onClick={close}
|
onClick={close}
|
||||||
>
|
>
|
||||||
Cartões
|
cartões
|
||||||
</MobileNavLink>
|
</MobileNavLink>
|
||||||
<MobileNavLink
|
<MobileNavLink
|
||||||
href="/contas"
|
href="/contas"
|
||||||
icon={<RiBankLine className="size-4" />}
|
icon={<RiBankLine className="size-4" />}
|
||||||
onClick={close}
|
onClick={close}
|
||||||
>
|
>
|
||||||
Contas
|
contas
|
||||||
</MobileNavLink>
|
</MobileNavLink>
|
||||||
<MobileNavLink
|
<MobileNavLink
|
||||||
href="/orcamentos"
|
href="/orcamentos"
|
||||||
icon={<RiFundsLine className="size-4" />}
|
icon={<RiFundsLine className="size-4" />}
|
||||||
onClick={close}
|
onClick={close}
|
||||||
>
|
>
|
||||||
Orçamentos
|
orçamentos
|
||||||
</MobileNavLink>
|
</MobileNavLink>
|
||||||
|
|
||||||
<MobileSectionLabel label="Organização" />
|
<MobileSectionLabel label="Organização" />
|
||||||
@@ -245,21 +252,21 @@ export function TopNavMenu({ preLancamentosCount = 0 }: TopNavMenuProps) {
|
|||||||
icon={<RiGroupLine className="size-4" />}
|
icon={<RiGroupLine className="size-4" />}
|
||||||
onClick={close}
|
onClick={close}
|
||||||
>
|
>
|
||||||
Pagadores
|
pagadores
|
||||||
</MobileNavLink>
|
</MobileNavLink>
|
||||||
<MobileNavLink
|
<MobileNavLink
|
||||||
href="/categorias"
|
href="/categorias"
|
||||||
icon={<RiPriceTag3Line className="size-4" />}
|
icon={<RiPriceTag3Line className="size-4" />}
|
||||||
onClick={close}
|
onClick={close}
|
||||||
>
|
>
|
||||||
Categorias
|
categorias
|
||||||
</MobileNavLink>
|
</MobileNavLink>
|
||||||
<MobileNavLink
|
<MobileNavLink
|
||||||
href="/anotacoes"
|
href="/anotacoes"
|
||||||
icon={<RiTodoLine className="size-4" />}
|
icon={<RiTodoLine className="size-4" />}
|
||||||
onClick={close}
|
onClick={close}
|
||||||
>
|
>
|
||||||
Anotações
|
anotações
|
||||||
</MobileNavLink>
|
</MobileNavLink>
|
||||||
|
|
||||||
<MobileSectionLabel label="Análise" />
|
<MobileSectionLabel label="Análise" />
|
||||||
@@ -268,22 +275,25 @@ export function TopNavMenu({ preLancamentosCount = 0 }: TopNavMenuProps) {
|
|||||||
icon={<RiSparklingLine className="size-4" />}
|
icon={<RiSparklingLine className="size-4" />}
|
||||||
onClick={close}
|
onClick={close}
|
||||||
>
|
>
|
||||||
Insights
|
insights
|
||||||
</MobileNavLink>
|
</MobileNavLink>
|
||||||
<MobileNavLink
|
<MobileNavLink
|
||||||
href="/relatorios/tendencias"
|
href="/relatorios/tendencias"
|
||||||
icon={<RiFileChartLine className="size-4" />}
|
icon={<RiFileChartLine className="size-4" />}
|
||||||
onClick={close}
|
onClick={close}
|
||||||
>
|
>
|
||||||
Tendências
|
tendências
|
||||||
</MobileNavLink>
|
</MobileNavLink>
|
||||||
<MobileNavLink
|
<MobileNavLink
|
||||||
href="/relatorios/uso-cartoes"
|
href="/relatorios/uso-cartoes"
|
||||||
icon={<RiBankCard2Line className="size-4" />}
|
icon={<RiBankCard2Line className="size-4" />}
|
||||||
onClick={close}
|
onClick={close}
|
||||||
>
|
>
|
||||||
Uso de Cartões
|
uso de cartões
|
||||||
</MobileNavLink>
|
</MobileNavLink>
|
||||||
|
|
||||||
|
<MobileSectionLabel label="Ferramentas" />
|
||||||
|
<MobileFerramentasItems onClose={close} />
|
||||||
</nav>
|
</nav>
|
||||||
</SheetContent>
|
</SheetContent>
|
||||||
</Sheet>
|
</Sheet>
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { RiSettings2Line } from "@remixicon/react";
|
import {
|
||||||
|
RiLogoutCircleLine,
|
||||||
|
RiMessageLine,
|
||||||
|
RiSettings2Line,
|
||||||
|
} from "@remixicon/react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useMemo } from "react";
|
import { useRouter } from "next/navigation";
|
||||||
import LogoutButton from "@/components/auth/logout-button";
|
import { useMemo, useState } from "react";
|
||||||
|
import { FeedbackDialogBody } from "@/components/feedback/feedback-dialog";
|
||||||
|
import { Dialog, DialogTrigger } from "@/components/ui/dialog";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
@@ -12,7 +18,14 @@ import {
|
|||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import { Spinner } from "@/components/ui/spinner";
|
||||||
|
import { authClient } from "@/lib/auth/client";
|
||||||
import { getAvatarSrc } from "@/lib/pagadores/utils";
|
import { getAvatarSrc } from "@/lib/pagadores/utils";
|
||||||
|
import { cn } from "@/lib/utils/ui";
|
||||||
|
import { version } from "@/package.json";
|
||||||
|
|
||||||
|
const itemClass =
|
||||||
|
"flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-sm transition-colors hover:bg-accent";
|
||||||
|
|
||||||
type TopbarUserProps = {
|
type TopbarUserProps = {
|
||||||
user: {
|
user: {
|
||||||
@@ -25,25 +38,40 @@ type TopbarUserProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function TopbarUser({ user, pagadorAvatarUrl }: TopbarUserProps) {
|
export function TopbarUser({ user, pagadorAvatarUrl }: TopbarUserProps) {
|
||||||
|
const router = useRouter();
|
||||||
|
const [logoutLoading, setLogoutLoading] = useState(false);
|
||||||
|
const [feedbackOpen, setFeedbackOpen] = useState(false);
|
||||||
|
|
||||||
const avatarSrc = useMemo(() => {
|
const avatarSrc = useMemo(() => {
|
||||||
if (pagadorAvatarUrl) return getAvatarSrc(pagadorAvatarUrl);
|
if (pagadorAvatarUrl) return getAvatarSrc(pagadorAvatarUrl);
|
||||||
if (user.image) return user.image;
|
if (user.image) return user.image;
|
||||||
return getAvatarSrc(null);
|
return getAvatarSrc(null);
|
||||||
}, [user.image, pagadorAvatarUrl]);
|
}, [user.image, pagadorAvatarUrl]);
|
||||||
|
|
||||||
|
async function handleLogout() {
|
||||||
|
await authClient.signOut({
|
||||||
|
fetchOptions: {
|
||||||
|
onSuccess: () => router.push("/login"),
|
||||||
|
onRequest: () => setLogoutLoading(true),
|
||||||
|
onResponse: () => setLogoutLoading(false),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Dialog open={feedbackOpen} onOpenChange={setFeedbackOpen}>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<button
|
<button
|
||||||
type="button"
|
className="relative flex size-9 items-center justify-center overflow-hidden rounded-full border-background bg-background shadow-lg"
|
||||||
className="flex items-center rounded-full ring-2 ring-foreground/30 hover:ring-foreground/60 transition-all focus-visible:outline-none focus-visible:ring-foreground"
|
aria-label="Menu do usuário"
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
src={avatarSrc}
|
src={avatarSrc}
|
||||||
alt={user.name}
|
alt={`Avatar de ${user.name}`}
|
||||||
width={32}
|
width={40}
|
||||||
height={32}
|
height={40}
|
||||||
className="size-8 rounded-full object-cover"
|
className="size-10 rounded-full object-cover"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
@@ -63,20 +91,56 @@ export function TopbarUser({ user, pagadorAvatarUrl }: TopbarUserProps) {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
|
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<div className="flex flex-col gap-1 pt-1">
|
|
||||||
<Link
|
<div className="flex flex-col gap-0.5 py-1">
|
||||||
href="/ajustes"
|
<Link href="/ajustes" className={cn(itemClass, "text-foreground")}>
|
||||||
className="flex items-center gap-2 rounded-sm px-2 py-1.5 text-sm hover:bg-accent transition-colors"
|
<RiSettings2Line className="size-4 text-muted-foreground shrink-0" />
|
||||||
>
|
|
||||||
<RiSettings2Line className="size-4 text-muted-foreground" />
|
|
||||||
Ajustes
|
Ajustes
|
||||||
</Link>
|
</Link>
|
||||||
<div className="px-1 py-0.5">
|
|
||||||
<LogoutButton />
|
<DialogTrigger asChild>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={cn(itemClass, "text-foreground")}
|
||||||
|
>
|
||||||
|
<RiMessageLine className="size-4 text-muted-foreground shrink-0" />
|
||||||
|
Enviar Feedback
|
||||||
|
</button>
|
||||||
|
</DialogTrigger>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
|
||||||
|
<div className="py-1">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleLogout}
|
||||||
|
disabled={logoutLoading}
|
||||||
|
aria-busy={logoutLoading}
|
||||||
|
className={cn(
|
||||||
|
itemClass,
|
||||||
|
"text-destructive hover:bg-destructive/10 hover:text-destructive disabled:opacity-60",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{logoutLoading ? (
|
||||||
|
<Spinner className="size-4 shrink-0" />
|
||||||
|
) : (
|
||||||
|
<RiLogoutCircleLine className="size-4 shrink-0" />
|
||||||
|
)}
|
||||||
|
{logoutLoading ? "Saindo..." : "Sair"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<div className="px-3 py-1.5">
|
||||||
|
<span className="text-[10px] font-mono text-muted-foreground/40 select-none">
|
||||||
|
Versão {version}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
<FeedbackDialogBody onClose={() => setFeedbackOpen(false)} />
|
||||||
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { RiArrowDownBoxFill } from "@remixicon/react";
|
import { RiArrowDropDownLine } from "@remixicon/react";
|
||||||
import { cva } from "class-variance-authority";
|
import { cva } from "class-variance-authority";
|
||||||
import { NavigationMenu as NavigationMenuPrimitive } from "radix-ui";
|
import { NavigationMenu as NavigationMenuPrimitive } from "radix-ui";
|
||||||
import type * as React from "react";
|
import type * as React from "react";
|
||||||
@@ -73,8 +73,8 @@ function NavigationMenuTrigger({
|
|||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}{" "}
|
{children}{" "}
|
||||||
<RiArrowDownBoxFill
|
<RiArrowDropDownLine
|
||||||
className="relative top-[1px] ml-1 size-3 transition duration-300 group-data-[state=open]:rotate-180"
|
className="relative top-px ml-1 size-4 text-primary transition duration-300 group-data-[state=open]:rotate-180"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
</NavigationMenuPrimitive.Trigger>
|
</NavigationMenuPrimitive.Trigger>
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import { and, eq, lt, sql } from "drizzle-orm";
|
import { and, eq, lt, ne, sql } from "drizzle-orm";
|
||||||
import { cartoes, faturas, lancamentos } from "@/db/schema";
|
import {
|
||||||
|
cartoes,
|
||||||
|
categorias,
|
||||||
|
faturas,
|
||||||
|
lancamentos,
|
||||||
|
orcamentos,
|
||||||
|
} from "@/db/schema";
|
||||||
import { db } from "@/lib/db";
|
import { db } from "@/lib/db";
|
||||||
import { INVOICE_PAYMENT_STATUS } from "@/lib/faturas";
|
import { INVOICE_PAYMENT_STATUS } from "@/lib/faturas";
|
||||||
import { getAdminPagadorId } from "@/lib/pagadores/get-admin-id";
|
import { getAdminPagadorId } from "@/lib/pagadores/get-admin-id";
|
||||||
@@ -16,15 +22,28 @@ export type DashboardNotification = {
|
|||||||
status: NotificationType;
|
status: NotificationType;
|
||||||
amount: number;
|
amount: number;
|
||||||
period?: string;
|
period?: string;
|
||||||
showAmount: boolean; // Controla se o valor deve ser exibido no card
|
showAmount: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BudgetStatus = "exceeded" | "critical";
|
||||||
|
|
||||||
|
export type BudgetNotification = {
|
||||||
|
id: string;
|
||||||
|
categoryName: string;
|
||||||
|
budgetAmount: number;
|
||||||
|
spentAmount: number;
|
||||||
|
usedPercentage: number;
|
||||||
|
status: BudgetStatus;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DashboardNotificationsSnapshot = {
|
export type DashboardNotificationsSnapshot = {
|
||||||
notifications: DashboardNotification[];
|
notifications: DashboardNotification[];
|
||||||
totalCount: number;
|
totalCount: number;
|
||||||
|
budgetNotifications: BudgetNotification[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const PAYMENT_METHOD_BOLETO = "Boleto";
|
const PAYMENT_METHOD_BOLETO = "Boleto";
|
||||||
|
const BUDGET_CRITICAL_THRESHOLD = 80;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calcula a data de vencimento de uma fatura baseado no período e dia de vencimento
|
* Calcula a data de vencimento de uma fatura baseado no período e dia de vencimento
|
||||||
@@ -97,13 +116,11 @@ function parseUTCDate(dateString: string): Date {
|
|||||||
function isOverdue(dueDate: string, today: Date): boolean {
|
function isOverdue(dueDate: string, today: Date): boolean {
|
||||||
const due = parseUTCDate(dueDate);
|
const due = parseUTCDate(dueDate);
|
||||||
const dueNormalized = normalizeDate(due);
|
const dueNormalized = normalizeDate(due);
|
||||||
|
|
||||||
return dueNormalized < today;
|
return dueNormalized < today;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verifica se uma data vence nos próximos X dias (incluindo hoje)
|
* Verifica se uma data vence nos próximos X dias (incluindo hoje)
|
||||||
* Exemplo: Se hoje é dia 4 e daysThreshold = 5, retorna true para datas de 4 a 8
|
|
||||||
*/
|
*/
|
||||||
function isDueWithinDays(
|
function isDueWithinDays(
|
||||||
dueDate: string,
|
dueDate: string,
|
||||||
@@ -112,25 +129,21 @@ function isDueWithinDays(
|
|||||||
): boolean {
|
): boolean {
|
||||||
const due = parseUTCDate(dueDate);
|
const due = parseUTCDate(dueDate);
|
||||||
const dueNormalized = normalizeDate(due);
|
const dueNormalized = normalizeDate(due);
|
||||||
|
|
||||||
// Data limite: hoje + daysThreshold dias (em UTC)
|
|
||||||
const limitDate = new Date(today);
|
const limitDate = new Date(today);
|
||||||
limitDate.setUTCDate(limitDate.getUTCDate() + daysThreshold);
|
limitDate.setUTCDate(limitDate.getUTCDate() + daysThreshold);
|
||||||
|
|
||||||
// Vence se está entre hoje (inclusive) e a data limite (inclusive)
|
|
||||||
return dueNormalized >= today && dueNormalized <= limitDate;
|
return dueNormalized >= today && dueNormalized <= limitDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toNum(value: unknown): number {
|
||||||
|
if (typeof value === "number") return value;
|
||||||
|
return Number(value) || 0;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Busca todas as notificações do dashboard
|
* Busca todas as notificações do dashboard:
|
||||||
*
|
* - Faturas de cartão atrasadas ou com vencimento próximo
|
||||||
* Regras:
|
* - Boletos não pagos atrasados ou com vencimento próximo
|
||||||
* - Períodos anteriores: TODOS os não pagos (sempre status "atrasado")
|
* - Orçamentos excedidos (≥ 100%) ou críticos (≥ 80%)
|
||||||
* - Período atual: Itens atrasados + os que vencem nos próximos dias (sem mostrar valor)
|
|
||||||
*
|
|
||||||
* Status:
|
|
||||||
* - "overdue": vencimento antes do dia atual (ou qualquer período anterior)
|
|
||||||
* - "due_soon": vencimento no dia atual ou nos próximos dias
|
|
||||||
*/
|
*/
|
||||||
export async function fetchDashboardNotifications(
|
export async function fetchDashboardNotifications(
|
||||||
userId: string,
|
userId: string,
|
||||||
@@ -141,8 +154,7 @@ export async function fetchDashboardNotifications(
|
|||||||
|
|
||||||
const adminPagadorId = await getAdminPagadorId(userId);
|
const adminPagadorId = await getAdminPagadorId(userId);
|
||||||
|
|
||||||
// Buscar faturas pendentes de períodos anteriores
|
// --- Faturas atrasadas (períodos anteriores) ---
|
||||||
// Apenas faturas com registro na tabela (períodos antigos devem ter sido finalizados)
|
|
||||||
const overdueInvoices = await db
|
const overdueInvoices = await db
|
||||||
.select({
|
.select({
|
||||||
invoiceId: faturas.id,
|
invoiceId: faturas.id,
|
||||||
@@ -171,8 +183,7 @@ export async function fetchDashboardNotifications(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Buscar faturas do período atual
|
// --- Faturas do período atual ---
|
||||||
// Usa LEFT JOIN para incluir cartões com lançamentos mesmo sem registro em faturas
|
|
||||||
const currentInvoices = await db
|
const currentInvoices = await db
|
||||||
.select({
|
.select({
|
||||||
invoiceId: faturas.id,
|
invoiceId: faturas.id,
|
||||||
@@ -213,13 +224,12 @@ export async function fetchDashboardNotifications(
|
|||||||
faturas.paymentStatus,
|
faturas.paymentStatus,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Buscar boletos não pagos (usando pagadorId direto ao invés de JOIN)
|
// --- Boletos não pagos ---
|
||||||
const boletosConditions = [
|
const boletosConditions = [
|
||||||
eq(lancamentos.userId, userId),
|
eq(lancamentos.userId, userId),
|
||||||
eq(lancamentos.paymentMethod, PAYMENT_METHOD_BOLETO),
|
eq(lancamentos.paymentMethod, PAYMENT_METHOD_BOLETO),
|
||||||
eq(lancamentos.isSettled, false),
|
eq(lancamentos.isSettled, false),
|
||||||
];
|
];
|
||||||
|
|
||||||
if (adminPagadorId) {
|
if (adminPagadorId) {
|
||||||
boletosConditions.push(eq(lancamentos.pagadorId, adminPagadorId));
|
boletosConditions.push(eq(lancamentos.pagadorId, adminPagadorId));
|
||||||
}
|
}
|
||||||
@@ -235,18 +245,44 @@ export async function fetchDashboardNotifications(
|
|||||||
.from(lancamentos)
|
.from(lancamentos)
|
||||||
.where(and(...boletosConditions));
|
.where(and(...boletosConditions));
|
||||||
|
|
||||||
|
// --- Orçamentos do período atual ---
|
||||||
|
const budgetJoinConditions = [
|
||||||
|
eq(lancamentos.categoriaId, orcamentos.categoriaId),
|
||||||
|
eq(lancamentos.userId, orcamentos.userId),
|
||||||
|
eq(lancamentos.period, orcamentos.period),
|
||||||
|
eq(lancamentos.transactionType, "Despesa"),
|
||||||
|
ne(lancamentos.condition, "cancelado"),
|
||||||
|
];
|
||||||
|
if (adminPagadorId) {
|
||||||
|
budgetJoinConditions.push(eq(lancamentos.pagadorId, adminPagadorId));
|
||||||
|
}
|
||||||
|
|
||||||
|
const budgetRows = await db
|
||||||
|
.select({
|
||||||
|
orcamentoId: orcamentos.id,
|
||||||
|
budgetAmount: orcamentos.amount,
|
||||||
|
categoriaName: categorias.name,
|
||||||
|
spentAmount: sql<number>`COALESCE(SUM(ABS(${lancamentos.amount})), 0)`,
|
||||||
|
})
|
||||||
|
.from(orcamentos)
|
||||||
|
.innerJoin(categorias, eq(orcamentos.categoriaId, categorias.id))
|
||||||
|
.leftJoin(lancamentos, and(...budgetJoinConditions))
|
||||||
|
.where(
|
||||||
|
and(eq(orcamentos.userId, userId), eq(orcamentos.period, currentPeriod)),
|
||||||
|
)
|
||||||
|
.groupBy(orcamentos.id, orcamentos.amount, categorias.name);
|
||||||
|
|
||||||
|
// =====================
|
||||||
|
// Processar notificações
|
||||||
|
// =====================
|
||||||
|
|
||||||
const notifications: DashboardNotification[] = [];
|
const notifications: DashboardNotification[] = [];
|
||||||
|
|
||||||
// Processar faturas atrasadas (períodos anteriores)
|
// Faturas atrasadas (períodos anteriores)
|
||||||
for (const invoice of overdueInvoices) {
|
for (const invoice of overdueInvoices) {
|
||||||
if (!invoice.period || !invoice.dueDay) continue;
|
if (!invoice.period || !invoice.dueDay) continue;
|
||||||
|
|
||||||
const dueDate = calculateDueDate(invoice.period, invoice.dueDay);
|
const dueDate = calculateDueDate(invoice.period, invoice.dueDay);
|
||||||
const amount =
|
const amount = toNum(invoice.totalAmount);
|
||||||
typeof invoice.totalAmount === "number"
|
|
||||||
? invoice.totalAmount
|
|
||||||
: Number(invoice.totalAmount) || 0;
|
|
||||||
|
|
||||||
const notificationId = invoice.invoiceId
|
const notificationId = invoice.invoiceId
|
||||||
? `invoice-${invoice.invoiceId}`
|
? `invoice-${invoice.invoiceId}`
|
||||||
: `invoice-${invoice.cardId}-${invoice.period}`;
|
: `invoice-${invoice.cardId}-${invoice.period}`;
|
||||||
@@ -259,43 +295,28 @@ export async function fetchDashboardNotifications(
|
|||||||
status: "overdue",
|
status: "overdue",
|
||||||
amount: Math.abs(amount),
|
amount: Math.abs(amount),
|
||||||
period: invoice.period,
|
period: invoice.period,
|
||||||
showAmount: true, // Mostrar valor para itens de períodos anteriores
|
showAmount: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Processar faturas do período atual (atrasadas + vencimento iminente)
|
// Faturas do período atual
|
||||||
for (const invoice of currentInvoices) {
|
for (const invoice of currentInvoices) {
|
||||||
if (!invoice.period || !invoice.dueDay) continue;
|
if (!invoice.period || !invoice.dueDay) continue;
|
||||||
|
const amount = toNum(invoice.totalAmount);
|
||||||
const amount =
|
const transactionCount = toNum(invoice.transactionCount);
|
||||||
typeof invoice.totalAmount === "number"
|
|
||||||
? invoice.totalAmount
|
|
||||||
: Number(invoice.totalAmount) || 0;
|
|
||||||
|
|
||||||
const transactionCount =
|
|
||||||
typeof invoice.transactionCount === "number"
|
|
||||||
? invoice.transactionCount
|
|
||||||
: Number(invoice.transactionCount) || 0;
|
|
||||||
|
|
||||||
const paymentStatus =
|
const paymentStatus =
|
||||||
invoice.paymentStatus ?? INVOICE_PAYMENT_STATUS.PENDING;
|
invoice.paymentStatus ?? INVOICE_PAYMENT_STATUS.PENDING;
|
||||||
|
|
||||||
// Ignora se não tem lançamentos e não tem registro de fatura
|
|
||||||
const shouldInclude =
|
const shouldInclude =
|
||||||
transactionCount > 0 ||
|
transactionCount > 0 ||
|
||||||
Math.abs(amount) > 0 ||
|
Math.abs(amount) > 0 ||
|
||||||
invoice.invoiceId !== null;
|
invoice.invoiceId !== null;
|
||||||
|
|
||||||
if (!shouldInclude) continue;
|
if (!shouldInclude) continue;
|
||||||
|
|
||||||
// Ignora se já foi paga
|
|
||||||
if (paymentStatus === INVOICE_PAYMENT_STATUS.PAID) continue;
|
if (paymentStatus === INVOICE_PAYMENT_STATUS.PAID) continue;
|
||||||
|
|
||||||
const dueDate = calculateDueDate(invoice.period, invoice.dueDay);
|
const dueDate = calculateDueDate(invoice.period, invoice.dueDay);
|
||||||
|
|
||||||
const invoiceIsOverdue = isOverdue(dueDate, today);
|
const invoiceIsOverdue = isOverdue(dueDate, today);
|
||||||
const invoiceIsDueSoon = isDueWithinDays(dueDate, today, DAYS_THRESHOLD);
|
const invoiceIsDueSoon = isDueWithinDays(dueDate, today, DAYS_THRESHOLD);
|
||||||
|
|
||||||
if (!invoiceIsOverdue && !invoiceIsDueSoon) continue;
|
if (!invoiceIsOverdue && !invoiceIsDueSoon) continue;
|
||||||
|
|
||||||
const notificationId = invoice.invoiceId
|
const notificationId = invoice.invoiceId
|
||||||
@@ -314,11 +335,9 @@ export async function fetchDashboardNotifications(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Processar boletos
|
// Boletos
|
||||||
for (const boleto of boletosRows) {
|
for (const boleto of boletosRows) {
|
||||||
if (!boleto.dueDate) continue;
|
if (!boleto.dueDate) continue;
|
||||||
|
|
||||||
// Converter para string no formato YYYY-MM-DD (UTC)
|
|
||||||
const dueDate =
|
const dueDate =
|
||||||
boleto.dueDate instanceof Date
|
boleto.dueDate instanceof Date
|
||||||
? `${boleto.dueDate.getUTCFullYear()}-${String(boleto.dueDate.getUTCMonth() + 1).padStart(2, "0")}-${String(boleto.dueDate.getUTCDate()).padStart(2, "0")}`
|
? `${boleto.dueDate.getUTCFullYear()}-${String(boleto.dueDate.getUTCMonth() + 1).padStart(2, "0")}-${String(boleto.dueDate.getUTCDate()).padStart(2, "0")}`
|
||||||
@@ -326,17 +345,11 @@ export async function fetchDashboardNotifications(
|
|||||||
|
|
||||||
const boletoIsOverdue = isOverdue(dueDate, today);
|
const boletoIsOverdue = isOverdue(dueDate, today);
|
||||||
const boletoIsDueSoon = isDueWithinDays(dueDate, today, DAYS_THRESHOLD);
|
const boletoIsDueSoon = isDueWithinDays(dueDate, today, DAYS_THRESHOLD);
|
||||||
|
|
||||||
const isOldPeriod = boleto.period < currentPeriod;
|
const isOldPeriod = boleto.period < currentPeriod;
|
||||||
const isCurrentPeriod = boleto.period === currentPeriod;
|
const isCurrentPeriod = boleto.period === currentPeriod;
|
||||||
|
const amount = toNum(boleto.amount);
|
||||||
|
|
||||||
// Período anterior: incluir todos (sempre atrasado)
|
|
||||||
if (isOldPeriod) {
|
if (isOldPeriod) {
|
||||||
const amount =
|
|
||||||
typeof boleto.amount === "number"
|
|
||||||
? boleto.amount
|
|
||||||
: Number(boleto.amount) || 0;
|
|
||||||
|
|
||||||
notifications.push({
|
notifications.push({
|
||||||
id: `boleto-${boleto.id}`,
|
id: `boleto-${boleto.id}`,
|
||||||
type: "boleto",
|
type: "boleto",
|
||||||
@@ -345,24 +358,15 @@ export async function fetchDashboardNotifications(
|
|||||||
status: "overdue",
|
status: "overdue",
|
||||||
amount: Math.abs(amount),
|
amount: Math.abs(amount),
|
||||||
period: boleto.period,
|
period: boleto.period,
|
||||||
showAmount: true, // Mostrar valor para períodos anteriores
|
showAmount: true,
|
||||||
});
|
});
|
||||||
}
|
} else if (isCurrentPeriod && (boletoIsOverdue || boletoIsDueSoon)) {
|
||||||
|
|
||||||
// Período atual: incluir atrasados e os que vencem em breve (sem valor)
|
|
||||||
else if (isCurrentPeriod && (boletoIsOverdue || boletoIsDueSoon)) {
|
|
||||||
const status: NotificationType = boletoIsOverdue ? "overdue" : "due_soon";
|
|
||||||
const amount =
|
|
||||||
typeof boleto.amount === "number"
|
|
||||||
? boleto.amount
|
|
||||||
: Number(boleto.amount) || 0;
|
|
||||||
|
|
||||||
notifications.push({
|
notifications.push({
|
||||||
id: `boleto-${boleto.id}`,
|
id: `boleto-${boleto.id}`,
|
||||||
type: "boleto",
|
type: "boleto",
|
||||||
name: boleto.name,
|
name: boleto.name,
|
||||||
dueDate,
|
dueDate,
|
||||||
status,
|
status: boletoIsOverdue ? "overdue" : "due_soon",
|
||||||
amount: Math.abs(amount),
|
amount: Math.abs(amount),
|
||||||
period: boleto.period,
|
period: boleto.period,
|
||||||
showAmount: boletoIsOverdue,
|
showAmount: boletoIsOverdue,
|
||||||
@@ -377,8 +381,37 @@ export async function fetchDashboardNotifications(
|
|||||||
return a.dueDate.localeCompare(b.dueDate);
|
return a.dueDate.localeCompare(b.dueDate);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Orçamentos excedidos e críticos
|
||||||
|
const budgetNotifications: BudgetNotification[] = [];
|
||||||
|
|
||||||
|
for (const row of budgetRows) {
|
||||||
|
const budgetAmount = toNum(row.budgetAmount);
|
||||||
|
const spentAmount = toNum(row.spentAmount);
|
||||||
|
if (budgetAmount <= 0) continue;
|
||||||
|
|
||||||
|
const usedPercentage = (spentAmount / budgetAmount) * 100;
|
||||||
|
if (usedPercentage < BUDGET_CRITICAL_THRESHOLD) continue;
|
||||||
|
|
||||||
|
budgetNotifications.push({
|
||||||
|
id: `budget-${row.orcamentoId}`,
|
||||||
|
categoryName: row.categoriaName,
|
||||||
|
budgetAmount,
|
||||||
|
spentAmount,
|
||||||
|
usedPercentage,
|
||||||
|
status: usedPercentage >= 100 ? "exceeded" : "critical",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Excedidos primeiro, depois por percentual decrescente
|
||||||
|
budgetNotifications.sort((a, b) => {
|
||||||
|
if (a.status === "exceeded" && b.status !== "exceeded") return -1;
|
||||||
|
if (a.status !== "exceeded" && b.status === "exceeded") return 1;
|
||||||
|
return b.usedPercentage - a.usedPercentage;
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
notifications,
|
notifications,
|
||||||
totalCount: notifications.length,
|
totalCount: notifications.length,
|
||||||
|
budgetNotifications,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import { ptBR } from "date-fns/locale";
|
import { ptBR } from "date-fns/locale";
|
||||||
import { and, eq, inArray, isNull, or, sql } from "drizzle-orm";
|
import { and, eq, inArray, isNull, or, sql } from "drizzle-orm";
|
||||||
import { categorias, lancamentos, pagadores } from "@/db/schema";
|
import { categorias, lancamentos } from "@/db/schema";
|
||||||
import { ACCOUNT_AUTO_INVOICE_NOTE_PREFIX } from "@/lib/accounts/constants";
|
import { ACCOUNT_AUTO_INVOICE_NOTE_PREFIX } from "@/lib/accounts/constants";
|
||||||
import { toNumber } from "@/lib/dashboard/common";
|
import { toNumber } from "@/lib/dashboard/common";
|
||||||
import { db } from "@/lib/db";
|
import { db } from "@/lib/db";
|
||||||
import { PAGADOR_ROLE_ADMIN } from "@/lib/pagadores/constants";
|
import { getAdminPagadorId } from "@/lib/pagadores/get-admin-id";
|
||||||
import { generatePeriodRange } from "./utils";
|
import { generatePeriodRange } from "./utils";
|
||||||
|
|
||||||
export type CategoryChartData = {
|
export type CategoryChartData = {
|
||||||
@@ -34,14 +34,17 @@ export async function fetchCategoryChartData(
|
|||||||
endPeriod: string,
|
endPeriod: string,
|
||||||
categoryIds?: string[],
|
categoryIds?: string[],
|
||||||
): Promise<CategoryChartData> {
|
): Promise<CategoryChartData> {
|
||||||
// Generate all periods in the range
|
|
||||||
const periods = generatePeriodRange(startPeriod, endPeriod);
|
const periods = generatePeriodRange(startPeriod, endPeriod);
|
||||||
|
|
||||||
// Build WHERE conditions
|
const adminPagadorId = await getAdminPagadorId(userId);
|
||||||
|
if (!adminPagadorId) {
|
||||||
|
return { months: [], categories: [], chartData: [], allCategories: [] };
|
||||||
|
}
|
||||||
|
|
||||||
const whereConditions = [
|
const whereConditions = [
|
||||||
eq(lancamentos.userId, userId),
|
eq(lancamentos.userId, userId),
|
||||||
|
eq(lancamentos.pagadorId, adminPagadorId),
|
||||||
inArray(lancamentos.period, periods),
|
inArray(lancamentos.period, periods),
|
||||||
eq(pagadores.role, PAGADOR_ROLE_ADMIN),
|
|
||||||
or(eq(categorias.type, "despesa"), eq(categorias.type, "receita")),
|
or(eq(categorias.type, "despesa"), eq(categorias.type, "receita")),
|
||||||
or(
|
or(
|
||||||
isNull(lancamentos.note),
|
isNull(lancamentos.note),
|
||||||
@@ -49,13 +52,12 @@ export async function fetchCategoryChartData(
|
|||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
// Add optional category filter
|
|
||||||
if (categoryIds && categoryIds.length > 0) {
|
if (categoryIds && categoryIds.length > 0) {
|
||||||
whereConditions.push(inArray(categorias.id, categoryIds));
|
whereConditions.push(inArray(categorias.id, categoryIds));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Query to get aggregated data by category and period
|
const [rows, allCategoriesRows] = await Promise.all([
|
||||||
const rows = await db
|
db
|
||||||
.select({
|
.select({
|
||||||
categoryId: categorias.id,
|
categoryId: categorias.id,
|
||||||
categoryName: categorias.name,
|
categoryName: categorias.name,
|
||||||
@@ -65,7 +67,6 @@ export async function fetchCategoryChartData(
|
|||||||
total: sql<number>`coalesce(sum(abs(${lancamentos.amount})), 0)`,
|
total: sql<number>`coalesce(sum(abs(${lancamentos.amount})), 0)`,
|
||||||
})
|
})
|
||||||
.from(lancamentos)
|
.from(lancamentos)
|
||||||
.innerJoin(pagadores, eq(lancamentos.pagadorId, pagadores.id))
|
|
||||||
.innerJoin(categorias, eq(lancamentos.categoriaId, categorias.id))
|
.innerJoin(categorias, eq(lancamentos.categoriaId, categorias.id))
|
||||||
.where(and(...whereConditions))
|
.where(and(...whereConditions))
|
||||||
.groupBy(
|
.groupBy(
|
||||||
@@ -74,10 +75,8 @@ export async function fetchCategoryChartData(
|
|||||||
categorias.icon,
|
categorias.icon,
|
||||||
categorias.type,
|
categorias.type,
|
||||||
lancamentos.period,
|
lancamentos.period,
|
||||||
);
|
),
|
||||||
|
db
|
||||||
// Fetch all categories for the user (for category selection)
|
|
||||||
const allCategoriesRows = await db
|
|
||||||
.select({
|
.select({
|
||||||
id: categorias.id,
|
id: categorias.id,
|
||||||
name: categorias.name,
|
name: categorias.name,
|
||||||
@@ -86,9 +85,9 @@ export async function fetchCategoryChartData(
|
|||||||
})
|
})
|
||||||
.from(categorias)
|
.from(categorias)
|
||||||
.where(eq(categorias.userId, userId))
|
.where(eq(categorias.userId, userId))
|
||||||
.orderBy(categorias.type, categorias.name);
|
.orderBy(categorias.type, categorias.name),
|
||||||
|
]);
|
||||||
|
|
||||||
// Map all categories
|
|
||||||
const allCategories = allCategoriesRows.map(
|
const allCategories = allCategoriesRows.map(
|
||||||
(cat: { id: string; name: string; icon: string | null; type: string }) => ({
|
(cat: { id: string; name: string; icon: string | null; type: string }) => ({
|
||||||
id: cat.id,
|
id: cat.id,
|
||||||
@@ -98,7 +97,6 @@ export async function fetchCategoryChartData(
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Process results into chart format
|
|
||||||
const categoryMap = new Map<
|
const categoryMap = new Map<
|
||||||
string,
|
string,
|
||||||
{
|
{
|
||||||
@@ -110,7 +108,6 @@ export async function fetchCategoryChartData(
|
|||||||
}
|
}
|
||||||
>();
|
>();
|
||||||
|
|
||||||
// Process each row
|
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
const amount = Math.abs(toNumber(row.total));
|
const amount = Math.abs(toNumber(row.total));
|
||||||
const { categoryId, categoryName, categoryIcon, categoryType, period } =
|
const { categoryId, categoryName, categoryIcon, categoryType, period } =
|
||||||
@@ -126,37 +123,31 @@ export async function fetchCategoryChartData(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const categoryItem = categoryMap.get(categoryId)!;
|
categoryMap.get(categoryId)!.dataByPeriod.set(period, amount);
|
||||||
categoryItem.dataByPeriod.set(period, amount);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build chart data
|
|
||||||
const chartData = periods.map((period) => {
|
const chartData = periods.map((period) => {
|
||||||
const [year, month] = period.split("-");
|
const [year, month] = period.split("-");
|
||||||
const date = new Date(parseInt(year, 10), parseInt(month, 10) - 1, 1);
|
const date = new Date(Number.parseInt(year, 10), Number.parseInt(month, 10) - 1, 1);
|
||||||
const monthLabel = format(date, "MMM", { locale: ptBR }).toUpperCase();
|
const monthLabel = format(date, "MMM", { locale: ptBR }).toUpperCase();
|
||||||
|
|
||||||
const dataPoint: { month: string; [key: string]: number | string } = {
|
const dataPoint: { month: string; [key: string]: number | string } = {
|
||||||
month: monthLabel,
|
month: monthLabel,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add data for each category
|
|
||||||
for (const category of categoryMap.values()) {
|
for (const category of categoryMap.values()) {
|
||||||
const value = category.dataByPeriod.get(period) ?? 0;
|
dataPoint[category.name] = category.dataByPeriod.get(period) ?? 0;
|
||||||
dataPoint[category.name] = value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return dataPoint;
|
return dataPoint;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Generate month labels
|
|
||||||
const months = periods.map((period) => {
|
const months = periods.map((period) => {
|
||||||
const [year, month] = period.split("-");
|
const [year, month] = period.split("-");
|
||||||
const date = new Date(parseInt(year, 10), parseInt(month, 10) - 1, 1);
|
const date = new Date(Number.parseInt(year, 10), Number.parseInt(month, 10) - 1, 1);
|
||||||
return format(date, "MMM", { locale: ptBR }).toUpperCase();
|
return format(date, "MMM", { locale: ptBR }).toUpperCase();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Build categories array
|
|
||||||
const categories = Array.from(categoryMap.values()).map((cat) => ({
|
const categories = Array.from(categoryMap.values()).map((cat) => ({
|
||||||
id: cat.id,
|
id: cat.id,
|
||||||
name: cat.name,
|
name: cat.name,
|
||||||
@@ -164,10 +155,5 @@ export async function fetchCategoryChartData(
|
|||||||
type: cat.type,
|
type: cat.type,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return {
|
return { months, categories, chartData, allCategories };
|
||||||
months,
|
|
||||||
categories,
|
|
||||||
chartData,
|
|
||||||
allCategories,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { and, eq, inArray, isNull, or, sql } from "drizzle-orm";
|
import { and, eq, inArray, isNull, or, sql } from "drizzle-orm";
|
||||||
import { categorias, lancamentos, pagadores } from "@/db/schema";
|
import { categorias, lancamentos } from "@/db/schema";
|
||||||
import { ACCOUNT_AUTO_INVOICE_NOTE_PREFIX } from "@/lib/accounts/constants";
|
import { ACCOUNT_AUTO_INVOICE_NOTE_PREFIX } from "@/lib/accounts/constants";
|
||||||
import { toNumber } from "@/lib/dashboard/common";
|
import { toNumber } from "@/lib/dashboard/common";
|
||||||
import { db } from "@/lib/db";
|
import { db } from "@/lib/db";
|
||||||
import { PAGADOR_ROLE_ADMIN } from "@/lib/pagadores/constants";
|
import { getAdminPagadorId } from "@/lib/pagadores/get-admin-id";
|
||||||
import type {
|
import type {
|
||||||
CategoryReportData,
|
CategoryReportData,
|
||||||
CategoryReportFilters,
|
CategoryReportFilters,
|
||||||
@@ -28,11 +28,16 @@ export async function fetchCategoryReport(
|
|||||||
// Generate all periods in the range
|
// Generate all periods in the range
|
||||||
const periods = generatePeriodRange(startPeriod, endPeriod);
|
const periods = generatePeriodRange(startPeriod, endPeriod);
|
||||||
|
|
||||||
|
const adminPagadorId = await getAdminPagadorId(userId);
|
||||||
|
if (!adminPagadorId) {
|
||||||
|
return { categories: [], periods, totals: new Map(), grandTotal: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
// Build WHERE conditions
|
// Build WHERE conditions
|
||||||
const whereConditions = [
|
const whereConditions = [
|
||||||
eq(lancamentos.userId, userId),
|
eq(lancamentos.userId, userId),
|
||||||
|
eq(lancamentos.pagadorId, adminPagadorId),
|
||||||
inArray(lancamentos.period, periods),
|
inArray(lancamentos.period, periods),
|
||||||
eq(pagadores.role, PAGADOR_ROLE_ADMIN),
|
|
||||||
or(eq(categorias.type, "despesa"), eq(categorias.type, "receita")),
|
or(eq(categorias.type, "despesa"), eq(categorias.type, "receita")),
|
||||||
or(
|
or(
|
||||||
isNull(lancamentos.note),
|
isNull(lancamentos.note),
|
||||||
@@ -56,7 +61,6 @@ export async function fetchCategoryReport(
|
|||||||
total: sql<number>`coalesce(sum(${lancamentos.amount}), 0)`,
|
total: sql<number>`coalesce(sum(${lancamentos.amount}), 0)`,
|
||||||
})
|
})
|
||||||
.from(lancamentos)
|
.from(lancamentos)
|
||||||
.innerJoin(pagadores, eq(lancamentos.pagadorId, pagadores.id))
|
|
||||||
.innerJoin(categorias, eq(lancamentos.categoriaId, categorias.id))
|
.innerJoin(categorias, eq(lancamentos.categoriaId, categorias.id))
|
||||||
.where(and(...whereConditions))
|
.where(and(...whereConditions))
|
||||||
.groupBy(
|
.groupBy(
|
||||||
|
|||||||
@@ -72,6 +72,7 @@
|
|||||||
"next": "16.1.6",
|
"next": "16.1.6",
|
||||||
"next-themes": "0.4.6",
|
"next-themes": "0.4.6",
|
||||||
"pg": "8.18.0",
|
"pg": "8.18.0",
|
||||||
|
"radix-ui": "^1.4.3",
|
||||||
"react": "19.2.4",
|
"react": "19.2.4",
|
||||||
"react-day-picker": "^9.13.2",
|
"react-day-picker": "^9.13.2",
|
||||||
"react-dom": "19.2.4",
|
"react-dom": "19.2.4",
|
||||||
|
|||||||
491
pnpm-lock.yaml
generated
491
pnpm-lock.yaml
generated
@@ -143,6 +143,9 @@ importers:
|
|||||||
pg:
|
pg:
|
||||||
specifier: 8.18.0
|
specifier: 8.18.0
|
||||||
version: 8.18.0
|
version: 8.18.0
|
||||||
|
radix-ui:
|
||||||
|
specifier: ^1.4.3
|
||||||
|
version: 1.4.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
react:
|
react:
|
||||||
specifier: 19.2.4
|
specifier: 19.2.4
|
||||||
version: 19.2.4
|
version: 19.2.4
|
||||||
@@ -1089,6 +1092,19 @@ packages:
|
|||||||
'@radix-ui/primitive@1.1.3':
|
'@radix-ui/primitive@1.1.3':
|
||||||
resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==}
|
resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==}
|
||||||
|
|
||||||
|
'@radix-ui/react-accessible-icon@1.1.7':
|
||||||
|
resolution: {integrity: sha512-XM+E4WXl0OqUJFovy6GjmxxFyx9opfCAIUku4dlKRd5YEPqt4kALOkQOp0Of6reHuUkJuiPBEc5k0o4z4lTC8A==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
'@types/react-dom': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
'@types/react-dom':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@radix-ui/react-accordion@1.2.12':
|
'@radix-ui/react-accordion@1.2.12':
|
||||||
resolution: {integrity: sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==}
|
resolution: {integrity: sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -1128,6 +1144,32 @@ packages:
|
|||||||
'@types/react-dom':
|
'@types/react-dom':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@radix-ui/react-aspect-ratio@1.1.7':
|
||||||
|
resolution: {integrity: sha512-Yq6lvO9HQyPwev1onK1daHCHqXVLzPhSVjmsNjCa2Zcxy2f7uJD2itDtxknv6FzAKCwD1qQkeVDmX/cev13n/g==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
'@types/react-dom': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
'@types/react-dom':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@radix-ui/react-avatar@1.1.10':
|
||||||
|
resolution: {integrity: sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
'@types/react-dom': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
'@types/react-dom':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@radix-ui/react-avatar@1.1.11':
|
'@radix-ui/react-avatar@1.1.11':
|
||||||
resolution: {integrity: sha512-0Qk603AHGV28BOBO34p7IgD5m+V5Sg/YovfayABkoDDBM5d3NCx0Mp4gGrjzLGes1jV5eNOE1r3itqOR33VC6Q==}
|
resolution: {integrity: sha512-0Qk603AHGV28BOBO34p7IgD5m+V5Sg/YovfayABkoDDBM5d3NCx0Mp4gGrjzLGes1jV5eNOE1r3itqOR33VC6Q==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -1189,6 +1231,19 @@ packages:
|
|||||||
'@types/react':
|
'@types/react':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@radix-ui/react-context-menu@2.2.16':
|
||||||
|
resolution: {integrity: sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
'@types/react-dom': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
'@types/react-dom':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@radix-ui/react-context@1.1.2':
|
'@radix-ui/react-context@1.1.2':
|
||||||
resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==}
|
resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -1277,6 +1332,19 @@ packages:
|
|||||||
'@types/react-dom':
|
'@types/react-dom':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@radix-ui/react-form@0.1.8':
|
||||||
|
resolution: {integrity: sha512-QM70k4Zwjttifr5a4sZFts9fn8FzHYvQ5PiB19O2HsYibaHSVt9fH9rzB0XZo/YcM+b7t/p7lYCT/F5eOeF5yQ==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
'@types/react-dom': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
'@types/react-dom':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@radix-ui/react-hover-card@1.1.15':
|
'@radix-ui/react-hover-card@1.1.15':
|
||||||
resolution: {integrity: sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg==}
|
resolution: {integrity: sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -1299,6 +1367,19 @@ packages:
|
|||||||
'@types/react':
|
'@types/react':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@radix-ui/react-label@2.1.7':
|
||||||
|
resolution: {integrity: sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
'@types/react-dom': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
'@types/react-dom':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@radix-ui/react-label@2.1.8':
|
'@radix-ui/react-label@2.1.8':
|
||||||
resolution: {integrity: sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==}
|
resolution: {integrity: sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -1325,6 +1406,58 @@ packages:
|
|||||||
'@types/react-dom':
|
'@types/react-dom':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@radix-ui/react-menubar@1.1.16':
|
||||||
|
resolution: {integrity: sha512-EB1FktTz5xRRi2Er974AUQZWg2yVBb1yjip38/lgwtCVRd3a+maUoGHN/xs9Yv8SY8QwbSEb+YrxGadVWbEutA==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
'@types/react-dom': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
'@types/react-dom':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@radix-ui/react-navigation-menu@1.2.14':
|
||||||
|
resolution: {integrity: sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
'@types/react-dom': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
'@types/react-dom':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@radix-ui/react-one-time-password-field@0.1.8':
|
||||||
|
resolution: {integrity: sha512-ycS4rbwURavDPVjCb5iS3aG4lURFDILi6sKI/WITUMZ13gMmn/xGjpLoqBAalhJaDk8I3UbCM5GzKHrnzwHbvg==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
'@types/react-dom': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
'@types/react-dom':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@radix-ui/react-password-toggle-field@0.1.3':
|
||||||
|
resolution: {integrity: sha512-/UuCrDBWravcaMix4TdT+qlNdVwOM1Nck9kWx/vafXsdfj1ChfhOdfi3cy9SGBpWgTXwYCuboT/oYpJy3clqfw==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
'@types/react-dom': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
'@types/react-dom':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@radix-ui/react-popover@1.1.15':
|
'@radix-ui/react-popover@1.1.15':
|
||||||
resolution: {integrity: sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==}
|
resolution: {integrity: sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -1403,6 +1536,19 @@ packages:
|
|||||||
'@types/react-dom':
|
'@types/react-dom':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@radix-ui/react-progress@1.1.7':
|
||||||
|
resolution: {integrity: sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
'@types/react-dom': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
'@types/react-dom':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@radix-ui/react-progress@1.1.8':
|
'@radix-ui/react-progress@1.1.8':
|
||||||
resolution: {integrity: sha512-+gISHcSPUJ7ktBy9RnTqbdKW78bcGke3t6taawyZ71pio1JewwGSJizycs7rLhGTvMJYCQB1DBK4KQsxs7U8dA==}
|
resolution: {integrity: sha512-+gISHcSPUJ7ktBy9RnTqbdKW78bcGke3t6taawyZ71pio1JewwGSJizycs7rLhGTvMJYCQB1DBK4KQsxs7U8dA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -1468,6 +1614,19 @@ packages:
|
|||||||
'@types/react-dom':
|
'@types/react-dom':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@radix-ui/react-separator@1.1.7':
|
||||||
|
resolution: {integrity: sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
'@types/react-dom': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
'@types/react-dom':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@radix-ui/react-separator@1.1.8':
|
'@radix-ui/react-separator@1.1.8':
|
||||||
resolution: {integrity: sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g==}
|
resolution: {integrity: sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -1481,6 +1640,19 @@ packages:
|
|||||||
'@types/react-dom':
|
'@types/react-dom':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@radix-ui/react-slider@1.3.6':
|
||||||
|
resolution: {integrity: sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
'@types/react-dom': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
'@types/react-dom':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@radix-ui/react-slot@1.2.3':
|
'@radix-ui/react-slot@1.2.3':
|
||||||
resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==}
|
resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -1525,6 +1697,19 @@ packages:
|
|||||||
'@types/react-dom':
|
'@types/react-dom':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@radix-ui/react-toast@1.2.15':
|
||||||
|
resolution: {integrity: sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
'@types/react-dom': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
'@types/react-dom':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@radix-ui/react-toggle-group@1.1.11':
|
'@radix-ui/react-toggle-group@1.1.11':
|
||||||
resolution: {integrity: sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==}
|
resolution: {integrity: sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -1551,6 +1736,19 @@ packages:
|
|||||||
'@types/react-dom':
|
'@types/react-dom':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@radix-ui/react-toolbar@1.1.11':
|
||||||
|
resolution: {integrity: sha512-4ol06/1bLoFu1nwUqzdD4Y5RZ9oDdKeiHIsntug54Hcr1pgaHiPqHFEaXI1IFP/EsOfROQZ8Mig9VTIRza6Tjg==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
'@types/react-dom': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
'@types/react-dom':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@radix-ui/react-tooltip@1.2.8':
|
'@radix-ui/react-tooltip@1.2.8':
|
||||||
resolution: {integrity: sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==}
|
resolution: {integrity: sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -2503,6 +2701,19 @@ packages:
|
|||||||
resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==}
|
resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
radix-ui@1.4.3:
|
||||||
|
resolution: {integrity: sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
'@types/react-dom': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
'@types/react-dom':
|
||||||
|
optional: true
|
||||||
|
|
||||||
raf@3.4.1:
|
raf@3.4.1:
|
||||||
resolution: {integrity: sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==}
|
resolution: {integrity: sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==}
|
||||||
|
|
||||||
@@ -3314,6 +3525,15 @@ snapshots:
|
|||||||
|
|
||||||
'@radix-ui/primitive@1.1.3': {}
|
'@radix-ui/primitive@1.1.3': {}
|
||||||
|
|
||||||
|
'@radix-ui/react-accessible-icon@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
|
||||||
|
dependencies:
|
||||||
|
'@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
react: 19.2.4
|
||||||
|
react-dom: 19.2.4(react@19.2.4)
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 19.2.14
|
||||||
|
'@types/react-dom': 19.2.3(@types/react@19.2.14)
|
||||||
|
|
||||||
'@radix-ui/react-accordion@1.2.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
|
'@radix-ui/react-accordion@1.2.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/primitive': 1.1.3
|
'@radix-ui/primitive': 1.1.3
|
||||||
@@ -3354,6 +3574,28 @@ snapshots:
|
|||||||
'@types/react': 19.2.14
|
'@types/react': 19.2.14
|
||||||
'@types/react-dom': 19.2.3(@types/react@19.2.14)
|
'@types/react-dom': 19.2.3(@types/react@19.2.14)
|
||||||
|
|
||||||
|
'@radix-ui/react-aspect-ratio@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
|
||||||
|
dependencies:
|
||||||
|
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
react: 19.2.4
|
||||||
|
react-dom: 19.2.4(react@19.2.4)
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 19.2.14
|
||||||
|
'@types/react-dom': 19.2.3(@types/react@19.2.14)
|
||||||
|
|
||||||
|
'@radix-ui/react-avatar@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
|
||||||
|
dependencies:
|
||||||
|
'@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4)
|
||||||
|
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4)
|
||||||
|
'@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.14)(react@19.2.4)
|
||||||
|
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4)
|
||||||
|
react: 19.2.4
|
||||||
|
react-dom: 19.2.4(react@19.2.4)
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 19.2.14
|
||||||
|
'@types/react-dom': 19.2.3(@types/react@19.2.14)
|
||||||
|
|
||||||
'@radix-ui/react-avatar@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
|
'@radix-ui/react-avatar@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/react-context': 1.1.3(@types/react@19.2.14)(react@19.2.4)
|
'@radix-ui/react-context': 1.1.3(@types/react@19.2.14)(react@19.2.4)
|
||||||
@@ -3417,6 +3659,20 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.2.14
|
'@types/react': 19.2.14
|
||||||
|
|
||||||
|
'@radix-ui/react-context-menu@2.2.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
|
||||||
|
dependencies:
|
||||||
|
'@radix-ui/primitive': 1.1.3
|
||||||
|
'@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4)
|
||||||
|
'@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4)
|
||||||
|
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4)
|
||||||
|
react: 19.2.4
|
||||||
|
react-dom: 19.2.4(react@19.2.4)
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 19.2.14
|
||||||
|
'@types/react-dom': 19.2.3(@types/react@19.2.14)
|
||||||
|
|
||||||
'@radix-ui/react-context@1.1.2(@types/react@19.2.14)(react@19.2.4)':
|
'@radix-ui/react-context@1.1.2(@types/react@19.2.14)(react@19.2.4)':
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 19.2.4
|
react: 19.2.4
|
||||||
@@ -3502,6 +3758,20 @@ snapshots:
|
|||||||
'@types/react': 19.2.14
|
'@types/react': 19.2.14
|
||||||
'@types/react-dom': 19.2.3(@types/react@19.2.14)
|
'@types/react-dom': 19.2.3(@types/react@19.2.14)
|
||||||
|
|
||||||
|
'@radix-ui/react-form@0.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
|
||||||
|
dependencies:
|
||||||
|
'@radix-ui/primitive': 1.1.3
|
||||||
|
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
|
||||||
|
'@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4)
|
||||||
|
'@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4)
|
||||||
|
'@radix-ui/react-label': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
react: 19.2.4
|
||||||
|
react-dom: 19.2.4(react@19.2.4)
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 19.2.14
|
||||||
|
'@types/react-dom': 19.2.3(@types/react@19.2.14)
|
||||||
|
|
||||||
'@radix-ui/react-hover-card@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
|
'@radix-ui/react-hover-card@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/primitive': 1.1.3
|
'@radix-ui/primitive': 1.1.3
|
||||||
@@ -3526,6 +3796,15 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.2.14
|
'@types/react': 19.2.14
|
||||||
|
|
||||||
|
'@radix-ui/react-label@2.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
|
||||||
|
dependencies:
|
||||||
|
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
react: 19.2.4
|
||||||
|
react-dom: 19.2.4(react@19.2.4)
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 19.2.14
|
||||||
|
'@types/react-dom': 19.2.3(@types/react@19.2.14)
|
||||||
|
|
||||||
'@radix-ui/react-label@2.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
|
'@radix-ui/react-label@2.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/react-primitive': 2.1.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
'@radix-ui/react-primitive': 2.1.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
@@ -3561,6 +3840,82 @@ snapshots:
|
|||||||
'@types/react': 19.2.14
|
'@types/react': 19.2.14
|
||||||
'@types/react-dom': 19.2.3(@types/react@19.2.14)
|
'@types/react-dom': 19.2.3(@types/react@19.2.14)
|
||||||
|
|
||||||
|
'@radix-ui/react-menubar@1.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
|
||||||
|
dependencies:
|
||||||
|
'@radix-ui/primitive': 1.1.3
|
||||||
|
'@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
|
||||||
|
'@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4)
|
||||||
|
'@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4)
|
||||||
|
'@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4)
|
||||||
|
'@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
'@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4)
|
||||||
|
react: 19.2.4
|
||||||
|
react-dom: 19.2.4(react@19.2.4)
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 19.2.14
|
||||||
|
'@types/react-dom': 19.2.3(@types/react@19.2.14)
|
||||||
|
|
||||||
|
'@radix-ui/react-navigation-menu@1.2.14(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
|
||||||
|
dependencies:
|
||||||
|
'@radix-ui/primitive': 1.1.3
|
||||||
|
'@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
|
||||||
|
'@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4)
|
||||||
|
'@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4)
|
||||||
|
'@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
'@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4)
|
||||||
|
'@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4)
|
||||||
|
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4)
|
||||||
|
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4)
|
||||||
|
'@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.4)
|
||||||
|
'@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
react: 19.2.4
|
||||||
|
react-dom: 19.2.4(react@19.2.4)
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 19.2.14
|
||||||
|
'@types/react-dom': 19.2.3(@types/react@19.2.14)
|
||||||
|
|
||||||
|
'@radix-ui/react-one-time-password-field@0.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
|
||||||
|
dependencies:
|
||||||
|
'@radix-ui/number': 1.1.1
|
||||||
|
'@radix-ui/primitive': 1.1.3
|
||||||
|
'@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
|
||||||
|
'@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4)
|
||||||
|
'@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4)
|
||||||
|
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
'@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4)
|
||||||
|
'@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.2.4)
|
||||||
|
'@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.14)(react@19.2.4)
|
||||||
|
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4)
|
||||||
|
react: 19.2.4
|
||||||
|
react-dom: 19.2.4(react@19.2.4)
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 19.2.14
|
||||||
|
'@types/react-dom': 19.2.3(@types/react@19.2.14)
|
||||||
|
|
||||||
|
'@radix-ui/react-password-toggle-field@0.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
|
||||||
|
dependencies:
|
||||||
|
'@radix-ui/primitive': 1.1.3
|
||||||
|
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
|
||||||
|
'@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4)
|
||||||
|
'@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4)
|
||||||
|
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4)
|
||||||
|
'@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.2.4)
|
||||||
|
'@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.14)(react@19.2.4)
|
||||||
|
react: 19.2.4
|
||||||
|
react-dom: 19.2.4(react@19.2.4)
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 19.2.14
|
||||||
|
'@types/react-dom': 19.2.3(@types/react@19.2.14)
|
||||||
|
|
||||||
'@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
|
'@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/primitive': 1.1.3
|
'@radix-ui/primitive': 1.1.3
|
||||||
@@ -3640,6 +3995,16 @@ snapshots:
|
|||||||
'@types/react': 19.2.14
|
'@types/react': 19.2.14
|
||||||
'@types/react-dom': 19.2.3(@types/react@19.2.14)
|
'@types/react-dom': 19.2.3(@types/react@19.2.14)
|
||||||
|
|
||||||
|
'@radix-ui/react-progress@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
|
||||||
|
dependencies:
|
||||||
|
'@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4)
|
||||||
|
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
react: 19.2.4
|
||||||
|
react-dom: 19.2.4(react@19.2.4)
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 19.2.14
|
||||||
|
'@types/react-dom': 19.2.3(@types/react@19.2.14)
|
||||||
|
|
||||||
'@radix-ui/react-progress@1.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
|
'@radix-ui/react-progress@1.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/react-context': 1.1.3(@types/react@19.2.14)(react@19.2.4)
|
'@radix-ui/react-context': 1.1.3(@types/react@19.2.14)(react@19.2.4)
|
||||||
@@ -3731,6 +4096,15 @@ snapshots:
|
|||||||
'@types/react': 19.2.14
|
'@types/react': 19.2.14
|
||||||
'@types/react-dom': 19.2.3(@types/react@19.2.14)
|
'@types/react-dom': 19.2.3(@types/react@19.2.14)
|
||||||
|
|
||||||
|
'@radix-ui/react-separator@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
|
||||||
|
dependencies:
|
||||||
|
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
react: 19.2.4
|
||||||
|
react-dom: 19.2.4(react@19.2.4)
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 19.2.14
|
||||||
|
'@types/react-dom': 19.2.3(@types/react@19.2.14)
|
||||||
|
|
||||||
'@radix-ui/react-separator@1.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
|
'@radix-ui/react-separator@1.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/react-primitive': 2.1.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
'@radix-ui/react-primitive': 2.1.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
@@ -3740,6 +4114,25 @@ snapshots:
|
|||||||
'@types/react': 19.2.14
|
'@types/react': 19.2.14
|
||||||
'@types/react-dom': 19.2.3(@types/react@19.2.14)
|
'@types/react-dom': 19.2.3(@types/react@19.2.14)
|
||||||
|
|
||||||
|
'@radix-ui/react-slider@1.3.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
|
||||||
|
dependencies:
|
||||||
|
'@radix-ui/number': 1.1.1
|
||||||
|
'@radix-ui/primitive': 1.1.3
|
||||||
|
'@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
|
||||||
|
'@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4)
|
||||||
|
'@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4)
|
||||||
|
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4)
|
||||||
|
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4)
|
||||||
|
'@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.4)
|
||||||
|
'@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.4)
|
||||||
|
react: 19.2.4
|
||||||
|
react-dom: 19.2.4(react@19.2.4)
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 19.2.14
|
||||||
|
'@types/react-dom': 19.2.3(@types/react@19.2.14)
|
||||||
|
|
||||||
'@radix-ui/react-slot@1.2.3(@types/react@19.2.14)(react@19.2.4)':
|
'@radix-ui/react-slot@1.2.3(@types/react@19.2.14)(react@19.2.4)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
|
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
|
||||||
@@ -3785,6 +4178,26 @@ snapshots:
|
|||||||
'@types/react': 19.2.14
|
'@types/react': 19.2.14
|
||||||
'@types/react-dom': 19.2.3(@types/react@19.2.14)
|
'@types/react-dom': 19.2.3(@types/react@19.2.14)
|
||||||
|
|
||||||
|
'@radix-ui/react-toast@1.2.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
|
||||||
|
dependencies:
|
||||||
|
'@radix-ui/primitive': 1.1.3
|
||||||
|
'@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
|
||||||
|
'@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4)
|
||||||
|
'@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
'@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
'@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4)
|
||||||
|
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4)
|
||||||
|
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4)
|
||||||
|
'@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
react: 19.2.4
|
||||||
|
react-dom: 19.2.4(react@19.2.4)
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 19.2.14
|
||||||
|
'@types/react-dom': 19.2.3(@types/react@19.2.14)
|
||||||
|
|
||||||
'@radix-ui/react-toggle-group@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
|
'@radix-ui/react-toggle-group@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/primitive': 1.1.3
|
'@radix-ui/primitive': 1.1.3
|
||||||
@@ -3811,6 +4224,21 @@ snapshots:
|
|||||||
'@types/react': 19.2.14
|
'@types/react': 19.2.14
|
||||||
'@types/react-dom': 19.2.3(@types/react@19.2.14)
|
'@types/react-dom': 19.2.3(@types/react@19.2.14)
|
||||||
|
|
||||||
|
'@radix-ui/react-toolbar@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
|
||||||
|
dependencies:
|
||||||
|
'@radix-ui/primitive': 1.1.3
|
||||||
|
'@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4)
|
||||||
|
'@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4)
|
||||||
|
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
'@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
'@radix-ui/react-separator': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
'@radix-ui/react-toggle-group': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
react: 19.2.4
|
||||||
|
react-dom: 19.2.4(react@19.2.4)
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 19.2.14
|
||||||
|
'@types/react-dom': 19.2.3(@types/react@19.2.14)
|
||||||
|
|
||||||
'@radix-ui/react-tooltip@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
|
'@radix-ui/react-tooltip@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/primitive': 1.1.3
|
'@radix-ui/primitive': 1.1.3
|
||||||
@@ -4573,6 +5001,69 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
xtend: 4.0.2
|
xtend: 4.0.2
|
||||||
|
|
||||||
|
radix-ui@1.4.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
|
||||||
|
dependencies:
|
||||||
|
'@radix-ui/primitive': 1.1.3
|
||||||
|
'@radix-ui/react-accessible-icon': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
'@radix-ui/react-accordion': 1.2.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
'@radix-ui/react-alert-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
'@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
'@radix-ui/react-aspect-ratio': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
'@radix-ui/react-avatar': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
'@radix-ui/react-checkbox': 1.3.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
'@radix-ui/react-collapsible': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
'@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
|
||||||
|
'@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4)
|
||||||
|
'@radix-ui/react-context-menu': 2.2.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
'@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
'@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4)
|
||||||
|
'@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
'@radix-ui/react-dropdown-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
'@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.4)
|
||||||
|
'@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
'@radix-ui/react-form': 0.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
'@radix-ui/react-hover-card': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
'@radix-ui/react-label': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
'@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
'@radix-ui/react-menubar': 1.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
'@radix-ui/react-navigation-menu': 1.2.14(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
'@radix-ui/react-one-time-password-field': 0.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
'@radix-ui/react-password-toggle-field': 0.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
'@radix-ui/react-popover': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
'@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
'@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
'@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
'@radix-ui/react-progress': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
'@radix-ui/react-radio-group': 1.3.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
'@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
'@radix-ui/react-scroll-area': 1.2.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
'@radix-ui/react-select': 2.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
'@radix-ui/react-separator': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
'@radix-ui/react-slider': 1.3.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
'@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4)
|
||||||
|
'@radix-ui/react-switch': 1.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
'@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
'@radix-ui/react-toast': 1.2.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
'@radix-ui/react-toggle': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
'@radix-ui/react-toggle-group': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
'@radix-ui/react-toolbar': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
'@radix-ui/react-tooltip': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4)
|
||||||
|
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4)
|
||||||
|
'@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.2.4)
|
||||||
|
'@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.14)(react@19.2.4)
|
||||||
|
'@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.14)(react@19.2.4)
|
||||||
|
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4)
|
||||||
|
'@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.4)
|
||||||
|
'@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
react: 19.2.4
|
||||||
|
react-dom: 19.2.4(react@19.2.4)
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 19.2.14
|
||||||
|
'@types/react-dom': 19.2.3(@types/react@19.2.14)
|
||||||
|
|
||||||
raf@3.4.1:
|
raf@3.4.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
performance-now: 2.1.0
|
performance-now: 2.1.0
|
||||||
|
|||||||
Reference in New Issue
Block a user