Files
mai-bot/dashboard/src/components/background-effects-controls.tsx
DrSmoothl 0811213db0 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.
2026-03-16 22:19:05 +08:00

268 lines
7.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { RotateCcw } from 'lucide-react'
import * as React from 'react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { Slider } from '@/components/ui/slider'
import { hexToHSL } from '@/lib/theme/palette'
import {
type BackgroundEffects,
defaultBackgroundEffects,
} from '@/lib/theme/tokens'
function hslToHex(hsl: string): string {
if (!hsl) return '#000000'
const parts = hsl.split(' ').filter(Boolean)
if (parts.length < 3) return '#000000'
const h = parseFloat(parts[0])
const s = parseFloat(parts[1].replace('%', ''))
const l = parseFloat(parts[2].replace('%', ''))
const sDecimal = s / 100
const lDecimal = l / 100
const c = (1 - Math.abs(2 * lDecimal - 1)) * sDecimal
const x = c * (1 - Math.abs(((h / 60) % 2) - 1))
const m = lDecimal - c / 2
let r = 0
let g = 0
let b = 0
if (h >= 0 && h < 60) {
r = c
g = x
} else if (h >= 60 && h < 120) {
r = x
g = c
} else if (h >= 120 && h < 180) {
g = c
b = x
} else if (h >= 180 && h < 240) {
g = x
b = c
} else if (h >= 240 && h < 300) {
r = x
b = c
} else if (h >= 300 && h < 360) {
r = c
b = x
}
const toHex = (value: number) => {
const hex = Math.round((value + m) * 255).toString(16)
return hex.length === 1 ? `0${hex}` : hex
}
return `#${toHex(r)}${toHex(g)}${toHex(b)}`
}
type BackgroundEffectsControlsProps = {
effects: BackgroundEffects
onChange: (effects: BackgroundEffects) => void
disabled?: boolean
}
export function BackgroundEffectsControls({
effects,
onChange,
disabled = false,
}: BackgroundEffectsControlsProps) {
const handleValueChange = (key: keyof BackgroundEffects, value: number) => {
if (disabled) return
onChange({
...effects,
[key]: value,
})
}
const handleColorChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (disabled) return
const hex = e.target.value
const hsl = hexToHSL(hex)
onChange({
...effects,
overlayColor: hsl,
})
}
const handlePositionChange = (value: string) => {
if (disabled) return
onChange({
...effects,
position: value as BackgroundEffects['position'],
})
}
const handleGradientChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (disabled) return
onChange({
...effects,
gradientOverlay: e.target.value,
})
}
const handleReset = () => {
if (disabled) return
onChange(defaultBackgroundEffects)
}
return (
<div className={disabled ? 'space-y-6 opacity-50' : 'space-y-6'}>
<div className="flex items-center justify-between">
<h3 className="text-sm font-medium"></h3>
<Button
variant="outline"
size="sm"
onClick={handleReset}
disabled={disabled}
className="h-8 px-2 text-xs"
>
<RotateCcw className="mr-2 h-3.5 w-3.5" />
</Button>
</div>
<div className="grid gap-6">
<div className="space-y-3">
<div className="flex items-center justify-between">
<Label> (Blur)</Label>
<span className="text-xs text-muted-foreground">{effects.blur}px</span>
</div>
<Slider
value={[effects.blur]}
min={0}
max={50}
step={1}
disabled={disabled}
onValueChange={(vals) => handleValueChange('blur', vals[0])}
/>
</div>
<div className="space-y-3">
<Label> (Overlay Color)</Label>
<div className="flex items-center gap-3">
<div className="h-9 w-9 overflow-hidden rounded-md border shadow-sm">
<input
type="color"
value={hslToHex(effects.overlayColor)}
onChange={handleColorChange}
disabled={disabled}
className="h-[150%] w-[150%] -translate-x-1/4 -translate-y-1/4 cursor-pointer border-0 p-0"
/>
</div>
<Input
value={hslToHex(effects.overlayColor)}
readOnly
disabled={disabled}
className="flex-1 font-mono uppercase"
/>
</div>
</div>
<div className="space-y-3">
<div className="flex items-center justify-between">
<Label> (Opacity)</Label>
<span className="text-xs text-muted-foreground">
{Math.round(effects.overlayOpacity * 100)}%
</span>
</div>
<Slider
value={[effects.overlayOpacity * 100]}
min={0}
max={100}
step={1}
disabled={disabled}
onValueChange={(vals) => handleValueChange('overlayOpacity', vals[0] / 100)}
/>
</div>
<div className="space-y-3">
<Label> (Position)</Label>
<Select value={effects.position} onValueChange={handlePositionChange} disabled={disabled}>
<SelectTrigger disabled={disabled}>
<SelectValue placeholder="选择位置" />
</SelectTrigger>
<SelectContent>
<SelectItem value="cover"> (Cover)</SelectItem>
<SelectItem value="contain"> (Contain)</SelectItem>
<SelectItem value="center"> (Center)</SelectItem>
<SelectItem value="stretch"> (Stretch)</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-3">
<div className="flex items-center justify-between">
<Label> (Brightness)</Label>
<span className="text-xs text-muted-foreground">{effects.brightness}%</span>
</div>
<Slider
value={[effects.brightness]}
min={0}
max={200}
step={1}
disabled={disabled}
onValueChange={(vals) => handleValueChange('brightness', vals[0])}
/>
</div>
<div className="space-y-3">
<div className="flex items-center justify-between">
<Label> (Contrast)</Label>
<span className="text-xs text-muted-foreground">{effects.contrast}%</span>
</div>
<Slider
value={[effects.contrast]}
min={0}
max={200}
step={1}
disabled={disabled}
onValueChange={(vals) => handleValueChange('contrast', vals[0])}
/>
</div>
<div className="space-y-3">
<div className="flex items-center justify-between">
<Label> (Saturate)</Label>
<span className="text-xs text-muted-foreground">{effects.saturate}%</span>
</div>
<Slider
value={[effects.saturate]}
min={0}
max={200}
step={1}
disabled={disabled}
onValueChange={(vals) => handleValueChange('saturate', vals[0])}
/>
</div>
<div className="space-y-3">
<Label>CSS (Gradient Overlay)</Label>
<Input
value={effects.gradientOverlay || ''}
onChange={handleGradientChange}
disabled={disabled}
placeholder="e.g. linear-gradient(to bottom, transparent, black)"
className="font-mono text-xs"
/>
<p className="text-[10px] text-muted-foreground"> CSS gradient </p>
</div>
</div>
</div>
)
}