diff options
Diffstat (limited to 'renderer/src')
| -rw-r--r-- | renderer/src/components/Header.tsx | 23 | ||||
| -rw-r--r-- | renderer/src/components/SettingsPage.tsx | 17 | ||||
| -rw-r--r-- | renderer/src/components/settings/GitSettings.tsx | 78 | ||||
| -rw-r--r-- | renderer/src/styles/globals.css | 93 |
4 files changed, 206 insertions, 5 deletions
diff --git a/renderer/src/components/Header.tsx b/renderer/src/components/Header.tsx index 3a530d3..1d954ec 100644 --- a/renderer/src/components/Header.tsx +++ b/renderer/src/components/Header.tsx | |||
| @@ -1,6 +1,8 @@ | |||
| 1 | import React, { useState } from "react"; | 1 | import React, { useState, useEffect } from "react"; |
| 2 | import type { Project, Session, Phase } from "../types"; | 2 | import type { Project, Session, Phase } from "../types"; |
| 3 | 3 | ||
| 4 | const api = window.api; | ||
| 5 | |||
| 4 | type Theme = "dark" | "light"; | 6 | type Theme = "dark" | "light"; |
| 5 | 7 | ||
| 6 | interface HeaderProps { | 8 | interface HeaderProps { |
| @@ -86,6 +88,16 @@ export function Header({ | |||
| 86 | setIsRenamingSession(false); | 88 | setIsRenamingSession(false); |
| 87 | }; | 89 | }; |
| 88 | 90 | ||
| 91 | // ── Maximize ───────────────────────────────────────────────── | ||
| 92 | const [isMaximized, setIsMaximized] = useState(false); | ||
| 93 | |||
| 94 | useEffect(() => { | ||
| 95 | // Returns the unsubscribe function; React cleanup calls it on unmount. | ||
| 96 | // On macOS, clicking the native green traffic light also fires this, | ||
| 97 | // keeping the glyph accurate when native controls are used. | ||
| 98 | return api.onWindowMaximized(setIsMaximized); | ||
| 99 | }, []); | ||
| 100 | |||
| 89 | // ── Branch copy ────────────────────────────────────────────── | 101 | // ── Branch copy ────────────────────────────────────────────── |
| 90 | const [copied, setCopied] = useState(false); | 102 | const [copied, setCopied] = useState(false); |
| 91 | 103 | ||
| @@ -225,6 +237,15 @@ export function Header({ | |||
| 225 | {theme === "dark" ? "[light]" : "[dark]"} | 237 | {theme === "dark" ? "[light]" : "[dark]"} |
| 226 | </button> | 238 | </button> |
| 227 | 239 | ||
| 240 | {/* ── Maximize toggle ── */} | ||
| 241 | <button | ||
| 242 | className="maximize-btn" | ||
| 243 | onClick={() => api.toggleMaximize()} | ||
| 244 | title={isMaximized ? "Restore window" : "Maximize window"} | ||
| 245 | > | ||
| 246 | {isMaximized ? '⊡' : '□'} | ||
| 247 | </button> | ||
| 248 | |||
| 228 | {/* ── Settings button ── */} | 249 | {/* ── Settings button ── */} |
| 229 | <button className="settings-btn" onClick={onOpenSettings} title="Settings"> | 250 | <button className="settings-btn" onClick={onOpenSettings} title="Settings"> |
| 230 | ⚙ | 251 | ⚙ |
diff --git a/renderer/src/components/SettingsPage.tsx b/renderer/src/components/SettingsPage.tsx index 5267665..9ebde44 100644 --- a/renderer/src/components/SettingsPage.tsx +++ b/renderer/src/components/SettingsPage.tsx | |||
| @@ -1,7 +1,8 @@ | |||
| 1 | import React, { useState } from "react"; | 1 | import React, { useState } from "react"; |
| 2 | import { SystemPromptsSettings } from "./settings/SystemPromptsSettings"; | 2 | import { SystemPromptsSettings } from "./settings/SystemPromptsSettings"; |
| 3 | import { GitSettings } from "./settings/GitSettings"; | ||
| 3 | 4 | ||
| 4 | type SettingsSection = "system-prompts"; | 5 | type SettingsSection = "system-prompts" | "git"; |
| 5 | 6 | ||
| 6 | interface SettingsPageProps { | 7 | interface SettingsPageProps { |
| 7 | onClose: () => void; | 8 | onClose: () => void; |
| @@ -16,14 +17,14 @@ export function SettingsPage({ onClose }: SettingsPageProps) { | |||
| 16 | {/* Header — matches the main app header height/style */} | 17 | {/* Header — matches the main app header height/style */} |
| 17 | <div className="settings-header"> | 18 | <div className="settings-header"> |
| 18 | <div className="settings-header-left"> | 19 | <div className="settings-header-left"> |
| 19 | <span className="settings-title">\u2699 Settings</span> | 20 | <span className="settings-title">{'⚙'} Settings</span> |
| 20 | </div> | 21 | </div> |
| 21 | <button | 22 | <button |
| 22 | className="settings-close" | 23 | className="settings-close" |
| 23 | onClick={onClose} | 24 | onClick={onClose} |
| 24 | title="Close settings" | 25 | title="Close settings" |
| 25 | > | 26 | > |
| 26 | \u00d7 | 27 | {'×'} |
| 27 | </button> | 28 | </button> |
| 28 | </div> | 29 | </div> |
| 29 | 30 | ||
| @@ -38,12 +39,20 @@ export function SettingsPage({ onClose }: SettingsPageProps) { | |||
| 38 | > | 39 | > |
| 39 | System Prompts | 40 | System Prompts |
| 40 | </button> | 41 | </button> |
| 41 | {/* Future sections added here */} | 42 | <button |
| 43 | className={`settings-nav-item${ | ||
| 44 | activeSection === "git" ? " active" : "" | ||
| 45 | }`} | ||
| 46 | onClick={() => setActiveSection("git")} | ||
| 47 | > | ||
| 48 | Git | ||
| 49 | </button> | ||
| 42 | </nav> | 50 | </nav> |
| 43 | 51 | ||
| 44 | {/* Content */} | 52 | {/* Content */} |
| 45 | <div className="settings-content"> | 53 | <div className="settings-content"> |
| 46 | {activeSection === "system-prompts" && <SystemPromptsSettings />} | 54 | {activeSection === "system-prompts" && <SystemPromptsSettings />} |
| 55 | {activeSection === "git" && <GitSettings />} | ||
| 47 | </div> | 56 | </div> |
| 48 | </div> | 57 | </div> |
| 49 | </div> | 58 | </div> |
diff --git a/renderer/src/components/settings/GitSettings.tsx b/renderer/src/components/settings/GitSettings.tsx new file mode 100644 index 0000000..0ec9875 --- /dev/null +++ b/renderer/src/components/settings/GitSettings.tsx | |||
| @@ -0,0 +1,78 @@ | |||
| 1 | import React, { useState, useEffect } from "react"; | ||
| 2 | |||
| 3 | const api = window.api; | ||
| 4 | |||
| 5 | export function GitSettings() { | ||
| 6 | // null = setting not yet loaded; true/false = loaded value | ||
| 7 | const [branchingEnabled, setBranchingEnabled] = useState<boolean | null>(null); | ||
| 8 | const [saveStatus, setSaveStatus] = useState<"idle" | "saved">("idle"); | ||
| 9 | |||
| 10 | useEffect(() => { | ||
| 11 | api.getSettings(["git.branchingEnabled"]).then((settings) => { | ||
| 12 | // Key absent or any value other than "true" → false (opt-in default) | ||
| 13 | setBranchingEnabled(settings["git.branchingEnabled"] === "true"); | ||
| 14 | }); | ||
| 15 | }, []); | ||
| 16 | |||
| 17 | const handleToggle = async (enabled: boolean) => { | ||
| 18 | setBranchingEnabled(enabled); | ||
| 19 | if (enabled) { | ||
| 20 | await api.setSetting("git.branchingEnabled", "true"); | ||
| 21 | } else { | ||
| 22 | await api.deleteSetting("git.branchingEnabled"); | ||
| 23 | } | ||
| 24 | setSaveStatus("saved"); | ||
| 25 | setTimeout(() => setSaveStatus("idle"), 1500); | ||
| 26 | }; | ||
| 27 | |||
| 28 | if (branchingEnabled === null) { | ||
| 29 | return ( | ||
| 30 | <div style={{ color: "var(--text-secondary)", fontSize: 12 }}> | ||
| 31 | Loading... | ||
| 32 | </div> | ||
| 33 | ); | ||
| 34 | } | ||
| 35 | |||
| 36 | return ( | ||
| 37 | <div> | ||
| 38 | <div className="settings-section-title">Git</div> | ||
| 39 | <div className="settings-section-desc"> | ||
| 40 | Configure how Claude Flow interacts with your project's git repository | ||
| 41 | during the implement phase. | ||
| 42 | </div> | ||
| 43 | |||
| 44 | <div className="settings-toggle-row"> | ||
| 45 | <label className="settings-toggle-label" htmlFor="git-branching-toggle"> | ||
| 46 | <span className="settings-toggle-text"> | ||
| 47 | <strong>Create a branch per session</strong> | ||
| 48 | <span className="settings-toggle-hint"> | ||
| 49 | When enabled, Claude Flow checks out a new{" "} | ||
| 50 | <code>claude-flow/<name>-<id></code> branch when | ||
| 51 | entering the implement phase. When disabled, commits land on | ||
| 52 | your current branch. | ||
| 53 | </span> | ||
| 54 | </span> | ||
| 55 | <span | ||
| 56 | className={`settings-toggle${branchingEnabled ? " on" : ""}`} | ||
| 57 | role="switch" | ||
| 58 | aria-checked={branchingEnabled} | ||
| 59 | id="git-branching-toggle" | ||
| 60 | tabIndex={0} | ||
| 61 | onClick={() => handleToggle(!branchingEnabled)} | ||
| 62 | onKeyDown={(e) => { | ||
| 63 | if (e.key === " " || e.key === "Enter") { | ||
| 64 | e.preventDefault(); | ||
| 65 | handleToggle(!branchingEnabled); | ||
| 66 | } | ||
| 67 | }} | ||
| 68 | /> | ||
| 69 | </label> | ||
| 70 | {saveStatus === "saved" && ( | ||
| 71 | <span className="settings-custom-badge" style={{ color: "var(--accent)" }}> | ||
| 72 | saved ✓ | ||
| 73 | </span> | ||
| 74 | )} | ||
| 75 | </div> | ||
| 76 | </div> | ||
| 77 | ); | ||
| 78 | } | ||
diff --git a/renderer/src/styles/globals.css b/renderer/src/styles/globals.css index 61a37c0..8e43f3a 100644 --- a/renderer/src/styles/globals.css +++ b/renderer/src/styles/globals.css | |||
| @@ -905,3 +905,96 @@ html[data-theme="light"] .settings-textarea:focus { | |||
| 905 | background: var(--bg-tertiary); | 905 | background: var(--bg-tertiary); |
| 906 | color: var(--text-primary); | 906 | color: var(--text-primary); |
| 907 | } | 907 | } |
| 908 | |||
| 909 | /* ── Settings Toggle Row ─────────────────────────────────────── */ | ||
| 910 | .settings-toggle-row { | ||
| 911 | display: flex; | ||
| 912 | align-items: center; | ||
| 913 | gap: 12px; | ||
| 914 | margin-top: 16px; | ||
| 915 | } | ||
| 916 | |||
| 917 | .settings-toggle-label { | ||
| 918 | display: flex; | ||
| 919 | align-items: flex-start; | ||
| 920 | gap: 14px; | ||
| 921 | cursor: pointer; | ||
| 922 | flex: 1; | ||
| 923 | } | ||
| 924 | |||
| 925 | .settings-toggle-text { | ||
| 926 | display: flex; | ||
| 927 | flex-direction: column; | ||
| 928 | gap: 4px; | ||
| 929 | font-size: 12px; | ||
| 930 | color: var(--text-primary); | ||
| 931 | line-height: 1.4; | ||
| 932 | } | ||
| 933 | |||
| 934 | .settings-toggle-hint { | ||
| 935 | color: var(--text-secondary); | ||
| 936 | font-size: 11px; | ||
| 937 | } | ||
| 938 | |||
| 939 | .settings-toggle-hint code { | ||
| 940 | font-family: monospace; | ||
| 941 | background: var(--bg-tertiary); | ||
| 942 | padding: 1px 3px; | ||
| 943 | border-radius: 2px; | ||
| 944 | } | ||
| 945 | |||
| 946 | /* The pill toggle switch */ | ||
| 947 | .settings-toggle { | ||
| 948 | flex-shrink: 0; | ||
| 949 | width: 36px; | ||
| 950 | height: 20px; | ||
| 951 | border-radius: 10px; | ||
| 952 | background: var(--border); | ||
| 953 | position: relative; | ||
| 954 | cursor: pointer; | ||
| 955 | transition: background 0.2s; | ||
| 956 | outline-offset: 2px; | ||
| 957 | } | ||
| 958 | |||
| 959 | .settings-toggle:focus-visible { | ||
| 960 | outline: 2px solid var(--accent); | ||
| 961 | } | ||
| 962 | |||
| 963 | .settings-toggle::after { | ||
| 964 | content: ""; | ||
| 965 | position: absolute; | ||
| 966 | top: 3px; | ||
| 967 | left: 3px; | ||
| 968 | width: 14px; | ||
| 969 | height: 14px; | ||
| 970 | border-radius: 50%; | ||
| 971 | background: var(--text-secondary); | ||
| 972 | transition: transform 0.2s, background 0.2s; | ||
| 973 | } | ||
| 974 | |||
| 975 | .settings-toggle.on { | ||
| 976 | background: var(--accent); | ||
| 977 | } | ||
| 978 | |||
| 979 | .settings-toggle.on::after { | ||
| 980 | transform: translateX(16px); | ||
| 981 | background: #fff; | ||
| 982 | } | ||
| 983 | |||
| 984 | /* ── Header Maximize Button ──────────────────────────────────── */ | ||
| 985 | .maximize-btn { | ||
| 986 | padding: 5px 8px; | ||
| 987 | background: transparent; | ||
| 988 | border: 1px solid var(--border); | ||
| 989 | border-radius: 2px; | ||
| 990 | color: var(--text-secondary); | ||
| 991 | cursor: pointer; | ||
| 992 | font-size: 13px; | ||
| 993 | font-family: inherit; | ||
| 994 | transition: background 0.15s, color 0.15s; | ||
| 995 | } | ||
| 996 | |||
| 997 | .maximize-btn:hover { | ||
| 998 | background: var(--bg-tertiary); | ||
| 999 | color: var(--text-primary); | ||
| 1000 | } | ||
