bug(frontend): Swap direction button does not mask the Pay/Receive seam line #659

Closed
opened 2026-08-26 04:08:06 +00:00 by PlasticDigits · 6 comments
PlasticDigits commented 2026-08-26 04:08:06 +00:00 (Migrated from gitlab.com)

Summary

On Swap (/), the switch-directions control sits on the You Pay / You Receive seam, but the hairline divider still draws through the button and its arrows. The control looks unfinished: a faint grey line crosses the blue up/down icon.

This is a paint / stacking bug, not a quote or execute bug. Flip still swaps pay/receive tokens. Same surface is used for wrap/unwrap quotes on Swap (Playwright wrap-swap E5). Bundle dark + light, phone + desktop, idle + hover + keyboard focus into one issue. Do not split “opaque fill” vs “hover gap” vs “focus ring.”

Community report (anonymized): Swap page, dark theme — “the horizontal line going through the switch directions button is ugly.”

Related (do not regress): #488 design tokens, #653 one chrome layer (Swap IO cards stay allowlisted), #144 / #181 focus-visible, #489 copy density, #415 *-glass rename.


Current codebase

Layout (intentional seam)

SwapPage.tsx builds one .swap-io-stack inside the Swap shell-panel-strong:

  1. You Pay — card-glass swap-io-card-pay
  2. Direction button — wrapper relative z-20 … -my-5 sm:-my-[22px] (negative margin pulls the control onto the seam; pointer-events-none on the wrapper, pointer-events-auto on the button)
  3. You Receive — card-glass swap-io-card-receive

The button is aria-label="Swap pay and receive tokens", class swap-direction-btn, and swaps fromToken / toToken. Icon is an inline SVG (vertical arrows). Hover uses Tailwind hover:-translate-y-0.5.

There is no second Swap IO stack. Wrap/unwrap uses this same page. /trade Bid↔Ask and .limit-side-flip-btn are different controls and are out of scope unless a copy-paste would reintroduce a transparent plate on a seam.

CSS (why the line shows through)

index.css (comment: “two halves abut; direction button sits on the seam”):

Rule Effect
.swap-io-stack isolation: isolate, overflow: hidden, 24px radius, --chrome-border, --shadow-card, blur
.swap-io-stack::before Full-bleed --io-stack-bg at z-index: 0
.swap-io-card-pay.card-glass / .swap-io-card-receive.card-glass z-index: 1, transparent fill (stack paint shows through), no own shadow
.swap-io-card-pay.card-glass border-bottom: 1px solid var(--chrome-border) — this is the horizontal line
.swap-io-card-receive.card-glass margin-top: -1px (abut)
.swap-direction-btn Blue hairline, background: var(--control-surface), --shadow-card
.swap-direction-btn:hover background: var(--control-surface-hover)

Dark theme-dark.css / :root in index.css: --control-surface: rgba(255, 255, 255, 0.035) and hover 0.06. That is a glass wash, not a plate. The button is already z-20 above the cards; the seam still shows through the fill. Light theme-light.css uses --control-surface: rgba(255, 255, 255, 0.72) — less severe, but the hairline can still read through the rounded plate, especially on hover translate.

--io-stack-bg mid stops (the seam band) are roughly rgba(22, 28, 42) / rgba(16, 21, 32) in dark and rgba(248, 250, 252) / rgba(241, 245, 249) in light.

Gaps

  • No :focus-visible rule on .swap-direction-btn (contrast .limit-side-flip-btn:focus-visible which stacks --focus-ring). Keyboard Tab to the flip control has no dedicated ring.
  • Hover -translate-y-0.5 moves the plate off the seam. Even after an opaque fill, the vacated 2px can re-expose the hairline unless a static mask / underlay stays on the seam.
  • .swap-io-stack { overflow: hidden } will clip any halo or translate that extends outside the rounded stack.
  • E2E only asserts the button exists (e2e/swap.spec.ts, e2e/wrap-swap.spec.ts E5). No paint / computed-background assertion.

Why this is needed

