Skip to content

Mastering Astryx / lesson 13 of 13

Final Assembly: Composing the Astryx Application

Review the complete Kanban application — every file, how it composes, and the full Astryx vocabulary you have mastered.

The board is already fully functional — it was assembled piece by piece across the last twelve lessons. In this final lesson we step back: the complete file tree, the full KanbanBoard as one reference, and a recap of every Astryx concept you used.

Files this lesson: none to create — page.tsx is still untouched since lesson 1, and the board is complete.

The Completed Application

src/
└── app/
├── globals.css # CSS layer imports + @stylex directive
├── layout.tsx # Root layout (written in lesson 1)
├── page.tsx # <KanbanBoard /> (written once in lesson 1)
├── providers.tsx # Theme + i18n providers + usePreferences()
├── messages.ts # en/fr translation catalogs
├── types.ts # Domain contracts
├── data.ts # Column configs, priorities, initial items
├── styles.ts # Centralized StyleX stylesheet
├── hooks/
│ └── useKanbanDrag.ts # Drag-and-drop interaction hook
└── components/
├── BoardCardBody.tsx # Work item presentation
├── BoardCard.tsx # Interactive card shell
├── BoardColumn.tsx # Column shell & empty state
├── BoardToolbar.tsx # Top bar: sprint, actions, theme & language toggles
├── FloatingCard.tsx # Fixed drag overlay clone
└── KanbanBoard.tsx # The board — root composition

Notice what didn’t happen: page.tsx was written once in lesson 1 and never touched again. Everything else grew in place, one lesson at a time.

The Full Reference: KanbanBoard.tsx

The board is the composition root — it owns state (items, sprint), calls the drag hook, and renders the shell, toolbar, columns, and floating overlay. Here is the complete file exactly as it stands after lesson 12:

src/app/components/KanbanBoard.tsx
'use client';
import { useState, type ReactNode } from 'react';
import { Section } from '@astryxdesign/core/Section';
import {
Layout,
LayoutHeader,
LayoutContent,
HStack,
VStack,
} from '@astryxdesign/core/Layout';
import { styles } from '../styles';
import { BoardCard } from './BoardCard';
import { BoardColumn } from './BoardColumn';
import { BoardToolbar } from './BoardToolbar';
import { FloatingCard } from './FloatingCard';
import { useKanbanDrag } from '../hooks/useKanbanDrag';
import { COLUMNS, INITIAL_ITEMS } from '../data';
import type { ColumnId, WorkItem } from '../types';
export default function KanbanBoard() {
const [items, setItems] = useState<WorkItem[]>(INITIAL_ITEMS);
const [sprint, setSprint] = useState('003');
const { drag, itemsByColumn, getColumnRef, getCardRef, onCardPointerDown } =
useKanbanDrag(items, setItems);
const moveItem = (id: string, to: ColumnId) => {
setItems(prev =>
prev.map(item => (item.id === id ? { ...item, column: to } : item)),
);
};
const renderColumnCards = (colId: ColumnId): ReactNode => {
const colItems = itemsByColumn[colId];
const visible = drag ? colItems.filter(it => it.id !== drag.id) : colItems;
const ghostTarget =
drag && drag.target && drag.target.column === colId ? drag : null;
if (visible.length === 0 && !ghostTarget) {
return null;
}
const nodes: ReactNode[] = visible.map(it => (
<BoardCard
key={it.id}
item={it}
cardRef={getCardRef(it.id)}
onPointerDown={onCardPointerDown}
onMove={moveItem}
/>
));
if (ghostTarget && ghostTarget.target) {
const index = Math.min(ghostTarget.target.index, nodes.length);
nodes.splice(
index,
0,
<VStack key="drag-ghost" xstyle={styles.ghost(ghostTarget.height)} />,
);
}
return <VStack gap={2}>{nodes}</VStack>;
};
const draggedItem = drag ? items.find(it => it.id === drag.id) : undefined;
return (
<Section height="100dvh">
<Layout
height="fill"
header={
<LayoutHeader hasDivider padding={4}>
<BoardToolbar
totalTasks={items.length}
sprint={sprint}
onSprintChange={setSprint}
dragStatus={
drag
? `Dragging ${drag.id} → ${
drag.target
? `${drag.target.column} #${drag.target.index}`
: 'no target'
}`
: undefined
}
/>
</LayoutHeader>
}
content={
<LayoutContent padding={0}>
<HStack gap={4} xstyle={styles.boardColumns}>
{COLUMNS.map(meta => (
<BoardColumn
key={meta.id}
meta={meta}
count={itemsByColumn[meta.id].length}
contentRef={getColumnRef(meta.id)}
>
{renderColumnCards(meta.id)}
</BoardColumn>
))}
</HStack>
</LayoutContent>
}
/>
{drag && draggedItem ? (
<FloatingCard drag={drag} item={draggedItem} />
) : null}
</Section>
);
}

What the Board Does

Run npm run dev and visit http://localhost:3000. The finished board:

Astryx Vocabulary Recap

AreaAstryx vocabularyWhere you used it
ProvidersTheme, InternationalizationProvider, usePreferences contextApp root: theme, i18n, app state (lessons 1, 3, 5)
LayoutSection, Layout, LayoutHeader, LayoutContent, HStack, VStackApp shell, column shells, card content (lessons 2, 9, 11–12)
TypographyHeading, Texttype, color, maxLines, hasTabularNumbersTitles, descriptions, tabular counters (lessons 3, 6–7, 9)
FeedbackCard, Badge, StatusDot, EmptyState, TooltipBoard shells, badges, status dots, empty columns (lessons 6–9)
ActionsToolbar, Selector, Divider, IconButton, Button, Icon, MoreMenuTop action bar, contextual menus (lessons 7, 10)
Stylingxstyle + StyleX — static rules, pseudo-classes, media queries, dynamic functionsHover elevation, floating overlay, ghost slot (lessons 4, 8, 12)
Tokens--spacing-*, --shadow-*, --color-*, --radius-*Every custom rule in styles.ts (lesson 5)
i18nuseTranslator, catalogs, locale chainsHeader, toolbar, counters (lessons 3, 10)

Course Summary

Congratulations! You have completed the Mastering Astryx course.

You learned how to:

Most importantly, you built one real application — a Kanban board — in place, lesson by lesson.


Share this post on:

Previous
Floating Drag Overlays & Dynamic StyleX Functions