fix exported zines; fix keyboard nav

Hunter
Monday, February 2nd, 2026 at 4:08 PM

resources/editor.css+51 
resources/editor.js+86-4

resources/editor.css
 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 {
resources/editor.js
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();