diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..016b59e --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# build output +dist/ + +# generated types +.astro/ + +# dependencies +node_modules/ + +# logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# environment variables +.env +.env.production + +# macOS-specific files +.DS_Store + +# jetbrains setting folder +.idea/ diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..22a1505 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,4 @@ +{ + "recommendations": ["astro-build.astro-vscode"], + "unwantedRecommendations": [] +} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..d642209 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,11 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "command": "./node_modules/.bin/astro dev", + "name": "Development server", + "request": "launch", + "type": "node-terminal" + } + ] +} diff --git a/README.md b/README.md index 414a13a..7aa2833 100644 --- a/README.md +++ b/README.md @@ -1,46 +1 @@ -# Astro Starter Kit: Basics - -```sh -npm create astro@latest -- --template basics -``` - -> 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun! - -## 🚀 Project Structure - -Inside of your Astro project, you'll see the following folders and files: - -```text -/ -├── public/ -│ └── favicon.svg -├── src -│   ├── assets -│   │   └── astro.svg -│   ├── components -│   │   └── Welcome.astro -│   ├── layouts -│   │   └── Layout.astro -│   └── pages -│   └── index.astro -└── package.json -``` - -To learn more about the folder structure of an Astro project, refer to [our guide on project structure](https://docs.astro.build/en/basics/project-structure/). - -## 🧞 Commands - -All commands are run from the root of the project, from a terminal: - -| Command | Action | -| :------------------------ | :----------------------------------------------- | -| `npm install` | Installs dependencies | -| `npm run dev` | Starts local dev server at `localhost:4321` | -| `npm run build` | Build your production site to `./dist/` | -| `npm run preview` | Preview your build locally, before deploying | -| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` | -| `npm run astro -- --help` | Get help using the Astro CLI | - -## 👀 Want to learn more? - -Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat). +doodly fuck \ No newline at end of file diff --git a/astro.config.mjs b/astro.config.mjs new file mode 100644 index 0000000..e12cfee --- /dev/null +++ b/astro.config.mjs @@ -0,0 +1,12 @@ +// @ts-check +import { defineConfig } from 'astro/config'; + +import svelte from '@astrojs/svelte'; + +// https://astro.build/config +export default defineConfig({ + integrations: [svelte()], + devToolbar: { + enabled: false + } +}); \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..5f8e52a --- /dev/null +++ b/package.json @@ -0,0 +1,24 @@ +{ + "name": "Scratch", + "type": "module", + "version": "0.0.1", + "engines": { + "node": ">=22.12.0" + }, + "scripts": { + "dev": "astro dev", + "build": "astro build", + "preview": "astro preview", + "astro": "astro" + }, + "dependencies": { + "@astrojs/svelte": "^8.1.0", + "@rferl/veronica": "github:rferl/veronica", + "astro": "^6.3.1", + "svelte": "^5.55.5", + "typescript": "^5.9.3" + }, + "devDependencies": { + "sass-embedded": "^1.99.0" + } +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..df3825f --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,3702 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@astrojs/svelte': + specifier: ^8.1.0 + version: 8.1.0(astro@6.3.1(rollup@4.60.3)(sass-embedded@1.99.0)(sass@1.99.0))(sass-embedded@1.99.0)(sass@1.99.0)(svelte@5.55.5)(typescript@5.9.3) + '@rferl/veronica': + specifier: github:rferl/veronica + version: https://codeload.github.com/rferl/veronica/tar.gz/6935ec341ba7dce2a839f045deaba4783f4e8afc + astro: + specifier: ^6.3.1 + version: 6.3.1(rollup@4.60.3)(sass-embedded@1.99.0)(sass@1.99.0) + svelte: + specifier: ^5.55.5 + version: 5.55.5 + typescript: + specifier: ^5.9.3 + version: 5.9.3 + devDependencies: + sass-embedded: + specifier: ^1.99.0 + version: 1.99.0 + +packages: + + '@astrojs/compiler@4.0.0': + resolution: {integrity: sha512-eouss7G8ygdZqHuke033VMcVw5HTZUu+PXd/h06DGDUg/jt5btPYPqh66ENWw/mU78rBrf/oeC4oqoBwMtDMNA==} + + '@astrojs/internal-helpers@0.9.0': + resolution: {integrity: sha512-GdYkzR26re8izmyYlBqf4z2s7zNngmWLFuxw0UKiPNqHraZGS6GKWIwSHgS22RDlu2ePFJ8bzmpBcUszut/SDg==} + + '@astrojs/markdown-remark@7.1.1': + resolution: {integrity: sha512-C6e9BnLGlbdv6bV8MYGeHpHxsUHrCrB4OuRLqi5LI7oiBVcBcqfUN06zpwFQdHgV48QCCrMmLpyqBr7VqC+swA==} + + '@astrojs/prism@4.0.1': + resolution: {integrity: sha512-nksZQVjlferuWzhPsBpQ1JE5XuKAf1id1/9Hj4a9KG4+ofrlzxUUwX4YGQF/SuDiuiGKEnzopGOt38F3AnVWsQ==} + engines: {node: '>=22.12.0'} + + '@astrojs/svelte@8.1.0': + resolution: {integrity: sha512-yZrHRFOxDJeo2hr9rGAMou6/6OL3agEaUCvWNWrea8YhZultsERTYZthfKNC58onAtZs76xNklOYV+G2Dp10kw==} + engines: {node: '>=22.12.0'} + peerDependencies: + astro: ^6.0.0 + svelte: ^5.43.6 + typescript: ^5.3.3 + + '@astrojs/telemetry@3.3.2': + resolution: {integrity: sha512-j8DNruA8ors99Al39RYZPJK4DC1bKkoNm93mAMuBhY9TCNC4R8n1q7ovFnJ5qhGh5Lsh7pa1gpQVpYpsJPeTHQ==} + engines: {node: 18.20.8 || ^20.3.0 || >=22.0.0} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.3': + resolution: {integrity: sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + + '@bufbuild/protobuf@2.12.0': + resolution: {integrity: sha512-B/XlCaFIP8LOwzo+bz5uFzATYokcwCKQcghqnlfwSmM5eX/qTkvDBnDPs+gXtX/RyjxJ4DRikECcPJbyALA8FA==} + + '@capsizecss/unpack@4.0.0': + resolution: {integrity: sha512-VERIM64vtTP1C4mxQ5thVT9fK0apjPFobqybMtA1UdUujWka24ERHbRHFGmpbbhp73MhV+KSsHQH9C6uOTdEQA==} + engines: {node: '>=18'} + + '@clack/core@1.3.0': + resolution: {integrity: sha512-xJPHpAmEQUBrXSLx0gF+q5K/IyihXpsHZcha+jB+tyahsKRK3Dxo4D0coZDewHo12NhiuzC3dTtMPbm53GEAAA==} + engines: {node: '>= 20.12.0'} + + '@clack/prompts@1.3.0': + resolution: {integrity: sha512-GgcWwRCs/xPtaqlMy8qRhPnZf9vlWcWZNHAitnVQ3yk7JmSralSiq5q07yaffYE8SogtDm7zFeKccx1QNVARpw==} + engines: {node: '>= 20.12.0'} + + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + + '@esbuild/aix-ppc64@0.27.7': + resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.7': + resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.7': + resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.7': + resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.7': + resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.7': + resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.7': + resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.7': + resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.7': + resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.7': + resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.7': + resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.7': + resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.7': + resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.7': + resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.7': + resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.7': + resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.7': + resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.7': + resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.7': + resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.7': + resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.7': + resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.7': + resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.7': + resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.7': + resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.7': + resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.7': + resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@img/colour@1.1.0': + resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.34.5': + resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.34.5': + resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.2.4': + resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.2.4': + resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.2.4': + resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-arm@1.2.4': + resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-ppc64@1.2.4': + resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-riscv64@1.2.4': + resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-s390x@1.2.4': + resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-x64@1.2.4': + resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-linux-arm64@0.34.5': + resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-arm@0.34.5': + resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-ppc64@0.34.5': + resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-riscv64@0.34.5': + resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-s390x@0.34.5': + resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-x64@0.34.5': + resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-linuxmusl-arm64@0.34.5': + resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-linuxmusl-x64@0.34.5': + resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-wasm32@0.34.5': + resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.34.5': + resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.34.5': + resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.34.5': + resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@oslojs/encoding@1.1.0': + resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==} + + '@parcel/watcher-android-arm64@2.5.6': + resolution: {integrity: sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [android] + + '@parcel/watcher-darwin-arm64@2.5.6': + resolution: {integrity: sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [darwin] + + '@parcel/watcher-darwin-x64@2.5.6': + resolution: {integrity: sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [darwin] + + '@parcel/watcher-freebsd-x64@2.5.6': + resolution: {integrity: sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [freebsd] + + '@parcel/watcher-linux-arm-glibc@2.5.6': + resolution: {integrity: sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@parcel/watcher-linux-arm-musl@2.5.6': + resolution: {integrity: sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + libc: [musl] + + '@parcel/watcher-linux-arm64-glibc@2.5.6': + resolution: {integrity: sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@parcel/watcher-linux-arm64-musl@2.5.6': + resolution: {integrity: sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@parcel/watcher-linux-x64-glibc@2.5.6': + resolution: {integrity: sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@parcel/watcher-linux-x64-musl@2.5.6': + resolution: {integrity: sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@parcel/watcher-win32-arm64@2.5.6': + resolution: {integrity: sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [win32] + + '@parcel/watcher-win32-ia32@2.5.6': + resolution: {integrity: sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==} + engines: {node: '>= 10.0.0'} + cpu: [ia32] + os: [win32] + + '@parcel/watcher-win32-x64@2.5.6': + resolution: {integrity: sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [win32] + + '@parcel/watcher@2.5.6': + resolution: {integrity: sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==} + engines: {node: '>= 10.0.0'} + + '@rferl/veronica@https://codeload.github.com/rferl/veronica/tar.gz/6935ec341ba7dce2a839f045deaba4783f4e8afc': + resolution: {gitHosted: true, integrity: sha512-xkeBP1Te2lwj1/9Nstbz6GmaTuAudZ35SGBm5ZUUaobBTeuTxcstHw491d+bTbbewgRY3P652tQJsx8hP+Q3KA==, tarball: https://codeload.github.com/rferl/veronica/tar.gz/6935ec341ba7dce2a839f045deaba4783f4e8afc} + version: 1.0.0 + + '@rollup/pluginutils@5.3.0': + resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/rollup-android-arm-eabi@4.60.3': + resolution: {integrity: sha512-x35CNW/ANXG3hE/EZpRU8MXX1JDN86hBb2wMGAtltkz7pc6cxgjpy1OMMfDosOQ+2hWqIkag/fGok1Yady9nGw==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.60.3': + resolution: {integrity: sha512-xw3xtkDApIOGayehp2+Rz4zimfkaX65r4t47iy+ymQB2G4iJCBBfj0ogVg5jpvjpn8UWn/+q9tprxleYeNp3Hw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.60.3': + resolution: {integrity: sha512-vo6Y5Qfpx7/5EaamIwi0WqW2+zfiusVihKatLvtN1VFVy3D13uERk/6gZLU1UiHRL6fDXqj/ELIeVRGnvcTE1g==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.60.3': + resolution: {integrity: sha512-D+0QGcZhBzTN82weOnsSlY7V7+RMmPuF1CkbxyMAGE8+ZHeUjyb76ZiWmBlCu//AQQONvxcqRbwZTajZKqjuOw==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.60.3': + resolution: {integrity: sha512-6HnvHCT7fDyj6R0Ph7A6x8dQS/S38MClRWeDLqc0MdfWkxjiu1HSDYrdPhqSILzjTIC/pnXbbJbo+ft+gy/9hQ==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.60.3': + resolution: {integrity: sha512-KHLgC3WKlUYW3ShFKnnosZDOJ0xjg9zp7au3sIm2bs/tGBeC2ipmvRh/N7JKi0t9Ue20C0dpEshi8WUubg+cnA==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.60.3': + resolution: {integrity: sha512-DV6fJoxEYWJOvaZIsok7KrYl0tPvga5OZ2yvKHNNYyk/2roMLqQAbGhr78EQ5YhHpnhLKJD3S1WFusAkmUuV5g==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.60.3': + resolution: {integrity: sha512-mQKoJAzvuOs6F+TZybQO4GOTSMUu7v0WdxEk24krQ/uUxXoPTtHjuaUuPmFhtBcM4K0ons8nrE3JyhTuCFtT/w==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.60.3': + resolution: {integrity: sha512-Whjj2qoiJ6+OOJMGptTYazaJvjOJm+iKHpXQM1P3LzGjt7Ff++Tp7nH4N8J/BUA7R9IHfDyx4DJIflifwnbmIA==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.60.3': + resolution: {integrity: sha512-4YTNHKqGng5+yiZt3mg77nmyuCfmNfX4fPmyUapBcIk+BdwSwmCWGXOUxhXbBEkFHtoN5boLj/5NON+u5QC9tg==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.60.3': + resolution: {integrity: sha512-SU3kNlhkpI4UqlUc2VXPGK9o886ZsSeGfMAX2ba2b8DKmMXq4AL7KUrkSWVbb7koVqx41Yczx6dx5PNargIrEA==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.60.3': + resolution: {integrity: sha512-6lDLl5h4TXpB1mTf2rQWnAk/LcXrx9vBfu/DT5TIPhvMhRWaZ5MxkIc8u4lJAmBo6klTe1ywXIUHFjylW505sg==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.60.3': + resolution: {integrity: sha512-BMo8bOw8evlup/8G+cj5xWtPyp93xPdyoSN16Zy90Q2QZ0ZYRhCt6ZJSwbrRzG9HApFabjwj2p25TUPDWrhzqQ==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.60.3': + resolution: {integrity: sha512-E0L8X1dZN1/Rph+5VPF6Xj2G7JJvMACVXtamTJIDrVI44Y3K+G8gQaMEAavbqCGTa16InptiVrX6eM6pmJ+7qA==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.60.3': + resolution: {integrity: sha512-oZJ/WHaVfHUiRAtmTAeo3DcevNsVvH8mbvodjZy7D5QKvCefO371SiKRpxoDcCxB3PTRTLayWBkvmDQKTcX/sw==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.60.3': + resolution: {integrity: sha512-Dhbyh7j9FybM3YaTgaHmVALwA8AkUwTPccyCQ79TG9AJUsMQqgN1DDEZNr4+QUfwiWvLDumW5vdwzoeUF+TNxQ==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.60.3': + resolution: {integrity: sha512-cJd1X5XhHHlltkaypz1UcWLA8AcoIi1aWhsvaWDskD1oz2eKCypnqvTQ8ykMNI0RSmm7NkTdSqSSD7zM0xa6Ig==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.60.3': + resolution: {integrity: sha512-DAZDBHQfG2oQuhY7mc6I3/qB4LU2fQCjRvxbDwd/Jdvb9fypP4IJ4qmtu6lNjes6B531AI8cg1aKC2di97bUxA==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.60.3': + resolution: {integrity: sha512-cRxsE8c13mZOh3vP+wLDxpQBRrOHDIGOWyDL93Sy0Ga8y515fBcC2pjUfFwUe5T7tqvTvWbCpg1URM/AXdWIXA==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.60.3': + resolution: {integrity: sha512-QaWcIgRxqEdQdhJqW4DJctsH6HCmo5vHxY0krHSX4jMtOqfzC+dqDGuHM87bu4H8JBeibWx7jFz+h6/4C8wA5Q==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.60.3': + resolution: {integrity: sha512-AaXwSvUi3QIPtroAUw1t5yHGIyqKEXwH54WUocFolZhpGDruJcs8c+xPNDRn4XiQsS7MEwnYsHW2l0MBLDMkWg==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.60.3': + resolution: {integrity: sha512-65LAKM/bAWDqKNEelHlcHvm2V+Vfb8C6INFxQXRHCvaVN1rJfwr4NvdP4FyzUaLqWfaCGaadf6UbTm8xJeYfEg==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.60.3': + resolution: {integrity: sha512-EEM2gyhBF5MFnI6vMKdX1LAosE627RGBzIoGMdLloPZkXrUN0Ckqgr2Qi8+J3zip/8NVVro3/FjB+tjhZUgUHA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.60.3': + resolution: {integrity: sha512-E5Eb5H/DpxaoXH++Qkv28RcUJboMopmdDUALBczvHMf7hNIxaDZqwY5lK12UK1BHacSmvupoEWGu+n993Z0y1A==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.60.3': + resolution: {integrity: sha512-hPt/bgL5cE+Qp+/TPHBqptcAgPzgj46mPcg/16zNUmbQk0j+mOEQV/+Lqu8QRtDV3Ek95Q6FeFITpuhl6OTsAA==} + cpu: [x64] + os: [win32] + + '@shikijs/core@4.0.2': + resolution: {integrity: sha512-hxT0YF4ExEqB8G/qFdtJvpmHXBYJ2lWW7qTHDarVkIudPFE6iCIrqdgWxGn5s+ppkGXI0aEGlibI0PAyzP3zlw==} + engines: {node: '>=20'} + + '@shikijs/engine-javascript@4.0.2': + resolution: {integrity: sha512-7PW0Nm49DcoUIQEXlJhNNBHyoGMjalRETTCcjMqEaMoJRLljy1Bi/EGV3/qLBgLKQejdspiiYuHGQW6dX94Nag==} + engines: {node: '>=20'} + + '@shikijs/engine-oniguruma@4.0.2': + resolution: {integrity: sha512-UpCB9Y2sUKlS9z8juFSKz7ZtysmeXCgnRF0dlhXBkmQnek7lAToPte8DkxmEYGNTMii72zU/lyXiCB6StuZeJg==} + engines: {node: '>=20'} + + '@shikijs/langs@4.0.2': + resolution: {integrity: sha512-KaXby5dvoeuZzN0rYQiPMjFoUrz4hgwIE+D6Du9owcHcl6/g16/yT5BQxSW5cGt2MZBz6Hl0YuRqf12omRfUUg==} + engines: {node: '>=20'} + + '@shikijs/primitive@4.0.2': + resolution: {integrity: sha512-M6UMPrSa3fN5ayeJwFVl9qWofl273wtK1VG8ySDZ1mQBfhCpdd8nEx7nPZ/tk7k+TYcpqBZzj/AnwxT9lO+HJw==} + engines: {node: '>=20'} + + '@shikijs/themes@4.0.2': + resolution: {integrity: sha512-mjCafwt8lJJaVSsQvNVrJumbnnj1RI8jbUKrPKgE6E3OvQKxnuRoBaYC51H4IGHePsGN/QtALglWBU7DoKDFnA==} + engines: {node: '>=20'} + + '@shikijs/types@4.0.2': + resolution: {integrity: sha512-qzbeRooUTPnLE+sHD/Z8DStmaDgnbbc/pMrU203950aRqjX/6AFHeDYT+j00y2lPdz0ywJKx7o/7qnqTivtlXg==} + engines: {node: '>=20'} + + '@shikijs/vscode-textmate@10.0.2': + resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + + '@sveltejs/acorn-typescript@1.0.9': + resolution: {integrity: sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA==} + peerDependencies: + acorn: ^8.9.0 + + '@sveltejs/vite-plugin-svelte-inspector@5.0.2': + resolution: {integrity: sha512-TZzRTcEtZffICSAoZGkPSl6Etsj2torOVrx6Uw0KpXxrec9Gg6jFWQ60Q3+LmNGfZSxHRCZL7vXVZIWmuV50Ig==} + engines: {node: ^20.19 || ^22.12 || >=24} + peerDependencies: + '@sveltejs/vite-plugin-svelte': ^6.0.0-next.0 + svelte: ^5.0.0 + vite: ^6.3.0 || ^7.0.0 + + '@sveltejs/vite-plugin-svelte@6.2.4': + resolution: {integrity: sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA==} + engines: {node: ^20.19 || ^22.12 || >=24} + peerDependencies: + svelte: ^5.0.0 + vite: ^6.3.0 || ^7.0.0 + + '@types/debug@4.1.13': + resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/hast@3.0.4': + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + + '@types/nlcst@2.0.3': + resolution: {integrity: sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA==} + + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + + '@ungap/structured-clone@1.3.1': + resolution: {integrity: sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==} + + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + aria-query@5.3.1: + resolution: {integrity: sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==} + engines: {node: '>= 0.4'} + + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + + array-iterate@2.0.1: + resolution: {integrity: sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg==} + + astro@6.3.1: + resolution: {integrity: sha512-atz6dmkE3Gu24bDgb7g2RE/BYnKqPYIHd6hTUM1UXvu/i7qNZOKLAqEHvgYpv9PQVcgWsXpk4/OOXZ0E/FzvSQ==} + engines: {node: '>=22.12.0', npm: '>=9.6.5', pnpm: '>=7.1.0'} + hasBin: true + + axobject-query@4.1.0: + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} + engines: {node: '>= 0.4'} + + bail@2.0.2: + resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + + boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + character-entities@2.0.2: + resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + chokidar@5.0.0: + resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} + engines: {node: '>= 20.19.0'} + + ci-info@4.4.0: + resolution: {integrity: sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==} + engines: {node: '>=8'} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + colorjs.io@0.5.2: + resolution: {integrity: sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==} + + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + + commander@11.1.0: + resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} + engines: {node: '>=16'} + + common-ancestor-path@2.0.0: + resolution: {integrity: sha512-dnN3ibLeoRf2HNC+OlCiNc5d2zxbLJXOtiZUudNFSXZrNSydxcCsSpRzXwfu7BBWCIfHPw+xTayeBvJCP/D8Ng==} + engines: {node: '>= 18'} + + cookie-es@1.2.3: + resolution: {integrity: sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw==} + + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + + crossws@0.3.5: + resolution: {integrity: sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==} + + css-select@5.2.2: + resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==} + + css-tree@2.2.1: + resolution: {integrity: sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} + + css-tree@3.2.1: + resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + css-what@6.2.2: + resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} + engines: {node: '>= 6'} + + csso@5.0.5: + resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decode-named-character-reference@1.3.0: + resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==} + + dedent-js@1.0.1: + resolution: {integrity: sha512-OUepMozQULMLUmhxS95Vudo0jb0UchLimi3+pQ2plj61Fcy8axbP9hbiD4Sz6DPqn6XG3kfmziVfQ1rSys5AJQ==} + + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + defu@6.1.7: + resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + destr@2.0.5: + resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + devalue@5.8.0: + resolution: {integrity: sha512-2zA9pFEsnp7vWBZbXF5JAgAq0fsUIt/1XPbRiAmRV3lp/2C3upzH+sADiyy66aFCihoLEsrQHxNM5w1gIDfsBg==} + + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + + diff@8.0.4: + resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==} + engines: {node: '>=0.3.1'} + + dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + + domutils@3.2.2: + resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + + dset@3.1.4: + resolution: {integrity: sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==} + engines: {node: '>=4'} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + + es-module-lexer@2.1.0: + resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==} + + esbuild@0.27.7: + resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} + engines: {node: '>=18'} + hasBin: true + + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + + esm-env@1.2.2: + resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==} + + esrap@2.2.6: + resolution: {integrity: sha512-WN0clHt0a4mzC780UBVVBpsj4vSSjOFNRd2WjYtduB9HeKxm1sjHMNUwLEHVjI3FdCQD/Hurgz9ftbKEzP79Ow==} + peerDependencies: + '@typescript-eslint/types': ^8.2.0 + peerDependenciesMeta: + '@typescript-eslint/types': + optional: true + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + eventemitter3@5.0.4: + resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + + fast-string-truncated-width@3.0.3: + resolution: {integrity: sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==} + + fast-string-width@3.0.2: + resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==} + + fast-wrap-ansi@0.2.0: + resolution: {integrity: sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + flattie@1.1.1: + resolution: {integrity: sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ==} + engines: {node: '>=8'} + + fontace@0.4.1: + resolution: {integrity: sha512-lDMvbAzSnHmbYMTEld5qdtvNH2/pWpICOqpean9IgC7vUbUJc3k+k5Dokp85CegamqQpFbXf0rAVkbzpyTA8aw==} + + fontkitten@1.0.3: + resolution: {integrity: sha512-Wp1zXWPVUPBmfoa3Cqc9ctaKuzKAV6uLstRqlR56kSjplf5uAce+qeyYym7F+PHbGTk+tCEdkCW6RD7DX/gBZw==} + engines: {node: '>=20'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + get-tsconfig@5.0.0-beta.4: + resolution: {integrity: sha512-7nF7C9fIPFEMHgEMEfgIlO9wDdZ8CyHw27rWciFZfHvHDReIiPhsYuzPRXsfvBCqFy1l8RRyyWV7QLM+ZhUJsQ==} + engines: {node: '>=20.20.0'} + + github-slugger@2.0.0: + resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==} + + h3@1.15.11: + resolution: {integrity: sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg==} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + hast-util-from-html@2.0.3: + resolution: {integrity: sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==} + + hast-util-from-parse5@8.0.3: + resolution: {integrity: sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==} + + hast-util-is-element@3.0.0: + resolution: {integrity: sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==} + + hast-util-parse-selector@4.0.0: + resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==} + + hast-util-raw@9.1.0: + resolution: {integrity: sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==} + + hast-util-to-html@9.0.5: + resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} + + hast-util-to-parse5@8.0.1: + resolution: {integrity: sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA==} + + hast-util-to-text@4.0.2: + resolution: {integrity: sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + + hastscript@9.0.1: + resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==} + + html-escaper@3.0.3: + resolution: {integrity: sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==} + + html-void-elements@3.0.0: + resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + + http-cache-semantics@4.2.0: + resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} + + immutable@5.1.5: + resolution: {integrity: sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==} + + iron-webcrypto@1.2.1: + resolution: {integrity: sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==} + + is-docker@3.0.0: + resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + + is-docker@4.0.0: + resolution: {integrity: sha512-LHE+wROyG/Y/0ZnbktRCoTix2c1RhgWaZraMZ8o1Q7zCh0VSrICJQO5oqIIISrcSBtrXv0o233w1IYwsWCjTzA==} + engines: {node: '>=20'} + hasBin: true + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-inside-container@1.0.0: + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} + engines: {node: '>=14.16'} + hasBin: true + + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + + is-reference@3.0.3: + resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} + + is-wsl@3.1.1: + resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==} + engines: {node: '>=16'} + + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + + jsonc-parser@3.3.1: + resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} + + locate-character@3.0.0: + resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} + + longest-streak@3.1.0: + resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + + lru-cache@11.3.6: + resolution: {integrity: sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==} + engines: {node: 20 || >=22} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + magicast@0.5.2: + resolution: {integrity: sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==} + + markdown-table@3.0.4: + resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} + + mdast-util-definitions@6.0.0: + resolution: {integrity: sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ==} + + mdast-util-find-and-replace@3.0.2: + resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} + + mdast-util-from-markdown@2.0.3: + resolution: {integrity: sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==} + + mdast-util-gfm-autolink-literal@2.0.1: + resolution: {integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==} + + mdast-util-gfm-footnote@2.1.0: + resolution: {integrity: sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==} + + mdast-util-gfm-strikethrough@2.0.0: + resolution: {integrity: sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==} + + mdast-util-gfm-table@2.0.0: + resolution: {integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==} + + mdast-util-gfm-task-list-item@2.0.0: + resolution: {integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==} + + mdast-util-gfm@3.1.0: + resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==} + + mdast-util-phrasing@4.1.0: + resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} + + mdast-util-to-hast@13.2.1: + resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} + + mdast-util-to-markdown@2.1.2: + resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} + + mdast-util-to-string@4.0.0: + resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + + mdn-data@2.0.28: + resolution: {integrity: sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==} + + mdn-data@2.27.1: + resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} + + micromark-core-commonmark@2.0.3: + resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} + + micromark-extension-gfm-autolink-literal@2.1.0: + resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==} + + micromark-extension-gfm-footnote@2.1.0: + resolution: {integrity: sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==} + + micromark-extension-gfm-strikethrough@2.1.0: + resolution: {integrity: sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==} + + micromark-extension-gfm-table@2.1.1: + resolution: {integrity: sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==} + + micromark-extension-gfm-tagfilter@2.0.0: + resolution: {integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==} + + micromark-extension-gfm-task-list-item@2.1.0: + resolution: {integrity: sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==} + + micromark-extension-gfm@3.0.0: + resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==} + + micromark-factory-destination@2.0.1: + resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} + + micromark-factory-label@2.0.1: + resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} + + micromark-factory-space@2.0.1: + resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} + + micromark-factory-title@2.0.1: + resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} + + micromark-factory-whitespace@2.0.1: + resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-chunked@2.0.1: + resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} + + micromark-util-classify-character@2.0.1: + resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} + + micromark-util-combine-extensions@2.0.1: + resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} + + micromark-util-decode-numeric-character-reference@2.0.2: + resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} + + micromark-util-decode-string@2.0.1: + resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-html-tag-name@2.0.1: + resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} + + micromark-util-normalize-identifier@2.0.1: + resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} + + micromark-util-resolve-all@2.0.1: + resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-subtokenize@2.1.0: + resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + + micromark@4.0.2: + resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} + + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} + engines: {node: '>=10'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.12: + resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + neotraverse@0.6.18: + resolution: {integrity: sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA==} + engines: {node: '>= 10'} + + nlcst-to-string@4.0.0: + resolution: {integrity: sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA==} + + node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + + node-fetch-native@1.6.7: + resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} + + node-mock-http@1.0.4: + resolution: {integrity: sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ==} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + + obug@2.1.1: + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + + ofetch@1.5.1: + resolution: {integrity: sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==} + + ohash@2.0.11: + resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} + + oniguruma-parser@0.12.2: + resolution: {integrity: sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==} + + oniguruma-to-es@4.3.6: + resolution: {integrity: sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==} + + p-limit@7.3.0: + resolution: {integrity: sha512-7cIXg/Z0M5WZRblrsOla88S4wAK+zOQQWeBYfV3qJuJXMr+LnbYjaadrFaS0JILfEDPVqHyKnZ1Z/1d6J9VVUw==} + engines: {node: '>=20'} + + p-queue@9.2.0: + resolution: {integrity: sha512-dWgLE8AH0HjQ9fe74pUkKkvzzYT18Inp4zra3lKHnnwqGvcfcUBrvF2EAVX+envufDNBOzpPq/IBUONDbI7+3g==} + engines: {node: '>=20'} + + p-timeout@7.0.1: + resolution: {integrity: sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg==} + engines: {node: '>=20'} + + package-manager-detector@1.6.0: + resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==} + + parse-latin@7.0.0: + resolution: {integrity: sha512-mhHgobPPua5kZ98EF4HWiH167JWBfl4pvAIXXdbaVohtK7a6YBOy56kvhCqduqyo/f3yrHFWmqmiMg/BkBkYYQ==} + + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + + piccolore@0.1.3: + resolution: {integrity: sha512-o8bTeDWjE086iwKrROaDf31K0qC/BENdm15/uH9usSC/uZjJOKb2YGiVHfLY4GhwsERiPI1jmwI2XrA7ACOxVw==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + postcss@8.5.14: + resolution: {integrity: sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==} + engines: {node: ^10 || ^12 || >=14} + + prismjs@1.30.0: + resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==} + engines: {node: '>=6'} + + property-information@7.1.0: + resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} + + radix3@1.1.2: + resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + readdirp@5.0.0: + resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} + engines: {node: '>= 20.19.0'} + + regex-recursion@6.0.2: + resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} + + regex-utilities@2.3.0: + resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==} + + regex@6.1.0: + resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==} + + rehype-parse@9.0.1: + resolution: {integrity: sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag==} + + rehype-raw@7.0.0: + resolution: {integrity: sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==} + + rehype-stringify@10.0.1: + resolution: {integrity: sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA==} + + rehype@13.0.2: + resolution: {integrity: sha512-j31mdaRFrwFRUIlxGeuPXXKWQxet52RBQRvCmzl5eCefn/KGbomK5GMHNMsOJf55fgo3qw5tST5neDuarDYR2A==} + + remark-gfm@4.0.1: + resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==} + + remark-parse@11.0.0: + resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} + + remark-rehype@11.1.2: + resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==} + + remark-smartypants@3.0.2: + resolution: {integrity: sha512-ILTWeOriIluwEvPjv67v7Blgrcx+LZOkAUVtKI3putuhlZm84FnqDORNXPPm+HY3NdZOMhyDwZ1E+eZB/Df5dA==} + engines: {node: '>=16.0.0'} + + remark-stringify@11.0.0: + resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + retext-latin@4.0.0: + resolution: {integrity: sha512-hv9woG7Fy0M9IlRQloq/N6atV82NxLGveq+3H2WOi79dtIYWN8OaxogDm77f8YnVXJL2VD3bbqowu5E3EMhBYA==} + + retext-smartypants@6.2.0: + resolution: {integrity: sha512-kk0jOU7+zGv//kfjXEBjdIryL1Acl4i9XNkHxtM7Tm5lFiCog576fjNC9hjoR7LTKQ0DsPWy09JummSsH1uqfQ==} + + retext-stringify@4.0.0: + resolution: {integrity: sha512-rtfN/0o8kL1e+78+uxPTqu1Klt0yPzKuQ2BfWwwfgIUSayyzxpM1PJzkKt4V8803uB9qSy32MvI7Xep9khTpiA==} + + retext@9.0.0: + resolution: {integrity: sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA==} + + rollup@4.60.3: + resolution: {integrity: sha512-pAQK9HalE84QSm4Po3EmWIZPd3FnjkShVkiMlz1iligWYkWQ7wHYd1PF/T7QZ5TVSD6uSTon5gBVMSM4JfBV+A==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + rxjs@7.8.2: + resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + + sass-embedded-all-unknown@1.99.0: + resolution: {integrity: sha512-qPIRG8Uhjo6/OKyAKixTnwMliTz+t9K6Duk0mx5z+K7n0Ts38NSJz2sjDnc7cA/8V9Lb3q09H38dZ1CLwD+ssw==} + cpu: ['!arm', '!arm64', '!riscv64', '!x64'] + + sass-embedded-android-arm64@1.99.0: + resolution: {integrity: sha512-fNHhdnP23yqqieCbAdym4N47AleSwjbNt6OYIYx4DdACGdtERjQB4iOX/TaKsW034MupfF7SjnAAK8w7Ptldtg==} + engines: {node: '>=14.0.0'} + cpu: [arm64] + os: [android] + + sass-embedded-android-arm@1.99.0: + resolution: {integrity: sha512-EHvJ0C7/VuP78Qr6f8gIUVUmCqIorEQpw2yp3cs3SMg02ZuumlhjXvkTcFBxHmFdFR23vTNk1WnhY6QSeV1nFQ==} + engines: {node: '>=14.0.0'} + cpu: [arm] + os: [android] + + sass-embedded-android-riscv64@1.99.0: + resolution: {integrity: sha512-4zqDFRvgGDTL5vTHuIhRxUpXFoh0Cy7Gm5Ywk19ASd8Settmd14YdPRZPmMxfgS1GH292PofV1fq1ifiSEJWBw==} + engines: {node: '>=14.0.0'} + cpu: [riscv64] + os: [android] + + sass-embedded-android-x64@1.99.0: + resolution: {integrity: sha512-Uk53k/dGYt04RjOL4gFjZ0Z9DH9DKh8IA8WsXUkNqsxerAygoy3zqRBS2zngfE9K2jiOM87q+1R1p87ory9oQQ==} + engines: {node: '>=14.0.0'} + cpu: [x64] + os: [android] + + sass-embedded-darwin-arm64@1.99.0: + resolution: {integrity: sha512-u61/7U3IGLqoO6gL+AHeiAtlTPFwJK1+964U8gp45ZN0hzh1yrARf5O1mivXv8NnNgJvbG2wWJbiNZP0lG/lTg==} + engines: {node: '>=14.0.0'} + cpu: [arm64] + os: [darwin] + + sass-embedded-darwin-x64@1.99.0: + resolution: {integrity: sha512-j/kkk/NcXdIameLezSfXjgCiBkVcA+G60AXrX768/3g0miK1g7M9dj7xOhCb1i7/wQeiEI3rw2LLuO63xRIn4A==} + engines: {node: '>=14.0.0'} + cpu: [x64] + os: [darwin] + + sass-embedded-linux-arm64@1.99.0: + resolution: {integrity: sha512-btNcFpItcB56L40n8hDeL7sRSMLDXQ56nB5h2deddJx1n60rpKSElJmkaDGHtpkrY+CTtDRV0FZDjHeTJddYew==} + engines: {node: '>=14.0.0'} + cpu: [arm64] + os: [linux] + libc: glibc + + sass-embedded-linux-arm@1.99.0: + resolution: {integrity: sha512-d4IjJZrX2+AwB2YCy1JySwdptJECNP/WfAQLUl8txI3ka8/d3TUI155GtelnoZUkio211PwIeFvvAeZ9RXPQnw==} + engines: {node: '>=14.0.0'} + cpu: [arm] + os: [linux] + libc: glibc + + sass-embedded-linux-musl-arm64@1.99.0: + resolution: {integrity: sha512-Hi2bt/IrM5P4FBKz6EcHAlniwfpoz9mnTdvSd58y+avA3SANM76upIkAdSayA8ZGwyL3gZokru1AKDPF9lJDNw==} + engines: {node: '>=14.0.0'} + cpu: [arm64] + os: [linux] + libc: musl + + sass-embedded-linux-musl-arm@1.99.0: + resolution: {integrity: sha512-2gvHOupgIw3ytatXT4nFUow71LFbuOZPEwG+HUzcNQDH8ue4Ez8cr03vsv5MDv3lIjOKcXwDvWD980t18MwkoQ==} + engines: {node: '>=14.0.0'} + cpu: [arm] + os: [linux] + libc: musl + + sass-embedded-linux-musl-riscv64@1.99.0: + resolution: {integrity: sha512-mKqGvVaJ9rHMqyZsF0kikQe4NO0f4osb67+X6nLhBiVDKvyazQHJ3zJQreNefIE36yL2sjHIclSB//MprzaQDg==} + engines: {node: '>=14.0.0'} + cpu: [riscv64] + os: [linux] + libc: musl + + sass-embedded-linux-musl-x64@1.99.0: + resolution: {integrity: sha512-huhgOMmOc30r7CH7qbRbT9LerSEGSnWuS4CYNOskr9BvNeQp4dIneFufNRGZ7hkOAxUM8DglxIZJN/cyAT95Ew==} + engines: {node: '>=14.0.0'} + cpu: [x64] + os: [linux] + libc: musl + + sass-embedded-linux-riscv64@1.99.0: + resolution: {integrity: sha512-mevFPIFAVhrH90THifxLfOntFmHtcEKOcdWnep2gJ0X4DVva4AiVIRlQe/7w9JFx5+gnDRE1oaJJkzuFUuYZsA==} + engines: {node: '>=14.0.0'} + cpu: [riscv64] + os: [linux] + libc: glibc + + sass-embedded-linux-x64@1.99.0: + resolution: {integrity: sha512-9k7IkULqIZdCIVt4Mboryt6vN8Mjmm3EhI1P3mClU5y5i3wLK5ExC3cbVWk047KsID/fvB1RLslqghXJx5BoxA==} + engines: {node: '>=14.0.0'} + cpu: [x64] + os: [linux] + libc: glibc + + sass-embedded-unknown-all@1.99.0: + resolution: {integrity: sha512-P7MxiUtL/XzGo3PX0CaB8lNNEFLQWKikPA8pbKytx9ZCLZSDkt2NJcdAbblB/sqMs4AV3EK2NadV8rI/diq3xg==} + os: ['!android', '!darwin', '!linux', '!win32'] + + sass-embedded-win32-arm64@1.99.0: + resolution: {integrity: sha512-8whpsW7S+uO8QApKfQuc36m3P9EISzbVZOgC79goob4qGy09u8Gz/rYvw8h1prJDSjltpHGhOzBE6LDz7WvzVw==} + engines: {node: '>=14.0.0'} + cpu: [arm64] + os: [win32] + + sass-embedded-win32-x64@1.99.0: + resolution: {integrity: sha512-ipuOv1R2K4MHeuCEAZGpuUbAgma4gb0sdacyrTjJtMOy/OY9UvWfVlwErdB09KIkp4fPDpQJDJfvYN6bC8jeNg==} + engines: {node: '>=14.0.0'} + cpu: [x64] + os: [win32] + + sass-embedded@1.99.0: + resolution: {integrity: sha512-gF/juR1aX02lZHkvwxdF80SapkQeg2fetoDF6gIQkNbSw5YEUFspMkyGTjPjgZSgIHuZpy+Wz4PlebKnLXMjdg==} + engines: {node: '>=16.0.0'} + hasBin: true + + sass@1.99.0: + resolution: {integrity: sha512-kgW13M54DUB7IsIRM5LvJkNlpH+WhMpooUcaWGFARkF1Tc82v9mIWkCbCYf+MBvpIUBSeSOTilpZjEPr2VYE6Q==} + engines: {node: '>=14.0.0'} + hasBin: true + + sax@1.6.0: + resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==} + engines: {node: '>=11.0.0'} + + scule@1.3.0: + resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==} + + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + + sharp@0.34.5: + resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + + shiki@4.0.2: + resolution: {integrity: sha512-eAVKTMedR5ckPo4xne/PjYQYrU3qx78gtJZ+sHlXEg5IHhhoQhMfZVzetTYuaJS0L2Ef3AcCRzCHV8T0WI6nIQ==} + engines: {node: '>=20'} + + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + + smol-toml@1.6.1: + resolution: {integrity: sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==} + engines: {node: '>= 18'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + + svelte2tsx@0.7.55: + resolution: {integrity: sha512-JWzgeM3lqySRNfqcsesvVEh8LhTWBxQJ9RMjzJ+VepdmXtVnNd0SbtGctG6+/fbHq0N6mhwSd823gszw9JHeGQ==} + peerDependencies: + svelte: ^3.55 || ^4.0.0-next.0 || ^4.0 || ^5.0.0-next.0 + typescript: ^4.9.4 || ^5.0.0 || ^6.0.0 + + svelte@5.55.5: + resolution: {integrity: sha512-2uCs/LZ9us+AktdzYJM8OcxQ8qnPS1kpaO7syGT/MgO+6Qr1Ybl+TqPq+97u7PHqmmMlye5ZkoyXONy5mjjAbw==} + engines: {node: '>=18'} + + svgo@4.0.1: + resolution: {integrity: sha512-XDpWUOPC6FEibaLzjfe0ucaV0YrOjYotGJO1WpF0Zd+n6ZGEQUsSugaoLq9QkEZtAfQIxT42UChcssDVPP3+/w==} + engines: {node: '>=16'} + hasBin: true + + sync-child-process@1.0.2: + resolution: {integrity: sha512-8lD+t2KrrScJ/7KXCSyfhT3/hRq78rC0wBFqNJXv3mZyn6hW2ypM05JmlSvtqRbeq6jqA94oHbxAr2vYsJ8vDA==} + engines: {node: '>=16.0.0'} + + sync-message-port@1.2.0: + resolution: {integrity: sha512-gAQ9qrUN/UCypHtGFbbe7Rc/f9bzO88IwrG8TDo/aMKAApKyD6E3W4Cm0EfhfBb6Z6SKt59tTCTfD+n1xmAvMg==} + engines: {node: '>=16.0.0'} + + tiny-inflate@1.0.3: + resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==} + + tinyclip@0.1.12: + resolution: {integrity: sha512-Ae3OVUqifDw0wBriIBS7yVaW44Dp6eSHQcyq4Igc7eN2TJH/2YsicswaW+J/OuMvhpDPOKEgpAZCjkb4hpoyeA==} + engines: {node: ^16.14.0 || >= 17.3.0} + + tinyexec@1.1.2: + resolution: {integrity: sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==} + engines: {node: '>=18'} + + tinyglobby@0.2.16: + resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} + engines: {node: '>=12.0.0'} + + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + trough@2.2.0: + resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + ufo@1.6.4: + resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==} + + ultrahtml@1.6.0: + resolution: {integrity: sha512-R9fBn90VTJrqqLDwyMph+HGne8eqY1iPfYhPzZrvKpIfwkWZbcYlfpsb8B9dTvBfpy1/hqAD7Wi8EKfP9e8zdw==} + + uncrypto@0.1.3: + resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==} + + unified@11.0.5: + resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} + + unifont@0.7.4: + resolution: {integrity: sha512-oHeis4/xl42HUIeHuNZRGEvxj5AaIKR+bHPNegRq5LV1gdc3jundpONbjglKpihmJf+dswygdMJn3eftGIMemg==} + + unist-util-find-after@5.0.0: + resolution: {integrity: sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==} + + unist-util-is@6.0.1: + resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} + + unist-util-modify-children@4.0.0: + resolution: {integrity: sha512-+tdN5fGNddvsQdIzUF3Xx82CU9sMM+fA0dLgR9vOmT0oPT2jH+P1nd5lSqfCfXAw+93NhcXNY2qqvTUtE4cQkw==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-remove-position@5.0.0: + resolution: {integrity: sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-children@3.0.0: + resolution: {integrity: sha512-RgmdTfSBOg04sdPcpTSD1jzoNBjt9a80/ZCzp5cI9n1qPzLZWF9YdvWGN2zmTumP1HWhXKdUWexjy/Wy/lJ7tA==} + + unist-util-visit-parents@6.0.2: + resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} + + unist-util-visit@5.1.0: + resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} + + unstorage@1.17.5: + resolution: {integrity: sha512-0i3iqvRfx29hkNntHyQvJTpf5W9dQ9ZadSoRU8+xVlhVtT7jAX57fazYO9EHvcRCfBCyi5YRya7XCDOsbTgkPg==} + peerDependencies: + '@azure/app-configuration': ^1.8.0 + '@azure/cosmos': ^4.2.0 + '@azure/data-tables': ^13.3.0 + '@azure/identity': ^4.6.0 + '@azure/keyvault-secrets': ^4.9.0 + '@azure/storage-blob': ^12.26.0 + '@capacitor/preferences': ^6 || ^7 || ^8 + '@deno/kv': '>=0.9.0' + '@netlify/blobs': ^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0 + '@planetscale/database': ^1.19.0 + '@upstash/redis': ^1.34.3 + '@vercel/blob': '>=0.27.1' + '@vercel/functions': ^2.2.12 || ^3.0.0 + '@vercel/kv': ^1 || ^2 || ^3 + aws4fetch: ^1.0.20 + db0: '>=0.2.1' + idb-keyval: ^6.2.1 + ioredis: ^5.4.2 + uploadthing: ^7.4.4 + peerDependenciesMeta: + '@azure/app-configuration': + optional: true + '@azure/cosmos': + optional: true + '@azure/data-tables': + optional: true + '@azure/identity': + optional: true + '@azure/keyvault-secrets': + optional: true + '@azure/storage-blob': + optional: true + '@capacitor/preferences': + optional: true + '@deno/kv': + optional: true + '@netlify/blobs': + optional: true + '@planetscale/database': + optional: true + '@upstash/redis': + optional: true + '@vercel/blob': + optional: true + '@vercel/functions': + optional: true + '@vercel/kv': + optional: true + aws4fetch: + optional: true + db0: + optional: true + idb-keyval: + optional: true + ioredis: + optional: true + uploadthing: + optional: true + + varint@6.0.0: + resolution: {integrity: sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==} + + vfile-location@5.0.3: + resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==} + + vfile-message@4.0.3: + resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + + vite@7.3.3: + resolution: {integrity: sha512-/4XH147Ui7OGTjg3HbdWe5arnZQSbfuRzdr9Ec7TQi5I7R+ir0Rlc9GIvD4v0XZurELqA035KVXJXpR61xhiTA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitefu@1.1.3: + resolution: {integrity: sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==} + peerDependencies: + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + vite: + optional: true + + web-namespaces@2.0.1: + resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} + + which-pm-runs@1.1.0: + resolution: {integrity: sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA==} + engines: {node: '>=4'} + + xxhash-wasm@1.1.0: + resolution: {integrity: sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==} + + yargs-parser@22.0.0: + resolution: {integrity: sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=23} + + yocto-queue@1.2.2: + resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==} + engines: {node: '>=12.20'} + + zimmerframe@1.1.4: + resolution: {integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==} + + zod@4.4.3: + resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + +snapshots: + + '@astrojs/compiler@4.0.0': {} + + '@astrojs/internal-helpers@0.9.0': + dependencies: + picomatch: 4.0.4 + + '@astrojs/markdown-remark@7.1.1': + dependencies: + '@astrojs/internal-helpers': 0.9.0 + '@astrojs/prism': 4.0.1 + github-slugger: 2.0.0 + hast-util-from-html: 2.0.3 + hast-util-to-text: 4.0.2 + js-yaml: 4.1.1 + mdast-util-definitions: 6.0.0 + rehype-raw: 7.0.0 + rehype-stringify: 10.0.1 + remark-gfm: 4.0.1 + remark-parse: 11.0.0 + remark-rehype: 11.1.2 + remark-smartypants: 3.0.2 + retext-smartypants: 6.2.0 + shiki: 4.0.2 + smol-toml: 1.6.1 + unified: 11.0.5 + unist-util-remove-position: 5.0.0 + unist-util-visit: 5.1.0 + unist-util-visit-parents: 6.0.2 + vfile: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@astrojs/prism@4.0.1': + dependencies: + prismjs: 1.30.0 + + '@astrojs/svelte@8.1.0(astro@6.3.1(rollup@4.60.3)(sass-embedded@1.99.0)(sass@1.99.0))(sass-embedded@1.99.0)(sass@1.99.0)(svelte@5.55.5)(typescript@5.9.3)': + dependencies: + '@sveltejs/vite-plugin-svelte': 6.2.4(svelte@5.55.5)(vite@7.3.3(sass-embedded@1.99.0)(sass@1.99.0)) + astro: 6.3.1(rollup@4.60.3)(sass-embedded@1.99.0)(sass@1.99.0) + svelte: 5.55.5 + svelte2tsx: 0.7.55(svelte@5.55.5)(typescript@5.9.3) + typescript: 5.9.3 + vite: 7.3.3(sass-embedded@1.99.0)(sass@1.99.0) + vitefu: 1.1.3(vite@7.3.3(sass-embedded@1.99.0)(sass@1.99.0)) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - terser + - tsx + - yaml + + '@astrojs/telemetry@3.3.2': + dependencies: + ci-info: 4.4.0 + dset: 3.1.4 + is-docker: 4.0.0 + is-wsl: 3.1.1 + which-pm-runs: 1.1.0 + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/parser@7.29.3': + dependencies: + '@babel/types': 7.29.0 + + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@bufbuild/protobuf@2.12.0': {} + + '@capsizecss/unpack@4.0.0': + dependencies: + fontkitten: 1.0.3 + + '@clack/core@1.3.0': + dependencies: + fast-wrap-ansi: 0.2.0 + sisteransi: 1.0.5 + + '@clack/prompts@1.3.0': + dependencies: + '@clack/core': 1.3.0 + fast-string-width: 3.0.2 + fast-wrap-ansi: 0.2.0 + sisteransi: 1.0.5 + + '@emnapi/runtime@1.10.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@esbuild/aix-ppc64@0.27.7': + optional: true + + '@esbuild/android-arm64@0.27.7': + optional: true + + '@esbuild/android-arm@0.27.7': + optional: true + + '@esbuild/android-x64@0.27.7': + optional: true + + '@esbuild/darwin-arm64@0.27.7': + optional: true + + '@esbuild/darwin-x64@0.27.7': + optional: true + + '@esbuild/freebsd-arm64@0.27.7': + optional: true + + '@esbuild/freebsd-x64@0.27.7': + optional: true + + '@esbuild/linux-arm64@0.27.7': + optional: true + + '@esbuild/linux-arm@0.27.7': + optional: true + + '@esbuild/linux-ia32@0.27.7': + optional: true + + '@esbuild/linux-loong64@0.27.7': + optional: true + + '@esbuild/linux-mips64el@0.27.7': + optional: true + + '@esbuild/linux-ppc64@0.27.7': + optional: true + + '@esbuild/linux-riscv64@0.27.7': + optional: true + + '@esbuild/linux-s390x@0.27.7': + optional: true + + '@esbuild/linux-x64@0.27.7': + optional: true + + '@esbuild/netbsd-arm64@0.27.7': + optional: true + + '@esbuild/netbsd-x64@0.27.7': + optional: true + + '@esbuild/openbsd-arm64@0.27.7': + optional: true + + '@esbuild/openbsd-x64@0.27.7': + optional: true + + '@esbuild/openharmony-arm64@0.27.7': + optional: true + + '@esbuild/sunos-x64@0.27.7': + optional: true + + '@esbuild/win32-arm64@0.27.7': + optional: true + + '@esbuild/win32-ia32@0.27.7': + optional: true + + '@esbuild/win32-x64@0.27.7': + optional: true + + '@img/colour@1.1.0': + optional: true + + '@img/sharp-darwin-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.2.4 + optional: true + + '@img/sharp-darwin-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.2.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.2.4': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-s390x@1.2.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + optional: true + + '@img/sharp-linux-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.2.4 + optional: true + + '@img/sharp-linux-arm@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.2.4 + optional: true + + '@img/sharp-linux-ppc64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.2.4 + optional: true + + '@img/sharp-linux-riscv64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.2.4 + optional: true + + '@img/sharp-linux-s390x@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.2.4 + optional: true + + '@img/sharp-linux-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + optional: true + + '@img/sharp-wasm32@0.34.5': + dependencies: + '@emnapi/runtime': 1.10.0 + optional: true + + '@img/sharp-win32-arm64@0.34.5': + optional: true + + '@img/sharp-win32-ia32@0.34.5': + optional: true + + '@img/sharp-win32-x64@0.34.5': + optional: true + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@oslojs/encoding@1.1.0': {} + + '@parcel/watcher-android-arm64@2.5.6': + optional: true + + '@parcel/watcher-darwin-arm64@2.5.6': + optional: true + + '@parcel/watcher-darwin-x64@2.5.6': + optional: true + + '@parcel/watcher-freebsd-x64@2.5.6': + optional: true + + '@parcel/watcher-linux-arm-glibc@2.5.6': + optional: true + + '@parcel/watcher-linux-arm-musl@2.5.6': + optional: true + + '@parcel/watcher-linux-arm64-glibc@2.5.6': + optional: true + + '@parcel/watcher-linux-arm64-musl@2.5.6': + optional: true + + '@parcel/watcher-linux-x64-glibc@2.5.6': + optional: true + + '@parcel/watcher-linux-x64-musl@2.5.6': + optional: true + + '@parcel/watcher-win32-arm64@2.5.6': + optional: true + + '@parcel/watcher-win32-ia32@2.5.6': + optional: true + + '@parcel/watcher-win32-x64@2.5.6': + optional: true + + '@parcel/watcher@2.5.6': + dependencies: + detect-libc: 2.1.2 + is-glob: 4.0.3 + node-addon-api: 7.1.1 + picomatch: 4.0.4 + optionalDependencies: + '@parcel/watcher-android-arm64': 2.5.6 + '@parcel/watcher-darwin-arm64': 2.5.6 + '@parcel/watcher-darwin-x64': 2.5.6 + '@parcel/watcher-freebsd-x64': 2.5.6 + '@parcel/watcher-linux-arm-glibc': 2.5.6 + '@parcel/watcher-linux-arm-musl': 2.5.6 + '@parcel/watcher-linux-arm64-glibc': 2.5.6 + '@parcel/watcher-linux-arm64-musl': 2.5.6 + '@parcel/watcher-linux-x64-glibc': 2.5.6 + '@parcel/watcher-linux-x64-musl': 2.5.6 + '@parcel/watcher-win32-arm64': 2.5.6 + '@parcel/watcher-win32-ia32': 2.5.6 + '@parcel/watcher-win32-x64': 2.5.6 + optional: true + + '@rferl/veronica@https://codeload.github.com/rferl/veronica/tar.gz/6935ec341ba7dce2a839f045deaba4783f4e8afc': {} + + '@rollup/pluginutils@5.3.0(rollup@4.60.3)': + dependencies: + '@types/estree': 1.0.9 + estree-walker: 2.0.2 + picomatch: 4.0.4 + optionalDependencies: + rollup: 4.60.3 + + '@rollup/rollup-android-arm-eabi@4.60.3': + optional: true + + '@rollup/rollup-android-arm64@4.60.3': + optional: true + + '@rollup/rollup-darwin-arm64@4.60.3': + optional: true + + '@rollup/rollup-darwin-x64@4.60.3': + optional: true + + '@rollup/rollup-freebsd-arm64@4.60.3': + optional: true + + '@rollup/rollup-freebsd-x64@4.60.3': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.60.3': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.60.3': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.60.3': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.60.3': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.60.3': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.60.3': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.60.3': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.60.3': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.60.3': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.60.3': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.60.3': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.60.3': + optional: true + + '@rollup/rollup-linux-x64-musl@4.60.3': + optional: true + + '@rollup/rollup-openbsd-x64@4.60.3': + optional: true + + '@rollup/rollup-openharmony-arm64@4.60.3': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.60.3': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.60.3': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.60.3': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.60.3': + optional: true + + '@shikijs/core@4.0.2': + dependencies: + '@shikijs/primitive': 4.0.2 + '@shikijs/types': 4.0.2 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + hast-util-to-html: 9.0.5 + + '@shikijs/engine-javascript@4.0.2': + dependencies: + '@shikijs/types': 4.0.2 + '@shikijs/vscode-textmate': 10.0.2 + oniguruma-to-es: 4.3.6 + + '@shikijs/engine-oniguruma@4.0.2': + dependencies: + '@shikijs/types': 4.0.2 + '@shikijs/vscode-textmate': 10.0.2 + + '@shikijs/langs@4.0.2': + dependencies: + '@shikijs/types': 4.0.2 + + '@shikijs/primitive@4.0.2': + dependencies: + '@shikijs/types': 4.0.2 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + + '@shikijs/themes@4.0.2': + dependencies: + '@shikijs/types': 4.0.2 + + '@shikijs/types@4.0.2': + dependencies: + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + + '@shikijs/vscode-textmate@10.0.2': {} + + '@sveltejs/acorn-typescript@1.0.9(acorn@8.16.0)': + dependencies: + acorn: 8.16.0 + + '@sveltejs/vite-plugin-svelte-inspector@5.0.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.55.5)(vite@7.3.3(sass-embedded@1.99.0)(sass@1.99.0)))(svelte@5.55.5)(vite@7.3.3(sass-embedded@1.99.0)(sass@1.99.0))': + dependencies: + '@sveltejs/vite-plugin-svelte': 6.2.4(svelte@5.55.5)(vite@7.3.3(sass-embedded@1.99.0)(sass@1.99.0)) + obug: 2.1.1 + svelte: 5.55.5 + vite: 7.3.3(sass-embedded@1.99.0)(sass@1.99.0) + + '@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.55.5)(vite@7.3.3(sass-embedded@1.99.0)(sass@1.99.0))': + dependencies: + '@sveltejs/vite-plugin-svelte-inspector': 5.0.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.55.5)(vite@7.3.3(sass-embedded@1.99.0)(sass@1.99.0)))(svelte@5.55.5)(vite@7.3.3(sass-embedded@1.99.0)(sass@1.99.0)) + deepmerge: 4.3.1 + magic-string: 0.30.21 + obug: 2.1.1 + svelte: 5.55.5 + vite: 7.3.3(sass-embedded@1.99.0)(sass@1.99.0) + vitefu: 1.1.3(vite@7.3.3(sass-embedded@1.99.0)(sass@1.99.0)) + + '@types/debug@4.1.13': + dependencies: + '@types/ms': 2.1.0 + + '@types/estree@1.0.8': {} + + '@types/estree@1.0.9': {} + + '@types/hast@3.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/ms@2.1.0': {} + + '@types/nlcst@2.0.3': + dependencies: + '@types/unist': 3.0.3 + + '@types/trusted-types@2.0.7': {} + + '@types/unist@3.0.3': {} + + '@ungap/structured-clone@1.3.1': {} + + acorn@8.16.0: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.2 + + argparse@2.0.1: {} + + aria-query@5.3.1: {} + + aria-query@5.3.2: {} + + array-iterate@2.0.1: {} + + astro@6.3.1(rollup@4.60.3)(sass-embedded@1.99.0)(sass@1.99.0): + dependencies: + '@astrojs/compiler': 4.0.0 + '@astrojs/internal-helpers': 0.9.0 + '@astrojs/markdown-remark': 7.1.1 + '@astrojs/telemetry': 3.3.2 + '@capsizecss/unpack': 4.0.0 + '@clack/prompts': 1.3.0 + '@oslojs/encoding': 1.1.0 + '@rollup/pluginutils': 5.3.0(rollup@4.60.3) + aria-query: 5.3.2 + axobject-query: 4.1.0 + ci-info: 4.4.0 + clsx: 2.1.1 + common-ancestor-path: 2.0.0 + cookie: 1.1.1 + devalue: 5.8.0 + diff: 8.0.4 + dset: 3.1.4 + es-module-lexer: 2.1.0 + esbuild: 0.27.7 + flattie: 1.1.1 + fontace: 0.4.1 + get-tsconfig: 5.0.0-beta.4 + github-slugger: 2.0.0 + html-escaper: 3.0.3 + http-cache-semantics: 4.2.0 + js-yaml: 4.1.1 + jsonc-parser: 3.3.1 + magic-string: 0.30.21 + magicast: 0.5.2 + mrmime: 2.0.1 + neotraverse: 0.6.18 + obug: 2.1.1 + p-limit: 7.3.0 + p-queue: 9.2.0 + package-manager-detector: 1.6.0 + piccolore: 0.1.3 + picomatch: 4.0.4 + rehype: 13.0.2 + semver: 7.7.4 + shiki: 4.0.2 + smol-toml: 1.6.1 + svgo: 4.0.1 + tinyclip: 0.1.12 + tinyexec: 1.1.2 + tinyglobby: 0.2.16 + ultrahtml: 1.6.0 + unifont: 0.7.4 + unist-util-visit: 5.1.0 + unstorage: 1.17.5 + vfile: 6.0.3 + vite: 7.3.3(sass-embedded@1.99.0)(sass@1.99.0) + vitefu: 1.1.3(vite@7.3.3(sass-embedded@1.99.0)(sass@1.99.0)) + xxhash-wasm: 1.1.0 + yargs-parser: 22.0.0 + zod: 4.4.3 + optionalDependencies: + sharp: 0.34.5 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@types/node' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - db0 + - idb-keyval + - ioredis + - jiti + - less + - lightningcss + - rollup + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - uploadthing + - yaml + + axobject-query@4.1.0: {} + + bail@2.0.2: {} + + boolbase@1.0.0: {} + + ccount@2.0.1: {} + + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + + character-entities@2.0.2: {} + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + optional: true + + chokidar@5.0.0: + dependencies: + readdirp: 5.0.0 + + ci-info@4.4.0: {} + + clsx@2.1.1: {} + + colorjs.io@0.5.2: {} + + comma-separated-tokens@2.0.3: {} + + commander@11.1.0: {} + + common-ancestor-path@2.0.0: {} + + cookie-es@1.2.3: {} + + cookie@1.1.1: {} + + crossws@0.3.5: + dependencies: + uncrypto: 0.1.3 + + css-select@5.2.2: + dependencies: + boolbase: 1.0.0 + css-what: 6.2.2 + domhandler: 5.0.3 + domutils: 3.2.2 + nth-check: 2.1.1 + + css-tree@2.2.1: + dependencies: + mdn-data: 2.0.28 + source-map-js: 1.2.1 + + css-tree@3.2.1: + dependencies: + mdn-data: 2.27.1 + source-map-js: 1.2.1 + + css-what@6.2.2: {} + + csso@5.0.5: + dependencies: + css-tree: 2.2.1 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decode-named-character-reference@1.3.0: + dependencies: + character-entities: 2.0.2 + + dedent-js@1.0.1: {} + + deepmerge@4.3.1: {} + + defu@6.1.7: {} + + dequal@2.0.3: {} + + destr@2.0.5: {} + + detect-libc@2.1.2: + optional: true + + devalue@5.8.0: {} + + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + + diff@8.0.4: {} + + dom-serializer@2.0.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + + domelementtype@2.3.0: {} + + domhandler@5.0.3: + dependencies: + domelementtype: 2.3.0 + + domutils@3.2.2: + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + + dset@3.1.4: {} + + entities@4.5.0: {} + + entities@6.0.1: {} + + es-module-lexer@2.1.0: {} + + esbuild@0.27.7: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.7 + '@esbuild/android-arm': 0.27.7 + '@esbuild/android-arm64': 0.27.7 + '@esbuild/android-x64': 0.27.7 + '@esbuild/darwin-arm64': 0.27.7 + '@esbuild/darwin-x64': 0.27.7 + '@esbuild/freebsd-arm64': 0.27.7 + '@esbuild/freebsd-x64': 0.27.7 + '@esbuild/linux-arm': 0.27.7 + '@esbuild/linux-arm64': 0.27.7 + '@esbuild/linux-ia32': 0.27.7 + '@esbuild/linux-loong64': 0.27.7 + '@esbuild/linux-mips64el': 0.27.7 + '@esbuild/linux-ppc64': 0.27.7 + '@esbuild/linux-riscv64': 0.27.7 + '@esbuild/linux-s390x': 0.27.7 + '@esbuild/linux-x64': 0.27.7 + '@esbuild/netbsd-arm64': 0.27.7 + '@esbuild/netbsd-x64': 0.27.7 + '@esbuild/openbsd-arm64': 0.27.7 + '@esbuild/openbsd-x64': 0.27.7 + '@esbuild/openharmony-arm64': 0.27.7 + '@esbuild/sunos-x64': 0.27.7 + '@esbuild/win32-arm64': 0.27.7 + '@esbuild/win32-ia32': 0.27.7 + '@esbuild/win32-x64': 0.27.7 + + escape-string-regexp@5.0.0: {} + + esm-env@1.2.2: {} + + esrap@2.2.6: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + estree-walker@2.0.2: {} + + eventemitter3@5.0.4: {} + + extend@3.0.2: {} + + fast-string-truncated-width@3.0.3: {} + + fast-string-width@3.0.2: + dependencies: + fast-string-truncated-width: 3.0.3 + + fast-wrap-ansi@0.2.0: + dependencies: + fast-string-width: 3.0.2 + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + + flattie@1.1.1: {} + + fontace@0.4.1: + dependencies: + fontkitten: 1.0.3 + + fontkitten@1.0.3: + dependencies: + tiny-inflate: 1.0.3 + + fsevents@2.3.3: + optional: true + + get-tsconfig@5.0.0-beta.4: + dependencies: + resolve-pkg-maps: 1.0.0 + + github-slugger@2.0.0: {} + + h3@1.15.11: + dependencies: + cookie-es: 1.2.3 + crossws: 0.3.5 + defu: 6.1.7 + destr: 2.0.5 + iron-webcrypto: 1.2.1 + node-mock-http: 1.0.4 + radix3: 1.1.2 + ufo: 1.6.4 + uncrypto: 0.1.3 + + has-flag@4.0.0: {} + + hast-util-from-html@2.0.3: + dependencies: + '@types/hast': 3.0.4 + devlop: 1.1.0 + hast-util-from-parse5: 8.0.3 + parse5: 7.3.0 + vfile: 6.0.3 + vfile-message: 4.0.3 + + hast-util-from-parse5@8.0.3: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + devlop: 1.1.0 + hastscript: 9.0.1 + property-information: 7.1.0 + vfile: 6.0.3 + vfile-location: 5.0.3 + web-namespaces: 2.0.1 + + hast-util-is-element@3.0.0: + dependencies: + '@types/hast': 3.0.4 + + hast-util-parse-selector@4.0.0: + dependencies: + '@types/hast': 3.0.4 + + hast-util-raw@9.1.0: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + '@ungap/structured-clone': 1.3.1 + hast-util-from-parse5: 8.0.3 + hast-util-to-parse5: 8.0.1 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.1 + parse5: 7.3.0 + unist-util-position: 5.0.0 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + web-namespaces: 2.0.1 + zwitch: 2.0.4 + + hast-util-to-html@9.0.5: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + comma-separated-tokens: 2.0.3 + hast-util-whitespace: 3.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.1 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + stringify-entities: 4.0.4 + zwitch: 2.0.4 + + hast-util-to-parse5@8.0.1: + dependencies: + '@types/hast': 3.0.4 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + web-namespaces: 2.0.1 + zwitch: 2.0.4 + + hast-util-to-text@4.0.2: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + hast-util-is-element: 3.0.0 + unist-util-find-after: 5.0.0 + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.4 + + hastscript@9.0.1: + dependencies: + '@types/hast': 3.0.4 + comma-separated-tokens: 2.0.3 + hast-util-parse-selector: 4.0.0 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + + html-escaper@3.0.3: {} + + html-void-elements@3.0.0: {} + + http-cache-semantics@4.2.0: {} + + immutable@5.1.5: {} + + iron-webcrypto@1.2.1: {} + + is-docker@3.0.0: {} + + is-docker@4.0.0: {} + + is-extglob@2.1.1: + optional: true + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + optional: true + + is-inside-container@1.0.0: + dependencies: + is-docker: 3.0.0 + + is-plain-obj@4.1.0: {} + + is-reference@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + is-wsl@3.1.1: + dependencies: + is-inside-container: 1.0.0 + + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + jsonc-parser@3.3.1: {} + + locate-character@3.0.0: {} + + longest-streak@3.1.0: {} + + lru-cache@11.3.6: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + magicast@0.5.2: + dependencies: + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 + source-map-js: 1.2.1 + + markdown-table@3.0.4: {} + + mdast-util-definitions@6.0.0: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + unist-util-visit: 5.1.0 + + mdast-util-find-and-replace@3.0.2: + dependencies: + '@types/mdast': 4.0.4 + escape-string-regexp: 5.0.0 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + + mdast-util-from-markdown@2.0.3: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + mdast-util-to-string: 4.0.0 + micromark: 4.0.2 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-decode-string: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-stringify-position: 4.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-autolink-literal@2.0.1: + dependencies: + '@types/mdast': 4.0.4 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-find-and-replace: 3.0.2 + micromark-util-character: 2.1.1 + + mdast-util-gfm-footnote@2.1.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + micromark-util-normalize-identifier: 2.0.1 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-strikethrough@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-table@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + markdown-table: 3.0.4 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-task-list-item@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm@3.1.0: + dependencies: + mdast-util-from-markdown: 2.0.3 + mdast-util-gfm-autolink-literal: 2.0.1 + mdast-util-gfm-footnote: 2.1.0 + mdast-util-gfm-strikethrough: 2.0.0 + mdast-util-gfm-table: 2.0.0 + mdast-util-gfm-task-list-item: 2.0.0 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-phrasing@4.1.0: + dependencies: + '@types/mdast': 4.0.4 + unist-util-is: 6.0.1 + + mdast-util-to-hast@13.2.1: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.3.1 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + + mdast-util-to-markdown@2.1.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + longest-streak: 3.1.0 + mdast-util-phrasing: 4.1.0 + mdast-util-to-string: 4.0.0 + micromark-util-classify-character: 2.0.1 + micromark-util-decode-string: 2.0.1 + unist-util-visit: 5.1.0 + zwitch: 2.0.4 + + mdast-util-to-string@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + + mdn-data@2.0.28: {} + + mdn-data@2.27.1: {} + + micromark-core-commonmark@2.0.3: + dependencies: + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-factory-destination: 2.0.1 + micromark-factory-label: 2.0.1 + micromark-factory-space: 2.0.1 + micromark-factory-title: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-html-tag-name: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-autolink-literal@2.1.0: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-footnote@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-strikethrough@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-table@2.1.1: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-tagfilter@2.0.0: + dependencies: + micromark-util-types: 2.0.2 + + micromark-extension-gfm-task-list-item@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm@3.0.0: + dependencies: + micromark-extension-gfm-autolink-literal: 2.1.0 + micromark-extension-gfm-footnote: 2.1.0 + micromark-extension-gfm-strikethrough: 2.1.0 + micromark-extension-gfm-table: 2.1.1 + micromark-extension-gfm-tagfilter: 2.0.0 + micromark-extension-gfm-task-list-item: 2.1.0 + micromark-util-combine-extensions: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-destination@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-label@2.0.1: + dependencies: + devlop: 1.1.0 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-space@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-types: 2.0.2 + + micromark-factory-title@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-whitespace@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-chunked@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-classify-character@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-combine-extensions@2.0.1: + dependencies: + micromark-util-chunked: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-decode-numeric-character-reference@2.0.2: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-decode-string@2.0.1: + dependencies: + decode-named-character-reference: 1.3.0 + micromark-util-character: 2.1.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-symbol: 2.0.1 + + micromark-util-encode@2.0.1: {} + + micromark-util-html-tag-name@2.0.1: {} + + micromark-util-normalize-identifier@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-resolve-all@2.0.1: + dependencies: + micromark-util-types: 2.0.2 + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-subtokenize@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.2: {} + + micromark@4.0.2: + dependencies: + '@types/debug': 4.1.13 + debug: 4.4.3 + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-combine-extensions: 2.0.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-encode: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + transitivePeerDependencies: + - supports-color + + mrmime@2.0.1: {} + + ms@2.1.3: {} + + nanoid@3.3.12: {} + + neotraverse@0.6.18: {} + + nlcst-to-string@4.0.0: + dependencies: + '@types/nlcst': 2.0.3 + + node-addon-api@7.1.1: + optional: true + + node-fetch-native@1.6.7: {} + + node-mock-http@1.0.4: {} + + normalize-path@3.0.0: {} + + nth-check@2.1.1: + dependencies: + boolbase: 1.0.0 + + obug@2.1.1: {} + + ofetch@1.5.1: + dependencies: + destr: 2.0.5 + node-fetch-native: 1.6.7 + ufo: 1.6.4 + + ohash@2.0.11: {} + + oniguruma-parser@0.12.2: {} + + oniguruma-to-es@4.3.6: + dependencies: + oniguruma-parser: 0.12.2 + regex: 6.1.0 + regex-recursion: 6.0.2 + + p-limit@7.3.0: + dependencies: + yocto-queue: 1.2.2 + + p-queue@9.2.0: + dependencies: + eventemitter3: 5.0.4 + p-timeout: 7.0.1 + + p-timeout@7.0.1: {} + + package-manager-detector@1.6.0: {} + + parse-latin@7.0.0: + dependencies: + '@types/nlcst': 2.0.3 + '@types/unist': 3.0.3 + nlcst-to-string: 4.0.0 + unist-util-modify-children: 4.0.0 + unist-util-visit-children: 3.0.0 + vfile: 6.0.3 + + parse5@7.3.0: + dependencies: + entities: 6.0.1 + + piccolore@0.1.3: {} + + picocolors@1.1.1: {} + + picomatch@2.3.2: {} + + picomatch@4.0.4: {} + + postcss@8.5.14: + dependencies: + nanoid: 3.3.12 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prismjs@1.30.0: {} + + property-information@7.1.0: {} + + radix3@1.1.2: {} + + readdirp@4.1.2: + optional: true + + readdirp@5.0.0: {} + + regex-recursion@6.0.2: + dependencies: + regex-utilities: 2.3.0 + + regex-utilities@2.3.0: {} + + regex@6.1.0: + dependencies: + regex-utilities: 2.3.0 + + rehype-parse@9.0.1: + dependencies: + '@types/hast': 3.0.4 + hast-util-from-html: 2.0.3 + unified: 11.0.5 + + rehype-raw@7.0.0: + dependencies: + '@types/hast': 3.0.4 + hast-util-raw: 9.1.0 + vfile: 6.0.3 + + rehype-stringify@10.0.1: + dependencies: + '@types/hast': 3.0.4 + hast-util-to-html: 9.0.5 + unified: 11.0.5 + + rehype@13.0.2: + dependencies: + '@types/hast': 3.0.4 + rehype-parse: 9.0.1 + rehype-stringify: 10.0.1 + unified: 11.0.5 + + remark-gfm@4.0.1: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-gfm: 3.1.0 + micromark-extension-gfm: 3.0.0 + remark-parse: 11.0.0 + remark-stringify: 11.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-parse@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.3 + micromark-util-types: 2.0.2 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-rehype@11.1.2: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + mdast-util-to-hast: 13.2.1 + unified: 11.0.5 + vfile: 6.0.3 + + remark-smartypants@3.0.2: + dependencies: + retext: 9.0.0 + retext-smartypants: 6.2.0 + unified: 11.0.5 + unist-util-visit: 5.1.0 + + remark-stringify@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-to-markdown: 2.1.2 + unified: 11.0.5 + + resolve-pkg-maps@1.0.0: {} + + retext-latin@4.0.0: + dependencies: + '@types/nlcst': 2.0.3 + parse-latin: 7.0.0 + unified: 11.0.5 + + retext-smartypants@6.2.0: + dependencies: + '@types/nlcst': 2.0.3 + nlcst-to-string: 4.0.0 + unist-util-visit: 5.1.0 + + retext-stringify@4.0.0: + dependencies: + '@types/nlcst': 2.0.3 + nlcst-to-string: 4.0.0 + unified: 11.0.5 + + retext@9.0.0: + dependencies: + '@types/nlcst': 2.0.3 + retext-latin: 4.0.0 + retext-stringify: 4.0.0 + unified: 11.0.5 + + rollup@4.60.3: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.60.3 + '@rollup/rollup-android-arm64': 4.60.3 + '@rollup/rollup-darwin-arm64': 4.60.3 + '@rollup/rollup-darwin-x64': 4.60.3 + '@rollup/rollup-freebsd-arm64': 4.60.3 + '@rollup/rollup-freebsd-x64': 4.60.3 + '@rollup/rollup-linux-arm-gnueabihf': 4.60.3 + '@rollup/rollup-linux-arm-musleabihf': 4.60.3 + '@rollup/rollup-linux-arm64-gnu': 4.60.3 + '@rollup/rollup-linux-arm64-musl': 4.60.3 + '@rollup/rollup-linux-loong64-gnu': 4.60.3 + '@rollup/rollup-linux-loong64-musl': 4.60.3 + '@rollup/rollup-linux-ppc64-gnu': 4.60.3 + '@rollup/rollup-linux-ppc64-musl': 4.60.3 + '@rollup/rollup-linux-riscv64-gnu': 4.60.3 + '@rollup/rollup-linux-riscv64-musl': 4.60.3 + '@rollup/rollup-linux-s390x-gnu': 4.60.3 + '@rollup/rollup-linux-x64-gnu': 4.60.3 + '@rollup/rollup-linux-x64-musl': 4.60.3 + '@rollup/rollup-openbsd-x64': 4.60.3 + '@rollup/rollup-openharmony-arm64': 4.60.3 + '@rollup/rollup-win32-arm64-msvc': 4.60.3 + '@rollup/rollup-win32-ia32-msvc': 4.60.3 + '@rollup/rollup-win32-x64-gnu': 4.60.3 + '@rollup/rollup-win32-x64-msvc': 4.60.3 + fsevents: 2.3.3 + + rxjs@7.8.2: + dependencies: + tslib: 2.8.1 + + sass-embedded-all-unknown@1.99.0: + dependencies: + sass: 1.99.0 + optional: true + + sass-embedded-android-arm64@1.99.0: + optional: true + + sass-embedded-android-arm@1.99.0: + optional: true + + sass-embedded-android-riscv64@1.99.0: + optional: true + + sass-embedded-android-x64@1.99.0: + optional: true + + sass-embedded-darwin-arm64@1.99.0: + optional: true + + sass-embedded-darwin-x64@1.99.0: + optional: true + + sass-embedded-linux-arm64@1.99.0: + optional: true + + sass-embedded-linux-arm@1.99.0: + optional: true + + sass-embedded-linux-musl-arm64@1.99.0: + optional: true + + sass-embedded-linux-musl-arm@1.99.0: + optional: true + + sass-embedded-linux-musl-riscv64@1.99.0: + optional: true + + sass-embedded-linux-musl-x64@1.99.0: + optional: true + + sass-embedded-linux-riscv64@1.99.0: + optional: true + + sass-embedded-linux-x64@1.99.0: + optional: true + + sass-embedded-unknown-all@1.99.0: + dependencies: + sass: 1.99.0 + optional: true + + sass-embedded-win32-arm64@1.99.0: + optional: true + + sass-embedded-win32-x64@1.99.0: + optional: true + + sass-embedded@1.99.0: + dependencies: + '@bufbuild/protobuf': 2.12.0 + colorjs.io: 0.5.2 + immutable: 5.1.5 + rxjs: 7.8.2 + supports-color: 8.1.1 + sync-child-process: 1.0.2 + varint: 6.0.0 + optionalDependencies: + sass-embedded-all-unknown: 1.99.0 + sass-embedded-android-arm: 1.99.0 + sass-embedded-android-arm64: 1.99.0 + sass-embedded-android-riscv64: 1.99.0 + sass-embedded-android-x64: 1.99.0 + sass-embedded-darwin-arm64: 1.99.0 + sass-embedded-darwin-x64: 1.99.0 + sass-embedded-linux-arm: 1.99.0 + sass-embedded-linux-arm64: 1.99.0 + sass-embedded-linux-musl-arm: 1.99.0 + sass-embedded-linux-musl-arm64: 1.99.0 + sass-embedded-linux-musl-riscv64: 1.99.0 + sass-embedded-linux-musl-x64: 1.99.0 + sass-embedded-linux-riscv64: 1.99.0 + sass-embedded-linux-x64: 1.99.0 + sass-embedded-unknown-all: 1.99.0 + sass-embedded-win32-arm64: 1.99.0 + sass-embedded-win32-x64: 1.99.0 + + sass@1.99.0: + dependencies: + chokidar: 4.0.3 + immutable: 5.1.5 + source-map-js: 1.2.1 + optionalDependencies: + '@parcel/watcher': 2.5.6 + optional: true + + sax@1.6.0: {} + + scule@1.3.0: {} + + semver@7.7.4: {} + + sharp@0.34.5: + dependencies: + '@img/colour': 1.1.0 + detect-libc: 2.1.2 + semver: 7.7.4 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.34.5 + '@img/sharp-darwin-x64': 0.34.5 + '@img/sharp-libvips-darwin-arm64': 1.2.4 + '@img/sharp-libvips-darwin-x64': 1.2.4 + '@img/sharp-libvips-linux-arm': 1.2.4 + '@img/sharp-libvips-linux-arm64': 1.2.4 + '@img/sharp-libvips-linux-ppc64': 1.2.4 + '@img/sharp-libvips-linux-riscv64': 1.2.4 + '@img/sharp-libvips-linux-s390x': 1.2.4 + '@img/sharp-libvips-linux-x64': 1.2.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + '@img/sharp-linux-arm': 0.34.5 + '@img/sharp-linux-arm64': 0.34.5 + '@img/sharp-linux-ppc64': 0.34.5 + '@img/sharp-linux-riscv64': 0.34.5 + '@img/sharp-linux-s390x': 0.34.5 + '@img/sharp-linux-x64': 0.34.5 + '@img/sharp-linuxmusl-arm64': 0.34.5 + '@img/sharp-linuxmusl-x64': 0.34.5 + '@img/sharp-wasm32': 0.34.5 + '@img/sharp-win32-arm64': 0.34.5 + '@img/sharp-win32-ia32': 0.34.5 + '@img/sharp-win32-x64': 0.34.5 + optional: true + + shiki@4.0.2: + dependencies: + '@shikijs/core': 4.0.2 + '@shikijs/engine-javascript': 4.0.2 + '@shikijs/engine-oniguruma': 4.0.2 + '@shikijs/langs': 4.0.2 + '@shikijs/themes': 4.0.2 + '@shikijs/types': 4.0.2 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + + sisteransi@1.0.5: {} + + smol-toml@1.6.1: {} + + source-map-js@1.2.1: {} + + space-separated-tokens@2.0.2: {} + + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + + svelte2tsx@0.7.55(svelte@5.55.5)(typescript@5.9.3): + dependencies: + dedent-js: 1.0.1 + scule: 1.3.0 + svelte: 5.55.5 + typescript: 5.9.3 + + svelte@5.55.5: + dependencies: + '@jridgewell/remapping': 2.3.5 + '@jridgewell/sourcemap-codec': 1.5.5 + '@sveltejs/acorn-typescript': 1.0.9(acorn@8.16.0) + '@types/estree': 1.0.9 + '@types/trusted-types': 2.0.7 + acorn: 8.16.0 + aria-query: 5.3.1 + axobject-query: 4.1.0 + clsx: 2.1.1 + devalue: 5.8.0 + esm-env: 1.2.2 + esrap: 2.2.6 + is-reference: 3.0.3 + locate-character: 3.0.0 + magic-string: 0.30.21 + zimmerframe: 1.1.4 + transitivePeerDependencies: + - '@typescript-eslint/types' + + svgo@4.0.1: + dependencies: + commander: 11.1.0 + css-select: 5.2.2 + css-tree: 3.2.1 + css-what: 6.2.2 + csso: 5.0.5 + picocolors: 1.1.1 + sax: 1.6.0 + + sync-child-process@1.0.2: + dependencies: + sync-message-port: 1.2.0 + + sync-message-port@1.2.0: {} + + tiny-inflate@1.0.3: {} + + tinyclip@0.1.12: {} + + tinyexec@1.1.2: {} + + tinyglobby@0.2.16: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + trim-lines@3.0.1: {} + + trough@2.2.0: {} + + tslib@2.8.1: {} + + typescript@5.9.3: {} + + ufo@1.6.4: {} + + ultrahtml@1.6.0: {} + + uncrypto@0.1.3: {} + + unified@11.0.5: + dependencies: + '@types/unist': 3.0.3 + bail: 2.0.2 + devlop: 1.1.0 + extend: 3.0.2 + is-plain-obj: 4.1.0 + trough: 2.2.0 + vfile: 6.0.3 + + unifont@0.7.4: + dependencies: + css-tree: 3.2.1 + ofetch: 1.5.1 + ohash: 2.0.11 + + unist-util-find-after@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + + unist-util-is@6.0.1: + dependencies: + '@types/unist': 3.0.3 + + unist-util-modify-children@4.0.0: + dependencies: + '@types/unist': 3.0.3 + array-iterate: 2.0.1 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-remove-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-visit: 5.1.0 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-children@3.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + + unist-util-visit@5.1.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + + unstorage@1.17.5: + dependencies: + anymatch: 3.1.3 + chokidar: 5.0.0 + destr: 2.0.5 + h3: 1.15.11 + lru-cache: 11.3.6 + node-fetch-native: 1.6.7 + ofetch: 1.5.1 + ufo: 1.6.4 + + varint@6.0.0: {} + + vfile-location@5.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile: 6.0.3 + + vfile-message@4.0.3: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.3 + + vite@7.3.3(sass-embedded@1.99.0)(sass@1.99.0): + dependencies: + esbuild: 0.27.7 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + postcss: 8.5.14 + rollup: 4.60.3 + tinyglobby: 0.2.16 + optionalDependencies: + fsevents: 2.3.3 + sass: 1.99.0 + sass-embedded: 1.99.0 + + vitefu@1.1.3(vite@7.3.3(sass-embedded@1.99.0)(sass@1.99.0)): + optionalDependencies: + vite: 7.3.3(sass-embedded@1.99.0)(sass@1.99.0) + + web-namespaces@2.0.1: {} + + which-pm-runs@1.1.0: {} + + xxhash-wasm@1.1.0: {} + + yargs-parser@22.0.0: {} + + yocto-queue@1.2.2: {} + + zimmerframe@1.1.4: {} + + zod@4.4.3: {} + + zwitch@2.0.4: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..dbb26c8 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +allowBuilds: + esbuild: true + sharp: true diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..7f48a94 Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 0000000..f157bd1 --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1,9 @@ + + + + diff --git a/src/assets/Photo-3468.jpg b/src/assets/Photo-3468.jpg new file mode 100755 index 0000000..7fcc076 Binary files /dev/null and b/src/assets/Photo-3468.jpg differ diff --git a/src/assets/Photo-3471.jpg b/src/assets/Photo-3471.jpg new file mode 100755 index 0000000..e421737 Binary files /dev/null and b/src/assets/Photo-3471.jpg differ diff --git a/src/assets/Photo-3515.jpg b/src/assets/Photo-3515.jpg new file mode 100755 index 0000000..5aab587 Binary files /dev/null and b/src/assets/Photo-3515.jpg differ diff --git a/src/assets/Photo-3585.jpg b/src/assets/Photo-3585.jpg new file mode 100755 index 0000000..43b35cc Binary files /dev/null and b/src/assets/Photo-3585.jpg differ diff --git a/src/components/@types/global.ts b/src/components/@types/global.ts new file mode 100644 index 0000000..9aaee2e --- /dev/null +++ b/src/components/@types/global.ts @@ -0,0 +1,128 @@ +import type { Component } from 'svelte'; +import type { TransitionOptions } from '../ScrollerVideo/ts/ScrollerVideo.js'; +import type { ScrollerVideoState } from '../ScrollerVideo/ts/state.svelte.js'; +/** + * Used for the list of + {/each} + + + + diff --git a/src/components/Framer/Framer.mdx b/src/components/Framer/Framer.mdx new file mode 100644 index 0000000..2d0b8a2 --- /dev/null +++ b/src/components/Framer/Framer.mdx @@ -0,0 +1,19 @@ +import { Meta } from '@storybook/blocks'; + +import * as FramerStories from './Framer.stories.svelte'; + + + +# FeaturePhoto + +An embed tool for development in the graphics kit. + +```svelte + + + +``` diff --git a/src/components/Framer/Framer.stories.svelte b/src/components/Framer/Framer.stories.svelte new file mode 100644 index 0000000..fa0a68d --- /dev/null +++ b/src/components/Framer/Framer.stories.svelte @@ -0,0 +1,19 @@ + + + diff --git a/src/components/Framer/Framer.svelte b/src/components/Framer/Framer.svelte new file mode 100644 index 0000000..4c7937e --- /dev/null +++ b/src/components/Framer/Framer.svelte @@ -0,0 +1,225 @@ + + +
+
+ +
+ + {#if embeds.length === 0} +
+

No embeds to show.

+
+ {:else} + {#if searchType === 'typeahead'} +
+ + embedTitles[d.index]} + data={embeds.map((embed, index) => ({ index, embed }))} + showDropdownOnFocus={true} + onselect={(detail) => { + if (typeof window !== 'undefined') { + window.localStorage.setItem( + 'framer-active-embed', + detail.original.embed + ); + } + activeEmbed = detail.original.embed; + // activeEmbedIndex = detail.original.index; + }} + /> +
+ {:else} + + {/if} + +
+

Preview

+
+
+ {/if} +
+ + + +{#if embeds.length > 0} + +{/if} + + diff --git a/src/components/Framer/Resizer/index.svelte b/src/components/Framer/Resizer/index.svelte new file mode 100644 index 0000000..e406ed9 --- /dev/null +++ b/src/components/Framer/Resizer/index.svelte @@ -0,0 +1,243 @@ + + + + +
+
+
+ {pixelLabel || $width}px +
+ +
+
+
+
+ +
+
+ + diff --git a/src/components/Framer/Typeahead/Search.svelte b/src/components/Framer/Typeahead/Search.svelte new file mode 100644 index 0000000..9e8566f --- /dev/null +++ b/src/components/Framer/Typeahead/Search.svelte @@ -0,0 +1,101 @@ + + +
{ + e.preventDefault(); + }} +> + + +
diff --git a/src/components/Framer/Typeahead/fuzzy.ts b/src/components/Framer/Typeahead/fuzzy.ts new file mode 100644 index 0000000..d04abfa --- /dev/null +++ b/src/components/Framer/Typeahead/fuzzy.ts @@ -0,0 +1,108 @@ +interface MatchOptions { + pre?: string; + post?: string; + caseSensitive?: boolean; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + extract?: (arg: any) => string; +} + +interface MatchResult { + rendered: string; + score: number; +} + +interface FilterResult { + string: string; + score: number; + index: number; + original: T; +} + +const fuzzy = { + simpleFilter(pattern: string, array: string[]): string[] { + return array.filter((str) => this.test(pattern, str)); + }, + + test(pattern: string, str: string): boolean { + return this.match(pattern, str) !== null; + }, + + match( + pattern: string, + str: string, + opts: MatchOptions = {} + ): MatchResult | null { + let patternIdx = 0; + const result: string[] = []; + const len = str.length; + let totalScore = 0; + let currScore = 0; + const pre = opts.pre || ''; + const post = opts.post || ''; + const compareString = opts.caseSensitive ? str : str.toLowerCase(); + pattern = opts.caseSensitive ? pattern : pattern.toLowerCase(); + + for (let idx = 0; idx < len; idx++) { + let ch = str[idx]; + if (compareString[idx] === pattern[patternIdx]) { + ch = pre + ch + post; + patternIdx += 1; + currScore += 1 + currScore; + } else { + currScore = 0; + } + totalScore += currScore; + result[result.length] = ch; + } + + if (patternIdx === pattern.length) { + totalScore = compareString === pattern ? Infinity : totalScore; + return { rendered: result.join(''), score: totalScore }; + } + + return null; + }, + + filter( + pattern: string, + arr: T[], + opts: MatchOptions = {} + ): FilterResult[] { + if (!arr || arr.length === 0) { + return []; + } + if (typeof pattern !== 'string') { + return arr.map((element, index) => ({ + string: element as unknown as string, + score: 0, + index, + original: element, + })); + } + + return arr + .reduce[]>((prev, element, idx) => { + let str = element as unknown as string; + if (opts.extract) { + str = opts.extract(element); + } + const rendered = this.match(pattern, str, opts); + if (rendered != null) { + prev.push({ + string: rendered.rendered, + score: rendered.score, + index: idx, + original: element, + }); + } + return prev; + }, []) + .sort((a, b) => { + const compare = b.score - a.score; + if (compare) return compare; + return a.index - b.index; + }); + }, +}; + +export default fuzzy; diff --git a/src/components/Framer/Typeahead/index.svelte b/src/components/Framer/Typeahead/index.svelte new file mode 100644 index 0000000..798f219 --- /dev/null +++ b/src/components/Framer/Typeahead/index.svelte @@ -0,0 +1,375 @@ + + + { + if (!hideDropdown && !comboboxRef?.contains(target as Node)) { + close(); + } + }} +/> + +
0} + aria-controls="{id}-listbox" + aria-expanded={showResults || + (isFocused && value.length > 0 && results.length === 0)} + id="{id}-typeahead" +> + = 0 && !hideDropdown && results.length > 0 + ) ? + `${id}-result-${selectedIndex}` + : null} + onfocus={() => { + open(); + if (showDropdownOnFocus) { + showResults = true; + isFocused = true; + } + }} + onclear={open} + onkeydown={(e: KeyboardEvent) => { + if (results.length === 0) return; + + switch (e.key) { + case 'Enter': + select(); + break; + case 'ArrowDown': + e.preventDefault(); + change(1); + break; + case 'ArrowUp': + e.preventDefault(); + change(-1); + break; + case 'Escape': + e.preventDefault(); + value = ''; + searchRef?.focus(); + close(); + break; + } + }} + {...restProps} + /> +
    + {#if showResults && !hideDropdown} + {#each results as result, index} +
  • { + if (result.disabled) return; + selectedIndex = index; + select(); + }} + onkeyup={(e) => { + if (e.key !== 'Enter') return; + if (result.disabled) return; + selectedIndex = index; + select(); + }} + onmouseenter={() => { + if (result.disabled) return; + selectedIndex = index; + }} + > + {@html result.string} +
  • + {/each} + {/if} + {#if value.length > 0 && results.length === 0} +
  • No embeds found...
  • + {/if} +
+
+ + diff --git a/src/components/Framer/stores.ts b/src/components/Framer/stores.ts new file mode 100644 index 0000000..69e7100 --- /dev/null +++ b/src/components/Framer/stores.ts @@ -0,0 +1,3 @@ +import { writable } from 'svelte/store'; + +export const width = writable(660); diff --git a/src/components/Framer/uniqNames.ts b/src/components/Framer/uniqNames.ts new file mode 100644 index 0000000..010ab3d --- /dev/null +++ b/src/components/Framer/uniqNames.ts @@ -0,0 +1,54 @@ +export default (embeds: string[]) => { + const nakedEmbeds = embeds + .map((e) => e.replace(/\?.+$/, '')) + .map((e) => e.replace(/index\.html$/, '')) + .map((e) => e.replace(/^http[s]*:\/\/[\w.]+\.com/, '')); + + // If just one, get the last path part + if (nakedEmbeds.length === 1) { + return [ + nakedEmbeds[0] + .split('/') + .filter((d) => d) + .slice(-1)[0], + ]; + } + + // If many, test each path part for unique-ness + const test = nakedEmbeds[0]; + let replacementForward = 0; + for (const i in test.split('/')) { + const pathPart = test.split('/')[i]; + const notUniq = nakedEmbeds.every((e) => e.split('/')[i] === pathPart); + if (notUniq) { + replacementForward += 1; + } else { + break; + } + } + + if (replacementForward === test.split('/').length) return nakedEmbeds; + + let replacementBackward = 0; + for (const i in test.split('/').reverse()) { + const pathPart = test.split('/').reverse()[i]; + const notUniq = nakedEmbeds.every( + (e) => e.split('/').reverse()[i] === pathPart + ); + if (notUniq) { + replacementBackward += 1; + } else { + break; + } + } + + return nakedEmbeds.map((e) => { + if (replacementBackward > 0) { + return e + .split('/') + .slice(replacementForward, replacementBackward * -1) + .join('/'); + } + return e.split('/').slice(replacementForward).join('/'); + }); +}; diff --git a/src/components/Geocoder/Geocoder.mdx b/src/components/Geocoder/Geocoder.mdx new file mode 100644 index 0000000..fdfa2b9 --- /dev/null +++ b/src/components/Geocoder/Geocoder.mdx @@ -0,0 +1,27 @@ +import { Meta, Canvas } from '@storybook/blocks'; + +import * as GeocoderStories from './Geocoder.stories.svelte'; + + + +# Geocoder + +The `Geocoder` component provides an autocomplete location search powered by the [Mapbox Geocoding v6 API](https://docs.mapbox.com/api/search/geocoding-v6/). It returns coordinates and a place name via the `onselect` callback. + +```svelte + + + +``` + +The `accessToken` prop is required. Look up the Reuters Mapbox token in the team's 1Password vault. All [Mapbox forward geocoding options](https://docs.mapbox.com/api/search/geocoding-v6/#forward-geocoding) are available as props, including `country`, `language`, `types`, `bbox`, `proximity`, `limit`, and `worldview`. + + diff --git a/src/components/Geocoder/Geocoder.stories.svelte b/src/components/Geocoder/Geocoder.stories.svelte new file mode 100644 index 0000000..a42e330 --- /dev/null +++ b/src/components/Geocoder/Geocoder.stories.svelte @@ -0,0 +1,23 @@ + + + + + +
+ +
+
diff --git a/src/components/Geocoder/Geocoder.svelte b/src/components/Geocoder/Geocoder.svelte new file mode 100644 index 0000000..70d616f --- /dev/null +++ b/src/components/Geocoder/Geocoder.svelte @@ -0,0 +1,303 @@ + + + +
+
+ +
+ (focused = true)} + onblur={() => (focused = false)} + type="text" + autocapitalize="words" + autocomplete="off" + enterkeyhint="search" + spellcheck="false" + role="combobox" + aria-expanded={showDropdown} + aria-label={searchPlaceholder} + aria-controls={showDropdown ? listboxId : undefined} + aria-activedescendant={selectedIndex >= 0 ? + `${listboxId}-option-${selectedIndex}` + : undefined} + aria-autocomplete="list" + placeholder={searchPlaceholder} + class="geocoder-input" + /> + {#if active} + + {/if} +
+ {statusMessage} +
+ {#if showDropdown} +
    + {#each suggestions as suggestion, i} +
  • { + e.preventDefault(); + selectSuggestion(suggestion); + }} + onmouseenter={() => (selectedIndex = i)} + > + {suggestion.properties.full_address || suggestion.properties.name} +
  • + {/each} +
+ {/if} +
+ + diff --git a/src/components/Geocoder/geocode.ts b/src/components/Geocoder/geocode.ts new file mode 100644 index 0000000..145d898 --- /dev/null +++ b/src/components/Geocoder/geocode.ts @@ -0,0 +1,94 @@ +export type GeocodeFeatureType = + | 'country' + | 'region' + | 'postcode' + | 'district' + | 'place' + | 'locality' + | 'neighborhood' + | 'street' + | 'address'; + +export interface GeocodeOptions { + /** Mapbox public access token. */ + accessToken: string; + /** Return partial prefix matches (true) or exact matches only (false). Defaults to true. */ + autocomplete?: boolean; + /** Limit results to a bounding box: [minLon, minLat, maxLon, maxLat]. Cannot cross the 180th meridian. */ + bbox?: [number, number, number, number]; + /** Filter results to one or more countries using ISO 3166-1 alpha-2 codes. */ + country?: string[]; + /** IETF language tags for the response. Also influences result scoring. Max 20. */ + language?: string[]; + /** Maximum number of results to return (1–10). Defaults to 5. */ + limit?: number; + /** Bias results toward a location: [lon, lat] coordinates or 'ip' to use the request IP. */ + proximity?: [number, number] | 'ip'; + /** Filter results by feature type. */ + types?: GeocodeFeatureType[]; + /** Geopolitical worldview for boundary representation (e.g. 'us', 'cn', 'in'). Defaults to 'us'. */ + worldview?: string; + /** Set to true if results will be stored/cached permanently. Defaults to false. */ + permanent?: boolean; + /** Return building entrance data when available (public preview). Defaults to false. */ + entrances?: boolean; +} + +export interface GeocodeFeature { + type: 'Feature'; + properties: { + mapbox_id: string; + feature_type: GeocodeFeatureType; + name: string; + name_preferred?: string; + place_formatted?: string; + full_address?: string; + coordinates: { longitude: number; latitude: number }; + context: Record< + string, + { mapbox_id: string; name: string; [key: string]: unknown } + >; + }; + geometry: { + type: 'Point'; + coordinates: [number, number]; + }; +} + +const BASE_URL = 'https://api.mapbox.com/search/geocode/v6/forward'; + +export async function geocode( + query: string, + options: GeocodeOptions, + signal?: AbortSignal +): Promise { + const params = new URLSearchParams({ + q: query, + access_token: options.accessToken, + }); + + if (options.autocomplete !== undefined) + params.set('autocomplete', String(options.autocomplete)); + if (options.bbox) params.set('bbox', options.bbox.join(',')); + if (options.country) params.set('country', options.country.join(',')); + if (options.language) params.set('language', options.language.join(',')); + if (options.limit !== undefined) params.set('limit', String(options.limit)); + if (options.proximity) + params.set( + 'proximity', + Array.isArray(options.proximity) ? + options.proximity.join(',') + : options.proximity + ); + if (options.types) params.set('types', options.types.join(',')); + if (options.worldview) params.set('worldview', options.worldview); + if (options.permanent !== undefined) + params.set('permanent', String(options.permanent)); + if (options.entrances !== undefined) + params.set('entrances', String(options.entrances)); + + const res = await fetch(`${BASE_URL}?${params}`, { signal }); + if (!res.ok) throw new Error(`Geocode request failed: ${res.status}`); + const data = await res.json(); + return data.features ?? []; +} diff --git a/src/components/GraphicBlock/GraphicBlock.mdx b/src/components/GraphicBlock/GraphicBlock.mdx new file mode 100644 index 0000000..e5ff886 --- /dev/null +++ b/src/components/GraphicBlock/GraphicBlock.mdx @@ -0,0 +1,215 @@ +import { Meta, Canvas } from '@storybook/blocks'; + +import * as GraphicBlockStories from './GraphicBlock.stories.svelte'; + + + +# GraphicBlock + +The `GraphicBlock` component is a special derivative of the [Block](?path=/docs/components-page-layout-block--docs) component that wraps around your graphic. It also adds a title, description, notes and other text elements. + +Many other Reuters Graphics components use `GraphicBlock` to wrap graphics with styled text. + +```svelte + + + + +
+ +``` + + + +## Using with ai2svelte and ArchieML docs + +The `GraphicBlock` component is built to handle [ai2svelte](https://github.com/reuters-graphics/ai2svelte) graphics in graphics kit. + +You'll likely get your text value from an ArchieML doc... + +```yaml +# ArchieML doc +[blocks] +type: ai-graphic +width: normal +chart: AiMap # IMPORTANT: This must match the name of the ai2svelte chart you import in App.svelte +title: Earthquake in Haiti +description: The 7.2-magnitude earthquake struck at 8:29 a.m. EST, Aug. 14, 2021. +notes: \Note: A shakemap represents the ground shaking produced by an earthquake. + +\Source: USGIS +:end +altText: A map that shows the shake intensity of the earthquake, which was worst in central Haiti. +:end +[] +``` + +... which you'll parse out of a ArchieML block object before passing to the `GraphicBlock` component. + +To pass your ai2svelte graphic into `GraphicBlock` component, import your ai2svelte graphic at the top of `App.svelte` and add it to the `aiCharts` object. + +> **Important❗:** Make sure that the value for `chart` in the ArchieML doc matches the name of the ai2svelte file imported in `App.svelte`. + +```svelte + + + + +{#each content.blocks as block} + {#if block.type === 'ai-graphic'} + {#if !aiCharts[block.chart]} + + + {:else} + + {@const AiChart = aiCharts[block.chart]} + + + + + {/if} + {/if} +{/each} +``` + + + +## Custom text + +You can override the default styles for title and notes by making your own custom elements and passing them as `title` and `notes` [snippets](https://svelte.dev/docs/svelte/snippet) instead of as strings: + +```svelte + + + {#snippet title()} +
My smaller title
+ {/snippet} + + +
+ + + {#snippet notes()} + + {/snippet} +
+``` + + + +## ARIA descriptions + +If the text in your chart isn't easily read by screen readers — for example, a map with annotations that wouldn't make sense without the visual — add an `ariaDescription` that describes the chart. + +The `ariaDescription` string will be processed as markdown, so you can add multiple paragraphs, links, headers, etc. in markdown. + +> **Note:** When you set an `ariaDescription`, your graphic will be automatically wrapped in a div with [aria-hidden="true"](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-hidden), which tells screen readers to read the hidden ARIA description and skip the text in the graphic. + +```svelte + + + + +``` + + + +## Custom ARIA descriptions + +Sometimes, instead of a simple sentence, we want to provide a data table or something more complex as an ARIA description. To do this, pass the custom elements as an `ariaDescription` [snippet](https://svelte.dev/docs/svelte/snippet) instead of as a string, as in the [example above](?path=/docs/components-graphics-graphicblock--docs#aria-descriptions). + +[Read this](https://accessibility.psu.edu/images/charts/) for more information on using screen reader data tables for charts. + +> **Note:** The `customAria` snippet will override the `ariaDescription` and will also hide the text in your graphic from screen readers. + +```svelte + + + + + + {#snippet ariaDescription()} +

+ A shakemap shows the intensity of the 7.2-magnitude earthquake that struck + Haiti at 8:29 a.m. EST, Aug. 14, 2021. +

+ + + + + + + + + + + + + + + +
CityFelt shake strength
Les CayesVery strong
JeremieStrong
+ {/snippet} +
+ + + +``` + + diff --git a/src/components/GraphicBlock/GraphicBlock.stories.svelte b/src/components/GraphicBlock/GraphicBlock.stories.svelte new file mode 100644 index 0000000..f19268e --- /dev/null +++ b/src/components/GraphicBlock/GraphicBlock.stories.svelte @@ -0,0 +1,134 @@ + + + + + + +
+ placeholder +
+
+
+ + + + + + + + + +
+ placeholder +
+ + {#snippet title()} +
My smaller title
+ {/snippet} + + {#snippet notes()} + + {/snippet} +
+
+ + + + + + + + + + + {#snippet ariaDescription()} +

+ A shakemap shows the intensity of the 7.2-magnitude earthquake that + struck Haiti at 8:29 a.m. EST, Aug. 14, 2021. +

+ + + + + + + + + + + + + + + +
CityFelt shake strength
Les CayesVery strong
JeremieStrong
+ {/snippet} +
+
+ + diff --git a/src/components/GraphicBlock/GraphicBlock.svelte b/src/components/GraphicBlock/GraphicBlock.svelte new file mode 100644 index 0000000..a9deaff --- /dev/null +++ b/src/components/GraphicBlock/GraphicBlock.svelte @@ -0,0 +1,148 @@ + + + +
+ + + {#if typeof title === 'string'} + + +

{title}

+ {#if description} + + {/if} +
+
+ {:else if title} + + + + {@render title()} + + + {/if} + + {#if ariaDescription} +
+ {#if typeof ariaDescription === 'string'} + + {:else} + + {@render ariaDescription()} + {/if} +
+ {/if} + {#if typeof notes === 'string'} + + + + + + {:else if notes} + + + + {@render notes()} + + + {/if} +
+
+ + diff --git a/src/components/GraphicBlock/components/AriaHidden.svelte b/src/components/GraphicBlock/components/AriaHidden.svelte new file mode 100644 index 0000000..6186b2e --- /dev/null +++ b/src/components/GraphicBlock/components/AriaHidden.svelte @@ -0,0 +1,22 @@ + + +{#if hidden} + +{:else} + {@render children()} +{/if} diff --git a/src/components/GraphicBlock/components/TextBlock.svelte b/src/components/GraphicBlock/components/TextBlock.svelte new file mode 100644 index 0000000..c7413fd --- /dev/null +++ b/src/components/GraphicBlock/components/TextBlock.svelte @@ -0,0 +1,23 @@ + + +{#if width} + + {@render children()} + +{:else} + {@render children()} +{/if} diff --git a/src/components/GraphicBlock/demo/ai2svelte/ai-chart.svelte b/src/components/GraphicBlock/demo/ai2svelte/ai-chart.svelte new file mode 100644 index 0000000..704b6ea --- /dev/null +++ b/src/components/GraphicBlock/demo/ai2svelte/ai-chart.svelte @@ -0,0 +1,630 @@ + + + + +
+ + {#if width && width >= 0 && width < 510} +
+
+
+
+

Shake intensity

+
+
+

Light

+
+
+

Moderate

+
+
+

Cap-Haitien

+
+
+

Strong

+
+
+

Very strong

+
+
+

Gonaïves

+
+
+

Caribbean

+

Sea

+
+
+

HAITI

+
+
+

Jeremie

+
+
+

Port-au-Prince

+
+
+

Epicenter

+
+
+

Jacmel

+
+
+

Les Cayes

+
+
+

50 mi

+
+
+

Dominican

+

Republic

+
+
+

50 km

+
+
+ {/if} + + {#if width && width >= 510 && width < 660} +
+
+
+
+

Shake intensity

+
+
+

Light

+
+
+

Moderate

+
+
+

Cap-Haitien

+
+
+

Strong

+
+
+

Very strong

+
+
+

Gonaïves

+
+
+

Caribbean

+

Sea

+
+
+

HAITI

+
+
+

Jeremie

+
+
+

Port-au-Prince

+
+
+

Epicenter

+
+
+

Dominican

+

Republic

+
+
+

Jacmel

+
+
+

Les Cayes

+
+
+

50 mi

+
+
+

50 km

+
+
+ {/if} + + {#if width && width >= 660} +
+
+
+
+

Shake intensity

+
+
+

Light

+
+
+

Moderate

+
+
+

Cap-Haitien

+
+
+

Strong

+
+
+

Very strong

+
+
+

Gonaïves

+
+
+

Caribbean

+

Sea

+
+
+

HAITI

+
+
+

Dominican

+

Republic

+
+
+

Jeremie

+
+
+

Epicenter

+
+
+

Port-au-Prince

+
+
+

Jacmel

+
+
+

Les Cayes

+
+
+

50 mi

+
+
+

50 km

+
+
+ {/if} +
+ + + + + diff --git a/src/components/GraphicBlock/demo/imgs/ai-chart-md.png b/src/components/GraphicBlock/demo/imgs/ai-chart-md.png new file mode 100644 index 0000000..7f62d44 Binary files /dev/null and b/src/components/GraphicBlock/demo/imgs/ai-chart-md.png differ diff --git a/src/components/GraphicBlock/demo/imgs/ai-chart-sm.png b/src/components/GraphicBlock/demo/imgs/ai-chart-sm.png new file mode 100644 index 0000000..49c06f6 Binary files /dev/null and b/src/components/GraphicBlock/demo/imgs/ai-chart-sm.png differ diff --git a/src/components/GraphicBlock/demo/imgs/ai-chart-xs.png b/src/components/GraphicBlock/demo/imgs/ai-chart-xs.png new file mode 100644 index 0000000..15d640d Binary files /dev/null and b/src/components/GraphicBlock/demo/imgs/ai-chart-xs.png differ diff --git a/src/components/GraphicBlock/demo/placeholder.png b/src/components/GraphicBlock/demo/placeholder.png new file mode 100644 index 0000000..ca52164 Binary files /dev/null and b/src/components/GraphicBlock/demo/placeholder.png differ diff --git a/src/components/Headline/Headline.mdx b/src/components/Headline/Headline.mdx new file mode 100644 index 0000000..320ed4a --- /dev/null +++ b/src/components/Headline/Headline.mdx @@ -0,0 +1,150 @@ +import { Meta, Canvas } from '@storybook/blocks'; + +import * as HeadlineStories from './Headline.stories.svelte'; + + + +# Headline + +The `Headline` component creates headlines in the legacy Reuters Graphics style, with the text centred on the page. + +```svelte + + + +``` + + + +## With bylines and dateline + +Optionally, you can add authors and a publish time to the headline, which the `Headline` component internally renders with the [Byline](./?path=/docs/components-text-elements-byline--docs) component. + +> **Note**: Since `Headline` uses `Byline`, you can customise the author page hyperlink and bylines with the `getAuthorPage`, `byline`, `published` and `updated` props. + +```svelte + + + `mailto:${author.replace(' ', '')}@example.com`} +/> +``` + + + +## Custom hed and dek + +Use the `hed` and/or `dek` [snippets](https://svelte.dev/docs/svelte/snippet) to override those elements with custom elements. + +```svelte + + + + + {#snippet hed()} +

+ The secret to + “The Nutcracker's” + success +

+ {/snippet} + + + {#snippet dek()} +

+ How “The Nutcracker” ballet became anAmerican holday stapleand a financial pillar of ballet companies across the country +

+ {/snippet} +
+ + + +``` + + + +## With crown image + +To add a crown image, use the `crown` [snippet](https://svelte.dev/docs/svelte/snippet). + +```svelte + + + + + {#snippet crown()} + Illustration of Europe + {/snippet} + +``` + + + +## With crown graphic + +Add a full graphic or any other component in the crown. + +```svelte + + + + + {#snippet crown()} + + + {/snippet} + +``` + + diff --git a/src/components/Headline/Headline.stories.svelte b/src/components/Headline/Headline.stories.svelte new file mode 100644 index 0000000..0b48df7 --- /dev/null +++ b/src/components/Headline/Headline.stories.svelte @@ -0,0 +1,111 @@ + + + + + + + + { + return `mailto:${author.replace(' ', '')}@example.com`; + }} + /> + + + + + {#snippet hed()} +

+ The secret to + “The Nutcracker's” + success +

+ {/snippet} + {#snippet dek()} +

+ How “The Nutcracker” ballet became anAmerican holday stapleand a financial pillar of ballet companies across the country +

+ {/snippet} +
+
+ + + + + {#snippet crown()} + Illustration of Europe + {/snippet} + + + + + + + {#snippet crown()} + + {/snippet} + + + + diff --git a/src/components/Headline/Headline.svelte b/src/components/Headline/Headline.svelte new file mode 100644 index 0000000..447f5b7 --- /dev/null +++ b/src/components/Headline/Headline.svelte @@ -0,0 +1,161 @@ + + + +
+ +
+ {#if crown} +
+ + {@render crown()} +
+ {/if} +
+ {#if section} +

+ {section} +

+ {/if} + {#if typeof hed === 'string'} +

+ +

+ {:else if hed} + + {@render hed()} + {/if} + {#if typeof dek === 'string'} +
+ +
+ {:else if dek} + +
+ {@render dek()} +
+ {/if} +
+ {#if authors.length > 0 || publishTime} + + {:else if byline} + + {@render byline()} + {/if} +
+
+
+ + diff --git a/src/components/Headline/demo/crown.png b/src/components/Headline/demo/crown.png new file mode 100644 index 0000000..c6bec93 Binary files /dev/null and b/src/components/Headline/demo/crown.png differ diff --git a/src/components/Headline/demo/graphic-lg.png b/src/components/Headline/demo/graphic-lg.png new file mode 100644 index 0000000..cc07e74 Binary files /dev/null and b/src/components/Headline/demo/graphic-lg.png differ diff --git a/src/components/Headline/demo/graphic-md.png b/src/components/Headline/demo/graphic-md.png new file mode 100644 index 0000000..88e3ca1 Binary files /dev/null and b/src/components/Headline/demo/graphic-md.png differ diff --git a/src/components/Headline/demo/graphic-sm.png b/src/components/Headline/demo/graphic-sm.png new file mode 100644 index 0000000..89e54ad Binary files /dev/null and b/src/components/Headline/demo/graphic-sm.png differ diff --git a/src/components/Headline/demo/graphic-xl.png b/src/components/Headline/demo/graphic-xl.png new file mode 100644 index 0000000..bc6488f Binary files /dev/null and b/src/components/Headline/demo/graphic-xl.png differ diff --git a/src/components/Headline/demo/graphic-xs.png b/src/components/Headline/demo/graphic-xs.png new file mode 100644 index 0000000..60ab82c Binary files /dev/null and b/src/components/Headline/demo/graphic-xs.png differ diff --git a/src/components/Headline/demo/graphic.svelte b/src/components/Headline/demo/graphic.svelte new file mode 100644 index 0000000..41afe75 --- /dev/null +++ b/src/components/Headline/demo/graphic.svelte @@ -0,0 +1,962 @@ + + +
+ + {#if width && width >= 0 && width < 510} +
+
+
+
+

GREENLAND

+
+
+

UKRAINE

+
+
+

RUSSIA

+
+
+

Moscow

+
+
+

CANADA

+
+
+

U.S.

+
+
+

BELARUS

+
+
+

SPAIN

+
+
+ {/if} + + {#if width && width >= 510 && width < 660} +
+
+
+
+

GREENLAND

+
+
+

FINLAND

+
+
+

RUSSIA

+
+
+

NORWAY

+
+
+

Moscow

+
+
+

BELARUS

+
+
+

ICELAND

+
+
+

UNITED

+

STATES

+
+
+

UK

+
+
+

UKRAINE

+
+
+

CANADA

+
+
+

IRELAND

+
+
+

IRAN

+
+
+

FRANCE

+
+
+

ITALY

+
+
+

SPAIN

+
+
+ {/if} + + {#if width && width >= 660 && width < 930} +
+
+
+ +
+

GREENLAND

+
+
+

FINLAND

+
+
+

RUSSIA

+
+
+

NORWAY

+
+
+

Moscow

+
+
+

BELARUS

+
+
+

ICELAND

+
+
+

CANADA

+
+
+

UNITED

+

STATES

+
+
+

UK

+
+
+

UKRAINE

+
+
+

IRELAND

+
+
+

IRAN

+
+
+

FRANCE

+
+
+

SAUDI

+

ARABIA

+
+
+

ITALY

+
+
+

SPAIN

+
+
+ {/if} + + {#if width && width >= 930 && width < 1200} +
+
+
+
+

FINLAND

+
+
+

RUSSIA

+
+
+

GREENLAND

+
+
+

KAZAKHSTAN

+
+
+

NORWAY

+
+
+

Moscow

+
+
+

BELARUS

+
+
+

ICELAND

+
+
+

CANADA

+
+
+

UNITED

+

STATES

+
+
+

UK

+
+
+

UKRAINE

+
+
+

IRELAND

+
+
+

IRAN

+
+
+

FRANCE

+
+
+

SPAIN

+
+
+

SAUDI

+

ARABIA

+
+
+

ITALY

+
+
+ {/if} + + {#if width && width >= 1200} +
+
+
+
+

FINLAND

+
+
+

RUSSIA

+
+
+

GREENLAND

+
+
+

KAZAKHSTAN

+
+
+

NORWAY

+
+
+

Moscow

+
+
+

BELARUS

+
+
+

ICELAND

+
+
+

CANADA

+
+
+

UKRAINE

+
+
+

UK

+
+
+

UNITED

+

STATES

+
+
+

POLAND

+
+
+

GERMANY

+
+
+

IRELAND

+
+
+

ROMANIA

+
+
+

IRAN

+
+
+

FRANCE

+
+
+

ITALY

+
+
+

SAUDI

+

ARABIA

+
+
+

SPAIN

+
+
+

GREECE

+
+
+ {/if} +
+ + + + + + + + diff --git a/src/components/HorizontalScroller/Debug.svelte b/src/components/HorizontalScroller/Debug.svelte new file mode 100644 index 0000000..f3da8a3 --- /dev/null +++ b/src/components/HorizontalScroller/Debug.svelte @@ -0,0 +1,355 @@ + + + + +{#snippet triggerPoints()} + {#if componentState.triggerStops.length > 0} + {#if componentState.scrubbed} + {@const totalStops = componentState.triggerStops.length} + {#each Array(totalStops) as _, index} + | + {/each} + {:else} + {@const stops = componentState.triggerStops.map((x: number) => + mappedStop(x) + )} + {#each stops as stop, index} + {#if index < stops.length - 1} + | + {/if} + {/each} + {/if} + {/if} +{/snippet} + +
+
+ + CONSOLE + +
+ +

Progress:

+
+

+ {componentState.progress} +

+
+ +

Mapped progress:

+
+

+ {@render triggerPoints()} + {fmt.format(componentState.mappedProgress)} +   +

+
+
+
+
+ +

Eased Progress:

+
+

+ {#if componentState.stops.length > 0} + {#each componentState.stops as stop} + {stop} + {/each} + {/if} + {fmt.format(componentState.easedProgress)} +   +

+
+
+
+
+ +

Direction:

+
+

+ {componentState.direction} +

+
+ + {#if componentState.stops.length > 0} +

Stops:

+
+

+ {#each componentState.stops as stop} + {stop} + {/each} +

+
+ {/if} + +

Handle scroll:

+
+

+ {componentState.handleScroll} +

+
+ +

Scrubbed:

+
+

+ {componentState.scrubbed} +

+
+ +

Easing:

+
+

+ {componentState.easing} +

+
+ +

+ Duration: + {#if componentState.scrubbed} + NA + {/if} +

+
+

+ {componentState.duration} +

+
+ +
+
+
+ + diff --git a/src/components/HorizontalScroller/HorizontalScroller.mdx b/src/components/HorizontalScroller/HorizontalScroller.mdx new file mode 100644 index 0000000..1fae940 --- /dev/null +++ b/src/components/HorizontalScroller/HorizontalScroller.mdx @@ -0,0 +1,447 @@ +import { Meta } from '@storybook/blocks'; + +import * as HorizontalScrollerStories from './HorizontalScroller.stories.svelte'; + +import IllustratorScreenshot from './assets/illustrator.png'; + + + +# HorizontalScroller + +The `HorizontalScroller` component creates a horizontal scrolling section that scrolls through any child content wider than `100vw`. + +To use `HorizontalScroller`, wrap it around the content that you want to horizontally scroll through. The scroll length is controlled by the height of the `HorizontalScroller` container, which is set by the prop `height`. `height` defaults to `200lvh`, but you can adjust this to any valid CSS height value such as `1200px` or `400lvh`. + +The child content inside the `HorizontalScroller` must be wider than `100vw` so that there is overflow to horizontal scroll through. By default, only the top `100lvh` of the child content is visible. You can use CSS `transform: translate()` on the child content to adjust its vertical positioning within the visible area. + +> 💡TIP: Use `lvh` or `svh` units instead of `vh` unit for the height, as [these units](https://www.w3.org/TR/css-values-4/#large-viewport-size) are more reliable on mobile or other devices where elements such as the address bar toggle between being shown and hidden. + +> 💡TIP: Set the `showDebugInfo` prop to `true` to visualise the scroll progress and other useful information. + +See the full list of available props under the `Controls` tab in the [demo](?path=/story/components-graphics-horizontalscroller--demo). + +```svelte + + + + + + + +
+ alt text +
+
+
+``` + +## Controlling scroll behaviour with stops and easing + +The `HorizontalScroller` allows you to control the horizontal scroll behaviour and pacing with various props. + +**`stops`:** + +`stops` is an optional prop that accepts an array of numbers between `0` and `1`. At these points, which corresponds to the scroll `progress` values, the scrolling stops or slows down. This is useful for adding custom pauses based on progress. + +For example, as shown in the demo below, if you define `stops` as `[0.2, 0.5, 0.9]`, the scrolling will pause or slow down at these `progress` values as the user scrolls through the `HorizontalScroller` section. + +**`scrubbed`:** + +The `scrubbed` prop controls whether the scrolling is tied exactly to the scroll position (`scrubbed: true`) or is smoothed out (`scrubbed: false`). This prop defaults to `true`. + +If `scrubbed` is set to `false` and `stops` are defined, the scrolling transitions smoothly between the stop values. + +**`easing`** and **`duration`**: + +`easing` accepts any easing function from `svelte/easing` or a custom easing function, while `duration` sets the time, in milliseconds, for each transition between stops. + +So, if the stops are at irregular intervals — for example, `[0.2, 0.9]` — the scroll to the first stop will be much quicker than the scroll to the second stop since the distance to travel is different but the duration of the transition is the same. + +By default, `duration` is set to `400` milliseconds. + +[Demo](?path=/story/components-graphics-horizontalscroller--with-stops) + +```svelte + + + + + + +
+ alt text +
+
+
+``` + +## Extended boundaries + +`HorizontalScroller` has `mappedStart` and `mappedEnd` props, which extend the horizontal scroll boundaries beyond the default 0 to 1 range. This is useful when you want to create an overscroll effect or have more control over the horizontal scroll range. By default, these values are set to 0 and 1 respectively. + +If using custom `mappedStart` and `mappedEnd` values, you must also set `stops` values that are within the mapped range. + +> 💡TIP: In the debugging info box, `Progress` indicates the raw scroll progress value between `0` and `1`. `Mapped Progress` indicates the vertical progress mapped to `mappedStart` and `mappedEnd`. If they are not set, `Mapped Progress` is bound between 0 and 1 and matches `Progress`. `Eased Progress` indicates the scroll progress with any stops and easing applied. `Eased Progress` is what reflects the actual transition of the horizontal scroll position. + +[Demo](?path=/story/components-graphics-horizontalscroller--extended-boundaries) + +```svelte + + + + + + +
+ alt text +
+
+
+``` + +## With ai2svelte components + +With [ai2svelte](https://reuters-graphics.github.io/ai2svelte/) v1.0.3 onwards, you can export your ai2svelte graphic with a wider-than-viewport layout and use it directly inside `HorizontalScroller` to create horizontally scrolling graphics. + +To do that, follow these steps: + +1. In Illustrator, rename your artboard with the breakpoint at which you want that artboard to be visible on the page. For example, to make the XL artboard visible on viewports wider than 1200px, rename it to `xl:1200`. You can have multiple artboards with different breakpoints. +2. Add these properties to the ai2svelte settings and run the script to export the component. + +```yaml +include_resizer_css: false +respect_height: true +allow_overflow: true +``` + +Screenshot showing Illustrator document with artboard panel + +[Demo](?path=/story/components-graphics-horizontalscroller--scrollable-ai-2-svelte) + +```svelte + + + + + + + + +``` + +## With ai2svelte components: advanced + +You can use the bound prop `progress` to create advanced interactivity with an ai2svelte graphic. + +The demo below has 2 advanced interactions: fade in/out of caption boxes based on scroll position and parallax movement of a `png` layer. + +### Captions fading in/out + +Caption boxes are exported as `htext` [tagged layers](https://reuters-graphics.github.io/ai2svelte/users/tagged-layers/) in ai2svelte. In this example, we use the `handleScroll()` function to check the position of each caption box relative to the viewport width and set its opacity to `1` (visible) or `0` (hidden) based on whether the caption box is within the `threshold` of the viewport. In Adobe Illustrator, set `override_text: true` in the ai2svelte export settings to allow custom HTML content in tagged text layers. + +### Parallax effect with png layer + +This demo has a tagged `png` [layer](https://reuters-graphics.github.io/ai2svelte/users/tagged-layers/), which contains the foreground overlay image. The `handleScroll()` function uses the bound `progress` value to calculate a horizontal translation for the `png` layer, creating a parallax effect as the user scrolls through the `HorizontalScroller`. + +[Demo](?path=/story/components-graphics-horizontalscroller--scrollable-ai-2-svelte-advanced) + +```svelte + + + + + + Caption 1!
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
', + caption2: + '
Caption 2!
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
', + caption3: + '
Caption 3!
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
', + caption4: + '
Caption 4!
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
', + }, + }, + }} + /> + + + + +``` + +## With custom child components + +You can create a custom horizontal layout with any component and pass it as a child to the `HorizontalScroller`. Here's an example with `DatawrapperChart`, `Headline` and ai2svelte components laid out in a horizontal scroll. + +[Demo](?path=/story/components-graphics-horizontalscroller--custom-children) + +```svelte + + + + + +
+
+ +
+
+ +
+
+ + + +
+
+
+
+ + +``` + +## With ScrollerBase + +You can also integrate HorizontalScroller with `ScrollerBase` for a horizontal scroll with vertical captions. + +When using `HorizontalScroller` with `ScrollerBase` or other scrollers, you must: + +- Create a `progress` state variable and bind it to both `ScrollerBase` and `HorizontalScroller` +- Set `HorizontalScroller`'s `height` to `100lvh` +- Set `handleScroll` to `false` + +> **⚠️ Warning:** It is not recommended to use HorizontalScroller with vertical ScrollerBase. This example is only to serve the purpose of demonstrating how to control the HorizontalScroller with an external progress value (ScrollerBase's progress in this case). + +[Demo](?path=/story/components-graphics-horizontalscroller--with-scroller-base) + +```svelte + + + + {#snippet backgroundSnippet()} + + + + + + + {/snippet} + {#snippet foregroundSnippet()} + +

Step 1

+

Step 2

+

Step 3

+

Step 4

+

Step 5

+ {/snippet} +
+ + +``` diff --git a/src/components/HorizontalScroller/HorizontalScroller.stories.svelte b/src/components/HorizontalScroller/HorizontalScroller.stories.svelte new file mode 100644 index 0000000..c17dfbf --- /dev/null +++ b/src/components/HorizontalScroller/HorizontalScroller.stories.svelte @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/HorizontalScroller/HorizontalScroller.svelte b/src/components/HorizontalScroller/HorizontalScroller.svelte new file mode 100644 index 0000000..7a66e6d --- /dev/null +++ b/src/components/HorizontalScroller/HorizontalScroller.svelte @@ -0,0 +1,254 @@ + + + + +
+
+ {#if children} + {@render children()} + {/if} + {#if showDebugInfo} +
+ +
+ {/if} +
+
+ + diff --git a/src/components/HorizontalScroller/assets/illustrator.png b/src/components/HorizontalScroller/assets/illustrator.png new file mode 100644 index 0000000..7de9355 Binary files /dev/null and b/src/components/HorizontalScroller/assets/illustrator.png differ diff --git a/src/components/HorizontalScroller/demo/AdvancedScrollableGraphic.svelte b/src/components/HorizontalScroller/demo/AdvancedScrollableGraphic.svelte new file mode 100644 index 0000000..c3f51e4 --- /dev/null +++ b/src/components/HorizontalScroller/demo/AdvancedScrollableGraphic.svelte @@ -0,0 +1,103 @@ + + + + + + + + + Caption 1!
Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + caption2: + '
Caption 2!
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
', + caption3: + '
Caption 3!
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
', + caption4: + '
Caption 4!
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
', + }, + }, + }} + /> +
+
+ + + + diff --git a/src/components/HorizontalScroller/demo/CustomChildrenSnippet.svelte b/src/components/HorizontalScroller/demo/CustomChildrenSnippet.svelte new file mode 100644 index 0000000..fb2749c --- /dev/null +++ b/src/components/HorizontalScroller/demo/CustomChildrenSnippet.svelte @@ -0,0 +1,44 @@ + + +
+
+ +
+
+ +
+
+ + + +
+
+ + diff --git a/src/components/HorizontalScroller/demo/Demo.svelte b/src/components/HorizontalScroller/demo/Demo.svelte new file mode 100644 index 0000000..c0fbb8d --- /dev/null +++ b/src/components/HorizontalScroller/demo/Demo.svelte @@ -0,0 +1,42 @@ + + + + +{#if args.toggleScrub} + + + +{/if} + + + + + + + + diff --git a/src/components/HorizontalScroller/demo/DemoSnippet.svelte b/src/components/HorizontalScroller/demo/DemoSnippet.svelte new file mode 100644 index 0000000..cda252d --- /dev/null +++ b/src/components/HorizontalScroller/demo/DemoSnippet.svelte @@ -0,0 +1,7 @@ +
+ An ultra wide scenic view of cityscape +
diff --git a/src/components/HorizontalScroller/demo/ScrollableGraphic.svelte b/src/components/HorizontalScroller/demo/ScrollableGraphic.svelte new file mode 100644 index 0000000..7b03f26 --- /dev/null +++ b/src/components/HorizontalScroller/demo/ScrollableGraphic.svelte @@ -0,0 +1,29 @@ + + + + + + + + + + + + diff --git a/src/components/HorizontalScroller/demo/graphic/ai2svelte/ai-chart.svelte b/src/components/HorizontalScroller/demo/graphic/ai2svelte/ai-chart.svelte new file mode 100644 index 0000000..704b6ea --- /dev/null +++ b/src/components/HorizontalScroller/demo/graphic/ai2svelte/ai-chart.svelte @@ -0,0 +1,630 @@ + + + + +
+ + {#if width && width >= 0 && width < 510} +
+
+
+
+

Shake intensity

+
+
+

Light

+
+
+

Moderate

+
+
+

Cap-Haitien

+
+
+

Strong

+
+
+

Very strong

+
+
+

Gonaïves

+
+
+

Caribbean

+

Sea

+
+
+

HAITI

+
+
+

Jeremie

+
+
+

Port-au-Prince

+
+
+

Epicenter

+
+
+

Jacmel

+
+
+

Les Cayes

+
+
+

50 mi

+
+
+

Dominican

+

Republic

+
+
+

50 km

+
+
+ {/if} + + {#if width && width >= 510 && width < 660} +
+
+
+
+

Shake intensity

+
+
+

Light

+
+
+

Moderate

+
+
+

Cap-Haitien

+
+
+

Strong

+
+
+

Very strong

+
+
+

Gonaïves

+
+
+

Caribbean

+

Sea

+
+
+

HAITI

+
+
+

Jeremie

+
+
+

Port-au-Prince

+
+
+

Epicenter

+
+
+

Dominican

+

Republic

+
+
+

Jacmel

+
+
+

Les Cayes

+
+
+

50 mi

+
+
+

50 km

+
+
+ {/if} + + {#if width && width >= 660} +
+
+
+
+

Shake intensity

+
+
+

Light

+
+
+

Moderate

+
+
+

Cap-Haitien

+
+
+

Strong

+
+
+

Very strong

+
+
+

Gonaïves

+
+
+

Caribbean

+

Sea

+
+
+

HAITI

+
+
+

Dominican

+

Republic

+
+
+

Jeremie

+
+
+

Epicenter

+
+
+

Port-au-Prince

+
+
+

Jacmel

+
+
+

Les Cayes

+
+
+

50 mi

+
+
+

50 km

+
+
+ {/if} +
+ + + + + diff --git a/src/components/HorizontalScroller/demo/graphic/ai2svelte/demo.svelte b/src/components/HorizontalScroller/demo/graphic/ai2svelte/demo.svelte new file mode 100644 index 0000000..de14038 --- /dev/null +++ b/src/components/HorizontalScroller/demo/graphic/ai2svelte/demo.svelte @@ -0,0 +1,280 @@ + + + + +
+ + {#if aiBoxWidth && aiBoxWidth >= 0 && aiBoxWidth < 1200} +
+
+
+
+ {/if} + + {#if aiBoxWidth && aiBoxWidth >= 1200} +
+
+
+
+

+ {@html taggedText?.htext?.captions?.caption2 || ''} +

+
+
+

+ {@html taggedText?.htext?.captions?.caption3 || ''} +

+
+
+

+ {@html taggedText?.htext?.captions?.caption4 || ''} +

+
+
+

+ {@html taggedText?.htext?.captions?.caption1 || ''} +

+
+
+ {/if} +
+ + + + + + + diff --git a/src/components/HorizontalScroller/demo/graphic/imgs/ai-chart-md.png b/src/components/HorizontalScroller/demo/graphic/imgs/ai-chart-md.png new file mode 100644 index 0000000..7f62d44 Binary files /dev/null and b/src/components/HorizontalScroller/demo/graphic/imgs/ai-chart-md.png differ diff --git a/src/components/HorizontalScroller/demo/graphic/imgs/ai-chart-sm.png b/src/components/HorizontalScroller/demo/graphic/imgs/ai-chart-sm.png new file mode 100644 index 0000000..49c06f6 Binary files /dev/null and b/src/components/HorizontalScroller/demo/graphic/imgs/ai-chart-sm.png differ diff --git a/src/components/HorizontalScroller/demo/graphic/imgs/ai-chart-xs.png b/src/components/HorizontalScroller/demo/graphic/imgs/ai-chart-xs.png new file mode 100644 index 0000000..15d640d Binary files /dev/null and b/src/components/HorizontalScroller/demo/graphic/imgs/ai-chart-xs.png differ diff --git a/src/components/HorizontalScroller/demo/graphic/imgs/demo-lg.jpg b/src/components/HorizontalScroller/demo/graphic/imgs/demo-lg.jpg new file mode 100644 index 0000000..7a22ffb Binary files /dev/null and b/src/components/HorizontalScroller/demo/graphic/imgs/demo-lg.jpg differ diff --git a/src/components/HorizontalScroller/demo/graphic/imgs/demo-xl.jpg b/src/components/HorizontalScroller/demo/graphic/imgs/demo-xl.jpg new file mode 100644 index 0000000..342cf78 Binary files /dev/null and b/src/components/HorizontalScroller/demo/graphic/imgs/demo-xl.jpg differ diff --git a/src/components/HorizontalScroller/demo/graphic/imgs/layer-overlay-lg.png b/src/components/HorizontalScroller/demo/graphic/imgs/layer-overlay-lg.png new file mode 100644 index 0000000..93b1bb0 Binary files /dev/null and b/src/components/HorizontalScroller/demo/graphic/imgs/layer-overlay-lg.png differ diff --git a/src/components/HorizontalScroller/demo/graphic/imgs/layer-overlay-xl.png b/src/components/HorizontalScroller/demo/graphic/imgs/layer-overlay-xl.png new file mode 100644 index 0000000..2a601fb Binary files /dev/null and b/src/components/HorizontalScroller/demo/graphic/imgs/layer-overlay-xl.png differ diff --git a/src/components/HorizontalScroller/demo/graphic/placeholder.png b/src/components/HorizontalScroller/demo/graphic/placeholder.png new file mode 100644 index 0000000..ca52164 Binary files /dev/null and b/src/components/HorizontalScroller/demo/graphic/placeholder.png differ diff --git a/src/components/HorizontalScroller/demo/withScrollerBase.svelte b/src/components/HorizontalScroller/demo/withScrollerBase.svelte new file mode 100644 index 0000000..fe3a267 --- /dev/null +++ b/src/components/HorizontalScroller/demo/withScrollerBase.svelte @@ -0,0 +1,63 @@ + + + + + + {#snippet backgroundSnippet()} + + + + + + {/snippet} + {#snippet foregroundSnippet()} + +

Step 1

+

Step 2

+

Step 3

+

Step 4

+

Step 5

+ {/snippet} +
+ + diff --git a/src/components/HorizontalScroller/utils/index.ts b/src/components/HorizontalScroller/utils/index.ts new file mode 100644 index 0000000..06807f9 --- /dev/null +++ b/src/components/HorizontalScroller/utils/index.ts @@ -0,0 +1,40 @@ +/** + * Clamp a number `n` to the inclusive range [low, high]. + */ +export function clamp(n: number, low: number, high: number): number { + // Ensure low <= high even if caller swaps them + const min = Math.min(low, high); + const max = Math.max(low, high); + return Math.max(min, Math.min(n, max)); +} + +/** + * Linearly maps a value `n` from range [inStart, inEnd] to [outStart, outEnd]. + * + * @param {number} n - The input value to map. + * @param {number} inStart - Input range start. + * @param {number} inEnd - Input range end. + * @param {number} outStart - Output range start. + * @param {number} outEnd - Output range end. + * @param {boolean} withinBounds - If true, clamp the mapped value to [outStart, outEnd]. + * @returns {number} - Mapped (and optionally clamped) value. + */ +export function map( + n: number, + inStart: number, + inEnd: number, + outStart: number, + outEnd: number, + withinBounds: boolean = true +): number { + // Avoid division by zero: when input range is degenerate, return outStart + const inSpan = inEnd - inStart; + if (inSpan === 0) { + return withinBounds ? clamp(outStart, outStart, outEnd) : outStart; + } + + const t = (n - inStart) / inSpan; // normalized 0..1 in input space (or beyond) + const out = t * (outEnd - outStart) + outStart; + + return withinBounds ? clamp(out, outStart, outEnd) : out; +} diff --git a/src/components/Lottie/Debug.svelte b/src/components/Lottie/Debug.svelte new file mode 100644 index 0000000..8fc6faf --- /dev/null +++ b/src/components/Lottie/Debug.svelte @@ -0,0 +1,269 @@ + + + + +
+
+ + CONSOLE + +
+ +

Progress:

+
+

{componentState.progress}

+
+
+
+
+ +

Duration:

+

+ {componentState.duration}s +

+ + {#if componentState.segment} +

Segment:

+

+ {componentState.segment[0]} -- {componentState.segment[1]} +

+ {/if} + +

Current frame:

+

+ {componentState.currentFrame}/{componentState.totalFrames} +

+ +

Speed:

+

+ {componentState.speed} +

+ +

Autoplay:

+

+ {componentState.autoplay} +

+ +

Loop:

+

+ {componentState.loop} + {componentState.loop ? `(Loop count: ${componentState.loopCount})` : ''} +

+ +

Mode:

+

+ {componentState.mode} +

+ +

Layout:

+

+ {JSON.stringify(componentState.layout)} +

+ + {#if Object.keys(componentState.allMarkers).length} +

All markers:

+

+ {componentState.allMarkers} +

+ {/if} + + {#if componentState.marker} +

Active marker:

+

+ {componentState.marker} +

+ {/if} + + {#if componentState.allThemes.length} +

All themes:

+

+ {componentState.allThemes.join(', ')} +

+ {/if} + {#if componentState.activeThemeId} +

Active theme ID:

+

+ {componentState.activeThemeId} +

+ {/if} + +

isPaused:

+

+ {componentState.isPaused} +

+ +

isPlaying:

+

+ {componentState.isPlaying} +

+ +

isStopped:

+

+ {componentState.isStopped} +

+ +

isLoaded:

+

+ {componentState.isLoaded} +

+ +

isFrozen:

+

+ {componentState.isFrozen} +

+
+
+
+ + diff --git a/src/components/Lottie/Lottie.mdx b/src/components/Lottie/Lottie.mdx new file mode 100644 index 0000000..6c0029b --- /dev/null +++ b/src/components/Lottie/Lottie.mdx @@ -0,0 +1,386 @@ +import { Meta } from '@storybook/blocks'; + +import * as LottieStories from './Lottie.stories.svelte'; +import CompositionMarkerImage from './assets/marker.jpg?url'; + + + +# Lottie + +The `Lottie` component uses the [dotLottie-web](https://developers.lottiefiles.com/docs/dotlottie-player/dotlottie-web/) library to render Lottie animations. + +## How to prepare Lottie files + +[LottieFiles](https://lottiefiles.com/) is the official platform for creating and editing Lottie animations. The free version of LottieFiles has limited features, so [Bodymovin](https://exchange.adobe.com/apps/cc/12557/bodymovin) remains a popular, free way to export Lottie animations as JSON files. + +[dotLottie](https://dotlottie.io/) is another common format for Lottie files. This format bundles the Lottie JSON file and any associated assets, such as images and fonts, into a single compressed file with the extension `.lottie`. + +This `Lottie` component is flexible and supports both `dotLottie` and JSON Lottie files. For best performance it is recommended that you convert your Lottie JSON file into a `.zip` file by following these steps: + +1. Export your Lottie animation as a JSON file using [Bodymovin](https://exchange.adobe.com/apps/cc/12557/bodymovin) or another Lottie exporter. +2. Use the [LottieFiles converter](https://lottiefiles.com/tools/lottie-to-dotlottie) to convert the JSON file into a `.lottie` file. +3. Change the file extension to `.zip` from `.lottie`. This ensures full compatibility with the Reuters graphics publisher while maintaining the benefits of dotLottie format's compression and optimisation. + +## When not to use Lottie + +Lottie animations are great for lightweight, scalable animations. However, they may not be suitable for all use cases. Consider the following before using Lottie: + +- **Huge raster images**: Lottie is best suited for simple to moderately complex animations. Animations with large raster images may not render well or could lead to performance issues. In such cases, consider using a [Video](?path=/docs/components-multimedia-video--docs) component or a [ScrollerVideo](?path=/docs/components-graphics-scrollervideo--docs) component instead. + +- **Complex effects**: Some advanced effects and features available in After Effects may not be fully supported in Lottie, which could lead to discrepancies between the original design and the rendered animation. Check the [Lottie documentation](https://lottiefiles.com/supported-features) for a list of supported features. + +- **Text rendering**: Lottie renders text as vector shapes. If you need DOM text for accessibility or CSS manipulation, consider using HTML/CSS animations instead. + +- **SVG DOM manipulation**: Lottie renders animations on a canvas. If you need to manipulate individual elements of the animation using JavaScript or CSS, consider using SVG animations instead. + +## Basic demo + +To use the `Lottie` component, import it and provide the Lottie animation source. The height of the container defaults to `100lvh`, but you can adjust this to any valid CSS height value such as `1200px` or `200lvh` with the `height` prop. + +**Use `lvh` or `svh` units instead of `vh`** as [these units](https://www.w3.org/TR/css-values-4/#large-viewport-size) are more reliable on mobile and other devices where elements such as the address bar appear and disappear and affect the height. + +The component also provides a `width` prop to set the width of the Lottie container. While the `width` prop defaults to `fluid`, it allows any `ContainerWidth` value such as `narrower`, `narrow`, `normal`, `wide`, `wider`, `widest`, `fluid`, or a custom CSS width value like `600px` or `80vw`. + +If importing the Lottie file directly into a Svelte component, make sure to append **?url** to the import statement (see example below). This ensures that the file is treated as a URL. + +> 💡TIP: Set `showDebugInfo` prop to `true` to display information about the component state. + +[Demo](?path=/story/components-graphics-scrollerlottie--demo) + +```svelte + + + +``` + +## Using with ArchieML + +If you are using `Lottie` with ArchieML, store your Lottie zip file in the `src/statics/lottie/` folder. + +With the graphics kit, you'll likely get your text and prop values from an ArchieML doc... + +```yaml +# ArchieML doc +[blocks] + type: lottie + + # Lottie file stored in `src/statics/lottie/` folder + src: lottie/LottieFile.zip + autoplay: true + loop: true + showDebugInfo: true +[] +``` + +... which you'll parse out of a ArchieML block object before passing to the `Lottie` component. + +```svelte + + +{#each content.blocks as block} + + {#if block.type == 'lottie'} + + {/if} +{/each} +``` + +## Playing a segment + +The `Lottie` component can play a specific segment of the Lottie animation using the `segment` prop. The `segment` prop expects an array of two numbers representing the start and end frames of the segment. + +[Demo](?path=/story/components-graphics-scrollerlottie--segment) + +With the graphics kit, you'll likely get your text and prop values from an ArchieML doc... + +```yaml +# ArchieML doc +[blocks] + type: lottie + showDebugInfo: true + loop: true + + # Optionally, set playback speed + speed: 0.5 + + # Lottie file stored in `src/statics/lottie/` folder + src: lottie/LottieFile.zip + [.segment] + start: 0 + end: 20 + [] +[] +``` + +... which you'll parse out of a ArchieML block object before passing to the `Lottie` component. + +```svelte + + +{#each content.blocks as block} + + {#if block.type == 'lottie'} + + {/if} +{/each} +``` + +## Markers + +The `Lottie` component can also play a specific portion of the Lottie animation using markers set in [AfterEffects](https://helpx.adobe.com/in/after-effects/using/layer-markers-composition-markers.html). + +The list of available markers, which can be passed into the `marker` prop, can be found in the debug info box that appears when `showDebugInfo` is set to `true`. + +When setting markers in AfterEffects, ensure that the **Comment** section of the Composition Marker contains only the name of your marker: + +Composition Marker Dialog + +[Demo](?path=/story/components-graphics-scrollerlottie--marker) + +```svelte + + + +``` + +## Switching themes + +[Lottie Creator](https://lottiefiles.com/theming) allows you to define multiple colour themes for your animation. You can switch between these themes using the `theme` prop. + +Available themes can be found in the debug info when the `showDebugInfo` prop is set to `true`. + +You can set multiple themes and switch between them dynamically -- for example, based on the `progress` of the animation. + +[Demo](?path=/story/components-graphics-scrollerlottie--themes) + +```svelte + + + +``` + +## Using with `ScrollerBase` + +The `Lottie` component can be used with the `ScrollerBase` component to create a more complex scrolling experience. `ScrollerBase` provides a scrollable container sets the `Lottie` component as a background. + +```svelte + + + + {#snippet backgroundSnippet()} + + + {/snippet} + {#snippet foregroundSnippet()} +
+

Step 1

+
+
+

Step 2

+
+
+

Step 3

+
+ {/snippet} +
+ + +``` + +## With foregrounds + +The `Lottie` component can also be used with the `LottieForeground` component to display foreground elements at specific times in the animation. + +[Demo](?path=/story/components-graphics-scrollerlottie--with-foregrounds). + +```svelte + + + + + +
+ + + +
+
+ + + +
+``` + +### Using with ArchieML + +With the graphics kit, you'll likely get your text and prop values from an ArchieML doc... + +```yaml +# ArchieML doc +[blocks] + type: lottie + + # Lottie file stored in `src/statics/lottie/` folder + src: lottie/LottieFile.zip + + # Array of foregrounds + [.foregrounds] + + # Foreground 1: Headline component + startFrame: 0 # When in the animation to start showing the foreground + endFrame: 50 # When to stop showing the foreground + + # Set foreground type + type: component + + # Set props to pass into `LottieForeground` + {.foregroundProps} + componentName: Headline + hed: Headline + dek: Some deck text + [.authors] + * Jane Doe + * John Smith + [] + {} + + # Foreground 2: Text only + startFrame: 50 + endFrame: 100 + + # Set foreground type + type: text + + # If the foreground type is `text`, set text prop here + {.foregroundProps} + text: Some text for the foreground + {} + +[] +``` + +... which you'll parse out of a ArchieML block object before passing to the `Lottie` component. + +```svelte + + +{#each content.blocks as block} + + {#if block.type == 'lottie'} + + {#each block.foregrounds as foreground} + {#if foreground.type == 'text'} + + {:else if foreground.type == 'component'} + {@const Component = + Components[foreground.foregroundProps.componentName]} + + + + {/if} + {/each} + + {/if} +{/each} +``` diff --git a/src/components/Lottie/Lottie.stories.svelte b/src/components/Lottie/Lottie.stories.svelte new file mode 100644 index 0000000..8ecc7da --- /dev/null +++ b/src/components/Lottie/Lottie.stories.svelte @@ -0,0 +1,150 @@ + + + + {#snippet children(args)} + + {/snippet} + + + + {#snippet children(args)} + + {/snippet} + + + + {#snippet children(args)} + + {/snippet} + + + + {#snippet children(args)} + + {/snippet} + + + + + + + + + +
+ + + +
+
+ + +
+
+ + diff --git a/src/components/Lottie/Lottie.svelte b/src/components/Lottie/Lottie.svelte new file mode 100644 index 0000000..d7a64ec --- /dev/null +++ b/src/components/Lottie/Lottie.svelte @@ -0,0 +1,443 @@ + + +
+ {#if showDebugInfo && lottiePlayer} + + {/if} + +
+ +
+ + {#if children} + {@render children()} + {/if} +
+ + diff --git a/src/components/Lottie/LottieForeground.svelte b/src/components/Lottie/LottieForeground.svelte new file mode 100644 index 0000000..ed74a21 --- /dev/null +++ b/src/components/Lottie/LottieForeground.svelte @@ -0,0 +1,145 @@ + + +
+ {#if componentState?.currentFrame && componentState.currentFrame >= startFrame && componentState.currentFrame <= endFrame} +
+ + {#if text} + +
+ +
+
+ + {:else if children} + {@render children()} + {/if} +
+ {/if} +
+ + diff --git a/src/components/Lottie/assets/marker.jpg b/src/components/Lottie/assets/marker.jpg new file mode 100644 index 0000000..37f729e Binary files /dev/null and b/src/components/Lottie/assets/marker.jpg differ diff --git a/src/components/Lottie/demo/withScrollerBase.svelte b/src/components/Lottie/demo/withScrollerBase.svelte new file mode 100644 index 0000000..483626d --- /dev/null +++ b/src/components/Lottie/demo/withScrollerBase.svelte @@ -0,0 +1,64 @@ + + + + + + {#snippet backgroundSnippet()} + + {/snippet} + {#snippet foregroundSnippet()} +

Step 1

+

Step 2

+

Step 3

+

Step 4

+

Step 5

+ {/snippet} +
+ + + + diff --git a/src/components/Lottie/lottie/demo.zip b/src/components/Lottie/lottie/demo.zip new file mode 100644 index 0000000..2739af5 Binary files /dev/null and b/src/components/Lottie/lottie/demo.zip differ diff --git a/src/components/Lottie/lottie/foregroundSample.zip b/src/components/Lottie/lottie/foregroundSample.zip new file mode 100644 index 0000000..2b9e0b5 Binary files /dev/null and b/src/components/Lottie/lottie/foregroundSample.zip differ diff --git a/src/components/Lottie/lottie/markerSample.zip b/src/components/Lottie/lottie/markerSample.zip new file mode 100644 index 0000000..8064b2e Binary files /dev/null and b/src/components/Lottie/lottie/markerSample.zip differ diff --git a/src/components/Lottie/lottie/themesLottie.zip b/src/components/Lottie/lottie/themesLottie.zip new file mode 100644 index 0000000..eed4feb Binary files /dev/null and b/src/components/Lottie/lottie/themesLottie.zip differ diff --git a/src/components/Lottie/ts/lottieState.svelte.ts b/src/components/Lottie/ts/lottieState.svelte.ts new file mode 100644 index 0000000..2b8e971 --- /dev/null +++ b/src/components/Lottie/ts/lottieState.svelte.ts @@ -0,0 +1,61 @@ +import type { Layout } from '@lottiefiles/dotlottie-web'; + +export interface LottieState { + [key: string]: + | number + | boolean + | string + | null + | Array + | Array + | [number, number] + | Layout + | undefined; + progress: number; + currentFrame: number; + totalFrames: number; + duration: number; + loop: boolean; + speed: number; + loopCount: number; + mode: string; + isPaused: boolean; + isPlaying: boolean; + isStopped: boolean; + isLoaded: boolean; + isFrozen: boolean; + segment: null | [number, number]; + autoplay: boolean; + layout: null | Layout; + allMarkers: Array; + marker: undefined | string; + allThemes: Array; + activeThemeId: null | string; +} + +export function createLottieState(): LottieState { + const lottieState = $state({ + progress: 0, + currentFrame: 0, + totalFrames: 0, + duration: 0, + loop: false, + speed: 1, + loopCount: 0, + mode: '', + isPaused: false, + isPlaying: false, + isStopped: false, + isLoaded: false, + isFrozen: false, + segment: null, + autoplay: false, + layout: null, + allMarkers: [], + marker: undefined, + allThemes: [], + activeThemeId: null, + }); + + return lottieState; +} diff --git a/src/components/Lottie/ts/types.ts b/src/components/Lottie/ts/types.ts new file mode 100644 index 0000000..f4b088f --- /dev/null +++ b/src/components/Lottie/ts/types.ts @@ -0,0 +1,45 @@ +// Types +import type { Snippet } from 'svelte'; +import { + type Config, + type DotLottie as DotLottieType, +} from '@lottiefiles/dotlottie-web'; +import { type LottieState } from './lottieState.svelte'; +import type { ContainerWidth } from '../../@types/global'; + +type DotlottieProps = { + autoplay?: Config['autoplay']; + backgroundColor?: Config['backgroundColor']; + data?: Config['data']; + loop?: Config['loop']; + mode?: Config['mode']; + renderConfig?: Config['renderConfig']; + segment?: Config['segment']; + speed?: Config['speed']; + src: Config['src']; + useFrameInterpolation?: Config['useFrameInterpolation']; + marker?: Config['marker'] | undefined; + layout?: Config['layout']; + animationId?: Config['animationId']; + themeId?: Config['themeId']; + playOnHover?: boolean; + themeData?: string; + dotLottieRefCallback?: (dotLottie: DotLottieType) => void; + onLoad?: () => void; + onRender?: () => void; + onComplete?: () => void; +}; + +export type Props = DotlottieProps & { + // Additional properties can be added here if needed + lottiePlayer?: DotLottieType | undefined; + showDebugInfo?: boolean; + width?: string | ContainerWidth; + height?: string; + lottieState?: LottieState; + progress?: number; + tweenDuration?: number; + easing?: (t: number) => number; + /** Children render function */ + children?: Snippet; +}; diff --git a/src/components/Lottie/ts/utils.ts b/src/components/Lottie/ts/utils.ts new file mode 100644 index 0000000..dec2c2a --- /dev/null +++ b/src/components/Lottie/ts/utils.ts @@ -0,0 +1,127 @@ +import type { DotLottie } from '@lottiefiles/dotlottie-web'; +import type { LottieState } from './lottieState.svelte'; +import type { ContainerWidth } from '$lib/components/@types/global'; + +function constrain(n: number, low: number, high: number) { + return Math.max(Math.min(n, high), low); +} + +export function map( + n: number, + start1: number, + stop1: number, + start2: number, + stop2: number, + withinBounds: boolean = true +) { + const newval = ((n - start1) / (stop1 - start1)) * (stop2 - start2) + start2; + if (!withinBounds) { + return newval; + } + if (start2 < stop2) { + return constrain(newval, start2, stop2); + } else { + return constrain(newval, stop2, start2); + } +} + +/** + * Syncs the lottie player state with the component's lottie state + */ +export function syncLottieState( + lottiePlayer: DotLottie, + lottieState: LottieState +) { + lottieState.currentFrame = lottiePlayer.currentFrame; + lottieState.totalFrames = lottiePlayer.totalFrames; + lottieState.duration = lottiePlayer.duration; + lottieState.loop = lottiePlayer.loop; + lottieState.speed = lottiePlayer.speed; + lottieState.loopCount = lottiePlayer.loopCount; + lottieState.mode = lottiePlayer.mode; + lottieState.isPaused = lottiePlayer.isPaused; + lottieState.isPlaying = lottiePlayer.isPlaying; + lottieState.isStopped = lottiePlayer.isStopped; + lottieState.isLoaded = lottiePlayer.isLoaded; + lottieState.isFrozen = lottiePlayer.isFrozen; + lottieState.segment = lottiePlayer.segment ?? null; + lottieState.autoplay = lottiePlayer.autoplay ?? false; + lottieState.layout = lottiePlayer.layout ?? null; + lottieState.activeThemeId = lottiePlayer.activeThemeId ?? null; + lottieState.marker = lottiePlayer.marker ?? undefined; +} + +/** + * Gets marker info by name + */ +export function getMarkerByName(lottiePlayer: DotLottie, markerName: string) { + return lottiePlayer.markers().find((m) => m.name === markerName); +} + +/** + * Gets the start and end frames for a marker + */ +export function getMarkerRange( + lottiePlayer: DotLottie, + markerName: string +): [number, number] { + const marker = getMarkerByName(lottiePlayer, markerName); + const start = marker?.time ?? 0; + const end = start + (marker?.duration ?? 0); + return [start, end]; +} + +/** + * Calculates target frame based on progress and mode + */ +export function calculateTargetFrame( + progress: number, + mode: string, + start: number, + end: number +): number { + const adjustedProgress = + mode === 'reverse' || mode === 'reverse-bounce' ? 1 - progress : progress; + return map(adjustedProgress, 0, 1, start, end); +} + +/** + * Determines if mode is reverse + */ +export function isReverseMode(mode: string): boolean { + return mode === 'reverse' || mode === 'reverse-bounce'; +} + +/** + * Creates render config with optimized defaults + */ +export function createRenderConfig() { + return { + autoResize: true, + devicePixelRatio: + window.devicePixelRatio > 1 ? window.devicePixelRatio * 0.75 : 1, + freezeOnOffscreen: true, + }; +} + +/** + * Checks if a value is null or undefined (empty marker check) + */ +export function isNullish(value: unknown): boolean { + return value === null || value === undefined || value === ''; +} + +/** + * Checks if a value is of type ContainerWidth + */ +export function isContainerWidth(string: string): string is ContainerWidth { + return ( + string === 'narrower' || + string === 'narrow' || + string === 'normal' || + string === 'wide' || + string === 'wider' || + string === 'widest' || + string === 'fluid' + ); +} diff --git a/src/components/PhotoPack/PhotoPack.mdx b/src/components/PhotoPack/PhotoPack.mdx new file mode 100644 index 0000000..e5988a1 --- /dev/null +++ b/src/components/PhotoPack/PhotoPack.mdx @@ -0,0 +1,177 @@ +import { Meta, Canvas } from '@storybook/blocks'; + +import * as PhotoPackStories from './PhotoPack.stories.svelte'; + + + +# PhotoPack + +The `PhotoPack` component makes simple photo grids with custom layouts at various breakpoints. + +`images` are defined with their src, alt text, captions and an optional `maxHeight`, which ensures that an image is no taller than that height in any layout. + +```javascript +const images = [ + { + src: 'https://...', + altText: 'Alt text', + caption: 'Lorem ipsum. REUTERS/Photog', + // Optional max-height of images across all layouts + maxHeight: 800, + }, + // ... +]; +``` + +`layouts` optionally define how images are laid out at different breakpoints. You can customise the layouts and group images into `rows` above a certain `breakpoint` by specifying the number of images that should go in that row. For example: + +```javascript +const layouts = [ + { + breakpoint: 450, + rows: [1, 2, 1], + }, +]; +``` + +... tells the component that when the `PhotoPack` container is 450 pixels or wider, it should group the 4 images in 3 rows: 1 in the first, 2 in the second and 1 in the last. + +If you don't specify any layouts, the component will use a default responsive layout based on the number of images in your pack. + +You can define as many layouts for as many images as you like. + +```svelte + + + +``` + + + +## Using with ArchieML docs + +With the graphics kit, you'll likely get your text value from an ArchieML doc... + +```yaml +# ArchieML doc +[blocks] +type: photo-pack +id: my-photo-pack # Optional +class: mb-2 # Optional +width: wide # Optional +textWidth: normal # Optional +gap: 10 # Optional; must be a number. + +# Array of image metadata + [.images] + src: images/my-img-1.jpg + altText: Alt text + caption: Lorem ipsum. REUTERS/Photog + + src: images/my-img-2.jpg + altText: Alt text + caption: Lorem ipsum. REUTERS/Photog + + ... + [] + +[] +``` + +... which you'll parse out of a ArchieML block object before passing to the `PhotoPack` component. + +> **Important ❗**: The prop `gap` must be a number. ArchieML renders all values -- including numbers -- as strings, so convert the `prop` value to a number before passing it to `PhotoPack`. + +```svelte + + + +{#each content.blocks as block} + {#if block.type === 'photo-pack'} + + ({ + ...img, + src: `${assets}/${img.src}`, + }))} + layouts={[ + { breakpoint: 750, rows: [2, 3] }, + { breakpoint: 450, rows: [1, 2, 2] }, + ]} + /> + {/if} +{/each} +``` + + + +## Smart default layouts + +If you don't specify the `layouts` prop, `PhotoPack` will automatically generate responsive layouts based on the number of images and the container width. + +**How it works:** + +- **Desktop** (1024px+): Number of images per row depends on container width: + - `normal`: max 2 per row + - `wide` / `wider`: max 3 per row + - `widest` / `fluid`: max 4 per row +- **Tablet** (768px+): Always max 2 per row +- **Mobile** (below 768px): 1 per row + +The smart defaults use a **bottom-heavy distribution**, meaning earlier rows have fewer images (making them larger and more prominent), while later rows have more images. + +**Examples:** + +- 5 images, `wide` container, desktop: `[2, 3]` (2 in first row, 3 in second) +- 7 images, `widest` container, desktop: `[3, 4]` (3 in first row, 4 in second) +- 4 images, any container, desktop: `[2, 2]` (evenly distributed) + +```svelte + + + + +``` diff --git a/src/components/PhotoPack/PhotoPack.stories.svelte b/src/components/PhotoPack/PhotoPack.stories.svelte new file mode 100644 index 0000000..1eb8522 --- /dev/null +++ b/src/components/PhotoPack/PhotoPack.stories.svelte @@ -0,0 +1,140 @@ + + + + + + + + + {#snippet children(args)} + {@const { imageCount, ...photoPackProps } = args as SmartDefaultsArgs} + + {/snippet} + diff --git a/src/components/PhotoPack/PhotoPack.svelte b/src/components/PhotoPack/PhotoPack.svelte new file mode 100644 index 0000000..971b29a --- /dev/null +++ b/src/components/PhotoPack/PhotoPack.svelte @@ -0,0 +1,151 @@ + + + + +
+ {#each rows as row, ri} +
+ {#each row as img, i} +
+ {img.altText} + {#if !img.altText} +
altText
+ {/if} +
+ {/each} +
+ {/each} +
+ +
+ + {#each rows as row, ri} + {#each row as img, i} + {#if img.caption} +
+ +
+ {/if} + {/each} + {/each} +
+
+
+
+ + diff --git a/src/components/PhotoPack/utils.ts b/src/components/PhotoPack/utils.ts new file mode 100644 index 0000000..ed022ff --- /dev/null +++ b/src/components/PhotoPack/utils.ts @@ -0,0 +1,94 @@ +import type { Image, Layout } from './PhotoPack.svelte'; + +// Breakpoint constants for smart default layouts +export const DESKTOP_BREAKPOINT = 1024; +export const TABLET_BREAKPOINT = 768; + +/** + * Generates a smart layout for a given number of images with bottom-heavy distribution. + * Avoids single-image rows by redistributing when necessary. + * + * @param imageCount - Total number of images + * @param maxPerRow - Maximum images per row + * @param breakpoint - Breakpoint threshold for this layout + * @returns Layout object with rows array + */ +export const generateSmartLayout = ( + imageCount: number, + maxPerRow: number, + breakpoint: number +): Layout => { + // Handle edge cases + if (imageCount === 0) return { breakpoint, rows: [] }; + if (imageCount === 1) return { breakpoint, rows: [1] }; + + const fullRows = Math.floor(imageCount / maxPerRow); + const remainder = imageCount % maxPerRow; + + let rows: number[] = []; + + if (remainder === 0) { + // Perfect division: all rows have maxPerRow + rows = Array(fullRows).fill(maxPerRow); + } else { + // Bottom-heavy: smaller row at top, larger rows below + // This makes early images larger (fewer per row = bigger display size) + rows = [remainder, ...Array(fullRows).fill(maxPerRow)]; + } + + return { + breakpoint, + rows, + }; +}; + +type ContainerWidth = 'normal' | 'wide' | 'wider' | 'widest' | 'fluid'; + +/** + * Generates smart default layouts for desktop and tablet breakpoints. + * Mobile (below TABLET_BREAKPOINT) automatically shows 1 image per row. + * + * Max images per row by container width: + * - normal: 2 + * - wide/wider: 3 + * - widest/fluid: 4 + * + * @param imageCount - Total number of images + * @param width - Container width setting + * @returns Array of 2 layouts [desktop, tablet] + */ +export const generateDefaultLayouts = ( + imageCount: number, + width: ContainerWidth +): Layout[] => { + // Map container width to max images per row for desktop + const desktopMaxPerRow = + width === 'normal' ? 2 + : width === 'widest' || width === 'fluid' ? 4 + : 3; + + // Tablet always uses max 2 per row + const tabletMaxPerRow = 2; + + return [ + generateSmartLayout(imageCount, desktopMaxPerRow, DESKTOP_BREAKPOINT), + generateSmartLayout(imageCount, tabletMaxPerRow, TABLET_BREAKPOINT), + ]; +}; + +export const groupRows = (images: Image[], layout?: Layout) => { + // Default layout, one img per row + if (!layout) return images.map((img) => [img]); + // Otherwise, chunk into rows according to layout scheme + let i = 0; + const rows = []; + for (const rowLength of layout.rows) { + const row = []; + for (const imgI of [...Array(rowLength).keys()]) { + row.push(images[imgI + i]); + } + rows.push(row); + i += rowLength; + } + return rows; +}; diff --git a/src/components/Scroller/Background.svelte b/src/components/Scroller/Background.svelte new file mode 100644 index 0000000..7a1c599 --- /dev/null +++ b/src/components/Scroller/Background.svelte @@ -0,0 +1,51 @@ + + +{#each steps as step, i} + {#if showStep(i)} +
+ +
+ {/if} +{/each} + + diff --git a/src/components/Scroller/Embedded/Background.svelte b/src/components/Scroller/Embedded/Background.svelte new file mode 100644 index 0000000..b7bc244 --- /dev/null +++ b/src/components/Scroller/Embedded/Background.svelte @@ -0,0 +1,18 @@ + + + + + diff --git a/src/components/Scroller/Embedded/Foreground.svelte b/src/components/Scroller/Embedded/Foreground.svelte new file mode 100644 index 0000000..a1f451b --- /dev/null +++ b/src/components/Scroller/Embedded/Foreground.svelte @@ -0,0 +1,48 @@ + + +{#if step.foreground === '' || !step.foreground} + +
+ + {#if typeof step.altText === 'string'} +
+ +
+ {/if} +{:else if typeof step.foreground === 'string'} + +
+ +
+ + {#if typeof step.altText === 'string'} +
+ +
+ {/if} +
+{:else} +
+ +
+{/if} + + diff --git a/src/components/Scroller/Embedded/index.svelte b/src/components/Scroller/Embedded/index.svelte new file mode 100644 index 0000000..1148401 --- /dev/null +++ b/src/components/Scroller/Embedded/index.svelte @@ -0,0 +1,31 @@ + + +{#each steps as step, index} + + {#if embeddedLayout === 'bf'} + + + + {:else} + + + {/if} +{/each} diff --git a/src/components/Scroller/Foreground.svelte b/src/components/Scroller/Foreground.svelte new file mode 100644 index 0000000..e42912f --- /dev/null +++ b/src/components/Scroller/Foreground.svelte @@ -0,0 +1,64 @@ + + +{#each steps as step, i} +
+ {#if step.foreground === '' || !step.foreground} + +
+ {#if typeof step.altText === 'string'} +
+ +
+ {/if} + {:else} +
+ {#if typeof step.foreground === 'string'} + + {:else} + + {/if} +
+ {#if typeof step.altText === 'string'} +
+ +
+ {/if} + {/if} +
+{/each} + + diff --git a/src/components/Scroller/Scroller.mdx b/src/components/Scroller/Scroller.mdx new file mode 100644 index 0000000..9c2b9e1 --- /dev/null +++ b/src/components/Scroller/Scroller.mdx @@ -0,0 +1,279 @@ +import { Meta } from '@storybook/blocks'; + +import * as ScrollerStories from './Scroller.stories.svelte'; + + + +# Scroller + +The `Scroller` component creates a basic scrollytelling graphic with layout options. + +This component is designed to handle most common layouts for scrollytelling. To make something more complex, customise [ScrollerBase](?path=/story/components-graphics-scrollerbase--docs), which is a Svelte 5 version of the [svelte-scroller](https://github.com/sveltejs/svelte-scroller). + +[Demo](?path=/story/components-graphics-scroller--demo) + +```svelte + + + +``` + +## Using with ArchieML and ai2svelte + +[Demo](?path=/story/components-graphics-scroller--archie-ml) + +In your graphics kit project, import your ai2svelte graphics in `App.svelte` and add them to the `aiCharts` object: + +```svelte + + +``` + +Then add the following structure to your ArchieML doc, making sure that the names of your charts in the `aiCharts` object match the names of each step's `background` in the ArchieML doc: + +```yaml +# ArchieML doc +[blocks] +type: ai-scroller +id: my-map-scroller +width: fluid +foregroundPosition: right +stackBackground: true + +# Array of step objects + [.steps] + background: AiMap1 + foreground: #### Step 1 + + Here's where something happend. + :end + altText: A map showing the Upper West side in New York City. + + Can add paragraphs of alt text if you want to break up sentences. + :end + + background: AiMap2 + foreground: #### Step 2 + + Something happened on some street... + :end + altText: The same map now highlights 98th Street. + :end + + background: AiMap3 + foreground: #### Step 3 + + ... and now there are multiple protests. + :end + altText: The same map now highlights three locations near 98th Street where something particulary important happened. + :end + [] +[] +``` + +Then parse the relevant ArchieML block object before passing to the `Scroller` component. + +```svelte + +{#each content.blocks as block} + {#if block.type === 'ai-scroller'} + ({ + background: aiCharts[step.background], + backgroundProps: { assetsPath: assets || '/' }, + foreground: step.foreground, + altText: step.altText, + }))} + /> + {/if} +{/each} +``` + +> **Note:** Some props, like `stackBackground`, expect boolean values. If you're using the graphics kit, use the `truthy()` util function to convert a string value to a boolean. + +> **Note:** In the graphics kit, the image source paths in ai2svelte components have to be fixed by passing `assets` to each step object, like in the example above. + +## Custom foreground + +[Demo](?path=/story/components-graphics-scroller--custom-foreground) + +Instead of just text, you can use components as foregrounds, and optionally pass props to it. + +If you're customising your own foreground component, remember to add alt text that describes the background graphic. + +```svelte + + + +``` + +## Custom foreground with ArchieML + +[Demo](?path=/story/components-graphics-scroller--customforeground-archie-ml) + +You can use custom foreground components with ArchieML with a few additional steps. + +In your graphics kit project's `App.svelte`, import your custom foregroud components and add them to a `foregroundComponents` object, just as you import ai2svelte background graphics and add them to the `aiCharts` object: + +```svelte + + +``` + +Then add the following structure to your ArchieML doc, making sure that the names of your charts in the `aiCharts` and `foregroundComponents` objects match the names of each step's `background` and `foreground` in the ArchieML doc: + +```yaml +# ArchieML doc +[blocks] +type: ai-scroller +id: my-map-scroller +foregroundPosition: left +stackBackground: true + +# Array of step objects + [.steps] + background: AiMap1 + # You can still use a markdown string even if other step/s use a custom foreground component + foreground: #### Step 1 + + Here's where something happend. + :end + altText: A map showing the Upper West side in New York City. + :end + + background: AiMap2 + foreground: Foreground1 # The name of your custom foreground component + altText: The same map now highlights 98th Street. + :end + background: AiMap3 + foreground: #### Step 3 + + ... and now there are multiple protests. + :end + altText: The same map now highlights three locations near 98th Street where something particulary important happened. + :end + [] +[] +``` + +Then parse the relevant ArchieML block object before passing to the `Scroller` component. + +```svelte + +{#each content.blocks as block} + {#if block.type === 'ai-scroller'} + ({ + background: aiCharts[step.background], + backgroundProps: { assetsPath: assets || '/' }, + foreground: foregroundComponents[step.foreground] || step.foreground, + foregroundProps: { assetsPath: assets || '/' }, + altText: step.altText, + }))} + /> + {/if} +{/each} +``` + +> **Note:** You only need to pass `foregroundProps: { assetsPath: assets || '/' }` in the graphics kit if your foreground components are ai2svelte graphicss. diff --git a/src/components/Scroller/Scroller.stories.svelte b/src/components/Scroller/Scroller.stories.svelte new file mode 100644 index 0000000..ca233b3 --- /dev/null +++ b/src/components/Scroller/Scroller.stories.svelte @@ -0,0 +1,195 @@ + + + + + + + + ({ + background: step.background, + foreground: step.foreground, + altText: step.altText, + }))} + /> + + + + + + ({ + background: step.background, + foreground: step.foreground, + altText: step.altText, + }))} + /> + diff --git a/src/components/Scroller/Scroller.svelte b/src/components/Scroller/Scroller.svelte new file mode 100644 index 0000000..e82e4d7 --- /dev/null +++ b/src/components/Scroller/Scroller.svelte @@ -0,0 +1,193 @@ + + + +{#if !embedded} + + + {#snippet backgroundSnippet()} + + {/snippet} + {#snippet foregroundSnippet()} +
+ +
+ {/snippet} +
+
+{:else} + + + +{/if} + + diff --git a/src/components/Scroller/demo/components/ai2svelte/ai-foreground.svelte b/src/components/Scroller/demo/components/ai2svelte/ai-foreground.svelte new file mode 100644 index 0000000..6af40a5 --- /dev/null +++ b/src/components/Scroller/demo/components/ai2svelte/ai-foreground.svelte @@ -0,0 +1,112 @@ + + +
+ + {#if width && width >= 0} +
+
+
+
+

Likelihood of something happening

+
+ +
+

0-25%

+
+
+

50-75%

+
+
+

75-100%

+
+
+

25-50%

+
+
+ {/if} +
+ + + + + + diff --git a/src/components/Scroller/demo/components/ai2svelte/ai-scroller-1.svelte b/src/components/Scroller/demo/components/ai2svelte/ai-scroller-1.svelte new file mode 100644 index 0000000..df60d00 --- /dev/null +++ b/src/components/Scroller/demo/components/ai2svelte/ai-scroller-1.svelte @@ -0,0 +1,121 @@ + + + + +
+ + {#if width && width >= 1200} +
+
+
+
+ {/if} + + {#if width && width >= 930 && width < 1200} +
+
+
+
+ {/if} + + {#if width && width >= 660 && width < 930} +
+
+
+
+ {/if} + + {#if width && width >= 510 && width < 660} +
+
+
+
+ {/if} + + {#if width && width >= 0 && width < 510} +
+
+
+
+ {/if} +
+ + + + + + + diff --git a/src/components/Scroller/demo/components/ai2svelte/ai-scroller-2.svelte b/src/components/Scroller/demo/components/ai2svelte/ai-scroller-2.svelte new file mode 100644 index 0000000..18a5870 --- /dev/null +++ b/src/components/Scroller/demo/components/ai2svelte/ai-scroller-2.svelte @@ -0,0 +1,310 @@ + + + + +
+ + {#if width && width >= 1200} +
+
+
+
+

This thing here is

+

particularly important

+
+
+ {/if} + + {#if width && width >= 930 && width < 1200} +
+
+
+
+

This thing here is

+

particularly important

+
+
+ {/if} + + {#if width && width >= 660 && width < 930} +
+
+
+
+

This thing here is

+

particularly important

+
+
+ {/if} + + {#if width && width >= 510 && width < 660} +
+
+
+
+

This thing here is

+

particularly important

+
+
+ {/if} + + {#if width && width >= 0 && width < 510} +
+
+
+
+

This thing here is

+

particularly important

+
+
+ {/if} +
+ + + + + + diff --git a/src/components/Scroller/demo/components/ai2svelte/ai-scroller-3.svelte b/src/components/Scroller/demo/components/ai2svelte/ai-scroller-3.svelte new file mode 100644 index 0000000..33538e0 --- /dev/null +++ b/src/components/Scroller/demo/components/ai2svelte/ai-scroller-3.svelte @@ -0,0 +1,310 @@ + + + + +
+ + {#if width && width >= 1200} +
+
+
+
+

Something

+

happened here

+
+
+ {/if} + + {#if width && width >= 930 && width < 1200} +
+
+
+
+

Something

+

happened here

+
+
+ {/if} + + {#if width && width >= 660 && width < 930} +
+
+
+
+

Something

+

happened here

+
+
+ {/if} + + {#if width && width >= 510 && width < 660} +
+
+
+
+

Something

+

happened here

+
+
+ {/if} + + {#if width && width >= 0 && width < 510} +
+
+
+
+

Something

+

happened here

+
+
+ {/if} +
+ + + + + + diff --git a/src/components/Scroller/demo/components/ai2svelte/images/Body-issues-key-xs.png b/src/components/Scroller/demo/components/ai2svelte/images/Body-issues-key-xs.png new file mode 100644 index 0000000..e03e5e1 Binary files /dev/null and b/src/components/Scroller/demo/components/ai2svelte/images/Body-issues-key-xs.png differ diff --git a/src/components/Scroller/demo/components/ai2svelte/images/step-1-lg.png b/src/components/Scroller/demo/components/ai2svelte/images/step-1-lg.png new file mode 100644 index 0000000..de1888e Binary files /dev/null and b/src/components/Scroller/demo/components/ai2svelte/images/step-1-lg.png differ diff --git a/src/components/Scroller/demo/components/ai2svelte/images/step-1-md.png b/src/components/Scroller/demo/components/ai2svelte/images/step-1-md.png new file mode 100644 index 0000000..ead091c Binary files /dev/null and b/src/components/Scroller/demo/components/ai2svelte/images/step-1-md.png differ diff --git a/src/components/Scroller/demo/components/ai2svelte/images/step-1-sm.png b/src/components/Scroller/demo/components/ai2svelte/images/step-1-sm.png new file mode 100644 index 0000000..dada63c Binary files /dev/null and b/src/components/Scroller/demo/components/ai2svelte/images/step-1-sm.png differ diff --git a/src/components/Scroller/demo/components/ai2svelte/images/step-1-xl.png b/src/components/Scroller/demo/components/ai2svelte/images/step-1-xl.png new file mode 100644 index 0000000..55823fb Binary files /dev/null and b/src/components/Scroller/demo/components/ai2svelte/images/step-1-xl.png differ diff --git a/src/components/Scroller/demo/components/ai2svelte/images/step-1-xs.png b/src/components/Scroller/demo/components/ai2svelte/images/step-1-xs.png new file mode 100644 index 0000000..26e4449 Binary files /dev/null and b/src/components/Scroller/demo/components/ai2svelte/images/step-1-xs.png differ diff --git a/src/components/Scroller/demo/components/ai2svelte/images/step-2-lg.png b/src/components/Scroller/demo/components/ai2svelte/images/step-2-lg.png new file mode 100644 index 0000000..9619643 Binary files /dev/null and b/src/components/Scroller/demo/components/ai2svelte/images/step-2-lg.png differ diff --git a/src/components/Scroller/demo/components/ai2svelte/images/step-2-md.png b/src/components/Scroller/demo/components/ai2svelte/images/step-2-md.png new file mode 100644 index 0000000..37450dc Binary files /dev/null and b/src/components/Scroller/demo/components/ai2svelte/images/step-2-md.png differ diff --git a/src/components/Scroller/demo/components/ai2svelte/images/step-2-sm.png b/src/components/Scroller/demo/components/ai2svelte/images/step-2-sm.png new file mode 100644 index 0000000..e236862 Binary files /dev/null and b/src/components/Scroller/demo/components/ai2svelte/images/step-2-sm.png differ diff --git a/src/components/Scroller/demo/components/ai2svelte/images/step-2-xl.png b/src/components/Scroller/demo/components/ai2svelte/images/step-2-xl.png new file mode 100644 index 0000000..fa15744 Binary files /dev/null and b/src/components/Scroller/demo/components/ai2svelte/images/step-2-xl.png differ diff --git a/src/components/Scroller/demo/components/ai2svelte/images/step-2-xs.png b/src/components/Scroller/demo/components/ai2svelte/images/step-2-xs.png new file mode 100644 index 0000000..0fef62a Binary files /dev/null and b/src/components/Scroller/demo/components/ai2svelte/images/step-2-xs.png differ diff --git a/src/components/Scroller/demo/components/ai2svelte/images/step-3-lg.png b/src/components/Scroller/demo/components/ai2svelte/images/step-3-lg.png new file mode 100644 index 0000000..9e4f037 Binary files /dev/null and b/src/components/Scroller/demo/components/ai2svelte/images/step-3-lg.png differ diff --git a/src/components/Scroller/demo/components/ai2svelte/images/step-3-md.png b/src/components/Scroller/demo/components/ai2svelte/images/step-3-md.png new file mode 100644 index 0000000..91cfa1a Binary files /dev/null and b/src/components/Scroller/demo/components/ai2svelte/images/step-3-md.png differ diff --git a/src/components/Scroller/demo/components/ai2svelte/images/step-3-sm.png b/src/components/Scroller/demo/components/ai2svelte/images/step-3-sm.png new file mode 100644 index 0000000..876a388 Binary files /dev/null and b/src/components/Scroller/demo/components/ai2svelte/images/step-3-sm.png differ diff --git a/src/components/Scroller/demo/components/ai2svelte/images/step-3-xl.png b/src/components/Scroller/demo/components/ai2svelte/images/step-3-xl.png new file mode 100644 index 0000000..03e47dd Binary files /dev/null and b/src/components/Scroller/demo/components/ai2svelte/images/step-3-xl.png differ diff --git a/src/components/Scroller/demo/components/ai2svelte/images/step-3-xs.png b/src/components/Scroller/demo/components/ai2svelte/images/step-3-xs.png new file mode 100644 index 0000000..26223ca Binary files /dev/null and b/src/components/Scroller/demo/components/ai2svelte/images/step-3-xs.png differ diff --git a/src/components/Scroller/demo/components/basic/InteractiveForeground.svelte b/src/components/Scroller/demo/components/basic/InteractiveForeground.svelte new file mode 100644 index 0000000..769ec63 --- /dev/null +++ b/src/components/Scroller/demo/components/basic/InteractiveForeground.svelte @@ -0,0 +1,17 @@ + + +

Interactive step

+ +

The count is {count}

+ + diff --git a/src/components/Scroller/demo/components/basic/Step.svelte b/src/components/Scroller/demo/components/basic/Step.svelte new file mode 100644 index 0000000..d914ec8 --- /dev/null +++ b/src/components/Scroller/demo/components/basic/Step.svelte @@ -0,0 +1,18 @@ + + +
+ + diff --git a/src/components/ScrollerAnimate/Background.svelte b/src/components/ScrollerAnimate/Background.svelte new file mode 100644 index 0000000..8a37201 --- /dev/null +++ b/src/components/ScrollerAnimate/Background.svelte @@ -0,0 +1,51 @@ + + +{#each steps as step, i} + {#if showStep(i)} +
+ +
+ {/if} +{/each} + + diff --git a/src/components/ScrollerAnimate/Foreground.svelte b/src/components/ScrollerAnimate/Foreground.svelte new file mode 100644 index 0000000..e42912f --- /dev/null +++ b/src/components/ScrollerAnimate/Foreground.svelte @@ -0,0 +1,64 @@ + + +{#each steps as step, i} +
+ {#if step.foreground === '' || !step.foreground} + +
+ {#if typeof step.altText === 'string'} +
+ +
+ {/if} + {:else} +
+ {#if typeof step.foreground === 'string'} + + {:else} + + {/if} +
+ {#if typeof step.altText === 'string'} +
+ +
+ {/if} + {/if} +
+{/each} + + diff --git a/src/components/ScrollerBase/ScrollerBase.mdx b/src/components/ScrollerBase/ScrollerBase.mdx new file mode 100644 index 0000000..7d8da9a --- /dev/null +++ b/src/components/ScrollerBase/ScrollerBase.mdx @@ -0,0 +1,77 @@ +import { Meta } from '@storybook/blocks'; + +import * as ScrollerBaseStories from './ScrollerBase.stories.svelte'; + + + +# ScrollerBase + +The `ScrollerBase` component powers the [`Scroller` component](?path=/story/components-graphics-scroller--docs), which creates a basic storytelling graphic with preset layout options. `ScrollerBase` contains the bare minimum code necessary for a scrollytelling section, and allows for customisation beyond what the [`Scroller` component](?path=/story/components-graphics-scroller--docs) allows. + +`ScrollerBase` is a Svelte 5 version of the [svelte-scroller](https://github.com/sveltejs/svelte-scroller). + +> **Important❗:** Make sure the HTML element containing each foreground is a div with the class `step-foreground-container`. If you're modifying this to something else, pass the appropriate selector to the `query` prop. + +[Demo](?path=/story/components-graphics-scrollerbase--demo) + +```svelte + + + + {#snippet backgroundSnippet()} + +

+ Current step: {index + 1}/{count} +

+ + +

Offset in current step

+ + +

Total progress

+ + {/snippet} + {#snippet foregroundSnippet()} + +
Step 1
+
Step 2
+
Step 3
+
Step 4
+
Step 5
+ {/snippet} +
+ + +``` diff --git a/src/components/ScrollerBase/ScrollerBase.stories.svelte b/src/components/ScrollerBase/ScrollerBase.stories.svelte new file mode 100644 index 0000000..e8cbb78 --- /dev/null +++ b/src/components/ScrollerBase/ScrollerBase.stories.svelte @@ -0,0 +1,12 @@ + + + diff --git a/src/components/ScrollerBase/ScrollerBase.svelte b/src/components/ScrollerBase/ScrollerBase.svelte new file mode 100644 index 0000000..13a8c11 --- /dev/null +++ b/src/components/ScrollerBase/ScrollerBase.svelte @@ -0,0 +1,270 @@ + + + + + + + + + + + {@render backgroundSnippet()} + + + + + {@render foregroundSnippet()} + + + + diff --git a/src/components/ScrollerBase/demo/DraggableLabel.svelte b/src/components/ScrollerBase/demo/DraggableLabel.svelte new file mode 100644 index 0000000..34e7cd8 --- /dev/null +++ b/src/components/ScrollerBase/demo/DraggableLabel.svelte @@ -0,0 +1,95 @@ + + +
{ + const customEvent = event as unknown as { detail: { value: number } }; + value = customEvent.detail.value; + }} + role="slider" + aria-valuemin="0" + aria-valuemax="1" + aria-valuenow={value} + tabindex="0" +> +
+
+

{label}: {round(value)}

+
+ + diff --git a/src/components/ScrollerBase/demo/ScrollerDemo.svelte b/src/components/ScrollerBase/demo/ScrollerDemo.svelte new file mode 100644 index 0000000..485f685 --- /dev/null +++ b/src/components/ScrollerBase/demo/ScrollerDemo.svelte @@ -0,0 +1,82 @@ + + + + +
+ + {#snippet backgroundSnippet()} +

+ Current step: {index + 1}/{count} +

+ + +

Offset in current step

+ + +

Total progress

+ + {/snippet} + {#snippet foregroundSnippet()} +
Step 1
+
Step 2
+
Step 3
+
Step 4
+
Step 5
+ {/snippet} +
+
+ + + + + + + + diff --git a/src/components/ScrollerVideo/Debug.svelte b/src/components/ScrollerVideo/Debug.svelte new file mode 100644 index 0000000..b922f22 --- /dev/null +++ b/src/components/ScrollerVideo/Debug.svelte @@ -0,0 +1,222 @@ + + + + +
+
+ + CONSOLE + +
+

Source:

+

{componentState.generalData.src}

+ +

Progress:

+
+

{componentState.generalData.videoPercentage}

+
+
+
+
+ +

Framerate:

+

{componentState.generalData.frameRate}

+ +

Current time:

+

+ {componentState.generalData.currentTime}/{componentState.generalData + .totalTime} +

+ + {#if componentState.usingWebCodecs} +

Codec:

+

+ {componentState.framesData.codec} +

+ + +

Current frame:

+

+ {componentState.framesData.currentFrame}/{componentState.framesData + .totalFrames} +

+ {/if} + +

Will Autoplay?:

+

+ {componentState.willAutoPlay} +

+ + {#if componentState.willAutoPlay} +

Autoplaying:

+

+ {componentState.isAutoPlaying} +

+

Autoplay progress:

+
+

{componentState.autoplayProgress}

+
+
+
+
+ {/if} +
+
+
+ + diff --git a/src/components/ScrollerVideo/ScrollerVideo.mdx b/src/components/ScrollerVideo/ScrollerVideo.mdx new file mode 100644 index 0000000..82c0069 --- /dev/null +++ b/src/components/ScrollerVideo/ScrollerVideo.mdx @@ -0,0 +1,483 @@ +import { Meta } from '@storybook/blocks'; + +import * as ScrollerVideoStories from './ScrollerVideo.stories.svelte'; + + + +# ScrollerVideo + +The `ScrollerVideo` component creates interactive video experiences that respond to user scrolling. It is built on top of [ScrollyVideo.js](https://scrollyvideo.js.org/) and is designed to work seamlessly with Svelte. + +## Basic demo + +To use the `ScrollerVideo` component, import it and provide the video source. The scroll height defaults to `200lvh`, but you can adjust this to any valid CSS height value such as `1200px` or `200lvh` with the `height` prop. + +> 💡TIP: Use `lvh` or `svh` units instead of `vh` unit for the height, as [these units](https://www.w3.org/TR/css-values-4/#large-viewport-size) are more reliable on mobile or other devices where elements such as the address bar toggle between being shown and hidden. + +[Demo](?path=/story/components-graphics-scrollervideo--demo) + +```svelte + + + + +``` + +## Optimising videos + +When using the `ScrollerVideo` component, minimise the video file size and ensure that the video is encoded in a format that is widely supported across browsers. Videos encoded at higher frame rates (FPS) are bound to crash on mobile devices, so 24 FPS is recommended. + +If at any point your page crashes while using this component (happens often only on phone devices), it is likely due to the video being too large or encoded at a high frame rate. You could also try separate videos for desktop and phone devices to save quality for the desktop viewing experience. + +> 💡**TIP:** Set the `showDebugInfo` prop to `true` to see video encoding information + +To optimise your video for the web, you can use `ffmpeg` to convert the video to a suitable format. Here is an example terminal command that converts a video to H.264 format with a resolution of 720p and a frame rate of 24 FPS: + +```bash +npx ffmpeg -y -i .mp4 -c:v libx264 -movflags +faststart -crf 24 -r 24 -g 72 -vf scale=720:-1 -profile:v high -preset veryslow -pix_fmt yuv420p -color_primaries 1 -color_trc 1 -colorspace 1 -an .mp4 +``` + +Adjust the `-crf` value to control the quality. A lower `-crf` value means higher quality, with 20-24 being a generally good balance. The video framerate can be altered by using `-r` flag. Set keyframe intervals using the `-g` flag. It is advisable to keep it around 3 seconds (3 \* video framerate) for a good output. See [FFmpeg documentation](https://ffmpeg.org/ffmpeg.html) and [Testing Media Capabilities](https://cconcolato.github.io/media-mime-support/mediacapabilities.html) for more. + +## Setting custom width + +By default, the `ScrollerVideo` component takes up the full width of its container. To set a custom width, you can wrap the `ScrollerVideo` component in a `` with one of the acceptable `ContainerWidth` values. + +Further, it also allows you to set the `objectFit` property to control how the video should be resized to fit its container. The available options are `cover` and `contain`. The default value is `cover`. + +[Demo](?path=/story/components-graphics-scrollervideo--object-fit) + +```svelte + + + + + + +``` + +## Responsive videos + +To show different videos based on the screen width, use the `ScrollerVideo` component with conditional logic that uses a different video source depending on the [window width](https://svelte.dev/docs/svelte/svelte-window). + +[Demo](?path=/story/components-graphics-scrollervideo--responsive-videos) + +```svelte + + + + +{#if width < 600} + + +{:else if width < 1200} + + +{:else} + + +{/if} +``` + +## Embeds + +Setting `embedded` to `true` will turn `ScrollerVideo` into an embeddable version, where the video autoplays when the user scrolls upon it. Optionally, you can control the embed video behaviour by passing `embeddedProps` to control the autoplay `delay`, `threshold` for triggering autoplay, and the `duration` of the video. + +> 💡**TIP:** Another way to recreate the ScrollerVideo experience for embeds is to record the desktop screen with [Scroll Capture](https://chromewebstore.google.com/detail/scroll-capture/egmhoeaacclmanaimofoooiamhpkimkk?hl=en) while scrolling through the video and use that video instead as an HTML video component. + +[Demo](?path=/story/components-graphics-scrollervideo--embed) + +```svelte + + + +``` + +## Autoplay + +The `autoplay` option combines the autoplay and scrollytelling experience. If set to `true`, the video will start playing automatically when the component is mounted, but switch to scrollytelling when the user starts scrolling. The scroll height is calculated based on how much of the video remains, which means that if the user lets the video autoplay to near the end, the user would only have to scroll through a small height to get to the end. If the user lets the video autoplay to the end, there will be no scrolling effect. + +[Demo](?path=/story/components-graphics-scrollervideo--autoplay) + +```svelte + + + +``` + +## Time-based text foregrounds with ArchieML + +The `ScrollerVideo` component can also be used to display text as foregrounds at specific times in the video. To do so, use the `text` prop in `ScrollerVideoForeground` component. + +[Demo](?path=/story/components-graphics-scrollervideo--archie-ml-foregrounds) + +With the graphics kit, you'll likely get your text and prop values from an ArchieML doc... + +```yaml +# ArchieML doc + +[blocks] + type: scroller-video + id: alps-scroller + src: videos/alps.mp4 + height: 800lvh + + # Array of foregrounds + [.foregrounds] + startTime: 3 # When in the video to start showing the foreground + endTime: 7 # When to stop showing the foreground + width: normal # text container width + position: bottom center # Position of the text. Optional; defaults to 'center center'. Must be a combination of `top/bottom/center center/left/right` + backgroundColour: rgba(0, 0, 0, 0.8) # Optional; defaults to white + text: #### The Alps + The Alps stretch across eight countries: France, Switzerland, Italy, Monaco, Liechtenstein, Austria, Germany, and Slovenia, covering about 1,200 kilometers (750 miles). + :end + + startTime: 9 + endTime: 12 + width: normal + position: bottom center + backgroundColour: rgba(0, 0, 0, 0.8) + text: Mont Blanc, standing at 4,809 meters (15,777 feet), is the highest peak in the Alps and Western Europe, though there's ongoing debate between France and Italy about exactly where the summit lies. + :end + + startTime: 16 + endTime: 20 + width: normal + position: bottom center + backgroundColour: rgba(0, 0, 0, 0.8) + text: #### History + The Alps were formed around **65 million years** ago when the African and Eurasian tectonic plates collided, pushing the land upward. Over 14 million people live in the Alpine region, with tourism supporting approximately 120 million visitors annually. + :end + [] +[] +``` + +... which you'll parse out of a ArchieML block object before passing to the `ScrollerVideo` and `ScrollerVideoForeground` components. + +```svelte + + +{#each content.blocks as block} + + {#if block.type == 'scroller-video'} + + + {#each block.foregrounds as foreground} + + {/each} + + {/if} +{/each} +``` + +## Time-based component foregrounds with ArchieML + +The `ScrollerVideo` component can also be used to display components, such as `Headline` or ai2svelte files, as foregrounds at specific times in the video. To do so, use the `Foreground` prop in `ScrollerVideoForeground` component. + +> **IMPORTANT❗**: When layering ai2svelte files over a video, the aspect ratio of the ai2svelte graphics should match that of the video. If the ai2svelte graphic is responsive and has, for example, small, medium and large versions — which is generally the case — make sure to also render small, medium and large versions of the video at the appropriate screen sizes. See [Responsive videos](#responsive-videos) for more details. + +[Demo](?path=/story/components-graphics-scrollervideo--component-archie-ml-foregrounds) + +With the graphics kit, you'll likely get your text and prop values from an ArchieML doc... + +```yaml +# ArchieML doc + +# Headline +hed: Wind and waves +[authors] + * Jane Doe +[] +publishTime: 2020-01-01T00:00:00Z +startTime: 0 # When in the video to start showing the headline +endTime: 0.3 # When to stop showing the headline + +[blocks] + type: scroller-video + id: my-scroller-video + height: 800lvh + + # Adjust prop names as needed + srcSm: videos/my-video-sm.mp4 + srcMd: videos/my-video-md.mp4 + srcLg: videos/my-video-lg.mp4 + + # Array of foregrounds + [.foregrounds] + startTime: 0.3 # When in the video to start showing the foreground + endTime: 2.2 # When to stop showing the foreground + width: fluid # foreground container width + Foreground: Foreground1 # Name of the ai2svelte component to render + + startTime: 2.2 + endTime: 3.2 + width: fluid + Foreground: Foreground2 + + startTime: 3.2 + endTime: 4.5 + width: fluid + Foreground: Foreground3 + + startTime: 6.5 + endTime: 8 + width: fluid + Foreground: Foreground4 + [] +[] +``` + +... which you'll parse out of a ArchieML block object before passing to the `ScrollerVideo` and `ScrollerVideoForeground` components. + +```svelte + + + + + +{#each content.blocks as block} + {#if block.type == 'scroller-video'} + + {#snippet ScrollVideo(height: string, src: string)} + + + + + + + + {#each block.foregrounds as foreground} + + {/each} + + {/snippet} + + + {#if width < 600} + {@render ScrollVideo(block.height, `${assets}/${block.srcSm}`)} + {:else if width < 1200} + {@render ScrollVideo(block.height, `${assets}/${block.srcMd}`)} + {:else} + {@render ScrollVideo(block.height, `${assets}/${block.srcLg}`)} + {/if} + {/if} +{/each} +``` + +## Using with `ScrollerBase` + +The `ScrollerVideo` component can be used inside the [ScrollerBase](?path=/story/components-graphics-scrollerbase--docs) component to add foreground content. This allows for a foreground that scrolls up and down over the video, instead of fading in and out at specific times. + +> **Note**: To use `ScrollerVideo` with `ScrollerBase`, set `trackScroll` to `false` and pass the bindable prop `progress` from `ScrollerBase` as `videoPercentage` to `ScrollerVideo`. + +[Demo](?path=/story/components-graphics-scrollervideo--scroller-base) + +```svelte + + + + {#snippet backgroundSnippet()} + + + {/snippet} + {#snippet foregroundSnippet()} + +
+

Step 1

+
+
+

Step 2

+
+
+

Step 3

+
+ {/snippet} +
+ + +``` + +## Advanced usecases + +Using the methods attached to the bindable prop `scrollerVideo` allows for advanced customisation of the scroll video behaviour. For example, you can create a looping video that plays a specific section of the video repeatedly, or jump to a specific time in the video when the user scrolls to a certain point. + +This code below would make the video smoothly jump to the halfway point of the video. Setting `jump` to `true` will make the video jump to the specified percentage abruptly: + +```js +scrollerVideo.setVideoPercentage( + 0.5, // progress set to 50% + { + transitionSpeed: 12, // playback rate for the video + jump: false, // flag to change transition video abruptly + easing: (t) => t, // linear easing. Can also pass d3 easing functions - d3.easeLinear + } +); +``` + +> **Note**: When using these methods, it's recommended to set `trackScroll` to `false` to avoid video playback on scroll and pass functions to the `onReady` prop to ensure that the video is ready before calling any methods on it. + +Here is a demo that uses `ScrollerVideo` with `ScrollerBase` to make the video jump to the start or the end of the video depending on what step of the scroller the user is on. + +[Demo](?path=/story/components-graphics-scrollervideo--advanced) + +```svelte + + + + + {#snippet backgroundSnippet()} + + + {/snippet} + + + {#snippet foregroundSnippet()} +
+

Index {index}

+
+
+

Index {index}

+
+ {/snippet} +
+``` diff --git a/src/components/ScrollerVideo/ScrollerVideo.stories.svelte b/src/components/ScrollerVideo/ScrollerVideo.stories.svelte new file mode 100644 index 0000000..94d9a1e --- /dev/null +++ b/src/components/ScrollerVideo/ScrollerVideo.stories.svelte @@ -0,0 +1,197 @@ + + + + + + + + + + + + + + + + + + {#if width < 600} + + {:else if width < 1200} + + {:else} + + {/if} + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/ScrollerVideo/ScrollerVideo.svelte b/src/components/ScrollerVideo/ScrollerVideo.svelte new file mode 100644 index 0000000..3c1da95 --- /dev/null +++ b/src/components/ScrollerVideo/ScrollerVideo.svelte @@ -0,0 +1,299 @@ + + + + +{#snippet supportingElements()} + {#if scrollerVideo} + {#if showDebugInfo} +
+ +
+ {/if} + + + {#if children} + {@render children()} + {/if} + {/if} +{/snippet} + +{#if embedded} +
{ + if (scrollerVideo && embeddedContainer) { + let scrollProgress = + embeddedContainer.scrollTop / embeddedContainerScrollHeight; + scrollerVideo.setVideoPercentage(scrollProgress, { + jump: scrollProgress == 0, + easing: (t) => t, + }); + } + }} + > +
+
+ {@render supportingElements()} +
+
+
+{:else} +
+
+ {@render supportingElements()} +
+
+{/if} + + diff --git a/src/components/ScrollerVideo/ScrollerVideoForeground.svelte b/src/components/ScrollerVideo/ScrollerVideoForeground.svelte new file mode 100644 index 0000000..545432f --- /dev/null +++ b/src/components/ScrollerVideo/ScrollerVideoForeground.svelte @@ -0,0 +1,166 @@ + + + + {#if componentState.generalData.currentTime >= startTime && componentState.generalData.currentTime <= endTime} +
+ + {#if text} + +
+ +
+
+ + {:else if children} +
+ {@render children()} +
+ + {:else if Foreground} +
+ + + +
+ {/if} +
+ {/if} +
+ + diff --git a/src/components/ScrollerVideo/demo/AdvancedUsecases.svelte b/src/components/ScrollerVideo/demo/AdvancedUsecases.svelte new file mode 100644 index 0000000..6404978 --- /dev/null +++ b/src/components/ScrollerVideo/demo/AdvancedUsecases.svelte @@ -0,0 +1,84 @@ + + + + + {#snippet backgroundSnippet()} + + + {/snippet} + + + {#snippet foregroundSnippet()} +
+

Index {index}

+
+
+

Index {index}

+
+ {/snippet} +
+ + diff --git a/src/components/ScrollerVideo/demo/Embedded.svelte b/src/components/ScrollerVideo/demo/Embedded.svelte new file mode 100644 index 0000000..af9b4f9 --- /dev/null +++ b/src/components/ScrollerVideo/demo/Embedded.svelte @@ -0,0 +1,100 @@ + + + + + + + + + + + {#each scrollerVideoBlock.foregrounds as foreground} + + {/each} + + + + + + + + + diff --git a/src/components/ScrollerVideo/demo/WithAi2svelteForegrounds.svelte b/src/components/ScrollerVideo/demo/WithAi2svelteForegrounds.svelte new file mode 100644 index 0000000..9b0c8b9 --- /dev/null +++ b/src/components/ScrollerVideo/demo/WithAi2svelteForegrounds.svelte @@ -0,0 +1,119 @@ + + + + +{#snippet ScrollVideo(height: string, src: string)} + + + + + + {#each scrollerVideoBlock.foregrounds as foreground} + + {/each} + +{/snippet} + +{#if width < 600} + {@render ScrollVideo(scrollerVideoBlock.height, SM)} +{:else if width < 1200} + {@render ScrollVideo(scrollerVideoBlock.height, MD)} +{:else} + {@render ScrollVideo(scrollerVideoBlock.height, LG)} +{/if} + + diff --git a/src/components/ScrollerVideo/demo/WithScrollerBase.svelte b/src/components/ScrollerVideo/demo/WithScrollerBase.svelte new file mode 100644 index 0000000..9912fa7 --- /dev/null +++ b/src/components/ScrollerVideo/demo/WithScrollerBase.svelte @@ -0,0 +1,53 @@ + + + + {#snippet backgroundSnippet()} + + {/snippet} + {#snippet foregroundSnippet()} + +
+

Step 1

+
+
+

Step 2

+
+
+

Step 3

+
+ {/snippet} +
+ + diff --git a/src/components/ScrollerVideo/demo/WithTextForegrounds.svelte b/src/components/ScrollerVideo/demo/WithTextForegrounds.svelte new file mode 100644 index 0000000..46465c7 --- /dev/null +++ b/src/components/ScrollerVideo/demo/WithTextForegrounds.svelte @@ -0,0 +1,78 @@ + + + + {#each scrollerVideoBlock.foregrounds as foreground} + + {/each} + + + diff --git a/src/components/ScrollerVideo/demo/graphic/ai2svelte/ai-chart.svelte b/src/components/ScrollerVideo/demo/graphic/ai2svelte/ai-chart.svelte new file mode 100644 index 0000000..61bd743 --- /dev/null +++ b/src/components/ScrollerVideo/demo/graphic/ai2svelte/ai-chart.svelte @@ -0,0 +1,633 @@ + + + + +
+ + {#if width && width >= 0 && width < 510} +
+
+
+
+

Shake intensity

+
+
+

Light

+
+
+

Moderate

+
+
+

Cap-Haitien

+
+
+

Strong

+
+
+

Very strong

+
+
+

Gonaïves

+
+
+

Caribbean

+

Sea

+
+
+

HAITI

+
+
+

Jeremie

+
+
+

Port-au-Prince

+
+
+

Epicenter

+
+
+

Jacmel

+
+
+

Les Cayes

+
+
+

50 mi

+
+
+

Dominican

+

Republic

+
+
+

50 km

+
+
+ {/if} + + {#if width && width >= 510 && width < 660} +
+
+
+
+

Shake intensity

+
+
+

Light

+
+
+

Moderate

+
+
+

Cap-Haitien

+
+
+

Strong

+
+
+

Very strong

+
+
+

Gonaïves

+
+
+

Caribbean

+

Sea

+
+
+

HAITI

+
+
+

Jeremie

+
+
+

Port-au-Prince

+
+
+

Epicenter

+
+
+

Dominican

+

Republic

+
+
+

Jacmel

+
+
+

Les Cayes

+
+
+

50 mi

+
+
+

50 km

+
+
+ {/if} + + {#if width && width >= 660} +
+
+
+
+

Shake intensity

+
+
+

Light

+
+
+

Moderate

+
+
+

Cap-Haitien

+
+
+

Strong

+
+
+

Very strong

+
+
+

Gonaïves

+
+
+

Caribbean

+

Sea

+
+
+

HAITI

+
+
+

Dominican

+

Republic

+
+
+

Jeremie

+
+
+

Epicenter

+
+
+

Port-au-Prince

+
+
+

Jacmel

+
+
+

Les Cayes

+
+
+

50 mi

+
+
+

50 km

+
+
+ {/if} +
+ + + + + diff --git a/src/components/ScrollerVideo/demo/graphic/ai2svelte/annotation1.svelte b/src/components/ScrollerVideo/demo/graphic/ai2svelte/annotation1.svelte new file mode 100644 index 0000000..e69acb7 --- /dev/null +++ b/src/components/ScrollerVideo/demo/graphic/ai2svelte/annotation1.svelte @@ -0,0 +1,453 @@ + + +
+ + {#if width && width >= 0 && width < 510} +
+
+
+
+

Solid southwest swell

+
+
+

+ Tahitian local Vahine Fierro drops into a double overhead wave during + a recentcompetition. Timing and position are critical to make the + take-off and maximize scoring +

+

+

 

+
+
+

Shallow water hides

+

a sharp reef

+
+
+ {/if} + + {#if width && width >= 510 && width < 660} +
+
+
+
+

Wave forms from hitting reef, and

+

tube will track the line of the shelf

+
+
+

Solid southwest swell

+
+
+

Tahitian local Vahine Fierro drops into

+

a double overhead wave during a recent

+

competition. Timing and position are critical

+

to make the take-off and maximize scoring

+

+

 

+
+
+

Shallow water hides

+

a sharp reef

+
+
+ {/if} + + {#if width && width >= 660 && width < 930} +
+
+
+
+

Wave forms from hitting reef, and

+

tube will track the line of the shelf

+
+
+

Solid southwest swell

+
+
+

Tahitian local Vahine Fierro drops into

+

a double overhead wave during a recent

+

competition. Timing and position are critical

+

to make the take-off and maximize scoring

+

+

 

+
+
+

Shallow water hides

+

a sharp reef

+
+
+ {/if} + + {#if width && width >= 930 && width < 1200} +
+
+
+
+

Wave forms from hitting reef, and

+

tube will track the line of the shelf

+
+
+

Solid southwest swell

+
+
+

Tahitian local Vahine Fierro drops into

+

a double overhead wave during a recent

+

competition. Timing and position are critical

+

to make the take-off and maximize scoring

+

+

 

+
+
+

Shallow water hides

+

a sharp reef

+
+
+ {/if} + + {#if width && width >= 1200} +
+
+
+
+

Wave forms from hitting reef, and

+

tube will track the line of the shelf

+
+
+

Solid southwest swell

+
+
+

Tahitian local Vahine Fierro drops into

+

a double overhead wave during a recent

+

competition. Timing and position are critical

+

to make the take-off and maximize scoring

+

+

 

+
+
+

Shallow water hides

+

+ a sharp reef +

+
+
+ {/if} +
+ + + + + + diff --git a/src/components/ScrollerVideo/demo/graphic/ai2svelte/annotation2.svelte b/src/components/ScrollerVideo/demo/graphic/ai2svelte/annotation2.svelte new file mode 100644 index 0000000..73625d5 --- /dev/null +++ b/src/components/ScrollerVideo/demo/graphic/ai2svelte/annotation2.svelte @@ -0,0 +1,423 @@ + + +
+ + {#if width && width >= 0 && width < 510} +
+
+
+
+

The wave at Teahupo'o is

+

known to be very heavy

+

with a thick lip that can

+

easily knock a surfer off the

+

wave and onto the reef below

+
+
+

Surfer speed is up

+

to 35 km per hour

+

(20 mph)

+
+
+

One cubic meter

+

of water weighs

+

1,000 kg. (2,200 lb)

+
+
+

+ A surfer will adjust speed to stay in the tube by dragging a hand to + slow, or pumping the board to speed up +

+
+
+ {/if} + + {#if width && width >= 510 && width < 660} +
+
+
+
+ {/if} + + {#if width && width >= 660 && width < 930} +
+
+
+
+

The wave at Teahupo'o is

+

known to be very heavy

+

with a thick lip that can

+

easily knock a surfer off the

+

wave and onto the reef below

+
+
+

Surfer speed is up to

+

35 km per hour (20 mph)

+
+
+

A surfer will adjust speed to stay in

+

the tube by dragging a hand to slow,

+

or pumping the board to speed up

+
+
+

One cubic meter of water

+

weighs 1,000 kg. (2,200 lb)

+
+
+ {/if} + + {#if width && width >= 930 && width < 1200} +
+
+
+
+

The wave at Teahupo'o is

+

known to be very heavy

+

with a thick lip that can

+

easily knock a surfer off the

+

wave and onto the reef below

+
+
+

The wave at Teahupo'o is

+

known to be very heavy

+

with a thick lip that can

+

easily knock a surfer off the

+

wave and onto the reef below

+
+
+

Surfer speed is up to

+

35 km per hour (20 mph)

+
+
+

Surfer speed is up to

+

35 km per hour (20 mph)

+
+
+

A surfer will adjust speed to stay in

+

the tube by dragging a hand to slow,

+

or pumping the board to speed up

+
+
+

A surfer will adjust speed to stay in

+

the tube by dragging a hand to slow,

+

or pumping the board to speed up

+
+
+

One cubic meter of water

+

weighs 1,000 kg. (2,200 lb)

+
+
+

One cubic meter of water

+

weighs 1,000 kg. (2,200 lb)

+
+
+ {/if} + + {#if width && width >= 1200} +
+
+
+
+

The wave at Teahupo'o is

+

known to be very heavy

+

with a thick lip that can

+

easily knock a surfer off the

+

wave and onto the reef below

+
+
+

Surfer speed is up to

+

35 km per hour (20 mph)

+
+
+

A surfer will adjust speed to stay in

+

the tube by dragging a hand to slow,

+

or pumping the board to speed up

+
+
+

One cubic meter of water

+

weighs 1,000 kg. (2,200 lb)

+
+
+ {/if} +
+ + + + + + diff --git a/src/components/ScrollerVideo/demo/graphic/ai2svelte/annotation3.svelte b/src/components/ScrollerVideo/demo/graphic/ai2svelte/annotation3.svelte new file mode 100644 index 0000000..18dee42 --- /dev/null +++ b/src/components/ScrollerVideo/demo/graphic/ai2svelte/annotation3.svelte @@ -0,0 +1,431 @@ + + +
+ + {#if width && width >= 0 && width < 510} +
+
+
+
+

+ Fierro is envelopedin the barrel for over two seconds, which is a long + time in such an unstable environment +

+
+
+

+ In comptitions, more points are scored for being deep inside a barrel + which raises the chance the wave will close out on top of them. +

+
+
+ {/if} + + {#if width && width >= 510 && width < 660} +
+
+
+
+

+ Fierro is envelopedin the barrel for over two seconds, which is a long + time in such an unstable environment +

+
+
+

+ Surfers often say time distorts inside the barrel, slowing down and + that the crashing wave noise falls silent, asthey expereince a sense + of awe +

+
+
+

+ In comptitions, more points are scored for being deep inside a barrel + which raises the chance the wave will close out on top of them. +

+
+
+ {/if} + + {#if width && width >= 660 && width < 930} +
+
+
+
+

+ Fierro is envelopedin the barrel for over two seconds, which is a long + time in such an unstable environment +

+
+
+

+ Surfers often say time distorts inside the barrel, slowing down and + that the crashing wave noise falls silent, as they expereince a sense + of awe +

+
+
+

+ In comptitions, more points are scored for being deep inside a barrel + which raises the chance the wave will close out on top of them. +

+
+
+ {/if} + + {#if width && width >= 930 && width < 1200} +
+
+
+
+

+ Fierro is envelopedin the barrel for over two seconds, which is a long + time in such an unstable environment +

+
+
+

+ Surfers often say time distorts inside the barrel, slowing down and + that the crashing wave noise falls silent, as they expereince a sense + of awe +

+
+
+

+ In comptitions, more points are scored for being deep inside a barrel + which raises the chance the wave will close out on top of them. +

+
+
+ {/if} + + {#if width && width >= 1200} +
+
+
+
+

+ Fierro is envelopedin the barrel for over two seconds, which is a long + time in such an unstable environment +

+
+
+

 

+

Fierro is enveloped

+

 

+

in the barrel for over

+

 

+

2 seconds, which is a

+

 

+

long time in such an

+

 

+

unstable environment

+

 

+

Surfers often say time distorts inside

+

 

+

the barrel, slowing down and that the

+

 

+

crashing wave noise falls silent, as

+

 

+

they expereince a sense of awe

+

 

+

In comptitions, more points are

+

 

+

scored for being deep inside a

+

 

+

barrel which raises the chance

+

 

+

the wave will close out on top of them.

+
+
+

+ Surfers often say time distorts inside the barrel, slowing down and + that the crashing wave noise falls silent, as they expereince a sense + of awe +

+
+
+

+ In comptitions, more points are scored for being deep inside a barrel + which raises the chance the wave will close out on top of them. +

+
+
+ {/if} +
+ + + + + + diff --git a/src/components/ScrollerVideo/demo/graphic/ai2svelte/annotation4.svelte b/src/components/ScrollerVideo/demo/graphic/ai2svelte/annotation4.svelte new file mode 100644 index 0000000..9aba7b4 --- /dev/null +++ b/src/components/ScrollerVideo/demo/graphic/ai2svelte/annotation4.svelte @@ -0,0 +1,392 @@ + + +
+ + {#if width && width >= 0 && width < 510} +
+
+
+
+

The wave path is a short one, only about 100 yards long

+
+
+

+ Fierro exits the barrel with a score of 9.33 out of 10, as she was + deep in the tube, had good speed and went over the foam ball on her + exit +

+
+
+ {/if} + + {#if width && width >= 510 && width < 660} +
+
+
+
+

The wave path is a short one, only about 100 yards long

+
+
+

+ Fierro exits the barrel with a score of 9.33 out of 10, as she was + deep in the tube, had good speed and went over the foam ball on her + exit +

+
+
+

+ The wave heaves so quickly that as it forms, it pulls water off the + reef, creating a ride that is often below sea level - seen as the + surfer exits up and over the wave to higher water +

+
+
+ {/if} + + {#if width && width >= 660 && width < 930} +
+
+
+
+

The wave path is a short one, only about 100 yards long

+
+
+

+ Fierro exits the barrel with a score of 9.33 out of 10, as she was + deep in the tube, had good speed and went over the foam ball on her + exit +

+
+
+

+ The wave heaves so quickly that as it forms, it pulls water off the + reef, creating a ride that is often below sea level - seen as the + surfer exits up and over the wave to higher water +

+
+
+ {/if} + + {#if width && width >= 930 && width < 1200} +
+
+
+
+

The wave path is a short one,

+

only about 100 yards long

+
+
+

+ Fierro exits the barrel with a score of 9.33 out of 10, as she was + deep in the tube, had good speed and went over the foam ball on her + exit +

+
+
+

+ The wave heaves so quickly that as it forms, it pulls water off the + reef, creating a ride that is often below sea level - seen as the + surfer exits up and over the wave to higher water +

+
+
+ {/if} + + {#if width && width >= 1200} +
+
+
+
+

The wave path is a short one,

+

only about 100 yards long

+
+
+

Fierro exits the barrel with a score of

+

9.33 out of 10, as she was deep in the

+

tube, had good speed and went over

+

the foam ball on her exit

+
+
+

The wave heaves so quickly that as it forms,

+

+ it pulls water off the reef, creating a rid e that +

+

is often below sea level - seen as the surfer

+

exits up and over the wave to higher water

+
+
+ {/if} +
+ + + + + + diff --git a/src/components/ScrollerVideo/demo/graphic/imgs/ai-chart-md.png b/src/components/ScrollerVideo/demo/graphic/imgs/ai-chart-md.png new file mode 100644 index 0000000..7f62d44 Binary files /dev/null and b/src/components/ScrollerVideo/demo/graphic/imgs/ai-chart-md.png differ diff --git a/src/components/ScrollerVideo/demo/graphic/imgs/ai-chart-sm.png b/src/components/ScrollerVideo/demo/graphic/imgs/ai-chart-sm.png new file mode 100644 index 0000000..49c06f6 Binary files /dev/null and b/src/components/ScrollerVideo/demo/graphic/imgs/ai-chart-sm.png differ diff --git a/src/components/ScrollerVideo/demo/graphic/imgs/ai-chart-xs.png b/src/components/ScrollerVideo/demo/graphic/imgs/ai-chart-xs.png new file mode 100644 index 0000000..15d640d Binary files /dev/null and b/src/components/ScrollerVideo/demo/graphic/imgs/ai-chart-xs.png differ diff --git a/src/components/ScrollerVideo/demo/graphic/imgs/annotation1-lg.png b/src/components/ScrollerVideo/demo/graphic/imgs/annotation1-lg.png new file mode 100644 index 0000000..66a9255 Binary files /dev/null and b/src/components/ScrollerVideo/demo/graphic/imgs/annotation1-lg.png differ diff --git a/src/components/ScrollerVideo/demo/graphic/imgs/annotation1-md.png b/src/components/ScrollerVideo/demo/graphic/imgs/annotation1-md.png new file mode 100644 index 0000000..cf5353f Binary files /dev/null and b/src/components/ScrollerVideo/demo/graphic/imgs/annotation1-md.png differ diff --git a/src/components/ScrollerVideo/demo/graphic/imgs/annotation1-sm.png b/src/components/ScrollerVideo/demo/graphic/imgs/annotation1-sm.png new file mode 100644 index 0000000..7758da8 Binary files /dev/null and b/src/components/ScrollerVideo/demo/graphic/imgs/annotation1-sm.png differ diff --git a/src/components/ScrollerVideo/demo/graphic/imgs/annotation1-xl.png b/src/components/ScrollerVideo/demo/graphic/imgs/annotation1-xl.png new file mode 100644 index 0000000..35731fc Binary files /dev/null and b/src/components/ScrollerVideo/demo/graphic/imgs/annotation1-xl.png differ diff --git a/src/components/ScrollerVideo/demo/graphic/imgs/annotation1-xs.png b/src/components/ScrollerVideo/demo/graphic/imgs/annotation1-xs.png new file mode 100644 index 0000000..9621a6f Binary files /dev/null and b/src/components/ScrollerVideo/demo/graphic/imgs/annotation1-xs.png differ diff --git a/src/components/ScrollerVideo/demo/graphic/imgs/annotation2-lg.png b/src/components/ScrollerVideo/demo/graphic/imgs/annotation2-lg.png new file mode 100644 index 0000000..deae2d2 Binary files /dev/null and b/src/components/ScrollerVideo/demo/graphic/imgs/annotation2-lg.png differ diff --git a/src/components/ScrollerVideo/demo/graphic/imgs/annotation2-md.png b/src/components/ScrollerVideo/demo/graphic/imgs/annotation2-md.png new file mode 100644 index 0000000..5e45881 Binary files /dev/null and b/src/components/ScrollerVideo/demo/graphic/imgs/annotation2-md.png differ diff --git a/src/components/ScrollerVideo/demo/graphic/imgs/annotation2-sm.png b/src/components/ScrollerVideo/demo/graphic/imgs/annotation2-sm.png new file mode 100644 index 0000000..16441ea Binary files /dev/null and b/src/components/ScrollerVideo/demo/graphic/imgs/annotation2-sm.png differ diff --git a/src/components/ScrollerVideo/demo/graphic/imgs/annotation2-xl.png b/src/components/ScrollerVideo/demo/graphic/imgs/annotation2-xl.png new file mode 100644 index 0000000..dbf15ba Binary files /dev/null and b/src/components/ScrollerVideo/demo/graphic/imgs/annotation2-xl.png differ diff --git a/src/components/ScrollerVideo/demo/graphic/imgs/annotation2-xs.png b/src/components/ScrollerVideo/demo/graphic/imgs/annotation2-xs.png new file mode 100644 index 0000000..911411f Binary files /dev/null and b/src/components/ScrollerVideo/demo/graphic/imgs/annotation2-xs.png differ diff --git a/src/components/ScrollerVideo/demo/graphic/imgs/annotation3-lg.png b/src/components/ScrollerVideo/demo/graphic/imgs/annotation3-lg.png new file mode 100644 index 0000000..6c22553 Binary files /dev/null and b/src/components/ScrollerVideo/demo/graphic/imgs/annotation3-lg.png differ diff --git a/src/components/ScrollerVideo/demo/graphic/imgs/annotation3-md.png b/src/components/ScrollerVideo/demo/graphic/imgs/annotation3-md.png new file mode 100644 index 0000000..e536624 Binary files /dev/null and b/src/components/ScrollerVideo/demo/graphic/imgs/annotation3-md.png differ diff --git a/src/components/ScrollerVideo/demo/graphic/imgs/annotation3-sm.png b/src/components/ScrollerVideo/demo/graphic/imgs/annotation3-sm.png new file mode 100644 index 0000000..16441ea Binary files /dev/null and b/src/components/ScrollerVideo/demo/graphic/imgs/annotation3-sm.png differ diff --git a/src/components/ScrollerVideo/demo/graphic/imgs/annotation3-xl.png b/src/components/ScrollerVideo/demo/graphic/imgs/annotation3-xl.png new file mode 100644 index 0000000..4acb55b Binary files /dev/null and b/src/components/ScrollerVideo/demo/graphic/imgs/annotation3-xl.png differ diff --git a/src/components/ScrollerVideo/demo/graphic/imgs/annotation3-xs.png b/src/components/ScrollerVideo/demo/graphic/imgs/annotation3-xs.png new file mode 100644 index 0000000..875f692 Binary files /dev/null and b/src/components/ScrollerVideo/demo/graphic/imgs/annotation3-xs.png differ diff --git a/src/components/ScrollerVideo/demo/graphic/imgs/annotation4-lg.png b/src/components/ScrollerVideo/demo/graphic/imgs/annotation4-lg.png new file mode 100644 index 0000000..dc52b8b Binary files /dev/null and b/src/components/ScrollerVideo/demo/graphic/imgs/annotation4-lg.png differ diff --git a/src/components/ScrollerVideo/demo/graphic/imgs/annotation4-md.png b/src/components/ScrollerVideo/demo/graphic/imgs/annotation4-md.png new file mode 100644 index 0000000..7cdf4ba Binary files /dev/null and b/src/components/ScrollerVideo/demo/graphic/imgs/annotation4-md.png differ diff --git a/src/components/ScrollerVideo/demo/graphic/imgs/annotation4-sm.png b/src/components/ScrollerVideo/demo/graphic/imgs/annotation4-sm.png new file mode 100644 index 0000000..b220b1a Binary files /dev/null and b/src/components/ScrollerVideo/demo/graphic/imgs/annotation4-sm.png differ diff --git a/src/components/ScrollerVideo/demo/graphic/imgs/annotation4-xl.png b/src/components/ScrollerVideo/demo/graphic/imgs/annotation4-xl.png new file mode 100644 index 0000000..b70a645 Binary files /dev/null and b/src/components/ScrollerVideo/demo/graphic/imgs/annotation4-xl.png differ diff --git a/src/components/ScrollerVideo/demo/graphic/imgs/annotation4-xs.png b/src/components/ScrollerVideo/demo/graphic/imgs/annotation4-xs.png new file mode 100644 index 0000000..fcdde81 Binary files /dev/null and b/src/components/ScrollerVideo/demo/graphic/imgs/annotation4-xs.png differ diff --git a/src/components/ScrollerVideo/ts/ScrollerVideo.ts b/src/components/ScrollerVideo/ts/ScrollerVideo.ts new file mode 100644 index 0000000..acf4ba7 --- /dev/null +++ b/src/components/ScrollerVideo/ts/ScrollerVideo.ts @@ -0,0 +1,959 @@ +import { UAParser } from 'ua-parser-js'; +import videoDecoder from './videoDecoder'; +import { debounce, isScrollPositionAtTarget, map, constrain } from './utils'; +import { createComponentState, type ScrollerVideoState } from './state.svelte'; + +interface ScrollerVideoArgs { + src: string; + scrollerVideoContainer: HTMLElement | string; + objectFit?: string; + sticky?: boolean; + full?: boolean; + trackScroll?: boolean; + lockScroll?: boolean; + transitionSpeed?: number; + frameThreshold?: number; + useWebCodecs?: boolean; + onReady?: () => void; + onChange?: (percentage?: number) => void; + debug?: boolean; + autoplay?: boolean; + setVideoPercentage?: ( + percentage: number, + options?: TransitionOptions + ) => void; + resize?: () => void; +} + +export interface TransitionOptions { + jump: boolean; + transitionSpeed?: number; + easing?: ((progress: number) => number) | null; + autoplay?: boolean; +} + +/** + * ScrollerVideo class for scroll-driven or programmatic video playback with Svelte integration. + */ +class ScrollerVideo { + /** + * The container element for the video or canvas. + * @type {HTMLElement | null} + */ + container: HTMLElement | null; + /** + * The original container argument (element or string ID). + * @type {Element | string | undefined} + */ + scrollerVideoContainer: Element | string | undefined; + /** + * Video source URL. + * @type {string} + */ + src: string; + /** + * Speed of transitions. + * @type {number} + */ + transitionSpeed: number; + /** + * Threshold for frame transitions. + * @type {number} + */ + frameThreshold: number; + /** + * Whether to use WebCodecs for decoding. + * @type {boolean} + */ + useWebCodecs: boolean; + /** + * CSS object-fit property for video/canvas. + * @type {string} + */ + objectFit: string; + /** + * Whether to use sticky positioning. + * @type {boolean} + */ + sticky: boolean; + /** + * Whether to track scroll position. + * @type {boolean} + */ + trackScroll: boolean; + /** + * Callback when ready. + * @type {() => void} + */ + onReady: () => void; + /** + * Callback on scroll percentage change. + * @type {(percentage?: number) => void} + */ + onChange: (percentage?: number) => void; + /** + * Enable debug logging. + * @type {boolean} + */ + debug: boolean; + /** + * Enable autoplay. + * @type {boolean} + */ + autoplay: boolean; + /** + * The HTML video element. + * @type {HTMLVideoElement | undefined} + */ + video: HTMLVideoElement | undefined; + /** + * Current scroll/video percentage (0-1). + * @type {number} + */ + videoPercentage: number; + /** + * True if browser is Safari. + * @type {boolean} + */ + isSafari: boolean; + /** + * Current video time in seconds. + * @type {number} + */ + currentTime: number; + /** + * Target video time in seconds. + * @type {number} + */ + targetTime: number; + /** + * Canvas for rendering frames (if using WebCodecs). + * @type {HTMLCanvasElement | null} + */ + canvas: HTMLCanvasElement | null; + /** + * 2D context for the canvas. + * @type {CanvasRenderingContext2D | null} + */ + context: CanvasRenderingContext2D | null; + /** + * Decoded video frames (if using WebCodecs). + * @type {ImageBitmap[] | null} + */ + frames: ImageBitmap[] | null; + /** + * Video frame rate. + * @type {number} + */ + frameRate: number; + /** + * Target scroll position in pixels, if set. + * @type {number | null} + */ + targetScrollPosition: number | null = null; + /** + * Current frame index (if using WebCodecs). + * @type {number} + */ + currentFrame: number; + /** + * True if using WebCodecs for decoding. + * @type {boolean} + */ + usingWebCodecs: boolean; + /** + * Total video duration in seconds. + * @type {number} + */ + totalTime: number; + /** + * RequestAnimationFrame ID for transitions. + * @type {number | null} + */ + transitioningRaf: number | null; + /** + * State object for component-level state. + * @type {ScrollerVideoState} + */ + componentState: ScrollerVideoState; + /** + * Function to update scroll percentage (set in constructor). + * @type {((jump: boolean) => void) | undefined} + */ + updateScrollPercentage: ((jump: boolean) => void) | undefined; + /** + * Function to handle resize events (set in constructor). + * @type {(() => void) | undefined} + */ + resize: (() => void) | undefined; + + /** + * Creates a new ScrollerVideo instance. + * @param {ScrollerVideoArgs} args - The arguments for initialization. + */ + constructor({ + src, + scrollerVideoContainer, + objectFit = 'cover', + sticky = true, + full = true, + trackScroll = true, + lockScroll = true, + transitionSpeed = 8, + frameThreshold = 0.1, + useWebCodecs = true, + onReady = () => {}, + onChange = (_percentage?: number) => {}, + debug = false, + autoplay = false, + }: ScrollerVideoArgs) { + this.src = src; + this.scrollerVideoContainer = scrollerVideoContainer; + this.objectFit = objectFit; + this.sticky = sticky; + this.trackScroll = trackScroll; + this.transitionSpeed = transitionSpeed; + this.frameThreshold = frameThreshold; + this.useWebCodecs = useWebCodecs; + this.onReady = onReady; + this.onChange = onChange; + this.debug = debug; + this.autoplay = autoplay; + this.videoPercentage = 0; + this.isSafari = false; + this.currentTime = 0; + this.targetTime = 0; + this.canvas = null; + this.context = null; + this.container = null; + this.frames = null; + this.frameRate = 0; + this.currentTime = 0; // Saves the currentTime of the video, synced with this.video.currentTime + this.targetTime = 0; // The target time before a transition happens + this.canvas = null; // The canvas for drawing the frames decoded by webCodecs + this.context = null; // The canvas context + this.frames = []; // The frames decoded by webCodecs + this.frameRate = 0; // Calculation of frameRate so we know which frame to paint + this.currentFrame = 0; + this.videoPercentage = 0; + this.usingWebCodecs = false; // Whether we are using webCodecs + this.totalTime = 0; // The total time of the video, used for calculating percentage + this.transitioningRaf = null; + this.componentState = createComponentState(); + this.componentState.willAutoPlay = autoplay; + + // Save the container. If the container is a string we get the element + + if (scrollerVideoContainer && scrollerVideoContainer instanceof HTMLElement) + this.container = scrollerVideoContainer; + // otherwise it should better be an element + else if (typeof scrollerVideoContainer === 'string') { + this.container = document.getElementById(scrollerVideoContainer) || null; + if (!this.container) + throw new Error('scrollerVideoContainer must be a valid DOM object'); + } else { + throw new Error('scrollerVideoContainer must be a valid DOM object'); + } + + // Create the initial video object. Even if we are going to use webcodecs, + // we start with a paused video object + + this.video = document.createElement('video'); + this.video.src = src; + this.video.preload = 'auto'; + this.video.tabIndex = 0; + this.video.preload = 'auto'; + this.video.playsInline = true; + this.video.muted = true; + this.video.pause(); + this.video.load(); + + this.video.addEventListener( + 'canplaythrough', + () => { + this.onReady(); + if (this.autoplay && !this.useWebCodecs) { + this.autoplayScroll(); + } + }, + { once: true } + ); + + // Start the video percentage at 0 + this.videoPercentage = 0; + + // Adds the video to the container + this.container.appendChild(this.video); + + // Setting CSS properties for sticky + if (sticky) { + this.container.style.display = 'block'; + this.container.style.position = 'sticky'; + this.container.style.top = '0'; + } + + // Setting CSS properties for full + if (full) { + this.container.style.width = '100%'; + this.container.style.height = '100lvh'; + this.container.style.overflow = 'hidden'; + } + + // Setting CSS properties for cover + if (objectFit) this.setCoverStyle(this.video); + + // Detect webkit (safari), because webkit requires special attention + const browserEngine = new UAParser().getEngine(); + + this.isSafari = browserEngine.name === 'WebKit'; + if (debug && this.isSafari) console.info('Safari browser detected'); + + const debouncedScroll = debounce(() => { + window.requestAnimationFrame(() => { + this.setScrollPercent(this.videoPercentage); + }); + }, 100); + + // Add scroll listener for responding to scroll position + this.updateScrollPercentage = (jump = false) => { + // Used for internally setting the scroll percentage based on built-in listeners + let containerBoundingClientRect; + if ( + this.container && + this.container.parentNode && + (this.container.parentNode as Element).getBoundingClientRect + ) { + containerBoundingClientRect = ( + this.container.parentNode as Element + ).getBoundingClientRect(); + } else { + if (this.debug) { + console.error( + 'ScrollerVideo: container or parentNode is null or invalid.' + ); + } + return; + } + + // Calculate the current scroll percent of the video + let scrollPercent = + -containerBoundingClientRect.top / + (containerBoundingClientRect.height - window.innerHeight); + + // if autplay, trim the playing time to last locked video position + if (this.componentState.autoplayProgress > 0) { + scrollPercent = map( + scrollPercent, + 0, + 1, + this.componentState.autoplayProgress, + 1 + ); + } + + if (this.debug) { + console.info('ScrollerVideo scrolled to', scrollPercent); + } + + // toggle autoplaying state on manual intervention + if (this.componentState.isAutoPlaying && this.frames) { + if (this.debug) console.warn('Stopping autoplay due to manual scroll'); + + if (this.usingWebCodecs) { + this.componentState.autoplayProgress = parseFloat( + (this.currentFrame / this.frames.length).toFixed(4) + ); + } else { + this.componentState.autoplayProgress = parseFloat( + (this.currentTime / this.totalTime).toFixed(4) + ); + } + + this.componentState.isAutoPlaying = false; + } + + this.videoPercentage = scrollPercent; + + if (this.targetScrollPosition == null) { + this.setTargetTimePercent(scrollPercent, { jump }); + this.onChange(scrollPercent); + } else if (isScrollPositionAtTarget(this.targetScrollPosition)) { + this.targetScrollPosition = null; + } else if (lockScroll && this.targetScrollPosition != null) { + debouncedScroll(); + } + + this.updateDebugInfo(); + }; + + // Add our event listeners for handling changes to the window or scroll + if (this.trackScroll) { + window.addEventListener('scroll', () => { + if (this.updateScrollPercentage) { + this.updateScrollPercentage(false); + } + }); + + // Set the initial scroll percentage + this.video.addEventListener( + 'loadedmetadata', + () => { + if (this.updateScrollPercentage) { + this.updateScrollPercentage(true); + } + if (this.video) { + this.totalTime = this.video.duration; + } + this.setCoverStyle(this.canvas || this.video); + }, + { once: true } + ); + } else { + this.video.addEventListener( + 'loadedmetadata', + () => { + this.setTargetTimePercent(0, { jump: true }); + if (this.video) { + this.totalTime = this.video.duration; + } + this.setCoverStyle(this.canvas || this.video); + }, + { once: true } + ); + } + + // Add resize function + this.resize = () => { + if (this.debug) console.info('ScrollerVideo resizing...'); + // On resize, we need to reset the cover style + if (this.objectFit) this.setCoverStyle(this.canvas || this.video); + // Then repaint the canvas, if we are in useWebcodecs + this.paintCanvasFrame(Math.floor(this.currentTime * this.frameRate)); + }; + + window.addEventListener('resize', this.resize); + this.video.addEventListener('progress', this.resize); + + // Calls decode video to attempt webcodecs method + this.decodeVideo(); + this.updateDebugInfo(); + } + + /** + * Sets the currentTime of the video as a specified percentage of its total duration. + * + * @param percentage - The percentage of the video duration to set as the current time. + * @param options - Configuration options for adjusting the video playback. + * - autoplay: boolean - If true, the video will start playing immediately after setting the percentage. Default is false. + * - jump: boolean - If true, the video currentTime will jump directly to the specified percentage. If false, the change will be animated over time. + * - transitionSpeed: number - Defines the speed of the transition when `jump` is false. Represents the duration of the transition in milliseconds. Default is 8. + * - easing: (progress: number) => number - A function that defines the easing curve for the transition. It takes the progress ratio (a number between 0 and 1) as an argument and returns the eased value, affecting the playback speed during the transition. + */ + setVideoPercentage( + percentage: number, + options: TransitionOptions = { jump: false, transitionSpeed: 8 } + ) { + // Early termination if the video percentage is already at the percentage that is intended. + if (this.videoPercentage === percentage) return; + + if (this.transitioningRaf) { + window.cancelAnimationFrame(this.transitioningRaf); + } + + this.videoPercentage = percentage; + + this.onChange(percentage); + + if (this.trackScroll && !options.autoplay) { + this.setScrollPercent(percentage); + } + + this.setTargetTimePercent(percentage, options); + } + + /** + * Sets the style of the video or canvas to "cover" its container. + * @param {HTMLElement | HTMLCanvasElement | undefined} el - The element to style. + */ + setCoverStyle(el: HTMLElement | HTMLCanvasElement | undefined): void { + if (!el) { + if (this.debug) console.warn('No element to set cover style on'); + return; + } + + if (this.objectFit) { + el.style.position = 'absolute'; + el.style.top = '50%'; + el.style.left = '50%'; + el.style.transform = 'translate(-50%, -50%)'; + // el.style.minWidth = '101%'; + // el.style.minHeight = '101%'; + + // Gets the width and height of the container + const { width: containerWidth, height: containerHeight } = + this.container?.getBoundingClientRect() || { width: 0, height: 0 }; + + let width = 0, + height = 0; + + if (el instanceof HTMLVideoElement) { + width = el.videoWidth; + height = el.videoHeight; + } else if (el instanceof HTMLCanvasElement) { + width = el.width; + height = el.height; + } + + if (this.debug) + console.info('Container dimensions:', [ + containerWidth, + containerHeight, + ]); + if (this.debug) console.info('Element dimensions:', [width, height]); + + // Determines which axis needs to be 100% and which needs to be scaled + if (this.objectFit == 'cover') { + if (containerWidth / containerHeight > width / height) { + el.style.width = '100%'; + el.style.height = 'auto'; + } else { + el.style.height = '100%'; + el.style.width = 'auto'; + } + } else if (this.objectFit == 'contain') { + if (containerWidth / containerHeight > width / height) { + el.style.height = '100%'; + el.style.width = 'auto'; + } else { + el.style.width = '100%'; + el.style.height = 'auto'; + } + } + } + } + + /** + * Uses webCodecs to decode the video into frames. + * @returns {Promise} Resolves when decoding is complete. + */ + async decodeVideo(): Promise { + if (!this.useWebCodecs) { + if (this.debug) + console.warn('Cannot perform video decode: `useWebCodes` disabled'); + + return; + } + + if (!this.src) { + if (this.debug) + console.warn('Cannot perform video decode: no `src` found'); + + return; + } + + try { + await videoDecoder( + this.src, + (frame) => { + this.frames?.push(frame); + }, + this.debug + ).then((codec) => { + this.usingWebCodecs = true; + if (typeof codec == 'string') { + this.componentState.framesData.codec = codec; + } + }); + } catch (error) { + if (this.debug) + console.error('Error encountered while decoding video', error); + + // Remove all decoded frames if a failure happens during decoding + this.frames = []; + + // Force a video reload when videoDecoder fails + this.video?.load(); + } + + // If no frames, something went wrong + if (this.frames?.length === 0) { + if (this.debug) console.error('No frames were received from webCodecs'); + + this.onReady(); + return; + } + + // Calculate the frameRate based on number of frames and the duration + this.frameRate = + this.frames && this.video ? this.frames.length / this.video.duration : 0; + if (this.debug) + console.info( + 'Received', + this.frames?.length, + 'frames. Video frame rate:', + this.frameRate + ); + + // Remove the video and add the canvas + this.canvas = document.createElement('canvas'); + this.context = this.canvas.getContext('2d'); + + // Hide the video and add the canvas to the container + if (this.video) { + this.video.style.display = 'none'; + } + if (this.container) { + this.container.appendChild(this.canvas); + } + if (this.objectFit) this.setCoverStyle(this.canvas); + + // Paint our first frame + this.paintCanvasFrame(Math.floor(this.currentTime * this.frameRate)); + this.onReady(); + + if (this.autoplay) this.autoplayScroll(); + } + + /** + * Paints the frame to the canvas. + * @param {number} frameNum - The frame index to paint. + */ + paintCanvasFrame(frameNum: number): void { + if (!this.frames) { + if (this.debug) console.warn('No frames available to paint'); + return; + } + + // Get the frame and paint it to the canvas + const currFrame = this.frames[frameNum]; + this.currentFrame = frameNum; + + if (!this.canvas || !currFrame) { + return; + } + + if (this.debug) { + console.info('Painting frame', frameNum); + } + + // Make sure the canvas is scaled properly, similar to setCoverStyle + this.canvas.width = currFrame.width; + this.canvas.height = currFrame.height; + const { width, height } = this.container?.getBoundingClientRect() || { + width: 0, + height: 0, + }; + + if (this.objectFit == 'cover') { + if (width / height > currFrame.width / currFrame.height) { + this.canvas.style.width = '100%'; + this.canvas.style.height = 'auto'; + } else { + this.canvas.style.height = '100%'; + this.canvas.style.width = 'auto'; + } + } else if (this.objectFit == 'contain') { + if (width / height > currFrame.width / currFrame.height) { + this.canvas.style.height = '100%'; + this.canvas.style.width = 'auto'; + } else { + this.canvas.style.width = '100%'; + this.canvas.style.height = 'auto'; + } + } + + // Draw the frame to the canvas context + if (!this.context) { + if (this.debug) console.warn('No canvas context available to paint'); + return; + } + + this.context.drawImage(currFrame, 0, 0, currFrame.width, currFrame.height); + this.updateDebugInfo(); + } + + /** + * Transitions the video or the canvas to the proper frame. + * + * @param options - Configuration options for adjusting the video playback. + * - jump: boolean - If true, the video currentTime will jump directly to the specified percentage. If false, the change will be animated over time. + * - transitionSpeed: number - Defines the speed of the transition when `jump` is false. Represents the duration of the transition in milliseconds. Default is 8. + * - easing: (progress: number) => number - A function that defines the easing curve for the transition. It takes the progress ratio (a number between 0 and 1) as an argument and returns the eased value, affecting the playback speed during the transition. + */ + transitionToTargetTime({ + jump, + transitionSpeed = this.transitionSpeed, + easing = null, + }: TransitionOptions) { + if (!this.video) { + console.warn('No video found'); + return; + } + + if (this.debug) { + console.info( + 'Transitioning targetTime:', + this.targetTime, + 'currentTime:', + this.currentTime + ); + } + + const diff = this.targetTime - this.currentTime; + const distance = Math.abs(diff); + const duration = distance * 1000; + const isForwardTransition = diff > 0; + + const tick = ({ + startCurrentTime, + startTimestamp, + timestamp, + }: { + startCurrentTime: number; + startTimestamp: number; + timestamp: number; + }) => { + if (!this.video) { + console.warn('No video found during transition tick'); + return; + } + + const progress = (timestamp - startTimestamp) / duration; + + // if frameThreshold is too low to catch condition Math.abs(this.targetTime - this.currentTime) < this.frameThreshold + const hasPassedThreshold = + isForwardTransition ? + this.currentTime >= this.targetTime + : this.currentTime <= this.targetTime; + + if (this.componentState.isAutoPlaying) { + this.componentState.autoplayProgress = parseFloat( + (this.currentTime / this.totalTime).toFixed(4) + ); + } + + // If we are already close enough to our target, pause the video and return. + // This is the base case of the recursive function + if ( + isNaN(this.targetTime) || + // If the currentTime is already close enough to the targetTime + Math.abs(this.targetTime - this.currentTime) < this.frameThreshold || + hasPassedThreshold + ) { + this.video?.pause(); + + if (this.transitioningRaf) { + cancelAnimationFrame(this.transitioningRaf); + this.transitioningRaf = null; + } + + return; + } + + // Make sure we don't go out of time bounds + if (this.targetTime > this.video.duration) + this.targetTime = this.video.duration; + if (this.targetTime < 0) this.targetTime = 0; + + // How far forward we need to transition + const transitionForward = this.targetTime - this.currentTime; + const easedProgress = + easing && Number.isFinite(progress) ? easing(progress) : 0; + const easedCurrentTime = + isForwardTransition ? + startCurrentTime + + easedProgress * Math.abs(distance) * transitionSpeed + : startCurrentTime - + easedProgress * Math.abs(distance) * transitionSpeed; + + if (this.canvas) { + if (jump) { + // If jump, we go directly to the frame + this.currentTime = this.targetTime; + } else if (easedProgress) { + this.currentTime = easedCurrentTime; + } else { + this.currentTime += transitionForward / (256 / transitionSpeed); + } + + this.paintCanvasFrame(Math.floor(this.currentTime * this.frameRate)); + } else if (jump || this.isSafari || !isForwardTransition) { + // We can't use a negative playbackRate, so if the video needs to go backwards, + // We have to use the inefficient method of modifying currentTime rapidly to + // get an effect. + this.video.pause(); + + if (easedProgress) { + this.currentTime = easedCurrentTime; + } else { + this.currentTime += transitionForward / (64 / transitionSpeed); + } + + // If jump, we go directly to the frame + if (jump) { + this.currentTime = this.targetTime; + } + + this.video.currentTime = this.currentTime; + } else { + // Otherwise, we play the video and adjust the playbackRate to get a smoother + // animation effect. + const playbackRate = Math.max( + Math.min(transitionForward * 4, transitionSpeed, 16), + 1 + ); + if (this.debug) + console.info('ScrollerVideo playbackRate:', playbackRate); + + if (!isNaN(playbackRate)) { + this.video.playbackRate = playbackRate; + this.video.play(); + } + // Set the currentTime to the video's currentTime + this.currentTime = this.video.currentTime; + } + + // Recursively calls ourselves until the animation is done. + if (typeof requestAnimationFrame === 'function') { + this.transitioningRaf = requestAnimationFrame((currentTimestamp) => + tick({ + startCurrentTime, + startTimestamp, + timestamp: currentTimestamp, + }) + ); + } + }; + + if (typeof requestAnimationFrame === 'function') { + this.transitioningRaf = requestAnimationFrame((startTimestamp) => { + tick({ + startCurrentTime: this.currentTime, + startTimestamp, + timestamp: startTimestamp, + }); + }); + } + } + + /** + * Sets the currentTime of the video as a specified percentage of its total duration. + * + * @param percentage - The percentage of the video duration to set as the current time. + * @param options - Configuration options for adjusting the video playback. + * - jump: boolean - If true, the video currentTime will jump directly to the specified percentage. If false, the change will be animated over time. + * - transitionSpeed: number - Defines the speed of the transition when `jump` is false. Represents the duration of the transition in milliseconds. Default is 8. + * - easing: (progress: number) => number - A function that defines the easing curve for the transition. It takes the progress ratio (a number between 0 and 1) as an argument and returns the eased value, affecting the playback speed during the transition. + */ + setTargetTimePercent( + percentage: number, + options: TransitionOptions = { jump: false, transitionSpeed: 8 } + ) { + const targetDuration = + this.frames?.length && this.frameRate ? + this.frames.length / this.frameRate + : this.video?.duration || 0; + // The time we want to transition to + this.targetTime = Math.max(Math.min(percentage, 1), 0) * targetDuration; + + // If we are close enough, return early + if ( + !options.jump && + Math.abs(this.currentTime - this.targetTime) < this.frameThreshold + ) + return; + + // Play the video if we are in video mode + if (!this.canvas && !this.video?.paused) this.video?.play(); + + this.transitionToTargetTime(options); + } + + /** + * Simulate trackScroll programmatically (scrolls on page by percentage of video). + * @param {number} percentage - The percentage of the video to scroll to. + */ + setScrollPercent(percentage: number) { + if (!this.trackScroll) { + console.warn('`setScrollPercent` requires enabled `trackScroll`'); + return; + } + + const parent = this.container?.parentNode; + let top = 0, + height = 0; + + if (parent && parent instanceof Element) { + const rect = parent.getBoundingClientRect(); + top = rect.top; + height = rect.height; + } + + const startPoint = top + window.pageYOffset; + + const containerHeightInViewport = height - window.innerHeight; + const targetPosition = startPoint + containerHeightInViewport * percentage; + + if (isScrollPositionAtTarget(targetPosition)) { + this.targetScrollPosition = null; + } else { + window.scrollTo({ top: targetPosition, behavior: 'smooth' }); + this.targetScrollPosition = targetPosition; + } + } + + /** + * Call to destroy this ScrollerVideo object. + */ + destroy() { + if (this.debug) console.info('Destroying ScrollerVideo'); + + if (this.trackScroll && this.updateScrollPercentage) + window.removeEventListener('scroll', () => this.updateScrollPercentage); + + if (this.resize) { + window.removeEventListener('resize', this.resize); + } + + // Clear component + if (this.container) this.container.innerHTML = ''; + } + + /** + * Autoplay the video by scrolling to the end. + */ + autoplayScroll() { + this.setVideoPercentage(1, { + jump: false, + transitionSpeed: this.totalTime * 0.1, + easing: (i) => i, + autoplay: true, + }); + this.componentState.isAutoPlaying = true; + } + + /** + * Updates debug information in the component state. + */ + updateDebugInfo() { + this.componentState.generalData.src = this.src; + this.componentState.generalData.videoPercentage = constrain( + parseFloat(this.videoPercentage.toFixed(4)), + 0, + 1 + ); + this.componentState.generalData.frameRate = parseFloat( + this.frameRate.toFixed(2) + ); + this.componentState.generalData.currentTime = parseFloat( + this.currentTime.toFixed(4) + ); + this.componentState.generalData.totalTime = parseFloat( + this.totalTime.toFixed(4) + ); + this.componentState.usingWebCodecs = this.usingWebCodecs; + this.componentState.framesData.currentFrame = this.currentFrame; + this.componentState.framesData.totalFrames = this.frames?.length || 0; + } +} +export default ScrollerVideo; diff --git a/src/components/ScrollerVideo/ts/mp4box.d.ts b/src/components/ScrollerVideo/ts/mp4box.d.ts new file mode 100644 index 0000000..d5d9ed9 --- /dev/null +++ b/src/components/ScrollerVideo/ts/mp4box.d.ts @@ -0,0 +1,137 @@ +declare module 'mp4box' { + export interface MP4MediaTrack { + id: number; + created: Date; + modified: Date; + movie_duration: number; + movie_timescale: number; + layer: number; + alternate_group: number; + volume: number; + track_width: number; + track_height: number; + timescale: number; + duration: number; + bitrate: number; + codec: string; + language: string; + nb_samples: number; + } + + export interface MP4VideoData { + width: number; + height: number; + } + + export interface MP4VideoTrack extends MP4MediaTrack { + video: MP4VideoData; + } + + export interface MP4AudioData { + sample_rate: number; + channel_count: number; + sample_size: number; + } + + export interface MP4AudioTrack extends MP4MediaTrack { + audio: MP4AudioData; + } + + export type MP4Track = MP4VideoTrack | MP4AudioTrack; + + export interface MP4Info { + duration: number; + timescale: number; + fragment_duration: number; + isFragmented: boolean; + isProgressive: boolean; + hasIOD: boolean; + brands: string[]; + created: Date; + modified: Date; + tracks: MP4Track[]; + audioTracks: MP4AudioTrack[]; + videoTracks: MP4VideoTrack[]; + } + + export interface MP4Sample { + alreadyRead: number; + chunk_index: number; + chunk_run_index: number; + cts: number; + data: Uint8Array; + degradation_priority: number; + depends_on: number; + description: unknown; + description_index: number; + dts: number; + duration: number; + has_redundancy: number; + is_depended_on: number; + is_leading: number; + is_sync: boolean; + number: number; + offset: number; + size: number; + timescale: number; + track_id: number; + } + + export type MP4ArrayBuffer = ArrayBuffer & { fileStart: number }; + + export class DataStream { + static BIG_ENDIAN: boolean; + static LITTLE_ENDIAN: boolean; + buffer: ArrayBuffer; + constructor( + arrayBuffer?: ArrayBuffer, + byteOffset: number, + endianness: boolean + ): void; + // TODO: Complete interface + } + + export interface Trak { + mdia?: { + minf?: { + stbl?: { + stsd?: { + entries: { + avcC?: { + write: (stream: DataStream) => void; + }; + hvcC?: { + write: (stream: DataStream) => void; + }; + }[]; + }; + }; + }; + }; + // TODO: Complete interface + } + + export interface MP4File { + [x: string]: unknown; + onMoovStart?: () => void; + onReady?: (info: MP4Info) => void; + onError?: (e: string) => void; + onSamples?: (id: number, user: unknown, samples: MP4Sample[]) => unknown; + + appendBuffer(data: MP4ArrayBuffer): number; + start(): void; + stop(): void; + flush(): void; + releaseUsedSamples(trackId: number, sampleNumber: number): void; + setExtractionOptions( + trackId: number, + user?: unknown, + options?: { nbSamples?: number; rapAlignment?: number } + ): void; + getTrackById(trackId: number): Trak; + } + + export function createFile(): MP4File; + + export {}; +} diff --git a/src/components/ScrollerVideo/ts/state.svelte.ts b/src/components/ScrollerVideo/ts/state.svelte.ts new file mode 100644 index 0000000..e873790 --- /dev/null +++ b/src/components/ScrollerVideo/ts/state.svelte.ts @@ -0,0 +1,74 @@ +/** + * General video data for ScrollerVideo state. + * @typedef {Object} GeneralData + * @property {string} src - Video source URL. + * @property {number} videoPercentage - Current video percentage (0-1). + * @property {number} frameRate - Video frame rate. + * @property {number} currentTime - Current video time in seconds. + * @property {number} totalTime - Total video duration in seconds. + */ +type GeneralData = { + src: string; + videoPercentage: number; + frameRate: number; + currentTime: number; + totalTime: number; +}; + +/** + * Frame-level data for ScrollerVideo state. + * @typedef {Object} FramesData + * @property {string} codec - Video codec string. + * @property {number} currentFrame - Current frame index. + * @property {number} totalFrames - Total number of frames. + */ +type FramesData = { + codec: string; + currentFrame: number; + totalFrames: number; +}; + +/** + * State object for ScrollerVideo component. + * @typedef {Object} ScrollerVideoState + * @property {GeneralData} generalData - General video data. + * @property {boolean} usingWebCodecs - Whether WebCodecs is used. + * @property {FramesData} framesData - Frame-level data. + * @property {boolean} isAutoPlaying - Whether video is autoplaying. + * @property {number} autoplayProgress - Progress of autoplay (0-1). + */ +export type ScrollerVideoState = { + generalData: GeneralData; + usingWebCodecs: boolean; + framesData: FramesData; + willAutoPlay: boolean; + isAutoPlaying: boolean; + autoplayProgress: number; +}; + +/** + * Creates a new ScrollerVideoState object with default values. + * @returns {ScrollerVideoState} The initialized state object. + */ +export function createComponentState(): ScrollerVideoState { + const scrollerVideoState = $state({ + generalData: { + src: '', + videoPercentage: 0, + frameRate: 0, + currentTime: 0, + totalTime: 0, + }, + usingWebCodecs: false, + framesData: { + codec: '', + currentFrame: 0, + totalFrames: 0, + }, + willAutoPlay: false, + isAutoPlaying: false, + autoplayProgress: 0, + }); + + return scrollerVideoState; +} diff --git a/src/components/ScrollerVideo/ts/utils.ts b/src/components/ScrollerVideo/ts/utils.ts new file mode 100644 index 0000000..079415f --- /dev/null +++ b/src/components/ScrollerVideo/ts/utils.ts @@ -0,0 +1,146 @@ +import type { ScrollerVideoState } from './state.svelte'; + +/** + * Flattened version of ScrollerVideoState for easier access to all properties. + * @typedef {Object} FlattenedScrollerVideoState + * @property {string} src - Video source URL. + * @property {number} videoPercentage - Current video percentage (0-1). + * @property {number} frameRate - Video frame rate. + * @property {number} currentTime - Current video time in seconds. + * @property {number} totalTime - Total video duration in seconds. + * @property {boolean} usingWebCodecs - Whether WebCodecs is used. + * @property {string} codec - Video codec string. + * @property {number} currentFrame - Current frame index. + * @property {number} totalFrames - Total number of frames. + * @property {boolean} isAutoPlaying - Whether video is autoplaying. + * @property {number} autoplayProgress - Progress of autoplay (0-1). + */ +type FlattenedScrollerVideoState = { + src: string; + videoPercentage: number; + frameRate: number; + currentTime: number; + totalTime: number; + usingWebCodecs: boolean; + codec: string; + currentFrame: number; + totalFrames: number; + isAutoPlaying: boolean; + autoplayProgress: number; +}; + +/** + * Returns a debounced version of the given function. + * @template T + * @param {T} func - The function to debounce. + * @param {number} [delay=0] - The debounce delay in milliseconds. + * @returns {(...args: Parameters) => void} The debounced function. + */ +export function debounce void>( + func: T, + delay = 0 +) { + let timeoutId: ReturnType | undefined; + + return (...args: Parameters) => { + clearTimeout(timeoutId); + timeoutId = setTimeout(() => { + func(...args); + }, delay); + }; +} + +/** + * Checks if the current scroll position is at the target position within a threshold. + * @param {number} targetScrollPosition - The target scroll position in pixels. + * @param {number} [threshold=1] - The allowed threshold in pixels. + * @returns {boolean} True if the current scroll position is within the threshold of the target. + */ +export const isScrollPositionAtTarget = ( + targetScrollPosition: number, + threshold: number = 1 +) => { + const currentScrollPosition = window.pageYOffset; + const difference = Math.abs(currentScrollPosition - targetScrollPosition); + + return difference < threshold; +}; + +/** + * Constrains a number between a lower and upper bound. + * @param {number} n - The number to constrain. + * @param {number} low - The lower bound. + * @param {number} high - The upper bound. + * @returns {number} The constrained value. + */ +export function constrain(n: number, low: number, high: number): number { + return Math.max(Math.min(n, high), low); +} + +/** + * Maps a number from one range to another. + * @param {number} n - The number to map. + * @param {number} start1 - Lower bound of the value's current range. + * @param {number} stop1 - Upper bound of the value's current range. + * @param {number} start2 - Lower bound of the value's target range. + * @param {number} stop2 - Upper bound of the value's target range. + * @param {boolean} [withinBounds=true] - Whether to constrain the result within the target range. + * @returns {number} The mapped value. + */ +export function map( + n: number, + start1: number, + stop1: number, + start2: number, + stop2: number, + withinBounds: boolean = true +): number { + const newval = ((n - start1) / (stop1 - start1)) * (stop2 - start2) + start2; + if (!withinBounds) { + return newval; + } + if (start2 < stop2) { + return constrain(newval, start2, stop2); + } else { + return constrain(newval, stop2, start2); + } +} + +/** + * Flattens a ScrollerVideoState object into a single-level object for easier access. + * @param {ScrollerVideoState} obj - The state object to flatten. + * @returns {FlattenedScrollerVideoState} The flattened state object. + */ +export function flattenObject( + obj: ScrollerVideoState +): FlattenedScrollerVideoState { + const result: { [key: string]: unknown } = {}; + + function flatten(current: string | unknown[] | object, property: string) { + if (Object(current) !== current) { + result[property] = current; + } else if (Array.isArray(current)) { + for (let i = 0, l = current.length; i < l; i++) { + flatten(current[i], property + '[' + i + ']'); + if (l === 0) { + result[property] = []; + } + } + } else if (typeof current === 'object') { + let isEmpty = true; + for (const p in current) { + isEmpty = false; + flatten( + (current as { [key: string]: string | object | unknown[] })[p], + p + ); + } + if (isEmpty && property) { + result[property] = {}; + } + } + } + + flatten(obj, ''); + return result as FlattenedScrollerVideoState; +} diff --git a/src/components/ScrollerVideo/ts/videoDecoder.ts b/src/components/ScrollerVideo/ts/videoDecoder.ts new file mode 100644 index 0000000..d9758d4 --- /dev/null +++ b/src/components/ScrollerVideo/ts/videoDecoder.ts @@ -0,0 +1,299 @@ +import { createFile } from 'mp4box'; + +interface MP4BoxBuffer extends ArrayBuffer { + fileStart: number; +} + +/** + * Taken from https://github.com/w3c/webcodecs/blob/main/samples/mp4-decode/mp4_demuxer.js + */ +class Writer { + data: Uint8Array; + idx: number; + size: number; + + constructor(size: number) { + this.data = new Uint8Array(size); + this.idx = 0; + this.size = size; + } + + getData(): Uint8Array { + if (this.idx !== this.size) + throw new Error('Mismatch between size reserved and sized used'); + return this.data.slice(0, this.idx); + } + + writeUint8(value: number): void { + this.data.set([value], this.idx); + this.idx += 1; + } + + writeUint16(value: number): void { + const arr = new Uint16Array(1); + arr[0] = value; + const buffer = new Uint8Array(arr.buffer); + this.data.set([buffer[1], buffer[0]], this.idx); + this.idx += 2; + } + + writeUint8Array(value: number[]): void { + this.data.set(value, this.idx); + this.idx += value.length; + } +} + +/** + * Taken from https://github.com/w3c/webcodecs/blob/main/samples/mp4-decode/mp4_demuxer.js + * + * @param avccBox + * @returns {*} + */ +interface NALUnit { + length: number; + nalu: number[]; +} + +const getExtradata = (avccBox: { + SPS: NALUnit[]; + PPS: NALUnit[]; + configurationVersion: number; + AVCProfileIndication: number; + profile_compatibility: number; + AVCLevelIndication: number; + lengthSizeMinusOne: number; + nb_SPS_nalus: number; + nb_PPS_nalus: number; +}) => { + let i; + let size = 7; + for (i = 0; i < avccBox.SPS.length; i += 1) { + // nalu length is encoded as a uint16. + size += 2 + (avccBox.SPS[i] as { length: number }).length; + } + for (i = 0; i < avccBox.PPS.length; i += 1) { + // nalu length is encoded as a uint16. + size += 2 + (avccBox.PPS[i] as { length: number }).length; + } + + const writer = new Writer(size); + + writer.writeUint8(avccBox.configurationVersion); + writer.writeUint8(avccBox.AVCProfileIndication); + writer.writeUint8(avccBox.profile_compatibility); + writer.writeUint8(avccBox.AVCLevelIndication); + + writer.writeUint8(avccBox.lengthSizeMinusOne + (63 << 2)); + + writer.writeUint8(avccBox.nb_SPS_nalus + (7 << 5)); + for (i = 0; i < avccBox.SPS.length; i += 1) { + writer.writeUint16(avccBox.SPS[i].length); + writer.writeUint8Array(avccBox.SPS[i].nalu); + } + + writer.writeUint8(avccBox.nb_PPS_nalus); + for (i = 0; i < avccBox.PPS.length; i += 1) { + writer.writeUint16(avccBox.PPS[i].length); + writer.writeUint8Array(avccBox.PPS[i].nalu); + } + + return writer.getData(); +}; + +/** + * decodeVideo takes an url to a mp4 file and converts it into frames. + * + * The steps for this are: + * 1. Determine the codec for this video file and demux it into chunks. + * 2. Read the chunks with VideoDecoder as fast as possible. + * 3. Return an array of frames that we can efficiently draw to a canvas. + * + * @param src + * @param VideoDecoder + * @param EncodedVideoChunk + * @param emitFrame + * @param debug + * @returns {Promise} + */ +const decodeVideo = ( + src: string, + emitFrame: (frame: ImageBitmap) => void, + { + VideoDecoder, + EncodedVideoChunk, + debug, + }: { + VideoDecoder: typeof window.VideoDecoder; + EncodedVideoChunk: typeof window.EncodedVideoChunk; + debug: boolean; + } +): Promise => + new Promise((resolve, reject) => { + if (debug) console.info('Decoding video from', src); + + try { + // Uses mp4box for demuxing + const mp4boxfile = createFile(); + + // Holds the codec value + let codec = 'N/A'; + + // Creates a VideoDecoder instance + const decoder = new VideoDecoder({ + output: (frame) => { + createImageBitmap(frame, { resizeQuality: 'high' }).then((bitmap) => { + emitFrame(bitmap); + frame.close(); + + if (decoder.decodeQueueSize <= 0) { + // Give it an extra half second to finish everything + setTimeout(() => { + if (decoder.state !== 'closed') { + decoder.close(); + resolve(codec); + } + }, 500); + } + }); + }, + error: (e) => { + console.error(e); + reject(e); + }, + }); + + mp4boxfile.onReady = (info) => { + if (info && info.videoTracks && info.videoTracks[0]) { + [{ codec }] = info.videoTracks; + if (debug) console.info('Video with codec:', codec); + + // Define a type for moov to avoid using 'any' + interface AvcCBox { + SPS: NALUnit[]; + PPS: NALUnit[]; + configurationVersion: number; + AVCProfileIndication: number; + profile_compatibility: number; + AVCLevelIndication: number; + lengthSizeMinusOne: number; + nb_SPS_nalus: number; + nb_PPS_nalus: number; + } + interface StsdEntry { + avcC: AvcCBox; + } + interface Stsd { + entries: StsdEntry[]; + } + interface Stbl { + stsd: Stsd; + } + interface Minf { + stbl: Stbl; + } + interface Mdia { + minf: Minf; + } + interface Trak { + mdia: Mdia; + } + interface Moov { + traks: Trak[]; + } + + // Gets the avccbox used for reading extradata + const moov = mp4boxfile.moov as Moov | undefined; + const avccBox = moov?.traks[0].mdia.minf.stbl.stsd.entries[0].avcC; + if (!avccBox) { + reject(new Error('Could not find avcC box for extradata.')); + return; + } + const extradata = getExtradata(avccBox); + + // configure decoder + decoder.configure({ codec, description: extradata }); + + // Setup mp4box file for breaking it into chunks + mp4boxfile.setExtractionOptions(info.videoTracks[0].id); + mp4boxfile.start(); + } else reject(new Error('URL provided is not a valid mp4 video file.')); + }; + + mp4boxfile.onSamples = (_track_id, _ref, samples) => { + for (let i = 0; i < samples.length; i += 1) { + const sample = samples[i]; + const type = sample.is_sync ? 'key' : 'delta'; + + const chunk = new EncodedVideoChunk({ + type, + timestamp: sample.cts, + duration: sample.duration, + data: sample.data, + }); + + decoder.decode(chunk); + } + }; + + // Fetches the file into arraybuffers + fetch(src).then((res) => { + if (!res.body) throw new Error('Response body is null'); + const reader = res.body.getReader(); + let offset = 0; + + function appendBuffers( + result: ReadableStreamReadResult + ): Promise { + if (result.done) { + mp4boxfile.flush(); + return Promise.resolve(); + } + + const buf = result.value.buffer as MP4BoxBuffer; + buf.fileStart = offset; + offset += buf.byteLength; + mp4boxfile.appendBuffer(buf); + + return reader.read().then(appendBuffers); + } + + return reader.read().then(appendBuffers); + }); + } catch (e) { + reject(e); + } + }); + +/** + * The main function for decoding video. Deals with the polyfill cases first, + * then calls our decodeVideo. + * + * @param src + * @param emitFrame + * @param debug + * @returns {Promise|Promise} + */ +export default ( + src: string, + emitFrame: (frame: ImageBitmap) => void, + debug: boolean = false +) => { + // If our browser supports WebCodecs natively + if ( + typeof VideoDecoder === 'function' && + typeof EncodedVideoChunk === 'function' + ) { + if (debug) + console.info('WebCodecs is natively supported, using native version...'); + + return decodeVideo(src, emitFrame, { + VideoDecoder, + EncodedVideoChunk, + debug, + }); + } + + // Otherwise, resolve nothing + if (debug) console.info('WebCodecs is not available in this browser.'); + return Promise.resolve('N/A'); +}; diff --git a/src/components/ScrollerVideo/videos/HPO.mp4 b/src/components/ScrollerVideo/videos/HPO.mp4 new file mode 100644 index 0000000..7da0cb5 Binary files /dev/null and b/src/components/ScrollerVideo/videos/HPO.mp4 differ diff --git a/src/components/ScrollerVideo/videos/drone.mp4 b/src/components/ScrollerVideo/videos/drone.mp4 new file mode 100644 index 0000000..b5fdf74 Binary files /dev/null and b/src/components/ScrollerVideo/videos/drone.mp4 differ diff --git a/src/components/ScrollerVideo/videos/goldengate.mp4 b/src/components/ScrollerVideo/videos/goldengate.mp4 new file mode 100644 index 0000000..70c266b Binary files /dev/null and b/src/components/ScrollerVideo/videos/goldengate.mp4 differ diff --git a/src/components/ScrollerVideo/videos/tennis.mp4 b/src/components/ScrollerVideo/videos/tennis.mp4 new file mode 100644 index 0000000..688cb4e Binary files /dev/null and b/src/components/ScrollerVideo/videos/tennis.mp4 differ diff --git a/src/components/ScrollerVideo/videos/waves_lg.mp4 b/src/components/ScrollerVideo/videos/waves_lg.mp4 new file mode 100644 index 0000000..cbcaa0a Binary files /dev/null and b/src/components/ScrollerVideo/videos/waves_lg.mp4 differ diff --git a/src/components/ScrollerVideo/videos/waves_md.mp4 b/src/components/ScrollerVideo/videos/waves_md.mp4 new file mode 100644 index 0000000..08f6b44 Binary files /dev/null and b/src/components/ScrollerVideo/videos/waves_md.mp4 differ diff --git a/src/components/ScrollerVideo/videos/waves_sm.mp4 b/src/components/ScrollerVideo/videos/waves_sm.mp4 new file mode 100755 index 0000000..59b5539 Binary files /dev/null and b/src/components/ScrollerVideo/videos/waves_sm.mp4 differ diff --git a/src/components/Spinner/Spinner.mdx b/src/components/Spinner/Spinner.mdx new file mode 100644 index 0000000..fc15c31 --- /dev/null +++ b/src/components/Spinner/Spinner.mdx @@ -0,0 +1,25 @@ +import { Meta, Canvas } from '@storybook/blocks'; + +import * as SpinnerStories from './Spinner.stories.svelte'; + + + +# Spinner + +The `Spinner` component creates a simple loading spinner. Use it to hide long loading times for components that may be loading expensive media files or crunching through lots of data. + +```svelte + + +{#if somethingsLoading} + +{:else} +

The real deal is here.

+{/if} +``` + + diff --git a/src/components/Spinner/Spinner.stories.svelte b/src/components/Spinner/Spinner.stories.svelte new file mode 100644 index 0000000..f29e0b8 --- /dev/null +++ b/src/components/Spinner/Spinner.stories.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/components/Spinner/Spinner.svelte b/src/components/Spinner/Spinner.svelte new file mode 100644 index 0000000..62cc38a --- /dev/null +++ b/src/components/Spinner/Spinner.svelte @@ -0,0 +1,92 @@ + + + +
+
+
+
+
+
+
+ + diff --git a/src/components/Table/Table.mdx b/src/components/Table/Table.mdx new file mode 100644 index 0000000..d5e63e8 --- /dev/null +++ b/src/components/Table/Table.mdx @@ -0,0 +1,177 @@ +import { Meta, Canvas } from '@storybook/blocks'; + +import * as TableStories from './Table.stories.svelte'; + + + +# Table + +The `Table` component presents data as a table that you can make searchable, filtereable, sortable, or paginated. + +```svelte + + + +``` + + + +## Text elements + +Set the `title`, `dek`, `notes` and `source` options to add supporting metadata above and below the table. + +```svelte +
+``` + + + +## Truncated + +When your table has 10 or more rows, consider clipping it by setting the `truncated` option. When it is enabled, the table is clipped and readers must click a button below the table to see all rows. + +By default, this configuration will limit the table to 5 records. Change the cutoff point by adjusting the `truncateLength` option. + +This is a good option for simple tables with between 10 and 30 rows. It works best when the table doesn't require interactivity. + +```svelte +
+``` + + + +## Paginated + +When your table has many rows, you should consider breaking it up into pages by setting `paginated` to `true`. When it is enabled, readers can leaf through the data using a set of buttons below the table. + +By default, there are 25 records per page. Change the number by adjusting the `pageSize` option. + +This is a good option when publishing large tables for readers to explore. It works well with interactive features like searching and filters. + +```svelte +
+``` + + + +## Search bar + +Allow users to search the table by setting the optional `searchable` option to `true`. Modify the default text that appears in the box by setting `searchPlaceholder` to a different placeholder text. + +```svelte +
+``` + + + +## Filter + +Allow users to filter the table by providing one of the attributes as the `filterField`. This works best with categorical columns. + +Set `filterLabel` to make the category name more readable. For example, if the column is `Region`, set `filterLabel` to `regions` or `regions of the world`. + +```svelte +
+``` + + + +## Search and filter + +Feel free to both search and filter. + +```svelte +
+``` + + +``` + +## Sort + +Allow users to sort the table by setting `sortable` to `true`. Specify the starting order by setting `sortField` to a column name and `sortDirection` to `ascending` or `descending`. + +By default, all fields are sortable. If you'd like to limit the columns where sorting is allowed, provide a list to the `sortableFields` option. + +```svelte +
+``` + + + +## Format + +Format column values by supplying functions keyed to field names with the `fieldFormatters` option. Columns are still sorted using the raw, underlying values. + +Among other things, this feature can be used to provide a unit of measurement, such as `$` or `%`, with numeric fields. + +```svelte + + +
+``` + + diff --git a/src/components/Table/Table.stories.svelte b/src/components/Table/Table.stories.svelte new file mode 100644 index 0000000..7b230b9 --- /dev/null +++ b/src/components/Table/Table.stories.svelte @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + +, + }, + sortable: true, + sortField: 'Net worth (in billions)', + sortDirection: 'descending', + title: 'The Richest Women in the World', + source: 'Source: Forbes', + }} +/> diff --git a/src/components/Table/Table.svelte b/src/components/Table/Table.svelte new file mode 100644 index 0000000..0cd90bb --- /dev/null +++ b/src/components/Table/Table.svelte @@ -0,0 +1,385 @@ + + + + +
+ {#if title || dek || searchable || filterList} +
+ {#if title} +

{@html title}

+ {/if} + {#if dek} +

{@html dek}

+ {/if} + {#if searchable || filterList} +
truncateLength} + > + + + {#each includedFields as field} + + {/each} + + + + {#each currentPageData as item, idx} + + {#each includedFields as field} + + {/each} + + {/each} + {#if searchable && searchText && currentPageData.length === 0} + + + + {/if} + + {#if notes || source} + + {#if notes} + + + + {/if} + {#if source} + + + + {/if} + + {/if} +
+ {field} + {#if sortable && sortableFields.includes(field)} +
+ +
+ {/if} +
+ {@html formatValue(item, field, fieldFormatters)} +
+ No results found for "{searchText}" +
+
+ {@html notes} +
+
+
+ {@html source} +
+
+ + {#if truncated && searchableData.length > truncateLength} + + {/if} + {#if paginated} + {/if} + +
+ + diff --git a/src/components/Table/components/NextArrow.svelte b/src/components/Table/components/NextArrow.svelte new file mode 100644 index 0000000..3749a68 --- /dev/null +++ b/src/components/Table/components/NextArrow.svelte @@ -0,0 +1,21 @@ + + + diff --git a/src/components/Table/components/Pagination.svelte b/src/components/Table/components/Pagination.svelte new file mode 100644 index 0000000..10f3443 --- /dev/null +++ b/src/components/Table/components/Pagination.svelte @@ -0,0 +1,126 @@ + + + + + diff --git a/src/components/Table/components/PrevArrow.svelte b/src/components/Table/components/PrevArrow.svelte new file mode 100644 index 0000000..3545a94 --- /dev/null +++ b/src/components/Table/components/PrevArrow.svelte @@ -0,0 +1,21 @@ + + + diff --git a/src/components/Table/components/Select.svelte b/src/components/Table/components/Select.svelte new file mode 100644 index 0000000..9df3166 --- /dev/null +++ b/src/components/Table/components/Select.svelte @@ -0,0 +1,77 @@ + + +
+ +
+ +
+
+ + diff --git a/src/components/Table/components/SortArrow.svelte b/src/components/Table/components/SortArrow.svelte new file mode 100644 index 0000000..7eb22c7 --- /dev/null +++ b/src/components/Table/components/SortArrow.svelte @@ -0,0 +1,48 @@ + + + + + + + + diff --git a/src/components/Table/demo/homeRuns.json b/src/components/Table/demo/homeRuns.json new file mode 100644 index 0000000..4653061 --- /dev/null +++ b/src/components/Table/demo/homeRuns.json @@ -0,0 +1,42 @@ +[ + { + "Name": "Barry Bonds", + "Home runs": 762 + }, + { + "Name": "Hank Aaron", + "Home runs": 755 + }, + { + "Name": "Babe Ruth", + "Home runs": 714 + }, + { + "Name": "Albert Pujols", + "Home runs": 703 + }, + { + "Name": "Alex Rodriguez", + "Home runs": 696 + }, + { + "Name": "Willie Mays", + "Home runs": 660 + }, + { + "Name": "Ken Griffey Jr.", + "Home runs": 630 + }, + { + "Name": "Jim Thome", + "Home runs": 612 + }, + { + "Name": "Sammy Sosa", + "Home runs": 609 + }, + { + "Name": "Frank Robinson", + "Home runs": 586 + } +] diff --git a/src/components/Table/demo/pressFreedom.json b/src/components/Table/demo/pressFreedom.json new file mode 100644 index 0000000..3c53add --- /dev/null +++ b/src/components/Table/demo/pressFreedom.json @@ -0,0 +1,887 @@ +[ + { + "Country": "Afghanistan", + "Region": "Southern Asia", + "Score": 40.19 + }, + { + "Country": "Albania", + "Region": "Southern Europe", + "Score": 30.59 + }, + { + "Country": "Algeria", + "Region": "Northern Africa", + "Score": 47.26 + }, + { + "Country": "Andorra", + "Region": "Southern Europe", + "Score": 23.32 + }, + { + "Country": "Angola", + "Region": "Sub-Saharan Africa", + "Score": 34.06 + }, + { + "Country": "Argentina", + "Region": "Latin America and the Caribbean", + "Score": 28.99 + }, + { + "Country": "Armenia", + "Region": "Western Asia", + "Score": 28.83 + }, + { + "Country": "Australia", + "Region": "Australia and New Zealand", + "Score": 19.79 + }, + { + "Country": "Austria", + "Region": "Western Europe", + "Score": 16.34 + }, + { + "Country": "Azerbaijan", + "Region": "Western Asia", + "Score": 58.77 + }, + { + "Country": "Bahrain", + "Region": "Western Asia", + "Score": 61.1 + }, + { + "Country": "Bangladesh", + "Region": "Southern Asia", + "Score": 49.71 + }, + { + "Country": "Belarus", + "Region": "Eastern Europe", + "Score": 50.82 + }, + { + "Country": "Belgium", + "Region": "Western Europe", + "Score": 11.69 + }, + { + "Country": "Belize", + "Region": "Latin America and the Caribbean", + "Score": 27.61 + }, + { + "Country": "Benin", + "Region": "Sub-Saharan Africa", + "Score": 38.18 + }, + { + "Country": "Bhutan", + "Region": "Southern Asia", + "Score": 28.86 + }, + { + "Country": "Bolivia", + "Region": "Latin America and the Caribbean", + "Score": 35.47 + }, + { + "Country": "Bosnia and Herzegovina", + "Region": "Southern Europe", + "Score": 28.34 + }, + { + "Country": "Botswana", + "Region": "Sub-Saharan Africa", + "Score": 23.25 + }, + { + "Country": "Brazil", + "Region": "Latin America and the Caribbean", + "Score": 36.25 + }, + { + "Country": "Brunei", + "Region": "South-eastern Asia", + "Score": 49.91 + }, + { + "Country": "Bulgaria", + "Region": "Eastern Europe", + "Score": 37.29 + }, + { + "Country": "Burkina Faso", + "Region": "Sub-Saharan Africa", + "Score": 23.17 + }, + { + "Country": "Burundi", + "Region": "Sub-Saharan Africa", + "Score": 47.57 + }, + { + "Country": "Cambodia", + "Region": "South-eastern Asia", + "Score": 46.84 + }, + { + "Country": "Cameroon", + "Region": "Sub-Saharan Africa", + "Score": 43.78 + }, + { + "Country": "Canada", + "Region": "Northern America", + "Score": 15.25 + }, + { + "Country": "Cape Verde", + "Region": "Sub-Saharan Africa", + "Score": 20.09 + }, + { + "Country": "Central African Republic", + "Region": "Sub-Saharan Africa", + "Score": 41.92 + }, + { + "Country": "Chad", + "Region": "Sub-Saharan Africa", + "Score": 40.2 + }, + { + "Country": "Chile", + "Region": "Latin America and the Caribbean", + "Score": 27.89 + }, + { + "Country": "China", + "Region": "Eastern Asia", + "Score": 78.72 + }, + { + "Country": "Colombia", + "Region": "Latin America and the Caribbean", + "Score": 43.74 + }, + { + "Country": "Comoros", + "Region": "Sub-Saharan Africa", + "Score": 30.65 + }, + { + "Country": "Congo", + "Region": "Sub-Saharan Africa", + "Score": 38.83 + }, + { + "Country": "Costa Rica", + "Region": "Latin America and the Caribbean", + "Score": 8.76 + }, + { + "Country": "Cote d'Ivoire", + "Region": "Sub-Saharan Africa", + "Score": 28.87 + }, + { + "Country": "Croatia", + "Region": "Southern Europe", + "Score": 27.95 + }, + { + "Country": "Cuba", + "Region": "Latin America and the Caribbean", + "Score": 63.94 + }, + { + "Country": "Cyprus", + "Region": "Western Asia", + "Score": 19.85 + }, + { + "Country": "Czechia", + "Region": "Eastern Europe", + "Score": 23.38 + }, + { + "Country": "Democratic Republic of Congo", + "Region": "Sub-Saharan Africa", + "Score": 48.59 + }, + { + "Country": "Denmark", + "Region": "Northern Europe", + "Score": 8.57 + }, + { + "Country": "Djibouti", + "Region": "Sub-Saharan Africa", + "Score": 78.62 + }, + { + "Country": "Dominican Republic", + "Region": "Latin America and the Caribbean", + "Score": 25.6 + }, + { + "Country": "Ecuador", + "Region": "Latin America and the Caribbean", + "Score": 32.83 + }, + { + "Country": "Egypt", + "Region": "Northern Africa", + "Score": 56.17 + }, + { + "Country": "El Salvador", + "Region": "Latin America and the Caribbean", + "Score": 30.49 + }, + { + "Country": "Equatorial Guinea", + "Region": "Sub-Saharan Africa", + "Score": 55.67 + }, + { + "Country": "Eritrea", + "Region": "Sub-Saharan Africa", + "Score": 81.45 + }, + { + "Country": "Estonia", + "Region": "Northern Europe", + "Score": 15.25 + }, + { + "Country": "Eswatini", + "Region": "Sub-Saharan Africa", + "Score": 46.34 + }, + { + "Country": "Ethiopia", + "Region": "Sub-Saharan Africa", + "Score": 33.63 + }, + { + "Country": "Fiji", + "Region": "Melanesia", + "Score": 27.92 + }, + { + "Country": "Finland", + "Region": "Northern Europe", + "Score": 6.99 + }, + { + "Country": "France", + "Region": "Western Europe", + "Score": 22.6 + }, + { + "Country": "Gabon", + "Region": "Sub-Saharan Africa", + "Score": 38.6 + }, + { + "Country": "Gambia", + "Region": "Sub-Saharan Africa", + "Score": 30.76 + }, + { + "Country": "Georgia", + "Region": "Western Asia", + "Score": 28.64 + }, + { + "Country": "Germany", + "Region": "Western Europe", + "Score": 15.24 + }, + { + "Country": "Ghana", + "Region": "Sub-Saharan Africa", + "Score": 21.33 + }, + { + "Country": "Greece", + "Region": "Southern Europe", + "Score": 29.01 + }, + { + "Country": "Guatemala", + "Region": "Latin America and the Caribbean", + "Score": 38.45 + }, + { + "Country": "Guinea", + "Region": "Sub-Saharan Africa", + "Score": 35.42 + }, + { + "Country": "Guinea-Bissau", + "Region": "Sub-Saharan Africa", + "Score": 32.68 + }, + { + "Country": "Guyana", + "Region": "Latin America and the Caribbean", + "Score": 25.61 + }, + { + "Country": "Haiti", + "Region": "Latin America and the Caribbean", + "Score": 31.12 + }, + { + "Country": "Honduras", + "Region": "Latin America and the Caribbean", + "Score": 49.35 + }, + { + "Country": "Hong Kong", + "Region": "Eastern Asia", + "Score": 30.44 + }, + { + "Country": "Hungary", + "Region": "Eastern Europe", + "Score": 31.76 + }, + { + "Country": "Iceland", + "Region": "Northern Europe", + "Score": 15.37 + }, + { + "Country": "India", + "Region": "Southern Asia", + "Score": 46.56 + }, + { + "Country": "Indonesia", + "Region": "South-eastern Asia", + "Score": 37.4 + }, + { + "Country": "Iran", + "Region": "Southern Asia", + "Score": 72.7 + }, + { + "Country": "Iraq", + "Region": "Western Asia", + "Score": 55.57 + }, + { + "Country": "Ireland", + "Region": "Northern Europe", + "Score": 11.91 + }, + { + "Country": "Israel", + "Region": "Western Asia", + "Score": 30.9 + }, + { + "Country": "Italy", + "Region": "Southern Europe", + "Score": 23.39 + }, + { + "Country": "Jamaica", + "Region": "Latin America and the Caribbean", + "Score": 9.96 + }, + { + "Country": "Japan", + "Region": "Eastern Asia", + "Score": 28.88 + }, + { + "Country": "Jordan", + "Region": "Western Asia", + "Score": 42.89 + }, + { + "Country": "Kazakhstan", + "Region": "Central Asia", + "Score": 50.28 + }, + { + "Country": "Kenya", + "Region": "Sub-Saharan Africa", + "Score": 33.65 + }, + { + "Country": "Kuwait", + "Region": "Western Asia", + "Score": 34.36 + }, + { + "Country": "Kyrgyzstan", + "Region": "Central Asia", + "Score": 30.37 + }, + { + "Country": "Laos", + "Region": "South-eastern Asia", + "Score": 70.56 + }, + { + "Country": "Latvia", + "Region": "Northern Europe", + "Score": 19.26 + }, + { + "Country": "Lebanon", + "Region": "Western Asia", + "Score": 34.93 + }, + { + "Country": "Lesotho", + "Region": "Sub-Saharan Africa", + "Score": 31.61 + }, + { + "Country": "Liberia", + "Region": "Sub-Saharan Africa", + "Score": 33.36 + }, + { + "Country": "Libya", + "Region": "Northern Africa", + "Score": 55.73 + }, + { + "Country": "Liechtenstein", + "Region": "Western Europe", + "Score": 19.49 + }, + { + "Country": "Lithuania", + "Region": "Northern Europe", + "Score": 20.15 + }, + { + "Country": "Luxembourg", + "Region": "Western Europe", + "Score": 17.56 + }, + { + "Country": "Madagascar", + "Region": "Sub-Saharan Africa", + "Score": 28.24 + }, + { + "Country": "Malawi", + "Region": "Sub-Saharan Africa", + "Score": 28.8 + }, + { + "Country": "Malaysia", + "Region": "South-eastern Asia", + "Score": 39.47 + }, + { + "Country": "Maldives", + "Region": "Southern Asia", + "Score": 29.13 + }, + { + "Country": "Mali", + "Region": "Sub-Saharan Africa", + "Score": 33.5 + }, + { + "Country": "Malta", + "Region": "Southern Europe", + "Score": 30.46 + }, + { + "Country": "Mauritania", + "Region": "Sub-Saharan Africa", + "Score": 32.25 + }, + { + "Country": "Mauritius", + "Region": "Sub-Saharan Africa", + "Score": 28.74 + }, + { + "Country": "Mexico", + "Region": "Latin America and the Caribbean", + "Score": 46.71 + }, + { + "Country": "Moldova", + "Region": "Eastern Europe", + "Score": 31.61 + }, + { + "Country": "Mongolia", + "Region": "Eastern Asia", + "Score": 28.97 + }, + { + "Country": "Montenegro", + "Region": "Southern Europe", + "Score": 34.33 + }, + { + "Country": "Morocco", + "Region": "Northern Africa", + "Score": 43.94 + }, + { + "Country": "Mozambique", + "Region": "Sub-Saharan Africa", + "Score": 35.39 + }, + { + "Country": "Myanmar", + "Region": "South-eastern Asia", + "Score": 46.14 + }, + { + "Country": "Namibia", + "Region": "Sub-Saharan Africa", + "Score": 19.72 + }, + { + "Country": "Nepal", + "Region": "Southern Asia", + "Score": 34.62 + }, + { + "Country": "Netherlands", + "Region": "Western Europe", + "Score": 9.67 + }, + { + "Country": "New Zealand", + "Region": "Australia and New Zealand", + "Score": 10.04 + }, + { + "Country": "Nicaragua", + "Region": "Latin America and the Caribbean", + "Score": 39.98 + }, + { + "Country": "Niger", + "Region": "Sub-Saharan Africa", + "Score": 28.44 + }, + { + "Country": "Nigeria", + "Region": "Sub-Saharan Africa", + "Score": 39.69 + }, + { + "Country": "North Korea", + "Region": "Eastern Asia", + "Score": 81.28 + }, + { + "Country": "North Macedonia", + "Region": "Southern Europe", + "Score": 31.67 + }, + { + "Country": "Norway", + "Region": "Northern Europe", + "Score": 6.72 + }, + { + "Country": "Oman", + "Region": "Western Asia", + "Score": 43.37 + }, + { + "Country": "Pakistan", + "Region": "Southern Asia", + "Score": 46.86 + }, + { + "Country": "Palestine", + "Region": "Western Asia", + "Score": 43.18 + }, + { + "Country": "Panama", + "Region": "Latin America and the Caribbean", + "Score": 29.94 + }, + { + "Country": "Papua New Guinea", + "Region": "Melanesia", + "Score": 24.88 + }, + { + "Country": "Paraguay", + "Region": "Latin America and the Caribbean", + "Score": 33.52 + }, + { + "Country": "Peru", + "Region": "Latin America and the Caribbean", + "Score": 31.71 + }, + { + "Country": "Philippines", + "Region": "South-eastern Asia", + "Score": 45.64 + }, + { + "Country": "Poland", + "Region": "Eastern Europe", + "Score": 28.84 + }, + { + "Country": "Portugal", + "Region": "Southern Europe", + "Score": 10.11 + }, + { + "Country": "Qatar", + "Region": "Western Asia", + "Score": 42.6 + }, + { + "Country": "Romania", + "Region": "Eastern Europe", + "Score": 24.91 + }, + { + "Country": "Russia", + "Region": "Eastern Europe", + "Score": 48.71 + }, + { + "Country": "Rwanda", + "Region": "Sub-Saharan Africa", + "Score": 50.66 + }, + { + "Country": "Samoa", + "Region": "Polynesia", + "Score": 19.24 + }, + { + "Country": "Saudi Arabia", + "Region": "Western Asia", + "Score": 62.73 + }, + { + "Country": "Senegal", + "Region": "Sub-Saharan Africa", + "Score": 25.22 + }, + { + "Country": "Serbia", + "Region": "Southern Europe", + "Score": 32.03 + }, + { + "Country": "Seychelles", + "Region": "Sub-Saharan Africa", + "Score": 25.66 + }, + { + "Country": "Sierra Leone", + "Region": "Sub-Saharan Africa", + "Score": 29.61 + }, + { + "Country": "Singapore", + "Region": "South-eastern Asia", + "Score": 55.2 + }, + { + "Country": "Slovakia", + "Region": "Eastern Europe", + "Score": 23.02 + }, + { + "Country": "Slovenia", + "Region": "Southern Europe", + "Score": 23.1 + }, + { + "Country": "Somalia", + "Region": "Sub-Saharan Africa", + "Score": 55.47 + }, + { + "Country": "South Africa", + "Region": "Sub-Saharan Africa", + "Score": 21.59 + }, + { + "Country": "South Korea", + "Region": "Eastern Asia", + "Score": 23.43 + }, + { + "Country": "South Sudan", + "Region": "Sub-Saharan Africa", + "Score": 45.78 + }, + { + "Country": "Spain", + "Region": "Southern Europe", + "Score": 20.44 + }, + { + "Country": "Sri Lanka", + "Region": "Southern Asia", + "Score": 42.2 + }, + { + "Country": "Sudan", + "Region": "Northern Africa", + "Score": 52.93 + }, + { + "Country": "Suriname", + "Region": "Latin America and the Caribbean", + "Score": 16.95 + }, + { + "Country": "Sweden", + "Region": "Northern Europe", + "Score": 7.24 + }, + { + "Country": "Switzerland", + "Region": "Western Europe", + "Score": 10.55 + }, + { + "Country": "Syria", + "Region": "Western Asia", + "Score": 70.63 + }, + { + "Country": "Taiwan", + "Region": "Eastern Asia", + "Score": 23.86 + }, + { + "Country": "Tajikistan", + "Region": "Central Asia", + "Score": 55.52 + }, + { + "Country": "Tanzania", + "Region": "Sub-Saharan Africa", + "Score": 40.69 + }, + { + "Country": "Thailand", + "Region": "South-eastern Asia", + "Score": 45.22 + }, + { + "Country": "Timor", + "Region": "South-eastern Asia", + "Score": 29.11 + }, + { + "Country": "Togo", + "Region": "Sub-Saharan Africa", + "Score": 29.59 + }, + { + "Country": "Tonga", + "Region": "Polynesia", + "Score": 24.59 + }, + { + "Country": "Trinidad and Tobago", + "Region": "Latin America and the Caribbean", + "Score": 21.55 + }, + { + "Country": "Tunisia", + "Region": "Northern Africa", + "Score": 29.53 + }, + { + "Country": "Turkey", + "Region": "Western Asia", + "Score": 49.79 + }, + { + "Country": "Turkmenistan", + "Region": "Central Asia", + "Score": 80.03 + }, + { + "Country": "Uganda", + "Region": "Sub-Saharan Africa", + "Score": 41.19 + }, + { + "Country": "Ukraine", + "Region": "Eastern Europe", + "Score": 32.96 + }, + { + "Country": "United Arab Emirates", + "Region": "Western Asia", + "Score": 43.13 + }, + { + "Country": "United Kingdom", + "Region": "Northern Europe", + "Score": 21.59 + }, + { + "Country": "United States", + "Region": "Northern America", + "Score": 23.93 + }, + { + "Country": "Uruguay", + "Region": "Latin America and the Caribbean", + "Score": 16.38 + }, + { + "Country": "Uzbekistan", + "Region": "Central Asia", + "Score": 50.74 + }, + { + "Country": "Venezuela", + "Region": "Latin America and the Caribbean", + "Score": 47.6 + }, + { + "Country": "Vietnam", + "Region": "South-eastern Asia", + "Score": 78.46 + }, + { + "Country": "Yemen", + "Region": "Western Asia", + "Score": 62.35 + }, + { + "Country": "Zambia", + "Region": "Sub-Saharan Africa", + "Score": 38.21 + }, + { + "Country": "Zimbabwe", + "Region": "Sub-Saharan Africa", + "Score": 43.12 + } +] diff --git a/src/components/Table/demo/richestWomen.json b/src/components/Table/demo/richestWomen.json new file mode 100644 index 0000000..d3c9083 --- /dev/null +++ b/src/components/Table/demo/richestWomen.json @@ -0,0 +1,42 @@ +[ + { + "Name": "Francoise Bettencourt Meyers", + "Net worth (in billions)": 74.8 + }, + { + "Name": "Alice Walton", + "Net worth (in billions)": 65.3 + }, + { + "Name": "Julia Koch", + "Net worth (in billions)": 60 + }, + { + "Name": "MacKenzie Scott", + "Net worth (in billions)": 43.6 + }, + { + "Name": "Jacqueline Mars", + "Net worth (in billions)": 31.7 + }, + { + "Name": "Gina Rinehart", + "Net worth (in billions)": 30.2 + }, + { + "Name": "Miriam Adelson", + "Net worth (in billions)": 27.5 + }, + { + "Name": "Susanne Klatten", + "Net worth (in billions)": 24.3 + }, + { + "Name": "Iris Fontbona", + "Net worth (in billions)": 22.8 + }, + { + "Name": "Abigail Johnson", + "Net worth (in billions)": 21.2 + } +] diff --git a/src/components/Table/utils.ts b/src/components/Table/utils.ts new file mode 100644 index 0000000..51b36a7 --- /dev/null +++ b/src/components/Table/utils.ts @@ -0,0 +1,122 @@ +type FilterableDatum> = T & { + searchStr: string; +}; + +export function filterArray>( + data: FilterableDatum[], + searchText: string, + filterField: keyof FilterableDatum, + filterValue: FilterableDatum[keyof FilterableDatum] +) { + if (searchText) { + data = data.filter((item) => { + return item.searchStr.includes(searchText.toLowerCase()); + }); + } + + if (filterValue) { + data = data.filter((item) => { + if (!filterField) return true; // or handle the undefined case as appropriate + + return item[filterField] === filterValue; + }); + } + + return data; +} +export function paginateArray( + array: T[], + pageSize: number, + pageNumber: number +) { + return array.slice((pageNumber - 1) * pageSize, pageNumber * pageSize); +} + +/** + * We specify the output type here by adding `string` to the union because we want to explicitly define the output array as accepting strings. + * + * This is to get rid of the type error from `attrList.unshift('All')` + */ +function uniqueAttr(array: T[], attr: keyof T): (T[keyof T] | string)[] { + return array.map((e) => e[attr]).filter(unique); +} + +function unique(value: T, index: number, array: T[]) { + return array.indexOf(value) === index; +} + +export function getOptions(data: T[], attr: keyof T) { + // Get all the unique values in the provided field. Sort it. + const attrList = uniqueAttr(data, attr).sort((a, b) => { + // Throw errors if a and b are not strings. + // a and b should be strings since they are keys of T. + if (typeof a !== 'string' || typeof b !== 'string') { + throw new Error(`Expected string, got ${typeof a} and ${typeof b}`); + } + + return a.localeCompare(b); + }); + + // Tack 'All' at the start of `attrList`, making it the first option. + attrList.unshift('All'); + + // Convert the list into Option typed objects ready for our Select component + return attrList.map((a) => ({ text: a, value: a })); +} +interface SortableItem { + [key: string]: unknown; // Or more specific types if known +} + +/** + * Sorts an array of objects based on a specified column and direction. + */ +export function sortArray( + /** The array to sort. */ + array: T[], + /** The column to sort by. */ + column: keyof T, + /** The sorting direction ('ascending' or 'descending'). */ + direction: 'ascending' | 'descending', + /** Whether or not sorting is turned on */ + sortable: boolean +) { + if (!sortable) return array; + + const sorted = [...array].sort((a, b) => { + if (a[column] < b[column]) { + return direction === 'ascending' ? -1 : 1; + } else if (a[column] > b[column]) { + return direction === 'ascending' ? 1 : -1; + } else { + return 0; + } + }); + + return sorted; +} + +export type Formatter = (value: T) => string; + +export type FieldFormatters = { + [K in keyof T]?: Formatter; +}; +/** + * Formats a value based on a field and a dictionary of formatters. + */ +export function formatValue>( + /** The object containing the field. */ + item: FilterableDatum, + /** The field to format. */ + field: keyof T, + /** An optional dictionary of formatters. */ + fieldFormatters?: FieldFormatters +) { + const value = item[field]; + + if (fieldFormatters && field in fieldFormatters && fieldFormatters[field]) { + const formatter = fieldFormatters[field]; + return formatter(value); + } else { + return value; + } +} diff --git a/src/components/TileMap/TileMap.mdx b/src/components/TileMap/TileMap.mdx new file mode 100644 index 0000000..4057a4b --- /dev/null +++ b/src/components/TileMap/TileMap.mdx @@ -0,0 +1,286 @@ +import { Meta, Canvas } from '@storybook/blocks'; + +import * as TileMapStories from './TileMap.stories.svelte'; + + + +# TileMap + +Easily add an interactive map to your page using MapLibre GL and PMTiles. + +```svelte + + + +``` + +The TileMap component uses [MapLibre GL JS](https://maplibre.org/) and [PMTiles](https://protomaps.com/docs/pmtiles) for efficient, interactive mapping. It automatically configures the PMTiles protocol and uses the Reuters Protomaps style by default. + + + +## Globe view + +Use the `projection` prop to display a 3D globe. The projection accepts a [ProjectionSpecification](https://maplibre.org/maplibre-style-spec/types/#projectiondefinition) object: + + + +```svelte + +``` + +## Non-interactive mode + +Disable interaction for static maps: + + + +## Adding GeoJSON layers + +Use the `TileMapLayer` component to add GeoJSON data to your map. You can pass GeoJSON data directly or fetch it from a URL. Layer rendering order will directly correspond to the order in which you add the layers in the code. + +> **Note for TypeScript users:** When passing GeoJSON data objects, you'll need to type cast them using `as GeoJSON` to ensure TypeScript recognizes the correct type. This provides better type safety and error messages. See examples below. + + + +### Basic example with local data + +```svelte + + + + + + + + + +``` + +### Fetching GeoJSON from a URL + +You can also pass a URL string to fetch GeoJSON data: + +```svelte + + + +``` + +### Adding point markers + +```svelte + + + + + + + + + +``` + +### TileMapLayer Props + +- **id**: `string` (required) - Unique identifier for the layer +- **data**: `object | string` (required) - GeoJSON data or URL to fetch from +- **type**: `'fill' | 'line' | 'circle' | 'symbol' | 'fill-extrusion' | 'raster' | 'background' | 'heatmap' | 'hillshade'` - Layer type (default: `'fill'`) +- **paint**: `Record` - Paint properties for the layer +- **layout**: `Record` - Layout properties for the layer +- **beforeId**: `string` - Layer ID to insert before (for ordering) +- **minZoom**: `number` - Minimum zoom level to display layer +- **maxZoom**: `number` - Maximum zoom level to display layer +- **filter**: `unknown[]` - Filter expression for the layer + +### Multiple layers example + +You can combine multiple layers to create rich visualizations: + +```svelte + + + + + + + + + + + + + +``` + +## Advanced usage + +For more control over the map, you can use the `onMapReady` callback to access the MapLibre GL instance: + +```svelte + + + +``` + +## Props + +- **center**: `[longitude, latitude]` - Map center coordinates (default: `[0, 0]`) +- **zoom**: `number` - Initial zoom level (default: `2`) +- **minZoom**: `number` - Minimum zoom level (default: `0`) +- **maxZoom**: `number` - Maximum zoom level (default: `22`) +- **interactive**: `boolean` - Enable interactive controls (default: `true`) +- **styleUrl**: `string` - Map style URL (default: Reuters Protomaps style) +- **height**: `string` - Map height in CSS units (default: `'500px'`) +- **width**: `ContainerWidth` - Width within the text well (default: `'normal'`) +- **onMapReady**: `(map: maplibregl.Map) => void` - Callback when map is ready diff --git a/src/components/TileMap/TileMap.stories.svelte b/src/components/TileMap/TileMap.stories.svelte new file mode 100644 index 0000000..11e042b --- /dev/null +++ b/src/components/TileMap/TileMap.stories.svelte @@ -0,0 +1,237 @@ + + + + + + + + + + + + + + + + + + + + + + + { + geocoderMapRef = map; + }} + > +
+ { + geocoderMapRef?.flyTo({ center: [loc.lng, loc.lat], zoom: 10 }); + }} + /> +
+
+
diff --git a/src/components/TileMap/TileMap.svelte b/src/components/TileMap/TileMap.svelte new file mode 100644 index 0000000..6d5f5cd --- /dev/null +++ b/src/components/TileMap/TileMap.svelte @@ -0,0 +1,199 @@ + + + + + + +
+
+ {#if children} + {@render children()} + {/if} +
+
+ + diff --git a/src/components/TileMap/TileMapLayer.svelte b/src/components/TileMap/TileMapLayer.svelte new file mode 100644 index 0000000..d0abe6c --- /dev/null +++ b/src/components/TileMap/TileMapLayer.svelte @@ -0,0 +1,211 @@ + + diff --git a/src/components/Video/Video.mdx b/src/components/Video/Video.mdx new file mode 100644 index 0000000..3cd7ccb --- /dev/null +++ b/src/components/Video/Video.mdx @@ -0,0 +1,155 @@ +import { Meta, Canvas } from '@storybook/blocks'; + +import * as VideoStories from './Video.stories.svelte'; + + + +# Video + +The `Video` component adds a video with various controls to your page such as: + +- Play/pause button +- Autoplay controls, i.e. whether the video plays when it comes into view or on page load +- Looping +- Audio controls +- Text elements such as notes and titles + +```svelte + + +