Skip to content
All docs

Frontend: design system, session, mobile

One design system

All colors, radii and type come from tokens in web/src/app/globals.css. The product is light theme only (owner decision, 2026-10-08; the earlier dark/system theme was removed).

TokenValueUsed for
--color-bg#f6f5f1page background
--color-surface / -2 / -3white → warm greyscards, raised and pressed surfaces
--color-fg / -fg-2 / -mutedgraphite → greyprimary, secondary, muted text
--color-line / -line-strongsubtle / strong borderborders
--color-primary / -primary-hover / --color-on-primaryblack #16181c / #2c3038 / whiteprimary buttons, logo
--color-accent / -accent-bggraphite #16181c / warm grey #ebe9e3links, focus rings, active navigation, selected tabs, progress (monochrome: no blue anywhere, owner decision 2026-10-09; tested by e2e/monochrome.spec.ts)
--color-safe / warn / breach / info (+ -bg)muted semanticstatus, always paired with an icon and a word
--color-long / --color-shortgreen / redtrading direction only
--radius-sm / -md / -lg6 / 8 / 12 pxcontrols / cards / sheets

Buttons: three levels, one function

buttonClass(variant, size) in web/src/components/ui.tsx is the only button style. <Button> and every link styled as a button use it.

LevelVariantLookExample
Primaryprimaryblack background, white textOpen app, Create passkey, Claim realized profit
Secondarysecondaryneutral surface with borderSee how it works, Sign in, Close desk
Tertiarytertiaryaccent text, no boxView proof, Security model

long, short and danger are trading semantics, not brand. Inputs use inputClass(). Every primary button, on the landing page and in the app, is the same black.

Component audit (2026-10-08)

ComponentShared?Notes
ButtonsyesbuttonClass / Button everywhere, error page included
Button-styled linksyesbuttonClass (landing CTAs, header, menus)
InputsyesinputClass (order ticket, desk stake)
CardsyesCard (title, icon, padding)
Status badges / pills / noticesyesPill, EvidenceBadge, Notice with tone tokens
Tablesyestoken borders; card layout on phones for positions, tiers, LP desks, risk rules
NavigationyesSiteHeader, AppShell (sidebar, bottom bar, More sheet)
Chartyescandle and grid colors read from the same tokens

Session (why users were signed out, and what changed)

Cause. The signed-in account existed only in React memory, so anything that reloaded the page ended it:

  • a refresh;
  • typing or pasting a URL;
  • opening a new tab;
  • the dev server's reload after a code change;
  • links inside the Docs pages, which were plain HTML links that reloaded the page.

A 30-minute idle timer also signed the user out completely.

Now. One provider at the root (web/src/lib/account/AccountProvider.tsx) is the only source of truth. Every page reads the same state:

StateMeaningWhat the user sees
initializingreading the remembered accountplaceholders, never "Sign in"
signed-outno account on this devicesign-in prompt
lockedaccount known, signing key not in memoryall their data; the next transaction asks the passkey once
readysigning key in memoryeverything, no prompt
connectingpasskey prompt openprogress
  • Remembered: the address, kind and credential id are stored in localStorage. These are

public identifiers, not secrets. The private key is never stored; it lives in memory and is zeroed when signing locks.

  • After a refresh, new tab or direct URL the account comes back as locked and every

screen shows the user's data. Sending a transaction (getSigner()) asks for the passkey once, then signing stays unlocked.

  • Idle for 30 minutes: signing locks, the account stays.
  • Cancelled passkey prompt: shows "cancelled, nothing was sent" and never signs out.
  • Sign out happens only on an explicit "Sign out on this device"; other tabs follow.
  • Docs links now navigate client-side, and the desk data provider is mounted at the root,

so moving between /app, /lp, /proof and /docs keeps everything loaded.

Tested in web/e2e/passkey.spec.ts with a virtual WebAuthn authenticator:

  • every app page by sidebar;
  • public pages and back;
  • direct URLs for every route;
  • reload;
  • a new tab;
  • 375 px and 1280 px viewports;
  • unlock after reload;
  • a cancelled prompt.

Mobile layout

Site header (below 1024 px). ☰ and Imprest (plus Open app from 640 px). The menu holds the links, the network badge and Open app.

App bottom bar (below 1024 px). Home, Trade, Positions, Risk and More. More opens a bottom sheet with Claims, History, Desk & graduation, My receipts, LP console, Proof, Docs and Status, plus the network badge.

App header. Logo and the account button. "Create passkey" lives on the sign-in card in the page body.

Tables. On phones, positions, tiers, LP desks and risk rules become stacked cards with "Details" where needed. The two Proof research tables scroll sideways inside their own box and say so.

Trade. Market, chart, positions, then the order ticket (side, size, leverage, risk summary, submit). Everything is in one vertical column.

Responsive and visual tests

  • web/e2e/pages.spec.ts:
  • overflow at 320, 375, 390, 414, 430, 768, 1024, 1280, 1440 and 1920 px on 12 pages;
  • overflow again at 320–1440 px with a funded desk loaded (the testnet trader's public address remembered, no key).
  • web/e2e/contrast.spec.ts: WCAG contrast of at least 3:1 for every text element on 12 pages.
  • web/e2e/visual.spec.ts (opt-in, SCREENSHOTS=1): full-page screenshots per width and page into proof/screenshots/. Filters: SHOT_WIDTHS, SHOT_PAGES, SHOT_ACCOUNT.

Loading, metadata and icons

  • Loading: every data placeholder (Skeleton) shows a spinner and announces "Loading". Pages that read the chain for several seconds use PageLoading: a spinner plus what is being read, as on the LP console. /status runs its checks on the server and has a route-level loading screen. There is no root-level loading screen, because one makes unknown URLs return 200 instead of 404.
  • Metadata: the root layout sets the title template, description, keywords, canonical URL, Open Graph and Twitter summary_large_image tags. opengraph-image.tsx and twitter-image.tsx render a 1200×630 card. Every route has its own title and description; client pages get theirs from a small layout.tsx. Set NEXT_PUBLIC_SITE_URL if the site moves off imprest-chi.vercel.app.
  • Icons: app/icon.svg, app/favicon.ico (16–64 px) and app/apple-icon.png (180 px), all the Imprest logo. Regenerate the raster icons with python -I scripts/make-icons.py.