mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 05:42:19 +00:00
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:
1 parent
30a74ed3b8
commit
8370b2d107
7 files changed
+52
-35
No files matched your search
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user