fix(aegis): 0.9.8 — address row columns, resizable QR, inline copy

The row's grid template still described the pre-0.9.2 layout — six
columns including an address cell that no longer exists — while the row
renders five. Every cell therefore sat one column left of where it
belonged: copy landed in the label's space, the amount in the old label
column, and the ⋯ in the amount column instead of the edge. That is the
whole cause of "the copy button collides with the amount" and "the last
edit button is not on the edge".

Now five columns for five cells: icon · label · copy · amount · menu,
8px gaps. The label is the flexible one, left-aligned, so it takes the
slack and truncates rather than squeezing the number. Verified at 520,
400 and 300px: no cell overlap at any width, no row overflow, and the
amount never clips — only long labels give way.

The copy glyph was the 📋 emoji, which has no glyph in this platform's
font stack and rendered as a tofu box that read like a stray character
stuck to the balance. Replaced with an inline SVG in both the row and
the address card. Both confirmation flashes now swap innerHTML rather
than textContent, which would have deleted the SVG and left a blank
square.

Address card: the address and its copy button share one row, so the
button sits at the end of the value it copies. The address clamps to two
lines and truncates beyond that instead of growing the card in a narrow
sidebar.

The QR is always visible and the panel is drag-resizable from a grip
under it (pointer events, arrow keys as a non-mouse path, clamped
90–420px, capped against the panel's own width so a size set on a wide
sidebar cannot overflow a narrow one, persisted). That replaces the
0.9.2 show/hide toggle — a size set once beats a binary, and it frees
the row Copy was sharing with a QR button.

drawQr was setting cv.style.width/height, which would have reset the
panel to its intrinsic size on every redraw — i.e. every time the
address changed. It now sets only the backing store and re-asserts the
user's size after drawing.

"Next unused address" is correct at the adapter level on both mainnet
and chipnet (tested: the index advances and the address changes), so the
reported failure is elsewhere. The handler was discarding the error and
flashing a bare "Failed", which is why there was nothing to diagnose; it
now surfaces the real message.
This commit is contained in:
Local Dev 2026-09-24 22:30:10 +02:00
parent 36012e7c26
commit 4c7d8e3f56
3 changed files with 164 additions and 46 deletions

View file

