generated from nhcarrigan/template
feat: add custom theme support with color picker UI
- Add Custom variant to Theme enum and CustomThemeColors struct in Rust config - Add custom_theme_colors field to HikariConfig for storing user-defined colors - Create color picker UI in ConfigSidebar with 8 customizable color variables - Implement live preview when editing custom theme colors - Apply custom colors on app startup when custom theme is selected - Use dark theme as base, override with user's custom CSS variables
This commit is contained in:
@@ -92,6 +92,10 @@ pub struct HikariConfig {
|
|||||||
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub profile_bio: Option<String>,
|
pub profile_bio: Option<String>,
|
||||||
|
|
||||||
|
// Custom theme colors
|
||||||
|
#[serde(default)]
|
||||||
|
pub custom_theme_colors: CustomThemeColors,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for HikariConfig {
|
impl Default for HikariConfig {
|
||||||
@@ -118,6 +122,7 @@ impl Default for HikariConfig {
|
|||||||
profile_name: None,
|
profile_name: None,
|
||||||
profile_avatar_path: None,
|
profile_avatar_path: None,
|
||||||
profile_bio: None,
|
profile_bio: None,
|
||||||
|
custom_theme_colors: CustomThemeColors::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -150,6 +155,27 @@ pub enum Theme {
|
|||||||
Light,
|
Light,
|
||||||
#[serde(rename = "high-contrast")]
|
#[serde(rename = "high-contrast")]
|
||||||
HighContrast,
|
HighContrast,
|
||||||
|
Custom,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
|
||||||
|
pub struct CustomThemeColors {
|
||||||
|
#[serde(default)]
|
||||||
|
pub bg_primary: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub bg_secondary: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub bg_terminal: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub accent_primary: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub accent_secondary: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub text_primary: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub text_secondary: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub border_color: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -178,6 +204,7 @@ mod tests {
|
|||||||
assert!(config.profile_name.is_none());
|
assert!(config.profile_name.is_none());
|
||||||
assert!(config.profile_avatar_path.is_none());
|
assert!(config.profile_avatar_path.is_none());
|
||||||
assert!(config.profile_bio.is_none());
|
assert!(config.profile_bio.is_none());
|
||||||
|
assert_eq!(config.custom_theme_colors, CustomThemeColors::default());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -204,6 +231,7 @@ mod tests {
|
|||||||
profile_name: Some("Test User".to_string()),
|
profile_name: Some("Test User".to_string()),
|
||||||
profile_avatar_path: None,
|
profile_avatar_path: None,
|
||||||
profile_bio: Some("A test bio".to_string()),
|
profile_bio: Some("A test bio".to_string()),
|
||||||
|
custom_theme_colors: CustomThemeColors::default(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let json = serde_json::to_string(&config).unwrap();
|
let json = serde_json::to_string(&config).unwrap();
|
||||||
@@ -232,5 +260,8 @@ mod tests {
|
|||||||
serde_json::to_string(&high_contrast).unwrap(),
|
serde_json::to_string(&high_contrast).unwrap(),
|
||||||
"\"high-contrast\""
|
"\"high-contrast\""
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let custom = Theme::Custom;
|
||||||
|
assert_eq!(serde_json::to_string(&custom).unwrap(), "\"custom\"");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,9 @@
|
|||||||
configStore,
|
configStore,
|
||||||
type HikariConfig,
|
type HikariConfig,
|
||||||
type Theme,
|
type Theme,
|
||||||
|
type CustomThemeColors,
|
||||||
applyFontSize,
|
applyFontSize,
|
||||||
|
applyCustomThemeColors,
|
||||||
MIN_FONT_SIZE,
|
MIN_FONT_SIZE,
|
||||||
MAX_FONT_SIZE,
|
MAX_FONT_SIZE,
|
||||||
DEFAULT_FONT_SIZE,
|
DEFAULT_FONT_SIZE,
|
||||||
@@ -33,8 +35,20 @@
|
|||||||
profile_name: null,
|
profile_name: null,
|
||||||
profile_avatar_path: null,
|
profile_avatar_path: null,
|
||||||
profile_bio: null,
|
profile_bio: null,
|
||||||
|
custom_theme_colors: {
|
||||||
|
bg_primary: null,
|
||||||
|
bg_secondary: null,
|
||||||
|
bg_terminal: null,
|
||||||
|
accent_primary: null,
|
||||||
|
accent_secondary: null,
|
||||||
|
text_primary: null,
|
||||||
|
text_secondary: null,
|
||||||
|
border_color: null,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let showCustomThemeEditor = $state(false);
|
||||||
|
|
||||||
let isOpen = $state(false);
|
let isOpen = $state(false);
|
||||||
let isSaving = $state(false);
|
let isSaving = $state(false);
|
||||||
let saveError: string | null = $state(null);
|
let saveError: string | null = $state(null);
|
||||||
@@ -91,9 +105,33 @@
|
|||||||
|
|
||||||
async function handleThemeChange(theme: Theme) {
|
async function handleThemeChange(theme: Theme) {
|
||||||
config.theme = theme;
|
config.theme = theme;
|
||||||
await configStore.setTheme(theme);
|
showCustomThemeEditor = theme === "custom";
|
||||||
|
await configStore.setTheme(theme, config.custom_theme_colors);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleCustomColorChange(key: keyof CustomThemeColors, value: string) {
|
||||||
|
config.custom_theme_colors = {
|
||||||
|
...config.custom_theme_colors,
|
||||||
|
[key]: value || null,
|
||||||
|
};
|
||||||
|
// Live preview
|
||||||
|
if (config.theme === "custom") {
|
||||||
|
applyCustomThemeColors(config.custom_theme_colors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default dark theme colors for color picker defaults
|
||||||
|
const defaultDarkColors: Required<Record<keyof CustomThemeColors, string>> = {
|
||||||
|
bg_primary: "#1a1a2e",
|
||||||
|
bg_secondary: "#16213e",
|
||||||
|
bg_terminal: "#0f0f1a",
|
||||||
|
accent_primary: "#e94560",
|
||||||
|
accent_secondary: "#ff6b9d",
|
||||||
|
text_primary: "#ffffff",
|
||||||
|
text_secondary: "#a0a0a0",
|
||||||
|
border_color: "#2a2a4a",
|
||||||
|
};
|
||||||
|
|
||||||
function toggleTool(tool: string) {
|
function toggleTool(tool: string) {
|
||||||
if (config.auto_granted_tools.includes(tool)) {
|
if (config.auto_granted_tools.includes(tool)) {
|
||||||
config.auto_granted_tools = config.auto_granted_tools.filter((t) => t !== tool);
|
config.auto_granted_tools = config.auto_granted_tools.filter((t) => t !== tool);
|
||||||
@@ -421,10 +459,10 @@
|
|||||||
<!-- Theme Selection -->
|
<!-- Theme Selection -->
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label class="block text-sm text-[var(--text-secondary)] mb-2">Theme</label>
|
<label class="block text-sm text-[var(--text-secondary)] mb-2">Theme</label>
|
||||||
<div class="flex gap-2">
|
<div class="flex flex-wrap gap-2">
|
||||||
<button
|
<button
|
||||||
onclick={() => handleThemeChange("dark")}
|
onclick={() => handleThemeChange("dark")}
|
||||||
class="flex-1 px-3 py-2 rounded-lg border transition-colors {config.theme === 'dark'
|
class="flex-1 min-w-[70px] px-3 py-2 rounded-lg border transition-colors {config.theme === 'dark'
|
||||||
? 'bg-[var(--accent-primary)] border-[var(--accent-primary)] text-white'
|
? 'bg-[var(--accent-primary)] border-[var(--accent-primary)] text-white'
|
||||||
: 'bg-[var(--bg-primary)] border-[var(--border-color)] text-[var(--text-secondary)] hover:border-[var(--accent-primary)]'}"
|
: 'bg-[var(--bg-primary)] border-[var(--border-color)] text-[var(--text-secondary)] hover:border-[var(--accent-primary)]'}"
|
||||||
>
|
>
|
||||||
@@ -432,7 +470,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onclick={() => handleThemeChange("light")}
|
onclick={() => handleThemeChange("light")}
|
||||||
class="flex-1 px-3 py-2 rounded-lg border transition-colors {config.theme === 'light'
|
class="flex-1 min-w-[70px] px-3 py-2 rounded-lg border transition-colors {config.theme === 'light'
|
||||||
? 'bg-[var(--accent-primary)] border-[var(--accent-primary)] text-white'
|
? 'bg-[var(--accent-primary)] border-[var(--accent-primary)] text-white'
|
||||||
: 'bg-[var(--bg-primary)] border-[var(--border-color)] text-[var(--text-secondary)] hover:border-[var(--accent-primary)]'}"
|
: 'bg-[var(--bg-primary)] border-[var(--border-color)] text-[var(--text-secondary)] hover:border-[var(--accent-primary)]'}"
|
||||||
>
|
>
|
||||||
@@ -440,17 +478,150 @@
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onclick={() => handleThemeChange("high-contrast")}
|
onclick={() => handleThemeChange("high-contrast")}
|
||||||
class="flex-1 px-3 py-2 rounded-lg border transition-colors {config.theme ===
|
class="flex-1 min-w-[70px] px-3 py-2 rounded-lg border transition-colors {config.theme ===
|
||||||
'high-contrast'
|
'high-contrast'
|
||||||
? 'bg-[var(--accent-primary)] border-[var(--accent-primary)] text-white'
|
? 'bg-[var(--accent-primary)] border-[var(--accent-primary)] text-white'
|
||||||
: 'bg-[var(--bg-primary)] border-[var(--border-color)] text-[var(--text-secondary)] hover:border-[var(--accent-primary)]'}"
|
: 'bg-[var(--bg-primary)] border-[var(--border-color)] text-[var(--text-secondary)] hover:border-[var(--accent-primary)]'}"
|
||||||
title="High contrast mode for improved accessibility"
|
title="High contrast mode for improved accessibility"
|
||||||
>
|
>
|
||||||
High Contrast
|
Contrast
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onclick={() => handleThemeChange("custom")}
|
||||||
|
class="flex-1 min-w-[70px] px-3 py-2 rounded-lg border transition-colors {config.theme === 'custom'
|
||||||
|
? 'bg-[var(--accent-primary)] border-[var(--accent-primary)] text-white'
|
||||||
|
: 'bg-[var(--bg-primary)] border-[var(--border-color)] text-[var(--text-secondary)] hover:border-[var(--accent-primary)]'}"
|
||||||
|
title="Create your own custom theme"
|
||||||
|
>
|
||||||
|
Custom
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Custom Theme Editor -->
|
||||||
|
{#if config.theme === "custom" || showCustomThemeEditor}
|
||||||
|
<div class="mb-4 p-3 bg-[var(--bg-primary)] rounded-lg border border-[var(--border-color)]">
|
||||||
|
<h4 class="text-sm font-medium text-[var(--text-primary)] mb-3">Custom Theme Colors</h4>
|
||||||
|
<div class="grid grid-cols-2 gap-3">
|
||||||
|
<div class="color-input-group">
|
||||||
|
<label class="text-xs text-[var(--text-secondary)]">Background</label>
|
||||||
|
<div class="flex gap-2 items-center">
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
value={config.custom_theme_colors.bg_primary || defaultDarkColors.bg_primary}
|
||||||
|
oninput={(e) => handleCustomColorChange("bg_primary", e.currentTarget.value)}
|
||||||
|
class="color-picker"
|
||||||
|
/>
|
||||||
|
<span class="text-xs text-[var(--text-tertiary)] font-mono">
|
||||||
|
{config.custom_theme_colors.bg_primary || defaultDarkColors.bg_primary}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="color-input-group">
|
||||||
|
<label class="text-xs text-[var(--text-secondary)]">Secondary BG</label>
|
||||||
|
<div class="flex gap-2 items-center">
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
value={config.custom_theme_colors.bg_secondary || defaultDarkColors.bg_secondary}
|
||||||
|
oninput={(e) => handleCustomColorChange("bg_secondary", e.currentTarget.value)}
|
||||||
|
class="color-picker"
|
||||||
|
/>
|
||||||
|
<span class="text-xs text-[var(--text-tertiary)] font-mono">
|
||||||
|
{config.custom_theme_colors.bg_secondary || defaultDarkColors.bg_secondary}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="color-input-group">
|
||||||
|
<label class="text-xs text-[var(--text-secondary)]">Terminal BG</label>
|
||||||
|
<div class="flex gap-2 items-center">
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
value={config.custom_theme_colors.bg_terminal || defaultDarkColors.bg_terminal}
|
||||||
|
oninput={(e) => handleCustomColorChange("bg_terminal", e.currentTarget.value)}
|
||||||
|
class="color-picker"
|
||||||
|
/>
|
||||||
|
<span class="text-xs text-[var(--text-tertiary)] font-mono">
|
||||||
|
{config.custom_theme_colors.bg_terminal || defaultDarkColors.bg_terminal}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="color-input-group">
|
||||||
|
<label class="text-xs text-[var(--text-secondary)]">Border</label>
|
||||||
|
<div class="flex gap-2 items-center">
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
value={config.custom_theme_colors.border_color || defaultDarkColors.border_color}
|
||||||
|
oninput={(e) => handleCustomColorChange("border_color", e.currentTarget.value)}
|
||||||
|
class="color-picker"
|
||||||
|
/>
|
||||||
|
<span class="text-xs text-[var(--text-tertiary)] font-mono">
|
||||||
|
{config.custom_theme_colors.border_color || defaultDarkColors.border_color}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="color-input-group">
|
||||||
|
<label class="text-xs text-[var(--text-secondary)]">Accent Primary</label>
|
||||||
|
<div class="flex gap-2 items-center">
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
value={config.custom_theme_colors.accent_primary || defaultDarkColors.accent_primary}
|
||||||
|
oninput={(e) => handleCustomColorChange("accent_primary", e.currentTarget.value)}
|
||||||
|
class="color-picker"
|
||||||
|
/>
|
||||||
|
<span class="text-xs text-[var(--text-tertiary)] font-mono">
|
||||||
|
{config.custom_theme_colors.accent_primary || defaultDarkColors.accent_primary}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="color-input-group">
|
||||||
|
<label class="text-xs text-[var(--text-secondary)]">Accent Secondary</label>
|
||||||
|
<div class="flex gap-2 items-center">
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
value={config.custom_theme_colors.accent_secondary || defaultDarkColors.accent_secondary}
|
||||||
|
oninput={(e) => handleCustomColorChange("accent_secondary", e.currentTarget.value)}
|
||||||
|
class="color-picker"
|
||||||
|
/>
|
||||||
|
<span class="text-xs text-[var(--text-tertiary)] font-mono">
|
||||||
|
{config.custom_theme_colors.accent_secondary || defaultDarkColors.accent_secondary}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="color-input-group">
|
||||||
|
<label class="text-xs text-[var(--text-secondary)]">Text Primary</label>
|
||||||
|
<div class="flex gap-2 items-center">
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
value={config.custom_theme_colors.text_primary || defaultDarkColors.text_primary}
|
||||||
|
oninput={(e) => handleCustomColorChange("text_primary", e.currentTarget.value)}
|
||||||
|
class="color-picker"
|
||||||
|
/>
|
||||||
|
<span class="text-xs text-[var(--text-tertiary)] font-mono">
|
||||||
|
{config.custom_theme_colors.text_primary || defaultDarkColors.text_primary}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="color-input-group">
|
||||||
|
<label class="text-xs text-[var(--text-secondary)]">Text Secondary</label>
|
||||||
|
<div class="flex gap-2 items-center">
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
value={config.custom_theme_colors.text_secondary || defaultDarkColors.text_secondary}
|
||||||
|
oninput={(e) => handleCustomColorChange("text_secondary", e.currentTarget.value)}
|
||||||
|
class="color-picker"
|
||||||
|
/>
|
||||||
|
<span class="text-xs text-[var(--text-tertiary)] font-mono">
|
||||||
|
{config.custom_theme_colors.text_secondary || defaultDarkColors.text_secondary}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-[var(--text-tertiary)] mt-3">
|
||||||
|
Changes preview live. Click Save Settings to persist.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<!-- Font Size -->
|
<!-- Font Size -->
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label for="font-size" class="block text-sm text-[var(--text-secondary)] mb-2">
|
<label for="font-size" class="block text-sm text-[var(--text-secondary)] mb-2">
|
||||||
@@ -664,4 +835,39 @@
|
|||||||
background: var(--text-tertiary);
|
background: var(--text-tertiary);
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Color picker styling */
|
||||||
|
.color-input-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-picker {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
padding: 0;
|
||||||
|
border: 2px solid var(--border-color);
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-picker::-webkit-color-swatch-wrapper {
|
||||||
|
padding: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-picker::-webkit-color-swatch {
|
||||||
|
border-radius: 4px;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-picker::-moz-color-swatch {
|
||||||
|
border-radius: 4px;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-picker:hover {
|
||||||
|
border-color: var(--accent-primary);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -63,6 +63,16 @@
|
|||||||
profile_name: null,
|
profile_name: null,
|
||||||
profile_avatar_path: null,
|
profile_avatar_path: null,
|
||||||
profile_bio: null,
|
profile_bio: null,
|
||||||
|
custom_theme_colors: {
|
||||||
|
bg_primary: null,
|
||||||
|
bg_secondary: null,
|
||||||
|
bg_terminal: null,
|
||||||
|
accent_primary: null,
|
||||||
|
accent_secondary: null,
|
||||||
|
text_primary: null,
|
||||||
|
text_secondary: null,
|
||||||
|
border_color: null,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
let streamerModeActive = $state(false);
|
let streamerModeActive = $state(false);
|
||||||
|
|||||||
@@ -1,7 +1,18 @@
|
|||||||
import { writable, derived } from "svelte/store";
|
import { writable, derived } from "svelte/store";
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
|
||||||
export type Theme = "dark" | "light" | "high-contrast";
|
export type Theme = "dark" | "light" | "high-contrast" | "custom";
|
||||||
|
|
||||||
|
export interface CustomThemeColors {
|
||||||
|
bg_primary: string | null;
|
||||||
|
bg_secondary: string | null;
|
||||||
|
bg_terminal: string | null;
|
||||||
|
accent_primary: string | null;
|
||||||
|
accent_secondary: string | null;
|
||||||
|
text_primary: string | null;
|
||||||
|
text_secondary: string | null;
|
||||||
|
border_color: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
export interface HikariConfig {
|
export interface HikariConfig {
|
||||||
model: string | null;
|
model: string | null;
|
||||||
@@ -25,6 +36,7 @@ export interface HikariConfig {
|
|||||||
profile_name: string | null;
|
profile_name: string | null;
|
||||||
profile_avatar_path: string | null;
|
profile_avatar_path: string | null;
|
||||||
profile_bio: string | null;
|
profile_bio: string | null;
|
||||||
|
custom_theme_colors: CustomThemeColors;
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultConfig: HikariConfig = {
|
const defaultConfig: HikariConfig = {
|
||||||
@@ -49,6 +61,16 @@ const defaultConfig: HikariConfig = {
|
|||||||
profile_name: null,
|
profile_name: null,
|
||||||
profile_avatar_path: null,
|
profile_avatar_path: null,
|
||||||
profile_bio: null,
|
profile_bio: null,
|
||||||
|
custom_theme_colors: {
|
||||||
|
bg_primary: null,
|
||||||
|
bg_secondary: null,
|
||||||
|
bg_terminal: null,
|
||||||
|
accent_primary: null,
|
||||||
|
accent_secondary: null,
|
||||||
|
text_primary: null,
|
||||||
|
text_secondary: null,
|
||||||
|
border_color: null,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
function createConfigStore() {
|
function createConfigStore() {
|
||||||
@@ -104,9 +126,24 @@ function createConfigStore() {
|
|||||||
closeSidebar: () => isSidebarOpen.set(false),
|
closeSidebar: () => isSidebarOpen.set(false),
|
||||||
toggleSidebar: () => isSidebarOpen.update((open) => !open),
|
toggleSidebar: () => isSidebarOpen.update((open) => !open),
|
||||||
|
|
||||||
setTheme: async (theme: Theme) => {
|
setTheme: async (theme: Theme, customColors?: CustomThemeColors) => {
|
||||||
await updateConfig({ theme });
|
const updates: Partial<HikariConfig> = { theme };
|
||||||
applyTheme(theme);
|
if (customColors) {
|
||||||
|
updates.custom_theme_colors = customColors;
|
||||||
|
}
|
||||||
|
await updateConfig(updates);
|
||||||
|
let currentConfig: HikariConfig = defaultConfig;
|
||||||
|
config.subscribe((c) => (currentConfig = c))();
|
||||||
|
applyTheme(theme, currentConfig.custom_theme_colors);
|
||||||
|
},
|
||||||
|
|
||||||
|
setCustomThemeColors: async (colors: CustomThemeColors) => {
|
||||||
|
await updateConfig({ custom_theme_colors: colors });
|
||||||
|
let currentConfig: HikariConfig = defaultConfig;
|
||||||
|
config.subscribe((c) => (currentConfig = c))();
|
||||||
|
if (currentConfig.theme === "custom") {
|
||||||
|
applyCustomThemeColors(colors);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
setFontSize: async (size: number) => {
|
setFontSize: async (size: number) => {
|
||||||
@@ -176,12 +213,52 @@ function createConfigStore() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function applyTheme(theme: Theme) {
|
export function applyTheme(theme: Theme, customColors?: CustomThemeColors) {
|
||||||
if (typeof document !== "undefined") {
|
if (typeof document !== "undefined") {
|
||||||
document.documentElement.setAttribute("data-theme", theme);
|
// For custom theme, we use dark as the base and override with custom colors
|
||||||
|
document.documentElement.setAttribute("data-theme", theme === "custom" ? "dark" : theme);
|
||||||
|
|
||||||
|
// Clear any previously applied custom colors
|
||||||
|
clearCustomThemeColors();
|
||||||
|
|
||||||
|
// Apply custom colors if theme is custom
|
||||||
|
if (theme === "custom" && customColors) {
|
||||||
|
applyCustomThemeColors(customColors);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function applyCustomThemeColors(colors: CustomThemeColors) {
|
||||||
|
if (typeof document === "undefined") return;
|
||||||
|
|
||||||
|
const root = document.documentElement;
|
||||||
|
if (colors.bg_primary) root.style.setProperty("--bg-primary", colors.bg_primary);
|
||||||
|
if (colors.bg_secondary) root.style.setProperty("--bg-secondary", colors.bg_secondary);
|
||||||
|
if (colors.bg_terminal) root.style.setProperty("--bg-terminal", colors.bg_terminal);
|
||||||
|
if (colors.accent_primary) root.style.setProperty("--accent-primary", colors.accent_primary);
|
||||||
|
if (colors.accent_secondary) root.style.setProperty("--accent-secondary", colors.accent_secondary);
|
||||||
|
if (colors.text_primary) root.style.setProperty("--text-primary", colors.text_primary);
|
||||||
|
if (colors.text_secondary) root.style.setProperty("--text-secondary", colors.text_secondary);
|
||||||
|
if (colors.border_color) root.style.setProperty("--border-color", colors.border_color);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearCustomThemeColors() {
|
||||||
|
if (typeof document === "undefined") return;
|
||||||
|
|
||||||
|
const root = document.documentElement;
|
||||||
|
const customProperties = [
|
||||||
|
"--bg-primary",
|
||||||
|
"--bg-secondary",
|
||||||
|
"--bg-terminal",
|
||||||
|
"--accent-primary",
|
||||||
|
"--accent-secondary",
|
||||||
|
"--text-primary",
|
||||||
|
"--text-secondary",
|
||||||
|
"--border-color",
|
||||||
|
];
|
||||||
|
customProperties.forEach((prop) => root.style.removeProperty(prop));
|
||||||
|
}
|
||||||
|
|
||||||
const MIN_FONT_SIZE = 10;
|
const MIN_FONT_SIZE = 10;
|
||||||
const MAX_FONT_SIZE = 24;
|
const MAX_FONT_SIZE = 24;
|
||||||
const DEFAULT_FONT_SIZE = 14;
|
const DEFAULT_FONT_SIZE = 14;
|
||||||
|
|||||||
@@ -255,7 +255,7 @@
|
|||||||
|
|
||||||
// Apply saved settings on startup
|
// Apply saved settings on startup
|
||||||
const config = configStore.getConfig();
|
const config = configStore.getConfig();
|
||||||
applyTheme(config.theme);
|
applyTheme(config.theme, config.custom_theme_colors);
|
||||||
applyFontSize(config.font_size);
|
applyFontSize(config.font_size);
|
||||||
|
|
||||||
// Apply always-on-top setting
|
// Apply always-on-top setting
|
||||||
|
|||||||
Reference in New Issue
Block a user