diff --git a/src/App.tsx b/src/App.tsx index 2486b9d..434b8c5 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,7 +1,37 @@ +import { useEffect } from "react"; +import { useAppStore } from "@/stores/app-store"; +import { AppShell } from "@/components/layout/AppShell"; + export default function App() { + const initialized = useAppStore((s) => s.initialized); + const init = useAppStore((s) => s.init); + const view = useAppStore((s) => s.view); + + useEffect(() => { + init(); + }, [init]); + + if (!initialized) { + return ( +
+ + Loading... + +
+ ); + } + return ( -
-

OpenPylon

-
+ + {view.type === "board-list" ? ( +
+ Board List +
+ ) : ( +
+ Board View +
+ )} +
); } diff --git a/src/components/layout/AppShell.tsx b/src/components/layout/AppShell.tsx new file mode 100644 index 0000000..d6a9969 --- /dev/null +++ b/src/components/layout/AppShell.tsx @@ -0,0 +1,18 @@ +import type { ReactNode } from "react"; +import { TooltipProvider } from "@/components/ui/tooltip"; +import { TopBar } from "@/components/layout/TopBar"; + +interface AppShellProps { + children: ReactNode; +} + +export function AppShell({ children }: AppShellProps) { + return ( + +
+ +
{children}
+
+
+ ); +} diff --git a/src/components/layout/TopBar.tsx b/src/components/layout/TopBar.tsx new file mode 100644 index 0000000..dede9cf --- /dev/null +++ b/src/components/layout/TopBar.tsx @@ -0,0 +1,164 @@ +import { useState, useRef, useEffect, useCallback } from "react"; +import { ArrowLeft, Settings, Search } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { + Tooltip, + TooltipTrigger, + TooltipContent, +} from "@/components/ui/tooltip"; +import { useAppStore } from "@/stores/app-store"; +import { useBoardStore } from "@/stores/board-store"; + +export function TopBar() { + const view = useAppStore((s) => s.view); + const setView = useAppStore((s) => s.setView); + const board = useBoardStore((s) => s.board); + const updateBoardTitle = useBoardStore((s) => s.updateBoardTitle); + const saving = useBoardStore((s) => s.saving); + const lastSaved = useBoardStore((s) => s.lastSaved); + + const isBoardView = view.type === "board"; + + const [editing, setEditing] = useState(false); + const [editValue, setEditValue] = useState(""); + const inputRef = useRef(null); + + useEffect(() => { + if (editing && inputRef.current) { + inputRef.current.focus(); + inputRef.current.select(); + } + }, [editing]); + + const startEditing = useCallback(() => { + if (board) { + setEditValue(board.title); + setEditing(true); + } + }, [board]); + + const commitEdit = useCallback(() => { + const trimmed = editValue.trim(); + if (trimmed && trimmed !== board?.title) { + updateBoardTitle(trimmed); + } + setEditing(false); + }, [editValue, board?.title, updateBoardTitle]); + + const handleKeyDown = useCallback( + (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + commitEdit(); + } else if (e.key === "Escape") { + setEditing(false); + } + }, + [commitEdit] + ); + + const savingStatus = saving + ? "Saving..." + : lastSaved + ? `Saved ${formatTimeAgo(lastSaved)}` + : null; + + return ( +
+ {/* Left section */} +
+ {isBoardView && ( + + + + + Back to board list + + )} +
+ + {/* Center section */} +
+ {isBoardView && board ? ( + editing ? ( + setEditValue(e.target.value)} + onBlur={commitEdit} + onKeyDown={handleKeyDown} + className="h-7 rounded-md border border-border bg-transparent px-2 text-center font-heading text-lg text-pylon-text outline-none focus:border-pylon-accent" + /> + ) : ( + + ) + ) : ( + + OpenPylon + + )} +
+ + {/* Right section */} +
+ {savingStatus && ( + + {savingStatus} + + )} + + + + + + + Command palette{" "} + Ctrl+K + + + + + + + + Settings + +
+
+ ); +} + +function formatTimeAgo(timestamp: number): string { + const seconds = Math.floor((Date.now() - timestamp) / 1000); + if (seconds < 5) return "just now"; + if (seconds < 60) return `${seconds}s ago`; + const minutes = Math.floor(seconds / 60); + return `${minutes}m ago`; +} diff --git a/src/lib/board-factory.ts b/src/lib/board-factory.ts new file mode 100644 index 0000000..eb2bebc --- /dev/null +++ b/src/lib/board-factory.ts @@ -0,0 +1,44 @@ +import { ulid } from "ulid"; +import type { Board, ColumnWidth } from "@/types/board"; + +type Template = "blank" | "kanban" | "sprint"; + +export function createBoard( + title: string, + color: string, + template: Template = "blank" +): Board { + const ts = new Date().toISOString(); + const board: Board = { + id: ulid(), + title, + color, + createdAt: ts, + updatedAt: ts, + columns: [], + cards: {}, + labels: [], + settings: { attachmentMode: "link" }, + }; + + const col = (t: string, w: ColumnWidth = "standard") => ({ + id: ulid(), + title: t, + cardIds: [] as string[], + width: w, + }); + + if (template === "kanban") { + board.columns = [col("To Do"), col("In Progress"), col("Done")]; + } else if (template === "sprint") { + board.columns = [ + col("Backlog"), + col("To Do"), + col("In Progress", "wide"), + col("Review"), + col("Done", "narrow"), + ]; + } + + return board; +}