Add OpenPylon Kanban board design document
Comprehensive design spec for a local-first Kanban desktop app covering architecture, data model, UI design, accessibility, import/export, and interaction patterns.
This commit is contained in:
345
docs/plans/2026-02-15-openpylon-kanban-design.md
Normal file
345
docs/plans/2026-02-15-openpylon-kanban-design.md
Normal file
@@ -0,0 +1,345 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
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 via `cardIds[]` — 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 actions
|
||||||
|
- `useAppStore` — global app state: theme, recent boards, settings, current view
|
||||||
|
|
||||||
|
### Persistence Flow
|
||||||
|
|
||||||
|
1. Board open: Tauri `fs.readTextFile()` → parse JSON → validate with Zod → hydrate Zustand store
|
||||||
|
2. On mutation: store subscribes to itself, debounces writes at 500ms
|
||||||
|
3. 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.8rem` uppercase, `letter-spacing: 0.08em`, weight 600
|
||||||
|
- Card titles: `0.875rem`, weight 500
|
||||||
|
- Card metadata: `0.75rem` monospace
|
||||||
|
|
||||||
|
### 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 column
|
||||||
|
- `Arrow Up/Down` — focus prev/next card in column
|
||||||
|
- `Enter` — open focused card detail
|
||||||
|
- `Space` — quick-toggle first unchecked checklist item
|
||||||
|
- `D` — set/edit due date on focused card
|
||||||
|
- `L` — 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 solid` accent color, `2px offset`, visible in both themes
|
||||||
|
- Modal focus trapping
|
||||||
|
- Column/card counts via `aria-label`
|
||||||
|
- `prefers-reduced-motion`: all animations collapse to instant
|
||||||
|
- `prefers-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 `.json` or `.csv` anywhere 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
|
||||||
Reference in New Issue
Block a user