Feat: App shell with module loader and Tailwind

This commit is contained in:
2026-03-20 22:07:31 -07:00
parent 568d8dd08c
commit 0c8bf2585c

View File

@@ -4,154 +4,112 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>vibegif.lol — AI Generated Gifs</title>
<link rel="stylesheet" href="/styles.css">
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/lucide@latest"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gif.js/0.2.0/gif.js"></script>
<link rel="stylesheet" href="/styles.css">
</head>
<body class="min-h-screen bg-white text-neutral-800">
<!-- Header -->
<header class="fixed top-0 inset-x-0 z-50 bg-white/80 backdrop-blur border-b border-neutral-200">
<div class="max-w-2xl mx-auto px-4 h-14 flex items-center justify-between">
<h1 class="text-2xl tracking-tight">vibegif<span class="text-neutral-400">.lol</span></h1>
<button id="btn-settings" class="p-2 rounded-lg hover:bg-neutral-100 transition">
<body class="bg-white text-neutral-800 min-h-screen flex flex-col items-center">
<div id="app" class="w-full max-w-xl mx-auto px-4 py-8 flex flex-col items-center gap-6">
<header class="w-full flex items-center justify-between">
<h1 class="text-4xl tracking-tight">vibegif<span class="text-neutral-400">.lol</span></h1>
<button id="btn-settings" class="p-2 rounded-lg hover:bg-neutral-100 transition" title="Settings">
<i data-lucide="panel-left" class="w-5 h-5 text-neutral-500"></i>
</button>
</div>
</header>
</header>
<!-- Settings Modal -->
<div id="modal-settings" class="fixed inset-0 z-60 hidden items-center justify-center bg-black/30 backdrop-blur-sm">
<div class="bg-white rounded-2xl shadow-xl w-full max-w-md mx-4 p-6">
<div class="flex items-center justify-between mb-6">
<h2 class="text-xl">account settings</h2>
<button id="btn-close-settings" class="p-1 rounded-lg hover:bg-neutral-100">
<i data-lucide="x" class="w-5 h-5"></i>
</button>
<div id="setup-screen" class="w-full flex flex-col items-center gap-4 mt-12 hidden">
<p class="text-neutral-500 text-center text-lg">enter your <a href="https://openrouter.ai/keys" target="_blank" class="underline hover:text-neutral-800">OpenRouter</a> API key to start</p>
<input id="setup-key" type="password" placeholder="sk-or-..." class="w-full border border-neutral-300 rounded-lg px-4 py-3 text-center focus:outline-none focus:ring-2 focus:ring-neutral-400">
<button id="setup-save" class="bg-neutral-800 text-white rounded-lg px-6 py-3 hover:bg-neutral-700 transition">save & start vibing</button>
</div>
<div id="main-screen" class="w-full flex flex-col gap-5 hidden">
<div class="flex flex-col gap-2">
<label class="text-xs text-neutral-400 uppercase tracking-wider">model</label>
<select id="sel-model" class="border border-neutral-300 rounded-lg px-3 py-2 bg-white focus:outline-none focus:ring-2 focus:ring-neutral-400">
<option value="google/gemini-3.1-flash-image-preview">Gemini 3.1 Flash Image</option>
<option value="bytedance-seed/seedream-4.5">Seedream 4.5</option>
</select>
</div>
<div class="flex flex-col gap-2">
<label class="text-xs text-neutral-400 uppercase tracking-wider">prompt</label>
<input id="inp-prompt" type="text" placeholder="rolling cat" class="border border-neutral-300 rounded-lg px-4 py-3 focus:outline-none focus:ring-2 focus:ring-neutral-400">
<p class="text-xs text-neutral-400">keep it simple — e.g. "rolling cat", "bouncing ball", "waving hand"</p>
</div>
<div class="grid grid-cols-2 gap-4">
<div class="flex flex-col gap-2">
<label class="text-xs text-neutral-400 uppercase tracking-wider">frames</label>
<input id="inp-frames" type="number" value="4" min="2" max="24" class="border border-neutral-300 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-neutral-400">
</div>
<div class="flex flex-col gap-2">
<label class="text-xs text-neutral-400 uppercase tracking-wider">fps</label>
<input id="inp-fps" type="number" value="4" min="1" max="30" class="border border-neutral-300 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-neutral-400">
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<div class="flex flex-col gap-2">
<label class="text-xs text-neutral-400 uppercase tracking-wider">size</label>
<select id="sel-size" class="border border-neutral-300 rounded-lg px-3 py-2 bg-white focus:outline-none focus:ring-2 focus:ring-neutral-400">
<option value="1K">1K</option>
<option value="0.5K">0.5K (Gemini only)</option>
</select>
</div>
<div class="flex flex-col gap-2">
<label class="text-xs text-neutral-400 uppercase tracking-wider">aspect ratio</label>
<select id="sel-ratio" class="border border-neutral-300 rounded-lg px-3 py-2 bg-white focus:outline-none focus:ring-2 focus:ring-neutral-400">
<option value="1:1">1:1</option>
<option value="16:9">16:9</option>
<option value="9:16">9:16</option>
<option value="4:3">4:3</option>
<option value="3:2">3:2</option>
</select>
</div>
</div>
<button id="btn-generate" class="bg-neutral-800 text-white rounded-lg px-6 py-3 hover:bg-neutral-700 transition flex items-center justify-center gap-2 text-lg">
<i data-lucide="sparkles" class="w-5 h-5"></i>
generate gif
</button>
<div id="progress-area" class="w-full hidden flex-col items-center gap-3">
<div class="w-full bg-neutral-100 rounded-full h-2 overflow-hidden">
<div id="progress-bar" class="bg-neutral-800 h-2 rounded-full transition-all duration-300" style="width:0%"></div>
</div>
<p id="progress-text" class="text-sm text-neutral-400"></p>
<div id="frames-preview" class="flex gap-2 flex-wrap justify-center"></div>
</div>
<div id="result-area" class="w-full hidden flex-col items-center gap-4">
<img id="result-gif" class="rounded-xl border border-neutral-200 max-w-full">
<a id="btn-download" download="vibegif.gif" class="bg-neutral-800 text-white rounded-lg px-6 py-3 hover:bg-neutral-700 transition flex items-center gap-2 cursor-pointer">
<i data-lucide="download" class="w-5 h-5"></i>
download gif
</a>
</div>
<label class="block text-sm text-neutral-500 mb-2">openrouter api key</label>
<input id="input-apikey-modal" type="password" placeholder="sk-or-..."
class="w-full px-4 py-3 rounded-xl border border-neutral-200 bg-neutral-50 focus:outline-none focus:ring-2 focus:ring-neutral-300 font-mono text-sm">
<p class="mt-3 text-xs text-neutral-400">your key is stored locally and never sent anywhere except openrouter.</p>
</div>
</div>
<!-- Main Content -->
<main class="pt-20 pb-32 px-4 max-w-2xl mx-auto">
<!-- Onboarding -->
<div id="section-onboarding" class="text-center py-20 hidden">
<div class="text-6xl mb-4">✏️</div>
<h2 class="text-2xl mb-2">welcome to vibegif</h2>
<p class="text-neutral-500 mb-6">ai-generated gifs from simple prompts</p>
<label class="block text-sm text-neutral-500 mb-2">enter your openrouter api key to start</label>
<input id="input-apikey-onboard" type="password" placeholder="sk-or-..."
class="w-full max-w-sm mx-auto px-4 py-3 rounded-xl border border-neutral-200 bg-neutral-50 focus:outline-none focus:ring-2 focus:ring-neutral-300 font-mono text-sm">
<p class="mt-3 text-xs text-neutral-400">get one at <a href="https://openrouter.ai/keys" target="_blank" class="underline">openrouter.ai/keys</a></p>
</div>
<!-- Generator -->
<div id="section-generator" class="hidden">
<!-- Prompt -->
<div class="space-y-4 mb-8">
<div>
<label class="block text-sm text-neutral-500 mb-2">prompt</label>
<input id="input-prompt" type="text" placeholder="rolling cat"
class="w-full px-4 py-3 rounded-xl border border-neutral-200 bg-neutral-50 focus:outline-none focus:ring-2 focus:ring-neutral-300">
<p class="mt-1 text-xs text-neutral-400">keep it simple — e.g. "dancing star", "bouncing ball", "waving hand"</p>
</div>
<!-- Options -->
<div class="grid grid-cols-2 sm:grid-cols-4 gap-3">
<div>
<label class="block text-xs text-neutral-500 mb-1">model</label>
<select id="select-model" class="w-full px-3 py-2 rounded-lg border border-neutral-200 bg-neutral-50 text-sm focus:outline-none">
<option value="google/gemini-3.1-flash-image-preview">gemini flash</option>
<option value="bytedance-seed/seedream-4.5">seedream 4.5</option>
</select>
</div>
<div>
<label class="block text-xs text-neutral-500 mb-1">frames</label>
<select id="select-frames" class="w-full px-3 py-2 rounded-lg border border-neutral-200 bg-neutral-50 text-sm focus:outline-none">
<option value="2">2</option>
<option value="3">3</option>
<option value="4" selected>4</option>
<option value="5">5</option>
<option value="6">6</option>
<option value="7">7</option>
<option value="8">8</option>
</select>
</div>
<div>
<label class="block text-xs text-neutral-500 mb-1">fps</label>
<select id="select-fps" class="w-full px-3 py-2 rounded-lg border border-neutral-200 bg-neutral-50 text-sm focus:outline-none">
<option value="2">2</option>
<option value="4">4</option>
<option value="6" selected>6</option>
<option value="8">8</option>
<option value="10">10</option>
<option value="12">12</option>
</select>
</div>
<div>
<label class="block text-xs text-neutral-500 mb-1">size</label>
<select id="select-size" class="w-full px-3 py-2 rounded-lg border border-neutral-200 bg-neutral-50 text-sm focus:outline-none">
<option value="1K">1K</option>
<option id="option-half-k" value="0.5K">0.5K</option>
</select>
</div>
</div>
<!-- Aspect Ratio -->
<div>
<label class="block text-xs text-neutral-500 mb-1">aspect ratio</label>
<div id="aspect-buttons" class="flex flex-wrap gap-2">
<button data-ar="1:1" class="ar-btn active px-3 py-1.5 rounded-lg text-xs font-medium transition">1:1</button>
<button data-ar="16:9" class="ar-btn px-3 py-1.5 rounded-lg text-xs font-medium transition">16:9</button>
<button data-ar="9:16" class="ar-btn px-3 py-1.5 rounded-lg text-xs font-medium transition">9:16</button>
<button data-ar="4:3" class="ar-btn px-3 py-1.5 rounded-lg text-xs font-medium transition">4:3</button>
<button data-ar="3:4" class="ar-btn px-3 py-1.5 rounded-lg text-xs font-medium transition">3:4</button>
</div>
</div>
<!-- Generate Button -->
<button id="btn-generate"
class="w-full py-3 rounded-xl font-medium transition bg-neutral-800 text-white hover:bg-neutral-700">
generate gif
<!-- Settings Modal -->
<div id="modal-settings" class="fixed inset-0 bg-black/30 flex items-center justify-center z-50 hidden">
<div class="bg-white rounded-2xl p-6 w-full max-w-sm mx-4 flex flex-col gap-4 shadow-xl">
<div class="flex items-center justify-between">
<h2 class="text-xl">account settings</h2>
<button id="btn-close-modal" class="p-1 hover:bg-neutral-100 rounded-lg transition">
<i data-lucide="x" class="w-5 h-5 text-neutral-500"></i>
</button>
</div>
<!-- Frames Preview -->
<div id="section-frames" class="mb-8 hidden">
<h3 class="text-sm text-neutral-500 mb-3">frames <span id="frame-counter"></span></h3>
<div id="frames-grid" class="grid grid-cols-4 gap-2"></div>
<div class="flex flex-col gap-2">
<label class="text-xs text-neutral-400 uppercase tracking-wider">OpenRouter API Key</label>
<input id="modal-key" type="password" placeholder="sk-or-..." class="border border-neutral-300 rounded-lg px-4 py-3 focus:outline-none focus:ring-2 focus:ring-neutral-400">
</div>
<!-- Result -->
<div id="section-result" class="text-center hidden">
<h3 class="text-sm text-neutral-500 mb-3">your gif</h3>
<div class="inline-block rounded-2xl overflow-hidden border border-neutral-200 shadow-lg">
<img id="gif-preview" class="max-w-full" src="">
</div>
<div class="mt-4">
<a id="btn-download" href="#" download="vibegif.gif"
class="inline-flex items-center gap-2 px-6 py-3 bg-neutral-800 text-white rounded-xl hover:bg-neutral-700 transition">
<i data-lucide="download" class="w-4 h-4"></i>
download gif
</a>
</div>
</div>
<!-- Error -->
<div id="section-error" class="mt-4 p-4 rounded-xl bg-red-50 text-red-600 text-sm hidden"></div>
<button id="modal-save" class="bg-neutral-800 text-white rounded-lg px-6 py-3 hover:bg-neutral-700 transition">save</button>
</div>
</main>
</div>
<!-- Footer -->
<footer class="fixed bottom-0 inset-x-0 bg-white/80 backdrop-blur border-t border-neutral-100 py-3 text-center text-xs text-neutral-400">
byok · your key never leaves your browser
</footer>
<script src="/app.js"></script>
<script type="module" src="/app.js"></script>
</body>
</html>