summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-04-10 10:29:33 +0300
committerPaul Buetow <paul@buetow.org>2026-04-10 10:29:33 +0300
commitb4899f8a322c5df78731e3c5b6d583ec0835d129 (patch)
tree858034ff76e3aaf43c6821f9ae5a18298a978844 /internal
parentf40fee44e8f256328ca1419863b5441123a1014e (diff)
Release v0.1.1v0.1.1
Per-theme Web Audio presets; pagination footer bar with reduced height; brutalist splash label tweak; doc updates. Made-with: Cursor
Diffstat (limited to 'internal')
-rw-r--r--internal/generator/doc.go2
-rw-r--r--internal/generator/generator.go26
-rw-r--r--internal/generator/generator_test.go19
-rw-r--r--internal/generator/shared.go74
-rw-r--r--internal/generator/theme_aurora.go11
-rw-r--r--internal/generator/theme_brutalist.go12
-rw-r--r--internal/generator/theme_glass.go11
-rw-r--r--internal/generator/theme_matrix.go10
-rw-r--r--internal/generator/theme_minimal.go11
-rw-r--r--internal/generator/theme_neon.go11
-rw-r--r--internal/generator/theme_ocean.go11
-rw-r--r--internal/generator/theme_paper.go11
-rw-r--r--internal/generator/theme_retro.go10
-rw-r--r--internal/generator/theme_sounds.go179
-rw-r--r--internal/generator/theme_synthwave.go11
-rw-r--r--internal/generator/theme_terminal.go10
-rw-r--r--internal/version.go2
17 files changed, 347 insertions, 74 deletions
diff --git a/internal/generator/doc.go b/internal/generator/doc.go
index b22ede6..ad974ad 100644
--- a/internal/generator/doc.go
+++ b/internal/generator/doc.go
@@ -13,6 +13,8 @@
// {{template "navscript" .}}.
// - shared.go — navDefs: shared {{define}} blocks merged at parse time with
// the chosen theme so a single html/template parse sees every name.
+// - theme_sounds.go — Per-theme Web Audio parameters (splash arpeggio, nav blip,
+// modal open/close); embedded in pages as ThemeSoundsJSON for navscript.
// - templates.go — Short pointer: where templates and registry live.
//
// Dependency direction: themes and shared nav templates are composed only for
diff --git a/internal/generator/generator.go b/internal/generator/generator.go
index e880ba3..3d1a441 100644
--- a/internal/generator/generator.go
+++ b/internal/generator/generator.go
@@ -17,11 +17,12 @@ import (
// pageData holds the template variables for a single HTML page.
type pageData struct {
- Posts []postView
- PrevPage string // URL of the newer page, empty if none
- NextPage string // URL of the older page, empty if none
- PrevPageJSON template.JS
- NextPageJSON template.JS
+ Posts []postView
+ PrevPage string // URL of the newer page, empty if none
+ NextPage string // URL of the older page, empty if none
+ PrevPageJSON template.JS
+ NextPageJSON template.JS
+ ThemeSoundsJSON template.JS // Web Audio preset for this theme (splash + nav)
}
// postView is a render-friendly representation of a post for the HTML template.
@@ -121,7 +122,7 @@ const indexPageNavURL = "index.html?splash=0"
// writePage renders one HTML page and writes it to cfg.OutputDir.
func writePage(tmpl *template.Template, posts []*post.Post, pageIndex, totalPages int, cfg *config.Config) error {
- data := buildPageData(posts, pageIndex, totalPages)
+ data := buildPageData(posts, pageIndex, totalPages, cfg.Theme)
filename := pageFilename(pageIndex)
path := filepath.Join(cfg.OutputDir, filename)
@@ -140,7 +141,7 @@ func writePage(tmpl *template.Template, posts []*post.Post, pageIndex, totalPage
}
// buildPageData constructs the template data for a single page.
-func buildPageData(posts []*post.Post, pageIndex, totalPages int) pageData {
+func buildPageData(posts []*post.Post, pageIndex, totalPages int, theme string) pageData {
views := make([]postView, len(posts))
for i, p := range posts {
views[i] = postView{
@@ -166,11 +167,12 @@ func buildPageData(posts []*post.Post, pageIndex, totalPages int) pageData {
}
return pageData{
- Posts: views,
- PrevPage: prevPage,
- NextPage: nextPage,
- PrevPageJSON: jsonStringOrNull(prevPage),
- NextPageJSON: jsonStringOrNull(nextPage),
+ Posts: views,
+ PrevPage: prevPage,
+ NextPage: nextPage,
+ PrevPageJSON: jsonStringOrNull(prevPage),
+ NextPageJSON: jsonStringOrNull(nextPage),
+ ThemeSoundsJSON: themeSoundsJSON(theme),
}
}
diff --git a/internal/generator/generator_test.go b/internal/generator/generator_test.go
index cfab15e..0960d87 100644
--- a/internal/generator/generator_test.go
+++ b/internal/generator/generator_test.go
@@ -86,6 +86,23 @@ func TestJSONStringOrNull(t *testing.T) {
}
}
+func TestThemeSoundPresetsMatchRegistry(t *testing.T) {
+ t.Parallel()
+ for name := range themeRegistry {
+ if _, ok := themeSoundPresets[name]; !ok {
+ t.Errorf("theme %q has no sound preset in themeSoundPresets", name)
+ }
+ }
+}
+
+func TestThemeSoundsJSONNonEmpty(t *testing.T) {
+ t.Parallel()
+ j := themeSoundsJSON("neon")
+ if len(j) < 50 {
+ t.Fatalf("themeSoundsJSON too short: %q", j)
+ }
+}
+
func TestFormatPostTime(t *testing.T) {
t.Parallel()
@@ -161,7 +178,7 @@ func TestBuildPageData_navLinks(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
- data := buildPageData([]*post.Post{p}, tt.pageIndex, tt.totalPages)
+ data := buildPageData([]*post.Post{p}, tt.pageIndex, tt.totalPages, "neon")
if data.PrevPage != tt.wantPrev {
t.Fatalf("PrevPage=%q; want %q", data.PrevPage, tt.wantPrev)
}
diff --git a/internal/generator/shared.go b/internal/generator/shared.go
index 583851c..b10de47 100644
--- a/internal/generator/shared.go
+++ b/internal/generator/shared.go
@@ -6,7 +6,7 @@ package generator
// splash should not run (?splash=0, not index.html, or Referer from same-site index/pageN).
// - "navhints" — keyboard shortcut hint bar HTML
// - "navmodal" — full-screen expanded-post modal HTML + image-sizing CSS
-// - "navscript" — keyboard navigation JavaScript with distinct sounds per action
+// - "navscript" — keyboard navigation + Web Audio; splash/nav/modal sounds from themeSoundsJSON (per theme)
//
// Each theme calls {{template "splashGate"}}, {{template "navhints" .}}, {{template "navmodal" .}},
// and {{template "navscript" .}} at the appropriate points in its HTML.
@@ -62,9 +62,15 @@ const navDefs = `
.post-modal { background:rgba(0,0,0,0.55) !important; backdrop-filter:blur(6px) !important; }
/* Content area max-width across all themes */
.overlay { max-width:1200px; margin-left:auto; margin-right:auto; }
-/* Pagination: newer + older side by side at the bottom of the feed */
+/* Pagination: newer + older in a footer bar (below scrollable posts, like the header) */
.page-nav-dual { display:flex; justify-content:center; align-items:center; flex-wrap:wrap;
gap:clamp(16px,4vw,48px); }
+/* Flex column layout: let #post-content shrink so overflow-y scrolls; footer stays visible */
+#post-content.content { min-height:0; }
+.page-nav-footer { flex-shrink:0; width:100%; box-sizing:border-box; }
+.page-nav-footer .page-nav { margin:0; }
+/* ~Half-height footer bar vs default .page-nav padding */
+.page-nav-footer .page-nav a { padding-top:4px; padding-bottom:4px; }
/* Host note under the site subtitle (all themes) */
.logo-host { font-size:0.65rem; opacity:0.55; margin-top:4px; letter-spacing:0.3px; line-height:1.3; }
/* Atom feed link in header (paired with transmit in .nav) */
@@ -104,6 +110,12 @@ html.sno-splash-skip #splash-overlay { display:none !important; visibility:hidde
{{define "navscript"}}
<script>
+ const SNONUX_SOUNDS = {{.ThemeSoundsJSON}};
+ function snonuxWaveType(w) {
+ if (w === 'square') return 'square';
+ if (w === 'triangle') return 'triangle';
+ return 'sine';
+ }
(function splashSetup() {
var el = document.getElementById('splash-overlay');
if (!el) return;
@@ -117,7 +129,6 @@ html.sno-splash-skip #splash-overlay { display:none !important; visibility:hidde
}
var splashAudioCtx = null;
var splashChimePlayed = false;
- // Soft major arpeggio (G4 → C5 → E5 → G5); works once autopolicy allows audio.
function playSplashChime() {
if (splashChimePlayed) return;
try {
@@ -128,18 +139,22 @@ html.sno-splash-skip #splash-overlay { display:none !important; visibility:hidde
function ring() {
splashChimePlayed = true;
var now = ctx.currentTime;
- var freqs = [392, 523.25, 659.25, 783.99];
+ var sp = SNONUX_SOUNDS.splash;
+ var freqs = sp.freqs;
+ var spacing = sp.spacing != null ? sp.spacing : 0.075;
+ var gainAm = sp.gain != null ? sp.gain : 0.1;
+ var wave = snonuxWaveType(sp.wave);
var i, osc, g, t0;
for (i = 0; i < freqs.length; i++) {
osc = ctx.createOscillator();
g = ctx.createGain();
osc.connect(g);
g.connect(ctx.destination);
- osc.type = 'sine';
+ osc.type = wave;
osc.frequency.value = freqs[i];
- t0 = now + i * 0.075;
+ t0 = now + i * spacing;
g.gain.setValueAtTime(0, t0);
- g.gain.linearRampToValueAtTime(0.1, t0 + 0.028);
+ g.gain.linearRampToValueAtTime(gainAm, t0 + 0.028);
g.gain.exponentialRampToValueAtTime(0.001, t0 + 0.52);
osc.start(t0);
osc.stop(t0 + 0.55);
@@ -185,49 +200,56 @@ html.sno-splash-skip #splash-overlay { display:none !important; visibility:hidde
playNavSound();
}
- // playNavSound: short low beep for post selection (j/k navigation).
function playNavSound() {
try {
+ var n = SNONUX_SOUNDS.nav;
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.12, ctx.currentTime);
- gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.08);
- osc.start(ctx.currentTime); osc.stop(ctx.currentTime + 0.08);
+ osc.frequency.value = n.freq;
+ osc.type = snonuxWaveType(n.wave);
+ var dur = n.dur != null ? n.dur : 0.08;
+ var g = n.gain != null ? n.gain : 0.12;
+ gain.gain.setValueAtTime(g, ctx.currentTime);
+ gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + dur);
+ osc.start(ctx.currentTime); osc.stop(ctx.currentTime + dur + 0.02);
} catch (_) {}
}
- // playOpenSound: bright ascending chime when modal opens (Enter key).
function playOpenSound() {
try {
+ var o = SNONUX_SOUNDS.open;
const ctx = new (window.AudioContext || window.webkitAudioContext)();
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.connect(gain); gain.connect(ctx.destination);
- osc.type = 'triangle';
- osc.frequency.setValueAtTime(440, ctx.currentTime);
- osc.frequency.exponentialRampToValueAtTime(880, ctx.currentTime + 0.14);
- gain.gain.setValueAtTime(0.10, ctx.currentTime);
- gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.20);
- osc.start(ctx.currentTime); osc.stop(ctx.currentTime + 0.20);
+ osc.type = snonuxWaveType(o.wave);
+ var dur = o.dur != null ? o.dur : 0.14;
+ var g = o.gain != null ? o.gain : 0.1;
+ osc.frequency.setValueAtTime(o.start, ctx.currentTime);
+ osc.frequency.exponentialRampToValueAtTime(o.end, ctx.currentTime + dur);
+ gain.gain.setValueAtTime(g, ctx.currentTime);
+ gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + dur + 0.06);
+ osc.start(ctx.currentTime); osc.stop(ctx.currentTime + dur + 0.07);
} catch (_) {}
}
- // playCloseSound: descending sweep when modal closes (Esc key).
function playCloseSound() {
try {
+ var c = SNONUX_SOUNDS.close;
const ctx = new (window.AudioContext || window.webkitAudioContext)();
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.connect(gain); gain.connect(ctx.destination);
- osc.type = 'sine';
- osc.frequency.setValueAtTime(440, ctx.currentTime);
- osc.frequency.exponentialRampToValueAtTime(110, ctx.currentTime + 0.15);
- gain.gain.setValueAtTime(0.10, ctx.currentTime);
- gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.18);
- osc.start(ctx.currentTime); osc.stop(ctx.currentTime + 0.18);
+ osc.type = snonuxWaveType(c.wave);
+ var dur = c.dur != null ? c.dur : 0.15;
+ var g = c.gain != null ? c.gain : 0.1;
+ osc.frequency.setValueAtTime(c.start, ctx.currentTime);
+ osc.frequency.exponentialRampToValueAtTime(c.end, ctx.currentTime + dur);
+ gain.gain.setValueAtTime(g, ctx.currentTime);
+ gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + dur + 0.05);
+ osc.start(ctx.currentTime); osc.stop(ctx.currentTime + dur + 0.06);
} catch (_) {}
}
diff --git a/internal/generator/theme_aurora.go b/internal/generator/theme_aurora.go
index 887f936..058d52a 100644
--- a/internal/generator/theme_aurora.go
+++ b/internal/generator/theme_aurora.go
@@ -42,6 +42,9 @@ const auroraTemplate = `<!DOCTYPE html>
.page-nav a { border:1px solid var(--teal); color:var(--teal); padding:8px 20px;
border-radius:20px; text-decoration:none; font-size:0.82rem; letter-spacing:1px; }
.page-nav a:hover { background:var(--teal); color:var(--navy); }
+ .page-nav-footer { flex-shrink:0; padding:8px 28px; display:flex; justify-content:center;
+ background:rgba(5,13,26,0.78); backdrop-filter:blur(14px);
+ border-top:1px solid rgba(0,255,179,0.25); }
.post { background:rgba(5,20,35,0.72); border:1px solid rgba(0,255,179,0.2); border-radius:10px;
padding:20px; margin-bottom:14px; cursor:pointer;
transition:all 0.25s; backdrop-filter:blur(6px); }
@@ -141,13 +144,15 @@ const auroraTemplate = `<!DOCTYPE html>
<div class="post-text">{{$post.ContentHTML}}</div>
</div>
{{end}}
- {{if or .PrevPage .NextPage}}
+ </div>
+ {{if or .PrevPage .NextPage}}
+ <footer class="page-nav-footer" aria-label="Pagination">
<div class="page-nav page-nav-dual">
{{if .PrevPage}}<a href="{{.PrevPage}}">&larr; Newer</a>{{end}}
{{if .NextPage}}<a href="{{.NextPage}}">Older &rarr;</a>{{end}}
</div>
- {{end}}
- </div>
+ </footer>
+ {{end}}
</div>
{{template "navmodal" .}}
<script>
diff --git a/internal/generator/theme_brutalist.go b/internal/generator/theme_brutalist.go
index 2bb54da..1857615 100644
--- a/internal/generator/theme_brutalist.go
+++ b/internal/generator/theme_brutalist.go
@@ -44,6 +44,8 @@ const brutalistTemplate = `<!DOCTYPE html>
border-radius:0; text-decoration:none; font-family:Impact;
font-size:1rem; letter-spacing:2px; }
.page-nav a:hover { background:#fff; color:#000; }
+ .page-nav-footer { flex-shrink:0; padding:6px 24px; display:flex; justify-content:center;
+ background:#000; border-top:4px solid #fff; }
.post { background:#000; border:3px solid #fff; border-radius:0;
padding:20px 22px; margin-bottom:14px; cursor:pointer;
transition:border-color 0.1s,background 0.1s; }
@@ -82,7 +84,7 @@ const brutalistTemplate = `<!DOCTYPE html>
<canvas class="splash-gl-canvas" id="splash-gl-canvas" aria-hidden="true"></canvas>
<div class="splash-inner splash-frame">
<div class="splash-title">SNONUX.FOO</div>
- <div class="splash-tag">Brutalist channel</div>
+ <div class="splash-tag">Brutalist theme</div>
<div class="splash-hint">[ CLICK OR ENTER TO TRANSMIT ]</div>
</div>
</div>
@@ -131,13 +133,15 @@ const brutalistTemplate = `<!DOCTYPE html>
<div class="post-text">{{$post.ContentHTML}}</div>
</div>
{{end}}
- {{if or .PrevPage .NextPage}}
+ </div>
+ {{if or .PrevPage .NextPage}}
+ <footer class="page-nav-footer" aria-label="Pagination">
<div class="page-nav page-nav-dual">
{{if .PrevPage}}<a href="{{.PrevPage}}">&larr; NEWER</a>{{end}}
{{if .NextPage}}<a href="{{.NextPage}}">OLDER &rarr;</a>{{end}}
</div>
- {{end}}
- </div>
+ </footer>
+ {{end}}
</div>
{{template "navmodal" .}}
<script>
diff --git a/internal/generator/theme_glass.go b/internal/generator/theme_glass.go
index 40e3ee3..fef1f90 100644
--- a/internal/generator/theme_glass.go
+++ b/internal/generator/theme_glass.go
@@ -43,6 +43,9 @@ const cosmosTemplate = `<!DOCTYPE html>
.page-nav a { border:1px solid var(--purple); color:var(--purple); padding:8px 20px;
border-radius:20px; text-decoration:none; font-size:0.82rem; }
.page-nav a:hover { background:var(--purple); color:#fff; }
+ .page-nav-footer { flex-shrink:0; padding:8px 28px; display:flex; justify-content:center;
+ background:rgba(2,2,20,0.78); backdrop-filter:blur(14px);
+ border-top:1px solid rgba(255,209,102,0.2); }
.post { background:rgba(5,5,30,0.72); border:1px solid rgba(155,93,229,0.22); border-radius:10px;
padding:20px; margin-bottom:14px; cursor:pointer;
transition:all 0.25s; backdrop-filter:blur(6px); }
@@ -150,13 +153,15 @@ const cosmosTemplate = `<!DOCTYPE html>
<div class="post-text">{{$post.ContentHTML}}</div>
</div>
{{end}}
- {{if or .PrevPage .NextPage}}
+ </div>
+ {{if or .PrevPage .NextPage}}
+ <footer class="page-nav-footer" aria-label="Pagination">
<div class="page-nav page-nav-dual">
{{if .PrevPage}}<a href="{{.PrevPage}}">&larr; Newer</a>{{end}}
{{if .NextPage}}<a href="{{.NextPage}}">Older &rarr;</a>{{end}}
</div>
- {{end}}
- </div>
+ </footer>
+ {{end}}
</div>
{{template "navmodal" .}}
<script>
diff --git a/internal/generator/theme_matrix.go b/internal/generator/theme_matrix.go
index d964a11..37f629b 100644
--- a/internal/generator/theme_matrix.go
+++ b/internal/generator/theme_matrix.go
@@ -52,6 +52,8 @@ const matrixTemplate = `<!DOCTYPE html>
.page-nav a { border:1px solid var(--g2); color:var(--g); padding:7px 20px;
text-decoration:none; font-size:0.82rem; letter-spacing:2px; }
.page-nav a:hover { background:var(--g); color:var(--bg); }
+ .page-nav-footer { flex-shrink:0; padding:6px 24px; display:flex; justify-content:center;
+ background:#000; border-top:1px solid var(--g2); }
.post { background:#000; border:1px solid var(--g3); padding:16px 18px;
margin-bottom:10px; cursor:pointer; transition:border-color 0.15s; }
.post:hover { border-color:var(--g2); box-shadow:0 0 8px rgba(0,255,65,0.2); }
@@ -151,13 +153,15 @@ const matrixTemplate = `<!DOCTYPE html>
<div class="post-text">{{$post.ContentHTML}}</div>
</div>
{{end}}
- {{if or .PrevPage .NextPage}}
+ </div>
+ {{if or .PrevPage .NextPage}}
+ <footer class="page-nav-footer" aria-label="Pagination">
<div class="page-nav page-nav-dual">
{{if .PrevPage}}<a href="{{.PrevPage}}">&lt;-- NEWER</a>{{end}}
{{if .NextPage}}<a href="{{.NextPage}}">OLDER --&gt;</a>{{end}}
</div>
- {{end}}
- </div>
+ </footer>
+ {{end}}
</div>
{{template "navmodal" .}}
<script>
diff --git a/internal/generator/theme_minimal.go b/internal/generator/theme_minimal.go
index 4b7de26..6b5eceb 100644
--- a/internal/generator/theme_minimal.go
+++ b/internal/generator/theme_minimal.go
@@ -43,6 +43,9 @@ const plasmaTemplate = `<!DOCTYPE html>
.page-nav a { border:1px solid var(--cyan); color:var(--cyan); padding:8px 20px;
border-radius:20px; text-decoration:none; font-size:0.82rem; }
.page-nav a:hover { background:var(--cyan); color:var(--bg); }
+ .page-nav-footer { flex-shrink:0; padding:8px 28px; display:flex; justify-content:center;
+ background:rgba(5,0,8,0.8); backdrop-filter:blur(14px);
+ border-top:1px solid rgba(0,240,255,0.2); }
.post { background:rgba(10,0,20,0.75); border:1px solid rgba(0,240,255,0.18); border-radius:10px;
padding:20px; margin-bottom:14px; cursor:pointer;
transition:all 0.25s; backdrop-filter:blur(6px); }
@@ -146,13 +149,15 @@ const plasmaTemplate = `<!DOCTYPE html>
<div class="post-text">{{$post.ContentHTML}}</div>
</div>
{{end}}
- {{if or .PrevPage .NextPage}}
+ </div>
+ {{if or .PrevPage .NextPage}}
+ <footer class="page-nav-footer" aria-label="Pagination">
<div class="page-nav page-nav-dual">
{{if .PrevPage}}<a href="{{.PrevPage}}">&larr; Newer</a>{{end}}
{{if .NextPage}}<a href="{{.NextPage}}">Older &rarr;</a>{{end}}
</div>
- {{end}}
- </div>
+ </footer>
+ {{end}}
</div>
{{template "navmodal" .}}
<script>
diff --git a/internal/generator/theme_neon.go b/internal/generator/theme_neon.go
index eccf558..7ae0dae 100644
--- a/internal/generator/theme_neon.go
+++ b/internal/generator/theme_neon.go
@@ -40,6 +40,9 @@ const neonTemplate = `<!DOCTYPE html>
padding:10px 28px; border-radius:9999px; font-size:0.85rem; letter-spacing:2px;
text-decoration:none; transition:all 0.3s; }
.page-nav a:hover { background:var(--neon-cyan); color:#0b001a; }
+ .page-nav-footer { flex-shrink:0; padding:8px 30px; display:flex; justify-content:center;
+ background:rgba(11,0,26,0.8); backdrop-filter:blur(12px);
+ border-top:2px solid rgba(255,231,0,0.3); }
.post { background:rgba(20,5,45,0.9); border:2px solid transparent;
border-image:linear-gradient(45deg,var(--neon-cyan),var(--neon-magenta)) 1;
border-radius:24px; padding:28px; margin-bottom:28px;
@@ -197,13 +200,15 @@ const neonTemplate = `<!DOCTYPE html>
<div class="post-text">{{$post.ContentHTML}}</div>
</div>
{{end}}
- {{if or .PrevPage .NextPage}}
+ </div>
+ {{if or .PrevPage .NextPage}}
+ <footer class="page-nav-footer" aria-label="Pagination">
<div class="page-nav page-nav-dual">
{{if .PrevPage}}<a href="{{.PrevPage}}">&larr; NEWER TRANSMISSIONS</a>{{end}}
{{if .NextPage}}<a href="{{.NextPage}}">OLDER TRANSMISSIONS &rarr;</a>{{end}}
</div>
- {{end}}
- </div>
+ </footer>
+ {{end}}
</div>
{{template "navmodal" .}}
<script>
diff --git a/internal/generator/theme_ocean.go b/internal/generator/theme_ocean.go
index 76469e5..943701a 100644
--- a/internal/generator/theme_ocean.go
+++ b/internal/generator/theme_ocean.go
@@ -41,6 +41,9 @@ const oceanTemplate = `<!DOCTYPE html>
.page-nav a { border:1px solid var(--deep); color:var(--aqua); padding:8px 20px;
border-radius:20px; text-decoration:none; font-size:0.82rem; }
.page-nav a:hover { background:var(--teal); color:var(--navy); }
+ .page-nav-footer { flex-shrink:0; padding:8px 28px; display:flex; justify-content:center;
+ background:rgba(3,4,94,0.82); backdrop-filter:blur(12px);
+ border-top:1px solid rgba(0,180,216,0.3); }
.post { background:rgba(3,4,94,0.55); border:1px solid rgba(0,180,216,0.22); border-radius:10px;
padding:20px; margin-bottom:14px; cursor:pointer;
transition:all 0.25s; backdrop-filter:blur(6px); }
@@ -137,13 +140,15 @@ const oceanTemplate = `<!DOCTYPE html>
<div class="post-text">{{$post.ContentHTML}}</div>
</div>
{{end}}
- {{if or .PrevPage .NextPage}}
+ </div>
+ {{if or .PrevPage .NextPage}}
+ <footer class="page-nav-footer" aria-label="Pagination">
<div class="page-nav page-nav-dual">
{{if .PrevPage}}<a href="{{.PrevPage}}">&larr; Newer</a>{{end}}
{{if .NextPage}}<a href="{{.NextPage}}">Older &rarr;</a>{{end}}
</div>
- {{end}}
- </div>
+ </footer>
+ {{end}}
</div>
{{template "navmodal" .}}
<script>
diff --git a/internal/generator/theme_paper.go b/internal/generator/theme_paper.go
index 02b0bb8..2b91784 100644
--- a/internal/generator/theme_paper.go
+++ b/internal/generator/theme_paper.go
@@ -41,6 +41,9 @@ const volcanoTemplate = `<!DOCTYPE html>
.page-nav a { border:1px solid var(--ember); color:var(--ember); padding:8px 20px;
border-radius:4px; text-decoration:none; font-size:0.82rem; }
.page-nav a:hover { background:var(--lava); color:var(--bg); }
+ .page-nav-footer { flex-shrink:0; padding:7px 28px; display:flex; justify-content:center;
+ background:rgba(13,8,2,0.82); backdrop-filter:blur(12px);
+ border-top:1px solid rgba(255,68,0,0.3); }
.post { background:rgba(20,8,2,0.72); border:1px solid rgba(255,68,0,0.2); border-radius:8px;
padding:20px; margin-bottom:14px; cursor:pointer;
transition:all 0.25s; backdrop-filter:blur(4px); }
@@ -140,13 +143,15 @@ const volcanoTemplate = `<!DOCTYPE html>
<div class="post-text">{{$post.ContentHTML}}</div>
</div>
{{end}}
- {{if or .PrevPage .NextPage}}
+ </div>
+ {{if or .PrevPage .NextPage}}
+ <footer class="page-nav-footer" aria-label="Pagination">
<div class="page-nav page-nav-dual">
{{if .PrevPage}}<a href="{{.PrevPage}}">&larr; Newer</a>{{end}}
{{if .NextPage}}<a href="{{.NextPage}}">Older &rarr;</a>{{end}}
</div>
- {{end}}
- </div>
+ </footer>
+ {{end}}
</div>
{{template "navmodal" .}}
<script>
diff --git a/internal/generator/theme_retro.go b/internal/generator/theme_retro.go
index 563e37b..37ff0b6 100644
--- a/internal/generator/theme_retro.go
+++ b/internal/generator/theme_retro.go
@@ -53,6 +53,8 @@ const retroTemplate = `<!DOCTYPE html>
.page-nav a { border:1px solid var(--dim); color:var(--amber); padding:7px 20px;
text-decoration:none; font-size:0.82rem; letter-spacing:2px; }
.page-nav a:hover { background:var(--amber); color:var(--bg); border-color:var(--amber); }
+ .page-nav-footer { flex-shrink:0; padding:6px 24px; display:flex; justify-content:center;
+ background:var(--bg2); border-top:2px solid var(--amber); }
.post { background:var(--bg); border:1px solid var(--dim); padding:16px 18px;
margin-bottom:10px; cursor:pointer; transition:border-color 0.15s; }
.post:hover { border-color:var(--amber); box-shadow:0 0 8px rgba(255,176,0,0.25); }
@@ -147,13 +149,15 @@ const retroTemplate = `<!DOCTYPE html>
<div class="post-text">{{$post.ContentHTML}}</div>
</div>
{{end}}
- {{if or .PrevPage .NextPage}}
+ </div>
+ {{if or .PrevPage .NextPage}}
+ <footer class="page-nav-footer" aria-label="Pagination">
<div class="page-nav page-nav-dual">
{{if .PrevPage}}<a href="{{.PrevPage}}">&lt;-- NEWER</a>{{end}}
{{if .NextPage}}<a href="{{.NextPage}}">OLDER --&gt;</a>{{end}}
</div>
- {{end}}
- </div>
+ </footer>
+ {{end}}
</div>
{{template "navmodal" .}}
<script>
diff --git a/internal/generator/theme_sounds.go b/internal/generator/theme_sounds.go
new file mode 100644
index 0000000..e41e7a5
--- /dev/null
+++ b/internal/generator/theme_sounds.go
@@ -0,0 +1,179 @@
+package generator
+
+import (
+ "encoding/json"
+ "html/template"
+)
+
+// themeSounds is serialized into each page for Web Audio (splash + keyboard nav).
+// Wave: "sine" | "triangle" | "square".
+type themeSounds struct {
+ Splash struct {
+ Freqs []float64 `json:"freqs"`
+ Spacing float64 `json:"spacing"`
+ Gain float64 `json:"gain"`
+ Wave string `json:"wave"`
+ } `json:"splash"`
+ Nav struct {
+ Freq float64 `json:"freq"`
+ Wave string `json:"wave"`
+ Dur float64 `json:"dur"`
+ Gain float64 `json:"gain"`
+ } `json:"nav"`
+ Open struct {
+ Wave string `json:"wave"`
+ Start float64 `json:"start"`
+ End float64 `json:"end"`
+ Dur float64 `json:"dur"`
+ Gain float64 `json:"gain"`
+ } `json:"open"`
+ Close struct {
+ Wave string `json:"wave"`
+ Start float64 `json:"start"`
+ End float64 `json:"end"`
+ Dur float64 `json:"dur"`
+ Gain float64 `json:"gain"`
+ } `json:"close"`
+}
+
+// themeSoundPresets maps CLI theme names to synth parameters (see themes.go registry).
+var themeSoundPresets = map[string]themeSounds{
+ "neon": soundsNeon(),
+ "terminal": soundsTerminal(),
+ "synthwave": soundsSynthwave(),
+ "plasma": soundsPlasma(),
+ "brutalist": soundsBrutalist(),
+ "volcano": soundsVolcano(),
+ "aurora": soundsAurora(),
+ "matrix": soundsMatrix(),
+ "ocean": soundsOcean(),
+ "retro": soundsRetro(),
+ "cosmos": soundsCosmos(),
+}
+
+func soundsNeon() themeSounds {
+ var s themeSounds
+ s.Splash.Freqs = []float64{523.25, 659.25, 783.99, 1046.5}
+ s.Splash.Spacing, s.Splash.Gain, s.Splash.Wave = 0.055, 0.09, "sine"
+ s.Nav.Freq, s.Nav.Wave, s.Nav.Dur, s.Nav.Gain = 330, "square", 0.055, 0.11
+ s.Open.Wave, s.Open.Start, s.Open.End, s.Open.Dur, s.Open.Gain = "triangle", 523.25, 1046.5, 0.13, 0.1
+ s.Close.Wave, s.Close.Start, s.Close.End, s.Close.Dur, s.Close.Gain = "sine", 880, 261.63, 0.16, 0.09
+ return s
+}
+
+func soundsTerminal() themeSounds {
+ var s themeSounds
+ s.Splash.Freqs = []float64{523.25, 659.25, 783.99}
+ s.Splash.Spacing, s.Splash.Gain, s.Splash.Wave = 0.09, 0.11, "square"
+ s.Nav.Freq, s.Nav.Wave, s.Nav.Dur, s.Nav.Gain = 800, "square", 0.045, 0.12
+ s.Open.Wave, s.Open.Start, s.Open.End, s.Open.Dur, s.Open.Gain = "square", 600, 1200, 0.12, 0.1
+ s.Close.Wave, s.Close.Start, s.Close.End, s.Close.Dur, s.Close.Gain = "square", 900, 400, 0.14, 0.09
+ return s
+}
+
+func soundsSynthwave() themeSounds {
+ var s themeSounds
+ s.Splash.Freqs = []float64{196, 246.94, 293.66, 349.23}
+ s.Splash.Spacing, s.Splash.Gain, s.Splash.Wave = 0.1, 0.1, "sine"
+ s.Nav.Freq, s.Nav.Wave, s.Nav.Dur, s.Nav.Gain = 164.81, "triangle", 0.09, 0.1
+ s.Open.Wave, s.Open.Start, s.Open.End, s.Open.Dur, s.Open.Gain = "sine", 220, 440, 0.18, 0.1
+ s.Close.Wave, s.Close.Start, s.Close.End, s.Close.Dur, s.Close.Gain = "sine", 440, 110, 0.17, 0.09
+ return s
+}
+
+func soundsPlasma() themeSounds {
+ var s themeSounds
+ s.Splash.Freqs = []float64{311.13, 415.3, 466.16, 622.25}
+ s.Splash.Spacing, s.Splash.Gain, s.Splash.Wave = 0.08, 0.095, "triangle"
+ s.Nav.Freq, s.Nav.Wave, s.Nav.Dur, s.Nav.Gain = 246.94, "sine", 0.085, 0.11
+ s.Open.Wave, s.Open.Start, s.Open.End, s.Open.Dur, s.Open.Gain = "triangle", 349.23, 698.46, 0.15, 0.1
+ s.Close.Wave, s.Close.Start, s.Close.End, s.Close.Dur, s.Close.Gain = "sine", 523.25, 174.61, 0.17, 0.09
+ return s
+}
+
+func soundsBrutalist() themeSounds {
+ var s themeSounds
+ s.Splash.Freqs = []float64{100, 150, 200, 120}
+ s.Splash.Spacing, s.Splash.Gain, s.Splash.Wave = 0.07, 0.14, "square"
+ s.Nav.Freq, s.Nav.Wave, s.Nav.Dur, s.Nav.Gain = 120, "square", 0.07, 0.13
+ s.Open.Wave, s.Open.Start, s.Open.End, s.Open.Dur, s.Open.Gain = "square", 200, 400, 0.12, 0.11
+ s.Close.Wave, s.Close.Start, s.Close.End, s.Close.Dur, s.Close.Gain = "square", 400, 100, 0.14, 0.1
+ return s
+}
+
+func soundsVolcano() themeSounds {
+ var s themeSounds
+ s.Splash.Freqs = []float64{196, 246.94, 293.66, 349.23}
+ s.Splash.Spacing, s.Splash.Gain, s.Splash.Wave = 0.08, 0.1, "sine"
+ s.Nav.Freq, s.Nav.Wave, s.Nav.Dur, s.Nav.Gain = 180, "sine", 0.09, 0.11
+ s.Open.Wave, s.Open.Start, s.Open.End, s.Open.Dur, s.Open.Gain = "triangle", 261.63, 523.25, 0.16, 0.1
+ s.Close.Wave, s.Close.Start, s.Close.End, s.Close.Dur, s.Close.Gain = "sine", 392, 98, 0.17, 0.09
+ return s
+}
+
+func soundsAurora() themeSounds {
+ var s themeSounds
+ s.Splash.Freqs = []float64{659.25, 880, 987.77, 1046.5}
+ s.Splash.Spacing, s.Splash.Gain, s.Splash.Wave = 0.07, 0.085, "sine"
+ s.Nav.Freq, s.Nav.Wave, s.Nav.Dur, s.Nav.Gain = 440, "sine", 0.1, 0.09
+ s.Open.Wave, s.Open.Start, s.Open.End, s.Open.Dur, s.Open.Gain = "sine", 523.25, 880, 0.2, 0.09
+ s.Close.Wave, s.Close.Start, s.Close.End, s.Close.Dur, s.Close.Gain = "sine", 704, 352, 0.18, 0.085
+ return s
+}
+
+func soundsMatrix() themeSounds {
+ var s themeSounds
+ s.Splash.Freqs = []float64{523.25, 587.33, 659.25, 698.46}
+ s.Splash.Spacing, s.Splash.Gain, s.Splash.Wave = 0.05, 0.09, "square"
+ s.Nav.Freq, s.Nav.Wave, s.Nav.Dur, s.Nav.Gain = 523.25, "square", 0.045, 0.11
+ s.Open.Wave, s.Open.Start, s.Open.End, s.Open.Dur, s.Open.Gain = "square", 880, 1318.5, 0.11, 0.1
+ s.Close.Wave, s.Close.Start, s.Close.End, s.Close.Dur, s.Close.Gain = "square", 880, 330, 0.13, 0.09
+ return s
+}
+
+func soundsOcean() themeSounds {
+ var s themeSounds
+ s.Splash.Freqs = []float64{174.61, 196, 220, 246.94}
+ s.Splash.Spacing, s.Splash.Gain, s.Splash.Wave = 0.095, 0.095, "sine"
+ s.Nav.Freq, s.Nav.Wave, s.Nav.Dur, s.Nav.Gain = 196, "triangle", 0.1, 0.09
+ s.Open.Wave, s.Open.Start, s.Open.End, s.Open.Dur, s.Open.Gain = "sine", 349.23, 523.25, 0.2, 0.09
+ s.Close.Wave, s.Close.Start, s.Close.End, s.Close.Dur, s.Close.Gain = "sine", 415.3, 246.94, 0.18, 0.085
+ return s
+}
+
+func soundsRetro() themeSounds {
+ var s themeSounds
+ s.Splash.Freqs = []float64{1046.5, 1318.5}
+ s.Splash.Spacing, s.Splash.Gain, s.Splash.Wave = 0.12, 0.1, "square"
+ s.Nav.Freq, s.Nav.Wave, s.Nav.Dur, s.Nav.Gain = 1200, "square", 0.04, 0.11
+ s.Open.Wave, s.Open.Start, s.Open.End, s.Open.Dur, s.Open.Gain = "square", 800, 1600, 0.14, 0.1
+ s.Close.Wave, s.Close.Start, s.Close.End, s.Close.Dur, s.Close.Gain = "square", 1600, 400, 0.15, 0.09
+ return s
+}
+
+func soundsCosmos() themeSounds {
+ var s themeSounds
+ s.Splash.Freqs = []float64{220, 277.18, 329.63, 392}
+ s.Splash.Spacing, s.Splash.Gain, s.Splash.Wave = 0.09, 0.09, "sine"
+ s.Nav.Freq, s.Nav.Wave, s.Nav.Dur, s.Nav.Gain = 277.18, "sine", 0.09, 0.09
+ s.Open.Wave, s.Open.Start, s.Open.End, s.Open.Dur, s.Open.Gain = "triangle", 392, 587.33, 0.22, 0.09
+ s.Close.Wave, s.Close.Start, s.Close.End, s.Close.Dur, s.Close.Gain = "sine", 587.33, 196, 0.2, 0.085
+ return s
+}
+
+func defaultSounds() themeSounds {
+ return soundsNeon()
+}
+
+// themeSoundsJSON returns a JS object literal for embedding in <script> (safe JSON).
+func themeSoundsJSON(themeName string) template.JS {
+ p := defaultSounds()
+ if x, ok := themeSoundPresets[themeName]; ok {
+ p = x
+ }
+ b, err := json.Marshal(p)
+ if err != nil {
+ b, _ = json.Marshal(defaultSounds())
+ }
+ return template.JS(b) //nolint:gosec // JSON from fixed structs
+}
diff --git a/internal/generator/theme_synthwave.go b/internal/generator/theme_synthwave.go
index 94aaf55..c616f6f 100644
--- a/internal/generator/theme_synthwave.go
+++ b/internal/generator/theme_synthwave.go
@@ -46,6 +46,9 @@ const synthwaveTemplate = `<!DOCTYPE html>
.page-nav a { border:2px solid var(--purple); color:var(--purple); padding:8px 22px;
border-radius:4px; text-decoration:none; letter-spacing:2px; font-size:0.82rem; }
.page-nav a:hover { background:var(--purple); color:#fff; }
+ .page-nav-footer { flex-shrink:0; padding:8px 28px; display:flex; justify-content:center;
+ background:rgba(13,2,33,0.82); backdrop-filter:blur(10px);
+ border-top:2px solid var(--pink); }
.post { background:rgba(20,5,50,0.85); border:1px solid var(--purple); border-radius:6px;
padding:22px; margin-bottom:18px; cursor:pointer; transition:all 0.25s; }
.post:hover { border-color:var(--pink); box-shadow:0 0 22px rgba(255,45,120,0.35); transform:translateY(-3px); }
@@ -156,13 +159,15 @@ const synthwaveTemplate = `<!DOCTYPE html>
<div class="post-text">{{$post.ContentHTML}}</div>
</div>
{{end}}
- {{if or .PrevPage .NextPage}}
+ </div>
+ {{if or .PrevPage .NextPage}}
+ <footer class="page-nav-footer" aria-label="Pagination">
<div class="page-nav page-nav-dual">
{{if .PrevPage}}<a href="{{.PrevPage}}">&larr; NEWER</a>{{end}}
{{if .NextPage}}<a href="{{.NextPage}}">OLDER &rarr;</a>{{end}}
</div>
- {{end}}
- </div>
+ </footer>
+ {{end}}
</div>
{{template "navmodal" .}}
<script>
diff --git a/internal/generator/theme_terminal.go b/internal/generator/theme_terminal.go
index 9464664..8096b19 100644
--- a/internal/generator/theme_terminal.go
+++ b/internal/generator/theme_terminal.go
@@ -51,6 +51,8 @@ const terminalTemplate = `<!DOCTYPE html>
.page-nav a { border:1px solid var(--dim); color:var(--p); padding:7px 20px;
border-radius:0; text-decoration:none; letter-spacing:2px; font-size:0.82rem; }
.page-nav a:hover { background:var(--p); color:var(--bg); border-color:var(--p); }
+ .page-nav-footer { flex-shrink:0; padding:6px 24px; display:flex; justify-content:center;
+ background:var(--bg2); border-top:2px solid var(--p); }
.post { background:var(--bg); border:1px solid var(--dim); border-radius:0;
padding:18px 20px; margin-bottom:12px; cursor:pointer; transition:border-color 0.15s; }
.post:hover { border-color:var(--p); box-shadow:0 0 8px rgba(51,255,51,0.3); }
@@ -137,13 +139,15 @@ const terminalTemplate = `<!DOCTYPE html>
<div class="post-text">{{$post.ContentHTML}}</div>
</div>
{{end}}
- {{if or .PrevPage .NextPage}}
+ </div>
+ {{if or .PrevPage .NextPage}}
+ <footer class="page-nav-footer" aria-label="Pagination">
<div class="page-nav page-nav-dual">
{{if .PrevPage}}<a href="{{.PrevPage}}">&lt;-- NEWER</a>{{end}}
{{if .NextPage}}<a href="{{.NextPage}}">OLDER --&gt;</a>{{end}}
</div>
- {{end}}
- </div>
+ </footer>
+ {{end}}
</div>
{{template "navmodal" .}}
<script>
diff --git a/internal/version.go b/internal/version.go
index 9c12eae..c757f0a 100644
--- a/internal/version.go
+++ b/internal/version.go
@@ -2,4 +2,4 @@
package version
// Version is the application version (semantic versioning).
-const Version = "0.1.0"
+const Version = "0.1.1"