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",
|
"@lezer/lr",
|
||||||
...builtins,
|
...builtins,
|
||||||
],
|
],
|
||||||
|
conditions: ["svelte"],
|
||||||
format: "cjs",
|
format: "cjs",
|
||||||
target: "es2018",
|
target: "es2018",
|
||||||
logLevel: "info",
|
logLevel: "info",
|
||||||
|
|
|
@ -44,9 +44,13 @@
|
||||||
"typescript": "5.0.4"
|
"typescript": "5.0.4"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@humanspeak/svelte-virtual-list": "^0.2.6",
|
||||||
|
"@leveluptuts/svelte-fit": "^1.0.3",
|
||||||
"chart.js": "^4.5.0",
|
"chart.js": "^4.5.0",
|
||||||
"chroma-js": "^3.1.2",
|
"chroma-js": "^3.1.2",
|
||||||
"esbuild-sass-plugin": "^3.3.1",
|
"esbuild-sass-plugin": "^3.3.1",
|
||||||
|
"fitty": "^2.4.2",
|
||||||
|
"just-memoize": "^2.2.0",
|
||||||
"textfit": "^2.4.0",
|
"textfit": "^2.4.0",
|
||||||
"uuid": "^11.1.0",
|
"uuid": "^11.1.0",
|
||||||
"yaml": "^2.8.0",
|
"yaml": "^2.8.0",
|
||||||
|
|
|
@ -8,6 +8,12 @@ importers:
|
||||||
|
|
||||||
.:
|
.:
|
||||||
dependencies:
|
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:
|
chart.js:
|
||||||
specifier: ^4.5.0
|
specifier: ^4.5.0
|
||||||
version: 4.5.0
|
version: 4.5.0
|
||||||
|
@ -17,6 +23,12 @@ importers:
|
||||||
esbuild-sass-plugin:
|
esbuild-sass-plugin:
|
||||||
specifier: ^3.3.1
|
specifier: ^3.3.1
|
||||||
version: 3.3.1(esbuild@0.17.3)(sass-embedded@1.89.2)
|
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:
|
textfit:
|
||||||
specifier: ^2.4.0
|
specifier: ^2.4.0
|
||||||
version: 2.4.0
|
version: 2.4.0
|
||||||
|
@ -312,6 +324,11 @@ packages:
|
||||||
resolution: {integrity: sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==}
|
resolution: {integrity: sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==}
|
||||||
engines: {node: '>=18.18.0'}
|
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':
|
'@humanwhocodes/module-importer@1.0.1':
|
||||||
resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==}
|
resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==}
|
||||||
engines: {node: '>=12.22'}
|
engines: {node: '>=12.22'}
|
||||||
|
@ -345,6 +362,9 @@ packages:
|
||||||
'@kurkle/color@0.3.4':
|
'@kurkle/color@0.3.4':
|
||||||
resolution: {integrity: sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==}
|
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':
|
'@marijn/find-cluster-break@1.0.2':
|
||||||
resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==}
|
resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==}
|
||||||
|
|
||||||
|
@ -885,6 +905,9 @@ packages:
|
||||||
resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
|
resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
fitty@2.4.2:
|
||||||
|
resolution: {integrity: sha512-GNhWgImK4+wEkgEZjBkQMyu5NLSmmryg/CaRP7zYby+TWzCrUou6BHL+iqbjKzJRXMyzuJkH+LBB1+lh4oO77g==}
|
||||||
|
|
||||||
flat-cache@4.0.1:
|
flat-cache@4.0.1:
|
||||||
resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==}
|
resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==}
|
||||||
engines: {node: '>=16'}
|
engines: {node: '>=16'}
|
||||||
|
@ -1138,6 +1161,9 @@ packages:
|
||||||
json-stable-stringify-without-jsonify@1.0.1:
|
json-stable-stringify-without-jsonify@1.0.1:
|
||||||
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
|
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
|
||||||
|
|
||||||
|
just-memoize@2.2.0:
|
||||||
|
resolution: {integrity: sha512-zriv+MY+61RXT0QsrO1ZJtL5umouqqSWmCGBkp2wJm35kniunBAA4qhUKx8Lvg/QcwrF9xuw9E6PkevKFf4boQ==}
|
||||||
|
|
||||||
keyv@4.5.4:
|
keyv@4.5.4:
|
||||||
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
|
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
|
||||||
|
|
||||||
|
@ -1951,6 +1977,11 @@ snapshots:
|
||||||
'@humanfs/core': 0.19.1
|
'@humanfs/core': 0.19.1
|
||||||
'@humanwhocodes/retry': 0.3.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/module-importer@1.0.1': {}
|
||||||
|
|
||||||
'@humanwhocodes/retry@0.3.1': {}
|
'@humanwhocodes/retry@0.3.1': {}
|
||||||
|
@ -1976,6 +2007,8 @@ snapshots:
|
||||||
|
|
||||||
'@kurkle/color@0.3.4': {}
|
'@kurkle/color@0.3.4': {}
|
||||||
|
|
||||||
|
'@leveluptuts/svelte-fit@1.0.3': {}
|
||||||
|
|
||||||
'@marijn/find-cluster-break@1.0.2': {}
|
'@marijn/find-cluster-break@1.0.2': {}
|
||||||
|
|
||||||
'@nodelib/fs.scandir@2.1.5':
|
'@nodelib/fs.scandir@2.1.5':
|
||||||
|
@ -2608,6 +2641,8 @@ snapshots:
|
||||||
locate-path: 6.0.0
|
locate-path: 6.0.0
|
||||||
path-exists: 4.0.0
|
path-exists: 4.0.0
|
||||||
|
|
||||||
|
fitty@2.4.2: {}
|
||||||
|
|
||||||
flat-cache@4.0.1:
|
flat-cache@4.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
flatted: 3.3.3
|
flatted: 3.3.3
|
||||||
|
@ -2869,6 +2904,8 @@ snapshots:
|
||||||
|
|
||||||
json-stable-stringify-without-jsonify@1.0.1: {}
|
json-stable-stringify-without-jsonify@1.0.1: {}
|
||||||
|
|
||||||
|
just-memoize@2.2.0: {}
|
||||||
|
|
||||||
keyv@4.5.4:
|
keyv@4.5.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
json-buffer: 3.0.1
|
json-buffer: 3.0.1
|
||||||
|
|
|
@ -4,7 +4,6 @@
|
||||||
import Book from "@ui/components/bookshelf/Book.svelte";
|
import Book from "@ui/components/bookshelf/Book.svelte";
|
||||||
import Bookshelf from "@ui/components/bookshelf/Bookshelf.svelte";
|
import Bookshelf from "@ui/components/bookshelf/Bookshelf.svelte";
|
||||||
import BookStack from "@ui/components/bookshelf/BookStack.svelte";
|
import BookStack from "@ui/components/bookshelf/BookStack.svelte";
|
||||||
import BookStackElement from "@ui/components/bookshelf/BookStackElement.svelte";
|
|
||||||
import {
|
import {
|
||||||
createMetadata,
|
createMetadata,
|
||||||
setMetadataContext,
|
setMetadataContext,
|
||||||
|
@ -22,6 +21,8 @@
|
||||||
import DateFilter from "@ui/components/DateFilter.svelte";
|
import DateFilter from "@ui/components/DateFilter.svelte";
|
||||||
import Rating from "@ui/components/Rating.svelte";
|
import Rating from "@ui/components/Rating.svelte";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
import memoize from "just-memoize";
|
||||||
|
import VirtualList from "@humanspeak/svelte-virtual-list";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
plugin: BookTrackerPlugin;
|
plugin: BookTrackerPlugin;
|
||||||
|
@ -32,11 +33,11 @@
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
subtitle?: string;
|
subtitle?: string;
|
||||||
author: string;
|
authors: string[];
|
||||||
width: number;
|
width: number;
|
||||||
color: ColorName;
|
color: ColorName;
|
||||||
design: (typeof designs)[number];
|
design: (typeof designs)[number];
|
||||||
orientation: undefined | "tilted" | "on-display";
|
orientation: "default" | "tilted" | "front";
|
||||||
file: TFile;
|
file: TFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,12 +56,7 @@
|
||||||
const metadataStore = createMetadata(plugin, settings.statusFilter, true);
|
const metadataStore = createMetadata(plugin, settings.statusFilter, true);
|
||||||
setMetadataContext(metadataStore);
|
setMetadataContext(metadataStore);
|
||||||
|
|
||||||
const designs = [
|
const designs = ["default", "dual-top-bands", "split-bands"] as const;
|
||||||
"default",
|
|
||||||
"colored-spine",
|
|
||||||
"dual-top-bands",
|
|
||||||
"split-bands",
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
const randomDesign = () => randomElement(designs);
|
const randomDesign = () => randomElement(designs);
|
||||||
const randomColor = () => randomElement(COLOR_NAMES);
|
const randomColor = () => randomElement(COLOR_NAMES);
|
||||||
|
@ -68,11 +64,11 @@
|
||||||
const n = randomFloat();
|
const n = randomFloat();
|
||||||
|
|
||||||
if (n < 0.55) {
|
if (n < 0.55) {
|
||||||
return undefined;
|
return "default";
|
||||||
} else if (n < 0.8) {
|
} else if (n < 0.8) {
|
||||||
return "tilted";
|
return "tilted";
|
||||||
} else {
|
} else {
|
||||||
return "on-display";
|
return "front";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const randomStackChance = () => randomFloat() > 0.9;
|
const randomStackChance = () => randomFloat() > 0.9;
|
||||||
|
@ -91,23 +87,34 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getBookData(metadata: FileMetadata): BookData {
|
const getBookData = memoize(
|
||||||
return {
|
(metadata: FileMetadata): BookData => {
|
||||||
id: metadata.file.path,
|
const orientation = randomOrientation();
|
||||||
title: metadata.frontmatter[settings.titleProperty],
|
|
||||||
subtitle: settings.subtitleProperty
|
return {
|
||||||
? metadata.frontmatter[settings.subtitleProperty]
|
id: metadata.file.path,
|
||||||
: undefined,
|
title: metadata.frontmatter[settings.titleProperty],
|
||||||
author: metadata.frontmatter[settings.authorsProperty].join(", "),
|
subtitle: settings.subtitleProperty
|
||||||
width: metadata.frontmatter[
|
? metadata.frontmatter[settings.subtitleProperty]
|
||||||
settingsStore.settings.pageCountProperty
|
: undefined,
|
||||||
],
|
authors: metadata.frontmatter[settings.authorsProperty],
|
||||||
color: randomColor(),
|
width: Math.min(
|
||||||
design: randomDesign(),
|
Math.max(
|
||||||
orientation: randomOrientation(),
|
20,
|
||||||
file: metadata.file,
|
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 view = $state(settings.defaultView);
|
||||||
let books: (BookData | BookStackData)[] = $state([]);
|
let books: (BookData | BookStackData)[] = $state([]);
|
||||||
|
@ -156,13 +163,14 @@
|
||||||
<Bookshelf>
|
<Bookshelf>
|
||||||
{#each books as book (book.id)}
|
{#each books as book (book.id)}
|
||||||
{#if "books" in book}
|
{#if "books" in book}
|
||||||
<BookStack totalChildren={book.books.length}>
|
<BookStack>
|
||||||
{#each book.books as bookData (bookData.id)}
|
{#each book.books as bookData (bookData.id)}
|
||||||
<BookStackElement
|
<Book
|
||||||
title={bookData.title}
|
title={bookData.title}
|
||||||
subtitle={bookData.subtitle}
|
subtitle={bookData.subtitle}
|
||||||
color={bookData.color}
|
color={bookData.color}
|
||||||
design={bookData.design}
|
design={bookData.design}
|
||||||
|
orientation="flat"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
plugin.app.workspace
|
plugin.app.workspace
|
||||||
.getLeaf("tab")
|
.getLeaf("tab")
|
||||||
|
@ -174,7 +182,7 @@
|
||||||
<Book
|
<Book
|
||||||
title={book.title}
|
title={book.title}
|
||||||
subtitle={book.subtitle}
|
subtitle={book.subtitle}
|
||||||
author={book.author}
|
authors={book.authors}
|
||||||
width={book.width}
|
width={book.width}
|
||||||
color={book.color}
|
color={book.color}
|
||||||
design={book.design}
|
design={book.design}
|
||||||
|
@ -275,7 +283,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style lang="scss">
|
||||||
.shelf-code-block {
|
.shelf-code-block {
|
||||||
.controls {
|
.controls {
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
|
|
|
@ -1,205 +1,327 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Self from "./Book.svelte";
|
import type { Snippet } from "svelte";
|
||||||
import { Color, isColorName, type ColorName } from "@utils/color";
|
import BookshelfItem from "./BookshelfItem.svelte";
|
||||||
import BookTilted from "./BookTilted.svelte";
|
import { Color, type ColorName } from "@utils/color";
|
||||||
import BookOnDisplay from "./BookOnDisplay.svelte";
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
import BookText from "./BookText.svelte";
|
import { fit } from "@leveluptuts/svelte-fit";
|
||||||
const BOOK_SIZE_DEFAULT: number = 40;
|
|
||||||
const BOOK_SIZE_MIN: number = 15;
|
|
||||||
|
|
||||||
interface BookProps {
|
interface Props {
|
||||||
|
children?: Snippet;
|
||||||
title?: string;
|
title?: string;
|
||||||
subtitle?: string;
|
subtitle?: string;
|
||||||
author?: string;
|
authors?: string[];
|
||||||
color?: ColorName | string;
|
|
||||||
design?: "default" | "colored-spine" | "dual-top-bands" | "split-bands";
|
|
||||||
orientation?: "tilted" | "on-display";
|
|
||||||
height?: number;
|
height?: number;
|
||||||
width?: 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;
|
onClick?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
title,
|
title,
|
||||||
subtitle,
|
subtitle,
|
||||||
author,
|
authors,
|
||||||
color: colorRaw = "green",
|
height = 200,
|
||||||
|
width = 40,
|
||||||
|
color: colorName = "blue",
|
||||||
|
orientation = "default",
|
||||||
design = "default",
|
design = "default",
|
||||||
orientation,
|
role = "link",
|
||||||
height,
|
tabindex = 0,
|
||||||
width,
|
|
||||||
onClick,
|
onClick,
|
||||||
}: BookProps = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
function normalizeWidth(input: number | undefined) {
|
$effect(() => {
|
||||||
if (input) {
|
if (orientation === "front" && design !== "default") {
|
||||||
if (input <= 150) {
|
console.warn(
|
||||||
return BOOK_SIZE_MIN;
|
"The front orientation does not support different designs.",
|
||||||
}
|
);
|
||||||
return input / 10;
|
|
||||||
}
|
}
|
||||||
return BOOK_SIZE_DEFAULT;
|
});
|
||||||
}
|
|
||||||
|
|
||||||
const color = $derived(
|
const color = $derived(Color.fromName(colorName));
|
||||||
isColorName(colorRaw)
|
const backgroundColor = $derived(color.chroma.css());
|
||||||
? Color.fromName(colorRaw).darken()
|
const textColor = $derived(color.contrastColor.chroma.css());
|
||||||
: 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));
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if orientation}
|
<BookshelfItem>
|
||||||
{#if orientation === "tilted"}
|
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
||||||
<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}
|
|
||||||
<div
|
<div
|
||||||
class="bookshelf__book-wrapper"
|
class="book"
|
||||||
class:default={design === "default"}
|
class:tilted={orientation === "tilted"}
|
||||||
class:colored-spine={design === "colored-spine"}
|
class:flat={orientation === "flat"}
|
||||||
class:dual-top-bands={design === "dual-top-bands"}
|
class:front={orientation === "front"}
|
||||||
class:split-bands={design === "split-bands"}
|
style:--book-height="{height}px"
|
||||||
style:--book-color={backgroundColor.css()}
|
style:--book-width="{width}px"
|
||||||
style:--book-border-left-color={borderLeftColor.css()}
|
style:--book-bg-color={backgroundColor}
|
||||||
style:--book-border-right-color={borderRightColor.css()}
|
style:--book-text-color={textColor}
|
||||||
style:--book-band-color={bandColor.css()}
|
style:cursor={onClick ? "pointer" : "default"}
|
||||||
style:--book-width={verifiedWidth + "px"}
|
{role}
|
||||||
style:width={verifiedWidth + "px"}
|
{tabindex}
|
||||||
style:color={textColor}
|
|
||||||
onclick={onClick}
|
onclick={onClick}
|
||||||
onkeydown={(ev) => ev.key === "Enter" && onClick?.()}
|
onkeypress={(ev) => ev.key === "Enter" && onClick?.()}
|
||||||
role="link"
|
|
||||||
tabindex="0"
|
|
||||||
>
|
>
|
||||||
<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>
|
</div>
|
||||||
{/if}
|
</BookshelfItem>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
div.bookshelf__book-wrapper {
|
@use "./variables.scss" as bookshelf;
|
||||||
background: var(--book-color);
|
|
||||||
border-left: 2px solid var(--book-border-left-color);
|
|
||||||
border-right: 2px solid var(--book-border-right-color);
|
|
||||||
|
|
||||||
&.default,
|
$small-band-width: calc(bookshelf.$book-band-width / 1.5);
|
||||||
&.colored-spine {
|
$band-color: rgba(0, 0, 0, 0.2);
|
||||||
:global(.bookshelf__book-content) {
|
$band-top-offset: bookshelf.$book-band-offset;
|
||||||
height: calc(var(--book-width));
|
$band-top2-offset: $band-top-offset + $small-band-width +
|
||||||
margin: 0 var(--book-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 {
|
&:hover {
|
||||||
&:before {
|
transform: scale(bookshelf.$book-hover-scale);
|
||||||
content: " ";
|
}
|
||||||
display: block;
|
|
||||||
background: var(--book-band-color);
|
|
||||||
height: 100%;
|
|
||||||
width: calc(var(--book-width));
|
|
||||||
border-radius: 4px;
|
|
||||||
|
|
||||||
position: absolute;
|
&.tilted {
|
||||||
top: 0px;
|
@include bookshelf.tilt;
|
||||||
left: -2px;
|
|
||||||
|
&:hover {
|
||||||
|
transform: scale(bookshelf.$book-hover-scale);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.dual-top-bands,
|
// Flat book orientation for Book Stack
|
||||||
&.split-bands {
|
&.flat {
|
||||||
&:after,
|
height: var(--book-width);
|
||||||
&:before {
|
width: var(--book-height);
|
||||||
content: "";
|
|
||||||
display: block;
|
|
||||||
background: var(--book-band-color);
|
|
||||||
width: calc(100% + 4px);
|
|
||||||
|
|
||||||
position: absolute;
|
.book-inner {
|
||||||
left: -2px;
|
height: var(--book-width);
|
||||||
|
width: var(--book-height);
|
||||||
|
transform: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:before {
|
.book-band {
|
||||||
z-index: 2;
|
transform: none;
|
||||||
}
|
top: 0 !important;
|
||||||
|
|
||||||
:global(.bookshelf__book-content),
|
&.top {
|
||||||
:global(.fit-text) {
|
left: $band-top-offset;
|
||||||
height: calc(var(--book-width));
|
|
||||||
|
&: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 {
|
// Front book orientation (cover view)
|
||||||
&:after {
|
&.front {
|
||||||
height: 10px;
|
$book-width: calc(var(--book-height) * 3 / 4);
|
||||||
top: 8px;
|
width: $book-width;
|
||||||
}
|
|
||||||
|
|
||||||
&:before {
|
.book-inner {
|
||||||
height: 15px;
|
$padding-left: bookshelf.$book-crease-offset +
|
||||||
top: 26px;
|
bookshelf.$book-crease-width + bookshelf.$book-padding;
|
||||||
}
|
$padding-right: bookshelf.$book-padding;
|
||||||
|
$padding-total: $padding-left + $padding-right;
|
||||||
|
|
||||||
:global(.bookshelf__book-content),
|
--book-content-width: calc(#{$book-width} - #{$padding-total});
|
||||||
:global(.fit-text) {
|
transform: none;
|
||||||
width: calc(200px - 61px) !important;
|
height: var(--book-height);
|
||||||
}
|
width: $book-width;
|
||||||
|
padding-left: $padding-left;
|
||||||
|
|
||||||
:global(.bookshelf__book-content) {
|
.book-title {
|
||||||
margin: 40px var(--book-width);
|
height: calc(var(--book-height) / 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-subtitle {
|
||||||
|
height: calc(var(--book-height) / 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-authors {
|
||||||
|
height: calc(var(--book-height) / 2);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.split-bands {
|
// Book band for styles
|
||||||
&:after,
|
.book-band {
|
||||||
&:before {
|
position: absolute;
|
||||||
height: 20px;
|
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 {
|
&.top2 {
|
||||||
top: 10px;
|
width: $small-band-width;
|
||||||
|
top: $band-top2-offset;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:before {
|
&.bottom {
|
||||||
bottom: 10px;
|
top: calc(
|
||||||
|
var(--book-height) - #{bookshelf.$book-band-offset} - #{bookshelf.$book-band-width}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.bookshelf__book-content),
|
// Dual Top Band Offset
|
||||||
:global(.fit-text) {
|
& + .book-band + .book-inner {
|
||||||
width: calc(200px - 62px) !important;
|
$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) {
|
// Split Band Offset
|
||||||
margin: 30px var(--book-width);
|
&.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>
|
</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">
|
<script lang="ts">
|
||||||
import type { Snippet } from "svelte";
|
import type { Snippet } from "svelte";
|
||||||
|
import BookshelfItem from "./BookshelfItem.svelte";
|
||||||
const MAX_CHILDREN = 5;
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
children?: Snippet;
|
children?: Snippet;
|
||||||
totalChildren?: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let { children, totalChildren = 0 }: Props = $props();
|
let { children }: Props = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ul
|
<BookshelfItem>
|
||||||
class="bookshelf__bookStack-wrapper"
|
<div class="book-stack">
|
||||||
style:margin={`calc(20px + ${(MAX_CHILDREN - totalChildren) * 40}px) 1px 10px`}
|
{@render children?.()}
|
||||||
>
|
</div>
|
||||||
{@render children?.()}
|
</BookshelfItem>
|
||||||
</ul>
|
|
||||||
|
<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">
|
<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;
|
children?: Snippet;
|
||||||
};
|
}
|
||||||
|
|
||||||
let { children }: Props = $props();
|
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>
|
</script>
|
||||||
|
|
||||||
<div class="bookshelf__wrapper" style:--book-shelf-color={color}>
|
<div class="bookshelf" use:fitAllText>
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="scss">
|
<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(
|
background-image: linear-gradient(
|
||||||
color-mix(in srgb, var(--book-shelf-color), black 32%),
|
color.scale(bookshelf.$color, $lightness: -32%),
|
||||||
color-mix(in srgb, var(--book-shelf-color), black 30%) 220px,
|
color.scale(bookshelf.$color, $lightness: -30%) bookshelf.$height,
|
||||||
color-mix(in srgb, var(--book-shelf-color), white 4%) 220px,
|
|
||||||
color-mix(in srgb, var(--book-shelf-color), white 4%) 222px,
|
color.scale(bookshelf.$color, $lightness: bookshelf.$shadow-pct)
|
||||||
var(--book-shelf-color) 222px,
|
bookshelf.$height,
|
||||||
var(--book-shelf-color) 228px,
|
color.scale(bookshelf.$color, $lightness: bookshelf.$shadow-pct)
|
||||||
color-mix(in srgb, var(--book-shelf-color), black 4%) 228px,
|
calc(bookshelf.$height + bookshelf.$shadow-width),
|
||||||
color-mix(in srgb, var(--book-shelf-color), black 4%) 230px
|
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>
|
</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 { chart } from "./chart";
|
||||||
export { clickOutside } from "./clickOutside";
|
export { clickOutside } from "./clickOutside";
|
||||||
export { fitText } from "./fitText";
|
|
||||||
|
|
Loading…
Reference in New Issue