'use client'; import Link from 'next/link'; import { type ReactNode } from 'react'; import { cn } from '@/lib/utils'; interface ListCardProps { /** Detail-page URL the card navigates to when tapped. */ href: string; /** * Optional Tailwind background class painted on a 3px vertical strip on the * left edge — used to encode pipeline stage / status / category at a glance. * Pass `undefined` for entities with no status to surface (clients, etc.). */ accentClassName?: string; /** * Top-right action slot — typically a `` for edit/archive. * Rendered absolutely-positioned outside the navigation Link so its clicks * don't trigger detail navigation. */ actions?: ReactNode; ariaLabel: string; className?: string; children: ReactNode; } /** * Shared shell for every mobile list card. Wraps the body in a Link to the * detail page, paints an optional status accent bar on the left edge, and * exposes a top-right slot for an actions menu. Touch/hover feedback comes * from a soft `hover:bg-muted/30` + `active:bg-muted/50` tint, no shadow * shifts (which feel jittery on mobile). */ export function ListCard({ href, accentClassName, actions, ariaLabel, className, children, }: ListCardProps) { return (
{accentClassName ? ( ) : null} {children} {actions ?
{actions}
: null}
); } interface ListCardAvatarProps { /** Two-letter initials (or one for single-word names). Caller derives. */ initials?: string; /** Domain icon (Lucide). Used when the entity isn't a person — yacht, berth, company. */ icon?: ReactNode; className?: string; } /** * 40px lead-slot avatar. Pass `initials` for people-shaped entities, or * `icon` for non-person entities (yachts, berths, companies, expenses). * Uses the brand-soft background so it reads as part of the marina aesthetic * rather than a generic Material avatar. */ export function ListCardAvatar({ initials, icon, className }: ListCardAvatarProps) { return (
{icon ?? initials ?? '?'}
); } interface ListCardMetaProps { /** Optional Lucide icon, rendered at 12px next to the text. */ icon?: ReactNode; children: ReactNode; className?: string; } /** * Single inline meta segment: tiny icon (optional) + muted text. Compose * multiple segments inside a `
` to build the meta line. */ export function ListCardMeta({ icon, children, className }: ListCardMetaProps) { return ( {icon ? {icon} : null} {children} ); } /** * Derive 1–2 letter initials from a name, ignoring purely-numeric tokens * (so "Recovery Test 1777" → "RT", not "R1"). Returns "?" only for empty * input. Centralised here so every list card uses the same logic. */ export function deriveInitials(name: string): string { const alphaParts = name .trim() .split(/\s+/) .filter((p) => /^[A-Za-z]/.test(p)); if (alphaParts.length === 0) return name.trim().slice(0, 1).toUpperCase() || '?'; if (alphaParts.length === 1) return (alphaParts[0]?.[0] ?? '?').toUpperCase(); return ((alphaParts[0]?.[0] ?? '') + (alphaParts[1]?.[0] ?? '')).toUpperCase(); }