Add Python REPL, instant deploy, Gist import, presentation mode, and CSS visual tools

- Python mode via Pyodide WASM runtime with stdout/stderr console integration
- Publish fiddles to clean /p/:slug URLs as standalone HTML pages
- Import code from GitHub Gist URLs with auto-detection of language/mode
- Presentation mode with slide management, fullscreen viewer, and keyboard nav
- Enable Monaco color decorators for inline CSS color pickers
- Extract reusable generateStandaloneHtml from export module
This commit is contained in:
root
2026-02-27 15:50:55 -06:00
parent 26e232fd41
commit ae8dbafb20
11 changed files with 666 additions and 6 deletions

View File

@@ -13,12 +13,14 @@ import {
createFiddle, loadFiddle, updateFiddle, listTags,
listVersions, getVersion, revertVersion,
listCollections, createCollection, addToCollection,
publishFiddle, unpublishFiddle, getPublishStatus,
importGist,
} from './api.js';
import { getPref, setPref } from './preferences.js';
import { initEmmet } from './emmet.js';
import { initKeybindings } from './keybindings.js';
import { initResizer, clearInlineSizes } from './resizer.js';
import { exportHtml } from './export.js';
import { exportHtml, generateStandaloneHtml } from './export.js';
import { showQrModal } from './qr.js';
import { initDevtools } from './devtools.js';
import { initNetwork, clearNetwork } from './network-panel.js';
@@ -31,6 +33,7 @@ import { registerCustomThemes, THEMES } from './editor-themes.js';
import { GALLERY_TEMPLATES } from './templates.js';
import { configureTypeDefaults, registerSnippetProviders } from './autocomplete.js';
import { initNpmSearch } from './npm-search.js';
import { openSlideManager, addCurrentSlide, startPresentation, stopPresentation } from './presentation.js';
let currentId = null;
let debounceTimer = null;
@@ -68,6 +71,10 @@ const STARTER_TEMPLATES = {
js: `# Hello Markdown\n\nThis is a **Markdown** fiddle. Write your content here and see it rendered in the preview.\n\n## Features\n\n- Headers, **bold**, *italic*\n- Lists (ordered and unordered)\n- Code blocks with syntax highlighting\n- Links, images, and more\n\n### Code Example\n\n\`\`\`javascript\nconst greeting = "Hello, World!";\nconsole.log(greeting);\n\`\`\`\n\n> Blockquotes work too!\n\n| Column 1 | Column 2 |\n|----------|----------|\n| Cell A | Cell B |`,
css: `body {\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;\n max-width: 720px;\n margin: 0 auto;\n padding: 24px;\n line-height: 1.6;\n color: #1a1a1a;\n}\n\nh1, h2, h3 { margin-top: 1.5em; margin-bottom: 0.5em; }\nh1 { border-bottom: 2px solid #eee; padding-bottom: 0.3em; }\n\ncode {\n background: #f4f4f4;\n padding: 2px 6px;\n border-radius: 3px;\n font-size: 0.9em;\n}\n\npre {\n background: #f4f4f4;\n padding: 16px;\n border-radius: 6px;\n overflow-x: auto;\n}\n\npre code { background: none; padding: 0; }\n\nblockquote {\n border-left: 4px solid #ddd;\n margin: 1em 0;\n padding: 0.5em 1em;\n color: #555;\n}\n\ntable {\n border-collapse: collapse;\n width: 100%;\n margin: 1em 0;\n}\n\nth, td {\n border: 1px solid #ddd;\n padding: 8px 12px;\n text-align: left;\n}\n\nth { background: #f4f4f4; font-weight: 600; }`,
},
'python': {
js: `print("Hello from Python!")\n\nimport math\nprint(f"Pi is {math.pi:.4f}")\n\n# Use the sys module\nimport sys\nprint(f"Python {sys.version}")\n\n# List comprehension\nsquares = [x**2 for x in range(10)]\nprint(f"Squares: {squares}")`,
html: '',
},
'wasm': {
html: '<h1>WebAssembly Demo</h1>\n<div id="output"></div>',
css: `body {\n font-family: monospace;\n padding: 24px;\n background: #1a1a2e;\n color: #0f0;\n}\n\nh1 { color: #00d4ff; margin-bottom: 16px; }\n#output { white-space: pre; font-size: 14px; line-height: 1.8; }`,
@@ -494,6 +501,63 @@ async function init() {
}
});
// Publish button
$('#btn-publish').addEventListener('click', async () => {
if (!currentId) {
showToast('Save the fiddle first before publishing');
return;
}
const mode = getCurrentMode();
const { html, css, js } = getEditorValues();
const cssType = getCssType();
const title = $('#title-input').value || 'Untitled';
try {
const compiledCss = await compileCss(css, cssType);
const result = await compileJs(js, mode);
const standaloneHtml = generateStandaloneHtml({
title, html, css: compiledCss, js: result.js, mode,
extraCss: result.extraCss,
tailwind: getTailwindChecked(),
isModule: result.isModule || false,
renderedHtml: result.renderedHtml || null,
previewTheme: getPref('previewTheme'),
resources: currentResources,
});
const pubResult = await publishFiddle(currentId, standaloneHtml);
const fullUrl = `${location.origin}${pubResult.url}`;
try { await navigator.clipboard.writeText(fullUrl); } catch (_) {}
showToast(`Published! ${fullUrl} (copied)`);
} catch (e) {
showToast(`Publish failed: ${e.message}`);
}
});
// Import Gist button
$('#btn-import-gist').addEventListener('click', async () => {
const gistUrl = prompt('Paste a GitHub Gist URL:');
if (!gistUrl || !gistUrl.trim()) return;
try {
const data = await importGist(gistUrl.trim());
// Switch mode
if (data.mode) {
$('#framework-mode').value = data.mode;
handleModeChange(data.mode);
}
// Set CSS type
if (data.css_type && data.css_type !== 'css') {
setCssType(data.css_type);
}
// Set editor values
setEditorValues({ html: data.html || '', css: data.css || '', js: data.js || '' });
// Set title
if (data.title) $('#title-input').value = data.title;
run();
showToast('Gist imported successfully');
} catch (e) {
showToast(`Import failed: ${e.message}`);
}
});
$('#btn-qr').addEventListener('click', () => {
const url = currentId ? `${location.origin}/f/${currentId}` : location.href;
showQrModal(url);
@@ -531,6 +595,21 @@ async function init() {
openCollectionModal(); // refresh list
});
// Presentation mode
const presModal = $('#presentation-modal');
$('#btn-presentation').addEventListener('click', () => openSlideManager(currentId));
$('#presentation-modal-close').addEventListener('click', () => presModal.classList.add('hidden'));
presModal.addEventListener('click', (e) => { if (e.target === presModal) presModal.classList.add('hidden'); });
$('#btn-add-slide').addEventListener('click', () => addCurrentSlide(currentId));
$('#btn-start-presentation').addEventListener('click', () => startPresentation(currentId));
$('#pres-exit').addEventListener('click', () => stopPresentation());
$('#pres-prev').addEventListener('click', () => {
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowLeft' }));
});
$('#pres-next').addEventListener('click', () => {
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight' }));
});
// npm search in resources modal
initNpmSearch((pkg) => {
currentResources.push({ type: pkg.type, url: pkg.url });