Transcript
[00:00] So in our current application, each of these columns is being rendered out as a card. Where are we? So column stop map. And then we just render all these guys out. We are going to roll that up into a board column component. And we’re going to introduce new components from the Astryx design system. So we’re going to create a new component here called board column. We’re going to drop this guy in and we are going to take a quick look at it.
[00:28] So we’re bringing in status dot, which is exactly what it sounds like. We’re going to use some semantic colors there. We are bringing in tool tip, which I believe is our first tool tip. So if we scroll down here, we’ve got a layout within a card. We’ve got a layout header. We’ve got a tool tip on that guy underneath the heading. So we get a tool tip with an icon. It’s the size of small color of secondary and we’re implementing our empty state. So empty state is a component from Astryx design.
[01:02] So it’s not something we created. It is an empty state that’s meant to be utilized inside of these other components. So you can see we’ve got that as a child of a layout content component. This is going to work great. You can look into all the different attributes of it, but what I’m running with is compact. We are extending the style using X style from our styles column empty state. And then we’ve got an icon there of a empty icon, whatever that is. And we set that up in our data, I believe. Yeah.
[01:34] So each of these columns has an empty icon. So done is the check circle. Uh, in review is a clipboard document check icon and so on. So it’s picking up on each of those elements and will show us an empty state and empty title and an empty description that it gets from all of that mock data that we generated earlier. So we’ll save that. We’re going to jump over to our Kanban board up here, say right after board card, we’re going to bring in our board column. And then right here where we had this card and then all of our board cards, we are going to replace that with our board column. And then we’ve got all our board cards.
[02:12] Uh, so we save that and we can jump over here and we see our various colorings for the, uh, status dot. So we’ve got our neutral are in progress or in review and are done. We’ve got a count of how many tasks are in each column. And you can see here in our in review, which I believe the icon, let’s see. In review, the icon was clipboard document check icon. You can see we’ve got nothing in review items waiting, uh, for your review appear here. So if we wanted to, I could change that to this icon and you see, we’ve got that icon. Okay.
[02:54] Uh, so this is really starting to come together. And the next thing we’re going to do in the next lesson is work on this toolbar up at the top and just make it a bit more dynamic, kind of cool looking, bring in some icon buttons, some, uh, dropdowns, things of that nature. Just start to explore the components available to us in the Astryx design system. Okay.
The columns are inline in KanbanBoard right now. In this lesson we extract them into BoardColumn — the container component that gives each column its status dot, tooltip, live counter, and empty-state fallback.
Files this lesson: src/app/components/BoardColumn.tsx (new), src/app/components/KanbanBoard.tsx (rewritten). page.tsx stays untouched.
Astryx this lesson: Card (muted variant), nested Layout/LayoutHeader/LayoutContent, StatusDot, Heading, Tooltip, Icon, Text, EmptyState. Concepts: status dots, tooltip overlays, tabular counters (hasTabularNumbers), and compact empty states.
Component Design
- Header:
StatusDot: Visual variant indicator (neutral,accent,warning,success) — fed straight fromColumnMeta.variant.Heading level={4}: Column title.Tooltip&Icon: Informational tooltip describing column purpose (meta.tooltip).Text hasTabularNumbers: Live item counter.
- Content Area:
- A padded container hosting task cards — or a compact
EmptyStatefallback when no items are present (using the column’smeta.emptyIcon).
- A padded container hosting task cards — or a compact
Implementation: BoardColumn.tsx
Create src/app/components/BoardColumn.tsx:
import type { ReactNode } from 'react';import { Card } from '@astryxdesign/core/Card';import { Layout, LayoutHeader, LayoutContent, HStack,} from '@astryxdesign/core/Layout';import { StatusDot } from '@astryxdesign/core/StatusDot';import { Heading, Text } from '@astryxdesign/core/Text';import { Tooltip } from '@astryxdesign/core/Tooltip';import { Icon } from '@astryxdesign/core/Icon';import { EmptyState } from '@astryxdesign/core/EmptyState';import { InformationCircleIcon } from '@heroicons/react/24/outline';import { styles } from '../styles';import type { ColumnMeta } from '../types';
interface BoardColumnProps { meta: ColumnMeta; count: number; contentRef: (el: HTMLDivElement | null) => void; children: ReactNode;}
export function BoardColumn({ meta, count, contentRef, children,}: BoardColumnProps) { return ( <Card variant="muted" padding={0} xstyle={styles.columnShell}> <Layout height="fill" header={ <LayoutHeader hasDivider padding={3}> <HStack hAlign="between" vAlign="center"> <HStack gap={2} vAlign="center"> <StatusDot variant={meta.variant} label={`${meta.title} status`} /> <Heading level={4}>{meta.title}</Heading> <Tooltip content={meta.tooltip}> <Icon icon={InformationCircleIcon} size="sm" color="secondary" /> </Tooltip> </HStack> <Text type="supporting" color="secondary" hasTabularNumbers> {count} </Text> </HStack> </LayoutHeader> } content={ <LayoutContent ref={contentRef} padding={2}> {children ?? ( <EmptyState isCompact xstyle={styles.columnEmptyState} icon={ <Icon icon={meta.emptyIcon} size="lg" color="secondary" /> } title={meta.emptyTitle} description={meta.emptyDescription} /> )} </LayoutContent> } /> </Card> );}Notes:
StatusDot variant={meta.variant}flows the typed variant straight from the data model —accent,warning,success,neutral.Tooltip content={meta.tooltip}wraps the info icon; hover it for the column’s purpose.Text hasTabularNumberskeeps the live counter aligned as it changes.LayoutContent ref={contentRef}forwards the scrollable node — the drag hook in lesson 11 registers it for hit-testing.children ?? <EmptyState …/>— when a column has no cards, the compact empty state (with the column’s own icon and copy) appears instead.- The muted
Cardshell reusesstyles.columnShellfor the fixed-width, full-height sizing from lesson 4.
Using It: KanbanBoard.tsx
Rewrite src/app/components/KanbanBoard.tsx to render BoardColumn per COLUMNS entry, passing the column’s BoardCard list as children:
'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 { BoardCard } from './BoardCard';import { BoardColumn } from './BoardColumn';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 => ( <BoardCard key={item.id} item={item} cardRef={() => {}} onPointerDown={() => {}} onMove={moveItem} /> ))} </VStack> </VStack> </Card> <BoardColumn key={col.id} meta={col} count={items.length} contentRef={() => {}} > {/* null children trigger BoardColumn's EmptyState fallback */} {items.length > 0 ? ( <VStack gap={2}> {items.map(item => ( <BoardCard key={item.id} item={item} cardRef={() => {}} onPointerDown={() => {}} onMove={moveItem} /> ))} </VStack> ) : null} </BoardColumn> ); })} </HStack> </LayoutContent> } /> </Section> );}The column header, counter, tooltip, and empty-state logic now all live in BoardColumn. The “In review” column — which has no tasks — renders its compact EmptyState automatically.
See It in Action
Run npm run dev. Each column now has a colored status dot, a hoverable info tooltip, and an aligned counter. The empty In review column shows its compact empty state with the clipboard icon. Hover a column title’s info icon to see the tooltip; the counters use tabular numbers so they stay aligned.
What You Built
BoardColumn.tsx— the full column shell: status dot, tooltip, tabular counter, scrollable content, and empty-state fallback.- A board that delegates column rendering to a dedicated component with
contentRefready for the drag hook.
In the next lesson, we will build the top application action toolbar — and move the theme and language toggles into it.