In this chapter, we’re laying the visual and interactive groundwork for Kanbots: a functional Kanban board. This isn’t just about pretty pixels; it’s about creating the canvas where our AI agents will operate. By the end of this milestone, you will have a desktop application with a fully interactive Kanban board, allowing you to add, edit, and move task cards between columns. This core UI is essential for managing the AI-driven development tasks we’ll introduce later.
This chapter is critical because a robust, intuitive UI is the user’s direct interface to the powerful AI orchestration we’re building. Without a solid foundation here, managing complex multi-agent workflows would be cumbersome. When we finish, you’ll be able to confirm that you can manage tasks efficiently, setting the stage for integrating AI automation.
Planning the Kanban Board Structure
Our Kanbots application will present a familiar Kanban interface: columns representing stages (e.g., “To Do,” “In Progress,” “Done”) and cards representing individual tasks. Each card will eventually host one or more AI agents.
Core Components and Data Flow
We’ll use Svelte 5’s reactivity model and component-based architecture to build the UI. The state of our Kanban board (columns and cards) will be managed using Svelte stores, providing a centralized, reactive data source.
The primary components we’ll build are:
Board.svelte: The top-level component that orchestrates columns.Column.svelte: Represents a single Kanban column, managing its title and the list of cards within it.Card.svelte: Displays an individual task card, including its title and description.
Data will flow from the Board component, which holds the overall state, down to Column and then Card components via props. User interactions (e.g., adding a card, dragging a card) will trigger updates to the Svelte store, which in turn will reactively update the UI.
📌 Key Idea: A clear component hierarchy and centralized state management are crucial for building complex, interactive UIs that are easy to reason about and maintain.
Project File Structure
Within our existing Tauri project (initialized in Chapter 1), we’ll focus on the src-tauri/src/main.rs (minimal for now) and src/ directory for the Svelte frontend.
kanbots/
├── src-tauri/
│ ├── src/
│ │ └── main.rs
│ └── Cargo.toml
└── src/
├── App.svelte // Main Svelte application
├── main.ts // Svelte entry point
├── lib/
│ ├── components/
│ │ ├── Board.svelte
│ │ ├── Column.svelte
│ │ └── Card.svelte
│ └── stores.ts // Svelte stores for state management
└── index.css // Global stylesStep-by-Step Implementation
Let’s build out our Kanban board incrementally. We’ll start with the data model, then the components, and finally, the drag-and-drop functionality.
1. Define Data Models and Svelte Store
First, we need to define the structure for our cards and columns. We’ll use TypeScript for type safety and create a Svelte writable store to manage the board’s state.
Create src/lib/stores.ts:
// src/lib/stores.ts
import { writable } from 'svelte/store';
import { v4 as uuidv4 } from 'uuid'; // For unique IDs
// Define the shape of a Card
export interface Card {
id: string;
title: string;
description: string;
columnId: string; // To link cards to columns
}
// Define the shape of a Column
export interface Column {
id: string;
title: string;
}
// Initial board data
const initialColumns: Column[] = [
{ id: 'todo', title: 'To Do' },
{ id: 'in-progress', title: 'In Progress' },
{ id: 'done', title: 'Done' },
];
const initialCards: Card[] = [
{ id: uuidv4(), title: 'Set up Tauri + Svelte', description: 'Initialize the project and basic structure.', columnId: 'todo' },
{ id: uuidv4(), title: 'Design Kanban UI', description: 'Sketch out the layout for columns and cards.', columnId: 'in-progress' },
{ id: uuidv4(), title: 'Implement Card Drag & Drop', description: 'Enable moving cards between columns.', columnId: 'in-progress' },
{ id: uuidv4(), title: 'Write Chapter 1', description: 'Initial project setup documentation.', columnId: 'done' },
];
// Svelte writable stores
export const columns = writable<Column[]>(initialColumns);
export const cards = writable<Card[]>(initialCards);
// Function to add a new card
export function addCard(columnId: string, title: string, description: string) {
cards.update(currentCards => [
...currentCards,
{ id: uuidv4(), title, description, columnId }
]);
}
// Function to update a card
export function updateCard(cardId: string, newTitle: string, newDescription: string) {
cards.update(currentCards =>
currentCards.map(card =>
card.id === cardId ? { ...card, title: newTitle, description: newDescription } : card
)
);
}
// Function to move a card between columns
export function moveCard(cardId: string, targetColumnId: string) {
cards.update(currentCards =>
currentCards.map(card =>
card.id === cardId ? { ...card, columnId: targetColumnId } : card
)
);
}Explanation:
writablefromsvelte/storecreates reactive stores. Whencardsorcolumnsare updated, any components subscribing to them will automatically re-render.CardandColumninterfaces define our data types.uuidv4from theuuidlibrary (install it:npm install uuid @types/uuid) ensures unique IDs for cards, which is crucial for identifying and manipulating them.initialColumnsandinitialCardsprovide some starting data.addCard,updateCard, andmoveCardare helper functions to modify thecardsstore, encapsulating the update logic.
2. Create the Card Component
This component will display a single task card and make it draggable.
Create src/lib/components/Card.svelte:
<!-- src/lib/components/Card.svelte -->
<script lang="ts">
import { cards, updateCard } from '$lib/stores';
import type { Card } from '$lib/stores';
export let card: Card;
let isEditing = false;
let editedTitle = card.title;
let editedDescription = card.description;
function handleSave() {
updateCard(card.id, editedTitle, editedDescription);
isEditing = false;
}
function handleDragStart(event: DragEvent) {
if (event.dataTransfer) {
event.dataTransfer.setData('text/plain', card.id);
event.dataTransfer.effectAllowed = 'move';
}
}
</script>
<div
class="card"
draggable="true"
on:dragstart={handleDragStart}
on:dblclick={() => (isEditing = true)}
>
{#if isEditing}
<input type="text" bind:value={editedTitle} class="card-input-title" />
<textarea bind:value={editedDescription} class="card-input-description"></textarea>
<button on:click={handleSave} class="card-save-button">Save</button>
<button on:click={() => (isEditing = false)} class="card-cancel-button">Cancel</button>
{:else}
<h3>{card.title}</h3>
<p>{card.description}</p>
{/if}
</div>
<style>
.card {
background-color: var(--card-bg);
border-radius: 8px;
padding: 12px;
margin-bottom: 10px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
cursor: grab;
transition: transform 0.1s ease-in-out;
}
.card:active {
cursor: grabbing;
}
.card h3 {
margin-top: 0;
margin-bottom: 8px;
color: var(--text-color);
}
.card p {
font-size: 0.9em;
color: var(--text-color-light);
margin-bottom: 0;
}
.card-input-title, .card-input-description {
width: calc(100% - 16px);
padding: 8px;
margin-bottom: 8px;
border: 1px solid var(--border-color);
border-radius: 4px;
background-color: var(--input-bg);
color: var(--text-color);
}
.card-save-button, .card-cancel-button {
padding: 6px 10px;
margin-right: 5px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.85em;
}
.card-save-button {
background-color: var(--primary-color);
color: white;
}
.card-cancel-button {
background-color: var(--secondary-color);
color: var(--text-color);
}
</style>Explanation:
export let card: Card;declares a prop that expects aCardobject.draggable="true"makes thedivelement draggable.on:dragstart={handleDragStart}attaches an event listener. InsidehandleDragStart,event.dataTransfer.setData('text/plain', card.id)stores the card’s ID, which we’ll use to identify the dragged card.on:dblclicktoggles an editing state, allowing users to update card details.- Svelte’s
bind:valueis used for two-way data binding with input fields. - The
styleblock provides basic styling. We’ll define CSS variables soon.
3. Create the Column Component
This component will render a column title, display its cards, and handle dropping cards.
Create src/lib/components/Column.svelte:
<!-- src/lib/components/Column.svelte -->
<script lang="ts">
import { cards, moveCard, addCard } from '$lib/stores';
import type { Column, Card } from '$lib/stores';
import CardComponent from './Card.svelte';
export let column: Column;
// Filter cards belonging to this column
$: columnCards = $cards.filter(card => card.columnId === column.id);
let showAddCardForm = false;
let newCardTitle = '';
let newCardDescription = '';
function handleAddCard() {
if (newCardTitle.trim()) {
addCard(column.id, newCardTitle, newCardDescription);
newCardTitle = '';
newCardDescription = '';
showAddCardForm = false;
}
}
function handleDragOver(event: DragEvent) {
event.preventDefault(); // Crucial to allow dropping
if (event.dataTransfer) {
event.dataTransfer.dropEffect = 'move';
}
}
function handleDrop(event: DragEvent) {
event.preventDefault();
const cardId = event.dataTransfer?.getData('text/plain');
if (cardId) {
moveCard(cardId, column.id);
}
}
</script>
<div
class="column"
on:dragover={handleDragOver}
on:drop={handleDrop}
>
<h2>{column.title}</h2>
<div class="cards-container">
{#each columnCards as card (card.id)}
<CardComponent card={card} />
{/each}
</div>
{#if showAddCardForm}
<div class="add-card-form">
<input type="text" bind:value={newCardTitle} placeholder="Card title" class="add-card-input" />
<textarea bind:value={newCardDescription} placeholder="Description (optional)" class="add-card-textarea"></textarea>
<button on:click={handleAddCard} class="add-card-button">Add</button>
<button on:click={() => showAddCardForm = false} class="cancel-card-button">Cancel</button>
</div>
{:else}
<button on:click={() => showAddCardForm = true} class="add-card-toggle-button">+ Add Card</button>
{/if}
</div>
<style>
.column {
flex: 1;
min-width: 280px;
max-width: 320px;
background-color: var(--column-bg);
border-radius: 10px;
padding: 15px;
margin: 0 10px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
height: fit-content; /* Allow column to grow with content */
min-height: 200px; /* Minimum height for drag-and-drop target */
}
.column h2 {
margin-top: 0;
margin-bottom: 20px;
color: var(--text-color);
text-align: center;
}
.cards-container {
flex-grow: 1;
}
.add-card-toggle-button {
background-color: var(--secondary-color);
color: var(--text-color);
border: none;
padding: 10px 15px;
border-radius: 6px;
cursor: pointer;
margin-top: 15px;
width: 100%;
font-size: 1em;
transition: background-color 0.2s ease;
}
.add-card-toggle-button:hover {
background-color: var(--secondary-hover);
}
.add-card-form {
display: flex;
flex-direction: column;
margin-top: 15px;
}
.add-card-input, .add-card-textarea {
width: calc(100% - 16px);
padding: 8px;
margin-bottom: 8px;
border: 1px solid var(--border-color);
border-radius: 4px;
background-color: var(--input-bg);
color: var(--text-color);
}
.add-card-button, .cancel-card-button {
padding: 8px 12px;
margin-top: 5px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.9em;
}
.add-card-button {
background-color: var(--primary-color);
color: white;
}
.cancel-card-button {
background-color: var(--secondary-color);
color: var(--text-color);
}
</style>Explanation:
export let column: Column;expects aColumnobject.$: columnCards = $cards.filter(card => card.columnId === column.id);is a Svelte reactive declaration. It automatically re-runs whenever the$cardsstore changes, filtering cards relevant to this column.on:dragover={handleDragOver}is vital.event.preventDefault()allows a drop to occur.on:drop={handleDrop}retrieves thecardIdfromevent.dataTransferand callsmoveCardfrom our store to update the card’s column.- An “Add Card” button and form are included to create new cards directly within a column.
4. Create the Board Component
This component will arrange the columns and provide overall structure.
Create src/lib/components/Board.svelte:
<!-- src/lib/components/Board.svelte -->
<script lang="ts">
import { columns } from '$lib/stores';
import ColumnComponent from './Column.svelte';
</script>
<div class="board">
{#each $columns as column (column.id)}
<ColumnComponent column={column} />
{/each}
</div>
<style>
.board {
display: flex;
flex-wrap: nowrap; /* Prevent columns from wrapping */
padding: 20px;
gap: 20px; /* Space between columns */
overflow-x: auto; /* Allow horizontal scrolling if many columns */
height: 100vh; /* Take full viewport height */
align-items: flex-start; /* Align columns to the top */
background-color: var(--board-bg);
}
</style>Explanation:
{#each $columns as column (column.id)}iterates over thecolumnsstore. The$prefix automatically subscribes to the store.(column.id)is a Svelte key, which helps optimize list rendering.- Each
ColumnComponentreceives its respectivecolumndata as a prop. - The
boardclass uses Flexbox to lay out columns horizontally.
5. Update App.svelte and Global Styles
Now, let’s integrate the Board component into our main App.svelte and add global styles.
Update src/App.svelte:
<!-- src/App.svelte -->
<script lang="ts">
import Board from './lib/components/Board.svelte';
</script>
<main>
<Board />
</main>
<style>
:global(body) {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background-color: var(--board-bg); /* Ensure body matches board background */
}
:global(*) {
box-sizing: border-box;
}
main {
display: flex;
min-height: 100vh; /* Ensure main takes full height */
width: 100vw; /* Ensure main takes full width */
}
</style>Add src/index.css (create this file if it doesn’t exist):
/* src/index.css */
:root {
/* Color Palette */
--primary-color: #4CAF50; /* Green for success/primary actions */
--secondary-color: #FFC107; /* Amber for warnings/secondary actions */
--accent-color: #2196F3; /* Blue for info/highlights */
--danger-color: #F44336; /* Red for errors/destructive actions */
/* Theming - Dark Mode Inspired */
--background-color: #1a1a1a; /* Overall app background */
--text-color: #e0e0e0; /* Primary text color */
--text-color-light: #b0b0b0; /* Secondary text color */
--border-color: #333; /* Borders and dividers */
--shadow-color: rgba(0, 0, 0, 0.3); /* Shadow color */
/* Kanban Specific */
--board-bg: #282c34; /* Dark background for the entire board area */
--column-bg: #3c4048; /* Slightly lighter dark for columns */
--card-bg: #555c66; /* Even lighter dark for cards */
--input-bg: #444; /* Input field background */
/* Hover States */
--primary-hover: #45a049;
--secondary-hover: #ffca28;
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background-color: var(--background-color);
color: var(--text-color);
}
/* Scrollbar styling for a cleaner look */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--background-color);
}
::-webkit-scrollbar-thumb {
background: var(--column-bg);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--card-bg);
}Explanation:
- The
:global(body)and:global(*)selectors apply styles globally, necessary forindex.cssto be effective within Svelte’s scoped styling. mainis set toflexto ensure theBoardcomponent fills the available space.index.cssdefines a set of CSS variables (:root) for a consistent dark theme, making it easy to adjust colors later. These variables are then used throughout the components.
6. Install uuid and TypeScript Types
If you haven’t already, install the uuid library and its TypeScript types:
npm install uuid
npm install -D @types/uuidThis ensures uuidv4() is available and correctly typed in stores.ts.
Testing & Verification
Now that the core UI is in place, let’s run the application and verify its functionality.
Start the Tauri development server: Navigate to your project root in the terminal and run:
npm run tauri devThis command will compile your Rust backend, start the Svelte development server, and launch the Tauri desktop window.
Verify Initial Board State:
- You should see a desktop window open with three columns: “To Do,” “In Progress,” and “Done.”
- Each column should contain the initial cards defined in
src/lib/stores.ts.
Test Card Creation:
- Click the “+ Add Card” button in any column.
- Enter a title and an optional description.
- Click “Add.” A new card should appear at the bottom of that column.
Test Card Editing:
- Double-click on any card.
- Edit its title and/or description.
- Click “Save.” The card should update with the new information.
- Click “Cancel” to discard changes.
Test Drag and Drop:
- Click and hold on a card, then drag it to another column.
- Release the mouse button. The card should move from its original column to the new column.
- Try moving cards within the same column as well.
If all these steps work as described, you have successfully built the core Kanban board UI.
Production Considerations
While this is a foundational step, thinking about production early helps.
- State Persistence: Currently, our board state is lost when the application closes. For a real application, we would persist
cardsandcolumnsto a local database (e.g., SQLite via Tauri’s Rust backend) or a local file. This will be addressed in a later chapter. - Performance: For very large boards with hundreds or thousands of cards, rendering all cards at once can become slow. Techniques like list virtualization (only rendering visible items) would be necessary. For our initial scope, this is not a concern.
- Accessibility: Drag-and-drop functionality should ideally also be accessible via keyboard navigation. This requires additional ARIA attributes and event handlers, which is beyond our current scope but important for a polished production app.
- Error Handling: The UI assumes successful operations. In a production scenario, failed updates to the store (e.g., if persistence fails) would need user feedback.
Common Issues & Solutions
- Cards not draggable/droppable:
- Issue:
draggable="true"is missing on theCardcomponent’s root element, orevent.preventDefault()is missing inhandleDragOverin theColumncomponent. - Solution: Double-check these attributes and function calls.
event.preventDefault()is critical for enabling drop targets.
- Issue:
- Svelte reactivity issues:
- Issue: Changes to stores or props don’t seem to update the UI.
- Solution: Ensure you are correctly using
$storeNameto access store values and that updates are done viastoreName.update(...)orstoreName.set(...). Svelte’s reactivity model is powerful but requires adherence to its patterns.
- Styling not applying:
- Issue: Your CSS variables or component styles aren’t visible.
- Solution: Verify
src/index.cssis being imported (checksrc/main.tsorsrc/App.sveltefor an import statement likeimport './index.css';). Also, ensure:global()is used for base styles that should affect elements outside the Svelte component’s scope.
Summary & Next Step
You’ve successfully built the interactive Kanban board for Kanbots. You can now create, edit, and move task cards, providing a solid foundation for task management. This milestone is crucial because it gives us a tangible interface to interact with.
What’s ready:
- A cross-platform desktop application powered by Tauri and Svelte.
- A functional Kanban board with multiple columns.
- Interactive cards that can be added, edited, and moved via drag-and-drop.
In the next chapter, we will integrate Git worktrees, which will serve as isolated environments for our AI agents, allowing them to work on tasks without interfering with each other or the main codebase.
This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.