Mastering Astryx / lesson 6 of 13

Data Modeling with Astryx Icons & Semantic Variants

Define the TypeScript domain models, column metadata, priority mappings, and initial state — and watch the board render real data.

Play
Transcript

[00:00] So we’ve got our application here, we’ve got all these columns and what we want to do now is get something that represents more real data. So we want these columns to be like a Kanban board, to do in progress and review completed, stuff like that. And one thing we’re going to tap into, and you’re not really going to see a bunch of it in this lesson, but we are going to set things up for it, is semantic state that’s available to us in Astryx design system. So you can tap into all sorts of colors, red, green, blue, yada, yada, yada. But then they have semantic coloring like accent, neutral, warning, things of that nature. So we’re going to try to set ourselves up to use icons and colors that we can associate with the various states of this task. Now, like I said, most of this is going to be set up, but we will get some mock data in here, and turn these columns into real columns in this lesson.

[01:03] So one thing I want to point out is I should have had styled board columns on this guy. And there we go. Now we’ve got our proper columns. Okay. So what we’re going to do is jump into our app. We’re going to create a new file called types, and I’m just going to paste this in. Don’t worry. All the code will be available to you. There will be a link in the description where you can go get all this. I just don’t want to waste your time with a bunch of my slow typing.

[01:27] So what do we got here? We got column IDs to do in progress review and done. We’ve got priority, high, medium, and low. We’ve got a work item, which will be one of our tasks. So they all have, you know, which column are you in? What’s your priority? And so on. We have column meta. So this is going to be, you know, metadata for each individual column. And there we’re using the exact same keys in our union here for variance. So we’ve got neutral accent warning and success. Uh, and then we’ve got drop target and drag state, which I’m just putting in here now, cause we’re going to use it later. Uh, we don’t need to worry about that at this moment.

[02:09] So now what we need is some data. So we’re going to create data.ts. I’m going to drop this in, and this is all pretty straightforward. We’re bringing in some icons. We’re bringing in those types we just created again. Don’t worry about the drag threshold right now. Um, I am reproducing the column width here that we also had in styles. Uh, if you saw that lesson, you know that we need to have that hard coded in our styles. Uh, originally I tried to export it out of data into styles, and that’s not going to work with style X, uh, at least not the way that we’re using it. Um, so then we’ve got our columns, which is just a series of column metadata. So variants and tool tips and, uh, icons and things of that nature.

[02:53] Uh, and then we’ve got, uh, our priority meta, which is a record of priorities. So error warning teal, which is nice. And then we’ve got high, medium, and low. So we’ve got variants for each of those. Well, and that’s where that teal comes in on a low. I had to pick something. Um, and then we’ve got a bunch of initial work items. So that’s under initial items. Uh, you know, they’ve got IDs, which column they’re in their priority and so on. You will notice in each of these columns, I have this empty icon. Uh, that’s so that later we can render an empty state for each of these. So if there’s no to do items, we want to have something there that says, there’s no to do items and I’m going to be using an icon to help with that.

[03:39] So we’re going to jump over to our Kanban board and right here, I’m going to bring in the columns and the initial items that we just created in data. And right off the bat, we can replace this hard-coded for with initial items dot length. And we’re going to come down here where I was mapping over this kind of make-believe, and we’re going to replace it with our new make-believe tickets. I’m going to paste that in. We are iterating over each of our columns. And then we filter out to make sure that we’re putting the current item in the correct column. So we’re making sure that the items column matches the ID of the column. I probably don’t need to waste a bunch of time on that for you. Um, and then we drop everything in a card. Everything is going to look fairly similar.

[04:30] Uh, we’ve got, uh, the number of items are in this, uh, column and we list that off as tasks. And that’s going to be localized. Uh, and then everything else is stuff we’ve already done. We’ve got a little bit of X style going on here. Um, a lot of design tokens, everything’s looking good. We can load that up over here. And now we get, let me zoom this out a little bit. Uh, we get our actual columns with items in them. Uh, our light mode and dark mode still work. Our language toggle still works. And now we can move on to making these carts more full-fledged components in our application.

The shell, styling, i18n, and theming are in place. Now we give the board a real data model: TypeScript contracts, column metadata, priority mappings, and initial tasks. This is where the placeholder columns become the actual Kanban columns.

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

1. Domain Types (types.ts)

Create src/app/types.ts to define the domain contracts for tasks, columns, priorities, and drag state:

src/app/types.ts
import type { ComponentType } from 'react';
export type ColumnId = 'todo' | 'in-progress' | 'in-review' | 'done';
export type Priority = 'high' | 'medium' | 'low';
export interface WorkItem {
id: string;
column: ColumnId;
ref: string;
priority: Priority;
title: string;
description: string;
lastEdited: string;
dueDate: string;
}
export interface ColumnMeta {
id: ColumnId;
title: string;
variant: 'neutral' | 'accent' | 'warning' | 'success';
tooltip: string;
emptyTitle: string;
emptyDescription: string;
emptyIcon: ComponentType<{ className?: string }>;
}
export interface DropTarget {
column: ColumnId;
index: number;
}
export interface DragState {
id: string;
width: number;
height: number;
offsetX: number;
offsetY: number;
pointerX: number;
pointerY: number;
target: DropTarget | null;
}

