diff --git a/assets/static/labyricorn-social-card.png b/assets/static/labyricorn-social-card.png new file mode 100644 index 0000000..b35f83b Binary files /dev/null and b/assets/static/labyricorn-social-card.png differ diff --git a/assets/static/social-share.js b/assets/static/social-share.js new file mode 100644 index 0000000..f3beee4 --- /dev/null +++ b/assets/static/social-share.js @@ -0,0 +1,62 @@ +(() => { + const announce = (component, message) => { + const status = component.querySelector("[data-share-status]"); + if (!status) return; + status.textContent = ""; + window.requestAnimationFrame(() => { + status.textContent = message; + }); + }; + + const fallbackCopy = (url) => { + const field = document.createElement("textarea"); + field.value = url; + field.setAttribute("readonly", ""); + field.style.position = "fixed"; + field.style.opacity = "0"; + document.body.appendChild(field); + field.select(); + const copied = document.execCommand("copy"); + field.remove(); + return copied; + }; + + const copyLink = async (component) => { + const url = component.dataset.shareUrl; + try { + if (navigator.clipboard && window.isSecureContext) { + await navigator.clipboard.writeText(url); + } else if (!fallbackCopy(url)) { + throw new Error("Copy command unavailable"); + } + announce(component, "Link copied."); + } catch (_error) { + window.prompt("Copy this link:", url); + announce(component, "Copy the link from the dialog."); + } + }; + + document.querySelectorAll("[data-share-component]").forEach((component) => { + component.querySelector("[data-share-native]")?.addEventListener("click", async (event) => { + event.preventDefault(); + if (!navigator.share) { + await copyLink(component); + return; + } + try { + await navigator.share({ + title: component.dataset.shareTitle, + text: component.dataset.shareDescription, + url: component.dataset.shareUrl, + }); + } catch (error) { + if (error.name !== "AbortError") await copyLink(component); + } + }); + + component.querySelector("[data-share-copy]")?.addEventListener("click", async (event) => { + event.preventDefault(); + await copyLink(component); + }); + }); +})(); diff --git a/assets/static/style.css b/assets/static/style.css index a84e354..0f08e9d 100644 --- a/assets/static/style.css +++ b/assets/static/style.css @@ -432,7 +432,8 @@ h1 { max-width: 880px; margin: 0; font-size: clamp(30px, 4vw, 45px); font-style: grid-template-columns: minmax(0, 640px) minmax(230px, 280px); grid-template-areas: "header info" - "body info"; + "body info" + "share info"; gap: 34px clamp(54px, 7vw, 92px); align-items: start; padding-block: 68px 90px; @@ -486,6 +487,45 @@ h1 { max-width: 880px; margin: 0; font-size: clamp(30px, 4vw, 45px); font-style: .publication-links a:hover, .publication-links a:focus-visible { text-decoration: underline; text-underline-offset: 3px; } +.share-panel { + display: grid; + grid-template-columns: minmax(150px, .55fr) minmax(0, 1.45fr); + gap: 12px 28px; + border-block: 1px solid var(--rule); + padding-block: 22px; +} +.blog-post-layout > .share-panel, +.article-post-layout > .share-panel { grid-area: share; } +.share-panel-heading { align-self: center; } +.share-kicker { + margin: 0 0 5px; + color: var(--magenta); + font: 500 9px var(--mono); + letter-spacing: .14em; + text-transform: uppercase; +} +.share-panel h2 { margin: 0; font: 600 17px/1.3 var(--serif); } +.share-actions { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; } +.share-action { + border: 1px solid var(--rule); + padding: 7px 9px; + color: var(--muted); + font: 500 9px var(--mono); + letter-spacing: .07em; + text-transform: uppercase; +} +.share-action:hover, +.share-action:focus-visible { border-color: var(--teal); color: var(--teal); outline-color: var(--teal); } +.share-action-primary { border-color: rgba(81,182,165,.6); color: var(--teal); } +.share-status { + grid-column: 2; + min-height: 1.3em; + margin: 0; + color: var(--teal); + font: 500 9px var(--mono); + letter-spacing: .06em; +} + .narration-player { border: 1px solid var(--rule); padding: 18px 20px; @@ -548,6 +588,7 @@ h1 { max-width: 880px; margin: 0; font-size: clamp(30px, 4vw, 45px); font-style: padding-block: 68px 78px; } .project-narrative { width: 100%; } +.project-content > .share-panel { grid-column: 1 / -1; } .repository-card { border: 1px solid var(--rule); background: var(--panel); padding: 24px; } .repository-card dl { margin: 0; } .repository-card a { color: var(--teal); } @@ -601,6 +642,7 @@ h1 { max-width: 880px; margin: 0; font-size: clamp(30px, 4vw, 45px); font-style: grid-template-areas: "header info" "body info" + "share share" "navigation navigation" "back back"; gap: 34px clamp(54px, 7vw, 92px); @@ -610,6 +652,7 @@ h1 { max-width: 880px; margin: 0; font-size: clamp(30px, 4vw, 45px); font-style: .devlog-entry-header { grid-area: header; } .devlog-entry-header h1 { font-style: normal; } .devlog-entry-body { grid-area: body; width: 100%; } +.devlog-entry-layout > .share-panel { grid-area: share; } .devlog-info { grid-area: info; } .devlog-info .post-info-title { color: var(--violet); } .devlog-navigation { grid-area: navigation; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1px; margin-top: 38px; border: 1px solid var(--rule); background: var(--rule); } @@ -702,6 +745,7 @@ h1 { max-width: 880px; margin: 0; font-size: clamp(30px, 4vw, 45px); font-style: "header" "info" "body" + "share" "navigation" "back"; gap: 30px; @@ -716,11 +760,14 @@ h1 { max-width: 880px; margin: 0; font-size: clamp(30px, 4vw, 45px); font-style: grid-template-areas: "header" "info" - "body"; + "body" + "share"; gap: 30px; padding-block: 44px 65px; } .post-info { padding: 20px; } + .share-panel { grid-template-columns: 1fr; } + .share-status { grid-column: 1; } .narration-player { padding: 18px; } .narration-details { align-items: flex-start; flex-direction: column-reverse; } .footer-inner { align-items: flex-start; flex-direction: column; padding-block: 28px; } diff --git a/templates/base.html b/templates/base.html index 117aeff..7a39ca1 100644 --- a/templates/base.html +++ b/templates/base.html @@ -1,19 +1,21 @@ +{% from "macros/social-sharing.html" import social_metadata with context %} - {% if this._path != '/' %}{{ this.title }} — {% endif %}Labyricorn + {% block social_head %}{{ social_metadata(this) }}{% endblock %} - + +