fix: preset thumbnails no longer conflict with main preview animation
- Inject only @keyframes (not .animated class rule) per preset thumbnail so the main preview's .animated rule cannot override them - Drive thumbnail animation entirely via inline style properties - Remove isActive/currentName — presets should never appear selected Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -98,7 +98,7 @@ export function AnimationEditor() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Row 4: Preset Library */}
|
{/* Row 4: Preset Library */}
|
||||||
<PresetLibrary currentName={config.name} onSelect={loadPreset} />
|
<PresetLibrary onSelect={loadPreset} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,39 +3,29 @@
|
|||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
import { cn } from '@/lib/utils/cn';
|
|
||||||
import { PRESETS, PRESET_CATEGORIES } from '@/lib/animate/presets';
|
import { PRESETS, PRESET_CATEGORIES } from '@/lib/animate/presets';
|
||||||
import { buildCSS } from '@/lib/animate/cssBuilder';
|
import { buildKeyframesOnly } from '@/lib/animate/cssBuilder';
|
||||||
import type { AnimationConfig, AnimationPreset } from '@/types/animate';
|
import type { AnimationConfig, AnimationPreset } from '@/types/animate';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
currentName: string;
|
|
||||||
onSelect: (config: AnimationConfig) => void;
|
onSelect: (config: AnimationConfig) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function PresetCard({ preset, isActive, onSelect }: {
|
function PresetCard({ preset, onSelect }: {
|
||||||
preset: AnimationPreset;
|
preset: AnimationPreset;
|
||||||
isActive: boolean;
|
|
||||||
onSelect: () => void;
|
onSelect: () => void;
|
||||||
}) {
|
}) {
|
||||||
const styleRef = useRef<HTMLStyleElement | null>(null);
|
const styleRef = useRef<HTMLStyleElement | null>(null);
|
||||||
const key = `preset-${preset.id}`;
|
const animName = `preview-${preset.id}`;
|
||||||
|
|
||||||
// Each card gets its own @keyframes injected with a unique name to avoid conflicts
|
// Inject only the @keyframes block under a unique name — no .animated class rule
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const renamedConfig = {
|
const renamedConfig = { ...preset.config, name: animName };
|
||||||
...preset.config,
|
|
||||||
name: key,
|
|
||||||
keyframes: preset.config.keyframes,
|
|
||||||
};
|
|
||||||
if (!styleRef.current) {
|
if (!styleRef.current) {
|
||||||
styleRef.current = document.createElement('style');
|
styleRef.current = document.createElement('style');
|
||||||
document.head.appendChild(styleRef.current);
|
document.head.appendChild(styleRef.current);
|
||||||
}
|
}
|
||||||
styleRef.current.textContent = buildCSS(renamedConfig).replace(
|
styleRef.current.textContent = buildKeyframesOnly(renamedConfig);
|
||||||
/animation: \S+/,
|
|
||||||
`animation: ${key}`
|
|
||||||
);
|
|
||||||
return () => {
|
return () => {
|
||||||
styleRef.current?.remove();
|
styleRef.current?.remove();
|
||||||
styleRef.current = null;
|
styleRef.current = null;
|
||||||
@@ -43,32 +33,36 @@ function PresetCard({ preset, isActive, onSelect }: {
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Cap thumbnail duration so fast presets loop nicely; slow ones cap at 1.2s
|
||||||
|
const thumbDuration = Math.min(preset.config.duration, 1200);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={onSelect}
|
onClick={onSelect}
|
||||||
className={cn(
|
className="flex flex-col items-center gap-2 p-3 rounded-xl border border-border bg-card/50 transition-all duration-200 hover:border-primary/50 hover:bg-accent/30 hover:shadow-sm"
|
||||||
'group relative flex flex-col items-center gap-2 p-3 rounded-xl border transition-all duration-200 text-left hover:border-primary/50 hover:bg-accent/30',
|
|
||||||
isActive ? 'border-primary bg-primary/5 shadow-sm shadow-primary/20' : 'border-border bg-card/50'
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
{/* Mini preview */}
|
{/* Mini preview — animation driven entirely by inline style, not .animated class */}
|
||||||
<div className="w-full h-14 flex items-center justify-center rounded-lg bg-muted/30 overflow-hidden">
|
<div className="w-full h-14 flex items-center justify-center rounded-lg bg-muted/30 overflow-hidden">
|
||||||
<div
|
<div
|
||||||
className="animated w-8 h-8 rounded-md bg-gradient-to-br from-violet-500 to-purple-600"
|
className="w-8 h-8 rounded-md bg-gradient-to-br from-violet-500 to-purple-600"
|
||||||
style={{ animationName: key, animationDuration: `${preset.config.duration}ms`, animationIterationCount: 'infinite', animationDirection: 'alternate' }}
|
style={{
|
||||||
|
animationName: animName,
|
||||||
|
animationDuration: `${thumbDuration}ms`,
|
||||||
|
animationTimingFunction: preset.config.easing,
|
||||||
|
animationIterationCount: 'infinite',
|
||||||
|
animationDirection: 'alternate',
|
||||||
|
animationFillMode: 'both',
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span className={cn(
|
<span className="text-[11px] font-medium text-center leading-tight text-foreground/80">
|
||||||
'text-[11px] font-medium text-center leading-tight',
|
|
||||||
isActive ? 'text-primary' : 'text-foreground/80'
|
|
||||||
)}>
|
|
||||||
{preset.name}
|
{preset.name}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PresetLibrary({ currentName, onSelect }: Props) {
|
export function PresetLibrary({ onSelect }: Props) {
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@@ -90,7 +84,6 @@ export function PresetLibrary({ currentName, onSelect }: Props) {
|
|||||||
<PresetCard
|
<PresetCard
|
||||||
key={preset.id}
|
key={preset.id}
|
||||||
preset={preset}
|
preset={preset}
|
||||||
isActive={preset.id === currentName}
|
|
||||||
onSelect={() => onSelect(preset.config)}
|
onSelect={() => onSelect(preset.config)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -61,6 +61,24 @@ export function buildAnimationShorthand(config: AnimationConfig): string {
|
|||||||
return `${config.name} ${config.duration}ms ${config.easing}${delay} ${iter} ${config.direction} ${config.fillMode}`;
|
return `${config.name} ${config.duration}ms ${config.easing}${delay} ${iter} ${config.direction} ${config.fillMode}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function buildKeyframesOnly(config: AnimationConfig): string {
|
||||||
|
const sorted = [...config.keyframes].sort((a, b) => a.offset - b.offset);
|
||||||
|
let out = `@keyframes ${config.name} {\n`;
|
||||||
|
for (const kf of sorted) {
|
||||||
|
const lines = buildProperties(kf.properties);
|
||||||
|
if (lines.length === 0) {
|
||||||
|
out += ` ${kf.offset}% { }\n`;
|
||||||
|
} else {
|
||||||
|
out += ` ${kf.offset}% {\n`;
|
||||||
|
for (const line of lines) out += ` ${line};\n`;
|
||||||
|
if (kf.easing) out += ` animation-timing-function: ${kf.easing};\n`;
|
||||||
|
out += ` }\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out += `}\n`;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
export function buildCSS(config: AnimationConfig): string {
|
export function buildCSS(config: AnimationConfig): string {
|
||||||
const sorted = [...config.keyframes].sort((a, b) => a.offset - b.offset);
|
const sorted = [...config.keyframes].sort((a, b) => a.offset - b.offset);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user