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.
This commit is contained in:
Marc-André Bombeck committed 2026-06-21 12:31:26 +02:00
1 parent 6d9002bb71
commit 26230542ce
5 files changed
+71 -68

No files matched your search

@@ -80,7 +80,7 @@ describe("<ChronotypeSection>", () => {
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("<ChronotypeSection>", () => {
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", () => {
@@ -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("<SleepDebtCard>", () => {
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(<SleepDebtCard debt={READY_DEBT} />);
// 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("<ChronotypeCard>", () => {
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(<ChronotypeCard chronotype={READY_CHRONO} />);
// 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");
@@ -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("<SleepRhythmSection>", () => {
isError: false,
});
const html = render(<SleepRhythmSection enabled />);
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"');
});
@@ -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 `<SleepDebtCard>` 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 }) {
</div>
</CardHeader>
<CardContent className="space-y-4">
{/* 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. */}
<div className="space-y-1" data-slot="chronotype-band">
<p className="text-foreground text-2xl font-semibold tracking-tight sm:text-3xl">
{bandLabel}
</p>
{/* 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. */}
<div className="space-y-0.5" data-slot="chronotype-band">
<p className="text-2xl font-semibold">{bandLabel}</p>
{msfClock && (
<p className="text-muted-foreground text-sm">
<p className="text-muted-foreground text-xs">
{t("insights.sleep.chronotype.midpointHeadline", {
clock: msfClock,
})}
@@ -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 `<SleepRhythmSection>` (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" `<SectionHeading>` was removed: it
* duplicated the heading the `<ChronotypeCard>` 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 `<SleepDebtCard>` 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 (
<section data-slot="chronotype-section" className="space-y-3">
<SectionHeading
icon={Clock}
title={t("insights.sleep.chronotype.title")}
if (isError) {
return (
<div
data-slot="chronotype-error"
role="status"
className="bg-card border-border text-muted-foreground rounded-xl border p-4 text-sm"
>
{t("insights.sleep.rhythm.loadError")}
</div>
);
}
if (isLoading || !data) {
return (
<Skeleton
data-slot="chronotype-loading"
className="h-32 w-full rounded-xl"
/>
{isError ? (
<div
data-slot="chronotype-error"
role="status"
className="bg-card border-border text-muted-foreground rounded-xl border p-4 text-sm"
>
{t("insights.sleep.rhythm.loadError")}
</div>
) : isLoading || !data ? (
<Skeleton
data-slot="chronotype-loading"
className="h-32 w-full rounded-xl"
/>
) : (
<ChronotypeCard chronotype={data.chronotype} />
)}
</section>
);
);
}
return <ChronotypeCard chronotype={data.chronotype} />;
}