feat(theme): Implement Aspirant theme switcher with DesignSwitch component
This commit is contained in:
parent
3023155fde
commit
63d0e4ee2d
6 changed files with 181 additions and 14 deletions
|
|
@ -1,7 +1,10 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Lato:wght@300;400;700&family=Merriweather:wght@300;400;700;900&display=swap"
|
||||
rel="stylesheet">
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<meta name="theme-color" content="#10b981" />
|
||||
|
|
|
|||
21
frontend/src/components/DesignSwitch.tsx
Normal file
21
frontend/src/components/DesignSwitch.tsx
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { useTheme } from '../context/ThemeContext';
|
||||
import { Palette } from 'lucide-react';
|
||||
|
||||
export default function DesignSwitch() {
|
||||
const { theme, setTheme } = useTheme();
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={() => setTheme(theme === 'default' ? 'aspirant' : 'default')}
|
||||
className={`flex items-center gap-2 px-3 py-2 rounded-md transition-colors duration-fast text-sm font-medium
|
||||
${theme === 'aspirant'
|
||||
? 'text-[var(--color-primary)] bg-[var(--color-primary-soft)]'
|
||||
: 'text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] hover:bg-[var(--color-bg-tertiary)]'
|
||||
}`}
|
||||
title="Switch Design Theme"
|
||||
>
|
||||
<Palette size={16} />
|
||||
<span className="hidden sm:inline text-xs">{theme === 'aspirant' ? 'Aspirant' : 'Classic'}</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
|
@ -14,6 +14,7 @@ import { Breadcrumbs } from './ui/Breadcrumbs';
|
|||
import { pageVariants } from '../lib/animations';
|
||||
import { Search, Bell, Settings, Filter, ChevronDown } from 'lucide-react';
|
||||
import ThemeToggle from './ThemeToggle';
|
||||
import DesignSwitch from './DesignSwitch';
|
||||
import { UserMenu } from './layout/UserMenu';
|
||||
import { NotificationBell } from './notifications/NotificationBell';
|
||||
|
||||
|
|
@ -91,6 +92,7 @@ export default function Layout() {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<DesignSwitch />
|
||||
<ThemeToggle />
|
||||
<NotificationBell />
|
||||
<button className="lg:hidden p-2 text-[var(--color-text-tertiary)]" onClick={() => setMobileSheetOpen(true)}>
|
||||
|
|
|
|||
35
frontend/src/context/ThemeContext.tsx
Normal file
35
frontend/src/context/ThemeContext.tsx
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import React, { createContext, useContext, useEffect, useState } from 'react';
|
||||
|
||||
type Theme = 'default' | 'aspirant';
|
||||
|
||||
interface ThemeContextType {
|
||||
theme: Theme;
|
||||
setTheme: (theme: Theme) => void;
|
||||
}
|
||||
|
||||
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
|
||||
|
||||
export function ThemeProvider({ children }: { children: React.ReactNode }) {
|
||||
const [theme, setTheme] = useState<Theme>(() => {
|
||||
return (localStorage.getItem('veridian-theme') as Theme) || 'default';
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem('veridian-theme', theme);
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
}, [theme]);
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={{ theme, setTheme }}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useTheme() {
|
||||
const context = useContext(ThemeContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useTheme must be used within a ThemeProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
|
@ -11,7 +11,8 @@
|
|||
/* ============================================
|
||||
CSS Custom Properties (Design Tokens)
|
||||
============================================ */
|
||||
:root {
|
||||
:root,
|
||||
[data-theme="default"] {
|
||||
/* Font families */
|
||||
--font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
--font-display: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
|
|
@ -778,3 +779,105 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
/* ===== THEME: Aspirant (Agave & Bone) ===== */
|
||||
[data-theme="aspirant"] {
|
||||
/* Backgrounds - Bone scale */
|
||||
--color-bg-primary: #FDFCF8;
|
||||
--color-bg-secondary: #F5F2EB; /* slightly darker stone */
|
||||
--color-bg-tertiary: #E6E0D4;
|
||||
--color-bg-elevated: #F5F2EB;
|
||||
|
||||
/* Text - Charcoal */
|
||||
--color-text-primary: #2C2A26;
|
||||
--color-text-secondary: #5C554B;
|
||||
--color-text-tertiary: #847C6F;
|
||||
--color-text-quaternary: #A8A195;
|
||||
--color-text-inverse: #FDFCF8;
|
||||
|
||||
/* Borders */
|
||||
--color-border-default: #E6E0D4;
|
||||
--color-border-subtle: #F5F2EB;
|
||||
--color-border-strong: #A8A195;
|
||||
|
||||
/* Primary (Agave) */
|
||||
--color-primary: #4B7F79;
|
||||
--color-primary-hover: #3D6A64;
|
||||
--color-primary-soft: rgba(75, 127, 121, 0.15);
|
||||
|
||||
/* Accent (Gold) */
|
||||
--color-accent: #E0A96D;
|
||||
--color-accent-hover: #D49B5C;
|
||||
--color-accent-soft: rgba(224, 169, 109, 0.15);
|
||||
|
||||
/* Status Colors (Earthy) */
|
||||
--color-success: #6A994E;
|
||||
--color-warning: #E9C46A;
|
||||
--color-error: #C78D75;
|
||||
--color-info: #4B7F79;
|
||||
|
||||
/* Chart colors */
|
||||
--color-chart-green: #4B7F79;
|
||||
--color-chart-orange: #E0A96D;
|
||||
--color-chart-blue: #5F8D87;
|
||||
--color-chart-purple: #8B735B;
|
||||
--color-chart-gridline: #E6E0D4;
|
||||
|
||||
/* Layout override */
|
||||
--card-radius: 12px;
|
||||
|
||||
/* Fonts */
|
||||
--font-sans: 'Lato', sans-serif;
|
||||
--font-display: 'Merriweather', serif;
|
||||
|
||||
/* shadcn override */
|
||||
--background: 48 20% 98%;
|
||||
--foreground: 33 8% 16%;
|
||||
--card: 48 20% 98%;
|
||||
--card-foreground: 33 8% 16%;
|
||||
--primary: 172 26% 40%;
|
||||
--primary-foreground: 48 20% 98%;
|
||||
--secondary: 48 20% 94%;
|
||||
--secondary-foreground: 33 8% 16%;
|
||||
--muted: 48 20% 94%;
|
||||
--muted-foreground: 33 8% 40%;
|
||||
--accent: 33 65% 65%;
|
||||
--accent-foreground: 48 20% 98%;
|
||||
--destructive: 18 45% 62%;
|
||||
--destructive-foreground: 48 20% 98%;
|
||||
--border: 48 20% 90%;
|
||||
--input: 48 20% 90%;
|
||||
--ring: 172 26% 40%;
|
||||
--radius: 0.75rem;
|
||||
}
|
||||
|
||||
/* Aspirant Dark Mode Override */
|
||||
[data-theme="aspirant"].dark,
|
||||
[data-theme="aspirant"] .dark {
|
||||
/* Backgrounds - Obsidian */
|
||||
--color-bg-primary: #1A1916;
|
||||
--color-bg-secondary: #2C2A26;
|
||||
--color-bg-tertiary: #3D3A35;
|
||||
--color-bg-elevated: #2C2A26;
|
||||
|
||||
/* Text - Bone */
|
||||
--color-text-primary: #FDFCF8;
|
||||
--color-text-secondary: #A8A195;
|
||||
--color-text-tertiary: #847C6F;
|
||||
--color-text-quaternary: #5C554B;
|
||||
--color-text-inverse: #1A1916;
|
||||
|
||||
/* Borders */
|
||||
--color-border-default: #3D3A35;
|
||||
--color-border-subtle: #2C2A26;
|
||||
--color-border-strong: #4B4842;
|
||||
|
||||
/* Primary (Agave preserved) */
|
||||
--color-primary: #4B7F79;
|
||||
--color-primary-hover: #5F8D87;
|
||||
|
||||
/* shadcn override */
|
||||
--background: 33 8% 10%;
|
||||
--foreground: 48 20% 98%;
|
||||
--card: 33 8% 16%;
|
||||
--card-foreground: 48 20% 98%;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,12 +2,15 @@ import React from 'react'
|
|||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App.tsx'
|
||||
import './index.css'
|
||||
import { ThemeProvider } from './context/ThemeContext'
|
||||
|
||||
// Initialize i18n
|
||||
import './lib/i18n'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<ThemeProvider>
|
||||
<App />
|
||||
</ThemeProvider>
|
||||
</React.StrictMode>,
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue