diff --git a/src/App.tsx b/src/App.tsx
index 434b8c5..bd0ae7a 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,6 +1,8 @@
import { useEffect } from "react";
import { useAppStore } from "@/stores/app-store";
import { AppShell } from "@/components/layout/AppShell";
+import { BoardList } from "@/components/boards/BoardList";
+import { BoardView } from "@/components/board/BoardView";
export default function App() {
const initialized = useAppStore((s) => s.initialized);
@@ -23,15 +25,7 @@ export default function App() {
return (
- {view.type === "board-list" ? (
-
- Board List
-
- ) : (
-
- Board View
-
- )}
+ {view.type === "board-list" ? : }
);
}
diff --git a/src/components/boards/BoardCard.tsx b/src/components/boards/BoardCard.tsx
new file mode 100644
index 0000000..9d15376
--- /dev/null
+++ b/src/components/boards/BoardCard.tsx
@@ -0,0 +1,146 @@
+import { useState } from "react";
+import { formatDistanceToNow } from "date-fns";
+import { Trash2, Copy } from "lucide-react";
+import {
+ ContextMenu,
+ ContextMenuContent,
+ ContextMenuItem,
+ ContextMenuTrigger,
+ ContextMenuSeparator,
+} from "@/components/ui/context-menu";
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+ DialogDescription,
+ DialogFooter,
+} from "@/components/ui/dialog";
+import { Button } from "@/components/ui/button";
+import type { BoardMeta } from "@/types/board";
+import { useAppStore } from "@/stores/app-store";
+import { useBoardStore } from "@/stores/board-store";
+import { deleteBoard, loadBoard, saveBoard } from "@/lib/storage";
+
+interface BoardCardProps {
+ board: BoardMeta;
+}
+
+export function BoardCard({ board }: BoardCardProps) {
+ const [confirmDelete, setConfirmDelete] = useState(false);
+
+ const setView = useAppStore((s) => s.setView);
+ const addRecentBoard = useAppStore((s) => s.addRecentBoard);
+ const refreshBoards = useAppStore((s) => s.refreshBoards);
+ const openBoard = useBoardStore((s) => s.openBoard);
+
+ const relativeTime = formatDistanceToNow(new Date(board.updatedAt), {
+ addSuffix: true,
+ });
+
+ async function handleOpen() {
+ await openBoard(board.id);
+ setView({ type: "board", boardId: board.id });
+ addRecentBoard(board.id);
+ }
+
+ async function handleDelete() {
+ await deleteBoard(board.id);
+ await refreshBoards();
+ setConfirmDelete(false);
+ }
+
+ async function handleDuplicate() {
+ const original = await loadBoard(board.id);
+ const { ulid } = await import("ulid");
+ const ts = new Date().toISOString();
+ const duplicated = {
+ ...original,
+ id: ulid(),
+ title: `${original.title} (copy)`,
+ createdAt: ts,
+ updatedAt: ts,
+ };
+ await saveBoard(duplicated);
+ await refreshBoards();
+ }
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+ Duplicate
+
+
+ setConfirmDelete(true)}
+ >
+
+ Delete
+
+
+
+
+ {/* Delete confirmation dialog */}
+
+ >
+ );
+}
diff --git a/src/components/boards/BoardList.tsx b/src/components/boards/BoardList.tsx
new file mode 100644
index 0000000..5938aa4
--- /dev/null
+++ b/src/components/boards/BoardList.tsx
@@ -0,0 +1,54 @@
+import { useState } from "react";
+import { Plus } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import { useAppStore } from "@/stores/app-store";
+import { BoardCard } from "@/components/boards/BoardCard";
+import { NewBoardDialog } from "@/components/boards/NewBoardDialog";
+
+export function BoardList() {
+ const boards = useAppStore((s) => s.boards);
+ const [dialogOpen, setDialogOpen] = useState(false);
+
+ if (boards.length === 0) {
+ return (
+ <>
+
+
+ Create your first board
+
+
+
+
+ >
+ );
+ }
+
+ return (
+ <>
+
+ {/* Heading row */}
+
+
+ Your Boards
+
+
+
+
+ {/* Board grid */}
+
+ {boards.map((board) => (
+
+ ))}
+
+
+
+
+ >
+ );
+}
diff --git a/src/components/boards/NewBoardDialog.tsx b/src/components/boards/NewBoardDialog.tsx
new file mode 100644
index 0000000..7058c96
--- /dev/null
+++ b/src/components/boards/NewBoardDialog.tsx
@@ -0,0 +1,173 @@
+import { useState } from "react";
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+ DialogDescription,
+ DialogFooter,
+} from "@/components/ui/dialog";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { createBoard } from "@/lib/board-factory";
+import { saveBoard } from "@/lib/storage";
+import { useAppStore } from "@/stores/app-store";
+import { useBoardStore } from "@/stores/board-store";
+
+const PRESET_COLORS = [
+ "#6366f1", // indigo
+ "#8b5cf6", // violet
+ "#ec4899", // pink
+ "#f43f5e", // rose
+ "#f97316", // orange
+ "#eab308", // yellow
+ "#22c55e", // green
+ "#06b6d4", // cyan
+];
+
+type Template = "blank" | "kanban" | "sprint";
+
+interface NewBoardDialogProps {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+}
+
+export function NewBoardDialog({ open, onOpenChange }: NewBoardDialogProps) {
+ const [title, setTitle] = useState("");
+ const [color, setColor] = useState(PRESET_COLORS[0]);
+ const [template, setTemplate] = useState("blank");
+ const [creating, setCreating] = useState(false);
+
+ const refreshBoards = useAppStore((s) => s.refreshBoards);
+ const setView = useAppStore((s) => s.setView);
+ const addRecentBoard = useAppStore((s) => s.addRecentBoard);
+ const openBoard = useBoardStore((s) => s.openBoard);
+
+ async function handleCreate() {
+ const trimmed = title.trim();
+ if (!trimmed || creating) return;
+
+ setCreating(true);
+ try {
+ const board = createBoard(trimmed, color, template);
+ await saveBoard(board);
+ await refreshBoards();
+ await openBoard(board.id);
+ setView({ type: "board", boardId: board.id });
+ addRecentBoard(board.id);
+ onOpenChange(false);
+ resetForm();
+ } finally {
+ setCreating(false);
+ }
+ }
+
+ function resetForm() {
+ setTitle("");
+ setColor(PRESET_COLORS[0]);
+ setTemplate("blank");
+ }
+
+ function handleKeyDown(e: React.KeyboardEvent) {
+ if (e.key === "Enter" && title.trim()) {
+ handleCreate();
+ }
+ }
+
+ return (
+
+ );
+}