Add bookshelf view

This commit is contained in:
Evan Fiordeliso 2025-07-04 22:27:06 -04:00
parent 31cfa881c7
commit 5c73b8871c
23 changed files with 1337 additions and 0 deletions

View File

@ -2,6 +2,7 @@ import esbuild from "esbuild";
import process from "process"; import process from "process";
import builtins from "builtin-modules"; import builtins from "builtin-modules";
import esbuildSvelte from "esbuild-svelte"; import esbuildSvelte from "esbuild-svelte";
import { sassPlugin } from "esbuild-sass-plugin";
import { sveltePreprocess } from "svelte-preprocess"; import { sveltePreprocess } from "svelte-preprocess";
import dotenv from "dotenv"; import dotenv from "dotenv";
import dotenvExpand from "dotenv-expand"; import dotenvExpand from "dotenv-expand";
@ -60,6 +61,7 @@ const context = await esbuild.context({
!warning.filename?.includes("node_modules"), !warning.filename?.includes("node_modules"),
}, },
}), }),
sassPlugin(),
{ {
name: "copy-plugin", name: "copy-plugin",
setup(build) { setup(build) {

View File

@ -45,6 +45,7 @@
"dependencies": { "dependencies": {
"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",
"uuid": "^11.1.0", "uuid": "^11.1.0",
"yaml": "^2.8.0", "yaml": "^2.8.0",
"zod": "^3.25.67" "zod": "^3.25.67"

View File

@ -14,6 +14,9 @@ importers:
chroma-js: chroma-js:
specifier: ^3.1.2 specifier: ^3.1.2
version: 3.1.2 version: 3.1.2
esbuild-sass-plugin:
specifier: ^3.3.1
version: 3.3.1(esbuild@0.17.3)(sass-embedded@1.89.2)
uuid: uuid:
specifier: ^11.1.0 specifier: ^11.1.0
version: 11.1.0 version: 11.1.0
@ -112,6 +115,9 @@ packages:
resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
engines: {node: '>=6.0.0'} engines: {node: '>=6.0.0'}
'@bufbuild/protobuf@2.6.0':
resolution: {integrity: sha512-6cuonJVNOIL7lTj5zgo/Rc2bKAo4/GvN+rKCrUj7GdEHRzCk8zKOfFwUsL9nAVk5rSIsRmlgcpLzTRysopEeeg==}
'@codemirror/state@6.5.2': '@codemirror/state@6.5.2':
resolution: {integrity: sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==} resolution: {integrity: sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==}
@ -576,6 +582,9 @@ packages:
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
engines: {node: '>=8'} engines: {node: '>=8'}
buffer-builder@0.2.0:
resolution: {integrity: sha512-7VPMEPuYznPSoR21NE1zvd2Xna6c/CloiZCfcMXR1Jny6PjX0N4Nsa38zcBFo/FMK+BlA+FLKbJCQ0i2yxp+Xg==}
builtin-modules@3.3.0: builtin-modules@3.3.0:
resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==}
engines: {node: '>=6'} engines: {node: '>=6'}
@ -632,6 +641,9 @@ packages:
color-name@1.1.4: color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
colorjs.io@0.5.2:
resolution: {integrity: sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==}
concat-map@0.0.1: concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
@ -730,6 +742,12 @@ packages:
resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
esbuild-sass-plugin@3.3.1:
resolution: {integrity: sha512-SnO1ls+d52n6j8gRRpjexXI8MsHEaumS0IdDHaYM29Y6gakzZYMls6i9ql9+AWMSQk/eryndmUpXEgT34QrX1A==}
peerDependencies:
esbuild: '>=0.20.1'
sass-embedded: ^1.71.1
esbuild-svelte@0.9.3: esbuild-svelte@0.9.3:
resolution: {integrity: sha512-CgEcGY1r/d16+aggec3czoFBEBaYIrFOnMxpsO6fWNaNEqHregPN5DLAPZDqrL7rXDNplW+WMu8s3GMq9FqgJA==} resolution: {integrity: sha512-CgEcGY1r/d16+aggec3czoFBEBaYIrFOnMxpsO6fWNaNEqHregPN5DLAPZDqrL7rXDNplW+WMu8s3GMq9FqgJA==}
engines: {node: '>=18'} engines: {node: '>=18'}
@ -1339,6 +1357,9 @@ packages:
peerDependencies: peerDependencies:
svelte: ^5.7.0 svelte: ^5.7.0
rxjs@7.8.2:
resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==}
sade@1.8.1: sade@1.8.1:
resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==}
engines: {node: '>=6'} engines: {node: '>=6'}
@ -1347,6 +1368,9 @@ packages:
resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==}
engines: {node: '>=0.4'} engines: {node: '>=0.4'}
safe-identifier@0.4.2:
resolution: {integrity: sha512-6pNbSMW6OhAi9j+N8V+U715yBQsaWJ7eyEUaOrawX+isg5ZxhUlV1NipNtgaKHmFGiABwt+ZF04Ii+3Xjkg+8w==}
safe-push-apply@1.0.0: safe-push-apply@1.0.0:
resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@ -1355,6 +1379,107 @@ packages:
resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
sass-embedded-android-arm64@1.89.2:
resolution: {integrity: sha512-+pq7a7AUpItNyPu61sRlP6G2A8pSPpyazASb+8AK2pVlFayCSPAEgpwpCE9A2/Xj86xJZeMizzKUHxM2CBCUxA==}
engines: {node: '>=14.0.0'}
cpu: [arm64]
os: [android]
sass-embedded-android-arm@1.89.2:
resolution: {integrity: sha512-oHAPTboBHRZlDBhyRB6dvDKh4KvFs+DZibDHXbkSI6dBZxMTT+Yb2ivocHnctVGucKTLQeT7+OM5DjWHyynL/A==}
engines: {node: '>=14.0.0'}
cpu: [arm]
os: [android]
sass-embedded-android-riscv64@1.89.2:
resolution: {integrity: sha512-HfJJWp/S6XSYvlGAqNdakeEMPOdhBkj2s2lN6SHnON54rahKem+z9pUbCriUJfM65Z90lakdGuOfidY61R9TYg==}
engines: {node: '>=14.0.0'}
cpu: [riscv64]
os: [android]
sass-embedded-android-x64@1.89.2:
resolution: {integrity: sha512-BGPzq53VH5z5HN8de6jfMqJjnRe1E6sfnCWFd4pK+CAiuM7iw5Fx6BQZu3ikfI1l2GY0y6pRXzsVLdp/j4EKEA==}
engines: {node: '>=14.0.0'}
cpu: [x64]
os: [android]
sass-embedded-darwin-arm64@1.89.2:
resolution: {integrity: sha512-UCm3RL/tzMpG7DsubARsvGUNXC5pgfQvP+RRFJo9XPIi6elopY5B6H4m9dRYDpHA+scjVthdiDwkPYr9+S/KGw==}
engines: {node: '>=14.0.0'}
cpu: [arm64]
os: [darwin]
sass-embedded-darwin-x64@1.89.2:
resolution: {integrity: sha512-D9WxtDY5VYtMApXRuhQK9VkPHB8R79NIIR6xxVlN2MIdEid/TZWi1MHNweieETXhWGrKhRKglwnHxxyKdJYMnA==}
engines: {node: '>=14.0.0'}
cpu: [x64]
os: [darwin]
sass-embedded-linux-arm64@1.89.2:
resolution: {integrity: sha512-2N4WW5LLsbtrWUJ7iTpjvhajGIbmDR18ZzYRywHdMLpfdPApuHPMDF5CYzHbS+LLx2UAx7CFKBnj5LLjY6eFgQ==}
engines: {node: '>=14.0.0'}
cpu: [arm64]
os: [linux]
sass-embedded-linux-arm@1.89.2:
resolution: {integrity: sha512-leP0t5U4r95dc90o8TCWfxNXwMAsQhpWxTkdtySDpngoqtTy3miMd7EYNYd1znI0FN1CBaUvbdCMbnbPwygDlA==}
engines: {node: '>=14.0.0'}
cpu: [arm]
os: [linux]
sass-embedded-linux-musl-arm64@1.89.2:
resolution: {integrity: sha512-nTyuaBX6U1A/cG7WJh0pKD1gY8hbg1m2SnzsyoFG+exQ0lBX/lwTLHq3nyhF+0atv7YYhYKbmfz+sjPP8CZ9lw==}
engines: {node: '>=14.0.0'}
cpu: [arm64]
os: [linux]
sass-embedded-linux-musl-arm@1.89.2:
resolution: {integrity: sha512-Z6gG2FiVEEdxYHRi2sS5VIYBmp17351bWtOCUZ/thBM66+e70yiN6Eyqjz80DjL8haRUegNQgy9ZJqsLAAmr9g==}
engines: {node: '>=14.0.0'}
cpu: [arm]
os: [linux]
sass-embedded-linux-musl-riscv64@1.89.2:
resolution: {integrity: sha512-N6oul+qALO0SwGY8JW7H/Vs0oZIMrRMBM4GqX3AjM/6y8JsJRxkAwnfd0fDyK+aICMFarDqQonQNIx99gdTZqw==}
engines: {node: '>=14.0.0'}
cpu: [riscv64]
os: [linux]
sass-embedded-linux-musl-x64@1.89.2:
resolution: {integrity: sha512-K+FmWcdj/uyP8GiG9foxOCPfb5OAZG0uSVq80DKgVSC0U44AdGjvAvVZkrgFEcZ6cCqlNC2JfYmslB5iqdL7tg==}
engines: {node: '>=14.0.0'}
cpu: [x64]
os: [linux]
sass-embedded-linux-riscv64@1.89.2:
resolution: {integrity: sha512-g9nTbnD/3yhOaskeqeBQETbtfDQWRgsjHok6bn7DdAuwBsyrR3JlSFyqKc46pn9Xxd9SQQZU8AzM4IR+sY0A0w==}
engines: {node: '>=14.0.0'}
cpu: [riscv64]
os: [linux]
sass-embedded-linux-x64@1.89.2:
resolution: {integrity: sha512-Ax7dKvzncyQzIl4r7012KCMBvJzOz4uwSNoyoM5IV6y5I1f5hEwI25+U4WfuTqdkv42taCMgpjZbh9ERr6JVMQ==}
engines: {node: '>=14.0.0'}
cpu: [x64]
os: [linux]
sass-embedded-win32-arm64@1.89.2:
resolution: {integrity: sha512-j96iJni50ZUsfD6tRxDQE2QSYQ2WrfHxeiyAXf41Kw0V4w5KYR/Sf6rCZQLMTUOHnD16qTMVpQi20LQSqf4WGg==}
engines: {node: '>=14.0.0'}
cpu: [arm64]
os: [win32]
sass-embedded-win32-x64@1.89.2:
resolution: {integrity: sha512-cS2j5ljdkQsb4PaORiClaVYynE9OAPZG/XjbOMxpQmjRIf7UroY4PEIH+Waf+y47PfXFX9SyxhYuw2NIKGbEng==}
engines: {node: '>=14.0.0'}
cpu: [x64]
os: [win32]
sass-embedded@1.89.2:
resolution: {integrity: sha512-Ack2K8rc57kCFcYlf3HXpZEJFNUX8xd8DILldksREmYXQkRHI879yy8q4mRDJgrojkySMZqmmmW1NxrFxMsYaA==}
engines: {node: '>=16.0.0'}
hasBin: true
sass@1.89.2: sass@1.89.2:
resolution: {integrity: sha512-xCmtksBKd/jdJ9Bt9p7nPKiuqrlBMBuuGkQlkhZjjQk3Ty48lv93k5Dq6OPkKt4XwxDJ7tvlfrTa1MPA9bf+QA==} resolution: {integrity: sha512-xCmtksBKd/jdJ9Bt9p7nPKiuqrlBMBuuGkQlkhZjjQk3Ty48lv93k5Dq6OPkKt4XwxDJ7tvlfrTa1MPA9bf+QA==}
engines: {node: '>=14.0.0'} engines: {node: '>=14.0.0'}
@ -1480,6 +1605,10 @@ packages:
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
engines: {node: '>=8'} engines: {node: '>=8'}
supports-color@8.1.1:
resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==}
engines: {node: '>=10'}
supports-preserve-symlinks-flag@1.0.0: supports-preserve-symlinks-flag@1.0.0:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@ -1539,6 +1668,14 @@ packages:
resolution: {integrity: sha512-TF+8irl7rpj3+fpaLuPRX5BqReTAqckp0Fumxa/mCeK3fo0/MnBb9W/Z2bLwtqj3C3r5Lm6NKIAw7YrgIv1Fwg==} resolution: {integrity: sha512-TF+8irl7rpj3+fpaLuPRX5BqReTAqckp0Fumxa/mCeK3fo0/MnBb9W/Z2bLwtqj3C3r5Lm6NKIAw7YrgIv1Fwg==}
engines: {node: '>=18'} engines: {node: '>=18'}
sync-child-process@1.0.2:
resolution: {integrity: sha512-8lD+t2KrrScJ/7KXCSyfhT3/hRq78rC0wBFqNJXv3mZyn6hW2ypM05JmlSvtqRbeq6jqA94oHbxAr2vYsJ8vDA==}
engines: {node: '>=16.0.0'}
sync-message-port@1.1.3:
resolution: {integrity: sha512-GTt8rSKje5FilG+wEdfCkOcLL7LWqpMlr2c3LRuKt/YXxcJ52aGSbGBAdI4L3aaqfrBt6y711El53ItyH1NWzg==}
engines: {node: '>=16.0.0'}
to-regex-range@5.0.1: to-regex-range@5.0.1:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'} engines: {node: '>=8.0'}
@ -1602,6 +1739,9 @@ packages:
validate-npm-package-license@3.0.4: validate-npm-package-license@3.0.4:
resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==}
varint@6.0.0:
resolution: {integrity: sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==}
w3c-keyname@2.2.8: w3c-keyname@2.2.8:
resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==}
@ -1659,6 +1799,8 @@ snapshots:
'@jridgewell/gen-mapping': 0.3.8 '@jridgewell/gen-mapping': 0.3.8
'@jridgewell/trace-mapping': 0.3.25 '@jridgewell/trace-mapping': 0.3.25
'@bufbuild/protobuf@2.6.0': {}
'@codemirror/state@6.5.2': '@codemirror/state@6.5.2':
dependencies: dependencies:
'@marijn/find-cluster-break': 1.0.2 '@marijn/find-cluster-break': 1.0.2
@ -2056,6 +2198,8 @@ snapshots:
dependencies: dependencies:
fill-range: 7.1.1 fill-range: 7.1.1
buffer-builder@0.2.0: {}
builtin-modules@3.3.0: {} builtin-modules@3.3.0: {}
call-bind-apply-helpers@1.0.2: call-bind-apply-helpers@1.0.2:
@ -2112,6 +2256,8 @@ snapshots:
color-name@1.1.4: {} color-name@1.1.4: {}
colorjs.io@0.5.2: {}
concat-map@0.0.1: {} concat-map@0.0.1: {}
crelt@1.0.6: {} crelt@1.0.6: {}
@ -2269,6 +2415,14 @@ snapshots:
is-date-object: 1.1.0 is-date-object: 1.1.0
is-symbol: 1.1.1 is-symbol: 1.1.1
esbuild-sass-plugin@3.3.1(esbuild@0.17.3)(sass-embedded@1.89.2):
dependencies:
esbuild: 0.17.3
resolve: 1.22.10
safe-identifier: 0.4.2
sass: 1.89.2
sass-embedded: 1.89.2
esbuild-svelte@0.9.3(esbuild@0.17.3)(svelte@5.34.8): esbuild-svelte@0.9.3(esbuild@0.17.3)(svelte@5.34.8):
dependencies: dependencies:
'@jridgewell/trace-mapping': 0.3.25 '@jridgewell/trace-mapping': 0.3.25
@ -2916,6 +3070,10 @@ snapshots:
esm-env: 1.2.2 esm-env: 1.2.2
svelte: 5.34.8 svelte: 5.34.8
rxjs@7.8.2:
dependencies:
tslib: 2.4.0
sade@1.8.1: sade@1.8.1:
dependencies: dependencies:
mri: 1.2.0 mri: 1.2.0
@ -2928,6 +3086,8 @@ snapshots:
has-symbols: 1.1.0 has-symbols: 1.1.0
isarray: 2.0.5 isarray: 2.0.5
safe-identifier@0.4.2: {}
safe-push-apply@1.0.0: safe-push-apply@1.0.0:
dependencies: dependencies:
es-errors: 1.3.0 es-errors: 1.3.0
@ -2939,6 +3099,82 @@ snapshots:
es-errors: 1.3.0 es-errors: 1.3.0
is-regex: 1.2.1 is-regex: 1.2.1
sass-embedded-android-arm64@1.89.2:
optional: true
sass-embedded-android-arm@1.89.2:
optional: true
sass-embedded-android-riscv64@1.89.2:
optional: true
sass-embedded-android-x64@1.89.2:
optional: true
sass-embedded-darwin-arm64@1.89.2:
optional: true
sass-embedded-darwin-x64@1.89.2:
optional: true
sass-embedded-linux-arm64@1.89.2:
optional: true
sass-embedded-linux-arm@1.89.2:
optional: true
sass-embedded-linux-musl-arm64@1.89.2:
optional: true
sass-embedded-linux-musl-arm@1.89.2:
optional: true
sass-embedded-linux-musl-riscv64@1.89.2:
optional: true
sass-embedded-linux-musl-x64@1.89.2:
optional: true
sass-embedded-linux-riscv64@1.89.2:
optional: true
sass-embedded-linux-x64@1.89.2:
optional: true
sass-embedded-win32-arm64@1.89.2:
optional: true
sass-embedded-win32-x64@1.89.2:
optional: true
sass-embedded@1.89.2:
dependencies:
'@bufbuild/protobuf': 2.6.0
buffer-builder: 0.2.0
colorjs.io: 0.5.2
immutable: 5.1.3
rxjs: 7.8.2
supports-color: 8.1.1
sync-child-process: 1.0.2
varint: 6.0.0
optionalDependencies:
sass-embedded-android-arm: 1.89.2
sass-embedded-android-arm64: 1.89.2
sass-embedded-android-riscv64: 1.89.2
sass-embedded-android-x64: 1.89.2
sass-embedded-darwin-arm64: 1.89.2
sass-embedded-darwin-x64: 1.89.2
sass-embedded-linux-arm: 1.89.2
sass-embedded-linux-arm64: 1.89.2
sass-embedded-linux-musl-arm: 1.89.2
sass-embedded-linux-musl-arm64: 1.89.2
sass-embedded-linux-musl-riscv64: 1.89.2
sass-embedded-linux-musl-x64: 1.89.2
sass-embedded-linux-riscv64: 1.89.2
sass-embedded-linux-x64: 1.89.2
sass-embedded-win32-arm64: 1.89.2
sass-embedded-win32-x64: 1.89.2
sass@1.89.2: sass@1.89.2:
dependencies: dependencies:
chokidar: 4.0.3 chokidar: 4.0.3
@ -3084,6 +3320,10 @@ snapshots:
dependencies: dependencies:
has-flag: 4.0.0 has-flag: 4.0.0
supports-color@8.1.1:
dependencies:
has-flag: 4.0.0
supports-preserve-symlinks-flag@1.0.0: {} supports-preserve-symlinks-flag@1.0.0: {}
svelte-check@4.2.2(picomatch@4.0.2)(svelte@5.34.8)(typescript@5.0.4): svelte-check@4.2.2(picomatch@4.0.2)(svelte@5.34.8)(typescript@5.0.4):
@ -3128,6 +3368,12 @@ snapshots:
magic-string: 0.30.17 magic-string: 0.30.17
zimmerframe: 1.1.2 zimmerframe: 1.1.2
sync-child-process@1.0.2:
dependencies:
sync-message-port: 1.1.3
sync-message-port@1.1.3: {}
to-regex-range@5.0.1: to-regex-range@5.0.1:
dependencies: dependencies:
is-number: 7.0.0 is-number: 7.0.0
@ -3203,6 +3449,8 @@ snapshots:
spdx-correct: 3.2.0 spdx-correct: 3.2.0
spdx-expression-parse: 3.0.1 spdx-expression-parse: 3.0.1
varint@6.0.0: {}
w3c-keyname@2.2.8: {} w3c-keyname@2.2.8: {}
which-boxed-primitive@1.1.1: which-boxed-primitive@1.1.1:

