summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
Diffstat (limited to 'internal')
-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
16 files changed, 93 insertions, 34 deletions
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"