mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 14:16:12 +00:00
feat(targets): card layout with status pill + consistency + coach handoff
Replaces the inline 790-line page.tsx composition with the W3e
primitives extracted in the previous phase. The page is now ~270
lines of orchestration:
- TanStack-Query pulls the redesigned targets payload (with the
v1.4.25 consistency + summary additions).
- A second query reads /api/insights/provider-chain to derive
`aiEnabled = chainStatus?.activeProvider != null`. The flag threads
down into every <TargetCard>, which uses it to gate the per-card
Coach CTA — no broken-button state when the user has no provider
configured.
- A new `useCoachHandoff()` hook (src/hooks/use-coach-handoff.ts)
owns the drawer's open + prefill + scope triple. Per-card buttons
call `askCoach()`; the hook flips state and the drawer mounts
inline at the page level.
- Cards now render in a fixed order (BP / BP-in-target / WEIGHT /
PULSE / BMI / MOOD / COMPLIANCE / SLEEP / BODY_FAT / STEPS) — Marc
directive: stable visual hierarchy beats status-sorting because
the user's eye lands on the same metric in the same place every
visit.
- Source-citation URLs stay on the page (declarative routing) and
thread into <TargetCard> via the `sourceLink` prop.
The old inline TargetCard / RangeBar / TrendIcon / status-category
map are deleted along with the dead `Badge` + lucide imports.
Existing targets-i18n + targets-spacing tests updated to mock the
new useQueryClient + useMutation surfaces that the embedded
<CoachDrawer> needs at SSR; both suites still assert the same
visual contracts and pass.
Wave 2 deliverables also folded into this commit:
- pageSummary mounts via the W3e header above the grid
- consistency strip renders only when insufficientData === false
- recency caption ("last met goal: 2 days ago") renders only when
lastMetGoalAt !== null
- streak chip ("4-day streak") renders only when streakDays ≥ 3
This commit is contained in:
1 parent
96c5e79539
commit
f2b9f843b1
9 files changed
+269
-606
No files matched your search
@@ -122,9 +122,7 @@ export function ConsistencyStrip({
|
||||
data-band={band ?? "none"}
|
||||
className={cn(
|
||||
"size-2.5 rounded-full border transition-colors",
|
||||
band
|
||||
? BAND_STYLES[band]
|
||||
: "border-border bg-transparent",
|
||||
band ? BAND_STYLES[band] : "border-border bg-transparent",
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -161,9 +161,7 @@ function translateStatus(
|
||||
* middle, which paints the pill in the neutral amber tone rather than
|
||||
* lighting up a false-positive green/red.
|
||||
*/
|
||||
function statusGroupForCategory(
|
||||
category: string,
|
||||
): "in" | "near" | "out" {
|
||||
function statusGroupForCategory(category: string): "in" | "near" | "out" {
|
||||
const greenCategories = new Set([
|
||||
"Normal",
|
||||
"Optimal",
|
||||
@@ -370,11 +368,12 @@ export function TargetCard({
|
||||
target.consistency7d != null &&
|
||||
target.consistency7d.length > 0;
|
||||
|
||||
const showLastMet =
|
||||
target.lastMetGoalAt != null && !target.insufficientData;
|
||||
const showLastMet = target.lastMetGoalAt != null && !target.insufficientData;
|
||||
|
||||
const showStreak =
|
||||
target.streakDays != null && target.streakDays >= 3 && !target.insufficientData;
|
||||
target.streakDays != null &&
|
||||
target.streakDays >= 3 &&
|
||||
!target.insufficientData;
|
||||
|
||||
// Coach prefill is built from the live state of the card; the helper
|
||||
// file owns the per-metric template + locale resolution.
|
||||
@@ -409,7 +408,7 @@ export function TargetCard({
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<h3
|
||||
className="text-muted-foreground text-[0.6875rem] font-medium uppercase tracking-[0.08em]"
|
||||
className="text-muted-foreground text-[0.6875rem] font-medium tracking-[0.08em] uppercase"
|
||||
data-slot="target-card-title"
|
||||
>
|
||||
{titleLabel}
|
||||
@@ -456,7 +455,7 @@ export function TargetCard({
|
||||
<div className="flex items-baseline gap-2">
|
||||
{moodStabilityValue && moodStabilityCopy ? (
|
||||
<span
|
||||
className="text-foreground text-2xl font-semibold leading-none sm:text-3xl"
|
||||
className="text-foreground text-2xl leading-none font-semibold sm:text-3xl"
|
||||
data-slot="target-headline-value"
|
||||
data-mood-stability={moodStabilityValue}
|
||||
>
|
||||
@@ -464,7 +463,7 @@ export function TargetCard({
|
||||
</span>
|
||||
) : isBp && bpDiastolic?.current != null ? (
|
||||
<span
|
||||
className="text-foreground flex items-baseline gap-1 text-2xl font-semibold leading-none sm:text-3xl"
|
||||
className="text-foreground flex items-baseline gap-1 text-2xl leading-none font-semibold sm:text-3xl"
|
||||
data-slot="target-headline-value"
|
||||
>
|
||||
<span>{Math.round(target.current)}</span>
|
||||
@@ -473,7 +472,7 @@ export function TargetCard({
|
||||
</span>
|
||||
) : (
|
||||
<span
|
||||
className="text-foreground text-2xl font-semibold leading-none sm:text-3xl"
|
||||
className="text-foreground text-2xl leading-none font-semibold sm:text-3xl"
|
||||
data-slot="target-headline-value"
|
||||
>
|
||||
{target.type === "BODY_FAT"
|
||||
|
||||
@@ -4,10 +4,7 @@ import { Sparkles } from "lucide-react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import type {
|
||||
CoachScope,
|
||||
CoachScopeSource,
|
||||
} from "@/lib/ai/coach/types";
|
||||
import type { CoachScope, CoachScopeSource } from "@/lib/ai/coach/types";
|
||||
|
||||
/**
|
||||
* v1.4.25 W3e — per-card "Ask Coach about this" CTA.
|
||||
|
||||
@@ -67,7 +67,7 @@ export function TargetsSummaryHeader({
|
||||
<div className="flex items-center gap-2">
|
||||
{allMet && (
|
||||
<CheckCircle2
|
||||
className="text-[var(--dracula-green)] size-4 shrink-0"
|
||||
className="size-4 shrink-0 text-[var(--dracula-green)]"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
|
||||
Reference in new issue
Block a user