pause
This commit is contained in:
@@ -187,7 +187,6 @@ export function App() {
|
||||
return (
|
||||
<ThemeProvider>
|
||||
<Layout
|
||||
theme={appStore.resolveTheme()}
|
||||
layerDepth={layerDepth()}
|
||||
header={
|
||||
<TabNavigation activeTab={activeTab()} onTabSelect={setActiveTab} />
|
||||
|
||||
@@ -7,7 +7,7 @@ export function LayerIndicator({ layerDepth }: { layerDepth: number }) {
|
||||
const indicators = []
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const isActive = i <= layerDepth
|
||||
const color = isActive ? "#f6c177" : "#4c566a"
|
||||
const color = isActive ? "var(--color-accent)" : "var(--color-muted)"
|
||||
const size = isActive ? "●" : "○"
|
||||
indicators.push(
|
||||
<text fg={color} marginRight={1}>
|
||||
@@ -20,9 +20,9 @@ export function LayerIndicator({ layerDepth }: { layerDepth: number }) {
|
||||
|
||||
return (
|
||||
<box flexDirection="row" alignItems="center">
|
||||
<text fg="#7d8590" marginRight={1}>Depth:</text>
|
||||
<text fg="var(--color-muted)" marginRight={1}>Depth:</text>
|
||||
{getLayerIndicator()}
|
||||
<text fg="#7d8590" marginLeft={1}>
|
||||
<text fg="var(--color-muted)" marginLeft={1}>
|
||||
{layerDepth}
|
||||
</text>
|
||||
</box>
|
||||
|
||||
@@ -90,7 +90,7 @@ export function LoginScreen(props: LoginScreenProps) {
|
||||
|
||||
{/* Email field */}
|
||||
<box flexDirection="column" gap={0}>
|
||||
<text fg={focusField() === "email" ? "cyan" : undefined}>Email:</text>
|
||||
<text fg={focusField() === "email" ? "var(--color-primary)" : undefined}>Email:</text>
|
||||
<input
|
||||
value={email()}
|
||||
onInput={setEmail}
|
||||
@@ -99,13 +99,13 @@ export function LoginScreen(props: LoginScreenProps) {
|
||||
width={30}
|
||||
/>
|
||||
{emailError() && (
|
||||
<text fg="red">{emailError()}</text>
|
||||
<text fg="var(--color-error)">{emailError()}</text>
|
||||
)}
|
||||
</box>
|
||||
|
||||
{/* Password field */}
|
||||
<box flexDirection="column" gap={0}>
|
||||
<text fg={focusField() === "password" ? "cyan" : undefined}>
|
||||
<text fg={focusField() === "password" ? "var(--color-primary)" : undefined}>
|
||||
Password:
|
||||
</text>
|
||||
<input
|
||||
@@ -116,7 +116,7 @@ export function LoginScreen(props: LoginScreenProps) {
|
||||
width={30}
|
||||
/>
|
||||
{passwordError() && (
|
||||
<text fg="red">{passwordError()}</text>
|
||||
<text fg="var(--color-error)">{passwordError()}</text>
|
||||
)}
|
||||
</box>
|
||||
|
||||
@@ -127,9 +127,9 @@ export function LoginScreen(props: LoginScreenProps) {
|
||||
<box
|
||||
border
|
||||
padding={1}
|
||||
backgroundColor={focusField() === "submit" ? "#333" : undefined}
|
||||
backgroundColor={focusField() === "submit" ? "var(--color-primary)" : undefined}
|
||||
>
|
||||
<text fg={focusField() === "submit" ? "cyan" : undefined}>
|
||||
<text fg={focusField() === "submit" ? "var(--color-text)" : undefined}>
|
||||
{auth.isLoading ? "Signing in..." : "[Enter] Sign In"}
|
||||
</text>
|
||||
</box>
|
||||
@@ -137,21 +137,21 @@ export function LoginScreen(props: LoginScreenProps) {
|
||||
|
||||
{/* Auth error message */}
|
||||
{auth.error && (
|
||||
<text fg="red">{auth.error.message}</text>
|
||||
<text fg="var(--color-error)">{auth.error.message}</text>
|
||||
)}
|
||||
|
||||
<box height={1} />
|
||||
|
||||
{/* Alternative auth options */}
|
||||
<text fg="gray">Or authenticate with:</text>
|
||||
<text fg="var(--color-muted)">Or authenticate with:</text>
|
||||
|
||||
<box flexDirection="row" gap={2}>
|
||||
<box
|
||||
border
|
||||
padding={1}
|
||||
backgroundColor={focusField() === "code" ? "#333" : undefined}
|
||||
backgroundColor={focusField() === "code" ? "var(--color-primary)" : undefined}
|
||||
>
|
||||
<text fg={focusField() === "code" ? "yellow" : "gray"}>
|
||||
<text fg={focusField() === "code" ? "var(--color-accent)" : "var(--color-muted)"}>
|
||||
[C] Sync Code
|
||||
</text>
|
||||
</box>
|
||||
@@ -159,9 +159,9 @@ export function LoginScreen(props: LoginScreenProps) {
|
||||
<box
|
||||
border
|
||||
padding={1}
|
||||
backgroundColor={focusField() === "oauth" ? "#333" : undefined}
|
||||
backgroundColor={focusField() === "oauth" ? "var(--color-primary)" : undefined}
|
||||
>
|
||||
<text fg={focusField() === "oauth" ? "yellow" : "gray"}>
|
||||
<text fg={focusField() === "oauth" ? "var(--color-accent)" : "var(--color-muted)"}>
|
||||
[O] OAuth Info
|
||||
</text>
|
||||
</box>
|
||||
@@ -169,7 +169,7 @@ export function LoginScreen(props: LoginScreenProps) {
|
||||
|
||||
<box height={1} />
|
||||
|
||||
<text fg="gray">Tab to navigate, Enter to select</text>
|
||||
<text fg="var(--color-muted)">Tab to navigate, Enter to select</text>
|
||||
</box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -76,55 +76,55 @@ export function PreferencesPanel() {
|
||||
|
||||
return (
|
||||
<box flexDirection="column" gap={1}>
|
||||
<text fg="gray">Preferences</text>
|
||||
<text fg="var(--color-muted)">Preferences</text>
|
||||
|
||||
<box flexDirection="column" gap={1}>
|
||||
<box flexDirection="row" gap={1} alignItems="center">
|
||||
<text fg={focusField() === "theme" ? "cyan" : "gray"}>Theme:</text>
|
||||
<text fg={focusField() === "theme" ? "var(--color-primary)" : "var(--color-muted)"}>Theme:</text>
|
||||
<box border padding={0}>
|
||||
<text fg="white">{THEME_LABELS.find((t) => t.value === settings().theme)?.label}</text>
|
||||
<text fg="var(--color-text)">{THEME_LABELS.find((t) => t.value === settings().theme)?.label}</text>
|
||||
</box>
|
||||
<text fg="gray">[Left/Right]</text>
|
||||
<text fg="var(--color-muted)">[Left/Right]</text>
|
||||
</box>
|
||||
|
||||
<box flexDirection="row" gap={1} alignItems="center">
|
||||
<text fg={focusField() === "font" ? "cyan" : "gray"}>Font Size:</text>
|
||||
<text fg={focusField() === "font" ? "var(--color-primary)" : "var(--color-muted)"}>Font Size:</text>
|
||||
<box border padding={0}>
|
||||
<text fg="white">{settings().fontSize}px</text>
|
||||
<text fg="var(--color-text)">{settings().fontSize}px</text>
|
||||
</box>
|
||||
<text fg="gray">[Left/Right]</text>
|
||||
<text fg="var(--color-muted)">[Left/Right]</text>
|
||||
</box>
|
||||
|
||||
<box flexDirection="row" gap={1} alignItems="center">
|
||||
<text fg={focusField() === "speed" ? "cyan" : "gray"}>Playback:</text>
|
||||
<text fg={focusField() === "speed" ? "var(--color-primary)" : "var(--color-muted)"}>Playback:</text>
|
||||
<box border padding={0}>
|
||||
<text fg="white">{settings().playbackSpeed}x</text>
|
||||
<text fg="var(--color-text)">{settings().playbackSpeed}x</text>
|
||||
</box>
|
||||
<text fg="gray">[Left/Right]</text>
|
||||
<text fg="var(--color-muted)">[Left/Right]</text>
|
||||
</box>
|
||||
|
||||
<box flexDirection="row" gap={1} alignItems="center">
|
||||
<text fg={focusField() === "explicit" ? "cyan" : "gray"}>Show Explicit:</text>
|
||||
<text fg={focusField() === "explicit" ? "var(--color-primary)" : "var(--color-muted)"}>Show Explicit:</text>
|
||||
<box border padding={0}>
|
||||
<text fg={preferences().showExplicit ? "green" : "gray"}>
|
||||
<text fg={preferences().showExplicit ? "var(--color-success)" : "var(--color-muted)"}>
|
||||
{preferences().showExplicit ? "On" : "Off"}
|
||||
</text>
|
||||
</box>
|
||||
<text fg="gray">[Space]</text>
|
||||
<text fg="var(--color-muted)">[Space]</text>
|
||||
</box>
|
||||
|
||||
<box flexDirection="row" gap={1} alignItems="center">
|
||||
<text fg={focusField() === "auto" ? "cyan" : "gray"}>Auto Download:</text>
|
||||
<text fg={focusField() === "auto" ? "var(--color-primary)" : "var(--color-muted)"}>Auto Download:</text>
|
||||
<box border padding={0}>
|
||||
<text fg={preferences().autoDownload ? "green" : "gray"}>
|
||||
<text fg={preferences().autoDownload ? "var(--color-success)" : "var(--color-muted)"}>
|
||||
{preferences().autoDownload ? "On" : "Off"}
|
||||
</text>
|
||||
</box>
|
||||
<text fg="gray">[Space]</text>
|
||||
<text fg="var(--color-muted)">[Space]</text>
|
||||
</box>
|
||||
</box>
|
||||
|
||||
<text fg="gray">Tab to move focus, Left/Right to adjust</text>
|
||||
<text fg="var(--color-muted)">Tab to move focus, Left/Right to adjust</text>
|
||||
</box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ export function SettingsScreen(props: SettingsScreenProps) {
|
||||
<text>
|
||||
<strong>Settings</strong>
|
||||
</text>
|
||||
<text fg="gray">[Tab] Switch section | 1-4 jump | Esc up</text>
|
||||
<text fg="var(--color-muted)">[Tab] Switch section | 1-4 jump | Esc up</text>
|
||||
</box>
|
||||
|
||||
<box flexDirection="row" gap={1}>
|
||||
@@ -58,10 +58,10 @@ export function SettingsScreen(props: SettingsScreenProps) {
|
||||
<box
|
||||
border
|
||||
padding={0}
|
||||
backgroundColor={activeSection() === section.id ? "#2b303b" : undefined}
|
||||
backgroundColor={activeSection() === section.id ? "var(--color-primary)" : undefined}
|
||||
onMouseDown={() => setActiveSection(section.id)}
|
||||
>
|
||||
<text fg={activeSection() === section.id ? "cyan" : "gray"}>
|
||||
<text fg={activeSection() === section.id ? "var(--color-text)" : "var(--color-muted)"}>
|
||||
[{index + 1}] {section.label}
|
||||
</text>
|
||||
</box>
|
||||
@@ -74,21 +74,21 @@ export function SettingsScreen(props: SettingsScreenProps) {
|
||||
{activeSection() === "preferences" && <PreferencesPanel />}
|
||||
{activeSection() === "account" && (
|
||||
<box flexDirection="column" gap={1}>
|
||||
<text fg="gray">Account</text>
|
||||
<text fg="var(--color-muted)">Account</text>
|
||||
<box flexDirection="row" gap={2} alignItems="center">
|
||||
<text fg="gray">Status:</text>
|
||||
<text fg={props.accountStatus === "signed-in" ? "green" : "yellow"}>
|
||||
<text fg="var(--color-muted)">Status:</text>
|
||||
<text fg={props.accountStatus === "signed-in" ? "var(--color-success)" : "var(--color-warning)"}>
|
||||
{props.accountLabel}
|
||||
</text>
|
||||
</box>
|
||||
<box border padding={0} onMouseDown={() => props.onOpenAccount?.()}>
|
||||
<text fg="cyan">[A] Manage Account</text>
|
||||
<text fg="var(--color-primary)">[A] Manage Account</text>
|
||||
</box>
|
||||
</box>
|
||||
)}
|
||||
</box>
|
||||
|
||||
<text fg="gray">Enter to dive | Esc up</text>
|
||||
<text fg="var(--color-muted)">Enter to dive | Esc up</text>
|
||||
</box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -155,15 +155,15 @@ export function SourceManager(props: SourceManagerProps) {
|
||||
<strong>Podcast Sources</strong>
|
||||
</text>
|
||||
<box border padding={0} onMouseDown={props.onClose}>
|
||||
<text fg="cyan">[Esc] Close</text>
|
||||
<text fg="var(--color-primary)">[Esc] Close</text>
|
||||
</box>
|
||||
</box>
|
||||
|
||||
<text fg="gray">Manage where to search for podcasts</text>
|
||||
<text fg="var(--color-muted)">Manage where to search for podcasts</text>
|
||||
|
||||
{/* Source list */}
|
||||
<box border padding={1} flexDirection="column" gap={1}>
|
||||
<text fg={focusArea() === "list" ? "cyan" : "gray"}>Sources:</text>
|
||||
<text fg={focusArea() === "list" ? "var(--color-primary)" : "var(--color-muted)"}>Sources:</text>
|
||||
<scrollbox height={6}>
|
||||
<For each={sources()}>
|
||||
{(source, index) => (
|
||||
@@ -173,7 +173,7 @@ export function SourceManager(props: SourceManagerProps) {
|
||||
padding={0}
|
||||
backgroundColor={
|
||||
focusArea() === "list" && index() === selectedIndex()
|
||||
? "#333"
|
||||
? "var(--color-primary)"
|
||||
: undefined
|
||||
}
|
||||
onMouseDown={() => {
|
||||
@@ -184,21 +184,21 @@ export function SourceManager(props: SourceManagerProps) {
|
||||
>
|
||||
<text fg={
|
||||
focusArea() === "list" && index() === selectedIndex()
|
||||
? "cyan"
|
||||
: "gray"
|
||||
? "var(--color-primary)"
|
||||
: "var(--color-muted)"
|
||||
}>
|
||||
{focusArea() === "list" && index() === selectedIndex()
|
||||
? ">"
|
||||
: " "}
|
||||
</text>
|
||||
<text fg={source.enabled ? "green" : "red"}>
|
||||
<text fg={source.enabled ? "var(--color-success)" : "var(--color-error)"}>
|
||||
{source.enabled ? "[x]" : "[ ]"}
|
||||
</text>
|
||||
<text fg="yellow">{getSourceIcon(source)}</text>
|
||||
<text fg="var(--color-accent)">{getSourceIcon(source)}</text>
|
||||
<text
|
||||
fg={
|
||||
focusArea() === "list" && index() === selectedIndex()
|
||||
? "white"
|
||||
? "var(--color-text)"
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
@@ -208,54 +208,54 @@ export function SourceManager(props: SourceManagerProps) {
|
||||
)}
|
||||
</For>
|
||||
</scrollbox>
|
||||
<text fg="gray">Space/Enter to toggle, d to delete, a to add</text>
|
||||
<text fg="var(--color-muted)">Space/Enter to toggle, d to delete, a to add</text>
|
||||
|
||||
{/* API settings */}
|
||||
<box flexDirection="column" gap={1}>
|
||||
<text fg={isApiSource() ? "gray" : "yellow"}>
|
||||
<text fg={isApiSource() ? "var(--color-muted)" : "var(--color-accent)"}>
|
||||
{isApiSource() ? "API Settings" : "API Settings (select an API source)"}
|
||||
</text>
|
||||
<box flexDirection="row" gap={2}>
|
||||
<box
|
||||
border
|
||||
padding={0}
|
||||
backgroundColor={focusArea() === "country" ? "#333" : undefined}
|
||||
backgroundColor={focusArea() === "country" ? "var(--color-primary)" : undefined}
|
||||
>
|
||||
<text fg={focusArea() === "country" ? "cyan" : "gray"}>
|
||||
<text fg={focusArea() === "country" ? "var(--color-primary)" : "var(--color-muted)"}>
|
||||
Country: {sourceCountry()}
|
||||
</text>
|
||||
</box>
|
||||
<box
|
||||
border
|
||||
padding={0}
|
||||
backgroundColor={focusArea() === "language" ? "#333" : undefined}
|
||||
backgroundColor={focusArea() === "language" ? "var(--color-primary)" : undefined}
|
||||
>
|
||||
<text fg={focusArea() === "language" ? "cyan" : "gray"}>
|
||||
<text fg={focusArea() === "language" ? "var(--color-primary)" : "var(--color-muted)"}>
|
||||
Language: {sourceLanguage() === "ja_jp" ? "Japanese" : "English"}
|
||||
</text>
|
||||
</box>
|
||||
<box
|
||||
border
|
||||
padding={0}
|
||||
backgroundColor={focusArea() === "explicit" ? "#333" : undefined}
|
||||
backgroundColor={focusArea() === "explicit" ? "var(--color-primary)" : undefined}
|
||||
>
|
||||
<text fg={focusArea() === "explicit" ? "cyan" : "gray"}>
|
||||
<text fg={focusArea() === "explicit" ? "var(--color-primary)" : "var(--color-muted)"}>
|
||||
Explicit: {sourceExplicit() ? "Yes" : "No"}
|
||||
</text>
|
||||
</box>
|
||||
</box>
|
||||
<text fg="gray">Enter/Space to toggle focused setting</text>
|
||||
<text fg="var(--color-muted)">Enter/Space to toggle focused setting</text>
|
||||
</box>
|
||||
</box>
|
||||
|
||||
{/* Add new source form */}
|
||||
<box border padding={1} flexDirection="column" gap={1}>
|
||||
<text fg={focusArea() === "add" || focusArea() === "url" ? "cyan" : "gray"}>
|
||||
<text fg={focusArea() === "add" || focusArea() === "url" ? "var(--color-primary)" : "var(--color-muted)"}>
|
||||
Add New Source:
|
||||
</text>
|
||||
|
||||
<box flexDirection="row" gap={1}>
|
||||
<text fg="gray">Name:</text>
|
||||
<text fg="var(--color-muted)">Name:</text>
|
||||
<input
|
||||
value={newSourceName()}
|
||||
onInput={setNewSourceName}
|
||||
@@ -266,7 +266,7 @@ export function SourceManager(props: SourceManagerProps) {
|
||||
</box>
|
||||
|
||||
<box flexDirection="row" gap={1}>
|
||||
<text fg="gray">URL:</text>
|
||||
<text fg="var(--color-muted)">URL:</text>
|
||||
<input
|
||||
value={newSourceUrl()}
|
||||
onInput={(v) => {
|
||||
@@ -285,16 +285,16 @@ export function SourceManager(props: SourceManagerProps) {
|
||||
width={15}
|
||||
onMouseDown={handleAddSource}
|
||||
>
|
||||
<text fg="green">[+] Add Source</text>
|
||||
<text fg="var(--color-success)">[+] Add Source</text>
|
||||
</box>
|
||||
</box>
|
||||
|
||||
{/* Error message */}
|
||||
{error() && (
|
||||
<text fg="red">{error()}</text>
|
||||
<text fg="var(--color-error)">{error()}</text>
|
||||
)}
|
||||
|
||||
<text fg="gray">Tab to switch sections, Esc to close</text>
|
||||
<text fg="var(--color-muted)">Tab to switch sections, Esc to close</text>
|
||||
</box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ export function Tab(props: TabProps) {
|
||||
<box
|
||||
border
|
||||
onMouseDown={() => props.onSelect(props.tab.id)}
|
||||
style={{ padding: 1, backgroundColor: props.active ? "#333333" : "transparent" }}
|
||||
style={{ padding: 1, backgroundColor: props.active ? "var(--color-primary)" : "transparent" }}
|
||||
>
|
||||
<text>
|
||||
{props.active ? "[" : " "}
|
||||
|
||||
@@ -1,41 +1,64 @@
|
||||
import { createContext, useContext, createSignal } from "solid-js"
|
||||
import { createContext, useContext, createSignal, createEffect, onCleanup } from "solid-js"
|
||||
import type { ThemeColors, ThemeName } from "../types/settings"
|
||||
import { useAppStore } from "../stores/app"
|
||||
import { applyTheme, setThemeAttribute, getSystemThemeMode } from "../utils/theme"
|
||||
|
||||
type ThemeContextType = {
|
||||
themeName: () => ThemeName
|
||||
setThemeName: (theme: ThemeName) => void
|
||||
resolvedTheme: ThemeColors
|
||||
resolvedTheme: () => ThemeColors
|
||||
isSystemTheme: () => boolean
|
||||
currentMode: () => "dark" | "light"
|
||||
}
|
||||
|
||||
const ThemeContext = createContext<ThemeContextType>()
|
||||
|
||||
export function ThemeProvider({ children }: { children: any }) {
|
||||
const [themeName, setThemeName] = createSignal<ThemeName>("system")
|
||||
const appStore = useAppStore()
|
||||
const [themeName, setThemeName] = createSignal<ThemeName>(appStore.state().settings.theme)
|
||||
const [resolvedTheme, setResolvedTheme] = createSignal<ThemeColors>(appStore.resolveTheme())
|
||||
const [currentMode, setCurrentMode] = createSignal<"dark" | "light">(getSystemThemeMode())
|
||||
|
||||
const isSystemTheme = () => themeName() === "system"
|
||||
|
||||
const resolvedTheme = {
|
||||
background: "transparent",
|
||||
surface: "#1b1f27",
|
||||
primary: "#6fa8ff",
|
||||
secondary: "#a9b1d6",
|
||||
accent: "#f6c177",
|
||||
text: "#e6edf3",
|
||||
muted: "#7d8590",
|
||||
warning: "#f0b429",
|
||||
error: "#f47067",
|
||||
success: "#3fb950",
|
||||
layerBackgrounds: {
|
||||
layer0: "transparent",
|
||||
layer1: "#1e222e",
|
||||
layer2: "#161b22",
|
||||
layer3: "#0d1117",
|
||||
},
|
||||
// Update theme when appStore theme changes
|
||||
createEffect(() => {
|
||||
const currentTheme = appStore.state().settings.theme
|
||||
setThemeName(currentTheme)
|
||||
setResolvedTheme(appStore.resolveTheme())
|
||||
|
||||
// Apply theme to CSS variables
|
||||
if (currentTheme === "system") {
|
||||
const mode = getSystemThemeMode()
|
||||
setCurrentMode(mode)
|
||||
applyTheme(resolvedTheme())
|
||||
} else {
|
||||
setCurrentMode("dark") // All themes are dark by default
|
||||
}
|
||||
|
||||
setThemeAttribute(currentTheme === "system" ? "system" : currentTheme)
|
||||
})
|
||||
|
||||
// Handle system theme changes
|
||||
createEffect(() => {
|
||||
if (isSystemTheme()) {
|
||||
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)")
|
||||
const handler = () => {
|
||||
const newMode = getSystemThemeMode()
|
||||
setCurrentMode(newMode)
|
||||
setResolvedTheme(appStore.resolveTheme())
|
||||
}
|
||||
|
||||
mediaQuery.addEventListener("change", handler)
|
||||
|
||||
onCleanup(() => {
|
||||
mediaQuery.removeEventListener("change", handler)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={{ themeName, setThemeName, resolvedTheme, isSystemTheme }}>
|
||||
<ThemeContext.Provider value={{ themeName, setThemeName, resolvedTheme, isSystemTheme, currentMode }}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { render } from "@opentui/solid"
|
||||
import { App } from "./App"
|
||||
import "./styles/theme.css"
|
||||
|
||||
render(() => <App />)
|
||||
|
||||
138
src/styles/theme.css
Normal file
138
src/styles/theme.css
Normal file
@@ -0,0 +1,138 @@
|
||||
/* Theme CSS Variables */
|
||||
|
||||
:root {
|
||||
/* Base Colors */
|
||||
--color-background: transparent;
|
||||
--color-surface: #1b1f27;
|
||||
--color-primary: #6fa8ff;
|
||||
--color-secondary: #a9b1d6;
|
||||
--color-accent: #f6c177;
|
||||
--color-text: #e6edf3;
|
||||
--color-muted: #7d8590;
|
||||
--color-warning: #f0b429;
|
||||
--color-error: #f47067;
|
||||
--color-success: #3fb950;
|
||||
|
||||
/* Layer Backgrounds */
|
||||
--color-layer0: transparent;
|
||||
--color-layer1: #1e222e;
|
||||
--color-layer2: #161b22;
|
||||
--color-layer3: #0d1117;
|
||||
}
|
||||
|
||||
/* Dark Theme (Catppuccin default) */
|
||||
[data-theme="dark"] {
|
||||
--color-background: transparent;
|
||||
--color-surface: #1e1e2e;
|
||||
--color-primary: #89b4fa;
|
||||
--color-secondary: #cba6f7;
|
||||
--color-accent: #f9e2af;
|
||||
--color-text: #cdd6f4;
|
||||
--color-muted: #7f849c;
|
||||
--color-warning: #fab387;
|
||||
--color-error: #f38ba8;
|
||||
--color-success: #a6e3a1;
|
||||
|
||||
--color-layer0: transparent;
|
||||
--color-layer1: #181825;
|
||||
--color-layer2: #11111b;
|
||||
--color-layer3: #0a0a0f;
|
||||
}
|
||||
|
||||
/* Light Theme (Gruvbox) */
|
||||
[data-theme="light"] {
|
||||
--color-background: transparent;
|
||||
--color-surface: #282828;
|
||||
--color-primary: #fabd2f;
|
||||
--color-secondary: #83a598;
|
||||
--color-accent: #fe8019;
|
||||
--color-text: #ebdbb2;
|
||||
--color-muted: #928374;
|
||||
--color-warning: #fabd2f;
|
||||
--color-error: #fb4934;
|
||||
--color-success: #b8bb26;
|
||||
|
||||
--color-layer0: transparent;
|
||||
--color-layer1: #32302a;
|
||||
--color-layer2: #1d2021;
|
||||
--color-layer3: #0d0c0c;
|
||||
}
|
||||
|
||||
/* Tokyo Theme */
|
||||
[data-theme="tokyo"] {
|
||||
--color-background: transparent;
|
||||
--color-surface: #1a1b26;
|
||||
--color-primary: #7aa2f7;
|
||||
--color-secondary: #bb9af7;
|
||||
--color-accent: #e0af68;
|
||||
--color-text: #c0caf5;
|
||||
--color-muted: #565f89;
|
||||
--color-warning: #e0af68;
|
||||
--color-error: #f7768e;
|
||||
--color-success: #9ece6a;
|
||||
|
||||
--color-layer0: transparent;
|
||||
--color-layer1: #16161e;
|
||||
--color-layer2: #0f0f15;
|
||||
--color-layer3: #08080b;
|
||||
}
|
||||
|
||||
/* Nord Theme */
|
||||
[data-theme="nord"] {
|
||||
--color-background: transparent;
|
||||
--color-surface: #2e3440;
|
||||
--color-primary: #88c0d0;
|
||||
--color-secondary: #81a1c1;
|
||||
--color-accent: #ebcb8b;
|
||||
--color-text: #eceff4;
|
||||
--color-muted: #4c566a;
|
||||
--color-warning: #ebcb8b;
|
||||
--color-error: #bf616a;
|
||||
--color-success: #a3be8c;
|
||||
|
||||
--color-layer0: transparent;
|
||||
--color-layer1: #3b4252;
|
||||
--color-layer2: #242933;
|
||||
--color-layer3: #1a1c23;
|
||||
}
|
||||
|
||||
/* System Theme */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
[data-theme="system"] {
|
||||
--color-background: transparent;
|
||||
--color-surface: #1e1e2e;
|
||||
--color-primary: #89b4fa;
|
||||
--color-secondary: #cba6f7;
|
||||
--color-accent: #f9e2af;
|
||||
--color-text: #cdd6f4;
|
||||
--color-muted: #7f849c;
|
||||
--color-warning: #fab387;
|
||||
--color-error: #f38ba8;
|
||||
--color-success: #a6e3a1;
|
||||
|
||||
--color-layer0: transparent;
|
||||
--color-layer1: #181825;
|
||||
--color-layer2: #11111b;
|
||||
--color-layer3: #0a0a0f;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
[data-theme="system"] {
|
||||
--color-background: transparent;
|
||||
--color-surface: #282828;
|
||||
--color-primary: #fabd2f;
|
||||
--color-secondary: #83a598;
|
||||
--color-accent: #fe8019;
|
||||
--color-text: #ebdbb2;
|
||||
--color-muted: #928374;
|
||||
--color-warning: #fabd2f;
|
||||
--color-error: #fb4934;
|
||||
--color-success: #b8bb26;
|
||||
|
||||
--color-layer0: transparent;
|
||||
--color-layer1: #32302a;
|
||||
--color-layer2: #1d2021;
|
||||
--color-layer3: #0d0c0c;
|
||||
}
|
||||
}
|
||||
63
src/utils/theme.ts
Normal file
63
src/utils/theme.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* Theme CSS Variable Manager
|
||||
* Handles dynamic theme switching by updating CSS custom properties
|
||||
*/
|
||||
|
||||
export function applyTheme(theme: {
|
||||
background: string
|
||||
surface: string
|
||||
primary: string
|
||||
secondary: string
|
||||
accent: string
|
||||
text: string
|
||||
muted: string
|
||||
warning: string
|
||||
error: string
|
||||
success: string
|
||||
layerBackgrounds?: {
|
||||
layer0: string
|
||||
layer1: string
|
||||
layer2: string
|
||||
layer3: string
|
||||
}
|
||||
}) {
|
||||
const root = document.documentElement
|
||||
|
||||
// Apply base theme colors
|
||||
root.style.setProperty("--color-background", theme.background)
|
||||
root.style.setProperty("--color-surface", theme.surface)
|
||||
root.style.setProperty("--color-primary", theme.primary)
|
||||
root.style.setProperty("--color-secondary", theme.secondary)
|
||||
root.style.setProperty("--color-accent", theme.accent)
|
||||
root.style.setProperty("--color-text", theme.text)
|
||||
root.style.setProperty("--color-muted", theme.muted)
|
||||
root.style.setProperty("--color-warning", theme.warning)
|
||||
root.style.setProperty("--color-error", theme.error)
|
||||
root.style.setProperty("--color-success", theme.success)
|
||||
|
||||
// Apply layer backgrounds if available
|
||||
if (theme.layerBackgrounds) {
|
||||
root.style.setProperty("--color-layer0", theme.layerBackgrounds.layer0)
|
||||
root.style.setProperty("--color-layer1", theme.layerBackgrounds.layer1)
|
||||
root.style.setProperty("--color-layer2", theme.layerBackgrounds.layer2)
|
||||
root.style.setProperty("--color-layer3", theme.layerBackgrounds.layer3)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get theme mode from system preference
|
||||
*/
|
||||
export function getSystemThemeMode(): "dark" | "light" {
|
||||
if (typeof window === "undefined") return "dark"
|
||||
|
||||
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
return prefersDark ? "dark" : "light"
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply CSS variable data-theme attribute
|
||||
*/
|
||||
export function setThemeAttribute(themeName: string) {
|
||||
const root = document.documentElement
|
||||
root.setAttribute("data-theme", themeName)
|
||||
}
|
||||
@@ -13,13 +13,13 @@ Tasks
|
||||
- [x] 06 — Implement left/right layer navigation controls → `06-implement-layer-navigation-controls.md`
|
||||
- [x] 07 — Implement enter/escape layer navigation controls → `07-implement-enter-escape-controls.md`
|
||||
- [x] 08 — Design active layer background color system → `08-design-active-layer-colors.md`
|
||||
- [ ] 09 — Create theme context provider → `09-create-theme-context-provider.md`
|
||||
- [ ] 10 — Implement DesktopTheme type and structure → `10-implement-desktop-theme-types.md`
|
||||
- [ ] 11 — Implement theme resolution system → `11-implement-theme-resolution.md`
|
||||
- [ ] 12 — Create CSS variable token system → `12-create-css-token-system.md`
|
||||
- [ ] 13 — Implement system theme detection → `13-implement-system-theme-detection.md`
|
||||
- [ ] 14 — Integrate theme provider into App component → `14-integrate-theme-provider.md`
|
||||
- [ ] 15 — Update components to use theme tokens → `15-update-components-to-use-themes.md`
|
||||
- [x] 09 — Create theme context provider → `09-create-theme-context-provider.md`
|
||||
- [x] 10 — Implement DesktopTheme type and structure → `10-implement-desktop-theme-types.md`
|
||||
- [x] 11 — Implement theme resolution system → `11-implement-theme-resolution.md`
|
||||
- [x] 12 — Create CSS variable token system → `12-create-css-token-system.md`
|
||||
- [x] 13 — Implement system theme detection → `13-implement-system-theme-detection.md`
|
||||
- [x] 14 — Integrate theme provider into App component → `14-integrate-theme-provider.md`
|
||||
- [x] 15 — Update components to use theme tokens → `15-update-components-to-use-themes.md`
|
||||
- [ ] 16 — Test navigation flows and layer transitions → `16-test-navigation-flows.md`
|
||||
- [ ] 17 — Test tab crash fixes and edge cases → `17-test-tab-crash-fixes.md`
|
||||
- [ ] 18 — Test theming system with all modes → `18-test-theming-system.md`
|
||||
|
||||
Reference in New Issue
Block a user