Spaces:
Running
Running
| <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 • 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">⚡</div> | |
| <h2>Fast Preview</h2> | |
| <p>See your changes in real-time as you type.</p> | |
| </div> | |
| <div class="card"> | |
| <div class="card-icon">🎨</div> | |
| <h2>Beautiful UI</h2> | |
| <p>Modern interface with syntax highlighting.</p> | |
| </div> | |
| <div class="card"> | |
| <div class="card-icon">🔧</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, '&').replace(/</g, '<').replace(/>/g, '>'); | |
| if (lang === 'html') { | |
| // HTML tags | |
| code = code.replace(/(<\/?)(\w+)(.*?)(>)/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(/(<!--.*?-->)/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> |