summaryrefslogtreecommitdiff
path: root/internal/generator/shared.go
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-04-09 20:44:58 +0300
committerPaul Buetow <paul@buetow.org>2026-04-09 20:44:58 +0300
commit3e61d09873065f5342efc414ee3ea0d5fdc4c767 (patch)
tree7d0ac51cfb41b4774db6292deeb0cc3dce93cf07 /internal/generator/shared.go
parent51f95f88ca78471a50b3fc62dbcea8edb609dc80 (diff)
add snonux static microblog generator
Full Go implementation with: - txt/md/image/audio input processing, URL auto-linking in .txt files - Paginated HTML output with Atom feed - 11 visual themes: neon, terminal, synthwave, minimal, brutalist, paper, aurora, matrix, ocean, retro, glass (selectable via --theme flag) - Keyboard navigation (j/k/arrows, Enter modal, h/l page nav) - Shared nav templates (navhints, navmodal, navscript) across all themes - Magefile build automation; integration test suite covering all themes Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'internal/generator/shared.go')
-rw-r--r--internal/generator/shared.go100
1 files changed, 100 insertions, 0 deletions
diff --git a/internal/generator/shared.go b/internal/generator/shared.go
new file mode 100644
index 0000000..eed4de3
--- /dev/null
+++ b/internal/generator/shared.go
@@ -0,0 +1,100 @@
+package generator
+
+// navDefs is appended to every theme template when parsing.
+// It defines three named sub-templates shared across all themes:
+// - "navhints" — keyboard shortcut hint bar HTML
+// - "navmodal" — full-screen expanded-post modal HTML
+// - "navscript" — keyboard navigation JavaScript
+//
+// Each theme calls {{template "navhints" .}}, {{template "navmodal" .}}, and
+// {{template "navscript" .}} at the appropriate points in its HTML.
+// All CSS for these elements (colours, borders, backdrop) lives in each theme
+// so themes remain self-contained and independently styled.
+const navDefs = `
+{{define "navhints"}}
+<div class="nav-hints" aria-label="keyboard shortcuts">
+ <span><kbd>j</kbd><kbd>k</kbd> or <kbd>↑</kbd><kbd>↓</kbd> select post</span>
+ <span><kbd>Enter</kbd> expand</span>
+ <span><kbd>Esc</kbd> close</span>
+ <span><kbd>h</kbd><kbd>l</kbd> or <kbd>←</kbd><kbd>→</kbd> change page</span>
+</div>
+{{end}}
+
+{{define "navmodal"}}
+<div class="post-modal" id="post-modal">
+ <div class="modal-inner">
+ <button class="modal-close" onclick="closeModal()">[ ESC ] CLOSE</button>
+ <div id="modal-content"></div>
+ </div>
+</div>
+{{end}}
+
+{{define "navscript"}}
+<script>
+ // === KEYBOARD NAVIGATION ===
+ // j / ArrowDown → next post k / ArrowUp → previous post
+ // h / ArrowLeft → previous page l / ArrowRight → next page
+ // Enter → expand modal Esc → close modal
+ const posts = document.querySelectorAll('.post');
+ let currentIndex = posts.length > 0 ? 0 : -1;
+ const prevPageURL = {{.PrevPageJSON}};
+ const nextPageURL = {{.NextPageJSON}};
+
+ if (currentIndex >= 0) selectPost(0);
+
+ function selectPost(index) {
+ if (posts.length === 0) return;
+ if (currentIndex >= 0) posts[currentIndex].classList.remove('post-active');
+ currentIndex = Math.max(0, Math.min(index, posts.length - 1));
+ posts[currentIndex].classList.add('post-active');
+ posts[currentIndex].scrollIntoView({ behavior: 'smooth', block: 'nearest' });
+ playNavSound();
+ }
+
+ // playNavSound generates a short beep via the Web Audio API.
+ // A fresh AudioContext per call avoids state issues across navigations.
+ function playNavSound() {
+ try {
+ const ctx = new (window.AudioContext || window.webkitAudioContext)();
+ const osc = ctx.createOscillator();
+ const gain = ctx.createGain();
+ osc.connect(gain); gain.connect(ctx.destination);
+ osc.frequency.value = 220; osc.type = 'sine';
+ gain.gain.setValueAtTime(0.15, ctx.currentTime);
+ gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.08);
+ osc.start(ctx.currentTime); osc.stop(ctx.currentTime + 0.08);
+ } catch (_) {}
+ }
+
+ function openModal() {
+ if (currentIndex < 0) return;
+ document.getElementById('modal-content').innerHTML =
+ posts[currentIndex].querySelector('.post-text').innerHTML;
+ document.getElementById('post-modal').classList.add('active');
+ }
+
+ function closeModal() {
+ document.getElementById('post-modal').classList.remove('active');
+ }
+
+ document.addEventListener('keydown', function(e) {
+ if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
+ if (document.getElementById('post-modal').classList.contains('active')) {
+ if (e.key === 'Escape') { closeModal(); e.preventDefault(); }
+ return;
+ }
+ switch (e.key) {
+ case 'j': case 'ArrowDown': selectPost(currentIndex + 1); e.preventDefault(); break;
+ case 'k': case 'ArrowUp': selectPost(currentIndex - 1); e.preventDefault(); break;
+ case 'h': case 'ArrowLeft':
+ if (prevPageURL) { playNavSound(); window.location.href = prevPageURL; }
+ e.preventDefault(); break;
+ case 'l': case 'ArrowRight':
+ if (nextPageURL) { playNavSound(); window.location.href = nextPageURL; }
+ e.preventDefault(); break;
+ case 'Enter': openModal(); e.preventDefault(); break;
+ }
+ });
+</script>
+{{end}}
+`