diff --git a/docs/sprint-6.md b/docs/sprint-6.md new file mode 100644 index 00000000..6c7ef54b --- /dev/null +++ b/docs/sprint-6.md @@ -0,0 +1,37 @@ +# Sprint 6: Polish & Quality of Life + +**Goal:** Keyboard shortcuts, filtering, and UI polish. + +**Started:** 2026-01-26 +**Status:** In Progress + +--- + +## Stories + +| ID | Title | Status | Notes | +|----|-------|--------|-------| +| US-601 | Keyboard shortcuts | ✅ Complete | c, j/k, Enter, Esc, 1-4, ? | +| US-602 | Filter and search | ⏳ Todo | Project/type dropdowns, text search | +| US-603 | Task templates | ⏳ Todo | New from Template option | +| US-604 | Bulk actions | ⏳ Todo | Multi-select, bulk status/archive/delete | +| US-605 | Activity log | ⏳ Todo | Toggleable sidebar | + +--- + +## Progress Log + +### 2026-01-26 + +**US-601: Keyboard shortcuts** ✅ +- Created `useKeyboard` context provider +- Implemented shortcuts: + - `c` - Create new task + - `j/k` or `↓/↑` - Navigate tasks + - `Enter` - Open selected task + - `Escape` - Close panel/clear selection + - `1-4` - Move task to column (Todo/In Progress/Review/Done) + - `?` - Toggle keyboard shortcuts help dialog +- Added keyboard icon to header +- Created `KeyboardShortcutsDialog` help modal +- Task selection highlighting with ring indicator diff --git a/web/src/App.tsx b/web/src/App.tsx index 75be4615..813efe96 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -1,16 +1,21 @@ import { KanbanBoard } from './components/board/KanbanBoard'; import { Header } from './components/layout/Header'; import { Toaster } from './components/ui/toaster'; +import { KeyboardProvider } from './hooks/useKeyboard'; +import { KeyboardShortcutsDialog } from './components/layout/KeyboardShortcutsDialog'; function App() { return ( -
-
-
- -
- -
+ +
+
+
+ +
+ + +
+
); } diff --git a/web/src/components/board/KanbanBoard.tsx b/web/src/components/board/KanbanBoard.tsx index 93afb978..b15940ff 100644 --- a/web/src/components/board/KanbanBoard.tsx +++ b/web/src/components/board/KanbanBoard.tsx @@ -13,8 +13,9 @@ import { useSensor, useSensors, } from '@dnd-kit/core'; -import { useState } from 'react'; +import { useState, useEffect, useCallback } from 'react'; import { TaskCard } from '@/components/task/TaskCard'; +import { useKeyboard } from '@/hooks/useKeyboard'; const COLUMNS: { id: TaskStatus; title: string }[] = [ { id: 'todo', title: 'To Do' }, @@ -65,6 +66,40 @@ export function KanbanBoard() { const [activeTask, setActiveTask] = useState(null); const [selectedTask, setSelectedTask] = useState(null); const [detailOpen, setDetailOpen] = useState(false); + + const { + selectedTaskId, + setTasks, + setOnOpenTask, + setOnMoveTask, + } = useKeyboard(); + + // Register tasks with keyboard context + useEffect(() => { + if (tasks) { + setTasks(tasks); + } + }, [tasks, setTasks]); + + // Handler for opening a task + const handleTaskClick = useCallback((task: Task) => { + setSelectedTask(task); + setDetailOpen(true); + }, []); + + // Handler for moving a task + const handleMoveTask = useCallback((taskId: string, status: TaskStatus) => { + updateTask.mutate({ id: taskId, input: { status } }); + }, [updateTask]); + + // Register callbacks with keyboard context + useEffect(() => { + setOnOpenTask(handleTaskClick); + }, [handleTaskClick, setOnOpenTask]); + + useEffect(() => { + setOnMoveTask(handleMoveTask); + }, [handleMoveTask, setOnMoveTask]); const sensors = useSensors( useSensor(PointerSensor, { @@ -99,11 +134,6 @@ export function KanbanBoard() { } }; - const handleTaskClick = (task: Task) => { - setSelectedTask(task); - setDetailOpen(true); - }; - const handleDetailClose = (open: boolean) => { setDetailOpen(open); if (!open) { @@ -152,6 +182,7 @@ export function KanbanBoard() { title={column.title} tasks={tasksByStatus[column.id]} onTaskClick={handleTaskClick} + selectedTaskId={selectedTaskId} /> ))} diff --git a/web/src/components/board/KanbanColumn.tsx b/web/src/components/board/KanbanColumn.tsx index 0b9ce0c2..cb284262 100644 --- a/web/src/components/board/KanbanColumn.tsx +++ b/web/src/components/board/KanbanColumn.tsx @@ -8,6 +8,7 @@ interface KanbanColumnProps { title: string; tasks: Task[]; onTaskClick?: (task: Task) => void; + selectedTaskId?: string | null; } const columnColors: Record = { @@ -17,7 +18,7 @@ const columnColors: Record = { 'done': 'border-t-green-500', }; -export function KanbanColumn({ id, title, tasks, onTaskClick }: KanbanColumnProps) { +export function KanbanColumn({ id, title, tasks, onTaskClick, selectedTaskId }: KanbanColumnProps) { const { setNodeRef, isOver } = useDroppable({ id }); return ( @@ -52,6 +53,7 @@ export function KanbanColumn({ id, title, tasks, onTaskClick }: KanbanColumnProp key={task.id} task={task} onClick={() => onTaskClick?.(task)} + isSelected={task.id === selectedTaskId} /> )) )} diff --git a/web/src/components/layout/Header.tsx b/web/src/components/layout/Header.tsx index e1b0c0ee..210278ef 100644 --- a/web/src/components/layout/Header.tsx +++ b/web/src/components/layout/Header.tsx @@ -1,12 +1,19 @@ -import { Plus, Settings } from 'lucide-react'; +import { Plus, Settings, Keyboard } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { CreateTaskDialog } from '@/components/task/CreateTaskDialog'; import { SettingsDialog } from '@/components/settings/SettingsDialog'; -import { useState } from 'react'; +import { useState, useEffect } from 'react'; +import { useKeyboard } from '@/hooks/useKeyboard'; export function Header() { const [createOpen, setCreateOpen] = useState(false); const [settingsOpen, setSettingsOpen] = useState(false); + const { setOpenCreateDialog, openHelpDialog } = useKeyboard(); + + // Register the create dialog opener with keyboard context + useEffect(() => { + setOpenCreateDialog(() => setCreateOpen(true)); + }, [setOpenCreateDialog]); return (
@@ -28,6 +35,14 @@ export function Header() { New Task +