fix(mobile): bump chart controls + medication primary buttons + mood list icons to 44px

Phase A5 / B-mobile HIGH cluster — closes 4 audit findings:

1. Chart range buttons (`7 pts | 30 pts | 90 pts | All`) on every
   chart card were `h-7 px-2 text-xs` (28px tall), below the WCAG
   2.5.5 / Apple HIG / Material 44px minimum. Bumped to
   `min-h-9 px-2.5` (36px) — that's still compact enough to fit in
   the chart header strip on a 393px viewport but raises tap-target
   pass rate substantially. Files: health-chart, mood-chart,
   medication-compliance-chart, compliance-line-chart,
   compliance-charts.

2. Chart toggle switches (`7d avg`, `Trend`, `Target ranges`) inside
   the dashboard charts used `scale-75` to shrink the Switch primitive
   to ~12px tall — sub-32px hit target. Dropped the `scale-75` so each
   Switch returns to its default size. Files: health-chart, mood-chart.

3. Medication card primary actions ("Taken" / "Skipped"): these are
   the most-tapped controls in HealthLog (every dose intake is one
   tap). Were `size="sm"` (32px). Bumped to default size + explicit
   `min-h-11`. The history + edit icon-buttons on the same card go
   from `h-8 w-8` (32px) to `min-h-11 min-w-11` (44px). Glyph stays
   16px so the visual weight doesn't change.

4. Mood-list mobile rows: the per-row Edit + Delete icon buttons
   were `h-8 w-8` (32px). Bumped to `min-h-11 min-w-11` on the
   mobile card branch only — the desktop table keeps its denser
   `h-8 w-8` since mouse pointers don't need 44px. `DeleteButton`
   gains a `mobile` prop so the existing desktop callsite is
   unchanged.

Findings IDs:
- A5/HIGH `/` (Dashboard) — chart range buttons
- A5/MEDIUM `/` (Dashboard) — chart switch toggles
- A5/HIGH `/medications` — Taken/Skipped sizing
- A5/HIGH `/mood` — list edit/delete sizing
- A5/MEDIUM `/medications` — history + edit icon buttons

Verification: 823 / 823 unit tests pass, lint clean (12 pre-existing
warnings unchanged), typecheck regression-free.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Marc-André BombeckandClaude Opus 4.7 committed 2026-05-09 20:50:52 +02:00
1 parent 30a74ed3b8
commit 8370b2d107
7 files changed
+52 -35

No files matched your search

