Merge pull request #20 from felipegcoutinho/refactor/ui-improvements-mobile
refactor: melhorias de UI e responsividade mobile
This commit is contained in:
@@ -1,16 +1,20 @@
|
|||||||
import { DashboardGridSkeleton } from "@/components/skeletons";
|
import { DashboardGridSkeleton } from "@/components/skeletons";
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loading state para a página do dashboard
|
* Loading state para a página do dashboard
|
||||||
* Usa skeleton fiel ao layout final para evitar layout shift
|
* Estrutura: Welcome Banner → Month Picker → Section Cards → Widget Grid
|
||||||
*/
|
*/
|
||||||
export default function DashboardLoading() {
|
export default function DashboardLoading() {
|
||||||
return (
|
return (
|
||||||
<main className="flex flex-col gap-6 px-6">
|
<main className="flex flex-col gap-4">
|
||||||
{/* Month Picker placeholder */}
|
{/* Welcome Banner skeleton */}
|
||||||
<div className="h-[60px] animate-pulse rounded-2xl bg-foreground/10" />
|
<Skeleton className="h-[104px] w-full rounded-xl bg-foreground/10" />
|
||||||
|
|
||||||
{/* Dashboard content skeleton */}
|
{/* Month Picker skeleton */}
|
||||||
|
<Skeleton className="h-[56px] w-full rounded-xl bg-foreground/10" />
|
||||||
|
|
||||||
|
{/* Dashboard content skeleton (Section Cards + Widget Grid) */}
|
||||||
<DashboardGridSkeleton />
|
<DashboardGridSkeleton />
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -59,7 +59,8 @@ export default async function DashboardLayout({
|
|||||||
preLancamentosCount={preLancamentosCount}
|
preLancamentosCount={preLancamentosCount}
|
||||||
notificationsSnapshot={notificationsSnapshot}
|
notificationsSnapshot={notificationsSnapshot}
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-1 flex-col pt-14">
|
<div className="relative flex flex-1 flex-col pt-16">
|
||||||
|
<div className="pointer-events-none absolute inset-0 bg-linear-to-b from-primary/5 via-transparent to-transparent" />
|
||||||
<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-5 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}
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export default async function TopEstabelecimentosPage({
|
|||||||
|
|
||||||
<HighlightsCards summary={data.summary} />
|
<HighlightsCards summary={data.summary} />
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<EstablishmentsList establishments={data.establishments} />
|
<EstablishmentsList establishments={data.establishments} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -154,8 +154,8 @@ export default async function Page() {
|
|||||||
return (
|
return (
|
||||||
<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-card backdrop-blur-lg supports-backdrop-filter:bg-card/50">
|
||||||
<div className="max-w-8xl mx-auto px-4 flex h-14 items-center justify-between">
|
<div className="max-w-8xl mx-auto px-4 flex h-16 items-center justify-between">
|
||||||
<Logo variant="compact" />
|
<Logo variant="compact" />
|
||||||
|
|
||||||
{/* Center Navigation Links */}
|
{/* Center Navigation Links */}
|
||||||
|
|||||||
@@ -105,7 +105,7 @@
|
|||||||
/* Base surfaces - warm dark with consistent hue family */
|
/* Base surfaces - warm dark with consistent hue family */
|
||||||
--background: oklch(18.5% 0.002 70);
|
--background: oklch(18.5% 0.002 70);
|
||||||
--foreground: oklch(92% 0.015 80);
|
--foreground: oklch(92% 0.015 80);
|
||||||
--card: oklch(24% 0.003 70);
|
--card: oklch(22.717% 0.00244 67.467);
|
||||||
--card-foreground: oklch(92% 0.015 80);
|
--card-foreground: oklch(92% 0.015 80);
|
||||||
--popover: oklch(24% 0.003 70);
|
--popover: oklch(24% 0.003 70);
|
||||||
--popover-foreground: oklch(92% 0.015 80);
|
--popover-foreground: oklch(92% 0.015 80);
|
||||||
|
|||||||
@@ -197,13 +197,13 @@ export function NotesPage({ notes, archivedNotes }: NotesPageProps) {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex w-full flex-col gap-6">
|
<div className="flex w-full flex-col gap-6">
|
||||||
<div className="flex justify-start">
|
<div className="flex">
|
||||||
<NoteDialog
|
<NoteDialog
|
||||||
mode="create"
|
mode="create"
|
||||||
open={createOpen}
|
open={createOpen}
|
||||||
onOpenChange={handleCreateOpenChange}
|
onOpenChange={handleCreateOpenChange}
|
||||||
trigger={
|
trigger={
|
||||||
<Button>
|
<Button className="w-full sm:w-auto">
|
||||||
<RiAddCircleLine className="size-4" />
|
<RiAddCircleLine className="size-4" />
|
||||||
Nova anotação
|
Nova anotação
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -152,13 +152,13 @@ export function CardsPage({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex w-full flex-col gap-6">
|
<div className="flex w-full flex-col gap-6">
|
||||||
<div className="flex justify-start">
|
<div className="flex">
|
||||||
<CardDialog
|
<CardDialog
|
||||||
mode="create"
|
mode="create"
|
||||||
accounts={accounts}
|
accounts={accounts}
|
||||||
logoOptions={logoOptions}
|
logoOptions={logoOptions}
|
||||||
trigger={
|
trigger={
|
||||||
<Button>
|
<Button className="w-full sm:w-auto">
|
||||||
<RiAddCircleLine className="size-4" />
|
<RiAddCircleLine className="size-4" />
|
||||||
Novo cartão
|
Novo cartão
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -95,12 +95,12 @@ export function CategoriesPage({ categories }: CategoriesPageProps) {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex w-full flex-col gap-6">
|
<div className="flex w-full flex-col gap-6">
|
||||||
<div className="flex justify-start">
|
<div className="flex">
|
||||||
<CategoryDialog
|
<CategoryDialog
|
||||||
mode="create"
|
mode="create"
|
||||||
defaultType={activeType}
|
defaultType={activeType}
|
||||||
trigger={
|
trigger={
|
||||||
<Button>
|
<Button className="w-full sm:w-auto">
|
||||||
<RiAddCircleLine className="size-4" />
|
<RiAddCircleLine className="size-4" />
|
||||||
Nova categoria
|
Nova categoria
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -175,12 +175,12 @@ export function AccountsPage({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex w-full flex-col gap-6">
|
<div className="flex w-full flex-col gap-6">
|
||||||
<div className="flex justify-start">
|
<div className="flex">
|
||||||
<AccountDialog
|
<AccountDialog
|
||||||
mode="create"
|
mode="create"
|
||||||
logoOptions={logoOptions}
|
logoOptions={logoOptions}
|
||||||
trigger={
|
trigger={
|
||||||
<Button>
|
<Button className="w-full sm:w-auto">
|
||||||
<RiAddCircleLine className="size-4" />
|
<RiAddCircleLine className="size-4" />
|
||||||
Nova conta
|
Nova conta
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -75,9 +75,8 @@ export function InstallmentGroupCard({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<div className="flex items-start justify-between">
|
<div className="flex flex-col gap-1">
|
||||||
<div className="min-w-0 flex-1">
|
<div className="flex gap-1 items-center flex-wrap">
|
||||||
<div className="flex gap-1 items-center">
|
|
||||||
{group.cartaoLogo && (
|
{group.cartaoLogo && (
|
||||||
<img
|
<img
|
||||||
src={`/logos/${group.cartaoLogo}`}
|
src={`/logos/${group.cartaoLogo}`}
|
||||||
@@ -85,14 +84,13 @@ export function InstallmentGroupCard({
|
|||||||
className="h-6 w-auto object-contain rounded"
|
className="h-6 w-auto object-contain rounded"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<span className="font-medium">{group.name}</span>|
|
<span className="font-medium truncate">{group.name}</span>
|
||||||
<span className="text-xs text-muted-foreground">
|
<span className="text-xs text-muted-foreground">
|
||||||
{group.cartaoName}
|
| {group.cartaoName}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="shrink-0 flex items-center gap-3">
|
<div className="flex items-center gap-3 flex-wrap">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<span className="text-xs text-muted-foreground">Total:</span>
|
<span className="text-xs text-muted-foreground">Total:</span>
|
||||||
<MoneyValues
|
<MoneyValues
|
||||||
@@ -114,11 +112,11 @@ export function InstallmentGroupCard({
|
|||||||
|
|
||||||
{/* Progress bar */}
|
{/* Progress bar */}
|
||||||
<div className="mt-3">
|
<div className="mt-3">
|
||||||
<div className="mb-2 flex items-center px-1 justify-between text-xs text-muted-foreground">
|
<div className="mb-2 flex flex-wrap items-center px-1 justify-between gap-x-2 gap-y-0.5 text-xs text-muted-foreground">
|
||||||
<span>
|
<span>
|
||||||
{group.paidInstallments} de {group.totalInstallments} pagas
|
{group.paidInstallments} de {group.totalInstallments} pagas
|
||||||
</span>
|
</span>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
<span>
|
<span>
|
||||||
{unpaidCount} {unpaidCount === 1 ? "pendente" : "pendentes"}
|
{unpaidCount} {unpaidCount === 1 ? "pendente" : "pendentes"}
|
||||||
</span>
|
</span>
|
||||||
@@ -159,7 +157,7 @@ export function InstallmentGroupCard({
|
|||||||
|
|
||||||
{/* Lista de parcelas expandida */}
|
{/* Lista de parcelas expandida */}
|
||||||
{isExpanded && (
|
{isExpanded && (
|
||||||
<div className="px-8 mt-2 flex flex-col gap-2">
|
<div className="px-2 sm:px-8 mt-2 flex flex-col gap-2">
|
||||||
{group.pendingInstallments.map((installment) => {
|
{group.pendingInstallments.map((installment) => {
|
||||||
const isSelected = selectedInstallments.has(installment.id);
|
const isSelected = selectedInstallments.has(installment.id);
|
||||||
const isPaid = installment.isSettled;
|
const isPaid = installment.isSettled;
|
||||||
|
|||||||
@@ -38,23 +38,26 @@ function CategorySection({
|
|||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-sm font-medium text-foreground">{title}</span>
|
<span className="text-sm font-medium text-foreground">{title}</span>
|
||||||
<MoneyValues amount={total} />
|
<MoneyValues
|
||||||
|
amount={total}
|
||||||
|
className="text-sm font-medium tabular-nums"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Barra de progresso */}
|
{/* Barra de progresso */}
|
||||||
<Progress value={confirmedPercentage} className="h-2" />
|
<Progress value={confirmedPercentage} className="h-2" />
|
||||||
|
|
||||||
{/* Status de confirmados e pendentes */}
|
{/* Status de confirmados e pendentes */}
|
||||||
<div className="flex items-center justify-between gap-4 text-sm">
|
<div className="flex flex-col gap-1 text-sm sm:flex-row sm:items-center sm:justify-between sm:gap-4">
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<RiCheckboxCircleLine className="size-3 text-success" />
|
<RiCheckboxCircleLine className="size-3 shrink-0 text-success" />
|
||||||
<MoneyValues amount={confirmed} />
|
<MoneyValues amount={confirmed} className="tabular-nums" />
|
||||||
<span className="text-xs text-muted-foreground">confirmados</span>
|
<span className="text-xs text-muted-foreground">confirmados</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<RiHourglass2Line className="size-3 text-warning" />
|
<RiHourglass2Line className="size-3 shrink-0 text-warning" />
|
||||||
<MoneyValues amount={pending} />
|
<MoneyValues amount={pending} className="tabular-nums" />
|
||||||
<span className="text-xs text-muted-foreground">pendentes</span>
|
<span className="text-xs text-muted-foreground">pendentes</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export function BasicFieldsSection({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex w-full flex-col gap-2 md:flex-row">
|
<div className="flex w-full flex-col gap-2 md:flex-row">
|
||||||
<div className="w-1/2 space-y-1">
|
<div className="w-full md:w-1/2 space-y-1">
|
||||||
<Label htmlFor="purchaseDate">Data</Label>
|
<Label htmlFor="purchaseDate">Data</Label>
|
||||||
<DatePicker
|
<DatePicker
|
||||||
id="purchaseDate"
|
id="purchaseDate"
|
||||||
@@ -40,7 +40,7 @@ export function BasicFieldsSection({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="w-1/2 space-y-1">
|
<div className="w-full md:w-1/2 space-y-1">
|
||||||
<Label htmlFor="amount">Valor</Label>
|
<Label htmlFor="amount">Valor</Label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<CurrencyInput
|
<CurrencyInput
|
||||||
|
|||||||
@@ -261,16 +261,26 @@ export function LancamentosFilters({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("flex flex-wrap items-center gap-2", className)}>
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col gap-2 md:flex-row md:flex-wrap md:items-center",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
<Input
|
<Input
|
||||||
value={searchValue}
|
value={searchValue}
|
||||||
onChange={(event) => setSearchValue(event.target.value)}
|
onChange={(event) => setSearchValue(event.target.value)}
|
||||||
placeholder="Buscar"
|
placeholder="Buscar"
|
||||||
aria-label="Buscar lançamentos"
|
aria-label="Buscar lançamentos"
|
||||||
className="w-[250px] text-sm border-dashed"
|
className="w-full md:w-[250px] text-sm border-dashed"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<div className="flex w-full gap-2 md:w-auto">
|
||||||
|
{exportButton && (
|
||||||
|
<div className="flex-1 md:flex-none [&>*]:w-full [&>*]:md:w-auto">
|
||||||
{exportButton}
|
{exportButton}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{!hideAdvancedFilters && (
|
{!hideAdvancedFilters && (
|
||||||
<Drawer
|
<Drawer
|
||||||
@@ -281,7 +291,7 @@ export function LancamentosFilters({
|
|||||||
<DrawerTrigger asChild>
|
<DrawerTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="text-sm border-dashed relative"
|
className="flex-1 md:flex-none text-sm border-dashed relative"
|
||||||
aria-label="Abrir filtros"
|
aria-label="Abrir filtros"
|
||||||
>
|
>
|
||||||
<RiFilter3Line className="size-4" />
|
<RiFilter3Line className="size-4" />
|
||||||
@@ -543,5 +553,6 @@ export function LancamentosFilters({
|
|||||||
</Drawer>
|
</Drawer>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import {
|
|||||||
RiAddCircleFill,
|
RiAddCircleFill,
|
||||||
RiAddCircleLine,
|
RiAddCircleLine,
|
||||||
RiArrowLeftRightLine,
|
RiArrowLeftRightLine,
|
||||||
RiArrowRightSLine,
|
|
||||||
RiChat1Line,
|
RiChat1Line,
|
||||||
RiCheckLine,
|
RiCheckLine,
|
||||||
RiDeleteBin5Line,
|
RiDeleteBin5Line,
|
||||||
@@ -864,25 +863,21 @@ export function LancamentosTable({
|
|||||||
{showTopControls ? (
|
{showTopControls ? (
|
||||||
<div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
|
<div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
|
||||||
{onCreate || onMassAdd ? (
|
{onCreate || onMassAdd ? (
|
||||||
<div className="relative -mx-6 px-6 md:mx-0 md:px-0">
|
<div className="flex w-full flex-col gap-2 sm:w-auto sm:flex-row">
|
||||||
<div className="overflow-x-auto overflow-y-hidden scroll-smooth md:overflow-visible [-webkit-overflow-scrolling:touch] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
|
|
||||||
<div className="flex w-max shrink-0 gap-2 py-1 md:w-full md:py-0">
|
|
||||||
{onCreate ? (
|
{onCreate ? (
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => onCreate("Receita")}
|
onClick={() => onCreate("Receita")}
|
||||||
variant="outline"
|
className="w-full sm:w-auto"
|
||||||
className="h-8 shrink-0 px-3 text-xs sm:w-auto md:h-9 md:px-4 md:text-sm"
|
|
||||||
>
|
>
|
||||||
<RiAddCircleLine className="size-4 text-success" />
|
<RiAddCircleLine className="size-4" />
|
||||||
Nova Receita
|
Nova Receita
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => onCreate("Despesa")}
|
onClick={() => onCreate("Despesa")}
|
||||||
variant="outline"
|
className="w-full sm:w-auto"
|
||||||
className="h-8 shrink-0 px-3 text-xs sm:w-auto md:h-9 md:px-4 md:text-sm"
|
|
||||||
>
|
>
|
||||||
<RiAddCircleLine className="size-4 text-destructive" />
|
<RiAddCircleLine className="size-4" />
|
||||||
Nova Despesa
|
Nova Despesa
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
@@ -894,7 +889,7 @@ export function LancamentosTable({
|
|||||||
onClick={onMassAdd}
|
onClick={onMassAdd}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="size-8 shrink-0 md:size-9"
|
className="hidden size-9 sm:inline-flex"
|
||||||
>
|
>
|
||||||
<RiAddCircleFill className="size-4" />
|
<RiAddCircleFill className="size-4" />
|
||||||
<span className="sr-only">
|
<span className="sr-only">
|
||||||
@@ -908,14 +903,6 @@ export function LancamentosTable({
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="pointer-events-none absolute right-0 top-0 hidden h-9 w-10 items-center justify-end bg-gradient-to-l from-background to-transparent py-1 md:hidden"
|
|
||||||
aria-hidden
|
|
||||||
>
|
|
||||||
<RiArrowRightSLine className="size-5 shrink-0 text-muted-foreground" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
<span className={showFilters ? "hidden sm:block" : ""} />
|
<span className={showFilters ? "hidden sm:block" : ""} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export default function MonthNavigation() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="w-full flex-row bg-card text-card-foreground p-4 sticky top-14 z-10">
|
<Card className="flex w-full flex-row bg-card text-card-foreground p-4 sticky top-16 z-10">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<NavigationButton
|
<NavigationButton
|
||||||
direction="left"
|
direction="left"
|
||||||
|
|||||||
@@ -10,13 +10,13 @@ interface ReturnButtonProps {
|
|||||||
export default function ReturnButton({ disabled, onClick }: ReturnButtonProps) {
|
export default function ReturnButton({ disabled, onClick }: ReturnButtonProps) {
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
className="w-32 h-6 rounded-sm lowercase"
|
className="w-max h-6 lowercase"
|
||||||
size="sm"
|
size="sm"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
aria-label="Retornar para o mês atual"
|
aria-label="Retornar para o mês atual"
|
||||||
>
|
>
|
||||||
Ir para Mês Atual
|
Mês Atual
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export function AppNavbar({
|
|||||||
notificationsSnapshot,
|
notificationsSnapshot,
|
||||||
}: AppNavbarProps) {
|
}: AppNavbarProps) {
|
||||||
return (
|
return (
|
||||||
<header className="fixed top-0 left-0 right-0 z-50 h-15 shrink-0 flex items-center bg-card backdrop-blur-lg supports-backdrop-filter:bg-card/60">
|
<header className="fixed top-0 left-0 right-0 z-50 h-16 shrink-0 flex items-center bg-card backdrop-blur-lg supports-backdrop-filter:bg-card/50">
|
||||||
<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 href="/dashboard" className="shrink-0 mr-1">
|
<Link href="/dashboard" className="shrink-0 mr-1">
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export type NavItem = {
|
|||||||
icon: React.ReactNode;
|
icon: React.ReactNode;
|
||||||
badge?: number;
|
badge?: number;
|
||||||
preservePeriod?: boolean;
|
preservePeriod?: boolean;
|
||||||
|
hideOnMobile?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type NavSection = {
|
export type NavSection = {
|
||||||
@@ -44,6 +45,7 @@ export const NAV_SECTIONS: NavSection[] = [
|
|||||||
href: "/calendario",
|
href: "/calendario",
|
||||||
label: "calendário",
|
label: "calendário",
|
||||||
icon: <RiCalendarEventLine className="size-4" />,
|
icon: <RiCalendarEventLine className="size-4" />,
|
||||||
|
hideOnMobile: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export function NavMenu() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Desktop */}
|
{/* Desktop */}
|
||||||
<nav className="hidden md:flex items-center flex-1">
|
<nav className="hidden md:flex items-center justify-center flex-1">
|
||||||
<NavigationMenu viewport={false}>
|
<NavigationMenu viewport={false}>
|
||||||
<NavigationMenuList className="gap-0">
|
<NavigationMenuList className="gap-0">
|
||||||
<NavigationMenuItem>
|
<NavigationMenuItem>
|
||||||
@@ -63,13 +63,13 @@ export function NavMenu() {
|
|||||||
</NavigationMenu>
|
</NavigationMenu>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{/* Mobile */}
|
{/* Mobile - order-[-1] places hamburger before logo visually */}
|
||||||
<Sheet open={sheetOpen} onOpenChange={setSheetOpen}>
|
<Sheet open={sheetOpen} onOpenChange={setSheetOpen}>
|
||||||
<SheetTrigger asChild>
|
<SheetTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="md:hidden text-foreground hover:bg-foreground/10 hover:text-foreground"
|
className="-order-1 md:hidden text-foreground hover:bg-foreground/10 hover:text-foreground"
|
||||||
>
|
>
|
||||||
<RiMenuLine className="size-5" />
|
<RiMenuLine className="size-5" />
|
||||||
<span className="sr-only">Abrir menu</span>
|
<span className="sr-only">Abrir menu</span>
|
||||||
@@ -86,13 +86,18 @@ export function NavMenu() {
|
|||||||
onClick={close}
|
onClick={close}
|
||||||
preservePeriod
|
preservePeriod
|
||||||
>
|
>
|
||||||
Dashboard
|
dashboard
|
||||||
</MobileLink>
|
</MobileLink>
|
||||||
|
|
||||||
{NAV_SECTIONS.map((section) => (
|
{NAV_SECTIONS.map((section) => {
|
||||||
|
const mobileItems = section.items.filter(
|
||||||
|
(item) => !item.hideOnMobile,
|
||||||
|
);
|
||||||
|
if (mobileItems.length === 0) return null;
|
||||||
|
return (
|
||||||
<div key={section.label}>
|
<div key={section.label}>
|
||||||
<MobileSectionLabel label={section.label} />
|
<MobileSectionLabel label={section.label} />
|
||||||
{section.items.map((item) => (
|
{mobileItems.map((item) => (
|
||||||
<MobileLink
|
<MobileLink
|
||||||
key={item.href}
|
key={item.href}
|
||||||
href={item.href}
|
href={item.href}
|
||||||
@@ -105,7 +110,8 @@ export function NavMenu() {
|
|||||||
</MobileLink>
|
</MobileLink>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
<MobileSectionLabel label="Ferramentas" />
|
<MobileSectionLabel label="Ferramentas" />
|
||||||
<MobileTools onClose={close} />
|
<MobileTools onClose={close} />
|
||||||
|
|||||||
@@ -1,11 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import {
|
import { RiAddCircleLine, RiFileCopyLine, RiFundsLine } from "@remixicon/react";
|
||||||
RiAddCircleLine,
|
|
||||||
RiArrowRightSLine,
|
|
||||||
RiFileCopyLine,
|
|
||||||
RiFundsLine,
|
|
||||||
} from "@remixicon/react";
|
|
||||||
import { useCallback, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import {
|
import {
|
||||||
@@ -110,10 +105,7 @@ export function BudgetsPage({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex w-full flex-col gap-6">
|
<div className="flex w-full flex-col gap-6">
|
||||||
{/* No mobile: rolagem horizontal + seta + botões menores */}
|
<div className="flex flex-col gap-2 sm:flex-row sm:gap-3">
|
||||||
<div className="relative -mx-6 px-6 md:mx-0 md:px-0">
|
|
||||||
<div className="overflow-x-auto overflow-y-hidden scroll-smooth md:overflow-visible [-webkit-overflow-scrolling:touch] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
|
|
||||||
<div className="flex w-max shrink-0 justify-start gap-3 py-1 md:w-full md:gap-4 md:py-0">
|
|
||||||
<BudgetDialog
|
<BudgetDialog
|
||||||
mode="create"
|
mode="create"
|
||||||
categories={categories}
|
categories={categories}
|
||||||
@@ -121,7 +113,7 @@ export function BudgetsPage({
|
|||||||
trigger={
|
trigger={
|
||||||
<Button
|
<Button
|
||||||
disabled={categories.length === 0}
|
disabled={categories.length === 0}
|
||||||
className="h-8 shrink-0 px-3 text-xs md:h-9 md:px-4 md:text-sm"
|
className="w-full sm:w-auto"
|
||||||
>
|
>
|
||||||
<RiAddCircleLine className="size-4" />
|
<RiAddCircleLine className="size-4" />
|
||||||
Novo orçamento
|
Novo orçamento
|
||||||
@@ -132,20 +124,12 @@ export function BudgetsPage({
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
disabled={categories.length === 0}
|
disabled={categories.length === 0}
|
||||||
onClick={() => setDuplicateOpen(true)}
|
onClick={() => setDuplicateOpen(true)}
|
||||||
className="h-8 shrink-0 px-3 text-xs md:h-9 md:px-4 md:text-sm"
|
className="w-full sm:w-auto"
|
||||||
>
|
>
|
||||||
<RiFileCopyLine className="size-4" />
|
<RiFileCopyLine className="size-4" />
|
||||||
Copiar orçamentos do último mês
|
Copiar orçamentos do último mês
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="pointer-events-none absolute right-0 top-0 hidden h-9 w-10 items-center justify-end bg-gradient-to-l from-background to-transparent py-1 md:hidden"
|
|
||||||
aria-hidden
|
|
||||||
>
|
|
||||||
<RiArrowRightSLine className="size-5 shrink-0 text-muted-foreground" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{hasBudgets ? (
|
{hasBudgets ? (
|
||||||
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
|
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ export function PagadoresPage({
|
|||||||
mode="create"
|
mode="create"
|
||||||
avatarOptions={avatarOptions}
|
avatarOptions={avatarOptions}
|
||||||
trigger={
|
trigger={
|
||||||
<Button>
|
<Button className="w-full sm:w-auto">
|
||||||
<RiAddCircleLine className="size-4" />
|
<RiAddCircleLine className="size-4" />
|
||||||
Novo pagador
|
Novo pagador
|
||||||
</Button>
|
</Button>
|
||||||
@@ -139,14 +139,14 @@ export function PagadoresPage({
|
|||||||
/>
|
/>
|
||||||
<form
|
<form
|
||||||
onSubmit={handleJoinByCode}
|
onSubmit={handleJoinByCode}
|
||||||
className="flex w-full flex-col gap-2 sm:w-auto sm:flex-row"
|
className="flex w-full flex-row items-center justify-center gap-2 sm:w-auto"
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Código de Compartilhamento"
|
placeholder="Código de Compartilhamento"
|
||||||
value={shareCodeInput}
|
value={shareCodeInput}
|
||||||
onChange={(event) => setShareCodeInput(event.target.value)}
|
onChange={(event) => setShareCodeInput(event.target.value)}
|
||||||
disabled={joinPending}
|
disabled={joinPending}
|
||||||
className="w-56 border-dashed"
|
className="w-full sm:w-56 border-dashed"
|
||||||
/>
|
/>
|
||||||
<Button type="submit" disabled={joinPending}>
|
<Button type="submit" disabled={joinPending}>
|
||||||
{joinPending ? "Adicionando..." : "Adicionar por código"}
|
{joinPending ? "Adicionando..." : "Adicionar por código"}
|
||||||
|
|||||||
@@ -162,8 +162,8 @@ export function CategoryReportFilters({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
<div className="flex flex-wrap items-center justify-center gap-2 md:justify-between">
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex w-full flex-wrap items-center justify-center gap-2 md:w-auto md:justify-start">
|
||||||
{/* Category Multi-Select */}
|
{/* Category Multi-Select */}
|
||||||
<Popover open={open} onOpenChange={setOpen} modal>
|
<Popover open={open} onOpenChange={setOpen} modal>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
@@ -172,7 +172,7 @@ export function CategoryReportFilters({
|
|||||||
role="combobox"
|
role="combobox"
|
||||||
aria-expanded={open}
|
aria-expanded={open}
|
||||||
aria-label="Selecionar categorias para filtrar"
|
aria-label="Selecionar categorias para filtrar"
|
||||||
className="w-[180px] justify-between text-sm border-dashed border-input"
|
className="w-full md:w-[180px] justify-between text-sm border-dashed border-input"
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
>
|
>
|
||||||
<span className="truncate">{selectedText}</span>
|
<span className="truncate">{selectedText}</span>
|
||||||
@@ -257,7 +257,7 @@ export function CategoryReportFilters({
|
|||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="w-[150px] justify-start text-sm border-dashed"
|
className="w-[calc(50%-0.25rem)] md:w-[150px] justify-start text-sm border-dashed"
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
>
|
>
|
||||||
<RiCalendarLine className="mr-2 h-4 w-4" />
|
<RiCalendarLine className="mr-2 h-4 w-4" />
|
||||||
@@ -277,7 +277,7 @@ export function CategoryReportFilters({
|
|||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="w-[150px] justify-start text-sm border-dashed"
|
className="w-[calc(50%-0.25rem)] md:w-[150px] justify-start text-sm border-dashed"
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
>
|
>
|
||||||
<RiCalendarLine className="mr-2 h-4 w-4" />
|
<RiCalendarLine className="mr-2 h-4 w-4" />
|
||||||
@@ -299,13 +299,14 @@ export function CategoryReportFilters({
|
|||||||
size="sm"
|
size="sm"
|
||||||
onClick={handleReset}
|
onClick={handleReset}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
|
className="w-full text-center md:w-auto md:text-left"
|
||||||
>
|
>
|
||||||
Limpar
|
Limpar
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Export Button */}
|
{/* Export Button */}
|
||||||
{exportButton}
|
<div className="w-full md:w-auto">{exportButton}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Validation Message */}
|
{/* Validation Message */}
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ export function DashboardGridSkeleton() {
|
|||||||
{/* Section Cards no topo */}
|
{/* Section Cards no topo */}
|
||||||
<SectionCardsSkeleton />
|
<SectionCardsSkeleton />
|
||||||
|
|
||||||
{/* Grid de widgets */}
|
{/* Grid de widgets - mesmos breakpoints do dashboard real */}
|
||||||
<div className="grid grid-cols-1 gap-4 @3xl/main:grid-cols-2 @7xl/main:grid-cols-3">
|
<div className="grid grid-cols-1 gap-3 @4xl/main:grid-cols-2 @6xl/main:grid-cols-3">
|
||||||
{Array.from({ length: 12 }).map((_, i) => (
|
{Array.from({ length: 9 }).map((_, i) => (
|
||||||
<WidgetSkeleton key={i} />
|
<WidgetSkeleton key={i} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { Skeleton } from "@/components/ui/skeleton";
|
|||||||
*/
|
*/
|
||||||
export function WidgetSkeleton() {
|
export function WidgetSkeleton() {
|
||||||
return (
|
return (
|
||||||
<Card className="md:h-custom-height-1 relative h-auto md:overflow-hidden">
|
<Card className="relative h-auto md:h-custom-height-1 md:overflow-hidden">
|
||||||
<CardHeader className="border-b [.border-b]:pb-2">
|
<CardHeader className="border-b [.border-b]:pb-2">
|
||||||
<div className="flex w-full items-start justify-between">
|
<div className="flex w-full items-start justify-between">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ function DialogContent({
|
|||||||
<DialogPrimitive.Content
|
<DialogPrimitive.Content
|
||||||
data-slot="dialog-content"
|
data-slot="dialog-content"
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-background fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-10 shadow-lg sm:max-w-xl",
|
"bg-background fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] max-h-[90vh] overflow-y-auto translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-4 shadow-lg sm:p-10 sm:max-w-xl",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -111,8 +111,8 @@ export default function WidgetCard({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<DialogContent className="max-h-[85vh] w-full max-w-3xl overflow-hidden p-0">
|
<DialogContent className="max-h-[85vh] w-full max-w-[calc(100%-2rem)] sm:max-w-3xl overflow-hidden p-6">
|
||||||
<DialogHeader className="px-6 pt-4">
|
<DialogHeader className="text-left">
|
||||||
<DialogTitle className="flex items-center gap-2">
|
<DialogTitle className="flex items-center gap-2">
|
||||||
{icon}
|
{icon}
|
||||||
<span>{title}</span>
|
<span>{title}</span>
|
||||||
@@ -121,7 +121,7 @@ export default function WidgetCard({
|
|||||||
<p className="text-muted-foreground text-sm">{subtitle}</p>
|
<p className="text-muted-foreground text-sm">{subtitle}</p>
|
||||||
) : null}
|
) : null}
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="scrollbar-hide max-h-[calc(85vh-6rem)] overflow-y-auto px-6 pb-6">
|
<div className="scrollbar-hide max-h-[calc(85vh-6rem)] overflow-y-auto pb-6">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
Reference in New Issue
Block a user