diff options
| author | Paul Buetow <paul@buetow.org> | 2026-04-09 20:44:58 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-04-09 20:44:58 +0300 |
| commit | 3e61d09873065f5342efc414ee3ea0d5fdc4c767 (patch) | |
| tree | 7d0ac51cfb41b4774db6292deeb0cc3dce93cf07 /internal/generator/shared.go | |
| parent | 51f95f88ca78471a50b3fc62dbcea8edb609dc80 (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.go | 100 |
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}} +` |
