diff --git a/packages/app/src/settings/navigation.tsx b/packages/app/src/settings/navigation.tsx index e16d716f8dd..0f2c32bdaeb 100644 --- a/packages/app/src/settings/navigation.tsx +++ b/packages/app/src/settings/navigation.tsx @@ -116,7 +116,7 @@ export function SettingsNavigation(props: { - + {(group) => (
diff --git a/packages/app/src/settings/search.css b/packages/app/src/settings/search.css index bbfae1035ae..efa563b35e9 100644 --- a/packages/app/src/settings/search.css +++ b/packages/app/src/settings/search.css @@ -4,6 +4,11 @@ min-height: 0; } +/* Only results scroll inside the search; with the nav list below it, the list absorbs a short sidebar. */ +.settings-search[data-empty="true"] { + flex-shrink: 0; +} + .settings-search[data-empty="true"] [data-slot="text-input-v2-icon-button"] { visibility: hidden; } diff --git a/packages/app/src/settings/settings.css b/packages/app/src/settings/settings.css index 0bf4fa9a61d..3f61e0ac2de 100644 --- a/packages/app/src/settings/settings.css +++ b/packages/app/src/settings/settings.css @@ -85,9 +85,54 @@ flex-direction: column; gap: 16px; overflow-y: auto; + overscroll-behavior: contain; min-height: 0; } +@property --settings-nav-fade-start { + syntax: ""; + inherits: false; + initial-value: 0px; +} + +@property --settings-nav-fade-end { + syntax: ""; + inherits: false; + initial-value: 0px; +} + +/* Each edge fades in once content is hidden behind it. A list that does not overflow has no scroll timeline, so it keeps both lengths at 0. */ +@supports (animation-timeline: scroll()) { + .settings-nav-groups { + mask-image: linear-gradient( + to bottom, + transparent, + #000 var(--settings-nav-fade-start), + #000 calc(100% - var(--settings-nav-fade-end)), + transparent + ); + animation: + settings-nav-fade-start linear both, + settings-nav-fade-end linear both; + animation-timeline: scroll(self), scroll(self); + animation-range: + 0 24px, + calc(100% - 24px) 100%; + } +} + +@keyframes settings-nav-fade-start { + to { + --settings-nav-fade-start: 24px; + } +} + +@keyframes settings-nav-fade-end { + from { + --settings-nav-fade-end: 24px; + } +} + .settings-nav-groups [data-slot="tabs-v2-trigger-wrapper"] { width: 100%; height: 28px; diff --git a/packages/app/src/settings/shell.tsx b/packages/app/src/settings/shell.tsx index c7f68832bc9..659bc4067a5 100644 --- a/packages/app/src/settings/shell.tsx +++ b/packages/app/src/settings/shell.tsx @@ -179,7 +179,7 @@ export function SettingsScreen() { if (!root || event.ctrlKey || !event.deltaY || !(event.target instanceof Element)) return // The page scrolls inside its panel, so a wheel over the gutters or sidebar would otherwise do nothing. - if (event.target.closest(".settings-content") || scrollsNatively(event.target, root, event.deltaY)) return + if (event.target.closest(".settings-content") || insideScroller(event.target, root)) return const panel = root.querySelector(".settings-content > .settings-panel") if (!panel) return @@ -210,11 +210,12 @@ export function SettingsScreen() { ) } -function scrollsNatively(target: Element, root: Element, deltaY: number) { +// An overflowing scroller owns the wheel even at its edges, so reaching the end of the nav list does not scroll the page. +function insideScroller(target: Element, root: Element) { for (let node: Element | null = target; node && node !== root; node = node.parentElement) { if (!/auto|scroll/.test(getComputedStyle(node).overflowY)) continue - if (deltaY < 0 ? node.scrollTop > 0 : node.scrollHeight - node.clientHeight - node.scrollTop > 1) return true + if (node.scrollHeight - node.clientHeight > 1) return true } return false