Fix syntax coloring, modernize toolbar UI, and clean up CSS

- Fix Monarch tokenizer loading: await initLinter() before editor creation
  so loadScript() doesn't clobber window.define during lazy tokenizer init
- Fix JSX/TSX coloring: use file URIs with proper extensions (.jsx/.tsx)
  so Monaco enables JSX tokenization via the TypeScript language service
- Modernize toolbar: move settings to gear popover, replace text buttons
  with SVG icons, consolidate toggle checkboxes into compact group
- Clean up CSS: remove duplicate toggle classes, dead selectors, orphaned rules
This commit is contained in:
root
2026-02-27 15:19:10 -06:00
parent 0d84c56008
commit 26e232fd41
4 changed files with 222 additions and 140 deletions

View File

@@ -147,9 +147,6 @@ body.resizing iframe { pointer-events: none; }
.tab-lang-javascript .tab-color-dot { background: #F7DF1E; }
.tab-lang-typescript .tab-color-dot { background: #3178C6; }
.tab-lang-markdown .tab-color-dot { background: #83B; }
.tab-lang-html .tab-btn-label, .tab-lang-css .tab-btn-label,
.tab-lang-javascript .tab-btn-label, .tab-lang-typescript .tab-btn-label,
.tab-lang-markdown .tab-btn-label { /* no extra style needed, just grouping */ }
/* Active tab border matches language color */
.tab-lang-html.active { border-bottom-color: #E44D26; }
@@ -219,51 +216,39 @@ body.resizing iframe { pointer-events: none; }
flex-shrink: 0;
}
/* Auto-run toggle */
.auto-run-toggle {
/* Toolbar toggles (shared) */
.toolbar-toggles {
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
gap: 2px;
}
.toolbar-toggle {
display: flex;
align-items: center;
gap: 3px;
font-size: 11px;
color: var(--text-dim);
cursor: pointer;
user-select: none;
padding: 3px 6px;
border-radius: 4px;
transition: background 0.15s, color 0.15s;
}
.auto-run-toggle input { cursor: pointer; }
/* Tailwind toggle */
.tailwind-toggle {
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
color: var(--text-dim);
cursor: pointer;
user-select: none;
.toolbar-toggle:hover { background: rgba(255,255,255,0.06); color: var(--text); }
.toolbar-toggle:has(input:checked) { color: var(--accent); }
.toolbar-toggle input { display: none; }
.toolbar-toggle span { pointer-events: none; }
.toolbar-divider {
width: 1px;
height: 20px;
background: var(--border);
margin: 0 4px;
flex-shrink: 0;
}
.tailwind-toggle input { cursor: pointer; }
/* Dividers */
.divider { background: var(--border); transition: background 0.15s; z-index: 2; }
/* Layout/keybinding/preview-theme selects — match framework select */
#layout-mode, #keybinding-mode, #preview-theme, #editor-theme {
background: var(--bg); color: var(--text); border: 1px solid var(--border);
padding: 4px 6px; border-radius: 4px; font-size: 12px; cursor: pointer;
}
/* Listed toggle */
.listed-toggle {
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
color: var(--text-dim);
cursor: pointer;
user-select: none;
}
.listed-toggle input { cursor: pointer; }
/* Tags input */
.tags-input-wrap {
display: flex;
@@ -294,6 +279,58 @@ body.resizing iframe { pointer-events: none; }
}
.btn-secondary:hover { color: var(--text); background: var(--border); }
/* Icon buttons */
.btn-icon {
background: transparent;
color: var(--text-dim);
border: none;
padding: 5px 6px;
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.15s, color 0.15s;
}
.btn-icon:hover { color: var(--text); background: rgba(255,255,255,0.08); }
.btn-icon svg { display: block; }
/* Settings popover */
.settings-popover-wrap { position: relative; }
.settings-popover {
position: absolute;
top: calc(100% + 6px);
right: 0;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
padding: 12px 14px;
min-width: 220px;
z-index: 100;
box-shadow: 0 8px 24px rgba(0,0,0,0.4);
display: flex;
flex-direction: column;
gap: 10px;
}
.settings-popover.hidden { display: none; }
.settings-group {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
}
.settings-label {
font-size: 12px;
color: var(--text-dim);
white-space: nowrap;
font-weight: 500;
}
.settings-popover select {
background: var(--bg); color: var(--text); border: 1px solid var(--border);
padding: 3px 6px; border-radius: 4px; font-size: 11px; cursor: pointer;
min-width: 120px;
}
/* QR Modal */
.modal-overlay {
position: fixed; inset: 0; background: rgba(0,0,0,0.7);
@@ -617,24 +654,6 @@ body.resizing iframe { pointer-events: none; }
.templates-grid { grid-template-columns: 1fr; }
}
/* ===================== Format Save Toggle ===================== */
.format-save-toggle {
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
color: var(--text-dim);
cursor: pointer;
user-select: none;
}
.format-save-toggle input { cursor: pointer; }
/* ===================== Editor Font Select ===================== */
#editor-font {
background: var(--bg); color: var(--text); border: 1px solid var(--border);
padding: 4px 6px; border-radius: 4px; font-size: 12px; cursor: pointer;
}
/* ===================== Version History Modal ===================== */
.history-modal-content { min-width: 500px; max-width: 700px; text-align: left; }
.history-list {

View File

@@ -21,71 +21,115 @@
<option value="wasm">WASM</option>
</select>
<input type="text" id="title-input" placeholder="Untitled" spellcheck="false">
<select id="layout-mode">
<option value="default">Default</option>
<option value="top-bottom">Top / Bottom</option>
<option value="editor-only">Editor Only</option>
<option value="preview-only">Preview Only</option>
</select>
<select id="keybinding-mode">
<option value="default">Default</option>
<option value="vim">Vim</option>
<option value="emacs">Emacs</option>
</select>
<select id="editor-theme">
<option value="vs-dark">VS Dark</option>
<option value="vs">VS Light</option>
<option value="hc-black">High Contrast</option>
<option value="monokai">Monokai</option>
<option value="dracula">Dracula</option>
<option value="github-dark">GitHub Dark</option>
</select>
<select id="editor-font" title="Editor font">
<option value="default">Default Font</option>
<option value="Fira Code">Fira Code</option>
<option value="JetBrains Mono">JetBrains Mono</option>
<option value="Source Code Pro">Source Code Pro</option>
<option value="IBM Plex Mono">IBM Plex Mono</option>
<option value="Cascadia Code">Cascadia Code</option>
</select>
</div>
<div class="toolbar-right">
<div class="tags-input-wrap">
<input type="text" id="tags-input" placeholder="Add tags..." list="tags-datalist" spellcheck="false">
<input type="text" id="tags-input" placeholder="Tags..." list="tags-datalist" spellcheck="false">
<datalist id="tags-datalist"></datalist>
<div id="tags-display" class="tags-display"></div>
</div>
<select id="preview-theme" title="Preview background theme">
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
<button id="btn-templates" class="btn-secondary" title="Starter templates gallery">Templates</button>
<button id="btn-resources" class="btn-secondary" title="External CSS/JS resources">Resources</button>
<button id="btn-shortcuts" class="btn-secondary" title="Keyboard shortcuts (?)" aria-label="Keyboard shortcuts">?</button>
<label class="tailwind-toggle" title="Enable Tailwind CSS">
<input type="checkbox" id="tailwind-checkbox">
Tailwind
</label>
<label class="listed-toggle" title="Show in browse page">
<input type="checkbox" id="listed-checkbox" checked>
Listed
</label>
<label class="format-save-toggle" title="Auto-format code on save">
<input type="checkbox" id="format-save-checkbox">
Fmt
</label>
<label class="auto-run-toggle" title="Auto-run on change">
<input type="checkbox" id="auto-run-checkbox" checked>
Auto
</label>
</div>
<div class="toolbar-right">
<div class="toolbar-toggles">
<label class="toolbar-toggle" title="Enable Tailwind CSS">
<input type="checkbox" id="tailwind-checkbox">
<span>TW</span>
</label>
<label class="toolbar-toggle" title="Auto-run on change">
<input type="checkbox" id="auto-run-checkbox" checked>
<span>Auto</span>
</label>
<label class="toolbar-toggle" title="Auto-format on save">
<input type="checkbox" id="format-save-checkbox">
<span>Fmt</span>
</label>
<label class="toolbar-toggle" title="Show in browse page">
<input type="checkbox" id="listed-checkbox" checked>
<span>Listed</span>
</label>
</div>
<div class="toolbar-divider"></div>
<button id="btn-run" title="Run (Ctrl+Enter)">Run</button>
<button id="btn-save" title="Save (Ctrl+S)">Save</button>
<button id="btn-fork" title="Fork">Fork</button>
<button id="btn-history" title="Version history" class="btn-secondary">History</button>
<button id="btn-embed" title="Embed code generator" class="btn-secondary">Embed</button>
<button id="btn-collection" title="Add to collection" class="btn-secondary">Collection</button>
<button id="btn-export" title="Export standalone HTML" class="btn-secondary">Export</button>
<button id="btn-qr" title="QR code" class="btn-secondary">QR</button>
<button id="btn-fork" class="btn-secondary" title="Fork">Fork</button>
<div class="toolbar-divider"></div>
<button id="btn-templates" class="btn-icon" title="Starter templates">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg>
</button>
<button id="btn-resources" class="btn-icon" title="External resources">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>
</button>
<button id="btn-history" class="btn-icon" title="Version history">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
</button>
<button id="btn-embed" class="btn-icon" title="Embed code">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>
</button>
<button id="btn-export" class="btn-icon" title="Export HTML">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
</button>
<button id="btn-collection" class="btn-icon" title="Collections">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>
</button>
<button id="btn-qr" class="btn-icon" title="QR code">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="2" width="8" height="8" rx="1"/><rect x="14" y="2" width="8" height="8" rx="1"/><rect x="2" y="14" width="8" height="8" rx="1"/><rect x="14" y="14" width="4" height="4"/><line x1="22" y1="14" x2="22" y2="22"/><line x1="14" y1="22" x2="22" y2="22"/></svg>
</button>
<div class="toolbar-divider"></div>
<div class="settings-popover-wrap">
<button id="btn-settings" class="btn-icon" title="Settings">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
</button>
<div id="settings-popover" class="settings-popover hidden">
<div class="settings-group">
<label class="settings-label">Layout</label>
<select id="layout-mode">
<option value="default">Default</option>
<option value="top-bottom">Top / Bottom</option>
<option value="editor-only">Editor Only</option>
<option value="preview-only">Preview Only</option>
</select>
</div>
<div class="settings-group">
<label class="settings-label">Theme</label>
<select id="editor-theme">
<option value="vs-dark">VS Dark</option>
<option value="vs">VS Light</option>
<option value="hc-black">High Contrast</option>
<option value="monokai">Monokai</option>
<option value="dracula">Dracula</option>
<option value="github-dark">GitHub Dark</option>
</select>
</div>
<div class="settings-group">
<label class="settings-label">Font</label>
<select id="editor-font" title="Editor font">
<option value="default">Default Font</option>
<option value="Fira Code">Fira Code</option>
<option value="JetBrains Mono">JetBrains Mono</option>
<option value="Source Code Pro">Source Code Pro</option>
<option value="IBM Plex Mono">IBM Plex Mono</option>
<option value="Cascadia Code">Cascadia Code</option>
</select>
</div>
<div class="settings-group">
<label class="settings-label">Keybindings</label>
<select id="keybinding-mode">
<option value="default">Default</option>
<option value="vim">Vim</option>
<option value="emacs">Emacs</option>
</select>
</div>
<div class="settings-group">
<label class="settings-label">Preview</label>
<select id="preview-theme" title="Preview background">
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
</div>
<div class="settings-group">
<label class="settings-label">Shortcuts</label>
<button id="btn-shortcuts" class="btn-small">View all</button>
</div>
</div>
</div>
</div>
</header>

View File

@@ -290,8 +290,10 @@ function applyLayout(layout) {
}
async function init() {
// Load Emmet before editors so completion providers are registered
// Load ALL CDN scripts before editor creation so window.define
// (Monaco's RequireJS) is never clobbered during Monarch tokenizer init
await initEmmet();
await initLinter();
// Register custom Monaco themes before creating editors
registerCustomThemes();
@@ -312,7 +314,6 @@ async function init() {
initPerformance();
initResizer();
initKeybindings();
initLinter();
// Auto-run checkbox
const autoRunCb = $('#auto-run-checkbox');
@@ -571,6 +572,18 @@ async function init() {
$('#shortcuts-modal-close').addEventListener('click', () => scModal.classList.add('hidden'));
scModal.addEventListener('click', (e) => { if (e.target === scModal) scModal.classList.add('hidden'); });
// Settings popover toggle
const settingsPopover = $('#settings-popover');
$('#btn-settings').addEventListener('click', (e) => {
e.stopPropagation();
settingsPopover.classList.toggle('hidden');
});
document.addEventListener('click', (e) => {
if (!settingsPopover.classList.contains('hidden') && !e.target.closest('.settings-popover-wrap')) {
settingsPopover.classList.add('hidden');
}
});
// Load fiddle from URL if present
loadFromUrl();

View File

@@ -37,41 +37,41 @@ const editorOpts = {
export const MODE_TABS = {
'html-css-js': [
{ id: 'html', label: 'HTML', lang: 'html' },
{ id: 'css', label: 'CSS', lang: 'css' },
{ id: 'js', label: 'JavaScript', lang: 'javascript' },
{ id: 'html', label: 'HTML', lang: 'html', ext: 'html' },
{ id: 'css', label: 'CSS', lang: 'css', ext: 'css' },
{ id: 'js', label: 'JavaScript', lang: 'javascript', ext: 'js' },
],
'typescript': [
{ id: 'html', label: 'HTML', lang: 'html' },
{ id: 'css', label: 'CSS', lang: 'css' },
{ id: 'js', label: 'TypeScript', lang: 'typescript' },
{ id: 'html', label: 'HTML', lang: 'html', ext: 'html' },
{ id: 'css', label: 'CSS', lang: 'css', ext: 'css' },
{ id: 'js', label: 'TypeScript', lang: 'typescript', ext: 'ts' },
],
'react': [
{ id: 'js', label: 'JSX', lang: 'javascript' },
{ id: 'css', label: 'CSS', lang: 'css' },
{ id: 'html', label: 'HTML', lang: 'html' },
{ id: 'js', label: 'JSX', lang: 'javascript', ext: 'jsx' },
{ id: 'css', label: 'CSS', lang: 'css', ext: 'css' },
{ id: 'html', label: 'HTML', lang: 'html', ext: 'html' },
],
'react-ts': [
{ id: 'js', label: 'TSX', lang: 'typescript' },
{ id: 'css', label: 'CSS', lang: 'css' },
{ id: 'html', label: 'HTML', lang: 'html' },
{ id: 'js', label: 'TSX', lang: 'typescript', ext: 'tsx' },
{ id: 'css', label: 'CSS', lang: 'css', ext: 'css' },
{ id: 'html', label: 'HTML', lang: 'html', ext: 'html' },
],
'vue': [
{ id: 'js', label: 'Vue SFC', lang: 'html' },
{ id: 'css', label: 'CSS', lang: 'css' },
{ id: 'js', label: 'Vue SFC', lang: 'html', ext: 'vue' },
{ id: 'css', label: 'CSS', lang: 'css', ext: 'css' },
],
'svelte': [
{ id: 'js', label: 'Svelte', lang: 'html' },
{ id: 'css', label: 'CSS', lang: 'css' },
{ id: 'js', label: 'Svelte', lang: 'html', ext: 'svelte' },
{ id: 'css', label: 'CSS', lang: 'css', ext: 'css' },
],
'markdown': [
{ id: 'js', label: 'Markdown', lang: 'markdown' },
{ id: 'css', label: 'CSS', lang: 'css' },
{ id: 'js', label: 'Markdown', lang: 'markdown', ext: 'md' },
{ id: 'css', label: 'CSS', lang: 'css', ext: 'css' },
],
'wasm': [
{ id: 'html', label: 'HTML', lang: 'html' },
{ id: 'css', label: 'CSS', lang: 'css' },
{ id: 'js', label: 'JavaScript', lang: 'javascript' },
{ id: 'html', label: 'HTML', lang: 'html', ext: 'html' },
{ id: 'css', label: 'CSS', lang: 'css', ext: 'css' },
{ id: 'js', label: 'JavaScript', lang: 'javascript', ext: 'js' },
],
};
@@ -179,16 +179,20 @@ function configureJsxSupport(mode) {
}
}
let modelCounter = 0;
function createEditor(tabDef) {
const container = document.createElement('div');
container.className = 'editor-container';
container.id = `editor-${tabDef.id}`;
editorArea().appendChild(container);
const uri = monaco.Uri.parse(`file:///fiddle-${++modelCounter}.${tabDef.ext}`);
const model = monaco.editor.createModel('', tabDef.lang, uri);
const editor = monaco.editor.create(container, {
...editorOpts,
language: tabDef.lang,
value: '',
model,
});
if (onChangeCallback) {
@@ -289,11 +293,13 @@ export function initEditors(mode = 'html-css-js') {
export function switchMode(mode) {
if (mode === currentMode) return;
// Dispose existing editors
// Dispose existing editors and their models
const oldTabs = MODE_TABS[currentMode];
oldTabs.forEach((tab) => {
if (editors[tab.id]) {
const model = editors[tab.id].getModel();
editors[tab.id].dispose();
if (model) model.dispose();
}
const container = editors[`_container_${tab.id}`];
if (container && container.parentNode) {