diff options
| author | Paul Buetow <paul@buetow.org> | 2026-04-10 10:29:33 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-04-10 10:29:33 +0300 |
| commit | b4899f8a322c5df78731e3c5b6d583ec0835d129 (patch) | |
| tree | 858034ff76e3aaf43c6821f9ae5a18298a978844 /internal | |
| parent | f40fee44e8f256328ca1419863b5441123a1014e (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.go | 2 | ||||
| -rw-r--r-- | internal/generator/generator.go | 26 | ||||
| -rw-r--r-- | internal/generator/generator_test.go | 19 | ||||
| -rw-r--r-- | internal/generator/shared.go | 74 | ||||
| -rw-r--r-- | internal/generator/theme_aurora.go | 11 | ||||
| -rw-r--r-- | internal/generator/theme_brutalist.go | 12 | ||||
| -rw-r--r-- | internal/generator/theme_glass.go | 11 | ||||
| -rw-r--r-- | internal/generator/theme_matrix.go | 10 | ||||
| -rw-r--r-- | internal/generator/theme_minimal.go | 11 | ||||
| -rw-r--r-- | internal/generator/theme_neon.go | 11 | ||||
| -rw-r--r-- | internal/generator/theme_ocean.go | 11 | ||||
| -rw-r--r-- | internal/generator/theme_paper.go | 11 | ||||
| -rw-r--r-- | internal/generator/theme_retro.go | 10 | ||||
| -rw-r--r-- | internal/generator/theme_sounds.go | 179 | ||||
| -rw-r--r-- | internal/generator/theme_synthwave.go | 11 | ||||
| -rw-r--r-- | internal/generator/theme_terminal.go | 10 | ||||
| -rw-r--r-- | internal/version.go | 2 |
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}}">← Newer</a>{{end}} {{if .NextPage}}<a href="{{.NextPage}}">Older →</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}}">← NEWER</a>{{end}} {{if .NextPage}}<a href="{{.NextPage}}">OLDER →</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}}">← Newer</a>{{end}} {{if .NextPage}}<a href="{{.NextPage}}">Older →</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}}"><-- NEWER</a>{{end}} {{if .NextPage}}<a href="{{.NextPage}}">OLDER --></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}}">← Newer</a>{{end}} {{if .NextPage}}<a href="{{.NextPage}}">Older →</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}}">← NEWER TRANSMISSIONS</a>{{end}} {{if .NextPage}}<a href="{{.NextPage}}">OLDER TRANSMISSIONS →</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}}">← Newer</a>{{end}} {{if .NextPage}}<a href="{{.NextPage}}">Older →</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}}">← Newer</a>{{end}} {{if .NextPage}}<a href="{{.NextPage}}">Older →</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}}"><-- NEWER</a>{{end}} {{if .NextPage}}<a href="{{.NextPage}}">OLDER --></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}}">← NEWER</a>{{end}} {{if .NextPage}}<a href="{{.NextPage}}">OLDER →</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}}"><-- NEWER</a>{{end}} {{if .NextPage}}<a href="{{.NextPage}}">OLDER --></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" |
