Skip to main content

Components

Sizes are desktop (fine pointer) / mobile (coarse pointer). Every interactive element is a real <button> or <a> with an accessible name. Motion for each component is in motion.md; tokens are in tokens.md. Common states. These apply to every interactive component unless its section says otherwise.

Button

Controls in one row share one height (CLAUDE.md “Layout consistency”); there are no 28 or 36 px buttons beside 32 px ones. Where a row is shorter than 44 px on touch (the 28 px ladder rows on a fine pointer only), buttons grow an invisible hit area (a ::before with inset: -Npx) up to 44 × 44. Hit areas never overlap a neighbour’s; where rows are too tight, use the mobile layout from the screen spec.

Price button

The one-click trade control. It shows the limit price it sends. Used on the board, watchlist, market view and portfolio (Exit, Join, Add). Colour follows the side of the order it sends: Buy = --bid text on --bid-muted, showing the best ask. Sell = --ask text on --ask-muted, showing the best bid. Layout rules (CLAUDE.md “Layout consistency”):
  • Nothing is clipped. The button is shrink-0 and its width fits the widest price on the market’s tick (99.9¢ on a 0.001 tick) at numeric-price size. A price never truncates and never shrinks its font; Exit, Join and Add labels (Exit 39¢) reserve their widest value the same way.
  • Centred means optically centred. The price is centred on its own text. The reduced-motion direction marker, the sending bar and any other marker that comes and goes are positioned absolutely (position: absolute, outside the text box) and never take width from the price, so 42¢ sits in the same place with or without them, within 1 px of the button’s centre. On mobile the caption and the price are each centred on their own text.
  • One gap per group. Price buttons that form a grid (the mobile Sell/Buy pairs of a board card, the portfolio’s Exit/Join/Add row) use one gap from the spacing scale, the same across and down: 4 px on mobile.

Cancel all

Cancels every open order on every venue. One click, no confirmation, always reachable. It is the one trading control with icon and text.

Safe/Armed switch

A two-segment toggle. The state lives on the server and is checked by engine/risk. The switch shows the server state; it never changes on its own before the server confirms.
Text only: Arm/Safe change state, so the segments carry words and no icons. There is no hotkey to arm; arming is always a deliberate click. Cancel all does not change the switch.

Stake selector

The active stake (pUSD) used by every one-click buy. Global for the session.
  • Desktop: segmented control in the top bar. One segment per preset from settings (1–6), 40 × 32 each, numeric (14/20): 10 25 50 100. The active segment is --primary bg / --primary-foreground text. Label Stake $ sits left of it.
  • Mobile: the stake bar: a full-width 44 px row pinned above the tab bar on Live and Watchlist, and in the ladder sheet footer. Segments are 44 px tall and share the width equally.
  • Hotkeys 1–6 pick a preset in the focused panel (armed only).
  • Disabled segments: none. Stake selection works in Safe.

Status pill

24 px tall, --radius-sm, label text (12/16 semibold), 8 px padding. The top bar shows the worst current state; clicking opens a popover with one row per feed (market WS, user WS, sports WS, browser link, geoblock last check). Worst-first order: disconnected, geoblocked, trading off, cancel-only, stale, reconnecting, live. Panels showing data from one feed carry a smaller copy (20 px) of the stale pill in their header while that feed is stale.

Price cell (with flash)

A non-button price or value in tables and the ladder (numeric or numeric-dense).
  • Right-aligned, fixed width per column, tabular-nums.
  • Structure: position: relative cell, a ::after flash layer (inset: 0, opacity: 0, pointer-events: none) under the text.
  • On a change of the displayed value: the value swaps instantly, and the flash layer gets --flash-up (value rose) or --flash-down (value fell) and runs the flash (see motion.md). Sizes and depth do not flash; only prices and P&L columns do.
  • Stale: no flash, --muted-foreground text.
  • Reduced motion: no flash; a static lucide [arrow-up] (--bid) or [arrow-down] (--ask) after the value, cleared on the next change or after 5 s. It is positioned absolutely outside the value’s text box, so the value never moves when it appears.
  • Nothing is clipped: the cell reserves the widest value of its column and never truncates; beyond it the value switches to compact form.

Order chip

Your own order, drawn on the ladder level it rests at and in the Status column of the orders table. Side colour: buy chips use --bid text and border on --bid-muted; sell chips use --ask. Layout rules (CLAUDE.md “Layout consistency”):
  • Nothing is clipped. A chip is shrink-0 and never truncates its size or count. Its width fits the widest value it can show in its slot (12.3k, 12/25, ×2); a larger size switches to compact form (1.2k) with the exact size in the tooltip. In the ladder the chip sits at the outer edge of the Bid or Ask cell and the depth size gives way (it moves under the chip’s opposite side), never the chip.
  • Centred means optically centred. The size is centred on its own text. State markers that come and go (the [check] on a fill, [circle-alert] on a reject, [circle-help] on an unknown order, the countdown ring) are positioned absolutely at the chip’s inner left edge, inside padding the chip always has, so the size does not move when a marker appears or leaves.
  • A chip and the other controls in its row (steppers, cancel button in the orders table) use one height: the table’s --control-h, or the ladder’s chip height below.

