mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 04:50:59 +00:00
Merge branch 'wave/v1221-e' into release/v1.22.1
This commit is contained in:
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}
|
||||
|
||||
@@ -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} />
|
||||
) : (
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user