@ -1,7 +1,7 @@
{ {
"id": "aegis", "id": "aegis",
"name": "Aegis Wallet", "name": "Aegis Wallet",
"version": "0.9.7", "version": "0.9.8",
"category": "plugin", "category": "plugin",
"description": "Multi-chain wallet (BCH, BTC, TRX, ETH, SOL, SC, DGB) derived from your Theseus vault. Dapps get window.bitcoincash on .x sites; window.tronWeb / window.tronLink / window.ethereum / window.solana on any https page.", "description": "Multi-chain wallet (BCH, BTC, TRX, ETH, SOL, SC, DGB) derived from your Theseus vault. Dapps get window.bitcoincash on .x sites; window.tronWeb / window.tronLink / window.ethereum / window.solana on any https page.",
"author": "Silent Mode", "author": "Silent Mode",

View file

@ -220,6 +220,33 @@
.actions { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 10px; } .actions { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 10px; }
.qrwrap { display: grid; place-items: center; padding: 12px; background: #fff; border-radius: 10px; margin-top: 12px; } .qrwrap { display: grid; place-items: center; padding: 12px; background: #fff; border-radius: 10px; margin-top: 12px; }
.qrwrap[hidden] { display: none; } .qrwrap[hidden] { display: none; }
/* The canvas scales to whatever height the user dragged the panel to,
and never wider than the sidebar. aspect-ratio keeps it square. */
.qrwrap canvas { width: auto; height: auto; max-width: 100%;
aspect-ratio: 1 / 1; image-rendering: pixelated; }
/* Drag handle under the QR. A thin strip rather than a corner grip, so
it stays reachable at any sidebar width. */
.qrgrip { height: 14px; margin: 2px 0 -4px; display: grid; place-items: center;
cursor: ns-resize; touch-action: none; }
.qrgrip span { display: block; width: 34px; height: 3px; border-radius: 999px;
background: var(--line); transition: background .12s; }
.qrgrip:hover span, .qrgrip.dragging span { background: var(--acid, #d6ff3d); }
/* Address + inline copy. The value clamps to two lines; anything longer
is cut rather than growing the card. */
.addrrow { display: grid; grid-template-columns: minmax(0,1fr) 30px; gap: 6px;
align-items: start; }
.addrval { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
overflow: hidden; word-break: break-all; line-height: 1.35; }
.addrcopy { background: transparent; border: 1px solid var(--line); color: var(--dim);
cursor: pointer; width: 30px; height: 30px; border-radius: 7px;
display: inline-flex; align-items: center; justify-content: center;
line-height: 0; transition: color .12s, border-color .12s, background .12s; }
.addrcopy:hover { color: var(--acid, #d6ff3d); border-color: rgb(from var(--acid, #d6ff3d) r g b / .5); }
.addrcopy.copied { color: var(--acid, #d6ff3d); border-color: rgb(from var(--acid, #d6ff3d) r g b / .6); }
.addrcopy svg { width: 15px; height: 15px; display: block; }
/* Hide the action row entirely when its only child is hidden, so it
doesn't leave a gap under the address. */
#addrActions:not(:has(> :not([hidden]))) { display: none; }
canvas { image-rendering: pixelated; } canvas { image-rendering: pixelated; }
input[type=text], input[type=number], input[type=password], textarea { width: 100%; padding: 7px 9px; border-radius: 7px; background: var(--panel); input[type=text], input[type=number], input[type=password], textarea { width: 100%; padding: 7px 9px; border-radius: 7px; background: var(--panel);
border: 1px solid var(--line); color: var(--ink); font: inherit; font-size: 13px; outline: none; } border: 1px solid var(--line); color: var(--ink); font: inherit; font-size: 13px; outline: none; }
@ -415,31 +442,44 @@
"a" and "anthem…" occupy the same slot, amount is fixed to 90px so "a" and "anthem…" occupy the same slot, amount is fixed to 90px so
"0" and "0.000123" right-align identically, actions are fixed to "0" and "0.000123" right-align identically, actions are fixed to
46px. Net result: columns look like a table, not a flex mess. */ 46px. Net result: columns look like a table, not a flex mess. */
/* 0.9.8: five columns for the five cells the row actually renders —
icon · label · copy · amount · menu. The template still described the
pre-0.9.2 layout (six columns, including an address cell that no
longer exists), so every cell sat one column left of where it
belonged: copy landed in the label's space, the amount in the old
label column, and the ⋯ in the amount column instead of the edge.
That is what made copy and amount collide.
The label is the flexible column so it takes the slack and truncates
rather than squeezing the number, and the menu is pinned right. */
.wstrip .warow { display: grid; .wstrip .warow { display: grid;
grid-template-columns: 16px minmax(0,1fr) 22px 68px 90px 46px; grid-template-columns: 16px minmax(0,1fr) 24px auto 26px;
gap: 6px; align-items: center; padding: 4px 4px; gap: 8px; align-items: center; padding: 5px 6px;
border-radius: 6px; border: 1px solid transparent; cursor: pointer; min-height: 28px; } border-radius: 6px; border: 1px solid transparent; cursor: pointer; min-height: 30px; }
.wstrip .warow:hover { background: rgba(255,255,255,.04); } .wstrip .warow:hover { background: rgba(255,255,255,.04); }
.wstrip .warow.on { background: rgb(from var(--acid, #d6ff3d) r g b / .10); .wstrip .warow.on { background: rgb(from var(--acid, #d6ff3d) r g b / .10);
border-color: rgb(from var(--acid, #d6ff3d) r g b / .35); } border-color: rgb(from var(--acid, #d6ff3d) r g b / .35); }
.wstrip .warow .waaddr { font: 12px/1.15 ui-monospace, Consolas, monospace; color: var(--ink); .wstrip .warow .waaddr { font: 12px/1.15 ui-monospace, Consolas, monospace; color: var(--ink);
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; } overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
/* Copy chip anchored right after the address. Fixed 22px column so the /* Copy chip: its own 24px column between label and amount, so it can
copy button sits at the same x on every row. */ never crowd the number. SVG rather than the 📋 emoji, which rendered
as a tofu box here and read as a glyph stuck to the balance. */
.wstrip .warow .wacopy { background: transparent; border: 0; color: var(--dim); cursor: pointer; .wstrip .warow .wacopy { background: transparent; border: 0; color: var(--dim); cursor: pointer;
padding: 2px 4px; border-radius: 4px; font-size: 11px; line-height: 1; padding: 0; border-radius: 5px; line-height: 0;
transition: color .12s; justify-self: start; } width: 24px; height: 22px; display: inline-flex;
align-items: center; justify-content: center;
transition: color .12s, background .12s; justify-self: center; }
.wstrip .warow .wacopy svg { width: 13px; height: 13px; display: block; }
.wstrip .warow .wacopy:hover { color: var(--acid, #d6ff3d); background: rgba(255,255,255,.06); } .wstrip .warow .wacopy:hover { color: var(--acid, #d6ff3d); background: rgba(255,255,255,.06); }
.wstrip .warow .wacopy.copied { color: var(--acid, #d6ff3d); } .wstrip .warow .wacopy.copied { color: var(--acid, #d6ff3d); }
/* Label pill: fixed 68px column. Even an empty label renders an /* Label pill, left-aligned in the flexible column so a name reads as
invisible placeholder so the amount column stays put. Long labels the row's identity rather than floating mid-row. Truncates with an
truncate with ellipsis inside the pill (max-width: 100% of column). */ ellipsis instead of pushing the number around. */
.wstrip .warow .walabel { font-size: 11px; color: var(--mut); .wstrip .warow .walabel { font-size: 11.5px; color: var(--ink);
padding: 1px 6px; border-radius: 999px; padding: 2px 8px; border-radius: 999px;
background: rgba(255,255,255,.06); background: rgba(255,255,255,.07);
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
max-width: 100%; box-sizing: border-box; max-width: 100%; box-sizing: border-box;
justify-self: center; } justify-self: start; }
.wstrip .warow .walabel:empty { visibility: hidden; } .wstrip .warow .walabel:empty { visibility: hidden; }
/* Balance shares a single line with everything else — no vertical /* Balance shares a single line with everything else — no vertical
amount/fiat stack. Ticker follows the number in a dim tone so the amount/fiat stack. Ticker follows the number in a dim tone so the
@ -451,7 +491,8 @@
justify-self: end; overflow: hidden; } justify-self: end; overflow: hidden; }
.wstrip .warow .waamt .watkr { color: var(--dim); font-size: 11px; font-weight: 500; } .wstrip .warow .waamt .watkr { color: var(--dim); font-size: 11px; font-weight: 500; }
.wstrip .warow .wact { background: transparent; border: 0; color: var(--dim); cursor: pointer; .wstrip .warow .wact { background: transparent; border: 0; color: var(--dim); cursor: pointer;
padding: 2px 5px; border-radius: 4px; font-size: 12.5px; line-height: 1; } padding: 2px 4px; border-radius: 4px; font-size: 13px; line-height: 1;
justify-self: end; }
.wstrip .warow .wact:hover { color: var(--acid, #d6ff3d); background: rgba(255,255,255,.06); } .wstrip .warow .wact:hover { color: var(--acid, #d6ff3d); background: rgba(255,255,255,.06); }
/* Per-address asset list (0.8.8). The count chip expands the row into a /* Per-address asset list (0.8.8). The count chip expands the row into a
nested list of what that ONE address holds beyond the native coin. */ nested list of what that ONE address holds beyond the native coin. */
@ -627,13 +668,21 @@
the fold. Explorer / Faucet moved to the header. --> the fold. Explorer / Faucet moved to the header. -->
<div class="card"> <div class="card">
<div class="lbl">Receiving address <span id="addrMeta"></span></div> <div class="lbl">Receiving address <span id="addrMeta"></span></div>
<div class="mono" id="addr">—</div> <!-- 0.9.8: address and its copy button share one row, so the
<div class="actions"> button sits at the end of the value it copies instead of on a
<button class="btn sm" id="copyAddr">Copy</button> separate action line. The address clamps to two lines and
<button class="btn sm" id="qrToggle" aria-expanded="false">QR</button> truncates beyond that, rather than growing the card in a
narrow sidebar. -->
<div class="addrrow">
<div class="mono addrval" id="addr">—</div>
<button class="addrcopy" id="copyAddr" title="Copy this address"></button>
</div>
<div class="actions" id="addrActions">
<button class="btn sm" id="nextAddr" hidden>Next unused address</button> <button class="btn sm" id="nextAddr" hidden>Next unused address</button>
</div> </div>
<div class="qrwrap" id="qrWrap" hidden><canvas id="qr" width="200" height="200"></canvas></div> <!-- Drag the bottom edge to resize the QR. Height persists. -->
<div class="qrwrap" id="qrWrap"><canvas id="qr" width="200" height="200"></canvas></div>
<div class="qrgrip" id="qrGrip" title="Drag to resize the QR code"><span></span></div>
</div> </div>
<div class="card" id="tokensCard" hidden style="margin-top:12px"> <div class="card" id="tokensCard" hidden style="margin-top:12px">
<div class="lbl">Assets <span class="hint" id="tokensCount"></span></div> <div class="lbl">Assets <span class="hint" id="tokensCount"></span></div>

View file

@ -2500,6 +2500,12 @@ function wireStripDragDrop(el, chainGroups) {
}); });
} }
// Copy glyph as inline SVG. The 📋 emoji has no glyph in this platform's
// font stack, so it rendered as a tofu box that read like a stray
// character stuck to the balance beside it.
const COPY_ICON = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="11" height="11" rx="2"/><path d="M5 15V5a2 2 0 0 1 2-2h8"/></svg>`;
const CHECK_ICON = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg>`;
// Inline replacement for the modal address list. Rendered directly into // Inline replacement for the modal address list. Rendered directly into
// the wallet strip element when stripView.mode === "addresses". Header // the wallet strip element when stripView.mode === "addresses". Header
// row has a back arrow (returns to the coins summary) and the coin's // row has a back arrow (returns to the coins summary) and the coin's
@ -2557,7 +2563,7 @@ function renderInlineCoinList(el, groupKey, group) {
return `<div class="warow ${on}" data-listpick="${esc(w.id)}" title="${esc(w.label || fullAddr)}"> return `<div class="warow ${on}" data-listpick="${esc(w.id)}" title="${esc(w.label || fullAddr)}">
<span class="wcell">${logoSvg(meta.logo, 14)}</span> <span class="wcell">${logoSvg(meta.logo, 14)}</span>
<span class="walabel walabel-wide" title="${esc(w.label || "")}">${esc(w.label || shortName || "—")}</span> <span class="walabel walabel-wide" title="${esc(w.label || "")}">${esc(w.label || shortName || "—")}</span>
${fullAddr ? `<button class="wacopy" data-lpcopy="${esc(fullAddr)}" title="Copy full address (${esc(fullAddr)})">📋</button>` : `<span></span>`} ${fullAddr ? `<button class="wacopy" data-lpcopy="${esc(fullAddr)}" title="Copy full address (${esc(fullAddr)})">${COPY_ICON}</button>` : `<span></span>`}
<span class="waamt"><span>${esc(bal)}</span><span class="watkr">${esc(meta.ticker)}</span></span> <span class="waamt"><span>${esc(bal)}</span><span class="watkr">${esc(meta.ticker)}</span></span>
<span style="display:inline-flex;gap:2px;justify-self:end;align-items:center">${rowMenu}</span> <span style="display:inline-flex;gap:2px;justify-self:end;align-items:center">${rowMenu}</span>
</div>`; </div>`;
@ -2633,9 +2639,11 @@ function renderInlineCoinList(el, groupKey, group) {
document.body.appendChild(ta); ta.select(); document.body.appendChild(ta); ta.select();
try { document.execCommand("copy"); } finally { ta.remove(); } try { document.execCommand("copy"); } finally { ta.remove(); }
} }
const prev = b.textContent; // innerHTML, not textContent — the button holds an inline SVG now,
b.classList.add("copied"); b.textContent = "✓"; // and assigning textContent would destroy it and leave a blank
setTimeout(() => { b.classList.remove("copied"); b.textContent = prev; }, 1000); // square once the confirmation timed out.
b.classList.add("copied"); b.innerHTML = CHECK_ICON;
setTimeout(() => { b.classList.remove("copied"); b.innerHTML = COPY_ICON; }, 1000);
} catch {} } catch {}
})); }));
el.querySelectorAll("[data-lpedit]").forEach((b) => b.addEventListener("click", (e) => { el.querySelectorAll("[data-lpedit]").forEach((b) => b.addEventListener("click", (e) => {
@ -3403,42 +3411,103 @@ function drawQr(text) {
try { q = window.QR.build(text); } catch { g.clearRect(0, 0, cv.width, cv.height); return; } try { q = window.QR.build(text); } catch { g.clearRect(0, 0, cv.width, cv.height); return; }
const scale = Math.max(2, Math.floor(200 / (q.size + 2))); const scale = Math.max(2, Math.floor(200 / (q.size + 2)));
const px = (q.size + 2) * scale; const px = (q.size + 2) * scale;
// Backing store only. The DISPLAY size belongs to the user's dragged
// preference — setting cv.style here would reset the panel to its
// intrinsic size on every redraw (i.e. whenever the address changed).
cv.width = cv.height = px; cv.width = cv.height = px;
cv.style.width = cv.style.height = px + "px";
g.fillStyle = "#fff"; g.fillRect(0, 0, px, px); g.fillStyle = "#fff"; g.fillRect(0, 0, px, px);
g.fillStyle = "#000"; g.fillStyle = "#000";
for (let r = 0; r < q.size; r++) for (let c = 0; c < q.size; c++) if (q.modules[r][c]) g.fillRect((c + 1) * scale, (r + 1) * scale, scale, scale); for (let r = 0; r < q.size; r++) for (let c = 0; c < q.size; c++) if (q.modules[r][c]) g.fillRect((c + 1) * scale, (r + 1) * scale, scale, scale);
applyQrSize();
} }
// ---- receive actions ------------------------------------------------------- // ---- receive actions -------------------------------------------------------
// 0.9.2: the QR is collapsed by default — it was 200px of permanent // 0.9.8: the QR is always visible and the panel is drag-resizable, which
// vertical space above the address, which pushed the asset list off // replaces the 0.9.2 show/hide toggle — a size the user sets once is a
// screen. The choice sticks per-user because someone who receives by // better answer than a binary, and it means the QR button no longer
// QR wants it every time, and someone who copies never wants it. // competes with Copy for the one row that gets used.
(function wireQrToggle() { let applyQrSize = () => {};
const btn = $("qrToggle"); if (!btn) return; (function wireQrResize() {
let open = false; const grip = $("qrGrip"), wrap = $("qrWrap");
try { open = localStorage.getItem("aegis/qrOpen") === "1"; } catch {} if (!grip || !wrap) return;
const paint = () => { const MIN = 90, MAX = 420;
const wrap = $("qrWrap"); if (!wrap) return; const clamp = (v) => Math.max(MIN, Math.min(MAX, Math.round(v)));
wrap.hidden = !open; let size = 200;
btn.setAttribute("aria-expanded", open ? "true" : "false"); try {
btn.classList.toggle("primary", open); const saved = Number(localStorage.getItem("aegis/qrSize"));
if (Number.isFinite(saved) && saved > 0) size = clamp(saved);
} catch {}
const apply = () => {
// Cap against the actual panel width too — a size dragged wide on a
// roomy sidebar must not overflow when the sidebar is narrowed later.
const room = Math.max(MIN, (wrap.clientWidth || MAX) - 24);
const px = Math.min(size, room);
const cv = $("qr");
if (cv) { cv.style.width = px + "px"; cv.style.height = px + "px"; }
}; };
btn.addEventListener("click", () => { applyQrSize = apply;
open = !open; apply();
try { localStorage.setItem("aegis/qrOpen", open ? "1" : "0"); } catch {} window.addEventListener("resize", apply);
paint(); let startY = 0, startSize = 0, active = false;
const onMove = (e) => {
if (!active) return;
e.preventDefault();
size = clamp(startSize + (e.clientY - startY));
apply();
};
const onUp = () => {
if (!active) return;
active = false;
grip.classList.remove("dragging");
try { grip.releasePointerCapture?.(grip._pid); } catch {}
try { localStorage.setItem("aegis/qrSize", String(size)); } catch {}
window.removeEventListener("pointermove", onMove);
window.removeEventListener("pointerup", onUp);
};
grip.addEventListener("pointerdown", (e) => {
active = true; startY = e.clientY;
const cv = $("qr");
startSize = cv ? (parseInt(cv.style.width, 10) || 200) : 200;
grip._pid = e.pointerId;
grip.classList.add("dragging");
try { grip.setPointerCapture(e.pointerId); } catch {}
window.addEventListener("pointermove", onMove);
window.addEventListener("pointerup", onUp);
e.preventDefault();
}); });
paint(); // Keyboard affordance — a drag-only control is unusable without a mouse.
grip.tabIndex = 0;
grip.addEventListener("keydown", (e) => {
const step = e.shiftKey ? 24 : 8;
if (e.key === "ArrowUp") { size = clamp(size - step); }
else if (e.key === "ArrowDown") { size = clamp(size + step); }
else return;
e.preventDefault(); apply();
try { localStorage.setItem("aegis/qrSize", String(size)); } catch {}
});
})();
// The inline copy button holds an SVG; set it once at load.
(function paintCopyIcon() {
const b = $("copyAddr");
if (b && !b.firstElementChild) b.innerHTML = COPY_ICON;
})(); })();
$("copyAddr").addEventListener("click", async () => { $("copyAddr").addEventListener("click", async () => {
try { await navigator.clipboard.writeText(sel().address); flash($("copyAddr"), "Copied"); } catch {} const b = $("copyAddr");
try {
await navigator.clipboard.writeText(sel().address);
// innerHTML, not flash() — flash() swaps textContent, which would
// delete the inline SVG and leave an empty square behind.
b.classList.add("copied"); b.innerHTML = CHECK_ICON;
setTimeout(() => { b.classList.remove("copied"); b.innerHTML = COPY_ICON; }, 1000);
} catch {}
}); });
$("nextAddr").addEventListener("click", async () => { $("nextAddr").addEventListener("click", async () => {
try { const s = await S.invoke("nextAddress"); state.selected = { ...state.selected, ...s }; render(); } try { const s = await S.invoke("nextAddress"); state.selected = { ...state.selected, ...s }; render(); }
catch (e) { flash($("nextAddr"), "Failed"); } // The old handler discarded the error and flashed a bare "Failed", so
// neither the user nor a maintainer could tell WHY — which is why
// "next unused address doesn't work" had no diagnosable cause. Show it.
catch (e) { showErr("Next address: " + cleanErr(e)); }
}); });
$("viewAddr").addEventListener("click", () => openUrl(explorerHref(sel().explorerAddr, sel().address))); $("viewAddr").addEventListener("click", () => openUrl(explorerHref(sel().explorerAddr, sel().address)));
$("openFaucet").addEventListener("click", () => sel().faucet && openUrl(sel().faucet)); $("openFaucet").addEventListener("click", () => sel().faucet && openUrl(sel().faucet));