Use toast feedback for copy actions

This commit is contained in:
Codex
2026-07-08 06:18:14 +00:00
parent 72bd06fc9d
commit 65c1e8bddf
+32 -12
View File
@@ -115,6 +115,9 @@ h1 { margin: 0; font-size: clamp(28px, 5vw, 44px); line-height: 1.12; letter-spa
.shot-menu { position: absolute; left: 0; bottom: calc(100% + 8px); z-index: 20; min-width: 150px; padding: 6px; border: 1px solid var(--border); border-radius: 8px; background: var(--panel); box-shadow: 0 12px 30px rgba(0,0,0,.12); } .shot-menu { position: absolute; left: 0; bottom: calc(100% + 8px); z-index: 20; min-width: 150px; padding: 6px; border: 1px solid var(--border); border-radius: 8px; background: var(--panel); box-shadow: 0 12px 30px rgba(0,0,0,.12); }
.shot-menu button { width: 100%; border: 0; border-radius: 6px; background: transparent; color: var(--text); padding: 8px 10px; font: inherit; font-size: 13px; text-align: left; cursor: pointer; } .shot-menu button { width: 100%; border: 0; border-radius: 6px; background: transparent; color: var(--text); padding: 8px 10px; font: inherit; font-size: 13px; text-align: left; cursor: pointer; }
.shot-menu button:hover, .shot-menu button[data-current="true"] { background: var(--soft); } .shot-menu button:hover, .shot-menu button[data-current="true"] { background: var(--soft); }
.toast-region { position: fixed; left: 50%; bottom: 22px; z-index: 40; display: grid; justify-items: center; pointer-events: none; transform: translateX(-50%); }
.toast { max-width: min(calc(100vw - 32px), 320px); border: 1px solid var(--border); border-radius: 8px; background: var(--panel); color: var(--text); padding: 9px 12px; font-size: 13px; line-height: 1.35; box-shadow: 0 12px 30px rgba(0,0,0,.14); opacity: 0; transform: translateY(8px); transition: opacity .16s ease, transform .16s ease; }
.toast[data-visible="true"] { opacity: 1; transform: translateY(0); }
.source { max-width: 880px; margin: 0 auto; padding: 18px 20px 34px; color: var(--muted); font-size: 12px; } .source { max-width: 880px; margin: 0 auto; padding: 18px 20px 34px; color: var(--muted); font-size: 12px; }
.source a { color: var(--muted); } .source a { color: var(--muted); }
.failure { max-width: 720px; margin: 0 auto; padding: 80px 20px; } .failure { max-width: 720px; margin: 0 auto; padding: 80px 20px; }
@@ -145,29 +148,45 @@ function pageScript(slug: string): string {
function downloadShot(variant) { function downloadShot(variant) {
location.href = "/aishare/${slug}/screenshots/" + variant + ".png"; location.href = "/aishare/${slug}/screenshots/" + variant + ".png";
} }
var toastTimer;
function showToast(message) {
var region = document.querySelector("[data-toast-region]");
if (!region) return;
var toast = region.querySelector("[data-toast]");
if (!toast) {
toast = document.createElement("div");
toast.className = "toast";
toast.dataset.toast = "true";
region.appendChild(toast);
}
toast.textContent = message;
toast.dataset.visible = "true";
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
delete toast.dataset.visible;
}, 1600);
}
async function copyText(text, successMessage) {
try {
await navigator.clipboard.writeText(text);
showToast(successMessage);
} catch (error) {
showToast("Copy failed");
}
}
var params = new URLSearchParams(location.search); var params = new URLSearchParams(location.search);
var theme = params.get("theme"); var theme = params.get("theme");
if (theme === "light" || theme === "dark") document.documentElement.dataset.theme = theme; if (theme === "light" || theme === "dark") document.documentElement.dataset.theme = theme;
var copy = document.querySelector("[data-copy]"); var copy = document.querySelector("[data-copy]");
if (copy) copy.addEventListener("click", async function () { if (copy) copy.addEventListener("click", async function () {
await navigator.clipboard.writeText(location.href.replace(/[?#].*$/, "")); await copyText(location.href.replace(/[?#].*$/, ""), "Link copied");
copy.dataset.copied = "true";
copy.setAttribute("aria-label", "Copied");
copy.setAttribute("title", "Copied");
setTimeout(function () {
delete copy.dataset.copied;
copy.setAttribute("aria-label", "Copy link");
copy.setAttribute("title", "Copy link");
}, 1400);
}); });
document.querySelectorAll("[data-copy-code]").forEach(function (button) { document.querySelectorAll("[data-copy-code]").forEach(function (button) {
button.addEventListener("click", async function () { button.addEventListener("click", async function () {
var block = button.closest(".code-block"); var block = button.closest(".code-block");
var code = block && block.querySelector("code"); var code = block && block.querySelector("code");
if (!code) return; if (!code) return;
await navigator.clipboard.writeText(code.textContent || ""); await copyText(code.textContent || "", "Code copied");
button.textContent = "Copied";
setTimeout(function () { button.textContent = "Copy"; }, 1400);
}); });
}); });
var defaultShot = document.querySelector("[data-shot-current]"); var defaultShot = document.querySelector("[data-shot-current]");
@@ -241,6 +260,7 @@ export function renderConversationPage(slug: string, conversation: NormalizedCon
Captured from ChatGPT on ${escapeHtml(new Date(conversation.capturedAt).toLocaleString())}. Captured from ChatGPT on ${escapeHtml(new Date(conversation.capturedAt).toLocaleString())}.
Source: <a href="${escapeHtml(conversation.sourceUrl)}" rel="nofollow noreferrer">${escapeHtml(conversation.sourceUrl)}</a> Source: <a href="${escapeHtml(conversation.sourceUrl)}" rel="nofollow noreferrer">${escapeHtml(conversation.sourceUrl)}</a>
</footer> </footer>
<div class="toast-region" data-toast-region aria-live="polite" aria-atomic="true"></div>
</div> </div>
<script>${pageScript(slug)}</script> <script>${pageScript(slug)}</script>
</body> </body>