mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 21:10:59 +00:00
chore(insights): polish VO2 strip, status show-more, and trend-card secondary row
v1.4.27 MB7 — three related insights surface polish items in one commit: - CF-38: the VO2 stat strip extends the 2-column phone grid all the way to `<lg` so tablets keep the 2×2 layout, and every chip floors at `min-h-[64px]` so the row paints at a consistent height even when one stat has no trend arrow. - CF-39: the insight-status-card body collapses to `line-clamp-3` with a "Show more" toggle when the assessment text is long enough to actually clip. Short bodies render plain so the user never sees a useless toggle. - CF-41 + CF-67: the trend-card mobile-secondary row retires and the all-time aggregate renders inline alongside the `7d` / `30d` pair on every viewport. The flex-wrap already handles the overflow on narrow tiles, and the standalone comparison-delta block drops its `hidden sm:block` gate so the callout reads on every screen.
This commit is contained in:
1 parent
53c5163901
commit
071f26bf0f
3 files changed
+83
-55
No files matched your search
@@ -272,10 +272,15 @@ export function TrendCard({
|
||||
all-time + comparison-delta into a single line that stays
|
||||
scannable on Galaxy Fold (280 px) and Pixel 5 (375 px). The
|
||||
full layout returns at `>=sm`. */}
|
||||
{/* v1.4.27 MB7 / CF-41 — the `hidden sm:block` gate retired
|
||||
because the mobile-secondary block below is gone (CF-67
|
||||
vestigial wrapper drop). The callout paints on every
|
||||
viewport; when the parent grid is tight on `<sm`, the
|
||||
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={cn("mt-1", avgAllTime !== undefined && "hidden sm:block")}
|
||||
>
|
||||
<div className="mt-1">
|
||||
<span
|
||||
className={cn(
|
||||
"inline-block max-w-full text-xs leading-snug font-medium [overflow-wrap:anywhere] tabular-nums",
|
||||
@@ -374,14 +379,18 @@ export function TrendCard({
|
||||
to last-30 days). Other tiles leave `avgAllTime` undefined so
|
||||
the third sub-row never renders.
|
||||
|
||||
v1.4.22 W5 reconcile (Design-H2) — full label only at
|
||||
`>=sm`; on `<sm` the all-time number moves to the
|
||||
combined secondary row below so the tile keeps the same
|
||||
vertical density as every other trend card. */}
|
||||
v1.4.27 MB7 / CF-41 — the third sub-value renders inline
|
||||
alongside `7d` / `30d` on every viewport. The previous
|
||||
wave split `<sm` into a separate secondary row to keep
|
||||
tile heights uniform, but in practice the flex-wrap on
|
||||
the parent already handles the overflow gracefully — the
|
||||
all-time pair wraps to a second line when the first row
|
||||
fills up. Dropping the duplicated mobile-secondary block
|
||||
also dropped CF-67 (the vestigial wrapper). */}
|
||||
{avgAllTime !== undefined && (
|
||||
<span
|
||||
data-slot="trend-card-all-time"
|
||||
className="hidden max-w-full min-w-0 [overflow-wrap:anywhere] sm:inline"
|
||||
className="max-w-full min-w-0 [overflow-wrap:anywhere]"
|
||||
>
|
||||
{t(avgAllTimeLabelKey)}:{" "}
|
||||
<span
|
||||
@@ -395,48 +404,6 @@ export function TrendCard({
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{/* v1.4.22 W5 reconcile (Design-H2) — combined secondary row
|
||||
for `<sm` viewports. Renders the all-time aggregate AND
|
||||
(when comparison is on) the comparison delta on a single
|
||||
line: "All-time 11% · vs −0.4 last month". Disappears at
|
||||
`>=sm` because the full layout above takes over. */}
|
||||
{avgAllTime !== undefined && (
|
||||
<div
|
||||
data-slot="trend-card-secondary-mobile"
|
||||
className="text-muted-foreground mt-1 min-w-0 text-xs leading-snug [overflow-wrap:anywhere] sm:hidden"
|
||||
>
|
||||
<span>
|
||||
{t(avgAllTimeLabelKey)}:{" "}
|
||||
<span
|
||||
className={cn(
|
||||
"font-medium [overflow-wrap:anywhere] tabular-nums",
|
||||
avgAllTimeColorClass,
|
||||
)}
|
||||
>
|
||||
{avgAllTime !== null ? formatValue(avgAllTime) : "—"}
|
||||
</span>
|
||||
</span>
|
||||
{compareBaseline !== "none" && compareDelta != null && (
|
||||
<>
|
||||
<span aria-hidden="true"> · </span>
|
||||
<span
|
||||
className={cn(
|
||||
"font-medium [overflow-wrap:anywhere] tabular-nums",
|
||||
comparisonDeltaColor,
|
||||
)}
|
||||
data-slot="tile-compare-delta-mobile"
|
||||
data-compare-baseline={compareBaseline}
|
||||
>
|
||||
{`Δ ${formatDelta(compareDelta)}${unit ? ` ${unit}` : ""} ${t(
|
||||
compareBaseline === "lastMonth"
|
||||
? "comparison.captionLastMonth"
|
||||
: "comparison.captionLastYear",
|
||||
)}`}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { useTranslations, useFormatters } from "@/lib/i18n/context";
|
||||
import { stripChartTokens } from "@/lib/insights/chart-tokens";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
// ─── Types ────────────────────────────────────────────────
|
||||
|
||||
@@ -102,15 +105,68 @@ export function InsightStatusCard({
|
||||
token surfaces verbatim. The producer side now strips too;
|
||||
this consumer-side wrap keeps existing caches clean while
|
||||
they roll forward. */}
|
||||
<p className="text-muted-foreground text-sm leading-relaxed">
|
||||
{stripChartTokens(text)}
|
||||
</p>
|
||||
<StatusBody text={stripChartTokens(text)} />
|
||||
<LastUpdatedFooter updatedAt={updatedAt} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* v1.4.27 MB7 / CF-39 — collapsible status copy. The assessment text
|
||||
* routinely lands at 3-5 paragraphs of dense narrative which dominates
|
||||
* the surface on phones. The default collapses to 3 lines via
|
||||
* `line-clamp-3`; a "Show more" toggle reveals the full body. Desktop
|
||||
* users see the full text immediately because the clamp + toggle only
|
||||
* mount when the source string is long enough to actually clip.
|
||||
*/
|
||||
function StatusBody({ text }: { text: string }) {
|
||||
const { locale } = useTranslations();
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
// Cheap heuristic — only mount the toggle when the text is long
|
||||
// enough that the 3-line clamp will hide content. ~220 characters
|
||||
// covers the worst-case sm-viewport line at typical insights text
|
||||
// density; below that we render plain so the user never sees a
|
||||
// useless "Show more" affordance.
|
||||
const isLong = text.length > 220;
|
||||
if (!isLong) {
|
||||
return (
|
||||
<p className="text-muted-foreground text-sm leading-relaxed">{text}</p>
|
||||
);
|
||||
}
|
||||
// v1.4.27 MB7 / CF-39 — toggle copy is two short strings; rather
|
||||
// than reach into the catalogue (which this bucket cannot edit per
|
||||
// the dispatch brief), the labels resolve against the locale prefix
|
||||
// inline. The translation catalogue can claim these strings in a
|
||||
// later cycle without breaking the contract.
|
||||
const isDe = locale.startsWith("de");
|
||||
const showMoreLabel = isDe ? "Mehr anzeigen" : "Show more";
|
||||
const showLessLabel = isDe ? "Weniger anzeigen" : "Show less";
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
<p
|
||||
className={cn(
|
||||
"text-muted-foreground text-sm leading-relaxed",
|
||||
!expanded && "line-clamp-3",
|
||||
)}
|
||||
>
|
||||
{text}
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setExpanded((prev) => !prev)}
|
||||
aria-expanded={expanded}
|
||||
className={cn(
|
||||
"text-foreground/80 hover:text-foreground inline-flex text-xs font-medium",
|
||||
"focus-visible:ring-ring/50 focus-visible:ring-2 focus-visible:outline-none",
|
||||
)}
|
||||
>
|
||||
{expanded ? showLessLabel : showMoreLabel}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function LastUpdatedFooter({ updatedAt }: { updatedAt: string | null }) {
|
||||
const { t } = useTranslations();
|
||||
const fmt = useFormatters();
|
||||
|
||||
@@ -135,10 +135,15 @@ export function Vo2MaxChartRow({
|
||||
<p className="text-muted-foreground text-xs">{unit}</p>
|
||||
</div>
|
||||
|
||||
{/* Stat strip — latest / min / max / avg30 / Δ vs. prior period. */}
|
||||
{/* Stat strip — latest / min / max / avg30 / Δ vs. prior period.
|
||||
v1.4.27 MB7 / CF-38 — extend the 2-column phone grid all the
|
||||
way to `<lg` so tablets keep the 2×2 layout (avoids
|
||||
mid-viewport reflows on Pixel Fold) and lift each cell to a
|
||||
`min-h-[64px]` floor so the chips paint at consistent
|
||||
heights even when one stat has no trend arrow. */}
|
||||
<div
|
||||
data-slot="vo2-chart-row-stats"
|
||||
className="bg-card border-border grid grid-cols-2 gap-3 rounded-xl border p-4 sm:grid-cols-4"
|
||||
className="bg-card border-border grid grid-cols-2 gap-3 rounded-xl border p-4 lg:grid-cols-4 [&>div]:min-h-[64px]"
|
||||
>
|
||||
<div data-slot="vo2-stat" data-stat="latest" className="space-y-1">
|
||||
<p className="text-muted-foreground text-xs uppercase tracking-wide">
|
||||
|
||||
Reference in new issue
Block a user