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:
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user