Skip to content

Commit 7a0fcd1

Browse files
authored
feat: add operator night mode (#80)
1 parent e971e8f commit 7a0fcd1

2 files changed

Lines changed: 61 additions & 1 deletion

File tree

src/App.tsx

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ import {
88
Lock,
99
MessageCircle,
1010
MessagesSquare,
11+
Moon,
1112
Plus,
1213
Send,
14+
Sun,
1315
ThumbsDown,
1416
ThumbsUp,
1517
UserCheck,
@@ -23,6 +25,7 @@ import { readConversationSinceBreakpoint } from "./domain";
2325

2426
type View = "overview" | "forums" | "direct" | "suggestions" | "onboarding" | "gates" | "profile";
2527
type AgentStatus = "pending" | "approved" | "suspended";
28+
type ThemeMode = "day" | "night";
2629
type ForumDraft = {
2730
slug: string;
2831
name: string;
@@ -50,6 +53,29 @@ const emptyState: AgentCommsState = {
5053
};
5154

5255
const useDemoData = import.meta.env.DEV && new URLSearchParams(window.location.search).get("demo") === "1";
56+
const themePreferenceKey = "agent-comms-theme-mode";
57+
58+
const nightModeTheme: Record<string, string> = {
59+
"--color-bg": "#101714",
60+
"--color-surface": "#18231f",
61+
"--color-surface-hover": "#22312b",
62+
"--color-sidebar": "#0b1210",
63+
"--color-sidebar-hover": "#1b2a25",
64+
"--color-text": "#edf5ee",
65+
"--color-text-secondary": "#bdcbc2",
66+
"--color-muted": "#93a39a",
67+
"--color-line": "#33413b",
68+
"--color-line-strong": "#46554e",
69+
"--color-accent": "#8bc7a7",
70+
"--color-accent-soft": "#294338",
71+
"--color-warning": "#a98135",
72+
"--color-danger": "#ef7a64",
73+
"--color-ink": "#edf5ee",
74+
"--color-inverse": "#edf5ee",
75+
"--color-inverse-muted": "#9aada4",
76+
"--color-inverse-accent": "#b9e1c9",
77+
"--shadow-card": "0 1px 2px rgba(0, 0, 0, 0.22), 0 18px 34px -24px rgba(0, 0, 0, 0.82)",
78+
};
5379

5480
type LiveConversationSession = {
5581
id: string;
@@ -234,6 +260,12 @@ function shellSingleQuote(value: string) {
234260
return `'${value.replaceAll("'", "'\\''")}'`;
235261
}
236262

263+
function getInitialThemeMode(): ThemeMode {
264+
const stored = localStorage.getItem(themePreferenceKey);
265+
if (stored === "day" || stored === "night") return stored;
266+
return window.matchMedia?.("(prefers-color-scheme: dark)").matches ? "night" : "day";
267+
}
268+
237269
function onboardingCorrectionPrompt(agent: AgentIdentity) {
238270
const authStatus = agent.onboardingAuth?.status ?? "missing";
239271
const statusText = authStatus.replace("_", " ");
@@ -1455,11 +1487,13 @@ export function App() {
14551487
const [mintedTokens, setMintedTokens] = useState<Record<string, { token: string; copied?: boolean; fileCopied?: boolean } | undefined>>({});
14561488
const [liveSessions, setLiveSessions] = useState<LiveConversationSession[]>([]);
14571489
const [operatorToken] = useState(() => localStorage.getItem("agent-comms-operator-token") ?? "");
1490+
const [themeMode, setThemeMode] = useState<ThemeMode>(getInitialThemeMode);
14581491
const [apiStatus, setApiStatus] = useState(useDemoData ? "demo data" : "loading durable storage");
14591492
const [actionStatus, setActionStatus] = useState("");
14601493
const refreshSequenceRef = useRef(0);
14611494
const mutationEpochRef = useRef(0);
14621495
const activeOperatorMutationsRef = useRef(0);
1496+
const appTheme = themeMode === "night" ? { ...branding.theme, ...nightModeTheme } : branding.theme;
14631497

14641498
const beginOperatorMutation = useCallback(() => {
14651499
mutationEpochRef.current += 1;
@@ -1684,6 +1718,11 @@ export function App() {
16841718
localStorage.setItem("agent-comms-read-thread-activity-ids", JSON.stringify(readThreadActivityIds));
16851719
}, [readThreadActivityIds]);
16861720

1721+
useEffect(() => {
1722+
localStorage.setItem(themePreferenceKey, themeMode);
1723+
document.documentElement.dataset.theme = themeMode;
1724+
}, [themeMode]);
1725+
16871726
const latestConversationMessageIds = Object.fromEntries(
16881727
state.directConversations.map((conversation) => [
16891728
conversation.id,
@@ -2128,7 +2167,7 @@ export function App() {
21282167
};
21292168

21302169
return (
2131-
<main className="app-shell" style={branding.theme}>
2170+
<main className="app-shell" data-theme={themeMode} style={appTheme}>
21322171
<nav className="sidebar" aria-label="Main navigation">
21332172
<div className={branding.logoUrl ? "brand has-logo" : "brand"}>
21342173
{branding.logoUrl ? (
@@ -2161,6 +2200,16 @@ export function App() {
21612200
<p className="eyebrow">{branding.eyebrow}</p>
21622201
<h1>{branding.title}</h1>
21632202
</div>
2203+
<div className="topbar-actions">
2204+
<button
2205+
aria-label={themeMode === "night" ? "Switch to day mode" : "Switch to night mode"}
2206+
onClick={() => setThemeMode((current) => (current === "night" ? "day" : "night"))}
2207+
title={themeMode === "night" ? "Switch to day mode" : "Switch to night mode"}
2208+
type="button"
2209+
>
2210+
{themeMode === "night" ? <Sun aria-hidden="true" /> : <Moon aria-hidden="true" />}
2211+
</button>
2212+
</div>
21642213
</header>
21652214
<p className="api-status">Data source: {apiStatus}{actionStatus ? `; ${actionStatus}` : ""}</p>
21662215
{view === "overview" ? (

src/styles.css

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
--color-accent: #2f6f55;
1313
--color-accent-soft: #dcebbb;
1414
--color-warning: #f3dfb3;
15+
--color-danger: #cc4729;
16+
--color-ink: #193428;
1517
--color-inverse: #f8f4ea;
1618
--color-inverse-muted: #aec1b6;
1719
--color-inverse-accent: #e1ecb8;
@@ -33,6 +35,7 @@ body {
3335
margin: 0;
3436
min-width: 320px;
3537
min-height: 100vh;
38+
background: var(--color-bg);
3639
}
3740

3841
button {
@@ -44,6 +47,7 @@ button {
4447
display: grid;
4548
grid-template-columns: 280px minmax(0, 1fr);
4649
min-height: 100vh;
50+
background: var(--color-bg);
4751
}
4852

4953
.sidebar {
@@ -184,6 +188,13 @@ svg {
184188
color: var(--color-sidebar);
185189
}
186190

191+
.topbar-actions button:hover,
192+
.topbar-actions button:focus-visible {
193+
border-color: var(--color-accent);
194+
color: var(--color-accent);
195+
outline: 0;
196+
}
197+
187198
.section-title .section-action {
188199
width: auto;
189200
min-width: max-content;

0 commit comments

Comments
 (0)