Sean Blackburn пре 1 година
комит
d5f7a4efc0
74 измењених фајлова са 17834 додато и 0 уклоњено
  1. 24 0
      .eslintrc.cjs
  2. 270 0
      .gitignore
  3. 61 0
      .pre-commit-config.yaml
  4. 3 0
      .vscode/extensions.json
  5. 382 0
      LICENSE.md
  6. 1 0
      README.md
  7. 164 0
      cspell.config.yaml
  8. 17 0
      eslint.config.js
  9. 15 0
      index.html
  10. 6 0
      makefile
  11. 8079 0
      package-lock.json
  12. 41 0
      package.json
  13. 6 0
      postcss.config.cjs
  14. BIN
      public/favicon.png
  15. 31 0
      public/favicon.svg
  16. 2 0
      public/robots.txt
  17. 27 0
      src/App.vue
  18. 0 0
      src/assets/.gitkeep
  19. 152 0
      src/assets/coconuts.svg
  20. 31 0
      src/assets/logo.svg
  21. 17 0
      src/assets/searching-single.svg
  22. 212 0
      src/assets/searching.svg
  23. 315 0
      src/components/general/ClassFilter.vue
  24. 316 0
      src/components/general/ClassInfo.vue
  25. 178 0
      src/components/general/ClassRemovedRow.vue
  26. 189 0
      src/components/general/ClassRow.vue
  27. 67 0
      src/components/general/CurrentModuleExecutions.vue
  28. 150 0
      src/components/general/CurrentModuleExecutionsRow.vue
  29. 205 0
      src/components/general/DependencyTree.vue
  30. 280 0
      src/components/general/ModuleInfo.vue
  31. 177 0
      src/components/general/ModulesetManager.vue
  32. 145 0
      src/components/general/NumberedPagination.vue
  33. 68 0
      src/components/general/OrderingControl.vue
  34. 49 0
      src/components/general/PreviousModuleExecutions.vue
  35. 81 0
      src/components/general/PreviousModuleExecutionsRow.vue
  36. 42 0
      src/components/general/TeachingTypeIcon.vue
  37. 115 0
      src/components/general/WeekdayTimePicker.vue
  38. 167 0
      src/components/layout/HeaderBar.vue
  39. 354 0
      src/components/modals/AdditionalInfo.vue
  40. 80 0
      src/components/modals/BaseModal.vue
  41. 104 0
      src/components/modals/ClassModuleDetails.vue
  42. 116 0
      src/components/modals/ClassUpdateModal.vue
  43. 189 0
      src/components/modals/ModuleSearchModal.vue
  44. 138 0
      src/components/modals/ModulesetEdit.vue
  45. 67 0
      src/components/modals/OldSemesterReminderModal.vue
  46. 196 0
      src/components/modals/PersonalEventEdit.vue
  47. 75 0
      src/components/modals/RightDrawer.vue
  48. 269 0
      src/components/modals/SettingsModal.vue
  49. 94 0
      src/components/pages/404Page.vue
  50. 7 0
      src/components/pages/FullDepTreePage.vue
  51. 48 0
      src/components/pages/PlannerPage.vue
  52. 603 0
      src/components/views/CalendarView.vue
  53. 193 0
      src/components/views/ClassUpdateView.vue
  54. 278 0
      src/components/views/FullDependencyTree.vue
  55. 239 0
      src/components/views/ModuleSelector.vue
  56. 194 0
      src/helpers.ts
  57. 96 0
      src/main.ts
  58. 16 0
      src/router.ts
  59. 194 0
      src/stores/ClassVersion.ts
  60. 516 0
      src/stores/classes.ts
  61. 38 0
      src/stores/config.ts
  62. 30 0
      src/stores/lecturers.ts
  63. 168 0
      src/stores/modules.ts
  64. 620 0
      src/stores/planning.ts
  65. 85 0
      src/stores/state.ts
  66. 126 0
      src/stores/studenthub.ts
  67. 145 0
      src/stores/upgrade.ts
  68. 106 0
      src/style.css
  69. 302 0
      src/types.ts
  70. 1 0
      src/vite-env.d.ts
  71. 17 0
      tailwind.config.cjs
  72. 18 0
      tsconfig.json
  73. 9 0
      tsconfig.node.json
  74. 18 0
      vite.config.ts

+ 24 - 0
.eslintrc.cjs

@@ -0,0 +1,24 @@
+module.exports = {
+  env: {
+    node: true,
+  },
+  extends: [
+    "eslint:recommended",
+    "plugin:vue/vue3-recommended",
+    "prettier",
+    "plugin:@typescript-eslint/recommended",
+  ],
+  parser: "vue-eslint-parser",
+  parserOptions: {
+    parser: "@typescript-eslint/parser",
+    sourceType: "module",
+  },
+  rules: {
+    "@typescript-eslint/no-this-alias": [
+      "error",
+      {
+        allowedNames: ["vm"], // Allow `const vm= this`; `[]` by default
+      },
+    ],
+  },
+};

+ 270 - 0
.gitignore

@@ -0,0 +1,270 @@
+public/data/*
+!public/data/.gitkeep
+
+node_modules/
+
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+
+### Linux ###
+*~
+
+# temporary files which can be created if a process still has a handle open of a deleted file
+.fuse_hidden*
+
+# KDE directory preferences
+.directory
+
+# Linux trash folder which might appear on any partition or disk
+.Trash-*
+
+# .nfs files are created when an open file is removed but is still being accessed
+.nfs*
+
+### macOS ###
+# General
+.DS_Store
+.AppleDouble
+.LSOverride
+
+# Icon must end with two \r
+Icon
+
+
+# Thumbnails
+._*
+
+# Files that might appear in the root of a volume
+.DocumentRevisions-V100
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+.com.apple.timemachine.donotpresent
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
+
+### macOS Patch ###
+# iCloud generated files
+*.icloud
+
+### Python ###
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+#  Usually these files are written by a python script from a template
+#  before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+cover/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+.pybuilder/
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+#   For a library or package, you might want to ignore these files since the code is
+#   intended to run in multiple environments; otherwise, check them in:
+# .python-version
+
+# pipenv
+#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+#   However, in case of collaboration, if having platform-specific dependencies or dependencies
+#   having no cross-platform support, pipenv may install dependencies that don't work, or not
+#   install all needed dependencies.
+#Pipfile.lock
+
+# poetry
+#   Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
+#   This is especially recommended for binary packages to ensure reproducibility, and is more
+#   commonly ignored for libraries.
+#   https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
+#poetry.lock
+
+# pdm
+#   Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
+#pdm.lock
+#   pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
+#   in version control.
+#   https://pdm.fming.dev/#use-with-ide
+.pdm.toml
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
+
+# PyCharm
+#  JetBrains specific template is maintained in a separate JetBrains.gitignore that can
+#  be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
+#  and can be added to the global gitignore or merged into this file.  For a more nuclear
+#  option (not recommended) you can uncomment the following to ignore the entire idea folder.
+#.idea/
+
+### Python Patch ###
+# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
+poetry.toml
+
+
+### Windows ###
+# Windows thumbnail cache files
+Thumbs.db
+Thumbs.db:encryptable
+ehthumbs.db
+ehthumbs_vista.db
+
+# Dump file
+*.stackdump
+
+# Folder config file
+[Dd]esktop.ini
+
+# Recycle Bin used on file shares
+$RECYCLE.BIN/
+
+# Windows Installer files
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# Windows shortcuts
+*.lnk

+ 61 - 0
.pre-commit-config.yaml

@@ -0,0 +1,61 @@
+default_language_version:
+  python: python3.11
+
+repos:
+  - repo: https://github.com/commitizen-tools/commitizen
+    # Please run `pre-commit install --hook-type commit-msg` to check commit messages
+    rev: v3.29.1
+    hooks:
+      - id: commitizen
+
+  - repo: https://github.com/pre-commit/pre-commit-hooks
+    rev: v4.6.0
+    hooks:
+      - id: check-yaml
+      - id: end-of-file-fixer
+      - id: trailing-whitespace
+      - id: check-case-conflict
+      - id: check-ast
+      - id: check-added-large-files
+        args: ["--maxkb=1000"]
+
+  - repo: https://github.com/streetsidesoftware/cspell-cli.git
+    rev: v8.13.3
+    hooks:
+      - id: cspell
+        name: cspell
+        additional_dependencies:
+          - "@cspell/dict-de-ch"
+          - "@cspell/dict-en-gb"
+        args:
+          - "lint"
+          - "--show-suggestions"
+          - "--no-must-find-files"
+          - "--locale"
+          - "en-GB,de-CH"
+
+  - repo: local
+    hooks:
+      - id: install-frontend
+        name: Install frontend
+        entry: npm install
+        language: system
+        always_run: false
+        pass_filenames: false
+        stages: [commit, merge-commit, push, manual]
+      - id: eslint
+        name: eslint
+        entry: npm run lint
+        language: system
+        always_run: false
+        pass_filenames: false
+        verbose: true
+        stages: [commit, merge-commit, push, manual]
+      - id: build-frontend
+        name: Build frontend
+        entry: npm run build
+        language: system
+        always_run: false
+        pass_filenames: false
+        verbose: true
+        stages: [commit, merge-commit, push, manual]

+ 3 - 0
.vscode/extensions.json

@@ -0,0 +1,3 @@
+{
+  "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
+}

+ 382 - 0
LICENSE.md

@@ -0,0 +1,382 @@
+# Mozilla Public License Version 2.0
+
+1. Definitions
+
+---
+
+1.1. "Contributor"
+means each individual or legal entity that creates, contributes to
+the creation of, or owns Covered Software.
+
+1.2. "Contributor Version"
+means the combination of the Contributions of others (if any) used
+by a Contributor and that particular Contributor's Contribution.
+
+1.3. "Contribution"
+means Covered Software of a particular Contributor.
+
+1.4. "Covered Software"
+means Source Code Form to which the initial Contributor has attached
+the notice in Exhibit A, the Executable Form of such Source Code
+Form, and Modifications of such Source Code Form, in each case
+including portions thereof.
+
+1.5. "Incompatible With Secondary Licenses"
+means
+
+    (a) that the initial Contributor has attached the notice described
+        in Exhibit B to the Covered Software; or
+
+    (b) that the Covered Software was made available under the terms of
+        version 1.1 or earlier of the License, but not also under the
+        terms of a Secondary License.
+
+1.6. "Executable Form"
+means any form of the work other than Source Code Form.
+
+1.7. "Larger Work"
+means a work that combines Covered Software with other material, in
+a separate file or files, that is not Covered Software.
+
+1.8. "License"
+means this document.
+
+1.9. "Licensable"
+means having the right to grant, to the maximum extent possible,
+whether at the time of the initial grant or subsequently, any and
+all of the rights conveyed by this License.
+
+1.10. "Modifications"
+means any of the following:
+
+    (a) any file in Source Code Form that results from an addition to,
+        deletion from, or modification of the contents of Covered
+        Software; or
+
+    (b) any new file in Source Code Form that contains any Covered
+        Software.
+
+1.11. "Patent Claims" of a Contributor
+means any patent claim(s), including without limitation, method,
+process, and apparatus claims, in any patent Licensable by such
+Contributor that would be infringed, but for the grant of the
+License, by the making, using, selling, offering for sale, having
+made, import, or transfer of either its Contributions or its
+Contributor Version.
+
+1.12. "Secondary License"
+means either the GNU General Public License, Version 2.0, the GNU
+Lesser General Public License, Version 2.1, the GNU Affero General
+Public License, Version 3.0, or any later versions of those
+licenses.
+
+1.13. "Source Code Form"
+means the form of the work preferred for making modifications.
+
+1.14. "You" (or "Your")
+means an individual or a legal entity exercising rights under this
+License. For legal entities, "You" includes any entity that
+controls, is controlled by, or is under common control with You. For
+purposes of this definition, "control" means (a) the power, direct
+or indirect, to cause the direction or management of such entity,
+whether by contract or otherwise, or (b) ownership of more than
+fifty percent (50%) of the outstanding shares or beneficial
+ownership of such entity.
+
+2. License Grants and Conditions
+
+---
+
+2.1. Grants
+
+Each Contributor hereby grants You a world-wide, royalty-free,
+non-exclusive license:
+
+(a) under intellectual property rights (other than patent or trademark)
+Licensable by such Contributor to use, reproduce, make available,
+modify, display, perform, distribute, and otherwise exploit its
+Contributions, either on an unmodified basis, with Modifications, or
+as part of a Larger Work; and
+
+(b) under Patent Claims of such Contributor to make, use, sell, offer
+for sale, have made, import, and otherwise transfer either its
+Contributions or its Contributor Version.
+
+2.2. Effective Date
+
+The licenses granted in Section 2.1 with respect to any Contribution
+become effective for each Contribution on the date the Contributor first
+distributes such Contribution.
+
+2.3. Limitations on Grant Scope
+
+The licenses granted in this Section 2 are the only rights granted under
+this License. No additional rights or licenses will be implied from the
+distribution or licensing of Covered Software under this License.
+Notwithstanding Section 2.1(b) above, no patent license is granted by a
+Contributor:
+
+(a) for any code that a Contributor has removed from Covered Software;
+or
+
+(b) for infringements caused by: (i) Your and any other third party's
+modifications of Covered Software, or (ii) the combination of its
+Contributions with other software (except as part of its Contributor
+Version); or
+
+(c) under Patent Claims infringed by Covered Software in the absence of
+its Contributions.
+
+This License does not grant any rights in the trademarks, service marks,
+or logos of any Contributor (except as may be necessary to comply with
+the notice requirements in Section 3.4).
+
+2.4. Subsequent Licenses
+
+No Contributor makes additional grants as a result of Your choice to
+distribute the Covered Software under a subsequent version of this
+License (see Section 10.2) or under the terms of a Secondary License (if
+permitted under the terms of Section 3.3).
+
+2.5. Representation
+
+Each Contributor represents that the Contributor believes its
+Contributions are its original creation(s) or it has sufficient rights
+to grant the rights to its Contributions conveyed by this License.
+
+2.6. Fair Use
+
+This License is not intended to limit any rights You have under
+applicable copyright doctrines of fair use, fair dealing, or other
+equivalents.
+
+2.7. Conditions
+
+Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
+in Section 2.1.
+
+3. Responsibilities
+
+---
+
+3.1. Distribution of Source Form
+
+All distribution of Covered Software in Source Code Form, including any
+Modifications that You create or to which You contribute, must be under
+the terms of this License. You must inform recipients that the Source
+Code Form of the Covered Software is governed by the terms of this
+License, and how they can obtain a copy of this License. You may not
+attempt to alter or restrict the recipients' rights in the Source Code
+Form.
+
+3.2. Distribution of Executable Form
+
+If You distribute Covered Software in Executable Form then:
+
+(a) such Covered Software must also be made available in Source Code
+Form, as described in Section 3.1, and You must inform recipients of
+the Executable Form how they can obtain a copy of such Source Code
+Form by reasonable means in a timely manner, at a charge no more
+than the cost of distribution to the recipient; and
+
+(b) You may distribute such Executable Form under the terms of this
+License, or sublicense it under different terms, provided that the
+license for the Executable Form does not attempt to limit or alter
+the recipients' rights in the Source Code Form under this License.
+
+3.3. Distribution of a Larger Work
+
+You may create and distribute a Larger Work under terms of Your choice,
+provided that You also comply with the requirements of this License for
+the Covered Software. If the Larger Work is a combination of Covered
+Software with a work governed by one or more Secondary Licenses, and the
+Covered Software is not Incompatible With Secondary Licenses, this
+License permits You to additionally distribute such Covered Software
+under the terms of such Secondary License(s), so that the recipient of
+the Larger Work may, at their option, further distribute the Covered
+Software under the terms of either this License or such Secondary
+License(s).
+
+3.4. Notices
+
+You may not remove or alter the substance of any license notices
+(including copyright notices, patent notices, disclaimers of warranty,
+or limitations of liability) contained within the Source Code Form of
+the Covered Software, except that You may alter any license notices to
+the extent required to remedy known factual inaccuracies.
+
+3.5. Application of Additional Terms
+
+You may choose to offer, and to charge a fee for, warranty, support,
+indemnity or liability obligations to one or more recipients of Covered
+Software. However, You may do so only on Your own behalf, and not on
+behalf of any Contributor. You must make it absolutely clear that any
+such warranty, support, indemnity, or liability obligation is offered by
+You alone, and You hereby agree to indemnify every Contributor for any
+liability incurred by such Contributor as a result of warranty, support,
+indemnity or liability terms You offer. You may include additional
+disclaimers of warranty and limitations of liability specific to any
+jurisdiction.
+
+4. Inability to Comply Due to Statute or Regulation
+
+---
+
+If it is impossible for You to comply with any of the terms of this
+License with respect to some or all of the Covered Software due to
+statute, judicial order, or regulation then You must: (a) comply with
+the terms of this License to the maximum extent possible; and (b)
+describe the limitations and the code they affect. Such description must
+be placed in a text file included with all distributions of the Covered
+Software under this License. Except to the extent prohibited by statute
+or regulation, such description must be sufficiently detailed for a
+recipient of ordinary skill to be able to understand it.
+
+5. Termination
+
+---
+
+5.1. The rights granted under this License will terminate automatically
+if You fail to comply with any of its terms. However, if You become
+compliant, then the rights granted under this License from a particular
+Contributor are reinstated (a) provisionally, unless and until such
+Contributor explicitly and finally terminates Your grants, and (b) on an
+ongoing basis, if such Contributor fails to notify You of the
+non-compliance by some reasonable means prior to 60 days after You have
+come back into compliance. Moreover, Your grants from a particular
+Contributor are reinstated on an ongoing basis if such Contributor
+notifies You of the non-compliance by some reasonable means, this is the
+first time You have received notice of non-compliance with this License
+from such Contributor, and You become compliant prior to 30 days after
+Your receipt of the notice.
+
+5.2. If You initiate litigation against any entity by asserting a patent
+infringement claim (excluding declaratory judgment actions,
+counter-claims, and cross-claims) alleging that a Contributor Version
+directly or indirectly infringes any patent, then the rights granted to
+You by any and all Contributors for the Covered Software under Section
+2.1 of this License shall terminate.
+
+5.3. In the event of termination under Sections 5.1 or 5.2 above, all
+end user license agreements (excluding distributors and resellers) which
+have been validly granted by You or Your distributors under this License
+prior to termination shall survive termination.
+
+---
+
+-                                                                      *
+- 6. Disclaimer of Warranty \*
+- ------------------------- \*
+-                                                                      *
+- Covered Software is provided under this License on an "as is" \*
+- basis, without warranty of any kind, either expressed, implied, or \*
+- statutory, including, without limitation, warranties that the \*
+- Covered Software is free of defects, merchantable, fit for a \*
+- particular purpose or non-infringing. The entire risk as to the \*
+- quality and performance of the Covered Software is with You. \*
+- Should any Covered Software prove defective in any respect, You \*
+- (not any Contributor) assume the cost of any necessary servicing, \*
+- repair, or correction. This disclaimer of warranty constitutes an \*
+- essential part of this License. No use of any Covered Software is \*
+- authorized under this License except under this disclaimer. \*
+-                                                                      *
+
+---
+
+---
+
+-                                                                      *
+- 7. Limitation of Liability \*
+- -------------------------- \*
+-                                                                      *
+- Under no circumstances and under no legal theory, whether tort \*
+- (including negligence), contract, or otherwise, shall any \*
+- Contributor, or anyone who distributes Covered Software as \*
+- permitted above, be liable to You for any direct, indirect, \*
+- special, incidental, or consequential damages of any character \*
+- including, without limitation, damages for lost profits, loss of \*
+- goodwill, work stoppage, computer failure or malfunction, or any \*
+- and all other commercial damages or losses, even if such party \*
+- shall have been informed of the possibility of such damages. This \*
+- limitation of liability shall not apply to liability for death or \*
+- personal injury resulting from such party's negligence to the \*
+- extent applicable law prohibits such limitation. Some \*
+- jurisdictions do not allow the exclusion or limitation of \*
+- incidental or consequential damages, so this exclusion and \*
+- limitation may not apply to You. \*
+-                                                                      *
+
+---
+
+8. Litigation
+
+---
+
+Any litigation relating to this License may be brought only in the
+courts of a jurisdiction where the defendant maintains its principal
+place of business and such litigation shall be governed by laws of that
+jurisdiction, without reference to its conflict-of-law provisions.
+Nothing in this Section shall prevent a party's ability to bring
+cross-claims or counter-claims.
+
+9. Miscellaneous
+
+---
+
+This License represents the complete agreement concerning the subject
+matter hereof. If any provision of this License is held to be
+unenforceable, such provision shall be reformed only to the extent
+necessary to make it enforceable. Any law or regulation which provides
+that the language of a contract shall be construed against the drafter
+shall not be used to construe this License against a Contributor.
+
+10. Versions of the License
+
+---
+
+10.1. New Versions
+
+Mozilla Foundation is the license steward. Except as provided in Section
+10.3, no one other than the license steward has the right to modify or
+publish new versions of this License. Each version will be given a
+distinguishing version number.
+
+10.2. Effect of New Versions
+
+You may distribute the Covered Software under the terms of the version
+of the License under which You originally received the Covered Software,
+or under the terms of any subsequent version published by the license
+steward.
+
+10.3. Modified Versions
+
+If you create software not governed by this License, and you want to
+create a new license for such software, you may create and use a
+modified version of this License if you rename the license and remove
+any references to the name of the license steward (except to note that
+such modified license differs from this License).
+
+10.4. Distributing Source Code Form that is Incompatible With Secondary
+Licenses
+
+If You choose to distribute Source Code Form that is Incompatible With
+Secondary Licenses under the terms of this version of the License, the
+notice described in Exhibit B of this License must be attached.
+
+## Exhibit A - Source Code Form License Notice
+
+This Source Code Form is subject to the terms of the Mozilla Public
+License, v. 2.0. If a copy of the MPL was not distributed with this
+file, You can obtain one at <http://mozilla.org/MPL/2.0/>.
+
+If it is not possible or desirable to put the notice in a particular
+file, then You may include the notice in a location (such as a LICENSE
+file in a relevant directory) where a recipient would be likely to look
+for such a notice.
+
+You may add additional accurate notices of copyright ownership.
+
+## Exhibit B - "Incompatible With Secondary Licenses" Notice
+
+This Source Code Form is "Incompatible With Secondary Licenses", as
+defined by the Mozilla Public License, v. 2.0.

+ 1 - 0
README.md

@@ -0,0 +1 @@
+# Modulplaner

+ 164 - 0
cspell.config.yaml

@@ -0,0 +1,164 @@
+version: "0.2"
+ignorePaths:
+  - "*.svg"
+  - .pre-commit-config.yaml
+  - pyproject.toml
+  - .gitlab-ci.yml
+  - cspell.config.yaml
+  - CHANGELOG.md
+  - .mypy.ini
+  - .releaserc
+  - .gitignore
+  - package.json
+  - .vscode/settings.json
+  - .vscode/launch.json
+  - node_modules
+dictionaryDefinitions: []
+ignoreWords:
+  - .gitlab-ci.yml
+  - adresse1
+  - adresse2
+  - anlass
+  - anlassbezeichnung
+  - anlassleitungen
+  - anlassnummer
+  - anmeldungsDatum
+  - anrechnungEcts
+  - anzahl
+  - Ausb
+  - Ausb
+  - BBVT
+  - Bedevere
+  - bemerkung
+  - bemerkungen
+  - beruf
+  - bezeichnung
+  - Bezier
+  - bild
+  - blockclass
+  - BLOCKCLASS
+  - blockclasses
+  - BLOCKCLASSES
+  - blockcourse
+  - BLOCKCOURSE
+  - blöcke
+  - blockmodules
+  - Bridgekeeper
+  - brotli
+  - bverI
+  - bverl
+  - camelot
+  - changenotes
+  - clsss
+  - dataframe
+  - distributin'
+  - Dozentenkürzel
+  - dumpable
+  - ects
+  - edbs
+  - ELEKTRO
+  - english
+  - evenodd
+  - fa-gitlab
+  - fa-xmark
+  - faGitlab
+  - fillna
+  - firstname
+  - fitz
+  - flavor
+  - fontawesome
+  - fortawesome
+  - gidx
+  - GITLAB
+  - Heeeeeelp
+  - HISTORIZED
+  - iloc
+  - isna
+  - iterrows
+  - Jorj
+  - klassen
+  - korr
+  - kurse
+  - kürzel
+  - lect
+  - linecap
+  - linejoin
+  - lyin'
+  - maxsplit
+  - mediabox
+  - miterlimit
+  - mmdc
+  - modul
+  - modulegroups
+  - MODULEGROUPS
+  - moduleset
+  - nachname
+  - notiz
+  - notna
+  - nummer
+  - oidx
+  - oopi
+  - oopl
+  - parent_modulegroup_id
+  - pdfs
+  - pinia
+  - präsenz
+  - proi
+  - prov
+  - pydantic
+  - rect
+  - Rect
+  - rects
+  - resp
+  - ruleset
+  - Smits
+  - studenthub
+  - telefon
+  - TIMEBOX
+  - timecell
+  - titlebar
+  - Upgrader
+  - vers
+  - Vetur
+  - volar
+  - Volar
+  - vue-toastification
+  - wifi
+  - xlink
+  - Xmark
+dictionaries: []
+words:
+  - adxd
+  - annot
+  - bsys
+  - derotation
+  - dnet
+  - Dozierendenanpassungen
+  - dtds
+  - eana
+  - ecae
+  - ecpe
+  - einschr
+  - Elektro
+  - gles
+  - goek
+  - Ieng
+  - konami
+  - lalg
+  - matomo
+  - Matomo
+  - mgli
+  - nochecks
+  - Planänderungen
+  - Semesternote
+  - simag
+  - sprx
+  - stqm
+  - Studenthub
+  - Stundenplanänderung
+  - Stundenplanänderungen
+  - vars
+  - Whyyyyy
+import:
+  - "@cspell/dict-de-ch/cspell-ext.json"
+  - "@cspell/dict-en-gb/cspell-ext.json"

+ 17 - 0
eslint.config.js

@@ -0,0 +1,17 @@
+import js from "@eslint/js";
+import eslintPluginVue from "eslint-plugin-vue";
+import ts from "typescript-eslint";
+
+export default ts.config(
+  js.configs.recommended,
+  ...ts.configs.recommended,
+  ...eslintPluginVue.configs["flat/recommended"],
+  {
+    files: ["*.vue", "**/*.vue"],
+    languageOptions: {
+      parserOptions: {
+        parser: "@typescript-eslint/parser",
+      },
+    },
+  },
+);

+ 15 - 0
index.html

@@ -0,0 +1,15 @@
+<!doctype html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <meta name="description" content="Modulplaner für dein Studium." />
+    <link rel="shortcut icon" href="favicon.svg" type="image/svg" />
+    <link rel="shortcut icon" href="favicon.png" type="image/png" />
+    <title>Modulplaner</title>
+  </head>
+  <body class="bg-gray-100 dark:bg-gray-900 dark:text-white">
+    <div id="app"></div>
+    <script type="module" src="/src/main.ts"></script>
+  </body>
+</html>

+ 6 - 0
makefile

@@ -0,0 +1,6 @@
+run:
+	npm run dev;
+
+setup-pre-commit:
+	pre-commit install
+	pre-commit install --hook-type commit-msg

+ 8079 - 0
package-lock.json

@@ -0,0 +1,8079 @@
+{
+  "name": "modulplaner",
+  "version": "0.0.0",
+  "lockfileVersion": 2,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "modulplaner",
+      "version": "0.0.0",
+      "dependencies": {
+        "@fortawesome/fontawesome-svg-core": "^6.6.0",
+        "@fortawesome/free-brands-svg-icons": "^6.6.0",
+        "@fortawesome/free-solid-svg-icons": "^6.6.0",
+        "@fortawesome/vue-fontawesome": "^3.0.8",
+        "leaflet": "^1.9.4",
+        "mermaid": "^10.5.0",
+        "pinia": "^2.2.2",
+        "typescript-eslint": "^8.7.0",
+        "vue": "^3.5.10",
+        "vue-router": "^4.4.5",
+        "vue-toastification": "^2.0.0-rc.5",
+        "vue3-popper": "^1.5.0"
+      },
+      "devDependencies": {
+        "@typescript-eslint/eslint-plugin": "^8.7.0",
+        "@typescript-eslint/parser": "^8.7.0",
+        "@vitejs/plugin-vue": "^5.1.4",
+        "autoprefixer": "^10.4.20",
+        "eslint": "^9.11.1",
+        "eslint-config-prettier": "^9.1.0",
+        "eslint-plugin-vue": "^9.28.0",
+        "postcss": "^8.4.47",
+        "tailwindcss": "^3.4.13",
+        "typescript": "^5.6.2",
+        "vite": "^5.4.8",
+        "vue-tsc": "^2.1.6"
+      }
+    },
+    "node_modules/@aashutoshrathi/word-wrap": {
+      "version": "1.2.6",
+      "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz",
+      "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/@alloc/quick-lru": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
+      "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
+      "dev": true,
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/@babel/helper-string-parser": {
+      "version": "7.24.8",
+      "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz",
+      "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-validator-identifier": {
+      "version": "7.24.7",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz",
+      "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/parser": {
+      "version": "7.25.6",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.6.tgz",
+      "integrity": "sha512-trGdfBdbD0l1ZPmcJ83eNxB9rbEax4ALFTF7fN386TMYbeCQbyme5cOEXQhbGXKebwGaB/J52w1mrklMcbgy6Q==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/types": "^7.25.6"
+      },
+      "bin": {
+        "parser": "bin/babel-parser.js"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@babel/types": {
+      "version": "7.25.6",
+      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.6.tgz",
+      "integrity": "sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-string-parser": "^7.24.8",
+        "@babel/helper-validator-identifier": "^7.24.7",
+        "to-fast-properties": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@braintree/sanitize-url": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-6.0.2.tgz",
+      "integrity": "sha512-Tbsj02wXCbqGmzdnXNk0SOF19ChhRU70BsroIi4Pm6Ehp56in6vch94mfbdQ17DozxkL3BAVjbZ4Qc1a0HFRAg=="
+    },
+    "node_modules/@esbuild/aix-ppc64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
+      "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "aix"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/android-arm": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
+      "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/android-arm64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
+      "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/android-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
+      "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/darwin-arm64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
+      "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/darwin-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
+      "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/freebsd-arm64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
+      "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/freebsd-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
+      "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-arm": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
+      "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-arm64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
+      "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-ia32": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
+      "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-loong64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
+      "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-mips64el": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
+      "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
+      "cpu": [
+        "mips64el"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-ppc64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
+      "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-riscv64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
+      "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-s390x": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
+      "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
+      "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/netbsd-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
+      "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/openbsd-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
+      "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/sunos-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
+      "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "sunos"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/win32-arm64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
+      "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/win32-ia32": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
+      "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/win32-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
+      "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@eslint-community/eslint-utils": {
+      "version": "4.4.0",
+      "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
+      "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==",
+      "dependencies": {
+        "eslint-visitor-keys": "^3.3.0"
+      },
+      "engines": {
+        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+      },
+      "peerDependencies": {
+        "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+      }
+    },
+    "node_modules/@eslint-community/regexpp": {
+      "version": "4.11.1",
+      "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.1.tgz",
+      "integrity": "sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==",
+      "license": "MIT",
+      "engines": {
+        "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+      }
+    },
+    "node_modules/@eslint/config-array": {
+      "version": "0.18.0",
+      "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.18.0.tgz",
+      "integrity": "sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@eslint/object-schema": "^2.1.4",
+        "debug": "^4.3.1",
+        "minimatch": "^3.1.2"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      }
+    },
+    "node_modules/@eslint/core": {
+      "version": "0.6.0",
+      "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.6.0.tgz",
+      "integrity": "sha512-8I2Q8ykA4J0x0o7cg67FPVnehcqWTBehu/lmY+bolPFHGjh49YzGBMXTvpqVgEbBdvNCSxj6iFgiIyHzf03lzg==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      }
+    },
+    "node_modules/@eslint/eslintrc": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz",
+      "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==",
+      "license": "MIT",
+      "dependencies": {
+        "ajv": "^6.12.4",
+        "debug": "^4.3.2",
+        "espree": "^10.0.1",
+        "globals": "^14.0.0",
+        "ignore": "^5.2.0",
+        "import-fresh": "^3.2.1",
+        "js-yaml": "^4.1.0",
+        "minimatch": "^3.1.2",
+        "strip-json-comments": "^3.1.1"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
+    "node_modules/@eslint/eslintrc/node_modules/eslint-visitor-keys": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.1.0.tgz",
+      "integrity": "sha512-Q7lok0mqMUSf5a/AdAZkA5a/gHcO6snwQClVNNvFKCAVlxXucdU8pKydU5ZVZjBx5xr37vGbFFWtLQYreLzrZg==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
+    "node_modules/@eslint/eslintrc/node_modules/espree": {
+      "version": "10.2.0",
+      "resolved": "https://registry.npmjs.org/espree/-/espree-10.2.0.tgz",
+      "integrity": "sha512-upbkBJbckcCNBDBDXEbuhjbP68n+scUd3k/U2EkyM9nw+I/jPiL4cLF/Al06CF96wRltFda16sxDFrxsI1v0/g==",
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "acorn": "^8.12.0",
+        "acorn-jsx": "^5.3.2",
+        "eslint-visitor-keys": "^4.1.0"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
+    "node_modules/@eslint/eslintrc/node_modules/globals": {
+      "version": "14.0.0",
+      "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
+      "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/@eslint/js": {
+      "version": "9.11.1",
+      "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.11.1.tgz",
+      "integrity": "sha512-/qu+TWz8WwPWc7/HcIJKi+c+MOm46GdVaSlTTQcaqaL53+GsoA6MxWp5PtTx48qbSP7ylM1Kn7nhvkugfJvRSA==",
+      "license": "MIT",
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      }
+    },
+    "node_modules/@eslint/object-schema": {
+      "version": "2.1.4",
+      "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz",
+      "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      }
+    },
+    "node_modules/@eslint/plugin-kit": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.0.tgz",
+      "integrity": "sha512-vH9PiIMMwvhCx31Af3HiGzsVNULDbyVkHXwlemn/B0TFj/00ho3y55efXrUZTfQipxoHC5u4xq6zblww1zm1Ig==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "levn": "^0.4.1"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      }
+    },
+    "node_modules/@fortawesome/fontawesome-common-types": {
+      "version": "6.6.0",
+      "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.6.0.tgz",
+      "integrity": "sha512-xyX0X9mc0kyz9plIyryrRbl7ngsA9jz77mCZJsUkLl+ZKs0KWObgaEBoSgQiYWAsSmjz/yjl0F++Got0Mdp4Rw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/@fortawesome/fontawesome-svg-core": {
+      "version": "6.6.0",
+      "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.6.0.tgz",
+      "integrity": "sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg==",
+      "license": "MIT",
+      "dependencies": {
+        "@fortawesome/fontawesome-common-types": "6.6.0"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/@fortawesome/free-brands-svg-icons": {
+      "version": "6.6.0",
+      "resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.6.0.tgz",
+      "integrity": "sha512-1MPD8lMNW/earme4OQi1IFHtmHUwAKgghXlNwWi9GO7QkTfD+IIaYpIai4m2YJEzqfEji3jFHX1DZI5pbY/biQ==",
+      "license": "(CC-BY-4.0 AND MIT)",
+      "dependencies": {
+        "@fortawesome/fontawesome-common-types": "6.6.0"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/@fortawesome/free-solid-svg-icons": {
+      "version": "6.6.0",
+      "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.6.0.tgz",
+      "integrity": "sha512-IYv/2skhEDFc2WGUcqvFJkeK39Q+HyPf5GHUrT/l2pKbtgEIv1al1TKd6qStR5OIwQdN1GZP54ci3y4mroJWjA==",
+      "license": "(CC-BY-4.0 AND MIT)",
+      "dependencies": {
+        "@fortawesome/fontawesome-common-types": "6.6.0"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/@fortawesome/vue-fontawesome": {
+      "version": "3.0.8",
+      "resolved": "https://registry.npmjs.org/@fortawesome/vue-fontawesome/-/vue-fontawesome-3.0.8.tgz",
+      "integrity": "sha512-yyHHAj4G8pQIDfaIsMvQpwKMboIZtcHTUvPqXjOHyldh1O1vZfH4W03VDPv5RvI9P6DLTzJQlmVgj9wCf7c2Fw==",
+      "license": "MIT",
+      "peerDependencies": {
+        "@fortawesome/fontawesome-svg-core": "~1 || ~6",
+        "vue": ">= 3.0.0 < 4"
+      }
+    },
+    "node_modules/@humanwhocodes/module-importer": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+      "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+      "engines": {
+        "node": ">=12.22"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/nzakas"
+      }
+    },
+    "node_modules/@humanwhocodes/retry": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.0.tgz",
+      "integrity": "sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=18.18"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/nzakas"
+      }
+    },
+    "node_modules/@jridgewell/gen-mapping": {
+      "version": "0.3.3",
+      "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz",
+      "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==",
+      "dev": true,
+      "dependencies": {
+        "@jridgewell/set-array": "^1.0.1",
+        "@jridgewell/sourcemap-codec": "^1.4.10",
+        "@jridgewell/trace-mapping": "^0.3.9"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@jridgewell/resolve-uri": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz",
+      "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==",
+      "dev": true,
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@jridgewell/set-array": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz",
+      "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==",
+      "dev": true,
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@jridgewell/sourcemap-codec": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
+      "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
+      "license": "MIT"
+    },
+    "node_modules/@jridgewell/trace-mapping": {
+      "version": "0.3.19",
+      "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz",
+      "integrity": "sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==",
+      "dev": true,
+      "dependencies": {
+        "@jridgewell/resolve-uri": "^3.1.0",
+        "@jridgewell/sourcemap-codec": "^1.4.14"
+      }
+    },
+    "node_modules/@nodelib/fs.scandir": {
+      "version": "2.1.5",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+      "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+      "dependencies": {
+        "@nodelib/fs.stat": "2.0.5",
+        "run-parallel": "^1.1.9"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/@nodelib/fs.stat": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+      "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/@nodelib/fs.walk": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+      "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+      "dependencies": {
+        "@nodelib/fs.scandir": "2.1.5",
+        "fastq": "^1.6.0"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/@popperjs/core": {
+      "version": "2.11.6",
+      "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz",
+      "integrity": "sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==",
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/popperjs"
+      }
+    },
+    "node_modules/@rollup/rollup-android-arm-eabi": {
+      "version": "4.22.5",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.22.5.tgz",
+      "integrity": "sha512-SU5cvamg0Eyu/F+kLeMXS7GoahL+OoizlclVFX3l5Ql6yNlywJJ0OuqTzUx0v+aHhPHEB/56CT06GQrRrGNYww==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ]
+    },
+    "node_modules/@rollup/rollup-android-arm64": {
+      "version": "4.22.5",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.22.5.tgz",
+      "integrity": "sha512-S4pit5BP6E5R5C8S6tgU/drvgjtYW76FBuG6+ibG3tMvlD1h9LHVF9KmlmaUBQ8Obou7hEyS+0w+IR/VtxwNMQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ]
+    },
+    "node_modules/@rollup/rollup-darwin-arm64": {
+      "version": "4.22.5",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.22.5.tgz",
+      "integrity": "sha512-250ZGg4ipTL0TGvLlfACkIxS9+KLtIbn7BCZjsZj88zSg2Lvu3Xdw6dhAhfe/FjjXPVNCtcSp+WZjVsD3a/Zlw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ]
+    },
+    "node_modules/@rollup/rollup-darwin-x64": {
+      "version": "4.22.5",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.22.5.tgz",
+      "integrity": "sha512-D8brJEFg5D+QxFcW6jYANu+Rr9SlKtTenmsX5hOSzNYVrK5oLAEMTUgKWYJP+wdKyCdeSwnapLsn+OVRFycuQg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+      "version": "4.22.5",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.22.5.tgz",
+      "integrity": "sha512-PNqXYmdNFyWNg0ma5LdY8wP+eQfdvyaBAojAXgO7/gs0Q/6TQJVXAXe8gwW9URjbS0YAammur0fynYGiWsKlXw==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+      "version": "4.22.5",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.22.5.tgz",
+      "integrity": "sha512-kSSCZOKz3HqlrEuwKd9TYv7vxPYD77vHSUvM2y0YaTGnFc8AdI5TTQRrM1yIp3tXCKrSL9A7JLoILjtad5t8pQ==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm64-gnu": {
+      "version": "4.22.5",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.22.5.tgz",
+      "integrity": "sha512-oTXQeJHRbOnwRnRffb6bmqmUugz0glXaPyspp4gbQOPVApdpRrY/j7KP3lr7M8kTfQTyrBUzFjj5EuHAhqH4/w==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm64-musl": {
+      "version": "4.22.5",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.22.5.tgz",
+      "integrity": "sha512-qnOTIIs6tIGFKCHdhYitgC2XQ2X25InIbZFor5wh+mALH84qnFHvc+vmWUpyX97B0hNvwNUL4B+MB8vJvH65Fw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
+      "version": "4.22.5",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.22.5.tgz",
+      "integrity": "sha512-TMYu+DUdNlgBXING13rHSfUc3Ky5nLPbWs4bFnT+R6Vu3OvXkTkixvvBKk8uO4MT5Ab6lC3U7x8S8El2q5o56w==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+      "version": "4.22.5",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.22.5.tgz",
+      "integrity": "sha512-PTQq1Kz22ZRvuhr3uURH+U/Q/a0pbxJoICGSprNLAoBEkyD3Sh9qP5I0Asn0y0wejXQBbsVMRZRxlbGFD9OK4A==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-s390x-gnu": {
+      "version": "4.22.5",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.22.5.tgz",
+      "integrity": "sha512-bR5nCojtpuMss6TDEmf/jnBnzlo+6n1UhgwqUvRoe4VIotC7FG1IKkyJbwsT7JDsF2jxR+NTnuOwiGv0hLyDoQ==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-x64-gnu": {
+      "version": "4.22.5",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.22.5.tgz",
+      "integrity": "sha512-N0jPPhHjGShcB9/XXZQWuWBKZQnC1F36Ce3sDqWpujsGjDz/CQtOL9LgTrJ+rJC8MJeesMWrMWVLKKNR/tMOCA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-x64-musl": {
+      "version": "4.22.5",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.22.5.tgz",
+      "integrity": "sha512-uBa2e28ohzNNwjr6Uxm4XyaA1M/8aTgfF2T7UIlElLaeXkgpmIJ2EitVNQxjO9xLLLy60YqAgKn/AqSpCUkE9g==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-arm64-msvc": {
+      "version": "4.22.5",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.22.5.tgz",
+      "integrity": "sha512-RXT8S1HP8AFN/Kr3tg4fuYrNxZ/pZf1HemC5Tsddc6HzgGnJm0+Lh5rAHJkDuW3StI0ynNXukidROMXYl6ew8w==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-ia32-msvc": {
+      "version": "4.22.5",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.22.5.tgz",
+      "integrity": "sha512-ElTYOh50InL8kzyUD6XsnPit7jYCKrphmddKAe1/Ytt74apOxDq5YEcbsiKs0fR3vff3jEneMM+3I7jbqaMyBg==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-x64-msvc": {
+      "version": "4.22.5",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.22.5.tgz",
+      "integrity": "sha512-+lvL/4mQxSV8MukpkKyyvfwhH266COcWlXE/1qxwN08ajovta3459zrjLghYMgDerlzNwLAcFpvU+WWE5y6nAQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@types/d3-scale": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.3.tgz",
+      "integrity": "sha512-PATBiMCpvHJSMtZAMEhc2WyL+hnzarKzI6wAHYjhsonjWJYGq5BXTzQjv4l8m2jO183/4wZ90rKvSeT7o72xNQ==",
+      "dependencies": {
+        "@types/d3-time": "*"
+      }
+    },
+    "node_modules/@types/d3-scale-chromatic": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz",
+      "integrity": "sha512-dsoJGEIShosKVRBZB0Vo3C8nqSDqVGujJU6tPznsBJxNJNwMF8utmS83nvCBKQYPpjCzaaHcrf66iTRpZosLPw=="
+    },
+    "node_modules/@types/d3-time": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.0.tgz",
+      "integrity": "sha512-sZLCdHvBUcNby1cB6Fd3ZBrABbjz3v1Vm90nysCQ6Vt7vd6e/h9Lt7SiJUoEX0l4Dzc7P5llKyhqSi1ycSf1Hg=="
+    },
+    "node_modules/@types/debug": {
+      "version": "4.1.8",
+      "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.8.tgz",
+      "integrity": "sha512-/vPO1EPOs306Cvhwv7KfVfYvOJqA/S/AXjaHQiJboCZzcNDb+TIJFN9/2C9DZ//ijSKWioNyUxD792QmDJ+HKQ==",
+      "dependencies": {
+        "@types/ms": "*"
+      }
+    },
+    "node_modules/@types/estree": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
+      "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
+      "license": "MIT"
+    },
+    "node_modules/@types/json-schema": {
+      "version": "7.0.15",
+      "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+      "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+      "license": "MIT"
+    },
+    "node_modules/@types/mdast": {
+      "version": "3.0.12",
+      "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.12.tgz",
+      "integrity": "sha512-DT+iNIRNX884cx0/Q1ja7NyUPpZuv0KPyL5rGNxm1WC1OtHstl7n4Jb7nk+xacNShQMbczJjt8uFzznpp6kYBg==",
+      "dependencies": {
+        "@types/unist": "^2"
+      }
+    },
+    "node_modules/@types/ms": {
+      "version": "0.7.31",
+      "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz",
+      "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA=="
+    },
+    "node_modules/@types/unist": {
+      "version": "2.0.7",
+      "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.7.tgz",
+      "integrity": "sha512-cputDpIbFgLUaGQn6Vqg3/YsJwxUwHLO13v3i5ouxT4lat0khip9AEWxtERujXV9wxIB1EyF97BSJFt6vpdI8g=="
+    },
+    "node_modules/@typescript-eslint/eslint-plugin": {
+      "version": "8.7.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.7.0.tgz",
+      "integrity": "sha512-RIHOoznhA3CCfSTFiB6kBGLQtB/sox+pJ6jeFu6FxJvqL8qRxq/FfGO/UhsGgQM9oGdXkV4xUgli+dt26biB6A==",
+      "license": "MIT",
+      "dependencies": {
+        "@eslint-community/regexpp": "^4.10.0",
+        "@typescript-eslint/scope-manager": "8.7.0",
+        "@typescript-eslint/type-utils": "8.7.0",
+        "@typescript-eslint/utils": "8.7.0",
+        "@typescript-eslint/visitor-keys": "8.7.0",
+        "graphemer": "^1.4.0",
+        "ignore": "^5.3.1",
+        "natural-compare": "^1.4.0",
+        "ts-api-utils": "^1.3.0"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependencies": {
+        "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0",
+        "eslint": "^8.57.0 || ^9.0.0"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@typescript-eslint/parser": {
+      "version": "8.7.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.7.0.tgz",
+      "integrity": "sha512-lN0btVpj2unxHlNYLI//BQ7nzbMJYBVQX5+pbNXvGYazdlgYonMn4AhhHifQ+J4fGRYA/m1DjaQjx+fDetqBOQ==",
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "@typescript-eslint/scope-manager": "8.7.0",
+        "@typescript-eslint/types": "8.7.0",
+        "@typescript-eslint/typescript-estree": "8.7.0",
+        "@typescript-eslint/visitor-keys": "8.7.0",
+        "debug": "^4.3.4"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependencies": {
+        "eslint": "^8.57.0 || ^9.0.0"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@typescript-eslint/scope-manager": {
+      "version": "8.7.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.7.0.tgz",
+      "integrity": "sha512-87rC0k3ZlDOuz82zzXRtQ7Akv3GKhHs0ti4YcbAJtaomllXoSO8hi7Ix3ccEvCd824dy9aIX+j3d2UMAfCtVpg==",
+      "license": "MIT",
+      "dependencies": {
+        "@typescript-eslint/types": "8.7.0",
+        "@typescript-eslint/visitor-keys": "8.7.0"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      }
+    },
+    "node_modules/@typescript-eslint/type-utils": {
+      "version": "8.7.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.7.0.tgz",
+      "integrity": "sha512-tl0N0Mj3hMSkEYhLkjREp54OSb/FI6qyCzfiiclvJvOqre6hsZTGSnHtmFLDU8TIM62G7ygEa1bI08lcuRwEnQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@typescript-eslint/typescript-estree": "8.7.0",
+        "@typescript-eslint/utils": "8.7.0",
+        "debug": "^4.3.4",
+        "ts-api-utils": "^1.3.0"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@typescript-eslint/types": {
+      "version": "8.7.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.7.0.tgz",
+      "integrity": "sha512-LLt4BLHFwSfASHSF2K29SZ+ZCsbQOM+LuarPjRUuHm+Qd09hSe3GCeaQbcCr+Mik+0QFRmep/FyZBO6fJ64U3w==",
+      "license": "MIT",
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      }
+    },
+    "node_modules/@typescript-eslint/typescript-estree": {
+      "version": "8.7.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.7.0.tgz",
+      "integrity": "sha512-MC8nmcGHsmfAKxwnluTQpNqceniT8SteVwd2voYlmiSWGOtjvGXdPl17dYu2797GVscK30Z04WRM28CrKS9WOg==",
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "@typescript-eslint/types": "8.7.0",
+        "@typescript-eslint/visitor-keys": "8.7.0",
+        "debug": "^4.3.4",
+        "fast-glob": "^3.3.2",
+        "is-glob": "^4.0.3",
+        "minimatch": "^9.0.4",
+        "semver": "^7.6.0",
+        "ts-api-utils": "^1.3.0"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+      "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+      "license": "MIT",
+      "dependencies": {
+        "balanced-match": "^1.0.0"
+      }
+    },
+    "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
+      "version": "9.0.5",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+      "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+      "license": "ISC",
+      "dependencies": {
+        "brace-expansion": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/@typescript-eslint/utils": {
+      "version": "8.7.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.7.0.tgz",
+      "integrity": "sha512-ZbdUdwsl2X/s3CiyAu3gOlfQzpbuG3nTWKPoIvAu1pu5r8viiJvv2NPN2AqArL35NCYtw/lrPPfM4gxrMLNLPw==",
+      "license": "MIT",
+      "dependencies": {
+        "@eslint-community/eslint-utils": "^4.4.0",
+        "@typescript-eslint/scope-manager": "8.7.0",
+        "@typescript-eslint/types": "8.7.0",
+        "@typescript-eslint/typescript-estree": "8.7.0"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependencies": {
+        "eslint": "^8.57.0 || ^9.0.0"
+      }
+    },
+    "node_modules/@typescript-eslint/visitor-keys": {
+      "version": "8.7.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.7.0.tgz",
+      "integrity": "sha512-b1tx0orFCCh/THWPQa2ZwWzvOeyzzp36vkJYOpVg0u8UVOIsfVrnuC9FqAw9gRKn+rG2VmWQ/zDJZzkxUnj/XQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@typescript-eslint/types": "8.7.0",
+        "eslint-visitor-keys": "^3.4.3"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      }
+    },
+    "node_modules/@vitejs/plugin-vue": {
+      "version": "5.1.4",
+      "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.1.4.tgz",
+      "integrity": "sha512-N2XSI2n3sQqp5w7Y/AN/L2XDjBIRGqXko+eDp42sydYSBeJuSm5a1sLf8zakmo8u7tA8NmBgoDLA1HeOESjp9A==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "^18.0.0 || >=20.0.0"
+      },
+      "peerDependencies": {
+        "vite": "^5.0.0",
+        "vue": "^3.2.25"
+      }
+    },
+    "node_modules/@volar/language-core": {
+      "version": "2.4.5",
+      "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.5.tgz",
+      "integrity": "sha512-F4tA0DCO5Q1F5mScHmca0umsi2ufKULAnMOVBfMsZdT4myhVl4WdKRwCaKcfOkIEuyrAVvtq1ESBdZ+rSyLVww==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@volar/source-map": "2.4.5"
+      }
+    },
+    "node_modules/@volar/source-map": {
+      "version": "2.4.5",
+      "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.5.tgz",
+      "integrity": "sha512-varwD7RaKE2J/Z+Zu6j3mNNJbNT394qIxXwdvz/4ao/vxOfyClZpSDtLKkwWmecinkOVos5+PWkWraelfMLfpw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@volar/typescript": {
+      "version": "2.4.5",
+      "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.5.tgz",
+      "integrity": "sha512-mcT1mHvLljAEtHviVcBuOyAwwMKz1ibXTi5uYtP/pf4XxoAzpdkQ+Br2IC0NPCvLCbjPZmbf3I0udndkfB1CDg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@volar/language-core": "2.4.5",
+        "path-browserify": "^1.0.1",
+        "vscode-uri": "^3.0.8"
+      }
+    },
+    "node_modules/@vue/compiler-core": {
+      "version": "3.5.10",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.10.tgz",
+      "integrity": "sha512-iXWlk+Cg/ag7gLvY0SfVucU8Kh2CjysYZjhhP70w9qI4MvSox4frrP+vDGvtQuzIcgD8+sxM6lZvCtdxGunTAA==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.25.3",
+        "@vue/shared": "3.5.10",
+        "entities": "^4.5.0",
+        "estree-walker": "^2.0.2",
+        "source-map-js": "^1.2.0"
+      }
+    },
+    "node_modules/@vue/compiler-dom": {
+      "version": "3.5.10",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.10.tgz",
+      "integrity": "sha512-DyxHC6qPcktwYGKOIy3XqnHRrrXyWR2u91AjP+nLkADko380srsC2DC3s7Y1Rk6YfOlxOlvEQKa9XXmLI+W4ZA==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/compiler-core": "3.5.10",
+        "@vue/shared": "3.5.10"
+      }
+    },
+    "node_modules/@vue/compiler-sfc": {
+      "version": "3.5.10",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.10.tgz",
+      "integrity": "sha512-to8E1BgpakV7224ZCm8gz1ZRSyjNCAWEplwFMWKlzCdP9DkMKhRRwt0WkCjY7jkzi/Vz3xgbpeig5Pnbly4Tow==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.25.3",
+        "@vue/compiler-core": "3.5.10",
+        "@vue/compiler-dom": "3.5.10",
+        "@vue/compiler-ssr": "3.5.10",
+        "@vue/shared": "3.5.10",
+        "estree-walker": "^2.0.2",
+        "magic-string": "^0.30.11",
+        "postcss": "^8.4.47",
+        "source-map-js": "^1.2.0"
+      }
+    },
+    "node_modules/@vue/compiler-ssr": {
+      "version": "3.5.10",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.10.tgz",
+      "integrity": "sha512-hxP4Y3KImqdtyUKXDRSxKSRkSm1H9fCvhojEYrnaoWhE4w/y8vwWhnosJoPPe2AXm5sU7CSbYYAgkt2ZPhDz+A==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/compiler-dom": "3.5.10",
+        "@vue/shared": "3.5.10"
+      }
+    },
+    "node_modules/@vue/compiler-vue2": {
+      "version": "2.7.16",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz",
+      "integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "de-indent": "^1.0.2",
+        "he": "^1.2.0"
+      }
+    },
+    "node_modules/@vue/devtools-api": {
+      "version": "6.6.4",
+      "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
+      "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
+      "license": "MIT"
+    },
+    "node_modules/@vue/language-core": {
+      "version": "2.1.6",
+      "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.1.6.tgz",
+      "integrity": "sha512-MW569cSky9R/ooKMh6xa2g1D0AtRKbL56k83dzus/bx//RDJk24RHWkMzbAlXjMdDNyxAaagKPRquBIxkxlCkg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@volar/language-core": "~2.4.1",
+        "@vue/compiler-dom": "^3.4.0",
+        "@vue/compiler-vue2": "^2.7.16",
+        "@vue/shared": "^3.4.0",
+        "computeds": "^0.0.1",
+        "minimatch": "^9.0.3",
+        "muggle-string": "^0.4.1",
+        "path-browserify": "^1.0.1"
+      },
+      "peerDependencies": {
+        "typescript": "*"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@vue/language-core/node_modules/brace-expansion": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+      "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "balanced-match": "^1.0.0"
+      }
+    },
+    "node_modules/@vue/language-core/node_modules/minimatch": {
+      "version": "9.0.5",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+      "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "brace-expansion": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/@vue/reactivity": {
+      "version": "3.5.10",
+      "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.10.tgz",
+      "integrity": "sha512-kW08v06F6xPSHhid9DJ9YjOGmwNDOsJJQk0ax21wKaUYzzuJGEuoKNU2Ujux8FLMrP7CFJJKsHhXN9l2WOVi2g==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/shared": "3.5.10"
+      }
+    },
+    "node_modules/@vue/runtime-core": {
+      "version": "3.5.10",
+      "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.10.tgz",
+      "integrity": "sha512-9Q86I5Qq3swSkFfzrZ+iqEy7Vla325M7S7xc1NwKnRm/qoi1Dauz0rT6mTMmscqx4qz0EDJ1wjB+A36k7rl8mA==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/reactivity": "3.5.10",
+        "@vue/shared": "3.5.10"
+      }
+    },
+    "node_modules/@vue/runtime-dom": {
+      "version": "3.5.10",
+      "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.10.tgz",
+      "integrity": "sha512-t3x7ht5qF8ZRi1H4fZqFzyY2j+GTMTDxRheT+i8M9Ph0oepUxoadmbwlFwMoW7RYCpNQLpP2Yx3feKs+fyBdpA==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/reactivity": "3.5.10",
+        "@vue/runtime-core": "3.5.10",
+        "@vue/shared": "3.5.10",
+        "csstype": "^3.1.3"
+      }
+    },
+    "node_modules/@vue/server-renderer": {
+      "version": "3.5.10",
+      "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.10.tgz",
+      "integrity": "sha512-IVE97tt2kGKwHNq9yVO0xdh1IvYfZCShvDSy46JIh5OQxP1/EXSpoDqetVmyIzL7CYOWnnmMkVqd7YK2QSWkdw==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/compiler-ssr": "3.5.10",
+        "@vue/shared": "3.5.10"
+      },
+      "peerDependencies": {
+        "vue": "3.5.10"
+      }
+    },
+    "node_modules/@vue/shared": {
+      "version": "3.5.10",
+      "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.10.tgz",
+      "integrity": "sha512-VkkBhU97Ki+XJ0xvl4C9YJsIZ2uIlQ7HqPpZOS3m9VCvmROPaChZU6DexdMJqvz9tbgG+4EtFVrSuailUq5KGQ==",
+      "license": "MIT"
+    },
+    "node_modules/acorn": {
+      "version": "8.12.1",
+      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz",
+      "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==",
+      "license": "MIT",
+      "bin": {
+        "acorn": "bin/acorn"
+      },
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/acorn-jsx": {
+      "version": "5.3.2",
+      "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+      "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+      "peerDependencies": {
+        "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+      }
+    },
+    "node_modules/ajv": {
+      "version": "6.12.6",
+      "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+      "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+      "license": "MIT",
+      "dependencies": {
+        "fast-deep-equal": "^3.1.1",
+        "fast-json-stable-stringify": "^2.0.0",
+        "json-schema-traverse": "^0.4.1",
+        "uri-js": "^4.2.2"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/epoberezkin"
+      }
+    },
+    "node_modules/ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/ansi-styles": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+      "dependencies": {
+        "color-convert": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/any-promise": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
+      "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
+      "dev": true
+    },
+    "node_modules/anymatch": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+      "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+      "dev": true,
+      "dependencies": {
+        "normalize-path": "^3.0.0",
+        "picomatch": "^2.0.4"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/arg": {
+      "version": "5.0.2",
+      "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
+      "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
+      "dev": true
+    },
+    "node_modules/argparse": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+      "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+      "license": "Python-2.0"
+    },
+    "node_modules/autoprefixer": {
+      "version": "10.4.20",
+      "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz",
+      "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/autoprefixer"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "browserslist": "^4.23.3",
+        "caniuse-lite": "^1.0.30001646",
+        "fraction.js": "^4.3.7",
+        "normalize-range": "^0.1.2",
+        "picocolors": "^1.0.1",
+        "postcss-value-parser": "^4.2.0"
+      },
+      "bin": {
+        "autoprefixer": "bin/autoprefixer"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >=14"
+      },
+      "peerDependencies": {
+        "postcss": "^8.1.0"
+      }
+    },
+    "node_modules/balanced-match": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+      "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
+    },
+    "node_modules/binary-extensions": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
+      "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/boolbase": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
+      "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
+      "dev": true
+    },
+    "node_modules/brace-expansion": {
+      "version": "1.1.11",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+      "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+      "dependencies": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "node_modules/braces": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+      "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+      "license": "MIT",
+      "dependencies": {
+        "fill-range": "^7.1.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/browserslist": {
+      "version": "4.24.0",
+      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.0.tgz",
+      "integrity": "sha512-Rmb62sR1Zpjql25eSanFGEhAxcFwfA1K0GuQcLoaJBAcENegrQut3hYdhXFF1obQfiDyqIW/cLM5HSJ/9k884A==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/browserslist"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/browserslist"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "caniuse-lite": "^1.0.30001663",
+        "electron-to-chromium": "^1.5.28",
+        "node-releases": "^2.0.18",
+        "update-browserslist-db": "^1.1.0"
+      },
+      "bin": {
+        "browserslist": "cli.js"
+      },
+      "engines": {
+        "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+      }
+    },
+    "node_modules/callsites": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+      "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/camelcase-css": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
+      "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
+      "dev": true,
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/caniuse-lite": {
+      "version": "1.0.30001664",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001664.tgz",
+      "integrity": "sha512-AmE7k4dXiNKQipgn7a2xg558IRqPN3jMQY/rOsbxDhrd0tyChwbITBfiwtnqz8bi2M5mIWbxAYBvk7W7QBUS2g==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/browserslist"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "CC-BY-4.0"
+    },
+    "node_modules/chalk": {
+      "version": "4.1.2",
+      "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+      "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+      "dependencies": {
+        "ansi-styles": "^4.1.0",
+        "supports-color": "^7.1.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/chalk?sponsor=1"
+      }
+    },
+    "node_modules/character-entities": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz",
+      "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==",
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/wooorm"
+      }
+    },
+    "node_modules/chokidar": {
+      "version": "3.5.3",
+      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
+      "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://paulmillr.com/funding/"
+        }
+      ],
+      "dependencies": {
+        "anymatch": "~3.1.2",
+        "braces": "~3.0.2",
+        "glob-parent": "~5.1.2",
+        "is-binary-path": "~2.1.0",
+        "is-glob": "~4.0.1",
+        "normalize-path": "~3.0.0",
+        "readdirp": "~3.6.0"
+      },
+      "engines": {
+        "node": ">= 8.10.0"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.2"
+      }
+    },
+    "node_modules/chokidar/node_modules/glob-parent": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+      "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+      "dev": true,
+      "dependencies": {
+        "is-glob": "^4.0.1"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/color-convert": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+      "dependencies": {
+        "color-name": "~1.1.4"
+      },
+      "engines": {
+        "node": ">=7.0.0"
+      }
+    },
+    "node_modules/color-name": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
+    },
+    "node_modules/commander": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
+      "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/computeds": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/computeds/-/computeds-0.0.1.tgz",
+      "integrity": "sha512-7CEBgcMjVmitjYo5q8JTJVra6X5mQ20uTThdK+0kR7UEaDrAWEQcRiBtWJzga4eRpP6afNwwLsX2SET2JhVB1Q==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/concat-map": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+      "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
+    },
+    "node_modules/cose-base": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz",
+      "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==",
+      "dependencies": {
+        "layout-base": "^1.0.0"
+      }
+    },
+    "node_modules/cross-spawn": {
+      "version": "7.0.3",
+      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
+      "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+      "dependencies": {
+        "path-key": "^3.1.0",
+        "shebang-command": "^2.0.0",
+        "which": "^2.0.1"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/cssesc": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
+      "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
+      "dev": true,
+      "bin": {
+        "cssesc": "bin/cssesc"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/csstype": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
+      "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
+      "license": "MIT"
+    },
+    "node_modules/cytoscape": {
+      "version": "3.26.0",
+      "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.26.0.tgz",
+      "integrity": "sha512-IV+crL+KBcrCnVVUCZW+zRRRFUZQcrtdOPXki+o4CFUWLdAEYvuZLcBSJC9EBK++suamERKzeY7roq2hdovV3w==",
+      "dependencies": {
+        "heap": "^0.2.6",
+        "lodash": "^4.17.21"
+      },
+      "engines": {
+        "node": ">=0.10"
+      }
+    },
+    "node_modules/cytoscape-cose-bilkent": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz",
+      "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==",
+      "dependencies": {
+        "cose-base": "^1.0.0"
+      },
+      "peerDependencies": {
+        "cytoscape": "^3.2.0"
+      }
+    },
+    "node_modules/cytoscape-fcose": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz",
+      "integrity": "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==",
+      "dependencies": {
+        "cose-base": "^2.2.0"
+      },
+      "peerDependencies": {
+        "cytoscape": "^3.2.0"
+      }
+    },
+    "node_modules/cytoscape-fcose/node_modules/cose-base": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-2.2.0.tgz",
+      "integrity": "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==",
+      "dependencies": {
+        "layout-base": "^2.0.0"
+      }
+    },
+    "node_modules/cytoscape-fcose/node_modules/layout-base": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-2.0.1.tgz",
+      "integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg=="
+    },
+    "node_modules/d3": {
+      "version": "7.8.5",
+      "resolved": "https://registry.npmjs.org/d3/-/d3-7.8.5.tgz",
+      "integrity": "sha512-JgoahDG51ncUfJu6wX/1vWQEqOflgXyl4MaHqlcSruTez7yhaRKR9i8VjjcQGeS2en/jnFivXuaIMnseMMt0XA==",
+      "dependencies": {
+        "d3-array": "3",
+        "d3-axis": "3",
+        "d3-brush": "3",
+        "d3-chord": "3",
+        "d3-color": "3",
+        "d3-contour": "4",
+        "d3-delaunay": "6",
+        "d3-dispatch": "3",
+        "d3-drag": "3",
+        "d3-dsv": "3",
+        "d3-ease": "3",
+        "d3-fetch": "3",
+        "d3-force": "3",
+        "d3-format": "3",
+        "d3-geo": "3",
+        "d3-hierarchy": "3",
+        "d3-interpolate": "3",
+        "d3-path": "3",
+        "d3-polygon": "3",
+        "d3-quadtree": "3",
+        "d3-random": "3",
+        "d3-scale": "4",
+        "d3-scale-chromatic": "3",
+        "d3-selection": "3",
+        "d3-shape": "3",
+        "d3-time": "3",
+        "d3-time-format": "4",
+        "d3-timer": "3",
+        "d3-transition": "3",
+        "d3-zoom": "3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-array": {
+      "version": "3.2.4",
+      "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
+      "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
+      "dependencies": {
+        "internmap": "1 - 2"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-axis": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz",
+      "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-brush": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz",
+      "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==",
+      "dependencies": {
+        "d3-dispatch": "1 - 3",
+        "d3-drag": "2 - 3",
+        "d3-interpolate": "1 - 3",
+        "d3-selection": "3",
+        "d3-transition": "3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-chord": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz",
+      "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==",
+      "dependencies": {
+        "d3-path": "1 - 3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-color": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
+      "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-contour": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz",
+      "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==",
+      "dependencies": {
+        "d3-array": "^3.2.0"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-delaunay": {
+      "version": "6.0.4",
+      "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz",
+      "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==",
+      "dependencies": {
+        "delaunator": "5"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-dispatch": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
+      "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-drag": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz",
+      "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
+      "dependencies": {
+        "d3-dispatch": "1 - 3",
+        "d3-selection": "3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-dsv": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz",
+      "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==",
+      "dependencies": {
+        "commander": "7",
+        "iconv-lite": "0.6",
+        "rw": "1"
+      },
+      "bin": {
+        "csv2json": "bin/dsv2json.js",
+        "csv2tsv": "bin/dsv2dsv.js",
+        "dsv2dsv": "bin/dsv2dsv.js",
+        "dsv2json": "bin/dsv2json.js",
+        "json2csv": "bin/json2dsv.js",
+        "json2dsv": "bin/json2dsv.js",
+        "json2tsv": "bin/json2dsv.js",
+        "tsv2csv": "bin/dsv2dsv.js",
+        "tsv2json": "bin/dsv2json.js"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-ease": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
+      "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-fetch": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz",
+      "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==",
+      "dependencies": {
+        "d3-dsv": "1 - 3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-force": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz",
+      "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==",
+      "dependencies": {
+        "d3-dispatch": "1 - 3",
+        "d3-quadtree": "1 - 3",
+        "d3-timer": "1 - 3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-format": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz",
+      "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-geo": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.0.tgz",
+      "integrity": "sha512-JEo5HxXDdDYXCaWdwLRt79y7giK8SbhZJbFWXqbRTolCHFI5jRqteLzCsq51NKbUoX0PjBVSohxrx+NoOUujYA==",
+      "dependencies": {
+        "d3-array": "2.5.0 - 3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-hierarchy": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz",
+      "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-interpolate": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
+      "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
+      "dependencies": {
+        "d3-color": "1 - 3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-path": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
+      "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-polygon": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz",
+      "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-quadtree": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz",
+      "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-random": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz",
+      "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-sankey": {
+      "version": "0.12.3",
+      "resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz",
+      "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==",
+      "dependencies": {
+        "d3-array": "1 - 2",
+        "d3-shape": "^1.2.0"
+      }
+    },
+    "node_modules/d3-sankey/node_modules/d3-array": {
+      "version": "2.12.1",
+      "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz",
+      "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==",
+      "dependencies": {
+        "internmap": "^1.0.0"
+      }
+    },
+    "node_modules/d3-sankey/node_modules/d3-path": {
+      "version": "1.0.9",
+      "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz",
+      "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="
+    },
+    "node_modules/d3-sankey/node_modules/d3-shape": {
+      "version": "1.3.7",
+      "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz",
+      "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==",
+      "dependencies": {
+        "d3-path": "1"
+      }
+    },
+    "node_modules/d3-sankey/node_modules/internmap": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz",
+      "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw=="
+    },
+    "node_modules/d3-scale": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
+      "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
+      "dependencies": {
+        "d3-array": "2.10.0 - 3",
+        "d3-format": "1 - 3",
+        "d3-interpolate": "1.2.0 - 3",
+        "d3-time": "2.1.1 - 3",
+        "d3-time-format": "2 - 4"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-scale-chromatic": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz",
+      "integrity": "sha512-Lx9thtxAKrO2Pq6OO2Ua474opeziKr279P/TKZsMAhYyNDD3EnCffdbgeSYN5O7m2ByQsxtuP2CSDczNUIZ22g==",
+      "dependencies": {
+        "d3-color": "1 - 3",
+        "d3-interpolate": "1 - 3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-selection": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
+      "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-shape": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
+      "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
+      "dependencies": {
+        "d3-path": "^3.1.0"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-time": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
+      "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
+      "dependencies": {
+        "d3-array": "2 - 3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-time-format": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
+      "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
+      "dependencies": {
+        "d3-time": "1 - 3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-timer": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
+      "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-transition": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz",
+      "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
+      "dependencies": {
+        "d3-color": "1 - 3",
+        "d3-dispatch": "1 - 3",
+        "d3-ease": "1 - 3",
+        "d3-interpolate": "1 - 3",
+        "d3-timer": "1 - 3"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "peerDependencies": {
+        "d3-selection": "2 - 3"
+      }
+    },
+    "node_modules/d3-zoom": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz",
+      "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
+      "dependencies": {
+        "d3-dispatch": "1 - 3",
+        "d3-drag": "2 - 3",
+        "d3-interpolate": "1 - 3",
+        "d3-selection": "2 - 3",
+        "d3-transition": "2 - 3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/dagre-d3-es": {
+      "version": "7.0.10",
+      "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.10.tgz",
+      "integrity": "sha512-qTCQmEhcynucuaZgY5/+ti3X/rnszKZhEQH/ZdWdtP1tA/y3VoHJzcVrO9pjjJCNpigfscAtoUB5ONcd2wNn0A==",
+      "dependencies": {
+        "d3": "^7.8.2",
+        "lodash-es": "^4.17.21"
+      }
+    },
+    "node_modules/dayjs": {
+      "version": "1.11.9",
+      "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.9.tgz",
+      "integrity": "sha512-QvzAURSbQ0pKdIye2txOzNaHmxtUBXerpY0FJsFXUMKbIZeFm5ht1LS/jFsrncjnmtv8HsG0W2g6c0zUjZWmpA=="
+    },
+    "node_modules/de-indent": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
+      "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/debounce": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz",
+      "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug=="
+    },
+    "node_modules/debug": {
+      "version": "4.3.4",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+      "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+      "dependencies": {
+        "ms": "2.1.2"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/decode-named-character-reference": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz",
+      "integrity": "sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==",
+      "dependencies": {
+        "character-entities": "^2.0.0"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/wooorm"
+      }
+    },
+    "node_modules/deep-is": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+      "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="
+    },
+    "node_modules/delaunator": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.0.tgz",
+      "integrity": "sha512-AyLvtyJdbv/U1GkiS6gUUzclRoAY4Gs75qkMygJJhU75LW4DNuSF2RMzpxs9jw9Oz1BobHjTdkG3zdP55VxAqw==",
+      "dependencies": {
+        "robust-predicates": "^3.0.0"
+      }
+    },
+    "node_modules/dequal": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
+      "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/didyoumean": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
+      "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
+      "dev": true
+    },
+    "node_modules/diff": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz",
+      "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==",
+      "engines": {
+        "node": ">=0.3.1"
+      }
+    },
+    "node_modules/dlv": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
+      "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
+      "dev": true
+    },
+    "node_modules/dompurify": {
+      "version": "3.1.7",
+      "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.7.tgz",
+      "integrity": "sha512-VaTstWtsneJY8xzy7DekmYWEOZcmzIe3Qb3zPd4STve1OBTa+e+WmS1ITQec1fZYXI3HCsOZZiSMpG6oxoWMWQ==",
+      "license": "(MPL-2.0 OR Apache-2.0)"
+    },
+    "node_modules/electron-to-chromium": {
+      "version": "1.5.29",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.29.tgz",
+      "integrity": "sha512-PF8n2AlIhCKXQ+gTpiJi0VhcHDb69kYX4MtCiivctc2QD3XuNZ/XIOlbGzt7WAjjEev0TtaH6Cu3arZExm5DOw==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/elkjs": {
+      "version": "0.8.2",
+      "resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.8.2.tgz",
+      "integrity": "sha512-L6uRgvZTH+4OF5NE/MBbzQx/WYpru1xCBE9respNj6qznEewGUIfhzmm7horWWxbNO2M0WckQypGctR8lH79xQ=="
+    },
+    "node_modules/entities": {
+      "version": "4.5.0",
+      "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
+      "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=0.12"
+      },
+      "funding": {
+        "url": "https://github.com/fb55/entities?sponsor=1"
+      }
+    },
+    "node_modules/esbuild": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
+      "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "bin": {
+        "esbuild": "bin/esbuild"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "optionalDependencies": {
+        "@esbuild/aix-ppc64": "0.21.5",
+        "@esbuild/android-arm": "0.21.5",
+        "@esbuild/android-arm64": "0.21.5",
+        "@esbuild/android-x64": "0.21.5",
+        "@esbuild/darwin-arm64": "0.21.5",
+        "@esbuild/darwin-x64": "0.21.5",
+        "@esbuild/freebsd-arm64": "0.21.5",
+        "@esbuild/freebsd-x64": "0.21.5",
+        "@esbuild/linux-arm": "0.21.5",
+        "@esbuild/linux-arm64": "0.21.5",
+        "@esbuild/linux-ia32": "0.21.5",
+        "@esbuild/linux-loong64": "0.21.5",
+        "@esbuild/linux-mips64el": "0.21.5",
+        "@esbuild/linux-ppc64": "0.21.5",
+        "@esbuild/linux-riscv64": "0.21.5",
+        "@esbuild/linux-s390x": "0.21.5",
+        "@esbuild/linux-x64": "0.21.5",
+        "@esbuild/netbsd-x64": "0.21.5",
+        "@esbuild/openbsd-x64": "0.21.5",
+        "@esbuild/sunos-x64": "0.21.5",
+        "@esbuild/win32-arm64": "0.21.5",
+        "@esbuild/win32-ia32": "0.21.5",
+        "@esbuild/win32-x64": "0.21.5"
+      }
+    },
+    "node_modules/escalade": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+      "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/escape-string-regexp": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+      "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/eslint": {
+      "version": "9.11.1",
+      "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.11.1.tgz",
+      "integrity": "sha512-MobhYKIoAO1s1e4VUrgx1l1Sk2JBR/Gqjjgw8+mfgoLE2xwsHur4gdfTxyTgShrhvdVFTaJSgMiQBl1jv/AWxg==",
+      "license": "MIT",
+      "dependencies": {
+        "@eslint-community/eslint-utils": "^4.2.0",
+        "@eslint-community/regexpp": "^4.11.0",
+        "@eslint/config-array": "^0.18.0",
+        "@eslint/core": "^0.6.0",
+        "@eslint/eslintrc": "^3.1.0",
+        "@eslint/js": "9.11.1",
+        "@eslint/plugin-kit": "^0.2.0",
+        "@humanwhocodes/module-importer": "^1.0.1",
+        "@humanwhocodes/retry": "^0.3.0",
+        "@nodelib/fs.walk": "^1.2.8",
+        "@types/estree": "^1.0.6",
+        "@types/json-schema": "^7.0.15",
+        "ajv": "^6.12.4",
+        "chalk": "^4.0.0",
+        "cross-spawn": "^7.0.2",
+        "debug": "^4.3.2",
+        "escape-string-regexp": "^4.0.0",
+        "eslint-scope": "^8.0.2",
+        "eslint-visitor-keys": "^4.0.0",
+        "espree": "^10.1.0",
+        "esquery": "^1.5.0",
+        "esutils": "^2.0.2",
+        "fast-deep-equal": "^3.1.3",
+        "file-entry-cache": "^8.0.0",
+        "find-up": "^5.0.0",
+        "glob-parent": "^6.0.2",
+        "ignore": "^5.2.0",
+        "imurmurhash": "^0.1.4",
+        "is-glob": "^4.0.0",
+        "is-path-inside": "^3.0.3",
+        "json-stable-stringify-without-jsonify": "^1.0.1",
+        "lodash.merge": "^4.6.2",
+        "minimatch": "^3.1.2",
+        "natural-compare": "^1.4.0",
+        "optionator": "^0.9.3",
+        "strip-ansi": "^6.0.1",
+        "text-table": "^0.2.0"
+      },
+      "bin": {
+        "eslint": "bin/eslint.js"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "url": "https://eslint.org/donate"
+      },
+      "peerDependencies": {
+        "jiti": "*"
+      },
+      "peerDependenciesMeta": {
+        "jiti": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/eslint-config-prettier": {
+      "version": "9.1.0",
+      "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz",
+      "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==",
+      "dev": true,
+      "bin": {
+        "eslint-config-prettier": "bin/cli.js"
+      },
+      "peerDependencies": {
+        "eslint": ">=7.0.0"
+      }
+    },
+    "node_modules/eslint-plugin-vue": {
+      "version": "9.28.0",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.28.0.tgz",
+      "integrity": "sha512-ShrihdjIhOTxs+MfWun6oJWuk+g/LAhN+CiuOl/jjkG3l0F2AuK5NMTaWqyvBgkFtpYmyks6P4603mLmhNJW8g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@eslint-community/eslint-utils": "^4.4.0",
+        "globals": "^13.24.0",
+        "natural-compare": "^1.4.0",
+        "nth-check": "^2.1.1",
+        "postcss-selector-parser": "^6.0.15",
+        "semver": "^7.6.3",
+        "vue-eslint-parser": "^9.4.3",
+        "xml-name-validator": "^4.0.0"
+      },
+      "engines": {
+        "node": "^14.17.0 || >=16.0.0"
+      },
+      "peerDependencies": {
+        "eslint": "^6.2.0 || ^7.0.0 || ^8.0.0 || ^9.0.0"
+      }
+    },
+    "node_modules/eslint-scope": {
+      "version": "7.2.2",
+      "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
+      "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==",
+      "dev": true,
+      "dependencies": {
+        "esrecurse": "^4.3.0",
+        "estraverse": "^5.2.0"
+      },
+      "engines": {
+        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
+    "node_modules/eslint-visitor-keys": {
+      "version": "3.4.3",
+      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+      "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+      "engines": {
+        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
+    "node_modules/eslint/node_modules/eslint-scope": {
+      "version": "8.1.0",
+      "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.1.0.tgz",
+      "integrity": "sha512-14dSvlhaVhKKsa9Fx1l8A17s7ah7Ef7wCakJ10LYk6+GYmP9yDti2oq2SEwcyndt6knfcZyhyxwY3i9yL78EQw==",
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "esrecurse": "^4.3.0",
+        "estraverse": "^5.2.0"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
+    "node_modules/eslint/node_modules/eslint-visitor-keys": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.1.0.tgz",
+      "integrity": "sha512-Q7lok0mqMUSf5a/AdAZkA5a/gHcO6snwQClVNNvFKCAVlxXucdU8pKydU5ZVZjBx5xr37vGbFFWtLQYreLzrZg==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
+    "node_modules/eslint/node_modules/espree": {
+      "version": "10.2.0",
+      "resolved": "https://registry.npmjs.org/espree/-/espree-10.2.0.tgz",
+      "integrity": "sha512-upbkBJbckcCNBDBDXEbuhjbP68n+scUd3k/U2EkyM9nw+I/jPiL4cLF/Al06CF96wRltFda16sxDFrxsI1v0/g==",
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "acorn": "^8.12.0",
+        "acorn-jsx": "^5.3.2",
+        "eslint-visitor-keys": "^4.1.0"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
+    "node_modules/espree": {
+      "version": "9.6.1",
+      "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
+      "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==",
+      "dev": true,
+      "dependencies": {
+        "acorn": "^8.9.0",
+        "acorn-jsx": "^5.3.2",
+        "eslint-visitor-keys": "^3.4.1"
+      },
+      "engines": {
+        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
+    "node_modules/esquery": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz",
+      "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==",
+      "dependencies": {
+        "estraverse": "^5.1.0"
+      },
+      "engines": {
+        "node": ">=0.10"
+      }
+    },
+    "node_modules/esrecurse": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+      "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+      "dependencies": {
+        "estraverse": "^5.2.0"
+      },
+      "engines": {
+        "node": ">=4.0"
+      }
+    },
+    "node_modules/estraverse": {
+      "version": "5.3.0",
+      "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+      "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+      "engines": {
+        "node": ">=4.0"
+      }
+    },
+    "node_modules/estree-walker": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
+      "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
+      "license": "MIT"
+    },
+    "node_modules/esutils": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+      "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/fast-deep-equal": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+      "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+      "license": "MIT"
+    },
+    "node_modules/fast-glob": {
+      "version": "3.3.2",
+      "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
+      "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==",
+      "dependencies": {
+        "@nodelib/fs.stat": "^2.0.2",
+        "@nodelib/fs.walk": "^1.2.3",
+        "glob-parent": "^5.1.2",
+        "merge2": "^1.3.0",
+        "micromatch": "^4.0.4"
+      },
+      "engines": {
+        "node": ">=8.6.0"
+      }
+    },
+    "node_modules/fast-glob/node_modules/glob-parent": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+      "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+      "dependencies": {
+        "is-glob": "^4.0.1"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/fast-json-stable-stringify": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+      "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+      "license": "MIT"
+    },
+    "node_modules/fast-levenshtein": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+      "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="
+    },
+    "node_modules/fastq": {
+      "version": "1.15.0",
+      "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz",
+      "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==",
+      "dependencies": {
+        "reusify": "^1.0.4"
+      }
+    },
+    "node_modules/file-entry-cache": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
+      "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
+      "license": "MIT",
+      "dependencies": {
+        "flat-cache": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=16.0.0"
+      }
+    },
+    "node_modules/fill-range": {
+      "version": "7.1.1",
+      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+      "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+      "license": "MIT",
+      "dependencies": {
+        "to-regex-range": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/find-up": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+      "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+      "dependencies": {
+        "locate-path": "^6.0.0",
+        "path-exists": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/flat-cache": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
+      "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
+      "license": "MIT",
+      "dependencies": {
+        "flatted": "^3.2.9",
+        "keyv": "^4.5.4"
+      },
+      "engines": {
+        "node": ">=16"
+      }
+    },
+    "node_modules/flatted": {
+      "version": "3.3.1",
+      "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz",
+      "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==",
+      "license": "ISC"
+    },
+    "node_modules/fraction.js": {
+      "version": "4.3.7",
+      "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
+      "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==",
+      "dev": true,
+      "engines": {
+        "node": "*"
+      },
+      "funding": {
+        "type": "patreon",
+        "url": "https://github.com/sponsors/rawify"
+      }
+    },
+    "node_modules/fs.realpath": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+      "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
+      "dev": true
+    },
+    "node_modules/fsevents": {
+      "version": "2.3.3",
+      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+      "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+      "dev": true,
+      "hasInstallScript": true,
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+      }
+    },
+    "node_modules/function-bind": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
+      "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
+      "dev": true
+    },
+    "node_modules/glob-parent": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+      "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+      "dependencies": {
+        "is-glob": "^4.0.3"
+      },
+      "engines": {
+        "node": ">=10.13.0"
+      }
+    },
+    "node_modules/globals": {
+      "version": "13.24.0",
+      "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz",
+      "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "type-fest": "^0.20.2"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/graphemer": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
+      "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="
+    },
+    "node_modules/has": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
+      "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
+      "dev": true,
+      "dependencies": {
+        "function-bind": "^1.1.1"
+      },
+      "engines": {
+        "node": ">= 0.4.0"
+      }
+    },
+    "node_modules/has-flag": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/he": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
+      "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "he": "bin/he"
+      }
+    },
+    "node_modules/heap": {
+      "version": "0.2.7",
+      "resolved": "https://registry.npmjs.org/heap/-/heap-0.2.7.tgz",
+      "integrity": "sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg=="
+    },
+    "node_modules/iconv-lite": {
+      "version": "0.6.3",
+      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+      "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+      "dependencies": {
+        "safer-buffer": ">= 2.1.2 < 3.0.0"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/ignore": {
+      "version": "5.3.2",
+      "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+      "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 4"
+      }
+    },
+    "node_modules/import-fresh": {
+      "version": "3.3.0",
+      "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
+      "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
+      "license": "MIT",
+      "dependencies": {
+        "parent-module": "^1.0.0",
+        "resolve-from": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/imurmurhash": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+      "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+      "engines": {
+        "node": ">=0.8.19"
+      }
+    },
+    "node_modules/inflight": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+      "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+      "dev": true,
+      "dependencies": {
+        "once": "^1.3.0",
+        "wrappy": "1"
+      }
+    },
+    "node_modules/inherits": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+      "dev": true
+    },
+    "node_modules/internmap": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
+      "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/is-binary-path": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+      "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+      "dev": true,
+      "dependencies": {
+        "binary-extensions": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/is-core-module": {
+      "version": "2.13.0",
+      "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz",
+      "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==",
+      "dev": true,
+      "dependencies": {
+        "has": "^1.0.3"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-extglob": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+      "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/is-glob": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+      "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+      "dependencies": {
+        "is-extglob": "^2.1.1"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/is-number": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+      "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.12.0"
+      }
+    },
+    "node_modules/is-path-inside": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
+      "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/isexe": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+      "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
+    },
+    "node_modules/jiti": {
+      "version": "1.21.6",
+      "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz",
+      "integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==",
+      "devOptional": true,
+      "license": "MIT",
+      "bin": {
+        "jiti": "bin/jiti.js"
+      }
+    },
+    "node_modules/js-yaml": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+      "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+      "license": "MIT",
+      "dependencies": {
+        "argparse": "^2.0.1"
+      },
+      "bin": {
+        "js-yaml": "bin/js-yaml.js"
+      }
+    },
+    "node_modules/json-buffer": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+      "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+      "license": "MIT"
+    },
+    "node_modules/json-schema-traverse": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+      "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+      "license": "MIT"
+    },
+    "node_modules/json-stable-stringify-without-jsonify": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+      "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="
+    },
+    "node_modules/keyv": {
+      "version": "4.5.4",
+      "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+      "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+      "license": "MIT",
+      "dependencies": {
+        "json-buffer": "3.0.1"
+      }
+    },
+    "node_modules/khroma": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.0.0.tgz",
+      "integrity": "sha512-2J8rDNlQWbtiNYThZRvmMv5yt44ZakX+Tz5ZIp/mN1pt4snn+m030Va5Z4v8xA0cQFDXBwO/8i42xL4QPsVk3g=="
+    },
+    "node_modules/kleur": {
+      "version": "4.1.5",
+      "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
+      "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/layout-base": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz",
+      "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg=="
+    },
+    "node_modules/leaflet": {
+      "version": "1.9.4",
+      "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
+      "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA=="
+    },
+    "node_modules/levn": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+      "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+      "dependencies": {
+        "prelude-ls": "^1.2.1",
+        "type-check": "~0.4.0"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/lilconfig": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz",
+      "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==",
+      "dev": true,
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/lines-and-columns": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+      "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
+      "dev": true
+    },
+    "node_modules/locate-path": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+      "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+      "dependencies": {
+        "p-locate": "^5.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/lodash": {
+      "version": "4.17.21",
+      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+      "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
+    },
+    "node_modules/lodash-es": {
+      "version": "4.17.21",
+      "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
+      "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="
+    },
+    "node_modules/lodash.merge": {
+      "version": "4.6.2",
+      "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+      "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="
+    },
+    "node_modules/magic-string": {
+      "version": "0.30.11",
+      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz",
+      "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==",
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/sourcemap-codec": "^1.5.0"
+      }
+    },
+    "node_modules/mdast-util-from-markdown": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.1.tgz",
+      "integrity": "sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww==",
+      "dependencies": {
+        "@types/mdast": "^3.0.0",
+        "@types/unist": "^2.0.0",
+        "decode-named-character-reference": "^1.0.0",
+        "mdast-util-to-string": "^3.1.0",
+        "micromark": "^3.0.0",
+        "micromark-util-decode-numeric-character-reference": "^1.0.0",
+        "micromark-util-decode-string": "^1.0.0",
+        "micromark-util-normalize-identifier": "^1.0.0",
+        "micromark-util-symbol": "^1.0.0",
+        "micromark-util-types": "^1.0.0",
+        "unist-util-stringify-position": "^3.0.0",
+        "uvu": "^0.5.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/unified"
+      }
+    },
+    "node_modules/mdast-util-to-string": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz",
+      "integrity": "sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==",
+      "dependencies": {
+        "@types/mdast": "^3.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/unified"
+      }
+    },
+    "node_modules/merge2": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+      "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/mermaid": {
+      "version": "10.5.0",
+      "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-10.5.0.tgz",
+      "integrity": "sha512-9l0o1uUod78D3/FVYPGSsgV+Z0tSnzLBDiC9rVzvelPxuO80HbN1oDr9ofpPETQy9XpypPQa26fr09VzEPfvWA==",
+      "dependencies": {
+        "@braintree/sanitize-url": "^6.0.1",
+        "@types/d3-scale": "^4.0.3",
+        "@types/d3-scale-chromatic": "^3.0.0",
+        "cytoscape": "^3.23.0",
+        "cytoscape-cose-bilkent": "^4.1.0",
+        "cytoscape-fcose": "^2.1.0",
+        "d3": "^7.4.0",
+        "d3-sankey": "^0.12.3",
+        "dagre-d3-es": "7.0.10",
+        "dayjs": "^1.11.7",
+        "dompurify": "^3.0.5",
+        "elkjs": "^0.8.2",
+        "khroma": "^2.0.0",
+        "lodash-es": "^4.17.21",
+        "mdast-util-from-markdown": "^1.3.0",
+        "non-layered-tidy-tree-layout": "^2.0.2",
+        "stylis": "^4.1.3",
+        "ts-dedent": "^2.2.0",
+        "uuid": "^9.0.0",
+        "web-worker": "^1.2.0"
+      }
+    },
+    "node_modules/micromark": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/micromark/-/micromark-3.2.0.tgz",
+      "integrity": "sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA==",
+      "funding": [
+        {
+          "type": "GitHub Sponsors",
+          "url": "https://github.com/sponsors/unifiedjs"
+        },
+        {
+          "type": "OpenCollective",
+          "url": "https://opencollective.com/unified"
+        }
+      ],
+      "dependencies": {
+        "@types/debug": "^4.0.0",
+        "debug": "^4.0.0",
+        "decode-named-character-reference": "^1.0.0",
+        "micromark-core-commonmark": "^1.0.1",
+        "micromark-factory-space": "^1.0.0",
+        "micromark-util-character": "^1.0.0",
+        "micromark-util-chunked": "^1.0.0",
+        "micromark-util-combine-extensions": "^1.0.0",
+        "micromark-util-decode-numeric-character-reference": "^1.0.0",
+        "micromark-util-encode": "^1.0.0",
+        "micromark-util-normalize-identifier": "^1.0.0",
+        "micromark-util-resolve-all": "^1.0.0",
+        "micromark-util-sanitize-uri": "^1.0.0",
+        "micromark-util-subtokenize": "^1.0.0",
+        "micromark-util-symbol": "^1.0.0",
+        "micromark-util-types": "^1.0.1",
+        "uvu": "^0.5.0"
+      }
+    },
+    "node_modules/micromark-core-commonmark": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-1.1.0.tgz",
+      "integrity": "sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw==",
+      "funding": [
+        {
+          "type": "GitHub Sponsors",
+          "url": "https://github.com/sponsors/unifiedjs"
+        },
+        {
+          "type": "OpenCollective",
+          "url": "https://opencollective.com/unified"
+        }
+      ],
+      "dependencies": {
+        "decode-named-character-reference": "^1.0.0",
+        "micromark-factory-destination": "^1.0.0",
+        "micromark-factory-label": "^1.0.0",
+        "micromark-factory-space": "^1.0.0",
+        "micromark-factory-title": "^1.0.0",
+        "micromark-factory-whitespace": "^1.0.0",
+        "micromark-util-character": "^1.0.0",
+        "micromark-util-chunked": "^1.0.0",
+        "micromark-util-classify-character": "^1.0.0",
+        "micromark-util-html-tag-name": "^1.0.0",
+        "micromark-util-normalize-identifier": "^1.0.0",
+        "micromark-util-resolve-all": "^1.0.0",
+        "micromark-util-subtokenize": "^1.0.0",
+        "micromark-util-symbol": "^1.0.0",
+        "micromark-util-types": "^1.0.1",
+        "uvu": "^0.5.0"
+      }
+    },
+    "node_modules/micromark-factory-destination": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-1.1.0.tgz",
+      "integrity": "sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg==",
+      "funding": [
+        {
+          "type": "GitHub Sponsors",
+          "url": "https://github.com/sponsors/unifiedjs"
+        },
+        {
+          "type": "OpenCollective",
+          "url": "https://opencollective.com/unified"
+        }
+      ],
+      "dependencies": {
+        "micromark-util-character": "^1.0.0",
+        "micromark-util-symbol": "^1.0.0",
+        "micromark-util-types": "^1.0.0"
+      }
+    },
+    "node_modules/micromark-factory-label": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-1.1.0.tgz",
+      "integrity": "sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w==",
+      "funding": [
+        {
+          "type": "GitHub Sponsors",
+          "url": "https://github.com/sponsors/unifiedjs"
+        },
+        {
+          "type": "OpenCollective",
+          "url": "https://opencollective.com/unified"
+        }
+      ],
+      "dependencies": {
+        "micromark-util-character": "^1.0.0",
+        "micromark-util-symbol": "^1.0.0",
+        "micromark-util-types": "^1.0.0",
+        "uvu": "^0.5.0"
+      }
+    },
+    "node_modules/micromark-factory-space": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-1.1.0.tgz",
+      "integrity": "sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ==",
+      "funding": [
+        {
+          "type": "GitHub Sponsors",
+          "url": "https://github.com/sponsors/unifiedjs"
+        },
+        {
+          "type": "OpenCollective",
+          "url": "https://opencollective.com/unified"
+        }
+      ],
+      "dependencies": {
+        "micromark-util-character": "^1.0.0",
+        "micromark-util-types": "^1.0.0"
+      }
+    },
+    "node_modules/micromark-factory-title": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-1.1.0.tgz",
+      "integrity": "sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ==",
+      "funding": [
+        {
+          "type": "GitHub Sponsors",
+          "url": "https://github.com/sponsors/unifiedjs"
+        },
+        {
+          "type": "OpenCollective",
+          "url": "https://opencollective.com/unified"
+        }
+      ],
+      "dependencies": {
+        "micromark-factory-space": "^1.0.0",
+        "micromark-util-character": "^1.0.0",
+        "micromark-util-symbol": "^1.0.0",
+        "micromark-util-types": "^1.0.0"
+      }
+    },
+    "node_modules/micromark-factory-whitespace": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-1.1.0.tgz",
+      "integrity": "sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ==",
+      "funding": [
+        {
+          "type": "GitHub Sponsors",
+          "url": "https://github.com/sponsors/unifiedjs"
+        },
+        {
+          "type": "OpenCollective",
+          "url": "https://opencollective.com/unified"
+        }
+      ],
+      "dependencies": {
+        "micromark-factory-space": "^1.0.0",
+        "micromark-util-character": "^1.0.0",
+        "micromark-util-symbol": "^1.0.0",
+        "micromark-util-types": "^1.0.0"
+      }
+    },
+    "node_modules/micromark-util-character": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.2.0.tgz",
+      "integrity": "sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==",
+      "funding": [
+        {
+          "type": "GitHub Sponsors",
+          "url": "https://github.com/sponsors/unifiedjs"
+        },
+        {
+          "type": "OpenCollective",
+          "url": "https://opencollective.com/unified"
+        }
+      ],
+      "dependencies": {
+        "micromark-util-symbol": "^1.0.0",
+        "micromark-util-types": "^1.0.0"
+      }
+    },
+    "node_modules/micromark-util-chunked": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-1.1.0.tgz",
+      "integrity": "sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ==",
+      "funding": [
+        {
+          "type": "GitHub Sponsors",
+          "url": "https://github.com/sponsors/unifiedjs"
+        },
+        {
+          "type": "OpenCollective",
+          "url": "https://opencollective.com/unified"
+        }
+      ],
+      "dependencies": {
+        "micromark-util-symbol": "^1.0.0"
+      }
+    },
+    "node_modules/micromark-util-classify-character": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-1.1.0.tgz",
+      "integrity": "sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw==",
+      "funding": [
+        {
+          "type": "GitHub Sponsors",
+          "url": "https://github.com/sponsors/unifiedjs"
+        },
+        {
+          "type": "OpenCollective",
+          "url": "https://opencollective.com/unified"
+        }
+      ],
+      "dependencies": {
+        "micromark-util-character": "^1.0.0",
+        "micromark-util-symbol": "^1.0.0",
+        "micromark-util-types": "^1.0.0"
+      }
+    },
+    "node_modules/micromark-util-combine-extensions": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.1.0.tgz",
+      "integrity": "sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA==",
+      "funding": [
+        {
+          "type": "GitHub Sponsors",
+          "url": "https://github.com/sponsors/unifiedjs"
+        },
+        {
+          "type": "OpenCollective",
+          "url": "https://opencollective.com/unified"
+        }
+      ],
+      "dependencies": {
+        "micromark-util-chunked": "^1.0.0",
+        "micromark-util-types": "^1.0.0"
+      }
+    },
+    "node_modules/micromark-util-decode-numeric-character-reference": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.1.0.tgz",
+      "integrity": "sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw==",
+      "funding": [
+        {
+          "type": "GitHub Sponsors",
+          "url": "https://github.com/sponsors/unifiedjs"
+        },
+        {
+          "type": "OpenCollective",
+          "url": "https://opencollective.com/unified"
+        }
+      ],
+      "dependencies": {
+        "micromark-util-symbol": "^1.0.0"
+      }
+    },
+    "node_modules/micromark-util-decode-string": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-1.1.0.tgz",
+      "integrity": "sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ==",
+      "funding": [
+        {
+          "type": "GitHub Sponsors",
+          "url": "https://github.com/sponsors/unifiedjs"
+        },
+        {
+          "type": "OpenCollective",
+          "url": "https://opencollective.com/unified"
+        }
+      ],
+      "dependencies": {
+        "decode-named-character-reference": "^1.0.0",
+        "micromark-util-character": "^1.0.0",
+        "micromark-util-decode-numeric-character-reference": "^1.0.0",
+        "micromark-util-symbol": "^1.0.0"
+      }
+    },
+    "node_modules/micromark-util-encode": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-1.1.0.tgz",
+      "integrity": "sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw==",
+      "funding": [
+        {
+          "type": "GitHub Sponsors",
+          "url": "https://github.com/sponsors/unifiedjs"
+        },
+        {
+          "type": "OpenCollective",
+          "url": "https://opencollective.com/unified"
+        }
+      ]
+    },
+    "node_modules/micromark-util-html-tag-name": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.2.0.tgz",
+      "integrity": "sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q==",
+      "funding": [
+        {
+          "type": "GitHub Sponsors",
+          "url": "https://github.com/sponsors/unifiedjs"
+        },
+        {
+          "type": "OpenCollective",
+          "url": "https://opencollective.com/unified"
+        }
+      ]
+    },
+    "node_modules/micromark-util-normalize-identifier": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.1.0.tgz",
+      "integrity": "sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q==",
+      "funding": [
+        {
+          "type": "GitHub Sponsors",
+          "url": "https://github.com/sponsors/unifiedjs"
+        },
+        {
+          "type": "OpenCollective",
+          "url": "https://opencollective.com/unified"
+        }
+      ],
+      "dependencies": {
+        "micromark-util-symbol": "^1.0.0"
+      }
+    },
+    "node_modules/micromark-util-resolve-all": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-1.1.0.tgz",
+      "integrity": "sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA==",
+      "funding": [
+        {
+          "type": "GitHub Sponsors",
+          "url": "https://github.com/sponsors/unifiedjs"
+        },
+        {
+          "type": "OpenCollective",
+          "url": "https://opencollective.com/unified"
+        }
+      ],
+      "dependencies": {
+        "micromark-util-types": "^1.0.0"
+      }
+    },
+    "node_modules/micromark-util-sanitize-uri": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.2.0.tgz",
+      "integrity": "sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A==",
+      "funding": [
+        {
+          "type": "GitHub Sponsors",
+          "url": "https://github.com/sponsors/unifiedjs"
+        },
+        {
+          "type": "OpenCollective",
+          "url": "https://opencollective.com/unified"
+        }
+      ],
+      "dependencies": {
+        "micromark-util-character": "^1.0.0",
+        "micromark-util-encode": "^1.0.0",
+        "micromark-util-symbol": "^1.0.0"
+      }
+    },
+    "node_modules/micromark-util-subtokenize": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-1.1.0.tgz",
+      "integrity": "sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A==",
+      "funding": [
+        {
+          "type": "GitHub Sponsors",
+          "url": "https://github.com/sponsors/unifiedjs"
+        },
+        {
+          "type": "OpenCollective",
+          "url": "https://opencollective.com/unified"
+        }
+      ],
+      "dependencies": {
+        "micromark-util-chunked": "^1.0.0",
+        "micromark-util-symbol": "^1.0.0",
+        "micromark-util-types": "^1.0.0",
+        "uvu": "^0.5.0"
+      }
+    },
+    "node_modules/micromark-util-symbol": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz",
+      "integrity": "sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==",
+      "funding": [
+        {
+          "type": "GitHub Sponsors",
+          "url": "https://github.com/sponsors/unifiedjs"
+        },
+        {
+          "type": "OpenCollective",
+          "url": "https://opencollective.com/unified"
+        }
+      ]
+    },
+    "node_modules/micromark-util-types": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz",
+      "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==",
+      "funding": [
+        {
+          "type": "GitHub Sponsors",
+          "url": "https://github.com/sponsors/unifiedjs"
+        },
+        {
+          "type": "OpenCollective",
+          "url": "https://opencollective.com/unified"
+        }
+      ]
+    },
+    "node_modules/micromatch": {
+      "version": "4.0.8",
+      "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+      "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+      "license": "MIT",
+      "dependencies": {
+        "braces": "^3.0.3",
+        "picomatch": "^2.3.1"
+      },
+      "engines": {
+        "node": ">=8.6"
+      }
+    },
+    "node_modules/minimatch": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+      "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+      "dependencies": {
+        "brace-expansion": "^1.1.7"
+      },
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/mri": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
+      "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==",
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/ms": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+      "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+    },
+    "node_modules/muggle-string": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz",
+      "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/mz": {
+      "version": "2.7.0",
+      "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
+      "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
+      "dev": true,
+      "dependencies": {
+        "any-promise": "^1.0.0",
+        "object-assign": "^4.0.1",
+        "thenify-all": "^1.0.0"
+      }
+    },
+    "node_modules/nanoid": {
+      "version": "3.3.7",
+      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
+      "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "bin": {
+        "nanoid": "bin/nanoid.cjs"
+      },
+      "engines": {
+        "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+      }
+    },
+    "node_modules/natural-compare": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+      "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="
+    },
+    "node_modules/node-releases": {
+      "version": "2.0.18",
+      "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz",
+      "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/non-layered-tidy-tree-layout": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/non-layered-tidy-tree-layout/-/non-layered-tidy-tree-layout-2.0.2.tgz",
+      "integrity": "sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw=="
+    },
+    "node_modules/normalize-path": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+      "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/normalize-range": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz",
+      "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/nth-check": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
+      "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
+      "dev": true,
+      "dependencies": {
+        "boolbase": "^1.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/fb55/nth-check?sponsor=1"
+      }
+    },
+    "node_modules/object-assign": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+      "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/object-hash": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
+      "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
+      "dev": true,
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/once": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+      "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+      "dev": true,
+      "dependencies": {
+        "wrappy": "1"
+      }
+    },
+    "node_modules/optionator": {
+      "version": "0.9.3",
+      "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz",
+      "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==",
+      "dependencies": {
+        "@aashutoshrathi/word-wrap": "^1.2.3",
+        "deep-is": "^0.1.3",
+        "fast-levenshtein": "^2.0.6",
+        "levn": "^0.4.1",
+        "prelude-ls": "^1.2.1",
+        "type-check": "^0.4.0"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/p-limit": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+      "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+      "dependencies": {
+        "yocto-queue": "^0.1.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/p-locate": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+      "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+      "dependencies": {
+        "p-limit": "^3.0.2"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/parent-module": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+      "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+      "license": "MIT",
+      "dependencies": {
+        "callsites": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/path-browserify": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
+      "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/path-exists": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+      "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/path-is-absolute": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+      "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/path-key": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+      "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/path-parse": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+      "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+      "dev": true
+    },
+    "node_modules/picocolors": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz",
+      "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==",
+      "license": "ISC"
+    },
+    "node_modules/picomatch": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+      "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+      "engines": {
+        "node": ">=8.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
+    "node_modules/pify": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+      "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/pinia": {
+      "version": "2.2.2",
+      "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.2.2.tgz",
+      "integrity": "sha512-ja2XqFWZC36mupU4z1ZzxeTApV7DOw44cV4dhQ9sGwun+N89v/XP7+j7q6TanS1u1tdbK4r+1BUx7heMaIdagA==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/devtools-api": "^6.6.3",
+        "vue-demi": "^0.14.10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/posva"
+      },
+      "peerDependencies": {
+        "@vue/composition-api": "^1.4.0",
+        "typescript": ">=4.4.4",
+        "vue": "^2.6.14 || ^3.3.0"
+      },
+      "peerDependenciesMeta": {
+        "@vue/composition-api": {
+          "optional": true
+        },
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/pinia/node_modules/vue-demi": {
+      "version": "0.14.10",
+      "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
+      "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
+      "hasInstallScript": true,
+      "license": "MIT",
+      "bin": {
+        "vue-demi-fix": "bin/vue-demi-fix.js",
+        "vue-demi-switch": "bin/vue-demi-switch.js"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      },
+      "peerDependencies": {
+        "@vue/composition-api": "^1.0.0-rc.1",
+        "vue": "^3.0.0-0 || ^2.6.0"
+      },
+      "peerDependenciesMeta": {
+        "@vue/composition-api": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/pirates": {
+      "version": "4.0.6",
+      "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz",
+      "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==",
+      "dev": true,
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/postcss": {
+      "version": "8.4.47",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz",
+      "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==",
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/postcss"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "nanoid": "^3.3.7",
+        "picocolors": "^1.1.0",
+        "source-map-js": "^1.2.1"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >=14"
+      }
+    },
+    "node_modules/postcss-import": {
+      "version": "15.1.0",
+      "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
+      "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
+      "dev": true,
+      "dependencies": {
+        "postcss-value-parser": "^4.0.0",
+        "read-cache": "^1.0.0",
+        "resolve": "^1.1.7"
+      },
+      "engines": {
+        "node": ">=14.0.0"
+      },
+      "peerDependencies": {
+        "postcss": "^8.0.0"
+      }
+    },
+    "node_modules/postcss-js": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz",
+      "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==",
+      "dev": true,
+      "dependencies": {
+        "camelcase-css": "^2.0.1"
+      },
+      "engines": {
+        "node": "^12 || ^14 || >= 16"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/postcss/"
+      },
+      "peerDependencies": {
+        "postcss": "^8.4.21"
+      }
+    },
+    "node_modules/postcss-load-config": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.1.tgz",
+      "integrity": "sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==",
+      "dev": true,
+      "dependencies": {
+        "lilconfig": "^2.0.5",
+        "yaml": "^2.1.1"
+      },
+      "engines": {
+        "node": ">= 14"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/postcss/"
+      },
+      "peerDependencies": {
+        "postcss": ">=8.0.9",
+        "ts-node": ">=9.0.0"
+      },
+      "peerDependenciesMeta": {
+        "postcss": {
+          "optional": true
+        },
+        "ts-node": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/postcss-nested": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz",
+      "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==",
+      "dev": true,
+      "dependencies": {
+        "postcss-selector-parser": "^6.0.11"
+      },
+      "engines": {
+        "node": ">=12.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/postcss/"
+      },
+      "peerDependencies": {
+        "postcss": "^8.2.14"
+      }
+    },
+    "node_modules/postcss-selector-parser": {
+      "version": "6.1.2",
+      "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
+      "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "cssesc": "^3.0.0",
+        "util-deprecate": "^1.0.2"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/postcss-value-parser": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
+      "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
+      "dev": true
+    },
+    "node_modules/prelude-ls": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+      "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/punycode": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+      "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/queue-microtask": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+      "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ]
+    },
+    "node_modules/read-cache": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
+      "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
+      "dev": true,
+      "dependencies": {
+        "pify": "^2.3.0"
+      }
+    },
+    "node_modules/readdirp": {
+      "version": "3.6.0",
+      "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+      "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+      "dev": true,
+      "dependencies": {
+        "picomatch": "^2.2.1"
+      },
+      "engines": {
+        "node": ">=8.10.0"
+      }
+    },
+    "node_modules/resolve": {
+      "version": "1.22.4",
+      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.4.tgz",
+      "integrity": "sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg==",
+      "dev": true,
+      "dependencies": {
+        "is-core-module": "^2.13.0",
+        "path-parse": "^1.0.7",
+        "supports-preserve-symlinks-flag": "^1.0.0"
+      },
+      "bin": {
+        "resolve": "bin/resolve"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/resolve-from": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+      "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/reusify": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
+      "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
+      "engines": {
+        "iojs": ">=1.0.0",
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/robust-predicates": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz",
+      "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg=="
+    },
+    "node_modules/rollup": {
+      "version": "4.22.5",
+      "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.22.5.tgz",
+      "integrity": "sha512-WoinX7GeQOFMGznEcWA1WrTQCd/tpEbMkc3nuMs9BT0CPjMdSjPMTVClwWd4pgSQwJdP65SK9mTCNvItlr5o7w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/estree": "1.0.6"
+      },
+      "bin": {
+        "rollup": "dist/bin/rollup"
+      },
+      "engines": {
+        "node": ">=18.0.0",
+        "npm": ">=8.0.0"
+      },
+      "optionalDependencies": {
+        "@rollup/rollup-android-arm-eabi": "4.22.5",
+        "@rollup/rollup-android-arm64": "4.22.5",
+        "@rollup/rollup-darwin-arm64": "4.22.5",
+        "@rollup/rollup-darwin-x64": "4.22.5",
+        "@rollup/rollup-linux-arm-gnueabihf": "4.22.5",
+        "@rollup/rollup-linux-arm-musleabihf": "4.22.5",
+        "@rollup/rollup-linux-arm64-gnu": "4.22.5",
+        "@rollup/rollup-linux-arm64-musl": "4.22.5",
+        "@rollup/rollup-linux-powerpc64le-gnu": "4.22.5",
+        "@rollup/rollup-linux-riscv64-gnu": "4.22.5",
+        "@rollup/rollup-linux-s390x-gnu": "4.22.5",
+        "@rollup/rollup-linux-x64-gnu": "4.22.5",
+        "@rollup/rollup-linux-x64-musl": "4.22.5",
+        "@rollup/rollup-win32-arm64-msvc": "4.22.5",
+        "@rollup/rollup-win32-ia32-msvc": "4.22.5",
+        "@rollup/rollup-win32-x64-msvc": "4.22.5",
+        "fsevents": "~2.3.2"
+      }
+    },
+    "node_modules/run-parallel": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+      "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "dependencies": {
+        "queue-microtask": "^1.2.2"
+      }
+    },
+    "node_modules/rw": {
+      "version": "1.3.3",
+      "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz",
+      "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ=="
+    },
+    "node_modules/sade": {
+      "version": "1.8.1",
+      "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz",
+      "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==",
+      "dependencies": {
+        "mri": "^1.1.0"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/safer-buffer": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+      "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
+    },
+    "node_modules/semver": {
+      "version": "7.6.3",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
+      "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/shebang-command": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+      "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+      "dependencies": {
+        "shebang-regex": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/shebang-regex": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+      "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/source-map-js": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+      "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/strip-ansi": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/strip-json-comments": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+      "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/stylis": {
+      "version": "4.1.3",
+      "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.1.3.tgz",
+      "integrity": "sha512-GP6WDNWf+o403jrEp9c5jibKavrtLW+/qYGhFxFrG8maXhwTBI7gLLhiBb0o7uFccWN+EOS9aMO6cGHWAO07OA=="
+    },
+    "node_modules/sucrase": {
+      "version": "3.34.0",
+      "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.34.0.tgz",
+      "integrity": "sha512-70/LQEZ07TEcxiU2dz51FKaE6hCTWC6vr7FOk3Gr0U60C3shtAN+H+BFr9XlYe5xqf3RA8nrc+VIwzCfnxuXJw==",
+      "dev": true,
+      "dependencies": {
+        "@jridgewell/gen-mapping": "^0.3.2",
+        "commander": "^4.0.0",
+        "glob": "7.1.6",
+        "lines-and-columns": "^1.1.6",
+        "mz": "^2.7.0",
+        "pirates": "^4.0.1",
+        "ts-interface-checker": "^0.1.9"
+      },
+      "bin": {
+        "sucrase": "bin/sucrase",
+        "sucrase-node": "bin/sucrase-node"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/sucrase/node_modules/commander": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
+      "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
+      "dev": true,
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/sucrase/node_modules/glob": {
+      "version": "7.1.6",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
+      "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
+      "dev": true,
+      "dependencies": {
+        "fs.realpath": "^1.0.0",
+        "inflight": "^1.0.4",
+        "inherits": "2",
+        "minimatch": "^3.0.4",
+        "once": "^1.3.0",
+        "path-is-absolute": "^1.0.0"
+      },
+      "engines": {
+        "node": "*"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/supports-color": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+      "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+      "dependencies": {
+        "has-flag": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/supports-preserve-symlinks-flag": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+      "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/tailwindcss": {
+      "version": "3.4.13",
+      "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.13.tgz",
+      "integrity": "sha512-KqjHOJKogOUt5Bs752ykCeiwvi0fKVkr5oqsFNt/8px/tA8scFPIlkygsf6jXrfCqGHz7VflA6+yytWuM+XhFw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@alloc/quick-lru": "^5.2.0",
+        "arg": "^5.0.2",
+        "chokidar": "^3.5.3",
+        "didyoumean": "^1.2.2",
+        "dlv": "^1.1.3",
+        "fast-glob": "^3.3.0",
+        "glob-parent": "^6.0.2",
+        "is-glob": "^4.0.3",
+        "jiti": "^1.21.0",
+        "lilconfig": "^2.1.0",
+        "micromatch": "^4.0.5",
+        "normalize-path": "^3.0.0",
+        "object-hash": "^3.0.0",
+        "picocolors": "^1.0.0",
+        "postcss": "^8.4.23",
+        "postcss-import": "^15.1.0",
+        "postcss-js": "^4.0.1",
+        "postcss-load-config": "^4.0.1",
+        "postcss-nested": "^6.0.1",
+        "postcss-selector-parser": "^6.0.11",
+        "resolve": "^1.22.2",
+        "sucrase": "^3.32.0"
+      },
+      "bin": {
+        "tailwind": "lib/cli.js",
+        "tailwindcss": "lib/cli.js"
+      },
+      "engines": {
+        "node": ">=14.0.0"
+      }
+    },
+    "node_modules/text-table": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
+      "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw=="
+    },
+    "node_modules/thenify": {
+      "version": "3.3.1",
+      "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
+      "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
+      "dev": true,
+      "dependencies": {
+        "any-promise": "^1.0.0"
+      }
+    },
+    "node_modules/thenify-all": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
+      "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
+      "dev": true,
+      "dependencies": {
+        "thenify": ">= 3.1.0 < 4"
+      },
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
+    "node_modules/to-fast-properties": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
+      "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/to-regex-range": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+      "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+      "license": "MIT",
+      "dependencies": {
+        "is-number": "^7.0.0"
+      },
+      "engines": {
+        "node": ">=8.0"
+      }
+    },
+    "node_modules/ts-api-utils": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz",
+      "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=16"
+      },
+      "peerDependencies": {
+        "typescript": ">=4.2.0"
+      }
+    },
+    "node_modules/ts-dedent": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz",
+      "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==",
+      "engines": {
+        "node": ">=6.10"
+      }
+    },
+    "node_modules/ts-interface-checker": {
+      "version": "0.1.13",
+      "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
+      "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
+      "dev": true
+    },
+    "node_modules/type-check": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+      "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+      "dependencies": {
+        "prelude-ls": "^1.2.1"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/type-fest": {
+      "version": "0.20.2",
+      "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
+      "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
+      "dev": true,
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/typescript": {
+      "version": "5.6.2",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz",
+      "integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==",
+      "license": "Apache-2.0",
+      "bin": {
+        "tsc": "bin/tsc",
+        "tsserver": "bin/tsserver"
+      },
+      "engines": {
+        "node": ">=14.17"
+      }
+    },
+    "node_modules/typescript-eslint": {
+      "version": "8.7.0",
+      "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.7.0.tgz",
+      "integrity": "sha512-nEHbEYJyHwsuf7c3V3RS7Saq+1+la3i0ieR3qP0yjqWSzVmh8Drp47uOl9LjbPANac4S7EFSqvcYIKXUUwIfIQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@typescript-eslint/eslint-plugin": "8.7.0",
+        "@typescript-eslint/parser": "8.7.0",
+        "@typescript-eslint/utils": "8.7.0"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/unist-util-stringify-position": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz",
+      "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==",
+      "dependencies": {
+        "@types/unist": "^2.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/unified"
+      }
+    },
+    "node_modules/update-browserslist-db": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz",
+      "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/browserslist"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/browserslist"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "escalade": "^3.2.0",
+        "picocolors": "^1.1.0"
+      },
+      "bin": {
+        "update-browserslist-db": "cli.js"
+      },
+      "peerDependencies": {
+        "browserslist": ">= 4.21.0"
+      }
+    },
+    "node_modules/uri-js": {
+      "version": "4.4.1",
+      "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+      "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "punycode": "^2.1.0"
+      }
+    },
+    "node_modules/util-deprecate": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+      "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+      "dev": true
+    },
+    "node_modules/uuid": {
+      "version": "9.0.0",
+      "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz",
+      "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==",
+      "bin": {
+        "uuid": "dist/bin/uuid"
+      }
+    },
+    "node_modules/uvu": {
+      "version": "0.5.6",
+      "resolved": "https://registry.npmjs.org/uvu/-/uvu-0.5.6.tgz",
+      "integrity": "sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==",
+      "dependencies": {
+        "dequal": "^2.0.0",
+        "diff": "^5.0.0",
+        "kleur": "^4.0.3",
+        "sade": "^1.7.3"
+      },
+      "bin": {
+        "uvu": "bin.js"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/vite": {
+      "version": "5.4.8",
+      "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.8.tgz",
+      "integrity": "sha512-FqrItQ4DT1NC4zCUqMB4c4AZORMKIa0m8/URVCZ77OZ/QSNeJ54bU1vrFADbDsuwfIPcgknRkmqakQcgnL4GiQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "esbuild": "^0.21.3",
+        "postcss": "^8.4.43",
+        "rollup": "^4.20.0"
+      },
+      "bin": {
+        "vite": "bin/vite.js"
+      },
+      "engines": {
+        "node": "^18.0.0 || >=20.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/vitejs/vite?sponsor=1"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.3"
+      },
+      "peerDependencies": {
+        "@types/node": "^18.0.0 || >=20.0.0",
+        "less": "*",
+        "lightningcss": "^1.21.0",
+        "sass": "*",
+        "sass-embedded": "*",
+        "stylus": "*",
+        "sugarss": "*",
+        "terser": "^5.4.0"
+      },
+      "peerDependenciesMeta": {
+        "@types/node": {
+          "optional": true
+        },
+        "less": {
+          "optional": true
+        },
+        "lightningcss": {
+          "optional": true
+        },
+        "sass": {
+          "optional": true
+        },
+        "sass-embedded": {
+          "optional": true
+        },
+        "stylus": {
+          "optional": true
+        },
+        "sugarss": {
+          "optional": true
+        },
+        "terser": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/vscode-uri": {
+      "version": "3.0.8",
+      "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz",
+      "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/vue": {
+      "version": "3.5.10",
+      "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.10.tgz",
+      "integrity": "sha512-Vy2kmJwHPlouC/tSnIgXVg03SG+9wSqT1xu1Vehc+ChsXsRd7jLkKgMltVEFOzUdBr3uFwBCG+41LJtfAcBRng==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/compiler-dom": "3.5.10",
+        "@vue/compiler-sfc": "3.5.10",
+        "@vue/runtime-dom": "3.5.10",
+        "@vue/server-renderer": "3.5.10",
+        "@vue/shared": "3.5.10"
+      },
+      "peerDependencies": {
+        "typescript": "*"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/vue-eslint-parser": {
+      "version": "9.4.3",
+      "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.4.3.tgz",
+      "integrity": "sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "debug": "^4.3.4",
+        "eslint-scope": "^7.1.1",
+        "eslint-visitor-keys": "^3.3.0",
+        "espree": "^9.3.1",
+        "esquery": "^1.4.0",
+        "lodash": "^4.17.21",
+        "semver": "^7.3.6"
+      },
+      "engines": {
+        "node": "^14.17.0 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/mysticatea"
+      },
+      "peerDependencies": {
+        "eslint": ">=6.0.0"
+      }
+    },
+    "node_modules/vue-router": {
+      "version": "4.4.5",
+      "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.4.5.tgz",
+      "integrity": "sha512-4fKZygS8cH1yCyuabAXGUAsyi1b2/o/OKgu/RUb+znIYOxPRxdkytJEx+0wGcpBE1pX6vUgh5jwWOKRGvuA/7Q==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/devtools-api": "^6.6.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/posva"
+      },
+      "peerDependencies": {
+        "vue": "^3.2.0"
+      }
+    },
+    "node_modules/vue-toastification": {
+      "version": "2.0.0-rc.5",
+      "resolved": "https://registry.npmjs.org/vue-toastification/-/vue-toastification-2.0.0-rc.5.tgz",
+      "integrity": "sha512-q73e5jy6gucEO/U+P48hqX+/qyXDozAGmaGgLFm5tXX4wJBcVsnGp4e/iJqlm9xzHETYOilUuwOUje2Qg1JdwA==",
+      "peerDependencies": {
+        "vue": "^3.0.2"
+      }
+    },
+    "node_modules/vue-tsc": {
+      "version": "2.1.6",
+      "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.1.6.tgz",
+      "integrity": "sha512-f98dyZp5FOukcYmbFpuSCJ4Z0vHSOSmxGttZJCsFeX0M4w/Rsq0s4uKXjcSRsZqsRgQa6z7SfuO+y0HVICE57Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@volar/typescript": "~2.4.1",
+        "@vue/language-core": "2.1.6",
+        "semver": "^7.5.4"
+      },
+      "bin": {
+        "vue-tsc": "bin/vue-tsc.js"
+      },
+      "peerDependencies": {
+        "typescript": ">=5.0.0"
+      }
+    },
+    "node_modules/vue3-popper": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/vue3-popper/-/vue3-popper-1.5.0.tgz",
+      "integrity": "sha512-xaEnx90YBnlSg5G2yWqm2DHWHg+DB99UVRp4VsyTF0QLXyHrqSuE1Xo5+sG0AQq/lBcrGMlk5NU5xE2MDLKViw==",
+      "dependencies": {
+        "@popperjs/core": "^2.9.2",
+        "debounce": "^1.2.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "peerDependencies": {
+        "vue": "^3.2.20"
+      }
+    },
+    "node_modules/web-worker": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.2.0.tgz",
+      "integrity": "sha512-PgF341avzqyx60neE9DD+XS26MMNMoUQRz9NOZwW32nPQrF6p77f1htcnjBSEV8BGMKZ16choqUG4hyI0Hx7mA=="
+    },
+    "node_modules/which": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+      "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+      "dependencies": {
+        "isexe": "^2.0.0"
+      },
+      "bin": {
+        "node-which": "bin/node-which"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/wrappy": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+      "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+      "dev": true
+    },
+    "node_modules/xml-name-validator": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz",
+      "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==",
+      "dev": true,
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/yaml": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.1.tgz",
+      "integrity": "sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==",
+      "dev": true,
+      "engines": {
+        "node": ">= 14"
+      }
+    },
+    "node_modules/yocto-queue": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+      "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    }
+  },
+  "dependencies": {
+    "@aashutoshrathi/word-wrap": {
+      "version": "1.2.6",
+      "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz",
+      "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA=="
+    },
+    "@alloc/quick-lru": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
+      "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
+      "dev": true
+    },
+    "@babel/helper-string-parser": {
+      "version": "7.24.8",
+      "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz",
+      "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ=="
+    },
+    "@babel/helper-validator-identifier": {
+      "version": "7.24.7",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz",
+      "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w=="
+    },
+    "@babel/parser": {
+      "version": "7.25.6",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.6.tgz",
+      "integrity": "sha512-trGdfBdbD0l1ZPmcJ83eNxB9rbEax4ALFTF7fN386TMYbeCQbyme5cOEXQhbGXKebwGaB/J52w1mrklMcbgy6Q==",
+      "requires": {
+        "@babel/types": "^7.25.6"
+      }
+    },
+    "@babel/types": {
+      "version": "7.25.6",
+      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.6.tgz",
+      "integrity": "sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw==",
+      "requires": {
+        "@babel/helper-string-parser": "^7.24.8",
+        "@babel/helper-validator-identifier": "^7.24.7",
+        "to-fast-properties": "^2.0.0"
+      }
+    },
+    "@braintree/sanitize-url": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-6.0.2.tgz",
+      "integrity": "sha512-Tbsj02wXCbqGmzdnXNk0SOF19ChhRU70BsroIi4Pm6Ehp56in6vch94mfbdQ17DozxkL3BAVjbZ4Qc1a0HFRAg=="
+    },
+    "@esbuild/aix-ppc64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
+      "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/android-arm": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
+      "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/android-arm64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
+      "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/android-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
+      "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/darwin-arm64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
+      "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/darwin-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
+      "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/freebsd-arm64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
+      "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/freebsd-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
+      "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/linux-arm": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
+      "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/linux-arm64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
+      "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/linux-ia32": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
+      "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/linux-loong64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
+      "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/linux-mips64el": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
+      "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/linux-ppc64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
+      "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/linux-riscv64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
+      "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/linux-s390x": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
+      "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/linux-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
+      "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/netbsd-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
+      "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/openbsd-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
+      "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/sunos-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
+      "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/win32-arm64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
+      "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/win32-ia32": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
+      "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/win32-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
+      "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
+      "dev": true,
+      "optional": true
+    },
+    "@eslint-community/eslint-utils": {
+      "version": "4.4.0",
+      "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
+      "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==",
+      "requires": {
+        "eslint-visitor-keys": "^3.3.0"
+      }
+    },
+    "@eslint-community/regexpp": {
+      "version": "4.11.1",
+      "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.1.tgz",
+      "integrity": "sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q=="
+    },
+    "@eslint/config-array": {
+      "version": "0.18.0",
+      "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.18.0.tgz",
+      "integrity": "sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==",
+      "requires": {
+        "@eslint/object-schema": "^2.1.4",
+        "debug": "^4.3.1",
+        "minimatch": "^3.1.2"
+      }
+    },
+    "@eslint/core": {
+      "version": "0.6.0",
+      "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.6.0.tgz",
+      "integrity": "sha512-8I2Q8ykA4J0x0o7cg67FPVnehcqWTBehu/lmY+bolPFHGjh49YzGBMXTvpqVgEbBdvNCSxj6iFgiIyHzf03lzg=="
+    },
+    "@eslint/eslintrc": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz",
+      "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==",
+      "requires": {
+        "ajv": "^6.12.4",
+        "debug": "^4.3.2",
+        "espree": "^10.0.1",
+        "globals": "^14.0.0",
+        "ignore": "^5.2.0",
+        "import-fresh": "^3.2.1",
+        "js-yaml": "^4.1.0",
+        "minimatch": "^3.1.2",
+        "strip-json-comments": "^3.1.1"
+      },
+      "dependencies": {
+        "eslint-visitor-keys": {
+          "version": "4.1.0",
+          "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.1.0.tgz",
+          "integrity": "sha512-Q7lok0mqMUSf5a/AdAZkA5a/gHcO6snwQClVNNvFKCAVlxXucdU8pKydU5ZVZjBx5xr37vGbFFWtLQYreLzrZg=="
+        },
+        "espree": {
+          "version": "10.2.0",
+          "resolved": "https://registry.npmjs.org/espree/-/espree-10.2.0.tgz",
+          "integrity": "sha512-upbkBJbckcCNBDBDXEbuhjbP68n+scUd3k/U2EkyM9nw+I/jPiL4cLF/Al06CF96wRltFda16sxDFrxsI1v0/g==",
+          "requires": {
+            "acorn": "^8.12.0",
+            "acorn-jsx": "^5.3.2",
+            "eslint-visitor-keys": "^4.1.0"
+          }
+        },
+        "globals": {
+          "version": "14.0.0",
+          "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
+          "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="
+        }
+      }
+    },
+    "@eslint/js": {
+      "version": "9.11.1",
+      "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.11.1.tgz",
+      "integrity": "sha512-/qu+TWz8WwPWc7/HcIJKi+c+MOm46GdVaSlTTQcaqaL53+GsoA6MxWp5PtTx48qbSP7ylM1Kn7nhvkugfJvRSA=="
+    },
+    "@eslint/object-schema": {
+      "version": "2.1.4",
+      "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz",
+      "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ=="
+    },
+    "@eslint/plugin-kit": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.0.tgz",
+      "integrity": "sha512-vH9PiIMMwvhCx31Af3HiGzsVNULDbyVkHXwlemn/B0TFj/00ho3y55efXrUZTfQipxoHC5u4xq6zblww1zm1Ig==",
+      "requires": {
+        "levn": "^0.4.1"
+      }
+    },
+    "@fortawesome/fontawesome-common-types": {
+      "version": "6.6.0",
+      "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.6.0.tgz",
+      "integrity": "sha512-xyX0X9mc0kyz9plIyryrRbl7ngsA9jz77mCZJsUkLl+ZKs0KWObgaEBoSgQiYWAsSmjz/yjl0F++Got0Mdp4Rw=="
+    },
+    "@fortawesome/fontawesome-svg-core": {
+      "version": "6.6.0",
+      "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.6.0.tgz",
+      "integrity": "sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg==",
+      "requires": {
+        "@fortawesome/fontawesome-common-types": "6.6.0"
+      }
+    },
+    "@fortawesome/free-brands-svg-icons": {
+      "version": "6.6.0",
+      "resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.6.0.tgz",
+      "integrity": "sha512-1MPD8lMNW/earme4OQi1IFHtmHUwAKgghXlNwWi9GO7QkTfD+IIaYpIai4m2YJEzqfEji3jFHX1DZI5pbY/biQ==",
+      "requires": {
+        "@fortawesome/fontawesome-common-types": "6.6.0"
+      }
+    },
+    "@fortawesome/free-solid-svg-icons": {
+      "version": "6.6.0",
+      "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.6.0.tgz",
+      "integrity": "sha512-IYv/2skhEDFc2WGUcqvFJkeK39Q+HyPf5GHUrT/l2pKbtgEIv1al1TKd6qStR5OIwQdN1GZP54ci3y4mroJWjA==",
+      "requires": {
+        "@fortawesome/fontawesome-common-types": "6.6.0"
+      }
+    },
+    "@fortawesome/vue-fontawesome": {
+      "version": "3.0.8",
+      "resolved": "https://registry.npmjs.org/@fortawesome/vue-fontawesome/-/vue-fontawesome-3.0.8.tgz",
+      "integrity": "sha512-yyHHAj4G8pQIDfaIsMvQpwKMboIZtcHTUvPqXjOHyldh1O1vZfH4W03VDPv5RvI9P6DLTzJQlmVgj9wCf7c2Fw==",
+      "requires": {}
+    },
+    "@humanwhocodes/module-importer": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+      "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="
+    },
+    "@humanwhocodes/retry": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.0.tgz",
+      "integrity": "sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew=="
+    },
+    "@jridgewell/gen-mapping": {
+      "version": "0.3.3",
+      "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz",
+      "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==",
+      "dev": true,
+      "requires": {
+        "@jridgewell/set-array": "^1.0.1",
+        "@jridgewell/sourcemap-codec": "^1.4.10",
+        "@jridgewell/trace-mapping": "^0.3.9"
+      }
+    },
+    "@jridgewell/resolve-uri": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz",
+      "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==",
+      "dev": true
+    },
+    "@jridgewell/set-array": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz",
+      "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==",
+      "dev": true
+    },
+    "@jridgewell/sourcemap-codec": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
+      "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="
+    },
+    "@jridgewell/trace-mapping": {
+      "version": "0.3.19",
+      "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz",
+      "integrity": "sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==",
+      "dev": true,
+      "requires": {
+        "@jridgewell/resolve-uri": "^3.1.0",
+        "@jridgewell/sourcemap-codec": "^1.4.14"
+      }
+    },
+    "@nodelib/fs.scandir": {
+      "version": "2.1.5",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+      "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+      "requires": {
+        "@nodelib/fs.stat": "2.0.5",
+        "run-parallel": "^1.1.9"
+      }
+    },
+    "@nodelib/fs.stat": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+      "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="
+    },
+    "@nodelib/fs.walk": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+      "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+      "requires": {
+        "@nodelib/fs.scandir": "2.1.5",
+        "fastq": "^1.6.0"
+      }
+    },
+    "@popperjs/core": {
+      "version": "2.11.6",
+      "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz",
+      "integrity": "sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw=="
+    },
+    "@rollup/rollup-android-arm-eabi": {
+      "version": "4.22.5",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.22.5.tgz",
+      "integrity": "sha512-SU5cvamg0Eyu/F+kLeMXS7GoahL+OoizlclVFX3l5Ql6yNlywJJ0OuqTzUx0v+aHhPHEB/56CT06GQrRrGNYww==",
+      "dev": true,
+      "optional": true
+    },
+    "@rollup/rollup-android-arm64": {
+      "version": "4.22.5",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.22.5.tgz",
+      "integrity": "sha512-S4pit5BP6E5R5C8S6tgU/drvgjtYW76FBuG6+ibG3tMvlD1h9LHVF9KmlmaUBQ8Obou7hEyS+0w+IR/VtxwNMQ==",
+      "dev": true,
+      "optional": true
+    },
+    "@rollup/rollup-darwin-arm64": {
+      "version": "4.22.5",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.22.5.tgz",
+      "integrity": "sha512-250ZGg4ipTL0TGvLlfACkIxS9+KLtIbn7BCZjsZj88zSg2Lvu3Xdw6dhAhfe/FjjXPVNCtcSp+WZjVsD3a/Zlw==",
+      "dev": true,
+      "optional": true
+    },
+    "@rollup/rollup-darwin-x64": {
+      "version": "4.22.5",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.22.5.tgz",
+      "integrity": "sha512-D8brJEFg5D+QxFcW6jYANu+Rr9SlKtTenmsX5hOSzNYVrK5oLAEMTUgKWYJP+wdKyCdeSwnapLsn+OVRFycuQg==",
+      "dev": true,
+      "optional": true
+    },
+    "@rollup/rollup-linux-arm-gnueabihf": {
+      "version": "4.22.5",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.22.5.tgz",
+      "integrity": "sha512-PNqXYmdNFyWNg0ma5LdY8wP+eQfdvyaBAojAXgO7/gs0Q/6TQJVXAXe8gwW9URjbS0YAammur0fynYGiWsKlXw==",
+      "dev": true,
+      "optional": true
+    },
+    "@rollup/rollup-linux-arm-musleabihf": {
+      "version": "4.22.5",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.22.5.tgz",
+      "integrity": "sha512-kSSCZOKz3HqlrEuwKd9TYv7vxPYD77vHSUvM2y0YaTGnFc8AdI5TTQRrM1yIp3tXCKrSL9A7JLoILjtad5t8pQ==",
+      "dev": true,
+      "optional": true
+    },
+    "@rollup/rollup-linux-arm64-gnu": {
+      "version": "4.22.5",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.22.5.tgz",
+      "integrity": "sha512-oTXQeJHRbOnwRnRffb6bmqmUugz0glXaPyspp4gbQOPVApdpRrY/j7KP3lr7M8kTfQTyrBUzFjj5EuHAhqH4/w==",
+      "dev": true,
+      "optional": true
+    },
+    "@rollup/rollup-linux-arm64-musl": {
+      "version": "4.22.5",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.22.5.tgz",
+      "integrity": "sha512-qnOTIIs6tIGFKCHdhYitgC2XQ2X25InIbZFor5wh+mALH84qnFHvc+vmWUpyX97B0hNvwNUL4B+MB8vJvH65Fw==",
+      "dev": true,
+      "optional": true
+    },
+    "@rollup/rollup-linux-powerpc64le-gnu": {
+      "version": "4.22.5",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.22.5.tgz",
+      "integrity": "sha512-TMYu+DUdNlgBXING13rHSfUc3Ky5nLPbWs4bFnT+R6Vu3OvXkTkixvvBKk8uO4MT5Ab6lC3U7x8S8El2q5o56w==",
+      "dev": true,
+      "optional": true
+    },
+    "@rollup/rollup-linux-riscv64-gnu": {
+      "version": "4.22.5",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.22.5.tgz",
+      "integrity": "sha512-PTQq1Kz22ZRvuhr3uURH+U/Q/a0pbxJoICGSprNLAoBEkyD3Sh9qP5I0Asn0y0wejXQBbsVMRZRxlbGFD9OK4A==",
+      "dev": true,
+      "optional": true
+    },
+    "@rollup/rollup-linux-s390x-gnu": {
+      "version": "4.22.5",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.22.5.tgz",
+      "integrity": "sha512-bR5nCojtpuMss6TDEmf/jnBnzlo+6n1UhgwqUvRoe4VIotC7FG1IKkyJbwsT7JDsF2jxR+NTnuOwiGv0hLyDoQ==",
+      "dev": true,
+      "optional": true
+    },
+    "@rollup/rollup-linux-x64-gnu": {
+      "version": "4.22.5",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.22.5.tgz",
+      "integrity": "sha512-N0jPPhHjGShcB9/XXZQWuWBKZQnC1F36Ce3sDqWpujsGjDz/CQtOL9LgTrJ+rJC8MJeesMWrMWVLKKNR/tMOCA==",
+      "dev": true,
+      "optional": true
+    },
+    "@rollup/rollup-linux-x64-musl": {
+      "version": "4.22.5",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.22.5.tgz",
+      "integrity": "sha512-uBa2e28ohzNNwjr6Uxm4XyaA1M/8aTgfF2T7UIlElLaeXkgpmIJ2EitVNQxjO9xLLLy60YqAgKn/AqSpCUkE9g==",
+      "dev": true,
+      "optional": true
+    },
+    "@rollup/rollup-win32-arm64-msvc": {
+      "version": "4.22.5",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.22.5.tgz",
+      "integrity": "sha512-RXT8S1HP8AFN/Kr3tg4fuYrNxZ/pZf1HemC5Tsddc6HzgGnJm0+Lh5rAHJkDuW3StI0ynNXukidROMXYl6ew8w==",
+      "dev": true,
+      "optional": true
+    },
+    "@rollup/rollup-win32-ia32-msvc": {
+      "version": "4.22.5",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.22.5.tgz",
+      "integrity": "sha512-ElTYOh50InL8kzyUD6XsnPit7jYCKrphmddKAe1/Ytt74apOxDq5YEcbsiKs0fR3vff3jEneMM+3I7jbqaMyBg==",
+      "dev": true,
+      "optional": true
+    },
+    "@rollup/rollup-win32-x64-msvc": {
+      "version": "4.22.5",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.22.5.tgz",
+      "integrity": "sha512-+lvL/4mQxSV8MukpkKyyvfwhH266COcWlXE/1qxwN08ajovta3459zrjLghYMgDerlzNwLAcFpvU+WWE5y6nAQ==",
+      "dev": true,
+      "optional": true
+    },
+    "@types/d3-scale": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.3.tgz",
+      "integrity": "sha512-PATBiMCpvHJSMtZAMEhc2WyL+hnzarKzI6wAHYjhsonjWJYGq5BXTzQjv4l8m2jO183/4wZ90rKvSeT7o72xNQ==",
+      "requires": {
+        "@types/d3-time": "*"
+      }
+    },
+    "@types/d3-scale-chromatic": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz",
+      "integrity": "sha512-dsoJGEIShosKVRBZB0Vo3C8nqSDqVGujJU6tPznsBJxNJNwMF8utmS83nvCBKQYPpjCzaaHcrf66iTRpZosLPw=="
+    },
+    "@types/d3-time": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.0.tgz",
+      "integrity": "sha512-sZLCdHvBUcNby1cB6Fd3ZBrABbjz3v1Vm90nysCQ6Vt7vd6e/h9Lt7SiJUoEX0l4Dzc7P5llKyhqSi1ycSf1Hg=="
+    },
+    "@types/debug": {
+      "version": "4.1.8",
+      "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.8.tgz",
+      "integrity": "sha512-/vPO1EPOs306Cvhwv7KfVfYvOJqA/S/AXjaHQiJboCZzcNDb+TIJFN9/2C9DZ//ijSKWioNyUxD792QmDJ+HKQ==",
+      "requires": {
+        "@types/ms": "*"
+      }
+    },
+    "@types/estree": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
+      "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="
+    },
+    "@types/json-schema": {
+      "version": "7.0.15",
+      "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+      "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="
+    },
+    "@types/mdast": {
+      "version": "3.0.12",
+      "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.12.tgz",
+      "integrity": "sha512-DT+iNIRNX884cx0/Q1ja7NyUPpZuv0KPyL5rGNxm1WC1OtHstl7n4Jb7nk+xacNShQMbczJjt8uFzznpp6kYBg==",
+      "requires": {
+        "@types/unist": "^2"
+      }
+    },
+    "@types/ms": {
+      "version": "0.7.31",
+      "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz",
+      "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA=="
+    },
+    "@types/unist": {
+      "version": "2.0.7",
+      "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.7.tgz",
+      "integrity": "sha512-cputDpIbFgLUaGQn6Vqg3/YsJwxUwHLO13v3i5ouxT4lat0khip9AEWxtERujXV9wxIB1EyF97BSJFt6vpdI8g=="
+    },
+    "@typescript-eslint/eslint-plugin": {
+      "version": "8.7.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.7.0.tgz",
+      "integrity": "sha512-RIHOoznhA3CCfSTFiB6kBGLQtB/sox+pJ6jeFu6FxJvqL8qRxq/FfGO/UhsGgQM9oGdXkV4xUgli+dt26biB6A==",
+      "requires": {
+        "@eslint-community/regexpp": "^4.10.0",
+        "@typescript-eslint/scope-manager": "8.7.0",
+        "@typescript-eslint/type-utils": "8.7.0",
+        "@typescript-eslint/utils": "8.7.0",
+        "@typescript-eslint/visitor-keys": "8.7.0",
+        "graphemer": "^1.4.0",
+        "ignore": "^5.3.1",
+        "natural-compare": "^1.4.0",
+        "ts-api-utils": "^1.3.0"
+      }
+    },
+    "@typescript-eslint/parser": {
+      "version": "8.7.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.7.0.tgz",
+      "integrity": "sha512-lN0btVpj2unxHlNYLI//BQ7nzbMJYBVQX5+pbNXvGYazdlgYonMn4AhhHifQ+J4fGRYA/m1DjaQjx+fDetqBOQ==",
+      "requires": {
+        "@typescript-eslint/scope-manager": "8.7.0",
+        "@typescript-eslint/types": "8.7.0",
+        "@typescript-eslint/typescript-estree": "8.7.0",
+        "@typescript-eslint/visitor-keys": "8.7.0",
+        "debug": "^4.3.4"
+      }
+    },
+    "@typescript-eslint/scope-manager": {
+      "version": "8.7.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.7.0.tgz",
+      "integrity": "sha512-87rC0k3ZlDOuz82zzXRtQ7Akv3GKhHs0ti4YcbAJtaomllXoSO8hi7Ix3ccEvCd824dy9aIX+j3d2UMAfCtVpg==",
+      "requires": {
+        "@typescript-eslint/types": "8.7.0",
+        "@typescript-eslint/visitor-keys": "8.7.0"
+      }
+    },
+    "@typescript-eslint/type-utils": {
+      "version": "8.7.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.7.0.tgz",
+      "integrity": "sha512-tl0N0Mj3hMSkEYhLkjREp54OSb/FI6qyCzfiiclvJvOqre6hsZTGSnHtmFLDU8TIM62G7ygEa1bI08lcuRwEnQ==",
+      "requires": {
+        "@typescript-eslint/typescript-estree": "8.7.0",
+        "@typescript-eslint/utils": "8.7.0",
+        "debug": "^4.3.4",
+        "ts-api-utils": "^1.3.0"
+      }
+    },
+    "@typescript-eslint/types": {
+      "version": "8.7.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.7.0.tgz",
+      "integrity": "sha512-LLt4BLHFwSfASHSF2K29SZ+ZCsbQOM+LuarPjRUuHm+Qd09hSe3GCeaQbcCr+Mik+0QFRmep/FyZBO6fJ64U3w=="
+    },
+    "@typescript-eslint/typescript-estree": {
+      "version": "8.7.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.7.0.tgz",
+      "integrity": "sha512-MC8nmcGHsmfAKxwnluTQpNqceniT8SteVwd2voYlmiSWGOtjvGXdPl17dYu2797GVscK30Z04WRM28CrKS9WOg==",
+      "requires": {
+        "@typescript-eslint/types": "8.7.0",
+        "@typescript-eslint/visitor-keys": "8.7.0",
+        "debug": "^4.3.4",
+        "fast-glob": "^3.3.2",
+        "is-glob": "^4.0.3",
+        "minimatch": "^9.0.4",
+        "semver": "^7.6.0",
+        "ts-api-utils": "^1.3.0"
+      },
+      "dependencies": {
+        "brace-expansion": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+          "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+          "requires": {
+            "balanced-match": "^1.0.0"
+          }
+        },
+        "minimatch": {
+          "version": "9.0.5",
+          "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+          "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+          "requires": {
+            "brace-expansion": "^2.0.1"
+          }
+        }
+      }
+    },
+    "@typescript-eslint/utils": {
+      "version": "8.7.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.7.0.tgz",
+      "integrity": "sha512-ZbdUdwsl2X/s3CiyAu3gOlfQzpbuG3nTWKPoIvAu1pu5r8viiJvv2NPN2AqArL35NCYtw/lrPPfM4gxrMLNLPw==",
+      "requires": {
+        "@eslint-community/eslint-utils": "^4.4.0",
+        "@typescript-eslint/scope-manager": "8.7.0",
+        "@typescript-eslint/types": "8.7.0",
+        "@typescript-eslint/typescript-estree": "8.7.0"
+      }
+    },
+    "@typescript-eslint/visitor-keys": {
+      "version": "8.7.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.7.0.tgz",
+      "integrity": "sha512-b1tx0orFCCh/THWPQa2ZwWzvOeyzzp36vkJYOpVg0u8UVOIsfVrnuC9FqAw9gRKn+rG2VmWQ/zDJZzkxUnj/XQ==",
+      "requires": {
+        "@typescript-eslint/types": "8.7.0",
+        "eslint-visitor-keys": "^3.4.3"
+      }
+    },
+    "@vitejs/plugin-vue": {
+      "version": "5.1.4",
+      "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.1.4.tgz",
+      "integrity": "sha512-N2XSI2n3sQqp5w7Y/AN/L2XDjBIRGqXko+eDp42sydYSBeJuSm5a1sLf8zakmo8u7tA8NmBgoDLA1HeOESjp9A==",
+      "dev": true,
+      "requires": {}
+    },
+    "@volar/language-core": {
+      "version": "2.4.5",
+      "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.5.tgz",
+      "integrity": "sha512-F4tA0DCO5Q1F5mScHmca0umsi2ufKULAnMOVBfMsZdT4myhVl4WdKRwCaKcfOkIEuyrAVvtq1ESBdZ+rSyLVww==",
+      "dev": true,
+      "requires": {
+        "@volar/source-map": "2.4.5"
+      }
+    },
+    "@volar/source-map": {
+      "version": "2.4.5",
+      "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.5.tgz",
+      "integrity": "sha512-varwD7RaKE2J/Z+Zu6j3mNNJbNT394qIxXwdvz/4ao/vxOfyClZpSDtLKkwWmecinkOVos5+PWkWraelfMLfpw==",
+      "dev": true
+    },
+    "@volar/typescript": {
+      "version": "2.4.5",
+      "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.5.tgz",
+      "integrity": "sha512-mcT1mHvLljAEtHviVcBuOyAwwMKz1ibXTi5uYtP/pf4XxoAzpdkQ+Br2IC0NPCvLCbjPZmbf3I0udndkfB1CDg==",
+      "dev": true,
+      "requires": {
+        "@volar/language-core": "2.4.5",
+        "path-browserify": "^1.0.1",
+        "vscode-uri": "^3.0.8"
+      }
+    },
+    "@vue/compiler-core": {
+      "version": "3.5.10",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.10.tgz",
+      "integrity": "sha512-iXWlk+Cg/ag7gLvY0SfVucU8Kh2CjysYZjhhP70w9qI4MvSox4frrP+vDGvtQuzIcgD8+sxM6lZvCtdxGunTAA==",
+      "requires": {
+        "@babel/parser": "^7.25.3",
+        "@vue/shared": "3.5.10",
+        "entities": "^4.5.0",
+        "estree-walker": "^2.0.2",
+        "source-map-js": "^1.2.0"
+      }
+    },
+    "@vue/compiler-dom": {
+      "version": "3.5.10",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.10.tgz",
+      "integrity": "sha512-DyxHC6qPcktwYGKOIy3XqnHRrrXyWR2u91AjP+nLkADko380srsC2DC3s7Y1Rk6YfOlxOlvEQKa9XXmLI+W4ZA==",
+      "requires": {
+        "@vue/compiler-core": "3.5.10",
+        "@vue/shared": "3.5.10"
+      }
+    },
+    "@vue/compiler-sfc": {
+      "version": "3.5.10",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.10.tgz",
+      "integrity": "sha512-to8E1BgpakV7224ZCm8gz1ZRSyjNCAWEplwFMWKlzCdP9DkMKhRRwt0WkCjY7jkzi/Vz3xgbpeig5Pnbly4Tow==",
+      "requires": {
+        "@babel/parser": "^7.25.3",
+        "@vue/compiler-core": "3.5.10",
+        "@vue/compiler-dom": "3.5.10",
+        "@vue/compiler-ssr": "3.5.10",
+        "@vue/shared": "3.5.10",
+        "estree-walker": "^2.0.2",
+        "magic-string": "^0.30.11",
+        "postcss": "^8.4.47",
+        "source-map-js": "^1.2.0"
+      }
+    },
+    "@vue/compiler-ssr": {
+      "version": "3.5.10",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.10.tgz",
+      "integrity": "sha512-hxP4Y3KImqdtyUKXDRSxKSRkSm1H9fCvhojEYrnaoWhE4w/y8vwWhnosJoPPe2AXm5sU7CSbYYAgkt2ZPhDz+A==",
+      "requires": {
+        "@vue/compiler-dom": "3.5.10",
+        "@vue/shared": "3.5.10"
+      }
+    },
+    "@vue/compiler-vue2": {
+      "version": "2.7.16",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz",
+      "integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==",
+      "dev": true,
+      "requires": {
+        "de-indent": "^1.0.2",
+        "he": "^1.2.0"
+      }
+    },
+    "@vue/devtools-api": {
+      "version": "6.6.4",
+      "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
+      "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g=="
+    },
+    "@vue/language-core": {
+      "version": "2.1.6",
+      "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.1.6.tgz",
+      "integrity": "sha512-MW569cSky9R/ooKMh6xa2g1D0AtRKbL56k83dzus/bx//RDJk24RHWkMzbAlXjMdDNyxAaagKPRquBIxkxlCkg==",
+      "dev": true,
+      "requires": {
+        "@volar/language-core": "~2.4.1",
+        "@vue/compiler-dom": "^3.4.0",
+        "@vue/compiler-vue2": "^2.7.16",
+        "@vue/shared": "^3.4.0",
+        "computeds": "^0.0.1",
+        "minimatch": "^9.0.3",
+        "muggle-string": "^0.4.1",
+        "path-browserify": "^1.0.1"
+      },
+      "dependencies": {
+        "brace-expansion": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+          "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+          "dev": true,
+          "requires": {
+            "balanced-match": "^1.0.0"
+          }
+        },
+        "minimatch": {
+          "version": "9.0.5",
+          "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+          "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+          "dev": true,
+          "requires": {
+            "brace-expansion": "^2.0.1"
+          }
+        }
+      }
+    },
+    "@vue/reactivity": {
+      "version": "3.5.10",
+      "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.10.tgz",
+      "integrity": "sha512-kW08v06F6xPSHhid9DJ9YjOGmwNDOsJJQk0ax21wKaUYzzuJGEuoKNU2Ujux8FLMrP7CFJJKsHhXN9l2WOVi2g==",
+      "requires": {
+        "@vue/shared": "3.5.10"
+      }
+    },
+    "@vue/runtime-core": {
+      "version": "3.5.10",
+      "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.10.tgz",
+      "integrity": "sha512-9Q86I5Qq3swSkFfzrZ+iqEy7Vla325M7S7xc1NwKnRm/qoi1Dauz0rT6mTMmscqx4qz0EDJ1wjB+A36k7rl8mA==",
+      "requires": {
+        "@vue/reactivity": "3.5.10",
+        "@vue/shared": "3.5.10"
+      }
+    },
+    "@vue/runtime-dom": {
+      "version": "3.5.10",
+      "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.10.tgz",
+      "integrity": "sha512-t3x7ht5qF8ZRi1H4fZqFzyY2j+GTMTDxRheT+i8M9Ph0oepUxoadmbwlFwMoW7RYCpNQLpP2Yx3feKs+fyBdpA==",
+      "requires": {
+        "@vue/reactivity": "3.5.10",
+        "@vue/runtime-core": "3.5.10",
+        "@vue/shared": "3.5.10",
+        "csstype": "^3.1.3"
+      }
+    },
+    "@vue/server-renderer": {
+      "version": "3.5.10",
+      "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.10.tgz",
+      "integrity": "sha512-IVE97tt2kGKwHNq9yVO0xdh1IvYfZCShvDSy46JIh5OQxP1/EXSpoDqetVmyIzL7CYOWnnmMkVqd7YK2QSWkdw==",
+      "requires": {
+        "@vue/compiler-ssr": "3.5.10",
+        "@vue/shared": "3.5.10"
+      }
+    },
+    "@vue/shared": {
+      "version": "3.5.10",
+      "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.10.tgz",
+      "integrity": "sha512-VkkBhU97Ki+XJ0xvl4C9YJsIZ2uIlQ7HqPpZOS3m9VCvmROPaChZU6DexdMJqvz9tbgG+4EtFVrSuailUq5KGQ=="
+    },
+    "acorn": {
+      "version": "8.12.1",
+      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz",
+      "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg=="
+    },
+    "acorn-jsx": {
+      "version": "5.3.2",
+      "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+      "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+      "requires": {}
+    },
+    "ajv": {
+      "version": "6.12.6",
+      "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+      "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+      "requires": {
+        "fast-deep-equal": "^3.1.1",
+        "fast-json-stable-stringify": "^2.0.0",
+        "json-schema-traverse": "^0.4.1",
+        "uri-js": "^4.2.2"
+      }
+    },
+    "ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="
+    },
+    "ansi-styles": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+      "requires": {
+        "color-convert": "^2.0.1"
+      }
+    },
+    "any-promise": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
+      "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
+      "dev": true
+    },
+    "anymatch": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+      "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+      "dev": true,
+      "requires": {
+        "normalize-path": "^3.0.0",
+        "picomatch": "^2.0.4"
+      }
+    },
+    "arg": {
+      "version": "5.0.2",
+      "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
+      "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
+      "dev": true
+    },
+    "argparse": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+      "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
+    },
+    "autoprefixer": {
+      "version": "10.4.20",
+      "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz",
+      "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==",
+      "dev": true,
+      "requires": {
+        "browserslist": "^4.23.3",
+        "caniuse-lite": "^1.0.30001646",
+        "fraction.js": "^4.3.7",
+        "normalize-range": "^0.1.2",
+        "picocolors": "^1.0.1",
+        "postcss-value-parser": "^4.2.0"
+      }
+    },
+    "balanced-match": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+      "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
+    },
+    "binary-extensions": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
+      "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
+      "dev": true
+    },
+    "boolbase": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
+      "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
+      "dev": true
+    },
+    "brace-expansion": {
+      "version": "1.1.11",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+      "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+      "requires": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "braces": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+      "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+      "requires": {
+        "fill-range": "^7.1.1"
+      }
+    },
+    "browserslist": {
+      "version": "4.24.0",
+      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.0.tgz",
+      "integrity": "sha512-Rmb62sR1Zpjql25eSanFGEhAxcFwfA1K0GuQcLoaJBAcENegrQut3hYdhXFF1obQfiDyqIW/cLM5HSJ/9k884A==",
+      "dev": true,
+      "requires": {
+        "caniuse-lite": "^1.0.30001663",
+        "electron-to-chromium": "^1.5.28",
+        "node-releases": "^2.0.18",
+        "update-browserslist-db": "^1.1.0"
+      }
+    },
+    "callsites": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+      "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="
+    },
+    "camelcase-css": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
+      "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
+      "dev": true
+    },
+    "caniuse-lite": {
+      "version": "1.0.30001664",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001664.tgz",
+      "integrity": "sha512-AmE7k4dXiNKQipgn7a2xg558IRqPN3jMQY/rOsbxDhrd0tyChwbITBfiwtnqz8bi2M5mIWbxAYBvk7W7QBUS2g==",
+      "dev": true
+    },
+    "chalk": {
+      "version": "4.1.2",
+      "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+      "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+      "requires": {
+        "ansi-styles": "^4.1.0",
+        "supports-color": "^7.1.0"
+      }
+    },
+    "character-entities": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz",
+      "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="
+    },
+    "chokidar": {
+      "version": "3.5.3",
+      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
+      "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
+      "dev": true,
+      "requires": {
+        "anymatch": "~3.1.2",
+        "braces": "~3.0.2",
+        "fsevents": "~2.3.2",
+        "glob-parent": "~5.1.2",
+        "is-binary-path": "~2.1.0",
+        "is-glob": "~4.0.1",
+        "normalize-path": "~3.0.0",
+        "readdirp": "~3.6.0"
+      },
+      "dependencies": {
+        "glob-parent": {
+          "version": "5.1.2",
+          "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+          "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+          "dev": true,
+          "requires": {
+            "is-glob": "^4.0.1"
+          }
+        }
+      }
+    },
+    "color-convert": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+      "requires": {
+        "color-name": "~1.1.4"
+      }
+    },
+    "color-name": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
+    },
+    "commander": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
+      "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="
+    },
+    "computeds": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/computeds/-/computeds-0.0.1.tgz",
+      "integrity": "sha512-7CEBgcMjVmitjYo5q8JTJVra6X5mQ20uTThdK+0kR7UEaDrAWEQcRiBtWJzga4eRpP6afNwwLsX2SET2JhVB1Q==",
+      "dev": true
+    },
+    "concat-map": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+      "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
+    },
+    "cose-base": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz",
+      "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==",
+      "requires": {
+        "layout-base": "^1.0.0"
+      }
+    },
+    "cross-spawn": {
+      "version": "7.0.3",
+      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
+      "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+      "requires": {
+        "path-key": "^3.1.0",
+        "shebang-command": "^2.0.0",
+        "which": "^2.0.1"
+      }
+    },
+    "cssesc": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
+      "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
+      "dev": true
+    },
+    "csstype": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
+      "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
+    },
+    "cytoscape": {
+      "version": "3.26.0",
+      "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.26.0.tgz",
+      "integrity": "sha512-IV+crL+KBcrCnVVUCZW+zRRRFUZQcrtdOPXki+o4CFUWLdAEYvuZLcBSJC9EBK++suamERKzeY7roq2hdovV3w==",
+      "requires": {
+        "heap": "^0.2.6",
+        "lodash": "^4.17.21"
+      }
+    },
+    "cytoscape-cose-bilkent": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz",
+      "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==",
+      "requires": {
+        "cose-base": "^1.0.0"
+      }
+    },
+    "cytoscape-fcose": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz",
+      "integrity": "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==",
+      "requires": {
+        "cose-base": "^2.2.0"
+      },
+      "dependencies": {
+        "cose-base": {
+          "version": "2.2.0",
+          "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-2.2.0.tgz",
+          "integrity": "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==",
+          "requires": {
+            "layout-base": "^2.0.0"
+          }
+        },
+        "layout-base": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-2.0.1.tgz",
+          "integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg=="
+        }
+      }
+    },
+    "d3": {
+      "version": "7.8.5",
+      "resolved": "https://registry.npmjs.org/d3/-/d3-7.8.5.tgz",
+      "integrity": "sha512-JgoahDG51ncUfJu6wX/1vWQEqOflgXyl4MaHqlcSruTez7yhaRKR9i8VjjcQGeS2en/jnFivXuaIMnseMMt0XA==",
+      "requires": {
+        "d3-array": "3",
+        "d3-axis": "3",
+        "d3-brush": "3",
+        "d3-chord": "3",
+        "d3-color": "3",
+        "d3-contour": "4",
+        "d3-delaunay": "6",
+        "d3-dispatch": "3",
+        "d3-drag": "3",
+        "d3-dsv": "3",
+        "d3-ease": "3",
+        "d3-fetch": "3",
+        "d3-force": "3",
+        "d3-format": "3",
+        "d3-geo": "3",
+        "d3-hierarchy": "3",
+        "d3-interpolate": "3",
+        "d3-path": "3",
+        "d3-polygon": "3",
+        "d3-quadtree": "3",
+        "d3-random": "3",
+        "d3-scale": "4",
+        "d3-scale-chromatic": "3",
+        "d3-selection": "3",
+        "d3-shape": "3",
+        "d3-time": "3",
+        "d3-time-format": "4",
+        "d3-timer": "3",
+        "d3-transition": "3",
+        "d3-zoom": "3"
+      }
+    },
+    "d3-array": {
+      "version": "3.2.4",
+      "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
+      "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
+      "requires": {
+        "internmap": "1 - 2"
+      }
+    },
+    "d3-axis": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz",
+      "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw=="
+    },
+    "d3-brush": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz",
+      "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==",
+      "requires": {
+        "d3-dispatch": "1 - 3",
+        "d3-drag": "2 - 3",
+        "d3-interpolate": "1 - 3",
+        "d3-selection": "3",
+        "d3-transition": "3"
+      }
+    },
+    "d3-chord": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz",
+      "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==",
+      "requires": {
+        "d3-path": "1 - 3"
+      }
+    },
+    "d3-color": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
+      "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="
+    },
+    "d3-contour": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz",
+      "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==",
+      "requires": {
+        "d3-array": "^3.2.0"
+      }
+    },
+    "d3-delaunay": {
+      "version": "6.0.4",
+      "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz",
+      "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==",
+      "requires": {
+        "delaunator": "5"
+      }
+    },
+    "d3-dispatch": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
+      "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg=="
+    },
+    "d3-drag": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz",
+      "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
+      "requires": {
+        "d3-dispatch": "1 - 3",
+        "d3-selection": "3"
+      }
+    },
+    "d3-dsv": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz",
+      "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==",
+      "requires": {
+        "commander": "7",
+        "iconv-lite": "0.6",
+        "rw": "1"
+      }
+    },
+    "d3-ease": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
+      "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w=="
+    },
+    "d3-fetch": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz",
+      "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==",
+      "requires": {
+        "d3-dsv": "1 - 3"
+      }
+    },
+    "d3-force": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz",
+      "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==",
+      "requires": {
+        "d3-dispatch": "1 - 3",
+        "d3-quadtree": "1 - 3",
+        "d3-timer": "1 - 3"
+      }
+    },
+    "d3-format": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz",
+      "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA=="
+    },
+    "d3-geo": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.0.tgz",
+      "integrity": "sha512-JEo5HxXDdDYXCaWdwLRt79y7giK8SbhZJbFWXqbRTolCHFI5jRqteLzCsq51NKbUoX0PjBVSohxrx+NoOUujYA==",
+      "requires": {
+        "d3-array": "2.5.0 - 3"
+      }
+    },
+    "d3-hierarchy": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz",
+      "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA=="
+    },
+    "d3-interpolate": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
+      "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
+      "requires": {
+        "d3-color": "1 - 3"
+      }
+    },
+    "d3-path": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
+      "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ=="
+    },
+    "d3-polygon": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz",
+      "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg=="
+    },
+    "d3-quadtree": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz",
+      "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw=="
+    },
+    "d3-random": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz",
+      "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ=="
+    },
+    "d3-sankey": {
+      "version": "0.12.3",
+      "resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz",
+      "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==",
+      "requires": {
+        "d3-array": "1 - 2",
+        "d3-shape": "^1.2.0"
+      },
+      "dependencies": {
+        "d3-array": {
+          "version": "2.12.1",
+          "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz",
+          "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==",
+          "requires": {
+            "internmap": "^1.0.0"
+          }
+        },
+        "d3-path": {
+          "version": "1.0.9",
+          "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz",
+          "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="
+        },
+        "d3-shape": {
+          "version": "1.3.7",
+          "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz",
+          "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==",
+          "requires": {
+            "d3-path": "1"
+          }
+        },
+        "internmap": {
+          "version": "1.0.1",
+          "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz",
+          "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw=="
+        }
+      }
+    },
+    "d3-scale": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
+      "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
+      "requires": {
+        "d3-array": "2.10.0 - 3",
+        "d3-format": "1 - 3",
+        "d3-interpolate": "1.2.0 - 3",
+        "d3-time": "2.1.1 - 3",
+        "d3-time-format": "2 - 4"
+      }
+    },
+    "d3-scale-chromatic": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz",
+      "integrity": "sha512-Lx9thtxAKrO2Pq6OO2Ua474opeziKr279P/TKZsMAhYyNDD3EnCffdbgeSYN5O7m2ByQsxtuP2CSDczNUIZ22g==",
+      "requires": {
+        "d3-color": "1 - 3",
+        "d3-interpolate": "1 - 3"
+      }
+    },
+    "d3-selection": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
+      "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ=="
+    },
+    "d3-shape": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
+      "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
+      "requires": {
+        "d3-path": "^3.1.0"
+      }
+    },
+    "d3-time": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
+      "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
+      "requires": {
+        "d3-array": "2 - 3"
+      }
+    },
+    "d3-time-format": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
+      "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
+      "requires": {
+        "d3-time": "1 - 3"
+      }
+    },
+    "d3-timer": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
+      "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="
+    },
+    "d3-transition": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz",
+      "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
+      "requires": {
+        "d3-color": "1 - 3",
+        "d3-dispatch": "1 - 3",
+        "d3-ease": "1 - 3",
+        "d3-interpolate": "1 - 3",
+        "d3-timer": "1 - 3"
+      }
+    },
+    "d3-zoom": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz",
+      "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
+      "requires": {
+        "d3-dispatch": "1 - 3",
+        "d3-drag": "2 - 3",
+        "d3-interpolate": "1 - 3",
+        "d3-selection": "2 - 3",
+        "d3-transition": "2 - 3"
+      }
+    },
+    "dagre-d3-es": {
+      "version": "7.0.10",
+      "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.10.tgz",
+      "integrity": "sha512-qTCQmEhcynucuaZgY5/+ti3X/rnszKZhEQH/ZdWdtP1tA/y3VoHJzcVrO9pjjJCNpigfscAtoUB5ONcd2wNn0A==",
+      "requires": {
+        "d3": "^7.8.2",
+        "lodash-es": "^4.17.21"
+      }
+    },
+    "dayjs": {
+      "version": "1.11.9",
+      "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.9.tgz",
+      "integrity": "sha512-QvzAURSbQ0pKdIye2txOzNaHmxtUBXerpY0FJsFXUMKbIZeFm5ht1LS/jFsrncjnmtv8HsG0W2g6c0zUjZWmpA=="
+    },
+    "de-indent": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
+      "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==",
+      "dev": true
+    },
+    "debounce": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz",
+      "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug=="
+    },
+    "debug": {
+      "version": "4.3.4",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+      "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+      "requires": {
+        "ms": "2.1.2"
+      }
+    },
+    "decode-named-character-reference": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz",
+      "integrity": "sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==",
+      "requires": {
+        "character-entities": "^2.0.0"
+      }
+    },
+    "deep-is": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+      "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="
+    },
+    "delaunator": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.0.tgz",
+      "integrity": "sha512-AyLvtyJdbv/U1GkiS6gUUzclRoAY4Gs75qkMygJJhU75LW4DNuSF2RMzpxs9jw9Oz1BobHjTdkG3zdP55VxAqw==",
+      "requires": {
+        "robust-predicates": "^3.0.0"
+      }
+    },
+    "dequal": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
+      "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="
+    },
+    "didyoumean": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
+      "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
+      "dev": true
+    },
+    "diff": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz",
+      "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw=="
+    },
+    "dlv": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
+      "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
+      "dev": true
+    },
+    "dompurify": {
+      "version": "3.1.7",
+      "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.7.tgz",
+      "integrity": "sha512-VaTstWtsneJY8xzy7DekmYWEOZcmzIe3Qb3zPd4STve1OBTa+e+WmS1ITQec1fZYXI3HCsOZZiSMpG6oxoWMWQ=="
+    },
+    "electron-to-chromium": {
+      "version": "1.5.29",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.29.tgz",
+      "integrity": "sha512-PF8n2AlIhCKXQ+gTpiJi0VhcHDb69kYX4MtCiivctc2QD3XuNZ/XIOlbGzt7WAjjEev0TtaH6Cu3arZExm5DOw==",
+      "dev": true
+    },
+    "elkjs": {
+      "version": "0.8.2",
+      "resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.8.2.tgz",
+      "integrity": "sha512-L6uRgvZTH+4OF5NE/MBbzQx/WYpru1xCBE9respNj6qznEewGUIfhzmm7horWWxbNO2M0WckQypGctR8lH79xQ=="
+    },
+    "entities": {
+      "version": "4.5.0",
+      "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
+      "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="
+    },
+    "esbuild": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
+      "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
+      "dev": true,
+      "requires": {
+        "@esbuild/aix-ppc64": "0.21.5",
+        "@esbuild/android-arm": "0.21.5",
+        "@esbuild/android-arm64": "0.21.5",
+        "@esbuild/android-x64": "0.21.5",
+        "@esbuild/darwin-arm64": "0.21.5",
+        "@esbuild/darwin-x64": "0.21.5",
+        "@esbuild/freebsd-arm64": "0.21.5",
+        "@esbuild/freebsd-x64": "0.21.5",
+        "@esbuild/linux-arm": "0.21.5",
+        "@esbuild/linux-arm64": "0.21.5",
+        "@esbuild/linux-ia32": "0.21.5",
+        "@esbuild/linux-loong64": "0.21.5",
+        "@esbuild/linux-mips64el": "0.21.5",
+        "@esbuild/linux-ppc64": "0.21.5",
+        "@esbuild/linux-riscv64": "0.21.5",
+        "@esbuild/linux-s390x": "0.21.5",
+        "@esbuild/linux-x64": "0.21.5",
+        "@esbuild/netbsd-x64": "0.21.5",
+        "@esbuild/openbsd-x64": "0.21.5",
+        "@esbuild/sunos-x64": "0.21.5",
+        "@esbuild/win32-arm64": "0.21.5",
+        "@esbuild/win32-ia32": "0.21.5",
+        "@esbuild/win32-x64": "0.21.5"
+      }
+    },
+    "escalade": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+      "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+      "dev": true
+    },
+    "escape-string-regexp": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+      "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="
+    },
+    "eslint": {
+      "version": "9.11.1",
+      "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.11.1.tgz",
+      "integrity": "sha512-MobhYKIoAO1s1e4VUrgx1l1Sk2JBR/Gqjjgw8+mfgoLE2xwsHur4gdfTxyTgShrhvdVFTaJSgMiQBl1jv/AWxg==",
+      "requires": {
+        "@eslint-community/eslint-utils": "^4.2.0",
+        "@eslint-community/regexpp": "^4.11.0",
+        "@eslint/config-array": "^0.18.0",
+        "@eslint/core": "^0.6.0",
+        "@eslint/eslintrc": "^3.1.0",
+        "@eslint/js": "9.11.1",
+        "@eslint/plugin-kit": "^0.2.0",
+        "@humanwhocodes/module-importer": "^1.0.1",
+        "@humanwhocodes/retry": "^0.3.0",
+        "@nodelib/fs.walk": "^1.2.8",
+        "@types/estree": "^1.0.6",
+        "@types/json-schema": "^7.0.15",
+        "ajv": "^6.12.4",
+        "chalk": "^4.0.0",
+        "cross-spawn": "^7.0.2",
+        "debug": "^4.3.2",
+        "escape-string-regexp": "^4.0.0",
+        "eslint-scope": "^8.0.2",
+        "eslint-visitor-keys": "^4.0.0",
+        "espree": "^10.1.0",
+        "esquery": "^1.5.0",
+        "esutils": "^2.0.2",
+        "fast-deep-equal": "^3.1.3",
+        "file-entry-cache": "^8.0.0",
+        "find-up": "^5.0.0",
+        "glob-parent": "^6.0.2",
+        "ignore": "^5.2.0",
+        "imurmurhash": "^0.1.4",
+        "is-glob": "^4.0.0",
+        "is-path-inside": "^3.0.3",
+        "json-stable-stringify-without-jsonify": "^1.0.1",
+        "lodash.merge": "^4.6.2",
+        "minimatch": "^3.1.2",
+        "natural-compare": "^1.4.0",
+        "optionator": "^0.9.3",
+        "strip-ansi": "^6.0.1",
+        "text-table": "^0.2.0"
+      },
+      "dependencies": {
+        "eslint-scope": {
+          "version": "8.1.0",
+          "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.1.0.tgz",
+          "integrity": "sha512-14dSvlhaVhKKsa9Fx1l8A17s7ah7Ef7wCakJ10LYk6+GYmP9yDti2oq2SEwcyndt6knfcZyhyxwY3i9yL78EQw==",
+          "requires": {
+            "esrecurse": "^4.3.0",
+            "estraverse": "^5.2.0"
+          }
+        },
+        "eslint-visitor-keys": {
+          "version": "4.1.0",
+          "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.1.0.tgz",
+          "integrity": "sha512-Q7lok0mqMUSf5a/AdAZkA5a/gHcO6snwQClVNNvFKCAVlxXucdU8pKydU5ZVZjBx5xr37vGbFFWtLQYreLzrZg=="
+        },
+        "espree": {
+          "version": "10.2.0",
+          "resolved": "https://registry.npmjs.org/espree/-/espree-10.2.0.tgz",
+          "integrity": "sha512-upbkBJbckcCNBDBDXEbuhjbP68n+scUd3k/U2EkyM9nw+I/jPiL4cLF/Al06CF96wRltFda16sxDFrxsI1v0/g==",
+          "requires": {
+            "acorn": "^8.12.0",
+            "acorn-jsx": "^5.3.2",
+            "eslint-visitor-keys": "^4.1.0"
+          }
+        }
+      }
+    },
+    "eslint-config-prettier": {
+      "version": "9.1.0",
+      "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz",
+      "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==",
+      "dev": true,
+      "requires": {}
+    },
+    "eslint-plugin-vue": {
+      "version": "9.28.0",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.28.0.tgz",
+      "integrity": "sha512-ShrihdjIhOTxs+MfWun6oJWuk+g/LAhN+CiuOl/jjkG3l0F2AuK5NMTaWqyvBgkFtpYmyks6P4603mLmhNJW8g==",
+      "dev": true,
+      "requires": {
+        "@eslint-community/eslint-utils": "^4.4.0",
+        "globals": "^13.24.0",
+        "natural-compare": "^1.4.0",
+        "nth-check": "^2.1.1",
+        "postcss-selector-parser": "^6.0.15",
+        "semver": "^7.6.3",
+        "vue-eslint-parser": "^9.4.3",
+        "xml-name-validator": "^4.0.0"
+      }
+    },
+    "eslint-scope": {
+      "version": "7.2.2",
+      "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
+      "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==",
+      "dev": true,
+      "requires": {
+        "esrecurse": "^4.3.0",
+        "estraverse": "^5.2.0"
+      }
+    },
+    "eslint-visitor-keys": {
+      "version": "3.4.3",
+      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+      "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="
+    },
+    "espree": {
+      "version": "9.6.1",
+      "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
+      "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==",
+      "dev": true,
+      "requires": {
+        "acorn": "^8.9.0",
+        "acorn-jsx": "^5.3.2",
+        "eslint-visitor-keys": "^3.4.1"
+      }
+    },
+    "esquery": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz",
+      "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==",
+      "requires": {
+        "estraverse": "^5.1.0"
+      }
+    },
+    "esrecurse": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+      "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+      "requires": {
+        "estraverse": "^5.2.0"
+      }
+    },
+    "estraverse": {
+      "version": "5.3.0",
+      "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+      "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="
+    },
+    "estree-walker": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
+      "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="
+    },
+    "esutils": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+      "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="
+    },
+    "fast-deep-equal": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+      "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
+    },
+    "fast-glob": {
+      "version": "3.3.2",
+      "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
+      "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==",
+      "requires": {
+        "@nodelib/fs.stat": "^2.0.2",
+        "@nodelib/fs.walk": "^1.2.3",
+        "glob-parent": "^5.1.2",
+        "merge2": "^1.3.0",
+        "micromatch": "^4.0.4"
+      },
+      "dependencies": {
+        "glob-parent": {
+          "version": "5.1.2",
+          "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+          "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+          "requires": {
+            "is-glob": "^4.0.1"
+          }
+        }
+      }
+    },
+    "fast-json-stable-stringify": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+      "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="
+    },
+    "fast-levenshtein": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+      "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="
+    },
+    "fastq": {
+      "version": "1.15.0",
+      "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz",
+      "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==",
+      "requires": {
+        "reusify": "^1.0.4"
+      }
+    },
+    "file-entry-cache": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
+      "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
+      "requires": {
+        "flat-cache": "^4.0.0"
+      }
+    },
+    "fill-range": {
+      "version": "7.1.1",
+      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+      "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+      "requires": {
+        "to-regex-range": "^5.0.1"
+      }
+    },
+    "find-up": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+      "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+      "requires": {
+        "locate-path": "^6.0.0",
+        "path-exists": "^4.0.0"
+      }
+    },
+    "flat-cache": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
+      "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
+      "requires": {
+        "flatted": "^3.2.9",
+        "keyv": "^4.5.4"
+      }
+    },
+    "flatted": {
+      "version": "3.3.1",
+      "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz",
+      "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw=="
+    },
+    "fraction.js": {
+      "version": "4.3.7",
+      "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
+      "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==",
+      "dev": true
+    },
+    "fs.realpath": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+      "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
+      "dev": true
+    },
+    "fsevents": {
+      "version": "2.3.3",
+      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+      "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+      "dev": true,
+      "optional": true
+    },
+    "function-bind": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
+      "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
+      "dev": true
+    },
+    "glob-parent": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+      "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+      "requires": {
+        "is-glob": "^4.0.3"
+      }
+    },
+    "globals": {
+      "version": "13.24.0",
+      "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz",
+      "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==",
+      "dev": true,
+      "requires": {
+        "type-fest": "^0.20.2"
+      }
+    },
+    "graphemer": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
+      "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="
+    },
+    "has": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
+      "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
+      "dev": true,
+      "requires": {
+        "function-bind": "^1.1.1"
+      }
+    },
+    "has-flag": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
+    },
+    "he": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
+      "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
+      "dev": true
+    },
+    "heap": {
+      "version": "0.2.7",
+      "resolved": "https://registry.npmjs.org/heap/-/heap-0.2.7.tgz",
+      "integrity": "sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg=="
+    },
+    "iconv-lite": {
+      "version": "0.6.3",
+      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+      "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+      "requires": {
+        "safer-buffer": ">= 2.1.2 < 3.0.0"
+      }
+    },
+    "ignore": {
+      "version": "5.3.2",
+      "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+      "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="
+    },
+    "import-fresh": {
+      "version": "3.3.0",
+      "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
+      "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
+      "requires": {
+        "parent-module": "^1.0.0",
+        "resolve-from": "^4.0.0"
+      }
+    },
+    "imurmurhash": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+      "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="
+    },
+    "inflight": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+      "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+      "dev": true,
+      "requires": {
+        "once": "^1.3.0",
+        "wrappy": "1"
+      }
+    },
+    "inherits": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+      "dev": true
+    },
+    "internmap": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
+      "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="
+    },
+    "is-binary-path": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+      "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+      "dev": true,
+      "requires": {
+        "binary-extensions": "^2.0.0"
+      }
+    },
+    "is-core-module": {
+      "version": "2.13.0",
+      "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz",
+      "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==",
+      "dev": true,
+      "requires": {
+        "has": "^1.0.3"
+      }
+    },
+    "is-extglob": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+      "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="
+    },
+    "is-glob": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+      "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+      "requires": {
+        "is-extglob": "^2.1.1"
+      }
+    },
+    "is-number": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+      "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="
+    },
+    "is-path-inside": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
+      "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ=="
+    },
+    "isexe": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+      "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
+    },
+    "jiti": {
+      "version": "1.21.6",
+      "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz",
+      "integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==",
+      "devOptional": true
+    },
+    "js-yaml": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+      "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+      "requires": {
+        "argparse": "^2.0.1"
+      }
+    },
+    "json-buffer": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+      "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="
+    },
+    "json-schema-traverse": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+      "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
+    },
+    "json-stable-stringify-without-jsonify": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+      "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="
+    },
+    "keyv": {
+      "version": "4.5.4",
+      "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+      "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+      "requires": {
+        "json-buffer": "3.0.1"
+      }
+    },
+    "khroma": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.0.0.tgz",
+      "integrity": "sha512-2J8rDNlQWbtiNYThZRvmMv5yt44ZakX+Tz5ZIp/mN1pt4snn+m030Va5Z4v8xA0cQFDXBwO/8i42xL4QPsVk3g=="
+    },
+    "kleur": {
+      "version": "4.1.5",
+      "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
+      "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="
+    },
+    "layout-base": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz",
+      "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg=="
+    },
+    "leaflet": {
+      "version": "1.9.4",
+      "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
+      "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA=="
+    },
+    "levn": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+      "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+      "requires": {
+        "prelude-ls": "^1.2.1",
+        "type-check": "~0.4.0"
+      }
+    },
+    "lilconfig": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz",
+      "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==",
+      "dev": true
+    },
+    "lines-and-columns": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+      "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
+      "dev": true
+    },
+    "locate-path": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+      "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+      "requires": {
+        "p-locate": "^5.0.0"
+      }
+    },
+    "lodash": {
+      "version": "4.17.21",
+      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+      "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
+    },
+    "lodash-es": {
+      "version": "4.17.21",
+      "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
+      "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="
+    },
+    "lodash.merge": {
+      "version": "4.6.2",
+      "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+      "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="
+    },
+    "magic-string": {
+      "version": "0.30.11",
+      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz",
+      "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==",
+      "requires": {
+        "@jridgewell/sourcemap-codec": "^1.5.0"
+      }
+    },
+    "mdast-util-from-markdown": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.1.tgz",
+      "integrity": "sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww==",
+      "requires": {
+        "@types/mdast": "^3.0.0",
+        "@types/unist": "^2.0.0",
+        "decode-named-character-reference": "^1.0.0",
+        "mdast-util-to-string": "^3.1.0",
+        "micromark": "^3.0.0",
+        "micromark-util-decode-numeric-character-reference": "^1.0.0",
+        "micromark-util-decode-string": "^1.0.0",
+        "micromark-util-normalize-identifier": "^1.0.0",
+        "micromark-util-symbol": "^1.0.0",
+        "micromark-util-types": "^1.0.0",
+        "unist-util-stringify-position": "^3.0.0",
+        "uvu": "^0.5.0"
+      }
+    },
+    "mdast-util-to-string": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz",
+      "integrity": "sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==",
+      "requires": {
+        "@types/mdast": "^3.0.0"
+      }
+    },
+    "merge2": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+      "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="
+    },
+    "mermaid": {
+      "version": "10.5.0",
+      "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-10.5.0.tgz",
+      "integrity": "sha512-9l0o1uUod78D3/FVYPGSsgV+Z0tSnzLBDiC9rVzvelPxuO80HbN1oDr9ofpPETQy9XpypPQa26fr09VzEPfvWA==",
+      "requires": {
+        "@braintree/sanitize-url": "^6.0.1",
+        "@types/d3-scale": "^4.0.3",
+        "@types/d3-scale-chromatic": "^3.0.0",
+        "cytoscape": "^3.23.0",
+        "cytoscape-cose-bilkent": "^4.1.0",
+        "cytoscape-fcose": "^2.1.0",
+        "d3": "^7.4.0",
+        "d3-sankey": "^0.12.3",
+        "dagre-d3-es": "7.0.10",
+        "dayjs": "^1.11.7",
+        "dompurify": "^3.0.5",
+        "elkjs": "^0.8.2",
+        "khroma": "^2.0.0",
+        "lodash-es": "^4.17.21",
+        "mdast-util-from-markdown": "^1.3.0",
+        "non-layered-tidy-tree-layout": "^2.0.2",
+        "stylis": "^4.1.3",
+        "ts-dedent": "^2.2.0",
+        "uuid": "^9.0.0",
+        "web-worker": "^1.2.0"
+      }
+    },
+    "micromark": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/micromark/-/micromark-3.2.0.tgz",
+      "integrity": "sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA==",
+      "requires": {
+        "@types/debug": "^4.0.0",
+        "debug": "^4.0.0",
+        "decode-named-character-reference": "^1.0.0",
+        "micromark-core-commonmark": "^1.0.1",
+        "micromark-factory-space": "^1.0.0",
+        "micromark-util-character": "^1.0.0",
+        "micromark-util-chunked": "^1.0.0",
+        "micromark-util-combine-extensions": "^1.0.0",
+        "micromark-util-decode-numeric-character-reference": "^1.0.0",
+        "micromark-util-encode": "^1.0.0",
+        "micromark-util-normalize-identifier": "^1.0.0",
+        "micromark-util-resolve-all": "^1.0.0",
+        "micromark-util-sanitize-uri": "^1.0.0",
+        "micromark-util-subtokenize": "^1.0.0",
+        "micromark-util-symbol": "^1.0.0",
+        "micromark-util-types": "^1.0.1",
+        "uvu": "^0.5.0"
+      }
+    },
+    "micromark-core-commonmark": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-1.1.0.tgz",
+      "integrity": "sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw==",
+      "requires": {
+        "decode-named-character-reference": "^1.0.0",
+        "micromark-factory-destination": "^1.0.0",
+        "micromark-factory-label": "^1.0.0",
+        "micromark-factory-space": "^1.0.0",
+        "micromark-factory-title": "^1.0.0",
+        "micromark-factory-whitespace": "^1.0.0",
+        "micromark-util-character": "^1.0.0",
+        "micromark-util-chunked": "^1.0.0",
+        "micromark-util-classify-character": "^1.0.0",
+        "micromark-util-html-tag-name": "^1.0.0",
+        "micromark-util-normalize-identifier": "^1.0.0",
+        "micromark-util-resolve-all": "^1.0.0",
+        "micromark-util-subtokenize": "^1.0.0",
+        "micromark-util-symbol": "^1.0.0",
+        "micromark-util-types": "^1.0.1",
+        "uvu": "^0.5.0"
+      }
+    },
+    "micromark-factory-destination": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-1.1.0.tgz",
+      "integrity": "sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg==",
+      "requires": {
+        "micromark-util-character": "^1.0.0",
+        "micromark-util-symbol": "^1.0.0",
+        "micromark-util-types": "^1.0.0"
+      }
+    },
+    "micromark-factory-label": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-1.1.0.tgz",
+      "integrity": "sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w==",
+      "requires": {
+        "micromark-util-character": "^1.0.0",
+        "micromark-util-symbol": "^1.0.0",
+        "micromark-util-types": "^1.0.0",
+        "uvu": "^0.5.0"
+      }
+    },
+    "micromark-factory-space": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-1.1.0.tgz",
+      "integrity": "sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ==",
+      "requires": {
+        "micromark-util-character": "^1.0.0",
+        "micromark-util-types": "^1.0.0"
+      }
+    },
+    "micromark-factory-title": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-1.1.0.tgz",
+      "integrity": "sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ==",
+      "requires": {
+        "micromark-factory-space": "^1.0.0",
+        "micromark-util-character": "^1.0.0",
+        "micromark-util-symbol": "^1.0.0",
+        "micromark-util-types": "^1.0.0"
+      }
+    },
+    "micromark-factory-whitespace": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-1.1.0.tgz",
+      "integrity": "sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ==",
+      "requires": {
+        "micromark-factory-space": "^1.0.0",
+        "micromark-util-character": "^1.0.0",
+        "micromark-util-symbol": "^1.0.0",
+        "micromark-util-types": "^1.0.0"
+      }
+    },
+    "micromark-util-character": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.2.0.tgz",
+      "integrity": "sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==",
+      "requires": {
+        "micromark-util-symbol": "^1.0.0",
+        "micromark-util-types": "^1.0.0"
+      }
+    },
+    "micromark-util-chunked": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-1.1.0.tgz",
+      "integrity": "sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ==",
+      "requires": {
+        "micromark-util-symbol": "^1.0.0"
+      }
+    },
+    "micromark-util-classify-character": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-1.1.0.tgz",
+      "integrity": "sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw==",
+      "requires": {
+        "micromark-util-character": "^1.0.0",
+        "micromark-util-symbol": "^1.0.0",
+        "micromark-util-types": "^1.0.0"
+      }
+    },
+    "micromark-util-combine-extensions": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.1.0.tgz",
+      "integrity": "sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA==",
+      "requires": {
+        "micromark-util-chunked": "^1.0.0",
+        "micromark-util-types": "^1.0.0"
+      }
+    },
+    "micromark-util-decode-numeric-character-reference": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.1.0.tgz",
+      "integrity": "sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw==",
+      "requires": {
+        "micromark-util-symbol": "^1.0.0"
+      }
+    },
+    "micromark-util-decode-string": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-1.1.0.tgz",
+      "integrity": "sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ==",
+      "requires": {
+        "decode-named-character-reference": "^1.0.0",
+        "micromark-util-character": "^1.0.0",
+        "micromark-util-decode-numeric-character-reference": "^1.0.0",
+        "micromark-util-symbol": "^1.0.0"
+      }
+    },
+    "micromark-util-encode": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-1.1.0.tgz",
+      "integrity": "sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw=="
+    },
+    "micromark-util-html-tag-name": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.2.0.tgz",
+      "integrity": "sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q=="
+    },
+    "micromark-util-normalize-identifier": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.1.0.tgz",
+      "integrity": "sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q==",
+      "requires": {
+        "micromark-util-symbol": "^1.0.0"
+      }
+    },
+    "micromark-util-resolve-all": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-1.1.0.tgz",
+      "integrity": "sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA==",
+      "requires": {
+        "micromark-util-types": "^1.0.0"
+      }
+    },
+    "micromark-util-sanitize-uri": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.2.0.tgz",
+      "integrity": "sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A==",
+      "requires": {
+        "micromark-util-character": "^1.0.0",
+        "micromark-util-encode": "^1.0.0",
+        "micromark-util-symbol": "^1.0.0"
+      }
+    },
+    "micromark-util-subtokenize": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-1.1.0.tgz",
+      "integrity": "sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A==",
+      "requires": {
+        "micromark-util-chunked": "^1.0.0",
+        "micromark-util-symbol": "^1.0.0",
+        "micromark-util-types": "^1.0.0",
+        "uvu": "^0.5.0"
+      }
+    },
+    "micromark-util-symbol": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz",
+      "integrity": "sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag=="
+    },
+    "micromark-util-types": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz",
+      "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg=="
+    },
+    "micromatch": {
+      "version": "4.0.8",
+      "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+      "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+      "requires": {
+        "braces": "^3.0.3",
+        "picomatch": "^2.3.1"
+      }
+    },
+    "minimatch": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+      "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+      "requires": {
+        "brace-expansion": "^1.1.7"
+      }
+    },
+    "mri": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
+      "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="
+    },
+    "ms": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+      "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+    },
+    "muggle-string": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz",
+      "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==",
+      "dev": true
+    },
+    "mz": {
+      "version": "2.7.0",
+      "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
+      "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
+      "dev": true,
+      "requires": {
+        "any-promise": "^1.0.0",
+        "object-assign": "^4.0.1",
+        "thenify-all": "^1.0.0"
+      }
+    },
+    "nanoid": {
+      "version": "3.3.7",
+      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
+      "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g=="
+    },
+    "natural-compare": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+      "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="
+    },
+    "node-releases": {
+      "version": "2.0.18",
+      "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz",
+      "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==",
+      "dev": true
+    },
+    "non-layered-tidy-tree-layout": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/non-layered-tidy-tree-layout/-/non-layered-tidy-tree-layout-2.0.2.tgz",
+      "integrity": "sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw=="
+    },
+    "normalize-path": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+      "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+      "dev": true
+    },
+    "normalize-range": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz",
+      "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==",
+      "dev": true
+    },
+    "nth-check": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
+      "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
+      "dev": true,
+      "requires": {
+        "boolbase": "^1.0.0"
+      }
+    },
+    "object-assign": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+      "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+      "dev": true
+    },
+    "object-hash": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
+      "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
+      "dev": true
+    },
+    "once": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+      "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+      "dev": true,
+      "requires": {
+        "wrappy": "1"
+      }
+    },
+    "optionator": {
+      "version": "0.9.3",
+      "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz",
+      "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==",
+      "requires": {
+        "@aashutoshrathi/word-wrap": "^1.2.3",
+        "deep-is": "^0.1.3",
+        "fast-levenshtein": "^2.0.6",
+        "levn": "^0.4.1",
+        "prelude-ls": "^1.2.1",
+        "type-check": "^0.4.0"
+      }
+    },
+    "p-limit": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+      "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+      "requires": {
+        "yocto-queue": "^0.1.0"
+      }
+    },
+    "p-locate": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+      "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+      "requires": {
+        "p-limit": "^3.0.2"
+      }
+    },
+    "parent-module": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+      "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+      "requires": {
+        "callsites": "^3.0.0"
+      }
+    },
+    "path-browserify": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
+      "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
+      "dev": true
+    },
+    "path-exists": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+      "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="
+    },
+    "path-is-absolute": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+      "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+      "dev": true
+    },
+    "path-key": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+      "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="
+    },
+    "path-parse": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+      "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+      "dev": true
+    },
+    "picocolors": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz",
+      "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw=="
+    },
+    "picomatch": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+      "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="
+    },
+    "pify": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+      "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
+      "dev": true
+    },
+    "pinia": {
+      "version": "2.2.2",
+      "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.2.2.tgz",
+      "integrity": "sha512-ja2XqFWZC36mupU4z1ZzxeTApV7DOw44cV4dhQ9sGwun+N89v/XP7+j7q6TanS1u1tdbK4r+1BUx7heMaIdagA==",
+      "requires": {
+        "@vue/devtools-api": "^6.6.3",
+        "vue-demi": "^0.14.10"
+      },
+      "dependencies": {
+        "vue-demi": {
+          "version": "0.14.10",
+          "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
+          "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
+          "requires": {}
+        }
+      }
+    },
+    "pirates": {
+      "version": "4.0.6",
+      "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz",
+      "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==",
+      "dev": true
+    },
+    "postcss": {
+      "version": "8.4.47",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz",
+      "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==",
+      "requires": {
+        "nanoid": "^3.3.7",
+        "picocolors": "^1.1.0",
+        "source-map-js": "^1.2.1"
+      }
+    },
+    "postcss-import": {
+      "version": "15.1.0",
+      "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
+      "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
+      "dev": true,
+      "requires": {
+        "postcss-value-parser": "^4.0.0",
+        "read-cache": "^1.0.0",
+        "resolve": "^1.1.7"
+      }
+    },
+    "postcss-js": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz",
+      "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==",
+      "dev": true,
+      "requires": {
+        "camelcase-css": "^2.0.1"
+      }
+    },
+    "postcss-load-config": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.1.tgz",
+      "integrity": "sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==",
+      "dev": true,
+      "requires": {
+        "lilconfig": "^2.0.5",
+        "yaml": "^2.1.1"
+      }
+    },
+    "postcss-nested": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz",
+      "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==",
+      "dev": true,
+      "requires": {
+        "postcss-selector-parser": "^6.0.11"
+      }
+    },
+    "postcss-selector-parser": {
+      "version": "6.1.2",
+      "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
+      "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
+      "dev": true,
+      "requires": {
+        "cssesc": "^3.0.0",
+        "util-deprecate": "^1.0.2"
+      }
+    },
+    "postcss-value-parser": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
+      "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
+      "dev": true
+    },
+    "prelude-ls": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+      "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="
+    },
+    "punycode": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+      "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="
+    },
+    "queue-microtask": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+      "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="
+    },
+    "read-cache": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
+      "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
+      "dev": true,
+      "requires": {
+        "pify": "^2.3.0"
+      }
+    },
+    "readdirp": {
+      "version": "3.6.0",
+      "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+      "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+      "dev": true,
+      "requires": {
+        "picomatch": "^2.2.1"
+      }
+    },
+    "resolve": {
+      "version": "1.22.4",
+      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.4.tgz",
+      "integrity": "sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg==",
+      "dev": true,
+      "requires": {
+        "is-core-module": "^2.13.0",
+        "path-parse": "^1.0.7",
+        "supports-preserve-symlinks-flag": "^1.0.0"
+      }
+    },
+    "resolve-from": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+      "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="
+    },
+    "reusify": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
+      "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw=="
+    },
+    "robust-predicates": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz",
+      "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg=="
+    },
+    "rollup": {
+      "version": "4.22.5",
+      "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.22.5.tgz",
+      "integrity": "sha512-WoinX7GeQOFMGznEcWA1WrTQCd/tpEbMkc3nuMs9BT0CPjMdSjPMTVClwWd4pgSQwJdP65SK9mTCNvItlr5o7w==",
+      "dev": true,
+      "requires": {
+        "@rollup/rollup-android-arm-eabi": "4.22.5",
+        "@rollup/rollup-android-arm64": "4.22.5",
+        "@rollup/rollup-darwin-arm64": "4.22.5",
+        "@rollup/rollup-darwin-x64": "4.22.5",
+        "@rollup/rollup-linux-arm-gnueabihf": "4.22.5",
+        "@rollup/rollup-linux-arm-musleabihf": "4.22.5",
+        "@rollup/rollup-linux-arm64-gnu": "4.22.5",
+        "@rollup/rollup-linux-arm64-musl": "4.22.5",
+        "@rollup/rollup-linux-powerpc64le-gnu": "4.22.5",
+        "@rollup/rollup-linux-riscv64-gnu": "4.22.5",
+        "@rollup/rollup-linux-s390x-gnu": "4.22.5",
+        "@rollup/rollup-linux-x64-gnu": "4.22.5",
+        "@rollup/rollup-linux-x64-musl": "4.22.5",
+        "@rollup/rollup-win32-arm64-msvc": "4.22.5",
+        "@rollup/rollup-win32-ia32-msvc": "4.22.5",
+        "@rollup/rollup-win32-x64-msvc": "4.22.5",
+        "@types/estree": "1.0.6",
+        "fsevents": "~2.3.2"
+      }
+    },
+    "run-parallel": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+      "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+      "requires": {
+        "queue-microtask": "^1.2.2"
+      }
+    },
+    "rw": {
+      "version": "1.3.3",
+      "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz",
+      "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ=="
+    },
+    "sade": {
+      "version": "1.8.1",
+      "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz",
+      "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==",
+      "requires": {
+        "mri": "^1.1.0"
+      }
+    },
+    "safer-buffer": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+      "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
+    },
+    "semver": {
+      "version": "7.6.3",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
+      "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="
+    },
+    "shebang-command": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+      "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+      "requires": {
+        "shebang-regex": "^3.0.0"
+      }
+    },
+    "shebang-regex": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+      "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="
+    },
+    "source-map-js": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+      "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="
+    },
+    "strip-ansi": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "requires": {
+        "ansi-regex": "^5.0.1"
+      }
+    },
+    "strip-json-comments": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+      "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="
+    },
+    "stylis": {
+      "version": "4.1.3",
+      "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.1.3.tgz",
+      "integrity": "sha512-GP6WDNWf+o403jrEp9c5jibKavrtLW+/qYGhFxFrG8maXhwTBI7gLLhiBb0o7uFccWN+EOS9aMO6cGHWAO07OA=="
+    },
+    "sucrase": {
+      "version": "3.34.0",
+      "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.34.0.tgz",
+      "integrity": "sha512-70/LQEZ07TEcxiU2dz51FKaE6hCTWC6vr7FOk3Gr0U60C3shtAN+H+BFr9XlYe5xqf3RA8nrc+VIwzCfnxuXJw==",
+      "dev": true,
+      "requires": {
+        "@jridgewell/gen-mapping": "^0.3.2",
+        "commander": "^4.0.0",
+        "glob": "7.1.6",
+        "lines-and-columns": "^1.1.6",
+        "mz": "^2.7.0",
+        "pirates": "^4.0.1",
+        "ts-interface-checker": "^0.1.9"
+      },
+      "dependencies": {
+        "commander": {
+          "version": "4.1.1",
+          "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
+          "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
+          "dev": true
+        },
+        "glob": {
+          "version": "7.1.6",
+          "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
+          "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
+          "dev": true,
+          "requires": {
+            "fs.realpath": "^1.0.0",
+            "inflight": "^1.0.4",
+            "inherits": "2",
+            "minimatch": "^3.0.4",
+            "once": "^1.3.0",
+            "path-is-absolute": "^1.0.0"
+          }
+        }
+      }
+    },
+    "supports-color": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+      "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+      "requires": {
+        "has-flag": "^4.0.0"
+      }
+    },
+    "supports-preserve-symlinks-flag": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+      "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+      "dev": true
+    },
+    "tailwindcss": {
+      "version": "3.4.13",
+      "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.13.tgz",
+      "integrity": "sha512-KqjHOJKogOUt5Bs752ykCeiwvi0fKVkr5oqsFNt/8px/tA8scFPIlkygsf6jXrfCqGHz7VflA6+yytWuM+XhFw==",
+      "dev": true,
+      "requires": {
+        "@alloc/quick-lru": "^5.2.0",
+        "arg": "^5.0.2",
+        "chokidar": "^3.5.3",
+        "didyoumean": "^1.2.2",
+        "dlv": "^1.1.3",
+        "fast-glob": "^3.3.0",
+        "glob-parent": "^6.0.2",
+        "is-glob": "^4.0.3",
+        "jiti": "^1.21.0",
+        "lilconfig": "^2.1.0",
+        "micromatch": "^4.0.5",
+        "normalize-path": "^3.0.0",
+        "object-hash": "^3.0.0",
+        "picocolors": "^1.0.0",
+        "postcss": "^8.4.23",
+        "postcss-import": "^15.1.0",
+        "postcss-js": "^4.0.1",
+        "postcss-load-config": "^4.0.1",
+        "postcss-nested": "^6.0.1",
+        "postcss-selector-parser": "^6.0.11",
+        "resolve": "^1.22.2",
+        "sucrase": "^3.32.0"
+      }
+    },
+    "text-table": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
+      "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw=="
+    },
+    "thenify": {
+      "version": "3.3.1",
+      "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
+      "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
+      "dev": true,
+      "requires": {
+        "any-promise": "^1.0.0"
+      }
+    },
+    "thenify-all": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
+      "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
+      "dev": true,
+      "requires": {
+        "thenify": ">= 3.1.0 < 4"
+      }
+    },
+    "to-fast-properties": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
+      "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog=="
+    },
+    "to-regex-range": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+      "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+      "requires": {
+        "is-number": "^7.0.0"
+      }
+    },
+    "ts-api-utils": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz",
+      "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==",
+      "requires": {}
+    },
+    "ts-dedent": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz",
+      "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ=="
+    },
+    "ts-interface-checker": {
+      "version": "0.1.13",
+      "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
+      "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
+      "dev": true
+    },
+    "type-check": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+      "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+      "requires": {
+        "prelude-ls": "^1.2.1"
+      }
+    },
+    "type-fest": {
+      "version": "0.20.2",
+      "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
+      "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
+      "dev": true
+    },
+    "typescript": {
+      "version": "5.6.2",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz",
+      "integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw=="
+    },
+    "typescript-eslint": {
+      "version": "8.7.0",
+      "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.7.0.tgz",
+      "integrity": "sha512-nEHbEYJyHwsuf7c3V3RS7Saq+1+la3i0ieR3qP0yjqWSzVmh8Drp47uOl9LjbPANac4S7EFSqvcYIKXUUwIfIQ==",
+      "requires": {
+        "@typescript-eslint/eslint-plugin": "8.7.0",
+        "@typescript-eslint/parser": "8.7.0",
+        "@typescript-eslint/utils": "8.7.0"
+      }
+    },
+    "unist-util-stringify-position": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz",
+      "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==",
+      "requires": {
+        "@types/unist": "^2.0.0"
+      }
+    },
+    "update-browserslist-db": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz",
+      "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==",
+      "dev": true,
+      "requires": {
+        "escalade": "^3.2.0",
+        "picocolors": "^1.1.0"
+      }
+    },
+    "uri-js": {
+      "version": "4.4.1",
+      "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+      "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+      "requires": {
+        "punycode": "^2.1.0"
+      }
+    },
+    "util-deprecate": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+      "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+      "dev": true
+    },
+    "uuid": {
+      "version": "9.0.0",
+      "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz",
+      "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg=="
+    },
+    "uvu": {
+      "version": "0.5.6",
+      "resolved": "https://registry.npmjs.org/uvu/-/uvu-0.5.6.tgz",
+      "integrity": "sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==",
+      "requires": {
+        "dequal": "^2.0.0",
+        "diff": "^5.0.0",
+        "kleur": "^4.0.3",
+        "sade": "^1.7.3"
+      }
+    },
+    "vite": {
+      "version": "5.4.8",
+      "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.8.tgz",
+      "integrity": "sha512-FqrItQ4DT1NC4zCUqMB4c4AZORMKIa0m8/URVCZ77OZ/QSNeJ54bU1vrFADbDsuwfIPcgknRkmqakQcgnL4GiQ==",
+      "dev": true,
+      "requires": {
+        "esbuild": "^0.21.3",
+        "fsevents": "~2.3.3",
+        "postcss": "^8.4.43",
+        "rollup": "^4.20.0"
+      }
+    },
+    "vscode-uri": {
+      "version": "3.0.8",
+      "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz",
+      "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==",
+      "dev": true
+    },
+    "vue": {
+      "version": "3.5.10",
+      "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.10.tgz",
+      "integrity": "sha512-Vy2kmJwHPlouC/tSnIgXVg03SG+9wSqT1xu1Vehc+ChsXsRd7jLkKgMltVEFOzUdBr3uFwBCG+41LJtfAcBRng==",
+      "requires": {
+        "@vue/compiler-dom": "3.5.10",
+        "@vue/compiler-sfc": "3.5.10",
+        "@vue/runtime-dom": "3.5.10",
+        "@vue/server-renderer": "3.5.10",
+        "@vue/shared": "3.5.10"
+      }
+    },
+    "vue-eslint-parser": {
+      "version": "9.4.3",
+      "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.4.3.tgz",
+      "integrity": "sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==",
+      "dev": true,
+      "requires": {
+        "debug": "^4.3.4",
+        "eslint-scope": "^7.1.1",
+        "eslint-visitor-keys": "^3.3.0",
+        "espree": "^9.3.1",
+        "esquery": "^1.4.0",
+        "lodash": "^4.17.21",
+        "semver": "^7.3.6"
+      }
+    },
+    "vue-router": {
+      "version": "4.4.5",
+      "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.4.5.tgz",
+      "integrity": "sha512-4fKZygS8cH1yCyuabAXGUAsyi1b2/o/OKgu/RUb+znIYOxPRxdkytJEx+0wGcpBE1pX6vUgh5jwWOKRGvuA/7Q==",
+      "requires": {
+        "@vue/devtools-api": "^6.6.4"
+      }
+    },
+    "vue-toastification": {
+      "version": "2.0.0-rc.5",
+      "resolved": "https://registry.npmjs.org/vue-toastification/-/vue-toastification-2.0.0-rc.5.tgz",
+      "integrity": "sha512-q73e5jy6gucEO/U+P48hqX+/qyXDozAGmaGgLFm5tXX4wJBcVsnGp4e/iJqlm9xzHETYOilUuwOUje2Qg1JdwA==",
+      "requires": {}
+    },
+    "vue-tsc": {
+      "version": "2.1.6",
+      "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.1.6.tgz",
+      "integrity": "sha512-f98dyZp5FOukcYmbFpuSCJ4Z0vHSOSmxGttZJCsFeX0M4w/Rsq0s4uKXjcSRsZqsRgQa6z7SfuO+y0HVICE57Q==",
+      "dev": true,
+      "requires": {
+        "@volar/typescript": "~2.4.1",
+        "@vue/language-core": "2.1.6",
+        "semver": "^7.5.4"
+      }
+    },
+    "vue3-popper": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/vue3-popper/-/vue3-popper-1.5.0.tgz",
+      "integrity": "sha512-xaEnx90YBnlSg5G2yWqm2DHWHg+DB99UVRp4VsyTF0QLXyHrqSuE1Xo5+sG0AQq/lBcrGMlk5NU5xE2MDLKViw==",
+      "requires": {
+        "@popperjs/core": "^2.9.2",
+        "debounce": "^1.2.1"
+      }
+    },
+    "web-worker": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.2.0.tgz",
+      "integrity": "sha512-PgF341avzqyx60neE9DD+XS26MMNMoUQRz9NOZwW32nPQrF6p77f1htcnjBSEV8BGMKZ16choqUG4hyI0Hx7mA=="
+    },
+    "which": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+      "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+      "requires": {
+        "isexe": "^2.0.0"
+      }
+    },
+    "wrappy": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+      "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+      "dev": true
+    },
+    "xml-name-validator": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz",
+      "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==",
+      "dev": true
+    },
+    "yaml": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.1.tgz",
+      "integrity": "sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==",
+      "dev": true
+    },
+    "yocto-queue": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+      "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="
+    }
+  }
+}

+ 41 - 0
package.json

@@ -0,0 +1,41 @@
+{
+  "name": "modulplaner",
+  "private": true,
+  "version": "0.0.0",
+  "type": "module",
+  "scripts": {
+    "dev": "vite",
+    "build": "vue-tsc && vite build",
+    "build-nochecks": "vite build",
+    "preview": "vite preview",
+    "lint": "eslint --fix ./src/"
+  },
+  "dependencies": {
+    "@fortawesome/fontawesome-svg-core": "^6.6.0",
+    "@fortawesome/free-brands-svg-icons": "^6.6.0",
+    "@fortawesome/free-solid-svg-icons": "^6.6.0",
+    "@fortawesome/vue-fontawesome": "^3.0.8",
+    "leaflet": "^1.9.4",
+    "mermaid": "^10.5.0",
+    "pinia": "^2.2.2",
+    "typescript-eslint": "^8.7.0",
+    "vue": "^3.5.10",
+    "vue-router": "^4.4.5",
+    "vue-toastification": "^2.0.0-rc.5",
+    "vue3-popper": "^1.5.0"
+  },
+  "devDependencies": {
+    "@typescript-eslint/eslint-plugin": "^8.7.0",
+    "@typescript-eslint/parser": "^8.7.0",
+    "@vitejs/plugin-vue": "^5.1.4",
+    "autoprefixer": "^10.4.20",
+    "eslint": "^9.11.1",
+    "eslint-config-prettier": "^9.1.0",
+    "eslint-plugin-vue": "^9.28.0",
+    "postcss": "^8.4.47",
+    "tailwindcss": "^3.4.13",
+    "typescript": "^5.6.2",
+    "vite": "^5.4.8",
+    "vue-tsc": "^2.1.6"
+  }
+}

+ 6 - 0
postcss.config.cjs

@@ -0,0 +1,6 @@
+module.exports = {
+  plugins: {
+    tailwindcss: {},
+    autoprefixer: {},
+  },
+};

BIN
public/favicon.png


+ 31 - 0
public/favicon.svg

@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg width="100%" height="100%" viewBox="0 0 3544 3544" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
+    <g transform="matrix(1,0,0,1,-5180,-4335)">
+        <g transform="matrix(1,0,0,1,1075.34,4335)">
+            <rect id="ArtBoard2" x="4104.66" y="0" width="3543.31" height="3543.31" style="fill:none;"/>
+            <clipPath id="_clip1">
+                <rect id="ArtBoard21" serif:id="ArtBoard2" x="4104.66" y="0" width="3543.31" height="3543.31"/>
+            </clipPath>
+            <g clip-path="url(#_clip1)">
+                <g transform="matrix(1,0,0,1,4105.31,0)">
+                    <path d="M3543.31,382.395C3543.31,171.346 3371.96,0 3160.91,0L382.395,0C171.346,0 0,171.346 0,382.395L0,3160.91C0,3371.96 171.346,3543.31 382.395,3543.31L3160.91,3543.31C3371.96,3543.31 3543.31,3371.96 3543.31,3160.91L3543.31,382.395Z" style="fill:white;"/>
+                </g>
+                <g transform="matrix(1,0,0,1,4105.31,0)">
+                    <g transform="matrix(1.31335,0,0,1.03486,149.384,-265.109)">
+                        <path d="M1132.49,739.164C1132.49,655.202 1078.78,587.036 1012.62,587.036L257.893,587.036C191.735,587.036 138.023,655.202 138.023,739.164L138.023,3247.97C138.023,3331.93 191.735,3400.1 257.893,3400.1L1012.62,3400.1C1078.78,3400.1 1132.49,3331.93 1132.49,3247.97L1132.49,739.164Z" style="fill:rgb(39,51,79);"/>
+                    </g>
+                    <g transform="matrix(1.31335,0,0,1.03486,1706.47,-265.109)">
+                        <path d="M1132.49,738.486C1132.49,654.899 1079.02,587.036 1013.15,587.036L257.359,587.036C191.496,587.036 138.023,654.899 138.023,738.486L138.023,3248.65C138.023,3332.23 191.496,3400.1 257.359,3400.1L1013.15,3400.1C1079.02,3400.1 1132.49,3332.23 1132.49,3248.65L1132.49,738.486Z" style="fill:rgb(39,51,79);"/>
+                    </g>
+                </g>
+                <g transform="matrix(1.20164,0,0,1.77939,4321.66,-1910.94)">
+                    <path d="M1063.1,1565.7C1063.1,1524.88 1014.03,1491.74 953.581,1491.74L323.595,1491.74C263.149,1491.74 214.074,1524.88 214.074,1565.7L214.074,1941.66C214.074,1982.48 263.149,2015.62 323.595,2015.62L953.581,2015.62C1014.03,2015.62 1063.1,1982.48 1063.1,1941.66L1063.1,1565.7Z" style="fill:rgb(189,231,169);"/>
+                </g>
+                <g transform="matrix(1.20164,0,0,1.77939,5878.75,-676.324)">
+                    <path d="M1063.1,1565.7C1063.1,1524.88 1014.03,1491.74 953.581,1491.74L323.595,1491.74C263.149,1491.74 214.074,1524.88 214.074,1565.7L214.074,1941.66C214.074,1982.48 263.149,2015.62 323.595,2015.62L953.581,2015.62C1014.03,2015.62 1063.1,1982.48 1063.1,1941.66L1063.1,1565.7Z" style="fill:rgb(60,133,222);"/>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

+ 2 - 0
public/robots.txt

@@ -0,0 +1,2 @@
+User-Agent: *
+Disallow: /

+ 27 - 0
src/App.vue

@@ -0,0 +1,27 @@
+<template>
+  <HeaderBar />
+  <router-view />
+</template>
+
+<script setup lang="ts">
+import HeaderBar from "./components/layout/HeaderBar.vue";
+import { useModulesStore } from "./stores/modules";
+import { useLecturersStore } from "./stores/lecturers";
+import { useStudenthubStore } from "./stores/studenthub";
+import { useClassVersionStore } from "./stores/ClassVersion";
+import { useClassesStore } from "./stores/classes";
+
+const moduleStore = useModulesStore();
+const lecturersStore = useLecturersStore();
+const studenthubStore = useStudenthubStore();
+const classVersionStore = useClassVersionStore();
+const classesStore = useClassesStore();
+
+classVersionStore.fetchData();
+classesStore.fetchData();
+
+moduleStore.fetchData();
+lecturersStore.fetchData();
+
+studenthubStore.loadData();
+</script>

+ 0 - 0
src/assets/.gitkeep


+ 152 - 0
src/assets/coconuts.svg

@@ -0,0 +1,152 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg width="100%" height="100%" viewBox="0 0 149 220" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;">
+    <g transform="matrix(1,0,0,1,-742.112,-3189.33)">
+        <g>
+            <g transform="matrix(0.961306,0.0790558,-0.0790558,0.961306,300.516,74.3971)">
+                <g transform="matrix(0.984291,0.00969807,0.00969807,1.10142,-14.7875,-364.772)">
+                    <path d="M802.292,3372.84C802.605,3372.15 804.813,3373.07 805.233,3373.24C807.21,3374.05 811.323,3376.82 810.51,3379.53C810.1,3380.89 808.842,3381.57 807.546,3381.91C804.876,3382.62 802.003,3383 799.233,3383" style="fill:rgb(172,174,163);stroke:black;stroke-width:3.28px;"/>
+                </g>
+                <g transform="matrix(0.974161,0.00321844,0.00321844,1.01303,33.019,-105.09)">
+                    <g transform="matrix(0.945166,0,0,0.945166,17.0888,242.835)">
+                        <path d="M811.214,3327.72C812.364,3328.3 813.82,3328.92 815.08,3329.51C815.942,3329.91 816.688,3330.52 817.42,3331.12C818.191,3331.75 819.14,3332.59 819.395,3333.61C819.885,3335.57 816.606,3337.68 815.3,3338.51C814.966,3338.72 813.033,3339.82 813.033,3340.34" style="fill:rgb(206,135,25);stroke:black;stroke-width:3.67px;"/>
+                    </g>
+                    <g transform="matrix(0.945166,0,0,0.945166,17.0888,242.835)">
+                        <path d="M767.045,3365.08C768.813,3365.49 778.208,3365.41 779.25,3364.37C779.777,3363.84 780.547,3330.99 783.288,3319.12C784.943,3311.95 791.329,3300.95 799.993,3306.85C816.922,3318.37 816.13,3379.96 793.164,3395.58C785.836,3400.57 769.866,3395.27 765.964,3386.73C759.796,3373.22 767.387,3365.08 767.045,3365.08Z" style="fill:rgb(172,174,163);stroke:black;stroke-width:3.67px;"/>
+                    </g>
+                    <g transform="matrix(0.945166,-1.31168e-17,1.31168e-17,0.945166,16.7045,241.596)">
+                        <path d="M776.734,3397.89C776.75,3397.85 777.029,3397.37 777.114,3397.54C777.282,3397.88 776.815,3398.42 776.691,3398.67C776.005,3400.04 775.386,3401.43 774.788,3402.83C774.743,3402.94 774.158,3404.21 774.365,3404.31C775.687,3404.97 777.185,3405.4 778.524,3406.07" style="fill:none;stroke:black;stroke-width:3.67px;"/>
+                    </g>
+                    <g transform="matrix(0.945166,0,0,0.945166,17.0888,242.835)">
+                        <path d="M796.654,3393.84C796.398,3393.96 796.956,3394.28 797.062,3394.37C797.838,3395.05 798.621,3395.72 799.388,3396.41C799.767,3396.76 800.105,3397.09 800.515,3397.4C801.843,3398.4 802.077,3398.31 803.335,3397.19C803.641,3396.92 803.972,3396.7 804.322,3396.49" style="fill:none;stroke:black;stroke-width:3.67px;"/>
+                    </g>
+                    <g transform="matrix(0.945166,0,0,0.945166,17.5432,236.957)">
+                        <path d="M799.076,3364.03C798.853,3364.14 799.8,3363.83 800.028,3363.77C800.689,3363.6 801.451,3363.4 802.124,3363.62C802.734,3363.83 803.072,3364.72 803.498,3365.14C804.52,3366.16 806.449,3365.87 807.618,3365.29C807.885,3365.16 809.281,3364.66 809.281,3364.42" style="fill:none;stroke:black;stroke-width:3.67px;"/>
+                    </g>
+                    <g transform="matrix(0.945166,0,0,0.945166,17.9002,237.76)">
+                        <path d="M801.9,3369.99C802.473,3369.45 804.077,3368.38 804.944,3368.9C805.896,3369.47 805.783,3370.69 807.329,3369.91" style="fill:none;stroke:black;stroke-width:3.67px;"/>
+                    </g>
+                    <g transform="matrix(0.945166,0,0,0.945166,17.0888,242.835)">
+                        <path d="M804.513,3325.3C804.459,3325.25 804.477,3325.31 804.477,3325.34" style="fill:none;stroke:black;stroke-width:3.67px;"/>
+                    </g>
+                    <g transform="matrix(0.945166,0,0,0.945166,17.0888,242.835)">
+                        <path d="M799.951,3325.16C799.866,3325.12 799.815,3325.12 799.724,3325.12" style="fill:none;stroke:black;stroke-width:3.67px;"/>
+                    </g>
+                </g>
+                <g transform="matrix(1.16286,0.130875,-0.104741,1.16033,254.469,-709.399)">
+                    <g transform="matrix(0.950458,-0.310852,0.310852,0.950458,-1039.01,413.646)">
+                        <g transform="matrix(0.941986,-0.077467,0.077467,0.941986,-239.292,314.254)">
+                            <path d="M819.981,3376.83C818.537,3376.52 816.388,3378.31 815.568,3379.13C814.547,3380.15 813.965,3382.2 813.839,3382.95C813.076,3387.53 817.301,3390.7 820.896,3390.7C821.79,3390.7 824.56,3390.29 826.084,3389.36" style="fill:rgb(128,78,54);stroke:black;stroke-width:3.1px;"/>
+                        </g>
+                        <g transform="matrix(0.941986,-0.077467,0.077467,0.941986,-239.292,314.254)">
+                            <path d="M819.322,3377.07C818.472,3377.88 819.501,3382.01 819.67,3382.48C820.522,3384.87 824.516,3390.2 826.521,3389.34C827.469,3388.93 827.485,3387.24 827.485,3386.34C827.485,3383.4 825.732,3380.28 823.438,3378.37C823.14,3378.12 820.601,3375.86 819.322,3377.07Z" style="fill:rgb(220,215,195);stroke:black;stroke-width:3.1px;"/>
+                        </g>
+                    </g>
+                    <g transform="matrix(0.582976,-0.521106,0.62349,0.697516,-1803.19,1505.62)">
+                        <path d="M828.404,3373.1C828.136,3372.75 828.305,3374.85 828.405,3375.2C828.749,3376.41 830.43,3381.38 834.036,3384.08C834.384,3384.34 836.506,3386.06 836.969,3385.82C837.674,3385.47 842.828,3382.36 842.457,3378.2C842.201,3375.33 838.378,3372.43 835.875,3372.05C832.519,3371.53 828.404,3372.84 828.404,3373.1Z" style="fill:rgb(128,78,54);stroke:black;stroke-width:3.41px;"/>
+                    </g>
+                </g>
+                <g transform="matrix(0.984291,0.00969807,0.00969807,1.10142,-24.1993,-345.123)">
+                    <path d="M802.292,3372.84C802.605,3372.15 804.813,3373.07 805.233,3373.24C807.21,3374.05 811.323,3376.82 810.51,3379.53C810.1,3380.89 808.842,3381.57 807.546,3381.91C804.876,3382.62 802.003,3383 799.233,3383" style="fill:rgb(172,174,163);stroke:black;stroke-width:3.28px;"/>
+                </g>
+                <g transform="matrix(1.28755,-5.14817e-17,-1.62369e-17,1.13455,-224.102,-449.314)">
+                    <g transform="matrix(1.18177,0,0,1.20106,-141.55,-691.305)">
+                        <path d="M779.673,3311.95C777.866,3308.33 773.174,3308.08 769.452,3309.47C766.361,3310.63 765.456,3315.61 765.963,3318.65C767.12,3325.6 779.257,3325.91 780.445,3318.78C780.9,3316.05 780.791,3314.18 779.673,3311.95Z" style="fill:rgb(149,71,71);stroke:black;stroke-width:2.39px;"/>
+                    </g>
+                    <g transform="matrix(1.08929,-1.38778e-17,1.38778e-17,1,-69.6543,-1.13687e-13)">
+                        <g transform="matrix(1,0,0,1.12926,0,-429.223)">
+                            <path d="M779.635,3324.84C777.666,3324.59 773.939,3325.19 771.943,3325.01C769.451,3324.78 766.96,3324.41 764.472,3324.16C763.284,3324.04 760.618,3324.54 759.773,3323.7C759.55,3323.47 760.172,3311.97 760.134,3311.66C759.975,3310.4 761.805,3301.96 770.24,3302.59C778.204,3303.18 779.429,3309.34 779.772,3311.93C780.082,3314.28 780.171,3320.26 779.877,3322.62C779.831,3322.98 780.255,3324.84 779.635,3324.84Z" style="fill:rgb(138,110,75);stroke:black;stroke-width:2.57px;"/>
+                        </g>
+                        <g transform="matrix(1,0,0,1,1.30109,-1.7938)">
+                            <path d="M760.625,3312.23C764.131,3312.06 767.61,3312.34 771.096,3312.61" style="fill:rgb(138,110,75);stroke:black;stroke-width:1.69px;"/>
+                        </g>
+                    </g>
+                    <g transform="matrix(1.10245,-2.77556e-17,0,1,-79.8813,-1.13687e-13)">
+                        <g transform="matrix(1,0,0,1,-0.442323,0.906001)">
+                            <path d="M763.297,3326.6C762.628,3324.31 757.902,3326.03 756.107,3328.1C754.257,3330.24 755.743,3334.32 755.179,3335.45C755.179,3335.45 752.402,3337.52 751.222,3339.09C749.468,3341.43 749.266,3344.75 749.802,3347.42C750.404,3350.44 757.534,3355.95 760.278,3353.21" style="fill:rgb(199,186,161);stroke:black;stroke-width:2.69px;"/>
+                        </g>
+                        <path d="M779.667,3326.1C780,3326.32 778.568,3326.16 778.151,3326.16C777.178,3326.16 776.193,3326.3 775.201,3326.3C772.287,3326.3 769.417,3326.03 766.485,3326.03C765.609,3326.03 763.897,3325.71 763.535,3326.43C762.627,3328.24 763.116,3331.26 762.864,3333.27C762.161,3338.9 761.121,3344.79 761.121,3350.43C761.121,3351.83 759.631,3356.79 761.389,3357.67C765.912,3359.93 772.814,3358.34 777.748,3358.34" style="fill:rgb(105,109,99);stroke:black;stroke-width:2.69px;"/>
+                        <g transform="matrix(1,0,0,1,0.252282,-0.023545)">
+                            <path d="M761.692,3332.19C760.613,3332.22 756.507,3334.93 755.758,3335.68C755.322,3336.12 758.725,3343.4 760.728,3345.4" style="fill:none;stroke:black;stroke-width:1.68px;"/>
+                        </g>
+                    </g>
+                    <path d="M782.649,3277.23C782.382,3282.98 781.017,3312.67 780.445,3318.39C779.743,3325.42 778.249,3361.14 778.249,3361.07" style="fill:none;stroke:black;stroke-width:2.85px;"/>
+                </g>
+            </g>
+            <g transform="matrix(1,0,0,1,-7.97774,6.25241)">
+                <g>
+                    <g transform="matrix(1,0,0,1,-0.00952215,-0.301541)">
+                        <path d="M882.072,3312.61C881.696,3312.63 890.827,3311.3 892.629,3314.49C894.505,3317.81 883.734,3328.52 883.225,3328.52" style="fill:rgb(227,128,67);stroke:black;stroke-width:3.33px;"/>
+                    </g>
+                    <g transform="matrix(-0.507508,-0.861647,0.861647,-0.507508,-1582.65,5813.3)">
+                        <path d="M863.766,3366.18C864.052,3366.14 862.427,3374.11 858.863,3373.93C856.158,3373.79 854.245,3369.66 855.511,3365.26" style="fill:rgb(204,201,194);stroke:black;stroke-width:3.33px;"/>
+                    </g>
+                    <g transform="matrix(0.998666,0.0516362,-0.0516362,0.998666,173.842,-40.252)">
+                        <path d="M845.907,3360.44C847.564,3359.46 851.365,3359.32 852.716,3357.97C853.284,3357.4 853.686,3355.71 853.686,3354.99C853.686,3351.54 852.292,3318.02 856.804,3305.78C858.44,3301.34 860.551,3294.59 865.752,3293.03C874.098,3290.53 877.355,3302.85 878.963,3308.58C881.601,3317.97 883.512,3350.72 883.512,3363.72C883.512,3371.74 883.328,3380.45 879.461,3387.71C873.035,3399.76 855.201,3398.58 847.609,3384.81C843.62,3377.58 842.792,3366.31 845.907,3360.44Z" style="fill:rgb(204,201,194);stroke:black;stroke-width:3.33px;"/>
+                    </g>
+                    <g transform="matrix(-1.40705,0,0,2.1925,2094.74,-3948.62)">
+                        <path d="M870.696,3311.97C870.683,3311.96 870.67,3311.95 870.657,3311.95" style="fill:none;stroke:black;stroke-width:1.81px;"/>
+                    </g>
+                    <g transform="matrix(1,0,0,1,0,-5.64845)">
+                        <path d="M876.147,3315.76C876.147,3315.76 876.159,3315.7 876.248,3315.78" style="fill:none;stroke:black;stroke-width:3.33px;"/>
+                    </g>
+                    <path d="M869.918,3346.11C869.86,3345.11 871.894,3344.44 872.724,3344.61C874.634,3344.99 875.547,3346.62 877.188,3347.17C878.103,3347.47 878.746,3346.37 879.503,3346.18C880.321,3345.97 881.067,3346.01 881.736,3346.01" style="fill:none;stroke:black;stroke-width:3.33px;"/>
+                    <path d="M870.762,3352.07C870.29,3351.99 871.548,3351.12 871.649,3351.06C872.353,3350.59 873.273,3350.39 874.129,3350.56C875.775,3350.89 876.778,3352.65 878.346,3353.04C879.608,3353.36 880.606,3351.88 881.57,3351.88" style="fill:none;stroke:black;stroke-width:3.33px;"/>
+                    <path d="M860.777,3366.12C861.063,3366.07 861.048,3368.06 861.048,3368.21C861.048,3370.07 860.857,3373.19 859.088,3374.37C858.399,3374.83 857.222,3374.62 856.497,3374.37C853.939,3373.48 850.254,3369.79 853.486,3367.37" style="fill:none;stroke:black;stroke-width:3.33px;"/>
+                    <g transform="matrix(0.988864,-0.148821,0.148821,0.988864,-492.434,166.096)">
+                        <path d="M872.801,3394.81C872.801,3394.57 874.572,3397.43 874.959,3397.97C875.145,3398.24 876.745,3400.57 877.045,3400.42C877.83,3400.03 878.602,3399.46 879.418,3399.05" style="fill:none;stroke:black;stroke-width:3.33px;"/>
+                    </g>
+                    <g transform="matrix(0.988445,0.151577,-0.151577,0.988445,523.991,-90.5273)">
+                        <path d="M853.463,3394.46C853.231,3395.75 851.281,3398.38 851.941,3399.7C852.148,3400.12 854.026,3400.85 854.458,3401" style="fill:none;stroke:black;stroke-width:3.33px;"/>
+                    </g>
+                </g>
+                <g>
+                    <g transform="matrix(1,0,0,0.829851,-0.333144,562.592)">
+                        <path d="M865.437,3304.14C865.458,3304.13 862.739,3311.41 862.45,3315.64C862.201,3319.31 863.663,3322.89 862.889,3325.76C862.55,3327.01 860.515,3327.5 858.258,3329.34C857.209,3330.19 854.262,3333.89 853.444,3333.39C852.742,3332.97 853.536,3327.66 853.751,3326.08C854.475,3320.72 854.641,3315.29 856.767,3310.33C857.191,3309.34 858.697,3305.76 858.881,3305.76" style="fill:rgb(105,105,105);stroke:black;stroke-width:3.63px;"/>
+                    </g>
+                    <g transform="matrix(1.06686,-0.149357,0.148206,0.989294,-547.572,163.715)">
+                        <g>
+                            <path d="M857.863,3304.51C858.394,3305.01 879.659,3304.19 879.893,3303.54C880.993,3300.45 882.593,3292.65 881.773,3291.99C879.893,3290.49 869.141,3287.05 866.608,3287.34C864.555,3287.58 854.615,3293.68 854.43,3294.06C854.08,3294.76 856.345,3303.09 857.863,3304.51Z" style="fill:rgb(129,129,129);stroke:black;stroke-width:3.19px;"/>
+                        </g>
+                    </g>
+                </g>
+            </g>
+            <g transform="matrix(0.997548,-0.0699817,0.0699817,0.997548,-208.381,81.1496)">
+                <g transform="matrix(0.776266,-1.06699,1.06699,0.776266,-3526.12,1509.41)">
+                    <path d="M883.249,3423.75C879.233,3427.24 879.844,3434.95 885.623,3436.39C889.61,3437.39 896.598,3436.73 899.382,3433.64C901.717,3431.04 901.951,3424.07 898.596,3422.2C894.52,3419.94 887.424,3420.12 883.249,3423.75Z" style="fill:rgb(113,74,52);stroke:black;stroke-width:2.53px;"/>
+                    <g>
+                        <g transform="matrix(1.26613,0,0,1.25966,-234.981,-887.802)">
+                            <circle cx="886.025" cy="3427.32" r="0.991" style="fill:rgb(65,37,24);"/>
+                        </g>
+                        <g transform="matrix(1.26613,0,0,1.25966,-232.228,-885.645)">
+                            <circle cx="886.025" cy="3427.32" r="0.991" style="fill:rgb(65,37,24);"/>
+                        </g>
+                        <g transform="matrix(1.26613,0,0,1.25966,-234.053,-889.212)">
+                            <circle cx="885.052" cy="3431.15" r="0.931" style="fill:rgb(65,37,24);"/>
+                        </g>
+                    </g>
+                </g>
+                <g transform="matrix(0.997548,0.0699817,-0.0699817,0.997548,218.312,-69.0719)">
+                    <path d="M837.64,3221.49C837.604,3221.43 837.032,3221.94 836.955,3222C835.971,3222.69 835.868,3222.84 836.33,3223.92C836.372,3224.01 836.526,3224.54 836.643,3224.54" style="fill:none;stroke:black;stroke-width:3.33px;"/>
+                </g>
+                <g transform="matrix(0.997548,0.0699817,-0.0699817,0.997548,218.312,-69.0719)">
+                    <path d="M841.334,3223.14C841.084,3223.21 840.943,3223.47 840.792,3223.69C840.631,3223.93 840.386,3224.15 840.256,3224.41C840.162,3224.59 840.437,3224.74 840.524,3224.85C840.883,3225.33 841.042,3226 841.461,3226.41" style="fill:none;stroke:black;stroke-width:3.33px;"/>
+                </g>
+                <g transform="matrix(0.863528,0,0,0.863528,105.939,447.031)">
+                    <g transform="matrix(1.08145,1.38778e-17,0,1.15977,-71.8217,-509.226)">
+                        <path d="M884.156,3179.33C884.04,3179.1 890.354,3182.16 890.543,3183.86C890.64,3184.73 889.562,3185.19 888.911,3185.45C887.993,3185.82 882.71,3187.17 881.743,3187.17" style="fill:rgb(244,202,112);stroke:black;stroke-width:3.44px;"/>
+                    </g>
+                    <g transform="matrix(0.800271,-0.220144,0.238513,0.867047,-585.024,614.912)">
+                        <path d="M845.149,3173.76C848.159,3170.76 850.391,3165.99 850.391,3161.66C850.391,3159.82 850.53,3157.94 850.2,3156.13C850.163,3155.93 849.613,3154.14 849.724,3154.03C849.99,3153.77 851.059,3154.5 851.248,3154.6C852.799,3155.49 854.326,3157.02 855.632,3158.22C861.37,3163.52 865.447,3169.14 865.447,3177.19" style="fill:rgb(165,151,123);stroke:black;stroke-width:4.46px;"/>
+                    </g>
+                    <path d="M882.238,3186.56C883.3,3184.79 885.344,3177.42 881.385,3175.12C879.18,3173.84 874.577,3173.78 873.325,3173.78C868.632,3173.78 869.283,3179.34 864.825,3179.34C862.255,3179.34 829.332,3176.81 829.046,3177.09C828.595,3177.54 830.407,3182.16 830.638,3182.6C833.84,3188.58 844.719,3197.55 859.232,3197.29C870.186,3197.09 880.211,3189.94 882.238,3186.56Z" style="fill:rgb(165,151,123);stroke:black;stroke-width:3.86px;"/>
+                    <g transform="matrix(0.99415,-0.108006,0.120004,1.10458,-370.436,-239.801)">
+                        <path d="M857.191,3188.7C857.672,3188.88 855.378,3190.91 855.251,3191.01C853.082,3192.67 850.61,3193.66 848.009,3194.44C842.868,3195.98 837.642,3196.73 832.286,3197.01C829.882,3197.14 827.258,3197.3 824.948,3196.53C824.652,3196.43 826.529,3195.77 826.759,3195.67C827.852,3195.21 828.944,3194.58 829.903,3193.86C832.589,3191.85 835.774,3189.64 837.527,3186.72C837.924,3186.06 838.67,3185.03 838.67,3184.24" style="fill:rgb(165,151,123);stroke:black;stroke-width:3.65px;"/>
+                    </g>
+                    <path d="M873.572,3179.65C873.523,3179.65 873.537,3179.64 873.537,3179.61" style="fill:none;stroke:black;stroke-width:3.86px;"/>
+                    <path d="M878.934,3180.2C878.939,3180.21 878.943,3180.21 878.948,3180.21" style="fill:none;stroke:black;stroke-width:3.86px;"/>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

+ 31 - 0
src/assets/logo.svg

@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg width="100%" height="100%" viewBox="0 0 3544 3544" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
+    <g transform="matrix(1,0,0,1,-5180,-4335)">
+        <g transform="matrix(1,0,0,1,1075.34,4335)">
+            <rect id="ArtBoard2" x="4104.66" y="0" width="3543.31" height="3543.31" style="fill:none;"/>
+            <clipPath id="_clip1">
+                <rect id="ArtBoard21" serif:id="ArtBoard2" x="4104.66" y="0" width="3543.31" height="3543.31"/>
+            </clipPath>
+            <g clip-path="url(#_clip1)">
+                <g transform="matrix(1,0,0,1,4105.31,0)">
+                    <path d="M3543.31,382.395C3543.31,171.346 3371.96,0 3160.91,0L382.395,0C171.346,0 0,171.346 0,382.395L0,3160.91C0,3371.96 171.346,3543.31 382.395,3543.31L3160.91,3543.31C3371.96,3543.31 3543.31,3371.96 3543.31,3160.91L3543.31,382.395Z" style="fill:white;"/>
+                </g>
+                <g transform="matrix(1,0,0,1,4105.31,0)">
+                    <g transform="matrix(1.31335,0,0,1.03486,149.384,-265.109)">
+                        <path d="M1132.49,739.164C1132.49,655.202 1078.78,587.036 1012.62,587.036L257.893,587.036C191.735,587.036 138.023,655.202 138.023,739.164L138.023,3247.97C138.023,3331.93 191.735,3400.1 257.893,3400.1L1012.62,3400.1C1078.78,3400.1 1132.49,3331.93 1132.49,3247.97L1132.49,739.164Z" style="fill:rgb(39,51,79);"/>
+                    </g>
+                    <g transform="matrix(1.31335,0,0,1.03486,1706.47,-265.109)">
+                        <path d="M1132.49,738.486C1132.49,654.899 1079.02,587.036 1013.15,587.036L257.359,587.036C191.496,587.036 138.023,654.899 138.023,738.486L138.023,3248.65C138.023,3332.23 191.496,3400.1 257.359,3400.1L1013.15,3400.1C1079.02,3400.1 1132.49,3332.23 1132.49,3248.65L1132.49,738.486Z" style="fill:rgb(39,51,79);"/>
+                    </g>
+                </g>
+                <g transform="matrix(1.20164,0,0,1.77939,4321.66,-1910.94)">
+                    <path d="M1063.1,1565.7C1063.1,1524.88 1014.03,1491.74 953.581,1491.74L323.595,1491.74C263.149,1491.74 214.074,1524.88 214.074,1565.7L214.074,1941.66C214.074,1982.48 263.149,2015.62 323.595,2015.62L953.581,2015.62C1014.03,2015.62 1063.1,1982.48 1063.1,1941.66L1063.1,1565.7Z" style="fill:rgb(189,231,169);"/>
+                </g>
+                <g transform="matrix(1.20164,0,0,1.77939,5878.75,-676.324)">
+                    <path d="M1063.1,1565.7C1063.1,1524.88 1014.03,1491.74 953.581,1491.74L323.595,1491.74C263.149,1491.74 214.074,1524.88 214.074,1565.7L214.074,1941.66C214.074,1982.48 263.149,2015.62 323.595,2015.62L953.581,2015.62C1014.03,2015.62 1063.1,1982.48 1063.1,1941.66L1063.1,1565.7Z" style="fill:rgb(60,133,222);"/>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

Разлика између датотеке није приказан због своје велике величине
+ 17 - 0
src/assets/searching-single.svg


+ 212 - 0
src/assets/searching.svg

@@ -0,0 +1,212 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg width="100%" height="100%" viewBox="0 0 588 205" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.68605;">
+    <g id="Artboard1" transform="matrix(1,0,0,1,2.77815,-74.1257)">
+        <rect x="-2.778" y="74.126" width="587.584" height="204.926" style="fill:none;"/>
+        <clipPath id="_clip1">
+            <rect x="-2.778" y="74.126" width="587.584" height="204.926"/>
+        </clipPath>
+        <g clip-path="url(#_clip1)">
+            <g transform="matrix(1.37104,0,0,1.37104,1190.57,-1332.86)">
+                <g id="ArtBoard2">
+                    <rect x="-874.81" y="951.498" width="585.922" height="279.056" style="fill:none;"/>
+                    <g transform="matrix(0.971921,0,0,0.992405,-1607.76,-995.683)">
+                        <g>
+                            <g transform="matrix(-1.33829,0,0,1.33829,2431.3,-728.641)">
+                                <g transform="matrix(1.03624,0.0066441,-0.007494,1.31702,1068.49,40.0789)">
+                                    <ellipse cx="33.82" cy="1616.5" rx="6.225" ry="0.785" style="fill:rgb(157,125,97);stroke:rgb(156,125,98);stroke-width:2.13px;"/>
+                                </g>
+                                <g transform="matrix(1.03624,0.0066441,-0.007494,1.31702,1213.48,38.8618)">
+                                    <ellipse cx="33.82" cy="1616.5" rx="6.225" ry="0.785" style="fill:rgb(157,125,97);stroke:rgb(156,125,98);stroke-width:2.13px;"/>
+                                </g>
+                                <g transform="matrix(1.03555,0.0407406,-0.045952,1.31614,1060.24,41.2558)">
+                                    <ellipse cx="33.82" cy="1616.5" rx="6.225" ry="0.785" style="fill:rgb(157,125,97);stroke:rgb(156,125,98);stroke-width:2.13px;"/>
+                                </g>
+                                <g transform="matrix(1.03555,0.0407406,-0.045952,1.31614,1205.23,40.0387)">
+                                    <ellipse cx="33.82" cy="1616.5" rx="6.225" ry="0.785" style="fill:rgb(157,125,97);stroke:rgb(156,125,98);stroke-width:2.13px;"/>
+                                </g>
+                                <g transform="matrix(0.917264,-0.012375,0.0115981,0.968705,1042.49,603.999)">
+                                    <ellipse cx="13.448" cy="1610.88" rx="7.263" ry="0.814" style="fill:rgb(157,125,97);stroke:rgb(156,125,98);stroke-width:2.69px;"/>
+                                </g>
+                                <g transform="matrix(0.917264,-0.012375,0.0115981,0.968705,1187.48,602.782)">
+                                    <ellipse cx="13.448" cy="1610.88" rx="7.263" ry="0.814" style="fill:rgb(157,125,97);stroke:rgb(156,125,98);stroke-width:2.69px;"/>
+                                </g>
+                                <g transform="matrix(0.917309,-0.00774344,0.00961861,1.28395,973.827,96.4853)">
+                                    <ellipse cx="13.448" cy="1610.88" rx="7.263" ry="0.814" style="fill:rgb(157,125,97);stroke:rgb(156,125,98);stroke-width:2.27px;"/>
+                                </g>
+                                <g transform="matrix(0.917309,-0.00774344,0.00961861,1.28395,1118.82,95.2682)">
+                                    <ellipse cx="13.448" cy="1610.88" rx="7.263" ry="0.814" style="fill:rgb(157,125,97);stroke:rgb(156,125,98);stroke-width:2.27px;"/>
+                                </g>
+                                <g transform="matrix(0.916486,-0.0419628,0.0469531,1.15552,950.175,303.804)">
+                                    <ellipse cx="13.448" cy="1610.88" rx="7.263" ry="0.814" style="fill:rgb(157,125,97);stroke:rgb(156,125,98);stroke-width:2.43px;"/>
+                                </g>
+                                <g transform="matrix(0.916486,-0.0419628,0.0469531,1.15552,1095.17,302.587)">
+                                    <ellipse cx="13.448" cy="1610.88" rx="7.263" ry="0.814" style="fill:rgb(157,125,97);stroke:rgb(156,125,98);stroke-width:2.43px;"/>
+                                </g>
+                                <g transform="matrix(0.915803,-0.056311,0.0630076,1.15466,850.839,305.916)">
+                                    <ellipse cx="13.448" cy="1610.88" rx="7.263" ry="0.814" style="fill:rgb(157,125,97);stroke:rgb(156,125,98);stroke-width:2.43px;"/>
+                                </g>
+                                <g transform="matrix(0.915803,-0.056311,0.0630076,1.15466,995.83,304.699)">
+                                    <ellipse cx="13.448" cy="1610.88" rx="7.263" ry="0.814" style="fill:rgb(157,125,97);stroke:rgb(156,125,98);stroke-width:2.43px;"/>
+                                </g>
+                                <g transform="matrix(0.925717,0.0791322,-0.0657724,0.867006,1159.28,765.976)">
+                                    <ellipse cx="3.201" cy="1618.99" rx="7.043" ry="1.484" style="fill:rgb(157,125,97);stroke:rgb(156,125,98);stroke-width:2.8px;"/>
+                                </g>
+                                <g transform="matrix(0.925717,0.0791322,-0.0657724,0.867006,1304.27,764.758)">
+                                    <ellipse cx="3.201" cy="1618.99" rx="7.043" ry="1.484" style="fill:rgb(157,125,97);stroke:rgb(156,125,98);stroke-width:2.8px;"/>
+                                </g>
+                                <g transform="matrix(0.928197,0.0328675,-0.0273186,0.869329,1024.95,762.626)">
+                                    <ellipse cx="3.201" cy="1618.99" rx="7.043" ry="1.484" style="fill:rgb(157,125,97);stroke:rgb(156,125,98);stroke-width:2.81px;"/>
+                                </g>
+                                <g transform="matrix(0.928197,0.0328675,-0.0273186,0.869329,1169.95,761.409)">
+                                    <ellipse cx="3.201" cy="1618.99" rx="7.043" ry="1.484" style="fill:rgb(157,125,97);stroke:rgb(156,125,98);stroke-width:2.81px;"/>
+                                </g>
+                            </g>
+                            <g transform="matrix(-1.10745,0,0,1.10745,2222.49,-239.485)">
+                                <g transform="matrix(0.991221,0,0,1,9.68285,-5.68434e-14)">
+                                    <g transform="matrix(1.03626,0,0,1.10001,1042.74,390.272)">
+                                        <path d="M50.739,1541.43C50.883,1541.07 52.721,1542.22 52.905,1542.32C54.533,1543.21 57.707,1544.32 58.063,1546.45C58.305,1547.9 56.142,1549.25 55.047,1549.7C54.78,1549.81 53.46,1550.18 53.46,1550.58" style="fill:rgb(221,118,31);stroke:black;stroke-width:2.88px;"/>
+                                    </g>
+                                    <g transform="matrix(1.03626,0,0,1.10001,1042.74,390.272)">
+                                        <path d="M21.588,1562.99C22.468,1562.15 25.316,1563.49 26.071,1562.73C26.361,1562.44 30.82,1537.54 31.618,1529.31C31.995,1525.41 34.19,1522.43 37.918,1521.94C43.203,1521.24 45.872,1529.95 47.627,1534.22C51.675,1544.07 54.521,1557.3 55.385,1568.52C55.778,1573.64 55.725,1579.51 51.525,1583.24C45.673,1588.44 35.247,1586.77 28.484,1584.69C26.289,1584.01 23.378,1582.61 21.849,1580.83C18.766,1577.23 18.423,1571.87 20.039,1567.56C20.423,1566.54 20.343,1564.6 21.125,1563.82" style="fill:rgb(199,199,199);stroke:black;stroke-width:2.88px;"/>
+                                    </g>
+                                    <g transform="matrix(1.03626,0,0,1.10001,1042.74,390.272)">
+                                        <path d="M25.367,1570.82C25.115,1570.78 25.065,1571.48 25.083,1571.66C25.176,1572.59 25.578,1573.65 26.169,1574.38C28.571,1577.31 31.248,1573.02 30.433,1570.58" style="fill:none;stroke:black;stroke-width:2.88px;"/>
+                                    </g>
+                                    <g transform="matrix(1.03626,0,0,1.10001,1042.74,390.272)">
+                                        <path d="M45.011,1564.28C44.693,1564.48 45.614,1563.19 45.683,1563.12C46.219,1562.51 46.815,1561.81 47.667,1561.69C48.778,1561.53 49.247,1562.86 50.286,1563.04C51.257,1563.2 53.054,1561.7 53.698,1561.05" style="fill:none;stroke:black;stroke-width:2.88px;"/>
+                                    </g>
+                                    <g transform="matrix(1.03626,0,0,1.10001,1042.74,390.272)">
+                                        <path d="M48.258,1568.81C47.969,1568.33 49.498,1567.2 49.889,1567C51.027,1566.43 51.127,1567.49 52.032,1567.72C52.79,1567.91 53.502,1567.4 54.095,1567" style="fill:none;stroke:black;stroke-width:2.88px;"/>
+                                    </g>
+                                    <g transform="matrix(1.03626,0,0,1.10001,1042.74,390.272)">
+                                        <path d="M43.215,1587.44C43.316,1587.97 43.167,1590.37 43.778,1590.49C44.382,1590.62 44.924,1590.26 45.524,1590.26" style="fill:none;stroke:black;stroke-width:2.88px;"/>
+                                    </g>
+                                    <g transform="matrix(1.03626,0,0,1.10001,1042.74,390.272)">
+                                        <path d="M37.981,1587.26C38.026,1587.91 38.451,1590.11 37.985,1590.57C37.851,1590.71 36.74,1590.26 36.477,1590.26" style="fill:none;stroke:black;stroke-width:2.88px;"/>
+                                    </g>
+                                    <g transform="matrix(1.03626,0,0,1.10001,1042.74,390.272)">
+                                        <path d="M34.837,1538.31L34.814,1538.31" style="fill:none;stroke:black;stroke-width:2.88px;"/>
+                                    </g>
+                                    <g transform="matrix(1.03626,0,0,1.10001,1042.74,390.272)">
+                                        <path d="M45.61,1537.59C45.601,1537.6 45.591,1537.6 45.581,1537.61" style="fill:none;stroke:black;stroke-width:2.88px;"/>
+                                    </g>
+                                </g>
+                                <g transform="matrix(1.02948,-0.0657608,0.0600841,1.05989,-156.163,-52.6654)">
+                                    <g transform="matrix(0.93298,-0.0881291,0.0918497,1.09568,906.468,400.319)">
+                                        <path d="M47.771,1529.12C47.999,1520.92 44.671,1519.42 39.83,1518.98C35.69,1518.61 30.168,1518.96 30.014,1528.01" style="fill:rgb(110,70,57);stroke:black;stroke-width:2.9px;"/>
+                                    </g>
+                                    <g transform="matrix(0.93298,-0.0881291,0.0918497,1.09568,906.468,400.319)">
+                                        <path d="M32.81,1529.09C26.098,1528.43 26.09,1524.95 26.005,1524.8C25.499,1523.93 24.786,1524.24 24.801,1525.06C24.877,1529.25 31.859,1530.28 39.892,1530.48C47.712,1530.66 53.183,1532.01 53.64,1526.7C53.648,1526.61 52.615,1525.51 52.355,1527.15C52.046,1529.09 50.371,1529.27 49.658,1529.66C48.122,1530.52 32.81,1529.09 32.81,1529.09Z" style="fill:rgb(110,70,57);stroke:black;stroke-width:2.9px;"/>
+                                    </g>
+                                </g>
+                            </g>
+                            <g transform="matrix(1.03399,0,0,1.08699,-91.6183,-190.681)">
+                                <g transform="matrix(1.36119,-0.556982,0.494297,1.36119,-1483.32,-153.845)">
+                                    <g transform="matrix(0.755034,-8.04527e-17,-2.17981e-16,1.10001,1066.66,390.272)">
+                                        <path d="M73.134,1609.24C73.834,1609.08 77.271,1608.22 78.164,1608.06C80.938,1607.56 84.549,1606.47 84.686,1607.9C84.795,1609.04 85.732,1609.5 84.529,1610.38C82.266,1612.05 76.102,1612.23 74.379,1612.7C72.644,1613.17 69.998,1609.98 73.134,1609.24Z" style="fill:rgb(111,92,75);stroke:black;stroke-width:2.4px;"/>
+                                    </g>
+                                    <g transform="matrix(1.03626,0,0,1.10001,1042.74,390.272)">
+                                        <path d="M85.176,1608.71C86.868,1608.08 88.388,1607.46 89.027,1607.33" style="fill:none;stroke:black;stroke-width:2.14px;"/>
+                                    </g>
+                                    <g transform="matrix(1.03626,0,0,1.10001,1042.74,390.272)">
+                                        <path d="M92.947,1600.53C85.983,1602.66 90.293,1613.59 96.86,1611.04C103.325,1608.52 99.96,1598.38 92.947,1600.53Z" style="fill:rgb(88,253,250);fill-opacity:0.61;"/>
+                                        <g>
+                                            <clipPath id="_clip2">
+                                                <path d="M92.947,1600.53C85.983,1602.66 90.293,1613.59 96.86,1611.04C103.325,1608.52 99.96,1598.38 92.947,1600.53Z"/>
+                                            </clipPath>
+                                            <g clip-path="url(#_clip2)">
+                                                <g transform="matrix(0.708684,0.192172,-0.192172,0.708684,338.379,455.474)">
+                                                    <path d="M93.768,1592.33C93.622,1592.15 90.147,1592.04 89.993,1592.17C89.23,1592.79 87.886,1593.98 87.628,1595.01C87.34,1596.16 89.783,1596.29 89.881,1597.47C90.064,1599.66 86.938,1600.98 84.556,1601.51C84.274,1601.58 87.618,1605.58 88.037,1605.35C88.394,1605.15 90.206,1604.27 90.563,1604.07C91.099,1603.78 91.58,1603.67 92.052,1603.28C93.825,1601.8 92.071,1600.19 91.816,1598.53C91.592,1597.08 93.972,1596.87 94.904,1595.85C95.166,1595.56 93.339,1592.33 93.768,1592.33Z" style="fill:white;fill-opacity:0.64;"/>
+                                                </g>
+                                            </g>
+                                        </g>
+                                        <path d="M92.947,1600.53C85.983,1602.66 90.293,1613.59 96.86,1611.04C103.325,1608.52 99.96,1598.38 92.947,1600.53Z" style="fill:none;stroke:black;stroke-width:2.14px;"/>
+                                    </g>
+                                </g>
+                                <g transform="matrix(1.05167,0,0,1.05167,-58.3869,-112.467)">
+                                    <g transform="matrix(1.03626,0,0,1.10001,1042.74,390.272)">
+                                        <path d="M76.605,1583.11C77.231,1583.15 78.435,1584.88 78.882,1585.32C80.676,1587.12 82.604,1589.66 83.606,1592C83.951,1592.81 84.372,1593.88 84.047,1594.77C83.746,1595.6 82.816,1596.09 82.094,1596.47C80.055,1597.56 77.528,1597.48 75.292,1597.73C74.587,1597.81 72.932,1597.63 72.394,1598.17" style="fill:rgb(211,132,29);stroke:black;stroke-width:2.84px;"/>
+                                    </g>
+                                    <g transform="matrix(1.55837,0.178522,-0.142346,1.40016,1233.92,-107.32)">
+                                        <path d="M71.784,1609.07C72.102,1608.13 74.975,1610.33 74.486,1611.63C73.984,1612.97 71.903,1613.73 70.659,1613.83C69.903,1613.89 69.194,1614.04 69.194,1613.1" style="fill:rgb(176,185,173);stroke:black;stroke-width:2.02px;"/>
+                                    </g>
+                                    <g transform="matrix(1.03626,0,0,1.10001,1042.74,390.272)">
+                                        <path d="M35.638,1592.24C35.354,1592.18 36.644,1592.65 36.973,1592.7C37.522,1592.79 40.064,1593.1 40.261,1592.7C42.235,1588.75 43.926,1584.71 46.471,1580.89C51.764,1572.95 55.732,1566.77 61.343,1558.97C63.948,1555.35 70.01,1551.32 73.017,1554.83C76.783,1559.23 79.123,1565.33 78.556,1571.42C77.7,1580.63 76.031,1590.29 74.13,1599.07C72.88,1604.85 69.983,1610.56 65.894,1614.26C63.201,1616.69 58.72,1617.79 54.118,1618.24C51.917,1618.46 46.113,1618.02 42.275,1616.46C35.55,1613.71 31.166,1609.23 30.561,1601.97C30.336,1599.28 31.815,1593.83 35.638,1592.24Z" style="fill:rgb(192,205,188);stroke:black;stroke-width:2.84px;"/>
+                                    </g>
+                                    <g transform="matrix(1.03626,0,0,1.10001,1042.74,390.272)">
+                                        <path d="M73.483,1575.04C73.571,1574.95 73.528,1574.96 73.528,1574.8" style="fill:none;stroke:black;stroke-width:2.84px;"/>
+                                    </g>
+                                    <g transform="matrix(1.03626,0,0,1.10001,1042.74,390.272)">
+                                        <path d="M65.226,1574.86C65.222,1574.86 65.218,1574.86 65.214,1574.87" style="fill:none;stroke:black;stroke-width:2.84px;"/>
+                                    </g>
+                                    <g transform="matrix(1.03626,0,0,1.10001,1042.74,390.272)">
+                                        <path d="M61.246,1603.44C61.579,1603.24 61.91,1603.26 62.317,1603.21C62.612,1603.18 62.91,1603.22 63.198,1603.27C66.14,1603.86 64.077,1606.55 65.592,1608.25C66.555,1609.33 68.309,1609.03 69.56,1609.45" style="fill:none;stroke:black;stroke-width:2.84px;"/>
+                                    </g>
+                                    <g transform="matrix(1.03626,0,0,1.10001,1042.74,390.272)">
+                                        <path d="M57.652,1608.52C58.231,1607.86 59.733,1608.06 59.986,1608.94C60.531,1610.85 58.277,1612.17 60.364,1613.79C60.67,1614.03 61.019,1614.2 61.372,1614.36C61.754,1614.53 62.161,1614.7 62.568,1614.8" style="fill:none;stroke:black;stroke-width:2.84px;"/>
+                                    </g>
+                                    <g transform="matrix(1.03626,0,0,1.10001,1042.74,390.272)">
+                                        <path d="M37.482,1600.57C37.081,1600.54 37.123,1601.26 37.185,1601.51C37.437,1602.52 37.916,1604.07 38.972,1604.52C40.96,1605.38 43.168,1603.34 44.428,1602.08" style="fill:none;stroke:black;stroke-width:2.84px;"/>
+                                    </g>
+                                    <g transform="matrix(1.03626,0,0,1.10001,1045.18,390.154)">
+                                        <path d="M51.547,1618.9C51.542,1618.21 51.91,1620.07 51.924,1620.28C51.991,1621.29 51.646,1622.62 52.113,1623.56C52.487,1624.3 54.251,1623.56 54.884,1623.56" style="fill:none;stroke:black;stroke-width:2.84px;"/>
+                                    </g>
+                                    <g transform="matrix(1.03626,0,0,1.10001,1045.75,390.505)">
+                                        <path d="M45.474,1618.13C45.695,1617.65 45.814,1619.9 45.814,1620.09C45.814,1621.32 45.625,1622.52 45.625,1623.74C45.625,1623.96 45.209,1623.62 44.995,1623.62C44.62,1623.62 42.035,1623.35 42.035,1623.68" style="fill:none;stroke:black;stroke-width:2.84px;"/>
+                                    </g>
+                                </g>
+                                <g transform="matrix(0.751171,0.0352167,-0.0312533,0.751171,350.428,396.129)">
+                                    <g transform="matrix(0.87435,0,0,0.912288,1048.47,704.568)">
+                                        <path d="M95.345,1672.23C91.826,1671.5 85.46,1669.64 84.185,1668.94C76.785,1664.83 60.248,1663.87 52.253,1667.3C48.004,1669.12 40.375,1670.26 40.225,1669.95C39.786,1669.08 48.436,1665.08 49.121,1663.44C51.828,1656.94 52.25,1652.23 57.834,1647.84C59.784,1646.31 65.355,1643.62 67.305,1643.57C69.566,1643.51 77.134,1644.71 80.723,1649.9C85.692,1657.1 84.76,1663.36 85.525,1664.89C85.97,1665.78 95.345,1671.48 95.345,1672.23Z" style="fill:rgb(125,115,105);stroke:black;stroke-width:4.73px;"/>
+                                    </g>
+                                    <g transform="matrix(1.03626,0,0,1.10001,1042.74,390.272)">
+                                        <path d="M61.228,1650.35C60.727,1649.93 60.212,1651.07 59.941,1651.45C58.687,1653.2 57.336,1655.05 56.649,1657.11" style="fill:none;stroke:black;stroke-width:2.96px;"/>
+                                    </g>
+                                    <g transform="matrix(1.03626,0,0,1.10001,1042.74,390.272)">
+                                        <path d="M65.88,1653.86C68.218,1655.7 68.685,1659.69 68.685,1662.45" style="fill:none;stroke:black;stroke-width:2.96px;"/>
+                                    </g>
+                                    <g transform="matrix(1.03626,0,0,1.10001,1042.74,390.272)">
+                                        <path d="M55.853,1666.64C55.019,1666.41 55.536,1664.88 55.671,1664.2" style="fill:none;stroke:black;stroke-width:2.96px;"/>
+                                    </g>
+                                    <g transform="matrix(1.03626,-3.78423e-17,9.12595e-18,1.94729,1043.72,-1006.55)">
+                                        <path d="M62.004,1648.09C61.563,1647.79 61.319,1647.16 61.021,1646.72C60.889,1646.52 60.492,1645.93 60.661,1646.1C61.318,1646.76 61.823,1648.25 62.975,1648.11C63.972,1647.98 64.13,1646 65.135,1646" style="fill:none;stroke:black;stroke-width:2.66px;"/>
+                                    </g>
+                                </g>
+                            </g>
+                            <g transform="matrix(1,0,0,1,49.0845,1.56129)">
+                                <g transform="matrix(1.03626,0,0,1.10001,1042.74,390.272)">
+                                    <path d="M81.233,1557.78C84.752,1559.16 88.111,1560.15 91.133,1562.62C92.041,1563.37 93.473,1564.73 93.139,1566.07C92.968,1566.75 92.033,1567.39 91.509,1567.76C89.697,1569.06 87.55,1570.21 85.556,1571.21" style="fill:rgb(217,122,22);stroke:black;stroke-width:3.17px;"/>
+                                </g>
+                                <g transform="matrix(1.03626,0,0,1.10001,1042.74,390.272)">
+                                    <path d="M36.306,1584.18C36.256,1583.08 42.03,1583.78 42.781,1583.03C43.112,1582.7 42.844,1561.97 43.009,1558.83C43.498,1549.54 42.617,1535.3 52.391,1530.42C60.588,1526.32 74.691,1543.48 79.817,1555.17C82.706,1561.76 90.802,1585.78 89.941,1592.38C87.614,1610.18 70.803,1610.06 57.589,1609.81C50.082,1609.67 39.924,1606.6 36.635,1603.57C32.866,1600.09 33.107,1594.58 33.487,1589.65C33.633,1587.75 34.598,1585.74 36.306,1584.18Z" style="fill:rgb(183,199,199);stroke:black;stroke-width:3.17px;"/>
+                                </g>
+                                <g transform="matrix(1.03626,0,0,1.10001,1042.74,390.272)">
+                                    <path d="M40.586,1592.09C40.587,1592.05 40.245,1591.7 40.208,1591.77C40.039,1592.11 40.335,1592.91 40.398,1593.22C40.601,1594.24 40.737,1596.27 41.831,1596.81C43.861,1597.83 45.454,1594.25 45.454,1592.72" style="fill:none;stroke:black;stroke-width:3.17px;"/>
+                                </g>
+                                <g transform="matrix(1.03626,0,0,1.10001,1042.74,390.272)">
+                                    <path d="M71.689,1554.72C71.626,1554.64 71.661,1554.68 71.582,1554.6" style="fill:none;stroke:black;stroke-width:3.17px;"/>
+                                </g>
+                                <g transform="matrix(1.03626,0,0,1.10001,1042.74,390.272)">
+                                    <path d="M67.241,1556.68C67.285,1556.66 67.254,1556.67 67.321,1556.73" style="fill:none;stroke:black;stroke-width:3.17px;"/>
+                                </g>
+                                <g transform="matrix(1.03626,0,0,1.10001,1042.74,390.272)">
+                                    <path d="M78.224,1580.49C78.146,1580.04 79.74,1579.18 80.101,1579.1C80.978,1578.9 81.595,1580.19 82.483,1580.29C83.629,1580.41 84.467,1579.44 85.509,1579.44" style="fill:none;stroke:black;stroke-width:3.17px;"/>
+                                </g>
+                                <g transform="matrix(1.03626,0,0,1.10001,1042.74,390.272)">
+                                    <path d="M82.292,1587.03C82.206,1586.55 83.493,1586.04 83.822,1585.94C85.445,1585.46 86.728,1586.95 88.437,1586.09" style="fill:none;stroke:black;stroke-width:3.17px;"/>
+                                </g>
+                                <g transform="matrix(1.0353,-0.0474472,0.0446976,1.09898,972.135,395.605)">
+                                    <path d="M73.83,1610.41C73.867,1610.34 75.422,1613.98 74.803,1615.01C74.54,1615.45 74.782,1615.67 73.955,1615.92C72.971,1616.21 71.061,1615.73 71.259,1615.78C71.455,1615.83 73.433,1616.2 74.051,1616.05C75.342,1615.75 76.317,1615.12 78.286,1614.23C80.551,1613.21 82.743,1611.9 83.665,1610.59" style="fill:none;stroke:black;stroke-width:3.98px;"/>
+                                </g>
+                                <g transform="matrix(1.03626,0,0,1.10001,1042.74,390.272)">
+                                    <path d="M48.038,1608.82C48.367,1608.62 47.852,1609.54 47.711,1609.89C47.381,1610.72 45.424,1613.61 46.4,1614.24L43.217,1612.45C43.217,1612.45 46.076,1614.14 46.941,1614.43C48.312,1614.89 48.08,1614.86 49.5,1615.12C50.162,1615.25 53.115,1615.41 54.522,1615.23" style="fill:none;stroke:black;stroke-width:3.97px;"/>
+                                </g>
+                            </g>
+                        </g>
+                    </g>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

+ 315 - 0
src/components/general/ClassFilter.vue

@@ -0,0 +1,315 @@
+<template>
+  <div class="flex gap-2">
+    <select
+      v-if="showType"
+      v-model="ruleset.column"
+      class="w-2/6 md:w-3/12"
+      name="searchType"
+      @change="filterTypeChanged"
+    >
+      <option
+        :value="ClassSelectorColumn.Module"
+        default
+      >
+        Modul
+      </option>
+      <option
+        :value="ClassSelectorColumn.Degree"
+        default
+      >
+        Studiengang
+      </option>
+      <option :value="ClassSelectorColumn.Room">
+        Raum
+      </option>
+      <option :value="ClassSelectorColumn.Class">
+        Klasse
+      </option>
+      <option :value="ClassSelectorColumn.Lecturer">
+        Dozent
+      </option>
+      <option :value="ClassSelectorColumn.Time">
+        Zeit
+      </option>
+      <option :value="ClassSelectorColumn.TeachingType">
+        Art
+      </option>
+    </select>
+
+    <div class="w-full justify-center">
+      <div
+        v-if="ruleset.column == ClassSelectorColumn.Time"
+        class="w-full gap-2 grid grid-cols-1"
+      >
+        <div class="flex items-center">
+          <span class="hidden md:inline w-12">Von:</span>
+          <WeekdayTimePicker
+            v-model="(ruleset.filterData as Record<string, number>).startTime"
+            :disable-greater-than="disableUpper"
+            class="w-full"
+          />
+        </div>
+        <div class="flex items-center">
+          <span class="hidden md:inline w-12">Bis:</span>
+          <WeekdayTimePicker
+            v-model="(ruleset.filterData as Record<string, number>).endTime"
+            :disable-lower-than="disableLower"
+            class="w-full"
+          />
+        </div>
+      </div>
+
+      <select
+        v-else-if="ruleset.column == ClassSelectorColumn.Degree"
+        id="degreeSelector"
+        v-model="ruleset.filterData.degree"
+        class="w-full"
+        @change="
+          () =>
+            stateStore.updateLastSelectedDegreeProgram(
+              ruleset.filterData.degree ?? '',
+            )
+        "
+      >
+        <option
+          default
+          value=""
+        >
+          Alle
+        </option>
+        <option
+          v-for="name in classesStore.degreePrograms"
+          :key="name"
+          :value="name"
+        >
+          {{ name }}
+        </option>
+      </select>
+
+      <select
+        v-else-if="ruleset.column == ClassSelectorColumn.TeachingType"
+        id="teachingTypeSelector"
+        v-model="ruleset.filterData.teachingType"
+        class="w-full capitalize"
+      >
+        <option
+          default
+          value=""
+        >
+          Alle
+        </option>
+        <option
+          v-for="name in TeachingType"
+          :key="name"
+          :value="name"
+        >
+          {{ name }}
+        </option>
+      </select>
+
+      <input
+        v-else
+        v-model="ruleset.filterData.term"
+        class="w-full"
+        name="search"
+        type="text"
+        :placeholder="searchPlaceholderText"
+        autocomplete="off"
+      >
+    </div>
+
+    <div
+      v-if="showAddRemove"
+      class="w-32 flex justify-center items-center"
+    >
+      <button
+        class="action-button"
+        title="Neuen Filter nach diesem einfügen"
+        @click="$emit('addFilter', ruleset)"
+      >
+        <font-awesome-icon icon="fa-solid fa-plus" />
+      </button>
+      <button
+        :disabled="isLastFilter"
+        class="action-button"
+        title="Diesen Filter entfernen"
+        @click="$emit('removeFilter', ruleset)"
+      >
+        <font-awesome-icon icon="fa-solid fa-minus" />
+      </button>
+      <div
+        title="Filtering ein/ausschalten"
+        class="action-checkbox"
+        @click="filterEnabledClick(false)"
+      >
+        <input
+          v-model="ruleset.enabled"
+          type="checkbox"
+          @change="filterEnabledClick(true)"
+        >
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts">
+import { PropType } from "vue";
+import { useClassesStore } from "../../stores/classes";
+import { useStateStore } from "../../stores/state";
+import { ClassSelectorColumn, FilterRule, TeachingType } from "../../types";
+import WeekdayTimePicker from "../general/WeekdayTimePicker.vue";
+
+export default {
+  name: "ClassFilter",
+  components: { WeekdayTimePicker },
+  props: {
+    modelValue: {
+      required: true,
+      type: Object as PropType<FilterRule>,
+    },
+    isLastFilter: {
+      required: false,
+      type: Boolean,
+      default: true,
+    },
+    showType: {
+      required: false,
+      type: Boolean,
+      default: true,
+    },
+    showAddRemove: {
+      required: false,
+      type: Boolean,
+      default: true,
+    },
+  },
+  emits: ["update:modelValue", "addFilter", "removeFilter"],
+  setup() {
+    const classesStore = useClassesStore();
+    const stateStore = useStateStore();
+
+    return {
+      ClassSelectorColumn,
+      classesStore,
+      stateStore,
+      TeachingType,
+    };
+  },
+  data() {
+    return {
+      ruleset: this.modelValue,
+    };
+  },
+  computed: {
+    disableUpper(): number {
+      return Math.floor(
+        ((this.ruleset.filterData as Record<string, number>).endTime ??
+          86400 * 7) / 86400,
+      );
+    },
+    disableLower(): number {
+      return Math.floor(
+        ((this.ruleset.filterData as Record<string, number>).startTime ?? 0) /
+          86400,
+      );
+    },
+    searchPlaceholderText(): string {
+      switch (this.ruleset.column) {
+        case ClassSelectorColumn.Module:
+          return "Suche nach Modulkürzel oder Name";
+        case ClassSelectorColumn.Room:
+          return "Suche nach Raumbezeichnung";
+        case ClassSelectorColumn.Class:
+          return "Suche nach Klassenbezeichnung";
+        case ClassSelectorColumn.Lecturer:
+          return "Suche nach Dozentenkürzel";
+        case ClassSelectorColumn.Time:
+          break;
+      }
+
+      return "";
+    },
+  },
+  methods: {
+    filterEnabledClick(direct: boolean) {
+      if (!direct) {
+        // Someone pressed the box around the checkbox.
+        this.ruleset.enabled = !this.ruleset.enabled;
+      } else {
+        // Only notify changes when the value change is detected directly
+        this.filterChanged();
+      }
+    },
+    filterChanged() {
+      this.$emit("update:modelValue", this.ruleset);
+    },
+    filterTypeChanged() {
+      if (this.ruleset.column == ClassSelectorColumn.Degree) {
+        if (!this.ruleset.filterData.degree)
+          this.ruleset.filterData.degree = this.classesStore.degreePrograms[0];
+      }
+      if (this.ruleset.column == ClassSelectorColumn.TeachingType) {
+        if (!this.ruleset.filterData.teachingType)
+          this.ruleset.filterData.teachingType = "";
+      }
+    },
+  },
+};
+</script>
+
+<style scoped>
+.action-button {
+  @apply h-5
+    text-xs
+    md:h-7
+    md:text-base
+    mx-0.5
+    md:mx-1
+    block
+    bg-gray-200
+    hover:bg-gray-300
+    active:bg-gray-400
+    disabled:bg-gray-200
+    disabled:text-gray-400
+    dark:bg-gray-700
+    dark:hover:bg-gray-600
+    dark:active:bg-gray-700
+    dark:disabled:bg-gray-800
+    dark:disabled:text-gray-600
+    disabled:pointer-events-none
+    aspect-square
+    rounded
+    transition-all
+    duration-200;
+}
+.action-checkbox {
+  @apply h-5
+    text-xs
+    m-2
+    md:h-7
+    md:text-base
+    mx-0.5
+    md:mx-1
+    bg-gray-200
+    hover:bg-gray-300
+    active:bg-gray-400
+    disabled:bg-gray-200
+    disabled:text-gray-400
+    dark:bg-gray-700
+    dark:hover:bg-gray-600
+    dark:active:bg-gray-700
+    dark:disabled:bg-gray-800
+    dark:disabled:text-gray-600
+    disabled:pointer-events-none
+    aspect-square
+    rounded
+    transition-all
+    duration-200
+    items-center
+    flex
+    cursor-pointer;
+}
+.action-checkbox input {
+  @apply cursor-pointer;
+}
+</style>

+ 316 - 0
src/components/general/ClassInfo.vue

@@ -0,0 +1,316 @@
+<template>
+  <div class="flex items-center justify-center w-full mt-5 gap-2">
+    <button
+      :title="chooseTitle"
+      class="action-button"
+      :disabled="hasCompletedModule || maxAttemptsReached"
+      @click="toggleClassState"
+    >
+      <font-awesome-icon
+        v-if="addable"
+        class="py-2 text-xl"
+        icon="fa-solid fa-plus"
+      />
+      <font-awesome-icon
+        v-else
+        class="py-2 text-xl"
+        icon="fa-solid fa-minus"
+      />
+    </button>
+
+    <button
+      title="Modulinformationen"
+      class="action-button"
+      :disabled="!module"
+      @click="stateStore.inspectingModule = module"
+    >
+      <font-awesome-icon
+        class="py-2 text-xl"
+        icon="fa-solid fa-puzzle-piece"
+      />
+    </button>
+
+    <a
+      :href="
+        !module
+          ? ''
+          : `${URLS.ADDITIONAL_MODULE_INFORMATION}${module.module_id}`
+      "
+      target="_blank"
+      :title="
+        module?.module_id === null
+          ? 'Keine Modul-ID vorhanden'
+          : `${SCHOOL_NAME} Modulbeschreibung`
+      "
+      class="action-button"
+      :class="[!module ? 'action-button-disabled' : '']"
+    >
+      <font-awesome-icon
+        class="py-2 text-xl"
+        icon="fa-solid fa-info"
+      />
+    </a>
+
+    <a
+      title="Klassen PDF in einem neuem Tab öffnen"
+      :href="classesPDFLink(cls)"
+      target="_blank"
+      class="action-button"
+    >
+      <font-awesome-icon
+        class="py-2 text-xl"
+        icon="fa-solid fa-file-pdf"
+      />
+    </a>
+  </div>
+
+  <table class="mt-5 w-full text-left">
+    <tbody>
+      <tr>
+        <td class="font-bold">
+          <span>Dozent</span>
+        </td>
+        <td>
+          <ul
+            class="list-inside"
+            :class="[lecturers.length > 1 ? 'list-disc' : '']"
+          >
+            <li
+              v-for="lecturer in lecturers"
+              :key="lecturer.short"
+            >
+              <span><b>{{ lecturer.short }}</b>: {{ lecturer.firstname }} {{ lecturer.surname }}</span>
+            </li>
+          </ul>
+        </td>
+      </tr>
+      <tr>
+        <td class="font-bold">
+          <span>Klasse</span>
+        </td>
+        <td>
+          <span>{{ cls.class }}</span>
+        </td>
+      </tr>
+      <tr>
+        <td class="font-bold">
+          <span>Raum</span>
+        </td>
+        <td>
+          <ul
+            class="list-inside"
+            :class="[cls.rooms.length > 1 ? 'list-disc' : '']"
+          >
+            <li
+              v-for="room in cls.rooms"
+              :key="room"
+            >
+              <span>{{ room }}</span>
+            </li>
+          </ul>
+        </td>
+      </tr>
+      <tr>
+        <td class="font-bold">
+          <span>Art</span>
+        </td>
+        <td>
+          <TeachingTypeIcon :teaching-type="cls.teaching_type" />
+        </td>
+      </tr>
+      <tr>
+        <td class="font-bold">
+          <span>Durchführung</span>
+        </td>
+        <td>
+          <span v-if="cls.weekday !== null">{{ dayMap[cls.weekday] }}, {{ toTime(cls.from) }} -
+            {{ toTime(cls.to) }}</span>
+          <span v-else>Blockmodul</span>
+        </td>
+      </tr>
+      <tr>
+        <td class="font-bold">
+          <span>ECTS</span>
+        </td>
+        <td>
+          <span>{{ module?.ects ?? "-" }}</span>
+        </td>
+      </tr>
+      <tr>
+        <td class="font-bold">
+          <span>Benotung</span>
+        </td>
+        <td>
+          <span>{{ module ? module.marksClean : "?" }}</span>
+        </td>
+      </tr>
+      <tr>
+        <td
+          :colspan="moduleClasses.length == 0 ? 1 : 2"
+          class="font-bold"
+        >
+          <span>Weitere Durchführungen</span>
+        </td>
+        <td>
+          <span v-if="moduleClasses.length == 0">Keine</span>
+        </td>
+      </tr>
+      <tr v-if="moduleClasses.length > 0">
+        <td
+          colspan="2"
+          class="pb-5"
+        >
+          <CurrentModuleExecutions
+            :module-classes="moduleClasses"
+            :highlight-class="cls"
+          />
+        </td>
+      </tr>
+      <tr>
+        <td class="font-bold">
+          <span>Abhängigkeiten</span>
+          <div
+            title="Abhängigkeiten sind teilweise sehr unklar von der Schule definiert und variieren basierend auf verschiedenen Dokumenten. Teilweise ist es nicht möglich, die Abhängigkeiten automatisch zu detektieren (keine Modulkürzel in der Abhängigkeitsbeschreibung)."
+            class="mx-2 w-5 text-xs aspect-square rounded-full bg-gray-200 dark:bg-gray-400 inline-flex items-center justify-center cursor-help"
+          >
+            <font-awesome-icon icon="fa-solid fa-info" />
+          </div>
+        </td>
+        <td>
+          <span>{{ module ? (hasModuleDeps ? "" : "Keine") : "-" }}</span>
+        </td>
+      </tr>
+    </tbody>
+  </table>
+</template>
+
+<script lang="ts">
+import { PropType } from "vue";
+import {
+  classesPDFLink,
+  dayMap,
+  toTime,
+  MAX_ATTEMPT_COUNT,
+} from "../../helpers";
+import { useClassesStore } from "../../stores/classes";
+import { useLecturersStore } from "../../stores/lecturers";
+import { useModulesStore } from "../../stores/modules";
+import { usePlanningStore } from "../../stores/planning";
+import { useStateStore } from "../../stores/state";
+import { useStudenthubStore } from "../../stores/studenthub";
+import { Module, TaughtClass, Lecturer, TeachingType } from "../../types";
+import CurrentModuleExecutions from "./CurrentModuleExecutions.vue";
+import TeachingTypeIcon from "./TeachingTypeIcon.vue";
+import { URLS, SCHOOL_NAME } from "../../helpers";
+
+export default {
+  name: "ClassInfo",
+  components: { CurrentModuleExecutions, TeachingTypeIcon },
+  props: {
+    cls: {
+      type: Object as PropType<TaughtClass>,
+      required: true,
+    },
+  },
+  setup() {
+    const modulesStore = useModulesStore();
+    const lecturersStore = useLecturersStore();
+    const planningStore = usePlanningStore();
+    const stateStore = useStateStore();
+    const studenthubStore = useStudenthubStore();
+    const classesStore = useClassesStore();
+    return {
+      modulesStore,
+      lecturersStore,
+      planningStore,
+      stateStore,
+      studenthubStore,
+      classesStore,
+      dayMap,
+      toTime,
+      classesPDFLink,
+      TeachingType,
+      URLS,
+      SCHOOL_NAME,
+    };
+  },
+  computed: {
+    lecturers(): Lecturer[] {
+      const r = this.lecturersStore.fromShort(this.cls.teachers);
+      return r;
+    },
+    hasCompletedModule(): boolean {
+      return this.studenthubStore.hasCompletedModule(
+        this.module?.module_id ?? null,
+      );
+    },
+    module(): Module | null {
+      return this.cls.module;
+    },
+    hasModuleDeps(): boolean {
+      if (this.module === null) return false;
+      return (
+        Object.values(this.module.dependencies).reduce(
+          (sum, dep) => sum + dep.length,
+          0,
+        ) > 0
+      );
+    },
+    maxAttemptsReached(): boolean {
+      return (this.module?.attemptCount ?? 0) >= MAX_ATTEMPT_COUNT;
+    },
+    chooseTitle(): string {
+      if (this.hasCompletedModule) return "Modul bereits bestanden";
+      if (this.maxAttemptsReached)
+        return `Du hast dieses Modul bereits ${this.module?.attemptCount} Mal versucht!`;
+      if (this.classChosen) return "Von der Planung entfernen";
+      return "Zur Planung hinzufügen";
+    },
+    classChosen(): boolean {
+      return this.planningStore.isModuleChosen(this.cls);
+    },
+    addable(): boolean {
+      return !this.classChosen || this.hasCompletedModule;
+    },
+    moduleClasses(): TaughtClass[] {
+      if (this.module === null) return [];
+      return this.classesStore.classesForModule(this.module.short);
+    },
+  },
+  methods: {
+    toggleClassState() {
+      if (this.classChosen) this.planningStore.remove(this.cls);
+      else this.planningStore.add(this.cls);
+    },
+  },
+};
+</script>
+
+<style scoped>
+.action-button {
+  @apply text-center
+    bg-gray-200
+    hover:bg-gray-300
+    active:bg-gray-400
+    dark:bg-gray-600
+    dark:hover:bg-gray-700
+    dark:active:bg-gray-900
+    rounded
+    cursor-pointer
+    w-full
+    p-0
+    transition-all
+    duration-200
+    disabled:dark:bg-gray-600
+    disabled:text-gray-500
+    disabled:bg-gray-100
+    disabled:pointer-events-none;
+}
+
+.action-button-disabled {
+  @apply pointer-events-none
+    dark:bg-gray-600
+    text-gray-500
+    bg-gray-100;
+}
+</style>

+ 178 - 0
src/components/general/ClassRemovedRow.vue

@@ -0,0 +1,178 @@
+<template>
+  <div
+    class="w-full rounded-lg drop-shadow-lg text-xs xl:text-base mb-5"
+    :class="
+      hasAlternativeChosen
+        ? 'bg-sky-200 dark:bg-sky-700'
+        : 'bg-white dark:bg-gray-600'
+    "
+  >
+    <div
+      class="flex flex-row gap-4 justify-between items-center m-2 py-3 mr-10"
+    >
+      <button
+        class="action-button flex-none"
+        :title="isOpen ? 'Schliessen' : 'Öffnen'"
+        @click="isOpen = !isOpen"
+      >
+        <font-awesome-icon
+          v-if="isOpen"
+          icon="fa-solid fa-chevron-down"
+        />
+        <font-awesome-icon
+          v-else
+          icon="fa-solid fa-chevron-right"
+        />
+      </button>
+
+      <span class="font-bold">{{ taughtClass.name }}</span>
+      <span class="">{{ taughtClass.executionTime }}</span>
+      <span class="">{{ taughtClass.class }}</span>
+      <span class="hidden md:inline-block">{{
+        taughtClass.teachers.join(", ")
+      }}</span>
+      <span class="hidden md:inline-block">{{
+        taughtClass.rooms.join(", ")
+      }}</span>
+      <span class="">
+        <TeachingTypeIcon :teaching-type="taughtClass.teaching_type" />
+      </span>
+    </div>
+
+    <hr
+      v-if="isOpen"
+      class="-mt-2"
+    >
+    <div
+      v-if="isOpen"
+      class="bg-white dark:bg-gray-800 w-full rounded-b-lg"
+    >
+      <table
+        v-if="alternatives.length > 0"
+        class="w-full upgrade-module-table"
+      >
+        <thead>
+          <tr>
+            <td class="pl-5">
+              Modul
+            </td>
+            <td>Durchführung</td>
+            <td>Klasse</td>
+            <td class="hidden md:table-cell">
+              Dozent
+            </td>
+            <td class="hidden md:table-cell">
+              Raum
+            </td>
+            <td>Art</td>
+            <td class="hidden md:table-cell">
+              MSP
+            </td>
+            <td class="w-24" />
+          </tr>
+        </thead>
+        <tbody>
+          <ClassRow
+            v-for="(alt, idx) in alternatives"
+            :key="alt.id"
+            :cls="alt"
+            :apply-row-class="idx == 0"
+          />
+        </tbody>
+      </table>
+      <div
+        v-else
+        class="w-full flex justify-center py-5 text-gray-800 dark:text-gray-300"
+      >
+        <span>Leider gibt es keine Alternativen!</span>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts">
+import { PropType } from "vue";
+import { TaughtClass } from "../../types";
+import ClassRow from "../general/ClassRow.vue";
+import { useClassesStore } from "../../stores/classes";
+import { usePlanningStore } from "../../stores/planning";
+import TeachingTypeIcon from "./TeachingTypeIcon.vue";
+
+export default {
+  name: "ClassRemovedRow",
+  components: { ClassRow, TeachingTypeIcon },
+  props: {
+    taughtClass: {
+      required: true,
+      type: Object as PropType<TaughtClass>,
+    },
+  },
+  data() {
+    return {
+      isOpen: false,
+    };
+  },
+  computed: {
+    alternatives(): TaughtClass[] {
+      const classesStore = useClassesStore();
+      return classesStore.allClasses.filter(
+        (cls) => cls.name == this.taughtClass.name,
+      );
+    },
+    hasAlternativeChosen(): boolean {
+      const alternatives = this.alternatives;
+      const planningStore = usePlanningStore();
+
+      return planningStore.chosen.some((el) => {
+        return alternatives.some((al) => el.id == al.id);
+      });
+    },
+  },
+};
+</script>
+
+<style>
+.upgrade-module-table thead td {
+  @apply border-b border-gray-700 h-8 text-gray-500 font-bold;
+}
+.upgrade-module-table thead tr {
+  @apply hover:bg-inherit;
+}
+.upgrade-module-table tbody td {
+  @apply border-0 h-12 cursor-pointer;
+}
+.upgrade-module-table tbody .row:last-child {
+  @apply border-b-0;
+}
+.upgrade-module-table .class-name {
+  @apply pl-5;
+}
+.upgrade-module-table tr {
+  @apply hover:bg-gray-900 border-0;
+}
+</style>
+
+<style scoped>
+.action-button {
+  @apply h-5
+    text-xs
+    md:h-7
+    md:text-base
+    mx-0.5
+    md:mx-1
+    block
+    hover:bg-gray-400
+    active:bg-gray-500
+    disabled:bg-gray-100
+    disabled:text-gray-400
+    dark:hover:bg-gray-800
+    dark:active:bg-gray-900
+    dark:disabled:bg-gray-800
+    dark:disabled:text-gray-600
+    disabled:pointer-events-none
+    aspect-square
+    rounded
+    transition-all
+    duration-200;
+}
+</style>

+ 189 - 0
src/components/general/ClassRow.vue

@@ -0,0 +1,189 @@
+<template>
+  <tr
+    class="row"
+    :class="[rowStyling]"
+  >
+    <td @click="showModuleInformation">
+      <span class="class-name">{{ cls.name }}</span>
+    </td>
+    <td @click="showModuleInformation">
+      <span> {{ cls.executionTime }}</span>
+    </td>
+    <td @click="showModuleInformation">
+      <span>{{ cls.class }}</span>
+    </td>
+    <td
+      class="hidden md:table-cell"
+      @click="showModuleInformation"
+    >
+      <span
+        v-for="teacher in cls.teachers"
+        :key="teacher"
+        class="block"
+      >{{
+        teacher
+      }}</span>
+    </td>
+    <td
+      class="hidden md:table-cell"
+      @click="showModuleInformation"
+    >
+      <span
+        v-for="room in cls.rooms"
+        :key="room"
+        class="block"
+      >{{
+        room
+      }}</span>
+    </td>
+    <td @click="showModuleInformation">
+      <TeachingTypeIcon :teaching-type="cls.teaching_type" />
+    </td>
+    <td
+      class="hidden xl:table-cell"
+      @click="showModuleInformation"
+    >
+      <span>{{ cls.module?.hasMSP === true ? "Ja" : "" }}</span>
+    </td>
+    <td class="cursor-default">
+      <button
+        :title="addRemoveClassTitle(cls.module, classChosen)"
+        class="action-button"
+        :disabled="cls.module?.hasCompleted || cls.module?.maxAttemptsReached"
+        @click="toggleClassState"
+      >
+        <font-awesome-icon
+          v-if="addable"
+          icon="fa-solid fa-plus"
+        />
+        <font-awesome-icon
+          v-else
+          icon="fa-solid fa-minus"
+        />
+      </button>
+      <a
+        title="Klassen PDF in einem neuem Tab öffnen"
+        target="_blank"
+        :href="classesPDFLink(cls)"
+      >
+        <button
+          class="action-button"
+          title="Klassen PDF in einem neuem Tab öffnen"
+        >
+          <!-- I just gave up styling the link... -->
+          <font-awesome-icon icon="fa-solid fa-file-pdf" />
+        </button>
+      </a>
+    </td>
+  </tr>
+</template>
+
+<script lang="ts">
+import { usePlanningStore } from "../../stores/planning";
+import {
+  classesPDFLink,
+  dayMap,
+  toTime,
+  addRemoveClassTitle,
+  rowStyling,
+} from "../../helpers";
+import { TaughtClass, ClassSelectorColumn, TeachingType } from "../../types";
+import { PropType } from "vue";
+import { useStudenthubStore } from "../../stores/studenthub";
+import { useStateStore } from "../../stores/state";
+import TeachingTypeIcon from "./TeachingTypeIcon.vue";
+
+export default {
+  name: "ClassRow",
+  components: { TeachingTypeIcon },
+  props: {
+    cls: {
+      required: true,
+      type: Object as PropType<TaughtClass>,
+    },
+    applyRowClass: {
+      required: false,
+      type: Boolean,
+      default: true,
+    },
+  },
+  setup() {
+    const planningStore = usePlanningStore();
+    const studenthubStore = useStudenthubStore();
+    const stateStore = useStateStore();
+    return {
+      planningStore,
+      studenthubStore,
+      stateStore,
+      toTime,
+      classesPDFLink,
+      addRemoveClassTitle,
+      dayMap,
+      ClassSelectorColumn,
+      TeachingType,
+    };
+  },
+  data() {
+    return {
+      currentPageIdx: 0,
+      inspectingClass: null as TaughtClass | null,
+    };
+  },
+  computed: {
+    classChosen(): boolean {
+      return this.planningStore.isModuleChosen(this.cls);
+    },
+    addable(): boolean {
+      return !this.classChosen || (this.cls.module?.hasCompleted ?? false);
+    },
+    rowStyling(): string {
+      return rowStyling(this.cls.module);
+    },
+  },
+  methods: {
+    showModuleInformation() {
+      this.stateStore.inspectingClass = this.cls;
+    },
+    toggleClassState() {
+      if (this.classChosen) this.planningStore.remove(this.cls);
+      else this.planningStore.add(this.cls);
+    },
+  },
+};
+</script>
+
+<style scoped>
+.row {
+  @apply border-b
+    cursor-pointer
+    dark:border-gray-500
+    hover:bg-slate-200
+    dark:hover:bg-gray-700
+    transition-all
+    duration-100;
+}
+
+.module-table .row td {
+  @apply border-x border-gray-300 dark:border-gray-500 h-11;
+}
+
+.action-button {
+  @apply rounded-lg
+    aspect-square
+    inline-block
+    w-6
+    text-xs
+    md:w-8
+    md:text-base
+    hover:bg-gray-300
+    active:bg-gray-300
+    dark:hover:bg-gray-500
+    dark:active:bg-gray-600
+    mx-0.5
+    transition-all
+    duration-200
+    disabled:hover:bg-inherit
+    disabled:text-gray-300
+    disabled:dark:text-gray-500;
+}
+</style>

+ 67 - 0
src/components/general/CurrentModuleExecutions.vue

@@ -0,0 +1,67 @@
+<template>
+  <table class="w-full">
+    <thead>
+      <tr
+        class="border-b dark:border-gray-500 font-bold bg-gray-200 h-10 dark:bg-gray-500 dark:text-white"
+      >
+        <td>Zeit</td>
+        <td>Klasse</td>
+        <td>Dozent</td>
+        <td>Raum</td>
+        <td />
+      </tr>
+    </thead>
+    <tbody>
+      <CurrentModuleExecutionsRow
+        v-for="cls in moduleClasses"
+        :key="cls.id"
+        :cls="cls"
+        :highlight="cls === highlightClass"
+      />
+    </tbody>
+  </table>
+</template>
+
+<script lang="ts">
+import { PropType } from "vue";
+import { addRemoveClassTitle, classesPDFLink } from "../../helpers";
+import { usePlanningStore } from "../../stores/planning";
+import { TaughtClass } from "../../types";
+import CurrentModuleExecutionsRow from "./CurrentModuleExecutionsRow.vue";
+
+export default {
+  name: "CurrentModuleExecutions",
+  components: { CurrentModuleExecutionsRow },
+  props: {
+    moduleClasses: {
+      required: true,
+      type: Object as PropType<TaughtClass[]>,
+    },
+    highlightClass: {
+      required: false,
+      type: Object as PropType<TaughtClass | undefined>,
+      default: undefined,
+    },
+  },
+  setup() {
+    const planningStore = usePlanningStore();
+    return {
+      planningStore,
+      classesPDFLink,
+      addRemoveClassTitle,
+    };
+  },
+  methods: {
+    classChosen(cls: TaughtClass): boolean {
+      return this.planningStore.isModuleChosen(cls);
+    },
+    toggleClassState(cls: TaughtClass) {
+      if (this.classChosen(cls)) this.planningStore.remove(cls);
+      else this.planningStore.add(cls);
+    },
+    addable(cls: TaughtClass): boolean {
+      return !this.classChosen || (cls.module?.hasCompleted ?? false);
+    },
+  },
+};
+</script>

+ 150 - 0
src/components/general/CurrentModuleExecutionsRow.vue

@@ -0,0 +1,150 @@
+<template>
+  <tr
+    class="row"
+    :class="[highlight ? 'italic bg-gray-300 dark:bg-gray-900' : '']"
+  >
+    <td
+      class="cursor-pointer"
+      @click="clickedRow"
+    >
+      {{ cls.executionTime }}
+    </td>
+    <td
+      class="cursor-pointer"
+      @click="clickedRow"
+    >
+      {{ cls.class }}
+    </td>
+    <td
+      class="cursor-pointer"
+      @click="clickedRow"
+    >
+      {{ cls.teachers.join(", ") }}
+    </td>
+    <td
+      class="cursor-pointer"
+      @click="clickedRow"
+    >
+      <span class="mr-2">{{ cls.rooms.join(", ") }}</span>
+      <TeachingTypeIcon :teaching-type="cls.teaching_type" />
+    </td>
+    <td>
+      <button
+        :title="addRemoveClassTitle(cls.module, classChosen)"
+        class="action-button"
+        :disabled="cls.module?.hasCompleted || cls.module?.maxAttemptsReached"
+        @click="toggleClassState"
+      >
+        <font-awesome-icon
+          v-if="addable"
+          icon="fa-solid fa-plus"
+        />
+        <font-awesome-icon
+          v-else
+          icon="fa-solid fa-minus"
+        />
+      </button>
+      <a
+        title="Klassen PDF in einem neuem Tab öffnen"
+        target="_blank"
+        :href="classesPDFLink(cls)"
+      >
+        <button
+          class="action-button"
+          title="Klassen PDF in einem neuem Tab öffnen"
+        >
+          <!-- I just gave up styling the link... -->
+          <font-awesome-icon icon="fa-solid fa-file-pdf" />
+        </button>
+      </a>
+    </td>
+  </tr>
+</template>
+
+<script lang="ts">
+import { PropType } from "vue";
+import { addRemoveClassTitle, classesPDFLink } from "../../helpers";
+import { usePlanningStore } from "../../stores/planning";
+import { useStateStore } from "../../stores/state";
+import { TaughtClass } from "../../types";
+import TeachingTypeIcon from "../general/TeachingTypeIcon.vue";
+
+export default {
+  name: "CurrentModuleExecutionsRow",
+  components: { TeachingTypeIcon },
+  props: {
+    cls: {
+      required: true,
+      type: Object as PropType<TaughtClass>,
+    },
+    highlight: {
+      required: false,
+      type: Boolean,
+      default: false,
+    },
+  },
+  setup() {
+    const planningStore = usePlanningStore();
+    const stateStore = useStateStore();
+    return {
+      planningStore,
+      stateStore,
+      classesPDFLink,
+      addRemoveClassTitle,
+    };
+  },
+  computed: {
+    classChosen(): boolean {
+      return this.planningStore.isModuleChosen(this.cls);
+    },
+    addable(): boolean {
+      return !this.classChosen || (this.cls.module?.hasCompleted ?? false);
+    },
+  },
+  methods: {
+    toggleClassState() {
+      if (this.classChosen) this.planningStore.remove(this.cls);
+      else this.planningStore.add(this.cls);
+    },
+    clickedRow() {
+      this.stateStore.inspectingClass = this.cls;
+      this.stateStore.inspectingModule = null;
+    },
+  },
+};
+</script>
+
+<style scoped>
+.row {
+  @apply border-b
+    dark:border-gray-500
+    hover:bg-slate-200
+    dark:hover:bg-gray-700
+    transition-all
+    duration-100;
+}
+.module-table td {
+  @apply border-x border-gray-300 dark:border-gray-500 h-11;
+}
+
+.action-button {
+  @apply rounded-lg
+    aspect-square
+    inline-block
+    w-6
+    text-xs
+    md:w-8
+    md:text-base
+    hover:bg-gray-300
+    active:bg-gray-300
+    dark:hover:bg-gray-500
+    dark:active:bg-gray-600
+    mx-0.5
+    transition-all
+    duration-200
+    disabled:hover:bg-inherit
+    disabled:text-gray-300
+    disabled:dark:text-gray-500
+    cursor-pointer;
+}
+</style>

+ 205 - 0
src/components/general/DependencyTree.vue

@@ -0,0 +1,205 @@
+<template>
+  <div class="flex justify-end items-center">
+    <button
+      v-for="lang in availableLanguages"
+      :key="lang"
+      class="language-button"
+      :disabled="lang == activeLang"
+      :title="`Modulabhängigkeits-Sprache ändern auf ${lang}`"
+      @click="activeLang = lang"
+    >
+      {{ lang }}
+    </button>
+
+    <div
+      v-if="hasData && availableLanguages.length > 0"
+      :title="`Die ${SCHOOL_NAME} hat teils Modulbeschreibungen in Deutsch & Englisch. Je nach Sprache des Unterrichts, können sich die Abhängigkeiten ändern. Standardmässig wir die Sprache des Unterrichts ausgewählt, sonst die erst verfügbare Sprache.`"
+      class="mx-2 w-5 h-5 text-xs aspect-square rounded-full bg-gray-200 dark:bg-gray-400 inline-flex items-center justify-center cursor-help"
+    >
+      <font-awesome-icon icon="fa-solid fa-info" />
+    </div>
+  </div>
+  <div class="mt-5">
+    <div
+      v-if="module && hasEnablers"
+      class="flex justify-center flex-wrap gap-3"
+    >
+      <button
+        v-for="dep in module.enablingModules[activeLang]"
+        :key="dep"
+        class="module-box"
+        title="Modulinformationen anzeigen"
+        :class="[rowStyling(moduleStore.fromModuleShort(dep), defaultActionBG)]"
+        @click="stateStore.inspectingModule = moduleStore.fromModuleShort(dep)"
+      >
+        {{ dep }}
+      </button>
+    </div>
+    <div
+      v-else
+      class="flex justify-center flex-wrap gap-3"
+    >
+      <span class="text-gray-500">Keine Nachfolgemodule</span>
+    </div>
+
+    <div class="flex justify-center mt-3">
+      <font-awesome-icon icon="fa-solid fa-arrow-up" />
+    </div>
+
+    <div class="flex justify-center mt-3">
+      <button
+        class="module-box font-bold"
+        :class="[rowStyling(module, defaultActionBG)]"
+        title="Modulinformationen anzeigen"
+        @click="stateStore.inspectingModule = module"
+      >
+        {{ module?.short ?? moduleName }}
+      </button>
+    </div>
+
+    <div class="flex justify-center mt-3">
+      <font-awesome-icon icon="fa-solid fa-arrow-up" />
+    </div>
+
+    <div
+      v-if="module && hasDeps"
+      class="flex justify-center flex-wrap gap-3 mt-3"
+    >
+      <button
+        v-for="dep in module.dependencies[activeLang]"
+        :key="dep"
+        class="module-box"
+        title="Modulinformationen anzeigen"
+        :class="[rowStyling(moduleStore.fromModuleShort(dep), defaultActionBG)]"
+        @click="stateStore.inspectingModule = moduleStore.fromModuleShort(dep)"
+      >
+        {{ dep }}
+      </button>
+    </div>
+    <div
+      v-else
+      class="flex justify-center flex-wrap gap-3 mt-3"
+    >
+      <span class="text-gray-500">Keine Abhängigkeiten</span>
+    </div>
+  </div>
+</template>
+
+<script lang="ts">
+import { PropType } from "vue";
+import { useModulesStore } from "../../stores/modules";
+import { useStateStore } from "../../stores/state";
+import { Module, ModuleLanguages } from "../../types";
+import { rowStyling, SCHOOL_NAME } from "../../helpers";
+
+function availableLanguages(module: Module): string[] {
+  const deps = module.dependencies;
+  const langs = Object.keys(deps).filter((o) => deps[o].length > 0);
+
+  const eDeps = module.enablingModules;
+  langs.push(...Object.keys(eDeps).filter((o) => eDeps[o].length > 0));
+  return [...new Set(langs)];
+}
+
+export default {
+  name: "DependencyTree",
+  props: {
+    module: {
+      required: true,
+      type: Object as PropType<Module | null>,
+    },
+    moduleName: {
+      required: true,
+      type: String,
+    },
+    classLanguage: {
+      required: false,
+      type: String,
+      default: ModuleLanguages.DE,
+    },
+  },
+  setup() {
+    const stateStore = useStateStore();
+    const moduleStore = useModulesStore();
+
+    return {
+      stateStore,
+      moduleStore,
+      rowStyling,
+      defaultActionBG: "bg-gray-200 dark:bg-gray-600",
+      SCHOOL_NAME,
+    };
+  },
+  data() {
+    let lang = this.classLanguage ?? "";
+    if (this.module !== null) {
+      const available = availableLanguages(this.module);
+      if (available.length > 0 && !available.includes(lang))
+        lang = available[0];
+    }
+
+    return {
+      activeLang: lang,
+    };
+  },
+  computed: {
+    availableLanguages(): string[] {
+      if (this.module === null) return [];
+      return availableLanguages(this.module);
+    },
+    hasDeps(): boolean {
+      return (
+        this.module !== null &&
+        this.module.dependencies[this.activeLang]?.length > 0
+      );
+    },
+    hasEnablers(): boolean {
+      return (
+        this.module !== null &&
+        (this.module.enablingModules[this.activeLang]?.length ?? 0) > 0
+      );
+    },
+    hasData(): boolean {
+      return this.hasDeps || this.hasEnablers;
+    },
+  },
+};
+</script>
+
+<style scoped>
+.module-box {
+  @apply w-28
+    py-2
+    rounded
+    text-center
+    hover:bg-gray-300
+    active:bg-gray-400
+    dark:hover:bg-gray-700
+    dark:active:bg-gray-900
+    cursor-pointer
+    transition-all
+    duration-200;
+}
+
+.language-button {
+  @apply text-center
+    bg-gray-200
+    hover:bg-gray-300
+    active:bg-gray-400
+    dark:bg-gray-600
+    dark:hover:bg-gray-700
+    dark:active:bg-gray-900
+    rounded
+    cursor-pointer
+    px-2
+    py-0.5
+    ml-2
+    transition-all
+    duration-200
+    disabled:dark:bg-blue-600
+    disabled:dark:text-gray-300
+    disabled:text-gray-400
+    disabled:bg-gray-100
+    disabled:pointer-events-none;
+}
+</style>

+ 280 - 0
src/components/general/ModuleInfo.vue

@@ -0,0 +1,280 @@
+<template>
+  <table class="mt-5 w-full text-left main-table">
+    <tbody>
+      <tr>
+        <td class="font-bold">
+          <span>ECTS</span>
+        </td>
+        <td>
+          <span>{{ module?.ects ?? "-" }}</span>
+        </td>
+      </tr>
+      <tr>
+        <td class="font-bold">
+          <span>Benotung</span>
+        </td>
+        <td>
+          <span>{{ module ? module.marksClean : "?" }}</span>
+        </td>
+      </tr>
+      <tr v-if="(module.for_degrees?.length ?? 0) > 1">
+        <td class="font-bold">
+          <span>Studiengänge</span>
+        </td>
+        <td>
+          <ul class="list-disc ml-4">
+            <li
+              v-for="degree in module.for_degrees"
+              :key="degree"
+            >
+              {{ degree }}
+            </li>
+          </ul>
+        </td>
+      </tr>
+      <tr v-else>
+        <td class="font-bold">
+          <span>Studiengang</span>
+        </td>
+        <td>
+          <span>{{ module.for_degrees ? module.for_degrees[0] : "-" }}</span>
+        </td>
+      </tr>
+      <tr>
+        <td class="font-bold">
+          <span>Detailbeschreibung</span>
+        </td>
+        <td>
+          <a
+            :href="`${URLS.ADDITIONAL_MODULE_INFORMATION}${module?.module_id}`"
+            target="_blank"
+            class="external-link"
+            :title="
+              module?.module_id === null
+                ? 'Keine Modul-ID vorhanden'
+                : `${SCHOOL_NAME} Modulbeschreibung`
+            "
+          >
+            <span class="mr-2">{{ SCHOOL_NAME }}</span>
+            <font-awesome-icon icon="fa-solid fa-arrow-up-right-from-square" />
+          </a>
+        </td>
+      </tr>
+
+      <!------------------------->
+
+      <tr>
+        <td class="font-bold">
+          <span>Abgeschlossen</span>
+        </td>
+        <td>
+          <span>{{ completionStateString }}</span>
+        </td>
+      </tr>
+      <tr>
+        <td class="font-bold">
+          <span>Bisherige Versuche</span>
+        </td>
+        <td>
+          <span>{{
+            module.attemptCount > 0 ? module.attemptCount : "Keine"
+          }}</span>
+        </td>
+      </tr>
+
+      <tr>
+        <td
+          :colspan="moduleClasses.length == 0 ? 1 : 2"
+          class="font-bold"
+        >
+          <span>Nächste Durchführungen</span>
+        </td>
+        <td>
+          <span v-if="moduleClasses.length == 0">Keine</span>
+        </td>
+      </tr>
+      <tr v-if="moduleClasses.length > 0">
+        <td
+          colspan="2"
+          class="pb-5"
+        >
+          <CurrentModuleExecutions :module-classes="moduleClasses" />
+        </td>
+      </tr>
+
+      <tr>
+        <td
+          class="font-bold"
+          :colspan="previousClasses.length == 0 ? 1 : 2"
+        >
+          <button
+            v-if="previousClasses.length > 0"
+            class="action-button mr-2"
+            :title="
+              'Vergangene Durchführungen ' +
+                (showingPastClasses ? 'ausblenden' : 'einblenden')
+            "
+            @click="() => (showingPastClasses = !showingPastClasses)"
+          >
+            <font-awesome-icon
+              v-if="showingPastClasses"
+              icon="fa-solid fa-chevron-down"
+            />
+            <font-awesome-icon
+              v-else
+              icon="fa-solid fa-chevron-right"
+            />
+          </button>
+
+          <span>Vergangene Durchführungen</span>
+          <span
+            v-if="previousClasses.length > 0"
+            class="font-bold"
+          >
+            ({{ previousClasses.length }})</span>
+        </td>
+        <td>
+          <span v-if="previousClasses.length == 0">Keine</span>
+        </td>
+      </tr>
+      <tr v-if="previousClasses.length > 0 && showingPastClasses">
+        <td
+          colspan="2"
+          class="pb-5"
+        >
+          <PreviousModuleExecutions :previous-classes="previousClasses" />
+        </td>
+      </tr>
+
+      <tr>
+        <td class="font-bold">
+          <span>Abhängigkeiten</span>
+          <div
+            title="Abhängigkeiten sind teilweise sehr unklar von der Schule definiert und variieren basierend auf verschiedenen Dokumenten. Teilweise ist es nicht möglich, die Abhängigkeiten automatisch zu detektieren (keine Modulkürzel in der Abhängigkeitsbeschreibung)."
+            class="mx-2 w-5 text-xs aspect-square rounded-full bg-gray-200 dark:bg-gray-400 inline-flex items-center justify-center cursor-help"
+          >
+            <font-awesome-icon icon="fa-solid fa-info" />
+          </div>
+        </td>
+        <td>
+          <span>{{ module ? (hasModuleDeps ? "" : "Keine") : "-" }}</span>
+        </td>
+      </tr>
+    </tbody>
+  </table>
+</template>
+
+<script lang="ts">
+import { PropType } from "vue";
+import {
+  classesPDFLink,
+  dayMap,
+  toTime,
+  MAX_ATTEMPT_COUNT,
+} from "../../helpers";
+import { useClassesStore } from "../../stores/classes";
+import { useLecturersStore } from "../../stores/lecturers";
+import { useModulesStore } from "../../stores/modules";
+import { usePlanningStore } from "../../stores/planning";
+import { useStateStore } from "../../stores/state";
+import { useStudenthubStore } from "../../stores/studenthub";
+import { HistoricClassEntry, Module, TaughtClass } from "../../types";
+import CurrentModuleExecutions from "./CurrentModuleExecutions.vue";
+import PreviousModuleExecutions from "./PreviousModuleExecutions.vue";
+import { URLS, SCHOOL_NAME } from "../../helpers";
+
+export default {
+  name: "ModuleInfo",
+  components: { CurrentModuleExecutions, PreviousModuleExecutions },
+  props: {
+    module: {
+      type: Object as PropType<Module>,
+      required: true,
+    },
+  },
+  setup() {
+    const modulesStore = useModulesStore();
+    const lecturersStore = useLecturersStore();
+    const planningStore = usePlanningStore();
+    const stateStore = useStateStore();
+    const studenthubStore = useStudenthubStore();
+    const classesStore = useClassesStore();
+
+    return {
+      modulesStore,
+      lecturersStore,
+      planningStore,
+      stateStore,
+      studenthubStore,
+      classesStore,
+      dayMap,
+      toTime,
+      classesPDFLink,
+      URLS,
+      SCHOOL_NAME,
+    };
+  },
+  data() {
+    return {
+      showingPastClasses: false,
+    };
+  },
+  computed: {
+    hasCompletedModule(): boolean {
+      return this.studenthubStore.hasCompletedModule(
+        this.module?.module_id ?? null,
+      );
+    },
+    hasModuleDeps(): boolean {
+      if (this.module === null) return false;
+      return (
+        Object.values(this.module.dependencies).reduce(
+          (sum, dep) => sum + dep.length,
+          0,
+        ) > 0
+      );
+    },
+    maxAttemptsReached(): boolean {
+      return this.module.attemptCount >= MAX_ATTEMPT_COUNT;
+    },
+    moduleClasses(): TaughtClass[] {
+      return this.classesStore.classesForModule(this.module.short);
+    },
+    previousClasses(): HistoricClassEntry[] {
+      return this.modulesStore.history[this.module.short] ?? [];
+    },
+    completionStateString(): string {
+      if (this.module.hasCompleted) return "Ja";
+      if (this.module.isActive) return "In Bearbeitung";
+      if (this.module.hasFailed) return "Fehlgeschlagen";
+      return "Nein";
+    },
+  },
+};
+</script>
+
+<style scoped>
+.action-button {
+  @apply text-center
+    bg-gray-200
+    hover:bg-gray-300
+    active:bg-gray-400
+    dark:bg-gray-600
+    dark:hover:bg-gray-700
+    dark:active:bg-gray-900
+    rounded
+    cursor-pointer
+    w-6
+    p-0
+    transition-all
+    duration-200
+    disabled:dark:bg-gray-600
+    disabled:text-gray-500
+    disabled:bg-gray-100
+    disabled:pointer-events-none;
+}
+
+.main-table td {
+  @apply py-1 align-top;
+}
+</style>

+ 177 - 0
src/components/general/ModulesetManager.vue

@@ -0,0 +1,177 @@
+<template>
+  <div class="flex justify-center items-center">
+    <label
+      class="mr-2"
+      for="planSelector"
+    >Plan:</label>
+    <select
+      id="planSelector"
+      v-model="planningStore.currentPlanName"
+      title="Momentaner Plan auswählen. Es können mehrere Pläne erstellt werden und dazwischen gewechselt werden."
+    >
+      <option
+        v-for="plan in planningStore.allPlans"
+        :key="plan.name"
+        :value="plan.name"
+      >
+        {{ plan.name }} ({{ parsePDFVersion(plan.planVersion).semester }})
+      </option>
+    </select>
+
+    <button
+      title="Planung bearbeiten"
+      class="action-button"
+      @click="editCurrentPlan"
+    >
+      <font-awesome-icon icon="fa-solid fa-pencil" />
+    </button>
+    <button
+      title="Neue Planung erstellen"
+      class="action-button"
+      @click="newPlan"
+    >
+      <font-awesome-icon icon="fa-solid fa-plus" />
+    </button>
+    <button
+      title="Planung löschen"
+      class="action-button"
+      @click="deletePlan"
+    >
+      <font-awesome-icon icon="fa-solid fa-trash-can" />
+    </button>
+    <button
+      title="Planung mit anderen teilen"
+      class="action-button"
+      @click="copyShareURL"
+    >
+      <font-awesome-icon icon="fa-solid fa-share" />
+    </button>
+
+    <div class="ml-2 border-l border-gray-500 h-full">
+&nbsp;
+    </div>
+
+    <button
+      title="Alle Module durchsuchen"
+      class="action-button mr-0"
+      @click="showModuleSearch"
+    >
+      <font-awesome-icon icon="fa-solid fa-magnifying-glass" />
+    </button>
+
+    <button
+      title="Einstellungen"
+      class="action-button mr-0"
+      @click="showSettings"
+    >
+      <font-awesome-icon icon="fa-solid fa-cog" />
+    </button>
+  </div>
+  <hr class="my-2 dark:border-zinc-600">
+
+  <ModuleSetEdit
+    :moduleset-name="editingModuleSet"
+    :is-new="addNewModuleSet"
+    @close="
+      () => {
+        editingModuleSet = undefined;
+        addNewModuleSet = false;
+      }
+    "
+  />
+</template>
+
+<script lang="ts">
+import ModuleSetEdit from "../modals/ModulesetEdit.vue";
+import { useToast } from "vue-toastification";
+import { copyToClipboard, parsePDFVersion } from "../../helpers";
+import { usePlanningStore } from "../../stores/planning";
+import { useStateStore } from "../../stores/state";
+
+export default {
+  name: "LoadSaveView",
+  components: { ModuleSetEdit },
+  setup() {
+    const planningStore = usePlanningStore();
+    const stateStore = useStateStore();
+    const toast = useToast();
+
+    return {
+      planningStore,
+      stateStore,
+      toast,
+      parsePDFVersion,
+    };
+  },
+  data() {
+    return {
+      editingModuleSet: undefined as string | undefined,
+      addNewModuleSet: false,
+    };
+  },
+  methods: {
+    editCurrentPlan() {
+      this.addNewModuleSet = false;
+      this.editingModuleSet = this.planningStore.currentPlanName;
+    },
+    newPlan() {
+      this.editingModuleSet = undefined;
+      this.addNewModuleSet = true;
+    },
+    deletePlan() {
+      if (
+        !confirm(
+          `Zusammengestellten Plan '${this.planningStore.currentPlanName}' löschen?`,
+        )
+      )
+        return;
+      this.planningStore.deleteChosen();
+    },
+    copyShareURL() {
+      const shareURL = this.planningStore.getShareURL();
+
+      const copy = copyToClipboard(shareURL);
+      if (copy == null) {
+        this.toast.error(
+          `Konnte die URL nicht in die Zwischenablage kopieren (${shareURL})`,
+        );
+        return;
+      }
+
+      copy.then(
+        () => {
+          this.toast.success(
+            `Der Share-Link wurde in die Zwischenablage kopiert`,
+          );
+        },
+        () => {
+          this.toast.error(
+            `Konnte die URL nicht in die Zwischenablage kopieren (${shareURL})`,
+          );
+        },
+      );
+    },
+    showSettings() {
+      this.stateStore.showingSettings = true;
+    },
+    showModuleSearch() {
+      this.stateStore.showingModuleSearch = true;
+    },
+  },
+};
+</script>
+
+<style scoped>
+.action-button {
+  @apply w-10
+    mx-1
+    hover:bg-gray-300
+    active:bg-gray-400
+    dark:hover:bg-gray-600
+    dark:active:bg-gray-700
+    aspect-square
+    rounded
+    transition-all
+    duration-200;
+}
+</style>

+ 145 - 0
src/components/general/NumberedPagination.vue

@@ -0,0 +1,145 @@
+<template>
+  <div
+    v-if="pageCount > 1"
+    class="flex justify-center pt-5 text-xs md:text-base"
+  >
+    <nav aria-label="module-nav">
+      <ul class="flex list-style-none dark:text-gray-200 text-gray-800">
+        <li class="page-item">
+          <button
+            aria-label="Vorhergehende Seite"
+            class="pagination-button"
+            :disabled="currentPageIdx == 0"
+            @click="
+              () => {
+                selectPage(Math.max(currentPageIdx - 1, 0));
+              }
+            "
+          >
+            <font-awesome-icon icon="fa-solid fa-arrow-left" />
+          </button>
+        </li>
+        <li
+          v-for="idx in paginationItems"
+          :key="idx ?? -1"
+          class="page-item"
+          :class="[idx == null ? 'flex justify-center items-center' : '']"
+        >
+          <span
+            v-if="idx == null"
+            class="px-3 md:px-4 text-gray-600 dark:text-gray-400"
+          >&middot;&middot;&middot;</span>
+          <!-- class="cursor-pointer py-1.5 px-3 md:px-4 rounded border-0 outline-none transition-all duration-300 hover:bg-gray-200 dark:hover:bg-gray-700 focus:shadow-none" -->
+          <button
+            v-else
+            :title="`Auf die ${idx}. Seite springen`"
+            class="pagination-button"
+            :class="[
+              idx - 1 == currentPageIdx ? 'bg-gray-300 dark:bg-gray-600' : '',
+            ]"
+            @click="
+              () => {
+                selectPage(idx - 1);
+              }
+            "
+          >
+            {{ idx }}
+          </button>
+        </li>
+        <li class="page-item">
+          <button
+            title="Nächste Seite"
+            class="pagination-button"
+            :disabled="currentPageIdx == pageCount - 1"
+            @click="
+              () => {
+                selectPage(Math.min(currentPageIdx + 1, pageCount - 1));
+              }
+            "
+          >
+            <font-awesome-icon icon="fa-solid fa-arrow-right" />
+          </button>
+        </li>
+      </ul>
+    </nav>
+  </div>
+</template>
+
+<script lang="ts">
+export default {
+  name: "NumberedPagination",
+  props: {
+    pageSize: {
+      required: true,
+      type: Number,
+    },
+    resultCount: {
+      required: true,
+      type: Number,
+    },
+    modelValue: {
+      required: true,
+      type: Number,
+    },
+  },
+  emits: ["update:modelValue"],
+  data() {
+    return {
+      currentPageIdx: 0,
+    };
+  },
+  computed: {
+    pageCount() {
+      return Math.ceil(this.resultCount / this.pageSize);
+    },
+    paginationItems() {
+      const pc = this.pageCount;
+      if (pc <= 9) {
+        let pagination = [];
+        for (let i = 1; i <= pc; i++) pagination.push(i);
+        return pagination;
+      }
+      const cpi = this.currentPageIdx;
+      if (cpi <= 4) {
+        return [1, 2, 3, 4, 5, 6, null, pc - 1, pc];
+      } else if (cpi >= pc - 5) {
+        return [1, 2, null, pc - 5, pc - 4, pc - 3, pc - 2, pc - 1, pc];
+      } else {
+        return [1, 2, null, cpi, cpi + 1, cpi + 2, null, pc - 1, pc];
+      }
+    },
+  },
+  watch: {
+    modelValue() {
+      this.currentPageIdx = this.modelValue;
+    },
+  },
+  methods: {
+    selectPage(pageIdx: number) {
+      if (this.currentPageIdx == pageIdx) return;
+      this.currentPageIdx = pageIdx;
+      this.$emit("update:modelValue", pageIdx);
+    },
+  },
+};
+</script>
+
+<style scoped>
+.pagination-button {
+  @apply cursor-pointer
+    py-1.5
+    px-2.5
+    mx-0.5
+    md:px-4
+    rounded
+    border-0
+    outline-none
+    transition-all
+    duration-200
+    hover:bg-gray-200
+    active:bg-gray-300
+    dark:hover:bg-gray-700
+    dark:active:bg-gray-800
+    focus:shadow-none;
+}
+</style>

+ 68 - 0
src/components/general/OrderingControl.vue

@@ -0,0 +1,68 @@
+<template>
+  <div
+    class="w-full text-center cursor-pointer"
+    @click="nextOrdering"
+  >
+    <span>{{ title }}
+      <span class="ml-2 w-10">
+        <font-awesome-icon
+          v-if="ordering == Ordering.None"
+          class="text-gray-400"
+          icon="fa-solid fa-sort"
+        />
+        <font-awesome-icon
+          v-if="ordering == Ordering.Asc"
+          icon="fa-solid fa-sort-up"
+        />
+        <font-awesome-icon
+          v-if="ordering == Ordering.Desc"
+          icon="fa-solid fa-sort-down"
+        /></span></span>
+  </div>
+</template>
+
+<script lang="ts">
+import { Ordering } from "../../types";
+
+export default {
+  name: "OrderingControl",
+  props: {
+    title: {
+      required: true,
+      type: String,
+    },
+  },
+  emits: ["changed"],
+  setup() {
+    return {
+      Ordering,
+    };
+  },
+  data() {
+    return {
+      ordering: Ordering.None,
+    };
+  },
+  methods: {
+    nextOrdering() {
+      switch (this.ordering) {
+        case Ordering.None:
+          this.ordering = Ordering.Asc;
+          break;
+        case Ordering.Asc:
+          this.ordering = Ordering.Desc;
+          break;
+        case Ordering.Desc:
+          this.ordering = Ordering.None;
+          break;
+
+        default:
+          console.error("Invalid ordering");
+          break;
+      }
+
+      this.$emit("changed", [this.ordering]);
+    },
+  },
+};
+</script>

+ 49 - 0
src/components/general/PreviousModuleExecutions.vue

@@ -0,0 +1,49 @@
+<template>
+  <table class="w-full">
+    <thead>
+      <tr
+        class="border-b dark:border-gray-500 font-bold bg-gray-200 h-10 dark:bg-gray-500 dark:text-white"
+      >
+        <td>Klasse</td>
+        <td>Zeit</td>
+        <td>Dozent</td>
+        <td>Raum</td>
+        <td />
+      </tr>
+    </thead>
+    <tbody>
+      <PreviousModuleExecutionsRow
+        v-for="(cls, idx) in previousClasses"
+        :key="cls.id"
+        :cls="cls"
+        :additional-spacing="isDifferentSemester(idx)"
+      />
+    </tbody>
+  </table>
+</template>
+
+<script lang="ts">
+import { PropType } from "vue";
+import { HistoricClassEntry } from "../../types";
+import PreviousModuleExecutionsRow from "./PreviousModuleExecutionsRow.vue";
+
+export default {
+  name: "PreviousModuleExecutions",
+  components: { PreviousModuleExecutionsRow },
+  props: {
+    previousClasses: {
+      required: true,
+      type: Object as PropType<HistoricClassEntry[]>,
+    },
+  },
+  methods: {
+    isDifferentSemester(idx: number): boolean {
+      if (idx == 0) return true;
+      return (
+        this.previousClasses[idx - 1].semester !==
+        this.previousClasses[idx]?.semester
+      );
+    },
+  },
+};
+</script>

+ 81 - 0
src/components/general/PreviousModuleExecutionsRow.vue

@@ -0,0 +1,81 @@
+<template>
+  <tr
+    v-if="additionalSpacing"
+    class="border-b-2"
+  >
+    <td
+      colspan="5"
+      class="pt-4"
+    >
+      <span class="font-black">{{ cls.semester }}</span>
+    </td>
+  </tr>
+
+  <tr class="row">
+    <td>{{ cls.class }}</td>
+    <td>{{ cls.executionTime ?? "-" }}</td>
+    <td>{{ cls.lecturers.join(", ") }}</td>
+    <td>
+      <span class="mr-2">{{ cls.rooms.join(", ") }}</span>
+      <TeachingTypeIcon :teaching-type="cls.teaching_type" />
+    </td>
+    <td>
+      <a
+        title="Klassen PDF in einem neuem Tab öffnen"
+        target="_blank"
+        :href="classesPDFLink(cls, cls.semester, cls.version)"
+      >
+        <button
+          class="action-button"
+          title="Klassen PDF in einem neuem Tab öffnen"
+        >
+          <!-- I just gave up styling the link... -->
+          <font-awesome-icon icon="fa-solid fa-file-pdf" />
+        </button>
+      </a>
+    </td>
+  </tr>
+</template>
+
+<script lang="ts">
+import { PropType } from "vue";
+import { classesPDFLink } from "../../helpers";
+import { HistoricClassEntry } from "../../types";
+import TeachingTypeIcon from "../general/TeachingTypeIcon.vue";
+
+export default {
+  name: "PreviousModuleExecutionsRow",
+  components: { TeachingTypeIcon },
+  props: {
+    cls: {
+      required: true,
+      type: Object as PropType<HistoricClassEntry>,
+    },
+    additionalSpacing: {
+      required: true,
+      type: Boolean,
+    },
+  },
+  setup() {
+    return {
+      classesPDFLink,
+    };
+  },
+  computed: {},
+};
+</script>
+
+<style scoped>
+.row {
+  @apply border-b
+   dark:border-gray-500
+    hover:bg-slate-100
+    dark:hover:bg-gray-700
+    transition-all
+    duration-100
+    align-top;
+}
+.module-table td {
+  @apply border-x border-gray-300 dark:border-gray-500 h-11;
+}
+</style>

+ 42 - 0
src/components/general/TeachingTypeIcon.vue

@@ -0,0 +1,42 @@
+<template>
+  <font-awesome-icon
+    v-if="teachingType == TeachingType.Online"
+    title="Online unterricht"
+    icon="fa-solid fa-wifi"
+  />
+  <font-awesome-icon
+    v-if="teachingType == TeachingType.Hybrid"
+    title="Hybrider Unterricht"
+    icon="fa-solid fa-house-laptop"
+  />
+  <font-awesome-icon
+    v-if="teachingType == TeachingType.Blockmodule"
+    title="Blockmodul"
+    icon="fa-solid fa-square"
+  />
+  <font-awesome-icon
+    v-if="teachingType == TeachingType.OnSite"
+    title="Vor Ort"
+    icon="fa-solid fa-school"
+  />
+</template>
+
+<script lang="ts">
+import { PropType } from "vue";
+import { TeachingType } from "../../types";
+
+export default {
+  name: "TeachingTypeIcon",
+  props: {
+    teachingType: {
+      required: true,
+      type: [String, () => Object as PropType<TeachingType>],
+    },
+  },
+  data() {
+    return {
+      TeachingType,
+    };
+  },
+};
+</script>

+ 115 - 0
src/components/general/WeekdayTimePicker.vue

@@ -0,0 +1,115 @@
+<template>
+  <div class="flex gap-2">
+    <select
+      v-model="weekday"
+      @change="emitChange"
+    >
+      <option :value="null">
+        -
+      </option>
+      <option
+        v-for="(wd, key) in dayMap"
+        :key="key"
+        :value="key"
+        :disabled="key < disableLowerThan || key > disableGreaterThan"
+      >
+        {{ wd }}
+      </option>
+    </select>
+    <input
+      v-model="time"
+      type="time"
+      @change="emitChange"
+    >
+  </div>
+</template>
+
+<script lang="ts">
+import { dayMap } from "../../helpers";
+
+export default {
+  name: "WeekdayTimePicker",
+  props: {
+    modelValue: {
+      required: false,
+      type: Number,
+      default: 0,
+    },
+    disableLowerThan: {
+      required: false,
+      type: Number,
+      default: 0,
+    },
+    disableGreaterThan: {
+      required: false,
+      type: Number,
+      default: 100,
+    },
+  },
+  emits: ["update:modelValue"],
+  setup() {
+    return {
+      dayMap,
+    };
+  },
+  data() {
+    return {
+      weekday: null as number | null,
+      time: "",
+    };
+  },
+  computed: {
+    singleValue(): number | undefined {
+      const hasTime = this.time.length > 0;
+      const hasWeekday = this.weekday != null;
+
+      if (!hasTime && !hasWeekday) return undefined;
+
+      let t = 0;
+      if (hasTime) {
+        const parts = this.time.split(":").map((el) => Number.parseInt(el));
+        t = parts[0] * 3600 + parts[1] * 60;
+      }
+
+      if (this.weekday == null) return -1 * t;
+      return this.weekday * 86400 + t;
+    },
+  },
+  watch: {
+    modelValue() {
+      this.fromSingleValue();
+    },
+  },
+  mounted() {
+    this.fromSingleValue();
+  },
+  methods: {
+    emitChange() {
+      this.$emit("update:modelValue", this.singleValue);
+    },
+    fromSingleValue() {
+      let val = this.modelValue;
+      if (val == undefined) {
+        this.weekday = null;
+        this.time = "";
+        return;
+      }
+
+      if (val >= 0) {
+        this.weekday = Math.floor(val / 86400);
+        val -= this.weekday * 86400;
+      } else {
+        val *= -1;
+      }
+
+      if (val > 0) {
+        const hours = Math.floor(val / 3600);
+        const minutes = Math.floor((val - hours * 3600) / 60);
+
+        this.time =
+          `${hours}`.padStart(2, "0") + ":" + `${minutes}`.padStart(2, "0");
+      }
+    },
+  },
+};
+</script>

+ 167 - 0
src/components/layout/HeaderBar.vue

@@ -0,0 +1,167 @@
+<template>
+  <div class="visible-in-print">
+    <div class="flex items-center">
+      <img
+        src="../../assets/logo.svg"
+        alt="Logo"
+        width="45"
+        height="45"
+        class="mr-3 inline"
+      >
+      <div>
+        <span class="font-bold text-2xl block">{{ SCHOOL_NAME }} Modulplaner</span>
+        <span class="text-xs text-gray-400 block">Made with <font-awesome-icon icon="fa-solid fa-heart" /> by Sean
+          Blackburn</span>
+      </div>
+    </div>
+  </div>
+
+  <div
+    class="w-full bg-gray-800 dark:bg-black px-5 md:px-10 py-5 hidden-in-print flex justify-between gap-4 relative"
+  >
+    <!-- <img
+      v-if="stateStore.hasShownHelpWantedModal"
+      title="Nachfolger gesucht!"
+      class="absolute h-full bottom-0 right-24 cursor-pointer hidden md:block"
+      src="../../assets/searching-single.svg"
+      @click="
+        stateStore.hasShownHelpWantedModal = !stateStore.hasShownHelpWantedModal
+      "
+    /> -->
+
+    <div class="flex items-center text-gray-200">
+      <img
+        src="../../assets/logo.svg"
+        alt="Logo"
+        width="42"
+        height="42"
+        class="mr-3"
+      >
+      <div class="hidden md:block">
+        <span class="font-bold text-2xl block">{{ SCHOOL_NAME }} Modulplaner</span>
+        <span class="text-xs text-gray-400 block">Made with <font-awesome-icon icon="fa-solid fa-heart" /> by Sean
+          Blackburn</span>
+      </div>
+    </div>
+
+    <div class="text-center text-gray-200 text-xs md:text-base">
+      <p class="text-red-500">
+        Alle Angaben ohne Gewähr! Bei Fehlern:
+        <a
+          title="Modulplaner GitLab Issues"
+          class="text-blue-400 underline"
+          target="_blank"
+          rel="noreferrer noopener"
+          :href="URLS.GITLAB_REPO_TICKET"
+        >Ticket</a>
+      </p>
+      <p>
+        <span class="hidden md:inline">PDF Version:</span>
+
+        <Popper
+          :arrow="true"
+          :hover="true"
+          :z-index="30"
+          style="border: 0px solid transparent; margin: 0px"
+        >
+          <span
+            class="p-1 px-2 ml-2 rounded inline text-xs md:text-sm font-mono"
+            :class="[
+              classVersionStore.isLatestSemesterVersion
+                ? 'bg-gray-700 cursor-help'
+                : 'bg-red-700 pulsating-orange p-1.5 px-4 cursor-pointer',
+            ]"
+            :title="
+              !classVersionStore.isLatestSemesterVersion
+                ? 'Click to upgrade'
+                : 'Aktuellste Version'
+            "
+            @click="showClassUpgrader"
+          >{{ currentVersionStr }}
+          </span>
+          <template #content>
+            <span
+              v-if="!classVersionStore.isLatestSemesterVersion"
+              class="p-3 m-2 font-bold block dark:bg-orange-700 bg-orange-500 text-white rounded-md cursor-pointer"
+              @click="showClassUpgrader"
+            >Veraltete Stundenplan-Version!</span>
+            Export: {{ configStore.config.export_date }}<br>
+            Parse: {{ configStore.config.parse_date }}
+          </template>
+        </Popper>
+      </p>
+    </div>
+
+    <div
+      class="flex items-center justify-end"
+      style="width: 42px"
+    >
+      <AdditionalInfo />
+    </div>
+  </div>
+</template>
+
+<script lang="ts">
+import { useConfigStore } from "../../stores/config";
+import Popper from "vue3-popper";
+import AdditionalInfo from "../modals/AdditionalInfo.vue";
+import { useClassVersionStore } from "../../stores/ClassVersion";
+import { useStateStore } from "../../stores/state";
+import { URLS, SCHOOL_NAME } from "../../helpers";
+
+export default {
+  name: "HeaderBar",
+  components: {
+    Popper,
+    AdditionalInfo,
+  },
+  setup() {
+    const configStore = useConfigStore();
+    const classVersionStore = useClassVersionStore();
+    const stateStore = useStateStore();
+
+    return {
+      configStore,
+      classVersionStore,
+      stateStore,
+      URLS,
+      SCHOOL_NAME,
+    };
+  },
+  computed: {
+    currentVersionStr(): string {
+      if (
+        this.classVersionStore.semester == null &&
+        this.classVersionStore.version == null
+      )
+        return "Laden...";
+
+      if (this.classVersionStore.semester == "latest")
+        return this.configStore.config.pdf_version;
+
+      return `${this.classVersionStore.semester} / ${this.classVersionStore.version}`;
+    },
+  },
+  methods: {
+    showClassUpgrader() {
+      if (this.classVersionStore.isLatestSemesterVersion) return;
+      this.stateStore.upgradingClassVersion = true;
+    },
+  },
+};
+</script>
+
+<style scoped>
+.pulsating-orange {
+  animation-name: color;
+  animation-duration: 1s;
+  animation-iteration-count: infinite;
+  animation-direction: alternate;
+}
+
+@keyframes color {
+  to {
+    background-color: rgb(194 65 12);
+  }
+}
+</style>

+ 354 - 0
src/components/modals/AdditionalInfo.vue

@@ -0,0 +1,354 @@
+<template>
+  <button
+    class="text-gray-200"
+    @click="toggleVisibility"
+  >
+    <font-awesome-icon
+      icon="fa-solid fa-bars"
+      class="text-2xl"
+    />
+  </button>
+
+  <RightDrawer
+    :show="showingAdditionalInfo"
+    @close="showingAdditionalInfo = false"
+  >
+    <div class="flex items-center">
+      <img
+        src="../../assets/logo.svg"
+        alt="Logo"
+        width="35"
+        height="35"
+        class="mr-3"
+      >
+      <h1 class="text-3xl font-bold">
+        Modulplaner
+      </h1>
+    </div>
+
+    <div class="mt-5">
+      <h2 class="text-2xl mb-2 mr-5">
+        Ansicht
+      </h2>
+      <div class="flex gap-4">
+        <router-link
+          to="/"
+          class="view-button"
+          @click="toggleVisibility"
+        >
+          <font-awesome-icon
+            class="text-2xl"
+            icon="fa-solid fa-calendar-day"
+          />
+        </router-link>
+        <router-link
+          to="/tree"
+          class="view-button"
+          @click="toggleVisibility"
+        >
+          <font-awesome-icon
+            class="text-2xl"
+            icon="fa-solid fa-sitemap"
+          />
+        </router-link>
+      </div>
+    </div>
+
+    <div class="mt-5">
+      <h2 class="text-2xl">
+        Quick-links
+      </h2>
+      <ul class="list-disc ml-7 my-1">
+        <li>
+          <a
+            class="external-link"
+            title="Stundenpläne"
+            :href="URLS.ALL_TIMETABLES"
+            target="_blank"
+            rel="noreferrer noopener"
+          >Stundenpläne
+            <font-awesome-icon
+              class="ml-2"
+              icon="fa-solid fa-arrow-up-right-from-square"
+            /></a>
+        </li>
+        <li>
+          <a
+            class="external-link"
+            title="Studenthub"
+            :href="URLS.STUDENTHUB"
+            target="_blank"
+            rel="noreferrer noopener"
+          >Studenthub
+            <font-awesome-icon
+              class="ml-2"
+              icon="fa-solid fa-arrow-up-right-from-square"
+            /></a>
+        </li>
+        <li>
+          <a
+            class="external-link"
+            title="Einschreibeportal"
+            :href="URLS.MODULE_SIGNUP"
+            target="_blank"
+            rel="noreferrer noopener"
+          >ESPortal
+            <font-awesome-icon
+              class="ml-2"
+              icon="fa-solid fa-arrow-up-right-from-square"
+            /></a>
+        </li>
+      </ul>
+    </div>
+
+    <div class="mt-10">
+      <h2 class="text-2xl">
+        Über
+      </h2>
+      <p class="text-gray-600 dark:text-gray-400 py-1">
+        Der Modulplaner ist ein privat entwickeltes Projekt. Es haben diverse
+        Studenten sowie Dozenten zum Inhalt dieser Seite beigetragen -
+        Herzlichen Dank!
+      </p>
+      <table class="w-full">
+        <tbody>
+          <tr>
+            <td class="font-bold">
+              Entwicklung
+            </td>
+            <td>Sean Blackburn</td>
+          </tr>
+          <tr>
+            <td class="font-bold">
+              Icon
+            </td>
+            <td>Claire Smits</td>
+          </tr>
+        </tbody>
+      </table>
+    </div>
+
+    <div class="mt-10">
+      <h2 class="text-2xl inline-block mr-5">
+        Mitentwickeln
+      </h2>
+      <a
+        title="Modulplaner GitLab Repo"
+        :href="URLS.GITLAB_REPO"
+        target="_blank"
+        rel="noreferrer noopener"
+        class="text-orange-600 hover:text-orange-400 transition-all duration-200"
+      ><font-awesome-icon
+        icon="fa-brands fa-gitlab"
+        class="text-2xl"
+      /></a>
+      <p class="text-gray-600 dark:text-gray-400 py-1">
+        Der Modulplaner ist ein open-source Projekt, welches unter der
+        MIT-Lizenz veröffentlicht ist. Helfe mit, indem du Fehler meldest oder
+        direkt am Code Änderungen anbringst.
+      </p>
+      <a
+        class="external-link inline-block mt-2"
+        title="Studenthub"
+        :href="URLS.GITLAB_REPO"
+        target="_blank"
+        rel="noreferrer noopener"
+      >GitLab Repo
+        <font-awesome-icon
+          class="ml-2"
+          icon="fa-solid fa-arrow-up-right-from-square"
+        /></a>
+    </div>
+
+    <div class="mt-10 hidden">
+      <button
+        class="action-button mr-2"
+        :title="
+          'Stundenplanänderungen ' +
+            (showingTimetableChanges ? 'ausblenden' : 'einblenden')
+        "
+        @click="() => (showingTimetableChanges = !showingTimetableChanges)"
+      >
+        <font-awesome-icon
+          v-if="showingTimetableChanges"
+          icon="fa-solid fa-chevron-down"
+        />
+        <font-awesome-icon
+          v-else
+          icon="fa-solid fa-chevron-right"
+        />
+      </button>
+
+      <h2 class="text-2xl inline-block mb-2">
+        Stundenplanänderungen
+      </h2>
+
+      <div
+        v-if="showingTimetableChanges"
+        id="timetable-changelog"
+        class="ml-5"
+      >
+        <div
+          v-for="entry in timetableChangelog.reverse()"
+          :key="entry.name"
+          class="block mb-10"
+        >
+          <h1>{{ entry.name }}</h1>
+
+          <!-- eslint-disable vue/no-v-html -->
+          <span
+            class="text-gray-700 dark:text-gray-300"
+            v-html="entry.changes"
+          />
+          <!--eslint-enable-->
+        </div>
+      </div>
+    </div>
+
+    <div class="mt-10">
+      <button
+        class="action-button mr-2"
+        :title="
+          'Stundenplanänderungen ' +
+            (showingChangelog ? 'ausblenden' : 'einblenden')
+        "
+        @click="() => (showingChangelog = !showingChangelog)"
+      >
+        <font-awesome-icon
+          v-if="showingChangelog"
+          icon="fa-solid fa-chevron-down"
+        />
+        <font-awesome-icon
+          v-else
+          icon="fa-solid fa-chevron-right"
+        />
+      </button>
+      <h2 class="text-2xl inline-block mb-2">
+        Changelog
+      </h2>
+
+      <!-- eslint-disable vue/no-v-html -->
+      <div
+        v-if="showingChangelog"
+        id="repo-changelog"
+        class="ml-5"
+        v-html="changelogContent"
+      />
+      <!--eslint-enable-->
+    </div>
+  </RightDrawer>
+</template>
+
+<script lang="ts">
+import RightDrawer from "./RightDrawer.vue";
+import { URLS } from "../../helpers";
+import { useClassVersionStore } from "../../stores/ClassVersion";
+import { MainView, TimetableChangelogEntry } from "../../types";
+import { useStateStore } from "../../stores/state";
+
+export default {
+  name: "AdditionalInfo",
+  components: {
+    RightDrawer,
+  },
+  setup() {
+    const classVersionStore = useClassVersionStore();
+    const stateStore = useStateStore();
+    return {
+      URLS,
+      classVersionStore,
+      stateStore,
+      MainView,
+    };
+  },
+  data() {
+    return {
+      showingAdditionalInfo: false,
+      changelogContent: "",
+      timetableChangelog: [] as TimetableChangelogEntry[],
+      showingTimetableChanges: false,
+      showingChangelog: false,
+    };
+  },
+  mounted() {
+    this.fetchChangelog();
+    // this.fetchTimetableChangelog();
+  },
+  methods: {
+    fetchChangelog() {
+      fetch("./data/changelog.html")
+        .then((resp) => resp.text())
+        .then((txt) => (this.changelogContent = txt));
+    },
+    fetchTimetableChangelog() {
+      fetch(this.classVersionStore.semesterFolder + "/changes.json")
+        .then((resp) => resp.json())
+        .then((data) => (this.timetableChangelog = data));
+    },
+    toggleVisibility() {
+      if (this.stateStore.inspectingClass)
+        this.stateStore.inspectingClass = null;
+      if (this.stateStore.inspectingModule)
+        this.stateStore.inspectingModule = null;
+
+      this.showingAdditionalInfo = !this.showingAdditionalInfo;
+    },
+  },
+};
+</script>
+
+<style>
+#repo-changelog h1,
+#timetable-changelog h1 {
+  @apply text-lg font-bold;
+}
+#repo-changelog a,
+#timetable-changelog a {
+  @apply text-blue-500 underline;
+}
+#timetable-changelog ul {
+  @apply list-disc mb-6 ml-5;
+}
+
+#repo-changelog h3 {
+  @apply italic font-bold ml-3;
+}
+#repo-changelog ul {
+  @apply list-disc ml-10 mb-7;
+}
+#repo-changelog li {
+  @apply text-gray-700 dark:text-gray-300;
+}
+</style>
+
+<style scoped>
+.action-button {
+  @apply text-center
+    hover:bg-gray-200
+    active:bg-gray-300
+    dark:hover:bg-gray-700
+    dark:active:bg-gray-900
+    rounded
+    cursor-pointer
+    w-7
+    aspect-square
+    transition-all
+    duration-200
+    disabled:pointer-events-none;
+}
+
+.view-button {
+  @apply rounded
+  w-full
+  py-2
+  text-center
+  bg-gray-100
+  hover:bg-gray-200
+  active:bg-gray-300
+  dark:bg-gray-600
+  dark:hover:bg-gray-700
+  dark:active:bg-gray-900
+  transition-all
+  duration-200;
+}
+</style>

+ 80 - 0
src/components/modals/BaseModal.vue

@@ -0,0 +1,80 @@
+<template>
+  <div
+    v-if="show"
+    class="relative z-10"
+    aria-labelledby="modal-title"
+    role="dialog"
+    aria-modal="true"
+    @keypress.esc="close()"
+    @click="
+      (event) => {
+        if (event.target == $refs.modal) close();
+      }
+    "
+  >
+    <div
+      class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity cursor-pointer"
+    />
+
+    <div class="fixed inset-0 z-10 overflow-y-auto">
+      <div
+        ref="modal"
+        class="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0"
+      >
+        <div
+          class="relative transform overflow-hidden rounded-lg bg-white dark:bg-gray-800 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg"
+          data-name="modal"
+        >
+          <div class="bg-white dark:bg-gray-800 px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
+            <button
+              title="Schliessen"
+              class="close-button"
+              @click="close()"
+            >
+              <font-awesome-icon icon="fa-solid fa-xmark" />
+            </button>
+
+            <div class="mt-3 sm:mt-0 sm:ml-4 sm:text-left">
+              <slot />
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts">
+export default {
+  name: "BaseModal",
+  props: {
+    show: {
+      type: Boolean,
+      required: true,
+    },
+  },
+  emits: ["close"],
+  methods: {
+    close() {
+      this.$emit("close");
+    },
+  },
+};
+</script>
+
+<style scoped>
+.close-button {
+  @apply absolute
+    right-1
+    top-0
+    m-4
+    cursor-pointer
+    hover:bg-gray-200
+    active:bg-gray-300
+    dark:hover:bg-gray-600
+    dark:active:bg-gray-700
+    aspect-square
+    w-7
+    rounded;
+}
+</style>

+ 104 - 0
src/components/modals/ClassModuleDetails.vue

@@ -0,0 +1,104 @@
+<template>
+  <BaseModal
+    :show="isInspecting"
+    @close="close"
+  >
+    <h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-white">
+      <span class="font-bold text-3xl mr-5 block">{{
+        module?.short ?? insCls?.name ?? "-"
+      }}</span>
+      <span class="font-light">{{ module?.name ?? "-" }}</span>
+    </h3>
+
+    <ModuleInfo
+      v-if="insModule !== null"
+      :module="insModule"
+    />
+    <ClassInfo
+      v-else-if="insCls"
+      :cls="insCls"
+    />
+    <div
+      v-else
+      class="my-10 mx-5 text-center"
+    >
+      <span class="inline-block">Well, something went wrong here :S And the error message isn't even in
+        German! Heeeeeelp!</span>
+      <span class="pt-5 inline-block">Or you could just open a ticket, as you should never see this message
+        :)</span>
+      <a
+        title="Modulplaner GitLab Repo"
+        :href="URLS.GITLAB_REPO_TICKET"
+        target="_blank"
+        rel="noreferrer noopener"
+        class="external-link mt-5 inline-block"
+      >Open a new ticket here
+        <font-awesome-icon
+          class="ml-2"
+          icon="fa-solid fa-arrow-up-right-from-square"
+        /></a>
+    </div>
+
+    <DependencyTree
+      :module="module"
+      :module-name="insCls?.name ?? '???'"
+      :class-language="insCls?.languageString ?? ModuleLanguages.DE"
+    />
+  </BaseModal>
+</template>
+
+<script lang="ts">
+import { useStateStore } from "../../stores/state";
+import { Module, ModuleLanguages, TaughtClass } from "../../types";
+import ClassInfo from "../general/ClassInfo.vue";
+import ModuleInfo from "../general/ModuleInfo.vue";
+import DependencyTree from "../general/DependencyTree.vue";
+import BaseModal from "./BaseModal.vue";
+import { URLS } from "../../helpers";
+
+export default {
+  name: "ClassModuleDetails",
+  components: {
+    DependencyTree,
+    BaseModal,
+    ClassInfo,
+    ModuleInfo,
+  },
+  setup() {
+    const stateStore = useStateStore();
+    return {
+      stateStore,
+      ModuleLanguages,
+      URLS,
+    };
+  },
+  computed: {
+    insCls(): TaughtClass | null {
+      return this.stateStore.inspectingClass;
+    },
+    insModule(): Module | null {
+      return this.stateStore.inspectingModule;
+    },
+    module(): Module | null {
+      if (this.stateStore.inspectingModule !== null)
+        return this.stateStore.inspectingModule;
+      return this.stateStore.inspectingClass?.module ?? null;
+    },
+    isInspectingClass(): boolean {
+      return this.stateStore.inspectingClass !== null;
+    },
+    isInspectingModule(): boolean {
+      return this.stateStore.inspectingModule !== null;
+    },
+    isInspecting(): boolean {
+      return this.isInspectingClass || this.isInspectingModule;
+    },
+  },
+  methods: {
+    close() {
+      this.stateStore.inspectingClass = null;
+      this.stateStore.inspectingModule = null;
+    },
+  },
+};
+</script>

+ 116 - 0
src/components/modals/ClassUpdateModal.vue

@@ -0,0 +1,116 @@
+<template>
+  <BaseModal
+    :show="stateStore.showingClassUpgradeModal"
+    @close="close"
+  >
+    <h1 class="text-3xl font-bold leading-6 text-gray-900 dark:text-white">
+      Planungs-Update
+    </h1>
+
+    <p class="my-9">
+      Es gibt ein neues Update für deine Planung.
+    </p>
+
+    <div class="w-full flex justify-center my-9">
+      <h1
+        class="text-xs sm:text-base font-bold leading-6 text-gray-900 dark:text-white"
+      >
+        <span
+          class="bg-orange-300 dark:bg-orange-600 rounded-md sm:rounded-xl py-2 px-2 sm:px-5"
+        >{{ classVersionStore.semester }} /
+          {{ classVersionStore.version }}</span>
+        <font-awesome-icon
+          class="px-5 sm:px-5"
+          icon="fa-solid fa-arrow-right"
+        />
+        <span
+          class="bg-blue-300 dark:bg-blue-600 rounded-md sm:rounded-xl py-2 px-2 sm:px-5"
+        >{{ classVersionStore.latestSemester?.semester }} /
+          {{ classVersionStore.latestSemester?.versions[0] }}</span>
+      </h1>
+    </div>
+
+    <p class="italic text-gray-600 dark:text-gray-400 text-center my-9">
+      Falls du dich gegen ein Update entscheidest, kannst du jederzeit den
+      pulsierenden orange/roten Knopf im Header klicken um das Update
+      durchzuführen!
+    </p>
+
+    <div class="mt-10">
+      <div class="flex justify-end items-center gap-4">
+        <div class="md:mr-10">
+          <input
+            id="do-not-remind-me-upgrade"
+            v-model="doNotRemindState"
+            type="checkbox"
+            class="w-auto inline mr-2 cursor-pointer"
+          >
+          <label
+            class="text-gray-500 select-none cursor-pointer"
+            for="do-not-remind-me-upgrade"
+          >Nicht mehr erinnern</label>
+        </div>
+
+        <button
+          class="bg-orange-500 text-white py-2 px-5 rounded"
+          @click="close"
+        >
+          Später
+        </button>
+        <button
+          class="bg-blue-500 text-white py-2 px-5 rounded"
+          @click="upgrade"
+        >
+          Upgrade
+        </button>
+      </div>
+    </div>
+  </BaseModal>
+</template>
+
+<script lang="ts">
+import { useStateStore } from "../../stores/state";
+import { useModulesStore } from "../../stores/modules";
+import { useClassesStore } from "../../stores/classes";
+import BaseModal from "./BaseModal.vue";
+import { useClassVersionStore } from "../../stores/ClassVersion";
+import { usePlanningStore } from "../../stores/planning";
+
+export default {
+  name: "ClassUpdateModal",
+  components: {
+    BaseModal,
+  },
+  setup() {
+    const stateStore = useStateStore();
+    const modulesStore = useModulesStore();
+    const classesStore = useClassesStore();
+    const classVersionStore = useClassVersionStore();
+
+    return {
+      stateStore,
+      modulesStore,
+      classesStore,
+      classVersionStore,
+    };
+  },
+  data() {
+    return {
+      doNotRemindState: false,
+    };
+  },
+  methods: {
+    close() {
+      this.stateStore.showingClassUpgradeModal = false;
+      if (this.doNotRemindState)
+        usePlanningStore().noLongerRequestVersionUpgrade();
+    },
+    upgrade() {
+      this.stateStore.upgradingClassVersion = true;
+      this.close();
+    },
+  },
+};
+</script>
+
+<style scoped></style>

+ 189 - 0
src/components/modals/ModuleSearchModal.vue

@@ -0,0 +1,189 @@
+<template>
+  <RightDrawer
+    :show="stateStore.showingModuleSearch"
+    @close="close"
+  >
+    <h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-white">
+      <span class="font-bold text-3xl mr-5 block">Modulsuche</span>
+    </h3>
+
+    <div class="mt-5 mb-7">
+      <ClassFilter
+        v-model="degreeFilter"
+        class="mb-2"
+        :show-type="false"
+        :show-add-remove="false"
+      />
+      <ClassFilter
+        v-model="moduleNameFilter"
+        :show-type="false"
+        :show-add-remove="false"
+      />
+    </div>
+
+    <div v-if="matchingModules.length > 0">
+      <ul>
+        <li
+          v-for="mod in paginatedModules"
+          :key="mod.short"
+          class="module-entry"
+          @click="showModuleInfo(mod)"
+        >
+          <div class="w-5/6">
+            <span class="font-bold block">{{ mod.short }}</span>
+            <span class="text-sm block truncate w-full">{{
+              mod.name.length > 0 ? mod.name : "-"
+            }}</span>
+          </div>
+
+          <span>
+            <font-awesome-icon icon="fa-solid fa-arrow-right" />
+          </span>
+        </li>
+      </ul>
+      <NumberedPagination
+        v-model="currentPageIdx"
+        :page-size="pageSize"
+        :result-count="matchingModules.length"
+        class="scale-90 sm:scale-75"
+      />
+    </div>
+    <div
+      v-else
+      class="w-full text-center text-gray-500"
+    >
+      <span>Leider keine Module gefunden!</span>
+    </div>
+  </RightDrawer>
+</template>
+
+<script lang="ts">
+import { useStateStore } from "../../stores/state";
+import { useModulesStore } from "../../stores/modules";
+import RightDrawer from "./RightDrawer.vue";
+import { ClassSelectorColumn, FilterRule, Module } from "../../types";
+import NumberedPagination from "../general/NumberedPagination.vue";
+import { useClassesStore } from "../../stores/classes";
+import { watch } from "vue";
+import ClassFilter from "../general/ClassFilter.vue";
+
+export default {
+  name: "ModuleSearchModal",
+  components: {
+    RightDrawer,
+    NumberedPagination,
+    ClassFilter,
+  },
+  setup() {
+    const stateStore = useStateStore();
+    const modulesStore = useModulesStore();
+    const classesStore = useClassesStore();
+
+    return {
+      stateStore,
+      modulesStore,
+      classesStore,
+      pageSize: 10,
+    };
+  },
+  data() {
+    return {
+      currentPageIdx: 0,
+      degreeFilter: {
+        column: ClassSelectorColumn.Degree,
+        filterData: { degree: "" },
+        pk: 0,
+        enabled: true,
+      } as FilterRule,
+      moduleNameFilter: {
+        column: ClassSelectorColumn.Module,
+        filterData: { term: "" },
+        pk: 1,
+        enabled: true,
+      } as FilterRule,
+    };
+  },
+  computed: {
+    matchingModules(): Module[] {
+      return this.modulesStore.getModulesMatching(
+        this.moduleNameFilter.filterData.term,
+        this.degreeFilter.filterData.degree,
+      );
+    },
+    paginatedModules() {
+      const idx = this.currentPageIdx;
+      return this.matchingModules.slice(
+        idx * this.pageSize,
+        (idx + 1) * this.pageSize,
+      );
+    },
+    currentSearchAndDegreeTerm(): [string, string] {
+      let filterString = "";
+      let filterDegree = "";
+      this.classesStore.filterRules.filter((f) => {
+        if (
+          f.column == ClassSelectorColumn.Module &&
+          filterString.length == 0 &&
+          !!f.filterData?.term
+        )
+          filterString = f.filterData?.term;
+
+        if (
+          f.column == ClassSelectorColumn.Degree &&
+          filterString.length == 0 &&
+          !!f.filterData?.degree
+        )
+          filterDegree = f.filterData?.degree;
+      });
+      return [filterString, filterDegree];
+    },
+  },
+  mounted() {
+    watch(
+      () => this.stateStore.showingModuleSearch,
+      () => {
+        const sd = this.currentSearchAndDegreeTerm;
+        this.moduleNameFilter.filterData.term = sd[0];
+        this.degreeFilter.filterData.degree = sd[1];
+      },
+    );
+    watch(
+      () => this.moduleNameFilter.filterData.term,
+      () => (this.currentPageIdx = 0),
+    );
+    watch(
+      () => this.degreeFilter.filterData.degree,
+      () => (this.currentPageIdx = 0),
+    );
+  },
+  methods: {
+    close() {
+      this.stateStore.showingModuleSearch = false;
+    },
+    showModuleInfo(mod: Module) {
+      this.stateStore.inspectingModule = mod;
+    },
+  },
+};
+</script>
+
+<style scoped>
+.module-entry {
+  @apply flex
+    items-center
+    justify-between
+    bg-gray-200
+    hover:bg-gray-300
+    active:bg-gray-400
+    dark:bg-gray-600
+    dark:hover:bg-gray-700
+    dark:active:bg-gray-900
+    my-2
+    py-1
+    px-5
+    rounded
+    cursor-pointer
+    transition-all
+    duration-200;
+}
+</style>

+ 138 - 0
src/components/modals/ModulesetEdit.vue

@@ -0,0 +1,138 @@
+<template>
+  <BaseModal
+    :show="shouldShow"
+    @close="close(null)"
+  >
+    <h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-white">
+      <span class="font-bold text-3xl mr-5 block">{{
+        isNew ? "Neue Modulplanung" : "Modulplanung bearbeiten"
+      }}</span>
+    </h3>
+
+    <div class="mt-5">
+      <label
+        for="calendarSetName"
+        class="text-lg mr-5 w-full"
+      >Name: </label>
+      <input
+        id="calendarSetName"
+        v-model="currentName"
+        type="text"
+        class="w-full mb-5"
+        autofocus
+      >
+    </div>
+
+    <div class="flex w-2/3 float-right justify-end my-5">
+      <button
+        v-if="!isNew"
+        title="Neue leere Planung erstellen"
+        class="bg-red-500 text-white py-2 w-1/3 rounded mx-1"
+        @click="emptyModuleSet"
+      >
+        Leeren
+      </button>
+      <button
+        v-if="!isNew"
+        title="Momentane Planung kopieren"
+        class="bg-pink-500 text-white py-2 w-1/3 rounded mx-1"
+        @click="copyModuleSet"
+      >
+        Kopieren
+      </button>
+      <button
+        :title="
+          isNew ? 'Neue Planung jetzt erstellen' : 'Änderungen abspeichern'
+        "
+        class="bg-green-500 text-white py-2 w-1/3 rounded mx-1"
+        @click="saveModuleSet"
+      >
+        {{ isNew ? "Erstellen" : "Speichern" }}
+      </button>
+    </div>
+  </BaseModal>
+</template>
+
+<script lang="ts">
+import { dayMap, toTime } from "../../helpers";
+import { usePlanningStore } from "../../stores/planning";
+import BaseModal from "./BaseModal.vue";
+
+export default {
+  name: "ModuleSetEdit",
+  components: { BaseModal },
+  props: {
+    modulesetName: {
+      required: false,
+      type: String,
+      default: null,
+    },
+    isNew: {
+      required: true,
+      type: Boolean,
+    },
+  },
+  emits: ["close"],
+  setup() {
+    const planningStore = usePlanningStore();
+    return {
+      planningStore,
+      dayMap,
+      toTime,
+    };
+  },
+  data() {
+    return {
+      currentName: undefined as string | undefined,
+    };
+  },
+  computed: {
+    name() {
+      if (this.modulesetName == undefined) return "default";
+      return this.modulesetName;
+    },
+    shouldShow() {
+      return (
+        this.modulesetName != undefined ||
+        (this.modulesetName == undefined && this.isNew)
+      );
+    },
+  },
+  watch: {
+    shouldShow() {
+      if (!this.shouldShow) return;
+      this.currentName = this.modulesetName;
+    },
+  },
+  methods: {
+    close(newName: string | null) {
+      this.$emit("close", newName);
+    },
+    emptyModuleSet() {
+      const isSure = confirm(
+        "Sollen alle Module im ausgewählten Plan gelöscht werden (Kann nicht rückgängig gemacht werden)? Persönliche Events werden nicht gelöscht!",
+      );
+
+      if (!isSure) return;
+      this.planningStore.emptyPlan();
+    },
+    copyModuleSet() {
+      if (!this.currentName) return;
+      const newName = this.planningStore.copyPlan();
+      if (newName == null) return;
+      this.planningStore.currentPlanName = newName;
+      this.close(newName);
+    },
+    saveModuleSet() {
+      if (!this.currentName) return;
+      if (this.isNew) {
+        if (!this.planningStore.createPlan(this.currentName)) return;
+      } else {
+        if (!this.planningStore.renamePlan(this.currentName)) return;
+      }
+      this.planningStore.currentPlanName = this.currentName;
+      this.close(this.currentName);
+    },
+  },
+};
+</script>

+ 67 - 0
src/components/modals/OldSemesterReminderModal.vue

@@ -0,0 +1,67 @@
+<template>
+  <BaseModal
+    :show="stateStore.showingOldPlanReminderModal"
+    @close="close"
+  >
+    <h1 class="text-3xl font-bold leading-6 text-gray-900 dark:text-white">
+      Altes Semester
+    </h1>
+
+    <p class="mt-10 mb-10">
+      Deine Planung liegt in einem vergangenen Semester (<span
+        class="font-bold"
+      >{{ classesVersionStore.semester }}</span>)!
+    </p>
+
+    <p class="mt-10 mb-10">
+      Du kannst in den Einstellungen (<font-awesome-icon
+        icon="fa-solid fa-cog"
+      />) das Semester des aktuellen Plans ändern oder einen neuen Plan
+      erstellen (<font-awesome-icon icon="fa-solid fa-plus" />), um ins neuste
+      Semester zu gelangen.
+    </p>
+
+    <p class="text-gray-700 dark:text-gray-200">
+      Oder du bearbeitest deinen Plan weiter im alten Semester mit den Modulen
+      die damals zur Auswahl standen.
+    </p>
+
+    <div class="flex justify-end mt-10 gap-4">
+      <button
+        class="bg-green-500 text-white py-2 px-5 rounded"
+        @click="close"
+      >
+        Verstanden
+      </button>
+    </div>
+  </BaseModal>
+</template>
+
+<script lang="ts">
+import { useStateStore } from "../../stores/state";
+import { useClassVersionStore } from "../../stores/ClassVersion";
+import BaseModal from "./BaseModal.vue";
+
+export default {
+  name: "ClassUpdateModal",
+  components: {
+    BaseModal,
+  },
+  setup() {
+    const stateStore = useStateStore();
+    const classesVersionStore = useClassVersionStore();
+
+    return {
+      stateStore,
+      classesVersionStore,
+    };
+  },
+  methods: {
+    close() {
+      this.stateStore.showingOldPlanReminderModal = false;
+    },
+  },
+};
+</script>
+
+<style scoped></style>

+ 196 - 0
src/components/modals/PersonalEventEdit.vue

@@ -0,0 +1,196 @@
+import BaseModal from './BaseModal.vue';
+
+<template>
+  <BaseModal
+    :show="stateStore.inspectingPersonalEvent != null"
+    @close="stateStore.inspectingPersonalEvent = null"
+  >
+    <h1 class="text-3xl mb-5">
+      Persönlicher Event
+    </h1>
+
+    <form
+      @submit.prevent="updateEntry"
+      @keydown.enter="$event.preventDefault()"
+    >
+      <div class="mb-5">
+        <label>Bezeichnung</label>
+        <input
+          v-model="name"
+          type="text"
+          placeholder="Verein, Musik, ..."
+          required
+        >
+      </div>
+
+      <div class="mb-5">
+        <label>ECTS</label>
+        <input
+          v-model="ects"
+          type="number"
+          required
+        >
+      </div>
+
+      <div class="flex items-end w-full gap-4">
+        <div class="w-2/3">
+          <label>Wochentag</label>
+          <select
+            v-model="weekday"
+            class="h-10 w-full"
+          >
+            <option
+              v-for="(day, idx) in dayMap"
+              :key="day"
+              :value="idx"
+            >
+              {{ day }}
+            </option>
+          </select>
+        </div>
+
+        <div class="w-2/3">
+          <label>Von</label>
+          <input
+            v-model="from"
+            type="time"
+            class="h-10"
+            required
+          >
+        </div>
+
+        <div class="w-2/3">
+          <label>Bis</label>
+          <input
+            v-model="to"
+            type="time"
+            class="h-10"
+            required
+            :class="[toTimeValid ? '' : 'border-red-500']"
+          >
+        </div>
+      </div>
+
+      <div class="flex justify-end mt-10 gap-4">
+        <button
+          v-if="!isNew"
+          class="bg-red-500 text-white py-2 px-5 rounded"
+          @click.prevent="deleteEntry"
+        >
+          Entfernen
+        </button>
+        <input
+          type="submit"
+          class="bg-green-500 text-white py-2 px-5 rounded active:bg-green-600"
+          :value="isNew ? 'Einfügen' : 'Speichern'"
+        >
+      </div>
+    </form>
+  </BaseModal>
+</template>
+
+<script lang="ts">
+import BaseModal from "./BaseModal.vue";
+import { useStateStore } from "../../stores/state";
+import { usePlanningStore } from "../../stores/planning";
+import { dayMap, fromTime, toTime } from "../../helpers";
+import { PersonalEvent } from "../../types";
+
+export default {
+  name: "PersonalEventEdit",
+  components: { BaseModal },
+  setup() {
+    const stateStore = useStateStore();
+    const planningStore = usePlanningStore();
+    return {
+      stateStore,
+      planningStore,
+      dayMap,
+    };
+  },
+  data() {
+    return {
+      name: "",
+      ects: 0,
+      weekday: 0,
+      from: "",
+      to: "",
+    };
+  },
+  computed: {
+    personalEvent(): PersonalEvent | null | boolean {
+      return this.stateStore.inspectingPersonalEvent;
+    },
+    isNew(): boolean {
+      return this.stateStore.inspectingPersonalEvent == true;
+    },
+    toTimeValid(): boolean {
+      if (this.from.length == 0 || this.to.length == 0) return true;
+      return fromTime(this.from) < fromTime(this.to);
+    },
+  },
+  watch: {
+    personalEvent() {
+      this.updateFields();
+    },
+  },
+  mounted() {
+    this.updateFields();
+  },
+  methods: {
+    deleteEntry() {
+      const pe = this.stateStore.inspectingPersonalEvent;
+      if (pe === false || pe === true) return;
+      this.planningStore.removePersonalEvent(pe);
+      this.stateStore.inspectingPersonalEvent = null;
+
+      this.resetFields();
+    },
+    updateEntry() {
+      if (!this.toTimeValid) {
+        return;
+      }
+
+      const from = fromTime(this.from);
+      const to = fromTime(this.to);
+      const event = {
+        name: this.name,
+        ects: this.ects,
+        weekday: this.weekday,
+        from: from,
+        to: to,
+        id: `personal-${this.name}-${this.weekday}-${from}-${to}`,
+      } as PersonalEvent;
+
+      const pe = this.personalEvent;
+      if (pe === false || pe === true || pe === null)
+        this.planningStore.addPersonalEvent(event);
+      else this.planningStore.updatePersonalEvent(pe, event);
+
+      this.stateStore.inspectingPersonalEvent = null;
+      this.resetFields();
+    },
+    resetFields() {
+      this.name = "";
+      this.ects = 0;
+      this.weekday = 0;
+      this.from = "";
+      this.to = "";
+    },
+    updateFields() {
+      const pe = this.personalEvent;
+      if (pe === null || pe === false || pe === true) {
+        this.resetFields();
+        return;
+      }
+      this.name = pe.name;
+      this.ects = pe.ects ?? 0;
+      this.weekday = pe.weekday;
+      this.from = toTime(pe.from);
+      this.to = toTime(pe.to);
+    },
+  },
+};
+</script>
+
+<style scoped></style>

+ 75 - 0
src/components/modals/RightDrawer.vue

@@ -0,0 +1,75 @@
+<template>
+  <div
+    v-if="show"
+    class="relative z-10"
+    aria-labelledby="modal-title"
+    role="dialog"
+    aria-modal="true"
+    @keypress.esc="close()"
+    @click="
+      (event) => {
+        if (event.target == $refs.modal) close();
+      }
+    "
+  >
+    <div
+      ref="modal"
+      class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity cursor-pointer"
+    />
+
+    <div
+      class="fixed bottom-0 flex flex-col max-w-full bg-white dark:bg-gray-800 bg-clip-padding drop-shadow-2xl outline-none transition duration-300 ease-in-out top-0 right-0 border-none"
+      style="width: 30rem"
+      tabindex="-1"
+    >
+      <div class="flex-grow p-4 overflow-y-auto">
+        <button
+          title="Schliessen"
+          class="close-button"
+          @click="close()"
+        >
+          <font-awesome-icon icon="fa-solid fa-xmark" />
+        </button>
+
+        <div class="mt-3 sm:mt-0 sm:ml-4 sm:text-left">
+          <slot />
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts">
+export default {
+  name: "RightDrawer",
+  props: {
+    show: {
+      type: Boolean,
+      required: true,
+    },
+  },
+  emits: ["close"],
+  methods: {
+    close() {
+      this.$emit("close");
+    },
+  },
+};
+</script>
+
+<style scoped>
+.close-button {
+  @apply absolute
+    right-1
+    top-0
+    m-4
+    cursor-pointer
+    hover:bg-gray-200
+    active:bg-gray-300
+    dark:hover:bg-gray-600
+    dark:active:bg-gray-700
+    aspect-square
+    w-7
+    rounded;
+}
+</style>

+ 269 - 0
src/components/modals/SettingsModal.vue

@@ -0,0 +1,269 @@
+<template>
+  <BaseModal
+    :show="stateStore.showingSettings"
+    @close="close"
+  >
+    <h1 class="text-3xl font-bold leading-6 text-gray-900 dark:text-white">
+      Einstellungen
+    </h1>
+
+    <h2 class="mt-5 text-lg font-bold">
+      Klassen PDF Version
+    </h2>
+
+    <div>
+      <select
+        ref="classVersionSelect"
+        v-model="selectedClassVersion"
+      >
+        <option
+          v-for="ver in semesterVersions"
+          :key="ver.key.join('/')"
+          :value="ver.key"
+          :disabled="ver.disabled"
+        >
+          {{ ver.title }}
+        </option>
+      </select>
+    </div>
+
+    <h2 class="mt-5 text-lg font-bold">
+      Studenthub
+    </h2>
+
+    <div>
+      <ul class="text-gray-600 dark:text-gray-400 list-decimal ml-5 md:ml-0">
+        <li>
+          Um die Studenthub-Daten zu verwenden, musst du dich zuerst einloggen
+          <span class="italic">(auch wenn der Modulteppich für dich noch nicht funktioniert)</span>:
+          <span
+            class="external-link"
+            @click="openURLInNewWindow('https://studenthub.technik.fhnw.ch/')"
+          >Login
+            <font-awesome-icon icon="fa-solid fa-arrow-up-right-from-square" /></span>
+        </li>
+        <li>
+          Danach kann die Ausgabe der APIs jeweils in das textfeld darunter
+          kopiert werden.
+        </li>
+      </ul>
+
+      <div
+        class="block my-2"
+        :class="[studenthubStore.hasSomeStudenthubData ? '' : 'text-gray-200']"
+      >
+        <span class="font-bold">Bereits bestandene Module ausblenden: </span>
+        <input
+          v-model="hideCompletedClasses"
+          type="checkbox"
+          class="inline w-auto ml-2"
+          :disabled="!studenthubStore.hasSomeStudenthubData"
+        >
+      </div>
+
+      <div class="mb-1">
+        <span class="font-bold mr-2">Student JSON:</span>
+        <span
+          class="external-link"
+          @click="
+            openURLInNewWindow(
+              'https://studenthub.technik.fhnw.ch/api/studenthub/student',
+            )
+          "
+        >API öffnen
+          <font-awesome-icon icon="fa-solid fa-arrow-up-right-from-square" /></span>
+      </div>
+      <textarea
+        :onchange="studenthubStudentChanged"
+        :class="[
+          errors.studenthubStudentParseError ? 'border border-red-500' : '',
+        ]"
+        class="font-mono w-full h-32 dark:bg-gray-900 bg-gray-100 p-1 text-xs"
+        :value="
+          studenthubStore.student ? JSON.stringify(studenthubStore.student) : ''
+        "
+      />
+    </div>
+
+    <div class="mt-2">
+      <div class="mb-1">
+        <span class="font-bold mr-2">Anmeldungen JSON:</span>
+        <span
+          class="external-link"
+          @click="
+            openURLInNewWindow(
+              'https://studenthub.technik.fhnw.ch/api/studenthub/anmeldungen',
+            )
+          "
+        >API öffnen
+          <font-awesome-icon icon="fa-solid fa-arrow-up-right-from-square" /></span>
+      </div>
+      <textarea
+        :onchange="studenthubApplicationsChanged"
+        :class="[
+          errors.studenthubApplicationsParseError
+            ? 'border border-red-500'
+            : '',
+        ]"
+        class="font-mono w-full h-32 dark:bg-gray-900 bg-gray-100 p-1 text-xs"
+        :value="
+          studenthubStore.applications
+            ? JSON.stringify(studenthubStore.applications)
+            : ''
+        "
+      />
+    </div>
+  </BaseModal>
+</template>
+
+<script lang="ts">
+import { openURLInNewWindow } from "../../helpers";
+import { useClassesStore } from "../../stores/classes";
+import { useClassVersionStore } from "../../stores/ClassVersion";
+import { useConfigStore } from "../../stores/config";
+import { usePlanningStore } from "../../stores/planning";
+import { useStateStore } from "../../stores/state";
+import { useStudenthubStore } from "../../stores/studenthub";
+import BaseModal from "./BaseModal.vue";
+
+type SemesterVersionEntry = {
+  disabled: boolean;
+  title: string;
+  key: [string, string];
+};
+
+export default {
+  name: "SettingsModal",
+  components: { BaseModal },
+  setup() {
+    const studenthubStore = useStudenthubStore();
+    const stateStore = useStateStore();
+    const classVersionStore = useClassVersionStore();
+    const configStore = useConfigStore();
+    const classesStore = useClassesStore();
+    const planningStore = usePlanningStore();
+
+    return {
+      studenthubStore,
+      stateStore,
+      classVersionStore,
+      configStore,
+      classesStore,
+      planningStore,
+      openURLInNewWindow,
+    };
+  },
+  data() {
+    return {
+      errors: {
+        studenthubApplicationsParseError: false,
+        studenthubStudentParseError: false,
+      },
+    };
+  },
+  computed: {
+    hideCompletedClasses: {
+      get(): boolean {
+        return this.stateStore.hideCompletedClasses;
+      },
+      set(value: boolean) {
+        this.stateStore.updateHideCompletedClasses(value);
+      },
+    },
+    selectedClassVersion: {
+      get(): [string, string] {
+        const selected = this.classVersionStore;
+        return [selected.semester ?? "", selected.version ?? ""];
+      },
+      set(value: [string, string]) {
+        if (this.planningStore.chosen.length > 0) {
+          const isSure = confirm(
+            "Es ist keine gute Idee die Version des Stundenplans zu ändern, währendem dieser Module eingetragen hat.\n\n" +
+              "Mache einen neuen, leeren Plan oder nimm in kauf, dass deine geplanten Module verschwinden.\n\n" +
+              "Willst du trotzdem fortfahren?",
+          );
+          if (!isSure) {
+            if (this.$refs.classVersionSelect)
+              // @ts-expect-error The value prop exists here, no worries :)
+              this.$refs.classVersionSelect.value = this.selectedClassVersion;
+            return;
+          }
+        }
+
+        const semester = value[0];
+        const version = value[1];
+
+        useClassVersionStore().useSemesterVersion(semester, version);
+        this.planningStore.saveChosen();
+
+        this.classVersionStore.useSemesterVersion(semester, version);
+      },
+    },
+    semesterVersions(): SemesterVersionEntry[] {
+      const out = [] as SemesterVersionEntry[];
+      this.classVersionStore.semesterVersions.forEach((sem) => {
+        out.push({ disabled: true, title: sem.semester, key: ["", ""] });
+        sem.versions.forEach((version) =>
+          out.push({
+            disabled: false,
+            title: `(${sem.semester}) ${version}`,
+            key: [sem.semester, version],
+          }),
+        );
+      });
+      return out;
+    },
+  },
+  methods: {
+    close() {
+      this.stateStore.showingSettings = false;
+    },
+    studenthubApplicationsChanged(event: Event) {
+      const raw = (event.target as HTMLInputElement).value as string;
+      this.errors.studenthubApplicationsParseError = false;
+      if (raw.length == 0) {
+        this.studenthubStore.setApplicationsData(null);
+        return;
+      }
+      let parsed;
+      try {
+        parsed = JSON.parse(raw);
+      } catch (e) {
+        this.studenthubStore.setApplicationsData(null);
+        this.errors.studenthubApplicationsParseError = true;
+        console.error(e);
+        return;
+      }
+      if (parsed === undefined) {
+        this.studenthubStore.setApplicationsData(null);
+        this.errors.studenthubApplicationsParseError = true;
+        return;
+      }
+      this.studenthubStore.setApplicationsData(parsed);
+    },
+    studenthubStudentChanged(event: Event) {
+      const raw = (event.target as HTMLInputElement).value as string;
+      this.errors.studenthubStudentParseError = false;
+      if (raw.length == 0) {
+        this.studenthubStore.setStudentData(null);
+        return;
+      }
+      let parsed;
+      try {
+        parsed = JSON.parse(raw);
+      } catch (e) {
+        this.studenthubStore.setStudentData(null);
+        this.errors.studenthubStudentParseError = true;
+        console.error(e);
+        return;
+      }
+      if (parsed === undefined) {
+        this.studenthubStore.setStudentData(null);
+        this.errors.studenthubStudentParseError = true;
+        return;
+      }
+      this.studenthubStore.setStudentData(parsed);
+    },
+  },
+};
+</script>

+ 94 - 0
src/components/pages/404Page.vue

@@ -0,0 +1,94 @@
+<template>
+  <div class="w-full flex justify-center my-10">
+    <div class="w-full md:w-1/2 flex flex-wrap justify-center text-center">
+      <div class="mb-10">
+        <h1 class="text-3xl font-extrabold">
+          You found the swallow that carries coconuts!
+        </h1>
+        <span class="text-gray-500">But probably not the page you are looking for...</span>
+      </div>
+      <a
+        href="https://www.youtube.com/watch?v=w8Rn_f75UHs"
+        target="_blank"
+        rel="noreferrer noopener"
+        class="w-1/2"
+      >
+        <img
+          src="../../assets/coconuts.svg"
+          alt="Coconuts"
+          width="300"
+          height="500"
+          class="mb-10"
+        >
+      </a>
+      <h1 class="mx-10 md:text-2xl font-medium mb-10">
+        {{ randomQuote }}
+      </h1>
+      <router-link
+        class="action-button text-xl"
+        to="/"
+      >
+        Zum Modulplaner
+        <font-awesome-icon
+          class="ml-2"
+          icon="fa-solid fa-arrow-right"
+        />
+      </router-link>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+// https://www.quoteambition.com/monty-python-and-the-holy-grail-quotes/
+
+import { computed } from "vue";
+
+const quotes = [
+  `"Every time I try to talk to someone it's 'sorry this' and 'forgive me that' and 'I'm not worthy." - God`,
+  `"Please! This is supposed to be a happy occasion. Let's not bicker and argue over who killed who."`,
+  `"Listen, strange women lyin' in ponds distributin' swords is no system for a basis of government. Supreme executive power derives from a mandate from the masses, not from some farcical aquatic ceremony."`,
+  `"On second thought, let's not go to Camelot. It is a silly place." - King Arthur`,
+  `"Stop. Who would cross the Bridge of Death must answer me these questions three, ere the other side he see." - Bridgekeeper`,
+  `"We dine well here in Camelot. We eat ham and jam and Spam a lot." - Knights of Camelot`,
+  `"We are the knights who say—Ni!" - Knight`,
+  `"What do you mean, an African or European swallow?" - King Arthur`,
+  `"Oh, but you can't expect to wield supreme executive power just because some watery tart threw a sword at you." - Dennis`,
+  `"Are you suggesting coconuts migrate?" - Soldier`,
+  `Sir Bedevere: "Good. Now, why do witches burn?", Peasant 3: "Because they're made of—wood?", Sir Bedevere: "Good. So how do you tell whether she is made of wood?", Peasant 1: "Build a bridge out of her."`,
+  `Swamp King: "One day all this will be yours!", Herbert: "What, the curtains?"`,
+  `"That, my liege, is how we know the earth to be banana-shaped." - Sir Bedevere `,
+  `"It's just a flesh wound." - The Black Knight`,
+  `"She turned me into a newt!" - Angry Villager`,
+  `"There are those who call me—Tim." - Tim The Enchanter`,
+  `"What are you going to do, bleed on me?" - King Arthur`,
+  `"The Black Knights always triumph!" - Black Knight`,
+  `"It's not a question of where he grips it! It's a simple question of weight ratios! A five-ounce bird could not carry a one-pound coconut.” - Soldier With a Keen Interest in Birds`,
+  `"Listen. In order to maintain air-speed velocity, a swallow needs to beat its wings 43 times every second, right?” - Soldier With a Keen Interest in Birds`,
+  `"This new learning amazes me, Sir Bedevere. Explain to me again how sheep's bladders may be employed to prevent earthquakes.” - King Arthur`,
+];
+
+const randomQuote = computed(() => {
+  const rnd = Math.floor(Math.random() * quotes.length);
+  return quotes[rnd];
+});
+</script>
+
+<style scoped>
+.action-button {
+  @apply text-center
+    text-gray-700
+    bg-gray-200
+    hover:bg-gray-300
+    active:bg-gray-400
+    dark:text-gray-300
+    dark:bg-gray-600
+    dark:hover:bg-gray-700
+    dark:active:bg-gray-900
+    rounded
+    cursor-pointer
+    py-2
+    px-5
+    transition-all
+    duration-200;
+}
+</style>

+ 7 - 0
src/components/pages/FullDepTreePage.vue

@@ -0,0 +1,7 @@
+<template>
+  <FullDependencyTree />
+</template>
+
+<script setup lang="ts">
+import FullDependencyTree from "../views/FullDependencyTree.vue";
+</script>

+ 48 - 0
src/components/pages/PlannerPage.vue

@@ -0,0 +1,48 @@
+<template>
+  <div class="lg:flex gap-5 p-3 md:p-5">
+    <div
+      class="hidden-in-print"
+      :class="[
+        planningStore.usingModulesFromURL ? 'hidden' : 'w-full lg:w-1/2',
+      ]"
+    >
+      <ClassUpdateView v-if="stateStore.upgradingClassVersion" />
+      <ModuleSelector v-else />
+    </div>
+    <div
+      class="w-full pt-5 lg:pt-0 lg:-order-1"
+      :class="[planningStore.usingModulesFromURL ? '' : 'lg:w-1/2']"
+    >
+      <CalendarView />
+    </div>
+  </div>
+
+  <ModuleDetails />
+  <SettingsModal />
+  <ClassUpdateModal />
+  <OldSemesterReminderModal />
+  <!-- <HelpWantedModal /> -->
+
+  <div class="visible-in-print w-full text-center">
+    <span>Diese Planung basiert auf dem `{{ configStore.config.pdf_version }}`
+      Klassen PDF</span><br>
+  </div>
+</template>
+
+<script setup lang="ts">
+import CalendarView from "../views/CalendarView.vue";
+import ModuleSelector from "../views/ModuleSelector.vue";
+import ClassUpdateView from "../views/ClassUpdateView.vue";
+import { usePlanningStore } from "../../stores/planning";
+import { useConfigStore } from "../../stores/config";
+import ModuleDetails from "../modals/ClassModuleDetails.vue";
+import SettingsModal from "../modals/SettingsModal.vue";
+import { useStateStore } from "../../stores/state";
+import ClassUpdateModal from "../modals/ClassUpdateModal.vue";
+import OldSemesterReminderModal from "../modals/OldSemesterReminderModal.vue";
+// import HelpWantedModal from "../modals/HelpWantedModal.vue";
+
+const planningStore = usePlanningStore();
+const configStore = useConfigStore();
+const stateStore = useStateStore();
+</script>

+ 603 - 0
src/components/views/CalendarView.vue

@@ -0,0 +1,603 @@
+<template>
+  <div
+    v-if="planningStore.usingModulesFromURL"
+    class="grid grid-cols-3 mb-5"
+  >
+    <div />
+    <div class="text-center">
+      <h1
+        v-if="planningStore.sharedDisplayName"
+        class="text-3xl font-bold"
+      >
+        {{ planningStore.sharedDisplayName }}
+      </h1>
+    </div>
+    <div class="flex justify-end">
+      <button
+        title="Zu meinem Kalender hinzufügen"
+        class="h-10 w-20 mx-1 action-button"
+        @click="showAddPlanModal = true"
+      >
+        <font-awesome-icon icon="fa-solid fa-user-plus" />
+      </button>
+    </div>
+  </div>
+
+  <!-- Header -->
+  <div class="overflow-hidden">
+    <table class="w-full text-center table-fixed -mb-0.5">
+      <tbody>
+        <tr class="border-t">
+          <td class="border-x w-14 md:w-20 bg-gray-300 dark:bg-gray-800" />
+          <td class="border-r w-1/7 bg-gray-300 dark:bg-gray-800">
+            Mo
+          </td>
+          <td class="border-r w-1/7 bg-gray-300 dark:bg-gray-800">
+            Di
+          </td>
+          <td class="border-r w-1/7 bg-gray-300 dark:bg-gray-800">
+            Mi
+          </td>
+          <td class="border-r w-1/7 bg-gray-300 dark:bg-gray-800">
+            Do
+          </td>
+          <td class="border-r w-1/7 bg-gray-300 dark:bg-gray-800">
+            Fr
+          </td>
+          <td class="border-r w-1/7 bg-gray-200 dark:bg-gray-700">
+            Sa
+          </td>
+        </tr>
+      </tbody>
+    </table>
+
+    <!-- Body -->
+    <table class="w-full">
+      <tbody>
+        <tr class="border-b">
+          <td class="w-14 md:w-20">
+            <!-- hours -->
+            <table class="w-full">
+              <tr
+                v-for="row in planningStore.hourEnd -
+                  planningStore.hourStart +
+                  1"
+                :key="`hours-row-${row}`"
+                class="border-t"
+              >
+                <td
+                  class="border-x bg-gray-300 dark:bg-gray-800 text-right align-text-top text-sm"
+                  :style="[
+                    `height: ${
+                      planningStore.cellHeight * planningStore.hourDivisions
+                    }px;`,
+                  ]"
+                >
+                  <span class="mr-2">
+                    {{
+                      String(planningStore.hourStart + row - 1).padStart(
+                        2,
+                        "0",
+                      )
+                    }}:00</span>
+                </td>
+              </tr>
+            </table>
+          </td>
+          <td class="w-6/7">
+            <div class="relative">
+              <!-- Time cells -->
+              <table class="w-full table-fixed">
+                <tbody>
+                  <tr
+                    v-for="row in (planningStore.hourEnd -
+                      planningStore.hourStart +
+                      1) *
+                      planningStore.hourDivisions"
+                    :key="`timecell-row-${row}`"
+                    class="border-t"
+                  >
+                    <td
+                      v-for="day in 6"
+                      :key="`timecell-${row}-${day}`"
+                      class="border-r w-1/7"
+                      :style="[`height: ${planningStore.cellHeight}px;`]"
+                      :class="[
+                        day == 6
+                          ? 'bg-gray-200 dark:bg-gray-800 opacity-50'
+                          : '',
+                      ]"
+                    />
+                  </tr>
+                </tbody>
+              </table>
+
+              <!-- Events -->
+              <table class="w-full table-fixed top-0 absolute">
+                <tbody>
+                  <tr>
+                    <td
+                      v-for="day in 6"
+                      :key="`event-day-${day}`"
+                      class="h-0 overflow-visible relative"
+                    >
+                      <div class="mr-2 relative h-1 -mt-1">
+                        <!-- Single event-->
+                        <div
+                          v-for="ce in planningStore.eventsByDay[day - 1]"
+                          :key="ce.taughtClass?.id ?? ce.personalEvent?.id ?? 0"
+                          :class="[
+                            shouldHighlightClass(ce),
+                            ce.personalEvent != null
+                              ? 'single-personal-event'
+                              : 'single-class-event',
+                          ]"
+                          :style="[
+                            `height: ${ce.length}px`,
+                            `top: ${ce.topOffset}px`,
+                            `left: ${ce.leftOffset}%`,
+                            `width: ${ce.width}%`,
+                          ]"
+                        >
+                          <div
+                            v-if="ce.taughtClass != null"
+                            class="cursor-pointer h-full"
+                            @click="stateStore.inspectingClass = ce.taughtClass"
+                          >
+                            <div class="text-xs px-2 text-white">
+                              <p>
+                                <span class="font-bold">{{
+                                  ce.taughtClass.name
+                                }}</span>
+                                ({{ ce.taughtClass.class }})
+                              </p>
+                              <p v-if="ce.taughtClass.rooms.length > 0">
+                                {{ ce.taughtClass.rooms.join(", ") }}
+                              </p>
+                              <p
+                                v-if="
+                                  ce.taughtClass.teaching_type ==
+                                    TeachingType.Online
+                                "
+                              >
+                                <TeachingTypeIcon
+                                  :teaching-type="ce.taughtClass.teaching_type"
+                                />
+                              </p>
+                              <p>{{ ce.taughtClass.teachers.join(", ") }}</p>
+                              <p
+                                class="absolute right-2 bottom-1 font-bold"
+                                title="MSP"
+                              >
+                                {{ ce.taughtClass.module?.hasMSP ? "M" : "" }}
+                              </p>
+                            </div>
+                          </div>
+
+                          <div
+                            v-else-if="ce.personalEvent != null"
+                            class="cursor-pointer h-full"
+                            @click="
+                              stateStore.inspectingPersonalEvent =
+                                ce.personalEvent
+                            "
+                          >
+                            <p class="font-bold text-xs px-2 text-white">
+                              {{ ce.personalEvent.name }}
+                            </p>
+                          </div>
+
+                          <div v-else>
+                            <span>Invalid!</span>
+                          </div>
+                        </div>
+                        <!-- / Single event-->
+                      </div>
+                    </td>
+                  </tr>
+                </tbody>
+              </table>
+
+              <span
+                v-if="!classVersionStore.isLatestSemester"
+                class="absolute text-red-500 opacity-10 dark:opacity-20 sm:text-20xl text-9xl select-none -z-10"
+                style="
+                  rotate: -45deg;
+                  left: 50%;
+                  top: 3%;
+                  transform: translate(-50%, -50%);
+                "
+              >{{ classVersionStore.semester }}</span>
+            </div>
+          </td>
+        </tr>
+      </tbody>
+    </table>
+  </div>
+
+  <div class="text-gray-700 dark:text-gray-300 mt-1 hidden-in-print">
+    <div class="flex items-center justify-between">
+      <div>
+        <button
+          class="action-button"
+          title="Persönlicher Event eintragen"
+          @click="stateStore.inspectingPersonalEvent = true"
+        >
+          <font-awesome-icon icon="fa-solid fa-user-plus" />
+        </button>
+      </div>
+
+      <div>
+        <Popper
+          :arrow="true"
+          :hover="true"
+        >
+          <span class="mr-5 cursor-help">MSP: {{ planningStore.modulesWithMSP.length }}</span>
+          <template #content>
+            Module mit einer MSP:
+            <ul
+              v-if="planningStore.modulesWithMSP.length > 0"
+              class="list-disc list-inside ml-5"
+            >
+              <li
+                v-for="mod in planningStore.modulesWithMSP"
+                :key="mod.short"
+              >
+                <span class="font-bold">{{ mod.short }}</span>:
+                <span class="text-gray-700 dark:text-gray-300">{{
+                  mod.name
+                }}</span>
+              </li>
+            </ul>
+            <span
+              v-else
+              class="font-bold block text-center mt-3"
+            >Keine 🎉</span>
+          </template>
+        </Popper>
+
+        <Popper
+          :arrow="true"
+          :hover="true"
+        >
+          <span
+            class="cursor-help"
+            :class="[hasUnsureModules ? 'text-red-500' : '']"
+          >ECTS:
+            {{
+              (hasUnsureModules ? "~" : "") + planningStore.totalECTSCount.ects
+            }}</span>
+          <template #content>
+            <table class="ml-2">
+              <tbody>
+                <tr>
+                  <td />
+                  <td class="float-right font-bold">
+                    ECTS
+                  </td>
+                </tr>
+                <tr>
+                  <td><span class="font-bold mr-5">VT Klassen</span></td>
+                  <td>
+                    <span class="float-right">{{
+                      planningStore.ectsInBBVTClasses.vt
+                    }}</span>
+                  </td>
+                </tr>
+                <tr>
+                  <td><span class="font-bold mr-5">BB Klassen</span></td>
+                  <td>
+                    <span class="float-right">{{
+                      planningStore.ectsInBBVTClasses.bb
+                    }}</span>
+                  </td>
+                </tr>
+                <tr>
+                  <td><span class="font-bold mr-5">Eng Klassen</span></td>
+                  <td>
+                    <span class="float-right">{{
+                      planningStore.ectsInEngClasses
+                    }}</span>
+                  </td>
+                </tr>
+                <tr>
+                  <td><span class="font-bold mr-5">Kontext Klassen</span></td>
+                  <td>
+                    <span class="float-right">{{
+                      planningStore.ectsInContextClasses
+                    }}</span>
+                  </td>
+                </tr>
+                <tr>
+                  <td><span class="font-bold mr-5">MSPs</span></td>
+                  <td>
+                    <span class="float-right">{{
+                      planningStore.ectsWithMSP
+                    }}</span>
+                  </td>
+                </tr>
+              </tbody>
+            </table>
+
+            <div
+              v-if="hasUnsureModules"
+              class="mt-5"
+            >
+              <span>Die folgenden Module haben keine Credits eingetragen:</span>
+              <ul class="list-disc list-inside ml-5">
+                <li
+                  v-for="m in unsureModules"
+                  :key="m"
+                >
+                  {{ m }}
+                </li>
+              </ul>
+            </div>
+          </template>
+        </Popper>
+
+        <Popper :arrow="true">
+          <div class="ml-4">
+            <button
+              title="Module hervorheben"
+              class="action-button"
+            >
+              <font-awesome-icon icon="fa-solid fa-eye" />
+            </button>
+          </div>
+
+          <template #content>
+            <span class="font-bold">Module hervorheben:</span>
+            <ul class="ml-5 mt-1">
+              <li>
+                <input
+                  id="only-bb-modules"
+                  v-model="stateStore.highlightBBModules"
+                  class="inline-block w-auto mr-2"
+                  type="checkbox"
+                >
+                <label for="only-bb-modules">BB Module</label>
+              </li>
+              <li>
+                <input
+                  id="only-vt-modules"
+                  v-model="stateStore.highlightVTModules"
+                  class="inline-block w-auto mr-2"
+                  type="checkbox"
+                >
+                <label for="only-vt-modules">VT Module</label>
+              </li>
+              <li>
+                <input
+                  id="only-context-modules"
+                  v-model="stateStore.highlightContextModules"
+                  class="inline-block w-auto mr-2"
+                  type="checkbox"
+                >
+                <label for="only-context-modules">Kontext Module</label>
+              </li>
+              <li>
+                <input
+                  id="only-first-phase-modules"
+                  v-model="stateStore.highlightFirstPhaseModules"
+                  class="inline-block w-auto mr-2"
+                  type="checkbox"
+                >
+                <label for="only-first-phase-modules">Module der 1. Einschreibphase</label>
+              </li>
+              <li>
+                <input
+                  id="only-english-modules"
+                  v-model="stateStore.highlightEnglishModules"
+                  class="inline-block w-auto mr-2"
+                  type="checkbox"
+                >
+                <label for="only-english-modules">Module in Englisch</label>
+              </li>
+              <li>
+                <input
+                  id="only-msp-modules"
+                  v-model="stateStore.highlightMSPModules"
+                  class="inline-block w-auto mr-2"
+                  type="checkbox"
+                >
+                <label for="only-msp-modules">Module mit MSP</label>
+              </li>
+            </ul>
+          </template>
+        </Popper>
+      </div>
+    </div>
+  </div>
+
+  <div
+    v-if="planningStore.eventsByDay[BLOCKCOURSE_DAY_IDX].length > 0"
+    class="mt-5"
+  >
+    <h2 class="font-bold text-xl mb-2">
+      Blockmodule
+    </h2>
+    <div class="flex flex-wrap gap-3">
+      <div
+        v-for="ce in planningStore.eventsByDay[BLOCKCOURSE_DAY_IDX]"
+        :key="ce.taughtClass?.id ?? -1"
+        class="single-class-event h-14 w-1/6"
+        style="left: inherit; position: relative"
+        :class="[shouldHighlightClass(ce)]"
+      >
+        <div
+          v-if="ce.taughtClass"
+          class="cursor-pointer h-full"
+          @click="stateStore.inspectingClass = ce.taughtClass"
+        >
+          <div class="text-xs px-2 text-white">
+            <p class="font-bold text-sm">
+              {{ ce.taughtClass.name }}
+            </p>
+            <p>{{ ce.taughtClass.class }}</p>
+            <p
+              class="absolute right-2 bottom-1 font-bold"
+              title="MSP"
+            >
+              {{ ce.taughtClass.module?.hasMSP ? "M" : "" }}
+            </p>
+          </div>
+        </div>
+        <div v-else>
+          Invalid
+        </div>
+      </div>
+    </div>
+  </div>
+
+  <PersonalEventEdit />
+  <ModuleSetEdit
+    :moduleset-name="undefined"
+    :is-new="showAddPlanModal"
+    @close="
+      (newName: string | null) => {
+        showAddPlanModal = false;
+        if (newName == null) return;
+        planningStore.copySharedTo(newName);
+      }
+    "
+  />
+</template>
+
+<script lang="ts">
+import { useModulesStore } from "../../stores/modules";
+import { usePlanningStore, BLOCKCOURSE_DAY_IDX } from "../../stores/planning";
+import { useStateStore } from "../../stores/state";
+import ModuleSetEdit from "../modals/ModulesetEdit.vue";
+import Popper from "vue3-popper";
+import { CalendarEvent, TeachingType } from "../../types";
+import PersonalEventEdit from "../modals/PersonalEventEdit.vue";
+import TeachingTypeIcon from "../general/TeachingTypeIcon.vue";
+import { useClassVersionStore } from "../../stores/ClassVersion";
+
+export default {
+  name: "CalendarView",
+  components: { ModuleSetEdit, Popper, PersonalEventEdit, TeachingTypeIcon },
+  setup() {
+    const planningStore = usePlanningStore();
+    const stateStore = useStateStore();
+    const modulesStore = useModulesStore();
+    const classVersionStore = useClassVersionStore();
+
+    return {
+      planningStore,
+      stateStore,
+      modulesStore,
+      classVersionStore,
+      BLOCKCOURSE_DAY_IDX,
+      TeachingType,
+    };
+  },
+  data() {
+    return {
+      showAddPlanModal: false,
+    };
+  },
+  computed: {
+    hasUnsureModules(): boolean {
+      return this.planningStore.totalECTSCount.unsureModules.length != 0;
+    },
+    unsureModules(): string[] {
+      return this.planningStore.totalECTSCount.unsureModules;
+    },
+  },
+  methods: {
+    shouldHighlightClass(ce: CalendarEvent): string {
+      if (!this.stateStore.hasHighlightedModules) return "";
+      const tc = ce.taughtClass;
+      const lowerOpacity = "opacity-40";
+
+      if (tc === null) return lowerOpacity;
+      if (this.stateStore.highlightBBModules && tc.isBB) return "";
+      if (this.stateStore.highlightVTModules && !tc.isBB) return "";
+      if (this.stateStore.highlightFirstPhaseModules && tc.isFirstPhase)
+        return "";
+      if (this.stateStore.highlightEnglishModules && tc.isEnglish) return "";
+      if (this.stateStore.highlightMSPModules && tc.module?.hasMSP) return "";
+      if (this.stateStore.highlightContextModules && tc.isContext) return "";
+      return lowerOpacity;
+    },
+  },
+};
+</script>
+
+<style scoped>
+table {
+  --border-colour: #999;
+}
+
+@media (prefers-color-scheme: dark) {
+  table {
+    --border-colour: #666;
+  }
+}
+
+table,
+tr,
+td {
+  border-color: var(--border-colour);
+}
+
+.single-class-event {
+  @apply bg-gray-500
+    hover:bg-gray-600
+    active:bg-gray-700
+    dark:bg-gray-600
+    dark:hover:bg-gray-700
+    dark:active:bg-gray-800
+    border
+    border-gray-700
+    hover:border-gray-800
+    dark:border-zinc-400
+    dark:hover:border-zinc-500
+    absolute
+    left-0
+    overflow-hidden
+    rounded
+    transition-all
+    duration-200;
+}
+
+.single-personal-event {
+  @apply bg-green-500
+    hover:bg-green-600
+    active:bg-green-700
+    dark:bg-green-600
+    dark:hover:bg-green-700
+    dark:active:bg-green-800
+    border
+    border-green-700
+    hover:border-green-800
+    dark:border-zinc-400
+    dark:hover:border-zinc-500
+    absolute
+    left-0
+    overflow-hidden
+    rounded
+    transition-all
+    duration-200;
+}
+
+.action-button {
+  @apply bg-gray-200
+    hover:bg-gray-300
+    active:bg-gray-400
+    dark:bg-gray-700
+    dark:hover:bg-gray-800
+    dark:active:bg-gray-900
+    rounded
+    cursor-pointer
+    py-0.5 px-3
+    transition-all
+    duration-200
+    disabled:dark:bg-gray-600
+    disabled:text-gray-500
+    disabled:bg-gray-100
+    disabled:pointer-events-none;
+}
+</style>

+ 193 - 0
src/components/views/ClassUpdateView.vue

@@ -0,0 +1,193 @@
+<template>
+  <h1 class="text-3xl font-bold">
+    Stundenplanänderung
+  </h1>
+
+  <div class="w-full flex justify-center py-12">
+    <h1
+      class="text-md md:text-2xl lg:text-xl xl:text-2xl font-bold leading-6 text-gray-900 dark:text-white"
+    >
+      <span class="bg-orange-300 dark:bg-orange-600 rounded-xl py-2 px-5">{{
+        changes?.oldVersion
+      }}</span>
+      <font-awesome-icon
+        class="px-5 md:px-12"
+        icon="fa-solid fa-arrow-right"
+      />
+      <span class="bg-blue-300 dark:bg-blue-600 rounded-xl py-2 px-5">{{
+        changes?.newVersion
+      }}</span>
+    </h1>
+  </div>
+
+  <div v-if="changes && hasChanges">
+    <div
+      v-if="changes.removals.length > 0"
+      class="mb-5"
+    >
+      <h2 class="text-xl font-bold mb-2">
+        Moduländerungen
+      </h2>
+      <ClassRemovedRow
+        v-for="major in changes.removals"
+        :key="major.id"
+        :taught-class="major"
+      />
+    </div>
+
+    <div v-if="changes.minorChanges.length > 0">
+      <h2 class="mt-5 text-xl font-bold mb-2">
+        Anpassungen
+      </h2>
+
+      <ul class="w-full text-xs xl:text-base">
+        <li
+          v-for="minor in changes.minorChanges"
+          :key="minor.name"
+          class="bg-white dark:bg-gray-800 shadow-lg py-2 px-4 rounded-lg mb-2"
+        >
+          <span class="font-bold w-20 block md:inline-block align-top">{{
+            minor.name
+          }}</span>
+          <table class="block md:inline-block align-top">
+            <tr
+              v-for="change in minor.changes"
+              :key="change.difference"
+            >
+              <td class="w-32 md:w-52">
+                <span>{{ change.difference }}</span>
+              </td>
+              <td>
+                <span class="w-24 inline-block">
+                  {{ change.oldValue }}
+                </span>
+                <span>
+                  <font-awesome-icon
+                    class="px-4 md:px-12 opacity-70"
+                    icon="fa-solid fa-arrow-right"
+                  />
+                </span>
+                <span class="w-24 inline-block">
+                  {{ change.newValue }}
+                </span>
+              </td>
+            </tr>
+          </table>
+        </li>
+      </ul>
+    </div>
+  </div>
+  <div
+    v-else
+    class="w-full flex justify-center py-10 text-gray-800 dark:text-gray-200"
+  >
+    <h1 class="text-3xl">
+      Es gibt keine Änderungen an deiner Planung!
+    </h1>
+  </div>
+  <div class="flex justify-end mt-10 gap-4">
+    <button
+      class="bg-orange-500 text-white py-2 px-5 rounded"
+      @click="cancel"
+    >
+      Abbrechen
+    </button>
+    <button
+      class="bg-blue-500 text-white py-2 px-5 rounded"
+      @click="upgrade"
+    >
+      Upgrade
+    </button>
+  </div>
+</template>
+
+<script lang="ts">
+import { useStateStore } from "../../stores/state";
+import { useModulesStore } from "../../stores/modules";
+import { useClassesStore } from "../../stores/classes";
+import {
+  classesPDFLink,
+  samePDFVersion,
+  semesterVersionString,
+} from "../../helpers";
+import { ClassUpgradeDifference } from "../../types";
+import { usePlanningStore } from "../../stores/planning";
+import { useClassVersionStore } from "../../stores/ClassVersion";
+import { useUpgradeStore } from "../../stores/upgrade";
+import ClassRemovedRow from "../general/ClassRemovedRow.vue";
+
+export default {
+  name: "ClassUpdateView",
+  components: { ClassRemovedRow },
+  setup() {
+    const stateStore = useStateStore();
+    const modulesStore = useModulesStore();
+    const classesStore = useClassesStore();
+    const planningStore = usePlanningStore();
+    const upgradeStore = useUpgradeStore();
+    const classVersionStore = useClassVersionStore();
+    return {
+      stateStore,
+      modulesStore,
+      classesStore,
+      planningStore,
+      upgradeStore,
+      classVersionStore,
+      classesPDFLink,
+    };
+  },
+  data() {
+    return {
+      changes: null as ClassUpgradeDifference | null,
+    };
+  },
+  computed: {
+    hasChanges() {
+      if (this.changes == null) return false;
+      return (
+        this.changes.minorChanges.length > 0 || this.changes.removals.length > 0
+      );
+    },
+  },
+  mounted() {
+    this.classVersionStore.fetchData().then(() => {
+      const latest = this.classVersionStore.latestSemester;
+      if (latest == null) {
+        console.error("No class versions stored on the server!");
+        return;
+      }
+
+      // Prevent from showing when on exactly the same version
+      const semVerStr = semesterVersionString(
+        latest.semester,
+        latest.versions[0],
+      );
+      if (samePDFVersion(semVerStr, this.classVersionStore.semVer)) {
+        this.cancel();
+        return;
+      }
+
+      this.upgradeStore
+        .getChangesToPlan(latest.semester, latest.versions[0])
+        ?.then((changes) => {
+          this.changes = changes;
+          this.upgradeStore.startUpgrade();
+        });
+    });
+  },
+  methods: {
+    cancel() {
+      this.stateStore.upgradingClassVersion = false;
+      this.upgradeStore.reset();
+      usePlanningStore().loadChosen();
+    },
+    upgrade() {
+      this.stateStore.upgradingClassVersion = false;
+      this.upgradeStore.finalize();
+      usePlanningStore().saveChosen();
+    },
+  },
+};
+</script>
+
+<style scoped></style>

+ 278 - 0
src/components/views/FullDependencyTree.vue

@@ -0,0 +1,278 @@
+<template>
+  <div class="md:flex w-full h-screen">
+    <!-- h-screen flex -->
+    <div
+      class="bg-white dark:bg-gray-700 drop-shadow-xl px-5 w-full pb-10 pt-1 md:w-160 md:order-1"
+    >
+      <div
+        v-if="stateStore.inspectingModule != null"
+        class="mt-5"
+      >
+        <h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-white">
+          <span class="font-bold text-3xl mr-5 block">{{
+            stateStore.inspectingModule.short
+          }}</span>
+          <span class="font-light">{{ stateStore.inspectingModule.name }}</span>
+        </h3>
+        <ModuleInfo :module="stateStore.inspectingModule" />
+        <DependencyTree
+          :module="stateStore.inspectingModule"
+          module-name=""
+          :class-language="depLang"
+        />
+      </div>
+
+      <div
+        v-else
+        class="mt-5 dark:text-gray-300 text-gray-700"
+      >
+        <label
+          class="block"
+          for="degree-select"
+        >Studiengang</label>
+        <select
+          id="degree-select"
+          v-model="degreeProgram"
+        >
+          <option
+            v-for="name in classesStore.degreePrograms"
+            :key="name"
+            :value="name"
+          >
+            {{ name }}
+          </option>
+        </select>
+        <label
+          class="block mt-5"
+          for="language-select"
+        >Sprache </label>
+        <div class="flex gap-4">
+          <button
+            v-for="lang in ModuleLanguages"
+            :key="lang"
+            class="lang-button"
+            :disabled="lang == depLang"
+            @click="depLang = lang"
+          >
+            {{ lang }}
+          </button>
+        </div>
+      </div>
+    </div>
+
+    <div
+      class="w-full h-full"
+      @click="deselect"
+    >
+      <!-- eslint-disable vue/no-v-html -->
+      <div
+        ref="svgTxt"
+        class="m-10 hidden"
+      />
+      <!--eslint-enable-->
+      <div
+        id="map"
+        class="h-full w-full z-0"
+      />
+    </div>
+  </div>
+</template>
+
+<script lang="ts">
+import { useModulesStore } from "../../stores/modules";
+import { useClassesStore } from "../../stores/classes";
+import { useStateStore } from "../../stores/state";
+import { useStudenthubStore } from "../../stores/studenthub";
+import { ColourScheme, ModuleLanguages } from "../../types";
+import ModuleInfo from "../general/ModuleInfo.vue";
+import DependencyTree from "../general/DependencyTree.vue";
+import mermaid from "mermaid";
+
+// @ts-expect-error Leaflet has no typing :(
+import L from "leaflet";
+import { storeToRefs } from "pinia";
+
+export default {
+  name: "FullDependencyTree",
+  components: { ModuleInfo, DependencyTree },
+  setup() {
+    mermaid.initialize({
+      theme: "null",
+      startOnLoad: false,
+      securityLevel: "loose",
+      flowchart: { rankSpacing: 150 },
+      maxTextSize: 300000,
+    });
+    const studenthubStore = useStudenthubStore();
+    const moduleStore = useModulesStore();
+    const stateStore = useStateStore();
+    const classesStore = useClassesStore();
+
+    const { inspectingModule } = storeToRefs(stateStore);
+
+    return {
+      moduleStore,
+      studenthubStore,
+      stateStore,
+      classesStore,
+      latLngBounds: L.latLngBounds([
+        [30, 10],
+        [45, 20],
+      ]),
+      ModuleLanguages,
+      inspectingModule,
+    };
+  },
+  data() {
+    return {
+      selectedShort: null as null | string,
+      currentSVGLayer: null,
+      // eslint-disable-next-line @typescript-eslint/no-explicit-any
+      map: null as null | any,
+      degreeProgram: "",
+      depLang: ModuleLanguages.DE,
+    };
+  },
+  computed: {
+    degreeModulesText(): string {
+      let lineId = 0;
+      const allDeps = [] as string[];
+      const linkStyles = [] as string[];
+      const modules = this.moduleStore.data.filter(
+        (m) =>
+          !m.isContext &&
+          !!m.for_degrees &&
+          m.for_degrees.includes(this.degreeProgram),
+      );
+      const completedModules = this.studenthubStore.completedModuleIds.map(
+        (mid) => this.moduleStore.fromModuleId(mid)?.short ?? "",
+      );
+      const defaultColour =
+        this.stateStore.colourScheme == ColourScheme.Dark
+          ? "lightgray"
+          : "black";
+
+      modules.forEach((m) => {
+        // m.dependencies --> m.short
+        if (!m.dependencies[this.depLang]) return;
+        const isSelectedModule = m.short != this.selectedShort;
+        m.dependencies[this.depLang].forEach((dep) => {
+          let colour = completedModules.includes(dep) ? "green" : defaultColour;
+          let linkStyle = `stroke:${colour},color:${colour}`;
+
+          allDeps.push(`${dep} --- ${m.short};`);
+          if (
+            this.selectedShort != null &&
+            isSelectedModule &&
+            dep != this.selectedShort
+          )
+            linkStyle += ",stroke-opacity:0.1";
+          linkStyles.push(`linkStyle ${lineId++} ${linkStyle};`);
+        });
+        allDeps.push(`${m.short}(${m.short});`);
+        allDeps.push(
+          `click ${m.short} href "javascript:moduleClicked('${m.short}');"`,
+        );
+        let currentStyle = ["stroke-width:0px"] as string[];
+        if (m.hasCompleted) currentStyle.push("fill:#d1fae5");
+        else if (m.isActive) currentStyle.push("fill:#dbeafe");
+        else if (m.hasFailed) currentStyle.push("fill:#ffedd9");
+        else currentStyle.push("fill:#fff");
+        if (this.selectedShort != null) {
+          if (isSelectedModule) currentStyle.push("fill-opacity:0.5");
+          else currentStyle.push("fill:#dec0f9");
+        }
+        allDeps.push(`style ${m.short} ${currentStyle.join(",")};`);
+      });
+      return allDeps.join("\n") + "\n" + linkStyles.join("\n");
+    },
+  },
+  watch: {
+    async degreeModulesText() {
+      console.log("Degree mod text updated!");
+      const { svg } = await mermaid.render(
+        "mermaid",
+        "graph BT\n" + this.degreeModulesText,
+      );
+      // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+      // @ts-ignore
+      this.$refs.svgTxt.innerHTML = svg;
+      setTimeout(this.updateMap, 5);
+    },
+    inspectingModule() {
+      this.selectedShort = this.inspectingModule?.short ?? null;
+    },
+  },
+  mounted() {
+    const cd = this.classesStore.currentData;
+    if (cd == null) {
+      console.error("The classes have not been loaded");
+      return;
+    }
+    cd.ready?.then(() => {
+      this.degreeProgram = this.classesStore.degreePrograms[0];
+    });
+    const vm = this; // eslint-disable-line @typescript-eslint/no-this-alias
+
+    // @ts-expect-error Sure, thee windows does not have it defined, so let's define it
+    window.moduleClicked = function (short: string) {
+      vm.selectedShort = short;
+      vm.stateStore.inspectingModule = vm.moduleStore.fromModuleShort(short);
+    };
+    this.map = L.map("map", { attributionControl: false, zoomSnap: 0.5 });
+
+    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+    //@ts-ignore
+    this.map.fitBounds(this.latLngBounds).setZoom(8);
+    this.updateMap();
+  },
+  methods: {
+    deselect(event: MouseEvent) {
+      if ((event.target as HTMLInputElement).id != "mermaid") return;
+
+      this.selectedShort = null;
+      this.stateStore.inspectingModule = null;
+    },
+    updateMap() {
+      if (this.map == null) return;
+      const svgElement = document.querySelector("#mermaid");
+      if (svgElement == null) {
+        console.warn(
+          "Failed to find the mermaid map! Not updating the leaflet map...",
+        );
+        return;
+      }
+
+      L.svgOverlay(svgElement, this.latLngBounds, {
+        opacity: 1,
+        interactive: true,
+      }).addTo(this.map);
+    },
+  },
+};
+</script>
+
+<style>
+.leaflet-container {
+  @apply bg-gray-100 dark:bg-gray-800;
+}
+</style>
+<style scoped>
+.lang-button {
+  @apply rounded
+  w-full
+  py-2
+  bg-gray-100
+  hover:bg-gray-200
+  active:bg-gray-300
+  disabled:bg-gray-50
+  disabled:text-gray-500
+  dark:bg-gray-800
+  dark:hover:bg-gray-900
+  dark:active:bg-black
+  dark:disabled:bg-gray-600
+  dark:disabled:text-gray-300
+  transition-all
+  duration-200;
+}
+</style>

+ 239 - 0
src/components/views/ModuleSelector.vue

@@ -0,0 +1,239 @@
+<template>
+  <LoadSaveView />
+
+  <form
+    @submit.prevent=""
+    @keydown.enter.prevent=""
+  >
+    <ul>
+      <li
+        v-for="(filterSet, idx) in classesStore.filterRules"
+        :key="filterSet.pk"
+        class="mb-2"
+      >
+        <ClassFilter
+          v-model="classesStore.filterRules[idx]"
+          :is-last-filter="classesStore.filterRules.length == 1"
+          @add-filter="classesStore.insertFilter"
+          @remove-filter="classesStore.removeFilter"
+        />
+      </li>
+    </ul>
+  </form>
+
+  <table
+    class="module-table mt-5 border-collapse w-full text-center text-xs xl:text-sm 2xl:text-base"
+  >
+    <thead>
+      <tr
+        class="border-b dark:border-gray-500 font-bold bg-gray-200 h-10 dark:bg-gray-500 dark:text-white"
+      >
+        <td>
+          <OrderingControl
+            title="Modul"
+            @changed="
+              (order) => orderingChanged(ClassSelectorColumn.Module, order)
+            "
+          />
+        </td>
+        <td>
+          <OrderingControl
+            title="Durchführung"
+            @changed="
+              (order) => orderingChanged(ClassSelectorColumn.Time, order)
+            "
+          />
+        </td>
+        <td>
+          <OrderingControl
+            title="Klasse"
+            @changed="
+              (order) => orderingChanged(ClassSelectorColumn.Class, order)
+            "
+          />
+        </td>
+        <td class="hidden md:table-cell">
+          <OrderingControl
+            title="Dozent"
+            @changed="
+              (order) => orderingChanged(ClassSelectorColumn.Lecturer, order)
+            "
+          />
+        </td>
+        <td class="hidden md:table-cell">
+          <OrderingControl
+            title="Raum"
+            @changed="
+              (order) => orderingChanged(ClassSelectorColumn.Room, order)
+            "
+          />
+        </td>
+        <td>
+          <OrderingControl
+            title="Art"
+            @changed="
+              (order) =>
+                orderingChanged(ClassSelectorColumn.TeachingType, order)
+            "
+          />
+        </td>
+        <td class="hidden xl:table-cell">
+          <OrderingControl
+            title="MSP"
+            @changed="
+              (order) => orderingChanged(ClassSelectorColumn.MSP, order)
+            "
+          />
+        </td>
+        <td />
+      </tr>
+    </thead>
+
+    <ClassRow
+      v-for="cls in paginatedClasses"
+      :key="cls.id"
+      :cls="cls"
+    />
+  </table>
+
+  <div
+    v-if="filteredModules.length == 0"
+    class="flex flex-wrap justify-center mt-10 text-center"
+  >
+    <span
+      class="text-2xl text-gray-400 dark:text-gray-600 no-results-text font-mono w-full"
+    >Schade, leider gibt es keine Resultate...</span>
+    <button
+      title="Module durchsuchen"
+      class="action-button mt-5"
+      @click="stateStore.showingModuleSearch = true"
+    >
+      Alle Module durchsuchen
+    </button>
+  </div>
+
+  <NumberedPagination
+    v-model="currentPageIdx"
+    :page-size="pageSize"
+    :result-count="filteredModules.length"
+  />
+  <ModuleSearchModal />
+</template>
+
+<script lang="ts">
+import { useClassesStore } from "../../stores/classes";
+import { usePlanningStore } from "../../stores/planning";
+import { classesPDFLink, dayMap, toTime } from "../../helpers";
+import { watch } from "vue";
+import LoadSaveView from "../general/ModulesetManager.vue";
+import OrderingControl from "../general/OrderingControl.vue";
+import { TaughtClass, ClassSelectorColumn, Ordering } from "../../types";
+import { useStateStore } from "../../stores/state";
+import { useStudenthubStore } from "../../stores/studenthub";
+import { useModulesStore } from "../../stores/modules";
+import ClassFilter from "../general/ClassFilter.vue";
+import ClassRow from "../general/ClassRow.vue";
+import ModuleSearchModal from "../modals/ModuleSearchModal.vue";
+import NumberedPagination from "../general/NumberedPagination.vue";
+
+export default {
+  name: "ModuleSelector",
+  components: {
+    LoadSaveView,
+    OrderingControl,
+    ClassFilter,
+    ClassRow,
+    ModuleSearchModal,
+    NumberedPagination,
+  },
+  setup() {
+    const classesStore = useClassesStore();
+    const planningStore = usePlanningStore();
+    const stateStore = useStateStore();
+    const studenthubStore = useStudenthubStore();
+    const modulesStore = useModulesStore();
+
+    return {
+      classesStore,
+      planningStore,
+      studenthubStore,
+      modulesStore,
+      toTime,
+      classesPDFLink,
+      dayMap,
+      pageSize: 12,
+      stateStore,
+      ClassSelectorColumn,
+    };
+  },
+  data() {
+    return {
+      currentPageIdx: 0,
+      inspectingClass: null as TaughtClass | null,
+    };
+  },
+  computed: {
+    filteredModules() {
+      return this.classesStore.filteredModules;
+    },
+    paginatedClasses() {
+      const idx = this.currentPageIdx;
+      return this.filteredModules.slice(
+        idx * this.pageSize,
+        (idx + 1) * this.pageSize,
+      );
+    },
+  },
+  mounted() {
+    watch(
+      () => this.filteredModules,
+      () => (this.currentPageIdx = 0),
+    );
+  },
+  methods: {
+    orderingChanged(col: ClassSelectorColumn, dir: Ordering) {
+      this.classesStore.updateOrdering(col, dir);
+    },
+  },
+};
+</script>
+
+<style scoped>
+.no-results-text::after {
+  @apply bg-gray-600 dark:bg-gray-300;
+  content: "";
+  width: 10px;
+  height: 20px;
+  margin-left: 3px;
+  display: inline-block;
+  animation: cursor-blink 1s alternate infinite;
+}
+
+@keyframes cursor-blink {
+  0% {
+    opacity: 0;
+  }
+}
+
+.action-button {
+  @apply h-10
+    text-xs
+    px-10
+    md:text-base
+    mx-0.5
+    md:mx-1
+    block
+    bg-gray-300
+    dark:bg-gray-700
+    hover:bg-gray-400
+    active:bg-gray-500
+    dark:hover:bg-gray-600
+    dark:active:bg-gray-700
+    dark:disabled:bg-gray-800
+    dark:disabled:text-gray-600
+    disabled:pointer-events-none
+    rounded
+    transition-all
+    duration-200;
+}
+</style>

+ 194 - 0
src/helpers.ts

@@ -0,0 +1,194 @@
+import { useClassVersionStore } from "./stores/ClassVersion";
+import { useClassesStore } from "./stores/classes";
+import {
+  HistoricClassEntry,
+  Module,
+  SemesterVersion,
+  TaughtClass,
+} from "./types";
+
+export function toTime(t: number): string {
+  const hours = Math.floor(t / 3600);
+  const minutes = (t - 3600 * hours) / 60;
+  return `${String(hours).padStart(2, "0")}:${String(minutes).padStart(
+    2,
+    "0",
+  )}`;
+}
+
+export function fromTime(t: string): number {
+  const spl = t.split(":");
+  const hours = Number.parseInt(spl[0]);
+  const minutes = Number.parseInt(spl[1]);
+  return hours * 3600 + minutes * 60;
+}
+
+export function copyToClipboard(text: string) {
+  if (!navigator.clipboard) {
+    return null;
+  }
+
+  return navigator.clipboard.writeText(text);
+}
+
+export const dayMap: Record<number, string> = {
+  0: "Mo",
+  1: "Di",
+  2: "Mi",
+  3: "Do",
+  4: "Fr",
+  5: "Sa",
+};
+
+export const waitTimeAfterUpgradeRequestModal = 3600000; // 1 Hour
+export const waitTimeAfterOldPlanReminder = 7 * 24 * 3600000; // 1 Week
+
+export function openURLInNewWindow(url: string) {
+  window.open(
+    url,
+    "_blank",
+    "resizable=yes, scrollbars=yes, titlebar=yes, width=800, height=600",
+  );
+}
+
+export const bbClassRe = /bb\d?$/;
+export const firstPhaseClassRe = /(?:bb\d?$|^\dK)/;
+export const englishClassRe = /(?:eng|KE[a-z])$/;
+export const contextClassRe = /^\dK/;
+
+export function classesPDFLink(
+  cls: TaughtClass | HistoricClassEntry,
+  semester: string | null = null,
+  version: string | null = null,
+): string {
+  let semesterFolder;
+  let semVerFolder;
+
+  if (semester == null || version == null) {
+    const versionStore = useClassVersionStore();
+    semesterFolder = versionStore.semesterFolder;
+    semVerFolder = versionStore.semVerFolder;
+  } else {
+    semesterFolder = getSemesterFolder(semester);
+    semVerFolder = getSemVerFolder(semester, version);
+  }
+
+  if (cls.weekday === null) {
+    const cs = useClassesStore();
+    const file = cs.currentData?.config.blockclass_file ?? "block.pdf";
+    return `${semesterFolder}/${file}#page=${cls.pages[0]}`;
+  }
+
+  return `${semVerFolder}/klassen.pdf#page=${cls.pages[0]}`;
+}
+
+export function getSemesterFolder(semester: string): string {
+  return `./data/${semester}`;
+}
+
+export function getSemVerFolder(semester: string, version: string): string {
+  return `${getSemesterFolder(semester)}/${version}`;
+}
+
+export function addRemoveClassTitle(
+  module: Module | null,
+  isChosen: boolean,
+): string {
+  if (module?.hasCompleted) return "Modul bereits bestanden";
+  if (module?.maxAttemptsReached)
+    return `Du hast dieses Modul bereits ${module.attemptCount} Mal versucht!`;
+
+  if (isChosen) return "Von der Planung entfernen";
+  return "Zur Planung hinzufügen";
+}
+
+export function rowStyling(mod: Module | null, defaultColour = ""): string {
+  if (mod == null) return defaultColour;
+
+  if (mod.hasCompleted) return "dark:bg-emerald-900 bg-emerald-100";
+  if (mod.isActive) return "dark:bg-blue-900 bg-blue-100";
+  if (mod.hasFailed) return "dark:bg-orange-900 bg-orange-100";
+  return defaultColour;
+}
+
+// eslint-disable-next-line
+export function matchesOneOf(list1: any[], list2: any[]): boolean {
+  if (list1.length == 0 || list2.length == 0) return false;
+  return !list1.every((e1) => {
+    return !list2.includes(e1);
+  });
+}
+
+export const MAX_ATTEMPT_COUNT = 2;
+
+// TODO: Set externally
+export const URLS = {
+  GITLAB_REPO: "https://example.com",
+  GITLAB_REPO_TICKET: "https://example.com",
+  MODULE_SIGNUP: "https://example.com",
+  STUDENTHUB: "https://example.com",
+  ALL_TIMETABLES: "https://example.com",
+  ADDITIONAL_MODULE_INFORMATION: "https://example.com",
+};
+
+export const SCHOOL_NAME = "<TODO>";
+
+export const MinFullNameSearchLength = 4;
+
+/**
+ * Parses version strings into [Semester, Version] from two formats:
+ * - "einschreiben / FS_23_einschr_2"
+ * - "23HS|einschr_2"
+ *
+ * @param versionStr The version string that should be parsed
+ * @returns {Semester, Version}
+ */
+export function parsePDFVersion(versionStr: string | null): {
+  semester: string | null;
+  version: string | null;
+} {
+  if (versionStr == null) return { semester: null, version: null };
+  const extractReInternal = /(.*)\|(.*)/;
+  const extractRePdf = /\/ (HS|FS)_(\d{2})_(.*)/;
+
+  const resInt = versionStr.match(extractReInternal);
+  if (resInt != null) {
+    const semester = resInt[1];
+    const version = resInt[2];
+    return { semester, version };
+  }
+
+  const res = versionStr.match(extractRePdf);
+  if (res == null) return { semester: null, version: null };
+
+  const semester = res[2] + res[1];
+  const version = res[3];
+  return { semester, version };
+}
+
+export function samePDFVersion(
+  vers1: string | null,
+  vers2: string | null,
+): boolean {
+  const parsed1 = parsePDFVersion(vers1);
+  const parsed2 = parsePDFVersion(vers2);
+
+  return (
+    parsed1.semester == parsed2.semester && parsed1.version == parsed2.version
+  );
+}
+
+export function semesterVersionString(
+  semester: string,
+  version: string,
+): string {
+  return `${semester}|${version}`;
+}
+
+export function semesterVersionStringC(
+  semVer: SemesterVersion | null,
+  versionIdx = 0,
+): string | null {
+  if (semVer == null) return null;
+  return `${semVer.semester}|${semVer.versions[versionIdx]}`;
+}

+ 96 - 0
src/main.ts

@@ -0,0 +1,96 @@
+import { createApp } from "vue";
+import { createPinia } from "pinia";
+import "./style.css";
+import "leaflet/dist/leaflet.css";
+import "vue-toastification/dist/index.css";
+// import Vue3Mermaid from "vue3-mermaid";
+
+import App from "./App.vue";
+
+import Toast, { PluginOptions } from "vue-toastification";
+import { library } from "@fortawesome/fontawesome-svg-core";
+import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
+import {
+  faArrowLeft,
+  faArrowRight,
+  faArrowUp,
+  faArrowUpRightFromSquare,
+  faBars,
+  faCalendarDay,
+  faChevronDown,
+  faChevronRight,
+  faCog,
+  faCopy,
+  faEye,
+  faFilePdf,
+  faHeart,
+  faInfo,
+  faMagnifyingGlass,
+  faMinus,
+  faPencil,
+  faPlus,
+  faPuzzlePiece,
+  faSchool,
+  faShare,
+  faSitemap,
+  faSort,
+  faSortDown,
+  faSortUp,
+  faTrashCan,
+  faUserPlus,
+  faWifi,
+  faXmark,
+  faSquare,
+  faHouseLaptop,
+  faEnvelope,
+  faChevronUp,
+} from "@fortawesome/free-solid-svg-icons";
+import { faGitlab } from "@fortawesome/free-brands-svg-icons";
+import { router } from "./router";
+
+library.add(faTrashCan);
+library.add(faPlus);
+library.add(faMinus);
+library.add(faWifi);
+library.add(faSchool);
+library.add(faArrowLeft);
+library.add(faArrowRight);
+library.add(faHeart);
+library.add(faGitlab);
+library.add(faFilePdf);
+library.add(faShare);
+library.add(faCopy);
+library.add(faXmark);
+library.add(faInfo);
+library.add(faPencil);
+library.add(faSort);
+library.add(faSortUp);
+library.add(faSortDown);
+library.add(faUserPlus);
+library.add(faArrowUp);
+library.add(faInfo);
+library.add(faCog);
+library.add(faArrowUpRightFromSquare);
+library.add(faEye);
+library.add(faChevronRight);
+library.add(faChevronDown);
+library.add(faMagnifyingGlass);
+library.add(faPuzzlePiece);
+library.add(faBars);
+library.add(faSitemap);
+library.add(faCalendarDay);
+library.add(faSquare);
+library.add(faHouseLaptop);
+library.add(faEnvelope);
+library.add(faChevronUp);
+
+const pinia = createPinia();
+const toastOptions: PluginOptions = {};
+
+createApp(App)
+  .component("font-awesome-icon", FontAwesomeIcon)
+  .use(router)
+  .use(pinia)
+  .use(Toast, toastOptions)
+  // .use(Vue3Mermaid)
+  .mount("#app");

+ 16 - 0
src/router.ts

@@ -0,0 +1,16 @@
+import { createRouter, createWebHashHistory } from "vue-router";
+
+import PlannerPage from "./components/pages/PlannerPage.vue";
+const FullDepTreePage = () => import("./components/pages/FullDepTreePage.vue");
+const NotFound = () => import("./components/pages/404Page.vue");
+
+const routes = [
+  { path: "/", name: "ModulePlanner", component: PlannerPage },
+  { path: "/tree", name: "FullDepTree", component: FullDepTreePage },
+  { path: "/:pathMatch(.*)*", name: "NotFound", component: NotFound },
+];
+
+export const router = createRouter({
+  history: createWebHashHistory(),
+  routes,
+});

+ 194 - 0
src/stores/ClassVersion.ts

@@ -0,0 +1,194 @@
+import { defineStore } from "pinia";
+import {
+  getSemesterFolder,
+  getSemVerFolder,
+  parsePDFVersion,
+  semesterVersionString,
+} from "../helpers";
+import { SemesterVersion } from "../types";
+import { useToast } from "vue-toastification";
+import { useClassesStore } from "./classes";
+import { useConfigStore } from "./config";
+import { usePlanningStore } from "./planning";
+const toast = useToast();
+
+function showUnknownVersionToast() {
+  toast.error(
+    "Die ausgewählte Semester / Stundenplan-Version existiert nicht!",
+  );
+}
+
+export const useClassVersionStore = defineStore("classVersion", {
+  state: () => {
+    return {
+      semester: null as null | string,
+      version: null as null | string,
+      semesterVersions: [] as SemesterVersion[],
+      ready: null as Promise<null> | null,
+    };
+  },
+  getters: {
+    latestSemester(): SemesterVersion | null {
+      if (this.semesterVersions.length == 0) return null;
+
+      const currSem = this.semesterVersions.find(
+        (el) => el.semester == this.semester,
+      );
+      if (currSem == null) return null;
+
+      return currSem;
+    },
+    isLatestSemester(): boolean {
+      if (this.semesterVersions.length == 0) return false;
+      return this.semester == this.semesterVersions[0].semester;
+    },
+    latestOverall(): SemesterVersion | null {
+      if (this.semesterVersions.length == 0) return null;
+      return this.semesterVersions[0];
+    },
+    semesterFolder(): string | null {
+      if (this.semester == null) return null;
+      return getSemesterFolder(this.semester);
+    },
+    semVerFolder(): string | null {
+      if (this.semester == null || this.version == null) return null;
+      return getSemVerFolder(this.semester, this.version);
+    },
+    semVer(): string | null {
+      if (this.semester == null || this.version == null) return null;
+      return semesterVersionString(this.semester, this.version);
+    },
+    isLatestSemesterVersion(): boolean {
+      if (this.semesterVersions.length == 0) return true;
+
+      const currSem = this.semesterVersions.find(
+        (el) => el.semester == this.semester,
+      );
+      if (currSem == null) return true;
+
+      return (
+        this.semester == currSem.semester && this.version == currSem.versions[0]
+      );
+    },
+    isLatestOverall(): boolean {
+      if (this.semesterVersions.length == 0) return true;
+
+      const latestEntry = this.semesterVersions[0];
+      return (
+        this.semester == latestEntry.semester &&
+        this.version == latestEntry.versions[0]
+      );
+    },
+  },
+  actions: {
+    fetchData(): Promise<null> {
+      if (this.ready != null) return this.ready;
+
+      this.ready = new Promise((resolve, reject) => {
+        fetch(`./data/semester-versions.json`)
+          .then((response) => response.json())
+          .then((data) => {
+            this.semesterVersions = data;
+            if (this.semester == null && this.version == null) {
+              const latestEntry = this.semesterVersions[0];
+              this.semester = latestEntry.semester;
+              this.version = latestEntry.versions[0];
+              this.update();
+            }
+            resolve(null);
+          })
+          .catch((error) => {
+            console.error(error);
+            toast.error(
+              "Die Stundenplan-Versionen konnten nicht geladen werden!",
+            );
+            this.semesterVersions = [];
+            reject();
+          });
+      });
+
+      return this.ready;
+    },
+    useFromPDFString(version: string): boolean {
+      const pdfVersion = parsePDFVersion(version);
+
+      if (pdfVersion.semester == null || pdfVersion.version == null)
+        return false;
+
+      if (
+        pdfVersion.semester != this.semester ||
+        pdfVersion.version != this.version
+      )
+        this.useSemesterVersion(pdfVersion.semester, pdfVersion.version);
+
+      return true;
+    },
+    hasSemesterVersion(
+      semester: string | null,
+      version: string | null,
+    ): boolean {
+      if (semester == null || version == null) return false;
+
+      return (
+        this.semesterVersions.filter(
+          (sv) => sv.semester == semester && sv.versions.includes(version),
+        ).length > 0
+      );
+    },
+    useSemester(semester: string) {
+      if (!this.hasSemesterVersion(semester, this.version)) {
+        showUnknownVersionToast();
+        return;
+      }
+
+      this.semester = semester;
+      this.update();
+    },
+    useVersion(version: string) {
+      if (!this.hasSemesterVersion(this.semester, version)) {
+        showUnknownVersionToast();
+        return;
+      }
+
+      this.version = version;
+      this.update();
+    },
+    useSemesterVersion(semester: string, version: string) {
+      if (!this.hasSemesterVersion(semester, version)) {
+        showUnknownVersionToast();
+        return;
+      }
+
+      this.semester = semester;
+      this.version = version;
+      this.update();
+    },
+    update() {
+      useConfigStore().fetchData();
+      useClassesStore().fetchData();
+      usePlanningStore().loadChosen(null, false);
+    },
+    semesterFolderFromSemVer(version: string): string {
+      const pdfVersion = parsePDFVersion(version);
+      if (pdfVersion.semester == null) {
+        console.error(
+          `Failed to find the semester in the given string: ${version}`,
+        );
+        return "";
+      }
+
+      return getSemesterFolder(pdfVersion.semester);
+    },
+    semVerFolderFromSemVer(version: string): string {
+      const pdfVersion = parsePDFVersion(version);
+      if (pdfVersion.semester == null || pdfVersion.version == null) {
+        console.error(
+          `Failed to find the semester in the given string: ${version}`,
+        );
+        return "";
+      }
+
+      return getSemVerFolder(pdfVersion.semester, pdfVersion.version);
+    },
+  },
+});

+ 516 - 0
src/stores/classes.ts

@@ -0,0 +1,516 @@
+import { defineStore } from "pinia";
+import {
+  MinFullNameSearchLength,
+  bbClassRe,
+  contextClassRe,
+  dayMap,
+  englishClassRe,
+  firstPhaseClassRe,
+  toTime,
+} from "../helpers";
+import {
+  TaughtClass,
+  ClassSelectorColumn,
+  Ordering,
+  FilterRule,
+  ModuleLanguages,
+  ClassesVersion,
+  SemesterConfiguration,
+} from "../types";
+import { useClassVersionStore } from "./ClassVersion";
+import { useModulesStore } from "./modules";
+import { usePlanningStore } from "./planning";
+import { useStateStore } from "./state";
+import { useStudenthubStore } from "./studenthub";
+
+// eslint-disable-next-line
+type SortingFunction = (o1: any, o2: any) => number;
+
+export const useClassesStore = defineStore("classes", {
+  state: () => {
+    const stateStore = useStateStore();
+    const cvStore = useClassVersionStore();
+
+    return {
+      data: {} as Record<string, ClassesVersion>,
+      filterRules: [
+        {
+          column: ClassSelectorColumn.Degree,
+          filterData: { degree: `${stateStore.lastSelectedDegreeProgram}` },
+          pk: Math.floor(Math.random() * 1e20),
+          enabled: true,
+        },
+        {
+          column: ClassSelectorColumn.Module,
+          filterData: { term: "" },
+          pk: Math.floor(Math.random() * 1e20),
+          enabled: true,
+        },
+      ] as FilterRule[],
+      sortBy: {} as Record<string, SortingFunction>,
+      cvStore,
+    };
+  },
+
+  getters: {
+    allClasses(): TaughtClass[] {
+      const cd = this.currentData;
+      if (cd == null) return [];
+      return [...cd.taughtClasses, ...cd.blockClasses];
+    },
+    classesFilteredByCompletion(): TaughtClass[] {
+      const stateStore = useStateStore();
+      if (!stateStore.hideCompletedClasses) return this.allClasses;
+
+      const studenthubStore = useStudenthubStore();
+      return this.allClasses.filter((c) => {
+        const m = c.module;
+        if (m?.module_id == null) return true;
+        return !studenthubStore.hasCompletedModule(m.module_id);
+      });
+    },
+    getAllSortedClasses(): TaughtClass[] {
+      const keys = Object.keys(this.sortBy);
+      const numberOfProperties = keys.length;
+      let modules = [...this.classesFilteredByCompletion];
+
+      if (numberOfProperties > 0) {
+        modules = modules.sort((obj1, obj2) => {
+          let i = 0;
+          let result = 0;
+
+          while (result === 0 && i < numberOfProperties) {
+            result = this.sortBy[keys[i]](obj1, obj2);
+            i++;
+          }
+
+          return result;
+        });
+      }
+      return modules;
+    },
+    filteredModules(): TaughtClass[] {
+      let classes = this.getAllSortedClasses;
+
+      const transformedRules = {} as Record<ClassSelectorColumn, FilterRule[]>;
+      this.filterRules.forEach((rule) => {
+        if (!rule.enabled) return;
+        if (rule.column in transformedRules)
+          transformedRules[rule.column].push(rule);
+        else transformedRules[rule.column] = [rule];
+      });
+
+      Object.keys(transformedRules).forEach((column) => {
+        const foundClasses = [] as TaughtClass[];
+        let didFilter = false;
+
+        if (column == ClassSelectorColumn.Time) {
+          transformedRules[column].forEach((rule) =>
+            foundClasses.push(...filterTimeColumn(classes, rule)),
+          );
+          classes = [...new Set(foundClasses)];
+          return;
+        }
+
+        if (column == ClassSelectorColumn.Degree) {
+          let shouldFilter = true;
+          transformedRules[column].forEach((rule) => {
+            const degree = rule.filterData?.degree ?? "";
+            if (degree.length == 0) {
+              shouldFilter = false;
+              return;
+            }
+            didFilter = true;
+            foundClasses.push(
+              ...classes.filter((c) => c.degree_prg == rule.filterData.degree),
+            );
+          });
+
+          if (shouldFilter && didFilter) classes = [...new Set(foundClasses)];
+          return;
+        }
+
+        switch (column) {
+          case ClassSelectorColumn.Module:
+            transformedRules[column].forEach((rule) => {
+              const term = rule.filterData?.term?.toLowerCase() ?? "";
+              const checkName = term.length >= MinFullNameSearchLength;
+              if (term.length == 0) return;
+              didFilter = true;
+              foundClasses.push(
+                ...classes.filter(
+                  (c) =>
+                    c.name.toLowerCase().includes(term) ||
+                    (checkName &&
+                      c.module?.name.toLocaleLowerCase().includes(term)),
+                ),
+              );
+            });
+            break;
+
+          case ClassSelectorColumn.Room:
+            transformedRules[column].forEach((rule) => {
+              const term = rule.filterData?.term?.toLowerCase() ?? "";
+              if (term.length == 0) return;
+              didFilter = true;
+              const clsss = classes.filter((c) => {
+                if (c.rooms.length == 0) return false;
+                return c.rooms.join(" ").toLowerCase().includes(term);
+              });
+              foundClasses.push(...clsss);
+            });
+            break;
+
+          case ClassSelectorColumn.Class:
+            transformedRules[column].forEach((rule) => {
+              const term = rule.filterData?.term?.toLowerCase() ?? "";
+              if (term.length == 0) return;
+              didFilter = true;
+              foundClasses.push(
+                ...classes.filter((c) => c.class.toLowerCase().includes(term)),
+              );
+            });
+            break;
+
+          case ClassSelectorColumn.Lecturer:
+            transformedRules[column].forEach((rule) => {
+              const term = rule.filterData?.term?.toLowerCase() ?? "";
+              if (term.length == 0) return;
+              didFilter = true;
+              foundClasses.push(
+                ...classes.filter((c) =>
+                  c.teachers.join().toLowerCase().includes(term),
+                ),
+              );
+            });
+            break;
+
+          case ClassSelectorColumn.TeachingType:
+            transformedRules[column].forEach((rule) => {
+              if (rule.filterData.teachingType?.length == 0) return;
+
+              didFilter = true;
+              foundClasses.push(
+                ...classes.filter(
+                  (c) => c.teaching_type == rule.filterData.teachingType,
+                ),
+              );
+            });
+            break;
+
+          default:
+            console.warn(`Filter type ${column} is not supported!`);
+            return;
+        }
+        if (didFilter) classes = [...new Set(foundClasses)];
+      });
+
+      return classes;
+    },
+    degreePrograms(): string[] {
+      const cd = this.currentData;
+      if (cd == null) return [];
+      return cd.degreePrograms;
+    },
+    currentData(): ClassesVersion | null {
+      const version = this.cvStore.semVer;
+      if (version == null) return null;
+      if (!(version in this.data)) {
+        console.error(`The version ${version} has not yet been loaded!`);
+        return null;
+      }
+
+      return this.data[version];
+    },
+  },
+
+  actions: {
+    fetchData(version: string | null = null): Promise<null> {
+      if (version == null) {
+        version = this.cvStore.semVer;
+
+        if (version == null) {
+          // console.error("The current semester/version was unexpectedly null!");
+          return new Promise((res, rej) => rej);
+        }
+      }
+
+      // Check if the data has already been loaded
+      if (version in this.data)
+        return this.data[version].ready ?? new Promise((res, rej) => rej);
+
+      const semVer = version;
+      this.data[semVer] = {
+        version: semVer,
+        ready: null,
+        loaded: false,
+        teachers: [],
+        rooms: [],
+        classes: [],
+        days: [],
+        taughtClasses: [],
+        blockClasses: [],
+        degreePrograms: [],
+        config: { blockclass_file: "block.pdf" },
+      };
+
+      const semVerFolder = this.cvStore.semVerFolderFromSemVer(semVer);
+      const semesterFolder = this.cvStore.semesterFolderFromSemVer(semVer);
+
+      this.data[semVer].ready = new Promise((resolve, reject) => {
+        const fe = fetch(`${semVerFolder}/classes.json`);
+        const bc = fetch(`${semesterFolder}/blockclasses.json`);
+        const sc = fetch(`${semesterFolder}/config.json`);
+        const modulesStore = useModulesStore();
+
+        const addModuleGetters = (cls: TaughtClass) => {
+          Object.defineProperty(cls, "module", {
+            get: function () {
+              return (
+                this._module ||
+                (this._module = modulesStore.fromModuleShort(cls.name))
+              );
+            },
+          });
+          cls.isBB = cls.class.match(bbClassRe) !== null;
+          cls.isFirstPhase = cls.class.match(firstPhaseClassRe) !== null;
+          cls.isEnglish = cls.class.match(englishClassRe) !== null;
+          cls.isContext = cls.class.match(contextClassRe) !== null;
+          cls.languageString = cls.isEnglish
+            ? ModuleLanguages.EN
+            : ModuleLanguages.DE;
+          cls.executionTime =
+            cls.weekday !== null
+              ? `${dayMap[cls.weekday]}, ${toTime(cls.from)}-${toTime(cls.to)}`
+              : "Blockmodul";
+        };
+
+        /**
+         * Process the block modules
+         */
+        const bmReady = new Promise((resolveBM) => {
+          Promise.all([bc, modulesStore.ready])
+            .then((response) => response[0].json())
+            .then((data: TaughtClass[]) => {
+              data.forEach(addModuleGetters);
+              this.data[semVer].blockClasses = data;
+              resolveBM(null);
+            })
+            .catch((error) => {
+              console.error(error);
+              this.data[semVer].blockClasses = [];
+              resolveBM(null); // We will resolve and not reject, as it's okay to not have a blockmodules.json on the server
+            });
+        });
+
+        /**
+         * Process the semester config
+         */
+        const scReady = new Promise((resolveSC) => {
+          Promise.all([sc, modulesStore.ready])
+            .then((response) => response[0].json())
+            .then((data: SemesterConfiguration) => {
+              this.data[semVer].config = data;
+              resolveSC(null);
+            })
+            .catch((error) => {
+              console.error(error);
+              this.data[semVer].config = { blockclass_file: "block.pdf" };
+              resolveSC(null); // We will resolve and not reject, as it's okay to not have a blockmodules.json on the server
+            });
+        });
+
+        /**
+         * Process the standard classes
+         */
+        Promise.all([fe, bmReady, scReady, modulesStore.ready])
+          .then((response) => (response[0] as Response).json())
+          .then((data: TaughtClass[]) => {
+            data.forEach(addModuleGetters);
+            this.data[semVer].taughtClasses = data;
+            this.updateUniqueDegreePrograms(semVer);
+
+            this.data[semVer].loaded = true;
+
+            const planningStore = usePlanningStore();
+            planningStore.loadFromStorage();
+            resolve(null);
+          })
+          .catch((error) => {
+            console.error(error);
+            this.data[semVer].taughtClasses = [];
+            reject(null);
+          });
+      });
+
+      return this.data[version].ready ?? new Promise((res, rej) => rej);
+    },
+    updateUniqueDegreePrograms(version: string) {
+      this.data[version].degreePrograms = [];
+
+      this.allClasses.forEach((m) => {
+        if (!this.data[version].degreePrograms.includes(m.degree_prg))
+          this.data[version].degreePrograms.push(m.degree_prg);
+      });
+
+      this.data[version].degreePrograms.sort((a, b) =>
+        a.toLowerCase().localeCompare(b.toLowerCase()),
+      );
+    },
+    getById(module_id: string): TaughtClass | null {
+      const matched = this.allClasses.filter((c) => c.id == module_id);
+
+      if (matched.length == 0) return null;
+      return matched[0];
+    },
+    updateOrdering(col: ClassSelectorColumn, dir: Ordering) {
+      const colName = col.valueOf();
+
+      if (dir == Ordering.None && col in this.sortBy) {
+        delete this.sortBy[col];
+        return;
+      }
+
+      switch (col) {
+        case ClassSelectorColumn.Time:
+          this.sortBy[col] = (a: TaughtClass, b: TaughtClass) => {
+            const ta = 86400 * (a.weekday ?? 0) + a.from;
+            const tb = 86400 * (b.weekday ?? 0) + b.from;
+            return (ta - tb) * dir;
+          };
+          break;
+
+        case ClassSelectorColumn.Lecturer:
+          this.sortBy[col] = (a: TaughtClass, b: TaughtClass) => {
+            const ta = a.teachers.join("").toLowerCase();
+            const tb = b.teachers.join("").toLowerCase();
+            return (ta < tb ? -1 : ta > tb ? 1 : 0) * dir;
+          };
+          break;
+
+        case ClassSelectorColumn.TeachingType:
+          this.sortBy[col] = (a: TaughtClass, b: TaughtClass) => {
+            const ta = a.teaching_type;
+            const tb = b.teaching_type;
+            return (ta < tb ? -1 : ta > tb ? 1 : 0) * dir;
+          };
+          break;
+
+        case ClassSelectorColumn.Room:
+          this.sortBy[col] = (a: TaughtClass, b: TaughtClass) => {
+            const ta = a.rooms.join("").toLowerCase();
+            const tb = b.rooms.join("").toLowerCase();
+            return (ta < tb ? -1 : ta > tb ? 1 : 0) * dir;
+          };
+          break;
+
+        case ClassSelectorColumn.MSP:
+          this.sortBy[col] = (a: TaughtClass, b: TaughtClass) => {
+            const aMSP = a.module?.hasMSP === true;
+            const bMSP = b.module?.hasMSP === true;
+            return (aMSP == bMSP ? 0 : aMSP ? -1 : 1) * dir;
+          };
+          break;
+
+        case ClassSelectorColumn.Module:
+        case ClassSelectorColumn.Class:
+          this.sortBy[col] = (a, b) =>
+            (a[colName] < b[colName] ? -1 : a[colName] > b[colName] ? 1 : 0) *
+            dir;
+          break;
+
+        default:
+          console.error("Unhandled sorting!");
+          break;
+      }
+    },
+    insertFilter(afterFilter: FilterRule | null) {
+      const emptyFilter = {
+        column: ClassSelectorColumn.Module,
+        filterData: { term: "" },
+        pk: Math.floor(Math.random() * 1e20),
+        enabled: true,
+      } as FilterRule;
+
+      if (!afterFilter) {
+        this.filterRules.push(emptyFilter);
+        return;
+      }
+
+      const idx = this.filterRules.findIndex((r) => r.pk == afterFilter.pk);
+      if (idx == -1) {
+        this.filterRules.push(emptyFilter);
+        return;
+      }
+
+      this.filterRules.splice(idx + 1, 0, emptyFilter);
+    },
+    removeFilter(filterRule: FilterRule) {
+      const idx = this.filterRules.findIndex((r) => r.pk == filterRule.pk);
+      if (idx == -1) {
+        console.error("Failed to find the filter rule in the current set!");
+        return;
+      }
+      this.filterRules.splice(idx, 1);
+    },
+    classesForModule(short: string): TaughtClass[] {
+      const cd = this.currentData;
+      if (cd == null) return [];
+
+      return cd.taughtClasses.filter((c) => c.name == short);
+    },
+  },
+});
+
+function filterTimeColumn(
+  modules: TaughtClass[],
+  filterset: FilterRule,
+): TaughtClass[] {
+  const timeData = filterset.filterData as Record<string, number>;
+  const start = timeData.startTime;
+  const end = timeData.endTime;
+  if (start == undefined && end == undefined) return modules;
+  let startingDay = -1;
+
+  if (start != undefined) {
+    if (start < 0) {
+      const pStart = -1 * start;
+      modules = modules.filter((m) => m.from >= pStart);
+    } else {
+      startingDay = Math.floor(start / 86400);
+      const startingTime = start - startingDay * 86400;
+      modules = modules.filter(
+        (m) =>
+          ((m.weekday ?? 0) == startingDay && m.from >= startingTime) ||
+          (m.weekday ?? 0) > startingDay,
+      );
+    }
+  }
+
+  if (end != undefined) {
+    let aEnd = end;
+    if (aEnd < 0) {
+      if (startingDay >= 0) {
+        const endingTime = aEnd * -1 - startingDay * 86400;
+        modules = modules.filter(
+          (m) =>
+            (m.weekday ?? 0) == startingDay &&
+            (endingTime == 0 || m.to <= endingTime),
+        );
+      } else {
+        aEnd *= -1;
+        modules = modules.filter((m) => m.to <= aEnd);
+      }
+    } else {
+      const endingDay = Math.floor(aEnd / 86400);
+      const endingTime = aEnd - endingDay * 86400;
+      modules = modules.filter(
+        (m) =>
+          (m.weekday ?? 0) < endingDay ||
+          ((m.weekday ?? 0) == endingDay &&
+            (endingTime == 0 || m.to <= endingTime)),
+      );
+    }
+  }
+  return modules;
+}

+ 38 - 0
src/stores/config.ts

@@ -0,0 +1,38 @@
+import { defineStore } from "pinia";
+import { Config } from "../types";
+import { useClassVersionStore } from "./ClassVersion";
+
+const defaultConfig = {
+  export_date: "",
+  parse_date: "",
+  pdf_version: "",
+} as Config;
+
+export const useConfigStore = defineStore("config", {
+  state: () => {
+    return {
+      config: defaultConfig as Config,
+      ready: null as Promise<Response> | null,
+    };
+  },
+
+  actions: {
+    fetchData() {
+      const cvStore = useClassVersionStore();
+      const semVerFolder = cvStore.semVerFolder;
+      if (semVerFolder == null) return;
+
+      this.ready = fetch(`${semVerFolder}/config.json`);
+
+      this.ready
+        .then((response) => response.json())
+        .then((data) => {
+          this.config = data;
+        })
+        .catch((error) => {
+          console.error(error);
+          this.config = defaultConfig;
+        });
+    },
+  },
+});

+ 30 - 0
src/stores/lecturers.ts

@@ -0,0 +1,30 @@
+import { defineStore } from "pinia";
+import { Lecturer } from "../types";
+
+export const useLecturersStore = defineStore("lecturers", {
+  state: () => {
+    return {
+      data: [] as Lecturer[],
+    };
+  },
+
+  actions: {
+    fetchData() {
+      fetch("./data/lecturers.json")
+        .then((response) => response.json())
+        .then((data) => {
+          this.data = data;
+        })
+        .catch((error) => {
+          console.error(error);
+          this.data = [];
+        });
+    },
+    fromShort(shorts: string[]): Lecturer[] {
+      const r = this.data.filter((el) => {
+        return shorts.includes(el.short);
+      });
+      return r;
+    },
+  },
+});

+ 168 - 0
src/stores/modules.ts

@@ -0,0 +1,168 @@
+import { defineStore } from "pinia";
+import {
+  dayMap,
+  matchesOneOf,
+  MAX_ATTEMPT_COUNT,
+  MinFullNameSearchLength,
+  toTime,
+} from "../helpers";
+import { Module, ModuleHistory as ModuleHistory } from "../types";
+import { useStudenthubStore } from "./studenthub";
+
+export const useModulesStore = defineStore("modules", {
+  state: () => {
+    return {
+      data: [] as Module[],
+      history: {} as ModuleHistory,
+      ready: null as Promise<Response> | null,
+    };
+  },
+
+  actions: {
+    fetchData() {
+      this.ready = fetch("./data/modules.json");
+      const shStore = useStudenthubStore();
+
+      this.ready
+        .then((response) => response.json())
+        .then((data: Module[]) => {
+          data.forEach((mod) => {
+            Object.defineProperty(mod, "hasMSP", {
+              get: function () {
+                return (
+                  this._hasMSP ||
+                  (this._hasMSP = mod.marks?.includes("MSP") === true)
+                );
+              },
+            });
+            Object.defineProperty(mod, "marksClean", {
+              get: function () {
+                return (
+                  this._marksClean ||
+                  (this._marksClean = [
+                    ...new Set(
+                      mod.marks
+                        ?.map((el) => {
+                          if (el == "MSP") return el;
+                          if (el == "SE" || el == "EN" || el == "SN")
+                            return "EN";
+                          return null;
+                        })
+                        .filter((el) => el),
+                    ),
+                  ].join(", "))
+                );
+              },
+            });
+            Object.defineProperty(mod, "hasCompleted", {
+              get() {
+                if (mod.module_id === null) return false;
+                return (
+                  this._hasCompleted ||
+                  (this._hasCompleted = matchesOneOf(
+                    shStore.completedModuleIds,
+                    mod.module_ids ?? [],
+                  ))
+                );
+              },
+            });
+            Object.defineProperty(mod, "isActive", {
+              get() {
+                if (mod.module_id === null) return false;
+                return (
+                  this._isActive ||
+                  (this._isActive = matchesOneOf(
+                    shStore.activeModuleIds,
+                    mod.module_ids ?? [],
+                  ))
+                );
+              },
+            });
+            Object.defineProperty(mod, "hasFailed", {
+              get() {
+                if (mod.module_id === null) return false;
+                return (
+                  this._hasFailed ||
+                  (this._hasFailed = matchesOneOf(
+                    shStore.failedModuleIds,
+                    mod.module_ids ?? [],
+                  ))
+                );
+              },
+            });
+            Object.defineProperty(mod, "attemptCount", {
+              get() {
+                if (mod.module_id === null) return 0;
+                if (this._attemptCount) return this._attemptCount;
+
+                const attempts = shStore.moduleAttemptCount;
+                this._attemptCount = 0;
+                if (mod.module_id in attempts)
+                  this._attemptCount = attempts[mod.module_id];
+
+                return this._attemptCount;
+              },
+            });
+            Object.defineProperty(mod, "maxAttemptsReached", {
+              get() {
+                return this.attemptCount >= MAX_ATTEMPT_COUNT;
+              },
+            });
+            mod.isContext = mod.cat == "Kontext";
+          });
+          this.data = data;
+        })
+        .catch((error) => {
+          console.error(error);
+          this.data = [];
+        });
+
+      fetch("./data/module-history.json")
+        .then((response) => response.json())
+        .then((data: ModuleHistory) => {
+          Object.values(data).forEach((moduleEntry) => {
+            moduleEntry.forEach((el) => {
+              el.executionTime =
+                el.weekday !== null
+                  ? `${dayMap[el.weekday]}, ${toTime(el.from)}-${toTime(el.to)}`
+                  : "Blockmodul";
+            });
+          });
+          this.history = data;
+        });
+    },
+    fromModuleShort(moduleShort: string): Module | null {
+      const r = this.data.find((el) => {
+        return el.short == moduleShort;
+      });
+      if (r == undefined) return null;
+      return r;
+    },
+    fromModuleId(mid: number): Module | null {
+      const r = this.data.find((el) => {
+        return el.module_id == mid;
+      });
+      if (r == undefined) return null;
+      return r;
+    },
+    getModulesMatching(
+      shortName: string | null | undefined,
+      degree: string | null | undefined,
+    ): Module[] {
+      if (!shortName) return this.data;
+
+      const lowerName = shortName.toLowerCase();
+      const checkName = lowerName.length >= MinFullNameSearchLength;
+      return this.data
+        .filter((m) => {
+          let inc = m.short.toLowerCase().includes(lowerName);
+          if (checkName) inc = inc || m.name.toLowerCase().includes(lowerName);
+          if (degree && degree.length > 0 && m.for_degrees)
+            inc = inc && m.for_degrees.includes(degree);
+
+          return inc;
+        })
+        .sort((a, b) => a.short.localeCompare(b.short));
+    },
+  },
+});

+ 620 - 0
src/stores/planning.ts

@@ -0,0 +1,620 @@
+import { defineStore } from "pinia";
+import { useToast } from "vue-toastification";
+import {
+  CalendarEvent,
+  TaughtClass,
+  Module,
+  PersonalEvent,
+  PlanningDump,
+  PlanEntry,
+} from "../types";
+import { useClassesStore } from "./classes";
+import { useModulesStore } from "./modules";
+import { useClassVersionStore } from "./ClassVersion";
+import { useStateStore } from "./state";
+import {
+  semesterVersionStringC,
+  waitTimeAfterOldPlanReminder,
+  waitTimeAfterUpgradeRequestModal,
+} from "../helpers";
+
+const MAX_MODULE_COUNT = 40;
+const toast = useToast();
+
+function otherPlanModificationToast() {
+  toast.error("Du kannst den Plan eines anderen nicht bearbeiten");
+}
+
+function reloadChosenModules() {
+  const planningStore = usePlanningStore();
+  planningStore.loadChosen();
+}
+
+const DEFAULT_PLAN_NAME = "default";
+
+export const BLOCKCOURSE_DAY_IDX = 6;
+export const usePlanningStore = defineStore("planning", {
+  state: () => {
+    return {
+      usingModulesFromURL: false,
+      chosen: [] as TaughtClass[],
+      personalEvents: [] as PersonalEvent[],
+      hourStart: 8,
+      hourEnd: 22,
+      hourDivisions: 2,
+      cellHeight: 24,
+      get currentPlanName(): string {
+        if (this.__currentPlanName != null) return this.__currentPlanName;
+        let name = localStorage["lastSelectedCalendarSet"];
+        if (!name) {
+          name = DEFAULT_PLAN_NAME;
+          localStorage["lastSelectedCalendarSet"] = DEFAULT_PLAN_NAME;
+        }
+        this.__currentPlanName = name;
+        return name;
+      },
+      set currentPlanName(newName: string) {
+        this.__currentPlanName = newName;
+        localStorage["lastSelectedCalendarSet"] = newName;
+        reloadChosenModules();
+      },
+      __currentPlanName: null as string | null,
+      allPlans: [] as PlanEntry[],
+      saveChanges: true,
+      lastPlanRequestTimestamp: undefined as undefined | number,
+      oldPlanReminderTimestamp: undefined as undefined | number,
+      doNotRequestVersionUpgrade: false,
+    };
+  },
+
+  getters: {
+    eventsByDay(): CalendarEvent[][] {
+      // Prefill with 7, because school is from Mo-Sa (6) + 1 day for the block courses
+      const events = new Array(7).fill(null).map(() => []) as CalendarEvent[][];
+
+      const allPlacable = [...this.chosen, ...this.personalEvents];
+
+      // Sort by longest duration, we want to have them leftmost on the calendar view
+      const ch = allPlacable
+        .filter((el) => el != null)
+        .sort((a, b) => b.to - b.from - (a.to - a.from));
+
+      ch.forEach((m, idx) => {
+        const topOffset =
+          (m.from / 3600 - this.hourStart) *
+          this.cellHeight *
+          this.hourDivisions;
+        const length =
+          ((m.to - m.from) / 3600) * this.cellHeight * this.hourDivisions;
+
+        let overlapCount = 0;
+        let overlapIdx = 0;
+        ch.forEach((om, oidx) => {
+          if (m.weekday != om.weekday || idx == oidx) return;
+
+          if (om.from >= m.from && om.from <= m.to) {
+            overlapCount++;
+            if (idx > oidx) overlapIdx++;
+          } else if (m.from >= om.from && m.from <= om.to) {
+            overlapCount++;
+            if (idx > oidx) overlapIdx++;
+          }
+        });
+
+        const width = 100 / (overlapCount + 1) - overlapIdx;
+        const leftOffset = overlapIdx * width + overlapIdx;
+
+        let tc = null as null | TaughtClass;
+        let pe = null as null | PersonalEvent;
+
+        if ((m as TaughtClass).teachers) tc = m as TaughtClass;
+        else pe = m as PersonalEvent;
+
+        events[m.weekday ?? BLOCKCOURSE_DAY_IDX].push({
+          taughtClass: tc,
+          personalEvent: pe,
+          topOffset: topOffset,
+          leftOffset: leftOffset,
+          length: length,
+          width: width,
+        });
+      });
+
+      return events;
+    },
+    sharedDisplayName(): string | null {
+      if (!this.usingModulesFromURL) return null;
+
+      const pn = this.currentPlanName;
+      if (pn.length == 0 || pn == DEFAULT_PLAN_NAME) return null;
+      return pn;
+    },
+    totalECTSCount(): { ects: number; unsureModules: string[] } {
+      let ects = 0;
+      const unsureModules = [] as string[];
+
+      const countedModules = [] as string[];
+      this.chosen.forEach((c) => {
+        const module = c.module;
+        if (module == null || module.ects == null) {
+          unsureModules.push(c.name);
+          return;
+        }
+
+        const moduleName = module.name;
+        if (countedModules.indexOf(moduleName) == -1) {
+          ects += module.ects;
+          countedModules.push(moduleName);
+        }
+      });
+      this.personalEvents.forEach((pe) => (ects += pe.ects ?? 0));
+
+      return { ects, unsureModules };
+    },
+    modulesWithMSP(): Module[] {
+      const modulesStore = useModulesStore();
+      return this.chosen
+        .map((c) => modulesStore.fromModuleShort(c.name))
+        .filter((m) => m?.hasMSP === true) as Module[];
+    },
+    ectsInBBVTClasses(): { bb: number; vt: number } {
+      const res = { bb: 0, vt: 0 };
+      const seen = { bb: [], vt: [] } as { bb: string[]; vt: string[] };
+
+      this.chosen.forEach((c) => {
+        const moduleName = c.name;
+        const ects = c.module?.ects ?? 0;
+
+        if (c.isBB) {
+          const found = seen.bb.indexOf(moduleName) >= 0;
+          if (found) return;
+          seen.bb.push(moduleName);
+
+          res.bb += ects;
+        } else {
+          const found = seen.vt.indexOf(moduleName) >= 0;
+          if (found) return;
+          seen.vt.push(moduleName);
+
+          res.vt += ects;
+        }
+      });
+      return res;
+    },
+    ectsInEngClasses(): number {
+      const seen = [] as string[];
+      return this.chosen
+        .filter((c) => {
+          const moduleName = c.name;
+          const found = seen.indexOf(moduleName) >= 0;
+          if (!found) seen.push(moduleName);
+          return found;
+        })
+        .reduce((sum, c) => sum + (c.isEnglish ? c.module?.ects ?? 0 : 0), 0);
+    },
+    ectsInContextClasses(): number {
+      const seen = [] as string[];
+      return this.chosen
+        .filter((c) => {
+          const moduleName = c.name;
+          const found = seen.indexOf(moduleName) >= 0;
+          if (!found) seen.push(moduleName);
+          return found;
+        })
+        .reduce((sum, c) => sum + (c.isContext ? c.module?.ects ?? 0 : 0), 0);
+    },
+    ectsWithMSP(): number {
+      const seen = [] as string[];
+      return this.chosen
+        .filter((c) => {
+          const moduleName = c.name;
+          const found = seen.indexOf(moduleName) >= 0;
+          if (!found) seen.push(moduleName);
+          return found;
+        })
+        .reduce(
+          (sum, c) => sum + (c.module?.hasMSP ? c.module?.ects ?? 0 : 0),
+          0,
+        );
+    },
+  },
+
+  actions: {
+    /**
+     * Updates the current plan from using the old ids in the format of
+     * `<className>-<name>-<weekday>-<room1<_room2, ...>>` to
+     * `<className>-<name>-<weekday>-<from_time>-<to_time>`.
+     *
+     * This function and it's calls can be safely (and should be) removed by the end of
+     * 2023! In addition to this code, some further code in `loadChosen` can also be
+     * removed (marked).
+     */
+    checkAndUpdateChosenIDs(modules: string[]): string[] {
+      const newIdRE = /^.*-.*-\d-\d+-\d+$/;
+      const classesStore = useClassesStore();
+      const currentClasses = [
+        ...(classesStore.currentData?.taughtClasses ?? []),
+        ...(classesStore.currentData?.blockClasses ?? []),
+      ];
+
+      return modules.map((el) => {
+        if (newIdRE.test(el)) return el;
+        const split = el.split("-", 4);
+        const cls = split[0];
+        const name = split[1];
+        const weekday = split[2];
+        const rooms = split[3];
+
+        // find the new id
+        const found = currentClasses.filter(
+          (c) =>
+            c.class == cls &&
+            c.name == name &&
+            `${c.weekday}` == weekday &&
+            c.rooms.join("_") == rooms,
+        );
+
+        if (found.length == 0) {
+          console.warn("Failed to find a replacement ID!");
+          return el;
+        }
+
+        return found[0].id;
+      });
+    },
+    loadFromStorage() {
+      // Saved plans
+      this.allPlans = [] as PlanEntry[];
+      for (const key in localStorage) {
+        if (key.startsWith("moduleset-")) {
+          const name = key.replace("moduleset-", "");
+          if (name.length > 0) {
+            const data = JSON.parse(
+              localStorage[`moduleset-${name}`],
+            ) as PlanningDump;
+            const version = data.version;
+            this.allPlans.push({ name, planVersion: version });
+          }
+        }
+      }
+
+      if (this.allPlans.length == 0) {
+        this.createPlan(DEFAULT_PLAN_NAME);
+        this.currentPlanName = DEFAULT_PLAN_NAME;
+      }
+
+      // Last selected plan
+      this.loadChosen();
+    },
+    createPlan(name: string): boolean {
+      if (this.allPlans.some((el) => el.name == name)) {
+        toast.warning(
+          `Der Plan '${name}' existiert bereits. Wähle einen anderen Namen.`,
+        );
+        return false;
+      }
+
+      const planVersion =
+        semesterVersionStringC(useClassVersionStore().latestOverall) ?? "";
+      this.allPlans.push({ name, planVersion });
+      this.saveChosen(name, true);
+      return true;
+    },
+    add(m: TaughtClass) {
+      if (this.chosen.includes(m) || this.chosen.length > MAX_MODULE_COUNT)
+        return;
+
+      if (this.usingModulesFromURL) {
+        otherPlanModificationToast();
+        return;
+      }
+
+      this.chosen.push(m);
+      this.saveChosen();
+    },
+    remove(m: TaughtClass) {
+      if (this.usingModulesFromURL) {
+        otherPlanModificationToast();
+        return;
+      }
+
+      const idx = this.chosen.indexOf(m);
+      if (idx == -1) return;
+      this.chosen.splice(idx, 1);
+      this.saveChosen();
+    },
+    removeIdx(idx: number) {
+      if (this.usingModulesFromURL) {
+        otherPlanModificationToast();
+        return;
+      }
+
+      this.chosen.splice(idx, 1);
+      this.saveChosen();
+    },
+    isModuleChosen(m: TaughtClass) {
+      return this.chosen.includes(m);
+    },
+    saveChosen(name: string | null = null, forceEmpty = false) {
+      if (!this.saveChanges) return;
+      if (this.usingModulesFromURL) {
+        console.log("Not saving the current plan, as this is a shared one!");
+        return;
+      }
+      if (name == null) name = this.currentPlanName;
+      localStorage[`moduleset-${name}`] = this.stateToJSON(forceEmpty);
+    },
+    loadChosen(name: string | null = null, useSavedVersion = true): boolean {
+      if (name == null) name = this.currentPlanName;
+      const params = window.location.href
+        .substr(window.location.href.indexOf("?"))
+        .replace("+", "%2B");
+      const param = new URLSearchParams(params);
+
+      let strData;
+      const stateParam = param.get("state");
+      if (stateParam) {
+        const state = stateParam.replace("%2B", "+");
+        strData = decodeURIComponent(escape(atob(state)));
+        this.usingModulesFromURL = true;
+      } else {
+        strData = localStorage[`moduleset-${name}`];
+        if (strData == null) return false;
+        this.usingModulesFromURL = false;
+      }
+
+      const classVersionStore = useClassVersionStore();
+      const modulesStore = useClassesStore();
+      const data = JSON.parse(strData) as PlanningDump;
+
+      classVersionStore.useFromPDFString(
+        useSavedVersion
+          ? data.version
+          : useClassVersionStore().semVer ?? data.version,
+      );
+      modulesStore.fetchData();
+
+      if (!modulesStore.currentData?.loaded) return false;
+
+      this.doNotRequestVersionUpgrade =
+        data.doNotRequestVersionUpgrade ?? false;
+
+      let shouldSave = false;
+      const cTimestamp = new Date().getTime();
+
+      if (!this.doNotRequestVersionUpgrade) {
+        this.lastPlanRequestTimestamp = data.lastPlanRequestTimestamp;
+        if (
+          !classVersionStore.isLatestSemesterVersion &&
+          (this.lastPlanRequestTimestamp == undefined ||
+            cTimestamp - this.lastPlanRequestTimestamp >
+              waitTimeAfterUpgradeRequestModal)
+        ) {
+          const stateStore = useStateStore();
+          stateStore.showingClassUpgradeModal = true;
+          this.lastPlanRequestTimestamp = cTimestamp;
+          shouldSave = true;
+        }
+      }
+
+      this.oldPlanReminderTimestamp = data.oldPlanReminderTimestamp;
+      if (
+        !classVersionStore.isLatestSemester &&
+        (this.oldPlanReminderTimestamp == undefined ||
+          cTimestamp - this.oldPlanReminderTimestamp >
+            waitTimeAfterOldPlanReminder)
+      ) {
+        const stateStore = useStateStore();
+        stateStore.showingOldPlanReminderModal = true;
+        this.oldPlanReminderTimestamp = cTimestamp;
+        shouldSave = true;
+      }
+
+      /**
+       * The following code can be removed at the end of 2023!
+       */
+      const updatedIDs = this.checkAndUpdateChosenIDs(data.modules);
+      this.chosen = updatedIDs
+        .map((el) => modulesStore.getById(el))
+        .filter((el) => el) as TaughtClass[];
+      /**
+       * End code that can be removed at the end of 2023!
+       */
+
+      if (this.usingModulesFromURL) this.__currentPlanName = data.name ?? "";
+
+      if (data.personalEvents) this.personalEvents = data.personalEvents;
+      else this.personalEvents = [];
+
+      /**
+       * The following code can be removed at the end of 2023!
+       */
+      shouldSave ||= !(
+        updatedIDs.length == data.modules.length &&
+        updatedIDs.every(function (element, index) {
+          return element === data.modules[index];
+        })
+      );
+      /**
+       * End code that can be removed at the end of 2023!
+       */
+
+      if (shouldSave) {
+        console.log("Saving due to ...");
+        this.saveChosen(name);
+      }
+
+      return true;
+    },
+    copySharedTo(name: string) {
+      const params = window.location.href
+        .substr(window.location.href.indexOf("?"))
+        .replace("+", "%2B");
+      const param = new URLSearchParams(params);
+      const stateParam = param.get("state");
+      if (!stateParam) return;
+
+      const state = stateParam.replace("%2B", "+");
+      const strData = decodeURIComponent(escape(atob(state)));
+      localStorage[`moduleset-${name}`] = strData;
+
+      window.history.pushState(null, "", window.location.href.split("?")[0]);
+      this.currentPlanName = name;
+
+      this.loadChosen(name);
+    },
+    deleteChosen(name: string | null = null): boolean {
+      if (name == null) name = this.currentPlanName;
+      const pIdx = this.allPlans.findIndex((el) => el.name == name);
+      if (pIdx == -1) return false;
+
+      this.allPlans.splice(pIdx, 1);
+      localStorage.removeItem(`moduleset-${name}`);
+
+      if (this.allPlans.length == 0) {
+        const planVersion =
+          semesterVersionStringC(useClassVersionStore().latestOverall) ?? "";
+        this.allPlans.push({ name: DEFAULT_PLAN_NAME, planVersion });
+        this.currentPlanName = DEFAULT_PLAN_NAME;
+        this.chosen = [];
+        this.saveChosen();
+        this.loadChosen();
+        return true;
+      }
+
+      if (this.currentPlanName == name)
+        this.currentPlanName = this.allPlans[0].name;
+
+      this.loadChosen();
+      return true;
+    },
+    emptyPlan() {
+      this.chosen = [];
+      this.saveChosen();
+    },
+    copyPlan(
+      copyName: string | null = null,
+      copyFrom: string | null = null,
+    ): string | null {
+      if (copyFrom == null) copyFrom = this.currentPlanName;
+
+      if (copyName == null) {
+        let counter = 2;
+        copyName = `Kopie von ${copyFrom}`;
+        while (this.allPlans.findIndex((el) => el.name == copyName) >= 0) {
+          copyName = `Kopie ${counter} von ${copyFrom}`;
+          counter++;
+        }
+      } else {
+        if (this.allPlans.findIndex((el) => el.name == copyName) >= 0) {
+          // Key already exists
+          toast.error(`Der Name '${copyName}' ist bereits vergeben!`);
+          return null;
+        }
+      }
+
+      if (localStorage[`moduleset-${copyFrom}`] == null) {
+        // copyFrom name does not exist!
+        toast.error(
+          "Die zu kopierende Modulauswahl wurde nicht im Speicher gefunden!",
+        );
+        return null;
+      }
+
+      localStorage[`moduleset-${copyName}`] =
+        localStorage[`moduleset-${copyFrom}`];
+
+      const copyData = JSON.parse(
+        localStorage[`moduleset-${copyName}`],
+      ) as PlanningDump;
+
+      this.allPlans.push({
+        name: copyName,
+        planVersion: copyData.version,
+      });
+
+      this.currentPlanName = copyName;
+      return copyName;
+    },
+    renamePlan(newName: string, current: string | null = null): boolean {
+      if (current == null) current = this.currentPlanName;
+      if (current == newName) return true;
+
+      if (localStorage[`moduleset-${newName}`] != null) {
+        // Key already exists
+        toast.error(`Der Name '${newName}' ist bereits vergeben!`);
+        return false;
+      }
+
+      if (localStorage[`moduleset-${current}`] == null) {
+        // Current name does not exist!
+        toast.error(
+          "Die zu umzubenennende Modulauswahl wurde nicht im Speicher gefunden!",
+        );
+        return false;
+      }
+
+      localStorage[`moduleset-${newName}`] =
+        localStorage[`moduleset-${current}`];
+      localStorage.removeItem(`moduleset-${current}`);
+      this.currentPlanName = newName;
+
+      const pIdx = this.allPlans.findIndex((el) => el.name == current);
+      if (pIdx >= 0) {
+        const planVersion = useClassVersionStore().semVer ?? "";
+        this.allPlans.splice(pIdx, 1, { name: newName, planVersion });
+      }
+
+      return true;
+    },
+    stateToJSON(forceEmpty = false, includeName = false): string {
+      let modules = [] as string[];
+      if (!forceEmpty) modules = this.chosen.map((m) => m.id);
+
+      const data = {
+        version: useClassVersionStore().semVer,
+        modules: modules,
+        personalEvents: this.personalEvents,
+        lastPlanRequestTimestamp: this.lastPlanRequestTimestamp,
+        oldPlanReminderTimestamp: this.oldPlanReminderTimestamp,
+        doNotRequestVersionUpgrade: this.doNotRequestVersionUpgrade,
+      } as PlanningDump;
+
+      if (includeName) data.name = this.currentPlanName;
+      return JSON.stringify(data);
+    },
+    getShareURL(): string {
+      const state = this.stateToJSON(false, true);
+      const encodedState = btoa(unescape(encodeURIComponent(state)));
+      return `${window.location.href}?state=${encodedState}`;
+    },
+    removePersonalEvent(event: PersonalEvent | null): boolean {
+      if (event == null) return false;
+      const idx = this.personalEvents.indexOf(event);
+      if (idx == -1) return false;
+      this.personalEvents.splice(idx, 1);
+      this.saveChosen();
+
+      return true;
+    },
+    addPersonalEvent(event: PersonalEvent) {
+      this.personalEvents.push(event);
+      this.saveChosen();
+    },
+    updatePersonalEvent(
+      oldEvent: PersonalEvent,
+      newEvent: PersonalEvent,
+    ): boolean {
+      const idx = this.personalEvents.indexOf(oldEvent);
+      if (idx == -1) return false;
+      this.personalEvents.splice(idx, 1, newEvent);
+      this.saveChosen();
+
+      return true;
+    },
+    noLongerRequestVersionUpgrade() {
+      this.doNotRequestVersionUpgrade = true;
+      this.saveChosen();
+    },
+  },
+});

+ 85 - 0
src/stores/state.ts

@@ -0,0 +1,85 @@
+import { defineStore } from "pinia";
+import {
+  TaughtClass,
+  Module,
+  MainView,
+  ColourScheme,
+  PersonalEvent,
+} from "../types";
+import { useStudenthubStore } from "./studenthub";
+
+const SELECTED_DEGREE_PROGRAM_KEY = "selectedDegreeProgram";
+const HIDE_COMPLETED_CLASSES_KEY = "hideCompletedClasses";
+
+window
+  .matchMedia("(prefers-color-scheme: dark)")
+  .addEventListener("change", (event) => {
+    const stateStore = useStateStore();
+    stateStore.colourScheme = event.matches
+      ? ColourScheme.Dark
+      : ColourScheme.Blinding;
+  });
+
+export const useStateStore = defineStore("state", {
+  state: () => {
+    const colourScheme =
+      window.matchMedia &&
+      window.matchMedia("(prefers-color-scheme: dark)").matches
+        ? ColourScheme.Dark
+        : ColourScheme.Blinding;
+    return {
+      currentView: MainView.ModulePlanner,
+      colourScheme: colourScheme,
+      inspectingClass: null as TaughtClass | null,
+      inspectingModule: null as Module | null,
+      inspectingPersonalEvent: null as boolean | null | PersonalEvent,
+      showingSettings: false,
+      showingModuleSearch: false,
+      showingClassUpgradeModal: false,
+      showingOldPlanReminderModal: false,
+      upgradingClassVersion: false,
+      lastSelectedDegreeProgram:
+        localStorage[SELECTED_DEGREE_PROGRAM_KEY] ?? "",
+      __hideCompletedClasses: null as boolean | null,
+      highlightBBModules: false,
+      highlightVTModules: false,
+      highlightFirstPhaseModules: false,
+      highlightEnglishModules: false,
+      highlightMSPModules: false,
+      highlightContextModules: false,
+      hasShownHelpWantedModal: false,
+    };
+  },
+
+  getters: {
+    hideCompletedClasses(): boolean {
+      const studenthubStore = useStudenthubStore();
+      if (!studenthubStore.hasAllStudenthubData) return false;
+      if (this.__hideCompletedClasses != null)
+        return this.__hideCompletedClasses;
+      this.__hideCompletedClasses =
+        localStorage[HIDE_COMPLETED_CLASSES_KEY] == "true";
+      return this.__hideCompletedClasses;
+    },
+    hasHighlightedModules(): boolean {
+      return (
+        this.highlightBBModules ||
+        this.highlightVTModules ||
+        this.highlightFirstPhaseModules ||
+        this.highlightEnglishModules ||
+        this.highlightMSPModules ||
+        this.highlightContextModules
+      );
+    },
+  },
+
+  actions: {
+    updateLastSelectedDegreeProgram(prg: string) {
+      localStorage[SELECTED_DEGREE_PROGRAM_KEY] = prg;
+    },
+    updateHideCompletedClasses(state: boolean) {
+      localStorage[HIDE_COMPLETED_CLASSES_KEY] = state ? "true" : "false";
+      this.__hideCompletedClasses = state;
+    },
+  },
+});

+ 126 - 0
src/stores/studenthub.ts

@@ -0,0 +1,126 @@
+import { defineStore } from "pinia";
+import { StudenthubAnmeldung, StudenthubStudent } from "../types";
+
+const STUDENTHUB_STUDENT_DATA = "studenthubStudentData";
+const STUDENTHUB_APPLICATIONS_DATA = "studenthubApplicationsData";
+
+export const useStudenthubStore = defineStore("studenthub", {
+  state: () => {
+    return {
+      student: null as StudenthubStudent | null,
+      applications: null as StudenthubAnmeldung[] | null,
+    };
+  },
+
+  getters: {
+    hasSomeStudenthubData(): boolean {
+      return (
+        (this.student != null && this.student.studierendeId != null) ||
+        (this.applications != null && this.applications?.length != null)
+      );
+    },
+    hasAllStudenthubData(): boolean {
+      return (
+        this.student != null &&
+        this.student.studierendeId != null &&
+        this.applications != null &&
+        this.applications?.length != null
+      );
+    },
+    passedModuleIds(): number[] {
+      if (this.applications === null) return [];
+      return this.applications
+        .filter((a) => a.bestanden && a.modulanlass?.nummer.endsWith(".HN"))
+        .map((a) => a.modulanlass?.modulId ?? -1);
+    },
+    creditedModuleIds(): number[] {
+      if (this.student === null) return [];
+      return this.student.modulAnrechnungen?.map((m) => m.modulId) ?? [];
+    },
+    completedModuleIds(): number[] {
+      return [...this.passedModuleIds, ...this.creditedModuleIds];
+    },
+    activeModuleIds(): number[] {
+      if (this.applications === null) return [];
+      return this.applications
+        .filter(
+          (a) =>
+            !a.bestanden &&
+            a.modulanlass &&
+            a.statusName == "aA.Angemeldet" &&
+            a.modulanlass.nummer.endsWith(".HN"),
+        )
+        .map((a) => a.modulanlass?.modulId ?? -1);
+    },
+    failedModuleIds(): number[] {
+      if (this.applications === null) return [];
+      return this.applications
+        .filter(
+          (a) =>
+            !a.bestanden &&
+            a.modulanlass &&
+            a.statusName == "aA.Nicht erfolgreich teilgenommen" &&
+            a.modulanlass.nummer.endsWith(".HN"),
+        )
+        .map((a) => a.modulanlass?.modulId ?? -1);
+    },
+    moduleAttemptCount(): Record<number, number> {
+      if (this.applications === null) return [];
+      const attempts = {} as Record<number, number>;
+      this.applications
+        .filter((a) => a.modulanlass && a.modulanlass.nummer.endsWith(".HN"))
+        .forEach((a) => {
+          const mid = a.modulanlass?.modulId;
+          if (!mid) return;
+
+          if (mid in attempts) attempts[mid]++;
+          else attempts[mid] = 1;
+        });
+      return attempts;
+    },
+  },
+
+  actions: {
+    loadData() {
+      try {
+        this.student = JSON.parse(localStorage[STUDENTHUB_STUDENT_DATA]);
+      } catch (e) {
+        console.info("Failed to parse the studenthub student data!");
+        console.error(e);
+      }
+
+      try {
+        this.applications = JSON.parse(
+          localStorage[STUDENTHUB_APPLICATIONS_DATA],
+        );
+      } catch (e) {
+        console.info("Failed to parse the studenthub applications data!");
+        console.error(e);
+      }
+    },
+    setStudentData(stud: StudenthubStudent | null) {
+      if (stud === null) {
+        localStorage[STUDENTHUB_STUDENT_DATA] = null;
+        this.student = null;
+        return;
+      }
+
+      localStorage[STUDENTHUB_STUDENT_DATA] = JSON.stringify(stud);
+      this.student = stud;
+    },
+    setApplicationsData(application: StudenthubAnmeldung[] | null) {
+      if (application === null) {
+        localStorage[STUDENTHUB_APPLICATIONS_DATA] = null;
+        this.applications = null;
+        return;
+      }
+
+      localStorage[STUDENTHUB_APPLICATIONS_DATA] = JSON.stringify(application);
+      this.applications = application;
+    },
+    hasCompletedModule(moduleID: number | null) {
+      if (moduleID === null) return false;
+      return this.completedModuleIds.find((m) => m == moduleID) !== undefined;
+    },
+  },
+});

+ 145 - 0
src/stores/upgrade.ts

@@ -0,0 +1,145 @@
+import { defineStore } from "pinia";
+import { useToast } from "vue-toastification";
+import { useClassesStore } from "./classes";
+import {
+  ClassUpgradeDifference,
+  ClassUpgradeMinorDifferences,
+  ClassUpgradeMinorDifferencesSet,
+} from "../types";
+import { semesterVersionString } from "../helpers";
+import { usePlanningStore } from "./planning";
+import { useClassVersionStore } from "./ClassVersion";
+
+const toast = useToast();
+
+export const useUpgradeStore = defineStore("upgrade", {
+  state: () => {
+    return {};
+  },
+
+  getters: {},
+
+  actions: {
+    reset() {
+      usePlanningStore().saveChanges = true;
+    },
+    finalize() {
+      usePlanningStore().saveChanges = true;
+    },
+    startUpgrade() {
+      usePlanningStore().saveChanges = false;
+    },
+    getChangesToPlan(
+      semester: string,
+      version: string,
+    ): Promise<ClassUpgradeDifference> | null {
+      const classesStore = useClassesStore();
+      const classesVersionStore = useClassVersionStore();
+      const planningStore = usePlanningStore();
+
+      planningStore.loadChosen();
+
+      const newSemVer = semesterVersionString(semester, version);
+      const currSemVer = classesVersionStore.semVer;
+
+      if (currSemVer == null) {
+        console.error(
+          "The current planning store does not have a version set!",
+        );
+        return null;
+      }
+
+      const prom = classesStore.fetchData(newSemVer);
+
+      if (prom == null) {
+        toast.error("Konnte den neuen Stundenplan nicht laden.");
+        console.error("Failed to fetch the other timetable from the server!");
+        return null;
+      }
+
+      return new Promise((resolve, reject) => {
+        Promise.all([classesStore.currentData?.ready, prom])
+          .then(() => {
+            const newClass = classesStore.data[newSemVer];
+
+            const allNewClasses = [
+              ...newClass.taughtClasses,
+              ...newClass.blockClasses,
+            ];
+            const oldChosen = [...planningStore.chosen];
+            classesVersionStore.useSemesterVersion(semester, version);
+
+            // Find any removed classes
+            const removed = oldChosen.filter((el) => {
+              const found = allNewClasses.filter((c) => c.id == el.id);
+              if (found.length == 0) return true;
+              return false;
+            });
+
+            const minorChanges = oldChosen
+              .map((el) => {
+                if (removed.includes(el)) return null;
+
+                const newCls = allNewClasses.filter((c) => c.id == el.id);
+                const changes = [] as ClassUpgradeMinorDifferences[];
+                const c = newCls[0];
+
+                if (c.class !== el.class)
+                  changes.push({
+                    difference: "Klasse",
+                    oldValue: el.class,
+                    newValue: c.class,
+                  });
+
+                if (c.languageString !== el.languageString)
+                  changes.push({
+                    difference: "Sprache",
+                    oldValue: el.languageString,
+                    newValue: c.languageString,
+                  });
+
+                if (c.teaching_type !== el.teaching_type)
+                  changes.push({
+                    difference: "Unterrichtsart",
+                    oldValue: el.teaching_type,
+                    newValue: c.teaching_type,
+                  });
+
+                if (c.rooms.join(", ") !== el.rooms.join(", "))
+                  changes.push({
+                    difference: "Zimmer",
+                    oldValue: el.rooms.join(", "),
+                    newValue: c.rooms.join(", "),
+                  });
+
+                if (c.teachers.join(", ") !== el.teachers.join(", "))
+                  changes.push({
+                    difference: "Dozenten",
+                    oldValue: el.teachers.join(", "),
+                    newValue: c.teachers.join(", "),
+                  });
+
+                if (changes.length == 0) return null;
+
+                return {
+                  name: el.name,
+                  changes: changes,
+                };
+              })
+              .filter((el) => el) as ClassUpgradeMinorDifferencesSet[];
+
+            resolve({
+              oldVersion: currSemVer.replace("|", " / "),
+              newVersion: newSemVer.replace("|", " / "),
+              removals: removed,
+              minorChanges: minorChanges,
+            });
+          })
+          .catch(() => {
+            console.error("Failed to load both PDF versions.");
+            reject();
+          });
+      });
+    },
+  },
+});

+ 106 - 0
src/style.css

@@ -0,0 +1,106 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+@layer components {
+  input {
+    @apply block
+      w-full
+      px-4
+      py-1.5
+      text-base
+      font-normal
+      text-gray-700
+     dark:text-white
+      bg-white dark:bg-zinc-900
+      dark:border-zinc-600
+      bg-clip-padding
+      border border-solid border-gray-300
+      rounded-lg
+      transition
+      ease-in-out
+      m-0
+      focus:bg-white
+      focus:dark:bg-zinc-800
+      focus:border-blue-600
+      focus:outline-none;
+  }
+
+  select {
+    @apply block
+      w-full
+      px-3
+      py-1.5
+      text-base
+      font-normal
+      text-gray-700
+      dark:text-white
+      bg-white dark:bg-zinc-900
+      dark:border-zinc-600
+      bg-clip-padding bg-no-repeat
+      border border-solid border-gray-300
+      rounded-lg
+      transition
+      ease-in-out
+      m-0
+      focus:bg-white
+      focus:dark:bg-zinc-800
+      focus:border-blue-600
+      focus:outline-none;
+  }
+
+  .external-link {
+    @apply text-blue-600
+      hover:text-blue-800
+      active:text-blue-900
+      dark:text-blue-400
+      dark:hover:text-blue-300
+      dark:active:text-blue-200
+      cursor-pointer
+      transition-all
+      duration-200;
+  }
+}
+
+:root {
+  --popper-theme-background-color: #eee;
+  --popper-theme-background-color-hover: #eee;
+  --popper-theme-text-color: #000000;
+  --popper-theme-border-radius: 6px;
+  --popper-theme-padding: 15px;
+  --popper-theme-box-shadow: 0 6px 30px -6px rgba(0, 0, 0, 0.5);
+}
+
+@media (prefers-color-scheme: dark) {
+  :root {
+    --popper-theme-background-color: #333333;
+    --popper-theme-background-color-hover: #333333;
+    --popper-theme-text-color: #ffffff;
+    --popper-theme-border-radius: 6px;
+    --popper-theme-padding: 15px;
+    --popper-theme-box-shadow: 0 6px 30px -6px rgba(0, 0, 0, 0.25);
+  }
+}
+
+.visible-in-print {
+  display: none;
+}
+
+@media print {
+  #app {
+    color: black !important;
+    margin: 0;
+    zoom: 75%;
+  }
+
+  #app .single-event p {
+    color: black !important;
+  }
+
+  .hidden-in-print {
+    display: none;
+  }
+  .visible-in-print {
+    display: inherit;
+  }
+}

+ 302 - 0
src/types.ts

@@ -0,0 +1,302 @@
+export type TaughtClass = {
+  id: string;
+  from: number;
+  to: number;
+  weekday: number | null;
+  class: string;
+  name: string;
+  rooms: string[];
+  teachers: string[];
+  teaching_type: TeachingType;
+  pages: number[];
+  degree_prg: string;
+  part_of_other_classes: string[];
+  module: Module | null;
+  isBB: boolean; // Berufsbegleitend, otherwise Full/Part time
+  isFirstPhase: boolean;
+  isEnglish: boolean;
+  isContext: boolean;
+  languageString: string;
+  executionTime: string;
+};
+
+export type Module = {
+  short: string;
+  name: string;
+  marks: string[] | null;
+  studiengang_id: number | null;
+  for_degrees: string | null;
+  module_id: number | null;
+  module_ids: number[] | null;
+  cat: string | null;
+  sub_cat: string | null;
+  ects: number | null;
+  full: string | null;
+  dependencies: Record<string, string[]>;
+  enablingModules: Record<string, string[]>;
+  hasMSP: boolean;
+  marksClean: string;
+  hasCompleted: boolean;
+  isActive: boolean;
+  hasFailed: boolean;
+  attemptCount: number;
+  maxAttemptsReached: boolean;
+  isContext: boolean;
+};
+
+export type HistoricClassEntry = {
+  semester: string;
+  class: string;
+  version: string;
+  lecturers: string[];
+  teaching_type: TeachingType;
+  rooms: string[];
+  from: number;
+  to: number;
+  id: string;
+  weekday: number;
+  pages: number[];
+  executionTime: string;
+};
+
+export type ModuleHistory = Record<string, HistoricClassEntry[]>;
+
+export type Lecturer = {
+  short: string;
+  surname: string;
+  firstname: string;
+};
+
+export type NameModule = {
+  name: string;
+  modules: TaughtClass[];
+};
+
+export type NameModuleNumber = {
+  name: number;
+  modules: TaughtClass[];
+};
+
+export type CalendarEvent = {
+  taughtClass: TaughtClass | null;
+  personalEvent: PersonalEvent | null;
+  topOffset: number;
+  leftOffset: number;
+  length: number;
+  width: number;
+};
+
+export type Config = {
+  export_date: string;
+  parse_date: string;
+  pdf_version: string;
+};
+
+export type StudienjahrgangAnmeldung = {
+  studienjahrgangAnmeldungId: number;
+  studienjahrgangId: number;
+  studierendeId: number;
+  statusId: number;
+  statusName: string;
+  anmeldungsDatum: string;
+  hochschule: string;
+  ausbildungsart: number;
+};
+
+export type ModulAnrechnung = {
+  modulAnrechnungId: number;
+  studierendeId: number;
+  modulId: number;
+  anrechnungEcts: number;
+};
+
+export type PauschalAnrechnung = {
+  pauschalAnrechnungId: number;
+  modulStandardGruppeId: number;
+  studierendeId: number;
+  studiengangId: number;
+  anrechnungEcts: number;
+  zeugnisText: string;
+};
+
+export type StudenthubStudent = {
+  studierendeId: number | null;
+  anrede: string | null;
+  nachname: string | null;
+  vorname: string | null;
+  adresse1: string | null;
+  adresse2: string | null;
+  plz: string | null;
+  ort: string | null;
+  land: string | null;
+  emailAdmin: string | null;
+  emailEducation: string | null;
+  emailPrivat: string | null;
+  telefon: string | null;
+  mobile: string | null;
+  bild: string | null;
+  geburtsdatum: string | null;
+  beruf: string | null;
+  studienjahrgangAnmeldungen: StudienjahrgangAnmeldung[] | null;
+  modulAnrechnungen: ModulAnrechnung[] | null;
+  pauschalAnrechnungen: PauschalAnrechnung[] | null;
+};
+
+export type Modulanlass = {
+  modulanlassId: number;
+  modulId: number;
+  nummer: string;
+  bezeichnung: string;
+  statusId: number;
+  statusName: string;
+  hochschule: string;
+  ausbildungsart: number;
+  maxTeilnehmende: number;
+  datumVon: string;
+  datumBis: string;
+  wochentag: null;
+  zeitVon: null;
+  zeitBis: null;
+  anzahlAnmeldung: number;
+  anlassleitungen: object[];
+};
+
+export type StudenthubAnmeldung = {
+  modulanlassAnmeldungId: number | null;
+  modulanlassId: number | null;
+  studierendeId: number | null;
+  statusId: number | null;
+  statusName: string | null;
+  freieNote: number | null;
+  noteId: null | null;
+  titelArbeit: null | null;
+  text: string | null;
+  anmeldungsDatum: string | null;
+  hochschule: string | null;
+  ausbildungsart: number | null;
+  modulanlass: Modulanlass | null;
+  bestanden: boolean | null;
+  noteBezeichnung: string | null;
+};
+
+export type FilterRule = {
+  column: ClassSelectorColumn;
+  filterData: {
+    term?: string;
+    degree?: string;
+    startTime?: number;
+    endTime?: number;
+    teachingType?: TeachingType | "";
+  };
+  pk: number;
+  enabled: boolean;
+};
+
+export type SemesterVersion = {
+  semester: string;
+  versions: string[];
+};
+
+export type TimetableChangelogEntry = {
+  name: string;
+  changes: string;
+};
+
+export type PersonalEvent = {
+  name: string;
+  weekday: number;
+  from: number;
+  ects?: number;
+  to: number;
+  id: string;
+};
+
+export type PlanningDump = {
+  modules: string[];
+  version: string;
+  name?: string;
+  personalEvents?: PersonalEvent[];
+  lastPlanRequestTimestamp?: number;
+  oldPlanReminderTimestamp?: number;
+  doNotRequestVersionUpgrade?: boolean;
+};
+
+export type PlanEntry = {
+  name: string;
+  planVersion: string;
+};
+
+export type ClassesVersion = {
+  version: string;
+  ready: Promise<null> | null;
+  loaded: boolean;
+  teachers: NameModule[];
+  rooms: NameModule[];
+  classes: NameModule[];
+  days: NameModuleNumber[];
+  taughtClasses: TaughtClass[];
+  blockClasses: TaughtClass[];
+  degreePrograms: string[];
+  config: SemesterConfiguration;
+};
+
+export type SemesterConfiguration = {
+  blockclass_file: string;
+};
+
+export type ClassUpgradeDifference = {
+  oldVersion: string;
+  newVersion: string;
+  removals: TaughtClass[];
+  minorChanges: ClassUpgradeMinorDifferencesSet[];
+};
+
+export type ClassUpgradeMinorDifferencesSet = {
+  name: string;
+  changes: ClassUpgradeMinorDifferences[];
+};
+
+export type ClassUpgradeMinorDifferences = {
+  difference: string;
+  oldValue: string;
+  newValue: string;
+};
+
+export enum Ordering {
+  None = 0,
+  Asc = 1,
+  Desc = -1,
+}
+
+export enum TeachingType {
+  OnSite = "on_site",
+  Hybrid = "hybrid",
+  Online = "online",
+  Blockmodule = "blockmodule",
+}
+
+export enum ClassSelectorColumn {
+  Module = "name",
+  Time = "weekday",
+  Class = "class",
+  Lecturer = "lecturer",
+  Room = "room",
+  TeachingType = "teaching_type",
+  MSP = "msp",
+  Degree = "degree",
+}
+
+export enum ModuleLanguages {
+  EN = "en",
+  DE = "de",
+}
+
+export enum MainView {
+  ModulePlanner = "ModulePlanner",
+  DependencyTree = "DependencyTree",
+}
+
+export enum ColourScheme {
+  Dark = "dark",
+  Blinding = "blinding",
+}

+ 1 - 0
src/vite-env.d.ts

@@ -0,0 +1 @@
+/// <reference types="vite/client" />

+ 17 - 0
tailwind.config.cjs

@@ -0,0 +1,17 @@
+/** @type {import('tailwindcss').Config} */
+module.exports = {
+  content: ["./index.html", "./src/**/*.{vue,js,ts,jsx,tsx}"],
+  theme: {
+    extend: {
+      width: {
+        128: "32rem",
+        140: "35rem",
+        160: "40rem",
+      },
+      fontSize: {
+        "20xl": "20em",
+      },
+    },
+  },
+  plugins: [],
+};

+ 18 - 0
tsconfig.json

@@ -0,0 +1,18 @@
+{
+  "compilerOptions": {
+    "target": "ESNext",
+    "useDefineForClassFields": true,
+    "module": "ESNext",
+    "moduleResolution": "Node",
+    "strict": true,
+    "jsx": "preserve",
+    "resolveJsonModule": true,
+    "isolatedModules": true,
+    "esModuleInterop": true,
+    "lib": ["ESNext", "DOM"],
+    "skipLibCheck": true,
+    "noEmit": true
+  },
+  "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
+  "references": [{ "path": "./tsconfig.node.json" }]
+}

+ 9 - 0
tsconfig.node.json

@@ -0,0 +1,9 @@
+{
+  "compilerOptions": {
+    "composite": true,
+    "module": "ESNext",
+    "moduleResolution": "Node",
+    "allowSyntheticDefaultImports": true
+  },
+  "include": ["vite.config.ts"]
+}

+ 18 - 0
vite.config.ts

@@ -0,0 +1,18 @@
+import { defineConfig } from "vite";
+import vue from "@vitejs/plugin-vue";
+
+// https://vitejs.dev/config/
+export default defineConfig({
+  plugins: [vue()],
+  build: {
+    chunkSizeWarningLimit: 1400,
+    rollupOptions: {
+      output: {
+        manualChunks: {
+          leaflet: ["leaflet"],
+          mermaid: ["mermaid"],
+        },
+      },
+    },
+  },
+});

Неке датотеке нису приказане због велике количине промена