bug(frontend): /tiers fee cards wrap Hold and fee columns on phone-width #651
Labels
No labels
agent:fix_bugfix
agent:fix_conflicts
agent:fix_security
agent:gap_analysis
agent:implement
agent:implement
agent:implement
agent:open_issues
agent:ready
agent:research
agent:security_audit
agent:verify
architecture
backend
blocker:hybrid
blocker:launch
blocker:limit-orders
blocker:v2
block:log_only
block:security
bug
ci
contracts
correctness
deploy
dev
devops
docs
documentation
duplicate
e2e
enhancement
epic
feature
frontend
functional-completion
gas
good first issue
governance
help wanted
high-risk
hooks
hybrid
indexer
infra
infrastructure
integrators
invalid
launch-blocker
limit-orders
localnet
localterra
low priority
missing-implementation
needs-design
ops
performance
priority
high
priority
medium
product
qa
QA
question
ready
ready
research
scripts
security
security-hardening
smartcontracts
tech-debt
testing
ux
UX
v2
verification
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
code/cl8y-dex-terraclassic#651
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
On phone-width
/tiers(reported on iPhone + Brave onhttps://dex.cl8y.com/tiers, disconnected), each self-register tier card wraps so badly that Tier N, Hold {n} CL8Y, and fee discount read as stacked single words. A large empty gap sits where the Register column is reserved even when no wallet is connected.Community report (2026-08, anonymized): iPhone, Brave, mainnet
/tiers. Reporter first guessed the gap was for wallet-connect chrome, then correctly identified it as the connected Register slot. They still want the left/middle identity (Tier 1/Hold 1 CL8Y) to stay readable — either as two short lines (Tier 1thenHold 1 CL8Y) or without wrapping at all so more of the ladder fits on one screen.This is a layout bug, not a fee-math or register-tx bug. Numbers on the card match the canonical ladder (#198). Desktop
/tiersis usable; QA row 11.1.4 (“Tiers page mobile — table readable, buttons accessible”) is currently failing on production phones.Related (do not regress): #476 (Hold + Register eligibility / 18-dec CL8Y labels), #384 (register/deregister gas), #514 (limit-place discount vs swap), #537 (pair-scoped fee chrome), #489 (retail copy). Do not fold this into picker viewport work (#632).
Current codebase
Page and row
TiersPage.tsxis the only/tierssurface (App.tsxlazy route).TierRowis a single horizontal flex (flex items-center gap-4 p-4) with five children that never wrap and never collapse:w-12 h-12rounded box withtier_idflex-1 min-w-0—uppercase tracking-wideTier {id} +text-smHold {formatCl8y} CL8Ymin-w-0)text-right—{discountLabel}+uppercasefee discountshrink-0/whitespace-nowraptext-right min-w-[4.5rem]— mint{effectiveFeeLabel}+ eff. fee*w-28(7rem / 112px)When disconnected,
canSelfRegisteris false (!!address && !isOnGovernanceTier), so slot 5 renders an emptyw-28div. That is the “big blank” on the screenshot. Combined withgap-4× 4 and cardp-4, a ~390px iPhone content width leaves ~60–80px for identity — souppercaseTIER 1 wraps toTIER/1and Hold 1 CL8Y wraps toHold/1/CL8Y. FEE DISCOUNT wraps the same way. The mint EFF. FEE* column looks fine because it has a min-width and short labels.Intended copy is already the reporter’s preferred form: a single span
Hold {formatCl8y(tier.min_cl8y_balance)} CL8Y(notHold:+ amount on the next line). The wrap is CSS, not a missing colon.Connected vs disconnected chrome
Connect your wallet to register for a fee discount tier.(shell-panel-strong). No Your Status panel. Tier cards still reserve the Register column.canSelfRegister && !isCurrentTier. That panel is not the empty gap inside each card.There is no
TiersPageVitest file (gap noted ingaps/GAP_1780200149.md). Playwrighte2e/fee-tiers.spec.tsuses the default 1280×720 viewport (playwright.config.ts) and assertsgetByText(/Hold.*CL8Y/)— it cannot fail on wrap. QA 11.1.4 is manual only.How it works table (same page, same class of bug)
Below the cards, a fixed
grid-cols-5dumps Tier / CL8Y Hold / Discount / Eff. Fee* / Limit place* for the full 1–9 ladder. On ~390px that grid also wraps every header and cell. Bundle a mobile treatment here so the page is readable end-to-end.Fee labels (must stay)
baseFeecomes from factorydefault_fee_bps(fallback 180). Display helpers:discountLabel,limitPlaceLabel/resolveLimitDiscountBps/makerPlacementFeeBps(#514 I13). Hold amounts useformatTokenAmountAbbrev+ CL8Y 18 decimals (#476 / I12). Self-register filter:!tier.governance_only(tiers 1–9 only; 0 / 255 stay off this list — I3).Why this is needed
/tiersis the only place a retail wallet registers for a fee discount (I12: holding alone does not apply). On the devices people actually use (iPhone Safari/WebKit — Brave on iOS is WebKit, not Chromium), the ladder is unreadable: “Hold 1 CL8Y” looks like three labels, and disconnected users see a dead 112px column on every card.That wastes vertical space (nine cards + How it works) and makes the Register CTA harder to find once connected. This is not “users should rotate to landscape.” Phone-width must show Tier N, Hold {n} CL8Y, {pct} fee discount, and {pct} eff. fee* as intact phrases.
Constraints / guardrails
Register/Deregisterexecute msgs, gas limits (#384), or on-chainGetTiers/GetRegistrationqueries.discount_bps+ factorydefault_fee_bps(I4). Do not apply a walletget_discountto the published ladder. Pair-scoped chrome stays on Swap/Pool/Trade (#537 / I14) —/tiersis the registry ladder, not a pair quote.limit_discount_bps(tier 9 place = 0). Do not raise swapdiscount_bpson tier 9. Crossing the book still uses the taker swap discount.formatTokenAmountAbbrev). Do not relabel asuluna/ wei /min_cl8y_balance. Do not imply holding alone registers the wallet.governance_only: falserows in the self-register list. No Register on tier 0 / 255.Tier 1thenHold 1 CL8Y) is OK. Do not add always-on essays (“tap Register after connect”).btn-primary/btn-muted,shell-panel-strong,var(--ink)/--mint(alias blue). No*-neo. One chrome layer per card. Light + dark.w-28when the button is absent.resizes-visual); this is not avisualViewportpicker issue.dangerouslySetInnerHTMLon LCD tier fields.e2e-smoke. Do not adde2e-txfor this layout fix. Register/deregister txs stay in existinge2e/fee-tier-tx.spec.ts/make verify-issue-384if touched at all (they should not be).docs/reference/fee-discount-tiers.md.Relevant files
Primary
frontend-dapp/src/pages/TiersPage.tsx—TierRowflex + How it worksgrid-cols-5frontend-dapp/src/App.tsx—/tiersroutefrontend-dapp/src/index.css/theme-dark.css/theme-light.css— only if a shared card utility is addedCopy / math (read, do not fork)
frontend-dapp/src/utils/formatAmount.ts—formatTokenAmountAbbrevfrontend-dapp/src/utils/limitOrderFeeSummary.ts—effectiveSwapFeeBps,makerPlacementFeeBps,resolveLimitDiscountBpsfrontend-dapp/src/utils/tokenRegistry.ts— CL8Y 18 decimalsfrontend-dapp/src/services/terraclassic/feeDiscount.ts—getTiers/getRegistration/register/deregisterdocs/reference/fee-discount-tiers.md— canonical 1–9 minima / bpsskills/AGENTS_FEE_DISCOUNT_TIERS.mddocs/frontend.md§ Tiers Page (thin today — extend with phone-width invariants)skills/AGENTS_FRONTEND_DESIGN_SYSTEM.md/skills/AGENTS_FRONTEND_COPY_COGNITIVE_LOAD.mdQA_TEMPLATE.md— 4.1.* and 11.1.4Tests to add / extend
frontend-dapp/src/pages/TiersPage.test.tsx— new (page currently has no unit coverage)frontend-dapp/e2e/fee-tiers.spec.ts— add a 390×844 (or 375×667) project/test; assert no wrap of Hold / fee phrasesfrontend-dapp/e2e/fee-tier-tx.spec.ts— must stay green; do not require it for the layout MRRecommended direction
Ship one responsive
TierRow+ a mobile How it works treatment. Do not keep five always-on columns on ≤767px.A — Stop reserving the empty Register column
Render the
w-28(or equivalent) only when a Register button or “Governance only” label is actually shown. Disconnected and current-tier rows should not leave a 112px hole. That alone returns enough width for Hold 1 CL8Y on many phones.B — Phone-width identity + fee cluster (preferred)
At
max-width: 767px(same breakpoint as.app-mobile-nav-shell):Tier N+Hold {n} CL8Y) takes the remaining row (or a full first row). Keep Hold {n} CL8Y as one unbreakable phrase (whitespace-nowrapon that span, or a single line underTier N).shrink-0 text-right:{pct}+fee discounton the left of the cluster, mint{pct}+eff. fee*on the right — or stacked as two compact right-aligned stats. Do not let FEE / DISCOUNT wrap to one word per line.Desktop (≥768) may keep a single row, but still must not reserve an empty
w-28.C — How it works on phone
Replace
grid-cols-5on ≤767 with either:overflow-x-auto,min-won the grid, no clipped headers), orDo not ship five squeezed columns that wrap every cell.
D — Out of scope
visualViewport(#632).Acceptance criteria
Tier+Nwithout stackingTIERabove1as the only content in a 2-em column). Hold {n} CL8Y is one phrase on one line (e.g.Hold 1 CL8Y,Hold 7,500 CL8Y).{discount}%+fee discountand{eff}%+eff. fee*remain intact phrases (noFEE/DISCOUNT/EFF./FEE*stacked as single words).e2e/fee-tiers.spec.tsheading / 9 tiers / connect prompt stay green.*-neo. No new fee-trivia banner.docs/frontend.md§ Tiers Page notes phone-width card + How it works rules. QA 11.1.4 tightened to the phrases above.Test plan (all paths)
Unit (new
TiersPage.test.tsx)Mock
getTiers/getRegistration/getFactoryConfig/ wallet store (same pattern as other pages).w-28(assert Register column absent orwidth0 / not in layout).Hold 1 CL8Y(tier 1) andHold 5 CL8Y(tier 2) present as full strings.default_fee_bps180, tier 1 shows2.5%and1.75%; tier 9 shows95%and the matching I4 eff. fee. How it works Limit place* for tier 9 is0/0.00%per I13 helpers — do not hardcode a second ladder.VITE_FEE_DISCOUNT_ADDRESS: “Fee discount contract not configured.”RetryError+ retry calls refetch.matchMedia('(max-width: 767px)')if the implementation keys off it; assert identity + hold text nodes are single-line phrases (no split across multiple block children). If CSS wrap cannot be asserted in jsdom, use adata-testidon the hold span and assertwhitespace-nowrap/ class contract.Vitest (must stay green)
(
limitOrderFeeSummarytest path may differ — run the existing fee-summary suite the page imports.)Playwright (
e2e-smoke, 5 workers)e2e/fee-tiers.spec.tsat 1280×720 (heading, 9×Tier N, discounts, connect prompt, How it works).page.setViewportSize({ width: 390, height: 844 })(and 375×667).getByTextexact / regexHold 1 CL8Y,Hold 5 CL8Y,Hold 7,500 CL8Y(or the liveformatTokenAmountAbbrevstring) is visible as one element — not three separate nodes that only match/Hold/+/1/+/CL8Y/.heightis a single line (≤ ~28px attext-sm); discount label fee discount height is a single line.getBoundingClientRect()— no empty 100px+ trailing hole (e.g. last text cluster’srightis within ~24px of the card’s content box).CL8Y Hold/Eff. Fee*/Limit place*visible without requiring horizontal overflow that clips the first column; if scroll is used,scrollWidthworks and headers stay one line.connectWallet): viewport 390×844 — Register buttons ≥ 1; each buttonheight≥ 44; tap target does not overlap the previous card’s hold text (getBoundingClientRectgap ≥ 0).e2e-txhere.Manual (required — iOS Brave/Safari not in Playwright)
https://dex.cl8y.com/tiers(or LocalTerra/tiers): screenshot cards 1–4 and 9. ConfirmTier N+Hold {n} CL8Y+ fee cluster. No empty Register column.default_fee_bps≠ 180 (if a LocalTerra pair config differs): card footnote still explains pair override; published eff. fee still uses factory default (today’s behavior).Test plan (attack, hack, and abuse)
Layout sits on the register path. Treat Register as a trust-boundary control even though msgs are unchanged.
tier_id2 or 4. RTL/unit: each button’sonRegister(tier_id)matches that row’sentry.tier_id. Playwright: click Register on a mid list row (tier 5) and assert the mutation / pending state is scoped (ordata-testid={register-tier-${id}}).governance_only: falsespoof is out of band; still: fixtures with tier 0 / 255 must not show Register. Do not trusttier_idtext alone — filter ongovernance_only.tierstrings / future names render as text. NodangerouslySetInnerHTML.CL8Y_TOKEN_ADDRESSin How it works staysfont-monotext.tier_id. Truncation must not turnHold 7,500 CL8YintoHold 7orHold 7500without thousands separator consistency (formatTokenAmountAbbrev). Overflow ellipsis on the hold phrase is forbidden if it hides the magnitude (prefer wrap the whole card, not7,500→7…).95%next to tier 1’s hold line. Keep discount + hold in the same card DOM subtree (no grid that interleaves rows). Snapshot / testids per row.z-indexbelow Connect / Legal / WalletConnect (z-[9999]). Must not be a transparent hit layer over the header CONNECT button.disabled={isRegistering}on all Register buttons while a tx is in flight (today’s behavior). Reflow must not remount a fresh enabled button mid-tx.*/ “some pairs may differ” must remain; do not present the mint eff. fee as a guaranteed pair quote (I14 / #537).devicePixelRatio/ 200% text: card must grow vertically, not overlap the next Register. Nooverflow: hiddenthat clips the hold amount or the CTA.Verification criteria
Done when:
/tiers: all nine self-register cards show intact Hold {n} CL8Y and fee phrases; no empty Register column. Connected: Register ≥44px, correcttier_id.TiersPage.test.tsxcovers disconnected / unregistered / registered / governance filter / fee labels.e2e/fee-tiers.spec.tsdesktop +make test-frontend(or scoped Vitest above) green. Noe2e-txrequired.docs/frontend.md§ Tiers Page + QA 11.1.4 updated. No duplicated numeric ladder in new docs.make lint-frontendclean. No*-neo. Light + dark checked.mentioned in merge request !1153
mentioned in commit
4295334f2fmentioned in issue #654
marked as related to #654
Merge !1153 is on
main(4295334f). Localmake verify-issue-651was 9/9 including Playwrighte2e/fee-tiers.spec.ts14/14 (5 workers): desktop + 390×844 + 375×667 + connected Register geometry.Problems / leftovers (tracked on #654 — do not reopen this issue unless a merged invariant is wrong):
ci_quota_exceeded(not a code defect). Did not wait for CI./tierswas not run.mentioned in issue #669