diff options
Diffstat (limited to 'renderer/src/components/Header.tsx')
| -rw-r--r-- | renderer/src/components/Header.tsx | 172 |
1 files changed, 21 insertions, 151 deletions
diff --git a/renderer/src/components/Header.tsx b/renderer/src/components/Header.tsx index dc88a73..4e193e8 100644 --- a/renderer/src/components/Header.tsx +++ b/renderer/src/components/Header.tsx | |||
| @@ -1,27 +1,18 @@ | |||
| 1 | import React, { useState, useEffect } from "react"; | 1 | import React, { useState, useEffect } from "react"; |
| 2 | import type { Project, Session, Phase } from "../types"; | 2 | import type { Session, Phase } from "../types"; |
| 3 | import { formatSessionLabel } from "../utils/timeFormat"; | ||
| 4 | 3 | ||
| 5 | const api = window.api; | 4 | const api = window.api; |
| 6 | 5 | ||
| 7 | type Theme = "dark" | "light"; | 6 | type Theme = "dark" | "light"; |
| 8 | 7 | ||
| 9 | interface HeaderProps { | 8 | interface HeaderProps { |
| 10 | projects: Project[]; | ||
| 11 | sessions: Session[]; | ||
| 12 | selectedProject: Project | null; | ||
| 13 | selectedSession: Session | null; | 9 | selectedSession: Session | null; |
| 14 | onSelectProject: (project: Project | null) => void; | ||
| 15 | onSelectSession: (session: Session | null) => void; | ||
| 16 | onCreateProject: () => void; | ||
| 17 | onCreateSession: () => void; | ||
| 18 | onDeleteProject?: (id: string) => void; | ||
| 19 | onDeleteSession?: (id: string) => void; | ||
| 20 | onRenameSession?: (id: string, name: string) => void; | ||
| 21 | theme: Theme; | 10 | theme: Theme; |
| 22 | onToggleTheme: () => void; | 11 | onToggleTheme: () => void; |
| 23 | gitBranch: string | null; | 12 | gitBranch: string | null; |
| 24 | onOpenSettings: () => void; | 13 | onOpenSettings: () => void; |
| 14 | viewPhase: Phase; | ||
| 15 | onViewPhase: (phase: Phase) => void; | ||
| 25 | } | 16 | } |
| 26 | 17 | ||
| 27 | const phaseLabels: Record<Phase, string> = { | 18 | const phaseLabels: Record<Phase, string> = { |
| @@ -33,69 +24,18 @@ const phaseLabels: Record<Phase, string> = { | |||
| 33 | const phases: Phase[] = ["research", "plan", "implement"]; | 24 | const phases: Phase[] = ["research", "plan", "implement"]; |
| 34 | 25 | ||
| 35 | export function Header({ | 26 | export function Header({ |
| 36 | projects, | ||
| 37 | sessions, | ||
| 38 | selectedProject, | ||
| 39 | selectedSession, | 27 | selectedSession, |
| 40 | onSelectProject, | ||
| 41 | onSelectSession, | ||
| 42 | onCreateProject, | ||
| 43 | onCreateSession, | ||
| 44 | onDeleteProject, | ||
| 45 | onDeleteSession, | ||
| 46 | onRenameSession, | ||
| 47 | theme, | 28 | theme, |
| 48 | onToggleTheme, | 29 | onToggleTheme, |
| 49 | gitBranch, | 30 | gitBranch, |
| 50 | onOpenSettings, | 31 | onOpenSettings, |
| 32 | viewPhase, | ||
| 33 | onViewPhase, | ||
| 51 | }: HeaderProps) { | 34 | }: HeaderProps) { |
| 52 | const handleDeleteProject = () => { | ||
| 53 | if (!selectedProject || !onDeleteProject) return; | ||
| 54 | if (confirm(`Delete project "${selectedProject.name}"? This cannot be undone.`)) { | ||
| 55 | onDeleteProject(selectedProject.id); | ||
| 56 | } | ||
| 57 | }; | ||
| 58 | |||
| 59 | const handleDeleteSession = () => { | ||
| 60 | if (!selectedSession || !onDeleteSession) return; | ||
| 61 | if (confirm(`Delete session "${selectedSession.name}"? This cannot be undone.`)) { | ||
| 62 | onDeleteSession(selectedSession.id); | ||
| 63 | } | ||
| 64 | }; | ||
| 65 | |||
| 66 | const [isRenamingSession, setIsRenamingSession] = useState(false); | ||
| 67 | const [renameValue, setRenameValue] = useState(""); | ||
| 68 | // Guard against double-commit (onKeyDown Enter → unmount → onBlur) | ||
| 69 | const renameCommitted = React.useRef(false); | ||
| 70 | |||
| 71 | const startRename = () => { | ||
| 72 | if (!selectedSession) return; | ||
| 73 | renameCommitted.current = false; | ||
| 74 | setRenameValue(selectedSession.name); | ||
| 75 | setIsRenamingSession(true); | ||
| 76 | }; | ||
| 77 | |||
| 78 | const commitRename = () => { | ||
| 79 | if (renameCommitted.current) return; | ||
| 80 | renameCommitted.current = true; | ||
| 81 | if (selectedSession && onRenameSession && renameValue.trim()) { | ||
| 82 | onRenameSession(selectedSession.id, renameValue.trim()); | ||
| 83 | } | ||
| 84 | setIsRenamingSession(false); | ||
| 85 | }; | ||
| 86 | |||
| 87 | const cancelRename = () => { | ||
| 88 | renameCommitted.current = true; // prevent blur from committing after cancel | ||
| 89 | setIsRenamingSession(false); | ||
| 90 | }; | ||
| 91 | |||
| 92 | // ── Maximize ───────────────────────────────────────────────── | 35 | // ── Maximize ───────────────────────────────────────────────── |
| 93 | const [isMaximized, setIsMaximized] = useState(false); | 36 | const [isMaximized, setIsMaximized] = useState(false); |
| 94 | 37 | ||
| 95 | useEffect(() => { | 38 | useEffect(() => { |
| 96 | // Returns the unsubscribe function; React cleanup calls it on unmount. | ||
| 97 | // On macOS, clicking the native green traffic light also fires this, | ||
| 98 | // keeping the glyph accurate when native controls are used. | ||
| 99 | return api.onWindowMaximized(setIsMaximized); | 39 | return api.onWindowMaximized(setIsMaximized); |
| 100 | }, []); | 40 | }, []); |
| 101 | 41 | ||
| @@ -112,89 +52,7 @@ export function Header({ | |||
| 112 | return ( | 52 | return ( |
| 113 | <header className="header"> | 53 | <header className="header"> |
| 114 | <div className="header-left"> | 54 | <div className="header-left"> |
| 115 | {/* ── Wordmark ── */} | ||
| 116 | <span className="app-wordmark">Claude Flow</span> | 55 | <span className="app-wordmark">Claude Flow</span> |
| 117 | |||
| 118 | <select | ||
| 119 | value={selectedProject?.id || ""} | ||
| 120 | onChange={(e) => { | ||
| 121 | const project = projects.find((p) => p.id === e.target.value); | ||
| 122 | onSelectProject(project || null); | ||
| 123 | onSelectSession(null); | ||
| 124 | }} | ||
| 125 | > | ||
| 126 | <option value="">Select Project...</option> | ||
| 127 | {projects.map((p) => ( | ||
| 128 | <option key={p.id} value={p.id}> | ||
| 129 | {p.name} | ||
| 130 | </option> | ||
| 131 | ))} | ||
| 132 | </select> | ||
| 133 | <button onClick={onCreateProject}>+ Project</button> | ||
| 134 | {selectedProject && onDeleteProject && ( | ||
| 135 | <button | ||
| 136 | onClick={handleDeleteProject} | ||
| 137 | className="btn-delete" | ||
| 138 | title="Delete project" | ||
| 139 | > | ||
| 140 | 🗑️ | ||
| 141 | </button> | ||
| 142 | )} | ||
| 143 | |||
| 144 | {selectedProject && ( | ||
| 145 | <> | ||
| 146 | {isRenamingSession ? ( | ||
| 147 | <input | ||
| 148 | autoFocus | ||
| 149 | value={renameValue} | ||
| 150 | onChange={(e) => setRenameValue(e.target.value)} | ||
| 151 | onKeyDown={(e) => { | ||
| 152 | if (e.key === "Enter") commitRename(); | ||
| 153 | if (e.key === "Escape") cancelRename(); | ||
| 154 | }} | ||
| 155 | onBlur={commitRename} | ||
| 156 | className="session-rename-input" | ||
| 157 | /> | ||
| 158 | ) : ( | ||
| 159 | <select | ||
| 160 | value={selectedSession?.id || ""} | ||
| 161 | onChange={(e) => { | ||
| 162 | const session = sessions.find((s) => s.id === e.target.value); | ||
| 163 | onSelectSession(session || null); | ||
| 164 | }} | ||
| 165 | > | ||
| 166 | <option value="">Select Session...</option> | ||
| 167 | {sessions.map((s) => ( | ||
| 168 | <option key={s.id} value={s.id}> | ||
| 169 | {formatSessionLabel(s.name, s.updated_at)} | ||
| 170 | </option> | ||
| 171 | ))} | ||
| 172 | </select> | ||
| 173 | )} | ||
| 174 | <button onClick={onCreateSession}>+ Session</button> | ||
| 175 | {selectedSession && | ||
| 176 | onRenameSession && | ||
| 177 | !isRenamingSession && | ||
| 178 | selectedSession.phase !== "implement" && ( | ||
| 179 | <button | ||
| 180 | onClick={startRename} | ||
| 181 | className="btn-rename" | ||
| 182 | title="Rename session" | ||
| 183 | > | ||
| 184 | ✏️ | ||
| 185 | </button> | ||
| 186 | )} | ||
| 187 | {selectedSession && onDeleteSession && ( | ||
| 188 | <button | ||
| 189 | onClick={handleDeleteSession} | ||
| 190 | className="btn-delete" | ||
| 191 | title="Delete session" | ||
| 192 | > | ||
| 193 | 🗑️ | ||
| 194 | </button> | ||
| 195 | )} | ||
| 196 | </> | ||
| 197 | )} | ||
| 198 | </div> | 56 | </div> |
| 199 | 57 | ||
| 200 | <div className="header-right"> | 58 | <div className="header-right"> |
| @@ -205,15 +63,27 @@ export function Header({ | |||
| 205 | const currentIndex = phases.indexOf(selectedSession.phase); | 63 | const currentIndex = phases.indexOf(selectedSession.phase); |
| 206 | const isComplete = phaseIndex < currentIndex; | 64 | const isComplete = phaseIndex < currentIndex; |
| 207 | const isActive = phase === selectedSession.phase; | 65 | const isActive = phase === selectedSession.phase; |
| 66 | const isReachable = phaseIndex <= currentIndex; | ||
| 67 | const isViewing = phase === viewPhase && !isActive; | ||
| 208 | 68 | ||
| 209 | return ( | 69 | return isReachable ? ( |
| 210 | <span | 70 | <button |
| 211 | key={phase} | 71 | key={phase} |
| 212 | className={`phase-step ${isActive ? "active" : ""} ${ | 72 | className={`phase-step ${isActive ? "active" : ""} ${ |
| 213 | isComplete ? "complete" : "" | 73 | isComplete ? "complete" : "" |
| 214 | }`} | 74 | } ${isViewing ? "viewing" : ""}`} |
| 75 | onClick={() => onViewPhase(phase)} | ||
| 76 | title={ | ||
| 77 | isActive | ||
| 78 | ? `Viewing ${phaseLabels[phase]} (current)` | ||
| 79 | : `View ${phaseLabels[phase]} artifact` | ||
| 80 | } | ||
| 215 | > | 81 | > |
| 216 | {phaseLabels[phase]} | 82 | {phaseLabels[phase]} |
| 83 | </button> | ||
| 84 | ) : ( | ||
| 85 | <span key={phase} className="phase-step"> | ||
| 86 | {phaseLabels[phase]} | ||
| 217 | </span> | 87 | </span> |
| 218 | ); | 88 | ); |
| 219 | })} | 89 | })} |
| @@ -244,7 +114,7 @@ export function Header({ | |||
| 244 | onClick={() => api.toggleMaximize()} | 114 | onClick={() => api.toggleMaximize()} |
| 245 | title={isMaximized ? "Restore window" : "Maximize window"} | 115 | title={isMaximized ? "Restore window" : "Maximize window"} |
| 246 | > | 116 | > |
| 247 | {isMaximized ? '⊡' : '□'} | 117 | {isMaximized ? "⊡" : "□"} |
| 248 | </button> | 118 | </button> |
| 249 | 119 | ||
| 250 | {/* ── Settings button ── */} | 120 | {/* ── Settings button ── */} |
