diff --git a/frontend/index.html b/frontend/index.html
index f904f23..2adc1f5 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -55,6 +55,10 @@
Important Disclaimer
+
@@ -396,6 +400,7 @@ Open Position
×
+
Conservative
diff --git a/frontend/src/main.ts b/frontend/src/main.ts
index fcc1ceb..3a377ce 100644
--- a/frontend/src/main.ts
+++ b/frontend/src/main.ts
@@ -336,6 +336,26 @@ const fmt = (n: number, d = 2) =>
const aprToApy = (apr: number) => (Math.exp(apr / 100) - 1) * 100;
const fmtAddr = (addr: string) => addr.slice(0, 6) + "…" + addr.slice(-4);
+// ── Safe Max settings ─────────────────────────────────────────────────────────
+
+const SAFE_MAX_HF_FLOOR_KEY = "safeMaxHfFloor";
+const DEFAULT_SAFE_MAX_HF_FLOOR = 1.2;
+
+function sanitizeSafeMaxFloor(value: number): number {
+ if (!Number.isFinite(value)) return DEFAULT_SAFE_MAX_HF_FLOOR;
+ return Math.min(10, Math.max(MIN_HF_NORMAL, Math.round(value * 1000) / 1000));
+}
+
+let safeMaxHfFloor = sanitizeSafeMaxFloor(
+ parseFloat(localStorage.getItem(SAFE_MAX_HF_FLOOR_KEY) ?? String(DEFAULT_SAFE_MAX_HF_FLOOR))
+);
+
+function renderSafeMaxFloorInput() {
+ const input = $("safe-max-floor") as HTMLInputElement;
+ input.value = safeMaxHfFloor.toFixed(2);
+ input.min = MIN_HF_NORMAL.toFixed(2);
+}
+
// ── Skeleton loading (#3) ────────────────────────────────────────────────────
function setSkeleton(id: string) {
@@ -610,6 +630,8 @@ function updateLeverageSlider(c: number, l: number = 1) {
slider.max = numIn.max = String(leverageable ? maxLev : 1.0);
slider.step = numIn.step = "0.1";
slider.disabled = numIn.disabled = !leverageable;
+ const safeMaxBtn = document.getElementById("safe-max-btn") as HTMLButtonElement | null;
+ if (safeMaxBtn) safeMaxBtn.disabled = !leverageable;
const cur = parseFloat(slider.value);
const clamped = Math.min(parseFloat(slider.max), Math.max(1.0, cur));
if (clamped !== cur) { slider.value = String(clamped); numIn.value = String(clamped); }
@@ -1167,6 +1189,40 @@ function switchAdjustSubTab(sub: "leverage" | "add-funds") {
// ── Leverage preview ──────────────────────────────────────────────────────────
+function saveSafeMaxFloorFromInput() {
+ const input = $("safe-max-floor") as HTMLInputElement;
+ const parsed = parseFloat(input.value);
+ const next = sanitizeSafeMaxFloor(parsed);
+ safeMaxHfFloor = next;
+ localStorage.setItem(SAFE_MAX_HF_FLOOR_KEY, String(next));
+ renderSafeMaxFloorInput();
+ updatePreview();
+}
+
+function applySafeMaxLeverage() {
+ const slider = $("leverage-slider") as HTMLInputElement;
+ const numIn = $("leverage-input") as HTMLInputElement;
+ const rs = reserves.find(r => r.asset.id === selectedAsset.id);
+ const c = rs ? rs.cFactor : selectedAsset.cFactor;
+ const l = rs?.lFactor ?? 1;
+ const floor = Math.max(safeMaxHfFloor, minHF());
+ const sliderMax = parseFloat(slider.max) || 1.0;
+ const rawMax = Math.min(sliderMax, maxLeverageFor(c, l, floor));
+ const steppedMax = Math.floor(rawMax * 10) / 10;
+ const next = Math.max(1.0, steppedMax);
+
+ if (!Number.isFinite(next) || next <= 1.0) {
+ slider.value = numIn.value = "1.0";
+ updatePreview();
+ toast(`HF floor ${fmt(floor, 2)} only allows 1.0x leverage.`, "info");
+ return;
+ }
+
+ slider.value = numIn.value = next.toFixed(1);
+ updatePreview();
+ toast(`Safe Max set to ${next.toFixed(1)}x for HF floor ${fmt(floor, 2)}.`, "success");
+}
+
function updatePreview() {
const slider = $("leverage-slider") as HTMLInputElement;
const numIn = $("leverage-input") as HTMLInputElement;
@@ -2023,12 +2079,21 @@ function toggleTheme() {
$("theme-toggle").addEventListener("click", toggleTheme);
document.getElementById("mobile-theme-toggle")?.addEventListener("click", toggleTheme);
+renderSafeMaxFloorInput();
+$("safe-max-floor").addEventListener("change", saveSafeMaxFloorFromInput);
+$("safe-max-floor").addEventListener("keydown", (e) => {
+ if ((e as KeyboardEvent).key === "Enter") {
+ (e.target as HTMLInputElement).blur();
+ }
+});
+
// Settings dropdown toggle
$("settings-btn").addEventListener("click", (e) => {
e.stopPropagation();
$("settings-dropdown").classList.toggle("hidden");
$("pool-dropdown").classList.add("hidden");
});
+$("settings-dropdown").addEventListener("click", (e) => e.stopPropagation());
// Network toggle
$("network-toggle").addEventListener("click", () => {
@@ -2201,6 +2266,7 @@ async function refreshAddFundsBalance() {
}
($("leverage-slider") as HTMLInputElement).addEventListener("input", updatePreview);
+$("safe-max-btn").addEventListener("click", applySafeMaxLeverage);
// Live preview while typing (no clamping so user can type multi-digit numbers like "10")
($("leverage-input") as HTMLInputElement).addEventListener("input", () => {
const numIn = $("leverage-input") as HTMLInputElement;
diff --git a/frontend/src/style.css b/frontend/src/style.css
index 0d4348f..cce685f 100644
--- a/frontend/src/style.css
+++ b/frontend/src/style.css
@@ -217,6 +217,16 @@ body {
}
.settings-dropdown-item:hover { background: var(--tab-hover-bg); color: var(--text); }
.settings-badge { font-size: 10px; padding: 1px 6px; border-radius: 99px; background: var(--surface2); color: var(--text-3); }
+.settings-field {
+ display: flex; align-items: center; justify-content: space-between; gap: 10px;
+ padding: 8px 12px; color: var(--text-2); font-size: 13px; font-weight: 500;
+}
+.settings-number {
+ width: 66px; padding: 4px 6px;
+ background: var(--input-bg); border: 1px solid var(--border); border-radius: var(--r-xs);
+ color: var(--text); font-size: 12px; text-align: right; outline: none;
+}
+.settings-number:focus { border-color: var(--primary); box-shadow: 0 0 0 3px var(--primary-glow); }
/* Legacy sidebar — hidden by default, used for mobile drawer */
.sidebar { display: none; }
@@ -606,6 +616,7 @@ main { flex: 1; max-width: 1200px; width: 100%; margin: 0 auto; padding: 20px 24
.slider::-webkit-slider-thumb:hover { background: var(--primary-h); box-shadow: 0 0 0 6px var(--slider-glow-h); }
.leverage-num-input { width: 68px; padding: 5px 6px; font-size: 15px; text-align: right; padding-right: 4px; }
.slider-value-x { font-family: var(--mono); font-size: 16px; font-weight: 700; color: var(--text-3); margin-left: -4px; }
+.safe-max-btn { flex-shrink: 0; padding: 5px 10px; font-size: 12px; }
/* Slider risk zones */
.slider-zones {
@@ -1192,6 +1203,7 @@ main { flex: 1; max-width: 1200px; width: 100%; margin: 0 auto; padding: 20px 24
.swap-asset-select { width: 100%; }
.slider-row { flex-wrap: wrap; }
.leverage-num-input { width: 100%; margin-top: 4px; }
+ .safe-max-btn { width: 100%; justify-content: center; }
#wallet-area { flex-wrap: wrap; gap: 6px; }
.btn-connect { font-size: 12px; padding: 5px 10px; }
.slippage-opt { padding: 2px 6px; font-size: 11px; }