mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
feat(charts): mini ChartSkeleton variant for trends-row slot
The trends-row chart slot is now pinned to 140 px, but the default
`<ChartSkeleton>` paints a `rounded-xl` card with `p-4 md:p-6` and a
240 px chart band — overflowing the slot and reintroducing the
layout shift the height pin was meant to eliminate.
Add a `mini` prop to `<ChartSkeleton>` that renders the lighter
`rounded-md p-2` chrome plus a 140 px band. Expose
`HealthChartDynamicMini` from the chart-dynamic boundary so the
trends-row consumers pick up the matching loading shell without
having to wire the skeleton choice at every call site.
Trends-row swaps both `<HealthChartDynamic>` calls (BP + weight) for
the mini variant and the local `<MoodChart>` `next/dynamic` loader
now passes `mini` to its skeleton. The dashboard hero + sub-page
charts keep the default card chrome (`mini={false}`).
This commit is contained in:
1 parent
4d111e39dd
commit
b4bef03fd7
4 files changed
+110
-5
No files matched your search
@@ -0,0 +1,53 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
|
||||
import { I18nProvider } from "@/lib/i18n/context";
|
||||
import { ChartSkeleton } from "../chart-skeleton";
|
||||
|
||||
/**
|
||||
* v1.4.36 W2 — `<ChartSkeleton>` carries two variants so the loading
|
||||
* shell occupies the same visible footprint as the chart that lands
|
||||
* after `next/dynamic` resolves.
|
||||
*
|
||||
* - Default (dashboard / sub-page hero): rounded-xl card chrome,
|
||||
* full header row, `--chart-height` band.
|
||||
* - `mini` (trends-row 140 px slot): rounded-md, light padding,
|
||||
* 140 px band — no header chrome.
|
||||
*/
|
||||
function render(node: React.ReactNode) {
|
||||
return renderToStaticMarkup(
|
||||
<I18nProvider initialLocale="en">{node}</I18nProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
describe("<ChartSkeleton>", () => {
|
||||
it("renders the default card chrome (rounded-xl + p-4 md:p-6)", () => {
|
||||
const html = render(<ChartSkeleton />);
|
||||
expect(html).toContain('data-slot="chart-skeleton"');
|
||||
expect(html).not.toContain('data-mini="true"');
|
||||
expect(html).toMatch(/rounded-xl/);
|
||||
expect(html).toMatch(/p-4/);
|
||||
expect(html).toMatch(/md:p-6/);
|
||||
});
|
||||
|
||||
it("renders the loading announcement for assistive tech", () => {
|
||||
const html = render(<ChartSkeleton />);
|
||||
expect(html).toMatch(/role="status"/);
|
||||
expect(html).toMatch(/aria-busy="true"/);
|
||||
});
|
||||
|
||||
it("mini variant uses the lighter rounded-md + p-2 chrome", () => {
|
||||
const html = render(<ChartSkeleton mini />);
|
||||
expect(html).toContain('data-slot="chart-skeleton"');
|
||||
expect(html).toContain('data-mini="true"');
|
||||
expect(html).toMatch(/rounded-md/);
|
||||
expect(html).toMatch(/p-2/);
|
||||
// Default card-chrome classes must NOT appear in the mini variant.
|
||||
expect(html).not.toMatch(/rounded-xl/);
|
||||
});
|
||||
|
||||
it("mini variant pins the skeleton band at h-[140px] (trends-row slot)", () => {
|
||||
const html = render(<ChartSkeleton mini />);
|
||||
expect(html).toMatch(/h-\[140px\]/);
|
||||
});
|
||||
});
|
||||
@@ -23,12 +23,46 @@ import { useTranslations } from "@/lib/i18n/context";
|
||||
* The cumulative box matches the loaded chart's height to within a
|
||||
* pixel so the page does not jump on hydration.
|
||||
*
|
||||
* v1.4.36 W2 — `mini` variant for the trends-row 140 px chart slot.
|
||||
* The default skeleton's rounded-xl border + p-4/p-6 padding inflated
|
||||
* past the 140 px wrapper and pushed the row taller than the loaded
|
||||
* chart, re-introducing the very layout shift the wrapper now
|
||||
* eliminates. The mini variant trades the card chrome for
|
||||
* `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.
|
||||
*
|
||||
* `prefers-reduced-motion` is honoured automatically — the `<Skeleton>`
|
||||
* primitive's `animate-pulse` is suppressed via Tailwind's
|
||||
* `motion-reduce:animate-none` modifier.
|
||||
*/
|
||||
export function ChartSkeleton({ className }: { className?: string }) {
|
||||
export function ChartSkeleton({
|
||||
className,
|
||||
mini = false,
|
||||
}: {
|
||||
className?: string;
|
||||
/**
|
||||
* When true, render the compact variant used inside the trends-row
|
||||
* chart slot (140 px band, lighter chrome). Defaults to false for
|
||||
* the dashboard hero / sub-page charts.
|
||||
*/
|
||||
mini?: boolean;
|
||||
}) {
|
||||
const { t } = useTranslations();
|
||||
if (mini) {
|
||||
return (
|
||||
<div
|
||||
data-slot="chart-skeleton"
|
||||
data-mini="true"
|
||||
role="status"
|
||||
aria-busy="true"
|
||||
aria-live="polite"
|
||||
className={cn("bg-card border-border rounded-md border p-2", className)}
|
||||
>
|
||||
<span className="sr-only">{t("charts.loadingLabel")}</span>
|
||||
<Skeleton className="h-[140px] w-full" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div
|
||||
data-slot="chart-skeleton"
|
||||
|
||||
@@ -27,3 +27,21 @@ export const HealthChartDynamic = dynamic(
|
||||
})),
|
||||
{ ssr: false, loading: () => <ChartSkeleton /> },
|
||||
);
|
||||
|
||||
/**
|
||||
* v1.4.36 W2 — mini variant for the trends-row 140 px chart slot. Same
|
||||
* underlying `<HealthChart>` import, but the loading shell renders the
|
||||
* `mini` `<ChartSkeleton>` so the loading box matches the painted
|
||||
* chart's footprint instead of inflating past the row contract.
|
||||
*
|
||||
* Consumers inside a fixed-height row (BP + weight cards in
|
||||
* `trends-row.tsx`) pass `mini` on the chart itself; this wrapper
|
||||
* ensures the next/dynamic loader paints the matching skeleton.
|
||||
*/
|
||||
export const HealthChartDynamicMini = dynamic(
|
||||
() =>
|
||||
import("@/components/charts/health-chart").then((mod) => ({
|
||||
default: mod.HealthChart,
|
||||
})),
|
||||
{ ssr: false, loading: () => <ChartSkeleton mini /> },
|
||||
);
|
||||
@@ -4,7 +4,7 @@ import dynamic from "next/dynamic";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { ChartSkeleton } from "@/components/charts/chart-skeleton";
|
||||
import { HealthChartDynamic } from "@/components/charts/health-chart-dynamic";
|
||||
import { HealthChartDynamicMini } from "@/components/charts/health-chart-dynamic";
|
||||
import {
|
||||
TrendAnnotation,
|
||||
type TrendAnnotationConfidenceBand,
|
||||
@@ -32,7 +32,7 @@ const MoodChart = dynamic(
|
||||
import("@/components/charts/mood-chart").then((mod) => ({
|
||||
default: mod.MoodChart,
|
||||
})),
|
||||
{ ssr: false, loading: () => <ChartSkeleton /> },
|
||||
{ ssr: false, loading: () => <ChartSkeleton mini /> },
|
||||
);
|
||||
|
||||
interface TrendsRowProps {
|
||||
@@ -118,7 +118,7 @@ export function TrendsRow({ annotations, confidence }: TrendsRowProps) {
|
||||
shadcn Card) lines up with the BP/weight tiles' lighter
|
||||
shell. Both chart types ship the same data-slot now. */}
|
||||
<div data-slot="trends-row-chart-slot" className="h-[140px] shrink-0">
|
||||
<HealthChartDynamic
|
||||
<HealthChartDynamicMini
|
||||
types={["BLOOD_PRESSURE_SYS", "BLOOD_PRESSURE_DIA"]}
|
||||
title={t("charts.bloodPressure")}
|
||||
colors={["#ff79c6", "#8be9fd"]}
|
||||
@@ -140,7 +140,7 @@ export function TrendsRow({ annotations, confidence }: TrendsRowProps) {
|
||||
className="flex h-full flex-col gap-2 md:min-h-[300px]"
|
||||
>
|
||||
<div data-slot="trends-row-chart-slot" className="h-[140px] shrink-0">
|
||||
<HealthChartDynamic
|
||||
<HealthChartDynamicMini
|
||||
types={["WEIGHT"]}
|
||||
title={t("charts.weight")}
|
||||
colors={["#bd93f9"]}
|
||||
|
||||
Reference in new issue
Block a user