feat: melhora responsividade e dialogos da interface

This commit is contained in:
Felipe Coutinho
2026-03-06 13:59:38 +00:00
parent 0e4dbe6a3f
commit d60eb7dd8b
23 changed files with 149 additions and 82 deletions

View File

@@ -3,7 +3,7 @@ import { Skeleton } from "@/components/ui/skeleton";
export default function Loading() { export default function Loading() {
return ( return (
<main className="flex flex-col gap-4 px-6"> <main className="flex flex-col gap-4">
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between"> <div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<Skeleton className="h-8 w-48" /> <Skeleton className="h-8 w-48" />

View File

@@ -4,7 +4,7 @@ import {
RiArchiveLine, RiArchiveLine,
RiCheckLine, RiCheckLine,
RiDeleteBin5Line, RiDeleteBin5Line,
RiEyeLine, RiFileList2Line,
RiInboxUnarchiveLine, RiInboxUnarchiveLine,
RiPencilLine, RiPencilLine,
} from "@remixicon/react"; } from "@remixicon/react";
@@ -60,7 +60,7 @@ export function NoteCard({
}, },
{ {
label: "detalhes", label: "detalhes",
icon: <RiEyeLine className="size-4" aria-hidden />, icon: <RiFileList2Line className="size-4" aria-hidden />,
onClick: onDetails, onClick: onDetails,
variant: "default" as const, variant: "default" as const,
}, },
@@ -115,7 +115,9 @@ export function NoteCard({
</div> </div>
<span <span
className={`leading-relaxed ${ className={`leading-relaxed ${
task.completed ? "text-muted-foreground" : "text-foreground" task.completed
? "text-muted-foreground line-through"
: "text-foreground"
}`} }`}
> >
{task.text} {task.text}

View File

@@ -72,11 +72,11 @@ export function NoteDetailsDialog({
</DialogHeader> </DialogHeader>
{isTask ? ( {isTask ? (
<div className="max-h-[320px] overflow-auto space-y-3"> <Card className="max-h-[320px] overflow-auto gap-2 p-2">
{sortedTasks.map((task) => ( {sortedTasks.map((task) => (
<Card <div
key={task.id} key={task.id}
className="flex gap-3 p-3 flex-row items-center" className="flex items-center gap-3 px-3 py-1.5 space-y-1 rounded-md hover:bg-muted/50"
> >
<div <div
className={`mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center rounded-sm border ${ className={`mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center rounded-sm border ${
@@ -91,14 +91,16 @@ export function NoteDetailsDialog({
</div> </div>
<span <span
className={`text-sm ${ className={`text-sm ${
task.completed ? "text-muted-foreground" : "text-foreground" task.completed
? "text-muted-foreground line-through"
: "text-foreground"
}`} }`}
> >
{task.text} {task.text}
</span> </span>
</Card>
))}
</div> </div>
))}
</Card>
) : ( ) : (
<div className="max-h-[320px] overflow-auto whitespace-pre-line wrap-break-word text-sm text-foreground"> <div className="max-h-[320px] overflow-auto whitespace-pre-line wrap-break-word text-sm text-foreground">
{note.description} {note.description}

View File

@@ -338,7 +338,7 @@ export function NoteDialog({
</div> </div>
{sortedTasks.length > 0 && ( {sortedTasks.length > 0 && (
<div className="space-y-1 max-h-[240px] overflow-y-auto pr-1"> <div className="space-y-1 max-h-[300px] overflow-y-auto pr-1 mt-4 rounded-md p-2 bg-card ">
{sortedTasks.map((task) => ( {sortedTasks.map((task) => (
<div <div
key={task.id} key={task.id}

View File

@@ -50,8 +50,11 @@ export function CalculatorDialogContent({
return ( return (
<DialogContent <DialogContent
ref={contentRefCallback} ref={contentRefCallback}
className="p-4 sm:max-w-sm" className="p-5 sm:max-w-sm sm:p-6"
onEscapeKeyDown={(e) => e.preventDefault()} onEscapeKeyDown={(e) => e.preventDefault()}
onPointerDownOutside={(e) => e.preventDefault()}
onFocusOutside={(e) => e.preventDefault()}
onInteractOutside={(e) => e.preventDefault()}
> >
<DialogHeader <DialogHeader
className="cursor-grab select-none space-y-2 active:cursor-grabbing" className="cursor-grab select-none space-y-2 active:cursor-grabbing"

View File

@@ -1,5 +1,6 @@
import { RiCheckLine, RiFileCopyLine } from "@remixicon/react"; import { RiCheckLine, RiFileCopyLine } from "@remixicon/react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils/ui";
export type CalculatorDisplayProps = { export type CalculatorDisplayProps = {
history: string | null; history: string | null;
@@ -7,6 +8,7 @@ export type CalculatorDisplayProps = {
resultText: string | null; resultText: string | null;
copied: boolean; copied: boolean;
onCopy: () => void; onCopy: () => void;
isResultView: boolean;
}; };
export function CalculatorDisplay({ export function CalculatorDisplay({
@@ -15,14 +17,27 @@ export function CalculatorDisplay({
resultText, resultText,
copied, copied,
onCopy, onCopy,
isResultView,
}: CalculatorDisplayProps) { }: CalculatorDisplayProps) {
return ( return (
<div className="rounded-xl border bg-muted px-4 py-5 text-right"> <div className="flex h-24 flex-col rounded-xl border bg-muted px-4 py-4 text-right">
{history && ( <div className="min-h-5 truncate text-sm text-muted-foreground">
<div className="text-sm text-muted-foreground">{history}</div> {history ?? (
<span
className="pointer-events-none opacity-0 select-none"
aria-hidden
>
0 + 0
</span>
)} )}
<div className="flex items-center justify-end gap-2"> </div>
<div className="text-right text-3xl font-semibold tracking-tight tabular-nums"> <div className="mt-auto flex items-end justify-end gap-2">
<div
className={cn(
"truncate text-right font-semibold tracking-tight tabular-nums leading-none transition-all",
isResultView ? "text-2xl" : "text-3xl",
)}
>
{expression} {expression}
</div> </div>
{resultText && ( {resultText && (

View File

@@ -64,6 +64,7 @@ export default function Calculator({
resultText={resultText} resultText={resultText}
copied={copied} copied={copied}
onCopy={copyToClipboard} onCopy={copyToClipboard}
isResultView={Boolean(history)}
/> />
<CalculatorKeypad buttons={buttons} activeOperator={operator} /> <CalculatorKeypad buttons={buttons} activeOperator={operator} />
{onSelectValue && ( {onSelectValue && (

View File

@@ -3,7 +3,7 @@
import { import {
RiChat3Line, RiChat3Line,
RiDeleteBin5Line, RiDeleteBin5Line,
RiEyeLine, RiFileList2Line,
RiPencilLine, RiPencilLine,
} from "@remixicon/react"; } from "@remixicon/react";
import Image from "next/image"; import Image from "next/image";
@@ -143,7 +143,7 @@ export function CardItem({
}, },
{ {
label: "ver fatura", label: "ver fatura",
icon: <RiEyeLine className="size-4" aria-hidden />, icon: <RiFileList2Line className="size-4" aria-hidden />,
onClick: onInvoice, onClick: onInvoice,
className: "text-primary", className: "text-primary",
}, },

View File

@@ -245,7 +245,7 @@ export function BoletosWidget({ boletos }: BoletosWidgetProps) {
}} }}
> >
<DialogContent <DialogContent
className="max-w-md" className="max-w-[calc(100%-2rem)] sm:max-w-md"
onEscapeKeyDown={(event) => { onEscapeKeyDown={(event) => {
if (isProcessing) { if (isProcessing) {
event.preventDefault(); event.preventDefault();

View File

@@ -16,8 +16,7 @@ import {
sortableKeyboardCoordinates, sortableKeyboardCoordinates,
} from "@dnd-kit/sortable"; } from "@dnd-kit/sortable";
import { import {
RiArrowDownLine, RiAddCircleLine,
RiArrowUpLine,
RiCheckLine, RiCheckLine,
RiCloseLine, RiCloseLine,
RiDragMove2Line, RiDragMove2Line,
@@ -201,11 +200,11 @@ export function DashboardGridEditable({
{/* Toolbar */} {/* Toolbar */}
<div className="flex flex-wrap items-center justify-between gap-2"> <div className="flex flex-wrap items-center justify-between gap-2">
{!isEditing ? ( {!isEditing ? (
<div className="flex min-w-0 flex-col gap-1 sm:flex-row sm:items-center sm:gap-2 px-1"> <div className="flex w-full min-w-0 flex-col gap-1 px-1 sm:w-auto sm:flex-row sm:items-center sm:gap-2">
<span className="text-[11px] font-semibold uppercase tracking-wide text-muted-foreground"> <span className="text-[11px] font-semibold uppercase tracking-wide text-muted-foreground">
Ações rápidas Ações rápidas
</span> </span>
<div className="-mb-1 flex items-center gap-2 overflow-x-auto pb-1 sm:mb-0 sm:overflow-visible sm:pb-0"> <div className="-mb-1 grid w-full grid-cols-3 gap-1 pb-1 sm:mb-0 sm:flex sm:w-auto sm:items-center sm:gap-2 sm:overflow-visible sm:pb-0">
<LancamentoDialog <LancamentoDialog
mode="create" mode="create"
pagadorOptions={quickActionOptions.pagadorOptions} pagadorOptions={quickActionOptions.pagadorOptions}
@@ -218,9 +217,16 @@ export function DashboardGridEditable({
defaultPeriod={period} defaultPeriod={period}
defaultTransactionType="Receita" defaultTransactionType="Receita"
trigger={ trigger={
<Button size="sm" variant="outline" className="gap-2"> <Button
<RiArrowUpLine className="size-4 text-success/80" /> size="sm"
Nova receita variant="outline"
className="h-12 w-full min-w-0 flex-col justify-center gap-0.5 px-1.5 text-sm whitespace-normal sm:h-8 sm:w-auto sm:flex-row sm:gap-2 sm:px-3 sm:whitespace-nowrap"
>
<span className="flex items-center gap-0.5">
<RiAddCircleLine className="size-3.5 shrink-0 text-success/80" />
</span>
<span className="sm:hidden">Receita</span>
<span className="hidden sm:inline">Nova receita</span>
</Button> </Button>
} }
/> />
@@ -236,18 +242,30 @@ export function DashboardGridEditable({
defaultPeriod={period} defaultPeriod={period}
defaultTransactionType="Despesa" defaultTransactionType="Despesa"
trigger={ trigger={
<Button size="sm" variant="outline" className="gap-2"> <Button
<RiArrowDownLine className="size-4 text-destructive/80" /> size="sm"
Nova despesa variant="outline"
className="h-12 w-full min-w-0 flex-col justify-center gap-0.5 px-1.5 text-sm whitespace-normal sm:h-8 sm:w-auto sm:flex-row sm:gap-2 sm:px-3 sm:whitespace-nowrap"
>
<span className="flex items-center gap-0.5">
<RiAddCircleLine className="size-3.5 shrink-0 text-destructive/80" />
</span>
<span className="sm:hidden">Despesa</span>
<span className="hidden sm:inline">Nova despesa</span>
</Button> </Button>
} }
/> />
<NoteDialog <NoteDialog
mode="create" mode="create"
trigger={ trigger={
<Button size="sm" variant="outline" className="gap-2"> <Button
<RiTodoLine className="size-4 text-info/80" /> size="sm"
Nova anotação variant="outline"
className="h-12 w-full min-w-0 flex-col justify-center gap-0.5 px-1.5 text-sm whitespace-normal sm:h-8 sm:w-auto sm:flex-row sm:gap-2 sm:px-3 sm:whitespace-nowrap"
>
<RiTodoLine className="size-3.5 shrink-0 text-info/80" />
<span className="sm:hidden">Anotação</span>
<span className="hidden sm:inline">Nova anotação</span>
</Button> </Button>
} }
/> />
@@ -257,7 +275,7 @@ export function DashboardGridEditable({
<div /> <div />
)} )}
<div className="flex items-center gap-2"> <div className="flex w-full items-center justify-end gap-2 sm:w-auto">
{isEditing ? ( {isEditing ? (
<> <>
<Button <Button
@@ -281,22 +299,23 @@ export function DashboardGridEditable({
</Button> </Button>
</> </>
) : ( ) : (
<> <div className="grid w-full grid-cols-2 gap-2 sm:flex sm:w-auto">
<WidgetSettingsDialog <WidgetSettingsDialog
hiddenWidgets={hiddenWidgets} hiddenWidgets={hiddenWidgets}
onToggleWidget={handleToggleWidget} onToggleWidget={handleToggleWidget}
onReset={handleReset} onReset={handleReset}
triggerClassName="w-full sm:w-auto"
/> />
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
onClick={handleStartEditing} onClick={handleStartEditing}
className="gap-2" className="w-full gap-2 sm:w-auto"
> >
<RiDragMove2Line className="size-4" /> <RiDragMove2Line className="size-4" />
Reordenar Reordenar
</Button> </Button>
</> </div>
)} )}
</div> </div>
</div> </div>

View File

@@ -80,7 +80,7 @@ export function InstallmentGroupCard({
{group.cartaoLogo && ( {group.cartaoLogo && (
<img <img
src={`/logos/${group.cartaoLogo}`} src={`/logos/${group.cartaoLogo}`}
alt={group.cartaoName} alt={group.cartaoName ?? "Cartão"}
className="h-6 w-auto object-contain rounded" className="h-6 w-auto object-contain rounded"
/> />
)} )}

View File

@@ -1,7 +1,6 @@
import type { import type {
InstallmentAnalysisData, InstallmentAnalysisData,
InstallmentGroup, InstallmentGroup,
PendingInvoice,
} from "@/lib/dashboard/expenses/installment-analysis"; } from "@/lib/dashboard/expenses/installment-analysis";
export type { InstallmentAnalysisData, InstallmentGroup, PendingInvoice }; export type { InstallmentAnalysisData, InstallmentGroup };

View File

@@ -419,7 +419,7 @@ export function InvoicesWidget({ invoices }: InvoicesWidgetProps) {
}} }}
> >
<DialogContent <DialogContent
className="max-w-md" className="max-w-[calc(100%-2rem)] sm:max-w-md"
onEscapeKeyDown={(event) => { onEscapeKeyDown={(event) => {
if (modalState === "processing") { if (modalState === "processing") {
event.preventDefault(); event.preventDefault();

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import { RiEyeLine, RiPencilLine, RiTodoLine } from "@remixicon/react"; import { RiFileList2Line, RiPencilLine, RiTodoLine } from "@remixicon/react";
import { useCallback, useMemo, useState } from "react"; import { useCallback, useMemo, useState } from "react";
import { NoteDetailsDialog } from "@/components/anotacoes/note-details-dialog"; import { NoteDetailsDialog } from "@/components/anotacoes/note-details-dialog";
import { NoteDialog } from "@/components/anotacoes/note-dialog"; import { NoteDialog } from "@/components/anotacoes/note-dialog";
@@ -100,13 +100,10 @@ export function NotesWidget({ notes }: NotesWidgetProps) {
{buildDisplayTitle(note.title)} {buildDisplayTitle(note.title)}
</p> </p>
<div className="mt-1 flex items-center gap-2"> <div className="mt-1 flex items-center gap-2">
<Badge <Badge variant="outline" className="h-5 px-1.5 text-[10px]">
variant="secondary"
className="h-5 px-1.5 text-[10px]"
>
{getTasksSummary(note)} {getTasksSummary(note)}
</Badge> </Badge>
<p className="truncate text-xs text-muted-foreground"> <p className="truncate text-[11px] text-muted-foreground">
{DATE_FORMATTER.format(new Date(note.createdAt))} {DATE_FORMATTER.format(new Date(note.createdAt))}
</p> </p>
</div> </div>
@@ -131,7 +128,7 @@ export function NotesWidget({ notes }: NotesWidgetProps) {
note.title, note.title,
)}`} )}`}
> >
<RiEyeLine className="size-4" /> <RiFileList2Line className="size-4" />
</Button> </Button>
</div> </div>
</li> </li>

View File

@@ -14,24 +14,31 @@ import {
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { widgetsConfig } from "@/lib/dashboard/widgets/widgets-config"; import { widgetsConfig } from "@/lib/dashboard/widgets/widgets-config";
import { cn } from "@/lib/utils";
type WidgetSettingsDialogProps = { type WidgetSettingsDialogProps = {
hiddenWidgets: string[]; hiddenWidgets: string[];
onToggleWidget: (widgetId: string) => void; onToggleWidget: (widgetId: string) => void;
onReset: () => void; onReset: () => void;
triggerClassName?: string;
}; };
export function WidgetSettingsDialog({ export function WidgetSettingsDialog({
hiddenWidgets, hiddenWidgets,
onToggleWidget, onToggleWidget,
onReset, onReset,
triggerClassName,
}: WidgetSettingsDialogProps) { }: WidgetSettingsDialogProps) {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
return ( return (
<Dialog open={open} onOpenChange={setOpen}> <Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button variant="outline" size="sm" className="gap-2"> <Button
variant="outline"
size="sm"
className={cn("gap-2", triggerClassName)}
>
<RiSettings4Line className="size-4" /> <RiSettings4Line className="size-4" />
Widgets Widgets
</Button> </Button>

View File

@@ -53,9 +53,9 @@ export function LancamentoDetailsDialog({
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="p-0 sm:max-w-xl"> <DialogContent className="p-0 sm:max-w-xl sm:border-0 sm:p-2">
<div className="gap-2 space-y-4 py-6"> <div className="gap-2 space-y-4 py-4">
<CardHeader className="flex flex-row items-start border-b"> <CardHeader className="flex flex-row items-start border-b sm:border-b-0">
<div> <div>
<DialogTitle className="group flex items-center gap-2 text-lg"> <DialogTitle className="group flex items-center gap-2 text-lg">
#{lancamento.id} #{lancamento.id}

View File

@@ -277,7 +277,7 @@ export function LancamentosFilters({
<div className="flex w-full gap-2 md:w-auto"> <div className="flex w-full gap-2 md:w-auto">
{exportButton && ( {exportButton && (
<div className="flex-1 md:flex-none [&>*]:w-full [&>*]:md:w-auto"> <div className="flex-1 md:flex-none *:w-full *:md:w-auto">
{exportButton} {exportButton}
</div> </div>
)} )}
@@ -291,13 +291,13 @@ export function LancamentosFilters({
<DrawerTrigger asChild> <DrawerTrigger asChild>
<Button <Button
variant="outline" variant="outline"
className="flex-1 md:flex-none text-sm border-dashed relative" className="flex-1 md:flex-none text-sm border-dashed relative bg-transparent"
aria-label="Abrir filtros" aria-label="Abrir filtros"
> >
<RiFilter3Line className="size-4" /> <RiFilter3Line className="size-4" />
Filtros Filtros
{hasActiveFilters && ( {hasActiveFilters && (
<span className="absolute -top-1 -right-1 size-2 rounded-full bg-primary" /> <span className="absolute -top-1 -right-1 size-3 rounded-full bg-primary" />
)} )}
</Button> </Button>
</DrawerTrigger> </DrawerTrigger>

View File

@@ -6,8 +6,8 @@ import {
RiChat1Line, RiChat1Line,
RiCheckLine, RiCheckLine,
RiDeleteBin5Line, RiDeleteBin5Line,
RiEyeLine,
RiFileCopyLine, RiFileCopyLine,
RiFileList2Line,
RiGroupLine, RiGroupLine,
RiHistoryLine, RiHistoryLine,
RiMoreFill, RiMoreFill,
@@ -31,8 +31,8 @@ import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
import { CategoryIcon } from "@/components/categorias/category-icon"; import { CategoryIcon } from "@/components/categorias/category-icon";
import { EmptyState } from "@/components/empty-state";
import MoneyValues from "@/components/money-values"; import MoneyValues from "@/components/money-values";
import { EmptyState } from "@/components/shared/empty-state";
import { TypeBadge } from "@/components/type-badge"; import { TypeBadge } from "@/components/type-badge";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
@@ -588,7 +588,7 @@ const buildColumns = ({
<DropdownMenuItem <DropdownMenuItem
onSelect={() => handleViewDetails(row.original)} onSelect={() => handleViewDetails(row.original)}
> >
<RiEyeLine className="size-4" /> <RiFileList2Line className="size-4" />
Detalhes Detalhes
</DropdownMenuItem> </DropdownMenuItem>
{row.original.userId === currentUserId && ( {row.original.userId === currentUserId && (

View File

@@ -2,7 +2,7 @@
import { import {
RiDeleteBin5Line, RiDeleteBin5Line,
RiEyeLine, RiFileList2Line,
RiMailSendLine, RiMailSendLine,
RiPencilLine, RiPencilLine,
RiVerifiedBadgeFill, RiVerifiedBadgeFill,
@@ -101,7 +101,7 @@ export function PagadorCard({ pagador, onEdit, onRemove }: PagadorCardProps) {
href={`/pagadores/${pagador.id}`} href={`/pagadores/${pagador.id}`}
className={`text-primary flex items-center gap-1 font-medium transition-opacity hover:opacity-80`} className={`text-primary flex items-center gap-1 font-medium transition-opacity hover:opacity-80`}
> >
<RiEyeLine className="size-4" aria-hidden /> <RiFileList2Line className="size-4" aria-hidden />
detalhes detalhes
</Link> </Link>

View File

@@ -36,7 +36,7 @@ export function CardCategoryBreakdown({ data }: CardCategoryBreakdownProps) {
const _totalAmount = data.reduce((acc, c) => acc + c.amount, 0); const _totalAmount = data.reduce((acc, c) => acc + c.amount, 0);
return ( return (
<Card className="h-full"> <Card className="h-full overflow-hidden">
<CardHeader className="pb-3"> <CardHeader className="pb-3">
<CardTitle className="flex items-center gap-1.5 text-base"> <CardTitle className="flex items-center gap-1.5 text-base">
<RiPieChartLine className="size-4 text-primary" /> <RiPieChartLine className="size-4 text-primary" />
@@ -44,7 +44,7 @@ export function CardCategoryBreakdown({ data }: CardCategoryBreakdownProps) {
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent className="pt-0"> <CardContent className="overflow-x-hidden pt-0">
<div className="flex flex-col"> <div className="flex flex-col">
{data.map((category, index) => ( {data.map((category, index) => (
<div <div
@@ -80,7 +80,7 @@ export function CardCategoryBreakdown({ data }: CardCategoryBreakdownProps) {
</div> </div>
{/* Progress bar */} {/* Progress bar */}
<div className="ml-11 mt-1.5"> <div className="pl-11 mt-1.5">
<Progress className="h-1.5" value={category.percent} /> <Progress className="h-1.5" value={category.percent} />
</div> </div>
</div> </div>

View File

@@ -38,14 +38,14 @@ export function CardTopExpenses({ data }: CardTopExpensesProps) {
const maxAmount = Math.max(...data.map((e) => e.amount)); const maxAmount = Math.max(...data.map((e) => e.amount));
return ( return (
<Card className="h-full"> <Card className="h-full overflow-hidden">
<CardHeader className="pb-3"> <CardHeader className="pb-3">
<CardTitle className="flex items-center gap-1.5 text-base"> <CardTitle className="flex items-center gap-1.5 text-base">
<RiShoppingBag3Line className="size-4 text-primary" /> <RiShoppingBag3Line className="size-4 text-primary" />
Top 10 Gastos do Mês Top 10 Gastos do Mês
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent className="pt-0"> <CardContent className="overflow-x-hidden pt-0">
<div className="flex flex-col"> <div className="flex flex-col">
{data.map((expense, index) => ( {data.map((expense, index) => (
<div <div
@@ -66,14 +66,14 @@ export function CardTopExpenses({ data }: CardTopExpensesProps) {
<span className="text-sm font-medium truncate block"> <span className="text-sm font-medium truncate block">
{expense.name} {expense.name}
</span> </span>
<div className="flex items-center gap-1 mt-0.5 flex-wrap"> <div className="mt-0.5 flex min-w-0 flex-col gap-0.5">
<span className="text-xs text-muted-foreground"> <span className="text-xs text-muted-foreground">
{expense.date} {expense.date}
</span> </span>
{expense.category && ( {expense.category && (
<Badge <Badge
variant="secondary" variant="secondary"
className="text-xs px-1.5 py-0 h-5" className="h-5 max-w-full px-1.5 py-0 text-xs truncate"
> >
{expense.category} {expense.category}
</Badge> </Badge>
@@ -92,7 +92,7 @@ export function CardTopExpenses({ data }: CardTopExpensesProps) {
</div> </div>
{/* Progress bar */} {/* Progress bar */}
<div className="ml-12 mt-1.5"> <div className="pl-12 mt-1.5">
<Progress <Progress
className="h-1.5" className="h-1.5"
value={(expense.amount / maxAmount) * 100} value={(expense.amount / maxAmount) * 100}

View File

@@ -80,14 +80,14 @@ export function CardUsageChart({ data, limit, card }: CardUsageChartProps) {
return ( return (
<Card> <Card>
<CardHeader className="pb-2"> <CardHeader className="pb-2">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between gap-2">
<CardTitle className="flex items-center gap-1.5 text-base"> <CardTitle className="flex items-center gap-1.5 text-base">
<RiBarChartBoxLine className="size-4 text-primary" /> <RiBarChartBoxLine className="size-4 text-primary" />
Histórico de Uso Histórico de Uso
</CardTitle> </CardTitle>
{/* Card logo and name */} {/* Card logo and name */}
<div className="flex items-center gap-2"> <div className="flex min-w-0 items-center gap-2">
{logoPath ? ( {logoPath ? (
<Image <Image
src={logoPath} src={logoPath}
@@ -99,13 +99,13 @@ export function CardUsageChart({ data, limit, card }: CardUsageChartProps) {
) : ( ) : (
<RiBankCard2Line className="size-5 text-muted-foreground" /> <RiBankCard2Line className="size-5 text-muted-foreground" />
)} )}
<span className="text-sm font-medium text-muted-foreground"> <span className="max-w-24 truncate text-sm font-medium text-muted-foreground sm:max-w-none">
{card.name} {card.name}
</span> </span>
</div> </div>
</div> </div>
</CardHeader> </CardHeader>
<CardContent> <CardContent className="px-2 sm:px-6">
<ChartContainer config={chartConfig} className="h-[280px] w-full"> <ChartContainer config={chartConfig} className="h-[280px] w-full">
<BarChart <BarChart
data={chartData} data={chartData}

View File

@@ -10,10 +10,17 @@ function clampPosition(
elementWidth: number, elementWidth: number,
elementHeight: number, elementHeight: number,
): Position { ): Position {
const maxX = window.innerWidth - MIN_VISIBLE_PX; // Dialog starts centered (left/top 50% + translate(-50%, -50%)).
const minX = MIN_VISIBLE_PX - elementWidth; // Clamp offsets so at least MIN_VISIBLE_PX remains visible on each axis.
const maxY = window.innerHeight - MIN_VISIBLE_PX; const halfViewportWidth = window.innerWidth / 2;
const minY = MIN_VISIBLE_PX - elementHeight; const halfViewportHeight = window.innerHeight / 2;
const halfElementWidth = elementWidth / 2;
const halfElementHeight = elementHeight / 2;
const minX = MIN_VISIBLE_PX - (halfViewportWidth + halfElementWidth);
const maxX = halfViewportWidth + halfElementWidth - MIN_VISIBLE_PX;
const minY = MIN_VISIBLE_PX - (halfViewportHeight + halfElementHeight);
const maxY = halfViewportHeight + halfElementHeight - MIN_VISIBLE_PX;
return { return {
x: Math.min(Math.max(x, minX), maxX), x: Math.min(Math.max(x, minX), maxX),
@@ -21,11 +28,14 @@ function clampPosition(
}; };
} }
function applyTranslate(el: HTMLElement, x: number, y: number) { function applyPosition(el: HTMLElement, x: number, y: number) {
if (x === 0 && y === 0) { if (x === 0 && y === 0) {
el.style.translate = ""; el.style.translate = "";
el.style.transform = "";
} else { } else {
el.style.translate = `${x}px ${y}px`; // Keep the dialog's centered baseline (-50%, -50%) and only add drag offset.
el.style.translate = `calc(-50% + ${x}px) calc(-50% + ${y}px)`;
el.style.transform = "";
} }
} }
@@ -56,18 +66,28 @@ export function useDraggableDialog() {
const clamped = clampPosition(rawX, rawY, el.offsetWidth, el.offsetHeight); const clamped = clampPosition(rawX, rawY, el.offsetWidth, el.offsetHeight);
offset.current = clamped; offset.current = clamped;
applyTranslate(el, clamped.x, clamped.y); applyPosition(el, clamped.x, clamped.y);
}, []); }, []);
const onPointerUp = useCallback((e: React.PointerEvent<HTMLElement>) => { const onPointerUp = useCallback((e: React.PointerEvent<HTMLElement>) => {
dragStart.current = null; dragStart.current = null;
if ((e.currentTarget as HTMLElement).hasPointerCapture(e.pointerId)) {
(e.currentTarget as HTMLElement).releasePointerCapture(e.pointerId); (e.currentTarget as HTMLElement).releasePointerCapture(e.pointerId);
}
}, []);
const onPointerCancel = useCallback(() => {
dragStart.current = null;
}, []);
const onLostPointerCapture = useCallback(() => {
dragStart.current = null;
}, []); }, []);
const resetPosition = useCallback(() => { const resetPosition = useCallback(() => {
offset.current = { x: 0, y: 0 }; offset.current = { x: 0, y: 0 };
if (contentRef.current) { if (contentRef.current) {
applyTranslate(contentRef.current, 0, 0); applyPosition(contentRef.current, 0, 0);
} }
}, []); }, []);
@@ -75,6 +95,8 @@ export function useDraggableDialog() {
onPointerDown, onPointerDown,
onPointerMove, onPointerMove,
onPointerUp, onPointerUp,
onPointerCancel,
onLostPointerCapture,
style: { touchAction: "none" as const, cursor: "grab" }, style: { touchAction: "none" as const, cursor: "grab" },
}; };