Skip to content

Commit 0b17b52

Browse files
committed
feat: add light theme with storybook toggle
1 parent dced39c commit 0b17b52

File tree

17 files changed

+458
-23
lines changed

17 files changed

+458
-23
lines changed

.storybook/preview.tsx

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,50 @@
1+
import React, { useEffect } from "react";
12
import type { Preview } from "@storybook/react-vite";
3+
import {
4+
ThemeProvider,
5+
useTheme,
6+
type ThemeMode,
7+
} from "../src/browser/contexts/ThemeContext";
28
import "../src/browser/styles/globals.css";
39

10+
const ThemeStorySync: React.FC<{ mode: ThemeMode }> = ({ mode }) => {
11+
const { theme, setTheme } = useTheme();
12+
13+
useEffect(() => {
14+
if (theme !== mode) {
15+
setTheme(mode);
16+
}
17+
}, [mode, setTheme, theme]);
18+
19+
return null;
20+
};
21+
422
const preview: Preview = {
23+
globalTypes: {
24+
theme: {
25+
name: "Theme",
26+
description: "Choose between light and dark UI themes",
27+
defaultValue: "dark",
28+
toolbar: {
29+
icon: "mirror",
30+
items: [
31+
{ value: "dark", title: "Dark" },
32+
{ value: "light", title: "Light" },
33+
],
34+
dynamicTitle: true,
35+
},
36+
},
37+
},
538
decorators: [
6-
(Story) => (
7-
<>
8-
<Story />
9-
</>
10-
),
39+
(Story, context) => {
40+
const mode = (context.globals.theme ?? "dark") as ThemeMode;
41+
return (
42+
<ThemeProvider>
43+
<ThemeStorySync mode={mode} />
44+
<Story />
45+
</ThemeProvider>
46+
);
47+
},
1148
],
1249
parameters: {
1350
controls: {
@@ -16,6 +53,12 @@ const preview: Preview = {
1653
date: /Date$/i,
1754
},
1855
},
56+
chromatic: {
57+
modes: {
58+
dark: { globals: { theme: "dark" } },
59+
light: { globals: { theme: "light" } },
60+
},
61+
},
1962
},
2063
};
2164

bun.lock

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"ghostty-web": "next",
3535
"jsonc-parser": "^3.3.1",
3636
"lru-cache": "^11.2.2",
37+
"lucide-react": "^0.553.0",
3738
"markdown-it": "^14.1.0",
3839
"minimist": "^1.2.8",
3940
"motion": "^12.23.24",
@@ -2226,7 +2227,7 @@
22262227

22272228
"lru-cache": ["lru-cache@11.2.2", "", {}, "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg=="],
22282229

2229-
"lucide-react": ["lucide-react@0.542.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-w3hD8/SQB7+lzU2r4VdFyzzOzKnUjTZIF/MQJGSSvni7Llewni4vuViRppfRAa2guOsY5k4jZyxw/i9DQHv+dw=="],
2230+
"lucide-react": ["lucide-react@0.553.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-BRgX5zrWmNy/lkVAe0dXBgd7XQdZ3HTf+Hwe3c9WK6dqgnj9h+hxV+MDncM88xDWlCq27+TKvHGE70ViODNILw=="],
22302231

22312232
"lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="],
22322233

@@ -3514,6 +3515,8 @@
35143515

35153516
"stack-utils/escape-string-regexp": ["escape-string-regexp@2.0.0", "", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="],
35163517

3518+
"streamdown/lucide-react": ["lucide-react@0.542.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-w3hD8/SQB7+lzU2r4VdFyzzOzKnUjTZIF/MQJGSSvni7Llewni4vuViRppfRAa2guOsY5k4jZyxw/i9DQHv+dw=="],
3519+
35173520
"string-length/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
35183521

35193522
"string_decoder/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],

index.html

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,38 @@
1616
margin: 0;
1717
padding: 0;
1818
overflow: hidden;
19-
background: #1e1e1e;
19+
background: var(--color-background, #1e1e1e);
2020
}
2121
#root {
2222
height: 100vh;
2323
overflow: hidden;
2424
}
2525
</style>
26+
<script>
27+
(function () {
28+
const THEME_KEY = "uiTheme";
29+
try {
30+
const stored = window.localStorage.getItem(THEME_KEY);
31+
const parsed = stored ? JSON.parse(stored) : null;
32+
const prefersLight = window.matchMedia
33+
? window.matchMedia("(prefers-color-scheme: light)").matches
34+
: false;
35+
const theme = parsed === "light" || parsed === "dark" ? parsed : prefersLight ? "light" : "dark";
36+
37+
document.documentElement.dataset.theme = theme;
38+
document.documentElement.style.colorScheme = theme;
39+
40+
const metaThemeColor = document.querySelector('meta[name="theme-color"]');
41+
if (metaThemeColor) {
42+
metaThemeColor.setAttribute("content", theme === "light" ? "#f5f6f8" : "#1e1e1e");
43+
}
44+
} catch (error) {
45+
console.warn("Failed to apply preferred theme early", error);
46+
document.documentElement.dataset.theme = "dark";
47+
document.documentElement.style.colorScheme = "dark";
48+
}
49+
})();
50+
</script>
2651
</head>
2752
<body>
2853
<div id="root"></div>

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@
7474
"ghostty-web": "next",
7575
"jsonc-parser": "^3.3.1",
7676
"lru-cache": "^11.2.2",
77+
"lucide-react": "^0.553.0",
7778
"markdown-it": "^14.1.0",
7879
"minimist": "^1.2.8",
7980
"motion": "^12.23.24",

