This document provides guidance for AI agents working on the Board codebase.
Board is a privacy-focused, local-only Kanban board application. Key principles:
- No account required - All data stays in the user's browser (localStorage)
- Privacy by default - No tracking, no external services
- Self-hostable - Docker support with SQLite persistence for teams
- PWA support - Installable, works offline
Live Demo: https://floschu.github.io/board/
- React 19 with TypeScript
- Vite 7 - Build tool
- Tailwind CSS 4 - Styling
- Zustand 5 - State management
- @dnd-kit - Drag-and-drop
- @headlessui/react - Accessible UI components
- Motion (Framer Motion) - Animations
- Express.js - API server
- better-sqlite3 - SQLite database
- Vitest - Test runner
- @testing-library/react - Component testing
board/
βββ .github/workflows/ # CI/CD (build, test, deploy)
βββ docs/ # Documentation assets (screenshots)
βββ public/ # Static assets (icons, favicon)
βββ scripts/ # Build utilities (favicon, icons, screenshots)
βββ server/ # Express backend for self-hosting
β βββ db.js # SQLite database operations
β βββ index.js # Express server entry point
β βββ validation.js # Server-side validation
βββ src/
β βββ components/ # React components
β βββ hooks/ # Custom React hooks
β βββ storage/ # Storage abstractions (localStorage, API)
β βββ store/ # Zustand stores
β βββ test/ # Test setup
β βββ types/ # TypeScript type definitions
β βββ utils/ # Utility functions
β βββ App.tsx # Main application component
β βββ main.tsx # React entry point
β βββ index.css # Global styles and Tailwind config
βββ Dockerfile # Multi-stage Docker build
βββ docker-compose.yml # Docker Compose configuration
βββ index.html # HTML entry point
# Development
npm run dev # Start dev server (port 5173)
# Building
npm run build # TypeScript check + Vite build
npm run preview # Preview production build
# Testing
npm run test # Run Vitest in watch mode
npm run test:run # Run tests once (CI)
# Linting
npm run lint # Run ESLint
# Server (self-hosted mode)
npm run server # Start Express server only
npm run start # Build + start serverThree separate Zustand stores handle different concerns:
| Store | File | Purpose |
|---|---|---|
useBoardStore |
src/store/index.ts |
Main data (projects, lists, cards) |
useThemeStore |
src/store/theme.ts |
Theme preferences (persisted) |
useCardFocusStore |
src/store/cardFocus.ts |
Keyboard navigation focus |
The main store auto-persists to localStorage via Zustand's subscribe middleware.
Hierarchical structure with position-based ordering:
Projects
βββ Lists
βββ Cards
Each entity has:
id- UUIDposition- Integer for orderingcreatedAt/updatedAt- Timestamps
Two storage backends with the same interface:
src/storage/localStorage.ts- Browser-only mode (default)src/storage/api.ts- Self-hosted mode with Express backend
Validation logic is shared between client (src/utils/validation.ts) and server (server/validation.js).
| File | Purpose |
|---|---|
src/main.tsx |
React application entry |
src/App.tsx |
Root component, initialization |
server/index.js |
Express server entry |
src/store/index.ts |
Main Zustand store |
- Strict mode enabled with
noUnusedLocals,noUnusedParameters - Use
import type { ... }for type-only imports - Define interfaces for object types, type aliases for unions
- Explicit return types on exported functions
// Good
import type { Project } from '../types';
export function getProject(id: string): Project | undefined {
// ...
}- Functional components with hooks only
- Custom hooks prefixed with
use(e.g.,useKeyboardShortcuts) - Props interfaces defined per component
interface CardProps {
card: Card;
listId: string;
onEdit: () => void;
}
export function Card({ card, listId, onEdit }: CardProps) {
// ...
}- Tailwind CSS utility classes - No custom CSS unless necessary
- Dark theme only - Catppuccin Mocha color palette
- CSS custom properties for theming defined in
index.css - Glow color variables:
--glow-color,--glow-rgb
| Type | Convention | Example |
|---|---|---|
| Components | PascalCase | CardModal.tsx |
| Functions/variables | camelCase | getActiveProject |
| Hooks | camelCase with use prefix |
useKeyboardShortcuts |
| Constants | SCREAMING_SNAKE_CASE | MAX_TITLE_LENGTH |
| CSS classes | kebab-case | card-container |
- JSDoc for utility functions with
@paramand@returns SECURITY:prefix for security-critical code sections- Inline comments for complex logic only
/**
* Validates a URL to prevent XSS attacks.
* SECURITY: Blocks javascript:, data:, and other dangerous protocols.
* @param url - The URL to validate
* @returns true if the URL is safe
*/
export function isValidUrl(url: string): boolean {
// ...
}- Vitest with jsdom environment
- @testing-library/react for component tests
- Setup file:
src/test/setup.ts - Tests are co-located with source files (e.g.,
Card.test.tsx)
src/
βββ components/
β βββ Card.test.tsx
βββ store/
β βββ index.test.ts
β βββ cardFocus.test.ts
β βββ dragAndDrop.test.ts
βββ hooks/
β βββ useKeyboardShortcuts.test.ts
βββ utils/
β βββ date.test.ts
β βββ url.test.ts
β βββ importExport.test.ts
βββ storage/
βββ localStorage.test.ts
Reset store state between tests:
beforeEach(() => {
useBoardStore.setState({
projects: [],
lists: [],
cards: [],
activeProjectId: null,
});
});Mock localStorage:
vi.mock('../storage/localStorage', () => ({
saveToLocalStorage: vi.fn(),
loadFromLocalStorage: vi.fn(),
}));Wrap drag-and-drop components:
import { DndContext } from '@dnd-kit/core';
render(
<DndContext>
<Card {...props} />
</DndContext>
);Use fake timers for date-dependent tests:
beforeEach(() => {
vi.useFakeTimers();
vi.setSystemTime(new Date('2024-01-15'));
});
afterEach(() => {
vi.useRealTimers();
});Always include tests for XSS prevention when handling URLs:
it('should reject javascript: URLs', () => {
expect(isValidUrl('javascript:alert(1)')).toBe(false);
});
it('should reject data: URLs', () => {
expect(isValidUrl('data:text/html,<script>alert(1)</script>')).toBe(false);
});All user-provided URLs must be validated to prevent XSS:
- Use
isValidUrl()fromsrc/utils/url.ts - Blocks:
javascript:,data:,vbscript:, and other dangerous protocols - Only allows:
http:,https:, and relative URLs
- Max length constraints enforced in
src/utils/validation.ts - Same validation runs on both client and server
- Referential integrity checked on data import
src/utils/url.ts- URL validation and sanitizationsrc/utils/validation.ts- Input validation (client)server/validation.js- Input validation (server, mirrors client)
- Create component file in
src/components/ - Use TypeScript interface for props
- Use Tailwind CSS for styling
- Add test file alongside component
- Add action to store in
src/store/index.ts - Include type in the store interface
- Update persistence if needed
- Add tests in
src/store/index.test.ts
- Add function to appropriate file in
src/utils/ - Include JSDoc comment with
@paramand@returns - Add
SECURITY:comment if handling user input - Add tests covering edge cases and security scenarios
- Add route in
server/index.js - Add validation in
server/validation.js(mirror client validation) - Add database operations in
server/db.js - Test with both valid and invalid inputs
- Build and test run on every push/PR (
.github/workflows/build.yml) - Docker image published to
ghcr.ioon release tags - GitHub Pages deployment on release (
.github/workflows/release.yml)
| Package | Purpose |
|---|---|
@dnd-kit/core |
Drag-and-drop core |
@dnd-kit/sortable |
Sortable lists |
@headlessui/react |
Accessible dialogs, menus |
@heroicons/react |
Icon set |
zustand |
State management |
motion |
Animations |
react-markdown |
Markdown rendering |
uuid |
ID generation |
ogl |
WebGL effects (Aurora) |