Skip to content

Commit 5bf8e13

Browse files
DLhuglyclaude
andcommitted
feat: add 5 new themes and 3 font dropdowns
Add Cyberpunk, Ember, Forest, Solarized Dark, and Monokai themes with full support across editor, terminal, and UI layers. Add independent font selection dropdowns for editor, terminal, and UI with Google Font loading. Extract duplicated terminal themes into shared module. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 8923810 commit 5bf8e13

File tree

12 files changed

+889
-194
lines changed

12 files changed

+889
-194
lines changed

clif-pad-ide/src/App.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { registerKeybinding, initKeybindings } from "./lib/keybindings";
99
import { saveActiveFile, projectRoot, openProject } from "./stores/fileStore";
1010
import { initGit } from "./stores/gitStore";
1111
import { configureMonaco } from "./lib/monaco-setup";
12+
import { loadGoogleFont, applyUiFont } from "./lib/fonts";
1213
import type { TerminalPanelRef } from "./components/terminal/TerminalPanel";
1314

1415
const TerminalPanel = lazy(() => import("./components/terminal/TerminalPanel"));
@@ -109,6 +110,12 @@ const App: Component = () => {
109110
applyTheme(s.theme);
110111
setUiFontSize(s.fontSize);
111112

113+
// Load and apply saved fonts
114+
loadGoogleFont(s.editorFont);
115+
loadGoogleFont(s.terminalFont);
116+
loadGoogleFont(s.uiFont);
117+
applyUiFont(s.uiFont);
118+
112119
registerKeybinding("s", ["ctrl"], saveActiveFile, "Save file");
113120
registerKeybinding("`", ["ctrl"], toggleTerminal, "Toggle terminal");
114121
registerKeybinding("b", ["ctrl"], toggleSidebar, "Toggle sidebar");

clif-pad-ide/src/components/editor/DiffView.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Component, onMount, onCleanup, createEffect } from "solid-js";
22
import * as monaco from "monaco-editor";
33
import { theme, fontSize } from "../../stores/uiStore";
4+
import { settings } from "../../stores/settingsStore";
45
import { monacoThemes } from "../../lib/themes";
56
import type { Theme } from "../../stores/uiStore";
67

@@ -81,6 +82,14 @@ const DiffView: Component<DiffViewProps> = (props) => {
8182
}
8283
});
8384

85+
// Watch editor font changes
86+
createEffect(() => {
87+
const font = settings().editorFont;
88+
if (diffEditor) {
89+
diffEditor.updateOptions({ fontFamily: `${font}, Menlo, Monaco, 'Courier New', monospace` });
90+
}
91+
});
92+
8493
onCleanup(() => {
8594
if (originalModel && !originalModel.isDisposed()) {
8695
originalModel.dispose();

clif-pad-ide/src/components/editor/MonacoEditor.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Component, onMount, onCleanup, createEffect } from "solid-js";
22
import * as monaco from "monaco-editor";
33
import { activeFile, updateFileContent, saveActiveFile } from "../../stores/fileStore";
44
import { theme, fontSize } from "../../stores/uiStore";
5+
import { settings } from "../../stores/settingsStore";
56
import { monacoThemes } from "../../lib/themes";
67
import { registerGhostTextProvider } from "./GhostText";
78
import type { Theme } from "../../stores/uiStore";
@@ -137,6 +138,14 @@ const MonacoEditor: Component = () => {
137138
}
138139
});
139140

