mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-15 15:02:04 +00:00
feat(dashboard): cohesion polish — hero score rings return, per-metric accents, quiet milestone moment
- Resurface the selected hero score rings on the Today hero: the Settings picker's selection and drag order render again as a calm ring cluster beneath the health ring, each ring in its wellness hue, flat-painted, and linking to its existing detail surface (score anatomy pages, medications). The health ring now opens the Insights overview like its cluster siblings. Empty selection keeps the hero exactly as before. - Per-metric identity accents on the daily rail via the existing --tile-* hue vocabulary: a soft inset edge (stress for tension windows, strain for ECG pointers, primary for dose and coach cards); the same mark ties the ECG cross-link and the intraday pulse card shell to their rail siblings. Utility items (sync, preventive care) stay accent-less. - Quiet "reached" treatment for the milestone card: a soft success halo over the theme card plus a one-shot spring reveal on --ring-spring, with its reduced-motion fallback beside the keyframes. - The Today hero carries the one sanctioned atmosphere wash (a faint primary mix over the card in the wellness-tile pattern); the loading skeleton shares it so the swap changes content, never the surface. - Skeleton primitive upgraded from the whole-block pulse to a single soft shimmer sweep, reduced-motion guarded in globals.css; dependent test assertions repointed. - Ring link / dose-tally aria strings restored in all six locales.
This commit is contained in:
1 parent
8ac38926c4
commit
cb883e1470
21 files changed
+427
-48
No files matched your search
@@ -138,8 +138,10 @@ describe("TrendCardSkeleton", () => {
|
||||
// Same outer chrome + min-height floor as the real TrendCard slot.
|
||||
expect(html).toContain("rounded-xl");
|
||||
expect(html).toContain("min-h-[8rem]");
|
||||
// Reduced motion honoured via the Skeleton primitive.
|
||||
expect(html).toContain("motion-reduce:animate-none");
|
||||
// Reduced motion honoured via the Skeleton primitive's shimmer class
|
||||
// (its prefers-reduced-motion guard lives beside the keyframes in
|
||||
// globals.css).
|
||||
expect(html).toContain("skeleton-shimmer");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -897,6 +897,113 @@
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* ── v1.29.0 cohesion pass — the daily-value system's finishing layer ──
|
||||
*
|
||||
* Four small surface treatments, all expressed through the existing
|
||||
* token/motion vocabulary (no new palette, no new easing):
|
||||
*
|
||||
* `.today-hero-wash` — the ONE sanctioned Today atmosphere: a faint
|
||||
* `--primary` mix over the theme `--card` in the
|
||||
* `.wellness-tile` pattern, leaning toward the
|
||||
* ring corner. Softer than the insights
|
||||
* `.hero-gradient` (8% → 0 vs 18/12) so the
|
||||
* promoted hero reads as the calm first surface,
|
||||
* not a banner. Static — nothing to motion-gate.
|
||||
* `.metric-accent` — per-metric identity edge for rail cards and
|
||||
* cross-link pointers. Reads `--tile-hue` (set
|
||||
* inline from the SAME `--tile-*` vocabulary the
|
||||
* wellness tiles use; `--primary` for the med /
|
||||
* coach family) and paints a soft inset edge on
|
||||
* the reading side via a ::before overlay, so it
|
||||
* composes with the card's own `shadow-sm` and
|
||||
* the semantic status washes (which keep the
|
||||
* background). No layout width is added; the
|
||||
* inset shadow follows the border radius.
|
||||
* `.milestone-reached` — the S12 quiet celebration: a soft `--success`
|
||||
* halo over the theme `--card` plus a one-shot
|
||||
* spring reveal on the shared `--ring-spring`
|
||||
* curve. No confetti, no loop; reduced-motion
|
||||
* paints the settled card at once.
|
||||
* `.skeleton-shimmer` — the loading language upgrade: a single soft
|
||||
* `--foreground`-mix sweep across the muted
|
||||
* skeleton base instead of the whole-block
|
||||
* `animate-pulse` throb. Load-terminating by
|
||||
* construction (skeletons unmount with the data);
|
||||
* reduced-motion collapses to the static base.
|
||||
*/
|
||||
.today-hero-wash {
|
||||
background: radial-gradient(
|
||||
120% 150% at 100% 0%,
|
||||
color-mix(in srgb, var(--primary) 8%, var(--card)) 0%,
|
||||
var(--card) 60%
|
||||
);
|
||||
}
|
||||
|
||||
.metric-accent {
|
||||
position: relative;
|
||||
}
|
||||
.metric-accent::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: inherit;
|
||||
pointer-events: none;
|
||||
box-shadow: inset 3px 0 0 0
|
||||
color-mix(in srgb, var(--tile-hue, var(--primary)) 55%, transparent);
|
||||
}
|
||||
|
||||
.milestone-reached {
|
||||
background: radial-gradient(
|
||||
130% 130% at 85% 0%,
|
||||
color-mix(in srgb, var(--success) 14%, var(--card)) 0%,
|
||||
color-mix(in srgb, var(--success) 4%, var(--card)) 60%,
|
||||
var(--card) 100%
|
||||
);
|
||||
animation: milestone-reached-in 640ms var(--ring-spring) both;
|
||||
}
|
||||
@keyframes milestone-reached-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(6px) scale(0.98);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.milestone-reached {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes skeleton-shimmer {
|
||||
from {
|
||||
background-position: 200% 0;
|
||||
}
|
||||
to {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
}
|
||||
.skeleton-shimmer {
|
||||
background-image: linear-gradient(
|
||||
100deg,
|
||||
transparent 36%,
|
||||
color-mix(in srgb, var(--foreground) 6%, transparent) 50%,
|
||||
transparent 64%
|
||||
);
|
||||
background-size: 250% 100%;
|
||||
background-repeat: no-repeat;
|
||||
animation: skeleton-shimmer 1.8s ease-in-out infinite;
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.skeleton-shimmer {
|
||||
animation: none;
|
||||
background-image: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Global reduced-motion backstop ──────────────────────────────────
|
||||
* Defensive catch-all so any animation/transition we forget to gate
|
||||
* per-component still collapses to near-instant when the user has asked
|
||||
|
||||
+30
-1
@@ -27,8 +27,11 @@ import { convertGlucose, resolveGlucoseUnit } from "@/lib/glucose";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
resolveDashboardLayout,
|
||||
resolveHeroRingOrder,
|
||||
HEALTH_SCORE_RING_ID,
|
||||
type DashboardLayout,
|
||||
} from "@/lib/dashboard-layout";
|
||||
import type { DashboardScoreRing } from "@/lib/dashboard/score-rings";
|
||||
import type { DashboardAnalyticsData as AnalyticsData } from "@/types/analytics";
|
||||
import dynamic from "next/dynamic";
|
||||
import Link from "next/link";
|
||||
@@ -232,6 +235,32 @@ export default function DashboardPageClient() {
|
||||
const insightsEnabled = useModuleEnabled("insights");
|
||||
const digestQuery = useDailyDigest(isAuthenticated && insightsEnabled);
|
||||
|
||||
// v1.29.0 — the Settings-picked hero score rings, resurfaced on the Today
|
||||
// hero. The snapshot resolves the selection server-side (data- and
|
||||
// module-gated, selection order preserved); `resolveHeroRingOrder`
|
||||
// reconciles the user's persisted drag order against what actually
|
||||
// resolved — the SAME reconciler the Settings picker uses. The
|
||||
// health-score ring stays the hero's fixed anchor, so its position in the
|
||||
// stored order is skipped here; the legacy non-snapshot path carries no
|
||||
// resolved rings and degrades to the health ring alone.
|
||||
const heroScoreRings = useMemo<DashboardScoreRing[]>(() => {
|
||||
const snap = snapshotQuery.data;
|
||||
const rings = snap?.scoreRings ?? [];
|
||||
if (rings.length === 0) return [];
|
||||
const order = resolveHeroRingOrder(
|
||||
snap?.layout?.heroRingOrder,
|
||||
rings.map((r) => r.id),
|
||||
);
|
||||
const byId = new Map(rings.map((r) => [r.id, r]));
|
||||
const ordered: DashboardScoreRing[] = [];
|
||||
for (const id of order) {
|
||||
if (id === HEALTH_SCORE_RING_ID) continue;
|
||||
const ring = byId.get(id);
|
||||
if (ring) ordered.push(ring);
|
||||
}
|
||||
return ordered;
|
||||
}, [snapshotQuery.data]);
|
||||
|
||||
const analyticsSlimQuery = useAnalyticsQuery({
|
||||
slice: "summaries",
|
||||
enabled: !snapshotEnabled && isAuthenticated,
|
||||
@@ -783,7 +812,7 @@ export default function DashboardPageClient() {
|
||||
(!mounted || digestQuery.isLoading ? (
|
||||
<TodayHeroSkeleton />
|
||||
) : digestQuery.data ? (
|
||||
<TodayHero digest={digestQuery.data} />
|
||||
<TodayHero digest={digestQuery.data} rings={heroScoreRings} />
|
||||
) : null)}
|
||||
|
||||
{/* v1.18.6 — the spotlight tour launcher moved to the app-shell
|
||||
|
||||
Reference in new issue
Block a user