diff --git a/lyra/web/static/index.html b/lyra/web/static/index.html
index 06f58d1..a77a5d6 100644
--- a/lyra/web/static/index.html
+++ b/lyra/web/static/index.html
@@ -499,7 +499,10 @@
// iOS over plain-HTTP LAN (where navigator.clipboard is undefined).
function copyToClipboard(text) {
text = text == null ? "" : String(text);
- if (navigator.clipboard && navigator.clipboard.writeText) {
+ // Only trust the async Clipboard API in a secure context; on the LAN PWA
+ // (plain HTTP) it's either absent or resolves without actually copying, so
+ // we go straight to the iOS-tuned execCommand path there.
+ if (window.isSecureContext && navigator.clipboard && navigator.clipboard.writeText) {
return navigator.clipboard.writeText(text).catch(() => legacyCopy(text));
}
return legacyCopy(text);
@@ -508,19 +511,24 @@
return new Promise((resolve, reject) => {
const ta = document.createElement("textarea");
ta.value = text;
+ // iOS will only copy from a readOnly + contentEditable field with a real
+ // Range selection; readOnly also stops the keyboard from popping.
+ ta.readOnly = true;
ta.contentEditable = "true";
- ta.readOnly = false;
ta.style.position = "fixed";
ta.style.top = "0";
ta.style.left = "0";
- ta.style.opacity = "0";
+ ta.style.width = "1px";
+ ta.style.height = "1px";
+ ta.style.fontSize = "16px"; // avoid iOS zoom side-effects
document.body.appendChild(ta);
+ ta.focus();
const range = document.createRange();
range.selectNodeContents(ta);
const sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
- ta.setSelectionRange(0, text.length); // iOS needs an explicit range
+ ta.setSelectionRange(0, text.length); // the bit iOS actually needs
let ok = false;
try { ok = document.execCommand("copy"); } catch (e) { ok = false; }
sel.removeAllRanges();
@@ -537,14 +545,17 @@
b.title = "Copy message";
b.addEventListener("click", (e) => {
e.stopPropagation();
- copyToClipboard(typeof getText === "function" ? getText() : getText)
+ const text = typeof getText === "function" ? getText() : getText;
+ copyToClipboard(text)
.then(() => {
b.textContent = "✓"; b.classList.add("copied");
setTimeout(() => { b.textContent = "⧉"; b.classList.remove("copied"); }, 1200);
})
.catch(() => {
- b.textContent = "✗";
- setTimeout(() => { b.textContent = "⧉"; }, 1200);
+ // Last resort (some iOS configs block programmatic copy): surface the
+ // text in a prompt so it can be selected + copied by hand.
+ window.prompt("Copy this message:", text);
+ b.textContent = "⧉";
});
});
return b;