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

@@ -81,7 +81,7 @@ export function AddBerthToInterestDialog({
title="Pitching specifically"
description="The client is pitched on this exact berth."
consequence="This berth will appear as under interest on the public map."
icon={<Eye className="size-4" />}
icon={<Eye className="size-4" aria-hidden />}
/>
<RoleCard
value="exploring"
@@ -89,7 +89,7 @@ export function AddBerthToInterestDialog({
title="Just exploring"
description="The berth is being considered or covered by the EOI bundle, but not pitched specifically."
consequence="This berth is hidden from the public map."
icon={<EyeOff className="size-4" />}
icon={<EyeOff className="size-4" aria-hidden />}
/>
</RadioGroup>
@@ -109,7 +109,9 @@ export function AddBerthToInterestDialog({
Cancel
</Button>
<Button type="button" onClick={handleSubmit} disabled={mutation.isPending}>
{mutation.isPending ? <Loader2 className="mr-1.5 size-3.5 animate-spin" /> : null}
{mutation.isPending ? (
<Loader2 className="mr-1.5 size-3.5 animate-spin" aria-hidden />
) : null}
Add berth to interest
</Button>
</DialogFooter>

View File

@@ -156,7 +156,7 @@ function RecommendationCard({ rec, portSlug, onAdd }: RecommendationCardProps) {
</span>
{showHeat ? (
<span className="inline-flex items-center gap-1 rounded-md border border-rose-200 bg-rose-50 px-2 py-0.5 text-xs font-medium text-rose-800">
<Flame className="size-3" />
<Flame className="size-3" aria-hidden />
Heat {Math.round(rec.heat!.total)}
</span>
) : null}
@@ -174,9 +174,9 @@ function RecommendationCard({ rec, portSlug, onAdd }: RecommendationCardProps) {
</div>
</div>
{expanded ? (
<ChevronUp className="size-4 shrink-0 text-muted-foreground" />
<ChevronUp className="size-4 shrink-0 text-muted-foreground" aria-hidden />
) : (
<ChevronDown className="size-4 shrink-0 text-muted-foreground" />
<ChevronDown className="size-4 shrink-0 text-muted-foreground" aria-hidden />
)}
</button>
@@ -213,7 +213,7 @@ function RecommendationCard({ rec, portSlug, onAdd }: RecommendationCardProps) {
onAdd(rec);
}}
>
<Plus className="mr-1.5 size-3.5" />
<Plus className="mr-1.5 size-3.5" aria-hidden />
Add to interest
</Button>
<Button asChild size="sm" variant="outline">
@@ -375,7 +375,7 @@ export function BerthRecommenderPanel({
<div className="flex flex-wrap items-start justify-between gap-2">
<div className="min-w-0 space-y-1">
<CardTitle className="flex items-center gap-2 text-base">
<Sparkles className="size-4 text-brand-600" />
<Sparkles className="size-4 text-brand-600" aria-hidden />
Recommendations for {formatDesired(desiredLengthFt, desiredWidthFt, desiredDraftFt)}
</CardTitle>
{!hasDimensions ? (
@@ -392,7 +392,7 @@ export function BerthRecommenderPanel({
onClick={() => setFiltersOpen((v) => !v)}
disabled={!hasDimensions}
>
<Filter className="mr-1.5 size-3.5" />
<Filter className="mr-1.5 size-3.5" aria-hidden />
{filtersOpen ? 'Hide filters' : 'Add filters'}
</Button>
<Button

View File

@@ -146,9 +146,9 @@ export function ExternalEoiUploadDialog({ open, onOpenChange, interestId, onSucc
</Button>
<Button onClick={() => mutation.mutate()} disabled={!file || mutation.isPending}>
{mutation.isPending ? (
<Loader2 className="h-3.5 w-3.5 animate-spin mr-1.5" />
<Loader2 className="h-3.5 w-3.5 animate-spin mr-1.5" aria-hidden />
) : (
<Upload className="h-3.5 w-3.5 mr-1.5" />
<Upload className="h-3.5 w-3.5 mr-1.5" aria-hidden />
)}
Upload
</Button>

View File

@@ -217,9 +217,9 @@ export function InlineStagePicker({
>
<span>{STAGE_LABELS[stage]}</span>
{mutation.isPending ? (
<Loader2 className="size-3 animate-spin" />
<Loader2 className="size-3 animate-spin" aria-hidden />
) : showChevron ? (
<ChevronDown className="size-3 opacity-70" />
<ChevronDown className="size-3 opacity-70" aria-hidden />
) : null}
</button>
</PopoverTrigger>
@@ -234,7 +234,7 @@ export function InlineStagePicker({
// strongly nudged for the audit log.
<div className="p-3 space-y-3">
<div className="flex items-start gap-2">
<AlertTriangle className="size-4 shrink-0 text-amber-600 mt-0.5" />
<AlertTriangle className="size-4 shrink-0 text-amber-600 mt-0.5" aria-hidden />
<div className="text-sm">
<p className="font-medium text-foreground">Override transition</p>
<p className="text-xs text-muted-foreground">
@@ -270,7 +270,7 @@ export function InlineStagePicker({
disabled={mutation.isPending}
className="gap-1"
>
<ChevronLeft className="size-3.5" />
<ChevronLeft className="size-3.5" aria-hidden />
Back
</Button>
<Button
@@ -279,7 +279,9 @@ export function InlineStagePicker({
onClick={commitOverride}
disabled={mutation.isPending}
>
{mutation.isPending && <Loader2 className="size-3.5 animate-spin mr-1" />}
{mutation.isPending && (
<Loader2 className="size-3.5 animate-spin mr-1" aria-hidden />
)}
Confirm override
</Button>
</div>
@@ -324,9 +326,12 @@ export function InlineStagePicker({
/>
<span className="flex-1">{STAGE_LABELS[s]}</span>
{isPending ? (
<Loader2 className="size-3.5 animate-spin text-muted-foreground" />
<Loader2
className="size-3.5 animate-spin text-muted-foreground"
aria-hidden
/>
) : isCurrent ? (
<Check className="size-3.5 text-muted-foreground" />
<Check className="size-3.5 text-muted-foreground" aria-hidden />
) : isOverride && canOverride ? (
<span
className="text-[10px] uppercase tracking-wide text-amber-600"
@@ -376,7 +381,7 @@ export function InlineStagePicker({
if (openConfirmTarget) void unlinkAllAndOpen(openConfirmTarget);
}}
>
{unlinking && <Loader2 className="mr-1.5 size-3.5 animate-spin" />}
{unlinking && <Loader2 className="mr-1.5 size-3.5 animate-spin" aria-hidden />}
Unlink {linkedBerthCount} {linkedBerthCount === 1 ? 'berth' : 'berths'} & reset
</AlertDialogAction>
</AlertDialogFooter>

View File

@@ -73,16 +73,16 @@ export function InterestCard({ interest, portSlug, onEdit, onArchive }: Interest
onClick={(e) => e.stopPropagation()}
aria-label={`Actions for ${clientName}'s interest`}
>
<MoreHorizontal className="h-4 w-4" />
<MoreHorizontal className="h-4 w-4" aria-hidden />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => onEdit(interest)}>
<Pencil className="mr-2 h-3.5 w-3.5" />
<Pencil className="mr-2 h-3.5 w-3.5" aria-hidden />
Edit
</DropdownMenuItem>
<DropdownMenuItem className="text-destructive" onClick={() => onArchive(interest)}>
<Archive className="mr-2 h-3.5 w-3.5" />
<Archive className="mr-2 h-3.5 w-3.5" aria-hidden />
Archive
</DropdownMenuItem>
</DropdownMenuContent>
@@ -104,7 +104,7 @@ export function InterestCard({ interest, portSlug, onEdit, onArchive }: Interest
aria-label={`${notesCount} note${notesCount === 1 ? '' : 's'}`}
className="inline-flex shrink-0 items-center text-muted-foreground"
>
<MessageSquare className="size-3.5" />
<MessageSquare className="size-3.5" aria-hidden />
</span>
) : null}
</div>

View File

@@ -134,7 +134,7 @@ export function getInterestColumns({
aria-label={`${notesCount} note${notesCount === 1 ? '' : 's'}`}
className="inline-flex items-center text-muted-foreground"
>
<MessageSquare className="size-3.5" />
<MessageSquare className="size-3.5" aria-hidden />
</span>
) : null}
</div>
@@ -294,16 +294,16 @@ export function getInterestColumns({
className="h-7 w-7"
onClick={(e) => e.stopPropagation()}
>
<MoreHorizontal className="h-4 w-4" />
<MoreHorizontal className="h-4 w-4" aria-hidden />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => onEdit(row.original)}>
<Pencil className="mr-2 h-3.5 w-3.5" />
<Pencil className="mr-2 h-3.5 w-3.5" aria-hidden />
Edit
</DropdownMenuItem>
<DropdownMenuItem className="text-destructive" onClick={() => onArchive(row.original)}>
<Archive className="mr-2 h-3.5 w-3.5" />
<Archive className="mr-2 h-3.5 w-3.5" aria-hidden />
Archive
</DropdownMenuItem>
</DropdownMenuContent>

View File

@@ -114,15 +114,15 @@ export function InterestContactLogTab({ interestId }: InterestContactLogTabProps
</p>
</div>
<Button size="sm" onClick={() => setComposeOpen(true)} className="gap-1.5">
<Plus className="size-4" />
<Plus className="size-4" aria-hidden />
Log contact
</Button>
</div>
{isLoading ? (
<div className="space-y-2">
<Skeleton className="h-20 rounded-lg" />
<Skeleton className="h-20 rounded-lg" />
<Skeleton className="h-20 rounded-lg" aria-hidden />
<Skeleton className="h-20 rounded-lg" aria-hidden />
</div>
) : entries.length === 0 ? (
<EmptyState onAdd={() => setComposeOpen(true)} />
@@ -183,7 +183,7 @@ function ContactLogRow({
channelMeta.tone,
)}
>
<Icon className="size-3.5" />
<Icon className="size-3.5" aria-hidden />
</span>
<div className="min-w-0 flex-1 space-y-1.5">
<div className="flex items-center gap-2 flex-wrap">
@@ -201,7 +201,7 @@ function ContactLogRow({
<p className="text-sm text-foreground whitespace-pre-wrap">{entry.summary}</p>
{entry.followUpAt && (
<p className="inline-flex items-center gap-1.5 rounded-md border border-amber-200 bg-amber-50 px-2 py-0.5 text-xs text-amber-900">
<Bell className="size-3" />
<Bell className="size-3" aria-hidden />
Follow up {format(new Date(entry.followUpAt), 'MMM d, yyyy')} (reminder created)
</p>
)}
@@ -209,12 +209,12 @@ function ContactLogRow({
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="h-7 w-7" aria-label="Row actions">
<MoreVertical className="h-4 w-4" />
<MoreVertical className="h-4 w-4" aria-hidden />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => onEdit(entry)}>
<Pencil className="mr-2 size-3.5" />
<Pencil className="mr-2 size-3.5" aria-hidden />
Edit
</DropdownMenuItem>
<DropdownMenuItem
@@ -229,7 +229,7 @@ function ContactLogRow({
if (ok) deleteMutation.mutate();
}}
>
<Trash2 className="mr-2 size-3.5" />
<Trash2 className="mr-2 size-3.5" aria-hidden />
Delete
</DropdownMenuItem>
</DropdownMenuContent>
@@ -246,7 +246,7 @@ function EmptyState({ onAdd }: { onAdd: () => void }) {
return (
<div className="rounded-xl border border-dashed bg-muted/20 p-8 text-center">
<div className="mx-auto flex size-12 items-center justify-center rounded-full bg-background text-muted-foreground">
<Phone className="size-5" />
<Phone className="size-5" aria-hidden />
</div>
<h3 className="mt-3 text-sm font-medium text-foreground">No contact logged yet</h3>
<p className="mt-1 text-xs text-muted-foreground">
@@ -254,7 +254,7 @@ function EmptyState({ onAdd }: { onAdd: () => void }) {
picks up the deal.
</p>
<Button size="sm" onClick={onAdd} className="mt-4 gap-1.5">
<Plus className="size-3.5" />
<Plus className="size-3.5" aria-hidden />
Log first contact
</Button>
</div>

View File

@@ -106,7 +106,7 @@ export function InterestContractTab({ interestId, clientId: _clientId }: Interes
return (
<div className="space-y-5">
{docsLoading ? (
<Skeleton className="h-44 w-full rounded-lg" />
<Skeleton className="h-44 w-full rounded-lg" aria-hidden />
) : activeDoc ? (
<ActiveContractCard
doc={activeDoc}
@@ -145,7 +145,7 @@ export function InterestContractTab({ interestId, clientId: _clientId }: Interes
className="text-xs text-primary hover:underline inline-flex items-center gap-1"
>
Open
<ExternalLink className="size-3" />
<ExternalLink className="size-3" aria-hidden />
</Link>
)}
</li>
@@ -232,7 +232,7 @@ function ActiveContractCard({
<header className="flex items-start justify-between gap-3">
<div className="min-w-0 flex-1 space-y-1">
<div className="flex items-center gap-2 flex-wrap">
<FileSignature className="size-4 text-foreground" />
<FileSignature className="size-4 text-foreground" aria-hidden />
<h2 className="truncate text-base font-semibold text-foreground">{doc.title}</h2>
<StatusBadge status={doc.status} />
</div>
@@ -261,7 +261,11 @@ function ActiveContractCard({
onClick={() => remindAllMutation.mutate()}
className="gap-1.5 [&_svg]:size-3.5"
>
{remindAllMutation.isPending ? <Loader2 className="animate-spin" /> : <RefreshCw />}
{remindAllMutation.isPending ? (
<Loader2 className="animate-spin" aria-hidden />
) : (
<RefreshCw />
)}
Remind all
</Button>
)}
@@ -274,7 +278,7 @@ function ActiveContractCard({
</h3>
{signersLoading ? (
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Loader2 className="size-3.5 animate-spin" /> Loading signers
<Loader2 className="size-3.5 animate-spin" aria-hidden /> Loading signers
</div>
) : signers.length === 0 ? (
<p className="text-sm text-muted-foreground italic">
@@ -287,7 +291,7 @@ function ActiveContractCard({
<footer className="mt-3 flex flex-wrap items-center justify-between gap-2 text-xs text-muted-foreground">
<p className="flex items-center gap-1.5">
<AlertTriangle className="size-3 text-amber-600" />
<AlertTriangle className="size-3 text-amber-600" aria-hidden />
Reminders are rate-limited (max once per 7 days per signer).
</p>
<div className="flex items-center gap-1">
@@ -338,7 +342,7 @@ function EmptyContractState({
return (
<section className="rounded-xl border border-dashed bg-muted/20 p-8 text-center">
<div className="mx-auto flex size-14 items-center justify-center rounded-full bg-background text-muted-foreground">
<FileSignature className="size-6" />
<FileSignature className="size-6" aria-hidden />
</div>
<h2 className="mt-4 text-base font-semibold text-foreground">
No contract in flight for this interest
@@ -349,11 +353,11 @@ function EmptyContractState({
</p>
<div className="mt-5 flex flex-wrap items-center justify-center gap-2">
<Button onClick={onUploadForSigning} size="sm" className="gap-1.5">
<FileSignature className="size-4" />
<FileSignature className="size-4" aria-hidden />
Upload draft for signing
</Button>
<Button onClick={onUploadSigned} variant="outline" size="sm" className="gap-1.5">
<Upload className="size-4" />
<Upload className="size-4" aria-hidden />
Upload paper-signed copy
</Button>
</div>
@@ -372,7 +376,7 @@ function StatusBadge({ status }: { status: DocumentRow['status'] }) {
STATUS_TONES[status],
)}
>
{status === 'completed' && <CheckCircle2 className="mr-1 size-3" />}
{status === 'completed' && <CheckCircle2 className="mr-1 size-3" aria-hidden />}
{STATUS_LABELS[status]}
</Badge>
);

View File

@@ -231,7 +231,7 @@ export function InterestDetailHeader({ portSlug, interest }: InterestDetailHeade
interest.activeReminderCount === 1 ? '' : 's'
}`}
>
<AlarmClock className="size-3" />
<AlarmClock className="size-3" aria-hidden />
{interest.activeReminderCount}
</span>
) : null}
@@ -336,7 +336,7 @@ export function InterestDetailHeader({ portSlug, interest }: InterestDetailHeade
'hover:bg-foreground/5 disabled:opacity-50',
)}
>
<RefreshCcw className="size-3.5" />
<RefreshCcw className="size-3.5" aria-hidden />
Reopen
</button>
) : (
@@ -358,7 +358,7 @@ export function InterestDetailHeader({ portSlug, interest }: InterestDetailHeade
'hover:bg-emerald-100',
)}
>
<Trophy className="size-3.5" />
<Trophy className="size-3.5" aria-hidden />
<span className="hidden sm:inline">Mark won</span>
</button>
<button
@@ -372,7 +372,7 @@ export function InterestDetailHeader({ portSlug, interest }: InterestDetailHeade
'hover:bg-rose-50',
)}
>
<XCircle className="size-3.5" />
<XCircle className="size-3.5" aria-hidden />
<span className="hidden sm:inline">Close as lost</span>
</button>
</>
@@ -395,7 +395,7 @@ export function InterestDetailHeader({ portSlug, interest }: InterestDetailHeade
'hover:bg-foreground/5 hover:text-foreground',
)}
>
<Pencil className="size-4" />
<Pencil className="size-4" aria-hidden />
</button>
</PermissionGate>
<PermissionGate resource="interests" action="delete">
@@ -410,7 +410,11 @@ export function InterestDetailHeader({ portSlug, interest }: InterestDetailHeade
isArchived ? 'hover:text-foreground' : 'hover:text-destructive',
)}
>
{isArchived ? <RotateCcw className="size-4" /> : <Archive className="size-4" />}
{isArchived ? (
<RotateCcw className="size-4" aria-hidden />
) : (
<Archive className="size-4" aria-hidden />
)}
</button>
</PermissionGate>
</div>

View File

@@ -110,7 +110,7 @@ export function InterestDocumentsTab({ interestId }: InterestDocumentsTabProps)
emptyState={
<div className="flex flex-col items-center gap-3 rounded-lg border border-dashed border-border bg-muted/20 px-6 py-10 text-center">
<div className="flex size-10 items-center justify-center rounded-full bg-background text-muted-foreground">
<FileSignature className="size-5" />
<FileSignature className="size-5" aria-hidden />
</div>
<div className="space-y-1">
<p className="text-sm font-medium text-foreground">No documents yet</p>

View File

@@ -106,7 +106,7 @@ export function InterestEoiTab({ interestId, clientId }: InterestEoiTabProps) {
return (
<div className="space-y-5">
{docsLoading ? (
<Skeleton className="h-44 w-full rounded-lg" />
<Skeleton className="h-44 w-full rounded-lg" aria-hidden />
) : activeDoc ? (
<ActiveEoiCard
doc={activeDoc}
@@ -148,7 +148,7 @@ export function InterestEoiTab({ interestId, clientId }: InterestEoiTabProps) {
className="text-xs text-primary hover:underline inline-flex items-center gap-1"
>
Open
<ExternalLink className="size-3" />
<ExternalLink className="size-3" aria-hidden />
</Link>
)}
</li>
@@ -221,7 +221,7 @@ function ActiveEoiCard({
<header className="flex items-start justify-between gap-3">
<div className="min-w-0 flex-1 space-y-1">
<div className="flex items-center gap-2 flex-wrap">
<FileSignature className="size-4 text-foreground" />
<FileSignature className="size-4 text-foreground" aria-hidden />
<h2 className="truncate text-base font-semibold text-foreground">{doc.title}</h2>
<StatusBadge status={doc.status} />
</div>
@@ -250,7 +250,11 @@ function ActiveEoiCard({
onClick={() => remindAllMutation.mutate()}
className="gap-1.5 [&_svg]:size-3.5"
>
{remindAllMutation.isPending ? <Loader2 className="animate-spin" /> : <RefreshCw />}
{remindAllMutation.isPending ? (
<Loader2 className="animate-spin" aria-hidden />
) : (
<RefreshCw />
)}
Remind all
</Button>
)}
@@ -263,7 +267,7 @@ function ActiveEoiCard({
</h3>
{signersLoading ? (
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Loader2 className="size-3.5 animate-spin" /> Loading signers
<Loader2 className="size-3.5 animate-spin" aria-hidden /> Loading signers
</div>
) : signers.length === 0 ? (
<p className="text-sm text-muted-foreground italic">
@@ -276,7 +280,7 @@ function ActiveEoiCard({
<footer className="mt-3 flex flex-wrap items-center justify-between gap-2 text-xs text-muted-foreground">
<p className="flex items-center gap-1.5">
<AlertTriangle className="size-3 text-amber-600" />
<AlertTriangle className="size-3 text-amber-600" aria-hidden />
Reminders are rate-limited (max once per 7 days per signer).
</p>
<div className="flex items-center gap-1">
@@ -327,7 +331,7 @@ function EmptyEoiState({
return (
<section className="rounded-xl border border-dashed bg-muted/20 p-8 text-center">
<div className="mx-auto flex size-14 items-center justify-center rounded-full bg-background text-muted-foreground">
<FileSignature className="size-6" />
<FileSignature className="size-6" aria-hidden />
</div>
<h2 className="mt-4 text-base font-semibold text-foreground">
No EOI in flight for this interest
@@ -338,11 +342,11 @@ function EmptyEoiState({
</p>
<div className="mt-5 flex flex-wrap items-center justify-center gap-2">
<Button onClick={onGenerate} size="sm" className="gap-1.5">
<FileSignature className="size-4" />
<FileSignature className="size-4" aria-hidden />
Generate EOI
</Button>
<Button onClick={onUploadSigned} variant="outline" size="sm" className="gap-1.5">
<Upload className="size-4" />
<Upload className="size-4" aria-hidden />
Upload paper-signed copy
</Button>
</div>
@@ -359,7 +363,7 @@ function StatusBadge({ status }: { status: DocumentRow['status'] }) {
STATUS_TONES[status],
)}
>
{status === 'completed' && <CheckCircle2 className="mr-1 size-3" />}
{status === 'completed' && <CheckCircle2 className="mr-1 size-3" aria-hidden />}
{STATUS_LABELS[status]}
</Badge>
);

View File

@@ -278,7 +278,7 @@ export function InterestForm({ open, onOpenChange, defaultClientId, interest }:
disabled={isEdit}
>
{selectedClient?.label ?? interest?.clientName ?? 'Select client...'}
<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-(--radix-popper-anchor-width) min-w-[280px] p-0">
@@ -336,7 +336,7 @@ export function InterestForm({ open, onOpenChange, defaultClientId, interest }:
)}
>
{selectedBerth?.label ?? interest?.berthMooringNumber ?? 'Select berth...'}
<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-(--radix-popper-anchor-width) min-w-[280px] p-0">
@@ -398,7 +398,7 @@ export function InterestForm({ open, onOpenChange, defaultClientId, interest }:
className="h-7 text-xs"
onClick={() => setCreateYachtOpen(true)}
>
<Plus className="mr-1 h-3 w-3" />
<Plus className="mr-1 h-3 w-3" aria-hidden />
Add new
</Button>
)}
@@ -581,7 +581,7 @@ export function InterestForm({ open, onOpenChange, defaultClientId, interest }:
</Button>
<Button type="submit" disabled={isSubmitting || mutation.isPending}>
{(isSubmitting || mutation.isPending) && (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
<Loader2 className="mr-2 h-4 w-4 animate-spin" aria-hidden />
)}
{isEdit ? 'Save changes' : 'Create Interest'}
</Button>

View File

@@ -179,7 +179,7 @@ export function InterestList() {
className="rounded-none"
onClick={() => setViewMode('table')}
>
<LayoutList className="h-4 w-4" />
<LayoutList className="h-4 w-4" aria-hidden />
</Button>
<Button
size="sm"
@@ -187,12 +187,12 @@ export function InterestList() {
className="rounded-none"
onClick={() => setViewMode('board')}
>
<Kanban className="h-4 w-4" />
<Kanban className="h-4 w-4" aria-hidden />
</Button>
</div>
<PermissionGate resource="interests" action="create">
<Button size="sm" onClick={() => setCreateOpen(true)}>
<Plus className="mr-1.5 h-4 w-4" />
<Plus className="mr-1.5 h-4 w-4" aria-hidden />
New Interest
</Button>
</PermissionGate>
@@ -350,7 +350,7 @@ export function InterestList() {
aria-label="New interest"
className="fixed bottom-[calc(env(safe-area-inset-bottom)+86px)] right-4 z-40 inline-flex h-12 w-12 items-center justify-center rounded-full bg-primary text-primary-foreground shadow-lg transition-transform hover:scale-105 active:scale-95 lg:hidden"
>
<Plus className="h-6 w-6" />
<Plus className="h-6 w-6" aria-hidden />
</button>
</PermissionGate>

View File

@@ -83,9 +83,9 @@ export function InterestOutcomeDialog({ interestId, open, onOpenChange, mode }:
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
{mode === 'won' ? (
<Trophy className="h-4 w-4 text-emerald-600" />
<Trophy className="h-4 w-4 text-emerald-600" aria-hidden />
) : (
<XCircle className="h-4 w-4 text-rose-600" />
<XCircle className="h-4 w-4 text-rose-600" aria-hidden />
)}
{mode === 'won' ? 'Mark interest as won' : 'Close interest as lost'}
</DialogTitle>
@@ -148,7 +148,7 @@ export function InterestOutcomeDialog({ interestId, open, onOpenChange, mode }:
: 'bg-rose-600 hover:bg-rose-700'
}
>
{mutation.isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
{mutation.isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" aria-hidden />}
{mode === 'won' ? 'Mark as won' : 'Close as lost'}
</Button>
</DialogFooter>

View File

@@ -73,7 +73,7 @@ export function InterestPicker({
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

@@ -109,7 +109,7 @@ export function InterestReservationTab({
return (
<div className="space-y-5">
{docsLoading ? (
<Skeleton className="h-44 w-full rounded-lg" />
<Skeleton className="h-44 w-full rounded-lg" aria-hidden />
) : activeDoc ? (
<ActiveReservationCard
doc={activeDoc}
@@ -148,7 +148,7 @@ export function InterestReservationTab({
className="text-xs text-primary hover:underline inline-flex items-center gap-1"
>
Open
<ExternalLink className="size-3" />
<ExternalLink className="size-3" aria-hidden />
</Link>
)}
</li>
@@ -235,7 +235,7 @@ function ActiveReservationCard({
<header className="flex items-start justify-between gap-3">
<div className="min-w-0 flex-1 space-y-1">
<div className="flex items-center gap-2 flex-wrap">
<FileSignature className="size-4 text-foreground" />
<FileSignature className="size-4 text-foreground" aria-hidden />
<h2 className="truncate text-base font-semibold text-foreground">{doc.title}</h2>
<StatusBadge status={doc.status} />
</div>
@@ -264,7 +264,11 @@ function ActiveReservationCard({
onClick={() => remindAllMutation.mutate()}
className="gap-1.5 [&_svg]:size-3.5"
>
{remindAllMutation.isPending ? <Loader2 className="animate-spin" /> : <RefreshCw />}
{remindAllMutation.isPending ? (
<Loader2 className="animate-spin" aria-hidden />
) : (
<RefreshCw />
)}
Remind all
</Button>
)}
@@ -277,7 +281,7 @@ function ActiveReservationCard({
</h3>
{signersLoading ? (
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Loader2 className="size-3.5 animate-spin" /> Loading signers
<Loader2 className="size-3.5 animate-spin" aria-hidden /> Loading signers
</div>
) : signers.length === 0 ? (
<p className="text-sm text-muted-foreground italic">
@@ -290,7 +294,7 @@ function ActiveReservationCard({
<footer className="mt-3 flex flex-wrap items-center justify-between gap-2 text-xs text-muted-foreground">
<p className="flex items-center gap-1.5">
<AlertTriangle className="size-3 text-amber-600" />
<AlertTriangle className="size-3 text-amber-600" aria-hidden />
Reminders are rate-limited (max once per 7 days per signer).
</p>
<div className="flex items-center gap-1">
@@ -341,7 +345,7 @@ function EmptyReservationState({
return (
<section className="rounded-xl border border-dashed bg-muted/20 p-8 text-center">
<div className="mx-auto flex size-14 items-center justify-center rounded-full bg-background text-muted-foreground">
<FileSignature className="size-6" />
<FileSignature className="size-6" aria-hidden />
</div>
<h2 className="mt-4 text-base font-semibold text-foreground">
No reservation in flight for this interest
@@ -352,11 +356,11 @@ function EmptyReservationState({
</p>
<div className="mt-5 flex flex-wrap items-center justify-center gap-2">
<Button onClick={onUploadForSigning} size="sm" className="gap-1.5">
<FileSignature className="size-4" />
<FileSignature className="size-4" aria-hidden />
Upload draft for signing
</Button>
<Button onClick={onUploadSigned} variant="outline" size="sm" className="gap-1.5">
<Upload className="size-4" />
<Upload className="size-4" aria-hidden />
Upload paper-signed copy
</Button>
</div>
@@ -375,7 +379,7 @@ function StatusBadge({ status }: { status: DocumentRow['status'] }) {
STATUS_TONES[status],
)}
>
{status === 'completed' && <CheckCircle2 className="mr-1 size-3" />}
{status === 'completed' && <CheckCircle2 className="mr-1 size-3" aria-hidden />}
{STATUS_LABELS[status]}
</Badge>
);

View File

@@ -115,7 +115,7 @@ export function InterestStagePicker({
: 'border-red-300 bg-red-50 text-red-900'
}`}
>
<AlertTriangle className="h-4 w-4 mt-0.5 shrink-0" />
<AlertTriangle className="h-4 w-4 mt-0.5 shrink-0" aria-hidden />
{canOverride ? (
<span>
This is not a normal forward transition. Override is enabled supply a reason
@@ -175,7 +175,7 @@ export function InterestStagePicker({
!reasonValid
}
>
{mutation.isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
{mutation.isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" aria-hidden />}
{overrideEffective ? 'Override stage' : 'Confirm'}
</Button>
</DialogFooter>

View File

@@ -340,7 +340,7 @@ function MilestoneSection({
return (
<li key={step.label} className="flex items-start gap-2 text-sm">
{done ? (
<CheckCircle2 className="mt-0.5 size-4 shrink-0 text-emerald-600" />
<CheckCircle2 className="mt-0.5 size-4 shrink-0 text-emerald-600" aria-hidden />
) : (
<Circle
className={cn(
@@ -650,7 +650,7 @@ function OverviewTab({
<div className="flex flex-wrap items-center gap-x-3 gap-y-1.5">
<Button asChild size="sm" className="h-7 px-2.5 text-xs">
<Link href={`/${portSlug}/invoices/new?interestId=${interestId}&kind=deposit`}>
<Plus className="size-3.5" />
<Plus className="size-3.5" aria-hidden />
Create deposit invoice
</Link>
</Button>
@@ -710,7 +710,7 @@ function OverviewTab({
<span className="text-[10px] font-semibold uppercase tracking-wide">Past</span>
{pastMilestones.map((m) => (
<span key={m.key} className="inline-flex items-center gap-1.5">
<CheckCircle2 className="size-3 text-emerald-600" />
<CheckCircle2 className="size-3 text-emerald-600" aria-hidden />
<span className="font-medium text-foreground">{m.title}</span>
<span>·</span>
<span>{m.pastSummary}</span>

View File

@@ -46,17 +46,23 @@ function eventIcon(event: TimelineEvent) {
if (type === 'outcome_set') {
const outcome = (event.metadata as Record<string, unknown>).outcome as string | undefined;
if (outcome === 'won') return <Trophy className="h-4 w-4 text-emerald-600" />;
if (outcome && LOST_OUTCOMES.has(outcome)) return <XCircle className="h-4 w-4 text-rose-600" />;
return <XCircle className="h-4 w-4 text-rose-600" />;
if (outcome === 'won') return <Trophy className="h-4 w-4 text-emerald-600" aria-hidden />;
if (outcome && LOST_OUTCOMES.has(outcome))
return <XCircle className="h-4 w-4 text-rose-600" aria-hidden />;
return <XCircle className="h-4 w-4 text-rose-600" aria-hidden />;
}
if (type === 'outcome_cleared') return <RefreshCcw className="h-4 w-4 text-blue-500" />;
if (event.type === 'document_event') return <FileText className="h-4 w-4 text-sky-600" />;
if (event.action === 'create') return <PlusCircle className="h-4 w-4 text-green-500" />;
if (event.action === 'archive') return <Archive className="h-4 w-4 text-orange-500" />;
if (event.action === 'restore') return <RotateCcw className="h-4 w-4 text-blue-500" />;
if (type === 'stage_change') return <Clock className="h-4 w-4 text-purple-500" />;
return <Pencil className="h-4 w-4 text-muted-foreground" />;
if (type === 'outcome_cleared')
return <RefreshCcw className="h-4 w-4 text-blue-500" aria-hidden />;
if (event.type === 'document_event')
return <FileText className="h-4 w-4 text-sky-600" aria-hidden />;
if (event.action === 'create')
return <PlusCircle className="h-4 w-4 text-green-500" aria-hidden />;
if (event.action === 'archive')
return <Archive className="h-4 w-4 text-orange-500" aria-hidden />;
if (event.action === 'restore')
return <RotateCcw className="h-4 w-4 text-blue-500" aria-hidden />;
if (type === 'stage_change') return <Clock className="h-4 w-4 text-purple-500" aria-hidden />;
return <Pencil className="h-4 w-4 text-muted-foreground" aria-hidden />;
}
function actorLabel(event: TimelineEvent): string | null {
@@ -121,7 +127,7 @@ export function InterestTimeline({ interestId }: InterestTimelineProps) {
{event.description}
{isAuto ? (
<span className="ml-2 inline-flex items-center gap-1 rounded-full bg-muted px-1.5 py-0.5 align-middle text-[10px] font-medium uppercase tracking-wide text-muted-foreground">
<Bot className="h-3 w-3" />
<Bot className="h-3 w-3" aria-hidden />
Auto
</span>
) : null}

View File

@@ -220,7 +220,7 @@ function BypassDialog({ row, open, onOpenChange, onSubmit, isPending }: BypassDi
onClick={() => onSubmit(reason.trim().length > 0 ? reason.trim() : null)}
disabled={isPending || reason.trim().length === 0}
>
{isPending ? <Loader2 className="mr-1.5 size-3.5 animate-spin" /> : null}
{isPending ? <Loader2 className="mr-1.5 size-3.5 animate-spin" aria-hidden /> : null}
Save bypass
</Button>
</DialogFooter>
@@ -266,7 +266,7 @@ function LinkedBerthRowItem({ row, portSlug, eoiStatus, onUpdate, onRemove, isPe
<StatusPill status={statusToPill(row.status)}>{formatStatus(row.status)}</StatusPill>
{row.isPrimary ? (
<span className="inline-flex items-center gap-1 rounded-md border border-brand-200 bg-brand-50 px-2 py-0.5 text-xs font-medium text-brand-800">
<Star className="size-3" />
<Star className="size-3" aria-hidden />
Primary
</span>
) : null}
@@ -287,7 +287,7 @@ function LinkedBerthRowItem({ row, portSlug, eoiStatus, onUpdate, onRemove, isPe
onClick={() => onUpdate(row.berthId, { isPrimary: true })}
disabled={isPending}
>
<Star className="mr-1.5 size-3.5" />
<Star className="mr-1.5 size-3.5" aria-hidden />
Set as primary
</Button>
) : null}
@@ -300,7 +300,7 @@ function LinkedBerthRowItem({ row, portSlug, eoiStatus, onUpdate, onRemove, isPe
className="text-destructive hover:text-destructive"
aria-label={`Remove berth ${row.mooringNumber ?? row.berthId}`}
>
<Trash2 className="size-3.5" />
<Trash2 className="size-3.5" aria-hidden />
</Button>
</div>
</div>
@@ -334,7 +334,7 @@ function LinkedBerthRowItem({ row, portSlug, eoiStatus, onUpdate, onRemove, isPe
className="inline-flex h-5 w-5 items-center justify-center rounded-full text-muted-foreground hover:bg-muted/60 hover:text-foreground"
aria-label="What does Specifically pitching do?"
>
<HelpCircle className="h-3.5 w-3.5" />
<HelpCircle className="h-3.5 w-3.5" aria-hidden />
</button>
</TooltipTrigger>
<TooltipContent side="top" className="max-w-xs text-[11px] leading-snug">
@@ -369,7 +369,7 @@ function LinkedBerthRowItem({ row, portSlug, eoiStatus, onUpdate, onRemove, isPe
className="inline-flex h-5 w-5 items-center justify-center rounded-full text-muted-foreground hover:bg-muted/60 hover:text-foreground"
aria-label="What does Mark in EOI bundle do?"
>
<HelpCircle className="h-3.5 w-3.5" />
<HelpCircle className="h-3.5 w-3.5" aria-hidden />
</button>
</TooltipTrigger>
<TooltipContent side="top" className="max-w-xs text-[11px] leading-snug">
@@ -484,7 +484,7 @@ export function LinkedBerthsList({ interestId }: LinkedBerthsListProps) {
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-base">
<Anchor className="size-4 text-brand-600" />
<Anchor className="size-4 text-brand-600" aria-hidden />
Linked berths{rows.length > 0 ? ` (${rows.length})` : ''}
</CardTitle>
</CardHeader>

View File

@@ -58,9 +58,9 @@ export function RecommendationList({ interestId }: RecommendationListProps) {
disabled={generateMutation.isPending}
>
{generateMutation.isPending ? (
<Loader2 className="mr-1.5 h-4 w-4 animate-spin" />
<Loader2 className="mr-1.5 h-4 w-4 animate-spin" aria-hidden />
) : (
<Sparkles className="mr-1.5 h-4 w-4" />
<Sparkles className="mr-1.5 h-4 w-4" aria-hidden />
)}
Generate Recommendations
</Button>

View File

@@ -28,7 +28,7 @@ export function SendFromInterestButton({
return (
<>
<Button variant="outline" size="sm" onClick={() => setOpen(true)}>
<Send className="mr-2 h-4 w-4" /> Send documents
<Send className="mr-2 h-4 w-4" aria-hidden /> Send documents
</Button>
<SendDocumentsDialog
open={open}

View File

@@ -22,7 +22,7 @@ export function StageLegend() {
className="h-8 gap-1.5 text-muted-foreground"
aria-label="What do the colors mean?"
>
<Info className="h-3.5 w-3.5" />
<Info className="h-3.5 w-3.5" aria-hidden />
<span className="hidden sm:inline">Legend</span>
</Button>
</PopoverTrigger>