From 0cf740822593fe01d3aeb8b53496e88a47b6c728 Mon Sep 17 00:00:00 2001 From: Franz Rolfsvaag Date: Wed, 17 Jun 2026 21:32:24 +0200 Subject: [PATCH] fix command page buttons --- TODO.md | 1 + package-lock.json | 4 +- package.json | 2 +- src/web/public/app.js | 175 ++++++++++++++++++++------------------ src/web/public/styles.css | 17 ++++ 5 files changed, 113 insertions(+), 86 deletions(-) diff --git a/TODO.md b/TODO.md index 9f16829..82a7ca3 100644 --- a/TODO.md +++ b/TODO.md @@ -125,6 +125,7 @@ This file tracks larger Lumi work that cannot safely be completed in one pass. K ## Done +- 2026-06-17: Fixed custom command Edit buttons and `/commands` Copy Link / expand buttons with delegated handlers, clipboard fallback, and v0.1.5 patch bump. - 2026-06-17: Fixed repo-based core updates deleting `data/update-cache/repo` during apply, added a verification guard, and bumped core package version to v0.1.4. - 2026-06-17: Bumped core package version to v0.1.3. - 2026-06-17: Completed homepage hero embed pass for Discord widgets, YouTube video playback options, external embed fallback/sandbox controls, admin validation, platform-specific fields, and Test preview behavior. diff --git a/package-lock.json b/package-lock.json index 21190d6..f4ba4ea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "lumi-bot", - "version": "0.1.4", + "version": "0.1.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "lumi-bot", - "version": "0.1.4", + "version": "0.1.5", "dependencies": { "adm-zip": "^0.5.12", "better-sqlite3": "^11.5.0", diff --git a/package.json b/package.json index b0adef8..667b0bd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lumi-bot", - "version": "0.1.4", + "version": "0.1.5", "private": true, "type": "commonjs", "scripts": { diff --git a/src/web/public/app.js b/src/web/public/app.js index c9d4802..1e83dd2 100644 --- a/src/web/public/app.js +++ b/src/web/public/app.js @@ -57,35 +57,36 @@ } }); - const editToggles = Array.from( - document.querySelectorAll("[data-edit-toggle]") - ); - if (editToggles.length) { - const editRows = Array.from(document.querySelectorAll("[data-edit-row]")); - const updateToggleStates = () => { - editToggles.forEach((button) => { - const key = button.dataset.editToggle; - const row = editRows.find((item) => item.dataset.editRow === key); - const isOpen = row?.classList.contains("is-open"); - button.setAttribute("aria-expanded", isOpen ? "true" : "false"); - }); - }; + const selectorEscape = (value) => { + if (window.CSS?.escape) return CSS.escape(value); + return String(value || "").replace(/["\\]/g, "\\$&"); + }; - editToggles.forEach((button) => { - button.addEventListener("click", () => { - const key = button.dataset.editToggle; - const target = editRows.find((item) => item.dataset.editRow === key); - const willOpen = target ? !target.classList.contains("is-open") : false; - editRows.forEach((row) => { - row.classList.remove("is-open"); - }); - if (target && willOpen) { - target.classList.add("is-open"); - } - updateToggleStates(); - }); + const updateEditToggleStates = () => { + document.querySelectorAll("[data-edit-toggle]").forEach((button) => { + const key = button.dataset.editToggle; + const row = key ? document.querySelector(`[data-edit-row="${selectorEscape(key)}"]`) : null; + const isOpen = row?.classList.contains("is-open"); + button.setAttribute("aria-expanded", isOpen ? "true" : "false"); }); - } + }; + + document.addEventListener("click", (event) => { + const button = event.target.closest("[data-edit-toggle]"); + if (!button) return; + event.preventDefault(); + const key = button.dataset.editToggle; + const target = key ? document.querySelector(`[data-edit-row="${selectorEscape(key)}"]`) : null; + const willOpen = target ? !target.classList.contains("is-open") : false; + document.querySelectorAll("[data-edit-row].is-open").forEach((row) => { + row.classList.remove("is-open"); + }); + if (target && willOpen) { + target.classList.add("is-open"); + } + updateEditToggleStates(); + }); + updateEditToggleStates(); document.querySelectorAll("[data-table]").forEach((table) => { const tbody = table.tBodies[0]; @@ -198,21 +199,23 @@ setGroupExpanded(group, false); }); - tbody.querySelectorAll("[data-command-toggle]").forEach((button) => { - button.addEventListener("click", (event) => { - event.preventDefault(); - const rootRow = button.closest("tr"); - if (!rootRow) { - return; - } - const key = rootRow.dataset.commandRoot; - const group = key ? commandGroups.get(key) : null; - if (!group) { - return; - } - const expanded = rootRow.dataset.expanded === "true"; - setGroupExpanded(group, !expanded); - }); + tbody.addEventListener("click", (event) => { + const button = event.target.closest("[data-command-toggle]"); + if (!button || !tbody.contains(button)) { + return; + } + event.preventDefault(); + const rootRow = button.closest("tr"); + if (!rootRow) { + return; + } + const key = rootRow.dataset.commandRoot; + const group = key ? commandGroups.get(key) : null; + if (!group) { + return; + } + const expanded = rootRow.dataset.expanded === "true"; + setGroupExpanded(group, !expanded); }); revealAnchorRow(); @@ -864,50 +867,56 @@ }); } - document.querySelectorAll("[data-copy]").forEach((button) => { - button.addEventListener("click", async () => { - const text = button.getAttribute("data-copy") || ""; - if (!text) { - return; - } - const label = button.querySelector("[data-copy-label]"); - const originalLabel = label ? label.textContent : ""; + const copyText = async (text) => { + if (navigator.clipboard?.writeText && window.isSecureContext) { + await navigator.clipboard.writeText(text); + return true; + } + const tempInput = document.createElement("textarea"); + tempInput.value = text; + tempInput.setAttribute("readonly", ""); + tempInput.style.position = "fixed"; + tempInput.style.left = "-9999px"; + tempInput.style.top = "0"; + document.body.appendChild(tempInput); + tempInput.focus(); + tempInput.select(); + try { + return document.execCommand("copy"); + } finally { + tempInput.remove(); + } + }; - const markCopied = () => { - button.classList.add("copied"); + document.addEventListener("click", async (event) => { + const button = event.target.closest("[data-copy]"); + if (!button) return; + event.preventDefault(); + const text = button.getAttribute("data-copy") || ""; + if (!text) { + return; + } + const label = button.querySelector("[data-copy-label]"); + const originalLabel = label ? label.textContent : ""; + + const markCopyResult = (copied) => { + button.classList.toggle("copied", copied); + button.classList.toggle("copy-failed", !copied); + if (label) { + label.textContent = copied ? "Copied" : "Copy failed"; + } + window.setTimeout(() => { + button.classList.remove("copied", "copy-failed"); if (label) { - label.textContent = "Copied"; + label.textContent = originalLabel; } - window.setTimeout(() => { - button.classList.remove("copied"); - if (label) { - label.textContent = originalLabel; - } - }, 1200); - }; + }, copied ? 1200 : 1800); + }; - try { - if (navigator.clipboard && navigator.clipboard.writeText) { - await navigator.clipboard.writeText(text); - markCopied(); - return; - } - } catch { - // Fall back to legacy copy. - } - - const tempInput = document.createElement("input"); - tempInput.value = text; - document.body.appendChild(tempInput); - tempInput.select(); - try { - document.execCommand("copy"); - markCopied(); - } catch { - // Ignore copy errors. - } finally { - tempInput.remove(); - } - }); + try { + markCopyResult(await copyText(text)); + } catch { + markCopyResult(false); + } }); })(); diff --git a/src/web/public/styles.css b/src/web/public/styles.css index 6d8d998..6a75d57 100644 --- a/src/web/public/styles.css +++ b/src/web/public/styles.css @@ -777,6 +777,23 @@ body { border-radius: 999px; } +.copy-pill.copy-failed { + border-color: var(--danger); +} + +.copy-pill.copy-failed::after { + content: "Copy failed"; + position: absolute; + top: -10px; + right: -6px; + background: var(--danger); + color: white; + font-size: 0.65rem; + padding: 2px 6px; + border-radius: 999px; + white-space: nowrap; +} + .copy-link { min-width: 96px; justify-content: center;