Files
fiddle/public/js/templates.js
root 6ca8519250 Add responsive preview, editor themes, template gallery, devtools, and autocomplete
- Device breakpoint toggles (mobile 375px / tablet 768px / desktop 100%)
- Editor theme selector with 6 themes (VS Dark/Light, High Contrast, Monokai, Dracula, GitHub Dark)
- Starter template gallery with 8 pre-built templates (Todo, API Fetch, CSS Animation, etc.)
- Code autocomplete with DOM/React type definitions and snippet completions
- Devtools panels: console, network, elements, performance
- Code formatter (Prettier), diff view, and linter integration
2026-02-27 01:22:16 -06:00

339 lines
15 KiB
JavaScript

// Starter template gallery data
export const GALLERY_TEMPLATES = [
{
id: 'todo-app',
title: 'Todo App',
description: 'Classic todo list with add, complete, and delete',
mode: 'html-css-js',
icon: '\u2713',
html: `<div class="todo-app">
<h1>Todo List</h1>
<form id="todo-form">
<input type="text" id="todo-input" placeholder="Add a task..." autofocus>
<button type="submit">Add</button>
</form>
<ul id="todo-list"></ul>
</div>`,
css: `body { font-family: -apple-system, sans-serif; background: #f0f2f5; display: flex; justify-content: center; padding: 40px 20px; }
.todo-app { background: #fff; border-radius: 12px; padding: 24px; width: 100%; max-width: 400px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
h1 { margin: 0 0 16px; font-size: 24px; color: #1a1a1a; }
#todo-form { display: flex; gap: 8px; margin-bottom: 16px; }
#todo-input { flex: 1; padding: 10px 12px; border: 2px solid #e0e0e0; border-radius: 8px; font-size: 14px; outline: none; }
#todo-input:focus { border-color: #0078d4; }
button { background: #0078d4; color: #fff; border: none; padding: 10px 20px; border-radius: 8px; cursor: pointer; font-size: 14px; }
button:hover { background: #106ebe; }
ul { list-style: none; padding: 0; }
li { display: flex; align-items: center; gap: 10px; padding: 10px 0; border-bottom: 1px solid #f0f0f0; }
li.done span { text-decoration: line-through; color: #999; }
li span { flex: 1; font-size: 14px; }
li .delete { background: none; color: #e74c3c; padding: 4px 8px; font-size: 18px; }
li .delete:hover { background: #fde8e8; border-radius: 4px; }`,
js: `const form = document.getElementById('todo-form');
const input = document.getElementById('todo-input');
const list = document.getElementById('todo-list');
form.addEventListener('submit', (e) => {
e.preventDefault();
const text = input.value.trim();
if (!text) return;
addTodo(text);
input.value = '';
});
function addTodo(text) {
const li = document.createElement('li');
li.innerHTML = \`<input type="checkbox"><span>\${text}</span><button class="delete">&times;</button>\`;
li.querySelector('input').addEventListener('change', () => li.classList.toggle('done'));
li.querySelector('.delete').addEventListener('click', () => li.remove());
list.appendChild(li);
}`,
},
{
id: 'api-fetch',
title: 'API Fetch',
description: 'Fetch data from a REST API and display results',
mode: 'html-css-js',
icon: '\u21C5',
html: `<div class="container">
<h1>Random Users</h1>
<button id="btn-fetch">Fetch Users</button>
<div id="users" class="users"></div>
</div>`,
css: `body { font-family: -apple-system, sans-serif; background: #f5f5f5; padding: 40px 20px; }
.container { max-width: 600px; margin: 0 auto; }
h1 { margin: 0 0 16px; color: #333; }
button { background: #0078d4; color: #fff; border: none; padding: 10px 24px; border-radius: 8px; cursor: pointer; font-size: 14px; margin-bottom: 20px; }
button:hover { background: #106ebe; }
.users { display: flex; flex-direction: column; gap: 12px; }
.user-card { display: flex; align-items: center; gap: 14px; background: #fff; padding: 14px; border-radius: 10px; box-shadow: 0 1px 4px rgba(0,0,0,0.08); }
.user-card img { width: 50px; height: 50px; border-radius: 50%; }
.user-info h3 { margin: 0; font-size: 15px; color: #1a1a1a; }
.user-info p { margin: 4px 0 0; font-size: 13px; color: #666; }`,
js: `document.getElementById('btn-fetch').addEventListener('click', fetchUsers);
async function fetchUsers() {
const container = document.getElementById('users');
container.innerHTML = '<p>Loading...</p>';
try {
const res = await fetch('https://randomuser.me/api/?results=5');
const data = await res.json();
container.innerHTML = data.results.map(u => \`
<div class="user-card">
<img src="\${u.picture.medium}" alt="\${u.name.first}">
<div class="user-info">
<h3>\${u.name.first} \${u.name.last}</h3>
<p>\${u.email}</p>
</div>
</div>
\`).join('');
} catch (e) {
container.innerHTML = '<p>Error fetching users.</p>';
}
}
fetchUsers();`,
},
{
id: 'css-animation',
title: 'CSS Animation',
description: 'Animated shapes with pure CSS keyframes',
mode: 'html-css-js',
icon: '\u25CF',
html: `<div class="scene">
<div class="orbit">
<div class="planet"></div>
</div>
<div class="sun"></div>
<div class="stars"></div>
</div>`,
css: `body { margin: 0; overflow: hidden; background: #0a0a2e; display: flex; align-items: center; justify-content: center; height: 100vh; }
.scene { position: relative; width: 300px; height: 300px; }
.sun { position: absolute; top: 50%; left: 50%; width: 60px; height: 60px; margin: -30px 0 0 -30px; background: radial-gradient(circle, #ffd700, #ff8c00); border-radius: 50%; box-shadow: 0 0 40px #ffd700, 0 0 80px #ff8c0066; animation: pulse 2s ease-in-out infinite; }
@keyframes pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.08); } }
.orbit { position: absolute; top: 50%; left: 50%; width: 200px; height: 200px; margin: -100px 0 0 -100px; border: 1px solid rgba(255,255,255,0.1); border-radius: 50%; animation: spin 6s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
.planet { position: absolute; top: -10px; left: 50%; margin-left: -10px; width: 20px; height: 20px; background: radial-gradient(circle, #4fc3f7, #0288d1); border-radius: 50%; box-shadow: 0 0 12px #4fc3f766; }
.stars { position: absolute; inset: -50px; background-image: radial-gradient(2px 2px at 20px 30px, #fff, transparent), radial-gradient(2px 2px at 80px 120px, #fff, transparent), radial-gradient(1px 1px at 160px 60px, #ddd, transparent), radial-gradient(2px 2px at 240px 180px, #fff, transparent), radial-gradient(1px 1px at 50px 200px, #ccc, transparent), radial-gradient(1px 1px at 190px 20px, #eee, transparent); background-repeat: repeat; animation: twinkle 4s ease-in-out infinite alternate; }
@keyframes twinkle { from { opacity: 0.6; } to { opacity: 1; } }`,
js: `// Pure CSS animation - no JS needed!
console.log('CSS animation running');`,
},
{
id: 'form-validation',
title: 'Form Validation',
description: 'Client-side form validation with live feedback',
mode: 'html-css-js',
icon: '\u2611',
html: `<div class="form-wrap">
<h1>Sign Up</h1>
<form id="signup" novalidate>
<div class="field">
<label>Name</label>
<input type="text" id="name" placeholder="Your name" required>
<span class="error" id="name-error"></span>
</div>
<div class="field">
<label>Email</label>
<input type="email" id="email" placeholder="you@example.com" required>
<span class="error" id="email-error"></span>
</div>
<div class="field">
<label>Password</label>
<input type="password" id="password" placeholder="Min 8 characters" required>
<span class="error" id="password-error"></span>
</div>
<button type="submit">Create Account</button>
</form>
<div id="success" class="success hidden">Account created!</div>
</div>`,
css: `body { font-family: -apple-system, sans-serif; background: #f0f2f5; display: flex; justify-content: center; padding: 40px 20px; }
.form-wrap { background: #fff; padding: 32px; border-radius: 12px; width: 100%; max-width: 380px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
h1 { margin: 0 0 20px; font-size: 22px; }
.field { margin-bottom: 16px; }
label { display: block; font-size: 13px; font-weight: 600; margin-bottom: 4px; color: #333; }
input { width: 100%; padding: 10px 12px; border: 2px solid #e0e0e0; border-radius: 8px; font-size: 14px; outline: none; box-sizing: border-box; }
input:focus { border-color: #0078d4; }
input.invalid { border-color: #e74c3c; }
input.valid { border-color: #27ae60; }
.error { font-size: 12px; color: #e74c3c; min-height: 18px; display: block; }
button { width: 100%; background: #0078d4; color: #fff; border: none; padding: 12px; border-radius: 8px; font-size: 15px; cursor: pointer; }
button:hover { background: #106ebe; }
.success { text-align: center; color: #27ae60; font-size: 16px; font-weight: 600; padding: 16px 0; }
.hidden { display: none; }`,
js: `const form = document.getElementById('signup');
const fields = {
name: { el: document.getElementById('name'), err: document.getElementById('name-error'), validate: v => v.length >= 2 ? '' : 'Name must be at least 2 characters' },
email: { el: document.getElementById('email'), err: document.getElementById('email-error'), validate: v => /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(v) ? '' : 'Enter a valid email' },
password: { el: document.getElementById('password'), err: document.getElementById('password-error'), validate: v => v.length >= 8 ? '' : 'Password must be at least 8 characters' },
};
Object.values(fields).forEach(f => {
f.el.addEventListener('input', () => {
const msg = f.validate(f.el.value);
f.err.textContent = msg;
f.el.classList.toggle('invalid', !!msg);
f.el.classList.toggle('valid', !msg && f.el.value.length > 0);
});
});
form.addEventListener('submit', (e) => {
e.preventDefault();
let valid = true;
Object.values(fields).forEach(f => {
const msg = f.validate(f.el.value);
f.err.textContent = msg;
f.el.classList.toggle('invalid', !!msg);
f.el.classList.toggle('valid', !msg && f.el.value.length > 0);
if (msg) valid = false;
});
if (valid) {
form.classList.add('hidden');
document.getElementById('success').classList.remove('hidden');
}
});`,
},
{
id: 'react-counter',
title: 'React Counter',
description: 'Stateful React component with hooks',
mode: 'react',
icon: '\u269B',
html: '',
css: `body { font-family: -apple-system, sans-serif; background: #f0f2f5; display: flex; justify-content: center; padding: 60px 20px; }
.counter { background: #fff; padding: 40px; border-radius: 16px; text-align: center; box-shadow: 0 2px 12px rgba(0,0,0,0.1); min-width: 280px; }
h1 { margin: 0 0 8px; font-size: 22px; color: #333; }
.count { font-size: 64px; font-weight: 700; color: #0078d4; margin: 20px 0; }
.buttons { display: flex; gap: 12px; justify-content: center; }
.buttons button { font-size: 20px; width: 48px; height: 48px; border-radius: 50%; border: 2px solid #0078d4; background: #fff; color: #0078d4; cursor: pointer; display: flex; align-items: center; justify-content: center; }
.buttons button:hover { background: #0078d4; color: #fff; }
.reset { margin-top: 16px; font-size: 13px; background: none; border: none; color: #999; cursor: pointer; text-decoration: underline; }`,
js: `const App = () => {
const [count, setCount] = React.useState(0);
return (
<div className="counter">
<h1>React Counter</h1>
<div className="count">{count}</div>
<div className="buttons">
<button onClick={() => setCount(c => c - 1)}>\u2212</button>
<button onClick={() => setCount(c => c + 1)}>+</button>
</div>
<button className="reset" onClick={() => setCount(0)}>Reset</button>
</div>
);
};
ReactDOM.createRoot(document.getElementById('root')).render(<App />);`,
},
{
id: 'canvas-game',
title: 'Canvas Game',
description: 'Simple bouncing ball canvas animation',
mode: 'html-css-js',
icon: '\u25B6',
html: `<canvas id="canvas" width="400" height="300"></canvas>`,
css: `body { margin: 0; background: #1a1a2e; display: flex; align-items: center; justify-content: center; height: 100vh; }
canvas { border-radius: 8px; background: #16213e; box-shadow: 0 4px 20px rgba(0,0,0,0.3); }`,
js: `const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const balls = Array.from({ length: 8 }, () => ({
x: Math.random() * canvas.width,
y: Math.random() * canvas.height,
r: 8 + Math.random() * 16,
dx: (Math.random() - 0.5) * 4,
dy: (Math.random() - 0.5) * 4,
color: \`hsl(\${Math.random() * 360}, 70%, 60%)\`,
}));
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (const b of balls) {
b.x += b.dx;
b.y += b.dy;
if (b.x - b.r < 0 || b.x + b.r > canvas.width) b.dx *= -1;
if (b.y - b.r < 0 || b.y + b.r > canvas.height) b.dy *= -1;
ctx.beginPath();
ctx.arc(b.x, b.y, b.r, 0, Math.PI * 2);
ctx.fillStyle = b.color;
ctx.fill();
ctx.shadowColor = b.color;
ctx.shadowBlur = 12;
}
ctx.shadowBlur = 0;
requestAnimationFrame(draw);
}
draw();`,
},
{
id: 'analog-clock',
title: 'Analog Clock',
description: 'Real-time analog clock with CSS and JS',
mode: 'html-css-js',
icon: '\u23F0',
html: `<div class="clock">
<div class="hand hour" id="hour"></div>
<div class="hand minute" id="minute"></div>
<div class="hand second" id="second"></div>
<div class="center"></div>
</div>`,
css: `body { margin: 0; background: #1a1a2e; display: flex; align-items: center; justify-content: center; height: 100vh; }
.clock { position: relative; width: 200px; height: 200px; border: 4px solid #e0e0e0; border-radius: 50%; background: #fff; }
.hand { position: absolute; bottom: 50%; left: 50%; transform-origin: bottom center; border-radius: 4px; }
.hour { width: 4px; height: 50px; background: #333; margin-left: -2px; }
.minute { width: 3px; height: 70px; background: #555; margin-left: -1.5px; }
.second { width: 1.5px; height: 80px; background: #e74c3c; margin-left: -0.75px; }
.center { position: absolute; top: 50%; left: 50%; width: 10px; height: 10px; margin: -5px 0 0 -5px; background: #333; border-radius: 50%; z-index: 2; }`,
js: `function tick() {
const now = new Date();
const s = now.getSeconds() + now.getMilliseconds() / 1000;
const m = now.getMinutes() + s / 60;
const h = (now.getHours() % 12) + m / 60;
document.getElementById('second').style.transform = \`rotate(\${s * 6}deg)\`;
document.getElementById('minute').style.transform = \`rotate(\${m * 6}deg)\`;
document.getElementById('hour').style.transform = \`rotate(\${h * 30}deg)\`;
requestAnimationFrame(tick);
}
tick();`,
},
{
id: 'svelte-reactive',
title: 'Svelte Reactive',
description: 'Svelte reactivity with bound inputs and computed values',
mode: 'svelte',
icon: '\u26A1',
html: '',
css: '',
js: `<script>
let name = 'World';
let count = 0;
$: doubled = count * 2;
$: greeting = \`Hello, \${name}!\`;
</script>
<div class="app">
<h1>{greeting}</h1>
<label>
Name: <input bind:value={name}>
</label>
<div class="counter">
<button on:click={() => count--}>\u2212</button>
<span>{count} (doubled: {doubled})</span>
<button on:click={() => count++}>+</button>
</div>
</div>
<style>
.app { font-family: -apple-system, sans-serif; max-width: 400px; margin: 40px auto; text-align: center; }
h1 { color: #ff3e00; }
label { display: block; margin: 16px 0; font-size: 14px; }
input { padding: 8px 12px; border: 2px solid #ddd; border-radius: 6px; font-size: 14px; }
input:focus { border-color: #ff3e00; outline: none; }
.counter { display: flex; align-items: center; justify-content: center; gap: 16px; margin-top: 20px; }
button { width: 40px; height: 40px; border-radius: 50%; border: 2px solid #ff3e00; background: #fff; color: #ff3e00; font-size: 20px; cursor: pointer; }
button:hover { background: #ff3e00; color: #fff; }
span { font-size: 18px; font-weight: 600; }
</style>`,
},
];