Mastering Astryx / lesson 7 of 13

Presenting Work Items: Card, Badge & MoreMenu

Extract the work-item presentation into BoardCardBody using Card, Badge, Heading, Text, and MoreMenu.

Play
Transcript

[00:00] So in our last video, we created these columns with these groovy little cards. And what we’re gonna do now is we’re gonna expand on these cards quite a bit. So where we have them in this VStack, we’re gonna replace all that with a new component. And we’re gonna call that board card body.tsx. And the reason I’m calling that is ‘cause we’re gonna expand on this even more. And this is gonna end up being the body of a board card component. So I’m dropping this in here and we’re gonna take a quick look at it ‘cause there are some cool things.

[00:37] A lot of this you’ve seen before, HStack, VStack. We’ve got our badges, which is a new component we haven’t used before, but they’re pretty straightforward. I don’t think you’re gonna be super surprised by those. They’re just little rounded things around text. They do take in the same types of variants. We are utilizing our priority variant. We’ve brought in a more menu. So this is, you know, your three dots, your hamburger menu type thing. This is included with the Astryx design system. So we’ve got a label, a size, and then we’ve got our items. I’ve got those doing basically nothing right now. And that’s probably gonna remain the same. I am passing in this move targets, which we will take a closer look at a little bit later. One that I really like and I wanted to point out.

[01:29] So this is text. We’ve used a text component a bunch of times in this application. We’ve got our types and our coloring, and then we’ve got max lines. So what this does is this clamps for us. So this is gonna do our, you know, ellipses are. Don’t let it extend beyond two lines in this case, which is really, really cool. ‘Cause I know I’ve written that CSS and gone and looked it up a thousand times. So I’m really happy that they just incorporated that right into the text component where it belongs. So that’s really cool. Excellent job, Astryx folks.

[02:04] So we’re gonna jump over to our Kanban board. We’re gonna bring in our body card. Sorry, our board card body. And we’re gonna bring in column ID from types. Now, when it comes to the move, I’m gonna drop that in right here. This is just a temporary thing that’s gonna alert when we try to move. It’s not actually gonna do anything. All right, so we’re gonna come down here and in this V stack, we’re gonna keep the card, but we’re gonna drop the rest of its body. And we’re gonna drop in our board card body. I’m gonna save that. We’re gonna take a look over here and cool. Now our cards are really starting to take shape. We’ve got our badges and it’s recognizing the priority based on the mock data that we created. We’ve got a little more menu, which isn’t gonna do anything, but if I hit move to do, it’s gonna say, move task P1 to to do. It doesn’t actually move it yet though. Everything else seems to be working, our light mode, our dark mode, our language switcher. All of this is working. This is looking great.

The board renders tasks inline right now. In this lesson we extract the presentation into its own component — BoardCardBody — the reusable body shown both inside columns and, later, inside the floating drag clone.

Files this lesson: src/app/components/BoardCardBody.tsx (new), src/app/components/KanbanBoard.tsx (rewritten). page.tsx stays untouched.

Astryx this lesson: Card, Badge, Heading, Text, MoreMenu. Concepts: content hierarchy (heading levels, type="supporting"/color="secondary"), line clamping (maxLines), and the MoreMenu items contract (actions + dividers).

Component Features

  1. Header Row: Task reference badge (Badge variant="neutral"), priority badge (Badge variant={priority.variant}), and contextual actions dropdown (MoreMenu).
  2. Title & Description: Heading level={5} and clamped description Text maxLines={2}.
  3. Metadata Footer: Secondary text displaying last edited time and due date.
  4. Move Actions: Dynamically generates "Move to [Column]" menu items for non-current columns.
  5. MoreMenu items contract: each item is either an action ({ label, onClick }) or a separator ({ type: 'divider' }) — the same contract Astryx’s menu system uses across the design system.

Implementation: BoardCardBody.tsx

Create src/app/components/BoardCardBody.tsx:

src/app/components/BoardCardBody.tsx
import { HStack, VStack } from '@astryxdesign/core/Layout';
import { Heading, Text } from '@astryxdesign/core/Text';
import { Badge } from '@astryxdesign/core/Badge';
import { MoreMenu } from '@astryxdesign/core/MoreMenu';
import { COLUMNS, PRIORITY_META } from '../data';
import type { ColumnId, WorkItem } from '../types';
interface BoardCardBodyProps {
item: WorkItem;
onMove: (id: string, to: ColumnId) => void;
}
export function BoardCardBody({ item, onMove }: BoardCardBodyProps) {
const priority = PRIORITY_META[item.priority];
// Dynamic move options for the MoreMenu dropdown
const moveTargets = COLUMNS
.filter(c => c.id !== item.column)
.map(c => ({
label: `Move to ${c.title}`,
onClick: () => onMove(item.id, c.id),
}));
return (
<VStack gap={2}>
<HStack hAlign="between" vAlign="start">
<HStack gap={1} vAlign="center" wrap="wrap">
<Badge label={item.ref} variant="neutral" />
<Badge label={priority.label} variant={priority.variant} />
</HStack>
<MoreMenu
label="Work item actions"
size="sm"
items={[
{ label: 'Open', onClick: () => {} },
{ label: 'Assign to me', onClick: () => {} },
{ type: 'divider' },
...moveTargets,
]}
/>
</HStack>
<VStack gap={1}>
<Heading level={5}>{item.title}</Heading>
<Text type="supporting" color="secondary" maxLines={2}>
{item.description}
</Text>
</VStack>
<Text type="supporting" color="secondary">
Edited {item.lastEdited} · Due {item.dueDate}
</Text>
</VStack>
);
}

Read the pieces:

By decoupling the body from the draggable container, we can render the exact same UI inside column lists and inside the floating drag preview clone (lesson 12).

Using It: KanbanBoard.tsx

Rewrite src/app/components/KanbanBoard.tsx so the task card in each column renders a BoardCardBody (and a temporary alert stub for moves — real state-driven moves arrive with the drag hook in lesson 11):

src/app/components/KanbanBoard.tsx
'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 { 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}
>
<Heading level={5}>{item.title}</Heading>
<Text
type="supporting"
color="secondary"
maxLines={2}
>
{item.description}
</Text>
<BoardCardBody item={item} onMove={moveItem} />
</Card>
))}
</VStack>
</VStack>
</Card>
);
})}
</HStack>
</LayoutContent>
}
/>
</Section>
);
}

Each task card is now a Card shell (hoverable via styles.card) wrapping a BoardCardBody. The body carries the badges, title, description, metadata footer, and the actions menu.

See It in Action

Run npm run dev. Task cards now show the reference badge, the priority badge (colored per PRIORITY_META), a clamped description, the Edited/Due footer, and a menu. Open a card’s menu and pick Move to … — you get an alert stub for now. Flip the theme toggle: the priority badge colors adapt to the dark token set automatically.

What You Built

In the next lesson, we will wrap this body in BoardCard.tsx with pointer drag listeners.


Share this post on:

Previous
Data Modeling with Astryx Icons & Semantic Variants
Next
Interactive Cards & the xstyle Prop