generated from tpl/obsidian-sample-plugin
			Rewrite bookshelf view from scratch
This commit is contained in:
		
							parent
							
								
									2df1cf4b30
								
							
						
					
					
						commit
						349fcd903e
					
				| 
						 | 
				
			
			@ -111,6 +111,7 @@ const context = await esbuild.context({
 | 
			
		|||
		"@lezer/lr",
 | 
			
		||||
		...builtins,
 | 
			
		||||
	],
 | 
			
		||||
	conditions: ["svelte"],
 | 
			
		||||
	format: "cjs",
 | 
			
		||||
	target: "es2018",
 | 
			
		||||
	logLevel: "info",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -44,9 +44,13 @@
 | 
			
		|||
		"typescript": "5.0.4"
 | 
			
		||||
	},
 | 
			
		||||
	"dependencies": {
 | 
			
		||||
		"@humanspeak/svelte-virtual-list": "^0.2.6",
 | 
			
		||||
		"@leveluptuts/svelte-fit": "^1.0.3",
 | 
			
		||||
		"chart.js": "^4.5.0",
 | 
			
		||||
		"chroma-js": "^3.1.2",
 | 
			
		||||
		"esbuild-sass-plugin": "^3.3.1",
 | 
			
		||||
		"fitty": "^2.4.2",
 | 
			
		||||
		"just-memoize": "^2.2.0",
 | 
			
		||||
		"textfit": "^2.4.0",
 | 
			
		||||
		"uuid": "^11.1.0",
 | 
			
		||||
		"yaml": "^2.8.0",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,6 +8,12 @@ importers:
 | 
			
		|||
 | 
			
		||||
  .:
 | 
			
		||||
    dependencies:
 | 
			
		||||
      '@humanspeak/svelte-virtual-list':
 | 
			
		||||
        specifier: ^0.2.6
 | 
			
		||||
        version: 0.2.6(svelte@5.34.8)
 | 
			
		||||
      '@leveluptuts/svelte-fit':
 | 
			
		||||
        specifier: ^1.0.3
 | 
			
		||||
        version: 1.0.3
 | 
			
		||||
      chart.js:
 | 
			
		||||
        specifier: ^4.5.0
 | 
			
		||||
        version: 4.5.0
 | 
			
		||||
| 
						 | 
				
			
			@ -17,6 +23,12 @@ importers:
 | 
			
		|||
      esbuild-sass-plugin:
 | 
			
		||||
        specifier: ^3.3.1
 | 
			
		||||
        version: 3.3.1(esbuild@0.17.3)(sass-embedded@1.89.2)
 | 
			
		||||
      fitty:
 | 
			
		||||
        specifier: ^2.4.2
 | 
			
		||||
        version: 2.4.2
 | 
			
		||||
      just-memoize:
 | 
			
		||||
        specifier: ^2.2.0
 | 
			
		||||
        version: 2.2.0
 | 
			
		||||
      textfit:
 | 
			
		||||
        specifier: ^2.4.0
 | 
			
		||||
        version: 2.4.0
 | 
			
		||||
| 
						 | 
				
			
			@ -312,6 +324,11 @@ packages:
 | 
			
		|||
    resolution: {integrity: sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==}
 | 
			
		||||
    engines: {node: '>=18.18.0'}
 | 
			
		||||
 | 
			
		||||
  '@humanspeak/svelte-virtual-list@0.2.6':
 | 
			
		||||
    resolution: {integrity: sha512-nfD81b4LQw2bTSFYV/M0ky/pnUkfI0KYr5qCVJHZe6h3dUtXoNIhoxypI6JGBcpW3D8jN5Y53NOkfDggJNb5nA==}
 | 
			
		||||
    peerDependencies:
 | 
			
		||||
      svelte: ^5.0.0
 | 
			
		||||
 | 
			
		||||
  '@humanwhocodes/module-importer@1.0.1':
 | 
			
		||||
    resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==}
 | 
			
		||||
    engines: {node: '>=12.22'}
 | 
			
		||||
| 
						 | 
				
			
			@ -345,6 +362,9 @@ packages:
 | 
			
		|||
  '@kurkle/color@0.3.4':
 | 
			
		||||
    resolution: {integrity: sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==}
 | 
			
		||||
 | 
			
		||||
  '@leveluptuts/svelte-fit@1.0.3':
 | 
			
		||||
    resolution: {integrity: sha512-Hg8Xz06Mf1pwI92cY60LdFKDjXUkix0KcNP5orgGjtV7ecfK4zZ8gwPHOSDioZt73/7Muj1O43QzvtK2oPwEfA==}
 | 
			
		||||
 | 
			
		||||
  '@marijn/find-cluster-break@1.0.2':
 | 
			
		||||
    resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -885,6 +905,9 @@ packages:
 | 
			
		|||
    resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
 | 
			
		||||
    engines: {node: '>=10'}
 | 
			
		||||
 | 
			
		||||
  fitty@2.4.2:
 | 
			
		||||
    resolution: {integrity: sha512-GNhWgImK4+wEkgEZjBkQMyu5NLSmmryg/CaRP7zYby+TWzCrUou6BHL+iqbjKzJRXMyzuJkH+LBB1+lh4oO77g==}
 | 
			
		||||
 | 
			
		||||
  flat-cache@4.0.1:
 | 
			
		||||
    resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==}
 | 
			
		||||
    engines: {node: '>=16'}
 | 
			
		||||
| 
						 | 
				
			
			@ -1138,6 +1161,9 @@ packages:
 | 
			
		|||
  json-stable-stringify-without-jsonify@1.0.1:
 | 
			
		||||
    resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
 | 
			
		||||
 | 
			
		||||
  just-memoize@2.2.0:
 | 
			
		||||
    resolution: {integrity: sha512-zriv+MY+61RXT0QsrO1ZJtL5umouqqSWmCGBkp2wJm35kniunBAA4qhUKx8Lvg/QcwrF9xuw9E6PkevKFf4boQ==}
 | 
			
		||||
 | 
			
		||||
  keyv@4.5.4:
 | 
			
		||||
    resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -1951,6 +1977,11 @@ snapshots:
 | 
			
		|||
      '@humanfs/core': 0.19.1
 | 
			
		||||
      '@humanwhocodes/retry': 0.3.1
 | 
			
		||||
 | 
			
		||||
  '@humanspeak/svelte-virtual-list@0.2.6(svelte@5.34.8)':
 | 
			
		||||
    dependencies:
 | 
			
		||||
      esm-env: 1.2.2
 | 
			
		||||
      svelte: 5.34.8
 | 
			
		||||
 | 
			
		||||
  '@humanwhocodes/module-importer@1.0.1': {}
 | 
			
		||||
 | 
			
		||||
  '@humanwhocodes/retry@0.3.1': {}
 | 
			
		||||
| 
						 | 
				
			
			@ -1976,6 +2007,8 @@ snapshots:
 | 
			
		|||
 | 
			
		||||
  '@kurkle/color@0.3.4': {}
 | 
			
		||||
 | 
			
		||||
  '@leveluptuts/svelte-fit@1.0.3': {}
 | 
			
		||||
 | 
			
		||||
  '@marijn/find-cluster-break@1.0.2': {}
 | 
			
		||||
 | 
			
		||||
  '@nodelib/fs.scandir@2.1.5':
 | 
			
		||||
| 
						 | 
				
			
			@ -2608,6 +2641,8 @@ snapshots:
 | 
			
		|||
      locate-path: 6.0.0
 | 
			
		||||
      path-exists: 4.0.0
 | 
			
		||||
 | 
			
		||||
  fitty@2.4.2: {}
 | 
			
		||||
 | 
			
		||||
  flat-cache@4.0.1:
 | 
			
		||||
    dependencies:
 | 
			
		||||
      flatted: 3.3.3
 | 
			
		||||