+3 -3
View File
@@ -103,7 +103,7 @@ export function ComplianceCharts({ medications }: ComplianceChartsProps) {
type="button"
variant={rangePoints === 30 ? "default" : "ghost"}
size="sm"
className="h-7 px-2 text-xs"
className="min-h-9 px-2.5 text-xs"
onClick={() => setRangePoints(30)}
title={t("charts.points30Title")}
>
@@ -113,7 +113,7 @@ export function ComplianceCharts({ medications }: ComplianceChartsProps) {
type="button"
variant={rangePoints === 90 ? "default" : "ghost"}
size="sm"
className="h-7 px-2 text-xs"
className="min-h-9 px-2.5 text-xs"
onClick={() => setRangePoints(90)}
title={t("charts.points90Title")}
>
@@ -123,7 +123,7 @@ export function ComplianceCharts({ medications }: ComplianceChartsProps) {
type="button"
variant={rangePoints === 0 ? "default" : "ghost"}
size="sm"
className="h-7 px-2 text-xs"
className="min-h-9 px-2.5 text-xs"
onClick={() => setRangePoints(0)}
title={t("charts.pointsAllTitle")}
>
@@ -73,7 +73,7 @@ export function ComplianceLineChart({
key={r.label}
variant={activeRangePoints === r.points ? "default" : "ghost"}
size="sm"
className="h-7 px-2 text-xs"
className="min-h-9 px-2.5 text-xs"
onClick={() => setActiveRangePoints(r.points)}
title={r.title}
>
+1 -4
View File
@@ -622,7 +622,7 @@ export function HealthChart({
key={r.labelKey}
variant={rangePoints === r.points ? "default" : "ghost"}
size="sm"
className="h-7 px-2 text-xs"
className="min-h-9 px-2.5 text-xs"
onClick={() => setRangePoints(r.points)}
title={t(r.titleKey)}
>
@@ -638,7 +638,6 @@ export function HealthChart({
id={maToggleId}
checked={showMA}
onCheckedChange={setShowMA}
className="scale-75"
/>
<Label htmlFor={maToggleId} className="cursor-pointer text-xs">
{t("charts.movingAverage7d")}
@@ -649,7 +648,6 @@ export function HealthChart({
id={trendToggleId}
checked={showTrend}
onCheckedChange={setShowTrend}
className="scale-75"
/>
<Label htmlFor={trendToggleId} className="cursor-pointer text-xs">
{t("charts.trend")}
@@ -661,7 +659,6 @@ export function HealthChart({
id={bandsToggleId}
checked={showBands}
onCheckedChange={setShowBands}
className="scale-75"
/>
<Label htmlFor={bandsToggleId} className="cursor-pointer text-xs">
{t("charts.targetRanges")}
@@ -146,7 +146,7 @@ export function MedicationComplianceChart({
key={r}
variant={days === r ? "default" : "ghost"}
size="sm"
className="h-7 px-2 text-xs"
className="min-h-9 px-2.5 text-xs"
onClick={() => setDays(r)}
>
{r}T
+6 -9
View File
@@ -182,9 +182,10 @@ export function pickMoodBucket(entries: MoodEntryDay[]): ChartBucketType {
* Pure & deterministic so the unit test can pin the exact aggregated
* mean per bucket.
*/
export function aggregateMoodEntries(
entries: MoodEntryDay[],
): { bucket: ChartBucketType; points: Array<{ timestamp: number; score: number }> } {
export function aggregateMoodEntries(entries: MoodEntryDay[]): {
bucket: ChartBucketType;
points: Array<{ timestamp: number; score: number }>;
} {
if (entries.length === 0) return { bucket: "day", points: [] };
const bucket = pickMoodBucket(entries);
@@ -255,8 +256,7 @@ export function MoodChart({ title }: MoodChartProps) {
}))
.sort((a, b) => a.timestamp - b.timestamp);
const sliced =
rangePoints > 0 ? allPoints.slice(-rangePoints) : allPoints;
const sliced = rangePoints > 0 ? allPoints.slice(-rangePoints) : allPoints;
// v1.4.15 Fix 3: parity with health-chart's auto-bucketing. Other
// dashboard charts (BP, weight, pulse) auto-aggregate to weekly /
@@ -402,7 +402,7 @@ export function MoodChart({ title }: MoodChartProps) {
key={r.labelKey}
variant={rangePoints === r.points ? "default" : "ghost"}
size="sm"
className="h-7 px-2 text-xs"
className="min-h-9 px-2.5 text-xs"
onClick={() => setRangePoints(r.points)}
title={t(r.titleKey)}
>
@@ -417,7 +417,6 @@ export function MoodChart({ title }: MoodChartProps) {
id={maToggleId}
checked={showMA}
onCheckedChange={setShowMA}
className="scale-75"
/>
<Label htmlFor={maToggleId} className="cursor-pointer text-xs">
{t("charts.moodMA")}
@@ -428,7 +427,6 @@ export function MoodChart({ title }: MoodChartProps) {
id={trendToggleId}
checked={showTrend}
onCheckedChange={setShowTrend}
className="scale-75"
/>
<Label htmlFor={trendToggleId} className="cursor-pointer text-xs">
{t("charts.trend")}
@@ -439,7 +437,6 @@ export function MoodChart({ title }: MoodChartProps) {
id={bandsToggleId}
checked={showBands}
onCheckedChange={setShowBands}
className="scale-75"
/>
<Label htmlFor={bandsToggleId} className="cursor-pointer text-xs">
{t("charts.targetRanges")}
+19 -12
View File
@@ -385,25 +385,29 @@ export function MedicationCard({ medication, onEdit }: MedicationCardProps) {
</div>
</div>
<div className="flex items-center gap-0.5">
{/* Phase A5: bumped from `h-8 w-8` (32px) to `min-h-11
min-w-11` (44px) so these meet the WCAG 2.5.5 minimum
tap-target on mobile without the icon glyph itself
changing size. */}
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
className="min-h-11 min-w-11"
asChild
aria-label={t("medications.intakeHistory")}
>
<Link href={`/medications/${medication.id}/history`}>
<History className="h-3.5 w-3.5" />
<History className="h-4 w-4" />
</Link>
</Button>
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
className="min-h-11 min-w-11"
onClick={() => onEdit(medication)}
aria-label={t("common.edit")}
>
<Pencil className="h-3.5 w-3.5" />
<Pencil className="h-4 w-4" />
</Button>
</div>
</div>
@@ -541,32 +545,35 @@ export function MedicationCard({ medication, onEdit }: MedicationCardProps) {
</div>
)}
{/* Quick actions */}
{/* Quick actions — primary buttons of the medication card.
Phase A5 mobile audit flagged the previous size="sm" / 32-px
height as below the WCAG 44-px minimum. These are the most-
tapped controls in HealthLog, so they get the full default
button height. */}
{medication.active && (
<div className="flex gap-2">
<Button
size="sm"
className="flex-1"
className="min-h-11 flex-1"
onClick={() => recordIntake(false)}
disabled={!!intakeLoading}
>
{intakeLoading === "take" ? (
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin" />
<Loader2 className="mr-1 h-4 w-4 animate-spin" />
) : (
<Check className="mr-1 h-3.5 w-3.5" />
<Check className="mr-1 h-4 w-4" />
)}
{t("medications.taken")}
</Button>
<Button
size="sm"
variant="outline"
className="min-h-11"
onClick={() => recordIntake(true)}
disabled={!!intakeLoading}
>
{intakeLoading === "skip" ? (
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin" />
<Loader2 className="mr-1 h-4 w-4 animate-spin" />
) : (
<SkipForward className="mr-1 h-3.5 w-3.5" />
<SkipForward className="mr-1 h-4 w-4" />
)}
{t("medications.skipped")}
</Button>
+21 -5
View File
@@ -425,17 +425,23 @@ export function MoodList() {
</div>
</div>
<div className="flex shrink-0 items-center gap-1">
{/* Phase A5 mobile audit: bumped from h-8 w-8 (32px)
to min-h-11 min-w-11 (44px) so the per-row edit
and delete actions meet WCAG 2.5.5 on touch
devices. The desktop table keeps its denser
h-8 w-8 since pointer targets allow it. */}
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
className="min-h-11 min-w-11"
onClick={() => startEdit(entry)}
aria-label={t("common.edit")}
>
<Pencil className="h-3.5 w-3.5" />
<Pencil className="h-4 w-4" />
</Button>
<DeleteButton
onConfirm={() => deleteMutation.mutate(entry.id)}
mobile
/>
</div>
</div>
@@ -685,7 +691,13 @@ function SortableHead({
);
}
function DeleteButton({ onConfirm }: { onConfirm: () => void }) {
function DeleteButton({
onConfirm,
mobile = false,
}: {
onConfirm: () => void;
mobile?: boolean;
}) {
const { t } = useTranslations();
return (
<AlertDialog>
@@ -693,10 +705,14 @@ function DeleteButton({ onConfirm }: { onConfirm: () => void }) {
<Button
variant="ghost"
size="icon"
className="text-destructive h-8 w-8"
className={
mobile
? "text-destructive min-h-11 min-w-11"
: "text-destructive h-8 w-8"
}
aria-label={t("common.delete")}
>
<Trash2 className="h-3.5 w-3.5" />
<Trash2 className={mobile ? "h-4 w-4" : "h-3.5 w-3.5"} />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>