feat: enhance background layer handling and uploader functionality

- Introduced automatic overlay opacity and gradient based on layer ID in BackgroundLayer component.
- Added disabled state to BackgroundUploader, preventing actions when disabled.
- Updated component CSS editor to handle disabled state, preventing changes when disabled.
- Modified Header and Layout components to manage background inheritance from the page layer.
- Improved Sidebar and Card components to respect background inheritance and layering.
- Refactored theme management to include default accent color and normalization functions.
- Enhanced AppearanceTab to manage accent color changes with debouncing and validation.
- Added UI feedback for inherited background layers in AppearanceTab.
This commit is contained in:
DrSmoothl
2026-03-16 22:19:05 +08:00
parent a5a6d2cb26
commit 0811213db0
16 changed files with 359 additions and 170 deletions

View File

@@ -3,6 +3,8 @@
* 统一管理所有前端 localStorage 设置
*/
import { DEFAULT_ACCENT_COLOR_HSL } from './theme/palette'
// 所有设置的 key 定义
export const STORAGE_KEYS = {
// 外观设置
@@ -32,7 +34,7 @@ export const STORAGE_KEYS = {
export const DEFAULT_SETTINGS = {
// 外观
theme: 'system' as 'light' | 'dark' | 'system',
accentColor: 'blue',
accentColor: DEFAULT_ACCENT_COLOR_HSL,
enableAnimations: true,
enableWavesBackground: true,

View File

@@ -6,6 +6,9 @@ type HSL = {
l: number
}
export const DEFAULT_ACCENT_COLOR_HSL = '188.5 100% 45.5%'
export const DEFAULT_ACCENT_COLOR_HEX = '#00c7e8'
const clamp = (value: number, min: number, max: number): number => {
if (value < min) return min
if (value > max) return max
@@ -45,6 +48,11 @@ export const formatHSL = (h: number, s: number, l: number): string => {
return `${safeH} ${safeS}% ${safeL}%`
}
export const isValidHSLString = (value: string): boolean => {
const cleaned = value.trim()
return /^-?\d+(?:\.\d+)?\s+-?\d+(?:\.\d+)?%\s+-?\d+(?:\.\d+)?%$/i.test(cleaned)
}
export const hexToHSL = (hex: string): string => {
let cleaned = hex.trim().replace('#', '')
if (cleaned.length === 3) {
@@ -91,6 +99,25 @@ export const hexToHSL = (hex: string): string => {
return formatHSL(h, s * 100, l * 100)
}
export const normalizeAccentColor = (accentColor?: string | null): string => {
const trimmed = accentColor?.trim()
if (!trimmed) {
return DEFAULT_ACCENT_COLOR_HSL
}
if (trimmed.startsWith('#')) {
return hexToHSL(trimmed)
}
if (isValidHSLString(trimmed)) {
const { h, s, l } = parseHSL(trimmed)
return formatHSL(h, s, l)
}
return DEFAULT_ACCENT_COLOR_HSL
}
export const adjustLightness = (hsl: string, amount: number): string => {
const { h, s, l } = parseHSL(hsl)
return formatHSL(h, s, l + amount)
@@ -170,8 +197,13 @@ export const generatePalette = (accentHSL: string, isDark: boolean): ColorTokens
const chartSteps = [0, 72, 144, 216, 288]
const charts = chartSteps.map((step) => rotateHue(chartBase, step))
const card = adjustLightness(background, isDark ? 2 : -1)
const popover = adjustLightness(background, isDark ? 3 : -0.5)
const surfaceSaturation = clamp(accent.s * (isDark ? 0.18 : 0.14), isDark ? 10 : 6, isDark ? 24 : 16)
const card = formatHSL(accent.h, surfaceSaturation, isDark ? 8.8 : 98.6)
const popover = formatHSL(
accent.h,
clamp(surfaceSaturation + (isDark ? 3 : 2), 0, 100),
isDark ? 10.5 : 99.3,
)
return {
primary,

View File

@@ -3,6 +3,7 @@
* 统一处理主题相关的存储操作,包括加载、保存、导出、导入和迁移旧 key
*/
import { DEFAULT_ACCENT_COLOR_HSL, normalizeAccentColor } from './palette'
import type { BackgroundConfigMap, UserThemeConfig } from './tokens'
/**
@@ -23,7 +24,7 @@ export const THEME_STORAGE_KEYS = {
*/
const DEFAULT_THEME_CONFIG: UserThemeConfig = {
selectedPreset: 'light',
accentColor: 'blue',
accentColor: DEFAULT_ACCENT_COLOR_HSL,
tokenOverrides: {},
customCSS: '',
backgroundConfig: {} as BackgroundConfigMap,
@@ -65,7 +66,7 @@ export function loadThemeConfig(): UserThemeConfig {
return {
selectedPreset: preset || DEFAULT_THEME_CONFIG.selectedPreset,
accentColor: accent || DEFAULT_THEME_CONFIG.accentColor,
accentColor: normalizeAccentColor(accent),
tokenOverrides,
customCSS: customCSS || DEFAULT_THEME_CONFIG.customCSS,
backgroundConfig,
@@ -79,7 +80,7 @@ export function loadThemeConfig(): UserThemeConfig {
*/
export function saveThemeConfig(config: UserThemeConfig): void {
localStorage.setItem(THEME_STORAGE_KEYS.PRESET, config.selectedPreset)
localStorage.setItem(THEME_STORAGE_KEYS.ACCENT, config.accentColor)
localStorage.setItem(THEME_STORAGE_KEYS.ACCENT, normalizeAccentColor(config.accentColor))
localStorage.setItem(THEME_STORAGE_KEYS.OVERRIDES, JSON.stringify(config.tokenOverrides))
localStorage.setItem(THEME_STORAGE_KEYS.CUSTOM_CSS, config.customCSS)
if (config.backgroundConfig) {
@@ -215,7 +216,7 @@ export function migrateOldKeys(): void {
const newAccent = localStorage.getItem(THEME_STORAGE_KEYS.ACCENT)
if (accentColor && !newAccent) {
localStorage.setItem(THEME_STORAGE_KEYS.ACCENT, accentColor)
localStorage.setItem(THEME_STORAGE_KEYS.ACCENT, normalizeAccentColor(accentColor))
}
// 删除旧 key

View File

@@ -154,27 +154,27 @@ export type UserThemeConfig = {
export const defaultLightTokens: ThemeTokens = {
color: {
primary: '221.2 83.2% 53.3%',
primary: '188.5 100% 45.5%',
'primary-foreground': '210 40% 98%',
'primary-gradient': 'none',
secondary: '210 40% 96.1%',
secondary: '188.5 35% 96%',
'secondary-foreground': '222.2 47.4% 11.2%',
muted: '210 40% 96.1%',
'muted-foreground': '215.4 16.3% 46.9%',
accent: '210 40% 96.1%',
muted: '188.5 12% 96%',
'muted-foreground': '188.5 20% 46.9%',
accent: '223.5 60% 50.4%',
'accent-foreground': '222.2 47.4% 11.2%',
destructive: '0 84.2% 60.2%',
'destructive-foreground': '210 40% 98%',
background: '0 0% 100%',
foreground: '222.2 84% 4.9%',
card: '0 0% 100%',
card: '188.5 14% 98.6%',
'card-foreground': '222.2 84% 4.9%',
popover: '0 0% 100%',
popover: '188.5 16% 99.3%',
'popover-foreground': '222.2 84% 4.9%',
border: '214.3 31.8% 91.4%',
input: '214.3 31.8% 91.4%',
ring: '221.2 83.2% 53.3%',
'chart-1': '221.2 83.2% 53.3%',
border: '188.5 20% 91.4%',
input: '188.5 20% 91.4%',
ring: '188.5 100% 45.5%',
'chart-1': '188.5 100% 45.5%',
'chart-2': '160 60% 45%',
'chart-3': '30 80% 55%',
'chart-4': '280 65% 60%',
@@ -249,27 +249,27 @@ export const defaultLightTokens: ThemeTokens = {
export const defaultDarkTokens: ThemeTokens = {
color: {
primary: '217.2 91.2% 59.8%',
primary: '188.5 100% 45.5%',
'primary-foreground': '210 40% 98%',
'primary-gradient': 'none',
secondary: '217.2 32.6% 17.5%',
secondary: '188.5 35% 17.5%',
'secondary-foreground': '210 40% 98%',
muted: '217.2 32.6% 17.5%',
'muted-foreground': '215 20.2% 65.1%',
accent: '217.2 32.6% 17.5%',
muted: '188.5 12% 17.5%',
'muted-foreground': '188.5 20% 65.1%',
accent: '223.5 60% 35.3%',
'accent-foreground': '210 40% 98%',
destructive: '0 62.8% 30.6%',
'destructive-foreground': '210 40% 98%',
background: '222.2 84% 4.9%',
foreground: '210 40% 98%',
card: '222.2 84% 4.9%',
card: '188.5 18% 8.8%',
'card-foreground': '210 40% 98%',
popover: '222.2 84% 4.9%',
popover: '188.5 21% 10.5%',
'popover-foreground': '210 40% 98%',
border: '217.2 32.6% 17.5%',
input: '217.2 32.6% 17.5%',
ring: '224.3 76.3% 48%',
'chart-1': '217.2 91.2% 59.8%',
border: '188.5 20% 17.5%',
input: '188.5 20% 17.5%',
ring: '188.5 100% 45.5%',
'chart-1': '188.5 100% 45.5%',
'chart-2': '160 60% 50%',
'chart-3': '30 80% 60%',
'chart-4': '280 65% 65%',