fix exported zines; fix keyboard nav
Hunter
Monday, February 2nd, 2026 at 4:08 PM
61 height: 100%;
62 font-family: monospace;
63 font-size: 21px;
64 + outline: none;
65 +}
66 +
67 +.editor-pane:focus,
68 +.editor-pane:focus-within {
69 + outline: none;
70 }
71
72 /* CodeMirror styling */
108 outline: none;
109 }
110
111 +/* Remove any focus rings/borders from CodeMirror elements */
112 +.cm-editor,
113 +.cm-editor *,
114 +.cm-editor *:focus,
115 +.cm-scroller,
116 +.cm-scroller:focus,
117 +.cm-content,
118 +.cm-content:focus {
119 + outline: none !important;
120 + box-shadow: none !important;
121 +}
122 +
123 +/* Remove any focus indicators from CodeMirror gutters */
124 +.cm-gutters {
125 + border-right: none !important;
126 +}
127 +
128 +/* Hide the selection layer border that can appear on the right edge */
129 +.cm-selectionLayer {
130 + border: none !important;
131 +}
132 +
133 +/* Target the focus ring that appears next to scrollbar */
134 +.cm-scroller:focus-visible,
135 +.cm-editor:focus-visible,
136 +.cm-content:focus-visible {
137 + outline: none !important;
138 +}
139 +
140 +/* Ensure no focus styling on any focusable element within editor */
141 +#editor *:focus {
142 + outline: none !important;
143 + box-shadow: none !important;
144 + border-color: transparent !important;
145 +}
146 +
147 +/* Remove macOS focus ring */
148 +#editor {
149 + -webkit-focus-ring-color: transparent;
150 +}
151 +
152 +#editor *:focus {
153 + -webkit-focus-ring-color: transparent;
154 +}
155 +
156 .cm-editor .cm-content,
157 .cm-editor .cm-line,
158 .cm-editor {
299 currentSpread = newSpread;
300 updateSpreadIndicator();
301 updatePreview();
302 + // Refocus preview so keyboard navigation continues to work
303 + preview.focus();
304 }
305 }
306
340 navigateSpread(-1);
341 } else if (event.data === 'nextSpread') {
342 navigateSpread(1);
343 + } else if (event.data === 'saveFile') {
344 + window.saveFile();
345 + } else if (event.data === 'saveFileWithViewer') {
346 + window.saveFileWithViewer();
347 }
348 });
349
437 doc.defaultView.addEventListener('resize', scaleToFit);
438 }
439
- // Add keyboard listener for arrow key navigation and print
440 + // Add keyboard listener for navigation and shortcuts
441 doc.addEventListener('keydown', function(e) {
442 if (e.key === 'ArrowLeft') {
443 e.preventDefault();
448 } else if ((e.metaKey || e.ctrlKey) && e.key === 'p') {
449 e.preventDefault();
450 window.print();
451 + } else if ((e.metaKey || e.ctrlKey) && e.key === 's') {
452 + e.preventDefault();
453 + parent.postMessage('saveFile', '*');
454 + } else if ((e.metaKey || e.ctrlKey) && e.key === 'e') {
455 + e.preventDefault();
456 + parent.postMessage('saveFileWithViewer', '*');
457 }
458 });
459
584 justify-content: center;
585 `;
586
587 +// Height of nav bar for layout calculations
588 +const NAV_HEIGHT = 60;
589 +
590 // Generate standalone viewer CSS and JS (matches preview pane rendering)
591 function generateViewerCode() {
592 const css = `
602 display: flex !important;
603 justify-content: center !important;
604 align-items: center !important;
605 + padding-bottom: ${NAV_HEIGHT}px !important;
606 }
607 body > *:not(.zine-spread-container):not(.zine-nav) {
608 display: none !important;
617 bottom: 0;
618 left: 0;
619 right: 0;
620 + height: ${NAV_HEIGHT}px;
621 ${NAV_BUTTON_CSS}
622 background: transparent;
623 }
626 }
627 .zine-nav button:hover:not(:disabled) { background: rgba(255,255,255,0.25); }
628 .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; }
629 + .zine-nav span { color: white; font-size: 14px; min-width: 100px; text-align: center; font-family: monospace; }
630 }
631
632 @media print {
633 ${ZINE_PRINT_CSS}
634 }
635 +
636 + /* Hide content when viewport is very short */
637 + @media screen and (max-height: 200px) {
638 + .zine-spread-container, .zine-nav {
639 + display: none !important;
640 + }
641 + }
642 `;
643
644 // Serialize SPREADS array for the viewer
648 const js = `
649 const SPREADS = ${spreadsJson};
650 const PAGE_IDS = ${pageIdsJson};
651 + const NAV_HEIGHT = ${NAV_HEIGHT};
652 let currentSpread = 0;
653 let container;
654
655 function scaleToFit() {
656 if (!container) return;
657 const vw = document.documentElement.clientWidth;
- const vh = document.documentElement.clientHeight;
658 + const vh = document.documentElement.clientHeight - NAV_HEIGHT;
659 const spreadWidthPx = container.offsetWidth;
660 const spreadHeightPx = container.offsetHeight;
661 if (spreadWidthPx === 0 || spreadHeightPx === 0) return;
734 e.preventDefault();
735 window.print();
736 }
737 + else if ((e.metaKey || e.ctrlKey) && (e.key === 's' || e.key === 'e')) {
738 + e.preventDefault();
739 + // Re-download this viewer file (clean version without dynamic elements)
740 + // Move pages back to body and remove dynamic elements temporarily
741 + PAGE_IDS.forEach(id => {
742 + const page = document.getElementById(id);
743 + if (page) {
744 + page.style.display = '';
745 + document.body.appendChild(page);
746 + }
747 + });
748 + const tempContainer = document.querySelector('.zine-spread-container');
749 + const tempNav = document.querySelector('.zine-nav');
750 + if (tempContainer) tempContainer.remove();
751 + if (tempNav) tempNav.remove();
752 +
753 + const html = '<!DOCTYPE html>' + document.documentElement.outerHTML;
754 +
755 + // Restore dynamic elements
756 + const newContainer = document.createElement('div');
757 + newContainer.className = 'zine-spread-container';
758 + document.body.appendChild(newContainer);
759 + container = newContainer;
760 +
761 + const newNav = document.createElement('div');
762 + newNav.className = 'zine-nav';
763 + 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>';
764 + document.body.appendChild(newNav);
765 + document.getElementById('zine-prev').addEventListener('click', () => navigate(-1));
766 + document.getElementById('zine-next').addEventListener('click', () => navigate(1));
767 +
768 + showSpread(currentSpread);
769 +
770 + const blob = new Blob([html], { type: 'text/html' });
771 + const url = URL.createObjectURL(blob);
772 + const a = document.createElement('a');
773 + a.href = url;
774 + a.download = 'my-zine.html';
775 + a.click();
776 + URL.revokeObjectURL(url);
777 + }
778 });
779
780 window.addEventListener('resize', scaleToFit);
945 document.getElementById('nextSpread').addEventListener('click', () => navigateSpread(1));
946 updateSpreadIndicator();
947
948 + // Focus preview when clicking anywhere in the preview pane (for keyboard nav)
949 + previewPane.addEventListener('click', (e) => {
950 + // Don't steal focus if clicking nav buttons
951 + if (!e.target.closest('.spread-nav button')) {
952 + preview.focus();
953 + }
954 + });
955 +
956 // Initial preview
957 updatePreview();
958
967
968 editorView.focus();
969
- // Keyboard shortcuts
970 + // Keyboard shortcuts (only when editor not focused, since CodeMirror handles its own)
971 document.addEventListener('keydown', function(e) {
972 + // Skip if editor is focused - CodeMirror handles these
973 + const editorHasFocus = editorView.hasFocus;
974 +
975 if ((e.metaKey || e.ctrlKey) && e.key === 'f') {
976 e.preventDefault();
977 closeSearchPanel(editorView) || openSearchPanel(editorView);
978 }
979 + // Export with viewer (Cmd+E) - only handle when editor not focused
980 + if (!editorHasFocus && (e.metaKey || e.ctrlKey) && e.key === 'e') {
981 + e.preventDefault();
982 + window.saveFileWithViewer();
983 + }
984 // Print the iframe content instead of the parent page
985 if ((e.metaKey || e.ctrlKey) && e.key === 'p') {
986 e.preventDefault();