mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 22:02:16 +00:00
The muted-foreground token is AA-tuned in both themes; the /70 and /80 opacity modifiers silently undid that (2.9-4.2:1 measured). Every informational text node drops the modifier for the full token: sleep hypnogram captions, learning-gate caveat, integration-card setup hints, module lock notes, glucose panel and token-footer meta, invite-token hint, wizard hint, notification channel labels and disabled notes (the disabled state reads through the note and the disabled switches, not through dimming), plus the resting state of the timestamp, suggestion chip and sidebar-collapse controls. User-authored mood notes move to foreground text-sm — user data is never muted. Decorative non-text strokes (sparkline grid, calendar dot, titration connector) keep their opacity; icons keep the /60 graphics floor.
94 lines
2.8 KiB
TypeScript
94 lines
2.8 KiB
TypeScript
import * as React from "react";
|
|
|
|
import { cn } from "@/lib/utils";
|
|
|
|
/**
|
|
* v1.17.1 — the one "we're still learning your X" gate.
|
|
*
|
|
* Before this primitive, glucose, sleep-debt, chronotype, the trajectory
|
|
* forecast, and the composite-score anatomy each hand-rolled the warm
|
|
* "not enough data yet" paragraph — five copies with different markers,
|
|
* different `data-slot` values, and no shared a11y contract. The calm
|
|
* voice held only because each author copied the tone by hand, which is
|
|
* exactly the drift that bites the next metric author.
|
|
*
|
|
* `LearningGate` owns the *presentation and behaviour* of a learning
|
|
* state — the muted body paragraph, an optional secondary caveat line,
|
|
* the `data-state="learning"` marker, and the polite live region — while
|
|
* the caller passes the metric-specific localized copy and keeps its own
|
|
* card title / icon chrome. One look, one behaviour, copy still per-metric.
|
|
*
|
|
* Variants:
|
|
* - `"inline"` (default): a bare muted paragraph for use INSIDE a card
|
|
* that already has its own header (glucose / sleep / trajectory).
|
|
* - `"bordered"`: a dashed-border centered note for STANDALONE use where
|
|
* there is no surrounding card chrome (score anatomy).
|
|
*/
|
|
export interface LearningGateProps extends Omit<
|
|
React.HTMLAttributes<HTMLDivElement>,
|
|
"title"
|
|
> {
|
|
/** Localized primary "still learning" sentence. */
|
|
message: React.ReactNode;
|
|
/** Optional secondary line — a caveat or a "N more days" nudge. */
|
|
caveat?: React.ReactNode;
|
|
variant?: "inline" | "bordered";
|
|
/** Compact density for tiles / narrow rails. */
|
|
compact?: boolean;
|
|
/**
|
|
* Optional override for the body `data-slot`, so existing per-surface
|
|
* test selectors (e.g. `glucose-learning-body`,
|
|
* `trajectory-insufficient`, `score-anatomy-insufficient`) keep
|
|
* resolving after the repoint.
|
|
*/
|
|
bodySlot?: string;
|
|
}
|
|
|
|
export function LearningGate({
|
|
message,
|
|
caveat,
|
|
variant = "inline",
|
|
compact = false,
|
|
bodySlot,
|
|
className,
|
|
...props
|
|
}: LearningGateProps) {
|
|
const body = (
|
|
<p
|
|
data-slot={bodySlot ?? "learning-gate-body"}
|
|
className={cn(
|
|
"text-muted-foreground",
|
|
compact ? "text-xs leading-snug" : "text-sm",
|
|
variant === "bordered" && "text-center text-xs",
|
|
)}
|
|
>
|
|
{message}
|
|
</p>
|
|
);
|
|
|
|
return (
|
|
<div
|
|
data-slot="learning-gate"
|
|
data-state="learning"
|
|
role="status"
|
|
aria-live="polite"
|
|
className={cn(
|
|
"space-y-2",
|
|
variant === "bordered" && "rounded-lg border border-dashed px-4 py-6",
|
|
className,
|
|
)}
|
|
{...props}
|
|
>
|
|
{body}
|
|
{caveat ? (
|
|
<p
|
|
data-slot="learning-gate-caveat"
|
|
className="text-muted-foreground text-xs leading-snug"
|
|
>
|
|
{caveat}
|
|
</p>
|
|
) : null}
|
|
</div>
|
|
);
|
|
}
|