mirror of
https://github.com/pheralb/svgl.git
synced 2025-12-29 08:01:36 +08:00
Merge pull request #777 from SameerJS6/add-lina
feat: add Lina (Improved ScrollArea)
This commit is contained in:
+1
-1
@@ -69,7 +69,7 @@
|
|||||||
"@sveltejs/vite-plugin-svelte": "6.1.3",
|
"@sveltejs/vite-plugin-svelte": "6.1.3",
|
||||||
"@tailwindcss/vite": "4.1.12",
|
"@tailwindcss/vite": "4.1.12",
|
||||||
"@types/node": "24.3.0",
|
"@types/node": "24.3.0",
|
||||||
"bits-ui": "2.9.4",
|
"bits-ui": "2.11.3",
|
||||||
"clsx": "2.1.1",
|
"clsx": "2.1.1",
|
||||||
"eslint": "9.33.0",
|
"eslint": "9.33.0",
|
||||||
"eslint-config-prettier": "10.1.8",
|
"eslint-config-prettier": "10.1.8",
|
||||||
|
|||||||
Generated
+30
-20
@@ -79,8 +79,8 @@ importers:
|
|||||||
specifier: 24.3.0
|
specifier: 24.3.0
|
||||||
version: 24.3.0
|
version: 24.3.0
|
||||||
bits-ui:
|
bits-ui:
|
||||||
specifier: 2.9.4
|
specifier: 2.11.3
|
||||||
version: 2.9.4(@internationalized/date@3.8.2)(svelte@5.38.2)
|
version: 2.11.3(@internationalized/date@3.8.2)(svelte@5.38.2)
|
||||||
clsx:
|
clsx:
|
||||||
specifier: 2.1.1
|
specifier: 2.1.1
|
||||||
version: 2.1.1
|
version: 2.1.1
|
||||||
@@ -1127,8 +1127,8 @@ packages:
|
|||||||
balanced-match@1.0.2:
|
balanced-match@1.0.2:
|
||||||
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||||
|
|
||||||
bits-ui@2.9.4:
|
bits-ui@2.11.3:
|
||||||
resolution: {integrity: sha512-Cqn685P6DDuEyBZT/CWMyS5+8JAnYbctvoEVPcmiut+HUpG3SozVgjoDaUib5VG4ZYUKEi1FPwHxiXo9c6J0PA==}
|
resolution: {integrity: sha512-XhTuOgcgFatw2qxd+hIlu3EqfYu9R2FFMeJg8srZo3UVKBdOVWHbT0L3GBH46XsaESkTz3aEjU+BiGVoI/2rnQ==}
|
||||||
engines: {node: '>=20'}
|
engines: {node: '>=20'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@internationalized/date': ^3.8.1
|
'@internationalized/date': ^3.8.1
|
||||||
@@ -2661,6 +2661,11 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
svelte: ^5.7.0
|
svelte: ^5.7.0
|
||||||
|
|
||||||
|
runed@0.31.1:
|
||||||
|
resolution: {integrity: sha512-v3czcTnO+EJjiPvD4dwIqfTdHLZ8oH0zJheKqAHh9QMViY7Qb29UlAMRpX7ZtHh7AFqV60KmfxaJ9QMy+L1igQ==}
|
||||||
|
peerDependencies:
|
||||||
|
svelte: ^5.7.0
|
||||||
|
|
||||||
sade@1.8.1:
|
sade@1.8.1:
|
||||||
resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==}
|
resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@@ -2863,18 +2868,18 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
svelte: ^5.0.0
|
svelte: ^5.0.0
|
||||||
|
|
||||||
|
svelte-toolbelt@0.10.5:
|
||||||
|
resolution: {integrity: sha512-8e+eWTgxw1aiLxhDE8Rb1X6AoLitqpJz+WhAul2W7W58C8KoLoJQf1TgQdFPBiCPJ0Jg5y0Zi1uyua9em4VS0w==}
|
||||||
|
engines: {node: '>=18', pnpm: '>=8.7.0'}
|
||||||
|
peerDependencies:
|
||||||
|
svelte: ^5.30.2
|
||||||
|
|
||||||
svelte-toolbelt@0.7.1:
|
svelte-toolbelt@0.7.1:
|
||||||
resolution: {integrity: sha512-HcBOcR17Vx9bjaOceUvxkY3nGmbBmCBBbuWLLEWO6jtmWH8f/QoWmbyUfQZrpDINH39en1b8mptfPQT9VKQ1xQ==}
|
resolution: {integrity: sha512-HcBOcR17Vx9bjaOceUvxkY3nGmbBmCBBbuWLLEWO6jtmWH8f/QoWmbyUfQZrpDINH39en1b8mptfPQT9VKQ1xQ==}
|
||||||
engines: {node: '>=18', pnpm: '>=8.7.0'}
|
engines: {node: '>=18', pnpm: '>=8.7.0'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
svelte: ^5.0.0
|
svelte: ^5.0.0
|
||||||
|
|
||||||
svelte-toolbelt@0.9.3:
|
|
||||||
resolution: {integrity: sha512-HCSWxCtVmv+c6g1ACb8LTwHVbDqLKJvHpo6J8TaqwUme2hj9ATJCpjCPNISR1OCq2Q4U1KT41if9ON0isINQZw==}
|
|
||||||
engines: {node: '>=18', pnpm: '>=8.7.0'}
|
|
||||||
peerDependencies:
|
|
||||||
svelte: ^5.30.2
|
|
||||||
|
|
||||||
svelte@5.38.2:
|
svelte@5.38.2:
|
||||||
resolution: {integrity: sha512-iAcp/oFAWauVSGILdD67n7DiwgLHXZzWZIdzl7araRxu72jUr7PFAo2Iie7gXt0IbnlYvhxCb9GT3ZJUquO3PA==}
|
resolution: {integrity: sha512-iAcp/oFAWauVSGILdD67n7DiwgLHXZzWZIdzl7araRxu72jUr7PFAo2Iie7gXt0IbnlYvhxCb9GT3ZJUquO3PA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -4173,15 +4178,15 @@ snapshots:
|
|||||||
|
|
||||||
balanced-match@1.0.2: {}
|
balanced-match@1.0.2: {}
|
||||||
|
|
||||||
bits-ui@2.9.4(@internationalized/date@3.8.2)(svelte@5.38.2):
|
bits-ui@2.11.3(@internationalized/date@3.8.2)(svelte@5.38.2):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@floating-ui/core': 1.7.3
|
'@floating-ui/core': 1.7.3
|
||||||
'@floating-ui/dom': 1.7.4
|
'@floating-ui/dom': 1.7.4
|
||||||
'@internationalized/date': 3.8.2
|
'@internationalized/date': 3.8.2
|
||||||
esm-env: 1.2.2
|
esm-env: 1.2.2
|
||||||
runed: 0.29.2(svelte@5.38.2)
|
runed: 0.31.1(svelte@5.38.2)
|
||||||
svelte: 5.38.2
|
svelte: 5.38.2
|
||||||
svelte-toolbelt: 0.9.3(svelte@5.38.2)
|
svelte-toolbelt: 0.10.5(svelte@5.38.2)
|
||||||
tabbable: 6.2.0
|
tabbable: 6.2.0
|
||||||
|
|
||||||
body-parser@2.2.0:
|
body-parser@2.2.0:
|
||||||
@@ -5775,6 +5780,11 @@ snapshots:
|
|||||||
esm-env: 1.2.2
|
esm-env: 1.2.2
|
||||||
svelte: 5.38.2
|
svelte: 5.38.2
|
||||||
|
|
||||||
|
runed@0.31.1(svelte@5.38.2):
|
||||||
|
dependencies:
|
||||||
|
esm-env: 1.2.2
|
||||||
|
svelte: 5.38.2
|
||||||
|
|
||||||
sade@1.8.1:
|
sade@1.8.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
mri: 1.2.0
|
mri: 1.2.0
|
||||||
@@ -6023,6 +6033,13 @@ snapshots:
|
|||||||
runed: 0.28.0(svelte@5.38.2)
|
runed: 0.28.0(svelte@5.38.2)
|
||||||
svelte: 5.38.2
|
svelte: 5.38.2
|
||||||
|
|
||||||
|
svelte-toolbelt@0.10.5(svelte@5.38.2):
|
||||||
|
dependencies:
|
||||||
|
clsx: 2.1.1
|
||||||
|
runed: 0.29.2(svelte@5.38.2)
|
||||||
|
style-to-object: 1.0.9
|
||||||
|
svelte: 5.38.2
|
||||||
|
|
||||||
svelte-toolbelt@0.7.1(svelte@5.38.2):
|
svelte-toolbelt@0.7.1(svelte@5.38.2):
|
||||||
dependencies:
|
dependencies:
|
||||||
clsx: 2.1.1
|
clsx: 2.1.1
|
||||||
@@ -6030,13 +6047,6 @@ snapshots:
|
|||||||
style-to-object: 1.0.9
|
style-to-object: 1.0.9
|
||||||
svelte: 5.38.2
|
svelte: 5.38.2
|
||||||
|
|
||||||
svelte-toolbelt@0.9.3(svelte@5.38.2):
|
|
||||||
dependencies:
|
|
||||||
clsx: 2.1.1
|
|
||||||
runed: 0.29.2(svelte@5.38.2)
|
|
||||||
style-to-object: 1.0.9
|
|
||||||
svelte: 5.38.2
|
|
||||||
|
|
||||||
svelte@5.38.2:
|
svelte@5.38.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/remapping': 2.3.5
|
'@jridgewell/remapping': 2.3.5
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import ShowCategories from "@/components/layout/showCategories.svelte";
|
import ShowCategories from "@/components/layout/showCategories.svelte";
|
||||||
import ShowSidebarLinks from "@/components/layout/showSidebarLinks.svelte";
|
import ShowSidebarLinks from "@/components/layout/showSidebarLinks.svelte";
|
||||||
|
import ScrollArea from "@/components/ui/scroll-area/scroll-area.svelte";
|
||||||
|
import ScrollAreaScrollbar from "@/components/ui/scroll-area/scroll-area-scrollbar.svelte";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
@@ -11,17 +13,25 @@
|
|||||||
class={cn(
|
class={cn(
|
||||||
"md:fixed md:left-1 md:h-[calc(100vh-4.5rem)]",
|
"md:fixed md:left-1 md:h-[calc(100vh-4.5rem)]",
|
||||||
"overflow-x-hidden",
|
"overflow-x-hidden",
|
||||||
"w-54 pr-2 pl-2",
|
"w-54",
|
||||||
"hidden flex-col space-y-3 md:flex",
|
"hidden flex-col space-y-3 md:flex",
|
||||||
"bg-neutral-100 dark:bg-neutral-950",
|
"bg-neutral-100 dark:bg-neutral-950",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<nav class="flex flex-col space-y-0.5">
|
<nav class="flex flex-col space-y-0.5 px-2">
|
||||||
<ShowSidebarLinks />
|
<ShowSidebarLinks />
|
||||||
</nav>
|
</nav>
|
||||||
<Separator orientation="horizontal" />
|
<div class="px-2">
|
||||||
<nav class="relative flex h-auto flex-col space-y-0.5 overflow-y-auto">
|
<Separator orientation="horizontal" />
|
||||||
<ShowCategories />
|
</div>
|
||||||
|
<nav class="relative flex h-auto flex-col space-y-0.5 overflow-hidden">
|
||||||
|
<ScrollArea
|
||||||
|
maskClassName="before:from-neutral-100 after:from-neutral-100 dark:before:from-neutral-950 dark:after:from-neutral-950"
|
||||||
|
class="flex size-full flex-col gap-y-0.5 overflow-hidden px-2"
|
||||||
|
>
|
||||||
|
<ShowCategories />
|
||||||
|
<ScrollAreaScrollbar orientation="vertical" />
|
||||||
|
</ScrollArea>
|
||||||
</nav>
|
</nav>
|
||||||
</aside>
|
</aside>
|
||||||
<main class={cn("px-2 md:mr-4 md:ml-56 md:px-0", "overflow-hidden")}>
|
<main class={cn("px-2 md:mr-4 md:ml-56 md:px-0", "overflow-hidden")}>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
|
|
||||||
import ShowCategories from "@/components/layout/showCategories.svelte";
|
import ShowCategories from "@/components/layout/showCategories.svelte";
|
||||||
import ShowSidebarLinks from "@/components/layout/showSidebarLinks.svelte";
|
import ShowSidebarLinks from "@/components/layout/showSidebarLinks.svelte";
|
||||||
|
import ScrollArea from "@/components/ui/scroll-area/scroll-area.svelte";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: string;
|
className?: string;
|
||||||
@@ -33,10 +34,14 @@
|
|||||||
<h2 class="text-xl font-medium tracking-tight">svgl</h2>
|
<h2 class="text-xl font-medium tracking-tight">svgl</h2>
|
||||||
</Sheet.Title>
|
</Sheet.Title>
|
||||||
</Sheet.Header>
|
</Sheet.Header>
|
||||||
<nav class="flex flex-col space-y-0.5 overflow-y-auto px-3 pb-3">
|
<ScrollArea
|
||||||
|
class="flex size-full flex-col"
|
||||||
|
viewportClassName="pb-3 px-3 space-y-0.5"
|
||||||
|
maskClassName="before:from-white after:from-white dark:before:from-neutral-900 dark:after:from-neutral-900"
|
||||||
|
>
|
||||||
<ShowSidebarLinks />
|
<ShowSidebarLinks />
|
||||||
<Separator orientation="horizontal" class="my-3" />
|
<Separator orientation="horizontal" class="my-3" />
|
||||||
<ShowCategories />
|
<ShowCategories />
|
||||||
</nav>
|
</ScrollArea>
|
||||||
</Sheet.Content>
|
</Sheet.Content>
|
||||||
</Sheet.Root>
|
</Sheet.Root>
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Snippet } from "svelte";
|
import type { Snippet } from "svelte";
|
||||||
import { cn } from "@/utils/cn";
|
import { cn } from "@/utils/cn";
|
||||||
|
import ScrollArea from "./ui/scroll-area/scroll-area.svelte";
|
||||||
|
import { ScrollAreaScrollbar } from "./ui/scroll-area";
|
||||||
|
|
||||||
interface PageCardProps {
|
interface PageCardProps {
|
||||||
children: Snippet;
|
children: Snippet;
|
||||||
@@ -21,14 +23,16 @@
|
|||||||
containerClass,
|
containerClass,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div
|
<ScrollArea
|
||||||
|
maskHeight={50}
|
||||||
|
maskClassName="before:from-transparent after:from-white dark:before:from-[#0f0f0f] dark:after:from-[#0f0f0f]"
|
||||||
class={cn(
|
class={cn(
|
||||||
"max-h-[calc(100vh-4.5rem)] min-h-[calc(100vh-4.5rem)]",
|
"flex size-full max-h-[calc(100vh-4.5rem)] min-h-[calc(100vh-4.5rem)] flex-col",
|
||||||
"overflow-hidden overflow-y-auto",
|
|
||||||
contentCardClass,
|
contentCardClass,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
<ScrollAreaScrollbar orientation="vertical" class="relative z-99999" />
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</div>
|
</ScrollArea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import Scrollbar from "./scroll-area-scrollbar.svelte";
|
||||||
|
import Root from "./scroll-area.svelte";
|
||||||
|
import Mask from "./scroll-area-mask.svelte";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
Scrollbar,
|
||||||
|
Mask,
|
||||||
|
//
|
||||||
|
Root as ScrollArea,
|
||||||
|
Scrollbar as ScrollAreaScrollbar,
|
||||||
|
Mask as ScrollAreaMask,
|
||||||
|
};
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { cn } from "@/utils/cn";
|
||||||
|
type Mask = {
|
||||||
|
top: boolean;
|
||||||
|
bottom: boolean;
|
||||||
|
left: boolean;
|
||||||
|
right: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
let {
|
||||||
|
showMask,
|
||||||
|
maskHeight,
|
||||||
|
class: className = "",
|
||||||
|
}: {
|
||||||
|
showMask: Mask;
|
||||||
|
maskHeight: number;
|
||||||
|
class?: string;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
style={`--top-fade-height: ${showMask.top ? `${maskHeight}px` : "0px"}; --bottom-fade-height: ${showMask.bottom ? `${maskHeight}px` : "0px"};`}
|
||||||
|
class={cn(
|
||||||
|
"pointer-events-none absolute inset-0 z-10",
|
||||||
|
"before:absolute before:inset-x-0 before:top-0 before:transition-[height,opacity] before:duration-300 before:content-['']",
|
||||||
|
"after:absolute after:inset-x-0 after:bottom-0 after:transition-[height,opacity] after:duration-300 after:content-['']",
|
||||||
|
"before:h-(--top-fade-height) after:h-(--bottom-fade-height)",
|
||||||
|
showMask.top ? "before:opacity-100" : "before:opacity-0",
|
||||||
|
showMask.bottom ? "after:opacity-100" : "after:opacity-0",
|
||||||
|
"before:from-background before:bg-gradient-to-b before:to-transparent",
|
||||||
|
"after:from-background after:bg-gradient-to-t after:to-transparent",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
style={`--left-fade-width: ${showMask.left ? `${maskHeight}px` : "0px"}; --right-fade-width: ${showMask.right ? `${maskHeight}px` : "0px"};`}
|
||||||
|
class={cn(
|
||||||
|
"pointer-events-none absolute inset-0 z-10",
|
||||||
|
"before:absolute before:inset-y-0 before:left-0 before:transition-[width,opacity] before:duration-300 before:content-['']",
|
||||||
|
"after:absolute after:inset-y-0 after:right-0 after:transition-[width,opacity] after:duration-300 after:content-['']",
|
||||||
|
"before:w-(--left-fade-width) after:w-(--right-fade-width)",
|
||||||
|
showMask.left ? "before:opacity-100" : "before:opacity-0",
|
||||||
|
showMask.right ? "after:opacity-100" : "after:opacity-0",
|
||||||
|
"before:from-background before:bg-gradient-to-r before:to-transparent",
|
||||||
|
"after:from-background after:bg-gradient-to-l after:to-transparent",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
></div>
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { ScrollArea as ScrollAreaPrimitive } from "bits-ui";
|
||||||
|
import { cn, type WithoutChild } from "@/utils/cn";
|
||||||
|
import { useHasPrimaryTouch } from "@/hooks/use-has-primary-touch";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
orientation = "vertical",
|
||||||
|
children,
|
||||||
|
thumbClassName,
|
||||||
|
...restProps
|
||||||
|
}: WithoutChild<ScrollAreaPrimitive.ScrollbarProps> & {
|
||||||
|
thumbClassName?: string;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
const hasPrimaryTouch = useHasPrimaryTouch();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if !$hasPrimaryTouch}
|
||||||
|
<ScrollAreaPrimitive.Scrollbar
|
||||||
|
bind:ref
|
||||||
|
data-slot="scroll-area-scrollbar"
|
||||||
|
{orientation}
|
||||||
|
class={cn(
|
||||||
|
"flex touch-none p-px transition-[colors] duration-150 select-none hover:bg-neutral-200 data-[state=hidden]:animate-out data-[state=hidden]:fade-out-0 data-[state=visible]:animate-in data-[state=visible]:fade-in-0 dark:hover:bg-neutral-900",
|
||||||
|
orientation === "vertical" &&
|
||||||
|
"h-full w-2.5 border-l border-l-transparent",
|
||||||
|
orientation === "horizontal" &&
|
||||||
|
"h-2.5 flex-col border-t border-t-transparent px-1 pr-1.25",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
<ScrollAreaPrimitive.Thumb
|
||||||
|
data-slot="scroll-area-thumb"
|
||||||
|
class={cn(
|
||||||
|
"relative my-0.5 flex-1 rounded-full bg-neutral-300 transition-colors ease-out hover:bg-neutral-500/50 active:bg-neutral-500/75 dark:bg-neutral-800 dark:hover:bg-neutral-700 dark:active:bg-neutral-600",
|
||||||
|
thumbClassName,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</ScrollAreaPrimitive.Scrollbar>
|
||||||
|
{/if}
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithoutChild } from "@/utils/cn";
|
||||||
|
import { ScrollArea as ScrollAreaPrimitive } from "bits-ui";
|
||||||
|
import { Scrollbar } from "./index.js";
|
||||||
|
import ScrollAreaMask from "./scroll-area-mask.svelte";
|
||||||
|
import { useHasPrimaryTouch } from "@/hooks/use-has-primary-touch";
|
||||||
|
|
||||||
|
type Mask = {
|
||||||
|
top: boolean;
|
||||||
|
bottom: boolean;
|
||||||
|
left: boolean;
|
||||||
|
right: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
orientation = "vertical",
|
||||||
|
scrollbarXClasses = "",
|
||||||
|
scrollbarYClasses = "",
|
||||||
|
children,
|
||||||
|
scrollHideDelay = 0,
|
||||||
|
maskHeight = 30,
|
||||||
|
maskClassName = "",
|
||||||
|
viewportClassName = "",
|
||||||
|
...restProps
|
||||||
|
}: WithoutChild<ScrollAreaPrimitive.RootProps> & {
|
||||||
|
orientation?: "vertical" | "horizontal" | "both" | undefined;
|
||||||
|
scrollbarXClasses?: string | undefined;
|
||||||
|
scrollbarYClasses?: string | undefined;
|
||||||
|
maskHeight?: number;
|
||||||
|
maskClassName?: string;
|
||||||
|
viewportClassName?: string;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
let viewportRef: HTMLDivElement | null = $state(null);
|
||||||
|
const showMask = $state<Mask>({
|
||||||
|
top: true,
|
||||||
|
bottom: true,
|
||||||
|
left: true,
|
||||||
|
right: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasPrimaryTouchStore = useHasPrimaryTouch();
|
||||||
|
|
||||||
|
const checkScrollability = () => {
|
||||||
|
const element = viewportRef;
|
||||||
|
if (!element) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
scrollTop,
|
||||||
|
scrollLeft,
|
||||||
|
scrollWidth,
|
||||||
|
clientWidth,
|
||||||
|
scrollHeight,
|
||||||
|
clientHeight,
|
||||||
|
} = element;
|
||||||
|
|
||||||
|
showMask.top = scrollTop > 0;
|
||||||
|
showMask.bottom = scrollTop + clientHeight < scrollHeight - 1;
|
||||||
|
showMask.left = scrollLeft > 0;
|
||||||
|
showMask.right = scrollLeft + clientWidth < scrollWidth - 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
const element = viewportRef;
|
||||||
|
if (!element) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const controller = new AbortController();
|
||||||
|
const { signal } = controller;
|
||||||
|
|
||||||
|
const resizeObserver = new ResizeObserver(checkScrollability);
|
||||||
|
resizeObserver.observe(element);
|
||||||
|
|
||||||
|
element.addEventListener("scroll", checkScrollability, { signal });
|
||||||
|
window.addEventListener("resize", checkScrollability, { signal });
|
||||||
|
|
||||||
|
checkScrollability();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
controller.abort();
|
||||||
|
resizeObserver.disconnect();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if $hasPrimaryTouchStore}
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
role="group"
|
||||||
|
data-slot="scroll-area"
|
||||||
|
aria-roledescription="scroll area"
|
||||||
|
class={cn("relative overflow-hidden", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
bind:this={viewportRef}
|
||||||
|
data-slot="scroll-area-viewport"
|
||||||
|
class={cn("size-full overflow-auto rounded-[inherit]", viewportClassName)}
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if maskHeight > 0}
|
||||||
|
<ScrollAreaMask {showMask} class={maskClassName} {maskHeight} />{/if}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<ScrollAreaPrimitive.Root
|
||||||
|
bind:ref
|
||||||
|
{scrollHideDelay}
|
||||||
|
data-slot="scroll-area"
|
||||||
|
class={cn("relative", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
<ScrollAreaPrimitive.Viewport
|
||||||
|
bind:ref={viewportRef}
|
||||||
|
data-slot="scroll-area-viewport"
|
||||||
|
class={cn("focus-ring size-full rounded-[inherit]", viewportClassName)}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</ScrollAreaPrimitive.Viewport>
|
||||||
|
|
||||||
|
{#if maskHeight > 0}
|
||||||
|
<ScrollAreaMask {maskHeight} class={maskClassName} {showMask} />
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if orientation === "vertical" || orientation === "both"}
|
||||||
|
<Scrollbar orientation="vertical" class={scrollbarYClasses} />
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if orientation === "horizontal" || orientation === "both"}
|
||||||
|
<Scrollbar orientation="horizontal" class={scrollbarXClasses} />
|
||||||
|
{/if}
|
||||||
|
<ScrollAreaPrimitive.Corner />
|
||||||
|
</ScrollAreaPrimitive.Root>
|
||||||
|
{/if}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { browser } from "$app/environment";
|
||||||
|
import { writable } from "svelte/store";
|
||||||
|
|
||||||
|
export function useHasPrimaryTouch() {
|
||||||
|
const { subscribe, set } = writable(false);
|
||||||
|
if (!browser) {
|
||||||
|
return {
|
||||||
|
subscribe,
|
||||||
|
destroy: () => {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const controller = new AbortController();
|
||||||
|
const { signal } = controller;
|
||||||
|
|
||||||
|
const handleTouch = () => {
|
||||||
|
const hasTouch = "ontouchstart" in window || navigator.maxTouchPoints > 0;
|
||||||
|
const prefersTouch = window.matchMedia("(pointer: coarse)").matches;
|
||||||
|
set(hasTouch && prefersTouch);
|
||||||
|
};
|
||||||
|
|
||||||
|
const mq = window.matchMedia("(pointer: coarse)");
|
||||||
|
mq.addEventListener("change", handleTouch, { signal });
|
||||||
|
window.addEventListener("pointerdown", handleTouch, { signal });
|
||||||
|
|
||||||
|
handleTouch();
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe,
|
||||||
|
destroy: () => controller.abort(),
|
||||||
|
};
|
||||||
|
}
|
||||||
+13
-2
@@ -1,6 +1,17 @@
|
|||||||
import { clsx, type ClassValue } from "clsx";
|
import { type ClassValue, clsx } from "clsx";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs));
|
return twMerge(clsx(inputs));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
export type WithoutChild<T> = T extends { child?: any } ? Omit<T, "child"> : T;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
export type WithoutChildren<T> = T extends { children?: any }
|
||||||
|
? Omit<T, "children">
|
||||||
|
: T;
|
||||||
|
export type WithoutChildrenOrChild<T> = WithoutChildren<WithoutChild<T>>;
|
||||||
|
export type WithElementRef<T, U extends HTMLElement = HTMLElement> = T & {
|
||||||
|
ref?: U | null;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user