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>
Adds smooth fade+slide animations when list items enter/leave on the
three highest-visibility realtime surfaces:
- alert-rail.tsx — socket-driven alerts appearing / dismissed.
- my-reminders-rail.tsx — reminders completed / arriving via realtime.
- notes-list.tsx — notes added / edited / deleted.
One-line `useAutoAnimate()` hook per site, no CSS, ~2kb gzip. Replaces
the jarring "row just appears/disappears" pattern with a per-item
transition.
Skipped on pipeline-board (kanban) — combining auto-animate with
@dnd-kit's SortableContext causes double-animation glitches because
both libraries fight to animate the same layout shift.
Verified: tsc clean, vitest 1293/1293 pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mobile + responsive
- berth-form full-width on phones (was 480px fixed → overflowed iPhone)
- currency-input switched to inputMode=decimal with live thousands separator
- client-form Country/Timezone/Source/Preferred-Contact full-width <sm
- contacts row restructured so Primary toggle + Remove get their own strip
- customize-dashboard footer stacks vertically on mobile; Done full-width
- interest-form client/berth pickers no longer cmdk-filter on UUID (typing
"Carlos" now returns Carlos Vega instead of "No clients found")
Data + consistency
- SOURCES + SOURCE_LABELS + formatSource() in lib/constants; 9 surfaces
now resolve interest/client source from one place
- INTEREST_OUTCOMES adds lost_other (picker, badge, timeline)
- Berth options natural-sort A1 → A2 → … → A10 via lib/utils/mooring-sort
- archiver downgraded ^8 → ^7.0.1 so the GDPR export route compiles
- TableBody last-row uses border-b-0 (not border-0); colored left-accent
on the bottom berth row now renders
- Hide Invite-to-Portal until port setting === true (was !== false default-show)
- OwnerPicker primer query resolves entity name on first paint (no more
UUID flash before the popover opens)
Terminology
- Replaced user-facing "Documenso" with "signing service" / "Generated EOI" /
"Manual EOI" in 8 components (admin/internal references kept)
- Plainer status-change copy on berth-detail-header
Forms + editing
- InlineEditableField gained a `date` variant (native picker); applied to
company incorporation date and ready for other YYYY-MM-DD plaintext fields
- Inline source picker on interest-tabs detail (was free text)
- TagPicker self-hides when port has no tags AND nothing is selected
- New ReminderDaysInput with preset chips (1d / 3d / 1wk / 2wk / 1mo / custom)
- Compose dialog follow-up is now a toggle that reveals datetime picker
Pipeline milestones
- changeStageSchema accepts optional milestoneDate; service stamps it on the
matching date column instead of always using now
- MilestoneAdvanceButton popover collects a back-date before stage advance
- Applied to every "Mark X manually" surface on the interest overview
EOI / linked-berths polish
- Add-bypass row aligned inline with toggle descriptions
- Tooltips on "Specifically pitching" / "Mark in EOI bundle" explain their
legal vs. public-map consequences
Surfaces
- Companies list now has the column picker + persisted hidden-column prefs
- NotesList aggregate flag enabled on clients, companies, residential_clients
(yachts already aggregated)
ft/m unit toggle (interim, before drift fix)
- "Berth size desired" gets a section-level ft/m toggle; per-field hint shows
the converted value. Storage stays canonical-ft for now; the drift-safe
persistence migration is the next step.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
DateRangePicker grows a "Custom range" mode (From/To inputs capped
at today, mutually-bounded so From <= To). dashboard-shell threads
the range through to /api/v1/analytics, which validates calendar
dates via ISO round-trip and enforces a 365-day cap as a backstop
against the occupancy timeline N+1.
KpiCards now gates its query on currentPortId so the early
unhydrated-store fetch can't cache a zeroed/error response and
display "-" until staleTime expires.
MyRemindersRail drops xl:h-full so the rail no longer stretches
past its grid row and overlaps ActivityFeed below.
useRealtimeInvalidation switches to partial-prefix queryKeys so a
realtime mutation invalidates every cached range bucket at once
instead of just the one currently visible.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Alerts rail and Reminders rail were using h-full unconditionally, which
worked at xl: where the dashboard grid pairs them with a sibling chart
column, but produced weirdly stretched empty cards in the single-column
mobile stack (no fixed-height context to fill).
alert-rail / my-reminders-rail: h-full -> xl:h-full
KPI tiles + skeleton rendered the same desktop padding (p-5) and font sizes
on phone, leaving the value cramped against a wide white frame. Tighter
mobile defaults that scale up at sm:
KPITile p-3 sm:p-5, label text-[10px] sm:text-xs,
value mt-1 text-lg sm:mt-2 sm:text-2xl, value truncates
KpiTileSkeleton: matching p-3 sm:p-5 + smaller skeleton bars on mobile
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sales-CRM workflow batch — closes audit recommendations #2, #3, #4, #6,
#7, #8, #9, #10, #13, #15. Skips #11 (My-pipeline filter — needs a real
assignee column on interests, defer until ownership model lands) and #12
(keyboard shortcuts — explicit user call).
Interest list (the rep's main triage surface):
- Last activity column replaces Created (sortable by
dateLastContact). Postgres NULLs-last on DESC means
never-contacted leads sort to the bottom — exactly the right
triage default.
- Comment-icon next to client name when notesCount > 0, with a
tooltip showing the count. Cheap, glanceable signal that the
lead has correspondence to peek at.
- Urgency badges under stage when criteria fire: "Silent Nd"
for mid-funnel interests with no contact in 7+ days,
"EOI Nd" for EOIs awaiting signature 14+ days, "Deposit Nd"
for eoi_signed interests with no deposit after 21 days.
Pure derived — no extra fetch, computed from the dates the
row already returns.
- Bulk select checkbox column with bulk-archive (existing
DataTable.bulkActions API; just wired with a confirm-dialog
and a Promise.all fan-out).
- Mobile FAB (+) for new interest, anchored above the bottom-tab
bar with safe-area inset awareness.
All four signals mirrored on the mobile InterestCard (comment
icon, urgency badges, last-activity footer).
Interest detail:
- Reminder bell badge in the header showing pending/snoozed
reminder count linked to the interest. Surfaced via
getInterestById's new `activeReminderCount`.
- "Latest note" teaser on the Overview tab — truncated 3-line
preview of the most recent threaded note + relative time +
"View all" link to the Notes tab. Saves a click for the
common "what was discussed last?" peek.
- Color-block swatches in InlineStagePicker dropdown (rounded-sm
mini-bars in the stage's progressive saturation color, replacing
the previous tiny dots). Reads as a visual scan instead of a
list.
Dashboard:
- MyRemindersRail on the right sidebar above the existing
AlertRail. Shows pending+snoozed reminders for the current
user (overdue first), each with priority pill, relative due
time, and click-through to the linked interest/client/berth.
Berth detail:
- BerthInterestPulse card at the top of the Overview tab,
replacing the old "buried in tab" pattern. Shows up to 5
active interests with avatar, stage pill, urgency badges, and
last-activity. Mirrors the old Nuxt CRM's beloved "Interested
Parties" panel but with the new triage signals.
Realtime toasts:
- New <RealtimeToasts /> mounted inside SocketProvider in the
dashboard layout. Subscribes to interest:stageChanged,
document:completed, document:signer:signed, and
interest:outcomeSet — fires sonner toasts so reps watching any
page learn about pipeline events without refreshing.
Service layer:
- listInterests: notesCount per row (left join + count + groupBy).
- getInterestById: clientPrimaryPhone + clientPrimaryPhoneE164
(for the Email/Call/WhatsApp buttons added last commit; phone
pieces were missing), notesCount, recentNote, activeReminderCount.
- sortColumn switch handles 'dateLastContact' explicitly; default
stays 'updatedAt'.
tsc clean. vitest 835/835 pass. ESLint clean on every file touched.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>