Skip to content

Commit 747a68f

Browse files
committed
Add settings page dark mode
1 parent 19a0343 commit 747a68f

3 files changed

Lines changed: 254 additions & 76 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ Core loop:
2828
- Live, non-blocking agent run indicators in the desktop UI (phase updates + progress bars while fix suggestions are generated, then auto-hidden when the run ends)
2929
- Three-column desktop workspace layout: errors (left), graph (center, primary), and a suggestion panel (right) that appears only after an agent workflow run starts
3030
- Suggestion lifecycle management across agent runs with a single active suggestion panel: close hides the panel, and per-error `Open Suggestion` actions re-open the latest generated suggestion without re-running the agent
31+
- Settings page with a persisted dark mode toggle for the desktop UI
3132

3233
## Agent Workflow (Implemented in `python/agents`)
3334

src/frontend/app/App.tsx

Lines changed: 125 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ import GraphView from "../components/GraphView";
77
import { useAppState } from "../hooks/useAppState";
88
import { useCodeHealthController } from "../hooks/useCodeHealthController";
99

10+
type AppView = "dashboard" | "settings";
11+
type Theme = "light" | "dark";
12+
13+
const THEME_STORAGE_KEY = "structa-theme";
14+
1015
export default function App() {
1116
const { state, actions, canAnalyze } = useAppState();
1217
const {
@@ -27,12 +32,25 @@ export default function App() {
2732
service: tauriCodeHealthService
2833
});
2934
const [copyStatus, setCopyStatus] = useState("");
35+
const [view, setView] = useState<AppView>("dashboard");
36+
const [theme, setTheme] = useState<Theme>(() => {
37+
const storedTheme = window.localStorage.getItem(THEME_STORAGE_KEY);
38+
if (storedTheme === "dark" || storedTheme === "light") {
39+
return storedTheme;
40+
}
41+
return "light";
42+
});
3043
const showSuggestionPanel = Boolean(activeSuggestion);
3144

3245
useEffect(() => {
3346
setCopyStatus("");
3447
}, [activeSuggestion?.runId]);
3548

49+
useEffect(() => {
50+
document.documentElement.setAttribute("data-theme", theme);
51+
window.localStorage.setItem(THEME_STORAGE_KEY, theme);
52+
}, [theme]);
53+
3654
async function handleCopySuggestion() {
3755
if (!activeSuggestion) {
3856
return;
@@ -66,84 +84,115 @@ export default function App() {
6684

6785
return (
6886
<main className="app-shell">
69-
<h1>Structa</h1>
70-
<Controls
71-
projectPath={state.projectPath}
72-
status={state.status}
73-
onSelectFolder={handleSelectFolder}
74-
onAnalyze={handleAnalyze}
75-
canAnalyze={canAnalyze}
76-
/>
77-
78-
{state.error ? <div className="card error">{state.error}</div> : null}
79-
{agentRunProgress ? <AgentProgressPanel progress={agentRunProgress} /> : null}
80-
81-
<section
82-
className={`layout-grid ${
83-
showSuggestionPanel ? "layout-grid-with-suggestion" : "layout-grid-without-suggestion"
84-
}`}
85-
>
86-
<div className="column-left">
87-
<ErrorListPanel
88-
graph={state.graph}
89-
selectedNodeId={state.selectedNodeId}
90-
onSelectError={handleErrorClick}
91-
onResolveError={handleResolveError}
92-
onOpenSuggestion={handleOpenSuggestionForError}
93-
hasSuggestionForError={hasSuggestionForError}
94-
resolvingErrorId={resolvingErrorId}
95-
resolvingProgress={
96-
agentRunProgress && resolvingErrorId === agentRunProgress.errorId
97-
? agentRunProgress.progress
98-
: null
99-
}
100-
resolvingMessage={
101-
agentRunProgress && resolvingErrorId === agentRunProgress.errorId
102-
? agentRunProgress.message
103-
: null
104-
}
105-
/>
87+
<header className="app-header">
88+
<h1>Structa</h1>
89+
<div className="header-actions">
90+
<button type="button" onClick={() => setView("dashboard")} disabled={view === "dashboard"}>
91+
Dashboard
92+
</button>
93+
<button type="button" onClick={() => setView("settings")} disabled={view === "settings"}>
94+
Settings
95+
</button>
10696
</div>
107-
<div className="column-right">
108-
<GraphView
109-
graph={state.graph}
110-
focusRequest={state.graphFocusRequest}
111-
selectedNodeId={state.selectedNodeId}
112-
onNodeClick={handleNodeClick}
97+
</header>
98+
99+
{view === "dashboard" ? (
100+
<>
101+
<Controls
102+
projectPath={state.projectPath}
103+
status={state.status}
104+
onSelectFolder={handleSelectFolder}
105+
onAnalyze={handleAnalyze}
106+
canAnalyze={canAnalyze}
113107
/>
114-
</div>
115-
{showSuggestionPanel ? (
116-
<div className="column-suggestion">
117-
<section className="card panel suggestion-panel">
118-
<h2>Agent Suggestion</h2>
119-
{activeSuggestion ? (
120-
<>
121-
<p className="suggestion-meta">
122-
Error: <code>{activeSuggestion.errorId}</code> ({activeSuggestion.ruleCode}) | Model:{" "}
123-
{activeSuggestion.model} | Run: <code>{activeSuggestion.runId}</code>
124-
</p>
125-
<div className="suggestion-actions">
126-
<button type="button" onClick={() => void handleCopySuggestion()}>
127-
Copy Suggestion
128-
</button>
129-
<button type="button" onClick={() => handleCloseSuggestion(activeSuggestion.runId)}>
130-
Close Suggestion
131-
</button>
132-
<span className="suggestion-copy-status" aria-live="polite">
133-
{copyStatus}
134-
</span>
135-
</div>
136-
<pre>{activeSuggestion.suggestion}</pre>
137-
</>
138-
) : (
139-
<p className="suggestion-empty-state">
140-
No active suggestion.
141-
</p>
142-
)}
143-
</section>
144-
</div>
145-
) : null}
146-
</section>
108+
109+
{state.error ? <div className="card error">{state.error}</div> : null}
110+
{agentRunProgress ? <AgentProgressPanel progress={agentRunProgress} /> : null}
111+
112+
<section
113+
className={`layout-grid ${
114+
showSuggestionPanel ? "layout-grid-with-suggestion" : "layout-grid-without-suggestion"
115+
}`}
116+
>
117+
<div className="column-left">
118+
<ErrorListPanel
119+
graph={state.graph}
120+
selectedNodeId={state.selectedNodeId}
121+
onSelectError={handleErrorClick}
122+
onResolveError={handleResolveError}
123+
onOpenSuggestion={handleOpenSuggestionForError}
124+
hasSuggestionForError={hasSuggestionForError}
125+
resolvingErrorId={resolvingErrorId}
126+
resolvingProgress={
127+
agentRunProgress && resolvingErrorId === agentRunProgress.errorId
128+
? agentRunProgress.progress
129+
: null
130+
}
131+
resolvingMessage={
132+
agentRunProgress && resolvingErrorId === agentRunProgress.errorId
133+
? agentRunProgress.message
134+
: null
135+
}
136+
/>
137+
</div>
138+
<div className="column-right">
139+
<GraphView
140+
graph={state.graph}
141+
focusRequest={state.graphFocusRequest}
142+
selectedNodeId={state.selectedNodeId}
143+
onNodeClick={handleNodeClick}
144+
/>
145+
</div>
146+
{showSuggestionPanel ? (
147+
<div className="column-suggestion">
148+
<section className="card panel suggestion-panel">
149+
<h2>Agent Suggestion</h2>
150+
{activeSuggestion ? (
151+
<>
152+
<p className="suggestion-meta">
153+
Error: <code>{activeSuggestion.errorId}</code> ({activeSuggestion.ruleCode}) | Model:{" "}
154+
{activeSuggestion.model} | Run: <code>{activeSuggestion.runId}</code>
155+
</p>
156+
<div className="suggestion-actions">
157+
<button type="button" onClick={() => void handleCopySuggestion()}>
158+
Copy Suggestion
159+
</button>
160+
<button type="button" onClick={() => handleCloseSuggestion(activeSuggestion.runId)}>
161+
Close Suggestion
162+
</button>
163+
<span className="suggestion-copy-status" aria-live="polite">
164+
{copyStatus}
165+
</span>
166+
</div>
167+
<pre>{activeSuggestion.suggestion}</pre>
168+
</>
169+
) : (
170+
<p className="suggestion-empty-state">
171+
No active suggestion.
172+
</p>
173+
)}
174+
</section>
175+
</div>
176+
) : null}
177+
</section>
178+
</>
179+
) : (
180+
<section className="settings-layout">
181+
<section className="card settings-panel">
182+
<h2>Appearance</h2>
183+
<label className="toggle-row" htmlFor="dark-mode-toggle">
184+
<span>Dark mode</span>
185+
<input
186+
id="dark-mode-toggle"
187+
type="checkbox"
188+
checked={theme === "dark"}
189+
onChange={(event) => setTheme(event.currentTarget.checked ? "dark" : "light")}
190+
/>
191+
</label>
192+
<p className="settings-note">Theme preference is saved on this device.</p>
193+
</section>
194+
</section>
195+
)}
147196
</main>
148197
);
149198
}

src/frontend/styles/styles.css

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,19 @@
44
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
55
}
66

7+
:root[data-theme="dark"] {
8+
color: #e5e7eb;
9+
background: #111827;
10+
}
11+
712
* {
813
box-sizing: border-box;
914
}
1015

1116
body {
1217
margin: 0;
18+
color: inherit;
19+
background: inherit;
1320
}
1421

1522
.app-shell {
@@ -24,6 +31,18 @@ h1 {
2431
margin: 0 0 1rem;
2532
}
2633

34+
.app-header {
35+
display: flex;
36+
justify-content: space-between;
37+
align-items: center;
38+
gap: 0.75rem;
39+
}
40+
41+
.header-actions {
42+
display: flex;
43+
gap: 0.5rem;
44+
}
45+
2746
.card {
2847
background: #ffffff;
2948
border: 1px solid #d9e2ec;
@@ -575,13 +594,122 @@ button:disabled {
575594
cursor: not-allowed;
576595
}
577596

597+
.settings-layout {
598+
margin-top: 1rem;
599+
flex: 1;
600+
}
601+
602+
.settings-panel {
603+
max-width: 480px;
604+
display: grid;
605+
gap: 0.75rem;
606+
}
607+
608+
.settings-panel h2 {
609+
margin: 0;
610+
}
611+
612+
.toggle-row {
613+
display: flex;
614+
justify-content: space-between;
615+
align-items: center;
616+
gap: 0.6rem;
617+
}
618+
619+
.toggle-row input[type="checkbox"] {
620+
width: 1.05rem;
621+
height: 1.05rem;
622+
}
623+
624+
.settings-note {
625+
margin: 0;
626+
font-size: 0.88rem;
627+
color: #486581;
628+
}
629+
630+
/* Dark mode overrides for existing UI surfaces and controls. */
631+
:root[data-theme="dark"] .app-shell {
632+
color: #e5e7eb;
633+
}
634+
635+
:root[data-theme="dark"] .card,
636+
:root[data-theme="dark"] .error-list-item,
637+
:root[data-theme="dark"] .graph-canvas,
638+
:root[data-theme="dark"] .selected-path,
639+
:root[data-theme="dark"] .node-type-filters,
640+
:root[data-theme="dark"] .graph-display-controls,
641+
:root[data-theme="dark"] .suggestion-panel pre,
642+
:root[data-theme="dark"] .suggestion-empty-state,
643+
:root[data-theme="dark"] .loading-track,
644+
:root[data-theme="dark"] .agent-progress-track,
645+
:root[data-theme="dark"] .error-agent-progress-track {
646+
background: #1f2937;
647+
border-color: #4b5563;
648+
}
649+
650+
:root[data-theme="dark"] .graph-canvas {
651+
background: #111827;
652+
}
653+
654+
:root[data-theme="dark"] .loading-label,
655+
:root[data-theme="dark"] .graph-meta,
656+
:root[data-theme="dark"] .agent-progress-meta,
657+
:root[data-theme="dark"] .agent-progress-elapsed,
658+
:root[data-theme="dark"] .suggestion-meta,
659+
:root[data-theme="dark"] .error-node-label,
660+
:root[data-theme="dark"] .settings-note,
661+
:root[data-theme="dark"] .agent-progress-badge,
662+
:root[data-theme="dark"] .agent-progress-message,
663+
:root[data-theme="dark"] .graph-control-inline strong,
664+
:root[data-theme="dark"] .graph-control-inline,
665+
:root[data-theme="dark"] .node-type-filter,
666+
:root[data-theme="dark"] .status-idle {
667+
color: #cbd5e1;
668+
}
669+
670+
:root[data-theme="dark"] .graph-control-inline select,
671+
:root[data-theme="dark"] .reset-view-button {
672+
background: #111827;
673+
color: #e5e7eb;
674+
border-color: #4b5563;
675+
}
676+
677+
:root[data-theme="dark"] button {
678+
background: #1d4ed8;
679+
color: #f8fafc;
680+
border-color: #2563eb;
681+
}
682+
683+
:root[data-theme="dark"] button:hover:not(:disabled) {
684+
background: #1e40af;
685+
}
686+
687+
:root[data-theme="dark"] .error {
688+
border-color: #b91c1c;
689+
color: #fecaca;
690+
}
691+
692+
:root[data-theme="dark"] .error-list-item:hover {
693+
border-color: #64748b;
694+
}
695+
696+
:root[data-theme="dark"] .error-list-item.is-selected {
697+
border-color: #38bdf8;
698+
box-shadow: inset 0 0 0 1px #38bdf8;
699+
}
700+
578701
@media (max-width: 960px) {
579702
.app-shell {
580703
min-height: 100vh;
581704
height: auto;
582705
overflow: visible;
583706
}
584707

708+
.app-header {
709+
flex-direction: column;
710+
align-items: flex-start;
711+
}
712+
585713
.controls {
586714
grid-template-columns: 1fr;
587715
}

0 commit comments

Comments
 (0)