From ef3731de6483fc0375c8ee49de886743b5ad3119 Mon Sep 17 00:00:00 2001 From: Rafael Martins Date: Sun, 2 Aug 2026 14:36:13 -0300 Subject: [PATCH 1/2] fix(ui): stop the hero value from wrapping mid-number The value used a fixed text-8xl with break-words, so anything longer than about nine characters broke inside the number itself: "R$ 58.480,37" rendered with the trailing "7" alone on a second line. Size it against the panel instead of the viewport: the wrapper becomes a container and the font scales with the character count, clamped between 1.5rem and the previous 6rem. Measured in Chromium at 375, 768, 1280, 1536 and 2560 px with values up to "R$ 926.150,68" - one line, no overflow, at every size. The size travels as a custom property because jsdom drops a fontSize holding clamp() with cqi, which would leave the behaviour untestable. Co-Authored-By: Claude Opus 5 --- __tests__/hero-panel.test.tsx | 13 +++++++++++++ components/molecules/hero-panel.tsx | 15 ++++++++++++--- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/__tests__/hero-panel.test.tsx b/__tests__/hero-panel.test.tsx index dd47b9d..c31a068 100644 --- a/__tests__/hero-panel.test.tsx +++ b/__tests__/hero-panel.test.tsx @@ -30,6 +30,19 @@ describe("HeroPanel", () => { expect(screen.getByText("Resumo Financeiro")).toBeInTheDocument(); }); + it("shrinks the font as the value gets longer so it never wraps", () => { + const readCqi = (element: HTMLElement) => Number.parseFloat(/([\d.]+)cqi/.exec(element.outerHTML)?.[1] ?? ""); + + const { rerender } = render(); + const shortValueCqi = readCqi(screen.getByText("R$ 25,00")); + + rerender(); + const longValueCqi = readCqi(screen.getByText("R$ 926.150,68")); + + expect(shortValueCqi).toBeGreaterThan(0); + expect(longValueCqi).toBeLessThan(shortValueCqi); + }); + it("omits the footer separator when there is no footer", () => { render(); diff --git a/components/molecules/hero-panel.tsx b/components/molecules/hero-panel.tsx index af0dbb6..5f15216 100644 --- a/components/molecules/hero-panel.tsx +++ b/components/molecules/hero-panel.tsx @@ -1,4 +1,4 @@ -import type { ElementType, ReactNode } from "react"; +import type { CSSProperties, ElementType, ReactNode } from "react"; import { cn } from "@/lib/utils"; const TONE_CLASSES = { @@ -7,6 +7,8 @@ const TONE_CLASSES = { blue: "bg-blue-600 shadow-blue-500/30", } as const; +const VALUE_INLINE_SIZE_CQI = 208; + interface HeroPanelProps { icon: ElementType; label: string; @@ -37,8 +39,15 @@ export function HeroPanel({ icon: Icon, label, value, tone, badge, footer, child {badge} -
-

+

+

{value}

{children} From 4c772f767aaf0a562e80024062f8406a2447cc87 Mon Sep 17 00:00:00 2001 From: Rafael Martins Date: Sun, 2 Aug 2026 15:42:25 -0300 Subject: [PATCH 2/2] fix(ui): keep the hero font size finite for an empty value Dividing by value.length gave Infinity for an empty string, so the custom property became clamp(1.5rem, Infinitycqi, 6rem) - invalid CSS that the browser drops entirely, falling back to the inherited size. The test now reads the custom property off element.style rather than regexing outerHTML, which would have matched any cqi in the subtree. Co-Authored-By: Claude Opus 5 --- __tests__/hero-panel.test.tsx | 17 +++++++++++++---- components/molecules/hero-panel.tsx | 2 +- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/__tests__/hero-panel.test.tsx b/__tests__/hero-panel.test.tsx index c31a068..f2c01f5 100644 --- a/__tests__/hero-panel.test.tsx +++ b/__tests__/hero-panel.test.tsx @@ -3,6 +3,10 @@ import { Clock } from "lucide-react"; import { describe, expect, it } from "vitest"; import { HeroPanel } from "@/components/molecules/hero-panel"; +function readValueCqi(element: HTMLElement): number { + return Number.parseFloat(/([\d.]+)cqi/.exec(element.style.getPropertyValue("--hero-value-size"))?.[1] ?? ""); +} + describe("HeroPanel", () => { it("renders the label and the highlighted value", () => { render(); @@ -31,18 +35,23 @@ describe("HeroPanel", () => { }); it("shrinks the font as the value gets longer so it never wraps", () => { - const readCqi = (element: HTMLElement) => Number.parseFloat(/([\d.]+)cqi/.exec(element.outerHTML)?.[1] ?? ""); - const { rerender } = render(); - const shortValueCqi = readCqi(screen.getByText("R$ 25,00")); + const shortValueCqi = readValueCqi(screen.getByText("R$ 25,00")); rerender(); - const longValueCqi = readCqi(screen.getByText("R$ 926.150,68")); + const longValueCqi = readValueCqi(screen.getByText("R$ 926.150,68")); expect(shortValueCqi).toBeGreaterThan(0); expect(longValueCqi).toBeLessThan(shortValueCqi); }); + it("still asks for a finite size when there is no value to show", () => { + const { container } = render(); + const valueElement = container.querySelector("p.font-black"); + + expect(readValueCqi(valueElement as HTMLElement)).toBeGreaterThan(0); + }); + it("omits the footer separator when there is no footer", () => { render(); diff --git a/components/molecules/hero-panel.tsx b/components/molecules/hero-panel.tsx index 5f15216..25d881b 100644 --- a/components/molecules/hero-panel.tsx +++ b/components/molecules/hero-panel.tsx @@ -44,7 +44,7 @@ export function HeroPanel({ icon: Icon, label, value, tone, badge, footer, child className="font-black tracking-tighter tabular-nums whitespace-nowrap text-[length:var(--hero-value-size)]" style={ { - "--hero-value-size": `clamp(1.5rem, ${VALUE_INLINE_SIZE_CQI / value.length}cqi, 6rem)`, + "--hero-value-size": `clamp(1.5rem, ${VALUE_INLINE_SIZE_CQI / Math.max(1, value.length)}cqi, 6rem)`, } as CSSProperties } >