mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 21:32:18 +00:00
fix(dashboard): equal-height tile contract on mobile
The dashboard tile strip enforces equal heights at `sm:` and above via `grid auto-rows-fr` but lets each tile take its content height at `<sm`. Sibling drift of 16-24 px appears whenever the comparison callout or the sub-row pair paints on one tile and not on its neighbour (the glucose tiles ship without `trend7Delta` / `compareDelta`, making this the visible case). Pin a 140-px contract at `<sm` and let it release back to `auto` from `sm:` upwards: - Strip wrapper carries `[--tile-h:140px] sm:[--tile-h:auto]`. - Each tile cell consumes `h-[var(--tile-h)] sm:h-auto`. - Inside `<TrendCard>`, the comparison-callout slot reserves `min-h-[18px]` even when empty and clamps to `line-clamp-1` at `<sm`. - Sub-row pair switches to `flex-nowrap overflow-hidden` at `<sm` (clipping over wrapping) and releases to `flex-wrap` from `sm:`. A new snapshot test pins the contract: callout slot reserves the minimum height, callout clamps to a single line, sub-row pair renders with `flex-nowrap` + `overflow-hidden`.
This commit is contained in:
1 parent
f463ca812c
commit
d381f23e59
3 files changed
+89
-7
No files matched your search
+9
-1
@@ -1211,6 +1211,14 @@ export default function DashboardPage() {
|
||||
// branch above `sm:`.
|
||||
className={cn(
|
||||
"flex snap-x snap-mandatory gap-3 overflow-x-auto pb-2",
|
||||
// v1.4.29 — fixed mobile tile height contract.
|
||||
// At `<sm` every tile pins to 140 px via the
|
||||
// `--tile-h` CSS custom property so divergent
|
||||
// content (callout on / off, sub-row wrap / no
|
||||
// wrap) cannot drift sibling heights. `sm:`
|
||||
// releases to `auto` where `grid auto-rows-fr`
|
||||
// already enforces equal heights.
|
||||
"[--tile-h:140px] sm:[--tile-h:auto]",
|
||||
"sm:grid sm:snap-none sm:auto-rows-fr sm:overflow-visible",
|
||||
"sm:[grid-template-columns:repeat(auto-fit,minmax(min(100%,9rem),1fr))]",
|
||||
)}
|
||||
@@ -1221,7 +1229,7 @@ export default function DashboardPage() {
|
||||
{trendCards.map((entry) => (
|
||||
<div
|
||||
key={entry.id}
|
||||
className="flex min-w-[10rem] shrink-0 snap-start sm:min-w-0 sm:shrink"
|
||||
className="flex h-[var(--tile-h)] min-w-[10rem] shrink-0 snap-start sm:h-auto sm:min-w-0 sm:shrink"
|
||||
>
|
||||
{entry.node}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { Activity } from "lucide-react";
|
||||
|
||||
import { TrendCard } from "../trend-card";
|
||||
import { I18nProvider } from "@/lib/i18n/context";
|
||||
|
||||
/**
|
||||
* v1.4.29 — mobile tile equal-height contract.
|
||||
*
|
||||
* On `<sm` viewports each dashboard tile pins to `--tile-h: 140px`
|
||||
* via the strip wrapper. The tile-internal callout slot reserves
|
||||
* `min-h-[18px]` even when no callout is rendered, and the sub-row
|
||||
* pair switches to `flex-nowrap overflow-hidden` so a narrow tile
|
||||
* cannot grow vertically.
|
||||
*
|
||||
* Per R-C §6 this contract pins regardless of which combination of
|
||||
* `compareDelta` / `trend7Delta` / sub-rows the tile is rendering.
|
||||
*/
|
||||
|
||||
const RISING = { slope: 0.5, direction: "up" as const, confidence: 0.8 };
|
||||
|
||||
function render(node: React.ReactNode) {
|
||||
return renderToStaticMarkup(
|
||||
<I18nProvider initialLocale="en">{node}</I18nProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
describe("<TrendCard> mobile tile equal-height contract", () => {
|
||||
const baseProps = {
|
||||
label: "Pulse",
|
||||
latest: 72,
|
||||
unit: "bpm",
|
||||
avg7: 70,
|
||||
avg30: 71,
|
||||
icon: Activity,
|
||||
slope30: RISING,
|
||||
};
|
||||
|
||||
it("renders the callout slot with min-h-[18px] reserved", () => {
|
||||
const html = render(<TrendCard {...baseProps} />);
|
||||
// The callout slot wrapper sits between value row + sub-rows and
|
||||
// reserves a fixed minimum height so sibling tiles don't drift
|
||||
// when the callout is absent.
|
||||
expect(html).toContain("min-h-[18px]");
|
||||
});
|
||||
|
||||
it("clamps the comparison callout to one line at <sm", () => {
|
||||
const html = render(
|
||||
<TrendCard
|
||||
{...baseProps}
|
||||
compareBaseline="lastMonth"
|
||||
compareDelta={1.2}
|
||||
/>,
|
||||
);
|
||||
expect(html).toContain("line-clamp-1");
|
||||
expect(html).toContain('data-slot="tile-compare-delta"');
|
||||
});
|
||||
|
||||
it("renders the sub-row pair with flex-nowrap + overflow-hidden", () => {
|
||||
const html = render(<TrendCard {...baseProps} />);
|
||||
expect(html).toContain("flex-nowrap");
|
||||
expect(html).toContain("overflow-hidden");
|
||||
});
|
||||
});
|
||||
@@ -265,11 +265,16 @@ export function TrendCard({
|
||||
flex-wrap on the sibling row + this callout's `mt-1`
|
||||
horizontal-row layout absorb the overflow without crowding
|
||||
the headline value above. */}
|
||||
{compareBaseline !== "none" && compareDelta != null && (
|
||||
<div className="mt-1">
|
||||
{/* v1.4.29 — clamp the callout to one line at `<sm` so the
|
||||
tile's overall height stays inside the 140-px contract.
|
||||
`min-h-[18px]` reserves the slot when no callout is being
|
||||
rendered so the sub-row pair below pins to the same y
|
||||
across every tile. `sm:` releases both bounds. */}
|
||||
<div className="mt-1 min-h-[18px] sm:min-h-0">
|
||||
{compareBaseline !== "none" && compareDelta != null && (
|
||||
<span
|
||||
className={cn(
|
||||
"inline-block max-w-full text-xs leading-snug font-medium [overflow-wrap:anywhere] tabular-nums",
|
||||
"line-clamp-1 inline-block max-w-full text-xs leading-snug font-medium tabular-nums sm:line-clamp-none sm:[overflow-wrap:anywhere]",
|
||||
comparisonDeltaColor,
|
||||
)}
|
||||
data-slot="tile-compare-delta"
|
||||
@@ -286,10 +291,14 @@ export function TrendCard({
|
||||
: "comparison.captionLastYear",
|
||||
)}`}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
<TooltipProvider>
|
||||
<div className="text-muted-foreground mt-auto flex min-w-0 flex-wrap gap-x-3 gap-y-1 pt-1 text-xs leading-snug">
|
||||
{/* v1.4.29 — sub-row pair clips rather than wraps at `<sm`
|
||||
so a narrow tile cannot grow vertically beyond the
|
||||
140-px contract. `sm:` releases back to the original
|
||||
wrap behaviour. */}
|
||||
<div className="text-muted-foreground mt-auto flex min-w-0 flex-nowrap items-baseline gap-x-3 overflow-hidden pt-1 text-xs leading-snug sm:flex-wrap sm:gap-y-1">
|
||||
<span className="max-w-full min-w-0 [overflow-wrap:anywhere]">
|
||||
{t(avg7LabelKey)}:{" "}
|
||||
{avg7Hint ? (
|
||||
|
||||
Reference in new issue
Block a user