From 21302bdfe95843d27bd8807be58a8ce2f7e25998 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 15 Feb 2026 21:41:16 +0200 Subject: [PATCH] feat: redesign card detail modal as 2-column dashboard grid Replace 60/40 split layout with a 2x3 CSS grid where all sections get equal weight. Cover color header, progress bar on checklist, compact markdown editor, scroll containment on long lists. --- .../2026-02-15-card-detail-redesign-design.md | 73 +++ ...-15-card-detail-redesign-implementation.md | 467 ++++++++++++++++++ .../card-detail/CardDetailModal.tsx | 152 ++++-- .../card-detail/ChecklistSection.tsx | 28 +- src/components/card-detail/MarkdownEditor.tsx | 4 +- 5 files changed, 665 insertions(+), 59 deletions(-) create mode 100644 docs/plans/2026-02-15-card-detail-redesign-design.md create mode 100644 docs/plans/2026-02-15-card-detail-redesign-implementation.md diff --git a/docs/plans/2026-02-15-card-detail-redesign-design.md b/docs/plans/2026-02-15-card-detail-redesign-design.md new file mode 100644 index 0000000..f202b7b --- /dev/null +++ b/docs/plans/2026-02-15-card-detail-redesign-design.md @@ -0,0 +1,73 @@ +# Card Detail Modal Redesign — Design Document + +**Date:** 2026-02-15 +**Status:** Approved + +## Problem + +The current card detail modal uses a 60/40 split layout with a massive markdown editor on the left and all metadata (cover, labels, due date, checklist, attachments) crammed into a narrow right sidebar. This is unbalanced — the description field dominates despite being lightly used, while actionable sections like checklist are squeezed. + +## Design Decisions (from brainstorming) + +| Question | Answer | +|----------|--------| +| Primary use when opening card | Scan everything at once | +| Description importance | Light use (short notes) | +| Modal size | Go wider | +| Layout style | Dashboard grid | +| Section prominence | Equal weight | +| Long checklist behavior | Scroll within cell | +| Title position | Full-width header with cover color | +| Attachment display | Compact list | +| Description visibility | Always visible cell in grid | + +## Layout + +``` +┌──────────────────────────────────────────────────────────┐ +│ ██████████████████████████████████████████████████ [×] │ Cover color header +│ Card Title (click to edit) │ Inline-editable +├────────────────────────────┬─────────────────────────────┤ +│ LABELS │ DUE DATE │ Row 1: metadata +│ [Tag] [Tag] [Tag] [+] │ Feb 20 · 5 days left │ +├────────────────────────────┼─────────────────────────────┤ +│ CHECKLIST 2/5 │ DESCRIPTION │ Row 2: content +│ ✓ item 1 │ Short notes here... │ +│ ☐ item 2 (scroll) │ (click to edit) │ +│ + Add item │ │ +├────────────────────────────┼─────────────────────────────┤ +│ COVER │ ATTACHMENTS │ Row 3: secondary +│ ○ ○ ○ ○ ○ ○ ○ ○ × │ file.pdf · doc.png │ +│ │ [+ Add file] │ +└────────────────────────────┴─────────────────────────────┘ +``` + +### Header +- Full-width bar with cover color as background (or neutral `pylon-surface` if no cover) +- White text on colored bg, normal text on surface bg +- Inline-editable title (click → input → Enter/Escape) +- Close [×] button top-right + +### Grid Body +- CSS Grid: `grid-template-columns: 1fr 1fr`, `gap: 1rem` +- Each cell: `rounded-lg bg-pylon-column/50 p-4` +- Section headers: `font-mono text-xs uppercase tracking-widest text-pylon-text-secondary` +- Row 1: Labels + Due Date (small metadata) +- Row 2: Checklist + Description (main content, max-h ~200px with internal scroll) +- Row 3: Cover Color + Attachments (secondary) + +### Modal +- Width: `max-w-4xl` (up from `max-w-3xl`) +- Max height: `max-h-[85vh]` with body scrollable +- Shared layout animation preserved (`layoutId`) + +### Animation +- Grid cells stagger in with `fadeSlideUp` + `staggerContainer(0.05)` +- Backdrop blur + fade (existing) +- Escape/click-outside to close (existing) + +## Files to Modify +- `src/components/card-detail/CardDetailModal.tsx` — Full rewrite of layout +- `src/components/card-detail/MarkdownEditor.tsx` — Remove min-h-200, adapt for grid cell +- `src/components/card-detail/ChecklistSection.tsx` — Add max-height + scroll +- Minor: LabelPicker, DueDatePicker, AttachmentSection — No structural changes needed diff --git a/docs/plans/2026-02-15-card-detail-redesign-implementation.md b/docs/plans/2026-02-15-card-detail-redesign-implementation.md new file mode 100644 index 0000000..484e8a1 --- /dev/null +++ b/docs/plans/2026-02-15-card-detail-redesign-implementation.md @@ -0,0 +1,467 @@ +# Card Detail Modal Redesign — Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Replace the 60/40 split card detail modal with a 2-column dashboard grid where every section gets equal weight. + +**Architecture:** Full rewrite of `CardDetailModal.tsx` to use CSS Grid (2 cols, 3 rows) under a cover-color header. Sub-components (`MarkdownEditor`, `ChecklistSection`) get minor tweaks for cell sizing. `CoverColorPicker` moves from inline private component to its own grid cell. Framer Motion stagger preserved. + +**Tech Stack:** React 19, TypeScript, Framer Motion 12, Tailwind 4, Zustand + +--- + +### Task 1: Rewrite CardDetailModal — header + grid shell + +**Files:** +- Modify: `src/components/card-detail/CardDetailModal.tsx` (full rewrite, lines 1-245) + +**Step 1: Replace the entire file with the new layout** + +Replace the full contents of `CardDetailModal.tsx` with: + +```tsx +import { useState, useEffect, useRef } from "react"; +import { AnimatePresence, motion } from "framer-motion"; +import { X } from "lucide-react"; +import { useBoardStore } from "@/stores/board-store"; +import { MarkdownEditor } from "@/components/card-detail/MarkdownEditor"; +import { ChecklistSection } from "@/components/card-detail/ChecklistSection"; +import { LabelPicker } from "@/components/card-detail/LabelPicker"; +import { DueDatePicker } from "@/components/card-detail/DueDatePicker"; +import { AttachmentSection } from "@/components/card-detail/AttachmentSection"; +import { springs, staggerContainer, fadeSlideUp } from "@/lib/motion"; + +interface CardDetailModalProps { + cardId: string | null; + onClose: () => void; +} + +export function CardDetailModal({ cardId, onClose }: CardDetailModalProps) { + const card = useBoardStore((s) => + cardId ? s.board?.cards[cardId] ?? null : null + ); + const boardLabels = useBoardStore((s) => s.board?.labels ?? []); + const updateCard = useBoardStore((s) => s.updateCard); + + const open = cardId != null && card != null; + + return ( + + {open && card && cardId && ( + <> + {/* Backdrop */} + + + {/* Modal */} +
+ e.stopPropagation()} + > + + Card detail editor + + {/* Header: cover color background + title + close */} +
+ + +
+ + {/* Dashboard grid body */} + + {/* Row 1: Labels + Due Date */} + + + + + + + + + {/* Row 2: Checklist + Description */} + + + + + + + + + {/* Row 3: Cover + Attachments */} + + + + + + + + +
+
+ + )} +
+ ); +} + +/* ---------- Escape key handler ---------- */ + +function EscapeHandler({ onClose }: { onClose: () => void }) { + useEffect(() => { + function handleKeyDown(e: KeyboardEvent) { + if (e.key === "Escape") onClose(); + } + document.addEventListener("keydown", handleKeyDown); + return () => document.removeEventListener("keydown", handleKeyDown); + }, [onClose]); + return null; +} + +/* ---------- Inline editable title ---------- */ + +interface InlineTitleProps { + cardId: string; + title: string; + updateCard: (cardId: string, updates: { title: string }) => void; + hasColor: boolean; +} + +function InlineTitle({ cardId, title, updateCard, hasColor }: InlineTitleProps) { + const [editing, setEditing] = useState(false); + const [draft, setDraft] = useState(title); + const inputRef = useRef(null); + + useEffect(() => { + setDraft(title); + }, [title]); + + useEffect(() => { + if (editing && inputRef.current) { + inputRef.current.focus(); + inputRef.current.select(); + } + }, [editing]); + + function handleSave() { + const trimmed = draft.trim(); + if (trimmed && trimmed !== title) { + updateCard(cardId, { title: trimmed }); + } else { + setDraft(title); + } + setEditing(false); + } + + function handleKeyDown(e: React.KeyboardEvent) { + if (e.key === "Enter") { + e.preventDefault(); + handleSave(); + } else if (e.key === "Escape") { + setDraft(title); + setEditing(false); + } + } + + const textColor = hasColor ? "text-white" : "text-pylon-text"; + const hoverColor = hasColor ? "hover:text-white/80" : "hover:text-pylon-accent"; + + if (editing) { + return ( + setDraft(e.target.value)} + onBlur={handleSave} + onKeyDown={handleKeyDown} + className={`flex-1 font-heading text-xl bg-transparent outline-none border-b pb-0.5 ${ + hasColor ? "text-white border-white/50" : "text-pylon-text border-pylon-accent" + }`} + /> + ); + } + + return ( +

setEditing(true)} + className={`flex-1 cursor-pointer font-heading text-xl transition-colors ${textColor} ${hoverColor}`} + > + {title} +

