Repository: jupyter/nbgrader (language: python)

Issue:
Port the notebook extensions to JupyterLab
This is so we can discuss whether this is the right path to follow for nbgrader, I'm not 100% sure yet but at least we can come to a decision here...

We're still using nbgrader at Cal quite a bit, and as I consider moving the students more to JupyterLab as their "daily driver", the absence of the student-facing extension for submitting notebooks is becoming a more relevant issue.

Curious what @jhamrick @ellisonbg think in this context - how realistic would it be to put some cycles into porting the extension?  This is more of an nbgrader than a Lab issue, but it's turning out to be kind of a blocker to our use of Lab by default (which I *really* want to do :).

CC @jasongrout @ian-r-rose as per recent discussions on blockers and extensions...
We're creating a maintainer team! Join us :D
Hi all,

We're hoping some of our long-time users or enthusiasts for nbgrader would like to be part of a team of individuals maintaining nbgrader.

We've structured JupyterHub and Binder in a similar way. See https://jupyterhub-team-compass.readthedocs.io/en/latest/team.html#jupyterhub-team 

Please leave a comment on this issue. If you have questions, please leave them here too.


Discussion / hints:
ps - forgive me if it's been discussed here already, I did a search and didn't find it but feel free to point me to the right location.
Fernando, I have never used the student facing extension. Can you describe
exactly what features you are wanting for them? Would be great to help you
be able to switch to lab...

On Wed, Aug 29, 2018 at 6:27 PM Fernando Perez <notifications@github.com>
wrote:

> ps - forgive me if it's been discussed here already, I did a search and
> didn't find it but feel free to point me to the right location.
>
> —
> You are receiving this because you were mentioned.
> Reply to this email directly, view it on GitHub
> <https://github.com/jupyter/nbgrader/issues/1006#issuecomment-417159499>,
> or mute the thread
> <https://github.com/notifications/unsubscribe-auth/AABr0NiNEmc1IG3BDiaWSmK2LRiAARQzks5uVz9kgaJpZM4WSnqV>
> .
>


-- 
Brian E. Granger
Associate Professor of Physics and Data Science
Cal Poly State University, San Luis Obispo
@ellisonbg on Twitter and GitHub
bgranger@calpoly.edu and ellisonbg@gmail.com

It's the notebook validation button in JS, that gives this kind of feedback:

![image](https://user-images.githubusercontent.com/57394/44878779-a7623500-ac5c-11e8-8489-b54c769ee995.png)

It's really just a thin layer atop the cmd line validation, so we're considering telling the students to run it at the cmd line... But I was wondering if anyone had thought about porting it over to Lab.
I unfortunately don't have the cycles to work on porting this over, but I agree it is something that should probably be done...
Might do some work on this - thought I'd start by trying to bring the assignments tab and maybe the notebook validate button to lab side. I'll update once (if) I get anything meaningful done!
What would be really useful is to get the assignments [and exchange] code and the formgrader code to be written in the same way, using the same style & idioms: they were written separately, and are very different.

I know that jupyterlab is all angular & typescript in the UI, so there's a chunk of work to just move from javascript to typescript!

(if you can also have space in your thinking for making the exchange a proper API, rather than assumed to be copying files on disk, that would be awesome [it's an outstanding piece of work that was raised at the Edinburgh hackaton... however we've been side-tracked by work & an impending new academic year....])

.... but absolutely: getting nbgrader into jupyterlabs would be fanTASTic!
Just checking whether there are more updates on this.

I don't think so, last I saw was a rough proof of concept embedding
a nbgrader view into a side panel - no time to do more, too much other
stuff.  I will ping @murhum1 in January to see how it's going, maybe
the new year will bring some more time.

- Richard

Since it's now March, just thought I'd check in again. 
I don't know of any update, but since this brought this to the top of my inbox, I'll reply to a comment earlier:



> I know that jupyterlab is all angular & typescript in the UI, so there's a chunk of work to just move from javascript to typescript!

JupyterLab is typescript and *Lumino*, but you can implement extensions in straight javascript if you like.
Hello everyone. @aalmanza1998, @lxylxy123456, @rkevin-arch, and I will be porting the nbgrader UI to JupyterLab as part of a project for integrating nbgrader with Kubernetes and JupyterLab. We are wrapping up the Kubernetes portion (see jupyterhub/zero-to-jupyterhub-k8s#1556) and starting the JupyterLab part. We hope to provide periodic updates on our progress.

Before we dive too deep, we'd like to gather comments and pointers from the community. Your input will be much appreciated!

---
> Might do some work on this - thought I'd start by trying to bring the assignments tab and maybe the notebook validate button to lab side. I'll update once (if) I get anything meaningful done!

@murhum1 Sounds great! Any updates?
Thank you very much @aalmanza1998, @lxylxy123456, @rkevin-arch. I've "sidegraded" to Notebook so I can do NBgrader but the Lab interface is my favorite by a large margin. I look forward to kicking the tires on your releases.
I really like the idea of using Kubernetes for isolation with NBgrader. 
Thanks again
Some thoughts, observations, ..... and questions!

Are you thinking of writing _another_ nbgrader, to be labs-compatible, or writing _parallel_ extensions to enable functionality in either interface?  By this, I mean if I switch from `/tree` to `/lab`, will I see the same assignments, use the same validators, and configure things the same way... or will they be different?

Write tests. Lots and lots of tests. Our own nbexchange release doesn't have a master branch yet because we don't have enough tests! (If you're really brave - go for a test-driven development environment: you know how the various components currently work - so you can write tests to check them, and then code to pass the tests)

Don't paint yourself into a corner: make sure your exchange is pluggable* (see the afore-mentioned PR for that in the current system). Are there any other aspects that can [should] be abstracted out?

Documentation: It's horrible to write, but ensure that either the existing docs remain valid for the process, or that you have it covered.

Will any of the the areas you'll work in, for lab-mode, actually benefit from improvement in _both_ interfaces, for the code-base as a whole?


[*] To me, having the Notebook App talk to an abstracted API makes even more sense in a K8 cluster: why tightly couple the exchange to the hub when you're in a cloud?
We want to add a UI (labextensions) to nbgrader for JupyterLab and use the same back-end. The assignments, validators, configuration, etc. will be the same. I see that there are tests for the nbextensions. We will write similar tests for the labextensions. We will also write documentation on how to install the labextensions and use nbgrader with the JupyterLab interface.

>Don't paint yourself into a corner: make sure your exchange is pluggable* (see the afore-mentioned PR for that in the current system). Are there any other aspects that can [should] be abstracted out?

The ngshare exchange uses the same pluggable exchange as nbexchange. I hope this encourages the adoption of the pluggable exchange into nbgrader. The JupyterLab part of our project is probably independent of the exchange back-end, so there shouldn't be a problem with compatibility.

> Will any of the the areas you'll work in, for lab-mode, actually benefit from improvement in both interfaces, for the code-base as a whole?

At this point, I don't have any improvements in mind. We shall see what ideas come up when we port the extensions.
> We want to add a UI (labextensions) to nbgrader for JupyterLab and use the same back-end. The assignments, validators, configuration, etc. will be the same.

Excellent!
(and this will be a superb addition.)

You'll discover that the `formgrader` code and the `assignments` code were written by different people (I thought there was an issue about this, but U can't find it) ... feel free to ignore that & make the two halves of the system consistent :)

> The ngshare exchange uses the same pluggable exchange as nbexchange. I hope this encourages the adoption of the pluggable exchange into nbgrader. The JupyterLab part of our project is probably independent of the exchange back-end, so there shouldn't be a problem with compatibility.
> 
1. I'd love to see more versions/variations of an external exchange .... it's what keeps these exosystems alive.
2. The one I will note is _authentication_ - not everything will authenticate against the hub. (we don't, for example: we have an external LTI authenticator & then a local cookie )

> > Will any of the the areas you'll work in, for lab-mode, actually benefit from improvement in both interfaces, for the code-base as a whole?
> 
> At this point, I don't have any improvements in mind. We shall see what ideas come up when we port the extensions.

Excellent....

Most of my questions/observations are just to help clarify what you're aiming to do from the outset, and make it harder for "mission-creep" to edge in.

We will definitely be interested in what you come up with, and will be glad you share


Fix patch:
diff --git a/.gitignore b/.gitignore
index 01a1bfba8..c3d906390 100644
--- a/.gitignore
+++ b/.gitignore
@@ -22,6 +22,10 @@ var/
 .installed.cfg
 *.egg
 
+# built labextension
+tsconfig.tsbuildinfo
+nbgrader/labextension/
+
 # 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.
@@ -120,3 +124,12 @@ package-lock.json
 .goutputstream*
 .idea
 .mypy_cache
+
+
+# playwright output tests directory
+playwright-tests/
+playwright-report
+test-results
+/test-results/
+/playwright-report/
+/dist-pw/
diff --git a/.readthedocs.yaml b/.readthedocs.yaml
index 4b1886973..1f542f248 100644
--- a/.readthedocs.yaml
+++ b/.readthedocs.yaml
@@ -3,6 +3,7 @@ build:
   os: ubuntu-20.04
   tools:
     python: "3"
+    nodejs: "16"
 sphinx:
   builder: html
   configuration: nbgrader/docs/source/conf.py
diff --git a/MANIFEST.in b/MANIFEST.in
index c889cdc57..2ea1f7454 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -2,13 +2,25 @@ include *.md
 include *.json
 include *.txt
 
-include .coveragerc
+include tasks.py
+include .readthedocs.yaml
 include .bowerrc
 include mypy.ini
-include pytest.ini
 include codecov.yml
-include .readthedocs.yaml
-include tasks.py
+include yarn.lock
+
+include pytest.ini
+include .coveragerc
+include conftest.py
+include playwright.config.ts
+
+# Lab extension files
+graft nbgrader/labextension
+graft src
+graft style
+graft schema
+prune **/node_modules
+prune lib
 
 #nbgrader format files
 include nbgrader/nbgraderformat/*.json
@@ -32,11 +44,14 @@ recursive-include nbgrader/alembic *
 include nbgrader/tests/apps/files/*
 include nbgrader/tests/preprocessors/files/*
 include nbgrader/tests/nbextensions/files/*
-
+recursive-include nbgrader/tests/labextension_ui-tests *
 # global exclusion
 global-exclude *.pyc
 global-exclude .ipynb_checkpoints
 global-exclude __pycache__
+global-exclude .git
+global-exclude *.pyo
+global-exclude *~
 
 # directories to prune
 prune demos
diff --git a/README.md b/README.md
index ec9f9bd85..8dc8a89b2 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,8 @@
 # nbgrader
 
-Build: [![Build](https://img.shields.io/github/workflow/status/jupyter/nbgrader/Test?logo=github&label=tests)](https://github.com/jupyter/nbgrader/actions)  
-Forum: [![Google Group](https://img.shields.io/badge/-Google%20Group-lightgrey.svg)](https://groups.google.com/forum/#!forum/jupyter)  
-Coverage: [![codecov.io](http://codecov.io/github/jupyter/nbgrader/coverage.svg?branch=master)](http://codecov.io/github/jupyter/nbgrader?branch=master)  
+Build: [![Build](https://img.shields.io/github/workflow/status/jupyter/nbgrader/Test?logo=github&label=tests)](https://github.com/jupyter/nbgrader/actions)
+Forum: [![Google Group](https://img.shields.io/badge/-Google%20Group-lightgrey.svg)](https://groups.google.com/forum/#!forum/jupyter)
+Coverage: [![codecov.io](http://codecov.io/github/jupyter/nbgrader/coverage.svg?branch=master)](http://codecov.io/github/jupyter/nbgrader?branch=master)
 Cite: [![DOI](https://jose.theoj.org/papers/10.21105/jose.00032/status.svg)](https://doi.org/10.21105/jose.00032)
 
 A system for assigning and grading Jupyter notebooks.
diff --git a/demos/formgrader_workspace.json b/demos/formgrader_workspace.json
new file mode 100644
index 000000000..ec022ab7d
--- /dev/null
+++ b/demos/formgrader_workspace.json
@@ -0,0 +1,26 @@
+{
+  "data": {
+    "layout-restorer:data": {
+      "main": {
+        "dock": {
+          "type": "tab-area",
+          "currentIndex": 0,
+          "widgets": [
+            "nbgrader-formgrader:nbgrader-formgrader"
+          ]
+        },
+        "current": "nbgrader-formgrader:nbgrader-formgrader"
+      },
+      "down": {
+        "size": 0,
+        "widgets": []
+      }
+    },
+    "nbgrader-formgrader:nbgrader-formgrader": {
+      "data": {}
+    }
+  },
+  "metadata": {
+    "id": "formgrader"
+  }
+}
diff --git a/install.json b/install.json
new file mode 100644
index 000000000..b17e227ea
--- /dev/null
+++ b/install.json
@@ -0,0 +1,5 @@
+{
+  "packageManager": "python",
+  "packageName": "nbgrader",
+  "uninstallInstructions": "Use your Python package manager (pip, conda, etc.) to uninstall the package nbgrader"
+}
diff --git a/nbgrader/__init__.py b/nbgrader/__init__.py
index 8fb6c8833..73e99144f 100644
--- a/nbgrader/__init__.py
+++ b/nbgrader/__init__.py
@@ -4,7 +4,34 @@
 
 import os
 import sys
-from ._version import version_info, __version__
+import json
+from pathlib import Path
+from ._version import __version__
+
+from .server_extensions.assignment_list import load_jupyter_server_extension as load_assignments
+from .server_extensions.course_list import load_jupyter_server_extension as load_courses
+from .server_extensions.validate_assignment import load_jupyter_server_extension as load_validate
+from .server_extensions.formgrader import load_jupyter_server_extension as load_formgrader
+
+HERE = Path(__file__).parent.resolve()
+
+
+if os.path.exists(HERE / "labextension"):
+    with (HERE / "labextension" / "package.json").open() as fid:
+        data = json.load(fid)
+
+
+    def _jupyter_labextension_paths():
+        return [{
+            "src": "labextension",
+            "dest": data["name"]
+        }]
+
+
+def _jupyter_server_extension_points():
+    return [{
+        "module": "nbgrader"
+    }]
 
 
 # Classic notebook extensions
@@ -51,20 +78,6 @@ def _jupyter_nbextension_paths():
     return paths
 
 
-# Jupyter server extensions
-def _jupyter_server_extension_points():
-    paths = [
-        dict(module="nbgrader.server_extensions.formgrader"),
-        dict(module="nbgrader.server_extensions.validate_assignment")
-    ]
-
-    if sys.platform != 'win32':
-        paths.append(dict(module="nbgrader.server_extensions.assignment_list"))
-        paths.append(dict(module="nbgrader.server_extensions.course_list"))
-
-    return paths
-
-
 # Classic notebook server extensions
 def _jupyter_server_extension_paths():
     paths = [
@@ -77,3 +90,10 @@ def _jupyter_server_extension_paths():
         paths.append(dict(module="nbgrader.server_extensions.course_list"))
 
     return paths
+
+
+def _load_jupyter_server_extension(app):
+    load_assignments(app)
+    load_courses(app)
+    load_formgrader(app)
+    load_validate(app)
diff --git a/nbgrader/docs/source/configuration/jupyterhub_config.rst b/nbgrader/docs/source/configuration/jupyterhub_config.rst
index 89369003f..f87349b0f 100644
--- a/nbgrader/docs/source/configuration/jupyterhub_config.rst
+++ b/nbgrader/docs/source/configuration/jupyterhub_config.rst
@@ -85,7 +85,7 @@ You will additionally need to install and enable the various nbgrader extensions
    :widths: 33 33 33 33
    :header-rows: 1
 
-   * - 
+   * -
      - Students
      - Instructors
      - Formgraders
diff --git a/nbgrader/docs/source/contributor_guide/installation_developer.rst b/nbgrader/docs/source/contributor_guide/installation_developer.rst
index bc1fbebda..09bc7cd38 100644
--- a/nbgrader/docs/source/contributor_guide/installation_developer.rst
+++ b/nbgrader/docs/source/contributor_guide/installation_developer.rst
@@ -11,35 +11,38 @@ clone the nbgrader repository::
 
 Installing and building nbgrader
 -------------------------------------
-nbgrader installs and builds with one command::
+Building nbgrader jupyterlab extension requires nodejs to be installed.
+We recommand using `conda environment <https://docs.conda.io/en/latest/miniconda.html>`_ with `mamba <https://mamba.readthedocs.io/en/latest/>`_::
 
-    pip install -e .[docs,tests]
+    # create a new environment
+    mamba create -n nbgrader -c conda-forge python nodejs -y
 
+    # activate the environment
+    mamba activate nbgrader
 
-Installing notebook extensions
-------------------------------
-Previously this was done using the ``nbgrader extension install`` command.
-However, moving forward this is done using the ``jupyter nbextension`` and
-``jupyter serverextension`` commands.
+    pip install -e ".[docs,tests]"
 
-The nbextensions are Javascript/HTML/CSS so they require
-separate installation and enabling.
-The --symlink option is recommended since it updates the extensions
-whenever you update the nbgrader repository.
-The serverextension is a Python module inside nbgrader, so only an
-enable step is needed.
-To install and enable all the frontend nbextensions (*assignment list*,
-*create assignment*, and *formgrader*) along with the server extensions
-(*assignment list* and *formgrader*) run::
+Installing Jupyter labextensions
+--------------------------------
+The labextensions are compiled during installation, and should be in the ``nbgrader/labextension`` directory.
+There are 5 of them (*formgrader*, *assignment list*, *course list*, *validate assignment* and *create assignment*),
+and with the exception of *create assignment* they must be installed along with the server extensions.
 
-    jupyter nbextension install --symlink --sys-prefix --py nbgrader
-    jupyter nbextension enable --sys-prefix --py nbgrader
-    jupyter serverextension enable --sys-prefix --py nbgrader
+To install the server extensions all together run::
 
-To work properly, the *assignment list* and *formgrader* extensions require
-both the nbextension and serverextension. The *create assignment* extension
-only has an nbextension part.
+    jupyter server extension enable nbgrader --sys-prefix
 
-Installing Firefox Headless WebDriver
--------------------------------------
-To run tests while developing nbgrader and its documentation, the Firefox headless webdriver must be installed. Please `follow the Mozilla installation instructions <https://developer.mozilla.org/en-US/docs/Web/WebDriver>`_ to get Firefox properly setup on your system.
+It is possible to enable only some of them by running::
+
+    jupyter server extension enable nbgrader.server_extensions.formgrader --sys-prefix
+    jupyter server extension enable nbgrader.server_extensions.assignment_list --sys-prefix
+    jupyter server extension enable nbgrader.server_extensions.course_list --sys-prefix
+    jupyter server extension enable nbgrader.server_extensions.validate_assignment --sys-prefix
+
+To install labextensions run::
+
+    jupyter labextension install .
+
+or in developer mode::
+
+    jupyter labextension develop --overwrite .
diff --git a/nbgrader/docs/source/user_guide/autograded/bitdiddle/ps1/problem1.ipynb b/nbgrader/docs/source/user_guide/autograded/bitdiddle/ps1/problem1.ipynb
index 3c83c6cb6..19b0e6ec1 100644
--- a/nbgrader/docs/source/user_guide/autograded/bitdiddle/ps1/problem1.ipynb
+++ b/nbgrader/docs/source/user_guide/autograded/bitdiddle/ps1/problem1.ipynb
@@ -136,7 +136,7 @@
      "traceback": [
       "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
       "\u001b[0;31mAssertionError\u001b[0m                            Traceback (most recent call last)",
-      "\u001b[0;32m<ipython-input-4-f3fef5b9ed4e>\u001b[0m in \u001b[0;36m<module>\u001b[0;34m\u001b[0m\n\u001b[1;32m      1\u001b[0m \u001b[0;34m\"\"\"Check that squares returns the correct output for several inputs\"\"\"\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 2\u001b[0;31m \u001b[0;32massert\u001b[0m \u001b[0msquares\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m      3\u001b[0m \u001b[0;32massert\u001b[0m \u001b[0msquares\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m2\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m4\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m      4\u001b[0m \u001b[0;32massert\u001b[0m \u001b[0msquares\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m10\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m4\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m9\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m16\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m25\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m36\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m49\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m64\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m81\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m100\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m      5\u001b[0m \u001b[0;32massert\u001b[0m \u001b[0msquares\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m11\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m4\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m9\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m16\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m25\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m36\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m49\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m64\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m81\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m100\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m121\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
+      "Input \u001b[0;32mIn [4]\u001b[0m, in \u001b[0;36m<cell line: 2>\u001b[0;34m()\u001b[0m\n\u001b[1;32m      1\u001b[0m \u001b[38;5;124;03m\"\"\"Check that squares returns the correct output for several inputs\"\"\"\u001b[39;00m\n\u001b[0;32m----> 2\u001b[0m \u001b[38;5;28;01massert\u001b[39;00m squares(\u001b[38;5;241m1\u001b[39m) \u001b[38;5;241m==\u001b[39m [\u001b[38;5;241m1\u001b[39m]\n\u001b[1;32m      3\u001b[0m \u001b[38;5;28;01massert\u001b[39;00m squares(\u001b[38;5;241m2\u001b[39m) \u001b[38;5;241m==\u001b[39m [\u001b[38;5;241m1\u001b[39m, \u001b[38;5;241m4\u001b[39m]\n\u001b[1;32m      4\u001b[0m \u001b[38;5;28;01massert\u001b[39;00m squares(\u001b[38;5;241m10\u001b[39m) \u001b[38;5;241m==\u001b[39m [\u001b[38;5;241m1\u001b[39m, \u001b[38;5;241m4\u001b[39m, \u001b[38;5;241m9\u001b[39m, \u001b[38;5;241m16\u001b[39m, \u001b[38;5;241m25\u001b[39m, \u001b[38;5;241m36\u001b[39m, \u001b[38;5;241m49\u001b[39m, \u001b[38;5;241m64\u001b[39m, \u001b[38;5;241m81\u001b[39m, \u001b[38;5;241m100\u001b[39m]\n",
       "\u001b[0;31mAssertionError\u001b[0m: "
      ]
     }
@@ -274,7 +274,7 @@
      "traceback": [
       "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
       "\u001b[0;31mAssertionError\u001b[0m                            Traceback (most recent call last)",
-      "\u001b[0;32m<ipython-input-8-1a00eaa7c988>\u001b[0m in \u001b[0;36m<module>\u001b[0;34m\u001b[0m\n\u001b[1;32m      1\u001b[0m \u001b[0;34m\"\"\"Check that sum_of_squares returns the correct answer for various inputs.\"\"\"\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 2\u001b[0;31m \u001b[0;32massert\u001b[0m \u001b[0msum_of_squares\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0;36m1\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m      3\u001b[0m \u001b[0;32massert\u001b[0m \u001b[0msum_of_squares\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m2\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0;36m5\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m      4\u001b[0m \u001b[0;32massert\u001b[0m \u001b[0msum_of_squares\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m10\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0;36m385\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m      5\u001b[0m \u001b[0;32massert\u001b[0m \u001b[0msum_of_squares\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m11\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0;36m506\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
+      "Input \u001b[0;32mIn [8]\u001b[0m, in \u001b[0;36m<cell line: 2>\u001b[0;34m()\u001b[0m\n\u001b[1;32m      1\u001b[0m \u001b[38;5;124;03m\"\"\"Check that sum_of_squares returns the correct answer for various inputs.\"\"\"\u001b[39;00m\n\u001b[0;32m----> 2\u001b[0m \u001b[38;5;28;01massert\u001b[39;00m sum_of_squares(\u001b[38;5;241m1\u001b[39m) \u001b[38;5;241m==\u001b[39m \u001b[38;5;241m1\u001b[39m\n\u001b[1;32m      3\u001b[0m \u001b[38;5;28;01massert\u001b[39;00m sum_of_squares(\u001b[38;5;241m2\u001b[39m) \u001b[38;5;241m==\u001b[39m \u001b[38;5;241m5\u001b[39m\n\u001b[1;32m      4\u001b[0m \u001b[38;5;28;01massert\u001b[39;00m sum_of_squares(\u001b[38;5;241m10\u001b[39m) \u001b[38;5;241m==\u001b[39m \u001b[38;5;241m385\u001b[39m\n",
       "\u001b[0;31mAssertionError\u001b[0m: "
      ]
     }
@@ -384,7 +384,7 @@
      "traceback": [
       "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
       "\u001b[0;31mNotImplementedError\u001b[0m                       Traceback (most recent call last)",
-      "\u001b[0;32m<ipython-input-10-15b94d1fa268>\u001b[0m in \u001b[0;36m<module>\u001b[0;34m\u001b[0m\n\u001b[1;32m      1\u001b[0m \u001b[0;31m# YOUR CODE HERE\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 2\u001b[0;31m \u001b[0;32mraise\u001b[0m \u001b[0mNotImplementedError\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m",
+      "Input \u001b[0;32mIn [10]\u001b[0m, in \u001b[0;36m<cell line: 2>\u001b[0;34m()\u001b[0m\n\u001b[1;32m      1\u001b[0m \u001b[38;5;66;03m# YOUR CODE HERE\u001b[39;00m\n\u001b[0;32m----> 2\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mNotImplementedError\u001b[39;00m()\n",
       "\u001b[0;31mNotImplementedError\u001b[0m: "
      ]
     }
diff --git a/nbgrader/docs/source/user_guide/creating_and_grading_assignments.ipynb b/nbgrader/docs/source/user_guide/creating_and_grading_assignments.ipynb
index 30a7447b0..198ea9561 100644
--- a/nbgrader/docs/source/user_guide/creating_and_grading_assignments.ipynb
+++ b/nbgrader/docs/source/user_guide/creating_and_grading_assignments.ipynb
@@ -52,9 +52,13 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "The formgrader extension provides the core access to nbgrader's instructor tools. After the extension has been installed, you can access it through the tab in the notebook list:\n",
+    "The formgrader extension provides the core access to nbgrader's instructor tools. After the extension has been installed, you can access it through the command palette (using `Command/Ctrl + Shift + c`), then typing `formgrader` :\n",
     "\n",
-    "![](images/formgrader_tab.png)"
+    "![formgrader command palette](images/formgrader_command_palette.png)\n",
+    "\n",
+    "An new tab should open in Jupyter Lab :\n",
+    "\n",
+    "![formgrader jlab tab](images/formgrader_jlab_tab.png)"
    ]
   },
   {
@@ -88,9 +92,9 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "To create a new assignment, open the formgrader extension and click the \"Add new assignment...\" button at the bottom of the page. This will ask you to provide some information such as the name of the assignment and its due date. Then, you can add files to the assignment and edit them by clicking the name of the assignment:\n",
+    "To create a new assignment, open the formgrader extension and click the \"Add new assignment...\" button at the bottom of the page. This will ask you to provide some information such as the name of the assignment and its due date. Clicking the name of the assignment will open the assignment directory in the file browser (left panel), where files can be added :\n",
     "\n",
-    "![](images/manage_assignments1.png)"
+    "![manage assignments open](images/manage_assignments1_jlab.png)"
    ]
   },
   {
@@ -155,22 +159,25 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "Once the toolbar has been installed, you should see it in the drop down \"View -> Cell Toolbar\" menu:\n",
+    "Once the toolbar has been installed, you should see it the right panel:\n",
+    "\n",
+    "![assignment toolbar button](images/assignment_toolbar1_jlab.png)\n",
     "\n",
-    "![](images/assignment_toolbar.png)\n",
+    "In the \"Create Assignment\" toolbar, each cell has its own nbgrader menu (the blue bars let you know which one is selected) :\n",
     "\n",
-    "Selecting the \"Create Assignment\" toolbar will create a separate toolbar\n",
-    "for each cell which by default will be a dropdown menu with the \"-\" item\n",
-    "selected. For markdown cells, there are two additional options to choose\n",
-    "from, either \"Manually graded answer\" or \"Read-only\":\n",
+    "![assignment toolbar blue bars](images/assignment_toolbar2_jlab.png)\n",
     "\n",
-    "![](images/markdown_cell.png)\n",
+    "By default each cell will have a dropdown menu with the \"-\" item\n",
+    "selected. For markdown cells, there are three additional options to choose\n",
+    "from, either \"Manually graded answer\", \"Manually graded task\" or \"Read-only\":\n",
     "\n",
-    "For code cells, there are four options to choose from, including\n",
-    "\"Manually graded answer\", \"Autograded answer\", \"Autograder tests\", and\n",
+    "![markdown cell](images/markdown_cell_jlab.png)\n",
+    "\n",
+    "For code cells, there are five options to choose from, including\n",
+    "\"Manually graded answer\", \"Manually graded task\", \"Autograded answer\", \"Autograder tests\", and\n",
     "\"Read-only\":\n",
     "\n",
-    "![](images/code_cell.png)\n",
+    "![code cell](images/code_cell_jlab.png)\n",
     "\n",
     "The following sections go into detail about the different cell types,\n",
     "and show cells that are taken from a complete example of an assignment\n",
@@ -203,7 +210,7 @@
     "a cell that contains an answer that must be manually graded by a human\n",
     "grader. Here is an example of a manually graded answer cell:\n",
     "\n",
-    "![](images/manually_graded_answer.png)\n",
+    "![manually graded answer](images/manually_graded_answer_jlab.png)\n",
     "\n",
     "The most common use case for this type of cell is for written\n",
     "free-response answers (for example, which interpret the results of code\n",
@@ -217,14 +224,6 @@
     "When you specify a manually graded answer, you must additionally tell nbgrader how many points the answer is worth, and an id for the cell. Additionally, when creating the release version of the assignment (see :ref:`assign-and-release-an-assignment`), the bodies of answer cells will be replaced with a code or text stub indicating to the students that they should put their answer or solution there. Please see :doc:`/configuration/student_version` for details on how to customize this behavior."
    ]
   },
-  {
-   "cell_type": "markdown",
-   "metadata": {},
-   "source": [
-    "*Note: the blue border only shows up when the nbgrader extension toolbar\n",
-    "is active; it will not be visible to students.*"
-   ]
-  },
   {
    "cell_type": "raw",
    "metadata": {},
@@ -256,16 +255,13 @@
     "They must be manually graded by a human\n",
     "grader. Here is an example of a manually graded answer cell:\n",
     "\n",
-    "![](images/task-cell-source.png)\n",
+    "![task cell source](images/task_cell_source_jlab.png)\n",
     "\n",
     "The difference with a manually graded answer is that the manually graded tasks cells are not edited by the student. A manually or automatically graded cell ask students to perform a task *in* one cell. A manually graded task asks students to perform a task *with* cells.\n",
     "\n",
     "The common use case for this type of cell is for tasks that require the\n",
     "student to create several cells such as \"Process the data and create a plot to illustrate your results.\" \n",
-    "or to contain notebook-wide tasks such as \"adhere to the PEP8 style convention.\"\n",
-    "\n",
-    "*Note: the blue border only shows up when the nbgrader extension toolbar\n",
-    "is active; it will not be visible to students.*"
+    "or to contain notebook-wide tasks such as \"adhere to the PEP8 style convention.\""
    ]
   },
   {
@@ -322,7 +318,7 @@
     "contains an answer which will be autograded. Here is an example of an\n",
     "autograded graded answer cell:\n",
     "\n",
-    "![](images/autograded_answer.png)"
+    "![autograded answer](images/autograded_answer_jlab.png)"
    ]
   },
   {
@@ -339,10 +335,7 @@
     "Unlike manually graded answers, autograded answers aren't worth any\n",
     "points: instead, the points for autograded answers are specified for the\n",
     "particular tests that grade those answers. See the next section for\n",
-    "further details.\n",
-    "\n",
-    "*Note: the blue border only shows up when the nbgrader extension toolbar\n",
-    "is active; it will not be visible to students.*"
+    "further details.\n"
    ]
   },
   {
@@ -361,7 +354,7 @@
     "contains tests to be run during autograding. Here is an example of two\n",
     "test cells:\n",
     "\n",
-    "![](images/autograder_tests.png)"
+    "![autograder tests](images/autograder_tests_jlab.png)"
    ]
   },
   {
@@ -375,7 +368,7 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "The lock icon on the left side of the cell toolbar indicates that the\n",
+    "The lock icon on the right side of the assignment toolbar indicates that the\n",
     "tests are \"read-only\". See the next section for further details on what\n",
     "this means."
    ]
@@ -387,14 +380,6 @@
     "For tips on writing autograder tests, see :ref:`autograding-resources`."
    ]
   },
-  {
-   "cell_type": "markdown",
-   "metadata": {},
-   "source": [
-    "*Note: the blue border only shows up when the nbgrader extension toolbar\n",
-    "is active; it will not be visible to students.*"
-   ]
-  },
   {
    "cell_type": "raw",
    "metadata": {},
@@ -422,7 +407,7 @@
    "source": [
     "Tests in \"Autograder tests\" cells can be hidden through the use of a special syntax such as ``### BEGIN HIDDEN TESTS`` and ``### END HIDDEN TESTS``, for example:\n",
     "\n",
-    "![](images/autograder_tests_hidden_tests.png)"
+    "![autograder tests hidden tests](images/autograder_tests_hidden_tests_jlab.png)"
    ]
   },
   {
@@ -464,17 +449,7 @@
     "cannot be modified. This is indicated by a lock icon on the left side of\n",
     "the cell toolbar:\n",
     "\n",
-    "![](images/read_only.png)"
-   ]
-  },
-  {
-   "cell_type": "raw",
-   "metadata": {},
-   "source": [
-    "However, this doesn't actually mean that it is truly read-only when opened in the notebook. Instead, what it means is that during the ``nbgrader generate_assignment`` step (see :ref:`assign-and-release-an-assignment`), the source of these cells will be recorded into the database. Then, during the ``nbgrader autograde`` step (see :ref:`autograde-assignments`), nbgrader will check whether the source of the student's version of the cell has changed. If it has, it will replace the cell's source with the version in the database, thus effectively overwriting any changes the student made.\n",
-    "\n",
-    ".. versionadded:: 0.4.0\n",
-    "    Read-only cells (and test cells) are now truly read-only! However, at the moment this functionality will only work on the master version of the notebook (5.0.0.dev)."
+    "![read only](images/read_only_jlab.png)"
    ]
   },
   {
@@ -519,15 +494,15 @@
    "source": [
     "Ideally, the solutions in the instructor version should be correct and pass all the test cases to ensure that you are giving your students tests that they can actually pass. To verify this is the case, you can use the validate extension:\n",
     "\n",
-    "![](images/validate_extension.png)\n",
+    "![validate extension](images/validate_extension_jlab.png)\n",
     "\n",
     "If your assignment passes all the tests, you'll get a success pop-up:\n",
     "\n",
-    "![](images/validate_success.png)\n",
+    "![validate success](images/validate_success_jlab.png)\n",
     "\n",
     "If it doesn't pass all the tests, you'll get a message telling you which cells failed:\n",
     "\n",
-    "![](images/validate_failed.png)"
+    "![validate failed](images/validate_failed_jlab.png)"
    ]
   },
   {
@@ -547,23 +522,27 @@
   {
    "cell_type": "code",
    "execution_count": 1,
-   "metadata": {},
+   "metadata": {
+    "vscode": {
+     "languageId": "python"
+    }
+   },
    "outputs": [
     {
-     "name": "stdout",
+     "name": "stderr",
      "output_type": "stream",
      "text": [
-      "Success! Your notebook passes all the tests.\n",
-      "Success! Your notebook passes all the tests.\n"
+      "[ValidateApp | WARNING] No nbgrader_config.py file found (rerun with --debug to see where nbgrader is looking)\n",
+      "[ValidateApp | INFO] Validating '[NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/source/ps1/problem1.ipynb'\n",
+      "[ValidateApp | INFO] Validating '[NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/source/ps1/problem2.ipynb'\n"
      ]
     },
     {
-     "name": "stderr",
+     "name": "stdout",
      "output_type": "stream",
      "text": [
-      "[ValidateApp | WARNING] No nbgrader_config.py file found (rerun with --debug to see where nbgrader is looking)\n",
-      "[ValidateApp | INFO] Validating '[NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/source/ps1/problem1.ipynb'\n",
-      "[ValidateApp | INFO] Validating '[NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/source/ps1/problem2.ipynb'\n"
+      "Success! Your notebook passes all the tests.\n",
+      "Success! Your notebook passes all the tests.\n"
      ]
     }
    ],
@@ -616,11 +595,11 @@
    "source": [
     "After an assignment has been created with the assignment toolbar, you will want to generate the version that students will receive. You can do this from the formgrader by clicking the \"generate\" button:\n",
     "\n",
-    "![](images/manage_assignments2.png)\n",
+    "![manage assignments generate](images/manage_assignments2_jlab.png)\n",
     "\n",
     "This should succeed with a pop-up window containing log output:\n",
     "\n",
-    "![](images/generate_assignment.png)"
+    "![generate assignment](images/generate_assignment_jlab.png)"
    ]
   },
   {
@@ -666,7 +645,11 @@
   {
    "cell_type": "code",
    "execution_count": 2,
-   "metadata": {},
+   "metadata": {
+    "vscode": {
+     "languageId": "python"
+    }
+   },
    "outputs": [
     {
      "name": "stderr",
@@ -702,7 +685,7 @@
    "source": [
     "After generating the student version of assignment, you should preview it to make sure that it looks correct. You can do this from the formgrader extension by clicking the \"preview\" button:\n",
     "\n",
-    "![](images/manage_assignments3.png)"
+    "![manage assignments preview](images/manage_assignments3_jlab.png)"
    ]
   },
   {
@@ -725,23 +708,27 @@
   {
    "cell_type": "code",
    "execution_count": 3,
-   "metadata": {},
+   "metadata": {
+    "vscode": {
+     "languageId": "python"
+    }
+   },
    "outputs": [
     {
-     "name": "stdout",
+     "name": "stderr",
      "output_type": "stream",
      "text": [
-      "Success! The notebook does not pass any tests.\n",
-      "Success! The notebook does not pass any tests.\n"
+      "[ValidateApp | WARNING] No nbgrader_config.py file found (rerun with --debug to see where nbgrader is looking)\n",
+      "[ValidateApp | INFO] Validating '[NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/release/ps1/problem1.ipynb'\n",
+      "[ValidateApp | INFO] Validating '[NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/release/ps1/problem2.ipynb'\n"
      ]
     },
     {
-     "name": "stderr",
+     "name": "stdout",
      "output_type": "stream",
      "text": [
-      "[ValidateApp | WARNING] No nbgrader_config.py file found (rerun with --debug to see where nbgrader is looking)\n",
-      "[ValidateApp | INFO] Validating '[NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/release/ps1/problem1.ipynb'\n",
-      "[ValidateApp | INFO] Validating '[NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/release/ps1/problem2.ipynb'\n"
+      "Success! The notebook does not pass any tests.\n",
+      "Success! The notebook does not pass any tests.\n"
      ]
     }
    ],
@@ -886,19 +873,19 @@
    "source": [
     "You can autograde individual submissions from the formgrader directly. To do so, click on the the number of submissions in the \"Manage Assignments\" view:\n",
     "\n",
-    "![](images/manage_assignments4.png)\n",
+    "![manage assignments submission](images/manage_assignments4_jlab.png)\n",
     "\n",
     "This will take you to a new page where you can see all the submissions. For a particular submission, click the \"autograde\" button to autograde it:\n",
     "\n",
-    "![](images/manage_submissions1.png)\n",
+    "![manage submissions autograde](images/manage_submissions1_jlab.png)\n",
     "\n",
     "After autograding completes, you will see a pop-up window with log output:\n",
     "\n",
-    "![](images/autograde_assignment.png)\n",
+    "![autograde assignment](images/autograde_assignment_jlab.png)\n",
     "\n",
     "And back on the submissions screen, you will see that the status of the submission has changed to \"needs manual grading\" and there is now a reported score as well: \n",
     "\n",
-    "![](images/manage_submissions2.png)"
+    "![manage submissions manual](images/manage_submissions2_jlab.png)"
    ]
   },
   {
@@ -918,15 +905,19 @@
   {
    "cell_type": "code",
    "execution_count": 4,
-   "metadata": {},
+   "metadata": {
+    "vscode": {
+     "languageId": "python"
+    }
+   },
    "outputs": [
     {
      "name": "stderr",
      "output_type": "stream",
      "text": [
       "[AutogradeApp | WARNING] No nbgrader_config.py file found (rerun with --debug to see where nbgrader is looking)\n",
-      "[AutogradeApp | INFO] Copying [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/submitted/bitdiddle/ps1/timestamp.txt -> [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/autograded/bitdiddle/ps1/timestamp.txt\n",
       "[AutogradeApp | INFO] Copying [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/submitted/bitdiddle/ps1/jupyter.png -> [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/autograded/bitdiddle/ps1/jupyter.png\n",
+      "[AutogradeApp | INFO] Copying [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/submitted/bitdiddle/ps1/timestamp.txt -> [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/autograded/bitdiddle/ps1/timestamp.txt\n",
       "[AutogradeApp | INFO] Creating/updating student with ID 'bitdiddle': {}\n",
       "[AutogradeApp | INFO] SubmittedAssignment<ps1 for bitdiddle> submitted at [timestamp]\n",
       "[AutogradeApp | INFO] Overwriting files with master versions from the source directory\n",
@@ -946,8 +937,8 @@
       "[AutogradeApp | INFO] Converting notebook [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/autograded/bitdiddle/ps1/problem2.ipynb\n",
       "[AutogradeApp | INFO] Writing [size] bytes to [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/autograded/bitdiddle/ps1/problem2.ipynb\n",
       "[AutogradeApp | INFO] Setting destination file permissions to 444\n",
-      "[AutogradeApp | INFO] Copying [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/submitted/hacker/ps1/timestamp.txt -> [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/autograded/hacker/ps1/timestamp.txt\n",
       "[AutogradeApp | INFO] Copying [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/submitted/hacker/ps1/jupyter.png -> [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/autograded/hacker/ps1/jupyter.png\n",
+      "[AutogradeApp | INFO] Copying [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/submitted/hacker/ps1/timestamp.txt -> [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/autograded/hacker/ps1/timestamp.txt\n",
       "[AutogradeApp | INFO] Creating/updating student with ID 'hacker': {}\n",
       "[AutogradeApp | INFO] SubmittedAssignment<ps1 for hacker> submitted at [timestamp]\n",
       "[AutogradeApp | INFO] Overwriting files with master versions from the source directory\n",
@@ -1140,22 +1131,26 @@
   {
    "cell_type": "code",
    "execution_count": 5,
-   "metadata": {},
+   "metadata": {
+    "vscode": {
+     "languageId": "python"
+    }
+   },
    "outputs": [
     {
      "name": "stderr",
      "output_type": "stream",
      "text": [
       "[GenerateFeedbackApp | WARNING] No nbgrader_config.py file found (rerun with --debug to see where nbgrader is looking)\n",
-      "[GenerateFeedbackApp | INFO] Copying [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/autograded/bitdiddle/ps1/timestamp.txt -> [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/feedback/bitdiddle/ps1/timestamp.txt\n",
       "[GenerateFeedbackApp | INFO] Copying [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/autograded/bitdiddle/ps1/jupyter.png -> [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/feedback/bitdiddle/ps1/jupyter.png\n",
+      "[GenerateFeedbackApp | INFO] Copying [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/autograded/bitdiddle/ps1/timestamp.txt -> [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/feedback/bitdiddle/ps1/timestamp.txt\n",
       "[GenerateFeedbackApp | INFO] Converting notebook [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/autograded/bitdiddle/ps1/problem1.ipynb\n",
       "[GenerateFeedbackApp | INFO] Writing [size] bytes to [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/feedback/bitdiddle/ps1/problem1.html\n",
       "[GenerateFeedbackApp | INFO] Converting notebook [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/autograded/bitdiddle/ps1/problem2.ipynb\n",
       "[GenerateFeedbackApp | INFO] Writing [size] bytes to [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/feedback/bitdiddle/ps1/problem2.html\n",
       "[GenerateFeedbackApp | INFO] Setting destination file permissions to 644\n",
-      "[GenerateFeedbackApp | INFO] Copying [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/autograded/hacker/ps1/timestamp.txt -> [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/feedback/hacker/ps1/timestamp.txt\n",
       "[GenerateFeedbackApp | INFO] Copying [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/autograded/hacker/ps1/jupyter.png -> [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/feedback/hacker/ps1/jupyter.png\n",
+      "[GenerateFeedbackApp | INFO] Copying [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/autograded/hacker/ps1/timestamp.txt -> [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/feedback/hacker/ps1/timestamp.txt\n",
       "[GenerateFeedbackApp | INFO] Converting notebook [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/autograded/hacker/ps1/problem1.ipynb\n",
       "[GenerateFeedbackApp | INFO] Writing [size] bytes to [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/feedback/hacker/ps1/problem1.html\n",
       "[GenerateFeedbackApp | INFO] Converting notebook [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/autograded/hacker/ps1/problem2.ipynb\n",
@@ -1221,7 +1216,11 @@
   {
    "cell_type": "code",
    "execution_count": 6,
-   "metadata": {},
+   "metadata": {
+    "vscode": {
+     "languageId": "python"
+    }
+   },
    "outputs": [
     {
      "name": "stderr",
@@ -1249,7 +1248,11 @@
   {
    "cell_type": "code",
    "execution_count": 7,
-   "metadata": {},
+   "metadata": {
+    "vscode": {
+     "languageId": "python"
+    }
+   },
    "outputs": [
     {
      "name": "stdout",
@@ -1283,5 +1286,5 @@
   }
  },
  "nbformat": 4,
- "nbformat_minor": 1
+ "nbformat_minor": 4
 }
diff --git a/nbgrader/docs/source/user_guide/feedback/bitdiddle/ps1/problem1.html b/nbgrader/docs/source/user_guide/feedback/bitdiddle/ps1/problem1.html
index e86374c1f..f9638904e 100644
--- a/nbgrader/docs/source/user_guide/feedback/bitdiddle/ps1/problem1.html
+++ b/nbgrader/docs/source/user_guide/feedback/bitdiddle/ps1/problem1.html
@@ -20,22 +20,22 @@
 span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
 .highlight .hll { background-color: #ffffcc }
 .highlight { background: #f8f8f8; }
-.highlight .c { color: #408080; font-style: italic } /* Comment */
+.highlight .c { color: #3D7B7B; font-style: italic } /* Comment */
 .highlight .err { border: 1px solid #FF0000 } /* Error */
 .highlight .k { color: #008000; font-weight: bold } /* Keyword */
 .highlight .o { color: #666666 } /* Operator */
-.highlight .ch { color: #408080; font-style: italic } /* Comment.Hashbang */
-.highlight .cm { color: #408080; font-style: italic } /* Comment.Multiline */
-.highlight .cp { color: #BC7A00 } /* Comment.Preproc */
-.highlight .cpf { color: #408080; font-style: italic } /* Comment.PreprocFile */
-.highlight .c1 { color: #408080; font-style: italic } /* Comment.Single */
-.highlight .cs { color: #408080; font-style: italic } /* Comment.Special */
+.highlight .ch { color: #3D7B7B; font-style: italic } /* Comment.Hashbang */
+.highlight .cm { color: #3D7B7B; font-style: italic } /* Comment.Multiline */
+.highlight .cp { color: #9C6500 } /* Comment.Preproc */
+.highlight .cpf { color: #3D7B7B; font-style: italic } /* Comment.PreprocFile */
+.highlight .c1 { color: #3D7B7B; font-style: italic } /* Comment.Single */
+.highlight .cs { color: #3D7B7B; font-style: italic } /* Comment.Special */
 .highlight .gd { color: #A00000 } /* Generic.Deleted */
 .highlight .ge { font-style: italic } /* Generic.Emph */
-.highlight .gr { color: #FF0000 } /* Generic.Error */
+.highlight .gr { color: #E40000 } /* Generic.Error */
 .highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */
-.highlight .gi { color: #00A000 } /* Generic.Inserted */
-.highlight .go { color: #888888 } /* Generic.Output */
+.highlight .gi { color: #008400 } /* Generic.Inserted */
+.highlight .go { color: #717171 } /* Generic.Output */
 .highlight .gp { color: #000080; font-weight: bold } /* Generic.Prompt */
 .highlight .gs { font-weight: bold } /* Generic.Strong */
 .highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
@@ -48,15 +48,15 @@
 .highlight .kt { color: #B00040 } /* Keyword.Type */
 .highlight .m { color: #666666 } /* Literal.Number */
 .highlight .s { color: #BA2121 } /* Literal.String */
-.highlight .na { color: #7D9029 } /* Name.Attribute */
+.highlight .na { color: #687822 } /* Name.Attribute */
 .highlight .nb { color: #008000 } /* Name.Builtin */
 .highlight .nc { color: #0000FF; font-weight: bold } /* Name.Class */
 .highlight .no { color: #880000 } /* Name.Constant */
 .highlight .nd { color: #AA22FF } /* Name.Decorator */
-.highlight .ni { color: #999999; font-weight: bold } /* Name.Entity */
-.highlight .ne { color: #D2413A; font-weight: bold } /* Name.Exception */
+.highlight .ni { color: #717171; font-weight: bold } /* Name.Entity */
+.highlight .ne { color: #CB3F38; font-weight: bold } /* Name.Exception */
 .highlight .nf { color: #0000FF } /* Name.Function */
-.highlight .nl { color: #A0A000 } /* Name.Label */
+.highlight .nl { color: #767600 } /* Name.Label */
 .highlight .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */
 .highlight .nt { color: #008000; font-weight: bold } /* Name.Tag */
 .highlight .nv { color: #19177C } /* Name.Variable */
@@ -73,11 +73,11 @@
 .highlight .dl { color: #BA2121 } /* Literal.String.Delimiter */
 .highlight .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */
 .highlight .s2 { color: #BA2121 } /* Literal.String.Double */
-.highlight .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */
+.highlight .se { color: #AA5D1F; font-weight: bold } /* Literal.String.Escape */
 .highlight .sh { color: #BA2121 } /* Literal.String.Heredoc */
-.highlight .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */
+.highlight .si { color: #A45A77; font-weight: bold } /* Literal.String.Interpol */
 .highlight .sx { color: #008000 } /* Literal.String.Other */
-.highlight .sr { color: #BB6688 } /* Literal.String.Regex */
+.highlight .sr { color: #A45A77 } /* Literal.String.Regex */
 .highlight .s1 { color: #BA2121 } /* Literal.String.Single */
 .highlight .ss { color: #19177C } /* Literal.String.Symbol */
 .highlight .bp { color: #008000 } /* Name.Builtin.Pseudo */
@@ -472,12 +472,11 @@ <h2 id="Part-A-(2-points)">Part A (2 points)<a class="anchor-link" href="#Part-A
 <pre>
 <span class="ansi-red-fg">---------------------------------------------------------------------------</span>
 <span class="ansi-red-fg">AssertionError</span>                            Traceback (most recent call last)
-<span class="ansi-green-fg">&lt;ipython-input-4-f3fef5b9ed4e&gt;</span> in <span class="ansi-cyan-fg">&lt;module&gt;</span>
-<span class="ansi-green-intense-fg ansi-bold">      1</span> <span class="ansi-blue-fg">&#34;&#34;&#34;Check that squares returns the correct output for several inputs&#34;&#34;&#34;</span>
-<span class="ansi-green-fg">----&gt; 2</span><span class="ansi-red-fg"> </span><span class="ansi-green-fg">assert</span> squares<span class="ansi-blue-fg">(</span><span class="ansi-cyan-fg">1</span><span class="ansi-blue-fg">)</span> <span class="ansi-blue-fg">==</span> <span class="ansi-blue-fg">[</span><span class="ansi-cyan-fg">1</span><span class="ansi-blue-fg">]</span>
-<span class="ansi-green-intense-fg ansi-bold">      3</span> <span class="ansi-green-fg">assert</span> squares<span class="ansi-blue-fg">(</span><span class="ansi-cyan-fg">2</span><span class="ansi-blue-fg">)</span> <span class="ansi-blue-fg">==</span> <span class="ansi-blue-fg">[</span><span class="ansi-cyan-fg">1</span><span class="ansi-blue-fg">,</span> <span class="ansi-cyan-fg">4</span><span class="ansi-blue-fg">]</span>
-<span class="ansi-green-intense-fg ansi-bold">      4</span> <span class="ansi-green-fg">assert</span> squares<span class="ansi-blue-fg">(</span><span class="ansi-cyan-fg">10</span><span class="ansi-blue-fg">)</span> <span class="ansi-blue-fg">==</span> <span class="ansi-blue-fg">[</span><span class="ansi-cyan-fg">1</span><span class="ansi-blue-fg">,</span> <span class="ansi-cyan-fg">4</span><span class="ansi-blue-fg">,</span> <span class="ansi-cyan-fg">9</span><span class="ansi-blue-fg">,</span> <span class="ansi-cyan-fg">16</span><span class="ansi-blue-fg">,</span> <span class="ansi-cyan-fg">25</span><span class="ansi-blue-fg">,</span> <span class="ansi-cyan-fg">36</span><span class="ansi-blue-fg">,</span> <span class="ansi-cyan-fg">49</span><span class="ansi-blue-fg">,</span> <span class="ansi-cyan-fg">64</span><span class="ansi-blue-fg">,</span> <span class="ansi-cyan-fg">81</span><span class="ansi-blue-fg">,</span> <span class="ansi-cyan-fg">100</span><span class="ansi-blue-fg">]</span>
-<span class="ansi-green-intense-fg ansi-bold">      5</span> <span class="ansi-green-fg">assert</span> squares<span class="ansi-blue-fg">(</span><span class="ansi-cyan-fg">11</span><span class="ansi-blue-fg">)</span> <span class="ansi-blue-fg">==</span> <span class="ansi-blue-fg">[</span><span class="ansi-cyan-fg">1</span><span class="ansi-blue-fg">,</span> <span class="ansi-cyan-fg">4</span><span class="ansi-blue-fg">,</span> <span class="ansi-cyan-fg">9</span><span class="ansi-blue-fg">,</span> <span class="ansi-cyan-fg">16</span><span class="ansi-blue-fg">,</span> <span class="ansi-cyan-fg">25</span><span class="ansi-blue-fg">,</span> <span class="ansi-cyan-fg">36</span><span class="ansi-blue-fg">,</span> <span class="ansi-cyan-fg">49</span><span class="ansi-blue-fg">,</span> <span class="ansi-cyan-fg">64</span><span class="ansi-blue-fg">,</span> <span class="ansi-cyan-fg">81</span><span class="ansi-blue-fg">,</span> <span class="ansi-cyan-fg">100</span><span class="ansi-blue-fg">,</span> <span class="ansi-cyan-fg">121</span><span class="ansi-blue-fg">]</span>
+Input <span class="ansi-green-fg">In [4]</span>, in <span class="ansi-cyan-fg">&lt;cell line: 2&gt;</span><span class="ansi-blue-fg">()</span>
+<span class="ansi-green-intense-fg ansi-bold">      1</span> <span style="color: rgb(175,0,0)">&#34;&#34;&#34;Check that squares returns the correct output for several inputs&#34;&#34;&#34;</span>
+<span class="ansi-green-fg">----&gt; 2</span> <span class="ansi-bold" style="color: rgb(0,135,0)">assert</span> squares(<span style="color: rgb(98,98,98)">1</span>) <span style="color: rgb(98,98,98)">==</span> [<span style="color: rgb(98,98,98)">1</span>]
+<span class="ansi-green-intense-fg ansi-bold">      3</span> <span class="ansi-bold" style="color: rgb(0,135,0)">assert</span> squares(<span style="color: rgb(98,98,98)">2</span>) <span style="color: rgb(98,98,98)">==</span> [<span style="color: rgb(98,98,98)">1</span>, <span style="color: rgb(98,98,98)">4</span>]
+<span class="ansi-green-intense-fg ansi-bold">      4</span> <span class="ansi-bold" style="color: rgb(0,135,0)">assert</span> squares(<span style="color: rgb(98,98,98)">10</span>) <span style="color: rgb(98,98,98)">==</span> [<span style="color: rgb(98,98,98)">1</span>, <span style="color: rgb(98,98,98)">4</span>, <span style="color: rgb(98,98,98)">9</span>, <span style="color: rgb(98,98,98)">16</span>, <span style="color: rgb(98,98,98)">25</span>, <span style="color: rgb(98,98,98)">36</span>, <span style="color: rgb(98,98,98)">49</span>, <span style="color: rgb(98,98,98)">64</span>, <span style="color: rgb(98,98,98)">81</span>, <span style="color: rgb(98,98,98)">100</span>]
 
 <span class="ansi-red-fg">AssertionError</span>: </pre>
 </div>
@@ -627,12 +626,11 @@ <h2 id="Part-B-(1-point)">Part B (1 point)<a class="anchor-link" href="#Part-B-(
 <pre>
 <span class="ansi-red-fg">---------------------------------------------------------------------------</span>
 <span class="ansi-red-fg">AssertionError</span>                            Traceback (most recent call last)
-<span class="ansi-green-fg">&lt;ipython-input-8-1a00eaa7c988&gt;</span> in <span class="ansi-cyan-fg">&lt;module&gt;</span>
-<span class="ansi-green-intense-fg ansi-bold">      1</span> <span class="ansi-blue-fg">&#34;&#34;&#34;Check that sum_of_squares returns the correct answer for various inputs.&#34;&#34;&#34;</span>
-<span class="ansi-green-fg">----&gt; 2</span><span class="ansi-red-fg"> </span><span class="ansi-green-fg">assert</span> sum_of_squares<span class="ansi-blue-fg">(</span><span class="ansi-cyan-fg">1</span><span class="ansi-blue-fg">)</span> <span class="ansi-blue-fg">==</span> <span class="ansi-cyan-fg">1</span>
-<span class="ansi-green-intense-fg ansi-bold">      3</span> <span class="ansi-green-fg">assert</span> sum_of_squares<span class="ansi-blue-fg">(</span><span class="ansi-cyan-fg">2</span><span class="ansi-blue-fg">)</span> <span class="ansi-blue-fg">==</span> <span class="ansi-cyan-fg">5</span>
-<span class="ansi-green-intense-fg ansi-bold">      4</span> <span class="ansi-green-fg">assert</span> sum_of_squares<span class="ansi-blue-fg">(</span><span class="ansi-cyan-fg">10</span><span class="ansi-blue-fg">)</span> <span class="ansi-blue-fg">==</span> <span class="ansi-cyan-fg">385</span>
-<span class="ansi-green-intense-fg ansi-bold">      5</span> <span class="ansi-green-fg">assert</span> sum_of_squares<span class="ansi-blue-fg">(</span><span class="ansi-cyan-fg">11</span><span class="ansi-blue-fg">)</span> <span class="ansi-blue-fg">==</span> <span class="ansi-cyan-fg">506</span>
+Input <span class="ansi-green-fg">In [8]</span>, in <span class="ansi-cyan-fg">&lt;cell line: 2&gt;</span><span class="ansi-blue-fg">()</span>
+<span class="ansi-green-intense-fg ansi-bold">      1</span> <span style="color: rgb(175,0,0)">&#34;&#34;&#34;Check that sum_of_squares returns the correct answer for various inputs.&#34;&#34;&#34;</span>
+<span class="ansi-green-fg">----&gt; 2</span> <span class="ansi-bold" style="color: rgb(0,135,0)">assert</span> sum_of_squares(<span style="color: rgb(98,98,98)">1</span>) <span style="color: rgb(98,98,98)">==</span> <span style="color: rgb(98,98,98)">1</span>
+<span class="ansi-green-intense-fg ansi-bold">      3</span> <span class="ansi-bold" style="color: rgb(0,135,0)">assert</span> sum_of_squares(<span style="color: rgb(98,98,98)">2</span>) <span style="color: rgb(98,98,98)">==</span> <span style="color: rgb(98,98,98)">5</span>
+<span class="ansi-green-intense-fg ansi-bold">      4</span> <span class="ansi-bold" style="color: rgb(0,135,0)">assert</span> sum_of_squares(<span style="color: rgb(98,98,98)">10</span>) <span style="color: rgb(98,98,98)">==</span> <span style="color: rgb(98,98,98)">385</span>
 
 <span class="ansi-red-fg">AssertionError</span>: </pre>
 </div>
@@ -742,9 +740,9 @@ <h2 id="Part-D-(2-points)">Part D (2 points)<a class="anchor-link" href="#Part-D
 <pre>
 <span class="ansi-red-fg">---------------------------------------------------------------------------</span>
 <span class="ansi-red-fg">NotImplementedError</span>                       Traceback (most recent call last)
-<span class="ansi-green-fg">&lt;ipython-input-10-15b94d1fa268&gt;</span> in <span class="ansi-cyan-fg">&lt;module&gt;</span>
-<span class="ansi-green-intense-fg ansi-bold">      1</span> <span class="ansi-red-fg"># YOUR CODE HERE</span>
-<span class="ansi-green-fg">----&gt; 2</span><span class="ansi-red-fg"> </span><span class="ansi-green-fg">raise</span> NotImplementedError<span class="ansi-blue-fg">(</span><span class="ansi-blue-fg">)</span>
+Input <span class="ansi-green-fg">In [10]</span>, in <span class="ansi-cyan-fg">&lt;cell line: 2&gt;</span><span class="ansi-blue-fg">()</span>
+<span class="ansi-green-intense-fg ansi-bold">      1</span> <span style="color: rgb(95,135,135)"># YOUR CODE HERE</span>
+<span class="ansi-green-fg">----&gt; 2</span> <span class="ansi-bold" style="color: rgb(0,135,0)">raise</span> <span class="ansi-bold" style="color: rgb(215,95,95)">NotImplementedError</span>()
 
 <span class="ansi-red-fg">NotImplementedError</span>: </pre>
 </div>
diff --git a/nbgrader/docs/source/user_guide/feedback/bitdiddle/ps1/problem2.html b/nbgrader/docs/source/user_guide/feedback/bitdiddle/ps1/problem2.html
index 195768faa..3539525aa 100644
--- a/nbgrader/docs/source/user_guide/feedback/bitdiddle/ps1/problem2.html
+++ b/nbgrader/docs/source/user_guide/feedback/bitdiddle/ps1/problem2.html
@@ -20,22 +20,22 @@
 span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
 .highlight .hll { background-color: #ffffcc }
 .highlight { background: #f8f8f8; }
-.highlight .c { color: #408080; font-style: italic } /* Comment */
+.highlight .c { color: #3D7B7B; font-style: italic } /* Comment */
 .highlight .err { border: 1px solid #FF0000 } /* Error */
 .highlight .k { color: #008000; font-weight: bold } /* Keyword */
 .highlight .o { color: #666666 } /* Operator */
-.highlight .ch { color: #408080; font-style: italic } /* Comment.Hashbang */
-.highlight .cm { color: #408080; font-style: italic } /* Comment.Multiline */
-.highlight .cp { color: #BC7A00 } /* Comment.Preproc */
-.highlight .cpf { color: #408080; font-style: italic } /* Comment.PreprocFile */
-.highlight .c1 { color: #408080; font-style: italic } /* Comment.Single */
-.highlight .cs { color: #408080; font-style: italic } /* Comment.Special */
+.highlight .ch { color: #3D7B7B; font-style: italic } /* Comment.Hashbang */
+.highlight .cm { color: #3D7B7B; font-style: italic } /* Comment.Multiline */
+.highlight .cp { color: #9C6500 } /* Comment.Preproc */
+.highlight .cpf { color: #3D7B7B; font-style: italic } /* Comment.PreprocFile */
+.highlight .c1 { color: #3D7B7B; font-style: italic } /* Comment.Single */
+.highlight .cs { color: #3D7B7B; font-style: italic } /* Comment.Special */
 .highlight .gd { color: #A00000 } /* Generic.Deleted */
 .highlight .ge { font-style: italic } /* Generic.Emph */
-.highlight .gr { color: #FF0000 } /* Generic.Error */
+.highlight .gr { color: #E40000 } /* Generic.Error */
 .highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */
-.highlight .gi { color: #00A000 } /* Generic.Inserted */
-.highlight .go { color: #888888 } /* Generic.Output */
+.highlight .gi { color: #008400 } /* Generic.Inserted */
+.highlight .go { color: #717171 } /* Generic.Output */
 .highlight .gp { color: #000080; font-weight: bold } /* Generic.Prompt */
 .highlight .gs { font-weight: bold } /* Generic.Strong */
 .highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
@@ -48,15 +48,15 @@
 .highlight .kt { color: #B00040 } /* Keyword.Type */
 .highlight .m { color: #666666 } /* Literal.Number */
 .highlight .s { color: #BA2121 } /* Literal.String */
-.highlight .na { color: #7D9029 } /* Name.Attribute */
+.highlight .na { color: #687822 } /* Name.Attribute */
 .highlight .nb { color: #008000 } /* Name.Builtin */
 .highlight .nc { color: #0000FF; font-weight: bold } /* Name.Class */
 .highlight .no { color: #880000 } /* Name.Constant */
 .highlight .nd { color: #AA22FF } /* Name.Decorator */
-.highlight .ni { color: #999999; font-weight: bold } /* Name.Entity */
-.highlight .ne { color: #D2413A; font-weight: bold } /* Name.Exception */
+.highlight .ni { color: #717171; font-weight: bold } /* Name.Entity */
+.highlight .ne { color: #CB3F38; font-weight: bold } /* Name.Exception */
 .highlight .nf { color: #0000FF } /* Name.Function */
-.highlight .nl { color: #A0A000 } /* Name.Label */
+.highlight .nl { color: #767600 } /* Name.Label */
 .highlight .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */
 .highlight .nt { color: #008000; font-weight: bold } /* Name.Tag */
 .highlight .nv { color: #19177C } /* Name.Variable */
@@ -73,11 +73,11 @@
 .highlight .dl { color: #BA2121 } /* Literal.String.Delimiter */
 .highlight .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */
 .highlight .s2 { color: #BA2121 } /* Literal.String.Double */
-.highlight .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */
+.highlight .se { color: #AA5D1F; font-weight: bold } /* Literal.String.Escape */
 .highlight .sh { color: #BA2121 } /* Literal.String.Heredoc */
-.highlight .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */
+.highlight .si { color: #A45A77; font-weight: bold } /* Literal.String.Interpol */
 .highlight .sx { color: #008000 } /* Literal.String.Other */
-.highlight .sr { color: #BB6688 } /* Literal.String.Regex */
+.highlight .sr { color: #A45A77 } /* Literal.String.Regex */
 .highlight .s1 { color: #BA2121 } /* Literal.String.Single */
 .highlight .ss { color: #19177C } /* Literal.String.Symbol */
 .highlight .bp { color: #008000 } /* Name.Builtin.Pseudo */
diff --git a/nbgrader/docs/source/user_guide/feedback/hacker/ps1/problem1.html b/nbgrader/docs/source/user_guide/feedback/hacker/ps1/problem1.html
index 7ab52111d..c8c2f775a 100644
--- a/nbgrader/docs/source/user_guide/feedback/hacker/ps1/problem1.html
+++ b/nbgrader/docs/source/user_guide/feedback/hacker/ps1/problem1.html
@@ -20,22 +20,22 @@
 span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
 .highlight .hll { background-color: #ffffcc }
 .highlight { background: #f8f8f8; }
-.highlight .c { color: #408080; font-style: italic } /* Comment */
+.highlight .c { color: #3D7B7B; font-style: italic } /* Comment */
 .highlight .err { border: 1px solid #FF0000 } /* Error */
 .highlight .k { color: #008000; font-weight: bold } /* Keyword */
 .highlight .o { color: #666666 } /* Operator */
-.highlight .ch { color: #408080; font-style: italic } /* Comment.Hashbang */
-.highlight .cm { color: #408080; font-style: italic } /* Comment.Multiline */
-.highlight .cp { color: #BC7A00 } /* Comment.Preproc */
-.highlight .cpf { color: #408080; font-style: italic } /* Comment.PreprocFile */
-.highlight .c1 { color: #408080; font-style: italic } /* Comment.Single */
-.highlight .cs { color: #408080; font-style: italic } /* Comment.Special */
+.highlight .ch { color: #3D7B7B; font-style: italic } /* Comment.Hashbang */
+.highlight .cm { color: #3D7B7B; font-style: italic } /* Comment.Multiline */
+.highlight .cp { color: #9C6500 } /* Comment.Preproc */
+.highlight .cpf { color: #3D7B7B; font-style: italic } /* Comment.PreprocFile */
+.highlight .c1 { color: #3D7B7B; font-style: italic } /* Comment.Single */
+.highlight .cs { color: #3D7B7B; font-style: italic } /* Comment.Special */
 .highlight .gd { color: #A00000 } /* Generic.Deleted */
 .highlight .ge { font-style: italic } /* Generic.Emph */
-.highlight .gr { color: #FF0000 } /* Generic.Error */
+.highlight .gr { color: #E40000 } /* Generic.Error */
 .highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */
-.highlight .gi { color: #00A000 } /* Generic.Inserted */
-.highlight .go { color: #888888 } /* Generic.Output */
+.highlight .gi { color: #008400 } /* Generic.Inserted */
+.highlight .go { color: #717171 } /* Generic.Output */
 .highlight .gp { color: #000080; font-weight: bold } /* Generic.Prompt */
 .highlight .gs { font-weight: bold } /* Generic.Strong */
 .highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
@@ -48,15 +48,15 @@
 .highlight .kt { color: #B00040 } /* Keyword.Type */
 .highlight .m { color: #666666 } /* Literal.Number */
 .highlight .s { color: #BA2121 } /* Literal.String */
-.highlight .na { color: #7D9029 } /* Name.Attribute */
+.highlight .na { color: #687822 } /* Name.Attribute */
 .highlight .nb { color: #008000 } /* Name.Builtin */
 .highlight .nc { color: #0000FF; font-weight: bold } /* Name.Class */
 .highlight .no { color: #880000 } /* Name.Constant */
 .highlight .nd { color: #AA22FF } /* Name.Decorator */
-.highlight .ni { color: #999999; font-weight: bold } /* Name.Entity */
-.highlight .ne { color: #D2413A; font-weight: bold } /* Name.Exception */
+.highlight .ni { color: #717171; font-weight: bold } /* Name.Entity */
+.highlight .ne { color: #CB3F38; font-weight: bold } /* Name.Exception */
 .highlight .nf { color: #0000FF } /* Name.Function */
-.highlight .nl { color: #A0A000 } /* Name.Label */
+.highlight .nl { color: #767600 } /* Name.Label */
 .highlight .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */
 .highlight .nt { color: #008000; font-weight: bold } /* Name.Tag */
 .highlight .nv { color: #19177C } /* Name.Variable */
@@ -73,11 +73,11 @@
 .highlight .dl { color: #BA2121 } /* Literal.String.Delimiter */
 .highlight .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */
 .highlight .s2 { color: #BA2121 } /* Literal.String.Double */
-.highlight .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */
+.highlight .se { color: #AA5D1F; font-weight: bold } /* Literal.String.Escape */
 .highlight .sh { color: #BA2121 } /* Literal.String.Heredoc */
-.highlight .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */
+.highlight .si { color: #A45A77; font-weight: bold } /* Literal.String.Interpol */
 .highlight .sx { color: #008000 } /* Literal.String.Other */
-.highlight .sr { color: #BB6688 } /* Literal.String.Regex */
+.highlight .sr { color: #A45A77 } /* Literal.String.Regex */
 .highlight .s1 { color: #BA2121 } /* Literal.String.Single */
 .highlight .ss { color: #19177C } /* Literal.String.Symbol */
 .highlight .bp { color: #008000 } /* Name.Builtin.Pseudo */
diff --git a/nbgrader/docs/source/user_guide/feedback/hacker/ps1/problem2.html b/nbgrader/docs/source/user_guide/feedback/hacker/ps1/problem2.html
index 04743c74a..1fa256ce3 100644
--- a/nbgrader/docs/source/user_guide/feedback/hacker/ps1/problem2.html
+++ b/nbgrader/docs/source/user_guide/feedback/hacker/ps1/problem2.html
@@ -20,22 +20,22 @@
 span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
 .highlight .hll { background-color: #ffffcc }
 .highlight { background: #f8f8f8; }
-.highlight .c { color: #408080; font-style: italic } /* Comment */
+.highlight .c { color: #3D7B7B; font-style: italic } /* Comment */
 .highlight .err { border: 1px solid #FF0000 } /* Error */
 .highlight .k { color: #008000; font-weight: bold } /* Keyword */
 .highlight .o { color: #666666 } /* Operator */
-.highlight .ch { color: #408080; font-style: italic } /* Comment.Hashbang */
-.highlight .cm { color: #408080; font-style: italic } /* Comment.Multiline */
-.highlight .cp { color: #BC7A00 } /* Comment.Preproc */
-.highlight .cpf { color: #408080; font-style: italic } /* Comment.PreprocFile */
-.highlight .c1 { color: #408080; font-style: italic } /* Comment.Single */
-.highlight .cs { color: #408080; font-style: italic } /* Comment.Special */
+.highlight .ch { color: #3D7B7B; font-style: italic } /* Comment.Hashbang */
+.highlight .cm { color: #3D7B7B; font-style: italic } /* Comment.Multiline */
+.highlight .cp { color: #9C6500 } /* Comment.Preproc */
+.highlight .cpf { color: #3D7B7B; font-style: italic } /* Comment.PreprocFile */
+.highlight .c1 { color: #3D7B7B; font-style: italic } /* Comment.Single */
+.highlight .cs { color: #3D7B7B; font-style: italic } /* Comment.Special */
 .highlight .gd { color: #A00000 } /* Generic.Deleted */
 .highlight .ge { font-style: italic } /* Generic.Emph */
-.highlight .gr { color: #FF0000 } /* Generic.Error */
+.highlight .gr { color: #E40000 } /* Generic.Error */
 .highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */
-.highlight .gi { color: #00A000 } /* Generic.Inserted */
-.highlight .go { color: #888888 } /* Generic.Output */
+.highlight .gi { color: #008400 } /* Generic.Inserted */
+.highlight .go { color: #717171 } /* Generic.Output */
 .highlight .gp { color: #000080; font-weight: bold } /* Generic.Prompt */
 .highlight .gs { font-weight: bold } /* Generic.Strong */
 .highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
@@ -48,15 +48,15 @@
 .highlight .kt { color: #B00040 } /* Keyword.Type */
 .highlight .m { color: #666666 } /* Literal.Number */
 .highlight .s { color: #BA2121 } /* Literal.String */
-.highlight .na { color: #7D9029 } /* Name.Attribute */
+.highlight .na { color: #687822 } /* Name.Attribute */
 .highlight .nb { color: #008000 } /* Name.Builtin */
 .highlight .nc { color: #0000FF; font-weight: bold } /* Name.Class */
 .highlight .no { color: #880000 } /* Name.Constant */
 .highlight .nd { color: #AA22FF } /* Name.Decorator */
-.highlight .ni { color: #999999; font-weight: bold } /* Name.Entity */
-.highlight .ne { color: #D2413A; font-weight: bold } /* Name.Exception */
+.highlight .ni { color: #717171; font-weight: bold } /* Name.Entity */
+.highlight .ne { color: #CB3F38; font-weight: bold } /* Name.Exception */
 .highlight .nf { color: #0000FF } /* Name.Function */
-.highlight .nl { color: #A0A000 } /* Name.Label */
+.highlight .nl { color: #767600 } /* Name.Label */
 .highlight .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */
 .highlight .nt { color: #008000; font-weight: bold } /* Name.Tag */
 .highlight .nv { color: #19177C } /* Name.Variable */
@@ -73,11 +73,11 @@
 .highlight .dl { color: #BA2121 } /* Literal.String.Delimiter */
 .highlight .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */
 .highlight .s2 { color: #BA2121 } /* Literal.String.Double */
-.highlight .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */
+.highlight .se { color: #AA5D1F; font-weight: bold } /* Literal.String.Escape */
 .highlight .sh { color: #BA2121 } /* Literal.String.Heredoc */
-.highlight .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */
+.highlight .si { color: #A45A77; font-weight: bold } /* Literal.String.Interpol */
 .highlight .sx { color: #008000 } /* Literal.String.Other */
-.highlight .sr { color: #BB6688 } /* Literal.String.Regex */
+.highlight .sr { color: #A45A77 } /* Literal.String.Regex */
 .highlight .s1 { color: #BA2121 } /* Literal.String.Single */
 .highlight .ss { color: #19177C } /* Literal.String.Symbol */
 .highlight .bp { color: #008000 } /* Name.Builtin.Pseudo */
diff --git a/nbgrader/docs/source/user_guide/images/assignment_list_command_palette.png b/nbgrader/docs/source/user_guide/images/assignment_list_command_palette.png
new file mode 100644
index 000000000..0aa892816
Binary files /dev/null and b/nbgrader/docs/source/user_guide/images/assignment_list_command_palette.png differ
diff --git a/nbgrader/docs/source/user_guide/images/assignment_list_downloaded.png b/nbgrader/docs/source/user_guide/images/assignment_list_downloaded.png
deleted file mode 100644
index 9361a446f..000000000
Binary files a/nbgrader/docs/source/user_guide/images/assignment_list_downloaded.png and /dev/null differ
diff --git a/nbgrader/docs/source/user_guide/images/assignment_list_downloaded_expanded.png b/nbgrader/docs/source/user_guide/images/assignment_list_downloaded_expanded.png
deleted file mode 100644
index 17a36131c..000000000
Binary files a/nbgrader/docs/source/user_guide/images/assignment_list_downloaded_expanded.png and /dev/null differ
diff --git a/nbgrader/docs/source/user_guide/images/assignment_list_downloaded_expanded_jlab.png b/nbgrader/docs/source/user_guide/images/assignment_list_downloaded_expanded_jlab.png
new file mode 100644
index 000000000..da3bba6a5
Binary files /dev/null and b/nbgrader/docs/source/user_guide/images/assignment_list_downloaded_expanded_jlab.png differ
diff --git a/nbgrader/docs/source/user_guide/images/assignment_list_downloaded_jlab.png b/nbgrader/docs/source/user_guide/images/assignment_list_downloaded_jlab.png
new file mode 100644
index 000000000..7821c59a3
Binary files /dev/null and b/nbgrader/docs/source/user_guide/images/assignment_list_downloaded_jlab.png differ
diff --git a/nbgrader/docs/source/user_guide/images/assignment_list_released.png b/nbgrader/docs/source/user_guide/images/assignment_list_released.png
deleted file mode 100644
index c84913db3..000000000
Binary files a/nbgrader/docs/source/user_guide/images/assignment_list_released.png and /dev/null differ
diff --git a/nbgrader/docs/source/user_guide/images/assignment_list_released_jlab.png b/nbgrader/docs/source/user_guide/images/assignment_list_released_jlab.png
new file mode 100644
index 000000000..24efb76ea
Binary files /dev/null and b/nbgrader/docs/source/user_guide/images/assignment_list_released_jlab.png differ
diff --git a/nbgrader/docs/source/user_guide/images/assignment_list_submit_error.jpg b/nbgrader/docs/source/user_guide/images/assignment_list_submit_error.jpg
deleted file mode 100644
index 12cdb3639..000000000
Binary files a/nbgrader/docs/source/user_guide/images/assignment_list_submit_error.jpg and /dev/null differ
diff --git a/nbgrader/docs/source/user_guide/images/assignment_list_submit_error_jlab.png b/nbgrader/docs/source/user_guide/images/assignment_list_submit_error_jlab.png
new file mode 100644
index 000000000..3cf12a0a6
Binary files /dev/null and b/nbgrader/docs/source/user_guide/images/assignment_list_submit_error_jlab.png differ
diff --git a/nbgrader/docs/source/user_guide/images/assignment_list_submitted.png b/nbgrader/docs/source/user_guide/images/assignment_list_submitted.png
deleted file mode 100644
index a1c8ac39e..000000000
Binary files a/nbgrader/docs/source/user_guide/images/assignment_list_submitted.png and /dev/null differ
diff --git a/nbgrader/docs/source/user_guide/images/assignment_list_submitted_again.png b/nbgrader/docs/source/user_guide/images/assignment_list_submitted_again.png
deleted file mode 100644
index 885e424a2..000000000
Binary files a/nbgrader/docs/source/user_guide/images/assignment_list_submitted_again.png and /dev/null differ
diff --git a/nbgrader/docs/source/user_guide/images/assignment_list_submitted_again_jlab.png b/nbgrader/docs/source/user_guide/images/assignment_list_submitted_again_jlab.png
new file mode 100644
index 000000000..e93f7aee6
Binary files /dev/null and b/nbgrader/docs/source/user_guide/images/assignment_list_submitted_again_jlab.png differ
diff --git a/nbgrader/docs/source/user_guide/images/assignment_list_submitted_jlab.png b/nbgrader/docs/source/user_guide/images/assignment_list_submitted_jlab.png
new file mode 100644
index 000000000..9864f1a1d
Binary files /dev/null and b/nbgrader/docs/source/user_guide/images/assignment_list_submitted_jlab.png differ
diff --git a/nbgrader/docs/source/user_guide/images/assignment_list_validate_failed.png b/nbgrader/docs/source/user_guide/images/assignment_list_validate_failed.png
deleted file mode 100644
index bc439c05b..000000000
Binary files a/nbgrader/docs/source/user_guide/images/assignment_list_validate_failed.png and /dev/null differ
diff --git a/nbgrader/docs/source/user_guide/images/assignment_list_validate_failed_jlab.png b/nbgrader/docs/source/user_guide/images/assignment_list_validate_failed_jlab.png
new file mode 100644
index 000000000..1005e2277
Binary files /dev/null and b/nbgrader/docs/source/user_guide/images/assignment_list_validate_failed_jlab.png differ
diff --git a/nbgrader/docs/source/user_guide/images/assignment_list_validate_succeeded.png b/nbgrader/docs/source/user_guide/images/assignment_list_validate_succeeded.png
deleted file mode 100644
index 4cecab762..000000000
Binary files a/nbgrader/docs/source/user_guide/images/assignment_list_validate_succeeded.png and /dev/null differ
diff --git a/nbgrader/docs/source/user_guide/images/assignment_list_validate_succeeded_jlab.png b/nbgrader/docs/source/user_guide/images/assignment_list_validate_succeeded_jlab.png
new file mode 100644
index 000000000..72c3cf76c
Binary files /dev/null and b/nbgrader/docs/source/user_guide/images/assignment_list_validate_succeeded_jlab.png differ
diff --git a/nbgrader/docs/source/user_guide/images/assignment_toolbar.png b/nbgrader/docs/source/user_guide/images/assignment_toolbar.png
deleted file mode 100644
index 455125921..000000000
Binary files a/nbgrader/docs/source/user_guide/images/assignment_toolbar.png and /dev/null differ
diff --git a/nbgrader/docs/source/user_guide/images/assignment_toolbar1_jlab.png b/nbgrader/docs/source/user_guide/images/assignment_toolbar1_jlab.png
new file mode 100644
index 000000000..871b66b43
Binary files /dev/null and b/nbgrader/docs/source/user_guide/images/assignment_toolbar1_jlab.png differ
diff --git a/nbgrader/docs/source/user_guide/images/assignment_toolbar2_jlab.png b/nbgrader/docs/source/user_guide/images/assignment_toolbar2_jlab.png
new file mode 100644
index 000000000..adbfd21f0
Binary files /dev/null and b/nbgrader/docs/source/user_guide/images/assignment_toolbar2_jlab.png differ
diff --git a/nbgrader/docs/source/user_guide/images/autograde_assignment.png b/nbgrader/docs/source/user_guide/images/autograde_assignment.png
deleted file mode 100644
index 036c60f35..000000000
Binary files a/nbgrader/docs/source/user_guide/images/autograde_assignment.png and /dev/null differ
diff --git a/nbgrader/docs/source/user_guide/images/autograde_assignment_jlab.png b/nbgrader/docs/source/user_guide/images/autograde_assignment_jlab.png
new file mode 100644
index 000000000..130b0018c
Binary files /dev/null and b/nbgrader/docs/source/user_guide/images/autograde_assignment_jlab.png differ
diff --git a/nbgrader/docs/source/user_guide/images/autograded_answer.png b/nbgrader/docs/source/user_guide/images/autograded_answer.png
deleted file mode 100644
index 0e551e703..000000000
Binary files a/nbgrader/docs/source/user_guide/images/autograded_answer.png and /dev/null differ
diff --git a/nbgrader/docs/source/user_guide/images/autograded_answer_jlab.png b/nbgrader/docs/source/user_guide/images/autograded_answer_jlab.png
new file mode 100644
index 000000000..8c0ebcaea
Binary files /dev/null and b/nbgrader/docs/source/user_guide/images/autograded_answer_jlab.png differ
diff --git a/nbgrader/docs/source/user_guide/images/code_cell.png b/nbgrader/docs/source/user_guide/images/code_cell.png
deleted file mode 100644
index 0a709c636..000000000
Binary files a/nbgrader/docs/source/user_guide/images/code_cell.png and /dev/null differ
diff --git a/nbgrader/docs/source/user_guide/images/code_cell_jlab.png b/nbgrader/docs/source/user_guide/images/code_cell_jlab.png
new file mode 100644
index 000000000..e6bf986ed
Binary files /dev/null and b/nbgrader/docs/source/user_guide/images/code_cell_jlab.png differ
diff --git a/nbgrader/docs/source/user_guide/images/collect_assignment.png b/nbgrader/docs/source/user_guide/images/collect_assignment.png
deleted file mode 100644
index da3e53741..000000000
Binary files a/nbgrader/docs/source/user_guide/images/collect_assignment.png and /dev/null differ
diff --git a/nbgrader/docs/source/user_guide/images/collect_assignment_jlab.png b/nbgrader/docs/source/user_guide/images/collect_assignment_jlab.png
new file mode 100644
index 000000000..86a1c4494
Binary files /dev/null and b/nbgrader/docs/source/user_guide/images/collect_assignment_jlab.png differ
diff --git a/nbgrader/docs/source/user_guide/images/creating_assignment.gif b/nbgrader/docs/source/user_guide/images/creating_assignment.gif
index 2e1fb01d8..70460a82c 100644
Binary files a/nbgrader/docs/source/user_guide/images/creating_assignment.gif and b/nbgrader/docs/source/user_guide/images/creating_assignment.gif differ
diff --git a/nbgrader/docs/source/user_guide/images/formgrader.gif b/nbgrader/docs/source/user_guide/images/formgrader.gif
index 61d2858c1..fd6771a31 100644
Binary files a/nbgrader/docs/source/user_guide/images/formgrader.gif and b/nbgrader/docs/source/user_guide/images/formgrader.gif differ
diff --git a/nbgrader/docs/source/user_guide/images/formgrader_command_palette.png b/nbgrader/docs/source/user_guide/images/formgrader_command_palette.png
new file mode 100644
index 000000000..2848bfa53
Binary files /dev/null and b/nbgrader/docs/source/user_guide/images/formgrader_command_palette.png differ
diff --git a/nbgrader/docs/source/user_guide/images/formgrader_jlab_tab.png b/nbgrader/docs/source/user_guide/images/formgrader_jlab_tab.png
new file mode 100644
index 000000000..77b208ca0
Binary files /dev/null and b/nbgrader/docs/source/user_guide/images/formgrader_jlab_tab.png differ
diff --git a/nbgrader/docs/source/user_guide/images/formgrader_tab.png b/nbgrader/docs/source/user_guide/images/formgrader_tab.png
deleted file mode 100644
index b055380c6..000000000
Binary files a/nbgrader/docs/source/user_guide/images/formgrader_tab.png and /dev/null differ
diff --git a/nbgrader/docs/source/user_guide/images/generate_assignment.png b/nbgrader/docs/source/user_guide/images/generate_assignment.png
deleted file mode 100644
index e4d839db4..000000000
Binary files a/nbgrader/docs/source/user_guide/images/generate_assignment.png and /dev/null differ
diff --git a/nbgrader/docs/source/user_guide/images/generate_assignment_jlab.png b/nbgrader/docs/source/user_guide/images/generate_assignment_jlab.png
new file mode 100644
index 000000000..9d62e6830
Binary files /dev/null and b/nbgrader/docs/source/user_guide/images/generate_assignment_jlab.png differ
diff --git a/nbgrader/docs/source/user_guide/images/manage_assignments1.png b/nbgrader/docs/source/user_guide/images/manage_assignments1.png
deleted file mode 100644
index e16b9357d..000000000
Binary files a/nbgrader/docs/source/user_guide/images/manage_assignments1.png and /dev/null differ
diff --git a/nbgrader/docs/source/user_guide/images/manage_assignments1_jlab.png b/nbgrader/docs/source/user_guide/images/manage_assignments1_jlab.png
new file mode 100644
index 000000000..e3392ebe6
Binary files /dev/null and b/nbgrader/docs/source/user_guide/images/manage_assignments1_jlab.png differ
diff --git a/nbgrader/docs/source/user_guide/images/manage_assignments2.png b/nbgrader/docs/source/user_guide/images/manage_assignments2.png
deleted file mode 100644
index 7a8f6cb35..000000000
Binary files a/nbgrader/docs/source/user_guide/images/manage_assignments2.png and /dev/null differ
diff --git a/nbgrader/docs/source/user_guide/images/manage_assignments2_jlab.png b/nbgrader/docs/source/user_guide/images/manage_assignments2_jlab.png
new file mode 100644
index 000000000..cd64f4f88
Binary files /dev/null and b/nbgrader/docs/source/user_guide/images/manage_assignments2_jlab.png differ
diff --git a/nbgrader/docs/source/user_guide/images/manage_assignments3.png b/nbgrader/docs/source/user_guide/images/manage_assignments3.png
deleted file mode 100644
index e046c2023..000000000
Binary files a/nbgrader/docs/source/user_guide/images/manage_assignments3.png and /dev/null differ
diff --git a/nbgrader/docs/source/user_guide/images/manage_assignments3_jlab (copie).png b/nbgrader/docs/source/user_guide/images/manage_assignments3_jlab (copie).png
new file mode 100644
index 000000000..28d48fc9a
Binary files /dev/null and b/nbgrader/docs/source/user_guide/images/manage_assignments3_jlab (copie).png differ
diff --git a/nbgrader/docs/source/user_guide/images/manage_assignments3_jlab.png b/nbgrader/docs/source/user_guide/images/manage_assignments3_jlab.png
new file mode 100644
index 000000000..915185ff1
Binary files /dev/null and b/nbgrader/docs/source/user_guide/images/manage_assignments3_jlab.png differ
diff --git a/nbgrader/docs/source/user_guide/images/manage_assignments4.png b/nbgrader/docs/source/user_guide/images/manage_assignments4.png
deleted file mode 100644
index 75d16bd94..000000000
Binary files a/nbgrader/docs/source/user_guide/images/manage_assignments4.png and /dev/null differ
diff --git a/nbgrader/docs/source/user_guide/images/manage_assignments4_jlab.png b/nbgrader/docs/source/user_guide/images/manage_assignments4_jlab.png
new file mode 100644
index 000000000..3b075b5f1
Binary files /dev/null and b/nbgrader/docs/source/user_guide/images/manage_assignments4_jlab.png differ
diff --git a/nbgrader/docs/source/user_guide/images/manage_assignments5.png b/nbgrader/docs/source/user_guide/images/manage_assignments5.png
deleted file mode 100644
index 42902ebb3..000000000
Binary files a/nbgrader/docs/source/user_guide/images/manage_assignments5.png and /dev/null differ
diff --git a/nbgrader/docs/source/user_guide/images/manage_assignments5_jlab.png b/nbgrader/docs/source/user_guide/images/manage_assignments5_jlab.png
new file mode 100644
index 000000000..c896bb8b4
Binary files /dev/null and b/nbgrader/docs/source/user_guide/images/manage_assignments5_jlab.png differ
diff --git a/nbgrader/docs/source/user_guide/images/manage_assignments6.png b/nbgrader/docs/source/user_guide/images/manage_assignments6.png
deleted file mode 100644
index a407e3aa1..000000000
Binary files a/nbgrader/docs/source/user_guide/images/manage_assignments6.png and /dev/null differ
diff --git a/nbgrader/docs/source/user_guide/images/manage_assignments6_jlab.png b/nbgrader/docs/source/user_guide/images/manage_assignments6_jlab.png
new file mode 100644
index 000000000..f7f1d7160
Binary files /dev/null and b/nbgrader/docs/source/user_guide/images/manage_assignments6_jlab.png differ
diff --git a/nbgrader/docs/source/user_guide/images/manage_assignments7.png b/nbgrader/docs/source/user_guide/images/manage_assignments7.png
deleted file mode 100644
index 3c065396a..000000000
Binary files a/nbgrader/docs/source/user_guide/images/manage_assignments7.png and /dev/null differ
diff --git a/nbgrader/docs/source/user_guide/images/manage_assignments7_jlab.png b/nbgrader/docs/source/user_guide/images/manage_assignments7_jlab.png
new file mode 100644
index 000000000..f219e7adc
Binary files /dev/null and b/nbgrader/docs/source/user_guide/images/manage_assignments7_jlab.png differ
diff --git a/nbgrader/docs/source/user_guide/images/manage_assignments8.png b/nbgrader/docs/source/user_guide/images/manage_assignments8.png
deleted file mode 100644
index 1b83f73df..000000000
Binary files a/nbgrader/docs/source/user_guide/images/manage_assignments8.png and /dev/null differ
diff --git a/nbgrader/docs/source/user_guide/images/manage_assignments8_jlab.png b/nbgrader/docs/source/user_guide/images/manage_assignments8_jlab.png
new file mode 100644
index 000000000..c9710a962
Binary files /dev/null and b/nbgrader/docs/source/user_guide/images/manage_assignments8_jlab.png differ
diff --git a/nbgrader/docs/source/user_guide/images/manage_assignments_jlab (copie).png b/nbgrader/docs/source/user_guide/images/manage_assignments_jlab (copie).png
new file mode 100644
index 000000000..bc011fa13
Binary files /dev/null and b/nbgrader/docs/source/user_guide/images/manage_assignments_jlab (copie).png differ
diff --git a/nbgrader/docs/source/user_guide/images/manage_submissions1.png b/nbgrader/docs/source/user_guide/images/manage_submissions1.png
deleted file mode 100644
index df736fed3..000000000
Binary files a/nbgrader/docs/source/user_guide/images/manage_submissions1.png and /dev/null differ
diff --git a/nbgrader/docs/source/user_guide/images/manage_submissions1_jlab (copie).png b/nbgrader/docs/source/user_guide/images/manage_submissions1_jlab (copie).png
new file mode 100644
index 000000000..0dd26cf26
Binary files /dev/null and b/nbgrader/docs/source/user_guide/images/manage_submissions1_jlab (copie).png differ
diff --git a/nbgrader/docs/source/user_guide/images/manage_submissions1_jlab.png b/nbgrader/docs/source/user_guide/images/manage_submissions1_jlab.png
new file mode 100644
index 000000000..2051d548e
Binary files /dev/null and b/nbgrader/docs/source/user_guide/images/manage_submissions1_jlab.png differ
diff --git a/nbgrader/docs/source/user_guide/images/manage_submissions2.png b/nbgrader/docs/source/user_guide/images/manage_submissions2.png
deleted file mode 100644
index f6080f108..000000000
Binary files a/nbgrader/docs/source/user_guide/images/manage_submissions2.png and /dev/null differ
diff --git a/nbgrader/docs/source/user_guide/images/manage_submissions2_jlab.png b/nbgrader/docs/source/user_guide/images/manage_submissions2_jlab.png
new file mode 100644
index 000000000..91343b2ff
Binary files /dev/null and b/nbgrader/docs/source/user_guide/images/manage_submissions2_jlab.png differ
diff --git a/nbgrader/docs/source/user_guide/images/manually_graded_answer.png b/nbgrader/docs/source/user_guide/images/manually_graded_answer.png
deleted file mode 100644
index a822557dc..000000000
Binary files a/nbgrader/docs/source/user_guide/images/manually_graded_answer.png and /dev/null differ
diff --git a/nbgrader/docs/source/user_guide/images/manually_graded_answer_jlab.png b/nbgrader/docs/source/user_guide/images/manually_graded_answer_jlab.png
new file mode 100644
index 000000000..4ff55f2cc
Binary files /dev/null and b/nbgrader/docs/source/user_guide/images/manually_graded_answer_jlab.png differ
diff --git a/nbgrader/docs/source/user_guide/images/markdown_cell.png b/nbgrader/docs/source/user_guide/images/markdown_cell.png
deleted file mode 100644
index 35c31a2b8..000000000
Binary files a/nbgrader/docs/source/user_guide/images/markdown_cell.png and /dev/null differ
diff --git a/nbgrader/docs/source/user_guide/images/markdown_cell_jlab.png b/nbgrader/docs/source/user_guide/images/markdown_cell_jlab.png
new file mode 100644
index 000000000..b59db1246
Binary files /dev/null and b/nbgrader/docs/source/user_guide/images/markdown_cell_jlab.png differ
diff --git a/nbgrader/docs/source/user_guide/images/read_only.png b/nbgrader/docs/source/user_guide/images/read_only.png
deleted file mode 100644
index 4203a4067..000000000
Binary files a/nbgrader/docs/source/user_guide/images/read_only.png and /dev/null differ
diff --git a/nbgrader/docs/source/user_guide/images/read_only_jlab.png b/nbgrader/docs/source/user_guide/images/read_only_jlab.png
new file mode 100644
index 000000000..e7f9f22d0
Binary files /dev/null and b/nbgrader/docs/source/user_guide/images/read_only_jlab.png differ
diff --git a/nbgrader/docs/source/user_guide/images/release_assignment.png b/nbgrader/docs/source/user_guide/images/release_assignment.png
deleted file mode 100644
index 75de8a2d0..000000000
Binary files a/nbgrader/docs/source/user_guide/images/release_assignment.png and /dev/null differ
diff --git a/nbgrader/docs/source/user_guide/images/release_assignment_jlab.png b/nbgrader/docs/source/user_guide/images/release_assignment_jlab.png
new file mode 100644
index 000000000..aa29ecf20
Binary files /dev/null and b/nbgrader/docs/source/user_guide/images/release_assignment_jlab.png differ
diff --git a/nbgrader/docs/source/user_guide/images/student_assignment.gif b/nbgrader/docs/source/user_guide/images/student_assignment.gif
index 9e6ba7c3c..a786a9159 100644
Binary files a/nbgrader/docs/source/user_guide/images/student_assignment.gif and b/nbgrader/docs/source/user_guide/images/student_assignment.gif differ
diff --git a/nbgrader/docs/source/user_guide/images/task-cell-source.png b/nbgrader/docs/source/user_guide/images/task-cell-source.png
deleted file mode 100644
index 61a44eb10..000000000
Binary files a/nbgrader/docs/source/user_guide/images/task-cell-source.png and /dev/null differ
diff --git a/nbgrader/docs/source/user_guide/images/task_cell_source_jlab.png b/nbgrader/docs/source/user_guide/images/task_cell_source_jlab.png
new file mode 100644
index 000000000..edfc49a22
Binary files /dev/null and b/nbgrader/docs/source/user_guide/images/task_cell_source_jlab.png differ
diff --git a/nbgrader/docs/source/user_guide/images/validate_extension.png b/nbgrader/docs/source/user_guide/images/validate_extension.png
deleted file mode 100644
index 232f0e36c..000000000
Binary files a/nbgrader/docs/source/user_guide/images/validate_extension.png and /dev/null differ
diff --git a/nbgrader/docs/source/user_guide/images/validate_extension_jlab.png b/nbgrader/docs/source/user_guide/images/validate_extension_jlab.png
new file mode 100644
index 000000000..9bc339f2b
Binary files /dev/null and b/nbgrader/docs/source/user_guide/images/validate_extension_jlab.png differ
diff --git a/nbgrader/docs/source/user_guide/images/validate_failed.png b/nbgrader/docs/source/user_guide/images/validate_failed.png
deleted file mode 100644
index cdb236b4e..000000000
Binary files a/nbgrader/docs/source/user_guide/images/validate_failed.png and /dev/null differ
diff --git a/nbgrader/docs/source/user_guide/images/validate_failed_jlab.png b/nbgrader/docs/source/user_guide/images/validate_failed_jlab.png
new file mode 100644
index 000000000..b7834c0c6
Binary files /dev/null and b/nbgrader/docs/source/user_guide/images/validate_failed_jlab.png differ
diff --git a/nbgrader/docs/source/user_guide/images/validate_success.png b/nbgrader/docs/source/user_guide/images/validate_success.png
deleted file mode 100644
index 712f9a8e7..000000000
Binary files a/nbgrader/docs/source/user_guide/images/validate_success.png and /dev/null differ
diff --git a/nbgrader/docs/source/user_guide/images/validate_success_jlab.png b/nbgrader/docs/source/user_guide/images/validate_success_jlab.png
new file mode 100644
index 000000000..53b9d9b4e
Binary files /dev/null and b/nbgrader/docs/source/user_guide/images/validate_success_jlab.png differ
diff --git a/nbgrader/docs/source/user_guide/installation.rst b/nbgrader/docs/source/user_guide/installation.rst
index 16ca1a700..bd313bd12 100644
--- a/nbgrader/docs/source/user_guide/installation.rst
+++ b/nbgrader/docs/source/user_guide/installation.rst
@@ -14,70 +14,65 @@ Or, if you use `Anaconda <https://www.anaconda.com/download>`__::
     conda install jupyter
     conda install -c conda-forge nbgrader
 
-nbgrader extensions
--------------------
 
-**Take note:** If you install nbgrader via `Anaconda
-<https://www.anaconda.com/download>`__ the nbgrader extensions will be
-installed and enabled for you upon installation. See the `Installation
-options`_ and `Disabling extensions`_ sections below for more information on
-changing the default installation option ``--sys-prefix`` or disabling one or
-more extensions.
+nbgrader extensions in Jupyter Lab
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-Installing and activating extensions
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Extensions will be automatically installed after the installation of nbgrader.
 
-You can install the nbgrader extensions for Jupyter notebook. Previously
-this was done using the ``nbgrader extension install`` command. However, moving
-forward this is done using the ``jupyter nbextension`` and ``jupyter
-serverextension`` commands.
+The installation will activated 4 server extensions
+(formgrader, assignment_list, course_list and validate_assignment)
+and 5 labextensions (formgrader, assignment_list, course_list, validate_assignment and create_assignment).
 
-To install and enable all nbextensions (**assignment list**, **create
-assignment**, **formgrader**, and **validate**) run::
+The server extensions can be disabled individually by running::
 
-    jupyter nbextension install --sys-prefix --py nbgrader --overwrite
-    jupyter nbextension enable --sys-prefix --py nbgrader
-    jupyter serverextension enable --sys-prefix --py nbgrader
+    jupyter server extension disable nbgrader.server_extensions.formgrader
+    jupyter server extension disable nbgrader.server_extensions.assignment_list
+    jupyter server extension disable nbgrader.server_extensions.course_list
+    jupyter server extension disable nbgrader.server_extensions.validate_assignment
 
-To work properly, the **assignment list**, **formgrader**, and **validate**
-extensions require both the nbextension and serverextension. The **create
-assignment** extension only has an nbextension part.
+The labextensions are all enabled by default, but can be disabled individually by running::
+
+    jupyter labextension disable nbgrader/formgrader
+    jupyter labextension disable nbgrader/assignment-list
+    jupyter labextension disable nbgrader/course-list
+    jupyter labextension disable nbgrader/create-assignment
+    jupyter labextension disable nbgrader/validate-assignment
+
+or enabled::
+
+    jupyter labextension enable nbgrader/formgrader
+    jupyter labextension enable nbgrader/assignment-list
+    jupyter labextension enable nbgrader/course-list
+    jupyter labextension enable nbgrader/create-assignment
+    jupyter labextension enable nbgrader/validate-assignment
+
+To work properly, the **assignment list**, **formgrader**, **course list** and **validate assignment**
+extensions require both the labextension and server extension. The **create
+assignment** extension only has an labextension part.
 
 Installation options
 ~~~~~~~~~~~~~~~~~~~~
 
-When installed/enabled with the ``--sys-prefix`` option, the nbextensions and
-serverextension will be installed and enabled for anyone using the particular
+When installed/enabled with the ``--sys-prefix`` option, the
+server extension will be installed and enabled for anyone using the particular
 Python installation or conda environment where nbgrader is installed. If that
 Python installation is available system-wide, all users will immediately be
 able to use the nbgrader extensions.
 
 There are a number of ways you may need to customize the installation:
 
--  To install or enable the nbextensions/serverextension for just the
+-  To install or enable the labextensions/server extension for just the
    current user, run the above commands with ``--user`` instead of ``--sys-prefix``::
 
-    jupyter nbextension install --user --py nbgrader --overwrite
-    jupyter nbextension enable --user --py nbgrader
-    jupyter serverextension enable --user --py nbgrader
+    jupyter labextension enable --level=user nbgrader
+    jupyter server extension enable --user --py nbgrader
 
--  To install or enable the nbextensions/serverextension for all
+-  To install or enable the labextensions/server extension for all
    Python installations on the system, run the above commands with ``--system`` instead of ``--sys-prefix``::
 
-    jupyter nbextension install --system --py nbgrader --overwrite
-    jupyter nbextension enable --system --py nbgrader
-    jupyter serverextension enable --system --py nbgrader
-
--  You can also use the ``--overwrite`` option along with the ``jupyter
-   nbextension install`` command to overwrite existing nbgrader extension
-   installation files, typically used when updating nbgrader, for
-   example::
-
-    jupyter nbextension install --sys-prefix --overwrite --py nbgrader
-
-Previous versions of nbgrader required each user on a system to enable the
-nbextensions; this is no longer needed if the ``--sys-prefix`` option is used
-for a system-wide python or the ``--system`` option is used.
+    jupyter labextension enable --level=system nbgrader
+    jupyter server extension enable --system --py nbgrader
 
 Disabling extensions
 ~~~~~~~~~~~~~~~~~~~~
@@ -86,47 +81,47 @@ You may want to only install one of the nbgrader extensions. To do this, follow
 the above steps to install everything and then disable the extension you don't
 need. For example, to disable the Assignment List extension::
 
-    jupyter nbextension disable --sys-prefix assignment_list/main --section=tree
-    jupyter serverextension disable --sys-prefix nbgrader.server_extensions.assignment_list
+    jupyter labextension disable --level=sys_prefix nbgrader/assignment_list
+    jupyter server extension disable --sys-prefix nbgrader.server_extensions.assignment_list
 
 or to disable the Create Assignment extension::
 
-    jupyter nbextension disable --sys-prefix create_assignment/main
+    jupyter labextension disable --level=sys_prefix nbgrader/create_assignment
 
 or to disable the Formgrader extension::
 
-    jupyter nbextension disable --sys-prefix formgrader/main --section=tree
-    jupyter serverextension disable --sys-prefix nbgrader.server_extensions.formgrader
+    jupyter labextension disable --level=sys_prefix nbgrader/formgrader
+    jupyter server extension disable --sys-prefix nbgrader.server_extensions.formgrader
 
 or to disable the Course List extension::
 
-    jupyter nbextension disable --sys-prefix course_list/main --section=tree
-    jupyter serverextension disable --sys-prefix nbgrader.server_extensions.course_list
+    jupyter labextension disable --level=sys_prefix nbgrader/course_list
+    jupyter server extension disable --sys-prefix nbgrader.server_extensions.course_list
 
 For example lets assume you have installed nbgrader via `Anaconda
-<https://www.anaconda.com/download>`__ (meaning all extensions are installed
+<https://www.anaconda.com/download>`__ (meaning all serverextensions are installed
 and enabled with the ``--sys-prefix`` flag, i.e. anyone using the particular
 Python installation or conda environment where nbgrader is installed). But you
 only want the *create assignment* extension available to a specific user and
 not everyone else. First you will need to disable the *create assignment*
 extension for everyone else::
 
-    jupyter nbextension disable --sys-prefix create_assignment/main
+    jupyter labextension disable nbgrader/create_assignment
 
 Log in with the specific user and then enable the *create assignment* extension
 only for that user::
 
-    jupyter nbextension enable --user create_assignment/main
+    jupyter labextension enable --level=user nbgrader/create_assignment
 
-Finally to see all installed nbextensions/server extensions, run::
+Finally to see all installed labextensions/server extensions, run::
 
-    jupyter nbextension list
-    jupyter serverextension list
+    jupyter labextension list
+    jupyter server extension list
 
 For further documentation on these commands run::
 
-    jupyter nbextension --help-all
-    jupyter serverextension --help-all
+    jupyter labextension --help-all
+    jupyter server extension --help-all
 
 For advanced instructions on installing the *assignment list* extension please
 see the :ref:`advanced installation instructions<assignment-list-installation>`.
diff --git a/nbgrader/docs/source/user_guide/managing_assignment_files.ipynb b/nbgrader/docs/source/user_guide/managing_assignment_files.ipynb
index 9cdef2c61..49a0b52d9 100644
--- a/nbgrader/docs/source/user_guide/managing_assignment_files.ipynb
+++ b/nbgrader/docs/source/user_guide/managing_assignment_files.ipynb
@@ -43,7 +43,11 @@
   {
    "cell_type": "code",
    "execution_count": 1,
-   "metadata": {},
+   "metadata": {
+    "vscode": {
+     "languageId": "python"
+    }
+   },
    "outputs": [
     {
      "name": "stdout",
@@ -72,7 +76,11 @@
   {
    "cell_type": "code",
    "execution_count": 2,
-   "metadata": {},
+   "metadata": {
+    "vscode": {
+     "languageId": "python"
+    }
+   },
    "outputs": [],
    "source": [
     "%%bash\n",
@@ -127,16 +135,16 @@
    "source": [
     "Using the formgrader extension, you may release assignments by clicking on the \"release\" button:\n",
     "\n",
-    "![](images/manage_assignments5.png)\n",
+    "![manage assignments release](images/manage_assignments5_jlab.png)\n",
     "\n",
     "**Note** that for the \"release\" button to become available, the `course_id` option must be set in `nbgrader_config.py`.\n",
     "Once completed, you will see a pop-up window with log output:\n",
     "\n",
-    "![](images/release_assignment.png)\n",
+    "![release assignment](images/release_assignment_jlab.png)\n",
     "\n",
     "If you decide you want to \"un-release\" an assignment, you may do so by clicking again on the \"release\" button (which is now an \"x\"). **However, note that students who have already downloaded the assignment will still have access to their downloaded copy. Unreleasing an assignment only prevents more students from downloading it.**\n",
     "\n",
-    "![](images/manage_assignments6.png)"
+    "![manage assignments un-release](images/manage_assignments6_jlab.png)"
    ]
   },
   {
@@ -156,7 +164,11 @@
   {
    "cell_type": "code",
    "execution_count": 3,
-   "metadata": {},
+   "metadata": {
+    "vscode": {
+     "languageId": "python"
+    }
+   },
    "outputs": [
     {
      "name": "stderr",
@@ -184,7 +196,11 @@
   {
    "cell_type": "code",
    "execution_count": 4,
-   "metadata": {},
+   "metadata": {
+    "vscode": {
+     "languageId": "python"
+    }
+   },
    "outputs": [
     {
      "name": "stderr",
@@ -248,7 +264,11 @@
   {
    "cell_type": "code",
    "execution_count": 5,
-   "metadata": {},
+   "metadata": {
+    "vscode": {
+     "languageId": "python"
+    }
+   },
    "outputs": [],
    "source": [
     "%%bash\n",
@@ -270,7 +290,11 @@
   {
    "cell_type": "code",
    "execution_count": 6,
-   "metadata": {},
+   "metadata": {
+    "vscode": {
+     "languageId": "python"
+    }
+   },
    "outputs": [
     {
      "name": "stdout",
@@ -304,25 +328,35 @@
     "  The \"Assignment List\" extension is not fully compatible with multiple\n",
     "  courses on the same server. Please see :ref:`multiple-classes` for details.\n",
     "\n",
-    "Alternatively, students can fetch assignments using the assignment list notebook server extension. You must have installed the extension by following the instructions :doc:`here </user_guide/installation>`, after which you should see an \"Assignments\" tab in dashboard:"
+    "Alternatively, students can fetch assignments using the assignment list notebook server extension. You must have installed the extension by following the instructions :doc:`here </user_guide/installation>`."
    ]
   },
   {
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "![](images/assignment_list_released.png)"
+    "\n",
+    "After which you can open \"Assignments\" tab from Jupyter Lab command palette (`Command/Ctrl + Shift + c`) and typing \"Assignment List\":\n",
+    "\n",
+    "![assignment list command palette](images/assignment_list_command_palette.png)\n"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "![assignment list released](images/assignment_list_released_jlab.png)"
    ]
   },
   {
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "The image above shows that there has been one assignment released (\"ps1\") for the class \"example_course\". To get this assignment, students can click the \"Fetch\" button (analogous to running `nbgrader fetch_assignment ps1 --course example_course`. **Note: this assumes nbgrader is always run from the root of the notebook server, which on JupyterHub is most likely the root of the user's home directory.**\n",
+    "The image above shows that there has been one assignment released (\"Problem Set 1\") for the class \"example_course\". To get this assignment, students can click the \"Fetch\" button (analogous to running `nbgrader fetch_assignment 'Problem Set 1' --course example_course`. **Note: this assumes nbgrader is always run from the root of the notebook server, which on JupyterHub is most likely the root of the user's home directory.**\n",
     "\n",
     "After the assignment is fetched, it will appear in the list of \"Downloaded assignments\":\n",
     "\n",
-    "![](images/assignment_list_downloaded.png)"
+    "![assignment list downloaded](images/assignment_list_downloaded_jlab.png)"
    ]
   },
   {
@@ -331,9 +365,9 @@
    "source": [
     "Students can click on the name of the assignment to expand it and see all the notebooks in the assignment:\n",
     "\n",
-    "![](images/assignment_list_downloaded_expanded.png)\n",
+    "![assignment list downloaded expanded](images/assignment_list_downloaded_expanded_jlab.png)\n",
     "\n",
-    "Clicking on a particular notebook will open it in a new tab in the browser."
+    "Clicking on a particular notebook will open it in a new tab in Jupyter Lab."
    ]
   },
   {
@@ -353,7 +387,11 @@
   {
    "cell_type": "code",
    "execution_count": 7,
-   "metadata": {},
+   "metadata": {
+    "vscode": {
+     "languageId": "python"
+    }
+   },
    "outputs": [
     {
      "name": "stderr",
@@ -381,7 +419,11 @@
   {
    "cell_type": "code",
    "execution_count": 8,
-   "metadata": {},
+   "metadata": {
+    "vscode": {
+     "languageId": "python"
+    }
+   },
    "outputs": [
     {
      "name": "stderr",
@@ -410,7 +452,11 @@
   {
    "cell_type": "code",
    "execution_count": 9,
-   "metadata": {},
+   "metadata": {
+    "vscode": {
+     "languageId": "python"
+    }
+   },
    "outputs": [
     {
      "name": "stdout",
@@ -463,7 +509,7 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "### From the notebook dashboard"
+    "### From the Jupyter Lab dashboard"
    ]
   },
   {
@@ -484,7 +530,7 @@
    "source": [
     "After students have worked on the assignment for a while, but before submitting, they can validate that their notebooks pass the tests by clicking the \"Validate\" button (analogous to running `nbgrader validate`). If any tests fail, they will see a warning:\n",
     "\n",
-    "![](images/assignment_list_validate_failed.png)"
+    "![assignment list validate failed](images/assignment_list_validate_failed_jlab.png)"
    ]
   },
   {
@@ -493,7 +539,7 @@
    "source": [
     "If there are no errors, they will see that the validation passes:\n",
     "\n",
-    "![](images/assignment_list_validate_succeeded.png)"
+    "![assignment list validate succeeded](images/assignment_list_validate_succeeded_jlab.png)"
    ]
   },
   {
@@ -513,7 +559,7 @@
    "source": [
     "Once students have validated all the notebooks, they can click the \"Submit\" button to submit the assignment (analogous to running `nbgrader submit ps1 --course example_course`). Afterwards, it will show up in the list of submitted assignments (and also still in the list of downloaded assignments):\n",
     "\n",
-    "![](images/assignment_list_submitted.png)"
+    "![assignment list submitted](images/assignment_list_submitted_jlab.png)"
    ]
   },
   {
@@ -522,7 +568,7 @@
    "source": [
     "Students may submit an assignment as many times as they'd like. All copies of a submission will show up in the submitted assignments list, and when the instructor collects the assignments, they will get the most recent version of the assignment:\n",
     "\n",
-    "![](images/assignment_list_submitted_again.png)"
+    "![assignment list submitted again](images/assignment_list_submitted_again_jlab.png)"
    ]
   },
   {
@@ -531,7 +577,7 @@
    "source": [
     "Similarly, if the ``strict`` option (in the student's ``nbgrader_config.py`` file) is set to ``True``, the students will not be able to submit an assignment with missing notebooks (for a given assignment):\n",
     "\n",
-    "![](images/assignment_list_submit_error.jpg)"
+    "![assignment list submit error](images/assignment_list_submit_error_jlab.png)"
    ]
   },
   {
@@ -551,7 +597,11 @@
   {
    "cell_type": "code",
    "execution_count": 10,
-   "metadata": {},
+   "metadata": {
+    "vscode": {
+     "languageId": "python"
+    }
+   },
    "outputs": [
     {
      "name": "stdout",
@@ -580,7 +630,11 @@
   {
    "cell_type": "code",
    "execution_count": 11,
-   "metadata": {},
+   "metadata": {
+    "vscode": {
+     "languageId": "python"
+    }
+   },
    "outputs": [
     {
      "name": "stderr",
@@ -609,14 +663,18 @@
   {
    "cell_type": "code",
    "execution_count": 12,
-   "metadata": {},
+   "metadata": {
+    "vscode": {
+     "languageId": "python"
+    }
+   },
    "outputs": [
     {
      "name": "stdout",
      "output_type": "stream",
      "text": [
       "total ##\n",
-      "-rw-r--r-- 1 nb_user nb_group [size] [date] [time] nbgrader_config.py\n",
+      "-rw-rw-r-- 1 nb_user nb_group [size] [date] [time] nbgrader_config.py\n",
       "drwxr-xr-x 1 nb_user nb_group [size] [date] [time] ps1\n"
      ]
     }
@@ -638,7 +696,11 @@
   {
    "cell_type": "code",
    "execution_count": 13,
-   "metadata": {},
+   "metadata": {
+    "vscode": {
+     "languageId": "python"
+    }
+   },
    "outputs": [
     {
      "name": "stderr",
@@ -666,7 +728,11 @@
   {
    "cell_type": "code",
    "execution_count": 14,
-   "metadata": {},
+   "metadata": {
+    "vscode": {
+     "languageId": "python"
+    }
+   },
    "outputs": [
     {
      "name": "stderr",
@@ -695,7 +761,11 @@
   {
    "cell_type": "code",
    "execution_count": 15,
-   "metadata": {},
+   "metadata": {
+    "vscode": {
+     "languageId": "python"
+    }
+   },
    "outputs": [
     {
      "name": "stderr",
@@ -731,7 +801,11 @@
   {
    "cell_type": "code",
    "execution_count": 16,
-   "metadata": {},
+   "metadata": {
+    "vscode": {
+     "languageId": "python"
+    }
+   },
    "outputs": [
     {
      "name": "stderr",
@@ -777,7 +851,11 @@
   {
    "cell_type": "code",
    "execution_count": 17,
-   "metadata": {},
+   "metadata": {
+    "vscode": {
+     "languageId": "python"
+    }
+   },
    "outputs": [
     {
      "name": "stdout",
@@ -802,7 +880,10 @@
    "metadata": {
     "tags": [
      "raises-exception"
-    ]
+    ],
+    "vscode": {
+     "languageId": "python"
+    }
    },
    "outputs": [
     {
@@ -868,7 +949,11 @@
   {
    "cell_type": "code",
    "execution_count": 19,
-   "metadata": {},
+   "metadata": {
+    "vscode": {
+     "languageId": "python"
+    }
+   },
    "outputs": [
     {
      "name": "stdout",
@@ -901,15 +986,15 @@
    "source": [
     "From the formgrader extension, we can collect submissions by clicking on the \"collect\" button:\n",
     "\n",
-    "![](images/manage_assignments7.png)\n",
+    "![manage assignments collect](images/manage_assignments7_jlab.png)\n",
     "\n",
     "As with releasing, this will display a pop-up window when the operation is complete, telling you how many submissions were collected:\n",
     "\n",
-    "![](images/collect_assignment.png)\n",
+    "![collect assignment](images/collect_assignment_jlab.png)\n",
     "\n",
     "From here, you can click on the number of submissions to grade the collected submissions:\n",
     "\n",
-    "![](images/manage_assignments8.png)"
+    "![manage assignments grade collected](images/manage_assignments8_jlab.png)"
    ]
   },
   {
@@ -929,7 +1014,11 @@
   {
    "cell_type": "code",
    "execution_count": 20,
-   "metadata": {},
+   "metadata": {
+    "vscode": {
+     "languageId": "python"
+    }
+   },
    "outputs": [
     {
      "name": "stderr",
@@ -958,7 +1047,11 @@
   {
    "cell_type": "code",
    "execution_count": 21,
-   "metadata": {},
+   "metadata": {
+    "vscode": {
+     "languageId": "python"
+    }
+   },
    "outputs": [
     {
      "name": "stderr",
@@ -985,16 +1078,20 @@
   {
    "cell_type": "code",
    "execution_count": 22,
-   "metadata": {},
+   "metadata": {
+    "vscode": {
+     "languageId": "python"
+    }
+   },
    "outputs": [
     {
      "name": "stdout",
      "output_type": "stream",
      "text": [
       "total ##\n",
-      "drwxr-xr-x 1 nb_user nb_group [size] [date] [time] bitdiddle\n",
-      "drwxr-xr-x 1 nb_user nb_group [size] [date] [time] hacker\n",
-      "drwxr-xr-x 1 nb_user nb_group [size] [date] [time] nb_user\n"
+      "drwxrwxr-x 1 nb_user nb_group [size] [date] [time] bitdiddle\n",
+      "drwxrwxr-x 1 nb_user nb_group [size] [date] [time] hacker\n",
+      "drwxrwxr-x 1 nb_user nb_group [size] [date] [time] nb_user\n"
      ]
     }
    ],
@@ -1020,5 +1117,5 @@
   }
  },
  "nbformat": 4,
- "nbformat_minor": 1
+ "nbformat_minor": 4
 }
diff --git a/nbgrader/docs/source/user_guide/managing_assignment_files_manually.ipynb b/nbgrader/docs/source/user_guide/managing_assignment_files_manually.ipynb
index 0978c6ce6..0ca98be97 100644
--- a/nbgrader/docs/source/user_guide/managing_assignment_files_manually.ipynb
+++ b/nbgrader/docs/source/user_guide/managing_assignment_files_manually.ipynb
@@ -183,7 +183,8 @@
      "output_type": "stream",
      "text": [
       "total ##\n",
-      "-rw-r--r-- 1 nb_user nb_group [size] [date] [time] notebooks.zip\n",
+      "-rw-rw-r-- 1 nb_user nb_group [size] [date] [time] jupyter.png\n",
+      "-rw-rw-r-- 1 nb_user nb_group [size] [date] [time] notebooks.zip\n",
       "-rw------- 1 nb_user nb_group [size] [date] [time] ps1_hacker_attempt_2016-01-30-20-30-10_problem1.ipynb\n"
      ]
     }
@@ -256,7 +257,8 @@
      "output_type": "stream",
      "text": [
       "total ##\n",
-      "-rw-r--r-- 1 nb_user nb_group [size] [date] [time] notebooks.zip\n",
+      "-rw-rw-r-- 1 nb_user nb_group [size] [date] [time] jupyter.png\n",
+      "-rw-rw-r-- 1 nb_user nb_group [size] [date] [time] notebooks.zip\n",
       "-rw------- 1 nb_user nb_group [size] [date] [time] ps1_hacker_attempt_2016-01-30-20-30-10_problem1.ipynb\n"
      ]
     }
@@ -352,11 +354,15 @@
       "[ZipCollectApp | INFO] Using file extractor: ExtractorPlugin\n",
       "[ZipCollectApp | INFO] Using file collector: FileNameCollectorPlugin\n",
       "[ZipCollectApp | WARNING] Directory not found. Creating: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/downloaded/ps1/extracted\n",
-      "[ZipCollectApp | INFO] Copying from: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/downloaded/ps1/archive/ps1_hacker_attempt_2016-01-30-20-30-10_problem1.ipynb\n",
-      "[ZipCollectApp | INFO]   Copying to: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/downloaded/ps1/extracted/ps1_hacker_attempt_2016-01-30-20-30-10_problem1.ipynb\n",
+      "[ZipCollectApp | INFO] Copying from: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/downloaded/ps1/archive/jupyter.png\n",
+      "[ZipCollectApp | INFO]   Copying to: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/downloaded/ps1/extracted/jupyter.png\n",
       "[ZipCollectApp | INFO] Extracting from: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/downloaded/ps1/archive/notebooks.zip\n",
       "[ZipCollectApp | INFO]   Extracting to: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/downloaded/ps1/extracted/notebooks\n",
+      "[ZipCollectApp | INFO] Copying from: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/downloaded/ps1/archive/ps1_hacker_attempt_2016-01-30-20-30-10_problem1.ipynb\n",
+      "[ZipCollectApp | INFO]   Copying to: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/downloaded/ps1/extracted/ps1_hacker_attempt_2016-01-30-20-30-10_problem1.ipynb\n",
       "[ZipCollectApp | INFO] Start collecting files...\n",
+      "[ZipCollectApp | INFO] Parsing file: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/downloaded/ps1/extracted/jupyter.png\n",
+      "[ZipCollectApp | WARNING] Skipped submission with no match information provided: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/downloaded/ps1/extracted/jupyter.png\n",
       "[ZipCollectApp | INFO] Parsing file: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/downloaded/ps1/extracted/notebooks/ps1_bitdiddle_attempt_2016-01-30-15-30-10_jupyter.png\n",
       "[ZipCollectApp | WARNING] Skipped submission with no match information provided: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/downloaded/ps1/extracted/notebooks/ps1_bitdiddle_attempt_2016-01-30-15-30-10_jupyter.png\n",
       "[ZipCollectApp | INFO] Parsing file: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/downloaded/ps1/extracted/notebooks/ps1_bitdiddle_attempt_2016-01-30-15-30-10_problem1.ipynb\n",
@@ -367,7 +373,7 @@
       "[ZipCollectApp | WARNING] Skipped notebook with invalid name 'myproblem1.ipynb'\n",
       "[ZipCollectApp | INFO] Parsing file: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/downloaded/ps1/extracted/notebooks/ps1_hacker_attempt_2016-01-30-16-30-10_problem2.ipynb\n",
       "[ZipCollectApp | INFO] Parsing file: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/downloaded/ps1/extracted/ps1_hacker_attempt_2016-01-30-20-30-10_problem1.ipynb\n",
-      "[ZipCollectApp | WARNING] 4 files collected, 3 files skipped\n",
+      "[ZipCollectApp | WARNING] 4 files collected, 4 files skipped\n",
       "[ZipCollectApp | INFO] Start transfering files...\n",
       "[ZipCollectApp | WARNING] Directory not found. Creating: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/submitted_zip/bitdiddle/ps1\n",
       "[ZipCollectApp | INFO] Copying from: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/downloaded/ps1/extracted/notebooks/ps1_bitdiddle_attempt_2016-01-30-15-30-10_problem1.ipynb\n",
@@ -407,7 +413,8 @@
      "output_type": "stream",
      "text": [
       "total ##\n",
-      "drwxr-xr-x 1 nb_user nb_group [size] [date] [time] notebooks\n",
+      "-rw-rw-r-- 1 nb_user nb_group [size] [date] [time] jupyter.png\n",
+      "drwxrwxr-x 1 nb_user nb_group [size] [date] [time] notebooks\n",
       "-rw------- 1 nb_user nb_group [size] [date] [time] ps1_hacker_attempt_2016-01-30-20-30-10_problem1.ipynb\n",
       "total ##\n",
       "-rw-rw-r-- 1 nb_user nb_group [size] [date] [time] ps1_bitdiddle_attempt_2016-01-30-15-30-10_jupyter.png\n",
@@ -445,8 +452,8 @@
      "output_type": "stream",
      "text": [
       "total ##\n",
-      "drwxr-xr-x 1 nb_user nb_group [size] [date] [time] bitdiddle\n",
-      "drwxr-xr-x 1 nb_user nb_group [size] [date] [time] hacker\n"
+      "drwxrwxr-x 1 nb_user nb_group [size] [date] [time] bitdiddle\n",
+      "drwxrwxr-x 1 nb_user nb_group [size] [date] [time] hacker\n"
      ]
     }
    ],
@@ -468,7 +475,7 @@
       "total ##\n",
       "-rw------- 1 nb_user nb_group [size] [date] [time] problem1.ipynb\n",
       "-rw-rw-r-- 1 nb_user nb_group [size] [date] [time] problem2.ipynb\n",
-      "-rw-r--r-- 1 nb_user nb_group [size] [date] [time] timestamp.txt\n"
+      "-rw-rw-r-- 1 nb_user nb_group [size] [date] [time] timestamp.txt\n"
      ]
     }
    ],
@@ -564,11 +571,15 @@
       "[ZipCollectApp | INFO] Using file extractor: ExtractorPlugin\n",
       "[ZipCollectApp | INFO] Using file collector: CustomPlugin\n",
       "[ZipCollectApp | WARNING] Clearing existing files in [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/downloaded/ps1/extracted\n",
-      "[ZipCollectApp | INFO] Copying from: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/downloaded/ps1/archive/ps1_hacker_attempt_2016-01-30-20-30-10_problem1.ipynb\n",
-      "[ZipCollectApp | INFO]   Copying to: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/downloaded/ps1/extracted/ps1_hacker_attempt_2016-01-30-20-30-10_problem1.ipynb\n",
+      "[ZipCollectApp | INFO] Copying from: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/downloaded/ps1/archive/jupyter.png\n",
+      "[ZipCollectApp | INFO]   Copying to: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/downloaded/ps1/extracted/jupyter.png\n",
       "[ZipCollectApp | INFO] Extracting from: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/downloaded/ps1/archive/notebooks.zip\n",
       "[ZipCollectApp | INFO]   Extracting to: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/downloaded/ps1/extracted/notebooks\n",
+      "[ZipCollectApp | INFO] Copying from: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/downloaded/ps1/archive/ps1_hacker_attempt_2016-01-30-20-30-10_problem1.ipynb\n",
+      "[ZipCollectApp | INFO]   Copying to: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/downloaded/ps1/extracted/ps1_hacker_attempt_2016-01-30-20-30-10_problem1.ipynb\n",
       "[ZipCollectApp | INFO] Start collecting files...\n",
+      "[ZipCollectApp | INFO] Parsing file: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/downloaded/ps1/extracted/jupyter.png\n",
+      "[ZipCollectApp | WARNING] Skipped submission with no match information provided: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/downloaded/ps1/extracted/jupyter.png\n",
       "[ZipCollectApp | INFO] Parsing file: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/downloaded/ps1/extracted/notebooks/ps1_bitdiddle_attempt_2016-01-30-15-30-10_jupyter.png\n",
       "[ZipCollectApp | WARNING] Skipped submission with no match information provided: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/downloaded/ps1/extracted/notebooks/ps1_bitdiddle_attempt_2016-01-30-15-30-10_jupyter.png\n",
       "[ZipCollectApp | INFO] Parsing file: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/downloaded/ps1/extracted/notebooks/ps1_bitdiddle_attempt_2016-01-30-15-30-10_problem1.ipynb\n",
@@ -579,7 +590,7 @@
       "[ZipCollectApp | WARNING] Skipped notebook with invalid name 'myproblem1.ipynb'\n",
       "[ZipCollectApp | INFO] Parsing file: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/downloaded/ps1/extracted/notebooks/ps1_hacker_attempt_2016-01-30-16-30-10_problem2.ipynb\n",
       "[ZipCollectApp | INFO] Parsing file: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/downloaded/ps1/extracted/ps1_hacker_attempt_2016-01-30-20-30-10_problem1.ipynb\n",
-      "[ZipCollectApp | WARNING] 4 files collected, 3 files skipped\n",
+      "[ZipCollectApp | WARNING] 4 files collected, 4 files skipped\n",
       "[ZipCollectApp | INFO] Start transfering files...\n",
       "[ZipCollectApp | WARNING] Clearing existing files in [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/submitted_zip/bitdiddle/ps1\n",
       "[ZipCollectApp | INFO] Copying from: [NB_GRADER_ROOT]/nbgrader/docs/source/user_guide/downloaded/ps1/extracted/notebooks/ps1_bitdiddle_attempt_2016-01-30-15-30-10_problem1.ipynb\n",
diff --git a/nbgrader/server_extensions/assignment_list/handlers.py b/nbgrader/server_extensions/assignment_list/handlers.py
index c4646cbdc..a78fed1d1 100644
--- a/nbgrader/server_extensions/assignment_list/handlers.py
+++ b/nbgrader/server_extensions/assignment_list/handlers.py
@@ -45,7 +45,6 @@ def root_dir(self, directory):
     def load_config(self):
         paths = jupyter_config_path()
         paths.insert(0, os.getcwd())
-
         app = NbGrader()
         app.config_file_paths.append(paths)
         app.load_config_file()
@@ -54,8 +53,9 @@ def load_config(self):
 
     @contextlib.contextmanager
     def get_assignment_dir_config(self):
+
         # first get the exchange assignment directory
-        with chdir(self.root_dir):
+        with chdir(self._root_dir):
             config = self.load_config()
 
         lister = ExchangeFactory(config=config).List(config=config)
@@ -104,9 +104,9 @@ def list_released_assignments(self, course_id=None):
             else:
                 for assignment in assignments:
                     if assignment['status'] == 'fetched':
-                        assignment['path'] = os.path.relpath(assignment['path'], self.root_dir)
+                        assignment['path'] = os.path.relpath(assignment['path'], self._root_dir)
                         for notebook in assignment['notebooks']:
-                            notebook['path'] = os.path.relpath(notebook['path'], self.root_dir)
+                            notebook['path'] = os.path.relpath(notebook['path'], self._root_dir)
                 retvalue = {
                     "success": True,
                     "value": sorted(assignments, key=lambda x: (x['course_id'], x['assignment_id']))
@@ -296,22 +296,30 @@ class AssignmentActionHandler(BaseAssignmentHandler):
 
     @web.authenticated
     def post(self, action):
+        try:
+            data = {
+                'assignment_id' : self.get_argument('assignment_id'),
+                'course_id' : self.get_argument('course_id')
+            }
+        except web.MissingArgumentError:
+            data = self.get_json_body()
+
         if action == 'fetch':
-            assignment_id = self.get_argument('assignment_id')
-            course_id = self.get_argument('course_id')
+            assignment_id = data['assignment_id']
+            course_id = data['course_id']
             self.manager.fetch_assignment(course_id, assignment_id)
             self.finish(json.dumps(self.manager.list_assignments(course_id=course_id)))
         elif action == 'submit':
-            assignment_id = self.get_argument('assignment_id')
-            course_id = self.get_argument('course_id')
+            assignment_id = data['assignment_id']
+            course_id = data['course_id']
             output = self.manager.submit_assignment(course_id, assignment_id)
             if output['success']:
                 self.finish(json.dumps(self.manager.list_assignments(course_id=course_id)))
             else:
                 self.finish(json.dumps(output))
         elif action == 'fetch_feedback':
-            assignment_id = self.get_argument('assignment_id')
-            course_id = self.get_argument('course_id')
+            assignment_id = data['assignment_id']
+            course_id = data['course_id']
             self.manager.fetch_feedback(course_id, assignment_id)
             self.finish(json.dumps(self.manager.list_assignments(course_id=course_id)))
 
diff --git a/nbgrader/server_extensions/formgrader/base.py b/nbgrader/server_extensions/formgrader/base.py
index 2607df1d5..d5bf2ec4d 100644
--- a/nbgrader/server_extensions/formgrader/base.py
+++ b/nbgrader/server_extensions/formgrader/base.py
@@ -16,11 +16,11 @@ def base_url(self):
 
     @property
     def db_url(self):
-        return self.settings['nbgrader_db_url']
+        return self.settings['nbgrader_coursedir'].db_url
 
     @property
     def url_prefix(self):
-        return self.settings['nbgrader_url_prefix']
+        return self.settings['nbgrader_formgrader'].url_prefix
 
     @property
     def coursedir(self):
@@ -50,6 +50,7 @@ def exporter(self):
     @property
     def api(self):
         level = self.log.level
+        self.coursedir.parent.load_config_file()
         api = NbGraderAPI(
             self.coursedir, self.authenticator, parent=self.coursedir.parent)
         api.log_level = level
diff --git a/nbgrader/server_extensions/formgrader/formgrader.py b/nbgrader/server_extensions/formgrader/formgrader.py
index dc0074fc4..d5806dfb6 100644
--- a/nbgrader/server_extensions/formgrader/formgrader.py
+++ b/nbgrader/server_extensions/formgrader/formgrader.py
@@ -7,6 +7,7 @@
 from tornado import web
 from jinja2 import Environment, FileSystemLoader
 from jupyter_server.utils import url_path_join as ujoin
+from jupyter_core.paths import jupyter_config_path
 
 from . import handlers, apihandlers
 from ...apps.baseapp import NbGrader
@@ -25,6 +26,21 @@ def root_dir(self):
     def root_dir(self, directory):
         self._root_dir = directory
 
+    @property
+    def url_prefix(self):
+        self.coursedir._load_config(self.load_config())
+        relpath = os.path.relpath(self.coursedir.root, self.root_dir)
+        return relpath
+
+    def load_config(self):
+        paths = jupyter_config_path()
+        paths.insert(0, os.getcwd())
+        app = NbGrader()
+        app.config_file_paths.append(paths)
+        app.load_config_file()
+
+        return app.config
+
     @default("classes")
     def _classes_default(self):
         classes = super(FormgradeExtension, self)._classes_default()
@@ -41,7 +57,8 @@ def init_tornado_settings(self, webapp):
         # Init jinja environment
         jinja_env = Environment(loader=FileSystemLoader([handlers.template_path]))
 
-        relpath = os.path.relpath(self.coursedir.root, self.root_dir)
+        relpath = self.url_prefix
+
         if relpath.startswith("../"):
             nbgrader_bad_setup = True
             self.log.error(
@@ -54,7 +71,7 @@ def init_tornado_settings(self, webapp):
 
         # Configure the formgrader settings
         tornado_settings = dict(
-            nbgrader_url_prefix=relpath,
+            nbgrader_formgrader=self,
             nbgrader_coursedir=self.coursedir,
             nbgrader_authenticator=self.authenticator,
             nbgrader_exporter=HTMLExporter(config=self.config),
@@ -89,6 +106,10 @@ def load_jupyter_server_extension(nbapp):
     """Load the formgrader extension"""
     nbapp.log.info("Loading the formgrader nbgrader serverextension")
     webapp = nbapp.web_app
+
+    # Save which kind of application is running : Jupyterlab like or classic Notebook
+    webapp.settings['is_jlab'] = not (nbapp.name == 'jupyter-notebook')
+
     formgrader = FormgradeExtension(parent=nbapp)
 
     # compatibility between notebook.notebookapp.NotebookApp and jupyter_server.serverapp.ServerApp
diff --git a/nbgrader/server_extensions/formgrader/handlers.py b/nbgrader/server_extensions/formgrader/handlers.py
index c5f0c429e..0fa8a9404 100644
--- a/nbgrader/server_extensions/formgrader/handlers.py
+++ b/nbgrader/server_extensions/formgrader/handlers.py
@@ -17,6 +17,7 @@ def get(self):
             "manage_assignments.tpl",
             url_prefix=self.url_prefix,
             base_url=self.base_url,
+            is_lab=int(self.settings['is_jlab']),
             windows=(sys.prefix == 'win32'),
             course_id=self.api.course_id,
             exchange=self.api.exchange_root,
@@ -107,9 +108,10 @@ def get(self, submission_id):
             'index': ix,
             'total': len(indices),
             'base_url': self.base_url,
-            'my_mathjax_url': self.base_url + '/' + self.mathjax_url,
+            'my_mathjax_url': self.mathjax_url if self.settings['is_jlab'] else self.base_url + '/' + self.mathjax_url,
             'student': student_id,
             'last_name': submission.student.last_name,
+            'last_name': submission.student.last_name,
             'first_name': submission.student.first_name,
             'notebook_path': self.url_prefix + '/' + relative_path
         }
diff --git a/nbgrader/server_extensions/formgrader/static/js/jupyterlab_utils.js b/nbgrader/server_extensions/formgrader/static/js/jupyterlab_utils.js
new file mode 100644
index 000000000..7e9bb9783
--- /dev/null
+++ b/nbgrader/server_extensions/formgrader/static/js/jupyterlab_utils.js
@@ -0,0 +1,10 @@
+/*
+ * Module of data to send to Jupyterlab's nbgrader extension to perform actions.
+ */
+
+var jlab_go_to_path = function(directory_path){
+    return JSON.stringify({
+               "command": "filebrowser:go-to-path",
+               "arguments": {"path": directory_path}
+           });
+}
\ No newline at end of file
diff --git a/nbgrader/server_extensions/formgrader/static/js/manage_assignments.js b/nbgrader/server_extensions/formgrader/static/js/manage_assignments.js
index e95e921c5..531c64eb8 100644
--- a/nbgrader/server_extensions/formgrader/static/js/manage_assignments.js
+++ b/nbgrader/server_extensions/formgrader/static/js/manage_assignments.js
@@ -12,7 +12,7 @@ var AssignmentUI = Backbone.View.extend({
 
     events: {},
 
-    initialize: function () {
+    initialize: function (options) {
         this.$modal = undefined;
         this.$modal_duedate = undefined;
         this.$modal_timezone = undefined;
@@ -34,6 +34,7 @@ var AssignmentUI = Backbone.View.extend({
         this.listenTo(this.model, "request", this.animateSaving);
         this.listenTo(this.model, "sync", this.closeModal);
 
+        this.is_lab = options.is_lab || false;
         this.render();
     },
 
@@ -96,14 +97,23 @@ var AssignmentUI = Backbone.View.extend({
 
     render: function () {
         this.clear();
-
+        var this_assignment = this;
         // assignment name
         var name = this.model.get("name")
         this.$name.attr("data-order", name);
+
+        /* Append link with :
+         *     - href if this is a Notebook<7 environment
+         *     - click listener to send message to iframe parent if this is Jupyter Lab environment
+         */
         this.$name.append($("<a/>")
-            .attr("target", "_blank")
-            .attr("href", base_url + "/tree/" + url_prefix + "/" + this.model.get("source_path"))
-            .text(name));
+            .text(name)
+            .attr("target", self.is_lab ? undefined : "_blank")
+            .attr("href", self.is_lab ? undefined : base_url + "/tree/" + url_prefix + "/" + this.model.get("source_path"))
+            .click(self.is_lab ? function(){
+                window.parent.postMessage(jlab_go_to_path(url_prefix + "/" + this_assignment.model.get("source_path")), '*');
+            } : undefined)
+        );
 
         // duedate
         var duedate = this.model.get("duedate");
@@ -144,9 +154,16 @@ var AssignmentUI = Backbone.View.extend({
         // preview student version
         var release_path = this.model.get("release_path");
         if (release_path) {
+            /* Append link with :
+             *     - href if this is a Notebook<7 environment
+             *     - click listener to send message to iframe parent if this is Jupyter Lab environment
+             */
             this.$preview.append($("<a/>")
-                .attr("target", "_blank")
-                .attr("href", base_url + "/tree/" + url_prefix + "/" + release_path)
+                .attr("target", self.is_lab ? undefined : "_blank")
+                .attr("href", self.is_lab ? undefined : base_url + "/tree/" + url_prefix + "/" + release_path)
+                .click(self.is_lab ? function(){
+                        window.parent.postMessage(jlab_go_to_path(url_prefix + "/" + release_path), '*');
+                    } : undefined)
                 .append($("<span/>")
                     .addClass("glyphicon glyphicon-search")
                     .attr("aria-hidden", "true")));
@@ -562,7 +579,7 @@ var createAssignmentModal = function () {
     modal = createModal("add-assignment-modal", "Add New Assignment", body, footer);
 };
 
-var loadAssignments = function () {
+var loadAssignments = function (is_lab=false) {
     var tbl = $("#main-table");
 
     models = new Assignments();
@@ -574,7 +591,8 @@ var loadAssignments = function () {
             models.each(function (model) {
                 var view = new AssignmentUI({
                     "model": model,
-                    "el": insertRow(tbl)
+                    "el": insertRow(tbl),
+                    "is_lab": is_lab
                 });
                 views.push(view);
             });
@@ -586,6 +604,9 @@ var loadAssignments = function () {
 
 var models = undefined;
 var views = [];
+
+var is_lab = is_lab || false;
+
 $(window).on('load', function () {
-    loadAssignments();
+    loadAssignments(is_lab);
 });
diff --git a/nbgrader/server_extensions/formgrader/templates/base.tpl b/nbgrader/server_extensions/formgrader/templates/base.tpl
index b2c6168f9..56eb423fb 100644
--- a/nbgrader/server_extensions/formgrader/templates/base.tpl
+++ b/nbgrader/server_extensions/formgrader/templates/base.tpl
@@ -10,6 +10,7 @@
   <script src="{{ base_url }}/formgrader/static/components/datatables.net-bs/js/dataTables.bootstrap.min.js"></script>
   <script src="{{ base_url }}/formgrader/static/js/backbone_xsrf.js"></script>
   <script src="{{ base_url }}/formgrader/static/js/utils.js"></script>
+  <script src="{{ base_url }}/formgrader/static/js/jupyterlab_utils.js"></script>
 
   <link rel="stylesheet" href="{{ base_url }}/formgrader/static/components/bootstrap/css/bootstrap.min.css" />
   <link rel="stylesheet" href="{{ base_url }}/formgrader/static/components/datatables.net-bs/css/dataTables.bootstrap.min.css">
diff --git a/nbgrader/server_extensions/formgrader/templates/formgrade/formgrade_macros.html.j2 b/nbgrader/server_extensions/formgrader/templates/formgrade/formgrade_macros.html.j2
index 8ed6a630f..e014b1f2e 100644
--- a/nbgrader/server_extensions/formgrader/templates/formgrade/formgrade_macros.html.j2
+++ b/nbgrader/server_extensions/formgrader/templates/formgrade/formgrade_macros.html.j2
@@ -32,6 +32,13 @@ function toggle_name(on) {
     $(".name-shown").hide();
   }
 }
+
+function jlab_open_notebook(notebook_path) {
+  return JSON.stringify({
+    "command": "docmanager:open",
+    "arguments": {"path": notebook_path}
+  });
+}
 </script>
 
 <link rel="stylesheet" href="{{ resources.base_url }}/formgrader/static/components/bootstrap/css/bootstrap.min.css" />
@@ -56,10 +63,22 @@ function toggle_name(on) {
             <li><a href="{{ resources.base_url }}/formgrader/gradebook/{{ resources.assignment_id }}">{{ resources.assignment_id }}</a></li>
             <li><a href="{{ resources.base_url }}/formgrader/gradebook/{{ resources.assignment_id }}/{{ resources.notebook_id }}">{{ resources.notebook_id }}</a></li>
             <li class="active live-notebook">
-              <a class="name-hidden" data-toggle="tooltip" data-placement="bottom" title="Open live notebook" target="_blank" href="{{ resources.base_url }}/notebooks/{{ resources.notebook_path }}">
+              <a class="name-hidden" data-toggle="tooltip" data-placement="bottom" title="Open live notebook"
+                {%- if is_lab -%}
+                onclick='window.parent.postMessage(jlab_open_notebook("{{ resources.notebook_path }}"), "*");'
+                {%- else -%}
+                target="_blank" href="{{ resources.base_url }}/notebooks/{{ resources.notebook_path }}"
+                {%- endif -%}
+              >
                 Submission #{{ resources.index + 1 }}
               </a>
-              <a class="name-shown" data-toggle="tooltip" data-placement="bottom" title="Open live notebook" target="_blank" href="{{ resources.base_url }}/notebooks/{{ resources.notebook_path }}">
+              <a class="name-shown" data-toggle="tooltip" data-placement="bottom" title="Open live notebook"
+                {%- if is_lab -%}
+                onclick='window.parent.postMessage(jlab_open_notebook("{{ resources.notebook_path }}"), "*"); '
+                {%- else -%}
+                target="_blank" href="{{ resources.base_url }}/notebooks/{{ resources.notebook_path }}"
+                {%- endif -%}
+              >
                 {%- if resources.last_name and resources.first_name -%}
                 {{ resources.last_name }}, {{ resources.first_name }}
                 {%- else -%}
diff --git a/nbgrader/server_extensions/formgrader/templates/manage_assignments.tpl b/nbgrader/server_extensions/formgrader/templates/manage_assignments.tpl
index 51b6969f8..1fb2ab3cf 100644
--- a/nbgrader/server_extensions/formgrader/templates/manage_assignments.tpl
+++ b/nbgrader/server_extensions/formgrader/templates/manage_assignments.tpl
@@ -3,6 +3,7 @@
 {%- block head -%}
 <script>
 var url_prefix = "{{ url_prefix }}";
+var is_lab = {% if is_lab %}true{% else %}false{% endif %};
 </script>
 
 <script src="{{ base_url }}/formgrader/static/js/manage_assignments.js"></script>
diff --git a/nbgrader/server_extensions/validate_assignment/handlers.py b/nbgrader/server_extensions/validate_assignment/handlers.py
index e93478fa8..880c097af 100644
--- a/nbgrader/server_extensions/validate_assignment/handlers.py
+++ b/nbgrader/server_extensions/validate_assignment/handlers.py
@@ -88,7 +88,13 @@ def validate_notebook(self, path):
 
     @web.authenticated
     def post(self):
-        output = self.validate_notebook(self.get_argument('path'))
+        try:
+            data = {
+                'path' : self.get_argument('path')
+            }
+        except web.MissingArgumentError:
+            data = self.get_json_body()
+        output = self.validate_notebook(data['path'])
         self.finish(json.dumps(output))
 
 
diff --git a/package.json b/package.json
index fe8a21153..03e85d9ec 100644
--- a/package.json
+++ b/package.json
@@ -1,14 +1,109 @@
 {
- "name": "nbgrader-deps",
- "version": "0.8.0.dev",
- "description": "nbgrader nodejs dependencies",
- "author": "Jupyter Developers",
- "license": "BSD",
- "repository": {
-  "type": "git",
-  "url": "https://github.com/jupyter/nbgrader.git"
- },
- "devDependencies": {
-  "bower": "*"
- }
+  "name": "nbgrader",
+  "version": "0.8.0",
+  "description": "nbgrader nodejs dependencies",
+  "keywords": [
+    "jupyter",
+    "jupyterlab",
+    "jupyterlab-extension"
+  ],
+  "license": "BSD-3-Clause",
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/jupyter/nbgrader.git"
+  },
+  "author": {
+    "name": "Jupyter Development Team",
+    "email": "jupyter@googlegroups.com"
+  },
+  "files": [
+    "lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}",
+    "style/**/*.{css,js,eot,gif,html,jpg,json,png,svg,woff2,ttf}",
+    "schema/*.json"
+  ],
+  "main": "lib/index.js",
+  "types": "lib/index.d.ts",
+  "style": "style/index.css",
+  "scripts": {
+    "build": "jlpm build:lib && jlpm build:labextension:dev",
+    "build:prod": "jlpm clean && jlpm build:lib && jlpm build:labextension",
+    "build:labextension": "jupyter labextension build .",
+    "build:labextension:dev": "jupyter labextension build --development True .",
+    "build:lib": "tsc",
+    "clean": "jlpm clean:lib",
+    "clean:lib": "rimraf lib tsconfig.tsbuildinfo",
+    "clean:labextension": "rimraf nbgrader/labextension",
+    "clean:all": "jlpm clean:lib && jlpm clean:labextension",
+    "install:labextension": "jlpm build",
+    "start:test": "python ./nbgrader/tests/labextension_ui-tests/run_jupyter_lab.py",
+    "test": "jlpm playwright test",
+    "watch": "run-p watch:src watch:labextension",
+    "watch:src": "tsc -w",
+    "watch:labextension": "jupyter labextension watch ."
+  },
+  "dependencies": {
+    "@jupyterlab/application": "^3.1.0",
+    "@jupyterlab/apputils": "^3.1.0",
+    "@jupyterlab/cells": "^3.3.3",
+    "@jupyterlab/coreutils": "^5.1.0",
+    "@jupyterlab/launcher": "^3.4.2",
+    "@jupyterlab/notebook": "^3.3.3",
+    "@jupyterlab/services": "^6.1.0",
+    "@jupyterlab/settingregistry": "^3.1.0"
+  },
+  "devDependencies": {
+    "@jupyterlab/builder": "^3.3.3",
+    "@jupyterlab/galata": "^4.2.4",
+    "@playwright/test": "^1.21.1",
+    "@typescript-eslint/eslint-plugin": "^4.8.1",
+    "@typescript-eslint/parser": "^4.8.1",
+    "bower": "*",
+    "eslint": "^7.14.0",
+    "eslint-config-prettier": "^6.15.0",
+    "eslint-plugin-prettier": "^3.1.4",
+    "mkdirp": "^1.0.3",
+    "npm-run-all": "^4.1.5",
+    "prettier": "^2.1.1",
+    "rimraf": "^3.0.2",
+    "stylelint": "^14.3.0",
+    "stylelint-config-prettier": "^9.0.3",
+    "stylelint-config-recommended": "^6.0.0",
+    "stylelint-config-standard": "~24.0.0",
+    "stylelint-prettier": "^2.0.0",
+    "typescript": "~4.1.3"
+  },
+  "sideEffects": [
+    "style/*.css",
+    "style/index.js"
+  ],
+  "styleModule": "style/index.js",
+  "publishConfig": {
+    "access": "public"
+  },
+  "jupyterlab": {
+    "discovery": {
+      "server": {
+        "managers": [
+          "pip"
+        ],
+        "base": {
+          "name": "nbgrader"
+        }
+      }
+    },
+    "extension": true,
+    "outputDir": "nbgrader/labextension",
+    "schemaDir": "schema"
+  },
+  "jupyter-releaser": {
+    "hooks": {
+      "before-build-npm": [
+        "python -m pip install jupyterlab~=3.1",
+        "jlpm"
+      ],
+      "before-build-python": [
+        "jlpm clean:all"
+      ]
+    }
+  }
 }
diff --git a/playwright.config.ts b/playwright.config.ts
new file mode 100644
index 000000000..a18d2d11f
--- /dev/null
+++ b/playwright.config.ts
@@ -0,0 +1,34 @@
+/**
+ * Configuration for Playwright using default from @jupyterlab/galata
+ */
+
+import type { PlaywrightTestConfig } from '@playwright/test';
+
+const config: PlaywrightTestConfig = {
+  testDir: './nbgrader/tests/labextension_ui-tests',
+  testMatch: '**/*.spec.ts',
+  testIgnore: '**/node_modules/**/*',
+  timeout: 120000,
+  reporter: [[process.env.CI ? 'dot' : 'list'], ['html', { outputFolder: 'playwright-tests' }]],
+  workers: 1,
+  use: {
+  // Browser options
+  // headless: false,
+  // slowMo: 500,
+  // Context options
+  viewport: { width: 1024, height: 768 },
+  // Artifacts
+  video: 'retain-on-failure'
+  },
+
+  webServer: {
+    command: 'jlpm start:test',
+    url: 'http://localhost:8888/lab',
+    timeout: 120 * 1000,
+    reuseExistingServer: !process.env.CI,
+  },
+
+};
+
+export default config;
+
diff --git a/pyproject.toml b/pyproject.toml
index 6cd7944f3..6680ad4a0 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,20 @@
 [build-system]
-requires = ["jupyter_packaging~=0.12.0", "setuptools>=40.8.0", "wheel"]
-build-backend = "setuptools.build_meta"
+requires = ["jupyter_packaging~=0.12.0", "jupyterlab<4"]
+build-backend = "jupyter_packaging.build_api"
+
+[tool.jupyter-packaging.options]
+skip-if-exists = ["nbgrader/labextension/static/style.js"]
+ensured-targets = ["nbgrader/labextension/static/style.js", "nbgrader/labextension/package.json"]
+
+[tool.jupyter-packaging.builder]
+factory = "jupyter_packaging.npm_builder"
+
+[tool.jupyter-packaging.build-args]
+build_cmd = "build:prod"
+npm = ["jlpm"]
+
+[tool.check-manifest]
+ignore = ["nbgrader/labextension/**", "yarn.lock"]
 
 [tool.tbump.version]
 current = "0.8.0.dev"
@@ -25,6 +39,7 @@ src = "pyproject.toml"
 
 [[tool.tbump.file]]
 src = "package.json"
+version_template = "{major}.{minor}.{patch}"
 search = '"version": "{current_version}"'
 
 [[tool.tbump.file]]
diff --git a/schema/assignment-list.json b/schema/assignment-list.json
new file mode 100644
index 000000000..4487c2cdc
--- /dev/null
+++ b/schema/assignment-list.json
@@ -0,0 +1,23 @@
+{
+  "jupyter.lab.shortcuts": [],
+  "title": "assignment-list",
+  "description": "nbgrader assignment list settings.",
+  "type": "object",
+  "properties": {},
+  "jupyter.lab.menus": {
+    "main": [
+      {
+        "id": "jp-mainmenu-nbgrader-menu",
+        "label": "Nbgrader",
+        "rank": 80,
+        "items": [
+          {
+            "command": "nbgrader:open-assignment-list",
+            "rank": 0
+          }
+        ]
+      }
+    ]
+  },
+  "additionalProperties": false
+}
\ No newline at end of file
diff --git a/schema/course-list.json b/schema/course-list.json
new file mode 100644
index 000000000..566a0a241
--- /dev/null
+++ b/schema/course-list.json
@@ -0,0 +1,23 @@
+{
+  "jupyter.lab.shortcuts": [],
+  "title": "assignment-list",
+  "description": "nbgrader assignment list settings.",
+  "type": "object",
+  "properties": {},
+  "jupyter.lab.menus": {
+    "main": [
+      {
+        "id": "jp-mainmenu-nbgrader-menu",
+        "label": "Nbgrader",
+        "rank": 80,
+        "items": [
+          {
+            "command": "nbgrader:open-course-list",
+            "rank": 2
+          }
+        ]
+      }
+    ]
+  },
+  "additionalProperties": false
+}
\ No newline at end of file
diff --git a/schema/formgrader.json b/schema/formgrader.json
new file mode 100644
index 000000000..f2c16051f
--- /dev/null
+++ b/schema/formgrader.json
@@ -0,0 +1,23 @@
+{
+  "jupyter.lab.shortcuts": [],
+  "title": "assignment-list",
+  "description": "nbgrader assignment list settings.",
+  "type": "object",
+  "properties": {},
+  "jupyter.lab.menus": {
+    "main": [
+      {
+        "id": "jp-mainmenu-nbgrader-menu",
+        "label": "Nbgrader",
+        "rank": 80,
+        "items": [
+          {
+            "command": "nbgrader:open-formgrader",
+            "rank": 1
+          }
+        ]
+      }
+    ]
+  },
+  "additionalProperties": false
+}
\ No newline at end of file
diff --git a/setup.cfg b/setup.cfg
index 0b6c5280f..18a5b71d7 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -9,12 +9,18 @@ author = Jupyter Development Team
 author_email = jupyter@googlegroups.com
 url = https://github.com/jupyter/nbgrader
 platforms = Linux, Mac OS X, Windows
-keywords = Jupyter, Notebooks, Grading, Homework
+keywords = Jupyter, Notebooks, Grading, Homework, JupyterLab
 classifiers =
     License :: OSI Approved :: BSD License
     Programming Language :: Python
     Programming Language :: Python :: 3
 
+    Framework :: Jupyter
+    Framework :: Jupyter :: JupyterLab
+    Framework :: Jupyter :: JupyterLab :: 3
+    Framework :: Jupyter :: JupyterLab :: Extensions
+    Framework :: Jupyter :: JupyterLab :: Extensions :: Prebuilt
+
 [options]
 include_package_data = True
 packages = find:
@@ -35,6 +41,8 @@ install_requires =
     ipywidgets>=7.6
     nbclient>=0.6.1
     traitlets<5.2.0
+    jupyterlab<4
+    jupyterlab_server
 
 [options.extras_require]
 docs =
@@ -47,6 +55,8 @@ tests =
     pytest-cov
     pytest-rerunfailures
     pytest-xdist==2.5.0
+    pytest-tornasync
+    jupyter_packaging
     coverage
     selenium>2.4
     codecov
diff --git a/setup.py b/setup.py
index 062e77a3e..18eb61af7 100644
--- a/setup.py
+++ b/setup.py
@@ -6,22 +6,57 @@
 
 from setuptools import setup
 from jupyter_packaging import create_cmdclass
-
-
-def get_data_files():
-    """Get the data files for the package."""
-    data_files = [
-        ("etc/jupyter/jupyter_server_config.d", "etc/jupyter/jupyter_server_config.d/", "*.json"),
-        ("etc/jupyter", "etc/jupyter/", "*.json"),
-    ]
-    return data_files
-
-
-cmdclass = create_cmdclass(data_files_spec=get_data_files())
-
-setup_args = dict(
-    cmdclass=cmdclass
-)
+from pathlib import Path
+import json
+import sys
+import os
+
+HERE = Path(__file__).parent.resolve()
+
+# Get the package info from package.json
+pkg_json = json.loads((HERE / "package.json").read_bytes())
+
+lab_path = (HERE / pkg_json["jupyterlab"]["outputDir"])
+
+# Representative files that should exist after a successful build
+ensured_targets = [
+    str(lab_path / "package.json"),
+    str(lab_path / "static/style.js")
+]
+
+labext_name = pkg_json["name"]
+
+data_files_spec = [
+    # labextension installation
+    ("etc/jupyter/jupyter_server_config.d", "etc/jupyter/jupyter_server_config.d/", "*.json"),
+    # For backward compatibility with notebook server
+    ("etc/jupyter", "etc/jupyter/", "*.json"),
+    # labextension files
+    ("share/jupyter/labextensions/%s" % labext_name, str(lab_path.relative_to(HERE)), "**"),
+    ("share/jupyter/labextensions/%s" % labext_name, str("."), "install.json"),
+]
+
+setup_args = dict()
+
+if not os.getenv("NBGRADER_NO_LAB"):
+    try:
+        from jupyter_packaging import (
+            wrap_installers,
+            npm_builder,
+            get_data_files
+        )
+
+        post_develop = npm_builder(
+            build_cmd="install:labextension", source_dir="src", build_dir=lab_path, npm="jlpm"
+        )
+        setup_args["cmdclass"] = wrap_installers(post_develop=post_develop, ensured_targets=ensured_targets)
+        setup_args["data_files"] = get_data_files(data_files_spec)
+    except ImportError as e:
+        import logging
+        logging.basicConfig(format="%(levelname)s: %(message)s")
+        logging.warning("Build tool `jupyter-packaging` is missing. Install it with pip or conda.")
+        if not ("--name" in sys.argv or "--version" in sys.argv):
+            raise e
 
 if __name__ == "__main__":
     setup(**setup_args)
diff --git a/src/assignment_list/assignmentlist.ts b/src/assignment_list/assignmentlist.ts
new file mode 100644
index 000000000..075cd87b1
--- /dev/null
+++ b/src/assignment_list/assignmentlist.ts
@@ -0,0 +1,768 @@
+import { JupyterFrontEnd } from '@jupyterlab/application';
+
+import { URLExt } from '@jupyterlab/coreutils';
+
+import { ServerConnection } from '@jupyterlab/services';
+
+import { Dialog } from '@jupyterlab/apputils';
+
+import { Widget } from '@lumino/widgets';
+
+import { PageConfig } from '@jupyterlab/coreutils';
+
+import * as React from 'react';
+
+import { showNbGraderDialog, validate } from '../common/validate';
+
+export class AssignmentList {
+
+  released_selector: string;
+  fetched_selector: string;
+  submitted_selector: string;
+  released_element: HTMLDivElement;
+  fetched_element: HTMLDivElement;
+  submitted_element: HTMLDivElement;
+  options: Map<string, string>;
+  base_url: string;
+  app: JupyterFrontEnd;
+  callback: () => void;
+
+  list_loading_ids = ['released_assignments_list_loading','fetched_assignments_list_loading','submitted_assignments_list_loading'];
+  list_placeholder_ids = ['released_assignments_list_placeholder','fetched_assignments_list_placeholder', 'submitted_assignments_list_placeholder'];
+  list_error_ids = ['released_assignments_list_error','fetched_assignments_list_error', 'submitted_assignments_list_error'];
+
+  constructor(widget: Widget, released_selector: string, fetched_selector: string, submitted_selector: string, options: Map<string, string>, app:JupyterFrontEnd){
+    this.released_selector = released_selector;
+    this.fetched_selector = fetched_selector;
+    this.submitted_selector = submitted_selector;
+
+    var div_elements = widget.node.getElementsByTagName('div');
+    this.released_element = div_elements.namedItem(released_selector);
+    this.fetched_element = div_elements.namedItem(fetched_selector);
+    this.submitted_element = div_elements.namedItem(submitted_selector);
+
+    this.options = options;
+    this.base_url = options.get('base_url') || PageConfig.getBaseUrl();
+
+    this.app = app;
+    this.callback = undefined;
+
+  }
+
+  public clear_list(loading: boolean): void {
+    var elems = [this.released_element, this.fetched_element, this.submitted_element];
+    var i;
+    var j;
+
+    // remove list items
+    for (i = 0; i < elems.length; i++) {
+
+      for(j =0; j < elems[i].children.length; ++j){
+        if(elems[i].children[j].classList.contains('list_item')){
+          elems[i].removeChild(elems[i].children[j]);
+          --j;
+        }
+
+      }
+
+      if (loading) {
+          // show loading
+          (<HTMLDivElement>elems[i].children.namedItem(this.list_loading_ids[i])).hidden = false;
+
+          // hide placeholders and errors
+          (<HTMLDivElement>elems[i].children.namedItem(this.list_placeholder_ids[i])).hidden = true;
+          (<HTMLDivElement>elems[i].children.namedItem(this.list_error_ids[i])).hidden = true;
+
+      } else {
+          // show placeholders display
+          (<HTMLDivElement>elems[i].children.namedItem(this.list_placeholder_ids[i])).hidden = false;
+
+          // hide loading and errors
+          (<HTMLDivElement>elems[i].children.namedItem(this.list_loading_ids[i])).hidden = true;
+          (<HTMLDivElement>elems[i].children.namedItem(this.list_error_ids[i])).hidden = true;
+
+      }
+    }
+  };
+
+  private load_list_success(data: string | any[]): void {
+    this.clear_list(false);
+
+    var len = data.length;
+    for (var i=0; i<len; i++) {
+        var element = document.createElement('div');
+        new Assignment(element, data[i], this.fetched_selector,
+          (newData)=>{this.handle_load_list(newData)}, this.options, this.app);
+        if (data[i].status === 'released') {
+          this.released_element.append(element);
+          (<HTMLDivElement>this.released_element.children.namedItem('released_assignments_list_placeholder')).hidden = true;
+        } else if (data[i]['status'] === 'fetched') {
+          this.fetched_element.append(element);
+          (<HTMLDivElement>this.fetched_element.children.namedItem('fetched_assignments_list_placeholder')).hidden = true;
+        } else if (data[i]['status'] === 'submitted') {
+          this.submitted_element.append(element);
+          (<HTMLDivElement>this.submitted_element.children.namedItem('submitted_assignments_list_placeholder')).hidden = true;
+        }
+    }
+
+    var assignments  = this.fetched_element.getElementsByClassName('assignment-notebooks-link');
+    for(let a of assignments){
+      var icon = document.createElement('i');
+      icon.classList.add('fa', 'fa-caret-right');
+      a.append(icon);
+      (<HTMLAnchorElement>a).onclick = function(event){
+
+        if(a.children[0].classList.contains('fa-caret-right')){
+          a.children[0].classList.remove('fa-caret-right');
+          a.children[0].classList.add('fa-caret-down');
+        }else{
+          a.children[0].classList.remove('fa-caret-down');
+          a.children[0].classList.add('fa-caret-right');
+        }
+
+        /* Open or close collapsed child list on click */
+        const list_item = (<HTMLAnchorElement>event.target).closest('.list_item');
+        list_item.querySelector('.collapse').classList.toggle('in');
+      }
+
+    }
+
+    if (this.callback) {
+      this.callback();
+      this.callback = undefined;
+    }
+
+
+  };
+
+  public show_error(error: string): void {
+    var elems = [this.released_element, this.fetched_element, this.submitted_element];
+    var i;
+
+    // remove list items
+    for (i = 0; i < elems.length; i++) {
+      for(var j =0; j < elems[i].children.length; ++j){
+        if(elems[i].children[j].classList.contains('list_item')){
+          elems[i].removeChild(elems[i].children[j]);
+          --j;
+        }
+
+      }
+
+      // show errors
+      (<HTMLDivElement>elems[i].children.namedItem(this.list_error_ids[i])).hidden = false;
+      (<HTMLDivElement>elems[i].children.namedItem(this.list_error_ids[i])).innerText = error;
+
+      // hide loading and placeholding
+      (<HTMLDivElement>elems[i].children.namedItem(this.list_loading_ids[i])).hidden = true;
+      (<HTMLDivElement>elems[i].children.namedItem(this.list_placeholder_ids[i])).hidden = true;
+    }
+  };
+
+  public handle_load_list(data: { success: any; value: any; }): void {
+    if (data.success) {
+        this.load_list_success(data.value);
+    } else {
+      this.show_error(data.value);
+    }
+  };
+
+  public async load_list(course: string, callback: any){
+    this.callback = callback;
+    this.clear_list(true);
+    try {
+      const data = await requestAPI<any>('assignments?course_id=' + course, {
+        method: 'GET',
+      });
+      this.handle_load_list(data)
+    } catch (reason) {
+      console.error(`Error on GET /assignments.\n${reason}`);
+    }
+
+  };
+
+
+};
+
+class Assignment {
+  element: HTMLDivElement;
+  data: any;
+  parent: string;
+  on_refresh: (data: any) => void;
+  options: Map<string, string>;
+  base_url: any;
+  app: JupyterFrontEnd;
+
+  constructor(element: HTMLDivElement , data: any, parent: string , on_refresh: (data: any) => void, options: Map<string, string>, app: JupyterFrontEnd){
+    this.element = element;
+    this.data = data;
+    this.parent = parent;
+    this.on_refresh = on_refresh;
+    this.options = options;
+    this.base_url = options.get('base_url') || PageConfig.getBaseUrl();
+    this.app = app;
+    this.style();
+    this.make_row();
+  }
+
+  private style(): void {
+    this.element.classList.add('list_item', "row");
+  };
+
+  private escape_id(): string {
+    // construct the id from the course id and the assignment id, and also
+    // prepend the id with "nbgrader" (this also ensures that the first
+    // character is always a letter, as required by HTML 4)
+    var id = "nbgrader-" + this.data['course_id'] + "-" + this.data['assignment_id'];
+
+    // replace spaces with '_'
+    id = id.replace(/ /g, "_");
+
+    // remove any characters that are invalid in HTML div ids
+    id = id.replace(/[^A-Za-z0-9\-_]/g, "");
+
+    return id;
+  };
+
+  private make_link(): HTMLSpanElement {
+    var container = document.createElement('span');;
+    container.classList.add('item_name', 'col-sm-6');
+
+    var link;
+    if (this.data['status'] === 'fetched') {
+        link = document.createElement ('a');
+        var id = this.escape_id();
+        link.classList.add('collapsed', 'assignment-notebooks-link');
+        link.setAttribute('role', 'button');
+        link.setAttribute('data-toggle', 'collapse');
+        link.setAttribute('data-parent', this.parent);
+        link.setAttribute('href', '#' + id);
+        link.setAttribute('aria-expanded', 'false');
+        link.setAttribute('aria-controls', id);
+    }else{
+      link = document.createElement('span');
+    }
+    link.innerText = (this.data['assignment_id']);
+    container.append(link);
+    return container;
+  };
+
+  private submit_error(data: { value: any; }): void {
+
+    const body_title = React.createElement('p', null, 'Assignment not submitted:');
+    const body_content = React.createElement('pre', null, data.value);
+    const body = React.createElement("div", {'id': 'submission-message'}, [body_title, body_content]);
+
+    showNbGraderDialog({
+      title: "Invalid Submission",
+      body: body,
+      buttons: [Dialog.okButton()]
+    }, true);
+
+
+  };
+
+  private make_button(): HTMLSpanElement{
+    var container = document.createElement('span');
+    container.classList.add('item_status', 'col-sm-4')
+    var button = document.createElement('button');
+    button.classList.add('btn', 'btn-primary', 'btn-xs')
+    container.append(button);
+    var that = this;
+    if (this.data['status'] === 'released') {
+
+      button.innerText = "fetch";
+      button.onclick = async function(){
+        button.innerText = 'Fetching...';
+        button.setAttribute('disabled', 'disabled');
+        const dataToSend = { 'course_id': that.data['course_id'], 'assignment_id': that.data['assignment_id']};
+        try {
+          const reply = await requestAPI<any>('assignments/fetch', {
+            body: JSON.stringify(dataToSend),
+            method: 'POST'
+          });
+
+          that.on_refresh(reply);
+        } catch (reason) {
+          remove_children(container);
+          container.innerText = 'Error fetching assignment.';
+          console.error(
+            `Error on POST /assignment_list/fetch ${dataToSend}.\n${reason}`
+          );
+        }
+
+      }
+    } else if (this.data.status == 'fetched') {
+        button.innerText = "Submit";
+        button.onclick = async function(){
+          button.innerText = 'submitting...';
+          button.setAttribute('disabled', 'disabled');
+          const dataToSend = { course_id: that.data['course_id'], assignment_id: that.data['assignment_id']};
+          try {
+            const reply = await requestAPI<any>('assignments/submit', {
+              body: JSON.stringify(dataToSend),
+              method: 'POST'
+            });
+
+            if(!reply.success){
+              that.submit_error(reply);
+              button.innerText = 'Submit'
+              button.removeAttribute('disabled')
+            }else{
+              that.on_refresh(reply);
+            }
+
+          } catch (reason) {
+            remove_children(container);
+            container.innerText = 'Error submitting assignment.';
+            console.error(
+              `Error on POST /assignment_list/assignments/submit ${dataToSend}.\n${reason}`
+            );
+          }
+
+        }
+
+
+    } else if (this.data.status == 'submitted') {
+      button.innerText = "Fetch Feedback";
+      button.onclick = async function(){
+        button.innerText = 'Fetching Feedback...';
+        button.setAttribute('disabled', 'disabled');
+        const dataToSend = { course_id: that.data['course_id'], assignment_id: that.data['assignment_id']};
+        try {
+          const reply = await requestAPI<any>('assignments/fetch_feedback', {
+            body: JSON.stringify(dataToSend),
+            method: 'POST'
+          });
+
+          that.on_refresh(reply);
+
+        } catch (reason) {
+          remove_children(container);
+          container.innerText = 'Error fetching feedback.';
+          console.error(
+            `Error on POST /assignments/fetch_feedback ${dataToSend}.\n${reason}`
+          );
+        }
+
+      }
+    }
+
+    return container;
+  };
+
+  private make_row(): void {
+    var row = document.createElement('div');
+    row.classList.add('col-md-12');
+    var link = this.make_link();
+    row.append(link);
+    var s = document.createElement('span');
+    s.classList.add('item_course', 'col-sm-2')
+    s.innerText = this.data['course_id']
+    row.append(s)
+
+    var id, element;
+    var children = document.createElement('div');
+    if (this.data['status'] == 'submitted') {
+      id = this.escape_id() + '-submissions';
+      children.id = id;
+      children.classList.add('panel-collapse', 'list_container', 'assignment-notebooks');
+      children.setAttribute('role', 'tabpanel');
+
+      var d = document.createElement('div');
+      d.classList.add('list_item', 'row');
+      children.append(d);
+      for (var i=0; i<this.data['submissions'].length; i++) {
+        element = document.createElement('div');
+        new Submission(element, this.data.submissions[i], this.options, this.app);
+        children.append(element);
+      }
+
+    } else if (this.data['status'] === 'fetched') {
+
+        id = this.escape_id();
+        children.id = id;
+        children.classList.add('panel-collapse', 'list_container', 'assignment-notebooks', 'collapse');
+        children.setAttribute('role', 'tabpanel');
+        var d = document.createElement('div');
+        d.classList.add('list_item', 'row');
+        children.append(d);
+        for (var i=0; i<this.data['notebooks'].length; i++) {
+            element = document.createElement('div');
+            this.data.notebooks[i]['course_id'] = this.data['course_id'];
+            this.data.notebooks[i]['assignment_id'] = this.data['assignment_id'];
+            new Notebook(element, this.data.notebooks[i], this.options, this.app);
+            children.append(element);
+        }
+    }
+
+    row.append(this.make_button());
+    this.element.innerHTML= ''
+
+    this.element.append(row);
+    this.element.append(children);
+  };
+
+};
+
+const remove_children = function (element: HTMLElement) {
+  element.innerHTML = '';
+}
+
+class Submission{
+
+  element: any;
+  data: any;
+  options: Map<string, string>;
+  base_url: any;
+  app: JupyterFrontEnd;
+
+  constructor(element: HTMLDivElement, data: any, options: Map<string, string>, app: JupyterFrontEnd){
+    this.element = element;
+    this.data = data;
+    this.options = options;
+    this.base_url = options.get('base_url') || PageConfig.getBaseUrl();
+    this.app = app
+    this.style();
+    this.make_row();
+
+  }
+
+  private style(): void {
+    this.element.classList.add('list_item', 'row', 'nested_list_item');
+  };
+
+  private make_row(): void{
+    var container = document.createElement('div')
+    container.classList.add('col-md-12');
+    var status = document.createElement('span')
+    status.classList.add('item_name', 'col-sm-6');
+    var s = document.createElement('span').innerText = this.data['timestamp'];
+    status.append(s);
+
+
+    if (this.data['has_local_feedback'] && !this.data['feedback_updated']) {
+      var app = this.app;
+      var feedback_path = this.data['local_feedback_path'];
+      // var url = URLExt.join(this.base_url, 'tree', this.data['local_feedback_path']);
+      var link = document.createElement('a')
+      link.onclick = function() {
+        app.commands.execute('filebrowser:go-to-path', {
+          path: feedback_path
+        });
+      }
+      link.innerText = ' (view feedback)';
+      status.append(link);
+    } else if (this.data['has_exchange_feedback']) {
+      var feedback = document.createElement('span');
+      feedback.innerText = ' (feedback available to fetch)';
+      status.append(feedback);
+    } else {
+      var feedback = document.createElement('span');
+      feedback.innerText = '';
+      status.append(feedback);
+    }
+    container.append(status);
+    var s1 = document.createElement('span');
+    s1.classList.add('item_course', 'col-sm-2')
+    container.append(s1);
+    var s2 = document.createElement('span');
+    s2.classList.add('item_status', 'col-sm-4')
+    container.append(s2);
+    this.element.append(container);
+  };
+};
+
+class Notebook{
+  element: HTMLDivElement;
+  data: any;
+  options: Map<string, string>;
+  base_url: any;
+  app: JupyterFrontEnd;
+
+  constructor (element: HTMLDivElement, data: any, options: Map<string, string>, app:JupyterFrontEnd) {
+    this.element = element;
+    this.data = data;
+    this.options = options;
+    this.base_url = options.get('base_url') || PageConfig.getBaseUrl();
+    this.app = app;
+    this.style();
+    this.make_row();
+
+  }
+
+  private style(): void  {
+    this.element.classList.add('list_item', 'row', 'nested_list_item');
+  };
+
+  private make_button(): HTMLSpanElement {
+    var that = this;
+    var container = document.createElement('span');
+    container.classList.add('item_status', 'col-sm-4');
+    var button = document.createElement('button')
+    button.classList.add('btn', 'btn-default', 'btn-xs')
+
+    container.append(button);
+
+    button.innerText = 'Validate';
+    button.onclick = async function(){
+      button.innerText = 'Validating...';
+      button.setAttribute('disabled', 'disabled');
+      const dataToSend = { path: that.data['path']}
+      try {
+        const reply = await requestAPI<any>('assignments/validate', {
+          body: JSON.stringify(dataToSend),
+          method: 'POST'
+        });
+
+        button.innerText = 'Validate'
+        button.removeAttribute('disabled')
+        const success = validate(reply);
+
+        if (success) that.validate_success(button);
+        else that.validate_failure(button);
+
+      } catch (reason) {
+        remove_children(container);
+        container.innerText = 'Error validating assignment.';
+        console.error(
+          `Error on POST /assignments/validate ${dataToSend}.\n${reason}`
+        );
+      }
+    }
+
+    return container;
+  };
+
+  private validate_success(button: HTMLButtonElement): void {
+    button.classList.remove('btn-default', 'btn-danger', 'btn-success');
+    button.classList.add('btn-success');
+  };
+
+  private validate_failure(button: HTMLButtonElement): void {
+    button.classList.remove('btn-default', 'btn-danger', 'btn-success');
+    button.classList.add("btn-danger");
+  };
+
+  private make_row(): void {
+
+    var app = this.app;
+    var nb_path = this.data['path']
+
+    var container = document.createElement('div');
+    container.classList.add('col-md-12');
+    var s1 = document.createElement('span');
+    s1.classList.add('item_name', 'col-sm-6');
+
+    var a = document.createElement('a');
+    a.href = '#';
+    a.innerText = this.data['notebook_id'];
+    a.onclick = function() {
+      app.commands.execute('docmanager:open', {
+        path: nb_path
+      });
+    }
+
+    s1.append(a);
+
+    container.append(s1);
+    var s2 = document.createElement('span');
+    s2.classList.add('item_course', 'col-sm-2');
+    container.append(s2);
+    container.append(this.make_button());
+    this.element.append(container);
+  };
+};
+
+export class CourseList{
+  course_list_selector: string;
+  default_course_selector: string;
+  dropdown_selector: string;
+  refresh_selector: string;
+  assignment_list: AssignmentList;
+  current_course: string;
+  options = new Map();
+  base_url: string;
+  data : string[];
+  course_list_element : HTMLUListElement;
+  default_course_element: HTMLButtonElement;
+  dropdown_element: HTMLButtonElement;
+  refresh_element: HTMLButtonElement;
+
+  constructor(widget: Widget, course_list_selector: string, default_course_selector: string, dropdown_selector: string, refresh_selector: string, assignment_list: AssignmentList, options: Map<string, string>) {
+
+  this.course_list_selector = course_list_selector;
+  this.default_course_selector = default_course_selector;
+  this.dropdown_selector = dropdown_selector;
+  this.refresh_selector = refresh_selector;
+  this.course_list_element = widget.node.getElementsByTagName('ul').namedItem(course_list_selector);
+  var buttons = widget.node.getElementsByTagName('button');
+  this.default_course_element = buttons.namedItem(default_course_selector);
+  this.dropdown_element = buttons.namedItem(dropdown_selector);
+  this.refresh_element = buttons.namedItem(refresh_selector);
+
+  this.assignment_list = assignment_list;
+  this.current_course = undefined;
+
+  //options = options || {};
+  this.options = options;
+  this.base_url = options.get('base_url') || PageConfig.getBaseUrl();
+
+  this.data = undefined;
+
+  var that = this;
+
+  /* Open the dropdown course_list when clicking on dropdown toggle button */
+  this.dropdown_element.onclick = function() {
+    that.course_list_element.classList.toggle('open');
+  }
+
+  /* Close the dropdown course_list if clicking anywhere else */
+  document.onclick = function(event) {
+    if ((<HTMLElement>event.target).closest('button') != that.dropdown_element) {
+      that.course_list_element.classList.remove('open');
+    }
+  }
+
+  this.refresh_element.onclick = function(){
+    that.load_list();
+  }
+  this.bind_events()
+}
+
+private enable_list(): void {
+  this.dropdown_element.removeAttribute("disabled");
+};
+
+private disable_list(): void {
+  this.dropdown_element.setAttribute("disabled", "disabled");
+};
+
+public clear_list(): void {
+  // remove list items
+  if(this.course_list_element.children.length > 0){
+    this.course_list_element.innerHTML = '';
+  }
+};
+
+private bind_events(): void {
+  this.refresh_element.click();
+};
+
+private async load_list() {
+  this.disable_list()
+  this.clear_list();
+  this.assignment_list.clear_list(true);
+
+  try {
+    const data = await requestAPI<any>('courses');
+    this.handle_load_list(data)
+  } catch (reason) {
+    console.error(`Error on GET /courses.\n${reason}`);
+  }
+
+};
+
+private handle_load_list(data: { success: any; value: any; }): void {
+  if (data.success) {
+      this.load_list_success(data.value);
+  } else {
+      this.default_course_element.innerText = "Error fetching courses!";
+      this.enable_list();
+      this.assignment_list.show_error(data.value);
+  }
+};
+
+private load_list_success(data: string[]): void {
+  this.data = data;
+  this.disable_list()
+  this.clear_list();
+
+  if (this.data.length === 0) {
+      this.default_course_element.innerText = "No courses found.";
+      this.assignment_list.clear_list(false);
+      this.enable_list()
+      return;
+  }
+
+  if (!this.data.includes(this.current_course)) {
+      this.current_course = undefined;
+  }
+
+  if (this.current_course === undefined) {
+      this.change_course(this.data[0]);
+  } else {
+      // we still want to "change" the course here to update the
+      // assignment list
+      this.change_course(this.current_course);
+  }
+};
+
+private change_course(course: string): void {
+  this.disable_list();
+  if (this.current_course !== undefined) {
+      this.default_course_element.innerText = course;
+  }
+  this.current_course = course;
+  this.default_course_element.innerText = this.current_course;
+  var success = ()=>{this.load_assignment_list_success()};
+  this.assignment_list.load_list(course, success);
+};
+
+private load_assignment_list_success(): void {
+  if (this.data) {
+      var that = this;
+      var set_course = function (course: string) {
+          return function () { that.change_course(course); };
+      }
+
+      for (var i=0; i<this.data.length; i++) {
+        var a = document.createElement('a');
+        a.href = '#';
+        a.innerText = this.data[i];
+        var element = document.createElement('li');
+        element.append(a);
+        element.onclick = set_course(this.data[i]);
+        this.course_list_element.append(element);
+      }
+      this.data = undefined;
+  }
+  this.enable_list();
+};
+
+}
+
+/**
+ * Call the API extension
+ *
+ * @param endPoint API REST end point for the extension
+ * @param init Initial values for the request
+ * @returns The response body interpreted as JSON
+ */
+export async function requestAPI<T>(
+  endPoint = '',
+  init: RequestInit = {}
+): Promise<T> {
+  // Make request to Jupyter API
+  const settings = ServerConnection.makeSettings();
+  const requestUrl = URLExt.join(
+    settings.baseUrl,
+    // 'assignment_list', // API Namespace
+    endPoint
+  );
+
+  let response: Response;
+  try {
+    response = await ServerConnection.makeRequest(requestUrl, init, settings);
+  } catch (error) {
+    throw new ServerConnection.NetworkError(error);
+  }
+
+  const data = await response.json();
+
+  if (!response.ok) {
+    throw new ServerConnection.ResponseError(response, data.message);
+  }
+
+  return data;
+}
diff --git a/src/assignment_list/index.ts b/src/assignment_list/index.ts
new file mode 100644
index 000000000..afee10422
--- /dev/null
+++ b/src/assignment_list/index.ts
@@ -0,0 +1,234 @@
+import {
+  ILayoutRestorer,
+  JupyterFrontEnd,
+  JupyterFrontEndPlugin
+} from '@jupyterlab/application';
+
+import {
+  ICommandPalette,
+  MainAreaWidget,
+  WidgetTracker
+} from '@jupyterlab/apputils';
+
+import {
+  Widget
+} from '@lumino/widgets';
+
+import {
+  PageConfig
+} from '@jupyterlab/coreutils';
+
+import {
+  requestAPI,
+  CourseList,
+  AssignmentList
+} from './assignmentlist';
+
+
+const PLUGIN_ID = 'nbgrader/assignment-list';
+const COMMAND_NAME = "nbgrader:open-assignment-list";
+
+
+class AssignmentListWidget extends Widget {
+
+  app: JupyterFrontEnd;
+
+  constructor(app: JupyterFrontEnd) {
+    super();
+    this.app = app;
+
+    console.log('Initializing the assignments list widget');
+
+    var assignment_html = ([
+      '<div id="assignments" class="tab-pane">',
+      '  <div id="assignments_toolbar" class="row list_toolbar">',
+      '    <div class="col-sm-8 no-padding">',
+      '      <span id="assignments_list_info" class="toolbar_info">Released, downloaded, and submitted assignments for course:</span>',
+      '      <div class="btn-group btn-group-xs">',
+      '        <button type="button" class="btn btn-default" id="course_list_default">Loading, please wait...</button>',
+      '        <button type="button" class="btn btn-default dropdown-toggle" id="course_list_dropdown" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" disabled="disabled">',
+      '          <span class="caret"></span>',
+      '          <span class="sr-only">Toggle Dropdown</span>',
+      '        </button>',
+      '        <ul class="dropdown-menu" id="course_list">',
+      '        </ul>',
+      '      </div>',
+      '    </div>',
+      '    <div class="col-sm-4 no-padding tree-buttons">',
+      '      <span id="assignments_buttons" class="pull-right toolbar_buttons">',
+      '      <button id="refresh_assignments_list" title="Refresh assignments list" class="btn btn-default btn-xs"><i class="fa fa-refresh"></i></button>',
+      '      </span>',
+      '    </div>',
+      '  </div>',
+      '  <div class="alert alert-danger version_error">',
+      '  </div>',
+      '  <div class="panel-group">',
+      '    <div class="panel panel-default">',
+      '      <div class="panel-heading">',
+      '        Released assignments',
+      '      </div>',
+      '      <div class="panel-body">',
+      '        <div id="released_assignments_list" class="list_container">',
+      '          <div id="released_assignments_list_placeholder" class="list_placeholder">',
+      '            <div> There are no assignments to fetch. </div>',
+      '          </div>',
+      '          <div id="released_assignments_list_loading" class="list_loading">',
+      '            <div> Loading, please wait... </div>',
+      '          </div>',
+      '          <div id="released_assignments_list_error" class="list_error">',
+      '            <div></div>',
+      '          </div>',
+      '        </div>',
+      '      </div>',
+      '    </div>',
+      '    <div class="panel panel-default">',
+      '      <div class="panel-heading">',
+      '        Downloaded assignments',
+      '      </div>',
+      '      <div class="panel-body">',
+      '        <div id="fetched_assignments_list" class="list_container" role="tablist" aria-multiselectable="true">',
+      '          <div id="fetched_assignments_list_placeholder" class="list_placeholder">',
+      '            <div> There are no downloaded assignments. </div>',
+      '          </div>',
+      '          <div id="fetched_assignments_list_loading" class="list_loading">',
+      '            <div> Loading, please wait... </div>',
+      '          </div>',
+      '          <div id="fetched_assignments_list_error" class="list_error">',
+      '            <div></div>',
+      '          </div>',
+      '        </div>',
+      '      </div>',
+      '    </div>',
+      '    <div class="panel panel-default">',
+      '      <div class="panel-heading">',
+      '        Submitted assignments',
+      '      </div>',
+      '      <div class="panel-body">',
+      '        <div id="submitted_assignments_list" class="list_container">',
+      '          <div id="submitted_assignments_list_placeholder" class="list_placeholder">',
+      '            <div> There are no submitted assignments. </div>',
+      '          </div>',
+      '          <div id="submitted_assignments_list_loading" class="list_loading">',
+      '            <div> Loading, please wait... </div>',
+      '          </div>',
+      '          <div id="submitted_assignments_list_error" class="list_error">',
+      '            <div></div>',
+      '          </div>',
+      '        </div>',
+      '      </div>',
+      '    </div>',
+      '  </div>   ',
+      '</div>'
+  ].join('\n'));
+
+    this.node.innerHTML = assignment_html;
+    this.node.style.overflowY = 'auto';
+
+    let base_url = PageConfig.getBaseUrl();
+    let options = new Map();
+    options.set('base_url',base_url);
+    var assignment_l = new AssignmentList(this,
+      'released_assignments_list',
+      'fetched_assignments_list',
+      'submitted_assignments_list',
+      options,
+      this.app);
+
+    new CourseList(this,
+      'course_list',
+      'course_list_default',
+      'course_list_dropdown',
+      'refresh_assignments_list',
+      assignment_l,
+      options
+    );
+
+    this.checkNbGraderVersion();
+
+  }
+
+  checkNbGraderVersion() {
+    var warning = this.node.getElementsByClassName('version_error')[0] as HTMLDivElement;
+    warning.hidden=false;
+    requestAPI<any>('nbgrader_version?version='+"0.8.0.dev")
+    .then(response => {
+        if (!response['success']) {
+          warning.innerText = response['message'];
+          warning.style.display = 'block'
+        }
+    })
+    .catch(reason => {
+        console.error(
+          `Error on GET /assignment_list/nbgrader_version.\n${reason}`
+        );
+    });
+  }
+
+}
+
+
+export const assignment_list_extension: JupyterFrontEndPlugin<void> = {
+  id: PLUGIN_ID,
+  autoStart: true,
+  requires: [ICommandPalette],
+  optional: [ILayoutRestorer],
+  activate: async (
+    app: JupyterFrontEnd,
+    palette: ICommandPalette,
+    restorer: ILayoutRestorer | null
+  )=> {
+
+    // Declare a widget variable
+    let widget: MainAreaWidget<AssignmentListWidget>;
+
+    // Add an application command
+    const command: string = COMMAND_NAME;
+
+    // Track the widget state
+    let tracker = new WidgetTracker<MainAreaWidget<AssignmentListWidget>>({
+      namespace: 'nbgrader-assignment-list'
+    });
+
+    app.commands.addCommand(command,{
+      label: 'Assignment List',
+      execute: () => {
+        if(!widget){
+          const content = new AssignmentListWidget(app);
+          widget = new MainAreaWidget({content});
+          widget.id = 'nbgrader-assignment-list';
+          widget.addClass('nbgrader-mainarea-widget');
+          widget.title.label = 'Assignments';
+          widget.title.closable = true;
+        }
+
+        if(!tracker.has(widget)){
+          // Track the state of the widget for later restoration
+          tracker.add(widget);
+        }
+        if(!widget.isAttached){
+          // Attach the widget to the mainwork area if it's not there
+          app.shell.add(widget, 'main');
+        }
+        widget.content.update();
+
+        // Activate the widget
+        app.shell.activateById(widget.id);
+      }
+    });
+
+    // Add the command to the palette
+    palette.addItem({command, category: 'nbgrader'});
+
+    // Restore the widget state
+    if (restorer != null) {
+      restorer.restore(tracker, {
+        command,
+        name: () => 'nbgrader-assignment-list'
+      });
+    }
+
+    console.log('JupyterLab extension assignment-list is activated!');
+  }
+};
+
+export default assignment_list_extension;
diff --git a/src/common/validate.ts b/src/common/validate.ts
new file mode 100644
index 000000000..35fb7fa58
--- /dev/null
+++ b/src/common/validate.ts
@@ -0,0 +1,133 @@
+import { Dialog } from '@jupyterlab/apputils';
+import { Widget } from '@lumino/widgets';
+
+
+const CSS_ERROR_DIALOG = 'nbgrader-ErrorDialog'
+const CSS_SUCCESS_DIALOG = 'nbgrader-SuccessDialog'
+
+export function showNbGraderDialog<T>(options: Partial<Dialog.IOptions<T>> = {}, error: boolean = false): Promise<Dialog.IResult<T>> {
+  const dialog = new Dialog(options);
+
+  if (error) dialog.addClass(CSS_ERROR_DIALOG);
+  else dialog.addClass(CSS_SUCCESS_DIALOG);
+
+  return dialog.launch();
+}
+
+export function validate(
+  data: { [x: string]: any; value: string; changed: string | any[]; passed: { source: string; }[]; failed: string | any[]; }
+  ): boolean {
+
+  var body = document.createElement('div') as HTMLDivElement;
+  body.id = 'validation-message';
+  var isError = false;
+  var success = false;
+
+  if (data.success === true) {
+      if (typeof(data.value) === "string") {
+          data = JSON.parse(data.value);
+      } else {
+          data = data.value;
+      }
+      if (data.type_changed !== undefined) {
+        isError = true;
+        for (let i=0; i<data.type_changed.length; i++) {
+          var div = document.createElement('div')
+          var paragraph = document.createElement('p')
+          paragraph.innerText = `The following ${data.type_changed[i].old_type} cell has changed to a ${data.type_changed[i].new_type} cell, but it should not have!`;
+          div.append(paragraph);
+          body.append(div);
+          var pre = document.createElement('pre');
+          pre.innerText = data.type_changed[i].source;
+          body.append(pre);
+        }
+        body.classList.add("validation-type-changed");
+      }
+      else if (data.changed !== undefined) {
+        isError = true;
+        for (var i=0; i<data.changed.length; i++) {
+          var div = document.createElement('div')
+          var paragraph = document.createElement('p')
+          paragraph.innerText = 'The source of the following cell has changed, but it should not have!';
+          div.append(paragraph);
+          body.append(div);
+          var pre = document.createElement('pre');
+          pre.innerText = data.changed[i].source;
+          body.append(pre);
+        }
+        body.classList.add("validation-changed");
+
+      } else if (data.passed !== undefined) {
+        for (var i=0; i<data.changed.length; i++) {
+          var div = document.createElement('div');
+          var paragraph = document.createElement('p');
+          paragraph.innerText = 'The following cell passed:';
+          div.append(paragraph)
+          body.append(div)
+          var pre = document.createElement('pre');
+          pre.innerText = data.passed[i].source;
+          body.append(pre);
+        }
+        body.classList.add("validation-passed");
+
+      } else if (data.failed !== undefined) {
+        isError = true;
+        for (var i=0; i<data.failed.length; i++) {
+          var div = document.createElement('div');
+          var paragraph = document.createElement('p');
+          paragraph.innerText = 'The following cell failed:';
+          div.append(paragraph);
+          body.append(div);
+
+          const source = document.createElement('div');
+          source.classList.add('jp-RenderedText');
+          var pre1 = document.createElement('pre');
+          pre1.innerText = data.failed[i].source;
+          source.append(pre1);
+          body.append(source);
+
+          const error = document.createElement('div');
+          error.classList.add('jp-RenderedText');
+          var pre2 = document.createElement('pre');
+          pre2.innerHTML = data.failed[i].error;
+          error.append(pre2);
+
+          body.append(error);
+
+        }
+        body.classList.add('validation-failed');
+
+      } else {
+        var div = document.createElement('div')
+        var paragraph  = document.createElement('p')
+        paragraph.innerText = 'Success! Your notebook passes all the tests.';
+        div.append(paragraph);
+        body.append(div);
+
+        body.classList.add("validation-success");
+        success = true;
+      }
+
+  } else {
+    isError = true;
+    var div  = document.createElement('div');
+    var paragraph = document.createElement('p');
+    paragraph.innerText = 'There was an error running the validate command:';
+    div.append(paragraph);
+    body.append(div);
+    var pre = document.createElement('pre');
+    pre.innerText = data.value
+    body.append(pre);
+
+  }
+
+  let b: Widget;
+  b = new Widget({node: body});
+  showNbGraderDialog({
+    title: "Validation Results",
+    body: b,
+    buttons: [Dialog.okButton()]
+  }, isError);
+
+  return success;
+};
diff --git a/src/course_list/courselist.ts b/src/course_list/courselist.ts
new file mode 100644
index 000000000..98f0b0f6f
--- /dev/null
+++ b/src/course_list/courselist.ts
@@ -0,0 +1,175 @@
+import { JupyterFrontEnd } from '@jupyterlab/application';
+
+import { URLExt } from '@jupyterlab/coreutils';
+
+import { ServerConnection } from '@jupyterlab/services';
+
+/**
+ * Call the API extension
+ *
+ * @param endPoint API REST end point for the extension
+ * @param init Initial values for the request
+ * @returns The response body interpreted as JSON
+ */
+export async function requestAPI<T>(
+  endPoint = '',
+  init: RequestInit = {}
+): Promise<T> {
+  // Make request to Jupyter API
+  const settings = ServerConnection.makeSettings();
+  const requestUrl = URLExt.join(
+    settings.baseUrl,
+    // 'course_list', // API Namespace
+    endPoint
+  );
+
+  let response: Response;
+  try {
+    response = await ServerConnection.makeRequest(requestUrl, init, settings);
+  } catch (error) {
+    throw new ServerConnection.NetworkError(error);
+  }
+
+  const data = await response.json();
+
+  if (!response.ok) {
+    throw new ServerConnection.ResponseError(response, data.message);
+  }
+
+  return data;
+}
+
+function createElementFromCourse(data: any, app: JupyterFrontEnd) {
+    var element = document.createElement('div') as HTMLDivElement;
+    element.classList.add('list_item','row');
+
+    var row = document.createElement('div') as HTMLDivElement;
+    row.classList.add('col-md-12');
+
+    var container = document.createElement('span') as HTMLSpanElement;
+    container.classList.add('item_name','col-sm-2');
+
+    var anchor = document.createElement('a') as HTMLAnchorElement;
+    anchor.href = '#';
+    anchor.innerText = data['course_id'];
+
+    if (data['kind'] == 'local') {
+      anchor.href = '#';
+      anchor.onclick = function() {
+        app.commands.execute('nbgrader:open-formgrader', data);
+      }
+    }
+    else {
+      const url = data['url'] as string;
+      anchor.href = URLExt.join(url.replace(/formgrader$/, 'lab'), 'workspaces', 'formgrader');
+      anchor.target = 'blank';
+    }
+
+    var fgkind = document.createElement('span') as HTMLSpanElement;
+    fgkind.classList.add('item_course', 'col-sm-2');
+    fgkind.textContent = data['kind'];
+
+    container.append(anchor);
+    row.append(container);
+    row.append(fgkind);
+    element.append(row);
+    return element;
+
+}
+
+export class CourseList {
+    listplaceholder: HTMLDivElement;
+    listloading: HTMLDivElement;
+    listerror: HTMLDivElement;
+    listerrortext: HTMLDivElement;
+    app: JupyterFrontEnd;
+
+    constructor(public course_list_element: HTMLDivElement, app: JupyterFrontEnd) {
+        this.app = app;
+        this.listplaceholder = document.createElement('div') as HTMLDivElement;
+        this.listplaceholder.id = 'formgrader_list_placeholder';
+        this.listplaceholder.classList.add('list_placeholder');
+        var listplaceholdertext = document.createElement('div') as HTMLDivElement;
+        listplaceholdertext.textContent = 'There are no available formgrader services.';
+        this.listplaceholder.hidden = true;
+        this.listplaceholder.appendChild(listplaceholdertext);
+        this.course_list_element.appendChild(this.listplaceholder);
+
+        this.listloading = document.createElement('div') as HTMLDivElement;
+        this.listloading.id = 'formgrader_list_loading';
+        this.listloading.classList.add('list_loading');
+        var listloadingtext = document.createElement('div') as HTMLDivElement;
+        listloadingtext.textContent = 'Loading, please wait...';
+        this.listloading.appendChild(listloadingtext);
+        this.course_list_element.appendChild(this.listloading);
+
+        this.listerror = document.createElement('div') as HTMLDivElement;
+        this.listerror.id = 'formgrader_list_error';
+        this.listerror.classList.add('list_error');
+        this.listerrortext = document.createElement('div') as HTMLDivElement;
+        this.listerrortext.textContent = 'There are no available formgrader services.';
+        this.listerror.hidden = true;
+        this.listerror.appendChild(this.listerrortext);
+        this.course_list_element.appendChild(this.listerror);
+    }
+
+    clear_list(loading: boolean) {
+        while((this.course_list_element.lastChild as HTMLElement).classList.contains('list_item')){
+            this.course_list_element.removeChild(this.course_list_element.lastChild);
+        }
+
+        if (loading) {
+            // show loading
+            this.listloading.hidden = false;
+            // hide placeholders and errors
+            this.listplaceholder.hidden = true;
+            this.listerror.hidden = true;
+        } else {
+            // show placeholders
+            this.listplaceholder.hidden = false;
+            // hide loading and errors
+            this.listloading.hidden = true;
+            this.listerror.hidden = true;
+        }
+    }
+
+    show_error(error: Error) {
+        while((this.course_list_element.lastChild as HTMLElement).classList.contains('list_item')){
+            this.course_list_element.removeChild(this.course_list_element.lastChild);
+        }
+        // show errors
+        this.listerrortext.textContent = error.message;
+        this.listerror.hidden = false;
+        // hide loading and placeholding
+        this.listloading.hidden = true;
+        this.listplaceholder.hidden = true;
+    }
+
+    load_list() {
+        this.clear_list(true);
+
+        requestAPI<any>('formgraders')
+            .then((data) => this.handle_load_list.call(this, data))
+            .catch(this.show_error);
+    }
+
+    handle_load_list(data: any){
+        if(data.success){
+            this.load_list_success(data.value);
+        } else {
+            this.show_error(data.value);
+        }
+    }
+
+    load_list_success(data: any) {
+        this.clear_list(false);
+        var len = data.length;
+        if(len > 0){
+            this.listplaceholder.hidden = true;
+        }
+        for (var i=0; i<len; i++) {
+            this.course_list_element.appendChild(createElementFromCourse(data[i], this.app));
+        }
+
+    }
+}
diff --git a/src/course_list/index.ts b/src/course_list/index.ts
new file mode 100644
index 000000000..a91bf7e15
--- /dev/null
+++ b/src/course_list/index.ts
@@ -0,0 +1,159 @@
+import {
+    ILayoutRestorer,
+    JupyterFrontEnd,
+    JupyterFrontEndPlugin
+} from '@jupyterlab/application';
+
+import { ICommandPalette, MainAreaWidget, WidgetTracker } from '@jupyterlab/apputils';
+
+import { Widget } from '@lumino/widgets';
+
+import { requestAPI, CourseList } from './courselist';
+
+
+const PLUGIN_ID = "nbgrader/course-list";
+const COMMAND_NAME = "nbgrader:open-course-list";
+
+class CourseListWidget extends Widget {
+    version_alert: HTMLDivElement;
+    courselist: CourseList;
+    app: JupyterFrontEnd;
+
+    constructor(app: JupyterFrontEnd) {
+        super();
+        this.app = app;
+        var maindiv = document.createElement('div') as HTMLDivElement;
+        maindiv.id = 'courses';
+        maindiv.classList.add("tab-pane");
+
+        this.version_alert = document.createElement('div') as HTMLDivElement;
+        this.version_alert.classList.add('alert', 'alert-danger', 'version_error')
+        this.version_alert.hidden = true;
+        maindiv.appendChild(this.version_alert);
+
+        var panelgroup = document.createElement('div') as HTMLDivElement;
+        panelgroup.classList.add('panel-group');
+        var panel = document.createElement('div') as HTMLDivElement;
+        panel.classList.add('panel', 'panel-default');
+
+        var panelheading = document.createElement('div') as HTMLDivElement;
+        panelheading.classList.add('panel-heading');
+        panelheading.textContent = 'Available formgraders';
+
+        var formgraderbuttons = document.createElement('span') as HTMLSpanElement;
+        formgraderbuttons.id = 'formgrader_buttons';
+        formgraderbuttons.classList.add('pull-right', 'toolbar_buttons');
+
+        var refreshbutton = document.createElement('button') as HTMLButtonElement;
+        refreshbutton.id = 'refresh_formgrader_list';
+        refreshbutton.title = 'Refresh formgrader list';
+        refreshbutton.classList.add('btn', 'btn-default', 'btn-xs');
+
+        // I have no idea why this is an italics tag, but I'm just recreating it so :/
+        var buttonimg = document.createElement('i') as HTMLElement;
+        buttonimg.classList.add('fa', 'fa-refresh');
+
+        refreshbutton.appendChild(buttonimg);
+        formgraderbuttons.appendChild(refreshbutton);
+        panelheading.appendChild(formgraderbuttons);
+        panel.appendChild(panelheading);
+
+        var panelbody = document.createElement('div') as HTMLDivElement;
+        panelbody.classList.add('panel-body');
+        var formgraderlist = document.createElement('div') as HTMLDivElement;
+        formgraderlist.id = 'formgrader_list';
+        formgraderlist.classList.add('list_container');
+
+        // further initialization of formgraderlist is in here
+        this.courselist = new CourseList(formgraderlist, this.app);
+
+        panelbody.appendChild(formgraderlist);
+        panel.appendChild(panelbody);
+        panelgroup.appendChild(panel);
+        maindiv.appendChild(panelgroup);
+        this.node.appendChild(maindiv);
+        this.node.style.overflowY = 'auto';
+
+        refreshbutton.onclick = () => this.courselist.load_list.call(this.courselist);
+        this.checkNbGraderVersion();
+        this.courselist.load_list();
+    }
+
+    checkNbGraderVersion() {
+        let nbgrader_version = '0.8.0.dev';
+        requestAPI<any>('nbgrader_version?version='+nbgrader_version)
+            .then(response => {
+                if (!response['success']) {
+                    this.version_alert.textContent = response['message'];
+                    this.version_alert.hidden = false;
+                }
+            })
+            .catch(reason => {
+                console.error(
+                    `The course_list server extension appears to be missing.\n${reason}`
+                );
+            });
+    }
+}
+
+/**
+ * Initialization data for the course_list extension.
+ */
+export const course_list_extension: JupyterFrontEndPlugin<void> = {
+    id: PLUGIN_ID,
+    autoStart: true,
+    requires: [ICommandPalette],
+    optional: [ILayoutRestorer],
+
+    activate: async (
+      app: JupyterFrontEnd,
+      palette: ICommandPalette,
+      restorer: ILayoutRestorer | null
+    ) => {
+
+        let widget: MainAreaWidget<CourseListWidget>;
+
+        const command:string = COMMAND_NAME;
+
+        // Track the widget state
+        let tracker = new WidgetTracker<MainAreaWidget<CourseListWidget>>({
+            namespace: 'nbgrader-course-list'
+        });
+
+        app.commands.addCommand(command, {
+            label: 'Course List',
+            execute: () => {
+                if (!widget) {
+                    const content = new CourseListWidget(app);
+                    widget = new MainAreaWidget({content});
+                    widget.id = 'nbgrader-course-list'
+                    widget.addClass('nbgrader-mainarea-widget');
+                    widget.title.label = 'Courses'
+                    widget.title.closable = true;
+                }
+                if (!tracker.has(widget)) {
+                    tracker.add(widget);
+                }
+                if (!widget.isAttached) {
+                    app.shell.add(widget, 'main');
+                }
+                widget.content.update();
+                app.shell.activateById(widget.id);
+            }
+        })
+
+        palette.addItem({ command, category: "nbgrader" });
+
+        // Restore the widget state
+        if (restorer != null){
+            restorer.restore(tracker, {
+                command,
+                name: () => 'nbgrader-course-list'
+            });
+        }
+
+        console.log('JupyterLab extension course-list is activated!');
+  }
+};
+
+export default course_list_extension;
diff --git a/src/create_assignment/create_assignment_extension.ts b/src/create_assignment/create_assignment_extension.ts
new file mode 100644
index 000000000..75f284028
--- /dev/null
+++ b/src/create_assignment/create_assignment_extension.ts
@@ -0,0 +1,1024 @@
+import {
+  Dialog,
+  Styling
+} from '@jupyterlab/apputils';
+
+import {
+  Cell,
+  ICellModel
+} from '@jupyterlab/cells';
+
+import {
+  IChangedArgs
+} from '@jupyterlab/coreutils';
+
+import {
+  DocumentRegistry
+} from '@jupyterlab/docregistry';
+
+import {
+  INotebookModel,
+  INotebookTracker,
+  Notebook,
+  NotebookPanel
+} from '@jupyterlab/notebook';
+
+import {
+  IObservableJSON,
+  IObservableList,
+  IObservableMap,
+  IObservableUndoableList
+} from '@jupyterlab/observables';
+
+import {
+  ReadonlyPartialJSONValue
+} from '@lumino/coreutils';
+
+import {
+  Message
+} from '@lumino/messaging';
+
+import {
+  ISignal,
+  Signal
+} from '@lumino/signaling';
+
+import {
+  Panel,
+  Widget
+} from '@lumino/widgets';
+
+import {
+  CellModel,
+  CellType,
+  NBGRADER_SCHEMA_VERSION,
+  ToolData
+} from './create_assignment_model';
+
+const CSS_CELL_HEADER = 'nbgrader-CellHeader';
+const CSS_CELL_ID = 'nbgrader-CellId';
+const CSS_CELL_POINTS = 'nbgrader-CellPoints';
+const CSS_CELL_TYPE = 'nbgrader-CellType';
+const CSS_CELL_WIDGET = 'nbgrader-CellWidget';
+const CSS_CREATE_ASSIGNMENT_WIDGET = 'nbgrader-CreateAssignmentWidget';
+const CSS_LOCK_BUTTON = 'nbgrader-LockButton';
+const CSS_MOD_ACTIVE = 'nbgrader-mod-active';
+const CSS_MOD_HIGHLIGHT = 'nbgrader-mod-highlight';
+const CSS_MOD_LOCKED = 'nbgrader-mod-locked';
+const CSS_MOD_UNEDITABLE = 'nbgrader-mod-uneditable';
+const CSS_NOTEBOOK_HEADER_WIDGET = 'nbgrader-NotebookHeaderWidget';
+const CSS_NOTEBOOK_PANEL_WIDGET = 'nbgrader-NotebookPanelWidget';
+const CSS_NOTEBOOK_POINTS = 'nbgrader-NotebookPoints';
+const CSS_NOTEBOOK_WIDGET = 'nbgrader-NotebookWidget';
+const CSS_TOTAL_POINTS_INPUT = 'nbgrader-TotalPointsInput';
+const CSS_ERROR_DIALOG = 'nbgrader-ErrorDialog'
+
+
+function showErrorDialog<T>(options: Partial<Dialog.IOptions<T>> = {}): Promise<Dialog.IResult<T>> {
+  const dialog = new Dialog(options);
+  dialog.addClass(CSS_ERROR_DIALOG);
+  return dialog.launch();
+}
+
+/**
+ * A widget which shows the "Create Assignment" widgets for the active notebook.
+ */
+export class CreateAssignmentWidget extends Panel {
+  private activeNotebook: NotebookPanel;
+  private currentNotebookListener: (tracker: INotebookTracker,
+                                    panel: NotebookPanel) => void;
+  private notebookPanelWidgets = new Map<NotebookPanel, NotebookPanelWidget>();
+  private notebookTracker: INotebookTracker;
+
+  constructor(tracker: INotebookTracker) {
+    super();
+    this.addClass(CSS_CREATE_ASSIGNMENT_WIDGET);
+    this.addNotebookListeners(tracker);
+    this.activeNotebook = null;
+    this.notebookTracker = tracker;
+  }
+
+  private addNotebookListeners(tracker: INotebookTracker): void {
+    this.currentNotebookListener = this.getCurrentNotebookListener();
+    tracker.currentChanged.connect(this.currentNotebookListener);
+  }
+
+  private async addNotebookWidget(tracker: INotebookTracker,
+                                  panel: NotebookPanel) {
+    await panel.revealed
+    const notebookPanelWidget = new NotebookPanelWidget(panel);
+    this.addWidget(notebookPanelWidget);
+    this.notebookPanelWidgets.set(panel, notebookPanelWidget);
+    panel.disposed.connect(() => {
+      notebookPanelWidget.dispose();
+    });
+    notebookPanelWidget.disposed.connect(() => {
+      this.notebookPanelWidgets.delete(panel);
+    });
+    if (tracker.currentWidget != panel) {
+      notebookPanelWidget.hide();
+    }
+    return panel.revealed;
+  }
+
+  dispose(): void {
+    if (this.isDisposed) {
+      return;
+    }
+    if (this.notebookPanelWidgets != null) {
+      for (const widget of this.notebookPanelWidgets) {
+        widget[1].dispose();
+      }
+    }
+    if (this.notebookTracker != null) {
+      this.removeNotebookListeners(this.notebookTracker);
+    }
+    this.activeNotebook = null;
+    this.notebookPanelWidgets = null;
+    this.notebookTracker = null;
+    super.dispose();
+  }
+
+  private getCurrentNotebookListener(): (tracker: INotebookTracker,
+                                         panel: NotebookPanel) => void {
+    return async (tracker: INotebookTracker, panel: NotebookPanel) => {
+      if (this.activeNotebook != null) {
+        const widget = this.notebookPanelWidgets.get(this.activeNotebook);
+        if (widget != null) {
+          widget.hide();
+        }
+      }
+      if (panel != null) {
+        if (this.isVisible && this.notebookPanelWidgets.get(panel) == null) {
+          await this.addNotebookWidget(tracker, panel);
+        }
+        const widget = this.notebookPanelWidgets.get(panel)
+        if (widget != null) {
+          widget.show();
+        }
+      }
+      this.activeNotebook = panel;
+    }
+  }
+
+  protected onBeforeShow(msg: Message): void {
+    super.onBeforeShow(msg);
+    const notebookWidget = this.notebookPanelWidgets.get(this.activeNotebook);
+    if (notebookWidget == null) {
+      this.addNotebookWidget(this.notebookTracker, this.activeNotebook);
+    }
+    else {
+      notebookWidget.show();
+    }
+  }
+
+  private removeNotebookListeners(tracker: INotebookTracker): void {
+    tracker.currentChanged.disconnect(this.currentNotebookListener);
+    this.currentNotebookListener = null;
+  }
+}
+
+/**
+ * Shows a cell's assignment data.
+ */
+class CellWidget extends Panel {
+  private _cell: Cell;
+  private _click = new Signal<this, void>(this);
+  private metadataChangedHandler:
+      (metadata: IObservableJSON,
+       changedArgs: IObservableMap.IChangedArgs<ReadonlyPartialJSONValue>) =>
+      void;
+  private onclick: (this: HTMLElement, ev: MouseEvent) => any;
+  private lock: HTMLAnchorElement;
+  private gradeId: HTMLDivElement;
+  private points: HTMLDivElement;
+  private taskInput: HTMLSelectElement;
+  private gradeIdInput: HTMLInputElement;
+  private pointsInput: HTMLInputElement;
+
+  constructor(cell: Cell) {
+    super();
+    this._cell = cell;
+    this.addMetadataListener(cell);
+    this.initLayout();
+    this.initClickListener();
+    this.initInputListeners();
+    this.initMetadata(cell);
+    this.addClass(CSS_CELL_WIDGET);
+  }
+
+  private async addMetadataListener(cell: Cell) {
+    await cell.ready;
+    this.metadataChangedHandler = this.getMetadataChangedHandler();
+    cell.model.metadata.changed.connect(this.metadataChangedHandler);
+  }
+
+  /**
+   * The notebook cell associated with this widget.
+   */
+  get cell(): Cell {
+    return this._cell;
+  }
+
+  private cleanNbgraderData(cell: Cell): void {
+    CellModel.cleanNbgraderData(cell.model.metadata, cell.model.type);
+  }
+
+  /**
+   * A signal for when this widget receives a click event.
+   */
+  get click(): ISignal<this, void> {
+    return this._click;
+  }
+
+  dispose(): void {
+    if (this.isDisposed) {
+      return;
+    }
+    if (this.metadataChangedHandler != null) {
+      this.cell?.model?.metadata?.changed?.disconnect(
+          this.metadataChangedHandler);
+    }
+    if (this.onclick != null) {
+      this.node?.removeEventListener('click', this.onclick);
+    }
+    if (this.taskInput != null) {
+      this.taskInput.onchange = null;
+    }
+    if (this.gradeIdInput != null) {
+      this.gradeIdInput.onchange = null;
+    }
+    if (this.pointsInput != null) {
+      this.pointsInput.onchange = null;
+    }
+    this._cell = null;
+    this._click = null;
+    this.metadataChangedHandler = null;
+    this.onclick = null;
+    this.lock = null;
+    this.gradeId = null;
+    this.points = null;
+    this.taskInput = null;
+    this.gradeIdInput = null;
+    this.pointsInput = null;
+    super.dispose();
+  }
+
+  private getCellStateChangedListener(
+      srcPrompt: HTMLElement, destPrompt: HTMLElement):
+      (model: ICellModel, changedArgs: IChangedArgs<any, any, string>) => void {
+    return (model: ICellModel, changedArgs: IChangedArgs<any, any, string>) => {
+      if (changedArgs.name == 'executionCount') {
+        destPrompt.innerText = srcPrompt.innerText;
+      }
+    }
+  }
+
+  private getMetadataChangedHandler():
+      (metadata: IObservableJSON,
+       changedArgs: IObservableMap.IChangedArgs<ReadonlyPartialJSONValue>) =>
+      void {
+    return (metadata: IObservableJSON, changedArgs:
+            IObservableMap.IChangedArgs<ReadonlyPartialJSONValue>) => {
+      const nbgraderData = CellModel.getNbgraderData(metadata);
+      const toolData = CellModel.newToolData(nbgraderData, this.cell.model.type);
+      this.updateValues(toolData);
+    }
+  }
+
+  private getOnInputChanged(): () => void {
+    return () => {
+      const toolData = new ToolData();
+      toolData.type = this.taskInput.value as CellType;
+      if (!this.gradeId.classList.contains(CSS_MOD_UNEDITABLE)) {
+        toolData.id = this.gradeIdInput.value;
+      }
+      else {
+        const nbgraderData = CellModel.getNbgraderData(
+            this.cell.model.metadata);
+        if (nbgraderData?.grade_id == null) {
+          toolData.id = 'cell-' + this.randomString(16);
+        }
+        else {
+          toolData.id = nbgraderData.grade_id;
+        }
+        this.gradeIdInput.value = toolData.id;
+      }
+      if (!this.points.classList.contains(CSS_MOD_UNEDITABLE)) {
+        toolData.points = this.pointsInput.valueAsNumber;
+      }
+      const data = CellModel.newNbgraderData(toolData);
+      CellModel.setNbgraderData(data, this.cell.model.metadata);
+    }
+  }
+
+  private getOnTaskInputChanged(): () => void {
+    const onInputChanged = this.getOnInputChanged();
+    return () => {
+      onInputChanged();
+      this.updateDisplayClass();
+    }
+  }
+
+  private initClickListener(): void {
+    this.onclick = () => {
+      this._click.emit();
+    };
+    this.node.addEventListener('click', this.onclick);
+  }
+
+  private initInputListeners(): void {
+    this.taskInput.onchange = this.getOnTaskInputChanged();
+    this.gradeIdInput.onchange = this.getOnInputChanged();
+    this.pointsInput.onchange = this.getOnInputChanged();
+  }
+
+  private initLayout(): void {
+    const bodyElement = document.createElement('div');
+    const headerElement = this.newHeaderElement();
+    const taskElement = this.newTaskElement();
+    const idElement = this.newIdElement();
+    const pointsElement = this.newPointsElement();
+    const elements = [headerElement, taskElement, idElement, pointsElement];
+    const fragment = document.createDocumentFragment();
+    for (const element of elements) {
+      fragment.appendChild(element);
+    }
+    bodyElement.appendChild(fragment);
+    this.node.appendChild(bodyElement);
+    this.lock = headerElement.getElementsByTagName('a')[0];
+    this.gradeId = idElement;
+    this.points = pointsElement;
+    this.taskInput = taskElement.getElementsByTagName('select')[0];
+    this.gradeIdInput = idElement.getElementsByTagName('input')[0];
+    this.pointsInput = pointsElement.getElementsByTagName('input')[0];
+  }
+
+  private async initMetadata(cell: Cell) {
+    await cell.ready
+    if (cell.model == null) {
+      return;
+    }
+    this.cleanNbgraderData(cell);
+    const nbgraderData = CellModel.getNbgraderData(cell.model.metadata);
+    const toolData = CellModel.newToolData(nbgraderData, this.cell.model.type);
+    CellModel.clearCellType(cell.model.metadata);
+    this.updateDisplayClass();
+    this.updateValues(toolData);
+  }
+
+  private newHeaderElement(): HTMLDivElement {
+    const element = document.createElement('div');
+    element.className = CSS_CELL_HEADER;
+    const promptNode =  this.cell.promptNode.cloneNode(true) as HTMLElement;
+    element.appendChild(promptNode);
+    this.cell.model.stateChanged.connect(this.getCellStateChangedListener(
+      this.cell.promptNode, promptNode));
+    const lockElement = document.createElement('a');
+    lockElement.className = CSS_LOCK_BUTTON;
+    const listElement = document.createElement('li');
+    listElement.className = 'fa fa-lock';
+    listElement.title = 'Student changes will be overwritten';
+    lockElement.appendChild(listElement);
+    element.appendChild(lockElement);
+    return element;
+  }
+
+  private newIdElement(): HTMLDivElement {
+    const element = document.createElement('div');
+    element.className = CSS_CELL_ID;
+    const label = document.createElement('label');
+    label.textContent = 'ID: ';
+    const input = document.createElement('input');
+    input.type = 'text';
+    label.appendChild(input);
+    element.appendChild(label);
+    return element;
+  }
+
+  private newPointsElement(): HTMLDivElement {
+    const element = document.createElement('div');
+    element.className = CSS_CELL_POINTS;
+    const label = document.createElement('label');
+    label.textContent = 'Points: ';
+    const input = document.createElement('input');
+    input.type = 'number';
+    input.min = '0';
+    label.appendChild(input);
+    element.appendChild(label);
+    return element;
+  }
+
+  private newTaskElement(): HTMLDivElement {
+    const element = document.createElement('div');
+    element.className = CSS_CELL_TYPE;
+    const label = document.createElement('label');
+    label.textContent = 'Type: ';
+    const select = document.createElement('select');
+    const options = new Map<string, string>([
+      ['', '-'],
+      ['manual', 'Manually graded answer'],
+      ['task', 'Manually graded task'],
+      ['solution', 'Autograded answer'],
+      ['tests', 'Autograded tests'],
+      ['readonly', 'Read-only']
+    ]);
+    if (this.cell.model.type !== 'code') {
+      options.delete('solution');
+      options.delete('tests');
+    }
+    const fragment = document.createDocumentFragment();
+    for (const optionEntry of options.entries()) {
+      const option = document.createElement('option');
+      option.value = optionEntry[0];
+      option.innerHTML = optionEntry[1];
+      fragment.appendChild(option);
+    }
+    select.appendChild(fragment);
+    const selectWrap = Styling.wrapSelect(select);
+    label.appendChild(selectWrap);
+    element.appendChild(label);
+    return element;
+  }
+
+  private randomString(length: number): string {
+    var result = '';
+    var chars = 'abcdef0123456789';
+    var i;
+    for (i = 0; i < length; i++) {
+      result += chars[Math.floor(Math.random() * chars.length)];
+    }
+    return result;
+  }
+
+  /**
+   * Sets this cell as active/focused.
+   */
+  setActive(active: boolean): void {
+    if (active) {
+      this.addClass(CSS_MOD_ACTIVE);
+    }
+    else {
+      this.removeClass(CSS_MOD_ACTIVE);
+    }
+  }
+
+  private setGradeId(value: string): void {
+    this.gradeIdInput.value = value;
+  }
+
+  private setElementEditable(element: HTMLElement, visible: boolean): void {
+    if (visible) {
+      element.classList.remove(CSS_MOD_UNEDITABLE);
+    }
+    else {
+      element.classList.add(CSS_MOD_UNEDITABLE);
+    }
+  }
+
+  private setGradeIdEditable(visible: boolean): void {
+    this.setElementEditable(this.gradeId, visible);
+  }
+
+  private setPoints(value: number): void {
+    this.pointsInput.value = value.toString();
+  }
+
+  private setPointsEditable(visible: boolean): void {
+    this.setElementEditable(this.points, visible);
+  }
+
+  private setTask(value: string): void {
+    this.taskInput.value = value;
+  }
+
+  private updateDisplayClass(): void {
+    const data = CellModel.getNbgraderData(this.cell.model.metadata);
+    if (CellModel.isRelevantToNbgrader(data)) {
+      this.addClass(CSS_MOD_HIGHLIGHT);
+    }
+    else {
+      this.removeClass(CSS_MOD_HIGHLIGHT);
+    }
+  }
+
+  private updateValues(data: ToolData): void {
+    this.setTask(data.type);
+    if (data.id == null) {
+      this.setGradeIdEditable(false);
+      this.setGradeId('');
+    }
+    else {
+      this.setGradeId(data.id);
+      this.setGradeIdEditable(true);
+    }
+    if (data.points == null) {
+      this.setPointsEditable(false);
+      this.setPoints(0);
+    }
+    else {
+      this.setPoints(data.points);
+      this.setPointsEditable(true);
+    }
+    if (data.locked) {
+      this.lock.classList.add(CSS_MOD_LOCKED);
+    }
+    else {
+      this.lock.classList.remove(CSS_MOD_LOCKED);
+    }
+  }
+}
+
+/**
+ * The header of a notebook's Create Assignment widget.
+ *
+ * Displays the total points in the notebook.
+ */
+class NotebookHeaderWidget extends Widget {
+  private pointsInput: HTMLInputElement;
+
+  constructor() {
+    super();
+    this.addClass(CSS_NOTEBOOK_HEADER_WIDGET);
+    this.initLayout();
+  }
+
+  dispose(): void {
+    if (this.isDisposed) {
+      return;
+    }
+    this.pointsInput = null;
+    super.dispose();
+  }
+
+  private initLayout(): void {
+    const totalPoints = this.newTotalPointsElement();
+    this.node.appendChild(totalPoints);
+    this.pointsInput = totalPoints.getElementsByTagName('input')[0];
+  }
+
+  private newTotalPointsElement(): HTMLDivElement {
+    const element = document.createElement('div');
+    element.className = CSS_NOTEBOOK_POINTS;
+    const label = document.createElement('label');
+    label.innerText = 'Total points:';
+    const input = document.createElement('input');
+    input.className = CSS_TOTAL_POINTS_INPUT;
+    input.type = 'number';
+    input.disabled = true;
+    label.appendChild(input);
+    element.appendChild(label);
+    return element;
+  }
+
+  /**
+   * The total points in the notebook.
+   */
+  set totalPoints(points: number) {
+    if (this.pointsInput != null) {
+      this.pointsInput.value = points.toString();
+    }
+  }
+}
+
+/**
+ * Contains a list of {@link CellWidget}s for a notebook.
+ */
+class NotebookWidget extends Panel {
+  private activeCell = null as Cell;
+  private activeCellWidgetListener: (cellWidget: CellWidget) => void;
+  private cellListener: (notebook: Notebook, cell: Cell) => void;
+  private cellListListener:
+      (sender: IObservableUndoableList<ICellModel>,
+       args: IObservableList.IChangedArgs<ICellModel>) => void;
+  private _cellMetadataChanged = new Signal<this, CellWidget>(this);
+  private cellWidgets = new Map<Cell, CellWidget>();
+  private metadataChangedHandlers = new Map<
+      CellWidget,
+      (metadata: IObservableJSON,
+       args: IObservableMap.IChangedArgs<ReadonlyPartialJSONValue>) => void>();
+  private notebookDisposedListener: (panel: NotebookPanel) => void;
+  private _notebookPanel: NotebookPanel;
+  private validateIdsListener:
+      (context: DocumentRegistry.IContext<INotebookModel>,
+       args: DocumentRegistry.SaveState) => void;
+
+  constructor(panel: NotebookPanel) {
+    super();
+    this.activeCell = panel.content.activeCell;
+    this.activeCellWidgetListener = this.getActiveCellWidgetListener();
+    this._notebookPanel = panel;
+    this.addClass(CSS_NOTEBOOK_WIDGET);
+    this.addCellListener(panel);
+    this.addCellListListener(panel);
+    this.initCellWidgets(panel.content);
+    this.validateSchemaVersion();
+    this.addValidateIdsListener();
+    this.addNotebookDisposedListener(panel);
+  }
+
+  private addCellListener(panel: NotebookPanel) {
+    this.cellListener = this.getActiveCellListener();
+    panel.content.activeCellChanged.connect(this.cellListener);
+  }
+
+  private addCellListListener(panel: NotebookPanel) {
+    this.cellListListener =
+      (sender: IObservableUndoableList<ICellModel>,
+       args: IObservableList.IChangedArgs<ICellModel>) => {
+         switch (args.type) {
+           case 'add': {
+             const cell = this.findCellInArray(args.newValues[0],
+                                                panel.content.widgets);
+             this.addCellWidget(cell, args.newIndex);
+             break;
+           }
+           case 'move': {
+             const cell = panel.content.widgets[args.newIndex];
+             this.moveCellWidget(cell, args.newIndex);
+             break;
+           }
+           case 'remove': {
+             const cell = this.findDeadCell(this.cellWidgets.keys());
+             if (cell != null) {
+               this.removeCellWidget(cell);
+             }
+             else {
+               console.warn('nbgrader: Unable to find newly deleted cell.');
+             }
+             break;
+           }
+           case 'set': {
+             // Existing notebook cell changed. Update the corresponding widget.
+             const oldCell = this.findDeadCell(this.cellWidgets.keys());
+             if (oldCell != null) {
+               const newCell = this.findCellInArray(args.newValues[0],
+                                                     panel.content.widgets);
+               this.cellWidgets.get(oldCell).dispose();
+               this.cellWidgets.delete(oldCell);
+               const cellWidget = this.addCellWidget(newCell, args.newIndex);
+               if (this.activeCell === newCell) {
+                 cellWidget.setActive(this.activeCell === newCell);
+                 this.scrollIntoViewNearest(cellWidget);
+               }
+             }
+           }
+         }
+    };
+    panel.model.cells.changed.connect(this.cellListListener);
+  }
+
+  private addCellWidget(cell: Cell, index = undefined as number): CellWidget {
+    const cellWidget = new CellWidget(cell);
+    this.cellWidgets.set(cell, cellWidget);
+    if (index == null) {
+      this.addWidget(cellWidget);
+    }
+    else {
+      this.insertWidget(index, cellWidget);
+    }
+    cellWidget.click.connect(this.activeCellWidgetListener);
+    const metadataChangedHandler = this.getMetadataChangedHandler(cellWidget);
+    cell.model.metadata.changed.connect(metadataChangedHandler);
+    this.metadataChangedHandlers.set(cellWidget, metadataChangedHandler);
+    return cellWidget;
+  }
+
+  private addNotebookDisposedListener(panel: NotebookPanel): void {
+    this.notebookDisposedListener = this.getNotebookDisposedListener();
+    panel.disposed.connect(this.notebookDisposedListener);
+  }
+
+  private addValidateIdsListener(): void {
+    this.validateIdsListener =
+      (context: DocumentRegistry.IContext<INotebookModel>,
+       args: DocumentRegistry.SaveState) => {
+         if (args != 'started') {
+           return;
+         }
+         this.validateIds();
+      };
+    this.notebookPanel.context.saveState.connect(this.validateIdsListener);
+  }
+
+  /**
+   * A signal which is evoked when one of the cell's metadata changes.
+   */
+  get cellMetadataChanged(): Signal<this, CellWidget> {
+    return this._cellMetadataChanged;
+  }
+
+  dispose(): void {
+    if (this.isDisposed) {
+      return;
+    }
+    if (this.cellWidgets != null) {
+      for (const widgets of this.cellWidgets) {
+        this.removeCellWidget(widgets[0]);
+      }
+    }
+    if (this.cellListener != null) {
+      this.notebookPanel?.content?.activeCellChanged?.disconnect(
+          this.cellListener);
+    }
+    if (this.cellListListener != null) {
+      this.notebookPanel?.model?.cells?.changed?.disconnect(
+          this.cellListListener);
+    }
+    if (this.validateIdsListener != null) {
+      this.notebookPanel?.context?.saveState?.disconnect(
+          this.validateIdsListener);
+    }
+    if (this.notebookDisposedListener != null) {
+      this.notebookPanel?.disposed?.disconnect(this.notebookDisposedListener);
+    }
+    this.notebookPanel?.dispose();
+    this.activeCell = null;
+    this.activeCellWidgetListener = null;
+    this.cellListener = null;
+    this.cellListListener = null;
+    this._cellMetadataChanged = null;
+    this.cellWidgets = null;
+    this.metadataChangedHandlers = null;
+    this.notebookDisposedListener = null;
+    this._notebookPanel = null;
+    this.validateIdsListener = null;
+    super.dispose();
+  }
+
+  private findCellInArray(model: ICellModel, cells: readonly Cell[]): Cell {
+    return cells.find(
+      (value: Cell, index: number, obj: readonly Cell[]) => {
+        return value.model === model;
+      });
+  }
+
+  private findDeadCell(cells: IterableIterator<Cell>): Cell {
+    for (const cell of cells) {
+      if (cell.model == null) {
+        return cell;
+      }
+    }
+    return undefined;
+  }
+
+  private getActiveCellListener(): (notebook: Notebook, cell: Cell) => void {
+    return (notebook: Notebook, cell: Cell) => {
+      if (this.activeCell != null) {
+        const activeWidget = this.cellWidgets.get(this.activeCell);
+        if (activeWidget != null) {
+          activeWidget.setActive(false);
+        }
+      }
+      if (cell != null) {
+        const activeWidget = this.cellWidgets.get(cell);
+        if (activeWidget != null) {
+          activeWidget.setActive(true);
+          this.scrollIntoViewNearest(activeWidget);
+        }
+      }
+      this.activeCell = cell;
+    }
+  }
+
+  private getActiveCellWidgetListener(): (cellWidget: CellWidget) => void {
+    return (cellWidget: CellWidget) => {
+      const i = this.notebookPanel.content.widgets.indexOf(cellWidget.cell);
+      this.notebookPanel.content.activeCellIndex = i;
+    }
+  }
+
+  private getNotebookDisposedListener(): (panel: NotebookPanel) => void {
+    return (panel: NotebookPanel) => {
+      this.dispose();
+    }
+  }
+
+  private initCellWidgets(notebook: Notebook): void {
+    for (const cell of notebook.widgets) {
+      const cellWidget = this.addCellWidget(cell);
+      cellWidget.setActive(notebook.activeCell === cell);
+    }
+  }
+
+  private getMetadataChangedHandler(cellWidget: CellWidget):
+      (metadata: IObservableJSON,
+       args: IObservableMap.IChangedArgs<ReadonlyPartialJSONValue>) => void {
+    return (metadata: IObservableJSON,
+            args: IObservableMap.IChangedArgs<ReadonlyPartialJSONValue>) => {
+      this.cellMetadataChanged.emit(cellWidget);
+    }
+  }
+
+  private moveCellWidget(cell: Cell, index: number): void {
+    const cellWidget = this.cellWidgets.get(cell);
+    this.insertWidget(index, cellWidget);
+  }
+
+  /**
+   * The notebook panel associated with this widget.
+   */
+  get notebookPanel(): NotebookPanel {
+    return this._notebookPanel;
+  }
+
+  private removeCellWidget(cell: Cell): void {
+    if (this.cellWidgets == null) {
+      return;
+    }
+    const cellWidget = this.cellWidgets.get(cell);
+    if (cellWidget == null) {
+      return;
+    }
+    if (this.activeCellWidgetListener != null) {
+      cellWidget.click?.disconnect(this.activeCellWidgetListener);
+    }
+    const handler = this.metadataChangedHandlers?.get(cellWidget);
+    if (handler != null) {
+      cell.model?.metadata?.changed?.disconnect(handler);
+    }
+    this.cellWidgets.delete(cell);
+    cellWidget.dispose();
+  }
+
+  private validateIds(): void {
+    const set = new Set<string>();
+    const valid = /^[a-zA-Z0-9_\-]+$/;
+    const iter = this.notebookPanel.model.cells.iter();
+    for (let cellModel = iter.next(); cellModel != null; cellModel = iter.next()) {
+      const data = CellModel.getNbgraderData(cellModel.metadata);
+
+      if (data == null) continue;
+
+      const id = data.grade_id;
+
+      if (!valid.test(id)) {
+        this.warnInvalidId(true, false, id);
+        return;
+      }
+      else if (set.has(id)) {
+        this.warnInvalidId(false, true, id);
+        return;
+      }
+      else {
+        set.add(id);
+      }
+    }
+  }
+
+  private validateSchemaVersion(): void {
+    const iter = this.notebookPanel.model.cells.iter();
+    for (let cellModel = iter.next(); cellModel != null;
+         cellModel = iter.next()) {
+      const data = CellModel.getNbgraderData(cellModel.metadata)
+      let version = data == null ? null : data.schema_version;
+      version = version === undefined ? 0 : version;
+      if (version != null && version < NBGRADER_SCHEMA_VERSION) {
+        this.warnSchemaVersion(version);
+        return;
+      }
+    }
+  }
+
+  private warnInvalidId(badFormat: boolean, duplicateId: boolean, id: string):
+      void {
+    const options = {
+      buttons: [Dialog.okButton()],
+      title: undefined as string,
+      body: undefined as string
+    };
+    if (badFormat) {
+      options.title = 'Invalid nbgrader cell ID';
+      options.body = 'At least one cell has an invalid nbgrader ID. Cell IDs ' +
+          'must contain at least one character, and may only contain ' +
+          'letters, numbers, hyphens, and/or underscores.';
+      showErrorDialog(options);
+      return;
+    }
+    else if (duplicateId) {
+      options.title = 'Duplicate nbgrader cell ID';
+      options.body = `The nbgrader ID "${id}" has been used for more than ` +
+          `one cell. Please make sure all grade cells have unique ids.`;
+      showErrorDialog(options);
+      return;
+    }
+  }
+
+  private warnSchemaVersion(schemaVersion: number): void {
+    const version = schemaVersion.toString();
+    const notebookPath = this.notebookPanel.sessionContext.path;
+    const body = document.createElement('p');
+    const code = document.createElement('code');
+    const bodyWidget = new Widget({node: body});
+    const options = {
+      title: 'Outdated schema version',
+      body: bodyWidget,
+      buttons: [Dialog.okButton()]
+    }
+    body.innerText =
+        `At least one cell has an old version (${version}) of the ` +
+        'nbgrader metadata. Please back up this notebook and then ' +
+        'update the metadata on the command ' +
+        'line using the following command: ';
+    code.innerText = `nbgrader update ${notebookPath}`;
+    body.appendChild(code);
+    showErrorDialog(options);
+  }
+
+  private scrollIntoViewNearest(widget: CellWidget): void {
+    const parentTop = this.node.scrollTop;
+    const parentBottom = parentTop + this.node.clientHeight;
+    const widgetTop = widget.node.offsetTop;
+    const widgetBottom = widgetTop + widget.node.clientHeight;
+    if (widgetTop < parentTop) {
+      widget.node.scrollIntoView(true);
+    }
+    else if (widgetBottom > parentBottom) {
+      if (widgetBottom - widgetTop > parentBottom - parentTop) {
+        widget.node.scrollIntoView(true);
+      }
+      else {
+        widget.node.scrollIntoView(false);
+      }
+    }
+  }
+}
+
+/**
+ * Contains a notebook's "Create Assignment" UI.
+ */
+class NotebookPanelWidget extends Panel {
+  private cellListListener:
+      (cellModels: IObservableUndoableList<ICellModel>,
+       args: IObservableList.IChangedArgs<ICellModel>) => void;
+  private cellModelListener:
+      (notebookWidget: NotebookWidget, cellWidget: CellWidget) => void;
+  private notebookHeaderWidget: NotebookHeaderWidget;
+  private notebookWidget: NotebookWidget;
+
+  constructor(panel: NotebookPanel) {
+    super();
+    this.addClass(CSS_NOTEBOOK_PANEL_WIDGET);
+    this.initLayout(panel);
+    this.setUpTotalPoints();
+  }
+
+  private calcTotalPoints(): number {
+    let totalPoints = 0;
+    const iter = this.notebookWidget.notebookPanel.model.cells.iter();
+    for (let cellModel = iter.next(); cellModel != null;
+         cellModel = iter.next()) {
+      const data = CellModel.getNbgraderData(cellModel.metadata);
+      const points = (data == null || data.points == null
+                      || !CellModel.isGraded(data)) ? 0 : data.points;
+      totalPoints += points;
+    }
+    return totalPoints;
+  }
+
+  dispose(): void {
+    if (this.isDisposed) {
+      return;
+    }
+    if (this.cellListListener != null) {
+      this.notebookWidget?.notebookPanel?.model?.cells?.changed?.disconnect(
+          this.cellListListener);
+    }
+    if (this.cellModelListener != null) {
+      this.notebookWidget?.cellMetadataChanged?.disconnect(
+          this.cellModelListener);
+    }
+    this.notebookHeaderWidget?.dispose();
+    this.notebookWidget?.dispose();
+    this.cellListListener = null;
+    this.cellModelListener = null;
+    this.notebookHeaderWidget = null;
+    this.notebookWidget = null;
+    super.dispose();
+  }
+
+  private initLayout(panel: NotebookPanel): void {
+    this.notebookHeaderWidget = new NotebookHeaderWidget();
+    this.notebookWidget = new NotebookWidget(panel);
+    this.addWidget(this.notebookHeaderWidget);
+    this.addWidget(this.notebookWidget);
+  }
+
+  private setUpTotalPoints(): void {
+    this.notebookHeaderWidget.totalPoints = this.calcTotalPoints();
+    this.cellListListener =
+        (cellModels: IObservableUndoableList<ICellModel>,
+         args: IObservableList.IChangedArgs<ICellModel>) => {
+           if (args.type != 'move') {
+             this.notebookHeaderWidget.totalPoints = this.calcTotalPoints();
+           }
+         };
+    this.cellModelListener =
+        (notebookWidget: NotebookWidget, cellWidget: CellWidget) => {
+          this.notebookHeaderWidget.totalPoints = this.calcTotalPoints();
+        };
+    this.notebookWidget.notebookPanel.model.cells.changed.connect(
+        this.cellListListener);
+    this.notebookWidget.cellMetadataChanged.connect(this.cellModelListener);
+  }
+}
diff --git a/src/create_assignment/create_assignment_model.ts b/src/create_assignment/create_assignment_model.ts
new file mode 100644
index 000000000..f707d58ea
--- /dev/null
+++ b/src/create_assignment/create_assignment_model.ts
@@ -0,0 +1,342 @@
+import * as nbformat from '@jupyterlab/nbformat';
+
+import {
+  IObservableJSON
+} from '@jupyterlab/observables';
+
+import {
+  JSONObject,
+  ReadonlyJSONObject
+} from '@lumino/coreutils';
+
+const NBGRADER_KEY = 'nbgrader';
+export const NBGRADER_SCHEMA_VERSION = 3;
+
+/**
+ * A namespace for conversions between {@link NbgraderData} and
+ * {@link ToolData} and for reading and writing to notebook cells' metadata.
+ */
+export namespace CellModel {
+  /**
+   * Cleans invalid nbgrader data if necessary.
+   *
+   * @returns Whether cleaning occurred.
+   */
+  export function cleanNbgraderData(cellMetadata: IObservableJSON,
+                                    cellType: nbformat.CellType): boolean {
+    const data = CellModel.getNbgraderData(cellMetadata);
+    if (data == null || !PrivateNbgraderData.isInvalid(data, cellType)) {
+      return false;
+    }
+
+    data.schema_version = NBGRADER_SCHEMA_VERSION;
+    data.solution = false;
+    data.grade = false;
+    data.locked = false;
+    data.task = false;
+    CellModel.setNbgraderData(data, cellMetadata);
+
+    return true;
+  }
+
+  /**
+   * Removes the "cell_type" property from the nbgrader data.
+   */
+  export function clearCellType(cellMetadata: IObservableJSON): void {
+    const data = cellMetadata.get(NBGRADER_KEY) as JSONObject;
+    if (data == null) {
+      return;
+    }
+    if ('cell_type' in data) {
+      data['cell_type'] = undefined;
+    }
+    cellMetadata.set(NBGRADER_KEY, data);
+  }
+
+  /**
+   * Read the nbgrader data from a cell's metadata.
+   *
+   * @returns The nbgrader data, or null if it doesn't exist.
+   */
+  export function getNbgraderData(cellMetadata: IObservableJSON): NbgraderData {
+    if (cellMetadata == null) {
+      return null;
+    }
+    const nbgraderValue = cellMetadata.get('nbgrader');
+    if (nbgraderValue == null) {
+      return null;
+    }
+    return nbgraderValue.valueOf() as NbgraderData;
+  }
+
+  /**
+   * @returns True if the cell is gradable.
+   */
+  export function isGraded(data: NbgraderData): boolean {
+    return PrivateNbgraderData.isGraded(data);
+  }
+
+  /**
+   * @returns True if the cell relevant to nbgrader. A cell is relevant if it is
+   * gradable or contains autograder tests.
+   */
+  export function isRelevantToNbgrader(data: NbgraderData): boolean {
+    return PrivateNbgraderData.isGraded(data)
+        || PrivateNbgraderData.isSolution(data);
+  }
+
+  /**
+   * Converts {@link ToolData} to {@link NbgraderData}.
+   *
+   * @returns The converted data, or null if the nbgrader cell type is not set.
+   */
+  export function newNbgraderData(data: ToolData): NbgraderData {
+    if (data.type === '') {
+      return null;
+    }
+    const nbgraderData = new NbgraderData();
+    nbgraderData.grade = PrivateToolData.getGrade(data);
+    nbgraderData.grade_id = PrivateToolData.getGradeId(data);
+    nbgraderData.locked = PrivateToolData.getLocked(data);
+    nbgraderData.points = PrivateToolData.getPoints(data);
+    nbgraderData.schema_version = PrivateToolData.getSchemeaVersion();
+    nbgraderData.solution = PrivateToolData.getSolution(data);
+    nbgraderData.task = PrivateToolData.getTask(data);
+    return nbgraderData;
+  }
+
+  /**
+   * Converts {@link NbgraderData} to {@link ToolData}.
+   *
+   * @param data The data to convert. Can be null.
+   * @param cellType The notebook cell widget type.
+   */
+  export function newToolData(data: NbgraderData, cellType: nbformat.CellType):
+      ToolData {
+    const toolData = new ToolData;
+
+    if (PrivateNbgraderData.isInvalid(data, cellType)) {
+      toolData.type = '';
+      return toolData;
+    }
+    toolData.type = PrivateNbgraderData.getType(data, cellType);
+    if (toolData.type === '') {
+      return toolData;
+    }
+
+    if (PrivateNbgraderData.isGrade(data)
+        || PrivateNbgraderData.isSolution(data)
+        || PrivateNbgraderData.isLocked(data)) {
+      toolData.id = PrivateNbgraderData.getGradeId(data);
+    }
+
+    if (PrivateNbgraderData.isGraded(data)) {
+      toolData.points = PrivateNbgraderData.getPoints(data);
+    }
+
+    toolData.locked = PrivateNbgraderData.isLocked(data);
+
+    return toolData;
+  }
+
+  /**
+   * Writes the nbgrader data to a cell's metadata.
+   *
+   * @param data The nbgrader data. If null, the nbgrader entry, if it exists,
+   * will be removed from the metadata.
+   */
+  export function setNbgraderData(data: NbgraderData, cellMetadata:
+                                  IObservableJSON): void {
+    if (data == null) {
+      if (cellMetadata.has(NBGRADER_KEY)) {
+        cellMetadata.delete(NBGRADER_KEY);
+      }
+      return;
+    }
+    const currentDataJson = cellMetadata.get(NBGRADER_KEY);
+    const currentData = currentDataJson == null ? null :
+        currentDataJson.valueOf() as NbgraderData;
+    if (currentData != data) {
+      cellMetadata.set(NBGRADER_KEY, data.toJson());
+    }
+  }
+}
+
+namespace PrivateNbgraderData {
+  export function getGradeId(nbgraderData: NbgraderData): string {
+    if (nbgraderData == null || nbgraderData.grade_id == null) {
+      return '';
+    }
+    return nbgraderData.grade_id;
+  }
+
+  export function getPoints(nbgraderData: NbgraderData): number {
+    if (nbgraderData == null) {
+      return 0;
+    }
+    return PrivateNbgraderData._to_float(nbgraderData.points);
+  }
+
+  export function getSchemeaVersion(nbgraderData: NbgraderData): number {
+    if (nbgraderData === null) {
+      return 0;
+    }
+    return nbgraderData.schema_version;
+  }
+
+  export function getType(nbgraderData: NbgraderData,
+                          cellType: nbformat.CellType): CellType {
+    if (PrivateNbgraderData.isTask(nbgraderData)) {
+      return 'task';
+    } else if (PrivateNbgraderData.isSolution(nbgraderData)
+               && isGrade(nbgraderData)) {
+      return 'manual';
+    } else if (PrivateNbgraderData.isSolution(nbgraderData)
+               && cellType === 'code') {
+      return 'solution';
+    } else if (PrivateNbgraderData.isGrade(nbgraderData)
+               && cellType === 'code') {
+      return 'tests';
+    } else if (PrivateNbgraderData.isLocked(nbgraderData)) {
+      return 'readonly';
+    } else {
+      return '';
+    }
+  }
+
+  export function isGrade(nbgraderData: NbgraderData): boolean {
+    return nbgraderData != null && nbgraderData.grade === true;
+  }
+
+  export function isGraded(nbgraderData: NbgraderData): boolean {
+    return PrivateNbgraderData.isGrade(nbgraderData)
+        || PrivateNbgraderData.isTask(nbgraderData);
+  }
+
+  export function isInvalid(nbgraderData: NbgraderData,
+                            cellType: nbformat.CellType): boolean {
+    return !PrivateNbgraderData.isTask(nbgraderData) && cellType !== 'code'
+        && (PrivateNbgraderData.isSolution(nbgraderData)
+            != PrivateNbgraderData.isGrade(nbgraderData));
+  }
+
+  export function isLocked(nbgraderData: NbgraderData): boolean {
+    return !PrivateNbgraderData.isSolution(nbgraderData)
+        && (PrivateNbgraderData.isGraded(nbgraderData)
+            || (nbgraderData != null && nbgraderData.locked === true));
+  }
+
+  export function isSolution(nbgraderData: NbgraderData): boolean {
+    return nbgraderData != null && nbgraderData.solution === true;
+  }
+
+  export function isTask(nbgraderData: NbgraderData): boolean {
+    return nbgraderData != null && nbgraderData.task === true;
+  }
+
+  export function _to_float(val: any): number {
+    if (val == null || val === '') {
+      return 0;
+    }
+    const valType = typeof(val);
+    if (valType === 'string') {
+      return parseFloat(val);
+    }
+    else if (valType === 'number') {
+      return val;
+    }
+    return 0;
+  }
+}
+
+namespace PrivateToolData {
+  export function getGrade(data: ToolData): boolean {
+    return data.type === 'manual' || data.type === 'tests';
+  }
+
+  export function getGradeId(data: ToolData): string {
+    return data.id == null ? '' : data.id;
+  }
+
+  export function getLocked(data: ToolData): boolean {
+    if (PrivateToolData.getSolution(data)) {
+      return false;
+    }
+    if (PrivateToolData.getGrade(data)) {
+      return true;
+    }
+    return data.type === 'task' || data.type === 'tests'
+        || data.type === 'readonly';
+  }
+
+  export function getPoints(data: ToolData): number {
+    if (!PrivateToolData.getGrade(data) && !PrivateToolData.getTask(data)) {
+      return undefined;
+    }
+    return data.points >= 0 ? data.points : 0;
+  }
+
+  export function getSchemeaVersion(): number {
+    return NBGRADER_SCHEMA_VERSION;
+  }
+
+  export function getSolution(data: ToolData): boolean {
+    return data.type === 'manual' || data.type === 'solution';
+  }
+
+  export function getTask(data: ToolData): boolean {
+    return data.type === 'task';
+  }
+}
+
+/**
+ * Dummy class for representing the nbgrader cell metadata.
+ */
+export class NbgraderData {
+  grade: boolean;
+  grade_id: string;
+  locked: boolean;
+  points: number;
+  schema_version: number;
+  solution: boolean;
+  task: boolean;
+
+  toJson(): ReadonlyJSONObject {
+    const json = {} as JSONObject;
+    if (this.grade != null) {
+      json['grade'] = this.grade;
+    }
+    if (this.grade_id != null) {
+      json['grade_id'] = this.grade_id;
+    }
+    if (this.locked != null) {
+      json['locked'] = this.locked;
+    }
+    if (this.points != null) {
+      json['points'] = this.points;
+    }
+    if (this.schema_version != null) {
+      json['schema_version'] = this.schema_version;
+    }
+    if (this.solution != null) {
+      json['solution'] = this.solution;
+    }
+    if (this.task != null) {
+      json['task'] = this.task;
+    }
+    return json;
+  }
+}
+
+/**
+ * Dummy class for representing the UI input/output values.
+ */
+export class ToolData {
+  type: CellType;
+  id: string;
+  points: number;
+  locked: boolean;
+}
+
+export type CellType = '' | 'manual' | 'task' | 'solution' | 'tests' |
+    'readonly';
diff --git a/src/create_assignment/index.ts b/src/create_assignment/index.ts
new file mode 100644
index 000000000..9b2babe17
--- /dev/null
+++ b/src/create_assignment/index.ts
@@ -0,0 +1,44 @@
+import {
+  JupyterFrontEnd,
+  JupyterFrontEndPlugin
+} from '@jupyterlab/application';
+
+import {
+  INotebookTracker
+} from '@jupyterlab/notebook';
+
+import {
+  Panel
+} from '@lumino/widgets';
+
+import {
+  CreateAssignmentWidget
+} from './create_assignment_extension';
+
+const PLUGIN_ID = "nbgrader/create-assignment"
+
+/**
+ * Initialization data for the create_assignment extension.
+ */
+export const create_assignment_extension: JupyterFrontEndPlugin<void> = {
+  id: PLUGIN_ID,
+  autoStart: true,
+  requires: [INotebookTracker],
+  activate: activate_extension
+};
+
+function activate_extension(app: JupyterFrontEnd, tracker: INotebookTracker) {
+  console.log('Activating extension "create_assignment".');
+
+  const panel = new Panel();
+  panel.node.style.overflowY = 'auto';
+  const createAssignmentWidget = new CreateAssignmentWidget(tracker);
+  panel.addWidget(createAssignmentWidget);
+  panel.id = 'nbgrader-create_assignemnt';
+  panel.title.label = 'Create Assignment';
+  panel.title.caption = 'nbgrader Create Assignment';
+  app.shell.add(panel, 'right');
+  console.log('Extension "create_assignment" activated.');
+}
+
+export default create_assignment_extension;
diff --git a/src/formgrader/index.ts b/src/formgrader/index.ts
new file mode 100644
index 000000000..c7772a13b
--- /dev/null
+++ b/src/formgrader/index.ts
@@ -0,0 +1,121 @@
+import {
+  ILayoutRestorer,
+  JupyterFrontEnd,
+  JupyterFrontEndPlugin
+} from '@jupyterlab/application';
+
+import { URLExt } from '@jupyterlab/coreutils';
+
+import { ServerConnection } from '@jupyterlab/services';
+
+import {
+  ICommandPalette,
+  MainAreaWidget,
+  WidgetTracker,
+  IFrame
+} from '@jupyterlab/apputils';
+
+const PLUGIN_ID = "nbgrader/formgrader"
+const COMMAND_NAME = "nbgrader:open-formgrader"
+
+class FormgraderWidget extends IFrame {
+
+    app: JupyterFrontEnd;
+
+    constructor(app: JupyterFrontEnd, url:string) {
+      super();
+      this.referrerPolicy = 'strict-origin-when-cross-origin';
+      this.sandbox = ['allow-scripts', 'allow-same-origin', 'allow-forms'];
+
+      this.node.id = "formgrader-iframe"
+      this.app = app;
+
+      this.url = url;
+
+      var this_widget = this;
+
+      window.addEventListener('message', function (event) {
+        this_widget.on_click(event);
+      });
+
+    }
+
+    private on_click(event:MessageEvent){
+      var contentWindow = this.node.querySelector('iframe').contentWindow;
+      if (contentWindow === event.source){
+        var data = JSON.parse(event.data);
+        this.app.commands.execute(data.command, data.arguments);
+      }
+    }
+};
+
+/**
+ * Initialization data for the formfrader extension.
+ */
+export const formgrader_extension: JupyterFrontEndPlugin<void> = {
+  id: PLUGIN_ID,
+  autoStart: true,
+  requires: [ICommandPalette],
+  optional: [ILayoutRestorer],
+  activate: async (
+    app: JupyterFrontEnd,
+    palette: ICommandPalette,
+    restorer: ILayoutRestorer | null
+  )=> {
+    console.log('JupyterLab extension formgrader is activated!');
+
+    // Declare a widget variable
+    let widget: MainAreaWidget<FormgraderWidget>;
+
+    // Add an application command
+    const command: string = COMMAND_NAME;
+
+    // Track the widget state
+    let tracker = new WidgetTracker<MainAreaWidget<FormgraderWidget>>({
+      namespace: 'nbgrader-formgrader'
+    });
+
+    app.commands.addCommand(command,{
+    label: 'Formgrader',
+    execute: async args => {
+      if(!widget){
+        const settings = ServerConnection.makeSettings();
+        const url = (args.url as string) || URLExt.join(settings.baseUrl, "formgrader");
+
+        const content = new FormgraderWidget(app, url);
+
+        widget = new MainAreaWidget({content});
+        widget.id = 'formgrader';
+        widget.title.label = 'Formgrader';
+        widget.title.closable = true;
+        }
+
+        if(!tracker.has(widget)){
+        // Track the state of the widget for later restoration
+        tracker.add(widget);
+        }
+        if(!widget.isAttached){
+        // Attach the widget to the mainwork area if it's not there
+        app.shell.add(widget, 'main');
+        }
+        widget.content.update();
+
+        // Activate the widget
+        app.shell.activateById(widget.id);
+      }
+    });
+
+    // Add the command to the palette
+    palette.addItem({command, category: 'nbgrader'});
+
+    // Restore the widget state
+    if (restorer != null){
+      restorer.restore(tracker, {
+        command,
+        name: () => 'nbgrader-formgrader'
+      });
+    }
+  }
+};
+
+export default formgrader_extension;
\ No newline at end of file
diff --git a/src/index.ts b/src/index.ts
new file mode 100644
index 000000000..d9eed91a0
--- /dev/null
+++ b/src/index.ts
@@ -0,0 +1,7 @@
+import { assignment_list_extension } from "./assignment_list/index";
+import { formgrader_extension } from "./formgrader/index";
+import { course_list_extension } from "./course_list/index";
+import { create_assignment_extension } from "./create_assignment/index";
+import { validate_assignment_extension } from "./validate_assignment/index";
+
+export default [formgrader_extension, assignment_list_extension, course_list_extension, create_assignment_extension, validate_assignment_extension]
diff --git a/src/validate_assignment/index.ts b/src/validate_assignment/index.ts
new file mode 100644
index 000000000..80b9235e1
--- /dev/null
+++ b/src/validate_assignment/index.ts
@@ -0,0 +1,207 @@
+import {
+  JupyterFrontEnd,
+  JupyterFrontEndPlugin
+} from '@jupyterlab/application';
+
+import {
+  IDisposable, DisposableDelegate
+} from '@lumino/disposable';
+
+import {
+  ToolbarButton, Dialog
+} from '@jupyterlab/apputils';
+
+import {
+  DocumentRegistry
+} from '@jupyterlab/docregistry';
+
+import {
+  NotebookPanel, INotebookModel
+} from '@jupyterlab/notebook';
+
+import { requestAPI } from './validateassignment';
+
+import { showNbGraderDialog, validate } from '../common/validate';
+
+var nbgrader_version = "0.8.0.dev"; // TODO: hardcoded value
+
+const PLUGIN_ID = "nbgrader/validate-assignment"
+
+class ValidateButton extends ToolbarButton {
+  private _buttonCallback = this.newButtonCallback();
+  private _versionCheckCallback = this.newVersionCheckCallback();
+  private _saveCallback = this.newSaveCallback();
+  private panel: NotebookPanel;
+
+  constructor(panel: NotebookPanel) {
+    super({
+      className: 'validate-button',
+      // iconClass: 'fa fa-fast-forward',
+      label: 'Validate',
+      onClick: () => {this.buttonCallback();},
+      tooltip: 'Validate Assignment'
+    });
+    this.panel = panel;
+  }
+
+  private get buttonCallback() {
+    return this._buttonCallback;
+  }
+
+  private get saveCallback() {
+    return this._saveCallback;
+  }
+
+  private get versionCheckCallback() {
+    return this._versionCheckCallback;
+  }
+
+  dispose() {
+    if (this.isDisposed) {
+      return;
+    }
+    this.panel = null;
+    super.dispose()
+  }
+
+  private newSaveCallback() {
+    return (sender: DocumentRegistry.IContext<INotebookModel>,
+            args: DocumentRegistry.SaveState) => {
+      if (args !== 'completed' && args !== 'failed') {
+        return;
+      }
+
+      this.panel.context.saveState.disconnect(this.saveCallback);
+
+      if (args !== "completed") {
+        showNbGraderDialog({
+          title: "Validation failed",
+          body: "Cannot save notebook",
+          buttons: [Dialog.okButton()],
+          focusNodeSelector: 'input'
+        }, true);
+        this.setButtonLabel();
+        this.setButtonDisabled(false);
+        return;
+      }
+
+      this.setButtonLabel('Validating...');
+      const notebook_path = this.panel.context.path
+      requestAPI<any>(
+          'assignments/validate',
+          { method: 'POST' },
+          new Map([['path', notebook_path]])
+      ).then(data => {
+        validate(data);
+        this.setButtonLabel();
+        this.setButtonDisabled(false);
+      }).catch(reason => {
+        showNbGraderDialog({
+          title: "Validation failed",
+          body: `Cannot validate: ${reason}`,
+          buttons: [Dialog.okButton()],
+          focusNodeSelector: 'input'
+        }, true);
+        this.setButtonLabel();
+        this.setButtonDisabled(false);
+      });
+    }
+  }
+
+  private newVersionCheckCallback() {
+    return (data: any) => {
+      if (data.success !== true) {
+        showNbGraderDialog({
+          title: "Version Mismatch",
+          body: data.message,
+          buttons: [Dialog.okButton()],
+          focusNodeSelector: 'input'
+        }, true);
+        return;
+      }
+
+      // tests/test-docregistry/src/context.spec.ts:98
+      this.setButtonDisabled();
+      this.setButtonLabel('Saving...');
+      this.panel.context.saveState.connect(this.saveCallback);
+      // examples/notebook/src/commands.ts:79
+      this.panel.context.save();
+    }
+  }
+
+  private newButtonCallback() {
+    return () => {
+      requestAPI<any>(
+          'nbgrader_version',
+          undefined,
+          new Map([['version', nbgrader_version]])
+      ).then(
+          this.versionCheckCallback
+      ).catch(reason => {
+        // The validate_assignment server extension appears to be missing
+        showNbGraderDialog({
+          title: "Validation failed",
+          body: `Cannot check version: ${reason}`,
+          buttons: [Dialog.okButton()],
+          focusNodeSelector: 'input'
+        }, true);
+      });
+    }
+  }
+
+  private setButtonDisabled(disabled: boolean = true): void {
+    const button = this.node.getElementsByTagName('button')[0];
+    if (disabled) {
+      button.setAttribute('disabled', 'disabled');
+    } else {
+      button.removeAttribute('disabled');
+    }
+  }
+
+  private setButtonLabel(label: string = 'Validate'): void {
+    const labelElement = this.node.getElementsByClassName(
+        'jp-ToolbarButtonComponent-label')[0] as HTMLElement;
+    labelElement.innerText = label;
+  }
+
+}
+
+export class ButtonExtension implements DocumentRegistry.IWidgetExtension<NotebookPanel, INotebookModel> {
+  /**
+   * Create a new extension object.
+   */
+  createNew(panel: NotebookPanel, context: DocumentRegistry.IContext<INotebookModel>): IDisposable {
+    const button = new ValidateButton(panel);
+
+    let children = panel.toolbar.children();
+    let index = 0;
+    for (let i = 0; ; i++) {
+      let widget = children.next();
+      if (widget == undefined) {
+        break;
+      }
+      if (widget.node.classList.contains("jp-Toolbar-spacer")) {
+        index = i;
+        break;
+      }
+    }
+    panel.toolbar.insertItem(index, 'runAll', button);
+    return new DisposableDelegate(() => {
+      button.dispose();
+    });
+  }
+}
+
+/**
+ * Initialization data for the validate_assignment extension.
+ */
+export const validate_assignment_extension: JupyterFrontEndPlugin<void> = {
+  id: PLUGIN_ID,
+  autoStart: true,
+  activate: (app: JupyterFrontEnd) => {
+    console.log('JupyterLab extension validate-assignment is activated!');
+    app.docRegistry.addWidgetExtension('Notebook', new ButtonExtension());
+  }
+};
+
+export default validate_assignment_extension;
diff --git a/src/validate_assignment/validateassignment.ts b/src/validate_assignment/validateassignment.ts
new file mode 100644
index 000000000..9640d9e47
--- /dev/null
+++ b/src/validate_assignment/validateassignment.ts
@@ -0,0 +1,45 @@
+import { URLExt } from '@jupyterlab/coreutils';
+
+import { ServerConnection } from '@jupyterlab/services';
+
+/**
+ * Call the API extension
+ *
+ * @param endPoint API REST end point for the extension
+ * @param init Initial values for the request
+ * @returns The response body interpreted as JSON
+ */
+export async function requestAPI<T>(
+  endPoint = '',
+  init: RequestInit = {},
+  params: Map<string, string> = undefined
+): Promise<T> {
+  const searchParams = new URLSearchParams();
+  if (params != null) {
+    for (const entry of params.entries()) {
+      searchParams.append(entry[0], entry[1]);
+    }
+  }
+
+  // Make request to Jupyter API
+  const settings = ServerConnection.makeSettings();
+  const requestUrl = URLExt.join(
+    settings.baseUrl,
+    endPoint
+  ) + '?' + searchParams.toString();
+
+  let response: Response;
+  try {
+    response = await ServerConnection.makeRequest(requestUrl, init, settings);
+  } catch (error) {
+    throw new ServerConnection.NetworkError(error);
+  }
+
+  const data = await response.json();
+
+  if (!response.ok) {
+    throw new ServerConnection.ResponseError(response, data.message);
+  }
+
+  return data;
+}
diff --git a/style/assignment_list.css b/style/assignment_list.css
new file mode 100644
index 000000000..df4642c8f
--- /dev/null
+++ b/style/assignment_list.css
@@ -0,0 +1,94 @@
+#nbgrader-assignment-list .panel-group .panel {
+  margin-top: 3px;
+  margin-bottom: 1em;
+}
+
+#nbgrader-assignment-list .panel-group .panel .panel-heading {
+  background-color: #eee;
+  padding-top: 4px;
+  padding-bottom: 4px;
+  padding-left: 7px;
+  padding-right: 7px;
+  line-height: 22px;
+}
+
+#nbgrader-assignment-list .panel-group .panel .panel-heading a:focus, a:hover {
+  text-decoration: none;
+}
+
+#nbgrader-assignment-list .panel-group .panel .panel-body {
+  padding: 0;
+}
+
+#nbgrader-assignment-list .panel-group .panel .panel-body .list_container {
+  margin-top: 0px;
+  margin-bottom: 0px;
+  border: 0px;
+  border-radius: 0px;
+}
+
+#nbgrader-assignment-list .panel-group .panel .panel-body .list_container .list_item {
+  border-bottom: 1px solid #ddd;
+}
+
+#nbgrader-assignment-list .panel-group .panel .panel-body .list_container .list_item:last-child {
+  border-bottom: 0px;
+}
+
+#nbgrader-assignment-list .assignment-notebooks .list_item {
+  background-color: inherit !important;
+}
+
+#nbgrader-assignment-list .assignment-notebooks .list_item:hover {
+  background-color: #ddd !important;
+}
+
+#nbgrader-assignment-list .assignment-notebooks .list_item:first-child:hover {
+  background-color: inherit !important;
+}
+
+#nbgrader-assignment-list .list_item {
+  padding-top: 4px;
+  padding-bottom: 4px;
+  padding-left: 7px;
+  line-height: 22px;
+}
+
+#nbgrader-assignment-list .list_item:not(.nested_list_item) {
+  padding-right: 7px;
+}
+
+#nbgrader-assignment-list .list_item > div {
+  padding-top: 0;
+  padding-bottom: 0;
+  padding-left: 0;
+  padding-right: 0;
+}
+
+#nbgrader-assignment-list .item_status {
+  text-align: right;
+}
+
+#nbgrader-assignment-list .item_status .btn {
+  min-width: 13ex;
+}
+
+#nbgrader-assignment-list .version_error {
+  display: none;
+}
+
+#submission-message pre {
+  white-space: pre;
+}
+
+#nbgrader-assignment-list .list_placeholder, #nbgrader-assignment-list .list_loading, #nbgrader-assignment-list .list_error {
+  font-weight: bold;
+  padding-top: 4px;
+  padding-bottom: 4px;
+  padding-left: 7px;
+  padding-right: 7px;
+}
+
+#assignments_toolbar {
+  padding: 3px;
+}
diff --git a/style/common.css b/style/common.css
new file mode 100644
index 000000000..d5fd11a1b
--- /dev/null
+++ b/style/common.css
@@ -0,0 +1,357 @@
+/*
+FOLLOWING RULES REPLACE BOOTSTRAP RULES
+*/
+
+.nbgrader-mainarea-widget ::after, .nbgrader-mainarea-widget ::before {
+  -webkit-box-sizing: border-box;
+  -moz-box-sizing: border-box;
+  box-sizing: border-box;
+}
+
+.nbgrader-mainarea-widget {
+  font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
+  font-size: 14px;
+  line-height: 1.42857143;
+  color: #333;
+  padding-left: 3px;
+  padding-right: 3px;
+}
+
+.nbgrader-mainarea-widget * {
+  -webkit-box-sizing: border-box;
+  -moz-box-sizing: border-box;
+  box-sizing: border-box;
+}
+
+.nbgrader-mainarea-widget [hidden] {
+  display: none;
+}
+
+.nbgrader-mainarea-widget button, .nbgrader-mainarea-widget input, .nbgrader-mainarea-widget select, .nbgrader-mainarea-widget textarea {
+  font-family: inherit;
+}
+
+.nbgrader-mainarea-widget button {
+  overflow: visible;
+}
+
+.nbgrader-mainarea-widget button, .nbgrader-mainarea-widget select {
+  text-transform: none;
+}
+
+.nbgrader-mainarea-widget button, .nbgrader-mainarea-widget html input[type="button"], .nbgrader-mainarea-widget input[type="reset"], .nbgrader-mainarea-widget input[type="submit"] {
+  -webkit-appearance: button;
+  cursor: pointer;
+}
+
+.nbgrader-mainarea-widget a {
+  background-color: transparent;
+}
+
+.nbgrader-mainarea-widget a {
+  color: #337ab7;
+  text-decoration: none;
+}
+
+.nbgrader-mainarea-widget [role="button"] {
+  cursor: pointer;
+}
+
+.nbgrader-mainarea-widget .collapse {
+  display: none;
+}
+
+.nbgrader-mainarea-widget .collapse.in {
+  display: block;
+}
+
+.panel-body::after, .panel-body::before, .row::after, .row::before {
+  display: table;
+  content: " ";
+}
+
+.col-md-12, .col-sm-2, .col-sm-4, .col-sm-6, .col-sm-8 {
+  position: relative;
+  min-height: 1px;
+}
+
+
+@media (min-width: 768px) {
+  .col-sm-2, .col-sm-4, .col-sm-6, .col-sm-8 {
+    float: left;
+  }
+}
+
+@media (min-width: 992px) {
+  .col-md-12 {
+    float: left;
+  }
+}
+
+@media (min-width: 768px) {
+  .col-sm-2 {
+    width: 16.66666667%;
+  }
+}
+
+@media (min-width: 768px) {
+  .col-sm-4 {
+    width: 33.33333333%;
+  }
+}
+
+@media (min-width: 768px) {
+  .col-sm-6 {
+    width: 50%;
+  }
+}
+
+@media (min-width: 768px) {
+  .col-sm-8 {
+    width: 66.66666667%;
+  }
+}
+
+@media (min-width: 992px) {
+  .col-md-12 {
+    width: 100%;
+  }
+}
+
+.btn {
+  display: inline-block;
+  margin-bottom: 0;
+  font-weight: 400;
+  text-align: center;
+  white-space: nowrap;
+  vertical-align: middle;
+  -ms-touch-action: manipulation;
+  touch-action: manipulation;
+  cursor: pointer;
+  background-image: none;
+  border: 1px solid transparent;
+    border-top-color: transparent;
+    border-right-color: transparent;
+    border-bottom-color: transparent;
+    border-left-color: transparent;
+  padding: 6px 12px;
+  font-size: 14px;
+  line-height: 1.42857143;
+  border-radius: 4px;
+  -webkit-user-select: none;
+  -moz-user-select: none;
+  -ms-user-select: none;
+  user-select: none;
+}
+
+.btn-default {
+  color: #333;
+  background-color: #fff;
+  border-color: #ccc;
+}
+
+.btn-primary {
+  color: #fff;
+  background-color: #337ab7;
+  border-color: #2e6da4;
+}
+
+.btn-group-xs > .btn, .btn-xs {
+  padding: 1px 5px;
+  font-size: 12px;
+  line-height: 1.5;
+  border-radius: 3px;
+  border-top-right-radius: 3px;
+  border-bottom-right-radius: 3px;
+}
+
+.btn-group-vertical > .btn, .btn-group > .btn {
+  position: relative;
+  float: left;
+}
+
+.btn-group > .btn:first-child {
+  margin-left: 0;
+}
+
+.btn-group > .btn:first-child:not(:last-child):not(.dropdown-toggle) {
+  border-top-right-radius: 0;
+  border-bottom-right-radius: 0;
+}
+
+.btn-group .btn + .btn, .btn-group .btn + .btn-group, .btn-group .btn-group + .btn, .btn-group .btn-group + .btn-group {
+  margin-left: -1px;
+}
+
+.btn-group > .btn:last-child:not(:first-child), .btn-group > .dropdown-toggle:not(:first-child) {
+  border-top-left-radius: 0;
+  border-bottom-left-radius: 0;
+}
+
+.btn-group, .btn-group-vertical {
+  position: relative;
+  display: inline-block;
+  vertical-align: middle;
+}
+
+.btn-group > .btn + .dropdown-toggle {
+  padding-right: 8px;
+  padding-left: 8px;
+}
+
+.btn.focus, .btn:focus, .btn:hover {
+  color: #333;
+  text-decoration: none;
+}
+
+.nbgrader-mainarea-widget .btn-primary:hover {
+  color: #fff;
+  background-color: #286090;
+  border-color: #204d74;
+}
+
+.dropdown-menu {
+  position: absolute;
+  top: 100%;
+  left: 0;
+  z-index: 1000;
+  display: none;
+  float: left;
+  min-width: 160px;
+  padding: 5px 0;
+  margin: 2px 0 0;
+  font-size: 14px;
+  text-align: left;
+  list-style: none;
+  background-color: #fff;
+  background-clip: padding-box;
+  border: 1px solid #ccc;
+  border: 1px solid rgba(0,0,0,.15);
+  border-radius: 4px;
+  -webkit-box-shadow: 0 6px 12px rgba(0,0,0,.175);
+  box-shadow: 0 6px 12px rgba(0,0,0,.175);
+}
+
+.dropdown-menu.open {
+  display: block;
+}
+
+.dropdown-menu > li > a {
+  display: block;
+  padding: 3px 20px;
+  clear: both;
+  font-weight: 400;
+  line-height: 1.42857143;
+  color: #333;
+  white-space: nowrap;
+}
+
+.dropdown-menu > li > a:focus, .dropdown-menu > li > a:hover {
+  color: #262626;
+  text-decoration: none;
+  background-color: #f5f5f5;
+}
+
+.caret {
+  display: inline-block;
+  width: 0;
+  height: 0;
+  margin-left: 2px;
+  vertical-align: middle;
+  border-top: 4px dashed;
+  border-top: 4px solid\9;
+  border-right: 4px solid transparent;
+  border-left: 4px solid transparent;
+}
+
+.btn .caret {
+  margin-left: 0;
+}
+
+.panel-body::after, .row::after {
+  clear: both;
+}
+
+.sr-only {
+  position: absolute;
+  width: 1px;
+  height: 1px;
+  padding: 0;
+  margin: -1px;
+  overflow: hidden;
+  clip: rect(0,0,0,0);
+  border: 0;
+}
+
+.pull-right {
+  float: right !important;
+}
+
+.alert {
+  padding: 15px;
+  margin-bottom: 20px;
+  border: 1px solid transparent;
+  border-top-color: transparent;
+  border-right-color: transparent;
+  border-bottom-color: transparent;
+  border-left-color: transparent;
+  border-radius: 4px;
+}
+
+.alert-danger {
+  color: #a94442;
+  background-color: #f2dede;
+  border-color: #ebccd1;
+}
+
+.panel-group {
+  margin-bottom: 20px;
+}
+
+.panel {
+  margin-bottom: 20px;
+  background-color: #fff;
+  border: 1px solid transparent;
+  border-top-color: transparent;
+  border-right-color: transparent;
+  border-bottom-color: transparent;
+  border-left-color: transparent;
+  border-radius: 4px;
+  -webkit-box-shadow: 0 1px 1px rgba(0,0,0,.05);
+  box-shadow: 0 1px 1px rgba(0,0,0,.05);
+}
+
+.panel-default {
+  border-color: #ddd;
+}
+
+.panel-group .panel-heading {
+  border-bottom: 0;
+  border-bottom-color: currentcolor;
+}
+
+.panel-default > .panel-heading {
+  color: #333;
+  background-color: #f5f5f5;
+  border-color: #ddd;
+}
+
+.panel-heading {
+  padding: 10px 15px;
+  border-bottom: 1px solid transparent;
+  border-top-left-radius: 3px;
+  border-top-right-radius: 3px;
+}
+
+.panel-body {
+  padding: 15px;
+}
+
+.panel-group .panel {
+  margin-bottom: 0;
+  border-radius: 4px;
+}
+
+.panel-group .panel + .panel {
+  margin-top: 5px;
+}
\ No newline at end of file
diff --git a/style/course_list.css b/style/course_list.css
new file mode 100644
index 000000000..466914933
--- /dev/null
+++ b/style/course_list.css
@@ -0,0 +1,81 @@
+#nbgrader-course-list {
+    overflow: auto;
+}
+
+#courses .alert-danger {
+  /* these are copied from bootstrap, since apparantly it needs them */
+  position: relative;
+  padding: 0.75rem 1.25rem;
+  margin-bottom: 1rem;
+  border: 1px solid transparent;
+  border-radius: 0.25rem;
+  color: #721c24;
+  background-color: #f8d7da;
+  border-color: #f5c6cb;
+}
+
+#courses .panel-group .panel {
+    margin-top: 3px;
+    margin-bottom: 1em;
+}
+
+#courses .panel-group .panel .panel-heading {
+    background-color: #eee;
+    padding-top: 4px;
+    padding-bottom: 4px;
+    padding-left: 7px;
+    padding-right: 7px;
+    line-height: 22px;
+}
+
+#courses .panel-group .panel .panel-heading a:focus, a:hover {
+    text-decoration: none;
+}
+
+#courses .panel-group .panel .panel-body {
+    padding: 0;
+}
+
+#courses .panel-group .panel .panel-body .list_container {
+    margin-top: 0px;
+    margin-bottom: 0px;
+    border: 0px;
+    border-radius: 0px;
+}
+
+#courses .panel-group .panel .panel-body .list_container .list_item {
+    border-bottom: 1px solid #ddd;
+}
+
+#courses .panel-group .panel .panel-body .list_container .list_item:last-child {
+    border-bottom: 0px;
+}
+
+#courses .list_item {
+    padding-top: 4px;
+    padding-bottom: 4px;
+    padding-left: 7px;
+    padding-right: 7px;
+    line-height: 22px;
+}
+
+#courses .list_item > div {
+    padding-top: 0;
+    padding-bottom: 0;
+    padding-left: 0;
+    padding-right: 0;
+}
+
+#courses .list_item .item_name, #courses .list_item .item_course{
+    display: inline-block;
+    width: 16.66666667%;
+}
+
+#courses .list_placeholder, #courses .list_loading, #courses .list_error {
+    font-weight: bold;
+    padding-top: 4px;
+    padding-bottom: 4px;
+    padding-left: 7px;
+    padding-right: 7px;
+}
+
diff --git a/style/create_assignment.css b/style/create_assignment.css
new file mode 100644
index 000000000..3ba9da6e8
--- /dev/null
+++ b/style/create_assignment.css
@@ -0,0 +1,158 @@
+.nbgrader-CreateAssignmentWidget {
+  background: var(--jp-layout-color1);
+  color: var(--jp-ui-font-color1);
+}
+
+.nbgrader-NotebookPanelWidget {
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+}
+
+.nbgrader-NotebookHeaderWidget {
+  padding-top: 12px;
+  border-bottom: solid var(--jp-border-color1) var(--jp-border-width);
+  box-shadow: var(--jp-toolbar-box-shadow);
+}
+
+.nbgrader-NotebookHeaderWidget>* {
+  padding: 0 12px;
+}
+
+.nbgrader-TotalPointsInput {
+  width: 5em;
+  height: 24px;
+  text-align: right;
+  line-height: 10px;
+  margin-left: 0.3em;
+  margin-right: 0.3em;
+
+  background: var(--jp-input-background);
+  font-size: var(--jp-ui-font-size1);
+  font-family: var(--jp-content-font-family);
+  color: var(--jp-ui-font-color0);
+  border: var(--jp-border-width) solid var(--jp-border-color1);
+  flex: 1 1 auto;
+  padding: 0 8px 0 8px;
+  outline: none;
+}
+
+.nbgrader-NotebookWidget {
+  height: 100%;
+}
+
+.nbgrader-CellWidget {
+  margin-top: 12px;
+  height: auto;
+  border-style: solid;
+  border-width: 0 0 var(--jp-border-width) 0;
+  border-color: var(--jp-border-color2);
+}
+
+.nbgrader-CellWidget:last-child {
+  border-width: 0;
+}
+
+.nbgrader-CellWidget>div {
+  padding: 0 12px 0 6px;
+  border-style: solid;
+  border-width: 0 0 0 6px;
+  border-color: #0000;
+}
+
+.nbgrader-CellWidget.nbgrader-mod-active>div {
+  border-color: var(--jp-brand-color1);
+}
+
+.nbgrader-CellWidget input {
+  background: var(--jp-input-background);
+  font-size: var(--jp-ui-font-size1);
+  font-family: var(--jp-content-font-family);
+  color: var(--jp-ui-font-color0);
+  border: var(--jp-border-width) solid var(--jp-border-color1);
+  flex: 1 1 auto;
+  padding: 0 8px 0 8px;
+  outline: none;
+}
+
+.nbgrader-CellWidget .jp-select-wrapper {
+  display: inline-flex;
+  font-size: var(--jp-ui-font-size1);
+  margin-bottom: 0;
+}
+
+.nbgrader-CellWidget .jp-select-wrapper select.jp-mod-styled {
+  font-size: var(--jp-ui-font-size1);
+}
+
+.nbgrader-CellWidget .nbgrader-mod-uneditable {
+  visibility: hidden;
+}
+
+.nbgrader-CellWidget select {
+  padding-right: 25px;
+}
+
+.nbgrader-CellWidget select:hover {
+  cursor: pointer;
+}
+
+.nbgrader-NotebookPoints, .nbgrader-CellHeader, .nbgrader-CellType, .nbgrader-CellPoints, .nbgrader-CellId {
+  margin-bottom: 12px;
+}
+
+.nbgrader-CellWidget>label {
+  font-size: var(--jp-ui-font-size1);
+}
+
+.nbgrader-CellHeader {
+  display: grid;
+  grid-template-columns: auto max-content;
+  background-color: #EEE;
+  padding: 0 0.5em;
+}
+
+.nbgrader-mod-highlight .nbgrader-CellHeader {
+  background-color: #337AB7;
+}
+
+.nbgrader-CellHeader>.jp-InputPrompt {
+  border-width: 0;
+  padding-left: 0;
+  text-align: left;
+  margin: auto 0;
+}
+
+.nbgrader-CellHeader>* {
+  color: black;
+}
+
+.nbgrader-mod-highlight .nbgrader-CellHeader>* {
+  color: white;
+}
+
+.nbgrader-CellHeader>.nbgrader-LockButton {
+  margin: auto 0;
+  padding: 0.25em;
+  cursor: default;
+}
+
+.nbgrader-LockButton {
+  opacity: 0;
+}
+
+.nbgrader-LockButton.nbgrader-mod-locked {
+  opacity: 1;
+}
+
+.nbgrader-CellPoints input {
+    width: 5em;
+    height: 22px;
+    text-align: right;
+    line-height: 10px;
+}
+
+.nbgrader-CellId input {
+    height: 22px;
+    text-align: left;
+}
diff --git a/style/index.css b/style/index.css
new file mode 100644
index 000000000..22e2ce531
--- /dev/null
+++ b/style/index.css
@@ -0,0 +1,6 @@
+
+@import url('./common.css');
+@import url('./assignment_list.css');
+@import url('./course_list.css');
+@import url('./create_assignment.css');
+@import url('./validation_message.css');
diff --git a/style/index.js b/style/index.js
new file mode 100644
index 000000000..33c3a442d
--- /dev/null
+++ b/style/index.js
@@ -0,0 +1,5 @@
+import './common.css';
+import './assignment_list.css';
+import './course_list.css';
+import './create_assignment.css';
+import './validation_message.css';
diff --git a/style/validation_message.css b/style/validation_message.css
new file mode 100644
index 000000000..3be05ec8f
--- /dev/null
+++ b/style/validation_message.css
@@ -0,0 +1,12 @@
+#submission-message p,
+#validation-message p {
+    margin-bottom: 1em;
+    padding-top: 1em;
+}
+
+#submission-message pre,
+#validation-message pre {
+    border: solid lightgray 1px;
+    padding: 5px;
+    margin: 1em;
+}
\ No newline at end of file
diff --git a/tasks.py b/tasks.py
index d2f3feff6..970bed344 100755
--- a/tasks.py
+++ b/tasks.py
@@ -10,9 +10,9 @@ def echo(msg):
     print("\033[1;37m{0}\033[0m".format(msg))
 
 
-def run(cmd):
+def run(cmd, **kwargs):
     echo(cmd)
-    return sp.check_call(cmd, shell=True)
+    return sp.check_call(cmd, shell=True, **kwargs)
 
 
 try:
@@ -77,6 +77,11 @@ def _run_tests(mark, skip, junitxml, paralell=False):
         run("coverage combine || true")
 
 
+def _run_ts_test():
+    cmd = ['npx', 'playwright', 'test', '--retries=3']
+    run(" ".join(cmd))
+
+
 def tests(args):
     if args.group == 'python':
         _run_tests(
@@ -85,11 +90,15 @@ def tests(args):
     elif args.group == 'nbextensions':
         _run_tests(mark="nbextensions", skip=args.skip, junitxml=args.junitxml)
 
+    elif args.group =='labextensions':
+        _run_ts_test()
+
     elif args.group == 'docs':
         docs(args)
 
     elif args.group == 'all':
         _run_tests(mark=None, skip=args.skip, junitxml=args.junitxml)
+        _run_ts_test()
 
     else:
         raise ValueError("Invalid test group: {}".format(args.group))
@@ -115,7 +124,11 @@ def install(args):
         cmd = 'pip install .[docs,tests]'
     else:
         cmd = 'pip install -e .[tests]'
-    run(cmd)
+
+    env = os.environ.copy()
+    if args.group not in ['all', 'labextensions']:
+        env['NBGRADER_NO_LAB'] = '1'
+    run(cmd, env=env)
 
 
 if __name__ == '__main__':
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 000000000..8cde80ebe
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,24 @@
+{
+  "compilerOptions": {
+    "allowSyntheticDefaultImports": true,
+    "composite": true,
+    "declaration": true,
+    "esModuleInterop": true,
+    "incremental": true,
+    "jsx": "react",
+    "module": "esnext",
+    "moduleResolution": "node",
+    "noEmitOnError": true,
+    "noImplicitAny": true,
+    "noUnusedLocals": true,
+    "preserveWatchOutput": true,
+    "resolveJsonModule": true,
+    "outDir": "lib",
+    "rootDir": "src",
+    "strict": true,
+    "strictNullChecks": false,
+    "target": "es2017",
+    "types": []
+  },
+  "include": ["src/**/*"]
+}
diff --git a/yarn.lock b/yarn.lock
new file mode 100644
index 000000000..c8bfcd1e0
--- /dev/null
+++ b/yarn.lock
@@ -0,0 +1,7845 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@babel/code-frame@7.12.11":
+  version "7.12.11"
+  resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.11.tgz#f4ad435aa263db935b8f10f2c552d23fb716a63f"
+  integrity sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==
+  dependencies:
+    "@babel/highlight" "^7.10.4"
+
+"@babel/code-frame@^7.0.0":
+  version "7.16.7"
+  resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.16.7.tgz#44416b6bd7624b998f5b1af5d470856c40138789"
+  integrity sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==
+  dependencies:
+    "@babel/highlight" "^7.16.7"
+
+"@babel/helper-validator-identifier@^7.16.7":
+  version "7.16.7"
+  resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz#e8c602438c4a8195751243da9031d1607d247cad"
+  integrity sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==
+
+"@babel/highlight@^7.10.4", "@babel/highlight@^7.16.7":
+  version "7.17.12"
+  resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.17.12.tgz#257de56ee5afbd20451ac0a75686b6b404257351"
+  integrity sha512-7yykMVF3hfZY2jsHZEEgLc+3x4o1O+fYyULu11GynEUQNwB6lua+IIQn1FiJxNucd5UlyJryrwsOh8PL9Sn8Qg==
+  dependencies:
+    "@babel/helper-validator-identifier" "^7.16.7"
+    chalk "^2.0.0"
+    js-tokens "^4.0.0"
+
+"@babel/runtime@^7.1.2":
+  version "7.18.3"
+  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.18.3.tgz#c7b654b57f6f63cf7f8b418ac9ca04408c4579f4"
+  integrity sha512-38Y8f7YUhce/K7RMwTp7m0uCumpv9hZkitCbBClqQIow1qSbCvGkcegKOXpEWCQLfWmevgRiWokZ1GkpfhbZug==
+  dependencies:
+    regenerator-runtime "^0.13.4"
+
+"@blueprintjs/colors@^4.0.0-alpha.3":
+  version "4.1.3"
+  resolved "https://registry.yarnpkg.com/@blueprintjs/colors/-/colors-4.1.3.tgz#7e0a32a086bdc68ea51df0dda1f94913dbd874c1"
+  integrity sha512-ANRQZT5h9+zC8B/y0S9B+SqEpicL0XRT4drAhiPFHBrOStRZWzOh3bPrwNSPqr7tdShxYtMyxbH+fkHMetZaxg==
+
+"@blueprintjs/core@^3.36.0", "@blueprintjs/core@^3.54.0":
+  version "3.54.0"
+  resolved "https://registry.yarnpkg.com/@blueprintjs/core/-/core-3.54.0.tgz#7269f34eccdf0d2874377c5ad973ca2a31562221"
+  integrity sha512-u2c1s6MNn0ocxhnC6CuiG5g3KV6b4cKUvSobznepA9SC3/AL1s3XOvT7DLWoHRv2B/vBOHFYEDzLw2/vlcGGZg==
+  dependencies:
+    "@blueprintjs/colors" "^4.0.0-alpha.3"
+    "@blueprintjs/icons" "^3.33.0"
+    "@juggle/resize-observer" "^3.3.1"
+    "@types/dom4" "^2.0.1"
+    classnames "^2.2"
+    dom4 "^2.1.5"
+    normalize.css "^8.0.1"
+    popper.js "^1.16.1"
+    react-lifecycles-compat "^3.0.4"
+    react-popper "^1.3.7"
+    react-transition-group "^2.9.0"
+    tslib "~2.3.1"
+
+"@blueprintjs/icons@^3.33.0":
+  version "3.33.0"
+  resolved "https://registry.yarnpkg.com/@blueprintjs/icons/-/icons-3.33.0.tgz#4dacdb7731abdf08d1ab240f3a23a185df60918b"
+  integrity sha512-Q6qoSDIm0kRYQZISm59UUcDCpV3oeHulkLuh3bSlw0HhcSjvEQh2PSYbtaifM60Q4aK4PCd6bwJHg7lvF1x5fQ==
+  dependencies:
+    classnames "^2.2"
+    tslib "~2.3.1"
+
+"@blueprintjs/select@^3.15.0":
+  version "3.19.1"
+  resolved "https://registry.yarnpkg.com/@blueprintjs/select/-/select-3.19.1.tgz#b5e8baa6f182a0647651a57fde8d1d97eaa1e997"
+  integrity sha512-8UJIZMaWXRMQHr14wbmzJc/CklcSKxOU5JUux0xXKQz/hDW/g1a650tlwJmnxufvRdShbGinlVfHupCs0EL6sw==
+  dependencies:
+    "@blueprintjs/core" "^3.54.0"
+    classnames "^2.2"
+    tslib "~2.3.1"
+
+"@csstools/selector-specificity@^2.0.1":
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/@csstools/selector-specificity/-/selector-specificity-2.0.1.tgz#b6b8d81780b9a9f6459f4bfe9226ac6aefaefe87"
+  integrity sha512-aG20vknL4/YjQF9BSV7ts4EWm/yrjagAN7OWBNmlbEOUiu0llj4OGrFoOKK3g2vey4/p2omKCoHrWtPxSwV3HA==
+
+"@discoveryjs/json-ext@^0.5.0":
+  version "0.5.7"
+  resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70"
+  integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==
+
+"@eslint/eslintrc@^0.4.3":
+  version "0.4.3"
+  resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.4.3.tgz#9e42981ef035beb3dd49add17acb96e8ff6f394c"
+  integrity sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==
+  dependencies:
+    ajv "^6.12.4"
+    debug "^4.1.1"
+    espree "^7.3.0"
+    globals "^13.9.0"
+    ignore "^4.0.6"
+    import-fresh "^3.2.1"
+    js-yaml "^3.13.1"
+    minimatch "^3.0.4"
+    strip-json-comments "^3.1.1"
+
+"@fortawesome/fontawesome-free@^5.12.0":
+  version "5.15.4"
+  resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.4.tgz#ecda5712b61ac852c760d8b3c79c96adca5554e5"
+  integrity sha512-eYm8vijH/hpzr/6/1CJ/V/Eb1xQFW2nnUKArb3z+yUWv7HTwj6M7SP957oMjfZjAHU6qpoNc2wQvIxBLWYa/Jg==
+
+"@gar/promisify@^1.0.1":
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6"
+  integrity sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==
+
+"@humanwhocodes/config-array@^0.5.0":
+  version "0.5.0"
+  resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.5.0.tgz#1407967d4c6eecd7388f83acf1eaf4d0c6e58ef9"
+  integrity sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==
+  dependencies:
+    "@humanwhocodes/object-schema" "^1.2.0"
+    debug "^4.1.1"
+    minimatch "^3.0.4"
+
+"@humanwhocodes/object-schema@^1.2.0":
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45"
+  integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==
+
+"@hypnosphi/create-react-context@^0.3.1":
+  version "0.3.1"
+  resolved "https://registry.yarnpkg.com/@hypnosphi/create-react-context/-/create-react-context-0.3.1.tgz#f8bfebdc7665f5d426cba3753e0e9c7d3154d7c6"
+  integrity sha512-V1klUed202XahrWJLLOT3EXNeCpFHCcJntdFGI15ntCwau+jfT386w7OFTMaCqOgXUH1fa0w/I1oZs+i/Rfr0A==
+  dependencies:
+    gud "^1.0.0"
+    warning "^4.0.3"
+
+"@jridgewell/gen-mapping@^0.3.0":
+  version "0.3.1"
+  resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.1.tgz#cf92a983c83466b8c0ce9124fadeaf09f7c66ea9"
+  integrity sha512-GcHwniMlA2z+WFPWuY8lp3fsza0I8xPFMWL5+n8LYyP6PSvPrXf4+n8stDHZY2DM0zy9sVkRDy1jDI4XGzYVqg==
+  dependencies:
+    "@jridgewell/set-array" "^1.0.0"
+    "@jridgewell/sourcemap-codec" "^1.4.10"
+    "@jridgewell/trace-mapping" "^0.3.9"
+
+"@jridgewell/resolve-uri@^3.0.3":
+  version "3.0.7"
+  resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.0.7.tgz#30cd49820a962aff48c8fffc5cd760151fca61fe"
+  integrity sha512-8cXDaBBHOr2pQ7j77Y6Vp5VDT2sIqWyWQ56TjEq4ih/a4iST3dItRe8Q9fp0rrIl9DoKhWQtUQz/YpOxLkXbNA==
+
+"@jridgewell/set-array@^1.0.0":
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.1.tgz#36a6acc93987adcf0ba50c66908bd0b70de8afea"
+  integrity sha512-Ct5MqZkLGEXTVmQYbGtx9SVqD2fqwvdubdps5D3djjAkgkKwT918VNOz65pEHFaYTeWcukmJmH5SwsA9Tn2ObQ==
+
+"@jridgewell/source-map@^0.3.2":
+  version "0.3.2"
+  resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.2.tgz#f45351aaed4527a298512ec72f81040c998580fb"
+  integrity sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==
+  dependencies:
+    "@jridgewell/gen-mapping" "^0.3.0"
+    "@jridgewell/trace-mapping" "^0.3.9"
+
+"@jridgewell/sourcemap-codec@^1.4.10":
+  version "1.4.13"
+  resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.13.tgz#b6461fb0c2964356c469e115f504c95ad97ab88c"
+  integrity sha512-GryiOJmNcWbovBxTfZSF71V/mXbgcV3MewDe3kIMCLyIh5e7SKAeUZs+rMnJ8jkMolZ/4/VsdBmMrw3l+VdZ3w==
+
+"@jridgewell/trace-mapping@^0.3.7", "@jridgewell/trace-mapping@^0.3.9":
+  version "0.3.13"
+  resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.13.tgz#dcfe3e95f224c8fe97a87a5235defec999aa92ea"
+  integrity sha512-o1xbKhp9qnIAoHJSWd6KlCZfqslL4valSF81H8ImioOAxluWYWOpWkpyktY2vnt4tbrX9XYaxovq6cgowaJp2w==
+  dependencies:
+    "@jridgewell/resolve-uri" "^3.0.3"
+    "@jridgewell/sourcemap-codec" "^1.4.10"
+
+"@juggle/resize-observer@^3.3.1":
+  version "3.3.1"
+  resolved "https://registry.yarnpkg.com/@juggle/resize-observer/-/resize-observer-3.3.1.tgz#b50a781709c81e10701004214340f25475a171a0"
+  integrity sha512-zMM9Ds+SawiUkakS7y94Ymqx+S0ORzpG3frZirN3l+UlXUmSUR7hF4wxCVqW+ei94JzV5kt0uXBcoOEAuiydrw==
+
+"@jupyterlab/application@^3.1.0", "@jupyterlab/application@^3.4.3":
+  version "3.4.3"
+  resolved "https://registry.yarnpkg.com/@jupyterlab/application/-/application-3.4.3.tgz#acaecafbce1be71776ceee11203b9fb0a02e27d8"
+  integrity sha512-JZy/4yB23N9o0gJFL8L219N7cWhUgulBECa+esN8K/i3TVoG0m/obacLBJrmwdgmQKAgyJ2Vfo1tyhW3UHvlFw==
+  dependencies:
+    "@fortawesome/fontawesome-free" "^5.12.0"
+    "@jupyterlab/apputils" "^3.4.3"
+    "@jupyterlab/coreutils" "^5.4.3"
+    "@jupyterlab/docregistry" "^3.4.3"
+    "@jupyterlab/rendermime" "^3.4.3"
+    "@jupyterlab/rendermime-interfaces" "^3.4.3"
+    "@jupyterlab/services" "^6.4.3"
+    "@jupyterlab/statedb" "^3.4.3"
+    "@jupyterlab/translation" "^3.4.3"
+    "@jupyterlab/ui-components" "^3.4.3"
+    "@lumino/algorithm" "^1.9.0"
+    "@lumino/application" "^1.27.0"
+    "@lumino/commands" "^1.19.0"
+    "@lumino/coreutils" "^1.11.0"
+    "@lumino/disposable" "^1.10.0"
+    "@lumino/messaging" "^1.10.0"
+    "@lumino/polling" "^1.9.0"
+    "@lumino/properties" "^1.8.0"
+    "@lumino/signaling" "^1.10.0"
+    "@lumino/widgets" "^1.30.0"
+
+"@jupyterlab/apputils@^3.1.0", "@jupyterlab/apputils@^3.4.3":
+  version "3.4.3"
+  resolved "https://registry.yarnpkg.com/@jupyterlab/apputils/-/apputils-3.4.3.tgz#ad69a437c4b07a5df62e9f7f33075850f5a2ca46"
+  integrity sha512-zpMnYVxXiOJSYHrrpfptmlmIzMXB/slmLh5Fv5gG6QAaBoMBHq11cVZWUiWoXt5r6hYEco8jIG7iMN8/MCsWYQ==
+  dependencies:
+    "@jupyterlab/coreutils" "^5.4.3"
+    "@jupyterlab/observables" "^4.4.3"
+    "@jupyterlab/services" "^6.4.3"
+    "@jupyterlab/settingregistry" "^3.4.3"
+    "@jupyterlab/statedb" "^3.4.3"
+    "@jupyterlab/translation" "^3.4.3"
+    "@jupyterlab/ui-components" "^3.4.3"
+    "@lumino/algorithm" "^1.9.0"
+    "@lumino/commands" "^1.19.0"
+    "@lumino/coreutils" "^1.11.0"
+    "@lumino/disposable" "^1.10.0"
+    "@lumino/domutils" "^1.8.0"
+    "@lumino/messaging" "^1.10.0"
+    "@lumino/polling" "^1.9.0"
+    "@lumino/properties" "^1.8.0"
+    "@lumino/signaling" "^1.10.0"
+    "@lumino/virtualdom" "^1.14.0"
+    "@lumino/widgets" "^1.30.0"
+    "@types/react" "^17.0.0"
+    react "^17.0.1"
+    react-dom "^17.0.1"
+    sanitize-html "~2.5.3"
+    url "^0.11.0"
+
+"@jupyterlab/attachments@^3.4.3":
+  version "3.4.3"
+  resolved "https://registry.yarnpkg.com/@jupyterlab/attachments/-/attachments-3.4.3.tgz#a958bb529c3f606694d0b60211b7b8b882a1ebf7"
+  integrity sha512-/Gwi00nwC294iUUM5yFu3RkN0Ds5aJV0w2j0b+FLZsJONy2BYfRSgYYAnm67483vuVu7zZU5umPS1UWLPXIMbg==
+  dependencies:
+    "@jupyterlab/nbformat" "^3.4.3"
+    "@jupyterlab/observables" "^4.4.3"
+    "@jupyterlab/rendermime" "^3.4.3"
+    "@jupyterlab/rendermime-interfaces" "^3.4.3"
+    "@lumino/disposable" "^1.10.0"
+    "@lumino/signaling" "^1.10.0"
+
+"@jupyterlab/builder@^3.3.3":
+  version "3.4.3"
+  resolved "https://registry.yarnpkg.com/@jupyterlab/builder/-/builder-3.4.3.tgz#060596148fb248e27526a7f7519836229b2010db"
+  integrity sha512-g6aSUyUOunN9vS5+eS1p+9w5gcmKqDy3mvchMQSrMqyhmt0cHQHO+eMQNbCs51IG1jTYl1e4+vGau8ce+JtLZg==
+  dependencies:
+    "@jupyterlab/buildutils" "^3.4.3"
+    "@lumino/algorithm" "^1.9.0"
+    "@lumino/application" "^1.27.0"
+    "@lumino/commands" "^1.19.0"
+    "@lumino/coreutils" "^1.11.0"
+    "@lumino/disposable" "^1.10.0"
+    "@lumino/domutils" "^1.8.0"
+    "@lumino/dragdrop" "^1.13.0"
+    "@lumino/messaging" "^1.10.0"
+    "@lumino/properties" "^1.8.0"
+    "@lumino/signaling" "^1.10.0"
+    "@lumino/virtualdom" "^1.14.0"
+    "@lumino/widgets" "^1.30.0"
+    ajv "^6.12.3"
+    commander "~6.0.0"
+    css-loader "^5.0.1"
+    duplicate-package-checker-webpack-plugin "^3.0.0"
+    file-loader "~6.0.0"
+    fs-extra "^9.0.1"
+    glob "~7.1.6"
+    license-webpack-plugin "^2.3.14"
+    mini-css-extract-plugin "~1.3.2"
+    path-browserify "^1.0.0"
+    process "^0.11.10"
+    raw-loader "~4.0.0"
+    style-loader "~2.0.0"
+    supports-color "^7.2.0"
+    svg-url-loader "~6.0.0"
+    terser-webpack-plugin "^4.1.0"
+    to-string-loader "^1.1.6"
+    url-loader "~4.1.0"
+    webpack "^5.41.1"
+    webpack-cli "^4.1.0"
+    webpack-merge "^5.1.2"
+    worker-loader "^3.0.2"
+
+"@jupyterlab/buildutils@^3.4.3":
+  version "3.4.3"
+  resolved "https://registry.yarnpkg.com/@jupyterlab/buildutils/-/buildutils-3.4.3.tgz#6057532c6bba5590abb203c51a6a29b1a6a08063"
+  integrity sha512-M09nIGIAevtQ2VkQiHqU7uvRd8AEHP7G5unLwsDyJvO0WON3oQPApPH1hJn/GwjkuxXQvB8MSx7LDmMKewNrEg==
+  dependencies:
+    "@lumino/coreutils" "^1.11.0"
+    "@yarnpkg/lockfile" "^1.1.0"
+    child_process "~1.0.2"
+    commander "~6.0.0"
+    crypto "~1.0.1"
+    dependency-graph "^0.9.0"
+    fs-extra "^9.0.1"
+    glob "~7.1.6"
+    inquirer "^7.1.0"
+    minimatch "~3.0.4"
+    os "~0.1.1"
+    package-json "^6.5.0"
+    prettier "~2.1.1"
+    process "^0.11.10"
+    semver "^7.3.2"
+    sort-package-json "~1.44.0"
+    typescript "~4.1.3"
+    verdaccio "^5.1.1"
+
+"@jupyterlab/cells@^3.3.3", "@jupyterlab/cells@^3.4.3":
+  version "3.4.3"
+  resolved "https://registry.yarnpkg.com/@jupyterlab/cells/-/cells-3.4.3.tgz#67f318d0d142716653fc5fda01b16dae99c89e80"
+  integrity sha512-ablkAPBd6w0Wxo1gi3VYEKX9HxexGHb7X2xbiglosuiBrng6a/4Ozata9vPiZbjNCmvFmstQF3HcVr5lg/Hv6A==
+  dependencies:
+    "@jupyterlab/apputils" "^3.4.3"
+    "@jupyterlab/attachments" "^3.4.3"
+    "@jupyterlab/codeeditor" "^3.4.3"
+    "@jupyterlab/codemirror" "^3.4.3"
+    "@jupyterlab/coreutils" "^5.4.3"
+    "@jupyterlab/filebrowser" "^3.4.3"
+    "@jupyterlab/nbformat" "^3.4.3"
+    "@jupyterlab/observables" "^4.4.3"
+    "@jupyterlab/outputarea" "^3.4.3"
+    "@jupyterlab/rendermime" "^3.4.3"
+    "@jupyterlab/services" "^6.4.3"
+    "@jupyterlab/shared-models" "^3.4.3"
+    "@jupyterlab/ui-components" "^3.4.3"
+    "@lumino/algorithm" "^1.9.0"
+    "@lumino/coreutils" "^1.11.0"
+    "@lumino/domutils" "^1.8.0"
+    "@lumino/dragdrop" "^1.13.0"
+    "@lumino/messaging" "^1.10.0"
+    "@lumino/polling" "^1.9.0"
+    "@lumino/signaling" "^1.10.0"
+    "@lumino/virtualdom" "^1.14.0"
+    "@lumino/widgets" "^1.30.0"
+    marked "^4.0.10"
+    react "^17.0.1"
+
+"@jupyterlab/codeeditor@^3.4.3":
+  version "3.4.3"
+  resolved "https://registry.yarnpkg.com/@jupyterlab/codeeditor/-/codeeditor-3.4.3.tgz#f69dbd1f25307382b863ecbe9b4f0a5037e264c0"
+  integrity sha512-xnEMGS6Y6UlPQi+suFCrf7rPPnNbC6hRI+yBgsK82O7MiMjHIti/V1dcjP93xjvTuCZexO8qCtRRLoMtTSELWA==
+  dependencies:
+    "@jupyterlab/coreutils" "^5.4.3"
+    "@jupyterlab/nbformat" "^3.4.3"
+    "@jupyterlab/observables" "^4.4.3"
+    "@jupyterlab/shared-models" "^3.4.3"
+    "@jupyterlab/translation" "^3.4.3"
+    "@jupyterlab/ui-components" "^3.4.3"
+    "@lumino/coreutils" "^1.11.0"
+    "@lumino/disposable" "^1.10.0"
+    "@lumino/dragdrop" "^1.13.0"
+    "@lumino/messaging" "^1.10.0"
+    "@lumino/signaling" "^1.10.0"
+    "@lumino/widgets" "^1.30.0"
+
+"@jupyterlab/codemirror@^3.4.3":
+  version "3.4.3"
+  resolved "https://registry.yarnpkg.com/@jupyterlab/codemirror/-/codemirror-3.4.3.tgz#349421866176143d2503dd600d2a55e77a1ab63a"
+  integrity sha512-Fe2yP/d6bUFAz4dNILwst4AsWZP0/cZOt+vnqu5VF/f2F/u3XTaaYNMQKnESRnm/eV+ZwkE+tKE7jF9begshuQ==
+  dependencies:
+    "@jupyterlab/apputils" "^3.4.3"
+    "@jupyterlab/codeeditor" "^3.4.3"
+    "@jupyterlab/coreutils" "^5.4.3"
+    "@jupyterlab/nbformat" "^3.4.3"
+    "@jupyterlab/observables" "^4.4.3"
+    "@jupyterlab/shared-models" "^3.4.3"
+    "@jupyterlab/statusbar" "^3.4.3"
+    "@jupyterlab/translation" "^3.4.3"
+    "@lumino/algorithm" "^1.9.0"
+    "@lumino/commands" "^1.19.0"
+    "@lumino/coreutils" "^1.11.0"
+    "@lumino/disposable" "^1.10.0"
+    "@lumino/polling" "^1.9.0"
+    "@lumino/signaling" "^1.10.0"
+    "@lumino/widgets" "^1.30.0"
+    codemirror "~5.61.0"
+    react "^17.0.1"
+    y-codemirror "^3.0.1"
+
+"@jupyterlab/coreutils@^5.1.0", "@jupyterlab/coreutils@^5.4.3":
+  version "5.4.3"
+  resolved "https://registry.yarnpkg.com/@jupyterlab/coreutils/-/coreutils-5.4.3.tgz#9a9ed66b045a9f7cee2064cb8b29e0bcc759f8c1"
+  integrity sha512-9X021xXTDVnCSYGXsRsMsxCaoHk28oHC/fUTytc5DRgTcpzOGPBO/mVpgJfyejOdy4HxBbAovJSnkBlPwjm1yA==
+  dependencies:
+    "@lumino/coreutils" "^1.11.0"
+    "@lumino/disposable" "^1.10.0"
+    "@lumino/signaling" "^1.10.0"
+    minimist "~1.2.0"
+    moment "^2.24.0"
+    path-browserify "^1.0.0"
+    url-parse "~1.5.1"
+
+"@jupyterlab/docmanager@^3.4.3":
+  version "3.4.3"
+  resolved "https://registry.yarnpkg.com/@jupyterlab/docmanager/-/docmanager-3.4.3.tgz#26e9571a93f1a6a6d7b59f9296499fdcabd998b7"
+  integrity sha512-l1rVpdbT2Y9KciNy+HN5Ef4YZsbYbWDVwPJhG7kkHDlwY0KXv7fk4NYccK0Q3LhwrJUh/iU3nJUVs2u2jJeIBg==
+  dependencies:
+    "@jupyterlab/apputils" "^3.4.3"
+    "@jupyterlab/coreutils" "^5.4.3"
+    "@jupyterlab/docprovider" "^3.4.3"
+    "@jupyterlab/docregistry" "^3.4.3"
+    "@jupyterlab/services" "^6.4.3"
+    "@jupyterlab/statusbar" "^3.4.3"
+    "@jupyterlab/translation" "^3.4.3"
+    "@lumino/algorithm" "^1.9.0"
+    "@lumino/coreutils" "^1.11.0"
+    "@lumino/disposable" "^1.10.0"
+    "@lumino/messaging" "^1.10.0"
+    "@lumino/properties" "^1.8.0"
+    "@lumino/signaling" "^1.10.0"
+    "@lumino/widgets" "^1.30.0"
+    react "^17.0.1"
+
+"@jupyterlab/docprovider@^3.4.3":
+  version "3.4.3"
+  resolved "https://registry.yarnpkg.com/@jupyterlab/docprovider/-/docprovider-3.4.3.tgz#2e041230424b2dc4dea5b72d90461cea8c4f5687"
+  integrity sha512-BnBkbFESEBGB9Df60hxtC5MAhHi1suReC4eN2Y1OsVXCbq2OTeTAP5DHeJjFDxBZDY1UQlss1li6+/IPbeXgVw==
+  dependencies:
+    "@jupyterlab/shared-models" "^3.4.3"
+    "@lumino/coreutils" "^1.11.0"
+    lib0 "^0.2.42"
+    y-websocket "^1.3.15"
+    yjs "^13.5.17"
+
+"@jupyterlab/docregistry@^3.4.3":
+  version "3.4.3"
+  resolved "https://registry.yarnpkg.com/@jupyterlab/docregistry/-/docregistry-3.4.3.tgz#ad24b5312797a2b53ba0d0975123e75c4eb37696"
+  integrity sha512-A+WXGj9HosbSh/I0XQbXN1Sxt4GbjUDXntQ5DGoeOoLfwLHBTkvgg7lZ+AJJyXTDz4jxCWe5jNWnwPkk5mp90Q==
+  dependencies:
+    "@jupyterlab/apputils" "^3.4.3"
+    "@jupyterlab/codeeditor" "^3.4.3"
+    "@jupyterlab/codemirror" "^3.4.3"
+    "@jupyterlab/coreutils" "^5.4.3"
+    "@jupyterlab/docprovider" "^3.4.3"
+    "@jupyterlab/observables" "^4.4.3"
+    "@jupyterlab/rendermime" "^3.4.3"
+    "@jupyterlab/rendermime-interfaces" "^3.4.3"
+    "@jupyterlab/services" "^6.4.3"
+    "@jupyterlab/shared-models" "^3.4.3"
+    "@jupyterlab/translation" "^3.4.3"
+    "@jupyterlab/ui-components" "^3.4.3"
+    "@lumino/algorithm" "^1.9.0"
+    "@lumino/coreutils" "^1.11.0"
+    "@lumino/disposable" "^1.10.0"
+    "@lumino/messaging" "^1.10.0"
+    "@lumino/signaling" "^1.10.0"
+    "@lumino/widgets" "^1.30.0"
+    yjs "^13.5.17"
+
+"@jupyterlab/filebrowser@^3.4.3":
+  version "3.4.3"
+  resolved "https://registry.yarnpkg.com/@jupyterlab/filebrowser/-/filebrowser-3.4.3.tgz#73d288a3a103d13c83734bd23647d1479bd297ec"
+  integrity sha512-VlUcEvRAG/eAkxgk90MKKbAYUv3BORxfp9VXUdRm/YOW66ZborF6TNbzjYh9C7L7psXAcpzQ+85p73o25iMDoA==
+  dependencies:
+    "@jupyterlab/apputils" "^3.4.3"
+    "@jupyterlab/coreutils" "^5.4.3"
+    "@jupyterlab/docmanager" "^3.4.3"
+    "@jupyterlab/docregistry" "^3.4.3"
+    "@jupyterlab/services" "^6.4.3"
+    "@jupyterlab/statedb" "^3.4.3"
+    "@jupyterlab/statusbar" "^3.4.3"
+    "@jupyterlab/translation" "^3.4.3"
+    "@jupyterlab/ui-components" "^3.4.3"
+    "@lumino/algorithm" "^1.9.0"
+    "@lumino/coreutils" "^1.11.0"
+    "@lumino/disposable" "^1.10.0"
+    "@lumino/domutils" "^1.8.0"
+    "@lumino/dragdrop" "^1.13.0"
+    "@lumino/messaging" "^1.10.0"
+    "@lumino/polling" "^1.9.0"
+    "@lumino/signaling" "^1.10.0"
+    "@lumino/virtualdom" "^1.14.0"
+    "@lumino/widgets" "^1.30.0"
+    react "^17.0.1"
+
+"@jupyterlab/galata@^4.2.4":
+  version "4.3.3"
+  resolved "https://registry.yarnpkg.com/@jupyterlab/galata/-/galata-4.3.3.tgz#46229726c0abeb53ca25a63253ac1d45beec29e8"
+  integrity sha512-xsBlB1gXuInp+HLvxZRDl2A4BiUGJWtax37jKYoI55Y/tBxJSYatrSgunc1QGUU8q1Zy2clnZRnRUYDN9kAQfA==
+  dependencies:
+    "@jupyterlab/application" "^3.4.3"
+    "@jupyterlab/cells" "^3.4.3"
+    "@jupyterlab/coreutils" "^5.4.3"
+    "@jupyterlab/docmanager" "^3.4.3"
+    "@jupyterlab/nbformat" "^3.4.3"
+    "@jupyterlab/notebook" "^3.4.3"
+    "@jupyterlab/services" "^6.4.3"
+    "@jupyterlab/settingregistry" "^3.4.3"
+    "@lumino/algorithm" "^1.9.0"
+    "@lumino/coreutils" "^1.11.0"
+    "@playwright/test" "^1.16.2"
+    "@stdlib/stats" "^0.0.13"
+    canvas "^2.6.1"
+    fs-extra "^9.0.1"
+    http-server "^13.0.0"
+    json5 "^2.1.1"
+    node-fetch "^2.6.0"
+    path "~0.12.7"
+    systeminformation "^5.8.6"
+    vega "^5.20.0"
+    vega-lite "^5.1.0"
+    vega-statistics "^1.7.9"
+
+"@jupyterlab/launcher@^3.4.2":
+  version "3.4.3"
+  resolved "https://registry.yarnpkg.com/@jupyterlab/launcher/-/launcher-3.4.3.tgz#3370cc6d5a4b85b6ef18ac4c8f18b69a9dae6541"
+  integrity sha512-f+N8tdrvt9zUkGg0dOlJGQVsjZDXlsMm57c5A5uEk7vujieuLbs6Mo1bJu9Cth3qZ5+GWx+axp5X8p/dHayPmQ==
+  dependencies:
+    "@jupyterlab/apputils" "^3.4.3"
+    "@jupyterlab/translation" "^3.4.3"
+    "@jupyterlab/ui-components" "^3.4.3"
+    "@lumino/algorithm" "^1.9.0"
+    "@lumino/commands" "^1.19.0"
+    "@lumino/coreutils" "^1.11.0"
+    "@lumino/disposable" "^1.10.0"
+    "@lumino/properties" "^1.8.0"
+    "@lumino/widgets" "^1.30.0"
+    react "^17.0.1"
+
+"@jupyterlab/nbformat@^3.4.3":
+  version "3.4.3"
+  resolved "https://registry.yarnpkg.com/@jupyterlab/nbformat/-/nbformat-3.4.3.tgz#cbab1bf507677b7f0f309d8353fc83fe5a973c82"
+  integrity sha512-i/yADrwhhAJJCUOTa+fEBMyJO7fvX9Y73I0B7V6dQhGcrmrEKLC3wk4yOo63+jRntd5+dupbiOtz3w1ncIXwIA==
+  dependencies:
+    "@lumino/coreutils" "^1.11.0"
+
+"@jupyterlab/notebook@^3.3.3", "@jupyterlab/notebook@^3.4.3":
+  version "3.4.3"
+  resolved "https://registry.yarnpkg.com/@jupyterlab/notebook/-/notebook-3.4.3.tgz#81cdbf6c918aa7fc10309dcc267dcf786b4f9f2e"
+  integrity sha512-gssdX3pgt9kPqlNkQhmMhHatmWDgYW+Q/2MXTwATR2p//ODAtsrSpA4Che88HSP9H5mDY9xy1vUr1aZcW0Tnzg==
+  dependencies:
+    "@jupyterlab/apputils" "^3.4.3"
+    "@jupyterlab/cells" "^3.4.3"
+    "@jupyterlab/codeeditor" "^3.4.3"
+    "@jupyterlab/coreutils" "^5.4.3"
+    "@jupyterlab/docregistry" "^3.4.3"
+    "@jupyterlab/nbformat" "^3.4.3"
+    "@jupyterlab/observables" "^4.4.3"
+    "@jupyterlab/rendermime" "^3.4.3"
+    "@jupyterlab/services" "^6.4.3"
+    "@jupyterlab/settingregistry" "^3.4.3"
+    "@jupyterlab/shared-models" "^3.4.3"
+    "@jupyterlab/statusbar" "^3.4.3"
+    "@jupyterlab/translation" "^3.4.3"
+    "@jupyterlab/ui-components" "^3.4.3"
+    "@lumino/algorithm" "^1.9.0"
+    "@lumino/coreutils" "^1.11.0"
+    "@lumino/domutils" "^1.8.0"
+    "@lumino/dragdrop" "^1.13.0"
+    "@lumino/messaging" "^1.10.0"
+    "@lumino/properties" "^1.8.0"
+    "@lumino/signaling" "^1.10.0"
+    "@lumino/virtualdom" "^1.14.0"
+    "@lumino/widgets" "^1.30.0"
+    react "^17.0.1"
+
+"@jupyterlab/observables@^4.4.3":
+  version "4.4.3"
+  resolved "https://registry.yarnpkg.com/@jupyterlab/observables/-/observables-4.4.3.tgz#41d07af0987dc37953214e20ee1dfc0b15669ef0"
+  integrity sha512-AUuNoBIcctmJip4pZEYfmw14/FjTeyO3lVgp0pgZWTowzI6ihJP8pWaxc5GtfHOPGTn+S81r1FSPSiLLFqFyZg==
+  dependencies:
+    "@lumino/algorithm" "^1.9.0"
+    "@lumino/coreutils" "^1.11.0"
+    "@lumino/disposable" "^1.10.0"
+    "@lumino/messaging" "^1.10.0"
+    "@lumino/signaling" "^1.10.0"
+
+"@jupyterlab/outputarea@^3.4.3":
+  version "3.4.3"
+  resolved "https://registry.yarnpkg.com/@jupyterlab/outputarea/-/outputarea-3.4.3.tgz#285bb767334d82bbbb3b94f80cefeee746972c3d"
+  integrity sha512-acckQMJZOR9D1gcKhaBtP6seopjDpZXYChF9ZeuwgnJJNc+ct3V3iL5lAn+Y2l9BslGP5ogbhT7DDNVsXU8eoQ==
+  dependencies:
+    "@jupyterlab/apputils" "^3.4.3"
+    "@jupyterlab/nbformat" "^3.4.3"
+    "@jupyterlab/observables" "^4.4.3"
+    "@jupyterlab/rendermime" "^3.4.3"
+    "@jupyterlab/rendermime-interfaces" "^3.4.3"
+    "@jupyterlab/services" "^6.4.3"
+    "@lumino/algorithm" "^1.9.0"
+    "@lumino/coreutils" "^1.11.0"
+    "@lumino/disposable" "^1.10.0"
+    "@lumino/messaging" "^1.10.0"
+    "@lumino/properties" "^1.8.0"
+    "@lumino/signaling" "^1.10.0"
+    "@lumino/widgets" "^1.30.0"
+    resize-observer-polyfill "^1.5.1"
+
+"@jupyterlab/rendermime-interfaces@^3.4.3":
+  version "3.4.3"
+  resolved "https://registry.yarnpkg.com/@jupyterlab/rendermime-interfaces/-/rendermime-interfaces-3.4.3.tgz#c563a9f1927ad2ab7dbc7fcd73db1ca9039583e7"
+  integrity sha512-DPAUHiNpGJBPV45yabEajQrV3wt9/YyFrPjLJpKxFolNmQcbSvNWCetKqq698DvNCa2Ng5U+j8ivJZA7Iyfbjg==
+  dependencies:
+    "@jupyterlab/translation" "^3.4.3"
+    "@lumino/coreutils" "^1.11.0"
+    "@lumino/widgets" "^1.30.0"
+
+"@jupyterlab/rendermime@^3.4.3":
+  version "3.4.3"
+  resolved "https://registry.yarnpkg.com/@jupyterlab/rendermime/-/rendermime-3.4.3.tgz#f3e0ba202965ed2d1bf113a4f9caa944b0f06d48"
+  integrity sha512-2EaevAXBopW7yZWX/Nmh9bBRLuvBLcg4okyLQfMKM+rOtR5mS+/4QRXqqp7Nk3R9ofuWUBw6tQEKk8nhiFJS9w==
+  dependencies:
+    "@jupyterlab/apputils" "^3.4.3"
+    "@jupyterlab/codemirror" "^3.4.3"
+    "@jupyterlab/coreutils" "^5.4.3"
+    "@jupyterlab/nbformat" "^3.4.3"
+    "@jupyterlab/observables" "^4.4.3"
+    "@jupyterlab/rendermime-interfaces" "^3.4.3"
+    "@jupyterlab/services" "^6.4.3"
+    "@jupyterlab/translation" "^3.4.3"
+    "@lumino/algorithm" "^1.9.0"
+    "@lumino/coreutils" "^1.11.0"
+    "@lumino/messaging" "^1.10.0"
+    "@lumino/signaling" "^1.10.0"
+    "@lumino/widgets" "^1.30.0"
+    lodash.escape "^4.0.1"
+    marked "^4.0.10"
+
+"@jupyterlab/services@^6.1.0", "@jupyterlab/services@^6.4.3":
+  version "6.4.3"
+  resolved "https://registry.yarnpkg.com/@jupyterlab/services/-/services-6.4.3.tgz#4c01dba29dfe4b85342479338f52068545bcb64c"
+  integrity sha512-h0z+qlK3aMGwC/b1GJXscrLp6KH6xTygu8SRkey81hBkOVitE6We32b7VZD3iVPjZN7EdD4/EcjAjtl1EgsQlQ==
+  dependencies:
+    "@jupyterlab/coreutils" "^5.4.3"
+    "@jupyterlab/nbformat" "^3.4.3"
+    "@jupyterlab/observables" "^4.4.3"
+    "@jupyterlab/settingregistry" "^3.4.3"
+    "@jupyterlab/statedb" "^3.4.3"
+    "@lumino/algorithm" "^1.9.0"
+    "@lumino/coreutils" "^1.11.0"
+    "@lumino/disposable" "^1.10.0"
+    "@lumino/polling" "^1.9.0"
+    "@lumino/signaling" "^1.10.0"
+    node-fetch "^2.6.0"
+    ws "^7.4.6"
+
+"@jupyterlab/settingregistry@^3.1.0", "@jupyterlab/settingregistry@^3.4.3":
+  version "3.4.3"
+  resolved "https://registry.yarnpkg.com/@jupyterlab/settingregistry/-/settingregistry-3.4.3.tgz#531cb702a7eefdd12cce541893152056f66841d2"
+  integrity sha512-DYrlQz4FIhx9JP3lmevGY1MWgvDN/2ujpQxBZeuz3TPEoSwMpLNwXcI7U69XSm/CF99IN2W3V8LGOKx0M+T9Ug==
+  dependencies:
+    "@jupyterlab/statedb" "^3.4.3"
+    "@lumino/commands" "^1.19.0"
+    "@lumino/coreutils" "^1.11.0"
+    "@lumino/disposable" "^1.10.0"
+    "@lumino/signaling" "^1.10.0"
+    ajv "^6.12.3"
+    json5 "^2.1.1"
+
+"@jupyterlab/shared-models@^3.4.3":
+  version "3.4.3"
+  resolved "https://registry.yarnpkg.com/@jupyterlab/shared-models/-/shared-models-3.4.3.tgz#656b7108f16f78e092b11b6bf7ddaec59d518099"
+  integrity sha512-l59ufJoLifv7i7Dlg5112FBLGhA9gTmcFbqniGwmPjnrd5rbS5l+pNJoeMGVID+rWUyI1C0eE0K3V9Vz9Ub+4w==
+  dependencies:
+    "@jupyterlab/nbformat" "^3.4.3"
+    "@lumino/coreutils" "^1.11.0"
+    "@lumino/disposable" "^1.10.0"
+    "@lumino/signaling" "^1.10.0"
+    y-protocols "^1.0.5"
+    yjs "^13.5.17"
+
+"@jupyterlab/statedb@^3.4.3":
+  version "3.4.3"
+  resolved "https://registry.yarnpkg.com/@jupyterlab/statedb/-/statedb-3.4.3.tgz#30b8801d0cfdb60f0a790d309bbd968dc4185a01"
+  integrity sha512-Gr96oF20qEVv7jFDgDvi6GciLoGp+qo3lElqQdJhgqmLrQI9oTqtYOwkxLYjOzY8uhXI+Z4X1tZ7cRkNdoUCVw==
+  dependencies:
+    "@lumino/commands" "^1.19.0"
+    "@lumino/coreutils" "^1.11.0"
+    "@lumino/disposable" "^1.10.0"
+    "@lumino/properties" "^1.8.0"
+    "@lumino/signaling" "^1.10.0"
+
+"@jupyterlab/statusbar@^3.4.3":
+  version "3.4.3"
+  resolved "https://registry.yarnpkg.com/@jupyterlab/statusbar/-/statusbar-3.4.3.tgz#d9a35b79bb5c61b3215b778ff563fcf260c60070"
+  integrity sha512-cNx0EPedajqA9HCY/Yc34wh6ouZRUkdlVNaahVAhhj+qTs3HfGHFZagkjgPAg36cSPYrHyq1speRh0UGyqqyTw==
+  dependencies:
+    "@jupyterlab/apputils" "^3.4.3"
+    "@jupyterlab/codeeditor" "^3.4.3"
+    "@jupyterlab/services" "^6.4.3"
+    "@jupyterlab/translation" "^3.4.3"
+    "@jupyterlab/ui-components" "^3.4.3"
+    "@lumino/algorithm" "^1.9.0"
+    "@lumino/coreutils" "^1.11.0"
+    "@lumino/disposable" "^1.10.0"
+    "@lumino/messaging" "^1.10.0"
+    "@lumino/signaling" "^1.10.0"
+    "@lumino/widgets" "^1.30.0"
+    csstype "~3.0.3"
+    react "^17.0.1"
+    typestyle "^2.0.4"
+
+"@jupyterlab/translation@^3.4.3":
+  version "3.4.3"
+  resolved "https://registry.yarnpkg.com/@jupyterlab/translation/-/translation-3.4.3.tgz#a8aebfd74ae0da073a349072f4db3186fe65f437"
+  integrity sha512-NNpSqdFxxmx/miCTbTk/8fZapMArM8YJ2Q4WCzYcEiRVYfJbzozrSqIu8X0gNcIbV3IGD+GXueqWQb7xr637bQ==
+  dependencies:
+    "@jupyterlab/coreutils" "^5.4.3"
+    "@jupyterlab/services" "^6.4.3"
+    "@jupyterlab/statedb" "^3.4.3"
+    "@lumino/coreutils" "^1.11.0"
+
+"@jupyterlab/ui-components@^3.4.3":
+  version "3.4.3"
+  resolved "https://registry.yarnpkg.com/@jupyterlab/ui-components/-/ui-components-3.4.3.tgz#180ba2e0a273fce78ec9cf38782060a12064f02c"
+  integrity sha512-oFl3QXiQDjDEte5emdNpdnjAptbGt+pm+TieUmx/+/SpYGsgGV5F1lRHw7kOdyUAB8CIRayqlGZ5BpWGNLFT7g==
+  dependencies:
+    "@blueprintjs/core" "^3.36.0"
+    "@blueprintjs/select" "^3.15.0"
+    "@jupyterlab/coreutils" "^5.4.3"
+    "@jupyterlab/translation" "^3.4.3"
+    "@lumino/algorithm" "^1.9.0"
+    "@lumino/commands" "^1.19.0"
+    "@lumino/coreutils" "^1.11.0"
+    "@lumino/disposable" "^1.10.0"
+    "@lumino/signaling" "^1.10.0"
+    "@lumino/virtualdom" "^1.14.0"
+    "@lumino/widgets" "^1.30.0"
+    "@rjsf/core" "^3.1.0"
+    react "^17.0.1"
+    react-dom "^17.0.1"
+    typestyle "^2.0.4"
+
+"@lumino/algorithm@^1.9.0", "@lumino/algorithm@^1.9.1":
+  version "1.9.1"
+  resolved "https://registry.yarnpkg.com/@lumino/algorithm/-/algorithm-1.9.1.tgz#a870598e031f5ee85e20e77ce7bfffbb0dffd7f5"
+  integrity sha512-d0rj7IYRzYj6WbWSrbJbKvrfO4H0NUnXT2yjSWS/sCklpTpSp0IGmndK/X4r6gG+ev5lb5+wBg9ofUDBvoAlAw==
+
+"@lumino/application@^1.27.0":
+  version "1.29.1"
+  resolved "https://registry.yarnpkg.com/@lumino/application/-/application-1.29.1.tgz#aa37f51af00f70bcd2bd18d9ebc464ad6bcbb651"
+  integrity sha512-71ScZSOvHTBIwwL9S+CAAivNhDGEP+RZKUIyWlQOxtgETf88CO8FCUbLqhUUHw6nnzyVXjOORCgiOQEnLMvN9w==
+  dependencies:
+    "@lumino/commands" "^1.20.0"
+    "@lumino/coreutils" "^1.12.0"
+    "@lumino/widgets" "^1.32.1"
+
+"@lumino/collections@^1.9.1":
+  version "1.9.1"
+  resolved "https://registry.yarnpkg.com/@lumino/collections/-/collections-1.9.1.tgz#268f1ec6850d5e131cfc8db232c7e1e106144aa0"
+  integrity sha512-5RaRGUY7BJ/1j173sc9DCfiVf70Z0hopRnBV8/AeAaK9bJJRAYjDhlZ9O8xTyouegh6krkOfiDyjl3pwogLrQw==
+  dependencies:
+    "@lumino/algorithm" "^1.9.1"
+
+"@lumino/commands@^1.19.0", "@lumino/commands@^1.20.0":
+  version "1.20.0"
+  resolved "https://registry.yarnpkg.com/@lumino/commands/-/commands-1.20.0.tgz#44c797134bb33946141a490c506420bd5f12ce0f"
+  integrity sha512-xyrzDIJ9QEbcbRAwmXrjb7A7/E5MDNbnLANKwqmFVNF+4LSnF62obdvY4On3Rify3HmfX0u16Xr9gfoWPX9wLQ==
+  dependencies:
+    "@lumino/algorithm" "^1.9.1"
+    "@lumino/coreutils" "^1.12.0"
+    "@lumino/disposable" "^1.10.1"
+    "@lumino/domutils" "^1.8.1"
+    "@lumino/keyboard" "^1.8.1"
+    "@lumino/signaling" "^1.10.1"
+    "@lumino/virtualdom" "^1.14.1"
+
+"@lumino/coreutils@^1.11.0", "@lumino/coreutils@^1.12.0":
+  version "1.12.0"
+  resolved "https://registry.yarnpkg.com/@lumino/coreutils/-/coreutils-1.12.0.tgz#fbdef760f736eaf2bd396a5c6fc3a68a4b449b15"
+  integrity sha512-DSglh4ylmLi820CNx9soJmDJCpUgymckdWeGWuN0Ash5g60oQvrQDfosVxEhzmNvtvXv45WZEqSBzDP6E5SEmQ==
+
+"@lumino/disposable@^1.10.0", "@lumino/disposable@^1.10.1":
+  version "1.10.1"
+  resolved "https://registry.yarnpkg.com/@lumino/disposable/-/disposable-1.10.1.tgz#58fddc619cf89335802d168564b76ff5315d5a84"
+  integrity sha512-mZQILc8sVGZC7mJNOGVmehDRO9/u3sIRdjZ+pCYjDgXKcINLd6HoPhZDquKCWiRBfHTL1B3tOHjnBhahBc2N/Q==
+  dependencies:
+    "@lumino/algorithm" "^1.9.1"
+    "@lumino/signaling" "^1.10.1"
+
+"@lumino/domutils@^1.8.0", "@lumino/domutils@^1.8.1":
+  version "1.8.1"
+  resolved "https://registry.yarnpkg.com/@lumino/domutils/-/domutils-1.8.1.tgz#cf118e4eba90c3bf1e3edf7f19cce8846ec7875c"
+  integrity sha512-QUVXwmDMIfcHC3yslhmyGK4HYBKaJ3xX5MTwDrjsSX7J7AZ4jwL4zfsxyF9ntdqEKraoJhLQ6BaUBY+Ur1cnYw==
+
+"@lumino/dragdrop@^1.13.0", "@lumino/dragdrop@^1.14.0":
+  version "1.14.0"
+  resolved "https://registry.yarnpkg.com/@lumino/dragdrop/-/dragdrop-1.14.0.tgz#48baacc190518d0cb563698daa0d5b976d6fe5c3"
+  integrity sha512-hO8sgF0BkpihKIP6UZgVJgiOEhz89i7Oxtp9FR9Jqw5alGocxSXt7q3cteMvqpcL6o2/s3CafZNRkVLRXmepNw==
+  dependencies:
+    "@lumino/coreutils" "^1.12.0"
+    "@lumino/disposable" "^1.10.1"
+
+"@lumino/keyboard@^1.8.1":
+  version "1.8.1"
+  resolved "https://registry.yarnpkg.com/@lumino/keyboard/-/keyboard-1.8.1.tgz#e7850e2fb973fbb4c6e737ca8d9307f2dc3eb74b"
+  integrity sha512-8x0y2ZQtEvOsblpI2gfTgf+gboftusP+5aukKEsgNQtzFl28RezQXEOSVd8iD3K6+Q1MaPQF0OALYP0ASqBjBg==
+
+"@lumino/messaging@^1.10.0", "@lumino/messaging@^1.10.1":
+  version "1.10.1"
+  resolved "https://registry.yarnpkg.com/@lumino/messaging/-/messaging-1.10.1.tgz#b29575cca46e2f23b84626b793ec8e2be46a53ba"
+  integrity sha512-XZSdt9ih94rdeeLL0cryUw6HHD51D7TP8c+MFf+YRF6VKwOFB9RoajfQWadeqpmH+schTs3EsrFfA9KHduzC7w==
+  dependencies:
+    "@lumino/algorithm" "^1.9.1"
+    "@lumino/collections" "^1.9.1"
+
+"@lumino/polling@^1.9.0":
+  version "1.10.0"
+  resolved "https://registry.yarnpkg.com/@lumino/polling/-/polling-1.10.0.tgz#94a92811edf4c2534c741510b30f500d8c16a395"
+  integrity sha512-ZNXObJQfugnS41Yrlr7yWcFiRK+xAGGOXO08JJ0Mctsg5mT30UEGFVWJY2AjZ6N5aQuLyGed/pMkBzLzrzt8OA==
+  dependencies:
+    "@lumino/coreutils" "^1.12.0"
+    "@lumino/disposable" "^1.10.1"
+    "@lumino/signaling" "^1.10.1"
+
+"@lumino/properties@^1.8.0", "@lumino/properties@^1.8.1":
+  version "1.8.1"
+  resolved "https://registry.yarnpkg.com/@lumino/properties/-/properties-1.8.1.tgz#47eb8516e92c987dcb2c404db83a258159efec3d"
+  integrity sha512-O+CCcAqP64Di32DUZ4Jqq0DtUyE5RJREN5vbkgGZGu+WauJ/RYoiLDe1ubbAeSaHk71OrS60ZBV7QyC8ZaBVsA==
+
+"@lumino/signaling@^1.10.0", "@lumino/signaling@^1.10.1":
+  version "1.10.1"
+  resolved "https://registry.yarnpkg.com/@lumino/signaling/-/signaling-1.10.1.tgz#c8a1cb5b661b6744ea817c99c758fdc897847c26"
+  integrity sha512-GZVbX4cfk/ZqLwkemPD/NwqToaTL/6q7qdLpEhgkiPlaH1S5/V7fDpP7N1uFy4n3BDITId8cpYgH/Ds32Mdp3A==
+  dependencies:
+    "@lumino/algorithm" "^1.9.1"
+
+"@lumino/virtualdom@^1.14.0", "@lumino/virtualdom@^1.14.1":
+  version "1.14.1"
+  resolved "https://registry.yarnpkg.com/@lumino/virtualdom/-/virtualdom-1.14.1.tgz#2551b146cbe87c48d23754f370c1331a60c9fe62"
+  integrity sha512-imIJd/wtRkoR1onEiG5nxPEaIrf70nn4PgD/56ri3/Lo6AJEX2CusF6iIA27GVB8yl/7CxgTHUnzzCwTFPypcA==
+  dependencies:
+    "@lumino/algorithm" "^1.9.1"
+
+"@lumino/widgets@^1.30.0", "@lumino/widgets@^1.32.1":
+  version "1.32.1"
+  resolved "https://registry.yarnpkg.com/@lumino/widgets/-/widgets-1.32.1.tgz#77b085518b3c5e530008f0ec4840e4c39b905ac2"
+  integrity sha512-IA4BuoiiL9NB48HrM/6R7WM4mKy4O7JVVJgz9zJOu84lhyBJehTLdS34rQLV9YuRHTj3jyrWPdNsgkQ26u1ugA==
+  dependencies:
+    "@lumino/algorithm" "^1.9.1"
+    "@lumino/commands" "^1.20.0"
+    "@lumino/coreutils" "^1.12.0"
+    "@lumino/disposable" "^1.10.1"
+    "@lumino/domutils" "^1.8.1"
+    "@lumino/dragdrop" "^1.14.0"
+    "@lumino/keyboard" "^1.8.1"
+    "@lumino/messaging" "^1.10.1"
+    "@lumino/properties" "^1.8.1"
+    "@lumino/signaling" "^1.10.1"
+    "@lumino/virtualdom" "^1.14.1"
+
+"@mapbox/node-pre-gyp@^1.0.0":
+  version "1.0.9"
+  resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.9.tgz#09a8781a3a036151cdebbe8719d6f8b25d4058bc"
+  integrity sha512-aDF3S3rK9Q2gey/WAttUlISduDItz5BU3306M9Eyv6/oS40aMprnopshtlKTykxRNIBEZuRMaZAnbrQ4QtKGyw==
+  dependencies:
+    detect-libc "^2.0.0"
+    https-proxy-agent "^5.0.0"
+    make-dir "^3.1.0"
+    node-fetch "^2.6.7"
+    nopt "^5.0.0"
+    npmlog "^5.0.1"
+    rimraf "^3.0.2"
+    semver "^7.3.5"
+    tar "^6.1.11"
+
+"@nodelib/fs.scandir@2.1.5":
+  version "2.1.5"
+  resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
+  integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==
+  dependencies:
+    "@nodelib/fs.stat" "2.0.5"
+    run-parallel "^1.1.9"
+
+"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2":
+  version "2.0.5"
+  resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b"
+  integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==
+
+"@nodelib/fs.walk@^1.2.3":
+  version "1.2.8"
+  resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a"
+  integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==
+  dependencies:
+    "@nodelib/fs.scandir" "2.1.5"
+    fastq "^1.6.0"
+
+"@npmcli/fs@^1.0.0":
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/@npmcli/fs/-/fs-1.1.1.tgz#72f719fe935e687c56a4faecf3c03d06ba593257"
+  integrity sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==
+  dependencies:
+    "@gar/promisify" "^1.0.1"
+    semver "^7.3.5"
+
+"@npmcli/move-file@^1.0.1":
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/@npmcli/move-file/-/move-file-1.1.2.tgz#1a82c3e372f7cae9253eb66d72543d6b8685c674"
+  integrity sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==
+  dependencies:
+    mkdirp "^1.0.4"
+    rimraf "^3.0.2"
+
+"@playwright/test@^1.16.2", "@playwright/test@^1.21.1":
+  version "1.22.2"
+  resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.22.2.tgz#b848f25f8918140c2d0bae8e9227a40198f2dd4a"
+  integrity sha512-cCl96BEBGPtptFz7C2FOSN3PrTnJ3rPpENe+gYCMx4GNNDlN4tmo2D89y13feGKTMMAIVrXfSQ/UmaQKLy1XLA==
+  dependencies:
+    "@types/node" "*"
+    playwright-core "1.22.2"
+
+"@rjsf/core@^3.1.0":
+  version "3.2.1"
+  resolved "https://registry.yarnpkg.com/@rjsf/core/-/core-3.2.1.tgz#8a7b24c9a6f01f0ecb093fdfc777172c12b1b009"
+  integrity sha512-dk8ihvxFbcuIwU7G+HiJbFgwyIvaumPt5g5zfnuC26mwTUPlaDGFXKK2yITp8tJ3+hcwS5zEXtAN9wUkfuM4jA==
+  dependencies:
+    "@types/json-schema" "^7.0.7"
+    ajv "^6.7.0"
+    core-js-pure "^3.6.5"
+    json-schema-merge-allof "^0.6.0"
+    jsonpointer "^5.0.0"
+    lodash "^4.17.15"
+    nanoid "^3.1.23"
+    prop-types "^15.7.2"
+    react-is "^16.9.0"
+
+"@sindresorhus/is@^0.14.0":
+  version "0.14.0"
+  resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea"
+  integrity sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==
+
+"@stdlib/array@^0.0.x":
+  version "0.0.12"
+  resolved "https://registry.yarnpkg.com/@stdlib/array/-/array-0.0.12.tgz#12f40ab95bb36d424cdad991f29fc3cb491ee29e"
+  integrity sha512-nDksiuvRC1dSTHrf5yOGQmlRwAzSKV8MdFQwFSvLbZGGhi5Y4hExqea5HloLgNVouVs8lnAFi2oubSM4Mc7YAg==
+  dependencies:
+    "@stdlib/assert" "^0.0.x"
+    "@stdlib/blas" "^0.0.x"
+    "@stdlib/complex" "^0.0.x"
+    "@stdlib/constants" "^0.0.x"
+    "@stdlib/math" "^0.0.x"
+    "@stdlib/symbol" "^0.0.x"
+    "@stdlib/types" "^0.0.x"
+    "@stdlib/utils" "^0.0.x"
+
+"@stdlib/assert@^0.0.x":
+  version "0.0.12"
+  resolved "https://registry.yarnpkg.com/@stdlib/assert/-/assert-0.0.12.tgz#1648c9016e5041291f55a6464abcc4069c5103ce"
+  integrity sha512-38FxFf+ZoQZbdc+m09UsWtaCmzd/2e7im0JOaaFYE7icmRfm+4KiE9BRvBT4tIn7ioLB2f9PsBicKjIsf+tY1w==
+  dependencies:
+    "@stdlib/array" "^0.0.x"
+    "@stdlib/cli" "^0.0.x"
+    "@stdlib/complex" "^0.0.x"
+    "@stdlib/constants" "^0.0.x"
+    "@stdlib/fs" "^0.0.x"
+    "@stdlib/math" "^0.0.x"
+    "@stdlib/ndarray" "^0.0.x"
+    "@stdlib/number" "^0.0.x"
+    "@stdlib/os" "^0.0.x"
+    "@stdlib/process" "^0.0.x"
+    "@stdlib/regexp" "^0.0.x"
+    "@stdlib/streams" "^0.0.x"
+    "@stdlib/string" "^0.0.x"
+    "@stdlib/symbol" "^0.0.x"
+    "@stdlib/types" "^0.0.x"
+    "@stdlib/utils" "^0.0.x"
+
+"@stdlib/bigint@^0.0.x":
+  version "0.0.11"
+  resolved "https://registry.yarnpkg.com/@stdlib/bigint/-/bigint-0.0.11.tgz#c416a1d727001c55f4897e6424124199d638f2fd"
+  integrity sha512-uz0aYDLABAYyqxaCSHYbUt0yPkXYUCR7TrVvHN+UUD3i8FZ02ZKcLO+faKisDyxKEoSFTNtn3Ro8Ir5ebOlVXQ==
+  dependencies:
+    "@stdlib/utils" "^0.0.x"
+
+"@stdlib/blas@^0.0.x":
+  version "0.0.12"
+  resolved "https://registry.yarnpkg.com/@stdlib/blas/-/blas-0.0.12.tgz#7e93e42b4621fc6903bf63264f045047333536c2"
+  integrity sha512-nWY749bWceuoWQ7gz977blCwR7lyQ/rsIXVO4b600h+NFpeA2i/ea7MYC680utIbeu2cnDWHdglBPoK535VAzA==
+  dependencies:
+    "@stdlib/array" "^0.0.x"
+    "@stdlib/assert" "^0.0.x"
+    "@stdlib/math" "^0.0.x"
+    "@stdlib/number" "^0.0.x"
+    "@stdlib/types" "^0.0.x"
+    "@stdlib/utils" "^0.0.x"
+
+"@stdlib/buffer@^0.0.x":
+  version "0.0.11"
+  resolved "https://registry.yarnpkg.com/@stdlib/buffer/-/buffer-0.0.11.tgz#6137b00845e6c905181cc7ebfae9f7e47c01b0ce"
+  integrity sha512-Jeie5eDDa1tVuRcuU+cBXI/oOXSmMxUUccZpqXzgYe0IO8QSNtNxv9mUTzJk/m5wH+lmLoDvNxzPpOH9TODjJg==
+  dependencies:
+    "@stdlib/array" "^0.0.x"
+    "@stdlib/assert" "^0.0.x"
+    "@stdlib/process" "^0.0.x"
+    "@stdlib/types" "^0.0.x"
+    "@stdlib/utils" "^0.0.x"
+
+"@stdlib/cli@^0.0.x":
+  version "0.0.10"
+  resolved "https://registry.yarnpkg.com/@stdlib/cli/-/cli-0.0.10.tgz#28e2fbe6865d7f5cd15b7dc5846c99bd3b91674f"
+  integrity sha512-OITGaxG46kwK799+NuOd/+ccosJ9koVuQBC610DDJv0ZJf8mD7sbjGXrmue9C4EOh8MP7Vm/6HN14BojX8oTCg==
+  dependencies:
+    "@stdlib/utils" "^0.0.x"
+    minimist "^1.2.0"
+
+"@stdlib/complex@^0.0.x":
+  version "0.0.12"
+  resolved "https://registry.yarnpkg.com/@stdlib/complex/-/complex-0.0.12.tgz#3afbc190cd0a9b37fc7c6e508c3aa9fda9106944"
+  integrity sha512-UbZBdaUxT2G+lsTIrVlRZwx2IRY6GXnVILggeejsIVxHSuK+oTyapfetcAv0FJFLP+Rrr+ZzrN4b9G3hBw6NHA==
+  dependencies:
+    "@stdlib/array" "^0.0.x"
+    "@stdlib/assert" "^0.0.x"
+    "@stdlib/types" "^0.0.x"
+    "@stdlib/utils" "^0.0.x"
+
+"@stdlib/constants@^0.0.x":
+  version "0.0.11"
+  resolved "https://registry.yarnpkg.com/@stdlib/constants/-/constants-0.0.11.tgz#78cd56d6c2982b30264843c3d75bde7125e90cd2"
+  integrity sha512-cWKy0L9hXHUQTvFzdPkTvZnn/5Pjv7H4UwY0WC1rLt+A5CxFDJKjvnIi9ypSzJS3CAiGl1ZaHCdadoqXhNdkUg==
+  dependencies:
+    "@stdlib/array" "^0.0.x"
+    "@stdlib/assert" "^0.0.x"
+    "@stdlib/number" "^0.0.x"
+    "@stdlib/utils" "^0.0.x"
+
+"@stdlib/fs@^0.0.x":
+  version "0.0.12"
+  resolved "https://registry.yarnpkg.com/@stdlib/fs/-/fs-0.0.12.tgz#662365fd5846a51f075724b4f2888ae88441b70d"
+  integrity sha512-zcDLbt39EEM3M3wJW6luChS53B8T+TMJkjs2526UpKJ71O0/0adR57cI7PfCpkMd33d05uM7GM+leEj4eks4Cw==
+  dependencies:
+    "@stdlib/array" "^0.0.x"
+    "@stdlib/assert" "^0.0.x"
+    "@stdlib/cli" "^0.0.x"
+    "@stdlib/math" "^0.0.x"
+    "@stdlib/process" "^0.0.x"
+    "@stdlib/string" "^0.0.x"
+    "@stdlib/utils" "^0.0.x"
+    debug "^2.6.9"
+
+"@stdlib/math@^0.0.x":
+  version "0.0.11"
+  resolved "https://registry.yarnpkg.com/@stdlib/math/-/math-0.0.11.tgz#eb6638bc03a20fbd6727dd5b977ee0170bda4649"
+  integrity sha512-qI78sR1QqGjHj8k/aAqkZ51Su2fyBvaR/jMKQqcB/ML8bpYpf+QGlGvTty5Qdru/wpqds4kVFOVbWGcNFIV2+Q==
+  dependencies:
+    "@stdlib/assert" "^0.0.x"
+    "@stdlib/constants" "^0.0.x"
+    "@stdlib/ndarray" "^0.0.x"
+    "@stdlib/number" "^0.0.x"
+    "@stdlib/strided" "^0.0.x"
+    "@stdlib/symbol" "^0.0.x"
+    "@stdlib/types" "^0.0.x"
+    "@stdlib/utils" "^0.0.x"
+    debug "^2.6.9"
+
+"@stdlib/ndarray@^0.0.x":
+  version "0.0.13"
+  resolved "https://registry.yarnpkg.com/@stdlib/ndarray/-/ndarray-0.0.13.tgz#2e8fc645e10f56a645a0ab81598808c0e8f43b82"
+  integrity sha512-Z+U9KJP4U2HWrLtuAXSPvhNetAdqaNLMcliR6S/fz+VPlFDeymRK7omRFMgVQ+1zcAvIgKZGJxpLC3vjiPUYEw==
+  dependencies:
+    "@stdlib/array" "^0.0.x"
+    "@stdlib/assert" "^0.0.x"
+    "@stdlib/bigint" "^0.0.x"
+    "@stdlib/buffer" "^0.0.x"
+    "@stdlib/complex" "^0.0.x"
+    "@stdlib/constants" "^0.0.x"
+    "@stdlib/math" "^0.0.x"
+    "@stdlib/number" "^0.0.x"
+    "@stdlib/string" "^0.0.x"
+    "@stdlib/types" "^0.0.x"
+    "@stdlib/utils" "^0.0.x"
+
+"@stdlib/nlp@^0.0.x":
+  version "0.0.11"
+  resolved "https://registry.yarnpkg.com/@stdlib/nlp/-/nlp-0.0.11.tgz#532ec0f7267b8d639e4c20c6de864e8de8a09054"
+  integrity sha512-D9avYWANm0Db2W7RpzdSdi5GxRYALGAqUrNnRnnKIO6sMEfr/DvONoAbWruda4QyvSC+0MJNwcEn7+PHhRwYhw==
+  dependencies:
+    "@stdlib/array" "^0.0.x"
+    "@stdlib/assert" "^0.0.x"
+    "@stdlib/math" "^0.0.x"
+    "@stdlib/random" "^0.0.x"
+    "@stdlib/string" "^0.0.x"
+    "@stdlib/utils" "^0.0.x"
+
+"@stdlib/number@^0.0.x":
+  version "0.0.10"
+  resolved "https://registry.yarnpkg.com/@stdlib/number/-/number-0.0.10.tgz#4030ad8fc3fac19a9afb415c443cee6deea0e65c"
+  integrity sha512-RyfoP9MlnX4kccvg8qv7vYQPbLdzfS1Mnp/prGOoWhvMG3pyBwFAan34kwFb5IS/zHC3W5EmrgXCV2QWyLg/Kg==
+  dependencies:
+    "@stdlib/array" "^0.0.x"
+    "@stdlib/assert" "^0.0.x"
+    "@stdlib/constants" "^0.0.x"
+    "@stdlib/math" "^0.0.x"
+    "@stdlib/os" "^0.0.x"
+    "@stdlib/string" "^0.0.x"
+    "@stdlib/types" "^0.0.x"
+    "@stdlib/utils" "^0.0.x"
+
+"@stdlib/os@^0.0.x":
+  version "0.0.12"
+  resolved "https://registry.yarnpkg.com/@stdlib/os/-/os-0.0.12.tgz#08bbf013c62a7153099fa9cbac086ca1349a4677"
+  integrity sha512-O7lklZ/9XEzoCmYvzjPh7jrFWkbpOSHGI71ve3dkSvBy5tyiSL3TtivfKsIC+9ZxuEJZ3d3lIjc9e+yz4HVbqQ==
+  dependencies:
+    "@stdlib/assert" "^0.0.x"
+    "@stdlib/cli" "^0.0.x"
+    "@stdlib/fs" "^0.0.x"
+    "@stdlib/process" "^0.0.x"
+    "@stdlib/utils" "^0.0.x"
+
+"@stdlib/process@^0.0.x":
+  version "0.0.12"
+  resolved "https://registry.yarnpkg.com/@stdlib/process/-/process-0.0.12.tgz#123325079d89a32f4212f72fb694f8fe3614cf18"
+  integrity sha512-P0X0TMvkissBE1Wr877Avi2/AxmP7X5Toa6GatHbpJdDg6jQmN4SgPd+NZNp98YtZUyk478c8XSIzMr1krQ20g==
+  dependencies:
+    "@stdlib/assert" "^0.0.x"
+    "@stdlib/buffer" "^0.0.x"
+    "@stdlib/cli" "^0.0.x"
+    "@stdlib/fs" "^0.0.x"
+    "@stdlib/streams" "^0.0.x"
+    "@stdlib/string" "^0.0.x"
+    "@stdlib/utils" "^0.0.x"
+
+"@stdlib/random@^0.0.x":
+  version "0.0.12"
+  resolved "https://registry.yarnpkg.com/@stdlib/random/-/random-0.0.12.tgz#e819c3abd602ed5559ba800dba751e49c633ff85"
+  integrity sha512-c5yND4Ahnm9Jx0I+jsKhn4Yrz10D53ALSrIe3PG1qIz3kNFcIPnmvCuNGd+3V4ch4Mbrez55Y8z/ZC5RJh4vJQ==
+  dependencies:
+    "@stdlib/array" "^0.0.x"
+    "@stdlib/assert" "^0.0.x"
+    "@stdlib/blas" "^0.0.x"
+    "@stdlib/buffer" "^0.0.x"
+    "@stdlib/cli" "^0.0.x"
+    "@stdlib/constants" "^0.0.x"
+    "@stdlib/fs" "^0.0.x"
+    "@stdlib/math" "^0.0.x"
+    "@stdlib/process" "^0.0.x"
+    "@stdlib/stats" "^0.0.x"
+    "@stdlib/streams" "^0.0.x"
+    "@stdlib/symbol" "^0.0.x"
+    "@stdlib/types" "^0.0.x"
+    "@stdlib/utils" "^0.0.x"
+    debug "^2.6.9"
+    readable-stream "^2.1.4"
+
+"@stdlib/regexp@^0.0.x":
+  version "0.0.13"
+  resolved "https://registry.yarnpkg.com/@stdlib/regexp/-/regexp-0.0.13.tgz#80b98361dc7a441b47bc3fa964bb0c826759e971"
+  integrity sha512-3JT5ZIoq/1nXY+dY+QtkU8/m7oWDeekyItEEXMx9c/AOf0ph8fmvTUGMDNfUq0RetcznFe3b66kFz6Zt4XHviA==
+  dependencies:
+    "@stdlib/assert" "^0.0.x"
+    "@stdlib/utils" "^0.0.x"
+
+"@stdlib/stats@^0.0.13", "@stdlib/stats@^0.0.x":
+  version "0.0.13"
+  resolved "https://registry.yarnpkg.com/@stdlib/stats/-/stats-0.0.13.tgz#87c973f385379d794707c7b5196a173dba8b07e1"
+  integrity sha512-hm+t32dKbx/L7+7WlQ1o4NDEzV0J4QSnwFBCsIMIAO8+VPxTZ4FxyNERl4oKlS3hZZe4AVKjoOVhBDtgEWrS4g==
+  dependencies:
+    "@stdlib/array" "^0.0.x"
+    "@stdlib/assert" "^0.0.x"
+    "@stdlib/blas" "^0.0.x"
+    "@stdlib/constants" "^0.0.x"
+    "@stdlib/math" "^0.0.x"
+    "@stdlib/ndarray" "^0.0.x"
+    "@stdlib/random" "^0.0.x"
+    "@stdlib/string" "^0.0.x"
+    "@stdlib/symbol" "^0.0.x"
+    "@stdlib/types" "^0.0.x"
+    "@stdlib/utils" "^0.0.x"
+
+"@stdlib/streams@^0.0.x":
+  version "0.0.12"
+  resolved "https://registry.yarnpkg.com/@stdlib/streams/-/streams-0.0.12.tgz#07f5ceae5852590afad8e1cb7ce94174becc8739"
+  integrity sha512-YLUlXwjJNknHp92IkJUdvn5jEQjDckpawKhDLLCoxyh3h5V+w/8+61SH7TMTfKx5lBxKJ8vvtchZh90mIJOAjQ==
+  dependencies:
+    "@stdlib/assert" "^0.0.x"
+    "@stdlib/buffer" "^0.0.x"
+    "@stdlib/cli" "^0.0.x"
+    "@stdlib/fs" "^0.0.x"
+    "@stdlib/types" "^0.0.x"
+    "@stdlib/utils" "^0.0.x"
+    debug "^2.6.9"
+    readable-stream "^2.1.4"
+
+"@stdlib/strided@^0.0.x":
+  version "0.0.12"
+  resolved "https://registry.yarnpkg.com/@stdlib/strided/-/strided-0.0.12.tgz#86ac48e660cb7f64a45cf07e80cbbfe58be21ae1"
+  integrity sha512-1NINP+Y7IJht34iri/bYLY7TVxrip51f6Z3qWxGHUCH33kvk5H5QqV+RsmFEGbbyoGtdeHrT2O+xA+7R2e3SNg==
+  dependencies:
+    "@stdlib/assert" "^0.0.x"
+    "@stdlib/math" "^0.0.x"
+    "@stdlib/ndarray" "^0.0.x"
+    "@stdlib/types" "^0.0.x"
+    "@stdlib/utils" "^0.0.x"
+
+"@stdlib/string@^0.0.x":
+  version "0.0.13"
+  resolved "https://registry.yarnpkg.com/@stdlib/string/-/string-0.0.13.tgz#37457ca49e8d1dff0e523c68f5673c655c79eb2d"
+  integrity sha512-nGMHi7Qk9LBW0+Y+e3pSePQEBqyWH7+7DjFR1APcbsYccJE0p4aCaQdhPhx9Tp7j3uRGBmqPFek8wpcvIuC+CQ==
+  dependencies:
+    "@stdlib/assert" "^0.0.x"
+    "@stdlib/cli" "^0.0.x"
+    "@stdlib/constants" "^0.0.x"
+    "@stdlib/fs" "^0.0.x"
+    "@stdlib/math" "^0.0.x"
+    "@stdlib/nlp" "^0.0.x"
+    "@stdlib/process" "^0.0.x"
+    "@stdlib/regexp" "^0.0.x"
+    "@stdlib/streams" "^0.0.x"
+    "@stdlib/types" "^0.0.x"
+    "@stdlib/utils" "^0.0.x"
+
+"@stdlib/symbol@^0.0.x":
+  version "0.0.12"
+  resolved "https://registry.yarnpkg.com/@stdlib/symbol/-/symbol-0.0.12.tgz#b9f396b0bf269c2985bb7fe99810a8e26d7288c3"
+  integrity sha512-2IDhpzWVGeLHgsvIsX12RXvf78r7xBkc4QLoRUv3k7Cp61BisR1Ym1p0Tq9PbxT8fknlvLToh9n5RpmESi2d4w==
+  dependencies:
+    "@stdlib/assert" "^0.0.x"
+    "@stdlib/utils" "^0.0.x"
+
+"@stdlib/time@^0.0.x":
+  version "0.0.14"
+  resolved "https://registry.yarnpkg.com/@stdlib/time/-/time-0.0.14.tgz#ea6daa438b1d3b019b99f5091117ee4bcef55d60"
+  integrity sha512-1gMFCQTabMVIgww+k4g8HHHIhyy1tIlvwT8mC0BHW7Q7TzDAgobwL0bvor+lwvCb5LlDAvNQEpaRgVT99QWGeQ==
+  dependencies:
+    "@stdlib/assert" "^0.0.x"
+    "@stdlib/cli" "^0.0.x"
+    "@stdlib/constants" "^0.0.x"
+    "@stdlib/fs" "^0.0.x"
+    "@stdlib/math" "^0.0.x"
+    "@stdlib/string" "^0.0.x"
+    "@stdlib/utils" "^0.0.x"
+
+"@stdlib/types@^0.0.x":
+  version "0.0.14"
+  resolved "https://registry.yarnpkg.com/@stdlib/types/-/types-0.0.14.tgz#02d3aab7a9bfaeb86e34ab749772ea22f7b2f7e0"
+  integrity sha512-AP3EI9/il/xkwUazcoY+SbjtxHRrheXgSbWZdEGD+rWpEgj6n2i63hp6hTOpAB5NipE0tJwinQlDGOuQ1lCaCw==
+
+"@stdlib/utils@^0.0.x":
+  version "0.0.12"
+  resolved "https://registry.yarnpkg.com/@stdlib/utils/-/utils-0.0.12.tgz#670de5a7b253f04f11a4cba38f790e82393bcb46"
+  integrity sha512-+JhFpl6l7RSq/xGnbWRQ5dAL90h9ONj8MViqlb7teBZFtePZLMwoRA1wssypFcJ8SFMRWQn7lPmpYVUkGwRSOg==
+  dependencies:
+    "@stdlib/array" "^0.0.x"
+    "@stdlib/assert" "^0.0.x"
+    "@stdlib/blas" "^0.0.x"
+    "@stdlib/buffer" "^0.0.x"
+    "@stdlib/cli" "^0.0.x"
+    "@stdlib/constants" "^0.0.x"
+    "@stdlib/fs" "^0.0.x"
+    "@stdlib/math" "^0.0.x"
+    "@stdlib/os" "^0.0.x"
+    "@stdlib/process" "^0.0.x"
+    "@stdlib/random" "^0.0.x"
+    "@stdlib/regexp" "^0.0.x"
+    "@stdlib/streams" "^0.0.x"
+    "@stdlib/string" "^0.0.x"
+    "@stdlib/symbol" "^0.0.x"
+    "@stdlib/time" "^0.0.x"
+    "@stdlib/types" "^0.0.x"
+    debug "^2.6.9"
+
+"@szmarczak/http-timer@^1.1.2":
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-1.1.2.tgz#b1665e2c461a2cd92f4c1bbf50d5454de0d4b421"
+  integrity sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==
+  dependencies:
+    defer-to-connect "^1.0.1"
+
+"@types/clone@~2.1.1":
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/@types/clone/-/clone-2.1.1.tgz#9b880d0ce9b1f209b5e0bd6d9caa38209db34024"
+  integrity sha512-BZIU34bSYye0j/BFcPraiDZ5ka6MJADjcDVELGf7glr9K+iE8NYVjFslJFVWzskSxkLLyCrSPScE82/UUoBSvg==
+
+"@types/dom4@^2.0.1":
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/@types/dom4/-/dom4-2.0.2.tgz#6495303f049689ce936ed328a3e5ede9c51408ee"
+  integrity sha512-Rt4IC1T7xkCWa0OG1oSsPa0iqnxlDeQqKXZAHrQGLb7wFGncWm85MaxKUjAGejOrUynOgWlFi4c6S6IyJwoK4g==
+
+"@types/eslint-scope@^3.7.3":
+  version "3.7.3"
+  resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.3.tgz#125b88504b61e3c8bc6f870882003253005c3224"
+  integrity sha512-PB3ldyrcnAicT35TWPs5IcwKD8S333HMaa2VVv4+wdvebJkjWuW/xESoB8IwRcog8HYVYamb1g/R31Qv5Bx03g==
+  dependencies:
+    "@types/eslint" "*"
+    "@types/estree" "*"
+
+"@types/eslint@*":
+  version "8.4.3"
+  resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.4.3.tgz#5c92815a3838b1985c90034cd85f26f59d9d0ece"
+  integrity sha512-YP1S7YJRMPs+7KZKDb9G63n8YejIwW9BALq7a5j2+H4yl6iOv9CB29edho+cuFRrvmJbbaH2yiVChKLJVysDGw==
+  dependencies:
+    "@types/estree" "*"
+    "@types/json-schema" "*"
+
+"@types/estree@*", "@types/estree@^0.0.51":
+  version "0.0.51"
+  resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.51.tgz#cfd70924a25a3fd32b218e5e420e6897e1ac4f40"
+  integrity sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==
+
+"@types/estree@^0.0.50":
+  version "0.0.50"
+  resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.50.tgz#1e0caa9364d3fccd2931c3ed96fdbeaa5d4cca83"
+  integrity sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw==
+
+"@types/glob@^7.1.1":
+  version "7.2.0"
+  resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.2.0.tgz#bc1b5bf3aa92f25bd5dd39f35c57361bdce5b2eb"
+  integrity sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==
+  dependencies:
+    "@types/minimatch" "*"
+    "@types/node" "*"
+
+"@types/json-schema@*", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.7", "@types/json-schema@^7.0.8":
+  version "7.0.11"
+  resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3"
+  integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==
+
+"@types/minimatch@*":
+  version "3.0.5"
+  resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.5.tgz#1001cc5e6a3704b83c236027e77f2f58ea010f40"
+  integrity sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==
+
+"@types/minimist@^1.2.0":
+  version "1.2.2"
+  resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.2.tgz#ee771e2ba4b3dc5b372935d549fd9617bf345b8c"
+  integrity sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==
+
+"@types/node@*":
+  version "18.0.0"
+  resolved "https://registry.yarnpkg.com/@types/node/-/node-18.0.0.tgz#67c7b724e1bcdd7a8821ce0d5ee184d3b4dd525a"
+  integrity sha512-cHlGmko4gWLVI27cGJntjs/Sj8th9aYwplmZFwmmgYQQvL5NUsgVJG7OddLvNfLqYS31KFN0s3qlaD9qCaxACA==
+
+"@types/normalize-package-data@^2.4.0":
+  version "2.4.1"
+  resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz#d3357479a0fdfdd5907fe67e17e0a85c906e1301"
+  integrity sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==
+
+"@types/parse-json@^4.0.0":
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
+  integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==
+
+"@types/prop-types@*":
+  version "15.7.5"
+  resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf"
+  integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==
+
+"@types/react@^17.0.0":
+  version "17.0.45"
+  resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.45.tgz#9b3d5b661fd26365fefef0e766a1c6c30ccf7b3f"
+  integrity sha512-YfhQ22Lah2e3CHPsb93tRwIGNiSwkuz1/blk4e6QrWS0jQzCSNbGLtOEYhPg02W0yGTTmpajp7dCTbBAMN3qsg==
+  dependencies:
+    "@types/prop-types" "*"
+    "@types/scheduler" "*"
+    csstype "^3.0.2"
+
+"@types/scheduler@*":
+  version "0.16.2"
+  resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39"
+  integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==
+
+"@types/source-list-map@*":
+  version "0.1.2"
+  resolved "https://registry.yarnpkg.com/@types/source-list-map/-/source-list-map-0.1.2.tgz#0078836063ffaf17412349bba364087e0ac02ec9"
+  integrity sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==
+
+"@types/webpack-sources@^0.1.5":
+  version "0.1.9"
+  resolved "https://registry.yarnpkg.com/@types/webpack-sources/-/webpack-sources-0.1.9.tgz#da69b06eb34f6432e6658acb5a6893c55d983920"
+  integrity sha512-bvzMnzqoK16PQIC8AYHNdW45eREJQMd6WG/msQWX5V2+vZmODCOPb4TJcbgRljTZZTwTM4wUMcsI8FftNA7new==
+  dependencies:
+    "@types/node" "*"
+    "@types/source-list-map" "*"
+    source-map "^0.6.1"
+
+"@typescript-eslint/eslint-plugin@^4.8.1":
+  version "4.33.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.33.0.tgz#c24dc7c8069c7706bc40d99f6fa87edcb2005276"
+  integrity sha512-aINiAxGVdOl1eJyVjaWn/YcVAq4Gi/Yo35qHGCnqbWVz61g39D0h23veY/MA0rFFGfxK7TySg2uwDeNv+JgVpg==
+  dependencies:
+    "@typescript-eslint/experimental-utils" "4.33.0"
+    "@typescript-eslint/scope-manager" "4.33.0"
+    debug "^4.3.1"
+    functional-red-black-tree "^1.0.1"
+    ignore "^5.1.8"
+    regexpp "^3.1.0"
+    semver "^7.3.5"
+    tsutils "^3.21.0"
+
+"@typescript-eslint/experimental-utils@4.33.0":
+  version "4.33.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.33.0.tgz#6f2a786a4209fa2222989e9380b5331b2810f7fd"
+  integrity sha512-zeQjOoES5JFjTnAhI5QY7ZviczMzDptls15GFsI6jyUOq0kOf9+WonkhtlIhh0RgHRnqj5gdNxW5j1EvAyYg6Q==
+  dependencies:
+    "@types/json-schema" "^7.0.7"
+    "@typescript-eslint/scope-manager" "4.33.0"
+    "@typescript-eslint/types" "4.33.0"
+    "@typescript-eslint/typescript-estree" "4.33.0"
+    eslint-scope "^5.1.1"
+    eslint-utils "^3.0.0"
+
+"@typescript-eslint/parser@^4.8.1":
+  version "4.33.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.33.0.tgz#dfe797570d9694e560528d18eecad86c8c744899"
+  integrity sha512-ZohdsbXadjGBSK0/r+d87X0SBmKzOq4/S5nzK6SBgJspFo9/CUDJ7hjayuze+JK7CZQLDMroqytp7pOcFKTxZA==
+  dependencies:
+    "@typescript-eslint/scope-manager" "4.33.0"
+    "@typescript-eslint/types" "4.33.0"
+    "@typescript-eslint/typescript-estree" "4.33.0"
+    debug "^4.3.1"
+
+"@typescript-eslint/scope-manager@4.33.0":
+  version "4.33.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.33.0.tgz#d38e49280d983e8772e29121cf8c6e9221f280a3"
+  integrity sha512-5IfJHpgTsTZuONKbODctL4kKuQje/bzBRkwHE8UOZ4f89Zeddg+EGZs8PD8NcN4LdM3ygHWYB3ukPAYjvl/qbQ==
+  dependencies:
+    "@typescript-eslint/types" "4.33.0"
+    "@typescript-eslint/visitor-keys" "4.33.0"
+
+"@typescript-eslint/types@4.33.0":
+  version "4.33.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.33.0.tgz#a1e59036a3b53ae8430ceebf2a919dc7f9af6d72"
+  integrity sha512-zKp7CjQzLQImXEpLt2BUw1tvOMPfNoTAfb8l51evhYbOEEzdWyQNmHWWGPR6hwKJDAi+1VXSBmnhL9kyVTTOuQ==
+
+"@typescript-eslint/typescript-estree@4.33.0":
+  version "4.33.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.33.0.tgz#0dfb51c2908f68c5c08d82aefeaf166a17c24609"
+  integrity sha512-rkWRY1MPFzjwnEVHsxGemDzqqddw2QbTJlICPD9p9I9LfsO8fdmfQPOX3uKfUaGRDFJbfrtm/sXhVXN4E+bzCA==
+  dependencies:
+    "@typescript-eslint/types" "4.33.0"
+    "@typescript-eslint/visitor-keys" "4.33.0"
+    debug "^4.3.1"
+    globby "^11.0.3"
+    is-glob "^4.0.1"
+    semver "^7.3.5"
+    tsutils "^3.21.0"
+
+"@typescript-eslint/visitor-keys@4.33.0":
+  version "4.33.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.33.0.tgz#2a22f77a41604289b7a186586e9ec48ca92ef1dd"
+  integrity sha512-uqi/2aSz9g2ftcHWf8uLPJA70rUv6yuMW5Bohw+bwcuzaxQIHaKFZCKGoGXIrc9vkTJ3+0txM73K0Hq3d5wgIg==
+  dependencies:
+    "@typescript-eslint/types" "4.33.0"
+    eslint-visitor-keys "^2.0.0"
+
+"@verdaccio/commons-api@10.2.0":
+  version "10.2.0"
+  resolved "https://registry.yarnpkg.com/@verdaccio/commons-api/-/commons-api-10.2.0.tgz#3b684c31749837b0574375bb2e10644ecea9fcca"
+  integrity sha512-F/YZANu4DmpcEV0jronzI7v2fGVWkQ5Mwi+bVmV+ACJ+EzR0c9Jbhtbe5QyLUuzR97t8R5E/Xe53O0cc2LukdQ==
+  dependencies:
+    http-errors "2.0.0"
+    http-status-codes "2.2.0"
+
+"@verdaccio/file-locking@10.3.0":
+  version "10.3.0"
+  resolved "https://registry.yarnpkg.com/@verdaccio/file-locking/-/file-locking-10.3.0.tgz#a4342665c549163817c267bfa451e32ed3009767"
+  integrity sha512-FE5D5H4wy/nhgR/d2J5e1Na9kScj2wMjlLPBHz7XF4XZAVSRdm45+kL3ZmrfA6b2HTADP/uH7H05/cnAYW8bhw==
+  dependencies:
+    lockfile "1.0.4"
+
+"@verdaccio/local-storage@10.3.0":
+  version "10.3.0"
+  resolved "https://registry.yarnpkg.com/@verdaccio/local-storage/-/local-storage-10.3.0.tgz#37ad9c200d7672c698de9872a0ff7a7c43c83f30"
+  integrity sha512-qpzYVqJ4NN9gYGkpuQ8N2IRRg6m8+dTvSZ69H/41rHEfeeC0LW3cZjQV7hZ8RfNNvY7hLtf1E+n6DKCGx/bSLg==
+  dependencies:
+    "@verdaccio/commons-api" "10.2.0"
+    "@verdaccio/file-locking" "10.3.0"
+    "@verdaccio/streams" "10.2.0"
+    async "3.2.3"
+    debug "4.3.4"
+    lodash "4.17.21"
+    lowdb "1.0.0"
+    mkdirp "1.0.4"
+
+"@verdaccio/readme@10.3.4":
+  version "10.3.4"
+  resolved "https://registry.yarnpkg.com/@verdaccio/readme/-/readme-10.3.4.tgz#35594d30cebb9624f29c51f0ddc380f301d6c5a4"
+  integrity sha512-E4SHDjVt7eJ3CwNNvkB3N0zV3Zza8i6yQf6+qE4AZsy1L18OaxXBFmp4O4HxxIahB3npVhip230FVVAWUZjK+w==
+  dependencies:
+    dompurify "2.3.8"
+    jsdom "15.2.1"
+    marked "4.0.16"
+
+"@verdaccio/streams@10.2.0":
+  version "10.2.0"
+  resolved "https://registry.yarnpkg.com/@verdaccio/streams/-/streams-10.2.0.tgz#e01d2bfdcfe8aa2389f31bc6b72a602628bd025b"
+  integrity sha512-FaIzCnDg0x0Js5kSQn1Le3YzDHl7XxrJ0QdIw5LrDUmLsH3VXNi4/NMlSHnw5RiTTMs4UbEf98V3RJRB8exqJA==
+
+"@verdaccio/ui-theme@6.0.0-6-next.24":
+  version "6.0.0-6-next.24"
+  resolved "https://registry.yarnpkg.com/@verdaccio/ui-theme/-/ui-theme-6.0.0-6-next.24.tgz#77e5405f2c7ee60153845deebca80347a771e8ef"
+  integrity sha512-tchic00TMWV9qm3EG1GmU7WLnzb29fGT51NJF8rmmNGc7V7tlpXSOE+WQ/dP99jaViIrZzh73Z03TpjQ3ZFd/A==
+
+"@webassemblyjs/ast@1.11.1":
+  version "1.11.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.1.tgz#2bfd767eae1a6996f432ff7e8d7fc75679c0b6a7"
+  integrity sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==
+  dependencies:
+    "@webassemblyjs/helper-numbers" "1.11.1"
+    "@webassemblyjs/helper-wasm-bytecode" "1.11.1"
+
+"@webassemblyjs/floating-point-hex-parser@1.11.1":
+  version "1.11.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz#f6c61a705f0fd7a6aecaa4e8198f23d9dc179e4f"
+  integrity sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ==
+
+"@webassemblyjs/helper-api-error@1.11.1":
+  version "1.11.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz#1a63192d8788e5c012800ba6a7a46c705288fd16"
+  integrity sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg==
+
+"@webassemblyjs/helper-buffer@1.11.1":
+  version "1.11.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz#832a900eb444884cde9a7cad467f81500f5e5ab5"
+  integrity sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA==
+
+"@webassemblyjs/helper-numbers@1.11.1":
+  version "1.11.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz#64d81da219fbbba1e3bd1bfc74f6e8c4e10a62ae"
+  integrity sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ==
+  dependencies:
+    "@webassemblyjs/floating-point-hex-parser" "1.11.1"
+    "@webassemblyjs/helper-api-error" "1.11.1"
+    "@xtuc/long" "4.2.2"
+
+"@webassemblyjs/helper-wasm-bytecode@1.11.1":
+  version "1.11.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz#f328241e41e7b199d0b20c18e88429c4433295e1"
+  integrity sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q==
+
+"@webassemblyjs/helper-wasm-section@1.11.1":
+  version "1.11.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz#21ee065a7b635f319e738f0dd73bfbda281c097a"
+  integrity sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg==
+  dependencies:
+    "@webassemblyjs/ast" "1.11.1"
+    "@webassemblyjs/helper-buffer" "1.11.1"
+    "@webassemblyjs/helper-wasm-bytecode" "1.11.1"
+    "@webassemblyjs/wasm-gen" "1.11.1"
+
+"@webassemblyjs/ieee754@1.11.1":
+  version "1.11.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz#963929e9bbd05709e7e12243a099180812992614"
+  integrity sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ==
+  dependencies:
+    "@xtuc/ieee754" "^1.2.0"
+
+"@webassemblyjs/leb128@1.11.1":
+  version "1.11.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.11.1.tgz#ce814b45574e93d76bae1fb2644ab9cdd9527aa5"
+  integrity sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw==
+  dependencies:
+    "@xtuc/long" "4.2.2"
+
+"@webassemblyjs/utf8@1.11.1":
+  version "1.11.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.1.tgz#d1f8b764369e7c6e6bae350e854dec9a59f0a3ff"
+  integrity sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ==
+
+"@webassemblyjs/wasm-edit@1.11.1":
+  version "1.11.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz#ad206ebf4bf95a058ce9880a8c092c5dec8193d6"
+  integrity sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA==
+  dependencies:
+    "@webassemblyjs/ast" "1.11.1"
+    "@webassemblyjs/helper-buffer" "1.11.1"
+    "@webassemblyjs/helper-wasm-bytecode" "1.11.1"
+    "@webassemblyjs/helper-wasm-section" "1.11.1"
+    "@webassemblyjs/wasm-gen" "1.11.1"
+    "@webassemblyjs/wasm-opt" "1.11.1"
+    "@webassemblyjs/wasm-parser" "1.11.1"
+    "@webassemblyjs/wast-printer" "1.11.1"
+
+"@webassemblyjs/wasm-gen@1.11.1":
+  version "1.11.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz#86c5ea304849759b7d88c47a32f4f039ae3c8f76"
+  integrity sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA==
+  dependencies:
+    "@webassemblyjs/ast" "1.11.1"
+    "@webassemblyjs/helper-wasm-bytecode" "1.11.1"
+    "@webassemblyjs/ieee754" "1.11.1"
+    "@webassemblyjs/leb128" "1.11.1"
+    "@webassemblyjs/utf8" "1.11.1"
+
+"@webassemblyjs/wasm-opt@1.11.1":
+  version "1.11.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz#657b4c2202f4cf3b345f8a4c6461c8c2418985f2"
+  integrity sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw==
+  dependencies:
+    "@webassemblyjs/ast" "1.11.1"
+    "@webassemblyjs/helper-buffer" "1.11.1"
+    "@webassemblyjs/wasm-gen" "1.11.1"
+    "@webassemblyjs/wasm-parser" "1.11.1"
+
+"@webassemblyjs/wasm-parser@1.11.1":
+  version "1.11.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz#86ca734534f417e9bd3c67c7a1c75d8be41fb199"
+  integrity sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA==
+  dependencies:
+    "@webassemblyjs/ast" "1.11.1"
+    "@webassemblyjs/helper-api-error" "1.11.1"
+    "@webassemblyjs/helper-wasm-bytecode" "1.11.1"
+    "@webassemblyjs/ieee754" "1.11.1"
+    "@webassemblyjs/leb128" "1.11.1"
+    "@webassemblyjs/utf8" "1.11.1"
+
+"@webassemblyjs/wast-printer@1.11.1":
+  version "1.11.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz#d0c73beda8eec5426f10ae8ef55cee5e7084c2f0"
+  integrity sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg==
+  dependencies:
+    "@webassemblyjs/ast" "1.11.1"
+    "@xtuc/long" "4.2.2"
+
+"@webpack-cli/configtest@^1.2.0":
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/@webpack-cli/configtest/-/configtest-1.2.0.tgz#7b20ce1c12533912c3b217ea68262365fa29a6f5"
+  integrity sha512-4FB8Tj6xyVkyqjj1OaTqCjXYULB9FMkqQ8yGrZjRDrYh0nOE+7Lhs45WioWQQMV+ceFlE368Ukhe6xdvJM9Egg==
+
+"@webpack-cli/info@^1.5.0":
+  version "1.5.0"
+  resolved "https://registry.yarnpkg.com/@webpack-cli/info/-/info-1.5.0.tgz#6c78c13c5874852d6e2dd17f08a41f3fe4c261b1"
+  integrity sha512-e8tSXZpw2hPl2uMJY6fsMswaok5FdlGNRTktvFk2sD8RjH0hE2+XistawJx1vmKteh4NmGmNUrp+Tb2w+udPcQ==
+  dependencies:
+    envinfo "^7.7.3"
+
+"@webpack-cli/serve@^1.7.0":
+  version "1.7.0"
+  resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-1.7.0.tgz#e1993689ac42d2b16e9194376cfb6753f6254db1"
+  integrity sha512-oxnCNGj88fL+xzV+dacXs44HcDwf1ovs3AuEzvP7mqXw7fQntqIhQ1BRmynh4qEKQSSSRSWVyXRjmTbZIX9V2Q==
+
+"@xtuc/ieee754@^1.2.0":
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790"
+  integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==
+
+"@xtuc/long@4.2.2":
+  version "4.2.2"
+  resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d"
+  integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==
+
+"@yarnpkg/lockfile@^1.1.0":
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz#e77a97fbd345b76d83245edcd17d393b1b41fb31"
+  integrity sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==
+
+JSONStream@1.3.5:
+  version "1.3.5"
+  resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.5.tgz#3208c1f08d3a4d99261ab64f92302bc15e111ca0"
+  integrity sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==
+  dependencies:
+    jsonparse "^1.2.0"
+    through ">=2.2.7 <3"
+
+abab@^2.0.0:
+  version "2.0.6"
+  resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291"
+  integrity sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==
+
+abbrev@1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
+  integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==
+
+abstract-leveldown@^6.2.1:
+  version "6.3.0"
+  resolved "https://registry.yarnpkg.com/abstract-leveldown/-/abstract-leveldown-6.3.0.tgz#d25221d1e6612f820c35963ba4bd739928f6026a"
+  integrity sha512-TU5nlYgta8YrBMNpc9FwQzRbiXsj49gsALsXadbGHt9CROPzX5fB0rWDR5mtdpOOKa5XqRFpbj1QroPAoPzVjQ==
+  dependencies:
+    buffer "^5.5.0"
+    immediate "^3.2.3"
+    level-concat-iterator "~2.0.0"
+    level-supports "~1.0.0"
+    xtend "~4.0.0"
+
+abstract-leveldown@~6.2.1, abstract-leveldown@~6.2.3:
+  version "6.2.3"
+  resolved "https://registry.yarnpkg.com/abstract-leveldown/-/abstract-leveldown-6.2.3.tgz#036543d87e3710f2528e47040bc3261b77a9a8eb"
+  integrity sha512-BsLm5vFMRUrrLeCcRc+G0t2qOaTzpoJQLOubq2XM72eNpjF5UdU5o/5NvlNhx95XHcAvcl8OMXr4mlg/fRgUXQ==
+  dependencies:
+    buffer "^5.5.0"
+    immediate "^3.2.3"
+    level-concat-iterator "~2.0.0"
+    level-supports "~1.0.0"
+    xtend "~4.0.0"
+
+accepts@~1.3.5, accepts@~1.3.8:
+  version "1.3.8"
+  resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e"
+  integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==
+  dependencies:
+    mime-types "~2.1.34"
+    negotiator "0.6.3"
+
+acorn-globals@^4.3.2:
+  version "4.3.4"
+  resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-4.3.4.tgz#9fa1926addc11c97308c4e66d7add0d40c3272e7"
+  integrity sha512-clfQEh21R+D0leSbUdWf3OcfqyaCSAQ8Ryq00bofSekfr9W8u1jyYZo6ir0xu9Gtcf7BjcHJpnbZH7JOCpP60A==
+  dependencies:
+    acorn "^6.0.1"
+    acorn-walk "^6.0.1"
+
+acorn-import-assertions@^1.7.6:
+  version "1.8.0"
+  resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz#ba2b5939ce62c238db6d93d81c9b111b29b855e9"
+  integrity sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==
+
+acorn-jsx@^5.3.1:
+  version "5.3.2"
+  resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
+  integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
+
+acorn-walk@^6.0.1:
+  version "6.2.0"
+  resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-6.2.0.tgz#123cb8f3b84c2171f1f7fb252615b1c78a6b1a8c"
+  integrity sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA==
+
+acorn@^6.0.1:
+  version "6.4.2"
+  resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.2.tgz#35866fd710528e92de10cf06016498e47e39e1e6"
+  integrity sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==
+
+acorn@^7.1.0, acorn@^7.4.0:
+  version "7.4.1"
+  resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa"
+  integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==
+
+acorn@^8.4.1, acorn@^8.5.0:
+  version "8.7.1"
+  resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.1.tgz#0197122c843d1bf6d0a5e83220a788f278f63c30"
+  integrity sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==
+
+agent-base@6:
+  version "6.0.2"
+  resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77"
+  integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==
+  dependencies:
+    debug "4"
+
+aggregate-error@^3.0.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a"
+  integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==
+  dependencies:
+    clean-stack "^2.0.0"
+    indent-string "^4.0.0"
+
+ajv-keywords@^3.5.2:
+  version "3.5.2"
+  resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d"
+  integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==
+
+ajv@^6.10.0, ajv@^6.12.3, ajv@^6.12.4, ajv@^6.12.5, ajv@^6.7.0:
+  version "6.12.6"
+  resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
+  integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
+  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"
+
+ajv@^8.0.1:
+  version "8.11.0"
+  resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.11.0.tgz#977e91dd96ca669f54a11e23e378e33b884a565f"
+  integrity sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==
+  dependencies:
+    fast-deep-equal "^3.1.1"
+    json-schema-traverse "^1.0.0"
+    require-from-string "^2.0.2"
+    uri-js "^4.2.2"
+
+ansi-colors@^4.1.1:
+  version "4.1.3"
+  resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b"
+  integrity sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==
+
+ansi-escapes@^4.2.1:
+  version "4.3.2"
+  resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e"
+  integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==
+  dependencies:
+    type-fest "^0.21.3"
+
+ansi-regex@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
+  integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
+
+ansi-styles@^3.2.1:
+  version "3.2.1"
+  resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
+  integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==
+  dependencies:
+    color-convert "^1.9.0"
+
+ansi-styles@^4.0.0, ansi-styles@^4.1.0:
+  version "4.3.0"
+  resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937"
+  integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==
+  dependencies:
+    color-convert "^2.0.1"
+
+apache-md5@1.1.7:
+  version "1.1.7"
+  resolved "https://registry.yarnpkg.com/apache-md5/-/apache-md5-1.1.7.tgz#dcef1802700cc231d60c5e08fd088f2f9b36375a"
+  integrity sha512-JtHjzZmJxtzfTSjsCyHgPR155HBe5WGyUyHTaEkfy46qhwCFKx1Epm6nAxgUG3WfUZP1dWhGqj9Z2NOBeZ+uBw==
+
+"aproba@^1.0.3 || ^2.0.0":
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc"
+  integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==
+
+are-we-there-yet@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz#372e0e7bd279d8e94c653aaa1f67200884bf3e1c"
+  integrity sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==
+  dependencies:
+    delegates "^1.0.0"
+    readable-stream "^3.6.0"
+
+argparse@^1.0.7:
+  version "1.0.10"
+  resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911"
+  integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==
+  dependencies:
+    sprintf-js "~1.0.2"
+
+argparse@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
+  integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
+
+array-equal@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93"
+  integrity sha512-H3LU5RLiSsGXPhN+Nipar0iR0IofH+8r89G2y1tBKxQ/agagKyAjhkAFDRBfodP2caPrNKHpAWNIM/c9yeL7uA==
+
+array-flat-polyfill@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/array-flat-polyfill/-/array-flat-polyfill-1.0.1.tgz#1e3a4255be619dfbffbfd1d635c1cf357cd034e7"
+  integrity sha512-hfJmKupmQN0lwi0xG6FQ5U8Rd97RnIERplymOv/qpq8AoNKPPAnxJadjFA23FNWm88wykh9HmpLJUUwUtNU/iw==
+
+array-flatten@1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
+  integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==
+
+array-union@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d"
+  integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==
+
+arrify@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d"
+  integrity sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==
+
+asn1@~0.2.3:
+  version "0.2.6"
+  resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.6.tgz#0d3a7bb6e64e02a90c0303b31f292868ea09a08d"
+  integrity sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==
+  dependencies:
+    safer-buffer "~2.1.0"
+
+assert-plus@1.0.0, assert-plus@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525"
+  integrity sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==
+
+astral-regex@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31"
+  integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==
+
+async-limiter@~1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd"
+  integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==
+
+async@3.2.3:
+  version "3.2.3"
+  resolved "https://registry.yarnpkg.com/async/-/async-3.2.3.tgz#ac53dafd3f4720ee9e8a160628f18ea91df196c9"
+  integrity sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==
+
+async@3.2.4:
+  version "3.2.4"
+  resolved "https://registry.yarnpkg.com/async/-/async-3.2.4.tgz#2d22e00f8cddeb5fde5dd33522b56d1cf569a81c"
+  integrity sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==
+
+async@^2.6.2:
+  version "2.6.4"
+  resolved "https://registry.yarnpkg.com/async/-/async-2.6.4.tgz#706b7ff6084664cd7eae713f6f965433b5504221"
+  integrity sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==
+  dependencies:
+    lodash "^4.17.14"
+
+asynckit@^0.4.0:
+  version "0.4.0"
+  resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
+  integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
+
+at-least-node@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2"
+  integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==
+
+atomic-sleep@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz#eb85b77a601fc932cfe432c5acd364a9e2c9075b"
+  integrity sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==
+
+aws-sign2@~0.7.0:
+  version "0.7.0"
+  resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"
+  integrity sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==
+
+aws4@^1.8.0:
+  version "1.11.0"
+  resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59"
+  integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==
+
+balanced-match@^1.0.0:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
+  integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
+
+balanced-match@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-2.0.0.tgz#dc70f920d78db8b858535795867bf48f820633d9"
+  integrity sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==
+
+base64-js@^1.3.1:
+  version "1.5.1"
+  resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
+  integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
+
+basic-auth@^1.0.3:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-1.1.0.tgz#45221ee429f7ee1e5035be3f51533f1cdfd29884"
+  integrity sha512-CtGuTyWf3ig+sgRyC7uP6DM3N+5ur/p8L+FPfsd+BbIfIs74TFfCajZTHnCw6K5dqM0bZEbRIqRy1fAdiUJhTA==
+
+bcrypt-pbkdf@^1.0.0:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e"
+  integrity sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==
+  dependencies:
+    tweetnacl "^0.14.3"
+
+bcryptjs@2.4.3:
+  version "2.4.3"
+  resolved "https://registry.yarnpkg.com/bcryptjs/-/bcryptjs-2.4.3.tgz#9ab5627b93e60621ff7cdac5da9733027df1d0cb"
+  integrity sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==
+
+big.js@^5.2.2:
+  version "5.2.2"
+  resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328"
+  integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==
+
+body-parser@1.20.0:
+  version "1.20.0"
+  resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.0.tgz#3de69bd89011c11573d7bfee6a64f11b6bd27cc5"
+  integrity sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg==
+  dependencies:
+    bytes "3.1.2"
+    content-type "~1.0.4"
+    debug "2.6.9"
+    depd "2.0.0"
+    destroy "1.2.0"
+    http-errors "2.0.0"
+    iconv-lite "0.4.24"
+    on-finished "2.4.1"
+    qs "6.10.3"
+    raw-body "2.5.1"
+    type-is "~1.6.18"
+    unpipe "1.0.0"
+
+bower@*:
+  version "1.8.14"
+  resolved "https://registry.yarnpkg.com/bower/-/bower-1.8.14.tgz#985722a3c1fcd35c93d4136ecbeafbeaaea74e86"
+  integrity sha512-8Rq058FD91q9Nwthyhw0la9fzpBz0iwZTrt51LWl+w+PnJgZk9J+5wp3nibsJcIUPglMYXr4NRBaR+TUj0OkBQ==
+
+brace-expansion@^1.1.7:
+  version "1.1.11"
+  resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
+  integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
+  dependencies:
+    balanced-match "^1.0.0"
+    concat-map "0.0.1"
+
+brace-expansion@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae"
+  integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==
+  dependencies:
+    balanced-match "^1.0.0"
+
+braces@^3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
+  integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
+  dependencies:
+    fill-range "^7.0.1"
+
+browser-process-hrtime@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626"
+  integrity sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==
+
+browserslist@^4.14.5:
+  version "4.20.4"
+  resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.20.4.tgz#98096c9042af689ee1e0271333dbc564b8ce4477"
+  integrity sha512-ok1d+1WpnU24XYN7oC3QWgTyMhY/avPJ/r9T00xxvUOIparA/gc+UPUMaod3i+G6s+nI2nUb9xZ5k794uIwShw==
+  dependencies:
+    caniuse-lite "^1.0.30001349"
+    electron-to-chromium "^1.4.147"
+    escalade "^3.1.1"
+    node-releases "^2.0.5"
+    picocolors "^1.0.0"
+
+buffer-equal-constant-time@1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819"
+  integrity sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==
+
+buffer-from@^1.0.0:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
+  integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==
+
+buffer@^5.5.0, buffer@^5.6.0:
+  version "5.7.1"
+  resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0"
+  integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==
+  dependencies:
+    base64-js "^1.3.1"
+    ieee754 "^1.1.13"
+
+bytes@3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048"
+  integrity sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==
+
+bytes@3.1.2:
+  version "3.1.2"
+  resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5"
+  integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==
+
+cacache@^15.0.5:
+  version "15.3.0"
+  resolved "https://registry.yarnpkg.com/cacache/-/cacache-15.3.0.tgz#dc85380fb2f556fe3dda4c719bfa0ec875a7f1eb"
+  integrity sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==
+  dependencies:
+    "@npmcli/fs" "^1.0.0"
+    "@npmcli/move-file" "^1.0.1"
+    chownr "^2.0.0"
+    fs-minipass "^2.0.0"
+    glob "^7.1.4"
+    infer-owner "^1.0.4"
+    lru-cache "^6.0.0"
+    minipass "^3.1.1"
+    minipass-collect "^1.0.2"
+    minipass-flush "^1.0.5"
+    minipass-pipeline "^1.2.2"
+    mkdirp "^1.0.3"
+    p-map "^4.0.0"
+    promise-inflight "^1.0.1"
+    rimraf "^3.0.2"
+    ssri "^8.0.1"
+    tar "^6.0.2"
+    unique-filename "^1.1.1"
+
+cacheable-request@^6.0.0:
+  version "6.1.0"
+  resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-6.1.0.tgz#20ffb8bd162ba4be11e9567d823db651052ca912"
+  integrity sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==
+  dependencies:
+    clone-response "^1.0.2"
+    get-stream "^5.1.0"
+    http-cache-semantics "^4.0.0"
+    keyv "^3.0.0"
+    lowercase-keys "^2.0.0"
+    normalize-url "^4.1.0"
+    responselike "^1.0.2"
+
+call-bind@^1.0.0, call-bind@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c"
+  integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==
+  dependencies:
+    function-bind "^1.1.1"
+    get-intrinsic "^1.0.2"
+
+callsites@^3.0.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
+  integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
+
+camelcase-keys@^6.2.2:
+  version "6.2.2"
+  resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-6.2.2.tgz#5e755d6ba51aa223ec7d3d52f25778210f9dc3c0"
+  integrity sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==
+  dependencies:
+    camelcase "^5.3.1"
+    map-obj "^4.0.0"
+    quick-lru "^4.0.1"
+
+camelcase@^5.3.1:
+  version "5.3.1"
+  resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
+  integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
+
+caniuse-lite@^1.0.30001349:
+  version "1.0.30001355"
+  resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001355.tgz#e240b7177443ed0198c737a7f609536976701c77"
+  integrity sha512-Sd6pjJHF27LzCB7pT7qs+kuX2ndurzCzkpJl6Qct7LPSZ9jn0bkOA8mdgMgmqnQAWLVOOGjLpc+66V57eLtb1g==
+
+canvas@^2.6.1:
+  version "2.9.1"
+  resolved "https://registry.yarnpkg.com/canvas/-/canvas-2.9.1.tgz#58ec841cba36cef0675bc7a74ebd1561f0b476b0"
+  integrity sha512-vSQti1uG/2gjv3x6QLOZw7TctfufaerTWbVe+NSduHxxLGB+qf3kFgQ6n66DSnuoINtVUjrLLIK2R+lxrBG07A==
+  dependencies:
+    "@mapbox/node-pre-gyp" "^1.0.0"
+    nan "^2.15.0"
+    simple-get "^3.0.3"
+
+caseless@~0.12.0:
+  version "0.12.0"
+  resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
+  integrity sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==
+
+chalk@^2.0.0, chalk@^2.3.0, chalk@^2.4.1:
+  version "2.4.2"
+  resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
+  integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
+  dependencies:
+    ansi-styles "^3.2.1"
+    escape-string-regexp "^1.0.5"
+    supports-color "^5.3.0"
+
+chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.2:
+  version "4.1.2"
+  resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
+  integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
+  dependencies:
+    ansi-styles "^4.1.0"
+    supports-color "^7.1.0"
+
+chardet@^0.7.0:
+  version "0.7.0"
+  resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
+  integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==
+
+child_process@~1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/child_process/-/child_process-1.0.2.tgz#b1f7e7fc73d25e7fd1d455adc94e143830182b5a"
+  integrity sha512-Wmza/JzL0SiWz7kl6MhIKT5ceIlnFPJX+lwUGj7Clhy5MMldsSoJR0+uvRzOS5Kv45Mq7t1PoE8TsOA9bzvb6g==
+
+chownr@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece"
+  integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==
+
+chrome-trace-event@^1.0.2:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac"
+  integrity sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==
+
+classnames@^2.2:
+  version "2.3.1"
+  resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e"
+  integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==
+
+clean-stack@^2.0.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b"
+  integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==
+
+cli-cursor@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307"
+  integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==
+  dependencies:
+    restore-cursor "^3.1.0"
+
+cli-width@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6"
+  integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==
+
+clipanion@3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/clipanion/-/clipanion-3.1.0.tgz#3e217dd6476bb9236638b07eb4673f7309839819"
+  integrity sha512-v025Hz+IDQ15FpOyK8p02h5bFznMu6rLFsJSyOPR+7WrbSnZ1Ek6pblPukV7K5tC/dsWfncQPIrJ4iUy2PXkbw==
+  dependencies:
+    typanion "^3.3.1"
+
+cliui@^7.0.2:
+  version "7.0.4"
+  resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f"
+  integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==
+  dependencies:
+    string-width "^4.2.0"
+    strip-ansi "^6.0.0"
+    wrap-ansi "^7.0.0"
+
+clone-deep@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387"
+  integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==
+  dependencies:
+    is-plain-object "^2.0.4"
+    kind-of "^6.0.2"
+    shallow-clone "^3.0.0"
+
+clone-regexp@^2.1.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/clone-regexp/-/clone-regexp-2.2.0.tgz#7d65e00885cd8796405c35a737e7a86b7429e36f"
+  integrity sha512-beMpP7BOtTipFuW8hrJvREQ2DrRu3BE7by0ZpibtfBA+qfHYvMGTc2Yb1JMYPKg/JUw0CHYvpg796aNTSW9z7Q==
+  dependencies:
+    is-regexp "^2.0.0"
+
+clone-response@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b"
+  integrity sha512-yjLXh88P599UOyPTFX0POsd7WxnbsVsGohcwzHOLspIhhpalPw1BcqED8NblyZLKcGrL8dTgMlcaZxV2jAD41Q==
+  dependencies:
+    mimic-response "^1.0.0"
+
+clone@~2.1.2:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f"
+  integrity sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==
+
+codemirror@~5.61.0:
+  version "5.61.1"
+  resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.61.1.tgz#ccfc8a43b8fcfb8b12e8e75b5ffde48d541406e0"
+  integrity sha512-+D1NZjAucuzE93vJGbAaXzvoBHwp9nJZWWWF9utjv25+5AZUiah6CIlfb4ikG4MoDsFsCG8niiJH5++OO2LgIQ==
+
+color-convert@^1.9.0:
+  version "1.9.3"
+  resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
+  integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
+  dependencies:
+    color-name "1.1.3"
+
+color-convert@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
+  integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==
+  dependencies:
+    color-name "~1.1.4"
+
+color-name@1.1.3:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
+  integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==
+
+color-name@~1.1.4:
+  version "1.1.4"
+  resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
+  integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
+
+color-support@^1.1.2:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2"
+  integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==
+
+colord@^2.9.2:
+  version "2.9.2"
+  resolved "https://registry.yarnpkg.com/colord/-/colord-2.9.2.tgz#25e2bacbbaa65991422c07ea209e2089428effb1"
+  integrity sha512-Uqbg+J445nc1TKn4FoDPS6ZZqAvEDnwrH42yo8B40JSOgSLxMZ/gt3h4nmCtPLQeXhjJJkqBx7SCY35WnIixaQ==
+
+colorette@^2.0.14:
+  version "2.0.19"
+  resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.19.tgz#cdf044f47ad41a0f4b56b3a0d5b4e6e1a2d5a798"
+  integrity sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==
+
+combined-stream@^1.0.6, combined-stream@~1.0.6:
+  version "1.0.8"
+  resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
+  integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
+  dependencies:
+    delayed-stream "~1.0.0"
+
+commander@2, commander@^2.20.0:
+  version "2.20.3"
+  resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
+  integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
+
+commander@7, commander@^7.0.0:
+  version "7.2.0"
+  resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7"
+  integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==
+
+commander@~6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/commander/-/commander-6.0.0.tgz#2b270da94f8fb9014455312f829a1129dbf8887e"
+  integrity sha512-s7EA+hDtTYNhuXkTlhqew4txMZVdszBmKWSPEMxGr8ru8JXR7bLUFIAtPhcSuFdJQ0ILMxnJi8GkQL0yvDy/YA==
+
+commondir@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
+  integrity sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==
+
+compressible@~2.0.16:
+  version "2.0.18"
+  resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba"
+  integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==
+  dependencies:
+    mime-db ">= 1.43.0 < 2"
+
+compression@1.7.4:
+  version "1.7.4"
+  resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.4.tgz#95523eff170ca57c29a0ca41e6fe131f41e5bb8f"
+  integrity sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==
+  dependencies:
+    accepts "~1.3.5"
+    bytes "3.0.0"
+    compressible "~2.0.16"
+    debug "2.6.9"
+    on-headers "~1.0.2"
+    safe-buffer "5.1.2"
+    vary "~1.1.2"
+
+compute-gcd@^1.2.1:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/compute-gcd/-/compute-gcd-1.2.1.tgz#34d639f3825625e1357ce81f0e456a6249d8c77f"
+  integrity sha512-TwMbxBNz0l71+8Sc4czv13h4kEqnchV9igQZBi6QUaz09dnz13juGnnaWWJTRsP3brxOoxeB4SA2WELLw1hCtg==
+  dependencies:
+    validate.io-array "^1.0.3"
+    validate.io-function "^1.0.2"
+    validate.io-integer-array "^1.0.0"
+
+compute-lcm@^1.1.0:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/compute-lcm/-/compute-lcm-1.1.2.tgz#9107c66b9dca28cefb22b4ab4545caac4034af23"
+  integrity sha512-OFNPdQAXnQhDSKioX8/XYT6sdUlXwpeMjfd6ApxMJfyZ4GxmLR1xvMERctlYhlHwIiz6CSpBc2+qYKjHGZw4TQ==
+  dependencies:
+    compute-gcd "^1.2.1"
+    validate.io-array "^1.0.3"
+    validate.io-function "^1.0.2"
+    validate.io-integer-array "^1.0.0"
+
+concat-map@0.0.1:
+  version "0.0.1"
+  resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
+  integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
+
+console-control-strings@^1.0.0, console-control-strings@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e"
+  integrity sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==
+
+content-disposition@0.5.4:
+  version "0.5.4"
+  resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe"
+  integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==
+  dependencies:
+    safe-buffer "5.2.1"
+
+content-type@~1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
+  integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==
+
+cookie-signature@1.0.6:
+  version "1.0.6"
+  resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
+  integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==
+
+cookie@0.5.0:
+  version "0.5.0"
+  resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b"
+  integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==
+
+cookies@0.8.0:
+  version "0.8.0"
+  resolved "https://registry.yarnpkg.com/cookies/-/cookies-0.8.0.tgz#1293ce4b391740a8406e3c9870e828c4b54f3f90"
+  integrity sha512-8aPsApQfebXnuI+537McwYsDtjVxGm8gTIzQI3FDW6t5t/DAhERxtnbEPN/8RX+uZthoz4eCOgloXaE5cYyNow==
+  dependencies:
+    depd "~2.0.0"
+    keygrip "~1.1.0"
+
+core-js-pure@^3.6.5:
+  version "3.23.1"
+  resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.23.1.tgz#0b27e4c3ad46178b84e790dbbb81987218ab82ad"
+  integrity sha512-3qNgf6TqI3U1uhuSYRzJZGfFd4T+YlbyVPl+jgRiKjdZopvG4keZQwWZDAWpu1UH9nCgTpUzIV3GFawC7cJsqg==
+
+core-util-is@1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
+  integrity sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==
+
+core-util-is@~1.0.0:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85"
+  integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==
+
+cors@2.8.5:
+  version "2.8.5"
+  resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29"
+  integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==
+  dependencies:
+    object-assign "^4"
+    vary "^1"
+
+corser@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/corser/-/corser-2.0.1.tgz#8eda252ecaab5840dcd975ceb90d9370c819ff87"
+  integrity sha512-utCYNzRSQIZNPIcGZdQc92UVJYAhtGAteCFg0yRaFm8f0P+CPtyGyHXJcGXnffjCybUCEx3FQ2G7U3/o9eIkVQ==
+
+cosmiconfig@^7.0.1:
+  version "7.0.1"
+  resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.0.1.tgz#714d756522cace867867ccb4474c5d01bbae5d6d"
+  integrity sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==
+  dependencies:
+    "@types/parse-json" "^4.0.0"
+    import-fresh "^3.2.1"
+    parse-json "^5.0.0"
+    path-type "^4.0.0"
+    yaml "^1.10.0"
+
+cross-spawn@^6.0.5:
+  version "6.0.5"
+  resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
+  integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==
+  dependencies:
+    nice-try "^1.0.4"
+    path-key "^2.0.1"
+    semver "^5.5.0"
+    shebang-command "^1.2.0"
+    which "^1.2.9"
+
+cross-spawn@^7.0.2, cross-spawn@^7.0.3:
+  version "7.0.3"
+  resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
+  integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
+  dependencies:
+    path-key "^3.1.0"
+    shebang-command "^2.0.0"
+    which "^2.0.1"
+
+crypto@~1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/crypto/-/crypto-1.0.1.tgz#2af1b7cad8175d24c8a1b0778255794a21803037"
+  integrity sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==
+
+css-functions-list@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/css-functions-list/-/css-functions-list-3.1.0.tgz#cf5b09f835ad91a00e5959bcfc627cd498e1321b"
+  integrity sha512-/9lCvYZaUbBGvYUgYGFJ4dcYiyqdhSjG7IPVluoV8A1ILjkF7ilmhp1OGUz8n+nmBcu0RNrQAzgD8B6FJbrt2w==
+
+css-loader@^5.0.1:
+  version "5.2.7"
+  resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-5.2.7.tgz#9b9f111edf6fb2be5dc62525644cbc9c232064ae"
+  integrity sha512-Q7mOvpBNBG7YrVGMxRxcBJZFL75o+cH2abNASdibkj/fffYD8qWbInZrD0S9ccI6vZclF3DsHE7njGlLtaHbhg==
+  dependencies:
+    icss-utils "^5.1.0"
+    loader-utils "^2.0.0"
+    postcss "^8.2.15"
+    postcss-modules-extract-imports "^3.0.0"
+    postcss-modules-local-by-default "^4.0.0"
+    postcss-modules-scope "^3.0.0"
+    postcss-modules-values "^4.0.0"
+    postcss-value-parser "^4.1.0"
+    schema-utils "^3.0.0"
+    semver "^7.3.5"
+
+cssesc@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
+  integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
+
+cssom@^0.4.1:
+  version "0.4.4"
+  resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.4.4.tgz#5a66cf93d2d0b661d80bf6a44fb65f5c2e4e0a10"
+  integrity sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==
+
+cssom@~0.3.6:
+  version "0.3.8"
+  resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.8.tgz#9f1276f5b2b463f2114d3f2c75250af8c1a36f4a"
+  integrity sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==
+
+cssstyle@^2.0.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-2.3.0.tgz#ff665a0ddbdc31864b09647f34163443d90b0852"
+  integrity sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==
+  dependencies:
+    cssom "~0.3.6"
+
+csstype@3.0.10:
+  version "3.0.10"
+  resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.10.tgz#2ad3a7bed70f35b965707c092e5f30b327c290e5"
+  integrity sha512-2u44ZG2OcNUO9HDp/Jl8C07x6pU/eTR3ncV91SiK3dhG9TWvRVsCoJw14Ckx5DgWkzGA3waZWO3d7pgqpUI/XA==
+
+csstype@^3.0.2:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.0.tgz#4ddcac3718d787cf9df0d1b7d15033925c8f29f2"
+  integrity sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA==
+
+csstype@~3.0.3:
+  version "3.0.11"
+  resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.11.tgz#d66700c5eacfac1940deb4e3ee5642792d85cd33"
+  integrity sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw==
+
+"d3-array@1 - 3", "d3-array@2 - 3", "d3-array@2.10.0 - 3", "d3-array@2.5.0 - 3", d3-array@^3.1.1:
+  version "3.1.6"
+  resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-3.1.6.tgz#0342c835925826f49b4d16eb7027aec334ffc97d"
+  integrity sha512-DCbBBNuKOeiR9h04ySRBMW52TFVc91O9wJziuyXw6Ztmy8D3oZbmCkOO3UHKC7ceNJsN2Mavo9+vwV8EAEUXzA==
+  dependencies:
+    internmap "1 - 2"
+
+d3-array@3.1.1:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-3.1.1.tgz#7797eb53ead6b9083c75a45a681e93fc41bc468c"
+  integrity sha512-33qQ+ZoZlli19IFiQx4QEpf2CBEayMRzhlisJHSCsSUbDXv6ZishqS1x7uFVClKG4Wr7rZVHvaAttoLow6GqdQ==
+  dependencies:
+    internmap "1 - 2"
+
+"d3-color@1 - 3", d3-color@^3.0.1:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-3.1.0.tgz#395b2833dfac71507f12ac2f7af23bf819de24e2"
+  integrity sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==
+
+d3-delaunay@^6.0.2:
+  version "6.0.2"
+  resolved "https://registry.yarnpkg.com/d3-delaunay/-/d3-delaunay-6.0.2.tgz#7fd3717ad0eade2fc9939f4260acfb503f984e92"
+  integrity sha512-IMLNldruDQScrcfT+MWnazhHbDJhcRJyOEBAJfwQnHle1RPh6WDuLvxNArUju2VSMSUuKlY5BGHRJ2cYyoFLQQ==
+  dependencies:
+    delaunator "5"
+
+"d3-dispatch@1 - 3":
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-3.0.1.tgz#5fc75284e9c2375c36c839411a0cf550cbfc4d5e"
+  integrity sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==
+
+d3-dsv@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/d3-dsv/-/d3-dsv-3.0.1.tgz#c63af978f4d6a0d084a52a673922be2160789b73"
+  integrity sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==
+  dependencies:
+    commander "7"
+    iconv-lite "0.6"
+    rw "1"
+
+d3-force@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/d3-force/-/d3-force-3.0.0.tgz#3e2ba1a61e70888fe3d9194e30d6d14eece155c4"
+  integrity sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==
+  dependencies:
+    d3-dispatch "1 - 3"
+    d3-quadtree "1 - 3"
+    d3-timer "1 - 3"
+
+"d3-format@1 - 3", d3-format@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-3.1.0.tgz#9260e23a28ea5cb109e93b21a06e24e2ebd55641"
+  integrity sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==
+
+d3-geo-projection@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/d3-geo-projection/-/d3-geo-projection-4.0.0.tgz#dc229e5ead78d31869a4e87cf1f45bd2716c48ca"
+  integrity sha512-p0bK60CEzph1iqmnxut7d/1kyTmm3UWtPlwdkM31AU+LW+BXazd5zJdoCn7VFxNCHXRngPHRnsNn5uGjLRGndg==
+  dependencies:
+    commander "7"
+    d3-array "1 - 3"
+    d3-geo "1.12.0 - 3"
+
+"d3-geo@1.12.0 - 3", d3-geo@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/d3-geo/-/d3-geo-3.0.1.tgz#4f92362fd8685d93e3b1fae0fd97dc8980b1ed7e"
+  integrity sha512-Wt23xBych5tSy9IYAM1FR2rWIBFWa52B/oF/GYe5zbdHrg08FU8+BuI6X4PvTwPDdqdAdq04fuWJpELtsaEjeA==
+  dependencies:
+    d3-array "2.5.0 - 3"
+
+d3-hierarchy@^3.1.0:
+  version "3.1.2"
+  resolved "https://registry.yarnpkg.com/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz#b01cd42c1eed3d46db77a5966cf726f8c09160c6"
+  integrity sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==
+
+"d3-interpolate@1.2.0 - 3", d3-interpolate@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz#3c47aa5b32c5b3dfb56ef3fd4342078a632b400d"
+  integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==
+  dependencies:
+    d3-color "1 - 3"
+
+"d3-path@1 - 3", d3-path@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-3.0.1.tgz#f09dec0aaffd770b7995f1a399152bf93052321e"
+  integrity sha512-gq6gZom9AFZby0YLduxT1qmrp4xpBA1YZr19OI717WIdKE2OM5ETq5qrHLb301IgxhLwcuxvGZVLeeWc/k1I6w==
+
+"d3-quadtree@1 - 3":
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/d3-quadtree/-/d3-quadtree-3.0.1.tgz#6dca3e8be2b393c9a9d514dabbd80a92deef1a4f"
+  integrity sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==
+
+d3-scale@^4.0.2:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-4.0.2.tgz#82b38e8e8ff7080764f8dcec77bd4be393689396"
+  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"
+
+d3-shape@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-3.1.0.tgz#c8a495652d83ea6f524e482fca57aa3f8bc32556"
+  integrity sha512-tGDh1Muf8kWjEDT/LswZJ8WF85yDZLvVJpYU9Nq+8+yW1Z5enxrmXOhTArlkaElU+CTn0OTVNli+/i+HP45QEQ==
+  dependencies:
+    d3-path "1 - 3"
+
+"d3-time-format@2 - 4", d3-time-format@^4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-4.1.0.tgz#7ab5257a5041d11ecb4fe70a5c7d16a195bb408a"
+  integrity sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==
+  dependencies:
+    d3-time "1 - 3"
+
+"d3-time@1 - 3", "d3-time@2.1.1 - 3", d3-time@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-3.0.0.tgz#65972cb98ae2d4954ef5c932e8704061335d4975"
+  integrity sha512-zmV3lRnlaLI08y9IMRXSDshQb5Nj77smnfpnd2LrBa/2K281Jijactokeak14QacHs/kKq0AQ121nidNYlarbQ==
+  dependencies:
+    d3-array "2 - 3"
+
+"d3-timer@1 - 3", d3-timer@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-3.0.1.tgz#6284d2a2708285b1abb7e201eda4380af35e63b0"
+  integrity sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==
+
+d@1, d@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/d/-/d-1.0.1.tgz#8698095372d58dbee346ffd0c7093f99f8f9eb5a"
+  integrity sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==
+  dependencies:
+    es5-ext "^0.10.50"
+    type "^1.0.1"
+
+dashdash@^1.12.0:
+  version "1.14.1"
+  resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
+  integrity sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==
+  dependencies:
+    assert-plus "^1.0.0"
+
+data-urls@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-1.1.0.tgz#15ee0582baa5e22bb59c77140da8f9c76963bbfe"
+  integrity sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ==
+  dependencies:
+    abab "^2.0.0"
+    whatwg-mimetype "^2.2.0"
+    whatwg-url "^7.0.0"
+
+dayjs@1.11.3:
+  version "1.11.3"
+  resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.3.tgz#4754eb694a624057b9ad2224b67b15d552589258"
+  integrity sha512-xxwlswWOlGhzgQ4TKzASQkUhqERI3egRNqgV4ScR8wlANA/A9tZ7miXa44vTTKEq5l7vWoL5G57bG3zA+Kow0A==
+
+debug@2.6.9, debug@^2.6.9:
+  version "2.6.9"
+  resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
+  integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
+  dependencies:
+    ms "2.0.0"
+
+debug@4, debug@4.3.4, debug@^4.0.1, debug@^4.1.1, debug@^4.3.1, debug@^4.3.3, debug@^4.3.4:
+  version "4.3.4"
+  resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
+  integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
+  dependencies:
+    ms "2.1.2"
+
+debug@^3.1.1, debug@^3.2.7:
+  version "3.2.7"
+  resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a"
+  integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==
+  dependencies:
+    ms "^2.1.1"
+
+decamelize-keys@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/decamelize-keys/-/decamelize-keys-1.1.0.tgz#d171a87933252807eb3cb61dc1c1445d078df2d9"
+  integrity sha512-ocLWuYzRPoS9bfiSdDd3cxvrzovVMZnRDVEzAs+hWIVXGDbHxWMECij2OBuyB/An0FFW/nLuq6Kv1i/YC5Qfzg==
+  dependencies:
+    decamelize "^1.1.0"
+    map-obj "^1.0.0"
+
+decamelize@^1.1.0, decamelize@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
+  integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==
+
+decompress-response@^3.3.0:
+  version "3.3.0"
+  resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-3.3.0.tgz#80a4dd323748384bfa248083622aedec982adff3"
+  integrity sha512-BzRPQuY1ip+qDonAOz42gRm/pg9F768C+npV/4JOsxRC2sq+Rlk+Q4ZCAsOhnIaMrgarILY+RMUIvMmmX1qAEA==
+  dependencies:
+    mimic-response "^1.0.0"
+
+decompress-response@^4.2.0:
+  version "4.2.1"
+  resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-4.2.1.tgz#414023cc7a302da25ce2ec82d0d5238ccafd8986"
+  integrity sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==
+  dependencies:
+    mimic-response "^2.0.0"
+
+deep-equal@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.1.1.tgz#b5c98c942ceffaf7cb051e24e1434a25a2e6076a"
+  integrity sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==
+  dependencies:
+    is-arguments "^1.0.4"
+    is-date-object "^1.0.1"
+    is-regex "^1.0.4"
+    object-is "^1.0.1"
+    object-keys "^1.1.1"
+    regexp.prototype.flags "^1.2.0"
+
+deep-extend@^0.6.0:
+  version "0.6.0"
+  resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac"
+  integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==
+
+deep-is@^0.1.3, deep-is@~0.1.3:
+  version "0.1.4"
+  resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831"
+  integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==
+
+deepmerge@^4.2.2:
+  version "4.2.2"
+  resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955"
+  integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==
+
+defer-to-connect@^1.0.1:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-1.1.3.tgz#331ae050c08dcf789f8c83a7b81f0ed94f4ac591"
+  integrity sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==
+
+deferred-leveldown@~5.3.0:
+  version "5.3.0"
+  resolved "https://registry.yarnpkg.com/deferred-leveldown/-/deferred-leveldown-5.3.0.tgz#27a997ad95408b61161aa69bd489b86c71b78058"
+  integrity sha512-a59VOT+oDy7vtAbLRCZwWgxu2BaCfd5Hk7wxJd48ei7I+nsg8Orlb9CLG0PMZienk9BSUKgeAqkO2+Lw+1+Ukw==
+  dependencies:
+    abstract-leveldown "~6.2.1"
+    inherits "^2.0.3"
+
+define-properties@^1.1.3, define-properties@^1.1.4:
+  version "1.1.4"
+  resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.4.tgz#0b14d7bd7fbeb2f3572c3a7eda80ea5d57fb05b1"
+  integrity sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==
+  dependencies:
+    has-property-descriptors "^1.0.0"
+    object-keys "^1.1.1"
+
+delaunator@5:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/delaunator/-/delaunator-5.0.0.tgz#60f052b28bd91c9b4566850ebf7756efe821d81b"
+  integrity sha512-AyLvtyJdbv/U1GkiS6gUUzclRoAY4Gs75qkMygJJhU75LW4DNuSF2RMzpxs9jw9Oz1BobHjTdkG3zdP55VxAqw==
+  dependencies:
+    robust-predicates "^3.0.0"
+
+delayed-stream@~1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
+  integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
+
+delegates@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
+  integrity sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==
+
+depd@2.0.0, depd@~2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df"
+  integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==
+
+dependency-graph@^0.9.0:
+  version "0.9.0"
+  resolved "https://registry.yarnpkg.com/dependency-graph/-/dependency-graph-0.9.0.tgz#11aed7e203bc8b00f48356d92db27b265c445318"
+  integrity sha512-9YLIBURXj4DJMFALxXw9K3Y3rwb5Fk0X5/8ipCzaN84+gKxoHK43tVKRNakCQbiEx07E8Uwhuq21BpUagFhZ8w==
+
+destroy@1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015"
+  integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==
+
+detect-indent@^6.0.0:
+  version "6.1.0"
+  resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-6.1.0.tgz#592485ebbbf6b3b1ab2be175c8393d04ca0d57e6"
+  integrity sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==
+
+detect-libc@^2.0.0:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.1.tgz#e1897aa88fa6ad197862937fbc0441ef352ee0cd"
+  integrity sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==
+
+detect-newline@3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651"
+  integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==
+
+dir-glob@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f"
+  integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==
+  dependencies:
+    path-type "^4.0.0"
+
+doctrine@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961"
+  integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==
+  dependencies:
+    esutils "^2.0.2"
+
+dom-helpers@^3.4.0:
+  version "3.4.0"
+  resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.4.0.tgz#e9b369700f959f62ecde5a6babde4bccd9169af8"
+  integrity sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==
+  dependencies:
+    "@babel/runtime" "^7.1.2"
+
+dom-serializer@^1.0.1:
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.4.1.tgz#de5d41b1aea290215dc45a6dae8adcf1d32e2d30"
+  integrity sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==
+  dependencies:
+    domelementtype "^2.0.1"
+    domhandler "^4.2.0"
+    entities "^2.0.0"
+
+dom4@^2.1.5:
+  version "2.1.6"
+  resolved "https://registry.yarnpkg.com/dom4/-/dom4-2.1.6.tgz#c90df07134aa0dbd81ed4d6ba1237b36fc164770"
+  integrity sha512-JkCVGnN4ofKGbjf5Uvc8mmxaATIErKQKSgACdBXpsQ3fY6DlIpAyWfiBSrGkttATssbDCp3psiAKWXk5gmjycA==
+
+domelementtype@^2.0.1, domelementtype@^2.2.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d"
+  integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==
+
+domexception@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/domexception/-/domexception-1.0.1.tgz#937442644ca6a31261ef36e3ec677fe805582c90"
+  integrity sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==
+  dependencies:
+    webidl-conversions "^4.0.2"
+
+domhandler@^4.0.0, domhandler@^4.2.0:
+  version "4.3.1"
+  resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.3.1.tgz#8d792033416f59d68bc03a5aa7b018c1ca89279c"
+  integrity sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==
+  dependencies:
+    domelementtype "^2.2.0"
+
+dompurify@2.3.8:
+  version "2.3.8"
+  resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.3.8.tgz#224fe9ae57d7ebd9a1ae1ac18c1c1ca3f532226f"
+  integrity sha512-eVhaWoVibIzqdGYjwsBWodIQIaXFSB+cKDf4cfxLMsK0xiud6SE+/WCVx/Xw/UwQsa4cS3T2eITcdtmTg2UKcw==
+
+domutils@^2.5.2:
+  version "2.8.0"
+  resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135"
+  integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==
+  dependencies:
+    dom-serializer "^1.0.1"
+    domelementtype "^2.2.0"
+    domhandler "^4.2.0"
+
+duplexer3@^0.1.4:
+  version "0.1.4"
+  resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2"
+  integrity sha512-CEj8FwwNA4cVH2uFCoHUrmojhYh1vmCdOaneKJXwkeY1i9jnlslVo9dx+hQ5Hl9GnH/Bwy/IjxAyOePyPKYnzA==
+
+duplicate-package-checker-webpack-plugin@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/duplicate-package-checker-webpack-plugin/-/duplicate-package-checker-webpack-plugin-3.0.0.tgz#78bb89e625fa7cf8c2a59c53f62b495fda9ba287"
+  integrity sha512-aO50/qPC7X2ChjRFniRiscxBLT/K01bALqfcDaf8Ih5OqQ1N4iT/Abx9Ofu3/ms446vHTm46FACIuJUmgUQcDQ==
+  dependencies:
+    chalk "^2.3.0"
+    find-root "^1.0.0"
+    lodash "^4.17.4"
+    semver "^5.4.1"
+
+ecc-jsbn@~0.1.1:
+  version "0.1.2"
+  resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9"
+  integrity sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==
+  dependencies:
+    jsbn "~0.1.0"
+    safer-buffer "^2.1.0"
+
+ecdsa-sig-formatter@1.0.11:
+  version "1.0.11"
+  resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf"
+  integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==
+  dependencies:
+    safe-buffer "^5.0.1"
+
+ee-first@1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
+  integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==
+
+electron-to-chromium@^1.4.147:
+  version "1.4.158"
+  resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.158.tgz#abbdaaf64676bfa4bc0307522125db34424a0ada"
+  integrity sha512-gppO3/+Y6sP432HtvwvuU8S+YYYLH4PmAYvQwqUtt9HDOmEsBwQfLnK9T8+1NIKwAS1BEygIjTaATC4H5EzvxQ==
+
+emoji-regex@^8.0.0:
+  version "8.0.0"
+  resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
+  integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
+
+emojis-list@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78"
+  integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==
+
+encodeurl@~1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
+  integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==
+
+encoding-down@^6.3.0:
+  version "6.3.0"
+  resolved "https://registry.yarnpkg.com/encoding-down/-/encoding-down-6.3.0.tgz#b1c4eb0e1728c146ecaef8e32963c549e76d082b"
+  integrity sha512-QKrV0iKR6MZVJV08QY0wp1e7vF6QbhnbQhb07bwpEyuz4uZiZgPlEGdkCROuFkUwdxlFaiPIhjyarH1ee/3vhw==
+  dependencies:
+    abstract-leveldown "^6.2.1"
+    inherits "^2.0.3"
+    level-codec "^9.0.0"
+    level-errors "^2.0.0"
+
+end-of-stream@^1.1.0:
+  version "1.4.4"
+  resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0"
+  integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==
+  dependencies:
+    once "^1.4.0"
+
+enhanced-resolve@^5.9.3:
+  version "5.9.3"
+  resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.9.3.tgz#44a342c012cbc473254af5cc6ae20ebd0aae5d88"
+  integrity sha512-Bq9VSor+kjvW3f9/MiiR4eE3XYgOl7/rS8lnSxbRbF3kS0B2r+Y9w5krBWxZgDxASVZbdYrn5wT4j/Wb0J9qow==
+  dependencies:
+    graceful-fs "^4.2.4"
+    tapable "^2.2.0"
+
+enquirer@^2.3.5:
+  version "2.3.6"
+  resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d"
+  integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==
+  dependencies:
+    ansi-colors "^4.1.1"
+
+entities@^2.0.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55"
+  integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==
+
+envinfo@7.8.1, envinfo@^7.7.3:
+  version "7.8.1"
+  resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.8.1.tgz#06377e3e5f4d379fea7ac592d5ad8927e0c4d475"
+  integrity sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==
+
+errno@~0.1.1:
+  version "0.1.8"
+  resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.8.tgz#8bb3e9c7d463be4976ff888f76b4809ebc2e811f"
+  integrity sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==
+  dependencies:
+    prr "~1.0.1"
+
+error-ex@^1.3.1:
+  version "1.3.2"
+  resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf"
+  integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==
+  dependencies:
+    is-arrayish "^0.2.1"
+
+es-abstract@^1.19.0, es-abstract@^1.19.1, es-abstract@^1.19.5:
+  version "1.20.1"
+  resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.20.1.tgz#027292cd6ef44bd12b1913b828116f54787d1814"
+  integrity sha512-WEm2oBhfoI2sImeM4OF2zE2V3BYdSF+KnSi9Sidz51fQHd7+JuF8Xgcj9/0o+OWeIeIS/MiuNnlruQrJf16GQA==
+  dependencies:
+    call-bind "^1.0.2"
+    es-to-primitive "^1.2.1"
+    function-bind "^1.1.1"
+    function.prototype.name "^1.1.5"
+    get-intrinsic "^1.1.1"
+    get-symbol-description "^1.0.0"
+    has "^1.0.3"
+    has-property-descriptors "^1.0.0"
+    has-symbols "^1.0.3"
+    internal-slot "^1.0.3"
+    is-callable "^1.2.4"
+    is-negative-zero "^2.0.2"
+    is-regex "^1.1.4"
+    is-shared-array-buffer "^1.0.2"
+    is-string "^1.0.7"
+    is-weakref "^1.0.2"
+    object-inspect "^1.12.0"
+    object-keys "^1.1.1"
+    object.assign "^4.1.2"
+    regexp.prototype.flags "^1.4.3"
+    string.prototype.trimend "^1.0.5"
+    string.prototype.trimstart "^1.0.5"
+    unbox-primitive "^1.0.2"
+
+es-module-lexer@^0.9.0:
+  version "0.9.3"
+  resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-0.9.3.tgz#6f13db00cc38417137daf74366f535c8eb438f19"
+  integrity sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==
+
+es-to-primitive@^1.2.1:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a"
+  integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==
+  dependencies:
+    is-callable "^1.1.4"
+    is-date-object "^1.0.1"
+    is-symbol "^1.0.2"
+
+es5-ext@^0.10.35, es5-ext@^0.10.46, es5-ext@^0.10.50, es5-ext@^0.10.53, es5-ext@~0.10.14, es5-ext@~0.10.2, es5-ext@~0.10.46:
+  version "0.10.61"
+  resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.61.tgz#311de37949ef86b6b0dcea894d1ffedb909d3269"
+  integrity sha512-yFhIqQAzu2Ca2I4SE2Au3rxVfmohU9Y7wqGR+s7+H7krk26NXhIRAZDgqd6xqjCEFUomDEA3/Bo/7fKmIkW1kA==
+  dependencies:
+    es6-iterator "^2.0.3"
+    es6-symbol "^3.1.3"
+    next-tick "^1.1.0"
+
+es6-iterator@^2.0.3:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7"
+  integrity sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==
+  dependencies:
+    d "1"
+    es5-ext "^0.10.35"
+    es6-symbol "^3.1.1"
+
+es6-symbol@^3.1.1, es6-symbol@^3.1.3:
+  version "3.1.3"
+  resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.3.tgz#bad5d3c1bcdac28269f4cb331e431c78ac705d18"
+  integrity sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==
+  dependencies:
+    d "^1.0.1"
+    ext "^1.1.2"
+
+es6-weak-map@^2.0.3:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/es6-weak-map/-/es6-weak-map-2.0.3.tgz#b6da1f16cc2cc0d9be43e6bdbfc5e7dfcdf31d53"
+  integrity sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==
+  dependencies:
+    d "1"
+    es5-ext "^0.10.46"
+    es6-iterator "^2.0.3"
+    es6-symbol "^3.1.1"
+
+escalade@^3.1.1:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
+  integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==
+
+escape-html@~1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
+  integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==
+
+escape-string-regexp@^1.0.5:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
+  integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==
+
+escape-string-regexp@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34"
+  integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==
+
+escodegen@^1.11.1:
+  version "1.14.3"
+  resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.14.3.tgz#4e7b81fba61581dc97582ed78cab7f0e8d63f503"
+  integrity sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==
+  dependencies:
+    esprima "^4.0.1"
+    estraverse "^4.2.0"
+    esutils "^2.0.2"
+    optionator "^0.8.1"
+  optionalDependencies:
+    source-map "~0.6.1"
+
+eslint-config-prettier@^6.15.0:
+  version "6.15.0"
+  resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-6.15.0.tgz#7f93f6cb7d45a92f1537a70ecc06366e1ac6fed9"
+  integrity sha512-a1+kOYLR8wMGustcgAjdydMsQ2A/2ipRPwRKUmfYaSxc9ZPcrku080Ctl6zrZzZNs/U82MjSv+qKREkoq3bJaw==
+  dependencies:
+    get-stdin "^6.0.0"
+
+eslint-import-resolver-node@0.3.6:
+  version "0.3.6"
+  resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz#4048b958395da89668252001dbd9eca6b83bacbd"
+  integrity sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw==
+  dependencies:
+    debug "^3.2.7"
+    resolve "^1.20.0"
+
+eslint-plugin-prettier@^3.1.4:
+  version "3.4.1"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-3.4.1.tgz#e9ddb200efb6f3d05ffe83b1665a716af4a387e5"
+  integrity sha512-htg25EUYUeIhKHXjOinK4BgCcDwtLHjqaxCDsMy5nbnUMkKFvIhMVCp+5GFUXQ4Nr8lBsPqtGAqBenbpFqAA2g==
+  dependencies:
+    prettier-linter-helpers "^1.0.0"
+
+eslint-scope@5.1.1, eslint-scope@^5.1.1:
+  version "5.1.1"
+  resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c"
+  integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==
+  dependencies:
+    esrecurse "^4.3.0"
+    estraverse "^4.1.1"
+
+eslint-utils@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-2.1.0.tgz#d2de5e03424e707dc10c74068ddedae708741b27"
+  integrity sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==
+  dependencies:
+    eslint-visitor-keys "^1.1.0"
+
+eslint-utils@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-3.0.0.tgz#8aebaface7345bb33559db0a1f13a1d2d48c3672"
+  integrity sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==
+  dependencies:
+    eslint-visitor-keys "^2.0.0"
+
+eslint-visitor-keys@^1.1.0, eslint-visitor-keys@^1.3.0:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e"
+  integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==
+
+eslint-visitor-keys@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303"
+  integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==
+
+eslint@^7.14.0:
+  version "7.32.0"
+  resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.32.0.tgz#c6d328a14be3fb08c8d1d21e12c02fdb7a2a812d"
+  integrity sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==
+  dependencies:
+    "@babel/code-frame" "7.12.11"
+    "@eslint/eslintrc" "^0.4.3"
+    "@humanwhocodes/config-array" "^0.5.0"
+    ajv "^6.10.0"
+    chalk "^4.0.0"
+    cross-spawn "^7.0.2"
+    debug "^4.0.1"
+    doctrine "^3.0.0"
+    enquirer "^2.3.5"
+    escape-string-regexp "^4.0.0"
+    eslint-scope "^5.1.1"
+    eslint-utils "^2.1.0"
+    eslint-visitor-keys "^2.0.0"
+    espree "^7.3.1"
+    esquery "^1.4.0"
+    esutils "^2.0.2"
+    fast-deep-equal "^3.1.3"
+    file-entry-cache "^6.0.1"
+    functional-red-black-tree "^1.0.1"
+    glob-parent "^5.1.2"
+    globals "^13.6.0"
+    ignore "^4.0.6"
+    import-fresh "^3.0.0"
+    imurmurhash "^0.1.4"
+    is-glob "^4.0.0"
+    js-yaml "^3.13.1"
+    json-stable-stringify-without-jsonify "^1.0.1"
+    levn "^0.4.1"
+    lodash.merge "^4.6.2"
+    minimatch "^3.0.4"
+    natural-compare "^1.4.0"
+    optionator "^0.9.1"
+    progress "^2.0.0"
+    regexpp "^3.1.0"
+    semver "^7.2.1"
+    strip-ansi "^6.0.0"
+    strip-json-comments "^3.1.0"
+    table "^6.0.9"
+    text-table "^0.2.0"
+    v8-compile-cache "^2.0.3"
+
+espree@^7.3.0, espree@^7.3.1:
+  version "7.3.1"
+  resolved "https://registry.yarnpkg.com/espree/-/espree-7.3.1.tgz#f2df330b752c6f55019f8bd89b7660039c1bbbb6"
+  integrity sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==
+  dependencies:
+    acorn "^7.4.0"
+    acorn-jsx "^5.3.1"
+    eslint-visitor-keys "^1.3.0"
+
+esprima@^4.0.0, esprima@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
+  integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
+
+esquery@^1.4.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.4.0.tgz#2148ffc38b82e8c7057dfed48425b3e61f0f24a5"
+  integrity sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==
+  dependencies:
+    estraverse "^5.1.0"
+
+esrecurse@^4.3.0:
+  version "4.3.0"
+  resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921"
+  integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==
+  dependencies:
+    estraverse "^5.2.0"
+
+estraverse@^4.1.1, estraverse@^4.2.0:
+  version "4.3.0"
+  resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d"
+  integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==
+
+estraverse@^5.1.0, estraverse@^5.2.0:
+  version "5.3.0"
+  resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123"
+  integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==
+
+esutils@^2.0.2:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
+  integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
+
+etag@~1.8.1:
+  version "1.8.1"
+  resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
+  integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==
+
+event-emitter@^0.3.5:
+  version "0.3.5"
+  resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39"
+  integrity sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==
+  dependencies:
+    d "1"
+    es5-ext "~0.10.14"
+
+eventemitter3@^4.0.0:
+  version "4.0.7"
+  resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f"
+  integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==
+
+events@^3.2.0:
+  version "3.3.0"
+  resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400"
+  integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==
+
+execall@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/execall/-/execall-2.0.0.tgz#16a06b5fe5099df7d00be5d9c06eecded1663b45"
+  integrity sha512-0FU2hZ5Hh6iQnarpRtQurM/aAvp3RIbfvgLHrcqJYzhXyV2KFruhuChf9NC6waAhiUR7FFtlugkI4p7f2Fqlow==
+  dependencies:
+    clone-regexp "^2.1.0"
+
+express-rate-limit@5.5.1:
+  version "5.5.1"
+  resolved "https://registry.yarnpkg.com/express-rate-limit/-/express-rate-limit-5.5.1.tgz#110c23f6a65dfa96ab468eda95e71697bc6987a2"
+  integrity sha512-MTjE2eIbHv5DyfuFz4zLYWxpqVhEhkTiwFGuB74Q9CSou2WHO52nlE5y3Zlg6SIsiYUIPj6ifFxnkPz6O3sIUg==
+
+express@4.18.1:
+  version "4.18.1"
+  resolved "https://registry.yarnpkg.com/express/-/express-4.18.1.tgz#7797de8b9c72c857b9cd0e14a5eea80666267caf"
+  integrity sha512-zZBcOX9TfehHQhtupq57OF8lFZ3UZi08Y97dwFCkD8p9d/d2Y3M+ykKcwaMDEL+4qyUolgBDX6AblpR3fL212Q==
+  dependencies:
+    accepts "~1.3.8"
+    array-flatten "1.1.1"
+    body-parser "1.20.0"
+    content-disposition "0.5.4"
+    content-type "~1.0.4"
+    cookie "0.5.0"
+    cookie-signature "1.0.6"
+    debug "2.6.9"
+    depd "2.0.0"
+    encodeurl "~1.0.2"
+    escape-html "~1.0.3"
+    etag "~1.8.1"
+    finalhandler "1.2.0"
+    fresh "0.5.2"
+    http-errors "2.0.0"
+    merge-descriptors "1.0.1"
+    methods "~1.1.2"
+    on-finished "2.4.1"
+    parseurl "~1.3.3"
+    path-to-regexp "0.1.7"
+    proxy-addr "~2.0.7"
+    qs "6.10.3"
+    range-parser "~1.2.1"
+    safe-buffer "5.2.1"
+    send "0.18.0"
+    serve-static "1.15.0"
+    setprototypeof "1.2.0"
+    statuses "2.0.1"
+    type-is "~1.6.18"
+    utils-merge "1.0.1"
+    vary "~1.1.2"
+
+ext@^1.1.2:
+  version "1.6.0"
+  resolved "https://registry.yarnpkg.com/ext/-/ext-1.6.0.tgz#3871d50641e874cc172e2b53f919842d19db4c52"
+  integrity sha512-sdBImtzkq2HpkdRLtlLWDa6w4DX22ijZLKx8BMPUuKe1c5lbN6xwQDQCxSfxBQnHZ13ls/FH0MQZx/q/gr6FQg==
+  dependencies:
+    type "^2.5.0"
+
+extend@~3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
+  integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
+
+external-editor@^3.0.3:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495"
+  integrity sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==
+  dependencies:
+    chardet "^0.7.0"
+    iconv-lite "^0.4.24"
+    tmp "^0.0.33"
+
+extsprintf@1.3.0:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
+  integrity sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==
+
+extsprintf@^1.2.0:
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.1.tgz#8d172c064867f235c0c84a596806d279bf4bcc07"
+  integrity sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==
+
+fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3, fast-deep-equal@~3.1.3:
+  version "3.1.3"
+  resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
+  integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
+
+fast-diff@^1.1.2:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03"
+  integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==
+
+fast-glob@^3.0.3, fast-glob@^3.2.11, fast-glob@^3.2.9:
+  version "3.2.11"
+  resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.11.tgz#a1172ad95ceb8a16e20caa5c5e56480e5129c1d9"
+  integrity sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==
+  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"
+
+fast-json-stable-stringify@^2.0.0, fast-json-stable-stringify@~2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
+  integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
+
+fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6:
+  version "2.0.6"
+  resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
+  integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==
+
+fast-redact@^3.0.0:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/fast-redact/-/fast-redact-3.1.1.tgz#790fcff8f808c2e12fabbfb2be5cb2deda448fa0"
+  integrity sha512-odVmjC8x8jNeMZ3C+rPMESzXVSEU8tSWSHv9HFxP2mm89G/1WwqhrerJDQm9Zus8X6aoRgQDThKqptdNA6bt+A==
+
+fast-safe-stringify@2.1.1, fast-safe-stringify@^2.0.8:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884"
+  integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==
+
+fastest-levenshtein@^1.0.12:
+  version "1.0.12"
+  resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz#9990f7d3a88cc5a9ffd1f1745745251700d497e2"
+  integrity sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow==
+
+fastq@^1.6.0:
+  version "1.13.0"
+  resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.13.0.tgz#616760f88a7526bdfc596b7cab8c18938c36b98c"
+  integrity sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==
+  dependencies:
+    reusify "^1.0.4"
+
+figures@^3.0.0:
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af"
+  integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==
+  dependencies:
+    escape-string-regexp "^1.0.5"
+
+file-entry-cache@^6.0.1:
+  version "6.0.1"
+  resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027"
+  integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==
+  dependencies:
+    flat-cache "^3.0.4"
+
+file-loader@~6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-6.0.0.tgz#97bbfaab7a2460c07bcbd72d3a6922407f67649f"
+  integrity sha512-/aMOAYEFXDdjG0wytpTL5YQLfZnnTmLNjn+AIrJ/6HVnTfDqLsVKUUwkDf4I4kgex36BvjuXEn/TX9B/1ESyqQ==
+  dependencies:
+    loader-utils "^2.0.0"
+    schema-utils "^2.6.5"
+
+fill-range@^7.0.1:
+  version "7.0.1"
+  resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
+  integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
+  dependencies:
+    to-regex-range "^5.0.1"
+
+finalhandler@1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32"
+  integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==
+  dependencies:
+    debug "2.6.9"
+    encodeurl "~1.0.2"
+    escape-html "~1.0.3"
+    on-finished "2.4.1"
+    parseurl "~1.3.3"
+    statuses "2.0.1"
+    unpipe "~1.0.0"
+
+find-cache-dir@^3.3.1:
+  version "3.3.2"
+  resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.3.2.tgz#b30c5b6eff0730731aea9bbd9dbecbd80256d64b"
+  integrity sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==
+  dependencies:
+    commondir "^1.0.1"
+    make-dir "^3.0.2"
+    pkg-dir "^4.1.0"
+
+find-root@^1.0.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/find-root/-/find-root-1.1.0.tgz#abcfc8ba76f708c42a97b3d685b7e9450bfb9ce4"
+  integrity sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==
+
+find-up@^4.0.0, find-up@^4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19"
+  integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==
+  dependencies:
+    locate-path "^5.0.0"
+    path-exists "^4.0.0"
+
+flat-cache@^3.0.4:
+  version "3.0.4"
+  resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11"
+  integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==
+  dependencies:
+    flatted "^3.1.0"
+    rimraf "^3.0.2"
+
+flatstr@^1.0.12:
+  version "1.0.12"
+  resolved "https://registry.yarnpkg.com/flatstr/-/flatstr-1.0.12.tgz#c2ba6a08173edbb6c9640e3055b95e287ceb5931"
+  integrity sha512-4zPxDyhCyiN2wIAtSLI6gc82/EjqZc1onI4Mz/l0pWrAlsSfYH/2ZIcU+e3oA2wDwbzIWNKwa23F8rh6+DRWkw==
+
+flatted@^3.1.0:
+  version "3.2.5"
+  resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.5.tgz#76c8584f4fc843db64702a6bd04ab7a8bd666da3"
+  integrity sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==
+
+follow-redirects@^1.0.0:
+  version "1.15.1"
+  resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.1.tgz#0ca6a452306c9b276e4d3127483e29575e207ad5"
+  integrity sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==
+
+forever-agent@~0.6.1:
+  version "0.6.1"
+  resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
+  integrity sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==
+
+form-data@~2.3.2:
+  version "2.3.3"
+  resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6"
+  integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==
+  dependencies:
+    asynckit "^0.4.0"
+    combined-stream "^1.0.6"
+    mime-types "^2.1.12"
+
+forwarded@0.2.0:
+  version "0.2.0"
+  resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811"
+  integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==
+
+free-style@3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/free-style/-/free-style-3.1.0.tgz#4e2996029534e6b1731611d843437b9e2f473f08"
+  integrity sha512-vJujYSIyT30iDoaoeigNAxX4yB1RUrh+N2ZMhIElMr3BvCuGXOw7XNJMEEJkDUeamK2Rnb/IKFGKRKlTWIGRWA==
+
+fresh@0.5.2:
+  version "0.5.2"
+  resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
+  integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==
+
+fs-extra@^9.0.1:
+  version "9.1.0"
+  resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d"
+  integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==
+  dependencies:
+    at-least-node "^1.0.0"
+    graceful-fs "^4.2.0"
+    jsonfile "^6.0.1"
+    universalify "^2.0.0"
+
+fs-minipass@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb"
+  integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==
+  dependencies:
+    minipass "^3.0.0"
+
+fs.realpath@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
+  integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==
+
+function-bind@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
+  integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
+
+function.prototype.name@^1.1.5:
+  version "1.1.5"
+  resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.5.tgz#cce0505fe1ffb80503e6f9e46cc64e46a12a9621"
+  integrity sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==
+  dependencies:
+    call-bind "^1.0.2"
+    define-properties "^1.1.3"
+    es-abstract "^1.19.0"
+    functions-have-names "^1.2.2"
+
+functional-red-black-tree@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327"
+  integrity sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==
+
+functions-have-names@^1.2.2:
+  version "1.2.3"
+  resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834"
+  integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==
+
+gauge@^3.0.0:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/gauge/-/gauge-3.0.2.tgz#03bf4441c044383908bcfa0656ad91803259b395"
+  integrity sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==
+  dependencies:
+    aproba "^1.0.3 || ^2.0.0"
+    color-support "^1.1.2"
+    console-control-strings "^1.0.0"
+    has-unicode "^2.0.1"
+    object-assign "^4.1.1"
+    signal-exit "^3.0.0"
+    string-width "^4.2.3"
+    strip-ansi "^6.0.1"
+    wide-align "^1.1.2"
+
+get-caller-file@^2.0.5:
+  version "2.0.5"
+  resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
+  integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
+
+get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.2.tgz#336975123e05ad0b7ba41f152ee4aadbea6cf598"
+  integrity sha512-Jfm3OyCxHh9DJyc28qGk+JmfkpO41A4XkneDSujN9MDXrm4oDKdHvndhZ2dN94+ERNfkYJWDclW6k2L/ZGHjXA==
+  dependencies:
+    function-bind "^1.1.1"
+    has "^1.0.3"
+    has-symbols "^1.0.3"
+
+get-stdin@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-6.0.0.tgz#9e09bf712b360ab9225e812048f71fde9c89657b"
+  integrity sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==
+
+get-stdin@^8.0.0:
+  version "8.0.0"
+  resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-8.0.0.tgz#cbad6a73feb75f6eeb22ba9e01f89aa28aa97a53"
+  integrity sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==
+
+get-stream@^4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5"
+  integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==
+  dependencies:
+    pump "^3.0.0"
+
+get-stream@^5.1.0:
+  version "5.2.0"
+  resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3"
+  integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==
+  dependencies:
+    pump "^3.0.0"
+
+get-symbol-description@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.0.tgz#7fdb81c900101fbd564dd5f1a30af5aadc1e58d6"
+  integrity sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==
+  dependencies:
+    call-bind "^1.0.2"
+    get-intrinsic "^1.1.1"
+
+getpass@^0.1.1:
+  version "0.1.7"
+  resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa"
+  integrity sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==
+  dependencies:
+    assert-plus "^1.0.0"
+
+git-hooks-list@1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/git-hooks-list/-/git-hooks-list-1.0.3.tgz#be5baaf78203ce342f2f844a9d2b03dba1b45156"
+  integrity sha512-Y7wLWcrLUXwk2noSka166byGCvhMtDRpgHdzCno1UQv/n/Hegp++a2xBWJL1lJarnKD3SWaljD+0z1ztqxuKyQ==
+
+glob-parent@^5.1.2:
+  version "5.1.2"
+  resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
+  integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
+  dependencies:
+    is-glob "^4.0.1"
+
+glob-to-regexp@^0.4.1:
+  version "0.4.1"
+  resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e"
+  integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==
+
+glob@^6.0.1:
+  version "6.0.4"
+  resolved "https://registry.yarnpkg.com/glob/-/glob-6.0.4.tgz#0f08860f6a155127b2fadd4f9ce24b1aab6e4d22"
+  integrity sha512-MKZeRNyYZAVVVG1oZeLaWie1uweH40m9AZwIwxyPbTSX4hHrVYSzLg0Ro5Z5R7XKkIX+Cc6oD1rqeDJnwsB8/A==
+  dependencies:
+    inflight "^1.0.4"
+    inherits "2"
+    minimatch "2 || 3"
+    once "^1.3.0"
+    path-is-absolute "^1.0.0"
+
+glob@^7.1.3, glob@^7.1.4:
+  version "7.2.3"
+  resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b"
+  integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==
+  dependencies:
+    fs.realpath "^1.0.0"
+    inflight "^1.0.4"
+    inherits "2"
+    minimatch "^3.1.1"
+    once "^1.3.0"
+    path-is-absolute "^1.0.0"
+
+glob@~7.1.6:
+  version "7.1.7"
+  resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90"
+  integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==
+  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"
+
+global-modules@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-2.0.0.tgz#997605ad2345f27f51539bea26574421215c7780"
+  integrity sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==
+  dependencies:
+    global-prefix "^3.0.0"
+
+global-prefix@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-3.0.0.tgz#fc85f73064df69f50421f47f883fe5b913ba9b97"
+  integrity sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==
+  dependencies:
+    ini "^1.3.5"
+    kind-of "^6.0.2"
+    which "^1.3.1"
+
+globals@^13.6.0, globals@^13.9.0:
+  version "13.15.0"
+  resolved "https://registry.yarnpkg.com/globals/-/globals-13.15.0.tgz#38113218c907d2f7e98658af246cef8b77e90bac"
+  integrity sha512-bpzcOlgDhMG070Av0Vy5Owklpv1I6+j96GhUI7Rh7IzDCKLzboflLrrfqMu8NquDbiR4EOQk7XzJwqVJxicxog==
+  dependencies:
+    type-fest "^0.20.2"
+
+globby@10.0.0:
+  version "10.0.0"
+  resolved "https://registry.yarnpkg.com/globby/-/globby-10.0.0.tgz#abfcd0630037ae174a88590132c2f6804e291072"
+  integrity sha512-3LifW9M4joGZasyYPz2A1U74zbC/45fvpXUvO/9KbSa+VV0aGZarWkfdgKyR9sExNP0t0x0ss/UMJpNpcaTspw==
+  dependencies:
+    "@types/glob" "^7.1.1"
+    array-union "^2.1.0"
+    dir-glob "^3.0.1"
+    fast-glob "^3.0.3"
+    glob "^7.1.3"
+    ignore "^5.1.1"
+    merge2 "^1.2.3"
+    slash "^3.0.0"
+
+globby@^11.0.3, globby@^11.1.0:
+  version "11.1.0"
+  resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b"
+  integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==
+  dependencies:
+    array-union "^2.1.0"
+    dir-glob "^3.0.1"
+    fast-glob "^3.2.9"
+    ignore "^5.2.0"
+    merge2 "^1.4.1"
+    slash "^3.0.0"
+
+globjoin@^0.1.4:
+  version "0.1.4"
+  resolved "https://registry.yarnpkg.com/globjoin/-/globjoin-0.1.4.tgz#2f4494ac8919e3767c5cbb691e9f463324285d43"
+  integrity sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==
+
+got@^9.6.0:
+  version "9.6.0"
+  resolved "https://registry.yarnpkg.com/got/-/got-9.6.0.tgz#edf45e7d67f99545705de1f7bbeeeb121765ed85"
+  integrity sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==
+  dependencies:
+    "@sindresorhus/is" "^0.14.0"
+    "@szmarczak/http-timer" "^1.1.2"
+    cacheable-request "^6.0.0"
+    decompress-response "^3.3.0"
+    duplexer3 "^0.1.4"
+    get-stream "^4.1.0"
+    lowercase-keys "^1.0.1"
+    mimic-response "^1.0.1"
+    p-cancelable "^1.0.0"
+    to-readable-stream "^1.0.0"
+    url-parse-lax "^3.0.0"
+
+graceful-fs@^4.1.2, graceful-fs@^4.1.3, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.9:
+  version "4.2.10"
+  resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c"
+  integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==
+
+gud@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/gud/-/gud-1.0.0.tgz#a489581b17e6a70beca9abe3ae57de7a499852c0"
+  integrity sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw==
+
+handlebars@4.7.7:
+  version "4.7.7"
+  resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.7.tgz#9ce33416aad02dbd6c8fafa8240d5d98004945a1"
+  integrity sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==
+  dependencies:
+    minimist "^1.2.5"
+    neo-async "^2.6.0"
+    source-map "^0.6.1"
+    wordwrap "^1.0.0"
+  optionalDependencies:
+    uglify-js "^3.1.4"
+
+har-schema@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92"
+  integrity sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==
+
+har-validator@~5.1.0, har-validator@~5.1.3:
+  version "5.1.5"
+  resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.5.tgz#1f0803b9f8cb20c0fa13822df1ecddb36bde1efd"
+  integrity sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==
+  dependencies:
+    ajv "^6.12.3"
+    har-schema "^2.0.0"
+
+hard-rejection@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/hard-rejection/-/hard-rejection-2.1.0.tgz#1c6eda5c1685c63942766d79bb40ae773cecd883"
+  integrity sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==
+
+has-bigints@^1.0.1, has-bigints@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa"
+  integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==
+
+has-flag@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
+  integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==
+
+has-flag@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
+  integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
+
+has-property-descriptors@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz#610708600606d36961ed04c196193b6a607fa861"
+  integrity sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==
+  dependencies:
+    get-intrinsic "^1.1.1"
+
+has-symbols@^1.0.1, has-symbols@^1.0.2, has-symbols@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8"
+  integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==
+
+has-tostringtag@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.0.tgz#7e133818a7d394734f941e73c3d3f9291e658b25"
+  integrity sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==
+  dependencies:
+    has-symbols "^1.0.2"
+
+has-unicode@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9"
+  integrity sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==
+
+has@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
+  integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
+  dependencies:
+    function-bind "^1.1.1"
+
+he@^1.1.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
+  integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
+
+hosted-git-info@^2.1.4:
+  version "2.8.9"
+  resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9"
+  integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==
+
+hosted-git-info@^4.0.1:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-4.1.0.tgz#827b82867e9ff1c8d0c4d9d53880397d2c86d224"
+  integrity sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==
+  dependencies:
+    lru-cache "^6.0.0"
+
+html-encoding-sniffer@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz#e70d84b94da53aa375e11fe3a351be6642ca46f8"
+  integrity sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw==
+  dependencies:
+    whatwg-encoding "^1.0.1"
+
+html-tags@^3.2.0:
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.2.0.tgz#dbb3518d20b726524e4dd43de397eb0a95726961"
+  integrity sha512-vy7ClnArOZwCnqZgvv+ddgHgJiAFXe3Ge9ML5/mBctVJoUoYPCdxVucOywjDARn6CVoh3dRSFdPHy2sX80L0Wg==
+
+htmlparser2@^6.0.0:
+  version "6.1.0"
+  resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.1.0.tgz#c4d762b6c3371a05dbe65e94ae43a9f845fb8fb7"
+  integrity sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==
+  dependencies:
+    domelementtype "^2.0.1"
+    domhandler "^4.0.0"
+    domutils "^2.5.2"
+    entities "^2.0.0"
+
+http-cache-semantics@^4.0.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390"
+  integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==
+
+http-errors@2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3"
+  integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==
+  dependencies:
+    depd "2.0.0"
+    inherits "2.0.4"
+    setprototypeof "1.2.0"
+    statuses "2.0.1"
+    toidentifier "1.0.1"
+
+http-proxy@^1.18.0:
+  version "1.18.1"
+  resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549"
+  integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==
+  dependencies:
+    eventemitter3 "^4.0.0"
+    follow-redirects "^1.0.0"
+    requires-port "^1.0.0"
+
+http-server@^13.0.0:
+  version "13.1.0"
+  resolved "https://registry.yarnpkg.com/http-server/-/http-server-13.1.0.tgz#ef2ceb4efd08da87e93ac0f33727ecb9f3f4d629"
+  integrity sha512-MLqBMXeY/YN0FYMz4ifeOQCcg8pKj8YdmzX1pr/Vb2VrNnbxHN1s4K9BuZRVSyK/j3DQ8UVrrABb8m6EmFjWog==
+  dependencies:
+    basic-auth "^1.0.3"
+    chalk "^4.1.2"
+    corser "^2.0.1"
+    he "^1.1.0"
+    http-proxy "^1.18.0"
+    mime "^1.6.0"
+    minimist "^1.2.5"
+    opener "^1.5.1"
+    portfinder "^1.0.25"
+    secure-compare "3.0.1"
+    union "~0.5.0"
+    url-join "^2.0.5"
+
+http-signature@~1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1"
+  integrity sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==
+  dependencies:
+    assert-plus "^1.0.0"
+    jsprim "^1.2.2"
+    sshpk "^1.7.0"
+
+http-status-codes@2.2.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/http-status-codes/-/http-status-codes-2.2.0.tgz#bb2efe63d941dfc2be18e15f703da525169622be"
+  integrity sha512-feERVo9iWxvnejp3SEfm/+oNG517npqL2/PIA8ORjyOZjGC7TwCRQsZylciLS64i6pJ0wRYz3rkXLRwbtFa8Ng==
+
+https-proxy-agent@5.0.1, https-proxy-agent@^5.0.0:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6"
+  integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==
+  dependencies:
+    agent-base "6"
+    debug "4"
+
+iconv-lite@0.4.24, iconv-lite@^0.4.24:
+  version "0.4.24"
+  resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
+  integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
+  dependencies:
+    safer-buffer ">= 2.1.2 < 3"
+
+iconv-lite@0.6:
+  version "0.6.3"
+  resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501"
+  integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==
+  dependencies:
+    safer-buffer ">= 2.1.2 < 3.0.0"
+
+icss-utils@^5.0.0, icss-utils@^5.1.0:
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae"
+  integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==
+
+ieee754@^1.1.13:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
+  integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
+
+ignore@^4.0.6:
+  version "4.0.6"
+  resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"
+  integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==
+
+ignore@^5.1.1, ignore@^5.1.8, ignore@^5.2.0:
+  version "5.2.0"
+  resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a"
+  integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==
+
+immediate@^3.2.3:
+  version "3.3.0"
+  resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.3.0.tgz#1aef225517836bcdf7f2a2de2600c79ff0269266"
+  integrity sha512-HR7EVodfFUdQCTIeySw+WDRFJlPcLOJbXfwwZ7Oom6tjsvZ3bOkCDJHehQC3nxJrv7+f9XecwazynjU8e4Vw3Q==
+
+import-fresh@^3.0.0, import-fresh@^3.2.1:
+  version "3.3.0"
+  resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b"
+  integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==
+  dependencies:
+    parent-module "^1.0.0"
+    resolve-from "^4.0.0"
+
+import-lazy@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-4.0.0.tgz#e8eb627483a0a43da3c03f3e35548be5cb0cc153"
+  integrity sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==
+
+import-local@^3.0.2:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.1.0.tgz#b4479df8a5fd44f6cdce24070675676063c95cb4"
+  integrity sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==
+  dependencies:
+    pkg-dir "^4.2.0"
+    resolve-cwd "^3.0.0"
+
+imurmurhash@^0.1.4:
+  version "0.1.4"
+  resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
+  integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==
+
+indent-string@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251"
+  integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==
+
+infer-owner@^1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/infer-owner/-/infer-owner-1.0.4.tgz#c4cefcaa8e51051c2a40ba2ce8a3d27295af9467"
+  integrity sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==
+
+inflight@^1.0.4:
+  version "1.0.6"
+  resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
+  integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==
+  dependencies:
+    once "^1.3.0"
+    wrappy "1"
+
+inherits@2, inherits@2.0.4, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3:
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
+  integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
+
+inherits@2.0.3:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
+  integrity sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==
+
+ini@^1.3.5, ini@~1.3.0:
+  version "1.3.8"
+  resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c"
+  integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==
+
+inquirer@^7.1.0:
+  version "7.3.3"
+  resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.3.3.tgz#04d176b2af04afc157a83fd7c100e98ee0aad003"
+  integrity sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==
+  dependencies:
+    ansi-escapes "^4.2.1"
+    chalk "^4.1.0"
+    cli-cursor "^3.1.0"
+    cli-width "^3.0.0"
+    external-editor "^3.0.3"
+    figures "^3.0.0"
+    lodash "^4.17.19"
+    mute-stream "0.0.8"
+    run-async "^2.4.0"
+    rxjs "^6.6.0"
+    string-width "^4.1.0"
+    strip-ansi "^6.0.0"
+    through "^2.3.6"
+
+internal-slot@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.3.tgz#7347e307deeea2faac2ac6205d4bc7d34967f59c"
+  integrity sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==
+  dependencies:
+    get-intrinsic "^1.1.0"
+    has "^1.0.3"
+    side-channel "^1.0.4"
+
+"internmap@1 - 2":
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/internmap/-/internmap-2.0.3.tgz#6685f23755e43c524e251d29cbc97248e3061009"
+  integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==
+
+interpret@^2.2.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/interpret/-/interpret-2.2.0.tgz#1a78a0b5965c40a5416d007ad6f50ad27c417df9"
+  integrity sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==
+
+ip-regex@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9"
+  integrity sha512-58yWmlHpp7VYfcdTwMTvwMmqx/Elfxjd9RXTDyMsbL7lLWmhMylLEqiYVLKuLzOZqVgiWXD9MfR62Vv89VRxkw==
+
+ipaddr.js@1.9.1:
+  version "1.9.1"
+  resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3"
+  integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==
+
+is-arguments@^1.0.4:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b"
+  integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==
+  dependencies:
+    call-bind "^1.0.2"
+    has-tostringtag "^1.0.0"
+
+is-arrayish@^0.2.1:
+  version "0.2.1"
+  resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
+  integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==
+
+is-bigint@^1.0.1:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3"
+  integrity sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==
+  dependencies:
+    has-bigints "^1.0.1"
+
+is-boolean-object@^1.1.0:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719"
+  integrity sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==
+  dependencies:
+    call-bind "^1.0.2"
+    has-tostringtag "^1.0.0"
+
+is-callable@^1.1.4, is-callable@^1.2.4:
+  version "1.2.4"
+  resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.4.tgz#47301d58dd0259407865547853df6d61fe471945"
+  integrity sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==
+
+is-core-module@^2.5.0, is-core-module@^2.8.1:
+  version "2.9.0"
+  resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.9.0.tgz#e1c34429cd51c6dd9e09e0799e396e27b19a9c69"
+  integrity sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==
+  dependencies:
+    has "^1.0.3"
+
+is-date-object@^1.0.1:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f"
+  integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==
+  dependencies:
+    has-tostringtag "^1.0.0"
+
+is-extglob@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
+  integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==
+
+is-fullwidth-code-point@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
+  integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
+
+is-glob@^4.0.0, is-glob@^4.0.1:
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
+  integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
+  dependencies:
+    is-extglob "^2.1.1"
+
+is-negative-zero@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.2.tgz#7bf6f03a28003b8b3965de3ac26f664d765f3150"
+  integrity sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==
+
+is-number-object@^1.0.4:
+  version "1.0.7"
+  resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.7.tgz#59d50ada4c45251784e9904f5246c742f07a42fc"
+  integrity sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==
+  dependencies:
+    has-tostringtag "^1.0.0"
+
+is-number@^7.0.0:
+  version "7.0.0"
+  resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
+  integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
+
+is-plain-obj@2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287"
+  integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==
+
+is-plain-obj@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e"
+  integrity sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==
+
+is-plain-object@^2.0.4:
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677"
+  integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==
+  dependencies:
+    isobject "^3.0.1"
+
+is-plain-object@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344"
+  integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==
+
+is-promise@^2.1.0, is-promise@^2.2.2:
+  version "2.2.2"
+  resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.2.2.tgz#39ab959ccbf9a774cf079f7b40c7a26f763135f1"
+  integrity sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==
+
+is-regex@^1.0.4, is-regex@^1.1.4:
+  version "1.1.4"
+  resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958"
+  integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==
+  dependencies:
+    call-bind "^1.0.2"
+    has-tostringtag "^1.0.0"
+
+is-regexp@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-2.1.0.tgz#cd734a56864e23b956bf4e7c66c396a4c0b22c2d"
+  integrity sha512-OZ4IlER3zmRIoB9AqNhEggVxqIH4ofDns5nRrPS6yQxXE1TPCUpFznBfRQmQa8uC+pXqjMnukiJBxCisIxiLGA==
+
+is-shared-array-buffer@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz#8f259c573b60b6a32d4058a1a07430c0a7344c79"
+  integrity sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==
+  dependencies:
+    call-bind "^1.0.2"
+
+is-string@^1.0.5, is-string@^1.0.7:
+  version "1.0.7"
+  resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd"
+  integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==
+  dependencies:
+    has-tostringtag "^1.0.0"
+
+is-symbol@^1.0.2, is-symbol@^1.0.3:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c"
+  integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==
+  dependencies:
+    has-symbols "^1.0.2"
+
+is-typedarray@~1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
+  integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==
+
+is-weakref@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2"
+  integrity sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==
+  dependencies:
+    call-bind "^1.0.2"
+
+isarray@~1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
+  integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==
+
+isexe@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
+  integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
+
+isobject@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
+  integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==
+
+isomorphic.js@^0.2.4:
+  version "0.2.5"
+  resolved "https://registry.yarnpkg.com/isomorphic.js/-/isomorphic.js-0.2.5.tgz#13eecf36f2dba53e85d355e11bf9d4208c6f7f88"
+  integrity sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==
+
+isstream@~0.1.2:
+  version "0.1.2"
+  resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
+  integrity sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==
+
+jest-worker@^26.5.0:
+  version "26.6.2"
+  resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.6.2.tgz#7f72cbc4d643c365e27b9fd775f9d0eaa9c7a8ed"
+  integrity sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==
+  dependencies:
+    "@types/node" "*"
+    merge-stream "^2.0.0"
+    supports-color "^7.0.0"
+
+jest-worker@^27.4.5:
+  version "27.5.1"
+  resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.5.1.tgz#8d146f0900e8973b106b6f73cc1e9a8cb86f8db0"
+  integrity sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==
+  dependencies:
+    "@types/node" "*"
+    merge-stream "^2.0.0"
+    supports-color "^8.0.0"
+
+"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
+  integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
+
+js-yaml@4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602"
+  integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==
+  dependencies:
+    argparse "^2.0.1"
+
+js-yaml@^3.13.1:
+  version "3.14.1"
+  resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537"
+  integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==
+  dependencies:
+    argparse "^1.0.7"
+    esprima "^4.0.0"
+
+jsbn@~0.1.0:
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
+  integrity sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==
+
+jsdom@15.2.1:
+  version "15.2.1"
+  resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-15.2.1.tgz#d2feb1aef7183f86be521b8c6833ff5296d07ec5"
+  integrity sha512-fAl1W0/7T2G5vURSyxBzrJ1LSdQn6Tr5UX/xD4PXDx/PDgwygedfW6El/KIj3xJ7FU61TTYnc/l/B7P49Eqt6g==
+  dependencies:
+    abab "^2.0.0"
+    acorn "^7.1.0"
+    acorn-globals "^4.3.2"
+    array-equal "^1.0.0"
+    cssom "^0.4.1"
+    cssstyle "^2.0.0"
+    data-urls "^1.1.0"
+    domexception "^1.0.1"
+    escodegen "^1.11.1"
+    html-encoding-sniffer "^1.0.2"
+    nwsapi "^2.2.0"
+    parse5 "5.1.0"
+    pn "^1.1.0"
+    request "^2.88.0"
+    request-promise-native "^1.0.7"
+    saxes "^3.1.9"
+    symbol-tree "^3.2.2"
+    tough-cookie "^3.0.1"
+    w3c-hr-time "^1.0.1"
+    w3c-xmlserializer "^1.1.2"
+    webidl-conversions "^4.0.2"
+    whatwg-encoding "^1.0.5"
+    whatwg-mimetype "^2.3.0"
+    whatwg-url "^7.0.0"
+    ws "^7.0.0"
+    xml-name-validator "^3.0.0"
+
+json-buffer@3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898"
+  integrity sha512-CuUqjv0FUZIdXkHPI8MezCnFCdaTAacej1TZYulLoAg1h/PhwkdXFN4V/gzY4g+fMBCOV2xF+rp7t2XD2ns/NQ==
+
+json-parse-better-errors@^1.0.1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9"
+  integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==
+
+json-parse-even-better-errors@^2.3.0, json-parse-even-better-errors@^2.3.1:
+  version "2.3.1"
+  resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d"
+  integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==
+
+json-schema-compare@^0.2.2:
+  version "0.2.2"
+  resolved "https://registry.yarnpkg.com/json-schema-compare/-/json-schema-compare-0.2.2.tgz#dd601508335a90c7f4cfadb6b2e397225c908e56"
+  integrity sha512-c4WYmDKyJXhs7WWvAWm3uIYnfyWFoIp+JEoX34rctVvEkMYCPGhXtvmFFXiffBbxfZsvQ0RNnV5H7GvDF5HCqQ==
+  dependencies:
+    lodash "^4.17.4"
+
+json-schema-merge-allof@^0.6.0:
+  version "0.6.0"
+  resolved "https://registry.yarnpkg.com/json-schema-merge-allof/-/json-schema-merge-allof-0.6.0.tgz#64d48820fec26b228db837475ce3338936bf59a5"
+  integrity sha512-LEw4VMQVRceOPLuGRWcxW5orTTiR9ZAtqTAe4rQUjNADTeR81bezBVFa0MqIwp0YmHIM1KkhSjZM7o+IQhaPbQ==
+  dependencies:
+    compute-lcm "^1.1.0"
+    json-schema-compare "^0.2.2"
+    lodash "^4.17.4"
+
+json-schema-traverse@^0.4.1:
+  version "0.4.1"
+  resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
+  integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==
+
+json-schema-traverse@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2"
+  integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==
+
+json-schema@0.4.0:
+  version "0.4.0"
+  resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.4.0.tgz#f7de4cf6efab838ebaeb3236474cbba5a1930ab5"
+  integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==
+
+json-stable-stringify-without-jsonify@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
+  integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==
+
+json-stringify-pretty-compact@~3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/json-stringify-pretty-compact/-/json-stringify-pretty-compact-3.0.0.tgz#f71ef9d82ef16483a407869556588e91b681d9ab"
+  integrity sha512-Rc2suX5meI0S3bfdZuA7JMFBGkJ875ApfVyq2WHELjBiiG22My/l7/8zPpH/CfFVQHuVLd8NLR0nv6vi0BYYKA==
+
+json-stringify-safe@~5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
+  integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==
+
+json5@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe"
+  integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==
+  dependencies:
+    minimist "^1.2.0"
+
+json5@^2.1.1, json5@^2.1.2:
+  version "2.2.1"
+  resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.1.tgz#655d50ed1e6f95ad1a3caababd2b0efda10b395c"
+  integrity sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==
+
+jsonfile@^6.0.1:
+  version "6.1.0"
+  resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae"
+  integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==
+  dependencies:
+    universalify "^2.0.0"
+  optionalDependencies:
+    graceful-fs "^4.1.6"
+
+jsonparse@^1.2.0:
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280"
+  integrity sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==
+
+jsonpointer@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-5.0.0.tgz#f802669a524ec4805fa7389eadbc9921d5dc8072"
+  integrity sha512-PNYZIdMjVIvVgDSYKTT63Y+KZ6IZvGRNNWcxwD+GNnUz1MKPfv30J8ueCjdwcN0nDx2SlshgyB7Oy0epAzVRRg==
+
+jsonwebtoken@8.5.1:
+  version "8.5.1"
+  resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d"
+  integrity sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==
+  dependencies:
+    jws "^3.2.2"
+    lodash.includes "^4.3.0"
+    lodash.isboolean "^3.0.3"
+    lodash.isinteger "^4.0.4"
+    lodash.isnumber "^3.0.3"
+    lodash.isplainobject "^4.0.6"
+    lodash.isstring "^4.0.1"
+    lodash.once "^4.0.0"
+    ms "^2.1.1"
+    semver "^5.6.0"
+
+jsprim@^1.2.2:
+  version "1.4.2"
+  resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.2.tgz#712c65533a15c878ba59e9ed5f0e26d5b77c5feb"
+  integrity sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==
+  dependencies:
+    assert-plus "1.0.0"
+    extsprintf "1.3.0"
+    json-schema "0.4.0"
+    verror "1.10.0"
+
+jwa@^1.4.1:
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a"
+  integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==
+  dependencies:
+    buffer-equal-constant-time "1.0.1"
+    ecdsa-sig-formatter "1.0.11"
+    safe-buffer "^5.0.1"
+
+jws@^3.2.2:
+  version "3.2.2"
+  resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304"
+  integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==
+  dependencies:
+    jwa "^1.4.1"
+    safe-buffer "^5.0.1"
+
+keygrip@~1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/keygrip/-/keygrip-1.1.0.tgz#871b1681d5e159c62a445b0c74b615e0917e7226"
+  integrity sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==
+  dependencies:
+    tsscmp "1.0.6"
+
+keyv@^3.0.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.1.0.tgz#ecc228486f69991e49e9476485a5be1e8fc5c4d9"
+  integrity sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==
+  dependencies:
+    json-buffer "3.0.0"
+
+kind-of@^6.0.2, kind-of@^6.0.3:
+  version "6.0.3"
+  resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd"
+  integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==
+
+kleur@4.1.4:
+  version "4.1.4"
+  resolved "https://registry.yarnpkg.com/kleur/-/kleur-4.1.4.tgz#8c202987d7e577766d039a8cd461934c01cda04d"
+  integrity sha512-8QADVssbrFjivHWQU7KkMgptGTl6WAcSdlbBPY4uNF+mWr6DGcKrvY2w4FQJoXch7+fKMjj0dRrL75vk3k23OA==
+
+known-css-properties@^0.25.0:
+  version "0.25.0"
+  resolved "https://registry.yarnpkg.com/known-css-properties/-/known-css-properties-0.25.0.tgz#6ebc4d4b412f602e5cfbeb4086bd544e34c0a776"
+  integrity sha512-b0/9J1O9Jcyik1GC6KC42hJ41jKwdO/Mq8Mdo5sYN+IuRTXs2YFHZC3kZSx6ueusqa95x3wLYe/ytKjbAfGixA==
+
+level-codec@^9.0.0:
+  version "9.0.2"
+  resolved "https://registry.yarnpkg.com/level-codec/-/level-codec-9.0.2.tgz#fd60df8c64786a80d44e63423096ffead63d8cbc"
+  integrity sha512-UyIwNb1lJBChJnGfjmO0OR+ezh2iVu1Kas3nvBS/BzGnx79dv6g7unpKIDNPMhfdTEGoc7mC8uAu51XEtX+FHQ==
+  dependencies:
+    buffer "^5.6.0"
+
+level-concat-iterator@~2.0.0:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/level-concat-iterator/-/level-concat-iterator-2.0.1.tgz#1d1009cf108340252cb38c51f9727311193e6263"
+  integrity sha512-OTKKOqeav2QWcERMJR7IS9CUo1sHnke2C0gkSmcR7QuEtFNLLzHQAvnMw8ykvEcv0Qtkg0p7FOwP1v9e5Smdcw==
+
+level-errors@^2.0.0, level-errors@~2.0.0:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/level-errors/-/level-errors-2.0.1.tgz#2132a677bf4e679ce029f517c2f17432800c05c8"
+  integrity sha512-UVprBJXite4gPS+3VznfgDSU8PTRuVX0NXwoWW50KLxd2yw4Y1t2JUR5In1itQnudZqRMT9DlAM3Q//9NCjCFw==
+  dependencies:
+    errno "~0.1.1"
+
+level-iterator-stream@~4.0.0:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/level-iterator-stream/-/level-iterator-stream-4.0.2.tgz#7ceba69b713b0d7e22fcc0d1f128ccdc8a24f79c"
+  integrity sha512-ZSthfEqzGSOMWoUGhTXdX9jv26d32XJuHz/5YnuHZzH6wldfWMOVwI9TBtKcya4BKTyTt3XVA0A3cF3q5CY30Q==
+  dependencies:
+    inherits "^2.0.4"
+    readable-stream "^3.4.0"
+    xtend "^4.0.2"
+
+level-js@^5.0.0:
+  version "5.0.2"
+  resolved "https://registry.yarnpkg.com/level-js/-/level-js-5.0.2.tgz#5e280b8f93abd9ef3a305b13faf0b5397c969b55"
+  integrity sha512-SnBIDo2pdO5VXh02ZmtAyPP6/+6YTJg2ibLtl9C34pWvmtMEmRTWpra+qO/hifkUtBTOtfx6S9vLDjBsBK4gRg==
+  dependencies:
+    abstract-leveldown "~6.2.3"
+    buffer "^5.5.0"
+    inherits "^2.0.3"
+    ltgt "^2.1.2"
+
+level-packager@^5.1.0:
+  version "5.1.1"
+  resolved "https://registry.yarnpkg.com/level-packager/-/level-packager-5.1.1.tgz#323ec842d6babe7336f70299c14df2e329c18939"
+  integrity sha512-HMwMaQPlTC1IlcwT3+swhqf/NUO+ZhXVz6TY1zZIIZlIR0YSn8GtAAWmIvKjNY16ZkEg/JcpAuQskxsXqC0yOQ==
+  dependencies:
+    encoding-down "^6.3.0"
+    levelup "^4.3.2"
+
+level-supports@~1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/level-supports/-/level-supports-1.0.1.tgz#2f530a596834c7301622521988e2c36bb77d122d"
+  integrity sha512-rXM7GYnW8gsl1vedTJIbzOrRv85c/2uCMpiiCzO2fndd06U/kUXEEU9evYn4zFggBOg36IsBW8LzqIpETwwQzg==
+  dependencies:
+    xtend "^4.0.2"
+
+level@^6.0.1:
+  version "6.0.1"
+  resolved "https://registry.yarnpkg.com/level/-/level-6.0.1.tgz#dc34c5edb81846a6de5079eac15706334b0d7cd6"
+  integrity sha512-psRSqJZCsC/irNhfHzrVZbmPYXDcEYhA5TVNwr+V92jF44rbf86hqGp8fiT702FyiArScYIlPSBTDUASCVNSpw==
+  dependencies:
+    level-js "^5.0.0"
+    level-packager "^5.1.0"
+    leveldown "^5.4.0"
+
+leveldown@^5.4.0:
+  version "5.6.0"
+  resolved "https://registry.yarnpkg.com/leveldown/-/leveldown-5.6.0.tgz#16ba937bb2991c6094e13ac5a6898ee66d3eee98"
+  integrity sha512-iB8O/7Db9lPaITU1aA2txU/cBEXAt4vWwKQRrrWuS6XDgbP4QZGj9BL2aNbwb002atoQ/lIotJkfyzz+ygQnUQ==
+  dependencies:
+    abstract-leveldown "~6.2.1"
+    napi-macros "~2.0.0"
+    node-gyp-build "~4.1.0"
+
+levelup@^4.3.2:
+  version "4.4.0"
+  resolved "https://registry.yarnpkg.com/levelup/-/levelup-4.4.0.tgz#f89da3a228c38deb49c48f88a70fb71f01cafed6"
+  integrity sha512-94++VFO3qN95cM/d6eBXvd894oJE0w3cInq9USsyQzzoJxmiYzPAocNcuGCPGGjoXqDVJcr3C1jzt1TSjyaiLQ==
+  dependencies:
+    deferred-leveldown "~5.3.0"
+    level-errors "~2.0.0"
+    level-iterator-stream "~4.0.0"
+    level-supports "~1.0.0"
+    xtend "~4.0.0"
+
+levn@^0.4.1:
+  version "0.4.1"
+  resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade"
+  integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==
+  dependencies:
+    prelude-ls "^1.2.1"
+    type-check "~0.4.0"
+
+levn@~0.3.0:
+  version "0.3.0"
+  resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee"
+  integrity sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==
+  dependencies:
+    prelude-ls "~1.1.2"
+    type-check "~0.3.2"
+
+lib0@^0.2.31, lib0@^0.2.42, lib0@^0.2.49:
+  version "0.2.51"
+  resolved "https://registry.yarnpkg.com/lib0/-/lib0-0.2.51.tgz#23b1271a26f39120a4d0f86b9dfb44577f5ce98c"
+  integrity sha512-05Erb3465CxJa38LQlMz4EbetNvRna1S3BzqEjC0/pmp5cQuQSfNNmeS0722Wev1dRlMUp2Cql0gQ55krSXf2Q==
+  dependencies:
+    isomorphic.js "^0.2.4"
+
+license-webpack-plugin@^2.3.14:
+  version "2.3.21"
+  resolved "https://registry.yarnpkg.com/license-webpack-plugin/-/license-webpack-plugin-2.3.21.tgz#152f5e82d5f51f8bab78905731f2b8042aa5691b"
+  integrity sha512-rVaYU9TddZN3ao8M/0PrRSCdTp2EW6VQymlgsuScld1vef0Ou7fALx3ePe83KLP3xAEDcPK5fkqUVqGBnbz1zQ==
+  dependencies:
+    "@types/webpack-sources" "^0.1.5"
+    webpack-sources "^1.2.0"
+
+lines-and-columns@^1.1.6:
+  version "1.2.4"
+  resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632"
+  integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==
+
+load-json-file@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b"
+  integrity sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==
+  dependencies:
+    graceful-fs "^4.1.2"
+    parse-json "^4.0.0"
+    pify "^3.0.0"
+    strip-bom "^3.0.0"
+
+loader-runner@^4.2.0:
+  version "4.3.0"
+  resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.0.tgz#c1b4a163b99f614830353b16755e7149ac2314e1"
+  integrity sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==
+
+loader-utils@^1.0.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.0.tgz#c579b5e34cb34b1a74edc6c1fb36bfa371d5a613"
+  integrity sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==
+  dependencies:
+    big.js "^5.2.2"
+    emojis-list "^3.0.0"
+    json5 "^1.0.1"
+
+loader-utils@^2.0.0, loader-utils@~2.0.0:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.2.tgz#d6e3b4fb81870721ae4e0868ab11dd638368c129"
+  integrity sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==
+  dependencies:
+    big.js "^5.2.2"
+    emojis-list "^3.0.0"
+    json5 "^2.1.2"
+
+locate-path@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0"
+  integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==
+  dependencies:
+    p-locate "^4.1.0"
+
+lockfile@1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/lockfile/-/lockfile-1.0.4.tgz#07f819d25ae48f87e538e6578b6964a4981a5609"
+  integrity sha512-cvbTwETRfsFh4nHsL1eGWapU1XFi5Ot9E85sWAwia7Y7EgB7vfqcZhTKZ+l7hCGxSPoushMv5GKhT5PdLv03WA==
+  dependencies:
+    signal-exit "^3.0.2"
+
+lodash.debounce@^4.0.8:
+  version "4.0.8"
+  resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
+  integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==
+
+lodash.escape@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/lodash.escape/-/lodash.escape-4.0.1.tgz#c9044690c21e04294beaa517712fded1fa88de98"
+  integrity sha512-nXEOnb/jK9g0DYMr1/Xvq6l5xMD7GDG55+GSYIYmS0G4tBk/hURD4JR9WCavs04t33WmJx9kCyp9vJ+mr4BOUw==
+
+lodash.includes@^4.3.0:
+  version "4.3.0"
+  resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f"
+  integrity sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==
+
+lodash.isboolean@^3.0.3:
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6"
+  integrity sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==
+
+lodash.isinteger@^4.0.4:
+  version "4.0.4"
+  resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343"
+  integrity sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==
+
+lodash.isnumber@^3.0.3:
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc"
+  integrity sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==
+
+lodash.isplainobject@^4.0.6:
+  version "4.0.6"
+  resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb"
+  integrity sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==
+
+lodash.isstring@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451"
+  integrity sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==
+
+lodash.merge@^4.6.2:
+  version "4.6.2"
+  resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
+  integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
+
+lodash.once@^4.0.0:
+  version "4.1.1"
+  resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac"
+  integrity sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==
+
+lodash.sortby@^4.7.0:
+  version "4.7.0"
+  resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
+  integrity sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==
+
+lodash.truncate@^4.4.2:
+  version "4.4.2"
+  resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193"
+  integrity sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==
+
+lodash@4, lodash@4.17.21, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.4:
+  version "4.17.21"
+  resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
+  integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
+
+loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
+  integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
+  dependencies:
+    js-tokens "^3.0.0 || ^4.0.0"
+
+lowdb@1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/lowdb/-/lowdb-1.0.0.tgz#5243be6b22786ccce30e50c9a33eac36b20c8064"
+  integrity sha512-2+x8esE/Wb9SQ1F9IHaYWfsC9FIecLOPrK4g17FGEayjUWH172H6nwicRovGvSE2CPZouc2MCIqCI7h9d+GftQ==
+  dependencies:
+    graceful-fs "^4.1.3"
+    is-promise "^2.1.0"
+    lodash "4"
+    pify "^3.0.0"
+    steno "^0.4.1"
+
+lowercase-keys@^1.0.0, lowercase-keys@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f"
+  integrity sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==
+
+lowercase-keys@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479"
+  integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==
+
+lru-cache@7.10.1:
+  version "7.10.1"
+  resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.10.1.tgz#db577f42a94c168f676b638d15da8fb073448cab"
+  integrity sha512-BQuhQxPuRl79J5zSXRP+uNzPOyZw2oFI9JLRQ80XswSvg21KMKNtQza9eF42rfI/3Z40RvzBdXgziEkudzjo8A==
+
+lru-cache@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
+  integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==
+  dependencies:
+    yallist "^4.0.0"
+
+lru-queue@^0.1.0:
+  version "0.1.0"
+  resolved "https://registry.yarnpkg.com/lru-queue/-/lru-queue-0.1.0.tgz#2738bd9f0d3cf4f84490c5736c48699ac632cda3"
+  integrity sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ==
+  dependencies:
+    es5-ext "~0.10.2"
+
+ltgt@^2.1.2:
+  version "2.2.1"
+  resolved "https://registry.yarnpkg.com/ltgt/-/ltgt-2.2.1.tgz#f35ca91c493f7b73da0e07495304f17b31f87ee5"
+  integrity sha512-AI2r85+4MquTw9ZYqabu4nMwy9Oftlfa/e/52t9IjtfG+mGBbTNdAoZ3RQKLHR6r0wQnwZnPIEh/Ya6XTWAKNA==
+
+lunr-mutable-indexes@2.3.2:
+  version "2.3.2"
+  resolved "https://registry.yarnpkg.com/lunr-mutable-indexes/-/lunr-mutable-indexes-2.3.2.tgz#864253489735d598c5140f3fb75c0a5c8be2e98c"
+  integrity sha512-Han6cdWAPPFM7C2AigS2Ofl3XjAT0yVMrUixodJEpyg71zCtZ2yzXc3s+suc/OaNt4ca6WJBEzVnEIjxCTwFMw==
+  dependencies:
+    lunr ">= 2.3.0 < 2.4.0"
+
+"lunr@>= 2.3.0 < 2.4.0":
+  version "2.3.9"
+  resolved "https://registry.yarnpkg.com/lunr/-/lunr-2.3.9.tgz#18b123142832337dd6e964df1a5a7707b25d35e1"
+  integrity sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==
+
+make-dir@^3.0.2, make-dir@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f"
+  integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==
+  dependencies:
+    semver "^6.0.0"
+
+map-obj@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d"
+  integrity sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==
+
+map-obj@^4.0.0:
+  version "4.3.0"
+  resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-4.3.0.tgz#9304f906e93faae70880da102a9f1df0ea8bb05a"
+  integrity sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==
+
+marked@4.0.16:
+  version "4.0.16"
+  resolved "https://registry.yarnpkg.com/marked/-/marked-4.0.16.tgz#9ec18fc1a723032eb28666100344d9428cf7a264"
+  integrity sha512-wahonIQ5Jnyatt2fn8KqF/nIqZM8mh3oRu2+l5EANGMhu6RFjiSG52QNE2eWzFMI94HqYSgN184NurgNG6CztA==
+
+marked@4.0.17, marked@^4.0.10:
+  version "4.0.17"
+  resolved "https://registry.yarnpkg.com/marked/-/marked-4.0.17.tgz#1186193d85bb7882159cdcfc57d1dfccaffb3fe9"
+  integrity sha512-Wfk0ATOK5iPxM4ptrORkFemqroz0ZDxp5MWfYA7H/F+wO17NRWV5Ypxi6p3g2Xmw2bKeiYOl6oVnLHKxBA0VhA==
+
+mathml-tag-names@^2.1.3:
+  version "2.1.3"
+  resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3"
+  integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==
+
+media-typer@0.3.0:
+  version "0.3.0"
+  resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
+  integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==
+
+memoizee@0.4.15:
+  version "0.4.15"
+  resolved "https://registry.yarnpkg.com/memoizee/-/memoizee-0.4.15.tgz#e6f3d2da863f318d02225391829a6c5956555b72"
+  integrity sha512-UBWmJpLZd5STPm7PMUlOw/TSy972M+z8gcyQ5veOnSDRREz/0bmpyTfKt3/51DhEBqCZQn1udM/5flcSPYhkdQ==
+  dependencies:
+    d "^1.0.1"
+    es5-ext "^0.10.53"
+    es6-weak-map "^2.0.3"
+    event-emitter "^0.3.5"
+    is-promise "^2.2.2"
+    lru-queue "^0.1.0"
+    next-tick "^1.1.0"
+    timers-ext "^0.1.7"
+
+memorystream@^0.3.1:
+  version "0.3.1"
+  resolved "https://registry.yarnpkg.com/memorystream/-/memorystream-0.3.1.tgz#86d7090b30ce455d63fbae12dda51a47ddcaf9b2"
+  integrity sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==
+
+meow@^9.0.0:
+  version "9.0.0"
+  resolved "https://registry.yarnpkg.com/meow/-/meow-9.0.0.tgz#cd9510bc5cac9dee7d03c73ee1f9ad959f4ea364"
+  integrity sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ==
+  dependencies:
+    "@types/minimist" "^1.2.0"
+    camelcase-keys "^6.2.2"
+    decamelize "^1.2.0"
+    decamelize-keys "^1.1.0"
+    hard-rejection "^2.1.0"
+    minimist-options "4.1.0"
+    normalize-package-data "^3.0.0"
+    read-pkg-up "^7.0.1"
+    redent "^3.0.0"
+    trim-newlines "^3.0.0"
+    type-fest "^0.18.0"
+    yargs-parser "^20.2.3"
+
+merge-descriptors@1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
+  integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==
+
+merge-stream@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
+  integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==
+
+merge2@^1.2.3, merge2@^1.3.0, merge2@^1.4.1:
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
+  integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
+
+methods@~1.1.2:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
+  integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==
+
+micromatch@^4.0.4, micromatch@^4.0.5:
+  version "4.0.5"
+  resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6"
+  integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==
+  dependencies:
+    braces "^3.0.2"
+    picomatch "^2.3.1"
+
+mime-db@1.52.0, "mime-db@>= 1.43.0 < 2":
+  version "1.52.0"
+  resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
+  integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
+
+mime-types@^2.1.12, mime-types@^2.1.27, mime-types@~2.1.19, mime-types@~2.1.24, mime-types@~2.1.34:
+  version "2.1.35"
+  resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
+  integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
+  dependencies:
+    mime-db "1.52.0"
+
+mime@1.6.0, mime@^1.6.0:
+  version "1.6.0"
+  resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
+  integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
+
+mime@3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/mime/-/mime-3.0.0.tgz#b374550dca3a0c18443b0c950a6a58f1931cf7a7"
+  integrity sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==
+
+mimic-fn@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
+  integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
+
+mimic-response@^1.0.0, mimic-response@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b"
+  integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==
+
+mimic-response@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-2.1.0.tgz#d13763d35f613d09ec37ebb30bac0469c0ee8f43"
+  integrity sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==
+
+min-indent@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869"
+  integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==
+
+mini-css-extract-plugin@~1.3.2:
+  version "1.3.9"
+  resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-1.3.9.tgz#47a32132b0fd97a119acd530e8421e8f6ab16d5e"
+  integrity sha512-Ac4s+xhVbqlyhXS5J/Vh/QXUz3ycXlCqoCPpg0vdfhsIBH9eg/It/9L1r1XhSCH737M1lqcWnMuWL13zcygn5A==
+  dependencies:
+    loader-utils "^2.0.0"
+    schema-utils "^3.0.0"
+    webpack-sources "^1.1.0"
+
+"minimatch@2 || 3", minimatch@^3.0.4, minimatch@^3.1.1:
+  version "3.1.2"
+  resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
+  integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
+  dependencies:
+    brace-expansion "^1.1.7"
+
+minimatch@5.1.0:
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.0.tgz#1717b464f4971b144f6aabe8f2d0b8e4511e09c7"
+  integrity sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==
+  dependencies:
+    brace-expansion "^2.0.1"
+
+minimatch@~3.0.4:
+  version "3.0.8"
+  resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.8.tgz#5e6a59bd11e2ab0de1cfb843eb2d82e546c321c1"
+  integrity sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==
+  dependencies:
+    brace-expansion "^1.1.7"
+
+minimist-options@4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/minimist-options/-/minimist-options-4.1.0.tgz#c0655713c53a8a2ebd77ffa247d342c40f010619"
+  integrity sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==
+  dependencies:
+    arrify "^1.0.1"
+    is-plain-obj "^1.1.0"
+    kind-of "^6.0.3"
+
+minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6, minimist@~1.2.0:
+  version "1.2.6"
+  resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
+  integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
+
+minipass-collect@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/minipass-collect/-/minipass-collect-1.0.2.tgz#22b813bf745dc6edba2576b940022ad6edc8c617"
+  integrity sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==
+  dependencies:
+    minipass "^3.0.0"
+
+minipass-flush@^1.0.5:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/minipass-flush/-/minipass-flush-1.0.5.tgz#82e7135d7e89a50ffe64610a787953c4c4cbb373"
+  integrity sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==
+  dependencies:
+    minipass "^3.0.0"
+
+minipass-pipeline@^1.2.2:
+  version "1.2.4"
+  resolved "https://registry.yarnpkg.com/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz#68472f79711c084657c067c5c6ad93cddea8214c"
+  integrity sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==
+  dependencies:
+    minipass "^3.0.0"
+
+minipass@^3.0.0, minipass@^3.1.1:
+  version "3.1.6"
+  resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.6.tgz#3b8150aa688a711a1521af5e8779c1d3bb4f45ee"
+  integrity sha512-rty5kpw9/z8SX9dmxblFA6edItUmwJgMeYDZRrwlIVN27i8gysGbznJwUggw2V/FVqFSDdWy040ZPS811DYAqQ==
+  dependencies:
+    yallist "^4.0.0"
+
+minizlib@^2.1.1:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931"
+  integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==
+  dependencies:
+    minipass "^3.0.0"
+    yallist "^4.0.0"
+
+mkdirp@1.0.4, mkdirp@^1.0.3, mkdirp@^1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
+  integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
+
+mkdirp@^0.5.5, mkdirp@~0.5.1:
+  version "0.5.6"
+  resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6"
+  integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==
+  dependencies:
+    minimist "^1.2.6"
+
+moment@^2.24.0:
+  version "2.29.3"
+  resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.3.tgz#edd47411c322413999f7a5940d526de183c031f3"
+  integrity sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw==
+
+ms@2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
+  integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==
+
+ms@2.1.2:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
+  integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
+
+ms@2.1.3, ms@^2.1.1:
+  version "2.1.3"
+  resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
+  integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
+
+mute-stream@0.0.8:
+  version "0.0.8"
+  resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d"
+  integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==
+
+mv@2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/mv/-/mv-2.1.1.tgz#ae6ce0d6f6d5e0a4f7d893798d03c1ea9559b6a2"
+  integrity sha512-at/ZndSy3xEGJ8i0ygALh8ru9qy7gWW1cmkaqBN29JmMlIvM//MEO9y1sk/avxuwnPcfhkejkLsuPxH81BrkSg==
+  dependencies:
+    mkdirp "~0.5.1"
+    ncp "~2.0.0"
+    rimraf "~2.4.0"
+
+nan@^2.15.0:
+  version "2.16.0"
+  resolved "https://registry.yarnpkg.com/nan/-/nan-2.16.0.tgz#664f43e45460fb98faf00edca0bb0d7b8dce7916"
+  integrity sha512-UdAqHyFngu7TfQKsCBgAA6pWDkT8MAO7d0jyOecVhN5354xbLqdn8mV9Tat9gepAupm0bt2DbeaSC8vS52MuFA==
+
+nanoid@^3.1.23, nanoid@^3.3.4:
+  version "3.3.4"
+  resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab"
+  integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==
+
+napi-macros@~2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/napi-macros/-/napi-macros-2.0.0.tgz#2b6bae421e7b96eb687aa6c77a7858640670001b"
+  integrity sha512-A0xLykHtARfueITVDernsAWdtIMbOJgKgcluwENp3AlsKN/PloyO10HtmoqnFAQAcxPkgZN7wdfPfEd0zNGxbg==
+
+natural-compare@^1.4.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
+  integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
+
+ncp@~2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/ncp/-/ncp-2.0.0.tgz#195a21d6c46e361d2fb1281ba38b91e9df7bdbb3"
+  integrity sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA==
+
+negotiator@0.6.3:
+  version "0.6.3"
+  resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd"
+  integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==
+
+neo-async@^2.6.0, neo-async@^2.6.2:
+  version "2.6.2"
+  resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
+  integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
+
+next-tick@1, next-tick@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.1.0.tgz#1836ee30ad56d67ef281b22bd199f709449b35eb"
+  integrity sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==
+
+nice-try@^1.0.4:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
+  integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
+
+node-fetch@2.6.7, node-fetch@^2.6.0, node-fetch@^2.6.7:
+  version "2.6.7"
+  resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"
+  integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==
+  dependencies:
+    whatwg-url "^5.0.0"
+
+node-gyp-build@~4.1.0:
+  version "4.1.1"
+  resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.1.1.tgz#d7270b5d86717068d114cc57fff352f96d745feb"
+  integrity sha512-dSq1xmcPDKPZ2EED2S6zw/b9NKsqzXRE6dVr8TVQnI3FJOTteUMuqF3Qqs6LZg+mLGYJWqQzMbIjMtJqTv87nQ==
+
+node-releases@^2.0.5:
+  version "2.0.5"
+  resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.5.tgz#280ed5bc3eba0d96ce44897d8aee478bfb3d9666"
+  integrity sha512-U9h1NLROZTq9uE1SNffn6WuPDg8icmi3ns4rEl/oTfIle4iLjTliCzgTsbaIFMq/Xn078/lfY/BL0GWZ+psK4Q==
+
+nopt@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88"
+  integrity sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==
+  dependencies:
+    abbrev "1"
+
+normalize-package-data@^2.3.2, normalize-package-data@^2.5.0:
+  version "2.5.0"
+  resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8"
+  integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==
+  dependencies:
+    hosted-git-info "^2.1.4"
+    resolve "^1.10.0"
+    semver "2 || 3 || 4 || 5"
+    validate-npm-package-license "^3.0.1"
+
+normalize-package-data@^3.0.0:
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-3.0.3.tgz#dbcc3e2da59509a0983422884cd172eefdfa525e"
+  integrity sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==
+  dependencies:
+    hosted-git-info "^4.0.1"
+    is-core-module "^2.5.0"
+    semver "^7.3.4"
+    validate-npm-package-license "^3.0.1"
+
+normalize-path@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
+  integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
+
+normalize-url@^4.1.0:
+  version "4.5.1"
+  resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.1.tgz#0dd90cf1288ee1d1313b87081c9a5932ee48518a"
+  integrity sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==
+
+normalize.css@^8.0.1:
+  version "8.0.1"
+  resolved "https://registry.yarnpkg.com/normalize.css/-/normalize.css-8.0.1.tgz#9b98a208738b9cc2634caacbc42d131c97487bf3"
+  integrity sha512-qizSNPO93t1YUuUhP22btGOo3chcvDFqFaj2TRybP0DMxkHOCTYwp3n34fel4a31ORXy4m1Xq0Gyqpb5m33qIg==
+
+npm-run-all@^4.1.5:
+  version "4.1.5"
+  resolved "https://registry.yarnpkg.com/npm-run-all/-/npm-run-all-4.1.5.tgz#04476202a15ee0e2e214080861bff12a51d98fba"
+  integrity sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==
+  dependencies:
+    ansi-styles "^3.2.1"
+    chalk "^2.4.1"
+    cross-spawn "^6.0.5"
+    memorystream "^0.3.1"
+    minimatch "^3.0.4"
+    pidtree "^0.3.0"
+    read-pkg "^3.0.0"
+    shell-quote "^1.6.1"
+    string.prototype.padend "^3.0.0"
+
+npmlog@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-5.0.1.tgz#f06678e80e29419ad67ab964e0fa69959c1eb8b0"
+  integrity sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==
+  dependencies:
+    are-we-there-yet "^2.0.0"
+    console-control-strings "^1.1.0"
+    gauge "^3.0.0"
+    set-blocking "^2.0.0"
+
+nwsapi@^2.2.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.0.tgz#204879a9e3d068ff2a55139c2c772780681a38b7"
+  integrity sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==
+
+oauth-sign@~0.9.0:
+  version "0.9.0"
+  resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455"
+  integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==
+
+object-assign@^4, object-assign@^4.1.1:
+  version "4.1.1"
+  resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
+  integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==
+
+object-inspect@^1.12.0, object-inspect@^1.9.0:
+  version "1.12.2"
+  resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.2.tgz#c0641f26394532f28ab8d796ab954e43c009a8ea"
+  integrity sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==
+
+object-is@^1.0.1:
+  version "1.1.5"
+  resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.5.tgz#b9deeaa5fc7f1846a0faecdceec138e5778f53ac"
+  integrity sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==
+  dependencies:
+    call-bind "^1.0.2"
+    define-properties "^1.1.3"
+
+object-keys@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
+  integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==
+
+object.assign@^4.1.2:
+  version "4.1.2"
+  resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.2.tgz#0ed54a342eceb37b38ff76eb831a0e788cb63940"
+  integrity sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==
+  dependencies:
+    call-bind "^1.0.0"
+    define-properties "^1.1.3"
+    has-symbols "^1.0.1"
+    object-keys "^1.1.1"
+
+on-finished@2.4.1:
+  version "2.4.1"
+  resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f"
+  integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==
+  dependencies:
+    ee-first "1.1.1"
+
+on-headers@~1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f"
+  integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==
+
+once@^1.3.0, once@^1.3.1, once@^1.4.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
+  integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==
+  dependencies:
+    wrappy "1"
+
+onetime@^5.1.0:
+  version "5.1.2"
+  resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e"
+  integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==
+  dependencies:
+    mimic-fn "^2.1.0"
+
+opener@^1.5.1:
+  version "1.5.2"
+  resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598"
+  integrity sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==
+
+optionator@^0.8.1:
+  version "0.8.3"
+  resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495"
+  integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==
+  dependencies:
+    deep-is "~0.1.3"
+    fast-levenshtein "~2.0.6"
+    levn "~0.3.0"
+    prelude-ls "~1.1.2"
+    type-check "~0.3.2"
+    word-wrap "~1.2.3"
+
+optionator@^0.9.1:
+  version "0.9.1"
+  resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499"
+  integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==
+  dependencies:
+    deep-is "^0.1.3"
+    fast-levenshtein "^2.0.6"
+    levn "^0.4.1"
+    prelude-ls "^1.2.1"
+    type-check "^0.4.0"
+    word-wrap "^1.2.3"
+
+os-tmpdir@~1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
+  integrity sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==
+
+os@~0.1.1:
+  version "0.1.2"
+  resolved "https://registry.yarnpkg.com/os/-/os-0.1.2.tgz#f29a50c62908516ba42652de42f7038600cadbc2"
+  integrity sha512-ZoXJkvAnljwvc56MbvhtKVWmSkzV712k42Is2mA0+0KTSRakq5XXuXpjZjgAt9ctzl51ojhQWakQQpmOvXWfjQ==
+
+p-cancelable@^1.0.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc"
+  integrity sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==
+
+p-limit@^2.2.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1"
+  integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==
+  dependencies:
+    p-try "^2.0.0"
+
+p-limit@^3.0.2:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b"
+  integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==
+  dependencies:
+    yocto-queue "^0.1.0"
+
+p-locate@^4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07"
+  integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==
+  dependencies:
+    p-limit "^2.2.0"
+
+p-map@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b"
+  integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==
+  dependencies:
+    aggregate-error "^3.0.0"
+
+p-try@^2.0.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
+  integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
+
+package-json@^6.5.0:
+  version "6.5.0"
+  resolved "https://registry.yarnpkg.com/package-json/-/package-json-6.5.0.tgz#6feedaca35e75725876d0b0e64974697fed145b0"
+  integrity sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ==
+  dependencies:
+    got "^9.6.0"
+    registry-auth-token "^4.0.0"
+    registry-url "^5.0.0"
+    semver "^6.2.0"
+
+parent-module@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2"
+  integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==
+  dependencies:
+    callsites "^3.0.0"
+
+parse-json@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0"
+  integrity sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==
+  dependencies:
+    error-ex "^1.3.1"
+    json-parse-better-errors "^1.0.1"
+
+parse-json@^5.0.0:
+  version "5.2.0"
+  resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd"
+  integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==
+  dependencies:
+    "@babel/code-frame" "^7.0.0"
+    error-ex "^1.3.1"
+    json-parse-even-better-errors "^2.3.0"
+    lines-and-columns "^1.1.6"
+
+parse-ms@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/parse-ms/-/parse-ms-2.1.0.tgz#348565a753d4391fa524029956b172cb7753097d"
+  integrity sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA==
+
+parse-srcset@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/parse-srcset/-/parse-srcset-1.0.2.tgz#f2bd221f6cc970a938d88556abc589caaaa2bde1"
+  integrity sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==
+
+parse5@5.1.0:
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.0.tgz#c59341c9723f414c452975564c7c00a68d58acd2"
+  integrity sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ==
+
+parseurl@~1.3.3:
+  version "1.3.3"
+  resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
+  integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==
+
+path-browserify@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-1.0.1.tgz#d98454a9c3753d5790860f16f68867b9e46be1fd"
+  integrity sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==
+
+path-exists@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
+  integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==
+
+path-is-absolute@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
+  integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==
+
+path-key@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
+  integrity sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==
+
+path-key@^3.1.0:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
+  integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
+
+path-parse@^1.0.7:
+  version "1.0.7"
+  resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
+  integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
+
+path-to-regexp@0.1.7:
+  version "0.1.7"
+  resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
+  integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==
+
+path-type@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f"
+  integrity sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==
+  dependencies:
+    pify "^3.0.0"
+
+path-type@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
+  integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
+
+path@~0.12.7:
+  version "0.12.7"
+  resolved "https://registry.yarnpkg.com/path/-/path-0.12.7.tgz#d4dc2a506c4ce2197eb481ebfcd5b36c0140b10f"
+  integrity sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q==
+  dependencies:
+    process "^0.11.1"
+    util "^0.10.3"
+
+performance-now@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
+  integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==
+
+picocolors@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
+  integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
+
+picomatch@^2.3.1:
+  version "2.3.1"
+  resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
+  integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
+
+pidtree@^0.3.0:
+  version "0.3.1"
+  resolved "https://registry.yarnpkg.com/pidtree/-/pidtree-0.3.1.tgz#ef09ac2cc0533df1f3250ccf2c4d366b0d12114a"
+  integrity sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA==
+
+pify@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176"
+  integrity sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==
+
+pino-std-serializers@^3.1.0:
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/pino-std-serializers/-/pino-std-serializers-3.2.0.tgz#b56487c402d882eb96cd67c257868016b61ad671"
+  integrity sha512-EqX4pwDPrt3MuOAAUBMU0Tk5kR/YcCM5fNPEzgCO2zJ5HfX0vbiH9HbJglnyeQsN96Kznae6MWD47pZB5avTrg==
+
+pino@6.14.0:
+  version "6.14.0"
+  resolved "https://registry.yarnpkg.com/pino/-/pino-6.14.0.tgz#b745ea87a99a6c4c9b374e4f29ca7910d4c69f78"
+  integrity sha512-iuhEDel3Z3hF9Jfe44DPXR8l07bhjuFY3GMHIXbjnY9XcafbyDDwl2sN2vw2GjMPf5Nkoe+OFao7ffn9SXaKDg==
+  dependencies:
+    fast-redact "^3.0.0"
+    fast-safe-stringify "^2.0.8"
+    flatstr "^1.0.12"
+    pino-std-serializers "^3.1.0"
+    process-warning "^1.0.0"
+    quick-format-unescaped "^4.0.3"
+    sonic-boom "^1.0.2"
+
+pkg-dir@^4.1.0, pkg-dir@^4.2.0:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3"
+  integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==
+  dependencies:
+    find-up "^4.0.0"
+
+pkginfo@0.4.1:
+  version "0.4.1"
+  resolved "https://registry.yarnpkg.com/pkginfo/-/pkginfo-0.4.1.tgz#b5418ef0439de5425fc4995042dced14fb2a84ff"
+  integrity sha512-8xCNE/aT/EXKenuMDZ+xTVwkT8gsoHN2z/Q29l80u0ppGEXVvsKRzNMbtKhg8LS8k1tJLAHHylf6p4VFmP6XUQ==
+
+playwright-core@1.22.2:
+  version "1.22.2"
+  resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.22.2.tgz#ed2963d79d71c2a18d5a6fd25b60b9f0a344661a"
+  integrity sha512-w/hc/Ld0RM4pmsNeE6aL/fPNWw8BWit2tg+TfqJ3+p59c6s3B6C8mXvXrIPmfQEobkcFDc+4KirNzOQ+uBSP1Q==
+
+pn@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/pn/-/pn-1.1.0.tgz#e2f4cef0e219f463c179ab37463e4e1ecdccbafb"
+  integrity sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==
+
+popper.js@^1.14.4, popper.js@^1.16.1:
+  version "1.16.1"
+  resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.16.1.tgz#2a223cb3dc7b6213d740e40372be40de43e65b1b"
+  integrity sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==
+
+portfinder@^1.0.25:
+  version "1.0.28"
+  resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.28.tgz#67c4622852bd5374dd1dd900f779f53462fac778"
+  integrity sha512-Se+2isanIcEqf2XMHjyUKskczxbPH7dQnlMjXX6+dybayyHvAf/TCgyMRlzf/B6QDhAEFOGes0pzRo3by4AbMA==
+  dependencies:
+    async "^2.6.2"
+    debug "^3.1.1"
+    mkdirp "^0.5.5"
+
+postcss-media-query-parser@^0.2.3:
+  version "0.2.3"
+  resolved "https://registry.yarnpkg.com/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz#27b39c6f4d94f81b1a73b8f76351c609e5cef244"
+  integrity sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==
+
+postcss-modules-extract-imports@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz#cda1f047c0ae80c97dbe28c3e76a43b88025741d"
+  integrity sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==
+
+postcss-modules-local-by-default@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz#ebbb54fae1598eecfdf691a02b3ff3b390a5a51c"
+  integrity sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ==
+  dependencies:
+    icss-utils "^5.0.0"
+    postcss-selector-parser "^6.0.2"
+    postcss-value-parser "^4.1.0"
+
+postcss-modules-scope@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz#9ef3151456d3bbfa120ca44898dfca6f2fa01f06"
+  integrity sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==
+  dependencies:
+    postcss-selector-parser "^6.0.4"
+
+postcss-modules-values@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz#d7c5e7e68c3bb3c9b27cbf48ca0bb3ffb4602c9c"
+  integrity sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==
+  dependencies:
+    icss-utils "^5.0.0"
+
+postcss-resolve-nested-selector@^0.1.1:
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.1.tgz#29ccbc7c37dedfac304e9fff0bf1596b3f6a0e4e"
+  integrity sha512-HvExULSwLqHLgUy1rl3ANIqCsvMS0WHss2UOsXhXnQaZ9VCc2oBvIpXrl00IUFT5ZDITME0o6oiXeiHr2SAIfw==
+
+postcss-safe-parser@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/postcss-safe-parser/-/postcss-safe-parser-6.0.0.tgz#bb4c29894171a94bc5c996b9a30317ef402adaa1"
+  integrity sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==
+
+postcss-selector-parser@^6.0.10, postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4:
+  version "6.0.10"
+  resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz#79b61e2c0d1bfc2602d549e11d0876256f8df88d"
+  integrity sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==
+  dependencies:
+    cssesc "^3.0.0"
+    util-deprecate "^1.0.2"
+
+postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514"
+  integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
+
+postcss@^8.2.15, postcss@^8.3.11, postcss@^8.4.14:
+  version "8.4.14"
+  resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.14.tgz#ee9274d5622b4858c1007a74d76e42e56fd21caf"
+  integrity sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==
+  dependencies:
+    nanoid "^3.3.4"
+    picocolors "^1.0.0"
+    source-map-js "^1.0.2"
+
+prelude-ls@^1.2.1:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
+  integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
+
+prelude-ls@~1.1.2:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
+  integrity sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==
+
+prepend-http@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897"
+  integrity sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA==
+
+prettier-bytes@^1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/prettier-bytes/-/prettier-bytes-1.0.4.tgz#994b02aa46f699c50b6257b5faaa7fe2557e62d6"
+  integrity sha512-dLbWOa4xBn+qeWeIF60qRoB6Pk2jX5P3DIVgOQyMyvBpu931Q+8dXz8X0snJiFkQdohDDLnZQECjzsAj75hgZQ==
+
+prettier-linter-helpers@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz#d23d41fe1375646de2d0104d3454a3008802cf7b"
+  integrity sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==
+  dependencies:
+    fast-diff "^1.1.2"
+
+prettier@^2.1.1:
+  version "2.7.1"
+  resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.7.1.tgz#e235806850d057f97bb08368a4f7d899f7760c64"
+  integrity sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==
+
+prettier@~2.1.1:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.1.2.tgz#3050700dae2e4c8b67c4c3f666cdb8af405e1ce5"
+  integrity sha512-16c7K+x4qVlJg9rEbXl7HEGmQyZlG4R9AgP+oHKRMsMsuk8s+ATStlf1NpDqyBI1HpVyfjLOeMhH2LvuNvV5Vg==
+
+pretty-ms@^7.0.1:
+  version "7.0.1"
+  resolved "https://registry.yarnpkg.com/pretty-ms/-/pretty-ms-7.0.1.tgz#7d903eaab281f7d8e03c66f867e239dc32fb73e8"
+  integrity sha512-973driJZvxiGOQ5ONsFhOF/DtzPMOMtgC11kCpUrPGMTgqp2q/1gwzCquocrN33is0VZ5GFHXZYMM9l6h67v2Q==
+  dependencies:
+    parse-ms "^2.1.0"
+
+process-nextick-args@~2.0.0:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
+  integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==
+
+process-warning@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/process-warning/-/process-warning-1.0.0.tgz#980a0b25dc38cd6034181be4b7726d89066b4616"
+  integrity sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q==
+
+process@^0.11.1, process@^0.11.10:
+  version "0.11.10"
+  resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
+  integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==
+
+progress@^2.0.0:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
+  integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==
+
+promise-inflight@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3"
+  integrity sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==
+
+prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2:
+  version "15.8.1"
+  resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
+  integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
+  dependencies:
+    loose-envify "^1.4.0"
+    object-assign "^4.1.1"
+    react-is "^16.13.1"
+
+proxy-addr@~2.0.7:
+  version "2.0.7"
+  resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025"
+  integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==
+  dependencies:
+    forwarded "0.2.0"
+    ipaddr.js "1.9.1"
+
+prr@~1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476"
+  integrity sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==
+
+psl@^1.1.24, psl@^1.1.28:
+  version "1.8.0"
+  resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24"
+  integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==
+
+pump@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64"
+  integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==
+  dependencies:
+    end-of-stream "^1.1.0"
+    once "^1.3.1"
+
+punycode@1.3.2:
+  version "1.3.2"
+  resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d"
+  integrity sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==
+
+punycode@^1.4.1:
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
+  integrity sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==
+
+punycode@^2.1.0, punycode@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
+  integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
+
+qs@6.10.3:
+  version "6.10.3"
+  resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.3.tgz#d6cde1b2ffca87b5aa57889816c5f81535e22e8e"
+  integrity sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==
+  dependencies:
+    side-channel "^1.0.4"
+
+qs@^6.4.0:
+  version "6.10.5"
+  resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.5.tgz#974715920a80ff6a262264acd2c7e6c2a53282b4"
+  integrity sha512-O5RlPh0VFtR78y79rgcgKK4wbAI0C5zGVLztOIdpWX6ep368q5Hv6XRxDvXuZ9q3C6v+e3n8UfZZJw7IIG27eQ==
+  dependencies:
+    side-channel "^1.0.4"
+
+qs@~6.5.2:
+  version "6.5.3"
+  resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.3.tgz#3aeeffc91967ef6e35c0e488ef46fb296ab76aad"
+  integrity sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==
+
+querystring@0.2.0:
+  version "0.2.0"
+  resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620"
+  integrity sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==
+
+querystringify@^2.1.1:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6"
+  integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==
+
+queue-microtask@^1.2.2:
+  version "1.2.3"
+  resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
+  integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
+
+quick-format-unescaped@^4.0.3:
+  version "4.0.4"
+  resolved "https://registry.yarnpkg.com/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz#93ef6dd8d3453cbc7970dd614fad4c5954d6b5a7"
+  integrity sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==
+
+quick-lru@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f"
+  integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==
+
+randombytes@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
+  integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==
+  dependencies:
+    safe-buffer "^5.1.0"
+
+range-parser@~1.2.1:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
+  integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
+
+raw-body@2.5.1:
+  version "2.5.1"
+  resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857"
+  integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==
+  dependencies:
+    bytes "3.1.2"
+    http-errors "2.0.0"
+    iconv-lite "0.4.24"
+    unpipe "1.0.0"
+
+raw-loader@~4.0.0:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/raw-loader/-/raw-loader-4.0.2.tgz#1aac6b7d1ad1501e66efdac1522c73e59a584eb6"
+  integrity sha512-ZnScIV3ag9A4wPX/ZayxL/jZH+euYb6FcUinPcgiQW0+UBtEv0O6Q3lGd3cqJ+GHH+rksEv3Pj99oxJ3u3VIKA==
+  dependencies:
+    loader-utils "^2.0.0"
+    schema-utils "^3.0.0"
+
+rc@^1.2.8:
+  version "1.2.8"
+  resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"
+  integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==
+  dependencies:
+    deep-extend "^0.6.0"
+    ini "~1.3.0"
+    minimist "^1.2.0"
+    strip-json-comments "~2.0.1"
+
+react-dom@^17.0.1:
+  version "17.0.2"
+  resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23"
+  integrity sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==
+  dependencies:
+    loose-envify "^1.1.0"
+    object-assign "^4.1.1"
+    scheduler "^0.20.2"
+
+react-is@^16.13.1, react-is@^16.9.0:
+  version "16.13.1"
+  resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
+  integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
+
+react-lifecycles-compat@^3.0.4:
+  version "3.0.4"
+  resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
+  integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
+
+react-popper@^1.3.7:
+  version "1.3.11"
+  resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-1.3.11.tgz#a2cc3f0a67b75b66cfa62d2c409f9dd1fcc71ffd"
+  integrity sha512-VSA/bS+pSndSF2fiasHK/PTEEAyOpX60+H5EPAjoArr8JGm+oihu4UbrqcEBpQibJxBVCpYyjAX7abJ+7DoYVg==
+  dependencies:
+    "@babel/runtime" "^7.1.2"
+    "@hypnosphi/create-react-context" "^0.3.1"
+    deep-equal "^1.1.1"
+    popper.js "^1.14.4"
+    prop-types "^15.6.1"
+    typed-styles "^0.0.7"
+    warning "^4.0.2"
+
+react-transition-group@^2.9.0:
+  version "2.9.0"
+  resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.9.0.tgz#df9cdb025796211151a436c69a8f3b97b5b07c8d"
+  integrity sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg==
+  dependencies:
+    dom-helpers "^3.4.0"
+    loose-envify "^1.4.0"
+    prop-types "^15.6.2"
+    react-lifecycles-compat "^3.0.4"
+
+react@^17.0.1:
+  version "17.0.2"
+  resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"
+  integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==
+  dependencies:
+    loose-envify "^1.1.0"
+    object-assign "^4.1.1"
+
+read-pkg-up@^7.0.1:
+  version "7.0.1"
+  resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-7.0.1.tgz#f3a6135758459733ae2b95638056e1854e7ef507"
+  integrity sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==
+  dependencies:
+    find-up "^4.1.0"
+    read-pkg "^5.2.0"
+    type-fest "^0.8.1"
+
+read-pkg@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-3.0.0.tgz#9cbc686978fee65d16c00e2b19c237fcf6e38389"
+  integrity sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==
+  dependencies:
+    load-json-file "^4.0.0"
+    normalize-package-data "^2.3.2"
+    path-type "^3.0.0"
+
+read-pkg@^5.2.0:
+  version "5.2.0"
+  resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-5.2.0.tgz#7bf295438ca5a33e56cd30e053b34ee7250c93cc"
+  integrity sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==
+  dependencies:
+    "@types/normalize-package-data" "^2.4.0"
+    normalize-package-data "^2.5.0"
+    parse-json "^5.0.0"
+    type-fest "^0.6.0"
+
+readable-stream@^2.1.4:
+  version "2.3.7"
+  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
+  integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==
+  dependencies:
+    core-util-is "~1.0.0"
+    inherits "~2.0.3"
+    isarray "~1.0.0"
+    process-nextick-args "~2.0.0"
+    safe-buffer "~5.1.1"
+    string_decoder "~1.1.1"
+    util-deprecate "~1.0.1"
+
+readable-stream@^3.4.0, readable-stream@^3.6.0:
+  version "3.6.0"
+  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198"
+  integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==
+  dependencies:
+    inherits "^2.0.3"
+    string_decoder "^1.1.1"
+    util-deprecate "^1.0.1"
+
+rechoir@^0.7.0:
+  version "0.7.1"
+  resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.7.1.tgz#9478a96a1ca135b5e88fc027f03ee92d6c645686"
+  integrity sha512-/njmZ8s1wVeR6pjTZ+0nCnv8SpZNRMT2D1RLOJQESlYFDBvwpTA4KWJpZ+sBJ4+vhjILRcK7JIFdGCdxEAAitg==
+  dependencies:
+    resolve "^1.9.0"
+
+redent@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f"
+  integrity sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==
+  dependencies:
+    indent-string "^4.0.0"
+    strip-indent "^3.0.0"
+
+regenerator-runtime@^0.13.4:
+  version "0.13.9"
+  resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52"
+  integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==
+
+regexp.prototype.flags@^1.2.0, regexp.prototype.flags@^1.4.3:
+  version "1.4.3"
+  resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz#87cab30f80f66660181a3bb7bf5981a872b367ac"
+  integrity sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==
+  dependencies:
+    call-bind "^1.0.2"
+    define-properties "^1.1.3"
+    functions-have-names "^1.2.2"
+
+regexpp@^3.1.0:
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2"
+  integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==
+
+registry-auth-token@^4.0.0:
+  version "4.2.1"
+  resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-4.2.1.tgz#6d7b4006441918972ccd5fedcd41dc322c79b250"
+  integrity sha512-6gkSb4U6aWJB4SF2ZvLb76yCBjcvufXBqvvEx1HbmKPkutswjW1xNVRY0+daljIYRbogN7O0etYSlbiaEQyMyw==
+  dependencies:
+    rc "^1.2.8"
+
+registry-url@^5.0.0:
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/registry-url/-/registry-url-5.1.0.tgz#e98334b50d5434b81136b44ec638d9c2009c5009"
+  integrity sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==
+  dependencies:
+    rc "^1.2.8"
+
+request-promise-core@1.1.4:
+  version "1.1.4"
+  resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.4.tgz#3eedd4223208d419867b78ce815167d10593a22f"
+  integrity sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw==
+  dependencies:
+    lodash "^4.17.19"
+
+request-promise-native@^1.0.7:
+  version "1.0.9"
+  resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.9.tgz#e407120526a5efdc9a39b28a5679bf47b9d9dc28"
+  integrity sha512-wcW+sIUiWnKgNY0dqCpOZkUbF/I+YPi+f09JZIDa39Ec+q82CpSYniDp+ISgTTbKmnpJWASeJBPZmoxH84wt3g==
+  dependencies:
+    request-promise-core "1.1.4"
+    stealthy-require "^1.1.1"
+    tough-cookie "^2.3.3"
+
+request@2.88.0:
+  version "2.88.0"
+  resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef"
+  integrity sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==
+  dependencies:
+    aws-sign2 "~0.7.0"
+    aws4 "^1.8.0"
+    caseless "~0.12.0"
+    combined-stream "~1.0.6"
+    extend "~3.0.2"
+    forever-agent "~0.6.1"
+    form-data "~2.3.2"
+    har-validator "~5.1.0"
+    http-signature "~1.2.0"
+    is-typedarray "~1.0.0"
+    isstream "~0.1.2"
+    json-stringify-safe "~5.0.1"
+    mime-types "~2.1.19"
+    oauth-sign "~0.9.0"
+    performance-now "^2.1.0"
+    qs "~6.5.2"
+    safe-buffer "^5.1.2"
+    tough-cookie "~2.4.3"
+    tunnel-agent "^0.6.0"
+    uuid "^3.3.2"
+
+request@^2.88.0:
+  version "2.88.2"
+  resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3"
+  integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==
+  dependencies:
+    aws-sign2 "~0.7.0"
+    aws4 "^1.8.0"
+    caseless "~0.12.0"
+    combined-stream "~1.0.6"
+    extend "~3.0.2"
+    forever-agent "~0.6.1"
+    form-data "~2.3.2"
+    har-validator "~5.1.3"
+    http-signature "~1.2.0"
+    is-typedarray "~1.0.0"
+    isstream "~0.1.2"
+    json-stringify-safe "~5.0.1"
+    mime-types "~2.1.19"
+    oauth-sign "~0.9.0"
+    performance-now "^2.1.0"
+    qs "~6.5.2"
+    safe-buffer "^5.1.2"
+    tough-cookie "~2.5.0"
+    tunnel-agent "^0.6.0"
+    uuid "^3.3.2"
+
+require-directory@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
+  integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==
+
+require-from-string@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909"
+  integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==
+
+requires-port@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
+  integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==
+
+resize-observer-polyfill@^1.5.1:
+  version "1.5.1"
+  resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"
+  integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==
+
+resolve-cwd@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d"
+  integrity sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==
+  dependencies:
+    resolve-from "^5.0.0"
+
+resolve-from@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
+  integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==
+
+resolve-from@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69"
+  integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==
+
+resolve@^1.10.0, resolve@^1.20.0, resolve@^1.9.0:
+  version "1.22.0"
+  resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.0.tgz#5e0b8c67c15df57a89bdbabe603a002f21731198"
+  integrity sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==
+  dependencies:
+    is-core-module "^2.8.1"
+    path-parse "^1.0.7"
+    supports-preserve-symlinks-flag "^1.0.0"
+
+responselike@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/responselike/-/responselike-1.0.2.tgz#918720ef3b631c5642be068f15ade5a46f4ba1e7"
+  integrity sha512-/Fpe5guzJk1gPqdJLJR5u7eG/gNY4nImjbRDaVWVMRhne55TCmj2i9Q+54PBRfatRC8v/rIiv9BN0pMd9OV5EQ==
+  dependencies:
+    lowercase-keys "^1.0.0"
+
+restore-cursor@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e"
+  integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==
+  dependencies:
+    onetime "^5.1.0"
+    signal-exit "^3.0.2"
+
+reusify@^1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
+  integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
+
+rimraf@^3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
+  integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
+  dependencies:
+    glob "^7.1.3"
+
+rimraf@~2.4.0:
+  version "2.4.5"
+  resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.4.5.tgz#ee710ce5d93a8fdb856fb5ea8ff0e2d75934b2da"
+  integrity sha512-J5xnxTyqaiw06JjMftq7L9ouA448dw/E7dKghkP9WpKNuwmARNNg+Gk8/u5ryb9N/Yo2+z3MCwuqFK/+qPOPfQ==
+  dependencies:
+    glob "^6.0.1"
+
+robust-predicates@^3.0.0:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/robust-predicates/-/robust-predicates-3.0.1.tgz#ecde075044f7f30118682bd9fb3f123109577f9a"
+  integrity sha512-ndEIpszUHiG4HtDsQLeIuMvRsDnn8c8rYStabochtUeCvfuvNptb5TUbVD68LRAILPX7p9nqQGh4xJgn3EHS/g==
+
+run-async@^2.4.0:
+  version "2.4.1"
+  resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455"
+  integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==
+
+run-parallel@^1.1.9:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee"
+  integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==
+  dependencies:
+    queue-microtask "^1.2.2"
+
+rw@1:
+  version "1.3.3"
+  resolved "https://registry.yarnpkg.com/rw/-/rw-1.3.3.tgz#3f862dfa91ab766b14885ef4d01124bfda074fb4"
+  integrity sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==
+
+rxjs@^6.6.0:
+  version "6.6.7"
+  resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9"
+  integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==
+  dependencies:
+    tslib "^1.9.0"
+
+safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
+  version "5.1.2"
+  resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
+  integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
+
+safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.2, safe-buffer@~5.2.0:
+  version "5.2.1"
+  resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
+  integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
+
+"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
+  integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
+
+sanitize-html@~2.5.3:
+  version "2.5.3"
+  resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-2.5.3.tgz#91aa3dc760b072cdf92f9c6973747569b1ba1cd8"
+  integrity sha512-DGATXd1fs/Rm287/i5FBKVYSBBUL0iAaztOA1/RFhEs4yqo39/X52i/q/CwsfCUG5cilmXSBmnQmyWfnKhBlOg==
+  dependencies:
+    deepmerge "^4.2.2"
+    escape-string-regexp "^4.0.0"
+    htmlparser2 "^6.0.0"
+    is-plain-object "^5.0.0"
+    parse-srcset "^1.0.2"
+    postcss "^8.3.11"
+
+saxes@^3.1.9:
+  version "3.1.11"
+  resolved "https://registry.yarnpkg.com/saxes/-/saxes-3.1.11.tgz#d59d1fd332ec92ad98a2e0b2ee644702384b1c5b"
+  integrity sha512-Ydydq3zC+WYDJK1+gRxRapLIED9PWeSuuS41wqyoRmzvhhh9nc+QQrVMKJYzJFULazeGhzSV0QleN2wD3boh2g==
+  dependencies:
+    xmlchars "^2.1.1"
+
+scheduler@^0.20.2:
+  version "0.20.2"
+  resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.2.tgz#4baee39436e34aa93b4874bddcbf0fe8b8b50e91"
+  integrity sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==
+  dependencies:
+    loose-envify "^1.1.0"
+    object-assign "^4.1.1"
+
+schema-utils@^2.6.5:
+  version "2.7.1"
+  resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.1.tgz#1ca4f32d1b24c590c203b8e7a50bf0ea4cd394d7"
+  integrity sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==
+  dependencies:
+    "@types/json-schema" "^7.0.5"
+    ajv "^6.12.4"
+    ajv-keywords "^3.5.2"
+
+schema-utils@^3.0.0, schema-utils@^3.1.0, schema-utils@^3.1.1:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.1.1.tgz#bc74c4b6b6995c1d88f76a8b77bea7219e0c8281"
+  integrity sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==
+  dependencies:
+    "@types/json-schema" "^7.0.8"
+    ajv "^6.12.5"
+    ajv-keywords "^3.5.2"
+
+secure-compare@3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/secure-compare/-/secure-compare-3.0.1.tgz#f1a0329b308b221fae37b9974f3d578d0ca999e3"
+  integrity sha512-AckIIV90rPDcBcglUwXPF3kg0P0qmPsPXAj6BBEENQE1p5yA1xfmDJzfi1Tappj37Pv2mVbKpL3Z1T+Nn7k1Qw==
+
+"semver@2 || 3 || 4 || 5", semver@^5.4.1, semver@^5.5.0, semver@^5.6.0:
+  version "5.7.1"
+  resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
+  integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
+
+semver@7.3.7, semver@^7.2.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5:
+  version "7.3.7"
+  resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.7.tgz#12c5b649afdbf9049707796e22a4028814ce523f"
+  integrity sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==
+  dependencies:
+    lru-cache "^6.0.0"
+
+semver@^6.0.0, semver@^6.2.0:
+  version "6.3.0"
+  resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
+  integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
+
+send@0.18.0:
+  version "0.18.0"
+  resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be"
+  integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==
+  dependencies:
+    debug "2.6.9"
+    depd "2.0.0"
+    destroy "1.2.0"
+    encodeurl "~1.0.2"
+    escape-html "~1.0.3"
+    etag "~1.8.1"
+    fresh "0.5.2"
+    http-errors "2.0.0"
+    mime "1.6.0"
+    ms "2.1.3"
+    on-finished "2.4.1"
+    range-parser "~1.2.1"
+    statuses "2.0.1"
+
+serialize-javascript@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-5.0.1.tgz#7886ec848049a462467a97d3d918ebb2aaf934f4"
+  integrity sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA==
+  dependencies:
+    randombytes "^2.1.0"
+
+serialize-javascript@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.0.tgz#efae5d88f45d7924141da8b5c3a7a7e663fefeb8"
+  integrity sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==
+  dependencies:
+    randombytes "^2.1.0"
+
+serve-static@1.15.0:
+  version "1.15.0"
+  resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540"
+  integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==
+  dependencies:
+    encodeurl "~1.0.2"
+    escape-html "~1.0.3"
+    parseurl "~1.3.3"
+    send "0.18.0"
+
+set-blocking@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
+  integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==
+
+setprototypeof@1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424"
+  integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==
+
+shallow-clone@^3.0.0:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3"
+  integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==
+  dependencies:
+    kind-of "^6.0.2"
+
+shebang-command@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
+  integrity sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==
+  dependencies:
+    shebang-regex "^1.0.0"
+
+shebang-command@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
+  integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==
+  dependencies:
+    shebang-regex "^3.0.0"
+
+shebang-regex@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3"
+  integrity sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==
+
+shebang-regex@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
+  integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
+
+shell-quote@^1.6.1:
+  version "1.7.3"
+  resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.3.tgz#aa40edac170445b9a431e17bb62c0b881b9c4123"
+  integrity sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==
+
+side-channel@^1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf"
+  integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==
+  dependencies:
+    call-bind "^1.0.0"
+    get-intrinsic "^1.0.2"
+    object-inspect "^1.9.0"
+
+signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.7:
+  version "3.0.7"
+  resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9"
+  integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==
+
+simple-concat@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.1.tgz#f46976082ba35c2263f1c8ab5edfe26c41c9552f"
+  integrity sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==
+
+simple-get@^3.0.3:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-3.1.1.tgz#cc7ba77cfbe761036fbfce3d021af25fc5584d55"
+  integrity sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==
+  dependencies:
+    decompress-response "^4.2.0"
+    once "^1.3.1"
+    simple-concat "^1.0.0"
+
+slash@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
+  integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
+
+slice-ansi@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b"
+  integrity sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==
+  dependencies:
+    ansi-styles "^4.0.0"
+    astral-regex "^2.0.0"
+    is-fullwidth-code-point "^3.0.0"
+
+sonic-boom@^1.0.2:
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-1.4.1.tgz#d35d6a74076624f12e6f917ade7b9d75e918f53e"
+  integrity sha512-LRHh/A8tpW7ru89lrlkU4AszXt1dbwSjVWguGrmlxE7tawVmDBlI1PILMkXAxJTwqhgsEeTHzj36D5CmHgQmNg==
+  dependencies:
+    atomic-sleep "^1.0.0"
+    flatstr "^1.0.12"
+
+sort-object-keys@^1.1.3:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/sort-object-keys/-/sort-object-keys-1.1.3.tgz#bff833fe85cab147b34742e45863453c1e190b45"
+  integrity sha512-855pvK+VkU7PaKYPc+Jjnmt4EzejQHyhhF33q31qG8x7maDzkeFhAAThdCYay11CISO+qAMwjOBP+fPZe0IPyg==
+
+sort-package-json@~1.44.0:
+  version "1.44.0"
+  resolved "https://registry.yarnpkg.com/sort-package-json/-/sort-package-json-1.44.0.tgz#470330be868f8a524a4607b26f2a0233e93d8b6d"
+  integrity sha512-u9GUZvpavUCXV5SbEqXu9FRbsJrYU6WM10r3zA0gymGPufK5X82MblCLh9GW9l46pXKEZvK+FA3eVTqC4oMp4A==
+  dependencies:
+    detect-indent "^6.0.0"
+    detect-newline "3.1.0"
+    git-hooks-list "1.0.3"
+    globby "10.0.0"
+    is-plain-obj "2.1.0"
+    sort-object-keys "^1.1.3"
+
+source-list-map@^2.0.0:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34"
+  integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==
+
+source-map-js@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
+  integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
+
+source-map-support@~0.5.20:
+  version "0.5.21"
+  resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f"
+  integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==
+  dependencies:
+    buffer-from "^1.0.0"
+    source-map "^0.6.0"
+
+source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1:
+  version "0.6.1"
+  resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
+  integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
+
+spdx-correct@^3.0.0:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9"
+  integrity sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==
+  dependencies:
+    spdx-expression-parse "^3.0.0"
+    spdx-license-ids "^3.0.0"
+
+spdx-exceptions@^2.1.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz#3f28ce1a77a00372683eade4a433183527a2163d"
+  integrity sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==
+
+spdx-expression-parse@^3.0.0:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz#cf70f50482eefdc98e3ce0a6833e4a53ceeba679"
+  integrity sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==
+  dependencies:
+    spdx-exceptions "^2.1.0"
+    spdx-license-ids "^3.0.0"
+
+spdx-license-ids@^3.0.0:
+  version "3.0.11"
+  resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.11.tgz#50c0d8c40a14ec1bf449bae69a0ea4685a9d9f95"
+  integrity sha512-Ctl2BrFiM0X3MANYgj3CkygxhRmr9mi6xhejbdO960nF6EDJApTYpn0BQnDKlnNBULKiCN1n3w9EBkHK8ZWg+g==
+
+sprintf-js@~1.0.2:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
+  integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==
+
+sshpk@^1.7.0:
+  version "1.17.0"
+  resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.17.0.tgz#578082d92d4fe612b13007496e543fa0fbcbe4c5"
+  integrity sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==
+  dependencies:
+    asn1 "~0.2.3"
+    assert-plus "^1.0.0"
+    bcrypt-pbkdf "^1.0.0"
+    dashdash "^1.12.0"
+    ecc-jsbn "~0.1.1"
+    getpass "^0.1.1"
+    jsbn "~0.1.0"
+    safer-buffer "^2.0.2"
+    tweetnacl "~0.14.0"
+
+ssri@^8.0.1:
+  version "8.0.1"
+  resolved "https://registry.yarnpkg.com/ssri/-/ssri-8.0.1.tgz#638e4e439e2ffbd2cd289776d5ca457c4f51a2af"
+  integrity sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==
+  dependencies:
+    minipass "^3.1.1"
+
+statuses@2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63"
+  integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==
+
+stealthy-require@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b"
+  integrity sha512-ZnWpYnYugiOVEY5GkcuJK1io5V8QmNYChG62gSit9pQVGErXtrKuPC55ITaVSukmMta5qpMU7vqLt2Lnni4f/g==
+
+steno@^0.4.1:
+  version "0.4.4"
+  resolved "https://registry.yarnpkg.com/steno/-/steno-0.4.4.tgz#071105bdfc286e6615c0403c27e9d7b5dcb855cb"
+  integrity sha512-EEHMVYHNXFHfGtgjNITnka0aHhiAlo93F7z2/Pwd+g0teG9CnM3JIINM7hVVB5/rhw9voufD7Wukwgtw2uqh6w==
+  dependencies:
+    graceful-fs "^4.1.3"
+
+"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
+  version "4.2.3"
+  resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
+  integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
+  dependencies:
+    emoji-regex "^8.0.0"
+    is-fullwidth-code-point "^3.0.0"
+    strip-ansi "^6.0.1"
+
+string.prototype.padend@^3.0.0:
+  version "3.1.3"
+  resolved "https://registry.yarnpkg.com/string.prototype.padend/-/string.prototype.padend-3.1.3.tgz#997a6de12c92c7cb34dc8a201a6c53d9bd88a5f1"
+  integrity sha512-jNIIeokznm8SD/TZISQsZKYu7RJyheFNt84DUPrh482GC8RVp2MKqm2O5oBRdGxbDQoXrhhWtPIWQOiy20svUg==
+  dependencies:
+    call-bind "^1.0.2"
+    define-properties "^1.1.3"
+    es-abstract "^1.19.1"
+
+string.prototype.trimend@^1.0.5:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz#914a65baaab25fbdd4ee291ca7dde57e869cb8d0"
+  integrity sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog==
+  dependencies:
+    call-bind "^1.0.2"
+    define-properties "^1.1.4"
+    es-abstract "^1.19.5"
+
+string.prototype.trimstart@^1.0.5:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz#5466d93ba58cfa2134839f81d7f42437e8c01fef"
+  integrity sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg==
+  dependencies:
+    call-bind "^1.0.2"
+    define-properties "^1.1.4"
+    es-abstract "^1.19.5"
+
+string_decoder@^1.1.1:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"
+  integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==
+  dependencies:
+    safe-buffer "~5.2.0"
+
+string_decoder@~1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"
+  integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==
+  dependencies:
+    safe-buffer "~5.1.0"
+
+strip-ansi@^6.0.0, strip-ansi@^6.0.1:
+  version "6.0.1"
+  resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
+  integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
+  dependencies:
+    ansi-regex "^5.0.1"
+
+strip-bom@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
+  integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==
+
+strip-indent@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001"
+  integrity sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==
+  dependencies:
+    min-indent "^1.0.0"
+
+strip-json-comments@^3.1.0, strip-json-comments@^3.1.1:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
+  integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
+
+strip-json-comments@~2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
+  integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==
+
+style-loader@~2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-2.0.0.tgz#9669602fd4690740eaaec137799a03addbbc393c"
+  integrity sha512-Z0gYUJmzZ6ZdRUqpg1r8GsaFKypE+3xAzuFeMuoHgjc9KZv3wMyCRjQIWEbhoFSq7+7yoHXySDJyyWQaPajeiQ==
+  dependencies:
+    loader-utils "^2.0.0"
+    schema-utils "^3.0.0"
+
+style-search@^0.1.0:
+  version "0.1.0"
+  resolved "https://registry.yarnpkg.com/style-search/-/style-search-0.1.0.tgz#7958c793e47e32e07d2b5cafe5c0bf8e12e77902"
+  integrity sha512-Dj1Okke1C3uKKwQcetra4jSuk0DqbzbYtXipzFlFMZtowbF1x7BKJwB9AayVMyFARvU8EDrZdcax4At/452cAg==
+
+stylelint-config-prettier@^9.0.3:
+  version "9.0.3"
+  resolved "https://registry.yarnpkg.com/stylelint-config-prettier/-/stylelint-config-prettier-9.0.3.tgz#0dccebeff359dcc393c9229184408b08964d561c"
+  integrity sha512-5n9gUDp/n5tTMCq1GLqSpA30w2sqWITSSEiAWQlpxkKGAUbjcemQ0nbkRvRUa0B1LgD3+hCvdL7B1eTxy1QHJg==
+
+stylelint-config-recommended@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/stylelint-config-recommended/-/stylelint-config-recommended-6.0.0.tgz#fd2523a322836005ad9bf473d3e5534719c09f9d"
+  integrity sha512-ZorSSdyMcxWpROYUvLEMm0vSZud2uB7tX1hzBZwvVY9SV/uly4AvvJPPhCcymZL3fcQhEQG5AELmrxWqtmzacw==
+
+stylelint-config-standard@~24.0.0:
+  version "24.0.0"
+  resolved "https://registry.yarnpkg.com/stylelint-config-standard/-/stylelint-config-standard-24.0.0.tgz#6823f207ab997ae0b641f9a636d007cc44d77541"
+  integrity sha512-+RtU7fbNT+VlNbdXJvnjc3USNPZRiRVp/d2DxOF/vBDDTi0kH5RX2Ny6errdtZJH3boO+bmqIYEllEmok4jiuw==
+  dependencies:
+    stylelint-config-recommended "^6.0.0"
+
+stylelint-prettier@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/stylelint-prettier/-/stylelint-prettier-2.0.0.tgz#ead781aea522379f2ffa2d136bafdfc451d699a5"
+  integrity sha512-jvT3G+9lopkeB0ARmDPszyfaOnvnIF+30QCjZxyt7E6fynI1T9mOKgYDNb9bXX17M7PXMZaX3j/26wqakjp1tw==
+  dependencies:
+    prettier-linter-helpers "^1.0.0"
+
+stylelint@^14.3.0:
+  version "14.9.1"
+  resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-14.9.1.tgz#6494ed38f148b1e75b402d678a3b6a8aae86dfda"
+  integrity sha512-RdAkJdPiLqHawCSnu21nE27MjNXaVd4WcOHA4vK5GtIGjScfhNnaOuWR2wWdfKFAvcWQPOYe311iveiVKSmwsA==
+  dependencies:
+    "@csstools/selector-specificity" "^2.0.1"
+    balanced-match "^2.0.0"
+    colord "^2.9.2"
+    cosmiconfig "^7.0.1"
+    css-functions-list "^3.1.0"
+    debug "^4.3.4"
+    execall "^2.0.0"
+    fast-glob "^3.2.11"
+    fastest-levenshtein "^1.0.12"
+    file-entry-cache "^6.0.1"
+    get-stdin "^8.0.0"
+    global-modules "^2.0.0"
+    globby "^11.1.0"
+    globjoin "^0.1.4"
+    html-tags "^3.2.0"
+    ignore "^5.2.0"
+    import-lazy "^4.0.0"
+    imurmurhash "^0.1.4"
+    is-plain-object "^5.0.0"
+    known-css-properties "^0.25.0"
+    mathml-tag-names "^2.1.3"
+    meow "^9.0.0"
+    micromatch "^4.0.5"
+    normalize-path "^3.0.0"
+    picocolors "^1.0.0"
+    postcss "^8.4.14"
+    postcss-media-query-parser "^0.2.3"
+    postcss-resolve-nested-selector "^0.1.1"
+    postcss-safe-parser "^6.0.0"
+    postcss-selector-parser "^6.0.10"
+    postcss-value-parser "^4.2.0"
+    resolve-from "^5.0.0"
+    string-width "^4.2.3"
+    strip-ansi "^6.0.1"
+    style-search "^0.1.0"
+    supports-hyperlinks "^2.2.0"
+    svg-tags "^1.0.0"
+    table "^6.8.0"
+    v8-compile-cache "^2.3.0"
+    write-file-atomic "^4.0.1"
+
+supports-color@^5.3.0:
+  version "5.5.0"
+  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
+  integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==
+  dependencies:
+    has-flag "^3.0.0"
+
+supports-color@^7.0.0, supports-color@^7.1.0, supports-color@^7.2.0:
+  version "7.2.0"
+  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
+  integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==
+  dependencies:
+    has-flag "^4.0.0"
+
+supports-color@^8.0.0:
+  version "8.1.1"
+  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c"
+  integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==
+  dependencies:
+    has-flag "^4.0.0"
+
+supports-hyperlinks@^2.2.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-2.2.0.tgz#4f77b42488765891774b70c79babd87f9bd594bb"
+  integrity sha512-6sXEzV5+I5j8Bmq9/vUphGRM/RJNT9SCURJLjwfOg51heRtguGWDzcaBlgAzKhQa0EVNpPEKzQuBwZ8S8WaCeQ==
+  dependencies:
+    has-flag "^4.0.0"
+    supports-color "^7.0.0"
+
+supports-preserve-symlinks-flag@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
+  integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
+
+svg-tags@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/svg-tags/-/svg-tags-1.0.0.tgz#58f71cee3bd519b59d4b2a843b6c7de64ac04764"
+  integrity sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==
+
+svg-url-loader@~6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/svg-url-loader/-/svg-url-loader-6.0.0.tgz#b94861d9f6badfb8ca3e7d3ec4655c1bf732ac5d"
+  integrity sha512-Qr5SCKxyxKcRnvnVrO3iQj9EX/v40UiGEMshgegzV7vpo3yc+HexELOdtWcA3MKjL8IyZZ1zOdcILmDEa/8JJQ==
+  dependencies:
+    file-loader "~6.0.0"
+    loader-utils "~2.0.0"
+
+symbol-tree@^3.2.2:
+  version "3.2.4"
+  resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
+  integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==
+
+systeminformation@^5.8.6:
+  version "5.11.20"
+  resolved "https://registry.yarnpkg.com/systeminformation/-/systeminformation-5.11.20.tgz#a0e1dcfc80c0dac12f1475d9a97e564ba8fa58a2"
+  integrity sha512-7PTbNtcTBKIdUJ8zY7KeHH0lUArh5IgkJhunWYtaVPQXU1N+9Pk4Ko2Adb3w4qgB6Zmm70SvX6zxnnzca+0j4A==
+
+table@^6.0.9, table@^6.8.0:
+  version "6.8.0"
+  resolved "https://registry.yarnpkg.com/table/-/table-6.8.0.tgz#87e28f14fa4321c3377ba286f07b79b281a3b3ca"
+  integrity sha512-s/fitrbVeEyHKFa7mFdkuQMWlH1Wgw/yEXMt5xACT4ZpzWFluehAxRtUUQKPuWhaLAWhFcVx6w3oC8VKaUfPGA==
+  dependencies:
+    ajv "^8.0.1"
+    lodash.truncate "^4.4.2"
+    slice-ansi "^4.0.0"
+    string-width "^4.2.3"
+    strip-ansi "^6.0.1"
+
+tapable@^2.1.1, tapable@^2.2.0:
+  version "2.2.1"
+  resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0"
+  integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==
+
+tar@^6.0.2, tar@^6.1.11:
+  version "6.1.11"
+  resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.11.tgz#6760a38f003afa1b2ffd0ffe9e9abbd0eab3d621"
+  integrity sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==
+  dependencies:
+    chownr "^2.0.0"
+    fs-minipass "^2.0.0"
+    minipass "^3.0.0"
+    minizlib "^2.1.1"
+    mkdirp "^1.0.3"
+    yallist "^4.0.0"
+
+terser-webpack-plugin@^4.1.0:
+  version "4.2.3"
+  resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-4.2.3.tgz#28daef4a83bd17c1db0297070adc07fc8cfc6a9a"
+  integrity sha512-jTgXh40RnvOrLQNgIkwEKnQ8rmHjHK4u+6UBEi+W+FPmvb+uo+chJXntKe7/3lW5mNysgSWD60KyesnhW8D6MQ==
+  dependencies:
+    cacache "^15.0.5"
+    find-cache-dir "^3.3.1"
+    jest-worker "^26.5.0"
+    p-limit "^3.0.2"
+    schema-utils "^3.0.0"
+    serialize-javascript "^5.0.1"
+    source-map "^0.6.1"
+    terser "^5.3.4"
+    webpack-sources "^1.4.3"
+
+terser-webpack-plugin@^5.1.3:
+  version "5.3.3"
+  resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.3.tgz#8033db876dd5875487213e87c627bca323e5ed90"
+  integrity sha512-Fx60G5HNYknNTNQnzQ1VePRuu89ZVYWfjRAeT5rITuCY/1b08s49e5kSQwHDirKZWuoKOBRFS98EUUoZ9kLEwQ==
+  dependencies:
+    "@jridgewell/trace-mapping" "^0.3.7"
+    jest-worker "^27.4.5"
+    schema-utils "^3.1.1"
+    serialize-javascript "^6.0.0"
+    terser "^5.7.2"
+
+terser@^5.3.4, terser@^5.7.2:
+  version "5.14.1"
+  resolved "https://registry.yarnpkg.com/terser/-/terser-5.14.1.tgz#7c95eec36436cb11cf1902cc79ac564741d19eca"
+  integrity sha512-+ahUAE+iheqBTDxXhTisdA8hgvbEG1hHOQ9xmNjeUJSoi6DU/gMrKNcfZjHkyY6Alnuyc+ikYJaxxfHkT3+WuQ==
+  dependencies:
+    "@jridgewell/source-map" "^0.3.2"
+    acorn "^8.5.0"
+    commander "^2.20.0"
+    source-map-support "~0.5.20"
+
+text-table@^0.2.0:
+  version "0.2.0"
+  resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
+  integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==
+
+"through@>=2.2.7 <3", through@^2.3.6:
+  version "2.3.8"
+  resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
+  integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==
+
+timers-ext@^0.1.7:
+  version "0.1.7"
+  resolved "https://registry.yarnpkg.com/timers-ext/-/timers-ext-0.1.7.tgz#6f57ad8578e07a3fb9f91d9387d65647555e25c6"
+  integrity sha512-b85NUNzTSdodShTIbky6ZF02e8STtVVfD+fu4aXXShEELpozH+bCpJLYMPZbsABN2wDH7fJpqIoXxJpzbf0NqQ==
+  dependencies:
+    es5-ext "~0.10.46"
+    next-tick "1"
+
+tmp@^0.0.33:
+  version "0.0.33"
+  resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
+  integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==
+  dependencies:
+    os-tmpdir "~1.0.2"
+
+to-readable-stream@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/to-readable-stream/-/to-readable-stream-1.0.0.tgz#ce0aa0c2f3df6adf852efb404a783e77c0475771"
+  integrity sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==
+
+to-regex-range@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
+  integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
+  dependencies:
+    is-number "^7.0.0"
+
+to-string-loader@^1.1.6:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/to-string-loader/-/to-string-loader-1.2.0.tgz#4364aa044b9aa876473f4d7a36ef7d216a276e9c"
+  integrity sha512-KsWUL8FccgBW9FPFm4vYoQbOOcO5m6hKOGYoXjbseD9/4Ft+ravXN5jolQ9kTKYcK4zPt1j+khx97GPGnVoi6A==
+  dependencies:
+    loader-utils "^1.0.0"
+
+toidentifier@1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35"
+  integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==
+
+topojson-client@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/topojson-client/-/topojson-client-3.1.0.tgz#22e8b1ed08a2b922feeb4af6f53b6ef09a467b99"
+  integrity sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw==
+  dependencies:
+    commander "2"
+
+tough-cookie@^2.3.3, tough-cookie@~2.5.0:
+  version "2.5.0"
+  resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2"
+  integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==
+  dependencies:
+    psl "^1.1.28"
+    punycode "^2.1.1"
+
+tough-cookie@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-3.0.1.tgz#9df4f57e739c26930a018184887f4adb7dca73b2"
+  integrity sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg==
+  dependencies:
+    ip-regex "^2.1.0"
+    psl "^1.1.28"
+    punycode "^2.1.1"
+
+tough-cookie@~2.4.3:
+  version "2.4.3"
+  resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781"
+  integrity sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==
+  dependencies:
+    psl "^1.1.24"
+    punycode "^1.4.1"
+
+tr46@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09"
+  integrity sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==
+  dependencies:
+    punycode "^2.1.0"
+
+tr46@~0.0.3:
+  version "0.0.3"
+  resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
+  integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==
+
+trim-newlines@^3.0.0:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.1.tgz#260a5d962d8b752425b32f3a7db0dcacd176c144"
+  integrity sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==
+
+tslib@^1.8.1, tslib@^1.9.0:
+  version "1.14.1"
+  resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
+  integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
+
+tslib@~2.3.1:
+  version "2.3.1"
+  resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01"
+  integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==
+
+tsscmp@1.0.6:
+  version "1.0.6"
+  resolved "https://registry.yarnpkg.com/tsscmp/-/tsscmp-1.0.6.tgz#85b99583ac3589ec4bfef825b5000aa911d605eb"
+  integrity sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==
+
+tsutils@^3.21.0:
+  version "3.21.0"
+  resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623"
+  integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==
+  dependencies:
+    tslib "^1.8.1"
+
+tunnel-agent@^0.6.0:
+  version "0.6.0"
+  resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"
+  integrity sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==
+  dependencies:
+    safe-buffer "^5.0.1"
+
+tweetnacl@^0.14.3, tweetnacl@~0.14.0:
+  version "0.14.5"
+  resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
+  integrity sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==
+
+typanion@^3.3.1:
+  version "3.9.0"
+  resolved "https://registry.yarnpkg.com/typanion/-/typanion-3.9.0.tgz#071a31a0f81c3c31226e190d0a6513ff1c8ae1a3"
+  integrity sha512-7yPk67IIquhKQcUXOBM27vDuGmZf6oJbEmzgVfDniHCkT6+z4JnKY85nKqbstoec8Kp7hD06TP3Kc98ij43PIg==
+
+type-check@^0.4.0, type-check@~0.4.0:
+  version "0.4.0"
+  resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1"
+  integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==
+  dependencies:
+    prelude-ls "^1.2.1"
+
+type-check@~0.3.2:
+  version "0.3.2"
+  resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72"
+  integrity sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==
+  dependencies:
+    prelude-ls "~1.1.2"
+
+type-fest@^0.18.0:
+  version "0.18.1"
+  resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.18.1.tgz#db4bc151a4a2cf4eebf9add5db75508db6cc841f"
+  integrity sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==
+
+type-fest@^0.20.2:
+  version "0.20.2"
+  resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4"
+  integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==
+
+type-fest@^0.21.3:
+  version "0.21.3"
+  resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37"
+  integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==
+
+type-fest@^0.6.0:
+  version "0.6.0"
+  resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.6.0.tgz#8d2a2370d3df886eb5c90ada1c5bf6188acf838b"
+  integrity sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==
+
+type-fest@^0.8.1:
+  version "0.8.1"
+  resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d"
+  integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==
+
+type-is@~1.6.18:
+  version "1.6.18"
+  resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
+  integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==
+  dependencies:
+    media-typer "0.3.0"
+    mime-types "~2.1.24"
+
+type@^1.0.1:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/type/-/type-1.2.0.tgz#848dd7698dafa3e54a6c479e759c4bc3f18847a0"
+  integrity sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==
+
+type@^2.5.0:
+  version "2.6.0"
+  resolved "https://registry.yarnpkg.com/type/-/type-2.6.0.tgz#3ca6099af5981d36ca86b78442973694278a219f"
+  integrity sha512-eiDBDOmkih5pMbo9OqsqPRGMljLodLcwd5XD5JbtNB0o89xZAwynY9EdCDsJU7LtcVCClu9DvM7/0Ep1hYX3EQ==
+
+typed-styles@^0.0.7:
+  version "0.0.7"
+  resolved "https://registry.yarnpkg.com/typed-styles/-/typed-styles-0.0.7.tgz#93392a008794c4595119ff62dde6809dbc40a3d9"
+  integrity sha512-pzP0PWoZUhsECYjABgCGQlRGL1n7tOHsgwYv3oIiEpJwGhFTuty/YNeduxQYzXXa3Ge5BdT6sHYIQYpl4uJ+5Q==
+
+typescript@~4.1.3:
+  version "4.1.6"
+  resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.6.tgz#1becd85d77567c3c741172339e93ce2e69932138"
+  integrity sha512-pxnwLxeb/Z5SP80JDRzVjh58KsM6jZHRAOtTpS7sXLS4ogXNKC9ANxHHZqLLeVHZN35jCtI4JdmLLbLiC1kBow==
+
+typestyle@^2.0.4:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/typestyle/-/typestyle-2.3.0.tgz#eff768918d5799d12009ec1543369b76b7e5c3ce"
+  integrity sha512-JZd1R5cWxRvwXzPAKVFsbxO/QjDkFeQlqGF6ZxR7IREEXyFQrf1f1mmlx5EynOTItLLx4aBbcVpCH+CDsl5ZTw==
+  dependencies:
+    csstype "3.0.10"
+    free-style "3.1.0"
+
+uglify-js@^3.1.4:
+  version "3.16.0"
+  resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.16.0.tgz#b778ba0831ca102c1d8ecbdec2d2bdfcc7353190"
+  integrity sha512-FEikl6bR30n0T3amyBh3LoiBdqHRy/f4H80+My34HOesOKyHfOsxAPAxOoqC0JUnC1amnO0IwkYC3sko51caSw==
+
+unbox-primitive@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e"
+  integrity sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==
+  dependencies:
+    call-bind "^1.0.2"
+    has-bigints "^1.0.2"
+    has-symbols "^1.0.3"
+    which-boxed-primitive "^1.0.2"
+
+union@~0.5.0:
+  version "0.5.0"
+  resolved "https://registry.yarnpkg.com/union/-/union-0.5.0.tgz#b2c11be84f60538537b846edb9ba266ba0090075"
+  integrity sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA==
+  dependencies:
+    qs "^6.4.0"
+
+unique-filename@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.1.tgz#1d69769369ada0583103a1e6ae87681b56573230"
+  integrity sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==
+  dependencies:
+    unique-slug "^2.0.0"
+
+unique-slug@^2.0.0:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-2.0.2.tgz#baabce91083fc64e945b0f3ad613e264f7cd4e6c"
+  integrity sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==
+  dependencies:
+    imurmurhash "^0.1.4"
+
+universalify@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717"
+  integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==
+
+unix-crypt-td-js@1.1.4:
+  version "1.1.4"
+  resolved "https://registry.yarnpkg.com/unix-crypt-td-js/-/unix-crypt-td-js-1.1.4.tgz#4912dfad1c8aeb7d20fa0a39e4c31918c1d5d5dd"
+  integrity sha512-8rMeVYWSIyccIJscb9NdCfZKSRBKYTeVnwmiRYT2ulE3qd1RaDQ0xQDP+rI3ccIWbhu/zuo5cgN8z73belNZgw==
+
+unpipe@1.0.0, unpipe@~1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
+  integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==
+
+uri-js@^4.2.2:
+  version "4.4.1"
+  resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e"
+  integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==
+  dependencies:
+    punycode "^2.1.0"
+
+url-join@^2.0.5:
+  version "2.0.5"
+  resolved "https://registry.yarnpkg.com/url-join/-/url-join-2.0.5.tgz#5af22f18c052a000a48d7b82c5e9c2e2feeda728"
+  integrity sha512-c2H1fIgpUdwFRIru9HFno5DT73Ok8hg5oOb5AT3ayIgvCRfxgs2jyt5Slw8kEB7j3QUr6yJmMPDT/odjk7jXow==
+
+url-loader@~4.1.0:
+  version "4.1.1"
+  resolved "https://registry.yarnpkg.com/url-loader/-/url-loader-4.1.1.tgz#28505e905cae158cf07c92ca622d7f237e70a4e2"
+  integrity sha512-3BTV812+AVHHOJQO8O5MkWgZ5aosP7GnROJwvzLS9hWDj00lZ6Z0wNak423Lp9PBZN05N+Jk/N5Si8jRAlGyWA==
+  dependencies:
+    loader-utils "^2.0.0"
+    mime-types "^2.1.27"
+    schema-utils "^3.0.0"
+
+url-parse-lax@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-3.0.0.tgz#16b5cafc07dbe3676c1b1999177823d6503acb0c"
+  integrity sha512-NjFKA0DidqPa5ciFcSrXnAltTtzz84ogy+NebPvfEgAck0+TNg4UJ4IN+fB7zRZfbgUf0syOo9MDxFkDSMuFaQ==
+  dependencies:
+    prepend-http "^2.0.0"
+
+url-parse@~1.5.1:
+  version "1.5.10"
+  resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.10.tgz#9d3c2f736c1d75dd3bd2be507dcc111f1e2ea9c1"
+  integrity sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==
+  dependencies:
+    querystringify "^2.1.1"
+    requires-port "^1.0.0"
+
+url@^0.11.0:
+  version "0.11.0"
+  resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1"
+  integrity sha512-kbailJa29QrtXnxgq+DdCEGlbTeYM2eJUxsz6vjZavrCYPMIFHMKQmSKYAIuUK2i7hgPm28a8piX5NTUtM/LKQ==
+  dependencies:
+    punycode "1.3.2"
+    querystring "0.2.0"
+
+util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
+  integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
+
+util@^0.10.3:
+  version "0.10.4"
+  resolved "https://registry.yarnpkg.com/util/-/util-0.10.4.tgz#3aa0125bfe668a4672de58857d3ace27ecb76901"
+  integrity sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==
+  dependencies:
+    inherits "2.0.3"
+
+utils-merge@1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
+  integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==
+
+uuid@^3.3.2:
+  version "3.4.0"
+  resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
+  integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
+
+v8-compile-cache@^2.0.3, v8-compile-cache@^2.3.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"
+  integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==
+
+validate-npm-package-license@^3.0.1:
+  version "3.0.4"
+  resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a"
+  integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==
+  dependencies:
+    spdx-correct "^3.0.0"
+    spdx-expression-parse "^3.0.0"
+
+validate.io-array@^1.0.3:
+  version "1.0.6"
+  resolved "https://registry.yarnpkg.com/validate.io-array/-/validate.io-array-1.0.6.tgz#5b5a2cafd8f8b85abb2f886ba153f2d93a27774d"
+  integrity sha512-DeOy7CnPEziggrOO5CZhVKJw6S3Yi7e9e65R1Nl/RTN1vTQKnzjfvks0/8kQ40FP/dsjRAOd4hxmJ7uLa6vxkg==
+
+validate.io-function@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/validate.io-function/-/validate.io-function-1.0.2.tgz#343a19802ed3b1968269c780e558e93411c0bad7"
+  integrity sha512-LlFybRJEriSuBnUhQyG5bwglhh50EpTL2ul23MPIuR1odjO7XaMLFV8vHGwp7AZciFxtYOeiSCT5st+XSPONiQ==
+
+validate.io-integer-array@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/validate.io-integer-array/-/validate.io-integer-array-1.0.0.tgz#2cabde033293a6bcbe063feafe91eaf46b13a089"
+  integrity sha512-mTrMk/1ytQHtCY0oNO3dztafHYyGU88KL+jRxWuzfOmQb+4qqnWmI+gykvGp8usKZOM0H7keJHEbRaFiYA0VrA==
+  dependencies:
+    validate.io-array "^1.0.3"
+    validate.io-integer "^1.0.4"
+
+validate.io-integer@^1.0.4:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/validate.io-integer/-/validate.io-integer-1.0.5.tgz#168496480b95be2247ec443f2233de4f89878068"
+  integrity sha512-22izsYSLojN/P6bppBqhgUDjCkr5RY2jd+N2a3DCAUey8ydvrZ/OkGvFPR7qfOpwR2LC5p4Ngzxz36g5Vgr/hQ==
+  dependencies:
+    validate.io-number "^1.0.3"
+
+validate.io-number@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/validate.io-number/-/validate.io-number-1.0.3.tgz#f63ffeda248bf28a67a8d48e0e3b461a1665baf8"
+  integrity sha512-kRAyotcbNaSYoDnXvb4MHg/0a1egJdLwS6oJ38TJY7aw9n93Fl/3blIXdyYvPOp55CNxywooG/3BcrwNrBpcSg==
+
+validator@13.7.0:
+  version "13.7.0"
+  resolved "https://registry.yarnpkg.com/validator/-/validator-13.7.0.tgz#4f9658ba13ba8f3d82ee881d3516489ea85c0857"
+  integrity sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw==
+
+vary@^1, vary@~1.1.2:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
+  integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==
+
+vega-canvas@^1.2.5, vega-canvas@^1.2.6:
+  version "1.2.6"
+  resolved "https://registry.yarnpkg.com/vega-canvas/-/vega-canvas-1.2.6.tgz#55e032ce9a62acd17229f6bac66d99db3d6879cd"
+  integrity sha512-rgeYUpslYn/amIfnuv3Sw6n4BGns94OjjZNtUc9IDji6b+K8LGS/kW+Lvay8JX/oFqtulBp8RLcHN6QjqPLA9Q==
+
+vega-crossfilter@~4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/vega-crossfilter/-/vega-crossfilter-4.1.0.tgz#b6c5a728ce987f2514074adb22cf86b9bc63e0c8"
+  integrity sha512-aiOJcvVpiEDIu5uNc4Kf1hakkkPaVOO5fw5T4RSFAw6GEDbdqcB6eZ1xePcsLVic1hxYD5SGiUPdiiIs0SMh2g==
+  dependencies:
+    d3-array "^3.1.1"
+    vega-dataflow "^5.7.3"
+    vega-util "^1.15.2"
+
+vega-dataflow@^5.7.3, vega-dataflow@^5.7.4, vega-dataflow@~5.7.4:
+  version "5.7.4"
+  resolved "https://registry.yarnpkg.com/vega-dataflow/-/vega-dataflow-5.7.4.tgz#7cafc0a41b9d0b11dd2e34a513f8b7ca345dfd74"
+  integrity sha512-JGHTpUo8XGETH3b1V892we6hdjzCWB977ybycIu8DPqRoyrZuj6t1fCVImazfMgQD1LAfJlQybWP+alwKDpKig==
+  dependencies:
+    vega-format "^1.0.4"
+    vega-loader "^4.3.2"
+    vega-util "^1.16.1"
+
+vega-encode@~4.9.0:
+  version "4.9.0"
+  resolved "https://registry.yarnpkg.com/vega-encode/-/vega-encode-4.9.0.tgz#3dd1031056bb8029f262afc4b4d58372c8f073a6"
+  integrity sha512-etv2BHuCn9bzEc0cxyA2TnbtcAFQGVFmsaqmB4sgBCaqTSEfXMoX68LK3yxBrsdm5LU+y3otJVoewi3qWYCx2g==
+  dependencies:
+    d3-array "^3.1.1"
+    d3-interpolate "^3.0.1"
+    vega-dataflow "^5.7.3"
+    vega-scale "^7.0.3"
+    vega-util "^1.15.2"
+
+vega-event-selector@^3.0.0, vega-event-selector@~3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/vega-event-selector/-/vega-event-selector-3.0.0.tgz#7b855ac0c3ddb59bc5b5caa0d96dbbc9fbd33a4c"
+  integrity sha512-Gls93/+7tEJGE3kUuUnxrBIxtvaNeF01VIFB2Q2Of2hBIBvtHX74jcAdDtkh5UhhoYGD8Q1J30P5cqEBEwtPoQ==
+
+vega-expression@^5.0.0, vega-expression@~5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/vega-expression/-/vega-expression-5.0.0.tgz#938f26689693a1e0d26716030cdaed43ca7abdfb"
+  integrity sha512-y5+c2frq0tGwJ7vYXzZcfVcIRF/QGfhf2e+bV1Z0iQs+M2lI1II1GPDdmOcMKimpoCVp/D61KUJDIGE1DSmk2w==
+  dependencies:
+    "@types/estree" "^0.0.50"
+    vega-util "^1.16.0"
+
+vega-force@~4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/vega-force/-/vega-force-4.1.0.tgz#cc8dea972baa52adc60840ff744ebb9e57d8f1f5"
+  integrity sha512-Sssf8iH48vYlz+E7/RpU+SUaJbuLoIL87U4tG2Av4gf/hRiImU49x2TI3EuhFWg1zpaCFxlz0CAaX++Oh/gjdw==
+  dependencies:
+    d3-force "^3.0.0"
+    vega-dataflow "^5.7.3"
+    vega-util "^1.15.2"
+
+vega-format@^1.0.4, vega-format@^1.1.0, vega-format@~1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/vega-format/-/vega-format-1.1.0.tgz#b9d81cf1bcf222ae5cbd94357ae70245d2c7b18d"
+  integrity sha512-6mgpeWw8yGdG0Zdi8aVkx5oUrpJGOpNxqazC2858RSDPvChM/jDFlgRMTYw52qk7cxU0L08ARp4BwmXaI75j0w==
+  dependencies:
+    d3-array "^3.1.1"
+    d3-format "^3.1.0"
+    d3-time-format "^4.1.0"
+    vega-time "^2.0.3"
+    vega-util "^1.15.2"
+
+vega-functions@^5.12.1, vega-functions@^5.13.0, vega-functions@~5.13.0:
+  version "5.13.0"
+  resolved "https://registry.yarnpkg.com/vega-functions/-/vega-functions-5.13.0.tgz#c9ab8c6eedbf39f75b424cca6776b1d0b8c74b32"
+  integrity sha512-Mf53zNyx+c9fFqagEI0T8zc9nMlx0zozOngr8oOpG1tZDKOgwOnUgN99zQKbLHjyv+UzWrq3LYTnSLyVe0ZmhQ==
+  dependencies:
+    d3-array "^3.1.1"
+    d3-color "^3.0.1"
+    d3-geo "^3.0.1"
+    vega-dataflow "^5.7.3"
+    vega-expression "^5.0.0"
+    vega-scale "^7.2.0"
+    vega-scenegraph "^4.9.3"
+    vega-selections "^5.3.1"
+    vega-statistics "^1.7.9"
+    vega-time "^2.1.0"
+    vega-util "^1.16.0"
+
+vega-geo@~4.4.0:
+  version "4.4.0"
+  resolved "https://registry.yarnpkg.com/vega-geo/-/vega-geo-4.4.0.tgz#ce792df57f8ca4c54a7a1a29467cfa24bc53f573"
+  integrity sha512-3YX41y+J5pu0PMjvBCASg0/lgvu9+QXWJZ+vl6FFKa8AlsIopQ67ZL7ObwqjZcoZMolJ4q0rc+ZO8aj1pXCYcw==
+  dependencies:
+    d3-array "^3.1.1"
+    d3-color "^3.0.1"
+    d3-geo "^3.0.1"
+    vega-canvas "^1.2.5"
+    vega-dataflow "^5.7.3"
+    vega-projection "^1.4.5"
+    vega-statistics "^1.7.9"
+    vega-util "^1.15.2"
+
+vega-hierarchy@~4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/vega-hierarchy/-/vega-hierarchy-4.1.0.tgz#605edbe3a6232853f9e8b57e3b905471d33b1a48"
+  integrity sha512-DWBK39IEt4FiQru12twzKSFUvFFZ7KtlH9+lAaqrJnKuIZFCyQ1XOUfKScfbKIlk4KS+DuCTNLI/pxC/f7Sk9Q==
+  dependencies:
+    d3-hierarchy "^3.1.0"
+    vega-dataflow "^5.7.3"
+    vega-util "^1.15.2"
+
+vega-label@~1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/vega-label/-/vega-label-1.2.0.tgz#bcb2659aec54f890f9debab3e41ab87a58292dce"
+  integrity sha512-1prOqkCAfXaUvMqavbGI0nbYGqV8UQR9qvuVwrPJ6Yxm3GIUIOA/JRqNY8eZR8USwMP/kzsqlfVEixj9+Y75VQ==
+  dependencies:
+    vega-canvas "^1.2.6"
+    vega-dataflow "^5.7.3"
+    vega-scenegraph "^4.9.2"
+    vega-util "^1.15.2"
+
+vega-lite@^5.1.0:
+  version "5.2.0"
+  resolved "https://registry.yarnpkg.com/vega-lite/-/vega-lite-5.2.0.tgz#bc3c5c70a38d9de8f3fb9644c7dd52f3b9f47a1b"
+  integrity sha512-Yxcg8MvYfxHcG6BbkaKT0oVCIMIcE19UvqIsEwBmyd/7h2nzW7oRnID81T8UrY7hpDrIr6wa2JADOT2dhGNErw==
+  dependencies:
+    "@types/clone" "~2.1.1"
+    array-flat-polyfill "^1.0.1"
+    clone "~2.1.2"
+    fast-deep-equal "~3.1.3"
+    fast-json-stable-stringify "~2.1.0"
+    json-stringify-pretty-compact "~3.0.0"
+    tslib "~2.3.1"
+    vega-event-selector "~3.0.0"
+    vega-expression "~5.0.0"
+    vega-util "~1.17.0"
+    yargs "~17.2.1"
+
+vega-loader@^4.3.2, vega-loader@^4.4.0, vega-loader@~4.5.0:
+  version "4.5.0"
+  resolved "https://registry.yarnpkg.com/vega-loader/-/vega-loader-4.5.0.tgz#b15acc4c8f84191f500e94d35ddfb9721ac943ad"
+  integrity sha512-EkAyzbx0pCYxH3v3wghGVCaKINWxHfgbQ2pYDiYv0yo8e04S8Mv/IlRGTt6BAe7cLhrk1WZ4zh20QOppnGG05w==
+  dependencies:
+    d3-dsv "^3.0.1"
+    node-fetch "^2.6.7"
+    topojson-client "^3.1.0"
+    vega-format "^1.1.0"
+    vega-util "^1.16.0"
+
+vega-parser@~6.1.4:
+  version "6.1.4"
+  resolved "https://registry.yarnpkg.com/vega-parser/-/vega-parser-6.1.4.tgz#4868e41af2c9645b6d7daeeb205cfad06b9d465c"
+  integrity sha512-tORdpWXiH/kkXcpNdbSVEvtaxBuuDtgYp9rBunVW9oLsjFvFXbSWlM1wvJ9ZFSaTfx6CqyTyGMiJemmr1QnTjQ==
+  dependencies:
+    vega-dataflow "^5.7.3"
+    vega-event-selector "^3.0.0"
+    vega-functions "^5.12.1"
+    vega-scale "^7.1.1"
+    vega-util "^1.16.0"
+
+vega-projection@^1.4.5, vega-projection@~1.5.0:
+  version "1.5.0"
+  resolved "https://registry.yarnpkg.com/vega-projection/-/vega-projection-1.5.0.tgz#51c5f0455170cd35b3c5f3e653e99c3616520640"
+  integrity sha512-aob7qojh555x3hQWZ/tr8cIJNSWQbm6EoWTJaheZgFOY2x3cDa4Qrg3RJbGw6KwVj/IQk2p40paRzixKZ2kr+A==
+  dependencies:
+    d3-geo "^3.0.1"
+    d3-geo-projection "^4.0.0"
+
+vega-regression@~1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/vega-regression/-/vega-regression-1.1.0.tgz#b4394db403ada93de52bb4536d04be336c983a8c"
+  integrity sha512-09K0RemY6cdaXBAyakDUNFfEkRcLkGjkDJyWQPAUqGK59hV2J+G3i4uxkZp18Vu0t8oqU7CgzwWim1s5uEpOcA==
+  dependencies:
+    d3-array "^3.1.1"
+    vega-dataflow "^5.7.3"
+    vega-statistics "^1.7.9"
+    vega-util "^1.15.2"
+
+vega-runtime@^6.1.3, vega-runtime@~6.1.3:
+  version "6.1.3"
+  resolved "https://registry.yarnpkg.com/vega-runtime/-/vega-runtime-6.1.3.tgz#01e18246f7a80cee034a96017ac30113b92c4034"
+  integrity sha512-gE+sO2IfxMUpV0RkFeQVnHdmPy3K7LjHakISZgUGsDI/ZFs9y+HhBf8KTGSL5pcZPtQsZh3GBQ0UonqL1mp9PA==
+  dependencies:
+    vega-dataflow "^5.7.3"
+    vega-util "^1.15.2"
+
+vega-scale@^7.0.3, vega-scale@^7.1.1, vega-scale@^7.2.0, vega-scale@~7.2.0:
+  version "7.2.0"
+  resolved "https://registry.yarnpkg.com/vega-scale/-/vega-scale-7.2.0.tgz#9e298cc02ad340498cb56847436b19439911f0fc"
+  integrity sha512-QYltO/otrZHLrCGGf06Y99XtPtqWXITr6rw7rO9oL+l3d9o5RFl9sjHrVxiM7v+vGoZVWbBd5IPbFhPsXZ6+TA==
+  dependencies:
+    d3-array "^3.1.1"
+    d3-interpolate "^3.0.1"
+    d3-scale "^4.0.2"
+    vega-time "^2.1.0"
+    vega-util "^1.17.0"
+
+vega-scenegraph@^4.10.0, vega-scenegraph@^4.9.2, vega-scenegraph@^4.9.3, vega-scenegraph@~4.10.1:
+  version "4.10.1"
+  resolved "https://registry.yarnpkg.com/vega-scenegraph/-/vega-scenegraph-4.10.1.tgz#944da67b8a28758fab2e1306259fb7ff6be89f6b"
+  integrity sha512-takIpkmNxYHhJYALOYzhTin3EDzbys6U4g+l1yJZVlXG9YTdiCMuEVAdtaQOCqF9/7qytD6pCrMxJY2HaoN0qQ==
+  dependencies:
+    d3-path "^3.0.1"
+    d3-shape "^3.1.0"
+    vega-canvas "^1.2.5"
+    vega-loader "^4.4.0"
+    vega-scale "^7.2.0"
+    vega-util "^1.15.2"
+
+vega-selections@^5.3.1:
+  version "5.4.0"
+  resolved "https://registry.yarnpkg.com/vega-selections/-/vega-selections-5.4.0.tgz#c2783897421fa39b674c015fa8f15a0023b8054e"
+  integrity sha512-Un3JdLDPjIpF9Dh4sw6m1c/QAcfam6m1YXHJ9vJxE/GdJ+sOrPxc7bcEU8VhOmTUN7IQUn4/1ry4JqqOVMbEhw==
+  dependencies:
+    d3-array "3.1.1"
+    vega-expression "^5.0.0"
+    vega-util "^1.16.0"
+
+vega-statistics@^1.7.9, vega-statistics@^1.8.0, vega-statistics@~1.8.0:
+  version "1.8.0"
+  resolved "https://registry.yarnpkg.com/vega-statistics/-/vega-statistics-1.8.0.tgz#ad66f7461473d58bc96671588981a059ffd60b59"
+  integrity sha512-dl+LCRS6qS4jWDme/NEdPVt5r649uB4IK6Kyr2/czmGA5JqjuFmtQ9lHQOnRu8945XLkqLf+JIQQo7vnw+nslA==
+  dependencies:
+    d3-array "^3.1.1"
+
+vega-time@^2.0.3, vega-time@^2.1.0, vega-time@~2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/vega-time/-/vega-time-2.1.0.tgz#acfbab88d7798b87ff63913b0dce2ca5eb0d46ca"
+  integrity sha512-Q9/l3S6Br1RPX5HZvyLD/cQ4K6K8DtpR09/1y7D66gxNorg2+HGzYZINH9nUvN3mxoXcBWg4cCUh3+JvmkDaEg==
+  dependencies:
+    d3-array "^3.1.1"
+    d3-time "^3.0.0"
+    vega-util "^1.15.2"
+
+vega-transforms@~4.10.0:
+  version "4.10.0"
+  resolved "https://registry.yarnpkg.com/vega-transforms/-/vega-transforms-4.10.0.tgz#a1017ede13cf4e25499f588610a3be4da615d82d"
+  integrity sha512-Yk6ByzVq5F2niFfPlSsrU5wi+NZhsF7IBpJCcTfms4U7eoyNepUXagdFEJ3VWBD/Lit6GorLXFgO17NYcyS5gg==
+  dependencies:
+    d3-array "^3.1.1"
+    vega-dataflow "^5.7.4"
+    vega-statistics "^1.8.0"
+    vega-time "^2.1.0"
+    vega-util "^1.16.1"
+
+vega-typings@~0.22.0:
+  version "0.22.3"
+  resolved "https://registry.yarnpkg.com/vega-typings/-/vega-typings-0.22.3.tgz#f6c73b5ffcdb152539cfcc5ad240a413af579ba7"
+  integrity sha512-PREcya3nXT9Tk7xU0IhEpOLVTlqizNtKXV55NhI6ApBjJtqVYbJL7IBh2ckKxGBy3YeUQ37BQZl56UqqiYVWBw==
+  dependencies:
+    vega-event-selector "^3.0.0"
+    vega-expression "^5.0.0"
+    vega-util "^1.15.2"
+
+vega-util@^1.15.2, vega-util@^1.16.0, vega-util@^1.16.1, vega-util@^1.17.0, vega-util@~1.17.0:
+  version "1.17.0"
+  resolved "https://registry.yarnpkg.com/vega-util/-/vega-util-1.17.0.tgz#b72ae0baa97f943bf591f8f5bb27ceadf06834ac"
+  integrity sha512-HTaydZd9De3yf+8jH66zL4dXJ1d1p5OIFyoBzFiOli4IJbwkL1jrefCKz6AHDm1kYBzDJ0X4bN+CzZSCTvNk1w==
+
+vega-view-transforms@~4.5.8:
+  version "4.5.8"
+  resolved "https://registry.yarnpkg.com/vega-view-transforms/-/vega-view-transforms-4.5.8.tgz#c8dc42c3c7d4aa725d40b8775180c9f23bc98f4e"
+  integrity sha512-966m7zbzvItBL8rwmF2nKG14rBp7q+3sLCKWeMSUrxoG+M15Smg5gWEGgwTG3A/RwzrZ7rDX5M1sRaAngRH25g==
+  dependencies:
+    vega-dataflow "^5.7.3"
+    vega-scenegraph "^4.9.2"
+    vega-util "^1.15.2"
+
+vega-view@~5.11.0:
+  version "5.11.0"
+  resolved "https://registry.yarnpkg.com/vega-view/-/vega-view-5.11.0.tgz#8a7b29a36776e43cc6599e087ed7f48a918b805d"
+  integrity sha512-MI9NTRFmtFX6ADk6KOHhi8bhHjC9pPm42Bj2+74c6l1d3NQZf9Jv7lkiGqKohdkQDNH9LPwz/6slhKwPU9JdkQ==
+  dependencies:
+    d3-array "^3.1.1"
+    d3-timer "^3.0.1"
+    vega-dataflow "^5.7.3"
+    vega-format "^1.1.0"
+    vega-functions "^5.13.0"
+    vega-runtime "^6.1.3"
+    vega-scenegraph "^4.10.0"
+    vega-util "^1.16.1"
+
+vega-voronoi@~4.2.0:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/vega-voronoi/-/vega-voronoi-4.2.0.tgz#14c74c84f52d9a16be2facd1bede879d32d988f2"
+  integrity sha512-1iuNAVZgUHRlBpdq4gSga3KlQmrgFfwy+KpyDgPLQ8HbLkhcVeT7RDh2L6naluqD7Op0xVLms3clR920WsYryQ==
+  dependencies:
+    d3-delaunay "^6.0.2"
+    vega-dataflow "^5.7.3"
+    vega-util "^1.15.2"
+
+vega-wordcloud@~4.1.3:
+  version "4.1.3"
+  resolved "https://registry.yarnpkg.com/vega-wordcloud/-/vega-wordcloud-4.1.3.tgz#ce90900333f4e0d3ee706ba4f36bb0905f8b4a9f"
+  integrity sha512-is4zYn9FMAyp9T4SAcz2P/U/wqc0Lx3P5YtpWKCbOH02a05vHjUQrQ2TTPOuvmMfAEDCSKvbMSQIJMOE018lJA==
+  dependencies:
+    vega-canvas "^1.2.5"
+    vega-dataflow "^5.7.3"
+    vega-scale "^7.1.1"
+    vega-statistics "^1.7.9"
+    vega-util "^1.15.2"
+
+vega@^5.20.0:
+  version "5.22.1"
+  resolved "https://registry.yarnpkg.com/vega/-/vega-5.22.1.tgz#e028f3645de18e0070317bc04410282975549e1e"
+  integrity sha512-KJBI7OWSzpfCPbmWl3GQCqBqbf2TIdpWS0mzO6MmWbvdMhWHf74P9IVnx1B1mhg0ZTqWFualx9ZYhWzMMwudaQ==
+  dependencies:
+    vega-crossfilter "~4.1.0"
+    vega-dataflow "~5.7.4"
+    vega-encode "~4.9.0"
+    vega-event-selector "~3.0.0"
+    vega-expression "~5.0.0"
+    vega-force "~4.1.0"
+    vega-format "~1.1.0"
+    vega-functions "~5.13.0"
+    vega-geo "~4.4.0"
+    vega-hierarchy "~4.1.0"
+    vega-label "~1.2.0"
+    vega-loader "~4.5.0"
+    vega-parser "~6.1.4"
+    vega-projection "~1.5.0"
+    vega-regression "~1.1.0"
+    vega-runtime "~6.1.3"
+    vega-scale "~7.2.0"
+    vega-scenegraph "~4.10.1"
+    vega-statistics "~1.8.0"
+    vega-time "~2.1.0"
+    vega-transforms "~4.10.0"
+    vega-typings "~0.22.0"
+    vega-util "~1.17.0"
+    vega-view "~5.11.0"
+    vega-view-transforms "~4.5.8"
+    vega-voronoi "~4.2.0"
+    vega-wordcloud "~4.1.3"
+
+verdaccio-audit@10.2.2:
+  version "10.2.2"
+  resolved "https://registry.yarnpkg.com/verdaccio-audit/-/verdaccio-audit-10.2.2.tgz#254380e57932fda64b45cb739e9c42cc9fb2dfdf"
+  integrity sha512-f2uZlKD7vi0yEB0wN8WOf+eA/3SCyKD9cvK17Hh7Wm8f/bl7k1B3hHOTtUCn/yu85DGsj2pcNzrAfp2wMVgz9Q==
+  dependencies:
+    body-parser "1.20.0"
+    express "4.18.1"
+    https-proxy-agent "5.0.1"
+    node-fetch "2.6.7"
+
+verdaccio-htpasswd@10.3.1:
+  version "10.3.1"
+  resolved "https://registry.yarnpkg.com/verdaccio-htpasswd/-/verdaccio-htpasswd-10.3.1.tgz#bda843dde3752e0ae2d4d2a5ba03fe922eef219a"
+  integrity sha512-FakeVHTyunBGurCTvkZgm89EkA9bxmcbUWrJ1rlUCFg48XpUtgHnemfH3pXRgItTX+cgLWENzK0jIpzkf7qUPw==
+  dependencies:
+    "@verdaccio/file-locking" "10.3.0"
+    apache-md5 "1.1.7"
+    bcryptjs "2.4.3"
+    http-errors "2.0.0"
+    unix-crypt-td-js "1.1.4"
+
+verdaccio@^5.1.1:
+  version "5.12.0"
+  resolved "https://registry.yarnpkg.com/verdaccio/-/verdaccio-5.12.0.tgz#dca3e44247b40e82db0f31a95ebbd9fd1720f2c0"
+  integrity sha512-2G/HcN21pVyyNtv4LyWYcGyScRwLZsC/b+MCG7AUinfG7aWbJmnNjj7grrCxnfDK1I9CVL3OnTVfveN9xsNFFg==
+  dependencies:
+    "@verdaccio/commons-api" "10.2.0"
+    "@verdaccio/local-storage" "10.3.0"
+    "@verdaccio/readme" "10.3.4"
+    "@verdaccio/streams" "10.2.0"
+    "@verdaccio/ui-theme" "6.0.0-6-next.24"
+    JSONStream "1.3.5"
+    async "3.2.4"
+    body-parser "1.20.0"
+    clipanion "3.1.0"
+    compression "1.7.4"
+    cookies "0.8.0"
+    cors "2.8.5"
+    dayjs "1.11.3"
+    debug "^4.3.3"
+    envinfo "7.8.1"
+    eslint-import-resolver-node "0.3.6"
+    express "4.18.1"
+    express-rate-limit "5.5.1"
+    fast-safe-stringify "2.1.1"
+    handlebars "4.7.7"
+    http-errors "2.0.0"
+    js-yaml "4.1.0"
+    jsonwebtoken "8.5.1"
+    kleur "4.1.4"
+    lodash "4.17.21"
+    lru-cache "7.10.1"
+    lunr-mutable-indexes "2.3.2"
+    marked "4.0.17"
+    memoizee "0.4.15"
+    mime "3.0.0"
+    minimatch "5.1.0"
+    mkdirp "1.0.4"
+    mv "2.1.1"
+    pino "6.14.0"
+    pkginfo "0.4.1"
+    prettier-bytes "^1.0.4"
+    pretty-ms "^7.0.1"
+    request "2.88.0"
+    semver "7.3.7"
+    validator "13.7.0"
+    verdaccio-audit "10.2.2"
+    verdaccio-htpasswd "10.3.1"
+
+verror@1.10.0:
+  version "1.10.0"
+  resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400"
+  integrity sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==
+  dependencies:
+    assert-plus "^1.0.0"
+    core-util-is "1.0.2"
+    extsprintf "^1.2.0"
+
+w3c-hr-time@^1.0.1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz#0a89cdf5cc15822df9c360543676963e0cc308cd"
+  integrity sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==
+  dependencies:
+    browser-process-hrtime "^1.0.0"
+
+w3c-xmlserializer@^1.1.2:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-1.1.2.tgz#30485ca7d70a6fd052420a3d12fd90e6339ce794"
+  integrity sha512-p10l/ayESzrBMYWRID6xbuCKh2Fp77+sA0doRuGn4tTIMrrZVeqfpKjXHY+oDh3K4nLdPgNwMTVP6Vp4pvqbNg==
+  dependencies:
+    domexception "^1.0.1"
+    webidl-conversions "^4.0.2"
+    xml-name-validator "^3.0.0"
+
+warning@^4.0.2, warning@^4.0.3:
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3"
+  integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==
+  dependencies:
+    loose-envify "^1.0.0"
+
+watchpack@^2.3.1:
+  version "2.4.0"
+  resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d"
+  integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==
+  dependencies:
+    glob-to-regexp "^0.4.1"
+    graceful-fs "^4.1.2"
+
+webidl-conversions@^3.0.0:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
+  integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==
+
+webidl-conversions@^4.0.2:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad"
+  integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==
+
+webpack-cli@^4.1.0:
+  version "4.10.0"
+  resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-4.10.0.tgz#37c1d69c8d85214c5a65e589378f53aec64dab31"
+  integrity sha512-NLhDfH/h4O6UOy+0LSso42xvYypClINuMNBVVzX4vX98TmTaTUxwRbXdhucbFMd2qLaCTcLq/PdYrvi8onw90w==
+  dependencies:
+    "@discoveryjs/json-ext" "^0.5.0"
+    "@webpack-cli/configtest" "^1.2.0"
+    "@webpack-cli/info" "^1.5.0"
+    "@webpack-cli/serve" "^1.7.0"
+    colorette "^2.0.14"
+    commander "^7.0.0"
+    cross-spawn "^7.0.3"
+    fastest-levenshtein "^1.0.12"
+    import-local "^3.0.2"
+    interpret "^2.2.0"
+    rechoir "^0.7.0"
+    webpack-merge "^5.7.3"
+
+webpack-merge@^5.1.2, webpack-merge@^5.7.3:
+  version "5.8.0"
+  resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-5.8.0.tgz#2b39dbf22af87776ad744c390223731d30a68f61"
+  integrity sha512-/SaI7xY0831XwP6kzuwhKWVKDP9t1QY1h65lAFLbZqMPIuYcD9QAW4u9STIbU9kaJbPBB/geU/gLr1wDjOhQ+Q==
+  dependencies:
+    clone-deep "^4.0.1"
+    wildcard "^2.0.0"
+
+webpack-sources@^1.1.0, webpack-sources@^1.2.0, webpack-sources@^1.4.3:
+  version "1.4.3"
+  resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.4.3.tgz#eedd8ec0b928fbf1cbfe994e22d2d890f330a933"
+  integrity sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==
+  dependencies:
+    source-list-map "^2.0.0"
+    source-map "~0.6.1"
+
+webpack-sources@^3.2.3:
+  version "3.2.3"
+  resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde"
+  integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==
+
+webpack@^5.41.1:
+  version "5.73.0"
+  resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.73.0.tgz#bbd17738f8a53ee5760ea2f59dce7f3431d35d38"
+  integrity sha512-svjudQRPPa0YiOYa2lM/Gacw0r6PvxptHj4FuEKQ2kX05ZLkjbVc5MnPs6its5j7IZljnIqSVo/OsY2X0IpHGA==
+  dependencies:
+    "@types/eslint-scope" "^3.7.3"
+    "@types/estree" "^0.0.51"
+    "@webassemblyjs/ast" "1.11.1"
+    "@webassemblyjs/wasm-edit" "1.11.1"
+    "@webassemblyjs/wasm-parser" "1.11.1"
+    acorn "^8.4.1"
+    acorn-import-assertions "^1.7.6"
+    browserslist "^4.14.5"
+    chrome-trace-event "^1.0.2"
+    enhanced-resolve "^5.9.3"
+    es-module-lexer "^0.9.0"
+    eslint-scope "5.1.1"
+    events "^3.2.0"
+    glob-to-regexp "^0.4.1"
+    graceful-fs "^4.2.9"
+    json-parse-even-better-errors "^2.3.1"
+    loader-runner "^4.2.0"
+    mime-types "^2.1.27"
+    neo-async "^2.6.2"
+    schema-utils "^3.1.0"
+    tapable "^2.1.1"
+    terser-webpack-plugin "^5.1.3"
+    watchpack "^2.3.1"
+    webpack-sources "^3.2.3"
+
+whatwg-encoding@^1.0.1, whatwg-encoding@^1.0.5:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz#5abacf777c32166a51d085d6b4f3e7d27113ddb0"
+  integrity sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==
+  dependencies:
+    iconv-lite "0.4.24"
+
+whatwg-mimetype@^2.2.0, whatwg-mimetype@^2.3.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf"
+  integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==
+
+whatwg-url@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"
+  integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==
+  dependencies:
+    tr46 "~0.0.3"
+    webidl-conversions "^3.0.0"
+
+whatwg-url@^7.0.0:
+  version "7.1.0"
+  resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.1.0.tgz#c2c492f1eca612988efd3d2266be1b9fc6170d06"
+  integrity sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==
+  dependencies:
+    lodash.sortby "^4.7.0"
+    tr46 "^1.0.1"
+    webidl-conversions "^4.0.2"
+
+which-boxed-primitive@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6"
+  integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==
+  dependencies:
+    is-bigint "^1.0.1"
+    is-boolean-object "^1.1.0"
+    is-number-object "^1.0.4"
+    is-string "^1.0.5"
+    is-symbol "^1.0.3"
+
+which@^1.2.9, which@^1.3.1:
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
+  integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==
+  dependencies:
+    isexe "^2.0.0"
+
+which@^2.0.1:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
+  integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
+  dependencies:
+    isexe "^2.0.0"
+
+wide-align@^1.1.2:
+  version "1.1.5"
+  resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.5.tgz#df1d4c206854369ecf3c9a4898f1b23fbd9d15d3"
+  integrity sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==
+  dependencies:
+    string-width "^1.0.2 || 2 || 3 || 4"
+
+wildcard@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.0.tgz#a77d20e5200c6faaac979e4b3aadc7b3dd7f8fec"
+  integrity sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==
+
+word-wrap@^1.2.3, word-wrap@~1.2.3:
+  version "1.2.3"
+  resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
+  integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==
+
+wordwrap@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
+  integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==
+
+worker-loader@^3.0.2:
+  version "3.0.8"
+  resolved "https://registry.yarnpkg.com/worker-loader/-/worker-loader-3.0.8.tgz#5fc5cda4a3d3163d9c274a4e3a811ce8b60dbb37"
+  integrity sha512-XQyQkIFeRVC7f7uRhFdNMe/iJOdO6zxAaR3EWbDp45v3mDhrTi+++oswKNxShUNjPC/1xUp5DB29YKLhFo129g==
+  dependencies:
+    loader-utils "^2.0.0"
+    schema-utils "^3.0.0"
+
+wrap-ansi@^7.0.0:
+  version "7.0.0"
+  resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
+  integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
+  dependencies:
+    ansi-styles "^4.0.0"
+    string-width "^4.1.0"
+    strip-ansi "^6.0.0"
+
+wrappy@1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
+  integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
+
+write-file-atomic@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-4.0.1.tgz#9faa33a964c1c85ff6f849b80b42a88c2c537c8f"
+  integrity sha512-nSKUxgAbyioruk6hU87QzVbY279oYT6uiwgDoujth2ju4mJ+TZau7SQBhtbTmUyuNYTuXnSyRn66FV0+eCgcrQ==
+  dependencies:
+    imurmurhash "^0.1.4"
+    signal-exit "^3.0.7"
+
+ws@^6.2.1:
+  version "6.2.2"
+  resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.2.tgz#dd5cdbd57a9979916097652d78f1cc5faea0c32e"
+  integrity sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw==
+  dependencies:
+    async-limiter "~1.0.0"
+
+ws@^7.0.0, ws@^7.4.6:
+  version "7.5.8"
+  resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.8.tgz#ac2729881ab9e7cbaf8787fe3469a48c5c7f636a"
+  integrity sha512-ri1Id1WinAX5Jqn9HejiGb8crfRio0Qgu8+MtL36rlTA6RLsMdWt1Az/19A2Qij6uSHUMphEFaTKa4WG+UNHNw==
+
+xml-name-validator@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a"
+  integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==
+
+xmlchars@^2.1.1:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"
+  integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==
+
+xtend@^4.0.2, xtend@~4.0.0:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
+  integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
+
+y-codemirror@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/y-codemirror/-/y-codemirror-3.0.1.tgz#d8a4e43cf46b5b557e0f03b7bbb65773ff436278"
+  integrity sha512-TsLSoouAZxkxOKbmTj7qdwZNS0lZMVqIdp7/j9EgUUqYj0remZYDGl6VBABrmp9UX1QvX6RoXXqzbNhftgfCbA==
+  dependencies:
+    lib0 "^0.2.42"
+
+y-leveldb@^0.1.0:
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/y-leveldb/-/y-leveldb-0.1.1.tgz#c2c35bc2b12a6c195b807a127c56c7c5a50cc610"
+  integrity sha512-L8Q0MQmxCQ0qWIOuPzLbWn95TNhrCI7M6LaHnilU4I2IX08e4Dmfg5Tgy4JZ3tnl2aiuZyDOJplHl/msIB/IsA==
+  dependencies:
+    level "^6.0.1"
+    lib0 "^0.2.31"
+
+y-protocols@^1.0.5:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/y-protocols/-/y-protocols-1.0.5.tgz#91d574250060b29fcac8f8eb5e276fbad594245e"
+  integrity sha512-Wil92b7cGk712lRHDqS4T90IczF6RkcvCwAD0A2OPg+adKmOe+nOiT/N2hvpQIWS3zfjmtL4CPaH5sIW1Hkm/A==
+  dependencies:
+    lib0 "^0.2.42"
+
+y-websocket@^1.3.15:
+  version "1.4.3"
+  resolved "https://registry.yarnpkg.com/y-websocket/-/y-websocket-1.4.3.tgz#4d4ca00832f0ab813e44a952f3088bd1eb5058f6"
+  integrity sha512-VobyJaAoyWIETETNZragnTpL7kcJr8a/CIUQP6DfXcQ4v0UmZUuANdsPsbmMjDsEeUECVFRhHauxpDtRhYqkaQ==
+  dependencies:
+    lib0 "^0.2.42"
+    lodash.debounce "^4.0.8"
+    y-protocols "^1.0.5"
+  optionalDependencies:
+    ws "^6.2.1"
+    y-leveldb "^0.1.0"
+
+y18n@^5.0.5:
+  version "5.0.8"
+  resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
+  integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==
+
+yallist@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
+  integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
+
+yaml@^1.10.0:
+  version "1.10.2"
+  resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"
+  integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==
+
+yargs-parser@^20.2.2, yargs-parser@^20.2.3:
+  version "20.2.9"
+  resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee"
+  integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==
+
+yargs@~17.2.1:
+  version "17.2.1"
+  resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.2.1.tgz#e2c95b9796a0e1f7f3bf4427863b42e0418191ea"
+  integrity sha512-XfR8du6ua4K6uLGm5S6fA+FIJom/MdJcFNVY8geLlp2v8GYbOXD4EB1tPNZsRn4vBzKGMgb5DRZMeWuFc2GO8Q==
+  dependencies:
+    cliui "^7.0.2"
+    escalade "^3.1.1"
+    get-caller-file "^2.0.5"
+    require-directory "^2.1.1"
+    string-width "^4.2.0"
+    y18n "^5.0.5"
+    yargs-parser "^20.2.2"
+
+yjs@^13.5.17:
+  version "13.5.39"
+  resolved "https://registry.yarnpkg.com/yjs/-/yjs-13.5.39.tgz#656763e61bfa0e50162657a8372ac2a50d955c6a"
+  integrity sha512-EoVT856l301lomtjjVspgTdSRiFqZ7gNKnmVPX4/V8NHI5EYS39/MdjB9iNv0Mw1weKDZRU8NgxgerqwJ3y2xA==
+  dependencies:
+    lib0 "^0.2.49"
+
+yocto-queue@^0.1.0:
+  version "0.1.0"
+  resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
+  integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test-docs-python-nbextensions.yml
similarity index 91%
rename from .github/workflows/test.yml
rename to .github/workflows/test-docs-python-nbextensions.yml
index 18a11aeeb..fcd190a01 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test-docs-python-nbextensions.yml
@@ -1,7 +1,7 @@
 # This is a GitHub workflow defining a set of jobs with a set of steps.
 # ref: https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-syntax-for-github-actions
 #
-name: Test
+name: Test python-nbextensions-doc
 
 on:
   pull_request:
@@ -58,6 +58,10 @@ jobs:
               echo "GROUP=python" >> $GITHUB_ENV
           fi
       - uses: actions/checkout@v2
+      - name: Install node
+        uses: actions/setup-node@v1
+        with:
+          node-version: '17.x'
       # NOTE: actions/setup-python@v2 can make use of a cache within the GitHub
       #       Action virtual environment and setup extremely fast. 3.9 isn't
       #       available in this cache as of November 2020.
@@ -74,6 +78,9 @@ jobs:
           python -m pip install --upgrade pip wheel setuptools
           python tasks.py install --group="$GROUP"
           pip freeze
+      - name: Install Playwright
+        run: |
+          npx playwright install
       - name: Run pytest
         run: |
           python tasks.py tests --group="$GROUP"
diff --git a/.github/workflows/test-labextensions.yml b/.github/workflows/test-labextensions.yml
new file mode 100644
index 000000000..a9416d1d2
--- /dev/null
+++ b/.github/workflows/test-labextensions.yml
@@ -0,0 +1,69 @@
+# This is a GitHub workflow defining a set of jobs with a set of steps.
+# ref: https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-syntax-for-github-actions
+#
+name: Test labextensions
+
+on:
+  pull_request:
+    branches: ["**"]
+  push:
+    branches: [main, master]
+    tags: ["**"]
+
+defaults:
+  run:
+    shell: bash
+
+jobs:
+  test_labextensions:
+    runs-on: ${{ matrix.os }}
+    timeout-minutes: 40
+
+    env:
+      # NOTE: UTF-8 content may be interpreted as ascii and causes errors
+      #       without this.
+      LANG: C.UTF-8
+      MOZ_HEADLESS: 1
+
+    strategy:
+      fail-fast: false
+      matrix:
+        os: [windows-latest, ubuntu-20.04]
+        python: ["3.9", "3.10"]
+    steps:
+      # This is how you set an environment variable in a GitHub workflow that
+      # will be available in following steps as if you would used `export
+      # MY_ENV=my-value`.
+      - name: Configure environment variables from job flags
+        run: |
+          echo "GROUP=labextensions" >> $GITHUB_ENV
+      - uses: actions/checkout@v2
+      - name: Install node
+        uses: actions/setup-node@v1
+        with:
+          node-version: '17.x'
+      # NOTE: actions/setup-python@v2 can make use of a cache within the GitHub
+      #       Action virtual environment and setup extremely fast. 3.9 isn't
+      #       available in this cache as of November 2020.
+      - name: Install Python ${{ matrix.python }}
+        uses: actions/setup-python@v2
+        with:
+          python-version: ${{ matrix.python }}
+      - name: Install Python dependencies
+        run: |
+          python -m pip install --upgrade pip wheel setuptools
+          python tasks.py install --group=labextensions
+          pip freeze
+      - name: Install Playwright
+        run: |
+          npx playwright install
+      - name: Run pytest
+        run: |
+          python tasks.py tests --group=labextensions
+      - name: Upload Playwright Test report
+        if: always()
+        uses: actions/upload-artifact@v2
+        with:
+          name: nbgrader-playwright-tests-os_${{ matrix.os }}-python_${{ matrix.python }}
+          path: |
+            playwright-tests
diff --git a/conftest.py b/conftest.py
new file mode 100644
index 000000000..7df7d7c03
--- /dev/null
+++ b/conftest.py
@@ -0,0 +1,8 @@
+import pytest
+
+pytest_plugins = ["jupyter_server.pytest_plugin"]
+
+
+@pytest.fixture
+def jp_server_config(jp_server_config):
+    return {"ServerApp": {"jpserver_extensions": {"nbgrader": True}}}
diff --git a/nbgrader/docs/source/contributor_guide/testing.rst b/nbgrader/docs/source/contributor_guide/testing.rst
index b75c8a98a..46acbe1f8 100644
--- a/nbgrader/docs/source/contributor_guide/testing.rst
+++ b/nbgrader/docs/source/contributor_guide/testing.rst
@@ -27,6 +27,9 @@ To run a selective group of tests you can use one of the following commands:
 | ``python tasks.py tests --group=nbextensions`` | Run tests only for the notebook    |
 |                                                | extensions                         |
 +------------------------------------------------+------------------------------------+
+| ``python tasks.py tests --group=labextensions``| Run tests only for the jupyter lab |
+|                                                | extensions                         |
++------------------------------------------------+------------------------------------+
 | ``python tasks.py tests --group=docs``         | Build the docs                     |
 +------------------------------------------------+------------------------------------+
 | ``python tasks.py tests --group=all``          | Same as ``python tasks.py tests``  |
diff --git a/nbgrader/docs/source/user_guide/images/autograder_tests.png b/nbgrader/docs/source/user_guide/images/autograder_tests.png
deleted file mode 100644
index 1a65cdc21..000000000
Binary files a/nbgrader/docs/source/user_guide/images/autograder_tests.png and /dev/null differ
diff --git a/nbgrader/docs/source/user_guide/images/autograder_tests_hidden_tests.png b/nbgrader/docs/source/user_guide/images/autograder_tests_hidden_tests.png
deleted file mode 100644
index 9c50dadac..000000000
Binary files a/nbgrader/docs/source/user_guide/images/autograder_tests_hidden_tests.png and /dev/null differ
diff --git a/nbgrader/docs/source/user_guide/images/autograder_tests_hidden_tests_jlab.png b/nbgrader/docs/source/user_guide/images/autograder_tests_hidden_tests_jlab.png
new file mode 100644
index 000000000..d99e3f715
Binary files /dev/null and b/nbgrader/docs/source/user_guide/images/autograder_tests_hidden_tests_jlab.png differ
diff --git a/nbgrader/docs/source/user_guide/images/autograder_tests_jlab.png b/nbgrader/docs/source/user_guide/images/autograder_tests_jlab.png
new file mode 100644
index 000000000..d27e19a03
Binary files /dev/null and b/nbgrader/docs/source/user_guide/images/autograder_tests_jlab.png differ
diff --git a/nbgrader/tests/labextension_ui-tests/files/blank.ipynb b/nbgrader/tests/labextension_ui-tests/files/blank.ipynb
new file mode 100644
index 000000000..610e781bc
--- /dev/null
+++ b/nbgrader/tests/labextension_ui-tests/files/blank.ipynb
@@ -0,0 +1,42 @@
+{
+ "cells": [
+  {
+   "cell_type": "code",
+   "execution_count": 1,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "this is a cell\n"
+     ]
+    }
+   ],
+   "source": [
+    "print(\"this is a code cell\")"
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3 (ipykernel)",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.9.12"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
diff --git a/nbgrader/tests/labextension_ui-tests/files/data.txt b/nbgrader/tests/labextension_ui-tests/files/data.txt
new file mode 100644
index 000000000..0f8a25a9d
--- /dev/null
+++ b/nbgrader/tests/labextension_ui-tests/files/data.txt
@@ -0,0 +1,4 @@
+line 1
+line 2
+line 3
+line 4
\ No newline at end of file
diff --git a/nbgrader/tests/labextension_ui-tests/files/empty.ipynb b/nbgrader/tests/labextension_ui-tests/files/empty.ipynb
new file mode 100644
index 000000000..fab2b41af
--- /dev/null
+++ b/nbgrader/tests/labextension_ui-tests/files/empty.ipynb
@@ -0,0 +1,32 @@
+{
+ "cells": [
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": []
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3 (ipykernel)",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.9.12"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
diff --git a/nbgrader/tests/labextension_ui-tests/files/nbgrader_config.py b/nbgrader/tests/labextension_ui-tests/files/nbgrader_config.py
new file mode 100644
index 000000000..5eb35c351
--- /dev/null
+++ b/nbgrader/tests/labextension_ui-tests/files/nbgrader_config.py
@@ -0,0 +1,2 @@
+c = get_config()
+c.Execute.execute_retries = 4
diff --git a/nbgrader/tests/labextension_ui-tests/files/old-schema.ipynb b/nbgrader/tests/labextension_ui-tests/files/old-schema.ipynb
new file mode 100644
index 000000000..f4e40cb76
--- /dev/null
+++ b/nbgrader/tests/labextension_ui-tests/files/old-schema.ipynb
@@ -0,0 +1,50 @@
+{
+ "cells": [
+  {
+   "cell_type": "code",
+   "execution_count": 1,
+   "metadata": {
+    "nbgrader": {
+     "cell_type": "code",
+     "grade": false,
+     "grade_id": "set_a",
+     "locked": false,
+     "solution": true
+    }
+   },
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "this is a cell\n"
+     ]
+    }
+   ],
+   "source": [
+    "print(\"this is a code cell\")"
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3 (ipykernel)",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.9.12"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
diff --git a/nbgrader/tests/labextension_ui-tests/files/open_relative_file.ipynb b/nbgrader/tests/labextension_ui-tests/files/open_relative_file.ipynb
new file mode 100644
index 000000000..9c2028ae0
--- /dev/null
+++ b/nbgrader/tests/labextension_ui-tests/files/open_relative_file.ipynb
@@ -0,0 +1,49 @@
+{
+ "cells": [
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "deletable": true,
+    "editable": true,
+    "nbgrader": {
+     "grade": true,
+     "grade_id": "open_file",
+     "locked": true,
+     "points": 10,
+     "schema_version": 3,
+     "solution": false
+    },
+    "tags": []
+   },
+   "outputs": [],
+   "source": [
+    "with open(\"data.txt\", \"r\") as f:\n",
+    "    data = f.read()\n",
+    "    \n",
+    "assert len(data.split(\"\\n\")) == 4"
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3 (ipykernel)",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.9.12"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
diff --git a/nbgrader/tests/labextension_ui-tests/files/submitted-answer-cell-type-changed.ipynb b/nbgrader/tests/labextension_ui-tests/files/submitted-answer-cell-type-changed.ipynb
new file mode 100644
index 000000000..68e9f7001
--- /dev/null
+++ b/nbgrader/tests/labextension_ui-tests/files/submitted-answer-cell-type-changed.ipynb
@@ -0,0 +1,166 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "metadata": {
+    "deletable": false,
+    "nbgrader": {
+     "cell_type": "code",
+     "checksum": "3d6aac4236f8e1ec85380e692dcc51b1",
+     "grade": false,
+     "grade_id": "set_a",
+     "locked": false,
+     "schema_version": 3,
+     "solution": true
+    }
+   },
+   "source": [
+    "# YOUR CODE HERE\n",
+    "raise NotImplementedError()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "deletable": false,
+    "nbgrader": {
+     "cell_type": "code",
+     "checksum": "8bb5c7c6f388fae724e5ef53dc4deeb2",
+     "grade": true,
+     "grade_id": "foo",
+     "locked": false,
+     "points": 1,
+     "schema_version": 3,
+     "solution": false
+    },
+    "tags": []
+   },
+   "outputs": [],
+   "source": [
+    "print(\"Success!\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "deletable": false,
+    "nbgrader": {
+     "cell_type": "code",
+     "checksum": "75d78cdf605a339809ceaace462c5f33",
+     "grade": true,
+     "grade_id": "bar",
+     "locked": false,
+     "points": 1,
+     "schema_version": 3,
+     "solution": false
+    },
+    "tags": []
+   },
+   "outputs": [],
+   "source": [
+    "assert a == 1"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {
+    "deletable": false,
+    "nbgrader": {
+     "cell_type": "markdown",
+     "checksum": "9e51fd0022c24c4105e38369d2f9d751",
+     "grade": true,
+     "grade_id": "baz",
+     "locked": false,
+     "points": 2,
+     "schema_version": 3,
+     "solution": true
+    }
+   },
+   "source": [
+    "YOUR ANSWER HERE"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "nbgrader": {
+     "cell_type": "code",
+     "checksum": "5a193c164d7b444efe9a3612bee09f4c",
+     "grade": true,
+     "grade_id": "quux",
+     "locked": false,
+     "points": 3,
+     "schema_version": 3,
+     "solution": true
+    },
+    "tags": []
+   },
+   "outputs": [],
+   "source": [
+    "# YOUR CODE HERE\n",
+    "raise NotImplementedError()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "nbgrader": {
+     "cell_type": "code",
+     "checksum": "86f5f877fe95faac003fcd4b8d43d093",
+     "grade": false,
+     "grade_id": "ro1",
+     "locked": true,
+     "schema_version": 3,
+     "solution": false
+    },
+    "tags": []
+   },
+   "outputs": [],
+   "source": [
+    "print(\"Don't change this cell!\")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {
+    "nbgrader": {
+     "cell_type": "markdown",
+     "checksum": "0122b50e5eaf367b9874d07ebaf80521",
+     "grade": false,
+     "grade_id": "ro2",
+     "locked": true,
+     "schema_version": 3,
+     "solution": false
+    }
+   },
+   "source": [
+    "This cell shouldn't be changed."
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3 (ipykernel)",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.9.12"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
diff --git a/nbgrader/tests/labextension_ui-tests/files/submitted-changed.ipynb b/nbgrader/tests/labextension_ui-tests/files/submitted-changed.ipynb
new file mode 100644
index 000000000..c07f912fd
--- /dev/null
+++ b/nbgrader/tests/labextension_ui-tests/files/submitted-changed.ipynb
@@ -0,0 +1,169 @@
+{
+ "cells": [
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "deletable": false,
+    "nbgrader": {
+     "cell_type": "code",
+     "checksum": "3d6aac4236f8e1ec85380e692dcc51b1",
+     "grade": false,
+     "grade_id": "set_a",
+     "locked": false,
+     "schema_version": 3,
+     "solution": true
+    },
+    "tags": []
+   },
+   "outputs": [],
+   "source": [
+    "# YOUR CODE HERE\n",
+    "a = 1"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "deletable": false,
+    "nbgrader": {
+     "cell_type": "code",
+     "checksum": "8bb5c7c6f388fae724e5ef53dc4deeb2",
+     "grade": true,
+     "grade_id": "foo",
+     "locked": false,
+     "points": 1,
+     "schema_version": 3,
+     "solution": false
+    },
+    "tags": []
+   },
+   "outputs": [],
+   "source": [
+    "print(\"Success!\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "deletable": false,
+    "nbgrader": {
+     "cell_type": "code",
+     "checksum": "75d78cdf605a339809ceaace462c5f33",
+     "grade": true,
+     "grade_id": "bar",
+     "locked": false,
+     "points": 1,
+     "schema_version": 3,
+     "solution": false
+    },
+    "tags": []
+   },
+   "outputs": [],
+   "source": [
+    "assert a == 1"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {
+    "deletable": false,
+    "nbgrader": {
+     "cell_type": "markdown",
+     "checksum": "9e51fd0022c24c4105e38369d2f9d751",
+     "grade": true,
+     "grade_id": "baz",
+     "locked": false,
+     "points": 2,
+     "schema_version": 3,
+     "solution": true
+    }
+   },
+   "source": [
+    "An answer!"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "nbgrader": {
+     "cell_type": "code",
+     "checksum": "5a193c164d7b444efe9a3612bee09f4c",
+     "grade": true,
+     "grade_id": "quux",
+     "locked": false,
+     "points": 3,
+     "schema_version": 3,
+     "solution": true
+    },
+    "tags": []
+   },
+   "outputs": [],
+   "source": [
+    "# YOUR CODE HERE\n",
+    "b = 2"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "nbgrader": {
+     "cell_type": "code",
+     "checksum": "86f5f877fe95faac003fcd4b8d43d093",
+     "grade": false,
+     "grade_id": "ro1",
+     "locked": true,
+     "schema_version": 3,
+     "solution": false
+    },
+    "tags": []
+   },
+   "outputs": [],
+   "source": [
+    "print(\"Don't change this cell!\")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {
+    "nbgrader": {
+     "cell_type": "markdown",
+     "checksum": "0122b50e5eaf367b9874d07ebaf80521",
+     "grade": false,
+     "grade_id": "ro2",
+     "locked": true,
+     "schema_version": 3,
+     "solution": false
+    }
+   },
+   "source": [
+    "This cell shouldn't be changed."
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3 (ipykernel)",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.9.12"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
diff --git a/nbgrader/tests/labextension_ui-tests/files/submitted-grade-cell-changed.ipynb b/nbgrader/tests/labextension_ui-tests/files/submitted-grade-cell-changed.ipynb
new file mode 100644
index 000000000..deb502980
--- /dev/null
+++ b/nbgrader/tests/labextension_ui-tests/files/submitted-grade-cell-changed.ipynb
@@ -0,0 +1,169 @@
+{
+ "cells": [
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "deletable": false,
+    "nbgrader": {
+     "cell_type": "code",
+     "checksum": "3d6aac4236f8e1ec85380e692dcc51b1",
+     "grade": false,
+     "grade_id": "set_a",
+     "locked": false,
+     "schema_version": 3,
+     "solution": true
+    },
+    "tags": []
+   },
+   "outputs": [],
+   "source": [
+    "# YOUR CODE HERE\n",
+    "raise NotImplementedError()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "deletable": false,
+    "nbgrader": {
+     "cell_type": "code",
+     "checksum": "8bb5c7c6f388fae724e5ef53dc4deeb2",
+     "grade": true,
+     "grade_id": "foo",
+     "locked": false,
+     "points": 1,
+     "schema_version": 3,
+     "solution": false
+    },
+    "tags": []
+   },
+   "outputs": [],
+   "source": [
+    "print(\"Success!\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "deletable": false,
+    "nbgrader": {
+     "cell_type": "code",
+     "checksum": "75d78cdf605a339809ceaace462c5f33",
+     "grade": true,
+     "grade_id": "bar",
+     "locked": false,
+     "points": 1,
+     "schema_version": 3,
+     "solution": false
+    },
+    "tags": []
+   },
+   "outputs": [],
+   "source": [
+    "#assert a == 1"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {
+    "deletable": false,
+    "nbgrader": {
+     "cell_type": "markdown",
+     "checksum": "9e51fd0022c24c4105e38369d2f9d751",
+     "grade": true,
+     "grade_id": "baz",
+     "locked": false,
+     "points": 2,
+     "schema_version": 3,
+     "solution": true
+    }
+   },
+   "source": [
+    "YOUR ANSWER HERE"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "nbgrader": {
+     "cell_type": "code",
+     "checksum": "5a193c164d7b444efe9a3612bee09f4c",
+     "grade": true,
+     "grade_id": "quux",
+     "locked": false,
+     "points": 3,
+     "schema_version": 3,
+     "solution": true
+    },
+    "tags": []
+   },
+   "outputs": [],
+   "source": [
+    "# YOUR CODE HERE\n",
+    "raise NotImplementedError()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "nbgrader": {
+     "cell_type": "code",
+     "checksum": "86f5f877fe95faac003fcd4b8d43d093",
+     "grade": false,
+     "grade_id": "ro1",
+     "locked": true,
+     "schema_version": 3,
+     "solution": false
+    },
+    "tags": []
+   },
+   "outputs": [],
+   "source": [
+    "print(\"Don't change this cell!\")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {
+    "nbgrader": {
+     "cell_type": "markdown",
+     "checksum": "0122b50e5eaf367b9874d07ebaf80521",
+     "grade": false,
+     "grade_id": "ro2",
+     "locked": true,
+     "schema_version": 3,
+     "solution": false
+    }
+   },
+   "source": [
+    "This cell shouldn't be changed."
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3 (ipykernel)",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.9.12"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
diff --git a/nbgrader/tests/labextension_ui-tests/files/submitted-grade-cell-type-changed.ipynb b/nbgrader/tests/labextension_ui-tests/files/submitted-grade-cell-type-changed.ipynb
new file mode 100644
index 000000000..33c4246e6
--- /dev/null
+++ b/nbgrader/tests/labextension_ui-tests/files/submitted-grade-cell-type-changed.ipynb
@@ -0,0 +1,166 @@
+{
+ "cells": [
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "deletable": false,
+    "nbgrader": {
+     "cell_type": "code",
+     "checksum": "3d6aac4236f8e1ec85380e692dcc51b1",
+     "grade": false,
+     "grade_id": "set_a",
+     "locked": false,
+     "schema_version": 3,
+     "solution": true
+    },
+    "tags": []
+   },
+   "outputs": [],
+   "source": [
+    "# YOUR CODE HERE\n",
+    "raise NotImplementedError()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "deletable": false,
+    "nbgrader": {
+     "cell_type": "code",
+     "checksum": "8bb5c7c6f388fae724e5ef53dc4deeb2",
+     "grade": true,
+     "grade_id": "foo",
+     "locked": false,
+     "points": 1,
+     "schema_version": 3,
+     "solution": false
+    },
+    "tags": []
+   },
+   "outputs": [],
+   "source": [
+    "print(\"Success!\")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {
+    "deletable": false,
+    "nbgrader": {
+     "cell_type": "code",
+     "checksum": "75d78cdf605a339809ceaace462c5f33",
+     "grade": true,
+     "grade_id": "bar",
+     "locked": false,
+     "points": 1,
+     "schema_version": 3,
+     "solution": false
+    }
+   },
+   "source": [
+    "assert a == 1"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {
+    "deletable": false,
+    "nbgrader": {
+     "cell_type": "markdown",
+     "checksum": "9e51fd0022c24c4105e38369d2f9d751",
+     "grade": true,
+     "grade_id": "baz",
+     "locked": false,
+     "points": 2,
+     "schema_version": 3,
+     "solution": true
+    }
+   },
+   "source": [
+    "YOUR ANSWER HERE"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "nbgrader": {
+     "cell_type": "code",
+     "checksum": "5a193c164d7b444efe9a3612bee09f4c",
+     "grade": true,
+     "grade_id": "quux",
+     "locked": false,
+     "points": 3,
+     "schema_version": 3,
+     "solution": true
+    },
+    "tags": []
+   },
+   "outputs": [],
+   "source": [
+    "# YOUR CODE HERE\n",
+    "raise NotImplementedError()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "nbgrader": {
+     "cell_type": "code",
+     "checksum": "86f5f877fe95faac003fcd4b8d43d093",
+     "grade": false,
+     "grade_id": "ro1",
+     "locked": true,
+     "schema_version": 3,
+     "solution": false
+    },
+    "tags": []
+   },
+   "outputs": [],
+   "source": [
+    "print(\"Don't change this cell!\")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {
+    "nbgrader": {
+     "cell_type": "markdown",
+     "checksum": "0122b50e5eaf367b9874d07ebaf80521",
+     "grade": false,
+     "grade_id": "ro2",
+     "locked": true,
+     "schema_version": 3,
+     "solution": false
+    }
+   },
+   "source": [
+    "This cell shouldn't be changed."
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3 (ipykernel)",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.9.12"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
diff --git a/nbgrader/tests/labextension_ui-tests/files/submitted-locked-cell-changed.ipynb b/nbgrader/tests/labextension_ui-tests/files/submitted-locked-cell-changed.ipynb
new file mode 100644
index 000000000..3246bc025
--- /dev/null
+++ b/nbgrader/tests/labextension_ui-tests/files/submitted-locked-cell-changed.ipynb
@@ -0,0 +1,170 @@
+{
+ "cells": [
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "deletable": false,
+    "nbgrader": {
+     "cell_type": "code",
+     "checksum": "3d6aac4236f8e1ec85380e692dcc51b1",
+     "grade": false,
+     "grade_id": "set_a",
+     "locked": false,
+     "schema_version": 3,
+     "solution": true
+    },
+    "tags": []
+   },
+   "outputs": [],
+   "source": [
+    "# YOUR CODE HERE\n",
+    "raise NotImplementedError()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "deletable": false,
+    "nbgrader": {
+     "cell_type": "code",
+     "checksum": "8bb5c7c6f388fae724e5ef53dc4deeb2",
+     "grade": true,
+     "grade_id": "foo",
+     "locked": false,
+     "points": 1,
+     "schema_version": 3,
+     "solution": false
+    },
+    "tags": []
+   },
+   "outputs": [],
+   "source": [
+    "print(\"Success!\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "deletable": false,
+    "nbgrader": {
+     "cell_type": "code",
+     "checksum": "75d78cdf605a339809ceaace462c5f33",
+     "grade": true,
+     "grade_id": "bar",
+     "locked": false,
+     "points": 1,
+     "schema_version": 3,
+     "solution": false
+    },
+    "tags": []
+   },
+   "outputs": [],
+   "source": [
+    "assert a == 1"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {
+    "deletable": false,
+    "nbgrader": {
+     "cell_type": "markdown",
+     "checksum": "9e51fd0022c24c4105e38369d2f9d751",
+     "grade": true,
+     "grade_id": "baz",
+     "locked": false,
+     "points": 2,
+     "schema_version": 3,
+     "solution": true
+    }
+   },
+   "source": [
+    "YOUR ANSWER HERE"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "nbgrader": {
+     "cell_type": "code",
+     "checksum": "5a193c164d7b444efe9a3612bee09f4c",
+     "grade": true,
+     "grade_id": "quux",
+     "locked": false,
+     "points": 3,
+     "schema_version": 3,
+     "solution": true
+    },
+    "tags": []
+   },
+   "outputs": [],
+   "source": [
+    "# YOUR CODE HERE\n",
+    "raise NotImplementedError()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "nbgrader": {
+     "cell_type": "code",
+     "checksum": "86f5f877fe95faac003fcd4b8d43d093",
+     "grade": false,
+     "grade_id": "ro1",
+     "locked": true,
+     "schema_version": 3,
+     "solution": false
+    },
+    "tags": []
+   },
+   "outputs": [],
+   "source": [
+    "#print(\"Don't change this cell!\")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {
+    "nbgrader": {
+     "cell_type": "markdown",
+     "checksum": "0122b50e5eaf367b9874d07ebaf80521",
+     "grade": false,
+     "grade_id": "ro2",
+     "locked": true,
+     "schema_version": 3,
+     "solution": false
+    }
+   },
+   "source": [
+    "This cell shouldn't \n",
+    "be changed."
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3 (ipykernel)",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.9.12"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
diff --git a/nbgrader/tests/labextension_ui-tests/files/submitted-unchanged.ipynb b/nbgrader/tests/labextension_ui-tests/files/submitted-unchanged.ipynb
new file mode 100644
index 000000000..c60cd516e
--- /dev/null
+++ b/nbgrader/tests/labextension_ui-tests/files/submitted-unchanged.ipynb
@@ -0,0 +1,169 @@
+{
+ "cells": [
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "deletable": false,
+    "nbgrader": {
+     "cell_type": "code",
+     "checksum": "3d6aac4236f8e1ec85380e692dcc51b1",
+     "grade": false,
+     "grade_id": "set_a",
+     "locked": false,
+     "schema_version": 3,
+     "solution": true
+    },
+    "tags": []
+   },
+   "outputs": [],
+   "source": [
+    "# YOUR CODE HERE\n",
+    "raise NotImplementedError()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "deletable": false,
+    "nbgrader": {
+     "cell_type": "code",
+     "checksum": "8bb5c7c6f388fae724e5ef53dc4deeb2",
+     "grade": true,
+     "grade_id": "foo",
+     "locked": false,
+     "points": 1,
+     "schema_version": 3,
+     "solution": false
+    },
+    "tags": []
+   },
+   "outputs": [],
+   "source": [
+    "print(\"Success!\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "deletable": false,
+    "nbgrader": {
+     "cell_type": "code",
+     "checksum": "75d78cdf605a339809ceaace462c5f33",
+     "grade": true,
+     "grade_id": "bar",
+     "locked": false,
+     "points": 1,
+     "schema_version": 3,
+     "solution": false
+    },
+    "tags": []
+   },
+   "outputs": [],
+   "source": [
+    "assert a == 1"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {
+    "deletable": false,
+    "nbgrader": {
+     "cell_type": "markdown",
+     "checksum": "9e51fd0022c24c4105e38369d2f9d751",
+     "grade": true,
+     "grade_id": "baz",
+     "locked": false,
+     "points": 2,
+     "schema_version": 3,
+     "solution": true
+    }
+   },
+   "source": [
+    "YOUR ANSWER HERE"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "nbgrader": {
+     "cell_type": "code",
+     "checksum": "5a193c164d7b444efe9a3612bee09f4c",
+     "grade": true,
+     "grade_id": "quux",
+     "locked": false,
+     "points": 3,
+     "schema_version": 3,
+     "solution": true
+    },
+    "tags": []
+   },
+   "outputs": [],
+   "source": [
+    "# YOUR CODE HERE\n",
+    "raise NotImplementedError()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "nbgrader": {
+     "cell_type": "code",
+     "checksum": "86f5f877fe95faac003fcd4b8d43d093",
+     "grade": false,
+     "grade_id": "ro1",
+     "locked": true,
+     "schema_version": 3,
+     "solution": false
+    },
+    "tags": []
+   },
+   "outputs": [],
+   "source": [
+    "print(\"Don't change this cell!\")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {
+    "nbgrader": {
+     "cell_type": "markdown",
+     "checksum": "0122b50e5eaf367b9874d07ebaf80521",
+     "grade": false,
+     "grade_id": "ro2",
+     "locked": true,
+     "schema_version": 3,
+     "solution": false
+    }
+   },
+   "source": [
+    "This cell shouldn't be changed."
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3 (ipykernel)",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.9.12"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
diff --git a/nbgrader/tests/labextension_ui-tests/files/task.ipynb b/nbgrader/tests/labextension_ui-tests/files/task.ipynb
new file mode 100644
index 000000000..f7908fc1c
--- /dev/null
+++ b/nbgrader/tests/labextension_ui-tests/files/task.ipynb
@@ -0,0 +1,32 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "This will become a task cell"
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3 (ipykernel)",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.9.12"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
diff --git a/nbgrader/tests/labextension_ui-tests/jupyter_server_test_config.py b/nbgrader/tests/labextension_ui-tests/jupyter_server_test_config.py
new file mode 100644
index 000000000..7ebc1d00e
--- /dev/null
+++ b/nbgrader/tests/labextension_ui-tests/jupyter_server_test_config.py
@@ -0,0 +1,18 @@
+"""Server configuration for integration tests.
+!! Never use this configuration in production because it
+opens the server to the world and provide access to JupyterLab
+JavaScript objects through the global window variable.
+"""
+from tempfile import mkdtemp
+
+c.ServerApp.port = 8888
+c.ServerApp.port_retries = 0
+c.ServerApp.open_browser = False
+
+c.ServerApp.token = ""
+c.ServerApp.password = ""
+c.ServerApp.disable_check_xsrf = True
+c.LabApp.expose_app_in_browser = True
+
+# Uncomment to set server log level to debug level
+# c.ServerApp.log_level = "DEBUG"
diff --git a/nbgrader/tests/labextension_ui-tests/run_jupyter_lab.py b/nbgrader/tests/labextension_ui-tests/run_jupyter_lab.py
new file mode 100644
index 000000000..51c6fdff8
--- /dev/null
+++ b/nbgrader/tests/labextension_ui-tests/run_jupyter_lab.py
@@ -0,0 +1,24 @@
+import sys
+import os
+from os.path import join
+import shutil
+import subprocess
+
+from tempfile import mkdtemp
+
+root_dir = mkdtemp(prefix="galata-test-")
+env = os.environ.copy()
+
+shutil.copyfile("./nbgrader/tests/labextension_ui-tests/jupyter_server_test_config.py",
+                join(root_dir, "jupyter_server_test_config.py"))
+
+shutil.copyfile("./nbgrader/tests/labextension_ui-tests/files/nbgrader_config.py",
+                join(root_dir, "nbgrader_config.py"))
+
+subprocess.check_call([sys.executable, "-m", "jupyter", "server", "extension", "enable", "--user", "--py", "nbgrader"], env=env)
+subprocess.check_call([sys.executable, "-m", "jupyter", "labextension", "develop", "."])
+
+os.chdir(root_dir)
+
+command = ["jupyter", "lab", "--config", "./jupyter_server_test_config.py"]
+subprocess.run(command)
diff --git a/nbgrader/tests/labextension_ui-tests/tests/test_assignment_list.spec.ts b/nbgrader/tests/labextension_ui-tests/tests/test_assignment_list.spec.ts
new file mode 100644
index 000000000..e313db592
--- /dev/null
+++ b/nbgrader/tests/labextension_ui-tests/tests/test_assignment_list.spec.ts
@@ -0,0 +1,670 @@
+import { test, galata, IJupyterLabPageFixture } from '@jupyterlab/galata';
+import { expect, Locator, Page } from '@playwright/test';
+import {
+  execute_command,
+  create_env,
+  wait_for_error_modal,
+  close_error_modal,
+  wait_for_success_modal,
+  close_success_modal} from './test_utils';
+
+import * as path from 'path';
+import * as os from 'os';
+import * as fs from 'fs';
+
+test.use({ tmpPath: 'nbgrader-assignment-list-test' });
+
+var exchange_dir:string;
+var cache_dir: string;
+
+const is_windows = os.platform().startsWith('win')
+
+/*
+ * Create environment
+ */
+test.beforeEach(async ({ baseURL, tmpPath }) => {
+
+  const contents = galata.newContentsHelper(baseURL);
+
+  await contents.createDirectory(tmpPath);
+
+  if (!is_windows){
+    exchange_dir = fs.mkdtempSync(path.join(os.tmpdir(), 'nbgrader_exchange_test_'));
+    cache_dir = fs.mkdtempSync(path.join(os.tmpdir(), 'nbgrader_cache_test_'));
+  }
+});
+
+/*
+ * delete temp directories at the end of test
+ */
+test.afterEach(async ({ baseURL, tmpPath }) => {
+  if (!is_windows){
+    fs.rmSync(exchange_dir, { recursive: true, force: true });
+    fs.rmSync(cache_dir, { recursive: true, force: true });
+  }
+
+  const contents = galata.newContentsHelper(baseURL);
+  await contents.deleteDirectory(tmpPath);
+
+  if (contents.fileExists("nbgrader_config.py")) contents.deleteFile("nbgrader_config.py");
+  contents.uploadFile(path.resolve(__dirname, "../files/nbgrader_config.py"), "nbgrader_config.py");
+});
+
+
+/*
+ * Create a nbgrader file system and modify config
+ */
+const add_courses = async (page:IJupyterLabPageFixture, baseURL:string, tmpPath:string) => {
+
+  const contents = galata.newContentsHelper(baseURL);
+
+  // copy files from the user guide
+  const source = path.resolve(__dirname, "..", "..", "..", "docs", "source", "user_guide", "source");
+  await contents.uploadDirectory(source, `${tmpPath}/source`);
+  await contents.renameDirectory(`${tmpPath}/source/ps1`, `${tmpPath}/source/Problem Set 1`);
+  await contents.renameFile(`${tmpPath}/source/Problem Set 1/problem1.ipynb`, `${tmpPath}/source/Problem Set 1/Problem 1.ipynb`)
+  await contents.renameFile(`${tmpPath}/source/Problem Set 1/problem2.ipynb`, `${tmpPath}/source/Problem Set 1/Problem 2.ipynb`)
+  await contents.createDirectory(`${tmpPath}/source/ps.01`);
+  await contents.uploadFile(
+    path.resolve(__dirname, '..', 'files', 'empty.ipynb'),
+    `${tmpPath}/source/ps.01/problem 1.ipynb`
+  )
+
+  const jupyter_config_content = await page.locator('#jupyter-config-data').textContent();
+  const rootDir = JSON.parse(jupyter_config_content)['serverRoot'];
+
+  // Necessary to generate and release assignments
+  fs.copyFileSync(path.resolve(rootDir, "nbgrader_config.py"), path.resolve(rootDir, tmpPath, "nbgrader_config.py"));
+}
+
+/*
+ * Open the assignment list tab from palette
+ */
+const open_assignment_list = async (page:IJupyterLabPageFixture) => {
+
+  await expect(page.locator("#jp-main-dock-panel .lm-TabBar-tab.p-TabBar-tab")).toHaveCount(1);
+
+  await page.keyboard.press('Control+Shift+c');
+  await page.locator('#modal-command-palette li[data-command="nbgrader:open-assignment-list"]').click();
+
+  await expect(page.locator("#jp-main-dock-panel .lm-TabBar-tab.p-TabBar-tab")).toHaveCount(2);
+
+  var tabs = page.locator("#jp-main-dock-panel .lm-TabBar-tab.p-TabBar-tab");
+  var newTab_label = tabs.last().locator(".lm-TabBar-tabLabel.p-TabBar-tabLabel");
+  await expect(newTab_label).toHaveText("Assignments");
+
+}
+
+/*
+ * Ensure that list of assignment has been loaded for a specific name ("released", "fetched", "submitted")
+ */
+const wait_for_list = async(page:IJupyterLabPageFixture, name:string, nb_rows:number): Promise<Locator> => {
+  await expect(page.locator(`#${name}_assignments_list_loading`)).not.toBeVisible();
+  await expect(page.locator(`#${name}_assignments_list_placeholder`)).not.toBeVisible();
+  await expect(page.locator(`#${name}_assignments_list_error`)).not.toBeVisible();
+
+  const rows = page.locator(`#${name}_assignments_list > .list_item`)
+  await expect(rows).toHaveCount(nb_rows);
+  return rows;
+}
+
+/*
+ * Select a course in dropdown list
+ */
+const select_course = async(page:IJupyterLabPageFixture, course:string) => {
+  await page.locator('#course_list_dropdown').click();
+  await page.locator(`#course_list > li :text("${course}")`).click();
+  await expect(page.locator('#course_list_default')).toHaveText(course);
+}
+
+/*
+ * Expand a fetched assignment
+ */
+const expand_fetched = async(page:IJupyterLabPageFixture, assignment:string, item_id:string): Promise<Locator> => {
+  await page.locator(`#fetched_assignments_list a:text("${assignment}")`).click();
+  await page.waitForSelector(`${item_id}.collapse.in`);
+
+  const rows = page.locator(`${item_id} .list_item`);
+  for (var i=1; i < await rows.count(); i++){
+    expect(rows.nth(i)).toBeVisible();
+  }
+  return rows;
+}
+
+/*
+ * Collapse an expended fetched assignment
+ */
+const collapse_fetched = async(page:IJupyterLabPageFixture, assignment:string, item_id:string) => {
+  await page.locator(`#fetched_assignments_list a:text("${assignment}")`).click();
+  await expect(page.locator(`${item_id}.collapse`)).not.toHaveClass('in');
+}
+
+
+/*
+ * Test opening assignment list tab from menu
+ */
+test('Open assignment list tab from menu', async({
+  page
+  }) => {
+
+    await expect(page.locator("#jp-main-dock-panel .lm-TabBar-tab.p-TabBar-tab")).toHaveCount(1);
+    // Check main menu exists
+    await expect(page.locator('#jp-menu-panel div.lm-MenuBar-itemLabel.p-MenuBar-itemLabel:text("Nbgrader")')).toHaveCount(1);
+    await page.locator('#jp-menu-panel div.lm-MenuBar-itemLabel.p-MenuBar-itemLabel:text("Nbgrader")').click();
+
+    await page.locator('#jp-mainmenu-nbgrader-menu li[data-command="nbgrader:open-assignment-list"]').click();
+
+    await expect(page.locator("#jp-main-dock-panel .lm-TabBar-tab.p-TabBar-tab")).toHaveCount(2);
+
+    var tabs = page.locator("#jp-main-dock-panel .lm-TabBar-tab.p-TabBar-tab");
+    var newTab_label = tabs.last().locator(".lm-TabBar-tabLabel.p-TabBar-tabLabel");
+    await expect(newTab_label).toHaveText("Assignments");
+
+});
+
+/*
+ * Test showing assignment list
+ */
+test('Show assignment list', async({
+  page,
+  baseURL,
+  tmpPath
+  }) => {
+
+    test.skip(is_windows, 'This feature is not implemented for Windows');
+
+    await create_env(page, tmpPath, exchange_dir, cache_dir, is_windows);
+    await add_courses(page, baseURL, tmpPath);
+    await open_assignment_list(page);
+
+    // Wait for DOM of each status
+    await page.waitForSelector('#released_assignments_list');
+    await page.waitForSelector('#fetched_assignments_list');
+    await page.waitForSelector('#submitted_assignments_list');
+
+    // release an assignment
+    await execute_command("nbgrader generate_assignment 'Problem Set 1' --force");
+    await execute_command("nbgrader release_assignment 'Problem Set 1' --course 'abc101' --force");
+
+    // refresh assignments
+    await page.locator('#refresh_assignments_list').click();
+
+    // expect finding the released assignment
+    const rows = await wait_for_list(page, 'released', 1);
+    expect(rows.first().locator('.item_name')).toHaveText("Problem Set 1");
+    expect(rows.first().locator('.item_course')).toHaveText("abc101");
+
+  });
+
+/*
+ * Test multiple released assignments
+ */
+test('Multiple released assignments', async({
+  page,
+  baseURL,
+  tmpPath
+  }) => {
+
+    test.skip(is_windows, 'This feature is not implemented for Windows');
+
+    await create_env(page, tmpPath, exchange_dir, cache_dir, is_windows);
+    await add_courses(page, baseURL, tmpPath);
+    await open_assignment_list(page);
+
+    // release two assignments
+    await execute_command("nbgrader generate_assignment 'Problem Set 1' --force");
+    await execute_command("nbgrader release_assignment 'Problem Set 1' --course 'abc101' --force");
+    await execute_command("nbgrader generate_assignment 'ps.01' --force");
+    await execute_command("nbgrader release_assignment 'ps.01' --course 'xyz 200' --force");
+
+    // refresh assignments
+    await page.locator('#refresh_assignments_list').click();
+
+    // select a course
+    await select_course(page, 'xyz 200');
+
+    // expect finding the released assignment for selected course
+    const rows = await wait_for_list(page, 'released', 1);
+    expect(rows.first().locator('.item_name')).toHaveText("ps.01");
+    expect(rows.first().locator('.item_course')).toHaveText("xyz 200");
+
+  });
+
+/*
+ * Test fetch assignment
+ */
+test('Fetch assignments', async({
+  page,
+  baseURL,
+  tmpPath
+  }) => {
+
+    test.skip(is_windows, 'This feature is not implemented for Windows');
+
+    await create_env(page, tmpPath, exchange_dir, cache_dir, is_windows);
+    await add_courses(page, baseURL, tmpPath);
+    await open_assignment_list(page);
+
+    // release some assignments
+    await execute_command("nbgrader generate_assignment 'Problem Set 1' --force");
+    await execute_command("nbgrader release_assignment 'Problem Set 1' --course 'abc101' --force");
+    await execute_command("nbgrader generate_assignment 'ps.01' --force");
+    await execute_command("nbgrader release_assignment 'ps.01' --course 'xyz 200' --force");
+
+    // refresh assignment list
+    await page.locator('#refresh_assignments_list').click();
+
+    // select one course
+    await select_course(page, 'xyz 200');
+
+    // check that there is only one released, and fetch it
+    var rows = await wait_for_list(page, 'released', 1);
+    await rows.first().locator('.item_status button').click()
+
+    // check that there is only one fetched
+    rows = await wait_for_list(page, 'fetched', 1);
+    expect(rows.first().locator('.item_name').first()).toHaveText("ps.01");
+    expect(rows.first().locator('.item_course').first()).toHaveText("xyz 200");
+
+    // check that the directory has been created
+    const contents = galata.newContentsHelper(baseURL);
+    expect(contents.directoryExists('ps.01'));
+
+    // expand assignment notebooks
+    rows = await expand_fetched(page, "ps.01", "#nbgrader-xyz_200-ps01");
+    await expect(rows).toHaveCount(2);
+    await expect(rows.last().locator('.item_name')).toHaveText('problem 1');
+
+    // collapse assignments notebooks
+    await collapse_fetched(page, "ps.01", "#nbgrader-xyz_200-ps01");
+
+  });
+
+/*
+ * Test submit assignment
+ */
+test('Submit assignments', async({
+  page,
+  baseURL,
+  tmpPath
+  }) => {
+
+    test.skip(is_windows, 'This feature is not implemented for Windows');
+
+    // create directories and config files, and open assignment_list tab
+    await create_env(page, tmpPath, exchange_dir, cache_dir, is_windows);
+    await add_courses(page, baseURL, tmpPath);
+    await open_assignment_list(page);
+
+    // release some assignments
+    await execute_command("nbgrader generate_assignment 'Problem Set 1' --force");
+    await execute_command("nbgrader release_assignment 'Problem Set 1' --course 'abc101' --force");
+    await execute_command("nbgrader generate_assignment 'ps.01' --force");
+    await execute_command("nbgrader release_assignment 'ps.01' --course 'xyz 200' --force");
+
+    // refresh assignment list
+    await page.locator('#refresh_assignments_list').click();
+
+    // select one course
+    await select_course(page, 'xyz 200');
+
+    // check that there is only one released, and fetch it
+    var rows = await wait_for_list(page, 'released', 1);
+    await rows.first().locator('.item_status button').click();
+
+    // check that there is only one fetched and submit
+    rows = await wait_for_list(page, 'fetched', 1);
+    await rows.first().locator('.item_status button:text("Submit")').click();
+
+    // check that there is only one submitted
+    rows = await wait_for_list(page, 'submitted', 1);
+    expect(rows.first().locator('.item_name').first()).toHaveText("ps.01");
+    expect(rows.first().locator('.item_course').first()).toHaveText("xyz 200");
+
+    // check again that there is only one submitted for that course
+    rows = page.locator('#nbgrader-xyz_200-ps01-submissions > .list_item');
+    // the first row should be empty
+    expect(rows).toHaveCount(2);
+
+    // submit a second time
+    rows = await wait_for_list(page, 'fetched', 1);
+    await rows.first().locator('.item_status button:text("Submit")').click();
+
+    // check there are two submitted (the first row is empty)
+    await expect(page.locator('#nbgrader-xyz_200-ps01-submissions > .list_item')).toHaveCount(3);
+    rows = page.locator('#nbgrader-xyz_200-ps01-submissions > .list_item');
+
+    const timestamp1 = rows.nth(1).locator(".item_name").textContent();
+    const timestamp2 = rows.nth(2).locator(".item_name").textContent();
+    expect(timestamp1 != timestamp2);
+
+  });
+
+/*
+ * Test submitting assignment without notebook
+ */
+test('submit assignment missing notebook', async ({
+  page,
+  baseURL,
+  tmpPath
+  }) => {
+
+    test.skip(is_windows, 'This feature is not implemented for Windows');
+
+    // create directories and config files, and open assignment_list tab
+    const rootDir = await create_env(page, tmpPath, exchange_dir, cache_dir, is_windows);
+    await add_courses(page, baseURL, tmpPath);
+    await open_assignment_list(page);
+
+    // release some assignments
+    await execute_command("nbgrader generate_assignment 'Problem Set 1' --force");
+    await execute_command("nbgrader release_assignment 'Problem Set 1' --course 'abc101' --force");
+    await execute_command("nbgrader generate_assignment 'ps.01' --force");
+    await execute_command("nbgrader release_assignment 'ps.01' --course 'xyz 200' --force");
+
+    // refresh assignment list
+    await page.locator('#refresh_assignments_list').click();
+
+    // select one course
+    await select_course(page, 'xyz 200');
+
+    // check that there is only one released, and fetch it
+    var rows = await wait_for_list(page, 'released', 1);
+    await rows.first().locator('.item_status button').click();
+
+    // check that there is only one fetched and submit
+    rows = await wait_for_list(page, 'fetched', 1);
+    await rows.first().locator('.item_status button:text("Submit")').click();
+
+    // check that there is only one submitted
+    rows = await wait_for_list(page, 'submitted', 1);
+    expect(rows.first().locator('.item_name').first()).toHaveText("ps.01");
+    expect(rows.first().locator('.item_course').first()).toHaveText("xyz 200");
+    rows = page.locator('#nbgrader-xyz_200-ps01-submissions > .list_item');
+    expect(rows).toHaveCount(2);
+
+    // rename the assignment notebook file
+    const contents = galata.newContentsHelper(baseURL);
+    expect(await contents.fileExists(`${tmpPath}/ps.01/problem 1.ipynb`));
+    await contents.renameFile(
+      `${tmpPath}/ps.01/problem 1.ipynb`,
+      `${tmpPath}/ps.01/my problem 1.ipynb`
+    );
+
+    // submit again and check it has submitted
+    rows = await wait_for_list(page, 'fetched', 1);
+    await rows.first().locator('.item_status button:text("Submit")').click();
+    rows = await wait_for_list(page, 'submitted', 1);
+    expect(rows.first().locator('.item_name').first()).toHaveText("ps.01");
+    expect(rows.first().locator('.item_course').first()).toHaveText("xyz 200");
+    rows = page.locator('#nbgrader-xyz_200-ps01-submissions > .list_item');
+    expect(rows).toHaveCount(3);
+
+    const timestamp1 = rows.nth(1).locator(".item_name").textContent();
+    const timestamp2 = rows.nth(2).locator(".item_name").textContent();
+    expect(timestamp1 != timestamp2);
+
+    // Set strict flag
+    fs.appendFileSync(path.resolve(rootDir, tmpPath, "nbgrader_config.py"), 'c.ExchangeSubmit.strict = True');
+
+    // submit again and check that nothing changes
+    rows = await wait_for_list(page, 'fetched', 1);
+    await rows.first().locator('.item_status button:text("Submit")').click();
+
+    // wait for error modal and close it
+    await wait_for_error_modal(page);
+    await close_error_modal(page);
+
+    // check that nothing has change in submitted list
+    rows = await wait_for_list(page, 'submitted', 1);
+    expect(rows.first().locator('.item_name').first()).toHaveText("ps.01");
+    expect(rows.first().locator('.item_course').first()).toHaveText("xyz 200");
+    rows = page.locator('#nbgrader-xyz_200-ps01-submissions > .list_item');
+    expect(rows).toHaveCount(3);
+
+});
+
+/*
+ * Test fetch a second assignment
+ */
+test('Fetch a second assignment', async({
+  page,
+  baseURL,
+  tmpPath
+  }) => {
+
+    test.skip(is_windows, 'This feature is not implemented for Windows');
+
+    await create_env(page, tmpPath, exchange_dir, cache_dir, is_windows);
+    await add_courses(page, baseURL, tmpPath);
+    await open_assignment_list(page);
+
+    // release some assignments
+    await execute_command("nbgrader generate_assignment 'Problem Set 1' --force");
+    await execute_command("nbgrader release_assignment 'Problem Set 1' --course 'abc101' --force");
+    await execute_command("nbgrader generate_assignment 'ps.01' --force");
+    await execute_command("nbgrader release_assignment 'ps.01' --course 'xyz 200' --force");
+
+    // refresh assignment list
+    await page.locator('#refresh_assignments_list').click();
+
+    // select one course
+    await select_course(page, 'xyz 200');
+
+    // check that there is only one released, and fetch it
+    var rows = await wait_for_list(page, 'released', 1);
+    await rows.first().locator('.item_status button').click()
+
+    // select the other course
+    await select_course(page, 'abc101');
+
+    // check that there is only one released, and fetch it
+    var rows = await wait_for_list(page, 'released', 1);
+    await rows.first().locator('.item_status button').click()
+
+    // check that there is only one fetched
+    rows = await wait_for_list(page, 'fetched', 1);
+    expect(rows.first().locator('.item_name').first()).toHaveText("Problem Set 1");
+    expect(rows.first().locator('.item_course').first()).toHaveText("abc101");
+
+    // check that the directory has been created
+    const contents = galata.newContentsHelper(baseURL);
+    expect(contents.directoryExists('Problem Set 1'));
+
+    // expand assignment notebooks
+    rows = await expand_fetched(page, "Problem Set 1", "#nbgrader-abc101-Problem_Set_1");
+    await expect(rows).toHaveCount(3);
+    await expect(rows.nth(1).locator('.item_name')).toHaveText('Problem 1');
+    await expect(rows.last().locator('.item_name')).toHaveText('Problem 2');
+
+    // collapse assignments notebooks
+    await collapse_fetched(page, "Problem Set 1", "#nbgrader-abc101-Problem_Set_1");
+
+  });
+
+/*
+ * Test submit another assignment
+ */
+test('Submit another assignments', async({
+  page,
+  baseURL,
+  tmpPath
+  }) => {
+
+    test.skip(is_windows, 'This feature is not implemented for Windows');
+
+    // create directories and config files, and open assignment_list tab
+    await create_env(page, tmpPath, exchange_dir, cache_dir, is_windows);
+    await add_courses(page, baseURL, tmpPath);
+    await open_assignment_list(page);
+
+    // release some assignments
+    await execute_command("nbgrader generate_assignment 'Problem Set 1' --force");
+    await execute_command("nbgrader release_assignment 'Problem Set 1' --course 'abc101' --force");
+    await execute_command("nbgrader generate_assignment 'ps.01' --force");
+    await execute_command("nbgrader release_assignment 'ps.01' --course 'xyz 200' --force");
+
+    // refresh assignment list
+    await page.locator('#refresh_assignments_list').click();
+
+    // select one course
+    await select_course(page, 'abc101');
+
+    // check that there is only one released, and fetch it
+    var rows = await wait_for_list(page, 'released', 1);
+    await rows.first().locator('.item_status button').click();
+
+    // check that there is only one fetched and submit
+    rows = await wait_for_list(page, 'fetched', 1);
+    await rows.first().locator('.item_status button:text("Submit")').click();
+
+    // check that there is only one submitted
+    rows = await wait_for_list(page, 'submitted', 1);
+    expect(rows.first().locator('.item_name').first()).toHaveText("Problem Set 1");
+    expect(rows.first().locator('.item_course').first()).toHaveText("abc101");
+
+    // check again that there is only one submitted for that course
+    rows = page.locator('#nbgrader-abc101-Problem_Set_1-submissions > .list_item');
+    // the first row should be empty
+    expect(rows).toHaveCount(2);
+
+  });
+
+/*
+ * Test validate assignment
+ */
+test('Validate OK', async({
+  page,
+  baseURL,
+  tmpPath
+  }) => {
+
+    test.skip(is_windows, 'This feature is not implemented for Windows');
+
+    // create directories and config files, and open assignment_list tab
+    await create_env(page, tmpPath, exchange_dir, cache_dir, is_windows);
+    await add_courses(page, baseURL, tmpPath);
+    await open_assignment_list(page);
+
+    // release some assignments
+    await execute_command("nbgrader generate_assignment 'Problem Set 1' --force");
+    await execute_command("nbgrader release_assignment 'Problem Set 1' --course 'abc101' --force");
+    await execute_command("nbgrader generate_assignment 'ps.01' --force");
+    await execute_command("nbgrader release_assignment 'ps.01' --course 'xyz 200' --force");
+
+    // refresh assignment list
+    await page.locator('#refresh_assignments_list').click();
+
+    // select one course
+    await select_course(page, 'xyz 200');
+
+    // check that there is only one released, and fetch it
+    var rows = await wait_for_list(page, 'released', 1);
+    await rows.first().locator('.item_status button').click();
+
+    // check that there is only one fetched and submit
+    rows = await wait_for_list(page, 'fetched', 1);
+
+    // expand assignment notebooks
+    rows = await expand_fetched(page, "ps.01", "#nbgrader-xyz_200-ps01");
+    await expect(rows).toHaveCount(2);
+    await expect(rows.last().locator('.item_name')).toHaveText('problem 1');
+
+    // Click on validate
+    await rows.last().locator('.item_status button:text("Validate")').click()
+
+    await wait_for_success_modal(page);
+    await close_success_modal(page);
+
+});
+
+/*
+ * Test validation failure
+ */
+test('Validate failure', async({
+  page,
+  baseURL,
+  tmpPath
+  }) => {
+
+    test.skip(is_windows, 'This feature is not implemented for Windows');
+
+    // create directories and config files, and open assignment_list tab
+    await create_env(page, tmpPath, exchange_dir, cache_dir, is_windows);
+    await add_courses(page, baseURL, tmpPath);
+    await open_assignment_list(page);
+
+    // release some assignments
+    await execute_command("nbgrader generate_assignment 'Problem Set 1' --force");
+    await execute_command("nbgrader release_assignment 'Problem Set 1' --course 'abc101' --force");
+    await execute_command("nbgrader generate_assignment 'ps.01' --force");
+    await execute_command("nbgrader release_assignment 'ps.01' --course 'xyz 200' --force");
+
+    // refresh assignment list
+    await page.locator('#refresh_assignments_list').click();
+
+    // select one course
+    await select_course(page, 'abc101');
+
+    // check that there is only one released, and fetch it
+    var rows = await wait_for_list(page, 'released', 1);
+    await rows.first().locator('.item_status button').click();
+
+    // check that there is only one fetched and submit
+    rows = await wait_for_list(page, 'fetched', 1);
+
+    // expand assignment notebooks
+    rows = await expand_fetched(page, "Problem Set 1", "#nbgrader-abc101-Problem_Set_1");
+    await expect(rows).toHaveCount(3);
+    await expect(rows.nth(1).locator('.item_name')).toHaveText('Problem 1');
+    await expect(rows.last().locator('.item_name')).toHaveText('Problem 2');
+
+    // Click on validate
+    await rows.last().locator('.item_status button:text("Validate")').click()
+    await wait_for_error_modal(page);
+    await close_error_modal(page);
+
+});
+
+/*
+ * Test missing exchange directory
+ */
+test('Missing exchange directory', async({
+  page,
+  baseURL,
+  tmpPath
+  }) => {
+
+    test.skip(is_windows, 'This feature is not implemented for Windows');
+
+    // create directories and config files
+    await create_env(page, tmpPath, exchange_dir, cache_dir, is_windows);
+    await add_courses(page, baseURL, tmpPath);
+
+    // delete exchange directory
+    fs.rmSync(exchange_dir, { recursive: true, force: true });
+
+    // open assignment_list tab
+    await open_assignment_list(page);
+
+    // Expecting error on lists and dropdown
+    await expect(page.locator(`#released_assignments_list_error`)).toBeVisible();
+    await expect(page.locator(`#fetched_assignments_list_error`)).toBeVisible();
+    await expect(page.locator(`#submitted_assignments_list_error`)).toBeVisible();
+
+    await expect(page.locator('#course_list_default')).toHaveText("Error fetching courses!");
+
+    // create exchange directory again
+    fs.mkdtempSync(exchange_dir);
+
+    // release assignment
+    await execute_command("nbgrader generate_assignment 'Problem Set 1' --force");
+    await execute_command("nbgrader release_assignment 'Problem Set 1' --course 'abc101' --force");
+
+    // refresh assignment list and expect retrieving released assignment
+    await page.locator('#refresh_assignments_list').click();
+    const rows = await wait_for_list(page, 'released', 1);
+    expect(rows.first().locator('.item_name')).toHaveText("Problem Set 1");
+    expect(rows.first().locator('.item_course')).toHaveText("abc101");
+
+});
diff --git a/nbgrader/tests/labextension_ui-tests/tests/test_course_list.spec.ts b/nbgrader/tests/labextension_ui-tests/tests/test_course_list.spec.ts
new file mode 100644
index 000000000..b916dc6c5
--- /dev/null
+++ b/nbgrader/tests/labextension_ui-tests/tests/test_course_list.spec.ts
@@ -0,0 +1,192 @@
+import { test, galata, IJupyterLabPageFixture} from '@jupyterlab/galata';
+import { expect } from '@playwright/test';
+import { create_env } from './test_utils';
+
+import * as fs from 'fs';
+import * as os from 'os';
+import * as path from 'path';
+
+
+test.use({ tmpPath: 'nbgrader-course-list-test' });
+
+var exchange_dir:string;
+var cache_dir: string;
+
+const is_windows = os.platform().startsWith('win')
+
+/*
+ * Create environment
+ */
+test.beforeEach(async ({ baseURL, tmpPath }) => {
+
+  const contents = galata.newContentsHelper(baseURL);
+
+  await contents.createDirectory(tmpPath);
+
+  if (!is_windows){
+    exchange_dir = fs.mkdtempSync(path.join(os.tmpdir(), 'nbgrader_exchange_test_'));
+    cache_dir = fs.mkdtempSync(path.join(os.tmpdir(), 'nbgrader_cache_test_'));
+  }
+});
+
+/*
+ * delete temp directories at the end of test
+ */
+test.afterEach(async ({ baseURL, tmpPath }) => {
+  const contents = galata.newContentsHelper(baseURL);
+  await contents.deleteDirectory(tmpPath);
+
+  if (!is_windows){
+    fs.rmSync(exchange_dir, { recursive: true, force: true });
+    fs.rmSync(cache_dir, { recursive: true, force: true });
+  }
+
+  if (contents.fileExists("nbgrader_config.py")) contents.deleteFile("nbgrader_config.py");
+  contents.uploadFile(path.resolve(__dirname, "../files/nbgrader_config.py"), "nbgrader_config.py");
+});
+
+/*
+ * Open a courses list tab
+ */
+const open_courses_list = async (page:IJupyterLabPageFixture) => {
+
+  await expect(page.locator("#jp-main-dock-panel .lm-TabBar-tab.p-TabBar-tab")).toHaveCount(1);
+
+  await page.keyboard.press('Control+Shift+c');
+  await page.locator('#modal-command-palette li[data-command="nbgrader:open-course-list"]').click();
+
+  await expect(page.locator("#jp-main-dock-panel .lm-TabBar-tab.p-TabBar-tab")).toHaveCount(2);
+
+  var tabs = page.locator("#jp-main-dock-panel .lm-TabBar-tab.p-TabBar-tab");
+  var newTab_label = tabs.last().locator(".lm-TabBar-tabLabel.p-TabBar-tabLabel");
+  await expect(newTab_label).toHaveText("Courses");
+  await page.waitForSelector("#formgrader_list");
+}
+
+/*
+ * Modify config file
+ */
+const update_config = async (page:IJupyterLabPageFixture, rootDir:string) => {
+
+  var text_to_append = `
+c.CourseDirectory.course_id = "course101"
+
+`
+
+  fs.appendFileSync(path.resolve(rootDir, "nbgrader_config.py"), text_to_append);
+}
+
+/*
+ * TODO: package the 4 extensions individually to be able to install/enable/disable each.
+*/
+// test('No formgrader', async ({
+//   page
+//   }) => {
+
+//     test.skip(is_windows, 'This feature is not implemented for Windows');
+//     await open_courses_list(page);
+
+//   }
+// );
+
+/*
+ * Test opening course list tab from menu
+ */
+test('Open course list tab from menu', async({
+  page
+  }) => {
+
+    await expect(page.locator("#jp-main-dock-panel .lm-TabBar-tab.p-TabBar-tab")).toHaveCount(1);
+
+    // Check main menu exists
+    await expect(page.locator('#jp-menu-panel div.lm-MenuBar-itemLabel.p-MenuBar-itemLabel:text("Nbgrader")')).toHaveCount(1);
+    await page.locator('#jp-menu-panel div.lm-MenuBar-itemLabel.p-MenuBar-itemLabel:text("Nbgrader")').click();
+
+    await page.locator('#jp-mainmenu-nbgrader-menu li[data-command="nbgrader:open-course-list"]').click();
+
+    await expect(page.locator("#jp-main-dock-panel .lm-TabBar-tab.p-TabBar-tab")).toHaveCount(2);
+
+    var tabs = page.locator("#jp-main-dock-panel .lm-TabBar-tab.p-TabBar-tab");
+    var newTab_label = tabs.last().locator(".lm-TabBar-tabLabel.p-TabBar-tabLabel");
+    await expect(newTab_label).toHaveText("Courses");
+
+});
+
+/*
+ * Test a local formgrader, expecting existing courses are local and opening formgrader works
+ */
+test('local formgrader', async ({
+  page,
+  baseURL,
+  tmpPath
+  }) => {
+
+    test.skip(is_windows, 'This feature is not implemented for Windows');
+
+    const rootDir = await create_env(page, tmpPath, exchange_dir, cache_dir, is_windows);
+
+    await update_config(page, rootDir);
+
+    await open_courses_list(page);
+    await expect(page.locator("#formgrader_list_loading")).not.toBeVisible();
+    await expect(page.locator("#formgrader_list_placeholder")).not.toBeVisible();
+    await expect(page.locator("#formgrader_list_error")).not.toBeVisible();
+    await expect(page.locator("#formgrader_list > .list_item")).toHaveCount(1);
+
+    await expect(page.locator("#formgrader_list > .list_item")).toHaveText("course101local");
+
+    await expect(page.locator("#formgrader_list > .list_item a")).toHaveCount(1);
+
+    await page.locator("#formgrader_list > .list_item a").click();
+    await expect(page.locator("#jp-main-dock-panel .lm-TabBar-tab.p-TabBar-tab")).toHaveCount(3);
+
+    var tabs = page.locator("#jp-main-dock-panel .lm-TabBar-tab.p-TabBar-tab");
+    var newTab_label = tabs.last().locator(".lm-TabBar-tabLabel.p-TabBar-tabLabel");
+    await expect(newTab_label).toHaveText("Formgrader");
+
+  }
+);
+
+
+// /*
+//  * Test using JupyterHub authenticator without jupyterHub, expecting same results as previous tests
+//  */
+// test('No jupyterhub', async ({
+//   page,
+//   baseURL,
+//   tmpPath,
+
+//   }) => {
+
+//     test.skip(is_windows, 'This feature is not implemented for Windows');
+
+//     const rootDir = await create_env(page, tmpPath, exchange_dir, cache_dir);
+
+//     await update_config(page, rootDir);
+
+//     var text_to_append = `
+// from nbgrader.auth import JupyterHubAuthPlugin
+// c.Authenticator.plugin_class = JupyterHubAuthPlugin
+//     `;
+
+//     fs.appendFileSync(path.resolve(rootDir, "nbgrader_config.py"), text_to_append);
+
+//     await open_courses_list(page);
+//     await expect(page.locator("#formgrader_list_loading")).not.toBeVisible();
+//     await expect(page.locator("#formgrader_list_placeholder")).not.toBeVisible();
+//     await expect(page.locator("#formgrader_list_error")).not.toBeVisible();
+//     await expect(page.locator("#formgrader_list > .list_item")).toHaveCount(1);
+
+//     await expect(page.locator("#formgrader_list > .list_item")).toHaveText("course101local");
+
+//     await expect(page.locator("#formgrader_list > .list_item a")).toHaveCount(1);
+
+//     await page.locator("#formgrader_list > .list_item a").click();
+//     await expect(page.locator("#jp-main-dock-panel .lm-TabBar-tab.p-TabBar-tab")).toHaveCount(3);
+
+//     var tabs = page.locator("#jp-main-dock-panel .lm-TabBar-tab.p-TabBar-tab");
+//     var newTab_label = tabs.last().locator(".lm-TabBar-tabLabel.p-TabBar-tabLabel");
+//     await expect(newTab_label).toHaveText("Formgrader");
+
+//   }
+// );
diff --git a/nbgrader/tests/labextension_ui-tests/tests/test_create_assignement.spec.ts b/nbgrader/tests/labextension_ui-tests/tests/test_create_assignement.spec.ts
new file mode 100644
index 000000000..c4ed41658
--- /dev/null
+++ b/nbgrader/tests/labextension_ui-tests/tests/test_create_assignement.spec.ts
@@ -0,0 +1,678 @@
+import { test, galata, IJupyterLabPageFixture } from '@jupyterlab/galata';
+import { expect } from '@playwright/test';
+import * as path from 'path';
+
+import { wait_for_error_modal, close_error_modal } from "./test_utils";
+
+test.use({ tmpPath: 'nbgrader-create-assignments-test' });
+
+const nb_files = ["blank.ipynb", "task.ipynb", "old-schema.ipynb"];
+
+/*
+ * copy notebook files before each test
+ */
+test.beforeEach(async ({ baseURL, tmpPath }) => {
+    const contents = galata.newContentsHelper(baseURL);
+    nb_files.forEach(elem => {
+         contents.uploadFile(
+            path.resolve(
+                __dirname,
+                `../files/${elem}`
+            ),
+            `${tmpPath}/${elem}`
+        );
+    });
+});
+
+/*
+ * delete temp directory at the end of test
+ */
+test.afterAll(async ({ baseURL, tmpPath }) => {
+    const contents = galata.newContentsHelper(baseURL);
+    await contents.deleteDirectory(tmpPath);
+
+    if (contents.fileExists("nbgrader_config.py")) contents.deleteFile("nbgrader_config.py");
+    contents.uploadFile(path.resolve(__dirname, "../files/nbgrader_config.py"), "nbgrader_config.py");
+});
+
+
+/*
+ * Open a notebook file
+ */
+const open_notebook = async (page:IJupyterLabPageFixture, notebook:string) => {
+
+    var filename = notebook + '.ipynb';
+    var tab_count = await page.locator("#jp-main-dock-panel .lm-TabBar-tab.p-TabBar-tab").count();
+    await page.locator(`#filebrowser .jp-DirListing-content .jp-DirListing-itemText span:text-is('${filename}')`).dblclick();
+    await expect(page.locator("#jp-main-dock-panel .lm-TabBar-tab.p-TabBar-tab")).toHaveCount(tab_count + 1);
+    await page.waitForSelector(".jp-Notebook-cell");
+
+}
+
+/*
+ * Save the current notebook file
+ */
+const save_current_notebook = async (page:IJupyterLabPageFixture) => {
+    return await page.evaluate(async () => {
+        var nb = window.jupyterapp.shell.currentWidget;
+        await nb.context.save();
+    });
+
+    // TODO : ensure metadata has been saved
+    // Read local file ?
+}
+
+/*
+ * Activate assignment toolbar in jupyterlab
+ */
+const activate_toolbar = async (page:IJupyterLabPageFixture) => {
+
+    if (await page.locator('.nbgrader-NotebookWidget').count() > 0){
+        if (page.locator('.nbgrader-NotebookWidget').isVisible()) {
+            return;
+        }
+    }
+
+    const widget_button = page.locator(".lm-TabBar-tab.p-TabBar-tab[title='nbgrader Create Assignment']");
+    const button_position = await widget_button.boundingBox();
+    await page.mouse.click(
+        button_position.x + button_position.width/2,
+        button_position.y + button_position.height/2
+    );
+
+    await expect(page.locator('.nbgrader-NotebookWidget')).toBeVisible();
+}
+
+/*
+ * Get the nbgrader's metadata of a cell
+ */
+const get_cell_metadata = async (page:IJupyterLabPageFixture, cell_number:Number=0) => {
+
+    return await page.evaluate((cell_num) => {
+        var nb = window.jupyterapp.shell.currentWidget;
+        return nb.model.cells.get(cell_num).metadata.get("nbgrader");
+    }, cell_number);
+}
+
+/*
+ * Set points to a notebook cell
+ */
+const set_points = async (page:IJupyterLabPageFixture, points:number=0, index:number=0) => {
+    await page.locator(".nbgrader-CellPoints input").nth(index).fill(points.toString());
+    await page.keyboard.press("Enter");
+}
+
+/*
+ * Set id to a notebook cell
+ */
+const set_id = async (page:IJupyterLabPageFixture, id:string="foo", index:number=0) => {
+    await page.locator(".nbgrader-CellId input").nth(index).fill(id);
+    await page.keyboard.press("Enter");
+}
+
+/*
+ * Select type of assignment of a cell in nbgrader toolbar
+ */
+const select_in_toolbar = async(page:IJupyterLabPageFixture, text:string, index:number=0) => {
+    var select = page.locator('.nbgrader-NotebookWidget select').nth(index);
+    await select.selectOption(text);
+}
+
+/*
+ * Get the total points of an assignment
+ */
+const get_total_points = async (page:IJupyterLabPageFixture, index:number=0) => {
+    return parseFloat(await page.locator('.nbgrader-TotalPointsInput').nth(0).inputValue());
+}
+
+/*
+ * Create a new cell in current notebook
+ */
+const create_new_cell = async (page:IJupyterLabPageFixture, after:number=0) => {
+    await page.locator('.jp-Cell .jp-InputArea-prompt').nth(after).click();
+    await page.keyboard.press('b');
+}
+
+/*
+ * Delete a cell in current notebook
+ */
+const delete_cell = async (page:IJupyterLabPageFixture, index:number=0) => {
+    await page.locator('.jp-Cell .jp-InputArea-prompt').nth(index).click();
+    await page.keyboard.press('d');
+    await page.keyboard.press('d');
+}
+
+/*
+ * Test manipulating a manually graded cell
+ */
+test('manual cell', async ({
+    page
+  }) => {
+
+    await open_notebook(page, "blank");
+    await activate_toolbar(page);
+
+    expect(await get_cell_metadata(page)).toBeUndefined();
+
+    await select_in_toolbar(page, 'manual');
+
+    var metadata = await get_cell_metadata(page);
+    expect(metadata).toHaveProperty('solution', true);
+    expect(metadata).toHaveProperty('grade', true);
+    expect(metadata).toHaveProperty('locked', false);
+
+    await expect(page.locator(".nbgrader-CellId >> nth=0")).toBeVisible();
+    await expect(page.locator(".nbgrader-CellPoints >> nth=0")).toBeVisible();
+    await expect(page.locator(".nbgrader-LockButton >> nth=0")).toBeVisible();
+
+    await set_points(page, 2);
+    expect(await get_cell_metadata(page)).toHaveProperty('points', 2);
+
+    expect((await get_cell_metadata(page))['grade_id']).toEqual(expect.stringMatching('^cell\-'));
+    await set_id(page);
+    expect(await get_cell_metadata(page)).toHaveProperty("grade_id", "foo");
+
+    await save_current_notebook(page);
+
+    await select_in_toolbar(page, '');
+    expect(await get_cell_metadata(page)).toBeUndefined();
+
+    await save_current_notebook(page);
+});
+
+/*
+ * Test manipulating a task cell
+ */
+test('task cell', async ({
+    page
+}) => {
+
+    await open_notebook(page, "task");
+    await activate_toolbar(page);
+
+    expect(await get_cell_metadata(page)).toBeUndefined();
+
+    await select_in_toolbar(page, 'task');
+
+    var metadata = await get_cell_metadata(page);
+    expect(metadata).toHaveProperty('solution', false);
+    expect(metadata).toHaveProperty('grade', false);
+    expect(metadata).toHaveProperty('locked', true);
+
+    await expect(page.locator(".nbgrader-CellId >> nth=0")).toBeVisible();
+    await expect(page.locator(".nbgrader-CellPoints >> nth=0")).toBeVisible();
+
+    await set_points(page, 2);
+    expect(await get_cell_metadata(page)).toHaveProperty('points', 2);
+
+    expect((await get_cell_metadata(page))['grade_id']).toEqual(expect.stringMatching('^cell\-'));
+    await set_id(page);
+    expect(await get_cell_metadata(page)).toHaveProperty("grade_id", "foo");
+
+    await save_current_notebook(page);
+
+    await select_in_toolbar(page, '');
+    expect(await get_cell_metadata(page)).toBeUndefined();
+
+    await save_current_notebook(page);
+})
+
+/*
+ * Test manipulating a solution graded cell
+ */
+test('solution cell', async ({
+    page
+}) => {
+
+    await open_notebook(page, "blank");
+    await activate_toolbar(page);
+
+    expect(await get_cell_metadata(page)).toBeUndefined();
+
+    await select_in_toolbar(page, 'solution');
+
+    var metadata = await get_cell_metadata(page);
+    expect(metadata).toHaveProperty('solution', true);
+    expect(metadata).toHaveProperty('grade', false);
+    expect(metadata).toHaveProperty('locked', false);
+
+    await expect(page.locator(".nbgrader-CellId >> nth=0")).toBeVisible();
+
+    expect((await get_cell_metadata(page))['grade_id']).toEqual(expect.stringMatching('^cell\-'));
+    await set_id(page);
+    expect(await get_cell_metadata(page)).toHaveProperty("grade_id", "foo");
+
+    await save_current_notebook(page);
+
+    await select_in_toolbar(page, '');
+    expect(await get_cell_metadata(page)).toBeUndefined();
+
+    await save_current_notebook(page);
+})
+
+/*
+ * Test manipulating a test graded cell
+ */
+test('tests cell', async ({
+    page
+}) => {
+
+    await open_notebook(page, "blank");
+    await activate_toolbar(page);
+
+    expect(await get_cell_metadata(page)).toBeUndefined();
+
+    await select_in_toolbar(page, 'tests');
+
+    var metadata = await get_cell_metadata(page);
+    expect(metadata).toHaveProperty('solution', false);
+    expect(metadata).toHaveProperty('grade', true);
+    expect(metadata).toHaveProperty('locked', true);
+
+    await expect(page.locator(".nbgrader-CellId >> nth=0")).toBeVisible();
+    await expect(page.locator(".nbgrader-CellPoints >> nth=0")).toBeVisible();
+    await expect(page.locator(".nbgrader-LockButton >> nth=0")).toBeVisible();
+
+    await set_points(page, 2);
+    expect(await get_cell_metadata(page)).toHaveProperty('points', 2);
+
+    expect((await get_cell_metadata(page))['grade_id']).toEqual(expect.stringMatching('^cell\-'));
+    await set_id(page);
+    expect(await get_cell_metadata(page)).toHaveProperty("grade_id", "foo");
+
+    await save_current_notebook(page);
+
+    await select_in_toolbar(page, '');
+    expect(await get_cell_metadata(page)).toBeUndefined();
+
+    await save_current_notebook(page);
+})
+
+/*
+ * Test converting cell's type
+ */
+test('tests to solution cell', async ({
+    page
+}) => {
+
+    await open_notebook(page, "blank");
+    await activate_toolbar(page);
+
+    expect(await get_cell_metadata(page)).toBeUndefined();
+
+    await select_in_toolbar(page, 'tests');
+
+    var metadata = await get_cell_metadata(page);
+    expect(metadata).toHaveProperty('solution', false);
+    expect(metadata).toHaveProperty('grade', true);
+    expect(metadata).toHaveProperty('locked', true);
+
+    await expect(page.locator(".nbgrader-CellId >> nth=0")).toBeVisible();
+    await expect(page.locator(".nbgrader-CellPoints >> nth=0")).toBeVisible();
+    await expect(page.locator(".nbgrader-LockButton >> nth=0")).toBeVisible();
+
+    await set_points(page, 2);
+    expect(await get_cell_metadata(page)).toHaveProperty('points', 2);
+
+    expect((await get_cell_metadata(page))['grade_id']).toEqual(expect.stringMatching('^cell\-'));
+    await set_id(page);
+    expect(await get_cell_metadata(page)).toHaveProperty("grade_id", "foo");
+
+    await save_current_notebook(page);
+
+    await select_in_toolbar(page, 'solution');
+    var metadata = await get_cell_metadata(page);
+    expect(metadata).toHaveProperty('solution', true);
+    expect(metadata).toHaveProperty('grade', false);
+    expect(metadata).toHaveProperty('locked', false);
+    expect(metadata['points']).toBeUndefined();
+    await save_current_notebook(page);
+
+    await select_in_toolbar(page, '');
+    expect(await get_cell_metadata(page)).toBeUndefined();
+    await save_current_notebook(page);
+})
+
+/*
+ * Tests on locked cell
+ */
+test('locked cell', async ({
+    page
+}) => {
+
+    await open_notebook(page, "blank");
+    await activate_toolbar(page);
+
+    expect(await get_cell_metadata(page)).toBeUndefined();
+
+    await select_in_toolbar(page, 'readonly');
+
+    var metadata = await get_cell_metadata(page);
+    expect(metadata).toHaveProperty('solution', false);
+    expect(metadata).toHaveProperty('grade', false);
+    expect(metadata).toHaveProperty('locked', true);
+
+    await expect(page.locator(".nbgrader-CellId >> nth=0")).toBeVisible();
+    await expect(page.locator(".nbgrader-LockButton >> nth=0")).toBeVisible();
+
+    expect((await get_cell_metadata(page))['grade_id']).toEqual(expect.stringMatching('^cell\-'));
+    await set_id(page);
+    expect(await get_cell_metadata(page)).toHaveProperty("grade_id", "foo");
+
+    await save_current_notebook(page);
+
+    await select_in_toolbar(page, '');
+    expect(await get_cell_metadata(page)).toBeUndefined();
+    await save_current_notebook(page);
+})
+
+/*
+ * Test focus using TAB key
+ */
+test('tab key', async ({
+    page
+}) => {
+
+    await open_notebook(page, "blank");
+    await activate_toolbar(page);
+
+    // make the cell manually grading
+    await select_in_toolbar(page, 'manual');
+
+    // focus on cell type
+    await page.locator(".nbgrader-CellType select").focus();
+    await expect(page.locator('.nbgrader-CellType select')).toBeFocused();
+
+    // press tab and focus on ID input
+    await page.keyboard.press("Tab");
+    await expect(page.locator('.nbgrader-CellId input')).toBeFocused();
+
+    // press tab again and focus on points input
+    await page.keyboard.press("Tab");
+    await expect(page.locator('.nbgrader-CellPoints input')).toBeFocused();
+
+})
+
+/*
+ * Test the total points of a notebook
+ */
+test('total points', async ({
+    page
+}) => {
+
+    await open_notebook(page, "blank");
+    await activate_toolbar(page);
+
+    // make sure the total points is zero
+    expect(await get_total_points(page)).toBe(0);
+
+    // make it autograder tests and set the points to two
+    await select_in_toolbar(page, 'tests');
+    await set_points(page, 2);
+    await set_id(page);
+    expect(await get_total_points(page)).toBe(2);
+
+    // make it manually graded
+    await select_in_toolbar(page, 'manual');
+    expect(await get_total_points(page)).toBe(2);
+
+    // make it a solution make sure the total points is zero
+    await select_in_toolbar(page, 'solution');
+    expect(await get_total_points(page)).toBe(0);
+
+    // make it task
+    await select_in_toolbar(page, 'task');
+    expect(await get_total_points(page)).toBe(0);
+    await set_points(page, 2);
+    expect(await get_total_points(page)).toBe(2);
+
+    // create a new cell
+    await create_new_cell(page)
+
+    // make it a test cell and check if total points is 3
+    await select_in_toolbar(page, 'tests', 1);
+    await set_points(page, 1, 1)
+    await set_id(page, "bar", 1);
+    expect(await get_total_points(page)).toBe(3);
+
+    // delete the first cell
+    await delete_cell(page);
+    expect(await get_total_points(page)).toBe(1);
+
+    // delete the new cell
+    await delete_cell(page);
+    expect(await get_total_points(page)).toBe(0);
+
+})
+
+/*
+ * Test the total points of a notebook using task cell
+ */
+test('task total points', async ({
+    page
+}) => {
+
+    await open_notebook(page, "task");
+    await activate_toolbar(page);
+
+    // make sure the total points is zero
+    expect(await get_total_points(page)).toBe(0);
+
+    // make cell autograded task and set the points to two
+    await select_in_toolbar(page, 'task');
+    await set_points(page, 2);
+    await set_id(page);
+    expect(await get_total_points(page)).toBe(2);
+
+    // make cell manually graded
+    await select_in_toolbar(page, 'manual');
+    expect(await get_total_points(page)).toBe(2);
+
+    // make cell a none graded and make sure the total points is zero
+    await select_in_toolbar(page, '');
+    expect(await get_total_points(page)).toBe(0);
+
+    // make cell a task again
+    await select_in_toolbar(page, 'task');
+    expect(await get_total_points(page)).toBe(0);
+    await set_points(page, 2);
+    expect(await get_total_points(page)).toBe(2);
+
+    // create a new cell
+    await create_new_cell(page)
+
+    // make it a test cell and check if total points is 3
+    await select_in_toolbar(page, 'tests', 1);
+    await set_points(page, 1, 1)
+    await set_id(page, "bar", 1);
+    expect(await get_total_points(page)).toBe(3);
+
+    // delete the first cell
+    await delete_cell(page);
+    expect(await get_total_points(page)).toBe(1);
+
+    // delete the new cell
+    await delete_cell(page);
+    expect(await get_total_points(page)).toBe(0);
+
+})
+
+/*
+ * Tests on cell ids
+ */
+test('cell ids', async ({
+    page
+}) => {
+
+    await open_notebook(page, "blank");
+    await activate_toolbar(page);
+
+    // turn it into a solution cell with an id
+    await select_in_toolbar(page, 'solution');
+    await set_id(page, "");
+
+    // wait for error on saving with empty id
+    await save_current_notebook(page);
+    await wait_for_error_modal(page);
+    await close_error_modal(page);
+
+    // set correct id
+    await set_id(page);
+
+    // create a new cell
+    await create_new_cell(page);
+
+    // make it a test cell and set the label
+    await select_in_toolbar(page, 'tests', 1);
+    await set_id(page, "foo", 1);
+
+    // wait for error on saving with empty id
+    await save_current_notebook(page);
+    await wait_for_error_modal(page);
+    await close_error_modal(page);
+
+})
+
+/*
+ * Tests on task's cell ids
+ */
+test('task cell ids', async ({
+    page
+}) => {
+
+    await open_notebook(page, "task");
+    await activate_toolbar(page);
+
+    // turn it into a task cell with an id
+    await select_in_toolbar(page, 'task');
+    await set_id(page, "");
+
+    // wait for error on saving with empty id
+    await save_current_notebook(page);
+    await wait_for_error_modal(page);
+    await close_error_modal(page);
+
+    // set correct id
+    await set_id(page);
+
+    // create a new cell
+    await create_new_cell(page);
+
+    // make it a test cell and set the label
+    await select_in_toolbar(page, 'task', 1);
+    await set_id(page, "foo", 1);
+
+    // wait for error on saving with empty id
+    await save_current_notebook(page);
+    await wait_for_error_modal(page);
+    await close_error_modal(page);
+
+})
+
+/*
+ * Test attributing negative points
+ */
+test('negative points', async ({
+    page
+}) => {
+
+    await open_notebook(page, "blank");
+    await activate_toolbar(page);
+
+    // make sure the total points is zero
+    expect(await get_total_points(page)).toBe(0);
+
+     // make it autograder tests and set the points to two
+    await select_in_toolbar(page, 'tests');
+    await set_points(page, 2);
+    await set_id(page);
+    expect(await get_total_points(page)).toBe(2);
+    expect(await get_cell_metadata(page)).toHaveProperty("points", 2);
+
+    // set the points to negative one
+    await set_points(page, -1);
+    expect(await get_total_points(page)).toBe(0);
+    expect(await get_cell_metadata(page)).toHaveProperty("points", 0);
+
+})
+
+/*
+ * Test attributing negative points on task's cell
+ */
+test('task negative points', async ({
+    page
+}) => {
+
+    await open_notebook(page, "task");
+    await activate_toolbar(page);
+
+    // make sure the total points is zero
+    expect(await get_total_points(page)).toBe(0);
+
+     // make it autograder tests and set the points to two
+    await select_in_toolbar(page, 'task');
+    await set_points(page, 2);
+    await set_id(page);
+    expect(await get_total_points(page)).toBe(2);
+    expect(await get_cell_metadata(page)).toHaveProperty("points", 2);
+
+    // set the points to negative one
+    await set_points(page, -1);
+    expect(await get_total_points(page)).toBe(0);
+    expect(await get_cell_metadata(page)).toHaveProperty("points", 0);
+
+})
+
+/*
+ * Test nbgrader schema version
+ */
+test('schema version', async ({
+    page
+}) => {
+
+    await open_notebook(page, "old-schema");
+
+    // activate toolbar should show an error modal
+    await activate_toolbar(page);
+    await wait_for_error_modal(page);
+    await close_error_modal(page);
+
+})
+
+/*
+ * Test an invalid cell type
+ */
+test('invalid nbgrader cell type', async ({
+    page
+    }) => {
+
+    await open_notebook(page, "blank");
+    await activate_toolbar(page);
+
+    await select_in_toolbar(page, 'solution');
+
+    // make the cell a solution cell
+    var metadata = await get_cell_metadata(page);
+    expect(metadata).toHaveProperty('solution', true);
+    expect(metadata).toHaveProperty('grade', false);
+    expect(metadata).toHaveProperty('locked', false);
+
+    await expect(page.locator(".nbgrader-CellId")).toBeVisible();
+
+    expect((await get_cell_metadata(page))['grade_id']).toEqual(expect.stringMatching('^cell\-'));
+    await set_id(page);
+    expect(await get_cell_metadata(page)).toHaveProperty('grade_id', 'foo');
+
+    await save_current_notebook(page);
+
+    // change the cell to markdown
+    await page.locator('.jp-Cell .jp-InputArea-prompt').first().click();
+    await page.keyboard.press('m');
+
+    var metadata = await get_cell_metadata(page);
+    expect(metadata).toHaveProperty('solution', false);
+    expect(metadata).toHaveProperty('grade', false);
+    expect(metadata).toHaveProperty('locked', false);
+    expect(metadata).toHaveProperty('grade_id', "foo");
+
+});
diff --git a/nbgrader/tests/labextension_ui-tests/tests/test_formgrader.spec.ts b/nbgrader/tests/labextension_ui-tests/tests/test_formgrader.spec.ts
new file mode 100644
index 000000000..4a8a326e5
--- /dev/null
+++ b/nbgrader/tests/labextension_ui-tests/tests/test_formgrader.spec.ts
@@ -0,0 +1,581 @@
+import { test, galata, IJupyterLabPageFixture } from '@jupyterlab/galata';
+import { expect, Frame } from '@playwright/test';
+
+import {
+  execute_command,
+  create_env
+} from './test_utils';
+
+// import * as sqlite3 from 'sqlite3';
+import * as path from 'path';
+import * as os from 'os';
+import * as fs from 'fs';
+
+const is_windows = os.platform().startsWith('win')
+
+test.use({ tmpPath: 'nbgrader-formgrader-test' });
+
+// const db = new sqlite3.Database("gradebook.db");
+
+var exchange_dir:string;
+var cache_dir: string;
+
+/*
+ * Create environment
+ */
+test.beforeEach(async ({ baseURL, tmpPath }) => {
+
+  const contents = galata.newContentsHelper(baseURL);
+
+  await contents.createDirectory(tmpPath);
+
+  if (!is_windows){
+    exchange_dir = fs.mkdtempSync(path.join(os.tmpdir(), 'nbgrader_exchange_test_'));
+    cache_dir = fs.mkdtempSync(path.join(os.tmpdir(), 'nbgrader_cache_test_'));
+  }
+});
+
+/*
+ * delete temp directories at the end of test
+ */
+test.afterEach(async ({ baseURL, tmpPath }) => {
+  if (!is_windows){
+    fs.rmSync(exchange_dir, { recursive: true, force: true });
+    fs.rmSync(cache_dir, { recursive: true, force: true });
+  }
+
+  const contents = galata.newContentsHelper(baseURL);
+  await contents.deleteDirectory(tmpPath);
+
+  if (contents.fileExists("nbgrader_config.py")) contents.deleteFile("nbgrader_config.py");
+  contents.uploadFile(path.resolve(__dirname, "../files/nbgrader_config.py"), "nbgrader_config.py");
+});
+
+/*
+ * Create a nbgrader file system
+ */
+const add_courses = async (page:IJupyterLabPageFixture, baseURL:string, tmpPath:string) => {
+
+  const contents = galata.newContentsHelper(baseURL);
+
+  // copy files from the user guide
+  const source_path = path.resolve(__dirname, "..", "..", "..", "docs", "source", "user_guide", "source");
+  const submitted_path = path.resolve(__dirname, "..", "..", "..", "docs", "source", "user_guide", "submitted");
+
+  await contents.uploadDirectory(source_path, `${tmpPath}/source`);
+
+  const students = ["bitdiddle", "hacker"];
+  for (var i=0; i<2; i++){
+    await contents.uploadDirectory(
+      path.resolve(submitted_path, students[i]),
+      `${tmpPath}/submitted/${students[i]}`
+    )
+  }
+
+  // Rename the files and directory to have spaces in names
+  await contents.renameDirectory(`${tmpPath}/source/ps1`, `${tmpPath}/source/Problem Set 1`);
+  await contents.renameFile(`${tmpPath}/source/Problem Set 1/problem1.ipynb`, `${tmpPath}/source/Problem Set 1/Problem 1.ipynb`);
+  await contents.renameFile(`${tmpPath}/source/Problem Set 1/problem2.ipynb`, `${tmpPath}/source/Problem Set 1/Problem 2.ipynb`);
+  await contents.renameDirectory(`${tmpPath}/submitted/bitdiddle`, `${tmpPath}/submitted/Bitdiddle`);
+  await contents.renameDirectory(`${tmpPath}/submitted/Bitdiddle/ps1`, `${tmpPath}/submitted/Bitdiddle/Problem Set 1`);
+  await contents.renameFile(`${tmpPath}/submitted/Bitdiddle/Problem Set 1/problem1.ipynb`, `${tmpPath}/submitted/Bitdiddle/Problem Set 1/Problem 1.ipynb`);
+  await contents.renameFile(`${tmpPath}/submitted/Bitdiddle/Problem Set 1/problem2.ipynb`, `${tmpPath}/submitted/Bitdiddle/Problem Set 1/Problem 2.ipynb`);
+  await contents.renameDirectory(`${tmpPath}/submitted/hacker`, `${tmpPath}/submitted/Hacker`);
+  await contents.renameDirectory(`${tmpPath}/submitted/Hacker/ps1`, `${tmpPath}/submitted/Hacker/Problem Set 1`);
+  await contents.renameFile(`${tmpPath}/submitted/Hacker/Problem Set 1/problem1.ipynb`, `${tmpPath}/submitted/Hacker/Problem Set 1/Problem 1.ipynb`);
+  await contents.renameFile(`${tmpPath}/submitted/Hacker/Problem Set 1/problem2.ipynb`, `${tmpPath}/submitted/Hacker/Problem Set 1/Problem 2.ipynb`);
+
+  const jupyter_config_content = await page.locator('#jupyter-config-data').textContent();
+  const rootDir = JSON.parse(jupyter_config_content)['serverRoot'];
+
+  fs.copyFileSync(path.resolve(rootDir, "nbgrader_config.py"), path.resolve(rootDir, tmpPath, "nbgrader_config.py"));
+
+  // generate some assignments
+  await execute_command(`nbgrader generate_assignment 'Problem Set 1' --IncludeHeaderFooter.header=${path.resolve(rootDir, tmpPath, "source", "header.ipynb")}`);
+
+  // autograde assignment
+  await execute_command("nbgrader autograde 'Problem Set 1'");
+
+}
+
+/*
+ * Open the formgrader tab
+ */
+const open_formgrader = async (page:IJupyterLabPageFixture) => {
+
+  await expect(page.locator("#jp-main-dock-panel .lm-TabBar-tab.p-TabBar-tab")).toHaveCount(1);
+
+  await page.keyboard.press('Control+Shift+c');
+  await page.locator('#modal-command-palette li[data-command="nbgrader:open-formgrader"]').click();
+
+  await expect(page.locator("#jp-main-dock-panel .lm-TabBar-tab.p-TabBar-tab")).toHaveCount(2);
+
+  var tabs = page.locator("#jp-main-dock-panel .lm-TabBar-tab.p-TabBar-tab");
+  var newTab_label = tabs.last().locator(".lm-TabBar-tabLabel.p-TabBar-tabLabel");
+  await expect(newTab_label).toHaveText("Formgrader");
+
+}
+
+/*
+ * Check jupyter lab file browser breadcrumbs
+ */
+const check_jl_breadcrumbs = async (page:IJupyterLabPageFixture, breadcrumbs:string) => {
+  await page.waitForSelector(`.jp-FileBrowser-crumbs > span.jp-BreadCrumbs-item[title="${breadcrumbs}"]`);
+}
+
+/*
+ * Check formgrader breadcrumbs
+ */
+const check_formgrader_breadcrumbs = async (iframe:Frame, breadcrumbs:string[]) => {
+
+  await expect(iframe.locator(".breadcrumb li")).toHaveCount(breadcrumbs.length);
+
+  const elements = iframe.locator(".breadcrumb li");
+  const array = [];
+  for (var i=0; i<await elements.count(); i++){
+    array.push(await elements.nth(i).textContent());
+  }
+  expect(array.sort()).toEqual(breadcrumbs.sort());
+}
+
+/*
+ * Check formgrader breadcrumbs
+ */
+const check_formgrade_view_breadcrumbs = async (
+  iframe:Frame,
+  breadcrumbs:string[],
+  no_submission_count?: boolean
+  ) => {
+
+  await expect(iframe.locator(".breadcrumb li a:visible")).toHaveCount(breadcrumbs.length);
+
+  const elements = iframe.locator(".breadcrumb li a:visible");
+  const in_page_breadcrumbs = [];
+  for (var i=0; i<await elements.count(); i++){
+    in_page_breadcrumbs.push((await elements.nth(i).textContent()).trim());
+  }
+
+  if (no_submission_count) {
+    expect(in_page_breadcrumbs.slice(0, -1).sort()).toEqual(breadcrumbs.slice(0, -1).sort());
+    expect(in_page_breadcrumbs[in_page_breadcrumbs.length - 1].includes(breadcrumbs[-1]));
+  }
+  else expect(in_page_breadcrumbs.sort()).toEqual(breadcrumbs.sort());
+}
+
+/*
+ * Click on link by text
+ */
+const click_link = async (iframe:Frame, text:string) => {
+  await iframe.click(`a:text-is('${text}')`);
+}
+
+// /*
+//  * Get comment box
+//  */
+// const get_comment_box = async (iframe:Frame, index:number) => {
+//   await expect(await iframe.locator('.comment').count()).toBeGreaterThanOrEqual(index);
+//   await expect poll(() => iframe.locator('.comment').count()).toBeGreaterThan(3);
+// }
+
+
+/*
+ * Test opening formgrader tab from menu
+ */
+test('Open assignment list tab from menu', async({
+  page
+  }) => {
+
+    await expect(page.locator("#jp-main-dock-panel .lm-TabBar-tab.p-TabBar-tab")).toHaveCount(1);
+    // Check main menu exists
+    await expect(page.locator('#jp-menu-panel div.lm-MenuBar-itemLabel.p-MenuBar-itemLabel:text("Nbgrader")')).toHaveCount(1);
+    await page.locator('#jp-menu-panel div.lm-MenuBar-itemLabel.p-MenuBar-itemLabel:text("Nbgrader")').click();
+
+    await page.locator('#jp-mainmenu-nbgrader-menu li[data-command="nbgrader:open-formgrader"]').click();
+
+    await expect(page.locator("#jp-main-dock-panel .lm-TabBar-tab.p-TabBar-tab")).toHaveCount(2);
+
+    var tabs = page.locator("#jp-main-dock-panel .lm-TabBar-tab.p-TabBar-tab");
+    var newTab_label = tabs.last().locator(".lm-TabBar-tabLabel.p-TabBar-tabLabel");
+    await expect(newTab_label).toHaveText("Formgrader");
+
+});
+
+/*
+ * Load manage assignments
+ */
+test('Load manage assignments', async ({
+    page,
+    baseURL,
+    tmpPath
+  }) => {
+
+    test.skip(is_windows, 'This test does not work on Windows');
+
+    // create environment
+    await create_env(page, tmpPath, exchange_dir, cache_dir, is_windows);
+    await add_courses(page, baseURL, tmpPath);
+    await open_formgrader(page);
+
+    // get formgrader iframe and check for breadcrumbs
+    const iframe = page.mainFrame().childFrames()[0];
+
+    await check_formgrader_breadcrumbs(iframe, ["Assignments"]);
+    expect(iframe.url()).toBe(encodeURI(`${baseURL}/formgrader`));
+
+    await page.waitForSelector(`.jp-FileBrowser-crumbs > span.jp-BreadCrumbs-item[title="${tmpPath}"]`);
+
+    // click on the "Problem Set 1" link and check if file browser has changed of directory
+    click_link(iframe, "Problem Set 1");
+    await page.waitForSelector(`.jp-FileBrowser-crumbs > span.jp-BreadCrumbs-item[title="${tmpPath.concat("/source/Problem Set 1")}"]`);
+
+    // click on preview link and check if file browser has changed of directory
+    iframe.locator("td.preview .glyphicon").click();
+    await page.waitForSelector(`.jp-FileBrowser-crumbs > span.jp-BreadCrumbs-item[title="${tmpPath.concat("/release/Problem Set 1")}"]`);
+
+    // click on the first number of submissions and check that iframe has change URL
+    await iframe.click("td.num-submissions a");
+    await check_formgrader_breadcrumbs(iframe, ["Assignments", "Problem Set 1"]);
+    expect(iframe.url()).toBe(encodeURI(`${baseURL}/formgrader/manage_submissions/Problem Set 1`));
+  }
+);
+
+/*
+ * Load manage submissions
+ */
+test('Load manage submissions', async ({
+  page,
+  baseURL,
+  tmpPath
+}) => {
+
+  test.skip(is_windows, 'This test does not work on Windows');
+
+  // create environment
+  await create_env(page, tmpPath, exchange_dir, cache_dir, is_windows);
+  await add_courses(page, baseURL, tmpPath);
+  await open_formgrader(page);
+
+  // get formgrader iframe
+  const iframe = page.mainFrame().childFrames()[0];
+
+  // Change iframe URL to manage_submissions
+  await iframe.goto(`${baseURL}/formgrader/manage_submissions/Problem Set 1`);
+
+  // await iframe.click("td.num-submissions a");
+  await check_formgrader_breadcrumbs(iframe, ["Assignments", "Problem Set 1"]);
+
+  // clicking on breadcrumbs should go back to manage_assignments
+  await click_link(iframe, "Assignments");
+  await check_formgrader_breadcrumbs(iframe, ["Assignments"]);
+  expect(iframe.url()).toBe(encodeURI(`${baseURL}/formgrader/manage_assignments`));
+
+  // page.goBack(); // seems endless
+  await iframe.goto(`${baseURL}/formgrader/manage_submissions/Problem Set 1`);
+  await check_formgrader_breadcrumbs(iframe, ["Assignments", "Problem Set 1"]);
+
+  // Check students links
+  await expect(iframe.locator("td.student-name")).toHaveCount(2);
+  for (var i=0; i<await iframe.locator("td.student-name").count(); i++){
+    var student_name = await iframe.locator("td.student-name").nth(i).getAttribute("data-order");
+    var student_id = await iframe.locator("td.student-id").nth(i).getAttribute("data-order");
+    await click_link(iframe, student_name);
+    await check_formgrader_breadcrumbs(iframe, ["Students", student_id, "Problem Set 1"]);
+    expect(iframe.url()).toBe(encodeURI(`${baseURL}/formgrader/manage_students/${student_id}/Problem Set 1`));
+    await iframe.goto(`${baseURL}/formgrader/manage_submissions/Problem Set 1`);
+  }
+});
+
+/*
+ * Load gradebook1
+ */
+test('Load gradebook1', async ({
+  page,
+  baseURL,
+  tmpPath
+}) => {
+
+  test.skip(is_windows, 'This test does not work on Windows');
+
+  // create environment
+  await create_env(page, tmpPath, exchange_dir, cache_dir, is_windows);
+  await add_courses(page, baseURL, tmpPath);
+  await open_formgrader(page);
+
+  // get formgrader iframe
+  const iframe = page.mainFrame().childFrames()[0];
+
+  // Change iframe URL to gradebook
+  await iframe.goto(`${baseURL}/formgrader/gradebook`);
+
+  // await iframe.click("td.num-submissions a");
+  await check_formgrader_breadcrumbs(iframe, ["Manual Grading"]);
+
+  // click on assignment
+  await click_link(iframe, "Problem Set 1");
+  await check_formgrader_breadcrumbs(iframe, ["Manual Grading", "Problem Set 1"]);
+  expect(iframe.url()).toBe(encodeURI(`${baseURL}/formgrader/gradebook/Problem Set 1`));
+
+  // test that the task column is present
+  await expect(iframe.locator('th:text-is("Avg. Task Score")')).toHaveCount(1);
+
+});
+
+/*
+ * Load gradebook2
+ */
+test('Load gradebook2', async ({
+  page,
+  baseURL,
+  tmpPath
+}) => {
+
+  test.skip(is_windows, 'This test does not work on Windows');
+
+  // create environment
+  await create_env(page, tmpPath, exchange_dir, cache_dir, is_windows);
+  await add_courses(page, baseURL, tmpPath);
+  await open_formgrader(page);
+
+  // get formgrader iframe
+  const iframe = page.mainFrame().childFrames()[0];
+
+  // Change iframe URL to gradebook Problem Set 1
+  await iframe.goto(`${baseURL}/formgrader/gradebook/Problem Set 1`);
+  await check_formgrader_breadcrumbs(iframe, ["Manual Grading", "Problem Set 1"]);
+
+  // clicking on breadcrumbs should go back to manual grading
+  await iframe.click('ol.breadcrumb a:text-is("Manual Grading")');
+  await check_formgrader_breadcrumbs(iframe, ["Manual Grading"]);
+  expect(iframe.url()).toBe(encodeURI(`${baseURL}/formgrader/gradebook`));
+
+  // Send back iframe URL to gradebook Problem Set 1
+  await iframe.goto(`${baseURL}/formgrader/gradebook/Problem Set 1`);
+
+  // test problems links
+  await expect(iframe.locator("td.name")).toHaveCount(2);
+  for (var i=0; i<await iframe.locator("td.name").count(); i++){
+    var problem_name = await iframe.locator("td.name").nth(i).getAttribute("data-order");
+    await click_link(iframe, problem_name);
+    await check_formgrader_breadcrumbs(iframe, ["Manual Grading", "Problem Set 1", problem_name]);
+    expect(iframe.url()).toBe(encodeURI(`${baseURL}/formgrader/gradebook/Problem Set 1/${problem_name}`));
+    await expect(iframe.locator('th:text-is("Task Score")')).toHaveCount(1);
+    await iframe.goto(`${baseURL}/formgrader/gradebook/Problem Set 1`);
+  }
+});
+
+/*
+ * Load gradebook3
+ */
+test('Load gradebook3', async ({
+  page,
+  baseURL,
+  tmpPath
+}) => {
+
+  test.skip(is_windows, 'This test does not work on Windows');
+
+  // create environment
+  await create_env(page, tmpPath, exchange_dir, cache_dir, is_windows);
+  await add_courses(page, baseURL, tmpPath);
+  await open_formgrader(page);
+
+  // get formgrader iframe
+  const iframe = page.mainFrame().childFrames()[0];
+
+  // Change iframe URL to gradebook Problem Set 1
+  await iframe.goto(`${baseURL}/formgrader/gradebook/Problem Set 1`);
+
+  // for each problem
+  await expect(iframe.locator("td.name")).toHaveCount(2);
+  for (var i=0; i<await iframe.locator("td.name").count(); i++){
+    var problem_name = await iframe.locator("td.name").nth(i).getAttribute("data-order");
+    await iframe.goto(`${baseURL}/formgrader/gradebook/Problem Set 1/${problem_name}`);
+    await check_formgrader_breadcrumbs(iframe, ["Manual Grading", "Problem Set 1", problem_name]);
+
+    // test click on breadcrumb 'Manual Grading' to change iframe URL, then go back
+    await iframe.click('ol.breadcrumb a:text-is("Manual Grading")');
+    await check_formgrader_breadcrumbs(iframe, ["Manual Grading"]);
+    expect(iframe.url()).toBe(encodeURI(`${baseURL}/formgrader/gradebook`));
+    await iframe.goto(`${baseURL}/formgrader/gradebook/Problem Set 1/${problem_name}`);
+
+    // test click on breadcrumb 'Problem Set 1' to change iframe URL, then go back
+    await iframe.click('ol.breadcrumb a:text-is("Problem Set 1")');
+    await check_formgrader_breadcrumbs(iframe, ["Manual Grading", "Problem Set 1"]);
+    await iframe.goto(`${baseURL}/formgrader/gradebook/Problem Set 1/${problem_name}`);
+
+    // test submissions links
+    await expect(iframe.locator("td.name")).toHaveCount(2);
+    for (var j=0; j<await iframe.locator("td.name").count(); j++){
+      var submission_id = parseInt(await iframe.locator("td.name").nth(j).getAttribute("data-order")) + 1;
+      await click_link(iframe, `Submission #${submission_id.toString()}`);
+      await check_formgrade_view_breadcrumbs(
+        iframe,
+        ["Manual Grading",
+         "Problem Set 1",
+         problem_name,
+         `Submission #${submission_id.toString()}`]
+      );
+      // TODO: find the submission ID to check URL ?
+
+      if (problem_name == "Problem 1"){
+        await expect(iframe.locator('span:text-is("Student\'s task")')).toHaveCount(1);
+      }
+      await iframe.goto(`${baseURL}/formgrader/gradebook/Problem Set 1/${problem_name}`);
+    }
+  }
+});
+
+/*
+ * Gradebook3 show/hide students names
+ */
+test('Gradebook3 show hide names', async ({
+  page,
+  baseURL,
+  tmpPath
+}) => {
+
+  test.skip(is_windows, 'This test does not work on Windows');
+
+  // create environment
+  await create_env(page, tmpPath, exchange_dir, cache_dir, is_windows);
+  await add_courses(page, baseURL, tmpPath);
+  await open_formgrader(page);
+
+  // get formgrader iframe
+  const iframe = page.mainFrame().childFrames()[0];
+
+  // Change iframe URL to gradebook Problem Set 1
+  await iframe.goto(`${baseURL}/formgrader/gradebook/Problem Set 1/Problem 1`);
+  await check_formgrader_breadcrumbs(iframe, ["Manual Grading", "Problem Set 1", "Problem 1"]);
+
+  const col2 = iframe.locator("td.name").first();
+  const hidden = iframe.locator("td .glyphicon.name-hidden").first();
+  const shown = iframe.locator("td .glyphicon.name-shown").first();
+
+  // check shown and hidden elements
+  await expect(col2).toHaveText(/Submission #[1-2]/, {useInnerText: true});
+  await expect(hidden).toBeVisible();
+  await expect(shown).toBeHidden();
+
+  // show name
+  await hidden.click();
+  await expect(col2).toHaveText(/(H, Alyssa|B, Ben)/, {useInnerText: true});
+  await expect(hidden).toBeHidden();
+  await expect(shown).toBeVisible();
+
+  // hide name again
+  await shown.click();
+  await expect(col2).toHaveText(/Submission #[1-2]/, {useInnerText: true});
+  await expect(hidden).toBeVisible();
+  await expect(shown).toBeHidden();
+
+});
+
+/*
+ * Load students and test students links
+ */
+test('Load students', async ({
+  page,
+  baseURL,
+  tmpPath
+}) => {
+
+  test.skip(is_windows, 'This test does not work on Windows');
+
+  // create environment
+  await create_env(page, tmpPath, exchange_dir, cache_dir, is_windows);
+  await add_courses(page, baseURL, tmpPath);
+  await open_formgrader(page);
+
+  // get formgrader iframe
+  const iframe = page.mainFrame().childFrames()[0];
+
+  // Change iframe URL to students
+  await iframe.goto(`${baseURL}/formgrader/manage_students`);
+  await check_formgrader_breadcrumbs(iframe, ["Students"]);
+
+  // Check students links
+  await expect(iframe.locator("td.name")).toHaveCount(3);
+  for (var i=0; i < await iframe.locator("td.name").count(); i++){
+    var student_name = await iframe.locator("td.name").nth(i).getAttribute("data-order");
+    var student_id = await iframe.locator("td.id").nth(i).getAttribute("data-order");
+    await click_link(iframe, student_name);
+    await check_formgrader_breadcrumbs(iframe, ["Students", student_id]);
+    expect(iframe.url()).toBe(encodeURI(`${baseURL}/formgrader/manage_students/${student_id}`));
+    await expect(iframe.locator('th:text("Task Score")')).toHaveCount(1);
+    await iframe.goto(`${baseURL}/formgrader/manage_students`);
+  }
+
+});
+
+/*
+ * Test students submissions
+ */
+test('Load students submissions', async ({
+  page,
+  baseURL,
+  tmpPath
+}) => {
+
+  test.skip(is_windows, 'This test does not work on Windows');
+
+  // create environment
+  await create_env(page, tmpPath, exchange_dir, cache_dir, is_windows);
+  await add_courses(page, baseURL, tmpPath);
+  await open_formgrader(page);
+
+  // get formgrader iframe
+  const iframe = page.mainFrame().childFrames()[0];
+
+  const student_ids = ["Bitdiddle", "Hacker"];
+
+  for(var i=0; i<2; i++){ // foreach loop does not work (raise at goto statement)
+    // Change iframe URL to student
+    await iframe.goto(`${baseURL}/formgrader/manage_students/${student_ids[i]}`);
+    await check_formgrader_breadcrumbs(iframe, ["Students", student_ids[i]]);
+
+    // Click on an assignment
+    await click_link(iframe, "Problem Set 1");
+    // await iframe.waitForNavigation({'url': encodeURI(`${baseURL}/formgrader/manage_students/${student_ids[i]}/Problem Set 1`)});
+    await check_formgrader_breadcrumbs(iframe, ["Students", student_ids[i], "Problem Set 1"]);
+    expect(iframe.url()).toBe(encodeURI(`${baseURL}/formgrader/manage_students/${student_ids[i]}/Problem Set 1`));
+    await expect(iframe.locator('th:text("Task Score")')).toHaveCount(1);
+  }
+});
+
+/*
+ * Switch views
+ */
+test('Switch views', async ({
+  page,
+  baseURL,
+  tmpPath
+}) => {
+
+  test.skip(is_windows, 'This test does not work on Windows');
+
+  // create environment
+  await create_env(page, tmpPath, exchange_dir, cache_dir, is_windows);
+  await add_courses(page, baseURL, tmpPath);
+  await open_formgrader(page);
+
+  // get formgrader iframe
+  const iframe = page.mainFrame().childFrames()[0];
+
+  const pages = ["", "manage_assignments", "gradebook", "manage_students"];
+  const links = [
+    ["Manage Assignments", "Assignments", "manage_assignments"],
+    ["Manual Grading", "Manual Grading", "gradebook"],
+    ["Manage Students", "Students", "manage_students"]
+  ];
+
+  for (var i=0; i<pages.length; i++) {
+    await iframe.goto(`${baseURL}/formgrader/${pages[i]}`);
+    for (var j=0; j<links.length; j++) {
+      click_link(iframe, links[j][0]);
+      await iframe.waitForNavigation({'url': encodeURI(`${baseURL}/formgrader/${links[j][2]}`)});
+      await check_formgrader_breadcrumbs(iframe, [links[j][1]]);
+      expect(iframe.url()).toBe(encodeURI(`${baseURL}/formgrader/${links[j][2]}`));
+    }
+  }
+});
diff --git a/nbgrader/tests/labextension_ui-tests/tests/test_utils.ts b/nbgrader/tests/labextension_ui-tests/tests/test_utils.ts
new file mode 100644
index 000000000..b9313207e
--- /dev/null
+++ b/nbgrader/tests/labextension_ui-tests/tests/test_utils.ts
@@ -0,0 +1,100 @@
+import { IJupyterLabPageFixture } from "@jupyterlab/galata"
+import { expect } from '@playwright/test';
+import { exec } from "child_process";
+import { promisify } from 'util';
+
+import * as path from 'path';
+import * as fs from 'fs';
+
+const async_exec = promisify(exec)
+
+
+export const execute_command = async (command: string) => {
+  const { stdout, stderr } = await async_exec(command);
+  if (stderr) {
+    if (stderr.includes("ERROR")){
+      console.log(`stderr: ${stderr}`);
+      throw new Error(`ERROR in command : ${command}\n${stderr}`);
+    }
+  }
+  return stdout;
+}
+
+/*
+ * Create a copy of default config file, append exchange directories to config file, and populate database
+ */
+export const create_env = async (
+  page:IJupyterLabPageFixture,
+  tmpPath:string,
+  exchange_dir:string,
+  cache_dir:string,
+  is_windows:boolean
+  ): Promise<string> => {
+
+  var content = await page.locator('#jupyter-config-data').textContent();
+  const rootDir = JSON.parse(content)['serverRoot'];
+
+  /* Add config_file to jupyter root directory, and change to that directory.
+  TODO : test on windows, the config file may change (see nbextension test)
+  */
+  try {
+
+    var text_to_append = `
+c.CourseDirectory.root = r"${path.resolve(rootDir, tmpPath)}"
+c.CourseDirectory.db_url = r"sqlite:///${path.resolve(rootDir, tmpPath, 'gradebook.db')}"
+
+`;
+
+    if (!is_windows){
+      text_to_append = text_to_append.concat(`
+c.Exchange.root = r"${exchange_dir}"
+c.Exchange.cache = r"${cache_dir}"
+c.Exchange.assignment_dir = r"${path.resolve(rootDir, tmpPath)}"
+
+`);
+    }
+
+    fs.appendFileSync(path.resolve(rootDir, "nbgrader_config.py"), text_to_append);
+    process.chdir(rootDir);
+  }
+  catch (e){
+    throw new Error(`ERROR : ${e}`);
+  }
+
+  /* Fill database */
+  await execute_command("nbgrader db assignment add 'Problem Set 1'");
+  await execute_command("nbgrader db assignment add ps.01");
+  await execute_command("nbgrader db student add Bitdiddle --first-name Ben --last-name B");
+  await execute_command("nbgrader db student add Hacker --first-name Alyssa --last-name H");
+  await execute_command("nbgrader db student add Reasoner --first-name Louis --last-name R");
+
+  return rootDir;
+}
+
+/*
+ * Wait for error modal
+ */
+export const wait_for_error_modal = async (page:IJupyterLabPageFixture) => {
+  await expect(page.locator(".nbgrader-ErrorDialog")).toHaveCount(1);
+}
+
+/*
+* Close error modal
+*/
+export const close_error_modal = async (page:IJupyterLabPageFixture) => {
+  await page.locator(".nbgrader-ErrorDialog button.jp-Dialog-button").click();
+}
+
+/*
+ * Wait for success modal
+ */
+export const wait_for_success_modal = async (page:IJupyterLabPageFixture) => {
+  await expect(page.locator(".nbgrader-SuccessDialog")).toHaveCount(1);
+}
+
+/*
+* Close success modal
+*/
+export const close_success_modal = async (page:IJupyterLabPageFixture) => {
+  await page.locator(".nbgrader-SuccessDialog button.jp-Dialog-button").click();
+}
diff --git a/nbgrader/tests/labextension_ui-tests/tests/test_validate_assignment.spec.ts b/nbgrader/tests/labextension_ui-tests/tests/test_validate_assignment.spec.ts
new file mode 100644
index 000000000..eb026c110
--- /dev/null
+++ b/nbgrader/tests/labextension_ui-tests/tests/test_validate_assignment.spec.ts
@@ -0,0 +1,211 @@
+import { test, galata, IJupyterLabPageFixture } from '@jupyterlab/galata';
+import { expect } from '@playwright/test';
+import * as path from 'path';
+
+import {
+    wait_for_success_modal,
+    close_success_modal,
+    wait_for_error_modal,
+    close_error_modal
+} from "./test_utils";
+
+test.use({ tmpPath: 'nbgrader-create-assignments-test' });
+
+const nb_files = [
+    "data.txt",
+    "submitted-changed.ipynb",
+    "submitted-unchanged.ipynb",
+    "submitted-grade-cell-changed.ipynb",
+    "submitted-locked-cell-changed.ipynb",
+    "open_relative_file.ipynb",
+    "submitted-grade-cell-type-changed.ipynb",
+    "submitted-answer-cell-type-changed.ipynb"
+];
+
+/*
+ * copy notebook files before each test
+ */
+test.beforeEach(async ({ baseURL, tmpPath }) => {
+    const contents = galata.newContentsHelper(baseURL);
+    nb_files.forEach(elem => {
+         contents.uploadFile(
+            path.resolve(
+                __dirname,
+                `../files/${elem}`
+            ),
+            `${tmpPath}/${elem}`
+        );
+    });
+});
+
+/*
+ * delete temp directory at the end of test
+ */
+test.afterAll(async ({ baseURL, tmpPath }) => {
+    const contents = galata.newContentsHelper(baseURL);
+    await contents.deleteDirectory(tmpPath);
+
+    if (contents.fileExists("nbgrader_config.py")) contents.deleteFile("nbgrader_config.py");
+    contents.uploadFile(path.resolve(__dirname, "../files/nbgrader_config.py"), "nbgrader_config.py");
+});
+
+
+/*
+ * Open a notebook file and wait for validate button
+ */
+const open_notebook = async (page:IJupyterLabPageFixture, notebook:string) => {
+
+    var filename = notebook + '.ipynb';
+    var tab_count = await page.locator("#jp-main-dock-panel .lm-TabBar-tab.p-TabBar-tab").count();
+    await page.locator(`#filebrowser .jp-DirListing-content .jp-DirListing-itemText span:text-is('${filename}')`).dblclick();
+    await expect(page.locator("#jp-main-dock-panel .lm-TabBar-tab.p-TabBar-tab")).toHaveCount(tab_count + 1);
+    await page.waitForSelector(".jp-Notebook-cell");
+
+    await page.waitForSelector("button.validate-button")
+}
+
+/*
+ * Test validation success
+ */
+test('Validation success', async ({
+    page
+  }) => {
+
+    // open the notebook
+    await open_notebook(page, "submitted-changed");
+
+    // click on validate, and expect a success modal
+    await page.locator('button.validate-button').click();
+    await wait_for_success_modal(page);
+
+    // close the modal
+    await close_success_modal(page);
+
+});
+
+/*
+ * Test validation failure
+ */
+test('Validation failure', async ({
+    page
+  }) => {
+
+    // open the notebook
+    await open_notebook(page, "submitted-unchanged");
+
+    // click on validate, and expect an error modal
+    await page.locator('button.validate-button').click();
+    await wait_for_error_modal(page);
+
+    await page.waitForSelector('.nbgrader-ErrorDialog .validation-failed');
+
+    // close the modal
+    await close_error_modal(page);
+
+});
+
+
+/*
+ * Test validation with grade cell changed
+ */
+test('Validation grade cell changed', async ({
+    page
+  }) => {
+
+    // open the notebook
+    await open_notebook(page, "submitted-grade-cell-changed");
+
+    // click on validate, and expect an error modal
+    await page.locator('button.validate-button').click();
+    await wait_for_error_modal(page);
+
+    await page.waitForSelector('.nbgrader-ErrorDialog .validation-changed');
+
+    // close the modal
+    await close_error_modal(page);
+
+});
+
+/*
+ * Test validation with locked cell changed
+ */
+test('Validation locked cell changed', async ({
+    page
+  }) => {
+
+    // open the notebook
+    await open_notebook(page, "submitted-locked-cell-changed");
+
+    // click on validate, and expect an error modal
+    await page.locator('button.validate-button').click();
+    await wait_for_error_modal(page);
+
+    await page.waitForSelector('.nbgrader-ErrorDialog .validation-changed');
+
+    // close the modal
+    await close_error_modal(page);
+
+});
+
+/*
+ * Test validation opening relative file
+ */
+test('Validation open relative file', async ({
+    page
+  }) => {
+
+    // open the notebook
+    await open_notebook(page, "open_relative_file");
+
+    // click on validate, and expect a success modal
+    await page.locator('button.validate-button').click();
+    await wait_for_success_modal(page);
+
+    await page.waitForSelector('.nbgrader-SuccessDialog .validation-success');
+
+    // close the modal
+    await close_success_modal(page);
+
+});
+
+/*
+ * Test validation with grade cell type changed
+ */
+test('Validation grade cell type changed', async ({
+    page
+  }) => {
+
+    // open the notebook
+    await open_notebook(page, "submitted-grade-cell-type-changed");
+
+    // click on validate, and expect an error modal
+    await page.locator('button.validate-button').click();
+    await wait_for_error_modal(page);
+
+    await page.waitForSelector('.nbgrader-ErrorDialog .validation-type-changed');
+
+    // close the modal
+    await close_error_modal(page);
+
+});
+
+/*
+* Test validation with answer cell type changed
+*/
+test('Validation answer cell type changed', async ({
+   page
+ }) => {
+
+   // open the notebook
+   await open_notebook(page, "submitted-answer-cell-type-changed");
+
+   // click on validate, and expect an error modal
+   await page.locator('button.validate-button').click();
+   await wait_for_error_modal(page);
+
+   await page.waitForSelector('.nbgrader-ErrorDialog .validation-type-changed');
+
+   // close the modal
+   await close_error_modal(page);
+
+});
diff --git a/nbgrader/tests/nbextensions/test_assignment_list.py b/nbgrader/tests/nbextensions/test_assignment_list.py
index 3a7a1cb17..d4caf596c 100644
--- a/nbgrader/tests/nbextensions/test_assignment_list.py
+++ b/nbgrader/tests/nbextensions/test_assignment_list.py
@@ -20,6 +20,7 @@
 from ...utils import rmtree
 
 
+
 @pytest.fixture(scope="module")
 def nbserver(request, port, tempdir, jupyter_config_dir, jupyter_data_dir, exchange, cache):
     server = _make_nbserver("", port, tempdir, jupyter_config_dir, jupyter_data_dir, exchange, cache)
diff --git a/pytest.ini b/pytest.ini
index bb9e3ff3c..fcc0e0066 100644
--- a/pytest.ini
+++ b/pytest.ini
@@ -1,4 +1,5 @@
 [pytest]
 addopts = --strict-markers
 markers =
-    nbextensions
\ No newline at end of file
+    nbextensions
+    labextensions