docs: add 15-improvements design doc and implementation plan
Also tracks Cargo.lock and BoardCardOverlay component from prior sessions.
This commit is contained in:
296
docs/plans/2026-02-16-15-improvements-design.md
Normal file
296
docs/plans/2026-02-16-15-improvements-design.md
Normal file
@@ -0,0 +1,296 @@
|
||||
# OpenPylon: 15 Improvements Design
|
||||
|
||||
## Overview
|
||||
|
||||
15 improvements to OpenPylon organized into 5 phases (0-4), designed for incremental delivery. Each phase builds on the previous. You can ship after any phase and have a coherent improvement.
|
||||
|
||||
## Decisions
|
||||
|
||||
- **Phasing**: 4 phases (quick wins first, progressively bigger features)
|
||||
- **Data compatibility**: New fields use Zod `.default()` values. No migration code. Old boards load cleanly.
|
||||
- **Templates storage**: JSON files in `data/templates/`
|
||||
- **Backup storage**: Timestamped files in `data/backups/{boardId}/`, keep last 10
|
||||
|
||||
---
|
||||
|
||||
## Phase 0: Data Model Foundation
|
||||
|
||||
All type/schema changes that later features depend on. Done first so everything builds on a stable base.
|
||||
|
||||
### Card type additions
|
||||
|
||||
```typescript
|
||||
// Added to Card interface + cardSchema
|
||||
priority: "none" | "low" | "medium" | "high" | "urgent"; // default: "none"
|
||||
comments: Comment[]; // default: []
|
||||
|
||||
// New type + schema
|
||||
interface Comment {
|
||||
id: string;
|
||||
text: string;
|
||||
createdAt: string;
|
||||
}
|
||||
```
|
||||
|
||||
### Column type additions
|
||||
|
||||
```typescript
|
||||
// Added to Column interface + columnSchema
|
||||
collapsed: boolean; // default: false
|
||||
wipLimit: number | null; // default: null
|
||||
```
|
||||
|
||||
### Files touched
|
||||
|
||||
- `src/types/board.ts` — Add fields to Card, Column interfaces. Add Comment interface.
|
||||
- `src/lib/schemas.ts` — Add Zod fields with defaults to cardSchema, columnSchema. Add commentSchema.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Quick Wins
|
||||
|
||||
Minimal changes, high value. ~4 files each.
|
||||
|
||||
### #8 — Consume defaultColumnWidth Setting
|
||||
|
||||
In `board-store.ts`, `addColumn` reads `useAppStore.getState().settings.defaultColumnWidth` instead of hardcoding `"standard"`.
|
||||
|
||||
**Files**: `src/stores/board-store.ts` (1 line change)
|
||||
|
||||
### #4 — Due Date Visual Indicators
|
||||
|
||||
Replace binary overdue/not logic in `CardThumbnail` with 4-tier color system:
|
||||
|
||||
| Status | Condition | Color |
|
||||
|--------|-----------|-------|
|
||||
| Overdue | past + not today | `pylon-danger` (red) |
|
||||
| Approaching | due within 2 days | amber `oklch(65% 0.15 70)` |
|
||||
| Comfortable | due but >2 days | green `oklch(55% 0.12 145)` |
|
||||
| No date | null | `pylon-text-secondary` (gray) |
|
||||
|
||||
Helper function `getDueDateStatus(dueDate: string | null)` returns `{ color, label }`.
|
||||
|
||||
**Files**: `src/components/board/CardThumbnail.tsx`
|
||||
|
||||
### #9 — Card Aging Visualization
|
||||
|
||||
Compute days since `card.updatedAt`. Apply opacity:
|
||||
|
||||
| Days stale | Opacity |
|
||||
|------------|---------|
|
||||
| 0-7 | 1.0 |
|
||||
| 7-14 | 0.85 |
|
||||
| 14-30 | 0.7 |
|
||||
| 30+ | 0.55 |
|
||||
|
||||
Applied as inline `opacity` on the card `motion.button`.
|
||||
|
||||
**Files**: `src/components/board/CardThumbnail.tsx`
|
||||
|
||||
### #12 — Open Attachments
|
||||
|
||||
Add "Open" button to each attachment in `AttachmentSection`. Uses `open()` from `@tauri-apps/plugin-opener` (already registered).
|
||||
|
||||
**Files**: `src/components/card-detail/AttachmentSection.tsx`
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Card Interactions & UI Enhancements
|
||||
|
||||
5 features that transform how cards feel to use.
|
||||
|
||||
### #2 — Card Priority Levels
|
||||
|
||||
**Thumbnail indicator**: Colored dot in footer row. Color map:
|
||||
- `none`: hidden
|
||||
- `low`: blue
|
||||
- `medium`: yellow
|
||||
- `high`: orange
|
||||
- `urgent`: red with pulse animation
|
||||
|
||||
**Detail modal**: Priority picker section in left column (like LabelPicker). Row of 5 clickable chips with colors.
|
||||
|
||||
**Files**: `CardThumbnail.tsx`, `CardDetailModal.tsx` (new PriorityPicker component inline or separate), `board-store.ts` (no new action needed — `updateCard` handles it)
|
||||
|
||||
### #5 — Card Context Menu
|
||||
|
||||
Wrap `CardThumbnail` in Radix `ContextMenu`.
|
||||
|
||||
**Menu items**:
|
||||
- Move to → (submenu listing columns except current)
|
||||
- Set priority → (submenu with 5 options)
|
||||
- Duplicate (new card with same fields, `(copy)` suffix, new ID, inserted below original)
|
||||
- Separator
|
||||
- Delete (confirmation dialog)
|
||||
|
||||
**New store action**: `duplicateCard(cardId): string` — clones card, inserts after original in same column.
|
||||
|
||||
**Files**: `CardThumbnail.tsx`, `board-store.ts`
|
||||
|
||||
### #10 — WIP Limits
|
||||
|
||||
**Column header display**: Shows `3/5` when wipLimit set. Background tint:
|
||||
- Under limit: normal
|
||||
- At limit: amber tint `oklch(75% 0.08 70 / 15%)`
|
||||
- Over limit: red tint `oklch(70% 0.08 25 / 15%)`
|
||||
|
||||
**Setting UI**: New "Set WIP Limit" item in ColumnHeader dropdown menu. Preset choices: None / 3 / 5 / 7 / 10 / Custom.
|
||||
|
||||
**New store action**: `setColumnWipLimit(columnId: string, limit: number | null)`
|
||||
|
||||
**Files**: `ColumnHeader.tsx`, `KanbanColumn.tsx`, `board-store.ts`
|
||||
|
||||
### #3 — Column Collapse/Expand
|
||||
|
||||
When `collapsed`, render a 40px-wide strip instead of full column:
|
||||
- Vertical text via `writing-mode: vertical-rl; rotate: 180deg`
|
||||
- Card count badge
|
||||
- Click to expand
|
||||
|
||||
Animate width from full to 40px using existing `animate={{ width }}` on outer `motion.div` with `springs.bouncy`.
|
||||
|
||||
**New store action**: `toggleColumnCollapse(columnId: string)`
|
||||
|
||||
**Collapse button**: Added to ColumnHeader dropdown menu + a small chevron icon on the collapsed strip.
|
||||
|
||||
**Files**: `KanbanColumn.tsx`, `ColumnHeader.tsx`, `board-store.ts`
|
||||
|
||||
### #11 — Checklist Item Reordering
|
||||
|
||||
Wrap checklist `<ul>` in `ChecklistSection` with `DndContext` + `SortableContext` (vertical strategy). Each `ChecklistRow` becomes sortable.
|
||||
|
||||
**Drag handle**: `GripVertical` icon on left of each item, visible on hover.
|
||||
|
||||
**Drop indicator**: Horizontal glow line (same as card drag — vertical list so horizontal line is correct).
|
||||
|
||||
**New store action**: `reorderChecklistItems(cardId: string, fromIndex: number, toIndex: number)`
|
||||
|
||||
**Files**: `ChecklistSection.tsx`, `board-store.ts`
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Navigation & Power User Features
|
||||
|
||||
Features that make power users fall in love.
|
||||
|
||||
### #1 — Card Filtering & Quick Search
|
||||
|
||||
**Filter bar**: Slides down below TopBar. Triggered by filter icon in TopBar or `/` keyboard shortcut.
|
||||
|
||||
**Filter controls** (horizontal row):
|
||||
- Text input (debounced 200ms, title search)
|
||||
- Label multi-select dropdown (ANY match)
|
||||
- Due date dropdown: All / Overdue / Due this week / Due today / No date
|
||||
- Priority dropdown: All / Urgent / High / Medium / Low
|
||||
- Clear all button
|
||||
|
||||
**State**: Local state in `BoardView` (not persisted — filters are ephemeral).
|
||||
|
||||
**Rendering**: `KanbanColumn` receives filtered card IDs. Non-matching cards fade out. Column counts show `3 of 7` when filtering.
|
||||
|
||||
**Files**: New `FilterBar.tsx` component, `BoardView.tsx`, `KanbanColumn.tsx`, `TopBar.tsx` (filter button)
|
||||
|
||||
### #7 — Keyboard Card Navigation
|
||||
|
||||
**State**: `focusedCardId` in `BoardView` local state.
|
||||
|
||||
**Key bindings** (when no input/textarea focused):
|
||||
| Key | Action |
|
||||
|-----|--------|
|
||||
| `J` / `ArrowDown` | Focus next card in column |
|
||||
| `K` / `ArrowUp` | Focus previous card in column |
|
||||
| `H` / `ArrowLeft` | Focus same-index card in previous column |
|
||||
| `L` / `ArrowRight` | Focus same-index card in next column |
|
||||
| `Enter` | Open focused card detail |
|
||||
| `Escape` | Clear focus / close modal |
|
||||
|
||||
**Visual**: Focused card gets `ring-2 ring-pylon-accent ring-offset-2`. Column auto-scrolls via `scrollIntoView({ block: "nearest" })`.
|
||||
|
||||
**Implementation**: `useKeyboardNavigation` hook. Passes `isFocused` through `KanbanColumn` to `CardThumbnail`.
|
||||
|
||||
**Files**: New `useKeyboardNavigation.ts` hook, `BoardView.tsx`, `KanbanColumn.tsx`, `CardThumbnail.tsx`
|
||||
|
||||
### #6 — Desktop Notifications for Due Dates
|
||||
|
||||
**Plugin**: Add `tauri-plugin-notification` to `Cargo.toml` and capabilities.
|
||||
|
||||
**Trigger**: On `useAppStore.init()`, after loading boards, scan all cards:
|
||||
- Cards due today → "You have X cards due today"
|
||||
- Cards overdue → "You have X overdue cards"
|
||||
|
||||
Batched (one notification per category). Store `lastNotificationCheck` in settings to skip if checked within last hour.
|
||||
|
||||
**Files**: `src-tauri/Cargo.toml`, `src-tauri/capabilities/default.json`, `src/stores/app-store.ts`, `src/types/settings.ts` (add `lastNotificationCheck`), `src/lib/schemas.ts`
|
||||
|
||||
### #13 — Card Comments / Activity Log
|
||||
|
||||
**UI in CardDetailModal**: New section in right column below description.
|
||||
- Scrollable comment list (newest first)
|
||||
- Each: text, relative timestamp, delete button (hover)
|
||||
- Add input: textarea + "Add" button. Enter submits, Shift+Enter newline.
|
||||
|
||||
**Store actions**: `addComment(cardId, text)`, `deleteComment(cardId, commentId)`. Comments get ULID IDs and `createdAt`.
|
||||
|
||||
**Files**: New `CommentsSection.tsx`, `CardDetailModal.tsx`, `board-store.ts`
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: System Features & Infrastructure
|
||||
|
||||
Deeper features touching storage and templates.
|
||||
|
||||
### #14 — Board Templates & Saved Structures
|
||||
|
||||
**Template type**:
|
||||
```typescript
|
||||
interface BoardTemplate {
|
||||
id: string;
|
||||
name: string;
|
||||
color: string;
|
||||
columns: { title: string; width: ColumnWidth; color: string | null; wipLimit: number | null }[];
|
||||
labels: Label[];
|
||||
settings: BoardSettings;
|
||||
}
|
||||
```
|
||||
|
||||
**Saving**: Context menu item on `BoardCard` — "Save as Template". Prompts for name. Strips cards/timestamps. Writes to `data/templates/{id}.json`.
|
||||
|
||||
**Creating**: `NewBoardDialog` shows built-in templates (Blank, Kanban, Sprint) + user templates below a separator. Delete button (X) on user templates.
|
||||
|
||||
**Storage functions**: `listTemplates()`, `saveTemplate()`, `deleteTemplate()` in `storage.ts`. `board-factory.ts` gets `createBoardFromTemplate()`.
|
||||
|
||||
**Files**: `storage.ts`, `board-factory.ts`, `NewBoardDialog.tsx`, `BoardCard.tsx`, new `src/types/template.ts`
|
||||
|
||||
### #15 — Auto-Backup & Version History
|
||||
|
||||
**Storage**: `data/backups/{boardId}/` directory. Timestamped files: `{boardId}-{ISO timestamp}.json`.
|
||||
|
||||
**Save flow** in `board-store.ts`:
|
||||
1. Read current file as previous version
|
||||
2. Write new board to `{boardId}.json`
|
||||
3. Write previous version to `data/backups/{boardId}/{boardId}-{timestamp}.json`
|
||||
4. Prune backups beyond 10
|
||||
|
||||
**UI — Version History dialog**: Accessible from board settings dropdown menu ("Version History"). Shows:
|
||||
- List of backups sorted newest-first
|
||||
- Each entry: relative timestamp, card count, column count
|
||||
- "Restore" button with confirmation dialog
|
||||
- Current board auto-backed-up before restore (restore is reversible)
|
||||
|
||||
**Storage functions**: `listBackups(boardId)`, `restoreBackup(boardId, filename)`, `pruneBackups(boardId, keep)`.
|
||||
|
||||
**Files**: `storage.ts`, `board-store.ts`, new `VersionHistoryDialog.tsx`, `TopBar.tsx` (menu item)
|
||||
|
||||
---
|
||||
|
||||
## Dependency Graph
|
||||
|
||||
```
|
||||
Phase 0 (data model)
|
||||
└── Phase 1 (quick wins) — no deps on Phase 0 except #8
|
||||
└── Phase 2 (card interactions) — needs priority + collapsed + wipLimit from Phase 0
|
||||
└── Phase 3 (power user) — needs priority for filtering, context menu patterns from Phase 2
|
||||
└── Phase 4 (infrastructure) — needs wipLimit in templates from Phase 0+2
|
||||
```
|
||||
|
||||
Phase 1 can actually run in parallel with Phase 0 since its features don't touch the new fields. Phases 2-4 are strictly sequential.
|
||||
2474
docs/plans/2026-02-16-15-improvements-implementation.md
Normal file
2474
docs/plans/2026-02-16-15-improvements-implementation.md
Normal file
File diff suppressed because it is too large
Load Diff
5447
src-tauri/Cargo.lock
generated
Normal file
5447
src-tauri/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
34
src/components/boards/BoardCardOverlay.tsx
Normal file
34
src/components/boards/BoardCardOverlay.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { formatDistanceToNow } from "date-fns";
|
||||
import type { BoardMeta } from "@/types/board";
|
||||
|
||||
interface BoardCardOverlayProps {
|
||||
board: BoardMeta;
|
||||
}
|
||||
|
||||
export function BoardCardOverlay({ board }: BoardCardOverlayProps) {
|
||||
const relativeTime = formatDistanceToNow(new Date(board.updatedAt), {
|
||||
addSuffix: true,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col rounded-lg bg-pylon-surface shadow-xl ring-2 ring-pylon-accent/40 opacity-90 cursor-grabbing">
|
||||
{/* Color accent stripe */}
|
||||
<div
|
||||
className="h-1 w-full rounded-t-lg"
|
||||
style={{ backgroundColor: board.color }}
|
||||
/>
|
||||
<div className="flex flex-col gap-2 p-4">
|
||||
<h3 className="font-heading text-lg text-pylon-text">
|
||||
{board.title}
|
||||
</h3>
|
||||
<p className="font-mono text-xs text-pylon-text-secondary">
|
||||
{board.cardCount} card{board.cardCount !== 1 ? "s" : ""} ·{" "}
|
||||
{board.columnCount} column{board.columnCount !== 1 ? "s" : ""}
|
||||
</p>
|
||||
<p className="font-mono text-xs text-pylon-text-secondary">
|
||||
{relativeTime}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user