mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-16 07:39:39 +02:00
style: auto-format codebase with Prettier
Applied formatting to 1487+ files using pnpm format:write - TypeScript/JavaScript files - Svelte components - Astro pages - JSON configs - Markdown docs 13 files still need manual review (Astro JSX comments)
This commit is contained in:
parent
0241f5554c
commit
d36b321d9d
3952 changed files with 661498 additions and 739751 deletions
|
|
@ -3,7 +3,12 @@ import type { ThemeVariant, ThemeVariantDefinition, ThemeColors } from './types'
|
|||
/**
|
||||
* All available theme variants
|
||||
*/
|
||||
export const THEME_VARIANTS: readonly ThemeVariant[] = ['lume', 'nature', 'stone', 'ocean'] as const;
|
||||
export const THEME_VARIANTS: readonly ThemeVariant[] = [
|
||||
'lume',
|
||||
'nature',
|
||||
'stone',
|
||||
'ocean',
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* HSL Color Definitions for all theme variants
|
||||
|
|
@ -22,209 +27,209 @@ export const THEME_VARIANTS: readonly ThemeVariant[] = ['lume', 'nature', 'stone
|
|||
*/
|
||||
|
||||
const lumeLight: ThemeColors = {
|
||||
primary: '47 95% 58%', // #f8d62b - Gold
|
||||
primaryForeground: '0 0% 0%', // Black text on gold
|
||||
secondary: '47 100% 41%', // #D4B200 - Darker gold
|
||||
secondaryForeground: '0 0% 0%',
|
||||
background: '0 0% 87%', // #dddddd - Light gray
|
||||
foreground: '0 0% 17%', // #2c2c2c - Dark text
|
||||
surface: '0 0% 100%', // #ffffff - White
|
||||
surfaceHover: '0 0% 96%', // #f5f5f5
|
||||
surfaceElevated: '0 0% 100%', // #ffffff
|
||||
muted: '0 0% 90%', // #e6e6e6
|
||||
mutedForeground: '0 0% 40%', // #666666
|
||||
border: '0 0% 90%', // #e6e6e6
|
||||
borderStrong: '0 0% 80%', // #cccccc
|
||||
error: '6 78% 57%', // #e74c3c
|
||||
success: '145 63% 42%', // #27ae60
|
||||
warning: '36 100% 50%', // #f39c12
|
||||
input: '0 0% 100%', // #ffffff
|
||||
ring: '47 95% 58%', // Same as primary
|
||||
primary: '47 95% 58%', // #f8d62b - Gold
|
||||
primaryForeground: '0 0% 0%', // Black text on gold
|
||||
secondary: '47 100% 41%', // #D4B200 - Darker gold
|
||||
secondaryForeground: '0 0% 0%',
|
||||
background: '0 0% 87%', // #dddddd - Light gray
|
||||
foreground: '0 0% 17%', // #2c2c2c - Dark text
|
||||
surface: '0 0% 100%', // #ffffff - White
|
||||
surfaceHover: '0 0% 96%', // #f5f5f5
|
||||
surfaceElevated: '0 0% 100%', // #ffffff
|
||||
muted: '0 0% 90%', // #e6e6e6
|
||||
mutedForeground: '0 0% 40%', // #666666
|
||||
border: '0 0% 90%', // #e6e6e6
|
||||
borderStrong: '0 0% 80%', // #cccccc
|
||||
error: '6 78% 57%', // #e74c3c
|
||||
success: '145 63% 42%', // #27ae60
|
||||
warning: '36 100% 50%', // #f39c12
|
||||
input: '0 0% 100%', // #ffffff
|
||||
ring: '47 95% 58%', // Same as primary
|
||||
};
|
||||
|
||||
const lumeDark: ThemeColors = {
|
||||
primary: '47 95% 58%', // #f8d62b - Gold (same in dark)
|
||||
primaryForeground: '0 0% 0%', // Black text on gold
|
||||
secondary: '47 70% 29%', // #7C6B16 - Muted gold
|
||||
secondaryForeground: '0 0% 100%',
|
||||
background: '0 0% 6%', // #101010 - Very dark
|
||||
foreground: '0 0% 100%', // #ffffff - White text
|
||||
surface: '0 0% 12%', // #1E1E1E - Dark surface
|
||||
surfaceHover: '0 0% 16%', // #292929
|
||||
surfaceElevated: '0 0% 14%', // #242424
|
||||
muted: '0 0% 20%', // #333333
|
||||
mutedForeground: '0 0% 60%', // #999999
|
||||
border: '0 0% 26%', // #424242
|
||||
borderStrong: '0 0% 35%', // #595959
|
||||
error: '6 78% 57%', // #e74c3c
|
||||
success: '145 63% 49%', // #2ecc71
|
||||
warning: '48 100% 50%', // #f1c40f
|
||||
input: '0 0% 14%', // #242424
|
||||
ring: '47 95% 58%',
|
||||
primary: '47 95% 58%', // #f8d62b - Gold (same in dark)
|
||||
primaryForeground: '0 0% 0%', // Black text on gold
|
||||
secondary: '47 70% 29%', // #7C6B16 - Muted gold
|
||||
secondaryForeground: '0 0% 100%',
|
||||
background: '0 0% 6%', // #101010 - Very dark
|
||||
foreground: '0 0% 100%', // #ffffff - White text
|
||||
surface: '0 0% 12%', // #1E1E1E - Dark surface
|
||||
surfaceHover: '0 0% 16%', // #292929
|
||||
surfaceElevated: '0 0% 14%', // #242424
|
||||
muted: '0 0% 20%', // #333333
|
||||
mutedForeground: '0 0% 60%', // #999999
|
||||
border: '0 0% 26%', // #424242
|
||||
borderStrong: '0 0% 35%', // #595959
|
||||
error: '6 78% 57%', // #e74c3c
|
||||
success: '145 63% 49%', // #2ecc71
|
||||
warning: '48 100% 50%', // #f1c40f
|
||||
input: '0 0% 14%', // #242424
|
||||
ring: '47 95% 58%',
|
||||
};
|
||||
|
||||
const natureLight: ThemeColors = {
|
||||
primary: '122 39% 49%', // #4CAF50 - Green
|
||||
primaryForeground: '0 0% 100%', // White text on green
|
||||
secondary: '122 38% 63%', // #81C784 - Light green
|
||||
secondaryForeground: '0 0% 0%',
|
||||
background: '80 33% 97%', // #FBFDF8 - Very light green tint
|
||||
foreground: '122 56% 24%', // #1B5E20 - Dark green text
|
||||
surface: '0 0% 100%', // #ffffff
|
||||
surfaceHover: '120 25% 95%', // #F1F8E9
|
||||
surfaceElevated: '0 0% 100%',
|
||||
muted: '120 25% 95%', // #F1F8E9
|
||||
mutedForeground: '122 20% 40%',
|
||||
border: '120 25% 91%', // #E8F5E9
|
||||
borderStrong: '120 26% 79%', // #C8E6C9
|
||||
error: '0 65% 67%', // #E57373
|
||||
success: '122 39% 49%', // Same as primary
|
||||
warning: '36 100% 50%',
|
||||
input: '0 0% 100%',
|
||||
ring: '122 39% 49%',
|
||||
primary: '122 39% 49%', // #4CAF50 - Green
|
||||
primaryForeground: '0 0% 100%', // White text on green
|
||||
secondary: '122 38% 63%', // #81C784 - Light green
|
||||
secondaryForeground: '0 0% 0%',
|
||||
background: '80 33% 97%', // #FBFDF8 - Very light green tint
|
||||
foreground: '122 56% 24%', // #1B5E20 - Dark green text
|
||||
surface: '0 0% 100%', // #ffffff
|
||||
surfaceHover: '120 25% 95%', // #F1F8E9
|
||||
surfaceElevated: '0 0% 100%',
|
||||
muted: '120 25% 95%', // #F1F8E9
|
||||
mutedForeground: '122 20% 40%',
|
||||
border: '120 25% 91%', // #E8F5E9
|
||||
borderStrong: '120 26% 79%', // #C8E6C9
|
||||
error: '0 65% 67%', // #E57373
|
||||
success: '122 39% 49%', // Same as primary
|
||||
warning: '36 100% 50%',
|
||||
input: '0 0% 100%',
|
||||
ring: '122 39% 49%',
|
||||
};
|
||||
|
||||
const natureDark: ThemeColors = {
|
||||
primary: '122 39% 49%', // #4CAF50
|
||||
primaryForeground: '0 0% 100%',
|
||||
secondary: '122 30% 35%', // Muted green
|
||||
secondaryForeground: '0 0% 100%',
|
||||
background: '0 0% 7%', // #121212
|
||||
foreground: '0 0% 100%', // White
|
||||
surface: '120 10% 12%', // Slight green tint
|
||||
surfaceHover: '120 10% 16%',
|
||||
surfaceElevated: '120 10% 14%',
|
||||
muted: '120 10% 20%',
|
||||
mutedForeground: '120 10% 60%',
|
||||
border: '120 10% 25%',
|
||||
borderStrong: '120 10% 35%',
|
||||
error: '0 65% 57%',
|
||||
success: '122 50% 55%',
|
||||
warning: '48 100% 50%',
|
||||
input: '120 10% 14%',
|
||||
ring: '122 39% 49%',
|
||||
primary: '122 39% 49%', // #4CAF50
|
||||
primaryForeground: '0 0% 100%',
|
||||
secondary: '122 30% 35%', // Muted green
|
||||
secondaryForeground: '0 0% 100%',
|
||||
background: '0 0% 7%', // #121212
|
||||
foreground: '0 0% 100%', // White
|
||||
surface: '120 10% 12%', // Slight green tint
|
||||
surfaceHover: '120 10% 16%',
|
||||
surfaceElevated: '120 10% 14%',
|
||||
muted: '120 10% 20%',
|
||||
mutedForeground: '120 10% 60%',
|
||||
border: '120 10% 25%',
|
||||
borderStrong: '120 10% 35%',
|
||||
error: '0 65% 57%',
|
||||
success: '122 50% 55%',
|
||||
warning: '48 100% 50%',
|
||||
input: '120 10% 14%',
|
||||
ring: '122 39% 49%',
|
||||
};
|
||||
|
||||
const stoneLight: ThemeColors = {
|
||||
primary: '200 18% 46%', // #607D8B - Blue gray
|
||||
primaryForeground: '0 0% 100%',
|
||||
secondary: '200 15% 62%', // #90A4AE - Light slate
|
||||
secondaryForeground: '0 0% 0%',
|
||||
background: '210 17% 97%', // #F5F7F9 - Very light blue gray
|
||||
foreground: '200 19% 18%', // #263238 - Dark slate
|
||||
surface: '0 0% 100%',
|
||||
surfaceHover: '200 10% 94%', // #ECEFF1
|
||||
surfaceElevated: '0 0% 100%',
|
||||
muted: '200 10% 94%', // #ECEFF1
|
||||
mutedForeground: '200 10% 45%',
|
||||
border: '200 10% 88%', // #CFD8DC
|
||||
borderStrong: '200 12% 75%', // #B0BEC5
|
||||
error: '4 90% 63%', // #EF5350
|
||||
success: '145 63% 42%',
|
||||
warning: '36 100% 50%',
|
||||
input: '0 0% 100%',
|
||||
ring: '200 18% 46%',
|
||||
primary: '200 18% 46%', // #607D8B - Blue gray
|
||||
primaryForeground: '0 0% 100%',
|
||||
secondary: '200 15% 62%', // #90A4AE - Light slate
|
||||
secondaryForeground: '0 0% 0%',
|
||||
background: '210 17% 97%', // #F5F7F9 - Very light blue gray
|
||||
foreground: '200 19% 18%', // #263238 - Dark slate
|
||||
surface: '0 0% 100%',
|
||||
surfaceHover: '200 10% 94%', // #ECEFF1
|
||||
surfaceElevated: '0 0% 100%',
|
||||
muted: '200 10% 94%', // #ECEFF1
|
||||
mutedForeground: '200 10% 45%',
|
||||
border: '200 10% 88%', // #CFD8DC
|
||||
borderStrong: '200 12% 75%', // #B0BEC5
|
||||
error: '4 90% 63%', // #EF5350
|
||||
success: '145 63% 42%',
|
||||
warning: '36 100% 50%',
|
||||
input: '0 0% 100%',
|
||||
ring: '200 18% 46%',
|
||||
};
|
||||
|
||||
const stoneDark: ThemeColors = {
|
||||
primary: '200 15% 52%', // #78909C - Lighter in dark mode
|
||||
primaryForeground: '0 0% 0%',
|
||||
secondary: '200 12% 35%',
|
||||
secondaryForeground: '0 0% 100%',
|
||||
background: '0 0% 7%', // #121212
|
||||
foreground: '0 0% 100%',
|
||||
surface: '200 10% 12%',
|
||||
surfaceHover: '200 10% 16%',
|
||||
surfaceElevated: '200 10% 14%',
|
||||
muted: '200 10% 20%',
|
||||
mutedForeground: '200 10% 60%',
|
||||
border: '200 10% 25%',
|
||||
borderStrong: '200 10% 35%',
|
||||
error: '4 90% 58%',
|
||||
success: '145 63% 49%',
|
||||
warning: '48 100% 50%',
|
||||
input: '200 10% 14%',
|
||||
ring: '200 15% 52%',
|
||||
primary: '200 15% 52%', // #78909C - Lighter in dark mode
|
||||
primaryForeground: '0 0% 0%',
|
||||
secondary: '200 12% 35%',
|
||||
secondaryForeground: '0 0% 100%',
|
||||
background: '0 0% 7%', // #121212
|
||||
foreground: '0 0% 100%',
|
||||
surface: '200 10% 12%',
|
||||
surfaceHover: '200 10% 16%',
|
||||
surfaceElevated: '200 10% 14%',
|
||||
muted: '200 10% 20%',
|
||||
mutedForeground: '200 10% 60%',
|
||||
border: '200 10% 25%',
|
||||
borderStrong: '200 10% 35%',
|
||||
error: '4 90% 58%',
|
||||
success: '145 63% 49%',
|
||||
warning: '48 100% 50%',
|
||||
input: '200 10% 14%',
|
||||
ring: '200 15% 52%',
|
||||
};
|
||||
|
||||
const oceanLight: ThemeColors = {
|
||||
primary: '199 98% 45%', // #039BE5 - Bright blue
|
||||
primaryForeground: '0 0% 100%',
|
||||
secondary: '199 92% 64%', // #4FC3F7 - Light blue
|
||||
secondaryForeground: '0 0% 0%',
|
||||
background: '199 100% 97%', // #F5FCFF - Very light blue
|
||||
foreground: '199 100% 18%', // #01579B - Dark blue
|
||||
surface: '0 0% 100%',
|
||||
surfaceHover: '199 100% 94%', // #E1F5FE
|
||||
surfaceElevated: '0 0% 100%',
|
||||
muted: '199 100% 94%', // #E1F5FE
|
||||
mutedForeground: '199 50% 40%',
|
||||
border: '199 71% 87%', // #B3E5FC
|
||||
borderStrong: '199 79% 76%', // #81D4FA
|
||||
error: '4 90% 63%', // #EF5350
|
||||
success: '145 63% 42%',
|
||||
warning: '36 100% 50%',
|
||||
input: '0 0% 100%',
|
||||
ring: '199 98% 45%',
|
||||
primary: '199 98% 45%', // #039BE5 - Bright blue
|
||||
primaryForeground: '0 0% 100%',
|
||||
secondary: '199 92% 64%', // #4FC3F7 - Light blue
|
||||
secondaryForeground: '0 0% 0%',
|
||||
background: '199 100% 97%', // #F5FCFF - Very light blue
|
||||
foreground: '199 100% 18%', // #01579B - Dark blue
|
||||
surface: '0 0% 100%',
|
||||
surfaceHover: '199 100% 94%', // #E1F5FE
|
||||
surfaceElevated: '0 0% 100%',
|
||||
muted: '199 100% 94%', // #E1F5FE
|
||||
mutedForeground: '199 50% 40%',
|
||||
border: '199 71% 87%', // #B3E5FC
|
||||
borderStrong: '199 79% 76%', // #81D4FA
|
||||
error: '4 90% 63%', // #EF5350
|
||||
success: '145 63% 42%',
|
||||
warning: '36 100% 50%',
|
||||
input: '0 0% 100%',
|
||||
ring: '199 98% 45%',
|
||||
};
|
||||
|
||||
const oceanDark: ThemeColors = {
|
||||
primary: '199 98% 48%', // Slightly brighter in dark
|
||||
primaryForeground: '0 0% 0%',
|
||||
secondary: '199 60% 35%',
|
||||
secondaryForeground: '0 0% 100%',
|
||||
background: '0 0% 7%', // #121212
|
||||
foreground: '0 0% 100%',
|
||||
surface: '199 30% 12%', // Slight blue tint
|
||||
surfaceHover: '199 30% 16%',
|
||||
surfaceElevated: '199 30% 14%',
|
||||
muted: '199 20% 20%',
|
||||
mutedForeground: '199 20% 60%',
|
||||
border: '199 20% 25%',
|
||||
borderStrong: '199 20% 35%',
|
||||
error: '4 90% 58%',
|
||||
success: '145 63% 49%',
|
||||
warning: '48 100% 50%',
|
||||
input: '199 30% 14%',
|
||||
ring: '199 98% 48%',
|
||||
primary: '199 98% 48%', // Slightly brighter in dark
|
||||
primaryForeground: '0 0% 0%',
|
||||
secondary: '199 60% 35%',
|
||||
secondaryForeground: '0 0% 100%',
|
||||
background: '0 0% 7%', // #121212
|
||||
foreground: '0 0% 100%',
|
||||
surface: '199 30% 12%', // Slight blue tint
|
||||
surfaceHover: '199 30% 16%',
|
||||
surfaceElevated: '199 30% 14%',
|
||||
muted: '199 20% 20%',
|
||||
mutedForeground: '199 20% 60%',
|
||||
border: '199 20% 25%',
|
||||
borderStrong: '199 20% 35%',
|
||||
error: '4 90% 58%',
|
||||
success: '145 63% 49%',
|
||||
warning: '48 100% 50%',
|
||||
input: '199 30% 14%',
|
||||
ring: '199 98% 48%',
|
||||
};
|
||||
|
||||
/**
|
||||
* Complete theme variant definitions
|
||||
*/
|
||||
export const THEME_DEFINITIONS: Record<ThemeVariant, ThemeVariantDefinition> = {
|
||||
lume: {
|
||||
name: 'lume',
|
||||
label: 'Lume',
|
||||
emoji: '✨',
|
||||
hue: 47,
|
||||
light: lumeLight,
|
||||
dark: lumeDark,
|
||||
},
|
||||
nature: {
|
||||
name: 'nature',
|
||||
label: 'Nature',
|
||||
emoji: '🌿',
|
||||
hue: 122,
|
||||
light: natureLight,
|
||||
dark: natureDark,
|
||||
},
|
||||
stone: {
|
||||
name: 'stone',
|
||||
label: 'Stone',
|
||||
emoji: '🪨',
|
||||
hue: 200,
|
||||
light: stoneLight,
|
||||
dark: stoneDark,
|
||||
},
|
||||
ocean: {
|
||||
name: 'ocean',
|
||||
label: 'Ocean',
|
||||
emoji: '🌊',
|
||||
hue: 199,
|
||||
light: oceanLight,
|
||||
dark: oceanDark,
|
||||
},
|
||||
lume: {
|
||||
name: 'lume',
|
||||
label: 'Lume',
|
||||
emoji: '✨',
|
||||
hue: 47,
|
||||
light: lumeLight,
|
||||
dark: lumeDark,
|
||||
},
|
||||
nature: {
|
||||
name: 'nature',
|
||||
label: 'Nature',
|
||||
emoji: '🌿',
|
||||
hue: 122,
|
||||
light: natureLight,
|
||||
dark: natureDark,
|
||||
},
|
||||
stone: {
|
||||
name: 'stone',
|
||||
label: 'Stone',
|
||||
emoji: '🪨',
|
||||
hue: 200,
|
||||
light: stoneLight,
|
||||
dark: stoneDark,
|
||||
},
|
||||
ocean: {
|
||||
name: 'ocean',
|
||||
label: 'Ocean',
|
||||
emoji: '🌊',
|
||||
hue: 199,
|
||||
light: oceanLight,
|
||||
dark: oceanDark,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,24 +1,24 @@
|
|||
// Types
|
||||
export type {
|
||||
ThemeMode,
|
||||
ThemeVariant,
|
||||
EffectiveMode,
|
||||
ThemeState,
|
||||
ThemeColors,
|
||||
ThemeVariantDefinition,
|
||||
AppThemeConfig,
|
||||
ThemeStore,
|
||||
HSLValue,
|
||||
ThemeMode,
|
||||
ThemeVariant,
|
||||
EffectiveMode,
|
||||
ThemeState,
|
||||
ThemeColors,
|
||||
ThemeVariantDefinition,
|
||||
AppThemeConfig,
|
||||
ThemeStore,
|
||||
HSLValue,
|
||||
} from './types';
|
||||
|
||||
// Constants
|
||||
export {
|
||||
THEME_VARIANTS,
|
||||
THEME_DEFINITIONS,
|
||||
DEFAULT_MODE,
|
||||
DEFAULT_VARIANT,
|
||||
CSS_VAR_PREFIX,
|
||||
STORAGE_KEY_SUFFIX,
|
||||
THEME_VARIANTS,
|
||||
THEME_DEFINITIONS,
|
||||
DEFAULT_MODE,
|
||||
DEFAULT_VARIANT,
|
||||
CSS_VAR_PREFIX,
|
||||
STORAGE_KEY_SUFFIX,
|
||||
} from './constants';
|
||||
|
||||
// Store
|
||||
|
|
@ -26,18 +26,18 @@ export { createThemeStore, APP_THEME_CONFIGS } from './store.svelte';
|
|||
|
||||
// Utils
|
||||
export {
|
||||
isBrowser,
|
||||
getSystemPreference,
|
||||
createSystemPreferenceListener,
|
||||
getThemeColors,
|
||||
colorsToCssVars,
|
||||
applyThemeToDocument,
|
||||
loadThemeFromStorage,
|
||||
saveThemeToStorage,
|
||||
parseHSL,
|
||||
createHSL,
|
||||
adjustLightness,
|
||||
adjustSaturation,
|
||||
getContrastColor,
|
||||
generateThemeCSS,
|
||||
isBrowser,
|
||||
getSystemPreference,
|
||||
createSystemPreferenceListener,
|
||||
getThemeColors,
|
||||
colorsToCssVars,
|
||||
applyThemeToDocument,
|
||||
loadThemeFromStorage,
|
||||
saveThemeToStorage,
|
||||
parseHSL,
|
||||
createHSL,
|
||||
adjustLightness,
|
||||
adjustSaturation,
|
||||
getContrastColor,
|
||||
generateThemeCSS,
|
||||
} from './utils';
|
||||
|
|
|
|||
|
|
@ -1,24 +1,19 @@
|
|||
import type {
|
||||
ThemeMode,
|
||||
ThemeVariant,
|
||||
EffectiveMode,
|
||||
ThemeStore,
|
||||
AppThemeConfig,
|
||||
HSLValue,
|
||||
ThemeMode,
|
||||
ThemeVariant,
|
||||
EffectiveMode,
|
||||
ThemeStore,
|
||||
AppThemeConfig,
|
||||
HSLValue,
|
||||
} from './types';
|
||||
import { THEME_VARIANTS, DEFAULT_MODE, DEFAULT_VARIANT, STORAGE_KEY_SUFFIX } from './constants';
|
||||
import {
|
||||
THEME_VARIANTS,
|
||||
DEFAULT_MODE,
|
||||
DEFAULT_VARIANT,
|
||||
STORAGE_KEY_SUFFIX,
|
||||
} from './constants';
|
||||
import {
|
||||
isBrowser,
|
||||
getSystemPreference,
|
||||
createSystemPreferenceListener,
|
||||
applyThemeToDocument,
|
||||
loadThemeFromStorage,
|
||||
saveThemeToStorage,
|
||||
isBrowser,
|
||||
getSystemPreference,
|
||||
createSystemPreferenceListener,
|
||||
applyThemeToDocument,
|
||||
loadThemeFromStorage,
|
||||
saveThemeToStorage,
|
||||
} from './utils';
|
||||
|
||||
/**
|
||||
|
|
@ -42,186 +37,186 @@ import {
|
|||
* ```
|
||||
*/
|
||||
export function createThemeStore(config: AppThemeConfig): ThemeStore {
|
||||
const {
|
||||
appId,
|
||||
defaultMode = DEFAULT_MODE,
|
||||
defaultVariant = DEFAULT_VARIANT,
|
||||
primaryColor,
|
||||
} = config;
|
||||
const {
|
||||
appId,
|
||||
defaultMode = DEFAULT_MODE,
|
||||
defaultVariant = DEFAULT_VARIANT,
|
||||
primaryColor,
|
||||
} = config;
|
||||
|
||||
const storageKey = `${appId}${STORAGE_KEY_SUFFIX}`;
|
||||
const storageKey = `${appId}${STORAGE_KEY_SUFFIX}`;
|
||||
|
||||
// Svelte 5 runes state
|
||||
let mode = $state<ThemeMode>(defaultMode);
|
||||
let variant = $state<ThemeVariant>(defaultVariant);
|
||||
let effectiveMode = $state<EffectiveMode>('light');
|
||||
// Svelte 5 runes state
|
||||
let mode = $state<ThemeMode>(defaultMode);
|
||||
let variant = $state<ThemeVariant>(defaultVariant);
|
||||
let effectiveMode = $state<EffectiveMode>('light');
|
||||
|
||||
// Derived state
|
||||
const isDark = $derived(effectiveMode === 'dark');
|
||||
// Derived state
|
||||
const isDark = $derived(effectiveMode === 'dark');
|
||||
|
||||
/**
|
||||
* Calculate effective mode from current mode and system preference
|
||||
*/
|
||||
function calculateEffectiveMode(currentMode: ThemeMode): EffectiveMode {
|
||||
if (currentMode === 'system') {
|
||||
return getSystemPreference();
|
||||
}
|
||||
return currentMode;
|
||||
}
|
||||
/**
|
||||
* Calculate effective mode from current mode and system preference
|
||||
*/
|
||||
function calculateEffectiveMode(currentMode: ThemeMode): EffectiveMode {
|
||||
if (currentMode === 'system') {
|
||||
return getSystemPreference();
|
||||
}
|
||||
return currentMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply current theme to document and save to storage
|
||||
*/
|
||||
function applyTheme(): void {
|
||||
const newEffectiveMode = calculateEffectiveMode(mode);
|
||||
effectiveMode = newEffectiveMode;
|
||||
/**
|
||||
* Apply current theme to document and save to storage
|
||||
*/
|
||||
function applyTheme(): void {
|
||||
const newEffectiveMode = calculateEffectiveMode(mode);
|
||||
effectiveMode = newEffectiveMode;
|
||||
|
||||
applyThemeToDocument(variant, newEffectiveMode, primaryColor);
|
||||
saveThemeToStorage(storageKey, mode, variant);
|
||||
}
|
||||
applyThemeToDocument(variant, newEffectiveMode, primaryColor);
|
||||
saveThemeToStorage(storageKey, mode, variant);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set theme mode
|
||||
*/
|
||||
function setMode(newMode: ThemeMode): void {
|
||||
if (newMode === mode) return;
|
||||
mode = newMode;
|
||||
applyTheme();
|
||||
}
|
||||
/**
|
||||
* Set theme mode
|
||||
*/
|
||||
function setMode(newMode: ThemeMode): void {
|
||||
if (newMode === mode) return;
|
||||
mode = newMode;
|
||||
applyTheme();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set theme variant
|
||||
*/
|
||||
function setVariant(newVariant: ThemeVariant): void {
|
||||
if (!THEME_VARIANTS.includes(newVariant)) {
|
||||
console.warn(`Invalid theme variant: ${newVariant}`);
|
||||
return;
|
||||
}
|
||||
if (newVariant === variant) return;
|
||||
variant = newVariant;
|
||||
applyTheme();
|
||||
}
|
||||
/**
|
||||
* Set theme variant
|
||||
*/
|
||||
function setVariant(newVariant: ThemeVariant): void {
|
||||
if (!THEME_VARIANTS.includes(newVariant)) {
|
||||
console.warn(`Invalid theme variant: ${newVariant}`);
|
||||
return;
|
||||
}
|
||||
if (newVariant === variant) return;
|
||||
variant = newVariant;
|
||||
applyTheme();
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle between light and dark mode
|
||||
* If currently on system, switches to opposite of effective mode
|
||||
*/
|
||||
function toggleMode(): void {
|
||||
if (mode === 'system') {
|
||||
// Switch to opposite of current effective mode
|
||||
setMode(effectiveMode === 'dark' ? 'light' : 'dark');
|
||||
} else {
|
||||
setMode(mode === 'dark' ? 'light' : 'dark');
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Toggle between light and dark mode
|
||||
* If currently on system, switches to opposite of effective mode
|
||||
*/
|
||||
function toggleMode(): void {
|
||||
if (mode === 'system') {
|
||||
// Switch to opposite of current effective mode
|
||||
setMode(effectiveMode === 'dark' ? 'light' : 'dark');
|
||||
} else {
|
||||
setMode(mode === 'dark' ? 'light' : 'dark');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cycle through modes: light → dark → system → light
|
||||
*/
|
||||
function cycleMode(): void {
|
||||
const modes: ThemeMode[] = ['light', 'dark', 'system'];
|
||||
const currentIndex = modes.indexOf(mode);
|
||||
const nextIndex = (currentIndex + 1) % modes.length;
|
||||
setMode(modes[nextIndex]);
|
||||
}
|
||||
/**
|
||||
* Cycle through modes: light → dark → system → light
|
||||
*/
|
||||
function cycleMode(): void {
|
||||
const modes: ThemeMode[] = ['light', 'dark', 'system'];
|
||||
const currentIndex = modes.indexOf(mode);
|
||||
const nextIndex = (currentIndex + 1) % modes.length;
|
||||
setMode(modes[nextIndex]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize theme store
|
||||
* - Loads saved preferences from localStorage
|
||||
* - Sets up system preference listener
|
||||
* - Applies initial theme
|
||||
*
|
||||
* @returns Cleanup function to remove listeners
|
||||
*/
|
||||
function initialize(): () => void {
|
||||
if (!isBrowser()) {
|
||||
return () => {};
|
||||
}
|
||||
/**
|
||||
* Initialize theme store
|
||||
* - Loads saved preferences from localStorage
|
||||
* - Sets up system preference listener
|
||||
* - Applies initial theme
|
||||
*
|
||||
* @returns Cleanup function to remove listeners
|
||||
*/
|
||||
function initialize(): () => void {
|
||||
if (!isBrowser()) {
|
||||
return () => {};
|
||||
}
|
||||
|
||||
// Load saved preferences
|
||||
const saved = loadThemeFromStorage(storageKey);
|
||||
if (saved) {
|
||||
if (saved.mode && ['light', 'dark', 'system'].includes(saved.mode)) {
|
||||
mode = saved.mode as ThemeMode;
|
||||
}
|
||||
if (saved.variant && THEME_VARIANTS.includes(saved.variant as ThemeVariant)) {
|
||||
variant = saved.variant as ThemeVariant;
|
||||
}
|
||||
}
|
||||
// Load saved preferences
|
||||
const saved = loadThemeFromStorage(storageKey);
|
||||
if (saved) {
|
||||
if (saved.mode && ['light', 'dark', 'system'].includes(saved.mode)) {
|
||||
mode = saved.mode as ThemeMode;
|
||||
}
|
||||
if (saved.variant && THEME_VARIANTS.includes(saved.variant as ThemeVariant)) {
|
||||
variant = saved.variant as ThemeVariant;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply initial theme
|
||||
applyTheme();
|
||||
// Apply initial theme
|
||||
applyTheme();
|
||||
|
||||
// Listen for system preference changes
|
||||
const cleanup = createSystemPreferenceListener((isDark) => {
|
||||
if (mode === 'system') {
|
||||
effectiveMode = isDark ? 'dark' : 'light';
|
||||
applyThemeToDocument(variant, effectiveMode, primaryColor);
|
||||
}
|
||||
});
|
||||
// Listen for system preference changes
|
||||
const cleanup = createSystemPreferenceListener((isDark) => {
|
||||
if (mode === 'system') {
|
||||
effectiveMode = isDark ? 'dark' : 'light';
|
||||
applyThemeToDocument(variant, effectiveMode, primaryColor);
|
||||
}
|
||||
});
|
||||
|
||||
return cleanup;
|
||||
}
|
||||
return cleanup;
|
||||
}
|
||||
|
||||
return {
|
||||
get mode() {
|
||||
return mode;
|
||||
},
|
||||
get variant() {
|
||||
return variant;
|
||||
},
|
||||
get effectiveMode() {
|
||||
return effectiveMode;
|
||||
},
|
||||
get isDark() {
|
||||
return isDark;
|
||||
},
|
||||
get variants() {
|
||||
return THEME_VARIANTS;
|
||||
},
|
||||
return {
|
||||
get mode() {
|
||||
return mode;
|
||||
},
|
||||
get variant() {
|
||||
return variant;
|
||||
},
|
||||
get effectiveMode() {
|
||||
return effectiveMode;
|
||||
},
|
||||
get isDark() {
|
||||
return isDark;
|
||||
},
|
||||
get variants() {
|
||||
return THEME_VARIANTS;
|
||||
},
|
||||
|
||||
setMode,
|
||||
setVariant,
|
||||
toggleMode,
|
||||
cycleMode,
|
||||
initialize,
|
||||
};
|
||||
setMode,
|
||||
setVariant,
|
||||
toggleMode,
|
||||
cycleMode,
|
||||
initialize,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-defined app configurations for convenience
|
||||
*/
|
||||
export const APP_THEME_CONFIGS = {
|
||||
memoro: {
|
||||
appId: 'memoro',
|
||||
defaultVariant: 'lume' as ThemeVariant,
|
||||
primaryColor: {
|
||||
light: '47 95% 58%' as HSLValue, // Gold #f8d62b
|
||||
dark: '47 95% 58%' as HSLValue,
|
||||
},
|
||||
},
|
||||
manacore: {
|
||||
appId: 'manacore',
|
||||
defaultVariant: 'ocean' as ThemeVariant,
|
||||
primaryColor: {
|
||||
light: '239 84% 67%' as HSLValue, // Indigo #6366f1
|
||||
dark: '239 84% 67%' as HSLValue,
|
||||
},
|
||||
},
|
||||
manadeck: {
|
||||
appId: 'manadeck',
|
||||
defaultVariant: 'ocean' as ThemeVariant,
|
||||
primaryColor: {
|
||||
light: '239 84% 67%' as HSLValue, // Indigo #6366f1
|
||||
dark: '239 84% 67%' as HSLValue,
|
||||
},
|
||||
},
|
||||
maerchenzauber: {
|
||||
appId: 'maerchenzauber',
|
||||
defaultVariant: 'nature' as ThemeVariant,
|
||||
primaryColor: {
|
||||
light: '280 60% 55%' as HSLValue, // Purple (storytelling magic)
|
||||
dark: '280 60% 60%' as HSLValue,
|
||||
},
|
||||
},
|
||||
memoro: {
|
||||
appId: 'memoro',
|
||||
defaultVariant: 'lume' as ThemeVariant,
|
||||
primaryColor: {
|
||||
light: '47 95% 58%' as HSLValue, // Gold #f8d62b
|
||||
dark: '47 95% 58%' as HSLValue,
|
||||
},
|
||||
},
|
||||
manacore: {
|
||||
appId: 'manacore',
|
||||
defaultVariant: 'ocean' as ThemeVariant,
|
||||
primaryColor: {
|
||||
light: '239 84% 67%' as HSLValue, // Indigo #6366f1
|
||||
dark: '239 84% 67%' as HSLValue,
|
||||
},
|
||||
},
|
||||
manadeck: {
|
||||
appId: 'manadeck',
|
||||
defaultVariant: 'ocean' as ThemeVariant,
|
||||
primaryColor: {
|
||||
light: '239 84% 67%' as HSLValue, // Indigo #6366f1
|
||||
dark: '239 84% 67%' as HSLValue,
|
||||
},
|
||||
},
|
||||
maerchenzauber: {
|
||||
appId: 'maerchenzauber',
|
||||
defaultVariant: 'nature' as ThemeVariant,
|
||||
primaryColor: {
|
||||
light: '280 60% 55%' as HSLValue, // Purple (storytelling magic)
|
||||
dark: '280 60% 60%' as HSLValue,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
|
|
|||
|
|
@ -18,9 +18,9 @@ export type EffectiveMode = 'light' | 'dark';
|
|||
* Complete theme state
|
||||
*/
|
||||
export interface ThemeState {
|
||||
mode: ThemeMode;
|
||||
variant: ThemeVariant;
|
||||
effectiveMode: EffectiveMode;
|
||||
mode: ThemeMode;
|
||||
variant: ThemeVariant;
|
||||
effectiveMode: EffectiveMode;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -33,101 +33,101 @@ export type HSLValue = string;
|
|||
* Theme color definition using HSL values
|
||||
*/
|
||||
export interface ThemeColors {
|
||||
/** Primary brand color */
|
||||
primary: HSLValue;
|
||||
/** Primary color for text on primary background */
|
||||
primaryForeground: HSLValue;
|
||||
/** Secondary accent color */
|
||||
secondary: HSLValue;
|
||||
/** Secondary foreground */
|
||||
secondaryForeground: HSLValue;
|
||||
/** Page background */
|
||||
background: HSLValue;
|
||||
/** Main text color */
|
||||
foreground: HSLValue;
|
||||
/** Card/content surface */
|
||||
surface: HSLValue;
|
||||
/** Surface hover state */
|
||||
surfaceHover: HSLValue;
|
||||
/** Elevated surface (modals, dropdowns) */
|
||||
surfaceElevated: HSLValue;
|
||||
/** Muted/disabled elements */
|
||||
muted: HSLValue;
|
||||
/** Muted text */
|
||||
mutedForeground: HSLValue;
|
||||
/** Border color */
|
||||
border: HSLValue;
|
||||
/** Strong border (focus, active) */
|
||||
borderStrong: HSLValue;
|
||||
/** Error/destructive color */
|
||||
error: HSLValue;
|
||||
/** Success color */
|
||||
success: HSLValue;
|
||||
/** Warning color */
|
||||
warning: HSLValue;
|
||||
/** Input background */
|
||||
input: HSLValue;
|
||||
/** Focus ring color */
|
||||
ring: HSLValue;
|
||||
/** Primary brand color */
|
||||
primary: HSLValue;
|
||||
/** Primary color for text on primary background */
|
||||
primaryForeground: HSLValue;
|
||||
/** Secondary accent color */
|
||||
secondary: HSLValue;
|
||||
/** Secondary foreground */
|
||||
secondaryForeground: HSLValue;
|
||||
/** Page background */
|
||||
background: HSLValue;
|
||||
/** Main text color */
|
||||
foreground: HSLValue;
|
||||
/** Card/content surface */
|
||||
surface: HSLValue;
|
||||
/** Surface hover state */
|
||||
surfaceHover: HSLValue;
|
||||
/** Elevated surface (modals, dropdowns) */
|
||||
surfaceElevated: HSLValue;
|
||||
/** Muted/disabled elements */
|
||||
muted: HSLValue;
|
||||
/** Muted text */
|
||||
mutedForeground: HSLValue;
|
||||
/** Border color */
|
||||
border: HSLValue;
|
||||
/** Strong border (focus, active) */
|
||||
borderStrong: HSLValue;
|
||||
/** Error/destructive color */
|
||||
error: HSLValue;
|
||||
/** Success color */
|
||||
success: HSLValue;
|
||||
/** Warning color */
|
||||
warning: HSLValue;
|
||||
/** Input background */
|
||||
input: HSLValue;
|
||||
/** Focus ring color */
|
||||
ring: HSLValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Theme variant definition with light and dark mode colors
|
||||
*/
|
||||
export interface ThemeVariantDefinition {
|
||||
name: string;
|
||||
label: string;
|
||||
emoji: string;
|
||||
/** The primary hue for this variant (used for accent colors) */
|
||||
hue: number;
|
||||
light: ThemeColors;
|
||||
dark: ThemeColors;
|
||||
name: string;
|
||||
label: string;
|
||||
emoji: string;
|
||||
/** The primary hue for this variant (used for accent colors) */
|
||||
hue: number;
|
||||
light: ThemeColors;
|
||||
dark: ThemeColors;
|
||||
}
|
||||
|
||||
/**
|
||||
* App-specific theme configuration
|
||||
*/
|
||||
export interface AppThemeConfig {
|
||||
/** App identifier for localStorage key */
|
||||
appId: string;
|
||||
/** Default theme mode */
|
||||
defaultMode?: ThemeMode;
|
||||
/** Default theme variant */
|
||||
defaultVariant?: ThemeVariant;
|
||||
/**
|
||||
* App-specific primary color override (HSL value)
|
||||
* This allows each app to have its own brand color
|
||||
* while sharing the same theme variants
|
||||
*/
|
||||
primaryColor?: {
|
||||
light: HSLValue;
|
||||
dark: HSLValue;
|
||||
};
|
||||
/** App identifier for localStorage key */
|
||||
appId: string;
|
||||
/** Default theme mode */
|
||||
defaultMode?: ThemeMode;
|
||||
/** Default theme variant */
|
||||
defaultVariant?: ThemeVariant;
|
||||
/**
|
||||
* App-specific primary color override (HSL value)
|
||||
* This allows each app to have its own brand color
|
||||
* while sharing the same theme variants
|
||||
*/
|
||||
primaryColor?: {
|
||||
light: HSLValue;
|
||||
dark: HSLValue;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Theme store interface
|
||||
*/
|
||||
export interface ThemeStore {
|
||||
/** Current theme mode (user preference) */
|
||||
readonly mode: ThemeMode;
|
||||
/** Current theme variant */
|
||||
readonly variant: ThemeVariant;
|
||||
/** Effective mode (resolved from system) */
|
||||
readonly effectiveMode: EffectiveMode;
|
||||
/** Whether dark mode is active */
|
||||
readonly isDark: boolean;
|
||||
/** All available variants */
|
||||
readonly variants: readonly ThemeVariant[];
|
||||
/** Current theme mode (user preference) */
|
||||
readonly mode: ThemeMode;
|
||||
/** Current theme variant */
|
||||
readonly variant: ThemeVariant;
|
||||
/** Effective mode (resolved from system) */
|
||||
readonly effectiveMode: EffectiveMode;
|
||||
/** Whether dark mode is active */
|
||||
readonly isDark: boolean;
|
||||
/** All available variants */
|
||||
readonly variants: readonly ThemeVariant[];
|
||||
|
||||
/** Set theme mode */
|
||||
setMode: (mode: ThemeMode) => void;
|
||||
/** Set theme variant */
|
||||
setVariant: (variant: ThemeVariant) => void;
|
||||
/** Toggle between light and dark mode */
|
||||
toggleMode: () => void;
|
||||
/** Cycle through modes: light → dark → system → light */
|
||||
cycleMode: () => void;
|
||||
/** Initialize theme (call on mount) */
|
||||
initialize: () => () => void;
|
||||
/** Set theme mode */
|
||||
setMode: (mode: ThemeMode) => void;
|
||||
/** Set theme variant */
|
||||
setVariant: (variant: ThemeVariant) => void;
|
||||
/** Toggle between light and dark mode */
|
||||
toggleMode: () => void;
|
||||
/** Cycle through modes: light → dark → system → light */
|
||||
cycleMode: () => void;
|
||||
/** Initialize theme (call on mount) */
|
||||
initialize: () => () => void;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,151 +5,149 @@ import { THEME_DEFINITIONS, CSS_VAR_PREFIX } from './constants';
|
|||
* Check if code is running in browser
|
||||
*/
|
||||
export function isBrowser(): boolean {
|
||||
return typeof window !== 'undefined' && typeof document !== 'undefined';
|
||||
return typeof window !== 'undefined' && typeof document !== 'undefined';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get system color scheme preference
|
||||
*/
|
||||
export function getSystemPreference(): EffectiveMode {
|
||||
if (!isBrowser()) return 'light';
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||
if (!isBrowser()) return 'light';
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a media query listener for system preference changes
|
||||
*/
|
||||
export function createSystemPreferenceListener(callback: (isDark: boolean) => void): () => void {
|
||||
if (!isBrowser()) return () => {};
|
||||
if (!isBrowser()) return () => {};
|
||||
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
|
||||
const handler = (e: MediaQueryListEvent) => callback(e.matches);
|
||||
const handler = (e: MediaQueryListEvent) => callback(e.matches);
|
||||
|
||||
// Modern browsers
|
||||
if (mediaQuery.addEventListener) {
|
||||
mediaQuery.addEventListener('change', handler);
|
||||
return () => mediaQuery.removeEventListener('change', handler);
|
||||
}
|
||||
// Modern browsers
|
||||
if (mediaQuery.addEventListener) {
|
||||
mediaQuery.addEventListener('change', handler);
|
||||
return () => mediaQuery.removeEventListener('change', handler);
|
||||
}
|
||||
|
||||
// Legacy browsers
|
||||
mediaQuery.addListener(handler);
|
||||
return () => mediaQuery.removeListener(handler);
|
||||
// Legacy browsers
|
||||
mediaQuery.addListener(handler);
|
||||
return () => mediaQuery.removeListener(handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get colors for a specific variant and mode
|
||||
*/
|
||||
export function getThemeColors(
|
||||
variant: ThemeVariant,
|
||||
mode: EffectiveMode,
|
||||
primaryOverride?: { light: HSLValue; dark: HSLValue }
|
||||
variant: ThemeVariant,
|
||||
mode: EffectiveMode,
|
||||
primaryOverride?: { light: HSLValue; dark: HSLValue }
|
||||
): ThemeColors {
|
||||
const definition = THEME_DEFINITIONS[variant];
|
||||
const colors = mode === 'dark' ? { ...definition.dark } : { ...definition.light };
|
||||
const definition = THEME_DEFINITIONS[variant];
|
||||
const colors = mode === 'dark' ? { ...definition.dark } : { ...definition.light };
|
||||
|
||||
// Apply app-specific primary color override
|
||||
if (primaryOverride) {
|
||||
const overrideColor = mode === 'dark' ? primaryOverride.dark : primaryOverride.light;
|
||||
colors.primary = overrideColor;
|
||||
colors.ring = overrideColor;
|
||||
}
|
||||
// Apply app-specific primary color override
|
||||
if (primaryOverride) {
|
||||
const overrideColor = mode === 'dark' ? primaryOverride.dark : primaryOverride.light;
|
||||
colors.primary = overrideColor;
|
||||
colors.ring = overrideColor;
|
||||
}
|
||||
|
||||
return colors;
|
||||
return colors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert ThemeColors to CSS variables object
|
||||
*/
|
||||
export function colorsToCssVars(colors: ThemeColors): Record<string, string> {
|
||||
return {
|
||||
[`${CSS_VAR_PREFIX}-primary`]: colors.primary,
|
||||
[`${CSS_VAR_PREFIX}-primary-foreground`]: colors.primaryForeground,
|
||||
[`${CSS_VAR_PREFIX}-secondary`]: colors.secondary,
|
||||
[`${CSS_VAR_PREFIX}-secondary-foreground`]: colors.secondaryForeground,
|
||||
[`${CSS_VAR_PREFIX}-background`]: colors.background,
|
||||
[`${CSS_VAR_PREFIX}-foreground`]: colors.foreground,
|
||||
[`${CSS_VAR_PREFIX}-surface`]: colors.surface,
|
||||
[`${CSS_VAR_PREFIX}-surface-hover`]: colors.surfaceHover,
|
||||
[`${CSS_VAR_PREFIX}-surface-elevated`]: colors.surfaceElevated,
|
||||
[`${CSS_VAR_PREFIX}-muted`]: colors.muted,
|
||||
[`${CSS_VAR_PREFIX}-muted-foreground`]: colors.mutedForeground,
|
||||
[`${CSS_VAR_PREFIX}-border`]: colors.border,
|
||||
[`${CSS_VAR_PREFIX}-border-strong`]: colors.borderStrong,
|
||||
[`${CSS_VAR_PREFIX}-error`]: colors.error,
|
||||
[`${CSS_VAR_PREFIX}-success`]: colors.success,
|
||||
[`${CSS_VAR_PREFIX}-warning`]: colors.warning,
|
||||
[`${CSS_VAR_PREFIX}-input`]: colors.input,
|
||||
[`${CSS_VAR_PREFIX}-ring`]: colors.ring,
|
||||
};
|
||||
return {
|
||||
[`${CSS_VAR_PREFIX}-primary`]: colors.primary,
|
||||
[`${CSS_VAR_PREFIX}-primary-foreground`]: colors.primaryForeground,
|
||||
[`${CSS_VAR_PREFIX}-secondary`]: colors.secondary,
|
||||
[`${CSS_VAR_PREFIX}-secondary-foreground`]: colors.secondaryForeground,
|
||||
[`${CSS_VAR_PREFIX}-background`]: colors.background,
|
||||
[`${CSS_VAR_PREFIX}-foreground`]: colors.foreground,
|
||||
[`${CSS_VAR_PREFIX}-surface`]: colors.surface,
|
||||
[`${CSS_VAR_PREFIX}-surface-hover`]: colors.surfaceHover,
|
||||
[`${CSS_VAR_PREFIX}-surface-elevated`]: colors.surfaceElevated,
|
||||
[`${CSS_VAR_PREFIX}-muted`]: colors.muted,
|
||||
[`${CSS_VAR_PREFIX}-muted-foreground`]: colors.mutedForeground,
|
||||
[`${CSS_VAR_PREFIX}-border`]: colors.border,
|
||||
[`${CSS_VAR_PREFIX}-border-strong`]: colors.borderStrong,
|
||||
[`${CSS_VAR_PREFIX}-error`]: colors.error,
|
||||
[`${CSS_VAR_PREFIX}-success`]: colors.success,
|
||||
[`${CSS_VAR_PREFIX}-warning`]: colors.warning,
|
||||
[`${CSS_VAR_PREFIX}-input`]: colors.input,
|
||||
[`${CSS_VAR_PREFIX}-ring`]: colors.ring,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply theme to document
|
||||
*/
|
||||
export function applyThemeToDocument(
|
||||
variant: ThemeVariant,
|
||||
effectiveMode: EffectiveMode,
|
||||
primaryOverride?: { light: HSLValue; dark: HSLValue }
|
||||
variant: ThemeVariant,
|
||||
effectiveMode: EffectiveMode,
|
||||
primaryOverride?: { light: HSLValue; dark: HSLValue }
|
||||
): void {
|
||||
if (!isBrowser()) return;
|
||||
if (!isBrowser()) return;
|
||||
|
||||
const root = document.documentElement;
|
||||
const colors = getThemeColors(variant, effectiveMode, primaryOverride);
|
||||
const cssVars = colorsToCssVars(colors);
|
||||
const root = document.documentElement;
|
||||
const colors = getThemeColors(variant, effectiveMode, primaryOverride);
|
||||
const cssVars = colorsToCssVars(colors);
|
||||
|
||||
// Set CSS variables
|
||||
Object.entries(cssVars).forEach(([key, value]) => {
|
||||
root.style.setProperty(key, value);
|
||||
});
|
||||
// Set CSS variables
|
||||
Object.entries(cssVars).forEach(([key, value]) => {
|
||||
root.style.setProperty(key, value);
|
||||
});
|
||||
|
||||
// Set data-theme attribute
|
||||
root.setAttribute('data-theme', variant);
|
||||
// Set data-theme attribute
|
||||
root.setAttribute('data-theme', variant);
|
||||
|
||||
// Set dark class
|
||||
if (effectiveMode === 'dark') {
|
||||
root.classList.add('dark');
|
||||
} else {
|
||||
root.classList.remove('dark');
|
||||
}
|
||||
// Set dark class
|
||||
if (effectiveMode === 'dark') {
|
||||
root.classList.add('dark');
|
||||
} else {
|
||||
root.classList.remove('dark');
|
||||
}
|
||||
|
||||
// Update color-scheme for native elements
|
||||
root.style.colorScheme = effectiveMode;
|
||||
// Update color-scheme for native elements
|
||||
root.style.colorScheme = effectiveMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load theme from localStorage
|
||||
*/
|
||||
export function loadThemeFromStorage(storageKey: string): { mode?: string; variant?: string } | null {
|
||||
if (!isBrowser()) return null;
|
||||
export function loadThemeFromStorage(
|
||||
storageKey: string
|
||||
): { mode?: string; variant?: string } | null {
|
||||
if (!isBrowser()) return null;
|
||||
|
||||
try {
|
||||
const stored = localStorage.getItem(storageKey);
|
||||
if (stored) {
|
||||
return JSON.parse(stored);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to load theme from storage:', e);
|
||||
}
|
||||
try {
|
||||
const stored = localStorage.getItem(storageKey);
|
||||
if (stored) {
|
||||
return JSON.parse(stored);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to load theme from storage:', e);
|
||||
}
|
||||
|
||||
return null;
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save theme to localStorage
|
||||
*/
|
||||
export function saveThemeToStorage(
|
||||
storageKey: string,
|
||||
mode: string,
|
||||
variant: string
|
||||
): void {
|
||||
if (!isBrowser()) return;
|
||||
export function saveThemeToStorage(storageKey: string, mode: string, variant: string): void {
|
||||
if (!isBrowser()) return;
|
||||
|
||||
try {
|
||||
localStorage.setItem(storageKey, JSON.stringify({ mode, variant }));
|
||||
} catch (e) {
|
||||
console.warn('Failed to save theme to storage:', e);
|
||||
}
|
||||
try {
|
||||
localStorage.setItem(storageKey, JSON.stringify({ mode, variant }));
|
||||
} catch (e) {
|
||||
console.warn('Failed to save theme to storage:', e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -157,46 +155,46 @@ export function saveThemeToStorage(
|
|||
* Input: "47 95% 58%" -> { h: 47, s: 95, l: 58 }
|
||||
*/
|
||||
export function parseHSL(hsl: HSLValue): { h: number; s: number; l: number } {
|
||||
const parts = hsl.split(' ');
|
||||
return {
|
||||
h: parseFloat(parts[0]),
|
||||
s: parseFloat(parts[1]),
|
||||
l: parseFloat(parts[2]),
|
||||
};
|
||||
const parts = hsl.split(' ');
|
||||
return {
|
||||
h: parseFloat(parts[0]),
|
||||
s: parseFloat(parts[1]),
|
||||
l: parseFloat(parts[2]),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create HSL string from components
|
||||
*/
|
||||
export function createHSL(h: number, s: number, l: number): HSLValue {
|
||||
return `${h} ${s}% ${l}%`;
|
||||
return `${h} ${s}% ${l}%`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjust lightness of an HSL color
|
||||
*/
|
||||
export function adjustLightness(hsl: HSLValue, amount: number): HSLValue {
|
||||
const { h, s, l } = parseHSL(hsl);
|
||||
const newL = Math.max(0, Math.min(100, l + amount));
|
||||
return createHSL(h, s, newL);
|
||||
const { h, s, l } = parseHSL(hsl);
|
||||
const newL = Math.max(0, Math.min(100, l + amount));
|
||||
return createHSL(h, s, newL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjust saturation of an HSL color
|
||||
*/
|
||||
export function adjustSaturation(hsl: HSLValue, amount: number): HSLValue {
|
||||
const { h, s, l } = parseHSL(hsl);
|
||||
const newS = Math.max(0, Math.min(100, s + amount));
|
||||
return createHSL(h, newS, l);
|
||||
const { h, s, l } = parseHSL(hsl);
|
||||
const newS = Math.max(0, Math.min(100, s + amount));
|
||||
return createHSL(h, newS, l);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get contrasting text color (black or white) for a background
|
||||
*/
|
||||
export function getContrastColor(backgroundHSL: HSLValue): HSLValue {
|
||||
const { l } = parseHSL(backgroundHSL);
|
||||
// Use white text for dark backgrounds, black for light
|
||||
return l > 55 ? '0 0% 0%' : '0 0% 100%';
|
||||
const { l } = parseHSL(backgroundHSL);
|
||||
// Use white text for dark backgrounds, black for light
|
||||
return l > 55 ? '0 0% 0%' : '0 0% 100%';
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -204,42 +202,42 @@ export function getContrastColor(backgroundHSL: HSLValue): HSLValue {
|
|||
* Useful for generating static CSS files
|
||||
*/
|
||||
export function generateThemeCSS(
|
||||
primaryOverrides?: Record<string, { light: HSLValue; dark: HSLValue }>
|
||||
primaryOverrides?: Record<string, { light: HSLValue; dark: HSLValue }>
|
||||
): string {
|
||||
let css = '';
|
||||
let css = '';
|
||||
|
||||
// Root (default Lume light)
|
||||
const defaultColors = getThemeColors('lume', 'light', primaryOverrides?.['lume']);
|
||||
const defaultVars = colorsToCssVars(defaultColors);
|
||||
css += ':root {\n';
|
||||
Object.entries(defaultVars).forEach(([key, value]) => {
|
||||
css += ` ${key}: ${value};\n`;
|
||||
});
|
||||
css += '}\n\n';
|
||||
// Root (default Lume light)
|
||||
const defaultColors = getThemeColors('lume', 'light', primaryOverrides?.['lume']);
|
||||
const defaultVars = colorsToCssVars(defaultColors);
|
||||
css += ':root {\n';
|
||||
Object.entries(defaultVars).forEach(([key, value]) => {
|
||||
css += ` ${key}: ${value};\n`;
|
||||
});
|
||||
css += '}\n\n';
|
||||
|
||||
// Each variant
|
||||
for (const [variantName, definition] of Object.entries(THEME_DEFINITIONS)) {
|
||||
const variant = variantName as ThemeVariant;
|
||||
const override = primaryOverrides?.[variant];
|
||||
// Each variant
|
||||
for (const [variantName, definition] of Object.entries(THEME_DEFINITIONS)) {
|
||||
const variant = variantName as ThemeVariant;
|
||||
const override = primaryOverrides?.[variant];
|
||||
|
||||
// Light mode
|
||||
const lightColors = getThemeColors(variant, 'light', override);
|
||||
const lightVars = colorsToCssVars(lightColors);
|
||||
css += `[data-theme="${variant}"] {\n`;
|
||||
Object.entries(lightVars).forEach(([key, value]) => {
|
||||
css += ` ${key}: ${value};\n`;
|
||||
});
|
||||
css += '}\n\n';
|
||||
// Light mode
|
||||
const lightColors = getThemeColors(variant, 'light', override);
|
||||
const lightVars = colorsToCssVars(lightColors);
|
||||
css += `[data-theme="${variant}"] {\n`;
|
||||
Object.entries(lightVars).forEach(([key, value]) => {
|
||||
css += ` ${key}: ${value};\n`;
|
||||
});
|
||||
css += '}\n\n';
|
||||
|
||||
// Dark mode
|
||||
const darkColors = getThemeColors(variant, 'dark', override);
|
||||
const darkVars = colorsToCssVars(darkColors);
|
||||
css += `[data-theme="${variant}"].dark,\n.dark[data-theme="${variant}"] {\n`;
|
||||
Object.entries(darkVars).forEach(([key, value]) => {
|
||||
css += ` ${key}: ${value};\n`;
|
||||
});
|
||||
css += '}\n\n';
|
||||
}
|
||||
// Dark mode
|
||||
const darkColors = getThemeColors(variant, 'dark', override);
|
||||
const darkVars = colorsToCssVars(darkColors);
|
||||
css += `[data-theme="${variant}"].dark,\n.dark[data-theme="${variant}"] {\n`;
|
||||
Object.entries(darkVars).forEach(([key, value]) => {
|
||||
css += ` ${key}: ${value};\n`;
|
||||
});
|
||||
css += '}\n\n';
|
||||
}
|
||||
|
||||
return css;
|
||||
return css;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue