Skip to content

Revamp: design tokens, loading/error UX, and ~6× faster wallet connect#837

Open
jamespepper81 wants to merge 12 commits into
devfrom
claude/bitsleuth-analyzer-revamp-jk96vm
Open

Revamp: design tokens, loading/error UX, and ~6× faster wallet connect#837
jamespepper81 wants to merge 12 commits into
devfrom
claude/bitsleuth-analyzer-revamp-jk96vm

Conversation

@jamespepper81

Copy link
Copy Markdown
Contributor

Summary

A full visual + performance revamp of BitSleuth across three workstreams. 12 commits, 64 files changed.

1. Visual foundation

  • Real font loading — Inter was declared in CSS but never actually loaded (the whole app silently rendered in system-ui); now loaded via next/font/google with the previously-dead font-headline utility defined.
  • Semantic design tokens — added success/warning/info tokens and swept ~330 hardcoded Tailwind palette classes across 30 files onto the token system, so gain/loss, statuses, and charts express one meaning with one color in both themes.
  • Unified chart palette — dark mode previously used entirely different hues than light; both now share one Bitcoin-anchored ramp.
  • Distinct accent--accent was identical to the primary orange, making every menu hover flash full brand color; it's now a subtle warm surface.
  • Loading & error UX — route-level loading.tsx/error.tsx, a branded full-page loader (replaces bare spinners on ~14 pages), no more theme-toggle flash.
  • Consistency & a11y — plain section titles under one BitSleuth brand, keyboard-operable wallet switcher, screen-reader dialog titles, icons in place of emoji.

2. Performance

  • Lazy-loaded crypto stack — the bitcoinjs/bip32/secp256k1 bundle no longer ships on every route (largest eager chunk 820 KB → 448 KB). Address derivation stays client-side; the xpub never leaves the browser.
  • Wallet context — memoized then split into scoped data/actions/AI/Nostr slices, ending app-wide re-render storms on every price tick.
  • On-demand exports — jspdf/jszip load on export click instead of with the report page.
  • Removed a dead update-poller that fetched full page HTML every 15 min looking for a script tag App Router never emits.
  • Virtualized transactions list (desktop table + mobile cards) so the DOM stays small for large wallets.
  • Idle-deferred, deduped wallet-cache writes and a shared Nostr relay pool (was opening 4 sockets per operation).
  • Build-time type enforcement — removed ignoreBuildErrors: true.

3. Connect speed (the headline fix)
Every blockchain lookup was an individual Next.js Server Action, and Server Actions run serially per client — so the discovery code's "parallel" batches were actually one round trip at a time (~70–200 sequential trips per connect). Discovery now uses batched Server Actions that fan out to the provider on the server. Measured at 100 ms emulated client RTT with cold caches: click → full dashboard data 17.0 s → 2.8 s (discovery phase 11.9 s → 1.3 s); sub-second on localhost.

Two intentional decisions worth a reviewer's eye: per-page sub-brand titles (BitTracker/BitInsight/…) were unified to plain names (Dashboard/Analysis/…), and tailwind.config.ts was deleted as a dead duplicate of the CSS @theme under Tailwind v4.

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation update
  • Refactor / cleanup

(Bug fixes bundled in: dead update-poller, toast destructive-close contrast, connect-dialog a11y title, nested SelectContent in the currency switcher, and stale/flaky E2E assertions.)

Test Plan

  • npm run typecheck, npm run lint, npm run test (65 unit tests), and the type-enforced npm run build all pass.
  • E2E login-flow.spec.ts passes 3/3 across three consecutive runs.
  • Browser-verified against a live wallet: connect flow with streaming discovery → correct balance/transactions; wallet switch; currency switch (balance re-renders across pages); AI chat send; transactions scroll on desktop + mobile (390×844); account-switcher / add-account / Nostr-connect / edit-profile dialogs; light + dark screenshots of dashboard, market, mempool, landing.
  • Connect timing measured in a fresh browser context with a cold Next fetch cache and 100 ms CDP-emulated network latency (production-like), before vs after the batching change.

Checklist

  • npm run typecheck passes
  • npm run lint passes
  • npm run test passes
  • Documentation updated (if applicable)

🤖 Generated with Claude Code


Generated by Claude Code

claude added 12 commits July 4, 2026 21:44
…ns, unified charts

- Load Inter via next/font/google (was declared in CSS but never loaded,
  silently falling back to system-ui); define font-headline so the
  existing font-headline classes stop being a no-op
- Add semantic success/warning/info tokens (light + dark) to globals.css
  and register them in the Tailwind v4 @theme block
- Give --accent its own subtle warm surface instead of duplicating the
  primary Bitcoin orange, so menu/dropdown hovers stop flashing brand color
- Unify the dark-mode chart palette with light mode (same Bitcoin-anchored
  hues, lightness-tuned) — charts no longer change identity across themes
