fix: /trade Market slippage chips wrap so 0.5% sits above 1% / 5% #528

Closed
opened 2026-08-15 21:45:46 +00:00 by PlasticDigits · 14 comments
PlasticDigits commented 2026-08-15 21:45:46 +00:00 (Migrated from gitlab.com)

Summary

On /trade Market, the 0.5% Slippage protection chip sits on the same row as the label while 1% and 5% wrap to the next row. The three presets look staggered instead of one aligned group.

This is one product surface: retail slippage preset chips (SLIPPAGE_TOLERANCE_PRESETS_PERCENT = [0.5, 1.0, 5.0]). Bundle Trade Market (the reported wrap) and the same three chips on Swap Settings so they cannot orphan the first preset. Do not split “Trade wrap” vs “Swap group” vs “shared helper” into separate issues.

Related: #497 (default 5% + shared presets), #417 (TRADE_SLIPPAGE_PRESET_CLASS ~44px touch target), #412 (retail Slippage protection label), #488 (tab-glass*), #489 (copy), #527 (ticket footer must not cover these chips).

Evidence

Phone-width /trade Market ticket (Buy {base}, disconnected). Label Slippage protection: + 0.5% on the first line; 1% and 5% on the line below. Copy Taker swap at 5% slippage protection confirms the store default is still 5% — this is layout only.

trade-market-slippage-0.5-misaligned

Current codebase

/trade Market tab renders TradeMarketOrderPanel. The chip row is a single flex flex-wrap whose children are the label and the three preset buttons:

<div className="flex flex-wrap gap-2 text-[10px]">
  <span style={{ color: 'var(--ink-dim)' }}>{SLIPPAGE_PROTECTION_LABEL}:</span>
  {SLIPPAGE_TOLERANCE_PRESETS_PERCENT.map((v) => (
    <button
      type="button"
      className={`${TRADE_SLIPPAGE_PRESET_CLASS} ${slippageTolerance === v ? 'tab-glass-active' : 'tab-glass-inactive'}`}
      data-testid={`trade-market-slippage-preset-${v}`}
      onClick={() => { sounds.playButtonPress(); setSlippageTolerance(v) }}
    >
      {v}%
    </button>
  ))}
