Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,300..800;1,9..40,300..800&display=swap"
href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,300..800;1,9..40,300..800&family=Inter:wght@300..800&family=Plus+Jakarta+Sans:wght@300..800&display=swap"
rel="stylesheet"
/>
<title>OK Code</title>
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/MarkdownPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ export const MarkdownPreview = memo(function MarkdownPreview({ contents }: Markd
"--cm-callout-bg": "var(--secondary)",
"--cm-radius": "12px",
"--cm-font":
'"DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif',
'var(--font-ui, "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif)',
"--cm-mono":
'"SF Mono", "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace',
} as CSSProperties
Expand Down
74 changes: 67 additions & 7 deletions apps/web/src/hooks/useTheme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,39 @@ type ColorTheme =
| "default"
| "iridescent-void"
| "solar-witch"
| "cursor-dark"
| "midnight-clarity"
| "carbon"
| "vapor"
| "cathedral-circuit";

type FontFamily = "dm-sans" | "inter" | "plus-jakarta-sans";

type ThemeSnapshot = {
theme: Theme;
systemDark: boolean;
colorTheme: ColorTheme;
fontFamily: FontFamily;
};

export const COLOR_THEMES: { id: ColorTheme; label: string }[] = [
{ id: "default", label: "Default" },
{ id: "iridescent-void", label: "Iridescent Void" },
{ id: "solar-witch", label: "Solar Witch" },
{ id: "cursor-dark", label: "Cursor Dark" },
{ id: "midnight-clarity", label: "Midnight Clarity" },
{ id: "carbon", label: "Carbon" },
{ id: "vapor", label: "Vapor" },
{ id: "cathedral-circuit", label: "Cathedral Circuit" },
];

export const FONT_FAMILIES: { id: FontFamily; label: string }[] = [
{ id: "inter", label: "Inter" },
{ id: "dm-sans", label: "DM Sans" },
{ id: "plus-jakarta-sans", label: "Plus Jakarta Sans" },
];

const STORAGE_KEY = "okcode:theme";
const COLOR_THEME_STORAGE_KEY = "okcode:color-theme";
const FONT_FAMILY_STORAGE_KEY = "okcode:font-family";
const MEDIA_QUERY = "(prefers-color-scheme: dark)";

let listeners: Array<() => void> = [];
Expand All @@ -49,14 +63,36 @@ function getStoredColorTheme(): ColorTheme {
raw === "default" ||
raw === "iridescent-void" ||
raw === "solar-witch" ||
raw === "cursor-dark" ||
raw === "midnight-clarity" ||
raw === "carbon" ||
raw === "vapor" ||
raw === "cathedral-circuit"
) {
return raw;
}
return "default";
}

function getStoredFontFamily(): FontFamily {
const raw = localStorage.getItem(FONT_FAMILY_STORAGE_KEY);
if (raw === "dm-sans" || raw === "inter" || raw === "plus-jakarta-sans") {
return raw;
}
return "inter";
}

const FONT_FAMILY_MAP: Record<FontFamily, string> = {
inter: '"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif',
"dm-sans": '"DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif',
"plus-jakarta-sans":
'"Plus Jakarta Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif',
};

function applyFont(fontFamily?: FontFamily) {
const font = fontFamily ?? getStoredFontFamily();
document.documentElement.style.setProperty("--font-ui", FONT_FAMILY_MAP[font]);
}

