diff options
| author | Adam Soltys <[email protected]> | 2026-06-24 20:13:25 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-06-24 20:13:25 -0700 |
| commit | 3db3ba8e134363a3f11081f99a23950f1e6af9e2 (patch) | |
| tree | 7d8a1f5df1cd1e9c009f2358d2a0e781f4fd511a /src | |
| parent | 9e2737e6736f9dd582d083c6d6fd669a28a99316 (diff) | |
| download | coinosv3-3db3ba8e134363a3f11081f99a23950f1e6af9e2.tar.gz coinosv3-3db3ba8e134363a3f11081f99a23950f1e6af9e2.zip | |
Fix dark-mode contrast and stop install-link jitter
- warn-box, addr-box, summary, toast and green badge/notice borders used
hardcoded light colors that washed out or vanished in dark mode; route
them through theme variables (new --toast-bg/-fg and --green-line pairs).
- Drive the footer "Install app" link from a persisted localStorage flag
instead of the live beforeinstallprompt event so it's present on first
paint after a refresh, eliminating the layout shift.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Diffstat (limited to 'src')
| -rw-r--r-- | src/app.js | 37 | ||||
| -rw-r--r-- | src/style.css | 18 |
2 files changed, 43 insertions, 12 deletions
@@ -119,8 +119,10 @@ function footer() { { style: 'margin-top:4px' }, h('button', { class: 'linklike', style: 'font-weight:400', onClick: openHowItWorks }, t('howItWorks')), // Chrome no longer prompts to install on its own โ surface our own link - // once it reports the app is installable (beforeinstallprompt fired). - installPrompt + // once it reports the app is installable. We render from a persisted flag + // (not the live event) so the link is present on the first paint after a + // refresh, avoiding a layout shift when beforeinstallprompt fires late. + installable() ? h('span', {}, h('span', { class: 'faint' }, ' ยท '), h('button', { class: 'linklike', style: 'font-weight:400', onClick: triggerInstall }, t('installApp'))) : null, @@ -151,25 +153,48 @@ function toggleTheme() { // PWA install. Chrome fires beforeinstallprompt when the app qualifies; we stash // the event and reveal an "Install app" link, then replay it on a user tap (the -// browser requires a gesture). beforeinstallprompt only fires when not already -// installed, so the link naturally hides once installed. +// browser requires a gesture). The link's visibility is driven by a persisted +// flag rather than the live event so it's present on the first paint after a +// refresh (no layout shift); the event only supplies the prompt to replay. +const INSTALLABLE_KEY = 'btc-wallet-installable'; let installPrompt = null; +function isStandalone() { + try { + return matchMedia('(display-mode: standalone)').matches || navigator.standalone === true; + } catch { return false; } +} +function installable() { + if (isStandalone()) return false; + try { return localStorage.getItem(INSTALLABLE_KEY) === '1'; } catch { return false; } +} +function setInstallable(v) { + try { + if (v) localStorage.setItem(INSTALLABLE_KEY, '1'); + else localStorage.removeItem(INSTALLABLE_KEY); + } catch {} +} if (typeof window !== 'undefined') { window.addEventListener('beforeinstallprompt', (e) => { e.preventDefault(); installPrompt = e; - render(); + // Only re-render if this changes what's on screen; on a refresh the link is + // already shown from the persisted flag, so nothing moves. + const wasShown = installable(); + setInstallable(true); + if (!wasShown) render(); }); window.addEventListener('appinstalled', () => { installPrompt = null; + setInstallable(false); render(); }); } async function triggerInstall() { const e = installPrompt; + // The prompt event may not have fired yet this load even though the link is + // shown from the persisted flag; bail quietly if so. if (!e) return; installPrompt = null; - render(); e.prompt(); try { await e.userChoice; diff --git a/src/style.css b/src/style.css index 6533854..545891c 100644 --- a/src/style.css +++ b/src/style.css @@ -13,10 +13,13 @@ --accent-soft: #fff3e2; --green: #14935a; --green-soft: #e7f6ee; + --green-line: #c4e7d4; --red: #c43b3b; --red-soft: #fbeaea; --shadow: 0 1px 2px rgba(16, 24, 40, 0.04), 0 10px 30px rgba(16, 24, 40, 0.07); --surface2: #f7f8fa; /* subtle raised surface (hover, mnemonic cells) */ + --toast-bg: #15171a; /* dark toast on light page */ + --toast-fg: #ffffff; --tab-bg: #e9ebef; /* tab strip background */ --info: #3949ab; --info-soft: #eef2ff; @@ -40,10 +43,13 @@ --accent-soft: #2e2412; --green: #2dbd80; --green-soft: #14271d; + --green-line: #235340; --red: #e66a6a; --red-soft: #2c1a1a; --shadow: 0 1px 2px rgba(0, 0, 0, 0.4), 0 8px 30px rgba(0, 0, 0, 0.5); --surface2: #20242b; + --toast-bg: #2c323b; /* raised toast that reads against the dark page */ + --toast-fg: #f3f5f8; --tab-bg: #20242b; --info: #9db4ff; --info-soft: #1a2030; @@ -109,7 +115,7 @@ summary { cursor: pointer; } background: var(--green); margin-right: 6px; vertical-align: middle; } .badge.off.dot::before { background: var(--faint); } -.badge.live { color: var(--green); border-color: #c4e7d4; background: var(--green-soft); } +.badge.live { color: var(--green); border-color: var(--green-line); background: var(--green-soft); } .badge.live.dot::before { animation: pulse 1.5s ease-in-out infinite; } @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: .3; } } .badge.testnet { color: var(--accent-ink); border-color: var(--accent); background: var(--accent-soft); } @@ -182,7 +188,7 @@ textarea { resize: vertical; min-height: 84px; font-family: var(--mono); font-si /* ---- address / qr ---- */ .qr { width: 220px; height: 220px; display: block; border-radius: 12px; border: 1px solid var(--line); } .addr-box { - font-family: var(--mono); font-size: 15px; background: #f7f8fa; border: 1px dashed var(--line-strong); + font-family: var(--mono); font-size: 15px; color: var(--ink); background: var(--surface2); border: 1px dashed var(--line-strong); border-radius: 11px; padding: 12px 14px; word-break: break-all; text-align: center; } .path { font-family: var(--mono); font-size: 12px; color: var(--faint); } @@ -199,7 +205,7 @@ textarea { resize: vertical; min-height: 84px; font-family: var(--mono); font-si .words .w .n { color: var(--faint); font-size: 12px; width: 18px; text-align: right; } .words .w .t { font-weight: 600; } .warn-box { - background: var(--accent-soft); border: 1px solid #f6d8a8; color: #7a4d05; + background: var(--accent-soft); border: 1px solid var(--accent); color: var(--accent-ink); border-radius: 11px; padding: 12px 14px; font-size: 14px; } @@ -223,7 +229,7 @@ textarea { resize: vertical; min-height: 84px; font-family: var(--mono); font-si .coin input[type=checkbox] { width: 18px; height: 18px; accent-color: var(--accent); } /* ---- review / summary ---- */ -.summary { background: #f7f8fa; border: 1px solid var(--line); border-radius: 12px; padding: 14px; } +.summary { background: var(--surface2); border: 1px solid var(--line); border-radius: 12px; padding: 14px; } .summary .line { display: flex; justify-content: space-between; gap: 12px; padding: 5px 0; font-size: 14px; align-items: baseline; } .summary .line .k { color: var(--muted); flex: none; } .summary .line .v { font-family: var(--mono); text-align: right; min-width: 0; word-break: break-word; } @@ -231,7 +237,7 @@ textarea { resize: vertical; min-height: 84px; font-family: var(--mono); font-si /* ---- misc ---- */ .notice { border-radius: 11px; padding: 11px 13px; font-size: 14px; } .notice.err { background: var(--red-soft); color: var(--red); border: 1px solid #f1c9c9; } -.notice.ok { background: var(--green-soft); color: var(--green); border: 1px solid #c4e7d4; } +.notice.ok { background: var(--green-soft); color: var(--green); border: 1px solid var(--green-line); } .notice.info { background: var(--info-soft); color: var(--info); border: 1px solid var(--info-line); } .spinner { width: 18px; height: 18px; border: 2px solid var(--line-strong); border-top-color: var(--accent); @@ -248,7 +254,7 @@ textarea { resize: vertical; min-height: 84px; font-family: var(--mono); font-si @keyframes spin { to { transform: rotate(360deg); } } .toast { position: fixed; left: 50%; bottom: 26px; transform: translateX(-50%); - background: var(--ink); color: #fff; padding: 10px 16px; border-radius: 10px; + background: var(--toast-bg); color: var(--toast-fg); padding: 10px 16px; border-radius: 10px; font-size: 14px; box-shadow: var(--shadow); z-index: 50; opacity: 0; pointer-events: none; transition: opacity .2s, transform .2s; } |
