diff options
| author | Adam Soltys <[email protected]> | 2026-08-02 23:26:38 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-08-02 23:26:38 -0700 |
| commit | 02d60eb824cad0b8c09cb5685a6c2dbe02e16c4f (patch) | |
| tree | 9c3637c21783bf55ce8d3d3aa6de4cd47c6abb59 /src | |
| parent | 70b02f00c63ba6f6cae939a5ebdedac6e3552eb6 (diff) | |
| download | coinosv3-02d60eb824cad0b8c09cb5685a6c2dbe02e16c4f.tar.gz coinosv3-02d60eb824cad0b8c09cb5685a6c2dbe02e16c4f.zip | |
Tab carousel, login avatar fix, signer callback hint
Tab changes no longer flash the whole page: only the content pane below
the tab strip slides sideways with a soft cross-fade — swipe direction
when a gesture caused it, index order for taps — while the balance card
and strip stay planted. Full-page entrances remain for real page
changes (settings, chat, profile, takeovers).
After a nostr sign-in the header briefly wore the wallet key's face:
nostrLoginIdentity now reports a just-connected live signer immediately
instead of waiting for the persisted link that only lands after the
wallet opens.
The nostrconnect URI carries a callbackUrl hint (NIP-55 style) so
signer apps that honor it can bounce back to the PWA after approval —
apps that don't will still find the session signed in when the user
returns, since the handshake completes over the relay in the
background.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Wvj4aoeMZwA7VUGdJP9Swi
Diffstat (limited to 'src')
| -rw-r--r-- | src/app.js | 32 | ||||
| -rw-r--r-- | src/features/nostrlogin.js | 4 | ||||
| -rw-r--r-- | src/nostr-login.js | 2 | ||||
| -rw-r--r-- | src/style.css | 11 |
4 files changed, 34 insertions, 15 deletions
@@ -285,8 +285,11 @@ function uiChanged(key, val) { // the animation started by this change are we?" — callers re-apply the class // with a negative animation-delay so a rebuild RESUMES the animation. let _navAt = -1e9; -let _navCls = 'anim-page'; let _swipeDir = null; // set by the swipe handler just before render +// tab switches carousel just the content pane, not the whole page +let _tabAt = -1e9; +let _tabCls = 'anim-tab-left'; +let _prevTabIdx = 0; const _animAt = new Map(); function animWindow(key, val, durMs) { if (uiChanged(key, val)) _animAt.set(key, performance.now()); @@ -348,14 +351,10 @@ function render() { : unlockScreen()); // Navigation animates; background repaints must not. The key is every // ui field that decides which page is on screen. - const navKey = [ui.screen, ui.tab, ui.chatOpen, ui.msgView, ui.msgPeer, ui.msgCommunity, + const navKey = [ui.screen, ui.tab === 'settings', ui.chatOpen, ui.msgView, ui.msgPeer, ui.msgCommunity, ui.profilePk, ui.settingsPage, ui.addrScan, ui.arkExitPage, ui.txDetail, ui.giftMode, ui.claimStep].join('|'); - if (uiChanged('nav', navKey)) { - _navAt = performance.now(); - _navCls = _swipeDir ? 'anim-slide-' + _swipeDir : 'anim-page'; - } - _swipeDir = null; - applyAnim(screen, _navCls, (performance.now() - _navAt) < 340 ? performance.now() - _navAt : -1); + if (uiChanged('nav', navKey)) _navAt = performance.now(); + applyAnim(screen, 'anim-page', (performance.now() - _navAt) < 340 ? performance.now() - _navAt : -1); root.replaceChildren(screen, footer()); if (fpath) { const el = nodeAtPath(fpath); @@ -2017,6 +2016,21 @@ function walletScreen() { h('div', { class: 'mt16' }, tabContent()) ); } + // Tab changes slide the content pane sideways like a carousel — swipe + // direction when a gesture caused it, index order for taps. The balance + // card and tab strip stay planted. + const TAB_ORDER = ['receive', 'send', 'history']; + if (uiChanged('tabnav', ui.tab)) { + const idx = TAB_ORDER.indexOf(ui.tab); + _tabCls = 'anim-tab-' + (_swipeDir || (idx >= 0 && idx < _prevTabIdx ? 'right' : 'left')); + _tabAt = performance.now(); + if (idx >= 0) _prevTabIdx = idx; + } + _swipeDir = null; + const pane = h('div', { class: 'tab-pane' }, tabContent()); + const tabDt = performance.now() - _tabAt; + // skip the pane slide while a full-page entrance is already running + if (performance.now() - _navAt > 400) applyAnim(pane, _tabCls, tabDt < 300 ? tabDt : -1); return h( 'div', { class: 'col', style: 'gap:0' }, @@ -2024,7 +2038,7 @@ function walletScreen() { h('div', { class: 'mt16' }, balanceCard()), ui.offlineFallback && wallet.offline ? offlineBanner() : null, tabsBar(), - tabContent() + pane ); } diff --git a/src/features/nostrlogin.js b/src/features/nostrlogin.js index 6b55bbf..35db691 100644 --- a/src/features/nostrlogin.js +++ b/src/features/nostrlogin.js @@ -241,6 +241,10 @@ export function nostrLoginFeature(ctx) { // The nostr identity this session is logged in as, for features that // should speak as the user (the payment address defaults to this npub). nostrLoginIdentity() { + // A just-connected signer counts immediately — the persisted link lands + // only after the wallet opens, and the header avatar shouldn't show the + // wallet key's face in the gap. + if (live) return { pubkey: live.pubkey, npub: npubOf(live.pubkey), signer: live }; const st = load(); if (!st.pubkey) return null; return { pubkey: st.pubkey, npub: npubOf(st.pubkey), signer: live }; diff --git a/src/nostr-login.js b/src/nostr-login.js index b142dd7..8a4603f 100644 --- a/src/nostr-login.js +++ b/src/nostr-login.js @@ -132,7 +132,7 @@ export function nostrConnect({ relays = ['wss://relay.coinos.io', 'wss://nos.lol name: 'coinos', url: 'https://v3.coinos.io', perms: ['get_public_key', 'sign_event', 'nip44_encrypt', 'nip44_decrypt'], - }); + }) + '&callbackUrl=' + encodeURIComponent('https://v3.coinos.io/'); const controller = new AbortController(); const timer = setTimeout(() => controller.abort(new Error('signer connect timed out')), timeoutMs); const ready = (async () => { diff --git a/src/style.css b/src/style.css index 1325118..9b6558e 100644 --- a/src/style.css +++ b/src/style.css @@ -467,10 +467,11 @@ body::after { } @media (prefers-reduced-motion: reduce) { - .anim-page, .anim-menu, .anim-unfold, .anim-slide-left, .anim-slide-right, .amt-flash, .check-badge, .balance::before { animation: none !important; } + .anim-page, .anim-menu, .anim-unfold, .anim-tab-left, .anim-tab-right, .amt-flash, .check-badge, .balance::before { animation: none !important; } *, *::before, *::after { transition-duration: .01ms !important; } } -@keyframes slide-in-left { from { opacity: 0; transform: translateX(30px); } } -@keyframes slide-in-right { from { opacity: 0; transform: translateX(-30px); } } -.anim-slide-left { animation: slide-in-left .28s cubic-bezier(.16, .84, .3, 1) both; } -.anim-slide-right { animation: slide-in-right .28s cubic-bezier(.16, .84, .3, 1) both; } +/* tab carousel: only the content pane moves, with a soft cross-fade */ +@keyframes tab-in-left { from { opacity: .2; transform: translateX(26px); } } +@keyframes tab-in-right { from { opacity: .2; transform: translateX(-26px); } } +.anim-tab-left { animation: tab-in-left .26s cubic-bezier(.25, .8, .35, 1) both; } +.anim-tab-right { animation: tab-in-right .26s cubic-bezier(.25, .8, .35, 1) both; } |
