12-task plan covering CSS foundation, top nav, Cmd+K search, kanban cards, detail panel, analytics, mappings, and polish. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1008 lines
32 KiB
Markdown
1008 lines
32 KiB
Markdown
# UI Redesign Implementation Plan
|
|
|
|
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
|
|
|
**Goal:** Transform the TaskTracker web UI from a generic dark dashboard into a Linear/Vercel-inspired premium interface with top navigation, refined kanban cards, muted color system, command-K search, and frosted detail panel.
|
|
|
|
**Architecture:** Replace the sidebar layout with a horizontal top nav. Centralize all colors into CSS custom properties in `index.css`. Restyle every component with the new muted palette, subtle effects (noise texture, gradient glows, backdrop blur), and improved typography hierarchy. No API or backend changes.
|
|
|
|
**Tech Stack:** React 19, Tailwind CSS 4, Vite 7, dnd-kit, Recharts, Lucide React. Add `framer-motion` for panel/search animations.
|
|
|
|
---
|
|
|
|
### Task 1: Install framer-motion and update CSS foundation
|
|
|
|
**Files:**
|
|
- Modify: `TaskTracker.Web/package.json`
|
|
- Modify: `TaskTracker.Web/src/index.css`
|
|
- Modify: `TaskTracker.Web/src/lib/constants.ts`
|
|
- Modify: `TaskTracker.Web/index.html`
|
|
|
|
**Step 1: Install framer-motion**
|
|
|
|
Run: `cd TaskTracker.Web && npm install framer-motion`
|
|
|
|
**Step 2: Update `index.html` title**
|
|
|
|
Change the `<title>` from `tasktracker-web` to `TaskTracker`.
|
|
|
|
**Step 3: Replace `index.css` with the new design foundation**
|
|
|
|
Replace the entire contents of `src/index.css` with:
|
|
|
|
```css
|
|
@import "tailwindcss";
|
|
|
|
@theme {
|
|
--font-sans: 'Inter', system-ui, sans-serif;
|
|
--color-page: #0a0a0f;
|
|
--color-surface: #12131a;
|
|
--color-elevated: #1a1b26;
|
|
--color-border: rgba(255, 255, 255, 0.06);
|
|
--color-border-hover: rgba(255, 255, 255, 0.12);
|
|
--color-text-primary: #e2e8f0;
|
|
--color-text-secondary: #64748b;
|
|
--color-text-tertiary: #334155;
|
|
--color-accent: #8b5cf6;
|
|
--color-accent-end: #6366f1;
|
|
--color-status-active: #3b82f6;
|
|
--color-status-paused: #eab308;
|
|
--color-status-completed: #22c55e;
|
|
--color-status-pending: #64748b;
|
|
}
|
|
|
|
/* Noise grain texture */
|
|
body::before {
|
|
content: '';
|
|
position: fixed;
|
|
inset: 0;
|
|
z-index: 9999;
|
|
pointer-events: none;
|
|
opacity: 0.03;
|
|
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E");
|
|
background-repeat: repeat;
|
|
background-size: 256px 256px;
|
|
}
|
|
|
|
/* Active task pulse */
|
|
@keyframes pulse-glow {
|
|
0%, 100% {
|
|
box-shadow: 0 0 6px rgba(59, 130, 246, 0.3);
|
|
}
|
|
50% {
|
|
box-shadow: 0 0 16px rgba(59, 130, 246, 0.5);
|
|
}
|
|
}
|
|
|
|
.animate-pulse-glow {
|
|
animation: pulse-glow 2.5s ease-in-out infinite;
|
|
}
|
|
|
|
/* Live dot pulse */
|
|
@keyframes live-dot {
|
|
0%, 100% { opacity: 1; }
|
|
50% { opacity: 0.4; }
|
|
}
|
|
|
|
.animate-live-dot {
|
|
animation: live-dot 1.5s ease-in-out infinite;
|
|
}
|
|
|
|
/* Card hover glow border */
|
|
.card-glow {
|
|
position: relative;
|
|
}
|
|
|
|
.card-glow::before {
|
|
content: '';
|
|
position: absolute;
|
|
inset: -1px;
|
|
border-radius: inherit;
|
|
background: linear-gradient(135deg, rgba(139, 92, 246, 0.2), rgba(99, 102, 241, 0.1), transparent);
|
|
opacity: 0;
|
|
transition: opacity 0.2s ease;
|
|
z-index: -1;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.card-glow:hover::before {
|
|
opacity: 1;
|
|
}
|
|
|
|
/* Custom scrollbar */
|
|
::-webkit-scrollbar {
|
|
width: 6px;
|
|
}
|
|
|
|
::-webkit-scrollbar-track {
|
|
background: transparent;
|
|
}
|
|
|
|
::-webkit-scrollbar-thumb {
|
|
background: #1a1b26;
|
|
border-radius: 3px;
|
|
}
|
|
|
|
::-webkit-scrollbar-thumb:hover {
|
|
background: #2a2d37;
|
|
}
|
|
|
|
/* Selection color */
|
|
::selection {
|
|
background: rgba(139, 92, 246, 0.3);
|
|
}
|
|
```
|
|
|
|
**Step 4: Update `constants.ts` with new status colors**
|
|
|
|
Replace the entire contents of `src/lib/constants.ts` with:
|
|
|
|
```typescript
|
|
export const COLUMN_CONFIG = [
|
|
{ status: 'Pending' as const, label: 'Pending', color: '#64748b' },
|
|
{ status: 'Active' as const, label: 'Active', color: '#3b82f6' },
|
|
{ status: 'Paused' as const, label: 'Paused', color: '#eab308' },
|
|
{ status: 'Completed' as const, label: 'Completed', color: '#22c55e' },
|
|
] as const
|
|
|
|
export const CATEGORY_COLORS: Record<string, string> = {
|
|
Development: '#6366f1',
|
|
Research: '#06b6d4',
|
|
Communication: '#8b5cf6',
|
|
DevOps: '#f97316',
|
|
Documentation: '#14b8a6',
|
|
Design: '#ec4899',
|
|
Testing: '#3b82f6',
|
|
General: '#64748b',
|
|
Email: '#f59e0b',
|
|
Engineering: '#6366f1',
|
|
LaserCutting: '#ef4444',
|
|
Unknown: '#475569',
|
|
}
|
|
```
|
|
|
|
**Step 5: Commit**
|
|
|
|
```bash
|
|
git add -A
|
|
git commit -m "feat(ui): add design foundation — CSS tokens, noise texture, card glow effects"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 2: Replace sidebar with top navigation bar
|
|
|
|
**Files:**
|
|
- Rewrite: `TaskTracker.Web/src/components/Layout.tsx`
|
|
|
|
**Step 1: Replace `Layout.tsx` entirely**
|
|
|
|
```tsx
|
|
import { NavLink, Outlet, useNavigate } from 'react-router-dom'
|
|
import { LayoutGrid, BarChart3, Link, Plus, Search } from 'lucide-react'
|
|
import { useState, useEffect, useCallback } from 'react'
|
|
import SearchModal from './SearchModal.tsx'
|
|
|
|
const navItems = [
|
|
{ to: '/board', label: 'Board', icon: LayoutGrid },
|
|
{ to: '/analytics', label: 'Analytics', icon: BarChart3 },
|
|
{ to: '/mappings', label: 'Mappings', icon: Link },
|
|
]
|
|
|
|
export default function Layout() {
|
|
const navigate = useNavigate()
|
|
const [searchOpen, setSearchOpen] = useState(false)
|
|
const [showCreateHint, setShowCreateHint] = useState(false)
|
|
|
|
// Global Cmd+K handler
|
|
const handleGlobalKeyDown = useCallback((e: KeyboardEvent) => {
|
|
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
|
e.preventDefault()
|
|
setSearchOpen(true)
|
|
}
|
|
}, [])
|
|
|
|
useEffect(() => {
|
|
document.addEventListener('keydown', handleGlobalKeyDown)
|
|
return () => document.removeEventListener('keydown', handleGlobalKeyDown)
|
|
}, [handleGlobalKeyDown])
|
|
|
|
return (
|
|
<div className="flex flex-col h-screen bg-[var(--color-page)] text-[var(--color-text-primary)] overflow-hidden">
|
|
{/* Top navigation bar */}
|
|
<header className="flex items-center h-12 px-4 border-b border-[var(--color-border)] shrink-0 bg-[var(--color-page)]">
|
|
{/* Logo */}
|
|
<div className="flex items-center gap-2 mr-8">
|
|
<div className="w-5 h-5 rounded bg-gradient-to-br from-[var(--color-accent)] to-[var(--color-accent-end)] flex items-center justify-center">
|
|
<span className="text-[10px] font-bold text-white">T</span>
|
|
</div>
|
|
<span className="text-sm font-semibold tracking-tight">TaskTracker</span>
|
|
</div>
|
|
|
|
{/* Nav tabs */}
|
|
<nav className="flex items-center gap-1">
|
|
{navItems.map(({ to, label, icon: Icon }) => (
|
|
<NavLink
|
|
key={to}
|
|
to={to}
|
|
className={({ isActive }) =>
|
|
`flex items-center gap-1.5 px-3 py-1.5 rounded-md text-[13px] font-medium transition-colors ${
|
|
isActive
|
|
? 'text-white bg-white/[0.08]'
|
|
: 'text-[var(--color-text-secondary)] hover:text-white hover:bg-white/[0.04]'
|
|
}`
|
|
}
|
|
>
|
|
<Icon size={15} />
|
|
{label}
|
|
</NavLink>
|
|
))}
|
|
</nav>
|
|
|
|
{/* Spacer */}
|
|
<div className="flex-1" />
|
|
|
|
{/* Search trigger */}
|
|
<button
|
|
onClick={() => setSearchOpen(true)}
|
|
className="flex items-center gap-2 h-7 px-2.5 rounded-md text-[12px] text-[var(--color-text-secondary)] bg-white/[0.04] border border-[var(--color-border)] hover:border-[var(--color-border-hover)] hover:text-[var(--color-text-primary)] transition-colors mr-2"
|
|
>
|
|
<Search size={13} />
|
|
<span className="hidden sm:inline">Search</span>
|
|
<kbd className="hidden sm:inline text-[10px] font-mono text-[var(--color-text-tertiary)] bg-white/[0.06] px-1 py-0.5 rounded">
|
|
Ctrl K
|
|
</kbd>
|
|
</button>
|
|
|
|
{/* New task button */}
|
|
<button
|
|
onClick={() => {
|
|
navigate('/board')
|
|
setShowCreateHint(true)
|
|
setTimeout(() => setShowCreateHint(false), 100)
|
|
}}
|
|
className="flex items-center gap-1 h-7 px-2.5 rounded-md text-[12px] font-medium text-white bg-gradient-to-r from-[var(--color-accent)] to-[var(--color-accent-end)] hover:brightness-110 transition-all"
|
|
>
|
|
<Plus size={14} />
|
|
<span className="hidden sm:inline">New Task</span>
|
|
</button>
|
|
</header>
|
|
|
|
{/* Content */}
|
|
<main className="flex-1 overflow-auto p-5">
|
|
<Outlet context={{ showCreateHint }} />
|
|
</main>
|
|
|
|
{/* Search modal */}
|
|
{searchOpen && (
|
|
<SearchModal
|
|
onSelect={(taskId) => {
|
|
setSearchOpen(false)
|
|
navigate(`/board?task=${taskId}`)
|
|
}}
|
|
onClose={() => setSearchOpen(false)}
|
|
/>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
```
|
|
|
|
**Step 2: Commit**
|
|
|
|
```bash
|
|
git add -A
|
|
git commit -m "feat(ui): replace sidebar with slim top navigation bar"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 3: Create Command-K search modal
|
|
|
|
**Files:**
|
|
- Create: `TaskTracker.Web/src/components/SearchModal.tsx`
|
|
- Delete logic from: `TaskTracker.Web/src/components/SearchBar.tsx` (this file will be deleted)
|
|
|
|
**Step 1: Create `SearchModal.tsx`**
|
|
|
|
```tsx
|
|
import { useState, useRef, useEffect, useCallback } from 'react'
|
|
import { Search, ArrowRight } from 'lucide-react'
|
|
import { motion, AnimatePresence } from 'framer-motion'
|
|
import { useTasks } from '../api/tasks.ts'
|
|
import { CATEGORY_COLORS, COLUMN_CONFIG } from '../lib/constants.ts'
|
|
import type { WorkTask } from '../types/index.ts'
|
|
|
|
interface SearchModalProps {
|
|
onSelect: (taskId: number) => void
|
|
onClose: () => void
|
|
}
|
|
|
|
export default function SearchModal({ onSelect, onClose }: SearchModalProps) {
|
|
const { data: tasks } = useTasks()
|
|
const [query, setQuery] = useState('')
|
|
const [selectedIndex, setSelectedIndex] = useState(0)
|
|
const inputRef = useRef<HTMLInputElement>(null)
|
|
|
|
useEffect(() => {
|
|
inputRef.current?.focus()
|
|
}, [])
|
|
|
|
// Filter tasks
|
|
const results: WorkTask[] = (() => {
|
|
if (!tasks) return []
|
|
if (!query.trim()) {
|
|
// Show recent/active tasks when no query
|
|
return tasks
|
|
.filter((t) => t.status === 'Active' || t.status === 'Paused' || t.status === 'Pending')
|
|
.slice(0, 8)
|
|
}
|
|
const q = query.toLowerCase()
|
|
return tasks
|
|
.filter(
|
|
(t) =>
|
|
t.title.toLowerCase().includes(q) ||
|
|
(t.description && t.description.toLowerCase().includes(q)) ||
|
|
(t.category && t.category.toLowerCase().includes(q))
|
|
)
|
|
.slice(0, 8)
|
|
})()
|
|
|
|
useEffect(() => {
|
|
setSelectedIndex(0)
|
|
}, [query])
|
|
|
|
const handleKeyDown = useCallback(
|
|
(e: React.KeyboardEvent) => {
|
|
switch (e.key) {
|
|
case 'ArrowDown':
|
|
e.preventDefault()
|
|
setSelectedIndex((prev) => Math.min(prev + 1, results.length - 1))
|
|
break
|
|
case 'ArrowUp':
|
|
e.preventDefault()
|
|
setSelectedIndex((prev) => Math.max(prev - 1, 0))
|
|
break
|
|
case 'Enter':
|
|
e.preventDefault()
|
|
if (results[selectedIndex]) {
|
|
onSelect(results[selectedIndex].id)
|
|
}
|
|
break
|
|
case 'Escape':
|
|
e.preventDefault()
|
|
onClose()
|
|
break
|
|
}
|
|
},
|
|
[results, selectedIndex, onSelect, onClose]
|
|
)
|
|
|
|
const getStatusColor = (status: string) => {
|
|
const col = COLUMN_CONFIG.find((c) => c.status === status)
|
|
return col ? col.color : '#64748b'
|
|
}
|
|
|
|
return (
|
|
<AnimatePresence>
|
|
<motion.div
|
|
className="fixed inset-0 z-50 flex items-start justify-center pt-[20vh]"
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
exit={{ opacity: 0 }}
|
|
transition={{ duration: 0.15 }}
|
|
>
|
|
{/* Backdrop */}
|
|
<div
|
|
className="absolute inset-0 bg-black/60 backdrop-blur-sm"
|
|
onClick={onClose}
|
|
/>
|
|
|
|
{/* Modal */}
|
|
<motion.div
|
|
className="relative w-full max-w-lg bg-[var(--color-elevated)] border border-[var(--color-border)] rounded-xl shadow-2xl shadow-black/50 overflow-hidden"
|
|
initial={{ opacity: 0, scale: 0.95, y: -10 }}
|
|
animate={{ opacity: 1, scale: 1, y: 0 }}
|
|
exit={{ opacity: 0, scale: 0.95, y: -10 }}
|
|
transition={{ duration: 0.15 }}
|
|
>
|
|
{/* Search input */}
|
|
<div className="flex items-center gap-3 px-4 py-3 border-b border-[var(--color-border)]">
|
|
<Search size={16} className="text-[var(--color-text-secondary)] shrink-0" />
|
|
<input
|
|
ref={inputRef}
|
|
type="text"
|
|
value={query}
|
|
onChange={(e) => setQuery(e.target.value)}
|
|
onKeyDown={handleKeyDown}
|
|
placeholder="Search tasks..."
|
|
className="flex-1 bg-transparent text-[var(--color-text-primary)] text-sm placeholder-[var(--color-text-tertiary)] outline-none"
|
|
/>
|
|
<kbd className="text-[10px] font-mono text-[var(--color-text-tertiary)] bg-white/[0.06] px-1.5 py-0.5 rounded border border-[var(--color-border)]">
|
|
ESC
|
|
</kbd>
|
|
</div>
|
|
|
|
{/* Results */}
|
|
{results.length > 0 ? (
|
|
<div className="max-h-[300px] overflow-y-auto py-1">
|
|
{!query.trim() && (
|
|
<div className="px-4 py-1.5 text-[10px] font-medium uppercase tracking-wider text-[var(--color-text-tertiary)]">
|
|
Recent tasks
|
|
</div>
|
|
)}
|
|
{results.map((task, index) => {
|
|
const categoryColor = CATEGORY_COLORS[task.category ?? 'Unknown'] ?? CATEGORY_COLORS['Unknown']
|
|
const statusColor = getStatusColor(task.status)
|
|
|
|
return (
|
|
<button
|
|
key={task.id}
|
|
onClick={() => onSelect(task.id)}
|
|
onMouseEnter={() => setSelectedIndex(index)}
|
|
className={`w-full flex items-center gap-3 px-4 py-2.5 text-left transition-colors ${
|
|
index === selectedIndex ? 'bg-white/[0.06]' : ''
|
|
}`}
|
|
>
|
|
{/* Status dot */}
|
|
<span
|
|
className="shrink-0 w-2 h-2 rounded-full"
|
|
style={{ backgroundColor: statusColor }}
|
|
/>
|
|
|
|
{/* Title */}
|
|
<span className="flex-1 text-sm text-[var(--color-text-primary)] truncate">
|
|
{task.title}
|
|
</span>
|
|
|
|
{/* Category */}
|
|
{task.category && (
|
|
<span
|
|
className="shrink-0 text-[10px] font-medium px-1.5 py-0.5 rounded"
|
|
style={{
|
|
backgroundColor: categoryColor + '15',
|
|
color: categoryColor,
|
|
}}
|
|
>
|
|
{task.category}
|
|
</span>
|
|
)}
|
|
|
|
{/* Arrow hint on selected */}
|
|
{index === selectedIndex && (
|
|
<ArrowRight size={12} className="shrink-0 text-[var(--color-text-tertiary)]" />
|
|
)}
|
|
</button>
|
|
)
|
|
})}
|
|
</div>
|
|
) : query.trim() ? (
|
|
<div className="px-4 py-8 text-center text-sm text-[var(--color-text-secondary)]">
|
|
No tasks found
|
|
</div>
|
|
) : null}
|
|
|
|
{/* Footer */}
|
|
<div className="flex items-center gap-4 px-4 py-2 border-t border-[var(--color-border)] text-[10px] text-[var(--color-text-tertiary)]">
|
|
<span className="flex items-center gap-1">
|
|
<kbd className="font-mono bg-white/[0.06] px-1 py-0.5 rounded">↑↓</kbd>
|
|
Navigate
|
|
</span>
|
|
<span className="flex items-center gap-1">
|
|
<kbd className="font-mono bg-white/[0.06] px-1 py-0.5 rounded">↵</kbd>
|
|
Open
|
|
</span>
|
|
</div>
|
|
</motion.div>
|
|
</motion.div>
|
|
</AnimatePresence>
|
|
)
|
|
}
|
|
```
|
|
|
|
**Step 2: Delete `SearchBar.tsx`**
|
|
|
|
Run: `rm TaskTracker.Web/src/components/SearchBar.tsx`
|
|
|
|
**Step 3: Commit**
|
|
|
|
```bash
|
|
git add -A
|
|
git commit -m "feat(ui): add Command-K search modal, remove inline search bar"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 4: Redesign kanban columns and task cards
|
|
|
|
**Files:**
|
|
- Rewrite: `TaskTracker.Web/src/components/KanbanColumn.tsx`
|
|
- Rewrite: `TaskTracker.Web/src/components/TaskCard.tsx`
|
|
- Modify: `TaskTracker.Web/src/components/KanbanBoard.tsx`
|
|
|
|
**Step 1: Rewrite `TaskCard.tsx`**
|
|
|
|
```tsx
|
|
import { useSortable } from '@dnd-kit/sortable'
|
|
import { CSS } from '@dnd-kit/utilities'
|
|
import { Clock } from 'lucide-react'
|
|
import { WorkTaskStatus } from '../types/index.ts'
|
|
import type { WorkTask } from '../types/index.ts'
|
|
import { CATEGORY_COLORS } from '../lib/constants.ts'
|
|
|
|
function formatElapsed(task: WorkTask): string | null {
|
|
if (!task.startedAt) return null
|
|
const start = new Date(task.startedAt).getTime()
|
|
const end = task.completedAt ? new Date(task.completedAt).getTime() : Date.now()
|
|
const mins = Math.floor((end - start) / 60_000)
|
|
if (mins < 60) return `${mins}m`
|
|
const hours = Math.floor(mins / 60)
|
|
const remainder = mins % 60
|
|
if (hours < 24) return `${hours}h ${remainder}m`
|
|
const days = Math.floor(hours / 24)
|
|
return `${days}d ${hours % 24}h`
|
|
}
|
|
|
|
interface TaskCardProps {
|
|
task: WorkTask
|
|
onClick: (id: number) => void
|
|
}
|
|
|
|
export default function TaskCard({ task, onClick }: TaskCardProps) {
|
|
const {
|
|
attributes,
|
|
listeners,
|
|
setNodeRef,
|
|
transform,
|
|
transition,
|
|
isDragging,
|
|
} = useSortable({ id: task.id })
|
|
|
|
const style = {
|
|
transform: CSS.Transform.toString(transform),
|
|
transition,
|
|
opacity: isDragging ? 0.4 : 1,
|
|
scale: isDragging ? '1.02' : '1',
|
|
}
|
|
|
|
const categoryColor = CATEGORY_COLORS[task.category ?? 'Unknown'] ?? CATEGORY_COLORS['Unknown']
|
|
const isActive = task.status === WorkTaskStatus.Active
|
|
const elapsed = formatElapsed(task)
|
|
|
|
const completedSubTasks = task.subTasks?.filter(
|
|
(s) => s.status === WorkTaskStatus.Completed
|
|
).length ?? 0
|
|
const totalSubTasks = task.subTasks?.length ?? 0
|
|
|
|
return (
|
|
<div
|
|
ref={setNodeRef}
|
|
style={style}
|
|
{...attributes}
|
|
{...listeners}
|
|
onClick={() => onClick(task.id)}
|
|
className={`
|
|
card-glow rounded-xl cursor-grab active:cursor-grabbing
|
|
bg-[var(--color-surface)] border transition-all duration-200
|
|
hover:-translate-y-0.5
|
|
${isActive
|
|
? 'border-[color:var(--color-status-active)]/30 animate-pulse-glow'
|
|
: 'border-[var(--color-border)] hover:border-[var(--color-border-hover)]'
|
|
}
|
|
${isDragging ? 'shadow-xl shadow-black/40' : ''}
|
|
`}
|
|
>
|
|
<div className="px-3.5 py-3">
|
|
{/* Title row */}
|
|
<div className="flex items-start gap-2 mb-1.5">
|
|
{isActive && (
|
|
<span className="shrink-0 mt-1.5 w-1.5 h-1.5 rounded-full bg-[var(--color-status-active)] animate-live-dot" />
|
|
)}
|
|
<p className="text-[13px] font-medium text-[var(--color-text-primary)] leading-snug flex-1">
|
|
{task.title}
|
|
</p>
|
|
</div>
|
|
|
|
{/* Meta row */}
|
|
<div className="flex items-center gap-2 text-[11px] text-[var(--color-text-secondary)]">
|
|
{task.category && (
|
|
<span className="flex items-center gap-1">
|
|
<span
|
|
className="w-1.5 h-1.5 rounded-full"
|
|
style={{ backgroundColor: categoryColor }}
|
|
/>
|
|
{task.category}
|
|
</span>
|
|
)}
|
|
|
|
{elapsed && (
|
|
<span className="flex items-center gap-1">
|
|
<Clock size={10} />
|
|
{elapsed}
|
|
</span>
|
|
)}
|
|
|
|
{totalSubTasks > 0 && (
|
|
<span className="ml-auto flex items-center gap-1">
|
|
{Array.from({ length: totalSubTasks }, (_, i) => (
|
|
<span
|
|
key={i}
|
|
className={`w-1 h-1 rounded-full ${
|
|
i < completedSubTasks
|
|
? 'bg-[var(--color-status-completed)]'
|
|
: 'bg-white/10'
|
|
}`}
|
|
/>
|
|
))}
|
|
<span className="ml-0.5">{completedSubTasks}/{totalSubTasks}</span>
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
```
|
|
|
|
**Step 2: Rewrite `KanbanColumn.tsx`**
|
|
|
|
```tsx
|
|
import { useState } from 'react'
|
|
import { useDroppable } from '@dnd-kit/core'
|
|
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable'
|
|
import { Plus } from 'lucide-react'
|
|
import { WorkTaskStatus, type WorkTask } from '../types/index.ts'
|
|
import TaskCard from './TaskCard.tsx'
|
|
import CreateTaskForm from './CreateTaskForm.tsx'
|
|
|
|
interface KanbanColumnProps {
|
|
status: string
|
|
label: string
|
|
color: string
|
|
tasks: WorkTask[]
|
|
onTaskClick: (id: number) => void
|
|
onAddTask?: () => void
|
|
}
|
|
|
|
export default function KanbanColumn({
|
|
status,
|
|
label,
|
|
color,
|
|
tasks,
|
|
onTaskClick,
|
|
}: KanbanColumnProps) {
|
|
const { setNodeRef, isOver } = useDroppable({ id: `column-${status}` })
|
|
const [showForm, setShowForm] = useState(false)
|
|
|
|
const taskIds = tasks.map((t) => t.id)
|
|
|
|
return (
|
|
<div className="flex flex-col min-h-[300px]">
|
|
{/* Column header */}
|
|
<div className="mb-3">
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<h2 className="text-[11px] font-semibold uppercase tracking-wider text-[var(--color-text-secondary)]">
|
|
{label}
|
|
</h2>
|
|
<span className="text-[11px] text-[var(--color-text-tertiary)]">
|
|
{tasks.length}
|
|
</span>
|
|
</div>
|
|
<div className="h-[2px] rounded-full" style={{ backgroundColor: color }} />
|
|
</div>
|
|
|
|
{/* Cards area */}
|
|
<div
|
|
ref={setNodeRef}
|
|
className={`flex-1 flex flex-col gap-2 rounded-lg transition-colors duration-200 py-1 ${
|
|
isOver ? 'bg-white/[0.02]' : ''
|
|
}`}
|
|
>
|
|
<SortableContext items={taskIds} strategy={verticalListSortingStrategy}>
|
|
{tasks.map((task) => (
|
|
<TaskCard key={task.id} task={task} onClick={onTaskClick} />
|
|
))}
|
|
</SortableContext>
|
|
|
|
{/* Empty state */}
|
|
{tasks.length === 0 && !showForm && (
|
|
<div className="flex-1 flex items-center justify-center min-h-[80px] rounded-lg border border-dashed border-white/[0.06]">
|
|
<span className="text-[11px] text-[var(--color-text-tertiary)]">No tasks</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Add task (Pending column only) */}
|
|
{status === WorkTaskStatus.Pending && (
|
|
<div className="mt-2">
|
|
{showForm ? (
|
|
<CreateTaskForm onClose={() => setShowForm(false)} />
|
|
) : (
|
|
<button
|
|
onClick={() => setShowForm(true)}
|
|
className="flex items-center gap-1.5 w-full py-2 rounded-lg
|
|
text-[11px] text-[var(--color-text-tertiary)]
|
|
hover:text-[var(--color-text-secondary)] hover:bg-white/[0.02]
|
|
transition-colors"
|
|
>
|
|
<Plus size={13} />
|
|
New task
|
|
</button>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
```
|
|
|
|
**Step 3: Update `KanbanBoard.tsx` drag overlay style**
|
|
|
|
In `KanbanBoard.tsx`, change the DragOverlay inner div from:
|
|
```tsx
|
|
<div className="rotate-2 scale-105">
|
|
```
|
|
to:
|
|
```tsx
|
|
<div className="rotate-1 scale-[1.03] opacity-90">
|
|
```
|
|
|
|
**Step 4: Commit**
|
|
|
|
```bash
|
|
git add -A
|
|
git commit -m "feat(ui): redesign kanban columns and task cards — borderless columns, glow cards, live dots"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 5: Redesign the task detail panel
|
|
|
|
**Files:**
|
|
- Rewrite: `TaskTracker.Web/src/components/TaskDetailPanel.tsx`
|
|
|
|
**Step 1: Rewrite `TaskDetailPanel.tsx`**
|
|
|
|
This is a large file. The key changes are:
|
|
- Width from 400px to 480px
|
|
- Add backdrop blur and frosted glass effect
|
|
- Use `framer-motion` for animation instead of manual CSS translate
|
|
- Larger, bolder title
|
|
- Status as small pill, not big badge
|
|
- Sections separated by faint rules, not uppercase labeled boxes
|
|
- Action buttons remain sticky at bottom
|
|
- Better visual hierarchy throughout
|
|
|
|
Replace the entire component. Key structural changes to make:
|
|
|
|
1. Replace the overlay `div` with a `motion.div` that has `backdrop-blur-sm`
|
|
2. Replace the panel `div` with a `motion.div` using `initial={{ x: '100%' }}` and `animate={{ x: 0 }}` with a spring transition
|
|
3. Change panel width to `w-[480px]`
|
|
4. Change panel background to `bg-[var(--color-elevated)]/95 backdrop-blur-xl`
|
|
5. Replace section headers from `<h3 className="text-xs font-semibold uppercase tracking-wider text-[#64748b]">` to `<h3 className="text-[11px] font-medium text-[var(--color-text-secondary)] mb-2">`
|
|
6. Replace all hardcoded colors with CSS variable references
|
|
7. Status badge: small, muted pill style
|
|
8. Action buttons: use gradient for primary action, outline for secondary
|
|
|
|
The full rewrite is needed because the component is 449 lines with extensive inline styles. The implementer should rewrite it with the new design system colors and the framer-motion animation, keeping all the same editing logic (inline title/desc/category/estimate editing, save handlers, escape key handling, subtask/notes integration).
|
|
|
|
**Step 2: Commit**
|
|
|
|
```bash
|
|
git add -A
|
|
git commit -m "feat(ui): redesign detail panel — frosted glass, spring animation, refined hierarchy"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 6: Restyle the filter bar and create task form
|
|
|
|
**Files:**
|
|
- Modify: `TaskTracker.Web/src/components/FilterBar.tsx`
|
|
- Modify: `TaskTracker.Web/src/components/CreateTaskForm.tsx`
|
|
|
|
**Step 1: Update FilterBar colors**
|
|
|
|
Replace all hardcoded color references in `FilterBar.tsx`:
|
|
- `bg-[#2a2d37]` → `bg-white/[0.06]`
|
|
- `text-[#94a3b8]` → `text-[var(--color-text-secondary)]`
|
|
- `bg-indigo-500` → `bg-[var(--color-accent)]`
|
|
- Change chip border radius from `rounded-full` to `rounded-md`
|
|
- Make chips smaller: `text-[10px]` instead of `text-[11px]`, `px-2 py-0.5` padding
|
|
|
|
**Step 2: Update CreateTaskForm colors**
|
|
|
|
Replace all hardcoded colors in `CreateTaskForm.tsx`:
|
|
- `bg-[#0f1117]` → `bg-[var(--color-page)]`
|
|
- `bg-[#1a1d27]` → `bg-[var(--color-surface)]`
|
|
- `border-white/10` → `border-[var(--color-border)]`
|
|
- `text-[#64748b]` → `text-[var(--color-text-secondary)]`
|
|
- `focus:ring-indigo-500/60` → `focus:ring-[var(--color-accent)]/60`
|
|
- `bg-indigo-600` → `bg-gradient-to-r from-[var(--color-accent)] to-[var(--color-accent-end)]`
|
|
|
|
**Step 3: Commit**
|
|
|
|
```bash
|
|
git add -A
|
|
git commit -m "feat(ui): restyle filter bar and create task form with design tokens"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 7: Restyle the Board page
|
|
|
|
**Files:**
|
|
- Modify: `TaskTracker.Web/src/pages/Board.tsx`
|
|
|
|
**Step 1: Update Board.tsx**
|
|
|
|
The Board page just orchestrates components. Ensure it uses the new color tokens for any wrapper elements. The main change is removing `SearchBar` imports (already deleted) and ensuring the layout passes through cleanly with the new top-nav layout.
|
|
|
|
No major structural changes needed — the components it renders (FilterBar, KanbanBoard, TaskDetailPanel) are already restyled.
|
|
|
|
**Step 2: Commit**
|
|
|
|
```bash
|
|
git add -A
|
|
git commit -m "feat(ui): update Board page to match new design system"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 8: Redesign the Analytics page
|
|
|
|
**Files:**
|
|
- Modify: `TaskTracker.Web/src/pages/Analytics.tsx`
|
|
- Modify: `TaskTracker.Web/src/components/analytics/Timeline.tsx`
|
|
- Modify: `TaskTracker.Web/src/components/analytics/CategoryBreakdown.tsx`
|
|
- Modify: `TaskTracker.Web/src/components/analytics/ActivityFeed.tsx`
|
|
|
|
**Step 1: Add stat cards to Analytics page header**
|
|
|
|
Add a row of 3 stat cards above the charts:
|
|
- "Open Tasks" — count of non-completed tasks
|
|
- "Active Time" — total elapsed time of active+completed tasks
|
|
- "Top Category" — most common category
|
|
|
|
Each card: small, `bg-[var(--color-surface)]` with `border-[var(--color-border)]`, subtle rounded corners.
|
|
|
|
**Step 2: Restyle section containers**
|
|
|
|
Change section container backgrounds from `bg-[#161922]` to `bg-[var(--color-surface)]` and borders from `border-white/5` to `border-[var(--color-border)]`.
|
|
|
|
Replace section headers from `text-[#94a3b8] uppercase tracking-wider` to `text-[11px] font-medium text-[var(--color-text-secondary)] uppercase tracking-wider`.
|
|
|
|
**Step 3: Update chart colors in Timeline and CategoryBreakdown**
|
|
|
|
Use accent gradient colors for chart fills. Update any hardcoded colors to reference the design system.
|
|
|
|
**Step 4: Restyle ActivityFeed with timeline connector**
|
|
|
|
Add a vertical line connecting activity dots (like a git log):
|
|
- Left side: colored dots with a thin vertical line connecting them
|
|
- Right side: relative timestamps
|
|
- Each entry: task title, event type, timestamp
|
|
|
|
**Step 5: Commit**
|
|
|
|
```bash
|
|
git add -A
|
|
git commit -m "feat(ui): redesign analytics page — stat cards, styled charts, git-log activity feed"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 9: Redesign the Mappings page
|
|
|
|
**Files:**
|
|
- Modify: `TaskTracker.Web/src/pages/Mappings.tsx`
|
|
|
|
**Step 1: Update all hardcoded colors to design tokens**
|
|
|
|
Replace throughout:
|
|
- `bg-[#1a1d27]` → `bg-[var(--color-surface)]`
|
|
- `bg-[#1e2230]` → `bg-white/[0.04]`
|
|
- `bg-[#161922]` → `bg-[var(--color-surface)]`
|
|
- `bg-[#0f1117]` → `bg-[var(--color-page)]`
|
|
- `text-[#94a3b8]` → `text-[var(--color-text-secondary)]`
|
|
- `border-white/5` → `border-[var(--color-border)]`
|
|
- `border-white/10` → `border-[var(--color-border)]`
|
|
- `bg-indigo-600` → `bg-gradient-to-r from-[var(--color-accent)] to-[var(--color-accent-end)]`
|
|
|
|
**Step 2: Improve table row styling**
|
|
|
|
- Remove heavy border dividers, use subtle `border-b border-[var(--color-border)]`
|
|
- Add `hover:bg-white/[0.03]` for rows
|
|
- Make the header row blend in more: `text-[10px] uppercase tracking-wider`
|
|
|
|
**Step 3: Improve empty state**
|
|
|
|
Add a muted icon (Link icon from Lucide, larger at 40px, with `text-[var(--color-text-tertiary)]`) above the "No mappings" text.
|
|
|
|
**Step 4: Commit**
|
|
|
|
```bash
|
|
git add -A
|
|
git commit -m "feat(ui): redesign mappings page with design tokens and refined table"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 10: Update SubtaskList and NotesList styling
|
|
|
|
**Files:**
|
|
- Modify: `TaskTracker.Web/src/components/SubtaskList.tsx`
|
|
- Modify: `TaskTracker.Web/src/components/NotesList.tsx`
|
|
|
|
**Step 1: Update SubtaskList colors**
|
|
|
|
Replace all hardcoded colors with CSS variable references. The structure stays the same — just update colors to match the new design system.
|
|
|
|
**Step 2: Update NotesList colors**
|
|
|
|
Same treatment. Also make the note type badges use muted pill styling consistent with the rest of the UI.
|
|
|
|
**Step 3: Commit**
|
|
|
|
```bash
|
|
git add -A
|
|
git commit -m "feat(ui): restyle subtask list and notes list with design tokens"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 11: Visual QA and polish pass
|
|
|
|
**Files:**
|
|
- Any files needing tweaks
|
|
|
|
**Step 1: Run the dev server**
|
|
|
|
Run: `cd TaskTracker.Web && npm run dev`
|
|
|
|
**Step 2: Visual QA checklist**
|
|
|
|
Open the browser and verify:
|
|
- [ ] Top nav renders correctly, active tab highlights
|
|
- [ ] Cmd+K opens search modal with backdrop blur
|
|
- [ ] Board columns show without boxes, colored underlines work
|
|
- [ ] Task cards have gradient glow on hover
|
|
- [ ] Active task shows pulsing blue dot
|
|
- [ ] Drag-and-drop still works correctly
|
|
- [ ] Detail panel slides in with spring animation and frosted glass
|
|
- [ ] Filter chips styled correctly
|
|
- [ ] Create task form matches design system
|
|
- [ ] Analytics page stat cards render
|
|
- [ ] Activity feed has timeline connector
|
|
- [ ] Mappings table rows are clean
|
|
- [ ] Background noise texture is visible but subtle
|
|
- [ ] No remnant old color values (cyan rings, old backgrounds)
|
|
|
|
**Step 3: Fix any issues found during QA**
|
|
|
|
**Step 4: Commit**
|
|
|
|
```bash
|
|
git add -A
|
|
git commit -m "fix(ui): visual QA polish — fix alignment, spacing, and color consistency"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 12: Build verification
|
|
|
|
**Step 1: Run the production build**
|
|
|
|
Run: `cd TaskTracker.Web && npm run build`
|
|
|
|
Ensure no TypeScript errors or build failures.
|
|
|
|
**Step 2: If errors, fix them and re-build**
|
|
|
|
**Step 3: Final commit if any fixes were needed**
|
|
|
|
```bash
|
|
git add -A
|
|
git commit -m "fix: resolve build errors from UI redesign"
|
|
```
|