diff --git a/src/assets/css/phosphor.css b/src/assets/css/phosphor.css index 6ed2615..e0aad8d 100644 --- a/src/assets/css/phosphor.css +++ b/src/assets/css/phosphor.css @@ -487,14 +487,13 @@ body { animation: flicker 12s infinite; } .project-tag.active { background: var(--p); border-color: var(--p); color: #000; } .tag-count { opacity: 0.65; font-size: 10px; } -/* Back button inside post view */ -.blog-back, -.project-back { +/* Back / share buttons injected into sub-page views */ +.win-back, +.win-share { appearance: none; -webkit-appearance: none; border: 1px solid var(--p-dim); background: transparent; - color: var(--p); font-family: var(--font-body); font-size: 13px; padding: 2px 12px; @@ -503,8 +502,11 @@ body { animation: flicker 12s infinite; } display: inline-block; margin-bottom: 4px; } -.blog-back:hover, -.project-back:hover { background: var(--p); color: #000; border-color: var(--p); } +.win-back { color: var(--p); margin-right: 6px; } +.win-back:hover { background: var(--p); color: #000; border-color: var(--p); } +.win-share { color: var(--p-dim); } +.win-share:hover, +.win-share.copied { border-color: var(--p); color: var(--p); } /* Post metadata line */ .post-meta { diff --git a/src/assets/js/blog.js b/src/assets/js/blog.js index 861c48c..9566ffd 100644 --- a/src/assets/js/blog.js +++ b/src/assets/js/blog.js @@ -1,21 +1,32 @@ (function () { "use strict"; - function syncWindow(container) { - container.scrollTop = 0; - var win = container.closest(".window"); - if (win && win._syncMore) win._syncMore(); - } + function injectShare(container, url) { + var parts = url.replace(/\/$/, "").split("/").filter(Boolean); + var type = parts[1]; // "blog" or "projects" + var slug = parts[2]; + if (type !== "blog" && type !== "projects") return; - function openSubPage(url, container, backUrl) { - WM.loadFragment(url, container, function () { - var back = document.createElement("button"); - back.className = "blog-back"; - back.dataset.backUrl = backUrl; - back.textContent = "\u2190 back"; - container.insertBefore(back, container.firstChild); - syncWindow(container); + var shareUrl = window.location.origin + "/#" + type + "/" + slug; + var btn = document.createElement("button"); + btn.className = "win-share"; + btn.textContent = "share"; + btn.addEventListener("click", function () { + navigator.clipboard.writeText(shareUrl).then(function () { + btn.textContent = "copied!"; + btn.classList.add("copied"); + setTimeout(function () { + btn.textContent = "share"; + btn.classList.remove("copied"); + }, 2000); + }).catch(function () { + window.prompt("Copy this link:", shareUrl); + }); }); + + var back = container.querySelector(".win-back"); + if (back) back.insertAdjacentElement("afterend", btn); + else container.insertBefore(btn, container.firstChild); } document.addEventListener("click", function (e) { @@ -24,8 +35,9 @@ var postLink = e.target.closest(".blog-open-post"); if (postLink) { e.preventDefault(); - var container = postLink.closest(".window-content"); - openSubPage(postLink.dataset.postUrl, container, "/fragments/blog/"); + WM.navigate(postLink.closest(".window-content"), postLink.dataset.postUrl, "/fragments/blog/", function (c) { + injectShare(c, postLink.dataset.postUrl); + }); return; } @@ -33,17 +45,8 @@ var projLink = e.target.closest(".project-open-item"); if (projLink) { e.preventDefault(); - var container = projLink.closest(".window-content"); - openSubPage(projLink.dataset.projectUrl, container, "/fragments/projects/"); - return; - } - - // Back button (shared by blog and projects) - var backBtn = e.target.closest(".blog-back"); - if (backBtn) { - var container = backBtn.closest(".window-content"); - WM.loadFragment(backBtn.dataset.backUrl, container, function () { - syncWindow(container); + WM.navigate(projLink.closest(".window-content"), projLink.dataset.projectUrl, "/fragments/projects/", function (c) { + injectShare(c, projLink.dataset.projectUrl); }); return; } @@ -77,4 +80,20 @@ } }); + + // Hash-based deep linking: /#blog/ or /#projects/ + document.addEventListener("wm:ready", function () { + var hash = window.location.hash.slice(1); + if (!hash) return; + var slash = hash.indexOf("/"); + if (slash === -1) return; + var type = hash.slice(0, slash); + var slug = hash.slice(slash + 1); + if (!slug || (type !== "blog" && type !== "projects")) return; + var fragUrl = "/fragments/" + type + "/" + slug + "/"; + WM.showAt("win-" + type, fragUrl, "/fragments/" + type + "/", function (c) { + injectShare(c, fragUrl); + }); + }); + }()); diff --git a/src/assets/js/wm.js b/src/assets/js/wm.js index 02bde83..ad0a86f 100644 --- a/src/assets/js/wm.js +++ b/src/assets/js/wm.js @@ -35,6 +35,39 @@ const WM = (() => { const mobile = () => window.matchMedia("(pointer: coarse)").matches; + /* Sub-page navigation */ + + function navigate(container, url, backUrl, onDone) { + loadFragment(url, container, () => { + if (backUrl) { + const back = document.createElement("button"); + back.className = "win-back"; + back.dataset.backUrl = backUrl; + back.textContent = "\u2190 back"; + container.insertBefore(back, container.firstChild); + } + if (onDone) onDone(container); + container.scrollTop = 0; + const win = container.closest(".window"); + if (win && win._syncMore) win._syncMore(); + }); + } + + function showAt(id, url, backUrl, onDone) { + const s = state.get(id); + if (!s) return; + s.contentLoaded = true; + s.el.classList.remove("hidden"); + s.hidden = false; + focusEl(s.el); + const contentEl = s.el.querySelector(".window-content"); + if (contentEl) navigate(contentEl, url, backUrl, onDone); + requestAnimationFrame(() => { + clampToArea(s.el); + if (s.el._syncMore) s.el._syncMore(); + }); + } + /* Init */ function init() { @@ -42,9 +75,16 @@ const WM = (() => { setupIcons(); document.addEventListener("mousemove", onMove); document.addEventListener("mouseup", onUp); + document.addEventListener("click", e => { + const btn = e.target.closest(".win-back"); + if (!btn) return; + const container = btn.closest(".window-content"); + if (container) navigate(container, btn.dataset.backUrl, null); + }); // Focus topmost visible window on load const visible = [...state.values()].filter(s => !s.hidden); if (visible.length) focusEl(visible[visible.length - 1].el); + document.dispatchEvent(new CustomEvent("wm:ready")); } /* Build and inject window chrome */ @@ -302,5 +342,5 @@ const WM = (() => { document.addEventListener("DOMContentLoaded", init); - return { show, hide, toggle, focus, loadFragment }; + return { show, hide, toggle, focus, loadFragment, navigate, showAt }; })();