mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 21:32:18 +00:00
A date is now a door to its day (#613). `?day=YYYY-MM-DD` opens the day over whichever page is open: docked beside it from 1280 px, the way the Coach's conversations panel sits, a sheet from the right below that, and a bottom sheet on a phone that opens at a little over half the height and grows to full height from its handle. On /coach it is always a sheet, so there is never more than one docked panel. Back and Escape close it, the previous and next day replace the history entry instead of adding one, Alt with an arrow steps a day, focus moves to the day's heading on open and returns to whatever opened it on close, and a polite region names the day. A malformed or future date is dropped from the URL. The day shows the value it was opened from with its comparison, what ran through the day, the readings against the person's own usual range on a short number line (eight curated, the rest behind "All values"), and what happened in order of the clock, each row linking to its record. Loading, failure, an empty day and areas a grant does not cover each read as such. The footer offers the timeline when that module is on and capture for the day, with the capture forms starting on that date. Doors into it: a click on a daily point of a metric chart (a tap shows the value and the tooltip offers the day on touch screens), the dates in the chart's data table, a quiet row of dots under the axis for days with entries and a ring for notable ones, the lab chart and lab readings, the measurement, mood and intake lists, symptom and illness day headings, episode onset and recovery, document dates, workout dates, the workout detail, the mood and adherence heatmaps, and the days a Coach answer's get_day calls read, shown as chips. The next planned visit carries what happened since the last one, each date opening its day, with a doctor report for that window. Not here, and frozen by a guard: the dashboard's today area, mini charts in tiles, the cycle calendar, model-written text, future dates, the clinician share link and the doctor-report PDF.
134 lines
4.5 KiB
TypeScript
134 lines
4.5 KiB
TypeScript
"use client";
|
|
|
|
import type { ReactElement, ReactNode } from "react";
|
|
|
|
/**
|
|
* Apple-Health-style rich tooltip for chart wrappers (B1a v1.4.16).
|
|
*
|
|
* Recharts' default tooltip is a flat list of `name: value` rows. Apple
|
|
* Health paints something denser and friendlier: rounded card with a
|
|
* faint drop shadow, the date on its own row, the metric value in
|
|
* tabular numerals, and an optional sub-line with delta vs. baseline /
|
|
* average. This wrapper renders that layout from a Recharts
|
|
* `TooltipProps.content` callback.
|
|
*
|
|
* Design constraints (per the v1.4.16 brief):
|
|
* - SSR-safe — no `window` / `document` access. The unit test renders
|
|
* it via `renderToStaticMarkup`.
|
|
* - Dracula tokens canonical. The card surface uses `--card`/`--border`
|
|
* so dark mode + light mode both look right; per-row metric colour
|
|
* comes from the dot fill the chart already declares.
|
|
* - The component is dumb — it accepts a fully-shaped payload and
|
|
* renders. The chart wrappers compute baseline / delta themselves.
|
|
*/
|
|
|
|
export interface RichTooltipRow {
|
|
/** Display name, e.g. "Systolic" or "Mood". */
|
|
name: string;
|
|
/**
|
|
* Already-formatted value string (the chart wrapper applies units +
|
|
* locale formatting). Example: `"128 mmHg"`.
|
|
*/
|
|
value: string;
|
|
/** Stroke colour of the corresponding line — used as a left dot. */
|
|
color: string;
|
|
/**
|
|
* Optional second-line annotation for *this* row, e.g.
|
|
* `"+3 mmHg vs. your normal"`. Painted in muted-foreground at 11px.
|
|
*/
|
|
delta?: string;
|
|
}
|
|
|
|
export interface RichChartTooltipProps {
|
|
/**
|
|
* Recharts passes `active=true` and a non-empty payload only when the
|
|
* cursor is over a data point. Outside a hover the renderer should
|
|
* paint nothing.
|
|
*/
|
|
active?: boolean;
|
|
/** Row label shown at the top of the card — typically a formatted
|
|
* date string (`"Mon, May 5"`). Empty string hides the row. */
|
|
label?: string;
|
|
/** Already-shaped, sorted rows. The tooltip never re-orders. */
|
|
rows: RichTooltipRow[];
|
|
/**
|
|
* v1.42 — one action under the rows, e.g. "View the whole day" on a touch
|
|
* screen, where the tooltip is pinned by a tap and is the way onward. The
|
|
* chart makes the tooltip take pointer events only when it passes one.
|
|
*/
|
|
action?: ReactNode;
|
|
}
|
|
|
|
/**
|
|
* Renders the tooltip card.
|
|
*
|
|
* Style choices that match the Apple Health "Trends" pop-over:
|
|
* - rounded-xl (matches the surrounding chart card)
|
|
* - drop-shadow `shadow-lg` for elevation
|
|
* - tabular-nums on the value cell so digits don't jiggle while
|
|
* hovering across a horizontal series
|
|
* - 11px sub-row (delta vs. baseline) keeps the tooltip compact
|
|
*/
|
|
export function RichChartTooltip({
|
|
active,
|
|
label,
|
|
rows,
|
|
action,
|
|
}: RichChartTooltipProps): ReactElement | null {
|
|
if (!active || rows.length === 0) return null;
|
|
|
|
return (
|
|
<div
|
|
data-slot="rich-chart-tooltip"
|
|
className="bg-card border-border min-w-[140px] rounded-xl border p-3 text-xs shadow-lg"
|
|
style={{
|
|
// The card itself is layered above the chart's own SVG; without
|
|
// an explicit background the gradient bleeds through the
|
|
// semi-transparent border in Safari.
|
|
backgroundColor: "var(--card)",
|
|
boxShadow:
|
|
"0 6px 20px rgba(0, 0, 0, 0.32), 0 1px 2px rgba(0, 0, 0, 0.16)",
|
|
}}
|
|
>
|
|
{label ? (
|
|
<div
|
|
className="text-muted-foreground mb-1.5 text-[11px] tracking-wide uppercase"
|
|
data-slot="rich-chart-tooltip-label"
|
|
>
|
|
{label}
|
|
</div>
|
|
) : null}
|
|
<div className="flex flex-col gap-1">
|
|
{rows.map((row) => (
|
|
<div
|
|
key={row.name}
|
|
className="flex flex-col"
|
|
data-slot="rich-chart-tooltip-row"
|
|
>
|
|
<div className="flex items-center justify-between gap-3">
|
|
<div className="flex items-center gap-1.5">
|
|
<span
|
|
aria-hidden="true"
|
|
className="inline-block h-2 w-2 rounded-full"
|
|
style={{ backgroundColor: row.color }}
|
|
/>
|
|
<span className="text-foreground/80">{row.name}</span>
|
|
</div>
|
|
<span className="text-foreground tabular-nums">{row.value}</span>
|
|
</div>
|
|
{row.delta ? (
|
|
<span
|
|
className="text-muted-foreground mt-0.5 ml-3.5 text-[11px]"
|
|
data-slot="rich-chart-tooltip-delta"
|
|
>
|
|
{row.delta}
|
|
</span>
|
|
) : null}
|
|
</div>
|
|
))}
|
|
</div>
|
|
{action ?? null}
|
|
</div>
|
|
);
|
|
}
|