diff --git a/static/app.css b/static/app.css
index 527e8fd..fce93c4 100644
--- a/static/app.css
+++ b/static/app.css
@@ -451,6 +451,9 @@ body {
.cli-install-row { align-items: stretch; }
.cli-install-cmd { display: inline-block; padding: 8px 12px; border-radius: 6px; background: var(--bg); border: 1px solid var(--border); font-size: 14px; user-select: all; overflow-x: auto; max-width: 100%; }
+.icon-btn { display: inline-flex; align-items: center; justify-content: center; padding: 8px 10px; }
+.icon-btn svg { width: 16px; height: 16px; }
+.icon-btn.ok { color: var(--primary); border-color: var(--primary); }
.tools-head { display: flex; align-items: center; gap: 12px; margin-bottom: 8px; }
.tools-head h1 { margin: 0; }
diff --git a/static/app.js b/static/app.js
index 531c980..b0da4a9 100644
--- a/static/app.js
+++ b/static/app.js
@@ -155,6 +155,25 @@ function escapeHtml(s) {
return (s || "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]));
}
+// navigator.clipboard only exists in secure contexts (HTTPS/localhost);
+// fall back to a hidden textarea + execCommand for plain-HTTP LAN use.
+async function copyText(text) {
+ if (window.isSecureContext && navigator.clipboard) {
+ try { await navigator.clipboard.writeText(text); return true; } catch (e) { /* fall through */ }
+ }
+ const ta = document.createElement("textarea");
+ ta.value = text;
+ ta.setAttribute("readonly", "");
+ ta.style.position = "fixed";
+ ta.style.left = "-9999px";
+ document.body.appendChild(ta);
+ ta.select();
+ let ok = false;
+ try { ok = document.execCommand("copy"); } catch (e) {}
+ ta.remove();
+ return ok;
+}
+
async function boot() {
try {
const me = await api("GET", "/api/me");
@@ -261,9 +280,22 @@ function showCli() {
const back = backButton(showHome);
const installCmd = "curl -fsSL " + window.location.origin + "/" + state.user + "/cli | bash";
const cmdEl = el("code", { class: "cli-install-cmd", text: installCmd });
- const copyBtn = el("button", {
- class: "btn", text: "Copy",
- onclick: () => { navigator.clipboard?.writeText(installCmd); copyBtn.textContent = "Copied!"; setTimeout(() => { copyBtn.textContent = "Copy"; }, 2000); },
+ const CLIP_ICON = '';
+ const CHECK_ICON = '';
+ const copyBtn = el("button", { class: "btn icon-btn", title: "Copy install command", "aria-label": "Copy install command", html: CLIP_ICON });
+ copyBtn.addEventListener("click", async () => {
+ const ok = await copyText(installCmd);
+ if (!ok) { // last resort: select the text so the user can ⌘C
+ const sel = window.getSelection();
+ const range = document.createRange();
+ range.selectNodeContents(cmdEl);
+ sel.removeAllRanges();
+ sel.addRange(range);
+ return;
+ }
+ copyBtn.innerHTML = CHECK_ICON;
+ copyBtn.classList.add("ok");
+ setTimeout(() => { copyBtn.innerHTML = CLIP_ICON; copyBtn.classList.remove("ok"); }, 2000);
});
const card = el("div", { class: "help-card" }, [