Interactions
How input maps to actions everywhere in the app. Component looks are in components.md; screen layouts are in screens/.Preconditions for trading
A trade action (anything that places or moves one of your orders) needs:- Trading enabled on the server (
/statustrading_enabled:LIVE_TRADINGset and not geoblocked). - The Safe/Armed switch on Armed (server state).
- Fresh data for the price being clicked (not stale).
engine/risk
enforces them; the UI hiding a button is not a safety control. Cancel actions only
need a connection: Cancel all, cancel-market and single cancels work in Safe, with
trading off, and on stale data.
Click, drag and long-press
Event timing
- Mouse: a trade fires on
pointerdown(primary button). It does not wait forclick, and no animation delays it. - Touch and pen: a trade fires on tap: pointer up within 500 ms and less than 8 px of movement. More movement is a scroll; a longer press is a long-press. This keeps scrolling from placing orders.
- Double-click guard: a second activation of the same target (same market, side, price and stake) within 350 ms reuses the first click’s idempotency key, so the backend places one order. A click after 350 ms is a new order. (The 350 ms is a behaviour constant in the frontend trade helper, not a motion token.)
The one rule for right-click and long-press
Right-click (desktop) or long-press (touch, 500 ms) on your own order cancels it. On anything else it opens that thing’s context menu (desktop menu, mobile action sheet). Long-press gives haptic feedback where supported (navigator.vibrate(10)). The browser
context menu is suppressed only inside trading panels.
Ladder (desktop)
Ladder (mobile bottom sheet)
Drag to reprice
Desktop only (mobile uses the order bar steppers).- Pointerdown on a placed or partially filled chip, then move more than 4 px: drag starts. The original chip turns dashed and stays at its level.
- A ghost chip follows the pointer snapped to rows; the target row gets a dashed
--ringoutline; the cursor tag readsMove buy 25 → 44¢. If the target crosses the book (a buy at or above the best ask), the tag adds· fills now. - Release on a row: send one reprice (the backend cancels and re-places; Polymarket has
no amend). Chips show the
movingstate until both steps resolve. - Release outside the ladder, on the original row, or press Esc: no action.
- The ladder does not auto-scroll during a drag. Near the edge, the user scrolls with the wheel while holding the chip.
Board, watchlist, portfolio, orders
Deferred list changes
Principle 3 applied to lists. Row values update live. Row structure (insert, remove, reorder) is deferred while the list is in use:- In use = the pointer is over the list (desktop), or a finger touched it in the last 1.5 s (mobile).
- A row that should be removed stays in place as a ghost: stale-styled, struck through, not interactive.
- New rows and re-sorted rows are held. A 24 px pill at the top of the list reads
3 updates. Clicking it applies them. - When the list stops being in use, pending changes apply in one frame, with no layout animation.
- Rows inserted above the viewport never shift visible rows: lists use scroll anchoring
(
overflow-anchor: auto) with stable row keys.
Stake selection
- The active stake is a pUSD amount picked from the presets in settings (1–6 values). It is global for the session and survives route changes; it resets to the first preset on reload.
- Where it shows: the top-bar segmented control (desktop), the stake bar (mobile), the ladder footer, and every cursor tag.
- How to change it: click a segment, or in a focused trading panel (armed) press
1–6to pick a preset, or-/=to step to the previous / next one (stopping at the ends). - How it becomes an order:
- Buy at price p: size = stake ÷ p shares.
- Sell from a Sell button or ask cell: size = min(stake ÷ p, position).
- Exit (portfolio, dock and the ladder header), Shift+click sell: size = whole position.
- Hedge: size comes from the backend’s hedge calculation.
- In every case the backend rounds to the market’s size tick and minimum size in
engine/risk. If the result is below the minimum, the order is rejected with the minimum stated.
- The cursor tag and the mobile stake bar show the estimated size before the click
(
$25 → 59.52 sh).
Hotkey map
Hotkeys register only throughlib/hotkeys. There are three classes:
- Navigation: always on, in Safe or Armed. They never touch orders.
- Global safety: Cancel all. Works everywhere, including inside text inputs, in Safe, and with any panel focused.
- Trading: only in the focused panel: the last one clicked, or reached with
F6. Ignored while focus is in a text input. Keys that place, move or cancel orders work only when Armed. Keys that only move a cursor or a selection, or open something (the arrows,Space,Enterwhere it opens a ladder or a market,Alt+Enter,Esc), send nothing and also work in Safe; they are marked “(Safe too)” below.
Enter and Space activate that control as usual.
Moving a panel’s cursor with the arrows moves keyboard focus from the control to the panel,
so the next Enter or Space acts on the cursor.
Focused panel indicator. The focused panel shows a 2 px --ring line along its top edge,
inside the panel, so nothing moves. It shows only when it tells the user something:
- after
F6/Shift+F6(andQ, which moves focus to the slip), in Safe or Armed, so keyboard users see where focus went; the next click hides it again unless the rule below holds; - while Armed, when more than one visible panel could take the trading keys: two or more panels with trading keys (the board beside a docked ladder or the slip), or a focused panel without them (the collapsed bottom dock, or its Fills tab) beside one that has them.
:focus-visible ring.
Mod = Ctrl on Windows/Linux, ⌘ on macOS. Single-letter keys have no modifier.
Navigation (always on)
Global safety
Trading (Armed, focused panel)
Any trading panel:
Ladder (docked, in market view, or the mobile sheet with a hardware keyboard):
While the keyboard cursor is on the ladder, it counts as in use: it does not re-centre by
itself until
Esc clears the cursor (Space and the edge pills still re-centre). A tick
size change rebuilds the grid and clears the cursor.
Board, watchlist and the market view’s outcome list (each outcome is one line):
Each outcome sits on its own line with its Sell and Buy buttons, and
B / S already pick
the side, so ← / → have no outcomes to move between within a line; they move between
games instead. In the market view the outcome list is its own panel: click it (or F6) for
these keys; the rest of the view keeps the ladder’s keys.
Portfolio and the dock’s Positions tab:
Orders and the dock’s Orders tab:
In the dock, the keys follow the open tab; the collapsed dock and the Fills tab have none.
The cursor and selection last while the tab stays open.
Combo slip (focused):
While combo mode is on,
B and S do nothing on the board, watchlist and market-view
outcome list, because their price buttons are leg toggles. The ladder keeps its keys.
When a trading hotkey is pressed in Safe, nothing is sent and the Safe/Armed switch
shows its one-off ring highlight. Cancel hotkeys (C, Delete) are in the trading class
too; in Safe, the Cancel buttons and right-click still cancel.
Sports delay window
On in-play sports markets the venue holds a marketable order for a few seconds before matching it (statusdelayed), and it cannot be cancelled during that time.
- Before the click: a market with an in-play delay shows a delay chip in the ladder
header and on the board row:
3s delay(muted text, no icon). Users know a crossing order will wait. - On the click: the order chip goes
sending, thendelayedwith a countdown ring and seconds, at its price level. The board row’s pending-order dot shows the warning colour. - During the delay:
- Right-click, long-press, drag,
[/], stepper and Delete on that order do nothing. The tooltip or toast readsIn delay window (2s). - Position, P&L and liquidation value do not change: they change on fills only.
- Other orders in the market can still be placed and cancelled.
- Right-click, long-press, drag,
- Cancel all or cancel-market during a delay: every other order is cancelled; each
delayed order gets a queued cancel (
cancel_pending), sent when its window ends, and the result toast says so:Cancelled 5 · 2 in delay window. - When the delay ends: the chip moves to
filled,partially filled(the rest rests asplaced), orplaced, as reported by the user feed. - If the backend does not send an end time, the chip reads
Delayedwithout a ring.
Pre-game wipe
The venue cancels resting orders on sports markets when the game starts.- Before the start, while you have resting orders in that game: the ladder status line
and the board row show
Starts in 8:04 · resting orders cancel at start(warning fill in the last 10 minutes, muted before). On the board it is theWipechip, with that text in its tooltip; a phone card, which has no tooltip, saysOrders cancel at start. - One start format. Every view writes a start the same way, from the same clock:
in 1:41(m:ss) in the last 10 minutes,in 12minside the hour (minutes rounded down from the seconds left, soin 10mis followed byin 10:00),in 3hunder 24 hours (lib/format’sformatWhen), then3 Oct 14:47, with the exact local time in the tooltip. The board clock, score badges, the ladder strip, the market’s Rules tab and the palette all show it, and tick every second in the last 10 minutes. - At the start, the
game_start_ordersnotification:Orders cancelled at start/3 orders · Lakers v Celtics(notifications.md), sent once every order open at the start has ended, at most 30 s after it. It lists only orders the venue reported cancelled without a cancel from this app, each for its unmatched size. Its toast carriesOpen;Re-postlives on the ladder status line and the board row, which read the wiped orders from the notification’sorders. The ladder status line shows3 orders cancelled at start · Re-postfor 5 minutes or until dismissed; an order the venue left open is not offered. - Re-post is one click and needs Armed. It sends the same side, price and unmatched size
for each wiped order as new orders, each through risk; each rejected one gets its own
reject toast (orders now outside the price band, for example). Re-posted and dismissed
orders are not offered again on any device: the server keeps each order’s
handled_at, set by the placement with the order’sclient_order_idor by Dismiss, and sends the change to every session. Thatclient_order_idis minted by the server with the list, so a second Re-post from any tab or device returns the first placement instead of a second order.
Error and reject feedback
Messages are factual: what happened and the numbers, no advice (CLAUDE.md).
Rules:
- Never auto-retry an order or reprice from the UI. After a failure, the user clicks again. The UI never resends on its own, and neither does the backend (CLAUDE.md).
- The reject toast always names the order (side, outcome, price), because the user may have clicked several.
- Rejected and failed intents are listed in the Orders screen’s
Rejectedtab for the session.
Click budgets
PLAN §4.1 budgets, with the design path that meets each. “Armed” is a precondition (one click per session on the switch) and is not counted.
Combo budgets (build and buy in 6, exit in 2) are in
combos.md.
Mobile ladder-sheet repricing costs one extra tap because a tap on a price level has to
place orders. The Orders tab meets the budget exactly (see
open question 13).