Swap is the default retail surface. The flip control is the only visual join between Pay and Receive. A hairline through the icon reads as a broken layer (divider on top of the button), not as a deliberate QuickSwap-style overlap.

--control-surface is the right token for inset chips on a panel; it is the wrong token for a control that must occlude a 1px seam. Fixing z-index alone cannot hide a line that is visible through a 3.5% white fill.


Constraints / guardrails

  1. Paint only. Do not change flip JS (fromToken / toToken), quote / hybrid / wrap execute, slippage, or toRawAmount filters.
  2. Keep the seam. The Pay/Receive hairline stays as IA. Do not delete .swap-io-card-pay border-bottom and do not split the stack into two floating cards with a gap (that fights #653 C653-2 / C653-5: Swap IO cards remain the allowlisted nest).
  3. Do not add chrome. No extra card-glass / shell-panel* around the button. Keep scripts/chrome_nesting_allowlist.txt → SwapPage.tsx as-is unless the file path moves.
  4. Tokens (#488). Opaque plate must use existing cool navy / light stack mid-tones (--io-stack-bg stop, --panel-bg, or a new theme token in both theme-dark.css and theme-light.css). No hard-coded Tailwind primary/dex hex. No gold fill. No *-neo.
  5. Hover must not re-open the line. If the button still translates on hover, leave a static occluder on the seam (underlay, local mask, or no-translate). Do not rely on the moving plate alone.
  6. overflow: hidden. Do not drop stack clipping just to unclip a halo — that also clips the 24px radius. Size the plate so it stays inside the stack at 375px and 1280px (w-10 / sm:w-11).
  7. Hit target / stacking. Keep the wrapper pointer-events-none + button pointer-events-auto. Do not raise z-index so the flip overlays token pickers, Settings, legal/risk modals, or portaled listboxes (#632). Min tap size stays ~40–44px.
  8. Focus (#144). While touching this control, add .swap-direction-btn:focus-visible using var(--focus-ring) (same mix as .limit-side-flip-btn / .swap-io-amount-input). :focus-visible only — not bare :focus.
  9. Copy (#489). Keep aria-label="Swap pay and receive tokens". No new lecture text under the flip.
  10. A11y. Icon stays aria-hidden. Contrast of blue glyph on the new plate must hold in both themes. No outline: none without a replacement ring.
  11. No dangerouslySetInnerHTML. Do not bind token metadata into the button.

Relevant files

File Role
frontend-dapp/src/pages/SwapPage.tsx IO stack markup; flip handler; swap-direction-btn
frontend-dapp/src/index.css .swap-io-stack*, .swap-io-card-*, .swap-direction-btn
frontend-dapp/src/theme-dark.css --control-surface, --io-stack-bg, --chrome-border
frontend-dapp/src/theme-light.css Light equivalents
frontend-dapp/e2e/swap.spec.ts Direction button present
frontend-dapp/e2e/wrap-swap.spec.ts E5 same control
scripts/chrome_nesting_allowlist.txt SwapPage.tsx allowlisted
skills/AGENTS_FRONTEND_DESIGN_SYSTEM.md Tokens / no *-neo
skills/AGENTS_FRONTEND_CHROME_NESTING.md C653-5 Swap IO cards stay card-glass
skills/AGENTS_FRONTEND_A11Y_FOCUS.md :focus-visible + --focus-ring
docs/design-system.md One chrome layer; keyboard focus

  1. Give .swap-direction-btn an opaque fill sampled from the seam band of --io-stack-bg (solid rgb(…) mid-stop, or a dedicated --swap-direction-surface in both theme files). Hover fill stays slightly lighter/darker but still opaque.
  2. Prefer a static occluder (pseudo-element or sibling plate on the seam, z-index between cards and the moving button) so hover:-translate-y-0.5 cannot uncover the hairline. Alternatively drop the translate.
  3. Keep the blue 1px border and --shadow-card so the control still “sits on” the stack.
  4. Add :focus-visible ring (copy the .limit-side-flip-btn pattern).
  5. Do not mask the seam with a full-width gap; only hide the line under the button.
  6. Optional small Vitest or computed-style assertion: .swap-direction-btn resolved background-color alpha is 1 (or equivalent opaque) in both data-theme values. E2E keep existence + click-to-swap pay/receive identities.

Acceptance criteria

  • S1 Dark theme, idle: no --chrome-border hairline is visible inside the direction button or across the arrow icon (375px and 1280px).
  • S2 Light theme: same as S1.
  • S3 Hover (and any translate): the seam does not flash through the vacated pixels.
  • S4 Keyboard: Tab to the button shows a visible --focus-ring; mouse click does not leave a persistent ring (:focus-visible only).
  • S5 Click still swaps pay/receive token identities; aria-label unchanged; wrap/unwrap path on / still shows the same control.
  • S6 Pay/Receive hairline remains left and right of the button (stack still reads as two halves).
  • S7 python3 scripts/check_chrome_nesting.py and python3 scripts/check_design_tokens.py stay green. No new *-neo. No extra card-glass nest.
  • S8 Token pickers, Settings, and legal/risk chrome still open above the stack; flip z-index does not steal those hits.

Test plan (all paths)

Visual / theme

  1. / disconnected, dark, 375px and 1280px — screenshot the seam; line must stop at the plate.
  2. Toggle light (#488 header toggle) — repeat.
  3. Hover and keyboard focus — S3 / S4.
  4. Settings open + slippage chips — stack radius / overflow still clips correctly; button not clipped into a D-shape.

Interaction

  1. Click flip: pay token ↔ receive token; amount field rules unchanged.
  2. Flip with empty amount, with typed amount, with Max (if wallet connected) — no JS errors; You Receive still follows quote rules (#496 clear-on-change still applies after token swap).
  3. Wrap/unwrap pair on / (same page) — E5 control still present and paintable.
  4. Token picker open from Pay, then Receive — listbox not covered by the flip plate (#632).

Regression commands

python3 scripts/check_design_tokens.py
python3 scripts/check_chrome_nesting.py
make lint-frontend
make test-frontend
# optional: existing smoke that the button exists
# frontend-dapp e2e/swap.spec.ts — “has a swap direction toggle button”
# frontend-dapp e2e/wrap-swap.spec.ts — E5
make verify-issue-653

No LocalTerra required for the paint fix. Do not bind-mount indexer/ for cargo.


Test plan (attack, hack, and abuse)

This change is CSS + optional static markup. Still verify it cannot be used to interfere with Swap.

Vector Why it matters Test
Hit-area overlay A full-width z-20 wrapper with pointer-events: auto would steal Pay amount / token-picker clicks. Wrapper stays pointer-events-none; only the ~40–44px button is auto. Click amount field and both token triggers around the seam.
z-index escalation z-50+ on the stack can cover Settings, TermsGate, risk modal, or portaled pickers. Open Settings, token listbox, and first-visit legal/risk if shown; all remain clickable. Do not use position: fixed on the flip.
Focus ring removal outline: none without :focus-visible hides the only keyboard affordance. Tab cycle: Pay amount → tokens → flip → Settings. Flip ring visible.
Contrast / spoof plate Near-black plate + near-black icon, or light plate that fails in dark. Both themes: blue glyph and border remain distinguishable from the plate.
Overflow clip → dead click Halo larger than the stack + overflow: hidden can clip the tap target so the top of the button does not receive clicks. Click all four quadrants of the button at 375px after hover.
Execute / quote injection Unrelated “cleanup” in SwapPage could change hybrid route or wrap combo gas. Diff stays in CSS / class names / a static occluder. No change to simulate, route/solve, or execute msgs.
HTML injection Rendering token symbol HTML in the button. Button children stay the static SVG. No dangerouslySetInnerHTML.
Theme token missing New variable only in dark → light flash or transparent fallback. Both theme files define the plate token; FOUC toggle does not restore the through-line.

Out of scope as attack surface: contracts, indexer, fee math.


Verification criteria

Done when a reviewer on make dev (or production-equivalent CSS) can open / in dark and light, at ~375px and ~1280px, and the Pay/Receive hairline stops at the button chrome in idle, hover, and :focus-visible, while flip behavior, Swap IO allowlist, and design-token checks are unchanged.

Not done if the line is gone only because the entire seam was deleted; if hover re-exposes it; if the button is transparent glass again; or if chrome-nesting / token scripts fail.

Evidence: before/after crops of the seam (dark + light) in the MR; note viewport. No user identity in screenshots or issue comments.

## Summary On **Swap** (`/`), the **switch-directions** control sits on the You Pay / You Receive seam, but the **hairline divider still draws through the button and its arrows**. The control looks unfinished: a faint grey line crosses the blue up/down icon. This is a **paint / stacking** bug, not a quote or execute bug. Flip still swaps pay/receive tokens. Same surface is used for wrap/unwrap quotes on Swap (Playwright wrap-swap E5). Bundle dark + light, phone + desktop, idle + hover + keyboard focus into **one** issue. Do **not** split “opaque fill” vs “hover gap” vs “focus ring.” Community report (anonymized): Swap page, dark theme — “the horizontal line going through the switch directions button is ugly.” Related (do not regress): [#488](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/488) design tokens, [#653](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/653) one chrome layer (Swap IO cards stay allowlisted), [#144](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/144) / [#181](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/181) focus-visible, [#489](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/489) copy density, [#415](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/415) `*-glass` rename. --- ## Current codebase ### Layout (intentional seam) [`SwapPage.tsx`](frontend-dapp/src/pages/SwapPage.tsx) builds one **`.swap-io-stack`** inside the Swap `shell-panel-strong`: 1. **You Pay** — `card-glass swap-io-card-pay` 2. **Direction button** — wrapper `relative z-20 … -my-5 sm:-my-[22px]` (negative margin pulls the control onto the seam; `pointer-events-none` on the wrapper, `pointer-events-auto` on the button) 3. **You Receive** — `card-glass swap-io-card-receive` The button is `aria-label="Swap pay and receive tokens"`, class `swap-direction-btn`, and swaps `fromToken` / `toToken`. Icon is an inline SVG (vertical arrows). Hover uses Tailwind `hover:-translate-y-0.5`. There is **no** second Swap IO stack. Wrap/unwrap uses this same page. `/trade` Bid↔Ask and [`.limit-side-flip-btn`](frontend-dapp/src/index.css) are **different** controls and are out of scope unless a copy-paste would reintroduce a transparent plate on a seam. ### CSS (why the line shows through) [`index.css`](frontend-dapp/src/index.css) (comment: “two halves abut; direction button sits on the seam”): | Rule | Effect | |------|--------| | `.swap-io-stack` | `isolation: isolate`, `overflow: hidden`, 24px radius, `--chrome-border`, `--shadow-card`, blur | | `.swap-io-stack::before` | Full-bleed `--io-stack-bg` at `z-index: 0` | | `.swap-io-card-pay.card-glass` / `.swap-io-card-receive.card-glass` | `z-index: 1`, **transparent** fill (stack paint shows through), no own shadow | | `.swap-io-card-pay.card-glass` | **`border-bottom: 1px solid var(--chrome-border)`** — this **is** the horizontal line | | `.swap-io-card-receive.card-glass` | `margin-top: -1px` (abut) | | `.swap-direction-btn` | Blue hairline, `background: var(--control-surface)`, `--shadow-card` | | `.swap-direction-btn:hover` | `background: var(--control-surface-hover)` | Dark [`theme-dark.css`](frontend-dapp/src/theme-dark.css) / `:root` in `index.css`: `--control-surface: rgba(255, 255, 255, 0.035)` and hover `0.06`. That is a **glass wash**, not a plate. The button is already `z-20` **above** the cards; the seam still **shows through** the fill. Light [`theme-light.css`](frontend-dapp/src/theme-light.css) uses `--control-surface: rgba(255, 255, 255, 0.72)` — less severe, but the hairline can still read through the rounded plate, especially on hover translate. `--io-stack-bg` mid stops (the seam band) are roughly `rgba(22, 28, 42)` / `rgba(16, 21, 32)` in dark and `rgba(248, 250, 252)` / `rgba(241, 245, 249)` in light. ### Gaps - **No** `:focus-visible` rule on `.swap-direction-btn` (contrast [`.limit-side-flip-btn:focus-visible`](frontend-dapp/src/index.css) which stacks `--focus-ring`). Keyboard Tab to the flip control has no dedicated ring. - Hover **`-translate-y-0.5`** moves the plate off the seam. Even after an opaque fill, the vacated 2px can **re-expose the hairline** unless a static mask / underlay stays on the seam. - `.swap-io-stack { overflow: hidden }` will clip any halo or translate that extends outside the rounded stack. - E2E only asserts the button **exists** ([`e2e/swap.spec.ts`](frontend-dapp/e2e/swap.spec.ts), [`e2e/wrap-swap.spec.ts`](frontend-dapp/e2e/wrap-swap.spec.ts) E5). No paint / computed-background assertion. --- ## Why this is needed Swap is the default retail surface. The flip control is the **only** visual join between Pay and Receive. A hairline through the icon reads as a broken layer (divider on top of the button), not as a deliberate QuickSwap-style overlap. `--control-surface` is the right token for **inset chips** on a panel; it is the wrong token for a control that must **occlude** a 1px seam. Fixing z-index alone cannot hide a line that is visible *through* a 3.5% white fill. --- ## Constraints / guardrails 1. **Paint only.** Do not change flip JS (`fromToken` / `toToken`), quote / hybrid / wrap execute, slippage, or `toRawAmount` filters. 2. **Keep the seam.** The Pay/Receive hairline stays as IA. Do **not** delete `.swap-io-card-pay` `border-bottom` and do **not** split the stack into two floating cards with a gap (that fights [#653](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/653) **C653-2** / **C653-5**: Swap IO cards remain the allowlisted nest). 3. **Do not add chrome.** No extra `card-glass` / `shell-panel*` around the button. Keep `scripts/chrome_nesting_allowlist.txt` → `SwapPage.tsx` as-is unless the file path moves. 4. **Tokens (#488).** Opaque plate must use existing cool navy / light stack mid-tones (`--io-stack-bg` stop, `--panel-bg`, or a **new** theme token in **both** `theme-dark.css` and `theme-light.css`). No hard-coded Tailwind `primary`/`dex` hex. No gold fill. No `*-neo`. 5. **Hover must not re-open the line.** If the button still translates on hover, leave a **static** occluder on the seam (underlay, local mask, or no-translate). Do not rely on the moving plate alone. 6. **`overflow: hidden`.** Do not drop stack clipping just to unclip a halo — that also clips the 24px radius. Size the plate so it stays inside the stack at 375px and 1280px (`w-10` / `sm:w-11`). 7. **Hit target / stacking.** Keep the wrapper `pointer-events-none` + button `pointer-events-auto`. Do **not** raise z-index so the flip overlays token pickers, Settings, legal/risk modals, or portaled listboxes ([#632](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/632)). Min tap size stays ~40–44px. 8. **Focus (#144).** While touching this control, add `.swap-direction-btn:focus-visible` using `var(--focus-ring)` (same mix as `.limit-side-flip-btn` / `.swap-io-amount-input`). `:focus-visible` only — not bare `:focus`. 9. **Copy (#489).** Keep `aria-label="Swap pay and receive tokens"`. No new lecture text under the flip. 10. **A11y.** Icon stays `aria-hidden`. Contrast of blue glyph on the new plate must hold in both themes. No `outline: none` without a replacement ring. 11. **No `dangerouslySetInnerHTML`.** Do not bind token metadata into the button. --- ## Relevant files | File | Role | |------|------| | `frontend-dapp/src/pages/SwapPage.tsx` | IO stack markup; flip handler; `swap-direction-btn` | | `frontend-dapp/src/index.css` | `.swap-io-stack*`, `.swap-io-card-*`, `.swap-direction-btn` | | `frontend-dapp/src/theme-dark.css` | `--control-surface`, `--io-stack-bg`, `--chrome-border` | | `frontend-dapp/src/theme-light.css` | Light equivalents | | `frontend-dapp/e2e/swap.spec.ts` | Direction button present | | `frontend-dapp/e2e/wrap-swap.spec.ts` | E5 same control | | `scripts/chrome_nesting_allowlist.txt` | `SwapPage.tsx` allowlisted | | `skills/AGENTS_FRONTEND_DESIGN_SYSTEM.md` | Tokens / no `*-neo` | | `skills/AGENTS_FRONTEND_CHROME_NESTING.md` | **C653-5** Swap IO cards stay `card-glass` | | `skills/AGENTS_FRONTEND_A11Y_FOCUS.md` | `:focus-visible` + `--focus-ring` | | `docs/design-system.md` | One chrome layer; keyboard focus | --- ## Recommended direction 1. Give `.swap-direction-btn` an **opaque** fill sampled from the **seam band** of `--io-stack-bg` (solid `rgb(…)` mid-stop, or a dedicated `--swap-direction-surface` in both theme files). Hover fill stays slightly lighter/darker but still **opaque**. 2. Prefer a **static** occluder (pseudo-element or sibling plate on the seam, `z-index` between cards and the moving button) so `hover:-translate-y-0.5` cannot uncover the hairline. Alternatively drop the translate. 3. Keep the blue 1px border and `--shadow-card` so the control still “sits on” the stack. 4. Add `:focus-visible` ring (copy the `.limit-side-flip-btn` pattern). 5. Do **not** mask the seam with a full-width gap; only hide the line **under the button**. 6. Optional small Vitest or computed-style assertion: `.swap-direction-btn` resolved `background-color` alpha is **1** (or equivalent opaque) in both `data-theme` values. E2E keep existence + click-to-swap pay/receive identities. --- ## Acceptance criteria - [ ] **S1** Dark theme, idle: no `--chrome-border` hairline is visible **inside** the direction button or across the arrow icon (375px and 1280px). - [ ] **S2** Light theme: same as S1. - [ ] **S3** Hover (and any translate): the seam does **not** flash through the vacated pixels. - [ ] **S4** Keyboard: Tab to the button shows a visible `--focus-ring`; mouse click does not leave a persistent ring (`:focus-visible` only). - [ ] **S5** Click still swaps pay/receive token identities; `aria-label` unchanged; wrap/unwrap path on `/` still shows the same control. - [ ] **S6** Pay/Receive hairline **remains** left and right of the button (stack still reads as two halves). - [ ] **S7** `python3 scripts/check_chrome_nesting.py` and `python3 scripts/check_design_tokens.py` stay green. No new `*-neo`. No extra `card-glass` nest. - [ ] **S8** Token pickers, Settings, and legal/risk chrome still open above the stack; flip `z-index` does not steal those hits. --- ## Test plan (all paths) ### Visual / theme 1. `/` disconnected, dark, 375px and 1280px — screenshot the seam; line must stop at the plate. 2. Toggle light ([#488](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/488) header toggle) — repeat. 3. Hover and keyboard focus — S3 / S4. 4. Settings open + slippage chips — stack radius / overflow still clips correctly; button not clipped into a D-shape. ### Interaction 5. Click flip: pay token ↔ receive token; amount field rules unchanged. 6. Flip with empty amount, with typed amount, with Max (if wallet connected) — no JS errors; You Receive still follows quote rules ([#496](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/496) clear-on-change still applies **after** token swap). 7. Wrap/unwrap pair on `/` (same page) — E5 control still present and paintable. 8. Token picker open from Pay, then Receive — listbox not covered by the flip plate ([#632](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/632)). ### Regression commands ```bash python3 scripts/check_design_tokens.py python3 scripts/check_chrome_nesting.py make lint-frontend make test-frontend # optional: existing smoke that the button exists # frontend-dapp e2e/swap.spec.ts — “has a swap direction toggle button” # frontend-dapp e2e/wrap-swap.spec.ts — E5 make verify-issue-653 ``` No LocalTerra required for the paint fix. Do not bind-mount `indexer/` for cargo. --- ## Test plan (attack, hack, and abuse) This change is **CSS + optional static markup**. Still verify it cannot be used to interfere with Swap. | Vector | Why it matters | Test | |--------|----------------|------| | **Hit-area overlay** | A full-width `z-20` wrapper with `pointer-events: auto` would steal Pay amount / token-picker clicks. | Wrapper stays `pointer-events-none`; only the ~40–44px button is `auto`. Click amount field and both token triggers around the seam. | | **z-index escalation** | `z-50`+ on the stack can cover Settings, TermsGate, risk modal, or portaled pickers. | Open Settings, token listbox, and first-visit legal/risk if shown; all remain clickable. Do not use `position: fixed` on the flip. | | **Focus ring removal** | `outline: none` without `:focus-visible` hides the only keyboard affordance. | Tab cycle: Pay amount → tokens → flip → Settings. Flip ring visible. | | **Contrast / spoof plate** | Near-black plate + near-black icon, or light plate that fails in dark. | Both themes: blue glyph and border remain distinguishable from the plate. | | **Overflow clip → dead click** | Halo larger than the stack + `overflow: hidden` can clip the tap target so the top of the button does not receive clicks. | Click all four quadrants of the button at 375px after hover. | | **Execute / quote injection** | Unrelated “cleanup” in `SwapPage` could change hybrid route or wrap combo gas. | Diff stays in CSS / class names / a static occluder. No change to simulate, `route/solve`, or execute msgs. | | **HTML injection** | Rendering token symbol HTML in the button. | Button children stay the static SVG. No `dangerouslySetInnerHTML`. | | **Theme token missing** | New variable only in dark → light flash or transparent fallback. | Both theme files define the plate token; FOUC toggle does not restore the through-line. | Out of scope as attack surface: contracts, indexer, fee math. --- ## Verification criteria **Done when** a reviewer on `make dev` (or production-equivalent CSS) can open `/` in dark and light, at ~375px and ~1280px, and the Pay/Receive hairline **stops at the button chrome** in idle, hover, and `:focus-visible`, while flip behavior, Swap IO allowlist, and design-token checks are unchanged. **Not done if** the line is gone only because the entire seam was deleted; if hover re-exposes it; if the button is transparent glass again; or if chrome-nesting / token scripts fail. **Evidence:** before/after crops of the seam (dark + light) in the MR; note viewport. No user identity in screenshots or issue comments.
PlasticDigits commented 2026-08-26 06:48:13 +00:00 (Migrated from gitlab.com)

mentioned in merge request !1155

mentioned in merge request !1155
PlasticDigits commented 2026-08-26 07:59:37 +00:00 (Migrated from gitlab.com)

mentioned in commit 74d39eb78a

mentioned in commit 74d39eb78adc6ff6865188e129be5e5f8afc5b36
PlasticDigits (Migrated from gitlab.com) closed this issue 2026-08-26 07:59:38 +00:00
PlasticDigits commented 2026-08-26 08:04:24 +00:00 (Migrated from gitlab.com)

mentioned in commit 98b2da570f

mentioned in commit 98b2da570f2d482bafadc88e96cf60f8fe6ec876
PlasticDigits commented 2026-08-26 08:08:56 +00:00 (Migrated from gitlab.com)

mentioned in commit 723c426856

mentioned in commit 723c4268566cc6f8a16a616745ffcfdf7173ca0a
PlasticDigits commented 2026-08-26 09:30:48 +00:00 (Migrated from gitlab.com)

Merged to main via !1155 (conflict-free). Pipelines were not green (ci_quota_exceeded); merged without waiting CI per batch instruction.

Leftover: visual QA on live Swap at 375/1280 light+dark that the flip plate still occludes the Pay/Receive seam. make verify-issue-659 when the frontend is rebuilt from current main.

Merged to `main` via !1155 (conflict-free). Pipelines were not green (`ci_quota_exceeded`); merged without waiting CI per batch instruction. Leftover: visual QA on live Swap at 375/1280 light+dark that the flip plate still occludes the Pay/Receive seam. `make verify-issue-659` when the frontend is rebuilt from current `main`.
PlasticDigits commented 2026-08-26 09:31:34 +00:00 (Migrated from gitlab.com)

mentioned in issue #673

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