diff --git a/bundled-addons/aegis/panel.html b/bundled-addons/aegis/panel.html index bdfda373..dba704de 100644 --- a/bundled-addons/aegis/panel.html +++ b/bundled-addons/aegis/panel.html @@ -41,6 +41,11 @@ .bal .big { font-size: 32px; font-weight: 680; letter-spacing: -.6px; line-height: 1.1; display: flex; align-items: baseline; gap: 5px; flex-wrap: wrap; } .bal .big .dec { color: var(--dim); font-weight: 620; } + /* Marks a shortened tail, and only appears when one was actually cut. */ + .bal .big .more { opacity: .5; margin-left: 1px; } + /* Pointer only when there is something to toggle — a balance that already + fits shows no affordance it cannot honour. */ + .bal .big #balMain.balx { cursor: pointer; } .bal .big small { font-size: 13px; color: var(--mut); font-weight: 500; margin-left: 0; } /* Scoped to the hero — .fiat is also the Send form's conversion line. */ .bal .big .fiat { font-size: 14px; margin-left: 6px; } diff --git a/bundled-addons/aegis/panel.js b/bundled-addons/aegis/panel.js index 3d0b6e07..0e8d21ff 100644 --- a/bundled-addons/aegis/panel.js +++ b/bundled-addons/aegis/panel.js @@ -140,22 +140,79 @@ function fmtBig(units, dec) { const s = (Number(units || 0) / Math.pow(10, d)).toFixed(d); return s.replace(/(\.\d*?[1-9])0+$|\.0+$/, "$1"); } -// Paint the header balance. Groups the integer part with thousands -// separators and wraps the fraction in .dec so CSS can dim it — the -// "$98,230.02" treatment the reference wallets lead with. Anything that is -// not a bare number (an em dash, a skeleton, an error) falls through to -// plain text. The regex guarantees the interpolated pieces are digits, -// sign and dot only, so building this with innerHTML cannot inject markup. +// Header balance. Groups the integer part and dims the fraction — the +// "98,230.02" treatment the reference wallets lead with — and by default +// shortens a long tail, because eight decimals of noise sit exactly where +// the eye lands. +// +// The shortening NEVER rounds and never collapses a non-zero balance to +// zeros, which is the way this idea normally goes wrong: a flat "2 decimal +// places" rule renders 0.00042 BCH as "0.00" and the wallet reads as empty. +// Instead it keeps two decimals minimum and then extends past any leading +// zeros so roughly four significant digits always survive, capped at what +// the amount actually has: +// +// 1204.23234145 -> 1,204.23 0.23234145 -> 0.2323 +// 0.00012345 -> 0.0001234 0.00000001 -> 0.00000001 (1 sat, intact) +// +// It truncates rather than rounds, so the figure shown is never more than +// the wallet holds and sending the displayed amount always clears. +// An ellipsis marks the cut, hover shows the exact figure, and clicking +// pins full precision (remembered). Only this hero is abbreviated: Send, +// MAX, history and the wallet strip keep calling fmtBig directly, so every +// number you act on is exact. +const BAL_MIN_DP = 2, BAL_SIG_DP = 4; +let balFullText = "—"; +let balFullPrecision = false; +try { balFullPrecision = localStorage.getItem("aegis/fullPrecision") === "1"; } catch (_e) {} + function setBalMain(text) { + balFullText = String(text == null ? "—" : text); + paintBalMain(); +} + +function paintBalMain() { const el = $("balMain"); if (!el) return; - const s = String(text == null ? "—" : text); - const m = /^(-?)(d+)(.d+)?$/.exec(s); - if (!m) { el.textContent = s; return; } - const grouped = m[2].replace(/B(?=(d{3})+(?!d))/g, ","); - el.innerHTML = m[1] + grouped + (m[3] ? '' + m[3] + "" : ""); + const m = /^(-?)(\d+)(\.\d+)?$/.exec(balFullText); + if (!m) { + el.textContent = balFullText; + el.removeAttribute("title"); + el.classList.remove("balx"); + return; + } + const group = (d) => d.replace(/\B(?=(\d{3})+(?!\d))/g, ","); + const head = m[1] + group(m[2]); + const fullFrac = m[3] ? m[3].slice(1) : ""; + let frac = fullFrac, cut = false; + if (!balFullPrecision && fullFrac) { + // Whole units already carry the significant digits, so anything with a + // non-zero integer part only needs the two decimals. It is the amounts + // under 1 that have to dig past their leading zeros to keep any meaning. + const whole = m[2] !== "0"; + const zeros = (/^0*/.exec(fullFrac) || [""])[0].length; + const want = whole ? BAL_MIN_DP : zeros + BAL_SIG_DP; + const keep = Math.max(BAL_MIN_DP, Math.min(fullFrac.length, want)); + if (keep < fullFrac.length) { frac = fullFrac.slice(0, keep); cut = true; } + } + // Every interpolated piece is digits, a sign, a dot or a comma the regex + // vouched for, so innerHTML here cannot inject markup. + el.innerHTML = head + (frac + ? '.' + frac + (cut ? '' + String.fromCharCode(0x2026) + "" : "") + "" + : ""); + const exact = head + (fullFrac ? "." + fullFrac : ""); + const toggleable = cut || balFullPrecision; + el.title = cut ? exact + " — click for every decimal" + : (balFullPrecision ? exact + " — click to shorten" : exact); + el.classList.toggle("balx", toggleable); } +$("balMain") && $("balMain").addEventListener("click", () => { + balFullPrecision = !balFullPrecision; + try { localStorage.setItem("aegis/fullPrecision", balFullPrecision ? "1" : "0"); } catch (_e) {} + paintBalMain(); +}); + function fmtSmall(units) { if (typeof units === "string" && /^-?\d+$/.test(units)) return units.replace(/\B(?=(\d{3})+(?!\d))/g, ","); return Number(units || 0).toLocaleString("en-US");