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