summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-06-03 23:30:21 +0300
committerPaul Buetow <paul@buetow.org>2026-06-03 23:30:21 +0300
commitb1bf055da68e6f77650dbbd13320f377c543f470 (patch)
tree1cac5590d6c8471b37048b6d6b26f79f9d4b4b5a /internal
parent1b24076788868f50812468b1bf4bb0ae86214e8c (diff)
Add per-post share button with permalink (mobile view)
Each post now exposes a share button in its header that surfaces only on mobile. Clicking it invokes navigator.share() with a permalink of the form "<page>#post-<ID>", which the existing deep-link handler in shared.js already opens directly in the modal. When navigator.share is unavailable the link is copied to the clipboard and a brief toast confirms the action; very old browsers fall back to window.prompt. Amp-Thread-ID: https://ampcode.com/threads/T-019e8f2a-b58e-72b3-b572-28d1fb1d9a1d Co-authored-by: Amp <amp@ampcode.com>
Diffstat (limited to 'internal')
-rw-r--r--internal/generator/generator_test.go3
-rw-r--r--internal/generator/templates/shared/shared.css26
-rw-r--r--internal/generator/templates/shared/shared.js42
-rw-r--r--internal/generator/templates/shell.tmpl3
4 files changed, 74 insertions, 0 deletions
diff --git a/internal/generator/generator_test.go b/internal/generator/generator_test.go
index a718c95..365fd45 100644
--- a/internal/generator/generator_test.go
+++ b/internal/generator/generator_test.go
@@ -465,6 +465,9 @@ func TestRun_writesPagesAndAtom(t *testing.T) {
if !strings.Contains(string(indexHTML), `rel="icon" href="favicon.ico"`) {
t.Fatalf("index.html missing favicon link: %s", string(indexHTML))
}
+ if !strings.Contains(string(indexHTML), `class="post-share-btn" data-share-id="a1"`) {
+ t.Fatalf("index.html missing per-post share button: %s", string(indexHTML))
+ }
}
func TestRun_writesVolcanoFontAssets(t *testing.T) {
diff --git a/internal/generator/templates/shared/shared.css b/internal/generator/templates/shared/shared.css
index 35f4aef..e06bb50 100644
--- a/internal/generator/templates/shared/shared.css
+++ b/internal/generator/templates/shared/shared.css
@@ -949,9 +949,35 @@ body[data-sno-theme="matrix"] .post-text img,
body[data-sno-theme="terminal"] .post-text img,
body[data-sno-theme="dos"] .post-text img { animation:sno-matrix-decode 0.8s ease-out both; }
+/* Per-post share button — only surfaced on mobile (see media query below).
+ Header is flex with @snonux on the left, time + share aligned to the right. */
+.post-header { align-items:center; }
+.post-share-btn {
+ display:none;
+ appearance:none; border:0; background:none; color:inherit; font:inherit;
+ padding:6px 8px; margin-left:6px; cursor:pointer; opacity:0.7;
+ transition:opacity 0.18s ease, transform 0.18s ease;
+ -webkit-tap-highlight-color:transparent;
+}
+.post-share-btn:hover { opacity:1; }
+.post-share-btn:active { transform:scale(0.92); }
+.post-share-btn:focus-visible { outline:1px solid currentColor; outline-offset:2px; border-radius:3px; }
+.post-share-btn i { font-size:0.95rem; }
+/* Toast surfaced when navigator.share is unavailable and the URL is copied to the clipboard. */
+.sno-share-toast {
+ position:fixed; left:50%; bottom:88px; transform:translateX(-50%);
+ z-index:6000; padding:10px 16px; border-radius:6px;
+ background:rgba(0,0,0,0.85); color:#fff; font:600 0.85rem/1.2 monospace;
+ pointer-events:none; box-shadow:0 4px 16px rgba(0,0,0,0.4);
+ opacity:1; transition:opacity 0.35s ease;
+}
+.sno-share-toast.sno-share-toast-out { opacity:0; }
+
/* Mobile touch-target and layout overrides */
@media(max-width:640px) {
.nav-hints { padding:4px 14px !important; gap:0; position:sticky; top:0; z-index:25; }
+ .post-share-btn { display:inline-flex; align-items:center; justify-content:center;
+ min-width:40px; min-height:40px; }
/* Remove navigation row on mobile */
.nav-hints-nav { display:none !important; }
/* FX row: single horizontal line of icon-only tiles */
diff --git a/internal/generator/templates/shared/shared.js b/internal/generator/templates/shared/shared.js
index 1e29831..3f90d4a 100644
--- a/internal/generator/templates/shared/shared.js
+++ b/internal/generator/templates/shared/shared.js
@@ -2029,6 +2029,48 @@
});
})();
+ // Per-post share button (visible only on mobile via CSS). Uses the Web Share
+ // API where available, otherwise copies the permalink to the clipboard and
+ // shows a brief toast. Falls back to window.prompt on very old browsers.
+ (function postShareButtons() {
+ var btns = document.querySelectorAll('.post-share-btn');
+ if (btns.length === 0) return;
+ function permalinkFor(id) {
+ return location.origin + location.pathname + '#post-' + encodeURIComponent(id);
+ }
+ function showToast(msg) {
+ var t = document.createElement('div');
+ t.className = 'sno-share-toast';
+ t.textContent = msg;
+ document.body.appendChild(t);
+ setTimeout(function() { t.classList.add('sno-share-toast-out'); }, 1400);
+ setTimeout(function() { if (t.parentNode) t.parentNode.removeChild(t); }, 1800);
+ }
+ btns.forEach(function(btn) {
+ btn.addEventListener('click', function(e) {
+ e.stopPropagation();
+ e.preventDefault();
+ var id = btn.getAttribute('data-share-id');
+ if (!id) return;
+ var url = permalinkFor(id);
+ var shareData = { title: document.title, text: '@snonux post', url: url };
+ if (navigator.share) {
+ navigator.share(shareData).catch(function() {});
+ return;
+ }
+ if (navigator.clipboard && navigator.clipboard.writeText) {
+ navigator.clipboard.writeText(url).then(function() {
+ showToast('Link copied');
+ }).catch(function() {
+ window.prompt('Copy link', url);
+ });
+ return;
+ }
+ window.prompt('Copy link', url);
+ });
+ });
+ })();
+
(function deepLinkFromHash() {
var h = location.hash;
if (!h || h.indexOf('#post-') !== 0) return;
diff --git a/internal/generator/templates/shell.tmpl b/internal/generator/templates/shell.tmpl
index 71f18cd..5da78a5 100644
--- a/internal/generator/templates/shell.tmpl
+++ b/internal/generator/templates/shell.tmpl
@@ -58,6 +58,9 @@
<div class="post-header">
<div><strong>@snonux</strong></div>
<div class="post-time">{{$post.FormattedTime}}</div>
+ <button type="button" class="post-share-btn" data-share-id="{{$post.ID}}" aria-label="Share post link">
+ <i class="fas fa-share-nodes" aria-hidden="true"></i>
+ </button>
</div>
<div class="post-text">{{$post.ContentHTML}}</div>
</div>