Merge pull request #20 from felipegcoutinho/refactor/ui-improvements-mobile

refactor: melhorias de UI e responsividade mobile
This commit is contained in:
Felipe Coutinho
2026-02-28 10:42:33 -03:00
committed by GitHub
26 changed files with 422 additions and 425 deletions

View File

@@ -1,16 +1,20 @@
import { DashboardGridSkeleton } from "@/components/skeletons";
import { Skeleton } from "@/components/ui/skeleton";
/**
* 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() {
return (
<main className="flex flex-col gap-6 px-6">
{/* Month Picker placeholder */}
<div className="h-[60px] animate-pulse rounded-2xl bg-foreground/10" />
<main className="flex flex-col gap-4">
{/* Welcome Banner skeleton */}
<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 />
</main>
);

View File

@@ -59,7 +59,8 @@ export default async function DashboardLayout({
preLancamentosCount={preLancamentosCount}
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="flex flex-col gap-4 py-5 md:gap-6 w-full max-w-8xl mx-auto px-4">
{children}

View File

@@ -63,7 +63,7 @@ export default async function TopEstabelecimentosPage({
<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>
<EstablishmentsList establishments={data.establishments} />
</div>

View File

@@ -154,8 +154,8 @@ export default async function Page() {
return (
<div className="flex min-h-screen flex-col">
{/* Navigation */}
<header className="sticky top-0 z-50 bg-background/95 backdrop-blur supports-backdrop-filter:bg-background/60">
<div className="max-w-8xl mx-auto px-4 flex h-14 items-center justify-between">
<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-16 items-center justify-between">
<Logo variant="compact" />
{/* Center Navigation Links */}

View File

@@ -105,7 +105,7 @@
/* Base surfaces - warm dark with consistent hue family */
--background: oklch(18.5% 0.002 70);
--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);
--popover: oklch(24% 0.003 70);
--popover-foreground: oklch(92% 0.015 80);

View File

@@ -197,13 +197,13 @@ export function NotesPage({ notes, archivedNotes }: NotesPageProps) {
return (
<>
<div className="flex w-full flex-col gap-6">
<div className="flex justify-start">
<div className="flex">
<NoteDialog
mode="create"
open={createOpen}
onOpenChange={handleCreateOpenChange}
trigger={
<Button>
<Button className="w-full sm:w-auto">
<RiAddCircleLine className="size-4" />
Nova anotação
</Button>

View File

@@ -152,13 +152,13 @@ export function CardsPage({
return (
<>
<div className="flex w-full flex-col gap-6">
<div className="flex justify-start">
<div className="flex">
<CardDialog
mode="create"
accounts={accounts}
logoOptions={logoOptions}
trigger={
<Button>
<Button className="w-full sm:w-auto">
<RiAddCircleLine className="size-4" />
Novo cartão
</Button>

View File

@@ -95,12 +95,12 @@ export function CategoriesPage({ categories }: CategoriesPageProps) {
return (
<>
<div className="flex w-full flex-col gap-6">
<div className="flex justify-start">
<div className="flex">
<CategoryDialog
mode="create"
defaultType={activeType}
trigger={
<Button>
<Button className="w-full sm:w-auto">
<RiAddCircleLine className="size-4" />
Nova categoria
</Button>

View File

@@ -175,12 +175,12 @@ export function AccountsPage({
return (
<>
<div className="flex w-full flex-col gap-6">
<div className="flex justify-start">
<div className="flex">
<AccountDialog
mode="create"
logoOptions={logoOptions}
trigger={
<Button>
<Button className="w-full sm:w-auto">
<RiAddCircleLine className="size-4" />
Nova conta
</Button>

View File

@@ -75,9 +75,8 @@ export function InstallmentGroupCard({
/>
<div className="min-w-0 flex-1">
<div className="flex items-start justify-between">
<div className="min-w-0 flex-1">
<div className="flex gap-1 items-center">
<div className="flex flex-col gap-1">
<div className="flex gap-1 items-center flex-wrap">
{group.cartaoLogo && (
<img
src={`/logos/${group.cartaoLogo}`}
@@ -85,14 +84,13 @@ export function InstallmentGroupCard({
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">
{group.cartaoName}
| {group.cartaoName}
</span>
</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">
<span className="text-xs text-muted-foreground">Total:</span>
<MoneyValues
@@ -114,11 +112,11 @@ export function InstallmentGroupCard({
{/* Progress bar */}
<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>
{group.paidInstallments} de {group.totalInstallments} pagas
</span>
<div className="flex items-center gap-2">
<div className="flex items-center gap-2 flex-wrap">
<span>
{unpaidCount} {unpaidCount === 1 ? "pendente" : "pendentes"}
</span>
@@ -159,7 +157,7 @@ export function InstallmentGroupCard({
{/* Lista de parcelas expandida */}
{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) => {
const isSelected = selectedInstallments.has(installment.id);
const isPaid = installment.isSettled;

View File

@@ -38,23 +38,26 @@ function CategorySection({
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-foreground">{title}</span>
<MoneyValues amount={total} />
<MoneyValues
amount={total}
className="text-sm font-medium tabular-nums"
/>
</div>
{/* Barra de progresso */}
<Progress value={confirmedPercentage} className="h-2" />
{/* Status de confirmados e pendentes */}
<div className="flex items-center justify-between gap-4 text-sm">
<div className="flex items-center gap-1.5 ">
<RiCheckboxCircleLine className="size-3 text-success" />
<MoneyValues amount={confirmed} />
<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">
<RiCheckboxCircleLine className="size-3 shrink-0 text-success" />
<MoneyValues amount={confirmed} className="tabular-nums" />
<span className="text-xs text-muted-foreground">confirmados</span>
</div>
<div className="flex items-center gap-1.5 ">
<RiHourglass2Line className="size-3 text-warning" />
<MoneyValues amount={pending} />
<div className="flex items-center gap-1.5">
<RiHourglass2Line className="size-3 shrink-0 text-warning" />
<MoneyValues amount={pending} className="tabular-nums" />
<span className="text-xs text-muted-foreground">pendentes</span>
</div>
</div>

View File

@@ -29,7 +29,7 @@ export function BasicFieldsSection({
</div>
<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>
<DatePicker
id="purchaseDate"
@@ -40,7 +40,7 @@ export function BasicFieldsSection({
/>
</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>
<div className="relative">
<CurrencyInput

View File

@@ -261,16 +261,26 @@ export function LancamentosFilters({
};
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
value={searchValue}
onChange={(event) => setSearchValue(event.target.value)}
placeholder="Buscar"
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}
</div>
)}
{!hideAdvancedFilters && (
<Drawer
@@ -281,7 +291,7 @@ export function LancamentosFilters({
<DrawerTrigger asChild>
<Button
variant="outline"
className="text-sm border-dashed relative"
className="flex-1 md:flex-none text-sm border-dashed relative"
aria-label="Abrir filtros"
>
<RiFilter3Line className="size-4" />
@@ -543,5 +553,6 @@ export function LancamentosFilters({
</Drawer>
)}
</div>
</div>
);
}

View File

@@ -3,7 +3,6 @@ import {
RiAddCircleFill,
RiAddCircleLine,
RiArrowLeftRightLine,
RiArrowRightSLine,
RiChat1Line,
RiCheckLine,
RiDeleteBin5Line,
@@ -864,25 +863,21 @@ export function LancamentosTable({
{showTopControls ? (
<div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
{onCreate || onMassAdd ? (
<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 gap-2 py-1 md:w-full md:py-0">
<div className="flex w-full flex-col gap-2 sm:w-auto sm:flex-row">
{onCreate ? (
<>
<Button
onClick={() => onCreate("Receita")}
variant="outline"
className="h-8 shrink-0 px-3 text-xs sm:w-auto md:h-9 md:px-4 md:text-sm"
className="w-full sm:w-auto"
>
<RiAddCircleLine className="size-4 text-success" />
<RiAddCircleLine className="size-4" />
Nova Receita
</Button>
<Button
onClick={() => onCreate("Despesa")}
variant="outline"
className="h-8 shrink-0 px-3 text-xs sm:w-auto md:h-9 md:px-4 md:text-sm"
className="w-full sm:w-auto"
>
<RiAddCircleLine className="size-4 text-destructive" />
<RiAddCircleLine className="size-4" />
Nova Despesa
</Button>
</>
@@ -894,7 +889,7 @@ export function LancamentosTable({
onClick={onMassAdd}
variant="outline"
size="icon"
className="size-8 shrink-0 md:size-9"
className="hidden size-9 sm:inline-flex"
>
<RiAddCircleFill className="size-4" />
<span className="sr-only">
@@ -908,14 +903,6 @@ export function LancamentosTable({
</Tooltip>
) : null}
</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" : ""} />
)}

View File

@@ -79,7 +79,7 @@ export default function MonthNavigation() {
};
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">
<NavigationButton
direction="left"

View File

@@ -10,13 +10,13 @@ interface ReturnButtonProps {
export default function ReturnButton({ disabled, onClick }: ReturnButtonProps) {
return (
<Button
className="w-32 h-6 rounded-sm lowercase"
className="w-max h-6 lowercase"
size="sm"
disabled={disabled}
onClick={onClick}
aria-label="Retornar para o mês atual"
>
Ir para Mês Atual
Mês Atual
</Button>
);
}

View File

@@ -26,7 +26,7 @@ export function AppNavbar({
notificationsSnapshot,
}: AppNavbarProps) {
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">
{/* Logo */}
<Link href="/dashboard" className="shrink-0 mr-1">

View File

@@ -18,6 +18,7 @@ export type NavItem = {
icon: React.ReactNode;
badge?: number;
preservePeriod?: boolean;
hideOnMobile?: boolean;
};
export type NavSection = {
@@ -44,6 +45,7 @@ export const NAV_SECTIONS: NavSection[] = [
href: "/calendario",
label: "calendário",
icon: <RiCalendarEventLine className="size-4" />,
hideOnMobile: true,
},
],
},

View File

@@ -31,7 +31,7 @@ export function NavMenu() {
return (
<>
{/* Desktop */}
<nav className="hidden md:flex items-center flex-1">
<nav className="hidden md:flex items-center justify-center flex-1">
<NavigationMenu viewport={false}>
<NavigationMenuList className="gap-0">
<NavigationMenuItem>
@@ -63,13 +63,13 @@ export function NavMenu() {
</NavigationMenu>
</nav>
{/* Mobile */}
{/* Mobile - order-[-1] places hamburger before logo visually */}
<Sheet open={sheetOpen} onOpenChange={setSheetOpen}>
<SheetTrigger asChild>
<Button
variant="ghost"
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" />
<span className="sr-only">Abrir menu</span>
@@ -86,13 +86,18 @@ export function NavMenu() {
onClick={close}
preservePeriod
>
Dashboard
dashboard
</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}>
<MobileSectionLabel label={section.label} />
{section.items.map((item) => (
{mobileItems.map((item) => (
<MobileLink
key={item.href}
href={item.href}
@@ -105,7 +110,8 @@ export function NavMenu() {
</MobileLink>
))}
</div>
))}
);
})}
<MobileSectionLabel label="Ferramentas" />
<MobileTools onClose={close} />

View File

@@ -1,11 +1,6 @@
"use client";
import {
RiAddCircleLine,
RiArrowRightSLine,
RiFileCopyLine,
RiFundsLine,
} from "@remixicon/react";
import { RiAddCircleLine, RiFileCopyLine, RiFundsLine } from "@remixicon/react";
import { useCallback, useState } from "react";
import { toast } from "sonner";
import {
@@ -110,10 +105,7 @@ export function BudgetsPage({
return (
<>
<div className="flex w-full flex-col gap-6">
{/* No mobile: rolagem horizontal + seta + botões menores */}
<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">
<div className="flex flex-col gap-2 sm:flex-row sm:gap-3">
<BudgetDialog
mode="create"
categories={categories}
@@ -121,7 +113,7 @@ export function BudgetsPage({
trigger={
<Button
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" />
Novo orçamento
@@ -132,20 +124,12 @@ export function BudgetsPage({
variant="outline"
disabled={categories.length === 0}
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" />
Copiar orçamentos do último mês
</Button>
</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 ? (
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-3">

View File

@@ -131,7 +131,7 @@ export function PagadoresPage({
mode="create"
avatarOptions={avatarOptions}
trigger={
<Button>
<Button className="w-full sm:w-auto">
<RiAddCircleLine className="size-4" />
Novo pagador
</Button>
@@ -139,14 +139,14 @@ export function PagadoresPage({
/>
<form
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
placeholder="Código de Compartilhamento"
value={shareCodeInput}
onChange={(event) => setShareCodeInput(event.target.value)}
disabled={joinPending}
className="w-56 border-dashed"
className="w-full sm:w-56 border-dashed"
/>
<Button type="submit" disabled={joinPending}>
{joinPending ? "Adicionando..." : "Adicionar por código"}

View File

@@ -162,8 +162,8 @@ export function CategoryReportFilters({
return (
<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 gap-2">
<div className="flex flex-wrap items-center justify-center gap-2 md:justify-between">
<div className="flex w-full flex-wrap items-center justify-center gap-2 md:w-auto md:justify-start">
{/* Category Multi-Select */}
<Popover open={open} onOpenChange={setOpen} modal>
<PopoverTrigger asChild>
@@ -172,7 +172,7 @@ export function CategoryReportFilters({
role="combobox"
aria-expanded={open}
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}
>
<span className="truncate">{selectedText}</span>
@@ -257,7 +257,7 @@ export function CategoryReportFilters({
<PopoverTrigger asChild>
<Button
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}
>
<RiCalendarLine className="mr-2 h-4 w-4" />
@@ -277,7 +277,7 @@ export function CategoryReportFilters({
<PopoverTrigger asChild>
<Button
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}
>
<RiCalendarLine className="mr-2 h-4 w-4" />
@@ -299,13 +299,14 @@ export function CategoryReportFilters({
size="sm"
onClick={handleReset}
disabled={isLoading}
className="w-full text-center md:w-auto md:text-left"
>
Limpar
</Button>
</div>
{/* Export Button */}
{exportButton}
<div className="w-full md:w-auto">{exportButton}</div>
</div>
{/* Validation Message */}

View File

@@ -11,9 +11,9 @@ export function DashboardGridSkeleton() {
{/* Section Cards no topo */}
<SectionCardsSkeleton />
{/* Grid de widgets */}
<div className="grid grid-cols-1 gap-4 @3xl/main:grid-cols-2 @7xl/main:grid-cols-3">
{Array.from({ length: 12 }).map((_, i) => (
{/* Grid de widgets - mesmos breakpoints do dashboard real */}
<div className="grid grid-cols-1 gap-3 @4xl/main:grid-cols-2 @6xl/main:grid-cols-3">
{Array.from({ length: 9 }).map((_, i) => (
<WidgetSkeleton key={i} />
))}
</div>

View File

@@ -7,7 +7,7 @@ import { Skeleton } from "@/components/ui/skeleton";
*/
export function WidgetSkeleton() {
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">
<div className="flex w-full items-start justify-between">
<div className="space-y-2">

View File

@@ -56,7 +56,7 @@ function DialogContent({
<DialogPrimitive.Content
data-slot="dialog-content"
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,
)}
{...props}

View File

@@ -111,8 +111,8 @@ export default function WidgetCard({
)}
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogContent className="max-h-[85vh] w-full max-w-3xl overflow-hidden p-0">
<DialogHeader className="px-6 pt-4">
<DialogContent className="max-h-[85vh] w-full max-w-[calc(100%-2rem)] sm:max-w-3xl overflow-hidden p-6">
<DialogHeader className="text-left">
<DialogTitle className="flex items-center gap-2">
{icon}
<span>{title}</span>
@@ -121,7 +121,7 @@ export default function WidgetCard({
<p className="text-muted-foreground text-sm">{subtitle}</p>
) : null}
</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}
</div>
</DialogContent>