Skip to content

Commit 0811213

Browse files
committed
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.
1 parent a5a6d2c commit 0811213

16 files changed

Lines changed: 359 additions & 170 deletions

dashboard/src/components/background-effects-controls.tsx

Lines changed: 35 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,9 @@ import {
1818
defaultBackgroundEffects,
1919
} from '@/lib/theme/tokens'
2020

21-
// ============================================================================
22-
// Helper Functions
23-
// ============================================================================
24-
25-
/**
26-
* 将 HSL 字符串转换为 HEX 格式
27-
* (从 settings.tsx 移植)
28-
*/
2921
function hslToHex(hsl: string): string {
3022
if (!hsl) return '#000000'
3123

32-
// 解析 "221.2 83.2% 53.3%" 格式
3324
const parts = hsl.split(' ').filter(Boolean)
3425
if (parts.length < 3) return '#000000'
3526

@@ -39,72 +30,65 @@ function hslToHex(hsl: string): string {
3930

4031
const sDecimal = s / 100
4132
const lDecimal = l / 100
42-
4333
const c = (1 - Math.abs(2 * lDecimal - 1)) * sDecimal
4434
const x = c * (1 - Math.abs(((h / 60) % 2) - 1))
4535
const m = lDecimal - c / 2
4636

47-
let r = 0,
48-
g = 0,
49-
b = 0
37+
let r = 0
38+
let g = 0
39+
let b = 0
5040

5141
if (h >= 0 && h < 60) {
5242
r = c
5343
g = x
54-
b = 0
5544
} else if (h >= 60 && h < 120) {
5645
r = x
5746
g = c
58-
b = 0
5947
} else if (h >= 120 && h < 180) {
60-
r = 0
6148
g = c
6249
b = x
6350
} else if (h >= 180 && h < 240) {
64-
r = 0
6551
g = x
6652
b = c
6753
} else if (h >= 240 && h < 300) {
6854
r = x
69-
g = 0
7055
b = c
7156
} else if (h >= 300 && h < 360) {
7257
r = c
73-
g = 0
7458
b = x
7559
}
7660

77-
const toHex = (n: number) => {
78-
const hex = Math.round((n + m) * 255).toString(16)
79-
return hex.length === 1 ? '0' + hex : hex
61+
const toHex = (value: number) => {
62+
const hex = Math.round((value + m) * 255).toString(16)
63+
return hex.length === 1 ? `0${hex}` : hex
8064
}
8165

8266
return `#${toHex(r)}${toHex(g)}${toHex(b)}`
8367
}
8468

85-
// ============================================================================
86-
// Component
87-
// ============================================================================
88-
8969
type BackgroundEffectsControlsProps = {
9070
effects: BackgroundEffects
9171
onChange: (effects: BackgroundEffects) => void
72+
disabled?: boolean
9273
}
9374

9475
export function BackgroundEffectsControls({
9576
effects,
9677
onChange,
78+
disabled = false,
9779
}: BackgroundEffectsControlsProps) {
98-
// 处理数值变更
9980
const handleValueChange = (key: keyof BackgroundEffects, value: number) => {
81+
if (disabled) return
82+
10083
onChange({
10184
...effects,
10285
[key]: value,
10386
})
10487
}
10588

106-
// 处理颜色变更
10789
const handleColorChange = (e: React.ChangeEvent<HTMLInputElement>) => {
90+
if (disabled) return
91+
10892
const hex = e.target.value
10993
const hsl = hexToHSL(hex)
11094
onChange({
@@ -113,35 +97,38 @@ export function BackgroundEffectsControls({
11397
})
11498
}
11599

116-
// 处理位置变更
117100
const handlePositionChange = (value: string) => {
101+
if (disabled) return
102+
118103
onChange({
119104
...effects,
120105
position: value as BackgroundEffects['position'],
121106
})
122107
}
123108

124-
// 处理渐变变更
125109
const handleGradientChange = (e: React.ChangeEvent<HTMLInputElement>) => {
110+
if (disabled) return
111+
126112
onChange({
127113
...effects,
128114
gradientOverlay: e.target.value,
129115
})
130116
}
131117

132-
// 重置为默认值
133118
const handleReset = () => {
119+
if (disabled) return
134120
onChange(defaultBackgroundEffects)
135121
}
136122

137123
return (
138-
<div className="space-y-6">
124+
<div className={disabled ? 'space-y-6 opacity-50' : 'space-y-6'}>
139125
<div className="flex items-center justify-between">
140126
<h3 className="text-sm font-medium">背景效果调节</h3>
141127
<Button
142128
variant="outline"
143129
size="sm"
144130
onClick={handleReset}
131+
disabled={disabled}
145132
className="h-8 px-2 text-xs"
146133
>
147134
<RotateCcw className="mr-2 h-3.5 w-3.5" />
@@ -150,24 +137,21 @@ export function BackgroundEffectsControls({
150137
</div>
151138

152139
<div className="grid gap-6">
153-
{/* 1. Blur (模糊) */}
154140
<div className="space-y-3">
155141
<div className="flex items-center justify-between">
156142
<Label>模糊程度 (Blur)</Label>
157-
<span className="text-xs text-muted-foreground">
158-
{effects.blur}px
159-
</span>
143+
<span className="text-xs text-muted-foreground">{effects.blur}px</span>
160144
</div>
161145
<Slider
162146
value={[effects.blur]}
163147
min={0}
164148
max={50}
165149
step={1}
150+
disabled={disabled}
166151
onValueChange={(vals) => handleValueChange('blur', vals[0])}
167152
/>
168153
</div>
169154

170-
{/* 2. Overlay Color (遮罩颜色) */}
171155
<div className="space-y-3">
172156
<Label>遮罩颜色 (Overlay Color)</Label>
173157
<div className="flex items-center gap-3">
@@ -176,18 +160,19 @@ export function BackgroundEffectsControls({
176160
type="color"
177161
value={hslToHex(effects.overlayColor)}
178162
onChange={handleColorChange}
163+
disabled={disabled}
179164
className="h-[150%] w-[150%] -translate-x-1/4 -translate-y-1/4 cursor-pointer border-0 p-0"
180165
/>
181166
</div>
182167
<Input
183168
value={hslToHex(effects.overlayColor)}
184169
readOnly
170+
disabled={disabled}
185171
className="flex-1 font-mono uppercase"
186172
/>
187173
</div>
188174
</div>
189175

190-
{/* 3. Overlay Opacity (遮罩不透明度) */}
191176
<div className="space-y-3">
192177
<div className="flex items-center justify-between">
193178
<Label>遮罩不透明度 (Opacity)</Label>
@@ -200,17 +185,15 @@ export function BackgroundEffectsControls({
200185
min={0}
201186
max={100}
202187
step={1}
203-
onValueChange={(vals) =>
204-
handleValueChange('overlayOpacity', vals[0] / 100)
205-
}
188+
disabled={disabled}
189+
onValueChange={(vals) => handleValueChange('overlayOpacity', vals[0] / 100)}
206190
/>
207191
</div>
208192

209-
{/* 4. Position (位置) */}
210193
<div className="space-y-3">
211194
<Label>背景位置 (Position)</Label>
212-
<Select value={effects.position} onValueChange={handlePositionChange}>
213-
<SelectTrigger>
195+
<Select value={effects.position} onValueChange={handlePositionChange} disabled={disabled}>
196+
<SelectTrigger disabled={disabled}>
214197
<SelectValue placeholder="选择位置" />
215198
</SelectTrigger>
216199
<SelectContent>
@@ -222,69 +205,61 @@ export function BackgroundEffectsControls({
222205
</Select>
223206
</div>
224207

225-
{/* 5. Brightness (亮度) */}
226208
<div className="space-y-3">
227209
<div className="flex items-center justify-between">
228210
<Label>亮度 (Brightness)</Label>
229-
<span className="text-xs text-muted-foreground">
230-
{effects.brightness}%
231-
</span>
211+
<span className="text-xs text-muted-foreground">{effects.brightness}%</span>
232212
</div>
233213
<Slider
234214
value={[effects.brightness]}
235215
min={0}
236216
max={200}
237217
step={1}
218+
disabled={disabled}
238219
onValueChange={(vals) => handleValueChange('brightness', vals[0])}
239220
/>
240221
</div>
241222

242-
{/* 6. Contrast (对比度) */}
243223
<div className="space-y-3">
244224
<div className="flex items-center justify-between">
245225
<Label>对比度 (Contrast)</Label>
246-
<span className="text-xs text-muted-foreground">
247-
{effects.contrast}%
248-
</span>
226+
<span className="text-xs text-muted-foreground">{effects.contrast}%</span>
249227
</div>
250228
<Slider
251229
value={[effects.contrast]}
252230
min={0}
253231
max={200}
254232
step={1}
233+
disabled={disabled}
255234
onValueChange={(vals) => handleValueChange('contrast', vals[0])}
256235
/>
257236
</div>
258237

259-
{/* 7. Saturate (饱和度) */}
260238
<div className="space-y-3">
261239
<div className="flex items-center justify-between">
262240
<Label>饱和度 (Saturate)</Label>
263-
<span className="text-xs text-muted-foreground">
264-
{effects.saturate}%
265-
</span>
241+
<span className="text-xs text-muted-foreground">{effects.saturate}%</span>
266242
</div>
267243
<Slider
268244
value={[effects.saturate]}
269245
min={0}
270246
max={200}
271247
step={1}
248+
disabled={disabled}
272249
onValueChange={(vals) => handleValueChange('saturate', vals[0])}
273250
/>
274251
</div>
275252

276-
{/* 8. Gradient Overlay (渐变叠加) */}
277253
<div className="space-y-3">
278254
<Label>CSS 渐变叠加 (Gradient Overlay)</Label>
279255
<Input
280256
value={effects.gradientOverlay || ''}
281257
onChange={handleGradientChange}
258+
disabled={disabled}
282259
placeholder="e.g. linear-gradient(to bottom, transparent, black)"
283260
className="font-mono text-xs"
284261
/>
285-
<p className="text-[10px] text-muted-foreground">
286-
可选:输入有效的 CSS gradient 字符串
287-
</p>
262+
<p className="text-[10px] text-muted-foreground">可选:输入有效的 CSS gradient 字符串</p>
288263
</div>
289264
</div>
290265
</div>

dashboard/src/components/background-layer.tsx

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,31 @@ type BackgroundLayerProps = {
88
layerId: string
99
}
1010

11+
function getAutoOverlayOpacity(layerId: string): number {
12+
switch (layerId) {
13+
case 'page':
14+
return 0.62
15+
case 'header':
16+
return 0.72
17+
case 'sidebar':
18+
return 0.78
19+
case 'card':
20+
return 0.82
21+
case 'dialog':
22+
return 0.88
23+
default:
24+
return 0.68
25+
}
26+
}
27+
28+
function getAutoGradientOverlay(layerId: string): string | undefined {
29+
if (layerId !== 'page') {
30+
return undefined
31+
}
32+
33+
return 'linear-gradient(to bottom, hsl(var(--background) / 0.82), hsl(var(--background) / 0.52) 28%, hsl(var(--background) / 0.7) 100%)'
34+
}
35+
1136
function buildFilterString(effects: BackgroundConfig['effects']): string {
1237
const parts: string[] = []
1338
if (effects.blur > 0) parts.push(`blur(${effects.blur}px)`)
@@ -84,10 +109,17 @@ export function BackgroundLayer({ config, layerId }: BackgroundLayerProps) {
84109

85110
const filterString = buildFilterString(config.effects)
86111
const { overlayColor, overlayOpacity, gradientOverlay } = config.effects
112+
const hasExplicitOverlay = overlayOpacity > 0
113+
const effectiveOverlayOpacity = hasExplicitOverlay ? overlayOpacity : getAutoOverlayOpacity(layerId)
114+
const effectiveOverlayColor = hasExplicitOverlay
115+
? `hsl(${overlayColor} / ${effectiveOverlayOpacity})`
116+
: `hsl(var(--background) / ${effectiveOverlayOpacity})`
117+
const effectiveGradientOverlay = gradientOverlay || getAutoGradientOverlay(layerId)
87118

88119
return (
89120
<div
90121
key={layerId}
122+
data-background-layer={layerId}
91123
style={{
92124
position: 'absolute',
93125
inset: 0,
@@ -136,25 +168,25 @@ export function BackgroundLayer({ config, layerId }: BackgroundLayerProps) {
136168
/>
137169
)}
138170

139-
{overlayOpacity > 0 && (
171+
{effectiveOverlayOpacity > 0 && (
140172
<div
141173
style={{
142174
position: 'absolute',
143175
inset: 0,
144176
zIndex: 1,
145-
backgroundColor: `hsl(${overlayColor} / ${overlayOpacity})`,
177+
backgroundColor: effectiveOverlayColor,
146178
pointerEvents: 'none',
147179
}}
148180
/>
149181
)}
150182

151-
{gradientOverlay && (
183+
{effectiveGradientOverlay && (
152184
<div
153185
style={{
154186
position: 'absolute',
155187
inset: 0,
156188
zIndex: 2,
157-
background: gradientOverlay,
189+
background: effectiveGradientOverlay,
158190
pointerEvents: 'none',
159191
}}
160192
/>

0 commit comments

Comments
 (0)