fix: /trade Place limit CTA floats mid-ticket on Chrome — dock money CTAs to panel bottom #527

Closed
opened 2026-08-15 13:18:33 +00:00 by PlasticDigits · 14 comments
PlasticDigits commented 2026-08-15 13:18:33 +00:00 (Migrated from gitlab.com)

Summary

On Chrome, the /trade order-ticket Place limit money CTA (data-testid="trade-limit-submit") floats mid-panel and sits on top of live form controls (Pay % chips above, Expiry No expiry / 24h / 7d below) instead of docking to the bottom of the ticket column.

This is one product surface: ticket money-CTA chrome. Bundle Limit + Market docking in this issue. Do not split “Chrome sticky bug” vs “align to bottom” vs “Market tab footer” into separate issues.

Related: #348 (CTA must stay visible without hunting), #500 (opaque sticky footer + guards in flow + scroll clearance), #482 (same opacity class on sticky header), #146 (desktop / tablet / phone workspace), #417 (money CTA sizing).

Evidence

Desktop /trade (Chrome). Reporter circled PLACE LIMIT floating between Balance / 25%–100% and Expiry chips. Chart + Collapse tape control visible on the left — desktop react-resizable-panels workspace, not /limits.

place-limit-chrome-float

Current codebase

/trade mounts a single TradeOrderTicket (desktop lg+ PanelGroup or sub-desktop grid — #178). The ticket card is flex flex-col h-full min-h-0. Header (Buy/Sell + wallet chip) is outside the scrollport. The body is:

.trade-order-ticket-scroll   /* flex-1 min-h-0 overflow-y-auto */
  pause / blacklist banners
  Limit | Market tabs
  Side (Buy/Sell + book heads)
  Limit tabpanel:
    price → Pay (% chips) → Receive → Expiry → Advanced → pre-submit
    .trade-limit-inline-guards     /* #500: banners in normal flow */
    .trade-limit-submit-sticky     /* position: sticky; bottom: 0 */
      Place limit | Update price | Connect Wallet
    My open limits
  Market tabpanel:
    TradeMarketOrderPanel (in-flow Market buy/sell — not sticky)
Layer Behavior today
CSS .trade-limit-submit-sticky is position: sticky; bottom: 0; z-index: 10 with layered var(--panel-bg-strong), var(--bg-1) + backdrop blur (#500). Negative horizontal margins pull it to the scrollport edges. --trade-limit-sticky-clearance adds end padding / scroll-padding-bottom so Expiry / Advanced can scroll clear.
Why it floats position: sticky; bottom: 0 only pins when the sticky containing block is the intended scrollport and the element would otherwise leave that box. Nested flex (flex flex-1 flex-col) + overflow-y-auto + react-resizable-panels is a known Chromium sticky containing-block failure: the CTA sticks to a mid-ticket box (or overlays Pay/Receive/Expiry) instead of the visual bottom of the column. When content is shorter than the panel, sticky also does not act like margin-top: auto — it stays in flow, which reads as “floating”.
Limit vs Market Only Limit uses the sticky chrome. Market trade-market-submit is in-flow inside TradeMarketOrderPanel and can fall below the fold in a tall ticket (#348 regression risk if we only fix Limit).
/limits Standalone place card uses an in-flow btn-primary btn-cta — no sticky class. Out of scope unless a helper is shared.
Tests TradePage.test.tsx asserts DOM order (guards precede sticky). e2e/trade-page-responsive.spec.ts checks opacity, hit-test, and expiry clearing the footer — it does not assert the CTA’s bottom edge aligns with the ticket column on Chrome at scroll-top.

Playbook: skills/AGENTS_FRONTEND_TRADE_LIMIT_STICKY_CTA.md. Spec: docs/frontend.md § Trade page — limit ticket sticky CTA.

Why this is needed

  1. Money CTA covers the form. On Chrome the button sits on Pay % chips / Receive / Expiry. Users cannot set size or expiry without clicking through or around the overlay. That is a layout defect, not a “keep it pinned” preference.
  2. #500 did not fix Chromium containing-block. Opacity + clearance + moving guards out of the sticky chrome are still required — but sticky inside this flex scrollport is the wrong primitive if Chrome paints the footer mid-column.
  3. Mis-click risk. A mid-form overlay on the primary submit can fire Place limit when the user meant Expiry or a % chip (see attack plan).
  4. #348 still applies. The CTA must remain visible without scrolling the ticket. Docking to the panel bottom (true footer) satisfies both “always visible” and “not floating mid-form”.

Constraints / guardrails

  1. Do not use position: fixed. A viewport-fixed CTA escapes the ticket, covers the mobile tab bar / legal footer / wallet modal, and can sit over other routes. Keep the footer inside the ticket card.
  2. Keep #348 visibility. On desktop (lg+, ~1280×720) and tablet chart|ticket row, Place limit / Update price / Market buy|sell / Connect Wallet must be visible in the ticket column without scrolling the ticket body.
  3. Keep #500 invariants that are still correct:
    • Opaque footer chrome (var(--bg-1) under var(--panel-bg-strong) + blur). No missing tokens (--card) and no translucent mix that lets ADVANCED / fee rows bleed through.
    • Place / gas / crossing / update-price guards stay in normal document flow above the footer, never inside the CTA chrome (they must not cover Expiry / datetime-local).
    • Footer payload = CTA + broadcast pending link + tx result alerts only.
  4. Do not change place / submit semantics. No change to place_limit_order / allowance sequence, invert convert-on-submit (#524), crossing preflight, pause / blacklist disable, or gas gates. Layout only.
  5. Preserve data-testids: trade-limit-submit-sticky (or rename and update every test + skill + docs/frontend.md in the same MR), trade-limit-submit, trade-limit-update-price-submit, trade-limit-inline-guards, trade-order-ticket-scroll, trade-market-submit.
  6. One ticket mount. Do not remount a second TradeOrderTicket to “fix” layout (#178).
  7. Theme / design system. btn-primary / TRADE_MONEY_CTA_CLASS. Tokens --ink, --line, --bg-1, --panel-bg-strong. No *-neo classes (#488). Light + dark both opaque enough that form copy cannot be read through the footer.
  8. Copy. Button labels stay Place limit / Update price / Connect Wallet / Market buy|sell. No token0 / token1 / raw bid/ask in retail chrome (#489).
  9. /limits standalone is out of scope unless it shares a new footer helper. Do not add sticky/fixed chrome there.
  10. Resizable panels. Footer must survive horizontal ticket-width drag and vertical chart/tape resize. Do not let the footer capture PanelResizeHandle pointer events.
  11. z-index. Ticket footer must stay under wallet modal, risk/NFA modal, clickwrap, toasts, and portaled listboxes (#trade-pair-select — #181). Do not raise z-index to “win” against those surfaces.
  12. Keyboard / a11y. Tab order remains fields → inline guards → CTA. Footer must not trap focus. Disabled CTA stays disabled (no overlay that looks enabled).

Relevant files

File Role
frontend-dapp/src/components/trade/TradeOrderTicket.tsx Ticket shell, Limit tab DOM order, sticky wrapper
frontend-dapp/src/components/trade/TradeMarketOrderPanel.tsx Market money CTA (in-flow today)
frontend-dapp/src/index.css .trade-order-ticket-scroll, .trade-limit-submit-sticky, --trade-limit-sticky-clearance
frontend-dapp/src/pages/TradePage.tsx Desktop Panel vs sub-desktop ticket column heights
frontend-dapp/src/theme-dark.css / theme-light.css --bg-1 / --panel-bg-strong
frontend-dapp/src/pages/TradePage.test.tsx RTL: guards vs CTA DOM order
frontend-dapp/e2e/trade-page-responsive.spec.ts Playwright #348 / #500 geometry
docs/frontend.md § Trade page — limit ticket sticky CTA Invariants to rewrite if sticky is replaced
skills/AGENTS_FRONTEND_TRADE_LIMIT_STICKY_CTA.md Agent playbook (update name/rules if footer is no longer sticky)
skills/AGENTS_FRONTEND_TRADE_PAGE_LAYOUT.md Cross-link

Replace sticky-inside-scroll with a true ticket footer (flex/grid), which Chromium handles reliably:

.card-glass.flex.flex-col.h-full.min-h-0
  header (shrink-0)
  .trade-order-ticket-scroll (flex-1 min-h-0 overflow-y-auto)
    tabs, side, fields, expiry, advanced, summary
    .trade-limit-inline-guards
    My open limits
  .trade-ticket-submit-footer (shrink-0, opaque, border-top)
    Place limit | Update price | Market buy/sell | Connect Wallet
    pending link + tx alerts
  • Limit and Market share the same footer slot (swap label/disabled with the active tab). Do not leave Market as a second in-flow button that can sit under the Limit footer.
  • Prefer flex + shrink-0 over position: sticky / absolute / fixed. If a short form leaves empty space, the footer still sits on the bottom edge of the ticket column.
  • Keep scroll clearance only if any in-flow content can still sit under the footer; with a sibling footer, extra --trade-limit-sticky-clearance padding is usually unnecessary — drop or retune it so you do not get a large dead gap above My open limits.
  • Update docs/frontend.md + the #500 playbook in the same MR: “sticky CTA” becomes “ticket footer CTA”; keep opacity / guards-in-flow / visibility invariants.

Acceptance criteria

  • AC1 — Docked, not floating (Chrome). On Chromium (Chrome, and Edge/Brave if locally available), at ticket scroll-top, the Limit money CTA’s bottom edge aligns with the ticket card’s bottom (≤ 8px). It must not sit between Pay % chips and Expiry, and must not overlap Pay / Receive / Expiry / Advanced.
  • AC2 — Same dock for Market. With the Market tab selected, Market buy/sell (or Connect Wallet) uses the same footer slot and the same bottom alignment. No second in-flow Market CTA left mid-form.
  • AC3 — #348 visibility. Desktop 1280×720: CTA fully visible in the ticket column without scrolling the ticket. Tablet ~820×1180 chart|ticket row: same.
  • AC4 — #500 opacity + guards. Footer background is not fully transparent. trade-limit-inline-guards is not a descendant of the footer. After scrolling Expiry / datetime-local into view in the ticket body, those controls sit above the footer and remain clickable / typeable.
  • AC5 — Scroll + short content. Tall form (Advanced open + My limits): body scrolls; footer stays docked; last fields can reach clear of the footer. Short form (empty amounts): footer still at panel bottom — no mid-column float.
  • AC6 — Modes. Place limit, Update price (book Edit, price-only), Connect Wallet (disconnected), disabled (pause / blacklist / crossing / gas / empty pair). Labels and disabled unchanged.
  • AC7 — Viewports + themes. Phone stack (#146), tablet, desktop; light and dark. Phone may keep the CTA in normal flow under the form (no mid-form overlay). Resize ticket width and chart/tape height: footer stays inside the ticket.
  • AC8 — Docs / tests. docs/frontend.md + AGENTS_FRONTEND_TRADE_LIMIT_STICKY_CTA.md match the new primitive. RTL + Playwright assert bottom alignment (not only opacity / DOM order). make verify-issue-<iid> covers unit + geometry grep/E2E hooks.

Test plan (all paths)

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

  1. Guards still precede the footer in DOM; guards are not inside the footer (TradePage.test.tsx #500 case — update selectors if the class name changes).
  2. Limit tab: trade-limit-submit is inside the footer wrapper; Market tab: trade-market-submit is inside the same footer wrapper (or documented equivalent testid).
  3. Disconnected → Connect Wallet; connected place → Place limit; price-only edit → Update price; pause/blacklist/crossing/gas → button disabled + guard text still above the footer.
  4. Invert (#524) + side flip: footer still present; no duplicate CTAs.
  5. Light/dark: computed footer background is not transparent / rgba(0,0,0,0) without a background-image underlay.

Playwright (e2e/trade-page-responsive.spec.ts, Chromium project)

Use bounding boxes, not screenshots-only:

# Path Assert
P1 Desktop 1280×720, Limit, scroll-top submit.bottom ≈ ticketCard.bottom (≤ 8px). submit does not intersect Pay % chips, Receive, or Expiry chips.
P2 Same, scroll ticket to end Footer still docked; My open limits is above the footer (or in the scroll body, fully reachable).
P3 Desktop, Market tab trade-market-submit same bottom alignment; no leftover Limit button visible mid-form.
P4 Tablet 820×1180 Chart left of ticket; CTA docked in trade-sub-lg-ticket-col.
P5 Phone 390×844 Book → ticket → chart stack; CTA not overlaying Expiry; no position: fixed covering the bottom nav.
P6 Expiry datetime-local scrollIntoView Input box bottom ≤ footer top + 2px; elementFromPoint on the input does not hit the footer.
P7 Advanced open + long pre-submit Fields scroll; footer does not cover the date input when that input is scrolled into view.
P8 Disconnected / paused / blacklist Footer label + disabled state; guards visible above footer.
P9 Book Edit → Update price Footer label Update price; still docked.
P10 Resize ticket panel (drag handle) Footer width tracks ticket; no overlap onto chart / book.
P11 Theme toggle Dark + light: footer opaque; form text not readable through the button.

Firefox/WebKit: if the suite runs them, P1 + P6 must pass there too (docking must not be Chrome-only CSS).

Manual QA (Chrome)

  1. make dev → /trade on a live pair. Confirm Place limit sits on the bottom edge of the right-hand ticket, not mid-form.
  2. Toggle Limit ↔ Market. One footer, correct label.
  3. Open Advanced, pick 24h / 7d, type a datetime. No click-through on the CTA.
  4. Collapse/expand tape; drag chart/ticket split. Footer stays in the ticket.
  5. Light theme. Repeat 1–3.
  6. Phone DevTools width: CTA below the form, not over the bottom nav.

Test plan — attack, hack, and abuse vectors

This is layout chrome, not a new signing path. Still treat the overlay as a click / focus integrity bug.

Vector What to prove
A1 Overlay clickjack At scroll-top, elementFromPoint on Expiry chips, % chips, price, and Pay amount hits those controls — not trade-limit-submit. A mid-form overlay must not silently submit.
A2 Accidental place Disabled CTA (empty amount, crossing, pause, blacklist) stays disabled. Footer chrome must not use a visible “enabled” layer over a disabled button.
A3 Double-submit Rapid click / Enter on the docked CTA still goes through existing useTerraBroadcastMutation pending/isPending disable. Do not add a second submit node that can fire in parallel.
A4 z-index escape Open wallet modal, pair search listbox, risk/NFA, clickwrap. Footer must not paint above those or steal their clicks.
A5 position: fixed / portal footer Forbidden. CTA must remain a descendant of the ticket card (trade-order-ticket-scroll’s sibling inside the card). A fixed node can cover other routes or the extension wallet prompt.
A6 Pointer capture vs resize Dragging PanelResizeHandle must not be captured by the footer (pointer-events on the footer must not extend outside the ticket).
A7 Focus trap / keyboard spoof Tab from Expiry reaches guards then CTA. Esc/overlay behavior of existing modals unchanged. No tabindex ≥ 0 on decorative footer chrome.
A8 Hidden guard Crossing / gas / escrow banners must remain readable. An opaque footer must not cover a blocking reason such that the user believes Place is safe.
A9 XSS / HTML in footer Footer still renders React text nodes (tx error Error.message via existing TxResultAlert). Do not innerHTML indexer or wallet strings into the footer.
A10 Theme / contrast spoof Light theme must not make the footer look like a system/browser bar. Keep it visually inside the ticket (border-top + card radius).

Verification criteria

Issue is done when all of the following are true:

  1. Chrome repro is gone. Fresh Chrome on /trade (desktop + tablet): Place limit / Market CTA is flush with the ticket bottom; screenshot comparable to the report no longer shows a mid-form float.
  2. Automated geometry. Playwright P1–P6 pass on the Chromium project; RTL guard/footer order still passes.
  3. Invariants preserved. #348 visibility, #500 opacity + guards-in-flow, #178 single ticket, #524 convert-on-submit, pause/blacklist disable.
  4. Docs match code. docs/frontend.md section and AGENTS_FRONTEND_TRADE_LIMIT_STICKY_CTA.md describe the footer primitive (not “sticky bottom: 0” if that is gone).
  5. make verify-issue-<iid> exists and is green locally (unit + docs/testid greps; E2E invoked when the LocalTerra stack is up — do not skip with a false “no LocalTerra” without make has-localterra / make setup-cloud-localterra).
  6. Attack rows A1–A8 have an automated or scripted check (Playwright elementFromPoint + disabled + modal z-index is enough; A9 is a grep / existing alert component).
  7. No contract / indexer / route-solve change in the MR.

Mode

  • limit order
  • hybrid (Market tab shares the same ticket footer)

Severity

  • major UX (money CTA overlays form controls on Chrome; mis-click / unusable Expiry)
## Summary On **Chrome**, the `/trade` order-ticket **Place limit** money CTA (`data-testid="trade-limit-submit"`) **floats mid-panel** and sits on top of live form controls (Pay % chips above, Expiry **No expiry / 24h / 7d** below) instead of docking to the **bottom of the ticket column**. This is one product surface: **ticket money-CTA chrome**. Bundle Limit + Market docking in this issue. Do **not** split “Chrome sticky bug” vs “align to bottom” vs “Market tab footer” into separate issues. Related: [#348](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/348) (CTA must stay visible without hunting), [#500](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/500) (opaque sticky footer + guards in flow + scroll clearance), [#482](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/482) (same opacity class on sticky header), [#146](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/146) (desktop / tablet / phone workspace), [#417](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/417) (money CTA sizing). ## Evidence Desktop `/trade` (Chrome). Reporter circled **PLACE LIMIT** floating between **Balance / 25%–100%** and **Expiry** chips. Chart + **Collapse** tape control visible on the left — desktop `react-resizable-panels` workspace, not `/limits`. ![place-limit-chrome-float](/uploads/c13859a9ae00cbd0d1b77ec540187c1f/place-limit-chrome-float.jpg) ## Current codebase `/trade` mounts a single `TradeOrderTicket` (desktop `lg+` `PanelGroup` or sub-desktop grid — [#178](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/178)). The ticket card is `flex flex-col h-full min-h-0`. Header (Buy/Sell + wallet chip) is outside the scrollport. The body is: ```text .trade-order-ticket-scroll /* flex-1 min-h-0 overflow-y-auto */ pause / blacklist banners Limit | Market tabs Side (Buy/Sell + book heads) Limit tabpanel: price → Pay (% chips) → Receive → Expiry → Advanced → pre-submit .trade-limit-inline-guards /* #500: banners in normal flow */ .trade-limit-submit-sticky /* position: sticky; bottom: 0 */ Place limit | Update price | Connect Wallet My open limits Market tabpanel: TradeMarketOrderPanel (in-flow Market buy/sell — not sticky) ``` | Layer | Behavior today | |-------|----------------| | CSS | `.trade-limit-submit-sticky` is `position: sticky; bottom: 0; z-index: 10` with layered `var(--panel-bg-strong), var(--bg-1)` + backdrop blur ([#500](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/500)). Negative horizontal margins pull it to the scrollport edges. `--trade-limit-sticky-clearance` adds end padding / `scroll-padding-bottom` so Expiry / Advanced can scroll clear. | | Why it floats | `position: sticky; bottom: 0` only pins when the sticky containing block is the intended scrollport **and** the element would otherwise leave that box. Nested flex (`flex flex-1 flex-col`) + `overflow-y-auto` + `react-resizable-panels` is a known Chromium sticky containing-block failure: the CTA sticks to a **mid-ticket** box (or overlays Pay/Receive/Expiry) instead of the visual bottom of the column. When content is shorter than the panel, sticky also does **not** act like `margin-top: auto` — it stays in flow, which reads as “floating”. | | Limit vs Market | Only Limit uses the sticky chrome. Market `trade-market-submit` is in-flow inside `TradeMarketOrderPanel` and can fall below the fold in a tall ticket ([#348](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/348) regression risk if we only fix Limit). | | `/limits` | Standalone place card uses an in-flow `btn-primary btn-cta` — **no** sticky class. Out of scope unless a helper is shared. | | Tests | `TradePage.test.tsx` asserts DOM order (guards precede sticky). `e2e/trade-page-responsive.spec.ts` checks opacity, hit-test, and expiry clearing the footer — it does **not** assert the CTA’s bottom edge aligns with the ticket column on Chrome at scroll-top. | Playbook: [`skills/AGENTS_FRONTEND_TRADE_LIMIT_STICKY_CTA.md`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/skills/AGENTS_FRONTEND_TRADE_LIMIT_STICKY_CTA.md). Spec: [`docs/frontend.md`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/docs/frontend.md) § *Trade page — limit ticket sticky CTA*. ## Why this is needed 1. **Money CTA covers the form.** On Chrome the button sits on Pay % chips / Receive / Expiry. Users cannot set size or expiry without clicking through or around the overlay. That is a layout defect, not a “keep it pinned” preference. 2. **[#500](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/500) did not fix Chromium containing-block.** Opacity + clearance + moving guards out of the sticky chrome are still required — but `sticky` inside this flex scrollport is the wrong primitive if Chrome paints the footer mid-column. 3. **Mis-click risk.** A mid-form overlay on the primary submit can fire **Place limit** when the user meant Expiry or a % chip (see attack plan). 4. **[#348](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/348) still applies.** The CTA must remain visible without scrolling the ticket. Docking to the **panel bottom** (true footer) satisfies both “always visible” and “not floating mid-form”. ## Constraints / guardrails 1. **Do not use `position: fixed`.** A viewport-fixed CTA escapes the ticket, covers the mobile tab bar / legal footer / wallet modal, and can sit over other routes. Keep the footer **inside** the ticket card. 2. **Keep [#348](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/348) visibility.** On desktop (`lg+`, ~1280×720) and tablet chart\|ticket row, **Place limit** / **Update price** / **Market buy|sell** / **Connect Wallet** must be visible in the ticket column without scrolling the ticket body. 3. **Keep [#500](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/500) invariants that are still correct:** - Opaque footer chrome (`var(--bg-1)` under `var(--panel-bg-strong)` + blur). No missing tokens (`--card`) and no translucent mix that lets ADVANCED / fee rows bleed through. - Place / gas / crossing / update-price **guards stay in normal document flow above the footer**, never inside the CTA chrome (they must not cover Expiry / `datetime-local`). - Footer payload = CTA + broadcast pending link + tx result alerts only. 4. **Do not change place / submit semantics.** No change to `place_limit_order` / allowance sequence, invert convert-on-submit ([#524](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/524)), crossing preflight, pause / blacklist disable, or gas gates. Layout only. 5. **Preserve `data-testid`s:** `trade-limit-submit-sticky` (or rename **and** update every test + skill + `docs/frontend.md` in the same MR), `trade-limit-submit`, `trade-limit-update-price-submit`, `trade-limit-inline-guards`, `trade-order-ticket-scroll`, `trade-market-submit`. 6. **One ticket mount.** Do not remount a second `TradeOrderTicket` to “fix” layout ([#178](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/178)). 7. **Theme / design system.** `btn-primary` / `TRADE_MONEY_CTA_CLASS`. Tokens `--ink`, `--line`, `--bg-1`, `--panel-bg-strong`. No `*-neo` classes ([#488](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/488)). Light + dark both opaque enough that form copy cannot be read through the footer. 8. **Copy.** Button labels stay **Place limit** / **Update price** / **Connect Wallet** / **Market buy|sell**. No `token0` / `token1` / raw `bid`/`ask` in retail chrome ([#489](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/489)). 9. **`/limits` standalone** is out of scope unless it shares a new footer helper. Do not add sticky/fixed chrome there. 10. **Resizable panels.** Footer must survive horizontal ticket-width drag and vertical chart/tape resize. Do not let the footer capture `PanelResizeHandle` pointer events. 11. **z-index.** Ticket footer must stay **under** wallet modal, risk/NFA modal, clickwrap, toasts, and portaled listboxes (`#trade-pair-select` — [#181](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/181)). Do not raise `z-index` to “win” against those surfaces. 12. **Keyboard / a11y.** Tab order remains fields → inline guards → CTA. Footer must not trap focus. Disabled CTA stays `disabled` (no overlay that looks enabled). ## Relevant files | File | Role | |------|------| | [`frontend-dapp/src/components/trade/TradeOrderTicket.tsx`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/components/trade/TradeOrderTicket.tsx) | Ticket shell, Limit tab DOM order, sticky wrapper | | [`frontend-dapp/src/components/trade/TradeMarketOrderPanel.tsx`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/components/trade/TradeMarketOrderPanel.tsx) | Market money CTA (in-flow today) | | [`frontend-dapp/src/index.css`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/index.css) | `.trade-order-ticket-scroll`, `.trade-limit-submit-sticky`, `--trade-limit-sticky-clearance` | | [`frontend-dapp/src/pages/TradePage.tsx`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/pages/TradePage.tsx) | Desktop `Panel` vs sub-desktop ticket column heights | | [`frontend-dapp/src/theme-dark.css`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/theme-dark.css) / [`theme-light.css`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/theme-light.css) | `--bg-1` / `--panel-bg-strong` | | [`frontend-dapp/src/pages/TradePage.test.tsx`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/pages/TradePage.test.tsx) | RTL: guards vs CTA DOM order | | [`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) | Playwright #348 / #500 geometry | | [`docs/frontend.md`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/docs/frontend.md) § *Trade page — limit ticket sticky CTA* | Invariants to rewrite if sticky is replaced | | [`skills/AGENTS_FRONTEND_TRADE_LIMIT_STICKY_CTA.md`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/skills/AGENTS_FRONTEND_TRADE_LIMIT_STICKY_CTA.md) | Agent playbook (update name/rules if footer is no longer sticky) | | [`skills/AGENTS_FRONTEND_TRADE_PAGE_LAYOUT.md`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/skills/AGENTS_FRONTEND_TRADE_PAGE_LAYOUT.md) | Cross-link | ## Recommended direction Replace **sticky-inside-scroll** with a **true ticket footer** (flex/grid), which Chromium handles reliably: ```text .card-glass.flex.flex-col.h-full.min-h-0 header (shrink-0) .trade-order-ticket-scroll (flex-1 min-h-0 overflow-y-auto) tabs, side, fields, expiry, advanced, summary .trade-limit-inline-guards My open limits .trade-ticket-submit-footer (shrink-0, opaque, border-top) Place limit | Update price | Market buy/sell | Connect Wallet pending link + tx alerts ``` - Limit and Market share the same footer slot (swap label/disabled with the active tab). Do not leave Market as a second in-flow button that can sit under the Limit footer. - Prefer `flex` + `shrink-0` over `position: sticky` / `absolute` / `fixed`. If a short form leaves empty space, the footer still sits on the **bottom edge of the ticket column**. - Keep scroll clearance only if any in-flow content can still sit under the footer; with a sibling footer, extra `--trade-limit-sticky-clearance` padding is usually unnecessary — drop or retune it so you do not get a large dead gap above **My open limits**. - Update `docs/frontend.md` + the #500 playbook in the same MR: “sticky CTA” becomes “ticket footer CTA”; keep opacity / guards-in-flow / visibility invariants. ## Acceptance criteria - [ ] **AC1 — Docked, not floating (Chrome).** On Chromium (Chrome, and Edge/Brave if locally available), at ticket scroll-top, the Limit money CTA’s **bottom edge aligns with the ticket card’s bottom** (≤ 8px). It must **not** sit between Pay % chips and Expiry, and must **not** overlap Pay / Receive / Expiry / Advanced. - [ ] **AC2 — Same dock for Market.** With the Market tab selected, **Market buy/sell** (or Connect Wallet) uses the same footer slot and the same bottom alignment. No second in-flow Market CTA left mid-form. - [ ] **AC3 — [#348](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/348) visibility.** Desktop 1280×720: CTA fully visible in the ticket column without scrolling the ticket. Tablet ~820×1180 chart\|ticket row: same. - [ ] **AC4 — [#500](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/500) opacity + guards.** Footer background is not fully transparent. `trade-limit-inline-guards` is **not** a descendant of the footer. After scrolling Expiry / `datetime-local` into view in the ticket body, those controls sit **above** the footer and remain clickable / typeable. - [ ] **AC5 — Scroll + short content.** Tall form (Advanced open + My limits): body scrolls; footer stays docked; last fields can reach clear of the footer. Short form (empty amounts): footer still at panel bottom — no mid-column float. - [ ] **AC6 — Modes.** Place limit, Update price (book Edit, price-only), Connect Wallet (disconnected), disabled (pause / blacklist / crossing / gas / empty pair). Labels and `disabled` unchanged. - [ ] **AC7 — Viewports + themes.** Phone stack ([#146](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/146)), tablet, desktop; light and dark. Phone may keep the CTA in normal flow under the form (no mid-form overlay). Resize ticket width and chart/tape height: footer stays inside the ticket. - [ ] **AC8 — Docs / tests.** `docs/frontend.md` + `AGENTS_FRONTEND_TRADE_LIMIT_STICKY_CTA.md` match the new primitive. RTL + Playwright assert **bottom alignment** (not only opacity / DOM order). `make verify-issue-<iid>` covers unit + geometry grep/E2E hooks. ## Test plan (all paths) ### Unit / RTL (`make test-frontend` / scoped Vitest) 1. Guards still precede the footer in DOM; guards are not inside the footer (`TradePage.test.tsx` #500 case — update selectors if the class name changes). 2. Limit tab: `trade-limit-submit` is inside the footer wrapper; Market tab: `trade-market-submit` is inside the **same** footer wrapper (or documented equivalent testid). 3. Disconnected → **Connect Wallet**; connected place → **Place limit**; price-only edit → **Update price**; pause/blacklist/crossing/gas → button `disabled` + guard text still **above** the footer. 4. Invert ([#524](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/524)) + side flip: footer still present; no duplicate CTAs. 5. Light/dark: computed footer background is not `transparent` / `rgba(0,0,0,0)` without a background-image underlay. ### Playwright (`e2e/trade-page-responsive.spec.ts`, Chromium project) Use **bounding boxes**, not screenshots-only: | # | Path | Assert | |---|------|--------| | P1 | Desktop 1280×720, Limit, scroll-top | `submit.bottom` ≈ `ticketCard.bottom` (≤ 8px). `submit` does not intersect Pay % chips, Receive, or Expiry chips. | | P2 | Same, scroll ticket to end | Footer still docked; **My open limits** is above the footer (or in the scroll body, fully reachable). | | P3 | Desktop, Market tab | `trade-market-submit` same bottom alignment; no leftover Limit button visible mid-form. | | P4 | Tablet 820×1180 | Chart left of ticket; CTA docked in `trade-sub-lg-ticket-col`. | | P5 | Phone 390×844 | Book → ticket → chart stack; CTA not overlaying Expiry; no `position: fixed` covering the bottom nav. | | P6 | Expiry `datetime-local` `scrollIntoView` | Input box bottom ≤ footer top + 2px; `elementFromPoint` on the input does **not** hit the footer. | | P7 | Advanced open + long pre-submit | Fields scroll; footer does not cover the date input when that input is scrolled into view. | | P8 | Disconnected / paused / blacklist | Footer label + disabled state; guards visible above footer. | | P9 | Book **Edit** → Update price | Footer label **Update price**; still docked. | | P10 | Resize ticket panel (drag handle) | Footer width tracks ticket; no overlap onto chart / book. | | P11 | Theme toggle | Dark + light: footer opaque; form text not readable through the button. | Firefox/WebKit: if the suite runs them, P1 + P6 must pass there too (docking must not be Chrome-only CSS). ### Manual QA (Chrome) 1. `make dev` → `/trade` on a live pair. Confirm **Place limit** sits on the **bottom edge** of the right-hand ticket, not mid-form. 2. Toggle Limit ↔ Market. One footer, correct label. 3. Open Advanced, pick **24h** / **7d**, type a datetime. No click-through on the CTA. 4. Collapse/expand tape; drag chart/ticket split. Footer stays in the ticket. 5. Light theme. Repeat 1–3. 6. Phone DevTools width: CTA below the form, not over the bottom nav. ## Test plan — attack, hack, and abuse vectors This is layout chrome, not a new signing path. Still treat the overlay as a **click / focus integrity** bug. | Vector | What to prove | |--------|----------------| | **A1 Overlay clickjack** | At scroll-top, `elementFromPoint` on Expiry chips, % chips, price, and Pay amount hits those controls — **not** `trade-limit-submit`. A mid-form overlay must not silently submit. | | **A2 Accidental place** | Disabled CTA (empty amount, crossing, pause, blacklist) stays `disabled`. Footer chrome must not use a visible “enabled” layer over a disabled button. | | **A3 Double-submit** | Rapid click / Enter on the docked CTA still goes through existing `useTerraBroadcastMutation` pending/`isPending` disable. Do not add a second submit node that can fire in parallel. | | **A4 z-index escape** | Open wallet modal, pair search listbox, risk/NFA, clickwrap. Footer must not paint above those or steal their clicks. | | **A5 `position: fixed` / portal footer** | Forbidden. CTA must remain a descendant of the ticket card (`trade-order-ticket-scroll`’s sibling inside the card). A fixed node can cover other routes or the extension wallet prompt. | | **A6 Pointer capture vs resize** | Dragging `PanelResizeHandle` must not be captured by the footer (`pointer-events` on the footer must not extend outside the ticket). | | **A7 Focus trap / keyboard spoof** | Tab from Expiry reaches guards then CTA. Esc/overlay behavior of existing modals unchanged. No `tabindex` ≥ 0 on decorative footer chrome. | | **A8 Hidden guard** | Crossing / gas / escrow banners must remain readable. An opaque footer must not cover a blocking reason such that the user believes Place is safe. | | **A9 XSS / HTML in footer** | Footer still renders React text nodes (tx error `Error.message` via existing `TxResultAlert`). Do not `innerHTML` indexer or wallet strings into the footer. | | **A10 Theme / contrast spoof** | Light theme must not make the footer look like a system/browser bar. Keep it visually inside the ticket (border-top + card radius). | ## Verification criteria Issue is done when **all** of the following are true: 1. **Chrome repro is gone.** Fresh Chrome on `/trade` (desktop + tablet): Place limit / Market CTA is flush with the ticket bottom; screenshot comparable to the report no longer shows a mid-form float. 2. **Automated geometry.** Playwright P1–P6 pass on the Chromium project; RTL guard/footer order still passes. 3. **Invariants preserved.** [#348](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/348) visibility, [#500](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/500) opacity + guards-in-flow, [#178](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/178) single ticket, [#524](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/524) convert-on-submit, pause/blacklist disable. 4. **Docs match code.** `docs/frontend.md` section and `AGENTS_FRONTEND_TRADE_LIMIT_STICKY_CTA.md` describe the footer primitive (not “sticky `bottom: 0`” if that is gone). 5. **`make verify-issue-<iid>`** exists and is green locally (unit + docs/testid greps; E2E invoked when the LocalTerra stack is up — do not skip with a false “no LocalTerra” without `make has-localterra` / `make setup-cloud-localterra`). 6. **Attack rows A1–A8** have an automated or scripted check (Playwright `elementFromPoint` + disabled + modal z-index is enough; A9 is a grep / existing alert component). 7. **No contract / indexer / route-solve change** in the MR. ## Mode - [x] limit order - [x] hybrid (Market tab shares the same ticket footer) ## Severity - [x] major UX (money CTA overlays form controls on Chrome; mis-click / unusable Expiry)
PlasticDigits commented 2026-08-15 13:18:35 +00:00 (Migrated from gitlab.com)

marked as related to #500

marked as related to #500
PlasticDigits commented 2026-08-15 13:18:35 +00:00 (Migrated from gitlab.com)

marked as related to #348

marked as related to #348
PlasticDigits commented 2026-08-15 13:20:36 +00:00 (Migrated from gitlab.com)

mentioned in commit 869951daca

mentioned in commit 869951daca61d6d1c3766f4728eb395731fba0ca
PlasticDigits commented 2026-08-15 21:45:48 +00:00 (Migrated from gitlab.com)

mentioned in issue #528

mentioned in issue #528
PlasticDigits commented 2026-08-16 07:14:04 +00:00 (Migrated from gitlab.com)

mentioned in issue #530

mentioned in issue #530
PlasticDigits commented 2026-08-16 07:31:14 +00:00 (Migrated from gitlab.com)

mentioned in merge request !1059

mentioned in merge request !1059
PlasticDigits commented 2026-08-16 08:14:36 +00:00 (Migrated from gitlab.com)

mentioned in merge request !1062

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

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

  • T527-1–T527-10 look implemented (flex shrink-0 ticket footer; no position:fixed/portal; guards stay in flow; one ticket mount).
  • Market tab can flash an empty footer for one frame until useLayoutEffect publishes submit chrome.
  • Post-place “View order” / “Place another” stay in the scroll body; with many open limits users scroll past the money CTA to reach them.
  • Combined DOM with #528/#530: slippage chips + My open limits stay in trade-order-ticket-scroll; only Place/Update/Market CTA lives in trade-ticket-submit-footer.
  • Playwright geometry (P1–P11) was not re-run in this merge pass. MR pipeline failed immediately (duration 0) — not treated as a code verdict.
Merge sanity check for !1062 (before landing on main): - T527-1–T527-10 look implemented (flex `shrink-0` ticket footer; no `position:fixed`/portal; guards stay in flow; one ticket mount). - Market tab can flash an empty footer for one frame until `useLayoutEffect` publishes submit chrome. - Post-place “View order” / “Place another” stay in the scroll body; with many open limits users scroll past the money CTA to reach them. - Combined DOM with #528/#530: slippage chips + My open limits stay in `trade-order-ticket-scroll`; only Place/Update/Market CTA lives in `trade-ticket-submit-footer`. - Playwright geometry (P1–P11) was not re-run in this merge pass. MR pipeline failed immediately (duration 0) — not treated as a code verdict.
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:47:48 +00:00 (Migrated from gitlab.com)

mentioned in commit 438e7bc00b

mentioned in commit 438e7bc00b5aee21b1bd0498c5addfddc1919543
PlasticDigits (Migrated from gitlab.com) closed this issue 2026-08-16 08:47:48 +00:00
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-18 12:08:53 +00:00 (Migrated from gitlab.com)

mentioned in issue #561

mentioned in issue #561
PlasticDigits commented 2026-08-18 12:09:31 +00:00 (Migrated from gitlab.com)

mentioned in issue #563

mentioned in issue #563
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#527
No description provided.