From 8c1e826d63c64b228f44a17f3ad339b6de2d2605 Mon Sep 17 00:00:00 2001 From: Steven RYDELL Date: Thu, 30 Jul 2026 17:38:40 +0200 Subject: [PATCH] feat: add Rose/Amber/Teal color themes, default to Ocean + Square Adds three new color themes alongside the existing Stalwart/Ocean/ Forest/Violet set so more people can find one they like, and switches the out-of-the-box defaults to Ocean + square corners instead of the neutral Stalwart theme + rounded corners (existing users' saved preferences are untouched, this only changes what a fresh install starts with). Verified in the browser: all three new themes render with legible primary-foreground contrast in both light and dark mode, and the pre-hydration flash-prevention script in main.tsx (which reads localStorage before React mounts) now recognizes the new theme names. Co-Authored-By: Claude Sonnet 5 --- src/i18n/en.json | 3 ++ src/index.css | 117 ++++++++++++++++++++++++++++++++++++++++++ src/main.tsx | 9 +++- src/stores/uiStore.ts | 13 +++-- 4 files changed, 136 insertions(+), 6 deletions(-) diff --git a/src/i18n/en.json b/src/i18n/en.json index 2cd8e5e..09eafaf 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -16,9 +16,12 @@ "switchToDark": "Switch to dark mode", "switchToLight": "Switch to light mode", "theme": { + "amber": "Amber", "forest": "Forest", "ocean": "Ocean", + "rose": "Rose", "stalwart": "Stalwart", + "teal": "Teal", "violet": "Violet" } }, diff --git a/src/index.css b/src/index.css index 527d8f5..e63c563 100644 --- a/src/index.css +++ b/src/index.css @@ -220,6 +220,123 @@ --chart-1: 263 70% 66%; } +:root[data-theme='rose'] { + --foreground: oklch(0.145 0.02 12); + --content-background: oklch(0.965 0.008 12); + --primary: oklch(0.577 0.245 12); + --primary-foreground: oklch(0.985 0.002 12); + --secondary: oklch(0.955 0.012 12); + --secondary-foreground: oklch(0.3 0.035 12); + --muted: oklch(0.935 0.01 12); + --muted-foreground: oklch(0.556 0.022 12); + --accent: oklch(0.94 0.018 12); + --accent-foreground: oklch(0.3 0.035 12); + --border: oklch(0.912 0.012 12); + --input: oklch(0.912 0.012 12); + --ring: oklch(0.577 0.245 12); + --chart-1: 350 83% 55%; +} + +.dark[data-theme='rose'] { + --background: oklch(0.152 0.014 12); + --foreground: oklch(0.96 0.008 12); + --card: oklch(0.235 0.02 12); + --card-foreground: oklch(0.96 0.008 12); + --popover: oklch(0.208 0.018 12); + --popover-foreground: oklch(0.96 0.008 12); + --content-background: oklch(0.122 0.012 12); + --primary: oklch(0.704 0.191 12); + --primary-foreground: oklch(0.21 0.03 12); + --secondary: oklch(0.28 0.022 12); + --secondary-foreground: oklch(0.96 0.008 12); + --muted: oklch(0.28 0.022 12); + --muted-foreground: oklch(0.715 0.022 12); + --accent: oklch(0.305 0.028 12); + --accent-foreground: oklch(0.96 0.008 12); + --border: oklch(0.29 0.024 12); + --input: oklch(0.29 0.024 12); + --ring: oklch(0.704 0.191 12); + --chart-1: 350 80% 66%; +} + +:root[data-theme='amber'] { + --foreground: oklch(0.145 0.02 70); + --content-background: oklch(0.965 0.008 70); + --primary: oklch(0.65 0.16 70); + --primary-foreground: oklch(0.2 0.02 70); + --secondary: oklch(0.955 0.012 70); + --secondary-foreground: oklch(0.3 0.035 70); + --muted: oklch(0.935 0.01 70); + --muted-foreground: oklch(0.556 0.022 70); + --accent: oklch(0.94 0.018 70); + --accent-foreground: oklch(0.3 0.035 70); + --border: oklch(0.912 0.012 70); + --input: oklch(0.912 0.012 70); + --ring: oklch(0.65 0.16 70); + --chart-1: 43 96% 56%; +} + +.dark[data-theme='amber'] { + --background: oklch(0.152 0.014 70); + --foreground: oklch(0.96 0.008 70); + --card: oklch(0.235 0.02 70); + --card-foreground: oklch(0.96 0.008 70); + --popover: oklch(0.208 0.018 70); + --popover-foreground: oklch(0.96 0.008 70); + --content-background: oklch(0.122 0.012 70); + --primary: oklch(0.75 0.17 75); + --primary-foreground: oklch(0.2 0.02 70); + --secondary: oklch(0.28 0.022 70); + --secondary-foreground: oklch(0.96 0.008 70); + --muted: oklch(0.28 0.022 70); + --muted-foreground: oklch(0.715 0.022 70); + --accent: oklch(0.305 0.028 70); + --accent-foreground: oklch(0.96 0.008 70); + --border: oklch(0.29 0.024 70); + --input: oklch(0.29 0.024 70); + --ring: oklch(0.75 0.17 75); + --chart-1: 43 96% 62%; +} + +:root[data-theme='teal'] { + --foreground: oklch(0.145 0.02 195); + --content-background: oklch(0.965 0.008 195); + --primary: oklch(0.55 0.13 195); + --primary-foreground: oklch(0.985 0.002 195); + --secondary: oklch(0.955 0.012 195); + --secondary-foreground: oklch(0.3 0.035 195); + --muted: oklch(0.935 0.01 195); + --muted-foreground: oklch(0.556 0.022 195); + --accent: oklch(0.94 0.018 195); + --accent-foreground: oklch(0.3 0.035 195); + --border: oklch(0.912 0.012 195); + --input: oklch(0.912 0.012 195); + --ring: oklch(0.55 0.13 195); + --chart-1: 189 94% 43%; +} + +.dark[data-theme='teal'] { + --background: oklch(0.152 0.014 195); + --foreground: oklch(0.96 0.008 195); + --card: oklch(0.235 0.02 195); + --card-foreground: oklch(0.96 0.008 195); + --popover: oklch(0.208 0.018 195); + --popover-foreground: oklch(0.96 0.008 195); + --content-background: oklch(0.122 0.012 195); + --primary: oklch(0.68 0.13 195); + --primary-foreground: oklch(0.2 0.02 195); + --secondary: oklch(0.28 0.022 195); + --secondary-foreground: oklch(0.96 0.008 195); + --muted: oklch(0.28 0.022 195); + --muted-foreground: oklch(0.715 0.022 195); + --accent: oklch(0.305 0.028 195); + --accent-foreground: oklch(0.96 0.008 195); + --border: oklch(0.29 0.024 195); + --input: oklch(0.29 0.024 195); + --ring: oklch(0.68 0.13 195); + --chart-1: 189 90% 55%; +} + @theme inline { --color-background: var(--background); --color-foreground: var(--foreground); diff --git a/src/main.tsx b/src/main.tsx index 6c9200b..4dfb752 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -25,7 +25,14 @@ import { getBasePath } from './lib/basePath'; const dark = theme === 'dark' || (theme !== 'light' && !!window.matchMedia?.('(prefers-color-scheme: dark)').matches); document.documentElement.classList.toggle('dark', dark); const colorTheme = state?.colorTheme; - if (colorTheme === 'ocean' || colorTheme === 'forest' || colorTheme === 'violet') { + if ( + colorTheme === 'ocean' || + colorTheme === 'forest' || + colorTheme === 'violet' || + colorTheme === 'rose' || + colorTheme === 'amber' || + colorTheme === 'teal' + ) { document.documentElement.dataset.theme = colorTheme; } if (state?.radius === 'square') { diff --git a/src/stores/uiStore.ts b/src/stores/uiStore.ts index 27fdd48..a69c0ee 100644 --- a/src/stores/uiStore.ts +++ b/src/stores/uiStore.ts @@ -9,7 +9,7 @@ import { persist } from 'zustand/middleware'; import { getApiBaseUrl } from '@/services/api'; export type Theme = 'light' | 'dark'; -export type ColorTheme = 'stalwart' | 'ocean' | 'forest' | 'violet'; +export type ColorTheme = 'stalwart' | 'ocean' | 'forest' | 'violet' | 'rose' | 'amber' | 'teal'; export type Radius = 'rounded' | 'square'; let logoAbortController: AbortController | null = null; @@ -73,6 +73,9 @@ export const COLOR_THEMES: { value: ColorTheme; labelKey: string; fallback: stri { value: 'ocean', labelKey: 'appearance.theme.ocean', fallback: 'Ocean', swatch: 'oklch(0.546 0.215 262.9)' }, { value: 'forest', labelKey: 'appearance.theme.forest', fallback: 'Forest', swatch: 'oklch(0.527 0.137 150.1)' }, { value: 'violet', labelKey: 'appearance.theme.violet', fallback: 'Violet', swatch: 'oklch(0.541 0.281 293)' }, + { value: 'rose', labelKey: 'appearance.theme.rose', fallback: 'Rose', swatch: 'oklch(0.577 0.245 12)' }, + { value: 'amber', labelKey: 'appearance.theme.amber', fallback: 'Amber', swatch: 'oklch(0.65 0.16 70)' }, + { value: 'teal', labelKey: 'appearance.theme.teal', fallback: 'Teal', swatch: 'oklch(0.55 0.13 195)' }, ]; export const useUIStore = create()( @@ -80,8 +83,8 @@ export const useUIStore = create()( (set, get) => ({ theme: typeof window !== 'undefined' && window.matchMedia?.('(prefers-color-scheme: dark)').matches ? 'dark' : 'light', - colorTheme: 'stalwart', - radius: 'rounded', + colorTheme: 'ocean', + radius: 'square', sidebarOpen: typeof window !== 'undefined' ? (window.matchMedia?.('(min-width: 768px)').matches ?? true) : true, activeSection: '', logoUrl: null, @@ -166,8 +169,8 @@ export const useUIStore = create()( return (state) => { if (state) { applyThemeClass(state.theme); - applyColorThemeAttribute(state.colorTheme ?? 'stalwart'); - applyRadiusAttribute(state.radius ?? 'rounded'); + applyColorThemeAttribute(state.colorTheme ?? 'ocean'); + applyRadiusAttribute(state.radius ?? 'square'); } }; },