Spaces:
Paused
Paused
| 'use client' | |
| import { useState, useCallback, useMemo } from 'react' | |
| import { Sparkles, Layers, Box, Type, Zap, ChevronRight, Check, RefreshCw } from 'lucide-react' | |
| interface DesignPanelProps { | |
| onCodeUpdate?: (code: { html?: string; css?: string; js?: string }) => void | |
| } | |
| type Category = 'animations' | 'threejs' | 'ui' | 'text' | |
| interface Param { | |
| label: string | |
| key: string | |
| type: 'range' | 'color' | 'select' | |
| min?: number | |
| max?: number | |
| step?: number | |
| default: number | string | |
| options?: string[] | |
| unit?: string | |
| } | |
| interface Preset { | |
| id: string | |
| name: string | |
| description: string | |
| category: Category | |
| emoji: string | |
| params: Param[] | |
| generate: (params: Record<string, any>) => { html?: string; css?: string; js?: string } | |
| } | |
| const PRESETS: Preset[] = [ | |
| // ββ ANIMATIONS ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| { | |
| id: 'fade-in', | |
| name: 'Fade In', | |
| description: 'Smooth opacity reveal on load', | |
| category: 'animations', | |
| emoji: 'β¨', | |
| params: [ | |
| { label: 'Duration (s)', key: 'duration', type: 'range', min: 0.1, max: 3, step: 0.1, default: 1, unit: 's' }, | |
| { label: 'Delay (s)', key: 'delay', type: 'range', min: 0, max: 2, step: 0.1, default: 0.2, unit: 's' }, | |
| ], | |
| generate: (p) => ({ | |
| css: `.fade-in { | |
| animation: fadeIn ${p.duration}s ease forwards; | |
| animation-delay: ${p.delay}s; | |
| opacity: 0; | |
| } | |
| @keyframes fadeIn { | |
| from { opacity: 0; transform: translateY(20px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| }`, | |
| html: `<div class="fade-in"> | |
| <h2>Fade In Element</h2> | |
| <p>This element fades in smoothly on load.</p> | |
| </div>`, | |
| }), | |
| }, | |
| { | |
| id: 'pulse-glow', | |
| name: 'Pulse Glow', | |
| description: 'Rhythmic glowing border pulse', | |
| category: 'animations', | |
| emoji: 'π«', | |
| params: [ | |
| { label: 'Color', key: 'color', type: 'color', default: '#667eea' }, | |
| { label: 'Speed (s)', key: 'speed', type: 'range', min: 0.5, max: 4, step: 0.1, default: 1.5, unit: 's' }, | |
| { label: 'Intensity (px)', key: 'blur', type: 'range', min: 5, max: 40, step: 1, default: 20, unit: 'px' }, | |
| ], | |
| generate: (p) => ({ | |
| css: `.pulse-glow { | |
| animation: pulseGlow ${p.speed}s ease-in-out infinite; | |
| border: 2px solid ${p.color}; | |
| border-radius: 12px; | |
| padding: 24px; | |
| display: inline-block; | |
| } | |
| @keyframes pulseGlow { | |
| 0%, 100% { box-shadow: 0 0 ${p.blur}px ${p.color}; } | |
| 50% { box-shadow: 0 0 ${Number(p.blur) * 2}px ${p.color}, 0 0 ${Number(p.blur) * 3}px ${p.color}44; } | |
| }`, | |
| html: `<div class="pulse-glow"> | |
| <h3 style="color: ${p.color}; margin: 0">Pulse Glow</h3> | |
| <p style="margin: 8px 0 0; opacity: 0.7">Glowing border element</p> | |
| </div>`, | |
| }), | |
| }, | |
| { | |
| id: 'slide-in', | |
| name: 'Slide In', | |
| description: 'Direction-based slide entrance', | |
| category: 'animations', | |
| emoji: 'β‘οΈ', | |
| params: [ | |
| { label: 'Direction', key: 'dir', type: 'select', default: 'left', options: ['left', 'right', 'top', 'bottom'] }, | |
| { label: 'Duration (s)', key: 'duration', type: 'range', min: 0.2, max: 2, step: 0.1, default: 0.6, unit: 's' }, | |
| { label: 'Distance (px)', key: 'dist', type: 'range', min: 20, max: 200, step: 10, default: 60, unit: 'px' }, | |
| ], | |
| generate: (p) => { | |
| const transforms: Record<string, string> = { | |
| left: `translateX(-${p.dist}px)`, | |
| right: `translateX(${p.dist}px)`, | |
| top: `translateY(-${p.dist}px)`, | |
| bottom: `translateY(${p.dist}px)`, | |
| } | |
| return { | |
| css: `.slide-in { | |
| animation: slideIn ${p.duration}s cubic-bezier(.22,.68,0,1.2) forwards; | |
| opacity: 0; | |
| } | |
| @keyframes slideIn { | |
| from { opacity: 0; transform: ${transforms[p.dir as string]}; } | |
| to { opacity: 1; transform: translate(0); } | |
| }`, | |
| html: `<div class="slide-in"> | |
| <h2>Slide In from ${p.dir}</h2> | |
| <p>Slides in with a smooth easing curve.</p> | |
| </div>`, | |
| } | |
| }, | |
| }, | |
| { | |
| id: 'stagger-list', | |
| name: 'Stagger List', | |
| description: 'Items reveal one after another', | |
| category: 'animations', | |
| emoji: 'π―', | |
| params: [ | |
| { label: 'Stagger (ms)', key: 'stagger', type: 'range', min: 50, max: 400, step: 10, default: 120, unit: 'ms' }, | |
| { label: 'Color', key: 'color', type: 'color', default: '#8b9cff' }, | |
| ], | |
| generate: (p) => ({ | |
| css: `.stagger-item { | |
| opacity: 0; | |
| transform: translateX(-20px); | |
| animation: staggerReveal 0.5s ease forwards; | |
| } | |
| .stagger-item:nth-child(1) { animation-delay: ${Number(p.stagger) * 0}ms; } | |
| .stagger-item:nth-child(2) { animation-delay: ${Number(p.stagger) * 1}ms; } | |
| .stagger-item:nth-child(3) { animation-delay: ${Number(p.stagger) * 2}ms; } | |
| .stagger-item:nth-child(4) { animation-delay: ${Number(p.stagger) * 3}ms; } | |
| @keyframes staggerReveal { | |
| to { opacity: 1; transform: translateX(0); } | |
| } | |
| .stagger-list { list-style: none; padding: 0; } | |
| .stagger-list li { padding: 8px 0; border-left: 3px solid ${p.color}; padding-left: 12px; margin-bottom: 8px; }`, | |
| html: `<ul class="stagger-list"> | |
| <li class="stagger-item">First item</li> | |
| <li class="stagger-item">Second item</li> | |
| <li class="stagger-item">Third item</li> | |
| <li class="stagger-item">Fourth item</li> | |
| </ul>`, | |
| }), | |
| }, | |
| // ββ THREE.JS ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| { | |
| id: 'particle-field', | |
| name: 'Particle Field', | |
| description: 'Floating 3D particle system', | |
| category: 'threejs', | |
| emoji: 'π', | |
| params: [ | |
| { label: 'Count', key: 'count', type: 'range', min: 100, max: 2000, step: 50, default: 500 }, | |
| { label: 'Color', key: 'color', type: 'color', default: '#667eea' }, | |
| { label: 'Speed', key: 'speed', type: 'range', min: 0.1, max: 3, step: 0.1, default: 0.5 }, | |
| { label: 'Size', key: 'size', type: 'range', min: 0.01, max: 0.2, step: 0.01, default: 0.05 }, | |
| ], | |
| generate: (p) => ({ | |
| html: `<canvas id="particle-canvas" style="width:100%;height:400px;display:block;background:#0a0a0a;border-radius:12px;"></canvas>`, | |
| js: `(function() { | |
| const script = document.createElement('script'); | |
| script.src = 'https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js'; | |
| script.onload = function() { | |
| const canvas = document.getElementById('particle-canvas'); | |
| const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true }); | |
| renderer.setSize(canvas.clientWidth, canvas.clientHeight); | |
| const scene = new THREE.Scene(); | |
| const camera = new THREE.PerspectiveCamera(75, canvas.clientWidth / canvas.clientHeight, 0.1, 1000); | |
| camera.position.z = 3; | |
| const geometry = new THREE.BufferGeometry(); | |
| const positions = new Float32Array(${p.count} * 3); | |
| for (let i = 0; i < ${p.count} * 3; i++) positions[i] = (Math.random() - 0.5) * 10; | |
| geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); | |
| const material = new THREE.PointsMaterial({ color: '${p.color}', size: ${p.size} }); | |
| const particles = new THREE.Points(geometry, material); | |
| scene.add(particles); | |
| function animate() { | |
| requestAnimationFrame(animate); | |
| particles.rotation.y += ${Number(p.speed) * 0.002}; | |
| particles.rotation.x += ${Number(p.speed) * 0.001}; | |
| renderer.render(scene, camera); | |
| } | |
| animate(); | |
| }; | |
| document.head.appendChild(script); | |
| })();`, | |
| }), | |
| }, | |
| { | |
| id: 'rotating-cube', | |
| name: 'Rotating Cube', | |
| description: 'Smooth 3D rotating cube', | |
| category: 'threejs', | |
| emoji: 'π²', | |
| params: [ | |
| { label: 'Color', key: 'color', type: 'color', default: '#764ba2' }, | |
| { label: 'Speed', key: 'speed', type: 'range', min: 0.1, max: 5, step: 0.1, default: 1 }, | |
| { label: 'Wireframe', key: 'wire', type: 'select', default: 'false', options: ['false', 'true'] }, | |
| { label: 'Size', key: 'size', type: 'range', min: 0.5, max: 3, step: 0.1, default: 1.5 }, | |
| ], | |
| generate: (p) => ({ | |
| html: `<canvas id="cube-canvas" style="width:100%;height:360px;display:block;background:#0a0a0a;border-radius:12px;"></canvas>`, | |
| js: `(function() { | |
| const script = document.createElement('script'); | |
| script.src = 'https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js'; | |
| script.onload = function() { | |
| const canvas = document.getElementById('cube-canvas'); | |
| const renderer = new THREE.WebGLRenderer({ canvas, antialias: true }); | |
| renderer.setSize(canvas.clientWidth, canvas.clientHeight); | |
| renderer.setClearColor(0x0a0a0a); | |
| const scene = new THREE.Scene(); | |
| const camera = new THREE.PerspectiveCamera(60, canvas.clientWidth / canvas.clientHeight, 0.1, 100); | |
| camera.position.z = 4; | |
| const geometry = new THREE.BoxGeometry(${p.size}, ${p.size}, ${p.size}); | |
| const material = new THREE.MeshPhongMaterial({ color: '${p.color}', wireframe: ${p.wire} }); | |
| const cube = new THREE.Mesh(geometry, material); | |
| scene.add(cube); | |
| scene.add(new THREE.AmbientLight(0xffffff, 0.4)); | |
| const light = new THREE.DirectionalLight(0xffffff, 1); | |
| light.position.set(5, 5, 5); | |
| scene.add(light); | |
| function animate() { | |
| requestAnimationFrame(animate); | |
| cube.rotation.x += ${Number(p.speed) * 0.01}; | |
| cube.rotation.y += ${Number(p.speed) * 0.015}; | |
| renderer.render(scene, camera); | |
| } | |
| animate(); | |
| }; | |
| document.head.appendChild(script); | |
| })();`, | |
| }), | |
| }, | |
| { | |
| id: 'wave-plane', | |
| name: 'Wave Plane', | |
| description: 'Animated wave mesh surface', | |
| category: 'threejs', | |
| emoji: 'π', | |
| params: [ | |
| { label: 'Color', key: 'color', type: 'color', default: '#00d4ff' }, | |
| { label: 'Amplitude', key: 'amp', type: 'range', min: 0.1, max: 2, step: 0.05, default: 0.5 }, | |
| { label: 'Frequency', key: 'freq', type: 'range', min: 0.5, max: 5, step: 0.1, default: 2 }, | |
| { label: 'Speed', key: 'speed', type: 'range', min: 0.5, max: 5, step: 0.1, default: 2 }, | |
| ], | |
| generate: (p) => ({ | |
| html: `<canvas id="wave-canvas" style="width:100%;height:360px;display:block;background:#0a0a0a;border-radius:12px;"></canvas>`, | |
| js: `(function() { | |
| const script = document.createElement('script'); | |
| script.src = 'https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js'; | |
| script.onload = function() { | |
| const canvas = document.getElementById('wave-canvas'); | |
| const renderer = new THREE.WebGLRenderer({ canvas, antialias: true }); | |
| renderer.setSize(canvas.clientWidth, canvas.clientHeight); | |
| renderer.setClearColor(0x0a0a0a); | |
| const scene = new THREE.Scene(); | |
| const camera = new THREE.PerspectiveCamera(60, canvas.clientWidth / canvas.clientHeight, 0.1, 100); | |
| camera.position.set(0, 3, 5); | |
| camera.lookAt(0, 0, 0); | |
| const geo = new THREE.PlaneGeometry(10, 10, 40, 40); | |
| const mat = new THREE.MeshBasicMaterial({ color: '${p.color}', wireframe: true }); | |
| const plane = new THREE.Mesh(geo, mat); | |
| plane.rotation.x = -Math.PI / 4; | |
| scene.add(plane); | |
| let t = 0; | |
| function animate() { | |
| requestAnimationFrame(animate); | |
| t += 0.01 * ${p.speed}; | |
| const pos = geo.attributes.position; | |
| for (let i = 0; i < pos.count; i++) { | |
| const x = pos.getX(i), y = pos.getY(i); | |
| pos.setZ(i, Math.sin(x * ${p.freq} + t) * ${p.amp} + Math.cos(y * ${p.freq} + t) * ${p.amp} * 0.5); | |
| } | |
| pos.needsUpdate = true; | |
| renderer.render(scene, camera); | |
| } | |
| animate(); | |
| }; | |
| document.head.appendChild(script); | |
| })();`, | |
| }), | |
| }, | |
| // ββ UI COMPONENTS ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| { | |
| id: 'glass-card', | |
| name: 'Glass Card', | |
| description: 'Frosted glass morphism card', | |
| category: 'ui', | |
| emoji: 'πͺ', | |
| params: [ | |
| { label: 'Blur (px)', key: 'blur', type: 'range', min: 4, max: 40, step: 1, default: 16, unit: 'px' }, | |
| { label: 'Opacity', key: 'opacity', type: 'range', min: 0.05, max: 0.4, step: 0.01, default: 0.1 }, | |
| { label: 'Border Color', key: 'border', type: 'color', default: '#ffffff' }, | |
| { label: 'Radius (px)', key: 'radius', type: 'range', min: 4, max: 40, step: 2, default: 20, unit: 'px' }, | |
| ], | |
| generate: (p) => ({ | |
| css: `.glass-card { | |
| background: rgba(255,255,255,${p.opacity}); | |
| backdrop-filter: blur(${p.blur}px); | |
| -webkit-backdrop-filter: blur(${p.blur}px); | |
| border: 1px solid rgba(255,255,255,0.2); | |
| border-radius: ${p.radius}px; | |
| padding: 32px; | |
| box-shadow: 0 8px 32px rgba(0,0,0,0.3); | |
| color: #fff; | |
| max-width: 360px; | |
| }`, | |
| html: `<div style="background: linear-gradient(135deg,#667eea,#764ba2); padding: 40px; min-height: 200px; display:flex; align-items:center; justify-content:center;"> | |
| <div class="glass-card"> | |
| <h3 style="margin:0 0 8px">Glass Card</h3> | |
| <p style="margin:0;opacity:0.8">Frosted glass morphism with backdrop blur effect.</p> | |
| </div> | |
| </div>`, | |
| }), | |
| }, | |
| { | |
| id: 'neon-button', | |
| name: 'Neon Button', | |
| description: 'Glowing neon CTA button', | |
| category: 'ui', | |
| emoji: 'β‘', | |
| params: [ | |
| { label: 'Color', key: 'color', type: 'color', default: '#00ff88' }, | |
| { label: 'Glow Size (px)', key: 'glow', type: 'range', min: 5, max: 40, step: 1, default: 15, unit: 'px' }, | |
| { label: 'Border (px)', key: 'border', type: 'range', min: 1, max: 4, step: 1, default: 2, unit: 'px' }, | |
| ], | |
| generate: (p) => ({ | |
| css: `.neon-btn { | |
| background: transparent; | |
| color: ${p.color}; | |
| border: ${p.border}px solid ${p.color}; | |
| padding: 14px 32px; | |
| font-size: 1rem; | |
| font-weight: 600; | |
| letter-spacing: 2px; | |
| text-transform: uppercase; | |
| cursor: pointer; | |
| border-radius: 4px; | |
| transition: all 0.3s ease; | |
| box-shadow: 0 0 ${p.glow}px ${p.color}88, inset 0 0 ${p.glow}px ${p.color}22; | |
| text-shadow: 0 0 8px ${p.color}; | |
| } | |
| .neon-btn:hover { | |
| background: ${p.color}22; | |
| box-shadow: 0 0 ${Number(p.glow) * 2}px ${p.color}, inset 0 0 ${p.glow}px ${p.color}44; | |
| }`, | |
| html: `<div style="background:#0a0a0a;padding:40px;display:flex;justify-content:center;"> | |
| <button class="neon-btn">Click Me</button> | |
| </div>`, | |
| }), | |
| }, | |
| { | |
| id: 'gradient-card', | |
| name: 'Gradient Card', | |
| description: 'Animated gradient background card', | |
| category: 'ui', | |
| emoji: 'π¨', | |
| params: [ | |
| { label: 'Color 1', key: 'c1', type: 'color', default: '#667eea' }, | |
| { label: 'Color 2', key: 'c2', type: 'color', default: '#764ba2' }, | |
| { label: 'Color 3', key: 'c3', type: 'color', default: '#f64f59' }, | |
| { label: 'Speed (s)', key: 'speed', type: 'range', min: 2, max: 12, step: 0.5, default: 6, unit: 's' }, | |
| ], | |
| generate: (p) => ({ | |
| css: `.gradient-card { | |
| background: linear-gradient(270deg, ${p.c1}, ${p.c2}, ${p.c3}); | |
| background-size: 400% 400%; | |
| animation: gradientShift ${p.speed}s ease infinite; | |
| border-radius: 20px; | |
| padding: 40px; | |
| color: white; | |
| max-width: 400px; | |
| } | |
| @keyframes gradientShift { | |
| 0% { background-position: 0% 50%; } | |
| 50% { background-position: 100% 50%; } | |
| 100% { background-position: 0% 50%; } | |
| }`, | |
| html: `<div class="gradient-card"> | |
| <h2 style="margin:0 0 12px">Gradient Card</h2> | |
| <p style="margin:0;opacity:0.9">Animated flowing gradient background that shifts between colors.</p> | |
| </div>`, | |
| }), | |
| }, | |
| { | |
| id: 'progress-bar', | |
| name: 'Animated Progress', | |
| description: 'Smooth animated progress bar', | |
| category: 'ui', | |
| emoji: 'π', | |
| params: [ | |
| { label: 'Color', key: 'color', type: 'color', default: '#667eea' }, | |
| { label: 'Progress (%)', key: 'pct', type: 'range', min: 5, max: 100, step: 1, default: 75 }, | |
| { label: 'Height (px)', key: 'h', type: 'range', min: 4, max: 24, step: 2, default: 10, unit: 'px' }, | |
| { label: 'Duration (s)', key: 'dur', type: 'range', min: 0.5, max: 3, step: 0.1, default: 1.2, unit: 's' }, | |
| ], | |
| generate: (p) => ({ | |
| css: `.progress-track { | |
| background: rgba(255,255,255,0.1); | |
| border-radius: 999px; | |
| height: ${p.h}px; | |
| overflow: hidden; | |
| width: 100%; | |
| max-width: 400px; | |
| } | |
| .progress-fill { | |
| height: 100%; | |
| width: 0; | |
| background: linear-gradient(90deg, ${p.color}88, ${p.color}); | |
| border-radius: 999px; | |
| animation: fillProgress ${p.dur}s cubic-bezier(.4,0,.2,1) forwards; | |
| box-shadow: 0 0 12px ${p.color}88; | |
| } | |
| @keyframes fillProgress { | |
| to { width: ${p.pct}%; } | |
| }`, | |
| html: `<div style="padding:32px"> | |
| <p style="margin:0 0 8px;font-size:.85rem;opacity:.6">Loading... ${p.pct}%</p> | |
| <div class="progress-track"> | |
| <div class="progress-fill"></div> | |
| </div> | |
| </div>`, | |
| }), | |
| }, | |
| // ββ TEXT EFFECTS βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| { | |
| id: 'typewriter', | |
| name: 'Typewriter', | |
| description: 'Character-by-character typing effect', | |
| category: 'text', | |
| emoji: 'β¨οΈ', | |
| params: [ | |
| { label: 'Speed (ms)', key: 'speed', type: 'range', min: 20, max: 300, step: 10, default: 80, unit: 'ms' }, | |
| { label: 'Color', key: 'color', type: 'color', default: '#8b9cff' }, | |
| { label: 'Cursor Color', key: 'cursor', type: 'color', default: '#667eea' }, | |
| ], | |
| generate: (p) => ({ | |
| css: `.typewriter-text { | |
| color: ${p.color}; | |
| font-size: 1.5rem; | |
| font-weight: 600; | |
| border-right: 3px solid ${p.cursor}; | |
| white-space: nowrap; | |
| overflow: hidden; | |
| display: inline-block; | |
| animation: blink 0.75s step-end infinite; | |
| } | |
| @keyframes blink { 0%,100% { border-color: ${p.cursor}; } 50% { border-color: transparent; } }`, | |
| html: `<div style="padding:32px"> | |
| <div id="tw-el" class="typewriter-text"></div> | |
| </div>`, | |
| js: `(function(){ | |
| const el = document.getElementById('tw-el'); | |
| const text = 'Hello, World! π'; | |
| let i = 0; | |
| function type() { | |
| if (i < text.length) { | |
| el.textContent += text[i++]; | |
| setTimeout(type, ${p.speed}); | |
| } | |
| } | |
| type(); | |
| })();`, | |
| }), | |
| }, | |
| { | |
| id: 'glitch-text', | |
| name: 'Glitch Text', | |
| description: 'Cyberpunk-style glitch distortion', | |
| category: 'text', | |
| emoji: 'πΎ', | |
| params: [ | |
| { label: 'Color', key: 'color', type: 'color', default: '#00ff88' }, | |
| { label: 'Speed (s)', key: 'speed', type: 'range', min: 0.5, max: 5, step: 0.1, default: 1.5, unit: 's' }, | |
| { label: 'Intensity (px)', key: 'dist', type: 'range', min: 2, max: 20, step: 1, default: 6, unit: 'px' }, | |
| ], | |
| generate: (p) => ({ | |
| css: `.glitch { | |
| font-size: 3rem; | |
| font-weight: 900; | |
| color: ${p.color}; | |
| position: relative; | |
| display: inline-block; | |
| text-transform: uppercase; | |
| letter-spacing: 4px; | |
| } | |
| .glitch::before, .glitch::after { | |
| content: attr(data-text); | |
| position: absolute; | |
| top: 0; left: 0; | |
| width: 100%; height: 100%; | |
| } | |
| .glitch::before { | |
| color: #ff0044; | |
| animation: glitchTop ${p.speed}s infinite; | |
| clip-path: polygon(0 0, 100% 0, 100% 35%, 0 35%); | |
| } | |
| .glitch::after { | |
| color: #00eeff; | |
| animation: glitchBot ${p.speed}s infinite; | |
| clip-path: polygon(0 65%, 100% 65%, 100% 100%, 0 100%); | |
| } | |
| @keyframes glitchTop { | |
| 0%,90%,100% { transform: translate(0); } | |
| 92% { transform: translate(-${p.dist}px, 1px); } | |
| 94% { transform: translate(${p.dist}px, -1px); } | |
| } | |
| @keyframes glitchBot { | |
| 0%,90%,100% { transform: translate(0); } | |
| 92% { transform: translate(${p.dist}px, 1px); } | |
| 94% { transform: translate(-${p.dist}px, -1px); } | |
| }`, | |
| html: `<div style="background:#0a0a0a;padding:48px;display:flex;justify-content:center;"> | |
| <span class="glitch" data-text="GLITCH">GLITCH</span> | |
| </div>`, | |
| }), | |
| }, | |
| { | |
| id: 'gradient-text', | |
| name: 'Gradient Text', | |
| description: 'Animated gradient flowing through text', | |
| category: 'text', | |
| emoji: 'π', | |
| params: [ | |
| { label: 'Color 1', key: 'c1', type: 'color', default: '#667eea' }, | |
| { label: 'Color 2', key: 'c2', type: 'color', default: '#f64f59' }, | |
| { label: 'Color 3', key: 'c3', type: 'color', default: '#ffd700' }, | |
| { label: 'Speed (s)', key: 'speed', type: 'range', min: 1, max: 8, step: 0.5, default: 3, unit: 's' }, | |
| ], | |
| generate: (p) => ({ | |
| css: `.gradient-text { | |
| font-size: 3rem; | |
| font-weight: 900; | |
| background: linear-gradient(90deg, ${p.c1}, ${p.c2}, ${p.c3}, ${p.c1}); | |
| background-size: 300% 100%; | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| animation: textShift ${p.speed}s linear infinite; | |
| display: inline-block; | |
| } | |
| @keyframes textShift { | |
| 0% { background-position: 0% center; } | |
| 100% { background-position: 300% center; } | |
| }`, | |
| html: `<div style="background:#0a0a0a;padding:48px;text-align:center;"> | |
| <span class="gradient-text">Beautiful Text</span> | |
| </div>`, | |
| }), | |
| }, | |
| { | |
| id: 'split-reveal', | |
| name: 'Split Reveal', | |
| description: 'Words reveal with split-clip animation', | |
| category: 'text', | |
| emoji: 'βοΈ', | |
| params: [ | |
| { label: 'Duration (s)', key: 'dur', type: 'range', min: 0.3, max: 1.5, step: 0.05, default: 0.7, unit: 's' }, | |
| { label: 'Stagger (ms)', key: 'stagger', type: 'range', min: 50, max: 400, step: 25, default: 150, unit: 'ms' }, | |
| { label: 'Color', key: 'color', type: 'color', default: '#ffffff' }, | |
| ], | |
| generate: (p) => ({ | |
| css: `.split-word { | |
| display: inline-block; | |
| overflow: hidden; | |
| vertical-align: top; | |
| margin-right: 0.25em; | |
| } | |
| .split-inner { | |
| display: inline-block; | |
| transform: translateY(110%); | |
| animation: splitReveal ${p.dur}s cubic-bezier(.16,1,.3,1) forwards; | |
| color: ${p.color}; | |
| font-size: 2.5rem; | |
| font-weight: 800; | |
| } | |
| .split-word:nth-child(1) .split-inner { animation-delay: 0ms; } | |
| .split-word:nth-child(2) .split-inner { animation-delay: ${p.stagger}ms; } | |
| .split-word:nth-child(3) .split-inner { animation-delay: ${Number(p.stagger) * 2}ms; } | |
| .split-word:nth-child(4) .split-inner { animation-delay: ${Number(p.stagger) * 3}ms; } | |
| @keyframes splitReveal { to { transform: translateY(0); } }`, | |
| html: `<div style="padding:40px"> | |
| <div> | |
| <span class="split-word"><span class="split-inner">Hello</span></span> | |
| <span class="split-word"><span class="split-inner">Beautiful</span></span> | |
| <span class="split-word"><span class="split-inner">World</span></span> | |
| <span class="split-word"><span class="split-inner">β¨</span></span> | |
| </div> | |
| </div>`, | |
| }), | |
| }, | |
| ] | |
| const CATEGORIES: { id: Category; label: string; icon: React.ReactNode }[] = [ | |
| { id: 'animations', label: 'Animations', icon: <Zap className="w-4 h-4" /> }, | |
| { id: 'threejs', label: '3D / Three.js', icon: <Box className="w-4 h-4" /> }, | |
| { id: 'ui', label: 'UI Components', icon: <Layers className="w-4 h-4" /> }, | |
| { id: 'text', label: 'Text Effects', icon: <Type className="w-4 h-4" /> }, | |
| ] | |
| // Build a full srcdoc HTML string from generated code | |
| function buildPreviewDoc(code: { html?: string; css?: string; js?: string }): string { | |
| return `<!DOCTYPE html> | |
| <html> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |
| <style> | |
| * { box-sizing: border-box; } | |
| body { margin: 0; background: #0a0a0a; color: #fff; font-family: system-ui, sans-serif; } | |
| ${code.css || ''} | |
| </style> | |
| </head> | |
| <body> | |
| ${code.html || ''} | |
| <script>${code.js || ''}<\/script> | |
| </body> | |
| </html>` | |
| } | |
| export function DesignPanel({ onCodeUpdate }: DesignPanelProps) { | |
| const [category, setCategory] = useState<Category>('animations') | |
| const [selectedId, setSelectedId] = useState<string | null>(null) | |
| const [paramValues, setParamValues] = useState<Record<string, Record<string, any>>>({}) | |
| const [applied, setApplied] = useState<string | null>(null) | |
| const [previewKey, setPreviewKey] = useState(0) | |
| const visiblePresets = PRESETS.filter(p => p.category === category) | |
| const getParam = useCallback((presetId: string, key: string, defaultVal: any) => { | |
| return paramValues[presetId]?.[key] ?? defaultVal | |
| }, [paramValues]) | |
| const setParam = useCallback((presetId: string, key: string, value: any) => { | |
| setParamValues(prev => ({ | |
| ...prev, | |
| [presetId]: { ...(prev[presetId] || {}), [key]: value } | |
| })) | |
| }, []) | |
| const selectedPreset = PRESETS.find(p => p.id === selectedId) | |
| // Resolve current params for the selected preset | |
| const resolvedParams = useMemo(() => { | |
| if (!selectedPreset) return {} | |
| const out: Record<string, any> = {} | |
| selectedPreset.params.forEach(p => { | |
| out[p.key] = getParam(selectedPreset.id, p.key, p.default) | |
| }) | |
| return out | |
| }, [selectedPreset, paramValues, getParam]) | |
| // Generate live preview srcdoc β updates any time params change | |
| const previewDoc = useMemo(() => { | |
| if (!selectedPreset) return '' | |
| const code = selectedPreset.generate(resolvedParams) | |
| return buildPreviewDoc(code) | |
| }, [selectedPreset, resolvedParams]) | |
| const handleApply = useCallback((preset: Preset) => { | |
| const code = preset.generate(resolvedParams) | |
| if (onCodeUpdate) onCodeUpdate(code) | |
| setApplied(preset.id) | |
| setTimeout(() => setApplied(null), 1800) | |
| }, [resolvedParams, onCodeUpdate]) | |
| return ( | |
| <div className="h-full flex flex-col overflow-hidden" style={{ background: 'rgba(8,8,12,0.95)' }}> | |
| {/* Header */} | |
| <div className="px-4 py-3 flex items-center gap-2 flex-shrink-0" style={{ borderBottom: '1px solid rgba(255,255,255,0.08)' }}> | |
| <Sparkles className="w-4 h-4" style={{ color: '#8b9cff' }} /> | |
| <span className="text-sm font-semibold" style={{ color: '#8b9cff' }}>Design Library</span> | |
| <span className="ml-auto text-xs" style={{ color: 'rgba(255,255,255,0.3)' }}>ReactBits Β· Three.js</span> | |
| </div> | |
| {/* Category Pills */} | |
| <div className="flex gap-2 px-4 py-3 flex-shrink-0 overflow-x-auto" style={{ borderBottom: '1px solid rgba(255,255,255,0.06)' }}> | |
| {CATEGORIES.map(cat => ( | |
| <button | |
| key={cat.id} | |
| onClick={() => { setCategory(cat.id); setSelectedId(null) }} | |
| className="flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-medium whitespace-nowrap transition-all" | |
| style={category === cat.id ? { | |
| background: 'rgba(102,126,234,0.25)', | |
| color: '#8b9cff', | |
| border: '1px solid rgba(102,126,234,0.4)', | |
| } : { | |
| background: 'rgba(255,255,255,0.04)', | |
| color: 'rgba(255,255,255,0.5)', | |
| border: '1px solid rgba(255,255,255,0.08)', | |
| }} | |
| > | |
| {cat.icon} | |
| {cat.label} | |
| </button> | |
| ))} | |
| </div> | |
| <div className="flex-1 overflow-hidden flex"> | |
| {/* Preset List */} | |
| <div className="overflow-y-auto flex-shrink-0" style={{ | |
| width: selectedId ? '38%' : '100%', | |
| borderRight: selectedId ? '1px solid rgba(255,255,255,0.08)' : 'none', | |
| transition: 'width 0.25s ease', | |
| }}> | |
| <div className="p-3 grid gap-2" style={{ gridTemplateColumns: selectedId ? '1fr' : 'repeat(2, 1fr)' }}> | |
| {visiblePresets.map(preset => ( | |
| <button | |
| key={preset.id} | |
| onClick={() => setSelectedId(selectedId === preset.id ? null : preset.id)} | |
| className="text-left rounded-xl p-3 transition-all" | |
| style={selectedId === preset.id ? { | |
| background: 'rgba(102,126,234,0.15)', | |
| border: '1px solid rgba(102,126,234,0.4)', | |
| } : { | |
| background: 'rgba(255,255,255,0.03)', | |
| border: '1px solid rgba(255,255,255,0.07)', | |
| }} | |
| > | |
| <div className="text-xl mb-1">{preset.emoji}</div> | |
| <div className="text-xs font-semibold mb-0.5" style={{ color: selectedId === preset.id ? '#8b9cff' : '#fff' }}> | |
| {preset.name} | |
| </div> | |
| {!selectedId && ( | |
| <div className="text-xs" style={{ color: 'rgba(255,255,255,0.4)' }}> | |
| {preset.description} | |
| </div> | |
| )} | |
| {selectedId === preset.id && ( | |
| <ChevronRight className="w-3 h-3 mt-1" style={{ color: '#8b9cff' }} /> | |
| )} | |
| </button> | |
| ))} | |
| </div> | |
| </div> | |
| {/* Right Panel: Live Preview + Params */} | |
| {selectedPreset && ( | |
| <div className="flex-1 overflow-hidden flex flex-col"> | |
| {/* ββ LIVE PREVIEW ββ */} | |
| <div className="flex-shrink-0" style={{ | |
| borderBottom: '1px solid rgba(255,255,255,0.08)', | |
| background: 'rgba(0,0,0,0.4)', | |
| }}> | |
| {/* Preview header */} | |
| <div className="flex items-center justify-between px-3 py-2"> | |
| <span className="text-xs font-medium" style={{ color: 'rgba(255,255,255,0.4)' }}> | |
| Live Preview | |
| </span> | |
| <button | |
| onClick={() => setPreviewKey(k => k + 1)} | |
| className="flex items-center gap-1 text-xs px-2 py-1 rounded transition-all hover:bg-white/10" | |
| style={{ color: 'rgba(255,255,255,0.4)' }} | |
| title="Replay animation" | |
| > | |
| <RefreshCw className="w-3 h-3" /> | |
| Replay | |
| </button> | |
| </div> | |
| {/* iframe */} | |
| <iframe | |
| key={previewKey} | |
| srcDoc={previewDoc} | |
| sandbox="allow-scripts" | |
| style={{ | |
| width: '100%', | |
| height: '200px', | |
| border: 'none', | |
| display: 'block', | |
| background: '#0a0a0a', | |
| }} | |
| title="Live Preview" | |
| /> | |
| </div> | |
| {/* ββ PARAMS + APPLY ββ */} | |
| <div className="flex-1 overflow-y-auto p-4 flex flex-col gap-4"> | |
| <div> | |
| <div className="text-sm font-bold mb-0.5" style={{ color: '#fff' }}> | |
| {selectedPreset.emoji} {selectedPreset.name} | |
| </div> | |
| <div className="text-xs" style={{ color: 'rgba(255,255,255,0.4)' }}> | |
| {selectedPreset.description} | |
| </div> | |
| </div> | |
| {/* Params */} | |
| <div className="flex flex-col gap-4"> | |
| {selectedPreset.params.map(param => { | |
| const val = getParam(selectedPreset.id, param.key, param.default) | |
| return ( | |
| <div key={param.key}> | |
| <div className="flex items-center justify-between mb-1.5"> | |
| <label className="text-xs font-medium" style={{ color: 'rgba(255,255,255,0.6)' }}> | |
| {param.label} | |
| </label> | |
| {param.type === 'range' && ( | |
| <span className="text-xs font-mono px-2 py-0.5 rounded" style={{ | |
| background: 'rgba(102,126,234,0.15)', | |
| color: '#8b9cff', | |
| }}> | |
| {Number(val).toFixed(param.step && param.step < 0.1 ? 2 : param.step && param.step < 1 ? 1 : 0)}{param.unit || ''} | |
| </span> | |
| )} | |
| </div> | |
| {param.type === 'range' && ( | |
| <input | |
| type="range" | |
| min={param.min} | |
| max={param.max} | |
| step={param.step} | |
| value={val} | |
| onChange={e => setParam(selectedPreset.id, param.key, parseFloat(e.target.value))} | |
| className="w-full h-1.5 rounded-full appearance-none cursor-pointer" | |
| style={{ | |
| background: `linear-gradient(to right, #667eea ${((val - (param.min||0)) / ((param.max||1) - (param.min||0))) * 100}%, rgba(255,255,255,0.1) 0%)`, | |
| outline: 'none', | |
| accentColor: '#667eea', | |
| }} | |
| /> | |
| )} | |
| {param.type === 'color' && ( | |
| <div className="flex items-center gap-2"> | |
| <input | |
| type="color" | |
| value={val} | |
| onChange={e => setParam(selectedPreset.id, param.key, e.target.value)} | |
| className="rounded cursor-pointer border-0" | |
| style={{ width: 40, height: 32, background: 'none' }} | |
| /> | |
| <span className="text-xs font-mono" style={{ color: 'rgba(255,255,255,0.4)' }}>{val}</span> | |
| </div> | |
| )} | |
| {param.type === 'select' && ( | |
| <select | |
| value={val} | |
| onChange={e => setParam(selectedPreset.id, param.key, e.target.value)} | |
| className="w-full px-3 py-2 rounded-lg text-sm" | |
| style={{ | |
| background: 'rgba(255,255,255,0.06)', | |
| border: '1px solid rgba(255,255,255,0.12)', | |
| color: '#fff', | |
| outline: 'none', | |
| }} | |
| > | |
| {param.options?.map(opt => ( | |
| <option key={opt} value={opt} style={{ background: '#1a1a2e' }}>{opt}</option> | |
| ))} | |
| </select> | |
| )} | |
| </div> | |
| ) | |
| })} | |
| </div> | |
| {/* Apply Button */} | |
| <button | |
| onClick={() => handleApply(selectedPreset)} | |
| className="w-full py-3 rounded-xl font-semibold text-sm flex items-center justify-center gap-2 transition-all mt-2" | |
| style={applied === selectedPreset.id ? { | |
| background: 'rgba(0,200,100,0.2)', | |
| border: '1px solid rgba(0,200,100,0.4)', | |
| color: '#00c864', | |
| } : { | |
| background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', | |
| color: '#fff', | |
| boxShadow: '0 8px 24px rgba(102,126,234,0.3)', | |
| }} | |
| > | |
| {applied === selectedPreset.id | |
| ? <><Check className="w-4 h-4" /> Applied!</> | |
| : <><Sparkles className="w-4 h-4" /> Apply to Project</> | |
| } | |
| </button> | |
| <p className="text-center text-xs" style={{ color: 'rgba(255,255,255,0.25)' }}> | |
| Injects into HTML, CSS & JS tabs | |
| </p> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| ) | |
| } | |