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",
"name": "Aegis Wallet",
"version": "0.15.0",
"version": "0.16.0",
"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.",
"author": "Silent Mode",

View file

@ -768,10 +768,19 @@
<button data-rcv-mode="consolidate" type="button">Consolidate <span id="rcvConsolidateCount" class="hint"></span></button>
</div>
<div id="rcvNormal">
<!-- 0.9.2: address first, assets next, QR last and collapsed.
The QR was 200px of always-on vertical space above the thing
people actually came for, and it pushed the token list below
the fold. Explorer / Faucet moved to the header. -->
<!-- 0.16.0: Address-and-QR and Assets are PEERS, switched by a
persistent chip row, instead of one stacked column. Stacking meant
the QR sat between the address and the token list, so assets were
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="lbl">Receiving address <span id="addrMeta"></span></div>
<!-- 0.9.8: address and its copy button share one row, so the
@ -790,10 +799,13 @@
<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 class="card" id="tokensCard" hidden style="margin-top:12px">
<div class="lbl">Assets <span class="hint" id="tokensCount"></span></div>
<div id="tokensList" class="kv"></div>
<div class="hint" id="tokensHint">Tokens held by this wallet.</div>
</div><!-- /rcvAddressPane -->
<div id="rcvAssetsPane" hidden>
<div class="card" id="tokensCard">
<div class="lbl">Assets <span class="hint" id="tokensCount"></span></div>
<div id="tokensList" class="kv"></div>
<div class="hint" id="tokensHint">Tokens held by this wallet.</div>
</div>
</div>
</div><!-- /rcvNormal -->
<!-- Consolidate view (inline, replaces Receive body when active). -->

View file

@ -2136,6 +2136,29 @@ function paintSendMode() {
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() {
const normal = $("rcvNormal"); const cons = $("rcvConsolidate");
if (!normal || !cons) return;
@ -2522,15 +2545,16 @@ function renderWalletStrip() {
pill = `<span class="wnetpill ${cls}" title="${esc(tip)}">${esc(meta.networkLabel)}</span>`;
}
// ▾ chevron shown only when the chain has more than one network —
// otherwise the click affordance would be a lie and the ticker acts
// like plain text.
const multiNet = nets.length > 1;
const chevron = multiNet ? `<span class="wchev" aria-hidden="true">▾</span>` : "";
const nameCls = multiNet ? "wcname wswitchable" : "wcname";
const nameTitle = multiNet
? `Switch network — ${nets.map((n) => networkLabelFor(chain, n, n)).join(" / ")}`
: meta.coinName;
// 0.16.0: no ▾ network dropdown on the coin row. The coin view already
// carries real network chips (data-browse-net — they filter the list and
// show a per-network count), so a popover menu doing the same job was a
// second way to do one thing, and the only one that hid its options
// behind a click. Chips are the single network control now; the ticker
// is plain text again.
const multiNet = false;
const chevron = "";
const nameCls = "wcname";
const nameTitle = meta.coinName;
// Single wallet under the active network → click selects it. Multi-
// wallet: click selects the "active" wallet for this bucket (defaults
@ -3323,6 +3347,7 @@ function render() {
if (!showToggle) { sendMode = "send"; rcvMode = "receive"; }
paintSendMode();
paintRcvMode();
paintRcvView();
}
// 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}`;
}
// 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() {
const s = sel();
const card = $("tokensCard");
@ -3362,7 +3405,7 @@ function renderTokens() {
// SOL wallets: SPL tokens with a Send button (existing flow).
if (chain() === "sol") {
const tokens = s?.tokens || [];
card.hidden = tokens.length === 0;
setAssetsCount(tokens.length);
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 (!tokens.length) return;
@ -3388,7 +3431,7 @@ function renderTokens() {
if (chain() === "bch") {
const balances = s?.tokenBalances || {};
const cats = Object.keys(balances);
card.hidden = cats.length === 0;
setAssetsCount(cats.length);
const hintEl = $("tokensHint");
if (hintEl) hintEl.textContent = "CashTokens held by this wallet. Names come from BCMR — configure custom registries in Settings.";
if (!cats.length) return;
@ -3432,8 +3475,8 @@ function renderTokens() {
// through to a hidden card the way ETH did until 0.9.3.
{
const tokens = s?.tokens || [];
if (!tokens.length) { card.hidden = true; return; }
card.hidden = false;
if (!tokens.length) { setAssetsCount(0); return; }
setAssetsCount(tokens.length);
const hintEl = $("tokensHint");
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.`;
@ -3830,10 +3873,17 @@ document.querySelectorAll("[data-send-mode]").forEach((b) => b.addEventListener(
consolidateInlineHost = null; // force re-render on next switch
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", () => {
rcvMode = b.dataset.rcvMode;
consolidateInlineHost = null;
paintRcvMode();
paintRcvView();
}));
["sendTo", "sendAmt"].forEach((id) => $(id).addEventListener("input", () => {
if (id === "sendAmt" && sendMax) return;