2025-11-21 17:42:36 +01:00
|
|
|
'use client';
|
|
|
|
|
|
|
|
|
|
import { useState } from 'react';
|
|
|
|
|
import { useLayerEffectsStore } from '@/store/layer-effects-store';
|
|
|
|
|
import { useToastStore } from '@/store/toast-store';
|
|
|
|
|
import type { EffectType, LayerEffect } from '@/types/layer-effects';
|
|
|
|
|
import {
|
|
|
|
|
Sparkles,
|
|
|
|
|
Eye,
|
|
|
|
|
EyeOff,
|
|
|
|
|
Trash2,
|
|
|
|
|
Copy,
|
|
|
|
|
Plus,
|
|
|
|
|
Settings,
|
|
|
|
|
ChevronDown,
|
|
|
|
|
ChevronRight,
|
|
|
|
|
} from 'lucide-react';
|
|
|
|
|
import { cn } from '@/lib/utils';
|
|
|
|
|
|
|
|
|
|
interface LayerEffectsPanelProps {
|
|
|
|
|
layerId: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const EFFECT_NAMES: Record<EffectType, string> = {
|
|
|
|
|
dropShadow: 'Drop Shadow',
|
|
|
|
|
innerShadow: 'Inner Shadow',
|
|
|
|
|
outerGlow: 'Outer Glow',
|
|
|
|
|
innerGlow: 'Inner Glow',
|
|
|
|
|
stroke: 'Stroke',
|
|
|
|
|
bevel: 'Bevel & Emboss',
|
|
|
|
|
colorOverlay: 'Color Overlay',
|
|
|
|
|
gradientOverlay: 'Gradient Overlay',
|
|
|
|
|
patternOverlay: 'Pattern Overlay',
|
|
|
|
|
satin: 'Satin',
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-21 17:53:59 +01:00
|
|
|
const EFFECT_ICONS: Record<EffectType, React.ReactElement> = {
|
2025-11-21 17:42:36 +01:00
|
|
|
dropShadow: <Sparkles className="h-3 w-3" />,
|
|
|
|
|
innerShadow: <Sparkles className="h-3 w-3" />,
|
|
|
|
|
outerGlow: <Sparkles className="h-3 w-3" />,
|
|
|
|
|
innerGlow: <Sparkles className="h-3 w-3" />,
|
|
|
|
|
stroke: <Sparkles className="h-3 w-3" />,
|
|
|
|
|
bevel: <Sparkles className="h-3 w-3" />,
|
|
|
|
|
colorOverlay: <Sparkles className="h-3 w-3" />,
|
|
|
|
|
gradientOverlay: <Sparkles className="h-3 w-3" />,
|
|
|
|
|
patternOverlay: <Sparkles className="h-3 w-3" />,
|
|
|
|
|
satin: <Sparkles className="h-3 w-3" />,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export function LayerEffectsPanel({ layerId }: LayerEffectsPanelProps) {
|
|
|
|
|
const {
|
|
|
|
|
getLayerEffects,
|
|
|
|
|
addEffect,
|
|
|
|
|
removeEffect,
|
|
|
|
|
toggleEffect,
|
|
|
|
|
duplicateEffect,
|
|
|
|
|
setLayerEffectsEnabled,
|
|
|
|
|
} = useLayerEffectsStore();
|
|
|
|
|
const { addToast } = useToastStore();
|
|
|
|
|
|
|
|
|
|
const [isExpanded, setIsExpanded] = useState(true);
|
|
|
|
|
const [showAddMenu, setShowAddMenu] = useState(false);
|
|
|
|
|
|
|
|
|
|
const effectsConfig = getLayerEffects(layerId);
|
|
|
|
|
const hasEffects = effectsConfig && effectsConfig.effects.length > 0;
|
|
|
|
|
|
|
|
|
|
const handleAddEffect = (effectType: EffectType) => {
|
|
|
|
|
addEffect(layerId, effectType);
|
|
|
|
|
setShowAddMenu(false);
|
|
|
|
|
addToast(`Added ${EFFECT_NAMES[effectType]}`, 'success');
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleRemoveEffect = (effectId: string, effectType: EffectType) => {
|
|
|
|
|
removeEffect(layerId, effectId);
|
|
|
|
|
addToast(`Removed ${EFFECT_NAMES[effectType]}`, 'success');
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleToggleEffect = (effectId: string) => {
|
|
|
|
|
toggleEffect(layerId, effectId);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleDuplicateEffect = (effectId: string, effectType: EffectType) => {
|
|
|
|
|
duplicateEffect(layerId, effectId);
|
|
|
|
|
addToast(`Duplicated ${EFFECT_NAMES[effectType]}`, 'success');
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleToggleAll = () => {
|
|
|
|
|
if (effectsConfig) {
|
|
|
|
|
setLayerEffectsEnabled(layerId, !effectsConfig.enabled);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (!hasEffects && !isExpanded) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="border-t border-border p-2">
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setIsExpanded(true)}
|
|
|
|
|
className="flex items-center gap-2 text-xs text-muted-foreground hover:text-foreground transition-colors w-full"
|
|
|
|
|
>
|
|
|
|
|
<ChevronRight className="h-3 w-3" />
|
|
|
|
|
<Sparkles className="h-3 w-3" />
|
|
|
|
|
<span>Layer Effects</span>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="border-t border-border">
|
|
|
|
|
{/* Header */}
|
|
|
|
|
<div className="flex items-center justify-between p-2 bg-accent/50">
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setIsExpanded(!isExpanded)}
|
|
|
|
|
className="flex items-center gap-2 text-xs font-medium text-card-foreground hover:text-foreground transition-colors"
|
|
|
|
|
>
|
|
|
|
|
{isExpanded ? (
|
|
|
|
|
<ChevronDown className="h-3 w-3" />
|
|
|
|
|
) : (
|
|
|
|
|
<ChevronRight className="h-3 w-3" />
|
|
|
|
|
)}
|
|
|
|
|
<Sparkles className="h-3 w-3" />
|
|
|
|
|
<span>Layer Effects</span>
|
|
|
|
|
{hasEffects && (
|
|
|
|
|
<span className="text-xs text-muted-foreground">
|
|
|
|
|
({effectsConfig.effects.length})
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
{hasEffects && (
|
|
|
|
|
<button
|
|
|
|
|
onClick={handleToggleAll}
|
|
|
|
|
className="p-1 hover:bg-accent rounded transition-colors"
|
|
|
|
|
title={effectsConfig.enabled ? 'Disable all effects' : 'Enable all effects'}
|
|
|
|
|
>
|
|
|
|
|
{effectsConfig.enabled ? (
|
|
|
|
|
<Eye className="h-3 w-3" />
|
|
|
|
|
) : (
|
|
|
|
|
<EyeOff className="h-3 w-3" />
|
|
|
|
|
)}
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{isExpanded && (
|
|
|
|
|
<div className="p-2 space-y-1">
|
|
|
|
|
{/* Effects List */}
|
|
|
|
|
{hasEffects && (
|
|
|
|
|
<div className="space-y-1 mb-2">
|
|
|
|
|
{effectsConfig.effects.map((effect) => (
|
|
|
|
|
<div
|
|
|
|
|
key={effect.id}
|
|
|
|
|
className={cn(
|
|
|
|
|
'group flex items-center gap-2 p-2 rounded border transition-colors',
|
|
|
|
|
effect.enabled
|
|
|
|
|
? 'border-border bg-background'
|
|
|
|
|
: 'border-border/50 bg-muted/30'
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
{/* Effect Icon & Name */}
|
|
|
|
|
<div className="flex items-center gap-2 flex-1 min-w-0">
|
|
|
|
|
<span className="text-muted-foreground">
|
|
|
|
|
{EFFECT_ICONS[effect.type]}
|
|
|
|
|
</span>
|
|
|
|
|
<span
|
|
|
|
|
className={cn(
|
|
|
|
|
'text-xs truncate',
|
|
|
|
|
effect.enabled ? 'text-foreground' : 'text-muted-foreground'
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
{EFFECT_NAMES[effect.type]}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Actions */}
|
|
|
|
|
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => handleToggleEffect(effect.id)}
|
|
|
|
|
className="p-1 hover:bg-accent rounded transition-colors"
|
|
|
|
|
title={effect.enabled ? 'Disable effect' : 'Enable effect'}
|
|
|
|
|
>
|
|
|
|
|
{effect.enabled ? (
|
|
|
|
|
<Eye className="h-3 w-3" />
|
|
|
|
|
) : (
|
|
|
|
|
<EyeOff className="h-3 w-3" />
|
|
|
|
|
)}
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => handleDuplicateEffect(effect.id, effect.type)}
|
|
|
|
|
className="p-1 hover:bg-accent rounded transition-colors"
|
|
|
|
|
title="Duplicate effect"
|
|
|
|
|
>
|
|
|
|
|
<Copy className="h-3 w-3" />
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => handleRemoveEffect(effect.id, effect.type)}
|
|
|
|
|
className="p-1 hover:bg-destructive/10 hover:text-destructive rounded transition-colors"
|
|
|
|
|
title="Remove effect"
|
|
|
|
|
>
|
|
|
|
|
<Trash2 className="h-3 w-3" />
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Add Effect Button */}
|
|
|
|
|
<div className="relative">
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setShowAddMenu(!showAddMenu)}
|
|
|
|
|
className="w-full flex items-center justify-center gap-2 p-2 text-xs bg-primary text-primary-foreground hover:bg-primary/90 rounded transition-colors"
|
|
|
|
|
>
|
|
|
|
|
<Plus className="h-3 w-3" />
|
|
|
|
|
<span>Add Effect</span>
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
{/* Add Effect Menu */}
|
|
|
|
|
{showAddMenu && (
|
|
|
|
|
<>
|
|
|
|
|
<div
|
|
|
|
|
className="fixed inset-0 z-[100]"
|
|
|
|
|
onClick={() => setShowAddMenu(false)}
|
|
|
|
|
/>
|
|
|
|
|
<div className="absolute bottom-full left-0 right-0 mb-1 z-[101] bg-card border border-border rounded-md shadow-lg py-1 max-h-48 overflow-y-auto">
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => handleAddEffect('dropShadow')}
|
|
|
|
|
className="w-full flex items-center gap-2 px-3 py-1.5 text-xs hover:bg-accent transition-colors text-left"
|
|
|
|
|
>
|
|
|
|
|
<Sparkles className="h-3 w-3" />
|
|
|
|
|
<span>Drop Shadow</span>
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => handleAddEffect('outerGlow')}
|
|
|
|
|
className="w-full flex items-center gap-2 px-3 py-1.5 text-xs hover:bg-accent transition-colors text-left"
|
|
|
|
|
>
|
|
|
|
|
<Sparkles className="h-3 w-3" />
|
|
|
|
|
<span>Outer Glow</span>
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => handleAddEffect('stroke')}
|
|
|
|
|
className="w-full flex items-center gap-2 px-3 py-1.5 text-xs hover:bg-accent transition-colors text-left"
|
|
|
|
|
>
|
|
|
|
|
<Sparkles className="h-3 w-3" />
|
|
|
|
|
<span>Stroke</span>
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => handleAddEffect('colorOverlay')}
|
|
|
|
|
className="w-full flex items-center gap-2 px-3 py-1.5 text-xs hover:bg-accent transition-colors text-left"
|
|
|
|
|
>
|
|
|
|
|
<Sparkles className="h-3 w-3" />
|
|
|
|
|
<span>Color Overlay</span>
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => handleAddEffect('innerShadow')}
|
|
|
|
|
className="w-full flex items-center gap-2 px-3 py-1.5 text-xs hover:bg-accent transition-colors text-left"
|
|
|
|
|
>
|
|
|
|
|
<Sparkles className="h-3 w-3" />
|
|
|
|
|
<span>Inner Shadow</span>
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => handleAddEffect('innerGlow')}
|
|
|
|
|
className="w-full flex items-center gap-2 px-3 py-1.5 text-xs hover:bg-accent transition-colors text-left"
|
|
|
|
|
>
|
|
|
|
|
<Sparkles className="h-3 w-3" />
|
|
|
|
|
<span>Inner Glow</span>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|