feat(components/tiles): autoscroll between pages

This commit is contained in:
mikhail "synzr" 2025-12-01 10:36:25 +05:00
parent fa6e1613f2
commit ee42b07e6a
5 changed files with 122 additions and 26 deletions

View file

@ -5,13 +5,55 @@ Metro-like tile. Must be in a group to display correctly.
--> -->
<script lang="ts"> <script lang="ts">
import randomNumber from "$lib/utils/random-number";
import { cva } from "class-variance-authority"; import { cva } from "class-variance-authority";
import { onDestroy, onMount } from "svelte";
/** /**
* Base size for a icon in pixels. * Base size of a icon in pixels.
**/ **/
const ICON_BASE_SIZE = 45; const ICON_BASE_SIZE = 45;
/**
* Icon sizes.
*/
const ICON_SIZES = {
small: ICON_BASE_SIZE,
medium: ICON_BASE_SIZE * 2,
wide: ICON_BASE_SIZE * 2,
large: ICON_BASE_SIZE * 4,
};
/**
* Base size of a page in pixels.
*/
const PAGE_BASE_SIZE = 75;
/**
* Page heights.
*/
const PAGE_HEIGHTS = {
small: PAGE_BASE_SIZE,
medium: PAGE_BASE_SIZE * 2,
wide: PAGE_BASE_SIZE * 2,
large: PAGE_BASE_SIZE * 4,
};
/**
* Max scroll interval delay in milliseconds.
*/
const SCROLL_INTERVAL_MAX = 10 * 1000;
/**
* Min scroll interval delay in milliseconds.
*/
const SCROLL_INTERVAL_MIN = SCROLL_INTERVAL_MAX / 2;
/**
* Tile element.
*/
let element: HTMLElement;
/** /**
* Properties. * Properties.
*/ */
@ -45,7 +87,9 @@ Metro-like tile. Must be in a group to display correctly.
/** /**
* Tile style. * Tile style.
**/ **/
const style = cva(["bg-(--tile-color)", "overflow-y-hidden"], { const style = cva(
["bg-(--tile-color)", "overflow-y-hidden", "scroll-smooth"],
{
variants: { variants: {
size: { size: {
small: ["row-span-1", "col-span-1"], small: ["row-span-1", "col-span-1"],
@ -54,34 +98,77 @@ Metro-like tile. Must be in a group to display correctly.
large: ["row-span-4", "col-span-4"], large: ["row-span-4", "col-span-4"],
}, },
}, },
}); },
);
/** /**
* Icon size. Based on tile's size. * Icon size.
*/ */
let iconSize: string = $state("0"); const iconSize = ICON_SIZES[size];
switch (size) {
case "small": /**
iconSize = `${ICON_BASE_SIZE}px`; * Page height.
break; */
case "medium": const pageHeight = PAGE_HEIGHTS[size];
case "wide":
iconSize = `${ICON_BASE_SIZE * 2}px`; /**
break; * Scroll side. Positive value is down, negative value is up.
case "large": */
iconSize = `${ICON_BASE_SIZE * 4}px`; let scrollSide = 1;
break;
/**
* Get page count based on scroll height.
*/
const getPageCount = () => Math.floor(element.scrollHeight / pageHeight);
/**
* Scroll between pages. Used by scroll interval.
*/
function scrollPages() {
if (element.clientHeight === element.scrollHeight - pageHeight) {
scrollSide = -scrollSide;
} }
element.scrollBy({ top: pageHeight * scrollSide });
}
/**
* Scroll interval.
*/
let scrollInterval: NodeJS.Timeout;
onMount(() => {
if (size === "small" || getPageCount() === 1) {
return;
}
const scrollDelay = randomNumber(
SCROLL_INTERVAL_MIN,
SCROLL_INTERVAL_MAX,
);
scrollInterval = setInterval(scrollPages, scrollDelay);
});
onDestroy(() => {
if (!scrollInterval) {
return;
}
clearInterval(scrollInterval);
});
</script> </script>
<div <div
class={style({ size })} class={style({ size })}
style=" style="
--tile-icon-size: {iconSize}; --tile-icon-size: {iconSize}px;
--tile-page-height: {pageHeight}px;
--tile-name-display: {size !== 'small' ? 'inline' : 'none'}; --tile-name-display: {size !== 'small' ? 'inline' : 'none'};
grid-row-start: {row}; grid-row-start: {row};
grid-column-start: {column}; grid-column-start: {column};
" "
bind:this={element}
> >
{@render children()} {@render children()}
</div> </div>

View file

@ -22,8 +22,9 @@ Icon page for a tile. Must be in a tile to display correctly.
</script> </script>
<div <div
class="h-full flex flex-col justify-end bg-center bg-no-repeat" class="flex flex-col justify-end bg-center bg-no-repeat"
style=" style="
height: var(--tile-page-height);
background-image: url('{icon}'); background-image: url('{icon}');
background-size: var(--tile-icon-size); background-size: var(--tile-icon-size);
" "

View file

@ -16,6 +16,6 @@ Image page for a tile. Must be in a tile to display correctly.
</script> </script>
<div <div
class="w-full h-full bg-cover bg-center bg-no-repeat" class="w-full bg-cover bg-center bg-no-repeat"
style="background-image: url('{image}');" style="height: var(--tile-page-height); background-image: url('{image}');"
></div> ></div>

View file

@ -0,0 +1,3 @@
export default function randomNumber(min: number, max: number) {
return min + Math.floor(Math.random() * (max - min));
}

View file

@ -14,13 +14,18 @@
<Tile size="small" row={5} column={2}> <Tile size="small" row={5} column={2}>
<TileIconPage icon={iconSynzr} name="mikhail" /> <TileIconPage icon={iconSynzr} name="mikhail" />
</Tile> </Tile>
<Tile size="medium" row={3} column={5}> <Tile size="medium" row={3} column={5}>
<TileIconPage icon={iconSynzr} name="mikhail" /> <TileIconPage icon={iconSynzr} name="mikhail" />
</Tile> </Tile>
<Tile size="wide" row={5} column={3}> <Tile size="wide" row={5} column={3}>
<TileIconPage icon={iconSynzr} name="mikhail" />
<TileImagePage image={imageTestRectangle} /> <TileImagePage image={imageTestRectangle} />
</Tile> </Tile>
<Tile size="large" row={1} column={1}> <Tile size="large" row={1} column={1}>
<TileIconPage icon={iconSynzr} name="mikhail" />
<TileImagePage image={imageTestSquare} /> <TileImagePage image={imageTestSquare} />
</Tile> </Tile>
</TileGroup> </TileGroup>