Bridge Training
UI Training

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

OrderControls
BoardCells
KQ74
A962
85
J73
South hand
Header
1Stayman after 1NT#K7P2QX
UsageBadge
Owned
Stats
LevelLevel 2Contract4PlayerSouthLeadQ
BiddingStayman
PlayTrump management
Footer › Remove
<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>
PartViewEditRole
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