diff options
| author | Paul Buetow <paul@buetow.org> | 2026-06-03 23:30:21 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-06-03 23:30:21 +0300 |
| commit | b1bf055da68e6f77650dbbd13320f377c543f470 (patch) | |
| tree | 1cac5590d6c8471b37048b6d6b26f79f9d4b4b5a /internal | |
| parent | 1b24076788868f50812468b1bf4bb0ae86214e8c (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.go | 3 | ||||
| -rw-r--r-- | internal/generator/templates/shared/shared.css | 26 | ||||
| -rw-r--r-- | internal/generator/templates/shared/shared.js | 42 | ||||
| -rw-r--r-- | internal/generator/templates/shell.tmpl | 3 |
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> |
