Merge branch 'wave/v1221-e' into release/v1.22.1

This commit is contained in:
Marc-André Bombeck committed 2026-06-27 18:06:56 +02:00
commit 11fd081f8c
7 files changed
+37 -63

No files matched your search

@@ -106,7 +106,7 @@ describe("<TrendsRow>", () => {
expect(slots.length).toBe(3);
});
it("pins every chart slot to h-[180px] so Recharts mounts with a known size (CLS fix)", () => {
it("pins every chart slot to a known responsive height so Recharts mounts with a size (CLS fix)", () => {
// v1.4.36 W2 — without an explicit height the Recharts
// ResponsiveContainer mounts with width=-1 height=-1 and emits a
// console warning per chart. The mood card also drifted taller
@@ -119,12 +119,17 @@ describe("<TrendsRow>", () => {
// TrendAnnotation row below — visible as text sitting on top of
// the chart. 180 px absorbs the full envelope so the three tiles
// share one chart-band baseline without overlap.
//
// v1.22.0 — the slot height became `clamp(140px,32vh,180px)` so a
// rotated phone doesn't lose half the screen to the fixed band. The
// 180 px envelope still holds on desktop + portrait (where 32vh
// clears 180 px); only short landscape viewports shrink to the floor.
const html = render(<TrendsRow />);
const slots =
html.match(/data-slot="trends-row-chart-slot"[^>]*class="[^"]*"/g) ?? [];
expect(slots.length).toBe(3);
for (const slot of slots) {
expect(slot).toMatch(/h-\[180px\]/);
expect(slot).toMatch(/h-\[clamp\(140px,32vh,180px\)\]/);
}
});
@@ -56,7 +56,7 @@ export function MoodDistributionChart({
// v1.15.14 — bounded compact height (was `aspect-[3/2] min-h-[180px]`,
// which ballooned the card on a wide viewport). A fixed band keeps this a
// tidy card rather than a dominant block, and matches the weekday sibling.
<div className="h-[150px] w-full">
<div className="h-[clamp(120px,26vh,150px)] w-full">
<ResponsiveContainer width="100%" height="100%">
<BarChart
data={data}
@@ -46,7 +46,9 @@ const MoodDistributionChart = dynamic(
})),
{
ssr: false,
loading: () => <Skeleton className="h-[150px] w-full rounded-md" />,
loading: () => (
<Skeleton className="h-[clamp(120px,26vh,150px)] w-full rounded-md" />
),
},
);
const MoodWeekdayChart = dynamic(
@@ -56,7 +58,9 @@ const MoodWeekdayChart = dynamic(
})),
{
ssr: false,
loading: () => <Skeleton className="h-[150px] w-full rounded-md" />,
loading: () => (
<Skeleton className="h-[clamp(120px,26vh,150px)] w-full rounded-md" />
),
},
);
const MoodTimeOfDayChart = dynamic(
@@ -66,7 +70,9 @@ const MoodTimeOfDayChart = dynamic(
})),
{
ssr: false,
loading: () => <Skeleton className="h-[220px] w-full rounded-md" />,
loading: () => (
<Skeleton className="h-[clamp(160px,38vh,220px)] w-full rounded-md" />
),
},
);
import { MoodTagBreakdown, type MoodTagRow } from "./mood-tag-breakdown";
@@ -78,7 +78,7 @@ export function MoodTimeOfDayChart({
width-constrained), so an aspect ratio derived the height off the full
card width and ballooned the chart to ~800px on a wide viewport. A
bounded height keeps it the same size as the sibling charts. */}
<div className="h-[220px] w-full">
<div className="h-[clamp(160px,38vh,220px)] w-full">
<ResponsiveContainer width="100%" height="100%">
<BarChart
data={data}
@@ -77,7 +77,7 @@ export function MoodWeekdayChart({ weekday }: { weekday: MoodWeekdayRow[] }) {
which derived its height off the full card width and ballooned on a
wide viewport). A fixed band keeps it a tidy card matching the
distribution sibling. */}
<div className="h-[150px] w-full">
<div className="h-[clamp(120px,26vh,150px)] w-full">
<ResponsiveContainer width="100%" height="100%">
<BarChart
data={data}
+10 -54
View File
@@ -9,7 +9,7 @@ import { useTranslations } from "@/lib/i18n/context";
import { useInsightsLayoutPrefs } from "@/hooks/use-insights-layout-prefs";
import { useAnalyticsQuery } from "@/lib/queries/use-analytics-query";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Card, CardContent } from "@/components/ui/card";
import { EmptyState } from "@/components/ui/empty-state";
import dynamic from "next/dynamic";
import { ChartErrorBoundary } from "@/components/charts/chart-error-state";
@@ -45,13 +45,16 @@ function SleepStageStackedBar(
*
* Pulls the analytics `sleepStages` aggregate (added in v1.4.23 W2)
* plus the SLEEP_DURATION trend series via `<SleepDurationChart>` and
* renders three blocks:
* renders two blocks:
*
* 1. Headline card with the average nightly total (h / min) and the
* number of nights covered.
* 2. Stage composition stacked bar (REM / Deep / Core / Awake / In
* 1. Stage composition stacked bar (REM / Deep / Core / Awake / In
* bed / Asleep) — the canonical sleep-phase distribution.
* 3. Duration trend chart with chart-cog parity (`chartKey="sleep"`).
* 2. Duration trend chart with chart-cog parity (`chartKey="sleep"`).
*
* v1.22.0 — the leading "average per night" headline card was dropped:
* the same figure + label already renders in `<AverageSleepCard>` inside
* the shared sleep-rhythm grid row directly below, so the top card only
* duplicated it.
*
* v1.18.7 W-D — the single-night "Last night" hypnogram card was removed:
* its per-stage breakdown (time + %) duplicated the phase distribution the
@@ -80,21 +83,9 @@ interface SleepAnalyticsResponse {
sleepStages: SleepStageBreakdown | null;
}
function formatHoursMinutes(
totalMinutes: number,
locale: string,
): { primary: string } {
const hours = Math.floor(totalMinutes / 60);
const mins = Math.round(totalMinutes - hours * 60);
if (locale === "de") {
return { primary: `${hours} Std. ${mins} Min.` };
}
return { primary: `${hours}h ${mins}m` };
}
export function SleepOverview() {
const { isAuthenticated, user } = useAuth();
const { t, locale } = useTranslations();
const { t } = useTranslations();
const { compareBaseline } = useInsightsLayoutPrefs(isAuthenticated);
// v1.4.33 IW2 — sleep overview reads `sleepStages` (a thick-only
@@ -127,43 +118,8 @@ export function SleepOverview() {
);
}
const averageMinutes = sleepSummary?.avg30 ?? sleepSummary?.avg7 ?? null;
const suffix = t("insights.sleep.headlineCaptionSuffix");
const { primary } =
averageMinutes != null
? formatHoursMinutes(averageMinutes, locale)
: { primary: "—" };
return (
<div className="space-y-4">
{/* v1.12.0 — headline card tightened: the default Card
`gap-4 md:gap-6` + `py-4 md:py-6` left a tall empty band around a
two-line readout. `gap-2 py-4 md:py-4` pulls the caption up under the
number without crowding it. */}
<Card className="gap-2 py-4 md:gap-2 md:py-4">
<CardHeader className="pb-0">
<div className="flex items-center gap-2">
<Moon className="text-dracula-cyan h-4 w-4" />
<CardTitle className="text-base font-semibold">
{t("insights.sleep.headlineTitle")}
</CardTitle>
</div>
</CardHeader>
<CardContent>
<div className="space-y-0.5">
<p className="text-2xl font-semibold">{primary}</p>
<p className="text-muted-foreground text-xs">
{sleepSummary?.count
? t("insights.sleep.headlineCaption", {
count: sleepSummary.count,
suffix,
})
: suffix}
</p>
</div>
</CardContent>
</Card>
{sleepStages ? (
<SleepStageStackedBar breakdown={sleepStages} />
) : (
+8 -1
View File
@@ -277,7 +277,14 @@ export function TrendsRow({
lives entirely on this slot. */}
<div
data-slot="trends-row-chart-slot"
className="h-[180px] shrink-0 overflow-hidden [--chart-height:120px]"
// v1.22.0 — the slot height is responsive to the viewport so a
// rotated phone doesn't lose half the screen to a fixed 180 px
// band. `clamp(140px,32vh,180px)` pins the same 180 px envelope
// on desktop + portrait (where 32vh clears 180 px) and only
// shrinks toward the 140 px floor on short landscape viewports.
// `overflow-hidden` already clips any stray axis label, so the
// tighter band stays tidy.
className="h-[clamp(140px,32vh,180px)] shrink-0 overflow-hidden [--chart-height:120px]"
>
<MetricChart config={config} title={title} />
</div>