</div>
Layer Behavior today
Why 0.5% sits high On a narrow ticket (phone stack, tablet ticket column, desktop after a tight Panel drag), flex-wrap fits Slippage protection: + the first chip, then wraps 1% / 5%. The first chip is vertically aligned with the label row, not with its siblings.
Chip chrome TRADE_SLIPPAGE_PRESET_CLASS = tab-glass min-h-11 px-3 py-2 text-sm (#417). Tall 44px chips + a text-[10px] label in one wrap list makes the orphan obvious. Default flex align-items: stretch does not keep a shared baseline once wrap occurs.
Store / on-chain useDexStore.slippageTolerance ← DEFAULT_SLIPPAGE_TOLERANCE_PERCENT (5). Click calls setSlippageTolerance(v) (clamped 0.01–50). Market submit maps max_spread = (slippageTolerance / 100).toString() (5 → "0.05"). Lead copy interpolates the same value: Taker swap at {slippageTolerance}% slippage protection.
Swap Settings SwapPage.tsx already puts the label on its own line (label-glass), then flex flex-wrap of 0.5 / 1 / 5 + a flex-1 Custom input. Narrow widths can still wrap mid-preset (e.g. 0.5%+1% on row 1, 5%+Custom on row 2). Same three values; keep them one group if a helper is extracted.
Pool withdraw PoolPage.tsx uses a different set (0.5 / 1.0 / 2.0) and already stacks label above flex gap-2 chips. Out of scope unless a shared helper is reused without changing those values.
Tests data-testid={trade-market-slippage-preset-${v}} exists. TradeMarketOrderPanel.submitSnapshot.test.tsx covers submit/max_spread snapshots, not chip geometry or wrap. No Playwright assert that the three Market chips share a row / baseline.

Playbook: skills/AGENTS_FRONTEND_DEFAULT_SLIPPAGE.md. Spec: docs/frontend.md § slippage protection. Tokens: docs/design-system.md (tab-glass* for slippage chips).

Why this is needed

  1. The three presets are one control. Users compare 0.5 / 1 / 5 as a segmented choice. Orphaning 0.5% next to the label reads as a broken control, not a wrap.
  2. Mis-tap risk on a money-adjacent setting. These chips set on-chain max_spread. A staggered hit area makes it easy to tap 0.5% when aiming for 1%, or to miss the group entirely. Wrong protection is a correctness / MEV-exposure issue, not only polish.
  3. Ticket column is often narrow. /trade phone stack, tablet chart|ticket, and a dragged-narrow desktop panel all hit the wrap. The screenshot is the default Market ticket, not an edge-case zoom.
  4. Swap Settings can orphan the same first chip when the Custom field shares the wrap list. Fix the grouping once.

Constraints / guardrails

  1. Layout only. Do not change preset values [0.5, 1.0, 5.0], default 5%, setSlippageTolerance clamp 0.01–50, or max_spread = percent / 100. Do not hard-code 0.5 as the product default (#497).
  2. Do not add a custom % input on Trade Market unless product already asked. Trade stays preset-only; Swap keeps Custom + range error + high-protection warn (HIGH_SLIPPAGE_PROTECTION_WARN_PERCENT = 5, warn only when strictly greater than 5%).
  3. Keep #417 touch targets. Trade chips stay TRADE_SLIPPAGE_PRESET_CLASS (min-h-11). Do not shrink to “fit the label row”.
  4. Label is not a chip. Slippage protection stays SLIPPAGE_PROTECTION_LABEL (#412). Do not put the label inside the wrap list with the buttons. Do not rename to “slippage tolerance” / max_spread in retail UI (#489).
  5. Chip group stays one unit. Prefer label on its own line (Swap / Pool pattern) + a role="group" row for the three buttons. If the ticket is too narrow for three min-h-11 chips, wrap as a group or use equal grid-cols-3 — never leave 0.5% on the label row.
  6. Shared baseline. Chips share the same top and bottom edge (≤ 2px). Use items-center (or a 3-column grid) on the chip group, not align-items: stretch across label + buttons.
  7. Design system. tab-glass / tab-glass-active / tab-glass-inactive. Tokens --ink, --ink-dim. No *-neo classes (#488). Light + dark.
  8. Preserve testids trade-market-slippage-preset-0.5, trade-market-slippage-preset-1, trade-market-slippage-preset-5 (note: 1.0 stringifies as 1). Add a group testid (e.g. trade-market-slippage-presets) for geometry. Update every test if a testid changes.
  9. A11y. Associate the group (aria-label / aria-labelledby pointing at the visible label). Keyboard: Tab 0.5 → 1 → 5; :focus-visible on .tab-glass unchanged (#144). Selected chip stays tab-glass-active and matches useDexStore.slippageTolerance.
  10. Do not change submit / quote semantics. Hybrid route-solve, assertSubmitHybridAligned, pause / blacklist / gas gates, invert convert-on-submit (#524) stay as they are. Clicking a chip must still only update the store (and therefore the next quote / max_spread).
  11. #527 footer. Chips stay in the scroll body, not in the money-CTA footer. A docked CTA must not cover the chip row (elementFromPoint on a chip must hit that chip).
  12. Pool withdraw 2.0% set is out of scope unless a shared primitive is used without changing those values.
  13. Copy. Lead line stays Taker swap at {n}% slippage protection. Best pool/book split by default. + optional Docs. No new essay (#489).

Relevant files

File Role
frontend-dapp/src/components/trade/TradeMarketOrderPanel.tsx Reported wrap: label + presets in one flex-wrap
frontend-dapp/src/utils/tradeMoneyCta.ts TRADE_SLIPPAGE_PRESET_CLASS
frontend-dapp/src/utils/slippageProtectionCopy.ts Label, default 5%, presets, high-warn threshold
frontend-dapp/src/stores/dex.ts slippageTolerance / setSlippageTolerance
frontend-dapp/src/pages/SwapPage.tsx Settings presets + Custom in a wrap row
frontend-dapp/src/index.css .tab-glass*
frontend-dapp/src/components/trade/__tests__/TradeMarketOrderPanel.submitSnapshot.test.tsx Submit / max_spread — extend for chip selection, not geometry
frontend-dapp/src/pages/SwapPage.test.tsx Settings label + default
frontend-dapp/e2e/trade-page-responsive.spec.ts Viewport geometry — add chip alignment
skills/AGENTS_FRONTEND_DEFAULT_SLIPPAGE.md Invariants — add “presets are one aligned group”
  • Split label and chip group in TradeMarketOrderPanel (label above, chips below). Put the three buttons in role="group" with flex items-center gap-2 or grid grid-cols-3 gap-2 so they share one row and one baseline.
  • If three min-h-11 chips cannot fit the ticket width, keep them equal-width in a 3-column grid (they shrink together) rather than wrapping the first chip onto the label line.
  • Optional shared SlippageProtectionPresets (Trade: three chips; Swap: three chips + Custom sibling outside the chip group). Do not invent a fourth Trade preset.
  • Add data-testid="trade-market-slippage-presets" (and Swap equivalent if extracted) for bounding-box tests.
  • RTL: click each testid → store value + tab-glass-active on that chip only. Playwright: getBoundingClientRect — three chips same y (±2px) and 0.5% y is not the label’s y when the label is on the previous line; chip row top ≥ label bottom.
  • Update the #497 playbook with the grouping invariant. No contract / indexer / route-solve change.

Acceptance criteria

  • AC1 — One aligned chip row (Trade Market). On /trade Market, 0.5%, 1%, and 5% share the same horizontal axis (top and bottom edges within 2px). 0.5% is not on the label’s line.
  • AC2 — Label is separate. Visible Slippage protection (with or without trailing :) sits above or beside the group, never as a flex-wrap sibling that can take the first chip with it.
  • AC3 — Narrow ticket. Phone 390×844, tablet ~820×1180 ticket column, desktop 1280×720, and a dragged-narrow desktop ticket: AC1 still holds. If width is extreme, chips stay a 3-up equal grid (or wrap as a whole group) — no orphan 0.5%.
  • AC4 — Selection + store. Click 0.5 / 1 / 5 updates useDexStore.slippageTolerance to that number; only that chip is tab-glass-active; lead copy Taker swap at {n}% slippage protection matches. Fresh session still defaults to 5% active.
  • AC5 — On-chain mapping unchanged. Market / Swap submit still sends max_spread as (percent/100) string. Snapshot tests that assert "0.05" at default still pass. Selecting 0.5% → "0.005"; 1% → "0.01".
  • AC6 — Swap Settings grouping. Opening Swap Settings: the three presets stay one aligned group (same baseline). Custom input may wrap below the group; it must not insert itself between 0.5% and 1% or orphan 0.5%. Custom 0.01–50 + high-warn >5% unchanged.
  • AC7 — Themes + a11y. Light and dark. Group has an accessible name. Tab order 0.5 → 1 → 5. :focus-visible ring visible. Hit targets ≥ 44px on Trade (min-h-11).
  • AC8 — No overlay. With Market tab selected, elementFromPoint at each chip center hits that chip — not the money CTA, pair search, or a sibling chip.
  • AC9 — Docs / tests. Playbook notes the grouping invariant. RTL + Playwright cover selection and geometry. make verify-issue-<iid> exists.

Test plan (all paths)

Unit / RTL (make test-frontend / scoped Vitest)

  1. Trade Market: all three trade-market-slippage-preset-* render when a pair is selected (connected and disconnected).
  2. Fresh store: 5% has tab-glass-active; 0.5% and 1% have tab-glass-inactive.
  3. Click 0.5% → store 0.5, only that chip active; click 1% → 1; click 5% → 5. Lead copy updates.
  4. Submit snapshot: default still max_spread: "0.05"; after clicking 0.5%, next submit / quote path uses "0.005" (extend TradeMarketOrderPanel.submitSnapshot.test.tsx or a focused chip test — do not weaken existing default-5% cases).
  5. Swap Settings: presets still [0.5, 1, 5]; Custom sanitizes non-numeric; values >50 clamp to 50; <0.01 shows range error and does not persist an out-of-range store write beyond existing behavior; warn copy only when store > 5.
  6. Pause / blacklist / empty pair: chips still render and change the store (they are not the money CTA). Disabled submit unchanged.
  7. Invert / Buy vs Sell (#524): chip row still present; values unchanged.
  8. Sound hook: existing playButtonPress on click still called (do not drop).

Playwright (e2e/trade-page-responsive.spec.ts and/or a focused spec, Chromium)

Use bounding boxes, not screenshots-only. Open /trade, select Market, wait for trade-market-slippage-preset-0.5.

# Path Assert
P1 Phone 390×844, Market Three chips: abs(y0.5 − y1) ≤ 2 and abs(y1 − y5) ≤ 2. Label bottom ≤ chip row top + 4px (label not sharing the chip baseline as a wrap sibling).
P2 Tablet 820×1180 ticket column Same as P1.
P3 Desktop 1280×720 Same as P1.
P4 Desktop, drag ticket panel to ~280px Still P1 or equal 3-col grid; 0.5% x is not immediately after the label on a shorter row than 1%/5%.
P5 Click 0.5 → 1 → 5 tab-glass-active follows the click; no overlap of chip boxes (gap ≥ 0; centers distinct).
P6 elementFromPoint at each chip center Hits that button (A8).
P7 Swap / Settings open, 390×844 Three preset boxes share y (±2px). Custom input, if wrapped, is below the group, not between chips.
P8 Light + dark (theme toggle) P1 still holds; active chip contrast visible.
P9 Disconnected Connect Wallet Chips still aligned and clickable; CTA label unchanged.
P10 Market ↔ Limit tab Returning to Market preserves store selection and alignment.

Firefox/WebKit: if the suite runs them, P1 + P5 must pass (alignment must not be Chrome-only CSS).

Manual QA

  1. make dev → /trade Market on a live pair. Confirm 0.5 / 1 / 5 sit on one row under (or clearly grouped with) Slippage protection.
  2. Phone DevTools width and a squeezed desktop ticket. Repeat.
  3. Click each chip; confirm lead copy and (if quoting) min-receive / max-spread in the pre-submit summary.
  4. Swap Settings: presets stay a group; Custom still works; type 99 (clamp/warn) and 0 (range error).
  5. Light theme. Repeat 1–3.
  6. Keyboard-only: Tab through chips; Enter/Space selects; focus ring visible.

Test plan — attack, hack, and abuse vectors

Layout chrome on a control that sets on-chain max_spread. Treat mis-hit and value injection as integrity bugs.

Vector What to prove
A1 Mis-tap / wrong protection Chip boxes do not overlap. elementFromPoint at each center hits that chip. A tap meant for 1% must not activate 0.5% because it sits on the label row.
A2 Overlay clickjack Money CTA / sticky-or-footer chrome / pair listbox / wallet modal must not sit on the chips. elementFromPoint on a chip is the chip (#527, #181).
A3 Silent max_spread change Clicking a chip is the only UI path that changes Trade Market protection (no hover-set, no scroll-wheel on the group). Store value always matches the active chip (or Custom on Swap). Submit payload max_spread matches the store at click time.
A4 Out-of-range / injection (Swap Custom) Keep sanitizing to digits + one .. No innerHTML. Values below 0.01 or above 50 do not reach the chain as raw user text. Reject 1e9, 0x1, 1; DROP, unicode digits that parse as something else.
A5 Default downgrade Fresh session / new Zustand persist must not start at 0.5% just because that chip is first in the DOM. Default remains 5% (#497).
A6 Race with in-flight quote Rapid chip clicks while a route-solve is in flight: the submitted max_spread / hop min-returns match the store at confirm, not a stale chip. Existing assertSubmitHybridAligned / submit-aligned sim still gate the CTA.
A7 Double-bind / second control Do not mount a second set of Trade Market presets (e.g. one in the panel and one in a footer). One group, one store write.
A8 Keyboard spoof No tabindex ≥ 0 on the label. Disabled money CTA stays disabled when chips change nothing about pause/blacklist. Esc/modals unchanged.
A9 Focus / contrast spoof Light theme must not make inactive chips look selected or look like the label. Active = tab-glass-active only.
A10 XSS in label Label is the constant SLIPPAGE_PROTECTION_LABEL, not indexer/wallet text. Do not interpolate pair symbols into the chip row.

Verification criteria

Issue is done when all of the following are true:

  1. Repro is gone. Fresh /trade Market at phone and desktop ticket widths: screenshot comparable to the report shows 0.5 / 1 / 5 on one aligned row, not 0.5% beside the label.
  2. Automated geometry. Playwright P1–P6 pass on Chromium; RTL selection + default-5% + max_spread mapping still pass.
  3. Invariants preserved. #497 default 5% and preset set; #417 44px Trade chips; #412 label; no new Custom on Trade; Pool 2.0% set untouched.
  4. Docs match code. AGENTS_FRONTEND_DEFAULT_SLIPPAGE.md states the chip-group invariant.
  5. make verify-issue-<iid> exists and is green locally (unit + testid/preset greps; E2E when LocalTerra is up — do not skip with a false “no LocalTerra” without make has-localterra / make setup-cloud-localterra).
  6. Attack rows A1–A6 have an automated or scripted check (geometry + store/submit mapping + Custom sanitize tests). A4/A5 are existing or extended unit tests.
  7. No contract / indexer / route-solve change in the MR.

Mode

  • hybrid (Market tab taker swap)
  • v2 (pool-only path still uses the same store max_spread)

Severity

  • major UX / incorrect accounting display (staggered chips invite the wrong max_spread; visual defect on the default Market ticket)
## Summary On **`/trade` Market**, the **0.5%** Slippage protection chip sits on the **same row as the label** while **1%** and **5%** wrap to the next row. The three presets look staggered instead of one aligned group. This is one product surface: **retail slippage preset chips** (`SLIPPAGE_TOLERANCE_PRESETS_PERCENT` = `[0.5, 1.0, 5.0]`). Bundle Trade Market (the reported wrap) and the same three chips on Swap Settings so they cannot orphan the first preset. Do **not** split “Trade wrap” vs “Swap group” vs “shared helper” into separate issues. Related: [#497](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/497) (default 5% + shared presets), [#417](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/417) (`TRADE_SLIPPAGE_PRESET_CLASS` ~44px touch target), [#412](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/412) (retail **Slippage protection** label), [#488](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/488) (`tab-glass*`), [#489](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/489) (copy), [#527](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/527) (ticket footer must not cover these chips). ## Evidence Phone-width `/trade` Market ticket (Buy {base}, disconnected). Label **Slippage protection:** + **0.5%** on the first line; **1%** and **5%** on the line below. Copy **Taker swap at 5% slippage protection** confirms the store default is still 5% — this is layout only. ![trade-market-slippage-0.5-misaligned](/uploads/1790631c0e2d72f21e52a92e456a59f3/trade-market-slippage-0.5-misaligned.png) ## Current codebase `/trade` Market tab renders `TradeMarketOrderPanel`. The chip row is a **single** `flex flex-wrap` whose children are the label **and** the three preset buttons: ```tsx <div className="flex flex-wrap gap-2 text-[10px]"> <span style={{ color: 'var(--ink-dim)' }}>{SLIPPAGE_PROTECTION_LABEL}:</span> {SLIPPAGE_TOLERANCE_PRESETS_PERCENT.map((v) => ( <button type="button" className={`${TRADE_SLIPPAGE_PRESET_CLASS} ${slippageTolerance === v ? 'tab-glass-active' : 'tab-glass-inactive'}`} data-testid={`trade-market-slippage-preset-${v}`} onClick={() => { sounds.playButtonPress(); setSlippageTolerance(v) }} > {v}% </button> ))} </div> ``` | Layer | Behavior today | |-------|----------------| | Why 0.5% sits high | On a narrow ticket (phone stack, tablet ticket column, desktop after a tight `Panel` drag), `flex-wrap` fits **Slippage protection:** + the first chip, then wraps **1%** / **5%**. The first chip is vertically aligned with the label row, not with its siblings. | | Chip chrome | `TRADE_SLIPPAGE_PRESET_CLASS` = `tab-glass min-h-11 px-3 py-2 text-sm` ([#417](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/417)). Tall 44px chips + a `text-[10px]` label in one wrap list makes the orphan obvious. Default flex `align-items: stretch` does not keep a shared baseline once wrap occurs. | | Store / on-chain | `useDexStore.slippageTolerance` ← `DEFAULT_SLIPPAGE_TOLERANCE_PERCENT` (**5**). Click calls `setSlippageTolerance(v)` (clamped 0.01–50). Market submit maps `max_spread = (slippageTolerance / 100).toString()` (5 → `"0.05"`). Lead copy interpolates the same value: `Taker swap at {slippageTolerance}% slippage protection`. | | Swap Settings | [`SwapPage.tsx`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/pages/SwapPage.tsx) already puts the label on its own line (`label-glass`), then `flex flex-wrap` of **0.5 / 1 / 5** + a flex-1 **Custom** input. Narrow widths can still wrap mid-preset (e.g. 0.5%+1% on row 1, 5%+Custom on row 2). Same three values; keep them one group if a helper is extracted. | | Pool withdraw | [`PoolPage.tsx`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/pages/PoolPage.tsx) uses a **different** set (`0.5 / 1.0 / 2.0`) and already stacks label above `flex gap-2` chips. **Out of scope** unless a shared helper is reused without changing those values. | | Tests | `data-testid={`trade-market-slippage-preset-${v}`}` exists. `TradeMarketOrderPanel.submitSnapshot.test.tsx` covers submit/`max_spread` snapshots, **not** chip geometry or wrap. No Playwright assert that the three Market chips share a row / baseline. | Playbook: [`skills/AGENTS_FRONTEND_DEFAULT_SLIPPAGE.md`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/skills/AGENTS_FRONTEND_DEFAULT_SLIPPAGE.md). Spec: [`docs/frontend.md`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/docs/frontend.md) § slippage protection. Tokens: [`docs/design-system.md`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/docs/design-system.md) (`tab-glass*` for slippage chips). ## Why this is needed 1. **The three presets are one control.** Users compare 0.5 / 1 / 5 as a segmented choice. Orphaning 0.5% next to the label reads as a broken control, not a wrap. 2. **Mis-tap risk on a money-adjacent setting.** These chips set on-chain `max_spread`. A staggered hit area makes it easy to tap 0.5% when aiming for 1%, or to miss the group entirely. Wrong protection is a correctness / MEV-exposure issue, not only polish. 3. **Ticket column is often narrow.** `/trade` phone stack, tablet chart\|ticket, and a dragged-narrow desktop panel all hit the wrap. The screenshot is the default Market ticket, not an edge-case zoom. 4. **Swap Settings can orphan the same first chip** when the Custom field shares the wrap list. Fix the grouping once. ## Constraints / guardrails 1. **Layout only.** Do **not** change preset values `[0.5, 1.0, 5.0]`, default **5%**, `setSlippageTolerance` clamp `0.01`–`50`, or `max_spread = percent / 100`. Do not hard-code `0.5` as the product default ([#497](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/497)). 2. **Do not add a custom % input on Trade Market** unless product already asked. Trade stays preset-only; Swap keeps Custom + range error + high-protection warn (`HIGH_SLIPPAGE_PROTECTION_WARN_PERCENT = 5`, warn only when **strictly greater than** 5%). 3. **Keep [#417](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/417) touch targets.** Trade chips stay `TRADE_SLIPPAGE_PRESET_CLASS` (`min-h-11`). Do not shrink to “fit the label row”. 4. **Label is not a chip.** **Slippage protection** stays `SLIPPAGE_PROTECTION_LABEL` ([#412](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/412)). Do not put the label inside the wrap list with the buttons. Do not rename to “slippage tolerance” / `max_spread` in retail UI ([#489](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/489)). 5. **Chip group stays one unit.** Prefer label on its own line (Swap / Pool pattern) + a `role="group"` row for the three buttons. If the ticket is too narrow for three `min-h-11` chips, wrap **as a group** or use equal `grid-cols-3` — never leave 0.5% on the label row. 6. **Shared baseline.** Chips share the same top and bottom edge (≤ 2px). Use `items-center` (or a 3-column grid) on the **chip group**, not `align-items: stretch` across label + buttons. 7. **Design system.** `tab-glass` / `tab-glass-active` / `tab-glass-inactive`. Tokens `--ink`, `--ink-dim`. No `*-neo` classes ([#488](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/488)). Light + dark. 8. **Preserve testids** `trade-market-slippage-preset-0.5`, `trade-market-slippage-preset-1`, `trade-market-slippage-preset-5` (note: `1.0` stringifies as `1`). Add a group testid (e.g. `trade-market-slippage-presets`) for geometry. Update every test if a testid changes. 9. **A11y.** Associate the group (`aria-label` / `aria-labelledby` pointing at the visible label). Keyboard: Tab 0.5 → 1 → 5; `:focus-visible` on `.tab-glass` unchanged ([#144](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/144)). Selected chip stays `tab-glass-active` and matches `useDexStore.slippageTolerance`. 10. **Do not change submit / quote semantics.** Hybrid route-solve, `assertSubmitHybridAligned`, pause / blacklist / gas gates, invert convert-on-submit ([#524](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/524)) stay as they are. Clicking a chip must still only update the store (and therefore the next quote / `max_spread`). 11. **[#527](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/527) footer.** Chips stay in the scroll body, not in the money-CTA footer. A docked CTA must not cover the chip row (`elementFromPoint` on a chip must hit that chip). 12. **Pool withdraw 2.0% set** is out of scope unless a shared primitive is used without changing those values. 13. **Copy.** Lead line stays `Taker swap at {n}% slippage protection. Best pool/book split by default.` + optional **Docs**. No new essay ([#489](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/489)). ## Relevant files | File | Role | |------|------| | [`frontend-dapp/src/components/trade/TradeMarketOrderPanel.tsx`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/components/trade/TradeMarketOrderPanel.tsx) | Reported wrap: label + presets in one `flex-wrap` | | [`frontend-dapp/src/utils/tradeMoneyCta.ts`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/utils/tradeMoneyCta.ts) | `TRADE_SLIPPAGE_PRESET_CLASS` | | [`frontend-dapp/src/utils/slippageProtectionCopy.ts`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/utils/slippageProtectionCopy.ts) | Label, default 5%, presets, high-warn threshold | | [`frontend-dapp/src/stores/dex.ts`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/stores/dex.ts) | `slippageTolerance` / `setSlippageTolerance` | | [`frontend-dapp/src/pages/SwapPage.tsx`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/pages/SwapPage.tsx) | Settings presets + Custom in a wrap row | | [`frontend-dapp/src/index.css`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/index.css) | `.tab-glass*` | | [`frontend-dapp/src/components/trade/__tests__/TradeMarketOrderPanel.submitSnapshot.test.tsx`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/components/trade/__tests__/TradeMarketOrderPanel.submitSnapshot.test.tsx) | Submit / `max_spread` — extend for chip selection, not geometry | | [`frontend-dapp/src/pages/SwapPage.test.tsx`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/pages/SwapPage.test.tsx) | Settings label + default | | [`frontend-dapp/e2e/trade-page-responsive.spec.ts`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/e2e/trade-page-responsive.spec.ts) | Viewport geometry — add chip alignment | | [`skills/AGENTS_FRONTEND_DEFAULT_SLIPPAGE.md`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/skills/AGENTS_FRONTEND_DEFAULT_SLIPPAGE.md) | Invariants — add “presets are one aligned group” | ## Recommended direction - Split **label** and **chip group** in `TradeMarketOrderPanel` (label above, chips below). Put the three buttons in `role="group"` with `flex items-center gap-2` or `grid grid-cols-3 gap-2` so they share one row and one baseline. - If three `min-h-11` chips cannot fit the ticket width, keep them equal-width in a 3-column grid (they shrink together) rather than wrapping the first chip onto the label line. - Optional shared `SlippageProtectionPresets` (Trade: three chips; Swap: three chips + Custom sibling **outside** the chip group). Do not invent a fourth Trade preset. - Add `data-testid="trade-market-slippage-presets"` (and Swap equivalent if extracted) for bounding-box tests. - RTL: click each testid → store value + `tab-glass-active` on that chip only. Playwright: `getBoundingClientRect` — three chips same `y` (±2px) and 0.5% `y` is **not** the label’s `y` when the label is on the previous line; chip row `top` ≥ label `bottom`. - Update the #497 playbook with the grouping invariant. No contract / indexer / route-solve change. ## Acceptance criteria - [ ] **AC1 — One aligned chip row (Trade Market).** On `/trade` Market, **0.5%**, **1%**, and **5%** share the same horizontal axis (top and bottom edges within 2px). **0.5%** is not on the label’s line. - [ ] **AC2 — Label is separate.** Visible **Slippage protection** (with or without trailing `:`) sits above or beside the **group**, never as a `flex-wrap` sibling that can take the first chip with it. - [ ] **AC3 — Narrow ticket.** Phone 390×844, tablet ~820×1180 ticket column, desktop 1280×720, and a dragged-narrow desktop ticket: AC1 still holds. If width is extreme, chips stay a 3-up equal grid (or wrap as a whole group) — no orphan 0.5%. - [ ] **AC4 — Selection + store.** Click 0.5 / 1 / 5 updates `useDexStore.slippageTolerance` to that number; only that chip is `tab-glass-active`; lead copy `Taker swap at {n}% slippage protection` matches. Fresh session still defaults to **5%** active. - [ ] **AC5 — On-chain mapping unchanged.** Market / Swap submit still sends `max_spread` as `(percent/100)` string. Snapshot tests that assert `"0.05"` at default still pass. Selecting 0.5% → `"0.005"`; 1% → `"0.01"`. - [ ] **AC6 — Swap Settings grouping.** Opening Swap Settings: the three presets stay one aligned group (same baseline). Custom input may wrap **below** the group; it must not insert itself between 0.5% and 1% or orphan 0.5%. Custom 0.01–50 + high-warn >5% unchanged. - [ ] **AC7 — Themes + a11y.** Light and dark. Group has an accessible name. Tab order 0.5 → 1 → 5. `:focus-visible` ring visible. Hit targets ≥ 44px on Trade (`min-h-11`). - [ ] **AC8 — No overlay.** With Market tab selected, `elementFromPoint` at each chip center hits that chip — not the money CTA, pair search, or a sibling chip. - [ ] **AC9 — Docs / tests.** Playbook notes the grouping invariant. RTL + Playwright cover selection and geometry. `make verify-issue-<iid>` exists. ## Test plan (all paths) ### Unit / RTL (`make test-frontend` / scoped Vitest) 1. Trade Market: all three `trade-market-slippage-preset-*` render when a pair is selected (connected and disconnected). 2. Fresh store: **5%** has `tab-glass-active`; 0.5% and 1% have `tab-glass-inactive`. 3. Click 0.5% → store `0.5`, only that chip active; click 1% → `1`; click 5% → `5`. Lead copy updates. 4. Submit snapshot: default still `max_spread: "0.05"`; after clicking 0.5%, next submit / quote path uses `"0.005"` (extend `TradeMarketOrderPanel.submitSnapshot.test.tsx` or a focused chip test — do not weaken existing default-5% cases). 5. Swap Settings: presets still `[0.5, 1, 5]`; Custom sanitizes non-numeric; values `>50` clamp to 50; `<0.01` shows range error and does not persist an out-of-range store write beyond existing behavior; warn copy only when store `> 5`. 6. Pause / blacklist / empty pair: chips still render and change the store (they are not the money CTA). Disabled submit unchanged. 7. Invert / Buy vs Sell ([#524](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/524)): chip row still present; values unchanged. 8. Sound hook: existing `playButtonPress` on click still called (do not drop). ### Playwright (`e2e/trade-page-responsive.spec.ts` and/or a focused spec, Chromium) Use **bounding boxes**, not screenshots-only. Open `/trade`, select Market, wait for `trade-market-slippage-preset-0.5`. | # | Path | Assert | |---|------|--------| | P1 | Phone 390×844, Market | Three chips: `abs(y0.5 − y1) ≤ 2` and `abs(y1 − y5) ≤ 2`. Label `bottom` ≤ chip row `top` + 4px (label not sharing the chip baseline as a wrap sibling). | | P2 | Tablet 820×1180 ticket column | Same as P1. | | P3 | Desktop 1280×720 | Same as P1. | | P4 | Desktop, drag ticket panel to ~280px | Still P1 or equal 3-col grid; 0.5% `x` is not immediately after the label on a shorter row than 1%/5%. | | P5 | Click 0.5 → 1 → 5 | `tab-glass-active` follows the click; no overlap of chip boxes (gap ≥ 0; centers distinct). | | P6 | `elementFromPoint` at each chip center | Hits that button (A8). | | P7 | Swap `/` Settings open, 390×844 | Three preset boxes share `y` (±2px). Custom input, if wrapped, is below the group, not between chips. | | P8 | Light + dark (theme toggle) | P1 still holds; active chip contrast visible. | | P9 | Disconnected Connect Wallet | Chips still aligned and clickable; CTA label unchanged. | | P10 | Market ↔ Limit tab | Returning to Market preserves store selection and alignment. | Firefox/WebKit: if the suite runs them, P1 + P5 must pass (alignment must not be Chrome-only CSS). ### Manual QA 1. `make dev` → `/trade` Market on a live pair. Confirm 0.5 / 1 / 5 sit on one row under (or clearly grouped with) **Slippage protection**. 2. Phone DevTools width and a squeezed desktop ticket. Repeat. 3. Click each chip; confirm lead copy and (if quoting) min-receive / max-spread in the pre-submit summary. 4. Swap Settings: presets stay a group; Custom still works; type `99` (clamp/warn) and `0` (range error). 5. Light theme. Repeat 1–3. 6. Keyboard-only: Tab through chips; Enter/Space selects; focus ring visible. ## Test plan — attack, hack, and abuse vectors Layout chrome on a control that sets **on-chain `max_spread`**. Treat mis-hit and value injection as integrity bugs. | Vector | What to prove | |--------|----------------| | **A1 Mis-tap / wrong protection** | Chip boxes do not overlap. `elementFromPoint` at each center hits that chip. A tap meant for 1% must not activate 0.5% because it sits on the label row. | | **A2 Overlay clickjack** | Money CTA / sticky-or-footer chrome / pair listbox / wallet modal must not sit on the chips. `elementFromPoint` on a chip is the chip ([#527](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/527), [#181](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/181)). | | **A3 Silent `max_spread` change** | Clicking a chip is the only UI path that changes Trade Market protection (no hover-set, no scroll-wheel on the group). Store value always matches the active chip (or Custom on Swap). Submit payload `max_spread` matches the store at click time. | | **A4 Out-of-range / injection (Swap Custom)** | Keep sanitizing to digits + one `.`. No `innerHTML`. Values below 0.01 or above 50 do not reach the chain as raw user text. Reject `1e9`, `0x1`, `1; DROP`, unicode digits that parse as something else. | | **A5 Default downgrade** | Fresh session / new Zustand persist must **not** start at 0.5% just because that chip is first in the DOM. Default remains 5% ([#497](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/497)). | | **A6 Race with in-flight quote** | Rapid chip clicks while a route-solve is in flight: the **submitted** `max_spread` / hop min-returns match the store at confirm, not a stale chip. Existing `assertSubmitHybridAligned` / submit-aligned sim still gate the CTA. | | **A7 Double-bind / second control** | Do not mount a second set of Trade Market presets (e.g. one in the panel and one in a footer). One group, one store write. | | **A8 Keyboard spoof** | No `tabindex` ≥ 0 on the label. Disabled money CTA stays `disabled` when chips change nothing about pause/blacklist. Esc/modals unchanged. | | **A9 Focus / contrast spoof** | Light theme must not make inactive chips look selected or look like the label. Active = `tab-glass-active` only. | | **A10 XSS in label** | Label is the constant `SLIPPAGE_PROTECTION_LABEL`, not indexer/wallet text. Do not interpolate pair symbols into the chip row. | ## Verification criteria Issue is done when **all** of the following are true: 1. **Repro is gone.** Fresh `/trade` Market at phone and desktop ticket widths: screenshot comparable to the report shows 0.5 / 1 / 5 on one aligned row, not 0.5% beside the label. 2. **Automated geometry.** Playwright P1–P6 pass on Chromium; RTL selection + default-5% + `max_spread` mapping still pass. 3. **Invariants preserved.** [#497](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/497) default 5% and preset set; [#417](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/417) 44px Trade chips; [#412](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/412) label; no new Custom on Trade; Pool 2.0% set untouched. 4. **Docs match code.** `AGENTS_FRONTEND_DEFAULT_SLIPPAGE.md` states the chip-group invariant. 5. **`make verify-issue-<iid>`** exists and is green locally (unit + testid/preset greps; E2E when LocalTerra is up — do not skip with a false “no LocalTerra” without `make has-localterra` / `make setup-cloud-localterra`). 6. **Attack rows A1–A6** have an automated or scripted check (geometry + store/submit mapping + Custom sanitize tests). A4/A5 are existing or extended unit tests. 7. **No contract / indexer / route-solve change** in the MR. ## Mode - [x] hybrid (Market tab taker swap) - [x] v2 (pool-only path still uses the same store `max_spread`) ## Severity - [x] major UX / incorrect accounting display (staggered chips invite the wrong `max_spread`; visual defect on the default Market ticket)
PlasticDigits commented 2026-08-15 21:45:47 +00:00 (Migrated from gitlab.com)

marked as related to #497

marked as related to #497
PlasticDigits commented 2026-08-15 21:45:48 +00:00 (Migrated from gitlab.com)

marked as related to #417

marked as related to #417
PlasticDigits commented 2026-08-15 21:45:48 +00:00 (Migrated from gitlab.com)

marked as related to #412

marked as related to #412
PlasticDigits commented 2026-08-15 21:48:41 +00:00 (Migrated from gitlab.com)

mentioned in commit 1f254d4ef6

mentioned in commit 1f254d4ef65adb5e51537538e167c6fcf807eb9e
PlasticDigits commented 2026-08-15 21:57:35 +00:00 (Migrated from gitlab.com)

mentioned in issue #514

mentioned in issue #514
PlasticDigits commented 2026-08-16 08:33:45 +00:00 (Migrated from gitlab.com)

mentioned in merge request !1063

mentioned in merge request !1063
PlasticDigits commented 2026-08-16 08:33:57 +00:00 (Migrated from gitlab.com)

mentioned in merge request !1064

mentioned in merge request !1064
PlasticDigits commented 2026-08-16 08:44:22 +00:00 (Migrated from gitlab.com)

Merge sanity check for !1064 (before landing on main):

  • S528-1–S528-10 look implemented (label above role="group" 3-up grid; Swap Custom below; presets/default/max_spread unchanged).
  • After #527 footer docks, re-check that chips stay in the ticket scroll body and are not covered by the money CTA (P6 elementFromPoint).
  • Remote branch 528-fix-trade-market-slippage-chips-wrap-so-0-5-sits-above-1-5- is an empty placeholder at origin/main (no unique commits). Safe to delete after !1064 lands.
  • Not blocking merge. MR pipeline failed immediately (duration 0) — not treated as a code verdict.
Merge sanity check for !1064 (before landing on main): - S528-1–S528-10 look implemented (label above `role="group"` 3-up grid; Swap Custom below; presets/default/`max_spread` unchanged). - After #527 footer docks, re-check that chips stay in the ticket scroll body and are not covered by the money CTA (P6 `elementFromPoint`). - Remote branch `528-fix-trade-market-slippage-chips-wrap-so-0-5-sits-above-1-5-` is an empty placeholder at `origin/main` (no unique commits). Safe to delete after !1064 lands. - Not blocking merge. MR pipeline failed immediately (duration 0) — not treated as a code verdict.
PlasticDigits commented 2026-08-16 08:44:25 +00:00 (Migrated from gitlab.com)

mentioned in issue #527

mentioned in issue #527
PlasticDigits commented 2026-08-16 08:46:26 +00:00 (Migrated from gitlab.com)

mentioned in commit a879d25890

mentioned in commit a879d25890ac67a322646c6715befc0f948c32ac
PlasticDigits (Migrated from gitlab.com) closed this issue 2026-08-16 08:46:27 +00:00
PlasticDigits commented 2026-08-16 08:47:44 +00:00 (Migrated from gitlab.com)

mentioned in commit 9ee15f75ec

mentioned in commit 9ee15f75ec998f5d78b2f64863055f8888d781e6
PlasticDigits commented 2026-08-16 08:49:51 +00:00 (Migrated from gitlab.com)

mentioned in commit 9fd8b4eadf

mentioned in commit 9fd8b4eadfbdbefcedfb3bc198ddaa814bf0e07d
PlasticDigits commented 2026-08-22 11:02:34 +00:00 (Migrated from gitlab.com)

mentioned in issue #595

mentioned in issue #595
PlasticDigits commented 2026-08-28 05:28:47 +00:00 (Migrated from gitlab.com)

mentioned in issue #693

mentioned in issue #693
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
code/cl8y-dex-terraclassic#528
No description provided.