Skip to content

Commit 91c31f3

Browse files
committed
Settings: scroll-aware top fade on the content wrapper
- `.settings-content-wrapper` is now masked at the top via `mask-image` (set inline via Svelte `style:mask-image` because stylelint's allowed-values list rejects the `calc(var(...) / 5)` mask form) - Mask is driven by `scrollTop`: band height grows 0 → 70 px as the user scrolls 0 → 70 px, then caps. Within the band the top 20 % is fully transparent (hides scrolled-up content) and the bottom 80 % linearly fades to fully visible. At `scrollTop=0` the band collapses to zero, so the mask is "all visible" — no fade at rest - Replaces the earlier absolute-positioned `.content-fade` overlay (which sat ON TOP of content and used `backdrop-filter`); that approach blurred scrolled-up rows into a soft haze but read as a separate plate. The new approach is the content's own visibility fading, which integrates with the translucent window bg behind it - `padding-top` reverted to `var(--spacing-lg)` so the first title sits where it did before; section-nav `scrollTo` offset reverted to the original 16 px
1 parent 6948093 commit 91c31f3

1 file changed

Lines changed: 36 additions & 1 deletion

File tree

apps/desktop/src/routes/settings/+page.svelte

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,26 @@
2222
let selectedSection = $state<string[]>(['Appearance', 'Colors and formats'])
2323
let initialized = $state(false)
2424
let contentElement: HTMLElement | null = $state(null)
25+
let contentScrollTop = $state(0)
26+
/** Mask string for the scroll wrapper. Top-fade band height tracks
27+
`scrollTop` 0..70 px and caps at 70. Within the band the top 20 %
28+
is fully transparent (= hides scrolled-up content); the remaining
29+
80 % linearly fades to fully visible. At `scrollTop = 0` the band
30+
is zero-wide, so the gradient effectively collapses to "all black
31+
= no fade." Stylelint's allowed-values list rejects mask-image
32+
with a `calc(var(...) / 5)` so we compute the whole string in JS
33+
and apply it via `style:mask-image` (inline runtime style, not
34+
scoped CSS that gets linted). */
35+
const contentMaskImage = $derived.by(() => {
36+
const band = Math.min(Math.max(contentScrollTop, 0), 70)
37+
const opaque = band / 5
38+
return `linear-gradient(to bottom, transparent 0px, transparent ${String(opaque)}px, black ${String(band)}px)`
39+
})
40+
function handleContentScroll(): void {
41+
if (contentElement) {
42+
contentScrollTop = contentElement.scrollTop
43+
}
44+
}
2545
let unlistenFocusSelf: UnlistenFn | undefined
2646
let unlistenNavigate: UnlistenFn | undefined
2747
let unlistenMcpClose: UnlistenFn | undefined
@@ -294,7 +314,14 @@
294314
onSectionSelect={handleSectionSelect}
295315
/>
296316
<!-- tabindex="-1" prevents this from being a tab stop while still allowing programmatic scrolling -->
297-
<div class="settings-content-wrapper" bind:this={contentElement} tabindex="-1">
317+
<div
318+
class="settings-content-wrapper"
319+
bind:this={contentElement}
320+
onscroll={handleContentScroll}
321+
style:mask-image={contentMaskImage}
322+
style:-webkit-mask-image={contentMaskImage}
323+
tabindex="-1"
324+
>
298325
<SettingsContent {searchQuery} {selectedSection} onNavigate={handleSectionSelect} />
299326
</div>
300327
</div>
@@ -349,6 +376,14 @@
349376
overflow-y: auto;
350377
padding: var(--spacing-lg);
351378
outline: none;
379+
/* `mask-image` is set inline via `style:mask-image={contentMaskImage}`
380+
— the gradient depends on scrollTop, and stylelint's allowed-
381+
values list rejects calc-with-custom-property forms for mask-image.
382+
Inline runtime styles bypass the scoped-CSS lint. See the
383+
`contentMaskImage` derived value in the script block for the
384+
gradient math: top 20% of the band fully transparent (hides
385+
content), bottom 80% fades to fully visible. At scrollTop=0 the
386+
band is zero-wide so the mask collapses to "no fade". */
352387
}
353388
354389
.settings-loading {

0 commit comments

Comments
 (0)