diff options
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/config/config.go | 3 | ||||
| -rw-r--r-- | internal/generator/doc.go | 3 | ||||
| -rw-r--r-- | internal/generator/shared.go | 81 | ||||
| -rw-r--r-- | internal/generator/theme_aurora.go | 3 | ||||
| -rw-r--r-- | internal/generator/theme_brutalist.go | 3 | ||||
| -rw-r--r-- | internal/generator/theme_glass.go | 3 | ||||
| -rw-r--r-- | internal/generator/theme_matrix.go | 3 | ||||
| -rw-r--r-- | internal/generator/theme_minimal.go | 3 | ||||
| -rw-r--r-- | internal/generator/theme_neon.go | 5 | ||||
| -rw-r--r-- | internal/generator/theme_ocean.go | 3 | ||||
| -rw-r--r-- | internal/generator/theme_paper.go | 3 | ||||
| -rw-r--r-- | internal/generator/theme_retro.go | 3 | ||||
| -rw-r--r-- | internal/generator/theme_synthwave.go | 3 | ||||
| -rw-r--r-- | internal/generator/theme_terminal.go | 3 | ||||
| -rw-r--r-- | internal/generator/themes.go | 3 | ||||
| -rw-r--r-- | internal/version/version.go | 2 |
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">> ./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" |