View File

@ -22,6 +22,7 @@ import { BackupReadingLogCommand } from "@commands/CreateReadingLogBackupCommand
import { RestoreReadingLogBackupCommand } from "@commands/RestoreReadingLogBackupCommand"; import { RestoreReadingLogBackupCommand } from "@commands/RestoreReadingLogBackupCommand";
import { Goodreads } from "@data-sources/Goodreads"; import { Goodreads } from "@data-sources/Goodreads";
import { CreateBookFromGoodreadsUrlCommand } from "@commands/CreateBookFromGoodreadsUrlCommand"; import { CreateBookFromGoodreadsUrlCommand } from "@commands/CreateBookFromGoodreadsUrlCommand";
import { registerBookshelfCodeBlockProcessor } from "@ui/code-blocks/BookshelfCodeBlock";
export default class BookTrackerPlugin extends Plugin { export default class BookTrackerPlugin extends Plugin {
public settings: BookTrackerPluginSettings; public settings: BookTrackerPluginSettings;
@ -85,6 +86,7 @@ export default class BookTrackerPlugin extends Plugin {
registerReadingLogCodeBlockProcessor(this); registerReadingLogCodeBlockProcessor(this);
registerReadingStatsCodeBlockProcessor(this); registerReadingStatsCodeBlockProcessor(this);
registerBookshelfCodeBlockProcessor(this);
} }
onunload() {} onunload() {}

View File

@ -0,0 +1,28 @@
import { registerCodeBlockRenderer } from ".";
import { SvelteCodeBlockRenderer } from "./SvelteCodeBlockRenderer";
import BookshelfCodeBlockView from "./BookshelfCodeBlockView.svelte";
import type BookTrackerPlugin from "@src/main";
export function registerBookshelfCodeBlockProcessor(
plugin: BookTrackerPlugin
): void {
registerCodeBlockRenderer(
plugin,
"bookshelf",
(source, el) => new BookshelfCodeBlockRenderer(plugin, source, el)
);
}
export class BookshelfCodeBlockRenderer extends SvelteCodeBlockRenderer<
typeof BookshelfCodeBlockView
> {
constructor(
plugin: BookTrackerPlugin,
source: string,
contentEl: HTMLElement
) {
super(contentEl, BookshelfCodeBlockView, { props: { plugin, source } });
}
onunload() {}
}

View File

@ -0,0 +1,131 @@
<script lang="ts">
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";
</script>
<Bookshelf>
<Book
title="Hello World"
color="purple"
width={1120}
design="colored-spine"
/>
<Book title="White Space" width={700} design="split-bands" color="pink" />
<Book
title="The Art of Computer Programming Vol 1"
width={1156}
design="dual-top-bands"
color="cyan"
/>
<Book
title="Cascading Style Sheets"
subtitle="Guide to Design"
width={560}
orientation="tilted"
/>
<Book
title="HTML5"
subtitle="Welcome to the Web"
width={1350}
design="colored-spine"
/>
<BookStack totalChildren={2}>
<BookStackElement
title="Coding for Dummies"
subtitle="JS tutorial"
color="blue"
design="colored-spine"
/>
<BookStackElement
title="Coding for Dummies"
subtitle="C# tutorial"
color="pink"
design="dual-top-bands"
/>
</BookStack>
<Book
title="CoffeeScript"
subtitle="The JS Alternative"
author="The Dev Guy"
design="split-bands"
color="green"
orientation="on-display"
/>
<Book
title="Cheat Sheet"
subtitle="Guide to Design"
color="blue"
design="split-bands"
width={870}
/>
<Book
title="Psychology of Colors"
color="pink"
design="dual-top-bands"
width={540}
/>
<Book
title="TypeScript"
subtitle="Intro JS to type checking"
color="cyan"
design="colored-spine"
width={1130}
/>
<Book title="Testing" width={10} />
<Book
title="JavaScript"
subtitle="The Definitive Guide"
author="David Flanagan"
design="split-bands"
color="purple"
orientation="on-display"
/>
<Book title="Pragmatic Programmer" color="red" />
<Book title="White Space" color="yellow" design="split-bands" />
<Book
title="W3 Schools"
subtitle="The best around"
color="blue"
design="split-bands"
orientation="tilted"
/>
<Book
title="UI/UX"
subtitle="Guide to Mobile Development"
author="John Doe"
color="purple"
orientation="on-display"
design="dual-top-bands"
/>
<Book
title="Clean Code"
color="orange"
orientation="tilted"
design="dual-top-bands"
/>
<Book title="Docs for Devs" />
<BookStack totalChildren={4}>
<BookStackElement
title="The Art of Computer Programming Vol 1"
color="green"
design="dual-top-bands"
/>
<BookStackElement
title="The Art of Computer Programming Vol 2"
color="red"
design="dual-top-bands"
/>
<BookStackElement
title="The Art of Computer Programming Vol 3"
color="blue"
design="dual-top-bands"
/>
<BookStackElement
title="The Art of Computer Programming Vol 4a"
color="pink"
design="dual-top-bands"
/>
</BookStack>
</Bookshelf>

View File

@ -0,0 +1,109 @@
<script lang="ts">
import Self from "./Book.svelte";
import { Color, COLOR_NAMES, type ColorName } from "@utils/color";
import BookTiltedDefault from "./designs/tilted/BookTiltedDefault.svelte";
import BookOnDisplay from "./designs/book/BookOnDisplay.svelte";
import BookText from "./BookText.svelte";
import BookSplitBands from "./designs/book/BookSplitBands.svelte";
import BookDualTopBands from "./designs/book/BookDualTopBands.svelte";
import BookColoredSpine from "./designs/book/BookColoredSpine.svelte";
import BookDefault from "./designs/book/BookDefault.svelte";
const BOOK_SIZE_DEFAULT: number = 40;
const BOOK_SIZE_MIN: number = 15;
interface BookProps {
title?: string;
subtitle?: string;
author?: string;
color?: ColorName | string;
design?: "default" | "colored-spine" | "dual-top-bands" | "split-bands";
orientation?: "tilted" | "on-display";
height?: number;
width?: number;
}
let {
title,
subtitle,
author,
color: colorRaw = "green",
design = "default",
orientation,
height,
width,
}: BookProps = $props();
function widthCheck(input: number | undefined) {
if (input) {
if (input <= 150) {
return BOOK_SIZE_MIN;
}
return input / 10;
}
return BOOK_SIZE_DEFAULT;
}
const color = $derived(
colorRaw in COLOR_NAMES
? Color.fromName(colorRaw as ColorName)
: Color.fromCSSColor(colorRaw),
);
const verifiedWidth = $derived(widthCheck(width));
const textColor = $derived(color.contrastColor.hex);
</script>
{#if orientation}
{#if orientation === "tilted"}
<BookTiltedDefault {design} width={verifiedWidth}>
<Self
{title}
{subtitle}
{author}
color={colorRaw}
{design}
{height}
{width}
/>
</BookTiltedDefault>
{:else if orientation === "on-display"}
<BookOnDisplay color={color.hex}>
<div
class="book-display-crease"
style:--book-color={color.hex}
></div>
{#if title}
<BookText {title} {subtitle} />
<p class="bookshelf__book-author">By: {author}</p>
{/if}
</BookOnDisplay>
{/if}
{:else if design === "split-bands"}
<BookSplitBands color={color.hex} width={verifiedWidth} {textColor}>
<BookText {title} {subtitle} />
</BookSplitBands>
{:else if design === "dual-top-bands"}
<BookDualTopBands color={color.hex} width={verifiedWidth} {textColor}>
<BookText {title} {subtitle} />
</BookDualTopBands>
{:else if design === "colored-spine"}
<BookColoredSpine color={color.hex} width={verifiedWidth} {textColor}>
<BookText {title} {subtitle} />
</BookColoredSpine>
{:else}
<BookDefault color={color.hex} width={verifiedWidth} {textColor}>
<BookText {title} {subtitle} />
</BookDefault>
{/if}
<style>
.book-display-crease {
background-color: color-mix(in srgb, var(--book-color), black 14%);
width: 5px;
height: 100%;
position: absolute;
top: 0;
left: 8px;
}
</style>

View File

@ -0,0 +1,19 @@
<script lang="ts">
import type { Snippet } from "svelte";
const MAX_CHILDREN = 5;
interface Props {
children?: Snippet;
totalChildren?: number;
}
let { children, totalChildren = 0 }: Props = $props();
</script>
<ul
class="bookshelf__bookStack-wrapper"
style:margin={`calc(20px + ${(MAX_CHILDREN - totalChildren) * 40}px) 1px 10px`}
>
{@render children?.()}
</ul>

View File

@ -0,0 +1,49 @@
<script lang="ts">
import { Color, COLOR_NAMES, type ColorName } from "@utils/color";
import BookText from "./BookText.svelte";
import BookStackColoredSpine from "./designs/stack/BookStackColoredSpine.svelte";
import BookStackDefault from "./designs/stack/BookStackDefault.svelte";
import BookStackDualTopBands from "./designs/stack/BookStackDualTopBands.svelte";
import BookStackSplitBands from "./designs/stack/BookStackSplitBands.svelte";
interface Props {
title?: string;
subtitle?: string;
color?: ColorName | string;
design?: "default" | "split-bands" | "dual-top-bands" | "colored-spine";
}
let {
title,
subtitle,
color: colorRaw = "green",
design,
}: Props = $props();
const color = $derived(
colorRaw in COLOR_NAMES
? Color.fromName(colorRaw as ColorName)
: Color.fromCSSColor(colorRaw),
);
const textColor = $derived(color.contrastColor.hex);
</script>
<li class="bookshelf__bookstack-elem">
{#if design === "split-bands"}
<BookStackSplitBands color={color.hex} {textColor}>
<BookText {title} {subtitle} />
</BookStackSplitBands>
{:else if design === "dual-top-bands"}
<BookStackDualTopBands color={color.hex} {textColor}>
<BookText {title} {subtitle} />
</BookStackDualTopBands>
{:else if design === "colored-spine"}
<BookStackColoredSpine color={color.hex} {textColor}>
<BookText {title} {subtitle} />
</BookStackColoredSpine>
{:else}
<BookStackDefault color={color.hex} {textColor}>
<BookText {title} {subtitle} />
</BookStackDefault>
{/if}
</li>

View File

@ -0,0 +1,20 @@
<script lang="ts">
interface Props {
title?: string;
subtitle?: string;
}
let { title, subtitle }: Props = $props();
</script>
<div
class="bookshelf__book-content"
class:center-content={subtitle === undefined}
>
{#if title}
<h2 class="bookshelf__book-title">{title}</h2>
{/if}
{#if subtitle}
<h4 class="bookshelf__book-subtitle">{subtitle}</h4>
{/if}
</div>

View File

@ -0,0 +1,32 @@
<script lang="ts">
import "./bookshelf.scss";
import type { Snippet } from "svelte";
type Props = {
children?: Snippet;
};
let { children }: Props = $props();
const color = "#a47148";
</script>
<div class="bookshelf__wrapper" style:--book-shelf-color={color}>
{@render children?.()}
</div>
<style lang="scss">
div {
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
);
border: 10px var(--book-shelf-color) solid;
}
</style>

View File

@ -0,0 +1,216 @@
$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;
.bookshelf__bookStack-wrapper {
width: 200px;
height: 100%;
display: inline-flex;
flex-flow: column nowrap;
list-style: none;
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;
line-height: 1.2;
letter-spacing: 1.25px;
border-radius: 6px;
}
.bookshelf__bookstack-elem {
margin-inline-start: 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 {
width: 150px;
height: 200px;
display: flex;
flex-flow: column wrap;
justify-content: space-between;
align-items: center;
text-align: center;
margin: 20px 2px 10px 2px;
position: relative;
.bookshelf__book-content {
width: calc(100% - 11px);
margin-left: 11px;
transform: rotate(0deg);
overflow: visible;
.bookshelf__book-title,
.bookshelf__book-subtitle {
line-height: 2;
margin-top: 16px;
width: 100%;
word-wrap: break-word;
}
}
.bookshelf__book-author {
font-size: 0.8em;
margin-left: 13px;
}
}
}

View File

@ -0,0 +1,49 @@
<script lang="ts">
import type { Snippet } from "svelte";
interface Props {
children?: Snippet;
color?: string;
width?: number;
textColor?: string;
}
let { children, color = "green", width = 40, textColor }: Props = $props();
</script>
<div
class="bookshelf__book-wrapper"
style:--book-color={color}
style:--book-width={width + "px"}
style:width={width + "px"}
style:color={textColor}
>
{@render children?.()}
</div>
<style lang="scss">
div {
background: var(--book-color);
border-left: 2px solid color-mix(in srgb, var(--book-color), white 4%);
border-right: 2px solid color-mix(in srgb, var(--book-color), black 4%);
&:before {
content: " ";
display: block;
background: color-mix(in srgb, var(--book-color), black 14%);
height: 100%;
width: calc(var(--book-width));
border-radius: 4px;
position: absolute;
top: 0px;
left: -2px;
}
:global(.bookshelf__book-content) {
height: calc(var(--book-width));
width: calc(200px - 60px);
margin: 0 var(--book-width);
}
}
</style>

View File

@ -0,0 +1,36 @@
<script lang="ts">
import type { Snippet } from "svelte";
interface Props {
children?: Snippet;
color?: ColorName | string;
width?: number;
textColor?: string;
}
let { children, color = "green", width = 40, textColor }: Props = $props();
</script>
<div
class="bookshelf__book-wrapper"
style:--book-color={color}
style:--book-width={width + "px"}
style:width={width + "px"}
style:color={textColor}
>
{@render children?.()}
</div>
<style lang="scss">
div {
background: var(--book-color);
border-left: 2px solid color-mix(in srgb, var(--book-color), white 4%);
border-right: 2px solid color-mix(in srgb, var(--book-color), black 4%);
:global(.bookshelf__book-content) {
height: calc(var(--book-width));
width: calc(200px - 60px);
margin: 0 var(--book-width);
}
}
</style>

View File

@ -0,0 +1,61 @@
<script lang="ts">
import type { Snippet } from "svelte";
interface Props {
children?: Snippet;
color?: string;
width?: number;
textColor?: string;
}
let { children, color = "green", width = 40, textColor }: Props = $props();
</script>
<div
class="bookshelf__book-wrapper"
style:--book-color={color}
style:--book-width={width + "px"}
style:width={width + "px"}
style:color={textColor}
>
{@render children?.()}
</div>
<style lang="scss">
div {
background: var(--book-color);
border-left: 2px solid color-mix(in srgb, var(--book-color), white 4%);
border-right: 2px solid color-mix(in srgb, var(--book-color), black 4%);
&:after {
content: "";
display: block;
background: color-mix(in srgb, var(--book-color), black 14%);
height: 20px;
width: calc(100% + 4px);
position: absolute;
top: 10px;
left: -2px;
}
&:before {
content: "";
display: block;
background: color-mix(in srgb, var(--book-color), black 14%);
height: 20px;
width: calc(100% + 4px);
position: absolute;
bottom: 10px;
left: -2px;
z-index: 2;
}
:global(.bookshelf__book-content) {
height: calc(var(--book-width));
width: calc(200px - 60px) !important;
margin: 30px var(--book-width);
}
}
</style>

View File

@ -0,0 +1,23 @@
<script lang="ts">
import type { Snippet } from "svelte";
interface Props {
children?: Snippet;
color?: string;
}
let { children, color = "green" }: Props = $props();
</script>
<div
class="bookshelf__book-wrapper bookshelf__book-onDisplay"
style:--book-color={color}
>
{@render children?.()}
</div>
<style lang="scss">
div {
background: var(--book-color);
}
</style>

View File

@ -0,0 +1,61 @@
<script lang="ts">
import type { Snippet } from "svelte";
interface Props {
children?: Snippet;
color?: string;
width?: number;
textColor?: string;
}
let { children, color = "green", width = 40, textColor }: Props = $props();
</script>
<div
class="bookshelf__book-wrapper"
style:--book-color={color}
style:--book-width={width + "px"}
style:width={width + "px"}
style:color={textColor}
>
{@render children?.()}
</div>
<style lang="scss">
div {
background: var(--book-color);
border-left: 2px solid color-mix(in srgb, var(--book-color), white 4%);
border-right: 2px solid color-mix(in srgb, var(--book-color), black 4%);
&:after {
content: "";
display: block;
background: color-mix(in srgb, var(--book-color), black 14%);
height: 20px;
width: calc(100% + 4px);
position: absolute;
top: 10px;
left: -2px;
}
&:before {
content: "";
display: block;
background: color-mix(in srgb, var(--book-color), black 14%);
height: 20px;
width: calc(100% + 4px);
position: absolute;
bottom: 10px;
left: -2px;
z-index: 2;
}
:global(.bookshelf__book-content) {
height: calc(var(--book-width));
width: calc(200px - 60px) !important;
margin: 30px var(--book-width);
}
}
</style>

View File

@ -0,0 +1,40 @@
<script lang="ts">
import type { Snippet } from "svelte";
interface Props {
children?: Snippet;
color?: string;
textColor?: string;
}
let { children, color = "green", textColor }: Props = $props();
</script>
<div
class="bookshelf__book-wrapper"
style:--book-color={color}
style:color={textColor}
>
{@render children?.()}
</div>
<style lang="scss">
div {
background: var(--book-color);
border-left: 2px solid color-mix(in srgb, var(--book-color), white 4%);
border-right: 2px solid color-mix(in srgb, var(--book-color), black 4%);
&:before {
content: " ";
display: block;
background: color-mix(in srgb, var(--book-color), black 14%);
height: 40px;
width: calc(100% + 4px);
border-radius: 4px;
position: absolute;
top: 0px;
left: -2px;
}
}
</style>

View File

@ -0,0 +1,27 @@
<script lang="ts">
import type { Snippet } from "svelte";
interface Props {
children?: Snippet;
color?: string;
textColor?: string;
}
let { children, color = "green", textColor }: Props = $props();
</script>
<div
class="bookshelf__book-wrapper"
style:--book-color={color}
style:color={textColor}
>
{@render children?.()}
</div>
<style lang="scss">
div {
background: var(--book-color);
border-left: 2px solid color-mix(in srgb, var(--book-color), white 4%);
border-right: 2px solid color-mix(in srgb, var(--book-color), black 4%);
}
</style>

View File

@ -0,0 +1,57 @@
<script lang="ts">
import type { Snippet } from "svelte";
interface Props {
children?: Snippet;
color?: string;
textColor?: string;
}
let { children, color = "green", textColor }: Props = $props();
</script>
<div
class="bookshelf__book-wrapper"
style:--book-color={color}
style:color={textColor}
>
{@render children?.()}
</div>
<style lang="scss">
div {
background: var(--book-color);
border-left: 2px solid color-mix(in srgb, var(--book-color), white 4%);
border-right: 2px solid color-mix(in srgb, var(--book-color), black 4%);
&:after {
content: " ";
display: block;
background: color-mix(in srgb, var(--book-color), black 14%);
height: 40px;
width: 10px;
position: absolute;
top: 0px;
left: 6px;
}
&:before {
content: " ";
display: block;
background: color-mix(in srgb, var(--book-color), black 14%);
height: 40px;
width: 15px;
position: absolute;
top: 0px;
left: 24px;
z-index: 2;
}
:global(.bookshelf__book-content) {
width: calc(200px - 31px) !important;
margin: 0 31px;
}
}
</style>

View File

@ -0,0 +1,57 @@
<script lang="ts">
import type { Snippet } from "svelte";
interface Props {
children?: Snippet;
color?: string;
textColor?: string;
}
let { children, color = "green", textColor }: Props = $props();
</script>
<div
class="bookshelf__book-wrapper"
style:--book-color={color}
style:color={textColor}
>
{@render children?.()}
</div>
<style lang="scss">
div {
background: var(--book-color);
border-left: 2px solid color-mix(in srgb, var(--book-color), white 4%);
border-right: 2px solid color-mix(in srgb, var(--book-color), black 4%);
&:after {
content: " ";
display: block;
background: color-mix(in srgb, var(--book-color), black 14%);
height: 40px;
width: 20px;
position: absolute;
top: 0px;
left: 10px;
}
&:before {
content: " ";
display: block;
background: color-mix(in srgb, var(--book-color), black 14%);
height: 40px;
width: 20px;
position: absolute;
top: 0px;
right: 10px;
z-index: 2;
}
:global(.bookshelf__book-content) {
width: calc(200px - 60px) !important;
margin: 0 30px;
}
}
</style>

View File

@ -0,0 +1,47 @@
<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) {
height: calc(var(--book-tilted-width));
width: calc(200px - 60px);
margin: var(--book-tilted-top-margin) var(--book-tilted-width);
}
}
</style>

View File

@ -40,6 +40,16 @@ export class Color {
return `rgba(${this.r}, ${this.g}, ${this.b}, ${this.a})`; return `rgba(${this.r}, ${this.g}, ${this.b}, ${this.a})`;
} }
get contrastColor(): Color {
return this.r * 0.299 +
this.g * 0.587 +
this.b * 0.114 +
(1 - this.a) * 255 >
186
? new Color(0, 0, 0)
: new Color(255, 255, 255);
}
alpha(alpha: number): Color { alpha(alpha: number): Color {
return new Color(this.r, this.g, this.b, alpha); return new Color(this.r, this.g, this.b, alpha);
} }
@ -56,6 +66,18 @@ export class Color {
); );
} }
static fromCSSColor(color: string): Color {
const ctx = document.createElement("canvas").getContext("2d")!;
ctx.fillStyle = color;
const hexColor = ctx.fillStyle;
return new Color(
parseInt(hexColor.slice(1, 3), 16),
parseInt(hexColor.slice(3, 5), 16),
parseInt(hexColor.slice(5, 7), 16)
);
}
static getAll(): Color[] { static getAll(): Color[] {
return COLOR_NAMES.map((color) => Color.fromName(color)); return COLOR_NAMES.map((color) => Color.fromName(color));
} }