Skip to content

feat(web): add settings page, vault detail modal, and skeleton loading states - #761

Merged
OnlyReeda merged 3 commits into
OdyxeeeLabs:mainfrom
ThatCodeBabe:feat/settings-vault-modal-skeletons-wave10
Oct 22, 2026
Merged

OnlyReeda merged 3 commits into
OdyxeeeLabs:mainfrom
ThatCodeBabe:feat/settings-vault-modal-skeletons-wave10

Conversation

@ThatCodeBabe

Copy link
Copy Markdown

Summary

Implements three Stellar Wave Program issues in one branch (#685 depends on loading states introduced by #684, so these were done in order: #682 → #684 → #685).

#682 — Settings page

/settings, five tabs (Network, Trading, Display, Security, Advanced). No backend settings service exists yet, so this is a client-only first cut backed by localStorage (web/lib/settings.ts + web/lib/settingsStore.ts, following the same createStore/createUseStore pattern context/WalletContext.tsx already uses). Reuses existing state instead of duplicating it where it already exists:

  • Network tab reads the build-time active network from lib/contracts.config.ts's getStage() (same source NetworkStatusBanner uses) rather than re-deriving it, and persists a UI-only "preferred network" + custom RPC endpoint + chain ID for a future runtime switcher.
  • Display tab embeds the existing <ThemeToggle /> as-is.
  • Security tab reuses useWallet() for the connected-wallets list.
  • Advanced tab reuses the existing featureFlagService singleton directly (the React <FeatureFlagProvider> is only mounted in the Pages Router tree, not available to this App Router page).

#684 — Vault detail modal

Expandable modal/drawer with strategy config, performance metrics, risk parameters, a historical APY sparkline, token info, and deposit/withdraw/manage actions (wired to a toast placeholder — no transaction-building). Data loads on demand (useVaultDetail, only fetches once the modal actually opens).

There's no backend endpoint yet returning strategy/risk/performance data, so lib/vault-detail.ts generates deterministic, seeded mock data from the vault id (same id → same numbers, covered by lib/vault-detail.test.cjs) rather than inventing a fake "looks real" contract. The historical chart is a small inline SVG sparkline — no charting library added (none exists in package.json, and next.config.js already splits vendor chunks deliberately for bundle size, #686).

VaultDetailContent is shared between the modal and the /vault/[id] and /vaults/[id] page routes (previously placeholder stubs), so the list-triggered and deep-linked views never drift apart. Both page routes now also export generateMetadata via the existing buildVaultMetadata/buildVaultJsonLd helpers, and correctly type Next.js 15+'s async params.

components/VaultRow.tsx gained an optional onClick (keyboard-accessible, doesn't conflict with the existing href/PrefetchLink case), and components/vault/VaultList.tsx had a broken import (./VaultRow instead of ../VaultRow) fixed so it actually compiles. A new /vaults list page wires VaultList → VaultDetailModal end-to-end, since neither component had a caller anywhere in the app before this change.

#685 — Skeleton loading states

New skeletons: VaultCardSkeleton/VaultListSkeleton, VaultDetailSkeleton (ships with #684 from the start, per the issue's own sequencing note), TransactionRowSkeleton (sized to the existing TRANSACTION_ROW_HEIGHT constant — no TransactionRow component exists yet to retrofit), DataTableSkeleton (mirrors components/ui/DataTable.tsx), and FormSkeleton (used by the Settings page's hydration guard). All follow the accessibility/visual convention already established by NutritionLabelSkeleton/ResultViewerSkeleton (animate-pulse, role="table"/"row"/"cell", aria-busy="true"), colored to match the content each stands in for.

Also wired the two existing-but-previously-unused skeletons into real call sites: ResultViewerSkeleton now replaces both the dynamic-import "Loading result viewer…" text fallback and the in-flight-simulation state in pages/index.tsx.

Left alone intentionally: the small Loader2 icons inside DynamicForm's and WasmUpload's active submit buttons — those are transient busy-state indicators on an already-labeled control ("Simulating…", "Invoking…"), not full-content loading screens; swapping a 16px icon for a skeleton there would be the wrong pattern.

Pre-existing issues found and fixed (unrelated to these 3 issues, but blocking verification)

  • app/layout.tsx and pages/_app.tsx each had duplicated/unreconciled JSX from two previously-merged features, failing to parse at all. Reconciled into single trees keeping every provider from both branches.
  • pages/index.tsx had a duplicated second results column; merged into one.
  • web/package.json was missing the @tanstack/react-virtual dependency that yarn.lock and existing vault-list code already required (present in the lockfile, never added to package.json) — added it; package-lock.json updated. (yarn.lock intentionally left alone — yarn 4.9.1 crashed with an unrelated internal error in the sandbox used for this change; a maintainer with a working yarn should run yarn install once to sync it.)
  • web/package.json's test script glob was silently dropping every direct-child test file under lib/ and __tests__/ when run through sh (** degrades to a single * under POSIX glob, requiring exactly one directory level). Quoted the globs so Node's own recursive glob engine handles them — this surfaced ~190 previously-dead tests, including the new ones added here, and 6 pre-existing, unrelated failures (component-docs registry drift, an App Router migration placeholder check, a mobile-responsiveness class-string check, and a CSP connect-src check) that were never actually being run before. None of those 6 are touched by this PR.
  • hooks/useFocusTrap.ts's FocusTrap interface hardcoded a non-generic ref type, breaking any typed caller (already affected NavDrawer.tsx; now needed for the new vault detail modal). Made it generic.
  • lib/vault-metadata.ts's normalizeVaultData read fields off a union type that didn't typecheck. Read through an explicit Record<string, unknown> view instead.

Verification

  • npm test (web/): 545/551 passing. The 6 failures are 100% pre-existing on a clean upstream/main (confirmed by running the suite on a pristine checkout with only the test-glob fix applied) and unrelated to these 3 issues — see above.
  • npm run typecheck (web/): fails, but only with the same pre-existing errors present on a clean upstream/main before this branch (missing optional devDependencies for storybook/playwright/testing-library/axe-core, a sentry SDK version mismatch, next-intl config types, lib/api.ts DTO narrowing, an unrelated dead-variable bug in ErrorBoundary.tsx, etc.) — confirmed by diffing the error list before/after this branch's changes. No new errors introduced; several pre-existing ones fixed (see above).
  • npm run lint (web/): fails project-wide, including on files this PR never touches — every file reports Definition for rule '@typescript-eslint/...' was not found, an eslint/eslint-config-next version-compatibility break pre-existing on upstream/main. Filtering that specific noise out, npx eslint on every file this PR adds/touches reports zero other issues.
  • npm run build (web/): fails on a clean upstream/main before this branch too — Next 16 treats the App Router/Pages Router path collisions on /, /admin/managers and /vault/[id] as hard build errors (web/Vault.md documents this App Router migration as still in progress, Phase 2 of 4). This branch adds no new colliding routes (/settings, /vaults, /vaults/[id] have no Pages Router equivalent) and doesn't change this pre-existing failure mode.

Judgment calls

  • Settings persistence: localStorage-backed client store, no backend — documented above and in lib/settings.ts's file header.
  • Network tab's network toggle is a UI-only preference; the real network is build-time-resolved (NEXT_PUBLIC_STELLAR_NETWORK) and shown read-only.
  • Language tab persists a preference rather than wiring the existing LanguageSwitcher (which depends on next/router, unavailable under the App Router page this Settings page renders on).
  • Vault detail data is deterministic mock data (seeded by vault id), since no backend endpoint returns strategy/risk/performance fields yet.
  • Historical performance chart is a hand-rolled SVG sparkline, not a new charting dependency.
  • Fixed several pre-existing, build/test-blocking bugs encountered along the way (see above) rather than leaving verification impossible; left the larger, unrelated pre-existing issues (App Router/Pages Router route collisions, the eslint plugin resolution break, the ~6 newly-surfaced-but-unrelated test failures) untouched as out of scope.

Closes #682
Closes #684
Closes #685

Found while implementing OdyxeeeLabs#682/OdyxeeeLabs#684/OdyxeeeLabs#685 and verifying lint/typecheck/
test/build per the required process — all pre-date this branch and are
unrelated to the three issues, but were blocking verification entirely:

- app/layout.tsx and pages/_app.tsx each had duplicated/unreconciled JSX
  trees (two merged features' provider trees left side by side instead of
  combined), making them fail to parse at all. Reconciled into single,
  coherent trees that keep every provider/feature from both branches
  (ErrorBoundary, MotionProvider, WalletProvider, ToastProvider,
  AnnouncerProvider, SkipLink, NetworkStatusBanner, RpcFallbackBanner,
  CommandPaletteLauncher, Analytics, ServiceWorkerManager).
- pages/index.tsx had a duplicated second results column (two versions of
  the same UploadZone/ContractInteraction block). Merged into one, keeping
  the newer ErrorBoundary-wrapped ResultViewer and the OdyxeeeLabs#470 live-region
  announcements from both. Also replaces the dynamic-import loading
  fallback and in-flight-simulation state with the previously-unused
  `ResultViewerSkeleton` instead of plain "Loading…" text (issue OdyxeeeLabs#685).
- web/package.json was missing the `@tanstack/react-virtual` dependency
  that `yarn.lock`, `VaultList.tsx` and `useVirtualVaultList.ts` already
  depend on (present in the lockfile but never added to package.json),
  so a clean install couldn't resolve it. Added as a direct dependency;
  package-lock.json updated accordingly. (yarn.lock intentionally left
  alone — yarn 4.9.1 crashes with an unrelated internal error in this
  sandbox; a maintainer with a working yarn should run `yarn install`
  once to sync it for yarn-based installs.)
- web/package.json's `test` script glob (`./lib/**/*.test.cjs` etc.) was
  silently dropping every direct child test file under `lib/` and
  `__tests__/` when run through `sh -c` (POSIX glob treats `**` as a
  single `*`, requiring exactly one directory level — only nested test
  files matched). Quoted the globs so Node's own test runner expands them
  (it supports true recursive globs), surfacing ~190 previously-dead
  tests, including the new ones added for OdyxeeeLabs#682/OdyxeeeLabs#684.
- hooks/useFocusTrap.ts's `FocusTrap` interface hardcoded
  `ref: RefObject<HTMLElement>` instead of being generic over `T`, so any
  new caller of the generic `useFocusTrap<T>()` (used here for the new
  vault detail modal, OdyxeeeLabs#684) failed to typecheck when attaching the ref to
  a typed element — the same bug already affected NavDrawer.tsx. Made the
  interface generic.
- lib/vault-metadata.ts's `normalizeVaultData` read fields directly off a
  `Partial<Vault | VaultRecord | VaultMetadataInput>` union, which doesn't
  typecheck since the members don't share those fields. Read through an
  explicit `Record<string, unknown>` view instead (every value is already
  re-validated via String()/Number() immediately after).

Confirmed via diffing `tsc --noEmit` output before/after: no new errors
introduced, and the fixes above remove 4 pre-existing error sites for
free. `npm run build` still fails on a clean upstream/main checkout for
unrelated reasons (literal App Router vs Pages Router route collisions on
/, /admin/managers and /vault/[id] — web/Vault.md documents this
migration as still in progress) and `npm run lint` fails project-wide
because `@typescript-eslint`'s rules aren't resolving under the installed
eslint-config-next/eslint version combination (every file in the repo
reports it, including ones untouched by this branch) — both pre-exist on
upstream/main and are out of scope for this change.
Closes OdyxeeeLabs#682.

Comprehensive settings page at /settings, organized into five tabs:

- Network: shows the build-time-resolved active network (reads
  lib/contracts.config.ts getStage(), the same source NetworkStatusBanner
  already uses — not duplicated), plus a persisted "preferred network"
  display preference, custom RPC endpoint and chain ID override for a
  future runtime network switcher.
- Trading: slippage tolerance (0.1-10%, clamped), gas price strategy
  (slow/standard/fast/custom with a custom gas price field), transaction
  deadline in minutes.
- Display: currency, number format, theme (reuses the existing
  <ThemeToggle /> component as-is rather than re-implementing `<html
  class="dark">` toggling), and a preferred-language preference.
- Security: 2FA toggle (flagged as requiring backend enrollment, not
  implemented), session timeout, and connected wallets — reuses
  useWallet() from context/WalletContext.tsx rather than re-reading wallet
  localStorage keys directly.
- Advanced: debug mode, feature flags (reuses the existing
  featureFlagService singleton directly — the React <FeatureFlagProvider>
  is only mounted in the Pages Router tree, not available to this
  App Router page), and a "download settings as JSON" data export.

Persistence: there is no backend settings service, so this is a
client-only first cut — lib/settings.ts holds validated, pure
get/merge/persist logic (deep-merges onto defaults so a corrupted or
partial localStorage blob never crashes the UI), and lib/settingsStore.ts
is a thin React binding following the same createStore/createUseStore
pattern context/WalletContext.tsx already uses. The page defers reading
localStorage until after mount (useHydrateSettings) and shows the new
FormSkeleton (OdyxeeeLabs#685) for that brief window instead of a flash of default
values or a spinner.

Tests: lib/settings.test.cjs covers clamping, merge-with-defaults
(including recovery from corrupted/out-of-range stored values), and
JSON parse/export, following the existing config.test.cjs convention
(mirrors the pure logic in plain JS + a source guard against drift).
Closes OdyxeeeLabs#684.

Expandable vault detail modal/drawer, data loaded on demand:

- types/vault.ts gains VaultDetail (strategy config, performance metrics,
  risk parameters, token info, historical points) alongside the existing
  lean Vault type used for list rows — kept separate since the detail
  view needs much richer data than every row fetch should pay for.
- lib/vault-detail.ts fetches it on demand. vaultService.get() only
  returns id/name/status/config_json today (no strategy/risk/performance
  fields), so this generates deterministic, seeded mock data from the
  vault id (same id -> same numbers, verified in vault-detail.test.cjs)
  rather than inventing a fake "looks real" backend contract that would
  need to be un-mocked later. buildVaultDetail is the single seam to
  start reading real fields from config_json once the backend has them.
- hooks/useVaultDetail.ts only starts fetching once an id is passed in,
  so a closed modal never pays the fetch cost.
- components/vault/HistoricalPerformanceChart.tsx is a small inline SVG
  sparkline — no charting library dependency added. package.json has none
  today, and next.config.js already deliberately splits vendor chunks to
  manage bundle size (OdyxeeeLabs#686); one sparkline isn't worth a new dependency.
- components/vault/VaultDetailContent.tsx renders the sections (token
  info, performance, chart, strategy, risk, actions) and is shared by
  VaultDetailModal (overlay) and the app/vault/[id] + app/vaults/[id]
  page routes (full page), so list-triggered and deep-linked views never
  drift apart. Deposit/withdraw/manage actions are wired to a toast
  placeholder — building real transactions is out of scope here.
- VaultDetailModal reuses the existing useFocusTrap hook (OdyxeeeLabs#703) rather
  than a bespoke focus trap, and ships with its own matching skeleton
  (VaultDetailSkeleton, OdyxeeeLabs#685) instead of a spinner.
- components/VaultRow.tsx gains an optional onClick, making the whole row
  an interactive trigger (role="button", keyboard support) without
  breaking the existing href/PrefetchLink case; components/vault/VaultList
  wires it to the modal and also had its VaultRow import fixed (it
  pointed at a nonexistent ./VaultRow instead of ../VaultRow).
- app/vault/[id]/page.tsx and app/vaults/[id]/page.tsx were placeholder
  stubs; both now call generateMetadata (via lib/vault-metadata.ts's
  existing buildVaultMetadata/buildVaultJsonLd) and render the vault
  detail content, with Next.js 15+'s async `params` handled correctly
  (previously typed as a synchronous object). This also satisfies two
  pre-existing tests in __tests__/vault-metadata.test.cjs and
  __tests__/vault-routing.test.cjs that were never actually running
  before this branch's test-glob fix and were failing against the old
  stubs.
- app/vaults/page.tsx is a small new list page (fixed demo id set — there
  is no "list all vaults for a manager" flow wired up yet) that
  demonstrates VaultList -> VaultDetailModal end to end, since neither
  component had any caller in the app before this change.

Also includes the remaining skeleton loading states for OdyxeeeLabs#685:
components/TransactionRowSkeleton.tsx (sized to the existing
TRANSACTION_ROW_HEIGHT constant — no TransactionRow component exists yet
to retrofit) and components/ui/DataTableSkeleton.tsx (mirrors
components/ui/DataTable.tsx's header/filter/row layout). VaultCardSkeleton
and VaultDetailSkeleton ship in this same commit since OdyxeeeLabs#684 needed them
immediately; see the previous commit for FormSkeleton and the
ResultViewerSkeleton wiring into pages/index.tsx.

All new skeletons follow the accessibility/visual convention already
established by NutritionLabelSkeleton/ResultViewerSkeleton
(animate-pulse, role="table"/"row"/"cell", aria-busy="true"), using the
color palette of the content each one stands in for (slate for
vault/transaction rows to match VaultRow, the existing #161b22 GitHub-dark
palette for the generic table/form skeletons).

Deliberately left alone: the small Loader2 icons inside DynamicForm's and
WasmUpload's active submit buttons (components/DynamicForm.tsx,
components/WasmUpload.tsx). Those are transient busy-state indicators on
a button that already has a label ("Simulating…", "Invoking…", a
per-file upload row), not full-content loading screens — replacing a
16px icon inside an already-visible, already-labeled control with a
skeleton would be the wrong pattern, per components/ui/Spinner.tsx's own
documented purpose.
@drips-wave

drips-wave Bot commented Oct 22, 2026

Copy link
Copy Markdown

@ThatCodeBabe Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@OnlyReeda
OnlyReeda merged commit b6906e4 into OdyxeeeLabs:main Oct 22, 2026
6 of 8 checks passed
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.

Add skeleton loading states Implement vault detail modal Create settings page

3 participants