function applyTheme(theme: Theme, suppressTransitions = false) {
if (suppressTransitions) {
document.documentElement.classList.add("no-transitions");
Expand All @@ -78,6 +114,9 @@ function applyTheme(theme: Theme, suppressTransitions = false) {
document.documentElement.classList.add(`theme-${colorTheme}`);
}

// Apply font family
applyFont();

syncDesktopTheme(theme);
if (suppressTransitions) {
// Force a reflow so the no-transitions class takes effect before removal
Expand Down Expand Up @@ -110,17 +149,19 @@ function getSnapshot(): ThemeSnapshot {
const theme = getStored();
const systemDark = theme === "system" ? getSystemDark() : false;
const colorTheme = getStoredColorTheme();
const fontFamily = getStoredFontFamily();

if (
lastSnapshot &&
lastSnapshot.theme === theme &&
lastSnapshot.systemDark === systemDark &&
lastSnapshot.colorTheme === colorTheme
lastSnapshot.colorTheme === colorTheme &&
lastSnapshot.fontFamily === fontFamily
) {
return lastSnapshot;
}

lastSnapshot = { theme, systemDark, colorTheme };
lastSnapshot = { theme, systemDark, colorTheme, fontFamily };
return lastSnapshot;
}

Expand All @@ -137,7 +178,11 @@ function subscribe(listener: () => void): () => void {

// Listen for storage changes from other tabs
const handleStorage = (e: StorageEvent) => {
if (e.key === STORAGE_KEY || e.key === COLOR_THEME_STORAGE_KEY) {
if (
e.key === STORAGE_KEY ||
e.key === COLOR_THEME_STORAGE_KEY ||
e.key === FONT_FAMILY_STORAGE_KEY
) {
applyTheme(getStored(), true);
emitChange();
}
Expand All @@ -155,6 +200,7 @@ export function useTheme() {
const snapshot = useSyncExternalStore(subscribe, getSnapshot);
const theme = snapshot.theme;
const colorTheme = snapshot.colorTheme;
const fontFamily = snapshot.fontFamily;

const resolvedTheme: "light" | "dark" =
theme === "system" ? (snapshot.systemDark ? "dark" : "light") : theme;
Expand All @@ -171,10 +217,24 @@ export function useTheme() {
emitChange();
}, []);

const setFontFamily = useCallback((next: FontFamily) => {
localStorage.setItem(FONT_FAMILY_STORAGE_KEY, next);
applyFont(next);
emitChange();
}, []);

// Keep DOM in sync on mount/change
useEffect(() => {
applyTheme(theme);
}, [theme]);

return { theme, setTheme, resolvedTheme, colorTheme, setColorTheme } as const;
return {
theme,
setTheme,
resolvedTheme,
colorTheme,
setColorTheme,
fontFamily,
setFontFamily,
} as const;
}
8 changes: 5 additions & 3 deletions apps/web/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -122,13 +122,15 @@
}

body {
font-family:
"DM Sans",
font-family: var(
--font-ui,
"Inter",
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
system-ui,
sans-serif;
sans-serif
);
margin: 0;
padding: 0;
min-height: 100vh;
Expand Down
39 changes: 37 additions & 2 deletions apps/web/src/routes/_chat.settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import { SidebarInset } from "../components/ui/sidebar";
import { Tooltip, TooltipPopup, TooltipTrigger } from "../components/ui/tooltip";
import { resolveAndPersistPreferredEditor } from "../editorPreferences";
import { isElectron } from "../env";
import { useTheme, COLOR_THEMES } from "../hooks/useTheme";
import { useTheme, COLOR_THEMES, FONT_FAMILIES } from "../hooks/useTheme";
import {
environmentVariablesQueryKeys,
globalEnvironmentVariablesQueryOptions,
Expand Down Expand Up @@ -209,7 +209,7 @@ function getErrorMessage(error: unknown): string {
}

function SettingsRouteView() {
const { theme, setTheme, colorTheme, setColorTheme } = useTheme();
const { theme, setTheme, colorTheme, setColorTheme, fontFamily, setFontFamily } = useTheme();
const { settings, defaults, updateSettings, resetSettings } = useAppSettings();
const serverConfigQuery = useQuery(serverConfigQueryOptions());
const queryClient = useQueryClient();
Expand Down Expand Up @@ -300,6 +300,7 @@ function SettingsRouteView() {
const changedSettingLabels = [
...(theme !== "system" ? ["Theme"] : []),
...(colorTheme !== "default" ? ["Color theme"] : []),
...(fontFamily !== "inter" ? ["Font"] : []),
...(settings.timestampFormat !== defaults.timestampFormat ? ["Time format"] : []),
...(settings.diffWordWrap !== defaults.diffWordWrap ? ["Diff line wrapping"] : []),
...(settings.enableAssistantStreaming !== defaults.enableAssistantStreaming
Expand Down Expand Up @@ -445,6 +446,7 @@ function SettingsRouteView() {

setTheme("system");
setColorTheme("default");
setFontFamily("inter");
resetSettings();
setOpenInstallProviders({
codex: false,
Expand Down Expand Up @@ -571,6 +573,39 @@ function SettingsRouteView() {
}
/>

<SettingsRow
title="Font"
description="Choose the typeface for the interface."
resetAction={
fontFamily !== "inter" ? (
<SettingResetButton label="font" onClick={() => setFontFamily("inter")} />
) : null
}
control={
<Select
value={fontFamily}
onValueChange={(value) => {
const match = FONT_FAMILIES.find((f) => f.id === value);
if (!match) return;
setFontFamily(match.id);
}}
>
<SelectTrigger className="w-full sm:w-40" aria-label="Font family">
<SelectValue>
{FONT_FAMILIES.find((f) => f.id === fontFamily)?.label ?? "Inter"}
</SelectValue>
</SelectTrigger>
<SelectPopup align="end" alignItemWithTrigger={false}>
{FONT_FAMILIES.map((f) => (
<SelectItem hideIndicator key={f.id} value={f.id}>
{f.label}
</SelectItem>
))}
</SelectPopup>
</Select>
}
/>

<SettingsRow
title="Window opacity"
description="Adjust the transparency of the entire application window."
Expand Down
Loading
Loading