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() {
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-1">
<Skeleton className="h-8 w-48" />

View File

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

View File

@@ -72,11 +72,11 @@ export function NoteDetailsDialog({
</DialogHeader>
{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) => (
<Card
<div
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
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>
<span
className={`text-sm ${
task.completed ? "text-muted-foreground" : "text-foreground"
task.completed
? "text-muted-foreground line-through"
: "text-foreground"
}`}
>
{task.text}
</span>
</Card>
</div>
))}
</div>
</Card>
) : (
<div className="max-h-[320px] overflow-auto whitespace-pre-line wrap-break-word text-sm text-foreground">
{note.description}

View File

@@ -338,7 +338,7 @@ export function NoteDialog({
</div>
{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) => (
<div
key={task.id}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -16,8 +16,7 @@ import {
sortableKeyboardCoordinates,
} from "@dnd-kit/sortable";
import {
RiArrowDownLine,
RiArrowUpLine,
RiAddCircleLine,
RiCheckLine,
RiCloseLine,
RiDragMove2Line,
@@ -201,11 +200,11 @@ export function DashboardGridEditable({
{/* Toolbar */}
<div className="flex flex-wrap items-center justify-between gap-2">
{!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">
Ações rápidas
</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
mode="create"
pagadorOptions={quickActionOptions.pagadorOptions}
@@ -218,9 +217,16 @@ export function DashboardGridEditable({
defaultPeriod={period}
defaultTransactionType="Receita"
trigger={
<Button size="sm" variant="outline" className="gap-2">
<RiArrowUpLine className="size-4 text-success/80" />
Nova receita
<Button
size="sm"
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>
}
/>
@@ -236,18 +242,30 @@ export function DashboardGridEditable({
defaultPeriod={period}
defaultTransactionType="Despesa"
trigger={
<Button size="sm" variant="outline" className="gap-2">
<RiArrowDownLine className="size-4 text-destructive/80" />
Nova despesa
<Button
size="sm"
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>
}
/>
<NoteDialog
mode="create"
trigger={
<Button size="sm" variant="outline" className="gap-2">
<RiTodoLine className="size-4 text-info/80" />
Nova anotação
<Button
size="sm"
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>
}
/>
@@ -257,7 +275,7 @@ export function DashboardGridEditable({
<div />
)}
<div className="flex items-center gap-2">
<div className="flex w-full items-center justify-end gap-2 sm:w-auto">
{isEditing ? (
<>
<Button
@@ -281,22 +299,23 @@ export function DashboardGridEditable({
</Button>
</>
) : (
<>
<div className="grid w-full grid-cols-2 gap-2 sm:flex sm:w-auto">
<WidgetSettingsDialog
hiddenWidgets={hiddenWidgets}
onToggleWidget={handleToggleWidget}
onReset={handleReset}
triggerClassName="w-full sm:w-auto"
/>
<Button
variant="outline"
size="sm"
onClick={handleStartEditing}
className="gap-2"
className="w-full gap-2 sm:w-auto"
>
<RiDragMove2Line className="size-4" />
Reordenar
</Button>
</>
</div>
)}
</div>
</div>

View File

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

View File

@@ -1,7 +1,6 @@
import type {
InstallmentAnalysisData,
InstallmentGroup,
PendingInvoice,
} 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
className="max-w-md"
className="max-w-[calc(100%-2rem)] sm:max-w-md"
onEscapeKeyDown={(event) => {
if (modalState === "processing") {
event.preventDefault();

View File

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

View File

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

View File

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

View File

@@ -277,7 +277,7 @@ export function LancamentosFilters({
<div className="flex w-full gap-2 md:w-auto">
{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}
</div>
)}
@@ -291,13 +291,13 @@ export function LancamentosFilters({
<DrawerTrigger asChild>
<Button
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"
>
<RiFilter3Line className="size-4" />
Filtros
{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>
</DrawerTrigger>

View File

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

View File

@@ -2,7 +2,7 @@
import {
RiDeleteBin5Line,
RiEyeLine,
RiFileList2Line,
RiMailSendLine,
RiPencilLine,
RiVerifiedBadgeFill,
@@ -101,7 +101,7 @@ export function PagadorCard({ pagador, onEdit, onRemove }: PagadorCardProps) {
href={`/pagadores/${pagador.id}`}
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
</Link>

View File

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

View File

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

View File

@@ -80,14 +80,14 @@ export function CardUsageChart({ data, limit, card }: CardUsageChartProps) {
return (
<Card>
<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">
<RiBarChartBoxLine className="size-4 text-primary" />
Histórico de Uso
</CardTitle>
{/* Card logo and name */}
<div className="flex items-center gap-2">
<div className="flex min-w-0 items-center gap-2">
{logoPath ? (
<Image
src={logoPath}
@@ -99,13 +99,13 @@ export function CardUsageChart({ data, limit, card }: CardUsageChartProps) {
) : (
<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}
</span>
</div>
</div>
</CardHeader>
<CardContent>
<CardContent className="px-2 sm:px-6">
<ChartContainer config={chartConfig} className="h-[280px] w-full">
<BarChart
data={chartData}

View File

@@ -10,10 +10,17 @@ function clampPosition(
elementWidth: number,
elementHeight: number,
): Position {
const maxX = window.innerWidth - MIN_VISIBLE_PX;
const minX = MIN_VISIBLE_PX - elementWidth;
const maxY = window.innerHeight - MIN_VISIBLE_PX;
const minY = MIN_VISIBLE_PX - elementHeight;
// Dialog starts centered (left/top 50% + translate(-50%, -50%)).
// Clamp offsets so at least MIN_VISIBLE_PX remains visible on each axis.
const halfViewportWidth = window.innerWidth / 2;
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 {
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) {
el.style.translate = "";
el.style.transform = "";
} 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);
offset.current = clamped;
applyTranslate(el, clamped.x, clamped.y);
applyPosition(el, clamped.x, clamped.y);
}, []);
const onPointerUp = useCallback((e: React.PointerEvent<HTMLElement>) => {
dragStart.current = null;
(e.currentTarget as HTMLElement).releasePointerCapture(e.pointerId);
if ((e.currentTarget as HTMLElement).hasPointerCapture(e.pointerId)) {
(e.currentTarget as HTMLElement).releasePointerCapture(e.pointerId);
}
}, []);
const onPointerCancel = useCallback(() => {
dragStart.current = null;
}, []);
const onLostPointerCapture = useCallback(() => {
dragStart.current = null;
}, []);
const resetPosition = useCallback(() => {
offset.current = { x: 0, y: 0 };
if (contentRef.current) {
applyTranslate(contentRef.current, 0, 0);
applyPosition(contentRef.current, 0, 0);
}
}, []);
@@ -75,6 +95,8 @@ export function useDraggableDialog() {
onPointerDown,
onPointerMove,
onPointerUp,
onPointerCancel,
onLostPointerCapture,
style: { touchAction: "none" as const, cursor: "grab" },
};