- Replace the fragile hardcoded-hex [role=alert] CSS overrides with a
  token-based Alert warning variant
- Delete tailwind.config.ts: dead v3-style duplicate of the CSS @theme
  under Tailwind v4 (no @config directive references it)
- disableTransitionOnChange on ThemeProvider (kills theme-toggle flash);
  styled branded Suspense fallback instead of unstyled "Loading..."

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016A3N8t6WuMeHgiKQLWFN7T
- Load @/lib/blockchain (bitcoinjs-lib + bip32 + ecpair + secp256k1) via
  dynamic import inside getWalletData instead of statically from the
  app-wide wallet context. The crypto stack leaves every page's initial
  bundle (largest eager chunk: 820KB -> 448KB) and loads only when wallet
  data is actually fetched. Derivation stays client-side — xpubs never
  leave the browser.
- Memoize the WalletContext provider value (was a fresh ~40-field object
  every render), so the 5-minute price poll and streaming discovery
  progress no longer re-render every useWallet() consumer app-wide
- Load jspdf/jspdf-autotable and jszip on demand inside the report export
  click handlers instead of bundling them with the report page
- Add recharts to experimental.optimizePackageImports
- Remove the dead update poller in the app layout: it fetched full page
  HTML with no-store every 15 minutes and regexed for __NEXT_DATA__,
  which the App Router never emits — it could never detect an update

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016A3N8t6WuMeHgiKQLWFN7T
…okens

Sweep ~330 hardcoded Tailwind palette classes (emerald/rose/blue/amber/
purple/orange, raw hex, rgba) across 30 files to the semantic token
system so both themes stay coherent from a single source of truth:

- Financial deltas -> chart-positive/chart-negative; statuses ->
  success/warning/destructive; informational blues -> info;
  decorative purples -> chart-purple; brand oranges -> primary
- IconContainer variants now token-based (previously six hardcoded
  palettes with manual dark variants)
- AIChart: hex slice colors -> chart tokens (slices 6-9 previously
  ignored the theme entirely); white pie labels and rgba gradient
  washes -> tokens
- Orbital loader: rgba(247,147,26) glows -> hsl(var(--brand)); shimmer
  via-white/10 -> via-foreground/10 (was invisible in light mode)
- Toast destructive close button and ErrorBoundary fallback moved to
  tokens (fixes red-on-red close affordance)
- Button: outline/ghost hovers use the new subtle accent surface
  instead of a heavy primary orange wash; solid variants get shadow-sm
- Card: default transition-shadow so existing hover elevations animate
- Dashboard empty-state tip: emoji -> Lightbulb icon, readable
  foreground text on info tint

