Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions frontend/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -797,6 +797,16 @@ <h3>APY Alerts</h3>

</main>
</div><!-- .main-wrap -->

<!-- Mobile sticky action bar (thumb-reach CTAs) -->
<div id="mobile-action-bar" class="mobile-action-bar hidden">
<button id="mob-open-btn" class="btn btn-primary mob-cta-btn">Open Position</button>
<button id="mob-adjust-btn" class="btn btn-primary mob-cta-btn hidden">Adjust Leverage</button>
<button id="mob-add-funds-btn" class="btn btn-primary mob-cta-btn hidden">Add Funds</button>
<button id="mob-close-btn" class="btn btn-danger mob-cta-btn hidden">Close</button>
<button id="mob-repay-btn" class="btn btn-secondary mob-cta-btn hidden">Repay</button>
</div>

</div>

<!-- Tooltip popover -->
Expand Down
68 changes: 68 additions & 0 deletions frontend/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1703,6 +1703,7 @@ function showConnected() {
$("dashboard").classList.remove("hidden");
$("asset-tabs-bar").style.display = "";
}
syncMobileActionBar();
}

async function connect() {
Expand Down Expand Up @@ -1818,6 +1819,7 @@ function switchView(view: AppView) {
closeDrawer();
// Close pool dropdown
$("pool-dropdown").classList.add("hidden");
syncMobileActionBar();
}

// ── Mobile sidebar drawer (#5) ───────────────────────────────────────────
Expand Down Expand Up @@ -2255,11 +2257,77 @@ $("demo-btn").addEventListener("click", () => {
toast("Demo mode \u2014 explore the UI without a wallet", "info");
});

// ── Mobile sticky action bar (A22) ───────────────────────────────────────────

/** Mirror the state of in-card primary CTAs to the sticky bottom bar. */
function syncMobileActionBar() {
const bar = $("mobile-action-bar");
// Only show on the leverage view when dashboard is visible
const visible = activeView === "leverage" && !$("dashboard").classList.contains("hidden");
bar.classList.toggle("hidden", !visible);
if (!visible) return;

const openBtn = $("open-btn") as HTMLButtonElement;
const adjustBtn = $("adjust-btn") as HTMLButtonElement;
const addFundsBtn = $("add-funds-btn") as HTMLButtonElement;
const closeBtn = $("close-btn") as HTMLButtonElement;
const repayBtn = $("repay-btn") as HTMLButtonElement;

const mobOpen = $("mob-open-btn") as HTMLButtonElement;
const mobAdjust = $("mob-adjust-btn") as HTMLButtonElement;
const mobAddFunds = $("mob-add-funds-btn") as HTMLButtonElement;
const mobClose = $("mob-close-btn") as HTMLButtonElement;
const mobRepay = $("mob-repay-btn") as HTMLButtonElement;

// Sync visibility
mobOpen.classList.toggle("hidden", openBtn.classList.contains("hidden"));
mobAdjust.classList.toggle("hidden", adjustBtn.classList.contains("hidden"));
mobAddFunds.classList.toggle("hidden", addFundsBtn.classList.contains("hidden"));
// Close/Repay always shown when position exists
const hasPos = !$("no-position").classList.contains("hidden") === false;
mobClose.classList.toggle("hidden", closeBtn.disabled && !hasPos);
mobRepay.classList.toggle("hidden", true); // only show repay when enabled

// Sync disabled state
mobOpen.disabled = openBtn.disabled;
mobAdjust.disabled = adjustBtn.disabled;
mobAddFunds.disabled = addFundsBtn.disabled;
mobClose.disabled = closeBtn.disabled;
mobRepay.disabled = repayBtn.disabled;

// Sync text
mobAdjust.textContent = adjustBtn.textContent;
mobAddFunds.textContent = addFundsBtn.textContent;

// Show close/repay only when position exists
const posExists = $("position-data") && !$("position-data").classList.contains("hidden");
mobClose.classList.toggle("hidden", !posExists);
mobRepay.classList.toggle("hidden", repayBtn.disabled);
}

$("mob-open-btn").addEventListener("click", () => ($("open-btn") as HTMLButtonElement).click());
$("mob-adjust-btn").addEventListener("click", () => ($("adjust-btn") as HTMLButtonElement).click());
$("mob-add-funds-btn").addEventListener("click",() => ($("add-funds-btn") as HTMLButtonElement).click());
$("mob-close-btn").addEventListener("click", () => ($("close-btn") as HTMLButtonElement).click());
$("mob-repay-btn").addEventListener("click", () => ($("repay-btn") as HTMLButtonElement).click());

// Patch updatePreview and renderPosition to also sync the bar
const _origUpdatePreview = updatePreview;
// We can't reassign const, so we call syncMobileActionBar at the end of the
// existing event-driven flow by observing MutationObserver on the buttons.
const _barObserver = new MutationObserver(syncMobileActionBar);
["open-btn","adjust-btn","add-funds-btn","close-btn","repay-btn"].forEach(id => {
_barObserver.observe($(id), { attributes: true, attributeFilter: ["disabled","class"] });
});
// Also sync on view switch
const _origSwitchView = switchView;

// Init preview with defaults
updatePreview();
renderTxHistory();
renderPoolFooter();
initTooltips();
syncMobileActionBar();

// ── Overview (cross-protocol dashboard) ───────────────────────────────────────

Expand Down
108 changes: 108 additions & 0 deletions frontend/src/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -1210,3 +1210,111 @@ main { flex: 1; max-width: 1200px; width: 100%; margin: 0 auto; padding: 20px 24
*, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
.skeleton { animation: none; background: var(--metric-bg); }
}

/* ── Mobile-first 375px rework (A22) ─────────────────────────────────────── */

/* Sticky bottom action bar */
.mobile-action-bar {
display: none;
position: fixed; bottom: 0; left: 0; right: 0; z-index: 120;
padding: 10px 12px calc(10px + env(safe-area-inset-bottom));
background: var(--surface); border-top: 1px solid var(--border);
gap: 8px;
}
.mob-cta-btn {
flex: 1; min-height: 48px; font-size: 15px; font-weight: 700;
border-radius: var(--r);
}

@media (max-width: 480px) {
/* Show sticky bar, hide in-card primary CTAs */
.mobile-action-bar { display: flex; }
#open-btn, #adjust-btn, #add-funds-btn { display: none !important; }

/* Pad main so content isn't hidden behind sticky bar */
main { padding-bottom: 90px; }

/* Fix .two-col overflow — was minmax(340px,380px) which exceeds 375px */
.two-col { grid-template-columns: 1fr; }

/* Stats row: 2-col at 480px, already handled; ensure no overflow */
.stats-row { grid-template-columns: 1fr 1fr; }

/* Slider zones: abbreviate labels to prevent overflow */
.slider-zones { font-size: 9px; letter-spacing: 0; padding: 0; }
.zone-conservative::after { content: 'Safe'; }
.zone-conservative > * { display: none; }
.zone-conservative { font-size: 0; }
.zone-conservative::after { font-size: 9px; font-weight: 600; color: var(--success); }
.zone-moderate::after { content: 'Mod'; }
.zone-moderate { font-size: 0; }
.zone-moderate::after { font-size: 9px; font-weight: 600; color: var(--primary); }
.zone-aggressive::after { content: 'Agg'; }
.zone-aggressive { font-size: 0; }
.zone-aggressive::after { font-size: 9px; font-weight: 600; color: var(--warning); }
.zone-degen::after { content: 'Degen'; }
.zone-degen { font-size: 0; }
.zone-degen::after { font-size: 9px; font-weight: 600; color: var(--danger); }
.zone-maxi-degen::after { content: 'MAX'; }
.zone-maxi-degen { font-size: 0; }
.zone-maxi-degen::after { font-size: 9px; font-weight: 600; color: var(--danger); }

/* Slider row: stack number input below slider */
.slider-row { flex-wrap: wrap; gap: 8px; }
.slider { min-width: 0; flex: 1 1 100%; order: 1; }
.leverage-num-input { order: 2; width: 72px; flex: 0 0 auto; }
.slider-value-x { order: 3; }

/* Slider thumb: enlarge to 44px touch target */
.slider::-webkit-slider-thumb {
width: 28px; height: 28px;
box-shadow: 0 0 0 8px var(--slider-glow);
}
.slider::-moz-range-thumb {
width: 28px; height: 28px; border-radius: 50%;
background: var(--primary); border: none; cursor: pointer;
}

/* Asset tabs: min 44px height */
.asset-tab {
min-height: 44px; padding: 0 14px;
display: flex; align-items: center;
}

/* Adjust sub-tabs: min 44px height */
.adjust-tab { min-height: 44px; padding: 0; }

/* Mobile card tabs: already 10px padding, ensure 44px */
.mobile-card-tab { min-height: 44px; }

/* Pool tabs in sidebar: min 44px */
.pool-tab { min-height: 44px; }

/* btn-sm: increase tap target */
.btn-sm { min-height: 36px; padding: 6px 12px; }

/* BLND actions: wrap on small screens */
.blnd-actions { flex-wrap: wrap; gap: 4px; }
.blnd-actions .btn { flex: 1; min-height: 44px; font-size: 12px; }

/* Position actions: full-width buttons */
.position-actions { flex-direction: column; }
.position-actions .btn { width: 100%; min-height: 44px; }

/* Wallet connected: hide switch/disconnect on very small screens, keep address */
.wallet-connected #switch-wallet-btn,
.wallet-connected #disconnect-btn { display: none; }

/* Prevent any element from causing horizontal overflow */
body { overflow-x: hidden; }
#app, .main-wrap, main { max-width: 100%; overflow-x: hidden; }

/* Overview table: scroll horizontally within its container */
.overview-table-wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; }
.overview-table { min-width: 480px; }
}

/* Wrap overview table for horizontal scroll on mobile */
@media (max-width: 600px) {
.overview-table { display: block; overflow-x: auto; -webkit-overflow-scrolling: touch; }
}