diff --git a/public/css/style.css b/public/css/style.css index 867075e..1c945e7 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -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 { diff --git a/public/index.html b/public/index.html index 44d8c86..52d540f 100644 --- a/public/index.html +++ b/public/index.html @@ -21,71 +21,115 @@ - - - - - -
- +
- - - - - - - - +
+
+
+ + + + +
+
- - - - - - + +
+ + + + + + + +
+
+ + +
diff --git a/public/js/app.js b/public/js/app.js index f6e6563..a5b8226 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -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(); diff --git a/public/js/editors.js b/public/js/editors.js index 4f53dd4..f487a32 100644 --- a/public/js/editors.js +++ b/public/js/editors.js @@ -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) {