Add QoL features: preview theme, external resources, shortcuts, mobile layout

- Dark/light preview theme toggle with localStorage persistence and
  dark CSS injection in preview, export, and embed
- External CSS/JS resources modal with per-fiddle persistence in
  options column, injected as link/script tags
- Keyboard shortcuts cheat sheet modal (? button or ? key)
- Mobile-responsive CSS with breakpoints at 768px and 480px
  for both editor and browse pages
This commit is contained in:
root
2026-02-26 15:39:16 -06:00
parent 77f64d2862
commit b18c9c1dc8
8 changed files with 239 additions and 10 deletions

View File

@@ -19,6 +19,7 @@ import { showQrModal } from './qr.js';
let currentId = null;
let debounceTimer = null;
let currentTags = [];
let currentResources = [];
const $ = (sel) => document.querySelector(sel);
@@ -84,12 +85,14 @@ async function run() {
tailwind: getTailwindChecked(),
isModule: result.isModule || false,
renderedHtml: result.renderedHtml || null,
previewTheme: getPref('previewTheme'),
resources: currentResources,
};
renderPreview(html, compiledCss, result.js, mode, result.extraCss || '', options);
} catch (e) {
clearConsole();
renderPreview(html, '', '', mode, '', { tailwind: getTailwindChecked() });
renderPreview(html, '', '', mode, '', { tailwind: getTailwindChecked(), previewTheme: getPref('previewTheme'), resources: currentResources });
window.postMessage({ type: 'console', method: 'error', args: [`Compile error: ${e.message}`] }, '*');
}
}
@@ -114,7 +117,7 @@ async function save() {
const js_type = MODE_TO_JS_TYPE[getCurrentMode()] || 'javascript';
const listed = $('#listed-checkbox').checked ? 1 : 0;
const tags = currentTags.slice();
const options = JSON.stringify({ tailwind: getTailwindChecked() });
const options = JSON.stringify({ tailwind: getTailwindChecked(), resources: currentResources });
try {
if (currentId) {
await updateFiddle(currentId, { title, html, css, css_type, js, js_type, listed, options, tags });
@@ -137,7 +140,7 @@ async function fork() {
const js_type = MODE_TO_JS_TYPE[getCurrentMode()] || 'javascript';
const listed = $('#listed-checkbox').checked ? 1 : 0;
const tags = currentTags.slice();
const options = JSON.stringify({ tailwind: getTailwindChecked() });
const options = JSON.stringify({ tailwind: getTailwindChecked(), resources: currentResources });
try {
const result = await createFiddle({ title, html, css, css_type, js, js_type, listed, options, tags });
currentId = result.id;
@@ -170,10 +173,12 @@ async function loadFromUrl() {
currentTags = (fiddle.tags || []).map(t => t.name);
renderTags();
// Restore options (tailwind checkbox)
// Restore options (tailwind checkbox, resources)
const opts = JSON.parse(fiddle.options || '{}');
const twCb = $('#tailwind-checkbox');
if (twCb) twCb.checked = !!opts.tailwind;
currentResources = opts.resources || [];
renderResourceList();
setEditorValues(fiddle);
setTimeout(run, 100);
@@ -264,6 +269,18 @@ async function init() {
e.preventDefault();
run();
}
// Escape closes any open modal
if (e.key === 'Escape') {
document.querySelectorAll('.modal-overlay:not(.hidden)').forEach(m => m.classList.add('hidden'));
}
// ? key opens shortcuts (only when not typing in an input/editor)
if (e.key === '?' && !e.ctrlKey && !e.metaKey) {
const tag = document.activeElement?.tagName;
if (tag !== 'INPUT' && tag !== 'TEXTAREA' && !document.activeElement?.closest('.editor-area')) {
e.preventDefault();
$('#shortcuts-modal').classList.remove('hidden');
}
}
});
// Tags input
@@ -298,6 +315,8 @@ async function init() {
tailwind: getTailwindChecked(),
isModule: result.isModule || false,
renderedHtml: result.renderedHtml || null,
previewTheme: getPref('previewTheme'),
resources: currentResources,
});
} catch (e) {
showToast(`Export failed: ${e.message}`);
@@ -309,6 +328,40 @@ async function init() {
showQrModal(url);
});
// Preview theme selector
const themeSel = $('#preview-theme');
const savedTheme = getPref('previewTheme');
themeSel.value = savedTheme;
themeSel.addEventListener('change', (e) => {
setPref('previewTheme', e.target.value);
scheduleRun();
});
// Resources modal
const resModal = $('#resources-modal');
$('#btn-resources').addEventListener('click', () => resModal.classList.remove('hidden'));
$('#resources-modal-close').addEventListener('click', () => resModal.classList.add('hidden'));
resModal.addEventListener('click', (e) => { if (e.target === resModal) resModal.classList.add('hidden'); });
$('#btn-add-css').addEventListener('click', () => {
const input = $('#resource-css-input');
const url = input.value.trim();
if (url) { currentResources.push({ type: 'css', url }); input.value = ''; renderResourceList(); scheduleRun(); }
});
$('#btn-add-js').addEventListener('click', () => {
const input = $('#resource-js-input');
const url = input.value.trim();
if (url) { currentResources.push({ type: 'js', url }); input.value = ''; renderResourceList(); scheduleRun(); }
});
$('#resource-css-input').addEventListener('keydown', (e) => { if (e.key === 'Enter') $('#btn-add-css').click(); });
$('#resource-js-input').addEventListener('keydown', (e) => { if (e.key === 'Enter') $('#btn-add-js').click(); });
// Shortcuts modal
const scModal = $('#shortcuts-modal');
$('#btn-shortcuts').addEventListener('click', () => scModal.classList.remove('hidden'));
$('#shortcuts-modal-close').addEventListener('click', () => scModal.classList.add('hidden'));
scModal.addEventListener('click', (e) => { if (e.target === scModal) scModal.classList.add('hidden'); });
// Load fiddle from URL if present
loadFromUrl();
@@ -334,6 +387,22 @@ function renderTags() {
}
}
function renderResourceList() {
const container = $('#resource-list');
container.innerHTML = '';
currentResources.forEach((r, i) => {
const item = document.createElement('div');
item.className = 'resource-item';
item.innerHTML = `<span class="resource-type ${r.type}">${r.type}</span><span class="resource-url" title="${r.url}">${r.url}</span><button class="resource-remove">&times;</button>`;
item.querySelector('.resource-remove').addEventListener('click', () => {
currentResources.splice(i, 1);
renderResourceList();
scheduleRun();
});
container.appendChild(item);
});
}
async function loadTagSuggestions() {
try {
const { tags } = await listTags();

View File

@@ -79,11 +79,25 @@ async function init() {
? `<script src="https://cdn.tailwindcss.com"><\/script>\n`
: '';
// Dark preview theme — from fiddle options or URL param
const previewTheme = params.get('theme') || opts.previewTheme || 'light';
const darkCss = previewTheme === 'dark'
? `<style>body { background: #1e1e1e; color: #ccc; }</style>\n`
: '';
// External resources
let resourceTags = '';
const resources = opts.resources || [];
for (const r of resources) {
if (r.type === 'css') resourceTags += `<link rel="stylesheet" href="${r.url}">\n`;
else if (r.type === 'js') resourceTags += `<script src="${r.url}"><\/script>\n`;
}
const doc = `<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
${tailwindScript}<style>${allCss}</style>
${darkCss}${resourceTags}${tailwindScript}<style>${allCss}</style>
${importMapTag}
</head>
<body>

View File

@@ -4,7 +4,7 @@ import { extractBareImports, buildImportMapTag } from './import-map.js';
/**
* Export a fiddle as a standalone HTML file and trigger download.
*/
export function exportHtml({ title, html, css, js, mode, extraCss = '', isModule = false, tailwind = false, renderedHtml = null }) {
export function exportHtml({ title, html, css, js, mode, extraCss = '', isModule = false, tailwind = false, renderedHtml = null, previewTheme = 'light', resources = [] }) {
const runtime = getFrameworkRuntime(mode);
const allCss = extraCss ? `${css}\n${extraCss}` : css;
@@ -47,13 +47,25 @@ export function exportHtml({ title, html, css, js, mode, extraCss = '', isModule
? `<script src="https://cdn.tailwindcss.com"><\/script>\n`
: '';
const darkCss = previewTheme === 'dark'
? `<style>body { background: #1e1e1e; color: #ccc; }</style>\n`
: '';
let resourceTags = '';
if (resources && resources.length) {
for (const r of resources) {
if (r.type === 'css') resourceTags += `<link rel="stylesheet" href="${r.url}">\n`;
else if (r.type === 'js') resourceTags += `<script src="${r.url}"><\/script>\n`;
}
}
const doc = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${escHtml(title)}</title>
${tailwindScript}<style>
${darkCss}${resourceTags}${tailwindScript}<style>
${allCss}
</style>
${importMapTag}

View File

@@ -5,6 +5,7 @@ const DEFAULTS = {
layout: 'default',
keybindings: 'default',
panelSizes: null,
previewTheme: 'light',
};
export function getPref(key) {

View File

@@ -107,12 +107,26 @@ export function renderPreview(html, css, js, mode = 'html-css-js', extraCss = ''
? `<script src="https://cdn.tailwindcss.com"><\/script>\n`
: '';
// Dark preview theme
const darkCss = options.previewTheme === 'dark'
? `<style>body { background: #1e1e1e; color: #ccc; }</style>\n`
: '';
// External resources
let resourceTags = '';
if (options.resources && options.resources.length) {
for (const r of options.resources) {
if (r.type === 'css') resourceTags += `<link rel="stylesheet" href="${r.url}">\n`;
else if (r.type === 'js') resourceTags += `<script src="${r.url}"><\/script>\n`;
}
}
const doc = `<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
${consoleInterceptor}
${tailwindScript}<style>${allCss}</style>
${darkCss}${resourceTags}${tailwindScript}<style>${allCss}</style>
${importMapTag}
</head>
<body>
@@ -120,5 +134,7 @@ ${bodyContent}
${loaderScript}
</body>
</html>`;
// Update iframe bg class
frame.classList.toggle('preview-dark', options.previewTheme === 'dark');
frame.srcdoc = doc;
}