From 349fcd903e747f63c96da25b1f659a8e7ef86ede Mon Sep 17 00:00:00 2001 From: Evan Fiordeliso Date: Sun, 6 Jul 2025 23:22:57 -0400 Subject: [PATCH] Rewrite bookshelf view from scratch --- esbuild.config.mjs | 1 + package.json | 4 + pnpm-lock.yaml | 37 ++ src/ui/code-blocks/ShelfCodeBlockView.svelte | 72 +-- src/ui/components/bookshelf/Book.svelte | 416 +++++++++++------- .../components/bookshelf/BookOnDisplay.svelte | 65 --- src/ui/components/bookshelf/BookStack.svelte | 32 +- .../bookshelf/BookStackElement.svelte | 142 ------ src/ui/components/bookshelf/BookText.svelte | 61 --- src/ui/components/bookshelf/BookTilted.svelte | 51 --- src/ui/components/bookshelf/Bookshelf.svelte | 88 +++- .../components/bookshelf/BookshelfItem.svelte | 30 ++ src/ui/components/bookshelf/bookshelf.scss | 217 --------- src/ui/components/bookshelf/variables.scss | 47 ++ src/ui/directives/fitText.ts | 16 - src/ui/directives/index.ts | 1 - 16 files changed, 522 insertions(+), 758 deletions(-) delete mode 100644 src/ui/components/bookshelf/BookOnDisplay.svelte delete mode 100644 src/ui/components/bookshelf/BookStackElement.svelte delete mode 100644 src/ui/components/bookshelf/BookText.svelte delete mode 100644 src/ui/components/bookshelf/BookTilted.svelte create mode 100644 src/ui/components/bookshelf/BookshelfItem.svelte delete mode 100644 src/ui/components/bookshelf/bookshelf.scss create mode 100644 src/ui/components/bookshelf/variables.scss delete mode 100644 src/ui/directives/fitText.ts diff --git a/esbuild.config.mjs b/esbuild.config.mjs index 71b44e3..b00986a 100644 --- a/esbuild.config.mjs +++ b/esbuild.config.mjs @@ -111,6 +111,7 @@ const context = await esbuild.context({ "@lezer/lr", ...builtins, ], + conditions: ["svelte"], format: "cjs", target: "es2018", logLevel: "info", diff --git a/package.json b/package.json index 4ef4421..96809c2 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 65fc1a6..be5758f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 diff --git a/src/ui/code-blocks/ShelfCodeBlockView.svelte b/src/ui/code-blocks/ShelfCodeBlockView.svelte index 1689ed7..3651b95 100644 --- a/src/ui/code-blocks/ShelfCodeBlockView.svelte +++ b/src/ui/code-blocks/ShelfCodeBlockView.svelte @@ -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 @@ {#each books as book (book.id)} {#if "books" in book} - + {#each book.books as bookData (bookData.id)} - plugin.app.workspace .getLeaf("tab") @@ -174,7 +182,7 @@ - diff --git a/src/ui/components/bookshelf/BookOnDisplay.svelte b/src/ui/components/bookshelf/BookOnDisplay.svelte deleted file mode 100644 index 20ed66f..0000000 --- a/src/ui/components/bookshelf/BookOnDisplay.svelte +++ /dev/null @@ -1,65 +0,0 @@ - - -
ev.key === "Enter" && onClick?.()} - role="link" - tabindex="0" -> -
- {@render children?.()} -
- - diff --git a/src/ui/components/bookshelf/BookStack.svelte b/src/ui/components/bookshelf/BookStack.svelte index 8d21913..8ef82b6 100644 --- a/src/ui/components/bookshelf/BookStack.svelte +++ b/src/ui/components/bookshelf/BookStack.svelte @@ -1,19 +1,31 @@ -
    - {@render children?.()} -
+ +
+ {@render children?.()} +
+
+ + diff --git a/src/ui/components/bookshelf/BookStackElement.svelte b/src/ui/components/bookshelf/BookStackElement.svelte deleted file mode 100644 index f2cdb0c..0000000 --- a/src/ui/components/bookshelf/BookStackElement.svelte +++ /dev/null @@ -1,142 +0,0 @@ - - -
  • -
    ev.key === "Enter" && onClick?.()} - role="link" - tabindex="0" - > - -
    -
  • - - diff --git a/src/ui/components/bookshelf/BookText.svelte b/src/ui/components/bookshelf/BookText.svelte deleted file mode 100644 index 80fbc2e..0000000 --- a/src/ui/components/bookshelf/BookText.svelte +++ /dev/null @@ -1,61 +0,0 @@ - - -
    - {#if title} -

    - {title} -

    - {/if} - {#if subtitle} -

    - {subtitle} -

    - {/if} -
    - - diff --git a/src/ui/components/bookshelf/BookTilted.svelte b/src/ui/components/bookshelf/BookTilted.svelte deleted file mode 100644 index b430ebe..0000000 --- a/src/ui/components/bookshelf/BookTilted.svelte +++ /dev/null @@ -1,51 +0,0 @@ - - -
    - {@render children?.()} -
    - - diff --git a/src/ui/components/bookshelf/Bookshelf.svelte b/src/ui/components/bookshelf/Bookshelf.svelte index 7438adb..fbd980d 100644 --- a/src/ui/components/bookshelf/Bookshelf.svelte +++ b/src/ui/components/bookshelf/Bookshelf.svelte @@ -1,32 +1,88 @@ -
    +
    {@render children?.()}
    diff --git a/src/ui/components/bookshelf/BookshelfItem.svelte b/src/ui/components/bookshelf/BookshelfItem.svelte new file mode 100644 index 0000000..b919bdd --- /dev/null +++ b/src/ui/components/bookshelf/BookshelfItem.svelte @@ -0,0 +1,30 @@ + + +
    + {@render children?.()} +
    + + diff --git a/src/ui/components/bookshelf/bookshelf.scss b/src/ui/components/bookshelf/bookshelf.scss deleted file mode 100644 index 11f2f74..0000000 --- a/src/ui/components/bookshelf/bookshelf.scss +++ /dev/null @@ -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; - } - } -} diff --git a/src/ui/components/bookshelf/variables.scss b/src/ui/components/bookshelf/variables.scss new file mode 100644 index 0000000..d31f1bc --- /dev/null +++ b/src/ui/components/bookshelf/variables.scss @@ -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; + } +} diff --git a/src/ui/directives/fitText.ts b/src/ui/directives/fitText.ts deleted file mode 100644 index bbcb980..0000000 --- a/src/ui/directives/fitText.ts +++ /dev/null @@ -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 { - doTextFit(el, opts); - - return { - update(opts) { - doTextFit(el, opts); - }, - destroy() {}, - }; -} diff --git a/src/ui/directives/index.ts b/src/ui/directives/index.ts index e50e551..f3814e5 100644 --- a/src/ui/directives/index.ts +++ b/src/ui/directives/index.ts @@ -1,3 +1,2 @@ export { chart } from "./chart"; export { clickOutside } from "./clickOutside"; -export { fitText } from "./fitText";