Revert "feat(ui): Smooth fading out on scroll, style fixes (#11683)"
This reverts commit e445dc0746.
This commit is contained in:
@@ -5,7 +5,6 @@ import { Select } from "@opencode-ai/ui/select"
|
|||||||
import { Switch } from "@opencode-ai/ui/switch"
|
import { Switch } from "@opencode-ai/ui/switch"
|
||||||
import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme"
|
import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
import { ScrollFade } from "@opencode-ai/ui/scroll-fade"
|
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { usePlatform } from "@/context/platform"
|
import { usePlatform } from "@/context/platform"
|
||||||
import { useSettings, monoFontFamily } from "@/context/settings"
|
import { useSettings, monoFontFamily } from "@/context/settings"
|
||||||
@@ -131,12 +130,7 @@ export const SettingsGeneral: Component = () => {
|
|||||||
const soundOptions = [...SOUND_OPTIONS]
|
const soundOptions = [...SOUND_OPTIONS]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollFade
|
<div class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10">
|
||||||
direction="vertical"
|
|
||||||
fadeStartSize={0}
|
|
||||||
fadeEndSize={16}
|
|
||||||
class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10"
|
|
||||||
>
|
|
||||||
<div class="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-raised-stronger-non-alpha)_calc(100%_-_24px),transparent)]">
|
<div class="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-raised-stronger-non-alpha)_calc(100%_-_24px),transparent)]">
|
||||||
<div class="flex flex-col gap-1 pt-6 pb-8">
|
<div class="flex flex-col gap-1 pt-6 pb-8">
|
||||||
<h2 class="text-16-medium text-text-strong">{language.t("settings.tab.general")}</h2>
|
<h2 class="text-16-medium text-text-strong">{language.t("settings.tab.general")}</h2>
|
||||||
@@ -417,7 +411,7 @@ export const SettingsGeneral: Component = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ScrollFade>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { Icon } from "@opencode-ai/ui/icon"
|
|||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
import { TextField } from "@opencode-ai/ui/text-field"
|
import { TextField } from "@opencode-ai/ui/text-field"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
import { ScrollFade } from "@opencode-ai/ui/scroll-fade"
|
|
||||||
import fuzzysort from "fuzzysort"
|
import fuzzysort from "fuzzysort"
|
||||||
import { formatKeybind, parseKeybind, useCommand } from "@/context/command"
|
import { formatKeybind, parseKeybind, useCommand } from "@/context/command"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
@@ -353,12 +352,7 @@ export const SettingsKeybinds: Component = () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollFade
|
<div class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10">
|
||||||
direction="vertical"
|
|
||||||
fadeStartSize={0}
|
|
||||||
fadeEndSize={16}
|
|
||||||
class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10"
|
|
||||||
>
|
|
||||||
<div class="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-raised-stronger-non-alpha)_calc(100%_-_24px),transparent)]">
|
<div class="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-raised-stronger-non-alpha)_calc(100%_-_24px),transparent)]">
|
||||||
<div class="flex flex-col gap-4 pt-6 pb-6 max-w-[720px]">
|
<div class="flex flex-col gap-4 pt-6 pb-6 max-w-[720px]">
|
||||||
<div class="flex items-center justify-between gap-4">
|
<div class="flex items-center justify-between gap-4">
|
||||||
@@ -436,6 +430,6 @@ export const SettingsKeybinds: Component = () => {
|
|||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</ScrollFade>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import { type Component, For, Show } from "solid-js"
|
|||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { useModels } from "@/context/models"
|
import { useModels } from "@/context/models"
|
||||||
import { popularProviders } from "@/hooks/use-providers"
|
import { popularProviders } from "@/hooks/use-providers"
|
||||||
import { ScrollFade } from "@opencode-ai/ui/scroll-fade"
|
|
||||||
|
|
||||||
type ModelItem = ReturnType<ReturnType<typeof useModels>["list"]>[number]
|
type ModelItem = ReturnType<ReturnType<typeof useModels>["list"]>[number]
|
||||||
|
|
||||||
@@ -40,12 +39,7 @@ export const SettingsModels: Component = () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollFade
|
<div class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10">
|
||||||
direction="vertical"
|
|
||||||
fadeStartSize={0}
|
|
||||||
fadeEndSize={16}
|
|
||||||
class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10"
|
|
||||||
>
|
|
||||||
<div class="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-raised-stronger-non-alpha)_calc(100%_-_24px),transparent)]">
|
<div class="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-raised-stronger-non-alpha)_calc(100%_-_24px),transparent)]">
|
||||||
<div class="flex flex-col gap-4 pt-6 pb-6 max-w-[720px]">
|
<div class="flex flex-col gap-4 pt-6 pb-6 max-w-[720px]">
|
||||||
<h2 class="text-16-medium text-text-strong">{language.t("settings.models.title")}</h2>
|
<h2 class="text-16-medium text-text-strong">{language.t("settings.models.title")}</h2>
|
||||||
@@ -131,6 +125,6 @@ export const SettingsModels: Component = () => {
|
|||||||
</Show>
|
</Show>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</ScrollFade>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import { useGlobalSync } from "@/context/global-sync"
|
|||||||
import { DialogConnectProvider } from "./dialog-connect-provider"
|
import { DialogConnectProvider } from "./dialog-connect-provider"
|
||||||
import { DialogSelectProvider } from "./dialog-select-provider"
|
import { DialogSelectProvider } from "./dialog-select-provider"
|
||||||
import { DialogCustomProvider } from "./dialog-custom-provider"
|
import { DialogCustomProvider } from "./dialog-custom-provider"
|
||||||
import { ScrollFade } from "@opencode-ai/ui/scroll-fade"
|
|
||||||
|
|
||||||
type ProviderSource = "env" | "api" | "config" | "custom"
|
type ProviderSource = "env" | "api" | "config" | "custom"
|
||||||
type ProviderMeta = { source?: ProviderSource }
|
type ProviderMeta = { source?: ProviderSource }
|
||||||
@@ -116,12 +115,7 @@ export const SettingsProviders: Component = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollFade
|
<div class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10">
|
||||||
direction="vertical"
|
|
||||||
fadeStartSize={0}
|
|
||||||
fadeEndSize={16}
|
|
||||||
class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10"
|
|
||||||
>
|
|
||||||
<div class="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-raised-stronger-non-alpha)_calc(100%_-_24px),transparent)]">
|
<div class="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-raised-stronger-non-alpha)_calc(100%_-_24px),transparent)]">
|
||||||
<div class="flex flex-col gap-1 pt-6 pb-8 max-w-[720px]">
|
<div class="flex flex-col gap-1 pt-6 pb-8 max-w-[720px]">
|
||||||
<h2 class="text-16-medium text-text-strong">{language.t("settings.providers.title")}</h2>
|
<h2 class="text-16-medium text-text-strong">{language.t("settings.providers.title")}</h2>
|
||||||
@@ -267,6 +261,6 @@ export const SettingsProviders: Component = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ScrollFade>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,25 @@
|
|||||||
|
@property --bottom-fade {
|
||||||
|
syntax: "<length>";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes scroll {
|
||||||
|
0% {
|
||||||
|
--bottom-fade: 20px;
|
||||||
|
}
|
||||||
|
90% {
|
||||||
|
--bottom-fade: 20px;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
--bottom-fade: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[data-component="list"] {
|
[data-component="list"] {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 8px;
|
gap: 12px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding: 0 12px;
|
padding: 0 12px;
|
||||||
|
|
||||||
@@ -19,9 +37,7 @@
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
transition-property: opacity;
|
transition: opacity 0.15s ease;
|
||||||
transition-duration: var(--transition-duration);
|
|
||||||
transition-timing-function: var(--transition-easing);
|
|
||||||
|
|
||||||
&:hover:not(:disabled),
|
&:hover:not(:disabled),
|
||||||
&:focus-visible:not(:disabled),
|
&:focus-visible:not(:disabled),
|
||||||
@@ -72,9 +88,7 @@
|
|||||||
height: 20px;
|
height: 20px;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
transition-property: opacity;
|
transition: opacity 0.15s ease;
|
||||||
transition-duration: var(--transition-duration);
|
|
||||||
transition-timing-function: var(--transition-easing);
|
|
||||||
|
|
||||||
&:hover:not(:disabled),
|
&:hover:not(:disabled),
|
||||||
&:focus-visible:not(:disabled),
|
&:focus-visible:not(:disabled),
|
||||||
@@ -117,6 +131,15 @@
|
|||||||
gap: 12px;
|
gap: 12px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overscroll-behavior: contain;
|
overscroll-behavior: contain;
|
||||||
|
mask: linear-gradient(to bottom, #ffff calc(100% - var(--bottom-fade)), #0000);
|
||||||
|
animation: scroll;
|
||||||
|
animation-timeline: --scroll;
|
||||||
|
scroll-timeline: --scroll y;
|
||||||
|
scrollbar-width: none;
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
[data-slot="list-empty-state"] {
|
[data-slot="list-empty-state"] {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -192,9 +215,7 @@
|
|||||||
background: linear-gradient(to bottom, var(--surface-raised-stronger-non-alpha), transparent);
|
background: linear-gradient(to bottom, var(--surface-raised-stronger-non-alpha), transparent);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition-property: opacity;
|
transition: opacity 0.15s ease;
|
||||||
transition-duration: var(--transition-duration);
|
|
||||||
transition-timing-function: var(--transition-easing);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&[data-stuck="true"]::after {
|
&[data-stuck="true"]::after {
|
||||||
@@ -230,22 +251,17 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
aspect-ratio: 1 / 1;
|
aspect-ratio: 1/1;
|
||||||
[data-component="icon"] {
|
[data-component="icon"] {
|
||||||
color: var(--icon-strong-base);
|
color: var(--icon-strong-base);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[name="check"] {
|
|
||||||
color: var(--icon-strong-base);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="list-item-active-icon"] {
|
[data-slot="list-item-active-icon"] {
|
||||||
display: none;
|
display: none;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
aspect-ratio: 1 / 1;
|
aspect-ratio: 1/1;
|
||||||
[data-component="icon"] {
|
[data-component="icon"] {
|
||||||
color: var(--icon-strong-base);
|
color: var(--icon-strong-base);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { useI18n } from "../context/i18n"
|
|||||||
import { Icon, type IconProps } from "./icon"
|
import { Icon, type IconProps } from "./icon"
|
||||||
import { IconButton } from "./icon-button"
|
import { IconButton } from "./icon-button"
|
||||||
import { TextField } from "./text-field"
|
import { TextField } from "./text-field"
|
||||||
import { ScrollFade } from "./scroll-fade"
|
|
||||||
|
|
||||||
function findByKey(container: HTMLElement, key: string) {
|
function findByKey(container: HTMLElement, key: string) {
|
||||||
const nodes = container.querySelectorAll<HTMLElement>('[data-slot="list-item"][data-key]')
|
const nodes = container.querySelectorAll<HTMLElement>('[data-slot="list-item"][data-key]')
|
||||||
@@ -280,7 +279,7 @@ export function List<T>(props: ListProps<T> & { ref?: (ref: ListRef) => void })
|
|||||||
{searchAction()}
|
{searchAction()}
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<ScrollFade ref={setScrollRef} direction="vertical" fadeStartSize={0} fadeEndSize={20} data-slot="list-scroll">
|
<div ref={setScrollRef} data-slot="list-scroll">
|
||||||
<Show
|
<Show
|
||||||
when={flat().length > 0 || showAdd()}
|
when={flat().length > 0 || showAdd()}
|
||||||
fallback={
|
fallback={
|
||||||
@@ -353,7 +352,7 @@ export function List<T>(props: ListProps<T> & { ref?: (ref: ListRef) => void })
|
|||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</Show>
|
</Show>
|
||||||
</ScrollFade>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,82 +0,0 @@
|
|||||||
[data-component="scroll-fade"] {
|
|
||||||
overflow: auto;
|
|
||||||
overscroll-behavior: contain;
|
|
||||||
scrollbar-width: none;
|
|
||||||
box-sizing: border-box;
|
|
||||||
color: inherit;
|
|
||||||
font: inherit;
|
|
||||||
-ms-overflow-style: none;
|
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&[data-direction="horizontal"] {
|
|
||||||
overflow-x: auto;
|
|
||||||
overflow-y: hidden;
|
|
||||||
|
|
||||||
/* Both fades */
|
|
||||||
&[data-fade-start][data-fade-end] {
|
|
||||||
mask-image: linear-gradient(
|
|
||||||
to right,
|
|
||||||
transparent,
|
|
||||||
black var(--scroll-fade-start),
|
|
||||||
black calc(100% - var(--scroll-fade-end)),
|
|
||||||
transparent
|
|
||||||
);
|
|
||||||
-webkit-mask-image: linear-gradient(
|
|
||||||
to right,
|
|
||||||
transparent,
|
|
||||||
black var(--scroll-fade-start),
|
|
||||||
black calc(100% - var(--scroll-fade-end)),
|
|
||||||
transparent
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Only start fade */
|
|
||||||
&[data-fade-start]:not([data-fade-end]) {
|
|
||||||
mask-image: linear-gradient(to right, transparent, black var(--scroll-fade-start), black 100%);
|
|
||||||
-webkit-mask-image: linear-gradient(to right, transparent, black var(--scroll-fade-start), black 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Only end fade */
|
|
||||||
&:not([data-fade-start])[data-fade-end] {
|
|
||||||
mask-image: linear-gradient(to right, black 0%, black calc(100% - var(--scroll-fade-end)), transparent);
|
|
||||||
-webkit-mask-image: linear-gradient(to right, black 0%, black calc(100% - var(--scroll-fade-end)), transparent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&[data-direction="vertical"] {
|
|
||||||
overflow-y: auto;
|
|
||||||
overflow-x: hidden;
|
|
||||||
|
|
||||||
&[data-fade-start][data-fade-end] {
|
|
||||||
mask-image: linear-gradient(
|
|
||||||
to bottom,
|
|
||||||
transparent,
|
|
||||||
black var(--scroll-fade-start),
|
|
||||||
black calc(100% - var(--scroll-fade-end)),
|
|
||||||
transparent
|
|
||||||
);
|
|
||||||
-webkit-mask-image: linear-gradient(
|
|
||||||
to bottom,
|
|
||||||
transparent,
|
|
||||||
black var(--scroll-fade-start),
|
|
||||||
black calc(100% - var(--scroll-fade-end)),
|
|
||||||
transparent
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Only start fade */
|
|
||||||
&[data-fade-start]:not([data-fade-end]) {
|
|
||||||
mask-image: linear-gradient(to bottom, transparent, black var(--scroll-fade-start), black 100%);
|
|
||||||
-webkit-mask-image: linear-gradient(to bottom, transparent, black var(--scroll-fade-start), black 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Only end fade */
|
|
||||||
&:not([data-fade-start])[data-fade-end] {
|
|
||||||
mask-image: linear-gradient(to bottom, black 0%, black calc(100% - var(--scroll-fade-end)), transparent);
|
|
||||||
-webkit-mask-image: linear-gradient(to bottom, black 0%, black calc(100% - var(--scroll-fade-end)), transparent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,206 +0,0 @@
|
|||||||
import { type JSX, createEffect, createSignal, onCleanup, onMount, splitProps } from "solid-js"
|
|
||||||
|
|
||||||
export interface ScrollFadeProps extends JSX.HTMLAttributes<HTMLDivElement> {
|
|
||||||
direction?: "horizontal" | "vertical"
|
|
||||||
fadeStartSize?: number
|
|
||||||
fadeEndSize?: number
|
|
||||||
trackTransformSelector?: string
|
|
||||||
ref?: (el: HTMLDivElement) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ScrollFade(props: ScrollFadeProps) {
|
|
||||||
const [local, others] = splitProps(props, [
|
|
||||||
"children",
|
|
||||||
"direction",
|
|
||||||
"fadeStartSize",
|
|
||||||
"fadeEndSize",
|
|
||||||
"trackTransformSelector",
|
|
||||||
"class",
|
|
||||||
"style",
|
|
||||||
"ref",
|
|
||||||
])
|
|
||||||
|
|
||||||
const direction = () => local.direction ?? "vertical"
|
|
||||||
const fadeStartSize = () => local.fadeStartSize ?? 20
|
|
||||||
const fadeEndSize = () => local.fadeEndSize ?? 20
|
|
||||||
|
|
||||||
const getTransformOffset = (element: Element): number => {
|
|
||||||
const style = getComputedStyle(element)
|
|
||||||
const transform = style.transform
|
|
||||||
if (!transform || transform === "none") return 0
|
|
||||||
|
|
||||||
const match = transform.match(/matrix(?:3d)?\(([^)]+)\)/)
|
|
||||||
if (!match) return 0
|
|
||||||
|
|
||||||
const values = match[1].split(",").map((v) => parseFloat(v.trim()))
|
|
||||||
const isHorizontal = direction() === "horizontal"
|
|
||||||
|
|
||||||
if (transform.startsWith("matrix3d")) {
|
|
||||||
return isHorizontal ? -(values[12] || 0) : -(values[13] || 0)
|
|
||||||
} else {
|
|
||||||
return isHorizontal ? -(values[4] || 0) : -(values[5] || 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let containerRef: HTMLDivElement | undefined
|
|
||||||
|
|
||||||
const [fadeStart, setFadeStart] = createSignal(0)
|
|
||||||
const [fadeEnd, setFadeEnd] = createSignal(0)
|
|
||||||
const [isScrollable, setIsScrollable] = createSignal(false)
|
|
||||||
|
|
||||||
let lastScrollPos = 0
|
|
||||||
let lastTransformPos = 0
|
|
||||||
let lastScrollSize = 0
|
|
||||||
let lastClientSize = 0
|
|
||||||
|
|
||||||
const updateFade = () => {
|
|
||||||
if (!containerRef) return
|
|
||||||
|
|
||||||
const isHorizontal = direction() === "horizontal"
|
|
||||||
const scrollPos = isHorizontal ? containerRef.scrollLeft : containerRef.scrollTop
|
|
||||||
const scrollSize = isHorizontal ? containerRef.scrollWidth : containerRef.scrollHeight
|
|
||||||
const clientSize = isHorizontal ? containerRef.clientWidth : containerRef.clientHeight
|
|
||||||
|
|
||||||
let transformPos = 0
|
|
||||||
if (local.trackTransformSelector) {
|
|
||||||
const transformElement = containerRef.querySelector(local.trackTransformSelector)
|
|
||||||
if (transformElement) {
|
|
||||||
transformPos = getTransformOffset(transformElement)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const effectiveScrollPos = Math.max(scrollPos, transformPos)
|
|
||||||
|
|
||||||
if (
|
|
||||||
effectiveScrollPos === lastScrollPos &&
|
|
||||||
transformPos === lastTransformPos &&
|
|
||||||
scrollSize === lastScrollSize &&
|
|
||||||
clientSize === lastClientSize
|
|
||||||
) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
lastScrollPos = effectiveScrollPos
|
|
||||||
lastTransformPos = transformPos
|
|
||||||
lastScrollSize = scrollSize
|
|
||||||
lastClientSize = clientSize
|
|
||||||
|
|
||||||
const maxScroll = scrollSize - clientSize
|
|
||||||
const canScroll = maxScroll > 1
|
|
||||||
|
|
||||||
setIsScrollable(canScroll)
|
|
||||||
|
|
||||||
if (!canScroll) {
|
|
||||||
setFadeStart(0)
|
|
||||||
setFadeEnd(0)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const progress = maxScroll > 0 ? effectiveScrollPos / maxScroll : 0
|
|
||||||
|
|
||||||
const startProgress = Math.min(progress / 0.1, 1)
|
|
||||||
setFadeStart(startProgress * fadeStartSize())
|
|
||||||
|
|
||||||
const endProgress = progress > 0.9 ? (1 - progress) / 0.1 : 1
|
|
||||||
setFadeEnd(Math.max(0, endProgress) * fadeEndSize())
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
if (!containerRef) return
|
|
||||||
|
|
||||||
updateFade()
|
|
||||||
|
|
||||||
let rafId: number | undefined
|
|
||||||
let isPolling = false
|
|
||||||
let pollTimeout: ReturnType<typeof setTimeout> | undefined
|
|
||||||
|
|
||||||
const startPolling = () => {
|
|
||||||
if (isPolling) return
|
|
||||||
isPolling = true
|
|
||||||
|
|
||||||
const pollScroll = () => {
|
|
||||||
updateFade()
|
|
||||||
rafId = requestAnimationFrame(pollScroll)
|
|
||||||
}
|
|
||||||
rafId = requestAnimationFrame(pollScroll)
|
|
||||||
}
|
|
||||||
|
|
||||||
const stopPolling = () => {
|
|
||||||
if (!isPolling) return
|
|
||||||
isPolling = false
|
|
||||||
if (rafId !== undefined) {
|
|
||||||
cancelAnimationFrame(rafId)
|
|
||||||
rafId = undefined
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const schedulePollingStop = () => {
|
|
||||||
if (pollTimeout !== undefined) clearTimeout(pollTimeout)
|
|
||||||
pollTimeout = setTimeout(stopPolling, 1000)
|
|
||||||
}
|
|
||||||
|
|
||||||
const onActivity = () => {
|
|
||||||
updateFade()
|
|
||||||
if (local.trackTransformSelector) {
|
|
||||||
startPolling()
|
|
||||||
schedulePollingStop()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
containerRef.addEventListener("scroll", onActivity, { passive: true })
|
|
||||||
|
|
||||||
const resizeObserver = new ResizeObserver(() => {
|
|
||||||
lastScrollSize = 0
|
|
||||||
lastClientSize = 0
|
|
||||||
onActivity()
|
|
||||||
})
|
|
||||||
resizeObserver.observe(containerRef)
|
|
||||||
|
|
||||||
const mutationObserver = new MutationObserver(() => {
|
|
||||||
lastScrollSize = 0
|
|
||||||
lastClientSize = 0
|
|
||||||
requestAnimationFrame(onActivity)
|
|
||||||
})
|
|
||||||
mutationObserver.observe(containerRef, {
|
|
||||||
childList: true,
|
|
||||||
subtree: true,
|
|
||||||
characterData: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
onCleanup(() => {
|
|
||||||
containerRef?.removeEventListener("scroll", onActivity)
|
|
||||||
resizeObserver.disconnect()
|
|
||||||
mutationObserver.disconnect()
|
|
||||||
stopPolling()
|
|
||||||
if (pollTimeout !== undefined) clearTimeout(pollTimeout)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
local.children
|
|
||||||
requestAnimationFrame(updateFade)
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={(el) => {
|
|
||||||
containerRef = el
|
|
||||||
local.ref?.(el)
|
|
||||||
}}
|
|
||||||
data-component="scroll-fade"
|
|
||||||
data-direction={direction()}
|
|
||||||
data-scrollable={isScrollable() || undefined}
|
|
||||||
data-fade-start={fadeStart() > 0 || undefined}
|
|
||||||
data-fade-end={fadeEnd() > 0 || undefined}
|
|
||||||
class={local.class}
|
|
||||||
style={{
|
|
||||||
...(typeof local.style === "object" ? local.style : {}),
|
|
||||||
"--scroll-fade-start": `${fadeStart()}px`,
|
|
||||||
"--scroll-fade-end": `${fadeEnd()}px`,
|
|
||||||
}}
|
|
||||||
{...others}
|
|
||||||
>
|
|
||||||
{local.children}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
import { type JSX, onCleanup, splitProps } from "solid-js"
|
|
||||||
import { ScrollFade, type ScrollFadeProps } from "./scroll-fade"
|
|
||||||
|
|
||||||
const SCROLL_SPEED = 60
|
|
||||||
const PAUSE_DURATION = 800
|
|
||||||
|
|
||||||
type ScrollAnimationState = {
|
|
||||||
rafId: number | null
|
|
||||||
startTime: number
|
|
||||||
running: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
const startScrollAnimation = (containerEl: HTMLElement): ScrollAnimationState | null => {
|
|
||||||
containerEl.offsetHeight
|
|
||||||
|
|
||||||
const extraWidth = containerEl.scrollWidth - containerEl.clientWidth
|
|
||||||
|
|
||||||
if (extraWidth <= 0) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const scrollDuration = (extraWidth / SCROLL_SPEED) * 1000
|
|
||||||
const totalDuration = PAUSE_DURATION + scrollDuration + PAUSE_DURATION + scrollDuration + PAUSE_DURATION
|
|
||||||
|
|
||||||
const state: ScrollAnimationState = {
|
|
||||||
rafId: null,
|
|
||||||
startTime: performance.now(),
|
|
||||||
running: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
const animate = (currentTime: number) => {
|
|
||||||
if (!state.running) return
|
|
||||||
|
|
||||||
const elapsed = currentTime - state.startTime
|
|
||||||
const progress = (elapsed % totalDuration) / totalDuration
|
|
||||||
|
|
||||||
const pausePercent = PAUSE_DURATION / totalDuration
|
|
||||||
const scrollPercent = scrollDuration / totalDuration
|
|
||||||
|
|
||||||
const pauseEnd1 = pausePercent
|
|
||||||
const scrollEnd1 = pauseEnd1 + scrollPercent
|
|
||||||
const pauseEnd2 = scrollEnd1 + pausePercent
|
|
||||||
const scrollEnd2 = pauseEnd2 + scrollPercent
|
|
||||||
|
|
||||||
let scrollPos = 0
|
|
||||||
|
|
||||||
if (progress < pauseEnd1) {
|
|
||||||
scrollPos = 0
|
|
||||||
} else if (progress < scrollEnd1) {
|
|
||||||
const scrollProgress = (progress - pauseEnd1) / scrollPercent
|
|
||||||
scrollPos = scrollProgress * extraWidth
|
|
||||||
} else if (progress < pauseEnd2) {
|
|
||||||
scrollPos = extraWidth
|
|
||||||
} else if (progress < scrollEnd2) {
|
|
||||||
const scrollProgress = (progress - pauseEnd2) / scrollPercent
|
|
||||||
scrollPos = extraWidth * (1 - scrollProgress)
|
|
||||||
} else {
|
|
||||||
scrollPos = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
containerEl.scrollLeft = scrollPos
|
|
||||||
state.rafId = requestAnimationFrame(animate)
|
|
||||||
}
|
|
||||||
|
|
||||||
state.rafId = requestAnimationFrame(animate)
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
|
|
||||||
const stopScrollAnimation = (state: ScrollAnimationState | null, containerEl?: HTMLElement) => {
|
|
||||||
if (state) {
|
|
||||||
state.running = false
|
|
||||||
if (state.rafId !== null) {
|
|
||||||
cancelAnimationFrame(state.rafId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (containerEl) {
|
|
||||||
containerEl.scrollLeft = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ScrollRevealProps extends Omit<ScrollFadeProps, "direction"> {
|
|
||||||
hoverDelay?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ScrollReveal(props: ScrollRevealProps) {
|
|
||||||
const [local, others] = splitProps(props, ["children", "hoverDelay", "ref"])
|
|
||||||
|
|
||||||
const hoverDelay = () => local.hoverDelay ?? 300
|
|
||||||
|
|
||||||
let containerRef: HTMLDivElement | undefined
|
|
||||||
let hoverTimeout: ReturnType<typeof setTimeout> | undefined
|
|
||||||
let scrollAnimationState: ScrollAnimationState | null = null
|
|
||||||
|
|
||||||
const handleMouseEnter: JSX.EventHandler<HTMLDivElement, MouseEvent> = () => {
|
|
||||||
hoverTimeout = setTimeout(() => {
|
|
||||||
if (!containerRef) return
|
|
||||||
|
|
||||||
containerRef.offsetHeight
|
|
||||||
|
|
||||||
const isScrollable = containerRef.scrollWidth > containerRef.clientWidth + 1
|
|
||||||
|
|
||||||
if (isScrollable) {
|
|
||||||
stopScrollAnimation(scrollAnimationState, containerRef)
|
|
||||||
scrollAnimationState = startScrollAnimation(containerRef)
|
|
||||||
}
|
|
||||||
}, hoverDelay())
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleMouseLeave: JSX.EventHandler<HTMLDivElement, MouseEvent> = () => {
|
|
||||||
if (hoverTimeout) {
|
|
||||||
clearTimeout(hoverTimeout)
|
|
||||||
hoverTimeout = undefined
|
|
||||||
}
|
|
||||||
stopScrollAnimation(scrollAnimationState, containerRef)
|
|
||||||
scrollAnimationState = null
|
|
||||||
}
|
|
||||||
|
|
||||||
onCleanup(() => {
|
|
||||||
if (hoverTimeout) {
|
|
||||||
clearTimeout(hoverTimeout)
|
|
||||||
}
|
|
||||||
stopScrollAnimation(scrollAnimationState, containerRef)
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ScrollFade
|
|
||||||
ref={(el) => {
|
|
||||||
containerRef = el
|
|
||||||
local.ref?.(el)
|
|
||||||
}}
|
|
||||||
fadeStartSize={8}
|
|
||||||
fadeEndSize={8}
|
|
||||||
direction="horizontal"
|
|
||||||
onMouseEnter={handleMouseEnter}
|
|
||||||
onMouseLeave={handleMouseLeave}
|
|
||||||
{...others}
|
|
||||||
>
|
|
||||||
{local.children}
|
|
||||||
</ScrollFade>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -41,7 +41,6 @@
|
|||||||
@import "../components/select.css" layer(components);
|
@import "../components/select.css" layer(components);
|
||||||
@import "../components/spinner.css" layer(components);
|
@import "../components/spinner.css" layer(components);
|
||||||
@import "../components/switch.css" layer(components);
|
@import "../components/switch.css" layer(components);
|
||||||
@import "../components/scroll-fade.css" layer(components);
|
|
||||||
@import "../components/session-review.css" layer(components);
|
@import "../components/session-review.css" layer(components);
|
||||||
@import "../components/session-turn.css" layer(components);
|
@import "../components/session-turn.css" layer(components);
|
||||||
@import "../components/sticky-accordion-header.css" layer(components);
|
@import "../components/sticky-accordion-header.css" layer(components);
|
||||||
|
|||||||
Reference in New Issue
Block a user