diff options
author | Wolfgang Müller | 2025-02-13 17:52:16 +0100 |
---|---|---|
committer | Wolfgang Müller | 2025-02-13 17:52:16 +0100 |
commit | dc4db405d2991d3ec6a114f3b08d3fccd057d3ee (patch) | |
tree | 2c620c9af2062ba09fa591f8b3ed961664adab58 | |
parent | 4df870d793123be95c8af031a340a39b5b8402ac (diff) | |
download | hircine-dc4db405d2991d3ec6a114f3b08d3fccd057d3ee.tar.gz |
frontend: Migrate to Svelte 5
146 files changed, 3784 insertions, 4115 deletions
diff --git a/frontend/package-lock.json b/frontend/package-lock.json index b17f195..d40a916 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -13,8 +13,8 @@ "@urql/svelte": "^4.2.2", "filesize": "^10.1.6", "graphql": "npm:graphql-web-lite@^16.6.0", - "svelecte": "^4.4.1", - "svelte-modals": "^1.3.0" + "svelecte": "^5.0.0", + "svelte-modals": "^2.0.0" }, "devDependencies": { "@eslint/eslintrc": "^3.2.0", @@ -26,7 +26,7 @@ "@iconify/tailwind": "^1.1.3", "@sveltejs/adapter-static": "^3.0.6", "@sveltejs/kit": "^2.8.1", - "@sveltejs/vite-plugin-svelte": "^3.0.2", + "@sveltejs/vite-plugin-svelte": "^5.0.0", "@typescript-eslint/eslint-plugin": "^8.14.0", "@typescript-eslint/parser": "^8.14.0", "@zerodevx/svelte-toast": "^0.9.6", @@ -37,24 +37,25 @@ "eslint-plugin-svelte": "^2.46.0", "fast-deep-equal": "^3.1.3", "globals": "^15.14.0", + "jsdom": "^26.0.0", "npm-check-updates": "^17.1.11", "postcss": "^8.4.49", "prettier": "^3.3.3", "prettier-plugin-svelte": "^3.2.8", "prettier-plugin-tailwindcss": "^0.6.8", - "svelte": "^4.2.12", - "svelte-check": "^3.6.6", + "svelte": "^5.5.0", + "svelte-check": "^4.0.0", "tailwindcss": "^3.4.14", "tslib": "^2.8.1", "typescript": "^5.6.3", - "vite": "^5.4.11", + "vite": "^6.0.0", "vitest": "^3.0.0" } }, "node_modules/@0no-co/graphql.web": { - "version": "1.0.13", - "resolved": "https://registry.npmjs.org/@0no-co/graphql.web/-/graphql.web-1.0.13.tgz", - "integrity": "sha512-jqYxOevheVTU1S36ZdzAkJIdvRp2m3OYIG5SEoKDw5NI8eVwkoI0D/Q3DYNGmXCxkA6CQuoa7zvMiDPTLqUNuw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@0no-co/graphql.web/-/graphql.web-1.1.0.tgz", + "integrity": "sha512-Ey/+GWfTlbeHVLMFnoJIfwJMaik2fLhiRrNiGD0sEyISw6IlADs6Gcsk9N2Rl/gWrokiEivkIU2BjT3zChdqQQ==", "license": "MIT", "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" @@ -92,19 +93,17 @@ } }, "node_modules/@ardatan/relay-compiler": { - "version": "12.0.1", - "resolved": "https://registry.npmjs.org/@ardatan/relay-compiler/-/relay-compiler-12.0.1.tgz", - "integrity": "sha512-q89DkY9HnvsyBRMu5YiYAJUN+B7cST364iCKLzeNqn0BUG3LWez2KfyKTbxPDdqSzGyUmIfUgTm/ThckIReF4g==", + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/@ardatan/relay-compiler/-/relay-compiler-12.0.2.tgz", + "integrity": "sha512-UTorfzSOtTN0PT80f8GiME2a30CliifqgZBKxhN3FESvdp5oEZWAO7nscMVKWoVl+NJy1tnNX0uMWCPBbMJdjg==", "dev": true, "license": "MIT", "dependencies": { "@babel/generator": "^7.14.0", "@babel/parser": "^7.14.0", "@babel/runtime": "^7.0.0", - "babel-preset-fbjs": "^3.4.0", "chalk": "^4.0.0", "fb-watchman": "^2.0.0", - "fbjs": "^3.0.0", "immutable": "~3.7.6", "invariant": "^2.2.4", "nullthrows": "^1.1.1", @@ -118,6 +117,27 @@ "graphql": "*" } }, + "node_modules/@asamuzakjp/css-color": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-2.8.3.tgz", + "integrity": "sha512-GIc76d9UI1hCvOATjZPyHFmE5qhRccp3/zGfMPapK3jBi+yocEzp6BBB0UnfRYP9NP4FANqUZYb0hnfs3TM3hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.1", + "@csstools/css-color-parser": "^3.0.7", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, "node_modules/@babel/code-frame": { "version": "7.26.2", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", @@ -134,9 +154,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.5.tgz", - "integrity": "sha512-XvcZi1KWf88RVbF9wn8MN6tYFloU5qX8KjuF3E1PVBmJ9eypXfs4GRiJwLuTZL0iSnJUKn1BFPa5BPZZJyFzPg==", + "version": "7.26.8", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.8.tgz", + "integrity": "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==", "dev": true, "license": "MIT", "engines": { @@ -144,22 +164,23 @@ } }, "node_modules/@babel/core": { - "version": "7.26.7", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.7.tgz", - "integrity": "sha512-SRijHmF0PSPgLIBYlWnG0hyeJLwXE2CgpsXaMOrtt2yp9/86ALw6oUlj9KYuZ0JN07T4eBMVIW4li/9S1j2BGA==", + "version": "7.26.8", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.8.tgz", + "integrity": "sha512-l+lkXCHS6tQEc5oUpK28xBOZ6+HwaH7YwoYQbLFiYb4nS2/l1tKnZEtEWkD0GuiYdvArf9qBS0XlQGXzPMsNqQ==", "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.26.5", + "@babel/generator": "^7.26.8", "@babel/helper-compilation-targets": "^7.26.5", "@babel/helper-module-transforms": "^7.26.0", "@babel/helpers": "^7.26.7", - "@babel/parser": "^7.26.7", - "@babel/template": "^7.25.9", - "@babel/traverse": "^7.26.7", - "@babel/types": "^7.26.7", + "@babel/parser": "^7.26.8", + "@babel/template": "^7.26.8", + "@babel/traverse": "^7.26.8", + "@babel/types": "^7.26.8", + "@types/gensync": "^1.0.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -175,14 +196,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.5.tgz", - "integrity": "sha512-2caSP6fN9I7HOe6nqhtft7V4g7/V/gfDsC3Ag4W7kEzzvRGKqiv0pu0HogPiZ3KaVSoNDhUws6IJjDjpfmYIXw==", + "version": "7.26.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.8.tgz", + "integrity": "sha512-ef383X5++iZHWAXX0SXQR6ZyQhw/0KtTkrTz61WXRhFM6dhpHulO/RJz79L8S6ugZHJkOOkUrUdxgdF2YiPFnA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.26.5", - "@babel/types": "^7.26.5", + "@babel/parser": "^7.26.8", + "@babel/types": "^7.26.8", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" @@ -191,19 +212,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", - "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-compilation-targets": { "version": "7.26.5", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.26.5.tgz", @@ -221,42 +229,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.9.tgz", - "integrity": "sha512-UTZQMvt0d/rSz6KI+qdu7GQze5TIajwTS++GUozlw8VBJDEOAqSXwm1WvmYEZwqdqSGQshRocPDqrt4HBZB3fQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "@babel/helper-member-expression-to-functions": "^7.25.9", - "@babel/helper-optimise-call-expression": "^7.25.9", - "@babel/helper-replace-supers": "^7.25.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", - "@babel/traverse": "^7.25.9", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.9.tgz", - "integrity": "sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-module-imports": { "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", @@ -289,19 +261,6 @@ "@babel/core": "^7.0.0" } }, - "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.25.9.tgz", - "integrity": "sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-plugin-utils": { "version": "7.26.5", "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz", @@ -312,38 +271,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-replace-supers": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.26.5.tgz", - "integrity": "sha512-bJ6iIVdYX1YooY2X7w1q6VITt+LnUILtNk7zT78ykuwStx8BauCzxvFqFaHjOpW1bVnSUM1PN1f0p5P21wHxvg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-member-expression-to-functions": "^7.25.9", - "@babel/helper-optimise-call-expression": "^7.25.9", - "@babel/traverse": "^7.26.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.9.tgz", - "integrity": "sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-string-parser": { "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", @@ -389,13 +316,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.26.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.7.tgz", - "integrity": "sha512-kEvgGGgEjRUutvdVvZhbn/BxVt+5VSpwXz1j3WYXQbXDo8KzFOPNG2GQbdAiNq8g6wn1yKk7C/qrke03a84V+w==", + "version": "7.26.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.8.tgz", + "integrity": "sha512-TZIQ25pkSoaKEYYaHbbxkfL36GNsQ6iFiBbeuzAkLnXayKR1yP1zFe+NxuZWWsUyvt8icPU9CCq0sgWGXR1GEw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.26.7" + "@babel/types": "^7.26.8" }, "bin": { "parser": "bin/babel-parser.js" @@ -404,74 +331,6 @@ "node": ">=6.0.0" } }, - "node_modules/@babel/plugin-proposal-class-properties": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz", - "integrity": "sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==", - "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-properties instead.", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-object-rest-spread": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.7.tgz", - "integrity": "sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg==", - "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-object-rest-spread instead.", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.20.5", - "@babel/helper-compilation-targets": "^7.20.7", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-transform-parameters": "^7.20.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-properties": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", - "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.12.13" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-flow": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.26.0.tgz", - "integrity": "sha512-B+O2DnPc0iG+YXFqOxv2WNuNU97ToWjOomUQ78DouOENWUaM5sVrmet9mcomUGQFwpJd//gvUagXBSdzO1fRKg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-syntax-import-assertions": { "version": "7.26.0", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.26.0.tgz", @@ -488,105 +347,54 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.9.tgz", - "integrity": "sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.25.9.tgz", - "integrity": "sha512-6jmooXYIwn9ca5/RylZADJ+EnSxVUS5sjeJ9UPk6RWRzXCmOJCy6dqItPJFpw2cuCangPK4OYr5uhGKcmrm5Qg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.26.5.tgz", - "integrity": "sha512-chuTSY+hq09+/f5lMj8ZSYgCFpppV2CbYrhNFJ1BFoXpiWPnnAb7R0MqrafCpN8E1+YRrtM1MXZHJdIx8B6rMQ==", + "node_modules/@babel/runtime": { + "version": "7.26.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.7.tgz", + "integrity": "sha512-AOPI3D+a8dXnja+iwsUqGRjr1BbZIe771sXdapOtYI531gSqpi92vXivKcq2asu/DFpdl1ceFAKZyRzK2PCVcQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.26.5" + "regenerator-runtime": "^0.14.0" }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.25.9.tgz", - "integrity": "sha512-1F05O7AYjymAtqbsFETboN1NvBdcnzMerO+zlMyJBEz6WkMdejvGWw9p05iTSjC85RLlBseHHQpYaM4gzJkBGg==", + "node_modules/@babel/template": { + "version": "7.26.8", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.8.tgz", + "integrity": "sha512-iNKaX3ZebKIsCvJ+0jd6embf+Aulaa3vNBqZ41kM7iTWjx5qzWKXGHiJUW3+nTpQ18SG11hdF8OAzKrpXkb96Q==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/code-frame": "^7.26.2", + "@babel/parser": "^7.26.8", + "@babel/types": "^7.26.8" }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-classes": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.9.tgz", - "integrity": "sha512-mD8APIXmseE7oZvZgGABDyM34GUmK45Um2TXiBUt7PnuAxrgoSVf123qUzPxEr/+/BHrRn5NMZCdE2m/1F8DGg==", + "node_modules/@babel/traverse": { + "version": "7.26.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.8.tgz", + "integrity": "sha512-nic9tRkjYH0oB2dzr/JoGIm+4Q6SuYeLEiIiZDwBscRMYFJ+tMAz98fuel9ZnbXViA2I0HVSSRRK8DW5fjXStA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "@babel/helper-compilation-targets": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-replace-supers": "^7.25.9", - "@babel/traverse": "^7.25.9", + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.8", + "@babel/parser": "^7.26.8", + "@babel/template": "^7.26.8", + "@babel/types": "^7.26.8", + "debug": "^4.3.1", "globals": "^11.1.0" }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-classes/node_modules/globals": { + "node_modules/@babel/traverse/node_modules/globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", @@ -596,343 +404,133 @@ "node": ">=4" } }, - "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.25.9.tgz", - "integrity": "sha512-HnBegGqXZR12xbcTHlJ9HGxw1OniltT26J5YpfruGqtUHlz/xKf/G2ak9e+t0rVqrjXa9WOhvYPz1ERfMj23AA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/template": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.25.9.tgz", - "integrity": "sha512-WkCGb/3ZxXepmMiX101nnGiU+1CAdut8oHyEOHxkKuS1qKpU2SMXE2uSvfz8PBuLd49V6LEsbtyPhWC7fnkgvQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-flow-strip-types": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.26.5.tgz", - "integrity": "sha512-eGK26RsbIkYUns3Y8qKl362juDDYK+wEdPGHGrhzUl6CewZFo55VZ7hg+CyMFU4dd5QQakBN86nBMpRsFpRvbQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.26.5", - "@babel/plugin-syntax-flow": "^7.26.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-for-of": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.25.9.tgz", - "integrity": "sha512-LqHxduHoaGELJl2uhImHwRQudhCM50pT46rIBNvtT/Oql3nqiS3wOwP+5ten7NpYSXrrVLgtZU3DZmPtWZo16A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-function-name": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.25.9.tgz", - "integrity": "sha512-8lP+Yxjv14Vc5MuWBpJsoUCd3hD6V9DgBon2FVYL4jJgbnVQ9fTgYmonchzZJOVNgzEgbxp4OwAf6xz6M/14XA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-compilation-targets": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/traverse": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-literals": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.25.9.tgz", - "integrity": "sha512-9N7+2lFziW8W9pBl2TzaNht3+pgMIRP74zizeCSrtnSKVdUl8mAjjOP2OOVQAfZ881P2cNjDj1uAMEdeD50nuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-member-expression-literals": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.25.9.tgz", - "integrity": "sha512-PYazBVfofCQkkMzh2P6IdIUaCEWni3iYEerAsRWuVd8+jlM1S9S9cz1dF9hIzyoZ8IA3+OwVYIp9v9e+GbgZhA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.26.3.tgz", - "integrity": "sha512-MgR55l4q9KddUDITEzEFYn5ZsGDXMSsU9E+kh7fjRXTIC3RHqfCo8RPRbyReYJh44HQ/yomFkqbOFohXvDCiIQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-transforms": "^7.26.0", - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-object-super": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.25.9.tgz", - "integrity": "sha512-Kj/Gh+Rw2RNLbCK1VAWj2U48yxxqL2x0k10nPtSdRa0O2xnHXalD0s+o1A6a0W43gJ00ANo38jxkQreckOzv5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-replace-supers": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-parameters": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.25.9.tgz", - "integrity": "sha512-wzz6MKwpnshBAiRmn4jR8LYz/g8Ksg0o80XmwZDlordjwEk9SxBzTWC7F5ef1jhbrbOW2DJ5J6ayRukrJmnr0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-property-literals": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.25.9.tgz", - "integrity": "sha512-IvIUeV5KrS/VPavfSM/Iu+RE6llrHrYIKY1yfCzyO/lMXHQ+p7uGhonmGVisv6tSBSVgWzMBohTcvkC9vQcQFA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-display-name": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.25.9.tgz", - "integrity": "sha512-KJfMlYIUxQB1CJfO3e0+h0ZHWOTLCPP115Awhaz8U0Zpq36Gl/cXlpoyMRnUWlhNUBAzldnCiAZNvCDj7CrKxQ==", + "node_modules/@babel/types": { + "version": "7.26.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.8.tgz", + "integrity": "sha512-eUuWapzEGWFEpHFxgEaBG8e3n6S8L3MSu0oda755rOfabWPnh0Our1AozNFVUxGFIhbKgd1ksprsoDGMinTOTA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-react-jsx": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.25.9.tgz", - "integrity": "sha512-s5XwpQYCqGerXl+Pu6VDL3x0j2d82eiV77UJ8a2mDHAW7j9SWRqQ2y1fNo1Z74CdcYipl5Z41zvjj4Nfzq36rw==", + "node_modules/@csstools/color-helpers": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.1.tgz", + "integrity": "sha512-MKtmkA0BX87PKaO1NFRTFH+UnkgnmySQOvNxJubsadusqPEC2aJ9MOQiMceZJJ6oitUl/i0L6u0M1IrmAOmgBA==", "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "@babel/helper-module-imports": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/plugin-syntax-jsx": "^7.25.9", - "@babel/types": "^7.25.9" - }, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=18" } }, - "node_modules/@babel/plugin-transform-shorthand-properties": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.25.9.tgz", - "integrity": "sha512-MUv6t0FhO5qHnS/W8XCbHmiRWOphNufpE1IVxhK5kuN3Td9FT1x4rx4K42s3RYdMXCXpfWkGSbCSd0Z64xA7Ng==", + "node_modules/@csstools/css-calc": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.1.tgz", + "integrity": "sha512-rL7kaUnTkL9K+Cvo2pnCieqNpTKgQzy5f+N+5Iuko9HAoasP+xgprVh7KN/MaJVvVL1l0EzQq2MoqBHKSrDrag==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, "engines": { - "node": ">=6.9.0" + "node": ">=18" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3" } }, - "node_modules/@babel/plugin-transform-spread": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.25.9.tgz", - "integrity": "sha512-oNknIB0TbURU5pqJFVbOOFspVlrpVwo2H1+HUIsVDvp5VauGGDP1ZEvO8Nn5xyMEs3dakajOxlmkNW7kNgSm6A==", + "node_modules/@csstools/css-color-parser": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.0.7.tgz", + "integrity": "sha512-nkMp2mTICw32uE5NN+EsJ4f5N+IGFeCFu4bGpiKgb2Pq/7J/MpyLBeQ5ry4KKtRFZaYs6sTmcMYrSRIyj5DFKA==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" + "@csstools/color-helpers": "^5.0.1", + "@csstools/css-calc": "^2.1.1" }, "engines": { - "node": ">=6.9.0" + "node": ">=18" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3" } }, - "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.25.9.tgz", - "integrity": "sha512-o97AE4syN71M/lxrCtQByzphAdlYluKPDBzDVzMmfCobUjjhAryZV0AIpRPrxN0eAkxXO6ZLEScmt+PNhj2OTw==", + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.4.tgz", + "integrity": "sha512-Up7rBoV77rv29d3uKHUIVubz1BTcgyUK72IvCQAbfbMv584xHcGKCKbWh7i8hPrRJ7qU4Y8IO3IY9m+iTB7P3A==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, "engines": { - "node": ">=6.9.0" + "node": ">=18" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/runtime": { - "version": "7.26.7", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.7.tgz", - "integrity": "sha512-AOPI3D+a8dXnja+iwsUqGRjr1BbZIe771sXdapOtYI531gSqpi92vXivKcq2asu/DFpdl1ceFAKZyRzK2PCVcQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "regenerator-runtime": "^0.14.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/template": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", - "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.25.9", - "@babel/parser": "^7.25.9", - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" + "@csstools/css-tokenizer": "^3.0.3" } }, - "node_modules/@babel/traverse": { - "version": "7.26.7", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.7.tgz", - "integrity": "sha512-1x1sgeyRLC3r5fQOM0/xtQKsYjyxmFjaOrLJNtZ81inNjyJHGIolTULPiSc/2qe1/qfpFLisLQYFnnZl7QoedA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.26.5", - "@babel/parser": "^7.26.7", - "@babel/template": "^7.25.9", - "@babel/types": "^7.26.7", - "debug": "^4.3.1", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse/node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/types": { - "version": "7.26.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.7.tgz", - "integrity": "sha512-t8kDRGrKXyp6+tjUh7hw2RLyclsW4TRoRvRHtSyAX9Bb5ldlFh+90YAYY6awRXrlB4G5G2izNeGySpATlFzmOg==", + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.3.tgz", + "integrity": "sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" - }, "engines": { - "node": ">=6.9.0" + "node": ">=18" } }, "node_modules/@envelop/core": { @@ -963,9 +561,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz", + "integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==", "cpu": [ "ppc64" ], @@ -976,13 +574,13 @@ "aix" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz", + "integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==", "cpu": [ "arm" ], @@ -993,13 +591,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz", + "integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==", "cpu": [ "arm64" ], @@ -1010,13 +608,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz", + "integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==", "cpu": [ "x64" ], @@ -1027,13 +625,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz", + "integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==", "cpu": [ "arm64" ], @@ -1044,13 +642,13 @@ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz", + "integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==", "cpu": [ "x64" ], @@ -1061,13 +659,13 @@ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz", + "integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==", "cpu": [ "arm64" ], @@ -1078,13 +676,13 @@ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz", + "integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==", "cpu": [ "x64" ], @@ -1095,13 +693,13 @@ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz", + "integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==", "cpu": [ "arm" ], @@ -1112,13 +710,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz", + "integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==", "cpu": [ "arm64" ], @@ -1129,13 +727,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz", + "integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==", "cpu": [ "ia32" ], @@ -1146,13 +744,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz", + "integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==", "cpu": [ "loong64" ], @@ -1163,13 +761,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz", + "integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==", "cpu": [ "mips64el" ], @@ -1180,13 +778,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz", + "integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==", "cpu": [ "ppc64" ], @@ -1197,13 +795,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz", + "integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==", "cpu": [ "riscv64" ], @@ -1214,13 +812,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz", + "integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==", "cpu": [ "s390x" ], @@ -1231,13 +829,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz", + "integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==", "cpu": [ "x64" ], @@ -1248,13 +846,30 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz", + "integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz", + "integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==", "cpu": [ "x64" ], @@ -1265,13 +880,30 @@ "netbsd" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz", + "integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz", + "integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==", "cpu": [ "x64" ], @@ -1282,13 +914,13 @@ "openbsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz", + "integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==", "cpu": [ "x64" ], @@ -1299,13 +931,13 @@ "sunos" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz", + "integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==", "cpu": [ "arm64" ], @@ -1316,13 +948,13 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz", + "integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==", "cpu": [ "ia32" ], @@ -1333,13 +965,13 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz", + "integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==", "cpu": [ "x64" ], @@ -1350,7 +982,7 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@eslint-community/eslint-utils": { @@ -1383,13 +1015,13 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.1.tgz", - "integrity": "sha512-fo6Mtm5mWyKjA/Chy1BYTdn5mGJoDNjC7C64ug20ADsRDGrA85bN3uK3MaKbeRkRuuIEAR5N33Jr1pbm411/PA==", + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.2.tgz", + "integrity": "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^2.1.5", + "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" }, @@ -1398,9 +1030,9 @@ } }, "node_modules/@eslint/core": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.10.0.tgz", - "integrity": "sha512-gFHJ+xBOo4G3WRlR1e/3G8A6/KZAH6zcE/hkLRCZTi/B9avAG365QhFA8uOGzTMqgTghpn7/fSnscW++dpMSAw==", + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.11.0.tgz", + "integrity": "sha512-DWUB2pksgNEb6Bz2fggIy1wh6fGgZP4Xyy/Mt0QZPiloKKXerbqq9D3SBQTlCRYOrcRPu4vuz+CGjwdfqxnoWA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1448,9 +1080,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.19.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.19.0.tgz", - "integrity": "sha512-rbq9/g38qjfqFLOVPvwjIvFFdNziEC5S65jmjPw5r6A//QH+W91akh9irMwjDN8zKUTak6W9EsAv4m/7Wnw0UQ==", + "version": "9.20.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.20.0.tgz", + "integrity": "sha512-iZA07H9io9Wn836aVTytRaNqh00Sad+EamwOVJT12GTLw1VGMFV/4JaME+JjLtr9fiGaoWgYnS54wrfWsSs4oQ==", "dev": true, "license": "MIT", "engines": { @@ -1458,9 +1090,9 @@ } }, "node_modules/@eslint/object-schema": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.5.tgz", - "integrity": "sha512-o0bhxnL89h5Bae5T318nFoFzGy+YE5i/gGkoPAgkmTVdRKTiv3p8JHevPiPaMwoloKfEiiaHlawCqaZMqRm+XQ==", + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1481,6 +1113,19 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.10.0.tgz", + "integrity": "sha512-gFHJ+xBOo4G3WRlR1e/3G8A6/KZAH6zcE/hkLRCZTi/B9avAG365QhFA8uOGzTMqgTghpn7/fSnscW++dpMSAw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@graphql-codegen/add": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/@graphql-codegen/add/-/add-5.0.3.tgz", @@ -1503,16 +1148,16 @@ "license": "0BSD" }, "node_modules/@graphql-codegen/cli": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/@graphql-codegen/cli/-/cli-5.0.3.tgz", - "integrity": "sha512-ULpF6Sbu2d7vNEOgBtE9avQp2oMgcPY/QBYcCqk0Xru5fz+ISjcovQX29V7CS7y5wWBRzNLoXwJQGeEyWbl05g==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@graphql-codegen/cli/-/cli-5.0.5.tgz", + "integrity": "sha512-9p9SI5dPhJdyU+O6p1LUqi5ajDwpm6pUhutb1fBONd0GZltLFwkgWFiFtM6smxkYXlYVzw61p1kTtwqsuXO16w==", "dev": true, "license": "MIT", "dependencies": { "@babel/generator": "^7.18.13", "@babel/template": "^7.18.10", "@babel/types": "^7.18.13", - "@graphql-codegen/client-preset": "^4.4.0", + "@graphql-codegen/client-preset": "^4.6.0", "@graphql-codegen/core": "^4.0.2", "@graphql-codegen/plugin-helpers": "^5.0.3", "@graphql-tools/apollo-engine-loader": "^8.0.0", @@ -1525,7 +1170,7 @@ "@graphql-tools/prisma-loader": "^8.0.0", "@graphql-tools/url-loader": "^8.0.0", "@graphql-tools/utils": "^10.0.0", - "@whatwg-node/fetch": "^0.9.20", + "@whatwg-node/fetch": "^0.10.0", "chalk": "^4.1.0", "cosmiconfig": "^8.1.3", "debounce": "^1.2.0", @@ -1565,21 +1210,21 @@ } }, "node_modules/@graphql-codegen/client-preset": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/@graphql-codegen/client-preset/-/client-preset-4.5.1.tgz", - "integrity": "sha512-UE2/Kz2eaxv35HIXFwlm2QwoUH77am6+qp54aeEWYq+T+WPwmIc6+YzqtGiT/VcaXgoOUSgidREGm9R6jKcf9g==", + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/@graphql-codegen/client-preset/-/client-preset-4.6.2.tgz", + "integrity": "sha512-C7BihcGMSZq95ppLGi2HI0zt4w+n2FDoXzrP1/SUS32zbJlvb3Vod/fHdHTWcFZzlAZFCue7MNU3DbiuRjGYQg==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.20.2", "@babel/template": "^7.20.7", "@graphql-codegen/add": "^5.0.3", - "@graphql-codegen/gql-tag-operations": "4.0.12", + "@graphql-codegen/gql-tag-operations": "4.0.14", "@graphql-codegen/plugin-helpers": "^5.1.0", - "@graphql-codegen/typed-document-node": "^5.0.12", - "@graphql-codegen/typescript": "^4.1.2", - "@graphql-codegen/typescript-operations": "^4.4.0", - "@graphql-codegen/visitor-plugin-common": "^5.6.0", + "@graphql-codegen/typed-document-node": "^5.0.13", + "@graphql-codegen/typescript": "^4.1.3", + "@graphql-codegen/typescript-operations": "^4.4.1", + "@graphql-codegen/visitor-plugin-common": "^5.6.1", "@graphql-tools/documents": "^1.0.0", "@graphql-tools/utils": "^10.0.0", "@graphql-typed-document-node/core": "3.2.0", @@ -1623,14 +1268,14 @@ "license": "0BSD" }, "node_modules/@graphql-codegen/gql-tag-operations": { - "version": "4.0.12", - "resolved": "https://registry.npmjs.org/@graphql-codegen/gql-tag-operations/-/gql-tag-operations-4.0.12.tgz", - "integrity": "sha512-v279i49FJ5dMmQXIGUgm6FtnnkxtJjVJWDNYh9JK4ppvOixdHp+PmEzW227DkLN6avhVxNnYdp/1gdRBwdWypw==", + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@graphql-codegen/gql-tag-operations/-/gql-tag-operations-4.0.14.tgz", + "integrity": "sha512-/jyW6zbIt9xiLAmkLsLwJDegeFytg6n5yf79dEbkhOflclIM2t1YhEAXQxIuqgDgM/PQ34Zfu3wtgWSgUOReXg==", "dev": true, "license": "MIT", "dependencies": { "@graphql-codegen/plugin-helpers": "^5.1.0", - "@graphql-codegen/visitor-plugin-common": "5.6.0", + "@graphql-codegen/visitor-plugin-common": "5.6.1", "@graphql-tools/utils": "^10.0.0", "auto-bind": "~4.0.0", "tslib": "~2.6.0" @@ -1700,14 +1345,14 @@ "license": "0BSD" }, "node_modules/@graphql-codegen/typed-document-node": { - "version": "5.0.12", - "resolved": "https://registry.npmjs.org/@graphql-codegen/typed-document-node/-/typed-document-node-5.0.12.tgz", - "integrity": "sha512-Wsbc1AqC+MFp3maWPzrmmyHLuWCPB63qBBFLTKtO6KSsnn0KnLocBp475wkfBZnFISFvzwpJ0e6LV71gKfTofQ==", + "version": "5.0.13", + "resolved": "https://registry.npmjs.org/@graphql-codegen/typed-document-node/-/typed-document-node-5.0.13.tgz", + "integrity": "sha512-/r23W1WF9PKymIET3SdCDfyuZ6tHeflvbZF3mL3cMp4849M1fe1J2eWefeqn2MMbKATstNqRVxtrq6peJ3A/Ew==", "dev": true, "license": "MIT", "dependencies": { "@graphql-codegen/plugin-helpers": "^5.1.0", - "@graphql-codegen/visitor-plugin-common": "5.6.0", + "@graphql-codegen/visitor-plugin-common": "5.6.1", "auto-bind": "~4.0.0", "change-case-all": "1.0.15", "tslib": "~2.6.0" @@ -1727,15 +1372,15 @@ "license": "0BSD" }, "node_modules/@graphql-codegen/typescript": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@graphql-codegen/typescript/-/typescript-4.1.2.tgz", - "integrity": "sha512-GhPgfxgWEkBrvKR2y77OThus3K8B6U3ESo68l7+sHH1XiL2WapK5DdClViblJWKQerJRjfJu8tcaxQ8Wpk6Ogw==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@graphql-codegen/typescript/-/typescript-4.1.3.tgz", + "integrity": "sha512-/7qNPj+owhxBZB3Kv0FuUILZq9A6Gl5P5wiIZGAmw500n6Vc8ceOFLRXeVkyvDccxTGWS/vJv+sUnl94T2Pu+A==", "dev": true, "license": "MIT", "dependencies": { "@graphql-codegen/plugin-helpers": "^5.1.0", "@graphql-codegen/schema-ast": "^4.0.2", - "@graphql-codegen/visitor-plugin-common": "5.6.0", + "@graphql-codegen/visitor-plugin-common": "5.6.1", "auto-bind": "~4.0.0", "tslib": "~2.6.0" }, @@ -1747,15 +1392,15 @@ } }, "node_modules/@graphql-codegen/typescript-operations": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@graphql-codegen/typescript-operations/-/typescript-operations-4.4.0.tgz", - "integrity": "sha512-oVlos2ySx8xIbbe8r5ZI6mOpI+OTeP14RmS2MchBJ6DL+S9G16O6+9V3Y8V22fTnmBTZkTfAAaBv4HYhhDGWVA==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@graphql-codegen/typescript-operations/-/typescript-operations-4.4.1.tgz", + "integrity": "sha512-iqAdEe4wfxGPT9s/VD+EhehBzaTxvWdisbsqiM6dMfk+8FfjrOj8SDBsHzKwmkRcrpMK6h9gLr3XcuBPu0JoFg==", "dev": true, "license": "MIT", "dependencies": { "@graphql-codegen/plugin-helpers": "^5.1.0", - "@graphql-codegen/typescript": "^4.1.2", - "@graphql-codegen/visitor-plugin-common": "5.6.0", + "@graphql-codegen/typescript": "^4.1.3", + "@graphql-codegen/visitor-plugin-common": "5.6.1", "auto-bind": "~4.0.0", "tslib": "~2.6.0" }, @@ -1781,9 +1426,9 @@ "license": "0BSD" }, "node_modules/@graphql-codegen/visitor-plugin-common": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/@graphql-codegen/visitor-plugin-common/-/visitor-plugin-common-5.6.0.tgz", - "integrity": "sha512-PowcVPJbUqMC9xTJ/ZRX1p/fsdMZREc+69CM1YY+AlFng2lL0zsdBskFJSRoviQk2Ch9IPhKGyHxlJCy9X22tg==", + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/@graphql-codegen/visitor-plugin-common/-/visitor-plugin-common-5.6.1.tgz", + "integrity": "sha512-q+DkGWWS7pvSc1c4Hw1xD0RI+EplTe2PCyTCT0WuaswnodBytteKTqFOVVGadISLX0xhO25aANTFB4+TLwTBSA==", "dev": true, "license": "MIT", "dependencies": { @@ -1812,31 +1457,14 @@ "dev": true, "license": "0BSD" }, - "node_modules/@graphql-hive/gateway-abort-signal-any": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/@graphql-hive/gateway-abort-signal-any/-/gateway-abort-signal-any-0.0.3.tgz", - "integrity": "sha512-TLYXRiK1DxkGXEdVrwbEtQ4JrsxJ4d/zXBeTzNzvuU+doTzot0wreFgrmmOq+bvqg/E6yMs1kOvBYz477gyMjA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@graphql-tools/utils": "^10.7.0", - "tslib": "^2.8.1" - }, - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "graphql": "^15.0.0 || ^16.9.0 || ^17.0.0" - } - }, "node_modules/@graphql-tools/apollo-engine-loader": { - "version": "8.0.13", - "resolved": "https://registry.npmjs.org/@graphql-tools/apollo-engine-loader/-/apollo-engine-loader-8.0.13.tgz", - "integrity": "sha512-0FH5Yh/4wO2yBO6nZZUwfOu2Wr7fF/twJ3YjuvURH6QS1jqRBGDdZ25xbQ2/yJ4jG+7Lo3vSdJNArc2dk2Pe3A==", + "version": "8.0.15", + "resolved": "https://registry.npmjs.org/@graphql-tools/apollo-engine-loader/-/apollo-engine-loader-8.0.15.tgz", + "integrity": "sha512-4Y3gmTrC9nK8Zb19VSvPGecncUV7nFnRg9CpsdsSvjS2N98wmUhFwH9jCYQzLyDKgvlJV5PEHhAeVQPQgKGFeg==", "dev": true, "license": "MIT", "dependencies": { - "@graphql-tools/utils": "^10.7.2", + "@graphql-tools/utils": "^10.8.1", "@whatwg-node/fetch": "^0.10.0", "sync-fetch": "0.6.0-2", "tslib": "^2.4.0" @@ -1848,43 +1476,14 @@ "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, - "node_modules/@graphql-tools/apollo-engine-loader/node_modules/@whatwg-node/fetch": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/@whatwg-node/fetch/-/fetch-0.10.3.tgz", - "integrity": "sha512-jCTL/qYcIW2GihbBRHypQ/Us7saWMNZ5fsumsta+qPY0Pmi1ccba/KRQvgctmQsbP69FWemJSs8zVcFaNwdL0w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@whatwg-node/node-fetch": "^0.7.7", - "urlpattern-polyfill": "^10.0.0" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@graphql-tools/apollo-engine-loader/node_modules/@whatwg-node/node-fetch": { - "version": "0.7.7", - "resolved": "https://registry.npmjs.org/@whatwg-node/node-fetch/-/node-fetch-0.7.7.tgz", - "integrity": "sha512-BDbIMOenThOTFDBLh1WscgBNAxfDAdAdd9sMG8Ff83hYxApJVbqEct38bUAj+zn8bTsfBx/lyfnVOTyq5xUlvg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@whatwg-node/disposablestack": "^0.0.5", - "busboy": "^1.6.0", - "tslib": "^2.6.3" - }, - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/@graphql-tools/batch-execute": { - "version": "9.0.11", - "resolved": "https://registry.npmjs.org/@graphql-tools/batch-execute/-/batch-execute-9.0.11.tgz", - "integrity": "sha512-v9b618cj3hIrRGTDrOotYzpK+ZigvNcKdXK3LNBM4g/uA7pND0d4GOnuOSBQGKKN6kT/1nsz4ZpUxCoUvWPbzg==", + "version": "9.0.12", + "resolved": "https://registry.npmjs.org/@graphql-tools/batch-execute/-/batch-execute-9.0.12.tgz", + "integrity": "sha512-AUKU/KLez9LvBFh8Uur4h5n2cKrHnBFADKyHWMP7/dAuG6vzFES047bYsKQR2oWhzO26ucQMVBm9GGw1+VCv8A==", "dev": true, "license": "MIT", "dependencies": { - "@graphql-tools/utils": "^10.7.0", + "@graphql-tools/utils": "^10.8.1", "dataloader": "^2.2.3", "tslib": "^2.8.1" }, @@ -1896,14 +1495,14 @@ } }, "node_modules/@graphql-tools/code-file-loader": { - "version": "8.1.13", - "resolved": "https://registry.npmjs.org/@graphql-tools/code-file-loader/-/code-file-loader-8.1.13.tgz", - "integrity": "sha512-zEj+DJhZ8vInnCDeEcyim+LJiROPERqTCZdwHGQXKZXqab1dpyqTiIU+rjWmNUJFrqrLY15gLzrhNSLmDGDdUA==", + "version": "8.1.15", + "resolved": "https://registry.npmjs.org/@graphql-tools/code-file-loader/-/code-file-loader-8.1.15.tgz", + "integrity": "sha512-XlrzWfuoBRfpx/5Uw8VBP5rmMJyQVv8HMd6k/7TxFT/cXU34rcQfmRk6f3J7gD5+3ueqgwPcmaIn3CRp+Z0r0w==", "dev": true, "license": "MIT", "dependencies": { - "@graphql-tools/graphql-tag-pluck": "8.3.12", - "@graphql-tools/utils": "^10.7.2", + "@graphql-tools/graphql-tag-pluck": "8.3.14", + "@graphql-tools/utils": "^10.8.1", "globby": "^11.0.3", "tslib": "^2.4.0", "unixify": "^1.0.0" @@ -1916,16 +1515,16 @@ } }, "node_modules/@graphql-tools/delegate": { - "version": "10.2.10", - "resolved": "https://registry.npmjs.org/@graphql-tools/delegate/-/delegate-10.2.10.tgz", - "integrity": "sha512-+p5F0+2I0Yk8FG6EwwOjKKWRA6hFRnZekj8zUFLu5Be4s2TMt/E+KJSaL+hayyXwEqQJT8CZHmOExPPqEMzZhw==", + "version": "10.2.12", + "resolved": "https://registry.npmjs.org/@graphql-tools/delegate/-/delegate-10.2.12.tgz", + "integrity": "sha512-vyPjy7znb5wZd7OsKgK77dHJVt3RhSiq4sqdDL5d55jkIuekcJQdBoLlKN6hsakSGV31YneNGiiOPpw0kWRu3Q==", "dev": true, "license": "MIT", "dependencies": { - "@graphql-tools/batch-execute": "^9.0.11", + "@graphql-tools/batch-execute": "^9.0.12", "@graphql-tools/executor": "^1.3.10", "@graphql-tools/schema": "^10.0.11", - "@graphql-tools/utils": "^10.7.0", + "@graphql-tools/utils": "^10.8.1", "@repeaterjs/repeater": "^3.0.6", "dataloader": "^2.2.3", "dset": "^3.1.2", @@ -1956,13 +1555,13 @@ } }, "node_modules/@graphql-tools/executor": { - "version": "1.3.12", - "resolved": "https://registry.npmjs.org/@graphql-tools/executor/-/executor-1.3.12.tgz", - "integrity": "sha512-FzLXZQJOZHB75SecYFOIEEHw/qcxkRFViw0lVqHpaL07c+GqDxv6VOto0FZCIiV9RgGdyRj3O8lXDCp9Cw1MbA==", + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/@graphql-tools/executor/-/executor-1.3.14.tgz", + "integrity": "sha512-tDk8bYIgbVmGNh7cYewi5/yNCq6UoVl9ugDU4rF//+E1R5TxkXNe9nu6AonE+j6XkA/z+FofVawOqCCiJhJ40g==", "dev": true, "license": "MIT", "dependencies": { - "@graphql-tools/utils": "^10.7.2", + "@graphql-tools/utils": "^10.8.1", "@graphql-typed-document-node/core": "^3.2.0", "@repeaterjs/repeater": "^3.0.4", "@whatwg-node/disposablestack": "^0.0.5", @@ -1977,14 +1576,14 @@ } }, "node_modules/@graphql-tools/executor-common": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/@graphql-tools/executor-common/-/executor-common-0.0.1.tgz", - "integrity": "sha512-Gan7uiQhKvAAl0UM20Oy/n5NGBBDNm+ASHvnYuD8mP+dAH0qY+2QMCHyi5py28WAlhAwr0+CAemEyzY/ZzOjdQ==", + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@graphql-tools/executor-common/-/executor-common-0.0.2.tgz", + "integrity": "sha512-FD3QYYzypurGESKJ9vjKXmsmC3UuZEghHcADSGGW8NcZB66vNN557zdJ3+lwVSdYuIjiyXhyLVp1mn59w2eMQQ==", "dev": true, "license": "MIT", "dependencies": { "@envelop/core": "^5.0.2", - "@graphql-tools/utils": "^10.7.0" + "@graphql-tools/utils": "^10.8.1" }, "engines": { "node": ">=18.0.0" @@ -1994,16 +1593,16 @@ } }, "node_modules/@graphql-tools/executor-graphql-ws": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/@graphql-tools/executor-graphql-ws/-/executor-graphql-ws-1.3.7.tgz", - "integrity": "sha512-9KUrlpil5nBgcb+XRUIxNQGI+c237LAfDBqYCdLGuYT+/oZz1b4rRIe6HuRk09vuxrbaMTzm7xHhn/iuwWW4eg==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@graphql-tools/executor-graphql-ws/-/executor-graphql-ws-2.0.2.tgz", + "integrity": "sha512-P5Z8K9h4H/eNMgMMZ9gQmRDYCb2yMERc+Eaag3EJ0ifeBXZS54uhAFB3All/YzY6zy0B8BUzjHb+w+/Zan6XOg==", "dev": true, "license": "MIT", "dependencies": { - "@graphql-tools/executor-common": "^0.0.1", - "@graphql-tools/utils": "^10.7.0", + "@graphql-tools/executor-common": "^0.0.2", + "@graphql-tools/utils": "^10.8.1", "@whatwg-node/disposablestack": "^0.0.5", - "graphql-ws": "^5.14.0", + "graphql-ws": "^6.0.3", "isomorphic-ws": "^5.0.0", "tslib": "^2.8.1", "ws": "^8.17.1" @@ -2016,15 +1615,14 @@ } }, "node_modules/@graphql-tools/executor-http": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@graphql-tools/executor-http/-/executor-http-1.2.5.tgz", - "integrity": "sha512-pG5YXsF2EhKS4JMhwFwI+0S5RGhPuJ3j3Dg1vWItzeBFiTzr2+VO8yyyahHIncLx7OzSYP/6pBDFp76FC55e+g==", + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@graphql-tools/executor-http/-/executor-http-1.2.7.tgz", + "integrity": "sha512-2zPcin33Ea3GlU2p6enn2ByGh1+nfivbI2+TJLROUByjfseTH6IuAiBK0R7p0ajQevysYQw2Gh+749i4RFA3WA==", "dev": true, "license": "MIT", "dependencies": { - "@graphql-hive/gateway-abort-signal-any": "^0.0.3", - "@graphql-tools/executor-common": "^0.0.1", - "@graphql-tools/utils": "^10.7.0", + "@graphql-tools/executor-common": "^0.0.2", + "@graphql-tools/utils": "^10.8.1", "@repeaterjs/repeater": "^3.0.4", "@whatwg-node/disposablestack": "^0.0.5", "@whatwg-node/fetch": "^0.10.1", @@ -2040,43 +1638,14 @@ "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, - "node_modules/@graphql-tools/executor-http/node_modules/@whatwg-node/fetch": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/@whatwg-node/fetch/-/fetch-0.10.3.tgz", - "integrity": "sha512-jCTL/qYcIW2GihbBRHypQ/Us7saWMNZ5fsumsta+qPY0Pmi1ccba/KRQvgctmQsbP69FWemJSs8zVcFaNwdL0w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@whatwg-node/node-fetch": "^0.7.7", - "urlpattern-polyfill": "^10.0.0" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@graphql-tools/executor-http/node_modules/@whatwg-node/node-fetch": { - "version": "0.7.7", - "resolved": "https://registry.npmjs.org/@whatwg-node/node-fetch/-/node-fetch-0.7.7.tgz", - "integrity": "sha512-BDbIMOenThOTFDBLh1WscgBNAxfDAdAdd9sMG8Ff83hYxApJVbqEct38bUAj+zn8bTsfBx/lyfnVOTyq5xUlvg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@whatwg-node/disposablestack": "^0.0.5", - "busboy": "^1.6.0", - "tslib": "^2.6.3" - }, - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/@graphql-tools/executor-legacy-ws": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/@graphql-tools/executor-legacy-ws/-/executor-legacy-ws-1.1.10.tgz", - "integrity": "sha512-ENyCAky0PrcP0dR5ZNIsCTww3CdOECBor/VuRtxAA+BffFhofNiOKcgR6MEsAOH2jHh0K2wwK38sgrW+D3GX3w==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@graphql-tools/executor-legacy-ws/-/executor-legacy-ws-1.1.12.tgz", + "integrity": "sha512-thZTsx4rGbekMdJxpv0r4ettUsGRpkhSx1z86bn/WEAItn2GjPL/lR508OtP8o/BHFGrQOEIURhwtSpetdINGA==", "dev": true, "license": "MIT", "dependencies": { - "@graphql-tools/utils": "^10.7.2", + "@graphql-tools/utils": "^10.8.1", "@types/ws": "^8.0.0", "isomorphic-ws": "^5.0.0", "tslib": "^2.4.0", @@ -2090,14 +1659,14 @@ } }, "node_modules/@graphql-tools/git-loader": { - "version": "8.0.17", - "resolved": "https://registry.npmjs.org/@graphql-tools/git-loader/-/git-loader-8.0.17.tgz", - "integrity": "sha512-UYrZmO0LRQecWQx4jpZdUYBLrP0uBGiQks2RGLDpAokqo60rneBxlivjJS3HfMaohhiYy27nU00Ahy/9iTn79Q==", + "version": "8.0.19", + "resolved": "https://registry.npmjs.org/@graphql-tools/git-loader/-/git-loader-8.0.19.tgz", + "integrity": "sha512-jOJ4memazxOmPK+rebhQ99fShKibGr+WSkbsMdwWzU149fuQ7nSj4opNRkhYvDsB6ZYw3wriSlPzSlpaBTLMJQ==", "dev": true, "license": "MIT", "dependencies": { - "@graphql-tools/graphql-tag-pluck": "8.3.12", - "@graphql-tools/utils": "^10.7.2", + "@graphql-tools/graphql-tag-pluck": "8.3.14", + "@graphql-tools/utils": "^10.8.1", "is-glob": "4.0.3", "micromatch": "^4.0.8", "tslib": "^2.4.0", @@ -2111,19 +1680,18 @@ } }, "node_modules/@graphql-tools/github-loader": { - "version": "8.0.13", - "resolved": "https://registry.npmjs.org/@graphql-tools/github-loader/-/github-loader-8.0.13.tgz", - "integrity": "sha512-1eaRdfLFniIhs+MAHGDwy5Q6KraPRd48XHUV+HDuD63LHi10JtxVBPTWSUgNUkPkW0XoReyISjx9NFgTPK423A==", + "version": "8.0.15", + "resolved": "https://registry.npmjs.org/@graphql-tools/github-loader/-/github-loader-8.0.15.tgz", + "integrity": "sha512-XPrkc8YotQybbyJ6kiCNlpyCIFzsmmhwnSoqMaZrgL5RRsKbRD4CR8KTmfvMGzZmvt+u4n/te4x1QSZPLnvLqA==", "dev": true, "license": "MIT", "dependencies": { "@graphql-tools/executor-http": "^1.1.9", - "@graphql-tools/graphql-tag-pluck": "^8.3.12", - "@graphql-tools/utils": "^10.7.2", + "@graphql-tools/graphql-tag-pluck": "^8.3.14", + "@graphql-tools/utils": "^10.8.1", "@whatwg-node/fetch": "^0.10.0", "sync-fetch": "0.6.0-2", - "tslib": "^2.4.0", - "value-or-promise": "^1.0.12" + "tslib": "^2.4.0" }, "engines": { "node": ">=16.0.0" @@ -2132,44 +1700,15 @@ "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, - "node_modules/@graphql-tools/github-loader/node_modules/@whatwg-node/fetch": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/@whatwg-node/fetch/-/fetch-0.10.3.tgz", - "integrity": "sha512-jCTL/qYcIW2GihbBRHypQ/Us7saWMNZ5fsumsta+qPY0Pmi1ccba/KRQvgctmQsbP69FWemJSs8zVcFaNwdL0w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@whatwg-node/node-fetch": "^0.7.7", - "urlpattern-polyfill": "^10.0.0" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@graphql-tools/github-loader/node_modules/@whatwg-node/node-fetch": { - "version": "0.7.7", - "resolved": "https://registry.npmjs.org/@whatwg-node/node-fetch/-/node-fetch-0.7.7.tgz", - "integrity": "sha512-BDbIMOenThOTFDBLh1WscgBNAxfDAdAdd9sMG8Ff83hYxApJVbqEct38bUAj+zn8bTsfBx/lyfnVOTyq5xUlvg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@whatwg-node/disposablestack": "^0.0.5", - "busboy": "^1.6.0", - "tslib": "^2.6.3" - }, - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/@graphql-tools/graphql-file-loader": { - "version": "8.0.12", - "resolved": "https://registry.npmjs.org/@graphql-tools/graphql-file-loader/-/graphql-file-loader-8.0.12.tgz", - "integrity": "sha512-fhn6IFAgj/LOM3zlr0KDtcYDZnkWacalHOouNVDat4wzpcD4AWyvlh7PoGx3EaDtnwGqmy/l/FMjwWPTjqd9zw==", + "version": "8.0.14", + "resolved": "https://registry.npmjs.org/@graphql-tools/graphql-file-loader/-/graphql-file-loader-8.0.14.tgz", + "integrity": "sha512-Mlcd8u1u6WMRgvvERKfFRL0txTLKtmbmq0x8DzIZ7BACrYCv2rwtV79J51LbFUNBO6cMzu8rzoxTneqYm6dRNg==", "dev": true, "license": "MIT", "dependencies": { - "@graphql-tools/import": "7.0.11", - "@graphql-tools/utils": "^10.7.2", + "@graphql-tools/import": "7.0.13", + "@graphql-tools/utils": "^10.8.1", "globby": "^11.0.3", "tslib": "^2.4.0", "unixify": "^1.0.0" @@ -2182,9 +1721,9 @@ } }, "node_modules/@graphql-tools/graphql-tag-pluck": { - "version": "8.3.12", - "resolved": "https://registry.npmjs.org/@graphql-tools/graphql-tag-pluck/-/graphql-tag-pluck-8.3.12.tgz", - "integrity": "sha512-C6Ddg5RTz1WM96LYBtMuSEwN4QHfivK/vtbiAq9Soo6SoW1vGE4gzt0QS2FDVnDeB16er3h8YQZJ0xwm4pLnfA==", + "version": "8.3.14", + "resolved": "https://registry.npmjs.org/@graphql-tools/graphql-tag-pluck/-/graphql-tag-pluck-8.3.14.tgz", + "integrity": "sha512-dRo5f5/VwLI8bHRfgxl0q11fGFB/K+0/8Z8goPRQOT/Olik1RYnHVPhnK5BGSTLAMVpE3E7F+5jntkXLmuHuRA==", "dev": true, "license": "MIT", "dependencies": { @@ -2193,7 +1732,7 @@ "@babel/plugin-syntax-import-assertions": "^7.20.0", "@babel/traverse": "^7.16.8", "@babel/types": "^7.16.8", - "@graphql-tools/utils": "^10.7.2", + "@graphql-tools/utils": "^10.8.1", "tslib": "^2.4.0" }, "engines": { @@ -2204,13 +1743,13 @@ } }, "node_modules/@graphql-tools/import": { - "version": "7.0.11", - "resolved": "https://registry.npmjs.org/@graphql-tools/import/-/import-7.0.11.tgz", - "integrity": "sha512-zUru+YhjLUpdyNnTKHXLBjV6bh+CpxVhxJr5mgsFT/Lk6fdpjkEyk+hzdgINuo5GbIulFa6KpLZUBoZsDARBpQ==", + "version": "7.0.13", + "resolved": "https://registry.npmjs.org/@graphql-tools/import/-/import-7.0.13.tgz", + "integrity": "sha512-END2Bg0bvLnXDHi8WUbD7xrnf8komlIkKMOzSexFLeGpEYPlMsBOM6m0RW31Zk8zdN01gLPAyyT4tQXSIzCGIw==", "dev": true, "license": "MIT", "dependencies": { - "@graphql-tools/utils": "^10.7.2", + "@graphql-tools/utils": "^10.8.1", "resolve-from": "5.0.0", "tslib": "^2.4.0" }, @@ -2222,13 +1761,13 @@ } }, "node_modules/@graphql-tools/json-file-loader": { - "version": "8.0.11", - "resolved": "https://registry.npmjs.org/@graphql-tools/json-file-loader/-/json-file-loader-8.0.11.tgz", - "integrity": "sha512-xsfIbPyxyXWnu+GSC5HCw945Gt++b+5NeEvpunw2cK9myGhF2Bkb8N4QTNwWy+7kvOAKzNopBGqGV+x3uaQAZA==", + "version": "8.0.13", + "resolved": "https://registry.npmjs.org/@graphql-tools/json-file-loader/-/json-file-loader-8.0.13.tgz", + "integrity": "sha512-T8s05fcWvwkB9iM77RQ8WBGylkzZQ+aFzYZabg51jXvusiXWLCN3BSKPsEvSPpb3Y7JJBAK4e+Hu7UmZxqolkA==", "dev": true, "license": "MIT", "dependencies": { - "@graphql-tools/utils": "^10.7.2", + "@graphql-tools/utils": "^10.8.1", "globby": "^11.0.3", "tslib": "^2.4.0", "unixify": "^1.0.0" @@ -2241,14 +1780,14 @@ } }, "node_modules/@graphql-tools/load": { - "version": "8.0.12", - "resolved": "https://registry.npmjs.org/@graphql-tools/load/-/load-8.0.12.tgz", - "integrity": "sha512-ZFqerNO7at64N4GHT76k0AkwToHNHVkpAh1iFDRHvvFpESpZ3LDz9Y6cs54Sf6zhATecDuUSwbWZoEE2WIDExA==", + "version": "8.0.14", + "resolved": "https://registry.npmjs.org/@graphql-tools/load/-/load-8.0.14.tgz", + "integrity": "sha512-ECdc/hoSs455B6ksO25mEK/FWDPQqWXQ5aMUgjqaApiPasipOfdsZEINHc8ikm/T+hF++/h6+9PDJNsIideWuQ==", "dev": true, "license": "MIT", "dependencies": { - "@graphql-tools/schema": "^10.0.16", - "@graphql-tools/utils": "^10.7.2", + "@graphql-tools/schema": "^10.0.18", + "@graphql-tools/utils": "^10.8.1", "p-limit": "3.1.0", "tslib": "^2.4.0" }, @@ -2260,13 +1799,13 @@ } }, "node_modules/@graphql-tools/merge": { - "version": "9.0.17", - "resolved": "https://registry.npmjs.org/@graphql-tools/merge/-/merge-9.0.17.tgz", - "integrity": "sha512-3K4g8KKbIqfdmK0L5+VtZsqwAeElPkvT5ejiH+KEhn2wyKNCi4HYHxpQk8xbu+dSwLlm9Lhet1hylpo/mWCkuQ==", + "version": "9.0.19", + "resolved": "https://registry.npmjs.org/@graphql-tools/merge/-/merge-9.0.19.tgz", + "integrity": "sha512-iJP3Xke+vgnST58A1Q/1+y3bzfbYalIMnegUNupYHNvHHSE0PXoq8YieqQF8JYzWVACMxiq/M4Y1vW75mS2UVg==", "dev": true, "license": "MIT", "dependencies": { - "@graphql-tools/utils": "^10.7.2", + "@graphql-tools/utils": "^10.8.1", "tslib": "^2.4.0" }, "engines": { @@ -2323,44 +1862,15 @@ "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, - "node_modules/@graphql-tools/prisma-loader/node_modules/@whatwg-node/fetch": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/@whatwg-node/fetch/-/fetch-0.10.3.tgz", - "integrity": "sha512-jCTL/qYcIW2GihbBRHypQ/Us7saWMNZ5fsumsta+qPY0Pmi1ccba/KRQvgctmQsbP69FWemJSs8zVcFaNwdL0w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@whatwg-node/node-fetch": "^0.7.7", - "urlpattern-polyfill": "^10.0.0" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@graphql-tools/prisma-loader/node_modules/@whatwg-node/node-fetch": { - "version": "0.7.7", - "resolved": "https://registry.npmjs.org/@whatwg-node/node-fetch/-/node-fetch-0.7.7.tgz", - "integrity": "sha512-BDbIMOenThOTFDBLh1WscgBNAxfDAdAdd9sMG8Ff83hYxApJVbqEct38bUAj+zn8bTsfBx/lyfnVOTyq5xUlvg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@whatwg-node/disposablestack": "^0.0.5", - "busboy": "^1.6.0", - "tslib": "^2.6.3" - }, - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/@graphql-tools/relay-operation-optimizer": { - "version": "7.0.12", - "resolved": "https://registry.npmjs.org/@graphql-tools/relay-operation-optimizer/-/relay-operation-optimizer-7.0.12.tgz", - "integrity": "sha512-4gSefj8ZiNAtf7AZyvVMg5RHxyZnMuoDMdjWGAcIyJNOOzQ1aBSc2aFEhk94mGFbQLXdLoBSrsAhYyFGdsej6w==", + "version": "7.0.14", + "resolved": "https://registry.npmjs.org/@graphql-tools/relay-operation-optimizer/-/relay-operation-optimizer-7.0.14.tgz", + "integrity": "sha512-sHrWbqwXxiYq10stPuFEGjDTz/BQbf+ujfW3xcm5hpkSJp3LjwdFFmFaQU42w3xYWLszbp7lR29dYnhCWpegJA==", "dev": true, "license": "MIT", "dependencies": { "@ardatan/relay-compiler": "^12.0.1", - "@graphql-tools/utils": "^10.7.2", + "@graphql-tools/utils": "^10.8.1", "tslib": "^2.4.0" }, "engines": { @@ -2371,16 +1881,15 @@ } }, "node_modules/@graphql-tools/schema": { - "version": "10.0.16", - "resolved": "https://registry.npmjs.org/@graphql-tools/schema/-/schema-10.0.16.tgz", - "integrity": "sha512-G2zgb8hNg9Sx6Z2FSXm57ToNcwMls9A9cUm+EsCrnGGDsryzN5cONYePUpSGj5NCFivVp3o1FT5dg19P/1qeqQ==", + "version": "10.0.18", + "resolved": "https://registry.npmjs.org/@graphql-tools/schema/-/schema-10.0.18.tgz", + "integrity": "sha512-6j2O/07v1zbGvASizMSO7YZdGt/9HfPDx8s9n75sD2xoGfeJ2aRSmI4LkyuvqOpi0ecaa9xErnMEEvUaKBqMbw==", "dev": true, "license": "MIT", "dependencies": { - "@graphql-tools/merge": "^9.0.17", - "@graphql-tools/utils": "^10.7.2", - "tslib": "^2.4.0", - "value-or-promise": "^1.0.12" + "@graphql-tools/merge": "^9.0.19", + "@graphql-tools/utils": "^10.8.1", + "tslib": "^2.4.0" }, "engines": { "node": ">=16.0.0" @@ -2390,23 +1899,22 @@ } }, "node_modules/@graphql-tools/url-loader": { - "version": "8.0.24", - "resolved": "https://registry.npmjs.org/@graphql-tools/url-loader/-/url-loader-8.0.24.tgz", - "integrity": "sha512-f+Yt6sswiEPrcWsInMbmf+3HNENV2IZK1z3IiGMHuyqb+QsMbJLxzDPHnxMtF2QGJOiRjBQy2sF2en7DPG+jSw==", + "version": "8.0.26", + "resolved": "https://registry.npmjs.org/@graphql-tools/url-loader/-/url-loader-8.0.26.tgz", + "integrity": "sha512-oX8WWpiHHhLvxYUoo0QVN0Jjn2x2Tx9EvfccH+r7Mmgr4QpDU+t5Kpzr7qCRt9kO1SNW1ns1MeiXVWXPjoT6MQ==", "dev": true, "license": "MIT", "dependencies": { - "@graphql-tools/executor-graphql-ws": "^1.3.2", + "@graphql-tools/executor-graphql-ws": "^2.0.1", "@graphql-tools/executor-http": "^1.1.9", - "@graphql-tools/executor-legacy-ws": "^1.1.10", - "@graphql-tools/utils": "^10.7.2", + "@graphql-tools/executor-legacy-ws": "^1.1.12", + "@graphql-tools/utils": "^10.8.1", "@graphql-tools/wrap": "^10.0.16", "@types/ws": "^8.0.0", "@whatwg-node/fetch": "^0.10.0", "isomorphic-ws": "^5.0.0", "sync-fetch": "0.6.0-2", "tslib": "^2.4.0", - "value-or-promise": "^1.0.11", "ws": "^8.17.1" }, "engines": { @@ -2416,39 +1924,10 @@ "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, - "node_modules/@graphql-tools/url-loader/node_modules/@whatwg-node/fetch": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/@whatwg-node/fetch/-/fetch-0.10.3.tgz", - "integrity": "sha512-jCTL/qYcIW2GihbBRHypQ/Us7saWMNZ5fsumsta+qPY0Pmi1ccba/KRQvgctmQsbP69FWemJSs8zVcFaNwdL0w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@whatwg-node/node-fetch": "^0.7.7", - "urlpattern-polyfill": "^10.0.0" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@graphql-tools/url-loader/node_modules/@whatwg-node/node-fetch": { - "version": "0.7.7", - "resolved": "https://registry.npmjs.org/@whatwg-node/node-fetch/-/node-fetch-0.7.7.tgz", - "integrity": "sha512-BDbIMOenThOTFDBLh1WscgBNAxfDAdAdd9sMG8Ff83hYxApJVbqEct38bUAj+zn8bTsfBx/lyfnVOTyq5xUlvg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@whatwg-node/disposablestack": "^0.0.5", - "busboy": "^1.6.0", - "tslib": "^2.6.3" - }, - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/@graphql-tools/utils": { - "version": "10.7.2", - "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-10.7.2.tgz", - "integrity": "sha512-Wn85S+hfkzfVFpXVrQ0hjnePa3p28aB6IdAGCiD1SqBCSMDRzL+OFEtyAyb30nV9Mqflqs9lCqjqlR2puG857Q==", + "version": "10.8.1", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-10.8.1.tgz", + "integrity": "sha512-fI5NNuqeEAHyp7NuCDjvxWR5PTUXM4AqY9BoC59ZcX4nePAJje27ZsFHbAMS6EKDosY1K/D4ADxsO0P5+FH07A==", "dev": true, "license": "MIT", "dependencies": { @@ -2465,15 +1944,15 @@ } }, "node_modules/@graphql-tools/wrap": { - "version": "10.0.28", - "resolved": "https://registry.npmjs.org/@graphql-tools/wrap/-/wrap-10.0.28.tgz", - "integrity": "sha512-QkoQTybeBfji2Na67jgdJNDKKgLgH2cAMfxCDTbNpzksah0u/b4LD5RebZTXZ8FAsbFUMRbDGh7aL1Th+dbffg==", + "version": "10.0.30", + "resolved": "https://registry.npmjs.org/@graphql-tools/wrap/-/wrap-10.0.30.tgz", + "integrity": "sha512-zzG7ol4DdbRtvEe2QdPkDucRT99gfvQtPnMqUEOIEjwfHWTbjGFCh/E4nynOSpEc2MiR0xaZIPAaIptAho/uoQ==", "dev": true, "license": "MIT", "dependencies": { - "@graphql-tools/delegate": "^10.2.10", + "@graphql-tools/delegate": "^10.2.12", "@graphql-tools/schema": "^10.0.11", - "@graphql-tools/utils": "^10.7.0", + "@graphql-tools/utils": "^10.8.1", "tslib": "^2.8.1" }, "engines": { @@ -2560,9 +2039,9 @@ } }, "node_modules/@iconify-json/material-symbols": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/@iconify-json/material-symbols/-/material-symbols-1.2.13.tgz", - "integrity": "sha512-7sVem5paCT87e1Hvk9tA2CHcmTSCwlJj6UlhouyNfgAqoC0W/Yj2vA9NFOdjviHgvoHT9kDCEfKxT5tpEMH6qA==", + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@iconify-json/material-symbols/-/material-symbols-1.2.14.tgz", + "integrity": "sha512-S0AAFFQPVr8Dkrprspz/otNjxdD3rJRXDGZjbO8a8zn8ZR5mO8jAF81lVoTfUWxPH6SCtH2lK1JQGXHGPxld7g==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2746,13 +2225,6 @@ "integrity": "sha512-0D6VFTWahCZymjTA33qmTOgo3sjePaYFalOura76kN/CKJ3HtS4lyxdNLklhrnDrqjWHqgZEqPMwuTGUqDL/+w==", "license": "MIT" }, - "node_modules/@kamilkisiela/fast-url-parser": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@kamilkisiela/fast-url-parser/-/fast-url-parser-1.1.4.tgz", - "integrity": "sha512-gbkePEBupNydxCelHCESvFSFM8XPh1Zs/OAVRW/rKpEqPAl5PbOM90Si8mv9bvnR53uPD2s/FiRxdvSejpRJew==", - "dev": true, - "license": "MIT" - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2817,9 +2289,9 @@ "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.32.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.32.0.tgz", - "integrity": "sha512-G2fUQQANtBPsNwiVFg4zKiPQyjVKZCUdQUol53R8E71J7AsheRMV/Yv/nB8giOcOVqP7//eB5xPqieBYZe9bGg==", + "version": "4.34.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.6.tgz", + "integrity": "sha512-+GcCXtOQoWuC7hhX1P00LqjjIiS/iOouHXhMdiDSnq/1DGTox4SpUvO52Xm+div6+106r+TcvOeo/cxvyEyTgg==", "cpu": [ "arm" ], @@ -2831,9 +2303,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.32.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.32.0.tgz", - "integrity": "sha512-qhFwQ+ljoymC+j5lXRv8DlaJYY/+8vyvYmVx074zrLsu5ZGWYsJNLjPPVJJjhZQpyAKUGPydOq9hRLLNvh1s3A==", + "version": "4.34.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.34.6.tgz", + "integrity": "sha512-E8+2qCIjciYUnCa1AiVF1BkRgqIGW9KzJeesQqVfyRITGQN+dFuoivO0hnro1DjT74wXLRZ7QF8MIbz+luGaJA==", "cpu": [ "arm64" ], @@ -2845,9 +2317,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.32.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.32.0.tgz", - "integrity": "sha512-44n/X3lAlWsEY6vF8CzgCx+LQaoqWGN7TzUfbJDiTIOjJm4+L2Yq+r5a8ytQRGyPqgJDs3Rgyo8eVL7n9iW6AQ==", + "version": "4.34.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.6.tgz", + "integrity": "sha512-z9Ib+OzqN3DZEjX7PDQMHEhtF+t6Mi2z/ueChQPLS/qUMKY7Ybn5A2ggFoKRNRh1q1T03YTQfBTQCJZiepESAg==", "cpu": [ "arm64" ], @@ -2859,9 +2331,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.32.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.32.0.tgz", - "integrity": "sha512-F9ct0+ZX5Np6+ZDztxiGCIvlCaW87HBdHcozUfsHnj1WCUTBUubAoanhHUfnUHZABlElyRikI0mgcw/qdEm2VQ==", + "version": "4.34.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.6.tgz", + "integrity": "sha512-PShKVY4u0FDAR7jskyFIYVyHEPCPnIQY8s5OcXkdU8mz3Y7eXDJPdyM/ZWjkYdR2m0izD9HHWA8sGcXn+Qrsyg==", "cpu": [ "x64" ], @@ -2873,9 +2345,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.32.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.32.0.tgz", - "integrity": "sha512-JpsGxLBB2EFXBsTLHfkZDsXSpSmKD3VxXCgBQtlPcuAqB8TlqtLcbeMhxXQkCDv1avgwNjF8uEIbq5p+Cee0PA==", + "version": "4.34.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.34.6.tgz", + "integrity": "sha512-YSwyOqlDAdKqs0iKuqvRHLN4SrD2TiswfoLfvYXseKbL47ht1grQpq46MSiQAx6rQEN8o8URtpXARCpqabqxGQ==", "cpu": [ "arm64" ], @@ -2887,9 +2359,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.32.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.32.0.tgz", - "integrity": "sha512-wegiyBT6rawdpvnD9lmbOpx5Sph+yVZKHbhnSP9MqUEDX08G4UzMU+D87jrazGE7lRSyTRs6NEYHtzfkJ3FjjQ==", + "version": "4.34.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.34.6.tgz", + "integrity": "sha512-HEP4CgPAY1RxXwwL5sPFv6BBM3tVeLnshF03HMhJYCNc6kvSqBgTMmsEjb72RkZBAWIqiPUyF1JpEBv5XT9wKQ==", "cpu": [ "x64" ], @@ -2901,9 +2373,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.32.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.32.0.tgz", - "integrity": "sha512-3pA7xecItbgOs1A5H58dDvOUEboG5UfpTq3WzAdF54acBbUM+olDJAPkgj1GRJ4ZqE12DZ9/hNS2QZk166v92A==", + "version": "4.34.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.34.6.tgz", + "integrity": "sha512-88fSzjC5xeH9S2Vg3rPgXJULkHcLYMkh8faix8DX4h4TIAL65ekwuQMA/g2CXq8W+NJC43V6fUpYZNjaX3+IIg==", "cpu": [ "arm" ], @@ -2915,9 +2387,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.32.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.32.0.tgz", - "integrity": "sha512-Y7XUZEVISGyge51QbYyYAEHwpGgmRrAxQXO3siyYo2kmaj72USSG8LtlQQgAtlGfxYiOwu+2BdbPjzEpcOpRmQ==", + "version": "4.34.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.34.6.tgz", + "integrity": "sha512-wM4ztnutBqYFyvNeR7Av+reWI/enK9tDOTKNF+6Kk2Q96k9bwhDDOlnCUNRPvromlVXo04riSliMBs/Z7RteEg==", "cpu": [ "arm" ], @@ -2929,9 +2401,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.32.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.32.0.tgz", - "integrity": "sha512-r7/OTF5MqeBrZo5omPXcTnjvv1GsrdH8a8RerARvDFiDwFpDVDnJyByYM/nX+mvks8XXsgPUxkwe/ltaX2VH7w==", + "version": "4.34.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.6.tgz", + "integrity": "sha512-9RyprECbRa9zEjXLtvvshhw4CMrRa3K+0wcp3KME0zmBe1ILmvcVHnypZ/aIDXpRyfhSYSuN4EPdCCj5Du8FIA==", "cpu": [ "arm64" ], @@ -2943,9 +2415,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.32.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.32.0.tgz", - "integrity": "sha512-HJbifC9vex9NqnlodV2BHVFNuzKL5OnsV2dvTw6e1dpZKkNjPG6WUq+nhEYV6Hv2Bv++BXkwcyoGlXnPrjAKXw==", + "version": "4.34.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.6.tgz", + "integrity": "sha512-qTmklhCTyaJSB05S+iSovfo++EwnIEZxHkzv5dep4qoszUMX5Ca4WM4zAVUMbfdviLgCSQOu5oU8YoGk1s6M9Q==", "cpu": [ "arm64" ], @@ -2957,9 +2429,9 @@ ] }, "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.32.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.32.0.tgz", - "integrity": "sha512-VAEzZTD63YglFlWwRj3taofmkV1V3xhebDXffon7msNz4b14xKsz7utO6F8F4cqt8K/ktTl9rm88yryvDpsfOw==", + "version": "4.34.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.34.6.tgz", + "integrity": "sha512-4Qmkaps9yqmpjY5pvpkfOerYgKNUGzQpFxV6rnS7c/JfYbDSU0y6WpbbredB5cCpLFGJEqYX40WUmxMkwhWCjw==", "cpu": [ "loong64" ], @@ -2971,9 +2443,9 @@ ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.32.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.32.0.tgz", - "integrity": "sha512-Sts5DST1jXAc9YH/iik1C9QRsLcCoOScf3dfbY5i4kH9RJpKxiTBXqm7qU5O6zTXBTEZry69bGszr3SMgYmMcQ==", + "version": "4.34.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.34.6.tgz", + "integrity": "sha512-Zsrtux3PuaxuBTX/zHdLaFmcofWGzaWW1scwLU3ZbW/X+hSsFbz9wDIp6XvnT7pzYRl9MezWqEqKy7ssmDEnuQ==", "cpu": [ "ppc64" ], @@ -2985,9 +2457,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.32.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.32.0.tgz", - "integrity": "sha512-qhlXeV9AqxIyY9/R1h1hBD6eMvQCO34ZmdYvry/K+/MBs6d1nRFLm6BOiITLVI+nFAAB9kUB6sdJRKyVHXnqZw==", + "version": "4.34.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.34.6.tgz", + "integrity": "sha512-aK+Zp+CRM55iPrlyKiU3/zyhgzWBxLVrw2mwiQSYJRobCURb781+XstzvA8Gkjg/hbdQFuDw44aUOxVQFycrAg==", "cpu": [ "riscv64" ], @@ -2999,9 +2471,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.32.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.32.0.tgz", - "integrity": "sha512-8ZGN7ExnV0qjXa155Rsfi6H8M4iBBwNLBM9lcVS+4NcSzOFaNqmt7djlox8pN1lWrRPMRRQ8NeDlozIGx3Omsw==", + "version": "4.34.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.34.6.tgz", + "integrity": "sha512-WoKLVrY9ogmaYPXwTH326+ErlCIgMmsoRSx6bO+l68YgJnlOXhygDYSZe/qbUJCSiCiZAQ+tKm88NcWuUXqOzw==", "cpu": [ "s390x" ], @@ -3013,9 +2485,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.32.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.32.0.tgz", - "integrity": "sha512-VDzNHtLLI5s7xd/VubyS10mq6TxvZBp+4NRWoW+Hi3tgV05RtVm4qK99+dClwTN1McA6PHwob6DEJ6PlXbY83A==", + "version": "4.34.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.6.tgz", + "integrity": "sha512-Sht4aFvmA4ToHd2vFzwMFaQCiYm2lDFho5rPcvPBT5pCdC+GwHG6CMch4GQfmWTQ1SwRKS0dhDYb54khSrjDWw==", "cpu": [ "x64" ], @@ -3027,9 +2499,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.32.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.32.0.tgz", - "integrity": "sha512-qcb9qYDlkxz9DxJo7SDhWxTWV1gFuwznjbTiov289pASxlfGbaOD54mgbs9+z94VwrXtKTu+2RqwlSTbiOqxGg==", + "version": "4.34.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.6.tgz", + "integrity": "sha512-zmmpOQh8vXc2QITsnCiODCDGXFC8LMi64+/oPpPx5qz3pqv0s6x46ps4xoycfUiVZps5PFn1gksZzo4RGTKT+A==", "cpu": [ "x64" ], @@ -3041,9 +2513,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.32.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.32.0.tgz", - "integrity": "sha512-pFDdotFDMXW2AXVbfdUEfidPAk/OtwE/Hd4eYMTNVVaCQ6Yl8et0meDaKNL63L44Haxv4UExpv9ydSf3aSayDg==", + "version": "4.34.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.6.tgz", + "integrity": "sha512-3/q1qUsO/tLqGBaD4uXsB6coVGB3usxw3qyeVb59aArCgedSF66MPdgRStUd7vbZOsko/CgVaY5fo2vkvPLWiA==", "cpu": [ "arm64" ], @@ -3055,9 +2527,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.32.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.32.0.tgz", - "integrity": "sha512-/TG7WfrCAjeRNDvI4+0AAMoHxea/USWhAzf9PVDFHbcqrQ7hMMKp4jZIy4VEjk72AAfN5k4TiSMRXRKf/0akSw==", + "version": "4.34.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.34.6.tgz", + "integrity": "sha512-oLHxuyywc6efdKVTxvc0135zPrRdtYVjtVD5GUm55I3ODxhU/PwkQFD97z16Xzxa1Fz0AEe4W/2hzRtd+IfpOA==", "cpu": [ "ia32" ], @@ -3069,9 +2541,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.32.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.32.0.tgz", - "integrity": "sha512-5hqO5S3PTEO2E5VjCePxv40gIgyS2KvO7E7/vvC/NbIW4SIRamkMr1hqj+5Y67fbBWv/bQLB6KelBQmXlyCjWA==", + "version": "4.34.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.6.tgz", + "integrity": "sha512-0PVwmgzZ8+TZ9oGBmdZoQVXflbvuwzN/HRclujpl4N/q3i+y0lqLw8n1bXA8ru3sApDjlmONaNAuYr38y1Kr9w==", "cpu": [ "x64" ], @@ -3093,9 +2565,9 @@ } }, "node_modules/@sveltejs/kit": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.16.1.tgz", - "integrity": "sha512-2pF5sgGJx9brYZ/9nNDYnh5KX0JguPF14dnvvtf/MqrvlWrDj/e7Rk3LBJPecFLLK1GRs6ZniD24gFPqZm/NFw==", + "version": "2.17.1", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.17.1.tgz", + "integrity": "sha512-CpoGSLqE2MCmcQwA2CWJvOsZ9vW+p/1H3itrFykdgajUNAEyQPbsaSn7fZb6PLHQwe+07njxje9ss0fjZoCAyw==", "dev": true, "license": "MIT", "dependencies": { @@ -3124,44 +2596,43 @@ } }, "node_modules/@sveltejs/vite-plugin-svelte": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-3.1.2.tgz", - "integrity": "sha512-Txsm1tJvtiYeLUVRNqxZGKR/mI+CzuIQuc2gn+YCs9rMTowpNZ2Nqt53JdL8KF9bLhAf2ruR/dr9eZCwdTriRA==", + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-5.0.3.tgz", + "integrity": "sha512-MCFS6CrQDu1yGwspm4qtli0e63vaPCehf6V7pIMP15AsWgMKrqDGCPFF/0kn4SP0ii4aySu4Pa62+fIRGFMjgw==", "dev": true, "license": "MIT", "dependencies": { - "@sveltejs/vite-plugin-svelte-inspector": "^2.1.0", - "debug": "^4.3.4", + "@sveltejs/vite-plugin-svelte-inspector": "^4.0.1", + "debug": "^4.4.0", "deepmerge": "^4.3.1", "kleur": "^4.1.5", - "magic-string": "^0.30.10", - "svelte-hmr": "^0.16.0", - "vitefu": "^0.2.5" + "magic-string": "^0.30.15", + "vitefu": "^1.0.4" }, "engines": { - "node": "^18.0.0 || >=20" + "node": "^18.0.0 || ^20.0.0 || >=22" }, "peerDependencies": { - "svelte": "^4.0.0 || ^5.0.0-next.0", - "vite": "^5.0.0" + "svelte": "^5.0.0", + "vite": "^6.0.0" } }, "node_modules/@sveltejs/vite-plugin-svelte-inspector": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-2.1.0.tgz", - "integrity": "sha512-9QX28IymvBlSCqsCll5t0kQVxipsfhFFL+L2t3nTWfXnddYwxBuAEtTtlaVQpRz9c37BhJjltSeY4AJSC03SSg==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-4.0.1.tgz", + "integrity": "sha512-J/Nmb2Q2y7mck2hyCX4ckVHcR5tu2J+MtBEQqpDrrgELZ2uvraQcK/ioCV61AqkdXFgriksOKIceDcQmqnGhVw==", "dev": true, "license": "MIT", "dependencies": { - "debug": "^4.3.4" + "debug": "^4.3.7" }, "engines": { - "node": "^18.0.0 || >=20" + "node": "^18.0.0 || ^20.0.0 || >=22" }, "peerDependencies": { - "@sveltejs/vite-plugin-svelte": "^3.0.0", - "svelte": "^4.0.0 || ^5.0.0-next.0", - "vite": "^5.0.0" + "@sveltejs/vite-plugin-svelte": "^5.0.0", + "svelte": "^5.0.0", + "vite": "^6.0.0" } }, "node_modules/@types/cookie": { @@ -3177,6 +2648,13 @@ "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", "license": "MIT" }, + "node_modules/@types/gensync": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@types/gensync/-/gensync-1.0.4.tgz", + "integrity": "sha512-C3YYeRQWp2fmq9OryX+FoDy8nXS6scQ7dPptD8LnFDAUNcKWJjXQKDNJD3HVm+kOUsXhTOkpi69vI4EuAr95bA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/js-yaml": { "version": "4.0.9", "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", @@ -3192,22 +2670,15 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.10.10", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.10.tgz", - "integrity": "sha512-X47y/mPNzxviAGY5TcYPtYL8JsY3kAq2n8fMmKoRCxq/c4v4pyGNCzM2R6+M5/umG4ZfHuT+sgqDYqWc9rJ6ww==", + "version": "22.13.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.2.tgz", + "integrity": "sha512-Z+r8y3XL9ZpI2EY52YYygAFmo2/oWfNSj4BCpAXE2McAexDk8VcnBMGC9Djn9gTKt4d2T/hhXqmPzo4hfIXtTg==", "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.20.0" } }, - "node_modules/@types/pug": { - "version": "2.0.10", - "resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.10.tgz", - "integrity": "sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/ws": { "version": "8.5.14", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.14.tgz", @@ -3219,21 +2690,21 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.21.0.tgz", - "integrity": "sha512-eTH+UOR4I7WbdQnG4Z48ebIA6Bgi7WO8HvFEneeYBxG8qCOYgTOFPSg6ek9ITIDvGjDQzWHcoWHCDO2biByNzA==", + "version": "8.24.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.24.0.tgz", + "integrity": "sha512-aFcXEJJCI4gUdXgoo/j9udUYIHgF23MFkg09LFz2dzEmU0+1Plk4rQWv/IYKvPHAtlkkGoB3m5e6oUp+JPsNaQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.21.0", - "@typescript-eslint/type-utils": "8.21.0", - "@typescript-eslint/utils": "8.21.0", - "@typescript-eslint/visitor-keys": "8.21.0", + "@typescript-eslint/scope-manager": "8.24.0", + "@typescript-eslint/type-utils": "8.24.0", + "@typescript-eslint/utils": "8.24.0", + "@typescript-eslint/visitor-keys": "8.24.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", - "ts-api-utils": "^2.0.0" + "ts-api-utils": "^2.0.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3249,16 +2720,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.21.0.tgz", - "integrity": "sha512-Wy+/sdEH9kI3w9civgACwabHbKl+qIOu0uFZ9IMKzX3Jpv9og0ZBJrZExGrPpFAY7rWsXuxs5e7CPPP17A4eYA==", + "version": "8.24.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.24.0.tgz", + "integrity": "sha512-MFDaO9CYiard9j9VepMNa9MTcqVvSny2N4hkY6roquzj8pdCBRENhErrteaQuu7Yjn1ppk0v1/ZF9CG3KIlrTA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.21.0", - "@typescript-eslint/types": "8.21.0", - "@typescript-eslint/typescript-estree": "8.21.0", - "@typescript-eslint/visitor-keys": "8.21.0", + "@typescript-eslint/scope-manager": "8.24.0", + "@typescript-eslint/types": "8.24.0", + "@typescript-eslint/typescript-estree": "8.24.0", + "@typescript-eslint/visitor-keys": "8.24.0", "debug": "^4.3.4" }, "engines": { @@ -3274,14 +2745,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.21.0.tgz", - "integrity": "sha512-G3IBKz0/0IPfdeGRMbp+4rbjfSSdnGkXsM/pFZA8zM9t9klXDnB/YnKOBQ0GoPmoROa4bCq2NeHgJa5ydsQ4mA==", + "version": "8.24.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.24.0.tgz", + "integrity": "sha512-HZIX0UByphEtdVBKaQBgTDdn9z16l4aTUz8e8zPQnyxwHBtf5vtl1L+OhH+m1FGV9DrRmoDuYKqzVrvWDcDozw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.21.0", - "@typescript-eslint/visitor-keys": "8.21.0" + "@typescript-eslint/types": "8.24.0", + "@typescript-eslint/visitor-keys": "8.24.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3292,16 +2763,16 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.21.0.tgz", - "integrity": "sha512-95OsL6J2BtzoBxHicoXHxgk3z+9P3BEcQTpBKriqiYzLKnM2DeSqs+sndMKdamU8FosiadQFT3D+BSL9EKnAJQ==", + "version": "8.24.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.24.0.tgz", + "integrity": "sha512-8fitJudrnY8aq0F1wMiPM1UUgiXQRJ5i8tFjq9kGfRajU+dbPyOuHbl0qRopLEidy0MwqgTHDt6CnSeXanNIwA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.21.0", - "@typescript-eslint/utils": "8.21.0", + "@typescript-eslint/typescript-estree": "8.24.0", + "@typescript-eslint/utils": "8.24.0", "debug": "^4.3.4", - "ts-api-utils": "^2.0.0" + "ts-api-utils": "^2.0.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3316,9 +2787,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.21.0.tgz", - "integrity": "sha512-PAL6LUuQwotLW2a8VsySDBwYMm129vFm4tMVlylzdoTybTHaAi0oBp7Ac6LhSrHHOdLM3efH+nAR6hAWoMF89A==", + "version": "8.24.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.24.0.tgz", + "integrity": "sha512-VacJCBTyje7HGAw7xp11q439A+zeGG0p0/p2zsZwpnMzjPB5WteaWqt4g2iysgGFafrqvyLWqq6ZPZAOCoefCw==", "dev": true, "license": "MIT", "engines": { @@ -3330,20 +2801,20 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.21.0.tgz", - "integrity": "sha512-x+aeKh/AjAArSauz0GiQZsjT8ciadNMHdkUSwBB9Z6PrKc/4knM4g3UfHml6oDJmKC88a6//cdxnO/+P2LkMcg==", + "version": "8.24.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.24.0.tgz", + "integrity": "sha512-ITjYcP0+8kbsvT9bysygfIfb+hBj6koDsu37JZG7xrCiy3fPJyNmfVtaGsgTUSEuTzcvME5YI5uyL5LD1EV5ZQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.21.0", - "@typescript-eslint/visitor-keys": "8.21.0", + "@typescript-eslint/types": "8.24.0", + "@typescript-eslint/visitor-keys": "8.24.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", - "ts-api-utils": "^2.0.0" + "ts-api-utils": "^2.0.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3383,9 +2854,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", "dev": true, "license": "ISC", "bin": { @@ -3396,16 +2867,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.21.0.tgz", - "integrity": "sha512-xcXBfcq0Kaxgj7dwejMbFyq7IOHgpNMtVuDveK7w3ZGwG9owKzhALVwKpTF2yrZmEwl9SWdetf3fxNzJQaVuxw==", + "version": "8.24.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.24.0.tgz", + "integrity": "sha512-07rLuUBElvvEb1ICnafYWr4hk8/U7X9RDCOqd9JcAMtjh/9oRmcfN4yGzbPVirgMR0+HLVHehmu19CWeh7fsmQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.21.0", - "@typescript-eslint/types": "8.21.0", - "@typescript-eslint/typescript-estree": "8.21.0" + "@typescript-eslint/scope-manager": "8.24.0", + "@typescript-eslint/types": "8.24.0", + "@typescript-eslint/typescript-estree": "8.24.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3420,13 +2891,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.21.0.tgz", - "integrity": "sha512-BkLMNpdV6prozk8LlyK/SOoWLmUFi+ZD+pcqti9ILCbVvHGk1ui1g4jJOc2WDLaeExz2qWwojxlPce5PljcT3w==", + "version": "8.24.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.24.0.tgz", + "integrity": "sha512-kArLq83QxGLbuHrTMoOEWO+l2MwsNS2TGISEdx8xgqpkbytB07XmlQyQdNDrCc1ecSqx0cnmhGvpX+VBwqqSkg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.21.0", + "@typescript-eslint/types": "8.24.0", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -3475,14 +2946,14 @@ } }, "node_modules/@vitest/expect": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.0.4.tgz", - "integrity": "sha512-Nm5kJmYw6P2BxhJPkO3eKKhGYKRsnqJqf+r0yOGRKpEP+bSCBDsjXgiu1/5QFrnPMEgzfC38ZEjvCFgaNBC0Eg==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.0.5.tgz", + "integrity": "sha512-nNIOqupgZ4v5jWuQx2DSlHLEs7Q4Oh/7AYwNyE+k0UQzG7tSmjPXShUikn1mpNGzYEN2jJbTvLejwShMitovBA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.0.4", - "@vitest/utils": "3.0.4", + "@vitest/spy": "3.0.5", + "@vitest/utils": "3.0.5", "chai": "^5.1.2", "tinyrainbow": "^2.0.0" }, @@ -3491,13 +2962,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.0.4.tgz", - "integrity": "sha512-gEef35vKafJlfQbnyOXZ0Gcr9IBUsMTyTLXsEQwuyYAerpHqvXhzdBnDFuHLpFqth3F7b6BaFr4qV/Cs1ULx5A==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.0.5.tgz", + "integrity": "sha512-CLPNBFBIE7x6aEGbIjaQAX03ZZlBMaWwAjBdMkIf/cAn6xzLTiM3zYqO/WAbieEjsAZir6tO71mzeHZoodThvw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.0.4", + "@vitest/spy": "3.0.5", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, @@ -3518,9 +2989,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.4.tgz", - "integrity": "sha512-ts0fba+dEhK2aC9PFuZ9LTpULHpY/nd6jhAQ5IMU7Gaj7crPCTdCFfgvXxruRBLFS+MLraicCuFXxISEq8C93g==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.5.tgz", + "integrity": "sha512-CjUtdmpOcm4RVtB+up8r2vVDLR16Mgm/bYdkGFe3Yj/scRfCpbSi2W/BDSDcFK7ohw8UXvjMbOp9H4fByd/cOA==", "dev": true, "license": "MIT", "dependencies": { @@ -3531,13 +3002,13 @@ } }, "node_modules/@vitest/runner": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.0.4.tgz", - "integrity": "sha512-dKHzTQ7n9sExAcWH/0sh1elVgwc7OJ2lMOBrAm73J7AH6Pf9T12Zh3lNE1TETZaqrWFXtLlx3NVrLRb5hCK+iw==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.0.5.tgz", + "integrity": "sha512-BAiZFityFexZQi2yN4OX3OkJC6scwRo8EhRB0Z5HIGGgd2q+Nq29LgHU/+ovCtd0fOfXj5ZI6pwdlUmC5bpi8A==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "3.0.4", + "@vitest/utils": "3.0.5", "pathe": "^2.0.2" }, "funding": { @@ -3545,13 +3016,13 @@ } }, "node_modules/@vitest/snapshot": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.0.4.tgz", - "integrity": "sha512-+p5knMLwIk7lTQkM3NonZ9zBewzVp9EVkVpvNta0/PlFWpiqLaRcF4+33L1it3uRUCh0BGLOaXPPGEjNKfWb4w==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.0.5.tgz", + "integrity": "sha512-GJPZYcd7v8QNUJ7vRvLDmRwl+a1fGg4T/54lZXe+UOGy47F9yUfE18hRCtXL5aHN/AONu29NGzIXSVFh9K0feA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.0.4", + "@vitest/pretty-format": "3.0.5", "magic-string": "^0.30.17", "pathe": "^2.0.2" }, @@ -3560,9 +3031,9 @@ } }, "node_modules/@vitest/spy": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.0.4.tgz", - "integrity": "sha512-sXIMF0oauYyUy2hN49VFTYodzEAu744MmGcPR3ZBsPM20G+1/cSW/n1U+3Yu/zHxX2bIDe1oJASOkml+osTU6Q==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.0.5.tgz", + "integrity": "sha512-5fOzHj0WbUNqPK6blI/8VzZdkBlQLnT25knX0r4dbZI9qoZDf3qAdjoMmDcLG5A83W6oUUFJgUd0EYBc2P5xqg==", "dev": true, "license": "MIT", "dependencies": { @@ -3573,13 +3044,13 @@ } }, "node_modules/@vitest/utils": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.0.4.tgz", - "integrity": "sha512-8BqC1ksYsHtbWH+DfpOAKrFw3jl3Uf9J7yeFh85Pz52IWuh1hBBtyfEbRNNZNjl8H8A5yMLH9/t+k7HIKzQcZQ==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.0.5.tgz", + "integrity": "sha512-N9AX0NUoUtVwKwy21JtwzaqR5L5R5A99GAbrHfCCXK1lp593i/3AZAXhSP43wRQuxYsflrdzEfXZFo1reR1Nkg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.0.4", + "@vitest/pretty-format": "3.0.5", "loupe": "^3.1.2", "tinyrainbow": "^2.0.0" }, @@ -3601,13 +3072,13 @@ } }, "node_modules/@whatwg-node/fetch": { - "version": "0.9.23", - "resolved": "https://registry.npmjs.org/@whatwg-node/fetch/-/fetch-0.9.23.tgz", - "integrity": "sha512-7xlqWel9JsmxahJnYVUj/LLxWcnA93DR4c9xlw3U814jWTiYalryiH1qToik1hOxweKKRLi4haXHM5ycRksPBA==", + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@whatwg-node/fetch/-/fetch-0.10.3.tgz", + "integrity": "sha512-jCTL/qYcIW2GihbBRHypQ/Us7saWMNZ5fsumsta+qPY0Pmi1ccba/KRQvgctmQsbP69FWemJSs8zVcFaNwdL0w==", "dev": true, "license": "MIT", "dependencies": { - "@whatwg-node/node-fetch": "^0.6.0", + "@whatwg-node/node-fetch": "^0.7.7", "urlpattern-polyfill": "^10.0.0" }, "engines": { @@ -3615,15 +3086,14 @@ } }, "node_modules/@whatwg-node/node-fetch": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@whatwg-node/node-fetch/-/node-fetch-0.6.0.tgz", - "integrity": "sha512-tcZAhrpx6oVlkEsRngeTEEE7I5/QdLjeEz4IlekabGaESP7+Dkm/6a9KcF1KdCBB7mO9PXtBkwCuTCt8+UPg8Q==", + "version": "0.7.9", + "resolved": "https://registry.npmjs.org/@whatwg-node/node-fetch/-/node-fetch-0.7.9.tgz", + "integrity": "sha512-rp4/lzxK5DDxFFdXcCR3gG4OyWPHqmN1GrKostx2NEy7sHUQA/B3j0M2z+qfMQVg5SC4RRm0eFwD73e+GgAM3A==", "dev": true, "license": "MIT", "dependencies": { - "@kamilkisiela/fast-url-parser": "^1.1.4", + "@whatwg-node/disposablestack": "^0.0.5", "busboy": "^1.6.0", - "fast-querystring": "^1.1.1", "tslib": "^2.6.3" }, "engines": { @@ -3662,6 +3132,15 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/acorn-typescript": { + "version": "1.4.13", + "resolved": "https://registry.npmjs.org/acorn-typescript/-/acorn-typescript-1.4.13.tgz", + "integrity": "sha512-xsc9Xv0xlVfwp2o7sQ+GCQ1PgbkdcpWdTzrwXxO3xDMTAywVS3oXVOcOHuRjAPkS4P9b+yc/qNF15460v+jp4Q==", + "license": "MIT", + "peerDependencies": { + "acorn": ">=8.9.0" + } + }, "node_modules/agent-base": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", @@ -3826,6 +3305,13 @@ "node": ">=8" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, "node_modules/auto-bind": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/auto-bind/-/auto-bind-4.0.0.tgz", @@ -3886,52 +3372,6 @@ "node": ">= 0.4" } }, - "node_modules/babel-plugin-syntax-trailing-function-commas": { - "version": "7.0.0-beta.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-7.0.0-beta.0.tgz", - "integrity": "sha512-Xj9XuRuz3nTSbaTXWv3itLOcxyF4oPD8douBBmj7U9BBC6nEBYfyOJYQMf/8PJAFotC62UY5dFfIGEPr7WswzQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/babel-preset-fbjs": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/babel-preset-fbjs/-/babel-preset-fbjs-3.4.0.tgz", - "integrity": "sha512-9ywCsCvo1ojrw0b+XYk7aFvTH6D9064t0RIL1rtMf3nsa02Xw41MS7sZw216Im35xj/UY0PDBQsa1brUDDF1Ow==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/plugin-proposal-class-properties": "^7.0.0", - "@babel/plugin-proposal-object-rest-spread": "^7.0.0", - "@babel/plugin-syntax-class-properties": "^7.0.0", - "@babel/plugin-syntax-flow": "^7.0.0", - "@babel/plugin-syntax-jsx": "^7.0.0", - "@babel/plugin-syntax-object-rest-spread": "^7.0.0", - "@babel/plugin-transform-arrow-functions": "^7.0.0", - "@babel/plugin-transform-block-scoped-functions": "^7.0.0", - "@babel/plugin-transform-block-scoping": "^7.0.0", - "@babel/plugin-transform-classes": "^7.0.0", - "@babel/plugin-transform-computed-properties": "^7.0.0", - "@babel/plugin-transform-destructuring": "^7.0.0", - "@babel/plugin-transform-flow-strip-types": "^7.0.0", - "@babel/plugin-transform-for-of": "^7.0.0", - "@babel/plugin-transform-function-name": "^7.0.0", - "@babel/plugin-transform-literals": "^7.0.0", - "@babel/plugin-transform-member-expression-literals": "^7.0.0", - "@babel/plugin-transform-modules-commonjs": "^7.0.0", - "@babel/plugin-transform-object-super": "^7.0.0", - "@babel/plugin-transform-parameters": "^7.0.0", - "@babel/plugin-transform-property-literals": "^7.0.0", - "@babel/plugin-transform-react-display-name": "^7.0.0", - "@babel/plugin-transform-react-jsx": "^7.0.0", - "@babel/plugin-transform-shorthand-properties": "^7.0.0", - "@babel/plugin-transform-spread": "^7.0.0", - "@babel/plugin-transform-template-literals": "^7.0.0", - "babel-plugin-syntax-trailing-function-commas": "^7.0.0-beta.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -4077,16 +3517,6 @@ "ieee754": "^1.1.13" } }, - "node_modules/buffer-crc32": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", - "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -4141,9 +3571,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001695", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001695.tgz", - "integrity": "sha512-vHyLade6wTgI2u1ec3WQBxv+2BrTERV28UXQu9LO6lZ9pYeMk34vjXFLOxo1A4UBA8XTL4njRQZdno/yYaSmWw==", + "version": "1.0.30001699", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001699.tgz", + "integrity": "sha512-b+uH5BakXZ9Do9iK+CkDmctUSEqZl+SP056vc5usa0PL+ev5OHw003rZXcnjNDv3L8P5j6rwT6C0BPKSikW08w==", "dev": true, "funding": [ { @@ -4265,41 +3695,19 @@ } }, "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "dev": true, "license": "MIT", "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" + "readdirp": "^4.0.1" }, "engines": { - "node": ">= 8.10.0" + "node": ">= 14.16.0" }, "funding": { "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/chokidar/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" } }, "node_modules/clean-stack": { @@ -4408,17 +3816,13 @@ "node": ">=0.8" } }, - "node_modules/code-red": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/code-red/-/code-red-1.0.4.tgz", - "integrity": "sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==", + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15", - "@types/estree": "^1.0.1", - "acorn": "^8.10.0", - "estree-walker": "^3.0.3", - "periscopic": "^3.1.0" + "engines": { + "node": ">=6" } }, "node_modules/color-convert": { @@ -4448,6 +3852,19 @@ "dev": true, "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", @@ -4569,19 +3986,6 @@ "node": ">= 8" } }, - "node_modules/css-tree": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", - "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", - "license": "MIT", - "dependencies": { - "mdn-data": "2.0.30", - "source-map-js": "^1.0.1" - }, - "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" - } - }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -4595,6 +3999,20 @@ "node": ">=4" } }, + "node_modules/cssstyle": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.2.1.tgz", + "integrity": "sha512-9+vem03dMXG7gDmZ62uqmRiMRNtinIZ9ZyuF6BdxzfOD+FdN5hretzynkn0ReS2DO2GSw76RWHs0UmJPI2zUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^2.8.2", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/data-uri-to-buffer": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", @@ -4605,6 +4023,20 @@ "node": ">= 12" } }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/dataloader": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/dataloader/-/dataloader-2.2.3.tgz", @@ -4648,6 +4080,13 @@ } } }, + "node_modules/decimal.js": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.5.0.tgz", + "integrity": "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==", + "dev": true, + "license": "MIT" + }, "node_modules/deep-eql": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", @@ -4688,6 +4127,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/dependency-graph": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.11.0.tgz", @@ -4784,9 +4233,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.88", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.88.tgz", - "integrity": "sha512-K3C2qf1o+bGzbilTDCTBhTQcMS9KW60yTAaTeeXsfvQuTDDwlokLam/AdqlqcSy9u4UainDgsHV23ksXAOgamw==", + "version": "1.5.98", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.98.tgz", + "integrity": "sha512-bI/LbtRBxU2GzK7KK5xxFd2y9Lf9XguHooPYbcXWy6wUoT8NMnffsvRhPmSeUHLSDKAEtKuTaEtK4Ms15zkIEA==", "dev": true, "license": "ISC" }, @@ -4797,6 +4246,19 @@ "dev": true, "license": "MIT" }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -4814,17 +4276,10 @@ "dev": true, "license": "MIT" }, - "node_modules/es6-promise": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", - "integrity": "sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==", - "dev": true, - "license": "MIT" - }, "node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz", + "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -4832,32 +4287,34 @@ "esbuild": "bin/esbuild" }, "engines": { - "node": ">=12" + "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" + "@esbuild/aix-ppc64": "0.24.2", + "@esbuild/android-arm": "0.24.2", + "@esbuild/android-arm64": "0.24.2", + "@esbuild/android-x64": "0.24.2", + "@esbuild/darwin-arm64": "0.24.2", + "@esbuild/darwin-x64": "0.24.2", + "@esbuild/freebsd-arm64": "0.24.2", + "@esbuild/freebsd-x64": "0.24.2", + "@esbuild/linux-arm": "0.24.2", + "@esbuild/linux-arm64": "0.24.2", + "@esbuild/linux-ia32": "0.24.2", + "@esbuild/linux-loong64": "0.24.2", + "@esbuild/linux-mips64el": "0.24.2", + "@esbuild/linux-ppc64": "0.24.2", + "@esbuild/linux-riscv64": "0.24.2", + "@esbuild/linux-s390x": "0.24.2", + "@esbuild/linux-x64": "0.24.2", + "@esbuild/netbsd-arm64": "0.24.2", + "@esbuild/netbsd-x64": "0.24.2", + "@esbuild/openbsd-arm64": "0.24.2", + "@esbuild/openbsd-x64": "0.24.2", + "@esbuild/sunos-x64": "0.24.2", + "@esbuild/win32-arm64": "0.24.2", + "@esbuild/win32-ia32": "0.24.2", + "@esbuild/win32-x64": "0.24.2" } }, "node_modules/escalade": { @@ -4884,18 +4341,18 @@ } }, "node_modules/eslint": { - "version": "9.19.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.19.0.tgz", - "integrity": "sha512-ug92j0LepKlbbEv6hD911THhoRHmbdXt2gX+VDABAW/Ir7D3nqKdv5Pf5vtlyY6HQMTEP2skXY43ueqTCWssEA==", + "version": "9.20.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.20.1.tgz", + "integrity": "sha512-m1mM33o6dBUjxl2qb6wv6nGNwCAsns1eKtaQ4l/NPHeTvhiUPbtdfMyktxN4B3fgHIgsYh1VT3V9txblpQHq+g==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.19.0", - "@eslint/core": "^0.10.0", + "@eslint/core": "^0.11.0", "@eslint/eslintrc": "^3.2.0", - "@eslint/js": "9.19.0", + "@eslint/js": "9.20.0", "@eslint/plugin-kit": "^0.2.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -4960,9 +4417,9 @@ } }, "node_modules/eslint-compat-utils/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", "dev": true, "license": "ISC", "bin": { @@ -5021,9 +4478,9 @@ } }, "node_modules/eslint-plugin-svelte/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", "dev": true, "license": "ISC", "bin": { @@ -5080,7 +4537,6 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", - "dev": true, "license": "MIT" }, "node_modules/espree": { @@ -5127,6 +4583,15 @@ "node": ">=0.10" } }, + "node_modules/esrap": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-1.4.4.tgz", + "integrity": "sha512-tDN6xP/r/b3WmdpWm7LybrD252hY52IokcycPnO+WHfhFF0+n5AWtcLLK7VNV6m0uYgVRhGVs8OkZwRyfC7HzQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + } + }, "node_modules/esrecurse": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", @@ -5154,6 +4619,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, "license": "MIT", "dependencies": { "@types/estree": "^1.0.0" @@ -5207,13 +4673,6 @@ "url": "https://github.com/sponsors/jaydenseric" } }, - "node_modules/fast-decode-uri-component": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", - "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==", - "dev": true, - "license": "MIT" - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -5265,20 +4724,10 @@ "dev": true, "license": "MIT" }, - "node_modules/fast-querystring": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", - "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-decode-uri-component": "^1.0.1" - } - }, "node_modules/fastq": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.18.0.tgz", - "integrity": "sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==", + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.0.tgz", + "integrity": "sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA==", "dev": true, "license": "ISC", "dependencies": { @@ -5471,6 +4920,21 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/form-data": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/formdata-polyfill": { "version": "4.0.10", "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", @@ -5498,13 +4962,6 @@ "url": "https://github.com/sponsors/rawify" } }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, - "license": "ISC" - }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -5551,22 +5008,21 @@ } }, "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", "dev": true, "license": "ISC", "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" }, - "engines": { - "node": "*" + "bin": { + "glob": "dist/esm/bin.mjs" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -5585,10 +5041,36 @@ "node": ">=10.13.0" } }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/globals": { - "version": "15.14.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-15.14.0.tgz", - "integrity": "sha512-OkToC372DtlQeje9/zHIo5CT8lRP/FUgEOKBEhU4e0abL7J7CD24fD9ohiLN5hagG/kWCYj4K5oaxxtj2Z0Dig==", + "version": "15.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", "dev": true, "license": "MIT", "engines": { @@ -5619,13 +5101,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, - "license": "ISC" - }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", @@ -5741,16 +5216,30 @@ } }, "node_modules/graphql-ws": { - "version": "5.16.2", - "resolved": "https://registry.npmjs.org/graphql-ws/-/graphql-ws-5.16.2.tgz", - "integrity": "sha512-E1uccsZxt/96jH/OwmLPuXMACILs76pKF2i3W861LpKBCYtGIyPQGtWLuBLkND4ox1KHns70e83PS4te50nvPQ==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/graphql-ws/-/graphql-ws-6.0.3.tgz", + "integrity": "sha512-mvLRHihMg0llF74vo16063HufZHMGaiMxAjzyj0ARYueIikGzj1khlbPNl7vUc2h9rxbq9pGpQYbqypgq1fAXA==", "dev": true, "license": "MIT", "engines": { - "node": ">=10" + "node": ">=20" }, "peerDependencies": { - "graphql": ">=0.11 <=16" + "@fastify/websocket": "^10 || ^11", + "graphql": "^15.10.1 || ^16", + "uWebSockets.js": "^20", + "ws": "^8" + }, + "peerDependenciesMeta": { + "@fastify/websocket": { + "optional": true + }, + "uWebSockets.js": { + "optional": true + }, + "ws": { + "optional": true + } } }, "node_modules/has-flag": { @@ -5787,6 +5276,19 @@ "tslib": "^2.0.3" } }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/http-proxy-agent": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", @@ -5870,9 +5372,9 @@ } }, "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5940,18 +5442,6 @@ "node": ">=8" } }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -6109,6 +5599,13 @@ "node": ">=0.12.0" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, "node_modules/is-reference": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", @@ -6250,6 +5747,47 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdom": { + "version": "26.0.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.0.0.tgz", + "integrity": "sha512-BZYDGVAIriBWTpIxYzrXjv3E/4u8+/pSG5bQdIYCbNCGOvsPkDQfTVLAIXAf9ETdCpduCVTkDe2NNZ8NIwUVzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.4.3", + "form-data": "^4.0.1", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.0.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.0", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -6533,9 +6071,9 @@ } }, "node_modules/loupe": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.2.tgz", - "integrity": "sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.3.tgz", + "integrity": "sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==", "dev": true, "license": "MIT" }, @@ -6588,12 +6126,6 @@ "node": ">=0.10.0" } }, - "node_modules/mdn-data": { - "version": "2.0.30", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", - "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", - "license": "CC0-1.0" - }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -6636,24 +6168,37 @@ "node": ">=8.6" } }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "dev": true, "license": "MIT", "engines": { - "node": ">=6" + "node": ">= 0.6" } }, - "node_modules/min-indent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", - "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "dev": true, "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, "engines": { - "node": ">=4" + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" } }, "node_modules/minimatch": { @@ -6669,16 +6214,6 @@ "node": "*" } }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/minipass": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", @@ -6689,19 +6224,6 @@ "node": ">=16 || 14 >=14.17" } }, - "node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, "node_modules/mri": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", @@ -6826,6 +6348,31 @@ } } }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -6882,6 +6429,13 @@ "dev": true, "license": "MIT" }, + "node_modules/nwsapi": { + "version": "2.2.16", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.16.tgz", + "integrity": "sha512-F1I/bimDpj3ncaNDhfyMWuFqmQDBwDB0Fogc2qpL3BWvkQteFD/8BzWuIRl83rq0DXfm8SGt/HFhLXZyljTXcQ==", + "dev": true, + "license": "MIT" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -6902,16 +6456,6 @@ "node": ">= 6" } }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, "node_modules/onetime": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", @@ -7093,6 +6637,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse5": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz", + "integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^4.5.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/pascal-case": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", @@ -7125,16 +6682,6 @@ "node": ">=8" } }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -7210,9 +6757,9 @@ } }, "node_modules/pathe": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.2.tgz", - "integrity": "sha512-15Ztpk+nov8DR524R4BF7uEuzESgzUEAV4Ah7CUMNGXdE5ELuvxElxGXndBl32vMSsWa1jpNf22Z+Er3sKwq+w==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "dev": true, "license": "MIT" }, @@ -7226,17 +6773,6 @@ "node": ">= 14.16" } }, - "node_modules/periscopic": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/periscopic/-/periscopic-3.1.0.tgz", - "integrity": "sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==", - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "estree-walker": "^3.0.0", - "is-reference": "^3.0.0" - } - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -7278,9 +6814,9 @@ } }, "node_modules/postcss": { - "version": "8.5.1", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz", - "integrity": "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==", + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.2.tgz", + "integrity": "sha512-MjOadfU3Ys9KYoX0AdkBlFEF1Vx37uCCeN4ZHnmwm9FfpbsGWMZeBLMmmpY+6Ocqod7mkdZ0DT31OlbsFrLlkA==", "dev": true, "funding": [ { @@ -7486,9 +7022,9 @@ } }, "node_modules/prettier": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz", - "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==", + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.1.tgz", + "integrity": "sha512-hPpFQvHwL3Qv5AdRvBFMhnKo4tYxp0ReXiPn2bxkiohEX6mBeBwEpBSQTkD458RaaDKQMYSp4hX4UtfUTA5wDw==", "dev": true, "license": "MIT", "bin": { @@ -7658,16 +7194,17 @@ } }, "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.1.tgz", + "integrity": "sha512-h80JrZu/MHUZCyHu5ciuoI0+WxsCxzxJTILn6Fs8rxSnFPh+UVHYfeIxK1nVGugMqkfC4vJcBOYbkfkwYK0+gw==", "dev": true, "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, "engines": { - "node": ">=8.10.0" + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" } }, "node_modules/regenerator-runtime": { @@ -7786,24 +7323,10 @@ "dev": true, "license": "MIT" }, - "node_modules/rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, "node_modules/rollup": { - "version": "4.32.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.32.0.tgz", - "integrity": "sha512-JmrhfQR31Q4AuNBjjAX4s+a/Pu/Q8Q9iwjWBsjRH1q52SPFE2NqRMK6fUZKKnvKO6id+h7JIRf0oYsph53eATg==", + "version": "4.34.6", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.34.6.tgz", + "integrity": "sha512-wc2cBWqJgkU3Iz5oztRkQbfVkbxoz5EhnCGOrnJvnLnQ7O0WhQUYyv18qQI79O8L7DdHrrlJNeCHd4VGpnaXKQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7817,28 +7340,35 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.32.0", - "@rollup/rollup-android-arm64": "4.32.0", - "@rollup/rollup-darwin-arm64": "4.32.0", - "@rollup/rollup-darwin-x64": "4.32.0", - "@rollup/rollup-freebsd-arm64": "4.32.0", - "@rollup/rollup-freebsd-x64": "4.32.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.32.0", - "@rollup/rollup-linux-arm-musleabihf": "4.32.0", - "@rollup/rollup-linux-arm64-gnu": "4.32.0", - "@rollup/rollup-linux-arm64-musl": "4.32.0", - "@rollup/rollup-linux-loongarch64-gnu": "4.32.0", - "@rollup/rollup-linux-powerpc64le-gnu": "4.32.0", - "@rollup/rollup-linux-riscv64-gnu": "4.32.0", - "@rollup/rollup-linux-s390x-gnu": "4.32.0", - "@rollup/rollup-linux-x64-gnu": "4.32.0", - "@rollup/rollup-linux-x64-musl": "4.32.0", - "@rollup/rollup-win32-arm64-msvc": "4.32.0", - "@rollup/rollup-win32-ia32-msvc": "4.32.0", - "@rollup/rollup-win32-x64-msvc": "4.32.0", + "@rollup/rollup-android-arm-eabi": "4.34.6", + "@rollup/rollup-android-arm64": "4.34.6", + "@rollup/rollup-darwin-arm64": "4.34.6", + "@rollup/rollup-darwin-x64": "4.34.6", + "@rollup/rollup-freebsd-arm64": "4.34.6", + "@rollup/rollup-freebsd-x64": "4.34.6", + "@rollup/rollup-linux-arm-gnueabihf": "4.34.6", + "@rollup/rollup-linux-arm-musleabihf": "4.34.6", + "@rollup/rollup-linux-arm64-gnu": "4.34.6", + "@rollup/rollup-linux-arm64-musl": "4.34.6", + "@rollup/rollup-linux-loongarch64-gnu": "4.34.6", + "@rollup/rollup-linux-powerpc64le-gnu": "4.34.6", + "@rollup/rollup-linux-riscv64-gnu": "4.34.6", + "@rollup/rollup-linux-s390x-gnu": "4.34.6", + "@rollup/rollup-linux-x64-gnu": "4.34.6", + "@rollup/rollup-linux-x64-musl": "4.34.6", + "@rollup/rollup-win32-arm64-msvc": "4.34.6", + "@rollup/rollup-win32-ia32-msvc": "4.34.6", + "@rollup/rollup-win32-x64-msvc": "4.34.6", "fsevents": "~2.3.2" } }, + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, "node_modules/run-async": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", @@ -7924,17 +7454,17 @@ "dev": true, "license": "MIT" }, - "node_modules/sander": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/sander/-/sander-0.5.1.tgz", - "integrity": "sha512-3lVqBir7WuKDHGrKRDn/1Ye3kwpXaDOMsiRP1wd6wpZW56gJhsbp5RqQpA6JG/P+pkXizygnr1dKR8vzWaVsfA==", + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "es6-promise": "^3.1.2", - "graceful-fs": "^4.1.3", - "mkdirp": "^0.5.1", - "rimraf": "^2.5.2" + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" } }, "node_modules/scuid": { @@ -8088,26 +7618,11 @@ "tslib": "^2.0.3" } }, - "node_modules/sorcery": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/sorcery/-/sorcery-0.11.1.tgz", - "integrity": "sha512-o7npfeJE6wi6J9l0/5LKshFzZ2rMatRiCDwYeDQaOzqdzRJwALhX7mk/A/ecg6wjMu7wdZbmXfD2S/vpOg0bdQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.14", - "buffer-crc32": "^1.0.0", - "minimist": "^1.2.0", - "sander": "^0.5.0" - }, - "bin": { - "sorcery": "bin/sorcery" - } - }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -8221,19 +7736,6 @@ "node": ">=8" } }, - "node_modules/strip-indent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", - "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "min-indent": "^1.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -8270,53 +7772,6 @@ "node": ">=16 || 14 >=14.17" } }, - "node_modules/sucrase/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/sucrase/node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/sucrase/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -8344,61 +7799,91 @@ } }, "node_modules/svelecte": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/svelecte/-/svelecte-4.5.1.tgz", - "integrity": "sha512-s1oH043dTjJ2r6haM8+NuBlkUi/yL3lzw1tgil++9TAxQskPlcp6AvEMlASKgGADwYfiWmQT3dRSw8dPv2pg+A==", + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/svelecte/-/svelecte-5.1.4.tgz", + "integrity": "sha512-eKtwtTwv8l72yj/foEZu3dsg4Rm77/TNCNSr0e1GK8sOGd+kspesKWoRz1dHfztriGCQaCPMJ3ClIyzsi0n8vg==", "license": "MIT", - "dependencies": { - "svelte-tiny-virtual-list": "^2.1.0" - }, "peerDependencies": { - "svelte": "^3.43.0 || ^4.0.0" + "svelte": "^5.2.7" } }, "node_modules/svelte": { - "version": "4.2.19", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.19.tgz", - "integrity": "sha512-IY1rnGr6izd10B0A8LqsBfmlT5OILVuZ7XsI0vdGPEvuonFV7NYEUK4dAkm9Zg2q0Um92kYjTpS1CAP3Nh/KWw==", - "license": "MIT", - "dependencies": { - "@ampproject/remapping": "^2.2.1", - "@jridgewell/sourcemap-codec": "^1.4.15", - "@jridgewell/trace-mapping": "^0.3.18", - "@types/estree": "^1.0.1", - "acorn": "^8.9.0", - "aria-query": "^5.3.0", - "axobject-query": "^4.0.0", - "code-red": "^1.0.3", - "css-tree": "^2.3.1", - "estree-walker": "^3.0.3", - "is-reference": "^3.0.1", + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.20.0.tgz", + "integrity": "sha512-04HJfFLaTwTyEKdPm3vYGdaD/8ZAHcd9SEBufq0FZNIrdzJWdM1usVdm4KIlzzDfM5+aMzio6BBhpXPoPGuMjg==", + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@jridgewell/sourcemap-codec": "^1.5.0", + "@types/estree": "^1.0.5", + "acorn": "^8.12.1", + "acorn-typescript": "^1.4.13", + "aria-query": "^5.3.1", + "axobject-query": "^4.1.0", + "clsx": "^2.1.1", + "esm-env": "^1.2.1", + "esrap": "^1.4.3", + "is-reference": "^3.0.3", "locate-character": "^3.0.0", - "magic-string": "^0.30.4", - "periscopic": "^3.1.0" + "magic-string": "^0.30.11", + "zimmerframe": "^1.1.2" }, "engines": { - "node": ">=16" + "node": ">=18" } }, "node_modules/svelte-check": { - "version": "3.8.6", - "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-3.8.6.tgz", - "integrity": "sha512-ij0u4Lw/sOTREP13BdWZjiXD/BlHE6/e2e34XzmVmsp5IN4kVa3PWP65NM32JAgwjZlwBg/+JtiNV1MM8khu0Q==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.1.4.tgz", + "integrity": "sha512-v0j7yLbT29MezzaQJPEDwksybTE2Ups9rUxEXy92T06TiA0cbqcO8wAOwNUVkFW6B0hsYHA+oAX3BS8b/2oHtw==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/trace-mapping": "^0.3.17", - "chokidar": "^3.4.1", + "@jridgewell/trace-mapping": "^0.3.25", + "chokidar": "^4.0.1", + "fdir": "^6.2.0", "picocolors": "^1.0.0", - "sade": "^1.7.4", - "svelte-preprocess": "^5.1.3", - "typescript": "^5.0.3" + "sade": "^1.7.4" }, "bin": { "svelte-check": "bin/svelte-check" }, + "engines": { + "node": ">= 18.0.0" + }, "peerDependencies": { - "svelte": "^3.55.0 || ^4.0.0-next.0 || ^4.0.0 || ^5.0.0-next.0" + "svelte": "^4.0.0 || ^5.0.0-next.0", + "typescript": ">=5.0.0" + } + }, + "node_modules/svelte-check/node_modules/fdir": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.3.tgz", + "integrity": "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/svelte-check/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/svelte-eslint-parser": { @@ -8464,97 +7949,15 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/svelte-hmr": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/svelte-hmr/-/svelte-hmr-0.16.0.tgz", - "integrity": "sha512-Gyc7cOS3VJzLlfj7wKS0ZnzDVdv3Pn2IuVeJPk9m2skfhcu5bq3wtIZyQGggr7/Iim5rH5cncyQft/kRLupcnA==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^12.20 || ^14.13.1 || >= 16" - }, - "peerDependencies": { - "svelte": "^3.19.0 || ^4.0.0" - } - }, "node_modules/svelte-modals": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/svelte-modals/-/svelte-modals-1.3.0.tgz", - "integrity": "sha512-b1Ylnyv9O6b7VYeWGJVToaVU2lw7GtErVwiEdojyfnOuZcrhNlQ5eDqbTrL3xyKz8j2VTy/QiGUl1lm/6SnQ2A==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/svelte-modals/-/svelte-modals-2.0.0.tgz", + "integrity": "sha512-wBClXmScNStF/rG75ZP7hEiKXRJ5H+DhA2dkMPQd+FX2Hc1XTj1n5cZGC7uTYCYOVmQwrsDOlL5a2V5E6q3f+A==", "license": "MIT", "peerDependencies": { - "svelte": "^3.0.0 || ^4.0.0" - } - }, - "node_modules/svelte-preprocess": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-5.1.4.tgz", - "integrity": "sha512-IvnbQ6D6Ao3Gg6ftiM5tdbR6aAETwjhHV+UKGf5bHGYR69RQvF1ho0JKPcbUON4vy4R7zom13jPjgdOWCQ5hDA==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "@types/pug": "^2.0.6", - "detect-indent": "^6.1.0", - "magic-string": "^0.30.5", - "sorcery": "^0.11.0", - "strip-indent": "^3.0.0" - }, - "engines": { - "node": ">= 16.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.10.2", - "coffeescript": "^2.5.1", - "less": "^3.11.3 || ^4.0.0", - "postcss": "^7 || ^8", - "postcss-load-config": "^2.1.0 || ^3.0.0 || ^4.0.0 || ^5.0.0", - "pug": "^3.0.0", - "sass": "^1.26.8", - "stylus": "^0.55.0", - "sugarss": "^2.0.0 || ^3.0.0 || ^4.0.0", - "svelte": "^3.23.0 || ^4.0.0-next.0 || ^4.0.0 || ^5.0.0-next.0", - "typescript": ">=3.9.5 || ^4.0.0 || ^5.0.0" - }, - "peerDependenciesMeta": { - "@babel/core": { - "optional": true - }, - "coffeescript": { - "optional": true - }, - "less": { - "optional": true - }, - "postcss": { - "optional": true - }, - "postcss-load-config": { - "optional": true - }, - "pug": { - "optional": true - }, - "sass": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "typescript": { - "optional": true - } + "svelte": ">=5" } }, - "node_modules/svelte-tiny-virtual-list": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/svelte-tiny-virtual-list/-/svelte-tiny-virtual-list-2.1.2.tgz", - "integrity": "sha512-jeP/WMvgFUR4mYXHGPiCexjX5DuzSO+3xzHNhxfcsFyy+uYPtnqI5UGb383swpzQAyXB0OBqYfzpYihD/5gxnA==", - "license": "MIT" - }, "node_modules/swap-case": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/swap-case/-/swap-case-2.0.2.tgz", @@ -8565,6 +7968,13 @@ "tslib": "^2.0.3" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, "node_modules/sync-fetch": { "version": "0.6.0-2", "resolved": "https://registry.npmjs.org/sync-fetch/-/sync-fetch-0.6.0-2.tgz", @@ -8637,6 +8047,44 @@ "node": ">=14.0.0" } }, + "node_modules/tailwindcss/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/tailwindcss/node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/tailwindcss/node_modules/lilconfig": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", @@ -8686,6 +8134,19 @@ } } }, + "node_modules/tailwindcss/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -8780,6 +8241,26 @@ "tslib": "^2.0.3" } }, + "node_modules/tldts": { + "version": "6.1.77", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.77.tgz", + "integrity": "sha512-lBpoWgy+kYmuXWQ83+R7LlJCnsd9YW8DGpZSHhrMl4b8Ly/1vzOie3OdtmUJDkKxcgRGOehDu5btKkty+JEe+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.77" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.77", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.77.tgz", + "integrity": "sha512-bCaqm24FPk8OgBkM0u/SrEWJgHnhBWYqeBo6yUmcZJDCHt/IfyWBb+14CXdGi4RInMv4v7eUAin15W0DoA+Ytg==", + "dev": true, + "license": "MIT" + }, "node_modules/tmp": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", @@ -8816,17 +8297,36 @@ "node": ">=6" } }, + "node_modules/tough-cookie": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.1.tgz", + "integrity": "sha512-Ek7HndSVkp10hmHP9V4qZO1u+pn1RU5sI0Fw+jCU3lyvuMZcgqsNgc6CmJJZyByK4Vm/qotGRJlfgAX8q+4JiA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", + "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } }, "node_modules/ts-api-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.0.tgz", - "integrity": "sha512-xCt/TOAc+EOHS1XPnijD3/yzpH6qg2xppZO1YDqGoVsNXfQfzHpOdNuXwrwOU8u4ITXJyDCTyt8w5g1sZv9ynQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.1.tgz", + "integrity": "sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==", "dev": true, "license": "MIT", "engines": { @@ -9053,21 +8553,21 @@ } }, "node_modules/vite": { - "version": "5.4.14", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.14.tgz", - "integrity": "sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.1.0.tgz", + "integrity": "sha512-RjjMipCKVoR4hVfPY6GQTgveinjNuyLw+qruksLDvA5ktI1150VmcMBKmQaEWJhg/j6Uaf6dNCNA0AfdzUb/hQ==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" + "esbuild": "^0.24.2", + "postcss": "^8.5.1", + "rollup": "^4.30.1" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" @@ -9076,19 +8576,25 @@ "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", - "terser": "^5.4.0" + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" }, "peerDependenciesMeta": { "@types/node": { "optional": true }, + "jiti": { + "optional": true + }, "less": { "optional": true }, @@ -9109,13 +8615,19 @@ }, "terser": { "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true } } }, "node_modules/vite-node": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.0.4.tgz", - "integrity": "sha512-7JZKEzcYV2Nx3u6rlvN8qdo3QV7Fxyt6hx+CCKz9fbWxdX5IvUOmTWEAxMrWxaiSf7CKGLJQ5rFu8prb/jBjOA==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.0.5.tgz", + "integrity": "sha512-02JEJl7SbtwSDJdYS537nU6l+ktdvcREfLksk/NDAqtdKWGqHl+joXzEubHROmS3E6pip+Xgu2tFezMu75jH7A==", "dev": true, "license": "MIT", "dependencies": { @@ -9136,13 +8648,17 @@ } }, "node_modules/vitefu": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-0.2.5.tgz", - "integrity": "sha512-SgHtMLoqaeeGnd2evZ849ZbACbnwQCIwRH57t18FxcXoZop0uQu0uzlIhJBlF/eWVzuce0sHeqPcDo+evVcg8Q==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.0.5.tgz", + "integrity": "sha512-h4Vflt9gxODPFNGPwp4zAMZRpZR7eslzwH2c5hn5kNZ5rhnKyRJ50U+yGCdc2IRaBs8O4haIgLNGrV5CrpMsCA==", "dev": true, "license": "MIT", + "workspaces": [ + "tests/deps/*", + "tests/projects/*" + ], "peerDependencies": { - "vite": "^3.0.0 || ^4.0.0 || ^5.0.0" + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" }, "peerDependenciesMeta": { "vite": { @@ -9151,19 +8667,19 @@ } }, "node_modules/vitest": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.0.4.tgz", - "integrity": "sha512-6XG8oTKy2gnJIFTHP6LD7ExFeNLxiTkK3CfMvT7IfR8IN+BYICCf0lXUQmX7i7JoxUP8QmeP4mTnWXgflu4yjw==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.0.5.tgz", + "integrity": "sha512-4dof+HvqONw9bvsYxtkfUp2uHsTN9bV2CZIi1pWgoFpL1Lld8LA1ka9q/ONSsoScAKG7NVGf2stJTI7XRkXb2Q==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "3.0.4", - "@vitest/mocker": "3.0.4", - "@vitest/pretty-format": "^3.0.4", - "@vitest/runner": "3.0.4", - "@vitest/snapshot": "3.0.4", - "@vitest/spy": "3.0.4", - "@vitest/utils": "3.0.4", + "@vitest/expect": "3.0.5", + "@vitest/mocker": "3.0.5", + "@vitest/pretty-format": "^3.0.5", + "@vitest/runner": "3.0.5", + "@vitest/snapshot": "3.0.5", + "@vitest/spy": "3.0.5", + "@vitest/utils": "3.0.5", "chai": "^5.1.2", "debug": "^4.4.0", "expect-type": "^1.1.0", @@ -9175,7 +8691,7 @@ "tinypool": "^1.0.2", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0", - "vite-node": "3.0.4", + "vite-node": "3.0.5", "why-is-node-running": "^2.3.0" }, "bin": { @@ -9191,8 +8707,8 @@ "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.0.4", - "@vitest/ui": "3.0.4", + "@vitest/browser": "3.0.5", + "@vitest/ui": "3.0.5", "happy-dom": "*", "jsdom": "*" }, @@ -9220,6 +8736,19 @@ } } }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/wcwidth": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", @@ -9241,11 +8770,40 @@ } }, "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", "dev": true, - "license": "BSD-2-Clause" + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } }, "node_modules/whatwg-mimetype": { "version": "4.0.0", @@ -9258,14 +8816,17 @@ } }, "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.1.1.tgz", + "integrity": "sha512-mDGf9diDad/giZ/Sm9Xi2YcyzaFpbdLpJPr+E9fSkyQ7KpQD4SdFcugkRQYzhmfI4KeV4Qpnn2sKPdo+kmsgRQ==", "dev": true, "license": "MIT", "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" + "tr46": "^5.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" } }, "node_modules/which": { @@ -9351,13 +8912,6 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, - "license": "ISC" - }, "node_modules/ws": { "version": "8.18.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", @@ -9380,6 +8934,23 @@ } } }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -9458,6 +9029,12 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zimmerframe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.2.tgz", + "integrity": "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==", + "license": "MIT" } } } diff --git a/frontend/package.json b/frontend/package.json index 7364093..605f884 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -24,7 +24,7 @@ "@iconify/tailwind": "^1.1.3", "@sveltejs/adapter-static": "^3.0.6", "@sveltejs/kit": "^2.8.1", - "@sveltejs/vite-plugin-svelte": "^3.0.2", + "@sveltejs/vite-plugin-svelte": "^5.0.0", "@typescript-eslint/eslint-plugin": "^8.14.0", "@typescript-eslint/parser": "^8.14.0", "@zerodevx/svelte-toast": "^0.9.6", @@ -35,17 +35,18 @@ "eslint-plugin-svelte": "^2.46.0", "fast-deep-equal": "^3.1.3", "globals": "^15.14.0", + "jsdom": "^26.0.0", "npm-check-updates": "^17.1.11", "postcss": "^8.4.49", "prettier": "^3.3.3", "prettier-plugin-svelte": "^3.2.8", "prettier-plugin-tailwindcss": "^0.6.8", - "svelte": "^4.2.12", - "svelte-check": "^3.6.6", + "svelte": "^5.5.0", + "svelte-check": "^4.0.0", "tailwindcss": "^3.4.14", "tslib": "^2.8.1", "typescript": "^5.6.3", - "vite": "^5.4.11", + "vite": "^6.0.0", "vitest": "^3.0.0" }, "type": "module", @@ -54,7 +55,7 @@ "@urql/svelte": "^4.2.2", "filesize": "^10.1.6", "graphql": "npm:graphql-web-lite@^16.6.0", - "svelecte": "^4.4.1", - "svelte-modals": "^1.3.0" + "svelecte": "^5.0.0", + "svelte-modals": "^2.0.0" } } diff --git a/frontend/src/app.css b/frontend/src/app.css index 07939f6..f8322d0 100644 --- a/frontend/src/app.css +++ b/frontend/src/app.css @@ -193,6 +193,7 @@ --sv-separator-bg: theme(colors.gray.700); --sv-min-height: 38px; --sv-item-wrap-padding: 3px 3px 3px 5px; + --sv-selection-multi-wrap-padding: 3px 3px 3px 5px; } .svelecte.is-focused { diff --git a/frontend/src/gql/Utils.ts b/frontend/src/gql/Utils.ts index dd21bbe..177dff0 100644 --- a/frontend/src/gql/Utils.ts +++ b/frontend/src/gql/Utils.ts @@ -1,8 +1,23 @@ -import equal from 'fast-deep-equal'; +import { omit } from '$lib/Utils'; +import type { Client } from '@urql/svelte'; import * as gql from './graphql'; -export type OmitIdentifiers<T> = Omit<T, 'id' | '__typename'>; +type Typename = '__typename'; +type Identifiers = Typename | 'id'; + +export type OmitTypename<T> = Omit<T, Typename>; +export type OmitIdentifiers<T> = Omit<T, Identifiers>; export type RequiredName<T> = T & { name: string }; +export type MutationWith<T> = ( + client: Client, + args: { ids: number[] | number; input: T } +) => Promise<unknown>; + +export function omitIdentifiers<T extends { __typename?: unknown; id: number }>( + obj: T +): OmitIdentifiers<T> { + return omit(obj, '__typename', 'id'); +} export function isSuccess(object: any): object is gql.Success { if (object.__typename === undefined) { @@ -18,57 +33,3 @@ export function isError(object: any): object is gql.Error { } return object.__typename.endsWith('Error') && (object as gql.Error).message !== undefined; } - -type Item = { - id: number | string; - name: string; -}; - -export function itemEquals(a: Item, b: Item) { - return a.name == b.name; -} - -function assocEquals(as: Item[], bs: Item[]) { - return equal( - as.map((a) => a.id), - bs.map((b) => b.id) - ); -} - -function stringEquals(a: string | null | undefined, b: string | null | undefined) { - return (a ? a : null) == (b ? b : null); -} - -export function tagEquals(a: gql.FullTag, b: gql.FullTag) { - return ( - itemEquals(a, b) && - stringEquals(a.description, b.description) && - assocEquals(a.namespaces, b.namespaces) - ); -} - -export function comicEquals( - a: gql.FullComicFragment | undefined, - b: gql.FullComicFragment | undefined -) { - if (a === undefined) return b === undefined; - if (b === undefined) return a === undefined; - - return ( - stringEquals(a.title, b.title) && - stringEquals(a.originalTitle, b.originalTitle) && - stringEquals(a.url, b.url) && - stringEquals(a.date, b.date) && - a.category == b.category && - a.rating == b.rating && - a.censorship == b.censorship && - a.language == b.language && - a.direction == b.direction && - a.layout == b.layout && - assocEquals(a.artists, b.artists) && - assocEquals(a.circles, b.circles) && - assocEquals(a.characters, b.characters) && - assocEquals(a.tags, b.tags) && - assocEquals(a.worlds, b.worlds) - ); -} diff --git a/frontend/src/lib/Actions.ts b/frontend/src/lib/Actions.ts index 7231c2f..2c15b61 100644 --- a/frontend/src/lib/Actions.ts +++ b/frontend/src/lib/Actions.ts @@ -23,28 +23,6 @@ export function debounce( }; } -export function clickOutside( - node: HTMLElement, - { handler, ignore }: { handler: () => void; ignore?: HTMLElement } -) { - const handle = (event: Event) => { - const target = event.target as HTMLElement; - if (!target || target === ignore) return; - - if (node && !node.contains(target) && !event.defaultPrevented) { - handler(); - } - }; - - document.addEventListener('click', handle, true); - - return { - destroy() { - document.removeEventListener('click', handle, true); - } - }; -} - export const focusableElements = [ 'a[href]', 'area[href]', diff --git a/frontend/src/lib/Enums.ts b/frontend/src/lib/Enums.ts index 876aec8..3264de4 100644 --- a/frontend/src/lib/Enums.ts +++ b/frontend/src/lib/Enums.ts @@ -15,6 +15,7 @@ import { UpdateMode, WorldSort } from '$gql/graphql'; +import type { Key } from './Utils'; export interface EnumOption<T> { id: T; @@ -318,8 +319,6 @@ export const censorships: EnumOption<Censorship>[] = optionsFromLabel(Censorship export const categories: EnumOption<Category>[] = optionsFromLabel(CategoryLabel); export const languages: EnumOption<Language>[] = optionsFromLabel(LanguageLabel); -function optionsFromLabel<T extends string | number | symbol>( - labels: Record<T, string> -): EnumOption<T>[] { +function optionsFromLabel<T extends Key>(labels: Record<T, string>): EnumOption<T>[] { return Object.entries(labels).map(([k, v]) => ({ id: k as T, name: v as string })); } diff --git a/frontend/src/lib/Filter.ts b/frontend/src/lib/Filter.svelte.ts index 1340eaf..8c0fa82 100644 --- a/frontend/src/lib/Filter.ts +++ b/frontend/src/lib/Filter.svelte.ts @@ -7,10 +7,8 @@ import { type TagFilter, type TagFilterInput } from '$gql/graphql'; -import { getContext, setContext } from 'svelte'; -import { writable, type Writable } from 'svelte/store'; import { navigate } from './Navigation'; -import { numKeys } from './Utils'; +import { numKeys, type Key } from './Utils'; interface FilterInput<T> { include?: T | null; @@ -21,9 +19,9 @@ interface BasicFilter { name?: { contains?: string | null } | null; } -type FilterMode = 'any' | 'all' | 'exact'; +export type FilterType = 'include' | 'exclude'; -type Key = string | number | symbol; +type FilterMode = 'any' | 'all' | 'exact'; type Filter<T, K extends Key> = Partial<Record<K, T | null>>; @@ -50,10 +48,10 @@ interface Integrateable<F> { } class ComplexMember<K extends Key> { - values: unknown[] = []; + values: unknown[] = $state([]); key: K; - mode: FilterMode; - empty?: boolean | null; + mode: FilterMode = $state('all'); + empty?: boolean | null = $state(null); constructor(key: K, mode: FilterMode) { this.key = key; @@ -72,7 +70,7 @@ class ComplexMember<K extends Key> { } export class Association<K extends Key> extends ComplexMember<K> { - values: (string | number)[] = []; + values: (string | number)[] = $state([]); constructor(key: K, mode: FilterMode, filter?: AssocFilter<string | number, K> | null) { super(key, mode); @@ -98,7 +96,7 @@ export class Association<K extends Key> extends ComplexMember<K> { } export class Enum<K extends Key> extends ComplexMember<K> { - values: string[] = []; + values: string[] = $state([]); constructor(key: K, filter?: EnumFilter<K> | null) { super(key, 'any'); @@ -118,7 +116,7 @@ export class Enum<K extends Key> extends ComplexMember<K> { class Bool<K extends Key> { key: K; - value?: boolean = undefined; + value?: boolean = $state(undefined); constructor(key: K, filter?: Filter<boolean, K> | null) { this.key = key; @@ -137,7 +135,7 @@ class Bool<K extends Key> { class Str<K extends Key> { key: K; - contains = ''; + contains = $state(''); constructor(key: K, filter?: Filter<StringFilter, K> | null) { this.key = key; @@ -245,105 +243,78 @@ function buildFilterInput<F>(include?: F, exclude?: F) { } abstract class FilterContext<F> { - include!: { controls: Controls<F>; size: number }; - exclude!: { controls: Controls<F>; size: number }; + include!: Controls<F>; + exclude!: Controls<F>; + includes = 0; + excludes = 0; - apply(params: URLSearchParams) { + apply = (params: URLSearchParams) => { navigate( { - filter: buildFilterInput( - this.include.controls.buildFilter(), - this.exclude.controls.buildFilter() - ) + filter: buildFilterInput(this.include.buildFilter(), this.exclude.buildFilter()) }, params ); - } + }; } export class ArchiveFilterContext extends FilterContext<ArchiveFilter> { - include: { controls: ArchiveFilterControls; size: number }; - exclude: { controls: ArchiveFilterControls; size: number }; + include: ArchiveFilterControls; + exclude: ArchiveFilterControls; private static ignore = ['organized']; constructor(filter: ArchiveFilterInput) { super(); - this.include = { - controls: new ArchiveFilterControls(filter.include), - size: numKeys(filter.include, ArchiveFilterContext.ignore) - }; - this.exclude = { - controls: new ArchiveFilterControls(filter.exclude), - size: numKeys(filter.exclude, ArchiveFilterContext.ignore) - }; + this.include = new ArchiveFilterControls(filter.include); + this.exclude = new ArchiveFilterControls(filter.exclude); + this.includes = numKeys(filter.include, ArchiveFilterContext.ignore); + this.excludes = numKeys(filter.exclude, ArchiveFilterContext.ignore); } } export class ComicFilterContext extends FilterContext<ComicFilter> { - include: { controls: ComicFilterControls; size: number }; - exclude: { controls: ComicFilterControls; size: number }; + include: ComicFilterControls; + exclude: ComicFilterControls; private static ignore = ['title', 'favourite', 'organized', 'bookmarked']; constructor(filter: ComicFilterInput) { super(); - this.include = { - controls: new ComicFilterControls(filter.include, 'all'), - size: numKeys(filter.include, ComicFilterContext.ignore) - }; - this.exclude = { - controls: new ComicFilterControls(filter.exclude, 'any'), - size: numKeys(filter.exclude, ComicFilterContext.ignore) - }; + this.include = new ComicFilterControls(filter.include, 'all'); + this.exclude = new ComicFilterControls(filter.exclude, 'any'); + this.includes = numKeys(filter.include, ComicFilterContext.ignore); + this.excludes = numKeys(filter.exclude, ComicFilterContext.ignore); } } export class BasicFilterContext extends FilterContext<BasicFilter> { - include: { controls: BasicFilterControls; size: number }; - exclude: { controls: BasicFilterControls; size: number }; + include: BasicFilterControls; + exclude: BasicFilterControls; constructor(filter: FilterInput<BasicFilter>) { super(); - this.include = { - controls: new BasicFilterControls(filter.include), - size: numKeys(filter.include) - }; - this.exclude = { - controls: new BasicFilterControls(), - size: 0 - }; + this.include = new BasicFilterControls(filter.include); + this.exclude = new BasicFilterControls(); } } export class TagFilterContext extends FilterContext<TagFilter> { - include: { controls: TagFilterControls; size: number }; - exclude: { controls: TagFilterControls; size: number }; + include: TagFilterControls; + exclude: TagFilterControls; private static ignore = ['name']; constructor(filter: TagFilterInput) { super(); - this.include = { - controls: new TagFilterControls(filter.include, 'all'), - size: numKeys(filter.include, TagFilterContext.ignore) - }; - this.exclude = { - controls: new TagFilterControls(filter.exclude, 'any'), - size: numKeys(filter.exclude, TagFilterContext.ignore) - }; + this.include = new TagFilterControls(filter.include, 'all'); + this.exclude = new TagFilterControls(filter.exclude, 'any'); + this.includes = numKeys(filter.include, TagFilterContext.ignore); + this.excludes = numKeys(filter.exclude, TagFilterContext.ignore); } } -export function initFilterContext<F extends FilterContext<unknown>>() { - return setContext<Writable<F>>('filter', writable()); -} - -export function getFilterContext<F extends FilterContext<unknown>>() { - return getContext<Writable<F>>('filter'); -} - export function cycleBooleanFilter(value: boolean | undefined, tristate = true) { if (tristate) { if (value === undefined) { diff --git a/frontend/src/lib/Form.ts b/frontend/src/lib/Form.ts new file mode 100644 index 0000000..ab0f4f7 --- /dev/null +++ b/frontend/src/lib/Form.ts @@ -0,0 +1,76 @@ +import type { FullComicFragment, FullTag, Namespace } from '$gql/graphql'; +import type { OmitIdentifiers } from '$gql/Utils'; +import equal from 'fast-deep-equal'; +import type { Snippet } from 'svelte'; + +export interface FormProps<I, P> { + initial: OmitIdentifiers<I>; + submit: (input: P) => void; + children?: Snippet; +} + +interface Item { + id: number | string; + name: string; +} + +function stringPending(a?: string | null, b?: string | null) { + if (a?.length === 0) { + a = null; + } + + if (b?.length === 0) { + b = null; + } + + return a !== b; +} + +function associationPending(as: Item[], bs: Item[]) { + return !equal( + as.map((a) => a.id), + bs.map((b) => b.id) + ); +} + +export function itemPending(initial: OmitIdentifiers<Item>, current: OmitIdentifiers<Item>) { + return stringPending(initial.name, current.name); +} + +export function namespacePending( + initial: OmitIdentifiers<Namespace>, + current: OmitIdentifiers<Namespace> +) { + return itemPending(initial, current) || stringPending(initial.sortName, current.sortName); +} + +export function tagPending(a: OmitIdentifiers<FullTag>, b: OmitIdentifiers<FullTag>) { + return ( + itemPending(a, b) || + stringPending(a.description, b.description) || + associationPending(a.namespaces, b.namespaces) + ); +} + +export function comicPending(a?: FullComicFragment, b?: OmitIdentifiers<FullComicFragment>) { + if (a === undefined) return b === undefined; + if (b === undefined) return a === undefined; + + return ( + stringPending(a.title, b.title) || + stringPending(a.originalTitle, b.originalTitle) || + stringPending(a.url, b.url) || + stringPending(a.date, b.date) || + a.category !== b.category || + a.rating !== b.rating || + a.censorship !== b.censorship || + a.language !== b.language || + a.direction !== b.direction || + a.layout !== b.layout || + associationPending(a.artists, b.artists) || + associationPending(a.circles, b.circles) || + associationPending(a.characters, b.characters) || + associationPending(a.tags, b.tags) || + associationPending(a.worlds, b.worlds) + ); +} diff --git a/frontend/src/lib/Navigation.ts b/frontend/src/lib/Navigation.ts index 5ed3ec5..f3bc413 100644 --- a/frontend/src/lib/Navigation.ts +++ b/frontend/src/lib/Navigation.ts @@ -1,36 +1,44 @@ import { goto as svelteGoto } from '$app/navigation'; import { SortDirection } from '$gql/graphql'; import JsonURL from '@jsonurl/jsonurl'; -import { type PaginationData } from './Pagination'; -import { type SortData } from './Sort'; import { toastError } from './Toasts'; +import type { Key } from './Utils'; -function paramToNum<T>(value: string | null, fallback: T) { - if (value) { - const number = +value; +export interface PaginationData { + page: number; + items: number; +} - if (Number.isNaN(number) || number < 0) { - return fallback; - } +export interface SortData<T extends Key> { + on: T; + direction: SortDirection; + seed: number | undefined; +} + +function number<T>(value: string | null, fallback: T) { + if (!value) return fallback; + + const number = +value; - return number; + if (Number.isNaN(number) || number < 0) { + return fallback; } - return fallback; + return number; } -export function parseSortData<T>(params: URLSearchParams, fallback: T): SortData<T> { +export function parseSortData<T extends Key>(params: URLSearchParams, fallback: T): SortData<T> { return { on: (params.get('s') as T) || fallback, direction: (params.get('d') as SortDirection) || SortDirection.Ascending, - seed: paramToNum(params.get('r'), undefined) + seed: number(params.get('r'), undefined) }; } export function parsePaginationData(params: URLSearchParams, defaultItems = 120): PaginationData { return { - page: paramToNum(params.get('p'), 1), - items: paramToNum(params.get('i'), defaultItems) + page: number(params.get('p'), 1), + items: number(params.get('i'), defaultItems) }; } @@ -62,7 +70,7 @@ interface NavigationParameters<T> { pagination?: Partial<PaginationData>; } -function paramsFrom<T>( +function parametersFrom<T>( { pagination, filter, sort }: NavigationParameters<T>, current?: URLSearchParams ) { @@ -102,13 +110,13 @@ function paramsFrom<T>( return params; } -export function navigate(parameters: NavigationParameters<object>, current?: URLSearchParams) { +export function navigate(params: NavigationParameters<object>, current?: URLSearchParams) { goto({ - params: paramsFrom(parameters, current), + params: parametersFrom(params, current), options: { noScroll: false, keepFocus: true, replaceState: true } }); } export function href<T>(base: string, params: NavigationParameters<T>) { - return `/${base}/?${paramsFrom(params).toString()}`; + return `/${base}/?${parametersFrom(params).toString()}`; } diff --git a/frontend/src/lib/Pagination.ts b/frontend/src/lib/Pagination.ts deleted file mode 100644 index f05492b..0000000 --- a/frontend/src/lib/Pagination.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { navigate } from '$lib/Navigation'; -import { getContext, setContext } from 'svelte'; -import { writable, type Writable } from 'svelte/store'; - -export interface PaginationData { - page: number; - items: number; -} - -export class PaginationContext { - page = 0; - items = 0; - total = 0; - - set update({ page, items }: PaginationData) { - this.page = page; - this.items = items; - } - - apply(params: URLSearchParams) { - navigate({ pagination: { items: this.items } }, params); - } -} - -export function initPaginationContext() { - return setContext<Writable<PaginationContext>>('pagination', writable(new PaginationContext())); -} - -export function getPaginationContext() { - return getContext<Writable<PaginationContext>>('pagination'); -} diff --git a/frontend/src/lib/Reader.ts b/frontend/src/lib/Reader.ts deleted file mode 100644 index 8777b9b..0000000 --- a/frontend/src/lib/Reader.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { Layout, type PageFragment } from '$gql/graphql'; -import { getContext, setContext } from 'svelte'; -import { writable, type Writable } from 'svelte/store'; - -export interface Chunk { - main: PageFragment; - secondary?: PageFragment; - index: number; -} - -class ReaderContext { - visible = false; - sidebar = false; - pages: PageFragment[] = []; - page = 0; - - open(page: number) { - this.page = page; - this.visible = true; - - return this; - } -} - -export function initReaderContext() { - return setContext<Writable<ReaderContext>>('reader', writable(new ReaderContext())); -} - -export function getReaderContext() { - return getContext<Writable<ReaderContext>>('reader'); -} - -export function partition(pages: PageFragment[], layout: Layout): [Chunk[], number[]] { - const single = layout === Layout.Single; - const offset = layout === Layout.DoubleOffset; - - const chunks: Chunk[] = []; - const lookup: number[] = Array<number>(pages.length); - - for (let chunkIndex = 0, pageIndex = 0; pageIndex < pages.length; chunkIndex++) { - const wide = () => pages[pageIndex].image.aspectRatio > 1; - - const nextPage = () => { - lookup[pageIndex] = chunkIndex; - return pages[pageIndex++]; - }; - - const offsetFirst = pageIndex === 0 && offset; - const full = single || wide() || offsetFirst; - - const chunk: Chunk = { index: pageIndex, main: nextPage() }; - - if (!full && pageIndex < pages.length) { - if (!wide()) { - chunk.secondary = nextPage(); - } - } - - chunks.push(chunk); - } - return [chunks, lookup]; -} diff --git a/frontend/src/lib/Selection.ts b/frontend/src/lib/Selection.ts deleted file mode 100644 index 0ea85cc..0000000 --- a/frontend/src/lib/Selection.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { getContext, hasContext, setContext } from 'svelte'; -import { writable, type Writable } from 'svelte/store'; -import { range } from './Utils'; - -interface Item { - id: number; -} - -export const hasSelectionContext = () => hasContext('selection'); - -export function getSelectionContext<T extends Item>() { - return getContext<Writable<ItemSelection<T>>>('selection'); -} - -export function initSelectionContext<T extends Item>( - typename?: string, - toName?: (item: T) => string -) { - return setContext<Writable<ItemSelection<T>>>( - 'selection', - writable(new ItemSelection(typename, toName)) - ); -} - -export class ItemSelection<T extends Item> { - active = false; - typename: string; - #toName: (item: T) => string; - - #view: T[] = []; - selectable: (item: T) => boolean = () => true; - - #ids = new Set<number>(); - #masked = new Set<number>(); - - constructor(typename?: string, toName?: (item: T) => string) { - this.typename = typename ?? 'unknown'; - this.#toName = toName ?? (() => 'unknown'); - } - - set view(view: T[]) { - this.#view = view; - this.#updateMasked(); - } - - #indexOf = (id: number) => this.#view.findIndex((v) => v.id === id); - - update(index: number, shift: boolean) { - const id = this.#view[index].id; - - const selectableRange = (first: number, last: number) => - range(first, last) - .filter((i) => this.selectable(this.#view[i])) - .map((i) => this.#view[i].id); - - if (shift) { - const indices = this.indices; - - const first = indices.at(0); - const last = indices.at(-1); - - if (first === undefined || last === undefined) { - this.#ids.add(id); - } else if (index === first || index === last) { - this.#ids.clear(); - } else if (index > last) { - this.#ids = new Set([...this.#ids, ...selectableRange(last, index)]); - } else if (index < last) { - this.#ids = new Set([...this.#ids, ...selectableRange(index, last)]); - } - } else { - if (this.#ids.has(id)) { - this.#ids.delete(id); - } else { - this.#ids.add(id); - } - } - - this.#updateMasked(); - - return this; - } - - toggle() { - this.active = !this.active; - - if (!this.active) { - return this.none(); - } - - return this; - } - - all() { - this.#ids = new Set(this.#view.filter(this.selectable).map((i) => i.id)); - this.#updateMasked(); - - return this; - } - - none() { - this.#ids.clear(); - this.#masked.clear(); - - return this; - } - - clear() { - this.active = false; - - return this.none(); - } - - contains(id: number) { - return this.#masked.has(id); - } - - #updateMasked() { - this.#masked = new Set([...this.#ids].filter((i) => this.#indexOf(i) >= 0)); - } - - get ids() { - return [...this.#masked]; - } - - get size() { - return this.#masked.size; - } - - get indices() { - return [...this.#ids].map(this.#indexOf).filter((i) => i >= 0); - } - - get items() { - return this.indices.map((i) => this.#view[i]); - } - - get names() { - return this.items.map(this.#toName); - } -} diff --git a/frontend/src/lib/Shortcuts.ts b/frontend/src/lib/Shortcuts.ts index 300ddcb..1ff7679 100644 --- a/frontend/src/lib/Shortcuts.ts +++ b/frontend/src/lib/Shortcuts.ts @@ -1,5 +1,4 @@ -import { closeModal, modals } from 'svelte-modals'; -import { get } from 'svelte/store'; +import { modals } from 'svelte-modals'; type LowercaseLetter = | 'a' @@ -68,8 +67,8 @@ export function handleShortcuts(event: KeyboardEvent) { } if (event.key === 'Escape') { - if (get(modals).length > 0) { - closeModal(); + if (modals.stack.length > 0) { + modals.close(); event.preventDefault(); event.stopImmediatePropagation(); return; @@ -84,7 +83,7 @@ export function handleShortcuts(event: KeyboardEvent) { const handler = handlers.get(mode === undefined ? event.key : `${mode}${event.key}`); - if (!handler || get(modals).length > 0) { + if (!handler || modals.stack.length > 0) { mode = undefined; return; } diff --git a/frontend/src/lib/Sort.ts b/frontend/src/lib/Sort.ts deleted file mode 100644 index 4c9a353..0000000 --- a/frontend/src/lib/Sort.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { SortDirection } from '$gql/graphql'; -import { getContext, setContext } from 'svelte'; -import { writable, type Writable } from 'svelte/store'; -import { navigate } from './Navigation'; - -export interface SortData<T> { - on: T; - direction: SortDirection; - seed: number | undefined; -} - -export class SortContext<T extends string> { - on: T; - direction: SortDirection; - seed: number | undefined; - labels: Record<T, string>; - - constructor({ on, direction, seed }: SortData<T>, labels: Record<T, string>) { - this.on = on; - this.direction = direction; - this.seed = seed; - this.labels = labels; - } - - set update({ on, direction, seed }: SortData<T>) { - this.on = on; - this.direction = direction; - this.seed = seed; - } - - apply(params: URLSearchParams) { - navigate({ sort: { on: this.on, direction: this.direction, seed: this.seed } }, params); - } -} - -export function initSortContext<T extends string>(sort: SortData<T>, labels: Record<T, string>) { - return setContext<Writable<SortContext<T>>>('sort', writable(new SortContext(sort, labels))); -} - -export function getSortContext<T extends string>() { - return getContext<Writable<SortContext<T>>>('sort'); -} diff --git a/frontend/src/lib/Tabs.ts b/frontend/src/lib/Tabs.ts deleted file mode 100644 index 1c43068..0000000 --- a/frontend/src/lib/Tabs.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { getContext, setContext } from 'svelte'; -import { writable, type Writable } from 'svelte/store'; - -type Tab = string; -type Tabs = Record<Tab, { title: string; badge?: boolean }>; - -interface TabContext { - tabs: Tabs; - current: Tab; -} - -export function setTabContext(context: TabContext) { - return setContext<Writable<TabContext>>('tabs', writable(context)); -} - -export function getTabContext() { - return getContext<Writable<TabContext>>('tabs'); -} diff --git a/frontend/src/lib/Update.ts b/frontend/src/lib/Update.svelte.ts index 13aec61..1d684d5 100644 --- a/frontend/src/lib/Update.ts +++ b/frontend/src/lib/Update.svelte.ts @@ -4,8 +4,7 @@ import { type UpdateOptions, type UpdateTagInput } from '$gql/graphql'; - -type Key = string | number | symbol; +import type { Key } from './Utils'; interface AssociationUpdate { ids?: number[] | string[] | null; @@ -26,10 +25,10 @@ abstract class Entry<K extends Key> { } class Association<K extends Key> extends Entry<K> { - ids = []; - options = { + ids = $state([]); + options = $state({ mode: UpdateMode.Add - }; + }); constructor(key: K) { super(key); @@ -47,7 +46,7 @@ class Association<K extends Key> extends Entry<K> { } class Enum<K extends Key> extends Entry<K> { - value?: string = undefined; + value?: string = $state(undefined); constructor(key: K) { super(key); @@ -65,13 +64,13 @@ class Enum<K extends Key> extends Entry<K> { } abstract class Controls<I> { - toInput() { + input() { const input = {} as I; Object.values(this).forEach((v: Entry<keyof I>) => v.integrate(input)); return input; } - hasInput() { + pending() { return Object.values(this).some((i: Entry<keyof I>) => i.hasInput()); } } diff --git a/frontend/src/lib/Utils.ts b/frontend/src/lib/Utils.ts index 1a07be1..c0e5b6c 100644 --- a/frontend/src/lib/Utils.ts +++ b/frontend/src/lib/Utils.ts @@ -2,7 +2,8 @@ import { isError } from '$gql/Utils'; import type { ImageFragment } from '$gql/graphql'; import type { BeforeNavigate } from '@sveltejs/kit'; import type { OperationResultState } from '@urql/svelte'; -import { openModal } from 'svelte-modals'; +import { modals } from 'svelte-modals'; +import { toastFinally } from './Toasts'; import ConfirmDeletion from './dialogs/ConfirmDeletion.svelte'; export function range(from: number, to: number) { @@ -16,6 +17,8 @@ export function getRandomInt(min: number, max: number) { return Math.floor(Math.random() * (maxFloored - minCeiled) + minCeiled); } +export type Key = string | number | symbol; + export interface ListItem { id: number | string; name: string; @@ -68,11 +71,14 @@ export function confirmDeletion( callback: () => void, warning?: string ) { - openModal( - ConfirmDeletion, - { names: Array.isArray(names) ? names : [names], typename, callback: callback, warning }, - { replace: true } - ); + modals + .open(ConfirmDeletion, { + names: Array.isArray(names) ? names : [names], + typename, + callback, + warning + }) + .catch(toastFinally); } export function idFromLabel(label: string) { @@ -106,3 +112,12 @@ export function preventOnPending({ to, cancel }: BeforeNavigate, pending: boolea cancel(); } +export function omit<T, K extends keyof T>(obj: T, ...props: K[]): Omit<T, K> { + return props.reduce( + (o, k) => { + delete o[k]; + return o; + }, + { ...obj } + ); +} diff --git a/frontend/src/lib/components/AddButton.svelte b/frontend/src/lib/components/AddButton.svelte index 9c0ab29..f07eafd 100644 --- a/frontend/src/lib/components/AddButton.svelte +++ b/frontend/src/lib/components/AddButton.svelte @@ -1,7 +1,7 @@ <script lang="ts"> - export let title: string; + let { title, onclick }: { title: string; onclick: () => void } = $props(); </script> -<button class="btn-blue" {title} on:click> - <span class="icon-base icon-[material-symbols--add]" /> +<button class="btn-blue" {title} aria-label={title} {onclick}> + <span class="icon-base icon-[material-symbols--add]"></span> </button> diff --git a/frontend/src/lib/components/Badge.svelte b/frontend/src/lib/components/Badge.svelte index 7ad3173..6f8198a 100644 --- a/frontend/src/lib/components/Badge.svelte +++ b/frontend/src/lib/components/Badge.svelte @@ -2,7 +2,7 @@ import { fadeDefault } from '$lib/Transitions'; import { fade } from 'svelte/transition'; - export let number: number; + let { number }: { number: number } = $props(); </script> {#if number > 0} diff --git a/frontend/src/lib/components/BookmarkButton.svelte b/frontend/src/lib/components/BookmarkButton.svelte index 89570e6..bdcbd75 100644 --- a/frontend/src/lib/components/BookmarkButton.svelte +++ b/frontend/src/lib/components/BookmarkButton.svelte @@ -1,9 +1,15 @@ <script lang="ts"> import Bookmark from '$lib/icons/Bookmark.svelte'; + import type { MouseEventHandler } from 'svelte/elements'; - export let bookmarked: boolean; + interface Props { + bookmarked: boolean; + onclick: MouseEventHandler<HTMLButtonElement>; + } + + let { bookmarked, onclick }: Props = $props(); </script> -<button type="button" title="Toggle bookmark" class="flex text-base" on:click> +<button type="button" title="Toggle bookmark" class="flex text-base" {onclick}> <Bookmark hoverable {bookmarked} /> </button> diff --git a/frontend/src/lib/components/Card.svelte b/frontend/src/lib/components/Card.svelte index d209517..21181dc 100644 --- a/frontend/src/lib/components/Card.svelte +++ b/frontend/src/lib/components/Card.svelte @@ -1,4 +1,4 @@ -<script lang="ts" context="module"> +<script lang="ts" module> import type { ComicFragment, ImageFragment } from '$gql/graphql'; interface CardDetails { @@ -24,12 +24,29 @@ <script lang="ts"> import { src } from '$lib/Utils'; import Star from '$lib/icons/Star.svelte'; + import type { Snippet } from 'svelte'; - export let href: string; - export let details: CardDetails; - export let compact = false; - export let coverOnly = false; - export let ellipsis = true; + interface Props { + href: string; + details: CardDetails; + compact?: boolean; + coverOnly?: boolean; + ellipsis?: boolean; + overlay?: Snippet; + children?: Snippet; + onclick?: (event: MouseEvent) => void; + } + + let { + href, + details, + compact = false, + coverOnly = false, + ellipsis = true, + overlay, + children, + onclick + }: Props = $props(); </script> <a @@ -37,9 +54,9 @@ class="grid-card-v sm:grid-card-h focus-thick focus-blue relative grid overflow-hidden rounded bg-slate-900 shadow-md shadow-slate-950/30" class:compact class:grid-card-cover-only={coverOnly} - on:click + {onclick} > - <slot name="overlay" /> + {@render overlay?.()} {#if details.cover} <img class="h-full w-full object-cover object-[center_top]" @@ -76,7 +93,7 @@ </header> <section class="max-h-full grow overflow-auto border-t border-slate-800/80 pt-2 text-xs"> - <slot /> + {@render children?.()} </section> </article> {/if} diff --git a/frontend/src/lib/components/Cardlet.svelte b/frontend/src/lib/components/Cardlet.svelte index 04d8599..d249cc8 100644 --- a/frontend/src/lib/components/Cardlet.svelte +++ b/frontend/src/lib/components/Cardlet.svelte @@ -1,14 +1,27 @@ <script lang="ts"> import type { ComicFilter } from '$gql/graphql'; import { href } from '$lib/Navigation'; + import type { Snippet } from 'svelte'; - export let name: string; - export let title: string | null | undefined = undefined; + interface Props { + name: string; + title?: string | null; + filter?: keyof ComicFilter; + id?: number | string; + overlay?: Snippet; + onclick: (event: MouseEvent) => void; + } - export let filter: keyof ComicFilter | undefined = undefined; - export let id: number | string | undefined = undefined; + let { + name, + title = undefined, + filter = undefined, + id = undefined, + overlay, + onclick + }: Props = $props(); - const handleAux = (e: MouseEvent) => { + const onauxclick = (e: MouseEvent) => { if (filter === undefined || id === undefined || e.button !== 1) return; window.open(href('comics', { filter: { include: { [filter]: { all: [id] } } } })); }; @@ -18,10 +31,10 @@ type="button" class="relative flex overflow-hidden rounded bg-slate-900 text-left shadow-md shadow-slate-950/20" {title} - on:click - on:auxclick={handleAux} + {onclick} + {onauxclick} > - <slot name="overlay" /> + {@render overlay?.()} <article class="group h-full grow items-center gap-2 p-2 text-xs"> <h2 class="ellipsis-nowrap text-sm font-medium">{name}</h2> </article> diff --git a/frontend/src/lib/components/DeleteButton.svelte b/frontend/src/lib/components/DeleteButton.svelte index 8f5f116..bc94c8c 100644 --- a/frontend/src/lib/components/DeleteButton.svelte +++ b/frontend/src/lib/components/DeleteButton.svelte @@ -1,15 +1,22 @@ -<script> +<script lang="ts"> import { accelerator } from '$lib/Shortcuts'; + import type { MouseEventHandler } from 'svelte/elements'; - export let prominent = false; + interface Props { + prominent?: boolean; + onclick: MouseEventHandler<HTMLButtonElement>; + } + + let { prominent = false, onclick }: Props = $props(); </script> <button type="button" class={prominent ? 'btn-rose' : 'btn-slate hover:bg-rose-700'} title="Delete forever" - on:click + aria-label="Delete forever" + {onclick} use:accelerator={'Delete'} > - <span class="icon-base icon-[material-symbols--delete-forever]" /> + <span class="icon-base icon-[material-symbols--delete-forever]"></span> </button> diff --git a/frontend/src/lib/components/Dialog.svelte b/frontend/src/lib/components/Dialog.svelte index a0bbe5e..d300369 100644 --- a/frontend/src/lib/components/Dialog.svelte +++ b/frontend/src/lib/components/Dialog.svelte @@ -1,10 +1,16 @@ <script lang="ts"> import { trapFocus } from '$lib/Actions'; import { fadeDefault } from '$lib/Transitions'; - import { closeModal } from 'svelte-modals'; + import type { Snippet } from 'svelte'; + import type { ModalProps } from 'svelte-modals'; import { fade } from 'svelte/transition'; - export let isOpen: boolean; + interface Props extends ModalProps { + title: string; + children?: Snippet; + } + + let { isOpen, close, title, children }: Props = $props(); </script> {#if isOpen} @@ -18,18 +24,19 @@ class="pointer-events-auto flex flex-col rounded-md bg-slate-800 shadow-md shadow-slate-900" > <header class="flex items-center gap-1 border-b-2 border-slate-700/50 p-2"> - <slot name="header" /> + <h2>{title}</h2> <button type="button" class="ml-auto flex items-center text-white/30 hover:text-white" title="Cancel" - on:click={closeModal} + aria-label="Cancel" + onclick={close} > - <span class="icon-base icon-[material-symbols--close]" /> + <span class="icon-base icon-[material-symbols--close]"></span> </button> </header> <main class="m-3 w-80 sm:w-[34rem]"> - <slot /> + {@render children?.()} </main> </div> </div> diff --git a/frontend/src/lib/components/Dropdown.svelte b/frontend/src/lib/components/Dropdown.svelte index 9e935e4..ddd20a0 100644 --- a/frontend/src/lib/components/Dropdown.svelte +++ b/frontend/src/lib/components/Dropdown.svelte @@ -1,18 +1,37 @@ <script lang="ts"> - import { clickOutside } from '$lib/Actions'; import { fadeFast } from '$lib/Transitions'; + import type { Snippet } from 'svelte'; import { fade } from 'svelte/transition'; - export let visible: boolean; - export let parent: HTMLElement; + interface Props { + button: Snippet<[() => void]>; + children?: Snippet; + } + + let { button, children }: Props = $props(); + + let visible = $state(false); + + function onfocusout(event: FocusEvent & { currentTarget: EventTarget & HTMLDivElement }) { + if ( + event.relatedTarget instanceof HTMLElement && + event.currentTarget.contains(event.relatedTarget) + ) { + return; + } + + visible = false; + } </script> -{#if visible} - <div - class="absolute z-[1] mt-1 w-max rounded bg-slate-700 p-1 shadow-sm shadow-slate-900" - transition:fade={fadeFast} - use:clickOutside={{ handler: () => (visible = false), ignore: parent }} - > - <slot /> - </div> -{/if} +<div class="relative" {onfocusout}> + {@render button(() => (visible = !visible))} + {#if visible} + <div + class="absolute z-[1] mt-1 w-max rounded bg-slate-700 p-1 shadow-sm shadow-slate-900" + transition:fade={fadeFast} + > + {@render children?.()} + </div> + {/if} +</div> diff --git a/frontend/src/lib/components/Expander.svelte b/frontend/src/lib/components/Expander.svelte index a382658..8f23042 100644 --- a/frontend/src/lib/components/Expander.svelte +++ b/frontend/src/lib/components/Expander.svelte @@ -1,17 +1,21 @@ <script lang="ts"> - export let expanded: boolean; - export let title: string; + interface Props { + expanded: boolean; + title: string; + } + + let { expanded = $bindable(), title }: Props = $props(); + + function onclick() { + expanded = !expanded; + } </script> -<button - class="flex items-center text-base hover:text-white" - type="button" - on:click={() => (expanded = !expanded)} -> +<button class="flex items-center text-base hover:text-white" type="button" {onclick}> {#if expanded} - <span class="icon-base icon-[material-symbols--expand-less]" /> + <span class="icon-base icon-[material-symbols--expand-less]"></span> {:else} - <span class="icon-base icon-[material-symbols--expand-more]" /> + <span class="icon-base icon-[material-symbols--expand-more]"></span> {/if} {title} </button> diff --git a/frontend/src/lib/components/Guard.svelte b/frontend/src/lib/components/Guard.svelte index fd7ded4..38cbd65 100644 --- a/frontend/src/lib/components/Guard.svelte +++ b/frontend/src/lib/components/Guard.svelte @@ -1,9 +1,10 @@ <script lang="ts"> import { getResultState } from '$lib/Utils'; + import type { OperationResultStore } from '@urql/svelte'; import Spinner from './Spinner.svelte'; - export let result; - $: state = getResultState($result); + let { result }: { result: OperationResultStore } = $props(); + let state = $derived(getResultState($result)); </script> {#if state.fetching} diff --git a/frontend/src/lib/components/Head.svelte b/frontend/src/lib/components/Head.svelte index b4aed5b..5ddd543 100644 --- a/frontend/src/lib/components/Head.svelte +++ b/frontend/src/lib/components/Head.svelte @@ -1,6 +1,5 @@ <script lang="ts"> - export let section: string; - export let title = ''; + let { section, title = '' }: { section: string; title?: string } = $props(); function formatTitle(section: string, title?: string) { return [title, section, 'hircine'].filter((i) => i).join(' · '); diff --git a/frontend/src/lib/components/Labelled.svelte b/frontend/src/lib/components/Labelled.svelte deleted file mode 100644 index 4b36ad6..0000000 --- a/frontend/src/lib/components/Labelled.svelte +++ /dev/null @@ -1,10 +0,0 @@ -<script lang="ts"> - import { idFromLabel } from '$lib/Utils'; - - export let label: string; - - const id = idFromLabel(label); -</script> - -<label class="self-center" for={id}>{label}</label> -<slot {id} /> diff --git a/frontend/src/lib/components/LabelledBlock.svelte b/frontend/src/lib/components/LabelledBlock.svelte index feb563e..8f93667 100644 --- a/frontend/src/lib/components/LabelledBlock.svelte +++ b/frontend/src/lib/components/LabelledBlock.svelte @@ -1,7 +1,14 @@ <script lang="ts"> import { idFromLabel } from '$lib/Utils'; + import type { Snippet } from 'svelte'; - export let label: string; + interface Props { + label: string; + side?: Snippet; + children?: Snippet<[{ id: string }]>; + } + + let { label, side, children }: Props = $props(); const id = idFromLabel(label); </script> @@ -9,10 +16,10 @@ <div class="flex flex-col"> <div class="flex"> <label for={id}>{label}</label> - {#if $$slots.controls} - <div class="grow" /> - <slot name="controls" /> + {#if side} + <div class="grow"></div> + {@render side?.()} {/if} </div> - <slot {id} /> + {@render children?.({ id })} </div> diff --git a/frontend/src/lib/components/OrganizedButton.svelte b/frontend/src/lib/components/OrganizedButton.svelte index 9be985c..3838f7d 100644 --- a/frontend/src/lib/components/OrganizedButton.svelte +++ b/frontend/src/lib/components/OrganizedButton.svelte @@ -1,9 +1,15 @@ <script lang="ts"> import Organized from '$lib/icons/Organized.svelte'; + import type { MouseEventHandler } from 'svelte/elements'; - export let organized: boolean; + interface Props { + organized: boolean; + onclick: MouseEventHandler<HTMLButtonElement>; + } + + let { organized, onclick }: Props = $props(); </script> -<button type="button" title="Toggle organized" class="flex text-base" on:click> +<button type="button" title="Toggle organized" class="flex text-base" {onclick}> <Organized hoverable {organized} /> </button> diff --git a/frontend/src/lib/components/RefreshButton.svelte b/frontend/src/lib/components/RefreshButton.svelte index afab640..70ee2d1 100644 --- a/frontend/src/lib/components/RefreshButton.svelte +++ b/frontend/src/lib/components/RefreshButton.svelte @@ -1,3 +1,9 @@ -<button class="btn-blue" title="Refresh" on:click> - <span class="icon-base icon-[material-symbols--sync]" /> +<script lang="ts"> + import type { MouseEventHandler } from 'svelte/elements'; + + let { onclick }: { onclick: MouseEventHandler<HTMLButtonElement> } = $props(); +</script> + +<button class="btn-blue" title="Refresh" aria-label="Refresh" {onclick}> + <span class="icon-base icon-[material-symbols--sync]"></span> </button> diff --git a/frontend/src/lib/components/RemovePageButton.svelte b/frontend/src/lib/components/RemovePageButton.svelte index e23c079..8045f32 100644 --- a/frontend/src/lib/components/RemovePageButton.svelte +++ b/frontend/src/lib/components/RemovePageButton.svelte @@ -1,13 +1,17 @@ <script lang="ts"> import { accelerator } from '$lib/Shortcuts'; + import type { MouseEventHandler } from 'svelte/elements'; + + let { onclick }: { onclick: MouseEventHandler<HTMLButtonElement> } = $props(); </script> <button type="button" class="btn-rose" title="Remove selected pages" - on:click + aria-label="Remove selected pages" + {onclick} use:accelerator={'Delete'} > - <span class="icon-base icon-[material-symbols--scan-delete]" /> + <span class="icon-base icon-[material-symbols--scan-delete]"></span> </button> diff --git a/frontend/src/lib/components/Select.svelte b/frontend/src/lib/components/Select.svelte index dece4a5..44828d3 100644 --- a/frontend/src/lib/components/Select.svelte +++ b/frontend/src/lib/components/Select.svelte @@ -2,19 +2,28 @@ import type { ListItem } from '$lib/Utils'; import Svelecte from 'svelecte'; - let inputId: string; - let valueAsObject = false; - let multiple = false; - type Item = number | string | ListItem; type Value = Item | Item[] | undefined | null; - export let clearable = false; - export let placeholder = 'Select...'; - export let options: ListItem[] | undefined; - export let value: Value; + interface Props { + id: string; + object?: boolean; + multi?: boolean; + clearable?: boolean; + placeholder?: string; + options: ListItem[] | undefined; + value: Value; + } - export { inputId as id, valueAsObject as object, multiple as multi }; + let { + id: inputId, + object: valueAsObject = false, + multi: multiple = false, + clearable = false, + placeholder = 'Select...', + options, + value = $bindable() + }: Props = $props(); </script> {#if options !== null && options !== undefined} diff --git a/frontend/src/lib/components/Spinner.svelte b/frontend/src/lib/components/Spinner.svelte index 946329c..1a471a7 100644 --- a/frontend/src/lib/components/Spinner.svelte +++ b/frontend/src/lib/components/Spinner.svelte @@ -1,7 +1,7 @@ <script lang="ts"> import { onDestroy } from 'svelte'; - let show = false; + let show = $state(false); const timeout = setTimeout(() => (show = true), 150); onDestroy(() => clearTimeout(timeout)); @@ -9,7 +9,7 @@ {#if show} <div class="flex h-full w-full items-center justify-center"> - <span class="spinner" /> + <span class="spinner"></span> </div> {/if} diff --git a/frontend/src/lib/components/SubmitButton.svelte b/frontend/src/lib/components/SubmitButton.svelte index 8ac90b9..3b89ba7 100644 --- a/frontend/src/lib/components/SubmitButton.svelte +++ b/frontend/src/lib/components/SubmitButton.svelte @@ -1,7 +1,7 @@ <script lang="ts"> - export let active = false; + let { pending = false }: { pending?: boolean } = $props(); - $: title = active ? 'Save pending changes' : 'Save (no changes pending)'; + let title = $derived(pending ? 'Save pending changes' : 'Save (no changes pending)'); </script> -<button type="submit" class:active class="btn-slate [&.active]:btn-blue" {title}>Save</button> +<button type="submit" class:pending class="btn-slate [&.pending]:btn-blue" {title}>Save</button> diff --git a/frontend/src/lib/components/Titlebar.svelte b/frontend/src/lib/components/Titlebar.svelte index 2cdfa70..fe28cfe 100644 --- a/frontend/src/lib/components/Titlebar.svelte +++ b/frontend/src/lib/components/Titlebar.svelte @@ -1,12 +1,15 @@ <script lang="ts"> import Star from '$lib/icons/Star.svelte'; - import { createEventDispatcher } from 'svelte'; + import type { MouseEventHandler } from 'svelte/elements'; - export let title: string; - export let subtitle: string | null = ''; - export let favourite: boolean | undefined = undefined; + interface Props { + title: string; + subtitle?: string | null; + favourite?: boolean; + onfavourite?: MouseEventHandler<HTMLButtonElement>; + } - const dispatch = createEventDispatcher<{ favourite: null }>(); + let { title, subtitle, favourite, onfavourite }: Props = $props(); </script> <div class="flex flex-wrap gap-x-4"> @@ -16,7 +19,7 @@ type="button" class="focus-background mr-1 flex items-center" title="Toggle favourite" - on:click={() => dispatch('favourite')} + onclick={onfavourite} > <Star large hoverable {favourite} /> </button> diff --git a/frontend/src/lib/containers/Cardlets.svelte b/frontend/src/lib/containers/Cardlets.svelte index 129da61..5997a69 100644 --- a/frontend/src/lib/containers/Cardlets.svelte +++ b/frontend/src/lib/containers/Cardlets.svelte @@ -1,11 +1,13 @@ <script> import { fadeDefault } from '$lib/Transitions'; import { fade } from 'svelte/transition'; + + let { children } = $props(); </script> <div class="grid gap-4 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6 min-[1600px]:grid-cols-8 min-[1920px]:grid-cols-10" in:fade={fadeDefault} > - <slot /> + {@render children?.()} </div> diff --git a/frontend/src/lib/containers/Cards.svelte b/frontend/src/lib/containers/Cards.svelte index a19e8be..36a4b86 100644 --- a/frontend/src/lib/containers/Cards.svelte +++ b/frontend/src/lib/containers/Cards.svelte @@ -1,8 +1,10 @@ <script> import { fadeDefault } from '$lib/Transitions'; import { fade } from 'svelte/transition'; + + let { children } = $props(); </script> <div class="grid gap-4 xl:grid-cols-2 min-[1920px]:grid-cols-3" in:fade|global={fadeDefault}> - <slot /> + {@render children?.()} </div> diff --git a/frontend/src/lib/containers/Carousel.svelte b/frontend/src/lib/containers/Carousel.svelte index 1268a78..fb05b7d 100644 --- a/frontend/src/lib/containers/Carousel.svelte +++ b/frontend/src/lib/containers/Carousel.svelte @@ -1,6 +1,13 @@ <script lang="ts"> - export let title: string; - export let href: string; + import type { Snippet } from 'svelte'; + + interface Props { + title: string; + href: string; + children?: Snippet; + } + + let { title, href, children }: Props = $props(); </script> <div class="flex flex-col gap-1"> @@ -10,6 +17,6 @@ </a> </h2> <div class="flex flex-wrap gap-5"> - <slot /> + {@render children?.()} </div> </div> diff --git a/frontend/src/lib/containers/Column.svelte b/frontend/src/lib/containers/Column.svelte index 05daece..fe5ac47 100644 --- a/frontend/src/lib/containers/Column.svelte +++ b/frontend/src/lib/containers/Column.svelte @@ -1,3 +1,7 @@ +<script> + let { children } = $props(); +</script> + <div class="flex flex-col gap-4"> - <slot /> + {@render children?.()} </div> diff --git a/frontend/src/lib/containers/Grid.svelte b/frontend/src/lib/containers/Grid.svelte index 1224156..af5125a 100644 --- a/frontend/src/lib/containers/Grid.svelte +++ b/frontend/src/lib/containers/Grid.svelte @@ -1,14 +1,16 @@ -<script> +<script lang="ts"> import { fadeDefault } from '$lib/Transitions'; - + import type { Snippet } from 'svelte'; import { fade } from 'svelte/transition'; + + let { children }: { children?: Snippet } = $props(); </script> <div class="flex flex-col gap-1 lg:grid lg:h-full lg:max-h-full lg:overflow-auto" in:fade|global={fadeDefault} > - <slot /> + {@render children?.()} </div> <style> diff --git a/frontend/src/lib/dialogs/AddArtist.svelte b/frontend/src/lib/dialogs/AddArtist.svelte index 6ec93c5..9fc2ca1 100644 --- a/frontend/src/lib/dialogs/AddArtist.svelte +++ b/frontend/src/lib/dialogs/AddArtist.svelte @@ -1,30 +1,22 @@ <script lang="ts"> - import { addArtist, type ArtistInput } from '$gql/Mutations'; + import type { AddArtistInput } from '$gql/graphql'; + import { addArtist } from '$gql/Mutations'; import Dialog from '$lib/components/Dialog.svelte'; - import SubmitButton from '$lib/components/SubmitButton.svelte'; import ArtistForm from '$lib/forms/ArtistForm.svelte'; import { toastFinally } from '$lib/Toasts'; import { getContextClient } from '@urql/svelte'; - import { closeModal } from 'svelte-modals'; + import type { ModalProps } from 'svelte-modals'; const client = getContextClient(); - export let isOpen: boolean; + let modal: ModalProps = $props(); + const initial = { name: '' }; - let artist = { name: '' }; - - function add(event: CustomEvent<ArtistInput>) { - addArtist(client, { input: event.detail }).then(closeModal).catch(toastFinally); + function submit(input: AddArtistInput) { + addArtist(client, { input }).then(modal.close).catch(toastFinally); } </script> -<Dialog {isOpen}> - <svelte:fragment slot="header"> - <h2>Add Artist</h2> - </svelte:fragment> - <ArtistForm bind:artist on:submit={add}> - <div class="flex justify-end gap-4"> - <SubmitButton active={artist.name.length > 0} /> - </div> - </ArtistForm> +<Dialog title="Add Artist" {...modal}> + <ArtistForm {initial} {submit} /> </Dialog> diff --git a/frontend/src/lib/dialogs/AddCharacter.svelte b/frontend/src/lib/dialogs/AddCharacter.svelte index 23fea08..1585e34 100644 --- a/frontend/src/lib/dialogs/AddCharacter.svelte +++ b/frontend/src/lib/dialogs/AddCharacter.svelte @@ -1,30 +1,22 @@ <script lang="ts"> - import { addCharacter, type CharacterInput } from '$gql/Mutations'; + import type { AddCharacterInput } from '$gql/graphql'; + import { addCharacter } from '$gql/Mutations'; import Dialog from '$lib/components/Dialog.svelte'; - import SubmitButton from '$lib/components/SubmitButton.svelte'; import CharacterForm from '$lib/forms/CharacterForm.svelte'; import { toastFinally } from '$lib/Toasts'; import { getContextClient } from '@urql/svelte'; - import { closeModal } from 'svelte-modals'; + import type { ModalProps } from 'svelte-modals'; const client = getContextClient(); - export let isOpen: boolean; + let modal: ModalProps = $props(); + const initial = { name: '' }; - let character = { name: '' }; - - function add(event: CustomEvent<CharacterInput>) { - addCharacter(client, { input: event.detail }).then(closeModal).catch(toastFinally); + function submit(input: AddCharacterInput) { + addCharacter(client, { input }).then(modal.close).catch(toastFinally); } </script> -<Dialog {isOpen}> - <svelte:fragment slot="header"> - <h2>Add Character</h2> - </svelte:fragment> - <CharacterForm bind:character on:submit={add}> - <div class="flex justify-end gap-4"> - <SubmitButton active={character.name.length > 0} /> - </div> - </CharacterForm> +<Dialog title="Add Character" {...modal}> + <CharacterForm {initial} {submit} /> </Dialog> diff --git a/frontend/src/lib/dialogs/AddCircle.svelte b/frontend/src/lib/dialogs/AddCircle.svelte index f0ef014..faffc63 100644 --- a/frontend/src/lib/dialogs/AddCircle.svelte +++ b/frontend/src/lib/dialogs/AddCircle.svelte @@ -1,30 +1,22 @@ <script lang="ts"> - import { addCircle, type CircleInput } from '$gql/Mutations'; + import type { AddCircleInput } from '$gql/graphql'; + import { addCircle } from '$gql/Mutations'; import Dialog from '$lib/components/Dialog.svelte'; - import SubmitButton from '$lib/components/SubmitButton.svelte'; import CircleForm from '$lib/forms/CircleForm.svelte'; import { toastFinally } from '$lib/Toasts'; import { getContextClient } from '@urql/svelte'; - import { closeModal } from 'svelte-modals'; + import type { ModalProps } from 'svelte-modals'; const client = getContextClient(); - export let isOpen: boolean; + let modal: ModalProps = $props(); + const initial = { name: '' }; - let circle = { name: '' }; - - function add(event: CustomEvent<CircleInput>) { - addCircle(client, { input: event.detail }).then(closeModal).catch(toastFinally); + function submit(input: AddCircleInput) { + addCircle(client, { input }).then(modal.close).catch(toastFinally); } </script> -<Dialog {isOpen}> - <svelte:fragment slot="header"> - <h2>Add Circle</h2> - </svelte:fragment> - <CircleForm bind:circle on:submit={add}> - <div class="flex justify-end gap-4"> - <SubmitButton active={circle.name.length > 0} /> - </div> - </CircleForm> +<Dialog title="Add Circle" {...modal}> + <CircleForm {initial} {submit} /> </Dialog> diff --git a/frontend/src/lib/dialogs/AddNamespace.svelte b/frontend/src/lib/dialogs/AddNamespace.svelte index e81b22a..45183f4 100644 --- a/frontend/src/lib/dialogs/AddNamespace.svelte +++ b/frontend/src/lib/dialogs/AddNamespace.svelte @@ -1,30 +1,22 @@ <script lang="ts"> - import { addNamespace, type NamespaceInput } from '$gql/Mutations'; + import type { AddNamespaceInput } from '$gql/graphql'; + import { addNamespace } from '$gql/Mutations'; import Dialog from '$lib/components/Dialog.svelte'; - import SubmitButton from '$lib/components/SubmitButton.svelte'; import NamespaceForm from '$lib/forms/NamespaceForm.svelte'; import { toastFinally } from '$lib/Toasts'; import { getContextClient } from '@urql/svelte'; - import { closeModal } from 'svelte-modals'; + import type { ModalProps } from 'svelte-modals'; const client = getContextClient(); - export let isOpen: boolean; + let modal: ModalProps = $props(); + const initial = { name: '' }; - let namespace = { name: '' }; - - function add(event: CustomEvent<NamespaceInput>) { - addNamespace(client, { input: event.detail }).then(closeModal).catch(toastFinally); + function submit(input: AddNamespaceInput) { + addNamespace(client, { input }).then(modal.close).catch(toastFinally); } </script> -<Dialog {isOpen}> - <svelte:fragment slot="header"> - <h2>Add Namespace</h2> - </svelte:fragment> - <NamespaceForm bind:namespace on:submit={add}> - <div class="flex justify-end gap-4"> - <SubmitButton active={namespace.name.length > 0} /> - </div> - </NamespaceForm> +<Dialog title="Add Namespace" {...modal}> + <NamespaceForm {initial} {submit} /> </Dialog> diff --git a/frontend/src/lib/dialogs/AddTag.svelte b/frontend/src/lib/dialogs/AddTag.svelte index 00d3a03..da78bce 100644 --- a/frontend/src/lib/dialogs/AddTag.svelte +++ b/frontend/src/lib/dialogs/AddTag.svelte @@ -1,30 +1,22 @@ <script lang="ts"> - import { addTag, type TagInput } from '$gql/Mutations'; + import type { AddTagInput } from '$gql/graphql'; + import { addTag } from '$gql/Mutations'; import Dialog from '$lib/components/Dialog.svelte'; - import SubmitButton from '$lib/components/SubmitButton.svelte'; import TagForm from '$lib/forms/TagForm.svelte'; import { toastFinally } from '$lib/Toasts'; import { getContextClient } from '@urql/svelte'; - import { closeModal } from 'svelte-modals'; + import { type ModalProps } from 'svelte-modals'; const client = getContextClient(); - export let isOpen: boolean; + let modal: ModalProps = $props(); + const initial = { name: '', namespaces: [] }; - let tag = { name: '', namespaces: [] }; - - function add(event: CustomEvent<TagInput>) { - addTag(client, { input: event.detail }).then(closeModal).catch(toastFinally); + function submit(input: AddTagInput) { + addTag(client, { input }).then(modal.close).catch(toastFinally); } </script> -<Dialog {isOpen}> - <svelte:fragment slot="header"> - <h2>Add Tag</h2> - </svelte:fragment> - <TagForm bind:tag on:submit={add}> - <div class="flex justify-end gap-4"> - <SubmitButton active={tag.name.length > 0} /> - </div> - </TagForm> +<Dialog title="Add Tag" {...modal}> + <TagForm {initial} {submit} /> </Dialog> diff --git a/frontend/src/lib/dialogs/AddWorld.svelte b/frontend/src/lib/dialogs/AddWorld.svelte index ceb946e..075d872 100644 --- a/frontend/src/lib/dialogs/AddWorld.svelte +++ b/frontend/src/lib/dialogs/AddWorld.svelte @@ -1,30 +1,22 @@ <script lang="ts"> - import { addWorld, type WorldInput } from '$gql/Mutations'; + import type { AddWorldInput } from '$gql/graphql'; + import { addWorld } from '$gql/Mutations'; import Dialog from '$lib/components/Dialog.svelte'; - import SubmitButton from '$lib/components/SubmitButton.svelte'; import WorldForm from '$lib/forms/WorldForm.svelte'; import { toastFinally } from '$lib/Toasts'; import { getContextClient } from '@urql/svelte'; - import { closeModal } from 'svelte-modals'; + import type { ModalProps } from 'svelte-modals'; const client = getContextClient(); - export let isOpen: boolean; + let modal: ModalProps = $props(); + const initial = { name: '' }; - let world = { name: '' }; - - function add(event: CustomEvent<WorldInput>) { - addWorld(client, { input: event.detail }).then(closeModal).catch(toastFinally); + function submit(input: AddWorldInput) { + addWorld(client, { input }).then(modal.close).catch(toastFinally); } </script> -<Dialog {isOpen}> - <svelte:fragment slot="header"> - <h2>Add World</h2> - </svelte:fragment> - <WorldForm bind:world on:submit={add}> - <div class="flex justify-end gap-4"> - <SubmitButton active={world.name.length > 0} /> - </div> - </WorldForm> +<Dialog title="Add World" {...modal}> + <WorldForm {initial} {submit} /> </Dialog> diff --git a/frontend/src/lib/dialogs/ConfirmDeletion.svelte b/frontend/src/lib/dialogs/ConfirmDeletion.svelte index 6b0cbf8..571fd05 100644 --- a/frontend/src/lib/dialogs/ConfirmDeletion.svelte +++ b/frontend/src/lib/dialogs/ConfirmDeletion.svelte @@ -1,29 +1,30 @@ <script lang="ts"> import { accelerator } from '$lib/Shortcuts'; import Dialog from '$lib/components/Dialog.svelte'; - import { closeModal } from 'svelte-modals'; + import type { ModalProps } from 'svelte-modals'; - export let isOpen: boolean; - export let callback: () => void; + interface Props extends ModalProps { + callback: () => void; + names: string[]; + typename: string; + warning?: string; + } + + let { callback, names, typename, warning = undefined, ...modal }: Props = $props(); - export let names: string[]; - export let typename: string; - export let warning: string | undefined = undefined; const multiple = names.length > 1; const formattedTypename = multiple ? `${typename}s` : typename; const formattedNames = multiple ? `${names.length} ${formattedTypename}` : names[0]; - function confirm() { + function confirm(event: SubmitEvent) { + event.preventDefault(); callback(); - closeModal(); + modal.close(); } </script> -<Dialog {isOpen}> - <svelte:fragment slot="header"> - <h2>Delete {formattedTypename}</h2> - </svelte:fragment> - <form on:submit|preventDefault={confirm}> +<Dialog title="Delete {formattedTypename}" {...modal}> + <form onsubmit={confirm}> <div class="flex flex-col"> <p class="mb-3"> Are you sure you want to delete <span class="font-semibold">{formattedNames}</span>? @@ -39,13 +40,13 @@ {/if} {/if} {#if warning} - <p class="font-medium text-red-600">Warning: {warning}</p> + <p class="font-semibold text-rose-600">Warning: {warning}</p> {/if} </div> <div class="flex justify-end gap-4"> <button type="submit" class="btn-rose" use:accelerator={'Enter'}>Delete</button> - <button type="button" on:click={closeModal} class="btn-slate">Cancel</button> + <button type="button" onclick={() => modal.close()} class="btn-slate">Cancel</button> </div> </form> </Dialog> diff --git a/frontend/src/lib/dialogs/EditArtist.svelte b/frontend/src/lib/dialogs/EditArtist.svelte index dd08bc6..fa5c143 100644 --- a/frontend/src/lib/dialogs/EditArtist.svelte +++ b/frontend/src/lib/dialogs/EditArtist.svelte @@ -1,46 +1,37 @@ <script lang="ts"> - import { deleteArtists, updateArtists, type ArtistInput } from '$gql/Mutations'; - import { itemEquals } from '$gql/Utils'; - import { type Artist } from '$gql/graphql'; + import { deleteArtists, updateArtists } from '$gql/Mutations'; + import { omitIdentifiers } from '$gql/Utils'; + import type { Artist, UpdateArtistInput } from '$gql/graphql'; import { toastFinally } from '$lib/Toasts'; import { confirmDeletion } from '$lib/Utils'; import DeleteButton from '$lib/components/DeleteButton.svelte'; import Dialog from '$lib/components/Dialog.svelte'; - import SubmitButton from '$lib/components/SubmitButton.svelte'; import ArtistForm from '$lib/forms/ArtistForm.svelte'; import { getContextClient } from '@urql/svelte'; - import { closeModal } from 'svelte-modals'; + import type { ModalProps } from 'svelte-modals'; const client = getContextClient(); - export let isOpen: boolean; + interface Props extends ModalProps { + artist: Artist; + } - export let artist: Artist; - const original = structuredClone(artist); - $: pending = !itemEquals(artist, original); + let { artist, ...modal }: Props = $props(); + const initial = omitIdentifiers(artist); - function save(event: CustomEvent<ArtistInput>) { - updateArtists(client, { ids: artist.id, input: event.detail }) - .then(closeModal) - .catch(toastFinally); + function submit(input: UpdateArtistInput) { + updateArtists(client, { ids: artist.id, input }).then(modal.close).catch(toastFinally); } function deleteArtist() { confirmDeletion('Artist', artist.name, () => { - deleteArtists(client, { ids: artist.id }).then(closeModal).catch(toastFinally); + deleteArtists(client, { ids: artist.id }).then(modal.close).catch(toastFinally); }); } </script> -<Dialog {isOpen}> - <svelte:fragment slot="header"> - <h2>Edit Artist</h2> - </svelte:fragment> - <ArtistForm bind:artist on:submit={save}> - <div class="flex gap-4"> - <DeleteButton on:click={deleteArtist} /> - <div class="grow" /> - <SubmitButton active={pending} /> - </div> +<Dialog title="Edit Artist" {...modal}> + <ArtistForm {initial} {submit}> + <DeleteButton onclick={deleteArtist} /> </ArtistForm> </Dialog> diff --git a/frontend/src/lib/dialogs/EditCharacter.svelte b/frontend/src/lib/dialogs/EditCharacter.svelte index 3b45e78..71125db 100644 --- a/frontend/src/lib/dialogs/EditCharacter.svelte +++ b/frontend/src/lib/dialogs/EditCharacter.svelte @@ -1,46 +1,37 @@ <script lang="ts"> - import { deleteCharacters, updateCharacters, type CharacterInput } from '$gql/Mutations'; - import { itemEquals } from '$gql/Utils'; - import { type Character } from '$gql/graphql'; + import { deleteCharacters, updateCharacters } from '$gql/Mutations'; + import { omitIdentifiers } from '$gql/Utils'; + import type { Character, UpdateCharacterInput } from '$gql/graphql'; import { toastFinally } from '$lib/Toasts'; import { confirmDeletion } from '$lib/Utils'; import DeleteButton from '$lib/components/DeleteButton.svelte'; import Dialog from '$lib/components/Dialog.svelte'; - import SubmitButton from '$lib/components/SubmitButton.svelte'; import CharacterForm from '$lib/forms/CharacterForm.svelte'; import { getContextClient } from '@urql/svelte'; - import { closeModal } from 'svelte-modals'; + import type { ModalProps } from 'svelte-modals'; const client = getContextClient(); - export let isOpen: boolean; + interface Props extends ModalProps { + character: Character; + } - export let character: Character; - const original = structuredClone(character); - $: pending = !itemEquals(original, character); + let { character, ...modal }: Props = $props(); + const initial = omitIdentifiers(character); - function save(event: CustomEvent<CharacterInput>) { - updateCharacters(client, { ids: character.id, input: event.detail }) - .then(closeModal) - .catch(toastFinally); + function submit(input: UpdateCharacterInput) { + updateCharacters(client, { ids: character.id, input }).then(modal.close).catch(toastFinally); } function deleteCharacter() { confirmDeletion('Character', character.name, () => { - deleteCharacters(client, { ids: character.id }).then(closeModal).catch(toastFinally); + deleteCharacters(client, { ids: character.id }).then(modal.close).catch(toastFinally); }); } </script> -<Dialog {isOpen}> - <svelte:fragment slot="header"> - <h2>Edit Character</h2> - </svelte:fragment> - <CharacterForm bind:character on:submit={save}> - <div class="flex gap-4"> - <DeleteButton on:click={deleteCharacter} /> - <div class="grow" /> - <SubmitButton active={pending} /> - </div> +<Dialog title="Edit Character" {...modal}> + <CharacterForm {initial} {submit}> + <DeleteButton onclick={deleteCharacter} /> </CharacterForm> </Dialog> diff --git a/frontend/src/lib/dialogs/EditCircle.svelte b/frontend/src/lib/dialogs/EditCircle.svelte index bdc1217..7cb0f14 100644 --- a/frontend/src/lib/dialogs/EditCircle.svelte +++ b/frontend/src/lib/dialogs/EditCircle.svelte @@ -1,46 +1,37 @@ <script lang="ts"> - import { deleteCircles, updateCircles, type CircleInput } from '$gql/Mutations'; - import { itemEquals } from '$gql/Utils'; - import { type Circle } from '$gql/graphql'; + import { deleteCircles, updateCircles } from '$gql/Mutations'; + import { omitIdentifiers } from '$gql/Utils'; + import type { Circle, UpdateCircleInput } from '$gql/graphql'; import { toastFinally } from '$lib/Toasts'; import { confirmDeletion } from '$lib/Utils'; import DeleteButton from '$lib/components/DeleteButton.svelte'; import Dialog from '$lib/components/Dialog.svelte'; - import SubmitButton from '$lib/components/SubmitButton.svelte'; import CircleForm from '$lib/forms/CircleForm.svelte'; import { getContextClient } from '@urql/svelte'; - import { closeModal } from 'svelte-modals'; + import type { ModalProps } from 'svelte-modals'; const client = getContextClient(); - export let isOpen: boolean; + interface Props extends ModalProps { + circle: Circle; + } - export let circle: Circle; - const original = structuredClone(circle); - $: pending = !itemEquals(original, circle); + let { circle, ...modal }: Props = $props(); + const initial = omitIdentifiers(circle); - function save(event: CustomEvent<CircleInput>) { - updateCircles(client, { ids: circle.id, input: event.detail }) - .then(closeModal) - .catch(toastFinally); + function submit(input: UpdateCircleInput) { + updateCircles(client, { ids: circle.id, input }).then(modal.close).catch(toastFinally); } function deleteCircle() { confirmDeletion('Circle', circle.name, () => { - deleteCircles(client, { ids: circle.id }).then(closeModal).catch(toastFinally); + deleteCircles(client, { ids: circle.id }).then(modal.close).catch(toastFinally); }); } </script> -<Dialog {isOpen}> - <svelte:fragment slot="header"> - <h2>Edit Circle</h2> - </svelte:fragment> - <CircleForm bind:circle on:submit={save}> - <div class="flex gap-4"> - <DeleteButton on:click={deleteCircle} /> - <div class="grow" /> - <SubmitButton active={pending} /> - </div> +<Dialog title="Edit Circle" {...modal}> + <CircleForm {initial} {submit}> + <DeleteButton onclick={deleteCircle} /> </CircleForm> </Dialog> diff --git a/frontend/src/lib/dialogs/EditNamespace.svelte b/frontend/src/lib/dialogs/EditNamespace.svelte index f398b21..b104f83 100644 --- a/frontend/src/lib/dialogs/EditNamespace.svelte +++ b/frontend/src/lib/dialogs/EditNamespace.svelte @@ -1,46 +1,37 @@ <script lang="ts"> - import { deleteNamespaces, updateNamespaces, type NamespaceInput } from '$gql/Mutations'; - import { itemEquals } from '$gql/Utils'; - import { type Namespace } from '$gql/graphql'; + import { deleteNamespaces, updateNamespaces } from '$gql/Mutations'; + import { omitIdentifiers } from '$gql/Utils'; + import type { Namespace, UpdateNamespaceInput } from '$gql/graphql'; import { toastFinally } from '$lib/Toasts'; import { confirmDeletion } from '$lib/Utils'; import DeleteButton from '$lib/components/DeleteButton.svelte'; import Dialog from '$lib/components/Dialog.svelte'; - import SubmitButton from '$lib/components/SubmitButton.svelte'; import NamespaceForm from '$lib/forms/NamespaceForm.svelte'; import { getContextClient } from '@urql/svelte'; - import { closeModal } from 'svelte-modals'; + import type { ModalProps } from 'svelte-modals'; const client = getContextClient(); - export let isOpen: boolean; + interface Props extends ModalProps { + namespace: Namespace; + } - export let namespace: Namespace; - const original = structuredClone(namespace); - $: pending = !itemEquals(original, namespace); + let { namespace, ...modal }: Props = $props(); + const initial = omitIdentifiers(namespace); - function save(event: CustomEvent<NamespaceInput>) { - updateNamespaces(client, { ids: namespace.id, input: event.detail }) - .then(closeModal) - .catch(toastFinally); + function submit(input: UpdateNamespaceInput) { + updateNamespaces(client, { ids: namespace.id, input }).then(modal.close).catch(toastFinally); } function deleteNamespace() { confirmDeletion('Namespace', namespace.name, () => { - deleteNamespaces(client, { ids: namespace.id }).then(closeModal).catch(toastFinally); + deleteNamespaces(client, { ids: namespace.id }).then(modal.close).catch(toastFinally); }); } </script> -<Dialog {isOpen}> - <svelte:fragment slot="header"> - <h2>Edit Namespace</h2> - </svelte:fragment> - <NamespaceForm bind:namespace on:submit={save}> - <div class="flex gap-4"> - <DeleteButton on:click={deleteNamespace} /> - <div class="grow" /> - <SubmitButton active={pending} /> - </div> +<Dialog title="Edit Namespace" {...modal}> + <NamespaceForm {initial} {submit}> + <DeleteButton onclick={deleteNamespace} /> </NamespaceForm> </Dialog> diff --git a/frontend/src/lib/dialogs/EditTag.svelte b/frontend/src/lib/dialogs/EditTag.svelte index d2d0013..555d6d1 100644 --- a/frontend/src/lib/dialogs/EditTag.svelte +++ b/frontend/src/lib/dialogs/EditTag.svelte @@ -1,44 +1,37 @@ <script lang="ts"> - import { deleteTags, updateTags, type TagInput } from '$gql/Mutations'; - import { tagEquals } from '$gql/Utils'; - import { type FullTag } from '$gql/graphql'; + import { deleteTags, updateTags } from '$gql/Mutations'; + import { omitIdentifiers } from '$gql/Utils'; + import { type FullTag, type UpdateTagInput } from '$gql/graphql'; import { toastFinally } from '$lib/Toasts'; import { confirmDeletion } from '$lib/Utils'; import DeleteButton from '$lib/components/DeleteButton.svelte'; import Dialog from '$lib/components/Dialog.svelte'; - import SubmitButton from '$lib/components/SubmitButton.svelte'; import TagForm from '$lib/forms/TagForm.svelte'; import { getContextClient } from '@urql/svelte'; - import { closeModal } from 'svelte-modals'; + import { type ModalProps } from 'svelte-modals'; const client = getContextClient(); - export let isOpen: boolean; + interface Props extends ModalProps { + tag: FullTag; + } - export let tag: FullTag; - const original = structuredClone(tag); - $: pending = !tagEquals(original, tag); + let { tag, ...modal }: Props = $props(); + const initial = omitIdentifiers(tag); - function save(event: CustomEvent<TagInput>) { - updateTags(client, { ids: tag.id, input: event.detail }).then(closeModal).catch(toastFinally); + function submit(input: UpdateTagInput) { + updateTags(client, { ids: tag.id, input }).then(modal.close).catch(toastFinally); } function deleteTag() { confirmDeletion('Tag', tag.name, () => { - deleteTags(client, { ids: tag.id }).then(closeModal).catch(toastFinally); + deleteTags(client, { ids: tag.id }).then(modal.close).catch(toastFinally); }); } </script> -<Dialog {isOpen}> - <svelte:fragment slot="header"> - <h2>Edit Tag</h2> - </svelte:fragment> - <TagForm bind:tag on:submit={save}> - <div class="flex gap-4"> - <DeleteButton on:click={deleteTag} /> - <div class="grow" /> - <SubmitButton active={pending} /> - </div> +<Dialog title="Edit Tag" {...modal}> + <TagForm {initial} {submit}> + <DeleteButton onclick={deleteTag} /> </TagForm> </Dialog> diff --git a/frontend/src/lib/dialogs/EditWorld.svelte b/frontend/src/lib/dialogs/EditWorld.svelte index 82afe6a..869dc21 100644 --- a/frontend/src/lib/dialogs/EditWorld.svelte +++ b/frontend/src/lib/dialogs/EditWorld.svelte @@ -1,46 +1,37 @@ <script lang="ts"> - import { type World } from '$gql/graphql'; - import { deleteWorlds, updateWorlds, type WorldInput } from '$gql/Mutations'; - import { itemEquals } from '$gql/Utils'; + import { deleteWorlds, updateWorlds } from '$gql/Mutations'; + import { omitIdentifiers } from '$gql/Utils'; + import type { UpdateWorldInput, World } from '$gql/graphql'; + import { toastFinally } from '$lib/Toasts'; + import { confirmDeletion } from '$lib/Utils'; import DeleteButton from '$lib/components/DeleteButton.svelte'; import Dialog from '$lib/components/Dialog.svelte'; - import SubmitButton from '$lib/components/SubmitButton.svelte'; import WorldForm from '$lib/forms/WorldForm.svelte'; - import { toastFinally } from '$lib/Toasts'; - import { confirmDeletion } from '$lib/Utils'; import { getContextClient } from '@urql/svelte'; - import { closeModal } from 'svelte-modals'; + import type { ModalProps } from 'svelte-modals'; const client = getContextClient(); - export let isOpen: boolean; + interface Props extends ModalProps { + world: World; + } - export let world: World; - const original = structuredClone(world); - $: pending = !itemEquals(original, world); + let { world, ...modal }: Props = $props(); + const initial = omitIdentifiers(world); - function save(event: CustomEvent<WorldInput>) { - updateWorlds(client, { ids: world.id, input: event.detail }) - .then(closeModal) - .catch(toastFinally); + function submit(input: UpdateWorldInput) { + updateWorlds(client, { ids: world.id, input }).then(modal.close).catch(toastFinally); } function deleteWorld() { confirmDeletion('World', world.name, () => { - deleteWorlds(client, { ids: world.id }).then(closeModal).catch(toastFinally); + deleteWorlds(client, { ids: world.id }).then(modal.close).catch(toastFinally); }); } </script> -<Dialog {isOpen}> - <svelte:fragment slot="header"> - <h2>Edit World</h2> - </svelte:fragment> - <WorldForm bind:world on:submit={save}> - <div class="flex gap-4"> - <DeleteButton on:click={deleteWorld} /> - <div class="grow" /> - <SubmitButton active={pending} /> - </div> +<Dialog title="Edit World" {...modal}> + <WorldForm {initial} {submit}> + <DeleteButton onclick={deleteWorld} /> </WorldForm> </Dialog> diff --git a/frontend/src/lib/dialogs/UpdateComics.svelte b/frontend/src/lib/dialogs/UpdateComics.svelte index 8de9622..483e379 100644 --- a/frontend/src/lib/dialogs/UpdateComics.svelte +++ b/frontend/src/lib/dialogs/UpdateComics.svelte @@ -3,94 +3,109 @@ import { artistList, characterList, circleList, comicTagList, worldList } from '$gql/Queries'; import { categories, censorships, directions, languages, layouts, ratings } from '$lib/Enums'; import { toastFinally } from '$lib/Toasts'; - import { UpdateComicsControls } from '$lib/Update'; + import { UpdateComicsControls } from '$lib/Update.svelte'; import Dialog from '$lib/components/Dialog.svelte'; - import Labelled from '$lib/components/Labelled.svelte'; import LabelledBlock from '$lib/components/LabelledBlock.svelte'; import Select from '$lib/components/Select.svelte'; import SubmitButton from '$lib/components/SubmitButton.svelte'; import { getContextClient } from '@urql/svelte'; - import { closeModal } from 'svelte-modals'; + import type { ModalProps } from 'svelte-modals'; import UpdateModeSelector from './components/UpdateModeSelector.svelte'; const client = getContextClient(); - export let isOpen: boolean; - export let ids: number[]; + interface Props extends ModalProps { + ids: number[]; + } - $: tagsQuery = comicTagList(client); - $: artistsQuery = artistList(client); - $: charactersQuery = characterList(client); - $: circlesQuery = circleList(client); - $: worldsQuery = worldList(client); + let { ids, ...modal }: Props = $props(); - $: tags = $tagsQuery.data?.comicTags.edges; - $: artists = $artistsQuery.data?.artists.edges; - $: characters = $charactersQuery.data?.characters.edges; - $: circles = $circlesQuery.data?.circles.edges; - $: worlds = $worldsQuery.data?.worlds.edges; + let tagsQuery = $derived(comicTagList(client)); + let artistsQuery = $derived(artistList(client)); + let charactersQuery = $derived(characterList(client)); + let circlesQuery = $derived(circleList(client)); + let worldsQuery = $derived(worldList(client)); + + let tags = $derived($tagsQuery.data?.comicTags.edges); + let artists = $derived($artistsQuery.data?.artists.edges); + let characters = $derived($charactersQuery.data?.characters.edges); + let circles = $derived($circlesQuery.data?.circles.edges); + let worlds = $derived($worldsQuery.data?.worlds.edges); const controls = new UpdateComicsControls(); - const update = () => { - updateComics(client, { - ids: ids, - input: controls.toInput() - }) - .then(closeModal) - .catch(toastFinally); - }; + function update(event: SubmitEvent) { + event.preventDefault(); + + updateComics(client, { ids, input: controls.input() }).then(modal.close).catch(toastFinally); + } </script> -<Dialog {isOpen}> - <svelte:fragment slot="header"> - <h2>Edit Comics</h2> - </svelte:fragment> - <form on:submit|preventDefault={update}> +<Dialog title="Edit Comics" {...modal}> + <form onsubmit={update}> <div class="grid-labels"> - <Labelled label="Category" let:id> - <Select clearable {id} options={categories} bind:value={controls.category.value} /> - </Labelled> - <Labelled label="Rating" let:id> - <Select clearable {id} options={ratings} bind:value={controls.rating.value} /> - </Labelled> - <Labelled label="Censorship" let:id> - <Select clearable {id} options={censorships} bind:value={controls.censorship.value} /> - </Labelled> - <Labelled label="Language" let:id> - <Select clearable {id} options={languages} bind:value={controls.language.value} /> - </Labelled> - <Labelled label="Direction" let:id> - <Select clearable {id} options={directions} bind:value={controls.direction.value} /> - </Labelled> - <Labelled label="Layout" let:id> - <Select clearable {id} options={layouts} bind:value={controls.layout.value} /> - </Labelled> + <label class="self-center" for="category">Category</label> + <Select clearable id="category" options={categories} bind:value={controls.category.value} /> + + <label class="self-center" for="rating">Rating</label> + <Select clearable id="rating" options={ratings} bind:value={controls.rating.value} /> + + <label class="self-center" for="censor">Censorship</label> + <Select clearable id="censor" options={censorships} bind:value={controls.censorship.value} /> + + <label class="self-center" for="language">Language</label> + <Select clearable id="language" options={languages} bind:value={controls.language.value} /> + + <label class="self-center" for="direction">Direction</label> + <Select clearable id="direction" options={directions} bind:value={controls.direction.value} /> + + <label class="self-center" for="layout">Layout</label> + <Select clearable id="layout" options={layouts} bind:value={controls.layout.value} /> </div> - <LabelledBlock label="Artists" let:id> - <Select multi {id} options={artists} bind:value={controls.artists.ids} /> - <UpdateModeSelector bind:mode={controls.artists.options.mode} slot="controls" /> + <LabelledBlock label="Artists"> + {#snippet children({ id })} + <Select multi {id} options={artists} bind:value={controls.artists.ids} /> + {/snippet} + {#snippet side()} + <UpdateModeSelector bind:mode={controls.artists.options.mode} /> + {/snippet} </LabelledBlock> - <LabelledBlock label="Circles" let:id> - <Select multi {id} options={circles} bind:value={controls.circles.ids} /> - <UpdateModeSelector bind:mode={controls.circles.options.mode} slot="controls" /> + <LabelledBlock label="Circles"> + {#snippet children({ id })} + <Select multi {id} options={circles} bind:value={controls.circles.ids} /> + {/snippet} + {#snippet side()} + <UpdateModeSelector bind:mode={controls.circles.options.mode} /> + {/snippet} </LabelledBlock> - <LabelledBlock label="Characters" let:id> - <Select multi {id} options={characters} bind:value={controls.characters.ids} /> - <UpdateModeSelector bind:mode={controls.characters.options.mode} slot="controls" /> + <LabelledBlock label="Characters"> + {#snippet children({ id })} + <Select multi {id} options={characters} bind:value={controls.characters.ids} /> + {/snippet} + {#snippet side()} + <UpdateModeSelector bind:mode={controls.characters.options.mode} /> + {/snippet} </LabelledBlock> - <LabelledBlock label="Worlds" let:id> - <Select multi {id} options={worlds} bind:value={controls.worlds.ids} /> - <UpdateModeSelector bind:mode={controls.worlds.options.mode} slot="controls" /> + <LabelledBlock label="Worlds"> + {#snippet children({ id })} + <Select multi {id} options={worlds} bind:value={controls.worlds.ids} /> + {/snippet} + {#snippet side()} + <UpdateModeSelector bind:mode={controls.worlds.options.mode} /> + {/snippet} </LabelledBlock> - <LabelledBlock label="Tags" let:id> - <Select multi {id} options={tags} bind:value={controls.tags.ids} /> - <UpdateModeSelector bind:mode={controls.tags.options.mode} slot="controls" /> + <LabelledBlock label="Tags"> + {#snippet children({ id })} + <Select multi {id} options={tags} bind:value={controls.tags.ids} /> + {/snippet} + {#snippet side()} + <UpdateModeSelector bind:mode={controls.tags.options.mode} /> + {/snippet} </LabelledBlock> <div class="flex justify-end gap-4"> - <SubmitButton active={controls.hasInput()} /> + <SubmitButton pending={controls.pending()} /> </div> </form> </Dialog> diff --git a/frontend/src/lib/dialogs/UpdateTags.svelte b/frontend/src/lib/dialogs/UpdateTags.svelte index f753c7f..840e92e 100644 --- a/frontend/src/lib/dialogs/UpdateTags.svelte +++ b/frontend/src/lib/dialogs/UpdateTags.svelte @@ -2,44 +2,49 @@ import { updateTags } from '$gql/Mutations'; import { namespaceList } from '$gql/Queries'; import { toastFinally } from '$lib/Toasts'; - import { UpdateTagsControls } from '$lib/Update'; + import { UpdateTagsControls } from '$lib/Update.svelte'; import Dialog from '$lib/components/Dialog.svelte'; import LabelledBlock from '$lib/components/LabelledBlock.svelte'; import Select from '$lib/components/Select.svelte'; import SubmitButton from '$lib/components/SubmitButton.svelte'; import { getContextClient } from '@urql/svelte'; - import { closeModal } from 'svelte-modals'; + import { modals, type ModalProps } from 'svelte-modals'; import UpdateModeSelector from './components/UpdateModeSelector.svelte'; const client = getContextClient(); - $: namespaceQuery = namespaceList(client); - $: namespaces = $namespaceQuery.data?.namespaces.edges; + let namespaceQuery = $derived(namespaceList(client)); + let namespaces = $derived($namespaceQuery.data?.namespaces.edges); - export let isOpen: boolean; - export let ids: number[]; + interface Props extends ModalProps { + ids: number[]; + } - const controls = new UpdateTagsControls(); + let { ids, ...modal }: Props = $props(); + let controls = new UpdateTagsControls(); - const update = () => { - updateTags(client, { ids: ids, input: controls.toInput() }) - .then(closeModal) + function update(event: SubmitEvent) { + event.preventDefault(); + + updateTags(client, { ids, input: controls.input() }) + .then(() => modals.close()) .catch(toastFinally); - }; + } </script> -<Dialog {isOpen}> - <svelte:fragment slot="header"> - <h2>Edit Tags</h2> - </svelte:fragment> - <form on:submit|preventDefault={update}> - <LabelledBlock label="Namespaces" let:id> - <Select multi {id} options={namespaces} bind:value={controls.namespaces.ids} /> - <UpdateModeSelector bind:mode={controls.namespaces.options.mode} slot="controls" /> +<Dialog title="Edit Tags" {...modal}> + <form onsubmit={update}> + <LabelledBlock label="Namespaces"> + {#snippet children({ id })} + <Select multi {id} options={namespaces} bind:value={controls.namespaces.ids} /> + {/snippet} + {#snippet side()} + <UpdateModeSelector bind:mode={controls.namespaces.options.mode} /> + {/snippet} </LabelledBlock> <div class="flex justify-end gap-4"> - <SubmitButton active={controls.hasInput()} /> + <SubmitButton pending={controls.pending()} /> </div> </form> </Dialog> diff --git a/frontend/src/lib/dialogs/components/UpdateModeSelector.svelte b/frontend/src/lib/dialogs/components/UpdateModeSelector.svelte index e4b4479..6548fb5 100644 --- a/frontend/src/lib/dialogs/components/UpdateModeSelector.svelte +++ b/frontend/src/lib/dialogs/components/UpdateModeSelector.svelte @@ -2,7 +2,7 @@ import { UpdateMode } from '$gql/graphql'; import { UpdateModeLabel } from '$lib/Enums'; - export let mode: UpdateMode; + let { mode = $bindable() }: { mode: UpdateMode } = $props(); function select(e: string) { mode = e as UpdateMode; @@ -16,7 +16,7 @@ class:active={mode === e} class:dangerous={mode !== UpdateMode.Add} class="btn btn-xs hover:bg-slate-700 [&.active.dangerous]:bg-rose-800 [&.active]:bg-indigo-700" - on:click={() => select(e)} + onclick={() => select(e)} > {label} </button> diff --git a/frontend/src/lib/filter/ComicFilterForm.svelte b/frontend/src/lib/filter/ComicFilterForm.svelte index 13b5320..7f0058d 100644 --- a/frontend/src/lib/filter/ComicFilterForm.svelte +++ b/frontend/src/lib/filter/ComicFilterForm.svelte @@ -1,48 +1,61 @@ <script lang="ts"> - import { page } from '$app/stores'; import { artistList, characterList, circleList, comicTagList, worldList } from '$gql/Queries'; - import { ComicFilterContext, getFilterContext } from '$lib/Filter'; + import { categories, censorships, languages, ratings } from '$lib/Enums'; + import { ComicFilterContext } from '$lib/Filter.svelte'; import { getContextClient } from '@urql/svelte'; - import ComicFilterGroup from './components/ComicFilterGroup.svelte'; + import Filter from './components/Filter.svelte'; import FilterForm from './components/FilterForm.svelte'; const client = getContextClient(); - $: tagsQuery = comicTagList(client, { forFilter: true }); - $: artistsQuery = artistList(client); - $: charactersQuery = characterList(client); - $: circlesQuery = circleList(client); - $: worldsQuery = worldList(client); + let { filter }: { filter: ComicFilterContext } = $props(); - $: tags = $tagsQuery.data?.comicTags.edges; - $: artists = $artistsQuery.data?.artists.edges; - $: characters = $charactersQuery.data?.characters.edges; - $: circles = $circlesQuery.data?.circles.edges; - $: worlds = $worldsQuery.data?.worlds.edges; + let tagsQuery = $derived(comicTagList(client, { forFilter: true })); + let artistsQuery = $derived(artistList(client)); + let charactersQuery = $derived(characterList(client)); + let circlesQuery = $derived(circleList(client)); + let worldsQuery = $derived(worldList(client)); - const filter = getFilterContext<ComicFilterContext>(); - const apply = () => $filter.apply($page.url.searchParams); + let tags = $derived($tagsQuery.data?.comicTags.edges); + let artists = $derived($artistsQuery.data?.artists.edges); + let characters = $derived($charactersQuery.data?.characters.edges); + let circles = $derived($circlesQuery.data?.circles.edges); + let worlds = $derived($worldsQuery.data?.worlds.edges); </script> -<FilterForm type="grid" on:submit={apply}> - <ComicFilterGroup - slot="include" - type="include" - bind:controls={$filter.include.controls} - {tags} - {artists} - {characters} - {circles} - {worlds} - /> - <ComicFilterGroup - slot="exclude" - type="exclude" - bind:controls={$filter.exclude.controls} - {tags} - {artists} - {characters} - {circles} - {worlds} - /> +<FilterForm type="grid" apply={filter.apply} expanded={filter.excludes > 0}> + {#snippet include(type)} + <Filter + {type} + title="Tags" + options={tags} + filter={filter.include.tags} + --grid-column="span 2" + /> + <Filter {type} title="Artists" options={artists} filter={filter.include.artists} /> + <Filter {type} title="Circles" options={circles} filter={filter.include.circles} /> + <Filter {type} title="Characters" options={characters} filter={filter.include.characters} /> + <Filter {type} title="Worlds" options={worlds} filter={filter.include.worlds} /> + <Filter {type} title="Categories" options={categories} filter={filter.include.categories} /> + <Filter {type} title="Ratings" options={ratings} filter={filter.include.ratings} /> + <Filter {type} title="Censorship" options={censorships} filter={filter.include.censorships} /> + <Filter {type} title="Languages" options={languages} filter={filter.include.languages} /> + {/snippet} + {#snippet exclude(type)} + <Filter + {type} + title="Tags" + options={tags} + filter={filter.exclude.tags} + --grid-column="span 2" + /> + <Filter {type} title="Artists" options={artists} filter={filter.exclude.artists} /> + <Filter {type} title="Circles" options={circles} filter={filter.exclude.circles} /> + <Filter {type} title="Characters" options={characters} filter={filter.exclude.characters} /> + <Filter {type} title="Worlds" options={worlds} filter={filter.exclude.worlds} /> + <Filter {type} title="Categories" options={categories} filter={filter.exclude.categories} /> + <Filter {type} title="Ratings" options={ratings} filter={filter.exclude.ratings} /> + <Filter {type} title="Censorship" options={censorships} filter={filter.exclude.censorships} /> + <Filter {type} title="Languages" options={languages} filter={filter.exclude.languages} /> + {/snippet} </FilterForm> diff --git a/frontend/src/lib/filter/TagFilterForm.svelte b/frontend/src/lib/filter/TagFilterForm.svelte index be5996e..280db8a 100644 --- a/frontend/src/lib/filter/TagFilterForm.svelte +++ b/frontend/src/lib/filter/TagFilterForm.svelte @@ -1,31 +1,23 @@ <script lang="ts"> - import { page } from '$app/stores'; import { namespaceList } from '$gql/Queries'; - import { TagFilterContext, getFilterContext } from '$lib/Filter'; + import { TagFilterContext } from '$lib/Filter.svelte'; import { getContextClient } from '@urql/svelte'; + import Filter from './components/Filter.svelte'; import FilterForm from './components/FilterForm.svelte'; - import TagFilterGroup from './components/TagFilterGroup.svelte'; const client = getContextClient(); - $: namespaceQuery = namespaceList(client); - $: namespaces = $namespaceQuery.data?.namespaces.edges; + let { filter }: { filter: TagFilterContext } = $props(); - const filter = getFilterContext<TagFilterContext>(); - const apply = () => $filter.apply($page.url.searchParams); + let namespaceQuery = $derived(namespaceList(client)); + let namespaces = $derived($namespaceQuery.data?.namespaces.edges); </script> -<FilterForm on:submit={apply}> - <TagFilterGroup - slot="include" - type="include" - bind:controls={$filter.include.controls} - {namespaces} - /> - <TagFilterGroup - slot="exclude" - type="exclude" - bind:controls={$filter.exclude.controls} - {namespaces} - /> +<FilterForm apply={filter.apply} expanded={filter.excludes > 0}> + {#snippet include(type)} + <Filter {type} title="Namespaces" options={namespaces} filter={filter.include.namespaces} /> + {/snippet} + {#snippet exclude(type)} + <Filter {type} title="Namespaces" options={namespaces} filter={filter.exclude.namespaces} /> + {/snippet} </FilterForm> diff --git a/frontend/src/lib/filter/components/ComicFilterGroup.svelte b/frontend/src/lib/filter/components/ComicFilterGroup.svelte deleted file mode 100644 index d302de4..0000000 --- a/frontend/src/lib/filter/components/ComicFilterGroup.svelte +++ /dev/null @@ -1,27 +0,0 @@ -<script lang="ts"> - import { categories, censorships, languages, ratings } from '$lib/Enums'; - import { ComicFilterControls } from '$lib/Filter'; - import type { ListItem } from '$lib/Utils'; - import { setContext } from 'svelte'; - import Filter from './Filter.svelte'; - - export let tags: ListItem[] | undefined; - export let artists: ListItem[] | undefined; - export let circles: ListItem[] | undefined; - export let characters: ListItem[] | undefined; - export let worlds: ListItem[] | undefined; - export let controls: ComicFilterControls; - export let type: 'include' | 'exclude'; - - setContext('filter-type', type); -</script> - -<Filter title="Tags" options={tags} bind:filter={controls.tags} --grid-column="span 2" /> -<Filter title="Artists" options={artists} bind:filter={controls.artists} /> -<Filter title="Circles" options={circles} bind:filter={controls.circles} /> -<Filter title="Characters" options={characters} bind:filter={controls.characters} /> -<Filter title="Worlds" options={worlds} bind:filter={controls.worlds} /> -<Filter title="Categories" options={categories} bind:filter={controls.categories} /> -<Filter title="Ratings" options={ratings} bind:filter={controls.ratings} /> -<Filter title="Censorship" options={censorships} bind:filter={controls.censorships} /> -<Filter title="Languages" options={languages} bind:filter={controls.languages} /> diff --git a/frontend/src/lib/filter/components/Filter.svelte b/frontend/src/lib/filter/components/Filter.svelte index ead5c4d..c164cbb 100644 --- a/frontend/src/lib/filter/components/Filter.svelte +++ b/frontend/src/lib/filter/components/Filter.svelte @@ -1,17 +1,19 @@ <script lang="ts"> - import { Association, Enum } from '$lib/Filter'; + import { Association, Enum, type FilterType } from '$lib/Filter.svelte'; import type { ListItem } from '$lib/Utils'; import Select from '$lib/components/Select.svelte'; - import { getContext } from 'svelte'; - export let title: string; - const context: 'include' | 'exclude' = getContext('filter-type'); - $: exclude = context === 'exclude'; + interface Props { + title: string; + type: FilterType; + options: ListItem[] | undefined; + filter: Association<string> | Enum<string>; + } - const id = `${context}-${title.toLowerCase()}`; + let { title, type, options, filter }: Props = $props(); + let exclude = $derived(type === 'exclude'); - export let options: ListItem[] | undefined; - export let filter: Association<string> | Enum<string>; + const id = `${type}-${title.toLowerCase()}`; </script> <div class:exclude class="filter-container"> @@ -24,7 +26,7 @@ title="matches all" class:active={filter.mode === 'all'} class="btn btn-xs" - on:click={() => (filter.mode = 'all')} + onclick={() => (filter.mode = 'all')} > ∀ </button> @@ -33,7 +35,7 @@ title="matches any of" class:active={filter.mode === 'any'} class="btn btn-xs" - on:click={() => (filter.mode = 'any')} + onclick={() => (filter.mode = 'any')} > ∃ </button> @@ -42,7 +44,7 @@ title="matches exactly" class:active={filter.mode === 'exact'} class="btn btn-xs" - on:click={() => (filter.mode = 'exact')} + onclick={() => (filter.mode = 'exact')} > = </button> @@ -53,7 +55,7 @@ title="empty" class:active={filter.empty} class="btn btn-xs" - on:click={() => (filter.empty = !filter.empty)} + onclick={() => (filter.empty = !filter.empty)} > ∅ </button> diff --git a/frontend/src/lib/filter/components/FilterForm.svelte b/frontend/src/lib/filter/components/FilterForm.svelte index 6fc4c90..ed58ed9 100644 --- a/frontend/src/lib/filter/components/FilterForm.svelte +++ b/frontend/src/lib/filter/components/FilterForm.svelte @@ -1,30 +1,40 @@ <script lang="ts"> + import { page } from '$app/state'; import Expander from '$lib/components/Expander.svelte'; - import { getFilterContext } from '$lib/Filter'; + import type { FilterType } from '$lib/Filter.svelte'; + import type { Snippet } from 'svelte'; - const filter = getFilterContext(); - export let type: 'grid' | 'row' = 'row'; + interface Props { + type?: 'grid' | 'row'; + include?: Snippet<[FilterType]>; + exclude?: Snippet<[FilterType]>; + expanded: boolean; + apply: (params: URLSearchParams) => void; + } + + let { type = 'row', include, exclude, expanded: initialExpanded, apply }: Props = $props(); - let exclude = false; + let expanded = $state(initialExpanded); - $: if ($filter.exclude.size > 0) { - exclude = true; + function onsubmit(event: SubmitEvent) { + event.preventDefault(); + apply(page.url.searchParams); } </script> -<form on:submit|preventDefault class="gap-0"> +<form {onsubmit} class="gap-0"> {#if type === 'grid'} <div class="flex flex-col gap-4 px-2 md:grid md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6"> - <slot name="include" /> + {@render include?.('include')} </div> <div class="my-2 flex justify-start"> - <Expander title="Exclude" bind:expanded={exclude} /> + <Expander title="Exclude" bind:expanded /> </div> - {#if exclude} + {#if expanded} <div class="flex flex-col gap-4 bg-rose-950/50 p-2 md:grid md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6" > - <slot name="exclude" /> + {@render exclude?.('exclude')} </div> {/if} {:else} @@ -32,10 +42,10 @@ class="flex flex-wrap justify-center gap-2 [&>*]:basis-full xl:[&>*]:basis-1/3 2xl:[&>*]:basis-1/5" > <div class="p-2"> - <slot name="include" /> + {@render include?.('include')} </div> <div class="bg-rose-950/50 p-2"> - <slot name="exclude" /> + {@render exclude?.('exclude')} </div> </div> {/if} diff --git a/frontend/src/lib/filter/components/TagFilterGroup.svelte b/frontend/src/lib/filter/components/TagFilterGroup.svelte deleted file mode 100644 index 83b6997..0000000 --- a/frontend/src/lib/filter/components/TagFilterGroup.svelte +++ /dev/null @@ -1,14 +0,0 @@ -<script lang="ts"> - import { TagFilterControls } from '$lib/Filter'; - import type { ListItem } from '$lib/Utils'; - import { setContext } from 'svelte'; - import Filter from './Filter.svelte'; - - export let namespaces: ListItem[] | undefined; - export let controls: TagFilterControls; - export let type: 'include' | 'exclude'; - - setContext('filter-type', type); -</script> - -<Filter title="Namespaces" options={namespaces} bind:filter={controls.namespaces} /> diff --git a/frontend/src/lib/forms/ArtistForm.svelte b/frontend/src/lib/forms/ArtistForm.svelte index 7df5e8b..663c3ae 100644 --- a/frontend/src/lib/forms/ArtistForm.svelte +++ b/frontend/src/lib/forms/ArtistForm.svelte @@ -1,25 +1,29 @@ <script lang="ts"> - import { type ArtistInput } from '$gql/Mutations'; - import { type OmitIdentifiers } from '$gql/Utils'; - import { type Artist } from '$gql/graphql'; - import Labelled from '$lib/components/Labelled.svelte'; - import { createEventDispatcher } from 'svelte'; + import type { AddArtistInput, Artist } from '$gql/graphql'; + import SubmitButton from '$lib/components/SubmitButton.svelte'; + import { itemPending, type FormProps } from '$lib/Form'; - const dispatch = createEventDispatcher<{ submit: ArtistInput }>(); + let { initial, submit, children }: FormProps<Artist, AddArtistInput> = $props(); - export let artist: OmitIdentifiers<Artist>; + let input = $state(initial); + let pending = $derived(input.name.length > 0 && itemPending(initial, input)); - function submit() { - dispatch('submit', { name: artist.name }); + function onsubmit(event: SubmitEvent) { + event.preventDefault(); + + submit({ ...input }); } </script> -<form on:submit|preventDefault={submit}> +<form {onsubmit}> <div class="grid-labels"> - <Labelled label="Name" let:id> - <!-- svelte-ignore a11y-autofocus --> - <input autofocus required {id} bind:value={artist.name} /> - </Labelled> + <label class="self-center" for="name">Name</label> + <!-- svelte-ignore a11y_autofocus --> + <input autofocus required id="name" bind:value={input.name} /> + </div> + <div class="flex gap-4"> + {@render children?.()} + <div class="grow"></div> + <SubmitButton {pending} /> </div> - <slot /> </form> diff --git a/frontend/src/lib/forms/CharacterForm.svelte b/frontend/src/lib/forms/CharacterForm.svelte index 4cec37c..23b3ef7 100644 --- a/frontend/src/lib/forms/CharacterForm.svelte +++ b/frontend/src/lib/forms/CharacterForm.svelte @@ -1,25 +1,29 @@ <script lang="ts"> - import { type CharacterInput } from '$gql/Mutations'; - import { type OmitIdentifiers } from '$gql/Utils'; - import { type Character } from '$gql/graphql'; - import Labelled from '$lib/components/Labelled.svelte'; - import { createEventDispatcher } from 'svelte'; + import type { AddCharacterInput, Character } from '$gql/graphql'; + import SubmitButton from '$lib/components/SubmitButton.svelte'; + import { itemPending, type FormProps } from '$lib/Form'; - const dispatch = createEventDispatcher<{ submit: CharacterInput }>(); + let { initial, submit, children }: FormProps<Character, AddCharacterInput> = $props(); - export let character: OmitIdentifiers<Character>; + let input = $state(initial); + let pending = $derived(input.name.length > 0 && itemPending(initial, input)); - function submit() { - dispatch('submit', { name: character.name }); + function onsubmit(event: SubmitEvent) { + event.preventDefault(); + + submit({ ...input }); } </script> -<form on:submit|preventDefault={submit}> +<form {onsubmit}> <div class="grid-labels"> - <Labelled label="Name" let:id> - <!-- svelte-ignore a11y-autofocus --> - <input autofocus required {id} bind:value={character.name} /> - </Labelled> + <label class="self-center" for="name">Name</label> + <!-- svelte-ignore a11y_autofocus --> + <input autofocus required id="name" bind:value={input.name} /> + </div> + <div class="flex gap-4"> + {@render children?.()} + <div class="grow"></div> + <SubmitButton {pending} /> </div> - <slot /> </form> diff --git a/frontend/src/lib/forms/CircleForm.svelte b/frontend/src/lib/forms/CircleForm.svelte index b71256c..ba6013a 100644 --- a/frontend/src/lib/forms/CircleForm.svelte +++ b/frontend/src/lib/forms/CircleForm.svelte @@ -1,25 +1,29 @@ <script lang="ts"> - import { type CircleInput } from '$gql/Mutations'; - import { type OmitIdentifiers } from '$gql/Utils'; - import { type Circle } from '$gql/graphql'; - import Labelled from '$lib/components/Labelled.svelte'; - import { createEventDispatcher } from 'svelte'; + import type { AddCircleInput, Circle } from '$gql/graphql'; + import SubmitButton from '$lib/components/SubmitButton.svelte'; + import { itemPending, type FormProps } from '$lib/Form'; - const dispatch = createEventDispatcher<{ submit: CircleInput }>(); + let { initial, submit, children }: FormProps<Circle, AddCircleInput> = $props(); - export let circle: OmitIdentifiers<Circle>; + let input = $state(initial); + let pending = $derived(input.name.length > 0 && itemPending(initial, input)); - function submit() { - dispatch('submit', { name: circle.name }); + function onsubmit(event: SubmitEvent) { + event.preventDefault(); + + submit({ ...input }); } </script> -<form on:submit|preventDefault={submit}> +<form {onsubmit}> <div class="grid-labels"> - <Labelled label="Name" let:id> - <!-- svelte-ignore a11y-autofocus --> - <input required autofocus {id} bind:value={circle.name} /> - </Labelled> + <label class="self-center" for="name">Name</label> + <!-- svelte-ignore a11y_autofocus --> + <input autofocus required id="name" bind:value={input.name} /> + </div> + <div class="flex gap-4"> + {@render children?.()} + <div class="grow"></div> + <SubmitButton {pending} /> </div> - <slot /> </form> diff --git a/frontend/src/lib/forms/ComicForm.svelte b/frontend/src/lib/forms/ComicForm.svelte index 74051c8..adc6a34 100644 --- a/frontend/src/lib/forms/ComicForm.svelte +++ b/frontend/src/lib/forms/ComicForm.svelte @@ -3,98 +3,113 @@ import { type OmitIdentifiers } from '$gql/Utils'; import type { FullComicFragment, UpdateComicInput } from '$gql/graphql'; import { categories, censorships, directions, languages, layouts, ratings } from '$lib/Enums'; - import Labelled from '$lib/components/Labelled.svelte'; import LabelledBlock from '$lib/components/LabelledBlock.svelte'; import Select from '$lib/components/Select.svelte'; import { getContextClient } from '@urql/svelte'; - import { createEventDispatcher } from 'svelte'; + import { type Snippet } from 'svelte'; const client = getContextClient(); - const dispatch = createEventDispatcher<{ submit: UpdateComicInput }>(); - - export let comic: OmitIdentifiers<FullComicFragment>; - - $: tagsQuery = comicTagList(client); - $: artistsQuery = artistList(client); - $: charactersQuery = characterList(client); - $: circlesQuery = circleList(client); - $: worldsQuery = worldList(client); - - $: tags = $tagsQuery.data?.comicTags.edges; - $: artists = $artistsQuery.data?.artists.edges; - $: characters = $charactersQuery.data?.characters.edges; - $: circles = $circlesQuery.data?.circles.edges; - $: worlds = $worldsQuery.data?.worlds.edges; - - function submit() { - dispatch('submit', { - direction: comic.direction, - layout: comic.layout, - rating: comic.rating, - category: comic.category, - censorship: comic.censorship, - title: comic.title, - originalTitle: comic.originalTitle, - url: comic.url, - date: comic.date === '' ? null : comic.date, - language: comic.language, - tags: { ids: comic.tags.map((t) => t.id) }, - artists: { ids: comic.artists.map((a) => a.id) }, - characters: { ids: comic.characters.map((c) => c.id) }, - circles: { ids: comic.circles.map((c) => c.id) }, - worlds: { ids: comic.worlds.map((w) => w.id) } + + interface Props { + input: OmitIdentifiers<FullComicFragment>; + submit: (input: UpdateComicInput) => void; + children?: Snippet; + } + + let { input = $bindable(), submit, children }: Props = $props(); + + let tagsQuery = $derived(comicTagList(client)); + let artistsQuery = $derived(artistList(client)); + let charactersQuery = $derived(characterList(client)); + let circlesQuery = $derived(circleList(client)); + let worldsQuery = $derived(worldList(client)); + + let tags = $derived($tagsQuery.data?.comicTags.edges); + let artists = $derived($artistsQuery.data?.artists.edges); + let characters = $derived($charactersQuery.data?.characters.edges); + let circles = $derived($circlesQuery.data?.circles.edges); + let worlds = $derived($worldsQuery.data?.worlds.edges); + + function onsubmit(event: SubmitEvent) { + event.preventDefault(); + + submit({ + direction: input.direction, + layout: input.layout, + rating: input.rating, + category: input.category, + censorship: input.censorship, + title: input.title, + originalTitle: input.originalTitle, + url: input.url, + date: input.date === '' ? null : input.date, + language: input.language, + tags: { ids: input.tags.map((t) => t.id) }, + artists: { ids: input.artists.map((a) => a.id) }, + characters: { ids: input.characters.map((c) => c.id) }, + circles: { ids: input.circles.map((c) => c.id) }, + worlds: { ids: input.worlds.map((w) => w.id) } }); } </script> -<form on:submit|preventDefault={submit}> +<form {onsubmit}> <div class="grid-labels"> - <Labelled label="Title" let:id> - <input required {id} bind:value={comic.title} title={comic.title} /> - </Labelled> - <Labelled label="Original Title" let:id> - <input {id} bind:value={comic.originalTitle} title={comic.originalTitle} /> - </Labelled> - <Labelled label="URL" let:id> - <input {id} bind:value={comic.url} /> - </Labelled> - <Labelled label="Date" let:id> - <input {id} type="date" bind:value={comic.date} pattern={'d{4}-d{2}-d{2}'} /> - </Labelled> - <Labelled label="Category" let:id> - <Select {id} options={categories} bind:value={comic.category} /> - </Labelled> - <Labelled label="Rating" let:id> - <Select {id} options={ratings} bind:value={comic.rating} /> - </Labelled> - <Labelled label="Censorship" let:id> - <Select {id} options={censorships} bind:value={comic.censorship} /> - </Labelled> - <Labelled label="Language" let:id> - <Select {id} options={languages} bind:value={comic.language} /> - </Labelled> - <Labelled label="Direction" let:id> - <Select {id} options={directions} bind:value={comic.direction} /> - </Labelled> - <Labelled label="Layout" let:id> - <Select {id} options={layouts} bind:value={comic.layout} /> - </Labelled> + <label class="self-center" for="title">Title</label> + <input required id="title" bind:value={input.title} title={input.title} /> + + <label class="self-center" for="original-title">Original Title</label> + <input id="original-title" bind:value={input.originalTitle} title={input.originalTitle} /> + + <label class="self-center" for="url">URL</label> + <input id="url" bind:value={input.url} /> + + <label class="self-center" for="date">Date</label> + <input id="date" type="date" bind:value={input.date} pattern={'d{4}-d{2}-d{2}'} /> + + <label class="self-center" for="category">Category</label> + <Select id="category" options={categories} bind:value={input.category} /> + + <label class="self-center" for="rating">Rating</label> + <Select id="rating" options={ratings} bind:value={input.rating} /> + + <label class="self-center" for="censorship">Censorship</label> + <Select id="censorship" options={censorships} bind:value={input.censorship} /> + + <label class="self-center" for="language">Language</label> + <Select id="language" options={languages} bind:value={input.language} /> + + <label class="self-center" for="direction">Direction</label> + <Select id="direction" options={directions} bind:value={input.direction} /> + + <label class="self-center" for="layout">Layout</label> + <Select id="layout" options={layouts} bind:value={input.layout} /> </div> - <LabelledBlock label="Artists" let:id> - <Select multi object {id} options={artists} bind:value={comic.artists} /> + <LabelledBlock label="Artists"> + {#snippet children({ id })} + <Select multi object {id} options={artists} bind:value={input.artists} /> + {/snippet} </LabelledBlock> - <LabelledBlock label="Circles" let:id> - <Select multi object {id} options={circles} bind:value={comic.circles} /> + <LabelledBlock label="Circles"> + {#snippet children({ id })} + <Select multi object {id} options={circles} bind:value={input.circles} /> + {/snippet} </LabelledBlock> - <LabelledBlock label="Characters" let:id> - <Select multi object {id} options={characters} bind:value={comic.characters} /> + <LabelledBlock label="Characters"> + {#snippet children({ id })} + <Select multi object {id} options={characters} bind:value={input.characters} /> + {/snippet} </LabelledBlock> - <LabelledBlock label="Worlds" let:id> - <Select multi object {id} options={worlds} bind:value={comic.worlds} /> + <LabelledBlock label="Worlds"> + {#snippet children({ id })} + <Select multi object {id} options={worlds} bind:value={input.worlds} /> + {/snippet} </LabelledBlock> - <LabelledBlock label="Tags" let:id> - <Select multi object {id} options={tags} bind:value={comic.tags} /> + <LabelledBlock label="Tags"> + {#snippet children({ id })} + <Select multi object {id} options={tags} bind:value={input.tags} /> + {/snippet} </LabelledBlock> - <slot /> + {@render children?.()} </form> diff --git a/frontend/src/lib/forms/NamespaceForm.svelte b/frontend/src/lib/forms/NamespaceForm.svelte index c05b6d8..3631d84 100644 --- a/frontend/src/lib/forms/NamespaceForm.svelte +++ b/frontend/src/lib/forms/NamespaceForm.svelte @@ -1,28 +1,31 @@ <script lang="ts"> - import { type NamespaceInput } from '$gql/Mutations'; - import { type OmitIdentifiers } from '$gql/Utils'; - import { type Namespace } from '$gql/graphql'; - import Labelled from '$lib/components/Labelled.svelte'; - import { createEventDispatcher } from 'svelte'; + import type { AddNamespaceInput, Namespace } from '$gql/graphql'; + import SubmitButton from '$lib/components/SubmitButton.svelte'; + import { namespacePending, type FormProps } from '$lib/Form'; - const dispatch = createEventDispatcher<{ submit: NamespaceInput }>(); + let { initial, submit, children }: FormProps<Namespace, AddNamespaceInput> = $props(); - export let namespace: OmitIdentifiers<Namespace>; + let input = $state(initial); + let pending = $derived(input.name.length > 0 && namespacePending(initial, input)); - function submit() { - dispatch('submit', { name: namespace.name, sortName: namespace.sortName }); + function onsubmit(event: SubmitEvent) { + event.preventDefault(); + + submit({ ...input }); } </script> -<form on:submit|preventDefault={submit}> +<form {onsubmit}> <div class="grid-labels"> - <Labelled label="Name" let:id> - <!-- svelte-ignore a11y-autofocus --> - <input required autofocus {id} bind:value={namespace.name} /> - </Labelled> - <Labelled label="Sort name" let:id> - <input {id} bind:value={namespace.sortName} /> - </Labelled> + <label class="self-center" for="name">Name</label> + <!-- svelte-ignore a11y_autofocus --> + <input autofocus required id="name" bind:value={input.name} /> + <label class="self-center" for="sort-name">Sort name</label> + <input id="name" bind:value={input.sortName} /> + </div> + <div class="flex gap-4"> + {@render children?.()} + <div class="grow"></div> + <SubmitButton {pending} /> </div> - <slot /> </form> diff --git a/frontend/src/lib/forms/TagForm.svelte b/frontend/src/lib/forms/TagForm.svelte index 6cc2227..2789587 100644 --- a/frontend/src/lib/forms/TagForm.svelte +++ b/frontend/src/lib/forms/TagForm.svelte @@ -1,42 +1,41 @@ <script lang="ts"> - import type { TagInput } from '$gql/Mutations'; import { namespaceList } from '$gql/Queries'; - import type { OmitIdentifiers } from '$gql/Utils'; - import type { FullTag } from '$gql/graphql'; - import Labelled from '$lib/components/Labelled.svelte'; + import type { AddTagInput, FullTag } from '$gql/graphql'; + import { tagPending, type FormProps } from '$lib/Form'; import Select from '$lib/components/Select.svelte'; + import SubmitButton from '$lib/components/SubmitButton.svelte'; import { getContextClient } from '@urql/svelte'; - import { createEventDispatcher } from 'svelte'; - const client = getContextClient(); - const dispatch = createEventDispatcher<{ submit: TagInput }>(); + let { initial, submit, children }: FormProps<FullTag, AddTagInput> = $props(); - export let tag: OmitIdentifiers<FullTag>; + let input = $state(initial); + let pending = $derived(input.name.length > 0 && tagPending(initial, input)); - $: namespaceQuery = namespaceList(client); - $: namespaces = $namespaceQuery.data?.namespaces.edges; + let namespaceQuery = $derived(namespaceList(getContextClient())); + let namespaces = $derived($namespaceQuery.data?.namespaces.edges); - function submit() { - dispatch('submit', { - name: tag.name, - description: tag.description, - namespaces: { ids: tag.namespaces.map((n) => n.id) } - }); + function onsubmit(event: SubmitEvent) { + event.preventDefault(); + + submit({ ...input, namespaces: { ids: input.namespaces.map((n) => n.id) } }); } </script> -<form on:submit|preventDefault={submit}> +<form {onsubmit}> <div class="grid-labels"> - <Labelled label="Name" let:id> - <!-- svelte-ignore a11y-autofocus --> - <input autofocus required {id} bind:value={tag.name} /> - </Labelled> - <Labelled label="Description" let:id> - <textarea rows={3} {id} bind:value={tag.description} /> - </Labelled> - <Labelled label="Namespaces" let:id> - <Select multi object {id} options={namespaces} bind:value={tag.namespaces} /> - </Labelled> + <label class="self-center" for="name">Name</label> + <!-- svelte-ignore a11y_autofocus --> + <input autofocus required id="name" bind:value={input.name} /> + + <label class="self-center" for="description">Description</label> + <textarea rows={3} id="description" bind:value={input.description}></textarea> + + <label class="self-center" for="namespaces">Namespaces</label> + <Select multi object id="namespaces" options={namespaces} bind:value={input.namespaces} /> + </div> + <div class="flex gap-4"> + {@render children?.()} + <div class="grow"></div> + <SubmitButton {pending} /> </div> - <slot /> </form> diff --git a/frontend/src/lib/forms/WorldForm.svelte b/frontend/src/lib/forms/WorldForm.svelte index 103dd5b..e6b821f 100644 --- a/frontend/src/lib/forms/WorldForm.svelte +++ b/frontend/src/lib/forms/WorldForm.svelte @@ -1,25 +1,29 @@ <script lang="ts"> - import { type WorldInput } from '$gql/Mutations'; - import { type OmitIdentifiers } from '$gql/Utils'; - import { type World } from '$gql/graphql'; - import Labelled from '$lib/components/Labelled.svelte'; - import { createEventDispatcher } from 'svelte'; + import type { AddWorldInput, World } from '$gql/graphql'; + import SubmitButton from '$lib/components/SubmitButton.svelte'; + import { itemPending, type FormProps } from '$lib/Form'; - const dispatch = createEventDispatcher<{ submit: WorldInput }>(); + let { initial, submit, children }: FormProps<World, AddWorldInput> = $props(); - export let world: OmitIdentifiers<World>; + let input = $state(initial); + let pending = $derived(input.name.length > 0 && itemPending(initial, input)); - function submit() { - dispatch('submit', { name: world.name }); + function onsubmit(event: SubmitEvent) { + event.preventDefault(); + + submit({ ...input }); } </script> -<form on:submit|preventDefault={submit}> +<form {onsubmit}> <div class="grid-labels"> - <Labelled label="Name" let:id> - <!-- svelte-ignore a11y-autofocus --> - <input autofocus required {id} bind:value={world.name} /> - </Labelled> + <label class="self-center" for="name">Name</label> + <!-- svelte-ignore a11y_autofocus --> + <input autofocus required id="name" bind:value={input.name} /> + </div> + <div class="flex gap-4"> + {@render children?.()} + <div class="grow"></div> + <SubmitButton {pending} /> </div> - <slot /> </form> diff --git a/frontend/src/lib/gallery/Gallery.svelte b/frontend/src/lib/gallery/Gallery.svelte index 964c677..0480026 100644 --- a/frontend/src/lib/gallery/Gallery.svelte +++ b/frontend/src/lib/gallery/Gallery.svelte @@ -2,12 +2,18 @@ import type { PageFragment } from '$gql/graphql'; import GalleryPage from './GalleryPage.svelte'; - export let pages: PageFragment[]; + interface Props { + pages: PageFragment[]; + open: (page: number) => void; + updateCover: (page: number) => void; + } + + let { pages, open, updateCover }: Props = $props(); </script> <div class="max-h-full gap-2 overflow-auto p-1 pr-3" tabindex="-1"> {#each pages as page, index} - <GalleryPage {page} {index} on:open on:cover /> + <GalleryPage {page} {index} {open} {updateCover} /> {/each} </div> diff --git a/frontend/src/lib/gallery/GalleryPage.svelte b/frontend/src/lib/gallery/GalleryPage.svelte index f40b889..3169d6d 100644 --- a/frontend/src/lib/gallery/GalleryPage.svelte +++ b/frontend/src/lib/gallery/GalleryPage.svelte @@ -1,56 +1,55 @@ <script lang="ts"> import type { PageFragment } from '$gql/graphql'; - import { getSelectionContext } from '$lib/Selection'; + import { getSelectionContext } from '$lib/selection/Selection.svelte'; import SelectionOverlay from '$lib/selection/SelectionOverlay.svelte'; import { src } from '$lib/Utils'; - import { createEventDispatcher } from 'svelte'; - export let page: PageFragment; - export let index: number; - - const selection = getSelectionContext<PageFragment>(); + interface Props { + page: PageFragment; + index: number; + open: (page: number) => void; + updateCover: (page: number) => void; + } - let span: 'single' | 'double' | 'triple'; + let { page, index, open, updateCover }: Props = $props(); - $: page.image.aspectRatio, updateSpan(); + const selection = getSelectionContext<PageFragment>(); - function updateSpan() { + let span: 'single' | 'double' | 'triple' = $derived.by(() => { const aspectRatio = page.image.aspectRatio; if (aspectRatio <= 1) { - span = 'single'; + return 'single'; } else if (aspectRatio > 1 && aspectRatio <= 2) { - span = 'double'; - } else if (aspectRatio > 2) { - span = 'triple'; + return 'double'; + } else { + return 'triple'; } - } - - const dispatch = createEventDispatcher<{ open: number; cover: number }>(); + }); function press(event: MouseEvent | KeyboardEvent) { if (event instanceof KeyboardEvent && event.key !== 'Enter') { return; } - if ($selection.active) { + if (selection.active) { if (event.ctrlKey) { - dispatch('open', index); + open(index); } else if (selectable) { - $selection = $selection.update(index, event.shiftKey); + selection.update(index, event.shiftKey); } } else if (event.ctrlKey) { - dispatch('cover', page.id); + updateCover(page.id); } else { - dispatch('open', index); + open(index); } event.preventDefault(); } - $: selectable = $selection.selectable(page); - $: dim = $selection.active && !selectable; - $: selected = $selection.contains(page.id); + let selectable = $derived(selection.selectable(page)); + let dim = $derived(selection.active && !selectable); + let selected = $derived(selection.contains(page.id)); </script> <div @@ -58,8 +57,8 @@ role="button" tabindex="0" class="{span} focus-thick focus-blue relative overflow-hidden rounded" - on:click={press} - on:keydown={press} + onclick={press} + onkeydown={press} > <SelectionOverlay position="top" {selected} /> <img diff --git a/frontend/src/lib/icons/Bookmark.svelte b/frontend/src/lib/icons/Bookmark.svelte index 6f8e192..21b54ed 100644 --- a/frontend/src/lib/icons/Bookmark.svelte +++ b/frontend/src/lib/icons/Bookmark.svelte @@ -1,10 +1,15 @@ <script lang="ts"> - export let bookmarked: boolean | undefined = undefined; - export let hoverable = false; + interface Props { + bookmarked?: boolean; + hoverable?: boolean; + } + + let { bookmarked, hoverable = false }: Props = $props(); </script> {#if bookmarked} - <span class:hoverable class="icon-gray icon-base icon-[material-symbols--bookmark]" /> + <span class:hoverable class="icon-gray icon-base icon-[material-symbols--bookmark]"></span> {:else} - <span class:hoverable class="icon-gray icon-base dim icon-[material-symbols--bookmark-outline]" /> + <span class:hoverable class="icon-gray icon-base dim icon-[material-symbols--bookmark-outline]" + ></span> {/if} diff --git a/frontend/src/lib/icons/Female.svelte b/frontend/src/lib/icons/Female.svelte index c772a6a..7bc422b 100644 --- a/frontend/src/lib/icons/Female.svelte +++ b/frontend/src/lib/icons/Female.svelte @@ -1 +1 @@ -<span class="icon-xs icon-[material-symbols--female] -mx-[3px]" /> +<span class="icon-xs icon-[material-symbols--female] -mx-[3px]"></span> diff --git a/frontend/src/lib/icons/Location.svelte b/frontend/src/lib/icons/Location.svelte index e345f83..d785832 100644 --- a/frontend/src/lib/icons/Location.svelte +++ b/frontend/src/lib/icons/Location.svelte @@ -1 +1 @@ -<span class="icon-xs icon-[material-symbols--location-on-outline]" /> +<span class="icon-xs icon-[material-symbols--location-on-outline]"></span> diff --git a/frontend/src/lib/icons/Male.svelte b/frontend/src/lib/icons/Male.svelte index e3578b7..8c72c47 100644 --- a/frontend/src/lib/icons/Male.svelte +++ b/frontend/src/lib/icons/Male.svelte @@ -1 +1 @@ -<span class="icon-xs icon-[material-symbols--male] -mx-px" /> +<span class="icon-xs icon-[material-symbols--male] -mx-px"></span> diff --git a/frontend/src/lib/icons/Organized.svelte b/frontend/src/lib/icons/Organized.svelte index 66b5b00..ff177fa 100644 --- a/frontend/src/lib/icons/Organized.svelte +++ b/frontend/src/lib/icons/Organized.svelte @@ -1,21 +1,22 @@ <script lang="ts"> - export let organized: boolean | undefined = undefined; - export let hoverable = false; - export let tristate = false; - export let dim = false; + interface Props { + organized?: boolean; + hoverable?: boolean; + tristate?: boolean; + dim?: boolean; + } + + let { organized, hoverable = false, tristate = false, dim = false }: Props = $props(); </script> {#if organized} - <span class:hoverable class="icon-gray icon-base icon-[material-symbols--check-circle]" /> + <span class:hoverable class="icon-gray icon-base icon-[material-symbols--check-circle]"></span> {:else if organized === undefined || !tristate} <span class:hoverable class="icon-gray dim icon-base icon-[material-symbols--check-circle-outline]" - /> + ></span> {:else} - <span - class:hoverable - class:dim - class="icon-gray icon-base icon-[material-symbols--unpublished]" - /> + <span class:hoverable class:dim class="icon-gray icon-base icon-[material-symbols--unpublished]" + ></span> {/if} diff --git a/frontend/src/lib/icons/Star.svelte b/frontend/src/lib/icons/Star.svelte index 7613c55..bd8af67 100644 --- a/frontend/src/lib/icons/Star.svelte +++ b/frontend/src/lib/icons/Star.svelte @@ -1,17 +1,22 @@ <script lang="ts"> - export let large = false; - export let favourite: boolean | undefined = undefined; - export let hoverable = false; + interface Props { + large?: boolean; + favourite?: boolean; + hoverable?: boolean; + } + + let { large = false, favourite, hoverable = false }: Props = $props(); </script> {#if favourite} - <span class:hoverable class:large class="icon-yellow icon-[material-symbols--star-rounded]" /> + <span class:hoverable class:large class="icon-yellow icon-[material-symbols--star-rounded]" + ></span> {:else} <span class:hoverable class:large class="icon-yellow dim icon-[material-symbols--star-outline-rounded]" - /> + ></span> {/if} <style lang="postcss"> diff --git a/frontend/src/lib/icons/Transgender.svelte b/frontend/src/lib/icons/Transgender.svelte index 7d9adc6..fa7d38b 100644 --- a/frontend/src/lib/icons/Transgender.svelte +++ b/frontend/src/lib/icons/Transgender.svelte @@ -1 +1 @@ -<span class="icon-xs icon-[material-symbols--transgender]" /> +<span class="icon-xs icon-[material-symbols--transgender]"></span> diff --git a/frontend/src/lib/navigation/Link.svelte b/frontend/src/lib/navigation/Link.svelte index be09a36..9c7e218 100644 --- a/frontend/src/lib/navigation/Link.svelte +++ b/frontend/src/lib/navigation/Link.svelte @@ -1,20 +1,31 @@ <script lang="ts"> - import { page } from '$app/stores'; + import { page } from '$app/state'; import { accelerator, type Shortcut } from '$lib/Shortcuts'; - import type { HTMLAttributeAnchorTarget } from 'svelte/elements'; + import type { Snippet } from 'svelte'; + import type { HTMLAnchorAttributes } from 'svelte/elements'; - export let href: string; - export let title: string; - export let accel: Shortcut; - export let matchExact = false; - export let target: HTMLAttributeAnchorTarget | undefined = undefined; - $: active = matchExact ? $page.url.pathname === href : $page.url.pathname.startsWith(href); + interface Props extends Pick<HTMLAnchorAttributes, 'title' | 'target'> { + href: string; + accel: Shortcut; + matchExact?: boolean; + children?: Snippet; + } + + let { href, title, accel, matchExact = false, target, children }: Props = $props(); + + let active = $derived.by(() => { + if (matchExact) { + return page.url.pathname === href; + } else { + return page.url.pathname.startsWith(href); + } + }); </script> <li class:active class="items-center hover:bg-indigo-700 [&.active]:bg-indigo-700"> <a class="focus-background flex items-center" {target} {title} {href} use:accelerator={accel}> <div class="flex p-3"> - <slot /> + {@render children?.()} </div> </a> </li> diff --git a/frontend/src/lib/navigation/Navigation.svelte b/frontend/src/lib/navigation/Navigation.svelte index 76096c8..6734272 100644 --- a/frontend/src/lib/navigation/Navigation.svelte +++ b/frontend/src/lib/navigation/Navigation.svelte @@ -1,5 +1,11 @@ +<script lang="ts"> + import type { Snippet } from 'svelte'; + + let { children }: { children?: Snippet } = $props(); +</script> + <nav> <ul class="flex h-full flex-col bg-slate-700/70 font-medium"> - <slot /> + {@render children?.()} </ul> </nav> diff --git a/frontend/src/lib/pagination/Pagination.svelte b/frontend/src/lib/pagination/Pagination.svelte index 51612f4..fc2935c 100644 --- a/frontend/src/lib/pagination/Pagination.svelte +++ b/frontend/src/lib/pagination/Pagination.svelte @@ -1,45 +1,52 @@ <script lang="ts"> - import { getPaginationContext } from '$lib/Pagination'; + import type { PaginationData } from '$lib/Navigation'; import Target from './Target.svelte'; - const pagination = getPaginationContext(); - export let context = 2; + interface Props { + context?: number; + pagination: PaginationData; + total: number; + } - $: totalPages = Math.ceil($pagination.total / $pagination.items); - $: rightBoundary = $pagination.page - context; - $: leftBoundary = $pagination.page + context; + let { context = 2, pagination, total }: Props = $props(); - $: shiftRight = leftBoundary - totalPages; - $: shiftLeft = 1 - rightBoundary; + let totalPages = $derived(Math.ceil(total / pagination.items)); + let rightBoundary = $derived(pagination.page - context); + let leftBoundary = $derived(pagination.page + context); - $: containedLeft = leftBoundary <= totalPages; - $: containedRight = rightBoundary > 0; + let shiftRight = $derived(leftBoundary - totalPages); + let shiftLeft = $derived(1 - rightBoundary); - $: start = Math.max(1, containedLeft ? rightBoundary : rightBoundary - shiftRight); - $: end = Math.min(totalPages, containedRight ? leftBoundary : leftBoundary + shiftLeft); + let containedLeft = $derived(leftBoundary <= totalPages); + let containedRight = $derived(rightBoundary > 0); - $: leftmost = $pagination.page <= 1; - $: rightmost = $pagination.page >= totalPages; + let start = $derived(Math.max(1, containedLeft ? rightBoundary : rightBoundary - shiftRight)); + let end = $derived( + Math.min(totalPages, containedRight ? leftBoundary : leftBoundary + shiftLeft) + ); + + let leftmost = $derived(pagination.page <= 1); + let rightmost = $derived(pagination.page >= totalPages); </script> {#if totalPages > 1} <div class="flex justify-center gap-2"> - <Target disabled={leftmost} page={1}> - <span class="icon-base icon-[material-symbols--keyboard-double-arrow-left]" /> + <Target disabled={leftmost} target={1}> + <span class="icon-base icon-[material-symbols--keyboard-double-arrow-left]"></span> </Target> - <Target disabled={leftmost} page={$pagination.page - 1}> - <span class="icon-base icon-[material-symbols--keyboard-arrow-left]" /> + <Target disabled={leftmost} target={pagination.page - 1}> + <span class="icon-base icon-[material-symbols--keyboard-arrow-left]"></span> </Target> - {#each Array.from({ length: end + 1 - start }, (_, i) => i + start) as page} - <Target active={$pagination.page === page} {page}> - <p>{page.toString()}</p> + {#each Array.from({ length: end + 1 - start }, (_, i) => i + start) as target} + <Target active={pagination.page === target} {target}> + <p>{target.toString()}</p> </Target> {/each} - <Target disabled={rightmost} page={$pagination.page + 1}> - <span class="icon-base icon-[material-symbols--keyboard-arrow-right]" /> + <Target disabled={rightmost} target={pagination.page + 1}> + <span class="icon-base icon-[material-symbols--keyboard-arrow-right]"></span> </Target> - <Target disabled={rightmost} page={totalPages}> - <span class="icon-base icon-[material-symbols--keyboard-double-arrow-right]" /> + <Target disabled={rightmost} target={totalPages}> + <span class="icon-base icon-[material-symbols--keyboard-double-arrow-right]"></span> </Target> </div> {/if} diff --git a/frontend/src/lib/pagination/Target.svelte b/frontend/src/lib/pagination/Target.svelte index 9044bb9..6cbacbb 100644 --- a/frontend/src/lib/pagination/Target.svelte +++ b/frontend/src/lib/pagination/Target.svelte @@ -1,21 +1,28 @@ <script lang="ts"> - import { page as pageStore } from '$app/stores'; + import { page } from '$app/state'; import { navigate } from '$lib/Navigation'; + import type { Snippet } from 'svelte'; - export let active = false; + interface Props { + active?: boolean; + disabled?: boolean; + target: number; + children?: Snippet; + } - export let disabled = false; - export let page: number; + let { active = false, disabled = false, target, children }: Props = $props(); + + function onclick() { + navigate({ pagination: { page: target } }, page.url.searchParams); + } </script> <button - on:click={() => { - navigate({ pagination: { page: page } }, $pageStore.url.searchParams); - }} + {onclick} class:bg-slate-700={active} class:bg-slate-800={!active} class="flex h-8 w-8 items-center justify-center rounded-sm p-0 text-base hover:text-white disabled:text-slate-600" {disabled} > - <slot /> + {@render children?.()} </button> diff --git a/frontend/src/lib/pills/AssociationPill.svelte b/frontend/src/lib/pills/AssociationPill.svelte index 85dbe39..ffbc8c4 100644 --- a/frontend/src/lib/pills/AssociationPill.svelte +++ b/frontend/src/lib/pills/AssociationPill.svelte @@ -3,12 +3,13 @@ type Association = 'artist' | 'circle' | 'world' | 'character'; - export let name: string; - export let type: Association; + let { name, type }: { name: string; type: Association } = $props(); </script> <Pill {name}> - <span class={`${type} icon-xs`} slot="icon" /> + {#snippet icon()} + <span class={`${type} icon-xs`}></span> + {/snippet} </Pill> <style lang="postcss"> diff --git a/frontend/src/lib/pills/ComicPills.svelte b/frontend/src/lib/pills/ComicPills.svelte index 671bbf2..45c42fd 100644 --- a/frontend/src/lib/pills/ComicPills.svelte +++ b/frontend/src/lib/pills/ComicPills.svelte @@ -3,7 +3,7 @@ import AssociationPill from '$lib/pills/AssociationPill.svelte'; import TagPill from '$lib/pills/TagPill.svelte'; - export let comic: ComicFragment; + let { comic }: { comic: ComicFragment } = $props(); </script> <div class="flex flex-col gap-1"> diff --git a/frontend/src/lib/pills/Pill.svelte b/frontend/src/lib/pills/Pill.svelte index 7aa9670..24d617d 100644 --- a/frontend/src/lib/pills/Pill.svelte +++ b/frontend/src/lib/pills/Pill.svelte @@ -1,15 +1,22 @@ -<script lang="ts" context="module"> +<script lang="ts" module> export type PillColour = 'pink' | 'blue' | 'violet' | 'amber' | 'zinc' | 'sky'; </script> <script lang="ts"> - export let name: string; - export let tooltip: string | null | undefined = undefined; - export let colour: PillColour = 'zinc'; + import type { Snippet } from 'svelte'; + + interface Props { + name: string; + tooltip?: string | null; + colour?: PillColour; + icon?: Snippet; + } + + let { name, tooltip, colour = 'zinc', icon }: Props = $props(); </script> <div class="flex items-center rounded border p-0.5 {colour}" title={tooltip}> - <slot name="icon" /> + {@render icon?.()} <span>{name}</span> </div> diff --git a/frontend/src/lib/pills/TagPill.svelte b/frontend/src/lib/pills/TagPill.svelte index 60221bd..92d2a0b 100644 --- a/frontend/src/lib/pills/TagPill.svelte +++ b/frontend/src/lib/pills/TagPill.svelte @@ -3,11 +3,10 @@ import Location from '$lib/icons/Location.svelte'; import Male from '$lib/icons/Male.svelte'; import Transgender from '$lib/icons/Transgender.svelte'; - import { SvelteComponent } from 'svelte'; + import type { Component } from 'svelte'; import Pill, { type PillColour } from './Pill.svelte'; - export let name: string; - export let description: string | undefined | null = undefined; + let { name, description }: { name: string; description?: string | null } = $props(); let [namespace, tag] = name.split(':'); @@ -20,7 +19,7 @@ rest: 'zinc' }; - const icons: Record<string, typeof SvelteComponent<Record<string, unknown>>> = { + const icons: Record<string, Component> = { female: Female, male: Male, trans: Transgender, @@ -28,7 +27,7 @@ }; const colour = styles[namespace] ?? styles.rest; - const icon = icons[namespace]; + const Icon = icons[namespace]; function formatTooltip() { return [name, description].filter((v) => v).join('\n\n'); @@ -36,5 +35,9 @@ </script> <Pill name={tag} tooltip={formatTooltip()} {colour}> - <svelte:component this={icon} slot="icon" /> + {#snippet icon()} + {#if Icon} + <Icon /> + {/if} + {/snippet} </Pill> diff --git a/frontend/src/lib/reader/PageView.svelte b/frontend/src/lib/reader/PageView.svelte index 08764b7..81fbb97 100644 --- a/frontend/src/lib/reader/PageView.svelte +++ b/frontend/src/lib/reader/PageView.svelte @@ -1,8 +1,8 @@ <script lang="ts"> import { Direction, Layout, type PageFragment } from '$gql/graphql'; - import { getReaderContext, partition, type Chunk } from '$lib/Reader'; import { binds } from '$lib/Shortcuts'; import { src } from '$lib/Utils'; + import { getReaderContext, partition, type Chunk } from './Reader.svelte'; import ReaderPage from './ReaderPage.svelte'; const reader = getReaderContext(); @@ -19,14 +19,14 @@ function gotoChunk(to: number) { if (to < 0 || to >= chunks.length) return; - $reader.page = chunks[to].index; + reader.page = chunks[to].index; } function pagesAround(around: number) { const peek = (at: number) => { if (at < 0 || at >= chunks.length) return []; - const pages = [chunks[at].main]; + const pages: PageFragment[] = [chunks[at].main]; if (chunks[at].secondary) { pages.push(chunks[at].secondary); @@ -38,8 +38,8 @@ return [...peek(lookup[around] + 1), ...peek(lookup[around] - 1)]; } - const next = () => gotoChunk(lookup[$reader.page] + 1); - const prev = () => gotoChunk(lookup[$reader.page] - 1); + const next = () => gotoChunk(lookup[reader.page] + 1); + const prev = () => gotoChunk(lookup[reader.page] - 1); const clickLeft = () => (direction === Direction.LeftToRight ? prev() : next()); const clickRight = () => (direction === Direction.RightToLeft ? prev() : next()); @@ -56,8 +56,8 @@ } } - $: [chunks, lookup] = partition($reader.pages, layout); - $: layout, ({ main, secondary } = chunks[lookup[$reader.page]]); + $: [chunks, lookup] = partition(reader.pages, layout); + $: layout, ({ main, secondary } = chunks[lookup[reader.page]]); </script> <svelte:document @@ -76,16 +76,16 @@ /> {#if !secondary} - <ReaderPage page={main} on:click={clickMain} --justify="center" /> + <ReaderPage page={main} onclick={clickMain} --justify="center" /> {:else if direction === Direction.LeftToRight} - <ReaderPage page={main} on:click={prev} --justify="flex-end" /> - <ReaderPage page={secondary} on:click={next} --justify="flex-start" /> + <ReaderPage page={main} onclick={prev} --justify="flex-end" /> + <ReaderPage page={secondary} onclick={next} --justify="flex-start" /> {:else} - <ReaderPage page={secondary} on:click={next} --justify="flex-end" /> - <ReaderPage page={main} on:click={prev} --justify="flex-start" /> + <ReaderPage page={secondary} onclick={next} --justify="flex-end" /> + <ReaderPage page={main} onclick={prev} --justify="flex-start" /> {/if} <div class="invisible absolute"> - {#each pagesAround($reader.page) as page} + {#each pagesAround(reader.page) as page} <img src={src(page.image, 'full')} alt="" /> {/each} </div> diff --git a/frontend/src/lib/reader/Reader.svelte b/frontend/src/lib/reader/Reader.svelte index 9bc7a82..b5cc725 100644 --- a/frontend/src/lib/reader/Reader.svelte +++ b/frontend/src/lib/reader/Reader.svelte @@ -1,32 +1,96 @@ +<script lang="ts" module> + import { Layout, type PageFragment } from '$gql/graphql'; + import { getContext, setContext } from 'svelte'; + + export interface Chunk { + main: PageFragment; + secondary?: PageFragment; + index: number; + } + + class ReaderContext { + visible = $state(false); + sidebar = $state(false); + pages: PageFragment[] = $state([]); + page = $state(0); + + open = (page: number) => { + this.page = page; + this.visible = true; + }; + } + + export function initReaderContext() { + return setContext<ReaderContext>('reader', new ReaderContext()); + } + + export function getReaderContext() { + return getContext<ReaderContext>('reader'); + } + + export function partition(pages: PageFragment[], layout: Layout): [Chunk[], number[]] { + const single = layout === Layout.Single; + const offset = layout === Layout.DoubleOffset; + + const chunks: Chunk[] = []; + const lookup: number[] = Array<number>(pages.length); + + for (let chunkIndex = 0, pageIndex = 0; pageIndex < pages.length; chunkIndex++) { + const wide = () => pages[pageIndex].image.aspectRatio > 1; + + const nextPage = () => { + lookup[pageIndex] = chunkIndex; + return pages[pageIndex++]; + }; + + const offsetFirst = pageIndex === 0 && offset; + const full = single || wide() || offsetFirst; + + const chunk: Chunk = { index: pageIndex, main: nextPage() }; + + if (!full && pageIndex < pages.length) { + if (!wide()) { + chunk.secondary = nextPage(); + } + } + + chunks.push(chunk); + } + return [chunks, lookup]; + } +</script> + <script lang="ts"> import { trapFocus } from '$lib/Actions'; - import { getReaderContext } from '$lib/Reader'; import { fadeDefault, slideXDefault } from '$lib/Transitions'; + import type { Snippet } from 'svelte'; import { fade, slide } from 'svelte/transition'; import CloseReaderButton from './components/CloseReaderButton.svelte'; import PageIndicator from './components/PageIndicator.svelte'; import ReaderMenuButton from './components/ReaderMenuButton.svelte'; + let { sidebar, children }: { sidebar?: Snippet; children?: Snippet } = $props(); + const reader = getReaderContext(); </script> -{#if $reader.visible} +{#if reader.visible} <div role="dialog" class="fixed bottom-0 left-0 right-0 top-0 z-10 flex h-full w-full bg-black" transition:fade={fadeDefault} use:trapFocus > - {#if $$slots.sidebar && $reader.sidebar} + {#if sidebar && reader.sidebar} <aside class="w-[36rem] shrink-0 bg-slate-800" transition:slide={slideXDefault}> <div class="flex h-full min-w-[36rem] flex-col gap-4 overflow-auto p-4"> - <slot name="sidebar" /> + {@render sidebar?.()} </div> </aside> {/if} <main class="relative flex grow"> <div class="absolute flex w-full p-1 text-lg [&>*:last-child]:ml-auto"> - {#if $$slots.sidebar} + {#if sidebar} <ReaderMenuButton /> {/if} <CloseReaderButton /> @@ -36,7 +100,7 @@ </div> <div class="flex grow"> - <slot /> + {@render children?.()} </div> </main> </div> diff --git a/frontend/src/lib/reader/ReaderPage.svelte b/frontend/src/lib/reader/ReaderPage.svelte index fb3e780..83b2d1b 100644 --- a/frontend/src/lib/reader/ReaderPage.svelte +++ b/frontend/src/lib/reader/ReaderPage.svelte @@ -1,13 +1,19 @@ <script lang="ts"> import type { PageFragment } from '$gql/graphql'; import { src } from '$lib/Utils'; + import type { MouseEventHandler } from 'svelte/elements'; - export let page: PageFragment; + interface Props { + page: PageFragment; + onclick: MouseEventHandler<HTMLDivElement>; + } + + let { page, onclick }: Props = $props(); </script> -<!-- svelte-ignore a11y-click-events-have-key-events --> -<!-- svelte-ignore a11y-no-static-element-interactions --> -<div class="flex grow" on:click> +<!-- svelte-ignore a11y_click_events_have_key_events --> +<!-- svelte-ignore a11y_no_static_element_interactions --> +<div class="flex grow" {onclick}> <img class="h-auto w-auto object-contain" width={page.image.width} diff --git a/frontend/src/lib/reader/components/CloseReaderButton.svelte b/frontend/src/lib/reader/components/CloseReaderButton.svelte index 0c88323..f3eb4ba 100644 --- a/frontend/src/lib/reader/components/CloseReaderButton.svelte +++ b/frontend/src/lib/reader/components/CloseReaderButton.svelte @@ -1,19 +1,22 @@ <script lang="ts"> - import { getReaderContext } from '$lib/Reader'; import { accelerator } from '$lib/Shortcuts'; + import { getReaderContext } from '../Reader.svelte'; const reader = getReaderContext(); + + function onclick() { + reader.visible = false; + reader.sidebar = false; + } </script> <button type="button" class="btn floating" title="Close reader" - on:click={() => { - $reader.visible = false; - $reader.sidebar = false; - }} + aria-label="Close reader" + {onclick} use:accelerator={'Escape'} > - <span class="icon-lg icon-[material-symbols--close]" /> + <span class="icon-lg icon-[material-symbols--close]"></span> </button> diff --git a/frontend/src/lib/reader/components/PageIndicator.svelte b/frontend/src/lib/reader/components/PageIndicator.svelte index f79fc00..35190b3 100644 --- a/frontend/src/lib/reader/components/PageIndicator.svelte +++ b/frontend/src/lib/reader/components/PageIndicator.svelte @@ -1,9 +1,9 @@ <script lang="ts"> - import { getReaderContext } from '$lib/Reader'; + import { getReaderContext } from '../Reader.svelte'; const reader = getReaderContext(); </script> <div class="floating !p-2"> - {$reader.page + 1}/{$reader.pages.length} + {reader.page + 1}/{reader.pages.length} </div> diff --git a/frontend/src/lib/reader/components/ReaderMenuButton.svelte b/frontend/src/lib/reader/components/ReaderMenuButton.svelte index aa20206..58648e8 100644 --- a/frontend/src/lib/reader/components/ReaderMenuButton.svelte +++ b/frontend/src/lib/reader/components/ReaderMenuButton.svelte @@ -1,16 +1,19 @@ <script lang="ts"> - import { getReaderContext } from '$lib/Reader'; import { accelerator } from '$lib/Shortcuts'; + import { getReaderContext } from '../Reader.svelte'; const reader = getReaderContext(); + + let title = $derived(`${reader.sidebar ? 'Hide' : 'Show'} menu`); </script> <button type="button" class="btn floating invisible xl:visible" - title={`${$reader.sidebar ? 'Hide' : 'Show'} menu`} - on:click={() => ($reader.sidebar = !$reader.sidebar)} + {title} + aria-label={title} + onclick={() => (reader.sidebar = !reader.sidebar)} use:accelerator={'z'} > - <span class="icon-lg icon-[material-symbols--dock-to-right]" /> + <span class="icon-lg icon-[material-symbols--dock-to-right]"></span> </button> diff --git a/frontend/src/lib/scraper/ComicScrapeForm.svelte b/frontend/src/lib/scraper/ComicScrapeForm.svelte index 30ad89b..6cc3451 100644 --- a/frontend/src/lib/scraper/ComicScrapeForm.svelte +++ b/frontend/src/lib/scraper/ComicScrapeForm.svelte @@ -2,60 +2,69 @@ import { upsertComics } from '$gql/Mutations'; import { comicScrapersQuery, scrapeComic } from '$gql/Queries'; import { isError } from '$gql/Utils'; - import { OnMissing, type FullComicFragment } from '$gql/graphql'; - import { ScrapedComicSelector, getScraperContext } from '$lib/Scraper'; + import { OnMissing, type FullComicFragment, type ScrapeComicQuery } from '$gql/graphql'; import { toastError, toastFinally } from '$lib/Toasts'; import Select from '$lib/components/Select.svelte'; import Spinner from '$lib/components/Spinner.svelte'; - import { getContextClient } from '@urql/svelte'; + import { getContextClient, type OperationResult } from '@urql/svelte'; + import { getScraperContext, ScrapedComicSelector } from './Scraper.svelte'; import SelectorGroup from './components/SelectorGroup.svelte'; import SelectorItem from './components/SelectorItem.svelte'; let client = getContextClient(); const context = getScraperContext(); - export let comic: FullComicFragment; - let createMissing = false; - let loading = false; + interface Props { + comic: FullComicFragment; + onupsert: () => void; + } - $: scrapersResult = comicScrapersQuery(client, { id: comic.id }); - $: scrapers = $scrapersResult.data?.comicScrapers; + let { comic, onupsert }: Props = $props(); + let createMissing = $state(false); + let loading = $state(false); - function scrape() { - loading = true; - scrapeComic(client, { id: comic.id, scraper: $context.scraper }) - .then((result) => { - if (result.error) { - toastError(result.error.message); - return; - } + let scrapersResult = $derived(comicScrapersQuery(client, { id: comic.id })); + let scrapers = $derived($scrapersResult.data?.comicScrapers); - if (result.data) { - if (isError(result.data.scrapeComic)) { - toastError(result.data.scrapeComic.message); - return; - } + function scrape(event: SubmitEvent) { + event.preventDefault(); + if (!context.scraper) return; - if (result.data.scrapeComic.__typename === 'ScrapeComicResult') { - $context.selector = new ScrapedComicSelector(result.data.scrapeComic.data, comic); - $context.warnings = result.data.scrapeComic.warnings; - } - } - }) + loading = true; + scrapeComic(client, { id: comic.id, scraper: context.scraper }) + .then(handleScrapeResult) .catch(toastFinally) .finally(() => (loading = false)); } - function updateFromScrape(createMissing: boolean) { - if (!$context.selector) return; + function handleScrapeResult(result: OperationResult<ScrapeComicQuery>) { + if (result.error) { + toastError(result.error.message); + return; + } + + if (result.data) { + if (isError(result.data.scrapeComic)) { + toastError(result.data.scrapeComic.message); + return; + } + + if (result.data.scrapeComic.__typename === 'ScrapeComicResult') { + context.selector = new ScrapedComicSelector(result.data.scrapeComic.data, comic); + context.warnings = result.data.scrapeComic.warnings; + } + } + } + + function upsert(event: SubmitEvent) { + event.preventDefault(); + if (!context.selector) return; - upsertComics(client, { - ids: comic.id, - input: $context.selector.toInput(createMissing ? OnMissing.Create : OnMissing.Ignore) - }) + const input = context.selector.input(createMissing ? OnMissing.Create : OnMissing.Ignore); + upsertComics(client, { ids: comic.id, input }) .then(() => { - $context.selector = undefined; - $context.warnings = []; + onupsert(); + context.reset(); }) .catch(toastFinally); } @@ -65,56 +74,56 @@ {#if scrapers && scrapers.length === 0} <h2 class="text-base">No scrapers available.</h2> {:else} - <form on:submit|preventDefault={scrape}> + <form onsubmit={scrape}> <div class="grid grid-cols-6 gap-2"> <div class="col-span-5"> <Select id="scrapers" options={scrapers} placeholder={'Select scraper...'} - bind:value={$context.scraper} + bind:value={context.scraper} /> </div> - <button type="submit" disabled={!$context.scraper} class="btn-blue">Scrape</button> + <button type="submit" disabled={!context.scraper} class="btn-blue">Scrape</button> </div> </form> {/if} {#if loading} <Spinner /> - {:else if $context.selector} - {#if $context.warnings.length > 0} + {:else if context.selector} + {#if context.warnings.length > 0} <div class="flex flex-col gap-2"> <h2 class="flex gap-1 border-b border-slate-700 text-base font-medium">Warnings</h2> <ul class="ml-2 list-inside list-disc"> - {#each $context.warnings as warning} + {#each context.warnings as warning} <li>{warning}</li> {/each} </ul> </div> {/if} - {#if !$context.selector.hasData()} + {#if !context.selector.pending()} <h2 class="text-base">No data to merge.</h2> {:else} <div class="flex flex-col gap-2"> <h2 class="border-b border-slate-700 text-base font-medium">Results</h2> - <form on:submit|preventDefault={() => updateFromScrape(createMissing)}> + <form onsubmit={upsert}> <div class="grid grid-cols-6 gap-4 pb-2"> - <SelectorItem title="Title" selector={$context.selector.title} /> - <SelectorItem title="Original Title" selector={$context.selector.originalTitle} /> - <SelectorItem title="URL" selector={$context.selector.url} /> - <SelectorItem title="Date" selector={$context.selector.date} --span="2" /> - <SelectorItem title="Category" selector={$context.selector.category} --span="2" /> - <SelectorItem title="Language" selector={$context.selector.language} --span="2" /> - <SelectorItem title="Rating" selector={$context.selector.rating} --span="2" /> - <SelectorItem title="Censorship" selector={$context.selector.censorship} --span="2" /> - <SelectorItem title="Direction" selector={$context.selector.direction} --span="2" /> - <SelectorItem title="Layout" selector={$context.selector.layout} --span="2" /> - <SelectorGroup title="Artists" selectors={$context.selector.artists} /> - <SelectorGroup title="Circles" selectors={$context.selector.circles} /> - <SelectorGroup title="Characters" selectors={$context.selector.characters} /> - <SelectorGroup title="Worlds" selectors={$context.selector.worlds} /> - <SelectorGroup title="Tags" selectors={$context.selector.tags} /> + <SelectorItem title="Title" selector={context.selector.title} /> + <SelectorItem title="Original Title" selector={context.selector.originalTitle} /> + <SelectorItem title="URL" selector={context.selector.url} /> + <SelectorItem title="Date" selector={context.selector.date} --span="2" /> + <SelectorItem title="Category" selector={context.selector.category} --span="2" /> + <SelectorItem title="Language" selector={context.selector.language} --span="2" /> + <SelectorItem title="Rating" selector={context.selector.rating} --span="2" /> + <SelectorItem title="Censorship" selector={context.selector.censorship} --span="2" /> + <SelectorItem title="Direction" selector={context.selector.direction} --span="2" /> + <SelectorItem title="Layout" selector={context.selector.layout} --span="2" /> + <SelectorGroup title="Artists" selectors={context.selector.artists} /> + <SelectorGroup title="Circles" selectors={context.selector.circles} /> + <SelectorGroup title="Characters" selectors={context.selector.characters} /> + <SelectorGroup title="Worlds" selectors={context.selector.worlds} /> + <SelectorGroup title="Tags" selectors={context.selector.tags} /> </div> <div class="flex flex-col gap-2"> <h2 class="border-b border-slate-700 text-base font-medium">Options</h2> diff --git a/frontend/src/lib/Scraper.ts b/frontend/src/lib/scraper/Scraper.svelte.ts index 4baf370..93e756b 100644 --- a/frontend/src/lib/Scraper.ts +++ b/frontend/src/lib/scraper/Scraper.svelte.ts @@ -20,24 +20,28 @@ import { RatingLabel } from '$lib/Enums'; import { getContext, setContext } from 'svelte'; -import { writable, type Writable } from 'svelte/store'; -interface ScraperContext { - scraper: string; - warnings: string[]; - selector?: ScrapedComicSelector; +class ScraperContext { + scraper?: string = $state(); + warnings: string[] = $state([]); + selector?: ScrapedComicSelector = $state(); + + reset = () => { + this.selector = undefined; + this.warnings = []; + }; } export function initScraperContext() { - return setContext<Writable<ScraperContext>>('scraper', writable({ scraper: '', warnings: [] })); + return setContext<ScraperContext>('scraper', new ScraperContext()); } export function getScraperContext() { - return getContext<Writable<ScraperContext>>('scraper'); + return getContext<ScraperContext>('scraper'); } export class Selector<T extends string> { - keep = true; + keep = $state(true); value: T; display: string | undefined; @@ -46,6 +50,10 @@ export class Selector<T extends string> { this.display = display; } + toggle = () => { + this.keep = !this.keep; + }; + toString() { return this.display ?? this.value; } @@ -121,7 +129,7 @@ export class ScrapedComicSelector { this.worlds = Selector.fromList(scraped.worlds, comic.worlds); } - hasData() { + pending() { return ( Object.values(this).filter((i) => { if (i === undefined) { @@ -134,7 +142,7 @@ export class ScrapedComicSelector { ); } - toInput(onMissing: OnMissing): UpsertComicInput { + input(onMissing: OnMissing): UpsertComicInput { return { title: keepItem(this.title), originalTitle: keepItem(this.originalTitle), diff --git a/frontend/src/lib/scraper/components/SelectorButton.svelte b/frontend/src/lib/scraper/components/SelectorButton.svelte index b786f89..e976f91 100644 --- a/frontend/src/lib/scraper/components/SelectorButton.svelte +++ b/frontend/src/lib/scraper/components/SelectorButton.svelte @@ -1,19 +1,19 @@ <script lang="ts"> - import { Selector } from '$lib/Scraper'; + import { Selector } from '../Scraper.svelte'; - export let selector: Selector<string>; + let { selector }: { selector: Selector<string> } = $props(); </script> <button type="button" class="ml-1 flex rounded-sm border-slate-700 bg-slate-900 hover:brightness-110" - on:click={() => (selector.keep = !selector.keep)} + onclick={selector.toggle} > <div class="flex self-center pl-1"> {#if selector.keep} - <span class="icon-base icon-[material-symbols--check] text-green-400" /> + <span class="icon-base icon-[material-symbols--check] text-green-400"></span> {:else} - <span class="icon-base icon-[material-symbols--close] text-red-400" /> + <span class="icon-base icon-[material-symbols--close] text-red-400"></span> {/if} </div> <p class:opacity-50={!selector.keep} class="p-1 text-left"> diff --git a/frontend/src/lib/scraper/components/SelectorGroup.svelte b/frontend/src/lib/scraper/components/SelectorGroup.svelte index 1fdb8f2..11489b1 100644 --- a/frontend/src/lib/scraper/components/SelectorGroup.svelte +++ b/frontend/src/lib/scraper/components/SelectorGroup.svelte @@ -1,9 +1,13 @@ <script lang="ts"> - import { Selector } from '$lib/Scraper'; + import { Selector } from '../Scraper.svelte'; import SelectorButton from './SelectorButton.svelte'; - export let title: string; - export let selectors: Selector<string>[]; + interface Props { + title: string; + selectors: Selector<string>[]; + } + + let { title, selectors = $bindable() }: Props = $props(); function invert() { for (let selector of selectors) { @@ -20,8 +24,9 @@ <button type="button" class="flex items-end opacity-75 brightness-75 transition-opacity hover:opacity-100 hover:brightness-110 focus-visible:opacity-100" - on:click={invert} + onclick={invert} title="Invert selection" + aria-label="Invert selection" > <span class="icon-xs icon-[material-symbols--compare-arrows]"></span> </button> diff --git a/frontend/src/lib/scraper/components/SelectorItem.svelte b/frontend/src/lib/scraper/components/SelectorItem.svelte index dd3f5b4..5beba50 100644 --- a/frontend/src/lib/scraper/components/SelectorItem.svelte +++ b/frontend/src/lib/scraper/components/SelectorItem.svelte @@ -1,9 +1,8 @@ <script lang="ts"> - import { Selector } from '$lib/Scraper'; + import { Selector } from '../Scraper.svelte'; import SelectorButton from './SelectorButton.svelte'; - export let title: string; - export let selector: Selector<string> | undefined; + let { title, selector }: { title: string; selector?: Selector<string> } = $props(); </script> {#if selector} diff --git a/frontend/src/lib/selection/Selectable.svelte b/frontend/src/lib/selection/Selectable.svelte index 48b6ac7..4705f44 100644 --- a/frontend/src/lib/selection/Selectable.svelte +++ b/frontend/src/lib/selection/Selectable.svelte @@ -1,18 +1,20 @@ <script lang="ts"> - import { getSelectionContext } from '$lib/Selection'; + import type { Snippet } from 'svelte'; + import { getSelectionContext } from './Selection.svelte'; - export let id: number; - export let index: number; + interface Props { + id: number; + index: number; + edit?: ((id: number) => void) | undefined; + children?: Snippet<[{ onclick: (event: MouseEvent) => void; selected: boolean }]>; + } - export let edit: ((id: number) => void) | undefined = undefined; + let { id, index, edit = undefined, children }: Props = $props(); + let selection = getSelectionContext(); - const selection = getSelectionContext(); - - $: selected = $selection.contains(id); - - const handle = (event: MouseEvent) => { - if ($selection.active) { - $selection = $selection.update(index, event.shiftKey); + const onclick = (event: MouseEvent) => { + if (selection.active) { + selection.update(index, event.shiftKey); event.preventDefault(); } else if (edit) { edit(id); @@ -21,4 +23,4 @@ }; </script> -<slot {handle} {selected} /> +{@render children?.({ onclick, selected: selection.contains(id) })} diff --git a/frontend/src/lib/selection/Selection.svelte.ts b/frontend/src/lib/selection/Selection.svelte.ts new file mode 100644 index 0000000..dc294d0 --- /dev/null +++ b/frontend/src/lib/selection/Selection.svelte.ts @@ -0,0 +1,121 @@ +import { getContext, setContext } from 'svelte'; +import { SvelteSet } from 'svelte/reactivity'; +import { range } from '../Utils'; + +interface Selectable { + id: number; +} + +export function initSelectionContext<T extends Selectable>( + typename: string, + toName: (item: T) => string, + selectable?: (item: T) => boolean +) { + return setContext('selection', new ItemSelection(typename, toName, selectable)); +} + +export function getSelectionContext<T extends Selectable>() { + return getContext<ItemSelection<T>>('selection'); +} + +export class ItemSelection<T extends Selectable> { + active = $state(false); + view: T[] = $state([]); + + #ids = $state(new SvelteSet<number>()); + #masked = $derived(new SvelteSet([...this.#ids].filter((i) => this.#indexOf(i) >= 0))); + + typename: string; + #toName: (item: T) => string; + selectable: (item: T) => boolean; + + constructor( + typename: string, + toName: (item: T) => string, + selectable: (item: T) => boolean = () => true + ) { + this.typename = typename; + this.#toName = toName; + this.selectable = selectable; + } + + #indexOf = (id: number) => this.view.findIndex((v) => v.id === id); + + update = (index: number, shift: boolean) => { + const id = this.view[index].id; + + const selectableRange = (first: number, last: number) => + range(first, last) + .filter((i) => this.selectable(this.view[i])) + .map((i) => this.view[i].id); + + if (shift) { + const indices = this.indices; + + const first = indices.at(0); + const last = indices.at(-1); + + if (first === undefined || last === undefined) { + this.#ids.add(id); + } else if (index === first || index === last) { + this.#ids.clear(); + } else if (index > last) { + this.#ids = new SvelteSet([...this.#ids, ...selectableRange(last, index)]); + } else if (index < last) { + this.#ids = new SvelteSet([...this.#ids, ...selectableRange(index, last)]); + } + } else { + if (this.#ids.has(id)) { + this.#ids.delete(id); + } else { + this.#ids.add(id); + } + } + }; + + toggle = () => { + this.active = !this.active; + + if (!this.active) { + this.none(); + } + }; + + all = () => { + this.#ids = new SvelteSet(this.view.filter(this.selectable).map((i) => i.id)); + }; + + none = () => { + this.#ids.clear(); + this.#masked.clear(); + }; + + clear = () => { + this.active = false; + this.none(); + }; + + contains(id: number) { + return this.#masked.has(id); + } + + get ids() { + return [...this.#masked]; + } + + get size() { + return this.#masked.size; + } + + get indices() { + return [...this.#ids].map(this.#indexOf).filter((i) => i >= 0); + } + + get items() { + return this.indices.map((i) => this.view[i]); + } + + get names() { + return this.items.map(this.#toName); + } +} diff --git a/frontend/src/lib/selection/SelectionOverlay.svelte b/frontend/src/lib/selection/SelectionOverlay.svelte index 04ff382..97421b0 100644 --- a/frontend/src/lib/selection/SelectionOverlay.svelte +++ b/frontend/src/lib/selection/SelectionOverlay.svelte @@ -1,7 +1,11 @@ <script lang="ts"> - export let selected: boolean; - export let position: 'top' | 'right' | 'left' | 'bottom'; - export let centered = false; + interface Props { + selected: boolean; + position: 'top' | 'right' | 'left' | 'bottom'; + centered?: boolean; + } + + let { selected, position, centered = false }: Props = $props(); </script> {#if selected} @@ -9,7 +13,7 @@ class:items-center={centered} class="{position} pointer-events-none absolute z-[1] flex bg-emerald-700/95" > - <span class="icon-base icon-[material-symbols--check] text-[2rem]" /> + <span class="icon-base icon-[material-symbols--check] text-[2rem]"></span> </div> {/if} diff --git a/frontend/src/lib/statistics/Stat.svelte b/frontend/src/lib/statistics/Stat.svelte index c657526..7e03e09 100644 --- a/frontend/src/lib/statistics/Stat.svelte +++ b/frontend/src/lib/statistics/Stat.svelte @@ -1,8 +1,12 @@ <script lang="ts"> - export let title: string; - export let value: number; - export let precision = 0; - export let unit = ''; + interface Props { + title: string; + value: number; + precision?: number; + unit?: string; + } + + let { title, value, precision = 0, unit = '' }: Props = $props(); function format(value: number) { if (Number.isNaN(value) || !Number.isFinite(value)) { diff --git a/frontend/src/lib/statistics/StatGroup.svelte b/frontend/src/lib/statistics/StatGroup.svelte index e1b97da..e84c555 100644 --- a/frontend/src/lib/statistics/StatGroup.svelte +++ b/frontend/src/lib/statistics/StatGroup.svelte @@ -1,5 +1,7 @@ <script lang="ts"> - export let title; + import type { Snippet } from 'svelte'; + + let { title, children }: { title: string; children?: Snippet } = $props(); </script> <section @@ -7,6 +9,6 @@ > <h2 class="text-2xl">{title}</h2> <div class="flex flex-row flex-wrap gap-10"> - <slot /> + {@render children?.()} </div> </section> diff --git a/frontend/src/lib/tabs/AddOverlay.svelte b/frontend/src/lib/tabs/AddOverlay.svelte index b1c98bf..329b259 100644 --- a/frontend/src/lib/tabs/AddOverlay.svelte +++ b/frontend/src/lib/tabs/AddOverlay.svelte @@ -1,7 +1,7 @@ <script lang="ts"> - import { updateComics } from '$gql/Mutations'; import { UpdateMode } from '$gql/graphql'; - import { getSelectionContext } from '$lib/Selection'; + import { updateComics } from '$gql/Mutations'; + import { getSelectionContext } from '$lib/selection/Selection.svelte'; import { toastFinally } from '$lib/Toasts'; import { fadeDefault } from '$lib/Transitions'; import { getContextClient } from '@urql/svelte'; @@ -10,27 +10,30 @@ const client = getContextClient(); const selection = getSelectionContext(); - export let id: number; + let { id }: { id: number } = $props(); + + function onclick(event: MouseEvent) { + event.preventDefault(); - function addPages() { updateComics(client, { ids: id, - input: { pages: { ids: $selection.ids, options: { mode: UpdateMode.Add } } } + input: { pages: { ids: selection.ids, options: { mode: UpdateMode.Add } } } }) - .then(() => ($selection = $selection.none())) + .then(() => selection.none()) .catch(toastFinally); } </script> -{#if $selection.size > 0} +{#if selection.size > 0} <div class="absolute left-1 top-1" transition:fade={fadeDefault}> <button type="button" class="btn-blue rounded-full shadow-sm shadow-black" title="Add to this comic" - on:click|preventDefault={addPages} + aria-label="Add to this comic" + {onclick} > - <span class="icon-base icon-[material-symbols--note-add]" /> + <span class="icon-base icon-[material-symbols--note-add]"></span> </button> </div> {/if} diff --git a/frontend/src/lib/tabs/ArchiveDelete.svelte b/frontend/src/lib/tabs/ArchiveDelete.svelte index b0e3c58..50a99c2 100644 --- a/frontend/src/lib/tabs/ArchiveDelete.svelte +++ b/frontend/src/lib/tabs/ArchiveDelete.svelte @@ -9,7 +9,7 @@ const client = getContextClient(); - export let archive: FullArchiveFragment; + let { archive }: { archive: FullArchiveFragment } = $props(); function deleteArchive() { confirmDeletion('Archive', archive.name, () => { @@ -37,6 +37,6 @@ <p class="mt-2 font-medium">This action is irrevocable.</p> </div> <div class="flex"> - <DeleteButton prominent on:click={deleteArchive} /> + <DeleteButton prominent onclick={deleteArchive} /> </div> </div> diff --git a/frontend/src/lib/tabs/ArchiveDetails.svelte b/frontend/src/lib/tabs/ArchiveDetails.svelte index 9554557..b3d570f 100644 --- a/frontend/src/lib/tabs/ArchiveDetails.svelte +++ b/frontend/src/lib/tabs/ArchiveDetails.svelte @@ -8,7 +8,7 @@ import Header from './DetailsHeader.svelte'; import Section from './DetailsSection.svelte'; - export let archive: FullArchiveFragment; + let { archive }: { archive: FullArchiveFragment } = $props(); const now = Date.now(); const modifiedDate = new Date(archive.mtime); diff --git a/frontend/src/lib/tabs/ArchiveEdit.svelte b/frontend/src/lib/tabs/ArchiveEdit.svelte index 80efaed..83a492b 100644 --- a/frontend/src/lib/tabs/ArchiveEdit.svelte +++ b/frontend/src/lib/tabs/ArchiveEdit.svelte @@ -1,12 +1,12 @@ <script lang="ts"> import { addComic, updateArchives } from '$gql/Mutations'; import { type FullArchiveFragment } from '$gql/graphql'; - import { getSelectionContext } from '$lib/Selection'; import { toastFinally } from '$lib/Toasts'; import AddButton from '$lib/components/AddButton.svelte'; import Card, { comicCard } from '$lib/components/Card.svelte'; import OrganizedButton from '$lib/components/OrganizedButton.svelte'; import ComicPills from '$lib/pills/ComicPills.svelte'; + import { getSelectionContext } from '$lib/selection/Selection.svelte'; import SelectionControls from '$lib/toolbar/SelectionControls.svelte'; import { getContextClient } from '@urql/svelte'; import AddOverlay from './AddOverlay.svelte'; @@ -14,23 +14,23 @@ const client = getContextClient(); const selection = getSelectionContext(); - export let archive: FullArchiveFragment; + let { archive }: { archive: FullArchiveFragment } = $props(); function addNew() { addComic(client, { input: { archive: { id: archive.id }, title: archive.name, - pages: { ids: $selection.ids }, - cover: { id: archive.pages[$selection.indices.toSorted((a, b) => a - b)[0]].id } + pages: { ids: selection.ids }, + cover: { id: archive.pages[selection.indices.toSorted((a, b) => a - b)[0]].id } } }) .then((mutatation) => { const data = mutatation.addComic; if (data.__typename === 'AddComicSuccess' && !data.archivePagesRemaining) { - $selection = $selection.clear(); + selection.clear(); } else { - $selection = $selection.none(); + selection.none(); } }) .catch(toastFinally); @@ -46,10 +46,10 @@ <div class="flex flex-col gap-4"> <div class="flex gap-2 text-sm"> <SelectionControls page> - <AddButton title="Add Comic from selected" on:click={addNew} /> + <AddButton title="Add Comic from selected" onclick={addNew} /> </SelectionControls> - <div class="grow" /> - <OrganizedButton organized={archive.organized} on:click={toggleOrganized} /> + <div class="grow"></div> + <OrganizedButton organized={archive.organized} onclick={toggleOrganized} /> </div> {#if archive.comics.length > 0} @@ -58,7 +58,9 @@ <div class="flex shrink-0 flex-col gap-4"> {#each archive.comics as comic} <Card compact {...comicCard(comic)}> - <AddOverlay slot="overlay" id={comic.id} /> + {#snippet overlay()} + <AddOverlay id={comic.id} /> + {/snippet} <ComicPills {comic} /> </Card> {/each} diff --git a/frontend/src/lib/tabs/ComicDelete.svelte b/frontend/src/lib/tabs/ComicDelete.svelte index a10f6b2..3ae924c 100644 --- a/frontend/src/lib/tabs/ComicDelete.svelte +++ b/frontend/src/lib/tabs/ComicDelete.svelte @@ -9,9 +9,9 @@ const client = getContextClient(); - export let comic: FullComicFragment; + let { comic }: { comic: FullComicFragment } = $props(); - function deleteComic() { + function onclick() { confirmDeletion('Comic', comic.title, () => { deleteComics(client, { ids: comic.id }) .then(() => goto('/comics/')) @@ -29,6 +29,6 @@ <p class="mt-2 font-medium">This action is irrevocable.</p> </div> <div class="flex"> - <DeleteButton prominent on:click={deleteComic} /> + <DeleteButton prominent {onclick} /> </div> </div> diff --git a/frontend/src/lib/tabs/ComicDetails.svelte b/frontend/src/lib/tabs/ComicDetails.svelte index 0a131af..121f068 100644 --- a/frontend/src/lib/tabs/ComicDetails.svelte +++ b/frontend/src/lib/tabs/ComicDetails.svelte @@ -28,12 +28,24 @@ <div class="flex flex-col gap-4 text-sm"> <Header {title}> {#if comic.url} - <a href={comic.url} target="_blank" rel="noreferrer" class="btn-slate" title="Open URL"> - <span class="icon-base icon-[material-symbols--link]" /> + <a + href={comic.url} + target="_blank" + rel="noreferrer" + class="btn-slate" + title="Open URL" + aria-label="Open URL" + > + <span class="icon-base icon-[material-symbols--link]"></span> </a> {/if} - <a href={`/archives/${comic.archive.id}`} class="btn-slate" title="Go to Archive"> - <span class="icon-base icon-[material-symbols--folder-zip]" /> + <a + href={`/archives/${comic.archive.id}`} + class="btn-slate" + title="Go to Archive" + aria-label="Go to Archive" + > + <span class="icon-base icon-[material-symbols--folder-zip]"></span> </a> </Header> diff --git a/frontend/src/lib/tabs/DetailsHeader.svelte b/frontend/src/lib/tabs/DetailsHeader.svelte index f980f75..ee5fa23 100644 --- a/frontend/src/lib/tabs/DetailsHeader.svelte +++ b/frontend/src/lib/tabs/DetailsHeader.svelte @@ -1,5 +1,7 @@ <script lang="ts"> - export let title: string; + import type { Snippet } from 'svelte'; + + let { title, children }: { title: string; children?: Snippet } = $props(); </script> <div class="flex items-center gap-2"> @@ -7,5 +9,5 @@ {title} </h2> <div class="grow"></div> - <slot /> + {@render children?.()} </div> diff --git a/frontend/src/lib/tabs/DetailsSection.svelte b/frontend/src/lib/tabs/DetailsSection.svelte index 9a6ad51..5514aa3 100644 --- a/frontend/src/lib/tabs/DetailsSection.svelte +++ b/frontend/src/lib/tabs/DetailsSection.svelte @@ -1,10 +1,12 @@ <script lang="ts"> - export let title: string; + import type { Snippet } from 'svelte'; + + let { title, children }: { title: string; children?: Snippet } = $props(); </script> <section class="flex flex-col gap-1"> <h2 class="text-base font-medium">{title}</h2> <div class="flex flex-wrap gap-1 text-gray-300"> - <slot /> + {@render children?.()} </div> </section> diff --git a/frontend/src/lib/tabs/Tab.svelte b/frontend/src/lib/tabs/Tab.svelte index cddd072..f8dc67c 100644 --- a/frontend/src/lib/tabs/Tab.svelte +++ b/frontend/src/lib/tabs/Tab.svelte @@ -1,14 +1,28 @@ <script lang="ts"> - import { getTabContext } from '$lib/Tabs'; import { fadeDefault } from '$lib/Transitions'; + import type { Snippet } from 'svelte'; import { fade } from 'svelte/transition'; + import { getTabContext } from './Tabs.svelte'; + + interface Props { + id: string; + title: string; + initial?: boolean; + children: Snippet; + } + + let { id, title, initial = false, children }: Props = $props(); const context = getTabContext(); - export let id: string; + + context.tabs = { ...context.tabs, [id]: { title } }; + if (initial) { + context.current = id; + } </script> -{#if $context.current === id} +{#if context.current === id} <div class="h-full overflow-auto py-2 pe-3 ps-1" in:fade={fadeDefault}> - <slot /> + {@render children?.()} </div> {/if} diff --git a/frontend/src/lib/tabs/Tabs.svelte b/frontend/src/lib/tabs/Tabs.svelte index fd5d08e..1ae7c32 100644 --- a/frontend/src/lib/tabs/Tabs.svelte +++ b/frontend/src/lib/tabs/Tabs.svelte @@ -1,28 +1,50 @@ +<script lang="ts" module> + import { getContext, setContext } from 'svelte'; + + type Tab = string; + type Tabs = Record<Tab, { title: string }>; + + class TabContext { + tabs: Tabs = $state({}); + current: Tab = $state(''); + } + + export function getTabContext() { + return getContext<TabContext>('tabs'); + } + + function initTabContext() { + return setContext('tabs', new TabContext()); + } +</script> + <script lang="ts"> - import { getTabContext } from '$lib/Tabs'; import { fadeFast } from '$lib/Transitions'; + import type { Snippet } from 'svelte'; import { fade } from 'svelte/transition'; - const context = getTabContext(); + let { badges = {}, children }: { badges?: Record<Tab, boolean>; children?: Snippet } = $props(); + + const context = initTabContext(); </script> <div class="flex h-full max-h-full flex-col"> <nav> <ul class="me-3 ms-1 flex border-b-2 border-slate-700 text-sm"> - {#each Object.entries($context.tabs) as [id, { title, badge }]} + {#each Object.entries(context.tabs) as [id, { title }]} <li class="-mb-0.5"> <button type="button" - class:active={$context.current === id} + class:active={context.current === id} class="focus-background relative flex gap-1 p-1 px-3 hover:border-b-2 hover:border-slate-200" - on:click={() => ($context.current = id)} + onclick={() => (context.current = id)} > - {#if badge} + {#if badges[id]} <div class="absolute right-0 top-1 h-2 w-2 rounded-full bg-emerald-400" title="There are pending changes" transition:fade={fadeFast} - /> + ></div> {/if} <span>{title}</span> </button> @@ -30,7 +52,7 @@ {/each} </ul> </nav> - <slot /> + {@render children?.()} </div> <style lang="postcss"> diff --git a/frontend/src/lib/toolbar/DeleteSelection.svelte b/frontend/src/lib/toolbar/DeleteSelection.svelte index 7459a87..7b37313 100644 --- a/frontend/src/lib/toolbar/DeleteSelection.svelte +++ b/frontend/src/lib/toolbar/DeleteSelection.svelte @@ -1,26 +1,28 @@ <script lang="ts"> import type { DeleteMutation } from '$gql/Mutations'; - import { getSelectionContext } from '$lib/Selection'; + import DeleteButton from '$lib/components/DeleteButton.svelte'; + import { getSelectionContext } from '$lib/selection/Selection.svelte'; import { toastFinally } from '$lib/Toasts'; import { confirmDeletion } from '$lib/Utils'; - import DeleteButton from '$lib/components/DeleteButton.svelte'; import { getContextClient } from '@urql/svelte'; const client = getContextClient(); - const selection = getSelectionContext(); - export let mutation: DeleteMutation; - export let warning: string | undefined = undefined; + interface Props { + mutation: DeleteMutation; + warning?: string; + } + + let { mutation, warning = undefined }: Props = $props(); + let selection = getSelectionContext(); - function remove() { + function onclick() { const mutate = () => { - mutation(client, { ids: $selection.ids }) - .then(() => ($selection = $selection.clear())) - .catch(toastFinally); + mutation(client, { ids: selection.ids }).then(selection.clear).catch(toastFinally); }; - confirmDeletion($selection.typename, $selection.names, mutate, warning); + confirmDeletion(selection.typename, selection.names, mutate, warning); } </script> -<DeleteButton on:click={remove} /> +<DeleteButton {onclick} /> diff --git a/frontend/src/lib/toolbar/EditSelection.svelte b/frontend/src/lib/toolbar/EditSelection.svelte index 50e6656..1803ed4 100644 --- a/frontend/src/lib/toolbar/EditSelection.svelte +++ b/frontend/src/lib/toolbar/EditSelection.svelte @@ -1,20 +1,19 @@ <script lang="ts"> - import { getSelectionContext } from '$lib/Selection'; + import { getSelectionContext } from '$lib/selection/Selection.svelte'; import { accelerator } from '$lib/Shortcuts'; - import type { SvelteComponent } from 'svelte'; - import { openModal } from 'svelte-modals'; + import { toastFinally } from '$lib/Toasts'; + import { modals, type ModalComponent, type ModalProps } from 'svelte-modals'; const selection = getSelectionContext(); - export let dialog: typeof SvelteComponent<{ - isOpen: boolean; + interface DialogProps extends ModalProps { ids: number[]; - }>; + } + + let { dialog }: { dialog: ModalComponent<DialogProps> } = $props(); function edit() { - openModal(dialog, { - ids: $selection.ids - }); + modals.open(dialog, { ids: selection.ids }).catch(toastFinally); } </script> @@ -22,8 +21,9 @@ type="button" class="btn-slate hover:bg-blue-700" title="Edit selection" - on:click={edit} + aria-label="Edit selection" + onclick={edit} use:accelerator={'e'} > - <span class="icon-base icon-[material-symbols--edit]" /> + <span class="icon-base icon-[material-symbols--edit]"></span> </button> diff --git a/frontend/src/lib/toolbar/FilterBookmarked.svelte b/frontend/src/lib/toolbar/FilterBookmarked.svelte index bcbe295..76403ec 100644 --- a/frontend/src/lib/toolbar/FilterBookmarked.svelte +++ b/frontend/src/lib/toolbar/FilterBookmarked.svelte @@ -1,15 +1,16 @@ <script lang="ts"> - import { page } from '$app/stores'; - import { ComicFilterContext, cycleBooleanFilter, getFilterContext } from '$lib/Filter'; + import { page } from '$app/state'; + import { cycleBooleanFilter, type ComicFilterContext } from '$lib/Filter.svelte'; + import { accelerator } from '$lib/Shortcuts'; import Bookmark from '$lib/icons/Bookmark.svelte'; - const filter = getFilterContext<ComicFilterContext>(); - $: bookmarked = $filter.include.controls.bookmarked.value; + let { filter }: { filter: ComicFilterContext } = $props(); + let bookmarked = $derived(filter.include.bookmarked.value); const toggle = () => { - $filter.include.controls.bookmarked.value = cycleBooleanFilter(bookmarked, false); - $filter.apply($page.url.searchParams); + filter.include.bookmarked.value = cycleBooleanFilter(bookmarked, false); + filter.apply(page.url.searchParams); }; </script> @@ -17,7 +18,7 @@ class:toggled={bookmarked} class="btn-slate" title="Filter bookmarked" - on:click={toggle} + onclick={toggle} use:accelerator={'b'} > <Bookmark {bookmarked} /> diff --git a/frontend/src/lib/toolbar/FilterFavourites.svelte b/frontend/src/lib/toolbar/FilterFavourites.svelte index 6591cef..5e9beb7 100644 --- a/frontend/src/lib/toolbar/FilterFavourites.svelte +++ b/frontend/src/lib/toolbar/FilterFavourites.svelte @@ -1,15 +1,15 @@ <script lang="ts"> - import { page } from '$app/stores'; - import { ComicFilterContext, cycleBooleanFilter, getFilterContext } from '$lib/Filter'; + import { page } from '$app/state'; + import { ComicFilterContext, cycleBooleanFilter } from '$lib/Filter.svelte'; import { accelerator } from '$lib/Shortcuts'; import Star from '$lib/icons/Star.svelte'; - const filter = getFilterContext<ComicFilterContext>(); - $: favourite = $filter.include.controls.favourite.value; + let { filter }: { filter: ComicFilterContext } = $props(); + let favourite = $derived(filter.include.favourite.value); const toggle = () => { - $filter.include.controls.favourite.value = cycleBooleanFilter(favourite, false); - $filter.apply($page.url.searchParams); + filter.include.favourite.value = cycleBooleanFilter(favourite, false); + filter.apply(page.url.searchParams); }; </script> @@ -17,7 +17,7 @@ class:toggled={favourite} class="btn-slate" title="Filter favourites" - on:click={toggle} + onclick={toggle} use:accelerator={'f'} > <Star {favourite} /> diff --git a/frontend/src/lib/toolbar/FilterOrganized.svelte b/frontend/src/lib/toolbar/FilterOrganized.svelte index 754e663..0f95e5f 100644 --- a/frontend/src/lib/toolbar/FilterOrganized.svelte +++ b/frontend/src/lib/toolbar/FilterOrganized.svelte @@ -1,20 +1,20 @@ <script lang="ts"> - import { page } from '$app/stores'; + import { page } from '$app/state'; import { ArchiveFilterContext, - ComicFilterContext, cycleBooleanFilter, - getFilterContext - } from '$lib/Filter'; + type ComicFilterContext + } from '$lib/Filter.svelte'; + import { accelerator } from '$lib/Shortcuts'; import Organized from '$lib/icons/Organized.svelte'; - const filter = getFilterContext<ArchiveFilterContext | ComicFilterContext>(); - $: organized = $filter.include.controls.organized.value; + let { filter }: { filter: ComicFilterContext | ArchiveFilterContext } = $props(); + let organized = $derived(filter.include.organized.value); const toggle = () => { - $filter.include.controls.organized.value = cycleBooleanFilter(organized); - $filter.apply($page.url.searchParams); + filter.include.organized.value = cycleBooleanFilter(organized); + filter.apply(page.url.searchParams); }; </script> @@ -23,7 +23,7 @@ class:toggled={organized !== undefined} class="btn-slate" title="Filter organized" - on:click={toggle} + onclick={toggle} use:accelerator={'o'} > <Organized tristate {organized} /> diff --git a/frontend/src/lib/toolbar/MarkBookmark.svelte b/frontend/src/lib/toolbar/MarkBookmark.svelte index 792b84f..776ddd8 100644 --- a/frontend/src/lib/toolbar/MarkBookmark.svelte +++ b/frontend/src/lib/toolbar/MarkBookmark.svelte @@ -1,27 +1,25 @@ <script lang="ts"> - import { getSelectionContext } from '$lib/Selection'; - import { toastFinally } from '$lib/Toasts'; + import type { MutationWith } from '$gql/Utils'; import Bookmark from '$lib/icons/Bookmark.svelte'; - import { Client, getContextClient } from '@urql/svelte'; + import { getSelectionContext } from '$lib/selection/Selection.svelte'; + import { toastFinally } from '$lib/Toasts'; + import { getContextClient } from '@urql/svelte'; const client = getContextClient(); const selection = getSelectionContext(); - export let mutation: ( - client: Client, - args: { ids: number[]; input: { bookmarked: boolean } } - ) => Promise<unknown>; + let { mutation }: { mutation: MutationWith<{ bookmarked: boolean }> } = $props(); function mutate(bookmarked: boolean) { - mutation(client, { ids: $selection.ids, input: { bookmarked } }).catch(toastFinally); + mutation(client, { ids: selection.ids, input: { bookmarked } }).catch(toastFinally); } </script> -<button type="button" class="btn-slate flex justify-start gap-1" on:click={() => mutate(true)}> +<button type="button" class="btn-slate flex justify-start gap-1" onclick={() => mutate(true)}> <Bookmark bookmarked={true} /> <span>Bookmark</span> </button> -<button type="button" class="btn-slate flex justify-start gap-1" on:click={() => mutate(false)}> +<button type="button" class="btn-slate flex justify-start gap-1" onclick={() => mutate(false)}> <Bookmark bookmarked={false} /> <span>Unbookmark</span> </button> diff --git a/frontend/src/lib/toolbar/MarkFavourite.svelte b/frontend/src/lib/toolbar/MarkFavourite.svelte index 42eaa39..1af5d60 100644 --- a/frontend/src/lib/toolbar/MarkFavourite.svelte +++ b/frontend/src/lib/toolbar/MarkFavourite.svelte @@ -1,27 +1,25 @@ <script lang="ts"> - import { getSelectionContext } from '$lib/Selection'; - import { toastFinally } from '$lib/Toasts'; + import type { MutationWith } from '$gql/Utils'; import Star from '$lib/icons/Star.svelte'; - import { Client, getContextClient } from '@urql/svelte'; + import { getSelectionContext } from '$lib/selection/Selection.svelte'; + import { toastFinally } from '$lib/Toasts'; + import { getContextClient } from '@urql/svelte'; const client = getContextClient(); const selection = getSelectionContext(); - export let mutation: ( - client: Client, - args: { ids: number[]; input: { favourite: boolean } } - ) => Promise<unknown>; + let { mutation }: { mutation: MutationWith<{ favourite: boolean }> } = $props(); function mutate(favourite: boolean) { - mutation(client, { ids: $selection.ids, input: { favourite } }).catch(toastFinally); + mutation(client, { ids: selection.ids, input: { favourite } }).catch(toastFinally); } </script> -<button type="button" class="btn-slate justify-start gap-1" on:click={() => mutate(true)}> +<button type="button" class="btn-slate justify-start gap-1" onclick={() => mutate(true)}> <Star favourite={true} /> <span>Favourite</span> </button> -<button type="button" class="btn-slate justify-start gap-1" on:click={() => mutate(false)}> +<button type="button" class="btn-slate justify-start gap-1" onclick={() => mutate(false)}> <Star favourite={false} /> <span>Unfavourite</span> </button> diff --git a/frontend/src/lib/toolbar/MarkOrganized.svelte b/frontend/src/lib/toolbar/MarkOrganized.svelte index 4dc3a83..63c8622 100644 --- a/frontend/src/lib/toolbar/MarkOrganized.svelte +++ b/frontend/src/lib/toolbar/MarkOrganized.svelte @@ -1,27 +1,25 @@ <script lang="ts"> - import { getSelectionContext } from '$lib/Selection'; - import { toastFinally } from '$lib/Toasts'; + import type { MutationWith } from '$gql/Utils'; import Organized from '$lib/icons/Organized.svelte'; - import { Client, getContextClient } from '@urql/svelte'; + import { getSelectionContext } from '$lib/selection/Selection.svelte'; + import { toastFinally } from '$lib/Toasts'; + import { getContextClient } from '@urql/svelte'; const client = getContextClient(); const selection = getSelectionContext(); - export let mutation: ( - client: Client, - args: { ids: number[]; input: { organized: boolean } } - ) => Promise<unknown>; + let { mutation }: { mutation: MutationWith<{ organized: boolean }> } = $props(); function mutate(organized: boolean) { - mutation(client, { ids: $selection.ids, input: { organized } }).catch(toastFinally); + mutation(client, { ids: selection.ids, input: { organized } }).catch(toastFinally); } </script> -<button type="button" class="btn-slate flex justify-start gap-1" on:click={() => mutate(true)}> +<button type="button" class="btn-slate flex justify-start gap-1" onclick={() => mutate(true)}> <Organized tristate organized={true} /> <span>Organized</span> </button> -<button type="button" class="btn-slate flex justify-start gap-1" on:click={() => mutate(false)}> +<button type="button" class="btn-slate flex justify-start gap-1" onclick={() => mutate(false)}> <Organized dim tristate organized={false} /> <span>Unorganized</span> </button> diff --git a/frontend/src/lib/toolbar/MarkSelection.svelte b/frontend/src/lib/toolbar/MarkSelection.svelte index 27eb2c7..1af36ca 100644 --- a/frontend/src/lib/toolbar/MarkSelection.svelte +++ b/frontend/src/lib/toolbar/MarkSelection.svelte @@ -1,24 +1,23 @@ <script lang="ts"> import Dropdown from '$lib/components/Dropdown.svelte'; + import type { Snippet } from 'svelte'; - let visible = false; - let button: HTMLElement; + let { children }: { children: Snippet } = $props(); </script> -<div class="relative"> - <button - type="button" - class="btn-slate rounded-inherit relative hover:bg-blue-700 [&:not(:only-child)]:bg-blue-700" - title="Set flag..." - bind:this={button} - on:click={() => (visible = !visible)} - > - <span class="icon-base icon-[material-symbols--flag] pointer-events-none" /> - </button> - - <Dropdown parent={button} bind:visible> - <div class="grid grid-cols-[min-content_min-content] gap-1"> - <slot /> - </div> - </Dropdown> -</div> +<Dropdown> + {#snippet button(onclick)} + <button + type="button" + class="btn-slate rounded-inherit relative hover:bg-blue-700 [&:not(:only-child)]:bg-blue-700" + title="Set flag..." + aria-label="Set flag..." + {onclick} + > + <span class="icon-base icon-[material-symbols--flag] pointer-events-none"></span> + </button> + {/snippet} + <div class="grid grid-cols-[min-content_min-content] gap-1"> + {@render children?.()} + </div> +</Dropdown> diff --git a/frontend/src/lib/toolbar/Search.svelte b/frontend/src/lib/toolbar/Search.svelte index f033258..4806971 100644 --- a/frontend/src/lib/toolbar/Search.svelte +++ b/frontend/src/lib/toolbar/Search.svelte @@ -1,13 +1,15 @@ <script lang="ts"> - import { page } from '$app/stores'; + import { page } from '$app/state'; import { debounce } from '$lib/Actions'; - import { BasicFilterContext, getFilterContext } from '$lib/Filter'; import { accelerator } from '$lib/Shortcuts'; - const filter = getFilterContext<BasicFilterContext>(); + interface Props { + name: string; + field: string; + filter: { apply: (params: URLSearchParams) => void }; + } - export let name: string; - export let field: string; + let { name, field = $bindable(), filter }: Props = $props(); </script> <input @@ -16,6 +18,6 @@ class="btn-slate w-min" placeholder="Search {name}..." bind:value={field} - use:debounce={{ callback: () => $filter.apply($page.url.searchParams) }} + use:debounce={{ callback: () => filter.apply(page.url.searchParams) }} use:accelerator={'F'} /> diff --git a/frontend/src/lib/toolbar/SelectItems.svelte b/frontend/src/lib/toolbar/SelectItems.svelte index 7ff339e..68a0652 100644 --- a/frontend/src/lib/toolbar/SelectItems.svelte +++ b/frontend/src/lib/toolbar/SelectItems.svelte @@ -1,18 +1,19 @@ <script lang="ts"> - import { page } from '$app/stores'; - import { getPaginationContext } from '$lib/Pagination'; + import { page } from '$app/state'; + import { navigate, type PaginationData } from '$lib/Navigation'; - const pagination = getPaginationContext(); + let { pagination }: { pagination: PaginationData } = $props(); - $: values = new Set([24, 48, 72, 90, 120, 150, 180, $pagination.items].sort((a, b) => a - b)); + let values = $derived( + new Set([24, 48, 72, 90, 120, 150, 180, pagination.items].sort((a, b) => a - b)) + ); + + function onchange(e: Event & { currentTarget: EventTarget & HTMLSelectElement }) { + navigate({ pagination: { items: +e.currentTarget.value } }, page.url.searchParams); + } </script> -<select - class="btn-slate" - bind:value={$pagination.items} - on:change={() => $pagination.apply($page.url.searchParams)} - title="Limit displayed items to..." -> +<select class="btn-slate" value={pagination.items} {onchange} title="Limit displayed items to..."> {#each values as value} <option {value}>{value}</option> {/each} diff --git a/frontend/src/lib/toolbar/SelectSort.svelte b/frontend/src/lib/toolbar/SelectSort.svelte index fdcb057..0e59df6 100644 --- a/frontend/src/lib/toolbar/SelectSort.svelte +++ b/frontend/src/lib/toolbar/SelectSort.svelte @@ -1,60 +1,68 @@ <script lang="ts"> - import { page } from '$app/stores'; + import { page } from '$app/state'; import { SortDirection } from '$gql/graphql'; - - import { getSortContext } from '$lib/Sort'; + import { navigate, type SortData } from '$lib/Navigation'; import { slideXFast } from '$lib/Transitions'; import { getRandomInt } from '$lib/Utils'; import { slide } from 'svelte/transition'; - const sort = getSortContext(); + let { sort, labels }: { sort: SortData<string>; labels: Record<string, string> } = $props(); + + function apply(sort: SortData<string>) { + navigate({ sort }, page.url.searchParams); + } function toggle() { - if ($sort.direction === SortDirection.Ascending) { - $sort.direction = SortDirection.Descending; + if (sort.direction === SortDirection.Ascending) { + apply({ ...sort, direction: SortDirection.Descending }); } else { - $sort.direction = SortDirection.Ascending; + apply({ ...sort, direction: SortDirection.Ascending }); } + } - apply(); + function newSeed() { + return getRandomInt(0, 1000000000); } - function apply() { - if ($sort.on === 'RANDOM' && $sort.seed === undefined) { - $sort.seed = getRandomInt(0, 1000000000); - } - $sort.apply($page.url.searchParams); + function shuffle() { + apply({ ...sort, seed: newSeed() }); } - function reshuffle() { - $sort.seed = undefined; - apply(); + function onchange(e: Event & { currentTarget: EventTarget & HTMLSelectElement }) { + let seed: number | undefined = undefined; + + if (e.currentTarget.value === 'RANDOM') { + seed = newSeed(); + } + + apply({ ...sort, on: e.currentTarget.value, seed }); } </script> <div class="rounded-group flex flex-row"> - <select class="btn-slate" bind:value={$sort.on} on:change={apply} title="Sort on..."> - {#each Object.entries($sort.labels) as [value, label]} + <select class="btn-slate" value={sort.on} {onchange} title="Sort on..."> + {#each Object.entries(labels) as [value, label]} <option {value}>{label}</option> {/each} </select> - <button type="button" class="btn-slate" title="Toggle sort direction" on:click={toggle}> - {#if $sort.direction === SortDirection.Ascending} - <span class="icon-base icon-[material-symbols--sort] -scale-y-100" /> + <button type="button" class="btn-slate" title="Toggle sort direction" onclick={toggle}> + {#if sort.direction === SortDirection.Ascending} + <span class="icon-base icon-[material-symbols--sort] -scale-y-100"></span> {:else} - <span class="icon-base icon-[material-symbols--sort]" /> + <span class="icon-base icon-[material-symbols--sort]"></span> {/if} </button> - {#if $sort.on === 'RANDOM'} + {#if sort.on === 'RANDOM'} <button type="button" class="btn-slate" title="Reshuffle" - on:click={reshuffle} + aria-label="Reshuffle" + onclick={shuffle} transition:slide={slideXFast} > <div class="flex"> - <span class="icon-base icon-[material-symbols--shuffle]" /> + <span class="icon-base icon-[material-symbols--shuffle]"></span> </div> </button> {/if} diff --git a/frontend/src/lib/toolbar/SelectionControls.svelte b/frontend/src/lib/toolbar/SelectionControls.svelte index 4d309df..f0026c8 100644 --- a/frontend/src/lib/toolbar/SelectionControls.svelte +++ b/frontend/src/lib/toolbar/SelectionControls.svelte @@ -1,57 +1,64 @@ <script lang="ts"> - import { getSelectionContext } from '$lib/Selection'; + import Badge from '$lib/components/Badge.svelte'; + import { getSelectionContext } from '$lib/selection/Selection.svelte'; import { accelerator } from '$lib/Shortcuts'; import { fadeDefault, slideXFast } from '$lib/Transitions'; - import Badge from '$lib/components/Badge.svelte'; - import { onDestroy } from 'svelte'; + import { onDestroy, type Snippet } from 'svelte'; import { fade, slide } from 'svelte/transition'; - const selection = getSelectionContext(); - - export let page = false; - - const toggle = () => ($selection = $selection.toggle()); - const all = () => ($selection = $selection.all()); - const none = () => ($selection = $selection.none()); + let { page = false, children }: { page?: boolean; children?: Snippet } = $props(); + let selection = getSelectionContext(); - onDestroy(() => ($selection = $selection.clear())); + onDestroy(selection.clear); </script> <div class="rounded-group flex"> <button type="button" class="btn-slate relative" - class:toggled={$selection.active} - title={`${$selection.active ? 'Exit' : 'Enter'} ${page ? 'page ' : ' '}selection mode`} - on:click={toggle} + class:toggled={selection.active} + title={`${selection.active ? 'Exit' : 'Enter'} ${page ? 'page ' : ' '}selection mode`} + onclick={selection.toggle} use:accelerator={'s'} > - {#if $selection.active} + {#if selection.active} {#if page} - <span class="icon-base icon-[material-symbols--edit-document]" /> + <span class="icon-base icon-[material-symbols--edit-document]"></span> {:else} - <span class="icon-base icon-[material-symbols--remove-selection]" /> + <span class="icon-base icon-[material-symbols--remove-selection]"></span> {/if} {:else if page} - <span class="icon-base icon-[material-symbols--edit-document-outline]" /> + <span class="icon-base icon-[material-symbols--edit-document-outline]"></span> {:else} - <span class="icon-base icon-[material-symbols--select]" /> + <span class="icon-base icon-[material-symbols--select]"></span> {/if} - <Badge number={$selection.size} /> + <Badge number={selection.size} /> </button> - {#if $selection.active} + {#if selection.active} <div class="rounded-group-end flex" transition:slide={slideXFast}> - <button type="button" class="btn-slate" title="Select all" on:click={all}> - <span class="icon-base icon-[material-symbols--select-all]" /> + <button + type="button" + class="btn-slate" + title="Select all" + aria-label="Select all" + onclick={selection.all} + > + <span class="icon-base icon-[material-symbols--select-all]"></span> </button> - <button type="button" class="btn-slate" title="Select none" on:click={none}> - <span class="icon-base icon-[material-symbols--deselect]" /> + <button + type="button" + class="btn-slate" + title="Select none" + aria-label="Select all" + onclick={selection.none} + > + <span class="icon-base icon-[material-symbols--deselect]"></span> </button> </div> {/if} </div> -{#if $selection.size > 0} +{#if selection.size > 0} <div class="rounded-group flex" transition:fade={fadeDefault}> - <slot /> + {@render children?.()} </div> {/if} diff --git a/frontend/src/lib/toolbar/ToggleAdvancedFilters.svelte b/frontend/src/lib/toolbar/ToggleAdvancedFilters.svelte index 2e7869f..ee07902 100644 --- a/frontend/src/lib/toolbar/ToggleAdvancedFilters.svelte +++ b/frontend/src/lib/toolbar/ToggleAdvancedFilters.svelte @@ -1,39 +1,42 @@ <script lang="ts"> - import { page } from '$app/stores'; - import { getFilterContext } from '$lib/Filter'; + import { page } from '$app/state'; import { navigate } from '$lib/Navigation'; import { slideXFast } from '$lib/Transitions'; import Badge from '$lib/components/Badge.svelte'; import { slide } from 'svelte/transition'; - import { getToolbarContext } from './Toolbar.svelte'; + import type { ToolbarState } from './Toolbar.svelte'; - const toolbar = getToolbarContext(); - const filter = getFilterContext(); + interface Props extends ToolbarState { + filterSize: number; + } + + let { expanded, toggle, filterSize }: Props = $props(); </script> <div class="rounded-group flex"> <button - class:toggled={$toolbar.expand} + class:toggled={expanded} class="btn-slate relative" - title={`${$toolbar.expand ? 'Hide' : 'Show'} filters`} - on:click={() => ($toolbar.expand = !$toolbar.expand)} + title={`${expanded ? 'Hide' : 'Show'} filters`} + onclick={toggle} > - {#if $toolbar.expand} - <span class="icon-base icon-[material-symbols--filter-alt]" /> + {#if expanded} + <span class="icon-base icon-[material-symbols--filter-alt]"></span> {:else} - <span class="icon-base icon-[material-symbols--filter-alt-outline]" /> + <span class="icon-base icon-[material-symbols--filter-alt-outline]"></span> {/if} - <Badge number={$filter.include.size + $filter.exclude.size} /> + <Badge number={filterSize} /> </button> - {#if $filter.include.size + $filter.exclude.size > 0} + {#if filterSize > 0} <button class="btn-slate relative hover:bg-rose-700" - on:click={() => navigate({ filter: {} }, $page.url.searchParams)} + onclick={() => navigate({ filter: {} }, page.url.searchParams)} transition:slide={slideXFast} title="Reset filters" + aria-label="Reset filters" > <div class="flex"> - <span class="icon-base icon-[material-symbols--filter-alt-off]" /> + <span class="icon-base icon-[material-symbols--filter-alt-off]"></span> </div> </button> {/if} diff --git a/frontend/src/lib/toolbar/Toolbar.svelte b/frontend/src/lib/toolbar/Toolbar.svelte index e87d731..03cd892 100644 --- a/frontend/src/lib/toolbar/Toolbar.svelte +++ b/frontend/src/lib/toolbar/Toolbar.svelte @@ -1,23 +1,25 @@ -<script lang="ts" context="module"> - import { writable, type Writable } from 'svelte/store'; +<script lang="ts"> + import { type Snippet } from 'svelte'; - interface ToolbarContext { - expand: boolean; + export interface ToolbarState { + expanded: boolean; + toggle: () => void; } - function initToolbarContext() { - return setContext<Writable<ToolbarContext>>('toolbar', writable({ expand: false })); + interface Props { + start?: Snippet<[ToolbarState]>; + center?: Snippet<[ToolbarState]>; + end?: Snippet<[ToolbarState]>; + expansion?: Snippet; } - export function getToolbarContext() { - return getContext<Writable<ToolbarContext>>('toolbar'); - } -</script> + let { start, center, end, expansion }: Props = $props(); -<script lang="ts"> - import { getContext, setContext } from 'svelte'; + let expanded = $state(false); - const toolbar = initToolbarContext(); + function toggle() { + expanded = !expanded; + } </script> <div class="flex flex-col"> @@ -25,18 +27,18 @@ class="flex flex-row flex-wrap gap-4 text-sm xl:grid xl:grid-flow-col xl:grid-cols-[1fr_2fr_1fr]" > <div class="flex flex-row justify-start gap-2"> - <slot name="start" /> + {@render start?.({ expanded, toggle })} </div> <div class="flex flex-row flex-wrap justify-start gap-2 xl:flex-nowrap xl:justify-center"> - <slot name="center" /> + {@render center?.({ expanded, toggle })} </div> <div class="flex flex-row justify-end gap-2"> - <slot name="end" /> + {@render end?.({ expanded, toggle })} </div> </div> - {#if $toolbar.expand} + {#if expanded} <div class="mt-4"> - <slot /> + {@render expansion?.()} </div> {/if} </div> diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte index 6af3b88..29a1c16 100644 --- a/frontend/src/routes/+layout.svelte +++ b/frontend/src/routes/+layout.svelte @@ -1,5 +1,6 @@ <script lang="ts"> import { addShortcut, handleShortcuts } from '$lib/Shortcuts'; + import { toastFinally } from '$lib/Toasts'; import { fadeDefault } from '$lib/Transitions'; import AddArtist from '$lib/dialogs/AddArtist.svelte'; import AddCharacter from '$lib/dialogs/AddCharacter.svelte'; @@ -11,7 +12,7 @@ import Navigation from '$lib/navigation/Navigation.svelte'; import { cacheExchange, fetchExchange, initContextClient } from '@urql/svelte'; import { SvelteToast } from '@zerodevx/svelte-toast'; - import { Modals, closeModal, openModal } from 'svelte-modals'; + import { Modals, modals, type ModalComponent } from 'svelte-modals'; import { fade } from 'svelte/transition'; import '../app.css'; @@ -20,12 +21,16 @@ exchanges: [cacheExchange, fetchExchange] }); - addShortcut('na', () => openModal(AddArtist)); - addShortcut('nh', () => openModal(AddCharacter)); - addShortcut('ni', () => openModal(AddCircle)); - addShortcut('nn', () => openModal(AddNamespace)); - addShortcut('nt', () => openModal(AddTag)); - addShortcut('nw', () => openModal(AddWorld)); + function open(modal: ModalComponent) { + modals.open(modal).catch(toastFinally); + } + + addShortcut('na', () => open(AddArtist)); + addShortcut('nh', () => open(AddCharacter)); + addShortcut('ni', () => open(AddCircle)); + addShortcut('nn', () => open(AddNamespace)); + addShortcut('nt', () => open(AddTag)); + addShortcut('nw', () => open(AddWorld)); function keydown(event: KeyboardEvent) { handleShortcuts(event); @@ -36,38 +41,38 @@ <Navigation> <Link matchExact href="/" title="Home" accel="go"> - <span class="icon-base icon-[material-symbols--home]" /> + <span class="icon-base icon-[material-symbols--home]"></span> </Link> <Link href="/comics/" title="Comics" accel="gc"> - <span class="icon-base icon-[material-symbols--menu-book]" /> + <span class="icon-base icon-[material-symbols--menu-book]"></span> </Link> <Link href="/namespaces/" title="Namespaces" accel="gn"> - <span class="icon-base icon-[material-symbols--inbox]" /> + <span class="icon-base icon-[material-symbols--inbox]"></span> </Link> <Link href="/tags/" title="Tags" accel="gt"> - <span class="icon-base icon-[material-symbols--label]" /> + <span class="icon-base icon-[material-symbols--label]"></span> </Link> <Link href="/artists/" title="Artists" accel="ga"> - <span class="icon-base icon-[material-symbols--person]" /> + <span class="icon-base icon-[material-symbols--person]"></span> </Link> <Link href="/circles/" title="Circles" accel="gi"> - <span class="icon-base icon-[material-symbols--group]" /> + <span class="icon-base icon-[material-symbols--group]"></span> </Link> <Link href="/characters/" title="Characters" accel="gh"> - <span class="icon-base icon-[material-symbols--face]" /> + <span class="icon-base icon-[material-symbols--face]"></span> </Link> <Link href="/worlds/" title="Worlds" accel="gw"> - <span class="icon-base icon-[material-symbols--public]" /> + <span class="icon-base icon-[material-symbols--public]"></span> </Link> <Link href="/archives/" title="Archives" accel="gz"> - <span class="icon-base icon-[material-symbols--folder-zip]" /> + <span class="icon-base icon-[material-symbols--folder-zip]"></span> </Link> - <div class="mb-auto" /> + <div class="mb-auto"></div> <Link href="/statistics/" title="Statistics" accel="gs"> - <span class="icon-base icon-[material-symbols--bar-chart]" /> + <span class="icon-base icon-[material-symbols--bar-chart]"></span> </Link> <Link href="/help/" title="Help" accel="?" target="_blank"> - <span class="icon-base icon-[material-symbols--help]" /> + <span class="icon-base icon-[material-symbols--help]"></span> </Link> </Navigation> @@ -76,14 +81,15 @@ </div> <Modals> - <!-- svelte-ignore a11y-no-static-element-interactions --> - <!-- svelte-ignore a11y-click-events-have-key-events --> - <div - slot="backdrop" - on:click={closeModal} - transition:fade={fadeDefault} - class="fixed bottom-0 left-0 right-0 top-0 z-20 bg-stone-800/80" - /> + {#snippet backdrop({ close })} + <!-- svelte-ignore a11y-no-static-element-interactions --> + <!-- svelte-ignore a11y-click-events-have-key-events --> + <div + onclick={() => close()} + transition:fade={fadeDefault} + class="fixed bottom-0 left-0 right-0 top-0 z-20 bg-stone-800/80" + ></div> + {/snippet} </Modals> <SvelteToast options={{ reversed: true, intro: { y: 192 } }} /> diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte index 97a7a60..32e4e07 100644 --- a/frontend/src/routes/+page.svelte +++ b/frontend/src/routes/+page.svelte @@ -19,10 +19,10 @@ }); const favouriteLink = href('comics', { filter: { include: { favourite: true } } }); - $: query = frontpageQuery(getContextClient()); - $: recent = $query.data?.recent; - $: favourites = $query.data?.favourites; - $: bookmarked = $query.data?.bookmarked; + let query = $derived(frontpageQuery(getContextClient())); + let recent = $derived($query.data?.recent); + let favourites = $derived($query.data?.favourites); + let bookmarked = $derived($query.data?.bookmarked); </script> <Head section="Home" /> diff --git a/frontend/src/routes/archives/+page.svelte b/frontend/src/routes/archives/+page.svelte index 545058a..3fc4ed4 100644 --- a/frontend/src/routes/archives/+page.svelte +++ b/frontend/src/routes/archives/+page.svelte @@ -3,10 +3,7 @@ import { archivesQuery } from '$gql/Queries'; import type { ArchiveFragment } from '$gql/graphql'; import { ArchiveSortLabel } from '$lib/Enums'; - import { ArchiveFilterContext, initFilterContext } from '$lib/Filter'; - import { initPaginationContext } from '$lib/Pagination'; - import { initSelectionContext } from '$lib/Selection'; - import { initSortContext } from '$lib/Sort'; + import { ArchiveFilterContext } from '$lib/Filter.svelte'; import Card from '$lib/components/Card.svelte'; import Empty from '$lib/components/Empty.svelte'; import Guard from '$lib/components/Guard.svelte'; @@ -17,6 +14,7 @@ import Pagination from '$lib/pagination/Pagination.svelte'; import Pill from '$lib/pills/Pill.svelte'; import Selectable from '$lib/selection/Selectable.svelte'; + import { initSelectionContext } from '$lib/selection/Selection.svelte'; import SelectionOverlay from '$lib/selection/SelectionOverlay.svelte'; import DeleteSelection from '$lib/toolbar/DeleteSelection.svelte'; import FilterOrganized from '$lib/toolbar/FilterOrganized.svelte'; @@ -29,90 +27,91 @@ import Toolbar from '$lib/toolbar/Toolbar.svelte'; import { getContextClient } from '@urql/svelte'; import { filesize } from 'filesize'; - import type { PageData } from './$types'; + import type { PageProps } from './$types'; - let client = getContextClient(); + let { data }: PageProps = $props(); + let pagination = $derived(data.pagination); + let sort = $derived(data.sort); - export let data: PageData; + const client = getContextClient(); + let result = $derived(archivesQuery(client, { ...data })); + let archives = $derived($result.data?.archives); - $: result = archivesQuery(client, { - pagination: data.pagination, - filter: data.filter, - sort: data.sort + let selection = initSelectionContext<ArchiveFragment>('Archive', (a) => a.name); + $effect(() => { + if (archives) { + selection.view = archives.edges; + } }); - $: archives = $result.data?.archives; - - const selection = initSelectionContext<ArchiveFragment>('Archive', (a) => a.name); - $: if (archives) { - $selection.view = archives.edges; - $pagination.total = archives.count; - } - - const pagination = initPaginationContext(); - $: $pagination.update = data.pagination; - - const filter = initFilterContext<ArchiveFilterContext>(); - $: $filter = new ArchiveFilterContext(data.filter); - - const sort = initSortContext(data.sort, ArchiveSortLabel); - $: $sort.update = data.sort; - - function refresh() { - result.reexecute({ requestPolicy: 'network-only' }); - } + let filter = $state(new ArchiveFilterContext(data.filter)); + $effect(() => { + filter = new ArchiveFilterContext(data.filter); + }); </script> <Head section="Archives" /> <Column> <Toolbar> - <SelectionControls slot="start"> - <MarkSelection> - <MarkOrganized mutation={updateArchives} /> - </MarkSelection> - <DeleteSelection - mutation={deleteArchives} - warning="Deleting an archive will also delete its archive file on disk as well as all comics that belong to it." - /> - </SelectionControls> - <svelte:fragment slot="center"> - <Search name="Archives" bind:field={$filter.include.controls.path.contains} /> - <FilterOrganized /> - <SelectSort /> - <SelectItems /> - </svelte:fragment> - <RefreshButton slot="end" on:click={refresh} /> + {#snippet start()} + <SelectionControls> + <MarkSelection> + <MarkOrganized mutation={updateArchives} /> + </MarkSelection> + <DeleteSelection + mutation={deleteArchives} + warning="Deleting an archive will also delete its archive file on disk as well as all comics that belong to it." + /> + </SelectionControls> + {/snippet} + {#snippet center()} + <Search name="Archives" {filter} bind:field={filter.include.path.contains} /> + <FilterOrganized {filter} /> + <SelectSort {sort} labels={ArchiveSortLabel} /> + <SelectItems {pagination} /> + {/snippet} + {#snippet end()} + <RefreshButton onclick={() => result.reexecute({ requestPolicy: 'network-only' })} /> + {/snippet} </Toolbar> {#if archives} - <Pagination /> + <Pagination {pagination} total={archives.count} /> <main> <Cards> {#each archives.edges as { id, name, cover, size, pageCount }, index (id)} - <Selectable {index} {id} let:handle let:selected> - <Card - ellipsis={false} - href={id.toString()} - details={{ title: name, cover: cover }} - on:click={handle} - > - <SelectionOverlay position="left" {selected} slot="overlay" /> - <div class="flex gap-1 text-xs"> - <Pill name={`${pageCount} pages`}> - <span class="icon-[material-symbols--note] mr-0.5" slot="icon" /> - </Pill> - <Pill name={filesize(size, { base: 2 })}> - <span class="icon-[material-symbols--hard-drive] mr-0.5" slot="icon" /> - </Pill> - </div> - </Card> + <Selectable {index} {id}> + {#snippet children({ onclick, selected })} + <Card + ellipsis={false} + href={id.toString()} + details={{ title: name, cover }} + {onclick} + > + {#snippet overlay()} + <SelectionOverlay position="left" {selected} /> + {/snippet} + <div class="flex gap-1 text-xs"> + <Pill name={`${pageCount} pages`}> + {#snippet icon()} + <span class="icon-[material-symbols--note] mr-0.5"></span> + {/snippet} + </Pill> + <Pill name={filesize(size, { base: 2 })}> + {#snippet icon()} + <span class="icon-[material-symbols--hard-drive] mr-0.5"></span> + {/snippet} + </Pill> + </div> + </Card> + {/snippet} </Selectable> {:else} <Empty /> {/each} </Cards> </main> - <Pagination /> + <Pagination {pagination} total={archives.count} /> {:else} <Guard {result} /> {/if} diff --git a/frontend/src/routes/archives/[id]/+page.svelte b/frontend/src/routes/archives/[id]/+page.svelte index 50a2940..56c3273 100644 --- a/frontend/src/routes/archives/[id]/+page.svelte +++ b/frontend/src/routes/archives/[id]/+page.svelte @@ -2,9 +2,6 @@ import { updateArchives } from '$gql/Mutations'; import { archiveQuery } from '$gql/Queries'; import { Direction, Layout, type FullArchiveFragment, type PageFragment } from '$gql/graphql'; - import { initReaderContext } from '$lib/Reader'; - import { initSelectionContext } from '$lib/Selection'; - import { setTabContext } from '$lib/Tabs'; import { toastFinally } from '$lib/Toasts'; import Guard from '$lib/components/Guard.svelte'; import Head from '$lib/components/Head.svelte'; @@ -12,52 +9,40 @@ import Grid from '$lib/containers/Grid.svelte'; import Gallery from '$lib/gallery/Gallery.svelte'; import PageView from '$lib/reader/PageView.svelte'; - import Reader from '$lib/reader/Reader.svelte'; + import Reader, { initReaderContext } from '$lib/reader/Reader.svelte'; + import { initSelectionContext } from '$lib/selection/Selection.svelte'; import ArchiveDelete from '$lib/tabs/ArchiveDelete.svelte'; import ArchiveDetails from '$lib/tabs/ArchiveDetails.svelte'; import ArchiveEdit from '$lib/tabs/ArchiveEdit.svelte'; import Tab from '$lib/tabs/Tab.svelte'; import Tabs from '$lib/tabs/Tabs.svelte'; import { getContextClient } from '@urql/svelte'; - import type { PageData } from './$types'; + import type { PageProps } from './$types'; - export let data: PageData; + let { data }: PageProps = $props(); const client = getContextClient(); const reader = initReaderContext(); - setTabContext({ - tabs: { - details: { title: 'Details' }, - edit: { title: 'Edit' }, - deletion: { title: 'Delete' } - }, - current: 'details' - }); - - $: result = archiveQuery(client, { id: data.id }); - function updateCover(event: CustomEvent<number>) { - updateArchives(client, { ids: archive.id, input: { cover: { id: event.detail } } }).catch( - toastFinally - ); + function updateCover(id: number) { + updateArchives(client, { ids: data.id, input: { cover: { id } } }).catch(toastFinally); } - let archive: FullArchiveFragment; + let selection = initSelectionContext<PageFragment>( + 'Page', + (p) => p.path, + (p) => p.comicId === null + ); - $: $result, update(); - function update() { - if (!$result.stale && $result.data?.archive.__typename === 'FullArchive') { - archive = structuredClone($result.data.archive); + let result = $derived(archiveQuery(client, { id: data.id })); + let archive: FullArchiveFragment | undefined = $state(); - $reader.pages = archive.pages; + $effect(() => { + if (!$result.stale && $result.data?.archive.__typename === 'FullArchive') { + archive = $result.data.archive; + reader.pages = $result.data.archive.pages; + selection.view = $result.data.archive.pages; } - } - - const selection = initSelectionContext<PageFragment>('Page', (p) => p.path); - $selection.selectable = (p) => p.comicId === null; - - $: if (archive) { - $selection.view = archive.pages; - } + }); </script> <Head section="Archive" title={archive?.name} /> @@ -70,24 +55,20 @@ <aside> <Tabs> - <Tab id="details"> + <Tab initial id="details" title="Details"> <ArchiveDetails {archive} /> </Tab> - <Tab id="edit"> + <Tab id="edit" title="Edit"> <ArchiveEdit {archive} /> </Tab> - <Tab id="deletion"> + <Tab id="deletion" title="Delete"> <ArchiveDelete {archive} /> </Tab> </Tabs> </aside> <main class="overflow-auto"> - <Gallery - pages={archive.pages} - on:open={(e) => ($reader = $reader.open(e.detail))} - on:cover={updateCover} - /> + <Gallery pages={archive.pages} open={reader.open} {updateCover} /> </main> </Grid> {:else} diff --git a/frontend/src/routes/artists/+page.svelte b/frontend/src/routes/artists/+page.svelte index e07338c..c907470 100644 --- a/frontend/src/routes/artists/+page.svelte +++ b/frontend/src/routes/artists/+page.svelte @@ -3,10 +3,7 @@ import { artistsQuery, fetchArtist } from '$gql/Queries'; import type { Artist } from '$gql/graphql'; import { ArtistSortLabel } from '$lib/Enums'; - import { BasicFilterContext, initFilterContext } from '$lib/Filter'; - import { initPaginationContext } from '$lib/Pagination'; - import { initSelectionContext } from '$lib/Selection'; - import { initSortContext } from '$lib/Sort'; + import { BasicFilterContext } from '$lib/Filter.svelte'; import { toastFinally } from '$lib/Toasts'; import AddButton from '$lib/components/AddButton.svelte'; import Cardlet from '$lib/components/Cardlet.svelte'; @@ -19,6 +16,7 @@ import EditArtist from '$lib/dialogs/EditArtist.svelte'; import Pagination from '$lib/pagination/Pagination.svelte'; import Selectable from '$lib/selection/Selectable.svelte'; + import { initSelectionContext } from '$lib/selection/Selection.svelte'; import SelectionOverlay from '$lib/selection/SelectionOverlay.svelte'; import DeleteSelection from '$lib/toolbar/DeleteSelection.svelte'; import Search from '$lib/toolbar/Search.svelte'; @@ -27,38 +25,32 @@ import SelectionControls from '$lib/toolbar/SelectionControls.svelte'; import Toolbar from '$lib/toolbar/Toolbar.svelte'; import { getContextClient } from '@urql/svelte'; - import { openModal } from 'svelte-modals'; - import type { PageData } from './$types'; + import { modals } from 'svelte-modals'; + import type { PageProps } from './$types'; + + let { data }: PageProps = $props(); + let pagination = $derived(data.pagination); + let sort = $derived(data.sort); const client = getContextClient(); - export let data: PageData; + let result = $derived(artistsQuery(client, { ...data })); + let artists = $derived($result.data?.artists); - $: result = artistsQuery(client, { - pagination: data.pagination, - filter: data.filter, - sort: data.sort + let selection = initSelectionContext<Artist>('Artist', (a) => a.name); + $effect(() => { + if (artists) { + selection.view = artists.edges; + } }); - $: artists = $result.data?.artists; - - const selection = initSelectionContext<Artist>('Artist', (a) => a.name); - $: if (artists) { - $selection.view = artists.edges; - $pagination.total = artists.count; - } - - const filter = initFilterContext<BasicFilterContext>(); - $: $filter = new BasicFilterContext(data.filter); - - const sort = initSortContext(data.sort, ArtistSortLabel); - $: $sort.update = data.sort; - - const pagination = initPaginationContext(); - $: $pagination.update = data.pagination; + let filter = $state(new BasicFilterContext(data.filter)); + $effect(() => { + filter = new BasicFilterContext(data.filter); + }); const edit = (id: number) => { fetchArtist(client, id) - .then((artist) => openModal(EditArtist, { artist })) + .then((artist) => modals.open(EditArtist, { artist })) .catch(toastFinally); }; </script> @@ -67,34 +59,40 @@ <Column> <Toolbar> - <SelectionControls slot="start"> - <DeleteSelection mutation={deleteArtists} /> - </SelectionControls> - <svelte:fragment slot="center"> - <Search name="Artists" bind:field={$filter.include.controls.name.contains} /> - <SelectSort /> - <SelectItems /> - </svelte:fragment> - <svelte:fragment slot="end"> - <AddButton title="Add Artist" on:click={() => openModal(AddArtist)} /> - </svelte:fragment> + {#snippet start()} + <SelectionControls> + <DeleteSelection mutation={deleteArtists} /> + </SelectionControls> + {/snippet} + {#snippet center()} + <Search name="Artists" {filter} bind:field={filter.include.name.contains} /> + <SelectSort {sort} labels={ArtistSortLabel} /> + <SelectItems {pagination} /> + {/snippet} + {#snippet end()} + <AddButton title="Add Artist" onclick={() => modals.open(AddArtist)} /> + {/snippet} </Toolbar> {#if artists} - <Pagination /> + <Pagination {pagination} total={artists.count} /> <main> <Cardlets> {#each artists.edges as { id, name }, index (id)} - <Selectable {index} {id} {edit} let:handle let:selected> - <Cardlet {name} on:click={handle} filter="artists" {id}> - <SelectionOverlay slot="overlay" position="right" centered {selected} /> - </Cardlet> + <Selectable {index} {id} {edit}> + {#snippet children({ onclick, selected })} + <Cardlet {name} {onclick} filter="artists" {id}> + {#snippet overlay()} + <SelectionOverlay position="right" centered {selected} /> + {/snippet} + </Cardlet> + {/snippet} </Selectable> {:else} <Empty /> {/each} </Cardlets> </main> - <Pagination /> + <Pagination {pagination} total={artists.count} /> {:else} <Guard {result} /> {/if} diff --git a/frontend/src/routes/characters/+page.svelte b/frontend/src/routes/characters/+page.svelte index 0934bab..04c72cb 100644 --- a/frontend/src/routes/characters/+page.svelte +++ b/frontend/src/routes/characters/+page.svelte @@ -3,10 +3,7 @@ import { charactersQuery, fetchCharacter } from '$gql/Queries'; import type { Character } from '$gql/graphql'; import { CharacterSortLabel } from '$lib/Enums'; - import { BasicFilterContext, initFilterContext } from '$lib/Filter'; - import { initPaginationContext } from '$lib/Pagination'; - import { initSelectionContext } from '$lib/Selection'; - import { initSortContext } from '$lib/Sort'; + import { BasicFilterContext } from '$lib/Filter.svelte'; import { toastFinally } from '$lib/Toasts'; import AddButton from '$lib/components/AddButton.svelte'; import Cardlet from '$lib/components/Cardlet.svelte'; @@ -19,6 +16,7 @@ import EditCharacter from '$lib/dialogs/EditCharacter.svelte'; import Pagination from '$lib/pagination/Pagination.svelte'; import Selectable from '$lib/selection/Selectable.svelte'; + import { initSelectionContext } from '$lib/selection/Selection.svelte'; import SelectionOverlay from '$lib/selection/SelectionOverlay.svelte'; import DeleteSelection from '$lib/toolbar/DeleteSelection.svelte'; import Search from '$lib/toolbar/Search.svelte'; @@ -27,38 +25,32 @@ import SelectionControls from '$lib/toolbar/SelectionControls.svelte'; import Toolbar from '$lib/toolbar/Toolbar.svelte'; import { getContextClient } from '@urql/svelte'; - import { openModal } from 'svelte-modals'; - import type { PageData } from './$types'; + import { modals } from 'svelte-modals'; + import type { PageProps } from './$types'; + + let { data }: PageProps = $props(); + let pagination = $derived(data.pagination); + let sort = $derived(data.sort); const client = getContextClient(); - export let data: PageData; + let result = $derived(charactersQuery(client, { ...data })); + let characters = $derived($result.data?.characters); - $: result = charactersQuery(client, { - pagination: data.pagination, - filter: data.filter, - sort: data.sort + let selection = initSelectionContext<Character>('Character', (a) => a.name); + $effect(() => { + if (characters) { + selection.view = characters.edges; + } }); - $: characters = $result.data?.characters; - - const selection = initSelectionContext<Character>('Character', (c) => c.name); - $: if (characters) { - $selection.view = characters.edges; - $pagination.total = characters.count; - } - - const filter = initFilterContext<BasicFilterContext>(); - $: $filter = new BasicFilterContext(data.filter); - - const sort = initSortContext(data.sort, CharacterSortLabel); - $: $sort.update = data.sort; - - const pagination = initPaginationContext(); - $: $pagination.update = data.pagination; + let filter = $state(new BasicFilterContext(data.filter)); + $effect(() => { + filter = new BasicFilterContext(data.filter); + }); const edit = (id: number) => { fetchCharacter(client, id) - .then((character) => openModal(EditCharacter, { character })) + .then((character) => modals.open(EditCharacter, { character })) .catch(toastFinally); }; </script> @@ -67,34 +59,40 @@ <Column> <Toolbar> - <SelectionControls slot="start"> - <DeleteSelection mutation={deleteCharacters} /> - </SelectionControls> - <svelte:fragment slot="center"> - <Search name="Characters" bind:field={$filter.include.controls.name.contains} /> - <SelectSort /> - <SelectItems /> - </svelte:fragment> - <svelte:fragment slot="end"> - <AddButton title="Add Character" on:click={() => openModal(AddCharacter)} /> - </svelte:fragment> + {#snippet start()} + <SelectionControls> + <DeleteSelection mutation={deleteCharacters} /> + </SelectionControls> + {/snippet} + {#snippet center()} + <Search name="Characters" {filter} bind:field={filter.include.name.contains} /> + <SelectSort {sort} labels={CharacterSortLabel} /> + <SelectItems {pagination} /> + {/snippet} + {#snippet end()} + <AddButton title="Add Character" onclick={() => modals.open(AddCharacter)} /> + {/snippet} </Toolbar> {#if characters} - <Pagination /> + <Pagination {pagination} total={characters.count} /> <main> <Cardlets> {#each characters.edges as { id, name }, index (id)} - <Selectable {index} {id} {edit} let:handle let:selected> - <Cardlet {name} on:click={handle} filter="characters" {id}> - <SelectionOverlay slot="overlay" position="right" centered {selected} /> - </Cardlet> + <Selectable {index} {id} {edit}> + {#snippet children({ onclick, selected })} + <Cardlet {name} {onclick} filter="characters" {id}> + {#snippet overlay()} + <SelectionOverlay position="right" centered {selected} /> + {/snippet} + </Cardlet> + {/snippet} </Selectable> {:else} <Empty /> {/each} </Cardlets> </main> - <Pagination /> + <Pagination {pagination} total={characters.count} /> {:else} <Guard {result} /> {/if} diff --git a/frontend/src/routes/circles/+page.svelte b/frontend/src/routes/circles/+page.svelte index 14b0866..57520f8 100644 --- a/frontend/src/routes/circles/+page.svelte +++ b/frontend/src/routes/circles/+page.svelte @@ -3,10 +3,7 @@ import { circlesQuery, fetchCircle } from '$gql/Queries'; import type { Circle } from '$gql/graphql'; import { CircleSortLabel } from '$lib/Enums'; - import { BasicFilterContext, initFilterContext } from '$lib/Filter'; - import { initPaginationContext } from '$lib/Pagination'; - import { initSelectionContext } from '$lib/Selection'; - import { initSortContext } from '$lib/Sort'; + import { BasicFilterContext } from '$lib/Filter.svelte'; import { toastFinally } from '$lib/Toasts'; import AddButton from '$lib/components/AddButton.svelte'; import Cardlet from '$lib/components/Cardlet.svelte'; @@ -19,6 +16,7 @@ import EditCircle from '$lib/dialogs/EditCircle.svelte'; import Pagination from '$lib/pagination/Pagination.svelte'; import Selectable from '$lib/selection/Selectable.svelte'; + import { initSelectionContext } from '$lib/selection/Selection.svelte'; import SelectionOverlay from '$lib/selection/SelectionOverlay.svelte'; import DeleteSelection from '$lib/toolbar/DeleteSelection.svelte'; import Search from '$lib/toolbar/Search.svelte'; @@ -27,38 +25,32 @@ import SelectionControls from '$lib/toolbar/SelectionControls.svelte'; import Toolbar from '$lib/toolbar/Toolbar.svelte'; import { getContextClient } from '@urql/svelte'; - import { openModal } from 'svelte-modals'; - import type { PageData } from './$types'; + import { modals } from 'svelte-modals'; + import type { PageProps } from './$types'; + + let { data }: PageProps = $props(); + let pagination = $derived(data.pagination); + let sort = $derived(data.sort); const client = getContextClient(); - export let data: PageData; + let result = $derived(circlesQuery(client, { ...data })); + let circles = $derived($result.data?.circles); - $: result = circlesQuery(client, { - pagination: data.pagination, - filter: data.filter, - sort: data.sort + let selection = initSelectionContext<Circle>('Circle', (a) => a.name); + $effect(() => { + if (circles) { + selection.view = circles.edges; + } }); - $: circles = $result.data?.circles; - - const selection = initSelectionContext<Circle>('Circle', (c) => c.name); - $: if (circles) { - $selection.view = circles.edges; - $pagination.total = circles.count; - } - - const filter = initFilterContext<BasicFilterContext>(); - $: $filter = new BasicFilterContext(data.filter); - - const sort = initSortContext(data.sort, CircleSortLabel); - $: $sort.update = data.sort; - - const pagination = initPaginationContext(); - $: $pagination.update = data.pagination; + let filter = $state(new BasicFilterContext(data.filter)); + $effect(() => { + filter = new BasicFilterContext(data.filter); + }); const edit = (id: number) => { fetchCircle(client, id) - .then((circle) => openModal(EditCircle, { circle })) + .then((circle) => modals.open(EditCircle, { circle })) .catch(toastFinally); }; </script> @@ -67,34 +59,40 @@ <Column> <Toolbar> - <SelectionControls slot="start"> - <DeleteSelection mutation={deleteCircles} /> - </SelectionControls> - <svelte:fragment slot="center"> - <Search name="Circles" bind:field={$filter.include.controls.name.contains} /> - <SelectSort /> - <SelectItems /> - </svelte:fragment> - <svelte:fragment slot="end"> - <AddButton title="Add Circle" on:click={() => openModal(AddCircle)} /> - </svelte:fragment> + {#snippet start()} + <SelectionControls> + <DeleteSelection mutation={deleteCircles} /> + </SelectionControls> + {/snippet} + {#snippet center()} + <Search name="Circles" {filter} bind:field={filter.include.name.contains} /> + <SelectSort {sort} labels={CircleSortLabel} /> + <SelectItems {pagination} /> + {/snippet} + {#snippet end()} + <AddButton title="Add Circle" onclick={() => modals.open(AddCircle)} /> + {/snippet} </Toolbar> {#if circles} - <Pagination /> + <Pagination {pagination} total={circles.count} /> <main> <Cardlets> {#each circles.edges as { id, name }, index (id)} - <Selectable {index} {id} {edit} let:handle let:selected> - <Cardlet {name} on:click={handle} filter="circles" {id}> - <SelectionOverlay slot="overlay" position="right" centered {selected} /> - </Cardlet> + <Selectable {index} {id} {edit}> + {#snippet children({ onclick, selected })} + <Cardlet {name} {onclick} filter="circles" {id}> + {#snippet overlay()} + <SelectionOverlay position="right" centered {selected} /> + {/snippet} + </Cardlet> + {/snippet} </Selectable> {:else} <Empty /> {/each} </Cardlets> </main> - <Pagination /> + <Pagination {pagination} total={circles.count} /> {:else} <Guard {result} /> {/if} diff --git a/frontend/src/routes/comics/+page.svelte b/frontend/src/routes/comics/+page.svelte index 353d69c..372fd1a 100644 --- a/frontend/src/routes/comics/+page.svelte +++ b/frontend/src/routes/comics/+page.svelte @@ -3,10 +3,7 @@ import { comicsQuery } from '$gql/Queries'; import { type ComicFragment } from '$gql/graphql'; import { ComicSortLabel } from '$lib/Enums'; - import { ComicFilterContext, initFilterContext } from '$lib/Filter'; - import { initPaginationContext } from '$lib/Pagination'; - import { initSelectionContext } from '$lib/Selection'; - import { initSortContext } from '$lib/Sort'; + import { ComicFilterContext } from '$lib/Filter.svelte'; import Card, { comicCard } from '$lib/components/Card.svelte'; import Empty from '$lib/components/Empty.svelte'; import Guard from '$lib/components/Guard.svelte'; @@ -18,6 +15,7 @@ import Pagination from '$lib/pagination/Pagination.svelte'; import ComicPills from '$lib/pills/ComicPills.svelte'; import Selectable from '$lib/selection/Selectable.svelte'; + import { initSelectionContext } from '$lib/selection/Selection.svelte'; import SelectionOverlay from '$lib/selection/SelectionOverlay.svelte'; import DeleteSelection from '$lib/toolbar/DeleteSelection.svelte'; import EditSelection from '$lib/toolbar/EditSelection.svelte'; @@ -35,81 +33,82 @@ import ToggleAdvancedFilters from '$lib/toolbar/ToggleAdvancedFilters.svelte'; import Toolbar from '$lib/toolbar/Toolbar.svelte'; import { getContextClient } from '@urql/svelte'; - import type { PageData } from './$types'; + import type { PageProps } from './$types'; - export let data: PageData; + let { data }: PageProps = $props(); + let pagination = $derived(data.pagination); + let sort = $derived(data.sort); const client = getContextClient(); - - $: result = comicsQuery(client, { - pagination: data.pagination, - filter: data.filter, - sort: data.sort - }); - - $: comics = $result.data?.comics; + let result = $derived(comicsQuery(client, { ...data })); + let comics = $derived($result.data?.comics); const selection = initSelectionContext<ComicFragment>('Comic', (c) => c.title); - $: if (comics) { - $selection.view = comics.edges; - $pagination.total = comics.count; - } - - const filter = initFilterContext<ComicFilterContext>(); - $: $filter = new ComicFilterContext(data.filter); - - const sort = initSortContext(data.sort, ComicSortLabel); - $: $sort.update = data.sort; + $effect(() => { + if (comics) { + selection.view = comics.edges; + } + }); - const pagination = initPaginationContext(); - $: $pagination.update = data.pagination; + let filter = $state(new ComicFilterContext(data.filter)); + $effect(() => { + filter = new ComicFilterContext(data.filter); + }); </script> <Head section="Comics" /> <Column> <Toolbar> - <SelectionControls slot="start"> - <MarkSelection> - <MarkFavourite mutation={updateComics} /> - <hr class="col-span-2 border-slate-600" /> - <MarkBookmark mutation={updateComics} /> - <hr class="col-span-2 border-slate-600" /> - <MarkOrganized mutation={updateComics} /> - </MarkSelection> - <EditSelection dialog={UpdateComicsDialog} /> - <DeleteSelection mutation={deleteComics} /> - </SelectionControls> - <svelte:fragment slot="center"> - <Search name="Comics" bind:field={$filter.include.controls.title.contains} /> - <ToggleAdvancedFilters /> + {#snippet start()} + <SelectionControls> + <MarkSelection> + <MarkFavourite mutation={updateComics} /> + <hr class="col-span-2 border-slate-600" /> + <MarkBookmark mutation={updateComics} /> + <hr class="col-span-2 border-slate-600" /> + <MarkOrganized mutation={updateComics} /> + </MarkSelection> + <EditSelection dialog={UpdateComicsDialog} /> + <DeleteSelection mutation={deleteComics} /> + </SelectionControls> + {/snippet} + {#snippet center({ expanded, toggle })} + <Search name="Comics" {filter} bind:field={filter.include.title.contains} /> + <ToggleAdvancedFilters {expanded} {toggle} filterSize={filter.includes + filter.excludes} /> <div class="rounded-group flex"> - <FilterFavourites /> - <FilterBookmarked /> - <FilterOrganized /> + <FilterFavourites {filter} /> + <FilterBookmarked {filter} /> + <FilterOrganized {filter} /> </div> - <SelectSort /> - <SelectItems /> - </svelte:fragment> - <ComicFilterForm /> + <SelectSort {sort} labels={ComicSortLabel} /> + <SelectItems {pagination} /> + {/snippet} + {#snippet expansion()} + <ComicFilterForm {filter} /> + {/snippet} </Toolbar> {#if comics} - <Pagination /> + <Pagination {pagination} total={comics.count} /> <main> <Cards> {#each comics.edges as comic, index (comic.id)} - <Selectable {index} id={comic.id} let:handle let:selected> - <Card {...comicCard(comic)} on:click={handle}> - <SelectionOverlay position="left" {selected} slot="overlay" /> - <ComicPills {comic} /> - </Card> + <Selectable {index} id={comic.id}> + {#snippet children({ onclick, selected })} + <Card {...comicCard(comic)} {onclick}> + {#snippet overlay()} + <SelectionOverlay position="left" {selected} /> + {/snippet} + <ComicPills {comic} /> + </Card> + {/snippet} </Selectable> {:else} <Empty /> {/each} </Cards> </main> - <Pagination /> + <Pagination {pagination} total={comics.count} /> {:else} <Guard {result} /> {/if} diff --git a/frontend/src/routes/comics/[id]/+page.svelte b/frontend/src/routes/comics/[id]/+page.svelte index cfc5840..48c588f 100644 --- a/frontend/src/routes/comics/[id]/+page.svelte +++ b/frontend/src/routes/comics/[id]/+page.svelte @@ -2,12 +2,14 @@ import { beforeNavigate } from '$app/navigation'; import { updateComics } from '$gql/Mutations'; import { comicQuery } from '$gql/Queries'; - import { comicEquals } from '$gql/Utils'; - import { UpdateMode, type FullComicFragment, type UpdateComicInput } from '$gql/graphql'; - import { initReaderContext } from '$lib/Reader'; - import { initScraperContext } from '$lib/Scraper'; - import { initSelectionContext } from '$lib/Selection'; - import { setTabContext } from '$lib/Tabs'; + import { omitIdentifiers, type OmitIdentifiers } from '$gql/Utils'; + import { + UpdateMode, + type FullComicFragment, + type PageFragment, + type UpdateComicInput + } from '$gql/graphql'; + import { comicPending } from '$lib/Form'; import { toastFinally } from '$lib/Toasts'; import { preventOnPending } from '$lib/Utils'; import BookmarkButton from '$lib/components/BookmarkButton.svelte'; @@ -21,155 +23,140 @@ import ComicForm from '$lib/forms/ComicForm.svelte'; import Gallery from '$lib/gallery/Gallery.svelte'; import PageView from '$lib/reader/PageView.svelte'; - import Reader from '$lib/reader/Reader.svelte'; + import Reader, { initReaderContext } from '$lib/reader/Reader.svelte'; import ComicScrapeForm from '$lib/scraper/ComicScrapeForm.svelte'; + import { initScraperContext } from '$lib/scraper/Scraper.svelte'; + import { initSelectionContext } from '$lib/selection/Selection.svelte'; import ComicDelete from '$lib/tabs/ComicDelete.svelte'; import ComicDetails from '$lib/tabs/ComicDetails.svelte'; import Tab from '$lib/tabs/Tab.svelte'; import Tabs from '$lib/tabs/Tabs.svelte'; import SelectionControls from '$lib/toolbar/SelectionControls.svelte'; import { getContextClient } from '@urql/svelte'; - import type { PageData } from './$types'; + import { untrack } from 'svelte'; + import type { PageProps } from './$types'; + let { data }: PageProps = $props(); const client = getContextClient(); const reader = initReaderContext(); - const selection = initSelectionContext(); const scraper = initScraperContext(); - const tabContext = setTabContext({ - tabs: { - details: { title: 'Details' }, - edit: { title: 'Edit' }, - scrape: { title: 'Scrape' }, - deletion: { title: 'Delete' } - }, - current: 'details' - }); + const selection = initSelectionContext<PageFragment>('Page', (p) => p.path); - export let data: PageData; - $: result = comicQuery(client, { id: data.id }); - - let comic: FullComicFragment; - let original: Readonly<FullComicFragment>; - let updatePartial = false; - - $: $result, update(); - $: pending = !comicEquals(comic, original); - $: $tabContext.tabs.edit.badge = pending; - - function update() { - if (!$result.stale && $result.data?.comic.__typename === 'FullComic') { - original = $result.data.comic; - if (updatePartial) { - comic.pages = structuredClone(original.pages); - comic.favourite = original.favourite; - comic.bookmarked = original.bookmarked; - comic.organized = original.organized; - comic.updatedAt = original.updatedAt; - updatePartial = false; - } else { - comic = structuredClone(original); - } - - $reader.pages = original.pages; - $selection.view = comic.pages; - $scraper.selector = undefined; - } + let comic: FullComicFragment | undefined = $state(); + let input: OmitIdentifiers<FullComicFragment> | undefined = $state(); + let updateInput = $state(true); + + function invalidateInput() { + updateInput = true; } - function toggle(field: keyof Omit<UpdateComicInput, 'cover'>) { - updateComics(client, { ids: comic.id, input: { [field]: !comic[field] } }) - .then(() => (updatePartial = true)) - .catch(toastFinally); + function submit(input: UpdateComicInput) { + updateComics(client, { ids: data.id, input }).then(invalidateInput).catch(toastFinally); } - function updateComic(event: CustomEvent<UpdateComicInput>) { - updateComics(client, { ids: comic.id, input: event.detail }).catch(toastFinally); + function toggle(field: keyof Pick<UpdateComicInput, 'bookmarked' | 'favourite' | 'organized'>) { + if (!comic) return; + updateComics(client, { ids: data.id, input: { [field]: !comic[field] } }).catch(toastFinally); } - function updateCover(event: CustomEvent<number>) { - updateComics(client, { ids: comic.id, input: { cover: { id: event.detail } } }) - .then(() => (updatePartial = true)) - .catch(toastFinally); + function updateCover(id: number) { + updateComics(client, { ids: data.id, input: { cover: { id } } }).catch(toastFinally); } function removePages() { updateComics(client, { - ids: comic.id, - input: { pages: { ids: $selection.ids, options: { mode: UpdateMode.Remove } } } + ids: data.id, + input: { pages: { ids: selection.ids, options: { mode: UpdateMode.Remove } } } }) - .then(() => { - updatePartial = true; - $selection = $selection.clear(); - }) + .then(selection.clear) .catch(toastFinally); } beforeNavigate((navigation) => preventOnPending(navigation, pending)); + let result = $derived(comicQuery(client, { id: data.id })); + let pending = $derived(comicPending(comic, input)); + + $effect(() => { + if (!$result.stale) { + untrack(() => { + if ($result.data?.comic.__typename === 'FullComic') { + comic = $result.data.comic; + + if (updateInput) { + input = omitIdentifiers($result.data.comic); + updateInput = false; + } + + reader.pages = comic.pages; + selection.view = comic.pages; + scraper.reset(); + } + }); + } + }); </script> -<Head section="Comic" title={original?.title} /> +<Head section="Comic" title={comic?.title} /> -{#if comic} +{#if comic && input} <Grid> <header> <Titlebar - title={original.title} - subtitle={original.originalTitle} - bind:favourite={comic.favourite} - on:favourite={() => toggle('favourite')} + title={comic.title} + subtitle={comic.originalTitle} + favourite={comic.favourite} + onfavourite={() => toggle('favourite')} /> </header> <aside> - <Tabs> - <Tab id="details"> - <ComicDetails comic={original} /> + <Tabs badges={{ edit: pending }}> + <Tab initial id="details" title="Details"> + <ComicDetails {comic} /> </Tab> - <Tab id="edit"> + <Tab id="edit" title="Edit"> <div class="flex flex-col gap-4"> <div class="flex gap-2 text-sm"> <SelectionControls page> - <RemovePageButton on:click={removePages} /> + <RemovePageButton onclick={removePages} /> </SelectionControls> - <div class="grow" /> - <BookmarkButton bookmarked={comic.bookmarked} on:click={() => toggle('bookmarked')} /> - <OrganizedButton organized={comic.organized} on:click={() => toggle('organized')} /> + <div class="grow"></div> + <BookmarkButton bookmarked={comic.bookmarked} onclick={() => toggle('bookmarked')} /> + <OrganizedButton organized={comic.organized} onclick={() => toggle('organized')} /> </div> - <ComicForm bind:comic on:submit={updateComic}> + <ComicForm bind:input {submit}> <div class="flex gap-2"> - <div class="grow" /> - <SubmitButton active={pending} /> + <div class="grow"></div> + <SubmitButton {pending} /> </div> </ComicForm> </div> </Tab> - <Tab id="scrape"> - <ComicScrapeForm {comic} /> + <Tab id="scrape" title="Scrape"> + <ComicScrapeForm {comic} onupsert={invalidateInput} /> </Tab> - <Tab id="deletion"> + <Tab id="deletion" title="Delete"> <ComicDelete {comic} /> </Tab> </Tabs> </aside> <main class="overflow-auto"> - <Gallery - pages={comic.pages} - on:open={(e) => ($reader = $reader.open(e.detail))} - on:cover={updateCover} - /> + <Gallery pages={comic.pages} open={reader.open} {updateCover} /> </main> </Grid> <Reader> <PageView layout={comic.layout} direction={comic.direction} /> - <svelte:fragment slot="sidebar"> - <ComicForm bind:comic on:submit={updateComic}> - <div class="flex justify-end gap-2"> - <SubmitButton active={pending} /> - </div> - </ComicForm> - </svelte:fragment> + {#snippet sidebar()} + {#if input} + <ComicForm bind:input {submit}> + <div class="flex justify-end gap-2"> + <SubmitButton {pending} /> + </div> + </ComicForm> + {/if} + {/snippet} </Reader> {:else} <Guard {result} /> diff --git a/frontend/src/routes/namespaces/+page.svelte b/frontend/src/routes/namespaces/+page.svelte index f6568f9..04f7737 100644 --- a/frontend/src/routes/namespaces/+page.svelte +++ b/frontend/src/routes/namespaces/+page.svelte @@ -3,10 +3,7 @@ import { fetchNamespace, namespacesQuery } from '$gql/Queries'; import type { Namespace } from '$gql/graphql'; import { NamespaceSortLabel } from '$lib/Enums'; - import { BasicFilterContext, initFilterContext } from '$lib/Filter'; - import { initPaginationContext } from '$lib/Pagination'; - import { initSelectionContext } from '$lib/Selection'; - import { initSortContext } from '$lib/Sort'; + import { BasicFilterContext } from '$lib/Filter.svelte'; import { toastFinally } from '$lib/Toasts'; import AddButton from '$lib/components/AddButton.svelte'; import Cardlet from '$lib/components/Cardlet.svelte'; @@ -19,6 +16,7 @@ import EditNamespace from '$lib/dialogs/EditNamespace.svelte'; import Pagination from '$lib/pagination/Pagination.svelte'; import Selectable from '$lib/selection/Selectable.svelte'; + import { initSelectionContext } from '$lib/selection/Selection.svelte'; import SelectionOverlay from '$lib/selection/SelectionOverlay.svelte'; import DeleteSelection from '$lib/toolbar/DeleteSelection.svelte'; import Search from '$lib/toolbar/Search.svelte'; @@ -27,38 +25,32 @@ import SelectionControls from '$lib/toolbar/SelectionControls.svelte'; import Toolbar from '$lib/toolbar/Toolbar.svelte'; import { getContextClient } from '@urql/svelte'; - import { openModal } from 'svelte-modals'; - import type { PageData } from './$types'; + import { modals } from 'svelte-modals'; + import type { PageProps } from './$types'; + + let { data }: PageProps = $props(); + let pagination = $derived(data.pagination); + let sort = $derived(data.sort); const client = getContextClient(); - export let data: PageData; + let result = $derived(namespacesQuery(client, { ...data })); + let namespaces = $derived($result.data?.namespaces); - $: result = namespacesQuery(client, { - pagination: data.pagination, - filter: data.filter, - sort: data.sort + let selection = initSelectionContext<Namespace>('Namespace', (n) => n.name); + $effect(() => { + if (namespaces) { + selection.view = namespaces.edges; + } }); - $: namespaces = $result.data?.namespaces; - - const selection = initSelectionContext<Namespace>('Namespace', (n) => n.name); - $: if (namespaces) { - $selection.view = namespaces.edges; - $pagination.total = namespaces.count; - } - - const filter = initFilterContext<BasicFilterContext>(); - $: $filter = new BasicFilterContext(data.filter); - - const sort = initSortContext(data.sort, NamespaceSortLabel); - $: $sort.update = data.sort; - - const pagination = initPaginationContext(); - $: $pagination.update = data.pagination; + let filter = $state(new BasicFilterContext(data.filter)); + $effect(() => { + filter = new BasicFilterContext(data.filter); + }); const edit = (id: number) => { fetchNamespace(client, id) - .then((namespace) => openModal(EditNamespace, { namespace })) + .then((namespace) => modals.open(EditNamespace, { namespace })) .catch(toastFinally); }; </script> @@ -67,34 +59,40 @@ <Column> <Toolbar> - <SelectionControls slot="start"> - <DeleteSelection mutation={deleteNamespaces} /> - </SelectionControls> - <svelte:fragment slot="center"> - <Search name="Namespaces" bind:field={$filter.include.controls.name.contains} /> - <SelectSort /> - <SelectItems /> - </svelte:fragment> - <svelte:fragment slot="end"> - <AddButton title="Add Namespace" on:click={() => openModal(AddNamespace)} /> - </svelte:fragment> + {#snippet start()} + <SelectionControls> + <DeleteSelection mutation={deleteNamespaces} /> + </SelectionControls> + {/snippet} + {#snippet center()} + <Search name="Namespaces" {filter} bind:field={filter.include.name.contains} /> + <SelectSort {sort} labels={NamespaceSortLabel} /> + <SelectItems {pagination} /> + {/snippet} + {#snippet end()} + <AddButton title="Add Namespace" onclick={() => modals.open(AddNamespace)} /> + {/snippet} </Toolbar> {#if namespaces} - <Pagination /> + <Pagination {pagination} total={namespaces.count} /> <main> <Cardlets> {#each namespaces.edges as { id, name }, index (id)} - <Selectable {index} {id} {edit} let:handle let:selected> - <Cardlet {name} on:click={handle} filter="tags" id={`${id}:`}> - <SelectionOverlay slot="overlay" position="right" centered {selected} /> - </Cardlet> + <Selectable {index} {id} {edit}> + {#snippet children({ onclick, selected })} + <Cardlet {name} {onclick} filter="tags" id={`${id}:`}> + {#snippet overlay()} + <SelectionOverlay position="right" centered {selected} /> + {/snippet} + </Cardlet> + {/snippet} </Selectable> {:else} <Empty /> {/each} </Cardlets> </main> - <Pagination /> + <Pagination {pagination} total={namespaces.count} /> {:else} <Guard {result} /> {/if} diff --git a/frontend/src/routes/statistics/+page.svelte b/frontend/src/routes/statistics/+page.svelte index 7497bcf..1a5bb27 100644 --- a/frontend/src/routes/statistics/+page.svelte +++ b/frontend/src/routes/statistics/+page.svelte @@ -5,8 +5,8 @@ import StatGroup from '$lib/statistics/StatGroup.svelte'; import { getContextClient } from '@urql/svelte'; - $: query = statisticsQuery(getContextClient()); - $: totals = $query.data?.statistics.total; + let query = $derived(statisticsQuery(getContextClient())); + let totals = $derived($query.data?.statistics.total); </script> <Head section="Statistics" /> diff --git a/frontend/src/routes/tags/+page.svelte b/frontend/src/routes/tags/+page.svelte index e0909ad..30554c7 100644 --- a/frontend/src/routes/tags/+page.svelte +++ b/frontend/src/routes/tags/+page.svelte @@ -3,10 +3,7 @@ import { fetchTag, tagsQuery } from '$gql/Queries'; import { type Tag } from '$gql/graphql'; import { TagSortLabel } from '$lib/Enums'; - import { TagFilterContext, initFilterContext } from '$lib/Filter'; - import { initPaginationContext } from '$lib/Pagination'; - import { initSelectionContext } from '$lib/Selection'; - import { initSortContext } from '$lib/Sort'; + import { TagFilterContext } from '$lib/Filter.svelte'; import { toastFinally } from '$lib/Toasts'; import AddButton from '$lib/components/AddButton.svelte'; import Cardlet from '$lib/components/Cardlet.svelte'; @@ -17,10 +14,11 @@ import Column from '$lib/containers/Column.svelte'; import AddTag from '$lib/dialogs/AddTag.svelte'; import EditTag from '$lib/dialogs/EditTag.svelte'; - import UpdateTagsDialog from '$lib/dialogs/UpdateTags.svelte'; + import UpdateTags from '$lib/dialogs/UpdateTags.svelte'; import TagFilterForm from '$lib/filter/TagFilterForm.svelte'; import Pagination from '$lib/pagination/Pagination.svelte'; import Selectable from '$lib/selection/Selectable.svelte'; + import { initSelectionContext } from '$lib/selection/Selection.svelte'; import SelectionOverlay from '$lib/selection/SelectionOverlay.svelte'; import DeleteSelection from '$lib/toolbar/DeleteSelection.svelte'; import EditSelection from '$lib/toolbar/EditSelection.svelte'; @@ -31,39 +29,33 @@ import ToggleAdvancedFilters from '$lib/toolbar/ToggleAdvancedFilters.svelte'; import Toolbar from '$lib/toolbar/Toolbar.svelte'; import { getContextClient } from '@urql/svelte'; - import { openModal } from 'svelte-modals'; - import type { PageData } from './$types'; + import { modals } from 'svelte-modals'; + import type { PageProps } from './$types'; - const client = getContextClient(); - - export let data: PageData; + let { data }: PageProps = $props(); + let pagination = $derived(data.pagination); + let sort = $derived(data.sort); - $: result = tagsQuery(client, { - pagination: data.pagination, - filter: data.filter, - sort: data.sort - }); - - $: tags = $result.data?.tags; + const client = getContextClient(); + let result = $derived(tagsQuery(client, { ...data })); + let tags = $derived($result.data?.tags); const selection = initSelectionContext<Tag>('Tag', (t) => t.name); - $: if (tags) { - $selection.view = tags.edges; - $pagination.total = tags.count; - } - - const filter = initFilterContext<TagFilterContext>(); - $: $filter = new TagFilterContext(data.filter); - - const sort = initSortContext(data.sort, TagSortLabel); - $: $sort.update = data.sort; + $effect(() => { + if (tags) { + selection.view = tags.edges; + } + }); - const pagination = initPaginationContext(); - $: $pagination.update = data.pagination; + let filter = $state(new TagFilterContext(data.filter)); + let filterSize = $derived(filter.includes + filter.excludes); + $effect(() => { + filter = new TagFilterContext(data.filter); + }); const edit = (id: number) => { fetchTag(client, id) - .then((tag) => openModal(EditTag, { tag })) + .then((tag) => modals.open(EditTag, { tag })) .catch(toastFinally); }; </script> @@ -72,37 +64,45 @@ <Column> <Toolbar> - <SelectionControls slot="start"> - <EditSelection dialog={UpdateTagsDialog} /> - <DeleteSelection mutation={deleteTags} /> - </SelectionControls> - <svelte:fragment slot="center"> - <Search name="Tags" bind:field={$filter.include.controls.name.contains} /> - <ToggleAdvancedFilters /> - <SelectSort /> - <SelectItems /> - </svelte:fragment> - <svelte:fragment slot="end"> - <AddButton title="Add Tag" on:click={() => openModal(AddTag)} /> - </svelte:fragment> - <TagFilterForm /> + {#snippet start()} + <SelectionControls> + <EditSelection dialog={UpdateTags} /> + <DeleteSelection mutation={deleteTags} /> + </SelectionControls> + {/snippet} + {#snippet center({ expanded, toggle })} + <Search name="Tags" {filter} bind:field={filter.include.name.contains} /> + <ToggleAdvancedFilters {expanded} {toggle} {filterSize} /> + <SelectSort {sort} labels={TagSortLabel} /> + <SelectItems {pagination} /> + {/snippet} + {#snippet end()} + <AddButton title="Add Tag" onclick={() => modals.open(AddTag)} /> + {/snippet} + {#snippet expansion()} + <TagFilterForm {filter} /> + {/snippet} </Toolbar> {#if tags} - <Pagination /> + <Pagination {pagination} total={tags.count} /> <main> <Cardlets> {#each tags.edges as { id, name, description }, index (id)} - <Selectable {index} {id} {edit} let:handle let:selected> - <Cardlet {name} title={description} on:click={handle} filter="tags" id={`:${id}`}> - <SelectionOverlay slot="overlay" position="right" centered {selected} /> - </Cardlet> + <Selectable {index} {id} {edit}> + {#snippet children({ onclick, selected })} + <Cardlet {name} title={description} {onclick} filter="tags" id={`:${id}`}> + {#snippet overlay()} + <SelectionOverlay position="right" centered {selected} /> + {/snippet} + </Cardlet> + {/snippet} </Selectable> {:else} <Empty /> {/each} </Cardlets> </main> - <Pagination /> + <Pagination {pagination} total={tags.count} /> {:else} <Guard {result} /> {/if} diff --git a/frontend/src/routes/worlds/+page.svelte b/frontend/src/routes/worlds/+page.svelte index e0366e9..f223a61 100644 --- a/frontend/src/routes/worlds/+page.svelte +++ b/frontend/src/routes/worlds/+page.svelte @@ -3,10 +3,7 @@ import { fetchWorld, worldsQuery } from '$gql/Queries'; import type { World } from '$gql/graphql'; import { WorldSortLabel } from '$lib/Enums'; - import { BasicFilterContext, initFilterContext } from '$lib/Filter'; - import { initPaginationContext } from '$lib/Pagination'; - import { initSelectionContext } from '$lib/Selection'; - import { initSortContext } from '$lib/Sort'; + import { BasicFilterContext } from '$lib/Filter.svelte'; import { toastFinally } from '$lib/Toasts'; import AddButton from '$lib/components/AddButton.svelte'; import Cardlet from '$lib/components/Cardlet.svelte'; @@ -19,6 +16,7 @@ import EditWorld from '$lib/dialogs/EditWorld.svelte'; import Pagination from '$lib/pagination/Pagination.svelte'; import Selectable from '$lib/selection/Selectable.svelte'; + import { initSelectionContext } from '$lib/selection/Selection.svelte'; import SelectionOverlay from '$lib/selection/SelectionOverlay.svelte'; import DeleteSelection from '$lib/toolbar/DeleteSelection.svelte'; import Search from '$lib/toolbar/Search.svelte'; @@ -27,75 +25,74 @@ import SelectionControls from '$lib/toolbar/SelectionControls.svelte'; import Toolbar from '$lib/toolbar/Toolbar.svelte'; import { getContextClient } from '@urql/svelte'; - import { openModal } from 'svelte-modals'; - import type { PageData } from './$types'; + import { modals } from 'svelte-modals'; + import type { PageProps } from './$types'; - const client = getContextClient(); + let { data }: PageProps = $props(); + let pagination = $derived(data.pagination); + let sort = $derived(data.sort); - export let data: PageData; + const client = getContextClient(); + let result = $derived(worldsQuery(client, { ...data })); + let worlds = $derived($result.data?.worlds); - $: result = worldsQuery(client, { - pagination: data.pagination, - filter: data.filter, - sort: data.sort + let selection = initSelectionContext<World>('World', (a) => a.name); + $effect(() => { + if (worlds) { + selection.view = worlds.edges; + } }); - $: worlds = $result.data?.worlds; - - const selection = initSelectionContext<World>('World', (w) => w.name); - $: if (worlds) { - $selection.view = worlds.edges; - $pagination.total = worlds.count; - } - - const filter = initFilterContext<BasicFilterContext>(); - $: $filter = new BasicFilterContext(data.filter); - - const sort = initSortContext(data.sort, WorldSortLabel); - $: $sort.update = data.sort; - - const pagination = initPaginationContext(); - $: $pagination.update = data.pagination; + let filter = $state(new BasicFilterContext(data.filter)); + $effect(() => { + filter = new BasicFilterContext(data.filter); + }); const edit = (id: number) => { fetchWorld(client, id) - .then((world) => openModal(EditWorld, { world })) + .then((world) => modals.open(EditWorld, { world })) .catch(toastFinally); }; </script> -<Head section="Worlds" /> +<Head section="worlds" /> <Column> <Toolbar> - <SelectionControls slot="start"> - <DeleteSelection mutation={deleteWorlds} /> - </SelectionControls> - <svelte:fragment slot="center"> - <Search name="Worlds" bind:field={$filter.include.controls.name.contains} /> - <SelectSort /> - <SelectItems /> - </svelte:fragment> - <svelte:fragment slot="end"> - <AddButton title="Add World" on:click={() => openModal(AddWorld)} /> - </svelte:fragment> + {#snippet start()} + <SelectionControls> + <DeleteSelection mutation={deleteWorlds} /> + </SelectionControls> + {/snippet} + {#snippet center()} + <Search name="Worlds" {filter} bind:field={filter.include.name.contains} /> + <SelectSort {sort} labels={WorldSortLabel} /> + <SelectItems {pagination} /> + {/snippet} + {#snippet end()} + <AddButton title="Add World" onclick={() => modals.open(AddWorld)} /> + {/snippet} </Toolbar> {#if worlds} - <Pagination /> + <Pagination {pagination} total={worlds.count} /> <main> <Cardlets> {#each worlds.edges as { id, name }, index (id)} - <Selectable {index} {id} {edit} let:handle let:selected> - <Cardlet {name} on:click={handle} filter="worlds" {id}> - <SelectionOverlay slot="overlay" position="right" centered {selected} /> - </Cardlet> + <Selectable {index} {id} {edit}> + {#snippet children({ onclick, selected })} + <Cardlet {name} {onclick} filter="worlds" {id}> + {#snippet overlay()} + <SelectionOverlay position="right" centered {selected} /> + {/snippet} + </Cardlet> + {/snippet} </Selectable> {:else} <Empty /> {/each} </Cardlets> </main> - <Pagination /> + <Pagination {pagination} total={worlds.count} /> {:else} <Guard {result} /> {/if} diff --git a/frontend/tests/Reader.test.ts b/frontend/tests/Reader.test.ts index e12d69b..a4928af 100644 --- a/frontend/tests/Reader.test.ts +++ b/frontend/tests/Reader.test.ts @@ -1,5 +1,6 @@ import { Layout, type PageFragment } from '$gql/graphql'; -import { partition, type Chunk } from '$lib/Reader'; +import { partition, type Chunk } from '$lib/reader/Reader.svelte'; + import { expect, test } from 'vitest'; const normalAttrs = { aspectRatio: 0.7, width: 140, height: 200 }; diff --git a/frontend/tests/Selection.test.ts b/frontend/tests/Selection.test.ts index c7847cd..1bdbb55 100644 --- a/frontend/tests/Selection.test.ts +++ b/frontend/tests/Selection.test.ts @@ -1,4 +1,4 @@ -import { ItemSelection } from '$lib/Selection'; +import { ItemSelection } from '$lib/selection/Selection.svelte'; import { expect, test } from 'vitest'; interface TestItem { @@ -17,100 +17,104 @@ const all = items.map((i) => i.id); const selectable = items.filter((i) => i.selectable).map((i) => i.id); const setup = () => { - const selection = new ItemSelection<TestItem>(); + const selection = new ItemSelection<TestItem>('test', (t) => t.id.toString()); selection.view = items; return selection; }; test('selects a single item', () => { - let selection = setup(); + const selection = setup(); - selection = selection.update(0, false); + selection.update(0, false); expect(selection.ids).toStrictEqual([items[0].id]); }); test('selects a single item (with empty shift select)', () => { - let selection = setup(); + const selection = setup(); - selection = selection.update(0, true); + selection.update(0, true); expect(selection.ids).toStrictEqual([items[0].id]); }); test('selects multiple items (forwards)', () => { - let selection = setup(); + const selection = setup(); - selection = selection.update(0, false); - selection = selection.update(2, true); + selection.update(0, false); + selection.update(2, true); expect(selection.ids.toSorted((a, b) => a - b)).toStrictEqual(all.slice(0, 3)); }); test('selects multiple items (backwards)', () => { - let selection = setup(); + const selection = setup(); - selection = selection.update(2, false); - selection = selection.update(0, true); + selection.update(2, false); + selection.update(0, true); expect(selection.ids.toSorted((a, b) => a - b)).toStrictEqual(all.slice(0, 3)); }); test('selects multiple items (only selectables)', () => { - let selection = setup(); + const selection = setup(); selection.selectable = (i) => i.selectable; - selection = selection.update(0, false); - selection = selection.update(3, true); + selection.update(0, false); + selection.update(3, true); expect(selection.ids).toStrictEqual(selectable); }); test('selects all', () => { - const selection = setup().all(); + const selection = setup(); + + selection.all(); expect(selection.ids).toStrictEqual(all); }); test('selects all selectables', () => { - let selection = setup(); + const selection = setup(); selection.selectable = (i) => i.selectable; - selection = selection.all(); + selection.all(); expect(selection.ids).toStrictEqual(selectable); }); test('deselects all', () => { - let selection = setup().all(); + const selection = setup(); - selection = selection.none(); + selection.all(); + selection.none(); expect(selection.ids).toHaveLength(0); }); test('deselects a single item', () => { - let selection = setup().all(); + const selection = setup(); - selection = selection.update(0, false); + selection.all(); + selection.update(0, false); expect(selection.ids).toStrictEqual(all.slice(1)); }); test('deselects multiple items', () => { - let selection = setup(); + const selection = setup(); - selection = selection.update(0, false); - selection = selection.update(2, true); - selection = selection.update(2, true); + selection.update(0, false); + selection.update(2, true); + selection.update(2, true); expect(selection.ids).toHaveLength(0); }); test('retains selection', () => { - let selection = setup(); + const selection = setup(); - selection = selection.all(); + selection.all(); selection.view = items.slice(0, 2); expect(selection.ids).toStrictEqual(all.slice(0, 2)); @@ -125,40 +129,40 @@ test('is inactive by default', () => { }); test('is inactive after clearing', () => { - let selection = setup(); + const selection = setup(); selection.active = true; - selection = selection.clear(); + selection.clear(); expect(selection.active).toBeFalsy(); }); test('can be toggled', () => { - let selection = setup(); + const selection = setup(); - selection = selection.toggle(); + selection.toggle(); expect(selection.active).toBe(true); - selection = selection.all(); - selection = selection.toggle(); + selection.all(); + selection.toggle(); expect(selection.active).toBe(false); expect(selection.ids).toHaveLength(0); }); test('can be cleared', () => { - let selection = setup(); + const selection = setup(); - selection = selection.all(); + selection.all(); - selection = selection.clear(); + selection.clear(); expect(selection.ids).toHaveLength(0); }); test('reports selected items', () => { - let selection = setup(); + const selection = setup(); - selection = selection.update(0, false); - selection = selection.update(2, false); + selection.update(0, false); + selection.update(2, false); expect(selection.contains(all[0])).toBeTruthy(); expect(selection.contains(all[1])).toBeFalsy(); @@ -167,13 +171,17 @@ test('reports selected items', () => { }); test('reports size', () => { - const selection = setup().all(); + const selection = setup(); + + selection.all(); expect(selection.size).toBe(all.length); }); test('reports size of visible items', () => { - const selection = setup().all(); + const selection = setup(); + + selection.all(); selection.view = items.slice(0, 2); expect(selection.size).toBe(2); diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index 95c4753..a0d6da6 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -10,7 +10,9 @@ "sourceMap": true, "strict": true, "target": "ES2022", - "module": "ES2022" + "module": "ES2022", + "verbatimModuleSyntax": true, + "isolatedModules": true } // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias // diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 145901f..7905397 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -3,9 +3,6 @@ import { defineConfig } from 'vite'; export default defineConfig({ plugins: [sveltekit()], - server: { - fs: { - allow: ['objects'] - } - } + server: { fs: { allow: ['objects'] } }, + test: { environment: 'jsdom' } }); |