refactor: align animate tool with Calculate/Media blueprint
Layout: - AnimationEditor: lg:grid-cols-5 (2/5 edit, 3/5 visual); full viewport height; mobile Edit|Preview glass pill tabs; timeline embedded in edit panel on mobile, standalone on desktop; Export|Presets custom tab panel at the bottom of the right column Components (all shadcn removed): - AnimationSettings: Card/Label/Input/Select/Button → native inputs; direction & fill mode as 4-pill selectors; easing as native <select>; ∞ iterations as icon pill toggle - AnimationPreview: Card/ToggleGroup/Button → glass card; speed pills as inline glass pill group; element picker as compact icon pills; playback controls as glass icon buttons; subtle grid bg on canvas - KeyframeTimeline: Card/Button → glass card; embedded prop for rendering inside another card on mobile without double glass - KeyframeProperties: Card/Label/Input/Button → bare content section; SliderRow uses native number input; bg color toggle as pill button - ExportPanel: Card/Tabs/Button → bare section; CSS|Tailwind custom tab switcher; dark terminal (#06060e) code blocks - PresetLibrary: Card/Tabs → bare section; category pills replace Tabs; preset cards use glass border-border/20 bg-primary/3 styling Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,24 +1,20 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
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 } from '@/types/animate';
|
||||
import type { AnimationConfig, AnimationPreset, PresetCategory } from '@/types/animate';
|
||||
|
||||
interface Props {
|
||||
onSelect: (config: AnimationConfig) => void;
|
||||
}
|
||||
|
||||
function PresetCard({ preset, onSelect }: {
|
||||
preset: AnimationPreset;
|
||||
onSelect: () => 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);
|
||||
|
||||
// Inject only the @keyframes block under a unique name — no .animated class rule
|
||||
useEffect(() => {
|
||||
const renamedConfig = { ...preset.config, name: animName };
|
||||
if (!styleRef.current) {
|
||||
@@ -26,25 +22,18 @@ function PresetCard({ preset, onSelect }: {
|
||||
document.head.appendChild(styleRef.current);
|
||||
}
|
||||
styleRef.current.textContent = buildKeyframesOnly(renamedConfig);
|
||||
return () => {
|
||||
styleRef.current?.remove();
|
||||
styleRef.current = null;
|
||||
};
|
||||
return () => { styleRef.current?.remove(); styleRef.current = null; };
|
||||
// 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 (
|
||||
<button
|
||||
onClick={onSelect}
|
||||
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"
|
||||
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"
|
||||
>
|
||||
{/* 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-12 flex items-center justify-center rounded-lg bg-white/3 overflow-hidden">
|
||||
<div
|
||||
className="w-8 h-8 rounded-md bg-gradient-to-br from-violet-500 to-purple-600"
|
||||
className="w-7 h-7 rounded-md bg-gradient-to-br from-violet-500 to-purple-600"
|
||||
style={{
|
||||
animationName: animName,
|
||||
animationDuration: `${thumbDuration}ms`,
|
||||
@@ -55,7 +44,7 @@ function PresetCard({ preset, onSelect }: {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-[11px] font-medium text-center leading-tight text-foreground/80">
|
||||
<span className="text-[10px] font-mono text-center leading-tight text-foreground/60 group-hover:text-foreground/80 transition-colors">
|
||||
{preset.name}
|
||||
</span>
|
||||
</button>
|
||||
@@ -63,35 +52,32 @@ function PresetCard({ preset, onSelect }: {
|
||||
}
|
||||
|
||||
export function PresetLibrary({ onSelect }: Props) {
|
||||
const [category, setCategory] = useState<PresetCategory>(PRESET_CATEGORIES[0]);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Presets</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Tabs defaultValue="Entrance">
|
||||
<TabsList className="mb-4">
|
||||
{PRESET_CATEGORIES.map((cat) => (
|
||||
<TabsTrigger key={cat} value={cat} className="text-xs">
|
||||
{cat}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
<div className="space-y-3">
|
||||
<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) => (
|
||||
<TabsContent key={cat} value={cat}>
|
||||
<div className="grid grid-cols-3 sm:grid-cols-4 lg:grid-cols-6 gap-2">
|
||||
{PRESETS.filter((p) => p.category === cat).map((preset) => (
|
||||
<PresetCard
|
||||
key={preset.id}
|
||||
preset={preset}
|
||||
onSelect={() => onSelect(preset.config)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</TabsContent>
|
||||
<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>
|
||||
))}
|
||||
</Tabs>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user