v0.6.3: hero detail bottom drawer that pushes role tags, matches page summary dedupe.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -4,6 +4,13 @@
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.6.3] - 2026-07-30
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- 英雄详情改为自底部上滑抽屉:高度展开顶起定位标签(标签不挂在面板上);点击空白或下滑可关闭;去掉标签行全宽底与打开时的网格遮罩条。
|
||||||
|
- 比赛页去掉筛选侧栏重复的「共 X 场 · 第 X/X 页」(仅保留列表底部分页区统计)。
|
||||||
|
|
||||||
## [0.6.2] - 2026-07-30
|
## [0.6.2] - 2026-07-30
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ from shared.paths import (
|
|||||||
from seo_prerender import DEFAULT_SITE_ORIGIN, write_seo_bundle
|
from seo_prerender import DEFAULT_SITE_ORIGIN, write_seo_bundle
|
||||||
from serve_relations import WEB_DIR, build_payload
|
from serve_relations import WEB_DIR, build_payload
|
||||||
|
|
||||||
SITE_VERSION = "0.6.2"
|
SITE_VERSION = "0.6.3"
|
||||||
DEFAULT_OSS_BASE = "https://climperor.oss-cn-shanghai.aliyuncs.com"
|
DEFAULT_OSS_BASE = "https://climperor.oss-cn-shanghai.aliyuncs.com"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+208
-6
@@ -425,6 +425,15 @@ function onHeroClick(key) {
|
|||||||
|
|
||||||
function clearHeroDetail() {
|
function clearHeroDetail() {
|
||||||
if (!state.selectedKey) return false;
|
if (!state.selectedKey) return false;
|
||||||
|
const drawer = $("#detail-drawer");
|
||||||
|
if (
|
||||||
|
drawer &&
|
||||||
|
!drawer.classList.contains("is-collapsed") &&
|
||||||
|
!detailDrawerClosing
|
||||||
|
) {
|
||||||
|
collapseDetailDrawerThenClear();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
state.selectedKey = null;
|
state.selectedKey = null;
|
||||||
state.inspect = null;
|
state.inspect = null;
|
||||||
syncStateToUrl();
|
syncStateToUrl();
|
||||||
@@ -432,6 +441,188 @@ function clearHeroDetail() {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let detailDrawerDrag = null;
|
||||||
|
let detailDrawerSuppressClickUntil = 0;
|
||||||
|
let detailDrawerClosing = false;
|
||||||
|
|
||||||
|
function detailDrawerFullHeight() {
|
||||||
|
const panel = $("#detail");
|
||||||
|
if (panel && panel.offsetHeight) return panel.offsetHeight;
|
||||||
|
return Math.min(720, Math.round((window.innerHeight || 800) * 0.88));
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncDetailDrawer() {
|
||||||
|
const drawer = $("#detail-drawer");
|
||||||
|
if (!drawer) return;
|
||||||
|
const open = state.page === "heroes" && !!state.selectedKey && !detailDrawerClosing;
|
||||||
|
drawer.setAttribute("aria-hidden", open ? "false" : "true");
|
||||||
|
document.body.classList.toggle("detail-drawer-open", open);
|
||||||
|
if (!open) {
|
||||||
|
drawer.classList.add("is-collapsed");
|
||||||
|
drawer.style.maxHeight = "";
|
||||||
|
resetDetailDrawerDrag();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
drawer.classList.remove("is-collapsed");
|
||||||
|
drawer.style.maxHeight = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetDetailDrawerDrag() {
|
||||||
|
const drawer = $("#detail-drawer");
|
||||||
|
if (drawer) {
|
||||||
|
drawer.classList.remove("is-dragging");
|
||||||
|
if (!detailDrawerClosing) drawer.style.maxHeight = "";
|
||||||
|
}
|
||||||
|
detailDrawerDrag = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setDetailDrawerDragOffset(dy) {
|
||||||
|
const drawer = $("#detail-drawer");
|
||||||
|
if (!drawer) return;
|
||||||
|
const full = detailDrawerDrag?.fullH || detailDrawerFullHeight() || 1;
|
||||||
|
const h = Math.max(0, full - Math.max(0, dy));
|
||||||
|
drawer.style.maxHeight = `${h}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function endDetailDrawerDrag(dy, velocityY) {
|
||||||
|
const drawer = $("#detail-drawer");
|
||||||
|
if (!drawer) {
|
||||||
|
resetDetailDrawerDrag();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const full = detailDrawerDrag?.fullH || detailDrawerFullHeight() || 1;
|
||||||
|
const shouldClose = dy > Math.min(140, full * 0.22) || velocityY > 0.85;
|
||||||
|
if (detailDrawerDrag?.moved) {
|
||||||
|
detailDrawerSuppressClickUntil = performance.now() + 280;
|
||||||
|
}
|
||||||
|
if (shouldClose) {
|
||||||
|
collapseDetailDrawerThenClear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
drawer.classList.remove("is-dragging");
|
||||||
|
drawer.style.maxHeight = "";
|
||||||
|
detailDrawerDrag = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function collapseDetailDrawerThenClear() {
|
||||||
|
const drawer = $("#detail-drawer");
|
||||||
|
if (!state.selectedKey) return;
|
||||||
|
if (!drawer || detailDrawerClosing) {
|
||||||
|
state.selectedKey = null;
|
||||||
|
state.inspect = null;
|
||||||
|
syncStateToUrl();
|
||||||
|
render();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
detailDrawerClosing = true;
|
||||||
|
const fromH = drawer.offsetHeight || detailDrawerFullHeight();
|
||||||
|
drawer.classList.add("is-dragging");
|
||||||
|
drawer.style.maxHeight = `${fromH}px`;
|
||||||
|
void drawer.offsetHeight;
|
||||||
|
drawer.classList.remove("is-dragging");
|
||||||
|
drawer.classList.add("is-collapsed");
|
||||||
|
drawer.style.maxHeight = "0px";
|
||||||
|
document.body.classList.remove("detail-drawer-open");
|
||||||
|
let done = false;
|
||||||
|
const finish = () => {
|
||||||
|
if (done) return;
|
||||||
|
done = true;
|
||||||
|
drawer.removeEventListener("transitionend", finish);
|
||||||
|
detailDrawerClosing = false;
|
||||||
|
drawer.style.maxHeight = "";
|
||||||
|
resetDetailDrawerDrag();
|
||||||
|
state.selectedKey = null;
|
||||||
|
state.inspect = null;
|
||||||
|
syncStateToUrl();
|
||||||
|
render();
|
||||||
|
};
|
||||||
|
const reduce =
|
||||||
|
typeof matchMedia === "function" &&
|
||||||
|
matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||||||
|
if (reduce) {
|
||||||
|
finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
drawer.addEventListener("transitionend", finish);
|
||||||
|
window.setTimeout(finish, 360);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** True when the event target is drawer chrome (handle / head / empty padding), not interactive content. */
|
||||||
|
function isDetailDrawerDismissTarget(target) {
|
||||||
|
if (!(target instanceof Element)) return false;
|
||||||
|
if (target.closest(".detail-drawer-handle, .detail-head")) return true;
|
||||||
|
if (target.closest("button, a, input, select, textarea, label, video, .hero-inspect, .detail-tab-body, .detail-tabs, .detail-abilities, .detail-items, .detail-skills-layout, .detail-items-layout, .detail-stats-layout, .detail-matchups-layout, .detail-matches-layout, .detail-streamers-layout, .detail-patches-layout")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return target.id === "detail" || target.classList.contains("detail-drawer-panel");
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindDetailDrawerGestures() {
|
||||||
|
const drawer = $("#detail-drawer");
|
||||||
|
const panel = $("#detail");
|
||||||
|
if (!drawer || !panel) return;
|
||||||
|
|
||||||
|
panel.addEventListener("click", (e) => {
|
||||||
|
if (state.page !== "heroes" || !state.selectedKey) return;
|
||||||
|
if (performance.now() < detailDrawerSuppressClickUntil) return;
|
||||||
|
if (!isDetailDrawerDismissTarget(e.target)) return;
|
||||||
|
collapseDetailDrawerThenClear();
|
||||||
|
});
|
||||||
|
|
||||||
|
const onPointerDown = (e) => {
|
||||||
|
if (state.page !== "heroes" || !state.selectedKey) return;
|
||||||
|
if (e.button != null && e.button !== 0) return;
|
||||||
|
if (!e.target.closest(".detail-drawer-handle, .detail-head")) return;
|
||||||
|
const fullH = detailDrawerFullHeight();
|
||||||
|
detailDrawerDrag = {
|
||||||
|
pointerId: e.pointerId,
|
||||||
|
startY: e.clientY,
|
||||||
|
lastY: e.clientY,
|
||||||
|
lastT: performance.now(),
|
||||||
|
dy: 0,
|
||||||
|
velocityY: 0,
|
||||||
|
moved: false,
|
||||||
|
fullH,
|
||||||
|
};
|
||||||
|
drawer.classList.add("is-dragging");
|
||||||
|
drawer.style.maxHeight = `${fullH}px`;
|
||||||
|
try {
|
||||||
|
drawer.setPointerCapture(e.pointerId);
|
||||||
|
} catch (_) {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onPointerMove = (e) => {
|
||||||
|
if (!detailDrawerDrag || e.pointerId !== detailDrawerDrag.pointerId) return;
|
||||||
|
const now = performance.now();
|
||||||
|
const dy = e.clientY - detailDrawerDrag.startY;
|
||||||
|
const dt = Math.max(1, now - detailDrawerDrag.lastT);
|
||||||
|
detailDrawerDrag.velocityY = (e.clientY - detailDrawerDrag.lastY) / dt;
|
||||||
|
detailDrawerDrag.lastY = e.clientY;
|
||||||
|
detailDrawerDrag.lastT = now;
|
||||||
|
detailDrawerDrag.dy = dy;
|
||||||
|
if (Math.abs(dy) > 4) detailDrawerDrag.moved = true;
|
||||||
|
if (dy > 0) setDetailDrawerDragOffset(dy);
|
||||||
|
else setDetailDrawerDragOffset(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onPointerUp = (e) => {
|
||||||
|
if (!detailDrawerDrag || e.pointerId !== detailDrawerDrag.pointerId) return;
|
||||||
|
const { dy, velocityY, moved } = detailDrawerDrag;
|
||||||
|
if (!moved && dy <= 0) {
|
||||||
|
resetDetailDrawerDrag();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
endDetailDrawerDrag(Math.max(0, dy), velocityY);
|
||||||
|
};
|
||||||
|
|
||||||
|
drawer.addEventListener("pointerdown", onPointerDown);
|
||||||
|
drawer.addEventListener("pointermove", onPointerMove);
|
||||||
|
drawer.addEventListener("pointerup", onPointerUp);
|
||||||
|
drawer.addEventListener("pointercancel", onPointerUp);
|
||||||
|
}
|
||||||
|
|
||||||
function renderTagbar() {
|
function renderTagbar() {
|
||||||
const bar = $("#tagbar");
|
const bar = $("#tagbar");
|
||||||
if (!bar) return;
|
if (!bar) return;
|
||||||
@@ -2957,18 +3148,27 @@ function renderDetail() {
|
|||||||
root.innerHTML = "";
|
root.innerHTML = "";
|
||||||
|
|
||||||
if (!state.selectedKey) {
|
if (!state.selectedKey) {
|
||||||
root.classList.add("empty", "is-collapsed");
|
root.classList.add("empty");
|
||||||
|
root.innerHTML = "";
|
||||||
|
syncDetailDrawer();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
root.classList.remove("is-collapsed", "empty");
|
root.classList.remove("empty");
|
||||||
const hero = heroByKey(state.selectedKey);
|
const hero = heroByKey(state.selectedKey);
|
||||||
if (!hero) {
|
if (!hero) {
|
||||||
root.classList.add("empty");
|
root.classList.add("empty");
|
||||||
root.textContent = "未找到英雄数据";
|
root.textContent = "未找到英雄数据";
|
||||||
|
syncDetailDrawer();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handle = document.createElement("button");
|
||||||
|
handle.type = "button";
|
||||||
|
handle.className = "detail-drawer-handle";
|
||||||
|
handle.setAttribute("aria-label", "下滑或点击关闭详情");
|
||||||
|
root.appendChild(handle);
|
||||||
|
|
||||||
const head = document.createElement("div");
|
const head = document.createElement("div");
|
||||||
head.className = "detail-head";
|
head.className = "detail-head";
|
||||||
const title = document.createElement("h2");
|
const title = document.createElement("h2");
|
||||||
@@ -3112,6 +3312,7 @@ function renderDetail() {
|
|||||||
layout.appendChild(renderHeroInspect(state.selectedKey));
|
layout.appendChild(renderHeroInspect(state.selectedKey));
|
||||||
root.appendChild(layout);
|
root.appendChild(layout);
|
||||||
}
|
}
|
||||||
|
syncDetailDrawer();
|
||||||
}
|
}
|
||||||
|
|
||||||
function shopCatalog() {
|
function shopCatalog() {
|
||||||
@@ -3932,7 +4133,6 @@ function renderMatchesPage() {
|
|||||||
const body = $("#matches-body");
|
const body = $("#matches-body");
|
||||||
const originsEl = $("#matches-origins");
|
const originsEl = $("#matches-origins");
|
||||||
const playersEl = $("#matches-players");
|
const playersEl = $("#matches-players");
|
||||||
const sub = $("#matches-sub");
|
|
||||||
if (!body) return;
|
if (!body) return;
|
||||||
|
|
||||||
const origin = MATCH_ORIGIN_FILTERS.some((o) => o.id === state.matchesOrigin)
|
const origin = MATCH_ORIGIN_FILTERS.some((o) => o.id === state.matchesOrigin)
|
||||||
@@ -4039,7 +4239,6 @@ function renderMatchesPage() {
|
|||||||
const summary = total
|
const summary = total
|
||||||
? `共 ${total} 场 · 第 ${page}/${pageCount} 页`
|
? `共 ${total} 场 · 第 ${page}/${pageCount} 页`
|
||||||
: "";
|
: "";
|
||||||
if (sub) sub.textContent = summary;
|
|
||||||
|
|
||||||
if (!rows.length) {
|
if (!rows.length) {
|
||||||
body.innerHTML = state.matchesPlayerId
|
body.innerHTML = state.matchesPlayerId
|
||||||
@@ -5608,6 +5807,7 @@ function syncChrome() {
|
|||||||
document.querySelectorAll(".main-tab").forEach((btn) => {
|
document.querySelectorAll(".main-tab").forEach((btn) => {
|
||||||
btn.classList.toggle("active", btn.dataset.page === state.page);
|
btn.classList.toggle("active", btn.dataset.page === state.page);
|
||||||
});
|
});
|
||||||
|
const detailDrawer = $("#detail-drawer");
|
||||||
const heroesTb = $("#heroes-toolbar");
|
const heroesTb = $("#heroes-toolbar");
|
||||||
const heroSearch = $("#q");
|
const heroSearch = $("#q");
|
||||||
const itemSearch = $("#q-item");
|
const itemSearch = $("#q-item");
|
||||||
@@ -5619,8 +5819,8 @@ function syncChrome() {
|
|||||||
const mechanicsView = $("#mechanics-view");
|
const mechanicsView = $("#mechanics-view");
|
||||||
const itemsView = $("#items-view");
|
const itemsView = $("#items-view");
|
||||||
const patchesView = $("#patches-view");
|
const patchesView = $("#patches-view");
|
||||||
const detail = $("#detail");
|
|
||||||
if (heroesTb) heroesTb.classList.toggle("hidden", state.page !== "heroes");
|
if (heroesTb) heroesTb.classList.toggle("hidden", state.page !== "heroes");
|
||||||
|
if (detailDrawer) detailDrawer.classList.toggle("hidden", state.page !== "heroes");
|
||||||
if (heroSearch) heroSearch.classList.toggle("hidden", state.page !== "heroes");
|
if (heroSearch) heroSearch.classList.toggle("hidden", state.page !== "heroes");
|
||||||
if (itemSearch) itemSearch.classList.toggle("hidden", state.page !== "items");
|
if (itemSearch) itemSearch.classList.toggle("hidden", state.page !== "items");
|
||||||
if (heroesView) heroesView.classList.toggle("hidden", state.page !== "heroes");
|
if (heroesView) heroesView.classList.toggle("hidden", state.page !== "heroes");
|
||||||
@@ -5631,7 +5831,8 @@ function syncChrome() {
|
|||||||
if (mechanicsView) mechanicsView.classList.toggle("hidden", state.page !== "mechanics");
|
if (mechanicsView) mechanicsView.classList.toggle("hidden", state.page !== "mechanics");
|
||||||
if (itemsView) itemsView.classList.toggle("hidden", state.page !== "items");
|
if (itemsView) itemsView.classList.toggle("hidden", state.page !== "items");
|
||||||
if (patchesView) patchesView.classList.toggle("hidden", state.page !== "patches");
|
if (patchesView) patchesView.classList.toggle("hidden", state.page !== "patches");
|
||||||
if (detail) detail.classList.toggle("hidden", state.page !== "heroes");
|
// Drawer open animation needs detail content laid out first; heroes path syncs in renderDetail.
|
||||||
|
if (state.page !== "heroes") syncDetailDrawer();
|
||||||
}
|
}
|
||||||
|
|
||||||
function render() {
|
function render() {
|
||||||
@@ -5842,6 +6043,7 @@ function bindHeroesDismiss() {
|
|||||||
clearHeroDetail();
|
clearHeroDetail();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
bindDetailDrawerGestures();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/* Local defaults; production export overwrites via export_relations_site.py. */
|
/* Local defaults; production export overwrites via export_relations_site.py. */
|
||||||
var SITE_VERSION = "0.6.2";
|
var SITE_VERSION = "0.6.3";
|
||||||
var SITE_ORIGIN = "";
|
var SITE_ORIGIN = "";
|
||||||
var ABILITY_VIDEO_BASE = "";
|
var ABILITY_VIDEO_BASE = "";
|
||||||
var STATIC_ASSET_BASE = "";
|
var STATIC_ASSET_BASE = "";
|
||||||
|
|||||||
@@ -43,8 +43,8 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<link rel="icon" href="/ui-icon/dota2_logo.png" type="image/png" />
|
<link rel="icon" href="/ui-icon/dota2_logo.png" type="image/png" />
|
||||||
<link rel="stylesheet" href="/style.css?v=0.6.2" />
|
<link rel="stylesheet" href="/style.css?v=0.6.3" />
|
||||||
<script src="/mobile-gate.js?v=0.6.2"></script>
|
<script src="/mobile-gate.js?v=0.6.3"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1 class="sr-only">DOTA2 上分帝</h1>
|
<h1 class="sr-only">DOTA2 上分帝</h1>
|
||||||
@@ -150,7 +150,6 @@
|
|||||||
</h2>
|
</h2>
|
||||||
<div class="matches-origins" id="matches-origins" role="tablist" aria-label="职业或国服"></div>
|
<div class="matches-origins" id="matches-origins" role="tablist" aria-label="职业或国服"></div>
|
||||||
<div class="matches-players" id="matches-players" role="tablist" aria-label="选择选手"></div>
|
<div class="matches-players" id="matches-players" role="tablist" aria-label="选择选手"></div>
|
||||||
<p class="rankings-sub page-sub" id="matches-sub"></p>
|
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -229,11 +228,13 @@
|
|||||||
<div class="patches-detail" id="patches-detail"></div>
|
<div class="patches-detail" id="patches-detail"></div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<section class="detail" id="detail" aria-live="polite"></section>
|
<div id="detail-drawer" class="detail-drawer is-collapsed hidden" aria-hidden="true">
|
||||||
|
<section class="detail detail-drawer-panel" id="detail" role="dialog" aria-modal="true" aria-live="polite"></section>
|
||||||
|
</div>
|
||||||
<footer class="heroes-site-foot" id="heroes-site-foot" aria-hidden="true"></footer>
|
<footer class="heroes-site-foot" id="heroes-site-foot" aria-hidden="true"></footer>
|
||||||
|
|
||||||
<script src="/config.js?v=0.6.2"></script>
|
<script src="/config.js?v=0.6.3"></script>
|
||||||
<script src="/router.js?v=0.6.2"></script>
|
<script src="/router.js?v=0.6.3"></script>
|
||||||
<script src="/app.js?v=0.6.2"></script>
|
<script src="/app.js?v=0.6.3"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+83
-28
@@ -318,7 +318,12 @@ body {
|
|||||||
min-height: 52px;
|
min-height: 52px;
|
||||||
margin: 0 14px var(--space-sm);
|
margin: 0 14px var(--space-sm);
|
||||||
padding: var(--space-sm) var(--space-md);
|
padding: var(--space-sm) var(--space-md);
|
||||||
background: rgba(8, 12, 20, 0.62);
|
position: relative;
|
||||||
|
z-index: 3;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
body.detail-drawer-open .hero-role-toolbar {
|
||||||
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
.toolbar.hidden,
|
.toolbar.hidden,
|
||||||
.board.hidden,
|
.board.hidden,
|
||||||
@@ -464,8 +469,7 @@ body {
|
|||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Hero picker fills leftover space above role toolbar + pinned detail;
|
/* Hero grid above role tags; detail sits below tags and grows upward to push them. */
|
||||||
scrolls inside when the grid is taller than the region. */
|
|
||||||
#heroes-view {
|
#heroes-view {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
@@ -473,25 +477,81 @@ body {
|
|||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Heroes-only (#detail hidden on items page): pin to bottom with a gap above
|
/* In-flow bottom drawer: height animates so the tagbar above is pushed up. */
|
||||||
the hero grid + role toolbar. One shared fixed height across all tabs so
|
.detail-drawer {
|
||||||
技能 ↔ 核心装备 ↔ 被克装备 does not jump; tall tabs (走势/对位/…) scroll
|
--detail-fixed-h: min(720px, 88dvh);
|
||||||
inside .detail-tab-body. Prefer a taller detail on large viewports — the
|
flex: 0 0 auto;
|
||||||
hero grid only needs ~one board of portraits (+ scroll), not half the screen. */
|
position: relative;
|
||||||
.detail {
|
z-index: 2;
|
||||||
--detail-fixed-h: min(640px, calc(100dvh - clamp(280px, 32vh, 380px) - 7rem));
|
width: 100%;
|
||||||
|
max-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: max-height 320ms cubic-bezier(0.32, 0.72, 0, 1);
|
||||||
|
}
|
||||||
|
.detail-drawer:not(.is-collapsed) {
|
||||||
|
max-height: var(--detail-fixed-h);
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
.detail-drawer.is-dragging {
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
.detail-drawer-panel.detail {
|
||||||
|
--detail-fixed-h: inherit;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex: 0 0 auto;
|
width: 100%;
|
||||||
margin: 0 14px var(--space-md);
|
margin: 0;
|
||||||
padding: var(--space-md) var(--space-lg);
|
padding: var(--space-sm) var(--space-lg) var(--space-md);
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
border-radius: var(--radius-lg);
|
border-bottom: none;
|
||||||
background: var(--panel-soft);
|
border-radius: var(--radius-xl) var(--radius-xl) 0 0;
|
||||||
min-height: 0;
|
background: var(--panel);
|
||||||
overflow: hidden;
|
box-shadow: 0 -12px 36px rgba(0, 0, 0, 0.35);
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.detail-drawer-handle {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
align-self: center;
|
||||||
|
width: 56px;
|
||||||
|
height: 20px;
|
||||||
|
margin: 0 auto 6px;
|
||||||
|
border: 0;
|
||||||
|
padding: 0;
|
||||||
|
background: transparent;
|
||||||
|
cursor: grab;
|
||||||
|
touch-action: none;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.detail-drawer-handle::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
width: 44px;
|
||||||
|
height: 4px;
|
||||||
|
border-radius: var(--radius-full);
|
||||||
|
background: rgba(140, 170, 210, 0.38);
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
.detail-drawer.is-dragging .detail-drawer-handle {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
body.detail-drawer-open {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.detail-drawer {
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
.heroes-site-foot {
|
.heroes-site-foot {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
@@ -510,22 +570,17 @@ body {
|
|||||||
min-height: var(--detail-fixed-h);
|
min-height: var(--detail-fixed-h);
|
||||||
max-height: var(--detail-fixed-h);
|
max-height: var(--detail-fixed-h);
|
||||||
}
|
}
|
||||||
.detail:not(.empty) > * {
|
.detail:not(.empty) > *:not(.detail-drawer-handle) {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: none;
|
max-width: none;
|
||||||
}
|
}
|
||||||
.detail.is-collapsed {
|
.detail.empty {
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.detail.empty:not(.is-collapsed) {
|
|
||||||
height: auto;
|
height: auto;
|
||||||
min-height: 64px;
|
min-height: 0;
|
||||||
max-height: none;
|
max-height: none;
|
||||||
justify-content: center;
|
padding: 0;
|
||||||
color: var(--muted);
|
border: none;
|
||||||
font-size: var(--fs-body);
|
box-shadow: none;
|
||||||
line-height: var(--lh-body);
|
|
||||||
letter-spacing: 0.04em;
|
|
||||||
}
|
}
|
||||||
.detail-head {
|
.detail-head {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
Reference in New Issue
Block a user