Skip to content

Instantly share code, notes, and snippets.

@sibbng
Created June 5, 2025 19:28
Show Gist options
  • Select an option

  • Save sibbng/35f336a1ad2ea4d0d58cb905dac5b90e to your computer and use it in GitHub Desktop.

Select an option

Save sibbng/35f336a1ad2ea4d0d58cb905dac5b90e to your computer and use it in GitHub Desktop.
v0 clone voby
{
"imports": {
"voby": "https://esm.sh/voby",
"oby": "https://esm.sh/oby",
"react": "https://esm.sh/react",
"gsap": "https://esm.sh/gsap",
"gsap/ScrollTrigger": "https://esm.sh/gsap/ScrollTrigger",
"@/components/ui/button": "https://esm.sh/@/components/ui/button",
"@/components/ui/input": "https://esm.sh/@/components/ui/input",
"@/components/ui/card": "https://esm.sh/@/components/ui/card",
"lucide-react": "https://esm.sh/lucide-react"
}
}
@theme {
--font-sans: 'Inter', sans-serif;
}
@custom-variant dark (&:where(.dark, .dark *));
import { $, render, For, If, Observable, useEffect } from 'voby';
import { gsap } from 'gsap';
// --- Helper Components & Icons ---
const Icon = ({ path, className = "w-5 h-5" }) => <svg class={className} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d={path}></path></svg>;
const TimeIcon = () => <Icon path="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />;
const ProjectsIcon = () => <Icon path="M3 3h7v7H3zM14 3h7v7h-7zM14 14h7v7h-7zM3 14h7v7H3z" />;
const CommunityIcon = () => <Icon path="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.653-.124-1.28-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.653.124-1.28.356-1.857m0 0a3 3 0 015.288 0M12 14a4 4 0 100-8 4 4 0 000 8z" />;
const ChevronRightIcon = () => <Icon className="w-4 h-4" path="m9 18 6-6-6-6" />;
const ChevronDownIcon = () => <Icon className="w-4 h-4" path="m6 9 6 6 6-6" />;
const SparklesIcon = () => <Icon className="w-4 h-4" path="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 5l-1.414-1.414A2 2 0 0012 2a2 2 0 00-2.586 1.586L8 5m8 0a2 2 0 012 2v2h-4V7a2 2 0 012-2z" />;
const PaperclipIcon = () => <Icon className="w-4 h-4" path="M21.44 11.05l-9.19 9.19a6 6 0 01-8.49-8.49l9.19-9.19a4 4 0 015.66 5.66l-9.2 9.19a2 2 0 01-2.83-2.83l8.49-8.48" />;
const ArrowUpIcon = () => <Icon className="w-5 h-5" path="M5 10l7-7m0 0l7 7m-7-7v18" />;
const CloseIcon = () => <Icon className="w-4 h-4" path="M18 6L6 18M6 6l12 12" />;
const SettingsIcon = () => <Icon path="M12 6V3M12 21v-3M4.93 4.93l2.12 2.12M16.95 16.95l2.12 2.12M4.93 19.07l2.12-2.12M16.95 7.05l2.12-2.12M12 18a6 6 0 100-12 6 6 0 000 12z" />;
const GithubIcon = () => <Icon path="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.54 2.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22" />;
const MenuIcon = () => <Icon path="M3 12h18M3 6h18M3 18h18" />;
const FileIcon = () => <Icon className="w-4 h-4" path="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></Icon>;
// --- Resizer Handle Component ---
const Resizer = ({ onPointerDown }) => (
<div
class="w-1.5 bg-neutral-800 hover:bg-blue-600 active:bg-blue-500 transition-colors duration-200 cursor-col-resize flex-shrink-0"
onPointerDown={onPointerDown}
></div>
);
// --- Main Application Component ---
const CodingAgentApp = (): JSX.Element => {
// --- State Management ---
const activeView = $<'preview' | 'code'>('code');
const isConsoleOpen = $(false);
const isVersionOpen = $(true);
const isRecentsOpen = $(true);
const isSidebarOpen = $(true);
const isResizing = $(false); // New state to disable GSAP during resize
// --- Panel Widths ---
const sidebarWidth = $(256);
const rightPanelWidth = $(window.innerWidth / 2.5);
// --- Refs for Animation & Sizing ---
const sidebarRef = $<HTMLElement | null>(null);
const versionContentRef = $<HTMLDivElement | null>(null);
const recentsContentRef = $<HTMLDivElement | null>(null);
const consoleDrawerRef = $<HTMLDivElement | null>(null);
// --- Resizing Logic ---
const createResizeHandler = (setWidth, clampMin, clampMax, invert = false) => (e: PointerEvent) => {
e.preventDefault();
isResizing(true);
const startX = e.clientX;
const startWidth = setWidth();
const doDrag = (moveEvent: PointerEvent) => {
let dx = moveEvent.clientX - startX;
if (invert) {
dx = -dx;
}
const newWidth = startWidth + dx;
setWidth(Math.max(clampMin, Math.min(clampMax, newWidth)));
};
const stopDrag = () => {
window.removeEventListener('pointermove', doDrag);
window.removeEventListener('pointerup', stopDrag);
document.body.style.cursor = '';
document.body.style.userSelect = '';
setTimeout(() => isResizing(false), 50); // Defer to prevent conflicts
};
document.body.style.cursor = 'col-resize';
document.body.style.userSelect = 'none';
window.addEventListener('pointermove', doDrag);
window.addEventListener('pointerup', stopDrag);
};
const handleSidebarResize = createResizeHandler(sidebarWidth, 200, 400);
const handleMainResize = createResizeHandler(rightPanelWidth, 300, Infinity, true);
// --- GSAP Animations ---
useEffect(() => {
if (isResizing()) return; // Guard against animating during resize
const element = sidebarRef();
if (!element) return;
gsap.to(element, {
width: isSidebarOpen() ? sidebarWidth() : 0,
minWidth: isSidebarOpen() ? sidebarWidth() : 0,
opacity: isSidebarOpen() ? 1 : 0,
duration: 0.4,
ease: 'power3.inOut'
});
});
useEffect(() => {
if (isResizing()) return;
const element = versionContentRef();
if (!element) return;
gsap.to(element, {
height: isVersionOpen() ? 'auto' : 0,
opacity: isVersionOpen() ? 1 : 0,
paddingTop: isVersionOpen() ? 8 : 0,
paddingBottom: isVersionOpen() ? 8 : 0,
duration: 0.4,
ease: 'power3.inOut'
});
});
useEffect(() => {
if (isResizing()) return;
const element = recentsContentRef();
if (!element) return;
gsap.to(element, {
height: isRecentsOpen() ? 'auto' : 0,
opacity: isRecentsOpen() ? 1 : 0,
marginTop: isRecentsOpen() ? 8 : 0,
duration: 0.4,
ease: 'power3.inOut'
});
});
useEffect(() => {
if (isResizing()) return;
const element = consoleDrawerRef();
if (!element) return;
gsap.to(element, {
height: isConsoleOpen() ? '8rem' : 0,
duration: 0.4,
ease: 'power3.inOut'
});
});
const codeContent = `
"use client"
import { useEffect, useRef } from "react"
import { gsap } from "gsap"
// ... more code ...
`.trim();
return (
<div class="relative flex h-screen bg-neutral-900 text-neutral-300 font-sans overflow-hidden">
{/* --- Global Header --- */}
<header class="absolute top-0 left-0 right-0 flex justify-between items-center px-4 py-2 border-b border-neutral-800 z-20 bg-neutral-900/80 backdrop-blur-sm">
<div class="flex items-center gap-4">
<button onClick={() => isSidebarOpen(v => !v)} class="text-neutral-400 hover:text-white"><MenuIcon /></button>
<div class="w-6 h-6 bg-yellow-400 rounded-md"></div>
<span class="text-sm font-semibold text-white">Gsap landing page</span>
</div>
<div class="flex items-center gap-4">
<button class="flex items-center gap-2 text-sm text-neutral-300 hover:text-white"><SettingsIcon /></button>
<button class="flex items-center gap-2 text-sm text-neutral-300 hover:text-white"><GithubIcon /></button>
<button class="text-sm bg-neutral-800 px-3 py-1 rounded-md hover:bg-neutral-700">Share</button>
<button class="text-sm bg-white text-black font-semibold px-4 py-1 rounded-md hover:bg-neutral-200">Publish</button>
<div class="w-8 h-8 bg-yellow-400 rounded-full"></div>
</div>
</header>
{/* --- Main Content Wrapper --- */}
<div class="flex flex-1 pt-14">
{/* --- Left Sidebar --- */}
<aside class="bg-neutral-950/50 border-r border-neutral-800 flex flex-col p-0 overflow-hidden" style={() => ({ width: `${sidebarWidth()}px`, minWidth: `${sidebarWidth()}px` })} ref={sidebarRef}>
<div style={() => ({ width: `${sidebarWidth()}px`, minWidth: `${sidebarWidth()}px` })} class="h-full p-3 space-y-4 flex flex-col flex-1">
<button class="w-full bg-neutral-800 text-white font-semibold py-2 rounded-md hover:bg-neutral-700 transition-colors">New Chat</button>
<nav class="space-y-2">
<a href="#" class="flex items-center gap-3 px-2 py-1.5 rounded-md text-neutral-400 hover:bg-neutral-800 hover:text-white"><TimeIcon /> Recents</a>
<a href="#" class="flex items-center gap-3 px-2 py-1.5 rounded-md text-neutral-400 hover:bg-neutral-800 hover:text-white"><ProjectsIcon /> Projects</a>
</nav>
<div class="flex-1 overflow-y-auto space-y-4">
<div class="text-sm">
<button class="w-full flex items-center justify-between text-neutral-500" onClick={() => isRecentsOpen(v => !v)}>
<span>Recents</span>
<If when={isRecentsOpen} fallback={<ChevronRightIcon />}><ChevronDownIcon /></If>
</button>
<div class="overflow-hidden h-0" ref={recentsContentRef}>
<div class="space-y-1">
<a href="#" class="block bg-neutral-800 text-white font-semibold px-2 py-1.5 rounded-md">Gsap landing page</a>
<a href="#" class="block text-neutral-400 px-2 py-1.5 rounded-md hover:bg-neutral-800">Voby app example</a>
</div>
</div>
</div>
</div>
<div class="bg-neutral-800 p-3 rounded-md text-sm">
<p class="font-semibold text-white">New Feature</p>
<p class="text-neutral-400">Introducing GitHub sync on v0</p>
</div>
</div>
</aside>
<Resizer onPointerDown={handleSidebarResize} />
{/* --- Center & Right Panels --- */}
<div class="flex-1 flex overflow-hidden">
<main class="flex-1 flex flex-col min-w-0">
<div class="flex-1 p-6 overflow-y-auto">
<div class="w-full max-w-4xl mx-auto text-sm">
{/* ... Chat Content ... */}
<div class="flex items-start gap-4 mb-8">
<div class="w-8 h-8 rounded-full bg-yellow-400 flex-shrink-0"></div>
<p class="text-white pt-1">generate modern agency landing page with gsap</p>
</div>
<div class="flex items-start gap-4">
<div class="w-8 h-8 rounded-full bg-neutral-700 flex-shrink-0"></div>
<div class="flex-1">
<p class="text-neutral-400 text-sm mb-4">Thought for 5 seconds</p>
<p class="text-white mb-6">I'll create a modern agency landing page with smooth GSAP animations, featuring hero animations, scroll-triggered effects, and elegant transitions.</p>
<div class="border border-neutral-700 rounded-lg mb-6">
<button class="w-full flex justify-between items-center p-3" onClick={() => isVersionOpen(v => !v)}>
<div class="flex items-center gap-2 text-sm"><span>Version 1</span><If when={isVersionOpen} fallback={<ChevronRightIcon />}><ChevronDownIcon /></If></div>
<div><span class="text-xs bg-neutral-700 px-2 py-1 rounded-md mr-2">Latest</span><span class="text-xs bg-blue-600 text-white px-2 py-1 rounded-md">Viewing</span></div>
</button>
<div class="overflow-hidden h-0 p-0" ref={versionContentRef}>
<div class="px-3 space-y-2">
<div class="flex items-center gap-2 text-sm p-2 bg-neutral-800/50 rounded-md"><FileIcon /> app/page.tsx</div>
<div class="flex items-center gap-2 text-sm p-2 bg-neutral-800/50 rounded-md"><FileIcon /> components/ui/card.tsx</div>
</div>
</div>
</div>
<p class="mb-6">I've created a modern agency landing page with sophisticated GSAP animations including:</p>
<h3 class="text-white font-semibold mb-3">Key Features:</h3>
<ul class="list-disc list-inside space-y-2 text-neutral-300 mb-6">
<li><span class="font-semibold text-white">Hero Section:</span> Animated text reveals, staggered stats, and parallax background</li>
<li><span class="font-semibold text-white">Scroll Animations:</span> Triggered animations for each section as you scroll</li>
</ul>
</div>
</div>
</div>
</div>
<footer class="p-4 w-full max-w-4xl mx-auto">
<div class="space-y-3">
<div>
<span class="text-sm font-semibold text-white">Suggestions</span>
<div class="flex gap-2 mt-2">
<button class="text-sm bg-neutral-800 px-3 py-1.5 rounded-md hover:bg-neutral-700">Add Integration</button>
<button class="text-sm bg-neutral-800 px-3 py-1.5 rounded-md hover:bg-neutral-700">Add contact form</button>
</div>
</div>
<div class="bg-yellow-900/30 border border-yellow-700/50 text-yellow-300 text-sm px-4 py-2 rounded-lg flex justify-between items-center">
<span>You are running low on credits. Your limit will reset on July 1.</span>
<a href="#" class="font-semibold text-white hover:underline">Upgrade Plan</a>
</div>
<div class="bg-neutral-800 border border-neutral-700 rounded-lg p-3">
<textarea class="w-full bg-transparent focus:outline-none resize-none" placeholder="Ask a follow-up..." rows={2}></textarea>
<div class="flex justify-between items-center">
<button class="flex items-center gap-1 text-sm text-neutral-400 hover:text-white"><SparklesIcon /> v0-1.5-md <ChevronDownIcon /></button>
<div class="flex items-center gap-2">
<button class="text-neutral-400 hover:text-white"><PaperclipIcon /></button>
<button class="bg-neutral-600 text-white p-1.5 rounded-md hover:bg-neutral-500"><ArrowUpIcon /></button>
</div>
</div>
</div>
<p class="text-center text-xs text-neutral-600">v0 may make mistakes. Please use with discretion.</p>
</div>
</footer>
</main>
<Resizer onPointerDown={handleMainResize} />
<aside class="bg-black/50 border-l border-neutral-800 flex flex-col" style={() => ({ width: `${rightPanelWidth()}px`, minWidth: '300px' })}>
<div class="flex justify-between items-center p-3 border-b border-neutral-800">
<div class="flex gap-1 bg-neutral-800 p-1 rounded-md">
<button onClick={() => activeView('preview')} class={() => `px-3 py-1 text-sm rounded ${activeView() === 'preview' ? 'bg-neutral-600 text-white' : 'text-neutral-400'}`}>Preview</button>
<button onClick={() => activeView('code')} class={() => `px-3 py-1 text-sm rounded ${activeView() === 'code' ? 'bg-neutral-600 text-white' : 'text-neutral-400'}`}>Code</button>
</div>
</div>
<div class="flex-1 p-4 overflow-auto relative">
<If when={() => activeView() === 'code'}><pre class="text-sm"><code class="language-javascript font-mono">{codeContent}</code></pre></If>
<If when={() => activeView() === 'preview'}><div class="w-full h-full bg-white text-black p-4"><h1 class="text-2xl font-bold">Agency Landing Page</h1></div></If>
</div>
<div class="relative border-t border-neutral-800">
<button class="w-full text-left p-3 text-sm text-neutral-400" onClick={() => isConsoleOpen(v => !v)}>Console</button>
<div class="absolute bottom-full left-0 right-0 h-0 bg-neutral-900 border-t border-neutral-700 overflow-hidden flex flex-col" ref={consoleDrawerRef}>
<div class="p-2 flex justify-between items-center bg-neutral-800 flex-shrink-0">
<p class="text-xs">Console Output</p>
<button onClick={() => isConsoleOpen(false)} class="text-neutral-400 hover:text-white"><CloseIcon /></button>
</div>
<div class="p-4 bg-black h-full overflow-y-auto"><pre class="text-xs font-mono text-green-400">{`> GSAP animation sequence started...`}</pre></div>
</div>
</div>
</aside>
</div>
</div>
</div>
);
};
render(<CodingAgentApp />, document.getElementById('app'));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment