WebPile Pro Enterprise

Multi-file Monaco studio → isolated preview, console capture, validation, version history, ZIP export.

Project: —
Auto
Safe Full
Editor Files · Ctrl/+Enter render · Ctrl/+S snapshot
Loading editor…
Enterprise notes: this tool prefers local vendor assets for Monaco/JSZip, but can fall back to CDN. In locked networks, export a pack and vendorize dependencies.
Monaco couldn’t load (usually a network block). You’re in fallback mode (plain textarea). Preview, export, history, validation still work.
Ready
0 chars · 0 lines Session:
Preview Safe Mode (scripts blocked) · isolated sandbox
Isolated
Capture
Safe is for layout/CSS. Full runs scripts. “Isolated” keeps preview from reading your tool storage (recommended). Export ZIP for client handoff.
Single-file studio · enterprise defaults: isolated preview + network block + version snapshots
`, 'styles.css': `:root{ color-scheme: dark; } body{ margin:0; padding:32px; font-family:system-ui, -apple-system, Segoe UI, Roboto, Arial; background:#0b0b12; color:#eef; } .card{ max-width:900px; margin:0 auto; padding:24px; border-radius:18px; background:rgba(255,255,255,.06); border:1px solid rgba(255,255,255,.12) } h1{ margin:0 0 10px; letter-spacing:.06em; text-transform:uppercase } p{ opacity:.85; line-height:1.5 } button{ margin-top:14px; padding:10px 14px; border-radius:999px; border:1px solid rgba(255,255,255,.18); background:#7c3cff; color:white; font-weight:800; cursor:pointer }`, 'app.js': `document.getElementById('btn')?.addEventListener('click', () => { console.log('Hello from app.js ✅', { at: new Date().toISOString() }); });` }; const defaultProject = (name='New Project') => ({ id: uid(), name: sanitizeName(name), createdAt: now(), updatedAt: now(), files: [ { path: 'index.html', language: 'html', content: TEMPLATES['index.html'] }, { path: 'styles.css', language: 'css', content: TEMPLATES['styles.css'] }, { path: 'app.js', language: 'javascript', content: TEMPLATES['app.js'] }, ], settings: { auto: true, fullMode: false, isolated: true, instrument: true, network: 'block', debounce: 350, device: 'responsive', scale: '1', } }); const inferLang = (path) => { const p = String(path).toLowerCase(); if (p.endsWith('.html') || p.endsWith('.htm')) return 'html'; if (p.endsWith('.css')) return 'css'; if (p.endsWith('.js') || p.endsWith('.mjs') || p.endsWith('.cjs')) return 'javascript'; if (p.endsWith('.json')) return 'json'; if (p.endsWith('.md')) return 'markdown'; return 'plaintext'; }; // ---------- Runtime state ---------- const state = { usingCdn: false, monacoBase: null, project: null, activeFile: 'index.html', dirtyFiles: new Set(), editor: null, modelByPath: new Map(), fallbackMode: false, renderTimer: null, lastRenderHash: '', sessionToken: '', console: [], lastValidateReport: null, snapshots: [], projects: [] }; // ---------- Migration from older single-file version (best-effort) ---------- const migrateLegacy = async () => { // Legacy keys from your previous file: HTML_PREVIEW_STUDIO__CODE_V2_MONACO + WEBPILE_PRO_FILES_V1 let legacy = null; try { legacy = localStorage.getItem('HTML_PREVIEW_STUDIO__CODE_V2_MONACO'); } catch(_) {} if (legacy && legacy.trim()) { const p = defaultProject('Migrated — Single HTML'); p.files = [ { path:'index.html', language:'html', content: legacy }, { path:'styles.css', language:'css', content: '/* migrated: split your CSS out if you want */\n' }, { path:'app.js', language:'javascript', content: '// migrated: split your JS out if you want\n' } ]; p.updatedAt = now(); await idbPut('projects', p); try { localStorage.removeItem('HTML_PREVIEW_STUDIO__CODE_V2_MONACO'); } catch(_) {} toast('Migrated legacy editor state into a project', 'ok', 1800); return; } // Legacy WebPile list let legacyList = null; try { legacyList = JSON.parse(localStorage.getItem('WEBPILE_PRO_FILES_V1') || 'null'); } catch(_) {} if (Array.isArray(legacyList) && legacyList.length) { for (const entry of legacyList.slice(0, 50)) { const p = defaultProject('Migrated — ' + (entry.name || 'Site')); p.files = [ { path:'index.html', language:'html', content: String(entry.content || '') }, { path:'styles.css', language:'css', content: '/* migrated */\n' }, { path:'app.js', language:'javascript', content: '// migrated\n' } ]; p.updatedAt = now(); await idbPut('projects', p); } try { localStorage.removeItem('WEBPILE_PRO_FILES_V1'); } catch(_) {} toast('Migrated legacy WebPile sites into projects', 'ok', 1800); } }; // ---------- Monaco / editor layer ---------- const createMonacoTheme = (monaco) => { try { monaco.editor.defineTheme('kaixuNebula', { base: 'vs-dark', inherit: true, rules: [ { token: '', foreground: 'EAF0FF' }, { token: 'tag', foreground: '27D6FF' }, { token: 'attribute.name', foreground: 'FFCF5B' }, { token: 'attribute.value', foreground: 'FF2BD6' }, { token: 'string', foreground: 'FF2BD6' }, { token: 'comment', foreground: 'A8B2D6' } ], colors: { 'editor.background': '#07021a', 'editor.lineHighlightBackground': '#0b0320', 'editorCursor.foreground': '#ffcf5b', 'editor.selectionBackground': '#2a145a', 'editor.inactiveSelectionBackground': '#1b0f3a', 'editorIndentGuide.background': '#1b0f3a', 'editorIndentGuide.activeBackground': '#7c3cff', 'editorLineNumber.foreground': '#6f78a2', 'editorLineNumber.activeForeground': '#ffcf5b', 'editorWidget.background': '#0b0320', 'editorSuggestWidget.background': '#0b0320', 'editorSuggestWidget.border': '#1b0f3a', 'editorSuggestWidget.selectedBackground': '#1b0f3a' } }); } catch (_) {} }; const setActiveModel = (path) => { state.activeFile = path; if (state.fallbackMode) { els.fallbackTextarea.value = getFile(path).content || ''; renderFileTabs(); updateStats(); return; } const m = state.modelByPath.get(path); if (!m || !state.editor) return; state.editor.setModel(m); renderFileTabs(); updateStats(); }; const getFile = (path) => { const files = state.project?.files || []; return files.find(f => f.path === path) || files[0] || null; }; const setFileContent = (path, content) => { const f = getFile(path); if (!f) return; f.content = String(content ?? ''); state.project.updatedAt = now(); state.dirtyFiles.add(path); renderFileTabs(); }; const escapeHtml = (s) => String(s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); const renderFileTabs = () => { const files = state.project?.files || []; els.fileTabs.innerHTML = ''; for (const f of files) { const btn = document.createElement('div'); btn.className = 'tab' + (f.path === state.activeFile ? ' active' : '') + (state.dirtyFiles.has(f.path) ? ' dirty' : ''); btn.title = f.path; btn.innerHTML = `${escapeHtml(f.path)}`; btn.addEventListener('click', () => setActiveModel(f.path)); els.fileTabs.appendChild(btn); } }; const updateStats = () => { const v = getActiveContent() || ''; const lines = v.length ? (v.match(/\n/g)?.length ?? 0) + 1 : 0; els.stats.textContent = `${v.length.toLocaleString()} chars · ${lines.toLocaleString()} lines`; }; const getActiveContent = () => { if (!state.project) return ''; if (state.fallbackMode) return els.fallbackTextarea.value || ''; const model = state.modelByPath.get(state.activeFile); return model ? model.getValue() : (getFile(state.activeFile)?.content || ''); }; const applyEditorToProject = () => { if (!state.project) return; if (state.fallbackMode) { setFileContent(state.activeFile, els.fallbackTextarea.value || ''); return; } for (const f of state.project.files) { const m = state.modelByPath.get(f.path); if (m) f.content = m.getValue(); } state.project.updatedAt = now(); }; const persistProject = async (msg='Saved') => { if (!state.project) return; applyEditorToProject(); try { await idbPut('projects', state.project); state.dirtyFiles.clear(); setSave('ok', msg); renderFileTabs(); } catch (e) { setSave('warn', 'Save failed (storage blocked)'); } }; const schedulePersist = () => { clearTimeout(schedulePersist._t); schedulePersist._t = setTimeout(() => persistProject('Autosaved'), 450); }; const scheduleRender = () => { if (!state.project?.settings?.auto) return; const ms = clamp(Number(els.debounceMs.value || 0), 0, 5000); state.project.settings.debounce = ms; clearTimeout(state.renderTimer); state.renderTimer = setTimeout(() => renderNow(false), ms); }; // ---------- Compose / preview ---------- const hashStr = (s) => { let h = 2166136261; for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h += (h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24); } return (h >>> 0).toString(16); }; const applyViewport = () => { const s = state.project?.settings || {}; const vw = s.device === 'responsive' ? null : Number(s.device); const scale = Number(s.scale || '1'); els.viewport.style.width = vw ? `${vw}px` : '100%'; els.viewport.style.transformOrigin = 'top center'; els.viewport.style.transform = scale === 1 ? 'none' : `scale(${scale})`; els.viewport.style.height = '100%'; }; const setSandbox = () => { const s = state.project?.settings || {}; // IMPORTANT: For security, Full mode does NOT automatically enable allow-same-origin. // "Isolated" means: no allow-same-origin. Compatibility means: allow-same-origin (riskier). const allowSameOrigin = !s.isolated; if (s.fullMode) { const parts = [ 'allow-scripts', 'allow-forms', 'allow-modals', 'allow-popups', 'allow-pointer-lock' ]; if (allowSameOrigin) parts.push('allow-same-origin'); els.iframe.setAttribute('sandbox', parts.join(' ')); } else { els.iframe.setAttribute('sandbox', 'allow-forms allow-modals allow-popups'); } }; const buildInstrumentation = (token, networkPolicy) => { // Token gates messages so other frames can't spam our console UI. return `