Board card
Deal card of the collection and session lists, read-only or reorderable, fed by a BoardCardViewModel.
Import from @workspace/ui-training/deal/board-card/*. The card shows a deal in a list: the south hand on the left, its name, code and teaching metadata on the right. It comes as two compounds over the same parts: BoardCardView (read-only) and BoardCardEdit (drag, move arrows, remove). EditableBoardCard and UnavailableBoardCard are the ready-made rows of an edited list.
The hand, contract, player and lead render with @workspace/ui-bridge, so the host mounts a bridge context (DealBridgeProvider in apps/app) and a TooltipProvider above the list.
Anatomy
<BoardCardEdit.List values={ids} onReorder={setIds}>
{(shown) => shown.map((id, i) => (
<BoardCardEdit.Item value={id}> {/* reorder row */}
<BoardCardEdit.OrderControls /> {/* arrows + grip, beside the card */}
<BoardCardEdit.Root board={vm} index> {/* card shell, carries the ViewModel */}
<BoardCardEdit.BoardCells /> {/* south hand */}
<BoardCardEdit.Body> {/* right column */}
<BoardCardEdit.Header /> {/* board number — name, #code */}
<BoardCardEdit.UsageBadge />
<BoardCardEdit.SoftDeletedBadge />
<BoardCardEdit.Stats /> {/* level, contract, player, lead, themes */}
<BoardCardEdit.Footer> {/* actions, aligned to the end */}
<BoardCardEdit.Remove />
</BoardCardEdit.Footer>
</BoardCardEdit.Body>
</BoardCardEdit.Root>
</BoardCardEdit.Item>
))}
</BoardCardEdit.List>| Part | View | Edit | Role |
|---|---|---|---|
List | ✓ | Reorder list; renders its items from the order it shows, reports the new one once, on release | |
Item | ✓ | Reorder row; dragOnItem makes the whole row the drag surface (long press on touch) | |
OrderControls | ✓ | Move arrows and grip; placement="side" floats them beside the card, "footer" puts the arrows in the footer without the grip | |
Root | ✓ | ✓ | Card shell and context; with onClick, the whole card is a button (Enter and Space included) |
BoardCells | ✓ | ✓ | South hand, one suit per row |
Body | ✓ | ✓ | Right column |
Header | ✓ | ✓ | Board number, name (truncated, full name in a tooltip) and code |
UsageBadge | ✓ | ✓ | Whose deal it is: owned, public, author deleted |
SoftDeletedBadge | ✓ | ✓ | Renders only when isSoftDeleted |
Stats | ✓ | ✓ | Level, contract, player, lead, then one row per theme type; empty rows are skipped |
Footer | ✓ | ✓ | Action slot |
Remove | ✓ | Trash button; the host owns any confirmation, the press never starts a drag nor clicks the card |
Every part reads the card from Root's context, so they compose in any order and any subset. Parts other than Root, List, Item, OrderControls and Remove accept the props of a div.
BoardCardView
Read-only card, for a list nobody reorders. Click the card to count a preview.
import { BoardCardView } from '@workspace/ui-training/deal/board-card/board-card-view';
<BoardCardView.Root board={board} index={i + 1} onClick={() => openPreview(board.id)}>
<BoardCardView.BoardCells />
<BoardCardView.Body>
<BoardCardView.Header />
<BoardCardView.Stats />
</BoardCardView.Body>
</BoardCardView.Root>BoardCardEdit
Reorderable cards. List keeps the order local while a card is dragged and calls onReorder once on release, only when the order changed and the list did not change underneath. Its children is a function of the order shown, so the indexes and the move arrows follow the drag.
OrderControls sits beside Root, not inside it: when the host hides the controls, the card keeps no empty gutter.
EditableBoardCard
The row of an edited list, wired for both pointers: on desktop the controls float beside the card and a click opens the preview; on touch the whole card is the drag surface behind a long press, the arrows move to the footer, and a tap shows a "View deal" overlay, so a click never races a drop. disabled hides the controls and the footer; canReorder={false} hides the controls of a single-deal list.
UnavailableBoardCard keeps the place of a deal the viewer can no longer read: no data, but it moves and is removed like the others. Its title and description come from the host, which knows why the deal is gone.
import { BoardCardEdit } from '@workspace/ui-training/deal/board-card/board-card-edit';
import { EditableBoardCard } from '@workspace/ui-training/deal/board-card/editable-board-card';
<BoardCardEdit.List values={ids} onReorder={setIds}>
{(shown) => shown.map((id, i) => (
<EditableBoardCard
key={id}
board={boards[id]}
index={i}
canMoveUp={i > 0}
canMoveDown={i < shown.length - 1}
onMoveUp={() => move(i, i - 1)}
onMoveDown={() => move(i, i + 1)}
onPreview={() => openPreview(id)}
onRemove={() => confirmRemove(id)}
/>
))}
</BoardCardEdit.List>Prop
Type
BoardsListSkeleton
Loading placeholder of a card list, for the Suspense fallback.
BoardCardViewModel
Prop
Type
usage is a BoardUsageViewModel: owned when the viewer owns the deal, public for someone else's public deal, orphaned when its author is gone. themes reuses the DealRowThemes of the deal row.
In apps/app, toBoardCardViewModel(board, { currentUserId, lang }) builds it from the domain board, and EditableBoardRow loads the board by id, maps it and mounts the bridge context around an EditableBoardCard.
Strings
BoardCardStringsProvider and useBoardCardStrings (deal/board-card/strings), English defaults in defaultBoardCardStrings. In apps/app, BoardCardStringsBridge mounts them from the translations and takes per-screen overrides through strings, such as the remove label of a session or a collection.
Prop
Type