refactor(ui): route the sleep-composition header through the tile header

The stage-composition chart card was the last insights tile with a
hand-rolled title row. It now uses the canonical header primitive; the
subtitle and the window toggles keep the shape they had, since the toggles
have to drop to their own row on a phone.

Also sweeps the last off-scale spacing: the two dense tiles inside a
recommendation move onto the sanctioned inner-tile step, and the not-found
button onto the scale. The banned twenty-pixel step is now absent from the
tree. The chart body is untouched.
This commit is contained in:
Marc-André Bombeck committed 2026-07-25 14:12:01 +02:00
1 parent 2decc37f07
commit 338808eb5f
4 files changed
+16 -8

No files matched your search

+1 -1
View File
@@ -49,7 +49,7 @@ export default async function NotFound() {
</div>
<Link
href="/"
className="bg-primary text-primary-foreground hover:bg-primary/90 inline-flex min-h-11 items-center justify-center rounded-md px-5 text-sm font-medium transition-colors"
className="bg-primary text-primary-foreground hover:bg-primary/90 inline-flex min-h-11 items-center justify-center rounded-md px-4 text-sm font-medium transition-colors"
>
{t("notFound.backToDashboard")}
</Link>
@@ -51,6 +51,9 @@ describe("<SleepStageStackedBar>", () => {
const html = render(<SleepStageStackedBar breakdown={breakdown} />);
expect(html).toContain("Stage composition");
expect(html).toContain("Last 23 nights");
// The title routes through the canonical tile header rather than a
// hand-rolled title row (UI-STANDARDS §1 / §8).
expect(html).toContain('data-slot="tile-header"');
});
it("exposes an accessible label that announces the nights covered", () => {
@@ -240,7 +240,7 @@ function RationaleCard({
return (
<div
data-slot="rec-rationale-card"
className="bg-muted/30 mt-3 space-y-3 rounded-md px-3 py-2.5"
className="bg-muted/30 mt-3 space-y-3 rounded-md p-3"
>
<div className="space-y-1.5">
<RationaleRow
@@ -358,7 +358,7 @@ export function RecommendationCard({
<div
data-slot="rec-card"
data-index={index}
className="border-border/40 bg-card/20 rounded-lg border px-3 py-2.5"
className="border-border/40 bg-card/20 rounded-lg border p-3"
>
<div className="flex items-start justify-between gap-2">
<div className="flex min-w-0 flex-1 items-start gap-2">
@@ -14,7 +14,8 @@ import {
} from "recharts";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { TileHeader } from "@/components/insights/tile-header";
import { useTranslations } from "@/lib/i18n/context";
import { formatDurationMinutes } from "@/lib/i18n/duration";
import { resolveIntlLocale } from "@/lib/format-locale";
@@ -205,10 +206,14 @@ export function SleepStageStackedBar({ breakdown }: SleepStageStackedBarProps) {
<Card data-slot="sleep-stage-stacked-bar">
<CardHeader>
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<div className="flex flex-col gap-0.5">
<CardTitle className="text-base font-semibold">
{t("insights.sleep.compositionTitle")}
</CardTitle>
<div className="flex min-w-0 flex-col gap-0.5">
{/* The canonical tile header — the title used to be a bare
`CardTitle` in a hand-rolled row. The window toggles stay a
sibling of the title+subtitle pair rather than riding the
header's `right` slot: the subtitle belongs under the title,
and the toggles need to drop to their own row below `sm`
(the sanctioned inline-action-row shape). */}
<TileHeader title={t("insights.sleep.compositionTitle")} />
<span className="text-muted-foreground text-xs">
{t("insights.sleep.compositionSubtitle", {
nights: breakdown.nights,