From 26230542ce00e8b2bd42d44d86440a986a461993 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sun, 21 Jun 2026 12:31:26 +0200 Subject: [PATCH] fix(sleep): make the chronotype tile uniform with the sleep-debt tile The chronotype card carried its own section heading on top of the title it already renders, so 'Chronotype' appeared twice and the tile did not match its sibling. Drop the standalone heading and bring the card to the same shape as the sleep-debt tile: identical chrome and a bold primary value with one secondary line. The chronotype band is the primary value at the same scale the debt tile uses for its hours figure, with the natural sleep- midpoint clock as the companion line. --- .../__tests__/chronotype-section.test.tsx | 8 ++- .../__tests__/sleep-rhythm-cards.test.tsx | 25 +++---- .../__tests__/sleep-rhythm-section.test.tsx | 10 +-- .../insights/sleep/chronotype-card.tsx | 30 +++++---- .../insights/sleep/chronotype-section.tsx | 66 +++++++++---------- 5 files changed, 71 insertions(+), 68 deletions(-) diff --git a/src/components/insights/sleep/__tests__/chronotype-section.test.tsx b/src/components/insights/sleep/__tests__/chronotype-section.test.tsx index 7d09a89d0..8d26be570 100644 --- a/src/components/insights/sleep/__tests__/chronotype-section.test.tsx +++ b/src/components/insights/sleep/__tests__/chronotype-section.test.tsx @@ -80,7 +80,7 @@ describe("", () => { expect(html).not.toContain('data-slot="chronotype-error"'); }); - it("renders the large band treatment + midpoint clock on a settled DTO", () => { + it("renders the band value + midpoint clock on a settled DTO", () => { rhythmMock.mockReturnValue({ data: READY_DTO, isLoading: false, @@ -90,8 +90,10 @@ describe("", () => { expect(html).toContain('data-slot="chronotype-band"'); expect(html).toContain("Evening type"); expect(html).toContain("05:41"); - // The section heading carries the chronotype title. - expect(html).toContain('data-slot="section-heading"'); + // v1.19.0 — the duplicate standalone section heading is gone; the single + // chronotype title now lives inside the card, uniform with the debt tile. + expect(html).not.toContain('data-slot="section-heading"'); + expect(html).toContain('data-slot="chronotype-card"'); }); it("renders nothing when disabled", () => { diff --git a/src/components/insights/sleep/__tests__/sleep-rhythm-cards.test.tsx b/src/components/insights/sleep/__tests__/sleep-rhythm-cards.test.tsx index c2c0bc53f..6dfe7a9ca 100644 --- a/src/components/insights/sleep/__tests__/sleep-rhythm-cards.test.tsx +++ b/src/components/insights/sleep/__tests__/sleep-rhythm-cards.test.tsx @@ -21,16 +21,16 @@ const PARTIAL_DEBT: SleepDebtDto = { debtMinutes: 0, needMinutes: 420, nightsCounted: 3, - windowNights: 14, - nightsUntilReady: 4, + windowNights: 5, + nightsUntilReady: 1, }; const READY_DEBT: SleepDebtDto = { state: "ready", - debtMinutes: 600, + debtMinutes: 60, needMinutes: 420, - nightsCounted: 10, - windowNights: 14, + nightsCounted: 5, + windowNights: 5, nightsUntilReady: 0, }; @@ -65,11 +65,11 @@ describe("", () => { expect(html).not.toContain("short"); }); - it("asserts the cumulative debt in the ready state", () => { + it("asserts the running debt balance in the ready state", () => { const html = render(); - // 600 min = 10h 0m short. - expect(html).toContain("10h 0m short"); - expect(html).toContain("14 nights"); + // 60 min = 1h 0m short — the small, recovering balance a normal sleeper sees. + expect(html).toContain("1h 0m short"); + expect(html).toContain("5 nights"); }); it("inherits the standard Card rhythm (no compact override)", () => { @@ -110,10 +110,11 @@ describe("", () => { expect(html).toContain("04:30"); }); - it("v1.18.7 W-D — renders the band as the large hero label, not a corner readout", () => { + it("v1.19.0 — renders the band as the bold primary value, uniform with the debt tile", () => { const html = render(); - // The reworked bottom treatment leads with a large band label + the - // natural-midpoint headline. The retired top-right corner readout is gone. + // The band is the primary value at the SAME text-2xl scale the debt tile + // uses for its hours figure; the natural-midpoint clock is the secondary + // line. The retired top-right corner readout is gone. expect(html).not.toContain('data-slot="chronotype-corner"'); expect(html).toContain('data-slot="chronotype-band"'); expect(html).toContain("Intermediate type"); diff --git a/src/components/insights/sleep/__tests__/sleep-rhythm-section.test.tsx b/src/components/insights/sleep/__tests__/sleep-rhythm-section.test.tsx index 2d254ed3e..d4af80009 100644 --- a/src/components/insights/sleep/__tests__/sleep-rhythm-section.test.tsx +++ b/src/components/insights/sleep/__tests__/sleep-rhythm-section.test.tsx @@ -42,10 +42,10 @@ function render(node: React.ReactNode, locale: "en" | "de" = "en") { const READY_DTO: SleepRhythmDto = { sleepDebt: { state: "ready", - debtMinutes: 600, + debtMinutes: 60, needMinutes: 420, - nightsCounted: 10, - windowNights: 14, + nightsCounted: 5, + windowNights: 5, nightsUntilReady: 0, }, chronotype: { @@ -97,8 +97,8 @@ describe("", () => { isError: false, }); const html = render(); - expect(html).toContain("10h 0m short"); - // The chronotype band lives in the separate bottom section now, never here. + expect(html).toContain("1h 0m short"); + // The chronotype band lives in the separate sibling tile, never here. expect(html).not.toContain("Intermediate type"); expect(html).not.toContain('data-slot="chronotype-band"'); }); diff --git a/src/components/insights/sleep/chronotype-card.tsx b/src/components/insights/sleep/chronotype-card.tsx index 4918e9813..03bb455e6 100644 --- a/src/components/insights/sleep/chronotype-card.tsx +++ b/src/components/insights/sleep/chronotype-card.tsx @@ -28,12 +28,16 @@ function hoursMinutes(totalMinutes: number): { /** * v1.17.0 — chronotype card (MCTQ MSF / MSFsc band + social jetlag). * - * v1.18.7 W-D — reworked from a small corner-label card into the PROMINENT - * bottom treatment of the Sleep view: a LARGE band label is the hero, with the - * natural sleep-midpoint clock beneath it, and an "advanced" disclosure that - * reveals social jetlag + the sleep-debt-corrected MSFsc. While `learning` it - * keeps the calm "still learning your rhythm — N of M nights" copy and asserts - * NO band. Warm, grounded copy; mobile-first. + * v1.19.0 — brought to the SAME tile shape as `` so the two sit + * uniform in the shared grid row: identical card chrome, an icon + `text-base` + * `CardTitle` header, then a `space-y-0.5` value block with a bold primary value + * (`text-2xl font-semibold`) and one secondary `text-xs muted` line. The primary + * value is the chronotype BAND (a label, the slot the debt tile fills with an + * hours figure); the concrete companion — the natural sleep midpoint clock — is + * the secondary line, so both tiles read "bold value + one grounded line" at the + * same scale. The "advanced" disclosure (social jetlag + sleep-debt-corrected + * MSFsc) stays. While `learning` it keeps the calm "still learning your rhythm — + * N of M nights" copy and asserts NO band. Warm, grounded copy; mobile-first. */ export function ChronotypeCard({ chronotype }: { chronotype: ChronotypeDto }) { const { t } = useTranslations(); @@ -78,15 +82,13 @@ export function ChronotypeCard({ chronotype }: { chronotype: ChronotypeDto }) { - {/* The band is the hero of the bottom treatment — a large label the - eye lands on first, with the natural sleep-midpoint clock as the - grounding caption beneath it. */} -
-

- {bandLabel} -

+ {/* Uniform with the sleep-debt tile: a bold primary value (the band) at + the same scale, with the concrete companion (natural sleep midpoint + clock) as the secondary muted line. */} +
+

{bandLabel}

{msfClock && ( -

+

{t("insights.sleep.chronotype.midpointHeadline", { clock: msfClock, })} diff --git a/src/components/insights/sleep/chronotype-section.tsx b/src/components/insights/sleep/chronotype-section.tsx index 39f577634..38d27879d 100644 --- a/src/components/insights/sleep/chronotype-section.tsx +++ b/src/components/insights/sleep/chronotype-section.tsx @@ -1,28 +1,28 @@ "use client"; -import { Clock } from "lucide-react"; - import { useAuth } from "@/hooks/use-auth"; import { Skeleton } from "@/components/ui/skeleton"; import { useTranslations } from "@/lib/i18n/context"; -import { SectionHeading } from "@/components/insights/section-heading"; import { useSleepRhythm } from "./use-sleep-rhythm"; import { ChronotypeCard } from "./chronotype-card"; /** - * v1.18.7 W-D — chronotype as the prominent bottom block of the Sleep view. + * v1.18.7 W-D — chronotype card on the Sleep view. * - * Split out of `` (which now owns only the sleep-debt - * headline) so the chronotype gets a full-width treatment at the end of the - * page: a large band label, the natural sleep-midpoint clock beneath it, and an - * expandable disclosure for social jetlag + MSFsc. Reads the SAME - * server-authoritative DTO via `useSleepRhythm`, so it shares the + * v1.19.0 — the standalone "Chronotyp" `` was removed: it + * duplicated the heading the `` already carries in its own + * `CardTitle`, so "Chronotype" appeared twice and the tile no longer matched + * its sibling (the sleep-debt tile, which has no section heading). The card now + * stands as a peer of `` inside the shared grid row — same card + * chrome, one heading each, rendered inside the tile. + * + * Reads the SAME server-authoritative DTO via `useSleepRhythm`, so it shares the * `["sleep-rhythm"]` cache the debt section already warmed — no extra * round-trip. Gated on `enabled` so a source-less account never fetches. * * States mirror the debt section: loading → skeleton, error → quiet inline - * notice, settled → the card (which carries its own calm "still learning" - * copy for the no-nights-yet case, since the route always returns a full DTO). + * notice, settled → the card (which carries its own calm "still learning" copy + * for the no-nights-yet case, since the route always returns a full DTO). */ export function ChronotypeSection({ enabled }: { enabled: boolean }) { const { isAuthenticated } = useAuth(); @@ -33,28 +33,26 @@ export function ChronotypeSection({ enabled }: { enabled: boolean }) { if (!enabled) return null; - return ( -

- + {t("insights.sleep.rhythm.loadError")} +
+ ); + } + + if (isLoading || !data) { + return ( + - {isError ? ( -
- {t("insights.sleep.rhythm.loadError")} -
- ) : isLoading || !data ? ( - - ) : ( - - )} - - ); + ); + } + + return ; }