feat(aegis): 0.16.0 — Address & QR and Assets are peer chips, and the coin row loses its dropdown

Two changes to the same idea: one way to do each thing, and the same shape
for every coin.

**Assets is no longer buried under the QR.** The Receive body stacked
address → QR → assets in one column, so in a 380px panel the token list sat
permanently below the fold behind 200px of QR — and a coin holding no tokens
simply lost a section, which made every coin look structurally different.
Address & QR and Assets are now peers behind a persistent chip row. The count
rides on the chip, and a wallet with nothing says "No assets held by this
wallet" instead of the section vanishing. That mattered more than it sounds:
a chipnet wallet with 46 CashToken categories was indistinguishable from an
empty one until 0.15.0, because the only signal was an absent card.

The choice persists across re-renders and wallet switches — comparing token
balances between wallets should not throw you back to the QR on every click.
Switching back to Address redraws the QR, since a canvas sized while hidden
comes out blank and renderReceive only repaints when the address changes.

**The coin row's ▾ network dropdown is gone.** The coin view already carries
real network chips (data-browse-net: they filter the wallet list and show a
per-network count), so the popover was a second control for one job — and the
only one that hid its options behind a click. Chips are the single network
control now and the ticker is plain text again.

Verified in the panel: chips switch panes and track their own state, the
count shows, the empty state reads correctly, the QR survives the round trip,
and no .wchev / .wcname.wswitchable remain in the DOM.
This commit is contained in:
Local Dev 2026-09-28 23:05:48 +02:00
parent 74224cd7dd
commit b42f304ce8
3 changed files with 84 additions and 22 deletions

View file

