Comprehensive design spec for a local-first Kanban desktop app covering architecture, data model, UI design, accessibility, import/export, and interaction patterns.
13 KiB
OpenPylon — Local-First Kanban Board Design Document
Date: 2026-02-15 Status: Approved
Overview
OpenPylon is a local-first Kanban board desktop app for personal projects and task management. No account required, no cloud sync — a fast, drag-and-drop board that saves to local JSON files. Replaces Trello ($5-10/mo), Asana ($11/mo), Monday.com ($9/mo).
Tech Stack
- Runtime: Tauri (Rust backend, system webview, ~5MB bundle)
- Frontend: React + TypeScript
- State: Zustand (monolithic store per board, debounced JSON persistence)
- Styling: Tailwind CSS + shadcn/ui
- Drag & Drop: dnd-kit
- Undo/Redo: zundo (Zustand temporal middleware)
Architecture: Monolithic State Store
Single Zustand store per board, loaded entirely into memory from JSON on open. All mutations go through the store and auto-save back to disk with debounced writes (500ms). Board data is small (even 500 cards is ~1MB of JSON), so full in-memory loading is fine.
Data Model
Directory Structure
~/.openpylon/
├── settings.json # Global app settings
├── boards/
│ ├── board-<ulid>.json # One file per board
│ └── board-<ulid>.json
└── attachments/
└── board-<ulid>/ # Copied attachments (when setting enabled)
└── <ulid>-filename.png
Schema
interface Board {
id: string; // ULID
title: string;
color: string; // Accent color stripe for board list
createdAt: string; // ISO 8601
updatedAt: string;
columns: Column[];
cards: Record<string, Card>; // Flat map, referenced by columns
labels: Label[]; // Board-level label definitions
settings: BoardSettings; // Per-board settings (attachment mode, etc.)
}
interface Column {
id: string;
title: string;
cardIds: string[]; // Ordered references
width: "narrow" | "standard" | "wide"; // Collapsible widths
}
interface Card {
id: string;
title: string;
description: string; // Markdown
labels: string[]; // Label IDs
checklist: ChecklistItem[];
dueDate: string | null;
attachments: Attachment[];
createdAt: string;
updatedAt: string;
}
interface Label {
id: string;
name: string;
color: string;
}
interface ChecklistItem {
id: string;
text: string;
checked: boolean;
}
interface Attachment {
id: string;
name: string;
path: string; // Absolute (link mode) or relative (copy mode)
mode: "link" | "copy";
}
Key decisions:
- ULIDs instead of UUIDs — sortable by creation time, no collisions
- Cards stored flat (
cards: Record<string, Card>) with columns referencing viacardIds[]— drag-and-drop reordering is a simple array splice - Labels defined at board level, referenced by ID on cards
State Management & Persistence
Stores
useBoardStore— active board's full state + all mutation actionsuseAppStore— global app state: theme, recent boards, settings, current view
Persistence Flow
- Board open: Tauri
fs.readTextFile()→ parse JSON → validate with Zod → hydrate Zustand store - On mutation: store subscribes to itself, debounces writes at 500ms
- On board close / app quit: immediate flush via Tauri
window.onCloseRequested
Auto-Backup
On every successful save, rotate previous version to board-<ulid>.backup.json (one backup per board).
Undo/Redo
zundo (Zustand temporal middleware) tracks state history. Ctrl+Z / Ctrl+Shift+Z. Capped at ~50 steps.
Search
Global search reads all board JSON files from disk and searches card titles + descriptions. For personal Kanban (5-20 boards), this is instant. No index needed.
UI Design
Aesthetic Direction: Industrial Utility with Warmth
"Pylon" evokes infrastructure and strength. The app should feel like a well-made tool — a carpenter's organized workshop, not an IKEA showroom.
Color Palette (OKLCH)
Light mode:
- Background:
oklch(97% 0.005 80)— warm off-white - Surface/cards:
oklch(99% 0.003 80)— barely-there warmth - Column background:
oklch(95% 0.008 80)— subtle sand - Primary accent:
oklch(55% 0.12 160)— muted teal-green - Text primary:
oklch(25% 0.015 50)— warm near-black - Text secondary:
oklch(55% 0.01 50)— warm gray - Danger/overdue:
oklch(55% 0.18 25)— terracotta red
Dark mode:
- Background:
oklch(18% 0.01 50)— warm dark - Surface:
oklch(22% 0.01 50) - Cards:
oklch(25% 0.012 50)
Typography
- Headings: Instrument Serif — heritage serif with personality
- Body/cards: Satoshi — clean geometric sans, readable at small sizes
- Metadata (labels, dates, counts): Geist Mono — reinforces "tool" identity
Scale:
- Board title:
clamp(1.25rem, 2vw, 1.5rem), bold - Column headers:
0.8remuppercase,letter-spacing: 0.08em, weight 600 - Card titles:
0.875rem, weight 500 - Card metadata:
0.75remmonospace
App Shell Layout
┌──────────────────────────────────────────────────────┐
│ ← Boards Sprint Planning ⌘K ⚙ │
├──────────────────────────────────────────────────────┤
│ │
│ TO DO IN PROGRESS DONE │
│ ───── 4 ─────────── 2 ──── 3 │
│ │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
│ │ Card title │ │ Card title │ │ Card title │ │
│ │ 🟢🔵 Feb28 ▮▮▯│ │ 🟢 ▮▮▮▮│ │ ▮▮▯ │ │
│ └────────────┘ └────────────┘ └────────────┘ │
│ │
│ + Add card + Add card + Add card │
│ │
└──────────────────────────────────────────────────────┘
Key layout decisions:
- No vertical column dividers — whitespace gaps (24-32px) instead
- Column headers: uppercase, tracked-out, small — like section dividers
- Card count as quiet number beside underline, not a badge
- Command palette (
Ctrl+K) replaces search icon - Theme toggle lives in settings, not top bar
- Board title is click-to-edit inline, no
[edit]button
Card Design
- Label dots: 8px colored circles in a row, hover for tooltip with name
- Due date: Monospace, right-aligned, no icon. Overdue turns terracotta with subtle tint.
- Checklist: Tiny progress bar (filled/unfilled blocks), not "3/4" text
- No card borders. Subtle shadow (
0 1px 3px oklch(0% 0 0 / 0.06)) for separation. - Hover:
translateY(-1px)lift with faint shadow deepening, spring physics, 150ms - Drag ghost: 5-degree rotation,
scale(1.03),opacity(0.9), elevated shadow
Column Widths
Columns support three widths: narrow (titles only), standard, wide (active focus). Double-click header to cycle. Adds spatial meaning.
Card Detail Modal (Two-Panel)
┌──────────────────────────────────────────────────────┐
│ Fix auth token refresh ✕ │
│ │
│ ┌─────────────────────────┐ ┌────────────────────┐ │
│ │ │ │ LABELS │ │
│ │ Markdown description │ │ 🟢 Bug 🔵 Backend │ │
│ │ with live preview │ │ │ │
│ │ │ │ DUE DATE │ │
│ │ │ │ Feb 28, 2026 │ │
│ │ │ │ │ │
│ │ │ │ CHECKLIST 3/4 │ │
│ │ │ │ ✓ Research APIs │ │
│ │ │ │ ✓ Write tests │ │
│ │ │ │ ✓ Implement │ │
│ │ │ │ ○ Code review │ │
│ │ │ │ │ │
│ │ │ │ ATTACHMENTS │ │
│ │ │ │ spec.pdf │ │
│ └─────────────────────────┘ └────────────────────┘ │
└──────────────────────────────────────────────────────┘
- Left panel (60%): Title (inline edit) + markdown description (edit/preview toggle)
- Right sidebar (40%): Labels, due date, checklist, attachments. Each collapsible.
- No save button — auto-persist with subtle "Saved" indicator
- Card-to-modal morph animation via Framer Motion
layoutId— modal grows from card position
Command Palette (Ctrl+K)
Using shadcn's cmdk component:
- Search all cards across all boards by title/description
- Switch between boards
- Create new cards/boards
- Toggle theme
- Open settings
- Navigate to specific column
- Filter current board by label/date
Board List (Home Screen)
Grid of board cards with:
- Color accent stripe at top (user-chosen per board)
- Title, card count, column count
- Relative time ("2 min ago", "Yesterday")
- Right-click context menu: Duplicate, Export, Delete, Change color
- Empty state: "Create your first board" + single button
Keyboard Shortcuts
Global
| Action | Shortcut |
|---|---|
| Command palette | Ctrl+K |
| New card in focused column | N |
| New board | Ctrl+N |
| Undo | Ctrl+Z |
| Redo | Ctrl+Shift+Z |
| Settings | Ctrl+, |
| Close modal / cancel | Escape |
| Save & close card detail | Ctrl+Enter |
Board Navigation
Arrow Left/Right— focus prev/next columnArrow Up/Down— focus prev/next card in columnEnter— open focused card detailSpace— quick-toggle first unchecked checklist itemD— set/edit due date on focused cardL— open label picker on focused card
Drag-and-Drop Keyboard
dnd-kit keyboard sensor: Space to pick up, arrows to move, Space to drop, Escape to cancel. Movements announced via aria-live region.
Accessibility
- All interactive elements reachable via Tab
- Focus indicators:
2px solidaccent color,2px offset, visible in both themes - Modal focus trapping
- Column/card counts via
aria-label prefers-reduced-motion: all animations collapse to instantprefers-contrast: increased shadow intensity, subtle borders restored- Minimum touch target: 44x44px on all buttons
Import/Export
Export
- JSON: The board file itself is the export. Save As dialog.
- CSV: Flattened — one row per card with all fields.
- ZIP: For boards with copy-mode attachments — board JSON + attachments folder.
Import
- OpenPylon JSON: Drop file onto board list or use File > Import. Schema validation + preview before importing.
- CSV: Import wizard — map columns, preview rows, choose target board.
- Trello JSON: Dedicated adapter mapping Trello schema to OpenPylon.
- Drag-and-drop import: Dropping
.jsonor.csvanywhere triggers import flow.
Error Handling
- Corrupted board file: Recovery dialog — inspect in file explorer or restore from
.backup.json - Data directory inaccessible: Dialog to choose new directory on startup
- Disk full: Inline toast, changes preserved in memory, retry every 30s
- File locked: Warning dialog
- Schema migration: On load, validate with Zod, add missing fields with defaults, preserve unknown fields
- Drag edge cases: Empty column droppable, drop outside cancels with spring return
Micro-Interactions
| Interaction | Animation | Duration |
|---|---|---|
| Card appears (new) | Fade in + slide down | 200ms, spring |
| Card drag start | Lift + rotate + shadow | 150ms |
| Card drop | Settle with slight bounce | 250ms, spring |
| Column add | Slide in from right | 300ms |
| Card detail open | Morph from card position | 250ms |
| Card detail close | Reverse morph to card | 200ms |
| Checklist check | Strikethrough sweep + fill | 200ms |
| Board switch | Crossfade | 300ms |
All animations respect prefers-reduced-motion.
Empty States
- No boards: "Create your first board" + button + minimal illustration
- Empty column: Dashed border area + "Drag cards here or click + to add"
- No search results: "No matches" + suggestion to broaden
- No labels: "Create your first label" + color swatches