feat(ui): implement smooth Apple-style spring animation for toggle switch

Motivation:
The previous toggle switch animation was abrupt and rigid. The clickable area also spanned the entire row width, which led to unintentional clicks.

Implementation:
- CSS: Replaced static transitions with cubic-bezier spring physics, hardware-accelerated transforms (translate), and pressed-state squish feedback.
- JS: Wrapped the switch in a constrained flex container, replacing the full-width <label> wrapper to fix the clickable area.
This commit is contained in:
科林_Cyril Pilgrim
2026-03-05 12:06:48 +08:00
parent 39fe305b3b
commit 910d2792ce
2 changed files with 48 additions and 26 deletions
+47 -25
View File
@@ -473,59 +473,81 @@
}
/* ============================================================
SWITCH / TOGGLE
SWITCH / TOGGLE — Ultra-smooth Apple-style spring animation
============================================================ */
.llm-pm-switch {
position: relative;
display: inline-block;
width: 42px;
height: 22px;
width: 44px;
height: 24px;
flex-shrink: 0;
cursor: pointer;
}
.llm-pm-switch input {
opacity: 0;
width: 0;
height: 0;
position: absolute;
}
.llm-pm-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(255, 255, 255, 0.08);
transition: var(--glass-transition);
border-radius: 22px;
border: 1px solid var(--glass-border);
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.3);
inset: 0;
border-radius: 24px;
border: 1.5px solid rgba(255, 255, 255, 0.1);
background-color: rgba(255, 255, 255, 0.1);
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.35);
transition:
background-color 0.35s cubic-bezier(0.4, 0, 0.2, 1),
border-color 0.35s cubic-bezier(0.4, 0, 0.2, 1),
box-shadow 0.4s cubic-bezier(0.4, 0, 0.2, 1);
will-change: background-color;
}
/* The thumb (white circle) — spring physics */
.llm-pm-slider:before {
position: absolute;
content: "";
height: 16px;
width: 16px;
position: absolute;
height: 18px;
width: 18px;
left: 2px;
bottom: 2px;
background-color: var(--glass-text-secondary);
transition: var(--glass-transition);
top: 50%;
transform: translateY(-50%) translateX(0px);
background: rgba(200, 200, 210, 0.9);
border-radius: 50%;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4), 0 0 0 0.5px rgba(0, 0, 0, 0.15);
/* cubic-bezier(0.34, 1.56, 0.64, 1): fast shoot, slight overshoot, then settle — the "Apple spring" */
transition:
transform 0.38s cubic-bezier(0.34, 1.56, 0.64, 1),
width 0.18s cubic-bezier(0.4, 0, 0.2, 1),
background 0.25s ease;
will-change: transform;
}
/* Checked (ON) state */
.llm-pm-switch input:checked+.llm-pm-slider {
background: linear-gradient(135deg, var(--glass-accent), #8b5cf6);
background-color: var(--glass-accent);
border-color: transparent;
box-shadow: 0 0 12px var(--glass-accent-glow),
box-shadow:
0 0 0 3px rgba(99, 102, 241, 0.18),
0 0 14px rgba(99, 102, 241, 0.4),
inset 0 1px 2px rgba(255, 255, 255, 0.15);
}
.llm-pm-switch input:checked+.llm-pm-slider:before {
transform: translateX(20px);
background-color: white;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.25);
transform: translateY(-50%) translateX(20px);
background: white;
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.3);
}
/* Press feedback — thumb squishes horizontally on mousedown */
.llm-pm-switch:active .llm-pm-slider:before {
width: 22px;
}
.llm-pm-switch:active input:checked+.llm-pm-slider:before {
transform: translateY(-50%) translateX(18px);
}
/* ============================================================
+1 -1
View File
@@ -911,7 +911,7 @@ class ProviderManager {
// Build DOM
const fields = [
$el("div.llm-pm-field", [
$el("label", [
$el("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", fontSize: "0.8em", fontWeight: "600", color: "var(--glass-text-secondary)", textTransform: "uppercase", letterSpacing: "0.06em", marginBottom: "8px" } }, [
$el("span", t("provider_name")),
$el("div", { style: { display: "flex", alignItems: "center", gap: "8px" } }, [
$el("span", { style: { fontSize: "0.8em", fontWeight: "normal" } }, t("enable_nodes")),