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
This commit is contained in:
105
public/js/app.js
105
public/js/app.js
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
initEditors, switchMode, getEditorValues, setEditorValues,
|
||||
setOnChange, getCurrentMode, getCssType, setCssType,
|
||||
setOnChange, setOnTabSwitch, getCurrentMode, getCssType, setCssType,
|
||||
setOnFormat, setOnDiff, setEditorTheme,
|
||||
relayoutEditors,
|
||||
MODE_TABS, MODE_TO_JS_TYPE, JS_TYPE_TO_MODE,
|
||||
} from './editors.js';
|
||||
@@ -15,6 +16,16 @@ import { initKeybindings } from './keybindings.js';
|
||||
import { initResizer, clearInlineSizes } from './resizer.js';
|
||||
import { exportHtml } from './export.js';
|
||||
import { showQrModal } from './qr.js';
|
||||
import { initDevtools } from './devtools.js';
|
||||
import { initNetwork, clearNetwork } from './network-panel.js';
|
||||
import { initElements, clearElements } from './elements-panel.js';
|
||||
import { initPerformance, clearPerformance } from './performance-panel.js';
|
||||
import { formatActiveEditor } from './formatter.js';
|
||||
import { initLinter, lintOnChange } from './linter.js';
|
||||
import { toggleDiff, snapshotValues, onTabSwitch as diffOnTabSwitch } from './diff-view.js';
|
||||
import { registerCustomThemes, THEMES } from './editor-themes.js';
|
||||
import { GALLERY_TEMPLATES } from './templates.js';
|
||||
import { configureTypeDefaults, registerSnippetProviders } from './autocomplete.js';
|
||||
|
||||
let currentId = null;
|
||||
let debounceTimer = null;
|
||||
@@ -73,6 +84,9 @@ async function run() {
|
||||
const compiledCss = await compileCss(css, cssType);
|
||||
const result = await compileJs(js, mode);
|
||||
clearConsole();
|
||||
clearNetwork();
|
||||
clearElements();
|
||||
clearPerformance();
|
||||
|
||||
// Show warnings from compilation (e.g., Svelte)
|
||||
if (result.warnings && result.warnings.length) {
|
||||
@@ -121,10 +135,12 @@ async function save() {
|
||||
try {
|
||||
if (currentId) {
|
||||
await updateFiddle(currentId, { title, html, css, css_type, js, js_type, listed, options, tags });
|
||||
snapshotValues();
|
||||
showToast(`Saved! Share: ${location.origin}/f/${currentId}`);
|
||||
} else {
|
||||
const result = await createFiddle({ title, html, css, css_type, js, js_type, listed, options, tags });
|
||||
currentId = result.id;
|
||||
snapshotValues();
|
||||
history.pushState(null, '', `/f/${currentId}`);
|
||||
showToast(`Saved! Share: ${location.origin}/f/${currentId}`);
|
||||
}
|
||||
@@ -181,6 +197,7 @@ async function loadFromUrl() {
|
||||
renderResourceList();
|
||||
|
||||
setEditorValues(fiddle);
|
||||
snapshotValues();
|
||||
setTimeout(run, 100);
|
||||
} catch (e) {
|
||||
showToast(`Failed to load fiddle: ${e.message}`);
|
||||
@@ -225,11 +242,26 @@ async function init() {
|
||||
// Load Emmet before editors so completion providers are registered
|
||||
await initEmmet();
|
||||
|
||||
// Register custom Monaco themes before creating editors
|
||||
registerCustomThemes();
|
||||
|
||||
// Configure autocomplete: type defaults + snippet providers
|
||||
configureTypeDefaults();
|
||||
registerSnippetProviders();
|
||||
|
||||
initEditors('html-css-js');
|
||||
setOnChange(scheduleRun);
|
||||
setOnChange(() => { scheduleRun(); lintOnChange(); });
|
||||
setOnFormat(() => formatActiveEditor());
|
||||
setOnDiff(() => toggleDiff());
|
||||
setOnTabSwitch(diffOnTabSwitch);
|
||||
initConsole();
|
||||
initDevtools();
|
||||
initNetwork();
|
||||
initElements();
|
||||
initPerformance();
|
||||
initResizer();
|
||||
initKeybindings();
|
||||
initLinter();
|
||||
|
||||
// Auto-run checkbox
|
||||
const autoRunCb = $('#auto-run-checkbox');
|
||||
@@ -249,6 +281,65 @@ async function init() {
|
||||
if (savedLayout !== 'default') applyLayout(savedLayout);
|
||||
layoutSel.addEventListener('change', (e) => applyLayout(e.target.value));
|
||||
|
||||
// Editor theme selector
|
||||
const themeSel2 = $('#editor-theme');
|
||||
const savedEditorTheme = getPref('editorTheme') || 'vs-dark';
|
||||
themeSel2.value = savedEditorTheme;
|
||||
themeSel2.addEventListener('change', (e) => {
|
||||
setEditorTheme(e.target.value);
|
||||
setPref('editorTheme', e.target.value);
|
||||
});
|
||||
|
||||
// Device preview toggles
|
||||
const viewport = $('#preview-viewport');
|
||||
const savedDevice = getPref('previewDevice') || 'desktop';
|
||||
if (savedDevice !== 'desktop') {
|
||||
viewport.classList.add(`device-${savedDevice}`);
|
||||
document.querySelectorAll('.device-btn').forEach(b => {
|
||||
b.classList.toggle('active', b.dataset.device === savedDevice);
|
||||
});
|
||||
}
|
||||
document.querySelectorAll('.device-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const device = btn.dataset.device;
|
||||
viewport.classList.remove('device-tablet', 'device-mobile');
|
||||
if (device !== 'desktop') viewport.classList.add(`device-${device}`);
|
||||
document.querySelectorAll('.device-btn').forEach(b => b.classList.toggle('active', b === btn));
|
||||
setPref('previewDevice', device);
|
||||
});
|
||||
});
|
||||
|
||||
// Templates gallery
|
||||
const tplModal = $('#templates-modal');
|
||||
const tplGrid = $('#templates-grid');
|
||||
$('#btn-templates').addEventListener('click', () => tplModal.classList.remove('hidden'));
|
||||
$('#templates-modal-close').addEventListener('click', () => tplModal.classList.add('hidden'));
|
||||
tplModal.addEventListener('click', (e) => { if (e.target === tplModal) tplModal.classList.add('hidden'); });
|
||||
|
||||
// Render template cards
|
||||
tplGrid.innerHTML = '';
|
||||
for (const tpl of GALLERY_TEMPLATES) {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'template-card';
|
||||
card.innerHTML = `
|
||||
<div class="template-card-icon">${tpl.icon}</div>
|
||||
<div class="template-card-title">${tpl.title}</div>
|
||||
<div class="template-card-desc">${tpl.description}</div>
|
||||
<div class="template-card-mode">${tpl.mode}</div>
|
||||
`;
|
||||
card.addEventListener('click', () => {
|
||||
// Switch mode
|
||||
$('#framework-mode').value = tpl.mode;
|
||||
handleModeChange(tpl.mode);
|
||||
// Set editor values
|
||||
setEditorValues({ html: tpl.html || '', css: tpl.css || '', js: tpl.js || '' });
|
||||
// Close modal and run
|
||||
tplModal.classList.add('hidden');
|
||||
run();
|
||||
});
|
||||
tplGrid.appendChild(card);
|
||||
}
|
||||
|
||||
// Mode selector
|
||||
$('#framework-mode').addEventListener('change', (e) => {
|
||||
handleModeChange(e.target.value);
|
||||
@@ -273,6 +364,16 @@ async function init() {
|
||||
if (e.key === 'Escape') {
|
||||
document.querySelectorAll('.modal-overlay:not(.hidden)').forEach(m => m.classList.add('hidden'));
|
||||
}
|
||||
// Shift+Alt+F — format code
|
||||
if (e.key === 'F' && e.shiftKey && e.altKey) {
|
||||
e.preventDefault();
|
||||
formatActiveEditor();
|
||||
}
|
||||
// Ctrl/Cmd+D — toggle diff
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 'd') {
|
||||
e.preventDefault();
|
||||
toggleDiff();
|
||||
}
|
||||
// ? key opens shortcuts (only when not typing in an input/editor)
|
||||
if (e.key === '?' && !e.ctrlKey && !e.metaKey) {
|
||||
const tag = document.activeElement?.tagName;
|
||||
|
||||
Reference in New Issue
Block a user