aboutsummaryrefslogtreecommitdiffstats
path: root/renderer/src/components/Header.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'renderer/src/components/Header.tsx')
-rw-r--r--renderer/src/components/Header.tsx172
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 @@
1import React, { useState, useEffect } from "react"; 1import React, { useState, useEffect } from "react";
2import type { Project, Session, Phase } from "../types"; 2import type { Session, Phase } from "../types";
3import { formatSessionLabel } from "../utils/timeFormat";
4 3
5const api = window.api; 4const api = window.api;
6 5
7type Theme = "dark" | "light"; 6type Theme = "dark" | "light";
8 7
9interface HeaderProps { 8interface 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
27const phaseLabels: Record<Phase, string> = { 18const phaseLabels: Record<Phase, string> = {
@@ -33,69 +24,18 @@ const phaseLabels: Record<Phase, string> = {
33const phases: Phase[] = ["research", "plan", "implement"]; 24const phases: Phase[] = ["research", "plan", "implement"];
34 25
35export function Header({ 26export 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 ── */}