mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 22:02:16 +00:00
style(insights): calm and flatten the health-score rings
The score dials wore a token-tinted drop-shadow bloom, a conic specular sheen, a breathing pulse on the green ring, and a dark-to-neon two-stop arc gradient. Against the flat 2-D Recharts charts that read as a glowing 3-D gauge and made the overview restless. Make the calm treatment the default everywhere, not only behind the `flat` prop: - Drop the bloom, the sheen, and the pulse for every ring. `.wellness-ring-arc` is now a plain flat stroke; the sheen overlay paints nothing. The shared cycle ring inherits the same quiet. - Mute the arc colours toward the chart palette: the two gradient stops sit a hair apart for a whisper of depth instead of a neon sweep, pulled down and desaturated per hue in both the Dracula and Alucard sets. Band semantics (green/yellow/red thresholds) are unchanged. - `flat` now only drops the entrance sweep + count-up, so the dashboard hero ring paints its value at once. The /insights strip keeps the subtle spring fill; prefers-reduced-motion still zeroes it. Score and band logic, thresholds, and the data-band/aria contract are untouched.
This commit is contained in:
1 parent
f931f2f5c2
commit
d92b3b9efd
3 files changed
+116
-222
No files matched your search
@@ -1,22 +1,19 @@
|
||||
/**
|
||||
* v1.14.0 — per-metric ring hues for the wellness-score strip.
|
||||
*
|
||||
* The premium signature is a SINGLE-hue, TWO-stop gradient per ring
|
||||
* (dark → light tones of the *same* colour), never a multi-hue rainbow.
|
||||
* Each metric leans toward one intentional, distinct hue (Oura's per-metric
|
||||
* move): readiness green, recovery cyan, sleep purple, stress orange,
|
||||
* strain pink — five clearly different hues so the strip never reads as
|
||||
* "a wash of green/cyan". In the dark theme every `to` stop sits exactly
|
||||
* on the matching Dracula palette colour.
|
||||
* Each metric leans toward one intentional, distinct hue: readiness green,
|
||||
* recovery cyan, sleep purple, stress orange, strain pink — five clearly
|
||||
* different hues so the strip never reads as "a wash of green/cyan".
|
||||
*
|
||||
* The actual colours live as CSS custom properties in `globals.css`
|
||||
* v1.18.7 — CALM. The arc was a dark→neon two-stop gradient (the `to` stop
|
||||
* sat on the raw Dracula palette colour) plus a bloom + sheen, which glowed
|
||||
* against the flat charts. The two stops are now MUTED and sit a hair apart,
|
||||
* so each arc reads as a near-flat single tone — quiet as a chart series. The
|
||||
* actual colours live as CSS custom properties in `globals.css`
|
||||
* (`--ring-<key>-from` / `--ring-<key>-to` / `--tile-<key>`) with a Dracula
|
||||
* dark set on `:root` and an Alucard light set on `:root.light`, so each hue
|
||||
* is HAND-TUNED per theme — deep + saturated enough to hold contrast on the
|
||||
* white Alucard card, vivid on the dark Dracula card. (The v1.13.x version
|
||||
* mixed through ambiguous semantic tokens — `--info` == `--dracula-cyan` —
|
||||
* which collapsed three of the five rings to cyan in the dark theme and
|
||||
* washed recovery/strain out on white. Dedicated per-theme vars fix that.)
|
||||
* is HAND-TUNED per theme — deep enough to hold contrast on the white Alucard
|
||||
* card without going neon on the dark Dracula card.
|
||||
*
|
||||
* The band semantic is never carried by the hue: it still rides `data-band`
|
||||
* + the band word + the aria-label, so a red-band readiness still reads "low"
|
||||
@@ -29,11 +26,11 @@ import type { ScoreBand } from "./band-tokens";
|
||||
export type RingHue = "readiness" | "sleep" | "recovery" | "stress" | "strain";
|
||||
|
||||
/**
|
||||
* Single-hue two-stop gradient `[from, to]` (dark → light) per metric, plus a
|
||||
* band fallback for the anatomy detail view (which passes no `hue`, so the
|
||||
* ring keeps its green/yellow/red band semantics there). Values are CSS var
|
||||
* references resolved per theme — consumed straight by the SVG
|
||||
* `<linearGradient>` stops.
|
||||
* Near-flat single-hue arc `[from, to]` per metric, plus a band fallback for
|
||||
* the anatomy detail view (which passes no `hue`, so the ring keeps its
|
||||
* green/yellow/red band semantics there). The two stops are a hair apart for a
|
||||
* whisper of depth, not a neon sweep. Values are CSS var references resolved
|
||||
* per theme — consumed straight by the SVG `<linearGradient>` stops.
|
||||
*/
|
||||
export const RING_GRADIENT: Record<RingHue | ScoreBand, [string, string]> = {
|
||||
readiness: ["var(--ring-readiness-from)", "var(--ring-readiness-to)"],
|
||||
@@ -47,21 +44,6 @@ export const RING_GRADIENT: Record<RingHue | ScoreBand, [string, string]> = {
|
||||
red: ["var(--ring-red-from)", "var(--ring-red-to)"],
|
||||
};
|
||||
|
||||
/**
|
||||
* The `to` (light) stop per key — used as the ring's glow colour
|
||||
* (`--ring-glow`) so the bloom matches the brighter end of the arc.
|
||||
*/
|
||||
export const RING_GLOW: Record<RingHue | ScoreBand, string> = {
|
||||
readiness: "var(--ring-readiness-to)",
|
||||
recovery: "var(--ring-recovery-to)",
|
||||
sleep: "var(--ring-sleep-to)",
|
||||
stress: "var(--ring-stress-to)",
|
||||
strain: "var(--ring-strain-to)",
|
||||
green: "var(--ring-green-to)",
|
||||
yellow: "var(--ring-yellow-to)",
|
||||
red: "var(--ring-red-to)",
|
||||
};
|
||||
|
||||
/**
|
||||
* The surface tint per metric — the `--tile-hue` the `.wellness-tile` CSS
|
||||
* mixes (gently) over the theme `--card`. Set inline by each `RingTile`.
|
||||
|
||||
@@ -6,25 +6,31 @@ import { cn } from "@/lib/utils";
|
||||
import { useCountUp } from "@/hooks/use-count-up";
|
||||
import { prefersReducedMotion } from "@/lib/charts/reduced-motion";
|
||||
import { bandForScore, clampScore, type ScoreBand } from "./band-tokens";
|
||||
import { RING_GRADIENT, RING_GLOW, type RingHue } from "./ring-hues";
|
||||
import { RING_GRADIENT, type RingHue } from "./ring-hues";
|
||||
|
||||
/**
|
||||
* v1.14.0 — the composite-score dial: a hand-rolled SVG arc gauge with a
|
||||
* premium "signature reveal" (Oura/WHOOP-tier), 0 KB runtime, no animation
|
||||
* library. A score dial is a fixed-geometry gauge, not a data chart, so SVG
|
||||
* gives exact control over a thin round-cap arc, a segmented same-hue track,
|
||||
* a single-hue two-stop gradient, a faint "vs your baseline" ghost arc, and a
|
||||
* token-tinted bloom — all CSS-driven.
|
||||
* v1.14.0 — the composite-score dial: a hand-rolled SVG arc gauge, 0 KB
|
||||
* runtime, no animation library. A score dial is a fixed-geometry gauge, not a
|
||||
* data chart, so SVG gives exact control over a thin round-cap arc, a
|
||||
* segmented same-hue track, a near-flat single-hue arc, and a faint "vs your
|
||||
* baseline" ghost arc.
|
||||
*
|
||||
* Motion (all CSS-first, all gated on `prefers-reduced-motion`):
|
||||
* v1.18.7 — CALM by default. The earlier dial wore a token-tinted bloom, a
|
||||
* conic specular sheen, and a breathing pulse on the green ring; against the
|
||||
* flat 2-D Recharts charts that read as a glowing 3-D gauge. All three are
|
||||
* gone everywhere — the ring now paints a plain stroke in a muted single hue,
|
||||
* as quiet as a chart series. The only motion left is a subtle spring fill on
|
||||
* first reveal + a count-up, both gated on `prefers-reduced-motion`.
|
||||
*
|
||||
* Motion (CSS-first, gated on `prefers-reduced-motion`):
|
||||
* • the arc sweeps from empty on mount via a `linear()` SPRING that
|
||||
* overshoots ~6% then settles (`--ring-spring`); the spring is swapped for
|
||||
* a plain ease-out above score 94 so a high arc never wraps past full;
|
||||
* • `delayMs` staggers the sweep + the count-up so the strip reveals
|
||||
* left-to-right and each number trails its arc;
|
||||
* • a conic SHEEN + the tile rise + glow live in `globals.css`, gated on the
|
||||
* strip's `data-revealed` flag (set once per session) so a background
|
||||
* refetch never re-triggers the moment.
|
||||
* • the quiet tile rise lives in `globals.css`, gated on the strip's
|
||||
* `data-revealed` flag (set once per session) so a background refetch
|
||||
* never re-triggers it.
|
||||
*
|
||||
* The arc/track/ghost are real SVG JSX (`<linearGradient>` is NOT
|
||||
* `dangerouslySetInnerHTML`). The `<svg>` is rotated -90° so the arc starts at
|
||||
@@ -78,10 +84,10 @@ export interface ScoreRingProps {
|
||||
*/
|
||||
delayMs?: number;
|
||||
/**
|
||||
* Flat 2-D treatment: drops the drop-shadow bloom, the breathing pulse, the
|
||||
* conic sheen, and the sweep/count-up entrance. Used on the dashboard hero so
|
||||
* the ring sits as calmly as the surrounding chart + tile surfaces. The
|
||||
* /insights wellness strip + anatomy view keep the full premium reveal.
|
||||
* v1.18.7 — the bloom / sheen / pulse are gone for every ring, so the only
|
||||
* thing `flat` still does is drop the spring-fill sweep + the count-up: the
|
||||
* dashboard-hero ring paints its final value at once, with no entrance
|
||||
* motion. The /insights strip + anatomy view keep the subtle sweep.
|
||||
*/
|
||||
flat?: boolean;
|
||||
/**
|
||||
@@ -121,10 +127,10 @@ export function ScoreRing({
|
||||
const resolvedBand: ScoreBand = band ?? bandForScore(clamped);
|
||||
const hueKey = hue ?? resolvedBand;
|
||||
|
||||
// Single-hue two-stop gradient. The per-metric `hue` leans the colour
|
||||
// (Oura's move); with no `hue` the anatomy view falls back to the band token.
|
||||
// Near-flat single-hue arc. The two stops sit a hair apart for a whisper of
|
||||
// depth; the per-metric `hue` leans the colour, with no `hue` the anatomy
|
||||
// view falls back to the muted band token.
|
||||
const [from, to] = RING_GRADIENT[hueKey];
|
||||
const glow = RING_GLOW[hueKey];
|
||||
|
||||
const reduced = prefersReducedMotion();
|
||||
// `flat` forces the calm static treatment regardless of the `animate` prop:
|
||||
@@ -175,18 +181,15 @@ export function ScoreRing({
|
||||
{
|
||||
width: dims.px,
|
||||
height: dims.px,
|
||||
"--ring-glow": glow,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
{/* `overflow-visible`: the arc's drop-shadow bloom (`.wellness-ring-arc`)
|
||||
paints outside the viewBox — the arc fills the 0 0 100 100 box, so the
|
||||
default `overflow: hidden` on SVG clipped the glow flat at the circle's
|
||||
edge. The -90° rotation is unaffected (the bloom is radially symmetric)
|
||||
and the parent div never clips, so the glow now feathers out fully. */}
|
||||
{/* The -90° rotation starts the arc at 12 o'clock so it sweeps clockwise
|
||||
without per-point trig. The arc now carries no bloom, so the SVG needs
|
||||
no `overflow-visible`. */}
|
||||
<svg
|
||||
viewBox={`0 0 ${VIEW} ${VIEW}`}
|
||||
className="h-full w-full -rotate-90 overflow-visible"
|
||||
className="h-full w-full -rotate-90"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<defs>
|
||||
@@ -227,15 +230,10 @@ export function ScoreRing({
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{/* The progress arc — single-hue gradient, round cap, spring sweep, bloom. */}
|
||||
{/* The progress arc — muted single hue, round cap, subtle spring sweep.
|
||||
No bloom / pulse: `.wellness-ring-arc` is a plain flat stroke. */}
|
||||
<circle
|
||||
className={cn(
|
||||
flat ? "wellness-ring-arc--flat" : "wellness-ring-arc",
|
||||
!flat &&
|
||||
shouldAnimate &&
|
||||
resolvedBand === "green" &&
|
||||
"wellness-ring-arc--pulse",
|
||||
)}
|
||||
className="wellness-ring-arc"
|
||||
cx={CX}
|
||||
cy={CX}
|
||||
r={R}
|
||||
@@ -252,9 +250,6 @@ export function ScoreRing({
|
||||
}}
|
||||
/>
|
||||
</svg>
|
||||
{/* Conic specular sheen — sweeps the arc once on the strip reveal.
|
||||
Omitted entirely in the flat treatment (dashboard hero). */}
|
||||
{flat ? null : <div className="wellness-ring-sheen" aria-hidden="true" />}
|
||||
{/* Centred number as real DOM text — design tokens + tabular-nums. */}
|
||||
<div className="pointer-events-none absolute inset-0 flex flex-col items-center justify-center">
|
||||
<span
|
||||
|
||||
Reference in new issue
Block a user