Touch-target pass: 44px min on default-size buttons (WCAG 2.5.5)

Applies .mobile-touch-target to 32 default-size <Button> elements (32px
tall, below the mobile minimum) across 9 files for strict WCAG 2.5.5
compliance: Save, Cancel, Delete, Validate SSH, Run job, Build index,
Update connection, Add service, etc. Plus the shared DialogFooter Cancel
+ Confirm buttons (used by every ConfirmDialog).

The class applies min-height/min-width: 44px only below md
(max-width: 767px); no-op at md+, so desktop sizing is unchanged.

Completes the touch-target audit started in Slice 9 (which covered icon
buttons, size=sm buttons, checkboxes, switches). 122 tests pass; lint/
build green. No new tests (@media queries aren't honored by jsdom).

Refs openspec/changes/mobile-responsive-parity/verify-report.md residual
risk #2.
This commit is contained in:
Developer
2026-06-26 15:45:34 +00:00
parent 09b9c45665
commit d05de0aacd
9 changed files with 78 additions and 25 deletions
+6 -1
View File
@@ -50,7 +50,11 @@ export function DialogFooter({
}: DialogFooterProps) {
return (
<div className="flex flex-row flex-wrap items-center justify-end gap-2">
<Button variant="ghost" onClick={onCancel}>
<Button
variant="ghost"
className="mobile-touch-target"
onClick={onCancel}
>
{cancelLabel}
</Button>
{secondaryAction ? (
@@ -59,6 +63,7 @@ export function DialogFooter({
</div>
) : null}
<Button
className="mobile-touch-target"
variant={resolveConfirmVariant(confirmColor, confirmVariant)}
disabled={confirmDisabled}
onClick={onConfirm}
@@ -332,10 +332,10 @@ export function WidgetConfigDialog({ open, onClose }: Props) {
/>
{!isMobile ? (
<div className="flex justify-end gap-2">
<Button variant="outline" onClick={reset}>
<Button variant="outline" onClick={reset} className="mobile-touch-target">
Back
</Button>
<Button onClick={saveDraft} disabled={saveWidget.isPending}>
<Button onClick={saveDraft} disabled={saveWidget.isPending} className="mobile-touch-target">
Save widget
</Button>
</div>
+4 -4
View File
@@ -266,7 +266,7 @@ function TaskDialog({
confirmBusyLabel="Save action"
secondaryAction={
onDelete ? (
<Button variant="destructive" onClick={onDelete}>
<Button variant="destructive" onClick={onDelete} className="mobile-touch-target">
Delete
</Button>
) : undefined
@@ -412,13 +412,13 @@ export function Actions() {
description="Open the editor popup to modify this action."
action={
<div className="flex flex-row flex-wrap items-center gap-2">
<Button
<Button className="mobile-touch-target"
variant="outline"
onClick={() => openEdit(initialFromTask(editingTask))}
>
Edit
</Button>
<Button
<Button className="mobile-touch-target"
disabled={runTask.isPending || !runServiceId}
onClick={async () => {
await runTask.mutateAsync({
@@ -515,7 +515,7 @@ export function Actions() {
description="Select a saved action from the list on the left to view its details, run it, or open the editor popup. Use the button at the bottom to add a new action."
>
{tasks[0] && (
<Button variant="outline" onClick={() => setTab(tasks[0].id)}>
<Button variant="outline" onClick={() => setTab(tasks[0].id)} className="mobile-touch-target">
Select first action
</Button>
)}
+22 -4
View File
@@ -429,10 +429,20 @@ function ShortcutCard({
>
Open
</Button>
<Button size="sm" variant="outline" onClick={onEdit} className="mobile-touch-target">
<Button
size="sm"
variant="outline"
onClick={onEdit}
className="mobile-touch-target"
>
Edit
</Button>
<Button size="sm" variant="destructive" onClick={onDelete} className="mobile-touch-target">
<Button
size="sm"
variant="destructive"
onClick={onDelete}
className="mobile-touch-target"
>
Delete
</Button>
</div>
@@ -503,10 +513,18 @@ export function Dashboard() {
description="Quick links to websites today, with room for action and user shortcuts later."
action={
<div className="flex gap-2">
<Button variant="outline" onClick={() => setWidgetDialogOpen(true)}>
<Button
variant="outline"
className="mobile-touch-target"
onClick={() => setWidgetDialogOpen(true)}
>
Edit dashboard
</Button>
<Button variant="outline" onClick={openCreateShortcut}>
<Button
variant="outline"
className="mobile-touch-target"
onClick={openCreateShortcut}
>
Add shortcut
</Button>
</div>
+3 -3
View File
@@ -718,14 +718,14 @@ export function FileBrowser() {
<div className="flex flex-col gap-2 md:flex-row md:items-end">
<Button
variant="outline"
className="w-full md:w-auto"
className="w-full md:w-auto mobile-touch-target"
onClick={() => navigate(pathInput || "/")}
>
Open
</Button>
<Button
variant="outline"
className="w-full md:w-auto"
className="w-full md:w-auto mobile-touch-target"
onClick={() => refetch()}
>
Refresh
@@ -837,7 +837,7 @@ export function FileBrowser() {
</Select>
</div>
<div className="flex flex-col gap-2 md:col-span-3 md:flex-row md:items-end">
<Button
<Button className="mobile-touch-target"
disabled={!selectedJob || runJob.isPending}
onClick={() =>
runJob.mutate({
+3 -3
View File
@@ -485,7 +485,7 @@ export function Media() {
</p>
)}
<Button
<Button className="mobile-touch-target"
variant="outline"
onClick={() => buildIndex.mutate()}
disabled={
@@ -496,7 +496,7 @@ export function Media() {
</Button>
{buildRunning && (
<>
<Button
<Button className="mobile-touch-target"
variant="destructive"
onClick={() => stopBuildIndex.mutate()}
disabled={stopBuildIndex.isPending || buildCancelRequested}
@@ -507,7 +507,7 @@ export function Media() {
</Button>
<Button
variant="outline"
className="border-chart-3/40 text-chart-3 hover:bg-chart-3/10"
className="border-chart-3/40 text-chart-3 hover:bg-chart-3/10 mobile-touch-target"
onClick={() => forceStopBuildIndex.mutate()}
disabled={forceStopBuildIndex.isPending}
>
+18 -4
View File
@@ -212,7 +212,11 @@ export function ServicePage() {
<Label htmlFor="service-enabled">Enabled</Label>
</div>
{configFields}
<Button variant="destructive" onClick={() => setDeleteOpen(true)}>
<Button
className="mobile-touch-target"
variant="destructive"
onClick={() => setDeleteOpen(true)}
>
Delete service
</Button>
{widgetsCard}
@@ -252,10 +256,18 @@ export function ServicePage() {
<Label htmlFor="service-enabled">Enabled</Label>
</div>
<div className="flex justify-between">
<Button onClick={save} disabled={saveService.isPending}>
<Button
className="mobile-touch-target"
onClick={save}
disabled={saveService.isPending}
>
Save
</Button>
<Button variant="destructive" onClick={() => setDeleteOpen(true)}>
<Button
className="mobile-touch-target"
variant="destructive"
onClick={() => setDeleteOpen(true)}
>
Delete
</Button>
</div>
@@ -392,7 +404,9 @@ function ServiceConnectionFields({
</div>
)}
<Button onClick={handleUpdateConnection}>Update connection</Button>
<Button className="mobile-touch-target" onClick={handleUpdateConnection}>
Update connection
</Button>
</div>
);
+2 -2
View File
@@ -195,7 +195,7 @@ function CreateServiceDialog({
{!draft ? (
<div className="flex flex-col gap-2">
{types.map((t) => (
<Button
<Button className="mobile-touch-target"
key={t.service_type}
variant="outline"
onClick={() => setDraft(emptyDraft(t.service_type))}
@@ -287,7 +287,7 @@ export function ServicesPage() {
title="Services"
description="External services the app talks to. Configure URLs and API keys here; they are encrypted at rest."
action={
<Button variant="outline" onClick={() => setCreateOpen(true)}>
<Button variant="outline" onClick={() => setCreateOpen(true)} className="mobile-touch-target">
<Plus className="mr-1 h-3 w-3" />
Add service
</Button>
+18 -2
View File
@@ -466,6 +466,7 @@ function MachineEditor({
</Alert>
<div className="flex flex-row flex-wrap items-center gap-2">
<Button
className="mobile-touch-target"
variant="outline"
onClick={onValidateSSH}
disabled={
@@ -678,6 +679,7 @@ function SSHKeyManager({
</div>
<div className="flex flex-row flex-wrap items-center gap-2">
<Button
className="mobile-touch-target"
disabled={saveKey.isPending}
onClick={async () => {
await saveKey.mutateAsync(draft);
@@ -687,6 +689,7 @@ function SSHKeyManager({
{editing ? "Update key" : "Save key"}
</Button>
<Button
className="mobile-touch-target"
variant="outline"
disabled={generateKey.isPending}
onClick={async () => {
@@ -710,11 +713,16 @@ function SSHKeyManager({
>
{generateKey.isPending ? "Generating..." : "Generate key"}
</Button>
<Button variant="outline" onClick={clear}>
<Button
variant="outline"
onClick={clear}
className="mobile-touch-target"
>
Clear
</Button>
{selectedKey && (
<Button
className="mobile-touch-target"
variant="destructive"
onClick={() => deleteKey.mutate(selectedKey.id)}
>
@@ -780,7 +788,11 @@ function ResetLocalDatabaseCard() {
Reset the local SQLite settings/media index databases after
acknowledging the data loss.
</p>
<Button variant="destructive" onClick={() => setOpen(true)}>
<Button
variant="destructive"
onClick={() => setOpen(true)}
className="mobile-touch-target"
>
Reset local database
</Button>
{resetDatabase.error && (
@@ -1117,6 +1129,7 @@ export function Settings() {
</div>
<div className="flex flex-row flex-wrap items-center gap-2">
<Button
className="mobile-touch-target"
variant="outline"
onClick={() =>
openEditMachine(
@@ -1144,6 +1157,7 @@ export function Settings() {
Edit
</Button>
<Button
className="mobile-touch-target"
variant="destructive"
onClick={() => setDeleteMachineId(selectedMachine.id)}
>
@@ -1207,6 +1221,7 @@ export function Settings() {
/>
{machineDraft.id ? (
<Button
className="mobile-touch-target"
variant="destructive"
onClick={() => setDeleteMachineId(machineDraft.id as string)}
>
@@ -1266,6 +1281,7 @@ export function Settings() {
secondaryAction={
machineDraft.id ? (
<Button
className="mobile-touch-target"
variant="destructive"
onClick={() => {
setDeleteMachineId(machineDraft.id as string);