commit 51d72f63ad63a2f68653693f571c9a16cd16b00b
parent f69e676d98897be9949efe22535830409e09ec52
Author: Hunter
Date:   Mon,  2 Feb 2026 16:08:23 -0500

fix exported zines; fix keyboard nav

Diffstat:
Mresources/editor.css | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
Mresources/editor.js | 90+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
2 files changed, 137 insertions(+), 4 deletions(-)

diff --git a/resources/editor.css b/resources/editor.css @@ -61,6 +61,12 @@ body { height: 100%; font-family: monospace; font-size: 21px; + outline: none; +} + +.editor-pane:focus, +.editor-pane:focus-within { + outline: none; } /* CodeMirror styling */ @@ -102,6 +108,51 @@ body { outline: none; } +/* Remove any focus rings/borders from CodeMirror elements */ +.cm-editor, +.cm-editor *, +.cm-editor *:focus, +.cm-scroller, +.cm-scroller:focus, +.cm-content, +.cm-content:focus { + outline: none !important; + box-shadow: none !important; +} + +/* Remove any focus indicators from CodeMirror gutters */ +.cm-gutters { + border-right: none !important; +} + +/* Hide the selection layer border that can appear on the right edge */ +.cm-selectionLayer { + border: none !important; +} + +/* Target the focus ring that appears next to scrollbar */ +.cm-scroller:focus-visible, +.cm-editor:focus-visible, +.cm-content:focus-visible { + outline: none !important; +} + +/* Ensure no focus styling on any focusable element within editor */ +#editor *:focus { + outline: none !important; + box-shadow: none !important; + border-color: transparent !important; +} + +/* Remove macOS focus ring */ +#editor { + -webkit-focus-ring-color: transparent; +} + +#editor *:focus { + -webkit-focus-ring-color: transparent; +} + .cm-editor .cm-content, .cm-editor .cm-line, .cm-editor { diff --git a/resources/editor.js b/resources/editor.js @@ -299,6 +299,8 @@ function navigateSpread(delta) { currentSpread = newSpread; updateSpreadIndicator(); updatePreview(); + // Refocus preview so keyboard navigation continues to work + preview.focus(); } } @@ -338,6 +340,10 @@ window.addEventListener('message', function(event) { navigateSpread(-1); } else if (event.data === 'nextSpread') { navigateSpread(1); + } else if (event.data === 'saveFile') { + window.saveFile(); + } else if (event.data === 'saveFileWithViewer') { + window.saveFileWithViewer(); } }); @@ -431,7 +437,7 @@ function updatePreview() { doc.defaultView.addEventListener('resize', scaleToFit); } - // Add keyboard listener for arrow key navigation and print + // Add keyboard listener for navigation and shortcuts doc.addEventListener('keydown', function(e) { if (e.key === 'ArrowLeft') { e.preventDefault(); @@ -442,6 +448,12 @@ function updatePreview() { } else if ((e.metaKey || e.ctrlKey) && e.key === 'p') { e.preventDefault(); window.print(); + } else if ((e.metaKey || e.ctrlKey) && e.key === 's') { + e.preventDefault(); + parent.postMessage('saveFile', '*'); + } else if ((e.metaKey || e.ctrlKey) && e.key === 'e') { + e.preventDefault(); + parent.postMessage('saveFileWithViewer', '*'); } }); @@ -572,6 +584,9 @@ const NAV_BUTTON_STYLES = ` justify-content: center; `; +// Height of nav bar for layout calculations +const NAV_HEIGHT = 60; + // Generate standalone viewer CSS and JS (matches preview pane rendering) function generateViewerCode() { const css = ` @@ -587,6 +602,7 @@ function generateViewerCode() { display: flex !important; justify-content: center !important; align-items: center !important; + padding-bottom: ${NAV_HEIGHT}px !important; } body > *:not(.zine-spread-container):not(.zine-nav) { display: none !important; @@ -601,6 +617,7 @@ function generateViewerCode() { bottom: 0; left: 0; right: 0; + height: ${NAV_HEIGHT}px; ${NAV_BUTTON_CSS} background: transparent; } @@ -609,12 +626,19 @@ function generateViewerCode() { } .zine-nav button:hover:not(:disabled) { background: rgba(255,255,255,0.25); } .zine-nav button:disabled { opacity: 0.3; cursor: default; } - .zine-nav span { color: white; font-size: 14px; min-width: 100px; text-align: center; font-family: system-ui, sans-serif; } + .zine-nav span { color: white; font-size: 14px; min-width: 100px; text-align: center; font-family: monospace; } } @media print { ${ZINE_PRINT_CSS} } + + /* Hide content when viewport is very short */ + @media screen and (max-height: 200px) { + .zine-spread-container, .zine-nav { + display: none !important; + } + } `; // Serialize SPREADS array for the viewer @@ -624,13 +648,14 @@ function generateViewerCode() { const js = ` const SPREADS = ${spreadsJson}; const PAGE_IDS = ${pageIdsJson}; + const NAV_HEIGHT = ${NAV_HEIGHT}; let currentSpread = 0; let container; function scaleToFit() { if (!container) return; const vw = document.documentElement.clientWidth; - const vh = document.documentElement.clientHeight; + const vh = document.documentElement.clientHeight - NAV_HEIGHT; const spreadWidthPx = container.offsetWidth; const spreadHeightPx = container.offsetHeight; if (spreadWidthPx === 0 || spreadHeightPx === 0) return; @@ -709,6 +734,47 @@ function generateViewerCode() { e.preventDefault(); window.print(); } + else if ((e.metaKey || e.ctrlKey) && (e.key === 's' || e.key === 'e')) { + e.preventDefault(); + // Re-download this viewer file (clean version without dynamic elements) + // Move pages back to body and remove dynamic elements temporarily + PAGE_IDS.forEach(id => { + const page = document.getElementById(id); + if (page) { + page.style.display = ''; + document.body.appendChild(page); + } + }); + const tempContainer = document.querySelector('.zine-spread-container'); + const tempNav = document.querySelector('.zine-nav'); + if (tempContainer) tempContainer.remove(); + if (tempNav) tempNav.remove(); + + const html = '<!DOCTYPE html>' + document.documentElement.outerHTML; + + // Restore dynamic elements + const newContainer = document.createElement('div'); + newContainer.className = 'zine-spread-container'; + document.body.appendChild(newContainer); + container = newContainer; + + const newNav = document.createElement('div'); + newNav.className = 'zine-nav'; + newNav.innerHTML = '<button id="zine-prev" title="Previous spread">\\u2190</button><span id="zine-indicator"></span><button id="zine-next" title="Next spread">\\u2192</button>'; + document.body.appendChild(newNav); + document.getElementById('zine-prev').addEventListener('click', () => navigate(-1)); + document.getElementById('zine-next').addEventListener('click', () => navigate(1)); + + showSpread(currentSpread); + + const blob = new Blob([html], { type: 'text/html' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'my-zine.html'; + a.click(); + URL.revokeObjectURL(url); + } }); window.addEventListener('resize', scaleToFit); @@ -879,6 +945,14 @@ async function initializeCodeMirror() { document.getElementById('nextSpread').addEventListener('click', () => navigateSpread(1)); updateSpreadIndicator(); + // Focus preview when clicking anywhere in the preview pane (for keyboard nav) + previewPane.addEventListener('click', (e) => { + // Don't steal focus if clicking nav buttons + if (!e.target.closest('.spread-nav button')) { + preview.focus(); + } + }); + // Initial preview updatePreview(); @@ -893,12 +967,20 @@ async function initializeCodeMirror() { editorView.focus(); - // Keyboard shortcuts + // Keyboard shortcuts (only when editor not focused, since CodeMirror handles its own) document.addEventListener('keydown', function(e) { + // Skip if editor is focused - CodeMirror handles these + const editorHasFocus = editorView.hasFocus; + if ((e.metaKey || e.ctrlKey) && e.key === 'f') { e.preventDefault(); closeSearchPanel(editorView) || openSearchPanel(editorView); } + // Export with viewer (Cmd+E) - only handle when editor not focused + if (!editorHasFocus && (e.metaKey || e.ctrlKey) && e.key === 'e') { + e.preventDefault(); + window.saveFileWithViewer(); + } // Print the iframe content instead of the parent page if ((e.metaKey || e.ctrlKey) && e.key === 'p') { e.preventDefault();