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:
@@ -24,6 +24,7 @@ import {
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { DetailHeaderStrip } from '@/components/shared/detail-header-strip';
|
||||
import { PermissionGate } from '@/components/shared/permission-gate';
|
||||
import { BerthForm } from './berth-form';
|
||||
import { apiFetch } from '@/lib/api/client';
|
||||
@@ -94,7 +95,7 @@ function StatusChangeDialog({
|
||||
formState: { isSubmitting },
|
||||
} = useForm<UpdateBerthStatusInput>({
|
||||
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');
|
||||
@@ -127,7 +128,7 @@ function StatusChangeDialog({
|
||||
<Label>New Status</Label>
|
||||
<Select
|
||||
value={status}
|
||||
onValueChange={(v) => setValue('status', v as typeof BERTH_STATUSES[number])}
|
||||
onValueChange={(v) => setValue('status', v as (typeof BERTH_STATUSES)[number])}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
@@ -143,11 +144,7 @@ function StatusChangeDialog({
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Reason *</Label>
|
||||
<Textarea
|
||||
{...register('reason')}
|
||||
placeholder="Reason for status change..."
|
||||
rows={3}
|
||||
/>
|
||||
<Textarea {...register('reason')} placeholder="Reason for status change..." rows={3} />
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
|
||||
@@ -169,22 +166,18 @@ export function BerthDetailHeader({ berth }: BerthDetailHeaderProps) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="space-y-3">
|
||||
<DetailHeaderStrip>
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
<h1 className="text-2xl font-bold text-foreground">
|
||||
Berth {berth.mooringNumber}
|
||||
</h1>
|
||||
<h1 className="text-2xl font-bold text-foreground">Berth {berth.mooringNumber}</h1>
|
||||
<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'}`}
|
||||
>
|
||||
{STATUS_LABELS[berth.status] ?? berth.status}
|
||||
</span>
|
||||
</div>
|
||||
{berth.area && (
|
||||
<p className="text-muted-foreground mt-1">{berth.area}</p>
|
||||
)}
|
||||
{berth.area && <p className="text-muted-foreground mt-1">{berth.area}</p>}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
@@ -200,7 +193,7 @@ export function BerthDetailHeader({ berth }: BerthDetailHeaderProps) {
|
||||
</PermissionGate>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DetailHeaderStrip>
|
||||
|
||||
<BerthForm berth={berth} open={editOpen} onOpenChange={setEditOpen} />
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { TagBadge } from '@/components/shared/tag-badge';
|
||||
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 { apiFetch } from '@/lib/api/client';
|
||||
|
||||
@@ -68,7 +69,7 @@ export function ClientDetailHeader({ client }: ClientDetailHeaderProps) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="space-y-3">
|
||||
<DetailHeaderStrip>
|
||||
<div className="flex items-start gap-3 flex-wrap">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
@@ -140,7 +141,7 @@ export function ClientDetailHeader({ client }: ClientDetailHeaderProps) {
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DetailHeaderStrip>
|
||||
|
||||
<ArchiveConfirmDialog
|
||||
open={archiveOpen}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { toast } from 'sonner';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { ArchiveConfirmDialog } from '@/components/shared/archive-confirm-dialog';
|
||||
import { DetailHeaderStrip } from '@/components/shared/detail-header-strip';
|
||||
import { PermissionGate } from '@/components/shared/permission-gate';
|
||||
import { CompanyForm } from '@/components/companies/company-form';
|
||||
import { apiFetch } from '@/lib/api/client';
|
||||
@@ -74,7 +75,7 @@ export function CompanyDetailHeader({ company }: CompanyDetailHeaderProps) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="space-y-3">
|
||||
<DetailHeaderStrip>
|
||||
<div className="flex items-start gap-3 flex-wrap">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
@@ -118,7 +119,7 @@ export function CompanyDetailHeader({ company }: CompanyDetailHeaderProps) {
|
||||
</PermissionGate>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DetailHeaderStrip>
|
||||
|
||||
<CompanyForm
|
||||
open={editOpen}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { TagBadge } from '@/components/shared/tag-badge';
|
||||
import { ArchiveConfirmDialog } from '@/components/shared/archive-confirm-dialog';
|
||||
import { DetailHeaderStrip } from '@/components/shared/detail-header-strip';
|
||||
import { PermissionGate } from '@/components/shared/permission-gate';
|
||||
import { InterestForm } from '@/components/interests/interest-form';
|
||||
import { InterestStagePicker } from '@/components/interests/interest-stage-picker';
|
||||
@@ -70,8 +71,7 @@ export function InterestDetailHeader({ portSlug, interest }: InterestDetailHeade
|
||||
const isArchived = !!interest.archivedAt;
|
||||
|
||||
const archiveMutation = useMutation({
|
||||
mutationFn: () =>
|
||||
apiFetch(`/api/v1/interests/${interest.id}`, { method: 'DELETE' }),
|
||||
mutationFn: () => apiFetch(`/api/v1/interests/${interest.id}`, { method: 'DELETE' }),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['interests', interest.id] });
|
||||
queryClient.invalidateQueries({ queryKey: ['interests'] });
|
||||
@@ -80,8 +80,7 @@ export function InterestDetailHeader({ portSlug, interest }: InterestDetailHeade
|
||||
});
|
||||
|
||||
const restoreMutation = useMutation({
|
||||
mutationFn: () =>
|
||||
apiFetch(`/api/v1/interests/${interest.id}/restore`, { method: 'POST' }),
|
||||
mutationFn: () => apiFetch(`/api/v1/interests/${interest.id}/restore`, { method: 'POST' }),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['interests', interest.id] });
|
||||
queryClient.invalidateQueries({ queryKey: ['interests'] });
|
||||
@@ -91,7 +90,7 @@ export function InterestDetailHeader({ portSlug, interest }: InterestDetailHeade
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="space-y-3">
|
||||
<DetailHeaderStrip>
|
||||
<div className="flex items-start gap-3 flex-wrap">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
@@ -99,7 +98,9 @@ export function InterestDetailHeader({ portSlug, interest }: InterestDetailHeade
|
||||
{interest.clientName ?? 'Unknown Client'}
|
||||
</h1>
|
||||
{isArchived && (
|
||||
<Badge variant="secondary" className="text-xs">Archived</Badge>
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
Archived
|
||||
</Badge>
|
||||
)}
|
||||
<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'}`}
|
||||
@@ -130,8 +131,7 @@ export function InterestDetailHeader({ portSlug, interest }: InterestDetailHeade
|
||||
)}
|
||||
{interest.source && (
|
||||
<span>
|
||||
Source:{' '}
|
||||
<span className="text-foreground capitalize">{interest.source}</span>
|
||||
Source: <span className="text-foreground capitalize">{interest.source}</span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -176,7 +176,7 @@ export function InterestDetailHeader({ portSlug, interest }: InterestDetailHeade
|
||||
</PermissionGate>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DetailHeaderStrip>
|
||||
|
||||
<InterestForm
|
||||
open={editOpen}
|
||||
|
||||
@@ -85,15 +85,18 @@ export function InvoiceDetail({ invoiceId }: InvoiceDetailProps) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="flex items-center gap-3">
|
||||
<h2 className="text-xl font-semibold font-mono">{invoice.invoiceNumber}</h2>
|
||||
<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 className="min-w-0">
|
||||
<div className="text-xs font-semibold uppercase tracking-wide text-brand">Invoice</div>
|
||||
<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}`}>
|
||||
{invoice.status}
|
||||
</Badge>
|
||||
</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 className="flex items-center gap-2">
|
||||
{invoice.status === 'draft' && (
|
||||
|
||||
@@ -113,14 +113,17 @@ export function ResidentialClientDetail({ clientId }: { clientId: string }) {
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border bg-card p-6 space-y-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold">
|
||||
<div className="rounded-xl border border-slate-200 bg-gradient-brand-soft px-5 py-4 shadow-xs">
|
||||
<div className="text-xs font-semibold uppercase tracking-wide text-brand">
|
||||
Residential Client
|
||||
</div>
|
||||
<h1 className="mt-1 truncate text-2xl font-bold tracking-tight text-foreground">
|
||||
<InlineEditableField value={client.fullName} onSave={save('fullName')} />
|
||||
</h1>
|
||||
</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">
|
||||
<h3 className="text-sm font-medium mb-2">Contact</h3>
|
||||
<Row label="Email">
|
||||
|
||||
@@ -87,13 +87,12 @@ export function ResidentialInterestDetail({ interestId }: { interestId: string }
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border bg-card p-6 space-y-6">
|
||||
<div>
|
||||
<p className="text-xs uppercase text-muted-foreground tracking-wider mb-1">
|
||||
Residential interest
|
||||
<div className="rounded-xl border border-slate-200 bg-gradient-brand-soft px-5 py-4 shadow-xs">
|
||||
<p className="text-xs uppercase font-semibold tracking-wide text-brand">
|
||||
Residential Interest
|
||||
</p>
|
||||
{interest.client && (
|
||||
<h1 className="text-2xl font-semibold">
|
||||
<h1 className="mt-1 truncate text-2xl font-bold tracking-tight text-foreground">
|
||||
<Link
|
||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||
href={`/${portSlug}/residential/clients/${interest.client.id}` as any}
|
||||
@@ -105,7 +104,8 @@ export function ResidentialInterestDetail({ interestId }: { interestId: string }
|
||||
)}
|
||||
</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">
|
||||
<h3 className="text-sm font-medium mb-2">Pipeline</h3>
|
||||
<Row label="Stage">
|
||||
|
||||
20
src/components/shared/detail-header-strip.tsx
Normal file
20
src/components/shared/detail-header-strip.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import { toast } from 'sonner';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { ArchiveConfirmDialog } from '@/components/shared/archive-confirm-dialog';
|
||||
import { DetailHeaderStrip } from '@/components/shared/detail-header-strip';
|
||||
import { PermissionGate } from '@/components/shared/permission-gate';
|
||||
import { YachtForm } from '@/components/yachts/yacht-form';
|
||||
import { YachtTransferDialog } from '@/components/yachts/yacht-transfer-dialog';
|
||||
@@ -140,7 +141,7 @@ export function YachtDetailHeader({ yacht }: YachtDetailHeaderProps) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="space-y-3">
|
||||
<DetailHeaderStrip>
|
||||
<div className="flex items-start gap-3 flex-wrap">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
@@ -197,7 +198,7 @@ export function YachtDetailHeader({ yacht }: YachtDetailHeaderProps) {
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DetailHeaderStrip>
|
||||
|
||||
<YachtForm
|
||||
open={editOpen}
|
||||
|
||||
Reference in New Issue
Block a user