2025-11-30 22:44:47 +05:00
|
|
|
<!--
|
|
|
|
|
@component
|
|
|
|
|
|
|
|
|
|
Metro-like tile. Must be in a group to display correctly.
|
|
|
|
|
-->
|
|
|
|
|
|
|
|
|
|
<script lang="ts">
|
2025-12-01 10:36:25 +05:00
|
|
|
import randomNumber from "$lib/utils/random-number";
|
2025-11-30 22:44:47 +05:00
|
|
|
import { cva } from "class-variance-authority";
|
2025-12-01 10:36:25 +05:00
|
|
|
import { onDestroy, onMount } from "svelte";
|
2025-11-30 22:44:47 +05:00
|
|
|
|
2025-12-01 09:27:37 +05:00
|
|
|
/**
|
2025-12-01 10:36:25 +05:00
|
|
|
* Base size of a icon in pixels.
|
2025-12-01 09:27:37 +05:00
|
|
|
**/
|
2025-11-30 23:22:45 +05:00
|
|
|
const ICON_BASE_SIZE = 45;
|
|
|
|
|
|
2025-12-01 10:36:25 +05:00
|
|
|
/**
|
|
|
|
|
* 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;
|
|
|
|
|
|
2025-12-01 09:27:37 +05:00
|
|
|
/**
|
|
|
|
|
* Properties.
|
|
|
|
|
*/
|
2025-11-30 22:44:47 +05:00
|
|
|
let {
|
|
|
|
|
size,
|
|
|
|
|
row,
|
2025-12-01 11:04:21 +05:00
|
|
|
icon,
|
2025-11-30 22:44:47 +05:00
|
|
|
column,
|
2025-11-30 23:22:45 +05:00
|
|
|
children,
|
2025-11-30 22:44:47 +05:00
|
|
|
}: {
|
|
|
|
|
/**
|
|
|
|
|
* Size.
|
|
|
|
|
*/
|
|
|
|
|
size: "small" | "medium" | "wide" | "large";
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Row index. Must be in a small tiles.
|
|
|
|
|
*/
|
|
|
|
|
row: number;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Column index. Must be in a small tiles.
|
|
|
|
|
*/
|
|
|
|
|
column: number;
|
|
|
|
|
|
2025-12-01 11:04:21 +05:00
|
|
|
/**
|
|
|
|
|
* Icon. Must be a valid URL.
|
|
|
|
|
*/
|
|
|
|
|
icon: string;
|
|
|
|
|
|
2025-11-30 22:44:47 +05:00
|
|
|
/**
|
2025-11-30 23:22:45 +05:00
|
|
|
* Pages.
|
2025-11-30 22:44:47 +05:00
|
|
|
*/
|
2025-11-30 23:22:45 +05:00
|
|
|
children: () => any;
|
2025-11-30 22:44:47 +05:00
|
|
|
} = $props();
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Tile style.
|
|
|
|
|
**/
|
2025-12-01 10:36:25 +05:00
|
|
|
const style = cva(
|
|
|
|
|
["bg-(--tile-color)", "overflow-y-hidden", "scroll-smooth"],
|
|
|
|
|
{
|
|
|
|
|
variants: {
|
|
|
|
|
size: {
|
|
|
|
|
small: ["row-span-1", "col-span-1"],
|
|
|
|
|
medium: ["row-span-2", "col-span-2"],
|
|
|
|
|
wide: ["row-span-2", "col-span-4"],
|
|
|
|
|
large: ["row-span-4", "col-span-4"],
|
|
|
|
|
},
|
2025-11-30 22:44:47 +05:00
|
|
|
},
|
|
|
|
|
},
|
2025-12-01 10:36:25 +05:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Icon size.
|
|
|
|
|
*/
|
|
|
|
|
const iconSize = ICON_SIZES[size];
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Page height.
|
|
|
|
|
*/
|
|
|
|
|
const pageHeight = PAGE_HEIGHTS[size];
|
2025-11-30 23:22:45 +05:00
|
|
|
|
|
|
|
|
/**
|
2025-12-01 10:36:25 +05:00
|
|
|
* Scroll side. Positive value is down, negative value is up.
|
2025-11-30 23:22:45 +05:00
|
|
|
*/
|
2025-12-01 10:36:25 +05:00
|
|
|
let scrollSide = 1;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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 });
|
2025-11-30 23:22:45 +05:00
|
|
|
}
|
2025-12-01 10:36:25 +05:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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);
|
|
|
|
|
});
|
2025-11-30 22:44:47 +05:00
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
class={style({ size })}
|
|
|
|
|
style="
|
2025-12-01 11:04:21 +05:00
|
|
|
--tile-icon: url('{icon}');
|
2025-12-01 10:36:25 +05:00
|
|
|
--tile-icon-size: {iconSize}px;
|
|
|
|
|
--tile-page-height: {pageHeight}px;
|
2025-11-30 23:22:45 +05:00
|
|
|
--tile-name-display: {size !== 'small' ? 'inline' : 'none'};
|
2025-11-30 22:44:47 +05:00
|
|
|
grid-row-start: {row};
|
2025-11-30 23:22:45 +05:00
|
|
|
grid-column-start: {column};
|
2025-11-30 22:44:47 +05:00
|
|
|
"
|
2025-12-01 10:36:25 +05:00
|
|
|
bind:this={element}
|
2025-11-30 23:22:45 +05:00
|
|
|
>
|
|
|
|
|
{@render children()}
|
|
|
|
|
</div>
|