Countdown ring

12 px (ladder) or 14 px (tables, board) circle in --warning-foreground on the warning chip, emptying clockwise over the real delay length. Next to it, whole seconds left. Built from two half-discs rotated with transform so only transform animates. When the backend sends no end time, the ring is hidden and the chip reads Delayed.

Cursor tag

A small label that follows the mouse over any trade target and states exactly what a click sends. Fine pointer only; mobile uses the stake bar instead.
  • 24 px tall, --popover bg, 1 px --border, --shadow-md, label text, offset 12 px right and 16 px below the pointer, flipped at viewport edges. Moves with transform.
  • Appears on the first frame of hover, with no delay and no fade.
  • Content:
    • Buy: Buy $25 → 59.52 sh @ 42¢
    • Sell: Sell 59.52 sh @ 44¢ · +$1.19
    • Drag: Move buy 25 → 44¢
    • Safe: Safe: arm to trade
    • Stale: Stale 6s
    • Sell with no position: No position to sell
  • Share counts are an estimate. The backend rounds size to the market’s minimum and tick before signing.

Tick stepper

[−] 42¢ [+] inline editor for an order’s price. Buttons are --control-h square (32 desktop, 44 mobile); the price between them is numeric, 48 px wide, centred on its own text. The two buttons, the price and a following cancel button keep one gap (4 px). Each click moves the price by one tick and sends a replace (interactions.md). While a replace is in flight, the price shows the target in --muted-foreground with a → prefix. − is disabled at the lowest tick and + at the highest; both are disabled while the order is delayed.

Panel

Any focusable region that owns hotkeys: board, each docked ladder, dock tabs, portfolio, orders.
  • Header: 36 px, heading text, --border bottom line, actions on the right.
  • Focused: header title in --foreground. A 2 px --ring line along the panel’s top edge (the panel focus indicator) shows only after F6 / Shift+F6, or while Armed with more than one panel that takes trading keys visible (interactions.md). Unfocused headers use --muted-foreground. Exactly one panel is focused at a time.
  • Focus moves on pointerdown anywhere in the panel, with F6 / Shift+F6, or when a panel opens.

Position row

36 px table row (portfolio, bottom dock). The full column spec is in portfolio.md.

Ladder row

One price level: --ladder-row-h tall. Full spec in price-ladder.md.

Score badge

Possession: a 4 × 12 bar in --foreground next to the team with the ball (compact) or the word ball after the team abbreviation (large: LAL ball). Score values change instantly; a changed score gets a 400 ms --ring outline (motion.md), never a count-up.

Toast

sonner. Desktop: bottom-right, 16 px above the bottom dock, 360 px wide; at most 3 visible, older ones collapse behind and expand on hover. Mobile: at the bottom, above the stake bar and tab bar, 8 px side margins, one visible at a time, so the header (Safe/Armed and Cancel all) is never covered.
  • 56 px minimum height, --popover bg, --shadow-md, the overlay radius. A title line and a body line (body-sm, muted, up to two lines). A 3 px left stripe only for warning and critical; at most one text action and a close [x].
  • Hover pauses the timer (desktop). Swipe dismisses (mobile).
There are two sources of toasts, and one event never shows two:
  • Notifications (fills, rejects, alerts, feed and trading-state events) come only from the notifications WS topic, with their severity’s stripe and duration: info 4 s, warning 8 s, critical until closed. Copy, stacking and coalescing are in notifications.md.
  • Results of the user’s own action that is not a notification kind stay local:
Sounds and desktop notifications follow the notification rules in notifications.md; local toasts play no sound.

Command palette row

40 px (desktop) / 52 px (mobile).
Rows render instantly; they never animate in.

Empty states

Centred in the panel. 24 px icon in --muted-foreground, heading text, one line of body-sm, and at most one action button (secondary). Keep the panel’s own height; do not collapse it.

Skeletons

Shown only on a cold load (no cached data). Same geometry as the real content: the same row heights, column widths and button sizes, so nothing shifts when data arrives. --muted blocks with --radius-sm. Static, no shimmer or pulse (a pulse would exceed --dur-slow). Text blocks are 60–80% of the column width; numbers are right-aligned blocks. Replace the whole skeleton in one frame when data arrives.