@ -1,7 +1,7 @@
{ {
"id": "aegis", "id": "aegis",
"name": "Aegis Wallet", "name": "Aegis Wallet",
"version": "0.15.0", "version": "0.16.0",
"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

@ -768,10 +768,19 @@
<button data-rcv-mode="consolidate" type="button">Consolidate <span id="rcvConsolidateCount" class="hint"></span></button> <button data-rcv-mode="consolidate" type="button">Consolidate <span id="rcvConsolidateCount" class="hint"></span></button>
</div> </div>
<div id="rcvNormal"> <div id="rcvNormal">
<!-- 0.9.2: address first, assets next, QR last and collapsed. <!-- 0.16.0: Address-and-QR and Assets are PEERS, switched by a
The QR was 200px of always-on vertical space above the thing persistent chip row, instead of one stacked column. Stacking meant
people actually came for, and it pushed the token list below the QR sat between the address and the token list, so assets were
the fold. Explorer / Faucet moved to the header. --> permanently below the fold in a 380px panel; and it made every coin
look different, since a coin with no tokens simply lost a section.
Chips give one shape for every coin and a visible answer either
way. (0.9.2 had already reordered the stack for the same reason —
this replaces that workaround.) -->
<div id="rcvViewToggle" class="modetoggle">
<button data-rcv-view="address" class="on" type="button">Address &amp; QR</button>
<button data-rcv-view="assets" type="button">Assets <span id="rcvAssetsCount" class="hint"></span></button>
</div>
<div id="rcvAddressPane">
<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>
<!-- 0.9.8: address and its copy button share one row, so the <!-- 0.9.8: address and its copy button share one row, so the
@ -790,11 +799,14 @@
<div class="qrwrap" id="qrWrap"><canvas id="qr" width="200" height="200"></canvas></div> <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 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><!-- /rcvAddressPane -->
<div id="rcvAssetsPane" hidden>
<div class="card" id="tokensCard">
<div class="lbl">Assets <span class="hint" id="tokensCount"></span></div> <div class="lbl">Assets <span class="hint" id="tokensCount"></span></div>
<div id="tokensList" class="kv"></div> <div id="tokensList" class="kv"></div>
<div class="hint" id="tokensHint">Tokens held by this wallet.</div> <div class="hint" id="tokensHint">Tokens held by this wallet.</div>
</div> </div>
</div>
</div><!-- /rcvNormal --> </div><!-- /rcvNormal -->
<!-- Consolidate view (inline, replaces Receive body when active). --> <!-- Consolidate view (inline, replaces Receive body when active). -->
<div id="rcvConsolidate" hidden> <div id="rcvConsolidate" hidden>

View file

@ -2136,6 +2136,29 @@ function paintSendMode() {
if (host && consolidateInlineHost !== host) { consolidateInlineHost = host; renderConsolidateInline(host); } if (host && consolidateInlineHost !== host) { consolidateInlineHost = host; renderConsolidateInline(host); }
} }
} }
// Which half of the Receive pane is showing. Persistent: the choice sticks
// across re-renders and across wallet switches, because someone comparing
// token balances between wallets should not be knocked back to the QR on
// every click.
let rcvView = "address";
try { rcvView = localStorage.getItem("aegis/rcvView") === "assets" ? "assets" : "address"; } catch (_e) {}
function paintRcvView() {
const addr = $("rcvAddressPane"), assets = $("rcvAssetsPane");
if (!addr || !assets) return;
document.querySelectorAll("[data-rcv-view]").forEach((b) =>
b.classList.toggle("on", b.dataset.rcvView === rcvView));
addr.hidden = rcvView !== "address";
assets.hidden = rcvView !== "assets";
// The QR's backing store is sized from its rendered box, so one drawn
// while its pane was hidden comes out blank. Redraw on reveal — and note
// renderReceive only redraws when the address CHANGES, so switching back
// to an unchanged address would otherwise never repaint it.
if (rcvView === "address") {
const a = sel()?.address || "";
if (a) { try { drawQr(qrPayload(chain(), a, sel()?.network)); } catch (_e) {} }
}
}
function paintRcvMode() { function paintRcvMode() {
const normal = $("rcvNormal"); const cons = $("rcvConsolidate"); const normal = $("rcvNormal"); const cons = $("rcvConsolidate");
if (!normal || !cons) return; if (!normal || !cons) return;
@ -2522,15 +2545,16 @@ function renderWalletStrip() {
pill = `<span class="wnetpill ${cls}" title="${esc(tip)}">${esc(meta.networkLabel)}</span>`; pill = `<span class="wnetpill ${cls}" title="${esc(tip)}">${esc(meta.networkLabel)}</span>`;
} }
// ▾ chevron shown only when the chain has more than one network — // 0.16.0: no ▾ network dropdown on the coin row. The coin view already
// otherwise the click affordance would be a lie and the ticker acts // carries real network chips (data-browse-net — they filter the list and
// like plain text. // show a per-network count), so a popover menu doing the same job was a
const multiNet = nets.length > 1; // second way to do one thing, and the only one that hid its options
const chevron = multiNet ? `<span class="wchev" aria-hidden="true">▾</span>` : ""; // behind a click. Chips are the single network control now; the ticker
const nameCls = multiNet ? "wcname wswitchable" : "wcname"; // is plain text again.
const nameTitle = multiNet const multiNet = false;
? `Switch network — ${nets.map((n) => networkLabelFor(chain, n, n)).join(" / ")}` const chevron = "";
: meta.coinName; const nameCls = "wcname";
const nameTitle = meta.coinName;
// Single wallet under the active network → click selects it. Multi- // Single wallet under the active network → click selects it. Multi-
// wallet: click selects the "active" wallet for this bucket (defaults // wallet: click selects the "active" wallet for this bucket (defaults
@ -3323,6 +3347,7 @@ function render() {
if (!showToggle) { sendMode = "send"; rcvMode = "receive"; } if (!showToggle) { sendMode = "send"; rcvMode = "receive"; }
paintSendMode(); paintSendMode();
paintRcvMode(); paintRcvMode();
paintRcvView();
} }
// Sum every wallet's confirmed+unconfirmed × price and show "≈ $X across N // Sum every wallet's confirmed+unconfirmed × price and show "≈ $X across N
@ -3355,6 +3380,24 @@ function renderPortfolio() {
el.innerHTML = `Portfolio: <b>${esc(fmtFiat(total))}</b> across ${wallets.length} ${noun}`; el.innerHTML = `Portfolio: <b>${esc(fmtFiat(total))}</b> across ${wallets.length} ${noun}`;
} }
// Assets is a chip now, not a card that disappears when the count is zero.
// The count rides on the chip and an empty wallet says so in the pane, so
// "this wallet holds nothing" is a visible answer rather than a missing
// section the user has to infer — which is exactly how 46 CashTokens managed
// to look like a working, empty wallet before 0.15.0.
function setAssetsCount(n) {
const chip = $("rcvAssetsCount");
if (chip) chip.textContent = n ? String(n) : "";
const card = $("tokensCard");
if (card) card.hidden = false;
if (!n) {
const el = $("tokensList");
if (el) el.innerHTML = `<div class="hint" style="padding:4px 2px">No assets held by this wallet.</div>`;
const cnt = $("tokensCount");
if (cnt) cnt.textContent = "";
}
}
function renderTokens() { function renderTokens() {
const s = sel(); const s = sel();
const card = $("tokensCard"); const card = $("tokensCard");
@ -3362,7 +3405,7 @@ function renderTokens() {
// SOL wallets: SPL tokens with a Send button (existing flow). // SOL wallets: SPL tokens with a Send button (existing flow).
if (chain() === "sol") { if (chain() === "sol") {
const tokens = s?.tokens || []; const tokens = s?.tokens || [];
card.hidden = tokens.length === 0; setAssetsCount(tokens.length);
const hintEl = $("tokensHint"); const hintEl = $("tokensHint");
if (hintEl) hintEl.textContent = "SPL tokens held by this wallet. Send by picking one under the Send tab's Asset dropdown."; if (hintEl) hintEl.textContent = "SPL tokens held by this wallet. Send by picking one under the Send tab's Asset dropdown.";
if (!tokens.length) return; if (!tokens.length) return;
@ -3388,7 +3431,7 @@ function renderTokens() {
if (chain() === "bch") { if (chain() === "bch") {
const balances = s?.tokenBalances || {}; const balances = s?.tokenBalances || {};
const cats = Object.keys(balances); const cats = Object.keys(balances);
card.hidden = cats.length === 0; setAssetsCount(cats.length);
const hintEl = $("tokensHint"); const hintEl = $("tokensHint");
if (hintEl) hintEl.textContent = "CashTokens held by this wallet. Names come from BCMR — configure custom registries in Settings."; if (hintEl) hintEl.textContent = "CashTokens held by this wallet. Names come from BCMR — configure custom registries in Settings.";
if (!cats.length) return; if (!cats.length) return;
@ -3432,8 +3475,8 @@ function renderTokens() {
// through to a hidden card the way ETH did until 0.9.3. // through to a hidden card the way ETH did until 0.9.3.
{ {
const tokens = s?.tokens || []; const tokens = s?.tokens || [];
if (!tokens.length) { card.hidden = true; return; } if (!tokens.length) { setAssetsCount(0); return; }
card.hidden = false; setAssetsCount(tokens.length);
const hintEl = $("tokensHint"); const hintEl = $("tokensHint");
const kindLabel = chain() === "trx" ? "TRC20" : chain() === "eth" ? "ERC20" : "Tokens"; const kindLabel = chain() === "trx" ? "TRC20" : chain() === "eth" ? "ERC20" : "Tokens";
if (hintEl) hintEl.textContent = `${kindLabel} tokens held by this wallet. Read-only in this build — open the explorer from the header to move them.`; if (hintEl) hintEl.textContent = `${kindLabel} tokens held by this wallet. Read-only in this build — open the explorer from the header to move them.`;
@ -3830,10 +3873,17 @@ document.querySelectorAll("[data-send-mode]").forEach((b) => b.addEventListener(
consolidateInlineHost = null; // force re-render on next switch consolidateInlineHost = null; // force re-render on next switch
paintSendMode(); paintSendMode();
})); }));
// Address & QR / Assets — the persistent pair inside the Receive body.
document.querySelectorAll("[data-rcv-view]").forEach((b) => b.addEventListener("click", () => {
rcvView = b.dataset.rcvView === "assets" ? "assets" : "address";
try { localStorage.setItem("aegis/rcvView", rcvView); } catch (_e) {}
paintRcvView();
}));
document.querySelectorAll("[data-rcv-mode]").forEach((b) => b.addEventListener("click", () => { document.querySelectorAll("[data-rcv-mode]").forEach((b) => b.addEventListener("click", () => {
rcvMode = b.dataset.rcvMode; rcvMode = b.dataset.rcvMode;
consolidateInlineHost = null; consolidateInlineHost = null;
paintRcvMode(); paintRcvMode();
paintRcvView();
})); }));
["sendTo", "sendAmt"].forEach((id) => $(id).addEventListener("input", () => { ["sendTo", "sendAmt"].forEach((id) => $(id).addEventListener("input", () => {
if (id === "sendAmt" && sendMax) return; if (id === "sendAmt" && sendMax) return;