feat: add multiple color themes to appearance settings

This commit is contained in:
rustmailer
2026-05-10 04:11:43 +08:00
parent 41322c8357
commit c07e39495a
5 changed files with 811 additions and 214 deletions
+261 -33
View File
@@ -1,47 +1,275 @@
//
// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com)
//
// This file is part of the Bichon Email Archiving Project
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import { useEffect } from 'react'
import { Moon, Sun, Check } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { useCallback, useEffect } from 'react'
import { IconMoon, IconSun } from '@tabler/icons-react'
import { useTheme } from '@/context/theme-context'
import { Theme, useTheme } from '@/context/theme-context'
import { Button } from '@/components/ui/button'
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover'
import { cn } from '@/lib/utils'
const themeColors: Record<Theme, string> = {
'light': '#ffffff',
'dark': '#020817',
'rose-light': '#fb7185',
'rose-dark': '#881337',
'orange-light': '#fb923c',
'orange-dark': '#9a3412',
'green-light': '#4ade80',
'green-dark': '#166534',
'yellow-light': '#facc15',
'yellow-dark': '#a16207',
'blue-light': '#60a5fa',
'blue-dark': '#1e40af',
}
const LIGHT_THEMES = [
{
value: 'light',
labelKey: 'theme.default',
background: '#ffffff',
color: '#0f172a',
muted: '#cbd5e1',
},
{
value: 'rose-light',
labelKey: 'theme.rose',
background: '#fff1f2',
color: '#e11d48',
muted: '#fda4af',
},
{
value: 'orange-light',
labelKey: 'theme.orange',
background: '#fff7ed',
color: '#ea580c',
muted: '#fdba74',
},
{
value: 'green-light',
labelKey: 'theme.green',
background: '#f0fdf4',
color: '#16a34a',
muted: '#86efac',
},
{
value: 'yellow-light',
labelKey: 'theme.yellow',
background: '#fefce8',
color: '#ca8a04',
muted: '#fde047',
},
{
value: 'blue-light',
labelKey: 'theme.blue',
background: '#eff6ff',
color: '#2563eb',
muted: '#bfdbfe',
},
] as const
const DARK_THEMES = [
{
value: 'dark',
labelKey: 'theme.default',
background: '#020817',
color: '#94a3b8',
muted: '#334155',
},
{
value: 'rose-dark',
labelKey: 'theme.rose',
background: '#1f1115',
color: '#e11d48',
muted: '#881337',
},
{
value: 'orange-dark',
labelKey: 'theme.orange',
background: '#1c120d',
color: '#f97316',
muted: '#9a3412',
},
{
value: 'green-dark',
labelKey: 'theme.green',
background: '#0d1b12',
color: '#22c55e',
muted: '#166534',
},
{
value: 'yellow-dark',
labelKey: 'theme.yellow',
background: '#1c1608',
color: '#facc15',
muted: '#a16207',
},
{
value: 'blue-dark',
labelKey: 'theme.blue',
background: '#172554',
color: '#60a5fa',
muted: '#1e3a8a',
}
] as const
type ThemeOption =
| (typeof LIGHT_THEMES)[number]
| (typeof DARK_THEMES)[number]
function ThemePreview({
background,
primary,
muted,
}: {
background: string
primary: string
muted: string
}) {
return (
<div
className='flex h-10 w-20 shrink-0 flex-col gap-1.5 rounded-md border p-1.5'
style={{
backgroundColor: background,
borderColor: muted,
}}
>
<div className='flex gap-1'>
<span className='h-1 w-1 rounded-full' style={{ backgroundColor: muted }} />
<span className='h-1 w-1 rounded-full' style={{ backgroundColor: muted }} />
<span className='h-1 w-1 rounded-full' style={{ backgroundColor: primary }} />
</div>
<div className='h-1.5 w-12 rounded-sm' style={{ backgroundColor: primary }} />
<div className='h-1 w-16 rounded-sm opacity-70' style={{ backgroundColor: muted }} />
<div className='h-1 w-10 rounded-sm opacity-50' style={{ backgroundColor: muted }} />
</div>
)
}
function ThemeItem({
item,
active,
onSelect,
}: {
item: ThemeOption
active: boolean
onSelect: (v: Theme) => void
}) {
const { t } = useTranslation()
return (
<button
onClick={() => onSelect(item.value as Theme)}
className={cn(
'flex w-full items-center gap-2 rounded-lg px-2 py-1.5 transition-colors',
active ? 'bg-accent' : 'hover:bg-accent/50'
)}
>
<ThemePreview
background={item.background}
primary={item.color}
muted={item.muted}
/>
<span className='flex-1 text-left text-xs font-medium'>
{t(item.labelKey)}
</span>
<Check
className={cn(
'size-4 shrink-0 transition-opacity',
active ? 'opacity-100' : 'opacity-0'
)}
/>
</button>
)
}
function ThemeColumn({
icon,
label,
items,
currentTheme,
onSelect,
}: {
icon: React.ReactNode
label: string
items: readonly ThemeOption[]
currentTheme: Theme
onSelect: (v: Theme) => void
}) {
return (
<div className='space-y-1'>
<div className='mb-2 flex items-center gap-1.5 px-1 text-[11px] font-medium uppercase tracking-wider text-muted-foreground/60'>
{icon}
{label}
</div>
{items.map((item) => (
<ThemeItem
key={item.value}
item={item}
active={currentTheme === item.value}
onSelect={onSelect}
/>
))}
</div>
)
}
export function ThemeSwitch() {
const { theme, setTheme } = useTheme()
const { t } = useTranslation()
const isDark = theme.includes('dark')
useEffect(() => {
const themeColor = theme === 'dark' ? '#020817' : '#fff'
const metaThemeColor = document.querySelector("meta[name='theme-color']")
if (metaThemeColor) metaThemeColor.setAttribute('content', themeColor)
}, [theme])
const toggleTheme = useCallback(() => {
setTheme(theme === 'dark' ? 'light' : 'dark')
document
.querySelector("meta[name='theme-color']")
?.setAttribute('content', themeColors[theme])
}, [theme])
return (
<Button variant='ghost' size='icon' className='scale-95' onClick={toggleTheme}>
<IconSun className='size-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0' />
<IconMoon className='absolute size-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100' />
<span className='sr-only'>Toggle theme</span>
</Button>
<Popover>
<PopoverTrigger asChild>
<Button variant='ghost' size='icon' className='rounded-full'>
{isDark ? (
<Moon className='size-5' />
) : (
<Sun className='size-5' />
)}
</Button>
</PopoverTrigger>
<PopoverContent align='end' className='w-[420px] p-3'>
<p className='mb-3 px-1 text-[11px] font-medium uppercase tracking-wider text-muted-foreground/60'>
{t('theme.appearance')}
</p>
<div className='grid grid-cols-2 gap-3'>
<ThemeColumn
icon={<Sun className='size-3.5' />}
label={t('theme.light')}
items={LIGHT_THEMES}
currentTheme={theme}
onSelect={setTheme}
/>
<div className='border-l border-border/40 pl-3'>
<ThemeColumn
icon={<Moon className='size-3.5' />}
label={t('theme.dark')}
items={DARK_THEMES}
currentTheme={theme}
onSelect={setTheme}
/>
</div>
</div>
</PopoverContent>
</Popover>
)
}
+5 -3
View File
@@ -19,7 +19,8 @@
import { createContext, useContext, useEffect, useState } from 'react'
type Theme = 'dark' | 'light'
export type Theme = 'light' | 'dark' | 'rose-light' | 'rose-dark' | 'orange-light'
| 'orange-dark' | 'green-light' | 'green-dark' | 'yellow-light' | 'yellow-dark' | 'blue-light' | 'blue-dark'
type ThemeProviderProps = {
children: React.ReactNode
@@ -41,7 +42,7 @@ const ThemeProviderContext = createContext<ThemeProviderState>(initialState)
export function ThemeProvider({
children,
defaultTheme = 'dark',
defaultTheme = 'light',
storageKey = 'vite-ui-theme',
...props
}: ThemeProviderProps) {
@@ -51,7 +52,8 @@ export function ThemeProvider({
useEffect(() => {
const root = window.document.documentElement
root.classList.remove('light', 'dark')
root.classList.remove('light', 'dark', 'rose-light', 'rose-dark', 'orange-light',
'orange-dark', 'green-light', 'green-dark', 'yellow-light', 'yellow-dark', 'blue-light', 'blue-dark')
root.classList.add(theme)
localStorage.setItem(storageKey, theme)
}, [theme, storageKey])
@@ -1,22 +1,3 @@
//
// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com)
//
// This file is part of the Bichon Email Archiving Project
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import { z } from 'zod'
import { useForm } from 'react-hook-form'
import { CaretSortIcon, CheckIcon } from '@radix-ui/react-icons'
@@ -32,8 +13,8 @@ import {
FormLabel,
FormMessage,
} from '@/components/ui/form'
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
import { useTheme } from '@/context/theme-context'
import { RadioGroup } from '@/components/ui/radio-group'
import { useTheme, Theme } from '@/context/theme-context'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '@/components/ui/command'
import { useTranslation } from 'react-i18next'
@@ -42,7 +23,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'
import { update_user } from '@/api/users/api'
import { toast } from '@/hooks/use-toast'
import { AxiosError } from 'axios'
import { Card } from '@/components/ui/card'
const languages = [
{ value: 'ar', label: 'العربية' },
@@ -65,8 +46,28 @@ const languages = [
{ value: 'zh-tw', label: '繁體中文' },
]
const LIGHT_THEMES = [
{ value: 'light', labelKey: 'theme.default', background: '#ffffff', color: '#0f172a', muted: '#cbd5e1' },
{ value: 'rose-light', labelKey: 'theme.rose', background: '#fff1f2', color: '#e11d48', muted: '#fda4af' },
{ value: 'orange-light', labelKey: 'theme.orange', background: '#fff7ed', color: '#ea580c', muted: '#fdba74' },
{ value: 'green-light', labelKey: 'theme.green', background: '#f0fdf4', color: '#16a34a', muted: '#86efac' },
{ value: 'yellow-light', labelKey: 'theme.yellow', background: '#fefce8', color: '#ca8a04', muted: '#fde047' },
{ value: 'blue-light', labelKey: 'theme.blue', background: '#eff6ff', color: '#2563eb', muted: '#bfdbfe' },
] as const
const DARK_THEMES = [
{ value: 'dark', labelKey: 'theme.default', background: '#020817', color: '#94a3b8', muted: '#334155' },
{ value: 'rose-dark', labelKey: 'theme.rose', background: '#1f1115', color: '#e11d48', muted: '#881337' },
{ value: 'orange-dark', labelKey: 'theme.orange', background: '#1c120d', color: '#f97316', muted: '#9a3412' },
{ value: 'green-dark', labelKey: 'theme.green', background: '#0d1b12', color: '#22c55e', muted: '#166534' },
{ value: 'yellow-dark', labelKey: 'theme.yellow', background: '#1c1608', color: '#facc15', muted: '#a16207' },
{ value: 'blue-dark', labelKey: 'theme.blue', background: '#172554', color: '#60a5fa', muted: '#1e3a8a' }
] as const
const ALL_THEME_VALUES = [...LIGHT_THEMES.map(t => t.value), ...DARK_THEMES.map(t => t.value)] as [string, ...string[]]
const appearanceSchema = (t: (key: string) => string) => z.object({
theme: z.enum(['light', 'dark'], {
theme: z.enum(ALL_THEME_VALUES, {
required_error: t('settings.appearance.validation.theme.required'),
}),
language: z.string({
@@ -76,19 +77,30 @@ const appearanceSchema = (t: (key: string) => string) => z.object({
type AppearanceFormValues = z.infer<ReturnType<typeof appearanceSchema>>
function ThemePreview({ background, primary, muted }: { background: string; primary: string; muted: string }) {
return (
<div className='flex h-12 w-full flex-col gap-1.5 rounded-md border p-2 shadow-sm' style={{ backgroundColor: background, borderColor: 'transparent' }}>
<div className='flex gap-1'>
<span className='h-1.5 w-1.5 rounded-full' style={{ backgroundColor: muted }} />
<span className='h-1.5 w-1.5 rounded-full' style={{ backgroundColor: primary }} />
</div>
<div className='h-1.5 w-3/4 rounded-sm' style={{ backgroundColor: primary }} />
<div className='h-1.5 w-1/2 rounded-sm opacity-50' style={{ backgroundColor: muted }} />
</div>
)
}
export function AppearanceForm() {
const { data: user } = useCurrentUser();
const queryClient = useQueryClient();
const { t, i18n } = useTranslation();
const { theme, setTheme } = useTheme();
const { data: user } = useCurrentUser()
const queryClient = useQueryClient()
const { t, i18n } = useTranslation()
const { theme, setTheme } = useTheme()
const form = useForm<AppearanceFormValues>({
resolver: zodResolver(appearanceSchema(t)),
mode: 'onChange',
defaultValues: {
theme: (theme as 'light' | 'dark') || 'light',
theme: (theme as any) || 'light',
language: i18n.language || 'en',
},
})
@@ -108,21 +120,18 @@ export function AppearanceForm() {
description: (err.response?.data as any)?.message || err.message,
})
},
});
})
function onSubmit(data: AppearanceFormValues) {
i18n.changeLanguage(data.language);
setTheme(data.theme);
mutation.mutate(data);
i18n.changeLanguage(data.language)
setTheme(data.theme as Theme)
mutation.mutate(data)
}
return (
<div className="w-full max-w-6xl ml-0 px-4">
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-8 w-full max-w-screen-xl mx-auto px-4 md:px-6"
>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-10 w-full max-w-screen-xl mx-auto px-4 md:px-6">
<FormField
control={form.control}
name='language'
@@ -182,66 +191,61 @@ export function AppearanceForm() {
</FormItem>
)}
/>
<FormField
control={form.control}
name='theme'
render={({ field }) => (
<FormItem className="space-y-1">
<FormLabel>{t('settings.appearance.field.theme')}</FormLabel>
<FormDescription>
{t('settings.appearance.description.theme')}
</FormDescription>
<FormMessage />
<FormItem className="space-y-4">
<div>
<FormLabel className="text-base">{t('settings.appearance.field.theme')}</FormLabel>
<FormDescription>{t('settings.appearance.description.theme')}</FormDescription>
</div>
<RadioGroup
onValueChange={field.onChange}
onValueChange={(v) => {
setTheme(v as Theme);
field.onChange(v)
}}
defaultValue={field.value}
className='grid max-w-md grid-cols-2 gap-8 pt-2'
className='grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-4 pt-2'
>
<FormItem>
<FormLabel className='[&:has([data-state=checked])>div]:border-primary cursor-pointer'>
<FormControl>
<RadioGroupItem value='light' className='sr-only' />
</FormControl>
<div className='items-center rounded-md border-2 border-muted p-1 hover:border-accent'>
<div className='space-y-2 rounded-sm bg-[#ecedef] p-2'>
<div className='space-y-2 rounded-md bg-white p-2 shadow-sm'>
<div className='h-2 w-[80px] rounded-lg bg-[#ecedef]' />
<div className='h-2 w-[100px] rounded-lg bg-[#ecedef]' />
</div>
<div className='flex items-center space-x-2 rounded-md bg-white p-2 shadow-sm'>
<div className='h-4 w-4 rounded-full bg-[#ecedef]' />
<div className='h-2 w-[100px] rounded-lg bg-[#ecedef]' />
{[...LIGHT_THEMES, ...DARK_THEMES].map((item) => {
const selected = field.value === item.value
return (
<Card
key={item.value}
onClick={() => {
setTheme(item.value as Theme)
field.onChange(item.value)
}}
className={cn(
"cursor-pointer overflow-hidden border-2 transition-all hover:shadow-md",
selected ? "border-primary shadow-md" : "border-muted opacity-80"
)}
>
<div className="p-2">
<ThemePreview
background={item.background}
primary={item.color}
muted={item.muted}
/>
<div className="mt-2 flex items-center justify-between">
<span className="text-[11px] font-medium uppercase truncate">
{t(item.labelKey)}
</span>
{selected && (
<CheckIcon className="h-3.5 w-3.5 text-primary" />
)}
</div>
</div>
</div>
<span className='block w-full p-2 text-center font-normal'>
{t('settings.appearance.theme.light')}
</span>
</FormLabel>
</FormItem>
<FormItem>
<FormLabel className='[&:has([data-state=checked])>div]:border-primary cursor-pointer'>
<FormControl>
<RadioGroupItem value='dark' className='sr-only' />
</FormControl>
<div className='items-center rounded-md border-2 border-muted bg-popover p-1 hover:bg-accent hover:text-accent-foreground'>
<div className='space-y-2 rounded-sm bg-slate-950 p-2'>
<div className='space-y-2 rounded-md bg-slate-800 p-2 shadow-sm'>
<div className='h-2 w-[80px] rounded-lg bg-slate-400' />
<div className='h-2 w-[100px] rounded-lg bg-slate-400' />
</div>
<div className='flex items-center space-x-2 rounded-md bg-slate-800 p-2 shadow-sm'>
<div className='h-4 w-4 rounded-full bg-slate-400' />
<div className='h-2 w-[100px] rounded-lg bg-slate-400' />
</div>
</div>
</div>
<span className='block w-full p-2 text-center font-normal'>
{t('settings.appearance.theme.dark')}
</span>
</FormLabel>
</FormItem>
</Card>
)
})}
</RadioGroup>
<FormMessage />
</FormItem>
)}
/>
+2 -91
View File
@@ -1,101 +1,12 @@
@import "./theme.css";
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 215 28% 17%;
--muted: 210 40% 96.1%;
--muted-foreground: 215 16% 47%;
--popover: 0 0% 100%;
--popover-foreground: 215 28% 17%;
--card: 0 0% 100%;
--card-foreground: 215 28% 17%;
--border: 214 32% 88%;
--input: 214 32% 88%;
--primary: 221 83% 53%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222 47% 11%;
--accent: 210 40% 96.1%;
--accent-foreground: 222 47% 11%;
--destructive: 0 84% 60%;
--destructive-foreground: 210 40% 98%;
--ring: 221 83% 53%;
--radius: 0.35rem;
--chart-1: 221 83% 53%;
--chart-2: 200 90% 40%;
--chart-3: 35 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
--sidebar-background: 210 40% 98%;
--sidebar-foreground: 215 28% 17%;
--sidebar-primary: 221 83% 53%;
--sidebar-primary-foreground: 210 40% 98%;
--sidebar-accent: 214 32% 91%;
--sidebar-accent-foreground: 215 28% 17%;
--sidebar-border: 214 32% 88%;
--sidebar-ring: 221 83% 53%;
}
.dark {
--background: 215 20% 12%;
--foreground: 210 40% 98%;
--card: 215 20% 16%;
--card-foreground: 210 40% 98%;
--popover: 215 20% 16%;
--popover-foreground: 210 40% 98%;
--muted: 215 20% 22%;
--muted-foreground: 215 15% 70%;
--border: 215 20% 28%;
--input: 215 20% 28%;
--primary: 217 90% 70%;
--primary-foreground: 215 20% 12%;
--secondary: 215 20% 22%;
--secondary-foreground: 210 40% 98%;
--accent: 215 20% 22%;
--accent-foreground: 210 40% 98%;
--destructive: 0 70% 60%;
--destructive-foreground: 210 40% 98%;
--ring: 217 90% 70%;
--chart-1: 217 90% 70%;
--chart-2: 200 90% 60%;
--chart-3: 35 90% 65%;
--chart-4: 280 75% 75%;
--chart-5: 340 85% 70%;
--sidebar-background: 215 20% 10%;
--sidebar-foreground: 210 40% 98%;
--sidebar-primary: 217 90% 70%;
--sidebar-primary-foreground: 215 20% 12%;
--sidebar-accent: 215 20% 22%;
--sidebar-accent-foreground: 210 40% 98%;
--sidebar-border: 215 20% 28%;
--sidebar-ring: 217 90% 70%;
}
/* styles.css */
.CollapsibleContent {
overflow: hidden;
}
+452
View File
@@ -0,0 +1,452 @@
/* theme.css */
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 215 28% 17%;
--muted: 210 40% 96.1%;
--muted-foreground: 215 16% 47%;
--popover: 0 0% 100%;
--popover-foreground: 215 28% 17%;
--card: 0 0% 100%;
--card-foreground: 215 28% 17%;
--border: 214 32% 88%;
--input: 214 32% 88%;
--primary: 221 83% 53%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222 47% 11%;
--accent: 210 40% 96.1%;
--accent-foreground: 222 47% 11%;
--destructive: 0 84% 60%;
--destructive-foreground: 210 40% 98%;
--ring: 221 83% 53%;
--radius: 0.35rem;
--chart-1: 221 83% 53%;
--chart-2: 200 90% 40%;
--chart-3: 35 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
--sidebar: var(--background);
--sidebar-foreground: var(--foreground);
--sidebar-primary: var(--primary);
--sidebar-primary-foreground: var(--primary-foreground);
--sidebar-accent: var(--accent);
--sidebar-accent-foreground: var(--accent-foreground);
--sidebar-border: var(--border);
--sidebar-ring: var(--ring);
}
.dark {
--background: 215 20% 12%;
--foreground: 210 40% 98%;
--card: 215 20% 16%;
--card-foreground: 210 40% 98%;
--popover: 215 20% 16%;
--popover-foreground: 210 40% 98%;
--muted: 215 20% 22%;
--muted-foreground: 215 15% 70%;
--border: 215 20% 28%;
--input: 215 20% 28%;
--primary: 217 90% 70%;
--primary-foreground: 215 20% 12%;
--secondary: 215 20% 22%;
--secondary-foreground: 210 40% 98%;
--accent: 215 20% 22%;
--accent-foreground: 210 40% 98%;
--destructive: 0 70% 60%;
--destructive-foreground: 210 40% 98%;
--ring: 217 90% 70%;
--chart-1: 217 90% 70%;
--chart-2: 200 90% 60%;
--chart-3: 35 90% 65%;
--chart-4: 280 75% 75%;
--chart-5: 340 85% 70%;
--sidebar: var(--background);
--sidebar-foreground: var(--foreground);
--sidebar-primary: var(--primary);
--sidebar-primary-foreground: var(--primary-foreground);
--sidebar-accent: var(--accent);
--sidebar-accent-foreground: var(--accent-foreground);
--sidebar-border: var(--border);
--sidebar-ring: var(--ring);
}
.yellow-light {
--radius: 0.5rem;
--background: 180 0% 100%;
--foreground: 49 5% 15%;
--muted: 106 10% 97%;
--muted-foreground: 58 5% 55%;
--popover: 180 0% 100%;
--popover-foreground: 49 5% 15%;
--card: 180 0% 100%;
--card-foreground: 49 5% 15%;
--border: 49 3% 92%;
--input: 49 3% 92%;
--primary: 48 100% 76%;
--primary-foreground: 54 20% 28%;
--secondary: 106 10% 97%;
--secondary-foreground: 56 10% 22%;
--accent: 106 10% 97%;
--accent-foreground: 56 10% 22%;
--destructive: 2 65% 55%;
--destructive-foreground: 106 10% 98%;
--ring: 49 5% 15%;
--chart-1: 15 65% 55%;
--chart-2: 183 45% 50%;
--chart-3: 225 35% 35%;
--chart-4: 88 55% 75%;
--chart-5: 45 60% 65%;
--sidebar: var(--background);
--sidebar-foreground: var(--foreground);
--sidebar-primary: var(--primary);
--sidebar-primary-foreground: var(--primary-foreground);
--sidebar-accent: var(--accent);
--sidebar-accent-foreground: var(--accent-foreground);
--sidebar-border: var(--border);
--sidebar-ring: var(--ring);
}
.yellow-dark {
--radius: 0.5rem;
--background: 49 10% 20%;
--foreground: 106 10% 98%;
--muted: 38 10% 25%;
--muted-foreground: 56 10% 74%;
--popover: 49 10% 23%;
--popover-foreground: 106 10% 98%;
--card: 49 10% 23%;
--card-foreground: 106 10% 98%;
--border: 36 10% 30%;
--input: 36 10% 30%;
--primary: 48 100% 76%;
--primary-foreground: 54 20% 28%;
--secondary: 38 10% 25%;
--secondary-foreground: 106 10% 98%;
--accent: 38 10% 25%;
--accent-foreground: 106 10% 98%;
--destructive: 2 60% 45%;
--destructive-foreground: 106 10% 98%;
--ring: 66 65% 60%;
--chart-1: 262 55% 65%;
--chart-2: 165 40% 70%;
--chart-3: 60 50% 75%;
--chart-4: 312 50% 65%;
--chart-5: 6 55% 65%;
--sidebar: var(--background);
--sidebar-foreground: var(--foreground);
--sidebar-primary: var(--primary);
--sidebar-primary-foreground: var(--primary-foreground);
--sidebar-accent: var(--accent);
--sidebar-accent-foreground: var(--accent-foreground);
--sidebar-border: var(--border);
--sidebar-ring: var(--ring);
}
.rose-light {
--radius: 0.5rem;
--background: 180 0% 100%;
--foreground: 285 5% 15%;
--muted: 286 1% 97%;
--muted-foreground: 286 7% 55%;
--popover: 180 0% 100%;
--popover-foreground: 285 5% 15%;
--card: 180 0% 100%;
--card-foreground: 285 5% 15%;
--border: 286 3% 92%;
--input: 286 3% 92%;
--primary: 350 55% 45%;
--primary-foreground: 355 25% 97%;
--secondary: 286 1% 97%;
--secondary-foreground: 286 5% 21%;
--accent: 286 1% 97%;
--accent-foreground: 286 5% 21%;
--destructive: 2 65% 55%;
--destructive-foreground: 180 0% 98.5%;
--ring: 350 55% 45%;
--chart-1: 15 65% 55%;
--chart-2: 183 45% 50%;
--chart-3: 225 35% 35%;
--chart-4: 88 55% 75%;
--chart-5: 45 60% 65%;
--sidebar: var(--background);
--sidebar-foreground: var(--foreground);
--sidebar-primary: var(--primary);
--sidebar-primary-foreground: var(--primary-foreground);
--sidebar-accent: var(--accent);
--sidebar-accent-foreground: var(--accent-foreground);
--sidebar-border: var(--border);
--sidebar-ring: var(--ring);
}
.rose-dark {
--radius: 0.5rem;
--background: 49 5% 20.5%;
--foreground: 180 0% 96%;
--muted: 168 5% 31%;
--muted-foreground: 286 7% 75%;
--popover: 180 3% 24%;
--popover-foreground: 180 0% 96%;
--card: 56 6% 25%;
--card-foreground: 180 0% 96%;
--border: 286 5% 33%;
--input: 286 5% 33%;
--primary: 350 55% 45%;
--primary-foreground: 355 25% 97%;
--secondary: 286 5% 31%;
--secondary-foreground: 180 0% 98.5%;
--accent: 34 5% 31%;
--accent-foreground: 180 0% 98.5%;
--destructive: 2 60% 45%;
--destructive-foreground: 350 50% 97%;
--ring: 350 60% 50%;
--chart-1: 262 55% 65%;
--chart-2: 165 40% 70%;
--chart-3: 60 50% 75%;
--chart-4: 312 50% 65%;
--chart-5: 6 55% 65%;
--sidebar: var(--background);
--sidebar-foreground: var(--foreground);
--sidebar-primary: var(--primary);
--sidebar-primary-foreground: var(--primary-foreground);
--sidebar-accent: var(--accent);
--sidebar-accent-foreground: var(--accent-foreground);
--sidebar-border: var(--border);
--sidebar-ring: var(--ring);
}
.orange-light {
--radius: 0.5rem;
--background: 180 0% 100%;
--foreground: 49 5% 15%;
--muted: 106 1% 97%;
--muted-foreground: 58 5% 55%;
--popover: 180 0% 100%;
--popover-foreground: 49 5% 15%;
--card: 180 0% 100%;
--card-foreground: 49 5% 15%;
--border: 49 3% 92%;
--input: 49 3% 92%;
--primary: 25 85% 58%;
--primary-foreground: 106 1% 98.5%;
--secondary: 106 1% 97%;
--secondary-foreground: 56 10% 22%;
--accent: 106 1% 97%;
--accent-foreground: 56 10% 22%;
--destructive: 2 65% 55%;
--destructive-foreground: 106 1% 98.5%;
--ring: 25 85% 58%;
--chart-1: 15 65% 55%;
--chart-2: 183 45% 50%;
--chart-3: 225 35% 35%;
--chart-4: 88 55% 75%;
--chart-5: 45 60% 65%;
--sidebar: var(--background);
--sidebar-foreground: var(--foreground);
--sidebar-primary: var(--primary);
--sidebar-primary-foreground: var(--primary-foreground);
--sidebar-accent: var(--accent);
--sidebar-accent-foreground: var(--accent-foreground);
--sidebar-border: var(--border);
--sidebar-ring: var(--ring);
}
.orange-dark {
--radius: 0.5rem;
--background: 49 10% 20.5%;
--foreground: 106 1% 98.5%;
--muted: 34 10% 29%;
--muted-foreground: 56 10% 74%;
--popover: 49 10% 23.5%;
--popover-foreground: 106 1% 98.5%;
--card: 49 10% 23.5%;
--card-foreground: 106 1% 98.5%;
--border: 34 10% 32%;
--input: 34 10% 32%;
--primary: 22 80% 55%;
--primary-foreground: 106 1% 98.5%;
--secondary: 34 10% 29%;
--secondary-foreground: 106 1% 98.5%;
--accent: 34 10% 29%;
--accent-foreground: 106 1% 98.5%;
--destructive: 5 65% 55%;
--destructive-foreground: 106 1% 98.5%;
--ring: 22 75% 62%;
--chart-1: 262 55% 65%;
--chart-2: 165 40% 70%;
--chart-3: 60 50% 75%;
--chart-4: 312 50% 65%;
--chart-5: 6 55% 65%;
--sidebar: var(--background);
--sidebar-foreground: var(--foreground);
--sidebar-primary: var(--primary);
--sidebar-primary-foreground: var(--primary-foreground);
--sidebar-accent: var(--accent);
--sidebar-accent-foreground: var(--accent-foreground);
--sidebar-border: var(--border);
--sidebar-ring: var(--ring);
}
.green-light {
--radius: 0.5rem;
--background: 180 0% 100%;
--foreground: 285 5% 14%;
--muted: 286 1% 97%;
--muted-foreground: 286 7% 55%;
--popover: 180 0% 100%;
--popover-foreground: 285 5% 14%;
--card: 180 0% 100%;
--card-foreground: 285 5% 14%;
--border: 286 3% 92%;
--input: 286 3% 92%;
--primary: 149 55% 48%;
--primary-foreground: 12 25% 97%;
--secondary: 286 1% 97%;
--secondary-foreground: 286 5% 21%;
--accent: 286 1% 97%;
--accent-foreground: 286 5% 21%;
--destructive: 2 65% 55%;
--destructive-foreground: 180 0% 98.5%;
--ring: 149 55% 48%;
--chart-1: 15 65% 55%;
--chart-2: 183 45% 50%;
--chart-3: 225 35% 35%;
--chart-4: 88 55% 75%;
--chart-5: 45 60% 65%;
--sidebar: var(--background);
--sidebar-foreground: var(--foreground);
--sidebar-primary: var(--primary);
--sidebar-primary-foreground: var(--primary-foreground);
--sidebar-accent: var(--accent);
--sidebar-accent-foreground: var(--accent-foreground);
--sidebar-border: var(--border);
--sidebar-ring: var(--ring);
}
.green-dark {
--radius: 0.5rem;
--background: 49 10% 20.5%;
--foreground: 180 0% 96%;
--muted: 168 5% 31%;
--muted-foreground: 286 7% 75%;
--popover: 180 3% 24%;
--popover-foreground: 180 0% 96%;
--card: 56 6% 25%;
--card-foreground: 180 0% 96%;
--border: 286 5% 33%;
--input: 286 5% 33%;
--primary: 150 65% 65%;
--primary-foreground: 153 35% 20%;
--secondary: 286 5% 33%;
--secondary-foreground: 180 0% 98.5%;
--accent: 34 5% 31%;
--accent-foreground: 180 0% 98.5%;
--destructive: 2 60% 45%;
--destructive-foreground: 17 50% 97%;
--ring: 150 55% 55%;
--chart-1: 262 55% 65%;
--chart-2: 165 40% 70%;
--chart-3: 60 50% 75%;
--chart-4: 312 50% 65%;
--chart-5: 6 55% 65%;
--sidebar: var(--background);
--sidebar-foreground: var(--foreground);
--sidebar-primary: var(--primary);
--sidebar-primary-foreground: var(--primary-foreground);
--sidebar-accent: var(--accent);
--sidebar-accent-foreground: var(--accent-foreground);
--sidebar-border: var(--border);
--sidebar-ring: var(--ring);
}
.blue-light {
--radius: 0.5rem;
--background: 180 0% 100%;
--foreground: 222 47% 11%;
--muted: 210 20% 96%;
--muted-foreground: 215 25% 40%;
--popover: 180 0% 100%;
--popover-foreground: 222 47% 11%;
--card: 180 0% 100%;
--card-foreground: 222 47% 11%;
--border: 214 32% 91%;
--input: 214 32% 91%;
--primary: 221 83% 53%; /* 经典的 Shadcn 蓝色 */
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96%;
--secondary-foreground: 222 47% 11%;
--accent: 210 40% 96%;
--accent-foreground: 222 47% 11%;
--destructive: 0 84% 60%;
--destructive-foreground: 210 40% 98%;
--ring: 221 83% 53%;
--chart-1: 221 83% 53%;
--chart-2: 199 89% 48%;
--chart-3: 225 35% 35%;
--chart-4: 201 96% 32%;
--chart-5: 190 90% 40%;
--sidebar: var(--background);
--sidebar-foreground: var(--foreground);
--sidebar-primary: var(--primary);
--sidebar-primary-foreground: var(--primary-foreground);
--sidebar-accent: var(--accent);
--sidebar-accent-foreground: var(--accent-foreground);
--sidebar-border: var(--border);
--sidebar-ring: var(--ring);
}
.blue-dark {
--radius: 0.5rem;
--background: 222 47% 11%;
--foreground: 213 31% 91%;
--muted: 223 47% 11%;
--muted-foreground: 215 20% 65%;
--popover: 222 47% 11%;
--popover-foreground: 213 31% 91%;
--card: 222 47% 11%;
--card-foreground: 213 31% 91%;
--border: 216 34% 17%;
--input: 216 34% 17%;
--primary: 217 91% 60%;
--primary-foreground: 222 47% 11%;
--secondary: 222 47% 11%;
--secondary-foreground: 213 31% 91%;
--accent: 216 34% 17%;
--accent-foreground: 213 31% 91%;
--destructive: 0 63% 31%;
--destructive-foreground: 210 40% 98%;
--ring: 224 76% 48%;
--chart-1: 221 83% 53%;
--chart-2: 165 40% 70%;
--chart-3: 60 50% 75%;
--chart-4: 210 50% 65%;
--chart-5: 200 55% 65%;
--sidebar: var(--background);
--sidebar-foreground: var(--foreground);
--sidebar-primary: var(--primary);
--sidebar-primary-foreground: var(--primary-foreground);
--sidebar-accent: var(--accent);
--sidebar-accent-foreground: var(--accent-foreground);
--sidebar-border: var(--border);
--sidebar-ring: var(--ring);
}
}