feat(charts): show 'still computing' caption on long chart loads

This commit is contained in:
Marc-André Bombeck committed 2026-05-21 22:32:02 +02:00
1 parent c0a2c8784e
commit b2e2f78ac2
8 files changed
+60

No files matched your search

+1
View File
@@ -793,6 +793,7 @@
},
"charts": {
"loadingLabel": "Diagramm wird geladen…",
"loadingSlowHint": "Auswertungen werden berechnet — das kann einen Moment dauern.",
"weight": "Gewicht",
"bloodPressure": "Blutdruck",
"pulse": "Puls",
+1
View File
@@ -793,6 +793,7 @@
},
"charts": {
"loadingLabel": "Loading chart…",
"loadingSlowHint": "Computing analytics — this can take a moment.",
"weight": "Weight",
"bloodPressure": "Blood Pressure",
"pulse": "Pulse",
+1
View File
@@ -793,6 +793,7 @@
},
"charts": {
"loadingLabel": "Cargando la gráfica…",
"loadingSlowHint": "Calculando los análisis — puede tardar un momento.",
"weight": "Peso",
"bloodPressure": "Tensión arterial",
"pulse": "Pulso",
+1
View File
@@ -793,6 +793,7 @@
},
"charts": {
"loadingLabel": "Chargement du graphique…",
"loadingSlowHint": "Calcul des analyses — cela peut prendre un instant.",
"weight": "Poids",
"bloodPressure": "Tension artérielle",
"pulse": "Pouls",
+1
View File
@@ -793,6 +793,7 @@
},
"charts": {
"loadingLabel": "Caricamento del grafico…",
"loadingSlowHint": "Calcolo delle analisi — può richiedere un momento.",
"weight": "Peso",
"bloodPressure": "Pressione arteriosa",
"pulse": "Pulsazioni",
+1
View File
@@ -793,6 +793,7 @@
},
"charts": {
"loadingLabel": "Ładowanie wykresu…",
"loadingSlowHint": "Obliczanie analiz — to może chwilę potrwać.",
"weight": "Waga",
"bloodPressure": "Ciśnienie tętnicze",
"pulse": "Tętno",
@@ -50,4 +50,21 @@ describe("<ChartSkeleton>", () => {
const html = render(<ChartSkeleton mini />);
expect(html).toMatch(/h-\[140px\]/);
});
// v1.4.43 QoL (L4) — slow-hint caption appears after a 3 s grace.
// SSR markup never paints the caption because `useEffect` does not
// run server-side; this also keeps hydration mismatch-free.
it("does NOT paint the slow-hint caption in the initial SSR markup", () => {
const html = render(<ChartSkeleton />);
expect(html).not.toContain('data-slot="chart-skeleton-slow-hint"');
expect(html).not.toContain("Computing analytics");
});
it("mini variant never sets up the slow-hint timer", () => {
// Read the component source: the `useEffect` early-returns when
// `mini` is true, so a `<ChartSkeleton mini />` row in trends
// never paints the multi-line caption that would overflow it.
const html = render(<ChartSkeleton mini />);
expect(html).not.toContain('data-slot="chart-skeleton-slow-hint"');
});
});
+37
View File
@@ -1,5 +1,7 @@
"use client";
import { useEffect, useState } from "react";
import { Skeleton } from "@/components/ui/skeleton";
import { cn } from "@/lib/utils";
import { useTranslations } from "@/lib/i18n/context";
@@ -31,10 +33,21 @@ import { useTranslations } from "@/lib/i18n/context";
* `rounded-md p-2` and pins the inner band at 140 px so the loading
* shell occupies the same visible box as the painted chart.
*
* v1.4.43 QoL (L4) — when the skeleton is still painted three seconds
* in, surface a small caption ("Computing analytics — this can take
* a moment") so the user understands the chart is being computed
* rather than broken. Today the `/api/analytics` correlations branch
* can sit on the skeleton for ~9 s; the caption sets expectation.
* Only fires on the full variant — the mini-skeleton sits in a
* dashboard row strip where a multi-line caption would overflow.
*
* `prefers-reduced-motion` is honoured automatically — the `<Skeleton>`
* primitive's `animate-pulse` is suppressed via Tailwind's
* `motion-reduce:animate-none` modifier.
*/
const SLOW_HINT_DELAY_MS = 3_000;
export function ChartSkeleton({
className,
mini = false,
@@ -48,6 +61,21 @@ export function ChartSkeleton({
mini?: boolean;
}) {
const { t } = useTranslations();
// v1.4.43 QoL (L4) — `showSlowHint` flips true 3 s into the load so
// the caption only appears for genuinely slow renders. Using a
// `useEffect` keeps the SSR markup identical (caption absent) so
// hydration never mismatches; the timer kicks in client-side once
// the skeleton actually paints.
const [showSlowHint, setShowSlowHint] = useState(false);
useEffect(() => {
if (mini) return undefined;
const handle = setTimeout(
() => setShowSlowHint(true),
SLOW_HINT_DELAY_MS,
);
return () => clearTimeout(handle);
}, [mini]);
if (mini) {
return (
<div
@@ -93,6 +121,15 @@ export function ChartSkeleton({
{/* Chart band — mirrors the same `h-[var(--chart-height,…)]`
contract `health-chart.tsx` paints at line 1087-1089. */}
<Skeleton className="h-[var(--chart-height,240px)] w-full md:h-[var(--chart-height-md,280px)]" />
{showSlowHint && (
<p
data-slot="chart-skeleton-slow-hint"
className="text-muted-foreground mt-3 text-xs"
>
{t("charts.loadingSlowHint")}
</p>
)}
</div>
);
}