fix(insights): unify the left reading edge across metric/score cards

Establish one inset system on the Insights surface. SubPageShell's flat
px-6 header sat 8 px right of every card body on mobile — track the card
slot inset (px-4 md:px-6) so the heading-vs-body step disappears.
DeviceScoreTile and ResilienceTile hand-rolled a muted h-4 header row;
route them through TileHeader size=sm (foreground icon, shared gutter),
latest reading in the right slot. ScoreAnatomyView dropped its banned p-5
+ centred uppercase title for a left-aligned TileHeader on p-4 md:p-6, so
the ring hero stays a centred identity block while the header shares the
reading edge with the stacked assessment card. SparklineDeltaTile swaps
its hand-copied header row for the TileHeader primitive so it can't
drift, and its contributor label rounds off the sub-xs size.
This commit is contained in:
Marc-André Bombeck committed 2026-07-09 12:00:12 +02:00
1 parent df78b29cfd
commit de16da43fe
5 files changed
+84 -82

No files matched your search

@@ -10,6 +10,7 @@ import type {
DerivedCoverage,
DerivedProvenance,
} from "@/lib/insights/derived/types";
import { TileHeader } from "@/components/insights/tile-header";
import { ScoreRing } from "./score-ring";
import { CoverageMeter } from "./coverage-meter";
import {
@@ -126,20 +127,19 @@ export function ScoreAnatomyView({
: undefined
}
className={cn(
"flex flex-col gap-5 rounded-xl border p-5",
"flex flex-col gap-4 rounded-xl border p-4 md:p-6",
hue ? "wellness-detail-card" : "border-border bg-card",
className,
)}
aria-label={typeof title === "string" ? title : undefined}
>
{/* Hero: title + ring + caption */}
{/* Left-aligned canonical tile header — sits on the same reading edge as
the sibling assessment card that stacks beneath it on a score
sub-page, instead of the old centred uppercase title that floated
toward the middle and shared no left line with any card. */}
<TileHeader title={title} />
{/* Hero: ring + caption — a centred identity block below the header. */}
<div className="flex flex-col items-center gap-3 text-center">
<h2
data-slot="score-anatomy-title"
className="text-foreground text-sm font-semibold tracking-wide uppercase"
>
{title}
</h2>
{/* v1.15.12 F2 — vertical breathing room so the ring's bloom/glow is
never clipped top/bottom by the hero container. */}
<div className="py-2">
@@ -176,7 +176,7 @@ export function ScoreAnatomyView({
/>
) : (
<div data-slot="score-anatomy-contributors" className="space-y-3">
<p className="text-muted-foreground text-[11px] font-semibold tracking-wide uppercase">
<p className="text-muted-foreground text-xs font-semibold tracking-wide uppercase">
{t("insights.derived.anatomy.contributorsLabel")}
</p>
<ul className="space-y-2.5">
@@ -9,6 +9,7 @@ import {
sentimentColorClass,
type TrendDirectionSentiment,
} from "@/lib/insights/trend-sentiment";
import { TileHeader } from "@/components/insights/tile-header";
/**
* v1.10.0 — the Apple-Health-Highlights grid tile.
@@ -138,27 +139,26 @@ export function SparklineDeltaTile({
className,
)}
>
{/* Header row matches the canonical TileHeader language: a leading
foreground icon + a foreground `CardTitle`-weight heading — never
the old muted-uppercase caption. Kept on one line for the dense
grid; provenance trails to the right edge. */}
<div className="flex min-w-0 items-center gap-2">
<Icon className="text-foreground h-5 w-5 shrink-0" aria-hidden="true" />
<span
className="text-foreground min-w-0 flex-1 truncate text-base leading-none font-semibold whitespace-nowrap"
data-slot="sparkline-delta-tile-label"
>
{label}
</span>
{provenance ? (
<span
data-slot="sparkline-delta-tile-provenance"
className="ml-auto shrink-0"
>
{provenance}
</span>
) : null}
</div>
{/* The canonical tile-header primitive (was a hand-rolled copy of it):
leading foreground icon + foreground `CardTitle` heading, provenance
in the `right` slot. Routing through the primitive keeps this dense
grid tile from drifting out of the header contract. */}
<TileHeader
icon={Icon}
title={label}
className="min-w-0"
titleClassName="min-w-0 truncate whitespace-nowrap"
right={
provenance ? (
<span
data-slot="sparkline-delta-tile-provenance"
className="shrink-0"
>
{provenance}
</span>
) : undefined
}
/>
<div
className="mt-2 flex flex-wrap items-baseline gap-x-1.5"
+26 -26
View File
@@ -6,16 +6,11 @@ import { useAuth } from "@/hooks/use-auth";
import { useInsightsAnalytics } from "@/hooks/use-insights-analytics";
import { useTranslations } from "@/lib/i18n/context";
import type { DataSummary } from "@/lib/analytics/trends";
import {
Card,
CardAction,
CardContent,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { LearningGate } from "@/components/ui/learning-gate";
import { HealthChartDynamicMini } from "@/components/charts/health-chart-dynamic";
import { SectionHeading } from "@/components/insights/section-heading";
import { TileHeader } from "@/components/insights/tile-header";
/**
* v1.17.1 — calm, data-gated readout for a device-native score that has no
@@ -100,26 +95,31 @@ export function DeviceScoreTile({
className={className}
>
<CardHeader>
<CardTitle className="flex min-w-0 items-center gap-2 text-sm">
<Icon
className="text-muted-foreground h-4 w-4 shrink-0"
aria-hidden="true"
/>
<span className="truncate">{title}</span>
</CardTitle>
{latest != null ? (
<CardAction
data-slot="device-score-latest"
className="text-foreground self-baseline text-lg font-semibold tabular-nums"
>
{fmt(latest)}
{unit ? (
<span className="text-muted-foreground ml-1 text-xs font-normal">
{unit}
{/* Canonical compact tile header — foreground `h-4` icon + `text-sm`
title on the shared inset; the latest reading rides the `right`
slot so it stays pinned to the row's right edge. */}
<TileHeader
size="sm"
icon={Icon}
title={title}
className="min-w-0"
titleClassName="min-w-0 truncate"
right={
latest != null ? (
<span
data-slot="device-score-latest"
className="text-foreground text-lg font-semibold tabular-nums"
>
{fmt(latest)}
{unit ? (
<span className="text-muted-foreground ml-1 text-xs font-normal">
{unit}
</span>
) : null}
</span>
) : null}
</CardAction>
) : null}
) : undefined
}
/>
</CardHeader>
<CardContent className="space-y-3">
{isLearning ? (
@@ -4,15 +4,10 @@ import { ShieldCheck } from "lucide-react";
import { useInsightsAnalytics } from "@/hooks/use-insights-analytics";
import { useTranslations } from "@/lib/i18n/context";
import {
Card,
CardAction,
CardContent,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { LearningGate } from "@/components/ui/learning-gate";
import { LearnMoreLink } from "@/components/ui/learn-more-link";
import { TileHeader } from "@/components/insights/tile-header";
/**
* v1.19.2 — calm readout for Oura's daily resilience level.
@@ -96,21 +91,25 @@ export function ResilienceTile() {
return (
<Card data-slot="resilience-tile" data-metric="RESILIENCE">
<CardHeader>
<CardTitle className="flex min-w-0 items-center gap-2 text-sm">
<ShieldCheck
className="text-muted-foreground h-4 w-4 shrink-0"
aria-hidden="true"
/>
<span className="truncate">{t("measurements.typeResilience")}</span>
</CardTitle>
{bandKey != null ? (
<CardAction
data-slot="resilience-band"
className="text-foreground self-baseline text-lg font-semibold"
>
{t(bandKey)}
</CardAction>
) : null}
{/* Canonical compact tile header — foreground `h-4` icon + `text-sm`
title; the current band rides the `right` slot. */}
<TileHeader
size="sm"
icon={ShieldCheck}
title={t("measurements.typeResilience")}
className="min-w-0"
titleClassName="min-w-0 truncate"
right={
bandKey != null ? (
<span
data-slot="resilience-band"
className="text-foreground text-lg font-semibold"
>
{t(bandKey)}
</span>
) : undefined
}
/>
</CardHeader>
<CardContent className="space-y-3">
<p className="text-foreground text-sm leading-relaxed">
+7 -4
View File
@@ -218,10 +218,13 @@ export function SubPageShell({
<div data-slot="insights-subpage" className="space-y-3 md:space-y-4">
{/* v1.8.7.1 — back-nav leads the page, above the heading. */}
{backLink}
{/* The heading + explainer indent to the cards' inner text edge
(`px-6`, the Card content padding), so the page head reads as
part of the card column instead of hanging outside it. */}
<header className="space-y-1.5 px-6">
{/* The heading + explainer indent to the cards' inner text edge so
the page head reads as part of the card column instead of hanging
outside it. Track the card slot inset EXACTLY (`px-4 md:px-6`) —
never a flat `px-6`, which sat 8 px right of every card body on
mobile and reopened the heading-vs-body step (insights left-edge
rule: one reading edge across breakpoints). */}
<header className="space-y-1.5 px-4 md:px-6">
{/* v1.8.6 — the heading row pins the Coach icon top-right while the
title / nudge / badge wrap together on the left.