summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-04-10 15:42:11 +0300
committerPaul Buetow <paul@buetow.org>2026-04-10 15:42:11 +0300
commit150f8058d64e3e7545f3f81962441ed6d3a3ad9d (patch)
treea1432e3a909eebfc6120efdb39b8c9da7bf14101
parent14a4c2e29c3d96204de54b636e245eff78f0ef9d (diff)
Release v0.1.5v0.1.5
--sync rsync to pi mirrors when reachable; default dirs ./inbox ./dist; ignore inbox/dist. Splash index paths (/, trailing slash). PgUp/PgDn scroll + highlight. Nav shared CSS in head for valid HTML; ARIA/viewport fixes; neon viewport. Made-with: Cursor
-rw-r--r--.gitignore2
-rw-r--r--cmd/snonux/main.go11
-rw-r--r--cmd/snonux/main_test.go22
-rw-r--r--cmd/snonux/sync.go71
-rw-r--r--internal/config/config.go3
-rw-r--r--internal/generator/doc.go3
-rw-r--r--internal/generator/shared.go81
-rw-r--r--internal/generator/theme_aurora.go3
-rw-r--r--internal/generator/theme_brutalist.go3
-rw-r--r--internal/generator/theme_glass.go3
-rw-r--r--internal/generator/theme_matrix.go3
-rw-r--r--internal/generator/theme_minimal.go3
-rw-r--r--internal/generator/theme_neon.go5
-rw-r--r--internal/generator/theme_ocean.go3
-rw-r--r--internal/generator/theme_paper.go3
-rw-r--r--internal/generator/theme_retro.go3
-rw-r--r--internal/generator/theme_synthwave.go3
-rw-r--r--internal/generator/theme_terminal.go3
-rw-r--r--internal/generator/themes.go3
-rw-r--r--internal/version/version.go2
20 files changed, 197 insertions, 36 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..60dcd17
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+inbox/
+dist/
diff --git a/cmd/snonux/main.go b/cmd/snonux/main.go
index 1d00646..42a377f 100644
--- a/cmd/snonux/main.go
+++ b/cmd/snonux/main.go
@@ -4,7 +4,7 @@
//
// Usage:
//
-// snonux --input ./inbox --output ./outdir [--base-url https://snonux.foo]
+// snonux [--input ./inbox] [--output ./dist] [--base-url https://snonux.foo]
package main
import (
@@ -51,6 +51,12 @@ func main() {
if err := run(cfg); err != nil {
log.Fatalf("error: %v", err)
}
+
+ if cfg.Sync {
+ if err := syncOutput(cfg.OutputDir); err != nil {
+ log.Fatalf("error: %v", err)
+ }
+ }
}
// errParseFlags is returned when flag parsing fails (e.g. unknown flag).
@@ -69,9 +75,10 @@ func parseFlags(args []string) (*config.Config, cliMode, error) {
listThemes := fs.Bool("list-themes", false, "print all available theme names and exit")
fs.StringVar(&cfg.InputDir, "input", "./inbox", "directory containing new source files to process")
- fs.StringVar(&cfg.OutputDir, "output", "~/git/snonux.foo/dist", "root directory for generated static site output")
+ fs.StringVar(&cfg.OutputDir, "output", "./dist", "root directory for generated static site output")
fs.StringVar(&cfg.BaseURL, "base-url", "https://snonux.foo", "canonical base URL used in Atom feed links")
fs.StringVar(&cfg.Theme, "theme", "random", "visual theme name, or \"random\" to pick one at random")
+ fs.BoolVar(&cfg.Sync, "sync", false, "after a successful run, rsync -output to pi0/pi1 when both are pingable (SSH user: SNONUX_SYNC_USER or login name)")
if err := fs.Parse(args); err != nil {
return nil, modeRun, fmt.Errorf("%w: %w", errParseFlags, err)
diff --git a/cmd/snonux/main_test.go b/cmd/snonux/main_test.go
index 212efc8..7f05c8b 100644
--- a/cmd/snonux/main_test.go
+++ b/cmd/snonux/main_test.go
@@ -104,6 +104,28 @@ func TestParseFlags_run(t *testing.T) {
}
}
+func TestParseFlags_sync(t *testing.T) {
+ t.Parallel()
+
+ in := t.TempDir()
+ out := t.TempDir()
+ cfg, mode, err := parseFlags([]string{
+ "-input", in,
+ "-output", out,
+ "-theme", "neon",
+ "-sync",
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+ if mode != modeRun {
+ t.Fatalf("mode %v", mode)
+ }
+ if !cfg.Sync {
+ t.Fatal("expected cfg.Sync")
+ }
+}
+
func TestParseFlags_randomTheme(t *testing.T) {
t.Parallel()
diff --git a/cmd/snonux/sync.go b/cmd/snonux/sync.go
new file mode 100644
index 0000000..46993e2
--- /dev/null
+++ b/cmd/snonux/sync.go
@@ -0,0 +1,71 @@
+package main
+
+import (
+ "context"
+ "fmt"
+ "log"
+ "os"
+ "os/exec"
+ "os/user"
+ "path/filepath"
+ "time"
+)
+
+// SNONUX_SYNC_USER overrides the SSH username for rsync (default: current login name).
+const envSyncUser = "SNONUX_SYNC_USER"
+
+var syncTargets = []string{
+ "pi0.lan.buetow.org",
+ "pi1.lan.buetow.org",
+}
+
+const syncRemoteDir = "/var/www/html/snonux/"
+
+// syncOutput rsyncs localOutput (trailing-slash source) to each sync target over SSH
+// port 22. It runs only if every target answers ICMP ping (Linux iputils: ping -c 1 -W …).
+func syncOutput(localOutput string) error {
+ for _, host := range syncTargets {
+ if !hostPingable(host) {
+ log.Printf("sync skipped: %q not pingable (all mirror hosts must be reachable)", host)
+ return nil
+ }
+ }
+
+ sshUser := os.Getenv(envSyncUser)
+ if sshUser == "" {
+ u, err := user.Current()
+ if err != nil {
+ return fmt.Errorf("sync user: %w (set %s)", err, envSyncUser)
+ }
+ sshUser = u.Username
+ }
+
+ absOut, err := filepath.Abs(localOutput)
+ if err != nil {
+ return fmt.Errorf("sync output dir: %w", err)
+ }
+ src := filepath.Clean(absOut) + string(filepath.Separator)
+
+ ssh := "ssh -p 22 -o BatchMode=yes -o ConnectTimeout=15"
+ for _, host := range syncTargets {
+ dest := fmt.Sprintf("%s@%s:%s", sshUser, host, syncRemoteDir)
+ log.Printf("rsync %s -> %s", src, dest)
+ cmd := exec.Command("rsync", "-az", "-e", ssh, src, dest)
+ cmd.Stdout = os.Stdout
+ cmd.Stderr = os.Stderr
+ if err := cmd.Run(); err != nil {
+ return fmt.Errorf("rsync to %s: %w", host, err)
+ }
+ }
+ return nil
+}
+
+func hostPingable(host string) bool {
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+ // Linux iputils-ping: -c 1 one packet, -W 3 wait up to 3s for reply.
+ cmd := exec.CommandContext(ctx, "ping", "-c", "1", "-W", "3", host)
+ cmd.Stdout = nil
+ cmd.Stderr = nil
+ return cmd.Run() == nil
+}
diff --git a/internal/config/config.go b/internal/config/config.go
index fd6e560..f52b445 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -23,4 +23,7 @@ type Config struct {
// Theme selects the visual style for generated HTML pages.
// Defaults to "neon". Run with --help to see all available themes.
Theme string
+
+ // Sync, when true, rsyncs OutputDir to fixed mirror hosts after a successful run.
+ Sync bool
}
diff --git a/internal/generator/doc.go b/internal/generator/doc.go
index ad974ad..d7d4a53 100644
--- a/internal/generator/doc.go
+++ b/internal/generator/doc.go
@@ -9,7 +9,8 @@
// - themes.go — Theme registry (name → template string) and getTheme /
// ListThemes for the CLI.
// - theme_*.go — One file per visual theme: full-page HTML that invokes
-// {{template "splashGate"}}, {{template "navhints" .}}, {{template "navmodal" .}},
+// {{template "navSharedCSSInner"}} inside <style>, then {{template "splashGate"}},
+// {{template "navhints" .}}, {{template "navmodal" .}},
// {{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.
diff --git a/internal/generator/shared.go b/internal/generator/shared.go
index 5a264c3..b7cd4ed 100644
--- a/internal/generator/shared.go
+++ b/internal/generator/shared.go
@@ -3,12 +3,14 @@ package generator
// navDefs is appended to every theme template when parsing.
// It defines named sub-templates shared across all themes:
// - "splashGate" — synchronous script: first child of <body>; sets html.sno-splash-skip when
-// splash should not run (?splash=0, not index.html, or Referer from same-site index/pageN).
+// splash should not run (?splash=0, not index path, or Referer from same-site index/pageN).
// - "navhints" — keyboard shortcut hint bar HTML
-// - "navmodal" — full-screen expanded-post modal HTML + image-sizing CSS
+// - "navSharedCSSInner" — shared CSS (injected inside each theme’s <style> in <head>)
+// - "navmodal" — full-screen expanded-post modal HTML (no <style>; CSS lives in head)
// - "navscript" — keyboard navigation + Web Audio; splash/nav/modal sounds from themeSoundsJSON (per theme)
//
-// Each theme calls {{template "splashGate"}}, {{template "navhints" .}}, {{template "navmodal" .}},
+// Each theme ends its <style> with {{template "navSharedCSSInner"}} then calls
+// {{template "splashGate"}}, {{template "navhints" .}}, {{template "navmodal" .}},
// and {{template "navscript" .}} at the appropriate points in its HTML.
// All theme-specific CSS lives in each theme file so themes stay self-contained.
const navDefs = `
@@ -22,20 +24,27 @@ const navDefs = `
return;
}
} catch (_) {}
- var parts = location.pathname.split('/').filter(function(s) { return s.length; });
- var seg = (parts.length ? parts[parts.length - 1] : '').toLowerCase();
- var onIndex = (!seg || seg === 'index.html');
+ function isIndexLikePath(pathname) {
+ var p = pathname || '/';
+ if (p === '' || p === '/') return true;
+ var parts = p.split('/').filter(function(s) { return s.length; });
+ if (parts.length === 0) return true;
+ var last = parts[parts.length - 1].toLowerCase();
+ if (last === 'index.html' || last === 'index.htm') return true;
+ if (p.endsWith('/') && parts.length === 1) return true;
+ return false;
+ }
+ var onIndex = isIndexLikePath(location.pathname);
var ref = document.referrer;
function refIsSameSiteBlogPage(url) {
if (!url) return false;
try {
var ru = new URL(url), cu = new URL(location.href);
if (ru.origin !== cu.origin) return false;
+ if (isIndexLikePath(ru.pathname)) return true;
var rp = ru.pathname.split('/').filter(function(s) { return s.length; });
var rs = (rp.length ? rp[rp.length - 1] : '').toLowerCase();
- if (rs === 'index.html' || rs === '') return true;
- if (/^page\d+\.html$/.test(rs)) return true;
- return false;
+ return /^page\d+\.html$/.test(rs);
} catch (_) { return false; }
}
if (!onIndex || refIsSameSiteBlogPage(ref)) document.documentElement.classList.add('sno-splash-skip');
@@ -44,7 +53,7 @@ const navDefs = `
{{end}}
{{define "navhints"}}
-<div class="nav-hints" aria-label="keyboard shortcuts">
+<div class="nav-hints" role="region" aria-label="Keyboard shortcuts">
<span><kbd>j</kbd><kbd>k</kbd> or <kbd>↑</kbd><kbd>↓</kbd> select post</span>
<span><kbd>PgUp</kbd><kbd>PgDn</kbd> scroll</span>
<span><kbd>Enter</kbd> expand</span>
@@ -53,8 +62,7 @@ const navDefs = `
</div>
{{end}}
-{{define "navmodal"}}
-<style>
+{{define "navSharedCSSInner"}}
/* Thumbnail sizing in list view; modal overrides to full width. */
.post-image { max-height:220px; max-width:100%; object-fit:cover; cursor:pointer; }
#post-modal .post-image { max-height:none; width:100%; max-width:100%; object-fit:contain; cursor:default; }
@@ -100,8 +108,10 @@ a.header-feed-link:hover { opacity:1; text-decoration:underline; }
#splash-overlay.splash-brutalist .splash-inner.splash-frame {
padding: clamp(1.4rem, 4.5vw, 2.25rem) clamp(1.1rem, 3.5vw, 1.9rem); background: rgba(0, 0, 0, 0.78); }
html.sno-splash-skip #splash-overlay { display:none !important; visibility:hidden !important; pointer-events:none !important; }
-</style>
-<div class="post-modal" id="post-modal">
+{{end}}
+
+{{define "navmodal"}}
+<div class="post-modal" id="post-modal" role="dialog" aria-modal="true" aria-label="Expanded post">
<div class="modal-inner">
<button class="modal-close" onclick="closeModal()">[ ESC ] CLOSE</button>
<div id="modal-content"></div>
@@ -184,7 +194,7 @@ html.sno-splash-skip #splash-overlay { display:none !important; visibility:hidde
// === KEYBOARD NAVIGATION ===
// j / ArrowDown → next post k / ArrowUp → previous post
// h / ArrowLeft → previous page l / ArrowRight → next page
- // PageUp/PageDown → scroll the post list (viewport step on #post-content)
+ // PageUp/PageDown → scroll the post list; re-highlight post at top of visible area
// Enter → expand modal Esc → close modal
const posts = document.querySelectorAll('.post');
let currentIndex = posts.length > 0 ? 0 : -1;
@@ -193,13 +203,45 @@ html.sno-splash-skip #splash-overlay { display:none !important; visibility:hidde
if (currentIndex >= 0) selectPost(0);
- function selectPost(index) {
+ function setActiveHighlight(index, playSound, scrollIntoView) {
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();
+ if (scrollIntoView) {
+ posts[currentIndex].scrollIntoView({ behavior: 'smooth', block: 'nearest' });
+ }
+ if (playSound) playNavSound();
+ }
+
+ function selectPost(index) {
+ setActiveHighlight(index, true, true);
+ }
+
+ /** Pick the post that should be active for the current viewport (anchor near top of visible area). */
+ function activeIndexForVisibleRegion(sc) {
+ if (posts.length === 0) return -1;
+ var scrTop, scrBot, anchorY;
+ if (sc) {
+ var scr = sc.getBoundingClientRect();
+ scrTop = scr.top;
+ scrBot = scr.bottom;
+ anchorY = scr.top + Math.min(scr.height * 0.18, 100);
+ } else {
+ scrTop = 0;
+ scrBot = window.innerHeight;
+ anchorY = window.innerHeight * 0.15;
+ }
+ var i, pr;
+ for (i = 0; i < posts.length; i++) {
+ pr = posts[i].getBoundingClientRect();
+ if (pr.top <= anchorY && anchorY < pr.bottom) return i;
+ }
+ for (i = 0; i < posts.length; i++) {
+ pr = posts[i].getBoundingClientRect();
+ if (pr.bottom > scrTop && pr.top < scrBot) return i;
+ }
+ return posts.length - 1;
}
function playNavSound() {
@@ -293,7 +335,8 @@ html.sno-splash-skip #splash-overlay { display:none !important; visibility:hidde
} else {
window.scrollBy(0, dy);
}
- playNavSound();
+ var idx = activeIndexForVisibleRegion(sc);
+ if (idx >= 0) setActiveHighlight(idx, true, false);
e.preventDefault();
break;
}
diff --git a/internal/generator/theme_aurora.go b/internal/generator/theme_aurora.go
index b8a1c40..ff03cfb 100644
--- a/internal/generator/theme_aurora.go
+++ b/internal/generator/theme_aurora.go
@@ -105,11 +105,12 @@ const auroraTemplate = `<!DOCTYPE html>
}
.splash-aurora .splash-hint { color:rgba(224,248,240,0.88); margin-top:1.1rem; }
.splash-aurora .splash-inner { position:relative; z-index:2; }
+{{template "navSharedCSSInner"}}
</style>
</head>
<body>
{{template "splashGate"}}
- <div id="splash-overlay" class="splash-overlay splash-aurora" tabindex="-1" aria-label="Open microblog">
+ <div id="splash-overlay" class="splash-overlay splash-aurora" role="dialog" aria-modal="true" aria-label="Open microblog" tabindex="-1">
<canvas class="splash-gl-canvas" id="splash-gl-canvas" aria-hidden="true"></canvas>
<div class="splash-aurora-stars" aria-hidden="true"></div>
<div class="splash-aurora-glow" aria-hidden="true"></div>
diff --git a/internal/generator/theme_brutalist.go b/internal/generator/theme_brutalist.go
index 1857615..849f40b 100644
--- a/internal/generator/theme_brutalist.go
+++ b/internal/generator/theme_brutalist.go
@@ -76,11 +76,12 @@ const brutalistTemplate = `<!DOCTYPE html>
.splash-brutalist .splash-tag { font-family:'Courier New',monospace; color:var(--red); }
.splash-brutalist .splash-hint { font-family:'Courier New',monospace; color:#c8c8c8; }
.splash-brutalist .splash-inner { text-shadow: 0 0 12px #000, 0 2px 8px #000; }
+{{template "navSharedCSSInner"}}
</style>
</head>
<body>
{{template "splashGate"}}
- <div id="splash-overlay" class="splash-overlay splash-brutalist" tabindex="-1" aria-label="Open microblog">
+ <div id="splash-overlay" class="splash-overlay splash-brutalist" role="dialog" aria-modal="true" aria-label="Open microblog" tabindex="-1">
<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>
diff --git a/internal/generator/theme_glass.go b/internal/generator/theme_glass.go
index fef1f90..0aef42c 100644
--- a/internal/generator/theme_glass.go
+++ b/internal/generator/theme_glass.go
@@ -92,11 +92,12 @@ const cosmosTemplate = `<!DOCTYPE html>
.splash-cosmos .splash-hint { color:rgba(212,232,255,0.88); }
.splash-cosmos .splash-stars { z-index:1; }
.splash-cosmos .splash-inner { text-shadow: 0 2px 20px rgba(0,0,0,0.85); }
+{{template "navSharedCSSInner"}}
</style>
</head>
<body>
{{template "splashGate"}}
- <div id="splash-overlay" class="splash-overlay splash-cosmos" tabindex="-1" aria-label="Open microblog">
+ <div id="splash-overlay" class="splash-overlay splash-cosmos" role="dialog" aria-modal="true" aria-label="Open microblog" tabindex="-1">
<canvas class="splash-gl-canvas" id="splash-gl-canvas" aria-hidden="true"></canvas>
<div class="splash-stars" aria-hidden="true"></div>
<div class="splash-inner">
diff --git a/internal/generator/theme_matrix.go b/internal/generator/theme_matrix.go
index 37f629b..c21c1de 100644
--- a/internal/generator/theme_matrix.go
+++ b/internal/generator/theme_matrix.go
@@ -89,11 +89,12 @@ const matrixTemplate = `<!DOCTYPE html>
@keyframes splashMatrixGlow { from { opacity:0.85; } to { opacity:1; text-shadow:0 0 28px var(--g); } }
.splash-matrix .splash-tag { position:relative; z-index:1; color:rgba(0,255,65,0.88); }
.splash-matrix .splash-hint { position:relative; z-index:1; color:rgba(0,255,65,0.82); }
+{{template "navSharedCSSInner"}}
</style>
</head>
<body>
{{template "splashGate"}}
- <div id="splash-overlay" class="splash-overlay splash-matrix" tabindex="-1" aria-label="Open microblog">
+ <div id="splash-overlay" class="splash-overlay splash-matrix" role="dialog" aria-modal="true" aria-label="Open microblog" tabindex="-1">
<canvas class="splash-gl-canvas" id="splash-gl-canvas" aria-hidden="true"></canvas>
<div class="splash-rain" aria-hidden="true">01001110 01000101 01001111
10101010 11001100 00110011
diff --git a/internal/generator/theme_minimal.go b/internal/generator/theme_minimal.go
index 6b5eceb..6c7168a 100644
--- a/internal/generator/theme_minimal.go
+++ b/internal/generator/theme_minimal.go
@@ -89,11 +89,12 @@ const plasmaTemplate = `<!DOCTYPE html>
.splash-plasma .splash-hint { color:rgba(232,224,255,0.86); }
.splash-plasma .splash-blobs { z-index:1; }
.splash-plasma .splash-inner { text-shadow: 0 2px 22px rgba(0,0,0,0.9); }
+{{template "navSharedCSSInner"}}
</style>
</head>
<body>
{{template "splashGate"}}
- <div id="splash-overlay" class="splash-overlay splash-plasma" tabindex="-1" aria-label="Open microblog">
+ <div id="splash-overlay" class="splash-overlay splash-plasma" role="dialog" aria-modal="true" aria-label="Open microblog" tabindex="-1">
<canvas class="splash-gl-canvas" id="splash-gl-canvas" aria-hidden="true"></canvas>
<div class="splash-blobs" aria-hidden="true"></div>
<div class="splash-inner">
diff --git a/internal/generator/theme_neon.go b/internal/generator/theme_neon.go
index 7ae0dae..ac182d2 100644
--- a/internal/generator/theme_neon.go
+++ b/internal/generator/theme_neon.go
@@ -7,7 +7,7 @@ const neonTemplate = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>snonux.foo • NEON NEXUS</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r134/three.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
@@ -106,11 +106,12 @@ const neonTemplate = `<!DOCTYPE html>
.splash-neon .splash-tag { color: var(--neon-yellow); }
.splash-neon .splash-hint { color: rgba(224,248,255,0.9); font-family: 'Orbitron', sans-serif; }
.splash-neon .splash-inner { text-shadow: 0 2px 24px rgba(0,0,0,0.85), 0 0 40px rgba(11,0,26,0.9); }
+{{template "navSharedCSSInner"}}
</style>
</head>
<body>
{{template "splashGate"}}
- <div id="splash-overlay" class="splash-overlay splash-neon" tabindex="-1" aria-label="Open microblog">
+ <div id="splash-overlay" class="splash-overlay splash-neon" role="dialog" aria-modal="true" aria-label="Open microblog" tabindex="-1">
<canvas class="splash-gl-canvas" id="splash-gl-canvas" aria-hidden="true"></canvas>
<div class="splash-inner">
<div class="splash-deco" aria-hidden="true"></div>
diff --git a/internal/generator/theme_ocean.go b/internal/generator/theme_ocean.go
index 943701a..63fb06f 100644
--- a/internal/generator/theme_ocean.go
+++ b/internal/generator/theme_ocean.go
@@ -80,11 +80,12 @@ const oceanTemplate = `<!DOCTYPE html>
.splash-ocean .splash-tag { color:var(--aqua); letter-spacing:0.2em; }
.splash-ocean .splash-hint { color:rgba(202,240,248,0.88); }
.splash-ocean .splash-inner { text-shadow: 0 2px 16px rgba(3,4,94,0.9); }
+{{template "navSharedCSSInner"}}
</style>
</head>
<body>
{{template "splashGate"}}
- <div id="splash-overlay" class="splash-overlay splash-ocean" tabindex="-1" aria-label="Open microblog">
+ <div id="splash-overlay" class="splash-overlay splash-ocean" role="dialog" aria-modal="true" aria-label="Open microblog" tabindex="-1">
<canvas class="splash-gl-canvas" id="splash-gl-canvas" aria-hidden="true"></canvas>
<div class="splash-inner">
<div class="splash-wave" aria-hidden="true"></div>
diff --git a/internal/generator/theme_paper.go b/internal/generator/theme_paper.go
index 2b91784..c503b11 100644
--- a/internal/generator/theme_paper.go
+++ b/internal/generator/theme_paper.go
@@ -80,11 +80,12 @@ const volcanoTemplate = `<!DOCTYPE html>
.splash-volcano .splash-tag { color:var(--ember); letter-spacing:0.15em; }
.splash-volcano .splash-hint { color:rgba(255,232,204,0.88); }
.splash-volcano .splash-inner { text-shadow: 0 2px 18px rgba(0,0,0,0.85); }
+{{template "navSharedCSSInner"}}
</style>
</head>
<body>
{{template "splashGate"}}
- <div id="splash-overlay" class="splash-overlay splash-volcano" tabindex="-1" aria-label="Open microblog">
+ <div id="splash-overlay" class="splash-overlay splash-volcano" role="dialog" aria-modal="true" aria-label="Open microblog" tabindex="-1">
<canvas class="splash-gl-canvas" id="splash-gl-canvas" aria-hidden="true"></canvas>
<div class="splash-inner">
<div class="splash-ember" aria-hidden="true"></div>
diff --git a/internal/generator/theme_retro.go b/internal/generator/theme_retro.go
index 37ff0b6..7c449ae 100644
--- a/internal/generator/theme_retro.go
+++ b/internal/generator/theme_retro.go
@@ -92,11 +92,12 @@ const retroTemplate = `<!DOCTYPE html>
.splash-retro .splash-tag { color:#d4a020; }
.splash-retro .splash-hint { color:#c99528; }
.splash-retro .splash-inner { text-shadow: 0 0 10px #000, 0 2px 8px #000; }
+{{template "navSharedCSSInner"}}
</style>
</head>
<body>
{{template "splashGate"}}
- <div id="splash-overlay" class="splash-overlay splash-retro" tabindex="-1" aria-label="Open microblog">
+ <div id="splash-overlay" class="splash-overlay splash-retro" role="dialog" aria-modal="true" aria-label="Open microblog" tabindex="-1">
<canvas class="splash-gl-canvas" id="splash-gl-canvas" aria-hidden="true"></canvas>
<div class="splash-inner">
<div class="splash-title">*** SNONUX BBS ***</div>
diff --git a/internal/generator/theme_synthwave.go b/internal/generator/theme_synthwave.go
index c616f6f..b33edc6 100644
--- a/internal/generator/theme_synthwave.go
+++ b/internal/generator/theme_synthwave.go
@@ -98,11 +98,12 @@ const synthwaveTemplate = `<!DOCTYPE html>
.splash-synthwave .splash-tag { font-family:'Share Tech Mono',monospace; color:var(--purple); }
.splash-synthwave .splash-hint { font-family:'Share Tech Mono',monospace; color:rgba(255,255,255,0.88); }
.splash-synthwave .splash-inner { text-shadow: 0 2px 20px rgba(13,2,33,0.95); }
+{{template "navSharedCSSInner"}}
</style>
</head>
<body>
{{template "splashGate"}}
- <div id="splash-overlay" class="splash-overlay splash-synthwave" tabindex="-1" aria-label="Open microblog">
+ <div id="splash-overlay" class="splash-overlay splash-synthwave" role="dialog" aria-modal="true" aria-label="Open microblog" tabindex="-1">
<canvas class="splash-gl-canvas" id="splash-gl-canvas" aria-hidden="true"></canvas>
<div class="splash-grid" aria-hidden="true"></div>
<div class="splash-inner">
diff --git a/internal/generator/theme_terminal.go b/internal/generator/theme_terminal.go
index 8096b19..e4ddb8c 100644
--- a/internal/generator/theme_terminal.go
+++ b/internal/generator/theme_terminal.go
@@ -82,11 +82,12 @@ const terminalTemplate = `<!DOCTYPE html>
.splash-terminal .splash-tag { color:rgba(51,255,51,0.85); letter-spacing:0.25em; }
.splash-terminal .splash-hint { color:rgba(51,255,51,0.8); }
.splash-terminal .splash-inner { text-shadow: 0 0 8px #000, 0 2px 12px #000; }
+{{template "navSharedCSSInner"}}
</style>
</head>
<body>
{{template "splashGate"}}
- <div id="splash-overlay" class="splash-overlay splash-terminal" tabindex="-1" aria-label="Open microblog">
+ <div id="splash-overlay" class="splash-overlay splash-terminal" role="dialog" aria-modal="true" aria-label="Open microblog" tabindex="-1">
<canvas class="splash-gl-canvas" id="splash-gl-canvas" aria-hidden="true"></canvas>
<div class="splash-inner">
<div class="splash-prompt">&gt; ./snonux --boot</div>
diff --git a/internal/generator/themes.go b/internal/generator/themes.go
index 7100471..4d35db6 100644
--- a/internal/generator/themes.go
+++ b/internal/generator/themes.go
@@ -1,7 +1,8 @@
package generator
// themeRegistry maps theme names to their HTML template strings.
-// Each template must use {{template "navhints" .}}, {{template "navmodal" .}},
+// Each template must end its <style> with {{template "navSharedCSSInner"}}, then use
+// {{template "navhints" .}}, {{template "navmodal" .}},
// and {{template "navscript" .}} — these are defined in shared.go (navDefs).
var themeRegistry = map[string]string{
"neon": neonTemplate,
diff --git a/internal/version/version.go b/internal/version/version.go
index 58f0cda..0a14c44 100644
--- a/internal/version/version.go
+++ b/internal/version/version.go
@@ -2,4 +2,4 @@
package version
// Version is the application version (semantic versioning).
-const Version = "0.1.4"
+const Version = "0.1.5"