Skip to content

Commit 917f789

Browse files
committed
feat: add light theme with storybook toggle
1 parent cb9bdaf commit 917f789

File tree

18 files changed

+460
-25
lines changed

18 files changed

+460
-25
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
@@ -30,6 +30,7 @@
3030
"ghostty-web": "^0.1.1",
3131
"jsonc-parser": "^3.3.1",
3232
"lru-cache": "^11.2.2",
33+
"lucide-react": "^0.553.0",
3334
"markdown-it": "^14.1.0",
3435
"minimist": "^1.2.8",
3536
"motion": "^12.23.24",
@@ -2203,7 +2204,7 @@
22032204

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

2206-
"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=="],
2207+
"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=="],
22072208

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

@@ -3551,6 +3552,8 @@
35513552

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

3555+
"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=="],
3556+
35543557
"string-length/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
35553558

35563559
"tar-fs/chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="],

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: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@
4646
"dependencies": {
4747
"@ai-sdk/anthropic": "^2.0.44",
4848
"@ai-sdk/openai": "^2.0.66",
49+
"@homebridge/node-pty-prebuilt-multiarch": "^0.11.14",
4950
"@openrouter/ai-sdk-provider": "^1.2.2",
50-
"ghostty-web": "^0.1.1",
5151
"@radix-ui/react-dialog": "^1.1.15",
5252
"@radix-ui/react-dropdown-menu": "^2.1.16",
5353
"@radix-ui/react-scroll-area": "^1.2.10",
@@ -66,13 +66,14 @@
6666
"disposablestack": "^1.1.7",
6767
"electron-updater": "^6.6.2",
6868
"express": "^5.1.0",
69+
"ghostty-web": "^0.1.1",
6970
"jsonc-parser": "^3.3.1",
7071
"lru-cache": "^11.2.2",
72+
"lucide-react": "^0.553.0",
7173
"markdown-it": "^14.1.0",
7274
"minimist": "^1.2.8",
7375
"motion": "^12.23.24",
7476
"ollama-ai-provider-v2": "^1.5.4",
75-
"@homebridge/node-pty-prebuilt-multiarch": "^0.11.14",
7677
"rehype-harden": "^1.1.5",
7778
"shescape": "^2.1.6",
7879
"source-map-support": "^0.5.21",

src/browser/App.tsx

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { useStableReference, compareMaps } from "./hooks/useStableReference";
2121
import { CommandRegistryProvider, useCommandRegistry } from "./contexts/CommandRegistryContext";
2222
import type { CommandAction } from "./contexts/CommandRegistryContext";
2323
import { ModeProvider } from "./contexts/ModeContext";
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,
@@ -392,6 +400,7 @@ function AppInner() {
392400
projects,
393401
workspaceMetadata,
394402
selectedWorkspace,
403+
theme,
395404
getThinkingLevel: getThinkingLevelForWorkspace,
396405
onSetThinkingLevel: setThinkingLevelFromPalette,
397406
onStartWorkspaceCreation: openNewWorkspaceFromPalette,
@@ -404,6 +413,8 @@ function AppInner() {
404413
onToggleSidebar: toggleSidebarFromPalette,
405414
onNavigateWorkspace: navigateWorkspaceFromPalette,
406415
onOpenWorkspaceInTerminal: openWorkspaceInTerminal,
416+
onToggleTheme: toggleTheme,
417+
onSetTheme: setThemePreference,
407418
};
408419

409420
useEffect(() => {
@@ -586,7 +597,7 @@ function AppInner() {
586597
})()
587598
) : (
588599
<div
589-
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]"
600+
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]"
590601
style={{
591602
padding: "clamp(40px, 10vh, 100px) 20px",
592603
fontSize: "clamp(14px, 2vw, 16px)",
@@ -618,9 +629,11 @@ function AppInner() {
618629

619630
function App() {
620631
return (
621-
<CommandRegistryProvider>
622-
<AppInner />
623-
</CommandRegistryProvider>
632+
<ThemeProvider>
633+
<CommandRegistryProvider>
634+
<AppInner />
635+
</CommandRegistryProvider>
636+
</ThemeProvider>
624637
);
625638
}
626639

src/browser/components/Context1MCheckbox.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export const Context1MCheckbox: React.FC<Context1MCheckboxProps> = ({ modelStrin
1717

1818
return (
1919
<div className="flex items-center gap-1.5">
20-
<label className="text-foreground flex cursor-pointer items-center gap-1 truncate text-[10px] select-none hover:text-white">
20+
<label className="text-foreground flex cursor-pointer items-center gap-1 truncate text-[10px] select-none hover:text-foreground">
2121
<input type="checkbox" checked={use1M} onChange={(e) => setUse1M(e.target.checked)} />
2222
1M
2323
</label>

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: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ export const ProjectCreateModal: React.FC<ProjectCreateModalProps> = ({
107107
placeholder="/home/user/projects/my-project"
108108
autoFocus
109109
disabled={isCreating}
110-
className="bg-modal-bg border-border-medium focus:border-accent placeholder:text-muted mb-5 w-full rounded border px-3 py-2 font-mono text-sm text-white focus:outline-none disabled:opacity-50"
110+
className="bg-modal-bg border-border-medium focus:border-accent placeholder:text-muted mb-5 w-full rounded border px-3 py-2 font-mono text-sm text-foreground focus:outline-none disabled:opacity-50"
111111
/>
112112
{error && <div className="text-error -mt-3 mb-3 text-xs">{error}</div>}
113113
<ModalActions>

0 commit comments

Comments
 (0)