Note how ColumnMeta.variant is typed to exactly the variants StatusDot accepts (neutral, accent, warning, success) — an invalid combination is a compile error instead of a runtime surprise. DragState and DropTarget are defined now and used by the drag hook in lesson 11.

2. Columns Metadata & Initial Data (data.ts)

Create src/app/data.ts to export static configuration metadata and sample data:

src/app/data.ts
import {
ArrowPathIcon,
CheckCircleIcon,
ClipboardDocumentCheckIcon,
InboxIcon,
} from '@heroicons/react/24/outline';
import type { ColumnMeta, Priority, WorkItem } from './types';
export const DRAG_THRESHOLD = 5;
export const COLUMN_WIDTH = 300;
export const COLUMNS: ColumnMeta[] = [
{
id: 'todo',
title: 'To-do',
variant: 'neutral',
tooltip: 'Items assigned to this sprint, waiting to be picked up.',
emptyTitle: 'To-do is empty',
emptyDescription: 'Items pulled into this sprint appear here.',
emptyIcon: InboxIcon,
},
{
id: 'in-progress',
title: 'In progress',
variant: 'accent',
tooltip: 'Items currently in progress.',
emptyTitle: 'Nothing in progress',
emptyDescription: 'Items being worked on appear here.',
emptyIcon: ArrowPathIcon,
},
{
id: 'in-review',
title: 'In review',
variant: 'warning',
tooltip: 'Items waiting for your review.',
emptyTitle: 'Nothing in review',
emptyDescription: 'Items awaiting your review appear here.',
emptyIcon: ClipboardDocumentCheckIcon,
},
{
id: 'done',
title: 'Done',
variant: 'success',
tooltip: 'Items that have been handled.',
emptyTitle: 'Nothing done yet',
emptyDescription: 'Completed items appear here.',
emptyIcon: CheckCircleIcon,
},
];
export const PRIORITY_META: Record<
Priority,
{ label: string; variant: 'error' | 'warning' | 'teal' }
> = {
high: { label: 'High', variant: 'error' },
medium: { label: 'Medium', variant: 'warning' },
low: { label: 'Low', variant: 'teal' },
};
export const INITIAL_ITEMS: WorkItem[] = [
{
id: 't1',
column: 'todo',
ref: 'Task 4821',
priority: 'low',
title: 'Draft project kickoff brief',
description: 'Write a short brief outlining goals, scope, and success criteria.',
lastEdited: '2h ago',
dueDate: 'Jul 8',
},
{
id: 'p1',
column: 'in-progress',
ref: 'Task 4825',
priority: 'high',
title: 'Design the landing page layout',
description: 'Create a first-pass layout for the landing page.',
lastEdited: '18m ago',
dueDate: 'Jul 3',
},
{
id: 'r1',
column: 'done',
ref: 'Task 4788',
priority: 'low',
title: 'Write the weekly status update',
description: 'Summarize progress, blockers, and next steps for the team.',
lastEdited: 'Yesterday',
dueDate: 'Jul 1',
},
];

COLUMN_WIDTH lives in two places on purpose. data.ts exports it as the domain-level source of truth for components that need the number. styles.ts (lesson 4) keeps its own local copy because StyleX inlines every value inside stylex.create at build time and can’t resolve the ./data import — see the gotcha note in lesson 4.

Astryx Concepts: Icons & Semantic Variants

This lesson’s data layer is deliberately design-system-aware — the models are shaped by Astryx’s vocabulary so they flow straight into components without translation:

This is the Astryx way to model domain data: define your contracts against the design system’s tokens and variants, not against ad-hoc strings.

Rendering the Data: KanbanBoard.tsx

Rewrite src/app/components/KanbanBoard.tsx to map COLUMNS into the styled column shells and INITIAL_ITEMS into task cards. Every style rule comes from styles.ts; every string that is user-facing goes through the translator:

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 { COLUMNS, INITIAL_ITEMS } from '../data';
export default function KanbanBoard() {
const t = useTranslator();
const { mode, setMode, locale, setLocale } = usePreferences();
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>
4 {t('@app.tasks')}
{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}>
{[0, 1, 2, 3].map(i => (
<Card
key={i}
variant="muted"
padding={0}
xstyle={styles.columnShell}
>
<VStack gap={3} padding={3}>
<Heading level={4}>Column {i + 1}</Heading>
<Card padding={3} xstyle={styles.card}>
<Heading level={5}>Placeholder task</Heading>
<Text type="supporting" color="secondary" maxLines={2}>
A placeholder card inside the styled column shell.
</Text>
</Card>
</VStack>
</Card>
))}
{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>
</Card>
))}
</VStack>
</VStack>
</Card>
);
})}
</HStack>
</LayoutContent>
}
/>
</Section>
);
}

The columns now come from COLUMNS (title, variant, and — soon — status dot and empty state), and the tasks come from INITIAL_ITEMS. Columns with no tasks render with just their header — the empty state arrives in lesson 9.

See It in Action

Run npm run dev. The board now shows three real columns with tasks (To-do has one, In progress has one, Done has one, In review is empty), the header counter reads the real total, and hovering any task card still lifts it. Flip the language and theme toggles — data-driven rendering composes with both.

What You Built

In the next lesson, we will extract the task presentation into its own component: BoardCardBody.


Share this post on:

Previous
Theme System & Design Tokens
Next
Presenting Work Items: Card, Badge & MoreMenu