fix(ui): close consistency-audit contrast and duplication findings

Today hero's top-signal delta used opacity-on-muted text, landing below
AA on the app's most prominent card; five mood charts and the sleep-
stage IN_BED fill painted raw --dracula-*/hardcoded-hex hues with no
(or an unused) light-theme counterpart. Route all of them through
existing semantic/chart tokens instead (destructive/warning/success,
chart-1..5, and a newly minted --chart-inbed with its own Alucard
override) so every hue is AA-tuned per theme.

Also collapses three duplicated primitives the audit flagged: the
click-Popover VitalsInfoTip and hover-Tooltip InfoHint merge into one
touch-first ui/info-popover; MedicationViewToggle and ModuleViewToggle
were verbatim copy-paste and now wrap one shared ui/view-toggle; the
triplicated sport-icon map (already drifted — mixedCardio rendered a
different glyph in the list vs the detail/dashboard views) moves to
lib/workouts/sport-icons.ts with HeartPulse as the settled icon.

Adds a no-restricted-imports lint rule banning direct recharts imports
outside the chart-runtime barrel, catching the bundle-duplication
mistake at local lint instead of PR-CI.
This commit is contained in:
Marc-André Bombeck committed 2026-07-18 01:23:37 +02:00
1 parent 1ff179178f
commit 247bc4f33c
25 files changed
+401 -400

No files matched your search

+77
View File
@@ -0,0 +1,77 @@
"use client";
import type { LucideIcon } from "lucide-react";
import { cn } from "@/lib/utils";
export interface ViewToggleSegment<T extends string> {
value: T;
label: string;
icon: LucideIcon;
}
export interface ViewToggleProps<T extends string> {
view: T;
onChange: (view: T) => void;
segments: ViewToggleSegment<T>[];
/** Accessible name for the segmented-control group. */
groupLabel: string;
/** `data-slot` prefix per segment, rendered as `${dataSlotPrefix}-${value}`. */
dataSlotPrefix: string;
}
/**
* Shared icon-only segmented view toggle (e.g. cards vs table/list). A small
* two-segment control: the active segment carries the filled surface, the
* inactive one stays ghost. `aria-pressed` announces the state per segment;
* the group is named for screen readers. Segments keep the 44-px mobile tap
* floor (`size-11` via `size-10` + the `sm:size-8` desktop step), shrinking
* to the standard 36-px control height on desktop like a neighbouring Add
* button.
*
* Previously forked between the medications module
* (`medications/medication-view-toggle.tsx`) and the Vorsorge/Illness/Labs
* settings pages (`module-list/module-view-toggle.tsx`) — verbatim
* copy-paste that only diverged in icons + i18n keys and had already
* drifted. Both are now thin wrappers over this one primitive.
*/
export function ViewToggle<T extends string>({
view,
onChange,
segments,
groupLabel,
dataSlotPrefix,
}: ViewToggleProps<T>) {
return (
<div
role="group"
aria-label={groupLabel}
className="border-border bg-background/30 inline-flex shrink-0 items-center rounded-md border p-0.5"
>
{segments.map(({ value, label, icon: Icon }) => {
const active = view === value;
return (
<button
key={value}
type="button"
onClick={() => {
if (!active) onChange(value);
}}
aria-pressed={active}
aria-label={label}
title={label}
data-slot={`${dataSlotPrefix}-${value}`}
className={cn(
"focus-visible:ring-ring inline-flex size-10 items-center justify-center rounded-[5px] transition-colors focus-visible:ring-2 focus-visible:outline-none motion-reduce:transition-none sm:size-8",
active
? "bg-secondary text-secondary-foreground shadow-sm"
: "text-muted-foreground hover:text-foreground",
)}
>
<Icon className="size-4" aria-hidden="true" />
</button>
);
})}
</div>
);
}