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,10 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Slider } from '@/components/ui/slider';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { MousePointerClick } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils/cn';
|
||||
import type { Keyframe, KeyframeProperties, TransformValue } from '@/types/animate';
|
||||
@@ -28,26 +24,20 @@ interface SliderRowProps {
|
||||
function SliderRow({ label, unit, value, min, max, step = 1, onChange }: SliderRowProps) {
|
||||
return (
|
||||
<div className="grid grid-cols-[1fr_auto] gap-x-3 items-center">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[10px] text-muted-foreground">
|
||||
{label}{unit && <span className="text-muted-foreground/50"> ({unit})</span>}
|
||||
</Label>
|
||||
<Slider
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
value={[value]}
|
||||
onValueChange={([v]) => onChange(v)}
|
||||
/>
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-[9px] text-muted-foreground/50 font-mono">
|
||||
{label}{unit && <span className="opacity-50"> ({unit})</span>}
|
||||
</label>
|
||||
<Slider min={min} max={max} step={step} value={[value]} onValueChange={([v]) => onChange(v)} />
|
||||
</div>
|
||||
<Input
|
||||
<input
|
||||
type="number"
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
value={value}
|
||||
onChange={(e) => onChange(Number(e.target.value))}
|
||||
className="w-16 text-xs px-1.5 h-7 mt-4"
|
||||
className="w-14 bg-transparent border border-border/40 rounded-md px-1.5 py-1 text-[10px] font-mono text-center outline-none focus:border-primary/50 transition-colors text-foreground/80 mt-4"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -56,15 +46,12 @@ function SliderRow({ label, unit, value, min, max, step = 1, onChange }: SliderR
|
||||
export function KeyframeProperties({ keyframe, onChange }: Props) {
|
||||
if (!keyframe) {
|
||||
return (
|
||||
<Card className="h-full">
|
||||
<CardHeader>
|
||||
<CardTitle>Properties</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col items-center justify-center py-12 text-center">
|
||||
<MousePointerClick className="h-8 w-8 mx-auto mb-3 opacity-20" />
|
||||
<p className="text-xs text-muted-foreground">Select a keyframe on the timeline to edit its properties</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div className="flex flex-col items-center justify-center py-12 text-center gap-3">
|
||||
<MousePointerClick className="w-7 h-7 text-muted-foreground/20" />
|
||||
<p className="text-[10px] text-muted-foreground/40 font-mono leading-relaxed max-w-[180px]">
|
||||
Select a keyframe on the timeline to edit its properties
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -72,10 +59,7 @@ export function KeyframeProperties({ keyframe, onChange }: Props) {
|
||||
const t: TransformValue = { ...DEFAULT_TRANSFORM, ...props.transform };
|
||||
|
||||
const setTransform = (key: keyof TransformValue, value: number) => {
|
||||
onChange(keyframe.id, {
|
||||
...props,
|
||||
transform: { ...t, [key]: value },
|
||||
});
|
||||
onChange(keyframe.id, { ...props, transform: { ...t, [key]: value } });
|
||||
};
|
||||
|
||||
const setProp = <K extends keyof KeyframeProperties>(key: K, value: KeyframeProperties[K]) => {
|
||||
@@ -85,96 +69,77 @@ export function KeyframeProperties({ keyframe, onChange }: Props) {
|
||||
const hasBg = props.backgroundColor && props.backgroundColor !== 'none';
|
||||
|
||||
return (
|
||||
<Card className="h-full overflow-auto">
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
<div className="space-y-5">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[10px] font-semibold text-muted-foreground uppercase tracking-widest">
|
||||
Properties
|
||||
<span className="text-muted-foreground font-normal text-sm ml-2">{keyframe.offset}%</span>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-5">
|
||||
</span>
|
||||
<span className="text-[9px] text-primary/60 font-mono bg-primary/10 px-1.5 py-0.5 rounded">
|
||||
{keyframe.offset}%
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Transform */}
|
||||
<div className="space-y-3">
|
||||
<p className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">Transform</p>
|
||||
<SliderRow label="Translate X" unit="px" value={t.translateX} min={-500} max={500} onChange={(v) => setTransform('translateX', v)} />
|
||||
<SliderRow label="Translate Y" unit="px" value={t.translateY} min={-500} max={500} onChange={(v) => setTransform('translateY', v)} />
|
||||
<SliderRow label="Rotate" unit="°" value={t.rotate} min={-360} max={360} onChange={(v) => setTransform('rotate', v)} />
|
||||
<SliderRow label="Scale X" value={t.scaleX} min={0} max={3} step={0.01} onChange={(v) => setTransform('scaleX', v)} />
|
||||
<SliderRow label="Scale Y" value={t.scaleY} min={0} max={3} step={0.01} onChange={(v) => setTransform('scaleY', v)} />
|
||||
<SliderRow label="Skew X" unit="°" value={t.skewX} min={-90} max={90} onChange={(v) => setTransform('skewX', v)} />
|
||||
<SliderRow label="Skew Y" unit="°" value={t.skewY} min={-90} max={90} onChange={(v) => setTransform('skewY', v)} />
|
||||
</div>
|
||||
{/* Transform */}
|
||||
<div className="space-y-3">
|
||||
<p className="text-[9px] font-semibold uppercase tracking-wider text-muted-foreground/50">Transform</p>
|
||||
<SliderRow label="Translate X" unit="px" value={t.translateX} min={-500} max={500} onChange={(v) => setTransform('translateX', v)} />
|
||||
<SliderRow label="Translate Y" unit="px" value={t.translateY} min={-500} max={500} onChange={(v) => setTransform('translateY', v)} />
|
||||
<SliderRow label="Rotate" unit="°" value={t.rotate} min={-360} max={360} onChange={(v) => setTransform('rotate', v)} />
|
||||
<SliderRow label="Scale X" value={t.scaleX} min={0} max={3} step={0.01} onChange={(v) => setTransform('scaleX', v)} />
|
||||
<SliderRow label="Scale Y" value={t.scaleY} min={0} max={3} step={0.01} onChange={(v) => setTransform('scaleY', v)} />
|
||||
<SliderRow label="Skew X" unit="°" value={t.skewX} min={-90} max={90} onChange={(v) => setTransform('skewX', v)} />
|
||||
<SliderRow label="Skew Y" unit="°" value={t.skewY} min={-90} max={90} onChange={(v) => setTransform('skewY', v)} />
|
||||
</div>
|
||||
|
||||
{/* Visual */}
|
||||
<div className="space-y-3">
|
||||
<p className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">Visual</p>
|
||||
{/* Visual */}
|
||||
<div className="space-y-3">
|
||||
<p className="text-[9px] font-semibold uppercase tracking-wider text-muted-foreground/50">Visual</p>
|
||||
<SliderRow label="Opacity" value={props.opacity ?? 1} min={0} max={1} step={0.01} onChange={(v) => setProp('opacity', v)} />
|
||||
|
||||
<SliderRow
|
||||
label="Opacity"
|
||||
value={props.opacity ?? 1}
|
||||
min={0} max={1} step={0.01}
|
||||
onChange={(v) => setProp('opacity', v)}
|
||||
/>
|
||||
|
||||
{/* Background color */}
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-[10px] text-muted-foreground">Background Color</Label>
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
type="color"
|
||||
value={hasBg ? props.backgroundColor! : '#8b5cf6'}
|
||||
onChange={(e) => setProp('backgroundColor', e.target.value)}
|
||||
disabled={!hasBg}
|
||||
className={cn('w-9 h-9 p-1 shrink-0 cursor-pointer', !hasBg && 'opacity-30')}
|
||||
/>
|
||||
<Input
|
||||
type="text"
|
||||
value={hasBg ? props.backgroundColor! : ''}
|
||||
onChange={(e) => setProp('backgroundColor', e.target.value)}
|
||||
disabled={!hasBg}
|
||||
placeholder="none"
|
||||
className="font-mono text-xs flex-1"
|
||||
/>
|
||||
<Button
|
||||
size="xs"
|
||||
variant={hasBg ? 'default' : 'outline'}
|
||||
onClick={() => setProp('backgroundColor', hasBg ? 'none' : '#8b5cf6')}
|
||||
className="shrink-0"
|
||||
>
|
||||
{hasBg ? 'On' : 'Off'}
|
||||
</Button>
|
||||
</div>
|
||||
{/* Background color */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-1.5">
|
||||
<label className="text-[9px] text-muted-foreground/50 font-mono">Background Color</label>
|
||||
<button
|
||||
onClick={() => setProp('backgroundColor', hasBg ? 'none' : '#8b5cf6')}
|
||||
className={cn(
|
||||
'text-[9px] font-mono px-1.5 py-0.5 rounded border transition-all',
|
||||
hasBg
|
||||
? 'border-primary/40 text-primary bg-primary/10'
|
||||
: 'border-border/30 text-muted-foreground/50 hover:border-primary/30 hover:text-primary'
|
||||
)}
|
||||
>
|
||||
{hasBg ? 'On' : 'Off'}
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex gap-1.5">
|
||||
<input
|
||||
type="color"
|
||||
value={hasBg ? props.backgroundColor! : '#8b5cf6'}
|
||||
onChange={(e) => setProp('backgroundColor', e.target.value)}
|
||||
disabled={!hasBg}
|
||||
className={cn('w-8 h-8 rounded-lg cursor-pointer border border-border/40 bg-transparent shrink-0 p-0.5', !hasBg && 'opacity-30 cursor-not-allowed')}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={hasBg ? props.backgroundColor! : ''}
|
||||
onChange={(e) => setProp('backgroundColor', e.target.value)}
|
||||
disabled={!hasBg}
|
||||
placeholder="none"
|
||||
className="flex-1 bg-transparent border border-border/40 rounded-lg px-3 py-1.5 text-xs font-mono outline-none focus:border-primary/50 transition-colors text-foreground/80 placeholder:text-muted-foreground/30 disabled:opacity-30"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<SliderRow
|
||||
label="Border Radius"
|
||||
unit="px"
|
||||
value={props.borderRadius ?? 0}
|
||||
min={0} max={200}
|
||||
onChange={(v) => setProp('borderRadius', v)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Filters */}
|
||||
<div className="space-y-3">
|
||||
<p className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">Filter</p>
|
||||
<SliderRow
|
||||
label="Blur"
|
||||
unit="px"
|
||||
value={props.blur ?? 0}
|
||||
min={0} max={50}
|
||||
onChange={(v) => setProp('blur', v)}
|
||||
/>
|
||||
<SliderRow
|
||||
label="Brightness"
|
||||
value={props.brightness ?? 1}
|
||||
min={0} max={3} step={0.01}
|
||||
onChange={(v) => setProp('brightness', v)}
|
||||
/>
|
||||
</div>
|
||||
<SliderRow label="Border Radius" unit="px" value={props.borderRadius ?? 0} min={0} max={200} onChange={(v) => setProp('borderRadius', v)} />
|
||||
</div>
|
||||
|
||||
</CardContent>
|
||||
</Card>
|
||||
{/* Filters */}
|
||||
<div className="space-y-3">
|
||||
<p className="text-[9px] font-semibold uppercase tracking-wider text-muted-foreground/50">Filter</p>
|
||||
<SliderRow label="Blur" unit="px" value={props.blur ?? 0} min={0} max={50} onChange={(v) => setProp('blur', v)} />
|
||||
<SliderRow label="Brightness" value={props.brightness ?? 1} min={0} max={3} step={0.01} onChange={(v) => setProp('brightness', v)} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user