refactor: melhorias de UI e responsividade mobile
- Corrigir layout truncado no card de parcelas (analise-parcelas) - Empilhar cards de top estabelecimentos e categorias no mobile - Ajustes gerais de responsividade em múltiplos componentes Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -75,24 +75,22 @@ 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">
|
||||
{group.cartaoLogo && (
|
||||
<img
|
||||
src={`/logos/${group.cartaoLogo}`}
|
||||
alt={group.cartaoName}
|
||||
className="h-6 w-auto object-contain rounded"
|
||||
/>
|
||||
)}
|
||||
<span className="font-medium">{group.name}</span>|
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{group.cartaoName}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex gap-1 items-center flex-wrap">
|
||||
{group.cartaoLogo && (
|
||||
<img
|
||||
src={`/logos/${group.cartaoLogo}`}
|
||||
alt={group.cartaoName}
|
||||
className="h-6 w-auto object-contain rounded"
|
||||
/>
|
||||
)}
|
||||
<span className="font-medium truncate">{group.name}</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
| {group.cartaoName}
|
||||
</span>
|
||||
</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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -261,287 +261,298 @@ 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"
|
||||
/>
|
||||
|
||||
{exportButton}
|
||||
<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
|
||||
direction="right"
|
||||
open={drawerOpen}
|
||||
onOpenChange={setDrawerOpen}
|
||||
>
|
||||
<DrawerTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="text-sm border-dashed relative"
|
||||
aria-label="Abrir filtros"
|
||||
>
|
||||
<RiFilter3Line className="size-4" />
|
||||
Filtros
|
||||
{hasActiveFilters && (
|
||||
<span className="absolute -top-1 -right-1 size-2 rounded-full bg-primary" />
|
||||
)}
|
||||
</Button>
|
||||
</DrawerTrigger>
|
||||
<DrawerContent>
|
||||
<DrawerHeader>
|
||||
<DrawerTitle>Filtros</DrawerTitle>
|
||||
<DrawerDescription>
|
||||
Selecione os filtros desejados para refinar os lançamentos
|
||||
</DrawerDescription>
|
||||
</DrawerHeader>
|
||||
{!hideAdvancedFilters && (
|
||||
<Drawer
|
||||
direction="right"
|
||||
open={drawerOpen}
|
||||
onOpenChange={setDrawerOpen}
|
||||
>
|
||||
<DrawerTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex-1 md:flex-none text-sm border-dashed relative"
|
||||
aria-label="Abrir filtros"
|
||||
>
|
||||
<RiFilter3Line className="size-4" />
|
||||
Filtros
|
||||
{hasActiveFilters && (
|
||||
<span className="absolute -top-1 -right-1 size-2 rounded-full bg-primary" />
|
||||
)}
|
||||
</Button>
|
||||
</DrawerTrigger>
|
||||
<DrawerContent>
|
||||
<DrawerHeader>
|
||||
<DrawerTitle>Filtros</DrawerTitle>
|
||||
<DrawerDescription>
|
||||
Selecione os filtros desejados para refinar os lançamentos
|
||||
</DrawerDescription>
|
||||
</DrawerHeader>
|
||||
|
||||
<div className="flex-1 overflow-y-auto px-4 space-y-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">
|
||||
Tipo de Lançamento
|
||||
</label>
|
||||
<FilterSelect
|
||||
param="transacao"
|
||||
placeholder="Todos"
|
||||
options={buildStaticOptions(LANCAMENTO_TRANSACTION_TYPES)}
|
||||
widthClass="w-full border-dashed"
|
||||
disabled={isPending}
|
||||
getParamValue={getParamValue}
|
||||
onChange={handleFilterChange}
|
||||
renderContent={(label) => (
|
||||
<TransactionTypeSelectContent label={label} />
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto px-4 space-y-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">
|
||||
Tipo de Lançamento
|
||||
</label>
|
||||
<FilterSelect
|
||||
param="transacao"
|
||||
placeholder="Todos"
|
||||
options={buildStaticOptions(LANCAMENTO_TRANSACTION_TYPES)}
|
||||
widthClass="w-full border-dashed"
|
||||
disabled={isPending}
|
||||
getParamValue={getParamValue}
|
||||
onChange={handleFilterChange}
|
||||
renderContent={(label) => (
|
||||
<TransactionTypeSelectContent label={label} />
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">
|
||||
Condição de Lançamento
|
||||
</label>
|
||||
<FilterSelect
|
||||
param="condicao"
|
||||
placeholder="Todas"
|
||||
options={buildStaticOptions(LANCAMENTO_CONDITIONS)}
|
||||
widthClass="w-full border-dashed"
|
||||
disabled={isPending}
|
||||
getParamValue={getParamValue}
|
||||
onChange={handleFilterChange}
|
||||
renderContent={(label) => (
|
||||
<ConditionSelectContent label={label} />
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">
|
||||
Condição de Lançamento
|
||||
</label>
|
||||
<FilterSelect
|
||||
param="condicao"
|
||||
placeholder="Todas"
|
||||
options={buildStaticOptions(LANCAMENTO_CONDITIONS)}
|
||||
widthClass="w-full border-dashed"
|
||||
disabled={isPending}
|
||||
getParamValue={getParamValue}
|
||||
onChange={handleFilterChange}
|
||||
renderContent={(label) => (
|
||||
<ConditionSelectContent label={label} />
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">
|
||||
Forma de Pagamento
|
||||
</label>
|
||||
<FilterSelect
|
||||
param="pagamento"
|
||||
placeholder="Todos"
|
||||
options={buildStaticOptions(LANCAMENTO_PAYMENT_METHODS)}
|
||||
widthClass="w-full border-dashed"
|
||||
disabled={isPending}
|
||||
getParamValue={getParamValue}
|
||||
onChange={handleFilterChange}
|
||||
renderContent={(label) => (
|
||||
<PaymentMethodSelectContent label={label} />
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">
|
||||
Forma de Pagamento
|
||||
</label>
|
||||
<FilterSelect
|
||||
param="pagamento"
|
||||
placeholder="Todos"
|
||||
options={buildStaticOptions(LANCAMENTO_PAYMENT_METHODS)}
|
||||
widthClass="w-full border-dashed"
|
||||
disabled={isPending}
|
||||
getParamValue={getParamValue}
|
||||
onChange={handleFilterChange}
|
||||
renderContent={(label) => (
|
||||
<PaymentMethodSelectContent label={label} />
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Pagador</label>
|
||||
<Select
|
||||
value={getParamValue("pagador")}
|
||||
onValueChange={(value) =>
|
||||
handleFilterChange(
|
||||
"pagador",
|
||||
value === FILTER_EMPTY_VALUE ? null : value,
|
||||
)
|
||||
}
|
||||
disabled={isPending}
|
||||
>
|
||||
<SelectTrigger
|
||||
className="w-full text-sm border-dashed"
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Pagador</label>
|
||||
<Select
|
||||
value={getParamValue("pagador")}
|
||||
onValueChange={(value) =>
|
||||
handleFilterChange(
|
||||
"pagador",
|
||||
value === FILTER_EMPTY_VALUE ? null : value,
|
||||
)
|
||||
}
|
||||
disabled={isPending}
|
||||
>
|
||||
<span className="truncate">
|
||||
{selectedPagador ? (
|
||||
<PagadorSelectContent
|
||||
label={selectedPagador.label}
|
||||
avatarUrl={selectedPagador.avatarUrl}
|
||||
/>
|
||||
) : (
|
||||
"Todos"
|
||||
)}
|
||||
</span>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={FILTER_EMPTY_VALUE}>Todos</SelectItem>
|
||||
{pagadorSelectOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
<PagadorSelectContent
|
||||
label={option.label}
|
||||
avatarUrl={option.avatarUrl}
|
||||
/>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Categoria</label>
|
||||
<Popover
|
||||
open={categoriaOpen}
|
||||
onOpenChange={setCategoriaOpen}
|
||||
modal
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={categoriaOpen}
|
||||
className="w-full justify-between text-sm border-dashed"
|
||||
<SelectTrigger
|
||||
className="w-full text-sm border-dashed"
|
||||
disabled={isPending}
|
||||
>
|
||||
<span className="truncate flex items-center gap-2">
|
||||
{selectedCategoria ? (
|
||||
<CategoriaSelectContent
|
||||
label={selectedCategoria.label}
|
||||
icon={selectedCategoria.icon}
|
||||
<span className="truncate">
|
||||
{selectedPagador ? (
|
||||
<PagadorSelectContent
|
||||
label={selectedPagador.label}
|
||||
avatarUrl={selectedPagador.avatarUrl}
|
||||
/>
|
||||
) : (
|
||||
"Todas"
|
||||
"Todos"
|
||||
)}
|
||||
</span>
|
||||
<RiExpandUpDownLine className="ml-2 size-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="start" className="w-[220px] p-0">
|
||||
<Command>
|
||||
<CommandInput placeholder="Buscar categoria..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>Nada encontrado.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
<CommandItem
|
||||
value={FILTER_EMPTY_VALUE}
|
||||
onSelect={() => {
|
||||
handleFilterChange("categoria", null);
|
||||
setCategoriaOpen(false);
|
||||
}}
|
||||
>
|
||||
Todas
|
||||
{categoriaValue === FILTER_EMPTY_VALUE ? (
|
||||
<RiCheckLine className="ml-auto size-4" />
|
||||
) : null}
|
||||
</CommandItem>
|
||||
{categoriaOptions.map((option) => (
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={FILTER_EMPTY_VALUE}>Todos</SelectItem>
|
||||
{pagadorSelectOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
<PagadorSelectContent
|
||||
label={option.label}
|
||||
avatarUrl={option.avatarUrl}
|
||||
/>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Categoria</label>
|
||||
<Popover
|
||||
open={categoriaOpen}
|
||||
onOpenChange={setCategoriaOpen}
|
||||
modal
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={categoriaOpen}
|
||||
className="w-full justify-between text-sm border-dashed"
|
||||
disabled={isPending}
|
||||
>
|
||||
<span className="truncate flex items-center gap-2">
|
||||
{selectedCategoria ? (
|
||||
<CategoriaSelectContent
|
||||
label={selectedCategoria.label}
|
||||
icon={selectedCategoria.icon}
|
||||
/>
|
||||
) : (
|
||||
"Todas"
|
||||
)}
|
||||
</span>
|
||||
<RiExpandUpDownLine className="ml-2 size-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="start" className="w-[220px] p-0">
|
||||
<Command>
|
||||
<CommandInput placeholder="Buscar categoria..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>Nada encontrado.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
<CommandItem
|
||||
key={option.slug}
|
||||
value={option.slug}
|
||||
value={FILTER_EMPTY_VALUE}
|
||||
onSelect={() => {
|
||||
handleFilterChange("categoria", option.slug);
|
||||
handleFilterChange("categoria", null);
|
||||
setCategoriaOpen(false);
|
||||
}}
|
||||
>
|
||||
<CategoriaSelectContent
|
||||
label={option.label}
|
||||
icon={option.icon}
|
||||
/>
|
||||
{categoriaValue === option.slug ? (
|
||||
Todas
|
||||
{categoriaValue === FILTER_EMPTY_VALUE ? (
|
||||
<RiCheckLine className="ml-auto size-4" />
|
||||
) : null}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
{categoriaOptions.map((option) => (
|
||||
<CommandItem
|
||||
key={option.slug}
|
||||
value={option.slug}
|
||||
onSelect={() => {
|
||||
handleFilterChange("categoria", option.slug);
|
||||
setCategoriaOpen(false);
|
||||
}}
|
||||
>
|
||||
<CategoriaSelectContent
|
||||
label={option.label}
|
||||
icon={option.icon}
|
||||
/>
|
||||
{categoriaValue === option.slug ? (
|
||||
<RiCheckLine className="ml-auto size-4" />
|
||||
) : null}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Conta/Cartão</label>
|
||||
<Select
|
||||
value={getParamValue("contaCartao")}
|
||||
onValueChange={(value) =>
|
||||
handleFilterChange(
|
||||
"contaCartao",
|
||||
value === FILTER_EMPTY_VALUE ? null : value,
|
||||
)
|
||||
}
|
||||
disabled={isPending}
|
||||
>
|
||||
<SelectTrigger
|
||||
className="w-full text-sm border-dashed"
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Conta/Cartão</label>
|
||||
<Select
|
||||
value={getParamValue("contaCartao")}
|
||||
onValueChange={(value) =>
|
||||
handleFilterChange(
|
||||
"contaCartao",
|
||||
value === FILTER_EMPTY_VALUE ? null : value,
|
||||
)
|
||||
}
|
||||
disabled={isPending}
|
||||
>
|
||||
<span className="truncate">
|
||||
{selectedContaCartao ? (
|
||||
<ContaCartaoSelectContent
|
||||
label={selectedContaCartao.label}
|
||||
logo={selectedContaCartao.logo}
|
||||
isCartao={selectedContaCartao.kind === "cartao"}
|
||||
/>
|
||||
) : (
|
||||
"Todos"
|
||||
)}
|
||||
</span>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={FILTER_EMPTY_VALUE}>Todos</SelectItem>
|
||||
{contaOptions.length > 0 ? (
|
||||
<SelectGroup>
|
||||
<SelectLabel>Contas</SelectLabel>
|
||||
{contaOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
<ContaCartaoSelectContent
|
||||
label={option.label}
|
||||
logo={option.logo}
|
||||
isCartao={false}
|
||||
/>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
) : null}
|
||||
{cartaoOptions.length > 0 ? (
|
||||
<SelectGroup>
|
||||
<SelectLabel>Cartões</SelectLabel>
|
||||
{cartaoOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
<ContaCartaoSelectContent
|
||||
label={option.label}
|
||||
logo={option.logo}
|
||||
isCartao={true}
|
||||
/>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
) : null}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<SelectTrigger
|
||||
className="w-full text-sm border-dashed"
|
||||
disabled={isPending}
|
||||
>
|
||||
<span className="truncate">
|
||||
{selectedContaCartao ? (
|
||||
<ContaCartaoSelectContent
|
||||
label={selectedContaCartao.label}
|
||||
logo={selectedContaCartao.logo}
|
||||
isCartao={selectedContaCartao.kind === "cartao"}
|
||||
/>
|
||||
) : (
|
||||
"Todos"
|
||||
)}
|
||||
</span>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={FILTER_EMPTY_VALUE}>Todos</SelectItem>
|
||||
{contaOptions.length > 0 ? (
|
||||
<SelectGroup>
|
||||
<SelectLabel>Contas</SelectLabel>
|
||||
{contaOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
<ContaCartaoSelectContent
|
||||
label={option.label}
|
||||
logo={option.logo}
|
||||
isCartao={false}
|
||||
/>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
) : null}
|
||||
{cartaoOptions.length > 0 ? (
|
||||
<SelectGroup>
|
||||
<SelectLabel>Cartões</SelectLabel>
|
||||
{cartaoOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
<ContaCartaoSelectContent
|
||||
label={option.label}
|
||||
logo={option.logo}
|
||||
isCartao={true}
|
||||
/>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
) : null}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DrawerFooter>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={handleResetFilters}
|
||||
disabled={isPending || !hasActiveFilters}
|
||||
>
|
||||
Limpar filtros
|
||||
</Button>
|
||||
</DrawerFooter>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
)}
|
||||
<DrawerFooter>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={handleResetFilters}
|
||||
disabled={isPending || !hasActiveFilters}
|
||||
>
|
||||
Limpar filtros
|
||||
</Button>
|
||||
</DrawerFooter>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
RiAddCircleFill,
|
||||
RiAddCircleLine,
|
||||
RiArrowLeftRightLine,
|
||||
RiArrowRightSLine,
|
||||
RiChat1Line,
|
||||
RiCheckLine,
|
||||
RiDeleteBin5Line,
|
||||
@@ -864,57 +863,45 @@ 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">
|
||||
{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"
|
||||
>
|
||||
<RiAddCircleLine className="size-4 text-success" />
|
||||
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"
|
||||
>
|
||||
<RiAddCircleLine className="size-4 text-destructive" />
|
||||
Nova Despesa
|
||||
</Button>
|
||||
</>
|
||||
) : null}
|
||||
{onMassAdd ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
onClick={onMassAdd}
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="size-8 shrink-0 md:size-9"
|
||||
>
|
||||
<RiAddCircleFill className="size-4" />
|
||||
<span className="sr-only">
|
||||
Adicionar múltiplos lançamentos
|
||||
</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Adicionar múltiplos lançamentos</p>
|
||||
</TooltipContent>
|
||||
</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 className="flex w-full flex-col gap-2 sm:w-auto sm:flex-row">
|
||||
{onCreate ? (
|
||||
<>
|
||||
<Button
|
||||
onClick={() => onCreate("Receita")}
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
<RiAddCircleLine className="size-4" />
|
||||
Nova Receita
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => onCreate("Despesa")}
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
<RiAddCircleLine className="size-4" />
|
||||
Nova Despesa
|
||||
</Button>
|
||||
</>
|
||||
) : null}
|
||||
{onMassAdd ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
onClick={onMassAdd}
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="hidden size-9 sm:inline-flex"
|
||||
>
|
||||
<RiAddCircleFill className="size-4" />
|
||||
<span className="sr-only">
|
||||
Adicionar múltiplos lançamentos
|
||||
</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Adicionar múltiplos lançamentos</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
<span className={showFilters ? "hidden sm:block" : ""} />
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -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,26 +86,32 @@ export function NavMenu() {
|
||||
onClick={close}
|
||||
preservePeriod
|
||||
>
|
||||
Dashboard
|
||||
dashboard
|
||||
</MobileLink>
|
||||
|
||||
{NAV_SECTIONS.map((section) => (
|
||||
<div key={section.label}>
|
||||
<MobileSectionLabel label={section.label} />
|
||||
{section.items.map((item) => (
|
||||
<MobileLink
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
icon={item.icon}
|
||||
onClick={close}
|
||||
badge={item.badge}
|
||||
preservePeriod={item.preservePeriod}
|
||||
>
|
||||
{item.label}
|
||||
</MobileLink>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
{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} />
|
||||
{mobileItems.map((item) => (
|
||||
<MobileLink
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
icon={item.icon}
|
||||
onClick={close}
|
||||
badge={item.badge}
|
||||
preservePeriod={item.preservePeriod}
|
||||
>
|
||||
{item.label}
|
||||
</MobileLink>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
<MobileSectionLabel label="Ferramentas" />
|
||||
<MobileTools onClose={close} />
|
||||
|
||||
@@ -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,41 +105,30 @@ 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">
|
||||
<BudgetDialog
|
||||
mode="create"
|
||||
categories={categories}
|
||||
defaultPeriod={selectedPeriod}
|
||||
trigger={
|
||||
<Button
|
||||
disabled={categories.length === 0}
|
||||
className="h-8 shrink-0 px-3 text-xs md:h-9 md:px-4 md:text-sm"
|
||||
>
|
||||
<RiAddCircleLine className="size-4" />
|
||||
Novo orçamento
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:gap-3">
|
||||
<BudgetDialog
|
||||
mode="create"
|
||||
categories={categories}
|
||||
defaultPeriod={selectedPeriod}
|
||||
trigger={
|
||||
<Button
|
||||
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
|
||||
<RiAddCircleLine className="size-4" />
|
||||
Novo orçamento
|
||||
</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
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
disabled={categories.length === 0}
|
||||
onClick={() => setDuplicateOpen(true)}
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
<RiArrowRightSLine className="size-5 shrink-0 text-muted-foreground" />
|
||||
</div>
|
||||
<RiFileCopyLine className="size-4" />
|
||||
Copiar orçamentos do último mês
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{hasBudgets ? (
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user