From 88c1e9c962b6a3e47d9bc621271b0e7cab835a18 Mon Sep 17 00:00:00 2001 From: johnlaff <46091881+johnlaff@users.noreply.github.com> Date: Fri, 10 Jul 2026 08:22:29 -0300 Subject: [PATCH] =?UTF-8?q?feat(dashboard):=20"Hoje"=20em=20destaque=20com?= =?UTF-8?q?=20anel=20+=20Semana/M=C3=AAs=20lado=20a=20lado?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit O layout mobile dos resumos deixava metade do card vazia à direita (cards de largura intrínseca, alinhados à esquerda). Agora "Hoje" é um card em destaque com anel de progresso (feito/previsto) e Semana/Mês ocupam a largura em dois cards lado a lado. O saldo ganha cor semântica (vermelho = débito, verde = crédito), com o sinal "−" preservando a leitura sem depender só da cor. Atualiza o e2e de navegação mobile para o novo layout (Hoje full-width, Semana/Mês na mesma linha, sem overflow) — a asserção anterior fixava o formato intrínseco que a usuária pediu para trocar. --- e2e/mobile-navigation.spec.ts | 51 +++++----- src/components/daily-summary.tsx | 166 +++++++++++++++++++++++-------- 2 files changed, 150 insertions(+), 67 deletions(-) diff --git a/e2e/mobile-navigation.spec.ts b/e2e/mobile-navigation.spec.ts index 5b1c9ff..6d90812 100644 --- a/e2e/mobile-navigation.spec.ts +++ b/e2e/mobile-navigation.spec.ts @@ -12,35 +12,36 @@ async function openDashboard(page: Page, width: number) { } async function expectSummaryCardsFit(page: Page, contentWidth: number) { - for (const card of [ - page.getByTestId('summary-card-today'), - page.getByTestId('summary-card-week'), - page.getByTestId('summary-card-month'), - ]) { - await expect(card).toBeVisible() - const metrics = await card.evaluate((element) => { - const style = getComputedStyle(element) - return { - clientHeight: element.clientHeight, - clientWidth: element.clientWidth, - gap: Number.parseFloat(style.rowGap), - paddingBottom: Number.parseFloat(style.paddingBottom), - paddingTop: Number.parseFloat(style.paddingTop), - scrollHeight: element.scrollHeight, - scrollWidth: element.scrollWidth, - width: element.getBoundingClientRect().width, - } - }) + const today = page.getByTestId('summary-card-today') + const week = page.getByTestId('summary-card-week') + const month = page.getByTestId('summary-card-month') - // Com os dados normais da conta de teste, os três resumos devem continuar - // intrínsecos — ocupar toda a coluna reintroduziria a área vazia reportada. - expect(metrics.width).toBeLessThan(contentWidth) + for (const card of [today, week, month]) { + await expect(card).toBeVisible() + const metrics = await card.evaluate((element) => ({ + clientHeight: element.clientHeight, + clientWidth: element.clientWidth, + scrollHeight: element.scrollHeight, + scrollWidth: element.scrollWidth, + })) + // Nenhum card pode ter overflow interno, nem com saldos longos. expect(metrics.scrollWidth).toBeLessThanOrEqual(metrics.clientWidth) expect(metrics.scrollHeight).toBeLessThanOrEqual(metrics.clientHeight) - expect(metrics.gap).toBe(0) - expect(metrics.paddingTop).toBe(0) - expect(metrics.paddingBottom).toBe(0) } + + const [todayBox, weekBox, monthBox] = await Promise.all([ + today.boundingBox(), + week.boundingBox(), + month.boundingBox(), + ]) + if (!todayBox || !weekBox || !monthBox) throw new Error('summary cards sem boundingBox') + + // "Hoje" em destaque ocupa (quase) toda a largura da coluna — sem a área vazia à direita. + expect(todayBox.width).toBeGreaterThan(contentWidth * 0.9) + // Semana e Mês ficam lado a lado (mesma linha) e cada um cabe na metade da coluna. + expect(Math.abs(weekBox.y - monthBox.y)).toBeLessThanOrEqual(2) + expect(weekBox.width).toBeLessThanOrEqual(contentWidth / 2 + 2) + expect(monthBox.width).toBeLessThanOrEqual(contentWidth / 2 + 2) } async function stubExtremeSummary(page: Page) { diff --git a/src/components/daily-summary.tsx b/src/components/daily-summary.tsx index 20f693f..47454d7 100644 --- a/src/components/daily-summary.tsx +++ b/src/components/daily-summary.tsx @@ -1,72 +1,154 @@ 'use client' import { m } from 'motion/react' -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { Card } from '@/components/ui/card' import { ActivityTag } from '@/components/activity-selector' import { formatBRT, formatMinutes } from '@/lib/dates' +import { cn } from '@/lib/utils' import type { BalanceSummary, DailySummary } from '@/types' interface DailySummaryProps { summary: DailySummary } -function BalanceCard({ +const REVEAL_EASE = [0.16, 1, 0.3, 1] as const + +/** Saldo negativo (débito) em vermelho, positivo (crédito) em verde, zero neutro. */ +function saldoClass(minutes: number): string { + if (minutes < 0) return 'text-destructive' + if (minutes > 0) return 'text-emerald-600 dark:text-emerald-400' + return 'text-foreground' +} + +function ProgressRing({ actual, expected }: { actual: number; expected: number }) { + const radius = 24 + const circumference = 2 * Math.PI * radius + const ratio = expected > 0 ? Math.min(actual / expected, 1) : 0 + const label = expected > 0 ? `${Math.round((actual / expected) * 100)}%` : '—' + + return ( + // Decorativo: os números (feito, previsto, saldo) já estão no texto ao lado. +
+ ) +} + +/** "Hoje" em destaque: número grande + anel de progresso (feito/previsto). */ +function TodayCard({ balance }: { balance: BalanceSummary }) { + return ( +Hoje
++ {formatMinutes(balance.actualMinutes)} +
++ de {formatMinutes(balance.expectedMinutes)} · saldo{' '} + + {formatMinutes(balance.balanceMinutes)} + +
+{formatMinutes(balance.actualMinutes)}
-Previsto: {formatMinutes(balance.expectedMinutes)}
-Saldo: {formatMinutes(balance.balanceMinutes)}
- {cumulativeBalance != null &&Acumulado: {formatMinutes(cumulativeBalance)}
} -{title}
++ {formatMinutes(balance.actualMinutes)} +
++ saldo + + {formatMinutes(balance.balanceMinutes)} + +
+ {cumulative != null && ( ++ acum. {formatMinutes(cumulative)} +
+ )}