141+
// Watch editor font changes
142+
createEffect(() => {
143+
const font = settings().editorFont;
144+
if (editorInstance) {
145+
editorInstance.updateOptions({ fontFamily: `${font}, Menlo, Monaco, 'Courier New', monospace` });
146+
}
147+
});
148+
140149
onCleanup(() => {
141150
if (currentPath && editorInstance) {
142151
const state = editorInstance.saveViewState();

clif-pad-ide/src/components/layout/DevPreviewPanel.tsx

Lines changed: 13 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -3,69 +3,12 @@ import { Terminal } from "@xterm/xterm";
33
import { FitAddon } from "@xterm/addon-fit";
44
import { ptySpawn, ptyWrite, ptyResize, ptyKill, onPtyOutput, onPtyExit } from "../../lib/tauri";
55
import { theme, fontSize, devDrawerOpen, setDevDrawerOpen } from "../../stores/uiStore";
6+
import { settings } from "../../stores/settingsStore";
67
import { projectRoot } from "../../stores/fileStore";
7-
import type { Theme } from "../../stores/uiStore";
8+
import { terminalThemes } from "../../lib/terminalThemes";
89
import type { UnlistenFn } from "@tauri-apps/api/event";
910
import "@xterm/xterm/css/xterm.css";
1011

11-
const terminalThemes: Record<Theme, Record<string, string>> = {
12-
midnight: {
13-
background: "#0d1117",
14-
foreground: "#e6edf3",
15-
cursor: "#3b82f6",
16-
cursorAccent: "#0d1117",
17-
selectionBackground: "rgba(59, 130, 246, 0.3)",
18-
black: "#484f58", red: "#ff7b72", green: "#3fb950", yellow: "#d29922",
19-
blue: "#58a6ff", magenta: "#bc8cff", cyan: "#39c5cf", white: "#b1bac4",
20-
brightBlack: "#6e7681", brightRed: "#ffa198", brightGreen: "#56d364", brightYellow: "#e3b341",
21-
brightBlue: "#79c0ff", brightMagenta: "#d2a8ff", brightCyan: "#56d4dd", brightWhite: "#f0f6fc",
22-
},
23-
graphite: {
24-
background: "#1c1c1e",
25-
foreground: "#f5f5f7",
26-
cursor: "#f0883e",
27-
cursorAccent: "#1c1c1e",
28-
selectionBackground: "rgba(240, 136, 62, 0.3)",
29-
black: "#48484a", red: "#ff453a", green: "#30d158", yellow: "#ffd60a",
30-
blue: "#64d2ff", magenta: "#bf5af2", cyan: "#5ac8fa", white: "#d1d1d6",
31-
brightBlack: "#8e8e93", brightRed: "#ff6961", brightGreen: "#4cd964", brightYellow: "#ffe066",
32-
brightBlue: "#70d7ff", brightMagenta: "#da8fff", brightCyan: "#70d7ff", brightWhite: "#f5f5f7",
33-
},
34-
dawn: {
35-
background: "#ffffff",
36-
foreground: "#1f2328",
37-
cursor: "#0066cc",
38-
cursorAccent: "#ffffff",
39-
selectionBackground: "rgba(0, 102, 204, 0.2)",
40-
black: "#24292f", red: "#cf222e", green: "#1a7f37", yellow: "#9a6700",
41-
blue: "#0969da", magenta: "#8250df", cyan: "#1b7c83", white: "#6e7781",
42-
brightBlack: "#57606a", brightRed: "#a40e26", brightGreen: "#2da44e", brightYellow: "#bf8700",
43-
brightBlue: "#218bff", brightMagenta: "#a475f9", brightCyan: "#3192aa", brightWhite: "#8c959f",
44-
},
45-
arctic: {
46-
background: "#f0f4f8",
47-
foreground: "#0f172a",
48-
cursor: "#0284c7",
49-
cursorAccent: "#f0f4f8",
50-
selectionBackground: "rgba(2, 132, 199, 0.2)",
51-
black: "#334155", red: "#dc2626", green: "#059669", yellow: "#d97706",
52-
blue: "#0284c7", magenta: "#7c3aed", cyan: "#0891b2", white: "#64748b",
53-
brightBlack: "#475569", brightRed: "#ef4444", brightGreen: "#10b981", brightYellow: "#f59e0b",
54-
brightBlue: "#0ea5e9", brightMagenta: "#8b5cf6", brightCyan: "#06b6d4", brightWhite: "#94a3b8",
55-
},
56-
dusk: {
57-
background: "#1a1625",
58-
foreground: "#ede9fe",
59-
cursor: "#a855f7",
60-
cursorAccent: "#1a1625",
61-
selectionBackground: "rgba(168, 85, 247, 0.3)",
62-
black: "#372f48", red: "#fb7185", green: "#34d399", yellow: "#fbbf24",
63-
blue: "#818cf8", magenta: "#c084fc", cyan: "#67e8f9", white: "#a78bfa",
64-
brightBlack: "#7c6ba0", brightRed: "#fda4af", brightGreen: "#6ee7b7", brightYellow: "#fde68a",
65-
brightBlue: "#a5b4fc", brightMagenta: "#d8b4fe", brightCyan: "#a5f3fc", brightWhite: "#ede9fe",
66-
},
67-
};
68-
6912
const PRESET_COMMANDS = [
7013
{ label: "npm run dev", cmd: "npm run dev" },
7114
{ label: "npm start", cmd: "npm start" },
@@ -186,9 +129,10 @@ const DevPreviewPanel: Component = () => {
186129
terminalMounted = true;
187130

188131
const t = theme();
132+
const font = settings().terminalFont;
189133
terminal = new Terminal({
190134
fontSize: Math.max(11, fontSize() - 1),
191-
fontFamily: "JetBrains Mono, Menlo, Monaco, Courier New, monospace",
135+
fontFamily: `${font}, Menlo, Monaco, Courier New, monospace`,
192136
cursorBlink: true,
193137
cursorStyle: "bar",
194138
allowProposedApi: true,
@@ -270,6 +214,15 @@ const DevPreviewPanel: Component = () => {
270214
}
271215
});
272216

217+
// Watch terminal font changes
218+
createEffect(() => {
219+
const font = settings().terminalFont;
220+
if (terminal) {
221+
terminal.options.fontFamily = `${font}, Menlo, Monaco, Courier New, monospace`;
222+
fitAddon?.fit();
223+
}
224+
});
225+
273226
onCleanup(async () => {
274227
alive = false;
275228
resizeObserver?.disconnect();

clif-pad-ide/src/components/layout/TopBar.tsx

Lines changed: 160 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { Component, Show, For, createSignal, onCleanup } from "solid-js";
22
import { theme, applyTheme, fontSize, setUiFontSize, THEMES } from "../../stores/uiStore";
33
import type { Theme } from "../../stores/uiStore";
4-
import { updateSettings } from "../../stores/settingsStore";
4+
import { settings, updateSettings } from "../../stores/settingsStore";
55
import { projectRoot } from "../../stores/fileStore";
6+
import { MONO_FONTS, SANS_FONTS, loadGoogleFont, applyUiFont } from "../../lib/fonts";
67

78
const FolderIcon = () => (
89
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
@@ -36,6 +37,121 @@ const ClifCodeIcon = () => (
3637
</svg>
3738
);
3839

40+
const CheckIcon = () => (
41+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="var(--accent-primary)" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
42+
<polyline points="20 6 9 17 4 12" />
43+
</svg>
44+
);
45+
46+
interface FontDropdownProps {
47+
label: string;
48+
value: string;
49+
options: { label: string; value: string }[];
50+
onChange: (value: string) => void;
51+
}
52+
53+
const FontDropdown: Component<FontDropdownProps> = (props) => {
54+
const [open, setOpen] = createSignal(false);
55+
let ref: HTMLDivElement | undefined;
56+
57+
const handleClickOutside = (e: MouseEvent) => {
58+
if (ref && !ref.contains(e.target as Node)) {
59+
setOpen(false);
60+
}
61+
};
62+
63+
const toggle = () => {
64+
const next = !open();
65+
setOpen(next);
66+
if (next) {
67+
setTimeout(() => document.addEventListener("click", handleClickOutside), 0);
68+
} else {
69+
document.removeEventListener("click", handleClickOutside);
70+
}
71+
};
72+
73+
onCleanup(() => {
74+
document.removeEventListener("click", handleClickOutside);
75+
});
76+
77+
return (
78+
<div ref={ref} style={{ position: "relative" }}>
79+
<button
80+
class="flex items-center gap-1.5 rounded-lg px-2 py-1.5 text-xs transition-all duration-150"
81+
style={{
82+
color: "var(--text-secondary)",
83+
background: open() ? "var(--bg-hover)" : "transparent",
84+
border: "none",
85+
cursor: "pointer",
86+
"white-space": "nowrap",
87+
}}
88+
onMouseEnter={(e) => {
89+
if (!open()) (e.currentTarget as HTMLElement).style.background = "var(--bg-hover)";
90+
}}
91+
onMouseLeave={(e) => {
92+
if (!open()) (e.currentTarget as HTMLElement).style.background = "transparent";
93+
}}
94+
onClick={toggle}
95+
title={props.label}
96+
>
97+
<span style={{ color: "var(--text-muted)", "font-size": "10px" }}>{props.label}</span>
98+
<span style={{ "max-width": "90px", overflow: "hidden", "text-overflow": "ellipsis" }}>{props.value}</span>
99+
<ChevronIcon />
100+
</button>
101+
102+
<Show when={open()}>
103+
<div
104+
style={{
105+
position: "absolute",
106+
top: "calc(100% + 4px)",
107+
right: "0",
108+
background: "var(--bg-overlay)",
109+
border: "1px solid var(--border-default)",
110+
"border-radius": "var(--radius-lg)",
111+
"box-shadow": "var(--shadow-lg)",
112+
padding: "4px",
113+
"min-width": "170px",
114+
"z-index": "200",
115+
"backdrop-filter": "blur(20px)",
116+
"-webkit-backdrop-filter": "blur(20px)",
117+
}}
118+
>
119+
<For each={props.options}>
120+
{(opt) => (
121+
<button
122+
class="flex items-center gap-3 w-full rounded-md px-3 py-2 text-xs transition-colors duration-100"
123+
style={{
124+
color: props.value === opt.value ? "var(--text-primary)" : "var(--text-secondary)",
125+
background: props.value === opt.value ? "var(--bg-active)" : "transparent",
126+
border: "none",
127+
cursor: "pointer",
128+
"text-align": "left",
129+
}}
130+
onMouseEnter={(e) => {
131+
if (props.value !== opt.value) (e.currentTarget as HTMLElement).style.background = "var(--bg-hover)";
132+
}}
133+
onMouseLeave={(e) => {
134+
if (props.value !== opt.value) (e.currentTarget as HTMLElement).style.background = "transparent";
135+
}}
136+
onClick={() => {
137+
props.onChange(opt.value);
138+
setOpen(false);
139+
document.removeEventListener("click", handleClickOutside);
140+
}}
141+
>
142+
<span class="flex-1">{opt.label}</span>
143+
<Show when={props.value === opt.value}>
144+
<CheckIcon />
145+
</Show>
146+
</button>
147+
)}
148+
</For>
149+
</div>
150+
</Show>
151+
</div>
152+
);
153+
};
154+
39155
const TopBar: Component<{
40156
onLaunchClaude: () => void;
41157
onLaunchClifCode: () => void;
@@ -79,6 +195,22 @@ const TopBar: Component<{
79195
updateSettings({ fontSize: size });
80196
};
81197

198+
const handleEditorFontChange = (font: string) => {
199+
loadGoogleFont(font);
200+
updateSettings({ editorFont: font });
201+
};
202+
203+
const handleTerminalFontChange = (font: string) => {
204+
loadGoogleFont(font);
205+
updateSettings({ terminalFont: font });
206+
};
207+
208+
const handleUiFontChange = (font: string) => {
209+
loadGoogleFont(font);
210+
applyUiFont(font);
211+
updateSettings({ uiFont: font });
212+
};
213+
82214
const themeKeys = Object.keys(THEMES) as Theme[];
83215

84216
return (
@@ -164,6 +296,29 @@ const TopBar: Component<{
164296
{/* Divider */}
165297
<div style={{ width: "1px", height: "20px", background: "var(--border-default)", opacity: "0.5" }} />
166298

299+
{/* Font dropdowns */}
300+
<FontDropdown
301+
label="Editor"
302+
value={settings().editorFont}
303+
options={MONO_FONTS}
304+
onChange={handleEditorFontChange}
305+
/>
306+
<FontDropdown
307+
label="Terminal"
308+
value={settings().terminalFont}
309+
options={MONO_FONTS}
310+
onChange={handleTerminalFontChange}
311+
/>
312+
<FontDropdown
313+
label="UI"
314+
value={settings().uiFont}
315+
options={SANS_FONTS}
316+
onChange={handleUiFontChange}
317+
/>
318+
319+
{/* Divider */}
320+
<div style={{ width: "1px", height: "20px", background: "var(--border-default)", opacity: "0.5" }} />
321+
167322
{/* Theme dropdown */}
168323
<div ref={dropdownRef} style={{ position: "relative" }}>
169324
<button
@@ -212,7 +367,9 @@ const TopBar: Component<{
212367
"border-radius": "var(--radius-lg)",
213368
"box-shadow": "var(--shadow-lg)",
214369
padding: "4px",
215-
"min-width": "160px",
370+
"min-width": "170px",
371+
"max-height": "320px",
372+
"overflow-y": "auto",
216373
"z-index": "200",
217374
"backdrop-filter": "blur(20px)",
218375
"-webkit-backdrop-filter": "blur(20px)",
@@ -257,9 +414,7 @@ const TopBar: Component<{
257414
<span class="flex-1">{THEMES[t].label}</span>
258415
{/* Checkmark for active */}
259416
<Show when={theme() === t}>
260-
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="var(--accent-primary)" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
261-
<polyline points="20 6 9 17 4 12" />
262-
</svg>
417+
<CheckIcon />
263418
</Show>
264419
</button>
265420
)}

0 commit comments

Comments
 (0)