Pipeline
DealColumn
One column of the deals board: a droppable, infinitely scrolling list of DealCards under a header with the stage dot, a count badge and money totals.
Guidelines#
Use it for the Kanban view of a deal pipeline. Render it inside the board's DndContext. It registers with useDroppable under the stage id, with data: { type: "column", stageId }.
What you provide:
stage: DealStageDto:label, andcolorchosen fromSTAGE_COLORS.column: { items, total, page, isInitialLoading, isLoadingMore }.totals?: { count, sums: [{ currency, amount, weighted }] }.onLoadMore(stageId)andonOpenDeal(deal).
Anatomy:
- The column is
w-[min(100%,280px)] rounded-xl border bg-muted/20. - Header,
px-3 pt-3 pb-2:- a
size-2dot fromstageColorClasses(color).dot; - the label in
text-sm font-medium; - an outline
Badgewith the accentbadgeclasses, showingtotals.countor elsecolumn.total; - one
text-[11px]line per currency: "Σ €126,000 · weighted €63,000".
- a
- The list is
gap-2 p-2 pt-0 min-h-24 overflow-y-auto. - A drag over the column tints it
bg-primary/5 ring-1 ring-inset ring-primary/20.
Behaviour: scrolling within 120px of the bottom calls onLoadMore while items.length < total. The first load shows two h-16 skeletons; loading more shows one.
The default stages are Qualified (blue, 10%), Meeting booked (sky, 25%), Proposal (amber, 50%), Negotiation (orange, 75%), Won (emerald, 100%) and Lost (red, 0%).
Don't merge currencies into one total. Each currency keeps its own line.
Facts#
| Group | Pipeline |
|---|---|
| Product | Max |
| Global | window.DigitalCrew.DealColumn |
| Source | max-agent: src/features/deals/ui/components/deal-column.tsx |
| Import in the app | import { DealColumn } from "@/features/deals/ui/components/deal-column"; |
API#
TypeScript declarations emitted from src/features/deals/ui/components/deal-column.tsx.
type StageTotals = {
count: number;
sums: {
currency: string;
amount: number;
weighted: number;
}[];
};
type DealColumnProps = {
stage: DealStageDto;
column: DealColumnState;
totals: StageTotals | undefined;
onLoadMore: (stageId: string) => void;
onOpenDeal: (deal: DealWithRelationsDto) => void;
};
export declare function DealColumn({ stage, column, totals, onLoadMore, onOpenDeal, }: DealColumnProps): JSX.Element;Example#
The code of the preview above: plain React.createElement against window.DigitalCrew, with realistic data.
<div id="root" class="p-6"></div>
<script>
(function () {
var h = React.createElement, DC = window.DigitalCrew, I = DC.Icons;
var WS = "0b9f6c1e-3a52-4c1d-9e0a-5d2f7b8c4a10", PIPE = "5e1d2c3b-4a59-4f68-8a7b-6c5d4e3f2a1b";
function color(name) { return DC.STAGE_COLORS.indexOf(name) >= 0 ? name : DC.STAGE_COLORS[0]; }
// DealStageDto rows from the default "Sales pipeline" (features/deals/constants.ts).
function stage(id, key, label, c, type, winProbability, position) {
return { id: id, workspace_id: WS, pipeline_id: PIPE, key: key, label: label, color: color(c), type: type,
win_probability: winProbability, position: position, is_system: false,
created_at: "2026-06-01T08:00:00Z", updated_at: "2026-06-01T08:00:00Z" };
}
function deal(id, stageId, status, name, amount, currency, closeDate, orgName, contactName) {
return {
id: id, workspace_id: WS, pipeline_id: PIPE, stage_id: stageId, name: name, amount: amount, currency: currency,
close_date: closeDate, probability: null, status: status, won_at: null, lost_at: null, lost_reason: null,
owner_id: null, organization_id: orgName ? "org-" + id : null, description: null, custom_fields: null,
source: "agent", created_by: null, created_at: "2026-09-01T09:00:00Z", updated_at: "2026-09-20T09:00:00Z",
organization: orgName ? { id: "org-" + id, name: orgName, primary_domain: null } : null,
prospects: contactName ? [{ deal_id: id, prospect_id: "p-" + id, workspace_id: WS, role: null, is_primary: true, created_at: "2026-09-01T09:00:00Z",
prospect: { id: "p-" + id, full_name: contactName, first_name: null, last_name: null, email: null, title: null, photo_url: null, organization_id: null } }] : []
};
}
var PROPOSAL = stage("st-proposal", "proposal", "Proposal", "amber", "open", 50, 2);
var WON = stage("st-won", "won", "Won", "emerald", "won", 100, 4);
var PROPOSAL_DEALS = [
deal("a1", PROPOSAL.id, "open", "Northwind Traders — DACH pilot", 48000, "EUR", "2026-10-15", "Northwind Traders", "Claire Dubois"),
deal("a2", PROPOSAL.id, "open", "Litware — SDR augmentation", 36000, "EUR", "2026-10-30", "Litware", "Amelia Hart"),
deal("a3", PROPOSAL.id, "open", "Adatum renewal + LinkedIn seats", 42000, "EUR", "2026-11-06", "Adatum Corp", "Lucas Moreau")
];
var WON_DEALS = [
deal("b1", WON.id, "won", "Contoso Logistics — annual plan", 96000, "EUR", "2026-09-22", "Contoso Logistics", null),
deal("b2", WON.id, "won", "Tailspin Toys — outbound pod", 24000, "USD", "2026-09-18", "Tailspin Toys", "Priya Nair")
];
function column(items) { return { items: items, total: items.length, page: 1, isInitialLoading: false, isLoadingMore: false }; }
function App() {
return h(DC.DndContext, null,
h("div", { className: "flex items-start gap-3" },
h(DC.DealColumn, {
stage: PROPOSAL, column: column(PROPOSAL_DEALS), onLoadMore: function () {}, onOpenDeal: function () {},
totals: { count: 3, sums: [{ currency: "EUR", amount: 126000, weighted: 63000 }] }
}),
h(DC.DealColumn, {
stage: WON, column: column(WON_DEALS), onLoadMore: function () {}, onOpenDeal: function () {},
totals: { count: 2, sums: [{ currency: "EUR", amount: 96000, weighted: 96000 }, { currency: "USD", amount: 24000, weighted: 24000 }] }
})));
}
ReactDOM.createRoot(document.getElementById("root")).render(h(DC.TooltipProvider, { delayDuration: 80 }, h(App)));
})();
</script>