diff --git a/components/animate/AnimationEditor.tsx b/components/animate/AnimationEditor.tsx index 86bf710..43afb69 100644 --- a/components/animate/AnimationEditor.tsx +++ b/components/animate/AnimationEditor.tsx @@ -8,14 +8,20 @@ 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(DEFAULT_CONFIG); const [selectedId, setSelectedId] = useState( DEFAULT_CONFIG.keyframes[DEFAULT_CONFIG.keyframes.length - 1].id ); const [previewElement, setPreviewElement] = useState('box'); + const [mobileTab, setMobileTab] = useState('edit'); + const [rightTab, setRightTab] = useState('export'); const selectedKeyframe = config.keyframes.find((k) => k.id === selectedId) ?? null; @@ -35,8 +41,7 @@ export function AnimationEditor() { const deleteKeyframe = useCallback((id: string) => { setConfig((c) => { if (c.keyframes.length <= 2) return c; - const next = c.keyframes.filter((k) => k.id !== id); - return { ...c, keyframes: next }; + return { ...c, keyframes: c.keyframes.filter((k) => k.id !== id) }; }); setSelectedId((prev) => { if (prev !== id) return prev; @@ -58,47 +63,100 @@ export function AnimationEditor() { setSelectedId(presetConfig.keyframes[presetConfig.keyframes.length - 1].id); }, []); + const timelineProps = { + keyframes: config.keyframes, + selectedId, + onSelect: setSelectedId, + onAdd: addKeyframe, + onDelete: deleteKeyframe, + onMove: moveKeyframe, + }; + return ( -
- {/* Row 1: Settings + Preview */} -
-
- -
-
- -
+
+ + {/* ── Mobile tab switcher ─────────────────────────────── */} +
+ {(['edit', 'preview'] as MobileTab[]).map((t) => ( + + ))}
- {/* Row 2: Keyframe Timeline */} - + {/* ── Main layout ─────────────────────────────────────── */} +
- {/* Row 3: Keyframe Properties + Export */} -
-
- + {/* Left: Settings + Properties */} +
+
+
+ + + +
+ + {/* Timeline — embedded inside edit panel on mobile, hidden on desktop */} +
+ +
+
+ + +
+
-
- + + {/* Right: Preview + Timeline + Export/Presets */} +
+ + {/* Preview canvas */} + + + {/* Timeline — standalone on desktop */} +
+ +
+ + {/* Export / Presets tab panel */} +
+ {/* Tab switcher */} +
+ {(['export', 'presets'] as RightTab[]).map((t) => ( + + ))} +
+ {/* Content */} +
+ {rightTab === 'export' && } + {rightTab === 'presets' && } +
+
- - {/* Row 4: Preset Library */} -
); } diff --git a/components/animate/AnimationPreview.tsx b/components/animate/AnimationPreview.tsx index 8cb0b63..55ddbb5 100644 --- a/components/animate/AnimationPreview.tsx +++ b/components/animate/AnimationPreview.tsx @@ -1,10 +1,8 @@ 'use client'; import { useEffect, useRef, useState } from 'react'; -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; -import { Button } from '@/components/ui/button'; -import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group'; import { Play, Pause, RotateCcw, Square, Circle, Type } from 'lucide-react'; +import { cn } from '@/lib/utils/cn'; import { buildCSS } from '@/lib/animate/cssBuilder'; import type { AnimationConfig, PreviewElement } from '@/types/animate'; @@ -23,13 +21,26 @@ const SPEEDS: { label: string; value: string }[] = [ { label: '2×', value: '2' }, ]; +const ELEMENTS: { value: PreviewElement; icon: React.ReactNode; title: string }[] = [ + { value: 'box', icon: , title: 'Box' }, + { value: 'circle', icon: , title: 'Circle' }, + { value: 'text', icon: , title: 'Text' }, +]; + +const actionBtn = 'flex items-center justify-center w-7 h-7 glass rounded-md border border-border/30 text-muted-foreground hover:text-primary hover:border-primary/30 transition-all disabled:opacity-40 disabled:cursor-not-allowed'; + +const pillCls = (active: boolean) => + cn( + 'px-2 py-0.5 rounded text-[10px] font-mono transition-all', + active ? 'text-primary bg-primary/10' : 'text-muted-foreground/50 hover:text-muted-foreground' + ); + export function AnimationPreview({ config, element, onElementChange }: Props) { const styleRef = useRef(null); const [restartKey, setRestartKey] = useState(0); const [animState, setAnimState] = useState('playing'); const [speed, setSpeed] = useState('1'); - // Inject @keyframes CSS into document head useEffect(() => { if (!styleRef.current) { styleRef.current = document.createElement('style'); @@ -37,125 +48,113 @@ export function AnimationPreview({ config, element, onElementChange }: Props) { document.head.appendChild(styleRef.current); } styleRef.current.textContent = buildCSS(config); - // Restart preview whenever config changes so changes are immediately visible setAnimState('playing'); setRestartKey((k) => k + 1); }, [config]); - // Cleanup on unmount useEffect(() => { return () => { styleRef.current?.remove(); }; }, []); - const restart = () => { - setAnimState('playing'); - setRestartKey((k) => k + 1); - }; - - const handlePlay = () => { - if (animState === 'ended') { - // Animation finished — restart it - restart(); - } else { - setAnimState('playing'); - } - }; + const restart = () => { setAnimState('playing'); setRestartKey((k) => k + 1); }; const scaledDuration = Math.round(config.duration / Number(speed)); const isInfinite = config.iterationCount === 'infinite'; return ( - - - Preview - v && setSpeed(v)} variant="outline" size="sm"> +
+ {/* Header: speed pills */} +
+ Preview +
{SPEEDS.map((s) => ( - + ))} - - - - {/* Preview canvas */} -
- {/* Grid overlay */} -
+
+
- {/* Animated element */} -
!isInfinite && setAnimState('ended')} + {/* Canvas */} +
+
!isInfinite && setAnimState('ended')} + > + {element === 'box' && ( +
+ )} + {element === 'circle' && ( +
+ )} + {element === 'text' && ( + + Hello + + )} +
+
+ + {/* Controls: element selector + playback */} +
+ {/* Element picker */} +
+ {ELEMENTS.map(({ value, icon, title }) => ( + + ))} +
+ + {/* Playback */} +
+ + +
- - {/* Controls */} -
- v && onElementChange(v as PreviewElement)} variant="outline" size="sm"> - - - - - - - - - - - -
- - - -
-
- - +
+
); } diff --git a/components/animate/AnimationSettings.tsx b/components/animate/AnimationSettings.tsx index 56a840f..459fd9c 100644 --- a/components/animate/AnimationSettings.tsx +++ b/components/animate/AnimationSettings.tsx @@ -1,17 +1,7 @@ 'use client'; -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; -import { Input } from '@/components/ui/input'; -import { Label } from '@/components/ui/label'; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from '@/components/ui/select'; -import { Button } from '@/components/ui/button'; import { Infinity } from 'lucide-react'; +import { cn } from '@/lib/utils/cn'; import type { AnimationConfig } from '@/types/animate'; interface Props { @@ -30,14 +20,38 @@ const EASINGS = [ { value: 'steps(8, end)', label: 'Steps (8)' }, ]; +const DIRECTIONS: { value: AnimationConfig['direction']; label: string }[] = [ + { value: 'normal', label: 'Normal' }, + { value: 'reverse', label: 'Reverse' }, + { value: 'alternate', label: 'Alt' }, + { value: 'alternate-reverse', label: 'Alt-Rev' }, +]; + +const FILL_MODES: { value: AnimationConfig['fillMode']; label: string }[] = [ + { value: 'none', label: 'None' }, + { value: 'forwards', label: 'Fwd' }, + { value: 'backwards', label: 'Bwd' }, + { value: 'both', label: 'Both' }, +]; + +const inputCls = + 'w-full bg-transparent border border-border/40 rounded-lg px-3 py-2 text-xs font-mono outline-none focus:border-primary/50 transition-colors text-foreground/80 placeholder:text-muted-foreground/30'; + +const pillCls = (active: boolean) => + cn( + 'flex-1 py-1.5 rounded-lg border text-[10px] font-mono transition-all', + active + ? 'bg-primary/10 border-primary/40 text-primary' + : 'border-border/30 text-muted-foreground hover:border-primary/30 hover:text-foreground' + ); + export function AnimationSettings({ config, onChange }: Props) { const set = (key: K, value: AnimationConfig[K]) => onChange({ ...config, [key]: value }); const isInfinite = config.iterationCount === 'infinite'; - const isCubic = config.easing === 'cubic-bezier'; + const isCubic = config.easing.startsWith('cubic-bezier'); - // Parse cubic-bezier values from string like "cubic-bezier(x1,y1,x2,y2)" const cubicValues = (() => { const m = config.easing.match(/cubic-bezier\(([^)]+)\)/); if (!m) return [0.25, 0.1, 0.25, 1.0]; @@ -50,167 +64,153 @@ export function AnimationSettings({ config, onChange }: Props) { set('easing', `cubic-bezier(${v.join(',')})`); }; + const easingSelectValue = isCubic ? 'cubic-bezier' : config.easing; + return ( - - - Settings - - - {/* Name */} -
- - { - const val = e.target.value.replace(/\s+/g, '-').replace(/[^a-zA-Z0-9-_]/g, ''); - set('name', val || 'myAnimation'); - }} - className="font-mono text-xs" +
+ + + Settings + + + {/* Name */} +
+ + { + const val = e.target.value.replace(/\s+/g, '-').replace(/[^a-zA-Z0-9-_]/g, ''); + set('name', val || 'myAnimation'); + }} + className={inputCls} + /> +
+ + {/* Duration + Delay */} +
+
+ + set('duration', Math.max(50, Number(e.target.value)))} + className={inputCls} />
+
+ + set('delay', Math.max(0, Number(e.target.value)))} + className={inputCls} + /> +
+
- {/* Duration + Delay */} -
-
- -
- set('duration', Math.max(50, Number(e.target.value)))} - - /> - ms -
-
-
- -
- set('delay', Math.max(0, Number(e.target.value)))} - - /> - ms -
+ {/* Easing */} +
+ + +
+ + {/* Cubic-bezier inputs */} + {isCubic && ( +
+ +
+ {(['P1x', 'P1y', 'P2x', 'P2y'] as const).map((label, i) => ( +
+ + setCubic(i, Number(e.target.value))} + className="w-full bg-transparent border border-border/40 rounded-lg px-2 py-1.5 text-[10px] font-mono outline-none focus:border-primary/50 transition-colors text-foreground/80 text-center" + /> +
+ ))}
+ )} - {/* Easing */} -
- - set('iterationCount', Math.max(1, Number(e.target.value)))} + placeholder="1" + className={cn(inputCls, 'flex-1', isInfinite && 'opacity-30')} + /> +
+
- {/* Cubic-bezier inputs */} - {isCubic && ( -
- -
- {(['P1x', 'P1y', 'P2x', 'P2y'] as const).map((label, i) => ( -
- - setCubic(i, Number(e.target.value))} - className="text-xs px-1.5" - /> -
- ))} -
-
- )} - - {/* Iteration */} -
- -
- set('iterationCount', Math.max(1, Number(e.target.value)))} - className="text-xs flex-1" - placeholder="1" - /> - -
+ {/* Direction */} +
+ +
+ {DIRECTIONS.map(({ value, label }) => ( + + ))}
+
- {/* Direction */} -
- - + {/* Fill Mode */} +
+ +
+ {FILL_MODES.map(({ value, label }) => ( + + ))}
- - {/* Fill Mode */} -
- - -
- - +
+
); } diff --git a/components/animate/ExportPanel.tsx b/components/animate/ExportPanel.tsx index 10e1b12..65aaf7b 100644 --- a/components/animate/ExportPanel.tsx +++ b/components/animate/ExportPanel.tsx @@ -1,11 +1,9 @@ 'use client'; -import { useMemo } from 'react'; -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; -import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; -import { Button } from '@/components/ui/button'; +import { useMemo, useState } from 'react'; import { Copy, Download } from 'lucide-react'; import { toast } from 'sonner'; +import { cn } from '@/lib/utils/cn'; import { buildCSS, buildTailwindCSS } from '@/lib/animate/cssBuilder'; import type { AnimationConfig } from '@/types/animate'; @@ -13,6 +11,11 @@ interface Props { config: AnimationConfig; } +type ExportTab = 'css' | 'tailwind'; + +const actionBtn = + 'flex items-center justify-center gap-1.5 px-3 py-1.5 text-xs glass rounded-md border border-border/30 text-muted-foreground hover:text-primary hover:border-primary/30 hover:bg-primary/10 transition-all'; + function CodeBlock({ code, filename }: { code: string; filename: string }) { const copy = () => { navigator.clipboard.writeText(code); @@ -31,49 +34,50 @@ function CodeBlock({ code, filename }: { code: string; filename: string }) { }; return ( -
-
-
+    
+
+
           {code}
         
-
- - +
+ +
); } export function ExportPanel({ config }: Props) { + const [tab, setTab] = useState('css'); const css = useMemo(() => buildCSS(config), [config]); const tailwind = useMemo(() => buildTailwindCSS(config), [config]); return ( - - - Export - - - - - Plain CSS - Tailwind v4 - - - - - - - - - - +
+
+ Export +
+ {(['css', 'tailwind'] as ExportTab[]).map((t) => ( + + ))} +
+
+ {tab === 'css' && } + {tab === 'tailwind' && } +
); } diff --git a/components/animate/KeyframeProperties.tsx b/components/animate/KeyframeProperties.tsx index 387d364..4c0d25b 100644 --- a/components/animate/KeyframeProperties.tsx +++ b/components/animate/KeyframeProperties.tsx @@ -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 (
-
- - onChange(v)} - /> +
+ + onChange(v)} />
- 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" />
); @@ -56,15 +46,12 @@ function SliderRow({ label, unit, value, min, max, step = 1, onChange }: SliderR export function KeyframeProperties({ keyframe, onChange }: Props) { if (!keyframe) { return ( - - - Properties - - - -

Select a keyframe on the timeline to edit its properties

-
-
+
+ +

+ Select a keyframe on the timeline to edit its properties +

+
); } @@ -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 = (key: K, value: KeyframeProperties[K]) => { @@ -85,96 +69,77 @@ export function KeyframeProperties({ keyframe, onChange }: Props) { const hasBg = props.backgroundColor && props.backgroundColor !== 'none'; return ( - - - +
+
+ Properties - {keyframe.offset}% - - - + + + {keyframe.offset}% + +
- {/* Transform */} -
-

Transform

- setTransform('translateX', v)} /> - setTransform('translateY', v)} /> - setTransform('rotate', v)} /> - setTransform('scaleX', v)} /> - setTransform('scaleY', v)} /> - setTransform('skewX', v)} /> - setTransform('skewY', v)} /> -
+ {/* Transform */} +
+

