some kind of scrollbar implemented
Some checks failed
Rebuild signaller for deprived.dev to rebuild site / test_service (push) Failing after 0s

This commit is contained in:
BOTAlex 2025-08-20 01:10:20 +02:00
parent bd2a1f7f77
commit c6c2e2e31c
2 changed files with 392 additions and 9 deletions

View file

@ -1,9 +1,14 @@
<script>
import CustomScrollBar from "@src/routes/comps/CustomScrollBar.svelte";
</script>
<div class="grid grid-cols-6 w-full h-[80vh]"> <div class="grid grid-cols-6 w-full h-[80vh]">
<div class="h-full col-start-2 col-span-2"> <div class="h-full col-start-2 col-span-2">
<div class="flex flex-col items-center justify-center w-full h-full"> <div class="flex flex-col items-center justify-center w-full h-full">
<div class="flex flex-col w-72"> <div class="flex flex-col w-72">
<div class="w-full h-72 bg-lime-200"></div> <div class="w-full h-72 bg-lime-200"></div>
<div class="flex w-full gap-4 overflow-x-scroll"> <CustomScrollBar overflowX="scroll" overflowY="hidden" Class="h-32">
<div class="flex w-full gap-4">
{#each { length: 4 } as i} {#each { length: 4 } as i}
<img <img
src="https://placehold.co/600x400" src="https://placehold.co/600x400"
@ -12,6 +17,7 @@
/> />
{/each} {/each}
</div> </div>
</CustomScrollBar>
</div> </div>
</div> </div>
</div> </div>

View file

@ -0,0 +1,377 @@
<script lang="ts">
import { onMount, onDestroy } from "svelte";
// Public props
export let overflowX: "auto" | "scroll" | "hidden" = "hidden";
export let overflowY: "auto" | "scroll" | "hidden" = "auto";
// Visual tuning
export let thickness = 12; // px
export let padding = 6; // px, space around track inside the overlay
export let minThumb = 24; // px
export let contentPadding = 16; // px, inner padding for your content
export let Class = ""; // Extra classes for the scrollbar
// Styling (customize freely)
export let trackGradient =
"linear-gradient(180deg, #7aa2ff, #b57cff 40%, #ff84a3)";
export let trackOpacity = 0.55;
export let thumbClass =
"bg-neutral-900/90 shadow-[inset_0_0_0_1px_rgba(255,255,255,0.2)]";
let viewport: HTMLDivElement;
let vBar: HTMLDivElement; // vertical bar container
let hBar: HTMLDivElement; // horizontal bar container
let vThumb: HTMLDivElement;
let hThumb: HTMLDivElement;
let showBarY = false;
let showBarX = false;
// ——— utils
const sMaxY = () =>
Math.max(0, viewport.scrollHeight - viewport.clientHeight);
const sMaxX = () => Math.max(0, viewport.scrollWidth - viewport.clientWidth);
const tMaxY = () =>
Math.max(0, (vBar?.clientHeight || 0) - (vThumb?.offsetHeight || 0));
const tMaxX = () =>
Math.max(0, (hBar?.clientWidth || 0) - (hThumb?.offsetWidth || 0));
function updateVisibility() {
showBarY =
overflowY !== "hidden" && viewport.scrollHeight > viewport.clientHeight;
showBarX =
overflowX !== "hidden" && viewport.scrollWidth > viewport.clientWidth;
}
function updateVerticalThumb() {
if (!vThumb || !vBar) return;
const ratio = viewport.clientHeight / viewport.scrollHeight;
const h = Math.max(minThumb, Math.round(ratio * vBar.clientHeight));
vThumb.style.height = `${h}px`;
const top = sMaxY() ? (viewport.scrollTop / sMaxY()) * tMaxY() : 0;
vThumb.style.top = `${top}px`;
vThumb.setAttribute("aria-valuemin", "0");
vThumb.setAttribute("aria-valuemax", String(sMaxY()));
vThumb.setAttribute("aria-valuenow", String(viewport.scrollTop));
}
function updateHorizontalThumb() {
if (!hThumb || !hBar) return;
const ratio = viewport.clientWidth / viewport.scrollWidth;
const w = Math.max(minThumb, Math.round(ratio * hBar.clientWidth));
hThumb.style.width = `${w}px`;
const left = sMaxX() ? (viewport.scrollLeft / sMaxX()) * tMaxX() : 0;
hThumb.style.left = `${left}px`;
hThumb.setAttribute("aria-valuemin", "0");
hThumb.setAttribute("aria-valuemax", String(sMaxX()));
hThumb.setAttribute("aria-valuenow", String(viewport.scrollLeft));
}
function updateAll() {
updateVisibility();
updateVerticalThumb();
updateHorizontalThumb();
}
// Drag state
let draggingV = false;
let draggingH = false;
let dragStart = { x: 0, y: 0, scrollLeft: 0, scrollTop: 0 };
let pointerIdV: number | null = null;
let pointerIdH: number | null = null;
function onVPointerDown(e: PointerEvent) {
draggingV = true;
pointerIdV = e.pointerId;
vThumb.setPointerCapture(e.pointerId);
dragStart = {
x: e.clientX,
y: e.clientY,
scrollLeft: viewport.scrollLeft,
scrollTop: viewport.scrollTop,
};
e.preventDefault();
}
function onVPointerMove(e: PointerEvent) {
if (!draggingV) return;
const dy = e.clientY - dragStart.y;
const pixelsPerThumb = sMaxY() / Math.max(1, tMaxY());
viewport.scrollTop = Math.min(
sMaxY(),
Math.max(0, dragStart.scrollTop + dy * pixelsPerThumb),
);
}
function endVDrag(e: PointerEvent) {
if (!draggingV) return;
draggingV = false;
if (pointerIdV != null) {
try {
vThumb.releasePointerCapture(pointerIdV);
} catch {}
pointerIdV = null;
}
}
function onHPointerDown(e: PointerEvent) {
draggingH = true;
pointerIdH = e.pointerId;
hThumb.setPointerCapture(e.pointerId);
dragStart = {
x: e.clientX,
y: e.clientY,
scrollLeft: viewport.scrollLeft,
scrollTop: viewport.scrollTop,
};
e.preventDefault();
}
function onHPointerMove(e: PointerEvent) {
if (!draggingH) return;
const dx = e.clientX - dragStart.x;
const pixelsPerThumb = sMaxX() / Math.max(1, tMaxX());
viewport.scrollLeft = Math.min(
sMaxX(),
Math.max(0, dragStart.scrollLeft + dx * pixelsPerThumb),
);
}
function endHDrag(e: PointerEvent) {
if (!draggingH) return;
draggingH = false;
if (pointerIdH != null) {
try {
hThumb.releasePointerCapture(pointerIdH);
} catch {}
pointerIdH = null;
}
}
// Track clicks: jump to position
function onVTrackPointerDown(e: PointerEvent) {
if (e.target === vThumb) return;
const rect = vBar.getBoundingClientRect();
const y = e.clientY - rect.top - vThumb.offsetHeight / 2;
const clamped = Math.min(tMaxY(), Math.max(0, y));
viewport.scrollTop = (clamped / Math.max(1, tMaxY())) * sMaxY();
}
function onHTrackPointerDown(e: PointerEvent) {
if (e.target === hThumb) return;
const rect = hBar.getBoundingClientRect();
const x = e.clientX - rect.left - hThumb.offsetWidth / 2;
const clamped = Math.min(tMaxX(), Math.max(0, x));
viewport.scrollLeft = (clamped / Math.max(1, tMaxX())) * sMaxX();
}
// Keyboard on thumbs
function onVKeyDown(e: KeyboardEvent) {
const step = 40;
const page = Math.max(120, Math.floor(viewport.clientHeight * 0.9));
switch (e.key) {
case "ArrowDown":
viewport.scrollBy({ top: step });
e.preventDefault();
break;
case "ArrowUp":
viewport.scrollBy({ top: -step });
e.preventDefault();
break;
case "PageDown":
viewport.scrollBy({ top: page });
e.preventDefault();
break;
case "PageUp":
viewport.scrollBy({ top: -page });
e.preventDefault();
break;
case "Home":
viewport.scrollTo({ top: 0 });
e.preventDefault();
break;
case "End":
viewport.scrollTo({ top: sMaxY() });
e.preventDefault();
break;
}
}
function onHKeyDown(e: KeyboardEvent) {
const step = 40;
const page = Math.max(120, Math.floor(viewport.clientWidth * 0.9));
switch (e.key) {
case "ArrowRight":
viewport.scrollBy({ left: step });
e.preventDefault();
break;
case "ArrowLeft":
viewport.scrollBy({ left: -step });
e.preventDefault();
break;
case "PageDown":
viewport.scrollBy({ left: page });
e.preventDefault();
break;
case "PageUp":
viewport.scrollBy({ left: -page });
e.preventDefault();
break;
case "Home":
viewport.scrollTo({ left: 0 });
e.preventDefault();
break;
case "End":
viewport.scrollTo({ left: sMaxX() });
e.preventDefault();
break;
}
}
let ro: ResizeObserver;
onMount(() => {
const onScroll = () => {
updateVerticalThumb();
updateHorizontalThumb();
};
viewport.addEventListener("scroll", onScroll, { passive: true });
ro = new ResizeObserver(() => updateAll());
ro.observe(viewport);
if (viewport.firstElementChild instanceof HTMLElement) {
ro.observe(viewport.firstElementChild);
}
// Global pointer end (in case drag ends outside)
const endAll = () => {
draggingV = draggingH = false;
};
window.addEventListener("pointerup", endAll);
// Initial paint
queueMicrotask(updateAll);
return () => {
viewport.removeEventListener("scroll", onScroll as any);
window.removeEventListener("pointerup", endAll);
};
});
onDestroy(() => {
ro?.disconnect();
});
// Padding for content so the overlay bars dont cover it
$: pr = contentPadding + (showBarY ? thickness + padding * 2 : 0);
$: pb = contentPadding + (showBarX ? thickness + padding * 2 : 0);
</script>
<!-- Wrapper -->
<div class="relative overflow-hidden {Class}">
<!-- The real, native scrolling area (we hide its native scrollbar) -->
<div
bind:this={viewport}
class="csb-viewport absolute inset-0 overflow-auto focus:outline-none"
style="
padding: {contentPadding}px {pr}px {pb}px {contentPadding}px;
overscroll-behavior: contain;
-webkit-overflow-scrolling: touch;
"
>
<div class="min-w-0 min-h-0">
<slot />
</div>
</div>
<!-- Vertical overlay bar -->
{#if showBarY}
<div
bind:this={vBar}
class="absolute"
style="
top: {padding}px;
bottom: {padding}px;
right: {padding}px;
width: {thickness}px;
pointer-events: none;
"
aria-hidden="false"
>
<div
class="rounded-full transition-opacity"
style="
position: absolute; inset: 0;
background: {trackGradient};
opacity: {trackOpacity};
pointer-events: auto;
"
on:pointerdown={onVTrackPointerDown}
/>
<div
bind:this={vThumb}
role="scrollbar"
aria-orientation="vertical"
tabindex="0"
class={`absolute left-[2px] right-[2px] rounded-full cursor-grab active:cursor-grabbing focus-visible:outline focus-visible:outline-2 focus-visible:outline-sky-500 ${thumbClass}`}
style="height: 40px; pointer-events: auto; touch-action: none;"
on:pointerdown={onVPointerDown}
on:pointermove={onVPointerMove}
on:pointerup={endVDrag}
on:pointercancel={endVDrag}
on:keydown={onVKeyDown}
/>
</div>
{/if}
<!-- Horizontal overlay bar -->
{#if showBarX}
<div
bind:this={hBar}
class="absolute"
style="
left: {padding}px;
right: {padding}px;
bottom: {padding}px;
height: {thickness}px;
pointer-events: none;
"
aria-hidden="false"
>
<div
class="rounded-full transition-opacity"
style="
position: absolute; inset: 0;
background: {trackGradient};
opacity: {trackOpacity};
pointer-events: auto;
"
on:pointerdown={onHTrackPointerDown}
/>
<div
bind:this={hThumb}
role="scrollbar"
aria-orientation="horizontal"
tabindex="0"
class={`absolute top-[2px] bottom-[2px] rounded-full cursor-grab active:cursor-grabbing focus-visible:outline focus-visible:outline-2 focus-visible:outline-sky-500 ${thumbClass}`}
style="width: 40px; pointer-events: auto; touch-action: none;"
on:pointerdown={onHPointerDown}
on:pointermove={onHPointerMove}
on:pointerup={endHDrag}
on:pointercancel={endHDrag}
on:keydown={onHKeyDown}
/>
</div>
{/if}
</div>
<style>
/* Hide the native scrollbars while keeping native scrolling */
:global(.csb-viewport) {
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE/Edge legacy */
}
:global(.csb-viewport::-webkit-scrollbar) {
width: 0;
height: 0; /* WebKit */
}
</style>