style(detail): apply gradient header strip to client/interest/yacht/company/berth/residential/invoice details

Adds shared <DetailHeaderStrip> wrapper (rounded-xl + gradient-brand-soft + shadow-xs)
and applies it to every legacy domain detail header. Residential client/interest and
invoice detail get an inline gradient strip with eyebrow ('Residential Client',
'Residential Interest', 'Invoice'). Residential bodies normalized to lg:grid-cols-[2fr_1fr]
per spec.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matt Ciaccio
2026-04-28 12:09:47 +02:00
parent cda44e721b
commit 22f944fde2
9 changed files with 81 additions and 59 deletions

View File

@@ -24,6 +24,7 @@ import {
SelectValue, SelectValue,
} from '@/components/ui/select'; } from '@/components/ui/select';
import { Textarea } from '@/components/ui/textarea'; import { Textarea } from '@/components/ui/textarea';
import { DetailHeaderStrip } from '@/components/shared/detail-header-strip';
import { PermissionGate } from '@/components/shared/permission-gate'; import { PermissionGate } from '@/components/shared/permission-gate';
import { BerthForm } from './berth-form'; import { BerthForm } from './berth-form';
import { apiFetch } from '@/lib/api/client'; import { apiFetch } from '@/lib/api/client';
@@ -94,7 +95,7 @@ function StatusChangeDialog({
formState: { isSubmitting }, formState: { isSubmitting },
} = useForm<UpdateBerthStatusInput>({ } = useForm<UpdateBerthStatusInput>({
resolver: zodResolver(updateBerthStatusSchema), resolver: zodResolver(updateBerthStatusSchema),
defaultValues: { status: currentStatus as typeof BERTH_STATUSES[number], reason: '' }, defaultValues: { status: currentStatus as (typeof BERTH_STATUSES)[number], reason: '' },
}); });
const status = watch('status'); const status = watch('status');
@@ -127,7 +128,7 @@ function StatusChangeDialog({
<Label>New Status</Label> <Label>New Status</Label>
<Select <Select
value={status} value={status}
onValueChange={(v) => setValue('status', v as typeof BERTH_STATUSES[number])} onValueChange={(v) => setValue('status', v as (typeof BERTH_STATUSES)[number])}
> >
<SelectTrigger> <SelectTrigger>
<SelectValue /> <SelectValue />
@@ -143,11 +144,7 @@ function StatusChangeDialog({
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label>Reason *</Label> <Label>Reason *</Label>
<Textarea <Textarea {...register('reason')} placeholder="Reason for status change..." rows={3} />
{...register('reason')}
placeholder="Reason for status change..."
rows={3}
/>
</div> </div>
<DialogFooter> <DialogFooter>
<Button type="button" variant="outline" onClick={() => onOpenChange(false)}> <Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
@@ -169,22 +166,18 @@ export function BerthDetailHeader({ berth }: BerthDetailHeaderProps) {
return ( return (
<> <>
<div className="space-y-3"> <DetailHeaderStrip>
<div className="flex items-start gap-4"> <div className="flex items-start gap-4">
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center gap-3 flex-wrap"> <div className="flex items-center gap-3 flex-wrap">
<h1 className="text-2xl font-bold text-foreground"> <h1 className="text-2xl font-bold text-foreground">Berth {berth.mooringNumber}</h1>
Berth {berth.mooringNumber}
</h1>
<span <span
className={`inline-flex items-center rounded-full border px-3 py-1 text-sm font-medium ${STATUS_COLORS[berth.status] ?? 'bg-muted text-muted-foreground border-muted'}`} className={`inline-flex items-center rounded-full border px-3 py-1 text-sm font-medium ${STATUS_COLORS[berth.status] ?? 'bg-muted text-muted-foreground border-muted'}`}
> >
{STATUS_LABELS[berth.status] ?? berth.status} {STATUS_LABELS[berth.status] ?? berth.status}
</span> </span>
</div> </div>
{berth.area && ( {berth.area && <p className="text-muted-foreground mt-1">{berth.area}</p>}
<p className="text-muted-foreground mt-1">{berth.area}</p>
)}
</div> </div>
<div className="flex items-center gap-2 shrink-0"> <div className="flex items-center gap-2 shrink-0">
@@ -200,7 +193,7 @@ export function BerthDetailHeader({ berth }: BerthDetailHeaderProps) {
</PermissionGate> </PermissionGate>
</div> </div>
</div> </div>
</div> </DetailHeaderStrip>
<BerthForm berth={berth} open={editOpen} onOpenChange={setEditOpen} /> <BerthForm berth={berth} open={editOpen} onOpenChange={setEditOpen} />

View File

@@ -8,6 +8,7 @@ import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { TagBadge } from '@/components/shared/tag-badge'; import { TagBadge } from '@/components/shared/tag-badge';
import { ArchiveConfirmDialog } from '@/components/shared/archive-confirm-dialog'; import { ArchiveConfirmDialog } from '@/components/shared/archive-confirm-dialog';
import { DetailHeaderStrip } from '@/components/shared/detail-header-strip';
import { PortalInviteButton } from '@/components/clients/portal-invite-button'; import { PortalInviteButton } from '@/components/clients/portal-invite-button';
import { apiFetch } from '@/lib/api/client'; import { apiFetch } from '@/lib/api/client';
@@ -68,7 +69,7 @@ export function ClientDetailHeader({ client }: ClientDetailHeaderProps) {
return ( return (
<> <>
<div className="space-y-3"> <DetailHeaderStrip>
<div className="flex items-start gap-3 flex-wrap"> <div className="flex items-start gap-3 flex-wrap">
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap"> <div className="flex items-center gap-2 flex-wrap">
@@ -140,7 +141,7 @@ export function ClientDetailHeader({ client }: ClientDetailHeaderProps) {
</Button> </Button>
</div> </div>
</div> </div>
</div> </DetailHeaderStrip>
<ArchiveConfirmDialog <ArchiveConfirmDialog
open={archiveOpen} open={archiveOpen}

View File

@@ -9,6 +9,7 @@ import { toast } from 'sonner';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { ArchiveConfirmDialog } from '@/components/shared/archive-confirm-dialog'; import { ArchiveConfirmDialog } from '@/components/shared/archive-confirm-dialog';
import { DetailHeaderStrip } from '@/components/shared/detail-header-strip';
import { PermissionGate } from '@/components/shared/permission-gate'; import { PermissionGate } from '@/components/shared/permission-gate';
import { CompanyForm } from '@/components/companies/company-form'; import { CompanyForm } from '@/components/companies/company-form';
import { apiFetch } from '@/lib/api/client'; import { apiFetch } from '@/lib/api/client';
@@ -74,7 +75,7 @@ export function CompanyDetailHeader({ company }: CompanyDetailHeaderProps) {
return ( return (
<> <>
<div className="space-y-3"> <DetailHeaderStrip>
<div className="flex items-start gap-3 flex-wrap"> <div className="flex items-start gap-3 flex-wrap">
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap"> <div className="flex items-center gap-2 flex-wrap">
@@ -118,7 +119,7 @@ export function CompanyDetailHeader({ company }: CompanyDetailHeaderProps) {
</PermissionGate> </PermissionGate>
</div> </div>
</div> </div>
</div> </DetailHeaderStrip>
<CompanyForm <CompanyForm
open={editOpen} open={editOpen}

View File

@@ -9,6 +9,7 @@ import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { TagBadge } from '@/components/shared/tag-badge'; import { TagBadge } from '@/components/shared/tag-badge';
import { ArchiveConfirmDialog } from '@/components/shared/archive-confirm-dialog'; import { ArchiveConfirmDialog } from '@/components/shared/archive-confirm-dialog';
import { DetailHeaderStrip } from '@/components/shared/detail-header-strip';
import { PermissionGate } from '@/components/shared/permission-gate'; import { PermissionGate } from '@/components/shared/permission-gate';
import { InterestForm } from '@/components/interests/interest-form'; import { InterestForm } from '@/components/interests/interest-form';
import { InterestStagePicker } from '@/components/interests/interest-stage-picker'; import { InterestStagePicker } from '@/components/interests/interest-stage-picker';
@@ -70,8 +71,7 @@ export function InterestDetailHeader({ portSlug, interest }: InterestDetailHeade
const isArchived = !!interest.archivedAt; const isArchived = !!interest.archivedAt;
const archiveMutation = useMutation({ const archiveMutation = useMutation({
mutationFn: () => mutationFn: () => apiFetch(`/api/v1/interests/${interest.id}`, { method: 'DELETE' }),
apiFetch(`/api/v1/interests/${interest.id}`, { method: 'DELETE' }),
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['interests', interest.id] }); queryClient.invalidateQueries({ queryKey: ['interests', interest.id] });
queryClient.invalidateQueries({ queryKey: ['interests'] }); queryClient.invalidateQueries({ queryKey: ['interests'] });
@@ -80,8 +80,7 @@ export function InterestDetailHeader({ portSlug, interest }: InterestDetailHeade
}); });
const restoreMutation = useMutation({ const restoreMutation = useMutation({
mutationFn: () => mutationFn: () => apiFetch(`/api/v1/interests/${interest.id}/restore`, { method: 'POST' }),
apiFetch(`/api/v1/interests/${interest.id}/restore`, { method: 'POST' }),
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['interests', interest.id] }); queryClient.invalidateQueries({ queryKey: ['interests', interest.id] });
queryClient.invalidateQueries({ queryKey: ['interests'] }); queryClient.invalidateQueries({ queryKey: ['interests'] });
@@ -91,7 +90,7 @@ export function InterestDetailHeader({ portSlug, interest }: InterestDetailHeade
return ( return (
<> <>
<div className="space-y-3"> <DetailHeaderStrip>
<div className="flex items-start gap-3 flex-wrap"> <div className="flex items-start gap-3 flex-wrap">
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap"> <div className="flex items-center gap-2 flex-wrap">
@@ -99,7 +98,9 @@ export function InterestDetailHeader({ portSlug, interest }: InterestDetailHeade
{interest.clientName ?? 'Unknown Client'} {interest.clientName ?? 'Unknown Client'}
</h1> </h1>
{isArchived && ( {isArchived && (
<Badge variant="secondary" className="text-xs">Archived</Badge> <Badge variant="secondary" className="text-xs">
Archived
</Badge>
)} )}
<span <span
className={`inline-flex items-center rounded-full px-2.5 py-0.5 text-sm font-medium ${STAGE_COLORS[interest.pipelineStage] ?? 'bg-gray-100 text-gray-700'}`} className={`inline-flex items-center rounded-full px-2.5 py-0.5 text-sm font-medium ${STAGE_COLORS[interest.pipelineStage] ?? 'bg-gray-100 text-gray-700'}`}
@@ -130,8 +131,7 @@ export function InterestDetailHeader({ portSlug, interest }: InterestDetailHeade
)} )}
{interest.source && ( {interest.source && (
<span> <span>
Source:{' '} Source: <span className="text-foreground capitalize">{interest.source}</span>
<span className="text-foreground capitalize">{interest.source}</span>
</span> </span>
)} )}
</div> </div>
@@ -176,7 +176,7 @@ export function InterestDetailHeader({ portSlug, interest }: InterestDetailHeade
</PermissionGate> </PermissionGate>
</div> </div>
</div> </div>
</div> </DetailHeaderStrip>
<InterestForm <InterestForm
open={editOpen} open={editOpen}

View File

@@ -85,15 +85,18 @@ export function InvoiceDetail({ invoiceId }: InvoiceDetailProps) {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{/* Header */} {/* Header */}
<div className="flex items-center justify-between"> <div className="rounded-xl border border-slate-200 bg-gradient-brand-soft px-5 py-4 shadow-xs flex items-center justify-between gap-4 flex-wrap">
<div> <div className="min-w-0">
<div className="flex items-center gap-3"> <div className="text-xs font-semibold uppercase tracking-wide text-brand">Invoice</div>
<h2 className="text-xl font-semibold font-mono">{invoice.invoiceNumber}</h2> <div className="mt-1 flex items-center gap-3 flex-wrap">
<h1 className="truncate text-2xl font-bold tracking-tight text-foreground font-mono">
{invoice.invoiceNumber}
</h1>
<Badge variant="outline" className={`capitalize text-sm border ${statusColor}`}> <Badge variant="outline" className={`capitalize text-sm border ${statusColor}`}>
{invoice.status} {invoice.status}
</Badge> </Badge>
</div> </div>
<p className="text-sm text-muted-foreground mt-0.5">{invoice.clientName}</p> <p className="text-sm text-muted-foreground mt-1">{invoice.clientName}</p>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{invoice.status === 'draft' && ( {invoice.status === 'draft' && (

View File

@@ -113,14 +113,17 @@ export function ResidentialClientDetail({ clientId }: { clientId: string }) {
</Link> </Link>
</div> </div>
<div className="rounded-lg border bg-card p-6 space-y-6"> <div className="rounded-xl border border-slate-200 bg-gradient-brand-soft px-5 py-4 shadow-xs">
<div> <div className="text-xs font-semibold uppercase tracking-wide text-brand">
<h1 className="text-2xl font-semibold"> Residential Client
</div>
<h1 className="mt-1 truncate text-2xl font-bold tracking-tight text-foreground">
<InlineEditableField value={client.fullName} onSave={save('fullName')} /> <InlineEditableField value={client.fullName} onSave={save('fullName')} />
</h1> </h1>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> <div className="rounded-lg border bg-card p-6 space-y-6">
<div className="grid grid-cols-1 lg:grid-cols-[2fr_1fr] gap-6">
<div className="space-y-1"> <div className="space-y-1">
<h3 className="text-sm font-medium mb-2">Contact</h3> <h3 className="text-sm font-medium mb-2">Contact</h3>
<Row label="Email"> <Row label="Email">

View File

@@ -87,13 +87,12 @@ export function ResidentialInterestDetail({ interestId }: { interestId: string }
</Link> </Link>
</div> </div>
<div className="rounded-lg border bg-card p-6 space-y-6"> <div className="rounded-xl border border-slate-200 bg-gradient-brand-soft px-5 py-4 shadow-xs">
<div> <p className="text-xs uppercase font-semibold tracking-wide text-brand">
<p className="text-xs uppercase text-muted-foreground tracking-wider mb-1"> Residential Interest
Residential interest
</p> </p>
{interest.client && ( {interest.client && (
<h1 className="text-2xl font-semibold"> <h1 className="mt-1 truncate text-2xl font-bold tracking-tight text-foreground">
<Link <Link
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
href={`/${portSlug}/residential/clients/${interest.client.id}` as any} href={`/${portSlug}/residential/clients/${interest.client.id}` as any}
@@ -105,7 +104,8 @@ export function ResidentialInterestDetail({ interestId }: { interestId: string }
)} )}
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> <div className="rounded-lg border bg-card p-6 space-y-6">
<div className="grid grid-cols-1 lg:grid-cols-[2fr_1fr] gap-6">
<div className="space-y-1"> <div className="space-y-1">
<h3 className="text-sm font-medium mb-2">Pipeline</h3> <h3 className="text-sm font-medium mb-2">Pipeline</h3>
<Row label="Stage"> <Row label="Stage">

View File

@@ -0,0 +1,20 @@
import { type ReactNode } from 'react';
import { cn } from '@/lib/utils';
interface DetailHeaderStripProps {
children: ReactNode;
className?: string;
}
export function DetailHeaderStrip({ children, className }: DetailHeaderStripProps) {
return (
<div
className={cn(
'rounded-xl border border-slate-200 bg-gradient-brand-soft px-5 py-4 shadow-xs',
className,
)}
>
{children}
</div>
);
}

View File

@@ -10,6 +10,7 @@ import { toast } from 'sonner';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { ArchiveConfirmDialog } from '@/components/shared/archive-confirm-dialog'; import { ArchiveConfirmDialog } from '@/components/shared/archive-confirm-dialog';
import { DetailHeaderStrip } from '@/components/shared/detail-header-strip';
import { PermissionGate } from '@/components/shared/permission-gate'; import { PermissionGate } from '@/components/shared/permission-gate';
import { YachtForm } from '@/components/yachts/yacht-form'; import { YachtForm } from '@/components/yachts/yacht-form';
import { YachtTransferDialog } from '@/components/yachts/yacht-transfer-dialog'; import { YachtTransferDialog } from '@/components/yachts/yacht-transfer-dialog';
@@ -140,7 +141,7 @@ export function YachtDetailHeader({ yacht }: YachtDetailHeaderProps) {
return ( return (
<> <>
<div className="space-y-3"> <DetailHeaderStrip>
<div className="flex items-start gap-3 flex-wrap"> <div className="flex items-start gap-3 flex-wrap">
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap"> <div className="flex items-center gap-2 flex-wrap">
@@ -197,7 +198,7 @@ export function YachtDetailHeader({ yacht }: YachtDetailHeaderProps) {
</Button> </Button>
</div> </div>
</div> </div>
</div> </DetailHeaderStrip>
<YachtForm <YachtForm
open={editOpen} open={editOpen}