AgentSkillsCN

Theme Switching

主题切换

SKILL.md

name theme-system description CSS custom properties theme architecture for 4 themes (studio, earth, athlete, gradient) with data-theme attribute switching and theme-aware components. Use when implementing theme switching, defining color schemes, or creating theme-responsive UI elements. Theme System Theme Definitions /_ app/globals.css _/ @tailwind base; @tailwind components; @tailwind utilities;

@layer base { :root { /_ Default: Studio theme _/ --background: 0 0% 100%; --foreground: 0 0% 3.9%; --card: 0 0% 100%; --card-foreground: 0 0% 3.9%; --popover: 0 0% 100%; --popover-foreground: 0 0% 3.9%; --primary: 0 0% 9%; --primary-foreground: 0 0% 98%; --secondary: 0 0% 96.1%; --secondary-foreground: 0 0% 9%; --muted: 0 0% 96.1%; --muted-foreground: 0 0% 45.1%; --accent: 0 0% 96.1%; --accent-foreground: 0 0% 9%; --destructive: 0 84.2% 60.2%; --destructive-foreground: 0 0% 98%; --border: 0 0% 89.8%; --input: 0 0% 89.8%; --ring: 0 0% 3.9%; --radius: 0.5rem; }

/_ Theme: Earth - Soft organic warmth _/ [data-theme="earth"] { --background: 40 33% 98%; --foreground: 30 10% 15%; --card: 40 30% 97%; --card-foreground: 30 10% 15%; --primary: 30 30% 35%; --primary-foreground: 40 30% 98%; --secondary: 35 25% 92%; --secondary-foreground: 30 20% 25%; --muted: 35 20% 93%; --muted-foreground: 30 10% 40%; --accent: 35 30% 88%; --accent-foreground: 30 20% 20%; --border: 35 20% 85%; --input: 35 20% 88%; --ring: 30 30% 35%; }

/_ Theme: Studio - Clean minimal _/ [data-theme="studio"] { --background: 0 0% 100%; --foreground: 0 0% 5%; --card: 0 0% 100%; --card-foreground: 0 0% 5%; --primary: 0 0% 8%; --primary-foreground: 0 0% 100%; --secondary: 0 0% 97%; --secondary-foreground: 0 0% 10%; --muted: 0 0% 96%; --muted-foreground: 0 0% 40%; --accent: 0 0% 95%; --accent-foreground: 0 0% 10%; --border: 0 0% 92%; --input: 0 0% 94%; --ring: 0 0% 8%; }

/_ Theme: Athlete - Bold with purple accent _/ [data-theme="athlete"] { --background: 0 0% 99%; --foreground: 270 5% 10%; --card: 0 0% 100%; --card-foreground: 270 5% 10%; --primary: 270 60% 50%; --primary-foreground: 0 0% 100%; --secondary: 270 20% 95%; --secondary-foreground: 270 30% 25%; --muted: 270 10% 94%; --muted-foreground: 270 5% 40%; --accent: 270 40% 92%; --accent-foreground: 270 40% 30%; --border: 270 10% 88%; --input: 270 10% 90%; --ring: 270 60% 50%; }

/_ Theme: Gradient - Soft gradient hero _/ [data-theme="gradient"] { --background: 220 30% 99%; --foreground: 220 10% 10%; --card: 0 0% 100%; --card-foreground: 220 10% 10%; --primary: 220 80% 55%; --primary-foreground: 0 0% 100%; --secondary: 280 30% 95%; --secondary-foreground: 220 20% 20%; --muted: 220 20% 95%; --muted-foreground: 220 10% 40%; --accent: 280 40% 93%; --accent-foreground: 280 30% 25%; --border: 220 15% 90%; --input: 220 15% 92%; --ring: 220 80% 55%;

code
/* Gradient-specific variables */
--gradient-start: 220 80% 60%;
--gradient-end: 280 60% 65%;

} } Theme Configuration // lib/theme.ts export const themes = ['studio', 'earth', 'athlete', 'gradient'] as const; export type Theme = (typeof themes)[number]; export const defaultTheme: Theme = 'studio';

export function isValidTheme(theme: string): theme is Theme { return themes.includes(theme as Theme); } Theme Provider // components/ThemeProvider.tsx 'use client';

