anycoder-b8c90549 / index.html
MoShow's picture
Upload folder using huggingface_hub
5dadbd6 verified
Raw
History Blame Contribute Delete
40.2 kB
<!DOCTYPE html>
<html lang="en" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LiveCode Studio - Interactive Preview</title>
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@300;400;500;600;700&family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<!-- Lucide Icons -->
<script src="https://unpkg.com/lucide@latest"></script>
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
fontFamily: {
mono: ['Fira Code', 'monospace'],
sans: ['Inter', 'system-ui', 'sans-serif'],
},
colors: {
editor: {
bg: '#0d1117',
surface: '#161b22',
elevated: '#21262d',
border: '#30363d',
hover: '#30363d',
active: '#388bfd33',
},
code: {
html: '#7ee787',
css: '#79c0ff',
js: '#ffa657',
string: '#a5d6ff',
keyword: '#ff7b72',
function: '#d2a8ff',
number: '#79c0ff',
comment: '#8b949e',
}
}
}
}
}
</script>
<style>
/* Custom Scrollbar */
::-webkit-scrollbar {
width: 10px;
height: 10px;
}
::-webkit-scrollbar-track {
background: #0d1117;
}
::-webkit-scrollbar-thumb {
background: #30363d;
border-radius: 5px;
border: 2px solid #0d1117;
}
::-webkit-scrollbar-thumb:hover {
background: #484f58;
}
/* Editor Styles */
.editor-container {
position: relative;
height: 100%;
overflow: hidden;
}
.code-input {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
padding: 16px 16px 16px 56px;
background: transparent;
color: transparent;
caret-color: #c9d1d9;
font-family: 'Fira Code', monospace;
font-size: 13px;
line-height: 20px;
border: none;
outline: none;
resize: none;
white-space: pre;
overflow: auto;
z-index: 2;
tab-size: 2;
}
.code-highlight {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
padding: 16px 16px 16px 56px;
font-family: 'Fira Code', monospace;
font-size: 13px;
line-height: 20px;
color: #c9d1d9;
white-space: pre;
overflow: hidden;
pointer-events: none;
z-index: 1;
}
.line-numbers {
position: absolute;
top: 0;
left: 0;
width: 48px;
height: 100%;
padding: 16px 8px 16px 0;
background: #0d1117;
border-right: 1px solid #21262d;
font-family: 'Fira Code', monospace;
font-size: 13px;
line-height: 20px;
color: #484f58;
text-align: right;
user-select: none;
z-index: 3;
overflow: hidden;
}
/* Animations */
@keyframes slideUp {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.animate-slide-up {
animation: slideUp 0.6s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
.animate-fade-in {
animation: fadeIn 0.4s ease forwards;
}
/* Glassmorphism */
.glass {
background: rgba(13, 17, 23, 0.85);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
}
/* Syntax Highlighting Colors */
.token-keyword { color: #ff7b72; }
.token-string { color: #a5d6ff; }
.token-number { color: #79c0ff; }
.token-function { color: #d2a8ff; }
.token-comment { color: #8b949e; font-style: italic; }
.token-tag { color: #7ee787; }
.token-attr { color: #79c0ff; }
.token-selector { color: #d2a8ff; }
.token-property { color: #7ee787; }
.token-value { color: #a5d6ff; }
/* Resize Handle */
.resize-handle {
width: 4px;
cursor: col-resize;
background: transparent;
transition: background 0.2s;
}
.resize-handle:hover, .resize-handle.active {
background: #388bfd;
}
/* Console */
.console-item {
border-left: 3px solid transparent;
animation: fadeIn 0.15s ease;
}
.console-log { border-left-color: #58a6ff; }
.console-warn { border-left-color: #d29922; background: rgba(210, 153, 34, 0.05); }
.console-error { border-left-color: #f85149; background: rgba(248, 81, 73, 0.05); }
.console-info { border-left-color: #3fb950; }
/* Tab Active Indicator */
.tab-indicator {
position: absolute;
bottom: 0;
height: 2px;
background: #58a6ff;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
/* Toolbar Button Hover */
.toolbar-btn {
transition: all 0.2s ease;
}
.toolbar-btn:hover {
background: #21262d;
transform: translateY(-1px);
}
.toolbar-btn:active {
transform: translateY(0);
}
/* Preview iframe */
#preview-frame {
background: white;
border: none;
width: 100%;
height: 100%;
}
/* Custom Select */
.custom-select {
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%238b949e' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 8px center;
padding-right: 28px;
}
</style>
</head>
<body class="bg-editor-bg text-gray-300 font-sans h-screen w-screen overflow-hidden selection:bg-blue-500/30">
<!-- Header -->
<header class="h-14 border-b border-editor-border flex items-center justify-between px-4 glass z-50 animate-fade-in">
<div class="flex items-center gap-3">
<div class="relative group">
<div class="w-9 h-9 rounded-lg bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center shadow-lg shadow-blue-500/20 group-hover:shadow-blue-500/40 transition-shadow">
<i data-lucide="code-2" class="w-5 h-5 text-white"></i>
</div>
<div class="absolute -top-1 -right-1 w-3 h-3 bg-green-400 rounded-full border-2 border-editor-bg animate-pulse"></div>
</div>
<div class="flex flex-col">
<h1 class="text-sm font-semibold text-white tracking-tight">LiveCode Studio</h1>
<span class="text-[10px] text-gray-500 font-mono">v2.0.0 &bull; Interactive Preview</span>
</div>
</div>
<!-- Center: Action Buttons -->
<div class="flex items-center gap-1.5">
<button onclick="app.runCode()" class="toolbar-btn flex items-center gap-2 px-4 py-1.5 bg-green-600/20 text-green-400 rounded-md text-xs font-medium border border-green-600/30 hover:bg-green-600/30 hover:border-green-500/50">
<i data-lucide="play" class="w-3.5 h-3.5"></i>
Run
</button>
<button onclick="app.toggleConsole()" class="toolbar-btn flex items-center gap-2 px-3 py-1.5 text-gray-400 rounded-md text-xs font-medium border border-editor-border hover:text-gray-200">
<i data-lucide="terminal" class="w-3.5 h-3.5"></i>
Console
</button>
<button onclick="app.saveCode()" class="toolbar-btn flex items-center gap-2 px-3 py-1.5 text-gray-400 rounded-md text-xs font-medium border border-editor-border hover:text-gray-200">
<i data-lucide="download" class="w-3.5 h-3.5"></i>
Save
</button>
<div class="w-px h-6 bg-editor-border mx-1"></div>
<button onclick="app.resetCode()" class="toolbar-btn p-1.5 text-gray-500 rounded-md hover:text-red-400" title="Reset All">
<i data-lucide="rotate-ccw" class="w-3.5 h-3.5"></i>
</button>
<button onclick="app.toggleSettings()" class="toolbar-btn p-1.5 text-gray-500 rounded-md hover:text-gray-200" title="Settings">
<i data-lucide="settings" class="w-3.5 h-3.5"></i>
</button>
</div>
<!-- Right: Credits -->
<div class="flex items-center gap-3">
<span class="text-xs text-gray-500">Built with</span>
<a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="text-xs font-mono text-blue-400 hover:text-blue-300 transition-colors border-b border-blue-400/30 hover:border-blue-300">
anycoder
</a>
</div>
</header>
<!-- Settings Panel (Hidden by default) -->
<div id="settings-panel" class="fixed top-14 right-0 w-72 bg-editor-surface border-l border-editor-border shadow-2xl transform translate-x-full transition-transform duration-300 z-40">
<div class="p-4 border-b border-editor-border">
<h3 class="text-sm font-semibold text-white flex items-center gap-2">
<i data-lucide="settings-2" class="w-4 h-4"></i>
Settings
</h3>
</div>
<div class="p-4 space-y-4">
<div>
<label class="text-xs text-gray-500 mb-2 block">Theme</label>
<select id="theme-select" onchange="app.changeTheme()" class="custom-select w-full bg-editor-elevated border border-editor-border rounded-md px-3 py-2 text-xs text-gray-300 focus:outline-none focus:border-blue-500">
<option value="dark">Dark (Default)</option>
<option value="light">Light</option>
<option value="midnight">Midnight</option>
</select>
</div>
<div>
<label class="text-xs text-gray-500 mb-2 block">Font Size</label>
<input type="range" id="font-size" min="11" max="18" value="13" oninput="app.changeFontSize()" class="w-full accent-blue-500">
<div class="flex justify-between text-[10px] text-gray-600 mt-1">
<span>11px</span>
<span id="font-size-value">13px</span>
<span>18px</span>
</div>
</div>
<div>
<label class="text-xs text-gray-500 mb-2 block">Auto-run</label>
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" id="auto-run" checked class="accent-blue-500 w-4 h-4">
<span class="text-xs text-gray-400">Run on code change</span>
</label>
</div>
<div>
<label class="text-xs text-gray-500 mb-2 block">Layout</label>
<div class="grid grid-cols-2 gap-2">
<button onclick="app.setLayout('horizontal')" class="p-2 rounded-md border border-editor-border hover:border-blue-500/50 text-xs text-center">
<i data-lucide="layout" class="w-4 h-4 mx-auto mb-1"></i>
Split
</button>
<button onclick="app.setLayout('vertical')" class="p-2 rounded-md border border-editor-border hover:border-blue-500/50 text-xs text-center">
<i data-lucide="layout-template" class="w-4 h-4 mx-auto mb-1"></i>
Stack
</button>
</div>
</div>
</div>
</div>
<!-- Main Content -->
<div id="main-layout" class="flex h-[calc(100vh-56px)]">
<!-- Left Panel: Editor -->
<div id="editor-panel" class="flex-1 flex flex-col min-w-0">
<!-- Editor Tabs -->
<div class="flex items-center bg-editor-surface border-b border-editor-border relative">
<div class="flex" id="editor-tabs">
<button onclick="app.switchTab('html')" class="editor-tab relative px-4 py-2.5 text-xs font-medium transition-colors flex items-center gap-2 border-r border-editor-border" data-tab="html">
<i data-lucide="file-code" class="w-3.5 h-3.5 text-orange-400"></i>
<span>index.html</span>
</button>
<button onclick="app.switchTab('css')" class="editor-tab relative px-4 py-2.5 text-xs font-medium transition-colors flex items-center gap-2 border-r border-editor-border text-gray-500 hover:text-gray-300" data-tab="css">
<i data-lucide="palette" class="w-3.5 h-3.5 text-blue-400"></i>
<span>style.css</span>
</button>
<button onclick="app.switchTab('js')" class="editor-tab relative px-4 py-2.5 text-xs font-medium transition-colors flex items-center gap-2 border-r border-editor-border text-gray-500 hover:text-gray-300" data-tab="js">
<i data-lucide="file-json" class="w-3.5 h-3.5 text-yellow-400"></i>
<span>script.js</span>
</button>
</div>
<div class="tab-indicator" id="tab-indicator"></div>
<div class="ml-auto flex items-center gap-1 px-2">
<button onclick="app.addFile()" class="p-1.5 rounded hover:bg-editor-hover text-gray-500 hover:text-gray-300" title="New File">
<i data-lucide="plus" class="w-3.5 h-3.5"></i>
</button>
</div>
</div>
<!-- Code Editor -->
<div class="flex-1 relative">
<div id="html-editor" class="editor-container">
<div class="line-numbers" id="html-lines">1</div>
<pre class="code-highlight" id="html-highlight"></pre>
<textarea class="code-input" id="html-input" spellcheck="false" placeholder="<!-- Write your HTML here -->"></textarea>
</div>
<div id="css-editor" class="editor-container hidden">
<div class="line-numbers" id="css-lines">1</div>
<pre class="code-highlight" id="css-highlight"></pre>
<textarea class="code-input" id="css-input" spellcheck="false" placeholder="/* Write your CSS here */"></textarea>
</div>
<div id="js-editor" class="editor-container hidden">
<div class="line-numbers" id="js-lines">1</div>
<pre class="code-highlight" id="js-highlight"></pre>
<textarea class="code-input" id="js-input" spellcheck="false" placeholder="// Write your JavaScript here"></textarea>
</div>
</div>
</div>
<!-- Resize Handle -->
<div class="resize-handle" id="resize-handle"></div>
<!-- Right Panel: Preview -->
<div id="preview-panel" class="flex-1 flex flex-col min-w-0 bg-white">
<!-- Preview Toolbar -->
<div class="h-9 bg-gray-50 border-b border-gray-200 flex items-center justify-between px-3">
<div class="flex items-center gap-2">
<div class="flex gap-1.5">
<div class="w-2.5 h-2.5 rounded-full bg-red-400"></div>
<div class="w-2.5 h-2.5 rounded-full bg-yellow-400"></div>
<div class="w-2.5 h-2.5 rounded-full bg-green-400"></div>
</div>
<div class="ml-3 bg-white rounded px-2 py-0.5 text-[10px] text-gray-400 border border-gray-200 flex items-center gap-1.5 w-48">
<i data-lucide="lock" class="w-3 h-3"></i>
<span class="truncate">localhost:3000/preview</span>
</div>
</div>
<div class="flex items-center gap-1">
<button onclick="app.refreshPreview()" class="p-1 rounded hover:bg-gray-200 text-gray-400 hover:text-gray-600" title="Refresh">
<i data-lucide="refresh-cw" class="w-3.5 h-3.5"></i>
</button>
<button onclick="app.openFullscreen()" class="p-1 rounded hover:bg-gray-200 text-gray-400 hover:text-gray-600" title="Fullscreen">
<i data-lucide="maximize" class="w-3.5 h-3.5"></i>
</button>
</div>
</div>
<!-- Preview Frame -->
<div class="flex-1 relative">
<iframe id="preview-frame" sandbox="allow-scripts allow-same-origin allow-modals allow-forms"></iframe>
<!-- Preview Overlay (Loading State) -->
<div id="preview-loading" class="absolute inset-0 bg-white flex items-center justify-center hidden">
<div class="flex flex-col items-center gap-3">
<div class="w-8 h-8 border-2 border-blue-500 border-t-transparent rounded-full animate-spin"></div>
<span class="text-xs text-gray-400">Loading preview...</span>
</div>
</div>
</div>
</div>
</div>
<!-- Console Panel -->
<div id="console-panel" class="h-0 border-t border-editor-border bg-editor-bg transition-all duration-300 overflow-hidden">
<div class="flex items-center justify-between px-4 py-2 border-b border-editor-border bg-editor-surface">
<div class="flex items-center gap-2">
<i data-lucide="terminal" class="w-3.5 h-3.5 text-gray-500"></i>
<span class="text-xs font-medium text-gray-400">Console</span>
<span id="console-count" class="text-[10px] bg-editor-elevated px-1.5 py-0.5 rounded text-gray-500">0</span>
</div>
<div class="flex items-center gap-2">
<button onclick="app.clearConsole()" class="text-[10px] text-gray-500 hover:text-gray-300 flex items-center gap-1">
<i data-lucide="trash-2" class="w-3 h-3"></i>
Clear
</button>
<button onclick="app.toggleConsole()" class="text-gray-500 hover:text-gray-300">
<i data-lucide="x" class="w-4 h-4"></i>
</button>
</div>
</div>
<div id="console-content" class="h-32 overflow-y-auto p-2 font-mono text-xs">
<div class="text-gray-600 italic p-2">Console is ready. Run your code to see output.</div>
</div>
</div>
<!-- Toast Notification -->
<div id="toast" class="fixed bottom-6 left-1/2 -translate-x-1/2 bg-editor-surface border border-editor-border text-white px-4 py-2.5 rounded-lg shadow-2xl flex items-center gap-2 transform translate-y-20 opacity-0 transition-all duration-300 z-50">
<i data-lucide="check-circle" class="w-4 h-4 text-green-400"></i>
<span id="toast-message" class="text-xs">Code saved successfully!</span>
</div>
<script>
// Initialize Lucide icons
lucide.createIcons();
// Default code examples
const DEFAULT_HTML = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Awesome App</title>
</head>
<body>
<div class="container">
<header>
<h1>Welcome to LiveCode Studio</h1>
<p class="subtitle">Edit the code and see changes instantly!</p>
</header>
<main>
<div class="card">
<div class="card-icon">&#x26a1;</div>
<h2>Fast Preview</h2>
<p>See your changes in real-time as you type.</p>
</div>
<div class="card">
<div class="card-icon">&#x1f3a8;</div>
<h2>Beautiful UI</h2>
<p>Modern interface with syntax highlighting.</p>
</div>
<div class="card">
<div class="card-icon">&#x1f527;</div>
<h2>Interactive</h2>
<p>Full JavaScript support with console output.</p>
</div>
</main>
<footer>
<p>Click the Run button or enable auto-run to see changes</p>
<button id="action-btn" class="btn-primary">Click Me!</button>
</footer>
</div>
</body>
</html>`;
const DEFAULT_CSS = `/* Modern styling with CSS variables */
:root {
--primary: #6366f1;
--primary-dark: #4f46e5;
--bg: #f8fafc;
--card-bg: #ffffff;
--text: #1e293b;
--text-light: #64748b;
--border: #e2e8f0;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
background: var(--bg);
color: var(--text);
line-height: 1.6;
min-height: 100vh;
}
.container {
max-width: 800px;
margin: 0 auto;
padding: 40px 20px;
}
header {
text-align: center;
margin-bottom: 48px;
}
h1 {
font-size: 2.5rem;
font-weight: 700;
background: linear-gradient(135deg, var(--primary), #a855f7);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
margin-bottom: 12px;
}
.subtitle {
color: var(--text-light);
font-size: 1.1rem;
}
main {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 24px;
margin-bottom: 48px;
}
.card {
background: var(--card-bg);
border-radius: 16px;
padding: 28px;
border: 1px solid var(--border);
transition: all 0.3s ease;
cursor: pointer;
}
.card:hover {
transform: translateY(-4px);
box-shadow: 0 20px 40px -12px rgba(0, 0, 0, 0.1);
border-color: var(--primary);
}
.card-icon {
font-size: 2rem;
margin-bottom: 16px;
}
.card h2 {
font-size: 1.1rem;
font-weight: 600;
margin-bottom: 8px;
}
.card p {
color: var(--text-light);
font-size: 0.9rem;
}
footer {
text-align: center;
padding-top: 24px;
border-top: 1px solid var(--border);
}
footer p {
color: var(--text-light);
margin-bottom: 20px;
}
.btn-primary {
background: linear-gradient(135deg, var(--primary), var(--primary-dark));
color: white;
border: none;
padding: 12px 32px;
border-radius: 10px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4px 14px rgba(99, 102, 241, 0.3);
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(99, 102, 241, 0.4);
}
.btn-primary:active {
transform: translateY(0);
}`;
const DEFAULT_JS = `// Interactive JavaScript
const btn = document.getElementById('action-btn');
let clickCount = 0;
btn.addEventListener('click', () => {
clickCount++;
// Update button text
btn.textContent = \`Clicked \${clickCount} time\${clickCount > 1 ? 's' : ''}!\`;
// Random color effect
const hues = [260, 280, 300, 320, 340, 200, 220];
const hue = hues[Math.floor(Math.random() * hues.length)];
btn.style.background = \`linear-gradient(135deg, hsl(\${hue}, 70%, 60%), hsl(\${hue}, 70%, 50%))\`;
// Add a ripple effect
const ripple = document.createElement('span');
ripple.style.cssText = \`
position: absolute;
border-radius: 50%;
background: rgba(255,255,255,0.4);
transform: scale(0);
animation: ripple 0.6s linear;
pointer-events: none;
\`;
// Log to console
console.log(\`Button clicked! Count: \${clickCount}\`);
// Animate cards on click
const cards = document.querySelectorAll('.card');
cards.forEach((card, index) => {
setTimeout(() => {
card.style.transform = 'scale(1.02)';
setTimeout(() => {
card.style.transform = '';
}, 200);
}, index * 50);
});
});
// Add ripple animation
const style = document.createElement('style');
style.textContent = \`
@keyframes ripple {
to {
transform: scale(4);
opacity: 0;
}
}
\`;
document.head.appendChild(style);
// Log initialization
console.log('✨ Application initialized!');
console.log('Try clicking the button and editing the code!');
// Example: Dynamic greeting based on time
const hour = new Date().getHours();
let greeting = 'Good evening';
if (hour < 12) greeting = 'Good morning';
else if (hour < 18) greeting = 'Good afternoon';
console.log(\`👋 \${greeting}! Welcome to LiveCode Studio.\`);`;
// Application
const app = {
currentTab: 'html',
consoleOpen: false,
settingsOpen: false,
autoRun: true,
consoleLogs: [],
init() {
// Set default code
document.getElementById('html-input').value = DEFAULT_HTML;
document.getElementById('css-input').value = DEFAULT_CSS;
document.getElementById('js-input').value = DEFAULT_JS;
// Setup event listeners
this.setupEditors();
this.setupResize();
this.updateLineNumbers();
this.highlightAll();
this.runCode();
// Initial tab indicator position
this.updateTabIndicator();
// Setup auto-run
document.getElementById('auto-run').addEventListener('change', (e) => {
this.autoRun = e.target.checked;
});
},
setupEditors() {
['html', 'css', 'js'].forEach(lang => {
const input = document.getElementById(`${lang}-input`);
const highlight = document.getElementById(`${lang}-highlight`);
input.addEventListener('input', () => {
this.updateLineNumbers(lang);
this.highlightCode(lang);
if (this.autoRun) {
this.debounceRun();
}
});
input.addEventListener('scroll', () => {
highlight.scrollTop = input.scrollTop;
highlight.scrollLeft = input.scrollLeft;
document.getElementById(`${lang}-lines`).scrollTop = input.scrollTop;
});
input.addEventListener('keydown', (e) => {
if (e.key === 'Tab') {
e.preventDefault();
const start = input.selectionStart;
const end = input.selectionEnd;
input.value = input.value.substring(0, start) + ' ' + input.value.substring(end);
input.selectionStart = input.selectionEnd = start + 2;
this.updateLineNumbers(lang);
this.highlightCode(lang);
}
// Auto-indent on Enter
if (e.key === 'Enter') {
e.preventDefault();
const start = input.selectionStart;
const end = input.selectionEnd;
const value = input.value;
const lineStart = value.lastIndexOf('\n', start - 1) + 1;
const currentLine = value.substring(lineStart, start);
const indent = currentLine.match(/^\s*/)[0];
// Check if we need extra indent
const lastChar = value[start - 1];
const extraIndent = (lastChar === '{' || lastChar === '(' || lastChar === '[') ? ' ' : '';
const insert = '\n' + indent + extraIndent;
input.value = value.substring(0, start) + insert + value.substring(end);
input.selectionStart = input.selectionEnd = start + insert.length;
this.updateLineNumbers(lang);
this.highlightCode(lang);
}
});
});
},
debounceRun: (() => {
let timeout;
return function() {
clearTimeout(timeout);
timeout = setTimeout(() => this.runCode(), 800);
};
})(),
updateLineNumbers(lang) {
const langs = lang ? [lang] : ['html', 'css', 'js'];
langs.forEach(l => {
const input = document.getElementById(`${l}-input`);
const lines = input.value.split('\n').length;
const lineNumbers = document.getElementById(`${l}-lines`);
lineNumbers.innerHTML = Array.from({length: lines}, (_, i) => i + 1).join('<br>');
});
},
highlightAll() {
['html', 'css', 'js'].forEach(lang => this.highlightCode(lang));
},
highlightCode(lang) {
const input = document.getElementById(`${lang}-input`);
const highlight = document.getElementById(`${lang}-highlight`);
let code = input.value;
// Escape HTML
code = code.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
if (lang === 'html') {
// HTML tags
code = code.replace(/(&lt;\/?)(\w+)(.*?)(&gt;)/g, '<span class="token-tag">$1$2</span>$3<span class="token-tag">$4</span>');
// Attributes
code = code.replace(/(\s)(\w+)(=)/g, '$1<span class="token-attr">$2</span>$3');
// Strings in attributes
code = code.replace(/(".*?")/g, '<span class="token-string">$1</span>');
// Comments
code = code.replace(/(&lt;!--.*?--&gt;)/g, '<span class="token-comment">$1</span>');
} else if (lang === 'css') {
// Comments
code = code.replace(/(\/\*.*?\*\/)/g, '<span class="token-comment">$1</span>');
// Selectors
code = code.replace(/^([^\s{][^{]*?)(?=\s*\{)/gm, '<span class="token-selector">$1</span>');
// Properties
code = code.replace(/([\w-]+)(?=\s*:)/g, '<span class="token-property">$1</span>');
// Values
code = code.replace(/:\s*([^;{}]+)/g, ': <span class="token-value">$1</span>');
// Numbers
code = code.replace(/(\d+\.?\d*(?:px|em|rem|%|vh|vw|s|ms)?)/g, '<span class="token-number">$1</span>');
} else if (lang === 'js') {
// Comments
code = code.replace(/(\/\/.*$)/gm, '<span class="token-comment">$1</span>');
code = code.replace(/(\/\*[\s\S]*?\*\/)/g, '<span class="token-comment">$1</span>');
// Strings
code = code.replace(/(".*?"|'.*?'|`.*?`)/g, '<span class="token-string">$1</span>');
// Keywords
const keywords = ['const', 'let', 'var', 'function', 'return', 'if', 'else', 'for', 'while', 'class', 'import', 'export', 'from', 'async', 'await', 'try', 'catch', 'new', 'this', 'true', 'false', 'null', 'undefined'];
keywords.forEach(kw => {
const regex = new RegExp(`\\b(${kw})\\b`, 'g');
code = code.replace(regex, '<span class="token-keyword">$1</span>');
});
// Functions
code = code.replace(/(\w+)(?=\s*\()/g, '<span class="token-function">$1</span>');
// Numbers
code = code.replace(/\b(\d+\.?\d*)\b/g, '<span class="token-number">$1</span>');
}
highlight.innerHTML = code;
},
switchTab(tab) {
this.currentTab = tab;
// Update tab styles
document.querySelectorAll('.editor-tab').forEach(t => {
if (t.dataset.tab === tab) {
t.classList.remove('text-gray-500', 'hover:text-gray-300');
t.classList.add('text-white', 'bg-editor-bg');
} else {
t.classList.add('text-gray-500', 'hover:text-gray-300');
t.classList.remove('text-white', 'bg-editor-bg');
}
});
// Show/hide editors
['html', 'css', 'js'].forEach(lang => {
const editor = document.getElementById(`${lang}-editor`);
if (lang === tab) {
editor.classList.remove('hidden');
} else {
editor.classList.add('hidden');
}
});
this.updateTabIndicator();
},
updateTabIndicator() {
const activeTab = document.querySelector(`.editor-tab[data-tab="${this.currentTab}"]`);
const indicator = document.getElementById('tab-indicator');
if (activeTab && indicator) {
indicator.style.width = `${activeTab.offsetWidth}px`;
indicator.style.left = `${activeTab.offsetLeft}px`;
}
},
runCode() {
const html = document.getElementById('html-input').value;
const css = document.getElementById('css-input').value;
const js = document.getElementById('js-input').value;
const frame = document.getElementById('preview-frame');
const loading = document.getElementById('preview-loading');
loading.classList.remove('hidden');
setTimeout(() => {
const doc = frame.contentDocument || frame.contentWindow.document;
// Inject console capture script
const consoleScript = `
<script>
(function() {
const originalLog = console.log;
const originalWarn = console.warn;
const originalError = console.error;
const originalInfo = console.info;
function sendToParent(type, args) {
const message = args.map(arg => {
if (typeof arg === 'object') {
try {
return JSON.stringify(arg, null, 2);
} catch(e) {
return String(arg);
}
}
return String(arg);
}).join(' ');
window.parent.postMessage({
type: 'console',
level: type,
message: message,
timestamp: new Date().toLocaleTimeString()
}, '*');
}
console.log = function(...args) {
originalLog.apply(console, args);
sendToParent('log', args);
};
console.warn = function(...args) {
originalWarn.apply(console, args);
sendToParent('warn', args);
};
console.error = function(...args) {
originalError.apply(console, args);
sendToParent('error', args);
};
console.info = function(...args) {
originalInfo.apply(console, args);
sendToParent('info', args);
};
window.onerror = function(msg, url, line, col, error) {
window.parent.postMessage({
type: 'console',
level: 'error',
message: msg + ' (line ' + line + ')',
timestamp: new Date().toLocaleTimeString()
}, '*');
return false;
};
})();
<\/script>
`;
const fullHTML = html
.replace('</head>', `<style>${css}</style>${consoleScript}</head>`)
.replace('</body>', `<script>${js}<\/script></body>