Canvas fillStyle colors in the Discover force-graph are intentionally
left as literals (CSS vars don't resolve in canvas).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016A3N8t6WuMeHgiKQLWFN7T
- Add (app)/loading.tsx: skeleton page shell (header, metric grid, list)
  shown during navigation across all 13 app routes
- Add (app)/error.tsx and root error.tsx: styled recovery cards with a
  working reset button instead of falling through to the raw boundary
- Restyle FullPageLoader in place — branded Bitcoin pulse/spinner with
  an accessible status label — upgrading the ~14 pages that use it
  without touching consumers; ErrorDisplay becomes a proper card with
  icon
- LoadingProgress: pick the long-wait message once per mount (it
  re-rolled on every render, flickering between strings); replace the
  raw checkmark glyph with a lucide Check icon
- Remove dead SkeletonCard/SkeletonTransactionRow exports from
  orbital-loader (no consumers)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016A3N8t6WuMeHgiKQLWFN7T
…unification

- Add src/lib/format.ts (formatCurrency/formatBTC/formatNumber/
  formatPercent) with cached Intl.NumberFormat instances; replace the
  ten duplicated per-page formatCurrency definitions (two files defined
  it twice) and the report pages' compact/full variants
- Add useWalletDataGuard hook replacing the identical four-line
  loading/error guard block on security, analysis, transactions and
  coin-control
- Add useCopyToClipboard hook (with failure toast) replacing three
  hand-rolled handleCopy implementations
- Add shared EmptyState component for the duplicated address/block/
  transaction "Not Found" views
- A11y: the sidebar account-switcher trigger and wallet rows
  (role="button" divs) are now keyboard-operable with visible focus
  rings (tabIndex + Enter/Space handlers)
- Branding: replace per-page sub-brand titles (BitTracker, BitInsight,
  BitWatch, …) with the section names already used in the sidebar, and
  update the e2e assertion that keyed on "BitTracker"

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016A3N8t6WuMeHgiKQLWFN7T
Both wallet-connect progress modals rendered DialogContent without a
DialogTitle, tripping Radix's accessibility error on every connect.
Add visually-hidden titles.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016A3N8t6WuMeHgiKQLWFN7T
Remove typescript.ignoreBuildErrors — the codebase typechecks clean, so
the flag only allowed regressions to ship silently. next build now runs
full type validation (verified passing).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016A3N8t6WuMeHgiKQLWFN7T
…y pool

- New src/lib/wallet-cache.ts: wallet snapshots are persisted via
  requestIdleCallback (setTimeout fallback), deduplicated per xpub by a
  cheap fingerprint (tx count + balance + latest tx id), and coalesced so
  only the newest pending snapshot is written. Previously getWalletData
  JSON.stringified the entire WalletData synchronously on every wallet
  switch and background refresh. Stored shape is unchanged, so the cache
  read path is untouched; tracking is reset wherever caches are removed.
- Nostr operations now share one lazily-created SimplePool instead of
  opening and closing four relay websockets per call; the pool closes on
  Nostr disconnect and provider unmount.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016A3N8t6WuMeHgiKQLWFN7T
Window both layouts with @tanstack/react-virtual so DOM size no longer
scales with wallet history:

- Desktop keeps real <table> semantics via spacer rows (top/bottom
  padding <tr>s around the visible window), sticky header, 65vh scroll
  container
- Mobile card list uses the standard absolute-positioned item pattern
  with measureElement for variable card heights
- Remove the unbounded Load More accumulation — the full list renders
  through the virtualizer (a high-activity wallet previously mounted
  every loaded row); the header now shows the total count

Verified in-browser with a connected wallet: windowed rendering on both
viewports, smooth scroll to end, CSV export unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016A3N8t6WuMeHgiKQLWFN7T
…ayout/*

Pure extraction, no behavior change: AnalyticsWarning, AddAccountDialog,
AccountSwitcher, CurrencySwitcher, and the Nostr footer block (summary,
connect + edit-profile dialogs, save-xpubs prompt, and their forms) move
to src/components/layout/. The layout file drops from 845 to 265 lines
and keeps only the shell: nav, header, route guard, support toast.

One drive-by fix: CurrencySwitcher rendered a nested <SelectContent>
inside another <SelectContent>.

Verified in-browser: account switcher popover, add-account dialog, Nostr
connect dialog, and currency switch (balance re-renders in EUR) all work.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016A3N8t6WuMeHgiKQLWFN7T
… slices

WalletProvider now exposes four independently memoized contexts layered
inside the single provider:

- useWalletData: balances, transactions, discovery, currency derivations
- useWalletActions: referentially stable callbacks — action-only
  consumers never re-render on provider state churn
- useWalletAi: chat messages, suggestions, recommendations — streaming
  chat no longer re-renders data-only pages
- useNostr: Nostr account state and actions

useWallet() still returns the merged shape, so all existing consumers
work unchanged; the app shell (layout, account/currency switchers,
add-account dialog, Nostr block) and the chat page are migrated to the
scoped hooks where the split pays.

Verified in-browser: full connect flow with streaming discovery,
currency switch propagating across pages, chat greeting + message send,
no console errors.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016A3N8t6WuMeHgiKQLWFN7T
…nect

Root cause of slow connects: every blockchain lookup was an individual
Next.js Server Action, and Server Actions execute serially per client.
The gap-limit scan's Promise.allSettled over 20 per-address calls was
fake parallelism — each call paid a full client->server round trip, one
at a time, ~70-200 round trips per fresh connect.

- Add getAddressStatsBatch (<=40 addrs) and getAddressBundleBatch
  (<=20 addrs, stats+txs+utxos with the balance>0 UTXO retry) to the
  'use server' proxy; the server fans out to the provider in parallel
  with a politeness cap of 10, per-item address sanitization unchanged
- Discovery: type detection is one batch call; each gap window resolves
  in a single round trip; the per-address data phase fetches chunks of
  15 per call instead of 3 calls per address through a client worker pool
- getWalletDataProgressive: fetch ticker and chain tip in parallel; drop
  the standalone provider readiness check (first batch surfaces errors)
- Privacy unchanged: only derived addresses cross to the server, never
  the xpub

Measured with 100ms emulated client RTT (production-like), fresh cache:
click -> full dashboard data 17.0s -> 2.8s; discovery 11.9s -> 1.3s.
On localhost the full e2e connect now lands in under a second.

Tests: update architecture-snapshot assertions, rewrite the address-data
concurrency suite for chunked batching (incl. server-side UTXO-retry and
bounds coverage; 60 -> 65 tests), fix the e2e modal assertion that
referenced long-gone "Connecting Your Wallet" copy, and make the e2e
disconnect step resilient to a slow dev redirect. Full e2e suite passes
3/3 consecutive runs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016A3N8t6WuMeHgiKQLWFN7T

if (!activeXpub) return <FullPageLoader />;
if (isLoading && !data) return <FullPageLoader />;
if (error && !data) return <ErrorDisplay message={error ?? 'Unable to load wallet data.'} />;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants