feat(ui): add system/light/dark theme toggle

Add a 3-state theme toggle in folder selection and instance tabs, and update tokens/styles so light mode has readable contrast. Sync MUI surfaces and Shiki highlighting to CSS variables to prevent stale colors when switching themes.
This commit is contained in:
Shantur Rathore
2026-02-03 16:49:42 +00:00
parent a2127a11ac
commit 17a3e43ac7
18 changed files with 288 additions and 117 deletions

View File

@@ -55,7 +55,7 @@ export function CodeBlockInline(props: CodeBlockInlineProps) {
const highlighted = highlighter.codeToHtml(props.code, {
lang: props.language as CodeToHtmlOptions["lang"],
theme: isDark() ? "github-dark" : "github-light",
theme: isDark() ? "github-dark" : "github-light-high-contrast",
})
setHtml(highlighted)
} catch {

View File

@@ -5,6 +5,7 @@ import { useConfig } from "../stores/preferences"
import AdvancedSettingsModal from "./advanced-settings-modal"
import DirectoryBrowserDialog from "./directory-browser-dialog"
import Kbd from "./kbd"
import { ThemeModeToggle } from "./theme-mode-toggle"
import { openNativeFolderDialog, supportsNativeDialogs } from "../lib/native/native-functions"
import VersionPill from "./version-pill"
import { DiscordSymbolIcon, GitHubMarkIcon } from "./brand-icons"
@@ -313,8 +314,9 @@ const FolderSelectionView: Component<FolderSelectionViewProps> = (props) => {
</Select.Portal>
</Select>
</div>
<Show when={props.onOpenRemoteAccess}>
<div class="absolute top-4 right-6">
<div class="absolute top-4 right-6 flex items-center gap-2">
<ThemeModeToggle class="selector-button selector-button-secondary w-auto p-2 inline-flex items-center justify-center" />
<Show when={props.onOpenRemoteAccess}>
<button
type="button"
class="selector-button selector-button-secondary w-auto p-2 inline-flex items-center justify-center"
@@ -322,8 +324,8 @@ const FolderSelectionView: Component<FolderSelectionViewProps> = (props) => {
>
<MonitorUp class="w-4 h-4" />
</button>
</div>
</Show>
</Show>
</div>
<div class="mb-6 text-center shrink-0">
<div class="mb-3 flex justify-center">
<img src={codeNomadLogo} alt={t("folderSelection.logoAlt")} class="h-32 w-auto sm:h-48" loading="lazy" />

View File

@@ -5,6 +5,7 @@ import KeyboardHint from "./keyboard-hint"
import { Plus, MonitorUp } from "lucide-solid"
import { keyboardRegistry } from "../lib/keyboard-registry"
import { useI18n } from "../lib/i18n"
import { ThemeModeToggle } from "./theme-mode-toggle"
interface InstanceTabsProps {
instances: Map<string, Instance>
@@ -52,6 +53,7 @@ const InstanceTabs: Component<InstanceTabsProps> = (props) => {
/>
</div>
</Show>
<ThemeModeToggle class="new-tab-button" />
<Show when={Boolean(props.onOpenRemoteAccess)}>
<button
class="new-tab-button tab-remote-button"

View File

@@ -875,7 +875,7 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
</Show>
</div>
</div>
<div class="flex items-center gap-2">
<div class="flex items-center gap-2 text-primary">
<IconButton
size="small"
color="inherit"
@@ -1088,8 +1088,8 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
return (
<div class="flex flex-col h-full" ref={setRightDrawerContentEl}>
<div class="flex items-center justify-between px-4 py-2 border-b border-base">
<Typography variant="subtitle2" class="uppercase tracking-wide text-xs font-semibold">
<div class="flex items-center justify-between px-4 py-2 border-b border-base text-primary">
<Typography variant="subtitle2" class="uppercase tracking-wide text-xs font-semibold text-primary">
{t("instanceShell.rightPanel.title")}
</Typography>
<div class="flex items-center gap-2">
@@ -1331,13 +1331,13 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
<div class="flex flex-wrap items-center justify-center gap-2 pb-1">
<div class="inline-flex items-center gap-1 rounded-full border border-base px-2 py-0.5 text-xs text-primary">
<span class="uppercase text-[10px] tracking-wide text-primary/70">
<span class="uppercase text-[10px] tracking-wide text-muted">
{t("instanceShell.metrics.usedLabel")}
</span>
<span class="font-semibold text-primary">{formattedUsedTokens()}</span>
</div>
<div class="inline-flex items-center gap-1 rounded-full border border-base px-2 py-0.5 text-xs text-primary">
<span class="uppercase text-[10px] tracking-wide text-primary/70">
<span class="uppercase text-[10px] tracking-wide text-muted">
{t("instanceShell.metrics.availableLabel")}
</span>
<span class="font-semibold text-primary">{formattedAvailableTokens()}</span>
@@ -1361,13 +1361,13 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
<Show when={!showingInfoView()}>
<div class="inline-flex items-center gap-1 rounded-full border border-base px-2 py-0.5 text-xs text-primary">
<span class="uppercase text-[10px] tracking-wide text-primary/70">
<span class="uppercase text-[10px] tracking-wide text-muted">
{t("instanceShell.metrics.usedLabel")}
</span>
<span class="font-semibold text-primary">{formattedUsedTokens()}</span>
</div>
<div class="inline-flex items-center gap-1 rounded-full border border-base px-2 py-0.5 text-xs text-primary">
<span class="uppercase text-[10px] tracking-wide text-primary/70">
<span class="uppercase text-[10px] tracking-wide text-muted">
{t("instanceShell.metrics.availableLabel")}
</span>
<span class="font-semibold text-primary">{formattedAvailableTokens()}</span>

View File

@@ -3,7 +3,7 @@ import Kbd from "./kbd"
import { useI18n } from "../lib/i18n"
const METRIC_CHIP_CLASS = "inline-flex items-center gap-1 rounded-full border border-base px-2 py-0.5 text-xs text-primary"
const METRIC_LABEL_CLASS = "uppercase text-[10px] tracking-wide text-primary/70"
const METRIC_LABEL_CLASS = "uppercase text-[10px] tracking-wide text-muted"
interface MessageListHeaderProps {
usedTokens: number

View File

@@ -103,15 +103,15 @@ interface MessagePartProps {
<Match when={partType() === "text"}>
<Show when={!shouldHideTextPart() && partHasRenderableText(props.part)}>
<div class={textContainerClass()}>
<Show
when={isAssistantMessage()}
fallback={<span>{plainTextContent()}</span>}
>
<Markdown
part={createTextPartForMarkdown()}
instanceId={props.instanceId}
sessionId={props.sessionId}
isDark={isDark()}
<Show
when={isAssistantMessage()}
fallback={<span class="text-primary">{plainTextContent()}</span>}
>
<Markdown
part={createTextPartForMarkdown()}
instanceId={props.instanceId}
sessionId={props.sessionId}
isDark={isDark()}
size={isAssistantMessage() ? "tight" : "base"}
onRendered={props.onRendered}
/>

View File

@@ -9,8 +9,8 @@ interface ContextUsagePanelProps {
}
const chipClass = "inline-flex items-center gap-1 rounded-full border border-base px-2 py-0.5 text-xs text-primary"
const chipLabelClass = "uppercase text-[10px] tracking-wide text-primary/70"
const headingClass = "text-xs font-semibold text-primary/70 uppercase tracking-wide"
const chipLabelClass = "uppercase text-[10px] tracking-wide text-muted"
const headingClass = "text-xs font-semibold text-muted uppercase tracking-wide"
const ContextUsagePanel: Component<ContextUsagePanelProps> = (props) => {
const { t } = useI18n()
@@ -49,7 +49,7 @@ const ContextUsagePanel: Component<ContextUsagePanelProps> = (props) => {
return (
<div class="session-context-panel border-r border-base border-b px-3 py-3 space-y-3">
<div class="flex flex-wrap items-center gap-2 text-xs text-primary/90">
<div class="flex flex-wrap items-center gap-2 text-xs text-primary">
<div class={headingClass}>{t("contextUsagePanel.headings.tokens")}</div>
<div class={chipClass}>
<span class={chipLabelClass}>{t("contextUsagePanel.labels.input")}</span>
@@ -65,7 +65,7 @@ const ContextUsagePanel: Component<ContextUsagePanelProps> = (props) => {
</div>
</div>
<div class="flex flex-wrap items-center gap-2 text-xs text-primary/90">
<div class="flex flex-wrap items-center gap-2 text-xs text-primary">
<div class={headingClass}>{t("contextUsagePanel.headings.context")}</div>
<div class={chipClass}>
<span class={chipLabelClass}>{t("contextUsagePanel.labels.used")}</span>

View File

@@ -0,0 +1,39 @@
import { createMemo, type Component } from "solid-js"
import { Laptop, Moon, Sun } from "lucide-solid"
import { useI18n } from "../lib/i18n"
import { useTheme } from "../lib/theme"
interface ThemeModeToggleProps {
class?: string
}
export const ThemeModeToggle: Component<ThemeModeToggleProps> = (props) => {
const { t } = useI18n()
const { themeMode, cycleThemeMode } = useTheme()
const modeLabel = () => {
const mode = themeMode()
if (mode === "system") return t("theme.mode.system")
if (mode === "light") return t("theme.mode.light")
return t("theme.mode.dark")
}
const icon = createMemo(() => {
const mode = themeMode()
if (mode === "system") return <Laptop class="w-4 h-4" />
if (mode === "light") return <Sun class="w-4 h-4" />
return <Moon class="w-4 h-4" />
})
return (
<button
type="button"
class={props.class ?? "new-tab-button"}
onClick={cycleThemeMode}
aria-label={t("theme.toggle.ariaLabel", { mode: modeLabel() })}
title={t("theme.toggle.title", { mode: modeLabel() })}
>
{icon()}
</button>
)
}