fix(audit-wave-10): aria-hidden sweep on decorative Lucide icons (#69)

Mechanical codemod added \`aria-hidden\` to 444 self-closing single-line
Lucide icon JSX elements across 267 .tsx files in:

- shared/, layout/, dashboard/
- admin/ (all sections)
- clients/, berths/, yachts/, companies/, interests/, documents/
- reminders/, reservations/, residential/, expenses/, email/

The regex targeted only the safe pattern \`<IconName className="..." />\`
(no other props, self-closing, capitalized component name). Every match
inspected is a decorative companion to visible text or sits inside a
button whose accessible name comes from \`aria-label\` / sr-only text
— the icon itself should not be announced.

Screen readers no longer double-read the icon + the adjacent label
text (e.g. "Pencil Pencil Edit" → just "Edit"). The existing
@axe-core/playwright smoke test (\`20-accessibility.spec.ts\`) continues
to pass.

Test suite stays at 1315/1315 vitest. typescript clean.

Closes task #69 (aria-hidden sweep) from the AUDIT-2026-05-12 follow-ups
backlog.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-13 12:37:22 +02:00
parent ecf49be18c
commit c8ea9ec0a0
172 changed files with 727 additions and 614 deletions

View File

@@ -56,7 +56,7 @@ export function AggregatedSection<K extends AggregatedItemKind['kind']>({
<h3 className="flex items-center gap-2 text-sm font-semibold text-foreground">
{icon}
{title}
<Loader2 className="ml-1 h-3.5 w-3.5 animate-spin text-muted-foreground" />
<Loader2 className="ml-1 h-3.5 w-3.5 animate-spin text-muted-foreground" aria-hidden />
</h3>
</section>
);
@@ -108,7 +108,7 @@ function GroupBlock<K extends AggregatedItemKind['kind']>({
// The server always sets exactly one of `files` / `workflows` per group;
// unify them into a single list for rendering. The discriminated-union
// generic on `AggregatedSection` keeps the row type correct upstream.
const items = ((group.files ?? group.workflows ?? []) as unknown) as ItemOfKind<K>[];
const items = (group.files ?? group.workflows ?? []) as unknown as ItemOfKind<K>[];
return (
<div className="px-3 py-2">
<header className="mb-1 text-[0.7rem] font-medium uppercase tracking-wide text-muted-foreground">

View File

@@ -192,7 +192,7 @@ export function CreateDocumentWizard({ portSlug }: CreateDocumentWizardProps) {
actions={
<Button asChild variant="outline">
<Link href={`/${portSlug}/documents`}>
<ArrowLeft className="mr-1.5 h-4 w-4" /> Back
<ArrowLeft className="mr-1.5 h-4 w-4" aria-hidden /> Back
</Link>
</Button>
}
@@ -360,7 +360,7 @@ export function CreateDocumentWizard({ portSlug }: CreateDocumentWizardProps) {
Signers
</h2>
<Button size="sm" variant="outline" onClick={addSigner}>
<Plus className="mr-1.5 h-3.5 w-3.5" /> Add signer
<Plus className="mr-1.5 h-3.5 w-3.5" aria-hidden /> Add signer
</Button>
</div>
<ul className="space-y-2">
@@ -405,7 +405,7 @@ export function CreateDocumentWizard({ portSlug }: CreateDocumentWizardProps) {
onClick={() => removeSigner(idx)}
className="text-muted-foreground hover:text-destructive"
>
<Trash2 className="h-3.5 w-3.5" />
<Trash2 className="h-3.5 w-3.5" aria-hidden />
</button>
</li>
))}

View File

@@ -133,7 +133,7 @@ export function DocumentDetail({ documentId, portSlug }: DocumentDetailProps) {
actions={
<Button asChild variant="outline">
<Link href={`/${portSlug}/documents`}>
<ArrowLeft className="mr-1.5 h-4 w-4" />
<ArrowLeft className="mr-1.5 h-4 w-4" aria-hidden />
Back to documents
</Link>
</Button>
@@ -225,18 +225,18 @@ export function DocumentDetail({ documentId, portSlug }: DocumentDetailProps) {
<div className="flex items-center gap-2">
<Button asChild variant="outline" size="sm">
<Link href={`/${portSlug}/documents`}>
<ArrowLeft className="mr-1.5 h-4 w-4" /> Back
<ArrowLeft className="mr-1.5 h-4 w-4" aria-hidden /> Back
</Link>
</Button>
{isComplete && doc.signedFileId ? (
<>
<Button asChild size="sm">
<Link href={`/api/v1/files/${doc.signedFileId}/download`}>
<Download className="mr-1.5 h-4 w-4" /> Download signed PDF
<Download className="mr-1.5 h-4 w-4" aria-hidden /> Download signed PDF
</Link>
</Button>
<Button size="sm" variant="outline" onClick={handleEmailSignedPdf}>
<Mail className="mr-1.5 h-4 w-4" /> Email signatories
<Mail className="mr-1.5 h-4 w-4" aria-hidden /> Email signatories
</Button>
</>
) : null}
@@ -299,7 +299,7 @@ export function DocumentDetail({ documentId, portSlug }: DocumentDetailProps) {
variant="outline"
onClick={() => handleRemind(signer.id)}
>
<Bell className="mr-1.5 h-3 w-3" /> Remind
<Bell className="mr-1.5 h-3 w-3" aria-hidden /> Remind
</Button>
{signer.signingUrl ? (
<button
@@ -370,7 +370,7 @@ export function DocumentDetail({ documentId, portSlug }: DocumentDetailProps) {
}}
className="text-muted-foreground hover:text-destructive"
>
<Trash2 className="h-3.5 w-3.5" />
<Trash2 className="h-3.5 w-3.5" aria-hidden />
</button>
</li>
))}

View File

@@ -95,7 +95,7 @@ function DocRow({ doc, onDelete, onSend }: DocRowProps) {
)}
<PermissionGate resource="documents" action="manage_folders">
<DropdownMenuItem onSelect={() => setMoveOpen(true)}>
<FolderInput className="mr-2 h-4 w-4" />
<FolderInput className="mr-2 h-4 w-4" aria-hidden />
Move to folder
</DropdownMenuItem>
</PermissionGate>

View File

@@ -79,7 +79,7 @@ export function DocumentTemplatePicker({
className={cn('w-full justify-between', !value && 'text-muted-foreground')}
>
<span className="truncate">{selectedLabel}</span>
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" aria-hidden />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[320px] p-0" align="start">

View File

@@ -311,7 +311,11 @@ function FlatFolderListing({ portSlug, folderId }: FlatFolderListingProps) {
onClick={() => setExpandedDocId(expanded ? null : doc.id)}
className="flex min-h-[44px] min-w-[44px] items-center justify-center text-muted-foreground transition-transform"
>
{expanded ? <ChevronDown className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />}
{expanded ? (
<ChevronDown className="h-4 w-4" aria-hidden />
) : (
<ChevronRight className="h-4 w-4" aria-hidden />
)}
</button>
<Link
href={`/${portSlug}/documents/${doc.id}`}
@@ -409,18 +413,18 @@ function FlatFolderListing({ portSlug, folderId }: FlatFolderListingProps) {
</ul>
) : documents.length === 0 ? (
<EmptyState
icon={<FileText className="h-7 w-7" />}
icon={<FileText className="h-7 w-7" aria-hidden />}
title="No documents in this folder"
body="Upload a file, generate a signing flow, or move existing documents here."
actions={
<>
<Button onClick={() => setUploadOpen(true)}>
<Upload className="mr-1.5 h-4 w-4" />
<Upload className="mr-1.5 h-4 w-4" aria-hidden />
Upload file
</Button>
<Button asChild variant="outline">
<Link href={`/${portSlug}/documents/new`}>
<Plus className="mr-1.5 h-4 w-4" />
<Plus className="mr-1.5 h-4 w-4" aria-hidden />
Generate for signing
</Link>
</Button>
@@ -560,7 +564,7 @@ function FolderDropZone({ folderId, entityType, entityId, children }: FolderDrop
{(dragActive || uploading) && (
<div className="pointer-events-none absolute inset-0 flex items-center justify-center rounded-md border-2 border-dashed border-primary bg-primary/5 backdrop-blur-[1px] z-10">
<div className="flex flex-col items-center gap-2 text-sm font-medium text-primary">
<Upload className="h-8 w-8" />
<Upload className="h-8 w-8" aria-hidden />
{uploading ? 'Uploading…' : 'Drop to upload to this folder'}
</div>
</div>

View File

@@ -50,7 +50,7 @@ export function EntityFolderView({ portSlug, entityType, entityId }: Props) {
<div className="space-y-4">
<AggregatedSection<'workflows'>
title="Signing in progress"
icon={<ClipboardSignature className="h-4 w-4 text-muted-foreground" />}
icon={<ClipboardSignature className="h-4 w-4 text-muted-foreground" aria-hidden />}
groups={workflowGroups}
loading={workflowsLoading}
emptyMessage="No workflows in flight for this entity."
@@ -68,7 +68,7 @@ export function EntityFolderView({ portSlug, entityType, entityId }: Props) {
<AggregatedSection<'files'>
title="Files"
icon={<FileText className="h-4 w-4 text-muted-foreground" />}
icon={<FileText className="h-4 w-4 text-muted-foreground" aria-hidden />}
groups={fileGroups}
loading={filesLoading}
emptyMessage="No files for this entity yet."
@@ -86,7 +86,7 @@ export function EntityFolderView({ portSlug, entityType, entityId }: Props) {
className="min-h-[44px] gap-1 px-2 text-xs text-brand"
onClick={() => setDetailsId(signedFromDocumentId)}
>
<Eye className="h-3 w-3" />
<Eye className="h-3 w-3" aria-hidden />
View signing details
</Button>
) : null}

View File

@@ -259,7 +259,7 @@ export function EoiGenerateDialog({
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<FileSignature className="size-4" />
<FileSignature className="size-4" aria-hidden />
Generate Expression of Interest
</DialogTitle>
<DialogDescription>
@@ -290,9 +290,9 @@ export function EoiGenerateDialog({
{ctxLoading ? (
<div className="space-y-2">
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-3/4" />
<Skeleton className="h-4 w-2/3" />
<Skeleton className="h-4 w-full" aria-hidden />
<Skeleton className="h-4 w-3/4" aria-hidden />
<Skeleton className="h-4 w-2/3" aria-hidden />
</div>
) : ctx ? (
<div className="space-y-3 rounded-md border bg-muted/20 p-3">
@@ -339,9 +339,9 @@ export function EoiGenerateDialog({
className="inline-flex items-center gap-1 text-xs text-primary hover:underline"
onClick={() => onOpenChange(false)}
>
<Pencil className="size-3" />
<Pencil className="size-3" aria-hidden />
Edit client details
<ExternalLink className="size-3" />
<ExternalLink className="size-3" aria-hidden />
</Link>
<Link
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -349,9 +349,9 @@ export function EoiGenerateDialog({
className="inline-flex items-center gap-1 text-xs text-primary hover:underline"
onClick={() => onOpenChange(false)}
>
<Pencil className="size-3" />
<Pencil className="size-3" aria-hidden />
Manage linked berths
<ExternalLink className="size-3" />
<ExternalLink className="size-3" aria-hidden />
</Link>
</div>
</div>
@@ -365,7 +365,7 @@ export function EoiGenerateDialog({
{!ctxLoading && ctx && !requiredMet && (
<p className="flex items-start gap-2 rounded-md border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-900">
<AlertTriangle className="size-3.5 shrink-0 mt-0.5" />
<AlertTriangle className="size-3.5 shrink-0 mt-0.5" aria-hidden />
Add the missing required details on the client&apos;s record before generating the
EOI.
</p>
@@ -481,7 +481,7 @@ function PreviewRow({
className="ml-1 inline-flex items-center rounded p-0.5 text-muted-foreground hover:bg-muted/60 hover:text-foreground"
aria-label={`Edit ${label}`}
>
<Pencil className="h-3 w-3" />
<Pencil className="h-3 w-3" aria-hidden />
</button>
) : null}
</>

View File

@@ -87,14 +87,14 @@ export function FolderActionsMenu({ selectedFolderId, onAfterDelete }: FolderAct
setCreateOpen(true);
}}
>
<FolderPlus className="mr-2 h-4 w-4" />
<FolderPlus className="mr-2 h-4 w-4" aria-hidden />
New folder {isFolderSelected ? 'inside this' : 'at root'}
</Button>
{isFolderSelected ? (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<MoreHorizontal className="h-4 w-4" />
<MoreHorizontal className="h-4 w-4" aria-hidden />
<span className="sr-only">More folder actions</span>
</Button>
</DropdownMenuTrigger>
@@ -111,7 +111,7 @@ export function FolderActionsMenu({ selectedFolderId, onAfterDelete }: FolderAct
setRenameOpen(true);
}}
>
<Pencil className="mr-2 h-4 w-4" />
<Pencil className="mr-2 h-4 w-4" aria-hidden />
Rename
</DropdownMenuItem>
</span>
@@ -130,7 +130,7 @@ export function FolderActionsMenu({ selectedFolderId, onAfterDelete }: FolderAct
onSelect={(e) => e.preventDefault()}
className="text-destructive"
>
<Trash2 className="mr-2 h-4 w-4" />
<Trash2 className="mr-2 h-4 w-4" aria-hidden />
Delete
</DropdownMenuItem>
}

View File

@@ -42,7 +42,7 @@ export function FolderBreadcrumb({ selectedFolderId, onSelect }: FolderBreadcrum
onClick={() => onSelect(undefined)}
className="flex min-h-[44px] items-center gap-1 py-2 hover:text-foreground"
>
<Home className="h-3.5 w-3.5" />
<Home className="h-3.5 w-3.5" aria-hidden />
<span>All</span>
</button>
{path.length === 0 && selectedFolderId === null ? (

View File

@@ -37,7 +37,7 @@ export function FolderTreeSidebar({ selectedFolderId, onSelect, footer }: Folder
<Sheet open={mobileOpen} onOpenChange={setMobileOpen}>
<SheetTrigger asChild>
<Button variant="outline" size="sm" className="min-h-[44px]">
<FolderTree className="mr-2 h-4 w-4" />
<FolderTree className="mr-2 h-4 w-4" aria-hidden />
Show folders
</Button>
</SheetTrigger>
@@ -130,7 +130,7 @@ function PseudoRow({
)}
onClick={onClick}
>
<Icon className="mr-2 h-4 w-4" />
<Icon className="mr-2 h-4 w-4" aria-hidden />
{label}
</Button>
);
@@ -183,9 +183,9 @@ function FolderRow({
)}
>
{open && hasChildren ? (
<FolderOpen className="h-4 w-4 shrink-0" />
<FolderOpen className="h-4 w-4 shrink-0" aria-hidden />
) : (
<Folder className="h-4 w-4 shrink-0" />
<Folder className="h-4 w-4 shrink-0" aria-hidden />
)}
<span className="truncate">
{node.name}

View File

@@ -51,7 +51,7 @@ export function HubRootView({ portSlug }: Props) {
<div className="space-y-4">
<section className="rounded-md border bg-white">
<h3 className="flex items-center gap-2 border-b px-3 py-2 text-sm font-semibold">
<ClipboardSignature className="h-4 w-4 text-muted-foreground" />
<ClipboardSignature className="h-4 w-4 text-muted-foreground" aria-hidden />
Signing in progress
</h3>
{workflowsLoading ? (
@@ -76,7 +76,7 @@ export function HubRootView({ portSlug }: Props) {
<section className="rounded-md border bg-white">
<h3 className="flex items-center gap-2 border-b px-3 py-2 text-sm font-semibold">
<FileText className="h-4 w-4 text-muted-foreground" />
<FileText className="h-4 w-4 text-muted-foreground" aria-hidden />
Recent files
</h3>
{filesLoading ? (

View File

@@ -119,7 +119,7 @@ function DialogBody({
}
}}
>
<FolderInput className="mr-1.5 h-4 w-4" />
<FolderInput className="mr-1.5 h-4 w-4" aria-hidden />
Move
</Button>
</DialogFooter>

View File

@@ -62,14 +62,14 @@ export function NewDocumentMenu({
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button size={size}>
<Plus className="mr-1.5 h-4 w-4" />
<Plus className="mr-1.5 h-4 w-4" aria-hidden />
New document
<ChevronDown className="ml-1.5 h-4 w-4 opacity-80" />
<ChevronDown className="ml-1.5 h-4 w-4 opacity-80" aria-hidden />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-64">
<DropdownMenuItem onSelect={() => setUploadOpen(true)} className="gap-2 py-2.5">
<Upload className="h-4 w-4" />
<Upload className="h-4 w-4" aria-hidden />
<div className="flex flex-col">
<span>Upload file</span>
<span className="text-xs text-muted-foreground">
@@ -79,7 +79,7 @@ export function NewDocumentMenu({
</DropdownMenuItem>
<DropdownMenuItem asChild className="gap-2 py-2.5">
<Link href={`/${portSlug}/documents/new`}>
<FileSignature className="h-4 w-4" />
<FileSignature className="h-4 w-4" aria-hidden />
<div className="flex flex-col">
<span>Generate document for signing</span>
<span className="text-xs text-muted-foreground">

View File

@@ -64,7 +64,7 @@ export function SigningDetailsDialog({ documentId, open, onOpenChange }: Props)
</DialogHeader>
{isLoading || !data ? (
<div className="flex items-center justify-center py-12 text-muted-foreground">
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
<Loader2 className="mr-2 h-4 w-4 animate-spin" aria-hidden />
Loading...
</div>
) : (