diff options
| author | ivarlovlie <git@ivarlovlie.no> | 2023-02-25 13:15:44 +0100 |
|---|---|---|
| committer | ivarlovlie <git@ivarlovlie.no> | 2023-02-25 13:15:44 +0100 |
| commit | 900bb5e845c3ad44defbd427cae3d44a4a43321f (patch) | |
| tree | df3d96a93771884add571e82336c29fc3d9c7a1c /code/app | |
| download | greatoffice-900bb5e845c3ad44defbd427cae3d44a4a43321f.tar.xz greatoffice-900bb5e845c3ad44defbd427cae3d44a4a43321f.zip | |
feat: Initial commit
Diffstat (limited to 'code/app')
151 files changed, 10556 insertions, 0 deletions
diff --git a/code/app/.dockerignore b/code/app/.dockerignore new file mode 100644 index 0000000..00774fa --- /dev/null +++ b/code/app/.dockerignore @@ -0,0 +1,7 @@ +.env +.env-example +.svelte-kit +.git +static +node_modules +build
\ No newline at end of file diff --git a/code/app/.env-example b/code/app/.env-example new file mode 100644 index 0000000..270860f --- /dev/null +++ b/code/app/.env-example @@ -0,0 +1,4 @@ +VITE_LOG_LEVEL=DEBUG +VITE_TESTING=true +VITE_TEST_USERNAME="ms@test.tld" +VITE_TEST_PASSWORD="test123"
\ No newline at end of file diff --git a/code/app/.gitignore b/code/app/.gitignore new file mode 100644 index 0000000..f4401a3 --- /dev/null +++ b/code/app/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +node_modules +/build +/.svelte-kit +/package +.env +.env.* +!.env.example diff --git a/code/app/.npmrc b/code/app/.npmrc new file mode 100644 index 0000000..b6f27f1 --- /dev/null +++ b/code/app/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/code/app/.typesafe-i18n.json b/code/app/.typesafe-i18n.json new file mode 100644 index 0000000..38fa5ca --- /dev/null +++ b/code/app/.typesafe-i18n.json @@ -0,0 +1,5 @@ +{ + "adapter": "svelte", + "$schema": "https://unpkg.com/typesafe-i18n@5.24.0/schema/typesafe-i18n.json", + "outputPath": "src/i18n" +}
\ No newline at end of file diff --git a/code/app/.version b/code/app/.version new file mode 100644 index 0000000..626799f --- /dev/null +++ b/code/app/.version @@ -0,0 +1 @@ +v1 diff --git a/code/app/.version-dev b/code/app/.version-dev new file mode 100644 index 0000000..c227083 --- /dev/null +++ b/code/app/.version-dev @@ -0,0 +1 @@ +0
\ No newline at end of file diff --git a/code/app/Dockerfile b/code/app/Dockerfile new file mode 100644 index 0000000..e381c8d --- /dev/null +++ b/code/app/Dockerfile @@ -0,0 +1,13 @@ +FROM registry.hub.docker.com/library/node:lts-buster-slim AS builder +WORKDIR . +COPY package.json . +RUN npm i +COPY . . +RUN npm run build +FROM registry.hub.docker.com/library/node:lts-buster-slim +USER node:node +WORKDIR . +COPY --from=builder --chown=node:node build build +COPY --from=builder --chown=node:node node_modules node_modules +COPY --chown=node:node package.json . +CMD ["node","build"]
\ No newline at end of file diff --git a/code/app/build_and_push.sh b/code/app/build_and_push.sh new file mode 100755 index 0000000..6143419 --- /dev/null +++ b/code/app/build_and_push.sh @@ -0,0 +1,89 @@ +#!/usr/bin/env bash + +set -Eueo pipefail + +CURRENT_DEV_VERSION=$(cat .version-dev) +CURRENT_DEV_VERSION_INT=${CURRENT_DEV_VERSION//[!0-9]/} +CURRENT_VERSION=$(cat .version) +CURRENT_VERSION_INT=${CURRENT_VERSION//[!0-9]/} +if [ ${1-prod} == "dev" ]; then + NEW_VERSION="v$((CURRENT_DEV_VERSION_INT + 1))-dev" + OLD_VERSION=$CURRENT_DEV_VERSION +else + NEW_VERSION="v$((CURRENT_VERSION_INT + 1))" + OLD_VERSION=$CURRENT_VERSION +fi +IMAGE_NAME="greatoffice/app" +HUB_NAME="dr.ivar.systems/greatoffice/app" + +# Check for uncommited changes and optionally commit them +if [ "$(git status --untracked-files=no --porcelain)" ]; then + echo "Unclean git tree! press CTRL+C to exit or press ENTER to commit and push to the default branch" + read -n 1 + + read -p "Enter commit message: " COMMIT_MESSAGE + git add .. + git commit --quiet -m "$COMMIT_MESSAGE" +fi + +if [ ${1-prod} == "dev" ]; then + echo $NEW_VERSION >|.version-dev + git add .version-dev +else + echo $NEW_VERSION >|.version + git add .version +fi + +echo "Starting build of $HUB_NAME:$NEW_VERSION at $(date -u)..." +echo + +# Put version.txt inside of server +pushd static +echo "$NEW_VERSION" >version.txt +git add version.txt +popd + +git commit --quiet -m "chore(release): Bump version" + +read -p "Do you want to tag this build? (y/n) " -n 1 -r +echo +if [[ $REPLY =~ ^[Yy]$ ]]; then + read -p "Enter tag message (can be empty): " TAG_MESSAGE + git tag -am "$TAG_MESSAGE" $NEW_VERSION +fi + +read -p "Do you want to push the latest commits and tags to origin? (y/n) " -n 1 -r +echo +if [[ $REPLY =~ ^[Yy]$ ]]; then + echo "Pushing latest changes to remotes..." + echo + git push --quiet --follow-tags +fi + +# Build docker image +echo "Building docker image" +echo + +docker build -t $IMAGE_NAME:$NEW_VERSION . + +docker tag $IMAGE_NAME:$NEW_VERSION $HUB_NAME:$NEW_VERSION + +if [ ${1-prod} == "dev" ]; then + docker tag $IMAGE_NAME:$NEW_VERSION $HUB_NAME:latest-dev +fi +if [ ${1-prod} == "prod" ]; then + docker tag $IMAGE_NAME:$NEW_VERSION $HUB_NAME:latest +fi + +# Optionally push images to docker registry +echo "Press CTRL+C to exit or press ENTER to push docker image to registry" +read -n 1 +docker push $HUB_NAME:$NEW_VERSION + +if [ ${1-prod} == "dev" ]; then + docker push $HUB_NAME:latest-dev +fi + +if [ ${1-prod} == "prod" ]; then + docker push $HUB_NAME:latest +fi diff --git a/code/app/package.json b/code/app/package.json new file mode 100644 index 0000000..a868844 --- /dev/null +++ b/code/app/package.json @@ -0,0 +1,50 @@ +{ + "name": "greatoffice-kit", + "version": "0.0.1", + "private": true, + "type": "module", + "scripts": { + "dev": "npm-run-all --parallel vite typesafe-i18n", + "typesafe-i18n": "typesafe-i18n", + "vite": "vite", + "build": "vite build", + "preview": "vite preview", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + "test": "playwright test" + }, + "devDependencies": { + "@faker-js/faker": "^7.6.0", + "@playwright/test": "^1.30.0", + "@sveltejs/adapter-node": "1.1.7", + "@sveltejs/kit": "1.5.0", + "@tailwindcss/forms": "^0.5.3", + "@types/js-cookie": "^3.0.2", + "@vite-pwa/sveltekit": "^0.1.3", + "autoprefixer": "^10.4.13", + "npm-run-all": "^4.1.5", + "pino-pretty": "^9.1.1", + "postcss": "^8.4.21", + "postcss-load-config": "^4.0.1", + "svelte": "^3.55.1", + "svelte-check": "^3.0.3", + "svelte-preprocess": "^5.0.1", + "tailwindcss": "^3.2.6", + "tslib": "^2.5.0", + "typesafe-i18n": "^5.24.0", + "typescript": "^4.9.5", + "vite": "^4.1.1", + "vite-plugin-pwa": "^0.14.3" + }, + "dependencies": { + "@developermuch/dev-svelte-headlessui": "0.0.1", + "@rgossiaux/svelte-headlessui": "^1.0.2", + "fuzzysort": "^2.0.4", + "js-cookie": "^3.0.1", + "pino": "^8.9.0", + "pino-seq": "^0.9.0", + "svelte-headless-table": "^0.17.2", + "temporal-polyfill": "^0.0.8", + "turbo-query": "^1.9.0" + } +}
\ No newline at end of file diff --git a/code/app/playwright.config.ts b/code/app/playwright.config.ts new file mode 100644 index 0000000..22c46d9 --- /dev/null +++ b/code/app/playwright.config.ts @@ -0,0 +1,10 @@ +import type { PlaywrightTestConfig } from '@playwright/test'; + +const config: PlaywrightTestConfig = { + webServer: { + command: 'pnpm run build && pnpm run preview', + port: 4173 + } +}; + +export default config; diff --git a/code/app/pnpm-lock.yaml b/code/app/pnpm-lock.yaml new file mode 100644 index 0000000..04e088e --- /dev/null +++ b/code/app/pnpm-lock.yaml @@ -0,0 +1,4389 @@ +lockfileVersion: 5.4 + +specifiers: + '@developermuch/dev-svelte-headlessui': 0.0.1 + '@faker-js/faker': ^7.6.0 + '@playwright/test': ^1.30.0 + '@rgossiaux/svelte-headlessui': ^1.0.2 + '@sveltejs/adapter-node': 1.1.7 + '@sveltejs/kit': 1.5.0 + '@tailwindcss/forms': ^0.5.3 + '@types/js-cookie': ^3.0.2 + '@vite-pwa/sveltekit': ^0.1.3 + autoprefixer: ^10.4.13 + fuzzysort: ^2.0.4 + js-cookie: ^3.0.1 + npm-run-all: ^4.1.5 + pino: ^8.9.0 + pino-pretty: ^9.1.1 + pino-seq: ^0.9.0 + postcss: ^8.4.21 + postcss-load-config: ^4.0.1 + svelte: ^3.55.1 + svelte-check: ^3.0.3 + svelte-headless-table: ^0.17.2 + svelte-preprocess: ^5.0.1 + tailwindcss: ^3.2.6 + temporal-polyfill: ^0.0.8 + tslib: ^2.5.0 + turbo-query: ^1.9.0 + typesafe-i18n: ^5.24.0 + typescript: ^4.9.5 + vite: ^4.1.1 + vite-plugin-pwa: ^0.14.3 + +dependencies: + '@developermuch/dev-svelte-headlessui': 0.0.1_svelte@3.55.1 + '@rgossiaux/svelte-headlessui': 1.0.2_svelte@3.55.1 + fuzzysort: 2.0.4 + js-cookie: 3.0.1 + pino: 8.9.0 + pino-seq: 0.9.0 + svelte-headless-table: 0.17.2_svelte@3.55.1 + temporal-polyfill: 0.0.8 + turbo-query: 1.9.0 + +devDependencies: + '@faker-js/faker': 7.6.0 + '@playwright/test': 1.30.0 + '@sveltejs/adapter-node': 1.1.7_@sveltejs+kit@1.5.0 + '@sveltejs/kit': 1.5.0_svelte@3.55.1+vite@4.1.1 + '@tailwindcss/forms': 0.5.3_tailwindcss@3.2.6 + '@types/js-cookie': 3.0.2 + '@vite-pwa/sveltekit': 0.1.3_kkbswr7m3b4a2hnxfqban7fxma + autoprefixer: 10.4.13_postcss@8.4.21 + npm-run-all: 4.1.5 + pino-pretty: 9.1.1 + postcss: 8.4.21 + postcss-load-config: 4.0.1_postcss@8.4.21 + svelte: 3.55.1 + svelte-check: 3.0.3_gqx7lw3sljhsd4bstor5m2aa2u + svelte-preprocess: 5.0.1_b25a55hc532q56kmuqlrolam2i + tailwindcss: 3.2.6_postcss@8.4.21 + tslib: 2.5.0 + typesafe-i18n: 5.24.0_typescript@4.9.5 + typescript: 4.9.5 + vite: 4.1.1 + vite-plugin-pwa: 0.14.3_vite@4.1.1 + +packages: + + /@ampproject/remapping/2.2.0: + resolution: {integrity: sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==} + engines: {node: '>=6.0.0'} + dependencies: + '@jridgewell/gen-mapping': 0.1.1 + '@jridgewell/trace-mapping': 0.3.17 + dev: true + + /@apideck/better-ajv-errors/0.3.6_ajv@8.12.0: + resolution: {integrity: sha512-P+ZygBLZtkp0qqOAJJVX4oX/sFo5JR3eBWwwuqHHhK0GIgQOKWrAfiAaWX0aArHkRWHMuggFEgAZNxVPwPZYaA==} + engines: {node: '>=10'} + peerDependencies: + ajv: '>=8' + dependencies: + ajv: 8.12.0 + json-schema: 0.4.0 + jsonpointer: 5.0.1 + leven: 3.1.0 + dev: true + + /@babel/code-frame/7.18.6: + resolution: {integrity: sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/highlight': 7.18.6 + dev: true + + /@babel/compat-data/7.20.14: + resolution: {integrity: sha512-0YpKHD6ImkWMEINCyDAD0HLLUH/lPCefG8ld9it8DJB2wnApraKuhgYTvTY1z7UFIfBTGy5LwncZ+5HWWGbhFw==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/core/7.20.12: + resolution: {integrity: sha512-XsMfHovsUYHFMdrIHkZphTN/2Hzzi78R08NuHfDBehym2VsPDL6Zn/JAD/JQdnRvbSsbQc4mVaU1m6JgtTEElg==} + engines: {node: '>=6.9.0'} + dependencies: + '@ampproject/remapping': 2.2.0 + '@babel/code-frame': 7.18.6 + '@babel/generator': 7.20.14 + '@babel/helper-compilation-targets': 7.20.7_@babel+core@7.20.12 + '@babel/helper-module-transforms': 7.20.11 + '@babel/helpers': 7.20.13 + '@babel/parser': 7.20.15 + '@babel/template': 7.20.7 + '@babel/traverse': 7.20.13 + '@babel/types': 7.20.7 + convert-source-map: 1.9.0 + debug: 4.3.4 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/generator/7.20.14: + resolution: {integrity: sha512-AEmuXHdcD3A52HHXxaTmYlb8q/xMEhoRP67B3T4Oq7lbmSoqroMZzjnGj3+i1io3pdnF8iBYVu4Ilj+c4hBxYg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.20.7 + '@jridgewell/gen-mapping': 0.3.2 + jsesc: 2.5.2 + dev: true + + /@babel/helper-annotate-as-pure/7.18.6: + resolution: {integrity: sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.20.7 + dev: true + + /@babel/helper-builder-binary-assignment-operator-visitor/7.18.9: + resolution: {integrity: sha512-yFQ0YCHoIqarl8BCRwBL8ulYUaZpz3bNsA7oFepAzee+8/+ImtADXNOmO5vJvsPff3qi+hvpkY/NYBTrBQgdNw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-explode-assignable-expression': 7.18.6 + '@babel/types': 7.20.7 + dev: true + + /@babel/helper-compilation-targets/7.20.7_@babel+core@7.20.12: + resolution: {integrity: sha512-4tGORmfQcrc+bvrjb5y3dG9Mx1IOZjsHqQVUz7XCNHO+iTmqxWnVg3KRygjGmpRLJGdQSKuvFinbIb0CnZwHAQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/compat-data': 7.20.14 + '@babel/core': 7.20.12 + '@babel/helper-validator-option': 7.18.6 + browserslist: 4.21.5 + lru-cache: 5.1.1 + semver: 6.3.0 + dev: true + + /@babel/helper-create-class-features-plugin/7.20.12_@babel+core@7.20.12: + resolution: {integrity: sha512-9OunRkbT0JQcednL0UFvbfXpAsUXiGjUk0a7sN8fUXX7Mue79cUSMjHGDRRi/Vz9vYlpIhLV5fMD5dKoMhhsNQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-annotate-as-pure': 7.18.6 + '@babel/helper-environment-visitor': 7.18.9 + '@babel/helper-function-name': 7.19.0 + '@babel/helper-member-expression-to-functions': 7.20.7 + '@babel/helper-optimise-call-expression': 7.18.6 + '@babel/helper-replace-supers': 7.20.7 + '@babel/helper-skip-transparent-expression-wrappers': 7.20.0 + '@babel/helper-split-export-declaration': 7.18.6 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/helper-create-regexp-features-plugin/7.20.5_@babel+core@7.20.12: + resolution: {integrity: sha512-m68B1lkg3XDGX5yCvGO0kPx3v9WIYLnzjKfPcQiwntEQa5ZeRkPmo2X/ISJc8qxWGfwUr+kvZAeEzAwLec2r2w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-annotate-as-pure': 7.18.6 + regexpu-core: 5.3.0 + dev: true + + /@babel/helper-define-polyfill-provider/0.3.3_@babel+core@7.20.12: + resolution: {integrity: sha512-z5aQKU4IzbqCC1XH0nAqfsFLMVSo22SBKUc0BxGrLkolTdPTructy0ToNnlO2zA4j9Q/7pjMZf0DSY+DSTYzww==} + peerDependencies: + '@babel/core': ^7.4.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-compilation-targets': 7.20.7_@babel+core@7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + debug: 4.3.4 + lodash.debounce: 4.0.8 + resolve: 1.22.1 + semver: 6.3.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/helper-environment-visitor/7.18.9: + resolution: {integrity: sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-explode-assignable-expression/7.18.6: + resolution: {integrity: sha512-eyAYAsQmB80jNfg4baAtLeWAQHfHFiR483rzFK+BhETlGZaQC9bsfrugfXDCbRHLQbIA7U5NxhhOxN7p/dWIcg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.20.7 + dev: true + + /@babel/helper-function-name/7.19.0: + resolution: {integrity: sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/template': 7.20.7 + '@babel/types': 7.20.7 + dev: true + + /@babel/helper-hoist-variables/7.18.6: + resolution: {integrity: sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.20.7 + dev: true + + /@babel/helper-member-expression-to-functions/7.20.7: + resolution: {integrity: sha512-9J0CxJLq315fEdi4s7xK5TQaNYjZw+nDVpVqr1axNGKzdrdwYBD5b4uKv3n75aABG0rCCTK8Im8Ww7eYfMrZgw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.20.7 + dev: true + + /@babel/helper-module-imports/7.18.6: + resolution: {integrity: sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.20.7 + dev: true + + /@babel/helper-module-transforms/7.20.11: + resolution: {integrity: sha512-uRy78kN4psmji1s2QtbtcCSaj/LILFDp0f/ymhpQH5QY3nljUZCaNWz9X1dEj/8MBdBEFECs7yRhKn8i7NjZgg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-environment-visitor': 7.18.9 + '@babel/helper-module-imports': 7.18.6 + '@babel/helper-simple-access': 7.20.2 + '@babel/helper-split-export-declaration': 7.18.6 + '@babel/helper-validator-identifier': 7.19.1 + '@babel/template': 7.20.7 + '@babel/traverse': 7.20.13 + '@babel/types': 7.20.7 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/helper-optimise-call-expression/7.18.6: + resolution: {integrity: sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.20.7 + dev: true + + /@babel/helper-plugin-utils/7.20.2: + resolution: {integrity: sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-remap-async-to-generator/7.18.9_@babel+core@7.20.12: + resolution: {integrity: sha512-dI7q50YKd8BAv3VEfgg7PS7yD3Rtbi2J1XMXaalXO0W0164hYLnh8zpjRS0mte9MfVp/tltvr/cfdXPvJr1opA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-annotate-as-pure': 7.18.6 + '@babel/helper-environment-visitor': 7.18.9 + '@babel/helper-wrap-function': 7.20.5 + '@babel/types': 7.20.7 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/helper-replace-supers/7.20.7: + resolution: {integrity: sha512-vujDMtB6LVfNW13jhlCrp48QNslK6JXi7lQG736HVbHz/mbf4Dc7tIRh1Xf5C0rF7BP8iiSxGMCmY6Ci1ven3A==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-environment-visitor': 7.18.9 + '@babel/helper-member-expression-to-functions': 7.20.7 + '@babel/helper-optimise-call-expression': 7.18.6 + '@babel/template': 7.20.7 + '@babel/traverse': 7.20.13 + '@babel/types': 7.20.7 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/helper-simple-access/7.20.2: + resolution: {integrity: sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.20.7 + dev: true + + /@babel/helper-skip-transparent-expression-wrappers/7.20.0: + resolution: {integrity: sha512-5y1JYeNKfvnT8sZcK9DVRtpTbGiomYIHviSP3OQWmDPU3DeH4a1ZlT/N2lyQ5P8egjcRaT/Y9aNqUxK0WsnIIg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.20.7 + dev: true + + /@babel/helper-split-export-declaration/7.18.6: + resolution: {integrity: sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.20.7 + dev: true + + /@babel/helper-string-parser/7.19.4: + resolution: {integrity: sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-validator-identifier/7.19.1: + resolution: {integrity: sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-validator-option/7.18.6: + resolution: {integrity: sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-wrap-function/7.20.5: + resolution: {integrity: sha512-bYMxIWK5mh+TgXGVqAtnu5Yn1un+v8DDZtqyzKRLUzrh70Eal2O3aZ7aPYiMADO4uKlkzOiRiZ6GX5q3qxvW9Q==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-function-name': 7.19.0 + '@babel/template': 7.20.7 + '@babel/traverse': 7.20.13 + '@babel/types': 7.20.7 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/helpers/7.20.13: + resolution: {integrity: sha512-nzJ0DWCL3gB5RCXbUO3KIMMsBY2Eqbx8mBpKGE/02PgyRQFcPQLbkQ1vyy596mZLaP+dAfD+R4ckASzNVmW3jg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/template': 7.20.7 + '@babel/traverse': 7.20.13 + '@babel/types': 7.20.7 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/highlight/7.18.6: + resolution: {integrity: sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-validator-identifier': 7.19.1 + chalk: 2.4.2 + js-tokens: 4.0.0 + dev: true + + /@babel/parser/7.20.15: + resolution: {integrity: sha512-DI4a1oZuf8wC+oAJA9RW6ga3Zbe8RZFt7kD9i4qAspz3I/yHet1VvC3DiSy/fsUvv5pvJuNPh0LPOdCcqinDPg==} + engines: {node: '>=6.0.0'} + hasBin: true + dependencies: + '@babel/types': 7.20.7 + dev: true + + /@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/7.18.6_@babel+core@7.20.12: + resolution: {integrity: sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/7.20.7_@babel+core@7.20.12: + resolution: {integrity: sha512-sbr9+wNE5aXMBBFBICk01tt7sBf2Oc9ikRFEcem/ZORup9IMUdNhW7/wVLEbbtlWOsEubJet46mHAL2C8+2jKQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.13.0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-skip-transparent-expression-wrappers': 7.20.0 + '@babel/plugin-proposal-optional-chaining': 7.20.7_@babel+core@7.20.12 + dev: true + + /@babel/plugin-proposal-async-generator-functions/7.20.7_@babel+core@7.20.12: + resolution: {integrity: sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-environment-visitor': 7.18.9 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-remap-async-to-generator': 7.18.9_@babel+core@7.20.12 + '@babel/plugin-syntax-async-generators': 7.8.4_@babel+core@7.20.12 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-proposal-class-properties/7.18.6_@babel+core@7.20.12: + resolution: {integrity: sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-create-class-features-plugin': 7.20.12_@babel+core@7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-proposal-class-static-block/7.20.7_@babel+core@7.20.12: + resolution: {integrity: sha512-AveGOoi9DAjUYYuUAG//Ig69GlazLnoyzMw68VCDux+c1tsnnH/OkYcpz/5xzMkEFC6UxjR5Gw1c+iY2wOGVeQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.12.0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-create-class-features-plugin': 7.20.12_@babel+core@7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/plugin-syntax-class-static-block': 7.14.5_@babel+core@7.20.12 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-proposal-dynamic-import/7.18.6_@babel+core@7.20.12: + resolution: {integrity: sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/plugin-syntax-dynamic-import': 7.8.3_@babel+core@7.20.12 + dev: true + + /@babel/plugin-proposal-export-namespace-from/7.18.9_@babel+core@7.20.12: + resolution: {integrity: sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/plugin-syntax-export-namespace-from': 7.8.3_@babel+core@7.20.12 + dev: true + + /@babel/plugin-proposal-json-strings/7.18.6_@babel+core@7.20.12: + resolution: {integrity: sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/plugin-syntax-json-strings': 7.8.3_@babel+core@7.20.12 + dev: true + + /@babel/plugin-proposal-logical-assignment-operators/7.20.7_@babel+core@7.20.12: + resolution: {integrity: sha512-y7C7cZgpMIjWlKE5T7eJwp+tnRYM89HmRvWM5EQuB5BoHEONjmQ8lSNmBUwOyy/GFRsohJED51YBF79hE1djug==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4_@babel+core@7.20.12 + dev: true + + /@babel/plugin-proposal-nullish-coalescing-operator/7.18.6_@babel+core@7.20.12: + resolution: {integrity: sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3_@babel+core@7.20.12 + dev: true + + /@babel/plugin-proposal-numeric-separator/7.18.6_@babel+core@7.20.12: + resolution: {integrity: sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/plugin-syntax-numeric-separator': 7.10.4_@babel+core@7.20.12 + dev: true + + /@babel/plugin-proposal-object-rest-spread/7.20.7_@babel+core@7.20.12: + resolution: {integrity: sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/compat-data': 7.20.14 + '@babel/core': 7.20.12 + '@babel/helper-compilation-targets': 7.20.7_@babel+core@7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/plugin-syntax-object-rest-spread': 7.8.3_@babel+core@7.20.12 + '@babel/plugin-transform-parameters': 7.20.7_@babel+core@7.20.12 + dev: true + + /@babel/plugin-proposal-optional-catch-binding/7.18.6_@babel+core@7.20.12: + resolution: {integrity: sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/plugin-syntax-optional-catch-binding': 7.8.3_@babel+core@7.20.12 + dev: true + + /@babel/plugin-proposal-optional-chaining/7.20.7_@babel+core@7.20.12: + resolution: {integrity: sha512-T+A7b1kfjtRM51ssoOfS1+wbyCVqorfyZhT99TvxxLMirPShD8CzKMRepMlCBGM5RpHMbn8s+5MMHnPstJH6mQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-skip-transparent-expression-wrappers': 7.20.0 + '@babel/plugin-syntax-optional-chaining': 7.8.3_@babel+core@7.20.12 + dev: true + + /@babel/plugin-proposal-private-methods/7.18.6_@babel+core@7.20.12: + resolution: {integrity: sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-create-class-features-plugin': 7.20.12_@babel+core@7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-proposal-private-property-in-object/7.20.5_@babel+core@7.20.12: + resolution: {integrity: sha512-Vq7b9dUA12ByzB4EjQTPo25sFhY+08pQDBSZRtUAkj7lb7jahaHR5igera16QZ+3my1nYR4dKsNdYj5IjPHilQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-annotate-as-pure': 7.18.6 + '@babel/helper-create-class-features-plugin': 7.20.12_@babel+core@7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/plugin-syntax-private-property-in-object': 7.14.5_@babel+core@7.20.12 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-proposal-unicode-property-regex/7.18.6_@babel+core@7.20.12: + resolution: {integrity: sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==} + engines: {node: '>=4'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-create-regexp-features-plugin': 7.20.5_@babel+core@7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-syntax-async-generators/7.8.4_@babel+core@7.20.12: + resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-syntax-class-properties/7.12.13_@babel+core@7.20.12: + resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-syntax-class-static-block/7.14.5_@babel+core@7.20.12: + resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-syntax-dynamic-import/7.8.3_@babel+core@7.20.12: + resolution: {integrity: sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-syntax-export-namespace-from/7.8.3_@babel+core@7.20.12: + resolution: {integrity: sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-syntax-import-assertions/7.20.0_@babel+core@7.20.12: + resolution: {integrity: sha512-IUh1vakzNoWalR8ch/areW7qFopR2AEw03JlG7BbrDqmQ4X3q9uuipQwSGrUn7oGiemKjtSLDhNtQHzMHr1JdQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-syntax-json-strings/7.8.3_@babel+core@7.20.12: + resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-syntax-logical-assignment-operators/7.10.4_@babel+core@7.20.12: + resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-syntax-nullish-coalescing-operator/7.8.3_@babel+core@7.20.12: + resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-syntax-numeric-separator/7.10.4_@babel+core@7.20.12: + resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-syntax-object-rest-spread/7.8.3_@babel+core@7.20.12: + resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-syntax-optional-catch-binding/7.8.3_@babel+core@7.20.12: + resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-syntax-optional-chaining/7.8.3_@babel+core@7.20.12: + resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-syntax-private-property-in-object/7.14.5_@babel+core@7.20.12: + resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-syntax-top-level-await/7.14.5_@babel+core@7.20.12: + resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-transform-arrow-functions/7.20.7_@babel+core@7.20.12: + resolution: {integrity: sha512-3poA5E7dzDomxj9WXWwuD6A5F3kc7VXwIJO+E+J8qtDtS+pXPAhrgEyh+9GBwBgPq1Z+bB+/JD60lp5jsN7JPQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-transform-async-to-generator/7.20.7_@babel+core@7.20.12: + resolution: {integrity: sha512-Uo5gwHPT9vgnSXQxqGtpdufUiWp96gk7yiP4Mp5bm1QMkEmLXBO7PAGYbKoJ6DhAwiNkcHFBol/x5zZZkL/t0Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-module-imports': 7.18.6 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-remap-async-to-generator': 7.18.9_@babel+core@7.20.12 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-block-scoped-functions/7.18.6_@babel+core@7.20.12: + resolution: {integrity: sha512-ExUcOqpPWnliRcPqves5HJcJOvHvIIWfuS4sroBUenPuMdmW+SMHDakmtS7qOo13sVppmUijqeTv7qqGsvURpQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-transform-block-scoping/7.20.15_@babel+core@7.20.12: + resolution: {integrity: sha512-Vv4DMZ6MiNOhu/LdaZsT/bsLRxgL94d269Mv4R/9sp6+Mp++X/JqypZYypJXLlM4mlL352/Egzbzr98iABH1CA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-transform-classes/7.20.7_@babel+core@7.20.12: + resolution: {integrity: sha512-LWYbsiXTPKl+oBlXUGlwNlJZetXD5Am+CyBdqhPsDVjM9Jc8jwBJFrKhHf900Kfk2eZG1y9MAG3UNajol7A4VQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-annotate-as-pure': 7.18.6 + '@babel/helper-compilation-targets': 7.20.7_@babel+core@7.20.12 + '@babel/helper-environment-visitor': 7.18.9 + '@babel/helper-function-name': 7.19.0 + '@babel/helper-optimise-call-expression': 7.18.6 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-replace-supers': 7.20.7 + '@babel/helper-split-export-declaration': 7.18.6 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-computed-properties/7.20.7_@babel+core@7.20.12: + resolution: {integrity: sha512-Lz7MvBK6DTjElHAmfu6bfANzKcxpyNPeYBGEafyA6E5HtRpjpZwU+u7Qrgz/2OR0z+5TvKYbPdphfSaAcZBrYQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/template': 7.20.7 + dev: true + + /@babel/plugin-transform-destructuring/7.20.7_@babel+core@7.20.12: + resolution: {integrity: sha512-Xwg403sRrZb81IVB79ZPqNQME23yhugYVqgTxAhT99h485F4f+GMELFhhOsscDUB7HCswepKeCKLn/GZvUKoBA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-transform-dotall-regex/7.18.6_@babel+core@7.20.12: + resolution: {integrity: sha512-6S3jpun1eEbAxq7TdjLotAsl4WpQI9DxfkycRcKrjhQYzU87qpXdknpBg/e+TdcMehqGnLFi7tnFUBR02Vq6wg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-create-regexp-features-plugin': 7.20.5_@babel+core@7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-transform-duplicate-keys/7.18.9_@babel+core@7.20.12: + resolution: {integrity: sha512-d2bmXCtZXYc59/0SanQKbiWINadaJXqtvIQIzd4+hNwkWBgyCd5F/2t1kXoUdvPMrxzPvhK6EMQRROxsue+mfw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-transform-exponentiation-operator/7.18.6_@babel+core@7.20.12: + resolution: {integrity: sha512-wzEtc0+2c88FVR34aQmiz56dxEkxr2g8DQb/KfaFa1JYXOFVsbhvAonFN6PwVWj++fKmku8NP80plJ5Et4wqHw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-builder-binary-assignment-operator-visitor': 7.18.9 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-transform-for-of/7.18.8_@babel+core@7.20.12: + resolution: {integrity: sha512-yEfTRnjuskWYo0k1mHUqrVWaZwrdq8AYbfrpqULOJOaucGSp4mNMVps+YtA8byoevxS/urwU75vyhQIxcCgiBQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-transform-function-name/7.18.9_@babel+core@7.20.12: + resolution: {integrity: sha512-WvIBoRPaJQ5yVHzcnJFor7oS5Ls0PYixlTYE63lCj2RtdQEl15M68FXQlxnG6wdraJIXRdR7KI+hQ7q/9QjrCQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-compilation-targets': 7.20.7_@babel+core@7.20.12 + '@babel/helper-function-name': 7.19.0 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-transform-literals/7.18.9_@babel+core@7.20.12: + resolution: {integrity: sha512-IFQDSRoTPnrAIrI5zoZv73IFeZu2dhu6irxQjY9rNjTT53VmKg9fenjvoiOWOkJ6mm4jKVPtdMzBY98Fp4Z4cg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-transform-member-expression-literals/7.18.6_@babel+core@7.20.12: + resolution: {integrity: sha512-qSF1ihLGO3q+/g48k85tUjD033C29TNTVB2paCwZPVmOsjn9pClvYYrM2VeJpBY2bcNkuny0YUyTNRyRxJ54KA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-transform-modules-amd/7.20.11_@babel+core@7.20.12: + resolution: {integrity: sha512-NuzCt5IIYOW0O30UvqktzHYR2ud5bOWbY0yaxWZ6G+aFzOMJvrs5YHNikrbdaT15+KNO31nPOy5Fim3ku6Zb5g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-module-transforms': 7.20.11 + '@babel/helper-plugin-utils': 7.20.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-modules-commonjs/7.20.11_@babel+core@7.20.12: + resolution: {integrity: sha512-S8e1f7WQ7cimJQ51JkAaDrEtohVEitXjgCGAS2N8S31Y42E+kWwfSz83LYz57QdBm7q9diARVqanIaH2oVgQnw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-module-transforms': 7.20.11 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-simple-access': 7.20.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-modules-systemjs/7.20.11_@babel+core@7.20.12: + resolution: {integrity: sha512-vVu5g9BPQKSFEmvt2TA4Da5N+QVS66EX21d8uoOihC+OCpUoGvzVsXeqFdtAEfVa5BILAeFt+U7yVmLbQnAJmw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-hoist-variables': 7.18.6 + '@babel/helper-module-transforms': 7.20.11 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-validator-identifier': 7.19.1 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-modules-umd/7.18.6_@babel+core@7.20.12: + resolution: {integrity: sha512-dcegErExVeXcRqNtkRU/z8WlBLnvD4MRnHgNs3MytRO1Mn1sHRyhbcpYbVMGclAqOjdW+9cfkdZno9dFdfKLfQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-module-transforms': 7.20.11 + '@babel/helper-plugin-utils': 7.20.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-named-capturing-groups-regex/7.20.5_@babel+core@7.20.12: + resolution: {integrity: sha512-mOW4tTzi5iTLnw+78iEq3gr8Aoq4WNRGpmSlrogqaiCBoR1HFhpU4JkpQFOHfeYx3ReVIFWOQJS4aZBRvuZ6mA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-create-regexp-features-plugin': 7.20.5_@babel+core@7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-transform-new-target/7.18.6_@babel+core@7.20.12: + resolution: {integrity: sha512-DjwFA/9Iu3Z+vrAn+8pBUGcjhxKguSMlsFqeCKbhb9BAV756v0krzVK04CRDi/4aqmk8BsHb4a/gFcaA5joXRw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-transform-object-super/7.18.6_@babel+core@7.20.12: + resolution: {integrity: sha512-uvGz6zk+pZoS1aTZrOvrbj6Pp/kK2mp45t2B+bTDre2UgsZZ8EZLSJtUg7m/no0zOJUWgFONpB7Zv9W2tSaFlA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-replace-supers': 7.20.7 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-parameters/7.20.7_@babel+core@7.20.12: + resolution: {integrity: sha512-WiWBIkeHKVOSYPO0pWkxGPfKeWrCJyD3NJ53+Lrp/QMSZbsVPovrVl2aWZ19D/LTVnaDv5Ap7GJ/B2CTOZdrfA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-transform-property-literals/7.18.6_@babel+core@7.20.12: + resolution: {integrity: sha512-cYcs6qlgafTud3PAzrrRNbQtfpQ8+y/+M5tKmksS9+M1ckbH6kzY8MrexEM9mcA6JDsukE19iIRvAyYl463sMg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-transform-regenerator/7.20.5_@babel+core@7.20.12: + resolution: {integrity: sha512-kW/oO7HPBtntbsahzQ0qSE3tFvkFwnbozz3NWFhLGqH75vLEg+sCGngLlhVkePlCs3Jv0dBBHDzCHxNiFAQKCQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + regenerator-transform: 0.15.1 + dev: true + + /@babel/plugin-transform-reserved-words/7.18.6_@babel+core@7.20.12: + resolution: {integrity: sha512-oX/4MyMoypzHjFrT1CdivfKZ+XvIPMFXwwxHp/r0Ddy2Vuomt4HDFGmft1TAY2yiTKiNSsh3kjBAzcM8kSdsjA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-transform-shorthand-properties/7.18.6_@babel+core@7.20.12: + resolution: {integrity: sha512-eCLXXJqv8okzg86ywZJbRn19YJHU4XUa55oz2wbHhaQVn/MM+XhukiT7SYqp/7o00dg52Rj51Ny+Ecw4oyoygw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-transform-spread/7.20.7_@babel+core@7.20.12: + resolution: {integrity: sha512-ewBbHQ+1U/VnH1fxltbJqDeWBU1oNLG8Dj11uIv3xVf7nrQu0bPGe5Rf716r7K5Qz+SqtAOVswoVunoiBtGhxw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-skip-transparent-expression-wrappers': 7.20.0 + dev: true + + /@babel/plugin-transform-sticky-regex/7.18.6_@babel+core@7.20.12: + resolution: {integrity: sha512-kfiDrDQ+PBsQDO85yj1icueWMfGfJFKN1KCkndygtu/C9+XUfydLC8Iv5UYJqRwy4zk8EcplRxEOeLyjq1gm6Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-transform-template-literals/7.18.9_@babel+core@7.20.12: + resolution: {integrity: sha512-S8cOWfT82gTezpYOiVaGHrCbhlHgKhQt8XH5ES46P2XWmX92yisoZywf5km75wv5sYcXDUCLMmMxOLCtthDgMA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-transform-typeof-symbol/7.18.9_@babel+core@7.20.12: + resolution: {integrity: sha512-SRfwTtF11G2aemAZWivL7PD+C9z52v9EvMqH9BuYbabyPuKUvSWks3oCg6041pT925L4zVFqaVBeECwsmlguEw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-transform-unicode-escapes/7.18.10_@babel+core@7.20.12: + resolution: {integrity: sha512-kKAdAI+YzPgGY/ftStBFXTI1LZFju38rYThnfMykS+IXy8BVx+res7s2fxf1l8I35DV2T97ezo6+SGrXz6B3iQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/plugin-transform-unicode-regex/7.18.6_@babel+core@7.20.12: + resolution: {integrity: sha512-gE7A6Lt7YLnNOL3Pb9BNeZvi+d8l7tcRrG4+pwJjK9hD2xX4mEvjlQW60G9EEmfXVYRPv9VRQcyegIVHCql/AA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-create-regexp-features-plugin': 7.20.5_@babel+core@7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + dev: true + + /@babel/preset-env/7.20.2_@babel+core@7.20.12: + resolution: {integrity: sha512-1G0efQEWR1EHkKvKHqbG+IN/QdgwfByUpM5V5QroDzGV2t3S/WXNQd693cHiHTlCFMpr9B6FkPFXDA2lQcKoDg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/compat-data': 7.20.14 + '@babel/core': 7.20.12 + '@babel/helper-compilation-targets': 7.20.7_@babel+core@7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-validator-option': 7.18.6 + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.18.6_@babel+core@7.20.12 + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.20.7_@babel+core@7.20.12 + '@babel/plugin-proposal-async-generator-functions': 7.20.7_@babel+core@7.20.12 + '@babel/plugin-proposal-class-properties': 7.18.6_@babel+core@7.20.12 + '@babel/plugin-proposal-class-static-block': 7.20.7_@babel+core@7.20.12 + '@babel/plugin-proposal-dynamic-import': 7.18.6_@babel+core@7.20.12 + '@babel/plugin-proposal-export-namespace-from': 7.18.9_@babel+core@7.20.12 + '@babel/plugin-proposal-json-strings': 7.18.6_@babel+core@7.20.12 + '@babel/plugin-proposal-logical-assignment-operators': 7.20.7_@babel+core@7.20.12 + '@babel/plugin-proposal-nullish-coalescing-operator': 7.18.6_@babel+core@7.20.12 + '@babel/plugin-proposal-numeric-separator': 7.18.6_@babel+core@7.20.12 + '@babel/plugin-proposal-object-rest-spread': 7.20.7_@babel+core@7.20.12 + '@babel/plugin-proposal-optional-catch-binding': 7.18.6_@babel+core@7.20.12 + '@babel/plugin-proposal-optional-chaining': 7.20.7_@babel+core@7.20.12 + '@babel/plugin-proposal-private-methods': 7.18.6_@babel+core@7.20.12 + '@babel/plugin-proposal-private-property-in-object': 7.20.5_@babel+core@7.20.12 + '@babel/plugin-proposal-unicode-property-regex': 7.18.6_@babel+core@7.20.12 + '@babel/plugin-syntax-async-generators': 7.8.4_@babel+core@7.20.12 + '@babel/plugin-syntax-class-properties': 7.12.13_@babel+core@7.20.12 + '@babel/plugin-syntax-class-static-block': 7.14.5_@babel+core@7.20.12 + '@babel/plugin-syntax-dynamic-import': 7.8.3_@babel+core@7.20.12 + '@babel/plugin-syntax-export-namespace-from': 7.8.3_@babel+core@7.20.12 + '@babel/plugin-syntax-import-assertions': 7.20.0_@babel+core@7.20.12 + '@babel/plugin-syntax-json-strings': 7.8.3_@babel+core@7.20.12 + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4_@babel+core@7.20.12 + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3_@babel+core@7.20.12 + '@babel/plugin-syntax-numeric-separator': 7.10.4_@babel+core@7.20.12 + '@babel/plugin-syntax-object-rest-spread': 7.8.3_@babel+core@7.20.12 + '@babel/plugin-syntax-optional-catch-binding': 7.8.3_@babel+core@7.20.12 + '@babel/plugin-syntax-optional-chaining': 7.8.3_@babel+core@7.20.12 + '@babel/plugin-syntax-private-property-in-object': 7.14.5_@babel+core@7.20.12 + '@babel/plugin-syntax-top-level-await': 7.14.5_@babel+core@7.20.12 + '@babel/plugin-transform-arrow-functions': 7.20.7_@babel+core@7.20.12 + '@babel/plugin-transform-async-to-generator': 7.20.7_@babel+core@7.20.12 + '@babel/plugin-transform-block-scoped-functions': 7.18.6_@babel+core@7.20.12 + '@babel/plugin-transform-block-scoping': 7.20.15_@babel+core@7.20.12 + '@babel/plugin-transform-classes': 7.20.7_@babel+core@7.20.12 + '@babel/plugin-transform-computed-properties': 7.20.7_@babel+core@7.20.12 + '@babel/plugin-transform-destructuring': 7.20.7_@babel+core@7.20.12 + '@babel/plugin-transform-dotall-regex': 7.18.6_@babel+core@7.20.12 + '@babel/plugin-transform-duplicate-keys': 7.18.9_@babel+core@7.20.12 + '@babel/plugin-transform-exponentiation-operator': 7.18.6_@babel+core@7.20.12 + '@babel/plugin-transform-for-of': 7.18.8_@babel+core@7.20.12 + '@babel/plugin-transform-function-name': 7.18.9_@babel+core@7.20.12 + '@babel/plugin-transform-literals': 7.18.9_@babel+core@7.20.12 + '@babel/plugin-transform-member-expression-literals': 7.18.6_@babel+core@7.20.12 + '@babel/plugin-transform-modules-amd': 7.20.11_@babel+core@7.20.12 + '@babel/plugin-transform-modules-commonjs': 7.20.11_@babel+core@7.20.12 + '@babel/plugin-transform-modules-systemjs': 7.20.11_@babel+core@7.20.12 + '@babel/plugin-transform-modules-umd': 7.18.6_@babel+core@7.20.12 + '@babel/plugin-transform-named-capturing-groups-regex': 7.20.5_@babel+core@7.20.12 + '@babel/plugin-transform-new-target': 7.18.6_@babel+core@7.20.12 + '@babel/plugin-transform-object-super': 7.18.6_@babel+core@7.20.12 + '@babel/plugin-transform-parameters': 7.20.7_@babel+core@7.20.12 + '@babel/plugin-transform-property-literals': 7.18.6_@babel+core@7.20.12 + '@babel/plugin-transform-regenerator': 7.20.5_@babel+core@7.20.12 + '@babel/plugin-transform-reserved-words': 7.18.6_@babel+core@7.20.12 + '@babel/plugin-transform-shorthand-properties': 7.18.6_@babel+core@7.20.12 + '@babel/plugin-transform-spread': 7.20.7_@babel+core@7.20.12 + '@babel/plugin-transform-sticky-regex': 7.18.6_@babel+core@7.20.12 + '@babel/plugin-transform-template-literals': 7.18.9_@babel+core@7.20.12 + '@babel/plugin-transform-typeof-symbol': 7.18.9_@babel+core@7.20.12 + '@babel/plugin-transform-unicode-escapes': 7.18.10_@babel+core@7.20.12 + '@babel/plugin-transform-unicode-regex': 7.18.6_@babel+core@7.20.12 + '@babel/preset-modules': 0.1.5_@babel+core@7.20.12 + '@babel/types': 7.20.7 + babel-plugin-polyfill-corejs2: 0.3.3_@babel+core@7.20.12 + babel-plugin-polyfill-corejs3: 0.6.0_@babel+core@7.20.12 + babel-plugin-polyfill-regenerator: 0.4.1_@babel+core@7.20.12 + core-js-compat: 3.27.2 + semver: 6.3.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/preset-modules/0.1.5_@babel+core@7.20.12: + resolution: {integrity: sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/plugin-proposal-unicode-property-regex': 7.18.6_@babel+core@7.20.12 + '@babel/plugin-transform-dotall-regex': 7.18.6_@babel+core@7.20.12 + '@babel/types': 7.20.7 + esutils: 2.0.3 + dev: true + + /@babel/regjsgen/0.8.0: + resolution: {integrity: sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==} + dev: true + + /@babel/runtime/7.20.13: + resolution: {integrity: sha512-gt3PKXs0DBoL9xCvOIIZ2NEqAGZqHjAnmVbfQtB620V0uReIQutpel14KcneZuer7UioY8ALKZ7iocavvzTNFA==} + engines: {node: '>=6.9.0'} + dependencies: + regenerator-runtime: 0.13.11 + dev: true + + /@babel/template/7.20.7: + resolution: {integrity: sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.18.6 + '@babel/parser': 7.20.15 + '@babel/types': 7.20.7 + dev: true + + /@babel/traverse/7.20.13: + resolution: {integrity: sha512-kMJXfF0T6DIS9E8cgdLCSAL+cuCK+YEZHWiLK0SXpTo8YRj5lpJu3CDNKiIBCne4m9hhTIqUg6SYTAI39tAiVQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.18.6 + '@babel/generator': 7.20.14 + '@babel/helper-environment-visitor': 7.18.9 + '@babel/helper-function-name': 7.19.0 + '@babel/helper-hoist-variables': 7.18.6 + '@babel/helper-split-export-declaration': 7.18.6 + '@babel/parser': 7.20.15 + '@babel/types': 7.20.7 + debug: 4.3.4 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/types/7.20.7: + resolution: {integrity: sha512-69OnhBxSSgK0OzTJai4kyPDiKTIe3j+ctaHdIGVbRahTLAT7L3R9oeXHC2aVSuGYt3cVnoAMDmOCgJ2yaiLMvg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-string-parser': 7.19.4 + '@babel/helper-validator-identifier': 7.19.1 + to-fast-properties: 2.0.0 + dev: true + + /@developermuch/dev-svelte-headlessui/0.0.1_svelte@3.55.1: + resolution: {integrity: sha512-tfBlHliv75oQFRrC430nIsw+A8+iFmr5c2g0A+VTlVD3960nEL9jOE0LDHYKq6VhX5LnOLTFIZwVKC1DxFo0QA==} + peerDependencies: + svelte: ^3.44.0 + dependencies: + svelte: 3.55.1 + dev: false + + /@esbuild/android-arm/0.16.17: + resolution: {integrity: sha512-N9x1CMXVhtWEAMS7pNNONyA14f71VPQN9Cnavj1XQh6T7bskqiLLrSca4O0Vr8Wdcga943eThxnVp3JLnBMYtw==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-arm64/0.16.17: + resolution: {integrity: sha512-MIGl6p5sc3RDTLLkYL1MyL8BMRN4tLMRCn+yRJJmEDvYZ2M7tmAf80hx1kbNEUX2KJ50RRtxZ4JHLvCfuB6kBg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-x64/0.16.17: + resolution: {integrity: sha512-a3kTv3m0Ghh4z1DaFEuEDfz3OLONKuFvI4Xqczqx4BqLyuFaFkuaG4j2MtA6fuWEFeC5x9IvqnX7drmRq/fyAQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/darwin-arm64/0.16.17: + resolution: {integrity: sha512-/2agbUEfmxWHi9ARTX6OQ/KgXnOWfsNlTeLcoV7HSuSTv63E4DqtAc+2XqGw1KHxKMHGZgbVCZge7HXWX9Vn+w==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@esbuild/darwin-x64/0.16.17: + resolution: {integrity: sha512-2By45OBHulkd9Svy5IOCZt376Aa2oOkiE9QWUK9fe6Tb+WDr8hXL3dpqi+DeLiMed8tVXspzsTAvd0jUl96wmg==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@esbuild/freebsd-arm64/0.16.17: + resolution: {integrity: sha512-mt+cxZe1tVx489VTb4mBAOo2aKSnJ33L9fr25JXpqQqzbUIw/yzIzi+NHwAXK2qYV1lEFp4OoVeThGjUbmWmdw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/freebsd-x64/0.16.17: + resolution: {integrity: sha512-8ScTdNJl5idAKjH8zGAsN7RuWcyHG3BAvMNpKOBaqqR7EbUhhVHOqXRdL7oZvz8WNHL2pr5+eIT5c65kA6NHug==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-arm/0.16.17: + resolution: {integrity: sha512-iihzrWbD4gIT7j3caMzKb/RsFFHCwqqbrbH9SqUSRrdXkXaygSZCZg1FybsZz57Ju7N/SHEgPyaR0LZ8Zbe9gQ==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-arm64/0.16.17: + resolution: {integrity: sha512-7S8gJnSlqKGVJunnMCrXHU9Q8Q/tQIxk/xL8BqAP64wchPCTzuM6W3Ra8cIa1HIflAvDnNOt2jaL17vaW+1V0g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-ia32/0.16.17: + resolution: {integrity: sha512-kiX69+wcPAdgl3Lonh1VI7MBr16nktEvOfViszBSxygRQqSpzv7BffMKRPMFwzeJGPxcio0pdD3kYQGpqQ2SSg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-loong64/0.16.17: + resolution: {integrity: sha512-dTzNnQwembNDhd654cA4QhbS9uDdXC3TKqMJjgOWsC0yNCbpzfWoXdZvp0mY7HU6nzk5E0zpRGGx3qoQg8T2DQ==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-mips64el/0.16.17: + resolution: {integrity: sha512-ezbDkp2nDl0PfIUn0CsQ30kxfcLTlcx4Foz2kYv8qdC6ia2oX5Q3E/8m6lq84Dj/6b0FrkgD582fJMIfHhJfSw==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-ppc64/0.16.17: + resolution: {integrity: sha512-dzS678gYD1lJsW73zrFhDApLVdM3cUF2MvAa1D8K8KtcSKdLBPP4zZSLy6LFZ0jYqQdQ29bjAHJDgz0rVbLB3g==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-riscv64/0.16.17: + resolution: {integrity: sha512-ylNlVsxuFjZK8DQtNUwiMskh6nT0vI7kYl/4fZgV1llP5d6+HIeL/vmmm3jpuoo8+NuXjQVZxmKuhDApK0/cKw==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-s390x/0.16.17: + resolution: {integrity: sha512-gzy7nUTO4UA4oZ2wAMXPNBGTzZFP7mss3aKR2hH+/4UUkCOyqmjXiKpzGrY2TlEUhbbejzXVKKGazYcQTZWA/w==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-x64/0.16.17: + resolution: {integrity: sha512-mdPjPxfnmoqhgpiEArqi4egmBAMYvaObgn4poorpUaqmvzzbvqbowRllQ+ZgzGVMGKaPkqUmPDOOFQRUFDmeUw==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/netbsd-x64/0.16.17: + resolution: {integrity: sha512-/PzmzD/zyAeTUsduZa32bn0ORug+Jd1EGGAUJvqfeixoEISYpGnAezN6lnJoskauoai0Jrs+XSyvDhppCPoKOA==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/openbsd-x64/0.16.17: + resolution: {integrity: sha512-2yaWJhvxGEz2RiftSk0UObqJa/b+rIAjnODJgv2GbGGpRwAfpgzyrg1WLK8rqA24mfZa9GvpjLcBBg8JHkoodg==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/sunos-x64/0.16.17: + resolution: {integrity: sha512-xtVUiev38tN0R3g8VhRfN7Zl42YCJvyBhRKw1RJjwE1d2emWTVToPLNEQj/5Qxc6lVFATDiy6LjVHYhIPrLxzw==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-arm64/0.16.17: + resolution: {integrity: sha512-ga8+JqBDHY4b6fQAmOgtJJue36scANy4l/rL97W+0wYmijhxKetzZdKOJI7olaBaMhWt8Pac2McJdZLxXWUEQw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-ia32/0.16.17: + resolution: {integrity: sha512-WnsKaf46uSSF/sZhwnqE4L/F89AYNMiD4YtEcYekBt9Q7nj0DiId2XH2Ng2PHM54qi5oPrQ8luuzGszqi/veig==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-x64/0.16.17: + resolution: {integrity: sha512-y+EHuSchhL7FjHgvQL/0fnnFmO4T1bhvWANX6gcnqTjtnKWbTvUMCpGnv2+t+31d7RzyEAYAd4u2fnIhHL6N/Q==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@faker-js/faker/7.6.0: + resolution: {integrity: sha512-XK6BTq1NDMo9Xqw/YkYyGjSsg44fbNwYRx7QK2CuoQgyy+f1rrTDHoExVM5PsyXCtfl2vs2vVJ0MN0yN6LppRw==} + engines: {node: '>=14.0.0', npm: '>=6.0.0'} + dev: true + + /@jridgewell/gen-mapping/0.1.1: + resolution: {integrity: sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==} + engines: {node: '>=6.0.0'} + dependencies: + '@jridgewell/set-array': 1.1.2 + '@jridgewell/sourcemap-codec': 1.4.14 + dev: true + + /@jridgewell/gen-mapping/0.3.2: + resolution: {integrity: sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==} + engines: {node: '>=6.0.0'} + dependencies: + '@jridgewell/set-array': 1.1.2 + '@jridgewell/sourcemap-codec': 1.4.14 + '@jridgewell/trace-mapping': 0.3.17 + dev: true + + /@jridgewell/resolve-uri/3.1.0: + resolution: {integrity: sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==} + engines: {node: '>=6.0.0'} + dev: true + + /@jridgewell/set-array/1.1.2: + resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} + engines: {node: '>=6.0.0'} + dev: true + + /@jridgewell/source-map/0.3.2: + resolution: {integrity: sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==} + dependencies: + '@jridgewell/gen-mapping': 0.3.2 + '@jridgewell/trace-mapping': 0.3.17 + dev: true + + /@jridgewell/sourcemap-codec/1.4.14: + resolution: {integrity: sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==} + dev: true + + /@jridgewell/trace-mapping/0.3.17: + resolution: {integrity: sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==} + dependencies: + '@jridgewell/resolve-uri': 3.1.0 + '@jridgewell/sourcemap-codec': 1.4.14 + dev: true + + /@nodelib/fs.scandir/2.1.5: + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + dev: true + + /@nodelib/fs.stat/2.0.5: + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + dev: true + + /@nodelib/fs.walk/1.2.8: + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.15.0 + dev: true + + /@playwright/test/1.30.0: + resolution: {integrity: sha512-SVxkQw1xvn/Wk/EvBnqWIq6NLo1AppwbYOjNLmyU0R1RoQ3rLEBtmjTnElcnz8VEtn11fptj1ECxK0tgURhajw==} + engines: {node: '>=14'} + hasBin: true + dependencies: + '@types/node': 18.13.0 + playwright-core: 1.30.0 + dev: true + + /@polka/url/1.0.0-next.21: + resolution: {integrity: sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==} + dev: true + + /@rgossiaux/svelte-headlessui/1.0.2_svelte@3.55.1: + resolution: {integrity: sha512-sauopYTSivhzXe1kAvgawkhyYJcQlK8Li3p0d2OtcCIVprOzdbard5lbqWB4xHDv83zAobt2mR08oizO2poHLQ==} + peerDependencies: + svelte: ^3.44.0 + dependencies: + svelte: 3.55.1 + dev: false + + /@rollup/plugin-babel/5.3.1_3dsfpkpoyvuuxyfgdbpn4j4uzm: + resolution: {integrity: sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==} + engines: {node: '>= 10.0.0'} + peerDependencies: + '@babel/core': ^7.0.0 + '@types/babel__core': ^7.1.9 + rollup: ^1.20.0||^2.0.0 + peerDependenciesMeta: + '@types/babel__core': + optional: true + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-module-imports': 7.18.6 + '@rollup/pluginutils': 3.1.0_rollup@2.79.1 + rollup: 2.79.1 + dev: true + + /@rollup/plugin-commonjs/24.0.1_rollup@3.14.0: + resolution: {integrity: sha512-15LsiWRZk4eOGqvrJyu3z3DaBu5BhXIMeWnijSRvd8irrrg9SHpQ1pH+BUK4H6Z9wL9yOxZJMTLU+Au86XHxow==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^2.68.0||^3.0.0 + peerDependenciesMeta: + rollup: + optional: true + dependencies: + '@rollup/pluginutils': 5.0.2_rollup@3.14.0 + commondir: 1.0.1 + estree-walker: 2.0.2 + glob: 8.1.0 + is-reference: 1.2.1 + magic-string: 0.27.0 + rollup: 3.14.0 + dev: true + + /@rollup/plugin-json/6.0.0_rollup@3.14.0: + resolution: {integrity: sha512-i/4C5Jrdr1XUarRhVu27EEwjt4GObltD7c+MkCIpO2QIbojw8MUs+CCTqOphQi3Qtg1FLmYt+l+6YeoIf51J7w==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0 + peerDependenciesMeta: + rollup: + optional: true + dependencies: + '@rollup/pluginutils': 5.0.2_rollup@3.14.0 + rollup: 3.14.0 + dev: true + + /@rollup/plugin-node-resolve/11.2.1_rollup@2.79.1: + resolution: {integrity: sha512-yc2n43jcqVyGE2sqV5/YCmocy9ArjVAP/BeXyTtADTBBX6V0e5UMqwO8CdQ0kzjb6zu5P1qMzsScCMRvE9OlVg==} + engines: {node: '>= 10.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0 + dependencies: + '@rollup/pluginutils': 3.1.0_rollup@2.79.1 + '@types/resolve': 1.17.1 + builtin-modules: 3.3.0 + deepmerge: 4.3.0 + is-module: 1.0.0 + resolve: 1.22.1 + rollup: 2.79.1 + dev: true + + /@rollup/plugin-node-resolve/15.0.1_rollup@3.14.0: + resolution: {integrity: sha512-ReY88T7JhJjeRVbfCyNj+NXAG3IIsVMsX9b5/9jC98dRP8/yxlZdz7mHZbHk5zHr24wZZICS5AcXsFZAXYUQEg==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^2.78.0||^3.0.0 + peerDependenciesMeta: + rollup: + optional: true + dependencies: + '@rollup/pluginutils': 5.0.2_rollup@3.14.0 + '@types/resolve': 1.20.2 + deepmerge: 4.3.0 + is-builtin-module: 3.2.1 + is-module: 1.0.0 + resolve: 1.22.1 + rollup: 3.14.0 + dev: true + + /@rollup/plugin-replace/2.4.2_rollup@2.79.1: + resolution: {integrity: sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg==} + peerDependencies: + rollup: ^1.20.0 || ^2.0.0 + dependencies: + '@rollup/pluginutils': 3.1.0_rollup@2.79.1 + magic-string: 0.25.9 + rollup: 2.79.1 + dev: true + + /@rollup/plugin-replace/5.0.2_rollup@3.14.0: + resolution: {integrity: sha512-M9YXNekv/C/iHHK+cvORzfRYfPbq0RDD8r0G+bMiTXjNGKulPnCT9O3Ss46WfhI6ZOCgApOP7xAdmCQJ+U2LAA==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0 + peerDependenciesMeta: + rollup: + optional: true + dependencies: + '@rollup/pluginutils': 5.0.2_rollup@3.14.0 + magic-string: 0.27.0 + rollup: 3.14.0 + dev: true + + /@rollup/pluginutils/3.1.0_rollup@2.79.1: + resolution: {integrity: sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==} + engines: {node: '>= 8.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0 + dependencies: + '@types/estree': 0.0.39 + estree-walker: 1.0.1 + picomatch: 2.3.1 + rollup: 2.79.1 + dev: true + + /@rollup/pluginutils/5.0.2_rollup@3.14.0: + resolution: {integrity: sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0 + peerDependenciesMeta: + rollup: + optional: true + dependencies: + '@types/estree': 1.0.0 + estree-walker: 2.0.2 + picomatch: 2.3.1 + rollup: 3.14.0 + dev: true + + /@surma/rollup-plugin-off-main-thread/2.2.3: + resolution: {integrity: sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==} + dependencies: + ejs: 3.1.8 + json5: 2.2.3 + magic-string: 0.25.9 + string.prototype.matchall: 4.0.8 + dev: true + + /@sveltejs/adapter-node/1.1.7_@sveltejs+kit@1.5.0: + resolution: {integrity: sha512-N93uYDzH8+RlneYdfgS33nudVk9e8JhYy9vPOsJKeMCkWPb5ejJ0D+LUAEuyA1BOyA7ZkaVTupLStAG4nuhT2A==} + peerDependencies: + '@sveltejs/kit': ^1.0.0 + dependencies: + '@rollup/plugin-commonjs': 24.0.1_rollup@3.14.0 + '@rollup/plugin-json': 6.0.0_rollup@3.14.0 + '@rollup/plugin-node-resolve': 15.0.1_rollup@3.14.0 + '@sveltejs/kit': 1.5.0_svelte@3.55.1+vite@4.1.1 + rollup: 3.14.0 + dev: true + + /@sveltejs/kit/1.5.0_svelte@3.55.1+vite@4.1.1: + resolution: {integrity: sha512-AkWgCO9i2djZjTqCgIQJ5XfnSzRINowh2w2Gk9wDRuTwxKizSuYe3jNvds/HCDDGHo8XE5E0yWNC9j2XxbrX+g==} + engines: {node: ^16.14 || >=18} + hasBin: true + requiresBuild: true + peerDependencies: + svelte: ^3.54.0 + vite: ^4.0.0 + dependencies: + '@sveltejs/vite-plugin-svelte': 2.0.2_svelte@3.55.1+vite@4.1.1 + '@types/cookie': 0.5.1 + cookie: 0.5.0 + devalue: 4.2.3 + esm-env: 1.0.0 + kleur: 4.1.5 + magic-string: 0.27.0 + mime: 3.0.0 + sade: 1.8.1 + set-cookie-parser: 2.5.1 + sirv: 2.0.2 + svelte: 3.55.1 + tiny-glob: 0.2.9 + undici: 5.18.0 + vite: 4.1.1 + transitivePeerDependencies: + - supports-color + dev: true + + /@sveltejs/vite-plugin-svelte/2.0.2_svelte@3.55.1+vite@4.1.1: + resolution: {integrity: sha512-xCEan0/NNpQuL0l5aS42FjwQ6wwskdxC3pW1OeFtEKNZwRg7Evro9lac9HesGP6TdFsTv2xMes5ASQVKbCacxg==} + engines: {node: ^14.18.0 || >= 16} + peerDependencies: + svelte: ^3.54.0 + vite: ^4.0.0 + dependencies: + debug: 4.3.4 + deepmerge: 4.3.0 + kleur: 4.1.5 + magic-string: 0.27.0 + svelte: 3.55.1 + svelte-hmr: 0.15.1_svelte@3.55.1 + vite: 4.1.1 + vitefu: 0.2.4_vite@4.1.1 + transitivePeerDependencies: + - supports-color + dev: true + + /@tailwindcss/forms/0.5.3_tailwindcss@3.2.6: + resolution: {integrity: sha512-y5mb86JUoiUgBjY/o6FJSFZSEttfb3Q5gllE4xoKjAAD+vBrnIhE4dViwUuow3va8mpH4s9jyUbUbrRGoRdc2Q==} + peerDependencies: + tailwindcss: '>=3.0.0 || >= 3.0.0-alpha.1' + dependencies: + mini-svg-data-uri: 1.4.4 + tailwindcss: 3.2.6_postcss@8.4.21 + dev: true + + /@types/cookie/0.5.1: + resolution: {integrity: sha512-COUnqfB2+ckwXXSFInsFdOAWQzCCx+a5hq2ruyj+Vjund94RJQd4LG2u9hnvJrTgunKAaax7ancBYlDrNYxA0g==} + dev: true + + /@types/estree/0.0.39: + resolution: {integrity: sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==} + dev: true + + /@types/estree/1.0.0: + resolution: {integrity: sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==} + dev: true + + /@types/events/3.0.0: + resolution: {integrity: sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==} + dev: false + + /@types/js-cookie/3.0.2: + resolution: {integrity: sha512-6+0ekgfusHftJNYpihfkMu8BWdeHs9EOJuGcSofErjstGPfPGEu9yTu4t460lTzzAMl2cM5zngQJqPMHbbnvYA==} + dev: true + + /@types/node/18.13.0: + resolution: {integrity: sha512-gC3TazRzGoOnoKAhUx+Q0t8S9Tzs74z7m0ipwGpSqQrleP14hKxP4/JUeEQcD3W1/aIpnWl8pHowI7WokuZpXg==} + + /@types/pino/5.20.0: + resolution: {integrity: sha512-gz3Ahvx1UDEveXViOQtYqnUkjSVQFdoJqpZTW/63spEHwOGRJRJIi3JMJSClp5Sk1x1ljn9tHWjGczmP6s/rvg==} + dependencies: + '@types/events': 3.0.0 + '@types/node': 18.13.0 + dev: false + + /@types/pug/2.0.6: + resolution: {integrity: sha512-SnHmG9wN1UVmagJOnyo/qkk0Z7gejYxOYYmaAwr5u2yFYfsupN3sg10kyzN8Hep/2zbHxCnsumxOoRIRMBwKCg==} + dev: true + + /@types/resolve/1.17.1: + resolution: {integrity: sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==} + dependencies: + '@types/node': 18.13.0 + dev: true + + /@types/resolve/1.20.2: + resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} + dev: true + + /@types/sass/1.43.1: + resolution: {integrity: sha512-BPdoIt1lfJ6B7rw35ncdwBZrAssjcwzI5LByIrYs+tpXlj/CAkuVdRsgZDdP4lq5EjyWzwxZCqAoFyHKFwp32g==} + dependencies: + '@types/node': 18.13.0 + dev: true + + /@types/trusted-types/2.0.2: + resolution: {integrity: sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==} + dev: true + + /@vite-pwa/sveltekit/0.1.3_kkbswr7m3b4a2hnxfqban7fxma: + resolution: {integrity: sha512-wOo8riFow/eT+5UWyOcfO3ol72+2bT2fHDwd+sryqqriCWbRxOSUBKivVLlqzyna0QA0sNkM1jDGjJShpwq7aQ==} + engines: {node: '>=16.14'} + peerDependencies: + '@sveltejs/kit': ^1.0.0 + vite-plugin-pwa: ^0.14.0 + dependencies: + '@sveltejs/kit': 1.5.0_svelte@3.55.1+vite@4.1.1 + vite-plugin-pwa: 0.14.3_vite@4.1.1 + dev: true + + /abort-controller/3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + dependencies: + event-target-shim: 5.0.1 + + /acorn-node/1.8.2: + resolution: {integrity: sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==} + dependencies: + acorn: 7.4.1 + acorn-walk: 7.2.0 + xtend: 4.0.2 + dev: true + + /acorn-walk/7.2.0: + resolution: {integrity: sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==} + engines: {node: '>=0.4.0'} + dev: true + + /acorn/7.4.1: + resolution: {integrity: sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==} + engines: {node: '>=0.4.0'} + hasBin: true + dev: true + + /acorn/8.8.2: + resolution: {integrity: sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==} + engines: {node: '>=0.4.0'} + hasBin: true + dev: true + + /ajv/8.12.0: + resolution: {integrity: sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==} + dependencies: + fast-deep-equal: 3.1.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + uri-js: 4.4.1 + dev: true + + /ansi-styles/3.2.1: + resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} + engines: {node: '>=4'} + dependencies: + color-convert: 1.9.3 + dev: true + + /ansi-styles/4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + dependencies: + color-convert: 2.0.1 + dev: true + + /anymatch/3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + dev: true + + /arg/5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + dev: true + + /async/3.2.4: + resolution: {integrity: sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==} + dev: true + + /at-least-node/1.0.0: + resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} + engines: {node: '>= 4.0.0'} + dev: true + + /atomic-sleep/1.0.0: + resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} + engines: {node: '>=8.0.0'} + + /autoprefixer/10.4.13_postcss@8.4.21: + resolution: {integrity: sha512-49vKpMqcZYsJjwotvt4+h/BCjJVnhGwcLpDt5xkcaOG3eLrG/HUYLagrihYsQ+qrIBgIzX1Rw7a6L8I/ZA1Atg==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + dependencies: + browserslist: 4.21.5 + caniuse-lite: 1.0.30001451 + fraction.js: 4.2.0 + normalize-range: 0.1.2 + picocolors: 1.0.0 + postcss: 8.4.21 + postcss-value-parser: 4.2.0 + dev: true + + /available-typed-arrays/1.0.5: + resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==} + engines: {node: '>= 0.4'} + dev: true + + /babel-plugin-polyfill-corejs2/0.3.3_@babel+core@7.20.12: + resolution: {integrity: sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/compat-data': 7.20.14 + '@babel/core': 7.20.12 + '@babel/helper-define-polyfill-provider': 0.3.3_@babel+core@7.20.12 + semver: 6.3.0 + transitivePeerDependencies: + - supports-color + dev: true + + /babel-plugin-polyfill-corejs3/0.6.0_@babel+core@7.20.12: + resolution: {integrity: sha512-+eHqR6OPcBhJOGgsIar7xoAB1GcSwVUA3XjAd7HJNzOXT4wv6/H7KIdA/Nc60cvUlDbKApmqNvD1B1bzOt4nyA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-define-polyfill-provider': 0.3.3_@babel+core@7.20.12 + core-js-compat: 3.27.2 + transitivePeerDependencies: + - supports-color + dev: true + + /babel-plugin-polyfill-regenerator/0.4.1_@babel+core@7.20.12: + resolution: {integrity: sha512-NtQGmyQDXjQqQ+IzRkBVwEOz9lQ4zxAQZgoAYEtU9dJjnl1Oc98qnN7jcp+bE7O7aYzVpavXE3/VKXNzUbh7aw==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.20.12 + '@babel/helper-define-polyfill-provider': 0.3.3_@babel+core@7.20.12 + transitivePeerDependencies: + - supports-color + dev: true + + /balanced-match/1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + dev: true + + /base64-js/1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + /binary-extensions/2.2.0: + resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} + engines: {node: '>=8'} + dev: true + + /brace-expansion/1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + dev: true + + /brace-expansion/2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + dependencies: + balanced-match: 1.0.2 + dev: true + + /braces/3.0.2: + resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} + engines: {node: '>=8'} + dependencies: + fill-range: 7.0.1 + dev: true + + /browserslist/4.21.5: + resolution: {integrity: sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + dependencies: + caniuse-lite: 1.0.30001451 + electron-to-chromium: 1.4.289 + node-releases: 2.0.10 + update-browserslist-db: 1.0.10_browserslist@4.21.5 + dev: true + + /buffer-crc32/0.2.13: + resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + dev: true + + /buffer-from/1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + dev: true + + /buffer/6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + /builtin-modules/3.3.0: + resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} + engines: {node: '>=6'} + dev: true + + /busboy/1.6.0: + resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} + engines: {node: '>=10.16.0'} + dependencies: + streamsearch: 1.1.0 + dev: true + + /call-bind/1.0.2: + resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==} + dependencies: + function-bind: 1.1.1 + get-intrinsic: 1.2.0 + dev: true + + /callsites/3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + dev: true + + /camelcase-css/2.0.1: + resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} + engines: {node: '>= 6'} + dev: true + + /caniuse-lite/1.0.30001451: + resolution: {integrity: sha512-XY7UbUpGRatZzoRft//5xOa69/1iGJRBlrieH6QYrkKLIFn3m7OVEJ81dSrKoy2BnKsdbX5cLrOispZNYo9v2w==} + dev: true + + /chalk/2.4.2: + resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} + engines: {node: '>=4'} + dependencies: + ansi-styles: 3.2.1 + escape-string-regexp: 1.0.5 + supports-color: 5.5.0 + dev: true + + /chalk/4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + dev: true + + /chokidar/3.5.3: + resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} + engines: {node: '>= 8.10.0'} + dependencies: + anymatch: 3.1.3 + braces: 3.0.2 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.2 + dev: true + + /color-convert/1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + dependencies: + color-name: 1.1.3 + dev: true + + /color-convert/2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + dependencies: + color-name: 1.1.4 + dev: true + + /color-name/1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + dev: true + + /color-name/1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + dev: true + + /colorette/2.0.19: + resolution: {integrity: sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==} + dev: true + + /commander/2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + + /common-tags/1.8.2: + resolution: {integrity: sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==} + engines: {node: '>=4.0.0'} + dev: true + + /commondir/1.0.1: + resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} + dev: true + + /concat-map/0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + dev: true + + /convert-source-map/1.9.0: + resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} + dev: true + + /cookie/0.5.0: + resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} + engines: {node: '>= 0.6'} + dev: true + + /core-js-compat/3.27.2: + resolution: {integrity: sha512-welaYuF7ZtbYKGrIy7y3eb40d37rG1FvzEOfe7hSLd2iD6duMDqUhRfSvCGyC46HhR6Y8JXXdZ2lnRUMkPBpvg==} + dependencies: + browserslist: 4.21.5 + dev: true + + /cross-spawn/6.0.5: + resolution: {integrity: sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==} + engines: {node: '>=4.8'} + dependencies: + nice-try: 1.0.5 + path-key: 2.0.1 + semver: 5.7.1 + shebang-command: 1.2.0 + which: 1.3.1 + dev: true + + /crypto-random-string/2.0.0: + resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==} + engines: {node: '>=8'} + dev: true + + /cssesc/3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + dev: true + + /dateformat/4.6.3: + resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==} + dev: true + + /debug/4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + dev: true + + /deepmerge/4.3.0: + resolution: {integrity: sha512-z2wJZXrmeHdvYJp/Ux55wIjqo81G5Bp4c+oELTW+7ar6SogWHajt5a9gO3s3IDaGSAXjDk0vlQKN3rms8ab3og==} + engines: {node: '>=0.10.0'} + dev: true + + /define-properties/1.1.4: + resolution: {integrity: sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==} + engines: {node: '>= 0.4'} + dependencies: + has-property-descriptors: 1.0.0 + object-keys: 1.1.1 + dev: true + + /defined/1.0.1: + resolution: {integrity: sha512-hsBd2qSVCRE+5PmNdHt1uzyrFu5d3RwmFDKzyNZMFq/EwDNJF7Ee5+D5oEKF0hU6LhtoUF1macFvOe4AskQC1Q==} + dev: true + + /detect-indent/6.1.0: + resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} + engines: {node: '>=8'} + dev: true + + /detective/5.2.1: + resolution: {integrity: sha512-v9XE1zRnz1wRtgurGu0Bs8uHKFSTdteYZNbIPFVhUZ39L/S79ppMpdmVOZAnoz1jfEFodc48n6MX483Xo3t1yw==} + engines: {node: '>=0.8.0'} + hasBin: true + dependencies: + acorn-node: 1.8.2 + defined: 1.0.1 + minimist: 1.2.7 + dev: true + + /devalue/4.2.3: + resolution: {integrity: sha512-JG6Q248aN0pgFL57e3zqTVeFraBe+5W2ugvv1mLXsJP6YYIYJhRZhAl7QP8haJrqob6X10F9NEkuCvNILZTPeQ==} + dev: true + + /didyoumean/1.2.2: + resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + dev: true + + /dlv/1.1.3: + resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + dev: true + + /ejs/3.1.8: + resolution: {integrity: sha512-/sXZeMlhS0ArkfX2Aw780gJzXSMPnKjtspYZv+f3NiKLlubezAHDU5+9xz6gd3/NhG3txQCo6xlglmTS+oTGEQ==} + engines: {node: '>=0.10.0'} + hasBin: true + dependencies: + jake: 10.8.5 + dev: true + + /electron-to-chromium/1.4.289: + resolution: {integrity: sha512-relLdMfPBxqGCxy7Gyfm1HcbRPcFUJdlgnCPVgQ23sr1TvUrRJz0/QPoGP0+x41wOVSTN/Wi3w6YDgHiHJGOzg==} + dev: true + + /end-of-stream/1.4.4: + resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} + dependencies: + once: 1.4.0 + dev: true + + /error-ex/1.3.2: + resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + dependencies: + is-arrayish: 0.2.1 + dev: true + + /es-abstract/1.21.1: + resolution: {integrity: sha512-QudMsPOz86xYz/1dG1OuGBKOELjCh99IIWHLzy5znUB6j8xG2yMA7bfTV86VSqKF+Y/H08vQPR+9jyXpuC6hfg==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.5 + call-bind: 1.0.2 + es-set-tostringtag: 2.0.1 + es-to-primitive: 1.2.1 + function-bind: 1.1.1 + function.prototype.name: 1.1.5 + get-intrinsic: 1.2.0 + get-symbol-description: 1.0.0 + globalthis: 1.0.3 + gopd: 1.0.1 + has: 1.0.3 + has-property-descriptors: 1.0.0 + has-proto: 1.0.1 + has-symbols: 1.0.3 + internal-slot: 1.0.4 + is-array-buffer: 3.0.1 + is-callable: 1.2.7 + is-negative-zero: 2.0.2 + is-regex: 1.1.4 + is-shared-array-buffer: 1.0.2 + is-string: 1.0.7 + is-typed-array: 1.1.10 + is-weakref: 1.0.2 + object-inspect: 1.12.3 + object-keys: 1.1.1 + object.assign: 4.1.4 + regexp.prototype.flags: 1.4.3 + safe-regex-test: 1.0.0 + string.prototype.trimend: 1.0.6 + string.prototype.trimstart: 1.0.6 + typed-array-length: 1.0.4 + unbox-primitive: 1.0.2 + which-typed-array: 1.1.9 + dev: true + + /es-set-tostringtag/2.0.1: + resolution: {integrity: sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.0 + has: 1.0.3 + has-tostringtag: 1.0.0 + dev: true + + /es-to-primitive/1.2.1: + resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==} + engines: {node: '>= 0.4'} + dependencies: + is-callable: 1.2.7 + is-date-object: 1.0.5 + is-symbol: 1.0.4 + dev: true + + /es6-promise/3.3.1: + resolution: {integrity: sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==} + dev: true + + /esbuild/0.16.17: + resolution: {integrity: sha512-G8LEkV0XzDMNwXKgM0Jwu3nY3lSTwSGY6XbxM9cr9+s0T/qSV1q1JVPBGzm3dcjhCic9+emZDmMffkwgPeOeLg==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/android-arm': 0.16.17 + '@esbuild/android-arm64': 0.16.17 + '@esbuild/android-x64': 0.16.17 + '@esbuild/darwin-arm64': 0.16.17 + '@esbuild/darwin-x64': 0.16.17 + '@esbuild/freebsd-arm64': 0.16.17 + '@esbuild/freebsd-x64': 0.16.17 + '@esbuild/linux-arm': 0.16.17 + '@esbuild/linux-arm64': 0.16.17 + '@esbuild/linux-ia32': 0.16.17 + '@esbuild/linux-loong64': 0.16.17 + '@esbuild/linux-mips64el': 0.16.17 + '@esbuild/linux-ppc64': 0.16.17 + '@esbuild/linux-riscv64': 0.16.17 + '@esbuild/linux-s390x': 0.16.17 + '@esbuild/linux-x64': 0.16.17 + '@esbuild/netbsd-x64': 0.16.17 + '@esbuild/openbsd-x64': 0.16.17 + '@esbuild/sunos-x64': 0.16.17 + '@esbuild/win32-arm64': 0.16.17 + '@esbuild/win32-ia32': 0.16.17 + '@esbuild/win32-x64': 0.16.17 + dev: true + + /escalade/3.1.1: + resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} + engines: {node: '>=6'} + dev: true + + /escape-string-regexp/1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + dev: true + + /esm-env/1.0.0: + resolution: {integrity: sha512-Cf6VksWPsTuW01vU9Mk/3vRue91Zevka5SjyNf3nEpokFRuqt/KjUQoGAwq9qMmhpLTHmXzSIrFRw8zxWzmFBA==} + dev: true + + /estree-walker/1.0.1: + resolution: {integrity: sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==} + dev: true + + /estree-walker/2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + dev: true + + /esutils/2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + dev: true + + /event-target-shim/5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + + /events/3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + + /fast-copy/3.0.0: + resolution: {integrity: sha512-4HzS+9pQ5Yxtv13Lhs1Z1unMXamBdn5nA4bEi1abYpDNSpSp7ODYQ1KPMF6nTatfEzgH6/zPvXKU1zvHiUjWlA==} + dev: true + + /fast-deep-equal/3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + dev: true + + /fast-glob/3.2.12: + resolution: {integrity: sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==} + engines: {node: '>=8.6.0'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.5 + dev: true + + /fast-json-stable-stringify/2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + dev: true + + /fast-redact/3.1.2: + resolution: {integrity: sha512-+0em+Iya9fKGfEQGcd62Yv6onjBmmhV1uh86XVfOU8VwAe6kaFdQCWI9s0/Nnugx5Vd9tdbZ7e6gE2tR9dzXdw==} + engines: {node: '>=6'} + dev: false + + /fast-safe-stringify/2.1.1: + resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + dev: true + + /fastq/1.15.0: + resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==} + dependencies: + reusify: 1.0.4 + dev: true + + /filelist/1.0.4: + resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==} + dependencies: + minimatch: 5.1.6 + dev: true + + /fill-range/7.0.1: + resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} + engines: {node: '>=8'} + dependencies: + to-regex-range: 5.0.1 + dev: true + + /for-each/0.3.3: + resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} + dependencies: + is-callable: 1.2.7 + dev: true + + /fraction.js/4.2.0: + resolution: {integrity: sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==} + dev: true + + /fs-extra/9.1.0: + resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} + engines: {node: '>=10'} + dependencies: + at-least-node: 1.0.0 + graceful-fs: 4.2.10 + jsonfile: 6.1.0 + universalify: 2.0.0 + dev: true + + /fs.realpath/1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + dev: true + + /fsevents/2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /function-bind/1.1.1: + resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} + dev: true + + /function.prototype.name/1.1.5: + resolution: {integrity: sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.1.4 + es-abstract: 1.21.1 + functions-have-names: 1.2.3 + dev: true + + /functions-have-names/1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + dev: true + + /fuzzysort/2.0.4: + resolution: {integrity: sha512-Api1mJL+Ad7W7vnDZnWq5pGaXJjyencT+iKGia2PlHUcSsSzWwIQ3S1isiMpwpavjYtGd2FzhUIhnnhOULZgDw==} + dev: false + + /gensync/1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + dev: true + + /get-intrinsic/1.2.0: + resolution: {integrity: sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==} + dependencies: + function-bind: 1.1.1 + has: 1.0.3 + has-symbols: 1.0.3 + dev: true + + /get-own-enumerable-property-symbols/3.0.2: + resolution: {integrity: sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==} + dev: true + + /get-symbol-description/1.0.0: + resolution: {integrity: sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.0 + dev: true + + /glob-parent/5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + dependencies: + is-glob: 4.0.3 + dev: true + + /glob-parent/6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + dependencies: + is-glob: 4.0.3 + dev: true + + /glob/7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + dev: true + + /glob/8.1.0: + resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} + engines: {node: '>=12'} + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 5.1.6 + once: 1.4.0 + dev: true + + /globals/11.12.0: + resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} + engines: {node: '>=4'} + dev: true + + /globalthis/1.0.3: + resolution: {integrity: sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==} + engines: {node: '>= 0.4'} + dependencies: + define-properties: 1.1.4 + dev: true + + /globalyzer/0.1.0: + resolution: {integrity: sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==} + dev: true + + /globrex/0.1.2: + resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} + dev: true + + /gopd/1.0.1: + resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} + dependencies: + get-intrinsic: 1.2.0 + dev: true + + /graceful-fs/4.2.10: + resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==} + dev: true + + /has-bigints/1.0.2: + resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} + dev: true + + /has-flag/3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + dev: true + + /has-flag/4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + dev: true + + /has-property-descriptors/1.0.0: + resolution: {integrity: sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==} + dependencies: + get-intrinsic: 1.2.0 + dev: true + + /has-proto/1.0.1: + resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==} + engines: {node: '>= 0.4'} + dev: true + + /has-symbols/1.0.3: + resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} + engines: {node: '>= 0.4'} + dev: true + + /has-tostringtag/1.0.0: + resolution: {integrity: sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.0.3 + dev: true + + /has/1.0.3: + resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==} + engines: {node: '>= 0.4.0'} + dependencies: + function-bind: 1.1.1 + dev: true + + /help-me/4.2.0: + resolution: {integrity: sha512-TAOnTB8Tz5Dw8penUuzHVrKNKlCIbwwbHnXraNJxPwf8LRtE2HlM84RYuezMFcwOJmoYOCWVDyJ8TQGxn9PgxA==} + dependencies: + glob: 8.1.0 + readable-stream: 3.6.0 + dev: true + + /hosted-git-info/2.8.9: + resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} + dev: true + + /idb/7.1.1: + resolution: {integrity: sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==} + dev: true + + /ieee754/1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + /import-fresh/3.3.0: + resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} + engines: {node: '>=6'} + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + dev: true + + /inflight/1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + dev: true + + /inherits/2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + /internal-slot/1.0.4: + resolution: {integrity: sha512-tA8URYccNzMo94s5MQZgH8NB/XTa6HsOo0MLfXTKKEnHVVdegzaQoFZ7Jp44bdvLvY2waT5dc+j5ICEswhi7UQ==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.0 + has: 1.0.3 + side-channel: 1.0.4 + dev: true + + /is-array-buffer/3.0.1: + resolution: {integrity: sha512-ASfLknmY8Xa2XtB4wmbz13Wu202baeA18cJBCeCy0wXUHZF0IPyVEXqKEcd+t2fNSLLL1vC6k7lxZEojNbISXQ==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.0 + is-typed-array: 1.1.10 + dev: true + + /is-arrayish/0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + dev: true + + /is-bigint/1.0.4: + resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} + dependencies: + has-bigints: 1.0.2 + dev: true + + /is-binary-path/2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + dependencies: + binary-extensions: 2.2.0 + dev: true + + /is-boolean-object/1.1.2: + resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + has-tostringtag: 1.0.0 + dev: true + + /is-builtin-module/3.2.1: + resolution: {integrity: sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==} + engines: {node: '>=6'} + dependencies: + builtin-modules: 3.3.0 + dev: true + + /is-callable/1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + dev: true + + /is-core-module/2.11.0: + resolution: {integrity: sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==} + dependencies: + has: 1.0.3 + dev: true + + /is-date-object/1.0.5: + resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: true + + /is-extglob/2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + dev: true + + /is-glob/4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + dependencies: + is-extglob: 2.1.1 + dev: true + + /is-module/1.0.0: + resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==} + dev: true + + /is-negative-zero/2.0.2: + resolution: {integrity: sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==} + engines: {node: '>= 0.4'} + dev: true + + /is-number-object/1.0.7: + resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: true + + /is-number/7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + dev: true + + /is-obj/1.0.1: + resolution: {integrity: sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==} + engines: {node: '>=0.10.0'} + dev: true + + /is-reference/1.2.1: + resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} + dependencies: + '@types/estree': 1.0.0 + dev: true + + /is-regex/1.1.4: + resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + has-tostringtag: 1.0.0 + dev: true + + /is-regexp/1.0.0: + resolution: {integrity: sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==} + engines: {node: '>=0.10.0'} + dev: true + + /is-shared-array-buffer/1.0.2: + resolution: {integrity: sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==} + dependencies: + call-bind: 1.0.2 + dev: true + + /is-stream/2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + dev: true + + /is-string/1.0.7: + resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: true + + /is-symbol/1.0.4: + resolution: {integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.0.3 + dev: true + + /is-typed-array/1.1.10: + resolution: {integrity: sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.5 + call-bind: 1.0.2 + for-each: 0.3.3 + gopd: 1.0.1 + has-tostringtag: 1.0.0 + dev: true + + /is-weakref/1.0.2: + resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} + dependencies: + call-bind: 1.0.2 + dev: true + + /isexe/2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + dev: true + + /jake/10.8.5: + resolution: {integrity: sha512-sVpxYeuAhWt0OTWITwT98oyV0GsXyMlXCF+3L1SuafBVUIr/uILGRB+NqwkzhgXKvoJpDIpQvqkUALgdmQsQxw==} + engines: {node: '>=10'} + hasBin: true + dependencies: + async: 3.2.4 + chalk: 4.1.2 + filelist: 1.0.4 + minimatch: 3.1.2 + dev: true + + /jest-worker/26.6.2: + resolution: {integrity: sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==} + engines: {node: '>= 10.13.0'} + dependencies: + '@types/node': 18.13.0 + merge-stream: 2.0.0 + supports-color: 7.2.0 + dev: true + + /joycon/3.1.1: + resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} + engines: {node: '>=10'} + dev: true + + /js-cookie/3.0.1: + resolution: {integrity: sha512-+0rgsUXZu4ncpPxRL+lNEptWMOWl9etvPHc/koSRp6MPwpRYAhmk0dUG00J4bxVV3r9uUzfo24wW0knS07SKSw==} + engines: {node: '>=12'} + dev: false + + /js-tokens/4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + dev: true + + /jsesc/0.5.0: + resolution: {integrity: sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==} + hasBin: true + dev: true + + /jsesc/2.5.2: + resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} + engines: {node: '>=4'} + hasBin: true + dev: true + + /json-parse-better-errors/1.0.2: + resolution: {integrity: sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==} + dev: true + + /json-schema-traverse/1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + dev: true + + /json-schema/0.4.0: + resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + dev: true + + /json5/2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + dev: true + + /jsonfile/6.1.0: + resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + dependencies: + universalify: 2.0.0 + optionalDependencies: + graceful-fs: 4.2.10 + dev: true + + /jsonpointer/5.0.1: + resolution: {integrity: sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==} + engines: {node: '>=0.10.0'} + dev: true + + /kleur/4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + dev: true + + /leven/3.1.0: + resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} + engines: {node: '>=6'} + dev: true + + /lilconfig/2.0.6: + resolution: {integrity: sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg==} + engines: {node: '>=10'} + dev: true + + /load-json-file/4.0.0: + resolution: {integrity: sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==} + engines: {node: '>=4'} + dependencies: + graceful-fs: 4.2.10 + parse-json: 4.0.0 + pify: 3.0.0 + strip-bom: 3.0.0 + dev: true + + /lodash.debounce/4.0.8: + resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} + dev: true + + /lodash.sortby/4.7.0: + resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} + dev: true + + /lodash/4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + dev: true + + /lru-cache/5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + dependencies: + yallist: 3.1.1 + dev: true + + /magic-string/0.25.9: + resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==} + dependencies: + sourcemap-codec: 1.4.8 + dev: true + + /magic-string/0.27.0: + resolution: {integrity: sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==} + engines: {node: '>=12'} + dependencies: + '@jridgewell/sourcemap-codec': 1.4.14 + dev: true + + /memorystream/0.3.1: + resolution: {integrity: sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==} + engines: {node: '>= 0.10.0'} + dev: true + + /merge-stream/2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + dev: true + + /merge2/1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + dev: true + + /micromatch/4.0.5: + resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} + engines: {node: '>=8.6'} + dependencies: + braces: 3.0.2 + picomatch: 2.3.1 + dev: true + + /mime/3.0.0: + resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} + engines: {node: '>=10.0.0'} + hasBin: true + dev: true + + /min-indent/1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + dev: true + + /mini-svg-data-uri/1.4.4: + resolution: {integrity: sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==} + hasBin: true + dev: true + + /minimatch/3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + dependencies: + brace-expansion: 1.1.11 + dev: true + + /minimatch/5.1.6: + resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} + engines: {node: '>=10'} + dependencies: + brace-expansion: 2.0.1 + dev: true + + /minimist/1.2.7: + resolution: {integrity: sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==} + dev: true + + /mkdirp/0.5.6: + resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} + hasBin: true + dependencies: + minimist: 1.2.7 + dev: true + + /mri/1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + dev: true + + /mrmime/1.0.1: + resolution: {integrity: sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==} + engines: {node: '>=10'} + dev: true + + /ms/2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + dev: true + + /nanoid/3.3.4: + resolution: {integrity: sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + dev: true + + /nice-try/1.0.5: + resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==} + dev: true + + /node-releases/2.0.10: + resolution: {integrity: sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==} + dev: true + + /normalize-package-data/2.5.0: + resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} + dependencies: + hosted-git-info: 2.8.9 + resolve: 1.22.1 + semver: 5.7.1 + validate-npm-package-license: 3.0.4 + dev: true + + /normalize-path/3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + dev: true + + /normalize-range/0.1.2: + resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} + engines: {node: '>=0.10.0'} + dev: true + + /npm-run-all/4.1.5: + resolution: {integrity: sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==} + engines: {node: '>= 4'} + hasBin: true + dependencies: + ansi-styles: 3.2.1 + chalk: 2.4.2 + cross-spawn: 6.0.5 + memorystream: 0.3.1 + minimatch: 3.1.2 + pidtree: 0.3.1 + read-pkg: 3.0.0 + shell-quote: 1.8.0 + string.prototype.padend: 3.1.4 + dev: true + + /object-hash/3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + dev: true + + /object-inspect/1.12.3: + resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==} + dev: true + + /object-keys/1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + dev: true + + /object.assign/4.1.4: + resolution: {integrity: sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.1.4 + has-symbols: 1.0.3 + object-keys: 1.1.1 + dev: true + + /on-exit-leak-free/2.1.0: + resolution: {integrity: sha512-VuCaZZAjReZ3vUwgOB8LxAosIurDiAW0s13rI1YwmaP++jvcxP77AWoQvenZebpCA2m8WC1/EosPYPMjnRAp/w==} + + /once/1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + dependencies: + wrappy: 1.0.2 + dev: true + + /parent-module/1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + dependencies: + callsites: 3.1.0 + dev: true + + /parse-json/4.0.0: + resolution: {integrity: sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==} + engines: {node: '>=4'} + dependencies: + error-ex: 1.3.2 + json-parse-better-errors: 1.0.2 + dev: true + + /path-is-absolute/1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + dev: true + + /path-key/2.0.1: + resolution: {integrity: sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==} + engines: {node: '>=4'} + dev: true + + /path-parse/1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + dev: true + + /path-type/3.0.0: + resolution: {integrity: sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==} + engines: {node: '>=4'} + dependencies: + pify: 3.0.0 + dev: true + + /picocolors/1.0.0: + resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} + dev: true + + /picomatch/2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + dev: true + + /pidtree/0.3.1: + resolution: {integrity: sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA==} + engines: {node: '>=0.10'} + hasBin: true + dev: true + + /pify/2.3.0: + resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} + engines: {node: '>=0.10.0'} + dev: true + + /pify/3.0.0: + resolution: {integrity: sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==} + engines: {node: '>=4'} + dev: true + + /pino-abstract-transport/1.0.0: + resolution: {integrity: sha512-c7vo5OpW4wIS42hUVcT5REsL8ZljsUfBjqV/e2sFxmFEFZiq1XLUp5EYLtuDH6PEHq9W1egWqRbnLUP5FuZmOA==} + dependencies: + readable-stream: 4.3.0 + split2: 4.1.0 + + /pino-pretty/9.1.1: + resolution: {integrity: sha512-iJrnjgR4FWQIXZkUF48oNgoRI9BpyMhaEmihonHeCnZ6F50ZHAS4YGfGBT/ZVNsPmd+hzkIPGzjKdY08+/yAXw==} + hasBin: true + dependencies: + colorette: 2.0.19 + dateformat: 4.6.3 + fast-copy: 3.0.0 + fast-safe-stringify: 2.1.1 + help-me: 4.2.0 + joycon: 3.1.1 + minimist: 1.2.7 + on-exit-leak-free: 2.1.0 + pino-abstract-transport: 1.0.0 + pump: 3.0.0 + readable-stream: 4.3.0 + secure-json-parse: 2.7.0 + sonic-boom: 3.2.1 + strip-json-comments: 3.1.1 + dev: true + + /pino-seq/0.9.0: + resolution: {integrity: sha512-XbvixSHuoC6IXQ6rbdD2YFZDaPFTY505uEzWX4rNpiZQwecbwHzSyBxTdvgwLBR4WXOzfKWixKn/vXbhclIijw==} + hasBin: true + dependencies: + '@types/pino': 5.20.0 + commander: 2.20.3 + seq-logging: 1.1.2 + split2: 3.2.2 + dev: false + + /pino-std-serializers/6.1.0: + resolution: {integrity: sha512-KO0m2f1HkrPe9S0ldjx7za9BJjeHqBku5Ch8JyxETxT8dEFGz1PwgrHaOQupVYitpzbFSYm7nnljxD8dik2c+g==} + dev: false + + /pino/8.9.0: + resolution: {integrity: sha512-/x9qSrFW4wh+7OL5bLIbfl06aK/8yCSIldhD3VmVGiVYWSdFFpXvJh/4xWKENs+DhG1VkJnnpWxMF6fZ2zGXeg==} + hasBin: true + dependencies: + atomic-sleep: 1.0.0 + fast-redact: 3.1.2 + on-exit-leak-free: 2.1.0 + pino-abstract-transport: 1.0.0 + pino-std-serializers: 6.1.0 + process-warning: 2.1.0 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.4.2 + sonic-boom: 3.2.1 + thread-stream: 2.3.0 + dev: false + + /playwright-core/1.30.0: + resolution: {integrity: sha512-7AnRmTCf+GVYhHbLJsGUtskWTE33SwMZkybJ0v6rqR1boxq2x36U7p1vDRV7HO2IwTZgmycracLxPEJI49wu4g==} + engines: {node: '>=14'} + hasBin: true + dev: true + + /postcss-import/14.1.0_postcss@8.4.21: + resolution: {integrity: sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==} + engines: {node: '>=10.0.0'} + peerDependencies: + postcss: ^8.0.0 + dependencies: + postcss: 8.4.21 + postcss-value-parser: 4.2.0 + read-cache: 1.0.0 + resolve: 1.22.1 + dev: true + + /postcss-js/4.0.1_postcss@8.4.21: + resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==} + engines: {node: ^12 || ^14 || >= 16} + peerDependencies: + postcss: ^8.4.21 + dependencies: + camelcase-css: 2.0.1 + postcss: 8.4.21 + dev: true + + /postcss-load-config/3.1.4_postcss@8.4.21: + resolution: {integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==} + engines: {node: '>= 10'} + peerDependencies: + postcss: '>=8.0.9' + ts-node: '>=9.0.0' + peerDependenciesMeta: + postcss: + optional: true + ts-node: + optional: true + dependencies: + lilconfig: 2.0.6 + postcss: 8.4.21 + yaml: 1.10.2 + dev: true + + /postcss-load-config/4.0.1_postcss@8.4.21: + resolution: {integrity: sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==} + engines: {node: '>= 14'} + peerDependencies: + postcss: '>=8.0.9' + ts-node: '>=9.0.0' + peerDependenciesMeta: + postcss: + optional: true + ts-node: + optional: true + dependencies: + lilconfig: 2.0.6 + postcss: 8.4.21 + yaml: 2.2.1 + dev: true + + /postcss-nested/6.0.0_postcss@8.4.21: + resolution: {integrity: sha512-0DkamqrPcmkBDsLn+vQDIrtkSbNkv5AD/M322ySo9kqFkCIYklym2xEmWkwo+Y3/qZo34tzEPNUw4y7yMCdv5w==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.2.14 + dependencies: + postcss: 8.4.21 + postcss-selector-parser: 6.0.11 + dev: true + + /postcss-selector-parser/6.0.11: + resolution: {integrity: sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g==} + engines: {node: '>=4'} + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + dev: true + + /postcss-value-parser/4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + dev: true + + /postcss/8.4.21: + resolution: {integrity: sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==} + engines: {node: ^10 || ^12 || >=14} + dependencies: + nanoid: 3.3.4 + picocolors: 1.0.0 + source-map-js: 1.0.2 + dev: true + + /pretty-bytes/5.6.0: + resolution: {integrity: sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==} + engines: {node: '>=6'} + dev: true + + /pretty-bytes/6.1.0: + resolution: {integrity: sha512-Rk753HI8f4uivXi4ZCIYdhmG1V+WKzvRMg/X+M42a6t7D07RcmopXJMDNk6N++7Bl75URRGsb40ruvg7Hcp2wQ==} + engines: {node: ^14.13.1 || >=16.0.0} + dev: true + + /process-warning/2.1.0: + resolution: {integrity: sha512-9C20RLxrZU/rFnxWncDkuF6O999NdIf3E1ws4B0ZeY3sRVPzWBMsYDE2lxjxhiXxg464cQTgKUGm8/i6y2YGXg==} + dev: false + + /process/0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + + /pump/3.0.0: + resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==} + dependencies: + end-of-stream: 1.4.4 + once: 1.4.0 + dev: true + + /punycode/2.3.0: + resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} + engines: {node: '>=6'} + dev: true + + /queue-microtask/1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + dev: true + + /quick-format-unescaped/4.0.4: + resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + dev: false + + /quick-lru/5.1.1: + resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} + engines: {node: '>=10'} + dev: true + + /randombytes/2.1.0: + resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} + dependencies: + safe-buffer: 5.2.1 + dev: true + + /read-cache/1.0.0: + resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} + dependencies: + pify: 2.3.0 + dev: true + + /read-pkg/3.0.0: + resolution: {integrity: sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==} + engines: {node: '>=4'} + dependencies: + load-json-file: 4.0.0 + normalize-package-data: 2.5.0 + path-type: 3.0.0 + dev: true + + /readable-stream/3.6.0: + resolution: {integrity: sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==} + engines: {node: '>= 6'} + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + /readable-stream/4.3.0: + resolution: {integrity: sha512-MuEnA0lbSi7JS8XM+WNJlWZkHAAdm7gETHdFK//Q/mChGyj2akEFtdLZh32jSdkWGbRwCW9pn6g3LWDdDeZnBQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + abort-controller: 3.0.0 + buffer: 6.0.3 + events: 3.3.0 + process: 0.11.10 + + /readdirp/3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + dependencies: + picomatch: 2.3.1 + dev: true + + /real-require/0.2.0: + resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} + engines: {node: '>= 12.13.0'} + dev: false + + /regenerate-unicode-properties/10.1.0: + resolution: {integrity: sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==} + engines: {node: '>=4'} + dependencies: + regenerate: 1.4.2 + dev: true + + /regenerate/1.4.2: + resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==} + dev: true + + /regenerator-runtime/0.13.11: + resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} + dev: true + + /regenerator-transform/0.15.1: + resolution: {integrity: sha512-knzmNAcuyxV+gQCufkYcvOqX/qIIfHLv0u5x79kRxuGojfYVky1f15TzZEu2Avte8QGepvUNTnLskf8E6X6Vyg==} + dependencies: + '@babel/runtime': 7.20.13 + dev: true + + /regexp.prototype.flags/1.4.3: + resolution: {integrity: sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.1.4 + functions-have-names: 1.2.3 + dev: true + + /regexpu-core/5.3.0: + resolution: {integrity: sha512-ZdhUQlng0RoscyW7jADnUZ25F5eVtHdMyXSb2PiwafvteRAOJUjFoUPEYZSIfP99fBIs3maLIRfpEddT78wAAQ==} + engines: {node: '>=4'} + dependencies: + '@babel/regjsgen': 0.8.0 + regenerate: 1.4.2 + regenerate-unicode-properties: 10.1.0 + regjsparser: 0.9.1 + unicode-match-property-ecmascript: 2.0.0 + unicode-match-property-value-ecmascript: 2.1.0 + dev: true + + /regjsparser/0.9.1: + resolution: {integrity: sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==} + hasBin: true + dependencies: + jsesc: 0.5.0 + dev: true + + /require-from-string/2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + dev: true + + /resolve-from/4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + dev: true + + /resolve/1.22.1: + resolution: {integrity: sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==} + hasBin: true + dependencies: + is-core-module: 2.11.0 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + dev: true + + /reusify/1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + dev: true + + /rimraf/2.7.1: + resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==} + hasBin: true + dependencies: + glob: 7.2.3 + dev: true + + /rollup-plugin-terser/7.0.2_rollup@2.79.1: + resolution: {integrity: sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ==} + deprecated: This package has been deprecated and is no longer maintained. Please use @rollup/plugin-terser + peerDependencies: + rollup: ^2.0.0 + dependencies: + '@babel/code-frame': 7.18.6 + jest-worker: 26.6.2 + rollup: 2.79.1 + serialize-javascript: 4.0.0 + terser: 5.16.3 + dev: true + + /rollup/2.79.1: + resolution: {integrity: sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==} + engines: {node: '>=10.0.0'} + hasBin: true + optionalDependencies: + fsevents: 2.3.2 + dev: true + + /rollup/3.14.0: + resolution: {integrity: sha512-o23sdgCLcLSe3zIplT9nQ1+r97okuaiR+vmAPZPTDYB7/f3tgWIYNyiQveMsZwshBT0is4eGax/HH83Q7CG+/Q==} + engines: {node: '>=14.18.0', npm: '>=8.0.0'} + hasBin: true + optionalDependencies: + fsevents: 2.3.2 + dev: true + + /run-parallel/1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + dependencies: + queue-microtask: 1.2.3 + dev: true + + /sade/1.8.1: + resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} + engines: {node: '>=6'} + dependencies: + mri: 1.2.0 + dev: true + + /safe-buffer/5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + /safe-regex-test/1.0.0: + resolution: {integrity: sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.0 + is-regex: 1.1.4 + dev: true + + /safe-stable-stringify/2.4.2: + resolution: {integrity: sha512-gMxvPJYhP0O9n2pvcfYfIuYgbledAOJFcqRThtPRmjscaipiwcwPPKLytpVzMkG2HAN87Qmo2d4PtGiri1dSLA==} + engines: {node: '>=10'} + dev: false + + /sander/0.5.1: + resolution: {integrity: sha512-3lVqBir7WuKDHGrKRDn/1Ye3kwpXaDOMsiRP1wd6wpZW56gJhsbp5RqQpA6JG/P+pkXizygnr1dKR8vzWaVsfA==} + dependencies: + es6-promise: 3.3.1 + graceful-fs: 4.2.10 + mkdirp: 0.5.6 + rimraf: 2.7.1 + dev: true + + /secure-json-parse/2.7.0: + resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==} + dev: true + + /semver/5.7.1: + resolution: {integrity: sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==} + hasBin: true + dev: true + + /semver/6.3.0: + resolution: {integrity: sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==} + hasBin: true + dev: true + + /seq-logging/1.1.2: + resolution: {integrity: sha512-9n7bCIHiMdBene104oSEa2917OcNBw+uee2v+we4AQxmjqt/aeQkWy1296IvGsogbj5fK6wuDNhVhm/DYmauVA==} + dev: false + + /serialize-javascript/4.0.0: + resolution: {integrity: sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==} + dependencies: + randombytes: 2.1.0 + dev: true + + /set-cookie-parser/2.5.1: + resolution: {integrity: sha512-1jeBGaKNGdEq4FgIrORu/N570dwoPYio8lSoYLWmX7sQ//0JY08Xh9o5pBcgmHQ/MbsYp/aZnOe1s1lIsbLprQ==} + dev: true + + /shebang-command/1.2.0: + resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==} + engines: {node: '>=0.10.0'} + dependencies: + shebang-regex: 1.0.0 + dev: true + + /shebang-regex/1.0.0: + resolution: {integrity: sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==} + engines: {node: '>=0.10.0'} + dev: true + + /shell-quote/1.8.0: + resolution: {integrity: sha512-QHsz8GgQIGKlRi24yFc6a6lN69Idnx634w49ay6+jA5yFh7a1UY+4Rp6HPx/L/1zcEDPEij8cIsiqR6bQsE5VQ==} + dev: true + + /side-channel/1.0.4: + resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.0 + object-inspect: 1.12.3 + dev: true + + /sirv/2.0.2: + resolution: {integrity: sha512-4Qog6aE29nIjAOKe/wowFTxOdmbEZKb+3tsLljaBRzJwtqto0BChD2zzH0LhgCSXiI+V7X+Y45v14wBZQ1TK3w==} + engines: {node: '>= 10'} + dependencies: + '@polka/url': 1.0.0-next.21 + mrmime: 1.0.1 + totalist: 3.0.0 + dev: true + + /sonic-boom/3.2.1: + resolution: {integrity: sha512-iITeTHxy3B9FGu8aVdiDXUVAcHMF9Ss0cCsAOo2HfCrmVGT3/DT5oYaeu0M/YKZDlKTvChEyPq0zI9Hf33EX6A==} + dependencies: + atomic-sleep: 1.0.0 + + /sorcery/0.11.0: + resolution: {integrity: sha512-J69LQ22xrQB1cIFJhPfgtLuI6BpWRiWu1Y3vSsIwK/eAScqJxd/+CJlUuHQRdX2C9NGFamq+KqNywGgaThwfHw==} + hasBin: true + dependencies: + '@jridgewell/sourcemap-codec': 1.4.14 + buffer-crc32: 0.2.13 + minimist: 1.2.7 + sander: 0.5.1 + dev: true + + /source-map-js/1.0.2: + resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} + engines: {node: '>=0.10.0'} + dev: true + + /source-map-support/0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + dev: true + + /source-map/0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + dev: true + + /source-map/0.8.0-beta.0: + resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} + engines: {node: '>= 8'} + dependencies: + whatwg-url: 7.1.0 + dev: true + + /sourcemap-codec/1.4.8: + resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} + deprecated: Please use @jridgewell/sourcemap-codec instead + dev: true + + /spdx-correct/3.1.1: + resolution: {integrity: sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==} + dependencies: + spdx-expression-parse: 3.0.1 + spdx-license-ids: 3.0.12 + dev: true + + /spdx-exceptions/2.3.0: + resolution: {integrity: sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==} + dev: true + + /spdx-expression-parse/3.0.1: + resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} + dependencies: + spdx-exceptions: 2.3.0 + spdx-license-ids: 3.0.12 + dev: true + + /spdx-license-ids/3.0.12: + resolution: {integrity: sha512-rr+VVSXtRhO4OHbXUiAF7xW3Bo9DuuF6C5jH+q/x15j2jniycgKbxU09Hr0WqlSLUs4i4ltHGXqTe7VHclYWyA==} + dev: true + + /split2/3.2.2: + resolution: {integrity: sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==} + dependencies: + readable-stream: 3.6.0 + dev: false + + /split2/4.1.0: + resolution: {integrity: sha512-VBiJxFkxiXRlUIeyMQi8s4hgvKCSjtknJv/LVYbrgALPwf5zSKmEwV9Lst25AkvMDnvxODugjdl6KZgwKM1WYQ==} + engines: {node: '>= 10.x'} + + /streamsearch/1.1.0: + resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} + engines: {node: '>=10.0.0'} + dev: true + + /string.prototype.matchall/4.0.8: + resolution: {integrity: sha512-6zOCOcJ+RJAQshcTvXPHoxoQGONa3e/Lqx90wUA+wEzX78sg5Bo+1tQo4N0pohS0erG9qtCqJDjNCQBjeWVxyg==} + dependencies: + call-bind: 1.0.2 + define-properties: 1.1.4 + es-abstract: 1.21.1 + get-intrinsic: 1.2.0 + has-symbols: 1.0.3 + internal-slot: 1.0.4 + regexp.prototype.flags: 1.4.3 + side-channel: 1.0.4 + dev: true + + /string.prototype.padend/3.1.4: + resolution: {integrity: sha512-67otBXoksdjsnXXRUq+KMVTdlVRZ2af422Y0aTyTjVaoQkGr3mxl2Bc5emi7dOQ3OGVVQQskmLEWwFXwommpNw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.1.4 + es-abstract: 1.21.1 + dev: true + + /string.prototype.trimend/1.0.6: + resolution: {integrity: sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==} + dependencies: + call-bind: 1.0.2 + define-properties: 1.1.4 + es-abstract: 1.21.1 + dev: true + + /string.prototype.trimstart/1.0.6: + resolution: {integrity: sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==} + dependencies: + call-bind: 1.0.2 + define-properties: 1.1.4 + es-abstract: 1.21.1 + dev: true + + /string_decoder/1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + dependencies: + safe-buffer: 5.2.1 + + /stringify-object/3.3.0: + resolution: {integrity: sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==} + engines: {node: '>=4'} + dependencies: + get-own-enumerable-property-symbols: 3.0.2 + is-obj: 1.0.1 + is-regexp: 1.0.0 + dev: true + + /strip-bom/3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + dev: true + + /strip-comments/2.0.1: + resolution: {integrity: sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==} + engines: {node: '>=10'} + dev: true + + /strip-indent/3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + dependencies: + min-indent: 1.0.1 + dev: true + + /strip-json-comments/3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + dev: true + + /supports-color/5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + dependencies: + has-flag: 3.0.0 + dev: true + + /supports-color/7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + dependencies: + has-flag: 4.0.0 + dev: true + + /supports-preserve-symlinks-flag/1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + dev: true + + /svelte-check/3.0.3_gqx7lw3sljhsd4bstor5m2aa2u: + resolution: {integrity: sha512-ByBFXo3bfHRGIsYEasHkdMhLkNleVfszX/Ns1oip58tPJlKdo5Ssr8kgVIuo5oq00hss8AIcdesuy0Xt0BcTvg==} + hasBin: true + peerDependencies: + svelte: ^3.55.0 + dependencies: + '@jridgewell/trace-mapping': 0.3.17 + chokidar: 3.5.3 + fast-glob: 3.2.12 + import-fresh: 3.3.0 + picocolors: 1.0.0 + sade: 1.8.1 + svelte: 3.55.1 + svelte-preprocess: 5.0.1_b25a55hc532q56kmuqlrolam2i + typescript: 4.9.5 + transitivePeerDependencies: + - '@babel/core' + - coffeescript + - less + - postcss + - postcss-load-config + - pug + - sass + - stylus + - sugarss + dev: true + + /svelte-headless-table/0.17.2_svelte@3.55.1: + resolution: {integrity: sha512-XYG3PC6HzotIhzeqb+M+HGwtkfIhuRJr2+BgRYYD4xTANEUtlWg6M8tIgQp7fqJf7VKOdhmXQbyvjWKE/VGG3w==} + dependencies: + svelte-keyed: 1.1.6_svelte@3.55.1 + svelte-render: 1.6.0 + svelte-subscribe: 1.0.5 + transitivePeerDependencies: + - svelte + dev: false + + /svelte-hmr/0.15.1_svelte@3.55.1: + resolution: {integrity: sha512-BiKB4RZ8YSwRKCNVdNxK/GfY+r4Kjgp9jCLEy0DuqAKfmQtpL38cQK3afdpjw4sqSs4PLi3jIPJIFp259NkZtA==} + engines: {node: ^12.20 || ^14.13.1 || >= 16} + peerDependencies: + svelte: '>=3.19.0' + dependencies: + svelte: 3.55.1 + dev: true + + /svelte-keyed/1.1.6_svelte@3.55.1: + resolution: {integrity: sha512-sd/7j3waSpOkeiQqATstlvNySbEpSs49aoaZ/nc09x/iEndnXQ/9Zy5PTD06+C7hlZk8KjZ4bhrktAQ791jWMQ==} + peerDependencies: + svelte: ^3.49.0 + dependencies: + svelte: 3.55.1 + dev: false + + /svelte-preprocess/5.0.1_b25a55hc532q56kmuqlrolam2i: + resolution: {integrity: sha512-0HXyhCoc9rsW4zGOgtInylC6qj259E1hpFnJMJWTf+aIfeqh4O/QHT31KT2hvPEqQfdjmqBR/kO2JDkkciBLrQ==} + engines: {node: '>= 14.10.0'} + requiresBuild: true + 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 + 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 + typescript: ^3.9.5 || ^4.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 + dependencies: + '@types/pug': 2.0.6 + '@types/sass': 1.43.1 + detect-indent: 6.1.0 + magic-string: 0.27.0 + postcss: 8.4.21 + postcss-load-config: 4.0.1_postcss@8.4.21 + sorcery: 0.11.0 + strip-indent: 3.0.0 + svelte: 3.55.1 + typescript: 4.9.5 + dev: true + + /svelte-render/1.6.0: + resolution: {integrity: sha512-Y6Ea6U768MuhH0xR72pAPnXdSnJ+4yi24YyD/b9Q46yGalQFVxUyan496EELWtQEvgQE9Z2PsFAG8JjvrWcDAg==} + dependencies: + svelte-subscribe: 1.0.5 + dev: false + + /svelte-subscribe/1.0.5: + resolution: {integrity: sha512-p+vRSBVzR9BQC72mjd2eqCv8zx5euLZQJF7QqAw5d41aKzQVOq90y71/NXch+nDNMjWbRo0CX+brcWYgPryJlw==} + dev: false + + /svelte/3.55.1: + resolution: {integrity: sha512-S+87/P0Ve67HxKkEV23iCdAh/SX1xiSfjF1HOglno/YTbSTW7RniICMCofWGdJJbdjw3S+0PfFb1JtGfTXE0oQ==} + engines: {node: '>= 8'} + + /tailwindcss/3.2.6_postcss@8.4.21: + resolution: {integrity: sha512-BfgQWZrtqowOQMC2bwaSNe7xcIjdDEgixWGYOd6AL0CbKHJlvhfdbINeAW76l1sO+1ov/MJ93ODJ9yluRituIw==} + engines: {node: '>=12.13.0'} + hasBin: true + peerDependencies: + postcss: ^8.0.9 + dependencies: + arg: 5.0.2 + chokidar: 3.5.3 + color-name: 1.1.4 + detective: 5.2.1 + didyoumean: 1.2.2 + dlv: 1.1.3 + fast-glob: 3.2.12 + glob-parent: 6.0.2 + is-glob: 4.0.3 + lilconfig: 2.0.6 + micromatch: 4.0.5 + normalize-path: 3.0.0 + object-hash: 3.0.0 + picocolors: 1.0.0 + postcss: 8.4.21 + postcss-import: 14.1.0_postcss@8.4.21 + postcss-js: 4.0.1_postcss@8.4.21 + postcss-load-config: 3.1.4_postcss@8.4.21 + postcss-nested: 6.0.0_postcss@8.4.21 + postcss-selector-parser: 6.0.11 + postcss-value-parser: 4.2.0 + quick-lru: 5.1.1 + resolve: 1.22.1 + transitivePeerDependencies: + - ts-node + dev: true + + /temp-dir/2.0.0: + resolution: {integrity: sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==} + engines: {node: '>=8'} + dev: true + + /temporal-polyfill/0.0.8: + resolution: {integrity: sha512-IuA8GhS1PRC04H/zVNAIxJvCZQum6V5HjqFj7gz1a3SMUf/Kf1xIXILNYtxrWYnGqIU/RrDRxlCKCm/vmqnBvw==} + dependencies: + temporal-spec: 0.0.3 + dev: false + + /temporal-spec/0.0.3: + resolution: {integrity: sha512-gJu7QRqn5c2vTSkYWGC4qz1i+FZ9C+Cz16UIBMRcjgXOsHfXeSIgaWUKeq/2rz1iNfFxvmF/ywqbfC6ggTpjkA==} + dev: false + + /tempy/0.6.0: + resolution: {integrity: sha512-G13vtMYPT/J8A4X2SjdtBTphZlrp1gKv6hZiOjw14RCWg6GbHuQBGtjlx75xLbYV/wEc0D7G5K4rxKP/cXk8Bw==} + engines: {node: '>=10'} + dependencies: + is-stream: 2.0.1 + temp-dir: 2.0.0 + type-fest: 0.16.0 + unique-string: 2.0.0 + dev: true + + /terser/5.16.3: + resolution: {integrity: sha512-v8wWLaS/xt3nE9dgKEWhNUFP6q4kngO5B8eYFUuebsu7Dw/UNAnpUod6UHo04jSSkv8TzKHjZDSd7EXdDQAl8Q==} + engines: {node: '>=10'} + hasBin: true + dependencies: + '@jridgewell/source-map': 0.3.2 + acorn: 8.8.2 + commander: 2.20.3 + source-map-support: 0.5.21 + dev: true + + /thread-stream/2.3.0: + resolution: {integrity: sha512-kaDqm1DET9pp3NXwR8382WHbnpXnRkN9xGN9dQt3B2+dmXiW8X1SOwmFOxAErEQ47ObhZ96J6yhZNXuyCOL7KA==} + dependencies: + real-require: 0.2.0 + dev: false + + /tiny-glob/0.2.9: + resolution: {integrity: sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==} + dependencies: + globalyzer: 0.1.0 + globrex: 0.1.2 + dev: true + + /to-fast-properties/2.0.0: + resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} + engines: {node: '>=4'} + dev: true + + /to-regex-range/5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + dependencies: + is-number: 7.0.0 + dev: true + + /totalist/3.0.0: + resolution: {integrity: sha512-eM+pCBxXO/njtF7vdFsHuqb+ElbxqtI4r5EAvk6grfAFyJ6IvWlSkfZ5T9ozC6xWw3Fj1fGoSmrl0gUs46JVIw==} + engines: {node: '>=6'} + dev: true + + /tr46/1.0.1: + resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} + dependencies: + punycode: 2.3.0 + dev: true + + /tslib/2.5.0: + resolution: {integrity: sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==} + dev: true + + /turbo-query/1.9.0: + resolution: {integrity: sha512-14E/AJkYEexNBP6PiT4lJCPZloCLVg5HIPE1OrmcIAQmrwgq0QdJL3eIjYebCQ5uvTgIyg+jp/J7/o6l+3GTrQ==} + dev: false + + /type-fest/0.16.0: + resolution: {integrity: sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==} + engines: {node: '>=10'} + dev: true + + /typed-array-length/1.0.4: + resolution: {integrity: sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==} + dependencies: + call-bind: 1.0.2 + for-each: 0.3.3 + is-typed-array: 1.1.10 + dev: true + + /typesafe-i18n/5.24.0_typescript@4.9.5: + resolution: {integrity: sha512-GGIV+x+Azs+uVe1940ZX3MtIKSN0eXrO/x1Z7d0B/FO610evlmTEBIIYIHFWvjhZJslty11INedwRkZKSDVwTQ==} + hasBin: true + peerDependencies: + typescript: '>=3.5.1' + dependencies: + typescript: 4.9.5 + dev: true + + /typescript/4.9.5: + resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==} + engines: {node: '>=4.2.0'} + hasBin: true + dev: true + + /unbox-primitive/1.0.2: + resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} + dependencies: + call-bind: 1.0.2 + has-bigints: 1.0.2 + has-symbols: 1.0.3 + which-boxed-primitive: 1.0.2 + dev: true + + /undici/5.18.0: + resolution: {integrity: sha512-1iVwbhonhFytNdg0P4PqyIAXbdlVZVebtPDvuM36m66mRw4OGrCm2MYynJv/UENFLdP13J1nPVQzVE2zTs1OeA==} + engines: {node: '>=12.18'} + dependencies: + busboy: 1.6.0 + dev: true + + /unicode-canonical-property-names-ecmascript/2.0.0: + resolution: {integrity: sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==} + engines: {node: '>=4'} + dev: true + + /unicode-match-property-ecmascript/2.0.0: + resolution: {integrity: sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==} + engines: {node: '>=4'} + dependencies: + unicode-canonical-property-names-ecmascript: 2.0.0 + unicode-property-aliases-ecmascript: 2.1.0 + dev: true + + /unicode-match-property-value-ecmascript/2.1.0: + resolution: {integrity: sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==} + engines: {node: '>=4'} + dev: true + + /unicode-property-aliases-ecmascript/2.1.0: + resolution: {integrity: sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==} + engines: {node: '>=4'} + dev: true + + /unique-string/2.0.0: + resolution: {integrity: sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==} + engines: {node: '>=8'} + dependencies: + crypto-random-string: 2.0.0 + dev: true + + /universalify/2.0.0: + resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==} + engines: {node: '>= 10.0.0'} + dev: true + + /upath/1.2.0: + resolution: {integrity: sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==} + engines: {node: '>=4'} + dev: true + + /update-browserslist-db/1.0.10_browserslist@4.21.5: + resolution: {integrity: sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + dependencies: + browserslist: 4.21.5 + escalade: 3.1.1 + picocolors: 1.0.0 + dev: true + + /uri-js/4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + dependencies: + punycode: 2.3.0 + dev: true + + /util-deprecate/1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + /validate-npm-package-license/3.0.4: + resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} + dependencies: + spdx-correct: 3.1.1 + spdx-expression-parse: 3.0.1 + dev: true + + /vite-plugin-pwa/0.14.3_vite@4.1.1: + resolution: {integrity: sha512-o/CEzdHXamdSV4WJ6hp1EQNe+yVvoFf9b5q1nMhOSqKxaW7BaDqAHAwnq8jt21wakDmcaipnuF3/j78AzZJ6wg==} + peerDependencies: + vite: ^3.1.0 || ^4.0.0 + dependencies: + '@rollup/plugin-replace': 5.0.2_rollup@3.14.0 + debug: 4.3.4 + fast-glob: 3.2.12 + pretty-bytes: 6.1.0 + rollup: 3.14.0 + vite: 4.1.1 + workbox-build: 6.5.4 + workbox-window: 6.5.4 + transitivePeerDependencies: + - '@types/babel__core' + - supports-color + dev: true + + /vite/4.1.1: + resolution: {integrity: sha512-LM9WWea8vsxhr782r9ntg+bhSFS06FJgCvvB0+8hf8UWtvaiDagKYWXndjfX6kGl74keHJUcpzrQliDXZlF5yg==} + engines: {node: ^14.18.0 || >=16.0.0} + hasBin: true + peerDependencies: + '@types/node': '>= 14' + less: '*' + sass: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + sass: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + dependencies: + esbuild: 0.16.17 + postcss: 8.4.21 + resolve: 1.22.1 + rollup: 3.14.0 + optionalDependencies: + fsevents: 2.3.2 + dev: true + + /vitefu/0.2.4_vite@4.1.1: + resolution: {integrity: sha512-fanAXjSaf9xXtOOeno8wZXIhgia+CZury481LsDaV++lSvcU2R9Ch2bPh3PYFyoHW+w9LqAeYRISVQjUIew14g==} + peerDependencies: + vite: ^3.0.0 || ^4.0.0 + peerDependenciesMeta: + vite: + optional: true + dependencies: + vite: 4.1.1 + dev: true + + /webidl-conversions/4.0.2: + resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} + dev: true + + /whatwg-url/7.1.0: + resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} + dependencies: + lodash.sortby: 4.7.0 + tr46: 1.0.1 + webidl-conversions: 4.0.2 + dev: true + + /which-boxed-primitive/1.0.2: + resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} + dependencies: + is-bigint: 1.0.4 + is-boolean-object: 1.1.2 + is-number-object: 1.0.7 + is-string: 1.0.7 + is-symbol: 1.0.4 + dev: true + + /which-typed-array/1.1.9: + resolution: {integrity: sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.5 + call-bind: 1.0.2 + for-each: 0.3.3 + gopd: 1.0.1 + has-tostringtag: 1.0.0 + is-typed-array: 1.1.10 + dev: true + + /which/1.3.1: + resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} + hasBin: true + dependencies: + isexe: 2.0.0 + dev: true + + /workbox-background-sync/6.5.4: + resolution: {integrity: sha512-0r4INQZMyPky/lj4Ou98qxcThrETucOde+7mRGJl13MPJugQNKeZQOdIJe/1AchOP23cTqHcN/YVpD6r8E6I8g==} + dependencies: + idb: 7.1.1 + workbox-core: 6.5.4 + dev: true + + /workbox-broadcast-update/6.5.4: + resolution: {integrity: sha512-I/lBERoH1u3zyBosnpPEtcAVe5lwykx9Yg1k6f8/BGEPGaMMgZrwVrqL1uA9QZ1NGGFoyE6t9i7lBjOlDhFEEw==} + dependencies: + workbox-core: 6.5.4 + dev: true + + /workbox-build/6.5.4: + resolution: {integrity: sha512-kgRevLXEYvUW9WS4XoziYqZ8Q9j/2ziJYEtTrjdz5/L/cTUa2XfyMP2i7c3p34lgqJ03+mTiz13SdFef2POwbA==} + engines: {node: '>=10.0.0'} + dependencies: + '@apideck/better-ajv-errors': 0.3.6_ajv@8.12.0 + '@babel/core': 7.20.12 + '@babel/preset-env': 7.20.2_@babel+core@7.20.12 + '@babel/runtime': 7.20.13 + '@rollup/plugin-babel': 5.3.1_3dsfpkpoyvuuxyfgdbpn4j4uzm + '@rollup/plugin-node-resolve': 11.2.1_rollup@2.79.1 + '@rollup/plugin-replace': 2.4.2_rollup@2.79.1 + '@surma/rollup-plugin-off-main-thread': 2.2.3 + ajv: 8.12.0 + common-tags: 1.8.2 + fast-json-stable-stringify: 2.1.0 + fs-extra: 9.1.0 + glob: 7.2.3 + lodash: 4.17.21 + pretty-bytes: 5.6.0 + rollup: 2.79.1 + rollup-plugin-terser: 7.0.2_rollup@2.79.1 + source-map: 0.8.0-beta.0 + stringify-object: 3.3.0 + strip-comments: 2.0.1 + tempy: 0.6.0 + upath: 1.2.0 + workbox-background-sync: 6.5.4 + workbox-broadcast-update: 6.5.4 + workbox-cacheable-response: 6.5.4 + workbox-core: 6.5.4 + workbox-expiration: 6.5.4 + workbox-google-analytics: 6.5.4 + workbox-navigation-preload: 6.5.4 + workbox-precaching: 6.5.4 + workbox-range-requests: 6.5.4 + workbox-recipes: 6.5.4 + workbox-routing: 6.5.4 + workbox-strategies: 6.5.4 + workbox-streams: 6.5.4 + workbox-sw: 6.5.4 + workbox-window: 6.5.4 + transitivePeerDependencies: + - '@types/babel__core' + - supports-color + dev: true + + /workbox-cacheable-response/6.5.4: + resolution: {integrity: sha512-DCR9uD0Fqj8oB2TSWQEm1hbFs/85hXXoayVwFKLVuIuxwJaihBsLsp4y7J9bvZbqtPJ1KlCkmYVGQKrBU4KAug==} + dependencies: + workbox-core: 6.5.4 + dev: true + + /workbox-core/6.5.4: + resolution: {integrity: sha512-OXYb+m9wZm8GrORlV2vBbE5EC1FKu71GGp0H4rjmxmF4/HLbMCoTFws87M3dFwgpmg0v00K++PImpNQ6J5NQ6Q==} + dev: true + + /workbox-expiration/6.5.4: + resolution: {integrity: sha512-jUP5qPOpH1nXtjGGh1fRBa1wJL2QlIb5mGpct3NzepjGG2uFFBn4iiEBiI9GUmfAFR2ApuRhDydjcRmYXddiEQ==} + dependencies: + idb: 7.1.1 + workbox-core: 6.5.4 + dev: true + + /workbox-google-analytics/6.5.4: + resolution: {integrity: sha512-8AU1WuaXsD49249Wq0B2zn4a/vvFfHkpcFfqAFHNHwln3jK9QUYmzdkKXGIZl9wyKNP+RRX30vcgcyWMcZ9VAg==} + dependencies: + workbox-background-sync: 6.5.4 + workbox-core: 6.5.4 + workbox-routing: 6.5.4 + workbox-strategies: 6.5.4 + dev: true + + /workbox-navigation-preload/6.5.4: + resolution: {integrity: sha512-IIwf80eO3cr8h6XSQJF+Hxj26rg2RPFVUmJLUlM0+A2GzB4HFbQyKkrgD5y2d84g2IbJzP4B4j5dPBRzamHrng==} + dependencies: + workbox-core: 6.5.4 + dev: true + + /workbox-precaching/6.5.4: + resolution: {integrity: sha512-hSMezMsW6btKnxHB4bFy2Qfwey/8SYdGWvVIKFaUm8vJ4E53JAY+U2JwLTRD8wbLWoP6OVUdFlXsTdKu9yoLTg==} + dependencies: + workbox-core: 6.5.4 + workbox-routing: 6.5.4 + workbox-strategies: 6.5.4 + dev: true + + /workbox-range-requests/6.5.4: + resolution: {integrity: sha512-Je2qR1NXCFC8xVJ/Lux6saH6IrQGhMpDrPXWZWWS8n/RD+WZfKa6dSZwU+/QksfEadJEr/NfY+aP/CXFFK5JFg==} + dependencies: + workbox-core: 6.5.4 + dev: true + + /workbox-recipes/6.5.4: + resolution: {integrity: sha512-QZNO8Ez708NNwzLNEXTG4QYSKQ1ochzEtRLGaq+mr2PyoEIC1xFW7MrWxrONUxBFOByksds9Z4//lKAX8tHyUA==} + dependencies: + workbox-cacheable-response: 6.5.4 + workbox-core: 6.5.4 + workbox-expiration: 6.5.4 + workbox-precaching: 6.5.4 + workbox-routing: 6.5.4 + workbox-strategies: 6.5.4 + dev: true + + /workbox-routing/6.5.4: + resolution: {integrity: sha512-apQswLsbrrOsBUWtr9Lf80F+P1sHnQdYodRo32SjiByYi36IDyL2r7BH1lJtFX8fwNHDa1QOVY74WKLLS6o5Pg==} + dependencies: + workbox-core: 6.5.4 + dev: true + + /workbox-strategies/6.5.4: + resolution: {integrity: sha512-DEtsxhx0LIYWkJBTQolRxG4EI0setTJkqR4m7r4YpBdxtWJH1Mbg01Cj8ZjNOO8etqfA3IZaOPHUxCs8cBsKLw==} + dependencies: + workbox-core: 6.5.4 + dev: true + + /workbox-streams/6.5.4: + resolution: {integrity: sha512-FXKVh87d2RFXkliAIheBojBELIPnWbQdyDvsH3t74Cwhg0fDheL1T8BqSM86hZvC0ZESLsznSYWw+Va+KVbUzg==} + dependencies: + workbox-core: 6.5.4 + workbox-routing: 6.5.4 + dev: true + + /workbox-sw/6.5.4: + resolution: {integrity: sha512-vo2RQo7DILVRoH5LjGqw3nphavEjK4Qk+FenXeUsknKn14eCNedHOXWbmnvP4ipKhlE35pvJ4yl4YYf6YsJArA==} + dev: true + + /workbox-window/6.5.4: + resolution: {integrity: sha512-HnLZJDwYBE+hpG25AQBO8RUWBJRaCsI9ksQJEp3aCOFCaG5kqaToAYXFRAHxzRluM2cQbGzdQF5rjKPWPA1fug==} + dependencies: + '@types/trusted-types': 2.0.2 + workbox-core: 6.5.4 + dev: true + + /wrappy/1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + dev: true + + /xtend/4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + dev: true + + /yallist/3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + dev: true + + /yaml/1.10.2: + resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} + engines: {node: '>= 6'} + dev: true + + /yaml/2.2.1: + resolution: {integrity: sha512-e0WHiYql7+9wr4cWMx3TVQrNwejKaEe7/rHNmQmqRjazfOP5W8PB6Jpebb5o6fIapbz9o9+2ipcaTM2ZwDI6lw==} + engines: {node: '>= 14'} + dev: true diff --git a/code/app/postcss.config.cjs b/code/app/postcss.config.cjs new file mode 100644 index 0000000..a53e3b3 --- /dev/null +++ b/code/app/postcss.config.cjs @@ -0,0 +1,13 @@ +const tailwindcss = require("tailwindcss"); +const autoprefixer = require("autoprefixer"); +const nesting = require("tailwindcss/nesting"); + +const config = { + plugins: [ + nesting, + tailwindcss, + autoprefixer + ], +}; + +module.exports = config; diff --git a/code/app/src/actions/pwKey.ts b/code/app/src/actions/pwKey.ts new file mode 100644 index 0000000..cf85685 --- /dev/null +++ b/code/app/src/actions/pwKey.ts @@ -0,0 +1,7 @@ +import { is_testing } from "$configuration"; + +export default function pwKey(node: HTMLElement, value: string | undefined) { + if (!value) return; + if (!is_testing()) return; + node.setAttribute("pw-key", value); +}
\ No newline at end of file diff --git a/code/app/src/app.d.ts b/code/app/src/app.d.ts new file mode 100644 index 0000000..31b276e --- /dev/null +++ b/code/app/src/app.d.ts @@ -0,0 +1,9 @@ +// See https://kit.svelte.dev/docs/types#app +// for information about these interfaces +// and what to do when importing types +declare namespace App { + interface Locals { } + interface Platform { } + interface PrivateEnv { } + interface PublicEnv { } +}
\ No newline at end of file diff --git a/code/app/src/app.html b/code/app/src/app.html new file mode 100644 index 0000000..308b223 --- /dev/null +++ b/code/app/src/app.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html class="h-full bg-white" lang="en"> + +<head> + <meta charset="utf-8" /> + <meta name="viewport" content="width=device-width" /> + %sveltekit.head% +</head> + +<body class="h-full"> + <div>%sveltekit.body%</div> +</body> + +</html>
\ No newline at end of file diff --git a/code/app/src/app.pcss b/code/app/src/app.pcss new file mode 100644 index 0000000..5450db4 --- /dev/null +++ b/code/app/src/app.pcss @@ -0,0 +1,34 @@ +/* Write your global styles here, in PostCSS syntax */ +@tailwind base; +@tailwind components; +@tailwind utilities; + +pre { + font-family: monospace !important; +} + +*:focus-visible { + outline: 1px auto; +} + +.c-disabled { + cursor: not-allowed !important; + filter: opacity(.45); + pointer-events: none !important; +} + +.c-disabled.loading { + cursor: wait !important; +} + +.link { + @apply text-blue-600 hover:text-blue-700 transition duration-300 ease-in-out mb-4 cursor-pointer; + + &.danger { + @apply text-red-600 hover:text-red-700; + } + + &.active { + @apply underline + } +} diff --git a/code/app/src/components/alert.svelte b/code/app/src/components/alert.svelte new file mode 100644 index 0000000..16d8340 --- /dev/null +++ b/code/app/src/components/alert.svelte @@ -0,0 +1,268 @@ +<script lang="ts"> + import {random_string} from "$utilities/misc-helpers"; + import {createEventDispatcher} from "svelte"; + import {onMount} from "svelte"; + import pwKey from "$actions/pwKey"; + import {Temporal} from "temporal-polyfill"; + import {ExclamationTriangleIcon, CheckCircleIcon, InformationCircleIcon, XCircleIcon, XMarkIcon} from "./icons"; + + const dispatch = createEventDispatcher(); + const noCooldownSetting = "no-cooldown"; + + let iconComponent: any; + let colorClassPart = ""; + + /** + * An optional id for this alert, a default is set if not specified. + * This value is necessary for closeable cooldown to work. + */ + // if no unique id is supplied, cooldown will not work between page loads. + // Therefore we are disabling it with noCooldownSetting in the fallback id. + export let id = "alert--" + noCooldownSetting + "--" + random_string(4); + /** + * The title to communicate, value is optional + */ + export let title = ""; + /** + * The message to communicate, value is optional + */ + export let message = ""; + /** + * Changes the alerts color and icon. + */ + export let type: "info" | "success" | "warning" | "error" = "info"; + /** + * If true the alert can be removed from the DOM by clicking on a X icon on the upper right hand courner + */ + export let closeable = false; + /** + * The amount of seconds that should go by before this alert is shown again, only works when a unique id is set. + * Set to ~ if it should only be shown once per client (State stored in localestorage). + **/ + export let closeableCooldown = "-1"; + /** + * The text that is displayed on the right link + */ + export let rightLinkText = ""; + /** + * An array of list items displayed under the message or title + */ + export let listItems: Array<string> = []; + /** + * An array of {id:string;text:string;color?:string}, where id is dispatched back as an svelte event with this syntax act$id (ex: on:actcancel). + * Text is the button text + * Color is the optional tailwind color to used, the value is used in classes like bg-$color-50. + */ + export let actions: Array<{ id: string; text: string; color?: string }> = []; + /** + * This value is set on a plain anchor tag without any svelte routing, + * listen to the on:rightLinkClick if you want to intercept the click without navigating + */ + export let rightLinkHref = "javascript:void(0)"; + $: cooldownEnabled = + id.indexOf(noCooldownSetting) === -1 && closeable && (closeableCooldown === "~" || parseInt(closeableCooldown) > 0); + /** + * Sets this alerts visibility state, when this is false it is removed from the dom using an {#if} block. + */ + export let visible = closeableCooldown === "~" || parseInt(closeableCooldown) > 0 ? false : true; + + export let _pwKey: string | undefined = undefined; + + const cooldownStorageKey = "lastseen--" + id; + + $: switch (type) { + case "info": { + colorClassPart = "blue"; + iconComponent = InformationCircleIcon; + break; + } + case "warning": { + colorClassPart = "yellow"; + iconComponent = ExclamationTriangleIcon; + break; + } + case "error": { + colorClassPart = "red"; + iconComponent = XCircleIcon; + break; + } + case "success": { + colorClassPart = "green"; + iconComponent = CheckCircleIcon; + break; + } + } + + function close() { + visible = false; + if (cooldownEnabled) { + console.log("Cooldown enabled for " + id + ", " + closeableCooldown === "~" ? "with an endless cooldown" : ""); + localStorage.setItem(cooldownStorageKey, String(Temporal.Now.instant().epochSeconds)); + } + } + + function rightLinkClicked() { + dispatch("rightLinkCliked"); + } + + function actionClicked(name: string) { + dispatch("act" + name); + } + + // Manages the state of the alert if cooldown is enabled + function run_cooldown() { + if (!cooldownEnabled) { + console.log("Alert cooldown is not enabled for " + id); + return; + } + if (!localStorage.getItem(cooldownStorageKey)) { + console.log("Alert " + id + " has not been seen yet, displaying"); + visible = true; + return; + } + // if (!visible) { + // console.log( + // "Alert " + id + " is not visible, stopping cooldown change" + // ); + // return; + // } + if (closeableCooldown === "~") { + console.log("Alert " + id + " has an infinite cooldown, hiding"); + visible = false; + return; + } + + const lastSeen = Temporal.Instant.fromEpochSeconds(parseInt(localStorage.getItem(cooldownStorageKey) ?? "-1")); + if (Temporal.Instant.compare(Temporal.Now.instant(), lastSeen.add({seconds: parseInt(closeableCooldown)})) === 1) { + console.log( + "Alert " + + id + + " has a cooldown of " + + closeableCooldown + + " and was last seen " + + lastSeen.toLocaleString() + + " making it due for a showing", + ); + visible = true; + } else { + visible = false; + } + } + + onMount(() => { + if (cooldownEnabled) { + run_cooldown(); + } + + if (closeable && closeableCooldown && id.indexOf(noCooldownSetting) !== -1) { + // TODO: This prints twice before shutting up as it should, in this example look at the only alert with closeableCooldown in alertsbook. + // Looks like svelte mounts three times and that my id is only set on the third. Not sure it does at all after logging the id onMount. + console.error("Alert cooldown does not work without specifying a unique id, related id: " + id); + } + }); +</script> + +{#if visible} + <div class="rounded-md bg-{colorClassPart}-50 p-4 {$$restProps.class ?? ''}" use:pwKey={_pwKey}> + <div class="flex"> + <div class="flex-shrink-0"> + <svelte:component this={iconComponent} class="text-{colorClassPart}-400"/> + </div> + <div class="ml-3 text-sm w-full"> + {#if !rightLinkText} + {#if title} + <h3 class="font-bold text-{colorClassPart}-800"> + {title} + </h3> + {/if} + {#if message} + <div class="{title ? 'mt-2' : ''} text-{colorClassPart}-700 justify-start"> + <p> + {@html message} + </p> + </div> + {/if} + {#if listItems?.length ?? 0} + <ul class="list-disc space-y-1 pl-5 text-{colorClassPart}-700"> + {#each listItems as listItem} + <li>{listItem}</li> + {/each} + </ul> + {/if} + {:else} + <div class="flex-1 md:flex md:justify-between"> + <div> + {#if title} + <h3 class="font-medium text-{colorClassPart}-800"> + {title} + </h3> + {/if} + {#if message} + <div class="{title ? 'mt-2' : ''} text-{colorClassPart}-700 justify-start"> + <p> + {@html message} + </p> + </div> + {/if} + {#if listItems?.length ?? 0} + <ul class="list-disc space-y-1 pl-5 text-{colorClassPart}-700"> + {#each listItems as listItem} + <li>{listItem}</li> + {/each} + </ul> + {/if} + </div> + <p class="mt-3 text-sm md:mt-0 md:ml-6 flex items-end"> + <a + href={rightLinkHref} + on:click={() => rightLinkClicked()} + class="whitespace-nowrap font-medium text-{colorClassPart}-700 hover:text-{colorClassPart}-600" + > + {rightLinkText} + <span aria-hidden="true"> →</span> + </a> + </p> + </div> + {/if} + {#if actions?.length ?? 0} + <div class="ml-2 mt-4"> + <div class="-mx-2 -my-1.5 flex gap-1"> + {#each actions as action} + {@const color = action?.color ?? colorClassPart} + <button + type="button" + on:click={() => actionClicked(action.id)} + class="rounded-md + bg-{color}-50 + px-2 py-1.5 text-sm font-medium + text-{color}-800 + hover:bg-{color}-100 + focus:outline-none focus:ring-2 + focus:ring-{color}-600 + focus:ring-offset-2 + focus:ring-offset-{color}-50" + > + {action.text} + </button> + {/each} + </div> + </div> + {/if} + </div> + {#if closeable} + <div class="ml-auto pl-3"> + <div class="-mx-1.5 -my-1.5"> + <button + type="button" + on:click={() => close()} + class="inline-flex rounded-md bg-{colorClassPart}-50 p-1.5 text-{colorClassPart}-500 hover:bg-{colorClassPart}-100 focus:outline-none focus:ring-2 focus:ring-{colorClassPart}-600 focus:ring-offset-2 focus:ring-offset-{colorClassPart}-50" + > + <span class="sr-only">Dismiss</span> + <XMarkIcon/> + </button> + </div> + </div> + {/if} + </div> + </div> +{/if} diff --git a/code/app/src/components/badge.svelte b/code/app/src/components/badge.svelte new file mode 100644 index 0000000..f967c2d --- /dev/null +++ b/code/app/src/components/badge.svelte @@ -0,0 +1,76 @@ +<script lang="ts"> + import { createEventDispatcher } from "svelte"; + + export let id: string | undefined = undefined; + export let type: "default" | "red" | "yellow" | "green" | "blue" | "tame" = "default"; + export let text: string; + export let size: "large" | "default" = "default"; + export let withDot: boolean = false; + export let removable: boolean = false; + export let uppercase: boolean = false; + export let tabindex: string | undefined = undefined; + + let colorName = "gray"; + let sizeClass = "rounded px-2 py-0.5 text-xs"; + let dotSizeClass = "mr-1.5 h-2 w-2"; + + const dispatch = createEventDispatcher(); + + function handle_remove(event) { + dispatch("remove", { event, id, text }); + } + + $: switch (type) { + case "red": + colorName = "red"; + break; + case "yellow": + colorName = "yellow"; + break; + case "blue": + colorName = "blue"; + break; + case "green": + colorName = "teal"; + break; + case "default": + case "tame": + default: + colorName = "gray"; + break; + } + + $: switch (size) { + case "large": + sizeClass = "rounded-md px-2.5 py-0.5 text-sm"; + dotSizeClass = "-ml-0.5 mr-1.5 h-2 w-2"; + break; + case "default": + default: + sizeClass = "rounded px-2 py-0.5 text-xs"; + dotSizeClass = "mr-1.5 h-2 w-2"; + break; + } +</script> + +<span class="inline-flex items-center font-medium {uppercase ? 'uppercase' : ''} bg-{colorName}-100 text-{colorName}-800 {sizeClass}" {id}> + {#if withDot} + <svg class="{dotSizeClass} text-{colorName}-400" fill="currentColor" viewBox="0 0 8 8"> + <circle cx="4" cy="4" r="3" /> + </svg> + {/if} + {text} + {#if removable} + <button + on:click={handle_remove} + tabindex={parseInt(tabindex)} + type="button" + class="ml-0.5 inline-flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-full text-{colorName}-400 hover:bg-{colorName}-200 hover:text-{colorName}-500 focus:bg-{colorName}-500 focus:outline-none" + > + <span class="sr-only">Remove badge</span> + <svg class="h-2 w-2" stroke="currentColor" fill="none" viewBox="0 0 8 8"> + <path stroke-linecap="round" stroke-width="1.5" d="M1 1l6 6m0-6L1 7" /> + </svg> + </button> + {/if} +</span> diff --git a/code/app/src/components/button.svelte b/code/app/src/components/button.svelte new file mode 100644 index 0000000..1d6ac4b --- /dev/null +++ b/code/app/src/components/button.svelte @@ -0,0 +1,116 @@ +<script context="module" lang="ts"> + export type ButtonKind = "primary" | "secondary" | "white" | "reset"; + export type ButtonSize = "sm" | "lg" | "md" | "xl"; +</script> + +<script lang="ts"> + import pwKey from "$actions/pwKey"; + import { SpinnerIcon } from "./icons"; + + export let kind = "primary" as ButtonKind; + export let size = "md" as ButtonSize; + export let type: "button" | "submit" | "reset" = "button"; + export let id: string | undefined = undefined; + export let tabindex: string | undefined = undefined; + export let style: string | undefined = undefined; + export let title: string | undefined = undefined; + export let disabled: boolean | null = false; + export let href: string | undefined = undefined; + export let text: string; + export let loading = false; + export let fullWidth = false; + export let _pwKey: string | undefined = undefined; + + let sizeClasses = ""; + let kindClasses = ""; + let spinnerTextClasses = ""; + let spinnerMarginClasses = ""; + + $: shared_props = { + type: type, + id: id || null, + title: title || null, + disabled: disabled || loading || null, + tabindex: tabindex || null, + style: style || null, + } as any; + + $: switch (size) { + case "sm": + sizeClasses = "px-2.5 py-1.5 text-xs"; + spinnerMarginClasses = "mr-2"; + break; + case "md": + sizeClasses = "px-3 py-2 text-sm"; + spinnerMarginClasses = "mr-2"; + break; + case "lg": + sizeClasses = "px-3 py-2 text-lg"; + spinnerMarginClasses = "mr-2"; + break; + case "xl": + sizeClasses = "px-6 py-3 text-xl"; + spinnerMarginClasses = "mr-2"; + break; + } + + $: switch (kind) { + case "secondary": + kindClasses = "border-transparent text-teal-800 bg-teal-100 hover:bg-teal-200 focus:ring-teal-500"; + spinnerTextClasses = "teal-800"; + break; + case "primary": + kindClasses = "border-transparent text-teal-900 bg-teal-300 hover:bg-teal-400 focus:ring-teal-500"; + spinnerTextClasses = "text-teal-900"; + break; + case "white": + kindClasses = "border-gray-300 text-gray-700 bg-white hover:bg-gray-50 focus:ring-gray-400"; + spinnerTextClasses = "text-gray-700"; + break; + case "reset": + kindClasses = "reset outline-none ring-0 focus:ring-0 focus-visible:ring-0"; + break; + } +</script> + +{#if href} + <a + use:pwKey={_pwKey} + {...shared_props} + {href} + class="{sizeClasses} {kindClasses} {loading ? 'disabled:' : ''} {$$restProps.class ?? ''} {fullWidth + ? 'w-full justify-center' + : ''} disabled:cursor-not-allowed inline-flex items-center border font-bold rounded shadow-sm focus:outline-none focus:ring-2" + > + {#if loading} + <SpinnerIcon class={spinnerTextClasses + " " + spinnerMarginClasses} /> + {/if} + {text} + </a> +{:else} + <button + use:pwKey={_pwKey} + {...shared_props} + on:click + class="btn {sizeClasses} {kindClasses} {$$restProps.class ?? ''} + {fullWidth + ? 'w-full justify-center' + : ''} inline-flex items-center border font-bold rounded shadow-sm focus:outline-none focus:ring-2" + > + {#if loading} + <SpinnerIcon class={spinnerTextClasses + " " + spinnerMarginClasses} /> + {/if} + {text} + </button> +{/if} + +<style> + .reset { + border: 0px; + outline: none; + } + + .reset:focus { + outline: none; + } +</style> diff --git a/code/app/src/components/checkbox.svelte b/code/app/src/components/checkbox.svelte new file mode 100644 index 0000000..db72bee --- /dev/null +++ b/code/app/src/components/checkbox.svelte @@ -0,0 +1,29 @@ +<script lang="ts"> + import pwKey from "$actions/pwKey"; + import {random_string} from "$utilities/misc-helpers"; + + export let label: string; + export let id: string | undefined = "input__" + random_string(4); + export let name: string | undefined = undefined; + export let disabled: boolean | null = null; + export let checked: boolean; + export let required: boolean | null = null; + export let _pwKey: string | undefined = undefined; +</script> + +<div class="flex items-center"> + <input + {name} + use:pwKey={_pwKey} + {disabled} + {id} + {required} + type="checkbox" + bind:checked + class="h-4 w-4 text-teal-600 focus:ring-teal-500 border-gray-300 rounded" + /> + <label for={id} class="ml-2 block text-sm text-gray-900"> + {@html required ? "<span class='text-red-500'>*</span>" : ""} + {label} + </label> +</div> diff --git a/code/app/src/components/combobox.svelte b/code/app/src/components/combobox.svelte new file mode 100644 index 0000000..396c18a --- /dev/null +++ b/code/app/src/components/combobox.svelte @@ -0,0 +1,451 @@ +<script lang="ts" context="module"> + export type ComboboxOption = { + id: string; + name: string; + selected?: boolean; + }; +</script> + +<script lang="ts"> + import { CheckCircleIcon, ChevronUpDownIcon, XIcon } from "./icons"; + import { random_string } from "$utilities/misc-helpers"; + import { go, highlight } from "fuzzysort"; + import Badge from "./badge.svelte"; + import Button from "./button.svelte"; + import LL from "$i18n/i18n-svelte"; + import { element_has_focus } from "$utilities/dom-helpers"; + + export let id = "combobox-" + random_string(3); + export let label: string | undefined = undefined; + export let errorText: string | undefined = undefined; + export let disabled: boolean | undefined = undefined; + export let required: boolean | undefined = undefined; + export let maxlength: number | undefined = undefined; + export let placeholder: string = $LL.combobox.search(); + export let options: Array<ComboboxOption> | undefined = []; + export let createable = false; + export let loading = false; + export let multiple = false; + export let noResultsText: string = $LL.combobox.noRecordsFound(); + export let on_create_async = async ({ name: string }) => {}; + + export const reset = () => methods.reset(); + export const select = (id: string) => methods.select_entry(id); + export const deselect = (id: string) => methods.deselect_entry(id); + + const INTERNAL_ID = "INTERNAL__" + id; + + let optionsListId = id + "--options"; + let searchInputNode; + let searchResults: Array<any> = []; + let searchValue = ""; + let showCreationHint = false; + let showDropdown = false; + let inputHasFocus = false; + let lastKeydownCode = ""; + let mouseIsOverDropdown = false; + let mouseIsOverComponent = false; + + $: ariaErrorDescribedBy = id + "__" + "error"; + $: colorName = errorText ? "red" : "teal"; + $: attributes = { + "aria-describedby": errorText ? ariaErrorDescribedBy : null, + "aria-invalid": errorText ? "true" : null, + disabled: disabled || null, + required: required || null, + maxlength: maxlength || null, + id: id || null, + placeholder: placeholder || null, + } as any; + $: hasSelection = options.some((c) => c.selected === true); + $: if (searchValue.trim()) { + showCreationHint = createable && options.every((c) => search.normalise_value(c.name) !== search.normalise_value(searchValue)); + } else { + showCreationHint = false; + options = methods.get_sorted_array(options); + } + + function on_select(event) { + const node = event.target.closest("[data-id]"); + if (!node) return; + methods.select_entry(node.dataset.id); + } + + const search = { + normalise_value(value: string): string { + if (!value) { + return ""; + } + return value.trim().toLowerCase(); + }, + do() { + const query = search.normalise_value(searchValue); + + if (!query.trim()) { + searchResults = []; + return; + } + + // @ts-ignore + searchResults = go(query, options, { + limit: 15, + allowTypo: true, + threshold: -10000, + key: "name", + }); + showDropdown = true; + }, + on_input_focus() { + showDropdown = true; + inputHasFocus = true; + }, + on_input_click() { + showDropdown = true; + inputHasFocus = true; + }, + on_input_focusout() { + inputHasFocus = false; + if (lastKeydownCode !== "Tab" && (mouseIsOverDropdown || lastKeydownCode === "ArrowDown")) { + return; + } + const selected = options.find((c) => c.selected === true); + if (selected && !multiple) { + searchValue = selected.name; + } + document.querySelector("#" + INTERNAL_ID + " ul li.focus")?.classList.remove("focus"); + showDropdown = false; + }, + on_input_wrapper_focus(event) { + if (event.code && event.code !== "Space" && event.code !== "Enter") return; + if (!element_has_focus(searchInputNode)) searchInputNode.focus(); + showDropdown = true; + }, + }; + + const methods = { + reset(focus_input = false) { + searchValue = ""; + const copy = options; + for (const entry of copy) { + entry.selected = false; + } + options = methods.get_sorted_array(copy); + if (focus_input) { + searchInputNode?.focus(); + showDropdown = true; + } else { + showDropdown = false; + } + }, + async create_entry(name: string) { + if (!name || !createable || loading) { + console.log("Not sending creation event due to failed preconditions", { name, createable, loading }); + return; + } + try { + await on_create_async({ name }); + searchValue = ""; + loading = false; + } catch (e) { + console.error(e); + } + }, + select_entry(entryId: string) { + if (!entryId || loading) { + console.log("Not selecting entry due to failed preconditions", { + entryId, + loading, + }); + return; + } + + const copy = options; + for (const entry of options) { + if (entry.id === entryId) { + entry.selected = true; + if (multiple) { + searchValue = ""; + } else { + searchValue = entry.name; + } + } else if (!multiple) { + entry.selected = false; + } + } + options = methods.get_sorted_array(copy); + searchInputNode?.focus(); + searchResults = []; + }, + deselect_entry(entryId: string) { + if (!entryId || loading) { + console.log("Not deselecting entry due to failed preconditions", { + entryId, + loading, + }); + return; + } + console.log("Deselecting entry", entryId); + + const copy = options; + + for (const entry of copy) { + if (entry.id === entryId) { + entry.selected = false; + } + } + + options = methods.get_sorted_array(copy); + searchInputNode?.focus(); + }, + get_sorted_array(options: Array<ComboboxOption>): Array<ComboboxOption> { + if (!options) { + return; + } + if (options.length < 1) { + return []; + } + if (searchValue) { + return options; + } + + return options.sort((a, b) => search.normalise_value(a.name).localeCompare(search.normalise_value(b.name))); + }, + }; + + const windowEvents = { + on_mousemove(event: any) { + if (!event.target) return; + mouseIsOverDropdown = event.target?.closest("#" + INTERNAL_ID + " .tongue") != null ?? false; + mouseIsOverComponent = event.target?.closest("#" + INTERNAL_ID) != null ?? false; + }, + on_click() { + if (showDropdown && !mouseIsOverDropdown && !mouseIsOverComponent) { + showDropdown = false; + } + }, + on_keydown(event: any) { + lastKeydownCode = event.code; + const enterPressed = event.code === "Enter"; + const backspacePressed = event.code === "Backspace"; + const arrowUpPressed = event.code === "ArrowUp"; + const spacePressed = event.code === "Space"; + const arrowDownPressed = event.code === "ArrowDown"; + const searchInputHasFocus = element_has_focus(searchInputNode); + const focusedEntry = document.querySelector("#" + INTERNAL_ID + " ul li.focus") as HTMLLIElement; + + if (showDropdown && (enterPressed || arrowDownPressed || arrowUpPressed)) { + event.preventDefault(); + } + + if (searchInputHasFocus && backspacePressed && !searchValue && options.length > 0) { + if (options.filter((c) => c.selected === true).at(-1)?.id ?? false) { + methods.deselect_entry(options.filter((c) => c.selected === true).at(-1)?.id ?? ""); + } + return; + } + + if (searchInputHasFocus && enterPressed && showCreationHint) { + methods.create_entry(searchValue.trim()); + return; + } + + if (searchInputHasFocus && !focusedEntry && arrowDownPressed) { + const firstEntry = document.querySelector("#" + INTERNAL_ID + " ul li:first-of-type"); + if (firstEntry) { + firstEntry.classList.add("focus"); + return; + } + } + + if (focusedEntry && (arrowUpPressed || arrowDownPressed)) { + if (arrowDownPressed) { + if (focusedEntry.nextElementSibling) { + focusedEntry.nextElementSibling.classList.add("focus"); + focusedEntry.nextElementSibling.scrollIntoView(false); + } else { + const firstLIEl = document.querySelector("#" + INTERNAL_ID + " ul li:first-of-type"); + firstLIEl.classList.add("focus"); + firstLIEl.scrollIntoView(false); + } + } else if (arrowUpPressed) { + if (focusedEntry.previousElementSibling) { + focusedEntry.previousElementSibling.classList.add("focus"); + focusedEntry.previousElementSibling.scrollIntoView(false); + } else { + const lastLIEl = document.querySelector("#" + INTERNAL_ID + " ul li:last-of-type"); + lastLIEl.classList.add("focus"); + lastLIEl.scrollIntoView(false); + } + } + focusedEntry.classList.remove("focus"); + return; + } + + if (focusedEntry && (spacePressed || enterPressed)) { + methods.select_entry(focusedEntry.dataset.id); + return; + } + + if (lastKeydownCode === "Tab" && !searchInputHasFocus) { + showDropdown = false; + } + }, + on_touchend(event) { + windowEvents.on_mousemove(event); + }, + }; +</script> + +<svelte:window + on:keydown={windowEvents.on_keydown} + on:mousemove={windowEvents.on_mousemove} + on:touchend={windowEvents.on_touchend} + on:click={windowEvents.on_click} +/> + +<div id={INTERNAL_ID} class:cursor-wait={loading}> + {#if label} + <label for={id} class="block text-sm font-medium text-gray-700"> + {label} + {@html required ? "<span class='text-red-500'>*</span>" : ""} + </label> + {/if} + <div class="relative {label ? 'mt-1' : ''}"> + <div + on:click={search.on_input_wrapper_focus} + on:keypress={search.on_input_wrapper_focus} + class="cursor-text w-full flex rounded-md border bg-white py-2 pl-3 pr-12 sm:text-sm + {inputHasFocus ? `border-${colorName}-500 outline-none ring-1 ring-${colorName}-500` : 'shadow-sm border-gray-300'}" + > + {#if multiple === true && hasSelection} + <div class="flex gap-1 flex-wrap"> + {#each options.filter((c) => c.selected === true) as option} + <Badge + id={option.id} + removable + tabindex="-1" + on:remove={(e) => methods.deselect_entry(e.detail.id)} + text={option.name} + /> + {/each} + </div> + {/if} + <div> + <input + {...attributes} + type="text" + style="all: unset;" + role="combobox" + aria-controls={optionsListId} + aria-expanded={showDropdown} + bind:value={searchValue} + bind:this={searchInputNode} + on:input={() => search.do()} + on:click={search.on_input_click} + on:focus={search.on_input_focus} + on:blur={search.on_input_focusout} + autocomplete="off" + /> + {#if hasSelection} + <button + type="button" + on:click={() => reset()} + title={$LL.reset()} + tabindex="-1" + class="text-gray-400 absolute cursor-pointer inset-y-0 right-0 flex items-center rounded-r-md px-2" + > + <XIcon /> + </button> + {:else} + <span tabindex="-1" class="text-gray-400 absolute inset-y-0 right-0 flex items-center rounded-r-md px-2"> + <ChevronUpDownIcon /> + </span> + {/if} + </div> + </div> + {#if errorText} + <p class="mt-2 text-sm text-red-600" id={ariaErrorDescribedBy}> + {errorText} + </p> + {/if} + <div + class="tongue {showDropdown ? 'absolute' : 'hidden'} + z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white + text-base shadow-lg ring-1 ring-teal ring-opacity-5 focus:outline-none sm:text-sm" + > + <ul id={optionsListId} role="listbox" tabindex="-1"> + {#if searchResults.length > 0} + {#each searchResults.filter((c) => !c.selected) as result} + <li + class="item" + data-id={result.obj.id} + aria-selected={result.obj.selected} + role="option" + on:click={on_select} + on:keypress={on_select} + tabindex="-1" + > + {@html highlight(result, '<span class="font-bold">', "</span>")} + </li> + {/each} + {:else if options.length > 0} + {#each options as option} + <!-- + Combobox option, manage highlight styles based on mouseenter/mouseleave and keyboard navigation. + Active: "text-white bg-indigo-600", Not Active: "text-gray-900" + --> + <li + class="item" + aria-selected={option.selected} + role="option" + data-id={option.id} + on:click={on_select} + on:keypress={on_select} + tabindex="-1" + > + <span class="block truncate {option.selected ? 'text-semibold' : ''}">{option.name}</span> + {#if option.selected} + <span class="absolute inset-y-0 right-0 flex items-center pr-4 text-{colorName}-600"> + <CheckCircleIcon /> + </span> + {/if} + </li> + {/each} + {:else} + <slot name="no-records"> + <p class="px-2">{noResultsText}</p> + {#if createable && !searchValue} + <p class="px-2 text-gray-500">{$LL.combobox.createRecordHelpText()}</p> + {/if} + </slot> + {/if} + </ul> + {#if showCreationHint} + <div class="sticky bottom-0 w-full bg-white"> + <Button + text={$LL.combobox.createRecordButtonText(searchValue.trim())} + title={$LL.combobox.createRecordButtonText(searchValue.trim())} + {loading} + kind="reset" + type="button" + on:click={() => methods.create_entry(searchValue.trim())} + /> + </div> + {/if} + </div> + </div> +</div> + +<style lang="postcss"> + .focus { + @apply text-white bg-teal-300; + } + + .item { + @apply relative cursor-pointer select-none py-2 pl-3 pr-9 text-gray-900; + } + + .item[aria-selected="true"] { + @apply bg-teal-200; + } +</style> diff --git a/code/app/src/components/icons/adjustments.svelte b/code/app/src/components/icons/adjustments.svelte new file mode 100644 index 0000000..83bda27 --- /dev/null +++ b/code/app/src/components/icons/adjustments.svelte @@ -0,0 +1,14 @@ +<svg + xmlns="http://www.w3.org/2000/svg" + class="h-6 w-6 {$$restProps.class ?? ''}" + fill="none" + viewBox="0 0 24 24" + stroke="currentColor" + stroke-width="2" +> + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4" + /> +</svg> diff --git a/code/app/src/components/icons/bars-3-center-left.svelte b/code/app/src/components/icons/bars-3-center-left.svelte new file mode 100644 index 0000000..785ece3 --- /dev/null +++ b/code/app/src/components/icons/bars-3-center-left.svelte @@ -0,0 +1,15 @@ +<svg + class="h-6 w-6 {$$restProps.class ?? ''}" + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="1.5" + stroke="currentColor" + aria-hidden="true" +> + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M3.75 6.75h16.5M3.75 12H12m-8.25 5.25h16.5" + /> +</svg> diff --git a/code/app/src/components/icons/calendar.svelte b/code/app/src/components/icons/calendar.svelte new file mode 100644 index 0000000..e0053ee --- /dev/null +++ b/code/app/src/components/icons/calendar.svelte @@ -0,0 +1,14 @@ +<svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="1.5" + stroke="currentColor" + class="w-6 h-6 {$$restProps.class ?? ''}" +> + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5m-9-6h.008v.008H12v-.008zM12 15h.008v.008H12V15zm0 2.25h.008v.008H12v-.008zM9.75 15h.008v.008H9.75V15zm0 2.25h.008v.008H9.75v-.008zM7.5 15h.008v.008H7.5V15zm0 2.25h.008v.008H7.5v-.008zm6.75-4.5h.008v.008h-.008v-.008zm0 2.25h.008v.008h-.008V15zm0 2.25h.008v.008h-.008v-.008zm2.25-4.5h.008v.008H16.5v-.008zm0 2.25h.008v.008H16.5V15z" + /> +</svg> diff --git a/code/app/src/components/icons/check-circle.svelte b/code/app/src/components/icons/check-circle.svelte new file mode 100644 index 0000000..e30778e --- /dev/null +++ b/code/app/src/components/icons/check-circle.svelte @@ -0,0 +1,13 @@ +<svg + class="h-5 w-5 {$$restProps.class ?? ''}" + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 20 20" + fill="currentColor" + aria-hidden="true" +> + <path + fill-rule="evenodd" + d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" + clip-rule="evenodd" + /> +</svg> diff --git a/code/app/src/components/icons/chevron-down.svelte b/code/app/src/components/icons/chevron-down.svelte new file mode 100644 index 0000000..5b29ece --- /dev/null +++ b/code/app/src/components/icons/chevron-down.svelte @@ -0,0 +1,7 @@ +<svg class="h-5 w-5 {$$restProps.class ?? ''}" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"> + <path + fill-rule="evenodd" + d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z" + clip-rule="evenodd" + /> +</svg> diff --git a/code/app/src/components/icons/chevron-up-down.svelte b/code/app/src/components/icons/chevron-up-down.svelte new file mode 100644 index 0000000..c07aed5 --- /dev/null +++ b/code/app/src/components/icons/chevron-up-down.svelte @@ -0,0 +1,13 @@ +<svg + class="h-5 w-5 {$$restProps.class ?? ''}" + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 20 20" + fill="currentColor" + aria-hidden="true" +> + <path + fill-rule="evenodd" + d="M10 3a.75.75 0 01.55.24l3.25 3.5a.75.75 0 11-1.1 1.02L10 4.852 7.3 7.76a.75.75 0 01-1.1-1.02l3.25-3.5A.75.75 0 0110 3zm-3.76 9.2a.75.75 0 011.06.04l2.7 2.908 2.7-2.908a.75.75 0 111.1 1.02l-3.25 3.5a.75.75 0 01-1.1 0l-3.25-3.5a.75.75 0 01.04-1.06z" + clip-rule="evenodd" + /> +</svg> diff --git a/code/app/src/components/icons/chevron-up.svelte b/code/app/src/components/icons/chevron-up.svelte new file mode 100644 index 0000000..289e71d --- /dev/null +++ b/code/app/src/components/icons/chevron-up.svelte @@ -0,0 +1,7 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5"> + <path + fill-rule="evenodd" + d="M14.77 12.79a.75.75 0 01-1.06-.02L10 8.832 6.29 12.77a.75.75 0 11-1.08-1.04l4.25-4.5a.75.75 0 011.08 0l4.25 4.5a.75.75 0 01-.02 1.06z" + clip-rule="evenodd" + /> +</svg> diff --git a/code/app/src/components/icons/database.svelte b/code/app/src/components/icons/database.svelte new file mode 100644 index 0000000..6ffdadb --- /dev/null +++ b/code/app/src/components/icons/database.svelte @@ -0,0 +1,14 @@ +<svg + xmlns="http://www.w3.org/2000/svg" + class="h-6 w-6 {$$restProps.class ?? ''}" + fill="none" + viewBox="0 0 24 24" + stroke="currentColor" + stroke-width="2" +> + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4" + /> +</svg> diff --git a/code/app/src/components/icons/exclamation-circle.svelte b/code/app/src/components/icons/exclamation-circle.svelte new file mode 100644 index 0000000..2ce79b1 --- /dev/null +++ b/code/app/src/components/icons/exclamation-circle.svelte @@ -0,0 +1,13 @@ +<svg + class="h-5 w-5 {$$restProps.class ?? ''}" + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 20 20" + fill="currentColor" + aria-hidden="true" +> + <path + fill-rule="evenodd" + d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-8-5a.75.75 0 01.75.75v4.5a.75.75 0 01-1.5 0v-4.5A.75.75 0 0110 5zm0 10a1 1 0 100-2 1 1 0 000 2z" + clip-rule="evenodd" + /> +</svg> diff --git a/code/app/src/components/icons/exclamation-triangle.svelte b/code/app/src/components/icons/exclamation-triangle.svelte new file mode 100644 index 0000000..8d807db --- /dev/null +++ b/code/app/src/components/icons/exclamation-triangle.svelte @@ -0,0 +1,13 @@ +<svg + class="h-5 w-5 {$$restProps.class ?? ''}" + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 20 20" + fill="currentColor" + aria-hidden="true" +> + <path + fill-rule="evenodd" + d="M8.485 3.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 3.495zM10 6a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 6zm0 9a1 1 0 100-2 1 1 0 000 2z" + clip-rule="evenodd" + /> +</svg> diff --git a/code/app/src/components/icons/folder-open.svelte b/code/app/src/components/icons/folder-open.svelte new file mode 100644 index 0000000..409c8e2 --- /dev/null +++ b/code/app/src/components/icons/folder-open.svelte @@ -0,0 +1,14 @@ +<svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="1.5" + stroke="currentColor" + class="w-6 h-6 {$$restProps.class ?? ''}" +> + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M3.75 9.776c.112-.017.227-.026.344-.026h15.812c.117 0 .232.009.344.026m-16.5 0a2.25 2.25 0 00-1.883 2.542l.857 6a2.25 2.25 0 002.227 1.932H19.05a2.25 2.25 0 002.227-1.932l.857-6a2.25 2.25 0 00-1.883-2.542m-16.5 0V6A2.25 2.25 0 016 3.75h3.879a1.5 1.5 0 011.06.44l2.122 2.12a1.5 1.5 0 001.06.44H18A2.25 2.25 0 0120.25 9v.776" + /> +</svg> diff --git a/code/app/src/components/icons/funnel.svelte b/code/app/src/components/icons/funnel.svelte new file mode 100644 index 0000000..7e9daeb --- /dev/null +++ b/code/app/src/components/icons/funnel.svelte @@ -0,0 +1,7 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5"> + <path + fill-rule="evenodd" + d="M2.628 1.601C5.028 1.206 7.49 1 10 1s4.973.206 7.372.601a.75.75 0 01.628.74v2.288a2.25 2.25 0 01-.659 1.59l-4.682 4.683a2.25 2.25 0 00-.659 1.59v3.037c0 .684-.31 1.33-.844 1.757l-1.937 1.55A.75.75 0 018 18.25v-5.757a2.25 2.25 0 00-.659-1.591L2.659 6.22A2.25 2.25 0 012 4.629V2.34a.75.75 0 01.628-.74z" + clip-rule="evenodd" + /> +</svg> diff --git a/code/app/src/components/icons/home.svelte b/code/app/src/components/icons/home.svelte new file mode 100644 index 0000000..ee8305d --- /dev/null +++ b/code/app/src/components/icons/home.svelte @@ -0,0 +1,14 @@ +<svg + xmlns="http://www.w3.org/2000/svg" + class="h-6 w-6 {$$restProps.class ?? ''}" + fill="none" + viewBox="0 0 24 24" + stroke="currentColor" + stroke-width="2" +> + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" + /> +</svg> diff --git a/code/app/src/components/icons/index.ts b/code/app/src/components/icons/index.ts new file mode 100644 index 0000000..eb5b439 --- /dev/null +++ b/code/app/src/components/icons/index.ts @@ -0,0 +1,47 @@ +import XIcon from "./x.svelte"; +import MenuIcon from "./menu.svelte"; +import AdjustmentsIcon from "./adjustments.svelte"; +import DatabaseIcon from "./database.svelte"; +import HomeIcon from "./home.svelte"; +import InformationCircleIcon from "./information-circle.svelte"; +import ExclamationTriangleIcon from "./exclamation-triangle.svelte"; +import XCircleIcon from "./x-circle.svelte"; +import CheckCircleIcon from "./check-circle.svelte"; +import XMarkIcon from "./x-mark.svelte"; +import SpinnerIcon from "./spinner.svelte"; +import ExclamationCircleIcon from "./exclamation-circle.svelte"; +import ChevronUpDownIcon from "./chevron-up-down.svelte"; +import MagnifyingGlassIcon from "./magnifying-glass.svelte"; +import Bars3CenterLeftIcon from "./bars-3-center-left.svelte"; +import CalendarIcon from "./calendar.svelte"; +import FolderOpenIcon from "./folder-open.svelte"; +import MegaphoneIcon from "./megaphone.svelte"; +import QueueListIcon from "./queue-list.svelte"; +import ChevronDownIcon from "./chevron-down.svelte"; +import ChevronUpIcon from "./chevron-up.svelte"; +import FunnelIcon from "./funnel.svelte"; + +export { + FunnelIcon, + ChevronDownIcon, + ChevronUpIcon, + QueueListIcon, + FolderOpenIcon, + MegaphoneIcon, + CalendarIcon, + Bars3CenterLeftIcon, + MagnifyingGlassIcon, + ChevronUpDownIcon, + XIcon, + MenuIcon, + HomeIcon, + DatabaseIcon, + AdjustmentsIcon, + InformationCircleIcon, + ExclamationTriangleIcon, + ExclamationCircleIcon, + XCircleIcon, + CheckCircleIcon, + XMarkIcon, + SpinnerIcon +}
\ No newline at end of file diff --git a/code/app/src/components/icons/information-circle.svelte b/code/app/src/components/icons/information-circle.svelte new file mode 100644 index 0000000..68dbc60 --- /dev/null +++ b/code/app/src/components/icons/information-circle.svelte @@ -0,0 +1,13 @@ +<svg + class="h-5 w-5 {$$restProps.class ?? ''}" + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 20 20" + fill="currentColor" + aria-hidden="true" +> + <path + fill-rule="evenodd" + d="M19 10.5a8.5 8.5 0 11-17 0 8.5 8.5 0 0117 0zM8.25 9.75A.75.75 0 019 9h.253a1.75 1.75 0 011.709 2.13l-.46 2.066a.25.25 0 00.245.304H11a.75.75 0 010 1.5h-.253a1.75 1.75 0 01-1.709-2.13l.46-2.066a.25.25 0 00-.245-.304H9a.75.75 0 01-.75-.75zM10 7a1 1 0 100-2 1 1 0 000 2z" + clip-rule="evenodd" + /> +</svg> diff --git a/code/app/src/components/icons/magnifying-glass.svelte b/code/app/src/components/icons/magnifying-glass.svelte new file mode 100644 index 0000000..f8fdb6e --- /dev/null +++ b/code/app/src/components/icons/magnifying-glass.svelte @@ -0,0 +1,13 @@ +<svg + class="h-5 w-5 {$$restProps.class ?? ''}" + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 20 20" + fill="currentColor" + aria-hidden="true" +> + <path + fill-rule="evenodd" + d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z" + clip-rule="evenodd" + /> +</svg> diff --git a/code/app/src/components/icons/megaphone.svelte b/code/app/src/components/icons/megaphone.svelte new file mode 100644 index 0000000..7ada5f3 --- /dev/null +++ b/code/app/src/components/icons/megaphone.svelte @@ -0,0 +1,14 @@ +<svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="1.5" + stroke="currentColor" + class="w-6 h-6 {$$restProps.class ?? ''}" +> + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M10.34 15.84c-.688-.06-1.386-.09-2.09-.09H7.5a4.5 4.5 0 110-9h.75c.704 0 1.402-.03 2.09-.09m0 9.18c.253.962.584 1.892.985 2.783.247.55.06 1.21-.463 1.511l-.657.38c-.551.318-1.26.117-1.527-.461a20.845 20.845 0 01-1.44-4.282m3.102.069a18.03 18.03 0 01-.59-4.59c0-1.586.205-3.124.59-4.59m0 9.18a23.848 23.848 0 018.835 2.535M10.34 6.66a23.847 23.847 0 008.835-2.535m0 0A23.74 23.74 0 0018.795 3m.38 1.125a23.91 23.91 0 011.014 5.395m-1.014 8.855c-.118.38-.245.754-.38 1.125m.38-1.125a23.91 23.91 0 001.014-5.395m0-3.46c.495.413.811 1.035.811 1.73 0 .695-.316 1.317-.811 1.73m0-3.46a24.347 24.347 0 010 3.46" + /> +</svg> diff --git a/code/app/src/components/icons/menu.svelte b/code/app/src/components/icons/menu.svelte new file mode 100644 index 0000000..471d85f --- /dev/null +++ b/code/app/src/components/icons/menu.svelte @@ -0,0 +1,14 @@ +<svg + xmlns="http://www.w3.org/2000/svg" + class="h-6 w-6 {$$restProps.class ?? ''}" + fill="none" + viewBox="0 0 24 24" + stroke="currentColor" + stroke-width="2" +> + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M4 6h16M4 12h16M4 18h16" + /> +</svg> diff --git a/code/app/src/components/icons/queue-list.svelte b/code/app/src/components/icons/queue-list.svelte new file mode 100644 index 0000000..6148394 --- /dev/null +++ b/code/app/src/components/icons/queue-list.svelte @@ -0,0 +1,14 @@ +<svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="1.5" + stroke="currentColor" + class="w-6 h-6 {$$restProps.class ?? ''}" +> + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M3.75 12h16.5m-16.5 3.75h16.5M3.75 19.5h16.5M5.625 4.5h12.75a1.875 1.875 0 010 3.75H5.625a1.875 1.875 0 010-3.75z" + /> +</svg> diff --git a/code/app/src/components/icons/spinner.svelte b/code/app/src/components/icons/spinner.svelte new file mode 100644 index 0000000..80cc57c --- /dev/null +++ b/code/app/src/components/icons/spinner.svelte @@ -0,0 +1,20 @@ +<svg + class="-ml-1 mr-3 h-5 w-5 animate-spin {$$restProps.class ?? ''}" + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" +> + <circle + class="opacity-25" + cx="12" + cy="12" + r="10" + stroke="currentColor" + stroke-width="4" + /> + <path + class="opacity-75" + fill="currentColor" + d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" + /> +</svg> diff --git a/code/app/src/components/icons/x-circle.svelte b/code/app/src/components/icons/x-circle.svelte new file mode 100644 index 0000000..3793b5a --- /dev/null +++ b/code/app/src/components/icons/x-circle.svelte @@ -0,0 +1,13 @@ +<svg + class="h-5 w-5 {$$restProps.class ?? ''}" + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 20 20" + fill="currentColor" + aria-hidden="true" +> + <path + fill-rule="evenodd" + d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.28 7.22a.75.75 0 00-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 101.06 1.06L10 11.06l1.72 1.72a.75.75 0 101.06-1.06L11.06 10l1.72-1.72a.75.75 0 00-1.06-1.06L10 8.94 8.28 7.22z" + clip-rule="evenodd" + /> +</svg> diff --git a/code/app/src/components/icons/x-mark.svelte b/code/app/src/components/icons/x-mark.svelte new file mode 100644 index 0000000..fd1c6a1 --- /dev/null +++ b/code/app/src/components/icons/x-mark.svelte @@ -0,0 +1,11 @@ +<svg + class="h-5 w-5 {$$restProps.class ?? ''}" + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 20 20" + fill="currentColor" + aria-hidden="true" +> + <path + d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" + /> +</svg> diff --git a/code/app/src/components/icons/x.svelte b/code/app/src/components/icons/x.svelte new file mode 100644 index 0000000..6125ab8 --- /dev/null +++ b/code/app/src/components/icons/x.svelte @@ -0,0 +1,14 @@ +<svg + xmlns="http://www.w3.org/2000/svg" + class="h-6 w-6 {$$restProps.class ?? ''}" + fill="none" + viewBox="0 0 24 24" + stroke="currentColor" + stroke-width="2" +> + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M6 18L18 6M6 6l12 12" + /> +</svg> diff --git a/code/app/src/components/index.ts b/code/app/src/components/index.ts new file mode 100644 index 0000000..494bc0c --- /dev/null +++ b/code/app/src/components/index.ts @@ -0,0 +1,25 @@ +import Alert from "./alert.svelte"; +import Button from "./button.svelte"; +import Checkbox from "./checkbox.svelte"; +import Input from "./input.svelte"; +import LocaleSwitcher from "./locale-switcher.svelte"; +import Switch from "./switch.svelte"; +import Badge from "./badge.svelte"; +import ProjectStatusBadge from "./project-status-badge.svelte"; +import TextArea from "./textarea.svelte"; +import Combobox from "./combobox.svelte"; +import Notification from "./notification.svelte"; + +export { + Badge, + Combobox, + TextArea, + ProjectStatusBadge, + Alert, + Button, + Checkbox, + Input, + LocaleSwitcher, + Switch, + Notification, +};
\ No newline at end of file diff --git a/code/app/src/components/input.svelte b/code/app/src/components/input.svelte new file mode 100644 index 0000000..f97c1f1 --- /dev/null +++ b/code/app/src/components/input.svelte @@ -0,0 +1,112 @@ +<script lang="ts"> + import pwKey from "$actions/pwKey"; + import {random_string} from "$utilities/misc-helpers"; + import {ExclamationCircleIcon} from "./icons"; + + export let label: string | undefined = undefined; + export let type: string = "text"; + export let autocomplete: string | undefined = undefined; + export let required: boolean | undefined = undefined; + export let id: string | undefined = "input__" + random_string(4); + export let name: string | undefined = undefined; + export let placeholder: string | undefined = undefined; + export let helpText: string | undefined = undefined; + export let errorText: string | undefined = undefined; + export let errors: Array<string> | undefined = undefined; + export let disabled = false; + export let hideLabel = false; + export let cornerHint: string | undefined = undefined; + export let icon: any = undefined; + export let addon: string | undefined = undefined; + export let value: string | undefined; + export let wrapperClass: string | undefined = undefined; + export let _pwKey: string | undefined = undefined; + + $: ariaErrorDescribedBy = id + "__" + "error"; + $: attributes = { + "aria-describedby": errorText || errors?.length ? ariaErrorDescribedBy : null, + "aria-invalid": errorText || errors?.length ? "true" : null, + disabled: disabled || null, + autocomplete: autocomplete || null, + required: required || null, + } as any; + $: hasBling = icon || addon || errorText; + const defaultColorClass = "border-gray-300 focus:border-teal-500 focus:ring-teal-500"; + let colorClass = defaultColorClass; + $: if (errorText) { + colorClass = "placeholder-red-300 focus:border-red-500 focus:outline-none focus:ring-red-500 text-red-900 pr-10 border-red-300"; + } else { + colorClass = defaultColorClass; + } + + function typeAction(node: HTMLInputElement) { + node.type = type; + } +</script> + +<div class={wrapperClass}> + {#if label && !cornerHint && !hideLabel} + <label for={id} class={hideLabel ? "sr-only" : "block text-sm font-medium text-gray-700"}> + {label} + {@html required ? "<span class='text-red-500'>*</span>" : ""} + </label> + {:else if cornerHint && !hideLabel} + <div class="flex justify-between"> + {#if label} + <label for={id} class={hideLabel ? "sr-only" : "block text-sm font-medium text-gray-700"}> + {label} + {@html required ? "<span class='text-red-500'>*</span>" : ""} + </label> + {/if} + <span class="text-sm text-gray-500"> + {cornerHint} + </span> + </div> + {/if} + <div class="{label ? 'mt-1' : ''} {hasBling ? 'relative rounded-md' : ''} {addon ? 'flex' : ''}"> + {#if icon} + <div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3"> + <svelte:component this={icon} class={errorText ? "text-red-500" : "text-gray-400"}/> + </div> + {:else if addon} + <div class="inline-flex items-center rounded-l-md border border-r-0 border-gray-300 bg-gray-50 px-3 text-gray-500 sm:text-sm"> + <span class="text-gray-500 sm:text-sm">{addon}</span> + </div> + {/if} + <input + use:typeAction + use:pwKey={_pwKey} + {name} + {id} + {...attributes} + bind:value + class="block w-full rounded-md shadow-sm sm:text-sm + {colorClass} + {disabled ? 'disabled:cursor-not-allowed disabled:border-gray-200 disabled:bg-gray-50 disabled:text-gray-500' : ''} + {addon ? 'min-w-0 flex-1 rounded-none rounded-r-md' : ''} + {icon ? 'pl-10' : ''}" + {placeholder} + /> + {#if errorText} + <div class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3"> + <ExclamationCircleIcon class="text-red-500"/> + </div> + {/if} + </div> + {#if helpText && !errorText} + <p class="mt-2 text-sm text-gray-500"> + {helpText} + </p> + {/if} + {#if errorText || errors?.length === 1} + <p class="mt-2 text-sm text-red-600" id={ariaErrorDescribedBy}> + {errorText ?? errors[0]} + </p> + {:else if errors && errors.length} + <ul class="mt-2 list-disc" id={ariaErrorDescribedBy}> + {#each errors as error} + <li class="text-sm text-red-600">{error}</li> + {/each} + </ul> + {/if} +</div> diff --git a/code/app/src/components/locale-switcher.svelte b/code/app/src/components/locale-switcher.svelte new file mode 100644 index 0000000..fc03f39 --- /dev/null +++ b/code/app/src/components/locale-switcher.svelte @@ -0,0 +1,56 @@ +<script lang="ts"> + import pwKey from "$actions/pwKey"; + import {browser} from "$app/environment"; + import {page} from "$app/stores"; + import {CookieNames} from "$configuration"; + import {setLocale, locale} from "$i18n/i18n-svelte"; + import type {Locales} from "$i18n/i18n-types"; + import {locales} from "$i18n/i18n-util"; + import {loadLocaleAsync} from "$i18n/i18n-util.async"; + import Cookies from "js-cookie"; + + export let _pwKey: string | undefined = undefined; + export let tabindex: number | undefined = undefined; + let currentLocale = Cookies.get(CookieNames.locale); + + async function switch_locale(newLocale: Locales) { + if (!newLocale || $locale === newLocale) return; + await loadLocaleAsync(newLocale); + setLocale(newLocale); + document.querySelector("html")?.setAttribute("lang", newLocale); + Cookies.set(CookieNames.locale, newLocale); + currentLocale = newLocale; + console.log("Switched to: " + newLocale); + } + + function on_change(event: Event) { + const target = event.target as HTMLSelectElement; + switch_locale(target.options[target.selectedIndex].value as Locales); + } + + $: if (browser) { + switch_locale($page.params.lang as Locales); + } + + function get_locale_name(iso: string) { + switch (iso) { + case "nb": { + return "Norsk Bokmål"; + } + case "en": { + return "English"; + } + } + } +</script> + +<select + {tabindex} + use:pwKey={_pwKey} + on:change={on_change} + class="mt-1 mr-1 block border-none py-2 pl-3 pr-10 text-base rounded-md right-0 absolute focus:outline-none focus:ring-teal-500 sm:text-sm" +> + {#each locales as aLocale} + <option value={aLocale} selected={aLocale === currentLocale}>{get_locale_name(aLocale)}</option> + {/each} +</select> diff --git a/code/app/src/components/notification.svelte b/code/app/src/components/notification.svelte new file mode 100644 index 0000000..d78b3d3 --- /dev/null +++ b/code/app/src/components/notification.svelte @@ -0,0 +1,119 @@ +<script context="module" lang="ts"> + export type NotificationType = "info" | "error" | "success" | "warning" | "subtle"; +</script> + +<script lang="ts"> + import { Transition } from "@rgossiaux/svelte-headlessui"; + import { onDestroy } from "svelte"; + import { XMarkIcon, ExclamationCircleIcon, InformationCircleIcon, XCircleIcon, CheckCircleIcon } from "./icons"; + + export let title: string; + export let subtitle = ""; + export let show = false; + export let type: NotificationType = "info"; + export let hideAfterSeconds = -1; + export let nonClosable = false; + + $: _show = show && title.length > 0; + let timeout; + let iconClass = ""; + let icon = undefined; + let bgClass = ""; + let ringClass = ""; + + onDestroy(() => { + clearTimeout(timeout); + }); + + $: if (hideAfterSeconds > 0) { + timeout = setTimeout(() => close(), hideAfterSeconds * 1000); + } else { + timeout = -1; + show = true; + } + + $: switch (type) { + case "error": + iconClass = "text-red-400"; + bgClass = "bg-red-50"; + ringClass = "ring-1 ring-red-100"; + icon = XCircleIcon; + break; + case "info": + iconClass = "text-blue-400"; + bgClass = "bg-blue-50"; + ringClass = "ring-1 ring-blue-100"; + icon = InformationCircleIcon; + break; + case "success": + iconClass = "text-green-400"; + bgClass = "bg-green-50"; + ringClass = "ring-1 ring-green-100"; + icon = CheckCircleIcon; + break; + case "warning": + iconClass = "text-yellow-400"; + bgClass = "bg-yellow-50"; + ringClass = "ring-1 ring-yellow-100"; + icon = ExclamationCircleIcon; + break; + case "subtle": + icon = undefined; + bgClass = "bg-white"; + ringClass = "ring-1 ring-gray-100"; + break; + default: + icon = undefined; + bgClass = "bg-white"; + ringClass = ""; + break; + } + + function close() { + show = false; + } +</script> + +<div aria-live="assertive" class="pointer-events-none fixed inset-0 flex items-end px-4 py-6 sm:items-start sm:p-6"> + <div class="flex w-full flex-col items-center space-y-4 sm:items-end"> + <Transition + class="w-full flex justify-end" + show={_show} + enter="transform ease-out duration-300 transition" + enterFrom="translate-y-2 opacity-0 sm:translate-y-0 sm:translate-x-2" + enterTo="translate-y-0 opacity-100 sm:translate-x-0" + leave="transition ease-in duration-100" + leaveFrom="opacity-100" + leaveTo="opacity-0" + > + <div class="pointer-events-auto w-full max-w-sm overflow-hidden rounded-lg shadow-md {bgClass} {ringClass}"> + <div class="p-4"> + <div class="flex items-start"> + {#if icon} + <div class="flex-shrink-0"> + <svelte:component this={icon} class={iconClass} /> + </div> + {/if} + <div class="ml-3 w-0 flex-1 pt-0.5"> + <p class="text-sm font-medium text-gray-900">{title}</p> + {#if subtitle} + <p class="mt-1 text-sm text-gray-500">{subtitle}</p> + {/if} + </div> + {#if !nonClosable} + <div class="ml-4 flex flex-shrink-0"> + <button + on:click={close} + type="button" + class="inline-flex rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2" + > + <XMarkIcon /> + </button> + </div> + {/if} + </div> + </div> + </div> + </Transition> + </div> +</div> diff --git a/code/app/src/components/project-status-badge.svelte b/code/app/src/components/project-status-badge.svelte new file mode 100644 index 0000000..3e93935 --- /dev/null +++ b/code/app/src/components/project-status-badge.svelte @@ -0,0 +1,25 @@ +<script lang="ts"> + import type {ProjectStatus} from "$models/projects/ProjectStatus"; + import Badge from "./badge.svelte"; + + export let status: string | ProjectStatus; + + let text = ""; + let type = "default" as any; + $: switch (status) { + case "idl": + type = "tame"; + text = "IDLE"; + break; + case "exp": + type = "yellow"; + text = "EXPIRED"; + break; + case "act": + type = "green"; + text = "ACTIVE"; + break; + } +</script> + +<Badge {text} {type} uppercase/> diff --git a/code/app/src/components/switch.svelte b/code/app/src/components/switch.svelte new file mode 100644 index 0000000..1b67f80 --- /dev/null +++ b/code/app/src/components/switch.svelte @@ -0,0 +1,125 @@ +<script context="module" lang="ts"> + export type SwitchType = "short" | "icon" | "default"; +</script> + +<script lang="ts"> + import pwKey from "$actions/pwKey"; + + export let enabled = false; + export let type: SwitchType = "default"; + export let srText = "Use setting"; + export let label: string | undefined = undefined; + export let description: string | undefined = undefined; + export let rightAlignedLabelDescription = false; + export let _pwKey: string | undefined = undefined; + + $: colorClass = enabled ? "bg-teal-600 focus:ring-teal-500" : "bg-gray-200 focus:ring-teal-500"; + $: translateClass = enabled ? "translate-x-5" : "translate-x-0"; + $: hasLabelOrDescription = label || description; + + function toggle() { + enabled = !enabled; + } +</script> + +<div class="{hasLabelOrDescription ? 'flex items-center' : ''} {rightAlignedLabelDescription ? '' : 'justify-between'}"> + {#if hasLabelOrDescription && !rightAlignedLabelDescription} + <span class="flex flex-grow flex-col"> + {#if label} + <span class="text-sm font-medium text-gray-900">{label}</span> + {/if} + {#if description} + <span class="text-sm text-gray-500">{description}</span> + {/if} + </span> + {/if} + {#if type === "short"} + <button + type="button" + class="group relative inline-flex h-5 w-10 flex-shrink-0 cursor-pointer items-center justify-center rounded-full focus:outline-none focus:ring-2 focus:ring-offset-2" + role="switch" + aria-checked={enabled} + use:pwKey={_pwKey} + on:click={toggle} + > + <span class="sr-only">{srText}</span> + <span aria-hidden="true" class="pointer-events-none absolute h-full w-full rounded-md"/> + <span + aria-hidden="true" + class="{colorClass} pointer-events-none absolute mx-auto h-4 w-9 rounded-full transition-colors duration-200 ease-in-out" + /> + <span + aria-hidden="true" + class="{translateClass} pointer-events-none absolute left-0 inline-block h-5 w-5 transform rounded-full border border-gray-200 bg-white shadow ring-0 transition-transform duration-200 ease-in-out" + /> + </button> + {:else if type === "icon"} + <button + type="button" + class="{colorClass} relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-offset-2" + role="switch" + aria-checked={enabled} + use:pwKey={_pwKey} + on:click={toggle} + > + <span class="sr-only">{srText}</span> + <span + class="{translateClass} pointer-events-none relative inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out" + > + <span + class="{enabled + ? 'opacity-0 ease-out duration-100' + : 'opacity-100 ease-in duration-200'} absolute inset-0 flex h-full w-full items-center justify-center transition-opacity" + aria-hidden="true" + > + <svg class="h-3 w-3 text-gray-400" fill="none" viewBox="0 0 12 12"> + <path + d="M4 8l2-2m0 0l2-2M6 6L4 4m2 2l2 2" + stroke="currentColor" + stroke-width="2" + stroke-linecap="round" + stroke-linejoin="round" + /> + </svg> + </span> + <span + class="{enabled + ? 'opacity-100 ease-in duration-200' + : 'opacity-0 ease-out duration-100'} absolute inset-0 flex h-full w-full items-center justify-center transition-opacity" + aria-hidden="true" + > + <svg class="h-3 w-3 text-indigo-600" fill="currentColor" viewBox="0 0 12 12"> + <path + d="M3.707 5.293a1 1 0 00-1.414 1.414l1.414-1.414zM5 8l-.707.707a1 1 0 001.414 0L5 8zm4.707-3.293a1 1 0 00-1.414-1.414l1.414 1.414zm-7.414 2l2 2 1.414-1.414-2-2-1.414 1.414zm3.414 2l4-4-1.414-1.414-4 4 1.414 1.414z" + /> + </svg> + </span> + </span> + </button> + {:else if type === "default"} + <button + type="button" + class="{colorClass} relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-offset-2" + role="switch" + aria-checked={enabled} + use:pwKey={_pwKey} + on:click={toggle} + > + <span class="sr-only">{srText}</span> + <span + aria-hidden="true" + class="{translateClass} pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out" + /> + </button> + {/if} + {#if hasLabelOrDescription && rightAlignedLabelDescription} + <span class="ml-3"> + {#if label} + <span class="text-sm font-medium text-gray-900">{label}</span> + {/if} + {#if description} + <span class="text-sm text-gray-500">{description}</span> + {/if} + </span> + {/if} +</div> diff --git a/code/app/src/components/textarea.svelte b/code/app/src/components/textarea.svelte new file mode 100644 index 0000000..223a265 --- /dev/null +++ b/code/app/src/components/textarea.svelte @@ -0,0 +1,81 @@ +<script lang="ts"> + import {random_string} from "$utilities/misc-helpers"; + + export let id = "textarea-" + random_string(4); + export let disabled = false; + export let rows = 2; + export let cols = 0; + export let name = ""; + export let placeholder = ""; + export let value; + export let label = ""; + export let required = false; + export let errorText = ""; + export let errors: Array<string> | undefined = undefined; + + $: ariaErrorDescribedBy = id + "__" + "error"; + $: attributes = { + "aria-describedby": errorText || errors?.length ? ariaErrorDescribedBy : null, + "aria-invalid": errorText || errors?.length ? "true" : null, + rows: rows || null, + cols: cols || null, + name: name || null, + id: id || null, + disabled: disabled || null, + required: required || null, + } as any; + + let textareaElement; + let scrollHeight = 0; + const defaultColorClass = "border-gray-300 focus:border-teal-500 focus:ring-teal-500"; + let colorClass = defaultColorClass; + + $: if (errorText) { + colorClass = "placeholder-red-300 focus:border-red-500 focus:outline-none focus:ring-red-500 text-red-900 pr-10 border-red-300"; + } else { + colorClass = defaultColorClass; + } + + $: if (textareaElement) { + scrollHeight = textareaElement.scrollHeight; + } + + function on_input(event) { + event.target.style.height = "auto"; + event.target.style.height = this.scrollHeight + "px"; + } +</script> + +<div> + {#if label} + <label for={id} class="block text-sm font-medium text-gray-700"> + {label} + {@html required ? "<span class='text-red-500'>*</span>" : ""} + </label> + {/if} + <div class="mt-1"> + <textarea + {rows} + {name} + {id} + {...attributes} + style="overflow-y:hidden;min-height:calc(1.5em + .75rem + 2px);{scrollHeight ? 'height:{scrollHeight}px' : ''};" + bind:value + bind:this={textareaElement} + on:input={on_input} + {placeholder} + class="block w-full rounded-md {colorClass} shadow-sm sm:text-sm" + /> + {#if errorText || errors?.length === 1} + <p class="mt-2 text-sm text-red-600" id={ariaErrorDescribedBy}> + {errorText ?? errors[0]} + </p> + {:else if errors && errors.length} + <ul class="mt-2 list-disc" id={ariaErrorDescribedBy}> + {#each errors as error} + <li class="text-sm text-red-600">{error}</li> + {/each} + </ul> + {/if} + </div> +</div> diff --git a/code/app/src/configuration/index.ts b/code/app/src/configuration/index.ts new file mode 100644 index 0000000..abf6ac5 --- /dev/null +++ b/code/app/src/configuration/index.ts @@ -0,0 +1,64 @@ +import { env } from "$env/dynamic/private"; + +export const APP_ADDRESS = "https://stage.greatoffice.app"; +export const API_ADDRESS = "https://stage-api.greatoffice.app"; +export const DEV_APP_ADDRESS = "http://localhost"; +export const DEV_API_ADDRESS = "http://localhost:5000"; + +export function api_base(path: string = "", explicitVersion = 1): string { + if (path && !path.startsWith("_")) path = "v" + explicitVersion + path; + return (is_development() ? DEV_API_ADDRESS : API_ADDRESS) + (path !== "" ? "/" + path : ""); +} + +export function is_development(): boolean { + return import.meta.env.DEV; +} + +export function is_testing(): boolean { + return env.TESTING == "true"; +} + +export function is_debug(): boolean { + return localStorage.getItem(StorageKeys.debug) !== "true"; +} + +export const CookieNames = { + theme: "go_theme", + locale: "go_locale", + session: "go_session", +}; + +export function get_test_context(): TestContext { + return { + user: { + username: env.TEST_USERNAME, + password: env.TEST_PASSWORD, + }, + }; +} + +export interface TestContext { + user: { + username: string, + password: string + }; +} + +export const QueryKeys = { + labels: "labels", + categories: "categories", + entries: "entries", +}; + +export const StorageKeys = { + session: "sessionData", + theme: "theme", + debug: "debug", + categories: "categories", + labels: "labels", + entries: "entries", + stopwatch: "stopwatchState", + logLevel: "logLevel", +}; + +export type PortalMessage = "emailValidated";
\ No newline at end of file diff --git a/code/app/src/global.d.ts b/code/app/src/global.d.ts new file mode 100644 index 0000000..13f5e16 --- /dev/null +++ b/code/app/src/global.d.ts @@ -0,0 +1,11 @@ +/// <reference types="@sveltejs/kit" /> + +type Locales = import('$lib/i18n/i18n-types').Locales +type TranslationFunctions = import('$lib/i18n/i18n-types').TranslationFunctions + +declare namespace App { + interface Locals { + locale: Locales + LL: TranslationFunctions + } +}
\ No newline at end of file diff --git a/code/app/src/hooks.server.ts b/code/app/src/hooks.server.ts new file mode 100644 index 0000000..2720480 --- /dev/null +++ b/code/app/src/hooks.server.ts @@ -0,0 +1,49 @@ +import { CookieNames } from "$configuration"; +import { detectLocale, i18n, isLocale, locales } from "$i18n/i18n-util"; +import { log_debug } from "$utilities/logger"; +import type { Handle, RequestEvent } from "@sveltejs/kit"; +import { initAcceptLanguageHeaderDetector } from "typesafe-i18n/detectors"; +import type { Locales } from "$i18n/i18n-types"; +import { loadAllLocales } from "$i18n/i18n-util.sync"; + +loadAllLocales(); +const L = i18n(); + +export const handle: Handle = async ({ event, resolve }) => { + const localeCookie = event.cookies.get(CookieNames.locale); + const preferredLocale = getPreferredLocale(event); + let finalLocale = localeCookie ?? preferredLocale; + let forceCookieSet = false; + + log_debug("Handling locale", { + locales, + localeCookie, + preferredLocale, + finalLocale, + }); + + if (!isLocale(finalLocale)) { + log_debug(finalLocale + " is not a valid locale or it does not exist, switching to default: en"); + finalLocale = "en"; + forceCookieSet = true; + } + + if (!localeCookie || forceCookieSet) { + // Set a locale cookie + event.cookies.set(CookieNames.locale, finalLocale, { + sameSite: "strict", + path: "/", + httpOnly: false, + }); + } + + event.locals.locale = finalLocale as Locales; + event.locals.LL = L[finalLocale as Locales]; + + return resolve(event, { transformPageChunk: ({ html }) => html.replace("%lang%", finalLocale) }); +}; + +function getPreferredLocale(event: RequestEvent) { + const acceptLanguageDetector = initAcceptLanguageHeaderDetector(event.request); + return detectLocale(acceptLanguageDetector); +} diff --git a/code/app/src/i18n/en/app/index.ts b/code/app/src/i18n/en/app/index.ts new file mode 100644 index 0000000..7ccfc97 --- /dev/null +++ b/code/app/src/i18n/en/app/index.ts @@ -0,0 +1,7 @@ +import type { BaseTranslation } from '../../i18n-types' + +const en_app: BaseTranslation = { + members: "Members", +} + +export default en_app
\ No newline at end of file diff --git a/code/app/src/i18n/en/index.ts b/code/app/src/i18n/en/index.ts new file mode 100644 index 0000000..b38eb48 --- /dev/null +++ b/code/app/src/i18n/en/index.ts @@ -0,0 +1,63 @@ +import type { BaseTranslation } from "../i18n-types"; + +const en: BaseTranslation = { + or: "Or", + name: "Name", + emailAddress: "Email address", + password: "Password", + pageNotFound: "Page not found", + noInternet: "It seems like your device does not have a internet connection, please check your connection.", + reset: "Reset", + of: "{0} of {1}", + isRequired: "{0} is required", + submit: "Submit", + success: "Success", + tryAgainSoon: "Try again soon", + createANewAccount: "Create a new account", + unexpectedError: "An unexpected error occured", + notFound: "Not found", + documentation: "Documentation", + tos: "Terms of service", + privacyPolicy: "Privacy policy", + signIntoYourAccount: "Sign into your account", + combobox: { + search: "Search", + noRecordsFound: "No records found", + createRecordHelpText: "Create a record by typing the name in the search bar and pressing enter", + createRecordButtonText: "Press enter or click here to create {0}" + }, + signInPage: { + title: "Sign in", + notMyComputer: "This is not my computer", + resetPassword: "Reset password", + yourPasswordIsUpdated: "Your password is updated", + signIn: "Sign In", + yourNewPasswordIsApplied: "Your new password is applied", + signInBelow: "Sign in below", + yourAccountIsDisabled: "Your account is disabled", + contactYourAdminIfDisabled: "Contact your administrator if this feels wrong", + youHaveReachedInactivityLimit: "You've reached the hidden inactivity limit", + feelFreeToSignInAgain: "Feel free to sign in again" + }, + signUpPage: { + title: "Sign up", + createYourNewAccount: "Create your new account", + }, + resetPasswordPage: { + title: "Reset password", + fulfillTitle: "Set new password", + setANewPassword: "Set a new password", + expired: "Expired", + requestHasExpired: "Your request has expired", + requestANewReset: "Request a new reset", + invalidRequestTitle: "Your request is invalid", + invalidRequestMessage: "This could be due to it being expired, nonexsistent or something else", + newPassword: "New password", + requestSentMessage: "If we find your email address in our systems, you will receive an email with instructions on how to set a new password for your account.", + requestAPasswordReset: "Request a password reset", + requestNotFound: "Your request was not found", + submitANewRequestBelow: "Submit a new reset request below" + } +}; + +export default en; diff --git a/code/app/src/i18n/formatters.ts b/code/app/src/i18n/formatters.ts new file mode 100644 index 0000000..ade2f89 --- /dev/null +++ b/code/app/src/i18n/formatters.ts @@ -0,0 +1,13 @@ +import { capitalise } from "$utilities/misc-helpers"; +import type { FormattersInitializer } from "typesafe-i18n"; +import type { Locales, Formatters } from "./i18n-types"; + +export const initFormatters: FormattersInitializer<Locales, Formatters> = (locale: Locales) => { + + const formatters: Formatters = { + // add your formatter functions here + capitalise: (value: string) => capitalise(value), + }; + + return formatters; +}; diff --git a/code/app/src/i18n/i18n-svelte.ts b/code/app/src/i18n/i18n-svelte.ts new file mode 100644 index 0000000..6cdffb3 --- /dev/null +++ b/code/app/src/i18n/i18n-svelte.ts @@ -0,0 +1,12 @@ +// This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten. +/* eslint-disable */ + +import { initI18nSvelte } from 'typesafe-i18n/svelte' +import type { Formatters, Locales, TranslationFunctions, Translations } from './i18n-types' +import { loadedFormatters, loadedLocales } from './i18n-util' + +const { locale, LL, setLocale } = initI18nSvelte<Locales, Translations, TranslationFunctions, Formatters>(loadedLocales, loadedFormatters) + +export { locale, LL, setLocale } + +export default LL diff --git a/code/app/src/i18n/i18n-types.ts b/code/app/src/i18n/i18n-types.ts new file mode 100644 index 0000000..ef1d664 --- /dev/null +++ b/code/app/src/i18n/i18n-types.ts @@ -0,0 +1,461 @@ +// This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten. +/* eslint-disable */ +import type { BaseTranslation as BaseTranslationType, LocalizedString, RequiredParams } from 'typesafe-i18n' + +export type BaseTranslation = BaseTranslationType & DisallowNamespaces +export type BaseLocale = 'en' + +export type Locales = + | 'en' + | 'nb' + +export type Translation = RootTranslation & DisallowNamespaces + +export type Translations = RootTranslation & +{ + app: NamespaceAppTranslation +} + +type RootTranslation = { + /** + * O​r + */ + or: string + /** + * N​a​m​e + */ + name: string + /** + * E​m​a​i​l​ ​a​d​d​r​e​s​s + */ + emailAddress: string + /** + * P​a​s​s​w​o​r​d + */ + password: string + /** + * P​a​g​e​ ​n​o​t​ ​f​o​u​n​d + */ + pageNotFound: string + /** + * I​t​ ​s​e​e​m​s​ ​l​i​k​e​ ​y​o​u​r​ ​d​e​v​i​c​e​ ​d​o​e​s​ ​n​o​t​ ​h​a​v​e​ ​a​ ​i​n​t​e​r​n​e​t​ ​c​o​n​n​e​c​t​i​o​n​,​ ​p​l​e​a​s​e​ ​c​h​e​c​k​ ​y​o​u​r​ ​c​o​n​n​e​c​t​i​o​n​. + */ + noInternet: string + /** + * R​e​s​e​t + */ + reset: string + /** + * {​0​}​ ​o​f​ ​{​1​} + * @param {unknown} 0 + * @param {unknown} 1 + */ + of: RequiredParams<'0' | '1'> + /** + * {​0​}​ ​i​s​ ​r​e​q​u​i​r​e​d + * @param {unknown} 0 + */ + isRequired: RequiredParams<'0'> + /** + * S​u​b​m​i​t + */ + submit: string + /** + * S​u​c​c​e​s​s + */ + success: string + /** + * T​r​y​ ​a​g​a​i​n​ ​s​o​o​n + */ + tryAgainSoon: string + /** + * C​r​e​a​t​e​ ​a​ ​n​e​w​ ​a​c​c​o​u​n​t + */ + createANewAccount: string + /** + * A​n​ ​u​n​e​x​p​e​c​t​e​d​ ​e​r​r​o​r​ ​o​c​c​u​r​e​d + */ + unexpectedError: string + /** + * N​o​t​ ​f​o​u​n​d + */ + notFound: string + /** + * D​o​c​u​m​e​n​t​a​t​i​o​n + */ + documentation: string + /** + * T​e​r​m​s​ ​o​f​ ​s​e​r​v​i​c​e + */ + tos: string + /** + * P​r​i​v​a​c​y​ ​p​o​l​i​c​y + */ + privacyPolicy: string + /** + * S​i​g​n​ ​i​n​t​o​ ​y​o​u​r​ ​a​c​c​o​u​n​t + */ + signIntoYourAccount: string + combobox: { + /** + * S​e​a​r​c​h + */ + search: string + /** + * N​o​ ​r​e​c​o​r​d​s​ ​f​o​u​n​d + */ + noRecordsFound: string + /** + * C​r​e​a​t​e​ ​a​ ​r​e​c​o​r​d​ ​b​y​ ​t​y​p​i​n​g​ ​t​h​e​ ​n​a​m​e​ ​i​n​ ​t​h​e​ ​s​e​a​r​c​h​ ​b​a​r​ ​a​n​d​ ​p​r​e​s​s​i​n​g​ ​e​n​t​e​r + */ + createRecordHelpText: string + /** + * P​r​e​s​s​ ​e​n​t​e​r​ ​o​r​ ​c​l​i​c​k​ ​h​e​r​e​ ​t​o​ ​c​r​e​a​t​e​ ​{​0​} + * @param {unknown} 0 + */ + createRecordButtonText: RequiredParams<'0'> + } + signInPage: { + /** + * S​i​g​n​ ​i​n + */ + title: string + /** + * T​h​i​s​ ​i​s​ ​n​o​t​ ​m​y​ ​c​o​m​p​u​t​e​r + */ + notMyComputer: string + /** + * R​e​s​e​t​ ​p​a​s​s​w​o​r​d + */ + resetPassword: string + /** + * Y​o​u​r​ ​p​a​s​s​w​o​r​d​ ​i​s​ ​u​p​d​a​t​e​d + */ + yourPasswordIsUpdated: string + /** + * S​i​g​n​ ​I​n + */ + signIn: string + /** + * Y​o​u​r​ ​n​e​w​ ​p​a​s​s​w​o​r​d​ ​i​s​ ​a​p​p​l​i​e​d + */ + yourNewPasswordIsApplied: string + /** + * S​i​g​n​ ​i​n​ ​b​e​l​o​w + */ + signInBelow: string + /** + * Y​o​u​r​ ​a​c​c​o​u​n​t​ ​i​s​ ​d​i​s​a​b​l​e​d + */ + yourAccountIsDisabled: string + /** + * C​o​n​t​a​c​t​ ​y​o​u​r​ ​a​d​m​i​n​i​s​t​r​a​t​o​r​ ​i​f​ ​t​h​i​s​ ​f​e​e​l​s​ ​w​r​o​n​g + */ + contactYourAdminIfDisabled: string + /** + * Y​o​u​'​v​e​ ​r​e​a​c​h​e​d​ ​t​h​e​ ​h​i​d​d​e​n​ ​i​n​a​c​t​i​v​i​t​y​ ​l​i​m​i​t + */ + youHaveReachedInactivityLimit: string + /** + * F​e​e​l​ ​f​r​e​e​ ​t​o​ ​s​i​g​n​ ​i​n​ ​a​g​a​i​n + */ + feelFreeToSignInAgain: string + } + signUpPage: { + /** + * S​i​g​n​ ​u​p + */ + title: string + /** + * C​r​e​a​t​e​ ​y​o​u​r​ ​n​e​w​ ​a​c​c​o​u​n​t + */ + createYourNewAccount: string + } + resetPasswordPage: { + /** + * R​e​s​e​t​ ​p​a​s​s​w​o​r​d + */ + title: string + /** + * S​e​t​ ​n​e​w​ ​p​a​s​s​w​o​r​d + */ + fulfillTitle: string + /** + * S​e​t​ ​a​ ​n​e​w​ ​p​a​s​s​w​o​r​d + */ + setANewPassword: string + /** + * E​x​p​i​r​e​d + */ + expired: string + /** + * Y​o​u​r​ ​r​e​q​u​e​s​t​ ​h​a​s​ ​e​x​p​i​r​e​d + */ + requestHasExpired: string + /** + * R​e​q​u​e​s​t​ ​a​ ​n​e​w​ ​r​e​s​e​t + */ + requestANewReset: string + /** + * Y​o​u​r​ ​r​e​q​u​e​s​t​ ​i​s​ ​i​n​v​a​l​i​d + */ + invalidRequestTitle: string + /** + * T​h​i​s​ ​c​o​u​l​d​ ​b​e​ ​d​u​e​ ​t​o​ ​i​t​ ​b​e​i​n​g​ ​e​x​p​i​r​e​d​,​ ​n​o​n​e​x​s​i​s​t​e​n​t​ ​o​r​ ​s​o​m​e​t​h​i​n​g​ ​e​l​s​e + */ + invalidRequestMessage: string + /** + * N​e​w​ ​p​a​s​s​w​o​r​d + */ + newPassword: string + /** + * I​f​ ​w​e​ ​f​i​n​d​ ​y​o​u​r​ ​e​m​a​i​l​ ​a​d​d​r​e​s​s​ ​i​n​ ​o​u​r​ ​s​y​s​t​e​m​s​,​ ​y​o​u​ ​w​i​l​l​ ​r​e​c​e​i​v​e​ ​a​n​ ​e​m​a​i​l​ ​w​i​t​h​ ​i​n​s​t​r​u​c​t​i​o​n​s​ ​o​n​ ​h​o​w​ ​t​o​ ​s​e​t​ ​a​ ​n​e​w​ ​p​a​s​s​w​o​r​d​ ​f​o​r​ ​y​o​u​r​ ​a​c​c​o​u​n​t​. + */ + requestSentMessage: string + /** + * R​e​q​u​e​s​t​ ​a​ ​p​a​s​s​w​o​r​d​ ​r​e​s​e​t + */ + requestAPasswordReset: string + /** + * Y​o​u​r​ ​r​e​q​u​e​s​t​ ​w​a​s​ ​n​o​t​ ​f​o​u​n​d + */ + requestNotFound: string + /** + * S​u​b​m​i​t​ ​a​ ​n​e​w​ ​r​e​s​e​t​ ​r​e​q​u​e​s​t​ ​b​e​l​o​w + */ + submitANewRequestBelow: string + } +} + +export type NamespaceAppTranslation = { + /** + * M​e​m​b​e​r​s + */ + members: string +} + +export type Namespaces = + | 'app' + +type DisallowNamespaces = { + /** + * reserved for 'app'-namespace\ + * you need to use the `./app/index.ts` file instead + */ + app?: "[typesafe-i18n] reserved for 'app'-namespace. You need to use the `./app/index.ts` file instead." +} + +export type TranslationFunctions = { + /** + * Or + */ + or: () => LocalizedString + /** + * Name + */ + name: () => LocalizedString + /** + * Email address + */ + emailAddress: () => LocalizedString + /** + * Password + */ + password: () => LocalizedString + /** + * Page not found + */ + pageNotFound: () => LocalizedString + /** + * It seems like your device does not have a internet connection, please check your connection. + */ + noInternet: () => LocalizedString + /** + * Reset + */ + reset: () => LocalizedString + /** + * {0} of {1} + */ + of: (arg0: unknown, arg1: unknown) => LocalizedString + /** + * {0} is required + */ + isRequired: (arg0: unknown) => LocalizedString + /** + * Submit + */ + submit: () => LocalizedString + /** + * Success + */ + success: () => LocalizedString + /** + * Try again soon + */ + tryAgainSoon: () => LocalizedString + /** + * Create a new account + */ + createANewAccount: () => LocalizedString + /** + * An unexpected error occured + */ + unexpectedError: () => LocalizedString + /** + * Not found + */ + notFound: () => LocalizedString + /** + * Documentation + */ + documentation: () => LocalizedString + /** + * Terms of service + */ + tos: () => LocalizedString + /** + * Privacy policy + */ + privacyPolicy: () => LocalizedString + /** + * Sign into your account + */ + signIntoYourAccount: () => LocalizedString + combobox: { + /** + * Search + */ + search: () => LocalizedString + /** + * No records found + */ + noRecordsFound: () => LocalizedString + /** + * Create a record by typing the name in the search bar and pressing enter + */ + createRecordHelpText: () => LocalizedString + /** + * Press enter or click here to create {0} + */ + createRecordButtonText: (arg0: unknown) => LocalizedString + } + signInPage: { + /** + * Sign in + */ + title: () => LocalizedString + /** + * This is not my computer + */ + notMyComputer: () => LocalizedString + /** + * Reset password + */ + resetPassword: () => LocalizedString + /** + * Your password is updated + */ + yourPasswordIsUpdated: () => LocalizedString + /** + * Sign In + */ + signIn: () => LocalizedString + /** + * Your new password is applied + */ + yourNewPasswordIsApplied: () => LocalizedString + /** + * Sign in below + */ + signInBelow: () => LocalizedString + /** + * Your account is disabled + */ + yourAccountIsDisabled: () => LocalizedString + /** + * Contact your administrator if this feels wrong + */ + contactYourAdminIfDisabled: () => LocalizedString + /** + * You've reached the hidden inactivity limit + */ + youHaveReachedInactivityLimit: () => LocalizedString + /** + * Feel free to sign in again + */ + feelFreeToSignInAgain: () => LocalizedString + } + signUpPage: { + /** + * Sign up + */ + title: () => LocalizedString + /** + * Create your new account + */ + createYourNewAccount: () => LocalizedString + } + resetPasswordPage: { + /** + * Reset password + */ + title: () => LocalizedString + /** + * Set new password + */ + fulfillTitle: () => LocalizedString + /** + * Set a new password + */ + setANewPassword: () => LocalizedString + /** + * Expired + */ + expired: () => LocalizedString + /** + * Your request has expired + */ + requestHasExpired: () => LocalizedString + /** + * Request a new reset + */ + requestANewReset: () => LocalizedString + /** + * Your request is invalid + */ + invalidRequestTitle: () => LocalizedString + /** + * This could be due to it being expired, nonexsistent or something else + */ + invalidRequestMessage: () => LocalizedString + /** + * New password + */ + newPassword: () => LocalizedString + /** + * If we find your email address in our systems, you will receive an email with instructions on how to set a new password for your account. + */ + requestSentMessage: () => LocalizedString + /** + * Request a password reset + */ + requestAPasswordReset: () => LocalizedString + /** + * Your request was not found + */ + requestNotFound: () => LocalizedString + /** + * Submit a new reset request below + */ + submitANewRequestBelow: () => LocalizedString + } + app: { + /** + * Members + */ + members: () => LocalizedString + } +} + +export type Formatters = {} diff --git a/code/app/src/i18n/i18n-util.async.ts b/code/app/src/i18n/i18n-util.async.ts new file mode 100644 index 0000000..2e6717e --- /dev/null +++ b/code/app/src/i18n/i18n-util.async.ts @@ -0,0 +1,42 @@ +// This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten. +/* eslint-disable */ + +import { initFormatters } from './formatters' +import type { Locales, Namespaces, Translations } from './i18n-types' +import { loadedFormatters, loadedLocales, locales } from './i18n-util' + +const localeTranslationLoaders = { + en: () => import('./en'), + nb: () => import('./nb'), +} + +const localeNamespaceLoaders = { + en: { + app: () => import('./en/app') + }, + nb: { + app: () => import('./nb/app') + } +} + +const updateDictionary = (locale: Locales, dictionary: Partial<Translations>): Translations => + loadedLocales[locale] = { ...loadedLocales[locale], ...dictionary } + +export const importLocaleAsync = async (locale: Locales): Promise<Translations> => + (await localeTranslationLoaders[locale]()).default as unknown as Translations + +export const loadLocaleAsync = async (locale: Locales): Promise<void> => { + updateDictionary(locale, await importLocaleAsync(locale)) + loadFormatters(locale) +} + +export const loadAllLocalesAsync = (): Promise<void[]> => Promise.all(locales.map(loadLocaleAsync)) + +export const loadFormatters = (locale: Locales): void => + void (loadedFormatters[locale] = initFormatters(locale)) + +export const importNamespaceAsync = async<Namespace extends Namespaces>(locale: Locales, namespace: Namespace) => + (await localeNamespaceLoaders[locale][namespace]()).default as unknown as Translations[Namespace] + +export const loadNamespaceAsync = async <Namespace extends Namespaces>(locale: Locales, namespace: Namespace): Promise<void> => + void updateDictionary(locale, { [namespace]: await importNamespaceAsync(locale, namespace )}) diff --git a/code/app/src/i18n/i18n-util.sync.ts b/code/app/src/i18n/i18n-util.sync.ts new file mode 100644 index 0000000..8144fdc --- /dev/null +++ b/code/app/src/i18n/i18n-util.sync.ts @@ -0,0 +1,35 @@ +// This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten. +/* eslint-disable */ + +import { initFormatters } from './formatters' +import type { Locales, Translations } from './i18n-types' +import { loadedFormatters, loadedLocales, locales } from './i18n-util' + +import en from './en' +import nb from './nb' + +import en_app from './en/app' +import nb_app from './nb/app' + +const localeTranslations = { + en: { + ...en, + app: en_app + }, + nb: { + ...nb, + app: nb_app + }, +} + +export const loadLocale = (locale: Locales): void => { + if (loadedLocales[locale]) return + + loadedLocales[locale] = localeTranslations[locale] as unknown as Translations + loadFormatters(locale) +} + +export const loadAllLocales = (): void => locales.forEach(loadLocale) + +export const loadFormatters = (locale: Locales): void => + void (loadedFormatters[locale] = initFormatters(locale)) diff --git a/code/app/src/i18n/i18n-util.ts b/code/app/src/i18n/i18n-util.ts new file mode 100644 index 0000000..5b7b6ed --- /dev/null +++ b/code/app/src/i18n/i18n-util.ts @@ -0,0 +1,45 @@ +// This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten. +/* eslint-disable */ + +import { i18n as initI18n, i18nObject as initI18nObject, i18nString as initI18nString } from 'typesafe-i18n' +import type { LocaleDetector } from 'typesafe-i18n/detectors' +import type { LocaleTranslationFunctions, TranslateByString } from 'typesafe-i18n' +import { detectLocale as detectLocaleFn } from 'typesafe-i18n/detectors' +import { initExtendDictionary } from 'typesafe-i18n/utils' +import type { Formatters, Locales, Namespaces, Translations, TranslationFunctions } from './i18n-types' + +export const baseLocale: Locales = 'en' + +export const locales: Locales[] = [ + 'en', + 'nb' +] + +export const namespaces: Namespaces[] = [ + 'app' +] + +export const isLocale = (locale: string): locale is Locales => locales.includes(locale as Locales) + + + export const isNamespace = (namespace: string): namespace is Namespaces => namespaces.includes(namespace as Namespaces) + +export const loadedLocales: Record<Locales, Translations> = {} as Record<Locales, Translations> + +export const loadedFormatters: Record<Locales, Formatters> = {} as Record<Locales, Formatters> + +export const extendDictionary = initExtendDictionary<Translations>() + +export const i18nString = (locale: Locales): TranslateByString => initI18nString<Locales, Formatters>(locale, loadedFormatters[locale]) + +export const i18nObject = (locale: Locales): TranslationFunctions => + initI18nObject<Locales, Translations, TranslationFunctions, Formatters>( + locale, + loadedLocales[locale], + loadedFormatters[locale] + ) + +export const i18n = (): LocaleTranslationFunctions<Locales, Translations, TranslationFunctions> => + initI18n<Locales, Translations, TranslationFunctions, Formatters>(loadedLocales, loadedFormatters) + +export const detectLocale = (...detectors: LocaleDetector[]): Locales => detectLocaleFn<Locales>(baseLocale, locales, ...detectors) diff --git a/code/app/src/i18n/nb/app/index.ts b/code/app/src/i18n/nb/app/index.ts new file mode 100644 index 0000000..6bf9ba6 --- /dev/null +++ b/code/app/src/i18n/nb/app/index.ts @@ -0,0 +1,7 @@ +import type { NamespaceAppTranslation } from '../../i18n-types' + +const nb_app: NamespaceAppTranslation = { + members: "Medlemmer" +} + +export default nb_app diff --git a/code/app/src/i18n/nb/index.ts b/code/app/src/i18n/nb/index.ts new file mode 100644 index 0000000..ef67504 --- /dev/null +++ b/code/app/src/i18n/nb/index.ts @@ -0,0 +1,51 @@ +import type { Translation } from "../i18n-types"; + +const nb: Translation = { + or: "Eller", + name: "Navn", + emailAddress: "E-postadresse", + password: "Passord", + pageNotFound: "Fant ikke siden", + noInternet: "Det ser ut som at du ikke tilkoblet internettet, sjekk tilkoblingen din for å fortsette", + reset: "Tilbakestill", + of: "{0} av {1}", + isRequired: "{0} er påkrevd", + submit: "Send", + success: "Suksess", + tryAgainSoon: "Prøv igjen snart", + createANewAccount: "Lag en ny konto", + unexpectedError: "En uventet feil oppstod", + notFound: "Ikke funnet", + documentation: "Dokumentasjon", + tos: "Vilkår", + privacyPolicy: "Personvernerklæring", + signIntoYourAccount: "Logg inn med din konto", + signInPage: { + notMyComputer: "Dette er ikke min datamaskin", + resetPassword: "Tilbakestill passord", + yourPasswordIsUpdated: "Ditt passord er oppdater", + signIn: "Logg inn", + yourNewPasswordIsApplied: "Ditt nye passord er satt", + signInBelow: "Logg inn nedenfor", + yourAccountIsDisabled: "Din konto er deaktivert", + contactYourAdminIfDisabled: "Ta kontakt med din administrator hvis dette føles feil", + youHaveReachedInactivityLimit: "Du har nådd den hemmelige inaktivitetsgrensen", + feelFreeToSignInAgain: "Logg gjerne inn igjen" + }, + signUpPage: { + createYourNewAccount: "Opprett din nye konto", + }, + resetPasswordPage: { + setANewPassword: "Skriv et nytt passord", + expired: "Utgått", + requestHasExpired: "Din forespørsel er utgått", + requestANewReset: "Spør om en ny tilbakestillingslenke", + newPassword: "Nytt passord", + requestSentMessage: "Hvis vi finner e-postadressen din i våre systemer, vil du få en e-post med instrukser for å sette ditt nye passord.", + requestAPasswordReset: "Forespør tilbakestilling av ditt passord", + requestNotFound: "Din forespørsel ble ikke funnet", + submitANewRequestBelow: "Spør om en ny tilbakestillingslenke nedenfor" + } +} + +export default nb;
\ No newline at end of file diff --git a/code/app/src/models/base/Customer.ts b/code/app/src/models/base/Customer.ts new file mode 100644 index 0000000..ff52fbd --- /dev/null +++ b/code/app/src/models/base/Customer.ts @@ -0,0 +1,21 @@ +import type {CustomerContact} from "./CustomerContact"; +import type {User} from "./User"; + +export type Customer = { + /** + * Guid id for customer + */ + id: string, + /** + * The name of the company + */ + name: string, + /** + * Responsible contact in the current tenant + */ + tenantContact: User, + /** + * The customers main contact + */ + mainContact: CustomerContact, +}
\ No newline at end of file diff --git a/code/app/src/models/base/CustomerContact.ts b/code/app/src/models/base/CustomerContact.ts new file mode 100644 index 0000000..e8abea5 --- /dev/null +++ b/code/app/src/models/base/CustomerContact.ts @@ -0,0 +1,8 @@ +export type CustomerContact = { + firstName: string, + lastname: string, + email: string, + phone: string, + workTitle: string, + note: string +}
\ No newline at end of file diff --git a/code/app/src/models/base/CustomerEvent.ts b/code/app/src/models/base/CustomerEvent.ts new file mode 100644 index 0000000..af86511 --- /dev/null +++ b/code/app/src/models/base/CustomerEvent.ts @@ -0,0 +1,6 @@ +export type CustomerEvent = { + /** + * A descriptive name for the occured event + */ + name: string, +}
\ No newline at end of file diff --git a/code/app/src/models/base/SessionData.ts b/code/app/src/models/base/SessionData.ts new file mode 100644 index 0000000..015cbf3 --- /dev/null +++ b/code/app/src/models/base/SessionData.ts @@ -0,0 +1,5 @@ +export type SessionData = { + id: string, + username: string, + displayName: string, +}
\ No newline at end of file diff --git a/code/app/src/models/base/Tenant.ts b/code/app/src/models/base/Tenant.ts new file mode 100644 index 0000000..6307efc --- /dev/null +++ b/code/app/src/models/base/Tenant.ts @@ -0,0 +1,8 @@ +import type {User} from "./User"; + +export type Tenant = { + id: string, + name: string, + description: string, + masterUser: User, +}
\ No newline at end of file diff --git a/code/app/src/models/base/User.ts b/code/app/src/models/base/User.ts new file mode 100644 index 0000000..2b74d0e --- /dev/null +++ b/code/app/src/models/base/User.ts @@ -0,0 +1,13 @@ +import type {UserRole} from "./UserRole"; + +export type User = { + /** + * Guid id for user + */ + id: string, + firstName: string, + lastName: string, + role: UserRole, + username: string, + email: string +}
\ No newline at end of file diff --git a/code/app/src/models/base/UserRole.ts b/code/app/src/models/base/UserRole.ts new file mode 100644 index 0000000..ec32852 --- /dev/null +++ b/code/app/src/models/base/UserRole.ts @@ -0,0 +1,5 @@ +export enum UserRole { + REGULAR = "reg", + ADMINISTRATOR = "adm", + OWNER = "own" +}
\ No newline at end of file diff --git a/code/app/src/models/internal/FormError.ts b/code/app/src/models/internal/FormError.ts new file mode 100644 index 0000000..f6d8978 --- /dev/null +++ b/code/app/src/models/internal/FormError.ts @@ -0,0 +1,24 @@ +import type { KnownProblem } from "./KnownProblem"; + +export class FormError { + title: string; + subtitle: string; + constructor(title: string = "", subtitle: string = "") { + this.title = title; + this.title = subtitle; + } + + set(title: string = "", subtitle: string = "") { + this.title = title; + this.subtitle = subtitle; + } + + set_from_known_problem(knownProblem: KnownProblem) { + this.title = knownProblem.title ?? ""; + this.subtitle = knownProblem.subtitle ?? ""; + } + + has_error() { + return this.title?.length > 0 || this.subtitle?.length > 0; + } +}
\ No newline at end of file diff --git a/code/app/src/models/internal/IForm.ts b/code/app/src/models/internal/IForm.ts new file mode 100644 index 0000000..c14b770 --- /dev/null +++ b/code/app/src/models/internal/IForm.ts @@ -0,0 +1,15 @@ +import type { FormError } from "./FormError"; + +export interface IForm { + fields: Record<string, IFormField>; + error: FormError; + get_payload: Function; + submit_async: Function; + isLoading: boolean; + showError: boolean; +} + +export interface IFormField { + value: any; + errors: Array<string>; +} diff --git a/code/app/src/models/internal/KnownProblem.ts b/code/app/src/models/internal/KnownProblem.ts new file mode 100644 index 0000000..b6923d9 --- /dev/null +++ b/code/app/src/models/internal/KnownProblem.ts @@ -0,0 +1,10 @@ +export type KnownProblem = { + title: string, + subtitle: string, + errors: Record<string, string[]>, + traceId: string, +} + +export function is_known_problem(response: Response): boolean { + return response.headers.has("X-IsKnownProblem"); +}
\ No newline at end of file diff --git a/code/app/src/models/projects/Project.ts b/code/app/src/models/projects/Project.ts new file mode 100644 index 0000000..f265e67 --- /dev/null +++ b/code/app/src/models/projects/Project.ts @@ -0,0 +1,13 @@ +import type { Temporal } from "temporal-polyfill" +import type { ProjectMember } from "./ProjectMember" +import type { ProjectStatus } from "./ProjectStatus" + +export type Project = { + id: string, + name: string, + description?: string, + start: Temporal.PlainDate, + stop?: Temporal.PlainDate, + members: Array<ProjectMember>, + status: ProjectStatus +}
\ No newline at end of file diff --git a/code/app/src/models/projects/ProjectLabel.ts b/code/app/src/models/projects/ProjectLabel.ts new file mode 100644 index 0000000..59aa9d5 --- /dev/null +++ b/code/app/src/models/projects/ProjectLabel.ts @@ -0,0 +1,5 @@ +export type ProjectLabel = { + id: string, + name: string, + color: string +}
\ No newline at end of file diff --git a/code/app/src/models/projects/ProjectMember.ts b/code/app/src/models/projects/ProjectMember.ts new file mode 100644 index 0000000..de348ef --- /dev/null +++ b/code/app/src/models/projects/ProjectMember.ts @@ -0,0 +1,10 @@ +import type { ProjectRole } from "./ProjectRole" + +export type ProjectMember = { + id: string, + name: string, + role: ProjectRole, + email: string, + userId?: string, + customerId?: string +}
\ No newline at end of file diff --git a/code/app/src/models/projects/ProjectMeta.ts b/code/app/src/models/projects/ProjectMeta.ts new file mode 100644 index 0000000..c583b47 --- /dev/null +++ b/code/app/src/models/projects/ProjectMeta.ts @@ -0,0 +1,7 @@ +import type { Temporal } from "temporal-polyfill" +import type { User } from "../base/User" + +export type ProjectMeta = { + created: Temporal.PlainDateTime, + createdBy: User, +}
\ No newline at end of file diff --git a/code/app/src/models/projects/ProjectRole.ts b/code/app/src/models/projects/ProjectRole.ts new file mode 100644 index 0000000..0fa2347 --- /dev/null +++ b/code/app/src/models/projects/ProjectRole.ts @@ -0,0 +1,7 @@ +export enum ProjectRole { + EXTERNAL = "ext", + INTERNAL = "int", + RESOURCE = "res", + MANAGER = "man", + OWNER = "own" +}
\ No newline at end of file diff --git a/code/app/src/models/projects/ProjectStatus.ts b/code/app/src/models/projects/ProjectStatus.ts new file mode 100644 index 0000000..2df4b88 --- /dev/null +++ b/code/app/src/models/projects/ProjectStatus.ts @@ -0,0 +1,5 @@ +export enum ProjectStatus { + ACTIVE = "act", + EXPIRED = "exp", + IDLE = "idl" +}
\ No newline at end of file diff --git a/code/app/src/models/work/WorkCategory.ts b/code/app/src/models/work/WorkCategory.ts new file mode 100644 index 0000000..7dd85d5 --- /dev/null +++ b/code/app/src/models/work/WorkCategory.ts @@ -0,0 +1,5 @@ +export type WorkCategory = { + id: string, + name: string, + color: string +} diff --git a/code/app/src/models/work/WorkEntry.ts b/code/app/src/models/work/WorkEntry.ts new file mode 100644 index 0000000..2108b88 --- /dev/null +++ b/code/app/src/models/work/WorkEntry.ts @@ -0,0 +1,13 @@ +import type { WorkLabel } from "./WorkLabel"; +import type { WorkCategory } from "./WorkCategory"; +import type { Project } from "../projects/Project"; + +export type WorkEntry = { + id: string, + start: string, + stop: string, + description: string, + labels?: Array<WorkLabel>, + category?: WorkCategory, + project?: Project +} diff --git a/code/app/src/models/work/WorkEntryQueryResponse.ts b/code/app/src/models/work/WorkEntryQueryResponse.ts new file mode 100644 index 0000000..a6974f1 --- /dev/null +++ b/code/app/src/models/work/WorkEntryQueryResponse.ts @@ -0,0 +1,27 @@ +import type { WorkCategory } from "./WorkCategory"; +import type { WorkLabel } from "./WorkLabel"; +import type { Temporal } from "temporal-polyfill"; + +export interface WorkEntryQueryResponse { + duration: WorkEntryQueryDuration, + categories?: Array<WorkCategory>, + labels?: Array<WorkLabel>, + dateRange?: WorkEntryQueryDateRange, + specificDate?: Temporal.PlainDateTime + page: number, + pageSize: number +} + +export interface WorkEntryQueryDateRange { + from: Temporal.PlainDateTime, + to: Temporal.PlainDateTime +} + +export enum WorkEntryQueryDuration { + TODAY = 0, + THIS_WEEK = 1, + THIS_MONTH = 2, + THIS_YEAR = 3, + SPECIFIC_DATE = 4, + DATE_RANGE = 5, +} diff --git a/code/app/src/models/work/WorkLabel.ts b/code/app/src/models/work/WorkLabel.ts new file mode 100644 index 0000000..f7e2795 --- /dev/null +++ b/code/app/src/models/work/WorkLabel.ts @@ -0,0 +1,5 @@ +export interface WorkLabel { + id?: string, + name?: string, + color?: string +} diff --git a/code/app/src/models/work/WorkQuery.ts b/code/app/src/models/work/WorkQuery.ts new file mode 100644 index 0000000..93b0aa4 --- /dev/null +++ b/code/app/src/models/work/WorkQuery.ts @@ -0,0 +1,17 @@ +import type {WorkEntry} from "./WorkEntry"; + +export interface IWorkQuery { + results: Array<WorkEntry>, + page: number, + pageSize: number, + totalRecords: number, + totalPageCount: number, +} + +export class WorkQuery implements IWorkQuery { + results: WorkEntry[]; + page: number; + pageSize: number; + totalRecords: number; + totalPageCount: number; +} diff --git a/code/app/src/routes/(api)/delete-cookie/+server.ts b/code/app/src/routes/(api)/delete-cookie/+server.ts new file mode 100644 index 0000000..ee5e1dc --- /dev/null +++ b/code/app/src/routes/(api)/delete-cookie/+server.ts @@ -0,0 +1,8 @@ +import type { RequestHandler } from './$types'; + +export const GET: RequestHandler = async ({ cookies, url }) => { + const cookieToDelete = url.searchParams.get("key"); + if (!cookieToDelete || cookies.get(cookieToDelete) === undefined) return; + cookies.delete(cookieToDelete) + return new Response(); +};
\ No newline at end of file diff --git a/code/app/src/routes/(main)/(app)/+layout.svelte b/code/app/src/routes/(main)/(app)/+layout.svelte new file mode 100644 index 0000000..09dbb47 --- /dev/null +++ b/code/app/src/routes/(main)/(app)/+layout.svelte @@ -0,0 +1,379 @@ +<script lang="ts"> + import { + ChevronUpDownIcon, + MagnifyingGlassIcon, + Bars3CenterLeftIcon, + XMarkIcon, + HomeIcon, + MegaphoneIcon, + FolderOpenIcon, + QueueListIcon, + CalendarIcon, + } from "$components/icons"; + import { AccountService } from "$services/account-service"; + import { + Dialog, + Menu, + MenuButton, + MenuItem, + MenuItems, + Transition, + TransitionChild, + TransitionRoot, + } from "@rgossiaux/svelte-headlessui"; + import { DialogPanel } from "@developermuch/dev-svelte-headlessui"; + import { Input, Notification } from "$components"; + import { goto } from "$app/navigation"; + import { page } from "$app/stores"; + import { onMount } from "svelte"; + import { fgs, sgs } from "$utilities/global-state"; + + const accountService = AccountService.resolve(); + const session = { + profile: { + username: "Brukernavn", + displayName: "epost@adresse.no", + }, + }; + + let sidebarOpen = false; + let sidebarSearchValue: string | undefined; + let showEmailValidatedNotif = false; + + onMount(() => { + showEmailValidatedNotif = + fgs("showEmailValidatedAlertWhenLoggedIn") === "true"; + if (showEmailValidatedNotif) + sgs("showEmailValidatedAlertWhenLoggedIn", false); + }); + + function sign_out() { + accountService.end_session(() => goto("/sign-in")); + } + const navigationItems = [ + { + href: "/home", + name: "Home", + icon: HomeIcon, + }, + { + href: "/projects", + name: "Projects", + icon: CalendarIcon, + }, + { + href: "/tickets", + name: "Tickets", + icon: MegaphoneIcon, + }, + { + href: "/todo", + name: "Todo", + icon: QueueListIcon, + }, + { + href: "/wiki", + name: "Wiki", + icon: FolderOpenIcon, + }, + ]; +</script> + +{#if showEmailValidatedNotif} + <Notification + title="Email successfully validated" + subtitle="Because of this, you now have gained access to more functionality" + show={true} + /> +{/if} + +<div class="min-h-full"> + <!-- Mobile sidebar --> + <TransitionRoot show={sidebarOpen}> + <Dialog + as="div" + class="relative z-40 lg:hidden" + on:close={() => (sidebarOpen = false)} + > + <TransitionChild + as="div" + enter="transition-opacity ease-linear duration-300" + enterFrom="opacity-0" + enterTo="opacity-100" + leave="transition-opacity ease-linear duration-300" + leaveFrom="opacity-100" + leaveTo="opacity-0" + > + <div class="fixed inset-0 bg-gray-600 bg-opacity-75" /> + </TransitionChild> + + <div class="fixed inset-0 z-40 flex"> + <TransitionChild + as="div" + enter="transition ease-in-out duration-300 transform" + enterFrom="-translate-x-full" + enterTo="translate-x-0" + leave="transition ease-in-out duration-300 transform" + leaveFrom="translate-x-0" + leaveTo="-translate-x-full" + > + <DialogPanel + class="relative flex w-full max-w-xs flex-1 flex-col bg-white pt-5 pb-4" + > + <TransitionChild + as="div" + enter="ease-in-out duration-300" + enterFrom="opacity-0" + enterTo="opacity-100" + leave="ease-in-out duration-300" + leaveFrom="opacity-100" + leaveTo="opacity-0" + > + <div class="absolute top-0 right-0 -mr-12 pt-2"> + <button + type="button" + class="ml-1 flex h-10 w-10 items-center justify-center rounded-full focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white" + on:click={() => (sidebarOpen = false)} + > + <span class="sr-only">Close sidebar</span> + <XMarkIcon class="text-white" aria-hidden="true" /> + </button> + </div> + </TransitionChild> + <div class="mt-5 h-0 flex-1 overflow-y-auto"> + <nav class="px-2"> + <div class="space-y-1"> + {#each navigationItems as item} + {@const current = $page.url.pathname.startsWith(item.href)} + <a + href={item.href} + aria-current={current ? "page" : undefined} + class="group flex items-center px-2 py-2 text-base leading-5 font-medium rounded-md + {current + ? 'bg-gray-100 text-gray-900' + : 'text-gray-600 hover:text-gray-900 hover:bg-gray-50'}" + > + <svelte:component + this={item.icon} + class="mr-3 flex-shrink-0 h-6 w-6 {current + ? 'text-gray-500' + : 'text-gray-400 group-hover:text-gray-500'}" + aria-hidden="true" + /> + {item.name} + </a> + {/each} + </div> + </nav> + </div> + </DialogPanel> + </TransitionChild> + <div class="w-14 flex-shrink-0" aria-hidden="true"> + <!-- Dummy element to force sidebar to shrink to fit close icon --> + </div> + </div> + </Dialog> + </TransitionRoot> + + <!-- Static sidebar for desktop --> + <div + class="hidden lg:fixed lg:inset-y-0 lg:flex lg:w-64 lg:flex-col lg:border-r lg:border-gray-200 lg:bg-gray-100 lg:pb-4" + > + <div class="flex h-0 flex-1 p-3 flex-col overflow-y-auto"> + <!-- User account dropdown --> + <Menu class="relative inline-block text-left"> + <MenuButton + class="group w-full rounded-md bg-gray-100 px-3.5 py-2 text-left text-sm font-medium text-gray-700 hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-teal-500 focus:ring-offset-2 focus:ring-offset-gray-100" + > + <span class="flex w-full items-center justify-between"> + <span class="flex min-w-0 items-center justify-between space-x-3"> + <span class="flex min-w-0 flex-1 flex-col"> + <span class="truncate text-sm font-medium text-gray-900"> + {session.profile.username} + </span> + <span class="truncate text-sm text-gray-500" + >{session.profile.displayName}</span + > + </span> + </span> + <ChevronUpDownIcon + class="flex-shrink-0 text-gray-400 group-hover:text-gray-500" + aria-hidden="true" + /> + </span> + </MenuButton> + <Transition + leave="transition ease-in duration-75" + enter="transition ease-out duration-100" + enterFrom="transform opacity-0 scale-95" + enterTo="transform opacity-100 scale-100" + leaveFrom="transform opacity-100 scale-100" + leaveTo="transform opacity-0 scale-95" + as="div" + > + <MenuItems + class="absolute right-0 left-0 z-10 mt-1 origin-top divide-y divide-gray-200 rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none" + > + <div class="py-1"> + <MenuItem> + <a + href="/profile" + class="text-gray-700 block px-4 py-2 text-sm hover:text-gray-900 hover:bg-gray-100" + > + View profile + </a> + </MenuItem> + <MenuItem> + <a + href="/settings" + class="text-gray-700 block px-4 py-2 text-sm hover:text-gray-900 hover:bg-gray-100" + > + Settings + </a> + </MenuItem> + </div> + <div class="py-1"> + <MenuItem> + <span + on:click={() => sign_out()} + class="text-gray-700 block px-4 py-2 text-sm hover:bg-red-200 hover:text-red-900 cursor-pointer" + > + Sign out + </span> + </MenuItem> + </div> + </MenuItems> + </Transition> + </Menu> + <!-- Sidebar Search --> + <div class="mt-3 hidden"> + <label for="search" class="sr-only">Search</label> + <div class="relative mt-1 rounded-md shadow-sm"> + <Input + type="search" + name="search" + icon={MagnifyingGlassIcon} + placeholder="Search" + bind:value={sidebarSearchValue} + /> + </div> + </div> + <!-- Navigation --> + <nav class="mt-5"> + <div class="space-y-1"> + {#each navigationItems as item} + {@const current = $page.url.pathname.startsWith(item.href)} + <a + href={item.href} + aria-current={current ? "page" : undefined} + class="group flex items-center px-2 py-2 text-base leading-5 font-medium rounded-md + {current + ? 'bg-gray-200 text-gray-900' + : 'text-gray-700 hover:text-gray-900 hover:bg-gray-50'}" + > + <svelte:component + this={item.icon} + class="mr-3 flex-shrink-0 h-6 w-6 {current + ? 'text-gray-500' + : 'text-gray-400 group-hover:text-gray-500'}" + aria-hidden="true" + /> + {item.name} + </a> + {/each} + </div> + </nav> + </div> + </div> + + <!-- Main column --> + <div class="flex flex-col lg:pl-64"> + <!-- Search header --> + <div + class="sticky top-0 z-10 flex h-16 flex-shrink-0 border-b border-gray-200 bg-white lg:hidden" + > + <button + type="button" + class="border-r border-gray-200 px-4 text-gray-500 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-teal-500 lg:hidden" + on:click={() => (sidebarOpen = true)} + > + <span class="sr-only">Open sidebar</span> + <Bars3CenterLeftIcon aria-hidden="true" /> + </button> + <div class="flex flex-1 justify-between px-4 sm:px-6 lg:px-8"> + <div class="flex flex-1"> + <form class="flex w-full md:ml-0" action="#" method="GET"> + <label for="search-field" class="sr-only">Search</label> + <div + class="relative w-full text-gray-400 focus-within:text-gray-600" + > + <Input + bind:value={sidebarSearchValue} + icon={MagnifyingGlassIcon} + id="search-field" + name="search-field" + placeholder="Search" + type="search" + /> + </div> + </form> + </div> + <div class="flex items-center"> + <!-- Profile dropdown --> + <Menu as="div" class="relative ml-3"> + <div> + <MenuButton + class="flex max-w-xs items-center rounded-full bg-white text-sm focus:outline-none focus:ring-2 focus:ring-teal-500 focus:ring-offset-2" + > + <span class="sr-only">Open user menu</span> + </MenuButton> + </div> + <Transition + enterFrom="transform opacity-0 scale-95" + enterTo="transform opacity-100 scale-100" + leaveFrom="transform opacity-100 scale-100" + leaveTo="transform opacity-0 scale-95" + as="div" + > + <MenuItems + class="absolute right-0 z-10 mt-2 w-48 origin-top-right divide-y divide-gray-200 rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none" + > + <div class="py-1"> + <MenuItem> + <a + href="/profile" + class="text-gray-700 block px-4 py-2 text-sm" + > + View profile + </a> + </MenuItem> + <MenuItem> + <a + href="/settings" + class="text-gray-700 block px-4 py-2 text-sm hover:text-gray-900 hover:bg-gray-100" + > + Settings + </a> + </MenuItem> + <div class="py-1"> + <MenuItem> + <span + on:click={() => sign_out()} + class="text-gray-700 block px-4 py-2 text-sm" + > + Sign out + </span> + </MenuItem> + </div> + </div> + </MenuItems> + </Transition> + </Menu> + </div> + </div> + </div> + <main class="flex-1 p-3"> + <slot /> + </main> + </div> +</div> diff --git a/code/app/src/routes/(main)/(app)/home/+page.svelte b/code/app/src/routes/(main)/(app)/home/+page.svelte new file mode 100644 index 0000000..247ee47 --- /dev/null +++ b/code/app/src/routes/(main)/(app)/home/+page.svelte @@ -0,0 +1 @@ +<h1>Welcome Home</h1>
\ No newline at end of file diff --git a/code/app/src/routes/(main)/(app)/org/+page.svelte b/code/app/src/routes/(main)/(app)/org/+page.svelte new file mode 100644 index 0000000..429ec25 --- /dev/null +++ b/code/app/src/routes/(main)/(app)/org/+page.svelte @@ -0,0 +1,4 @@ +<script lang="ts"> +</script> + +<h1>$ORGNAME</h1> diff --git a/code/app/src/routes/(main)/(app)/profile/+page.svelte b/code/app/src/routes/(main)/(app)/profile/+page.svelte new file mode 100644 index 0000000..7c6eb3e --- /dev/null +++ b/code/app/src/routes/(main)/(app)/profile/+page.svelte @@ -0,0 +1,4 @@ +<script lang="ts"> +</script> + +<h1>Hi, Ivar</h1> diff --git a/code/app/src/routes/(main)/(app)/projects/+page.svelte b/code/app/src/routes/(main)/(app)/projects/+page.svelte new file mode 100644 index 0000000..2585331 --- /dev/null +++ b/code/app/src/routes/(main)/(app)/projects/+page.svelte @@ -0,0 +1,118 @@ +<script lang="ts"> + import { Button, ProjectStatusBadge, Input } from "$components"; + import type { Project } from "$models/projects/Project"; + import { createTable, Subscribe, Render } from "svelte-headless-table"; + import { addSortBy, addTableFilter } from "svelte-headless-table/plugins"; + import { writable, type Writable } from "svelte/store"; + import { ChevronDownIcon, ChevronUpIcon, ChevronUpDownIcon, MagnifyingGlassIcon, FunnelIcon } from "$components/icons"; + import LL from "$i18n/i18n-svelte"; + import { goto } from "$app/navigation"; + + const projects: Writable<Array<Project>> = writable([]); + + function on_open_project(event) { + if (event.code && (event.code !== "Enter" || event.code !== "Space")) return; + const name = event.target.innerText; + const projectId = $projects.find((p) => p.name === name).id; + goto("/projects/" + projectId); + } + + const table = createTable(projects, { + sort: addSortBy(), + filter: addTableFilter(), + }); + + const columns = table.createColumns([ + table.column({ header: $LL.name(), accessor: "name" }), + table.column({ header: "Status", accessor: "status" }), + table.column({ header: "Start", accessor: "start" }), + table.column({ header: "Description", accessor: "description", plugins: { sort: { disable: true } } }), + ]); + + const { headerRows, rows, tableAttrs, tableBodyAttrs, pluginStates } = table.createViewModel(columns); + const { filterValue } = pluginStates.filter; +</script> + +<div class="sm:flex sm:items-center"> + <div class="sm:flex-auto"> + <h1 class="text-xl font-semibold text-gray-900">Projects</h1> + <p class="mt-2 text-sm text-gray-700">A list of all the projects in your organsation.</p> + </div> + <div class="mt-4 sm:mt-0 sm:ml-16 inline-flex gap-1 sm:flex-none"> + <Input icon={MagnifyingGlassIcon} placeholder="Search" bind:value={$filterValue} /> + <Button text="Create project" href="/projects/create" /> + </div> +</div> +<div class="-mx-2 mt-6 rounded-md shadow overflow-auto max-h-[80vh] sm:-mx-6 md:mx-0"> + <table {...$tableAttrs} class="min-w-full divide-y divide-gray-300"> + <thead class="bg-gray-50"> + {#each $headerRows as headerRow (headerRow.id)} + <Subscribe rowAttrs={headerRow.attrs()} let:rowAttrs> + <tr {...rowAttrs} class="shadow-sm"> + {#each headerRow.cells as cell (cell.id)} + <Subscribe attrs={cell.attrs()} let:attrs props={cell.props()} let:props> + <th + {...attrs} + scope="col" + class="sticky top-0 bg-gray-50 bg-opacity-100 whitespace-nowrap px-2 py-3.5 text-left text-sm font-semibold text-gray-900" + > + <div class="group inline-flex"> + <Render of={cell.render()} /> + <span + on:click={props.sort.toggle} + on:keypress={props.sort.toggle} + class="{props.sort.disabled + ? 'bg-gray-200 text-gray-900 group-hover:bg-gray-300' + : 'invisible text-gray-400 group-hover:visible group-focus:visible'} + {props.sort.disabled ? '' : 'cursor-pointer'} + ml-2 flex-none rounded" + > + {#if props.sort.order === "asc"} + <ChevronUpIcon /> + {:else if props.sort.order === "desc"} + <ChevronDownIcon /> + {:else if !props.sort.disabled} + <ChevronUpDownIcon /> + {/if} + </span> + {#if cell.id === "status"} + <span + class="invisible text-gray-400 cursor-pointer group-hover:visible group-focus:visible ml-2 flex-none rounded" + > + <FunnelIcon /> + </span> + {/if} + </div> + </th> + </Subscribe> + {/each} + </tr> + </Subscribe> + {/each} + </thead> + <tbody {...$tableBodyAttrs} class="divide-y divide-gray-200 bg-white"> + {#each $rows as row (row.id)} + <Subscribe rowAttrs={row.attrs()} let:rowAttrs> + <tr {...rowAttrs}> + {#each row.cells as cell (cell.id)} + {@const materialisedCell = cell.render()} + <Subscribe attrs={cell.attrs()} let:attrs> + <td {...attrs} class="whitespace-nowrap px-2 py-2 text-sm"> + {#if cell.id === "name"} + <span class="link" title="Open project" on:click={on_open_project} on:keypress={on_open_project}> + <Render of={materialisedCell} /> + </span> + {:else if cell.id === "status"} + <ProjectStatusBadge status={materialisedCell.toString()} /> + {:else} + <Render of={materialisedCell} /> + {/if} + </td> + </Subscribe> + {/each} + </tr> + </Subscribe> + {/each} + </tbody> + </table> +</div> diff --git a/code/app/src/routes/(main)/(app)/projects/[id]/+page.svelte b/code/app/src/routes/(main)/(app)/projects/[id]/+page.svelte new file mode 100644 index 0000000..ca474e2 --- /dev/null +++ b/code/app/src/routes/(main)/(app)/projects/[id]/+page.svelte @@ -0,0 +1,5 @@ +<script lang="ts"> + import { page } from "$app/stores"; +</script> + +<h1>{$page.params.id}</h1> diff --git a/code/app/src/routes/(main)/(app)/projects/create/+page.svelte b/code/app/src/routes/(main)/(app)/projects/create/+page.svelte new file mode 100644 index 0000000..d710edc --- /dev/null +++ b/code/app/src/routes/(main)/(app)/projects/create/+page.svelte @@ -0,0 +1,59 @@ +<script lang="ts"> + import { Input, TextArea, Combobox, Button } from "$components"; + import type { ProjectMember } from "$models/projects/ProjectMember"; + import LL from "$i18n/i18n-svelte"; + + let members = []; + const formData = { + name: { + value: "", + errors: [], + }, + description: { + value: "", + errors: [], + }, + start: { + value: "", + errors: [], + }, + stop: { + value: "", + errors: [], + }, + members: { + value: [] as Array<ProjectMember>, + errors: [], + }, + }; + + const formError = { + title: "", + subtitle: "", + }; + + async function submit_form_async() { + alert("Submitted"); + } +</script> + +<h1>Create a new project</h1> +<form on:submit|preventDefault={submit_form_async} class="max-w-md flex flex-col gap-2"> + <Input label="Name" bind:value={formData.name.value} errors={formData.name.errors} required /> + <TextArea label="Description" bind:value={formData.description.value} errors={formData.description.errors} /> + <section class="grid grid-flow-row sm:grid-flow-col gap-2"> + <Input type="date" label="Start" bind:value={formData.start.value} errors={formData.start.errors} /> + <Input type="date" label="Stop" bind:value={formData.stop.value} errors={formData.stop.errors} /> + </section> + <Combobox options={members} label={$LL.app.members()}> + <svelte:fragment slot="no-records"> + <h1>No members found</h1> + {#if !members?.length} + <p> + <a href="/users/create" class="link">Click here</a> to create your first user + </p> + {/if} + </svelte:fragment> + </Combobox> + <Button text={$LL.submit()} /> +</form> diff --git a/code/app/src/routes/(main)/(app)/settings/+page.svelte b/code/app/src/routes/(main)/(app)/settings/+page.svelte new file mode 100644 index 0000000..8e99661 --- /dev/null +++ b/code/app/src/routes/(main)/(app)/settings/+page.svelte @@ -0,0 +1,205 @@ +<script lang="ts"> + import {Input, Button, Switch} from "$components"; +</script> + +<div class="relative mx-auto max-w-4xl md:px-8 xl:px-0"> + <div class="pt-10 pb-16"> + <div class="px-4 sm:px-6 md:px-0"> + <h1 class="text-3xl font-bold tracking-tight text-gray-900">Settings</h1> + </div> + <div class="px-4 sm:px-6 md:px-0"> + <div class="py-6"> + <!-- Tabs --> + <div class="lg:hidden"> + <label for="selected-tab" class="sr-only">Select a tab</label> + <select + id="selected-tab" + name="selected-tab" + class="mt-1 block w-full rounded-md border-gray-300 py-2 pl-3 pr-10 text-base focus:border-purple-500 focus:outline-none focus:ring-purple-500 sm:text-sm" + > + <option selected>General</option> + + <option>Password</option> + + <option>Notifications</option> + +> + + <option>Billing</option> + + <option>Team Members</option> + </select> + </div> + <div class="hidden lg:block"> + <div class="border-b border-gray-200"> + <nav class="-mb-px flex space-x-8"> + <!-- Current: "border-purple-500 text-purple-600", Default: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700" --> + <a href="#" + class="border-purple-500 text-purple-600 whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm" + >General</a + > + + <a + href="#" + class="border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm" + >Password</a + > + + <a + href="#" + class="border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm" + >Notifications</a + > + + <a + href="#" + class="border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm" + >Plan</a + > + + <a + href="#" + class="border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm" + >Billing</a + > + + <a + href="#" + class="border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm" + >Team Members</a + > + </nav> + </div> + </div> + + <!-- Description list with inline editing --> + <div class="mt-10 divide-y divide-gray-200"> + <div class="space-y-1"> + <h3 class="text-lg font-medium leading-6 text-gray-900">Profile</h3> + <p class="max-w-2xl text-sm text-gray-500"> + This information will be displayed publicly so be careful what you share. + </p> + </div> + <div class="mt-6"> + <dl class="divide-y divide-gray-200"> + <div class="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5"> + <dt class="text-sm font-medium text-gray-500">Name</dt> + <dd class="mt-1 flex text-sm text-gray-900 sm:col-span-2 sm:mt-0"> + <span class="flex-grow">Chelsea Hagon</span> + <span class="ml-4 flex-shrink-0"> + <button + type="button" + class="rounded-md bg-white font-medium text-purple-600 hover:text-purple-500 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2" + >Update</button + > + </span> + </dd> + </div> + <div class="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5 sm:pt-5"> + <dt class="text-sm font-medium text-gray-500">Photo</dt> + <dd class="mt-1 flex text-sm text-gray-900 sm:col-span-2 sm:mt-0"> + <span class="flex-grow"> + <img + class="h-8 w-8 rounded-full" + src="https://images.unsplash.com/photo-1550525811-e5869dd03032?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80" + alt="" + /> + </span> + <span class="ml-4 flex flex-shrink-0 items-start space-x-4"> + <button + type="button" + class="rounded-md bg-white font-medium text-purple-600 hover:text-purple-500 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2" + >Update</button + > + <span class="text-gray-300" aria-hidden="true">|</span> + <button + type="button" + class="rounded-md bg-white font-medium text-purple-600 hover:text-purple-500 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2" + >Remove</button + > + </span> + </dd> + </div> + <div class="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5 sm:pt-5"> + <dt class="text-sm font-medium text-gray-500">Email</dt> + <dd class="mt-1 flex text-sm text-gray-900 sm:col-span-2 sm:mt-0"> + <span class="flex-grow">chelsea.hagon@example.com</span> + <span class="ml-4 flex-shrink-0"> + <button + type="button" + class="rounded-md bg-white font-medium text-purple-600 hover:text-purple-500 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2" + >Update</button + > + </span> + </dd> + </div> + <div class="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:border-b sm:border-gray-200 sm:py-5"> + <dt class="text-sm font-medium text-gray-500">Job title</dt> + <dd class="mt-1 flex text-sm text-gray-900 sm:col-span-2 sm:mt-0"> + <span class="flex-grow">Human Resources Manager</span> + <span class="ml-4 flex-shrink-0"> + <button + type="button" + class="rounded-md bg-white font-medium text-purple-600 hover:text-purple-500 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2" + >Update</button + > + </span> + </dd> + </div> + </dl> + </div> + </div> + + <div class="mt-10 divide-y divide-gray-200"> + <div class="space-y-1"> + <h3 class="text-lg font-medium leading-6 text-gray-900">Account</h3> + <p class="max-w-2xl text-sm text-gray-500">Manage how information is displayed on your + account.</p> + </div> + <div class="mt-6"> + <dl class="divide-y divide-gray-200"> + <div class="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5"> + <dt class="text-sm font-medium text-gray-500">Language</dt> + <dd class="mt-1 flex text-sm text-gray-900 sm:col-span-2 sm:mt-0"> + <span class="flex-grow">English</span> + <span class="ml-4 flex-shrink-0"> + <button + type="button" + class="rounded-md bg-white font-medium text-purple-600 hover:text-purple-500 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2" + >Update</button + > + </span> + </dd> + </div> + <div class="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5 sm:pt-5"> + <dt class="text-sm font-medium text-gray-500">Date format</dt> + <dd class="mt-1 flex text-sm text-gray-900 sm:col-span-2 sm:mt-0"> + <span class="flex-grow">DD-MM-YYYY</span> + <span class="ml-4 flex flex-shrink-0 items-start space-x-4"> + <button + type="button" + class="rounded-md bg-white font-medium text-purple-600 hover:text-purple-500 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2" + >Update</button + > + <span class="text-gray-300" aria-hidden="true">|</span> + <button + type="button" + class="rounded-md bg-white font-medium text-purple-600 hover:text-purple-500 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2" + >Remove</button + > + </span> + </dd> + </div> + <div class="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5 sm:pt-5"> + <dt class="text-sm font-medium text-gray-500" id="timezone-option-label">Automatic + timezone + </dt> + <Switch/> + </div> + </dl> + </div> + </div> + </div> + </div> + </div> +</div> diff --git a/code/app/src/routes/(main)/(app)/tickets/+page.svelte b/code/app/src/routes/(main)/(app)/tickets/+page.svelte new file mode 100644 index 0000000..2a4792b --- /dev/null +++ b/code/app/src/routes/(main)/(app)/tickets/+page.svelte @@ -0,0 +1,4 @@ +<script lang="ts"> +</script> + +<h1>Tickets</h1> diff --git a/code/app/src/routes/(main)/(app)/todo/+page.svelte b/code/app/src/routes/(main)/(app)/todo/+page.svelte new file mode 100644 index 0000000..e29f263 --- /dev/null +++ b/code/app/src/routes/(main)/(app)/todo/+page.svelte @@ -0,0 +1,4 @@ +<script lang="ts"> +</script> + +<h1>Todo</h1> diff --git a/code/app/src/routes/(main)/(app)/wiki/+page.svelte b/code/app/src/routes/(main)/(app)/wiki/+page.svelte new file mode 100644 index 0000000..1762d43 --- /dev/null +++ b/code/app/src/routes/(main)/(app)/wiki/+page.svelte @@ -0,0 +1,4 @@ +<script lang="ts"> +</script> + +<h1>Wiki</h1> diff --git a/code/app/src/routes/(main)/(public)/+layout.svelte b/code/app/src/routes/(main)/(public)/+layout.svelte new file mode 100644 index 0000000..6da653c --- /dev/null +++ b/code/app/src/routes/(main)/(public)/+layout.svelte @@ -0,0 +1,18 @@ +<script> + import { LocaleSwitcher } from "$components"; + import LL from "$i18n/i18n-svelte"; +</script> + +<LocaleSwitcher tabindex={-1} /> +<slot /> +<footer class="grid sm:gap-5 grid-flow-row sm:justify-center px-2 sm:grid-flow-col"> + <a href="https://greatoffice.life/privacy" class="link"> + {$LL.privacyPolicy()} + </a> + <a href="https://greatoffice.life/terms" class="link"> + {$LL.tos()} + </a> + <a href="https://greatoffice.life/docs" class="link"> + {$LL.documentation()} + </a> +</footer> diff --git a/code/app/src/routes/(main)/(public)/portal/+page.svelte b/code/app/src/routes/(main)/(public)/portal/+page.svelte new file mode 100644 index 0000000..b363e4b --- /dev/null +++ b/code/app/src/routes/(main)/(public)/portal/+page.svelte @@ -0,0 +1,26 @@ +<script lang="ts"> + import { onMount } from "svelte"; + import type { PageData } from "./$types"; + import type { PortalMessage } from "$configuration"; + import { goto } from "$app/navigation"; + import { sgs } from "$utilities/global-state"; + + export let data: PageData; + + onMount(async () => { + switch (data.message as PortalMessage) { + case "emailValidated": { + sgs("showEmailValidatedAlertWhenLoggedIn", true); + await goto("/home"); + break; + } + default: { + await goto("/home"); + } + } + }); +</script> + +<div class="p-3"> + <h1>Warping...</h1> +</div> diff --git a/code/app/src/routes/(main)/(public)/portal/+page.ts b/code/app/src/routes/(main)/(public)/portal/+page.ts new file mode 100644 index 0000000..72338cb --- /dev/null +++ b/code/app/src/routes/(main)/(public)/portal/+page.ts @@ -0,0 +1,9 @@ +import type { PortalMessage } from '$configuration'; +import { redirect } from '@sveltejs/kit'; +import type { PageLoad } from './$types'; + +export const load: PageLoad = async ({ url }) => { + const message = url.searchParams.get("msg") as PortalMessage; + if (!message) throw redirect(302, "/"); + return { message }; +};
\ No newline at end of file diff --git a/code/app/src/routes/(main)/(public)/reset-password/+page.svelte b/code/app/src/routes/(main)/(public)/reset-password/+page.svelte new file mode 100644 index 0000000..a45ccdd --- /dev/null +++ b/code/app/src/routes/(main)/(public)/reset-password/+page.svelte @@ -0,0 +1,81 @@ +<script lang="ts"> + import { Alert, Input, Button } from "$components"; + import LL from "$i18n/i18n-svelte"; + import { FormError } from "$models/internal/FormError"; + import { PasswordResetService } from "$services/password-reset-service"; + + const formData = { + email: { + value: "", + errors: [], + }, + }; + + const formError = new FormError(); + const passwordResetService = PasswordResetService.resolve(); + + let loading = false; + let showSuccessAlert = false; + let showErrorAlert = false; + + async function submit_form_async() { + formError.set(); + showSuccessAlert = false; + showErrorAlert = false; + loading = true; + const response = await passwordResetService.create_request_async(formData.email.value); + loading = false; + if (response.isCreated) { + showSuccessAlert = true; + } else if (response.knownProblem) { + formError.set_from_known_problem(response.knownProblem); + for (const error of Object.entries(response.knownProblem.errors)) { + if (error[0] === "email") { + let errors = []; + error[1].forEach((e) => errors.push(e)); + formData.email.errors = errors; + } + } + } else { + formError.set($LL.unexpectedError(), $LL.tryAgainSoon()); + } + showErrorAlert = formError.has_error() && !showSuccessAlert; + } +</script> + +<div class="min-h-full flex flex-col justify-center py-12 sm:px-6 lg:px-8"> + <div class="sm:mx-auto sm:w-full p-2 sm:p-0 sm:max-w-md"> + <h2 class="mt-6 text-3xl tracking-tight font-bold text-gray-900"> + {$LL.resetPasswordPage.requestAPasswordReset()} + </h2> + <p class="mt-2 text-sm text-gray-600"> + {$LL.or().toLowerCase()} + <a href="/sign-in" class="link"> + {$LL.signIntoYourAccount().toLowerCase()} + </a> + </p> + </div> + + <div class="mt-8 sm:mx-auto sm:w-full sm:max-w-md"> + <div class="bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10"> + <form class="space-y-6" on:submit|preventDefault={submit_form_async}> + {#if showErrorAlert} + <Alert title={formError.title} message={formError.subtitle} type="error" /> + {:else if showSuccessAlert} + <Alert type="success" title={$LL.success()} message={$LL.resetPasswordPage.requestSentMessage()} /> + {/if} + <Input + id="email" + name="email" + type="email" + autocomplete="email" + errors={formData.email.errors} + bind:value={formData.email.value} + required + label={$LL.emailAddress()} + /> + <Button text={$LL.submit()} type="submit" {loading} fullWidth /> + </form> + </div> + </div> +</div> diff --git a/code/app/src/routes/(main)/(public)/reset-password/+page.ts b/code/app/src/routes/(main)/(public)/reset-password/+page.ts new file mode 100644 index 0000000..c0859e0 --- /dev/null +++ b/code/app/src/routes/(main)/(public)/reset-password/+page.ts @@ -0,0 +1,11 @@ +import LL from '$i18n/i18n-svelte'; +import { get } from 'svelte/store'; +import type { PageLoad } from './$types'; + +const l = get(LL); + +export const load: PageLoad = async () => { + return { + title: l.resetPasswordPage.title(), + }; +};
\ No newline at end of file diff --git a/code/app/src/routes/(main)/(public)/reset-password/[id]/+page.server.ts b/code/app/src/routes/(main)/(public)/reset-password/[id]/+page.server.ts new file mode 100644 index 0000000..22fa29d --- /dev/null +++ b/code/app/src/routes/(main)/(public)/reset-password/[id]/+page.server.ts @@ -0,0 +1,11 @@ +import { is_guid } from "$utilities/validators"; +import { redirect } from "@sveltejs/kit"; +import type { PageServerLoad } from "./$types"; + +export const load: PageServerLoad = async ({ params }) => { + const resetRequestId = params.id ?? ""; + if (!is_guid(resetRequestId)) throw redirect(302, "/reset-password"); + return { + resetRequestId, + }; +};
\ No newline at end of file diff --git a/code/app/src/routes/(main)/(public)/reset-password/[id]/+page.svelte b/code/app/src/routes/(main)/(public)/reset-password/[id]/+page.svelte new file mode 100644 index 0000000..27a1af5 --- /dev/null +++ b/code/app/src/routes/(main)/(public)/reset-password/[id]/+page.svelte @@ -0,0 +1,82 @@ +<script lang="ts"> + import { onMount } from "svelte"; + import LL from "$i18n/i18n-svelte"; + import { Alert, Input, Button } from "$components"; + import type { PageServerData } from "./$types"; + import { goto } from "$app/navigation"; + import { SignInPageMessage, signInPageMessageQueryKey } from "$routes/(main)/(public)/sign-in"; + import { PasswordResetService } from "$services/password-reset-service"; + + export let data: PageServerData; + const passwordResetService = PasswordResetService.resolve(); + + const formData = { + newPassword: { + value: "", + errors: [], + }, + }; + + let finishedPreliminaryLoading = false; + let loading = false; + let canSubmit = true; + let requestIsInvalid = false; + + async function submitFormAsync() { + if (!canSubmit) return; + loading = true; + const request = await passwordResetService.fulfill_request_async(data.resetRequestId, formData.newPassword.value); + if (request.isFulfilled) { + goto("/sign-in?" + signInPageMessageQueryKey + "=" + SignInPageMessage.AFTER_PASSWORD_RESET); + } else if (request.knownProblem) { + } + loading = false; + } + + onMount(async () => { + const response = await passwordResetService.request_is_valid_async(data.resetRequestId); + requestIsInvalid = !response.isValid; + finishedPreliminaryLoading = true; + }); +</script> + +<div class="min-h-full flex flex-col justify-center py-12 sm:px-6 lg:px-8"> + {#if finishedPreliminaryLoading} + <div class="sm:mx-auto sm:w-full p-2 sm:p-0 sm:max-w-md"> + <h2 class="mt-6 text-3xl tracking-tight font-bold text-gray-900"> + {$LL.resetPasswordPage.setANewPassword()} + </h2> + <p class="mt-2 text-sm text-gray-600"> + {$LL.or().toLowerCase()} + <a href="/sign-in" class="link"> + {$LL.signIntoYourAccount().toLowerCase()} + </a> + </p> + </div> + + <div class="mt-8 sm:mx-auto sm:w-full sm:max-w-md"> + <div class="bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10"> + <form class="space-y-6" on:submit|preventDefault={submitFormAsync}> + {#if requestIsInvalid} + <Alert + title={$LL.resetPasswordPage.invalidRequestTitle()} + message={$LL.resetPasswordPage.invalidRequestMessage()} + /> + {/if} + <Input + id="password" + name="password" + type="password" + autocomplete="new-password" + required + bind:value={formData.newPassword.value} + label={$LL.resetPasswordPage.newPassword()} + /> + <Button text={$LL.submit()} type="submit" {loading} fullWidth /> + </form> + </div> + </div> + {:else} + <p>Checking your request...</p> + {/if} +</div> diff --git a/code/app/src/routes/(main)/(public)/reset-password/[id]/+page.ts b/code/app/src/routes/(main)/(public)/reset-password/[id]/+page.ts new file mode 100644 index 0000000..3252b7a --- /dev/null +++ b/code/app/src/routes/(main)/(public)/reset-password/[id]/+page.ts @@ -0,0 +1,11 @@ +import LL from '$i18n/i18n-svelte'; +import { get } from 'svelte/store'; +import type { PageLoad } from './$types'; + +const l = get(LL); + +export const load: PageLoad = async () => { + return { + title: l.resetPasswordPage.fulfillTitle(), + }; +};
\ No newline at end of file diff --git a/code/app/src/routes/(main)/(public)/sign-in/+page.svelte b/code/app/src/routes/(main)/(public)/sign-in/+page.svelte new file mode 100644 index 0000000..66d4575 --- /dev/null +++ b/code/app/src/routes/(main)/(public)/sign-in/+page.svelte @@ -0,0 +1,155 @@ +<script lang="ts"> + import { goto } from "$app/navigation"; + import { Button, Checkbox, Input, Alert } from "$components"; + import LL from "$i18n/i18n-svelte"; + import pwKey from "$actions/pwKey"; + import { onMount } from "svelte"; + import { signInPageMessageQueryKey, signInPageTestKeys, type SignInPageMessage } from "."; + import { AccountService } from "$services/account-service"; + import type { LoginPayload } from "$services/abstractions/IAccountService"; + import { FormError } from "$models/internal/FormError"; + import type { IForm } from "$models/internal/IForm"; + + let messageType: SignInPageMessage | undefined = undefined; + + const accountService = AccountService.resolve(); + const form = { + fields: { + username: { + value: "", + errors: [], + }, + password: { + value: "", + errors: [], + }, + persist: { + value: false, + errors: [], + }, + }, + error: new FormError(), + isLoading: false, + showError: false, + get_payload(): LoginPayload { + return { + password: form.fields.password.value, + username: form.fields.username.value, + persist: !form.fields.persist.value, + }; + }, + async submit_async() { + console.log("sadf"); + form.error.set(); + form.showError = form.error.has_error(); + form.isLoading = true; + const loginResponse = await accountService.login_async(form.get_payload()); + if (loginResponse.isLoggedIn) { + await goto("/home"); + } else if (loginResponse.knownProblem) { + form.error.set_from_known_problem(loginResponse.knownProblem); + } else { + form.error.set($LL.unexpectedError(), $LL.tryAgainSoon()); + } + form.isLoading = false; + form.showError = form.error.has_error(); + }, + } as IForm; + + onMount(() => { + const queryParams = new URLSearchParams(window.location.search); + if (queryParams.get(signInPageMessageQueryKey)) { + messageType = queryParams.get(signInPageMessageQueryKey) as SignInPageMessage; + queryParams.delete(signInPageMessageQueryKey); + window.history.replaceState(null, "", window.location.origin + window.location.pathname); + } + }); +</script> + +<div class="min-h-full flex flex-col justify-center py-12 sm:px-6 lg:px-8"> + {#if messageType} + <div class="sm:max-w-md sm:mx-auto sm:w-full"> + {#if messageType === "after-password-reset"} + <Alert + title={$LL.signInPage.yourNewPasswordIsApplied()} + _pwKey={signInPageTestKeys.afterPasswordResetAlert} + message={$LL.signInPage.signInBelow()} + closeable + /> + {:else if messageType === "user-disabled"} + <Alert + title={$LL.signInPage.yourAccountIsDisabled()} + _pwKey={signInPageTestKeys.userDisabledAlert} + message={$LL.signInPage.contactYourAdminIfDisabled()} + closeable + /> + {:else if messageType === "user-inactivity"} + <Alert + title={$LL.signInPage.youHaveReachedInactivityLimit()} + _pwKey={signInPageTestKeys.userInactivityAlert} + message={$LL.signInPage.feelFreeToSignInAgain()} + closeable + /> + {/if} + </div> + {/if} + <div class="sm:mx-auto sm:w-full p-2 sm:p-0 sm:max-w-md"> + <h2 class="mt-6 text-3xl tracking-tight font-bold text-gray-900"> + {$LL.signInPage.signIn()} + </h2> + <p class="mt-2 text-sm text-gray-600"> + {$LL.or().toLowerCase()} + <a href="/sign-up" use:pwKey={signInPageTestKeys.signUpAnchor} class="link"> + {$LL.createANewAccount().toLowerCase()} + </a> + </p> + </div> + <div class="mt-8 sm:mx-auto sm:w-full sm:max-w-md"> + <div class="bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10"> + {#if form.showError} + <Alert title={form.error.title} message={form.error.subtitle} type="error" _pwKey={signInPageTestKeys.formErrorAlert} /> + {/if} + <form class="space-y-6 mt-2" use:pwKey={signInPageTestKeys.signInForm} on:submit|preventDefault={() => form.submit_async()}> + <Input + id="username" + _pwKey={signInPageTestKeys.usernameInput} + name="username" + type="email" + label={$LL.emailAddress()} + required + errors={form.fields.username.errors} + bind:value={form.fields.username.value} + /> + + <Input + id="password" + name="password" + type="password" + label={$LL.password()} + _pwKey={signInPageTestKeys.passwordInput} + autocomplete="current-password" + required + errors={form.fields.password.errors} + bind:value={form.fields.password.value} + /> + + <div class="flex items-center justify-between"> + <Checkbox + id="remember-me" + _pwKey={signInPageTestKeys.rememberMeCheckbox} + name="remember-me" + bind:checked={form.fields.persist.value} + label={$LL.signInPage.notMyComputer()} + /> + <div class="text-sm"> + <a href="/reset-password" class="link" use:pwKey={signInPageTestKeys.resetPasswordAnchor}> + {$LL.signInPage.resetPassword()} + </a> + </div> + </div> + + <Button text={$LL.submit()} fullWidth type="submit" loading={form.isLoading} /> + </form> + </div> + </div> +</div> diff --git a/code/app/src/routes/(main)/(public)/sign-in/+page.ts b/code/app/src/routes/(main)/(public)/sign-in/+page.ts new file mode 100644 index 0000000..bebc459 --- /dev/null +++ b/code/app/src/routes/(main)/(public)/sign-in/+page.ts @@ -0,0 +1,11 @@ +import LL from '$i18n/i18n-svelte'; +import { get } from 'svelte/store'; +import type { PageLoad } from './$types'; + +const l = get(LL); + +export const load: PageLoad = async () => { + return { + title: l.signInPage.title(), + }; +};
\ No newline at end of file diff --git a/code/app/src/routes/(main)/(public)/sign-in/index.spec.js b/code/app/src/routes/(main)/(public)/sign-in/index.spec.js new file mode 100644 index 0000000..9d0122d --- /dev/null +++ b/code/app/src/routes/(main)/(public)/sign-in/index.spec.js @@ -0,0 +1,12 @@ +import { test, expect } from "@playwright/test"; +import { signInPageTestKeys } from "./index.js"; +import { get_test_context } from "$configuration"; +import { get_pw_key_selector } from "$utilities/testing-helpers"; + +const context = get_test_context(); + +test("form loads", async ({ page }) => { + page.goto("/sign-in"); + const form = page.locator(get_pw_key_selector(signInPageTestKeys.signInForm)); + expect(form.isVisible()).toBeTruthy(); +}); diff --git a/code/app/src/routes/(main)/(public)/sign-in/index.ts b/code/app/src/routes/(main)/(public)/sign-in/index.ts new file mode 100644 index 0000000..c1a1929 --- /dev/null +++ b/code/app/src/routes/(main)/(public)/sign-in/index.ts @@ -0,0 +1,20 @@ +export enum SignInPageMessage { + AFTER_PASSWORD_RESET = "after-password-reset", + USER_INACTIVITY = "user-inactivity", + USER_DISABLED = "user-disabled", + LOGGED_OUT = "logged-out" +} + +export const signInPageMessageQueryKey = "m"; +export const signInPageTestKeys = { + passwordInput: "password-input", + usernameInput: "username-input", + rememberMeCheckbox: "remember-me-checkbox", + signInForm: "sign-in-form", + userInactivityAlert: SignInPageMessage.USER_INACTIVITY + "-alert", + userDisabledAlert: SignInPageMessage.USER_DISABLED + "-alert", + afterPasswordResetAlert: SignInPageMessage.AFTER_PASSWORD_RESET + "-alert", + formErrorAlert: "form-error-alert", + resetPasswordAnchor: "reset-password-anchor", + signUpAnchor: "sign-up-anchor", +};
\ No newline at end of file diff --git a/code/app/src/routes/(main)/(public)/sign-up/+page.svelte b/code/app/src/routes/(main)/(public)/sign-up/+page.svelte new file mode 100644 index 0000000..470ac5d --- /dev/null +++ b/code/app/src/routes/(main)/(public)/sign-up/+page.svelte @@ -0,0 +1,106 @@ +<script lang="ts"> + import { goto } from "$app/navigation"; + import { Button, Input, Alert } from "$components"; + import LL from "$i18n/i18n-svelte"; + import { FormError } from "$models/internal/FormError"; + import type { CreateAccountPayload } from "$services/abstractions/IAccountService"; + import { AccountService } from "$services/account-service"; + + const formData = { + username: { + value: "", + errors: [], + }, + password: { + value: "", + errors: [], + }, + as_payload(): CreateAccountPayload { + return { + username: formData.username.value, + password: formData.password.value, + }; + }, + }; + + const formError = new FormError(); + const accountService = new AccountService(); + + let loading = false; + let showErrorAlert = false; + + async function submit_form_async() { + loading = true; + showErrorAlert = false; + formError.set(); + formData.username.errors = []; + formData.password.errors = []; + const response = await accountService.create_account_async(formData.as_payload()); + if (response.isCreated) { + await goto("/home"); + } else if (response.knownProblem) { + formError.set_from_known_problem(response.knownProblem); + for (const error of Object.entries(response.knownProblem.errors)) { + if (error[0] === "username") { + const errors = []; + error[1].forEach((e) => errors.push(e)); + formData.username.errors = errors; + } + if (error[0] === "password") { + const errors = []; + error[1].forEach((e) => errors.push(e)); + formData.password.errors = errors; + } + } + } else { + formError.set($LL.unexpectedError(), $LL.tryAgainSoon()); + } + loading = false; + showErrorAlert = formError.has_error(); + } +</script> + +<div class="min-h-full flex flex-col justify-center py-12 sm:px-6 lg:px-8"> + <div class="sm:mx-auto sm:w-full p-2 sm:p-0 sm:max-w-md"> + <h2 class="mt-6 text-3xl tracking-tight font-bold text-gray-900"> + {$LL.signUpPage.createYourNewAccount()} + </h2> + <p class="mt-2 text-sm text-gray-600"> + {$LL.or().toLowerCase()} + <a href="/sign-in" class="link"> + {$LL.signIntoYourAccount().toLowerCase()} + </a> + </p> + </div> + + <div class="mt-8 sm:mx-auto sm:w-full sm:max-w-md"> + <div class="bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10"> + {#if showErrorAlert} + <Alert title={formError.title} message={formError.subtitle} type="error" class="mb-2" /> + {/if} + <form class="space-y-6" on:submit|preventDefault={submit_form_async}> + <Input + label={$LL.emailAddress()} + id="email" + name="email" + autocomplete="email" + required + type="email" + bind:value={formData.username.value} + errors={formData.username.errors} + /> + + <Input + label={$LL.password()} + id="password" + name="password" + required + type="password" + bind:value={formData.password.value} + errors={formData.password.errors} + /> + <Button type="submit" text={$LL.submit()} {loading} fullWidth /> + </form> + </div> + </div> +</div> diff --git a/code/app/src/routes/(main)/(public)/sign-up/+page.ts b/code/app/src/routes/(main)/(public)/sign-up/+page.ts new file mode 100644 index 0000000..8c86f55 --- /dev/null +++ b/code/app/src/routes/(main)/(public)/sign-up/+page.ts @@ -0,0 +1,11 @@ +import LL from '$i18n/i18n-svelte'; +import { get } from 'svelte/store'; +import type { PageLoad } from './$types'; + +const l = get(LL); + +export const load: PageLoad = async () => { + return { + title: l.signUpPage.title(), + }; +};
\ No newline at end of file diff --git a/code/app/src/routes/(main)/+layout.server.ts b/code/app/src/routes/(main)/+layout.server.ts new file mode 100644 index 0000000..25043aa --- /dev/null +++ b/code/app/src/routes/(main)/+layout.server.ts @@ -0,0 +1,45 @@ +import { api_base, CookieNames } from "$configuration"; +import { cached_result_async, CacheKeys } from "$utilities/cache"; +import { log_debug, log_error } from "$utilities/logger"; +import { get_md5_hash } from "$utilities/crypto-helpers"; +import { error, redirect } from "@sveltejs/kit"; +import type { LayoutServerLoad } from "./$types"; + +export const load: LayoutServerLoad = async ({ route, cookies, locals, fetch }) => { + const isBaseRoute = route.id === "/(main)"; + const isPortalRoute = route.id === "/(main)/(public)/portal"; + const isPublicRoute = (isBaseRoute || (route.id?.startsWith("/(main)/(public)") ?? false)) ?? true; + const sessionCookieValue = cookies.get(CookieNames.session); + let sessionIsValid = false; + if ((sessionCookieValue?.length > 0 ?? false)) { + const sessionHash = get_md5_hash(sessionCookieValue); + sessionIsValid = (await cached_result_async<Response>(sessionHash + "_" + CacheKeys.isAuthenticated, 120, () => fetch(api_base("_/is-authenticated"), { + headers: { + Cookie: CookieNames.session + "=" + sessionCookieValue, + }, + }).catch((e) => { + log_error(e); + throw error(503, { + message: "We are experiencing a service disruption! Have patience while we resolve the issue.", + }); + }))).ok; + } + + log_debug("Base Layout loaded", { + sessionIsValid, + isPublicRoute, + isBaseRoute, + isPortalRoute, + routeId: route.id, + }); + + if (sessionIsValid && isPublicRoute && !isPortalRoute) { + throw redirect(302, "/home"); + } else if (!isPortalRoute && (isBaseRoute || !sessionIsValid && !isPublicRoute)) { + throw redirect(302, "/sign-in"); + } + + return { + locale: locals.locale, + }; +}; diff --git a/code/app/src/routes/(main)/+layout.svelte b/code/app/src/routes/(main)/+layout.svelte new file mode 100644 index 0000000..7662d6a --- /dev/null +++ b/code/app/src/routes/(main)/+layout.svelte @@ -0,0 +1,31 @@ +<script lang="ts"> + import "../../app.pcss"; + import { setLocale } from "$i18n/i18n-svelte"; + import { ExclamationTriangleIcon } from "$components/icons"; + import { page } from "$app/stores"; + import type { LayoutData } from "./$types"; + + let online = true; + export let data: LayoutData; + setLocale(data.locale); +</script> + +<svelte:window bind:online /> +<svelte:head> + <title>{$page.data.title ? $page.data.title + " - Greatoffice" : "Greatoffice"}</title> +</svelte:head> + +{#if !online} + <div class="bg-yellow-50 relative z-50 p-4"> + <div class="flex"> + <div class="flex-shrink-0"> + <ExclamationTriangleIcon class="bg-yellow-50 text-yellow-500" /> + </div> + <div class="ml-3"> + <p class="text-sm text-yellow-700">You seem to be offline, please check your internet connection.</p> + </div> + </div> + </div> +{/if} + +<slot /> diff --git a/code/app/src/routes/(main)/+layout.ts b/code/app/src/routes/(main)/+layout.ts new file mode 100644 index 0000000..3893260 --- /dev/null +++ b/code/app/src/routes/(main)/+layout.ts @@ -0,0 +1,10 @@ +import type { LayoutLoad } from "./$types"; +import type { Locales } from "$i18n/i18n-types"; +import { loadLocaleAsync } from "$i18n/i18n-util.async"; +import { setLocale } from "$i18n/i18n-svelte"; + +export const load: LayoutLoad<{ locale: Locales }> = async ({ data: { locale } }) => { + await loadLocaleAsync(locale); + setLocale(locale); + return { locale }; +};
\ No newline at end of file diff --git a/code/app/src/routes/(main)/+page.svelte b/code/app/src/routes/(main)/+page.svelte new file mode 100644 index 0000000..e507a19 --- /dev/null +++ b/code/app/src/routes/(main)/+page.svelte @@ -0,0 +1 @@ +<p class="text-bold p-1">Hold on...</p> diff --git a/code/app/src/routes/book/+layout.svelte b/code/app/src/routes/book/+layout.svelte new file mode 100644 index 0000000..385d0a6 --- /dev/null +++ b/code/app/src/routes/book/+layout.svelte @@ -0,0 +1,46 @@ +<script> + import { page } from "$app/stores"; + import "../../app.pcss"; +</script> + +<div id="wrapper"> + <nav> + <a href="/book/alerts" class="link" class:active={$page.url.pathname.startsWith("/book/alerts")}>Alerts</a> + <a href="/book/buttons" class="link" class:active={$page.url.pathname.startsWith("/book/buttons")}>Buttons</a> + <a href="/book/toggles" class="link" class:active={$page.url.pathname.startsWith("/book/toggles")}>Toggles</a> + <a href="/book/inputs" class="link" class:active={$page.url.pathname.startsWith("/book/inputs")}>Inputs</a> + <a href="/book/badges" class="link" class:active={$page.url.pathname.startsWith("/book/badges")}>Badges</a> + <a href="/book/notifications" class="link" class:active={$page.url.pathname.startsWith("/book/notifications")}>Notifications</a> + </nav> + <main> + <slot /> + </main> +</div> + +<style global lang="postcss"> + #wrapper { + display: flex; + flex-direction: row; + } + nav { + min-width: 120px; + padding: 10px; + display: flex; + flex-direction: column; + position: sticky; + position: -webkit-sticky; + top: 0; + height: fit-content; + } + main { + width: 100%; + padding: 10px; + } + section { + margin-bottom: 25px; + + h2 { + margin-bottom: 5px; + } + } +</style> diff --git a/code/app/src/routes/book/+layout.ts b/code/app/src/routes/book/+layout.ts new file mode 100644 index 0000000..d297dfd --- /dev/null +++ b/code/app/src/routes/book/+layout.ts @@ -0,0 +1,3 @@ +export const ssr = import.meta.env.DEV; +export const csr = import.meta.env.DEV; +export const prerender = import.meta.env.DEV;
\ No newline at end of file diff --git a/code/app/src/routes/book/+page.svelte b/code/app/src/routes/book/+page.svelte new file mode 100644 index 0000000..635b3c2 --- /dev/null +++ b/code/app/src/routes/book/+page.svelte @@ -0,0 +1 @@ +<p>A showcase of greatoffices components</p> diff --git a/code/app/src/routes/book/alerts/+page.svelte b/code/app/src/routes/book/alerts/+page.svelte new file mode 100644 index 0000000..ed4c92b --- /dev/null +++ b/code/app/src/routes/book/alerts/+page.svelte @@ -0,0 +1,70 @@ +<script> + import Alert from "$components/alert.svelte"; +</script> + +<section> + <h2>Info</h2> + <Alert type="info" message="This is message" title="This is title"/> +</section> +<section> + <h2>Warning</h2> + <Alert type="warning" message="This is message" title="This is title"/> +</section> +<section> + <h2>Error</h2> + <Alert type="error" message="This is message" title="This is title"/> +</section> +<section> + <h2>Success</h2> + <Alert type="success" message="This is message" title="This is title"/> +</section> +<section> + <h2>Actions</h2> + <Alert + type="info" + message="This is message" + title="This is title" + closeable + actions={[ + { + id: "confirm", + text: "Yes!", + }, + { + id: "cancel", + text: "No!", + color: "red", + }, + ]} + /> +</section> +<section> + <h2>Right link</h2> + <Alert + on:rightLinkCliked={() => alert("Right link clicked")} + rightLinkText="Link or action" + title="Go here" + message="Hehe" + type="error" + /> +</section> +<section> + <h2>List</h2> + <Alert + title="This is title" + listItems={["Message 1", "Message 2"]} + type="error" + message="This is bad dude" + closeable + closeableCooldown="60" + id="alert-1" + on:actrepeat={() => { + alert("Repeat requested"); + }} + actions={[{ id: "repeat", text: "Try again" }]} + /> +</section> +<section> + <h2>Closeable</h2> + <Alert message="This is message" closeable type="info"/> +</section> diff --git a/code/app/src/routes/book/badges/+page.svelte b/code/app/src/routes/book/badges/+page.svelte new file mode 100644 index 0000000..50ae61e --- /dev/null +++ b/code/app/src/routes/book/badges/+page.svelte @@ -0,0 +1,19 @@ +<script lang="ts"> + import Badge from "$components/badge.svelte"; +</script> + +<section> + <h2>Variants</h2> + <Badge text="default"/> + <Badge type="blue" text="blue"/> + <Badge type="green" text="green"/> + <Badge type="red" text="red"/> + <Badge type="tame" text="tame"/> + <Badge type="yellow" text="yellow"/> + <Badge size="large" text="large"/> + <Badge text="with dot" withDot type="blue"/> + <Badge text="removable" removable id="badge-1" on:remove={(e) => alert("removed " + e.detail.id)}/> + <Badge text="with dot" size="large" withDot type="blue"/> + <Badge text="removable" removable size="large" id="badge-2" uppercase + on:remove={(e) => alert("removed " + e.detail.id)}/> +</section> diff --git a/code/app/src/routes/book/buttons/+page.svelte b/code/app/src/routes/book/buttons/+page.svelte new file mode 100644 index 0000000..6668a64 --- /dev/null +++ b/code/app/src/routes/book/buttons/+page.svelte @@ -0,0 +1,23 @@ +<script> + import Button from "$components/button.svelte"; +</script> + +<section> + <h2>Primary</h2> + <Button kind="primary" text="Small" size="sm"/> + <Button kind="primary" text="Medium/Default"/> + <Button kind="primary" text="Large" size="lg"/> + <Button kind="primary" text="Extra large" size="xl"/> +</section> +<section> + <h2>Secondary</h2> + <Button kind="secondary" text="Click me!"/> +</section> +<section> + <h2>White</h2> + <Button kind="white" text="Click me!"/> +</section> +<section> + <h2>Loading</h2> + <Button kind="primary" loading={true} text="Wait"/> +</section> diff --git a/code/app/src/routes/book/inputs/+page.svelte b/code/app/src/routes/book/inputs/+page.svelte new file mode 100644 index 0000000..433607b --- /dev/null +++ b/code/app/src/routes/book/inputs/+page.svelte @@ -0,0 +1,75 @@ +<script lang="ts"> + import {TextArea, Input, Combobox} from "$components"; + import {DatabaseIcon} from "$components/icons"; + + let value; + let i = 0; + let options = []; + let tempOptions = []; + while (i < 101) { + tempOptions.push({ + id: crypto.randomUUID(), + name: "Option " + i, + }); + options = tempOptions; + i++; + } + + async function add({name}) { + const copy = options; + copy.push({ + id: crypto.randomUUID(), + name: name, + }); + options = copy; + } +</script> + +<section> + <h2>Combobox</h2> + <Combobox {options} label="Wiii" multiple createable on_create_async={add}/> +</section> + +<section> + <h2>Default</h2> + <Input label="Input me" placeholder="Hello" bind:value/> +</section> + +<section> + <h2>With icon</h2> + <Input label="Input me" placeholder="Hello" icon={DatabaseIcon} bind:value/> +</section> + +<section> + <h2>With corner hint</h2> + <Input label="Input me ->" placeholder="Hello" cornerHint="Hint hint" bind:value/> +</section> + +<section> + <h2>Disabled</h2> + <Input label="No" placeholder="Sorry" disabled bind:value/> +</section> + +<section> + <h2>Errored</h2> + <Input label="No" placeholder="Sorry" errorText="That's not right" bind:value icon={DatabaseIcon}/> +</section> + +<section> + <h2>Many errors</h2> + <Input label="No" placeholder="Sorry" errors={["That's not right", "Call help!", "Get it together"]} bind:value + icon={DatabaseIcon}/> +</section> + +<section> + <h2>Help</h2> + <Input label="Go ahead" placeholder="Write here" helpText="Write above" bind:value/> +</section> +<section> + <h2>Addon</h2> + <Input label="Go ahead" placeholder="Write here" bind:value helpText="Write above" addon="To the right"/> +</section> +<section> + <h2>Textarea</h2> + <TextArea bind:value label="Hi"/> +</section> diff --git a/code/app/src/routes/book/notifications/+page.svelte b/code/app/src/routes/book/notifications/+page.svelte new file mode 100644 index 0000000..1a6144d --- /dev/null +++ b/code/app/src/routes/book/notifications/+page.svelte @@ -0,0 +1,50 @@ +<script lang="ts"> + import { Notification } from "$components"; + import type { NotificationType } from "$components/notification.svelte"; + + let type = "info" as NotificationType; + let nonClosable = false; + let title = "Title"; + let subtitle = "Subtitle"; + let hideAfterSeconds = -1; + let timeout; + + function open(newtype: NotificationType) { + console.log(newtype); + type = newtype; + } +</script> + +<section style="display: flex;flex-direction: column; max-width:200px;gap:5px"> + <h2>Type:</h2> + <select + on:change={(e) => { + //@ts-ignore + open(e.target.selectedOptions[0].value); + }} + > + <option value="info">info</option> + <option value="warning">warning</option> + <option value="error">error</option> + <option value="success">success</option> + <option value="subtle">subtle</option> + </select> + <label for="nonClosable"> + <input type="checkbox" id="nonClosable" bind:checked={nonClosable} /> + nonClosable + </label> + <input type="text" bind:value={title} /> + <input type="text" bind:value={subtitle} /> + <input type="number" bind:value={timeout} placeholder="hideAfterSeconds" /> + <small class="text-sm justify-end"> + <span class="link" on:click={() => (hideAfterSeconds = timeout ?? -1)}>Apply</span> + <span + class="link" + on:click={() => { + hideAfterSeconds = -1; + timeout = 0; + }}>Reset</span + > + </small> + <Notification {title} {subtitle} show={true} {type} {nonClosable} {hideAfterSeconds} /> +</section> diff --git a/code/app/src/routes/book/toggles/+page.svelte b/code/app/src/routes/book/toggles/+page.svelte new file mode 100644 index 0000000..cb0adec --- /dev/null +++ b/code/app/src/routes/book/toggles/+page.svelte @@ -0,0 +1,27 @@ +<script> + import Switch from "$components/switch.svelte"; +</script> + +<section> + <h2>Default</h2> + <Switch /> +</section> +<section> + <h2>Short</h2> + <Switch type="short" /> +</section> +<section> + <h2>Icon</h2> + <Switch type="icon" /> +</section> +<section> + <h2>Label / Description</h2> + <div class="max-w-md"> + <Switch label="Label" description="Some text" /> + </div> +</section> + +<section> + <h2>Label / Description (right aligned)</h2> + <Switch label="Label" description="Some text" rightAlignedLabelDescription /> +</section> diff --git a/code/app/src/services/abstractions/IAccountService.ts b/code/app/src/services/abstractions/IAccountService.ts new file mode 100644 index 0000000..d3d48b0 --- /dev/null +++ b/code/app/src/services/abstractions/IAccountService.ts @@ -0,0 +1,54 @@ +import type { KnownProblem } from "$models/internal/KnownProblem"; +import type { Writable } from "svelte/store"; + +export interface IAccountService { + session: Writable<Session>, + login_async(payload: LoginPayload): Promise<LoginResponse>, + logout_async(): Promise<void>, + end_session_async(callback?: Function): Promise<void>, + create_account_async(payload: CreateAccountPayload): Promise<CreateAccountResponse>, + delete_current_async(): Promise<DeleteAccountResponse>, + update_current_async(payload: UpdateAccountPayload): Promise<UpdateAccountResponse>, +} + +export type Session = { + username: string, + displayName: string, + id: string, + _lastUpdated: number +} + +export type LoginPayload = { + username: string, + password: string, + persist: boolean +} + +export type LoginResponse = { + isLoggedIn: boolean, + knownProblem?: KnownProblem +} + +export type CreateAccountPayload = { + username: string, + password: string, +} + +export type CreateAccountResponse = { + isCreated: boolean, + knownProblem?: KnownProblem +} + +export type DeleteAccountResponse = { + isDeleted: boolean +} + +export type UpdateAccountPayload = { + username: string, + password: string +} + +export type UpdateAccountResponse = { + isUpdated: boolean, + knownProblem?: KnownProblem +}
\ No newline at end of file diff --git a/code/app/src/services/abstractions/IApiTokenService.ts b/code/app/src/services/abstractions/IApiTokenService.ts new file mode 100644 index 0000000..fdf82eb --- /dev/null +++ b/code/app/src/services/abstractions/IApiTokenService.ts @@ -0,0 +1,34 @@ +import type { Temporal } from "temporal-polyfill" + +export interface IApiTokenService { + create_token_async(payload: CreateTokenPayload): Promise<CreateTokenResponse>, + delete_token_async(payload: DeleteTokenPayload): Promise<DeleteTokenResponse>, + get_tokens_async(query: TokenQuery): Promise<GetTokensResponse> +} +export type GetTokensResponse = { + results: Array<GetTokensTokenModel> +}; +export type GetTokensTokenModel = { + id: string, + name: string, + permissions: string[] +} +export type TokenQuery = { + includeStale: boolean +}; +export type DeleteTokenResponse = { + isDeleted: boolean +}; +export type DeleteTokenPayload = { + id: string +}; +export type CreateTokenResponse = { + isCreated: boolean +}; +export type CreateTokenPayload = { + expiryDate: Temporal.PlainDateTime, + allowRead: boolean, + allowCreate: boolean, + allowUpdate: boolean, + allowDelete: boolean +};
\ No newline at end of file diff --git a/code/app/src/services/abstractions/IPasswordResetService.ts b/code/app/src/services/abstractions/IPasswordResetService.ts new file mode 100644 index 0000000..59d2bc6 --- /dev/null +++ b/code/app/src/services/abstractions/IPasswordResetService.ts @@ -0,0 +1,21 @@ +import type { KnownProblem } from "$models/internal/KnownProblem" + +export interface IPasswordResetService { + create_request_async(email: string): Promise<CreateRequestResponse>, + fulfill_request_async(id: string, newPassword: string): Promise<FulfillRequestResponse>, + request_is_valid_async(id: string): Promise<RequestIsValidResponse> +} + +export type RequestIsValidResponse = { + isValid: boolean +} + +export type FulfillRequestResponse = { + isFulfilled: boolean, + knownProblem?: KnownProblem +} + +export type CreateRequestResponse = { + isCreated: boolean, + knownProblem?: KnownProblem +}
\ No newline at end of file diff --git a/code/app/src/services/abstractions/ISettingsService.ts b/code/app/src/services/abstractions/ISettingsService.ts new file mode 100644 index 0000000..366e337 --- /dev/null +++ b/code/app/src/services/abstractions/ISettingsService.ts @@ -0,0 +1,3 @@ +export interface ISettingsService { + get_user_settings(): Promise<void>, +}
\ No newline at end of file diff --git a/code/app/src/services/account-service.ts b/code/app/src/services/account-service.ts new file mode 100644 index 0000000..b2bb375 --- /dev/null +++ b/code/app/src/services/account-service.ts @@ -0,0 +1,124 @@ +import { http_delete_async, http_get_async, http_post_async } from "$utilities/_fetch"; +import { browser } from "$app/environment"; +import { api_base, CookieNames, StorageKeys } from "$configuration"; +import { is_known_problem } from "$models/internal/KnownProblem"; +import { log_debug } from "$utilities/logger"; +import { StoreType, create_writable_persistent } from "$utilities/persistent-store"; +import { get } from "svelte/store"; +import type { Writable } from "svelte/store"; +import { Temporal } from "temporal-polyfill"; +import type { + CreateAccountPayload, + CreateAccountResponse, + DeleteAccountResponse, + IAccountService, + LoginPayload, + LoginResponse, + Session, + UpdateAccountPayload, + UpdateAccountResponse, +} from "./abstractions/IAccountService"; + +export class AccountService implements IAccountService { + session: Writable<Session> | undefined; + private sessionCooldown = 3600; + + constructor() { + if (browser) { + this.session = create_writable_persistent({ + name: StorageKeys.session, + initialState: {} as Session, + options: { + store: StoreType.LOCAL, + }, + }); + this.refresh_session(); + } else { + this.session = undefined; + } + } + + static resolve(): IAccountService { + return new AccountService(); + } + + async refresh_session(forceRefresh: boolean = false): Promise<void> { + if (!this.session) return; + const currentValue = get(this.session); + const currentEpoch = Temporal.Now.instant().epochSeconds; + if (!forceRefresh && ((currentValue?._lastUpdated ?? 0) + this.sessionCooldown) > currentEpoch) { + log_debug("Session is not stale yet", { + currentEpoch, + staleEpoch: currentValue?._lastUpdated + this.sessionCooldown, + }); + return; + } + const sessionResponse = await http_get_async(api_base("_/session-data")); + if (sessionResponse.ok) { + this.session.set(await sessionResponse.json()); + } else { + this.session.set(null); + } + } + + async end_session_async(callback: Function = undefined): Promise<void> { + if (!this.session) return; + await this.logout_async(); + this.session.set(null); + if (callback && typeof callback === "function") callback(); + } + + async login_async(payload: LoginPayload): Promise<LoginResponse> { + const response = await http_post_async(api_base("_/account/login"), payload); + if (response.ok) return { isLoggedIn: true }; + if (is_known_problem(response)) return { + isLoggedIn: false, + knownProblem: await response.json(), + }; + return { + isLoggedIn: false, + }; + } + + async logout_async(): Promise<void> { + const response = await http_get_async(api_base("_/account/logout")); + if (!response.ok) { + const deleteCookieResponse = await fetch("/delete-cookie?key=" + CookieNames.session); + if (!deleteCookieResponse.ok) { + throw new Error("Could neither logout nor delete session cookie."); + } + } + return; + } + + async create_account_async(payload: CreateAccountPayload): Promise<CreateAccountResponse> { + const response = await http_post_async(api_base("_/account/create"), payload); + if (response.ok) return { isCreated: true }; + if (is_known_problem(response)) return { + isCreated: false, + knownProblem: await response.json(), + }; + return { + isCreated: false, + }; + } + + async delete_current_async(): Promise<DeleteAccountResponse> { + const response = await http_delete_async(api_base("_/account/delete")); + return { + isDeleted: response.ok, + }; + } + + async update_current_async(payload: UpdateAccountPayload): Promise<UpdateAccountResponse> { + const response = await http_post_async(api_base("_/account/update"), payload); + if (response.ok) return { isUpdated: true }; + if (is_known_problem(response)) return { + isUpdated: false, + knownProblem: await response.json(), + }; + return { + isUpdated: false, + }; + } +}
\ No newline at end of file diff --git a/code/app/src/services/api-tokens-service.ts b/code/app/src/services/api-tokens-service.ts new file mode 100644 index 0000000..fb8b126 --- /dev/null +++ b/code/app/src/services/api-tokens-service.ts @@ -0,0 +1,22 @@ +import { api_base } from "$configuration"; +import { http_delete_async, http_get_async, http_post_async } from "$utilities/_fetch"; +import type { CreateTokenPayload, CreateTokenResponse, DeleteTokenPayload, DeleteTokenResponse, GetTokensResponse, IApiTokenService, TokenQuery } from "./abstractions/IApiTokenService"; + +export class ApiTokenService implements IApiTokenService { + constructor() { } + static resolve() { + return new ApiTokenService(); + } + async create_token_async(payload: CreateTokenPayload): Promise<CreateTokenResponse> { + const response = await http_post_async(api_base("v1/api-tokens/create"), payload); + return; + }; + async delete_token_async(payload: DeleteTokenPayload): Promise<DeleteTokenResponse> { + const response = await http_delete_async(api_base("v1/api-tokens/delete"), payload); + return; + }; + async get_tokens_async(query: TokenQuery): Promise<GetTokensResponse> { + const response = await http_get_async(api_base("v1/api-tokens")); + return; + }; +}
\ No newline at end of file diff --git a/code/app/src/services/password-reset-service.ts b/code/app/src/services/password-reset-service.ts new file mode 100644 index 0000000..4a174fa --- /dev/null +++ b/code/app/src/services/password-reset-service.ts @@ -0,0 +1,48 @@ +import { http_get_async, http_post_async } from "$utilities/_fetch"; +import { api_base } from "$configuration"; +import { is_known_problem } from "$models/internal/KnownProblem"; +import type { + CreateRequestResponse, + FulfillRequestResponse, + IPasswordResetService, + RequestIsValidResponse, +} from "./abstractions/IPasswordResetService"; + +export class PasswordResetService implements IPasswordResetService { + static resolve(): IPasswordResetService { + return new PasswordResetService(); + } + async create_request_async(email: string): Promise<CreateRequestResponse> { + const response = await http_post_async(api_base("_/password-reset-request/create"), { email }); + if (response.ok) return { isCreated: true }; + if (is_known_problem(response)) return { + isCreated: false, + knownProblem: await response.json(), + }; + + return { + isCreated: false, + }; + } + + async fulfill_request_async(id: string, newPassword: string): Promise<FulfillRequestResponse> { + const response = await http_post_async(api_base("_/password-reset-request/fulfill"), { id: id, newPassword }); + if (response.ok) return { isFulfilled: true }; + if (is_known_problem(response)) return { + isFulfilled: false, + knownProblem: await response.json(), + }; + + return { + isFulfilled: false, + }; + } + + async request_is_valid_async(id: string): Promise<RequestIsValidResponse> { + const response = await http_get_async(api_base("_/password-reset-request/is-valid?id=" + id)); + const responseBody = await response.json() as { isValid: boolean }; + return { + isValid: responseBody.isValid, + }; + } +}
\ No newline at end of file diff --git a/code/app/src/services/settings-service.ts b/code/app/src/services/settings-service.ts new file mode 100644 index 0000000..a0a77d4 --- /dev/null +++ b/code/app/src/services/settings-service.ts @@ -0,0 +1,10 @@ +import type { ISettingsService } from "./abstractions/ISettingsService"; + +export class SettingService implements ISettingsService { + static resolve(): ISettingsService { + return new SettingService(); + } + get_user_settings(): Promise<void> { + throw new Error("Method not implemented."); + } +}
\ No newline at end of file diff --git a/code/app/src/utilities/_fetch.ts b/code/app/src/utilities/_fetch.ts new file mode 100644 index 0000000..992c7f5 --- /dev/null +++ b/code/app/src/utilities/_fetch.ts @@ -0,0 +1,94 @@ +import { Temporal } from "temporal-polyfill"; +import { redirect } from "@sveltejs/kit"; +import { browser } from "$app/environment"; +import { goto } from "$app/navigation"; +import { SignInPageMessage, signInPageMessageQueryKey } from "$routes/(main)/(public)/sign-in"; +import { log_error } from "$utilities/logger"; +import { AccountService } from "$services/account-service"; + +export async function http_post_async(url: string, body?: object | string, timeout = -1, skip_401_check = false, abort_signal?: AbortSignal): Promise<Response> { + const init = make_request_init("post", body, abort_signal); + const response = await internal_fetch_async({ url, init, timeout }); + if (!skip_401_check && await redirect_if_401_async(response)) throw new Error("Server returned 401"); + return response; +} + +export async function http_get_async(url: string, timeout = -1, skip_401_check = false, abort_signal?: AbortSignal): Promise<Response> { + const init = make_request_init("get", undefined, abort_signal); + const response = await internal_fetch_async({ url, init, timeout }); + if (!skip_401_check && await redirect_if_401_async(response)) throw new Error("Server returned 401"); + return response; +} + +export async function http_delete_async(url: string, body?: object | string, timeout = -1, skip_401_check = false, abort_signal?: AbortSignal): Promise<Response> { + const init = make_request_init("delete", body, abort_signal); + const response = await internal_fetch_async({ url, init, timeout }); + if (!skip_401_check && await redirect_if_401_async(response)) throw new Error("Server returned 401"); + return response; +} + +async function internal_fetch_async(request: InternalFetchRequest): Promise<Response> { + if (!request.init) throw new Error("request.init is required"); + const fetch_request = new Request(request.url, request.init); + let response: any; + + try { + if (request.timeout && request.timeout > 500) { + response = await Promise.race([ + fetch(fetch_request), + new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout")), request.timeout)), + ]); + } else { + response = await fetch(fetch_request); + } + } catch (error: any) { + log_error(error); + if (error.message === "Timeout") { + console.error("Request timed out"); + } else if (error.message === "Network request failed") { + console.error("No internet connection"); + } else { + throw error; + } + } + + return response; +} + +async function redirect_if_401_async(response: Response): Promise<boolean> { + if (response.status === 401) { + const redirectUrl = `/sign-in?${signInPageMessageQueryKey}=${SignInPageMessage.LOGGED_OUT}`; + await AccountService.resolve().end_session_async(); + if (browser) { + await goto(redirectUrl); + } else { + throw redirect(307, redirectUrl); + } + } + return false; +} + +function make_request_init(method: string, body?: any, signal?: AbortSignal): RequestInit { + const init = { + method, + credentials: "include", + signal, + headers: { + "X-TimeZone": Temporal.Now.timeZone().id, + }, + } as RequestInit; + + if (body) { + init.body = JSON.stringify(body); + init.headers["Content-Type"] = "application/json;charset=UTF-8"; + } + + return init; +} + +export type InternalFetchRequest = { + url: string, + init: RequestInit, + timeout?: number + retry_count?: number, +}
\ No newline at end of file diff --git a/code/app/src/utilities/cache.ts b/code/app/src/utilities/cache.ts new file mode 100644 index 0000000..db9be9a --- /dev/null +++ b/code/app/src/utilities/cache.ts @@ -0,0 +1,38 @@ +import { Temporal } from "temporal-polyfill"; +import { log_debug } from "$utilities/logger"; + +let cache = {}; + +export const CacheKeys = { + isAuthenticated: "isAuthenticated" +} + +export async function cached_result_async<T>(key: string, staleAfterSeconds: number, get_result: any, forceRefresh: boolean = false) { + if (!cache[key]) { + cache[key] = { + l: 0, + c: undefined as T, + }; + } + const staleEpoch = ((cache[key]?.l ?? 0) + staleAfterSeconds); + const isStale = forceRefresh || (staleEpoch < Temporal.Now.instant().epochSeconds); + if (isStale || !cache[key]?.c) { + cache[key].c = typeof get_result === "function" ? await get_result() : get_result; + cache[key].l = Temporal.Now.instant().epochSeconds; + } + + log_debug("Ran cached_result_async", { + cacheKey: key, + isStale, + cache: cache[key], + staleEpoch, + }); + + return cache[key].c as T; +} + +export function clear_cache_key(key: string) { + if (!key) throw new Error("No key was specified"); + cache[key].c = undefined; + log_debug("Cleared cache with key: " + key); +}
\ No newline at end of file diff --git a/code/app/src/utilities/colors.ts b/code/app/src/utilities/colors.ts new file mode 100644 index 0000000..34c7992 --- /dev/null +++ b/code/app/src/utilities/colors.ts @@ -0,0 +1,47 @@ +export function generate_random_hex_color(skip_contrast_check = false) { + let hex = __generate_random_hex_color(); + if (skip_contrast_check) return hex; + while ((__calculate_contrast_ratio("#ffffff", hex) < 4.5) || (__calculate_contrast_ratio("#000000", hex) < 4.5)) { + hex = __generate_random_hex_color(); + } + + return hex; +} + +// Largely copied from chroma js api +function __generate_random_hex_color(): string { + let code = "#"; + for (let i = 0; i < 6; i++) { + code += "0123456789abcdef".charAt(Math.floor(Math.random() * 16)); + } + return code; +} + +function __calculate_contrast_ratio(hex1: string, hex2: string): number { + const rgb1 = __hex_to_rgb(hex1); + const rgb2 = __hex_to_rgb(hex2); + const l1 = __get_luminance(rgb1[0], rgb1[1], rgb1[2]); + const l2 = __get_luminance(rgb2[0], rgb2[1], rgb2[2]); + const result = l1 > l2 ? (l1 + 0.05) / (l2 + 0.05) : (l2 + 0.05) / (l1 + 0.05); + return result; +} + +function __hex_to_rgb(hex: string): number[] { + if (!hex.match(/^#([A-Fa-f0-9]{6})$/)) return []; + if (hex[0] === "#") hex = hex.substring(1, hex.length); + return [parseInt(hex.substring(0, 2), 16), parseInt(hex.substring(2, 4), 16), parseInt(hex.substring(4, 6), 16)]; +} + +function __get_luminance(r: any, g: any, b: any) { + // relative luminance + // see http://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef + r = __luminance_x(r); + g = __luminance_x(g); + b = __luminance_x(b); + return 0.2126 * r + 0.7152 * g + 0.0722 * b; +} + +function __luminance_x(x: any) { + x /= 255; + return x <= 0.03928 ? x / 12.92 : Math.pow((x + 0.055) / 1.055, 2.4); +} diff --git a/code/app/src/utilities/crypto-helpers.ts b/code/app/src/utilities/crypto-helpers.ts new file mode 100644 index 0000000..49af7d3 --- /dev/null +++ b/code/app/src/utilities/crypto-helpers.ts @@ -0,0 +1,48 @@ +// A formatted version of a popular md5 implementation. +// Original copyright (c) Paul Johnston & Greg Holt. +// The function itself is now 42 lines long. +// https://stackoverflow.com/a/60467595 "Don't deny." + +export function get_md5_hash(inputString: string): string { + const hc = "0123456789abcdef"; + function rh(n) { var j, s = ""; for (j = 0; j <= 3; j++) s += hc.charAt((n >> (j * 8 + 4)) & 0x0F) + hc.charAt((n >> (j * 8)) & 0x0F); return s; } + function ad(x, y) { var l = (x & 0xFFFF) + (y & 0xFFFF); var m = (x >> 16) + (y >> 16) + (l >> 16); return (m << 16) | (l & 0xFFFF); } + function rl(n, c) { return (n << c) | (n >>> (32 - c)); } + function cm(q, a, b, x, s, t) { return ad(rl(ad(ad(a, q), ad(x, t)), s), b); } + function ff(a, b, c, d, x, s, t) { return cm((b & c) | ((~b) & d), a, b, x, s, t); } + function gg(a, b, c, d, x, s, t) { return cm((b & d) | (c & (~d)), a, b, x, s, t); } + function hh(a, b, c, d, x, s, t) { return cm(b ^ c ^ d, a, b, x, s, t); } + function ii(a, b, c, d, x, s, t) { return cm(c ^ (b | (~d)), a, b, x, s, t); } + function sb(x) { + var i; var nblk = ((x.length + 8) >> 6) + 1; var blks = new Array(nblk * 16); for (i = 0; i < nblk * 16; i++) blks[i] = 0; + for (i = 0; i < x.length; i++) blks[i >> 2] |= x.charCodeAt(i) << ((i % 4) * 8); + blks[i >> 2] |= 0x80 << ((i % 4) * 8); blks[nblk * 16 - 2] = x.length * 8; return blks; + } + var i, x = sb(inputString), a = 1732584193, b = -271733879, c = -1732584194, d = 271733878, olda, oldb, oldc, oldd; + for (i = 0; i < x.length; i += 16) { + olda = a; oldb = b; oldc = c; oldd = d; + a = ff(a, b, c, d, x[i + 0], 7, -680876936); d = ff(d, a, b, c, x[i + 1], 12, -389564586); c = ff(c, d, a, b, x[i + 2], 17, 606105819); + b = ff(b, c, d, a, x[i + 3], 22, -1044525330); a = ff(a, b, c, d, x[i + 4], 7, -176418897); d = ff(d, a, b, c, x[i + 5], 12, 1200080426); + c = ff(c, d, a, b, x[i + 6], 17, -1473231341); b = ff(b, c, d, a, x[i + 7], 22, -45705983); a = ff(a, b, c, d, x[i + 8], 7, 1770035416); + d = ff(d, a, b, c, x[i + 9], 12, -1958414417); c = ff(c, d, a, b, x[i + 10], 17, -42063); b = ff(b, c, d, a, x[i + 11], 22, -1990404162); + a = ff(a, b, c, d, x[i + 12], 7, 1804603682); d = ff(d, a, b, c, x[i + 13], 12, -40341101); c = ff(c, d, a, b, x[i + 14], 17, -1502002290); + b = ff(b, c, d, a, x[i + 15], 22, 1236535329); a = gg(a, b, c, d, x[i + 1], 5, -165796510); d = gg(d, a, b, c, x[i + 6], 9, -1069501632); + c = gg(c, d, a, b, x[i + 11], 14, 643717713); b = gg(b, c, d, a, x[i + 0], 20, -373897302); a = gg(a, b, c, d, x[i + 5], 5, -701558691); + d = gg(d, a, b, c, x[i + 10], 9, 38016083); c = gg(c, d, a, b, x[i + 15], 14, -660478335); b = gg(b, c, d, a, x[i + 4], 20, -405537848); + a = gg(a, b, c, d, x[i + 9], 5, 568446438); d = gg(d, a, b, c, x[i + 14], 9, -1019803690); c = gg(c, d, a, b, x[i + 3], 14, -187363961); + b = gg(b, c, d, a, x[i + 8], 20, 1163531501); a = gg(a, b, c, d, x[i + 13], 5, -1444681467); d = gg(d, a, b, c, x[i + 2], 9, -51403784); + c = gg(c, d, a, b, x[i + 7], 14, 1735328473); b = gg(b, c, d, a, x[i + 12], 20, -1926607734); a = hh(a, b, c, d, x[i + 5], 4, -378558); + d = hh(d, a, b, c, x[i + 8], 11, -2022574463); c = hh(c, d, a, b, x[i + 11], 16, 1839030562); b = hh(b, c, d, a, x[i + 14], 23, -35309556); + a = hh(a, b, c, d, x[i + 1], 4, -1530992060); d = hh(d, a, b, c, x[i + 4], 11, 1272893353); c = hh(c, d, a, b, x[i + 7], 16, -155497632); + b = hh(b, c, d, a, x[i + 10], 23, -1094730640); a = hh(a, b, c, d, x[i + 13], 4, 681279174); d = hh(d, a, b, c, x[i + 0], 11, -358537222); + c = hh(c, d, a, b, x[i + 3], 16, -722521979); b = hh(b, c, d, a, x[i + 6], 23, 76029189); a = hh(a, b, c, d, x[i + 9], 4, -640364487); + d = hh(d, a, b, c, x[i + 12], 11, -421815835); c = hh(c, d, a, b, x[i + 15], 16, 530742520); b = hh(b, c, d, a, x[i + 2], 23, -995338651); + a = ii(a, b, c, d, x[i + 0], 6, -198630844); d = ii(d, a, b, c, x[i + 7], 10, 1126891415); c = ii(c, d, a, b, x[i + 14], 15, -1416354905); + b = ii(b, c, d, a, x[i + 5], 21, -57434055); a = ii(a, b, c, d, x[i + 12], 6, 1700485571); d = ii(d, a, b, c, x[i + 3], 10, -1894986606); + c = ii(c, d, a, b, x[i + 10], 15, -1051523); b = ii(b, c, d, a, x[i + 1], 21, -2054922799); a = ii(a, b, c, d, x[i + 8], 6, 1873313359); + d = ii(d, a, b, c, x[i + 15], 10, -30611744); c = ii(c, d, a, b, x[i + 6], 15, -1560198380); b = ii(b, c, d, a, x[i + 13], 21, 1309151649); + a = ii(a, b, c, d, x[i + 4], 6, -145523070); d = ii(d, a, b, c, x[i + 11], 10, -1120210379); c = ii(c, d, a, b, x[i + 2], 15, 718787259); + b = ii(b, c, d, a, x[i + 9], 21, -343485551); a = ad(a, olda); b = ad(b, oldb); c = ad(c, oldc); d = ad(d, oldd); + } + return rh(a) + rh(b) + rh(c) + rh(d); +}
\ No newline at end of file diff --git a/code/app/src/utilities/dom-helpers.ts b/code/app/src/utilities/dom-helpers.ts new file mode 100644 index 0000000..94a74c1 --- /dev/null +++ b/code/app/src/utilities/dom-helpers.ts @@ -0,0 +1,105 @@ +interface CreateElementOptions { + name: string, + properties?: object, + children?: Array<HTMLElement | Function | Node> +} + +export function create_element_from_object(elementOptions: CreateElementOptions): HTMLElement { + return create_element(elementOptions.name, elementOptions.properties, elementOptions.children); +} + +export function create_element(name: string, properties?: object, children?: Array<HTMLElement | any>): HTMLElement { + if (!name || name.length < 1) { + throw new Error("name is required"); + } + const node = document.createElement(name); + if (properties) { + for (const [key, value] of Object.entries(properties)) { + // @ts-ignore + node[key] = value; + } + } + + if (children && children.length > 0) { + let actualChildren = children; + if (typeof children === "function") { + // @ts-ignore + actualChildren = children(); + } + for (const child of actualChildren) { + node.appendChild(child as Node); + } + } + return node; +} + +// https://stackoverflow.com/a/45215694/11961742 +export function get_selected_options(domElement: HTMLSelectElement): Array<string> { + const ret = []; + + // fast but not universally supported + if (domElement.selectedOptions !== undefined) { + for (let i = 0; i < domElement.selectedOptions.length; i++) { + ret.push(domElement.selectedOptions[i].value); + } + + // compatible, but can be painfully slow + } else { + for (let i = 0; i < domElement.options.length; i++) { + if (domElement.options[i].selected) { + ret.push(domElement.options[i].value); + } + } + } + return ret; +} + + +export function get_element_position(element: HTMLElement | any) { + if (!element) return {x: 0, y: 0}; + let x = 0; + let y = 0; + while (true) { + x += element.offsetLeft; + y += element.offsetTop; + if (element.offsetParent === null) { + break; + } + element = element.offsetParent; + } + return {x, y}; +} + +export function restrict_input_to_numbers(element: HTMLElement, specials: Array<string> = [], mergeSpecialsWithDefaults: boolean = false): void { + if (!element) return; + element.addEventListener("keydown", (e) => { + const defaultSpecials = ["Backspace", "ArrowLeft", "ArrowRight", "Tab"]; + let keys = specials.length > 0 ? specials : defaultSpecials; + if (mergeSpecialsWithDefaults && specials) { + keys = [...specials, ...defaultSpecials]; + } + if (keys.indexOf(e.key) !== -1) { + return; + } + if (isNaN(parseInt(e.key))) { + e.preventDefault(); + } + }); +} + +export function element_has_focus(element: HTMLElement): boolean { + return element === document.activeElement; +} + +export function move_focus(element: HTMLElement): void { + if (!element) { + element = document.getElementsByTagName("body")[0]; + } + element.focus(); + // @ts-ignore + if (!element_has_focus(element)) { + element.setAttribute("tabindex", "-1"); + element.focus(); + } +} + diff --git a/code/app/src/utilities/global-state.ts b/code/app/src/utilities/global-state.ts new file mode 100644 index 0000000..b585ced --- /dev/null +++ b/code/app/src/utilities/global-state.ts @@ -0,0 +1,22 @@ +import { get } from "svelte/store"; +import { create_writable_persistent } from "./persistent-store"; + +const state = create_writable_persistent<any>({ + initialState: {}, + name: "global-state" +}); + +export type GlobalStateKeys = "isLoggedIn" | "showEmailValidatedAlertWhenLoggedIn" | "all"; + +export function fgs(key: GlobalStateKeys): any { + const value = get(state); + if (key === "all") return value; + return value[key]; +} + +export function sgs(key: GlobalStateKeys, value: any) { + if (key === "all") throw new Error("Not allowed to set global state key: all"); + const stateValue = get(state); + stateValue[key] = JSON.stringify(value) + state.set(stateValue); +}
\ No newline at end of file diff --git a/code/app/src/utilities/logger.ts b/code/app/src/utilities/logger.ts new file mode 100644 index 0000000..c21bd76 --- /dev/null +++ b/code/app/src/utilities/logger.ts @@ -0,0 +1,118 @@ +import { browser, dev } from "$app/environment"; +import { env } from '$env/dynamic/private'; +import { StorageKeys } from "$configuration"; +import pino, { type Logger, type LoggerOptions } from "pino"; +import { createStream } from "pino-seq"; +import type { SeqConfig } from "pino-seq"; + +function get_pino_logger(): Logger { + const config = { + name: "greatoffice-app", + level: LogLevel.current().as_string(), + customLevels: { + "INFO": LogLevel.INFO, + "WARNING": LogLevel.WARNING, + "ERROR": LogLevel.ERROR, + "DEBUG": LogLevel.DEBUG, + "SILENT": LogLevel.SILENT, + } + } as LoggerOptions; + + const seq = { + config: { + apiKey: browser ? env.SEQ_API_KEY : "", + serverUrl: browser ? env.SEQ_SERVER_URL : "" + } as SeqConfig, + streams: [{ + level: LogLevel.to_string(LogLevel.DEBUG), + }], + enabled: () => ( + !browser + && !dev + && seq.config.apiKey.length > 0 + && seq.config.serverUrl.length > 0 + ) + }; + + return seq.enabled() ? pino(config, createStream(seq.config)) : pino(config); +} + +type LogLevelString = "DEBUG" | "INFO" | "WARNING" | "ERROR" | "SILENT"; + +export const LogLevel = { + DEBUG: 0, + INFO: 1, + WARNING: 2, + ERROR: 3, + SILENT: 4, + current(): { as_string: Function, as_number: Function } { + const logLevelString = (browser ? window.sessionStorage.getItem(StorageKeys.logLevel) : env.LOG_LEVEL) as LogLevelString; + return { + as_number(): number { + return LogLevel.to_number_or_default(logLevelString, LogLevel.INFO) + }, + as_string(): LogLevelString { + return logLevelString.length > 3 ? logLevelString : LogLevel.to_string(LogLevel.INFO); + } + } + }, + to_string(levelInt: number): LogLevelString { + switch (levelInt) { + case 0: + return "DEBUG"; + case 1: + return "INFO"; + case 2: + return "WARNING"; + case 3: + return "ERROR"; + case 4: + return "SILENT"; + default: + throw new Error("Unknown LogLevel number " + levelInt); + } + }, + to_number_or_default(levelString?: string | null, defaultValue?: number): number { + if (!levelString && defaultValue) return defaultValue; + else if (!levelString && !defaultValue) throw new Error("levelString was empty, and no default value was specified"); + switch (levelString?.toUpperCase()) { + case "DEBUG": + return 0; + case "INFO": + return 1; + case "WARNING": + return 2; + case "ERROR": + return 3; + case "SILENT": + return 4; + default: + if (!defaultValue) throw new Error("Unknown LogLevel string " + levelString + ", and no defaultValue"); + else return defaultValue; + } + }, +}; + +export function log_warning(message: string, ...additional: any[]): void { + if (LogLevel.current().as_number() <= LogLevel.WARNING) { + get_pino_logger().warn(message, additional); + } +} + +export function log_debug(message: string, ...additional: any[]): void { + if (LogLevel.current().as_number() <= LogLevel.DEBUG) { + get_pino_logger().debug(message, additional); + } +} + +export function log_info(message: string, ...additional: any[]): void { + if (LogLevel.current().as_number() <= LogLevel.INFO) { + get_pino_logger().info(message, additional); + } +} + +export function log_error(message: any, ...additional: any[]): void { + if (LogLevel.current().as_number() <= LogLevel.ERROR) { + get_pino_logger().error(message, additional); + } +}
\ No newline at end of file diff --git a/code/app/src/utilities/misc-helpers.ts b/code/app/src/utilities/misc-helpers.ts new file mode 100644 index 0000000..afb20e7 --- /dev/null +++ b/code/app/src/utilities/misc-helpers.ts @@ -0,0 +1,77 @@ +export function merge_obj_arr<T>(a: Array<T>, b: Array<T>, props: Array<string>): Array<T> { + let start = 0; + let merge = []; + + while (start < a.length) { + + if (a[start] === b[start]) { + //pushing the merged objects into array + merge.push({ ...a[start], ...b[start] }); + } + //incrementing start value + start = start + 1; + } + return merge; +} + +export function no_type_check(x: any) { + return x; +} + +export function capitalise(value: string): string { + return value.charAt(0).toUpperCase() + value.slice(1); +} + +export function get_query_string(params: any = {}): string { + const map = Object.keys(params).reduce((arr: Array<string>, key: string) => { + if (params[key] !== undefined) { + return arr.concat(`${key}=${encodeURIComponent(params[key])}`); + } + return arr; + }, [] as any); + + if (map.length) { + return `?${map.join("&")}`; + } + + return ""; +} + +export function make_url(url: string, params: object): string { + return `${url}${get_query_string(params)}`; +} + +export function noop() { +} + +export function random_string(length: number): string { + if (!length) { + throw new Error("length is undefined"); + } + let result = ""; + const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + const charactersLength = characters.length; + for (let i = 0; i < length; i++) { + result += characters.charAt(Math.floor(Math.random() * charactersLength)); + } + return result; +} + +export function get_random_int(min: number, max: number): number { + min = Math.ceil(min); + max = Math.floor(max); + return Math.floor(Math.random() * (max - min + 1)) + min; +} + +export function get_hash_code(value: string): number | undefined { + let hash = 0; + if (value.length === 0) { + return; + } + for (let i = 0; i < value.length; i++) { + const char = value.charCodeAt(i); + hash = (hash << 5) - hash + char; + hash |= 0; + } + return hash; +} diff --git a/code/app/src/utilities/persistent-store.ts b/code/app/src/utilities/persistent-store.ts new file mode 100644 index 0000000..3f56312 --- /dev/null +++ b/code/app/src/utilities/persistent-store.ts @@ -0,0 +1,111 @@ +import { browser } from "$app/environment"; +import { writable as _writable, readable as _readable } from "svelte/store"; +import type { Writable, Readable, StartStopNotifier } from "svelte/store"; +import { log_debug, log_info } from "./logger"; + +enum StoreType { + SESSION = 0, + LOCAL = 1 +} + +interface StoreOptions { + store?: StoreType; +} + +interface WritableStoreInit<T> { + name: string, + initialState: T, + options?: StoreOptions +} + +interface ReadableStoreInit<T> { + name: string, + initialState: T, + callback: StartStopNotifier<any>, + options?: StoreOptions +} + +function get_store(type: StoreType): Storage { + if (!browser) return undefined; + switch (type) { + case StoreType.SESSION: + return window.sessionStorage; + case StoreType.LOCAL: + return window.localStorage; + } +} + +function prepared_store_value(value: any): string { + try { + return JSON.stringify(value); + } catch (e) { + console.error(e); + return "__INVALID__"; + } +} + +function get_store_value<T>(init: WritableStoreInit<T> | ReadableStoreInit<T>): any { + try { + const storage = get_store(init.options.store); + if (!storage) return; + const value = storage.getItem(init.name); + if (!value) return false; + return JSON.parse(value); + } catch (e) { + console.error(e); + return { __INVALID__: true }; + } +} + +function hydrate<T>(store: Writable<T>, init: WritableStoreInit<T> | ReadableStoreInit<T>): void { + const value = get_store_value<T>(init); + if (value && store.set) store.set(value); +} + +function subscribe<T>(store: Writable<T> | Readable<T>, init: WritableStoreInit<T> | ReadableStoreInit<T>): void { + const storage = get_store(init.options.store); + if (!storage) return; + if (!store.subscribe) return; + store.subscribe((state: any) => { + storage.setItem(init.name, prepared_store_value(state)); + }); +} + +function create_writable_persistent<T>(init: WritableStoreInit<T>): Writable<T> { + if (!browser) { + log_info("WARN: Persistent store is only available in the browser"); + return; + } + if (init.options === undefined) throw new Error("init is a required parameter"); + log_debug("creating writable store with options: ", init); + const store = _writable<T>(init.initialState); + hydrate(store, init); + subscribe(store, init); + return store; +} + +function create_readable_persistent<T>(init: ReadableStoreInit<T>): Readable<T> { + if (!browser) { + log_info("WARN: Persistent store is only available in the browser"); + return; + } + if (init.options === undefined) throw new Error("init is a required parameter"); + log_debug("Creating readable store with options: ", init); + const store = _readable<T>(init.initialState, init.callback); + // hydrate(store, options); + subscribe(store, init); + return store; +} + +export { + create_writable_persistent, + create_readable_persistent, + StoreType, +}; + +export type { + WritableStoreInit as WritableStore, + ReadableStoreInit as ReadableStore, + StoreOptions, +}; + diff --git a/code/app/src/utilities/storage-helpers.ts b/code/app/src/utilities/storage-helpers.ts new file mode 100644 index 0000000..cce655c --- /dev/null +++ b/code/app/src/utilities/storage-helpers.ts @@ -0,0 +1,26 @@ +import { browser } from "$app/environment"; +import { is_empty_object } from "./validators"; + +export type StorageType = "local" | "session"; +export const browserStorage = { + remove_with_regex(type: StorageType, regex: RegExp): void { + if (!browser) return; + const storage = (type === "local" ? window.localStorage : window.sessionStorage); + let n = storage.length; + while (n--) { + const key = storage.key(n); + if (key && regex.test(key)) { + storage.removeItem(key); + } + } + }, + set_stringified(type: StorageType, key: string, value: object): void { + if (!browser) return; + if (is_empty_object(value)) return; + (type === "local" ? window.localStorage : window.sessionStorage).setItem(key, JSON.stringify(value)); + }, + get_stringified<T>(type: StorageType, key: string): T | any { + if (!browser) return; + return JSON.parse((type === "local" ? window.localStorage : window.sessionStorage).getItem(key) ?? "{}"); + } +}
\ No newline at end of file diff --git a/code/app/src/utilities/testing-helpers.ts b/code/app/src/utilities/testing-helpers.ts new file mode 100644 index 0000000..f21412e --- /dev/null +++ b/code/app/src/utilities/testing-helpers.ts @@ -0,0 +1,7 @@ +export function get_element_by_pw_key(key: string): HTMLElement | null { + return document.querySelector("[pw-key='" + key + "']"); +} + +export function get_pw_key_selector(key: string): string { + return "[pw-key='" + key + "']"; +}
\ No newline at end of file diff --git a/code/app/src/utilities/validators.ts b/code/app/src/utilities/validators.ts new file mode 100644 index 0000000..b69470e --- /dev/null +++ b/code/app/src/utilities/validators.ts @@ -0,0 +1,34 @@ +export const EMAIL_REGEX = new RegExp(/^([a-z0-9]+(?:([._\-])[a-z0-9]+)*@(?:[a-z0-9]+(?:(-)[a-z0-9]+)?\.)+[a-z0-9](?:[a-z0-9]*[a-z0-9])?)$/i); +export const URL_REGEX = new RegExp(/^(http:\/\/www\.|https:\/\/www\.|http:\/\/|https:\/\/)?[a-z0-9]+([\-.][a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?$/gm); +export const GUID_REGEX = new RegExp(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i); +export const NORWEGIAN_PHONE_NUMBER_REGEX = new RegExp(/(0047|\+47|47)?\d{8,12}/); + +export function is_email(value: string): boolean { + return EMAIL_REGEX.test(String(value).toLowerCase()); +} + +export function is_url(value: string): boolean { + return URL_REGEX.test(String(value).toLowerCase()); +} + +export function is_norwegian_phone_number(value: string): boolean { + if (value.length < 8 || value.length > 12) { + return false; + } + return NORWEGIAN_PHONE_NUMBER_REGEX.test(String(value)); +} + +export function is_guid(value: string): boolean { + if (!value) { + return false; + } + if (value[0] === "{") { + value = value.substring(1, value.length - 1); + } + return GUID_REGEX.test(value); +} + +export function is_empty_object(obj: object): boolean { + if (!obj) return true; + return obj !== void 0 && Object.keys(obj).length > 0; +}
\ No newline at end of file diff --git a/code/app/static/favicon.ico b/code/app/static/favicon.ico Binary files differnew file mode 100644 index 0000000..6848441 --- /dev/null +++ b/code/app/static/favicon.ico diff --git a/code/app/static/version.txt b/code/app/static/version.txt new file mode 100644 index 0000000..626799f --- /dev/null +++ b/code/app/static/version.txt @@ -0,0 +1 @@ +v1 diff --git a/code/app/svelte.config.js b/code/app/svelte.config.js new file mode 100644 index 0000000..2b4277a --- /dev/null +++ b/code/app/svelte.config.js @@ -0,0 +1,27 @@ +import adapter from "@sveltejs/adapter-node"; +import preprocess from "svelte-preprocess"; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + preprocess: [ + preprocess({ + postcss: true, + }), + ], + kit: { + adapter: adapter(), + alias: { + "$actions": "./src/actions", + "$routes": "./src/routes", + "$models": "./src/models", + "$api": "./src/api", + "$components": "./src/components", + "$utilities": "./src/utilities", + "$i18n": "./src/i18n", + "$services": "./src/services", + "$configuration": "./src/configuration", + } + }, +}; + +export default config; diff --git a/code/app/tailwind.config.cjs b/code/app/tailwind.config.cjs new file mode 100644 index 0000000..3727a79 --- /dev/null +++ b/code/app/tailwind.config.cjs @@ -0,0 +1,141 @@ +const defaultColors = require("tailwindcss/colors"); + +const refactoringUiPalette4 = { + "blue": { + "50": "#DCEEFB", + "100": "#B6E0FE", + "200": "#84C5F4", + "300": "#62B0E8", + "400": "#4098D7", + "500": "#2680C2", + "600": "#186FAF", + "700": "#0F609B", + "800": "#0A558C", + "900": "#003E6B", + }, + "red": { + "50": "#FFEEEE", + "100": "#FACDCD", + "200": "#F29B9B", + "300": "#E66A6A", + "400": "#D64545", + "500": "#BA2525", + "600": "#A61B1B", + "700": "#911111", + "800": "#780A0A", + "900": "#610404", + }, + "yellow": { + "50": "#FFFAEB", + "100": "#FCEFC7", + "200": "#F8E3A3", + "300": "#F9DA8B", + "400": "#F7D070", + "500": "#E9B949", + "600": "#C99A2E", + "700": "#A27C1A", + "800": "#7C5E10", + "900": "#513C06", + }, + "purple": { + "50": "#EAE2F8", + "100": "#CFBCF2", + "200": "#A081D9", + "300": "#8662C7", + "400": "#724BB7", + "500": "#653CAD", + "600": "#51279B", + "700": "#421987", + "800": "#34126F", + "900": "#240754", + }, + "blue-grey": { + "50": "#F0F4F8", + "100": "#D9E2EC", + "200": "#BCCCDC", + "300": "#9FB3C8", + "400": "#829AB1", + "500": "#627D98", + "600": "#486581", + "700": "#334E68", + "800": "#243B53", + "900": "#102A43", + }, + "teal": { + "50": "#EFFCF6", + "100": "#C6F7E2", + "200": "#8EEDC7", + "300": "#65D6AD", + "400": "#3EBD93", + "500": "#27AB83", + "600": "#199473", + "700": "#147D64", + "800": "#0C6B58", + "900": "#014D40", + } +} + +const config = { + content: ["./src/**/*.{html,js,svelte,ts}"], + theme: { + colors: { + "blue": refactoringUiPalette4.blue, + "red": refactoringUiPalette4.red, + "yellow": refactoringUiPalette4.yellow, + "purple": refactoringUiPalette4.purple, + "teal": refactoringUiPalette4.teal, + "green": refactoringUiPalette4.teal, + "gray": defaultColors.gray, + "white": defaultColors.white + } + }, + plugins: [ + require("@tailwindcss/forms"), + ], + safelist: [ + "bg-blue-50", + "bg-yellow-50", + "bg-red-50", + "bg-green-50", + "bg-blue-100", + "bg-yellow-100", + "bg-red-100", + "bg-green-100", + "text-blue-400", + "text-yellow-400", + "text-red-400", + "text-green-400", + "text-blue-800", + "text-yellow-800", + "text-red-800", + "text-green-800", + "text-blue-700", + "text-yellow-700", + "text-red-700", + "text-green-700", + "text-blue-500", + "text-yellow-500", + "text-red-500", + "text-green-500", + "border-teal-500", + "ring-teal-500", + "hover:text-blue-600", + "hover:text-yellow-600", + "hover:text-red-600", + "hover:text-green-600", + "hover:bg-blue-100", + "hover:bg-yellow-100", + "hover:bg-red-100", + "hover:bg-green-100", + "focus:ring-blue-600", + "focus:ring-yellow-600", + "focus:ring-red-600", + "focus:ring-green-600", + "focus:ring-offset-blue-50", + "focus:ring-offset-yellow-50", + "focus:ring-offset-red-50", + "focus:ring-offset-green-50", + ] +}; + +module.exports = config; diff --git a/code/app/tsconfig.json b/code/app/tsconfig.json new file mode 100644 index 0000000..b38b189 --- /dev/null +++ b/code/app/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "sourceMap": false, + } +}
\ No newline at end of file diff --git a/code/app/vite.config.js b/code/app/vite.config.js new file mode 100644 index 0000000..bc6ae94 --- /dev/null +++ b/code/app/vite.config.js @@ -0,0 +1,16 @@ +import { sveltekit } from "@sveltejs/kit/vite"; +import { SvelteKitPWA } from "@vite-pwa/sveltekit"; +/** @type {import('vite').UserConfig} */ +const config = { + plugins: [sveltekit(), SvelteKitPWA()], + build: { + target: "es2020", + }, + optimizeDeps: { + esbuildOptions: { + target: "es2020", + }, + }, +}; + +export default config; |