src/browser/App.tsx

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { CommandRegistryProvider, useCommandRegistry } from "./contexts/CommandR
2121
import type { CommandAction } from "./contexts/CommandRegistryContext";
2222
import { ModeProvider } from "./contexts/ModeContext";
2323
import { ProviderOptionsProvider } from "./contexts/ProviderOptionsContext";
24+
import { ThemeProvider, useTheme, type ThemeMode } from "./contexts/ThemeContext";
2425
import { ThinkingProvider } from "./contexts/ThinkingContext";
2526
import { CommandPalette } from "./components/CommandPalette";
2627
import { buildCoreSources, type BuildSourcesParams } from "./utils/commands/sources";
@@ -48,6 +49,13 @@ function AppInner() {
4849
beginWorkspaceCreation,
4950
clearPendingWorkspaceCreation,
5051
} = useWorkspaceContext();
52+
const { theme, setTheme, toggleTheme } = useTheme();
53+
const setThemePreference = useCallback(
54+
(nextTheme: ThemeMode) => {
55+
setTheme(nextTheme);
56+
},
57+
[setTheme]
58+
);
5159
const {
5260
projects,
5361
removeProject,
@@ -389,6 +397,7 @@ function AppInner() {
389397
projects,
390398
workspaceMetadata,
391399
selectedWorkspace,
400+
theme,
392401
getThinkingLevel: getThinkingLevelForWorkspace,
393402
onSetThinkingLevel: setThinkingLevelFromPalette,
394403
onStartWorkspaceCreation: openNewWorkspaceFromPalette,
@@ -401,6 +410,8 @@ function AppInner() {
401410
onToggleSidebar: toggleSidebarFromPalette,
402411
onNavigateWorkspace: navigateWorkspaceFromPalette,
403412
onOpenWorkspaceInTerminal: openWorkspaceInTerminal,
413+
onToggleTheme: toggleTheme,
414+
onSetTheme: setThemePreference,
404415
};
405416

406417
useEffect(() => {
@@ -587,7 +598,7 @@ function AppInner() {
587598
})()
588599
) : (
589600
<div
590-
className="[&_p]:text-muted mx-auto w-full max-w-3xl text-center [&_h2]:mb-4 [&_h2]:font-bold [&_h2]:tracking-tight [&_h2]:text-white [&_p]:leading-[1.6]"
601+
className="[&_p]:text-muted mx-auto w-full max-w-3xl text-center [&_h2]:mb-4 [&_h2]:font-bold [&_h2]:tracking-tight [&_h2]:text-foreground [&_p]:leading-[1.6]"
591602
style={{
592603
padding: "clamp(40px, 10vh, 100px) 20px",
593604
fontSize: "clamp(14px, 2vw, 16px)",
@@ -619,9 +630,11 @@ function AppInner() {
619630

620631
function App() {
621632
return (
622-
<CommandRegistryProvider>
623-
<AppInner />
624-
</CommandRegistryProvider>
633+
<ThemeProvider>
634+
<CommandRegistryProvider>
635+
<AppInner />
636+
</CommandRegistryProvider>
637+
</ThemeProvider>
625638
);
626639
}
627640

src/browser/components/Messages/MessageWindow.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,15 +66,15 @@ export const MessageWindow: React.FC<MessageWindowProps> = ({
6666
className={cn(
6767
"mt-4 mb-1 flex w-full flex-col relative isolate w-fit",
6868
variant === "user" && "ml-auto",
69-
variant === "assistant" && "text-white",
69+
variant === "assistant" && "text-foreground",
7070
isLastPartOfMessage && "mb-4"
7171
)}
7272
data-message-block
7373
>
7474
<div
7575
className={cn(
7676
variant === "user" &&
77-
"bg-neutral-700/50 border border-user-border/20 rounded-lg px-3 py-2 overflow-hidden",
77+
"bg-[var(--color-user-surface)] border border-[var(--color-user-border)] rounded-lg px-3 py-2 overflow-hidden shadow-sm",
7878
variant === "assistant" && "px-1 py-1"
7979
)}
8080
>
@@ -95,7 +95,7 @@ export const MessageWindow: React.FC<MessageWindowProps> = ({
9595
<div
9696
className={cn(
9797
"mt-2 flex flex-wrap items-center justify-between gap-3 text-[11px]",
98-
variant === "user" ? "ml-auto text-white/60" : "text-white/60"
98+
variant === "user" ? "ml-auto text-muted" : "text-muted"
9999
)}
100100
data-message-meta
101101
>

src/browser/components/Modal.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export const ModalContent: React.FC<
2929
<div
3030
className={cn(
3131
"bg-dark rounded-lg p-6 w-[90%] flex flex-col shadow-lg border border-border",
32-
"[&_h2]:mt-0 [&_h2]:mb-2 [&_h2]:text-white",
32+
"[&_h2]:mt-0 [&_h2]:mb-2 [&_h2]:text-foreground",
3333
maxHeight && "overflow-y-auto",
3434
className
3535
)}

src/browser/components/ModelSelector.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -236,8 +236,8 @@ export const ModelSelector = forwardRef<ModelSelectorRef, ModelSelectorProps>(
236236
"text-[11px] font-monospace py-1.5 px-2.5 cursor-pointer transition-colors duration-100",
237237
"first:rounded-t last:rounded-b",
238238
index === highlightedIndex
239-
? "text-white bg-hover"
240-
: "text-light bg-transparent hover:bg-hover hover:text-white"
239+
? "text-foreground bg-hover"
240+
: "text-light bg-transparent hover:bg-hover hover:text-foreground"
241241
)}
242242
onClick={() => handleSelectModel(model)}
243243
>

src/browser/components/ProjectCreateModal.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,14 +142,14 @@ export const ProjectCreateModal: React.FC<ProjectCreateModalProps> = ({
142142
placeholder="/home/user/projects/my-project"
143143
autoFocus
144144
disabled={isCreating}
145-
className="bg-modal-bg border-border-medium focus:border-accent placeholder:text-muted w-full flex-1 rounded border px-3 py-2 font-mono text-sm text-white focus:outline-none disabled:opacity-50"
145+
className="bg-modal-bg border-border-medium focus:border-accent placeholder:text-muted w-full flex-1 rounded border px-3 py-2 font-mono text-sm text-foreground focus:outline-none disabled:opacity-50"
146146
/>
147147
{(isDesktop || hasWebFsPicker) && (
148148
<button
149149
type="button"
150150
onClick={handleBrowseClick}
151151
disabled={isCreating}
152-
className="bg-border-medium hover:bg-border-darker border-border-medium rounded border px-4 text-sm font-medium text-white transition-colors disabled:opacity-50"
152+
className="bg-border-medium hover:bg-border-darker border-border-medium rounded border px-4 text-sm font-medium text-foreground transition-colors disabled:opacity-50"
153153
>
154154
Browse...
155155
</button>
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { MoonStar, SunMedium } from "lucide-react";
2+
import { useTheme } from "@/browser/contexts/ThemeContext";
3+
import { TooltipWrapper, Tooltip } from "./Tooltip";
4+
5+
export function ThemeToggleButton() {
6+
const { theme, toggleTheme } = useTheme();
7+
const label = theme === "light" ? "Switch to dark theme" : "Switch to light theme";
8+
const Icon = theme === "light" ? MoonStar : SunMedium;
9+
10+
return (
11+
<TooltipWrapper>
12+
<button
13+
type="button"
14+
onClick={toggleTheme}
15+
className="border-border-light text-muted-foreground hover:border-border-medium/80 hover:bg-toggle-bg/70 focus-visible:ring-1 focus-visible:ring-border-medium flex h-7 w-7 items-center justify-center rounded-md border bg-transparent transition-colors duration-150"
16+
aria-label={label}
17+
data-testid="theme-toggle"
18+
>
19+
<Icon className="h-4 w-4" aria-hidden />
20+
</button>
21+
<Tooltip align="right">{label}</Tooltip>
22+
</TooltipWrapper>
23+
);
24+
}

0 commit comments

Comments
 (0)