From 1592264514071a39726dd01b253199337385be2c Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 15 Feb 2026 20:18:38 +0200 Subject: [PATCH] docs: add visual glow-up implementation plan with 17 tasks Detailed step-by-step plan covering settings infrastructure, UI zoom, accent colors, density, board/column/card colors, toasts, keyboard help, backgrounds, onboarding, and polish. --- ...026-02-15-visual-glow-up-implementation.md | 1766 +++++++++++++++++ 1 file changed, 1766 insertions(+) create mode 100644 docs/plans/2026-02-15-visual-glow-up-implementation.md diff --git a/docs/plans/2026-02-15-visual-glow-up-implementation.md b/docs/plans/2026-02-15-visual-glow-up-implementation.md new file mode 100644 index 0000000..f7c662d --- /dev/null +++ b/docs/plans/2026-02-15-visual-glow-up-implementation.md @@ -0,0 +1,1766 @@ +# Visual Glow-Up Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Transform OpenPylon from functional-but-bare to visually polished — settings infrastructure, UI zoom, accent colors, density, board/column/card colors, toasts, keyboard help, backgrounds, onboarding, and polish. + +**Architecture:** Settings-first approach. Expand the AppSettings type with new appearance fields, wire them through the Zustand app-store to CSS custom properties on ``, then build the tabbed settings UI. Visual features (board colors, column colors, card covers, etc.) layer on top. Toast system is an independent Zustand store. No new npm dependencies needed — everything uses existing Tailwind, Lucide, Framer Motion, and Radix primitives. + +**Tech Stack:** React 19, TypeScript, Zustand 5, Tailwind CSS 4, Radix UI, Lucide React, Framer Motion, Tauri v2, Zod 4 + +--- + +### Task 1: Expand Settings Type & Schema + +**Files:** +- Modify: `src/types/settings.ts` +- Modify: `src/lib/schemas.ts` + +**Step 1: Update AppSettings interface** + +In `src/types/settings.ts`, replace the entire file: + +```typescript +import type { ColumnWidth } from "./board"; + +export interface AppSettings { + theme: "light" | "dark" | "system"; + dataDirectory: string | null; + recentBoardIds: string[]; + + // Appearance + accentColor: string; + uiZoom: number; + density: "compact" | "comfortable" | "spacious"; + + // Board defaults + defaultColumnWidth: ColumnWidth; +} +``` + +**Step 2: Update the Zod schema** + +In `src/lib/schemas.ts`, replace the `appSettingsSchema`: + +```typescript +export const appSettingsSchema = z.object({ + theme: z.enum(["light", "dark", "system"]).default("system"), + dataDirectory: z.string().nullable().default(null), + recentBoardIds: z.array(z.string()).default([]), + accentColor: z.string().default("160"), + uiZoom: z.number().min(0.75).max(1.5).default(1), + density: z.enum(["compact", "comfortable", "spacious"]).default("comfortable"), + defaultColumnWidth: z.enum(["narrow", "standard", "wide"]).default("standard"), +}); +``` + +**Step 3: Verify types compile** + +Run: `npx tsc --noEmit` +Expected: Should fail — `app-store.ts` default value doesn't include the new fields yet. That's expected, we'll fix it in Task 2. + +**Step 4: Commit** + +```bash +git add src/types/settings.ts src/lib/schemas.ts +git commit -m "feat: expand AppSettings with appearance and board default fields" +``` + +--- + +### Task 2: Wire App Store with Appearance Application + +**Files:** +- Modify: `src/stores/app-store.ts` + +**Step 1: Add applyAppearance and new actions** + +Replace `src/stores/app-store.ts` entirely: + +```typescript +import { create } from "zustand"; +import type { AppSettings } from "@/types/settings"; +import type { BoardMeta, ColumnWidth } from "@/types/board"; +import { loadSettings, saveSettings, listBoards, ensureDataDirs } from "@/lib/storage"; + +export type View = { type: "board-list" } | { type: "board"; boardId: string }; + +interface AppState { + settings: AppSettings; + boards: BoardMeta[]; + view: View; + initialized: boolean; + + init: () => Promise; + setTheme: (theme: AppSettings["theme"]) => void; + setAccentColor: (hue: string) => void; + setUiZoom: (zoom: number) => void; + setDensity: (density: AppSettings["density"]) => void; + setDefaultColumnWidth: (width: ColumnWidth) => void; + setView: (view: View) => void; + refreshBoards: () => Promise; + addRecentBoard: (boardId: string) => void; +} + +function applyTheme(theme: AppSettings["theme"]): void { + const root = document.documentElement; + if (theme === "system") { + const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches; + root.classList.toggle("dark", prefersDark); + } else { + root.classList.toggle("dark", theme === "dark"); + } +} + +function applyAppearance(settings: AppSettings): void { + const root = document.documentElement; + + // Zoom: set root font-size (Tailwind rem units scale automatically) + root.style.fontSize = `${settings.uiZoom * 16}px`; + + // Accent color: regenerate --pylon-accent from OKLCH hue + const hue = settings.accentColor; + const isDark = root.classList.contains("dark"); + const lightness = isDark ? "60%" : "55%"; + root.style.setProperty("--pylon-accent", `oklch(${lightness} 0.12 ${hue})`); + + // Density factor + const densityMap = { compact: "0.75", comfortable: "1", spacious: "1.25" }; + root.style.setProperty("--density-factor", densityMap[settings.density]); +} + +function updateAndSave( + get: () => AppState, + set: (partial: Partial) => void, + patch: Partial +): void { + const settings = { ...get().settings, ...patch }; + set({ settings }); + saveSettings(settings); +} + +export const useAppStore = create((set, get) => ({ + settings: { + theme: "system", + dataDirectory: null, + recentBoardIds: [], + accentColor: "160", + uiZoom: 1, + density: "comfortable", + defaultColumnWidth: "standard", + }, + boards: [], + view: { type: "board-list" }, + initialized: false, + + init: async () => { + await ensureDataDirs(); + const settings = await loadSettings(); + const boards = await listBoards(); + set({ settings, boards, initialized: true }); + applyTheme(settings.theme); + applyAppearance(settings); + }, + + setTheme: (theme) => { + updateAndSave(get, set, { theme }); + applyTheme(theme); + // Re-apply appearance since accent lightness depends on dark/light + applyAppearance({ ...get().settings, theme }); + }, + + setAccentColor: (accentColor) => { + updateAndSave(get, set, { accentColor }); + applyAppearance(get().settings); + }, + + setUiZoom: (uiZoom) => { + updateAndSave(get, set, { uiZoom }); + applyAppearance(get().settings); + }, + + setDensity: (density) => { + updateAndSave(get, set, { density }); + applyAppearance(get().settings); + }, + + setDefaultColumnWidth: (defaultColumnWidth) => { + updateAndSave(get, set, { defaultColumnWidth }); + }, + + setView: (view) => set({ view }), + + refreshBoards: async () => { + const boards = await listBoards(); + set({ boards }); + }, + + addRecentBoard: (boardId) => { + const settings = get().settings; + const recent = [ + boardId, + ...settings.recentBoardIds.filter((id) => id !== boardId), + ].slice(0, 10); + updateAndSave(get, set, { recentBoardIds: recent }); + }, +})); +``` + +**Step 2: Verify types compile** + +Run: `npx tsc --noEmit` +Expected: PASS (or only unrelated warnings) + +**Step 3: Commit** + +```bash +git add src/stores/app-store.ts +git commit -m "feat: wire app store with appearance actions and CSS variable application" +``` + +--- + +### Task 3: Add Density CSS Variable Support + +**Files:** +- Modify: `src/index.css` + +**Step 1: Add density variable defaults to `:root`** + +After the existing `--radius: 0.625rem;` line in `:root`, add: + +```css +--density-factor: 1; +``` + +**Step 2: Verify the app still loads** + +Run: `npm run dev` — open http://localhost:1420, confirm no visual breakage. + +**Step 3: Commit** + +```bash +git add src/index.css +git commit -m "feat: add density CSS variable with default value" +``` + +--- + +### Task 4: Rewrite Settings Dialog as Tabbed Panel + +**Files:** +- Modify: `src/components/settings/SettingsDialog.tsx` + +**Step 1: Rewrite SettingsDialog with four tabs** + +Replace the entire file with a tabbed settings panel. Tabs: Appearance, Boards, Keyboard Shortcuts, About. + +```typescript +import { useState } from "react"; +import { + Sun, Moon, Monitor, RotateCcw, + Columns3, LayoutList, Maximize, +} from "lucide-react"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, +} from "@/components/ui/dialog"; +import { Separator } from "@/components/ui/separator"; +import { Button } from "@/components/ui/button"; +import { useAppStore } from "@/stores/app-store"; +import type { AppSettings } from "@/types/settings"; +import type { ColumnWidth } from "@/types/board"; + +interface SettingsDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; +} + +type Tab = "appearance" | "boards" | "shortcuts" | "about"; + +const THEME_OPTIONS: { + value: AppSettings["theme"]; + label: string; + icon: typeof Sun; +}[] = [ + { value: "light", label: "Light", icon: Sun }, + { value: "dark", label: "Dark", icon: Moon }, + { value: "system", label: "System", icon: Monitor }, +]; + +const ACCENT_PRESETS: { hue: string; label: string }[] = [ + { hue: "160", label: "Teal" }, + { hue: "240", label: "Blue" }, + { hue: "300", label: "Purple" }, + { hue: "350", label: "Pink" }, + { hue: "25", label: "Red" }, + { hue: "55", label: "Orange" }, + { hue: "85", label: "Yellow" }, + { hue: "130", label: "Lime" }, + { hue: "200", label: "Cyan" }, + { hue: "0", label: "Slate" }, +]; + +const DENSITY_OPTIONS: { + value: AppSettings["density"]; + label: string; +}[] = [ + { value: "compact", label: "Compact" }, + { value: "comfortable", label: "Comfortable" }, + { value: "spacious", label: "Spacious" }, +]; + +const WIDTH_OPTIONS: { value: ColumnWidth; label: string }[] = [ + { value: "narrow", label: "Narrow" }, + { value: "standard", label: "Standard" }, + { value: "wide", label: "Wide" }, +]; + +const SHORTCUTS: { key: string; description: string; category: string }[] = [ + { key: "Ctrl+K", description: "Open command palette", category: "Navigation" }, + { key: "Ctrl+Z", description: "Undo", category: "Board" }, + { key: "Ctrl+Shift+Z", description: "Redo", category: "Board" }, + { key: "?", description: "Keyboard shortcuts", category: "Navigation" }, + { key: "Escape", description: "Close modal / cancel", category: "Navigation" }, +]; + +const TABS: { value: Tab; label: string }[] = [ + { value: "appearance", label: "Appearance" }, + { value: "boards", label: "Boards" }, + { value: "shortcuts", label: "Shortcuts" }, + { value: "about", label: "About" }, +]; + +function SectionLabel({ children }: { children: React.ReactNode }) { + return ( + + ); +} + +export function SettingsDialog({ open, onOpenChange }: SettingsDialogProps) { + const [tab, setTab] = useState("appearance"); + const settings = useAppStore((s) => s.settings); + const setTheme = useAppStore((s) => s.setTheme); + const setAccentColor = useAppStore((s) => s.setAccentColor); + const setUiZoom = useAppStore((s) => s.setUiZoom); + const setDensity = useAppStore((s) => s.setDensity); + const setDefaultColumnWidth = useAppStore((s) => s.setDefaultColumnWidth); + + return ( + + + + + Settings + + + Configure your OpenPylon preferences. + + + + {/* Tab bar */} +
+ {TABS.map((t) => ( + + ))} +
+ + {/* Tab content */} +
+ {tab === "appearance" && ( + <> + {/* Theme */} +
+ Theme +
+ {THEME_OPTIONS.map(({ value, label, icon: Icon }) => ( + + ))} +
+
+ + + + {/* UI Zoom */} +
+
+ UI Zoom +
+ + {Math.round(settings.uiZoom * 100)}% + + {settings.uiZoom !== 1 && ( + + )} +
+
+ setUiZoom(parseFloat(e.target.value))} + className="w-full accent-pylon-accent" + /> +
+ 75% + 100% + 150% +
+
+ + + + {/* Accent Color */} +
+ Accent Color +
+ {ACCENT_PRESETS.map(({ hue, label }) => { + const isAchromatic = hue === "0"; + const bg = isAchromatic + ? "oklch(50% 0 0)" + : `oklch(55% 0.12 ${hue})`; + return ( +
+
+ + + + {/* Density */} +
+ Density +
+ {DENSITY_OPTIONS.map(({ value, label }) => ( + + ))} +
+
+ + )} + + {tab === "boards" && ( +
+ Default Column Width +
+ {WIDTH_OPTIONS.map(({ value, label }) => ( + + ))} +
+
+ )} + + {tab === "shortcuts" && ( +
+ {SHORTCUTS.map(({ key, description }) => ( +
+ {description} + + {key} + +
+ ))} +
+ )} + + {tab === "about" && ( +
+

OpenPylon

+

+ v0.1.0 · Local-first Kanban board +

+

+ Built with Tauri, React, and TypeScript. +

+
+ )} +
+
+
+ ); +} +``` + +**Step 2: Verify the app loads and settings dialog works** + +Run: `npm run dev`, open Settings, verify all four tabs render and settings persist. + +**Step 3: Commit** + +```bash +git add src/components/settings/SettingsDialog.tsx +git commit -m "feat: rewrite settings dialog with tabbed panel — appearance, boards, shortcuts, about" +``` + +--- + +### Task 5: Apply Density to Cards and Columns + +**Files:** +- Modify: `src/components/board/KanbanColumn.tsx` +- Modify: `src/components/board/CardThumbnail.tsx` +- Modify: `src/components/board/BoardView.tsx` + +**Step 1: Update KanbanColumn to use density-responsive padding** + +In `KanbanColumn.tsx`, change the card list `
    ` padding from `p-2` to use `calc()`: + +Replace: +``` +className="flex min-h-[40px] list-none flex-col gap-2 p-2" +``` +With: +``` +className="flex min-h-[40px] list-none flex-col p-2" style={{ gap: `calc(0.5rem * var(--density-factor))`, padding: `calc(0.5rem * var(--density-factor))` }} +``` + +**Step 2: Update CardThumbnail to use density-responsive padding** + +In `CardThumbnail.tsx`, change the card button padding from `p-3` to inline style: + +Replace the `className` on the ``: +``` +className="w-full rounded-lg bg-pylon-surface shadow-sm text-left transition-all duration-200 hover:-translate-y-px hover:shadow-md" +``` +And add a style prop: +``` +style={{ ...style, padding: `calc(0.75rem * var(--density-factor))` }} +``` + +(Merge the existing DnD `style` with the padding.) + +**Step 3: Update BoardView gap** + +In `BoardView.tsx`, change the board container `gap-6 p-6` to use density: + +Replace: +``` +className="flex h-full gap-6 overflow-x-auto p-6" +``` +With: +``` +className="flex h-full overflow-x-auto" style={{ gap: `calc(1.5rem * var(--density-factor))`, padding: `calc(1.5rem * var(--density-factor))` }} +``` + +**Step 4: Verify all three density modes look correct** + +Open Settings > Appearance > toggle between Compact/Comfortable/Spacious. Verify cards and columns compress and expand. + +**Step 5: Commit** + +```bash +git add src/components/board/KanbanColumn.tsx src/components/board/CardThumbnail.tsx src/components/board/BoardView.tsx +git commit -m "feat: apply density factor to card, column, and board spacing" +``` + +--- + +### Task 6: Board Color in UI (TopBar + Column Headers) + +**Files:** +- Modify: `src/components/layout/TopBar.tsx` +- Modify: `src/components/board/ColumnHeader.tsx` + +**Step 1: Add board color accent to TopBar** + +In `TopBar.tsx`, modify the `
    ` to show the board color as a bottom border when viewing a board: + +Replace the header className: +``` +className="flex h-12 shrink-0 items-center gap-2 border-b border-border bg-pylon-surface px-3" +``` +With: +``` +className="flex h-12 shrink-0 items-center gap-2 bg-pylon-surface px-3" +style={{ borderBottom: isBoardView && board ? `2px solid ${board.color}` : '1px solid var(--border)' }} +``` + +Also add a color dot next to the board title. In the center section, before the board title text/button, add: + +```tsx + +``` + +**Step 2: Add board color to column headers** + +In `ColumnHeader.tsx`, add a `boardColor` prop: + +Update the interface: +```typescript +interface ColumnHeaderProps { + column: Column; + cardCount: number; + boardColor?: string; +} +``` + +Add a 3px top border to the column header wrapper: + +Replace the outer div className: +``` +className="flex items-center justify-between border-b border-border px-3 pb-2 pt-3" +``` +With: +``` +className="flex items-center justify-between border-b border-border px-3 pb-2 pt-3" +style={{ borderTop: boardColor ? `3px solid ${boardColor}30` : undefined }} +``` + +(The `30` suffix is hex 30% opacity appended to the hex color.) + +**Step 3: Pass boardColor through KanbanColumn** + +In `KanbanColumn.tsx`, pass the board color to ColumnHeader: + +```tsx + +``` + +**Step 4: Verify board color shows in TopBar border and column headers** + +Open a board — verify the bottom border of the TopBar matches the board color and columns have a faint colored top line. + +**Step 5: Commit** + +```bash +git add src/components/layout/TopBar.tsx src/components/board/ColumnHeader.tsx src/components/board/KanbanColumn.tsx +git commit -m "feat: apply board color to TopBar border and column header accents" +``` + +--- + +### Task 7: Expand Board Types (Column Color, Card Cover, Board Background) + +**Files:** +- Modify: `src/types/board.ts` +- Modify: `src/lib/schemas.ts` +- Modify: `src/lib/board-factory.ts` +- Modify: `src/stores/board-store.ts` + +**Step 1: Update Column, Card, and BoardSettings types** + +In `src/types/board.ts`: + +Add `color` to Column: +```typescript +export interface Column { + id: string; + title: string; + cardIds: string[]; + width: ColumnWidth; + color: string | null; +} +``` + +Add `coverColor` to Card: +```typescript +export interface Card { + id: string; + title: string; + description: string; + labels: string[]; + checklist: ChecklistItem[]; + dueDate: string | null; + attachments: Attachment[]; + coverColor: string | null; + createdAt: string; + updatedAt: string; +} +``` + +Add `background` to BoardSettings: +```typescript +export interface BoardSettings { + attachmentMode: "link" | "copy"; + background: "none" | "dots" | "grid" | "gradient"; +} +``` + +**Step 2: Update Zod schemas** + +In `src/lib/schemas.ts`: + +Update `columnSchema`: +```typescript +export const columnSchema = z.object({ + id: z.string(), + title: z.string(), + cardIds: z.array(z.string()).default([]), + width: z.enum(["narrow", "standard", "wide"]).default("standard"), + color: z.string().nullable().default(null), +}); +``` + +Update `cardSchema` — add `coverColor` after `attachments`: +```typescript +coverColor: z.string().nullable().default(null), +``` + +Update `boardSettingsSchema`: +```typescript +export const boardSettingsSchema = z.object({ + attachmentMode: z.enum(["link", "copy"]).default("link"), + background: z.enum(["none", "dots", "grid", "gradient"]).default("none"), +}); +``` + +**Step 3: Update board-factory defaults** + +In `src/lib/board-factory.ts`, update the `col` helper to include `color: null`: + +```typescript +const col = (t: string, w: ColumnWidth = "standard") => ({ + id: ulid(), + title: t, + cardIds: [] as string[], + width: w, + color: null as string | null, +}); +``` + +And update the board's default settings: +```typescript +settings: { attachmentMode: "link", background: "none" as const }, +``` + +**Step 4: Update board-store addCard and addColumn** + +In `src/stores/board-store.ts`: + +In `addColumn`, add `color: null`: +```typescript +{ + id: ulid(), + title, + cardIds: [], + width: "standard" as ColumnWidth, + color: null, +} +``` + +In `addCard`, add `coverColor: null` to the card object: +```typescript +const card: Card = { + id: cardId, + title, + description: "", + labels: [], + checklist: [], + dueDate: null, + attachments: [], + coverColor: null, + createdAt: now(), + updatedAt: now(), +}; +``` + +Add a `setColumnColor` action to the store interface and implementation: + +Interface addition: +```typescript +setColumnColor: (columnId: string, color: string | null) => void; +``` + +Implementation: +```typescript +setColumnColor: (columnId, color) => { + mutate(get, set, (b) => ({ + ...b, + updatedAt: now(), + columns: b.columns.map((c) => + c.id === columnId ? { ...c, color } : c + ), + })); +}, +``` + +**Step 5: Verify types compile** + +Run: `npx tsc --noEmit` +Expected: PASS + +**Step 6: Commit** + +```bash +git add src/types/board.ts src/lib/schemas.ts src/lib/board-factory.ts src/stores/board-store.ts +git commit -m "feat: add column color, card coverColor, and board background to data model" +``` + +--- + +### Task 8: Column Color UI + +**Files:** +- Modify: `src/components/board/ColumnHeader.tsx` +- Modify: `src/components/board/KanbanColumn.tsx` + +**Step 1: Add Color submenu to ColumnHeader dropdown** + +In `ColumnHeader.tsx`, add `setColumnColor` to store selectors: + +```typescript +const setColumnColor = useBoardStore((s) => s.setColumnColor); +``` + +Add the color swatches data (same as settings accent presets): + +```typescript +const COLOR_PRESETS = [ + { hue: "160", label: "Teal" }, + { hue: "240", label: "Blue" }, + { hue: "300", label: "Purple" }, + { hue: "350", label: "Pink" }, + { hue: "25", label: "Red" }, + { hue: "55", label: "Orange" }, + { hue: "85", label: "Yellow" }, + { hue: "130", label: "Lime" }, + { hue: "200", label: "Cyan" }, + { hue: "0", label: "Slate" }, +]; +``` + +Add a Color submenu after the Width submenu in the dropdown: + +```tsx + + Color + + setColumnColor(column.id, null)}> + None + {column.color == null && ( + * + )} + + +
    + {COLOR_PRESETS.map(({ hue, label }) => ( +
    +
    +
    +``` + +**Step 2: Use column color for top border** + +In the ColumnHeader, update the border-top style to prefer column color over board color: + +```typescript +const effectiveColor = column.color + ? `oklch(55% 0.12 ${column.color})` + : boardColor ?? undefined; +``` + +And apply: +``` +style={{ borderTop: effectiveColor ? `3px solid ${effectiveColor}${column.color ? '' : '30'}` : undefined }} +``` + +(Full opacity for explicit column colors, 30% for inherited board colors.) + +**Step 3: Verify column colors work** + +Open a board, right-click column header > Color > pick a color. Verify the top border changes. + +**Step 4: Commit** + +```bash +git add src/components/board/ColumnHeader.tsx +git commit -m "feat: add column color picker submenu with 10 preset colors" +``` + +--- + +### Task 9: Card Cover Color + +**Files:** +- Modify: `src/components/board/CardThumbnail.tsx` +- Modify: `src/components/card-detail/CardDetailModal.tsx` + +**Step 1: Render cover color bar in CardThumbnail** + +In `CardThumbnail.tsx`, add the cover color bar as the first child inside the ``: + +```tsx +{card.coverColor && ( +
    +)} +``` + +Note: The negative margins `-mx-3 -mt-3` need to match the card padding. Since we moved to density-based padding, adjust to use `calc(-0.75rem * var(--density-factor))` as inline margin, or simpler — wrap the cover bar outside the padding area. Actually, the simplest approach: put the cover bar before the padded content and use absolute positioning or restructure. Let's use a simpler approach — add the bar at the top with negative margins matching the density padding: + +```tsx +{card.coverColor && ( +
    +)} +``` + +**Step 2: Add cover color picker to CardDetailModal** + +In `CardDetailModal.tsx`, add a "Cover" section to the right sidebar, before Labels: + +```tsx + + +``` + +Create the `CoverColorPicker` component inline in the same file: + +```tsx +function CoverColorPicker({ cardId, coverColor }: { cardId: string; coverColor: string | null }) { + const updateCard = useBoardStore((s) => s.updateCard); + const presets = [ + { hue: "160", label: "Teal" }, { hue: "240", label: "Blue" }, + { hue: "300", label: "Purple" }, { hue: "350", label: "Pink" }, + { hue: "25", label: "Red" }, { hue: "55", label: "Orange" }, + { hue: "85", label: "Yellow" }, { hue: "130", label: "Lime" }, + { hue: "200", label: "Cyan" }, { hue: "0", label: "Slate" }, + ]; + + return ( +
    +

    + Cover +

    +
    + + {presets.map(({ hue, label }) => ( +
    +
    + ); +} +``` + +**Step 3: Verify card covers work** + +Open card detail > set a cover color > close modal > verify the card thumbnail shows the color bar. + +**Step 4: Commit** + +```bash +git add src/components/board/CardThumbnail.tsx src/components/card-detail/CardDetailModal.tsx +git commit -m "feat: add card cover color with picker in card detail and bar in thumbnail" +``` + +--- + +### Task 10: Richer Card Thumbnails + +**Files:** +- Modify: `src/components/board/CardThumbnail.tsx` + +**Step 1: Add attachment and description indicators** + +Import Paperclip and AlignLeft from lucide-react: + +```typescript +import { Paperclip, AlignLeft } from "lucide-react"; +``` + +Expand the footer condition to also check for description and attachments: + +Replace: +```tsx +{(hasDueDate || card.checklist.length > 0) && ( +``` +With: +```tsx +{(hasDueDate || card.checklist.length > 0 || card.attachments.length > 0 || card.description) && ( +``` + +Add the indicators inside the footer div, after the checklist bar: + +```tsx +{card.description && ( + +)} +{card.attachments.length > 0 && ( + + + {card.attachments.length} + +)} +``` + +**Step 2: Verify indicators show** + +Create a card with a description and attachment. Verify the icons appear in the thumbnail footer. + +**Step 3: Commit** + +```bash +git add src/components/board/CardThumbnail.tsx +git commit -m "feat: add description and attachment indicators to card thumbnails" +``` + +--- + +### Task 11: Toast Notification System + +**Files:** +- Create: `src/stores/toast-store.ts` +- Create: `src/components/toast/ToastContainer.tsx` +- Modify: `src/App.tsx` +- Modify: `src/components/import-export/ImportExportButtons.tsx` +- Modify: `src/components/boards/BoardCard.tsx` + +**Step 1: Create toast store** + +Create `src/stores/toast-store.ts`: + +```typescript +import { create } from "zustand"; + +export type ToastType = "success" | "error" | "info"; + +interface Toast { + id: string; + message: string; + type: ToastType; +} + +interface ToastState { + toasts: Toast[]; + addToast: (message: string, type?: ToastType) => void; + removeToast: (id: string) => void; +} + +let nextId = 0; + +export const useToastStore = create((set) => ({ + toasts: [], + + addToast: (message, type = "info") => { + const id = String(++nextId); + set((s) => ({ toasts: [...s.toasts, { id, message, type }] })); + setTimeout(() => { + set((s) => ({ toasts: s.toasts.filter((t) => t.id !== id) })); + }, 3000); + }, + + removeToast: (id) => { + set((s) => ({ toasts: s.toasts.filter((t) => t.id !== id) })); + }, +})); +``` + +**Step 2: Create ToastContainer component** + +Create `src/components/toast/ToastContainer.tsx`: + +```typescript +import { AnimatePresence, motion } from "framer-motion"; +import { useToastStore } from "@/stores/toast-store"; + +const TYPE_STYLES = { + success: "bg-pylon-accent/10 text-pylon-accent border-pylon-accent/20", + error: "bg-pylon-danger/10 text-pylon-danger border-pylon-danger/20", + info: "bg-pylon-surface text-pylon-text border-border", +} as const; + +export function ToastContainer() { + const toasts = useToastStore((s) => s.toasts); + + return ( +
    + + {toasts.map((toast) => ( + + {toast.message} + + ))} + +
    + ); +} +``` + +**Step 3: Mount ToastContainer in App.tsx** + +In `src/App.tsx`, import and add `` after the ``: + +```typescript +import { ToastContainer } from "@/components/toast/ToastContainer"; +``` + +Add inside the return, after ``: +```tsx + +``` + +**Step 4: Wire up toasts in ImportExportButtons** + +In `ImportExportButtons.tsx`, import the toast store: + +```typescript +import { useToastStore } from "@/stores/toast-store"; +``` + +Add at top of component: +```typescript +const addToast = useToastStore((s) => s.addToast); +``` + +After successful import (after `addRecentBoard`): +```typescript +addToast("Board imported successfully", "success"); +``` + +In the catch block: +```typescript +addToast("Import failed — check file format", "error"); +``` + +After export calls (`handleExportJson`, `handleExportCsv`), add: +```typescript +addToast("Board exported", "success"); +``` + +**Step 5: Wire up toasts in BoardCard delete** + +In `BoardCard.tsx`, import and use toast: + +```typescript +import { useToastStore } from "@/stores/toast-store"; +``` + +Add in component: +```typescript +const addToast = useToastStore((s) => s.addToast); +``` + +After `handleDelete` succeeds: +```typescript +addToast(`"${board.title}" deleted`, "info"); +``` + +**Step 6: Verify toasts appear** + +Delete a board, export a board, import a board — verify toast pills appear bottom-right and auto-dismiss. + +**Step 7: Commit** + +```bash +git add src/stores/toast-store.ts src/components/toast/ToastContainer.tsx src/App.tsx src/components/import-export/ImportExportButtons.tsx src/components/boards/BoardCard.tsx +git commit -m "feat: add toast notification system with success, error, and info variants" +``` + +--- + +### Task 12: Undo/Redo Buttons in TopBar + +**Files:** +- Modify: `src/components/layout/TopBar.tsx` + +**Step 1: Add undo/redo buttons** + +Import additional icons: +```typescript +import { ArrowLeft, Settings, Search, Undo2, Redo2 } from "lucide-react"; +``` + +Import temporal store access: +```typescript +import { useBoardStore } from "@/stores/board-store"; +``` + +In the right section div (before the saving status span), add: + +```tsx +{isBoardView && ( + <> + + + + + + Undo Ctrl+Z + + + + + + + + Redo Ctrl+Shift+Z + + + +)} +``` + +Note: The disabled state won't reactively update since we're reading temporal state imperatively. For reactive disabled state, subscribe to the temporal store. If that's too complex, skip the disabled state for now and always show the buttons enabled. + +**Step 2: Verify undo/redo buttons appear and work** + +Open a board, make a change, click the undo button, verify it reverts. + +**Step 3: Commit** + +```bash +git add src/components/layout/TopBar.tsx +git commit -m "feat: add undo/redo buttons to TopBar with tooltips" +``` + +--- + +### Task 13: Keyboard Shortcut Help Modal + +**Files:** +- Create: `src/components/shortcuts/ShortcutHelpModal.tsx` +- Modify: `src/hooks/useKeyboardShortcuts.ts` +- Modify: `src/App.tsx` + +**Step 1: Create ShortcutHelpModal** + +Create `src/components/shortcuts/ShortcutHelpModal.tsx`: + +```typescript +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, +} from "@/components/ui/dialog"; + +interface ShortcutHelpModalProps { + open: boolean; + onOpenChange: (open: boolean) => void; +} + +const SHORTCUT_GROUPS = [ + { + category: "Navigation", + shortcuts: [ + { key: "Ctrl+K", description: "Open command palette" }, + { key: "?", description: "Show keyboard shortcuts" }, + { key: "Escape", description: "Close modal / cancel" }, + ], + }, + { + category: "Board", + shortcuts: [ + { key: "Ctrl+Z", description: "Undo" }, + { key: "Ctrl+Shift+Z", description: "Redo" }, + ], + }, +]; + +export function ShortcutHelpModal({ open, onOpenChange }: ShortcutHelpModalProps) { + return ( + + + + + Keyboard Shortcuts + + + Quick reference for all keyboard shortcuts. + + + +
    + {SHORTCUT_GROUPS.map((group) => ( +
    +

    + {group.category} +

    +
    + {group.shortcuts.map(({ key, description }) => ( +
    + {description} + + {key} + +
    + ))} +
    +
    + ))} +
    +
    +
    + ); +} +``` + +**Step 2: Add `?` key handler to useKeyboardShortcuts** + +In `useKeyboardShortcuts.ts`, after the Escape handler, add: + +```typescript +// ? : open keyboard shortcut help +if (e.key === "?" || (e.shiftKey && e.key === "/")) { + e.preventDefault(); + document.dispatchEvent(new CustomEvent("open-shortcut-help")); + return; +} +``` + +**Step 3: Wire up in App.tsx** + +In `App.tsx`, add state and listener: + +```typescript +import { ShortcutHelpModal } from "@/components/shortcuts/ShortcutHelpModal"; +``` + +Add state: +```typescript +const [shortcutHelpOpen, setShortcutHelpOpen] = useState(false); +``` + +Add event listener (in the existing useEffect pattern or a new one): +```typescript +useEffect(() => { + function handleOpenShortcutHelp() { + setShortcutHelpOpen(true); + } + document.addEventListener("open-shortcut-help", handleOpenShortcutHelp); + return () => { + document.removeEventListener("open-shortcut-help", handleOpenShortcutHelp); + }; +}, []); +``` + +Add the modal to the return: +```tsx + +``` + +**Step 4: Verify ? key opens the modal** + +Press `?` on a board view (not in an input) — verify the shortcuts modal appears. + +**Step 5: Commit** + +```bash +git add src/components/shortcuts/ShortcutHelpModal.tsx src/hooks/useKeyboardShortcuts.ts src/App.tsx +git commit -m "feat: add keyboard shortcut help modal triggered by ? key" +``` + +--- + +### Task 14: Board Backgrounds + +**Files:** +- Modify: `src/components/board/BoardView.tsx` +- Modify: `src/components/layout/TopBar.tsx` + +**Step 1: Add background patterns to BoardView** + +In `BoardView.tsx`, compute the background CSS based on board settings: + +Add a helper function before the component: + +```typescript +function getBoardBackground(board: Board): React.CSSProperties { + const bg = board.settings.background; + if (bg === "dots") { + return { + backgroundImage: `radial-gradient(circle, currentColor 1px, transparent 1px)`, + backgroundSize: "20px 20px", + color: "oklch(50% 0 0 / 5%)", + }; + } + if (bg === "grid") { + return { + backgroundImage: `linear-gradient(currentColor 1px, transparent 1px), linear-gradient(90deg, currentColor 1px, transparent 1px)`, + backgroundSize: "24px 24px", + color: "oklch(50% 0 0 / 5%)", + }; + } + if (bg === "gradient") { + return { + background: `linear-gradient(135deg, ${board.color}08, ${board.color}03, transparent)`, + }; + } + return {}; +} +``` + +Apply it to the board container div: + +```tsx +
    +``` + +**Step 2: Add board settings gear button in TopBar** + +In `TopBar.tsx`, import `Settings2` from lucide-react (or use a gear icon) and add a board settings dropdown. + +For simplicity, add a button that dispatches a custom event to open a board settings popover. Or implement it directly as a dropdown in TopBar: + +```tsx +{isBoardView && board && ( + + + + + + + Background + + {(["none", "dots", "grid", "gradient"] as const).map((bg) => ( + useBoardStore.getState().updateBoardSettings({ ...board.settings, background: bg })} + > + {bg.charAt(0).toUpperCase() + bg.slice(1)} + {board.settings.background === bg && ( + * + )} + + ))} + + + + +)} +``` + +Import necessary dropdown components and `Sliders` icon from lucide-react in TopBar. + +**Step 3: Verify backgrounds render** + +Open board settings > Background > pick "Dots". Verify the subtle dot pattern appears behind columns. + +**Step 4: Commit** + +```bash +git add src/components/board/BoardView.tsx src/components/layout/TopBar.tsx +git commit -m "feat: add board background patterns (dots, grid, gradient) with settings dropdown" +``` + +--- + +### Task 15: Onboarding / Empty States + +**Files:** +- Modify: `src/components/boards/BoardList.tsx` +- Modify: `src/components/board/KanbanColumn.tsx` + +**Step 1: Upgrade empty board list state** + +In `BoardList.tsx`, replace the empty state block: + +```tsx +if (boards.length === 0) { + return ( + <> +
    +
    +

    + Welcome to OpenPylon +

    +

    + A local-first Kanban board that keeps your data on your machine. + Create your first board to get started. +

    +
    +
    + + +
    +
    + + + ); +} +``` + +**Step 2: Add empty column state** + +In `KanbanColumn.tsx`, when there are no cards, show a dashed placeholder: + +After the card list `
`, but still inside the ``, check for empty: + +Actually, the best approach is inside the `
    ` — when `column.cardIds.length === 0`, render a placeholder `
  • `: + +After `{column.cardIds.map(...)}`, add: + +```tsx +{column.cardIds.length === 0 && ( +
  • + Drop or add a card +
  • +)} +``` + +**Step 3: Verify empty states** + +Delete all boards — verify the welcome message. Create a board with blank template — verify empty columns show "Drop or add a card". + +**Step 4: Commit** + +```bash +git add src/components/boards/BoardList.tsx src/components/board/KanbanColumn.tsx +git commit -m "feat: upgrade empty states with welcome message and column placeholders" +``` + +--- + +### Task 16: Polish Pass + +**Files:** +- Modify: `src/index.css` +- Various touch-ups + +**Step 1: Add themed scrollbar styles** + +In `src/index.css`, add inside `@layer base`: + +```css +/* Thin themed scrollbars */ +* { + scrollbar-width: thin; + scrollbar-color: oklch(50% 0 0 / 20%) transparent; +} +.dark * { + scrollbar-color: oklch(80% 0 0 / 15%) transparent; +} +``` + +**Step 2: Verify dark mode works with all new features** + +Toggle to dark mode, verify: +- Accent colors look correct +- Column color borders visible +- Card cover bars visible +- Background patterns visible (dots, grid, gradient) +- Toasts styled correctly + +**Step 3: Verify zoom works at extremes** + +Set zoom to 75% and 150%, verify: +- No layout breakage +- Columns still have proper widths +- Card thumbnails still readable + +**Step 4: Commit** + +```bash +git add src/index.css +git commit -m "feat: add themed scrollbar styling and verify polish across modes" +``` + +--- + +### Task 17: Fix Capabilities & Final Verification + +**Files:** +- Modify: `src-tauri/capabilities/default.json` (if not already fixed) + +**Step 1: Ensure capabilities use Tauri v2 named permissions** + +Verify the file contains: +```json +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "default", + "description": "Default permissions for OpenPylon", + "windows": ["main"], + "permissions": [ + "core:default", + "opener:default", + "dialog:default", + "shell:default", + "fs:default", + "fs:allow-appdata-read-recursive", + "fs:allow-appdata-write-recursive", + "fs:allow-appdata-meta-recursive" + ] +} +``` + +**Step 2: Run the full app** + +Run: `npm run tauri dev` + +Verify end-to-end: +1. Create a board +2. Add columns and cards +3. Set column colors +4. Set card cover colors +5. Change board background +6. Toggle density and zoom in settings +7. Change accent color +8. Press `?` for shortcut help +9. Use undo/redo buttons +10. Export and import a board — verify toasts +11. Delete a board — verify toast +12. Toggle light/dark mode + +**Step 3: Final commit** + +```bash +git add -A +git commit -m "feat: complete visual glow-up — settings, zoom, accent, density, colors, toasts, backgrounds" +``` + +--- + +## Summary + +| Task | Description | Files | +|------|-------------|-------| +| 1 | Expand settings type & schema | settings.ts, schemas.ts | +| 2 | Wire app store with appearance | app-store.ts | +| 3 | Add density CSS variable | index.css | +| 4 | Rewrite settings dialog (tabbed) | SettingsDialog.tsx | +| 5 | Apply density to cards/columns | KanbanColumn, CardThumbnail, BoardView | +| 6 | Board color in UI | TopBar, ColumnHeader, KanbanColumn | +| 7 | Expand board types | board.ts, schemas.ts, board-factory.ts, board-store.ts | +| 8 | Column color UI | ColumnHeader | +| 9 | Card cover color | CardThumbnail, CardDetailModal | +| 10 | Richer card thumbnails | CardThumbnail | +| 11 | Toast notification system | toast-store.ts, ToastContainer, App, ImportExport, BoardCard | +| 12 | Undo/redo buttons | TopBar | +| 13 | Keyboard shortcut help modal | ShortcutHelpModal, useKeyboardShortcuts, App | +| 14 | Board backgrounds | BoardView, TopBar | +| 15 | Onboarding / empty states | BoardList, KanbanColumn | +| 16 | Polish pass | index.css | +| 17 | Fix capabilities & final test | capabilities/default.json |