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
3 changes: 2 additions & 1 deletion src/browser/components/Messages/MarkdownComponents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ const CodeBlock: React.FC<CodeBlockProps> = ({ code, language }) => {

useEffect(() => {
let cancelled = false;
const shikiTheme = themeMode === "light" ? SHIKI_LIGHT_THEME : SHIKI_DARK_THEME;
const isLight = themeMode === "light" || themeMode === "solarized-light";
const shikiTheme = isLight ? SHIKI_LIGHT_THEME : SHIKI_DARK_THEME;

setHighlightedLines(null);

Expand Down
33 changes: 13 additions & 20 deletions src/browser/components/Settings/sections/GeneralSection.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import React from "react";
import { MoonStar, SunMedium } from "lucide-react";
import { useTheme } from "@/browser/contexts/ThemeContext";
import { useTheme, THEME_OPTIONS, type ThemeMode } from "@/browser/contexts/ThemeContext";

export function GeneralSection() {
const { theme, toggleTheme } = useTheme();
const { theme, setTheme } = useTheme();

return (
<div className="space-y-6">
Expand All @@ -12,25 +11,19 @@ export function GeneralSection() {
<div className="flex items-center justify-between">
<div>
<div className="text-foreground text-sm">Theme</div>
<div className="text-muted text-xs">Choose light or dark appearance</div>
<div className="text-muted text-xs">Choose your preferred theme</div>
</div>
<button
type="button"
onClick={toggleTheme}
className="border-border-medium bg-background-secondary hover:bg-hover flex h-9 items-center gap-2 rounded-md border px-3 text-sm transition-colors"
<select
value={theme}
onChange={(e) => setTheme(e.target.value as ThemeMode)}
className="border-border-medium bg-background-secondary hover:bg-hover h-9 cursor-pointer rounded-md border px-3 text-sm transition-colors focus:outline-none"
>
{theme === "light" ? (
<>
<SunMedium className="h-4 w-4" />
<span>Light</span>
</>
) : (
<>
<MoonStar className="h-4 w-4" />
<span>Dark</span>
</>
)}
</button>
{THEME_OPTIONS.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</div>
</div>
</div>
Expand Down
26 changes: 26 additions & 0 deletions src/browser/components/ThemeSelector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { useTheme, THEME_OPTIONS, type ThemeMode } from "@/browser/contexts/ThemeContext";
import { TooltipWrapper, Tooltip } from "./Tooltip";

export function ThemeSelector() {
const { theme, setTheme } = useTheme();
const currentLabel = THEME_OPTIONS.find((t) => t.value === theme)?.label ?? theme;

return (
<TooltipWrapper>
<select
value={theme}
onChange={(e) => setTheme(e.target.value as ThemeMode)}
className="border-border-light text-muted-foreground hover:border-border-medium/80 hover:bg-toggle-bg/70 focus-visible:ring-border-medium h-5 cursor-pointer appearance-none rounded-md border bg-transparent px-1.5 text-[11px] transition-colors duration-150 focus:outline-none focus-visible:ring-1"
aria-label="Select theme"
data-testid="theme-selector"
>
{THEME_OPTIONS.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
<Tooltip align="right">Theme: {currentLabel}</Tooltip>
</TooltipWrapper>
);
}
33 changes: 27 additions & 6 deletions src/browser/contexts/ThemeContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,14 @@ import React, {
import { usePersistedState } from "@/browser/hooks/usePersistedState";
import { UI_THEME_KEY } from "@/common/constants/storage";

export type ThemeMode = "light" | "dark";
export type ThemeMode = "light" | "dark" | "solarized-light" | "solarized-dark";

export const THEME_OPTIONS: Array<{ value: ThemeMode; label: string }> = [
{ value: "light", label: "Light" },
{ value: "dark", label: "Dark" },
{ value: "solarized-light", label: "Solarized Light" },
{ value: "solarized-dark", label: "Solarized Dark" },
];

interface ThemeContextValue {
theme: ThemeMode;
Expand All @@ -21,8 +28,17 @@ interface ThemeContextValue {

const ThemeContext = createContext<ThemeContextValue | null>(null);

const DARK_THEME_COLOR = "#1e1e1e";
const LIGHT_THEME_COLOR = "#f5f6f8";
const THEME_COLORS: Record<ThemeMode, string> = {
dark: "#1e1e1e",
light: "#f5f6f8",
"solarized-light": "#fdf6e3",
"solarized-dark": "#002b36",
};

/** Map theme mode to CSS color-scheme value */
function getColorScheme(theme: ThemeMode): "light" | "dark" {
return theme === "light" || theme === "solarized-light" ? "light" : "dark";
}

function resolveSystemTheme(): ThemeMode {
if (typeof window === "undefined" || !window.matchMedia) {
Expand All @@ -39,9 +55,9 @@ function applyThemeToDocument(theme: ThemeMode) {

const root = document.documentElement;
root.dataset.theme = theme;
root.style.colorScheme = theme;
root.style.colorScheme = getColorScheme(theme);

const themeColor = theme === "light" ? LIGHT_THEME_COLOR : DARK_THEME_COLOR;
const themeColor = THEME_COLORS[theme];
const meta = document.querySelector<HTMLMetaElement>('meta[name="theme-color"]');
if (meta) {
meta.setAttribute("content", themeColor);
Expand Down Expand Up @@ -90,7 +106,12 @@ export function ThemeProvider({

const toggleTheme = useCallback(() => {
if (!isNestedUnderForcedProvider) {
setTheme((current) => (current === "dark" ? "light" : "dark"));
setTheme((current) => {
const themeValues = THEME_OPTIONS.map((t) => t.value);
const currentIndex = themeValues.indexOf(current);
const nextIndex = (currentIndex + 1) % themeValues.length;
return themeValues[nextIndex];
});
}
}, [setTheme, isNestedUnderForcedProvider]);

Expand Down
Loading