diff --git a/web/navi.css b/web/navi.css index 35e9fc9..c65644f 100644 --- a/web/navi.css +++ b/web/navi.css @@ -2490,3 +2490,21 @@ html[data-theme] .minimal-home.navi-dragging * { user-select: none !important; -webkit-user-select: none !important; } + +/* ═══ v91:滑动切换的淡出淡入只作用于"页框内的元素" ═══ + 用户反馈:① 此前整个内容框(含顶部页签行)一起淡出淡入 —— 页签不该闪; + ② 页签行不联动(实测 .minimal-headings-row scrollWidth 1306 > clientWidth 828, + 可横向滚动但 scrollLeft 恒为 0;切到第 7/8 个分组时选中页签在视口外)。 + 做法:淡出/淡入只打 .minimal-section-inner.selected 的直接子元素 + (= .section-items 卡片 + .minimal-widget-wrap),框体与页签行不动。 + 用 keyframes 而不是 transition:这些子元素自带 `transition: all`, + 改 transition 会覆盖它们原有的 hover 过渡。 + 页签行跟随由 navi.js 的 swipeRevealTab() 负责(滚动居中,reduced-motion 下瞬时)。 */ +@keyframes navi-section-out { to { opacity: .12; } } +@keyframes navi-section-in { from { opacity: .12; } to { opacity: 1; } } +html[data-theme] .minimal-home.navi-switching .minimal-section-inner.selected > * { + animation: navi-section-out 90ms ease forwards !important; +} +html[data-theme] .minimal-home.navi-fadein .minimal-section-inner.selected > * { + animation: navi-section-in 150ms ease !important; +} diff --git a/web/navi.js b/web/navi.js index 440fd2d..80ab880 100644 --- a/web/navi.js +++ b/web/navi.js @@ -655,23 +655,39 @@ } // 到头时的一次性提示:轻微回弹(transform / 仅 8px),禁用动效时退化为淡闪 function swipeEdgeHint(dir) { - const frame = swipeFrameEl(); - if (!frame || frame.dataset.naviHint === '1') return; - frame.dataset.naviHint = '1'; - setTimeout(() => { delete frame.dataset.naviHint; }, 300); + // 只动"页框内容"(.minimal-section-inner),不动页签行与框体本身 + const el = document.querySelector('.minimal-section-inner.selected'); + if (!el || el.dataset.naviHint === '1') return; + el.dataset.naviHint = '1'; + setTimeout(() => { delete el.dataset.naviHint; }, 320); if (reducedMotion()) { - frame.style.transition = 'opacity 110ms ease'; - frame.style.opacity = '0.75'; - setTimeout(() => { frame.style.opacity = ''; frame.style.transition = ''; }, 120); + el.style.transition = 'opacity 110ms ease'; + el.style.opacity = '0.78'; + setTimeout(() => { el.style.opacity = ''; el.style.transition = ''; }, 120); return; } - frame.style.transition = 'transform 150ms cubic-bezier(.22,1,.36,1)'; - frame.style.transform = 'translateX(' + (dir > 0 ? -8 : 8) + 'px)'; + el.style.transition = 'transform 150ms cubic-bezier(.22,1,.36,1)'; + el.style.transform = 'translateX(' + (dir > 0 ? -8 : 8) + 'px)'; setTimeout(() => { - frame.style.transform = ''; - setTimeout(() => { frame.style.transition = ''; }, 170); + el.style.transform = ''; + setTimeout(() => { el.style.transition = ''; }, 170); }, 150); } + // 页签行跟随:实测 .minimal-headings-row 可横向滚动(scrollWidth 1306 > clientWidth 828), + // 但 scrollLeft 恒为 0 —— 切到第 7/8 个分组时选中页签落在视口外, + // 用户会以为"手势没生效 / 页签没联动"。这里把选中项滚进可视区(居中)。 + function swipeRevealTab(tab) { + const row = tab && tab.closest ? tab.closest('.minimal-headings-row') : null; + if (!row) return; + const tr = tab.getBoundingClientRect(), rr = row.getBoundingClientRect(); + if (tr.left >= rr.left - 1 && tr.right <= rr.right + 1) return; // 已完整可见 + const delta = (tr.left + tr.width / 2) - (rr.left + rr.width / 2); + const max = Math.max(0, row.scrollWidth - row.clientWidth); + const left = Math.max(0, Math.min(max, row.scrollLeft + delta)); + if (reducedMotion() || !row.scrollTo) row.scrollLeft = left; + else row.scrollTo({ left: left, behavior: 'smooth' }); + } + async function swipeSwitch(dir) { if (swipeBusy || Date.now() < swipeCoolUntil) return false; const tabs = swipeTabs(); @@ -681,13 +697,11 @@ if (j < 0 || j >= tabs.length) { swipeEdgeHint(dir); return false; } // clamp swipeBusy = true; swipeCoolUntil = Date.now() + SWIPE.cooldown; - const frame = swipeFrameEl(); - const fade = frame && !reducedMotion(); - if (fade) { - frame.style.transition = 'opacity 90ms ease'; - frame.style.opacity = '0.28'; - await later(90); - } + // 淡出/淡入只作用于"页框内的元素"(.minimal-section-inner.selected > *), + // 页签行与框体本身保持不动(用户反馈:此前整框一起闪,页签也被带着淡出了)。 + const home = document.querySelector('.minimal-home'); + const fade = !!home && !reducedMotion(); + if (fade) { home.classList.add('navi-switching'); await later(90); } // 关键:这是我们自己发起的切换 click —— 必须绕开"防误触"拦截, // 否则会出现"手势收到了、手势条件也满足,但页面没切"的怪象(实测踩过)。 swipeEatClick = 0; @@ -695,10 +709,11 @@ try { tabs[j].click(); } finally { swipeSelfClick = false; } await later(70); if (fade) { - frame.style.transition = 'opacity 150ms ease'; - frame.style.opacity = '1'; - setTimeout(() => { frame.style.transition = ''; frame.style.opacity = ''; }, 190); + home.classList.remove('navi-switching'); + home.classList.add('navi-fadein'); + setTimeout(() => home.classList.remove('navi-fadein'), 220); } + swipeRevealTab(tabs[j]); // 页签行跟随选中项 swipeBusy = false; return true; } @@ -764,6 +779,12 @@ } }, { passive: true }); + // 用户自己点页签时,页签行同样跟随(把选中项滚进可视区) + frame.addEventListener('click', (evt) => { + const tab = evt.target && evt.target.closest ? evt.target.closest('.minimal-section-heading.headings') : null; + if (tab) setTimeout(() => swipeRevealTab(tab), 80); + }, { passive: true }); + if (!swipeDocBound) { swipeDocBound = true; // 滑动后吞掉紧随的一次 click,避免误开卡片里的链接