Files
kit-ui/components/animate/AnimationEditor.tsx
Sebastian Krüger ea464ef797 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>
2026-03-01 08:48:35 +01:00

163 lines
6.4 KiB
TypeScript

'use client';
import { useState, useCallback } from 'react';
import { AnimationSettings } from './AnimationSettings';
import { AnimationPreview } from './AnimationPreview';
import { KeyframeTimeline } from './KeyframeTimeline';
import { KeyframeProperties } from './KeyframeProperties';
import { PresetLibrary } from './PresetLibrary';
import { ExportPanel } from './ExportPanel';
import { DEFAULT_CONFIG, newKeyframe } from '@/lib/animate/defaults';
import { cn } from '@/lib/utils/cn';
import type { AnimationConfig, KeyframeProperties as KFProps, PreviewElement } from '@/types/animate';
type MobileTab = 'edit' | 'preview';
type RightTab = 'export' | 'presets';
export function AnimationEditor() {
const [config, setConfig] = useState<AnimationConfig>(DEFAULT_CONFIG);
const [selectedId, setSelectedId] = useState<string | null>(
DEFAULT_CONFIG.keyframes[DEFAULT_CONFIG.keyframes.length - 1].id
);
const [previewElement, setPreviewElement] = useState<PreviewElement>('box');
const [mobileTab, setMobileTab] = useState<MobileTab>('edit');
const [rightTab, setRightTab] = useState<RightTab>('export');
const selectedKeyframe = config.keyframes.find((k) => k.id === selectedId) ?? null;
const updateKeyframeProps = useCallback((id: string, props: KFProps) => {
setConfig((c) => ({
...c,
keyframes: c.keyframes.map((k) => k.id === id ? { ...k, properties: props } : k),
}));
}, []);
const addKeyframe = useCallback((offset: number) => {
const kf = newKeyframe(offset);
setConfig((c) => ({ ...c, keyframes: [...c.keyframes, kf] }));
setSelectedId(kf.id);
}, []);
const deleteKeyframe = useCallback((id: string) => {
setConfig((c) => {
if (c.keyframes.length <= 2) return c;
return { ...c, keyframes: c.keyframes.filter((k) => k.id !== id) };
});
setSelectedId((prev) => {
if (prev !== id) return prev;
const remaining = config.keyframes.filter((k) => k.id !== id);
return remaining[remaining.length - 1]?.id ?? null;
});
}, [config.keyframes]);
const moveKeyframe = useCallback((id: string, newOffset: number) => {
const clamped = Math.min(100, Math.max(0, Math.round(newOffset)));
setConfig((c) => ({
...c,
keyframes: c.keyframes.map((k) => k.id === id ? { ...k, offset: clamped } : k),
}));
}, []);
const loadPreset = useCallback((presetConfig: AnimationConfig) => {
setConfig(presetConfig);
setSelectedId(presetConfig.keyframes[presetConfig.keyframes.length - 1].id);
}, []);
const timelineProps = {
keyframes: config.keyframes,
selectedId,
onSelect: setSelectedId,
onAdd: addKeyframe,
onDelete: deleteKeyframe,
onMove: moveKeyframe,
};
return (
<div className="flex flex-col gap-4">
{/* ── Mobile tab switcher ─────────────────────────────── */}
<div className="flex lg:hidden glass rounded-xl p-1 gap-1">
{(['edit', 'preview'] as MobileTab[]).map((t) => (
<button
key={t}
onClick={() => setMobileTab(t)}
className={cn(
'flex-1 py-2.5 rounded-lg text-sm font-medium capitalize transition-all',
mobileTab === t
? 'bg-primary text-primary-foreground shadow-sm'
: 'text-muted-foreground hover:text-foreground'
)}
>
{t === 'edit' ? 'Edit' : 'Preview'}
</button>
))}
</div>
{/* ── Main layout ─────────────────────────────────────── */}
<div
className="grid grid-cols-1 lg:grid-cols-5 gap-4"
style={{ height: 'calc(100svh - 220px)', minHeight: '660px' }}
>
{/* Left: Settings + Properties */}
<div className={cn('lg:col-span-2 flex flex-col overflow-hidden', mobileTab !== 'edit' && 'hidden lg:flex')}>
<div className="glass rounded-xl p-4 flex flex-col flex-1 min-h-0 overflow-hidden">
<div className="flex-1 min-h-0 overflow-y-auto scrollbar-thin scrollbar-thumb-primary/20 scrollbar-track-transparent pr-0.5 space-y-5">
<AnimationSettings config={config} onChange={setConfig} />
<div className="border-t border-border/25" />
{/* Timeline — embedded inside edit panel on mobile, hidden on desktop */}
<div className="lg:hidden">
<KeyframeTimeline {...timelineProps} embedded />
</div>
<div className="lg:hidden border-t border-border/25" />
<KeyframeProperties keyframe={selectedKeyframe} onChange={updateKeyframeProps} />
</div>
</div>
</div>
{/* Right: Preview + Timeline + Export/Presets */}
<div className={cn('lg:col-span-3 flex flex-col gap-3 overflow-hidden', mobileTab !== 'preview' && 'hidden lg:flex')}>
{/* Preview canvas */}
<AnimationPreview config={config} element={previewElement} onElementChange={setPreviewElement} />
{/* Timeline — standalone on desktop */}
<div className="hidden lg:block shrink-0">
<KeyframeTimeline {...timelineProps} />
</div>
{/* Export / Presets tab panel */}
<div className="glass rounded-xl p-4 flex flex-col flex-1 min-h-0 overflow-hidden">
{/* Tab switcher */}
<div className="flex glass rounded-lg p-0.5 gap-0.5 mb-4 shrink-0">
{(['export', 'presets'] as RightTab[]).map((t) => (
<button
key={t}
onClick={() => setRightTab(t)}
className={cn(
'flex-1 py-1.5 rounded-md text-xs font-medium capitalize transition-all',
rightTab === t
? 'bg-primary text-primary-foreground shadow-sm'
: 'text-muted-foreground hover:text-foreground'
)}
>
{t === 'export' ? 'Export' : 'Presets'}
</button>
))}
</div>
{/* Content */}
<div className="flex-1 min-h-0 overflow-y-auto scrollbar-thin scrollbar-thumb-primary/20 scrollbar-track-transparent pr-0.5">
{rightTab === 'export' && <ExportPanel config={config} />}
{rightTab === 'presets' && <PresetLibrary onSelect={loadPreset} />}
</div>
</div>
</div>
</div>
</div>
);
}