mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 22:32:04 +00:00
merge: s4-yardstick into release/v1.34.0
This commit is contained in:
commit
b402be221d
39 files changed
+1605
-215
No files matched your search
@@ -359,3 +359,75 @@ describe("<HealthScoreCard>", () => {
|
||||
expect(progressMatch?.[0]).toContain('aria-valuemax="100"');
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* v1.34 — the weight row names its yardstick. Before this the score graded
|
||||
* weight against a band the user never set and the card said nothing at all,
|
||||
* which is why the defect survived for months.
|
||||
*/
|
||||
describe("<HealthScoreCard> — weight yardstick disclosure", () => {
|
||||
it("names the user's own target band", () => {
|
||||
const html = ssr(
|
||||
<HealthScoreCard
|
||||
score={80}
|
||||
band="green"
|
||||
components={baseComponents}
|
||||
delta={null}
|
||||
weightYardstick={{ source: "user", targetLabel: "64-70 kg" }}
|
||||
/>,
|
||||
);
|
||||
const line = html.match(
|
||||
/<p[^>]*data-slot="health-score-card-weight-yardstick"[^>]*>([\s\S]*?)<\/p>/,
|
||||
);
|
||||
expect(line).not.toBeNull();
|
||||
expect(line?.[0]).toContain('data-target-source="user"');
|
||||
expect(line?.[1]).toContain("64-70 kg");
|
||||
});
|
||||
|
||||
it("says plainly when no target graded the row", () => {
|
||||
const html = ssr(
|
||||
<HealthScoreCard
|
||||
score={80}
|
||||
band="green"
|
||||
components={baseComponents}
|
||||
delta={null}
|
||||
weightYardstick={{ source: "none", targetLabel: null }}
|
||||
/>,
|
||||
);
|
||||
const line = html.match(
|
||||
/<p[^>]*data-slot="health-score-card-weight-yardstick"[^>]*>([\s\S]*?)<\/p>/,
|
||||
);
|
||||
expect(line).not.toBeNull();
|
||||
expect(line?.[0]).toContain('data-target-source="none"');
|
||||
expect(line?.[1]).toMatch(/trend/i);
|
||||
});
|
||||
|
||||
it("discloses nothing when the payload carries no yardstick", () => {
|
||||
// An older cached body has no `targetSource`; silence beats a guess.
|
||||
const html = ssr(
|
||||
<HealthScoreCard
|
||||
score={80}
|
||||
band="green"
|
||||
components={baseComponents}
|
||||
delta={null}
|
||||
/>,
|
||||
);
|
||||
expect(html).not.toContain("health-score-card-weight-yardstick");
|
||||
});
|
||||
|
||||
it("stays quiet when the weight pillar itself has no value", () => {
|
||||
const html = ssr(
|
||||
<HealthScoreCard
|
||||
score={80}
|
||||
band="green"
|
||||
components={{
|
||||
...baseComponents,
|
||||
weight: { value: null, weight: 0 },
|
||||
}}
|
||||
delta={null}
|
||||
weightYardstick={{ source: "none", targetLabel: null }}
|
||||
/>,
|
||||
);
|
||||
expect(html).not.toContain("health-score-card-weight-yardstick");
|
||||
});
|
||||
});
|
||||
@@ -22,7 +22,8 @@ import { HealthScoreDeltaExplainer } from "./health-score-delta-explainer";
|
||||
* and stacks below the title block on `<lg`. Surfaces:
|
||||
* - the composite 0..100 score with a band-coloured number
|
||||
* - "vs last week" delta line with arrow + percentage
|
||||
* - 4 component rows (BP / Weight / Mood / Compliance) with sub-bars
|
||||
* - 4 component rows (BP / Weight / Mood / Compliance) with sub-bars,
|
||||
* the weight row naming the yardstick it was graded against
|
||||
* - "Indicative — not a clinical assessment" disclaimer
|
||||
* - "Ask the Coach" button that opens the B2 drawer with a prefill
|
||||
* ("Why is my health score X out of 100?") so the user can drill
|
||||
@@ -128,6 +129,20 @@ export interface HealthScoreCardProps {
|
||||
/** Days the metric has now sat back inside its range. */
|
||||
daysInside: number;
|
||||
} | null;
|
||||
/**
|
||||
* v1.34 — the weight row's yardstick, disclosed.
|
||||
*
|
||||
* `"user"` renders "vs your target 64-70 kg" (the parent formats
|
||||
* `targetLabel` in the reader's display unit); `"none"` renders the
|
||||
* trend-only line. The score used to grade weight against a BMI-22 band
|
||||
* the user never set, and no surface said so — that silence is what this
|
||||
* line ends. Omitted only by callers that render no weight pillar.
|
||||
*/
|
||||
weightYardstick?: {
|
||||
source: "user" | "none";
|
||||
/** Already unit-formatted band, e.g. "64-70 kg". Null when source is "none". */
|
||||
targetLabel: string | null;
|
||||
} | null;
|
||||
}
|
||||
|
||||
type ComponentKey = keyof HealthScoreCardProps["components"];
|
||||
@@ -212,6 +227,7 @@ export function HealthScoreCard({
|
||||
restModeActive = false,
|
||||
tension = null,
|
||||
returnToBand = null,
|
||||
weightYardstick = null,
|
||||
}: HealthScoreCardProps) {
|
||||
const { t, locale } = useTranslations();
|
||||
// The asOf timestamps render under the source pill as a tooltip
|
||||
@@ -539,38 +555,58 @@ export function HealthScoreCard({
|
||||
key={key}
|
||||
data-slot="health-score-card-component-row"
|
||||
data-component={key}
|
||||
className="flex items-center gap-2 text-xs"
|
||||
className="text-xs"
|
||||
>
|
||||
{/* v1.4.25 W3 — widened label column from w-16 (64px) to
|
||||
w-24 (96px) so the longest German label
|
||||
("Einnahmetreue" — 13 chars at 11px) sits inside the
|
||||
column without spilling into the bar/value chip. */}
|
||||
<span className="text-muted-foreground w-24 shrink-0 truncate">
|
||||
{label}
|
||||
</span>
|
||||
<div
|
||||
className="bg-muted/50 h-1.5 flex-1 overflow-hidden rounded-full"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{/* v1.4.25 W3 — widened label column from w-16 (64px) to
|
||||
w-24 (96px) so the longest German label
|
||||
("Einnahmetreue" — 13 chars at 11px) sits inside the
|
||||
column without spilling into the bar/value chip. */}
|
||||
<span className="text-muted-foreground w-24 shrink-0 truncate">
|
||||
{label}
|
||||
</span>
|
||||
<div
|
||||
className={cn(
|
||||
"h-full",
|
||||
value === null ? "bg-muted" : BAND_PROGRESS_CLASS[band],
|
||||
)}
|
||||
style={{
|
||||
width:
|
||||
value === null
|
||||
? "0%"
|
||||
: `${Math.max(0, Math.min(100, value))}%`,
|
||||
}}
|
||||
/>
|
||||
className="bg-muted/50 h-1.5 flex-1 overflow-hidden rounded-full"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"h-full",
|
||||
value === null ? "bg-muted" : BAND_PROGRESS_CLASS[band],
|
||||
)}
|
||||
style={{
|
||||
width:
|
||||
value === null
|
||||
? "0%"
|
||||
: `${Math.max(0, Math.min(100, value))}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
data-slot="health-score-card-component-value"
|
||||
className="text-foreground w-8 shrink-0 text-right tabular-nums"
|
||||
>
|
||||
{value === null ? "—" : Math.round(value)}
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
data-slot="health-score-card-component-value"
|
||||
className="text-foreground w-8 shrink-0 text-right tabular-nums"
|
||||
>
|
||||
{value === null ? "—" : Math.round(value)}
|
||||
</span>
|
||||
{/* v1.34 — the weight row names its yardstick. Either the user's
|
||||
own target band or an explicit "trend only, no target set".
|
||||
Indented to the label column so it reads as that row's
|
||||
caption rather than a new row. */}
|
||||
{key === "weight" && weightYardstick && value !== null && (
|
||||
<p
|
||||
data-slot="health-score-card-weight-yardstick"
|
||||
data-target-source={weightYardstick.source}
|
||||
className="text-muted-foreground mt-1 pl-[6.5rem] leading-snug"
|
||||
>
|
||||
{weightYardstick.source === "user" &&
|
||||
weightYardstick.targetLabel
|
||||
? t("insights.healthScore.weightTarget.user", {
|
||||
range: weightYardstick.targetLabel,
|
||||
})
|
||||
: t("insights.healthScore.weightTarget.none")}
|
||||
</p>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
@@ -95,6 +95,12 @@ interface HeroStripProps {
|
||||
*/
|
||||
restMode?: { active: boolean } | null;
|
||||
} | null;
|
||||
/**
|
||||
* v1.34 — the weight pillar's yardstick, already resolved into the reader's
|
||||
* display unit by the page (the hero carries no unit context of its own).
|
||||
* Forwarded verbatim to the score card, which renders the disclosure line.
|
||||
*/
|
||||
weightYardstick?: HealthScoreCardProps["weightYardstick"];
|
||||
/**
|
||||
* v1.21.2 (A5) — Tension Verdict, server-resolved + locale-agnostic. `band`
|
||||
* is the readiness composite's band; `positive` / `negative` carry the
|
||||
@@ -188,6 +194,7 @@ export function HeroStrip({
|
||||
noProviderStale = false,
|
||||
tension = null,
|
||||
returnToBand = null,
|
||||
weightYardstick = null,
|
||||
}: HeroStripProps) {
|
||||
const { t } = useTranslations();
|
||||
const fmt = useFormatters();
|
||||
@@ -412,6 +419,7 @@ export function HeroStrip({
|
||||
restModeActive={healthScore.restMode?.active ?? false}
|
||||
tension={tensionProp}
|
||||
returnToBand={returnToBandProp}
|
||||
weightYardstick={weightYardstick}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user