From 3594b549d99f92bde5d11ec0e9aa078eec621b66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sun, 14 Jun 2026 23:02:37 +0200 Subject: [PATCH] feat(sleep): place sleep-debt and rhythm cards side by side when both present MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When both the sleep-debt and chronotype cards carry a settled readout, render them two-up on large screens instead of stacked; they are compact and read better paired. A lone data-bearing card — or either card in its learning / partial state — keeps full width so it never leaves a half-width orphan. The grid collapses to one column below the lg breakpoint and uses items-start so a toggled-open chronotype card does not stretch its neighbour. --- .../__tests__/sleep-rhythm-section.test.tsx | 35 +++++++++++++++++++ .../insights/sleep/sleep-rhythm-section.tsx | 17 ++++++++- 2 files changed, 51 insertions(+), 1 deletion(-) 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 696d6b981..639992829 100644 --- a/src/components/insights/sleep/__tests__/sleep-rhythm-section.test.tsx +++ b/src/components/insights/sleep/__tests__/sleep-rhythm-section.test.tsx @@ -102,6 +102,41 @@ describe("", () => { expect(html).toContain("md:py-6"); }); + it("places the two cards side by side when both carry settled data", () => { + rhythmMock.mockReturnValue({ + data: READY_DTO, + isLoading: false, + isError: false, + }); + const html = render(); + // Two-up on large screens, single column below. + expect(html).toContain("lg:grid-cols-2"); + }); + + it("keeps a lone data-bearing card full width when the other is still learning", () => { + rhythmMock.mockReturnValue({ + data: { + ...READY_DTO, + // Sleep-debt settled, chronotype still learning → no two-up grid. + chronotype: { + state: "learning", + msfMinutes: null, + msfScMinutes: null, + band: null, + socialJetlagMinutes: null, + freeNightsCounted: 2, + workNightsCounted: 5, + freeNightsUntilReady: 4, + }, + } satisfies SleepRhythmDto, + isLoading: false, + isError: false, + }); + const html = render(); + expect(html).toContain("grid-cols-1"); + expect(html).not.toContain("lg:grid-cols-2"); + }); + it("renders nothing when disabled", () => { rhythmMock.mockReturnValue({ data: undefined, diff --git a/src/components/insights/sleep/sleep-rhythm-section.tsx b/src/components/insights/sleep/sleep-rhythm-section.tsx index 68c578377..a88466965 100644 --- a/src/components/insights/sleep/sleep-rhythm-section.tsx +++ b/src/components/insights/sleep/sleep-rhythm-section.tsx @@ -2,6 +2,7 @@ import { useAuth } from "@/hooks/use-auth"; import { Skeleton } from "@/components/ui/skeleton"; +import { cn } from "@/lib/utils"; import { useTranslations } from "@/lib/i18n/context"; import { useSleepRhythm } from "./use-sleep-rhythm"; import { SleepDebtCard } from "./sleep-debt-card"; @@ -52,8 +53,22 @@ export function SleepRhythmSection({ enabled }: { enabled: boolean }) { ); } + // Both cards are compact; when each carries a settled (non-learning) readout + // they sit two-up. A lone card with data spans full width so it never leaves + // a half-width orphan with dead space beside it (mirrors the mood Einordnung + // tiles). The cards' own learning/partial states stay stacked full-width. + const debtHasData = data.sleepDebt.state === "ready"; + const chronotypeHasData = + data.chronotype.state === "ready" && data.chronotype.band != null; + const bothHaveData = debtHasData && chronotypeHasData; + return ( -
+