Also restore scroll handling to ExportPanel and PresetLibrary, and remove maxHeight cap from CodeSnippet in ExportPanel. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
84 lines
3.2 KiB
TypeScript
84 lines
3.2 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useRef, useState } from 'react';
|
|
import { cn } from '@/lib/utils/cn';
|
|
import { PRESETS, PRESET_CATEGORIES } from '@/lib/animate/presets';
|
|
import { buildKeyframesOnly } from '@/lib/animate/cssBuilder';
|
|
import type { AnimationConfig, AnimationPreset, PresetCategory } from '@/types/animate';
|
|
|
|
interface Props {
|
|
onSelect: (config: AnimationConfig) => void;
|
|
}
|
|
|
|
function PresetCard({ preset, onSelect }: { preset: AnimationPreset; onSelect: () => void }) {
|
|
const styleRef = useRef<HTMLStyleElement | null>(null);
|
|
const animName = `preview-${preset.id}`;
|
|
const thumbDuration = Math.min(preset.config.duration, 1200);
|
|
|
|
useEffect(() => {
|
|
const renamedConfig = { ...preset.config, name: animName };
|
|
if (!styleRef.current) {
|
|
styleRef.current = document.createElement('style');
|
|
document.head.appendChild(styleRef.current);
|
|
}
|
|
styleRef.current.textContent = buildKeyframesOnly(renamedConfig);
|
|
return () => { styleRef.current?.remove(); styleRef.current = null; };
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, []);
|
|
|
|
return (
|
|
<button
|
|
onClick={onSelect}
|
|
className="flex flex-col items-center gap-2 p-3 rounded-xl border border-border/20 bg-primary/3 transition-all hover:border-primary/40 hover:bg-primary/8 group"
|
|
>
|
|
<div className="w-full h-12 flex items-center justify-center rounded-lg bg-white/3 overflow-hidden">
|
|
<div
|
|
className="w-7 h-7 rounded-md bg-gradient-to-br from-violet-500 to-purple-600"
|
|
style={{
|
|
animationName: animName,
|
|
animationDuration: `${thumbDuration}ms`,
|
|
animationTimingFunction: preset.config.easing,
|
|
animationIterationCount: 'infinite',
|
|
animationDirection: 'alternate',
|
|
animationFillMode: 'both',
|
|
}}
|
|
/>
|
|
</div>
|
|
<span className="text-[10px] font-mono text-center leading-tight text-foreground/60 group-hover:text-foreground/80 transition-colors">
|
|
{preset.name}
|
|
</span>
|
|
</button>
|
|
);
|
|
}
|
|
|
|
export function PresetLibrary({ onSelect }: Props) {
|
|
const [category, setCategory] = useState<PresetCategory>(PRESET_CATEGORIES[0]);
|
|
|
|
return (
|
|
<div className="space-y-3 overflow-y-auto scrollbar-thin scrollbar-thumb-primary/20 scrollbar-track-transparent pr-0.5">
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-[10px] font-semibold text-muted-foreground uppercase tracking-widest">Presets</span>
|
|
<div className="flex glass rounded-lg p-0.5 gap-0.5">
|
|
{PRESET_CATEGORIES.map((cat) => (
|
|
<button
|
|
key={cat}
|
|
onClick={() => setCategory(cat)}
|
|
className={cn(
|
|
'px-2 py-1 rounded-md text-[10px] font-mono transition-all',
|
|
category === cat ? 'bg-primary text-primary-foreground' : 'text-muted-foreground hover:text-foreground'
|
|
)}
|
|
>
|
|
{cat}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
<div className="grid grid-cols-3 sm:grid-cols-4 gap-2">
|
|
{PRESETS.filter((p) => p.category === category).map((preset) => (
|
|
<PresetCard key={preset.id} preset={preset} onSelect={() => onSelect(preset.config)} />
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|