mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
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:
1 parent
7b171cc5e3
commit
b07d77cbfa
4 files changed
+120
-22
No files matched your search
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user