aboutsummaryrefslogtreecommitdiffstats
path: root/src/app.js
Commit message (Collapse)AuthorAgeFilesLines
* Each account in the switcher wears its own face, and its own renewal warning.Adam Soltys2026-09-101-6/+21
| | | | | | | | | | | | | | | | | | | Two accounts could show the same name and avatar: the identity list read a key stamped onto the account record, and the stamp was taken during the gap in a wallet switch where the new account is already loaded and saved but the features haven't re-inited — so the PREVIOUS account's signer was still answering "who are you". The live signer now re-checks on every read that it still belongs to the open wallet, not just at init(); and the switcher reads each account's own persisted login (or its seed's nostr key) instead of the stamped mirror, so a record stamped wrong before this still lists right. The Spending renewal warning belonged to the wallet that computed it and stayed on screen after switching — the old wallet's amount and deadline over the new wallet's balance, with a 30-minute throttle keeping it there. It now resets with the ark manager, and the next connect re-decides. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_019mrt228Upn9nNCwGixzSLx
* Identity switching moves to the profile page, with faces.Adam Soltys2026-09-101-8/+45
| | | | | | | | | | | | | | | | The Accounts screen is about a seed's Spending/Savings faces — its rows say nothing about WHO each pair belongs to, so a "Sign into another account" button there was the wrong door. It's gone from there. On your own profile, "Sign into another account" now opens a switcher: every identity already signed in on this device as an avatar + name row (one per seed, wearing the nostr key its header avatar shows — the login key when there is one, else the seed's own; remembered on the account and in the durable directory by xpub, so vault-restored accounts have a face too), tap to switch with nothing logged out, plus "Sign into a new account" for the additive sign-in doors. With no other identity on the device the button goes straight to those doors. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01URmfo8VimCNkw6Y7JndYrv
* Profile page gets "Sign into another account" beside Log out.Adam Soltys2026-09-101-3/+12
| | | | | | | | | | | | The additive sign-in door lived only at the foot of the Accounts screen; the profile page — where people actually go looking for a way out of the current identity — offered just Log out. Now it carries the same button, via a shared signInAnother action on the feature ctx (clears the profile/chat state so the unlock screen's Back lands on the wallet, and the new identity doesn't open onto the old identity's profile). Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01URmfo8VimCNkw6Y7JndYrv
* Onboarding: fix the crash after a name claim/migration; keep a login-owned ↵Adam Soltys2026-09-101-2/+8
| | | | | | | | | | | | | | | | | | | name through signer resume. "Something broke" right after a coinos.io migration completed: the spend step advances past the avatar ask by calling nextAfterAvatar(), which was a const declared further down the same function — a temporal-dead-zone throw for any profile that already had a picture. Now a hoisted function. Also, the claimed-name ownership check only counted the LIVE login signer. A bunker/Google login resumes seconds after load, so for that window a name owned by the login identity (every migrated coinos.io name) looked foreign, was dropped, and the wallet key's npub-shaped default took its place — the address visibly flipped from [email protected] to npub…@coinos.io. The stored login pubkey now counts as ours too. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01Nw1Dijik1tLVKRM1WkU7xE
* Ark state moves to IndexedDB: raw bytes on disk, memory-fronted, write-behind.Adam Soltys2026-09-101-0/+1
| | | | | | | | | | | | | | | | | | | | | | | | | localStorage held a deep wallet's ark state as ~700KB of JSON (UTF-16 on disk), parsed on every boot and stringified on every save, and the wallet cache blob carried a second ~600KB copy. src/ark/store.js keeps the state in IndexedDB (db coinos-ark) with each coin's signed bytes as a Uint8Array, fronted by an in-memory map so loadArkState/saveArkState stay synchronous: reads return clones, writes coalesce (200ms) and flush immediately when the page hides. Existing localStorage copies migrate on first open (newest _rev wins) and are removed only after the IndexedDB write completed; no IndexedDB means localStorage exactly as before. Records keep their btc-wallet-cache:<id>:ark… keys, and a wallet wipe clears both stores via the new per-feature wipeCache hook. The ark cache extension is now syncOnly: it still rides the sync snapshot (and the Blossom blob) but the local wallet cache no longer embeds it — measured on a live wallet the cache blob went from 655KB to 10KB. Verified in the live tab: all wallets migrated, bytes raw on disk, balance and coin set unchanged, localStorage ark keys gone. tools/ark-store-test.js covers migration by _rev, clone-on-read, write-behind, wipe and fallback. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01Nw1Dijik1tLVKRM1WkU7xE
* Add "Sign into another account" — switch identities without logging out.Adam Soltys2026-09-081-0/+9
| | | | | | | | | The only way to move between identities (e.g. Adam via a Nostr remote signer and passkeyguy via passkey) was logout → forget all data → sign in again, which wipes every account. But the machinery to hold several accounts at once already exists: enterWallet/addOrGetAccount ADD an account (they don't replace), the unlock screen carries the sign-in doors (Nostr/passkey/Google via unlockExtra) plus a fromWallet Back button, activateAccount cleanly tears down and re-inits per-account nostr-login state, and the Accounts screen already switches between saved accounts. The nostr-login handoff even saves its session AFTER activating the new account, so each identity's signer state lands under its own namespace and resumes silently (persisted passkey sk / bunker session). The missing piece was a non-destructive entry point. Added a "Sign into another account" button on the Accounts screen that opens the unlock screen in additive mode (ui.fromWallet), so signing in adds the identity alongside the current one — then switching is just tapping a row on the Accounts screen (reached via the balance card's Accounts button), no logout, no data loss. Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
* Durable sync outbox: fix the fresh-device phantom "Sent" that showed a zero ↵Adam Soltys2026-09-061-1/+1
| | | | | | | | | | | | | | | | | | balance (authored by codex). Root cause of the "signed in and my balance is 0 with a huge Sent I never made": a device's snapshot carrying its change/new coins never reached the relay. The publish was a setTimeout debounce that got canceled on page-hide / logout / feature stop, so coins created by another session were never synced; a fresh device then loaded only the old (now-spent) coins, and reconcile dressed the spent inputs as a definitive "Sent" row and zeroed the balance. No funds ever moved — verified on the ASP that no such payment existed and the change coin was still spendable. Fix (codex): - src/sync-outbox.js (new): a durable outbox. Snapshots are signed synchronously at save time under the current identity (no cross-account mixing) and persisted already-encrypted to localStorage (no keys/plaintext in storage). Delivery retries with backoff and wakes on online/pageshow/visibilitychange/storage, surviving suspension, logout and relay outages. The last copy doubles as a local recovery source if an account cache is cleared. - sync.js: replace the cancel-on-stop debounced publish with the outbox; fetchAllStates also merges the outbox's own (possibly-unpublished) events so a rejected publish is still recoverable locally; forgetAll clears it. - nostr.js: factor out stateEvent() (synchronous signing) and decodeStateEvents(), shared by fetch and the outbox. - ark.js: adoptArkSnapshot() applies a synced snapshot to a LIVE connected manager without restarting payment ceremonies (a snapshot arriving while connected previously stayed invisible until reload); the pre-handshake hold-key merges instead of overwrites; and the reconcile row is reworded from a red "Sent -N" to a neutral "History incomplete / Payment details not synced" (↻) with an explainer, since a spent input proves neither an amount nor a date. - app.js: clearAll also clears the outbox (forgetAll). Tests (codex): sync-outbox-test, sync-browser-test (real Chrome: relay rejects a snapshot, browser closes, reopens and retries; a second browser recovers the balance + history without reload), sync-split-test, plus build and morph smoke — all green. Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
* LNURL/merchant payments read as PAYMENTS, not zaps, and skip redundant ↵Adam Soltys2026-09-061-0/+15
| | | | | | | steps. (1) Terminology: a payment is a "zap" only when it targets a nostr person or note (npub / note-zap with an eventId, captured as z.social at begin); paying a lightning address, LNURL or merchant now says "Pay over Lightning" / "Pay" everywhere — amount screen, review title, success screen — even when the endpoint supports zap receipts. (2) A fixed-amount payment (min==max — a POS tag, paywall, bolt card) has nothing to type, so the amount screen is skipped and the flow goes straight to the quote/review; the two perceived confirmations collapse to one. (3) Per-recipient auto-pay budget: the review offers "Auto-pay <merchant> up to N next time" (ctx.lnBudget/setLnBudget in prefs, synced), and a fixed charge within a set budget pays as soon as the quote lands — no tap. Guarded: only fixed amounts, only within the budget, only when funds cover it; over budget / no funds / quote error all fall back to the normal review, and social zaps are untouched. Verified with a driven-feature harness (fake ctx + local LNURL server): fixed skips to startLnPay with social=false/fixedSat set, a covering budget sets autopay, variable still shows the amount screen titled/buttoned "Pay" Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
* Spending wears just the ⚡ now, not ⚡ + the ark mark — the ark symbol ↵Adam Soltys2026-09-051-2/+1
| | | | | | | was noise beside lightning for a rail users think of as "instant lightning-like." Savings keeps its bitcoin disc. Dropped the now-unused ARK_MARK import Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
* morph: leave <details open> to the user; key the names settings sectionsAdam Soltys2026-09-051-1/+13
| | | | | | | | | | | | Every background repaint stripped the browser-set `open` attribute (the fresh render never carries it), collapsing expanded sections, and the positional child match handed one section's open state to its neighbour whenever the conditional zap-code section came or went. `open` on a details is now user-owned unless a render sets it, and nodes carrying data-key only patch into the same key. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01VMi2Y1oKPZ3TeeFmLej47y
* Account cards carry identity at a glance (user feedback): the Spending card ↵Adam Soltys2026-09-051-5/+16
| | | | | | | wears ⚡ + the ark mark in violet beside its label with a violet hairline across the card's crown; Savings wears the bitcoin disc in its orange with an orange crown. The tint is an inset shadow stacked with the card's own — clips to the radius, zero layout shift, so the carousel's equal-height cards stay equal. Kind-locked and single-balance wallets keep their plain label. (Same feedback proposed vertical-only navigation and one global send/receive button — parked, not obviously better than the carousel + per-account model.) Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
* Handle lightning:lnurl1… links properly. Two faults: (1) an intent often ↵Adam Soltys2026-09-051-0/+17
| | | | | | | arrives BEFORE the paying feature is ready — ark connects lazily, so zaps' matchSendText declined and the raw URI fell through into the recipient field as dead text; the intent dispatcher now keeps re-offering the text to the feature chain for up to 20s and sweeps the leftover raw text out of the recipient when a feature claims it (the zap amount screen takes over). (2) When the fallback button did render, it read "⚡ Zap lightning:lnurl1dp68gurn8ghj7…<overflow>" — zapDisplay now names targets like a human: the address when there is one, a shortened bech32 otherwise, on both the send-form button and the amount screen Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
* Scrub silent payments from everything user-facing — the receive feature is ↵Adam Soltys2026-09-051-1/+1
| | | | | | | gone (no sp in the feature registry) but its mentions lingered: the zapstore description now says plain "full BIP84 bitcoin wallet" (fresh kind 32267 republished, verified clean on the relay), the README loses the sp feature section and its mentions in the E2E-suite and dev-proxy lists, the dead receiveSpTab i18n key is removed, and two stale comments (app.js seams, build.js HAL_FEATURES list) stop naming it. Deliberately KEPT: silentPaymentNote — the send review still labels an output paying someone else's sp1 address, which remains supported Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
* Android wrapper app (android/): a minimal TWA around v3.coinos.io whose ↵Adam Soltys2026-09-041-0/+32
| | | | | | | whole reason to exist is intent registration — lightning:/bitcoin:/nostr:/lnurl:/lnurlw:/lnurlp: VIEW intents and the same schemes via NFC NDEF dispatch (bolt cards, address stickers; NFC declared required=false), plus verified https links to the site. LauncherActivity forwards a scheme URI into the web app as ?u=<encoded>; the web app reads it BEFORE features construct — a nostr npub/nprofile rewrites itself onto the existing /npub profile deep link, payment URIs stash and dispatch through handleScanned (the QR path) once a wallet is open, and the param is stripped from the URL either way. assetlinks.json (static/.well-known/, copied by build.js) vouches for the signing cert so the TWA runs URL-bar-less; the built APK is served at /coinos.apk for sideloading. Signing keystore lives in android/ GITIGNORED (keystore.properties beside it) — the assetlinks fingerprint pins it, so losing it means re-shipping assetlinks too. Verified: headless ?u= routing (lightning → Send with invoice filled + URL cleaned; nostr → profile page), APK installed on a real phone via adb, schemes registered (chooser appears where other wallets also claim lightning:) Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
* The pager's Prev/Next buttons loop within the pages instead of disabling at ↵Adam Soltys2026-09-041-2/+5
| | | | | | | the ends: Next on the last page returns to page 1, Prev on page 1 jumps to the end. Unlike the swipe ring they never leave the list for the tabs — buttons paginate, swipes navigate. Applies to the shared pager, so the address list wraps the same way Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
* Flip the swipe ring's tab order per feedback: … page0 ⇄ page1 … ↵Adam Soltys2026-09-041-8/+8
| | | | | | | lastPage ⇄ Receive ⇄ Send ⇄ page0 … Left from the last history page now wraps into Receive then Send (matching the tab bar's visual order and the pre-ring muscle memory of left-from-history landing on Receive); right from page 1 gives Send, then Receive, then the last page. Verified headless: right×3 walks history → Send → Receive → history(last), left×3 walks it back Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
* The wallet swipe surfaces now form ONE RING with no dead ends: … page0 ⇄ ↵Adam Soltys2026-09-041-25/+33
| | | | | | | page1 … lastPage ⇄ Send ⇄ Receive ⇄ page0 … Swiping left over history turns pages older and wraps into Send, then Receive, then back to page 0; swiping right from page 1 goes the other way — Receive, then Send, then the LAST page (entering history from a wrap lands on the chosen page; an oversized index clamps to the last). A detail view / spinner / empty list publishes no pager and is treated as a single page, so the swipe still reaches the tabs from anywhere. Verified headless with synthetic touches: right×3 walks history → Receive → Send → history(last), left×3 walks it back Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
* Your own profile page opens as a PROFILE now, not a form: npub (tap to copy ↵Adam Soltys2026-09-041-1/+1
| | | | | | | — it was hidden on your own page), bio, and latest posts, exactly like anyone else's, with the editor behind an explicit Edit profile button (Save or the new Cancel returns to the view) and a New post button that unfolds a composer — publishPost signs a plain kind-1 with the login/wallet identity, publishes to the identity's own relays, and prepends it to the notes list so it shows immediately. The header avatar's tooltip says "Your profile" instead of "Edit profile" to match. Compose/edit state clears on open/back so drafts never leak across profiles. Verified headless end-to-end: onboard → avatar → view mode (buttons + npub, no form) → Edit opens form → Cancel → New post opens composer Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
* Public read-only coinos community chat at /chat — a share-anywhere link ↵Adam Soltys2026-09-041-0/+14
| | | | | | | that shows the live chat history to anyone, logged in or not, with a "Sign in to chat" button ("Open coinos" when the device already has an account). No server-side decoder needed: the community's join material was already public by construction (baked into the client bundle — every coinos user is meant to be a member), so the page derives the stream keys and decrypts wraps client-side exactly like the app's Chat tab, worker-offloaded with the same sliced fallback. Renders messages + edits + deletes + reactions and honors the folded control state's ban list; presence/typing/guestbook deliberately omitted. Profiles come from ONE batched kind-0 query per burst (a REQ per author tripped relay concurrency limits). Plumbing: the join material moved to src/community.js (shared with messages.js so they can never drift), /chat is excluded from the single-segment profile deep-link (which was eating the path and rewriting the URL before the route ran), and app renders stand down in public-chat mode so a feature-init render can't paint onboarding over the page. Verified headless against the live relays: 59 messages decrypt, ban-fold applies, names resolve Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
* Swiping over the history list now turns its pages (left = older, matching ↵Adam Soltys2026-09-041-2/+18
| | | | | | | the pager's Next; right = newer) instead of bouncing straight into the Receive tab — the same swipe falls through to tab navigation only once the pages run out in that direction. historyTab publishes what's on screen ({page, pages} in _histPager, cleared for detail views/spinner/empty) and the wallet-screen swipe gesture consults it when the touch began inside the list card (.history-card) Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
* The new account's history now loads the moment a carousel flick is released, ↵Adam Soltys2026-09-041-26/+80
| | | | | | | not after the slide lands. The old model held ALL renders until the glide settled (a mid-glide morph used to strip the drag's inline snap-none and yank the card — so nothing could paint until rest, and the history switch waited out the whole animation). New model: the strip alone is frozen against the morph (_skipMorph short-circuits that subtree) while renders run freely, so the release can commit the account switch immediately — desktop knows the chosen card exactly (it computes the target itself), touch predicts it from the release position + scroll velocity (nearest card, pushed one over on a real flick — the same physics mandatory snap applies). settle stays the authority: it unfreezes the strip, runs a syncing render, and corrects a rare wrong touch guess when the snap actually lands. Renders stay held only while the finger is DOWN (pointermove writes scrollLeft on the main thread — a render there eats frames) Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
* The balance carousel no longer changes height mid-swipe: the Top up from ↵Adam Soltys2026-09-041-6/+17
| | | | | | | Savings button (and the unfold panel) used to render only on the SELECTED card, and the strip's flex row stretches both cards to the tallest — so the whole strip resized right as a swipe settled onto Spending. The extras now belong to the spending card itself, selected or not, so card heights never move during a drag; tapping the button on the peeking card selects Spending and opens the move panel in one go Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
* Remove the temporary carousel diagnostics (window.__dl / __scroll ring ↵Adam Soltys2026-09-041-41/+1
| | | | | | | buffers, frame-drop + scroll-position tracing) now that the stutter is fixed — the render-gate-during-drag fix stands on its own Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
* THE carousel stutter, root-caused via a per-frame scroll trace: a real flick ↵Adam Soltys2026-09-041-3/+25
| | | | | | | showed scrollLeft climbing (19→40→64) then JUMPING to 0 and stalling ~80ms before gliding — a position jump at a steady 17ms/frame, not a dropped frame. Cause: a background render (ark update, balance tick) firing mid-drag; the morph rebuilds the strip and strips the drag's inline scroll-snap-type:none, so mandatory snap yanks the card to its origin. Fix: renders are held for the sub-second of a carousel drag and one flushes when motion stops, so the DOM never churns under the finger. Verified: a mid-drag render now leaves scrollLeft untouched Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
* TEMP: scroll trace records only while moving (+700ms tail) so flick motion ↵Adam Soltys2026-09-041-2/+9
| | | | | | | survives in the buffer regardless of read timing Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
* TEMP: trace scrollLeft per frame into window.__scroll to catch a position ↵Adam Soltys2026-09-041-0/+10
| | | | | | | jump during a flick (a stutter that isn't a dropped frame) Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
* TEMP: add dropped-frame logging to carousel diagnostics — logs any >40ms ↵Adam Soltys2026-09-041-1/+12
| | | | | | | frame gap into __dl so a real drag's jank appears next to the release/settle it overlaps Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
* TEMP: carousel drag diagnostics (window.__dl ring buffer) logging each ↵Adam Soltys2026-09-031-1/+13
| | | | | | | release decision (vel/travelled/dir/from/best) and settle outcome, to capture what a real failing flick does. To be removed once diagnosed Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
* Critical fix: the commit-threshold change I just shipped threw on every drag ↵Adam Soltys2026-09-031-2/+2
| | | | | | | release. My new travel-fraction variable was also named 'moved', colliding with the drag-started flag of the same name in the enclosing scope — a temporal-dead-zone ReferenceError that threw up() before it could commit or even snap back, so a release did nothing. Renamed to 'travelled'. (This is what made 4f0e444's drags dead.) Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
* The carousel 'stutter' was a snap-back, not a stall: frame-by-frame of the ↵Adam Soltys2026-09-031-10/+16
| | | | | | | latest video shows the card sliding a third of the way toward the neighbor then springing back to where it started — a short flick that never crossed the halfway settle point. Release now commits to the neighbor on a deliberate small move (past ~15% of a card) or any quick flick, instead of only past the midpoint; otherwise it snaps back. Direction comes from flick velocity, or from how far the grabbed card actually travelled Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
* Fix the switch that scrollend broke, and land the actual stutter fix. ↵Adam Soltys2026-09-031-21/+15
| | | | | | | scrollend doesn't fire reliably for programmatic scrolls, so relying on it left the card visually switched but the account state stale. Back to the onScroll debounce — but the real mid-glide stall was never the debounce (it trails the glide's own scroll events and fires after motion stops); it was an EXPLICIT 150ms settle timer in the drag release firing mid-glide. That's gone, so the switch render lands only once the card is at rest. scrollend kept as a bonus early trigger where it does fire Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
* The carousel's stutter was the switch render landing mid-glide: a one-frame ↵Adam Soltys2026-09-031-6/+16
| | | | | | | stall showed up ~150ms into a slide — exactly when the settle timer fired the account switch and rebuilt the history list while the card was still moving. The switch now fires on scrollend (motion truly at rest, card already in place), so its render is invisible; the debounce timers survive only as long fallbacks for browsers without scrollend Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
* The real 'false start': the auto-select-Spending one-shot yanking the card ↵Adam Soltys2026-09-031-0/+4
| | | | | | | back. On open, when Savings is empty and Ark has a balance, the wallet flips you to Spending — a convenience. But it fires on every Ark update and only latches once the balance SYNCS, which after a refresh arrives a second late — right after you've dragged to Savings, so it overrides your choice and slides back. Now any deliberate pick (carousel scroll/tap/flick, swipe, accounts screen) sets accountUserChosen, and the auto-select consumes its one-shot without flipping when you've already chosen Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
* Two carousel polish fixes. The 'slight move after history loads' was the ↵Adam Soltys2026-09-031-1/+3
| | | | | | | vertical scrollbar: a short page has none, the history list filling in makes the page tall enough to get one, the viewport narrows ~15px, and the snapped card re-centers into the narrower width — a horizontal nudge right as history lands. scrollbar-gutter:stable reserves the gutter so growing content never changes the width. And the 'false start' — a boot render's alignment firing a scrollTo mid-drag — is guarded: alignment now bails while a finger/mouse is down Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
* The carousel stops snapping back mid-throw: on release the mandatory ↵Adam Soltys2026-09-031-1/+16
| | | | | | | snap-type was restored on a fixed 400ms timer, which during rapid back-and-forth fired while the next glide was only partway — re-applying mandatory snap then makes the browser snap to the nearest point, the card you started from, yanking it back. Snap now restores on the scrollend of that specific glide, guarded by a per-drag token so a stale restore from an earlier throw can't fire during a later one (timer kept only as a fallback for the no-scroll case) Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
* The balance carousel stops stuttering during boot: the post-render alignment ↵Adam Soltys2026-09-031-4/+13
| | | | | | | no longer reads layout (offsetLeft/scrollLeft) on every one of the many boot renders — an _alignedTo guard short-circuits before any layout touch once the card is in place, and the scroll's own settle marks itself aligned so it never re-aligns behind the user. The pass moved to rAF, and each slide gets layout containment so the history list building in below can't force it to reflow mid-scroll Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
* History stays home under the panes: Receive and Send now fold open ABOVE the ↵Adam Soltys2026-09-031-6/+16
| | | | | | | transaction history instead of replacing it, so returning to the list is a scroll, not an unintuitive re-tap of the toggle. A tapped history detail (or the receive celebration) still claims the whole area Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
* Reconcile rows earn a real detail page: date, amount, and every consumed ↵Adam Soltys2026-09-031-4/+6
| | | | | | | coin id — the folded row rides ui state (and nav history) since it isn't a stored movement. Ordinary send details also stop hiding their inputs: 'Coins spent' lists what a payment gathered (up to 24), beside the destination vtxo it minted Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
* The black flash on Get started dies at the morph: a reused button whose ↵Adam Soltys2026-09-021-0/+10
| | | | | | | class changes between screens is a different control wearing the same node, and its background transition was crossfading the old style into the new one — repaints aren't style animations, so the swap now snaps and the transition comes back after the paint Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
* The drag holds its tongue until you let go — no account switch while the ↵Adam Soltys2026-09-021-18/+37
| | | | | | | button or finger is still down (release is the choice, shared settle logic for mouse, touch and glide) — and the coins table's dotted meta line becomes little labeled pills: an hourglass countdown, depth, and exit cost each in their own chip, the expired one wearing the warning color Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
* The single-file wallet returns as a first-class artifact: every build now ↵Adam Soltys2026-09-021-3/+15
| | | | | | | emits standalone.html (~950KB, everything inlined, boots from a bare file:// with no server), How it works grows a 'Keep a copy' card linking the download, and the tab strip's phantom double-tap dies — the pointerdown's paired click was arriving after the render had already swapped in fresh handlers, and with toggling tabs that ghost click un-did the tap it belonged to. The timestamp guard now lives on the DOM node, which survives the morph Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
* The mouse drag learns to catch a flick: release velocity over the last ↵Adam Soltys2026-09-021-7/+28
| | | | | | | ~100ms throws the card in the flick's direction, so a quick short pull changes accounts instead of demanding a half-width haul; a slow release still settles nearest Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
* History becomes home: the tab bar slims to Receive | Send (tapping the lit ↵Adam Soltys2026-09-021-27/+87
| | | | | | | one puts it away), every landing — login, account creation, an account snapped into the carousel — is the account's own timeline, and that timeline is now SCOPED: Savings shows the on-chain ledger and gift reservations, Spending shows the ark side. The carousel meanwhile learns mouse and mind-reading: desktop gets a real grab-and-pull drag (snap lifted mid-drag, restored on the glide), the peeking neighbor switches on a single click, and the strip wears grab cursors. And a thread's avatars and author names finally go somewhere: profiles stack on top of the conversation and back returns to it Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
* The carousel becomes what it was meant to be: two separate balance CARDS ↵Adam Soltys2026-09-021-54/+57
| | | | | | | riding a full-bleed strip — the neighbor card peeks in from the screen edge and snaps into place, instead of two faces crowding one card's chrome. The active card carries the move/top-up buttons and unfold panel; the peeking one stays a clean preview Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
* The balance grows a card carousel: Spending and Savings become side-by-side ↵Adam Soltys2026-09-021-37/+82
| | | | | | | faces with the neighbor peeking at the card's edge, dragged and snapped by native scroll-snap — settling on a face IS the account switch, programmatic switches glide the carousel to match, and the old swipe-to-flip gesture yields to the carousel's own drag (it still serves single-face wallets) Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
* Recipient search warms its pipes on focus, not first keystroke: the Primal ↵Adam Soltys2026-09-021-3/+5
| | | | | | | cache socket, the registrar's TLS session, and the NIP-50 relay connections each cost a handshake that used to land on the first query — the cold second of dead air. Touching the send recipient field, opening user search, or unfolding New message now pre-opens all three (throttled; the idle reaper still closes the socket a minute after real use) Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
* Tab feedback at touch speed: the strip's active state flips with a direct ↵Adam Soltys2026-09-021-2/+14
| | | | | | | DOM write that paints in the same frame as the finger, and the heavy pane render runs after that paint (double-rAF) — running it synchronously on pointerdown meant even the button's own light-up waited out a 100ms+ rebuild Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
* Tabs answer the finger, not the click: switching happens on pointerdown (the ↵Adam Soltys2026-09-021-1/+16
| | | | | | | click event lands 100-250ms after the touch, and a tab has no scroll ambiguity to wait out — keyboard activation keeps the click path), every button takes touch-action:manipulation so double-tap-zoom arbitration stops taxing quick taps, and the sliding pane pre-promotes to its own compositor layer — a slow mid-slide re-render was letting Chrome expose unpainted gray tiles at the viewport edges Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
* The phone's Back button closes a payment's details instead of teleporting to ↵Adam Soltys2026-08-301-1/+5
| | | | | | | the last tab: the ark move/exit and gift detail views join NAV_FIELDS (an on-chain txDetail always had an entry; the ark rows — most Spending payments — minted none, so Back skipped the history list entirely), and their in-app Back buttons pop that entry rather than pushing another Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
* A migrated coinos.io account keeps its face and its cover: the claim fetches ↵Adam Soltys2026-08-301-1/+9
| | | | | | | the legacy avatar and banner and fills the kind 0's empty slots (fill, never overwrite — an existing nostr identity keeps its own look), the wizard's punk picker steps aside for a profile that already has a picture, and v3 finally wears cover photos — nostr's standard kind-0 banner renders full-bleed across the top of every profile card with the avatar overlapping it, settable by URL or upload in the profile editor Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr