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:
@@ -194,7 +194,7 @@ function MobileDrawer() {
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={setOpen}>
|
||||
<SheetTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="md:hidden">
|
||||
<Button variant="ghost" size="icon" className="mobile-touch-target md:hidden">
|
||||
<Menu className="h-5 w-5" />
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
@@ -281,7 +281,7 @@ function TopBar({
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={onToggleDarkMode}
|
||||
className="h-8 w-8"
|
||||
className="mobile-touch-target h-8 w-8"
|
||||
>
|
||||
{darkMode ? (
|
||||
<Sun className="h-4 w-4" />
|
||||
@@ -294,7 +294,7 @@ function TopBar({
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={onSignOut}
|
||||
className="gap-2"
|
||||
className="mobile-touch-target gap-2"
|
||||
>
|
||||
<LogOut className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">Logout</span>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -367,7 +367,7 @@ export function Actions() {
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full"
|
||||
className="mobile-touch-target w-full"
|
||||
onClick={createNew}
|
||||
>
|
||||
Add action
|
||||
|
||||
@@ -354,6 +354,7 @@ function ShortcutDialog({
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch
|
||||
id="shortcut-enabled"
|
||||
className="mobile-touch-target"
|
||||
checked={draft.enabled}
|
||||
onCheckedChange={(checked) =>
|
||||
onChange({ ...draft, enabled: checked })
|
||||
@@ -424,13 +425,14 @@ function ShortcutCard({
|
||||
size="sm"
|
||||
disabled={!shortcut.enabled || !href}
|
||||
onClick={onOpen}
|
||||
className="mobile-touch-target"
|
||||
>
|
||||
Open
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" onClick={onEdit}>
|
||||
<Button size="sm" variant="outline" onClick={onEdit} className="mobile-touch-target">
|
||||
Edit
|
||||
</Button>
|
||||
<Button size="sm" variant="destructive" onClick={onDelete}>
|
||||
<Button size="sm" variant="destructive" onClick={onDelete} className="mobile-touch-target">
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -879,6 +879,7 @@ export function FileBrowser() {
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => navigateToSettings("/settings")}
|
||||
className="mobile-touch-target"
|
||||
>
|
||||
Open Settings
|
||||
</Button>
|
||||
|
||||
@@ -167,6 +167,7 @@ function MediaMobilePagination({
|
||||
}
|
||||
disabled={pageIndex <= 0}
|
||||
aria-label="Previous page"
|
||||
className="mobile-touch-target"
|
||||
>
|
||||
Previous
|
||||
</Button>
|
||||
@@ -180,6 +181,7 @@ function MediaMobilePagination({
|
||||
}))
|
||||
}
|
||||
disabled={pageIndex >= pageCount - 1}
|
||||
className="mobile-touch-target"
|
||||
aria-label="Next page"
|
||||
>
|
||||
Next
|
||||
|
||||
@@ -238,6 +238,7 @@ export function ServicePage() {
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch
|
||||
id="service-enabled"
|
||||
className="mobile-touch-target"
|
||||
checked={enabled}
|
||||
onCheckedChange={setEnabled}
|
||||
/>
|
||||
|
||||
@@ -234,6 +234,7 @@ function CreateServiceDialog({
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch
|
||||
id="service-enabled"
|
||||
className="mobile-touch-target"
|
||||
checked={draft.enabled}
|
||||
onCheckedChange={(checked) =>
|
||||
setDraft({ ...draft, enabled: checked })
|
||||
@@ -328,6 +329,7 @@ export function ServicesPage() {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="mobile-touch-target"
|
||||
onClick={() =>
|
||||
navigate(`/services/${s.service_type}/${s.id}`)
|
||||
}
|
||||
@@ -337,7 +339,7 @@ export function ServicesPage() {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-destructive"
|
||||
className="mobile-touch-target h-8 w-8 text-destructive"
|
||||
onClick={() => setDeleteId(s.id)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
|
||||
@@ -232,6 +232,7 @@ function MachineEditor({
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch
|
||||
id="machine-enabled"
|
||||
className="mobile-touch-target"
|
||||
checked={draft.enabled}
|
||||
onCheckedChange={(checked) =>
|
||||
setDraft((current) => ({ ...current, enabled: checked }))
|
||||
@@ -534,7 +535,7 @@ function SSHKeyManager({
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full"
|
||||
className="mobile-touch-target w-full"
|
||||
onClick={() => {
|
||||
clear();
|
||||
}}
|
||||
@@ -782,6 +783,7 @@ function ResetLocalDatabaseCard() {
|
||||
<div className="flex flex-col gap-3">
|
||||
<label className="flex items-center gap-2 text-sm">
|
||||
<Checkbox
|
||||
className="mobile-touch-target"
|
||||
checked={ackSettings}
|
||||
onCheckedChange={(checked) => setAckSettings(Boolean(checked))}
|
||||
/>
|
||||
@@ -789,6 +791,7 @@ function ResetLocalDatabaseCard() {
|
||||
</label>
|
||||
<label className="flex items-center gap-2 text-sm">
|
||||
<Checkbox
|
||||
className="mobile-touch-target"
|
||||
checked={ackIndex}
|
||||
onCheckedChange={(checked) => setAckIndex(Boolean(checked))}
|
||||
/>
|
||||
@@ -796,6 +799,7 @@ function ResetLocalDatabaseCard() {
|
||||
</label>
|
||||
<label className="flex items-center gap-2 text-sm">
|
||||
<Checkbox
|
||||
className="mobile-touch-target"
|
||||
checked={ackIrreversible}
|
||||
onCheckedChange={(checked) =>
|
||||
setAckIrreversible(Boolean(checked))
|
||||
@@ -973,7 +977,7 @@ export function Settings() {
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full"
|
||||
className="mobile-touch-target w-full"
|
||||
onClick={() => {
|
||||
clearSSHValidation();
|
||||
setMachineDraft(emptyMachine("local"));
|
||||
|
||||
@@ -891,9 +891,10 @@ export function UsersPage() {
|
||||
<div className="flex flex-wrap gap-1">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<UiButton
|
||||
<UiButton
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="mobile-touch-target"
|
||||
onClick={() => insertMarkup("<strong>", "</strong>")}
|
||||
aria-label="Bold"
|
||||
>
|
||||
@@ -904,9 +905,10 @@ export function UsersPage() {
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<UiButton
|
||||
<UiButton
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="mobile-touch-target"
|
||||
onClick={() => insertMarkup("<em>", "</em>")}
|
||||
aria-label="Italic"
|
||||
>
|
||||
@@ -917,9 +919,10 @@ export function UsersPage() {
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<UiButton
|
||||
<UiButton
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="mobile-touch-target"
|
||||
onClick={addLink}
|
||||
aria-label="Link"
|
||||
>
|
||||
@@ -930,9 +933,10 @@ export function UsersPage() {
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<UiButton
|
||||
<UiButton
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="mobile-touch-target"
|
||||
onClick={() => insertMarkup("<ul><li>", "</li></ul>")}
|
||||
aria-label="Bullet list"
|
||||
>
|
||||
@@ -994,7 +998,7 @@ export function UsersPage() {
|
||||
type="button"
|
||||
aria-label={`Remove ${file.name}`}
|
||||
onClick={() => removeAttachment(index)}
|
||||
className="inline-flex items-center text-current [&>svg]:size-3"
|
||||
className="mobile-touch-target inline-flex items-center text-current [&>svg]:size-3"
|
||||
>
|
||||
<Trash2 />
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user