Transcript
[00:00] Our application is looking groovy. We’ve got all these cards here. You know, they’re actually a board card bodies and that’s what we’re going to address in this lesson. We are going to create the actual board card, which is going to encapsulate pretty much everything we see right here.
[00:16] And we’re going to set this up that we, what we want to be able to do is drag these around. So we want to be able to forward references to the element and we want to be able to pass down, you know, what to do on a pointer event. So what we’re going to do is we’re going to create a new component called board card.tsx and drop this code in here.
[00:41] And what we’re looking at is we’re bringing in the card. We’re bringing in our board card body. So we’re no longer going to need that in the Kenbin. We’re bringing in our styles and our types of column ID and work item. We’ve got our board card props, which include a card ref. The item and on pointer down event and an on move event.
[01:02] And then we just render out almost exactly what we just had. But we’ve got our ref, which is going to be the underlying DOM element, more or less our board card body. And we’ve got our on pointer down, which is going to run whatever we send in as our on pointer down. And then we render our board card and we pass that the on move, which we did set up as part of the board party party board card body props in a previous lesson.
[01:31] So all of that’s kind of already ready to go. We just need to tell it what to do. We’re going to jump back over to our Kanban board. We are going to swap out board card body for board card. And then where we have our card and our board card body, we’re going to replace all of this with our board card, we’ve got our key or item right now, our ref is empty and our pointer down is empty.
[01:59] Our move item is still just this mock move item, but we will address that in another lesson. We’ll take a look and everything seems to be working, our move to do. So all of that’s getting forwarded down or to from board card, sorry, from Kanban board to board card to board card body. We’re just basic prop drilling stuff, passing those refs and those events.
The BoardCardBody from lesson 7 is the presentation. Now we give it a draggable shell: BoardCard — the interactive wrapper that applies custom hover elevation through the xstyle prop and forwards pointer press events for the drag engine in lesson 11.
Files this lesson: src/app/components/BoardCard.tsx (new), src/app/components/KanbanBoard.tsx (rewritten). page.tsx stays untouched.
Astryx this lesson: Card deep-dive — the xstyle prop for safe style overrides, StyleX pseudo-classes (:hover), and how Card forwards its DOM node and onPointerDown for custom interactions.
The Card Contract
Astryx Card gives us three things we need:
xstyle— merges our custom StyleX rules (fromstyles.cardin lesson 4) into the card without breaking its design-system styles.- Forwarded ref —
refexposes the underlyingdiv, which the drag hook will use for hit-testing geometry. onPointerDown— a pointer event hook the drag engine will listen to.
The elevation rules live in styles.card (lesson 4): a grab cursor, userSelect: 'none', touchAction: 'none', a 120ms box-shadow transition, and the --shadow-med hover elevation — with the dark-mode light-ring override on top. The component itself stays thin.
Implementation: BoardCard.tsx
Create src/app/components/BoardCard.tsx:
import type { PointerEvent as ReactPointerEvent } from 'react';import { Card } from '@astryxdesign/core/Card';import { BoardCardBody } from './BoardCardBody';import { styles } from '../styles';import type { ColumnId, WorkItem } from '../types';
interface BoardCardProps { item: WorkItem; cardRef: (el: HTMLDivElement | null) => void; onPointerDown: (e: ReactPointerEvent, id: string) => void; onMove: (id: string, to: ColumnId) => void;}
export function BoardCard({ item, cardRef, onPointerDown, onMove,}: BoardCardProps) { return ( <Card ref={cardRef} padding={3} xstyle={styles.card} onPointerDown={e => onPointerDown(e, item.id)} > <BoardCardBody item={item} onMove={onMove} /> </Card> );}xstyle={styles.card}applies the hover elevation and interaction flags.ref={cardRef}forwards the DOM node upward — lesson 11 registers it for hit-testing.onPointerDown={e => onPointerDown(e, item.id)}forwards the press event with the item id, so the drag engine knows which card the pointer grabbed.- Everything visual is delegated to
BoardCardBody, so the floating drag clone (lesson 12) can render a pixel-identical copy.
Using It: KanbanBoard.tsx
Rewrite src/app/components/KanbanBoard.tsx to render BoardCard per item instead of the inline Card + BoardCardBody combo. The cardRef and onPointerDown props are stubbed until the drag hook in lesson 11:
'use client';
import { Section } from '@astryxdesign/core/Section';import { Layout, LayoutHeader, LayoutContent, HStack, VStack,} from '@astryxdesign/core/Layout';import { Heading, Text } from '@astryxdesign/core/Text';import { Card } from '@astryxdesign/core/Card';import { Button } from '@astryxdesign/core/Button';import { useTranslator } from '@astryxdesign/core/i18n';import { usePreferences } from '../providers';import { styles } from '../styles';import { BoardCardBody } from './BoardCardBody';import { BoardCard } from './BoardCard';import { COLUMNS, INITIAL_ITEMS } from '../data';import type { ColumnId } from '../types';
export default function KanbanBoard() { const t = useTranslator(); const { mode, setMode, locale, setLocale } = usePreferences();
// Temporary — real moves arrive with the drag hook in lesson 11. const moveItem = (id: string, to: ColumnId) => { alert(`Move task ${id} to ${to}`); };
return ( <Section height="100dvh"> <Layout height="fill" header={ <LayoutHeader hasDivider padding={4}> <HStack hAlign="between" vAlign="center"> <Heading level={3}>{t('@app.board.title')}</Heading> <HStack gap={2} vAlign="center"> <Text type="supporting" color="secondary" hasTabularNumbers> {INITIAL_ITEMS.length} {t('@app.tasks')} </Text> <Button label={t('@app.switchLanguage')} variant="secondary" onClick={() => setLocale(locale === 'en' ? 'fr' : 'en')} /> <Button label={ mode === 'system' ? 'Light mode' : mode === 'light' ? 'Dark mode' : 'System mode' } variant="secondary" onClick={() => setMode( mode === 'system' ? 'light' : mode === 'light' ? 'dark' : 'system', ) } /> </HStack> </HStack> </LayoutHeader> } content={ <LayoutContent padding={0}> <HStack gap={4} xstyle={styles.boardColumns}> {COLUMNS.map(col => { const items = INITIAL_ITEMS.filter(it => it.column === col.id); return ( <Card key={col.id} variant="muted" padding={0} xstyle={styles.columnShell} > <VStack gap={3} padding={3}> <Heading level={4}>{col.title}</Heading> <Text type="supporting" color="secondary" hasTabularNumbers > {items.length} {t('@app.tasks')} </Text> <VStack gap={2}> {items.map(item => ( <Card key={item.id} padding={3} xstyle={styles.card} > <BoardCardBody item={item} onMove={moveItem} /> </Card> <BoardCard key={item.id} item={item} cardRef={() => {}} onPointerDown={() => {}} onMove={moveItem} /> ))} </VStack> </VStack> </Card> ); })} </HStack> </LayoutContent> } /> </Section> );}Note we no longer import Card for task cards (only for column shells) — BoardCard owns the card rendering.
See It in Action
Run npm run dev. The board looks the same as lesson 7 — but now every task is a dedicated BoardCard: hover elevation, grab cursor, no text selection on drag-attempt presses, and the MoreMenu still works inside (the onPointerDown stub does nothing yet). In lesson 11, these stubbed props come alive.
What You Built
BoardCard.tsx— a thin, reusable interactive card shell that forwards its DOM node and pointer events.- A board where every task card is a component ready for drag-and-drop wiring.
In the next lesson, we will build the column shell with StatusDot, Tooltip, and EmptyState.