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) { }: DialogFooterProps) {
return ( return (
<div className="flex flex-row flex-wrap items-center justify-end gap-2"> <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} {cancelLabel}
</Button> </Button>
{secondaryAction ? ( {secondaryAction ? (
@@ -59,6 +63,7 @@ export function DialogFooter({
</div> </div>
) : null} ) : null}
<Button <Button
className="mobile-touch-target"
variant={resolveConfirmVariant(confirmColor, confirmVariant)} variant={resolveConfirmVariant(confirmColor, confirmVariant)}
disabled={confirmDisabled} disabled={confirmDisabled}
onClick={onConfirm} onClick={onConfirm}
@@ -332,10 +332,10 @@ export function WidgetConfigDialog({ open, onClose }: Props) {
/> />
{!isMobile ? ( {!isMobile ? (
<div className="flex justify-end gap-2"> <div className="flex justify-end gap-2">
<Button variant="outline" onClick={reset}> <Button variant="outline" onClick={reset} className="mobile-touch-target">
Back Back
</Button> </Button>
<Button onClick={saveDraft} disabled={saveWidget.isPending}> <Button onClick={saveDraft} disabled={saveWidget.isPending} className="mobile-touch-target">
Save widget Save widget
</Button> </Button>
</div> </div>
+4 -4
View File
@@ -266,7 +266,7 @@ function TaskDialog({
confirmBusyLabel="Save action" confirmBusyLabel="Save action"
secondaryAction={ secondaryAction={
onDelete ? ( onDelete ? (
<Button variant="destructive" onClick={onDelete}> <Button variant="destructive" onClick={onDelete} className="mobile-touch-target">
Delete Delete
</Button> </Button>
) : undefined ) : undefined
@@ -412,13 +412,13 @@ export function Actions() {
description="Open the editor popup to modify this action." description="Open the editor popup to modify this action."
action={ action={
<div className="flex flex-row flex-wrap items-center gap-2"> <div className="flex flex-row flex-wrap items-center gap-2">
<Button <Button className="mobile-touch-target"
variant="outline" variant="outline"
onClick={() => openEdit(initialFromTask(editingTask))} onClick={() => openEdit(initialFromTask(editingTask))}
> >
Edit Edit
</Button> </Button>
<Button <Button className="mobile-touch-target"
disabled={runTask.isPending || !runServiceId} disabled={runTask.isPending || !runServiceId}
onClick={async () => { onClick={async () => {
await runTask.mutateAsync({ 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." 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] && ( {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 Select first action
</Button> </Button>
)} )}
+22 -4
View File
@@ -429,10 +429,20 @@ function ShortcutCard({
> >
Open Open
</Button> </Button>
<Button size="sm" variant="outline" onClick={onEdit} className="mobile-touch-target"> <Button
size="sm"
variant="outline"
onClick={onEdit}
className="mobile-touch-target"
>
Edit Edit
</Button> </Button>
<Button size="sm" variant="destructive" onClick={onDelete} className="mobile-touch-target"> <Button
size="sm"
variant="destructive"
onClick={onDelete}
className="mobile-touch-target"
>
Delete Delete
</Button> </Button>
</div> </div>
@@ -503,10 +513,18 @@ export function Dashboard() {
description="Quick links to websites today, with room for action and user shortcuts later." description="Quick links to websites today, with room for action and user shortcuts later."
action={ action={
<div className="flex gap-2"> <div className="flex gap-2">
<Button variant="outline" onClick={() => setWidgetDialogOpen(true)}> <Button
variant="outline"
className="mobile-touch-target"
onClick={() => setWidgetDialogOpen(true)}
>
Edit dashboard Edit dashboard
</Button> </Button>
<Button variant="outline" onClick={openCreateShortcut}> <Button
variant="outline"
className="mobile-touch-target"
onClick={openCreateShortcut}
>
Add shortcut Add shortcut
</Button> </Button>
</div> </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"> <div className="flex flex-col gap-2 md:flex-row md:items-end">
<Button <Button
variant="outline" variant="outline"
className="w-full md:w-auto" className="w-full md:w-auto mobile-touch-target"
onClick={() => navigate(pathInput || "/")} onClick={() => navigate(pathInput || "/")}
> >
Open Open
</Button> </Button>
<Button <Button
variant="outline" variant="outline"
className="w-full md:w-auto" className="w-full md:w-auto mobile-touch-target"
onClick={() => refetch()} onClick={() => refetch()}
> >
Refresh Refresh
@@ -837,7 +837,7 @@ export function FileBrowser() {
</Select> </Select>
</div> </div>
<div className="flex flex-col gap-2 md:col-span-3 md:flex-row md:items-end"> <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} disabled={!selectedJob || runJob.isPending}
onClick={() => onClick={() =>
runJob.mutate({ runJob.mutate({
+3 -3
View File
@@ -485,7 +485,7 @@ export function Media() {
</p> </p>
)} )}
<Button <Button className="mobile-touch-target"
variant="outline" variant="outline"
onClick={() => buildIndex.mutate()} onClick={() => buildIndex.mutate()}
disabled={ disabled={
@@ -496,7 +496,7 @@ export function Media() {
</Button> </Button>
{buildRunning && ( {buildRunning && (
<> <>
<Button <Button className="mobile-touch-target"
variant="destructive" variant="destructive"
onClick={() => stopBuildIndex.mutate()} onClick={() => stopBuildIndex.mutate()}
disabled={stopBuildIndex.isPending || buildCancelRequested} disabled={stopBuildIndex.isPending || buildCancelRequested}
@@ -507,7 +507,7 @@ export function Media() {
</Button> </Button>
<Button <Button
variant="outline" 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()} onClick={() => forceStopBuildIndex.mutate()}
disabled={forceStopBuildIndex.isPending} disabled={forceStopBuildIndex.isPending}
> >
+18 -4
View File
@@ -212,7 +212,11 @@ export function ServicePage() {
<Label htmlFor="service-enabled">Enabled</Label> <Label htmlFor="service-enabled">Enabled</Label>
</div> </div>
{configFields} {configFields}
<Button variant="destructive" onClick={() => setDeleteOpen(true)}> <Button
className="mobile-touch-target"
variant="destructive"
onClick={() => setDeleteOpen(true)}
>
Delete service Delete service
</Button> </Button>
{widgetsCard} {widgetsCard}
@@ -252,10 +256,18 @@ export function ServicePage() {
<Label htmlFor="service-enabled">Enabled</Label> <Label htmlFor="service-enabled">Enabled</Label>
</div> </div>
<div className="flex justify-between"> <div className="flex justify-between">
<Button onClick={save} disabled={saveService.isPending}> <Button
className="mobile-touch-target"
onClick={save}
disabled={saveService.isPending}
>
Save Save
</Button> </Button>
<Button variant="destructive" onClick={() => setDeleteOpen(true)}> <Button
className="mobile-touch-target"
variant="destructive"
onClick={() => setDeleteOpen(true)}
>
Delete Delete
</Button> </Button>
</div> </div>
@@ -392,7 +404,9 @@ function ServiceConnectionFields({
</div> </div>
)} )}
<Button onClick={handleUpdateConnection}>Update connection</Button> <Button className="mobile-touch-target" onClick={handleUpdateConnection}>
Update connection
</Button>
</div> </div>
); );
+2 -2
View File
@@ -195,7 +195,7 @@ function CreateServiceDialog({
{!draft ? ( {!draft ? (
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
{types.map((t) => ( {types.map((t) => (
<Button <Button className="mobile-touch-target"
key={t.service_type} key={t.service_type}
variant="outline" variant="outline"
onClick={() => setDraft(emptyDraft(t.service_type))} onClick={() => setDraft(emptyDraft(t.service_type))}
@@ -287,7 +287,7 @@ export function ServicesPage() {
title="Services" title="Services"
description="External services the app talks to. Configure URLs and API keys here; they are encrypted at rest." description="External services the app talks to. Configure URLs and API keys here; they are encrypted at rest."
action={ 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" /> <Plus className="mr-1 h-3 w-3" />
Add service Add service
</Button> </Button>
+18 -2
View File
@@ -466,6 +466,7 @@ function MachineEditor({
</Alert> </Alert>
<div className="flex flex-row flex-wrap items-center gap-2"> <div className="flex flex-row flex-wrap items-center gap-2">
<Button <Button
className="mobile-touch-target"
variant="outline" variant="outline"
onClick={onValidateSSH} onClick={onValidateSSH}
disabled={ disabled={
@@ -678,6 +679,7 @@ function SSHKeyManager({
</div> </div>
<div className="flex flex-row flex-wrap items-center gap-2"> <div className="flex flex-row flex-wrap items-center gap-2">
<Button <Button
className="mobile-touch-target"
disabled={saveKey.isPending} disabled={saveKey.isPending}
onClick={async () => { onClick={async () => {
await saveKey.mutateAsync(draft); await saveKey.mutateAsync(draft);
@@ -687,6 +689,7 @@ function SSHKeyManager({
{editing ? "Update key" : "Save key"} {editing ? "Update key" : "Save key"}
</Button> </Button>
<Button <Button
className="mobile-touch-target"
variant="outline" variant="outline"
disabled={generateKey.isPending} disabled={generateKey.isPending}
onClick={async () => { onClick={async () => {
@@ -710,11 +713,16 @@ function SSHKeyManager({
> >
{generateKey.isPending ? "Generating..." : "Generate key"} {generateKey.isPending ? "Generating..." : "Generate key"}
</Button> </Button>
<Button variant="outline" onClick={clear}> <Button
variant="outline"
onClick={clear}
className="mobile-touch-target"
>
Clear Clear
</Button> </Button>
{selectedKey && ( {selectedKey && (
<Button <Button
className="mobile-touch-target"
variant="destructive" variant="destructive"
onClick={() => deleteKey.mutate(selectedKey.id)} onClick={() => deleteKey.mutate(selectedKey.id)}
> >
@@ -780,7 +788,11 @@ function ResetLocalDatabaseCard() {
Reset the local SQLite settings/media index databases after Reset the local SQLite settings/media index databases after
acknowledging the data loss. acknowledging the data loss.
</p> </p>
<Button variant="destructive" onClick={() => setOpen(true)}> <Button
variant="destructive"
onClick={() => setOpen(true)}
className="mobile-touch-target"
>
Reset local database Reset local database
</Button> </Button>
{resetDatabase.error && ( {resetDatabase.error && (
@@ -1117,6 +1129,7 @@ export function Settings() {
</div> </div>
<div className="flex flex-row flex-wrap items-center gap-2"> <div className="flex flex-row flex-wrap items-center gap-2">
<Button <Button
className="mobile-touch-target"
variant="outline" variant="outline"
onClick={() => onClick={() =>
openEditMachine( openEditMachine(
@@ -1144,6 +1157,7 @@ export function Settings() {
Edit Edit
</Button> </Button>
<Button <Button
className="mobile-touch-target"
variant="destructive" variant="destructive"
onClick={() => setDeleteMachineId(selectedMachine.id)} onClick={() => setDeleteMachineId(selectedMachine.id)}
> >
@@ -1207,6 +1221,7 @@ export function Settings() {
/> />
{machineDraft.id ? ( {machineDraft.id ? (
<Button <Button
className="mobile-touch-target"
variant="destructive" variant="destructive"
onClick={() => setDeleteMachineId(machineDraft.id as string)} onClick={() => setDeleteMachineId(machineDraft.id as string)}
> >
@@ -1266,6 +1281,7 @@ export function Settings() {
secondaryAction={ secondaryAction={
machineDraft.id ? ( machineDraft.id ? (
<Button <Button
className="mobile-touch-target"
variant="destructive" variant="destructive"
onClick={() => { onClick={() => {
setDeleteMachineId(machineDraft.id as string); setDeleteMachineId(machineDraft.id as string);