+ ); +} + +/* ---------- Cover color picker ---------- */ + +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 2: Verify TypeScript compiles** + +Run: `npx tsc --noEmit` +Expected: No errors + +**Step 3: Commit** + +```bash +git add src/components/card-detail/CardDetailModal.tsx +git commit -m "feat: rewrite card detail modal as 2-column dashboard grid" +``` + +--- + +### Task 2: Adapt MarkdownEditor for grid cell + +**Files:** +- Modify: `src/components/card-detail/MarkdownEditor.tsx` (lines 93-103) + +**Step 1: Replace `min-h-[200px]` with cell-friendly sizing** + +In `MarkdownEditor.tsx`, change the textarea className (line 99): + +``` +Old: className="min-h-[200px] w-full resize-y rounded-md ... +New: className="min-h-[100px] max-h-[160px] w-full resize-y rounded-md ... +``` + +And change the preview container className (line 103): + +``` +Old: className="min-h-[200px] cursor-pointer rounded-md ... +New: className="min-h-[100px] max-h-[160px] overflow-y-auto cursor-pointer rounded-md ... +``` + +**Step 2: Verify TypeScript compiles** + +Run: `npx tsc --noEmit` +Expected: No errors + +**Step 3: Commit** + +```bash +git add src/components/card-detail/MarkdownEditor.tsx +git commit -m "feat: adapt markdown editor sizing for dashboard grid cell" +``` + +--- + +### Task 3: Add scroll containment to ChecklistSection + +**Files:** +- Modify: `src/components/card-detail/ChecklistSection.tsx` (lines 52-64) + +**Step 1: Add max-height + overflow to the checklist items container** + +In `ChecklistSection.tsx`, change the items container (line 53): + +``` +Old:
+New:
+``` + +Also add a small progress bar under the header. Change lines 39-50 (the header section) to: + +```tsx + {/* Header + progress */} +
+
+