| 
						 | 
				
			
			@ -2869,6 +2904,8 @@ snapshots:
 | 
			
		|||
 | 
			
		||||
  json-stable-stringify-without-jsonify@1.0.1: {}
 | 
			
		||||
 | 
			
		||||
  just-memoize@2.2.0: {}
 | 
			
		||||
 | 
			
		||||
  keyv@4.5.4:
 | 
			
		||||
    dependencies:
 | 
			
		||||
      json-buffer: 3.0.1
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,7 +4,6 @@
 | 
			
		|||
	import Book from "@ui/components/bookshelf/Book.svelte";
 | 
			
		||||
	import Bookshelf from "@ui/components/bookshelf/Bookshelf.svelte";
 | 
			
		||||
	import BookStack from "@ui/components/bookshelf/BookStack.svelte";
 | 
			
		||||
	import BookStackElement from "@ui/components/bookshelf/BookStackElement.svelte";
 | 
			
		||||
	import {
 | 
			
		||||
		createMetadata,
 | 
			
		||||
		setMetadataContext,
 | 
			
		||||
| 
						 | 
				
			
			@ -22,6 +21,8 @@
 | 
			
		|||
	import DateFilter from "@ui/components/DateFilter.svelte";
 | 
			
		||||
	import Rating from "@ui/components/Rating.svelte";
 | 
			
		||||
	import { v4 as uuidv4 } from "uuid";
 | 
			
		||||
	import memoize from "just-memoize";
 | 
			
		||||
	import VirtualList from "@humanspeak/svelte-virtual-list";
 | 
			
		||||
 | 
			
		||||
	interface Props {
 | 
			
		||||
		plugin: BookTrackerPlugin;
 | 
			
		||||
| 
						 | 
				
			
			@ -32,11 +33,11 @@
 | 
			
		|||
		id: string;
 | 
			
		||||
		title: string;
 | 
			
		||||
		subtitle?: string;
 | 
			
		||||
		author: string;
 | 
			
		||||
		authors: string[];
 | 
			
		||||
		width: number;
 | 
			
		||||
		color: ColorName;
 | 
			
		||||
		design: (typeof designs)[number];
 | 
			
		||||
		orientation: undefined | "tilted" | "on-display";
 | 
			
		||||
		orientation: "default" | "tilted" | "front";
 | 
			
		||||
		file: TFile;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -55,12 +56,7 @@
 | 
			
		|||
	const metadataStore = createMetadata(plugin, settings.statusFilter, true);
 | 
			
		||||
	setMetadataContext(metadataStore);
 | 
			
		||||
 | 
			
		||||
	const designs = [
 | 
			
		||||
		"default",
 | 
			
		||||
		"colored-spine",
 | 
			
		||||
		"dual-top-bands",
 | 
			
		||||
		"split-bands",
 | 
			
		||||
	] as const;
 | 
			
		||||
	const designs = ["default", "dual-top-bands", "split-bands"] as const;
 | 
			
		||||
 | 
			
		||||
	const randomDesign = () => randomElement(designs);
 | 
			
		||||
	const randomColor = () => randomElement(COLOR_NAMES);
 | 
			
		||||
| 
						 | 
				
			
			@ -68,11 +64,11 @@
 | 
			
		|||
		const n = randomFloat();
 | 
			
		||||
 | 
			
		||||
		if (n < 0.55) {
 | 
			
		||||
			return undefined;
 | 
			
		||||
			return "default";
 | 
			
		||||
		} else if (n < 0.8) {
 | 
			
		||||
			return "tilted";
 | 
			
		||||
		} else {
 | 
			
		||||
			return "on-display";
 | 
			
		||||
			return "front";
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	const randomStackChance = () => randomFloat() > 0.9;
 | 
			
		||||
| 
						 | 
				
			
			@ -91,23 +87,34 @@
 | 
			
		|||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function getBookData(metadata: FileMetadata): BookData {
 | 
			
		||||
		return {
 | 
			
		||||
			id: metadata.file.path,
 | 
			
		||||
			title: metadata.frontmatter[settings.titleProperty],
 | 
			
		||||
			subtitle: settings.subtitleProperty
 | 
			
		||||
				? metadata.frontmatter[settings.subtitleProperty]
 | 
			
		||||
				: undefined,
 | 
			
		||||
			author: metadata.frontmatter[settings.authorsProperty].join(", "),
 | 
			
		||||
			width: metadata.frontmatter[
 | 
			
		||||
				settingsStore.settings.pageCountProperty
 | 
			
		||||
			],
 | 
			
		||||
			color: randomColor(),
 | 
			
		||||
			design: randomDesign(),
 | 
			
		||||
			orientation: randomOrientation(),
 | 
			
		||||
			file: metadata.file,
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
	const getBookData = memoize(
 | 
			
		||||
		(metadata: FileMetadata): BookData => {
 | 
			
		||||
			const orientation = randomOrientation();
 | 
			
		||||
 | 
			
		||||
			return {
 | 
			
		||||
				id: metadata.file.path,
 | 
			
		||||
				title: metadata.frontmatter[settings.titleProperty],
 | 
			
		||||
				subtitle: settings.subtitleProperty
 | 
			
		||||
					? metadata.frontmatter[settings.subtitleProperty]
 | 
			
		||||
					: undefined,
 | 
			
		||||
				authors: metadata.frontmatter[settings.authorsProperty],
 | 
			
		||||
				width: Math.min(
 | 
			
		||||
					Math.max(
 | 
			
		||||
						20,
 | 
			
		||||
						metadata.frontmatter[
 | 
			
		||||
							settingsStore.settings.pageCountProperty
 | 
			
		||||
						] / 10,
 | 
			
		||||
					),
 | 
			
		||||
					100,
 | 
			
		||||
				),
 | 
			
		||||
				color: randomColor(),
 | 
			
		||||
				design: orientation === "front" ? "default" : randomDesign(),
 | 
			
		||||
				orientation: randomOrientation(),
 | 
			
		||||
				file: metadata.file,
 | 
			
		||||
			};
 | 
			
		||||
		},
 | 
			
		||||
		(metadata: FileMetadata) => metadata.file.path,
 | 
			
		||||
	);
 | 
			
		||||
 | 
			
		||||
	let view = $state(settings.defaultView);
 | 
			
		||||
	let books: (BookData | BookStackData)[] = $state([]);
 | 
			
		||||
| 
						 | 
				
			
			@ -156,13 +163,14 @@
 | 
			
		|||
		<Bookshelf>
 | 
			
		||||
			{#each books as book (book.id)}
 | 
			
		||||
				{#if "books" in book}
 | 
			
		||||
					<BookStack totalChildren={book.books.length}>
 | 
			
		||||
					<BookStack>
 | 
			
		||||
						{#each book.books as bookData (bookData.id)}
 | 
			
		||||
							<BookStackElement
 | 
			
		||||
							<Book
 | 
			
		||||
								title={bookData.title}
 | 
			
		||||
								subtitle={bookData.subtitle}
 | 
			
		||||
								color={bookData.color}
 | 
			
		||||
								design={bookData.design}
 | 
			
		||||
								orientation="flat"
 | 
			
		||||
								onClick={() =>
 | 
			
		||||
									plugin.app.workspace
 | 
			
		||||
										.getLeaf("tab")
 | 
			
		||||
| 
						 | 
				
			
			@ -174,7 +182,7 @@
 | 
			
		|||
					<Book
 | 
			
		||||
						title={book.title}
 | 
			
		||||
						subtitle={book.subtitle}
 | 
			
		||||
						author={book.author}
 | 
			
		||||
						authors={book.authors}
 | 
			
		||||
						width={book.width}
 | 
			
		||||
						color={book.color}
 | 
			
		||||
						design={book.design}
 | 
			
		||||
| 
						 | 
				
			
			@ -275,7 +283,7 @@
 | 
			
		|||
	{/if}
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
<style lang="scss">
 | 
			
		||||
	.shelf-code-block {
 | 
			
		||||
		.controls {
 | 
			
		||||
			margin-bottom: 1rem;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,205 +1,327 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
	import Self from "./Book.svelte";
 | 
			
		||||
	import { Color, isColorName, type ColorName } from "@utils/color";
 | 
			
		||||
	import BookTilted from "./BookTilted.svelte";
 | 
			
		||||
	import BookOnDisplay from "./BookOnDisplay.svelte";
 | 
			
		||||
	import BookText from "./BookText.svelte";
 | 
			
		||||
	const BOOK_SIZE_DEFAULT: number = 40;
 | 
			
		||||
	const BOOK_SIZE_MIN: number = 15;
 | 
			
		||||
	import type { Snippet } from "svelte";
 | 
			
		||||
	import BookshelfItem from "./BookshelfItem.svelte";
 | 
			
		||||
	import { Color, type ColorName } from "@utils/color";
 | 
			
		||||
	import type { HTMLAttributes } from "svelte/elements";
 | 
			
		||||
	import { fit } from "@leveluptuts/svelte-fit";
 | 
			
		||||
 | 
			
		||||
	interface BookProps {
 | 
			
		||||
	interface Props {
 | 
			
		||||
		children?: Snippet;
 | 
			
		||||
		title?: string;
 | 
			
		||||
		subtitle?: string;
 | 
			
		||||
		author?: string;
 | 
			
		||||
		color?: ColorName | string;
 | 
			
		||||
		design?: "default" | "colored-spine" | "dual-top-bands" | "split-bands";
 | 
			
		||||
		orientation?: "tilted" | "on-display";
 | 
			
		||||
		authors?: string[];
 | 
			
		||||
		height?: number;
 | 
			
		||||
		width?: number;
 | 
			
		||||
		color?: ColorName;
 | 
			
		||||
		orientation?: "default" | "tilted" | "flat" | "front";
 | 
			
		||||
		design?: "default" | "split-bands" | "dual-top-bands";
 | 
			
		||||
		role?: ARIAMixin["role"];
 | 
			
		||||
		tabindex?: HTMLAttributes<HTMLDivElement>["tabindex"];
 | 
			
		||||
		onClick?: () => void;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	let {
 | 
			
		||||
		title,
 | 
			
		||||
		subtitle,
 | 
			
		||||
		author,
 | 
			
		||||
		color: colorRaw = "green",
 | 
			
		||||
		authors,
 | 
			
		||||
		height = 200,
 | 
			
		||||
		width = 40,
 | 
			
		||||
		color: colorName = "blue",
 | 
			
		||||
		orientation = "default",
 | 
			
		||||
		design = "default",
 | 
			
		||||
		orientation,
 | 
			
		||||
		height,
 | 
			
		||||
		width,
 | 
			
		||||
		role = "link",
 | 
			
		||||
		tabindex = 0,
 | 
			
		||||
		onClick,
 | 
			
		||||
	}: BookProps = $props();
 | 
			
		||||
	}: Props = $props();
 | 
			
		||||
 | 
			
		||||
	function normalizeWidth(input: number | undefined) {
 | 
			
		||||
		if (input) {
 | 
			
		||||
			if (input <= 150) {
 | 
			
		||||
				return BOOK_SIZE_MIN;
 | 
			
		||||
			}
 | 
			
		||||
			return input / 10;
 | 
			
		||||
	$effect(() => {
 | 
			
		||||
		if (orientation === "front" && design !== "default") {
 | 
			
		||||
			console.warn(
 | 
			
		||||
				"The front orientation does not support different designs.",
 | 
			
		||||
			);
 | 
			
		||||
		}
 | 
			
		||||
		return BOOK_SIZE_DEFAULT;
 | 
			
		||||
	}
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	const color = $derived(
 | 
			
		||||
		isColorName(colorRaw)
 | 
			
		||||
			? Color.fromName(colorRaw).darken()
 | 
			
		||||
			: Color.fromCSSColor(colorRaw),
 | 
			
		||||
	);
 | 
			
		||||
 | 
			
		||||
	const backgroundColor = $derived(
 | 
			
		||||
		design === "colored-spine"
 | 
			
		||||
			? color.chroma.mix("black", 0.14)
 | 
			
		||||
			: color.chroma,
 | 
			
		||||
	);
 | 
			
		||||
	const borderLeftColor = $derived(color.chroma.mix("white", 0.04));
 | 
			
		||||
	const borderRightColor = $derived(color.chroma.mix("black", 0.04));
 | 
			
		||||
	const bandColor = $derived(color.chroma.mix("black", 0.14));
 | 
			
		||||
	const textColor = $derived(new Color(backgroundColor).contrastColor.hex);
 | 
			
		||||
 | 
			
		||||
	const verifiedWidth = $derived(normalizeWidth(width));
 | 
			
		||||
	const color = $derived(Color.fromName(colorName));
 | 
			
		||||
	const backgroundColor = $derived(color.chroma.css());
 | 
			
		||||
	const textColor = $derived(color.contrastColor.chroma.css());
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
{#if orientation}
 | 
			
		||||
	{#if orientation === "tilted"}
 | 
			
		||||
		<BookTilted {design} width={verifiedWidth}>
 | 
			
		||||
			<Self
 | 
			
		||||
				{title}
 | 
			
		||||
				{subtitle}
 | 
			
		||||
				{author}
 | 
			
		||||
				color={color.hex}
 | 
			
		||||
				{design}
 | 
			
		||||
				{height}
 | 
			
		||||
				{width}
 | 
			
		||||
				{onClick}
 | 
			
		||||
			/>
 | 
			
		||||
		</BookTilted>
 | 
			
		||||
	{:else if orientation === "on-display"}
 | 
			
		||||
		<BookOnDisplay color={color.hex} {onClick}>
 | 
			
		||||
			{#if title}
 | 
			
		||||
				<BookText {title} {subtitle} allowWrap />
 | 
			
		||||
				<p class="bookshelf__book-author">By: {author}</p>
 | 
			
		||||
			{/if}
 | 
			
		||||
		</BookOnDisplay>
 | 
			
		||||
	{/if}
 | 
			
		||||
{:else}
 | 
			
		||||
<BookshelfItem>
 | 
			
		||||
	<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
 | 
			
		||||
	<div
 | 
			
		||||
		class="bookshelf__book-wrapper"
 | 
			
		||||
		class:default={design === "default"}
 | 
			
		||||
		class:colored-spine={design === "colored-spine"}
 | 
			
		||||
		class:dual-top-bands={design === "dual-top-bands"}
 | 
			
		||||
		class:split-bands={design === "split-bands"}
 | 
			
		||||
		style:--book-color={backgroundColor.css()}
 | 
			
		||||
		style:--book-border-left-color={borderLeftColor.css()}
 | 
			
		||||
		style:--book-border-right-color={borderRightColor.css()}
 | 
			
		||||
		style:--book-band-color={bandColor.css()}
 | 
			
		||||
		style:--book-width={verifiedWidth + "px"}
 | 
			
		||||
		style:width={verifiedWidth + "px"}
 | 
			
		||||
		style:color={textColor}
 | 
			
		||||
		class="book"
 | 
			
		||||
		class:tilted={orientation === "tilted"}
 | 
			
		||||
		class:flat={orientation === "flat"}
 | 
			
		||||
		class:front={orientation === "front"}
 | 
			
		||||
		style:--book-height="{height}px"
 | 
			
		||||
		style:--book-width="{width}px"
 | 
			
		||||
		style:--book-bg-color={backgroundColor}
 | 
			
		||||
		style:--book-text-color={textColor}
 | 
			
		||||
		style:cursor={onClick ? "pointer" : "default"}
 | 
			
		||||
		{role}
 | 
			
		||||
		{tabindex}
 | 
			
		||||
		onclick={onClick}
 | 
			
		||||
		onkeydown={(ev) => ev.key === "Enter" && onClick?.()}
 | 
			
		||||
		role="link"
 | 
			
		||||
		tabindex="0"
 | 
			
		||||
		onkeypress={(ev) => ev.key === "Enter" && onClick?.()}
 | 
			
		||||
	>
 | 
			
		||||
		<BookText {title} {subtitle} />
 | 
			
		||||
		{#if orientation !== "front" && (design === "split-bands" || design === "dual-top-bands")}
 | 
			
		||||
			<div class="book-band top"></div>
 | 
			
		||||
		{/if}
 | 
			
		||||
		{#if orientation !== "front" && design === "dual-top-bands"}
 | 
			
		||||
			<div class="book-band top2"></div>
 | 
			
		||||
		{/if}
 | 
			
		||||
		{#if orientation === "front"}
 | 
			
		||||
			<div class="book-crease"></div>
 | 
			
		||||
		{/if}
 | 
			
		||||
		<div class="book-inner">
 | 
			
		||||
			{#if title}
 | 
			
		||||
				<h2 class="book-title">
 | 
			
		||||
					{title}
 | 
			
		||||
				</h2>
 | 
			
		||||
				{#if subtitle}
 | 
			
		||||
					<h3 class="book-subtitle">
 | 
			
		||||
						{subtitle}
 | 
			
		||||
					</h3>
 | 
			
		||||
				{/if}
 | 
			
		||||
			{/if}
 | 
			
		||||
			{#if authors && orientation === "front"}
 | 
			
		||||
				<p class="book-authors">By: {authors?.join(", ")}</p>
 | 
			
		||||
			{/if}
 | 
			
		||||
		</div>
 | 
			
		||||
		{#if orientation !== "front" && design === "split-bands"}
 | 
			
		||||
			<div class="book-band bottom"></div>
 | 
			
		||||
		{/if}
 | 
			
		||||
	</div>
 | 
			
		||||
{/if}
 | 
			
		||||
</BookshelfItem>
 | 
			
		||||
 | 
			
		||||
<style lang="scss">
 | 
			
		||||
	div.bookshelf__book-wrapper {
 | 
			
		||||
		background: var(--book-color);
 | 
			
		||||
		border-left: 2px solid var(--book-border-left-color);
 | 
			
		||||
		border-right: 2px solid var(--book-border-right-color);
 | 
			
		||||
	@use "./variables.scss" as bookshelf;
 | 
			
		||||
 | 
			
		||||
		&.default,
 | 
			
		||||
		&.colored-spine {
 | 
			
		||||
			:global(.bookshelf__book-content) {
 | 
			
		||||
				height: calc(var(--book-width));
 | 
			
		||||
				margin: 0 var(--book-width);
 | 
			
		||||
	$small-band-width: calc(bookshelf.$book-band-width / 1.5);
 | 
			
		||||
	$band-color: rgba(0, 0, 0, 0.2);
 | 
			
		||||
	$band-top-offset: bookshelf.$book-band-offset;
 | 
			
		||||
	$band-top2-offset: $band-top-offset + $small-band-width +
 | 
			
		||||
		bookshelf.$book-band-gap;
 | 
			
		||||
 | 
			
		||||
	// prettier-ignore
 | 
			
		||||
	:global(:has(.book:not(.tilted)) + :has(.book.tilted:hover):has(+ * > .book.tilted)),
 | 
			
		||||
	:global(:has(.book.tilted) + :has(.book.tilted:not(:hover))) {
 | 
			
		||||
		& > div.book.tilted {
 | 
			
		||||
			margin-left: 0;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// prettier-ignore
 | 
			
		||||
	:global(:has(.book.tilted) + :has(.book.tilted:hover):has(+ * > .book:not(.tilted))),
 | 
			
		||||
	:global(:has(.book.tilted:not(:hover)):has(+ * > .book.tilted)) {
 | 
			
		||||
		div.book.tilted {
 | 
			
		||||
			margin-right: 0;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	div.book {
 | 
			
		||||
		box-sizing: content-box;
 | 
			
		||||
		position: relative;
 | 
			
		||||
		height: var(--book-height);
 | 
			
		||||
		width: var(--book-width);
 | 
			
		||||
		border-radius: bookshelf.$book-corner-radius;
 | 
			
		||||
		transition: transform 0.4s ease;
 | 
			
		||||
		background-color: var(--book-bg-color);
 | 
			
		||||
		color: var(--book-text-color);
 | 
			
		||||
 | 
			
		||||
		.book-inner {
 | 
			
		||||
			--book-content-width: calc(
 | 
			
		||||
				var(--book-height) - #{bookshelf.$book-padding * 2}
 | 
			
		||||
			);
 | 
			
		||||
			box-sizing: border-box;
 | 
			
		||||
			height: var(--book-width);
 | 
			
		||||
			width: var(--book-height);
 | 
			
		||||
			padding: bookshelf.$book-padding;
 | 
			
		||||
			transform-origin: 0 0;
 | 
			
		||||
			transform: translateX(var(--book-width)) rotate(90deg);
 | 
			
		||||
			display: flex;
 | 
			
		||||
			flex-direction: column;
 | 
			
		||||
			justify-content: center;
 | 
			
		||||
			align-items: center;
 | 
			
		||||
 | 
			
		||||
			$content-height: calc(
 | 
			
		||||
				var(--book-width) - #{bookshelf.$book-padding * 2}
 | 
			
		||||
			);
 | 
			
		||||
 | 
			
		||||
			.book-title,
 | 
			
		||||
			.book-subtitle,
 | 
			
		||||
			.book-authors {
 | 
			
		||||
				margin: 0;
 | 
			
		||||
				padding: 0;
 | 
			
		||||
				text-align: center;
 | 
			
		||||
				display: flex;
 | 
			
		||||
				justify-content: center;
 | 
			
		||||
				align-items: center;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			.book-title {
 | 
			
		||||
				font-size: 16px;
 | 
			
		||||
				height: $content-height;
 | 
			
		||||
				width: var(--book-content-width);
 | 
			
		||||
 | 
			
		||||
				&:has(+ .book-subtitle) {
 | 
			
		||||
					height: calc(#{$content-height} / 2);
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			.book-subtitle {
 | 
			
		||||
				font-size: 12px;
 | 
			
		||||
				height: calc(#{$content-height} / 2);
 | 
			
		||||
				width: var(--book-content-width);
 | 
			
		||||
				align-items: start;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			.book-authors {
 | 
			
		||||
				font-size: 10px;
 | 
			
		||||
				justify-self: end;
 | 
			
		||||
				width: var(--book-content-width);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		&.colored-spine {
 | 
			
		||||
			&:before {
 | 
			
		||||
				content: " ";
 | 
			
		||||
				display: block;
 | 
			
		||||
				background: var(--book-band-color);
 | 
			
		||||
				height: 100%;
 | 
			
		||||
				width: calc(var(--book-width));
 | 
			
		||||
				border-radius: 4px;
 | 
			
		||||
		&:hover {
 | 
			
		||||
			transform: scale(bookshelf.$book-hover-scale);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
				position: absolute;
 | 
			
		||||
				top: 0px;
 | 
			
		||||
				left: -2px;
 | 
			
		||||
		&.tilted {
 | 
			
		||||
			@include bookshelf.tilt;
 | 
			
		||||
 | 
			
		||||
			&:hover {
 | 
			
		||||
				transform: scale(bookshelf.$book-hover-scale);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		&.dual-top-bands,
 | 
			
		||||
		&.split-bands {
 | 
			
		||||
			&:after,
 | 
			
		||||
			&:before {
 | 
			
		||||
				content: "";
 | 
			
		||||
				display: block;
 | 
			
		||||
				background: var(--book-band-color);
 | 
			
		||||
				width: calc(100% + 4px);
 | 
			
		||||
		// Flat book orientation for Book Stack
 | 
			
		||||
		&.flat {
 | 
			
		||||
			height: var(--book-width);
 | 
			
		||||
			width: var(--book-height);
 | 
			
		||||
 | 
			
		||||
				position: absolute;
 | 
			
		||||
				left: -2px;
 | 
			
		||||
			.book-inner {
 | 
			
		||||
				height: var(--book-width);
 | 
			
		||||
				width: var(--book-height);
 | 
			
		||||
				transform: none;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			&:before {
 | 
			
		||||
				z-index: 2;
 | 
			
		||||
			}
 | 
			
		||||
			.book-band {
 | 
			
		||||
				transform: none;
 | 
			
		||||
				top: 0 !important;
 | 
			
		||||
 | 
			
		||||
			:global(.bookshelf__book-content),
 | 
			
		||||
			:global(.fit-text) {
 | 
			
		||||
				height: calc(var(--book-width));
 | 
			
		||||
				&.top {
 | 
			
		||||
					left: $band-top-offset;
 | 
			
		||||
 | 
			
		||||
					&:has(+ .top2) {
 | 
			
		||||
						width: $small-band-width;
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				&.top2 {
 | 
			
		||||
					width: $small-band-width;
 | 
			
		||||
					left: $band-top2-offset;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				&.bottom {
 | 
			
		||||
					left: calc(
 | 
			
		||||
						var(--book-height) - #{bookshelf.$book-band-offset} - #{bookshelf.$book-band-width}
 | 
			
		||||
					);
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		&.dual-top-bands {
 | 
			
		||||
			&:after {
 | 
			
		||||
				height: 10px;
 | 
			
		||||
				top: 8px;
 | 
			
		||||
			}
 | 
			
		||||
		// Front book orientation (cover view)
 | 
			
		||||
		&.front {
 | 
			
		||||
			$book-width: calc(var(--book-height) * 3 / 4);
 | 
			
		||||
			width: $book-width;
 | 
			
		||||
 | 
			
		||||
			&:before {
 | 
			
		||||
				height: 15px;
 | 
			
		||||
				top: 26px;
 | 
			
		||||
			}
 | 
			
		||||
			.book-inner {
 | 
			
		||||
				$padding-left: bookshelf.$book-crease-offset +
 | 
			
		||||
					bookshelf.$book-crease-width + bookshelf.$book-padding;
 | 
			
		||||
				$padding-right: bookshelf.$book-padding;
 | 
			
		||||
				$padding-total: $padding-left + $padding-right;
 | 
			
		||||
 | 
			
		||||
			:global(.bookshelf__book-content),
 | 
			
		||||
			:global(.fit-text) {
 | 
			
		||||
				width: calc(200px - 61px) !important;
 | 
			
		||||
			}
 | 
			
		||||
				--book-content-width: calc(#{$book-width} - #{$padding-total});
 | 
			
		||||
				transform: none;
 | 
			
		||||
				height: var(--book-height);
 | 
			
		||||
				width: $book-width;
 | 
			
		||||
				padding-left: $padding-left;
 | 
			
		||||
 | 
			
		||||
			:global(.bookshelf__book-content) {
 | 
			
		||||
				margin: 40px var(--book-width);
 | 
			
		||||
				.book-title {
 | 
			
		||||
					height: calc(var(--book-height) / 4);
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				.book-subtitle {
 | 
			
		||||
					height: calc(var(--book-height) / 4);
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				.book-authors {
 | 
			
		||||
					height: calc(var(--book-height) / 2);
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		&.split-bands {
 | 
			
		||||
			&:after,
 | 
			
		||||
			&:before {
 | 
			
		||||
				height: 20px;
 | 
			
		||||
		// Book band for styles
 | 
			
		||||
		.book-band {
 | 
			
		||||
			position: absolute;
 | 
			
		||||
			height: var(--book-width);
 | 
			
		||||
			width: bookshelf.$book-band-width;
 | 
			
		||||
			transform-origin: 0 0;
 | 
			
		||||
			transform: translateX(var(--book-width)) rotate(90deg);
 | 
			
		||||
			background-color: $band-color;
 | 
			
		||||
 | 
			
		||||
			&.top {
 | 
			
		||||
				top: $band-top-offset;
 | 
			
		||||
 | 
			
		||||
				&:has(+ .top2) {
 | 
			
		||||
					width: $small-band-width;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			&:after {
 | 
			
		||||
				top: 10px;
 | 
			
		||||
			&.top2 {
 | 
			
		||||
				width: $small-band-width;
 | 
			
		||||
				top: $band-top2-offset;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			&:before {
 | 
			
		||||
				bottom: 10px;
 | 
			
		||||
			&.bottom {
 | 
			
		||||
				top: calc(
 | 
			
		||||
					var(--book-height) - #{bookshelf.$book-band-offset} - #{bookshelf.$book-band-width}
 | 
			
		||||
				);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			:global(.bookshelf__book-content),
 | 
			
		||||
			:global(.fit-text) {
 | 
			
		||||
				width: calc(200px - 62px) !important;
 | 
			
		||||
			// Dual Top Band Offset
 | 
			
		||||
			& + .book-band + .book-inner {
 | 
			
		||||
				$padding-left: $band-top2-offset + $small-band-width +
 | 
			
		||||
					bookshelf.$book-padding;
 | 
			
		||||
				$padding-right: bookshelf.$book-padding;
 | 
			
		||||
				$padding-total: $padding-left + $padding-right;
 | 
			
		||||
 | 
			
		||||
				--book-content-width: calc(
 | 
			
		||||
					var(--book-height) - #{$padding-total}
 | 
			
		||||
				);
 | 
			
		||||
				padding-left: $padding-left;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			:global(.bookshelf__book-content) {
 | 
			
		||||
				margin: 30px var(--book-width);
 | 
			
		||||
			// Split Band Offset
 | 
			
		||||
			&.top + .book-inner {
 | 
			
		||||
				$padding: $band-top-offset + bookshelf.$book-band-width +
 | 
			
		||||
					bookshelf.$book-padding;
 | 
			
		||||
 | 
			
		||||
				--book-content-width: calc(
 | 
			
		||||
					var(--book-height) - #{$padding * 2}
 | 
			
		||||
				);
 | 
			
		||||
				padding-left: $padding;
 | 
			
		||||
				padding-right: $padding;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		.book-crease {
 | 
			
		||||
			position: absolute;
 | 
			
		||||
			height: var(--book-height);
 | 
			
		||||
			width: bookshelf.$book-crease-width;
 | 
			
		||||
			left: bookshelf.$book-crease-offset;
 | 
			
		||||
			background-color: $band-color;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,65 +0,0 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
	import type { Snippet } from "svelte";
 | 
			
		||||
	import chroma from "chroma-js";
 | 
			
		||||
 | 
			
		||||
	interface Props {
 | 
			
		||||
		children?: Snippet;
 | 
			
		||||
		color?: string;
 | 
			
		||||
		onClick?: () => void;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	let { children, color = "green", onClick }: Props = $props();
 | 
			
		||||
 | 
			
		||||
	let backgroundColor = $derived(chroma(color));
 | 
			
		||||
	let bandColor = $derived(chroma(color).mix("black", 0.14));
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<div
 | 
			
		||||
	class="bookshelf__book-wrapper bookshelf__book-onDisplay"
 | 
			
		||||
	style:--book-color={backgroundColor.css()}
 | 
			
		||||
	style:--book-band-color={bandColor.css()}
 | 
			
		||||
	onclick={onClick}
 | 
			
		||||
	onkeydown={(ev) => ev.key === "Enter" && onClick?.()}
 | 
			
		||||
	role="link"
 | 
			
		||||
	tabindex="0"
 | 
			
		||||
>
 | 
			
		||||
	<div class="book-display-crease"></div>
 | 
			
		||||
	{@render children?.()}
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<style lang="scss">
 | 
			
		||||
	$band-width: 5px;
 | 
			
		||||
	$band-offset: 8px;
 | 
			
		||||
 | 
			
		||||
	div.bookshelf__book-onDisplay {
 | 
			
		||||
		width: 150px;
 | 
			
		||||
		height: 200px;
 | 
			
		||||
		background: var(--book-color);
 | 
			
		||||
		display: flex;
 | 
			
		||||
		flex-flow: column wrap;
 | 
			
		||||
		justify-content: space-between;
 | 
			
		||||
		align-items: center;
 | 
			
		||||
		text-align: center;
 | 
			
		||||
		margin: 20px 2px 10px 2px;
 | 
			
		||||
		position: relative;
 | 
			
		||||
 | 
			
		||||
		padding-left: calc($band-offset + $band-width);
 | 
			
		||||
		padding-right: 8px;
 | 
			
		||||
 | 
			
		||||
		&:before {
 | 
			
		||||
			content: " ";
 | 
			
		||||
			display: block;
 | 
			
		||||
			background: var(--book-band-color);
 | 
			
		||||
			width: $band-width;
 | 
			
		||||
			height: 100%;
 | 
			
		||||
			position: absolute;
 | 
			
		||||
			top: 0;
 | 
			
		||||
			left: $band-offset;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		&:global(.bookshelf__book-author) {
 | 
			
		||||
			font-size: 0.8em;
 | 
			
		||||
			margin-left: 13px;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			@ -1,19 +1,31 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
	import type { Snippet } from "svelte";
 | 
			
		||||
 | 
			
		||||
	const MAX_CHILDREN = 5;
 | 
			
		||||
	import BookshelfItem from "./BookshelfItem.svelte";
 | 
			
		||||
 | 
			
		||||
	interface Props {
 | 
			
		||||
		children?: Snippet;
 | 
			
		||||
		totalChildren?: number;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	let { children, totalChildren = 0 }: Props = $props();
 | 
			
		||||
	let { children }: Props = $props();
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<ul
 | 
			
		||||
	class="bookshelf__bookStack-wrapper"
 | 
			
		||||
	style:margin={`calc(20px + ${(MAX_CHILDREN - totalChildren) * 40}px) 1px 10px`}
 | 
			
		||||
>
 | 
			
		||||
	{@render children?.()}
 | 
			
		||||
</ul>
 | 
			
		||||
<BookshelfItem>
 | 
			
		||||
	<div class="book-stack">
 | 
			
		||||
		{@render children?.()}
 | 
			
		||||
	</div>
 | 
			
		||||
</BookshelfItem>
 | 
			
		||||
 | 
			
		||||
<style lang="scss">
 | 
			
		||||
	@use "./variables.scss" as bookshelf;
 | 
			
		||||
 | 
			
		||||
	div.book-stack {
 | 
			
		||||
		display: flex;
 | 
			
		||||
		flex-direction: column;
 | 
			
		||||
		align-items: center;
 | 
			
		||||
		justify-content: end;
 | 
			
		||||
 | 
			
		||||
		:global(.bookshelf-item) {
 | 
			
		||||
			height: auto;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,142 +0,0 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
	import { Color, isColorName, type ColorName } from "@utils/color";
 | 
			
		||||
	import BookText from "./BookText.svelte";
 | 
			
		||||
 | 
			
		||||
	interface Props {
 | 
			
		||||
		title?: string;
 | 
			
		||||
		subtitle?: string;
 | 
			
		||||
		color?: ColorName | string;
 | 
			
		||||
		design?: "default" | "split-bands" | "dual-top-bands" | "colored-spine";
 | 
			
		||||
		onClick?: () => void;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	let {
 | 
			
		||||
		title,
 | 
			
		||||
		subtitle,
 | 
			
		||||
		color: colorRaw = "green",
 | 
			
		||||
		design,
 | 
			
		||||
		onClick,
 | 
			
		||||
	}: Props = $props();
 | 
			
		||||
 | 
			
		||||
	const color = $derived(
 | 
			
		||||
		isColorName(colorRaw)
 | 
			
		||||
			? Color.fromName(colorRaw).darken()
 | 
			
		||||
			: Color.fromCSSColor(colorRaw),
 | 
			
		||||
	);
 | 
			
		||||
 | 
			
		||||
	const backgroundColor = $derived(
 | 
			
		||||
		design === "colored-spine"
 | 
			
		||||
			? color.chroma.mix("black", 0.14)
 | 
			
		||||
			: color.chroma,
 | 
			
		||||
	);
 | 
			
		||||
	const borderLeftColor = $derived(color.chroma.mix("white", 0.04));
 | 
			
		||||
	const borderRightColor = $derived(color.chroma.mix("black", 0.04));
 | 
			
		||||
	const bandColor = $derived(color.chroma.mix("black", 0.14));
 | 
			
		||||
	const textColor = $derived(new Color(backgroundColor).contrastColor.hex);
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<li class="bookshelf__bookstack-elem">
 | 
			
		||||
	<div
 | 
			
		||||
		class="bookshelf__book-wrapper"
 | 
			
		||||
		class:default={design === "default"}
 | 
			
		||||
		class:colored-spine={design === "colored-spine"}
 | 
			
		||||
		class:dual-top-bands={design === "dual-top-bands"}
 | 
			
		||||
		class:split-bands={design === "split-bands"}
 | 
			
		||||
		style:--book-color={backgroundColor.css()}
 | 
			
		||||
		style:--book-border-left-color={borderLeftColor.css()}
 | 
			
		||||
		style:--book-border-right-color={borderRightColor.css()}
 | 
			
		||||
		style:--book-band-color={bandColor.css()}
 | 
			
		||||
		style:color={textColor}
 | 
			
		||||
		onclick={onClick}
 | 
			
		||||
		onkeydown={(ev) => ev.key === "Enter" && onClick?.()}
 | 
			
		||||
		role="link"
 | 
			
		||||
		tabindex="0"
 | 
			
		||||
	>
 | 
			
		||||
		<BookText {title} {subtitle} />
 | 
			
		||||
	</div>
 | 
			
		||||
</li>
 | 
			
		||||
 | 
			
		||||
<style lang="scss">
 | 
			
		||||
	div {
 | 
			
		||||
		background: var(--book-color);
 | 
			
		||||
		border-left: 2px solid var(--book-border-left-color);
 | 
			
		||||
		border-right: 2px solid var(--book-border-right-color);
 | 
			
		||||
 | 
			
		||||
		&.colored-spine {
 | 
			
		||||
			&:before {
 | 
			
		||||
				content: " ";
 | 
			
		||||
				display: block;
 | 
			
		||||
				background: var(--book-band-color);
 | 
			
		||||
				height: 40px;
 | 
			
		||||
				width: calc(100% + 4px);
 | 
			
		||||
				border-radius: 4px;
 | 
			
		||||
 | 
			
		||||
				position: absolute;
 | 
			
		||||
				top: 0px;
 | 
			
		||||
				left: -2px;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		&.split-bands,
 | 
			
		||||
		&.dual-top-bands {
 | 
			
		||||
			&:after,
 | 
			
		||||
			&:before {
 | 
			
		||||
				content: " ";
 | 
			
		||||
				display: block;
 | 
			
		||||
				background: var(--book-band-color);
 | 
			
		||||
				height: 40px;
 | 
			
		||||
				border-radius: 4px;
 | 
			
		||||
 | 
			
		||||
				position: absolute;
 | 
			
		||||
				top: 0px;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			&:before {
 | 
			
		||||
				z-index: 2;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		&.split-bands {
 | 
			
		||||
			&:after {
 | 
			
		||||
				width: 20px;
 | 
			
		||||
				left: 10px;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			&:before {
 | 
			
		||||
				width: 20px;
 | 
			
		||||
				right: 10px;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			:global(.bookshelf__book-content),
 | 
			
		||||
			:global(.fit-text) {
 | 
			
		||||
				width: calc(200px - 60px) !important;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			:global(.bookshelf__book-content) {
 | 
			
		||||
				margin: 0 30px;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		&.dual-top-bands {
 | 
			
		||||
			&:after {
 | 
			
		||||
				width: 10px;
 | 
			
		||||
				left: 6px;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			&:before {
 | 
			
		||||
				width: 15px;
 | 
			
		||||
				left: 24px;
 | 
			
		||||
				z-index: 2;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			:global(.bookshelf__book-content),
 | 
			
		||||
			:global(.fit-text) {
 | 
			
		||||
				width: calc(200px - 31px) !important;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			:global(.bookshelf__book-content) {
 | 
			
		||||
				margin: 0 31px;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			@ -1,61 +0,0 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
	import { fitText } from "@ui/directives";
 | 
			
		||||
	import { wrap } from "module";
 | 
			
		||||
 | 
			
		||||
	interface Props {
 | 
			
		||||
		title?: string;
 | 
			
		||||
		subtitle?: string;
 | 
			
		||||
		allowWrap?: boolean;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	let { title, subtitle, allowWrap }: Props = $props();
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<div
 | 
			
		||||
	class="bookshelf__book-content"
 | 
			
		||||
	class:center-content={subtitle === undefined}
 | 
			
		||||
>
 | 
			
		||||
	{#if title}
 | 
			
		||||
		<h2
 | 
			
		||||
			class="bookshelf__book-title fit-text"
 | 
			
		||||
			class:wrap={allowWrap}
 | 
			
		||||
			use:fitText={{
 | 
			
		||||
				minFontSize: 12,
 | 
			
		||||
				maxFontSize: 16,
 | 
			
		||||
				multiLine: allowWrap,
 | 
			
		||||
				detectMultiLine: false,
 | 
			
		||||
			}}
 | 
			
		||||
		>
 | 
			
		||||
			{title}
 | 
			
		||||
		</h2>
 | 
			
		||||
	{/if}
 | 
			
		||||
	{#if subtitle}
 | 
			
		||||
		<h4
 | 
			
		||||
			class="bookshelf__book-subtitle fit-text"
 | 
			
		||||
			use:fitText={{
 | 
			
		||||
				minFontSize: 10,
 | 
			
		||||
				maxFontSize: 14,
 | 
			
		||||
				multiLine: allowWrap,
 | 
			
		||||
				detectMultiLine: false,
 | 
			
		||||
			}}
 | 
			
		||||
		>
 | 
			
		||||
			{subtitle}
 | 
			
		||||
		</h4>
 | 
			
		||||
	{/if}
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<style lang="scss">
 | 
			
		||||
	.bookshelf__book-title,
 | 
			
		||||
	.bookshelf__book-subtitle {
 | 
			
		||||
		width: 100% !important;
 | 
			
		||||
 | 
			
		||||
		:global(.textFitted) {
 | 
			
		||||
			width: 100%;
 | 
			
		||||
			word-wrap: break-word;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.bookshelf__book-subtitle {
 | 
			
		||||
		overflow: hidden;
 | 
			
		||||
	}
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			@ -1,51 +0,0 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
	import type { Snippet } from "svelte";
 | 
			
		||||
 | 
			
		||||
	interface Props {
 | 
			
		||||
		children?: Snippet;
 | 
			
		||||
		width?: number;
 | 
			
		||||
		design?: "default" | "colored-spine" | "dual-top-bands" | "split-bands";
 | 
			
		||||
		topMargin?: number;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	let {
 | 
			
		||||
		children,
 | 
			
		||||
		width = 40,
 | 
			
		||||
		design = "default",
 | 
			
		||||
		topMargin,
 | 
			
		||||
	}: Props = $props();
 | 
			
		||||
 | 
			
		||||
	function getTopMargin(
 | 
			
		||||
		design: "default" | "colored-spine" | "dual-top-bands" | "split-bands",
 | 
			
		||||
	): number {
 | 
			
		||||
		switch (design) {
 | 
			
		||||
			case "split-bands":
 | 
			
		||||
				return 30;
 | 
			
		||||
			case "dual-top-bands":
 | 
			
		||||
				return 41;
 | 
			
		||||
		}
 | 
			
		||||
		return 0;
 | 
			
		||||
	}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<div
 | 
			
		||||
	class="bookshelf__book-wrapper bookshelf__book-tilted"
 | 
			
		||||
	style:--book-tilted-width={width + "px"}
 | 
			
		||||
	style:--book-tilted-top-margin={(topMargin ?? getTopMargin(design)) + "px"}
 | 
			
		||||
>
 | 
			
		||||
	{@render children?.()}
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<style lang="scss">
 | 
			
		||||
	div {
 | 
			
		||||
		:global(.bookshelf__book-content),
 | 
			
		||||
		:global(.fit-text) {
 | 
			
		||||
			height: calc(var(--book-tilted-width));
 | 
			
		||||
			width: calc(200px - 60px);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		:global(.bookshelf__book-content) {
 | 
			
		||||
			margin: var(--book-tilted-top-margin) var(--book-tilted-width);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			@ -1,32 +1,88 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
	import "./bookshelf.scss";
 | 
			
		||||
	import type { Snippet } from "svelte";
 | 
			
		||||
	import { type Snippet } from "svelte";
 | 
			
		||||
	import type { ActionReturn } from "svelte/action";
 | 
			
		||||
	import textFit from "textfit";
 | 
			
		||||
 | 
			
		||||
	type Props = {
 | 
			
		||||
	interface Props {
 | 
			
		||||
		children?: Snippet;
 | 
			
		||||
	};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	let { children }: Props = $props();
 | 
			
		||||
 | 
			
		||||
	const color = "#a47148";
 | 
			
		||||
	const fitAllText = (el: HTMLDivElement): ActionReturn => {
 | 
			
		||||
		const observer = new MutationObserver(() => {
 | 
			
		||||
			requestAnimationFrame(() => {
 | 
			
		||||
				textFit(el.getElementsByClassName("book-title"), {
 | 
			
		||||
					minFontSize: 8,
 | 
			
		||||
					maxFontSize: 16,
 | 
			
		||||
					multiLine: true,
 | 
			
		||||
				});
 | 
			
		||||
 | 
			
		||||
				textFit(el.getElementsByClassName("book-subtitle"), {
 | 
			
		||||
					minFontSize: 6,
 | 
			
		||||
					maxFontSize: 14,
 | 
			
		||||
					multiLine: true,
 | 
			
		||||
				});
 | 
			
		||||
			});
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		observer.observe(el, {
 | 
			
		||||
			childList: true,
 | 
			
		||||
			subtree: true,
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		return {
 | 
			
		||||
			destroy() {
 | 
			
		||||
				observer.disconnect();
 | 
			
		||||
			},
 | 
			
		||||
		};
 | 
			
		||||
	};
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<div class="bookshelf__wrapper" style:--book-shelf-color={color}>
 | 
			
		||||
<div class="bookshelf" use:fitAllText>
 | 
			
		||||
	{@render children?.()}
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<style lang="scss">
 | 
			
		||||
	div {
 | 
			
		||||
	@use "sass:color";
 | 
			
		||||
	@use "./variables.scss" as bookshelf;
 | 
			
		||||
 | 
			
		||||
	div.bookshelf {
 | 
			
		||||
		width: 80%;
 | 
			
		||||
		margin: 0 auto;
 | 
			
		||||
		min-height: 100vh;
 | 
			
		||||
		min-height: calc(bookshelf.$height + bookshelf.$shelf-width);
 | 
			
		||||
		background-image: linear-gradient(
 | 
			
		||||
			color-mix(in srgb, var(--book-shelf-color), black 32%),
 | 
			
		||||
			color-mix(in srgb, var(--book-shelf-color), black 30%) 220px,
 | 
			
		||||
			color-mix(in srgb, var(--book-shelf-color), white 4%) 220px,
 | 
			
		||||
			color-mix(in srgb, var(--book-shelf-color), white 4%) 222px,
 | 
			
		||||
			var(--book-shelf-color) 222px,
 | 
			
		||||
			var(--book-shelf-color) 228px,
 | 
			
		||||
			color-mix(in srgb, var(--book-shelf-color), black 4%) 228px,
 | 
			
		||||
			color-mix(in srgb, var(--book-shelf-color), black 4%) 230px
 | 
			
		||||
			color.scale(bookshelf.$color, $lightness: -32%),
 | 
			
		||||
			color.scale(bookshelf.$color, $lightness: -30%) bookshelf.$height,
 | 
			
		||||
 | 
			
		||||
			color.scale(bookshelf.$color, $lightness: bookshelf.$shadow-pct)
 | 
			
		||||
				bookshelf.$height,
 | 
			
		||||
			color.scale(bookshelf.$color, $lightness: bookshelf.$shadow-pct)
 | 
			
		||||
				calc(bookshelf.$height + bookshelf.$shadow-width),
 | 
			
		||||
			bookshelf.$color bookshelf.$height,
 | 
			
		||||
			bookshelf.$color calc(bookshelf.$height + bookshelf.$shelf-width),
 | 
			
		||||
			color.scale(
 | 
			
		||||
					bookshelf.$color,
 | 
			
		||||
					$lightness: -1 * bookshelf.$shadow-pct
 | 
			
		||||
				)
 | 
			
		||||
				calc(
 | 
			
		||||
					bookshelf.$height + bookshelf.$shelf-width -
 | 
			
		||||
						bookshelf.$shadow-width
 | 
			
		||||
				),
 | 
			
		||||
			color.scale(
 | 
			
		||||
					bookshelf.$color,
 | 
			
		||||
					$lightness: -1 * bookshelf.$shadow-pct
 | 
			
		||||
				)
 | 
			
		||||
				calc(bookshelf.$height + bookshelf.$shelf-width)
 | 
			
		||||
		);
 | 
			
		||||
		border: 10px var(--book-shelf-color) solid;
 | 
			
		||||
		background-size: 10px calc(bookshelf.$height + bookshelf.$shelf-width);
 | 
			
		||||
		border: 10px bookshelf.$color solid;
 | 
			
		||||
		padding-left: bookshelf.$book-spacing;
 | 
			
		||||
		padding-right: bookshelf.$book-spacing;
 | 
			
		||||
 | 
			
		||||
		display: flex;
 | 
			
		||||
		flex-wrap: wrap;
 | 
			
		||||
		gap: bookshelf.$shelf-width bookshelf.$book-spacing;
 | 
			
		||||
	}
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,30 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
	import type { Snippet } from "svelte";
 | 
			
		||||
 | 
			
		||||
	interface Props {
 | 
			
		||||
		children?: Snippet;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	let { children }: Props = $props();
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<div class="bookshelf-item">
 | 
			
		||||
	{@render children?.()}
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<style lang="scss">
 | 
			
		||||
	@use "./variables.scss" as bookshelf;
 | 
			
		||||
 | 
			
		||||
	div.bookshelf-item {
 | 
			
		||||
		box-sizing: border-box;
 | 
			
		||||
		height: bookshelf.$height;
 | 
			
		||||
		display: flex;
 | 
			
		||||
		align-items: end;
 | 
			
		||||
 | 
			
		||||
		// Why????
 | 
			
		||||
		padding-bottom: 0.75px;
 | 
			
		||||
		&:global(:has(.book.tilted)) {
 | 
			
		||||
			padding-bottom: 0;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			@ -1,217 +0,0 @@
 | 
			
		|||
$white: white;
 | 
			
		||||
$black: black;
 | 
			
		||||
$background: #dedede;
 | 
			
		||||
 | 
			
		||||
$bookWidth: 40px;
 | 
			
		||||
$bookHeight: 200px;
 | 
			
		||||
$bookEdge: 2px;
 | 
			
		||||
 | 
			
		||||
.bookshelf__wrapper {
 | 
			
		||||
	width: 80%;
 | 
			
		||||
	height: 100%;
 | 
			
		||||
	margin: 0 auto;
 | 
			
		||||
	overflow: hidden;
 | 
			
		||||
	background-size: 10px 230px;
 | 
			
		||||
	min-height: 230px;
 | 
			
		||||
 | 
			
		||||
	.bookshelf__bookStack-wrapper {
 | 
			
		||||
		width: 200px;
 | 
			
		||||
		height: 100%;
 | 
			
		||||
		display: inline-flex;
 | 
			
		||||
		flex-flow: column nowrap;
 | 
			
		||||
		list-style: none;
 | 
			
		||||
		float: left;
 | 
			
		||||
		margin: 0;
 | 
			
		||||
		padding: 0;
 | 
			
		||||
 | 
			
		||||
		.bookshelf__bookStack-outOfStock {
 | 
			
		||||
			background: #232323;
 | 
			
		||||
			color: #fff;
 | 
			
		||||
			height: 150px;
 | 
			
		||||
			padding: 0;
 | 
			
		||||
			margin: 50px 1px auto 1px;
 | 
			
		||||
			display: flex;
 | 
			
		||||
			flex-flow: column nowrap;
 | 
			
		||||
			justify-content: space-around;
 | 
			
		||||
			align-items: center;
 | 
			
		||||
			font-size: 1.1em;
 | 
			
		||||
			font-weight: 600;
 | 
			
		||||
			letter-spacing: 1.25px;
 | 
			
		||||
			border-radius: 6px;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		.bookshelf__bookstack-elem {
 | 
			
		||||
			margin-inline-start: 0;
 | 
			
		||||
			margin: 0;
 | 
			
		||||
			padding: 0;
 | 
			
		||||
 | 
			
		||||
			.bookshelf__book-wrapper {
 | 
			
		||||
				width: 100%;
 | 
			
		||||
				height: 40px;
 | 
			
		||||
				margin: 0;
 | 
			
		||||
				padding: 0;
 | 
			
		||||
 | 
			
		||||
				.bookshelf__book-content {
 | 
			
		||||
					height: $bookWidth;
 | 
			
		||||
					width: $bookHeight;
 | 
			
		||||
 | 
			
		||||
					transform-origin: 0% 0%;
 | 
			
		||||
					transform: rotate(0deg);
 | 
			
		||||
					overflow: hidden;
 | 
			
		||||
 | 
			
		||||
					// * Centering content
 | 
			
		||||
					display: flex;
 | 
			
		||||
					flex-flow: column nowrap;
 | 
			
		||||
					justify-content: center;
 | 
			
		||||
					align-items: center;
 | 
			
		||||
 | 
			
		||||
					.bookshelf__book-title,
 | 
			
		||||
					.bookshelf__book-subtitle {
 | 
			
		||||
						font-size: 0.8em;
 | 
			
		||||
						font-weight: 600;
 | 
			
		||||
 | 
			
		||||
						height: calc($bookWidth / 2);
 | 
			
		||||
						width: $bookHeight;
 | 
			
		||||
 | 
			
		||||
						padding: 0;
 | 
			
		||||
						margin: 0;
 | 
			
		||||
 | 
			
		||||
						// * Centering content
 | 
			
		||||
						display: flex;
 | 
			
		||||
						justify-content: center;
 | 
			
		||||
						align-items: center;
 | 
			
		||||
						text-align: center;
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					.bookshelf__book-subtitle {
 | 
			
		||||
						font-size: 0.6em;
 | 
			
		||||
						letter-spacing: 1px;
 | 
			
		||||
						font-weight: 400;
 | 
			
		||||
						font-style: italic;
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				.center-content {
 | 
			
		||||
					display: flex;
 | 
			
		||||
					justify-content: center;
 | 
			
		||||
					align-items: center;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				// .bookshelf__book-title{
 | 
			
		||||
				//     width: 140px;
 | 
			
		||||
				//     height: 40px;
 | 
			
		||||
				//     transform: rotate(0deg);
 | 
			
		||||
				//     margin-left: 29px;
 | 
			
		||||
				//     margin-top: 0;
 | 
			
		||||
				//     text-align: center;
 | 
			
		||||
				// }
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.bookshelf__book-wrapper {
 | 
			
		||||
		height: $bookHeight;
 | 
			
		||||
		width: $bookWidth;
 | 
			
		||||
		float: left;
 | 
			
		||||
		margin: 20px 1px 10px 1px;
 | 
			
		||||
		border-radius: 6px;
 | 
			
		||||
		transition: transform 0.4s ease;
 | 
			
		||||
		position: relative;
 | 
			
		||||
 | 
			
		||||
		.bookshelf__book-content {
 | 
			
		||||
			width: $bookHeight;
 | 
			
		||||
 | 
			
		||||
			transform-origin: 0% 0%;
 | 
			
		||||
			transform: rotate(90deg);
 | 
			
		||||
			overflow: hidden;
 | 
			
		||||
 | 
			
		||||
			// * Centering content
 | 
			
		||||
			display: flex;
 | 
			
		||||
			flex-flow: column nowrap;
 | 
			
		||||
			justify-content: center;
 | 
			
		||||
			align-items: center;
 | 
			
		||||
 | 
			
		||||
			.bookshelf__book-title,
 | 
			
		||||
			.bookshelf__book-subtitle {
 | 
			
		||||
				font-size: 0.8em;
 | 
			
		||||
				font-weight: 600;
 | 
			
		||||
 | 
			
		||||
				height: calc($bookWidth / 2);
 | 
			
		||||
				width: $bookHeight;
 | 
			
		||||
 | 
			
		||||
				padding: 0;
 | 
			
		||||
				margin: 0;
 | 
			
		||||
 | 
			
		||||
				// * Centering content
 | 
			
		||||
				display: flex;
 | 
			
		||||
				justify-content: center;
 | 
			
		||||
				align-items: center;
 | 
			
		||||
				text-align: center;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			.bookshelf__book-subtitle {
 | 
			
		||||
				font-size: 0.6em;
 | 
			
		||||
				letter-spacing: 1px;
 | 
			
		||||
				font-weight: 400;
 | 
			
		||||
				font-style: italic;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		&:hover {
 | 
			
		||||
			transform: scale(1.05);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		.center-content {
 | 
			
		||||
			display: flex;
 | 
			
		||||
			justify-content: center;
 | 
			
		||||
			align-items: center;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.bookshelf__book-tilted {
 | 
			
		||||
		float: left;
 | 
			
		||||
		width: 72px;
 | 
			
		||||
 | 
			
		||||
		.bookshelf__book-wrapper {
 | 
			
		||||
			--book-width: 40px !important;
 | 
			
		||||
			width: 40px !important;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		&:hover .bookshelf__book-wrapper {
 | 
			
		||||
			transform: translateY(-20px);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		& > .bookshelf__book-wrapper {
 | 
			
		||||
			transform: translateY(-22px) translateX(13px) rotate(9deg);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.bookshelf__book-onDisplay {
 | 
			
		||||
		.bookshelf__book-content {
 | 
			
		||||
			width: calc(100% - 11px);
 | 
			
		||||
			margin-left: 11px;
 | 
			
		||||
			transform: rotate(0deg);
 | 
			
		||||
			overflow: visible;
 | 
			
		||||
 | 
			
		||||
			.bookshelf__book-title,
 | 
			
		||||
			.bookshelf__book-subtitle {
 | 
			
		||||
				margin-top: 16px;
 | 
			
		||||
				width: 100%;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			.bookshelf__book-title {
 | 
			
		||||
				word-wrap: break-word;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			.bookshelf__book-subtitle {
 | 
			
		||||
				white-space: nowrap;
 | 
			
		||||
				text-overflow: ellipsis;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		.bookshelf__book-author {
 | 
			
		||||
			font-size: 0.8em;
 | 
			
		||||
			margin-left: 13px;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,47 @@
 | 
			
		|||
@use "sass:math";
 | 
			
		||||
 | 
			
		||||
$color: #a47148;
 | 
			
		||||
$height: 220px;
 | 
			
		||||
$shelf-width: 10px;
 | 
			
		||||
$shadow-pct: 4%;
 | 
			
		||||
$shadow-width: calc($shelf-width / 5);
 | 
			
		||||
$book-spacing: 2px;
 | 
			
		||||
$book-padding: 4px;
 | 
			
		||||
$book-corner-radius: 3.5px;
 | 
			
		||||
$book-hover-scale: 1.02;
 | 
			
		||||
$book-band-offset: 10px;
 | 
			
		||||
$book-band-gap: 5px;
 | 
			
		||||
$book-band-width: 10px;
 | 
			
		||||
$book-crease-offset: 10px;
 | 
			
		||||
$book-crease-width: 2px;
 | 
			
		||||
$tilt-angle: 9deg;
 | 
			
		||||
 | 
			
		||||
@mixin tilt() {
 | 
			
		||||
	$width: var(--book-width);
 | 
			
		||||
	$height: var(--book-height);
 | 
			
		||||
	$sin: math.sin($tilt-angle);
 | 
			
		||||
	$cos: math.cos($tilt-angle);
 | 
			
		||||
 | 
			
		||||
	// $newWidth: calc(abs($width * $cos) + abs($height * $sin));
 | 
			
		||||
	$newWidth: calc(
 | 
			
		||||
		max(($width * $cos), -1 * ($width * $cos)) +
 | 
			
		||||
			max(($height * $sin), -1 * ($height * $sin))
 | 
			
		||||
	);
 | 
			
		||||
	$margin-h: calc(($newWidth - $width) / 2);
 | 
			
		||||
 | 
			
		||||
	// $newHeight: calc(abs($width * $sin) + abs($height * $cos));
 | 
			
		||||
	$newHeight: calc(
 | 
			
		||||
		max(($width * $sin), -1 * ($width * $sin)) +
 | 
			
		||||
			max(($height * $cos), -1 * ($height * $cos))
 | 
			
		||||
	);
 | 
			
		||||
	$margin-v: calc(($newHeight - $height) / 2);
 | 
			
		||||
 | 
			
		||||
	transform: rotate($tilt-angle);
 | 
			
		||||
	margin: $margin-v $margin-h;
 | 
			
		||||
	transition: transform 0.4s ease, margin 0.4s ease;
 | 
			
		||||
 | 
			
		||||
	&:hover {
 | 
			
		||||
		margin-top: 0;
 | 
			
		||||
		margin-bottom: 0;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,16 +0,0 @@
 | 
			
		|||
import type { ActionReturn } from "svelte/action";
 | 
			
		||||
import { default as doTextFit, type TextFitOption } from "textfit";
 | 
			
		||||
 | 
			
		||||
export function fitText(
 | 
			
		||||
	el: HTMLElement,
 | 
			
		||||
	opts: TextFitOption = {}
 | 
			
		||||
): ActionReturn<TextFitOption> {
 | 
			
		||||
	doTextFit(el, opts);
 | 
			
		||||
 | 
			
		||||
	return {
 | 
			
		||||
		update(opts) {
 | 
			
		||||
			doTextFit(el, opts);
 | 
			
		||||
		},
 | 
			
		||||
		destroy() {},
 | 
			
		||||
	};
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,3 +1,2 @@
 | 
			
		|||
export { chart } from "./chart";
 | 
			
		||||
export { clickOutside } from "./clickOutside";
 | 
			
		||||
export { fitText } from "./fitText";
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue