Polish every product workflow across desktop and mobile
CI / verify (push) Successful in 3m32s

Reframe each screen around the decisions compute brokers make: sellable capacity, full-cost margin, pipeline movement, contract deadlines, evidence review, staged imports, and controlled agent access. Group the shell by operating domain, strengthen mobile navigation and sheets, add responsive record treatments, and make loading, error, empty, readiness, and retry states explicit.

The visual audit exposed sortable table targets and an unnamed file input only after exercising the rendered app, so this commit also pins those accessibility decisions at their actual interaction boundaries. Manrope is self-hosted as a single Latin variable subset to keep the stronger hierarchy without shipping unused font payloads.
This commit is contained in:
2026-08-13 05:34:23 -07:00
parent 1318c0b841
commit e12d27edd1
28 changed files with 672 additions and 522 deletions
+59 -224
View File
@@ -1,25 +1,18 @@
/**
* The pipeline boards, for both sides of the market.
*
* A column-per-stage board on desktop; on a phone, a stage picker and a single
* column. A horizontally scrolling eight-column board on a 390px screen is
* technically responsive and practically unusable — you cannot see where a
* card is going, which is the entire point of a board.
* Stages remain ordered, but wrap into a scanable desktop grid instead of
* hiding the back half of the funnel behind a multi-screen horizontal rail.
*/
import { useMemo, useState } from 'react';
import { useDeferredValue, useMemo, useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import type { PermissionGrant } from '@pig/core';
import { Pencil, Plus } from 'lucide-react';
import { Pencil, Plus, RefreshCw, Search } from 'lucide-react';
import { get, money, relativeTime } from '@/lib/api';
import { Badge, Button, Card, EmptyState, Skeleton } from '@/components/ui';
import { Badge, Button, Card, EmptyState, Input, Skeleton } from '@/components/ui';
import { usePageTitle } from '@/lib/title';
import { can } from '@/lib/permissions';
import {
DemandDealSheet,
SupplyDealSheet,
type DemandDealRecord,
type SupplyDealRecord,
} from '@/components/RecordSheets';
import { DemandDealSheet, SupplyDealSheet, type DemandDealRecord, type SupplyDealRecord } from '@/components/RecordSheets';
interface Board<T> {
stages: string[];
@@ -27,237 +20,79 @@ interface Board<T> {
}
const STAGE_LABELS: Record<string, string> = {
qualification: 'Qualification',
legal: 'Legal',
scoping: 'Scoping',
proposal: 'Proposal',
procurement: 'Procurement',
poc: 'POC',
deployment: 'Deployment',
expansion: 'Expansion',
closed_won: 'Closed won',
closed_lost: 'Closed lost',
sourced: 'Sourced',
qualifying: 'Qualifying',
technical_diligence: 'Technical diligence',
financial_diligence: 'Financial diligence',
pricing: 'Pricing',
contracting: 'Contracting',
onboarding: 'Onboarding',
live: 'Live',
renewal: 'Renewal',
churned: 'Churned',
rejected: 'Rejected',
qualification: 'Qualification', legal: 'Legal', scoping: 'Scoping', proposal: 'Proposal', procurement: 'Procurement', poc: 'POC', deployment: 'Deployment', expansion: 'Expansion', closed_won: 'Closed won', closed_lost: 'Closed lost', sourced: 'Sourced', qualifying: 'Qualifying', technical_diligence: 'Technical diligence', financial_diligence: 'Financial diligence', pricing: 'Pricing', contracting: 'Contracting', onboarding: 'Onboarding', live: 'Live', renewal: 'Renewal', churned: 'Churned', rejected: 'Rejected',
};
export function DemandPipeline() {
return (
<PipelineBoard<DemandDealRecord>
title="Demand"
subtitle="Selling compute and post-training. Note that legal sits early — paper gates the deal rather than closing it."
endpoint="/api/deals/demand"
team="demand"
renderSheet={({ open, onOpenChange, record }) => <DemandDealSheet open={open} onOpenChange={onOpenChange} record={record} />}
renderCard={(deal, accountName) => (
<>
<p className="truncate font-medium">{deal.name}</p>
<p className="truncate text-xs text-muted">{accountName ?? 'No account'}</p>
<div className="mt-2 flex flex-wrap items-center gap-1.5">
{deal.acvCents ? (
<span className="nums text-sm font-semibold">{money(deal.acvCents)}</span>
) : null}
<Badge tone="neutral">{deal.productLine.replace(/_/g, ' ')}</Badge>
{/* Contract state is surfaced on the card because shipping capacity
without executed paper is the mistake this pipeline prevents. */}
{deal.msaExecuted ? <Badge tone="positive">MSA</Badge> : null}
{deal.dpaExecuted ? <Badge tone="positive">DPA</Badge> : null}
</div>
</>
)}
/>
);
return <PipelineBoard<DemandDealRecord>
title="Demand" orientation="Sell-side"
subtitle="Selling compute and post-training. Legal sits early because paper gates delivery rather than merely closing it."
endpoint="/api/deals/demand" team="demand"
searchText={(deal, accountName) => `${deal.name} ${accountName ?? ''} ${deal.productLine}`}
metricLabel="Visible ACV" metricValue={(deals) => money(deals.reduce((total, deal) => total + (deal.acvCents ?? 0), 0))}
renderSheet={({ open, onOpenChange, record }) => <DemandDealSheet open={open} onOpenChange={onOpenChange} record={record} />}
renderCard={(deal, accountName) => <><p className="truncate font-medium">{deal.name}</p><p className="truncate text-xs text-muted">{accountName ?? 'No account'}</p><div className="mt-2 flex flex-wrap items-center gap-1.5">{deal.acvCents != null ? <span className="nums text-sm font-semibold">{money(deal.acvCents)}</span> : null}<Badge tone="neutral">{deal.productLine.replace(/_/g, ' ')}</Badge>{/* Paper state prevents delivery readiness from being inferred from stage. */}{deal.msaExecuted ? <Badge tone="positive">MSA</Badge> : null}{deal.dpaExecuted ? <Badge tone="positive">DPA</Badge> : null}</div></>}
/>;
}
export function SupplyPipeline() {
return (
<PipelineBoard<SupplyDealRecord>
title="Supply"
subtitle="Sourcing GPU capacity. Technical and financial diligence are separate gates — accepting capacity is a two-key decision."
endpoint="/api/deals/supply"
team="supply"
renderSheet={({ open, onOpenChange, record }) => <SupplyDealSheet open={open} onOpenChange={onOpenChange} record={record} />}
renderCard={(deal, accountName) => (
<>
<p className="truncate font-medium">{deal.name}</p>
<p className="truncate text-xs text-muted">{accountName ?? 'No account'}</p>
<div className="mt-2 flex flex-wrap items-center gap-1.5">
{deal.gpuCount && deal.gpuType ? (
<Badge tone="accent">
{deal.gpuCount}× {deal.gpuType}
</Badge>
) : null}
{deal.targetCostPerGpuHourCents ? (
<span className="nums text-xs text-muted">
{money(deal.targetCostPerGpuHourCents)}/hr target
</span>
) : null}
</div>
</>
)}
/>
);
return <PipelineBoard<SupplyDealRecord>
title="Supply" orientation="Buy-side"
subtitle="Sourcing GPU capacity. Technical and financial diligence remain separate gates because accepting capacity is a two-key decision."
endpoint="/api/deals/supply" team="supply"
searchText={(deal, accountName) => `${deal.name} ${accountName ?? ''} ${deal.gpuType ?? ''}`}
metricLabel="GPU opportunity" metricValue={(deals) => `${deals.reduce((total, deal) => total + (deal.gpuCount ?? 0), 0).toLocaleString()} GPUs`}
renderSheet={({ open, onOpenChange, record }) => <SupplyDealSheet open={open} onOpenChange={onOpenChange} record={record} />}
renderCard={(deal, accountName) => <><p className="truncate font-medium">{deal.name}</p><p className="truncate text-xs text-muted">{accountName ?? 'No account'}</p><div className="mt-2 flex flex-wrap items-center gap-1.5">{deal.gpuCount != null && deal.gpuType ? <Badge tone="accent">{deal.gpuCount}× {deal.gpuType}</Badge> : null}{deal.targetCostPerGpuHourCents != null ? <span className="nums text-xs text-muted">{money(deal.targetCostPerGpuHourCents)}/hr target</span> : null}</div></>}
/>;
}
function PipelineBoard<T extends { id: string; stage: string; updatedAt: string }>({
title,
subtitle,
endpoint,
team,
renderCard,
renderSheet,
}: {
title: string;
subtitle: string;
endpoint: string;
team: 'supply' | 'demand';
function PipelineBoard<T extends { id: string; stage: string; updatedAt: string }>({ title, orientation, subtitle, endpoint, team, searchText, metricLabel, metricValue, renderCard, renderSheet }: {
title: string; orientation: string; subtitle: string; endpoint: string; team: 'supply' | 'demand';
searchText: (deal: T, accountName: string | null) => string; metricLabel: string; metricValue: (deals: T[]) => string;
renderCard: (deal: T, accountName: string | null) => React.ReactNode;
renderSheet: (props: { open: boolean; onOpenChange(open: boolean): void; record?: T }) => React.ReactNode;
}) {
usePageTitle(title);
const { data, isLoading } = useQuery({
queryKey: [endpoint],
queryFn: () => get<Board<T>>(endpoint),
});
const { data: me } = useQuery({
queryKey: ['me'],
queryFn: () => get<{ permissions: PermissionGrant[] }>('/api/me'),
});
const boardQuery = useQuery({ queryKey: [endpoint], queryFn: () => get<Board<T>>(endpoint) });
const { data: me } = useQuery({ queryKey: ['me'], queryFn: () => get<{ permissions: PermissionGrant[] }>('/api/me') });
const writable = can(me, 'deal:write', team);
const [sheet, setSheet] = useState<{ open: boolean; record?: T }>({ open: false });
const [activeStage, setActiveStage] = useState<string | null>(null);
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query.trim().toLocaleLowerCase());
const filteredDeals = useMemo(() => !deferredQuery ? boardQuery.data?.deals ?? [] : (boardQuery.data?.deals ?? []).filter((row) => searchText(row.deal, row.accountName).toLocaleLowerCase().includes(deferredQuery)), [boardQuery.data?.deals, deferredQuery, searchText]);
const byStage = useMemo(() => {
const map = new Map<string, { deal: T; accountName: string | null }[]>();
for (const stage of data?.stages ?? []) map.set(stage, []);
for (const row of data?.deals ?? []) {
map.get(row.deal.stage)?.push(row);
}
for (const stage of boardQuery.data?.stages ?? []) map.set(stage, []);
for (const row of filteredDeals) map.get(row.deal.stage)?.push(row);
return map;
}, [data]);
}, [boardQuery.data?.stages, filteredDeals]);
const stages = boardQuery.data?.stages ?? [];
const populatedStage = stages.find((stage) => (byStage.get(stage)?.length ?? 0) > 0);
const currentStage = activeStage && stages.includes(activeStage) ? activeStage : populatedStage ?? stages[0] ?? '';
const activeStageCount = stages.filter((stage) => (byStage.get(stage)?.length ?? 0) > 0).length;
const sheetNode = renderSheet({ open: sheet.open, onOpenChange: (open) => setSheet((state) => ({ ...state, open })), record: sheet.record });
if (isLoading) return <Skeleton className="h-96" />;
if (boardQuery.isLoading) return <div className="space-y-5"><Header title={title} orientation={orientation} subtitle={subtitle} writable={writable} onCreate={() => setSheet({ open: true })} /><Skeleton className="h-96" /></div>;
if (boardQuery.isError) return <div className="space-y-5"><Header title={title} orientation={orientation} subtitle={subtitle} writable={writable} onCreate={() => setSheet({ open: true })} /><Card><EmptyState title={`${title} pipeline unavailable`} description={boardQuery.error.message} action={<Button variant="outline" onClick={() => void boardQuery.refetch()}><RefreshCw aria-hidden />Try again</Button>} /></Card>{sheetNode}</div>;
if (!boardQuery.data || boardQuery.data.deals.length === 0) return <div className="space-y-5"><Header title={title} orientation={orientation} subtitle={subtitle} writable={writable} onCreate={() => setSheet({ open: true })} /><Card><EmptyState title={`No ${title.toLowerCase()} deals yet`} description="Deals appear here once created. Stages follow how this market actually operates rather than a generic sales funnel." action={<Button variant="primary" disabled={!writable} onClick={() => setSheet({ open: true })}><Plus aria-hidden />New {title.toLowerCase()} deal</Button>} /></Card>{sheetNode}</div>;
if (!data || data.deals.length === 0) {
return (
<div className="space-y-5">
<Header title={title} subtitle={subtitle} writable={writable} onCreate={() => setSheet({ open: true })} />
<Card>
<EmptyState
title={`No ${title.toLowerCase()} deals yet`}
description="Deals appear here once created. Stages follow how this market actually operates rather than a generic sales funnel."
action={<Button variant="primary" disabled={!writable} onClick={() => setSheet({ open: true })}><Plus aria-hidden />New {title.toLowerCase()} deal</Button>}
/>
</Card>
{renderSheet({ open: sheet.open, onOpenChange: (open) => setSheet((state) => ({ ...state, open })), record: sheet.record })}
</div>
);
}
const stages = data.stages;
const currentStage = activeStage ?? stages[0]!;
return (
<div className="space-y-5">
<Header title={title} subtitle={subtitle} writable={writable} onCreate={() => setSheet({ open: true })} />
{/* Phone: pick one stage. The chips scroll; the board does not. */}
<div className="lg:hidden">
<div className="scroll-x -mx-4 flex gap-2 px-4 pb-1">
{stages.map((stage) => {
const count = byStage.get(stage)?.length ?? 0;
return (
<button
key={stage}
onClick={() => setActiveStage(stage)}
className={[
'tap shrink-0 rounded-full px-3.5 text-sm font-medium transition-colors',
stage === currentStage
? 'bg-primary text-accent-on'
: 'bg-surface-2 text-muted',
].join(' ')}
>
{STAGE_LABELS[stage] ?? stage}
<span className="ml-1.5 opacity-70">{count}</span>
</button>
);
})}
</div>
<div className="mt-3 space-y-2">
{(byStage.get(currentStage) ?? []).map((row) => (
<DealCard key={row.deal.id} row={row} renderCard={renderCard} writable={writable} onEdit={() => setSheet({ open: true, record: row.deal })} />
))}
{(byStage.get(currentStage) ?? []).length === 0 ? (
<p className="py-8 text-center text-sm text-muted">
Nothing in {STAGE_LABELS[currentStage] ?? currentStage}.
</p>
) : null}
</div>
</div>
{/* Desktop: the full board, scrolling horizontally within its own pane
so the page itself never scrolls sideways. */}
<div className="scroll-x hidden lg:block">
<div className="flex gap-3 pb-2">
{stages.map((stage) => {
const rows = byStage.get(stage) ?? [];
return (
<section key={stage} className="w-72 shrink-0">
<div className="mb-2 flex items-center justify-between px-1">
<h2 className="text-sm font-semibold">{STAGE_LABELS[stage] ?? stage}</h2>
<span className="nums text-xs text-muted">{rows.length}</span>
</div>
<div className="space-y-2">
{rows.map((row) => (
<DealCard key={row.deal.id} row={row} renderCard={renderCard} writable={writable} onEdit={() => setSheet({ open: true, record: row.deal })} />
))}
</div>
</section>
);
})}
</div>
</div>
{renderSheet({ open: sheet.open, onOpenChange: (open) => setSheet((state) => ({ ...state, open })), record: sheet.record })}
</div>
);
return <div className="space-y-5">
<Header title={title} orientation={orientation} subtitle={subtitle} writable={writable} onCreate={() => setSheet({ open: true })} />
<section className="grid gap-3 rounded-xl border border-border bg-surface-2/60 p-3 sm:grid-cols-[minmax(0,1fr)_auto_auto] sm:items-center">
<label className="relative min-w-0"><span className="sr-only">Search {title.toLowerCase()} pipeline</span><Search className="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted" aria-hidden /><Input className="h-11 pl-9" value={query} onChange={(event) => setQuery(event.target.value)} placeholder="Search deal, account or product" /></label>
<PipelineStat label={query ? 'Matches' : 'Deals'} value={String(filteredDeals.length)} /><PipelineStat label={metricLabel} value={metricValue(filteredDeals.map((row) => row.deal))} />
</section>
<div className="lg:hidden"><label className="block text-xs font-medium text-muted" htmlFor={`${team}-stage`}>Focus stage</label><select id={`${team}-stage`} className="mt-1 h-11 w-full rounded-lg border border-border bg-surface px-3 text-sm font-medium text-fg" value={currentStage} onChange={(event) => setActiveStage(event.target.value)}>{stages.map((stage) => <option key={stage} value={stage}>{STAGE_LABELS[stage] ?? stage} · {byStage.get(stage)?.length ?? 0}</option>)}</select><div className="mt-3 space-y-2">{(byStage.get(currentStage) ?? []).map((row) => <DealCard key={row.deal.id} row={row} renderCard={renderCard} writable={writable} onEdit={() => setSheet({ open: true, record: row.deal })} />)}{(byStage.get(currentStage) ?? []).length === 0 ? <StageEmpty stage={currentStage} filtered={Boolean(deferredQuery)} /> : null}</div></div>
<div className="hidden lg:block"><div className="mb-3 flex items-center justify-between gap-3"><p className="text-sm text-muted"><strong className="text-fg">{activeStageCount}</strong> of {stages.length} stages have {deferredQuery ? 'matching' : 'active'} work</p><p className="text-xs text-muted">Stage order runs left to right, then down.</p></div><div className="grid items-start gap-3 lg:grid-cols-3 2xl:grid-cols-4">{stages.map((stage, index) => { const rows = byStage.get(stage) ?? []; return <section key={stage} className="min-w-0 rounded-xl border border-border bg-surface-2/45 p-3" aria-labelledby={`${team}-${stage}`}><div className="mb-3 flex min-h-8 items-center justify-between gap-2"><div className="flex min-w-0 items-center gap-2"><span className="nums flex size-6 shrink-0 items-center justify-center rounded-full bg-surface text-[11px] text-muted">{index + 1}</span><h2 id={`${team}-${stage}`} className="truncate text-sm font-semibold">{STAGE_LABELS[stage] ?? stage}</h2></div><Badge tone={rows.length ? 'accent' : 'neutral'}>{rows.length}</Badge></div><div className="space-y-2">{rows.map((row) => <DealCard key={row.deal.id} row={row} renderCard={renderCard} writable={writable} onEdit={() => setSheet({ open: true, record: row.deal })} />)}{rows.length === 0 ? <StageEmpty stage={stage} filtered={Boolean(deferredQuery)} compact /> : null}</div></section>; })}</div></div>
{sheetNode}
</div>;
}
function DealCard<T extends { id: string; updatedAt: string }>({
row,
renderCard,
writable,
onEdit,
}: {
row: { deal: T; accountName: string | null };
renderCard: (deal: T, accountName: string | null) => React.ReactNode;
writable: boolean;
onEdit(): void;
}) {
return (
<article className="card relative p-3 pr-12">
<Button size="icon" variant="ghost" className="absolute right-1 top-1" disabled={!writable} onClick={onEdit} title="Edit deal"><Pencil aria-hidden /><span className="sr-only">Edit deal</span></Button>
{renderCard(row.deal, row.accountName)}
<p className="mt-2 text-[11px] text-muted">{relativeTime(row.deal.updatedAt)}</p>
</article>
);
}
function Header({ title, subtitle, writable, onCreate }: { title: string; subtitle: string; writable: boolean; onCreate(): void }) {
return (
<header className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div><h1 className="text-xl font-semibold tracking-tight sm:text-2xl">{title}</h1><p className="mt-1 max-w-2xl text-sm text-muted">{subtitle}</p></div>
<Button variant="primary" disabled={!writable} onClick={onCreate}><Plus aria-hidden />New deal</Button>
</header>
);
function DealCard<T extends { id: string; updatedAt: string }>({ row, renderCard, writable, onEdit }: { row: { deal: T; accountName: string | null }; renderCard: (deal: T, accountName: string | null) => React.ReactNode; writable: boolean; onEdit(): void }) {
return <article className="card relative min-w-0 p-3 pr-12 shadow-sm"><Button size="icon" variant="ghost" className="absolute right-1 top-1" disabled={!writable} onClick={onEdit} title={writable ? 'Edit deal' : 'Deal write access required'}><Pencil aria-hidden /><span className="sr-only">Edit deal</span></Button>{renderCard(row.deal, row.accountName)}<p className="mt-2 text-[11px] text-muted">Updated {relativeTime(row.deal.updatedAt)}</p></article>;
}
function PipelineStat({ label, value }: { label: string; value: string }) { return <div className="min-w-[7rem] rounded-lg bg-surface px-3 py-2"><p className="text-[11px] font-medium uppercase tracking-wide text-muted">{label}</p><p className="nums mt-0.5 truncate text-sm font-semibold">{value}</p></div>; }
function StageEmpty({ stage, filtered, compact = false }: { stage: string; filtered: boolean; compact?: boolean }) { return <p className={compact ? 'rounded-lg border border-dashed border-border px-3 py-5 text-center text-xs text-muted' : 'py-10 text-center text-sm text-muted'}>{filtered ? 'No matching deals' : `Nothing in ${STAGE_LABELS[stage] ?? stage}`}</p>; }
function Header({ title, orientation, subtitle, writable, onCreate }: { title: string; orientation: string; subtitle: string; writable: boolean; onCreate(): void }) { return <header className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between"><div className="min-w-0"><div className="flex flex-wrap items-center gap-2"><h1 className="text-xl font-semibold tracking-tight sm:text-2xl">{title}</h1><Badge tone={title === 'Supply' ? 'info' : 'neutral'}>{orientation}</Badge></div><p className="mt-1 max-w-2xl text-sm text-muted">{subtitle}</p></div><Button className="min-h-11 sm:shrink-0" variant="primary" disabled={!writable} onClick={onCreate} title={writable ? undefined : 'Deal write access required'}><Plus aria-hidden />New {title.toLowerCase()} deal</Button></header>; }