feat(insights): premium polish for the wellness score detail view

- carry the ring's per-metric tint onto the score-anatomy detail card so
  the tap-through reads as the same surface deepened, not a plain card
- pad the hero ring so its bloom is no longer clipped top/bottom
- lighten the detail card toward the bottom with a subtle theme-aware
  gradient
- drop the "keine klinische Bewertung" footer line; the non-clinical
  framing stays behind the provenance explainer, the composition rows stay
- move the info explainer from the card foot to the top-right corner at
  heading height; popover/sheet behaviour unchanged
This commit is contained in:
Marc-André Bombeck committed 2026-06-07 17:53:45 +02:00
1 parent 7b171cc5e3
commit b07d77cbfa
4 files changed
+120 -22

No files matched your search

+31
View File
@@ -513,6 +513,37 @@
pointer-events: none;
}
/*
* v1.15.12 (F1/F3) — `.wellness-detail-card` is the score-anatomy detail
* surface, deepened to match the dashboard ring tile the user tapped.
*
* F1 — carries the SAME per-metric `--tile-hue` mix over the theme `--card`
* as `.wellness-tile`, so the tap-through reads as the same surface
* rather than a plain bordered card. The hue is set inline per metric.
* F3 — a SUBTLE vertical gradient that lightens toward the BOTTOM: the top
* carries a touch more of the hue (16%) and it eases to 6% at the foot,
* giving the card a premium, lit-from-below depth. Theme-aware by
* construction — both stops mix over the theme `--card`, so it stays
* luminous on the white Alucard card and rich on the dark Dracula one.
* Kept minimal (a 10pp swing) so it never reads as a heavy slab.
*
* Static, no hover lift (it is a page surface, not a tap target) — so it
* needs no reduced-motion handling.
*/
.wellness-detail-card {
--tile-hue: var(--dracula-purple);
position: relative;
overflow: hidden;
background: linear-gradient(
180deg,
color-mix(in srgb, var(--tile-hue) 16%, var(--card)) 0%,
color-mix(in srgb, var(--tile-hue) 6%, var(--card)) 100%
);
border: 1px solid color-mix(in srgb, var(--tile-hue) 22%, transparent);
box-shadow: 0 12px 34px -28px
color-mix(in srgb, var(--tile-hue) 42%, transparent);
}
@media (hover: hover) {
.wellness-tile:hover {
transform: translateY(-2px);
@@ -53,7 +53,27 @@ describe("<ScoreAnatomyView>", () => {
expect(html).toContain('data-contributor="respiratory"');
expect(html).toContain('data-present="false"');
expect(html).toContain('data-slot="provenance-explainer-trigger"');
expect(html).toContain('data-slot="score-anatomy-disclaimer"');
// v1.15.12 F4 — the "keine klinische Bewertung" footer line is removed;
// the non-clinical framing now lives behind the provenance explainer.
expect(html).not.toContain('data-slot="score-anatomy-disclaimer"');
});
it("tints the card + leans the ring hue when a hue is passed (F1)", () => {
const html = render(
<ScoreAnatomyView
title="Readiness"
score={74}
hue="readiness"
contributors={CONTRIBUTORS}
coverage={COVERAGE}
confidence={CONFIDENCE}
provenance={PROVENANCE}
method="A blend of deviation components."
/>,
);
expect(html).toContain('data-tinted="true"');
expect(html).toContain("wellness-detail-card");
expect(html).toContain("--tile-hue");
});
it("renders the insufficient state with the provisional ring + nudge", () => {
@@ -12,6 +12,7 @@ import {
ScoreAnatomyView,
type AnatomyContributor,
} from "./score-anatomy-view";
import type { RingHue } from "./ring-hues";
import { METRIC_PROVENANCE } from "./standards";
/**
@@ -51,6 +52,20 @@ export interface CompositeScoreAnatomyProps {
className?: string;
}
/**
* v1.15.12 F1 — each anatomy metric maps to the same ring hue its dashboard
* tile wears, so the detail card carries the matching tint + the ring arc
* leans the same colour (continuity from the tile tap). Mirrors the per-metric
* `hue` the wellness strip passes its `RingTile`s.
*/
const METRIC_HUE: Record<AnatomyMetricId, RingHue> = {
SLEEP_SCORE: "sleep",
READINESS: "readiness",
RECOVERY_SCORE: "recovery",
STRESS_SCORE: "stress",
STRAIN_SCORE: "strain",
};
export function CompositeScoreAnatomy({
metric,
className,
@@ -117,6 +132,7 @@ export function CompositeScoreAnatomy({
<ScoreAnatomyView
title={title}
score={null}
hue={METRIC_HUE[metric]}
contributors={[]}
coverage={{
requiredInputs: 1,
@@ -181,6 +197,7 @@ export function CompositeScoreAnatomy({
<ScoreAnatomyView
title={title}
score={score}
hue={METRIC_HUE[metric]}
caption={caption}
contributors={contributors}
coverage={data.coverage}
@@ -15,6 +15,7 @@ import {
ProvenanceExplainer,
type ProvenanceStandard,
} from "./provenance-explainer";
import { TILE_HUE, type RingHue } from "./ring-hues";
import {
bandForScore,
BAND_PROGRESS_CLASS,
@@ -64,6 +65,14 @@ export interface ScoreAnatomyViewProps {
score: number | null;
/** Band override; derived from the score when omitted. */
band?: ScoreBand;
/**
* v1.15.12 (F1/F2/F3) — the metric's ring hue. When set, the detail card
* carries the same gentle per-metric tint the dashboard ring tile wears
* (visual continuity from tap to detail) and the ring arc leans the same
* hue instead of the generic band gradient. Omitted → plain `--card` surface
* + band-coloured ring (the legacy look).
*/
hue?: RingHue;
/** Short caption under the ring (e.g. a band word or a one-line summary). */
caption?: ReactNode;
/** Ranked contributors (the caller pre-ranks; the view renders in order). */
@@ -86,6 +95,7 @@ export function ScoreAnatomyView({
title,
score,
band,
hue,
caption,
contributors,
coverage,
@@ -104,12 +114,37 @@ export function ScoreAnatomyView({
<section
data-slot="score-anatomy-view"
data-status={insufficient ? "insufficient" : "ok"}
// v1.15.12 F1/F3 — when a `hue` is set, the detail card carries the same
// gentle `--tile-hue` mix + bottom-leaning gradient the dashboard ring
// tile wears (`.wellness-tile` family), so the tap-through reads as the
// same surface deepened. No hue → the plain bordered `--card` look.
data-tinted={hue ? "true" : undefined}
style={
hue
? ({ "--tile-hue": TILE_HUE[hue] } as React.CSSProperties)
: undefined
}
className={cn(
"border-border bg-card flex flex-col gap-5 rounded-xl border p-5",
"relative flex flex-col gap-5 rounded-xl border p-5",
hue
? "wellness-detail-card"
: "border-border bg-card",
className,
)}
aria-label={typeof title === "string" ? title : undefined}
>
{/* v1.15.12 F5 — the ⓘ explainer is pinned TOP-right at heading height
(it used to trail the card at bottom-right). Absolute so it never
shifts the centred hero title; the trigger keeps its 44px touch
target + popover/sheet behaviour. */}
<div className="absolute top-3 right-3 z-10">
<ProvenanceExplainer
provenance={provenance}
method={method}
standard={standard}
/>
</div>
{/* Hero: title + ring + caption */}
<div className="flex flex-col items-center gap-3 text-center">
<h2
@@ -118,12 +153,17 @@ export function ScoreAnatomyView({
>
{title}
</h2>
<ScoreRing
score={effectiveScore}
band={band}
label={t("insights.derived.anatomy.outOf")}
size="lg"
/>
{/* 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">
<ScoreRing
score={effectiveScore}
band={band}
hue={insufficient ? undefined : hue}
label={t("insights.derived.anatomy.outOf")}
size="lg"
/>
</div>
{caption && !insufficient ? (
<p
data-slot="score-anatomy-caption"
@@ -160,20 +200,10 @@ export function ScoreAnatomyView({
</div>
)}
{/* Provenance + cited standard + the non-clinical disclaimer */}
<div className="flex items-center justify-between gap-2 border-t pt-3">
<p
data-slot="score-anatomy-disclaimer"
className="text-muted-foreground text-[10px] leading-snug"
>
{t("insights.derived.anatomy.disclaimer")}
</p>
<ProvenanceExplainer
provenance={provenance}
method={method}
standard={standard}
/>
</div>
{/* v1.15.12 F4 — the "Richtwert — keine klinische Bewertung" footer line
is gone; the method + cited standard live behind the provenance
explainer (now top-right, F5), which carries the non-clinical framing
where it belongs. The composition (contributor rows above) stays. */}
</section>
);
}