import { createContext, useContext, useEffect, useState } from 'react'; import { type Theme, defaultTheme, isValidTheme } from '@/lib/theme';

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>(defaultTheme);

useEffect(() => { // Check URL param first const params = new URLSearchParams(window.location.search); const urlTheme = params.get('theme');

code
if (urlTheme && isValidTheme(urlTheme)) {
  setTheme(urlTheme);
  document.documentElement.setAttribute('data-theme', urlTheme);
  return;
}

// Check localStorage
const stored = localStorage.getItem('theme');
if (stored && isValidTheme(stored)) {
  setTheme(stored);
  document.documentElement.setAttribute('data-theme', stored);
}

}, []);

const handleSetTheme = (newTheme: Theme) => { setTheme(newTheme); document.documentElement.setAttribute('data-theme', newTheme); localStorage.setItem('theme', newTheme);

code
// Update URL without reload
const url = new URL(window.location.href);
url.searchParams.set('theme', newTheme);
window.history.replaceState({}, '', url);

};

return ( <ThemeContext.Provider value={{ theme, setTheme: handleSetTheme }}> {children} </ThemeContext.Provider> ); }

export function useTheme() { const context = useContext(ThemeContext); if (!context) { throw new Error('useTheme must be used within ThemeProvider'); } return context; } Theme Switcher (Dev Mode) // components/ThemeSwitcher.tsx 'use client';

import { useTheme } from './ThemeProvider'; import { themes, type Theme } from '@/lib/theme';

const themeLabels: Record<Theme, string> = { studio: 'Studio', earth: 'Earth', athlete: 'Athlete', gradient: 'Gradient', };

export function ThemeSwitcher() { const { theme, setTheme } = useTheme();

// Only show in development or with ?dev query param if (process.env.NODE_ENV === 'production') { return null; }

return (

<div className="fixed bottom-4 right-4 z-50 bg-card border rounded-lg p-2 shadow-lg"> <div className="flex gap-1"> {themes.map((t) => ( <button key={t} onClick={() => setTheme(t)} className={`px-3 py-1 text-sm rounded transition-colors ${ theme === t ? 'bg-primary text-primary-foreground' : 'hover:bg-muted' }`} > {themeLabels[t]} </button> ))} </div> </div> ); } Theme-Aware Components // components/Hero.tsx 'use client';

import { useTheme } from './ThemeProvider';

export function Hero() { const { theme } = useTheme();

return (

<section className={` relative min-h-[80vh] flex items-center ${theme === 'gradient' ? 'bg-gradient-to-br from-[hsl(var(--gradient-start))] to-[hsl(var(--gradient-end))]' : ''} ${theme === 'earth' ? 'bg-[url("/patterns/wave.svg")] bg-cover' : ''} `} > {theme === 'gradient' && ( <div className="absolute inset-0 backdrop-blur-sm bg-background/30" /> )}
code
  <div className="relative container mx-auto px-4">
    <h1 className={`
      text-4xl md:text-6xl font-bold
      ${theme === 'gradient' ? 'text-white drop-shadow-lg' : ''}
    `}>
      Pilates & Yoga
    </h1>
  </div>
</section>

); } Background Ornaments // components/BackgroundOrnament.tsx 'use client';

import { useTheme } from './ThemeProvider';

export function BackgroundOrnament() { const { theme } = useTheme();

if (theme === 'studio') { return null; // Minimal, no ornaments }

if (theme === 'earth') { return (

<div className="absolute inset-0 overflow-hidden pointer-events-none"> <svg className="absolute -top-20 -right-20 w-96 h-96 text-accent/30"> {/_ Organic wave shape _/} </svg> </div> ); }

if (theme === 'athlete') { return (

<div className="absolute top-0 left-0 w-full h-2 bg-primary" /> ); }

return null; } Layout Integration // app/[locale]/layout.tsx import { ThemeProvider } from '@/components/ThemeProvider'; import { ThemeSwitcher } from '@/components/ThemeSwitcher';

export default function LocaleLayout({ children }) { return ( <ThemeProvider> {children} <ThemeSwitcher /> </ThemeProvider> ); } Preview via URL Access themes via query parameter:

?theme=studio - Clean minimal ?theme=earth - Soft organic ?theme=athlete - Bold purple ?theme=gradient - Gradient hero