+ Checklist +

+ {checklist.length > 0 && ( + + {checked}/{checklist.length} + + )} +
+ {checklist.length > 0 && ( +
+
+
+ )} +
+``` + +**Step 2: Verify TypeScript compiles** + +Run: `npx tsc --noEmit` +Expected: No errors + +**Step 3: Commit** + +```bash +git add src/components/card-detail/ChecklistSection.tsx +git commit -m "feat: add scroll containment and progress bar to checklist" +``` + +--- + +### Task 4: Remove unused Separator import + visual verification + +**Files:** +- Modify: `src/components/card-detail/CardDetailModal.tsx` (verify no stale imports) + +**Step 1: Verify the file has no unused imports** + +The rewrite in Task 1 already removed the `Separator` import. Confirm the import block does NOT include: +- `import { Separator } from "@/components/ui/separator";` + +If it's still there, delete it. + +**Step 2: Run TypeScript check** + +Run: `npx tsc --noEmit` +Expected: No errors + +**Step 3: Run the dev server and visually verify** + +Run: `npm run dev` (or `npx tauri dev` if checking in Tauri) + +Verify: +- Card detail modal opens on card click +- Full-width header shows cover color (or neutral bg) +- Title is editable (click to edit, Enter/Escape) +- Close button [x] works +- 2x3 grid: Labels | Due Date / Checklist | Description / Cover | Attachments +- Each cell has rounded-lg background +- Checklist scrolls when > ~6 items +- Description shows compact preview +- All animations stagger in +- Escape closes modal +- Click outside closes modal + +**Step 4: Commit** + +```bash +git add -A +git commit -m "feat: card detail modal dashboard grid redesign complete" +``` diff --git a/src/components/card-detail/CardDetailModal.tsx b/src/components/card-detail/CardDetailModal.tsx index 35b0324..77d914b 100644 --- a/src/components/card-detail/CardDetailModal.tsx +++ b/src/components/card-detail/CardDetailModal.tsx @@ -1,6 +1,6 @@ import { useState, useEffect, useRef } from "react"; import { AnimatePresence, motion } from "framer-motion"; -import { Separator } from "@/components/ui/separator"; +import { X } from "lucide-react"; import { useBoardStore } from "@/stores/board-store"; import { MarkdownEditor } from "@/components/card-detail/MarkdownEditor"; import { ChecklistSection } from "@/components/card-detail/ChecklistSection"; @@ -38,74 +38,112 @@ export function CardDetailModal({ cardId, onClose }: CardDetailModalProps) { /> {/* Modal */} -
+
e.stopPropagation()} > - {/* Close on Escape */} - - {/* Hidden accessible description */} Card detail editor + {/* Header: cover color background + title + close */} +
+ + +
+ + {/* Dashboard grid body */} - {/* Left panel: Title + Markdown (60%) */} + {/* Row 1: Labels + Due Date */} -
- -
- - -
- - {/* Vertical separator */} - - - {/* Right sidebar (40%) */} - - - - - + - - + + - - + {/* Row 2: Checklist + Description */} + + - + + + + {/* Row 3: Cover + Attachments */} + + + + + void; + hasColor: boolean; } -function InlineTitle({ cardId, title, updateCard }: InlineTitleProps) { +function InlineTitle({ cardId, title, updateCard, hasColor }: InlineTitleProps) { const [editing, setEditing] = useState(false); const [draft, setDraft] = useState(title); const inputRef = useRef(null); @@ -177,6 +216,9 @@ function InlineTitle({ cardId, title, updateCard }: InlineTitleProps) { } } + const textColor = hasColor ? "text-white" : "text-pylon-text"; + const hoverColor = hasColor ? "hover:text-white/80" : "hover:text-pylon-accent"; + if (editing) { return ( setDraft(e.target.value)} onBlur={handleSave} onKeyDown={handleKeyDown} - className="font-heading text-xl text-pylon-text bg-transparent outline-none border-b border-pylon-accent pb-0.5" + className={`flex-1 font-heading text-xl bg-transparent outline-none border-b pb-0.5 ${ + hasColor ? "text-white border-white/50" : "text-pylon-text border-pylon-accent" + }`} /> ); } @@ -193,7 +237,7 @@ function InlineTitle({ cardId, title, updateCard }: InlineTitleProps) { return (

setEditing(true)} - className="cursor-pointer font-heading text-xl text-pylon-text transition-colors hover:text-pylon-accent" + className={`flex-1 cursor-pointer font-heading text-xl transition-colors ${textColor} ${hoverColor}`} > {title}

@@ -202,14 +246,25 @@ function InlineTitle({ cardId, title, updateCard }: InlineTitleProps) { /* ---------- Cover color picker ---------- */ -function CoverColorPicker({ cardId, coverColor }: { cardId: string; coverColor: string | null }) { +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" }, + { 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 ( @@ -232,7 +287,8 @@ function CoverColorPicker({ cardId, coverColor }: { cardId: string; coverColor: className="size-6 rounded-full transition-transform hover:scale-110" style={{ backgroundColor: `oklch(55% 0.12 ${hue})`, - outline: coverColor === hue ? "2px solid currentColor" : "none", + outline: + coverColor === hue ? "2px solid currentColor" : "none", outlineOffset: "1px", }} title={label} diff --git a/src/components/card-detail/ChecklistSection.tsx b/src/components/card-detail/ChecklistSection.tsx index 5c59d33..d03265f 100644 --- a/src/components/card-detail/ChecklistSection.tsx +++ b/src/components/card-detail/ChecklistSection.tsx @@ -37,20 +37,30 @@ export function ChecklistSection({ cardId, checklist }: ChecklistSectionProps) { return (
- {/* Header */} -
-

- Checklist -

+ {/* Header + progress */} +
+
+

+ Checklist +

+ {checklist.length > 0 && ( + + {checked}/{checklist.length} + + )} +
{checklist.length > 0 && ( - - {checked}/{checklist.length} - +
+
+
)}
{/* Items */} -
+
{checklist.map((item) => ( ) : (
setMode("edit")} > {draft ? (