Transform

+ setTransform('translateX', v)} /> + setTransform('translateY', v)} /> + setTransform('rotate', v)} /> + setTransform('scaleX', v)} /> + setTransform('scaleY', v)} /> + setTransform('skewX', v)} /> + setTransform('skewY', v)} /> +
- {/* Visual */} -
-

Visual

+ {/* Visual */} +
+

Visual

+ setProp('opacity', v)} /> - setProp('opacity', v)} - /> - - {/* Background color */} -
- -
- setProp('backgroundColor', e.target.value)} - disabled={!hasBg} - className={cn('w-9 h-9 p-1 shrink-0 cursor-pointer', !hasBg && 'opacity-30')} - /> - setProp('backgroundColor', e.target.value)} - disabled={!hasBg} - placeholder="none" - className="font-mono text-xs flex-1" - /> - -
+ {/* Background color */} +
+
+ + +
+
+ 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')} + /> + 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" + />
- - setProp('borderRadius', v)} - />
- {/* Filters */} -
-

Filter

- setProp('blur', v)} - /> - setProp('brightness', v)} - /> -
+ setProp('borderRadius', v)} /> +
- - + {/* Filters */} +
+

Filter

+ setProp('blur', v)} /> + setProp('brightness', v)} /> +
+
); } diff --git a/components/animate/KeyframeTimeline.tsx b/components/animate/KeyframeTimeline.tsx index d5a413f..d38840f 100644 --- a/components/animate/KeyframeTimeline.tsx +++ b/components/animate/KeyframeTimeline.tsx @@ -1,8 +1,6 @@ 'use client'; import { useRef } from 'react'; -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; -import { Button } from '@/components/ui/button'; import { Plus, Trash2 } from 'lucide-react'; import { cn } from '@/lib/utils/cn'; import type { Keyframe } from '@/types/animate'; @@ -14,11 +12,18 @@ interface Props { onAdd: (offset: number) => void; onDelete: (id: string) => void; onMove: (id: string, newOffset: number) => void; + embedded?: boolean; // when true, no glass card wrapper (use inside another card) } const TICKS = [0, 25, 50, 75, 100]; -export function KeyframeTimeline({ keyframes, selectedId, onSelect, onAdd, onDelete, onMove }: Props) { +const iconBtn = (disabled?: boolean) => + cn( + 'w-6 h-6 flex items-center justify-center rounded-md glass border border-border/30 text-muted-foreground hover:text-primary hover:border-primary/30 transition-all', + disabled && 'opacity-30 cursor-not-allowed pointer-events-none' + ); + +export function KeyframeTimeline({ keyframes, selectedId, onSelect, onAdd, onDelete, onMove, embedded = false }: Props) { const trackRef = useRef(null); const getOffsetFromEvent = (clientX: number): number => { @@ -29,7 +34,6 @@ export function KeyframeTimeline({ keyframes, selectedId, onSelect, onAdd, onDel }; const handleTrackClick = (e: React.MouseEvent) => { - // Ignore clicks that land directly on a keyframe marker if ((e.target as HTMLElement).closest('[data-keyframe-marker]')) return; onAdd(getOffsetFromEvent(e.clientX)); }; @@ -39,16 +43,11 @@ export function KeyframeTimeline({ keyframes, selectedId, onSelect, onAdd, onDel onSelect(id); const el = e.currentTarget as HTMLElement; el.setPointerCapture(e.pointerId); - - const handleMove = (me: PointerEvent) => { - onMove(id, getOffsetFromEvent(me.clientX)); - }; - + const handleMove = (me: PointerEvent) => onMove(id, getOffsetFromEvent(me.clientX)); const handleUp = () => { el.removeEventListener('pointermove', handleMove); el.removeEventListener('pointerup', handleUp); }; - el.addEventListener('pointermove', handleMove); el.addEventListener('pointerup', handleUp); }; @@ -56,91 +55,91 @@ export function KeyframeTimeline({ keyframes, selectedId, onSelect, onAdd, onDel const sorted = [...keyframes].sort((a, b) => a.offset - b.offset); const selectedKf = keyframes.find((k) => k.id === selectedId); - return ( - - - Keyframes + const content = ( +
+ {/* Header */} +
- - {keyframes.length} keyframe{keyframes.length !== 1 ? 's' : ''} - {selectedKf ? ` · selected: ${selectedKf.offset}%` : ''} + + Keyframes - -
+
+ + + +
- - - {/* Track */} -
- {/* Center line */} -
+
- {/* Tick marks */} - {TICKS.map((tick) => ( -
-
- {tick}% -
- ))} + {/* Track */} +
+
+ {TICKS.map((tick) => ( +
+
+ {tick}% +
+ ))} + {sorted.map((kf) => ( +
- {/* Keyframe markers */} - {sorted.map((kf) => ( -
+ {/* Offset labels */} +
+ {sorted.map((kf) => ( + + {kf.offset}% + + ))} +
+
+ ); - {/* Offset labels below */} -
- {sorted.map((kf) => ( - - {kf.offset}% - - ))} -
- - + if (embedded) return
{content}
; + + return ( +
+ {content} +
); } diff --git a/components/animate/PresetLibrary.tsx b/components/animate/PresetLibrary.tsx index bcba38d..07abccd 100644 --- a/components/animate/PresetLibrary.tsx +++ b/components/animate/PresetLibrary.tsx @@ -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(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 ( @@ -63,35 +52,32 @@ function PresetCard({ preset, onSelect }: { } export function PresetLibrary({ onSelect }: Props) { + const [category, setCategory] = useState(PRESET_CATEGORIES[0]); + return ( - - - Presets - - - - - {PRESET_CATEGORIES.map((cat) => ( - - {cat} - - ))} - +
+
+ Presets +
{PRESET_CATEGORIES.map((cat) => ( - -
- {PRESETS.filter((p) => p.category === cat).map((preset) => ( - onSelect(preset.config)} - /> - ))} -
-
+ ))} - - - +
+
+
+ {PRESETS.filter((p) => p.category === category).map((preset) => ( + onSelect(preset.config)} /> + ))} +
+
); }