@open-kingdom/shared-frontend-ui-stage-board
v0.0.2-17
Published
A presentational, generic kanban-style board with drag-and-drop between columns. Built on `@dnd-kit/core`. Generic over the card type (`TCard`) and the column-id literal union (`TColumnId`), so the board doesn't care whether you're moving opportunities be
Readme
@open-kingdom/shared-frontend-ui-stage-board
A presentational, generic kanban-style board with drag-and-drop between columns. Built on @dnd-kit/core. Generic over the card type (TCard) and the column-id literal union (TColumnId), so the board doesn't care whether you're moving opportunities between pipeline stages, tasks between status lanes, or candidates between hiring stages.
The component is purely presentational — it owns no data, no Redux, no fetching, and no domain coupling. The host passes a cards array, declares how to identify each card and which column it belongs to, and renders the card body. Drag interactions surface as a callback; the host updates its source of truth and re-renders.
Exports
| Export | Type | Description |
| ------------------ | ---------------------------------- | ------------------------------ |
| StageBoard | <TCard, TColumnId>(props) => JSX | The board component (generic). |
| StageBoardProps | interface<TCard, TColumnId> | Props for StageBoard. |
| StageBoardColumn | interface<TColumnId> | One column descriptor. |
Props
StageBoardProps<TCard, TColumnId extends string = string>
| Prop | Type | Required | Default | Description |
| --------------------- | ------------------------------------------------ | -------- | ------------ | ----------------------------------------------------------------------------------------------- |
| columns | readonly StageBoardColumn<TColumnId>[] | Yes | — | Column descriptors. Render order is array order. |
| cards | readonly TCard[] | Yes | — | All cards across all columns; the component buckets them by getCardColumnId. |
| getCardId | (card: TCard) => string | Yes | — | Stable card id used as React key and dnd-kit draggable id. |
| getCardColumnId | (card: TCard) => TColumnId | Yes | — | Returns the column the card currently lives in. |
| renderCard | (card: TCard) => ReactNode | Yes | — | Card body. Wrapped by the component in a draggable container; do not add your own drag handles. |
| renderColumnSummary | ({ column, cards }) => ReactNode | No | card count | Summary rendered to the right of the column header. Defaults to a count of cards in the column. |
| onCardMoved | ({ cardId, fromColumnId, toColumnId }) => void | No | — | Fired after a drop into a different column. Drops into the same column are suppressed. |
| className | string | No | — | Extra classes merged onto the outer board container. |
| columnClassName | string | No | — | Extra classes merged onto every column. |
| emptyColumnMessage | ReactNode | No | 'No items' | Placeholder rendered inside an empty column. Single value applied to every empty column. |
StageBoardColumn<TColumnId>
| Property | Type | Required | Description |
| ------------- | ----------- | -------- | -------------------------------------------------------------- |
| id | TColumnId | Yes | Stable column id. The component uses this as the droppable id. |
| label | string | Yes | Column header text. |
| description | string | No | Sub-header text shown beneath the label. |
Behavior
- Layout — columns render as a horizontally scrollable grid of
minmax(16rem, 1fr)tracks. Mobile-friendly (the row scrolls; columns don't shrink below 16rem). - Drag activation — uses
PointerSensorwith adistance: 4activation constraint, so a click doesn't start a drag. - Drag overlay — while dragging, the original card dims to 50% opacity and a clone is rendered in the overlay with a primary-tinted shadow.
- Drop handling — when the dragged card is released over a column other than its origin,
onCardMovedfires with the original column id (fromColumnId) and the target column id (toColumnId). Drops onto the same column or outside any column are no-ops. - No optimistic state — the component does not mutate any internal state on drop. The host is expected to update its source of truth in the
onCardMovedcallback (locally, via a mutation, or both); the next render will reflect the new column placement. This keeps the component a pure rendering surface.
Usage
Pipeline kanban
import { StageBoard, type StageBoardColumn } from '@open-kingdom/shared-frontend-ui-stage-board';
import { OPPORTUNITY_STAGES, type OpportunityStage } from '@open-kingdom/crm-poly-util-domain';
const columns: StageBoardColumn<OpportunityStage>[] = OPPORTUNITY_STAGES.map((stage) => ({
id: stage,
label: stage[0].toUpperCase() + stage.slice(1),
}));
export function PipelineBoard({ opportunities }: { opportunities: Opportunity[] }) {
const [updateStage] = useUpdateOpportunityMutation();
return (
<StageBoard
columns={columns}
cards={opportunities}
getCardId={(opp) => String(opp.id)}
getCardColumnId={(opp) => opp.stage as OpportunityStage}
renderCard={(opp) => (
<div className="p-3">
<p className="font-medium">{opp.title}</p>
<p className="text-xs text-muted-foreground">${opp.estimatedValue?.toLocaleString() ?? '—'}</p>
</div>
)}
renderColumnSummary={({ cards }) => (
<span className="text-xs text-muted-foreground">
{cards.length} · ${cards.reduce((acc, c) => acc + (c.estimatedValue ?? 0), 0).toLocaleString()}
</span>
)}
onCardMoved={({ cardId, toColumnId }) => updateStage({ id: Number(cardId), body: { stage: toColumnId } })}
/>
);
}Custom empty-column placeholder
<StageBoard
// …
emptyColumnMessage={<span className="italic">Nothing in this stage</span>}
/>Tailwind classes
Uses semantic palette classes from shared-frontend-ui-theme (bg-card, border-border, bg-muted/40, bg-primary/5, border-primary, etc.). The host's Tailwind config must include this library's source files in its content glob.
Testing
nx test shared-frontend-ui-stage-board