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-0and 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, so42¢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 byengine/risk. The
switch shows the server state; it never changes on its own before the server confirms.
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--primarybg /--primary-foregroundtext. LabelStake $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–6pick 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: relativecell, a::afterflash 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-foregroundtext. - 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-0and 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,
--popoverbg, 1 px--border,--shadow-md, label text, offset 12 px right and 16 px below the pointer, flipped at viewport edges. Moves withtransform. - 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
- Buy:
- 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,
--borderbottom line, actions on the right. - Focused: header title in
--foreground. A 2 px--ringline 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,
--popoverbg,--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).
- Notifications (fills, rejects, alerts, feed and trading-state events) come only
from the
notificationsWS 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.