Touch-target audit: 44px minimum on mobile interactive elements (Slice 9)

Apply the mobile-touch-target CSS class to 40 interactive elements across
12 files. The class applies min-height/min-width:44px only below md
(max-width:767px), satisfying WCAG 2.5.5 / Apple HIG on touch devices.
Desktop behavior is unchanged.

Audit log (before -> after hit-area):
- App.tsx: hamburger/dark-mode/sign-out (32/32/28 -> 44)
- Dashboard.tsx: shortcut open/edit/delete (28 -> 44), enabled switch (18 -> 44)
- Media.tsx: mobile pagination prev/next (28 -> 44)
- FileBrowser.impl.tsx: 'Open Settings' alert button (28 -> 44)
- UsersPage.impl.tsx: compose toolbar bold/italic/link/list (32 -> 44),
  attachment remove button (16 -> 44)
- Settings.tsx: machine switch (18 -> 44), clear/add-machine buttons (28 -> 44),
  reset-db checkboxes x3 (16 -> 44)
- Actions.tsx: 'Add action' button (28 -> 44)
- ServicePage.tsx: service enabled switch (18 -> 44)
- ServicesPage.tsx: service switch/open-link/delete-icon (18/28/32 -> 44)
- ObservabilityPage.tsx: retry + 4 asChild link buttons (28 -> 44)
- WidgetConfigDialog.tsx: 4 icon buttons (32 -> 44), 2 switches (18 -> 44),
  2 add-widget buttons (28 -> 44)
- SessionActivityPanel.tsx: 'Open in Users' button (28 -> 44)

Deliberately skipped: default-size text buttons (32px, borderline), desktop-only
sidebar toggle, DataTable internals (desktop-only below md), Select triggers.
Dashboard anchor pills and HoverEditButton already had the class from Slices 1/2.

No new tests (the class applies via @media which jsdom doesn't honor).
116 tests pass; lint/build green.

Refs openspec/changes/mobile-responsive-parity/ (spec R6, tasks slice 9).
This commit is contained in:
Developer
2026-06-26 14:37:40 +00:00
parent 7808822a55
commit 30f1b6e6db
12 changed files with 45 additions and 24 deletions
@@ -141,7 +141,7 @@ function QueryError({
<AlertTitle>{label} failed</AlertTitle>
<AlertDescription className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<span className="break-words">{error.message}</span>
<Button variant="outline" size="sm" onClick={() => refetch()}>
<Button variant="outline" size="sm" className="mobile-touch-target" onClick={() => refetch()}>
<RefreshCw className="mr-1 h-3 w-3" />
Retry
</Button>
@@ -255,7 +255,7 @@ function GrafanaLinkCard({
<div className="font-medium">{title}</div>
<div className="text-sm text-muted-foreground">{description}</div>
</div>
<Button variant="outline" size="sm" asChild>
<Button variant="outline" size="sm" className="mobile-touch-target" asChild>
<a
href={href}
target="_blank"
@@ -585,7 +585,7 @@ export function ObservabilityPage() {
title="No Node Exporter targets"
description="Enable Node Exporter on an SSH machine in Settings to populate Prometheus scrape targets."
action={
<Button variant="outline" size="sm" asChild>
<Button variant="outline" size="sm" className="mobile-touch-target" asChild>
<Link to="/settings">Open Settings</Link>
</Button>
}
@@ -641,7 +641,7 @@ export function ObservabilityPage() {
title="No Grafana service configured"
description="Add a Grafana service instance to enable deep-links to dashboards and logs."
action={
<Button variant="outline" size="sm" asChild>
<Button variant="outline" size="sm" className="mobile-touch-target" asChild>
<Link to="/services">Open Services</Link>
</Button>
}
@@ -653,7 +653,7 @@ export function ObservabilityPage() {
title="No machine selected"
description="Add monitoring machines in Settings to see Grafana drill-down links."
action={
<Button variant="outline" size="sm" asChild>
<Button variant="outline" size="sm" className="mobile-touch-target" asChild>
<Link to="/settings">Open Settings</Link>
</Button>
}
@@ -168,6 +168,7 @@ export function SessionActivityPanel({
<Button
variant="outline"
size="sm"
className="mobile-touch-target"
onClick={(event) => {
event.stopPropagation();
onSelectSession(session);
@@ -315,6 +315,7 @@ export function WidgetConfigDialog({ open, onClose }: Props) {
<div className="flex items-center gap-2">
<Switch
id="widget-enabled"
className="mobile-touch-target"
checked={draft.enabled}
onCheckedChange={(checked) =>
setDraft({ ...draft, enabled: checked })
@@ -377,7 +378,7 @@ export function WidgetConfigDialog({ open, onClose }: Props) {
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
className="mobile-touch-target h-8 w-8"
disabled={index === 0}
onClick={() => moveInstance(index, -1)}
>
@@ -386,21 +387,22 @@ export function WidgetConfigDialog({ open, onClose }: Props) {
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
className="mobile-touch-target h-8 w-8"
disabled={index === sortedInstances.length - 1}
onClick={() => moveInstance(index, 1)}
>
<ChevronDown className="h-4 w-4" />
</Button>
<Switch
checked={instance.enabled}
className="mobile-touch-target"
checked={instance.enabled}
onCheckedChange={() => toggleEnabled(instance)}
aria-label={`Toggle ${instance.title}`}
/>
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
className="mobile-touch-target h-8 w-8"
onClick={() => startEdit(instance)}
>
<Pencil className="h-4 w-4" />
@@ -408,7 +410,7 @@ export function WidgetConfigDialog({ open, onClose }: Props) {
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-destructive"
className="mobile-touch-target h-8 w-8 text-destructive"
onClick={() => removeInstance(instance)}
>
<Trash2 className="h-4 w-4" />
@@ -428,6 +430,7 @@ export function WidgetConfigDialog({ open, onClose }: Props) {
key={b.kind}
variant="outline"
size="sm"
className="mobile-touch-target"
onClick={() => startAddBuiltIn(b.kind)}
>
<Plus className="mr-1 h-3 w-3" />
@@ -442,6 +445,7 @@ export function WidgetConfigDialog({ open, onClose }: Props) {
key={`${s.id}:${w.kind}`}
variant="outline"
size="sm"
className="mobile-touch-target"
onClick={() => startAddService(s.id, w.kind)}
>
<Plus className="mr-1 h-3 w-3" />